WoT: documentation, XML-doc and member-order hygiene for conversion extraction

Comment/doc and member-order changes only; no behavior, oracle, default or limit changes.
This commit is contained in:
Marc
2026-10-15 09:04:24 +02:00
parent 3f62ef4e42
commit 5c368d3a48
6 changed files with 123 additions and 29 deletions
+33
View File
@@ -52,6 +52,7 @@ For current protocol-runtime architecture and the contributor guide for adding a
- [12.4.1 Parent placement through `uav:componentOf`](#1241-parent-placement-through-uavcomponentof)
- [12.4.2 Event notifier behaviour in projection Views](#1242-event-notifier-behaviour-in-projection-views)
- [12.4.3 Carried dependencies and View membership](#1243-carried-dependencies-and-view-membership)
- [12.4.4 Archive context for single-document conversion](#1244-archive-context-for-single-document-conversion)
- [12.5 Portable identifiers](#125-portable-identifiers)
- [12.6 The 1.02 asset surface](#126-the-102-asset-surface)
@@ -1253,6 +1254,38 @@ support-only in the outer. The View builder organizes only affordances that are
support-only, so the View lists the selected measurements and not their units unless
the units were selected.
### 12.4.4 Archive context for single-document conversion
A document of a partitioned NodeSet export states references whose authoritative facts
(a reverse reference, a symmetric ReferenceType) live in sibling partitions. A caller
that converts one document at a time supplies those siblings through
`IWotArchiveContextProvider` backed by a `WotNodeSetConverter.ArchiveContextIndex`.
The index groups partitions by their declared model identity (URI, version, publication
date) together with the namespace table, never by the namespace table alone. An archive
that declares no model has no authoritative identity and neither contributes to nor
receives a context. Partitions of a group must agree on server URIs and shared aliases,
and a group in which two partitions declare the same NodeId (in any equivalent
spelling) is conflicted, so asking for its context fails instead of letting one sibling
override another. A returned context is a completed, read-only snapshot. An index whose
build was cut short by a bound is marked incomplete, so a document that carries
preserved native nodes fails explicitly rather than validating against a subset.
A missing native event is accepted
only when both of these hold: the owning preserved type declares it through a native
`GeneratesEvent` reference (directly or through its type or supertype chain), and a
bounded, validated selected EventType carrier supplies an identity equal to the
normalized authored native target. When the native node is present it is compared in
full, and an ambiguous native key or a missing map stays strict. A readable document
is never archive authority by itself.
The resolver document budget (`WotNodeSetConverterOptions.MaxResolverDocuments`)
defaults to 256 documents. The aggregation sample states a larger budget (1024) because
its corpus exceeds the default; hosts with a larger corpus must do the same. The
canonical corpus inventory lists 540 JSON documents, and the verified registry
manifest holds 535 documents (394 Thing Descriptions and 141 Thing Models). These
statements describe the conversion contract only. Complete XML validation and full
JSON Schema validation remain unclaimed and deferred.
### 12.5 Portable identifiers
Two identifier forms are errors, because a document carrying either binds to the
+26 -26
View File
@@ -879,32 +879,6 @@ namespace Opc.Ua.Wot
}
}
/// <summary>
/// Determines whether an affordance of this document only exists as structural support for
/// another affordance of the same document.
/// </summary>
/// <remarks>
/// A resolved projection carries the affordances that a selected affordance depends on, for
/// example the unit property named by <c>uav:unitProperty</c>, so the resolved document
/// stays self-contained. Such a dependency was not selected by the projection itself.
/// An affordance that the projection selects explicitly is never support-only, even when
/// another selected affordance also depends on it. The state is not part of the JSON
/// document, is set only by the projection resolver and is always <c>false</c> for
/// documents that were not produced by one.
/// </remarks>
/// <param name="kind">The WoT affordance kind.</param>
/// <param name="name">The affordance name within its kind.</param>
/// <returns><c>true</c> when the affordance is carried only as a dependency.</returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
public bool IsSupportOnly(WotAffordanceKind kind, string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
return m_supportOnly != null && m_supportOnly.Contains((kind, name));
}
/// <summary>
/// Gets the <c>securityDefinitions</c> map, if present.
/// </summary>
@@ -953,6 +927,32 @@ namespace Opc.Ua.Wot
}
}
/// <summary>
/// Determines whether an affordance of this document only exists as structural support for
/// another affordance of the same document.
/// </summary>
/// <remarks>
/// A resolved projection carries the affordances that a selected affordance depends on, for
/// example the unit property named by <c>uav:unitProperty</c>, so the resolved document
/// stays self-contained. Such a dependency was not selected by the projection itself.
/// An affordance that the projection selects explicitly is never support-only, even when
/// another selected affordance also depends on it. The state is not part of the JSON
/// document, is set only by the projection resolver and is always <c>false</c> for
/// documents that were not produced by one.
/// </remarks>
/// <param name="kind">The WoT affordance kind.</param>
/// <param name="name">The affordance name within its kind.</param>
/// <returns><c>true</c> when the affordance is carried only as a dependency.</returns>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
public bool IsSupportOnly(WotAffordanceKind kind, string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
return m_supportOnly != null && m_supportOnly.Contains((kind, name));
}
/// <summary>
/// Attempts to get the <c>uav:nodeSet</c> preservation envelope.
/// </summary>
@@ -51,7 +51,8 @@ namespace Opc.Ua.Wot
{
/// <summary>
/// Gets the native NodeSet context for the document, or <c>null</c>
/// when the document has no siblings that share its namespace table.
/// when the document declares no model identity or has no siblings
/// that share its declared model and namespace table.
/// </summary>
/// <param name="document">The document being converted.</param>
/// <returns>The context, or <c>null</c>.</returns>
@@ -241,23 +242,54 @@ namespace Opc.Ua.Wot
key.Append(value.Length).Append(':').Append(value).Append(';');
}
/// <summary>
/// The partitions of one authoritative model and their merged aliases and identities.
/// </summary>
private sealed class Group
{
/// <summary>
/// Creates a group whose server URIs and model identity come from the first partition.
/// </summary>
/// <param name="header">The first partition, which supplies the group header.</param>
public Group(UANodeSet header)
{
Header = header;
}
/// <summary>
/// Gets the first partition, against which later partitions are compared.
/// </summary>
public UANodeSet Header { get; }
/// <summary>
/// Gets the aliases shared by the partitions of the group.
/// </summary>
public Dictionary<string, string> Aliases { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the preserved native nodes of all partitions.
/// </summary>
public List<UANode> Items { get; } = [];
/// <summary>
/// Gets the canonical identities of the nodes already contributed.
/// </summary>
public HashSet<string> NodeIds { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets whether the partitions declare conflicting server URIs, aliases or identities.
/// </summary>
public bool Conflicted { get; set; }
/// <summary>
/// Gets or sets the completed context snapshot, which is never mutated once built.
/// </summary>
public UANodeSet? Context { get; set; }
/// <summary>
/// Merges the server URIs and aliases of a partition, flagging any conflict.
/// </summary>
/// <param name="part">The partition to merge.</param>
public void Merge(UANodeSet part)
{
if (!(part.ServerUris ?? []).SequenceEqual(Header.ServerUris ?? [], StringComparer.Ordinal))
@@ -278,8 +310,6 @@ namespace Opc.Ua.Wot
}
}
}
public UANodeSet? Context { get; set; }
}
private readonly System.Threading.Lock m_lock = new();
@@ -490,6 +490,9 @@ namespace Opc.Ua.WotCon.Tests.Materialization
return namespaces;
}
/// <summary>
/// Verifies that selecting only a value organizes only it, not the unit it depends on.
/// </summary>
[Test]
public async Task ExplicitSelectionOfAValueOrganizesOnlyItAndNotItsUnitDependency()
{
@@ -505,6 +508,9 @@ namespace Opc.Ua.WotCon.Tests.Materialization
Assert.That(result.Plan.Omissions.Count, Is.Zero);
}
/// <summary>
/// Verifies that the resolved conversion document still carries the unit dependency of a value-only selection.
/// </summary>
[Test]
public async Task ResolvedDocumentOfAValueOnlySelectionStillCarriesTheUnitDependency()
{
@@ -533,6 +539,9 @@ namespace Opc.Ua.WotCon.Tests.Materialization
"The conversion document keeps the unit dependency and its HasProperty source.");
}
/// <summary>
/// Verifies that explicitly selecting a value and its unit organizes both.
/// </summary>
[Test]
public async Task ExplicitSelectionOfAValueAndItsUnitOrganizesBoth()
{
@@ -547,6 +556,9 @@ namespace Opc.Ua.WotCon.Tests.Materialization
"another selected Property also depends on it.");
}
/// <summary>
/// Verifies that selecting all of a source organizes the value and its unit.
/// </summary>
[Test]
public async Task SelectingAllOfASourceOrganizesTheValueAndItsUnit()
{
@@ -854,18 +854,27 @@ namespace Opc.Ua.WotCon.Tests.Samples
return (registry.Current, contents);
}
/// <summary>
/// A resolver that supplies a native archive context and resolves no external references.
/// </summary>
private sealed class ContextResolver : IWotThingResolver, IWotArchiveContextProvider
{
/// <summary>
/// Creates the resolver over a context index.
/// </summary>
/// <param name="contexts">The index that supplies the native contexts.</param>
public ContextResolver(WotNodeSetConverter.ArchiveContextIndex contexts)
{
m_contexts = contexts;
}
/// <inheritdoc/>
public UANodeSet? GetArchiveContext(WotDocument document)
{
return m_contexts.Find(document);
}
/// <inheritdoc/>
public ValueTask<WotResolverResult> ResolveThingAsync(
string reference,
WotResolutionContext context,
@@ -310,8 +310,16 @@ namespace Opc.Ua.WotCon.Tests.Samples
"samples", "WotCon", "AggregationClient", "Documents");
}
/// <summary>
/// A resolver that serves the corpus documents and supplies a native archive context.
/// </summary>
private sealed class CorpusResolver : IWotThingResolver, IWotArchiveContextProvider
{
/// <summary>
/// Creates the resolver over the corpus documents and a context index.
/// </summary>
/// <param name="documents">The corpus documents keyed by normalized reference.</param>
/// <param name="contexts">The index that supplies the native contexts.</param>
public CorpusResolver(
Dictionary<string, byte[]> documents,
WotNodeSetConverter.ArchiveContextIndex contexts)
@@ -320,11 +328,13 @@ namespace Opc.Ua.WotCon.Tests.Samples
m_contexts = contexts;
}
/// <inheritdoc/>
public UANodeSet? GetArchiveContext(WotDocument document)
{
return m_contexts.Find(document);
}
/// <inheritdoc/>
public ValueTask<WotResolverResult> ResolveThingAsync(
string reference,
WotResolutionContext context,