[WotCon] Track WoT Connectivity/Binding 1.1 and add projections (Section 12) - #4182
[WotCon] Track WoT Connectivity/Binding 1.1 and add projections (Section 12)#4182marcschier wants to merge 42 commits into
Conversation
Both specifications moved to 1.1-draft2 since the models in this repository were authored. Replace the two NodeSets with the generated artifacts the drafts publish, so the information model is byte-faithful to the specification rather than a hand-maintained approximation. For xRegistry the node graph is unchanged at 66 nodes with the same identifiers and method signatures; the version moves to 0.3.0, the categories become fine grained and Name becomes Mandatory on GroupType and ResourceType. For WoT Connectivity the version string becomes 1.1 with a publication date of 2026-07-31, three members are added at the end of the identifier block, and ThingId becomes Mandatory on ThingDescriptionFileType. The three added members are CatalogUri on ThingDescriptionGroupType and on ThingModelGroupType, and ModelId on ThingModelFileType. They take the three highest identifiers in the block, so appending them cannot renumber any existing member. Members that became Mandatory are materialized by the type itself, so the source generator no longer emits an optional-add helper for them. The projection code drops the three calls that created them by hand; the values were already assigned separately and are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Implement the fifteen model and platform vocabulary terms defined in the OPC UA WoT Binding 1.1-draft2 specification that previously had no representation in the converter: uav:browsePathAnchor (Section 5.1.4); uav:isComposite, uav:contains and uav:containedIn (Sections 6.1 and 6.3); uav:unitProperty, uav:scaleFactor and uav:decimalPlaces (Section 6.5); uav:semanticId, uav:metadata, uav:propertyConfiguration, uav:actionConfiguration and uav:eventConfiguration (Section 6.7); uav:includeInherited and uav:additionalProperties (Section 6.8); and uav:nameNamespace (Section 6.4). The terms are validated during WoT-to-NodeSet synthesis. Booleans, absolute IRIs, the non-zero scale factor, the non-negative integer decimal places, the RFC 6901 unit pointer and the containment references are range-checked, and malformed values raise the new WOT6006 through WOT6009 diagnostics instead of being silently accepted. The three opaque Section 6.7 configuration and metadata terms are deliberately never rejected, as the specification requires. uav:browsePathAnchor joins the portable-identity validation so a relative browse path anchors against a portable ExpandedNodeId rather than a session-local NodeId. None of these terms has a distinct readable NodeSet structure, so round-trip fidelity is preserved through the existing uav:nodes and WotJsonResidue mechanism exactly as the sibling mapping terms already do; a document carrying them survives a WoT to NodeSet to WoT conversion without loss. The defaults table in docs/WoTNodeSetConversion.md gains a row per term describing absent-versus-malformed behaviour, and a new subsection documents directionality and round-trip handling. Thirty-two NUnit tests cover the positive and negative path of every term together with round-trip preservation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
OPC 10101 v1.00 permitted two identifier forms that only make sense to the session that read the namespace table: the ns=<index> form in NodeId-valued terms, and a numeric namespace prefix in a browse name. Release 1.1 forbids both, because a document carrying them silently binds to the wrong namespace once the table is reordered, which is exactly what a persisted document must not do. The session-local NodeId form was previously accepted without comment. It is now reported as an error wherever a NodeId-valued term is read, which covers uav:id and every term that funnels through the same conversion. The numeric browse-name prefix was already detected but only warned about; it is now an error to match the release 1.1 validator. Documents authored against v1.00 still need to be readable while they are migrated, so AllowNonPortableIdentifiers downgrades both errors to warnings. The occurrences stay visible rather than being suppressed, and the values are then interpreted exactly as v1.00 defined them. The downgrade is applied once, where the diagnostics are collected, rather than threaded as a flag through the synthesis call chain. Test documents that used the numeric form incidentally now use the NamespaceUri-qualified form. The two tests that exercise the rules themselves assert the error, and two new tests cover the leniency path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
WoT Binding release 1.1 introduces the projection document: a Thing Description or Thing Model that declares, rather than defines, its affordances. It names source documents and states which of their affordances a view is assembled from, so a view carries references and annotations only and has nothing that can drift from its sources. This adds the model for that document and the rules a projection document has to satisfy. A projection is recognised by uav:projection in its @type. It shall declare uav:scenario as an absolute IRI, because a view whose purpose is not machine-readable cannot be told apart from a view built for something else. It shall declare a non-empty uav:projects manifest whose entries carry a unique source name, an href and a media type, and may pin a source revision with a sha-256 digest. Selection is modelled in its three forms: an enumerated tm:ref that is the only form able to annotate what it selects, whole-document selection, and predicate filters. The predicate set is closed to affordance kind, semantic identifier and type tokens, and a filter carrying any other key is rejected rather than ignored, so that a filter stays decidable by inspection and two implementations cannot disagree about what it selects. Every member of properties, actions and events shall carry tm:ref. A member without one is defining an affordance, which is the one thing a projection document must not do, so it is reported rather than silently treated as a definition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Records what the draft2 revision changed and how much of it the stack covers: the model metadata and required-model bump, the three added members and the three that became Mandatory, the removal of the group vocabulary, the two identifier forms that are now errors, and the projection document together with its materialization onto the View NodeClass. The identifier change is the only one that can break a document a consumer already has, so it also gets a migration entry that names both forbidden forms, what to write instead, and the option that keeps a v1.00 document readable while it is being rewritten. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Add WotProjectionResolver, which turns a projection document (a Thing Description or Thing Model that declares its affordances via tm:ref, uav:selectAll, or uav:select predicates rather than defining them) into a resolved view: an ordinary TD/TM that carries no uav:projection marker, so a consumer needs no projection support to use it. Resolution runs in four stages. Sources named by uav:projects are resolved depth-first through the existing IWotThingResolver, a source that is itself a projection is resolved recursively, cycles are rejected (ProjectionCycle), unretrievable sources report ProjectionSourceUnresolved, and a declared uav:sourceDigest is verified as SHA-256 over the retrieved bytes (ProjectionDigestMismatch). Selection honours one total order: by source position in uav:projects, enumerated members before bulk within a source, and source member order within a bulk selection; the first selection of a name wins and a later duplicate is dropped with ProjectionSelectionDropped. Bulk names keep the source name, upper-casing the first character after a declared uav:namePrefix. Enumerated selections merge the members written alongside tm:ref over the referenced definition, discarding tm:ref. Four independent carriage rules bring dependencies across. Forms: under source routing a relative href is absolutized against the source document's base (or its own URI when it declares none) and the view declares no base for carried forms; under projection routing the form is carried unchanged. Security: under source routing the transitive closure of a carried form's effective security is copied as <sourceName>_<scheme>, references inside copied schemes (a combo's allOf/oneOf) are rewritten to the copied names, the form gets an explicit security naming them, and a form with no effective security declares none. Anchors: a relative uav:browsePath also carries the source's effective uav:browsePathAnchor. Context: @context merges every source's prefix bindings and a prefix bound to two URIs is ProjectionContextConflict. Every resolved affordance carries uav:resolvedFrom: the tm:ref as written for an enumerated selection, or href#pointer for a bulk one. Section 12.7 organizing links (ua:Organizes with uav:refName) are parsed onto WotProjection.OrganizingLinks and carried through unchanged with an acyclicity check; OPC UA View materialization is intentionally left out. Tests cover all three selection forms, the total order and first-wins drop, annotation merge precedence, namePrefix upper-casing, cycle and digest detection, context conflict, both routing modes, the security closure and allOf rewrite, provenance for both selection kinds, and that the resolved view carries no uav:projection, using the spec's worked examples as fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The WoT Binding specification publishes twenty worked examples, two of which are a golden pair: a projection document and the resolved view the specification says it resolves to. Asserting against that pair checks the implementation against the specification's own expectation rather than against our reading of it, which is the only way to be confident about rules whose effect is easy to describe and easy to get subtly wrong. Embeds the examples as fixtures and resolves the predictive-maintenance projection with a resolver that serves them by relative reference. The resolved view is required to name exactly the affordances the published view names, which together exercise all three selection forms: an enumerated tm:ref, a predicate over property affordances, and a whole-document selection whose names carry a prefix. It is also required to carry provenance for every affordance, to apply the prefix with the source name's first character upper-cased, to define every security scheme the published view defines, and to carry no projection marker of its own. Also renames the manifest source type to WotProjectionManifestSource. The materialization layer already has a WotProjectionSource that means something different - a NodeSet source rather than a projected document - and the specification is careful to keep the two senses of "projection" apart, so the API should not force a reader to work out which one is meant from the namespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
WoT Binding 1.1-draft2 Section 12 and WoT Connectivity 1.1-draft2 Section 7.13 introduce the "projection document": a Thing Description or Thing Model carrying a uav:projection that selects (never defines) affordances from other registry documents. The address-space side of a projection is an OPC UA View NodeClass that Organizes the Nodes already materialized from its sources and creates no affordance Node of its own. View is a NodeClass, not an ObjectType, so it cannot be modelled in the NodeSet and is constructed by the materializer. This adds that materialization to Opc.Ua.WotCon.Server: - WotProjectionViewBuilder resolves a projection document through the existing Opc.Ua.Wot.WotProjectionResolver (so organizing acyclicity and the no-absorb rule are reused, not reimplemented) into a WotViewProjectionPlan: the ordered already-materialized source NodeIds the View Organizes, nested organizational groups for ua:Organizes links (only the outermost becomes a View, inner ones organizational Objects), a ViewVersion, the MaterializedNodeCount (View plus organizational Objects only, never the organized Nodes), and the omitted selections. - IWotMaterializedNodeIndex locates the NodeId already materialized for a selected affordance from the closure per-source root bookkeeping: it maps the affordance uav:resolvedFrom source href to the source resource, then prefers the affordance authored uav:id and falls back to the source root browse path. A selection whose source is not in this address space (e.g. a cross-server federation source) resolves to NodeId.Null and is omitted and reported, not failed - the resource still reaches LoadState = Active so federation is not broken. - IWotViewProjectionHost is the coordinator-to-address-space seam that applies/removes a plan; InMemoryWotViewProjectionHost is the injectable default and test double. The coordinator defers projection members past their sources (they already sort last via the uav:projects dependency edges), builds a View per projection, sets RootNodeId to the View NodeId and the HasWoTProjection/WoTProjectionOf reference endpoints, and reports omissions in WoTResourceLoadResultDataType.Message. - WotDependencyGraph records uav:projects sources as registry dependencies (WoTDependencyDataType edges, open-string RefType) and treats ua:Organizes links as edges, so a cyclic projection graph is rejected at Phase = DependencyResolution like any other dependency cycle. ViewVersion is an FNV-1a 32-bit hash over the ordinal-sorted resolved membership (source NodeIds plus nested group RefNames and their members, depth-first). It is deterministic across TFMs, changes when the resolved membership changes, and is stable across an unchanged refresh, so a client can treat a ViewVersion change as "membership changed". A type-level projection over a Thing Model also materializes as a View: View is the only select-don't-define NodeClass and Section 12.6 does not restrict projection to instances; the builder treats TD and TM identically and carries the distinction on the plan DocumentKind. Naming keeps the spec separation between "projection" (the address-space mapping) and the pre-existing Materialization.WotProjectionDocument / WotProjectionSource (a WoT closure to a RuntimeNodeSet); the new spec sense lives on the Wot* view types and is documented as such. Tests (NUnit): the View is created and Organizes the source Nodes; no affordance Node is created; MaterializedNodeCount counts only View plus organizational Objects; RootNodeId and the HasWoTProjection endpoints are wired; an out-of-address-space source is omitted, reported and still Active; organizing links produce Objects with only the outermost a View; an organizing cycle and a cyclic projection graph are rejected (the latter at DependencyResolution); ViewVersion changes on a membership change and is stable across an unchanged refresh. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
xRegistry 0.3.0 specifies how a GroupId or ResourceId is derived from the entity's source identity: the domain-defined string naming what the entity is, such as a namespace URI, an asset identifier or a Thing identifier. The result reads like a reverse-DNS symbol and uses an alphabet chosen so that one identifier is simultaneously safe in a URL, on a command line and as a file name. The construction reverses the authority labels, appends the port as a further label, percent-decodes and normalizes the path segments, and joins what survives. A URN is split on its colons rather than treated as a bare path, so its leading label survives and a URN can never alias a path that happens to normalize the same way. An identity from which no label survives becomes the single-underscore identifier. An identifier is never derived from a document, or from a digest of one. A resource is a stable umbrella over its versions, so its identifier has to stay invariant while the document changes from version to version; a content fingerprint identifies bytes and belongs to a version. The disambiguator appended on truncation or on a case-insensitive collision with a sibling is therefore taken over the exact source identity, so it does not move when a new version is written. Truncation drops trailing labels but never the first, which carries the reverse-DNS root a reader recognises. The construction is lossy and one-way by design. A consumer holding a source identity computes the identifier and confirms it against the entity's source-identity Property; a consumer holding only an identifier matches that Property within the collection. Nothing should try to invert it. All seven identifier examples the specification publishes are covered as test cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Specification pull request 8 revises WoT Connectivity to draft3. The node graph is unchanged - the same 286 nodes with the same identifiers, modelling rules and definitions - so the revision costs no generated-code change. What changed is what the model says, and two of those statements had to be reflected here. The 1.02 surface stops being deprecated. The ReleaseStatus marking is removed from all 96 attributes that carried it, because deprecating the flat asset surface said something the working group does not mean: serving a WoT asset that way is legitimate and revision 1.1 neither removes it nor discourages it. Nothing asserted the deprecation in code, but the package descriptions and the documentation did, and they now say superseded in capability but not deprecated. The 1.02 surface also carries its security obligation directly rather than by reference. It previously inherited role-based access control only through the optional registry backing, so a server implementing only that surface inherited nothing at all. The specification now names the WoTFile Write and CloseAndUpdate operations alongside the management Methods, and both were ungated here: the upload path reaches the same materializer as CreateAsset but was not consulting the access policy. Both handlers now enforce it, and the enforcement is injected rather than reached for, so a file manager constructed without a policy keeps working. A test drives the Write handler with an unauthorised context rather than only asserting that the policy covers the operation name, because a policy that covers a name proves nothing if nothing calls it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Close the last seam in the WoT projection-to-View feature: turn a WotViewProjectionPlan into real, browsable Nodes in a running server instead of only echoing a deterministic NodeId. Add WotProjectionViewNodeManager, an AsyncCustomNodeManager on the WoT-Con namespace that materializes a plan as one runtime-created View NodeClass (ViewState). The View Organizes each already-materialized member Node with forward-only references, so the organized Nodes (owned by other NodeManagers) are never modified and the View owns none of them. Each WotOrganizationalGroup becomes an organizational FolderState Object that the View Organizes, with its own members and nested groups beneath it; only the outermost materialization is a View. The standard ViewVersion is exposed as a HasProperty UInt32 Property set from the plan. No affordance Node is ever created - a projection selects, it never defines. The View is made discoverable by adding the inverse Organizes edge to the Views folder in-manager and the complementary forward edge on the (core-owned) Views folder at runtime through Server.NodeManager.AddReferencesAsync; DeleteNodeAsync tears both the View, its Objects, and the cross-manager forward edge back down while leaving the organized Nodes intact. Add LifecycleWotViewProjectionHost, the production IWotViewProjectionHost that lazily publishes one shared view NodeManager through the public INodeManagerLifecycle and delegates apply/remove to it. Because the coordinator refreshes a document by applying the new View before removing the old one under the same deterministic NodeId, a single manager replaces the View in place and removal is identity-checked against the handle returned by the matching apply, so a superseding refresh is never undone by the trailing remove of the generation it replaced. Wire the live host into DI as the default IWotViewProjectionHost (via TryAddSingleton so a deployment can override it) and pass it to the materialization coordinator, which still falls back to InMemoryWotViewProjectionHost when constructed directly without a host. Cover the behaviour end to end with WotViewProjectionLiveTests, which drives the host against a real ReferenceServer and asserts over the server-side Browse/Read call chain: a browsable View NodeClass, exact Organizes membership, no affordance Node, nested groups as Objects with only the outermost a View, ViewVersion set from and changing with the plan, clean teardown that leaves organized Nodes intact, and the stale-handle superseding-refresh guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The specification defines ten conformance units and four profiles, and until now nothing in this repository said which of them the implementation meets. A reader had to infer it from the code. States the position for each unit and names where it lives, notes that all four profiles follow from those units, and describes how the claim is checked: the specification publishes a projection document and the resolved view it is defined to resolve to, and the test suite runs the pair, so the claim rests on the specification's own expected output rather than on our reading of the prose. Also records the two points the specification deliberately leaves open - how ViewVersion is recomputed, and whether a projection over Thing Models becomes a View - together with the choice made for each, and the one compatibility switch that departs from release 1.1 behaviour only when a caller asks for it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Three things the specifications require or forbid that the implementation did not yet get right. WOTC-Legacy requires an uploaded document to be format validated before any node is materialized from it, and one that fails to materialize nothing and return Bad_DecodingError. The upload path only deserialized the bytes into the Thing Description shape, which accepts JSON that is well formed but is not a WoT document at all - every Thing Description member is simply absent, so materialization proceeded against an empty document. It now parses the upload as a WoT document first, which is the same check the registry path applies. Two vocabulary terms were invented rather than taken from the specifications. Neither appears anywhere in either document. uav:NodeSet2Preservation was declared and never used, so it is removed. uav:eventFields is read when compiling an OPC UA event form, and was described as being a term of the official binding, which it is not: release 1.1 Section 8 carries an event's fields in the event data schema and its delivery configuration in uav:eventConfiguration. It keeps working so a form already authored against this implementation is not broken, but it is now identified as a non-standard extension wherever it appears rather than attributed to a specification that does not define it. The prohibition on dereferencing a URI found in a document, which draft3 extends to the 1.02 upload path, is already satisfied by construction: the assembly contains no HTTP client, and every resolution goes through an injected resolver that reads the registry snapshot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The clause 12.6 text pushed to the spec draft makes ViewVersion a function of the resolved membership alone, taken in a canonical order, so it records what a View contains rather than how it is arranged. The builder already hashed a canonically sorted membership, but nothing held it to that: the existing tests covered stability across an unchanged build and change on a changed membership, both of which a hash over the resolved order would also satisfy. So the rule was unenforced in exactly the direction the specification had been silent on. This adds a source declaring the same two affordances in the opposite order and asserts the version does not move. Removing the ordinal sort from AppendMembership turns it red, which is the point of adding it. The doc comment on the property now states the rule the specification settled on, including the two consequences a caller has to know: reordering alone does not change it, and it is not monotonic, so it is compared for inequality only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…pecification The conformance section listed ViewVersion recomputation and the materialization of a Thing Model projection as points the specification left open and that this implementation had decided for itself. Both were carried upstream and are now normative in WoT Binding 12.6 and WoT Connectivity 7.13, so describing them as our own decisions is out of date and would mislead anyone checking conformance. The wording adopted matches what was implemented, so nothing here changes. What the entries now record is the requirement rather than the choice, including the property the clause added that this code was never held to before - that a reordering which selects the same members leaves the version alone - and the test that holds it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Patch coverage on this branch was 78.2%. The gap was not spread evenly: it sat almost entirely on validation and locating code, which is exactly the input-handling surface a projection document reaches. Four of the five defects below were in lines no test executed.
ViewVersion could collide. AppendMembership joined member NodeIds with ';' and wrapped groups as RefName + '{...}' with no escaping, and a NodeId string identifier is authored input that may contain any of those. The two members ns=2;s=A and ns=2;s=B therefore serialized identically to the single member whose identifier is A;ns=2;s=B, and the two memberships shared a version, so a client watching for a membership change would never see one. Tokens are now length-prefixed, which makes the encoding injective.
Group ordering was also not total. Groups were sorted on uav:refName alone, which is optional and defaults to empty, and List<T>.Sort is not stable - so two groups that tie on the name could come out in the authored order and the same membership could hash two ways. Ordering now falls back to each group's own canonical serialization.
Locate trusted an authored uav:id. It returned the id unchecked, and MergeAnnotation copies uav:id from the projection document onto the resolved affordance, so a projection author chose the NodeId their View organized - any node in the address space, including one that does not exist. It is now required to be the source root or beneath it, and an out-of-bounds id falls back to the derived name rather than being honoured.
The WoTFile access gate had a hole I left. The gate added earlier in this branch covers Write and CloseAndUpdate, but Open takes the single writer slot: an unauthorized caller could open for write and lock out every legitimate writer without ever calling a gated Method. Open in write mode is now gated too.
The xRegistry disambiguator could overflow MaxLength. FromSourceIdentity returns up to 128 characters and the collision path then appended nine more, so a colliding sibling could reach 137 - Truncate reserves room for exactly this and the collision path did not use it. These identifiers are NodeId components and file names, so the overflow is not benign.
Tests: two new files cover projection document validation and node locating, and both fixes to the version hash have a test that fails when the fix is reverted. Patch coverage is now 84.7%.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Patch coverage and code reviewPatch coverage: 78.21% → 84.72% (2561 / 3023 changed lines)Measured Codecov-style — only lines this PR adds or changes, with
The gap was not spread evenly — it sat almost entirely on validation and locating code, which is precisely the surface a client-supplied projection document reaches. Two new test files ( Five defects found and fixedA review of the diff found eight issues; five are fixed in
Both Three findings left open, deliberatelyRecorded rather than silently dropped:
These are worth fixing before this leaves draft, but they are larger than the coverage pass and are better reviewed on their own. ValidationAll four suites green on net10.0 and net48: Types 8548 / 8541, WotCon 1048, Bindings 559, XRegistry 196. All changed projects build 0 warnings / 0 errors across every TFM under |
There was a problem hiding this comment.
Pull request overview
Updates the OPC UA WoT Connectivity/Binding implementation to track newer draft revisions (Connectivity 1.1-draft3 / Binding 1.1-draft2), adds the projection/view feature set (Binding §12) including server materialization as OPC UA View nodes, and tightens conformance checks (portable identifiers, access gating, and spec-example driven validation).
Changes:
- Add projection document model + resolver, and server-side materialization of projections as OPC UA Views (with dependency tracking and node lookup seams).
- Enforce draft2/3 conformance deltas: portable identifier rules (with a leniency switch), xRegistry reverse-authority identifier behavior, and updated model metadata/docs.
- Expand automated verification with new unit tests and embedded spec example fixtures.
Reviewed changes
Copilot reviewed 67 out of 68 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Opc.Ua.XRegistry.Tests/XRegistryIdentifierTests.cs | Adds coverage for xRegistry reverse-authority identifier construction and collisions. |
| tests/Opc.Ua.WotCon.Tests/WotManagementAccessPolicyTests.cs | Extends management access policy tests to include upload operations. |
| tests/Opc.Ua.WotCon.Tests/WotAssetFileManagerTests.cs | Adds regression coverage for upload validation behavior. |
| tests/Opc.Ua.WotCon.Tests/Materialization/WotProjectionViewCoordinatorTests.cs | Adds coordinator tests for projection→View materialization and lifecycle behavior. |
| tests/Opc.Ua.WotCon.Tests/Materialization/WotMaterializedNodeIndexTests.cs | Adds tests for safely locating already-materialized nodes for projection membership. |
| tests/Opc.Ua.Types.Tests/Wot/WotSynthesisTests.cs | Updates expected synthesized browse name forms to URI-qualified format. |
| tests/Opc.Ua.Types.Tests/Wot/WotSpecExampleTests.cs | Adds conformance tests that execute the spec’s worked examples (incl. projection golden pair). |
| tests/Opc.Ua.Types.Tests/Wot/WotNativeFirstRoundtripTests.cs | Updates expected roundtrip strings for synthesized browse names. |
| tests/Opc.Ua.Types.Tests/Wot/WotBindingReviewTests.cs | Updates binding review tests for portable identifiers + adds leniency-path assertions. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/01-opcua-td-pump.jsonld | Adds spec fixture TD example. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/02-thing-model-pump.jsonld | Adds spec fixture TM example. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/03-nodeset-preservation-envelope.jsonld | Adds spec fixture for nodeset preservation envelope. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/04-type-reference-modelling-rule.jsonld | Adds spec fixture for modelling rules/refs. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/05-native-node-model.jsonld | Adds spec fixture for structured node model projection. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/06-anchored-paths-and-device-identity.jsonld | Adds spec fixture for anchored browse paths/identity. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/07-projection-predictive-maintenance.jsonld | Adds spec fixture projection document (golden input). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/08-projection-resolved.jsonld | Adds spec fixture resolved view output (golden expected). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/09-asset-instance.jsonld | Adds projection-authored asset instance fixture. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/10-group-process-data.jsonld | Adds projection group fixture (process data). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/11-group-condition-data.jsonld | Adds projection group fixture (condition data). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/12-group-alarms.jsonld | Adds projection group fixture (alarms). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/13-group-management.jsonld | Adds projection group fixture (management). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/14-asset-instance-resolved.jsonld | Adds resolved asset instance fixture. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/15-asset-model.jsonld | Adds projection-authored asset model fixture. |
| tests/Opc.Ua.Types.Tests/Wot/Assets/16-model-group-process-data.jsonld | Adds model-level projection group fixture (process data). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/17-model-group-condition-data.jsonld | Adds model-level projection group fixture (condition data). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/18-model-group-alarms.jsonld | Adds model-level projection group fixture (alarms). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/19-model-group-management.jsonld | Adds model-level projection group fixture (management). |
| tests/Opc.Ua.Types.Tests/Wot/Assets/20-asset-model-resolved.jsonld | Adds resolved asset model fixture. |
| tests/Opc.Ua.Types.Tests/Opc.Ua.Types.Tests.csproj | Embeds WoT spec example JSON-LD fixtures as resources. |
| src/Opc.Ua.XRegistry/Opc.Ua.XRegistry.NodeSet2.xml | Updates xRegistry model metadata and categorization per updated draft. |
| src/Opc.Ua.WotCon/Opc.Ua.WotCon.csproj | Updates package description to reflect non-deprecated 1.02 surface. |
| src/Opc.Ua.WotCon/Design/Opc.Ua.WotCon.NodeSet2.csv | Appends new/updated NodeSet members introduced by draft changes. |
| src/Opc.Ua.WotCon.Server/WotRegistryProjection.cs | Aligns optional-vs-mandatory node materialization helpers with updated model. |
| src/Opc.Ua.WotCon.Server/Opc.Ua.WotCon.Server.csproj | Updates package description text to remove “deprecated” wording. |
| src/Opc.Ua.WotCon.Server/Materialization/WotViewProjectionPlan.cs | Adds immutable plan/result types describing projection→View membership/materialization. |
| src/Opc.Ua.WotCon.Server/Materialization/WotProjectionViewNodeManager.cs | Adds NodeManager that owns runtime-created projection View nodes and groups. |
| src/Opc.Ua.WotCon.Server/Materialization/WotDependencyGraph.cs | Extends dependency discovery to include projection manifests and organizes links. |
| src/Opc.Ua.WotCon.Server/Materialization/LifecycleWotViewProjectionHost.cs | Adds production host that publishes projection Views via NodeManager lifecycle. |
| src/Opc.Ua.WotCon.Server/Materialization/IWotViewProjectionHost.cs | Adds host seam + in-memory host for tests/non-DI scenarios. |
| src/Opc.Ua.WotCon.Server/Materialization/IWotMaterializedNodeIndex.cs | Adds seam + implementation for locating already-materialized nodes from selections. |
| src/Opc.Ua.WotCon.Server/Hosting/OpcUaWotRegistryServerBuilderExtensions.cs | Wires projection view host into DI and coordinator construction. |
| src/Opc.Ua.WotCon.Server/EventIds.cs | Adds event-id allocation for projection-view NodeManager logs. |
| src/Opc.Ua.WotCon.Server/Assets/WotAssetFileManager.cs | Adds access enforcement for upload path + adds “format validation” pre-check. |
| src/Opc.Ua.WotCon.Server/Assets/AssetRegistry.cs | Wires management access enforcement into the asset file manager. |
| src/Opc.Ua.WotCon.Bindings/Planners/WotBindingSources.cs | Documents uav:eventFields as a non-standard extension. |
| src/Opc.Ua.WotCon.Bindings/Planners/OpcUaBindingPlanner.cs | Documents and retains compatibility behavior for non-standard uav:eventFields. |
| src/Opc.Ua.Types/Wot/WotVocabulary.cs | Adds constants for projection/view vocabulary terms. |
| src/Opc.Ua.Types/Wot/WotProjectionModel.cs | Introduces projection domain model types (manifest, filters, organizing links). |
| src/Opc.Ua.Types/Wot/WotNodeSetConverterOptions.cs | Adds AllowNonPortableIdentifiers option with spec-aligned documentation. |
| src/Opc.Ua.Types/Wot/WotNodeSetConverter.WoT.cs | Enforces portable identifiers; applies optional leniency by downgrading errors to warnings. |
| src/Opc.Ua.Types/Wot/WotDiagnostics.cs | Adds diagnostic codes for model vocabulary + projection validation scenarios. |
| docs/WoTNodeSetConversion.md | Documents model vocabulary handling and portable-identifier behaviors. |
| docs/WoTConnectivity.md | Updates documentation to draft3/draft2 revision alignment and projection behavior. |
| docs/WotBindings.md | Adds binding conformance matrix and explains spec-example validation approach. |
| docs/MigrationGuide.md | Adds migration guidance for the tightened portable-identifier rules. |
OPC 10000-100 clause 5.5 Table 48 defines ConnectsTo as a subtype of NonHierarchicalReferences, and the prose is explicit about why: "It is NonHierarchical and symmetric, because this is natural for this Reference." The WoT aggregation server was materializing it as a subtype of HierarchicalReferences, so a client browsing the model would have walked a topological connection as if it were containment. This was not a corruption of our copy. The official DI NodeSet carried HasSubtype i=33 through DI 1.03 and 1.04 and the OPC Foundation corrected it to i=32 in DI 1.05.0, published 2025-11-15. Our checked-in resource was a faithful DI 1.04.0 file, and Opc.Ua.Di.tm.json is generated from it, so the sample inherited the upstream error. The repository was already inconsistent about this: src/Opc.Ua.Di/Design/OpcUaDiModel.xml targets DI 1.05.0 and has always said NonHierarchicalReferences, which is why the pump device integration server was right and the aggregation server was wrong. The comparison of those two servers is what surfaced it. So the resource is replaced with the official DI 1.05.0 NodeSet rather than hand-edited. Patching the one reference would have left a file that claims to be an official artifact but is not, and the next refresh from upstream would silently reintroduce the defect. The replacement is byte-identical to the published file. Upgrading also closes a second gap the same comparison found. The aggregation server was missing six DI types the pump server has - CanUpdate, IAssetLocationIndicationType, LocationIndicationType, SoftwareClass, SoftwareFolderType and UpdateParent - because they are DI 1.05 additions absent from 1.04. The two servers now agree on the whole DI namespace. A regression test asserts ConnectsTo is a subtype of i=32 and is symmetric, checking both the source NodeSet and the generated Thing Model, so a future refresh from an older upstream revision fails instead of quietly regressing. Reverting the supertype turns it red. Verified live: the running aggregation server now reports ConnectsTo as a forward HasSubtype child of NonHierarchicalReferences. Source generation is unaffected - Opc.Ua.SourceGeneration.Core.Tests 3771 and Opc.Ua.SourceGeneration.Tests 127 pass unchanged - as are Opc.Ua.WotCon.Tests 1049, Opc.Ua.Types.Tests 8548 and Opc.Ua.WotCon.Bindings.Tests 559, on net10.0 and net48. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The document set section listed the four checked-in documents without saying where they come from or that the DI version is load-bearing. It now says both: each Thing Model is generated from a checked-in NodeSet2 and a test asserts the checked-in file is byte-identical to that output, and Opc.Ua.Di.tm.json specifically comes from DI 1.05.0 because every earlier official DI NodeSet declares ConnectsTo as hierarchical, contradicting OPC 10000-100 clause 5.5. Without that note the next person to refresh the NodeSet has no reason to suspect an older revision would reintroduce a non-compliant model, and the test that now stops them would look arbitrary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Comparing the WoT aggregation server against the hand-written pump device integration server - two independent routes to the same OPC 40223 Pump - turned up one node where they disagreed. Pump1.Identification was materialized as DI FunctionalGroupType where the native server has Pumps PumpIdentificationType. The cause is in the sample, not the stack. SamplePump.NodeSet2.xml declares its own namespace table as ns=1 instance, ns=2 Pumps, ns=3 Machinery, ns=4 DI, and the instance carried HasTypeDefinition ns=4;i=1005 - DI FunctionalGroupType - where OPC 40223 declares PumpType.Identification as PumpIdentificationType, ns=2;i=1005. The materializer reproduced exactly what it was asked for. Auditing all thirty-five HasTypeDefinition entries in that file, this was the only wrong one. Narrowing to the specified type is safe because PumpIdentificationType derives from Machinery's MachineryItemIdentificationType and ultimately from DI's FunctionalGroupType, so the three DI identification properties the sample publishes remain available by inheritance. The native server demonstrates this: its PumpIdentificationType instance exposes Manufacturer, SerialNumber and ProductInstanceUri. The BrowseName is corrected in the same pass. It was "Pump #1", and both a space and a # are RelativePath escaping characters, so a client composing a browse path across this node would have had to escape them. The name is now Pump_1 and "Pump #1" stays as the DisplayName, which is what the native pump sample does and what PumpBrowseNamesAvoidRelativePathEscapingCharactersAsync requires of it. Three expectations had been written from the wrong output and are corrected with it: the s_pumpNodes table, the end-to-end type-definition assertion, and a README line that documented "Pump1.Identification uses its DI type definition" as if it were intended. That is the whole reason this survived - every check restated the same assumption. Comparing against an independently built server was the only thing that could catch it. Verified live: the running aggregation server now reports Identification as Pumps PumpIdentificationType and the Pump as Pump_1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…e address space MaterializeProjectionViewsAsync had no exception handling, unlike the NodeSet projection path a few lines above it, which catches, documents its rollback and returns per-member failures. The consequence was worse than an aborted refresh. Views applied earlier in the loop are held only in a local list that the caller publishes into ClosureState on the success path, so an exception part way through left those View Nodes in the running address space with no handle recorded anywhere. Nothing could then remove them - not a later refresh, not retirement - so each failed refresh leaked another set. The per-member body moves into its own method and the loop contains failures around it. A member that throws is now reported failed like any other and the loop carries on, so every handle applied before and after it is still published and remains reachable for the next refresh to retire. Two WotDocument.Parse calls in WotProjectionResolver were also unguarded while a third, structurally identical one already caught FormatException and JsonException. Both are now guarded the same way. The one that resolves the assembled view is the reason this matters: that document is the merge of N sources, so it can exceed a size limit that none of the sources broke individually, and the exception escaped from a method whose contract documents only ArgumentNullException. No behaviour changes on the success path. Opc.Ua.WotCon.Tests 1049, Opc.Ua.Types.Tests 8548 and Opc.Ua.WotCon.Bindings.Tests 559 all pass unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The WoT aggregation server and the pump device integration server build the same OPC 40223 Pump by completely different routes: one runtime-loads DI, Machinery and Pumps Thing Models with no pump-specific code, the other is generated from the same companion NodeSets and wired by hand. Nothing checked that they agree, and the existing end-to-end test could not: it asserts a hand-written list of expected names, which restates the assumptions the converter already makes. That is how Identification kept the wrong type definition through every test, the sample documents and a README line describing the defect as intended behaviour. The pump server is an independent oracle, so this compares against it directly. Both servers are hosted in-process on free ports and browsed over a session, exactly as the manual investigation did. Two tests, because the two questions fail for different reasons and should be diagnosable apart. The first checks that every node the WoT server materializes under its Pump also exists under the native Pump with the same BrowseName, NodeClass and type definition; nodes the native server has and the WoT model does not - alarms, OpenUSD, the fuller Identification, the rest of the simulation - are reported for information rather than failed, because the Thing Description deliberately models a subset. The second checks the DI, Machinery and Pumps type definitions are equal in both, since both derive from the same companion models and any difference there is a defect rather than a scope decision. Comparison is on namespace URIs and browse names throughout. NodeIds, namespace indexes, modelling rules and values legitimately differ between the two servers and are ignored; comparing them would produce nothing but false positives. Reverting Identification to the type definition it had before this branch turns the first test red, naming the node and both type definitions, while the second stays green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
A refresh deliberately applies the replacement View first and only then retires the handle it supersedes, so that a client is never left without one. Both handles carry the same resource Xid, and InMemoryWotViewProjectionHost removed by Xid alone - so the trailing remove deleted the entry that had just been created. After the first re-materialization its Applied collection no longer matched the handles the coordinator had published in ClosureState. This is not only a test double. It is the default the coordinator falls back to when no view host is injected, so the divergence is reachable in a running server. LifecycleWotViewProjectionHost already guards the same ordering by checking the tracked instance is the one the handle was issued for; the in-memory host now does the same, keeping the request and its handle together and retiring an entry only for the handle it was issued for. A test refreshes a projection twice and asserts exactly one View remains applied. Removing the identity check turns it red. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…mise WoT Connectivity 6.7 requires HasWoTProjection from a stored projection document resource to the View it materialized, so a client can navigate from the document to its View and, through the inverse WoTProjectionOf, back. The ReferenceType was in the NodeSet, the interface documented the obligation, and WotViewProjectionRequest.ResourceNodeId existed for no other purpose than to carry the endpoint - but nothing ever read it. The reference was never created, so the navigation the documentation describes did not work. The View now carries the inverse edge and the forward edge is added through the master, because the resource Node belongs to the registry NodeManager rather than to this one. That mirrors how the Views folder Organizes edge is already handled, and it means the existing deletion path tears both down with the View instead of needing its own cleanup. Worth recording for anyone extending this: a reference to a Node that does not exist is dropped when the View is imported. That is why the new test uses a real Node as the resource rather than the synthetic NodeId the other tests share - the first version of the test failed for that reason and not because the reference was missing. Removing the AddReference call turns the test red. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…erver The companion-model shape section listed what the materialized Pump should look like, but every item in it was derived from this sample's own documents, so it could only ever confirm the documents agree with themselves. It did not catch Identification carrying the wrong type definition, because the documents, the tests and this file all said the same wrong thing. The section now says that outright and points at the comparison against PumpDeviceIntegrationServer, which reaches the same OPC 40223 Pump from generated companion NodeSets and hand-written wiring - an independent oracle rather than a restatement. It records what the comparison asserts, that it compares on namespace URIs and browse names because NodeIds and namespace indexes legitimately differ, and the two differences that remain on purpose: the Pump has no hierarchical parent because adding one currently fails activation, and the Thing Description deliberately models a subset of the simulation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
A reference type written as a bare name is an alias, and a NodeSet that forgets to declare it in <Aliases> is an easy authoring mistake. The import looked the name up, missed, and handed it to NodeId.Parse, which reported IdentifierMissing - a message that names neither the value that failed nor the fact that an alias was expected. There is nothing in it to act on. That cost real time. Adding an Organizes reference to the WoT pump sample failed activation with exactly this message, and because the diagnostic pointed nowhere it read as a defect in the materializer; it took bisecting the change to find that the sample's alias table listed only HasTypeDefinition, HasProperty and HasComponent. The import now says which value could not be resolved and that an alias has to be declared. Only the failure path changes. The lookup, the success path and the exception type callers already handle are untouched, which matters because this file feeds the source generator: Opc.Ua.Types.Tests 8549, SourceGeneration.Core.Tests 3771 and SourceGeneration.Tests 127 all pass unchanged. One detail worth recording, because it is why the first attempt at this did not work: NodeId.Parse throws ArgumentException for a missing identifier and ServiceResultException for other failures. Catching only the latter silently misses the case this is about. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Uploaded-document validation now checks what it claimed to. Parsing as a WoT document rejects JSON that is not an object, but every member of the ThingDescription POCO is optional, so an empty object still deserialized happily and was materialized. The check now also requires a member that identifies the document. It accepts either name or title, because this surface keys an asset on name while W3C WoT TD 1.1 clause 5.3.1 makes title mandatory - requiring title alone rejected the legacy documents this surface exists to serve, which the existing tests caught. The test that was supposed to cover this used a JSON array, which never reached the new validation because it fails to deserialize anyway. It is now a case-driven test over an array, an empty object and an object carrying only optional members. Removing the check turns the two object cases red and leaves the array case green, which is exactly the gap the reviewer described. The management access test drove only Write although the fix also gates CloseAndUpdate. It now drives both and asserts each returns BadUserAccessDenied and records its enforcement; CloseAndUpdate enforces access before it validates the handle, so no valid handle is needed. Documentation: - The WoT identifier section is removed from the migration guide. Nothing migrates from 1.5.378 here - the feature is new in 2.0 - and the material it carried now lives with the option it documents. - The deliberate-deviations section is removed from the bindings guide. Both points it listed were taken into the specification and are no longer deviations. - The compatibility switch is explained properly instead of being a single sentence: what each rejected form looks like, the same document rewritten portably, and what the option reports while a corpus is being migrated. - Clause 12 of the connectivity guide is rewritten as a statement of what the model requires and what the implementation provides, rather than a history of what each draft changed. The revision note above it goes with it. One review comment is not applied and is answered on its thread: the ViewVersion property BrowseName is deliberately in namespace 0 because ViewVersion is a standard OPC UA property (i=12170), the same way NodeStateModelChangeExtensions creates NodeVersion. Opc.Ua.WotCon.Tests 1053 pass on net10.0 and net48; the server builds 0 warnings on all six target frameworks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Review of spec-drafts PR #8 rejected the ViewVersion wording I contributed, and it was right on all three counts. It required "a deterministic function ... in a canonical order" while naming neither, so two conforming servers could still disagree - which is the single property the requirement exists to guarantee. It ignored that OPC 10000-3 clause 5.4 requires ViewVersion to be greater than zero. And "changes when, and only when, the membership changes" is unsatisfiable for a 32-bit value: collisions break the "when" direction, so I had asserted a property no implementation can hold. WoT Binding clause 12.6 now specifies the function outright, which is what makes it conformance-testable, and this implements it exactly: each resolved member's ExpandedNodeId in the portable nsu= form of clause 5.1.1, sorted ascending by Unicode code point, joined each followed by U+000A, encoded as UTF-8, and the first four octets of the SHA-256 digest read as a big-endian UInt32, with zero reported as one. The previous implementation matched none of that - FNV-1a over length-prefixed session-local NodeId strings, hashing groups as well as members, and free to return zero. Only members are hashed now; the clause says ViewVersion records what a View contains and not how it is arranged, so group names and nesting no longer contribute. The builder takes the server namespace table because the portable form needs it. A test pins the result to a vector computed independently of this code rather than to whatever the code produces: two members serialize to "nsu=urn:test:pump;i=1001\nnsu=urn:test:pump;s=Alpha\n", whose SHA-256 begins 1C A7 69 28, so ViewVersion is 480733480. The test that asserted two different memberships never collide is replaced. The specification now states that a UInt32 cannot separate every membership and that collisions may occur, so that assertion no longer describes the contract. What replaces it records something sharper and is raised against the specification: the specified join uses U+000A and does not escape it, while an ExpandedNodeId string identifier may contain U+000A, so one member embedding a newline serializes byte-for-byte as the two members it imitates. That is a structural collision an author can construct, not the statistical one the clause knowingly accepts. Also from the same review round: - The NodeSet is re-adopted at PR #8 head. ModelVersion is typed SemanticVersionString, whose syntax OPC 10000-5 fixes as major.minor.patch, so the value moves from 1.1 to 1.1.0. Nothing else in the 203 KB file differs. - Clause 11 now requires the endpoint allowlist, trust policy and size limits that guard a caller-supplied AssetEndpoint to apply to the endpoints DiscoverAssets probes. ConnectionTest and CreateAssetForEndpoint already validated theirs; DiscoverAssets returned whatever the provider found, which handed a caller the result of a probe it could not have made itself. Discovered endpoints are now filtered through the same policy and the withheld count is logged. Checked and already conformant: DiscoverAssets is in the role-based access list; the channel floor is a configurable minimum applied to every management operation, so it is at least as strict as the clause requires of the mutating ones; and legacy data points already attach to the asset by HasWoTComponent. Opc.Ua.WotCon.Tests 1054 pass on net10.0 and net48; the server builds 0 warnings on all six target frameworks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
WoT Binding 12.6 originally joined the portable member identities with U+000A and escaped nothing, but a NodeId string identifier may itself contain U+000A. A single member embedding a newline therefore serialized byte-for-byte as the two members it imitates, so two different memberships shared a ViewVersion. That is a structural collision an author can construct deliberately, distinct from the statistical 32-bit collision the clause knowingly accepts, so the paragraph admitting collisions did not cover it. Raised and fixed in spec-drafts PR #8 (e86e833): each member is now written as its length in UTF-8 octets, a colon, the string, and U+000A. This realigns the implementation with that text, re-derives the independently computed conformance vector, and flips the collision test from characterizing the defect to asserting the guarantee. Both are mutation-verified: removing the length prefix fails exactly those two. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
WoT Connectivity 11 requires a Thing Description auto-generated from a caller-chosen endpoint to be treated as untrusted input, subject to the same WOTC-Legacy format validation an uploaded document gets, and 14 requires a document that fails to materialize nothing and to return Bad_DecodingError. CreateAssetForEndpoint applied the endpoint policy to the AssetEndpoint argument but then handed whatever the discovery provider returned straight to the materializer. Deserializing into ThingDescription is not that check: every member of that type is optional, so an empty object deserializes happily. Neither the endpoint, which the caller chose, nor the provider, which is pluggable, is a trusted source. The gate now requires the document to identify itself by a non-empty name or title, deletes the asset created to hold it when it does not, and returns Bad_DecodingError. The rule moves into ThingDescriptionFormatValidator so the upload path and this path share one definition rather than two that can drift. Both new tests are mutation-verified and independent: neutering the gate kills only the rejection test, inverting it kills both, which is what shows the second test is holding the gate to not over-rejecting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The registry snapshot carried every document's bytes inline, so every commit rewrote the whole corpus: editing one small Thing Description rewrote every other document's blob. Bytes now live in a content-addressed IXRegistryResourceStore and the snapshot carries only metadata - digest, length, content type - which also lets the documents live somewhere every node of a distributed deployment can reach. Writing bytes before the manifest that names them collided with the way the file store recognises trouble. A blobs/ directory with no manifest means a lost generation or a crashed commit, and the store fails closed rather than report an empty registry and discard data; that is asserted by AbsentPrimaryWithPendingArtifactFailsClosed and AbsentPrimaryWithPriorStateFailsClosedWithoutDataLoss. Writing straight into blobs/ would have made the first write on a fresh deployment look exactly like that, so those two tests failed and no edit to them would have been honest - they were protecting a real property. Writes therefore land in staging/, which carries no such meaning, and the commit promotes the entries its snapshot references into blobs/ as artifacts it owns before switching the manifest. Reads prefer blobs/ and fall back to staging so an uncommitted write is still readable by the transaction that made it. A staged entry that is never promoted is inert: nothing can reference a document until a manifest names it. Two things the refactor had lost are restored. Content verification had become a length comparison, which would have accepted a blob altered without changing its size and would not have noticed one that could not be read at all - so the tamper and locked-blob tests were failing for a real reason. Each referenced document is streamed and hashed again, but incrementally over chunks, so verifying never requires holding a document in memory. And structural validation now runs before any content check, so a malformed snapshot is reported for what is wrong with it rather than for content it was never entitled to reference. FileWotRegistryStore also gains IDisposable to own the store areas it creates, and any decorator around IWotRegistryStore must now forward IWotRegistryResourceStoreProvider - one that drops it leaves the service writing into a private in-memory store while the wrapped store validates against its own. MutatingOneResourceDoesNotRewriteAnotherResourceBytes pins the win and is mutation-verified: injecting a rewrite of an unrelated blob fails it. 1057 tests pass on net10.0 and net48, 0 warnings on all six TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
A NodeSet may reference a Node another NodeManager owns - an inverse Organizes placing an Object under a folder some other model defines - and OPC 10000-3 requires the Reference to be visible from both ends. At startup that works because the master collects every NodeManager's external references first and applies them all afterwards, so creation order does not matter. A NodeManager added after startup had no such second phase: its references were applied only at its own commit, and a reference to a Node that is not yet in the address space is dropped rather than queued. Registering a NodeSet before the one it points into therefore lost the forward edge for good, and the two ends disagreed - the target browsed to the source, the source did not list the target. The master now retains the startup references and each dynamic NodeManager's references and replays them to any NodeManager registered later. Each NodeManager applies only the entries whose source Node it owns, so the replay is a no-op for everything else. This is what blocked placing the WoT sample's aggregate pump under the DI DeviceSet: the two are materialized from different NodeSet documents, and an earlier attempt produced exactly this one-way reference and was reverted rather than ship a non-conformant address space. RuntimeNodeSetCrossSourceReferenceTests covers both orders and is a red-to-green pair on this change: target-first passed before the fix, source-first failed and now passes. 4026 server tests pass on net10.0, 0 warnings on all TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
CommitAddAsync records a NodeManager's external references and then replays the retained references of every other registration into it. That replay runs arbitrary NodeManager code, so it can throw, and the rollback that follows takes the NodeManager back out of the routing table - but the recorded references stayed behind. Before the replay was added nothing could throw after the references were recorded, so the invariant "not routed implies not recorded" held structurally; adding the replay broke it. The leftover entry is worse than a leak. PublishAsync treats presence in the table as "already registered", so the instance could never be registered again, and every later add replays the dead references into the NodeManager being added - the opposite of what the replay exists to do, and capable of installing an edge to a target that was rolled back. The rollback now removes the entry when the NodeManager is no longer visible in the routing table. Found by review of this PR. FailedCommitDoesNotRetainExternalReferences- ForARolledBackNodeManagerAsync forces the replay to fail and then probes PublishAsync, which reads the table directly; without the fix it fails with "The NodeManager is already registered". 4027 server tests pass on net10.0, 0 warnings on all TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Review feedback on docs/WoTConnectivity.md: "Rename WOTC-Legacy to Wot-Con 1.02". "Legacy" said nothing about the unit's content and read as a deprecation label for a surface this revision deliberately stops deprecating. The new name states what the unit incorporates: the published OPC 10100-1 v1.02 shape. It also matches how OPC Foundation NodeSets spell a Category value - "DI Information Model", "DI DeviceSet" - rather than the hyphenated WOTC- form the other units use. The unit name is normative, so it was renamed in the specification first (spec-drafts 9b9cfb1: specification text, NodeSet and CHANGELOG, validator 0 errors) and the NodeSet re-adopted here, which keeps it byte-identical to the spec as required. The NodeSet diff is Category values and nothing else - no NodeId, NodeClass, reference or requirement moves - so no generated code changes. Also updated the four references in docs/WoTConnectivity.md and the five code and test comments that cite the unit. 1057 WotCon tests pass on net10.0 and net48, 3771 source-generation tests pass, 0 warnings on all six TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Code coverage✅ Coverage gate passed.
Uncovered changed lines
Coverage is above the recorded baseline - consider ratcheting Thresholds live in |
The WoT sample had nothing that could originate an event. FlatTagServer set EventNotifier=None on every node and its "Events" folder held boolean variables named after supervision signals - Cavitation, MotorOverheat - so there was no OPC UA event or condition anywhere for an aggregating server to subscribe to, project or acknowledge. Each supervision signal is now one signal seen twice: the boolean tag a flat-tag client polls, and an AlarmConditionState a Part 9 client subscribes to, acknowledges and confirms. Writing the tag drives the condition, so the two cannot disagree, and the tag is writable so the sample and its tests can trip an alarm without waiting on a simulated process. The pump carries EventNotifier and is registered as a root notifier, because a condition only reaches a client through a notifier. The pump also gains the Methods an operator manages it with - Start, Stop and Reset - which are the members the asset's management group will project. Reset returns a tripped signal to normal, and Start/Stop drive a Running variable so they are observable rather than no-ops. Conditions support confirmation as well as acknowledgement because the aggregating server will propagate a client's acknowledgement back here; without ConfirmedState there would be nothing for that round trip to reach. Two things worth recording: The optional condition children have to be attached before Create runs. Create is what assigns their NodeIds and wires the Part 9 handlers, so attaching them afterwards left them unidentified and the whole address space failed to import - which surfaced as the aggregating server reporting BadNotConnected against a source that had silently failed to start. The tests share one source server for the fixture and retry the initial connect. Accepting a TCP connection only means the transport is listening; the server answers BadServerHalted until its state reaches Running, so connecting on the first accept races startup. A server per test also added enough churn to destabilise the other suites in the same run. Four tests cover the transition, the acknowledgement state, Reset and Start/Stop, and are mutation-verified: severing the tag-to-condition link fails three and correctly leaves Start/Stop passing. Samples suite 13/13 on net10.0, 0 warnings on all six TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Everything lived in one executable, so an application that wanted OPC UA
MCP tools alongside its own had no way to get them short of forking or
shelling out. The Part 4 tools now ship as a library any MCP host can
reference.
A host composes them with its own tools:
services.AddOpcUaMcpCore();
services.AddMcpServer()
.WithStdioServerTransport()
.WithOpcUaMcpFilters()
.WithOpcUaCoreTools(McpToolProfile.Services)
.WithTools<MyApplicationTools>();
Moved: the session/browse/read/write/method/subscription/monitored-item/
node-management/NodeSet-export/PKI tools, OpcUaSessionManager,
SessionResources, McpServerOptions, McpToolProfile, OpcUaJsonHelper and
the request/schema filters. The filters go with them because they make
tool errors actionable and schemas explicit, which an embedded server
wants as much as the shipped one; they are registered by their own
extension rather than by each tool set, since filters are server-wide and
registering them per package would run them repeatedly.
The executable keeps its package id, command name, manifest and default
profile, and now delegates to the library.
Three things worth recording:
A profile naming tools this package does not own - PubSub, Diagnostics -
contributes nothing rather than throwing, so a host can pass one profile
to every OPC UA tool package it references and get exactly what those
packages provide.
Enabling XML documentation on the new library surfaced two pre-existing
defects the executable had hidden: eight undocumented public members on
OpcUaSessionManager, and two stacked doc comments on one method in
NodeSetExportTools. Both are fixed.
The reflective MCP tests in Opc.Ua.Core.Diagnostics.Tests hard-coded a
single assembly path. They now probe every Opc.Ua.Mcp*.dll beside the
server, so they assert behaviour rather than encoding which assembly
currently owns a type - and stay correct as the remaining tools move.
Seven new tests cover the embedding contract using only what a
third-party host can reach; they are mutation-verified: making the Core
profile register nothing fails the composition test. 295 Opc.Ua.Tools
tests and 511 of 512 Opc.Ua.Core.Diagnostics tests pass, the exception
being a pre-existing locale-dependent assertion in
ServiceCallReassemblerTests that passes under invariant culture and is
untouched by this change. 0 warnings on all TFMs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The PubSub runtime tools were only reachable by running the opcua-mcp executable, so an application wanting them alongside its own MCP tools had no way to get them. Move PubSubRuntimeManager and the action, discovery and runtime tools into a new Opc.Ua.Mcp.PubSub package and expose AddOpcUaMcpPubSub and WithOpcUaPubSubTools so a host can compose them. A profile that does not select PubSub contributes nothing rather than failing, so one profile value can be passed to every OPC UA tool package a host references. The executable now delegates to those extensions; its tool catalogue, package id and command name are unchanged. Widen the MCP grading workflow path filter, which otherwise stops running when the tools it grades move out of tools/Opc.Ua.Mcp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The UA-TCP capture, decode and replay tools were only reachable by running the opcua-mcp executable, so an application wanting them alongside its own MCP tools had no way to get them. Move them into a new Opc.Ua.Mcp.Diagnostics package and expose AddOpcUaMcpDiagnostics and WithOpcUaDiagnosticsTools. The gate on the key-disclosing tools moves with them and still defaults to disabled, honouring both Pcap:EnableDiagnosticsTools and OPCUA_PCAP_ENABLE_DIAGNOSTICS. The path-traversal guard the capture tools apply to caller-supplied paths was reachable only from within one assembly, so the PubSub capture tools could no longer call it once the packages separated. Move it to Opc.Ua.Mcp.Core as McpCapturePath.ResolveAndValidate, which both diagnostics packages now share rather than restating a security check. The executable delegates to the new extensions; its tool catalogue, package id and command name are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…tion Move the PubSub capture and decode tools into a new Opc.Ua.Mcp.PubSub.Diagnostics package with AddOpcUaMcpPubSubDiagnostics and WithOpcUaPubSubDiagnosticsTools. The key-loading decode tool keeps the same opt-in gate and stays off by default. With the last tools moved out, McpHostBuilder no longer restates the profile-to-tool mapping: it composes the four packages and adds only the connection tools the diagnostics profile pairs with capture. The executable now owns transport, logging and CLI plumbing and nothing else. Verified the shipped tool is unchanged by enumerating tools/list over stdio against builds from before and after the split: both expose the same 78 tools. Two reflective tests loaded a hard-coded Opc.Ua.Mcp.dll to reach types that have now moved. Replace both copies of that loader with one McpAssemblyProbe that probes every Opc.Ua.Mcp*.dll, so the tests assert behaviour rather than the current packaging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Describe the four packages, their dependency closures and the embedding example in docs/McpServer.md, the tool README, the NuGet readme and the root README, so the split is discoverable rather than only visible in the project layout. The embedding tests were guarded on NET8_0_OR_GREATER while the test project references the MCP projects only on net10.0, so they broke the net9.0 leg of a full solution build. Use the NET10_0 guard the sibling MCP tests already use. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Opc.Ua.Mcp.McpServerOptions collided by simple name with ModelContextProtocol.Server.McpServerOptions, so every file that used both had to alias one of them. Renaming removes the five aliases rather than propagating them into the newly published library packages. Breaking: the type, its file and McpHostBuilder.CreateMcpServerOptions are renamed with no compatibility shim, which is free to do now because the MCP library packages have not shipped yet. Also fixes the analyzer warnings a full-solution rebuild surfaced in the MCP test project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The asset path had no event support at any layer: ThingDescription had no events map, IWotAssetProvider had no event operation, and AssetRegistry built property and action nodes only. A TD event affordance therefore reached no client, which also left the event side of the WoT binding untestable. A TD events entry now materializes as a non-abstract BaseEventType subtype whose fields come from the event's data schema. The asset object becomes an event notifier and gains a GeneratesEvent reference to the type, so a client subscribing to the asset or to the Server object receives every occurrence. The registry subscribes the provider once per affordance for the lifetime of the TD generation rather than once per interested client, because the server's subscription machinery already decides who receives an event; a provider therefore never tracks per-client state. Breaking: IWotAssetProvider gains SubscribeEventAsync and UnsubscribeEventAsync as ordinary members, with no default implementation and no compatibility shim. An authored uav:severity outside the OPC 10000-5 1..1000 range is clamped rather than rejected, so one bad event definition cannot fail an otherwise valid asset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Findings from a security review of the PR. replay_pcap passed its caller-supplied pcapPath and keyLogPath straight to the replay manager while every sibling decode tool confined them, so it was the one capture tool that could read a file from outside the capture folder. Apply McpCapturePath.ResolveAndValidate there too. The tool is registered only behind the off-by-default diagnostics gate, so this closes a defence-in-depth gap rather than a reachable hole. Three defects in the event support added earlier in this branch: RemoveEventTypeNode dropped only the event type's index entry, but AddEventTypeNode indexes the whole subtree, so every per-field property leaked on each re-applied Thing Description. The registry never called IWotAssetProvider.UnsubscribeEventAsync, leaving the interface member dead and a provider that outlives a generation still subscribed. Unsubscribe the outgoing provider before disposing it. A provider callback runs on the provider's thread and could arrive after its Thing Description had been replaced, reporting an occurrence against a removed event type. Callbacks now capture a generation token and stale ones are dropped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Follow-up to #4128, which merged the WoT Connectivity 1.1 work against draft2 of the companion specifications. Both drafts have moved since, and this branch tracks them through to draft3, adds the projection feature they introduced, and closes the gaps a bidirectional re-audit found.
The specifications are the private
OPCF-Members/spec-draftsdrafts of OPC UA — WoT Connectivity 1.1 and OPC UA — WoT Binding 1.1. They are still drafts, which is why this is opened as a draft PR — see Status at the bottom.What changed in the specifications
Two revisions landed upstream while #4128 was in flight.
draft2 replaced the
eventGroup/actionGroupgrouping constructs with a single projection / view concept, added fifteen Section 6 vocabulary terms, and made two identifier rules normative. draft3 (spec PR #8) un-deprecated the incorporated OPC 10100-1 v1.02 surface, restructured the conformance clause into three profiles, and tightened two security obligations.The draft3 NodeSet is structurally identical to draft2 — I diffed all 286 nodes node-by-node. Every change is metadata: 96
ReleaseStatus="Deprecated"attributes removed,PublicationDateadvanced, andWOTC-ProjectionMaterializationclaimed by three Type Nodes that previously implemented it in name only.Projections (WoT Binding Section 12)
The substantial addition. A projection document selects affordances from other Thing Descriptions or Thing Models and resolves to a document of its own; materialized, it becomes an OPC UA
Viewthat creates no affordance Node and onlyOrganizeswhat its sources already materialized.Opc.Ua.Types/Wot/WotProjection.cs,WotProjectionModel.csOpc.Ua.Types/Wot/WotProjectionResolver.csOpc.Ua.WotCon.Server/Materialization/WotViewProjectionPlan.cs,WotProjectionViewBuilder.csWotProjectionViewNodeManager.cs,LifecycleWotViewProjectionHost.cs,IWotViewProjectionHost.csIWotMaterializedNodeIndex.csGroups reached by
ua:Organizesbecome organizational Objects rather than nested Views, only the outermost materialization is aView, and a member whose source is served by another server is omitted and reported rather than recreated — the federation case the clause calls out.How this is validated
The specification publishes twenty worked examples, two of which are a golden pair: a projection document and the resolved view it is defined to produce.
WotSpecExampleTestsembeds all twenty and runs the pair through the resolver, asserting against the specification's own expected output rather than against my reading of the prose. That single document exercises all three selection forms, the bulk naming rule, the security closure naming and the provenance term.This is the strongest evidence in the PR, and it is worth more than the unit tests around it: it is the same check the upstream
validate_local.pyperforms.Other spec work
WotNodeSetConverter.ModelVocabulary.cs).WotNodeSetConverterOptions.AllowNonPortableIdentifiersas a documented downgrade-to-warning switch for documents authored against v1.00. Defaults tofalse.XRegistryIdentifier.cs) — reverse-authority symbolic identifiers; all seven published worked examples are test cases.Gaps found by re-auditing against draft3
I audited in both directions — every normative requirement to code, and every
uav:term in code back to a spec that defines it. Three findings, all fixed:Wot-Con 1.02accepted anything well-formed. draft3 makes format validation part of the unit, because WoT-Con Minimal is one unit wide and materialization projects a valid document. The 1.02 upload path only deserialized into theThingDescriptionPOCO, which happily accepts JSON that is not a WoT document at all — every member of that type is optional, so{}deserialized and was materialized. It now parses viaWotDocument.Parseand requires the document to identify itself by a non-emptynameortitle, returningBad_DecodingErrorand materializing nothing otherwise. The rule lives inThingDescriptionFormatValidatorand is shared withCreateAssetForEndpoint, whose auto-generated Thing Descriptions §11 also treats as untrusted input.WoTFileoperations were ungated. draft3 §11 namesWriteandCloseAndUpdatealongside the five management methods; both reached the materializer without an access check.EnforceManagementAccessis now wired intoWotAssetFileManager.uav:terms.uav:NodeSet2Preservationwas declared and never used — removed.uav:eventFieldsis read byOpcUaBindingPlannerand defined in neither spec, while a comment claimed it came from OPC 10101. It still works for compatibility but is now documented as a non-standard extension at both sites rather than misattributed.The SSRF prohibition is satisfied by construction: there is no HTTP client anywhere in these assemblies, and all resolution goes through the injected
IWotThingResolver.Two ambiguities fixed in the specifications themselves
The audit turned up two places where a conforming implementer had to invent an answer. Rather than pick silently, I took both upstream — they are pushed to spec-drafts PR #8 (all checks green) and are now normative in Binding §12.6 and Connectivity §7.13:
ViewVersionhad one sentence saying only that it changes. It now changes iff the resolved membership changes, is updated at commit time alongside theNodeVersionstamping, and is a deterministic function of the membership alone in a canonical order — so two servers that resolved the same membership agree, which a per-server counter would not across a redundant pair. Explicitly not monotonic: compare for inequality only.Viewin both cases.Both were already what this code did, so neither changed behaviour — but the order-insensitivity the clause added was not covered by any test, and
ViewVersionIsUnchangedWhenOnlyTheOrderOfTheMembershipChangesnow holds it. It is mutation-verified: removing the ordinal sort turns it red.docs/WotBindings.mdno longer lists these as points "the specification leaves open".Validation
Opc.Ua.Types.TestsOpc.Ua.WotCon.TestsOpc.Ua.WotCon.Bindings.TestsOpc.Ua.XRegistry.TestsOpc.Ua.Server.TestsOpc.Ua.SourceGeneration.Core.TestsOpc.Ua.WotCon.Samples.TestsThe full
UA.slnxbuilds with 0 errors and 0 warnings attributable to this PR on all 6 TFMs (TreatWarningsAsErrorsis on).Opc.Ua.Server.Testsand the source-generation suites are listed because this PR now also touchesMasterNodeManagerand the WoT-Con NodeSet.Compatibility
No public API is removed or broken. Everything added is new surface using
ArrayOf<T>/ByteStringper the repo conventions, wired for DI with a direct-construction fallback, and the one behavioural tightening (Wot-Con 1.02format validation) rejects input that was never valid to begin with.One pre-existing issue is not addressed here because it came in with #4128 and is out of scope:
WotAssetFileManager.OnCloseAndUpdateuses.GetAwaiter().GetResult(), which the repo rules ban. Worth a follow-up.Landed since this PR was opened
ConnectsTowas declared a subtype ofHierarchicalReferences, contradicting OPC 10000-100 §5.5 Table 48. That is an upstream error in every official DI NodeSet up to 1.04, corrected by the OPC Foundation in 1.05.0, so the fixture is replaced with the verbatim 1.05.0 file rather than hand-patched. A guard test pins the corrected form. This also closed a second gap: six DI types the WoT model was missing are 1.05 additions.Pump1.Identificationcarried DI'sFunctionalGroupTypeinstead ofPumpIdentificationType. Every test, the sample documents and the README agreed with each other about it, which is why it survived.WotPumpAddressSpaceComparisonTestscompares this server againstPumpDeviceIntegrationServer, which builds the same OPC 40223 Pump from generated NodeSets and hand-written wiring. An independent oracle rather than a restatement of the same assumptions - it is what found the defect above.MaterializeProjectionViewsAsync. An exception mid-loop discarded the applied View handles before they were published, leaving View Nodes in the address space that nothing could remove.HasWoTProjectionis now created; it was documented and plumbed but never wired.IdentifierMissing, naming neither the value nor the fact that an alias was expected.ViewVersionfollows the algorithm the specification now states. My earlier spec text was rejected on review, correctly: it named neither the function nor the canonical order it demanded, ignored that OPC 10000-3 §5.4 requires a value greater than zero, and its "when, and only when" is unsatisfiable for a 32-bit value. §12.6 now specifies the algorithm outright and this implementation follows it exactly, pinned by an independently computed test vector. Implementing it surfaced a further defect in the clause - joining members on U+000A is not injective, because a NodeId string identifier may itself contain U+000A - which is fixed in the specification by length-prefixing each member.CreateAssetForEndpointvalidated the endpoint but handed whatever the discovery provider returned straight to the materializer. TheWot-Con 1.02format rule now gates both paths from one place.WOTC-Legacyis renamedWot-Con 1.02in the specification and here. Editorial: the NodeSet diff is<Category>values only.Status
Open for review. Two things may still move:
ViewVersiondefinition has since been settled - §12.6 specifies the algorithm and this follows it - but the same could happen to any other clause tracked here.uav:eventFieldsis kept as a documented non-standard extension. That is a judgement call the WG may want to reverse, either by adopting the term or removing it.Two known items are recorded rather than fixed:
Pump1has no hierarchical parent, so it is reachable by NodeId but not by browsing down fromObjects. The cross-NodeSet ordering defect this was originally attributed to is fixed above, and cross-manager references are now symmetric in both registration orders. The Pump itself is still blocked on a different cause found while verifying that fix: with the reference added,DeviceSetexists but the Pump has zero inverse references, so the edge is dropped at import - before any external-reference machinery runs. That is a separate defect in the materialization path, not the one that was fixed, and it is left recorded rather than guessed at.WotAssetFileManager.OnCloseAndUpdateuses.GetAwaiter().GetResult(), which the repo rules ban. Pre-existing from [WotCon] Add OPC UA WoT Connectivity 1.1 with the Types, source generation, Server and xRegistry work it builds on #4128 and out of scope here.Review of the projection resolver and the materialization host is the most valuable thing to look at; the vocabulary and identifier commits are mechanical by comparison.