diff --git a/README.md b/README.md
index 19eca65..2f9d46d 100644
--- a/README.md
+++ b/README.md
@@ -4,17 +4,68 @@ A scratch pad for **OPC UA specification drafts**.
This repository is a working area for authoring and iterating on draft OPC UA information models and companion specifications. It is intentionally informal: contents are experimental drafts used to explore modelling approaches, try out tooling, and prototype NodeSets before anything is proposed or released. Nothing here is normative, official, or final, and everything is subject to change or removal without notice.
+## The specifications
+
+Sixteen documents, grouped by the tree they live in. Every one is a **working draft**: nothing here is normative, official, or endorsed by the OPC Foundation, and namespace URIs and NodeIds are provisional.
+
+Each is generated from a single source of truth — a `tools/build_model.py` emits the NodeSet, the NodeId CSV and the annex tables — so the prose, the model and the tables cannot drift apart. The Word renderings are built into the official OPC Foundation template and are the format a submission is reviewed in.
+
+### core — additions to the base OPC UA namespace
+
+Proposed extensions to `http://opcfoundation.org/UA/` itself, rather than companion models beside it. These are the drafts aimed at an OPC Foundation Working Group as errata or new Parts.
+
+| Specification | What it is, and why it exists | Status | Documents |
+|---|---|---|---|
+| **OPC UA — Data Channels** | A named, authorized, flow-controlled stream of opaque bytes multiplexed onto a SecureChannel that is **already open** — no second port, no second handshake, no second trust anchor. It exists because OPC UA has no streaming primitive, so video, audio and other continuous content today runs over an RTSP or WebRTC endpoint *beside* the Server, with its own security to configure and get wrong. Errata against Parts 3, 4 and 6, plus an `opc.quic` transport. | Draft 0.1.1 | [Specification](core-specs/data-channels/OPC-UA-Data-Channels.md) · [Word](word-drafts/OPC-UA-Data-Channels.docx) |
+| **OPC UA — Apache Arrow Encoding** | A columnar DataEncoding: a Part 6 value mapping, a Part 14 PubSub **batch** message mapping, and an ADBC-style historian surface where Part 11 `HistoryRead` returns Arrow record batches. It exists because analytics and historian consumers read columns, and re-encoding row-at-a-time OPC UA data into columns at the edge of every pipeline is work nobody needs to do twice. | Draft 0.1.0 | [Specification](core-specs/arrow-encoding/OPC-UA-Arrow-Encoding.md) · [Word](word-drafts/OPC-UA-Arrow-Encoding.docx) |
+| **OPC UA — Apache Avro DataEncoding** | A Part 6 mapping of the full OPC UA type model and a Part 14 PubSub message mapping, including Action invoke/response and Discovery messages. Reversible — `decode(encode(x)) == x` — with a NodeSet-driven schema generator and a SchemaId handshake, so a disconnected consumer can decode a payload it did not negotiate. | *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).* | [Specification](https://github.com/OPCF-Members/spec-drafts/blob/main/core-specs/avro-encoding/OPC-UA-Avro-Encoding.md) · [Word](https://github.com/OPCF-Members/spec-drafts/blob/main/word-drafts/OPC-UA-Avro-Encoding.docx) |
+| **OPC UA — xRegistry** | An **abstract** base model projecting any [xRegistry](https://github.com/xregistry/spec) registry onto the AddressSpace: a registry or group is a `FolderType`, a resource version is a Part 5 `FileType`, so a resource can be streamed with `Open`/`Read`/`Close`. It exists so that every domain registry in this repository is the same shape rather than a private invention — Schema Registry, the WoT connectivity registry and the OpenUSD artifact registry all subtype it. | Draft 0.3.0 | [Specification](core-specs/xregistry/OPC-UA-xRegistry.md) · [Word](word-drafts/OPC-UA-xRegistry.docx) |
+| **xRegistry — OPC UA API** | The OPC UA binding of xRegistry written as a peer of its HTTP binding, targeted at the xRegistry project as `core/opcua.md` rather than at the OPC Foundation. It exists so the projection is defined once, on both sides of the boundary. | Draft 0.1.0 | [Specification](core-specs/xregistry/xRegistry-OPC-UA-Api.md) · — |
+
+### cloud — a Server's cloud-facing surface
+
+How a model reaches the systems that consume it, rather than the operators who browse it. Both are domain extensions of `xregistry/` or feed the encodings beside it.
+
+| Specification | What it is, and why it exists | Status | Documents |
+|---|---|---|---|
+| **OPC UA — Schema Registry** | A concrete xRegistry whose resources are schema documents, exposed as a well-known object under the Server object. It exists because a schema-based encoding is not self-describing: a decoder that receives an Avro or Arrow payload needs the exact schema that produced it, and Binary, XML and JSON never had that problem. Deliberately decoupled from PubSub — a Server need not implement Part 14 to be a schema registry. | Draft 0.5.0 | [Specification](cloud-specs/schema-registry/OPC-UA-Schema-Registry.md) · [Word](word-drafts/OPC-UA-Schema-Registry.docx) |
+| **OPC UA — Observability Export** | A transport-neutral layer letting a Server, or a companion specification, declare how its data lands in an observability system as OpenTelemetry metrics, logs and traces. It exists so one generic read-only bridge can forward any conforming Server without understanding the domain, instead of a bespoke exporter per companion specification. Ships addenda for DI, Pumps, Robotics and the Machinery facets. | Draft 0.1.0 | [Specification](cloud-specs/observability-export/OPC-UA-Observability-Export.md) · [Word](word-drafts/OPC-UA-Observability-Export.docx) |
+
+### metaverse — virtual worlds, perception and robot control
+
+Connecting OPC UA to the systems that visualize, perceive and command physical machines. See [`metaverse-specs/README.md`](metaverse-specs/README.md) for how the pieces relate.
+
+| Specification | What it is, and why it exists | Status | Documents |
+|---|---|---|---|
+| **OPC UA for OpenUSD — Part 1: Binding** | A representation, live-binding, command, alarm and composition layer letting any OPC UA companion model drive OpenUSD prims through a discoverable `Server/OpenUSD` contract. It exists so that adding a viewer does not mean writing another bridge. | *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).* | [Specification](https://github.com/OPCF-Members/spec-drafts/blob/main/metaverse-specs/openusd-binding/OPC-UA-OpenUSD-Bindings.md) · [Word](https://github.com/OPCF-Members/spec-drafts/blob/main/word-drafts/OPC-UA-OpenUSD-Binding-Part1.docx) |
+| **OPC UA for OpenUSD — Part 2: Scene Materialization** | The full OpenUSD data model — Stage, Prim, Attribute, Relationship, Composition, Variant — as native OPC UA types, with bidirectional `.usd` ↔ AddressSpace conversion. It approaches the same problem from the opposite end to Part 1 and is self-contained: its NodeSet requires only base OPC UA. | *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).* | [Specification](https://github.com/OPCF-Members/spec-drafts/blob/main/metaverse-specs/openusd-scene/OPC-UA-OpenUSD-Scene-Materialization.md) · [Word](https://github.com/OPCF-Members/spec-drafts/blob/main/word-drafts/OPC-UA-OpenUSD-Scene-Part2.docx) |
+| **OPC UA — Vision** | An information model for machine vision and robotics vision: the sensors, the media they emit, the AI that interprets them and the results they publish. It exists because OPC 40100-1 leaves result content undefined and OPC 40010-1 has no vision types at all, so what a camera actually *concluded* has no standard shape. Media is brokered, not carried. | Draft 0.1.0 | [Specification](metaverse-specs/vision/OPC-UA-Vision.md) · — |
+| **OPC UA — Robot Intent** | Commanding a robot at the level of task intent — move there, grasp that, pick from here — with a Part 10 lifecycle rather than a blocking Method. It exists because OPC 40010-1 describes robot topology and defines **no motion verbs**, so every integrator writes vendor motion code for work that is identical across vendors. Explicitly not safety-rated; it reports what the safety system enforces and refuses what would exceed it. | Draft 0.1.0 | [Specification](metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md) · — |
+| **OPC UA — AI Model Management and Inference** | What an AI model *is*, what it was trained on, where it executes, how it is invoked and how a better one replaces it. It exists because OPC UA has no way to say any of that, so a plant cannot answer *which model produced this answer, and can I audit it* — the question that arrives six months after the parts shipped. Domain-neutral by construction. Eleven [implementation guides](metaverse-specs/extras/ai-model-management/examples/index.md) map it onto real systems. | Draft 0.4.0 | [Specification](metaverse-specs/ai-model-management/OPC-UA-AI-Model-Management.md) · — |
+| **OpenUSD Artifact Registry Service** | An xRegistry domain specification for OpenUSD artifacts, proposed to [xregistry.org](https://github.com/xregistry/spec) rather than to the OPC Foundation, which is why it stays here while the OpenUSD parts are under review. | Draft | [Specification](metaverse-specs/openusd-binding/xRegistry-OpenUsd.md) · — |
+
+### wot — Web of Things
+
+Both specifications are under OPC Foundation review, so the `wot-specs/` tree is not in this repository at present.
+
+| Specification | What it is, and why it exists | Status | Documents |
+|---|---|---|---|
+| **OPC UA — Web of Things Binding** | A standalone revision of OPC 10101 describing an OPC UA interface as a W3C Thing Description or Thing Model, preserving the official `uav` vocabulary and the Read/Write/Observe/Call and security mappings, and adding a collision-safe model vocabulary plus bidirectional NodeSet2 conversion. It exists so a Thing Description round-trips without losing the facts OPC UA knows and the vocabulary cannot yet express. | *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).* | [Specification](https://github.com/OPCF-Members/spec-drafts/blob/main/wot-specs/WoT-Binding/OPC-UA-WoT-Binding.md) · [Word](https://github.com/OPCF-Members/spec-drafts/blob/main/word-drafts/OPC-UA-WoT-Binding.docx) |
+| **OPC UA — WoT Connectivity** | A registry-first revision of OPC 10100-1 layering a Thing Model / Thing Description document registry over the `xregistry/` base model, with the AddressSpace as a derived, shadow-switched projection of the stored documents. It exists because the documents are canonical and the projection is not, and the published 1.02 model had it the other way round. Incorporates every published NodeId. | *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).* | [Specification](https://github.com/OPCF-Members/spec-drafts/blob/main/wot-specs/WoT-Connectivity/OPC-UA-WoT-Connectivity.md) · [Word](https://github.com/OPCF-Members/spec-drafts/blob/main/word-drafts/OPC-UA-WoT-Connectivity.docx) |
+
+### companion — domain companion specifications
+
+| Specification | What it is, and why it exists | Status | Documents |
+|---|---|---|---|
+| **OPC UA for Generators (GenSets)** | A generator set as both an automation asset and a machine built around a prime-mover engine exposing SAE J1939 over CAN: nameplate, ratings, operating state and mode, health and control. It exists because the engine's own bus already carries most of this and nothing standardises how it reaches an OPC UA client. | Draft 1.0.0 | [Specification](companion-specs/Generators/OPC-UA-Companion-Specification-for-Generators.md) · [Word](word-drafts/OPC-UA-Generators.docx) |
+
+Vision, Robot Intent and AI Model Management have no Word rendering yet — they have no clause map under [`word-drafts/tools/specs/`](word-drafts/tools/specs/), which is what drives the build.
+
## Specifications under OPC Foundation review
Five drafts have been submitted to the OPC Foundation and are being reviewed under member confidentiality. They are **not in this repository while that review runs** — they live in [`OPCF-Members/spec-drafts`](https://github.com/OPCF-Members/spec-drafts), a private repository of the OPC Foundation members organization.
-| Specification | Was |
-|---|---|
-| OPC UA — Apache Avro DataEncoding | `core-specs/avro-encoding/` |
-| OPC UA for OpenUSD — Part 1: OpenUSD Binding | `metaverse-specs/openusd-binding/` |
-| OPC UA for OpenUSD — Part 2: Scene Materialization | `metaverse-specs/openusd-scene/` |
-| OPC UA — Web of Things Binding | `wot-specs/WoT-Binding/` |
-| OPC UA — WoT Connectivity | `wot-specs/WoT-Connectivity/` |
+They appear in the tables above with their status, and link to the private repository.
**If you reviewed one of these here, that is where the discussion continues.** The private repository carries the same contribution and review model as this one — the same Word round trip, the same label-driven agents, the same validation — so nothing about how you give feedback changes.
@@ -69,26 +120,20 @@ The Word renderings under `word-drafts/` are regenerated automatically whenever
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full workflow, validation, and conventions, and [`.github/copilot-instructions.md`](.github/copilot-instructions.md) for the specification authoring style — voice and tense, normative language, and the model, example and validator conventions the drafts follow — together with the build and validation commands and a map of the repository.
-## Layout
-
-- `companion-specs/` — draft OPC UA companion specifications, one folder per domain.
- - `Generators/` — draft Companion Specification for electrical power **Generator Sets (GenSets)**: the information model (`Opc.Ua.Generators.NodeSet2.xml`), the NodeId assignments (`Opc.Ua.Generators.NodeIds.csv`), the specification document, and `tools/build_model.py` — a generator that emits the NodeSet, CSV, and reference tables from a single source of truth.
-- `wot-specs/` — Web of Things specification drafts and their supporting tools/examples, kept separate from OPC UA Core and domain companion-specification work.
- - *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).*
- - *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).*
-- `core-specs/` — draft **extensions to the base OPC UA specification** (proposed additions to the `http://opcfoundation.org/UA/` namespace), intended for submission to an OPC Foundation Working Group. Each encoding/catalog folder below contains only the **normative** spec documents, its `README.md`, and the **base reference schema**; all tooling, examples, generated (non-base) schemas, and the shared validation package live under `core-specs/extras/` (a parallel, mirrored tree).
- - `data-channels/` — draft *OPC UA — Data Channels*: an **errata package against Parts 6, 4 and 3** that adds the streaming primitive OPC UA does not have. A *data channel* is a named, authorized, flow-controlled, bidirectional stream of opaque bytes **multiplexed onto a SecureChannel that is already open** — no second port, no second handshake, no second trust anchor — so media and other continuous content flow over the connection a Client already has instead of over an RTSP/WebRTC/gRPC endpoint beside it. Part 6 adds a `STR` MessageChunk that reuses the `MSG` header layout byte for byte (so no securing, verification or token-rollover clause changes), is **never chunked** (so it cannot block the `Publish` path), and carries credit-based flow control, priority scheduling with a normative anti-starvation rule, sender-side deadline expiry with gap notification, half-close and RTT probing; it also defines a new **`opc.quic`** transport where each channel is a native QUIC stream and `Unreliable` payload rides QUIC DATAGRAM frames, with connection migration across network path changes. Part 4 adds the **DataChannel Service Set** (`OpenDataChannel`/`ModifyDataChannel`/`CloseDataChannel`), a server-initiated **offer/accept** pattern that does not invert the request/response model, SecureChannel ownership with Session authorization, auditing and twelve StatusCodes. Part 3 adds `IDataChannelSourceType` — an **Interface**, so any companion type becomes streamable with one `HasInterface` reference and no new Node Attribute or NodeClass — plus `HasDataChannel`, the DataTypes, three EventTypes and `ServerCapabilities.DataChannelCapabilities`. Contains the three insertion-ready errata, a standalone combined spec with a worked H.264 + Opus session and a WebRTC parity comparison, and the generated base-namespace NodeSet/CSV; the reference frame codec, scheduler demo and wire vectors live under `core-specs/extras/data-channels/`.
- - *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).*
- - `arrow-encoding/` — **Apache Arrow (columnar)**: a Part 6 mapping, a Part 14 **batch publish/subscribe** mapping, and an **ADBC-style historian access** surface (Part 11 HistoryRead → Arrow RecordBatch streams). Actions are out of scope for Arrow.
- - `xregistry/` — draft *OPC UA — xRegistry*: an **abstract** companion model that projects any [xRegistry](https://github.com/xregistry/spec) registry onto the OPC UA AddressSpace (a registry/group is a `FolderType` folder; a resource/version document is a `FileType` file). Defines the reusable base types (`RegistryType`, `GroupType`, `ResourceType`, `AttributesType`), the common xRegistry attributes as Properties, a `Labels`/`AttributesType` container with `AddAttribute`/`RemoveAttribute`, auto-bootstrap, the three xRegistry representations, and federation via `ExpandedNodeId`; targeted at OPC Foundation standardization as the base for domain-specific registries (Schema, Asset, Semantic, …). Includes *xRegistry — OPC UA API* (`xRegistry-OPC-UA-Api.md`), the OPC UA API binding for xRegistry — a self-contained peer of the xRegistry HTTP binding, targeted at an xRegistry submission as `core/opcua.md`. Domain registries subtype it.
- - `extras/` — everything **secondary to standardization**, mirroring the structure above: per‑folder `tools/`, `examples/`, and generated (non‑base) `schemas/`; the shared `_common/` (`opcua_enc` package: canonical OPC UA type model, reversibility **corpus**, NodeSet DataType loader, JSON control codec, fingerprint/hexdump helpers); and `validate_all.py` + `requirements.txt`. Run `python core-specs/extras/validate_all.py` to (re)generate and validate every extension.
-- `cloud-specs/` — draft specifications for an OPC UA Server’s **cloud-facing surface**: how a model is published to the systems that consume it rather than to the operators who browse it. Both are **domain extensions of `core-specs/xregistry/`** or feed the encodings beside it, and both outgrew `core-specs/`, so they have their own tree and their own aggregate validator. Same specs/extras split as `core-specs/`, applied per specification.
- - `observability-export/` — draft *OPC UA — Observability Export*: a small, transport-neutral layer that lets a Server, or a companion specification, declare **how its data lands in an observability system** — as OpenTelemetry (OTEL) **metrics, logs and traces** — so a generic read-only **bridge** can forward it over the **classic client/server (RPC) interface** and, **optionally**, over **PubSub (Part 14)** without understanding the domain. Contains the base NodeSet, CSV, and specification document, plus one **standardized subfolder per companion spec** (`pumps/`, `robotics/`, `facets/`, `di/`), each holding the companion-spec **addendum(s)** and the instance-overlay `NodeSet2.xml`. The generator tooling and example descriptor sources live under `cloud-specs/extras/observability-export/` (below). A non-normative **overview deck** ([`cloud-specs/observability-export/README.md`](cloud-specs/observability-export/README.md)) summarizes the why, what and how.
- - `schema-registry/` — draft *OPC UA — Schema Registry*: a **domain extension of `xregistry/`** whose schema groups are `FolderType` folders and schema documents are `FileType` files, exposed as a well-known `SchemaRegistry` Object under the **Server** object (`i=2253`) — a first-class OPC UA binding of the xRegistry Schema Registry, decoupled from PubSub (a server need not support PubSub to be a schema registry; PubSub may optionally reference it) and federated/accessed the same way as an HTTP xRegistry Schema Registry. Adds the schema-decoding metadata and the on-wire **SchemaId** fast path (an Opaque NodeId whose Identifier bytes are the raw SchemaId) used by disconnected consumers of PubSub or historian/ADBC streams; the PubSub DataSet schema evolution is an optional profile (Annex C) and the HTTP correspondence is informative (Annex D).
- - `extras/` — the observability-export generator, its example descriptors and the example validator, mirroring the structure above. The schema-registry generator lives inline under `schema-registry/tools/`. Run `python cloud-specs/validate_all.py` to validate both.
-- `metaverse-specs/` — draft **OPC UA ⇄ OpenUSD** specifications (the metaverse / Universal Scene Description track), mirroring the `core-specs` specs/extras split under a top-level tree. See [`metaverse-specs/README.md`](metaverse-specs/README.md) for an overview of how the two parts relate and which one to pick.
- - *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).*
- - *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).*
- - `extras/` — tooling, USD assets, writers, renderers, converters, and the `metaverse-specs` validator. Run `python metaverse-specs/validate_all.py` to validate the OpenUSD extensions.
-- `skills/` — reusable authoring **skills** (agent instructions) that operate on the drafts.
- - `opcua-observability-export/` — a skill that generates observability-export bindings for any companion specification from its NodeSet (classifying its Variables and events into OTEL metrics, logs and traces; over RPC, optionally realized over PubSub; a machine-readable binding descriptor, an instance-overlay NodeSet, and a companion-spec addendum with annex tables and diagrams).
+## Repository layout
+
+Each tree holds only the **normative** documents and the generated base artifacts — the specification, its `NodeSet2.xml` and its `NodeIds.csv`. Everything secondary sits in a mirrored `extras/` tree: generators, validators, examples, descriptors and generated non-base schemas. The split is not applied uniformly, so look for the generator before assuming where it lives.
+
+| Path | Holds |
+|---|---|
+| [`core-specs/`](core-specs/) | Proposed additions to the base OPC UA namespace, plus [`core-specs/extras/`](core-specs/extras/) and the shared `_common/` encoding package. Validate with `python core-specs/extras/validate_all.py`. |
+| [`cloud-specs/`](cloud-specs/) | The cloud-facing surface. Validate with `python cloud-specs/validate_all.py`. |
+| [`metaverse-specs/`](metaverse-specs/) | Virtual worlds, perception and robot control. Validate with `python metaverse-specs/validate_all.py`. |
+| [`companion-specs/`](companion-specs/) | Domain companion specifications, one folder per domain. |
+| [`word-drafts/`](word-drafts/) | Submission-ready Word renderings built into the official OPC Foundation template, and the build that produces them. Never edit a `.docx` by hand — see [`word-drafts/README.md`](word-drafts/README.md). |
+| [`templates/`](templates/) | The official OPC Foundation companion specification template the Word build clones. |
+| [`skills/`](skills/) | Reusable authoring skills — agent instructions that operate on the drafts. |
+| [`release/`](release/) | The tooling that moves a specification to the private review repository and brings it back, and the manifest recording what has been submitted. |
+| [`spec-drafts/`](https://github.com/OPCF-Members/spec-drafts) | The private review repository, registered as a submodule. Empty unless you are an OPC Foundation member who asked for it. |
+
+Validation is **per specification**: each extension owns a `tools/validate_local.py`, and the three `validate_all.py` files drive lists of them. A tree drives only its own, so a specification that changes trees takes its entry with it.
diff --git a/metaverse-specs/README.md b/metaverse-specs/README.md
index 26a22eb..92dc823 100644
--- a/metaverse-specs/README.md
+++ b/metaverse-specs/README.md
@@ -1,12 +1,12 @@
-# metaverse-specs — OPC UA ⇄ OpenUSD
+# metaverse-specs — OPC UA for virtual worlds, perception and robot control
-Draft specifications connecting **OPC UA** to **OpenUSD** (Universal Scene Description) — the "metaverse" / digital-twin visualization track. The goal across both parts is the same: let a generic connector or renderer show **live industrial data** in a USD scene without anyone hard-coding the mapping.
+Draft specifications connecting **OPC UA** to the systems that visualize, perceive and command physical machines — the "metaverse" / digital-twin track. Two of them bind OPC UA to **OpenUSD** (Universal Scene Description); the other two supply information models OPC UA lacks entirely, for **machine vision** and for **commanding a robot**.
-Nothing here is normative, official, or endorsed by the OPC Foundation or the Alliance for OpenUSD. Namespace URIs and NodeIds are **provisional** and for prototyping only.
+Nothing here is normative, official, or endorsed by the OPC Foundation, the Alliance for OpenUSD, VDMA or any manufacturer. Namespace URIs and NodeIds are **provisional** and for prototyping only.
-> **Both parts are under OPC Foundation review** and are maintained in [`OPCF-Members/spec-drafts`](https://github.com/OPCF-Members/spec-drafts) until it completes. OPC Foundation members can [request access](https://github.com/OPCF-Members/Help); see [*Specifications under OPC Foundation review*](../README.md#specifications-under-opc-foundation-review). What remains here is the tooling, the examples, and the [xRegistry OpenUSD domain specification](openusd-binding/xRegistry-OpenUsd.md), which is proposed to xregistry.org rather than to the OPC Foundation.
+> **Both OpenUSD parts are under OPC Foundation review** and are maintained in [`OPCF-Members/spec-drafts`](https://github.com/OPCF-Members/spec-drafts) until it completes. OPC Foundation members can [request access](https://github.com/OPCF-Members/Help); see [*Specifications under OPC Foundation review*](../README.md#specifications-under-opc-foundation-review). What remains here is the tooling, the examples, and the [xRegistry OpenUSD domain specification](openusd-binding/xRegistry-OpenUsd.md), which is proposed to xregistry.org rather than to the OPC Foundation.
-## Two parts, two directions
+## The OpenUSD pair: two parts, two directions
The two specifications approach the same problem from opposite ends and are deliberately independent:
@@ -22,15 +22,34 @@ The two specifications approach the same problem from opposite ends and are deli
Pick Part 1 when you already have an artist-authored USD asset and want to drive it. Pick Part 2 when the scene itself should be the address space — browsable, subscribable, historizable.
+## The three standalone models
+
+| | [`vision/`](vision/) | [`robot-intent/`](robot-intent/) | [`ai-model-management/`](ai-model-management/) |
+|---|---|---|---|
+| Question | *What does this camera see, and what did it conclude?* | *How do I tell this robot what to do?* | *Which model produced this answer, and can I audit it?* |
+| The gap | OPC 40100-1 leaves result content undefined; OPC 40010-1 has no vision types at all | OPC 40010-1 describes robot topology and defines **no motion verbs** | OPC UA has no way to say what an AI model *is*, where it runs, or what it was trained on |
+| Namespace | `http://opcfoundation.org/UA/Vision/` | `http://opcfoundation.org/UA/RobotIntent/` | `http://opcfoundation.org/UA/AI/` |
+| Release | 0.1.0 | 0.1.0 | 0.4.0 |
+
+Vision and Robot Intent are self-contained on base OPC UA. AI Model Management additionally requires *OPC UA — xRegistry*, because its model catalogue is a domain extension of that abstract registry rather than a private invention — a model catalogue **is** a registry, and defining a second one would leave two incompatible ways to describe the same artefact.
+
+**They compose without coupling.** Vision's `InferencePipelineType.Deployment` is a plain `NodeId`, so a Server can publish cameras and verdicts with no AI model at all; where it does describe one, the chain *result → deployment → model → digest* is available end to end. Vision and Robot Intent share a frame vocabulary with identical literals and numbering. In every case the join is a **facet precondition**, never a `RequiredModel` — which is what lets a domain adopt one model without inheriting the others.
+
## Layout
- *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).*
- *Under OPC Foundation review — moved to [OPCF-Members/spec-drafts](https://github.com/OPCF-Members/spec-drafts); OPC Foundation members can [request access](https://github.com/OPCF-Members/Help).*
-- `extras/` — everything secondary to standardization, mirroring the two folders above:
+- `vision/` — **OPC UA — Vision**: sensors, the media they emit, the AI that interprets them, the results they produce, and the feedback path back in. Standalone on base OPC UA, with worked addenda for `robotics/` and `machine-vision/`.
+- `robot-intent/` — **OPC UA — Robot Intent**: task-level verbs for commanding a robot, with a Part 10 lifecycle. OPC 40010-1 describes robot topology and defines no motion verbs; this supplies the verbs and nothing else. Standalone on base OPC UA.
+- `ai-model-management/` — **OPC UA — AI Model Management and Inference**: what a model is, how to call it, how to call one hosted somewhere else, and how to get one from a catalogue onto the machine. Deliberately **domain-neutral** — it names no camera, no robot and no sensor — so any domain can build on it. The invocation surface does not change with where inference runs; what changes is the trust boundary, and clauses 8 and 10 are about saying so out loud: what happens when the link drops, and whether calling a model sends plant data off site.
+- `extras/` — everything secondary to standardization, mirroring the folders above:
- `openusd-binding/tools/` — the model generator and validator; `examples/` — the pumps and robotics USD assets, binding descriptors, writers, renderers and end-to-end guides.
- `openusd-scene/tools/` — the model generator, the `.usd` ↔ NodeSet converters, and the round-trip checker.
- `openusd-artifacts/` — the emitted xRegistry **artifact registry** for the examples (see Part 1 §7.11).
-- `validate_all.py` — validates every OpenUSD extension.
+ - `vision/tools/` — the model generator, the example builder and the validator.
+ - `robot-intent/tools/` — the model generator and the validator.
+ - `ai-model-management/tools/` — the model generator and the validator, including the domain-neutrality check that fails the build if a type name acquires a domain term.
+- `validate_all.py` — validates every extension in this tree.
## Validate
diff --git a/metaverse-specs/ai-model-management/CHANGELOG.md b/metaverse-specs/ai-model-management/CHANGELOG.md
new file mode 100644
index 0000000..1096005
--- /dev/null
+++ b/metaverse-specs/ai-model-management/CHANGELOG.md
@@ -0,0 +1,158 @@
+# Changelog — OPC UA — AI Model Management and Inference
+
+All notable changes to this specification and its information model.
+
+## Unreleased
+
+### Profiles
+
+Clause 13 has been titled *Profiles and conformance units* since 0.1.0 and defined only facets. §13.3 defines four profiles and §13.4 gives their URIs. The information model does not change, so the release version does not move: profiles are published through the base-UA `Server/ServerCapabilities/ServerProfileArray` and need no member.
+
+A facet is a building block; a profile is a complete claim. The distinction matters commercially rather than technically — a plant writes a profile name into a purchase order, and enumerating nine facets correctly is not something a procurement document does reliably.
+
+| Profile | Facets |
+|---|---|
+| AI Inference Device Server | AI-Base, AI-Invoke |
+| AI Inference Gateway Server | AI-Base, AI-Invoke, AI-OffServer, AI-Federation, AI-Residency |
+| AI Model Catalogue Server | AI-Base, AI-Catalogue, AI-Import |
+| AI Model Lifecycle Server | AI-Base, AI-Dataset, AI-Learning, AI-Catalogue, AI-Import |
+
+The first three were already in §13.1 as prose examples — "a device that runs one fixed model", "a gateway that calls a hosted model", "a plant MLOps node". Naming them cost nothing but a name. §13.1 now points at §13.3 rather than restating them, so the shapes have one definition.
+
+`AI-Residency` is **inside** the gateway profile rather than optional to it. Once inference leaves the Server, *where does my data go* has an answer, and a gateway that cannot state it is the arrangement §9.5 exists to prevent. A Server that federates and cannot answer claims the facets individually.
+
+The **Lifecycle** profile is witnessed by none of the eleven mapped systems, and is defined anyway. All eleven are inference or catalogue systems; none is a plant that trains, which is what clause 7 was written for. Its absence was also why `AI-Learning`, `AI-Dataset` and `AI-Stream` looked orphaned — they belonged to a Server shape the document described nowhere.
+
+`AI-Stream` remains in no profile, deliberately. §13.2 already says a Server answering only through `Invoke` is conformant without it, and a facet that is genuinely optional to every shape should not be bundled into one.
+
+§3 gains definitions for *conformance unit*, *facet* and *profile*. The document used "facet" throughout and defined it nowhere, and "profile" collides with the *typed profile* of a consuming specification (§6.4.1), which is a payload vocabulary and has nothing to do with conformance.
+
+`validate_examples.py` checks every `**… Server**` token in the guides against the profile table, so a guide claiming a shape the specification does not define fails the same way a misspelled facet does.
+
+## 0.4.0 — 2026-08-06
+
+The eleven mappings published under `extras/ai-model-management/examples/` were re-read with a different question: not *which rules are wrong* — that was 0.3.0 — but *what do these systems do that this model cannot represent at all*. The answer had a consistent signature. A guide would **name** a vendor field and then never map it, because there was no member to map it to, and the omission was invisible because nothing was missing from the guide's own tables. Five gaps were found that way, and three further defects were found by checking the specification against itself.
+
+### Breaking changes
+
+Three Methods gain arguments. A client that calls positionally will break; one that builds its call from `InputArguments` will not.
+
+| Method | Change |
+|---|---|
+| `DeploymentType.Invoke` | `PayloadUri` input, after `Payload` |
+| `DeploymentType.InvokeAsync` | `PayloadUri` input, after `Payload` |
+| `ModelSourceType.ListModels` | `ContinuationPoint` input and output |
+
+NodeIds are unaffected: an argument list is the *Value* of an existing `InputArguments` Variable, not its identity. Churn for the release is **0 changed, 0 removed, 12 added**.
+
+### A model had no time on it
+
+Six of the eleven return a model's creation or modification time in the very call a Server populates `ModelType` from — OpenAI, Azure and NIM `created`, Vertex `createTime` and `updateTime`, Hugging Face `lastModified`, Bedrock `startOfLifeTime`. Every guide listed the field and mapped it nowhere.
+
+The asymmetry made the omission plain rather than arguable. `DatasetType.CreatedAt` already exists with §6.3 defending it, and a catalogue resource inherits `CreatedAt`/`ModifiedAt` from xRegistry — so a model known through a catalogue had a vintage and the same model federated from an endpoint did not.
+
+`PublishedAt` and `LastModifiedAt` close it. `LastModifiedAt` is the one that carries weight: §9.3's `FollowsRef` lets the artefact change with nothing else changing, and §12.3.1 requires repointing to be an authorization-bearing act, pointing at `AiJobType.RequestedBy` for the record — but a reference that moves *at the source* produces no job, so the audit trail that clause demands could not be constructed on the one path it exists to cover. A Server following a mutable reference **shall** populate it.
+
+### A model had no end to it
+
+Bedrock alone publishes `modelLifecycle`, with `legacyTime` and `endOfLifeTime`. One vendor of eleven, and it is in this release anyway, because vendor count is the wrong measure for an industrial model: the other ten were built for an audience that does not have to keep a line running.
+
+`ModelCardType.DeprecatedFrom` and `SupportedUntil` are on the card rather than the nameplate for the reason the split exists — *how long will this keep working* is a question about whether the model may run here.
+
+What made it worth a member is what happens on the date. The deployment does not degrade; it stops. `Reachability` goes `Unreachable`, `FallbackPolicy` fires, and where that is `FallBackTo` the line keeps producing while something outside the qualified configuration answers. §12.3.2 constrains that fallback and `ModelUsed` records it faithfully, so nothing is hidden — it is simply not noticed, because nobody was watching for a date. Every other availability facility in the model is a way of coping *after* the fact; this is the only one whose value is a date in the future.
+
+### The asynchronous path could not carry a large payload
+
+`InferenceJobType` had no `TransferRequired` and no `Transfer`, so `InvokeAsync` was a `ByteString` in and out, bounded by exactly the three limits §8.2.4 says this model does not get to choose — while §8.6 motivated it with "a batch scored overnight and an analysis over months of recorded data", which are the requests most likely to exceed them. §13.2 asserted parity with `Invoke`; on size there was none. AI-Transfer is a separate facet, so a Server could implement either alone and a client with a large payload and a long-running job had no path giving it both.
+
+§8.6.1 separates two problems that had been collapsed into one. **A result too large to carry** is what `TransferRequired`/`Transfer` solve, and the job now carries the same pair on the same terms. **Data that never needed to move** is different: five hosted platforms take a storage URI in and out, and chunking a batch that already sits in the plant's object store copies it twice for no benefit. `PayloadUri` on `Invoke` and `InvokeAsync`, with `RequestUri` and `ResponseUri` on the job, is the answer — governed by the exactly-one rule §10.2 already applies to `Source` and `Registry`.
+
+Five guides had discarded the vendor mechanism on the same narrow ground, that it is not a chunked transfer. They were right that it is not, and there was nothing else to compare it to.
+
+Two obligations are inherited rather than invented: such a URI is untrusted input under §12.2, and it is an egress question under §9.5 — a deployment whose `EgressPermitted` is false **shall not** accept a `PayloadUri` naming somewhere outside the operator's boundary. A URI is a quieter way to move data than a payload, which is why it needs saying.
+
+### A deployment did not say what to send it
+
+Ten of the eleven publish a request contract. The model captured it for the tensor half only: §6.2 called `Inputs`/`Outputs` the only machine-readable description of what a deployment accepts, and they are empty for every system whose contract is a JSON body — which is why `AI-Signatures` was unclaimable by every hosted platform in the set.
+
+The vocabulary existed and was out of reach. `ApiDialect` sat on `ModelSourceType`, and §9.2 scoped it away in terms that were an explicit assertion rather than an oversight: *"They never affect how an OPC UA client calls this Server, which is always §8."* For an `OnServer` deployment `Source` is null, so there was not even an indirect path.
+
+§6.4.2 puts `ApiDialect` and `EndpointDescriptionUri` on `DeploymentType`, and §9.2's sentence is rewritten rather than quietly dropped. The replacement keeps what was true — how a client calls this Server is always §8, one Method and one opaque payload — and adds what was missing: naming *which* contract the opaque bytes must satisfy is not typing them, and §8.2's argument for opacity is about contents. The two dialects are read by different parties: the source's is what this Server speaks outward, the deployment's is what a caller must speak inward. A Server that passes the payload through publishes the same value twice.
+
+### A pinned deployment could move without anything changing
+
+OpenAI publishes `system_fingerprint` and NIM an active profile ID — an identity for the *serving configuration* as distinct from the artefact. §12.1.1 correctly forbade putting either in `Digest` and offered `ArtifactUri` or `ProvenanceUri` instead, which are scalar Strings on `ModelType` and cannot hold a per-deployment value. The datum was thrown away.
+
+`DeploymentType.RuntimeIdentity` holds it: opaque, compared only for equality, never parsed — the same contract `Digest` has. §9.3.1 states what makes it matter: a change to it **is** an observable change to the deployment, which is what makes §9.3's promise about a `Pinned` artefact true rather than aspirational. Under a `Pinned` binding it is often the only observable change available, because the model did not move and nothing else in the address space did either.
+
+§9.3.1 also states, at no model cost, what `Pinned` is actually worth. Where `DigestProvenance` is `NotAvailable` the deployment is pinned to a **name** the source promises to hold stable, and `Pinned` records that promise rather than this Server's verification. The two are already distinguishable by reading one member.
+
+### Three defects found by reading the specification against itself
+
+- **§6.4.3 defined a state transition with no modelled input.** `Degraded` was normative for a deployment "answering but missing its `LatencyBudget`", and no measured latency existed anywhere, so the rule could not be observed to be satisfied or violated by any legal Server. `ObservedLatency` supplies it. `LatencyBudget`'s own description claimed a client could detect regression from it, which was never true of a budget alone.
+- **§13.2's AI-InvokeAsync row** claimed parity with `Invoke` that was false on payload size. It is true once §8.6.1 lands, and the row now says which rules it rests on.
+- **§9.4's `ListModels` answered catalogue size with a cap and no cursor.** `MaxResults` bounds the response and puts everything past it permanently out of reach, which against a public catalogue is most of it. `ContinuationPoint` makes the enumeration completable, and an empty returned value is how a client knows to stop.
+
+### Nothing here is Mandatory
+
+Every added member is Optional, governed by a **conditional shall** stated in the clause that defines it — populate `LastModifiedAt` where the binding follows a mutable reference, `ObservedLatency` where the Server reports `Degraded` on latency grounds, `ApiDialect` where `Inputs`/`Outputs` do not describe the payload contract. A blanket Mandatory would have obliged nine of eleven Servers to publish `RuntimeIdentity` they cannot obtain, and a requirement no conformant implementation can satisfy is not a requirement. A conditional one binds exactly where it can be discharged, and stays testable.
+
+### Known gaps
+
+Unchanged from 0.3.0: `AI-Learning`, `AI-Stream` and `AI-Dataset` are witnessed by none of the eleven systems; `AI-Catalogue` and `AI-Import` are co-extensive across all of them; and clause 13 is titled "Profiles and conformance units" while defining no profiles.
+
+## 0.3.0 — 2026-08-05
+
+Eleven informative mappings of this model onto real systems — Microsoft Foundry, OpenAI, Amazon Bedrock, Amazon SageMaker, Google Vertex AI, NVIDIA NIM, NVIDIA Triton, KServe Open Inference Protocol, Hugging Face, embedded runtimes and a peer OPC UA Server — were written against release 0.2.0 and are published beside it under `extras/ai-model-management/examples/`. Writing them was a natural experiment: a rule that no real system can satisfy, or that two of them satisfy differently, shows up as a mapping that cannot be written honestly. This release is what the experiment found.
+
+### Model changes
+
+**`DigestProvenanceEnum` (`ns=2;i=3015`)**, with `DigestProvenance` **Mandatory** on `ModelType` and Optional on `ModelResourceType`.
+
+`Digest` is Mandatory so that its absence is uniform and browsable, and that remains right — seven of the eleven systems cannot fill it, and making it Optional would have made "no digest" indistinguishable from "does not implement digests". But an empty `Digest` then carried two meanings and a populated one carried three, and a client deciding whether to run a model on a line needs *nobody checked* separated from *two parties agree*.
+
+The absence of a positive answer was doing visible harm in the guides: five of them each prohibited a **different** wrong value — a response fingerprint, a resource name, a storage entity tag, a NIM manifest hash, a repository commit identifier — and none could say what to publish instead. §12.1.1 now states the prohibition once and supplies `NotAvailable` as the answer.
+
+Mandatory rather than Optional for the reason `Digest` itself is Mandatory, which §6.2 already gives: clause 12 depends on it, and a rule that depends on an Optional member is a rule a conformant Server can silently not satisfy. It is always answerable, because `NotAvailable` is always available.
+
+A StatusCode channel was considered and rejected: `DeclaredBySource` and `VerifiedOnStage` are both *Good*, so the distinction the member exists to make cannot be carried by a status.
+
+**`ModelImportJobType.Registry` (Optional `NodeId`)**, with §10.2 requiring exactly one of `Source` and `Registry` to be non-null.
+
+`Source` was Mandatory and typed `ModelSourceType`, so there was no path from an import job to a `ModelRegistryType` — contradicting the §4.4 and §5.1 diagrams, which draw exactly that edge. A Server importing from a catalogue had to invent a `ModelSourceType` wrapper around its own registry.
+
+`Source` keeps its Mandatory modelling rule and takes null on the registry path, which is the idiom `TargetDeployment` already uses. Nothing renumbers and no existing member changes its rule.
+
+**`ModelType.Name` is constrained rather than retyped.** Its `Text` **shall** be the source system's name for the model, carried across unchanged; a Server **may** add a translation for display and **shall not** translate, reformat or prettify the `Text`. Retyping from `LocalizedText` to `String` would have been the cleaner model and is breaking, and the identity property that was wanted — two Servers fetching one model from two mirrors produce the same string — is obtainable by constraining the field.
+
+NodeId churn for the release is **0 changed, 0 removed, 5 added**.
+
+### Normative changes carrying no model cost
+
+- **§8.2.3 — an empty `UnitKind` means the call was not metered.** `AI-Invoke` required `Usage` "populated on every response" while no clause imposed the obligation, and "empty" was unencodable: the three counts are `UInt64` and this model declares no optional fields, so leaving them empty encodes as `0`, indistinguishable from a metered zero. The facet was therefore unsatisfiable for every tensor and in-process runtime, and five guides claimed it while documenting empty `Usage`. The unit carries the sentinel because the counts cannot.
+
+- **§9.5 — residency composes end-to-end rather than next-hop.** A payload leaves a deployment along exactly two modelled edges, `FallsBackTo` and `Source`, and only the first was guarded. `EgressPermitted`'s only **shall** binds on `InferenceLocation` `Cloud`, so a cell-to-site `EdgeOffServer` hop escaped it entirely — one local hop with no internet in sight, while the site Server federates onward to a hosted endpoint. A Server federating to a peer now reads that peer's declarations and publishes nothing more permissive, defaulting fail-safe where it cannot read them.
+
+- **§12.1.1 — digest provenance does not strengthen by being forwarded**, on the ordering `NotAvailable` < `DeclaredBySource` < `ComputedByServer` < `VerifiedOnStage`. The same composition argument as §9.5, applied to the other thing a federated deployment forwards.
+
+- **§13.2 — `AI-Base` deployment requirements are conditional on the Server exposing a deployment.** §13.1 describes a plant MLOps node that "may never call `Invoke` at all" and the facet required at least one `DeploymentType`, so the specification promised a Server shape its own conformance table forbade. A catalogue Server is that shape.
+
+- **§13.2 — `AI-OffServer` cites §12.2's scheme requirement instead of restating it**, so the two cannot drift apart.
+
+- **§6.2, §9.2 — three members that were answering two questions each.** `ModelId` carries the source's identifier verbatim. `Publisher` names the organisation that **produced** the model, not the one serving it — two guides read it from an `owned_by` field and published the host, so two Servers serving one model disagreed about who made it. `AuthenticationKind` classifies what is **stored**, not the handshake performed, which is what makes a request-signing scheme answerable without a new literal. `ApiDialect` classifies the contract this Server speaks to that endpoint, and a catalogue-only source is `Proprietary` with an `EndpointDescriptionUri` — correct, rather than a shortcoming.
+
+### What the experiment vindicated
+
+Recorded because a design that survives contact with eleven systems is worth not relitigating: `Digest` Mandatory despite most sources being unable to fill it; dialects naming contracts rather than vendors; `UnitKind` in place of hardcoded token fields; `Throttled` separated from `Unreachable`; and an open-string `TaskKind`.
+
+### Known gaps
+
+`AI-Learning`, `AI-Stream` and `AI-Dataset` are witnessed by none of the eleven systems. `AI-Catalogue` and `AI-Import` are co-extensive across all of them. Clause 13 is titled "Profiles and conformance units" and defines no profiles.
+
+## 0.2.0 — 2026-08-03
+
+Federation, catalogue and chunked transfer. `ModelSourceType`, the xRegistry-derived registry types, `ModelImportJobType`, `InferenceJobType`, `InferenceTransferType`, `EvaluationRunType` and `ModelCardType`, with the residency and egress members on `DeploymentType`.
+
+## 0.1.0
+
+Initial working-group draft: `AiRootType`, `ModelType`, `DatasetType`, `DeploymentType` and `LearningJobType`.
diff --git a/metaverse-specs/ai-model-management/OPC-UA-AI-Model-Management.md b/metaverse-specs/ai-model-management/OPC-UA-AI-Model-Management.md
new file mode 100644
index 0000000..b148301
--- /dev/null
+++ b/metaverse-specs/ai-model-management/OPC-UA-AI-Model-Management.md
@@ -0,0 +1,1467 @@
+# OPC UA — AI Model Management and Inference
+
+> Status: Working-group draft (Release 0.4.0). This document, together with `Opc.Ua.AiModelManagement.NodeSet2.xml` and `Opc.Ua.AiModelManagement.NodeIds.csv`, defines an OPC UA information model for **the AI models an installation runs**: what a model is, what it was trained on, where it executes, and how a better one replaces it.
+>
+> It is deliberately **domain-neutral**. Nothing here names a camera, a sensor, an image or a robot: a model is trained on a dataset, deployed somewhere, and superseded — and that story is the same whether the input is a photograph, a vibration spectrum or a process trace.
+>
+> Nothing here is normative, official, or endorsed by the OPC Foundation or IDTA; namespace URIs and NodeIds are **provisional** and for prototyping only.
+
+## Contents
+
+- [1 Scope](#1-scope)
+ - [1.1 Motivation](#11-motivation)
+ - [1.2 What this specification does not do](#12-what-this-specification-does-not-do)
+ - [1.3 Capabilities and versioning](#13-capabilities-and-versioning)
+- [2 Normative references](#2-normative-references)
+- [3 Terms, definitions and abbreviations](#3-terms-definitions-and-abbreviations)
+- [4 Scenarios](#4-scenarios)
+ - [4.1 A model runs on the machine](#41-a-model-runs-on-the-machine)
+ - [4.2 The model runs somewhere else](#42-the-model-runs-somewhere-else)
+ - [4.3 The link drops and something else answers](#43-the-link-drops-and-something-else-answers)
+ - [4.4 A model arrives from a catalogue](#44-a-model-arrives-from-a-catalogue)
+ - [4.5 A large payload will not fit in a call](#45-a-large-payload-will-not-fit-in-a-call)
+ - [4.6 The answer arrives later](#46-the-answer-arrives-later)
+ - [4.7 Corrections become the next model](#47-corrections-become-the-next-model)
+ - [4.8 Someone asks what produced a decision](#48-someone-asks-what-produced-a-decision)
+- [5 Overview and concepts](#5-overview-and-concepts)
+ - [5.1 Objects and relationships](#51-objects-and-relationships)
+ - [5.2 Binding from a consuming specification](#52-binding-from-a-consuming-specification)
+ - [5.3 Model ownership](#53-model-ownership)
+- [6 Information model](#6-information-model)
+ - [6.1 Type hierarchy](#61-type-hierarchy)
+ - [6.2 `ModelType`](#62-modeltype)
+ - [6.3 `DatasetType`](#63-datasettype)
+ - [6.4 `DeploymentType`](#64-deploymenttype)
+ - [6.5 `UsesModel` and `TrainedOn`](#65-usesmodel-and-trainedon)
+ - [6.6 `AiJobType`](#66-aijobtype)
+- [7 The learning loop (normative)](#7-the-learning-loop-normative)
+ - [7.1 Method behaviour and StatusCodes (normative)](#71-method-behaviour-and-statuscodes-normative)
+ - [7.2 Relationship to models and deployments](#72-relationship-to-models-and-deployments)
+ - [7.3 Partial implementation](#73-partial-implementation)
+- [8 Inference (normative)](#8-inference-normative)
+ - [8.1 Location independence](#81-location-independence)
+ - [8.2 Payload and envelope](#82-payload-and-envelope)
+ - [8.3 Parameters](#83-parameters)
+ - [8.4 Capabilities](#84-capabilities)
+ - [8.5 Incremental results](#85-incremental-results)
+ - [8.6 Asynchronous inference](#86-asynchronous-inference)
+- [9 Consuming a model hosted elsewhere (normative)](#9-consuming-a-model-hosted-elsewhere-normative)
+ - [9.1 `ModelSourceType`](#91-modelsourcetype)
+ - [9.2 Wire contract and authentication](#92-wire-contract-and-authentication)
+ - [9.3 Version binding](#93-version-binding)
+ - [9.4 Availability and fallback](#94-availability-and-fallback)
+ - [9.5 Data residency and egress](#95-data-residency-and-egress)
+- [10 The catalogue and model import (normative)](#10-the-catalogue-and-model-import-normative)
+ - [10.1 The catalogue](#101-the-catalogue)
+ - [10.2 Importing a model](#102-importing-a-model)
+ - [10.3 Import modes](#103-import-modes)
+ - [10.4 Digest verification](#104-digest-verification)
+- [11 Governance and provenance (normative)](#11-governance-and-provenance-normative)
+ - [11.1 Model card](#111-model-card)
+ - [11.2 Evaluation](#112-evaluation)
+ - [11.3 Lineage](#113-lineage)
+ - [11.4 Safety assessment](#114-safety-assessment)
+- [12 Security](#12-security)
+ - [12.1 Provenance](#121-provenance)
+ - [12.2 URI handling](#122-uri-handling)
+ - [12.3 Promotion authorization](#123-promotion-authorization)
+ - [12.4 Digest and authorship](#124-digest-and-authorship)
+- [13 Profiles and conformance units](#13-profiles-and-conformance-units)
+ - [13.1 Declaring conformance](#131-declaring-conformance)
+ - [13.2 Facets](#132-facets)
+ - [13.3 Profiles](#133-profiles)
+ - [13.4 Profile and facet URIs](#134-profile-and-facet-uris)
+- [Annex A — Information model (generated)](#annex-a--information-model-generated)
+- [Annex B — Informative alignments](#annex-b--informative-alignments)
+- [Annex C — A worked arrangement (informative)](#annex-c--a-worked-arrangement-informative)
+ - [C.1 The situation](#c1-the-situation)
+ - [C.2 Getting the models here](#c2-getting-the-models-here)
+ - [C.3 The two deployments](#c3-the-two-deployments)
+ - [C.4 A normal call, and a link failure](#c4-a-normal-call-and-a-link-failure)
+ - [C.5 Throttling](#c5-throttling)
+- [Annex D — Deploying a classical model (informative)](#annex-d--deploying-a-classical-model-informative)
+ - [D.1 The model](#d1-the-model)
+ - [D.2 In the catalogue](#d2-in-the-catalogue)
+ - [D.3 Getting it onto the controller](#d3-getting-it-onto-the-controller)
+ - [D.4 The shape contract](#d4-the-shape-contract)
+ - [D.5 The deployment](#d5-the-deployment)
+ - [D.6 Calling it](#d6-calling-it)
+ - [D.7 Capabilities](#d7-capabilities)
+
+---
+
+## 1 Scope
+
+This specification defines an OPC UA information model that lets a Server describe:
+
+- **what model it is running** — identity, version, framework, format, and the digest that makes the artefact verifiable;
+- **what that model was trained on** — including whether the data was real, synthetic or both;
+- **where inference executes** — in the Server, on an edge node, in a cloud service, or in a simulator;
+- **how to actually run it** — one invocation surface that does not change with any of the above (clause 8);
+- **how to run a model this Server does not host** — the wire contract, the credential, the capabilities, and what happens when the link fails (clause 9);
+- **how a model gets here** — pulling one from a catalogue and either describing it where it stands or bringing its bytes across, with the digest checked at the moment that matters (clause 10);
+- **whether it may be used at all** — what it is for, where it stops working, how it measured, and whether calling it sends plant data off site (clause 11);
+- **how a model is replaced** — the capture, label, train and promote loop, and who is allowed to complete it.
+
+### 1.1 Motivation
+
+An industrial AI model is not device firmware. It is an artefact the operator or system integrator supplies, versions and approves, and the same physical equipment runs different models over its life. Someone has to be able to ask *which model produced this decision, what was it trained on, and who promoted it* — and today, no OPC UA specification lets them.
+
+Three IDTA submodel templates describe the pieces — **IDTA 02060** for a model nameplate, **IDTA 02058** for a dataset, **IDTA 02059** for a deployment — but they are Asset Administration Shell templates, not an OPC UA address space. This model aligns with them member-for-member so that an AAS can be populated from these nodes without loss, while remaining browsable, subscribable and callable in its own right.
+
+**Nothing here is specific to any one kind of input.** `TaskKind` is a String. `SourceKind` distinguishes real capture from simulator output. The learning loop runs `Idle → Collecting → Labelling → Training → Validating → Ready → Promoted`. A vibration-analysis model, a process soft sensor and a quality classifier all need exactly this, and none of them need a lens — which is why the model is domain-neutral by construction rather than by convention, and why its validator fails the build if a type name acquires a domain term.
+
+### 1.2 What this specification does not do
+
+- It does **not** carry model artefacts or training data. `ArtifactUri` says where the bytes are; the bytes travel by whatever means already moves large files, and `Digest` is what makes the retrieval verifiable.
+- It does **not** define what an inference payload *contains*. Clause 8 defines the envelope — routing, parameters, accounting, why output stopped, which model answered — and leaves the payload opaque, because what you pass to a model and what comes back is domain vocabulary: an image and a set of detections, a spectrum and a fault class. An envelope that tried to type that would need extending for every domain that ever adopted it.
+- It does **not** define a training algorithm, a scheduler or an MLOps platform. `TriggerTraining` requests training and `LearningJobType` observes it; where the training runs is out of scope, and clause 7 is explicit that a Server may implement only the capture stages.
+- It is **not** a governance or compliance framework. It records what is needed to answer provenance questions; whether an installation is permitted to run a given model is decided elsewhere.
+
+### 1.3 Capabilities and versioning
+
+This specification covers the model, the dataset, the deployment, the learning loop, the invocation surface, consumption of externally hosted models, the catalogue and the import bridge.
+
+The NodeSet declares **two** `RequiredModel` entries: the base OPC UA namespace, and *OPC UA — xRegistry*, because the catalogue of clause 10 is a domain extension of that abstract registry rather than a private invention. That is a real cost and it is taken deliberately — a model catalogue **is** a registry, and defining a second one here would leave two incompatible ways to describe the same artefact.
+
+It is worth being precise about what that dependency does **not** reach. A consuming specification still binds to this one through a plain `NodeId` Property (§4.2) and takes no NodeSet dependency of its own, so a vision or condition-monitoring Server is unaffected by this model's dependencies. The obligation lands on a Server that implements *this* specification, not on one that merely points at it.
+
+---
+
+## 2 Normative references
+
+- **OPC 10000-3, -4, -5** — Address Space Model, Services, Information Model.
+- **OPC 10000-6** — Mappings. Structure encoding of the DataTypes listed in Annex A.
+- **OPC 10000-10** — Programs. `ProgramStateMachineType` is the base type of `AiJobType` (§6.6) and supplies the lifecycle, the transition events and the `Start`/`Suspend`/`Resume`/`Halt` Methods that every long-running job here inherits rather than reinvents.
+- **OPC 10000-5** — `FileType`, reached through xRegistry's `ResourceType`. It is what lets a staged model artefact be read over OPC UA with `Open`/`Read`/`Close` (§10.3).
+- **OPC UA — xRegistry** — [`../../core-specs/xregistry/OPC-UA-xRegistry.md`](../../core-specs/xregistry/OPC-UA-xRegistry.md). A **working draft in this repository**, and the one **normative dependency** this model takes beyond base OPC UA. `ModelRegistryType`, `ModelPublisherType`, `ModelResourceType` and `DatasetResourceType` are domain extensions of its `RegistryType`, `GroupType` and `ResourceType` (clause 10). Because it is a draft, its NodeIds are provisional and so, transitively, is this model's dependency on them.
+
+Informative alignments — IDTA 02058, IDTA 02059, IDTA 02060, and the OPC UA ⇄ AAS bridge — are listed in Annex B. They are **not** normative references and impose no dependency, notwithstanding that the member sets here are drawn from them deliberately.
+
+---
+
+## 3 Terms, definitions and abbreviations
+
+| Term | Definition |
+|---|---|
+| **Model** | A trained artefact that maps input to output. Modelled as `ModelType`. Identity, not behaviour: this specification describes the model, it does not run it. |
+| **Dataset** | The samples a model was trained or validated on. Modelled as `DatasetType`. |
+| **Deployment** | A model made executable somewhere. Modelled as `DeploymentType`. One model may have several deployments; each names exactly one model. |
+| **Inference location** | Whether execution happens in the Server, on an edge node, in a cloud service or in a simulator. It changes the trust boundary and the latency, and it changes nothing else. |
+| **Learning job** | One turn of the capture, label, train and promote loop. Modelled as `LearningJobType`. |
+| **Promotion** | Making a candidate model the one deployments use. The one operation here that changes what the equipment does. |
+| **Digest** | A cryptographic hash of an artefact, which is what makes a retrieved artefact verifiable as the one described. |
+| **Conformance unit** | The smallest individually testable requirement of this specification. Grouped into facets rather than claimed one by one. |
+| **Facet** | A named, individually claimable set of conformance units — a building block, not a complete claim. Clause 13.2 defines them. |
+| **Profile** | A named set of facets describing one plausible Server, claimed by publishing its URI in `Server/ServerCapabilities/ServerProfileArray`. Clause 13.3 defines them. Not to be confused with a *typed profile* (§6.4.1), which is a consuming specification's payload vocabulary and has nothing to do with conformance. |
+
+---
+
+## 4 Scenarios
+
+This clause is **informative**. It sets out the arrangements this specification is meant to support, each with the flow that realises it, so that a reader can find the one resembling their installation before reading the normative clauses that define its parts.
+
+The scenarios are not alternatives to choose between. A single Server commonly implements several — a plant that runs a model locally, calls a hosted one for a second opinion, and imports both from a corporate catalogue is doing three of them at once.
+
+### 4.1 A model runs on the machine
+
+The simplest arrangement, and the most common in practice: the artefact sits on the controller and executes in its own process.
+
+```mermaid
+flowchart LR
+ C["Client"] -->|"Invoke"| D["DeploymentType
InferenceLocation OnServer"]
+ D -->|UsesModel| M["ModelType"]
+ M -->|Digest| A["local artefact"]
+ D -->|"response + ModelUsed"| C
+```
+
+Nothing is remote, so `Source` is null, `EgressPermitted` is false and `FallbackPolicy` is typically `Fail` — there is nothing to fall back to. Annex D works this end to end for an ONNX classifier.
+
+**Facets:** AI-Base, AI-Invoke, AI-Signatures.
+
+### 4.2 The model runs somewhere else
+
+The Server has no model of its own and calls one hosted elsewhere — an appliance on the plant network, or a service beyond it.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as Client
+ participant S as OPC UA Server
+ participant R as Remote endpoint
+ C->>S: Invoke
+ S->>S: read Source: dialect, credential, capabilities
+ S->>R: request in the dialect the endpoint speaks
+ R-->>S: response
+ S-->>C: response, ModelUsed, Usage, FinishReason
+```
+
+The client's call is unchanged from §4.1 — that is the point of §8.1. What changes is the trust boundary, and clause 9 is what makes the arrangement describable: the wire contract, the credential reference, and whether calling it sends plant data off site.
+
+**Facets:** AI-Base, AI-Invoke, AI-Federation, AI-Residency.
+
+### 4.3 The link drops and something else answers
+
+The arrangement §4.2 needs in order to be usable on a line that cannot stop.
+
+```mermaid
+flowchart TD
+ C["Client"] -->|Invoke| P["Primary deployment
Cloud or EdgeOffServer"]
+ P --> Q{"reachable?"}
+ Q -->|yes| OK["response
ModelUsed = primary model"]
+ Q -->|no| F{"FallbackPolicy"}
+ F -->|FallBackTo| S["Secondary deployment
OnServer"]
+ S --> R2["response
ModelUsed = secondary model"]
+ F -->|HoldLast| H["last result
LastSuccessAt shows its age"]
+ F -->|Fail| X["failure
nothing produced"]
+```
+
+The branch worth noticing is the middle one: the caller asked nothing different and got an answer from a **different model**, so `ModelUsed` is the only thing that says so. Annex C works this through with a WAN failure.
+
+**Facets:** as §4.2, plus a second deployment for the fallback.
+
+### 4.4 A model arrives from a catalogue
+
+How a model gets onto a machine at all, whether by federating its description or by bringing its bytes.
+
+```mermaid
+flowchart LR
+ CAT["ModelRegistryType
publisher / name / version"] -->|Registry| J["ModelImportJobType"]
+ J -->|Federate| FD["ModelType
artefact stays at source"]
+ J -->|Stage| ST["fetch + verify Digest"]
+ ST --> LD["ModelType
artefact local"]
+ FD --> DEP["DeploymentType"]
+ LD --> DEP
+```
+
+Staging is where the digest is verified, because it is the one moment a substituted artefact would enter the system (§10.4).
+
+**Facets:** AI-Catalogue, AI-Import.
+
+### 4.5 A large payload will not fit in a call
+
+An image or a sample window exceeds what a `ByteString` can carry, so the exchange is chunked.
+
+```mermaid
+flowchart LR
+ C["Client"] -->|"read MaxInlinePayloadSize"| D["DeploymentType"]
+ C -->|BeginTransfer| T["InferenceTransferType"]
+ C -->|"Request: Open/Write/Close"| T
+ C -->|Execute| T
+ T -->|"Response: Open/Read/Close"| C
+```
+
+`Invoke` is the shortcut that works while everything is small; this is the general path (§8.2.4).
+
+**Facets:** AI-Base, AI-Transfer.
+
+### 4.6 The answer arrives later
+
+Work that does not finish while a caller waits — a batch scored overnight, an analysis over recorded data.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as Client
+ participant D as DeploymentType
+ participant J as InferenceJobType
+ C->>D: InvokeAsync
+ D-->>C: Job
+ C->>J: Subscribe
+ J-->>C: Running, Progress
+ J-->>C: Halted, FinishReason
+ C->>J: read ResponsePayload, ModelUsed, Usage
+```
+
+**Facets:** AI-Base, AI-InvokeAsync.
+
+### 4.7 Corrections become the next model
+
+An operator disagrees with a verdict, the correction is retained, and it eventually becomes a model version.
+
+```mermaid
+stateDiagram-v2
+ [*] --> Idle
+ Idle --> Collecting: StartCollection
+ Collecting --> Training: TriggerTraining
+ Training --> Validating
+ Validating --> Ready
+ Ready --> Promoted: PromoteModel
+ Promoted --> Collecting: StartCollection
+```
+
+Most Servers implement only part of this — capture the corrections and leave training to an external system (§7.3).
+
+**Facets:** AI-Base, AI-Dataset, AI-Learning.
+
+### 4.8 Someone asks what produced a decision
+
+Not an operation but a question, and the reason several members exist at all.
+
+```mermaid
+flowchart LR
+ R["a published result"] --> D["DeploymentType"]
+ D -->|"ModelUsed"| M["ModelType"]
+ M -->|Digest| A["the artefact"]
+ M -->|ImportedFrom| CR["catalogue entry"]
+ M -->|DerivedFrom| B["the model it came from"]
+ M -->|EvaluatedBy| E["EvaluationRunType"]
+ M -->|Card| K["ModelCardType"]
+```
+
+**Facets:** AI-Base, and whichever of AI-Catalogue, AI-Dataset and AI-Import the installation implements.
+
+---
+
+## 5 Overview and concepts
+
+### 5.1 Objects and relationships
+
+```mermaid
+flowchart LR
+ CAT["ModelRegistryType
catalogue"] -->|ModelImportJobType| M
+ M["ModelType"] -->|TrainedOn| D["DatasetType"]
+ P["DeploymentType"] -->|UsesModel| M
+ P -->|Source| S["ModelSourceType
somewhere else"]
+ P -.->|FallsBackTo| P2["DeploymentType
fallback"]
+ C["Client"] -->|Invoke| P
+ J["LearningJobType"] -.->|CandidateModel| M
+ J -.->|Dataset| D
+ J -.->|PromoteModel updates| P
+```
+
+A dataset trains a model; a deployment executes one; a client calls the deployment. Where the model runs somewhere this Server does not control, the deployment names a **source** that says how to reach it and what to do when it cannot. Where a model comes from a catalogue, an **import job** brings it — as a description, or as bytes. A learning job accumulates a new dataset, produces a candidate, and promotes it, at which point the deployment executes a different model and the cycle repeats.
+
+The four questions this arrangement is arranged to answer:
+
+| Question | Where it is answered |
+|---|---|
+| What is running, and can I audit it? | `ModelType`, `DatasetType`, §12.1 |
+| How do I run it? | `DeploymentType.Invoke` (§8) |
+| What if it is not here, or stops answering? | `ModelSourceType`, `FallbackPolicy` (§9) |
+| How did it get here, and may it be used? | `ModelImportJobType` (§10), `ModelCardType` (§11) |
+
+A dataset trains a model; a deployment executes one. A learning job accumulates a new dataset, produces a candidate, and promotes it — at which point the deployment executes a different model and the cycle repeats.
+
+`UsesModel` and `TrainedOn` are **references**, because they are structural. `Dataset`, `BaseModel` and `CandidateModel` on a learning job are **NodeId Properties**, because a job's relationships change as it runs and a reference set that churns is harder to observe than a value that changes.
+
+### 5.2 Binding from a consuming specification
+
+A specification that runs inference — a vision model, a condition-monitoring model — binds by holding a **`NodeId` Property** naming a `DeploymentType` instance. It does **not** take a `RequiredModel` on this NodeSet and does **not** define a ReferenceType into it.
+
+That keeps both specifications loadable alone. A Server that describes its deployment some other way names that node instead, and a Server that implements neither is unaffected. The cost is that the provenance chain of §12 is only available where both are implemented, which is why it is stated as a conformance condition rather than assumed.
+
+### 5.3 Model ownership
+
+This is the assumption the whole model rests on, so it is stated plainly.
+
+The equipment manufacturer does not supply the model. The operator or the system integrator does, and replaces it, and is answerable for it. A Server therefore **shall** describe the model it is *currently* running rather than the one it shipped with, and `PromoteModel` **shall** require an authorization distinct from the one that permits ordinary operation (§12.3).
+
+The consequence for a reader: every member of `ModelType` is about *this* artefact, and none of it is nameplate data that could have been printed at the factory.
+
+---
+
+## 6 Information model
+
+### 6.1 Type hierarchy
+
+| Type | NodeId | Subtype of |
+|---|---|---|
+| `AiRootType` | `ns=2;i=1001` | `BaseObjectType` |
+| `ModelType` | `ns=2;i=1002` | `BaseObjectType` |
+| `DatasetType` | `ns=2;i=1003` | `BaseObjectType` |
+| `DeploymentType` | `ns=2;i=1004` | `BaseObjectType` |
+| `AiJobType` (abstract) | `ns=2;i=1006` | `ProgramStateMachineType` (`i=2391`) |
+| `LearningJobType` | `ns=2;i=1005` | `AiJobType` |
+| `ModelImportJobType` | `ns=2;i=1007` | `AiJobType` |
+| `InferenceJobType` | `ns=2;i=1008` | `AiJobType` |
+| `ModelSourceType` | `ns=2;i=1009` | `BaseObjectType` |
+| `EvaluationRunType` | `ns=2;i=1014` | `BaseObjectType` |
+| `ModelCardType` | `ns=2;i=1015` | `BaseObjectType` |
+| `ModelRegistryType` | `ns=2;i=1010` | xRegistry `RegistryType` |
+| `ModelPublisherType` | `ns=2;i=1011` | xRegistry `GroupType` |
+| `ModelResourceType` | `ns=2;i=1012` | xRegistry `ResourceType` |
+| `DatasetResourceType` | `ns=2;i=1013` | xRegistry `ResourceType` |
+
+`LearningJobType` keeps NodeId `1005` although it now sits below `1006`. NodeIds here are **append-only**: a type that acquires a base type does not move, because renumbering to make the file read tidily would break every client that cached an identifier.
+
+### 6.2 `ModelType`
+
+`ModelType` describes one trained artefact: which it is, where it came from, and what it accepts and returns. It is the node a client reaches when it asks what produced a result, and the node an auditor reaches when it asks whether that artefact is the one that was approved. Its member set is aligned with the IDTA 02060 AI Model Nameplate submodel template, so an Asset Administration Shell can be populated from it without loss.
+
+An instance is created when a model becomes known to the Server — whether it was imported from a catalogue (clause 10), trained by a learning job (clause 7), or configured by hand — and it outlives any single deployment of it, because the same artefact may be executed in several places at once.
+
+`ModelId`, `Name`, `Version`, `Digest` and `DigestAlgorithm` are **Mandatory**. The first three because a model that cannot be named cannot be discussed; the last two because clause 12 depends on them, and a rule that depends on an Optional member is a rule a conformant Server can silently not satisfy.
+
+**`ModelId` carries the source system's own identifier, verbatim.** Where a model came from a catalogue or a remote endpoint, it is the string that system uses — the string a client would send back to reach the same model there. It is not derived from the other members and it is not reformatted, because its value is that two Servers integrating the same source produce the same one. Where the triple below cannot be recovered, `ModelId` is what remains comparable.
+
+**`Name` is a `LocalizedText` whose `Text` is the source's name for the model, carried across unchanged.** A Server **may** add a translation for display and **shall not** translate, reformat or prettify the `Text` itself. The type is `LocalizedText` because that is how this model types names and retyping it would break every implementation; what is localizable is the presentation, not the identity. Two Servers that fetched one model from two mirrors are meant to produce the same string, and a name adjusted for house style is a name that no longer matches.
+
+**`Publisher` names the organisation that produced the model**, not the one serving it. A hosted endpoint that reports its own operator as the owner has answered a different question, and a Server **shall not** publish the serving organisation there: a `Publisher` of `azure` or `aws` for a model somebody else trained defeats the purpose §6.2.1 gives it, which is recognising the same artefact across two installations that fetched it from different places. Where only the serving organisation is known, `Publisher` is left empty.
+
+Leaving it empty is the right answer more often than it looks. `Publisher` and `Version` are best-effort against a source that publishes one opaque identifier and nothing else, and a decomposition guessed from the shape of that identifier is worth less than an honest gap — it is a convention of the vendor's naming, not a field they promised, and it changes without notice. `ModelId` is the member that always holds.
+
+`TaskKind` is a **String**, not an enumeration. The set of things models do is not closed, and an enumeration would date faster than the models it describes.
+
+`LabelClasses` is an ordered array whose **index** is the contract. A consuming specification's class identifier refers to a position in it, so a Server **shall not** reorder it in place: a model whose class 3 silently becomes class 4 produces results that are wrong in a way nothing detects.
+
+#### 6.2.1 Model identity
+
+`Publisher` completes the `Publisher`, `Name`, `Version` triple by which every catalogue in practice identifies a model (§10.2). It is what makes the same model recognisable across two installations that fetched it from different mirrors — the digests will match, but only if someone already suspected the two were the same artefact, and the triple is what raises that suspicion.
+
+`ProvenanceUri` is the hand-off point to whatever system governs approval. This model records *what is deployed and where it came from*; who signed it off, against which release criteria, under what retention policy, is the business of the organisation's governance system and deliberately not modelled here.
+
+#### 6.2.2 Cost and precision
+
+`ParameterCount` is a crude proxy for what a model will cost to run, and is the one such figure that is universally published.
+
+`Quantization` names the numeric precision the artefact is stored in — `fp32`, `int8`, `fp8`. This is **not** a packaging detail. A quantized model is a different artefact that produces different answers, and treating it as a variant of the original is how a model evaluated at full precision ends up deployed at reduced precision without anyone re-measuring it. §11.3 requires the derivation to be stated as well.
+
+`SafetyPolicyUri` names the policy applied to this model's output, where one is. Like every URI here it is untrusted input (§12.2).
+
+`Card` reaches the `ModelCardType` of §11.1. The split is deliberate: the nameplate answers *which artefact is this*, the card answers *should this be running on my line*, and those are asked by different people at different times.
+
+#### 6.2.3 When the artefact appeared, and when it last moved
+
+`PublishedAt` records when the source first published the model, and a Server **shall not** substitute its own acquisition time — a Server that did would make every model it serves appear to date from its last restart, and the value is only useful because it is the source's.
+
+It answers for a model what `CreatedAt` (§6.3) already answers for a dataset, and the argument transfers unchanged: a model trained before a process change may no longer represent the line it runs on. It is also, for a source that publishes one opaque identifier and no decomposable version, frequently the only datum by which two models can be ordered at all.
+
+`LastModifiedAt` records when the artefact behind the model last changed at the source, and exists for one case in particular. §9.3 defines `FollowsRef`, where the artefact **can** change with nothing else changing, and §12.3.1 requires repointing to be treated as an authorization-bearing act, pointing at `AiJobType.RequestedBy` for the record. But a reference that moves *at the source* produces no job, so there is no `RequestedBy` and no `StartedAt` — and without this member the audit trail §12.3.1 demands cannot be constructed on the one path that clause exists to cover. **A Server whose deployment follows a mutable reference shall populate `LastModifiedAt`.**
+
+Neither is the Server's `SourceTimestamp`. That records when this Server acquired a value; after a restart and a re-read it says today for a model published two years ago.
+
+`Inputs` and `Outputs` carry `TensorSignatureDataType` (`ns=2;i=3050`) — name, element type, shape with `-1` for a dynamic axis, and an optional layout hint. This is what lets a client check that what it intends to send matches what the model expects, before it sends it.
+
+Clause 8 leaves the invocation payload opaque, so these signatures are the **only** machine-readable description of what a deployment will accept. A client that ignores them discovers a shape mismatch as a rejected call at run time; one that reads them discovers it at configuration time, which is the difference between a commissioning problem and a production one.
+
+### 6.3 `DatasetType`
+
+`DatasetType` describes the samples a model was trained or validated on. It exists so that a question asked about a model's behaviour — why it fails on a particular part, whether it has ever seen a condition — can be answered by looking at what it learned from, rather than by inference from its outputs. Its member set is aligned with the IDTA 02058 AI Dataset submodel template.
+
+It is read at two moments in practice: when a model is being reviewed for use, and when a model has failed in the field and someone is establishing whether the failure was foreseeable.
+
+`SourceKind` (`DatasetSourceEnum`, `ns=2;i=3004`) is `Real` 0, `Synthetic` 1 or `Mixed` 2, and is **Mandatory**. It is the provenance a reviewer needs when synthetic data is involved, and the one question about a dataset that cannot be answered by looking at it. `Mixed` is not a hedge — synthetic pre-training followed by real fine-tuning is the common industrial arrangement, and forcing it into either neighbouring value would misdescribe it.
+
+`SampleCount` and `CreatedAt` describe the scale and the vintage of the data, and both matter when a model is being judged: a classifier trained on four hundred samples and one trained on four million invite different amounts of trust, and a dataset assembled before a process change may no longer represent the line it is used on. `LabelClasses` names what the samples were labelled with. `ArtifactUri` says where the data itself can be obtained and `Digest` establishes that whatever is retrieved from there is the data this node describes — the same pairing, and the same reasoning, as on a model.
+
+`LabelClasses` carries the same index-is-the-contract rule as `ModelType`. A dataset whose class list disagrees in **order** with the model trained on it is not detectably wrong anywhere — every identifier resolves, every count is plausible, and every label is off by one.
+
+A dataset is a **sibling** of the model rather than a part of it. It outlives the models trained on it and is cited by several, which is why `TrainedOn` (§6.5) is a repeating reference and why the catalogue gives datasets their own resource type (§10.1).
+
+### 6.4 `DeploymentType`
+
+`DeploymentType` is a model made executable somewhere. Where `ModelType` describes an artefact, a deployment describes an arrangement for running it — on what hardware, at what location, under what latency expectation, with what happens when it cannot serve. One model may have several deployments and each names exactly one model (§6.5), which is what allows the same artefact to run at the edge and in a central service without the two being confused for one another.
+
+It is the node a client actually interacts with: every Method in clause 8 hangs here, and every member clause 9 adds is about this arrangement rather than about the artefact. Its member set is aligned with the IDTA 02059 AI Deployment submodel template.
+
+`InferenceLocation` (`InferenceLocationEnum`, `ns=2;i=3001`) is `OnServer` 0, `EdgeOffServer` 1, `Cloud` 2 or `InSimulator` 3, and is **Mandatory**.
+
+> This property changes **where the computation happens and therefore the trust boundary**. It changes nothing else — not the result contract, not the model's identity, not what a client does with the output. A client that branches on it for any reason other than latency, availability or trust has misread it.
+
+`AcceleratorKind` (`AcceleratorKindEnum`, `ns=2;i=3002`) states the class of device executing the model — `Cpu`, `Gpu`, `Npu`, `Fpga`, `Tpu` or `Other`. A client reads it to understand why two deployments of the same artefact do not perform alike, and an operator reads it when deciding where a newly imported model can reasonably be placed. `AcceleratorName` carries the specific part alongside it as free text, because an enumeration cannot keep pace with the accelerators that ship each year, and the part number is what a support engineer actually needs when a deployment behaves differently from an apparently identical one.
+
+`LatencyBudget` states the latency this deployment is expected to meet. It is written when the deployment is commissioned, by whoever knows what the process requires, and read continuously thereafter by anything watching for regression. Its value is in the comparison rather than the number: without a declared expectation, a deployment that has become three times slower is indistinguishable from one that was always slow, and the degradation is noticed only when something downstream fails.
+
+`BatchSize` reports the configured inference batch size, and exists mainly so that latency can be interpreted rather than merely measured. A large batch trades per-item latency for throughput deliberately, so a `LatencyBudget` breach on a batched deployment may mean nothing is wrong at all — a client that reads the budget without the batch size will raise alarms that have no fault behind them.
+
+#### 6.4.1 Operational members
+
+The members described so far establish what a deployment *is*. Using one draws on members that later clauses add, and it is worth seeing them together, because a client assembling a call reads across all four groups rather than working clause by clause.
+
+`Invoke`, `InvokeAsync` and `GetCapabilities`, with the `Capabilities` list beside them, are what a client calls and what it reads before calling. They are added by clause 8. A client that intends to use a typed profile consults `Capabilities` at configuration time; one that only ever sends an opaque payload can call `Invoke` without reading anything else.
+
+`Source`, `VersionBinding` and `BoundRef` describe where execution happens and whether the artefact behind it can change without notice. Clauses 8.2 and 8.3 define them. These are read once when a deployment is commissioned and again whenever an audit asks what was running at a given time, since a `FollowsRef` binding means the answer can differ between two moments with nothing else having changed.
+
+`FallbackPolicy`, `Reachability`, `ConsecutiveFailures`, `LastSuccessAt` and `RateLimit` describe whether the deployment is currently able to serve and what happens when it is not. Clause 9.4 defines them. A supervisory client subscribes to these rather than polling them, because the moment they change is precisely the moment it needs to act.
+
+`DataJurisdiction`, `EgressPermitted`, `RetainsInput` and `EgressPolicyUri` state where input data goes. Clause 9.5 defines them. They are read by whoever approves a deployment rather than by whoever calls it, and they are stated on the deployment rather than the model because the same model deployed twice can answer differently.
+
+`ApiDialect`, `EndpointDescriptionUri` and `RuntimeIdentity` describe the contract a caller must satisfy and what is currently behind it. §9.2 and §9.3 define them, and unlike the four groups above they are read *before the first call ever succeeds*, because a client that does not know what shape its `Payload` should take cannot make one.
+
+#### 6.4.2 What a caller must send
+
+`Invoke` takes an opaque `ByteString`, and §8.2 argues at length for keeping it opaque. That argument is about the payload's **contents**, and it leaves a question it does not answer: a client browsing an unfamiliar deployment can see that `Invoke` exists and has no way to learn whether the bytes should be a chat-completions request body, an inference protocol body, or something a vendor documents elsewhere.
+
+For a tensor deployment `Inputs` and `Outputs` answer it, which is why §6.2 calls them the only machine-readable description of what a deployment accepts. For every deployment whose contract is a JSON request body rather than a tensor set, they are empty and nothing answers it at all.
+
+`ApiDialect` (`ApiDialectEnum`, `ns=2;i=3007`) does, naming **which** contract the opaque bytes are expected to satisfy without typing what is in them. `EndpointDescriptionUri` says where that contract is documented, and is untrusted input under §12.2.
+
+**A Server shall populate `ApiDialect` on every deployment whose payload contract is not described by `Inputs` and `Outputs`**, and **should** populate `EndpointDescriptionUri` wherever `ApiDialect` is `Proprietary` — which is the same *should* §9.2 applies to a source, for the same reason: `Proprietary` with no description names nothing.
+
+This is the same enumeration §9.2 uses, and deliberately so. A deployment that federates a remote endpoint generally passes the payload through, so the contract a client sends to this Server and the contract this Server speaks onward are the same one, and giving them two vocabularies would invite them to disagree. Where they genuinely differ — a Server that translates — the deployment states what **it** accepts, because that is the one a caller has to satisfy.
+
+#### 6.4.3 Deployment state
+
+`State` (`DeploymentStateEnum`, `ns=2;i=3003`) reports the deployment as this Server holds it: `Inactive` when it is declared but not serving, `Ready` when it can serve and has no work in progress, `Active` while it is serving, `Degraded` when it is serving below the quality it was configured for, and `Faulted` when it cannot serve at all.
+
+It is **Mandatory** because availability decisions rest on it. A consuming specification deciding whether to route work to a deployment reads `State` and nothing else, and the learning loop of clause 7 uses it to establish whether a promoted model is actually in service. A member that carried those decisions while being omissible would leave a conformant Server unable to answer the question its clients most often ask.
+
+`Degraded` earns its place between `Active` and `Faulted`. A deployment that is answering but missing its `LatencyBudget`, or falling back to a slower accelerator, is neither healthy nor broken, and collapsing it into either neighbour would either hide a developing fault or stop a line that is still producing usable results.
+
+That comparison needs a published input, and `ObservedLatency` is it: the most recent inference latency this Server measured. `LatencyBudget` states what the deployment is *expected* to meet and is set by whoever commissioned it; `ObservedLatency` states what it *did*. **A Server that reports `Degraded` on latency grounds shall populate `ObservedLatency`**, so the state it publishes can be checked against the numbers it publishes rather than being taken on trust.
+
+Without it the rule above would be untestable — a Server could report `Degraded`, or fail to, and nothing a client could read would distinguish a correct implementation from an incorrect one. A normative statement that cannot be observed to be satisfied or violated is not a requirement.
+
+A client is not obliged to use it. End-to-end latency is measurable from the calling side, and a client that measures its own sees the transport as well. What `ObservedLatency` adds is the Server's own view of the execution site, which is the half a caller cannot separate out — and against a federated deployment it is the only view of the remote leg that exists.
+
+Where a deployment executes somewhere this Server does not control, `State` is not the whole picture — a correctly configured deployment can be unable to reach its execution site. Clause 9 adds `Reachability` for that, and §9.4 sets out how the two combine.
+
+### 6.5 `UsesModel` and `TrainedOn`
+
+A `DeploymentType` instance **shall** have **exactly one** `UsesModel` reference, and its target **shall** be a `ModelType` instance.
+
+This is the only defined path from a running deployment to the artefact its results depend on, and §12.1's provenance argument is a walk along it. Zero references breaks the chain; more than one makes "which model produced this?" unanswerable, which is the question the chain exists to answer.
+
+`TrainedOn` links a model to a dataset it was trained or validated on. It is optional and may repeat: a model whose training data cannot be named is a model whose behaviour cannot be explained, but not every installation holds that information.
+
+### 6.6 `AiJobType`
+
+Every long-running operation in this model — learning, importing a model, inference that does not return while the caller waits — derives from `AiJobType`, which derives from the OPC 10000-10 `ProgramStateMachineType`.
+
+That base supplies the lifecycle (`Ready`, `Running`, `Suspended`, `Halted`), the transition events, and the `Start`, `Suspend`, `Resume` and `Halt` Methods. None of it is redefined here. A hand-rolled state variable would have had to reinvent the transition events to be observable, and would have been observable *differently* from every other program in a Server.
+
+`AiJobType` adds `JobId`, `LastError`, `StartedAt`, `FinishedAt`, `Progress` and `RequestedBy`.
+
+`Progress` is a fraction from 0.0 to 1.0. A Server **shall not** report a value it is guessing: null is informative, a fabricated 0.5 is not, and a progress bar that is wrong is worse than one that is absent because it is acted on.
+
+`RequestedBy` records the identity that started the job, at the moment it started. §12.3 requires it for any job that can promote a model — an authorization check that leaves no record answers "was this allowed" but not "who did it".
+
+**The lifecycle and the phase are different questions.** `LearningJobType.State` says what stage the loop is in; the inherited `CurrentState` says whether the program is running. A Server **shall** keep them consistent: a job whose `State` is `Failed` **shall not** report a `CurrentState` of `Running`.
+
+Annex A is the authoritative node reference and carries every member with its DataType, ValueRank and ModellingRule.
+
+---
+
+## 7 The learning loop (normative)
+
+`LearningJobType` exists so that corrections arriving from a consuming application have somewhere to accumulate and a defined path into a new model version.
+
+```mermaid
+stateDiagram-v2
+ [*] --> Idle
+ Idle --> Collecting: StartCollection
+ Collecting --> Labelling: StopCollection
+ Collecting --> Training: TriggerTraining
+ Labelling --> Training: TriggerTraining
+ Training --> Validating
+ Validating --> Ready
+ Validating --> Failed
+ Ready --> Promoted: PromoteModel
+ Promoted --> Collecting: StartCollection
+ Training --> Failed
+ Failed --> Collecting: StartCollection
+```
+
+`LearningJobStateEnum` (`ns=2;i=3005`) carries exactly these eight states.
+
+| From | Trigger | To |
+|---|---|---|
+| `Idle` | `StartCollection` | `Collecting` |
+| `Collecting` | `StopCollection` | `Labelling` |
+| `Collecting`, `Labelling` | `TriggerTraining` accepted | `Training` |
+| `Training` | Server: training finished | `Validating` |
+| `Validating` | Server: candidate met acceptance criteria | `Ready` |
+| `Validating` | Server: candidate rejected | `Failed` |
+| `Ready` | `PromoteModel` | `Promoted` |
+| `Promoted`, `Failed` | `StartCollection` | `Collecting` |
+| `Training`, `Validating` | Server: error | `Failed` |
+
+Transitions marked *Server* are driven by the Server or its training backend; the rest are Method-driven. A Server **shall not** perform a transition that is not in this table, **shall** populate `LastError` on entry to `Failed`, and **shall** have `CandidateModel` non-null on entry to `Ready` — a `Ready` job with nothing to promote cannot be acted on.
+
+`Promoted` and `Failed` both return to `Collecting`, which is what makes this a loop rather than a one-shot. A job that promoted a model last month is the same job that starts gathering evidence for the next one.
+
+### 7.1 Method behaviour and StatusCodes (normative)
+
+`StartCollection` and `StopCollection` are **idempotent**: calling either in the state it would move to is `Good` and changes nothing. Retrying after a lost response is otherwise indistinguishable from a second request.
+
+`TriggerTraining` returns `Accepted`. It returns `Accepted = false` **with `Good`** where the request was valid but the Server queued nothing — an external training system declined it, for instance — and `LastError` **shall** then carry the reason. This is not an error: the request was understood and refused, and a Bad StatusCode would tell a caller to retry something that will be refused again.
+
+`PromoteModel` takes a `Deployment` or null. **Null means every deployment fed by this job**, and a Server **shall** promote to all of them or to none. `PromotedModel` returns the model now in use, which is the same node in either case because it identifies the model rather than the deployment; a caller needing to know which deployments changed browses their `UsesModel` references afterwards.
+
+| StatusCode | Condition |
+|---|---|
+| `Bad_InvalidState` | `StartCollection` when `State` is not `Idle`, `Collecting`, `Promoted` or `Failed`; `TriggerTraining` when `State` is not `Collecting` or `Labelling`; `PromoteModel` when `State` is not `Ready` |
+| `Bad_NothingToDo` | `TriggerTraining` when `SamplesCollected` is 0 |
+| `Bad_NotFound` | `PromoteModel` when `Deployment` is non-null and does not resolve, or when `CandidateModel` is null |
+| `Bad_UserAccessDenied` | The caller is not authorized; `PromoteModel` requires the distinct authorization of §12.3 |
+
+**A Server may implement only part of this.** A Server that captures corrections and leaves training to an external MLOps system implements `StartCollection` and `StopCollection`, drives the state to `Labelling`, and stops. The state machine is the same either way, and a client reads `State` to learn how far this Server goes rather than inferring it from which Methods exist.
+
+`SamplesCollected` counts what has accumulated, including corrections fed back. `LastError` is the diagnostic for `Failed`, is for a human, and **shall not** be parsed.
+
+**Promotion is the operation that matters.** `PromoteModel` makes the candidate the model deployments use — it changes what the equipment does without changing anything a reader of the address space would notice, which is exactly the change that needs a separate permission (§12.3).
+
+A null `Deployment` argument means *every* deployment fed by this job. A Server **shall** promote to all of them or to none: a partial promotion leaves two lines judging the same parts by different models, which is a fault that shows up as an inexplicable disagreement between stations rather than as an error anywhere.
+
+### 7.2 Relationship to models and deployments
+
+The loop is the **producing** half of this specification; clauses 8 to 9 are the consuming half, and three joins connect them.
+
+`BaseModel` and `CandidateModel` are `ModelType` instances like any other, so a candidate carries the same `Digest`, the same `Card` and the same lineage obligations as a model that arrived from a catalogue. A model that a Server trained is not privileged over one it imported — §11.3 requires the candidate to state `DerivedFrom` the base it started from, for the same reason a quantized model must.
+
+Promotion **should** be gated on an `EvaluationRunType` (§11.2) whose `Passed` is true. This specification does not require it, because a Server that captures corrections and hands training to an external system may legitimately never see an evaluation — but a Server that promotes without one has no recorded answer to *why was this allowed*, and the question is asked after failures rather than before them.
+
+Where the promoted model backs a deployment whose `VersionBinding` is `FollowsRef` (§9.3), promotion and repointing are two routes to the same outcome. §12.3.1 requires both to be authorized alike.
+
+### 7.3 Partial implementation
+
+The state machine describes the whole loop; almost no Server implements the whole loop.
+
+A Server that only captures corrections implements `StartCollection` and `StopCollection`, drives `State` to `Labelling`, and stops. One that also promotes but trains elsewhere implements `PromoteModel` and lets `Training` and `Validating` be driven by its MLOps backend. Both are conformant to **AI-Learning** provided the transitions they *do* perform are the ones in §7.
+
+This is why `State` is read rather than inferred from which Methods exist. A client that probed for Methods would learn what a Server can be asked to do; reading `State` tells it how far this job actually got, which is the question it has.
+
+---
+
+## 8 Inference (normative)
+
+### 8.1 Location independence
+
+`DeploymentType.Invoke` runs inference and returns the result, and **one call serves wherever the model runs**.
+
+**Its signature does not change with `InferenceLocation`.** A model executing in the Server's own process and one executing in a remote service are called identically — same Method, same arguments, same outputs, same meanings. This is the single most important property in this clause, and it is not an aspiration: serving runtimes that run on a workstation and the hosted services they mirror already expose the same contract, differing only in where the request is addressed and how it is authenticated. A specification that made the call shape depend on the location would be describing an accident of deployment as though it were a property of the model.
+
+What the location *does* change is the trust boundary, the latency and what fails when the network does. Those are clause 9's subject.
+
+### 8.2 Payload and envelope
+
+`Payload` is a `ByteString` and `ContentType` is its media type. This specification does not say what is inside.
+
+That is not vagueness, it is the boundary. What goes into a model and what comes out is domain vocabulary — an image and a set of detections, a spectrum and a fault class, a maintenance history and a remaining-life estimate. An envelope that tried to type it would have to be extended by every domain that ever adopted this model, and the first domain to need something unforeseen would have to fork it.
+
+What this specification *does* fix is everything around the payload, because none of it is domain-specific and all of it is got wrong when left to each implementer:
+
+| Output | Why it is in the envelope |
+|---|---|
+| `ModelUsed` | Which model **actually** answered |
+| `Usage` | What the call consumed |
+| `FinishReason` | Whether the answer is complete |
+| `SafetyAssessment` | Whether anything was withheld |
+| `RetryAfter` | Whether, and when, to try again |
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as Client
+ participant D as DeploymentType
+ participant M as ModelType
+ C->>D: Invoke(Payload, ContentType, Parameters, Timeout)
+ D->>D: reject any unsupported Parameter (7.3)
+ D-->>C: ResponsePayload + ResponseContentType
+ D-->>C: ModelUsed, Usage, FinishReason
+ D-->>C: SafetyAssessment, RetryAfter
+ C->>M: browse ModelUsed for Digest (11.1)
+```
+
+#### 8.2.1 `ModelUsed`
+
+A Server **shall** return the model that actually produced the response, which is **not** necessarily the one the deployment names at the time the client looks.
+
+Two mechanisms defined here can move it between the call and the read: a fallback (§9.4) answers from a different deployment entirely, and a `FollowsRef` binding (§9.3) can be repointed at a new version. In both cases the deployment's current model is the *wrong* answer to "what produced this result", and it is wrong in the direction that matters — it names a model that looks plausible.
+
+The provenance chain of §12.1 therefore walks `ModelUsed`, not the deployment.
+
+#### 8.2.2 `FinishReason`
+
+A truncated answer is not a complete one, and nothing else in the response says which it is.
+
+`FinishReason` (`FinishReasonEnum`, `ns=2;i=3006`) is `Stop`, `Length`, `ToolCall`, `Filtered`, `Cancelled` or `Error`.
+
+Only `Stop` means the model finished saying what it had to say. `Length` means output hit a budget and **the result is incomplete**; `Filtered` means a safety policy withheld it; `ToolCall` means the model is waiting for something the caller must supply; `Cancelled` and `Error` speak for themselves.
+
+A client that branches only on the StatusCode will accept a `Length` response as final, because nothing failed. A Server **shall** populate `FinishReason` on every response, including successful ones, so that the distinction is available without inference.
+
+#### 8.2.3 `Usage`
+
+Accounting is deliberately not expressed in tokens.
+
+`UsageDataType` (`ns=2;i=3052`) carries `UnitKind`, `InputUnits`, `OutputUnits` and `TotalUnits`.
+
+The counts are deliberately **not** named tokens. A token is one accounting unit among several: a model that consumes images, audio seconds or sensor samples meters the same thing in a different unit, and a field called `InputTokens` on such a deployment is either empty or lying. `UnitKind` names the unit — `tokens`, `images`, `samples`, `seconds` — and the three counts are in it.
+
+`TotalUnits` is **not** required to be the sum of the other two. Caching, deduplication and shared prefixes mean the metered total legitimately differs from the arithmetic one, and a client that recomputes it will disagree with the bill.
+
+**Not every execution site meters at all.** A tensor predict contract returns output tensors and nothing that could be counted, and an in-process runtime returns what the library returns. A Server that supplied a count on such a site's behalf would be publishing a measurement it did not take.
+
+An **empty `UnitKind` means the call was not metered**. Where `UnitKind` is empty a Server **shall** set `InputUnits`, `OutputUnits` and `TotalUnits` to zero, and a client **shall not** read those zeros as measured quantities. A Server **shall not** report a non-empty `UnitKind` alongside counts it did not obtain from the execution site.
+
+The sentinel is the empty unit rather than a zero count because the counts cannot carry it: they are `UInt64`, so a Server with nothing to report and one that metered nothing would otherwise encode identically. Naming the unit is what makes the difference between *no measurement* and *a measurement of none* legible, and that difference is the whole of what a client reading `Usage` is entitled to know.
+
+#### 8.2.4 Payload size, and why `Invoke` is not the general case
+
+`Payload` is a `ByteString`, and a `ByteString` is bounded three times over: by the Server's `MaxByteStringLength`, by the channel's negotiated `MaxMessageSize`, and by the Session's `MaxResponseMessageSize`. **This model does not get to choose any of them.** An image, a point cloud or a window of high-rate samples exceeds them routinely, and a call that cannot carry its input is not a call.
+
+So `Invoke` is the **shortcut**, not the general path. `BeginTransfer` is the general path.
+
+**`MaxInlinePayloadSize` is Mandatory on every deployment** and states the largest request or response it will carry inline. A client reads it *before* calling rather than discovering the bound from a rejection, which matters because the three limits above are not all visible to a client — a Server **shall not** publish a value larger than the smallest of them permits. Zero means the deployment accepts nothing inline and `BeginTransfer` is the only way in.
+
+**A client that does not know its payload sizes in advance should use the transfer path from the outset.** Nothing is lost by doing so: the transfer path carries the same envelope and answers the same questions, and a client that starts there never has to discover mid-deployment that a payload has outgrown the shortcut.
+
+##### The exchange
+
+`InferenceTransferType` (`ns=2;i=1017`) carries one exchange. `Request` and `Response` are Part 5 **`FileType`** objects, so the client opens the request, writes it in chunks of its own choosing, and closes it; after `Execute`, the response is read the same way.
+
+Nothing here invents a transfer protocol. OPC UA already has one, every client already implements it, and a bespoke chunking scheme would be a second thing to get wrong.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as Client
+ participant D as DeploymentType
+ participant T as InferenceTransferType
+ C->>D: read MaxInlinePayloadSize
+ C->>D: BeginTransfer(ContentType, RequestSize)
+ D-->>C: Transfer, Accepted
+ C->>T: Request.Open / Write... / Close
+ Note over T: State Building -> Ready
+ C->>T: Execute
+ T-->>C: Accepted
+ Note over T: State Executing -> Completed
+ C->>T: read State, ModelUsed, Usage, FinishReason
+ C->>T: Response.Open / Read... / Close
+```
+
+`TransferId` names the exchange, and is Mandatory for the same reason every other identifier here is: a client holding several concurrent exchanges needs to say which one it means in a log or a support call, and the NodeId alone is not something a human carries around.
+
+`State` (`TransferStateEnum`, `ns=2;i=3014`) is `Building`, `Ready`, `Executing`, `Completed`, `Failed` or `Expired`. A client reads it rather than inferring progress from which Methods have succeeded, because a transfer that failed mid-write and one that was never started look alike from outside.
+
+`ExpiresAt` is when the Server may reclaim an exchange that has not completed. A client that abandons one would otherwise hold Server resources until its Session ends, and a Server that never reclaimed them would be one denial of service away from unusable. `Abort` releases an exchange early, and a client that has stopped caring about a response **should** call it rather than waiting out the expiry.
+
+##### When the *answer* is too large
+
+The awkward case is not a large request — the client knows its own input size. It is a request that fits and produces a response that does not.
+
+`Invoke` therefore returns **`TransferRequired`** and **`Transfer`**. Where `TransferRequired` is true, `ResponsePayload` is empty and **the work is not lost**: inference ran, and `Transfer` names the exchange to read the response from.
+
+A Server **shall not** fail such a call. Failing it would discard work that has already been done and, worse, would tell the caller nothing about why — a client would see an empty payload and conclude the model returned nothing, which is a different and wrong answer.
+
+##### Streaming
+
+Where output is produced progressively rather than merely being large, §8.5 applies instead: the Server publishes it through a Subscription, and **AI-Stream** optionally carries it over a data channel. The distinction is whether the client wants the answer *as it forms* — a transfer delivers one complete response, however big.
+
+### 8.3 Parameters
+
+An ignored parameter is worse than a rejected one, which is the whole of the rule below.
+
+`Parameters` is an array of `KeyValuePair`, carrying whatever the deployment accepts — a sampling temperature, an output-length bound, a decoding seed.
+
+A Server **shall** reject a parameter it does not support, and **shall not** ignore it.
+
+This is the one rule in the clause that costs implementers something, and it is worth the cost. A caller that sets a determinism seed and has it silently dropped believes its results are reproducible when they are not. A caller whose safety-relevant bound is discarded believes a limit is in force. Silent acceptance converts a caller's explicit instruction into a false belief, and there is no later point at which the caller can discover it.
+
+### 8.4 Capabilities
+
+Capabilities are asked for, never assumed from the kind of model behind a deployment.
+
+`Capabilities` (`CapabilityDataType`, `ns=2;i=3053`) is a list of names with a supported flag — `chat`, `embeddings`, `streaming`, `tool-call`, `structured-output` and whatever else a deployment offers.
+
+It is an **open list of strings, not an enumeration**, for the same reason `TaskKind` is: the set of things models can do is not closed, and an enumeration frozen at publication would be the first part of this specification to date. A client that meets a capability name it does not recognise is in exactly the position of one that meets an enumeration value added after it shipped, and no worse.
+
+`GetCapabilities` re-reads them from the execution site. It exists because a remote endpoint's capabilities change without anything in this address space changing — the cached list on the deployment can be stale in a way nothing else here can.
+
+### 8.5 Incremental results
+
+Where a deployment produces output progressively, a Server **shall** publish it by updating a Variable that a client subscribes to. There is no streaming Method: OPC UA already has the mechanism, and a Method that returned repeatedly would be a second one.
+
+Where the payload is large or the rate is high enough that Subscription overhead dominates, a Server **may** additionally offer the stream over a data channel; that is the **AI-Stream** facet (§13.2) and it is entirely optional. A Server that implements neither answers only through `Invoke`, and is fully conformant.
+
+### 8.6 Asynchronous inference
+
+`InvokeAsync` submits a request and returns immediately with the `InferenceJobType` (`ns=2;i=1008`) instance that will carry the result. The client subscribes to that job rather than polling it.
+
+This is not a convenience. A batch scored overnight and an analysis over months of recorded data are ordinary industrial requests, and modelling them as a Method that blocks for hours would hold a Session open for the duration and lose the work if it dropped. `InferenceJobType` derives from `AiJobType` (§6.6), so it is observed exactly like every other long-running operation here.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as Client
+ participant D as DeploymentType
+ participant J as InferenceJobType
+ C->>D: InvokeAsync(Payload or PayloadUri, ContentType, Parameters)
+ D-->>C: Job (NodeId)
+ C->>J: Subscribe
+ J-->>C: CurrentState Running, Progress
+ Note over J: the Part 10 lifecycle, not a poll loop
+ J-->>C: CurrentState Halted, FinishReason
+ C->>J: read ResponsePayload, ModelUsed, Usage
+```
+
+`InferenceJobType` carries `RequestPayload` and `RequestContentType`, `ResponsePayload` and `ResponseContentType`, and the same `ModelUsed`, `Usage`, `FinishReason` and `SafetyAssessment` that `Invoke` returns — the asynchronous path answers the same questions as the synchronous one, which is what makes it a path and not a different feature.
+
+That parity has to extend to size, and §8.6.1 is where it does. The jobs this clause exists for are the ones most likely to produce a result that will not fit in a call, so an asynchronous path bounded by the limits §8.2.4 says this model does not choose would be a path that fails exactly where it was needed.
+
+#### 8.6.1 A payload too large to carry, or already somewhere else
+
+Two different problems, and the model answers them separately because they have different remedies.
+
+**A result too large to return inline** is the problem `Invoke` solves with `TransferRequired` and `Transfer`, and `InferenceJobType` carries the same pair on the same terms: `TransferRequired` true means `ResponsePayload` is empty, the work is **not** lost, and `Transfer` names the `InferenceTransferType` to read it from. `MaxInlinePayloadSize` bounds both paths — it is a property of the deployment, not of the Method that happened to be called.
+
+**Data that never needed to move** is a different problem. A batch already sitting in the plant's object store, or a result the execution site writes to storage of its own, is not made smaller by chunking; carrying it through the Session copies it twice for no benefit and makes the Session the bottleneck for both copies.
+
+So `Invoke` and `InvokeAsync` both take a `PayloadUri`, and a Server **shall** accept exactly one of `Payload` and `PayloadUri` and **shall** reject a call supplying both or neither — the same exactly-one rule §10.2 applies to `Source` and `Registry`, for the same reason: two ways of saying where the input is, and a call that used both would not say which one was read. `InferenceJobType.RequestUri` records what was actually submitted, and `ResponseUri` names where the execution site wrote the result when it returns a location rather than bytes.
+
+This is the model's existing idiom rather than a new one. §1.2 already says this specification does not carry artefacts — `ArtifactUri` says where the bytes are — and §10.3's `Federate` mode is the same choice made about a model instead of a payload.
+
+Two obligations come with it, both inherited rather than invented. A `PayloadUri`, `RequestUri` or `ResponseUri` is **untrusted input** under §12.2 and subject to the same resolver policy as every other URI here. And it is an **egress question** under §9.5: a location the execution site reads is a location the input data reaches, so a deployment whose `EgressPermitted` is false **shall not** accept a `PayloadUri` naming somewhere outside the operator's boundary. A URI is a quieter way to move data than a payload, which is exactly why it needs saying.
+
+---
+
+## 9 Consuming a model hosted elsewhere (normative)
+
+### 9.1 `ModelSourceType`
+
+A URI says where to send bytes and nothing else that calling a remote model requires.
+
+A deployment whose `InferenceLocation` is not `OnServer` executes somewhere the Server must reach over a network. Naming that place is necessary and nowhere near sufficient: a Server holding only a URI knows where to send bytes and nothing about what shape they should take, how to prove it is entitled to send them, what the far end can do, or what to do when it stops answering.
+
+`ModelSourceType` (`ns=2;i=1009`) carries the rest. A deployment names one through its `Source` Property.
+
+| Member | The question it answers |
+|---|---|
+| `EndpointUri` | Where |
+| `ApiDialect` | In what shape |
+| `AuthenticationKind`, `CredentialReference`, `TokenAudience` | With what proof of entitlement |
+| `Capabilities` | Able to do what |
+| `Reachability`, `LastSuccessAt`, `ConsecutiveFailures`, `RateLimit` | Answering, or not |
+
+`SourceId` names the source, and is Mandatory for the same reason every other identifier here is: a source that cannot be named cannot be referred to by the deployment that uses it or by the import job that pulls from it.
+
+### 9.2 Wire contract and authentication
+
+`ApiDialect` (`ApiDialectEnum`, `ns=2;i=3007`) is `OpcUaInference`, `RestChatCompletions`, `OpenInferenceProtocol`, `TensorRemoteProcedure`, `EmbeddedRuntime` or `Proprietary`.
+
+These name **the contract the remote endpoint speaks**. `OpcUaInference` is another Server implementing this specification; `RestChatCompletions` is the de-facto REST contract for chat and embeddings that most serving runtimes expose, including ones that run on a single workstation — named here for what it does rather than for whoever published it first, because a literal in a standard should not be an advertisement; `OpenInferenceProtocol` is the KServe-derived predict contract; `TensorRemoteProcedure` covers the tensor-oriented RPC contracts of dedicated inference servers; `EmbeddedRuntime` is an in-process runtime reached through a library rather than a socket. `Proprietary` is an honest admission, and a Server using it **should** populate `EndpointDescriptionUri` — otherwise nothing in the address space says how the endpoint is called.
+
+**How an OPC UA client calls this Server is always §8** — one Method, one opaque payload, one envelope, whatever the source speaks. What the dialect on a *source* does not tell a client is what to put in that payload, and §6.4.2 puts the same enumeration on `DeploymentType` to answer that. The two are read by different parties for different purposes: the source's dialect is what this Server must speak outward, the deployment's is what a caller must speak inward, and a Server that translates between them publishes two different values. A Server that passes the payload through publishes the same value twice, which is not duplication so much as the honest answer given twice.
+
+The literals classify the contract **this Server speaks to that endpoint**, not everything the endpoint could offer. A runtime reached in-process is `EmbeddedRuntime` and the same runtime reached over its own loopback HTTP server is `RestChatCompletions`; a hosted endpoint reached through its OpenAI-compatible surface is `RestChatCompletions` and the same host reached through its native API is `Proprietary`. The value describes the integration, so it is answerable, and a Server that changes how it calls an endpoint changes it.
+
+Where a source serves **only as a catalogue** — §10's import reads from it and nothing calls `Invoke` through it — `ApiDialect` is `Proprietary` and `EndpointDescriptionUri` is populated. The member's value domain is inference contracts, a catalogue speaks none of them, and `Proprietary` is the accurate answer rather than a shortcoming: it says there is no inference contract here to recognise, and the description URI says what there is instead.
+
+`AuthenticationKind` (`AuthenticationKindEnum`, `ns=2;i=3008`) is `Anonymous`, `ApiKey`, `BearerToken`, `WorkloadIdentity` or `MutualTls`. `WorkloadIdentity` is preferred wherever the hosting platform offers it, because it is the only one of the five under which no secret is stored anywhere for an attacker to read.
+
+**It classifies the credential the Server stores, not the handshake it performs.** That is what makes it answerable against endpoints whose handshakes have nothing in common. Where a handshake is driven by an identity the platform assigns and no secret is stored, it is `WorkloadIdentity` whatever token the wire ultimately carries; where a secret is stored, it is `ApiKey` or `BearerToken` according to what the stored thing is. A request-signing scheme is therefore `WorkloadIdentity` when an assigned role signs it and `ApiKey` when a stored key does — one scheme, two values, because the question is what an attacker could steal.
+
+Read as a handshake classifier the member would be unanswerable for most real endpoints, and the five literals are deliberately not a taxonomy of handshakes. A source whose handshake a client genuinely needs described names it through `EndpointDescriptionUri`.
+
+**`CredentialReference` is a name, never a secret.** It identifies the credential in whatever store the Server uses. A Server **shall not** expose credential material through any Attribute of any node in this model, and a client that reads `CredentialReference` learns which credential is in use and nothing about what it is. This is stated as a prohibition rather than left implicit because the address space is a browsable, subscribable, historisable surface, and a secret placed in it is not merely readable — it is archived.
+
+### 9.3 Version binding
+
+`VersionBinding` (`VersionBindingEnum`, `ns=2;i=3010`) is `Pinned` or `FollowsRef`.
+
+A **`Pinned`** deployment names one immutable model version. The artefact behind it cannot change without an observable change to the deployment.
+
+A **`FollowsRef`** deployment names a mutable pointer — a branch, a channel, a "latest" alias — in `BoundRef`. The artefact behind it **can** change with nothing else changing.
+
+That second case is a promotion (§7) that nobody called `PromoteModel` for. It has the same effect: what the equipment decides changes, and no reader of the address space sees a structural difference. So §12.3's requirement applies to it unchanged — a Server **shall** treat repointing a followed reference as an authorization-bearing act, not as configuration.
+
+Stating this structurally, rather than as an upgrade-policy setting, is deliberate. What a client needs to know is whether the artefact can move under it. That is a property of the binding. When someone *intends* to move it is a schedule, and a schedule is not something a client can check.
+
+#### 9.3.1 What `Pinned` is worth, and what `RuntimeIdentity` adds
+
+`Pinned` says the artefact cannot change without an observable change to the deployment. How much that is worth depends on what this Server can actually verify, and `DigestProvenance` (§12.1.1) is where a client reads the answer.
+
+Where `DigestProvenance` is `ComputedByServer` or `VerifiedOnStage`, the Server holds the bytes and the guarantee is its own. Where it is `NotAvailable`, the deployment is pinned to a **name** the source promises to hold stable, and `Pinned` records that promise rather than this Server's verification. Both are legitimate; they are not the same assurance, and a Server **shall not** represent the second as the first — which it does not have to do explicitly, because the two are already distinguishable by reading one member.
+
+`RuntimeIdentity` is what closes the remaining gap. An artefact that has not changed can still be served by a different runtime build, a different engine compilation or a different accelerator arrangement, and produce different numbers for the same input. Where the execution site publishes an identity for its serving configuration, `RuntimeIdentity` carries it: opaque, compared only for equality, never parsed — the same contract `Digest` has.
+
+**A change to `RuntimeIdentity` is an observable change to the deployment**, and that is what makes the sentence at the top of this clause true rather than aspirational. Under a `Pinned` binding it is often the *only* observable change available, because the model did not move and nothing else in the address space did either.
+
+It answers a question asked long after the fact. An investigation opened in September into parts built in March walks §12.1's chain, reaches a `Digest` that is empty for good reason, and finds `VersionBinding` `Pinned` — and concludes nothing changed. Historising `RuntimeIdentity` makes *did the serving stack move between March and September* answerable through `HistoryRead`. It does not identify which build served an individual call, and a Server **shall not** be read as claiming that; concurrent calls during a rollover can straddle a change. The coarser question is the one that gets asked.
+
+### 9.4 Availability and fallback
+
+This is the question a plant asks that an inference API does not answer, because an inference API can assume its caller is willing to wait. A line is not.
+
+`FallbackPolicy` (`FallbackPolicyEnum`, `ns=2;i=3009`) is Mandatory on every deployment and states what the Server does when this one cannot serve:
+
+- **`Fail`** — report the failure and produce nothing. This is the safe default: a caller told that nothing happened can decide for itself, and deciding is often its job.
+- **`HoldLast`** — keep reporting the most recent successful result. Legitimate only where a stale answer is safe, and the caller **shall** be able to establish the staleness, for which `LastSuccessAt` is sufficient. A Server **shall not** present a held result as fresh.
+- **`FallBackTo`** — route to the deployment named by the `FallsBackTo` reference. The answer then comes from a **different model**, and `Invoke` **shall** report that model in `ModelUsed`. A fallback that answered without saying so would break the provenance chain precisely when it matters most.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as Client
+ participant P as Primary DeploymentType
+ participant S as Fallback DeploymentType
+ C->>P: Invoke
+ P--xP: execution site does not answer
+ P->>P: Reachability Unreachable, ConsecutiveFailures++
+ alt FallbackPolicy = FallBackTo
+ P->>S: route the call
+ S-->>P: result from a DIFFERENT model
+ P-->>C: response, ModelUsed = the fallback's model
+ else FallbackPolicy = Fail
+ P-->>C: failure, nothing produced
+ else FallbackPolicy = HoldLast
+ P-->>C: last successful result, LastSuccessAt shows its age
+ end
+```
+
+The `FallBackTo` branch is the one that needs care: the caller asked nothing different and got an answer from another model, so `ModelUsed` is the only thing that says so.
+
+`FallsBackTo` **shall not** form a cycle. A Server **shall** reject a configuration that closes one rather than discovering it at the moment of failure, which is the worst possible moment.
+
+`Reachability` (`ReachabilityEnum`, `ns=2;i=3013`) is `Unknown`, `Reachable`, `Unreachable` or `Throttled`. `Throttled` is separated from `Unreachable` deliberately: they look alike from the outside and call for opposite responses. An unreachable endpoint should be failed over; a throttled one will serve again shortly and failing it over merely moves the load. `RateLimit` (`RateLimitDataType`, `ns=2;i=3056`) carries `UnitKind`, `Limit`, `Remaining`, `Interval` and `RetryAfter` so a client can tell "the model said no" from "the quota said no".
+
+`ListModels` enumerates what the source offers, returning a `ModelReferenceDataType` for each. It takes a `Filter` and a `MaxResults` because a public catalogue holds more models than any client wants to page through, and a Method that could only return everything would be unusable against exactly the sources this clause exists to reach. It is Optional: a source that serves one known model needs no catalogue.
+
+A cap alone is not enough, and `ContinuationPoint` is why. `MaxResults` bounds the response and, on its own, puts every entry past it permanently out of reach — against a public catalogue that is most of them, which turns the member meant to make the Method usable into the one that truncates it. A client passes an empty `ContinuationPoint` on the first call and the value it received on each call after, and the enumeration is complete when the returned one is empty. That is how a client knows to stop, rather than by comparing a count against a bound it set itself and cannot distinguish from a source that happened to have exactly that many.
+
+`TestConnection` probes the endpoint and updates `Reachability`. It exists so a commissioning engineer can establish that credentials and network policy are right **before** production traffic depends on them, rather than learning it from the first failed inference.
+
+### 9.5 Data residency and egress
+
+Where the data goes is a different question from who can read it, and only the first is what a plant is asking.
+
+Encryption answers who can read data in flight. It does not answer where the data went, and that is the question a plant is actually asking.
+
+Three members on `DeploymentType` answer it, and all three are about the deployment rather than the model, because the same model deployed twice can give different answers:
+
+- **`DataJurisdiction`** is Mandatory and names where input is processed, in whatever scheme the operator uses — a site, a legal jurisdiction, a named zone. This specification does not fix the vocabulary because the operator's obligations do.
+- **`EgressPermitted`** is Mandatory and states whether calling this deployment sends input outside the operator's boundary. A Server **shall** set it true for every deployment whose `InferenceLocation` is `Cloud`, and **shall not** set it false because the channel is encrypted.
+- **`RetainsInput`** states whether the far end keeps input after serving the request — for provider-side logging, for evaluation, for training. **Unknown is not a value.** A Server that cannot establish the answer **shall** report `true`, because the assumption that keeps data in is the one that is safe to be wrong about.
+
+`EgressPolicyUri` names the governing policy for a human.
+
+These three members are **end-to-end, not next-hop**. `DataJurisdiction` names where input is ultimately processed; `EgressPermitted` states whether calling this deployment sends input outside the operator's boundary **by any path**; `RetainsInput` covers retention anywhere along that path. A hop that is itself local does not make the answer local.
+
+That distinction is invisible until a deployment federates. A cell Server calling a site Server over the plant network is one local hop with no internet in sight, and if that site Server is itself calling a hosted endpoint the payload leaves the site anyway. The cell Server publishing `EgressPermitted` false is then publishing something untrue about the only thing its caller wanted to know, while satisfying every rule above — because the rule on `EgressPermitted` binds on `InferenceLocation` being `Cloud`, and the cell Server's is `EdgeOffServer`.
+
+So where a deployment's `Source` names another Server implementing this specification, that Server's declarations are part of the answer. A Server **shall** read `DataJurisdiction`, `EgressPermitted` and `RetainsInput` from the upstream deployment it calls, and **shall not** publish values more permissive than the ones it read. A Server that cannot read them **shall** publish `EgressPermitted` and `RetainsInput` true, for the reason already given: the assumption that keeps data in is the one that is safe to be wrong about.
+
+This propagates assertions; it does not verify them. An upstream Server that declares something false makes its downstream neighbours wrong too, and no protocol can fix that. What it does fix is the case where every Server along a chain is honest and the answer still comes out wrong because nobody was obliged to look up.
+
+§12.3.2 states the same rule for the `FallsBackTo` edge. A payload leaves a deployment along exactly two modelled edges, and the rule is the same on both.
+
+---
+
+## 10 The catalogue and model import (normative)
+
+### 10.1 The catalogue
+
+A model catalogue **is** a registry, which is why this specification extends one rather than inventing a second.
+
+Models come from somewhere: a public hub, a vendor catalogue, an internal MLOps registry. Every such catalogue in practice has the same shape — publishers own namespaces, models and datasets are resources within them, versions are immutable and identified by content, and mutable names point at versions rather than being them.
+
+That shape is a registry, so this specification does not invent one. `ModelRegistryType` (`ns=2;i=1010`), `ModelPublisherType` (`ns=2;i=1011`), `ModelResourceType` (`ns=2;i=1012`) and `DatasetResourceType` (`ns=2;i=1013`) are domain extensions of *OPC UA — xRegistry*'s `RegistryType`, `GroupType` and `ResourceType`.
+
+Two consequences fall out of the base type rather than being designed here, and both are load-bearing:
+
+1. `ResourceType` **is** a `FileType`, so a model artefact a Server holds is readable with the inherited `Open`, `Read` and `Close`. Staging (§10.3) needs no new transport.
+2. `ResourceType` already carries `ExternalReference` and `ResourceUrl`, so a catalogue entry whose bytes live elsewhere is expressible without pretending to hold them.
+
+Each type **narrows** what its base left open, as a domain extension must. `ModelRegistryType` overrides the inherited `` placeholder so it admits `ModelPublisherType` and nothing else; `ModelPublisherType` overrides `` so it admits `AiResourceType` and nothing else.
+
+The narrowing reuses the **inherited BrowseNames**, and that detail is the whole mechanism rather than a formality. An InstanceDeclaration is overridden only by one with the same BrowseName, so a subtype that invents its own placeholder name leaves the inherited one fully open beside it — a registry that looks narrowed and still admits any group at all. Clause 13's conformance depends on the narrowing being real, so the validator checks the BrowseName rather than merely checking that some placeholder exists.
+
+`AiResourceType` (`ns=2;i=1016`) is an **abstract** base of `ModelResourceType` and `DatasetResourceType`. It exists because a publisher holds both models and datasets while `` can be overridden only once: narrowing to a common base admits exactly the two and nothing else. It adds no members of its own — its whole purpose is to be the type that the single override names.
+
+`ModelResourceType` adds `TaskKind`, `Framework`, `Digest`, `DigestAlgorithm`, `SizeBytes`, `Gated` and `MutableRefs`. `SizeBytes` lets a staging import decide whether it has room before it starts rather than after it fails; `Gated` says the artefact needs an entitlement beyond ordinary authentication, which is otherwise discovered part-way through a transfer; `MutableRefs` names the branches and channels a deployment may follow, which is what makes §9.3's `FollowsRef` checkable rather than a claim. `DatasetResourceType` is a **sibling** of it, not something beneath it, because a dataset outlives the models trained on it and is cited by several.
+
+### 10.2 Importing a model
+
+A Server reaches an external AI system in **two distinct ways**, and it is worth separating them because both are sometimes called bridging.
+
+Clause 9 covers the first: a Server that runs no model itself calls one hosted elsewhere, request by request, through a `ModelSourceType`. Nothing is brought across — the model stays where it is and the Server is a client of it.
+
+This clause covers the second: a Server obtains a model from a catalogue so that it can afterwards describe it, execute it, or both. That is what `ModelImportJobType` does, and it is a one-time transfer rather than a per-call relationship.
+
+`ModelImportJobType` (`ns=2;i=1007`) brings a model from a catalogue into this Server. It derives from `AiJobType`, so it is started, observed and audited like every other long-running operation here.
+
+It takes a `Source`, a `ModelReference` and a `Mode`, and produces `ImportedModel` — a `ModelType` instance in this Server's address space, carrying an `ImportedFrom` reference back to the catalogue resource it came from. That reference is what makes *"where did this model come from"* answerable later, rather than only at the moment of import when someone happened to be watching.
+
+An import reads from one of **two** things, and the job says which. `Source` names a `ModelSourceType` — a live endpoint the Server calls. `Registry` (`NodeId`, Optional) names a `ModelRegistryType` — a catalogue the Server browses, which is the path §4.4 and §5.1 draw and the one a plant MLOps node actually uses. A Server **shall** populate exactly one of them and **shall** leave the other null. A job that named both would not say which of the two produced the artefact whose digest §10.4 verifies, and that is the one question the job exists to make answerable.
+
+A Server that imports only from endpoints omits `Registry` altogether; `Source` is Mandatory and remains the only path for it. `Registry` is Optional because the registry types of this clause are themselves optional to implement — a Server obliged to expose a member it can never populate learns nothing and teaches a client nothing. A Server claiming **AI-Import** does implement them, because that facet requires **AI-Catalogue** (§13.2), so there the member is present and the exactly-one rule has both of its alternatives available.
+
+`ModelReferenceDataType` (`ns=2;i=3051`) is the `Publisher`, `Name`, `Version` triple. An import takes the triple rather than a URL because a URL says where a copy is today and the triple says which artefact is meant — and the two diverge the moment anyone mirrors anything.
+
+### 10.3 Import modes
+
+`ImportModeEnum` (`ns=2;i=3011`) is `Federate`, `Stage` or `Auto`.
+
+**`Federate`** materializes the catalogue entry as a `ModelType` and leaves the artefact where it is. Nothing is downloaded; inference runs at the source. This is the right mode whenever the model is large, the source is reliable, and the plant is content for data to reach it — and it is the mode under which a Server can describe hundreds of models it has never fetched.
+
+**`Stage`** fetches the artefact, verifies it, and makes it locally available so inference can run without the source. `BytesTransferred` tracks progress, which is zero throughout a federating import because a federating import moves none.
+
+**`Auto`** federates, then stages if the target deployment's `InferenceLocation` is `OnServer` or `EdgeOffServer` — because those cannot reach the source at inference time, which makes the choice determined rather than a preference.
+
+```mermaid
+flowchart TD
+ A["ModelImportJobType
Source or Registry, ModelReference, Mode"] --> B{Mode}
+ B -->|Federate| F["materialize ModelType
artefact stays at the source"]
+ B -->|Auto| G{"target InferenceLocation
OnServer or EdgeOffServer?"}
+ G -->|no| F
+ G -->|yes| S
+ B -->|Stage| S["fetch artefact
BytesTransferred climbs"]
+ S --> V{"computed Digest ==
catalogue Digest?"}
+ V -->|yes| OK["DigestVerified true
artefact deployable"]
+ V -->|no| NO["DigestVerified false
Halted, LastError set
SHALL NOT deploy"]
+ F --> R["ImportedModel
ImportedFrom the catalogue resource"]
+ OK --> R
+```
+
+### 10.4 Digest verification
+
+Staging is the one point in this model where a Server **shall** verify a digest rather than merely publish one.
+
+A staging import is the moment a substituted artefact would enter the system. Before it, the model is a description; after it, it is bytes that will produce decisions.
+
+A Server performing a staging import **shall** compute the digest of the fetched artefact, compare it with the one the catalogue resource declares, and set `DigestVerified` accordingly. Where they differ, it **shall not** deploy the artefact and **shall** leave the job in a failed state with `LastError` populated.
+
+`Cancel` **shall** discard a partially staged artefact rather than leaving it where a later deployment could pick it up. A half-transferred file that survives a cancellation is an unverified artefact with a plausible name.
+
+This is the point at which §12.1's requirement that `Digest` be Mandatory stops being bookkeeping and becomes an executable check. Everywhere else the digest lets someone verify an artefact if they choose to; here the Server **shall**.
+
+---
+
+## 11 Governance and provenance (normative)
+
+### 11.1 Model card
+
+The nameplate does not say whether a model may be used, and that is a separate question asked by different people.
+
+`ModelType` answers *which artefact is this*. It does not answer *should this be running on my line*, and those are different questions asked by different people at different times.
+
+`ModelCardType` (`ns=2;i=1015`), reached through `ModelType.Card`, answers the second. `IntendedUse` and `Limitations` are both **Mandatory**. A card that records only what a model can do describes half its behaviour, and it is the other half — where it stops working, on what inputs, under what conditions — that a commissioning engineer needs in order to decide whether the model suits the installation in front of them. `OutOfScopeUse`, `License`, `EthicalConsiderations` and `ContactUri` are optional.
+
+`TrainingDataCutoff` deserves its own mention. A model cannot know anything after it, and "the model was trained before this existed" is a common and commonly missed explanation for a field failure that otherwise looks like a defect.
+
+`DeprecatedFrom` and `SupportedUntil` are its forward-facing twins, and they are on the card rather than the nameplate for the reason the split exists: *how long will this keep working* is a question about whether the model may run here, not about which artefact it is.
+
+They are different dates with different responses. `DeprecatedFrom` is when the source stops treating the model as current while continuing to serve it — the date that starts a requalification. `SupportedUntil` is when the source stops serving it at all.
+
+The second is worth being blunt about, because its consequence is not the one the surrounding members suggest. On that date the deployment does not degrade; it stops. `Reachability` goes `Unreachable`, `ConsecutiveFailures` climbs, and `FallbackPolicy` decides what happens next — and where that is `FallBackTo`, the line keeps producing while something outside the qualified configuration answers. §12.3.2 constrains that fallback on residency grounds and `ModelUsed` records it faithfully, so nothing here is hidden; it is simply not noticed, because nobody was watching for a date.
+
+That is the whole value of the member. Every other availability facility in this model — `Reachability`, `ConsecutiveFailures`, `LastSuccessAt`, `FallbackPolicy` — is a way of coping *after* the fact. This is the only one whose value is a date in the future, and where a source publishes it in machine-readable form a Server **should** carry it, because a requalification takes longer to schedule than an outage takes to notice.
+
+### 11.2 Evaluation
+
+A metric without the threshold it was judged against cannot be acted on.
+
+`EvaluationRunType` (`ns=2;i=1014`) is one measurement of a model against a dataset. It is a first-class object rather than a field on the model because the same model is measured many times, and because the run that gated a promotion must remain readable afterwards to answer why the promotion was allowed.
+
+`RunId`, `EvaluatedModel` and `Metrics` are **Mandatory**: a run that cannot be named, or that does not say which model it measured, or that carries no measurement, records nothing that can be acted on. `Dataset`, `CompletedAt` and `ReportUri` are Optional — a Server may evaluate against data it does not model here, and the full report often lives outside OPC UA entirely.
+
+`EvaluationMetricDataType` (`ns=2;i=3055`) carries `Name`, `Value`, `Unit`, `Threshold`, `Comparison` and `Passed`. **The threshold travels with the metric.** An accuracy of 0.94 means nothing on its own; a reviewer reading it a year later has no way to recover what "good" meant, and the person who knew has moved on.
+
+`Passed` on the run is the conjunction of the individual ones. A Server **shall not** report it true while any metric's `Passed` is false — a summary that disagrees with its own detail is worse than no summary, because it is the field people read.
+
+Models carry `EvaluatedBy` references to their runs. It is optional and repeating: the run that gated promotion is not necessarily the most recent one.
+
+### 11.3 Lineage
+
+Lineage is a **chain**, not a field, and the difference is what makes it usable.
+
+`DerivedFrom` links a model to the one it was fine-tuned, distilled or quantized from.
+
+It is a reference and not a string because lineage is walked. A model three derivations from its base is answerable for all three — a defect in the base is a defect in every descendant — and a field naming only the immediate parent cannot be followed to find out.
+
+`Quantization` on `ModelType` states the numeric precision the artefact is stored in. A quantized model is a **different artefact with different behaviour**, not a packaging detail, and treating it as one is how a model that passed evaluation at full precision ends up deployed at reduced precision without being re-measured.
+
+### 11.4 Safety assessment
+
+Where a safety policy is applied to an inference call, what it produces is a set of **findings** — each naming a category, how severe it was, and whether anything was withheld as a result.
+
+`SafetyAssessmentDataType` (`ns=2;i=3054`) carries `Category`, `Severity`, `Filtered` and `Detail`, and is returned by `Invoke` where a policy was applied.
+
+`Severity` (`SafetySeverityEnum`, `ns=2;i=3012`) is `None`, `Low`, `Medium` or `High`. `Category` is a **String**, not an enumeration, because harm categories are set by the policy an installation adopts and an industrial taxonomy — out-of-distribution input, unsafe recommendation, sensitive-data exposure — looks nothing like a consumer one. Fixing the categories here would mean fixing them wrong for most adopters.
+
+`Filtered` distinguishes withheld from flagged. A client that treats the two alike will either discard usable output or act on output that was not meant to be acted on.
+
+---
+
+## 12 Security
+
+### 12.1 Provenance
+
+Provenance is the point of the digest: without it the other members describe an artefact nobody can confirm they hold.
+
+A published result is traceable to the artefact that produced it by: result → deployment (the consuming specification's `NodeId` Property) → `UsesModel` → `ModelType` → `Digest`.
+
+Every link is required for the chain to hold, which is why `UsesModel` is exactly-one (§6.5) and `Digest` is Mandatory (§6.2). A Server **shall** populate `Digest` for every model whose artefact is obtainable through `ArtifactUri`.
+
+`DigestAlgorithm` **shall** name a hash function with **at least 256-bit output and no known collision weakness**; `SHA-256` is the default and is always acceptable. It **shall not** be `MD5`, `SHA-1` or a truncated variant — chosen-prefix collisions against those are practical, so a substituted artefact would pass verification, and a verification that can be passed by the wrong artefact is worse than none because it is believed.
+
+```mermaid
+flowchart LR
+ R["a published result"] --> D["DeploymentType"]
+ D -->|"ModelUsed
not UsesModel"| M["ModelType"]
+ M -->|Digest + DigestAlgorithm| A["the artefact bytes"]
+ M -->|ImportedFrom| CR["catalogue resource
where it came from"]
+ M -->|DerivedFrom| B["the model it came from"]
+ M -->|TrainedOn| DS["DatasetType"]
+```
+
+#### 12.1.1 What a digest is worth, and why an empty one is not a failure
+
+`Digest` is Mandatory so that its absence is uniform and browsable: a client finds the member on every model and reads an empty value, rather than not finding the member and being unable to tell a model without a digest from a Server that does not implement digests. That is the right trade, and it is worth stating plainly that **most sources cannot fill it**. An endpoint that names models but never their content — which is what a hosted inference API generally is — has no digest to give, and a Server integrating one is behaving correctly when it publishes none.
+
+But "empty" then carries two meanings, and a present value carries three. `DigestProvenance` (`DigestProvenanceEnum`, `ns=2;i=3015`) is **Mandatory** on `ModelType` and separates them:
+
+| Value | `Digest` | What a client may conclude |
+|---|---|---|
+| `NotAvailable` | empty | The source publishes no digest. There is nothing to obtain and nothing was withheld. |
+| `DeclaredBySource` | present | An assertion forwarded. No party the Server can speak for has hashed the artefact. |
+| `ComputedByServer` | present | Evidence the Server holds. Nothing independent agrees with it, so a substitution that preceded the Server obtaining the bytes is undetected. |
+| `VerifiedOnStage` | present | The Server computed it during a staging import and it matched what the source declared (§10.4). Two independent parties agree, which is the strongest statement this model carries. |
+
+It is Mandatory for the reason `Digest` itself is, stated in §6.2: clause 12 depends on it, and a rule that depends on an Optional member is one a conformant Server can silently not satisfy. A client deciding whether to run a model on a line needs to distinguish *nobody checked* from *two parties agree*, and an Optional member would let a Server decline to answer exactly where the answer matters.
+
+`ModelResourceType` carries the same member as **Optional**, mirroring the `Digest` it qualifies, which is Optional there. A catalogue declaring a digest it did not compute is `DeclaredBySource`; one serving the artefact through the inherited `Open`, `Read` and `Close` can reach `ComputedByServer`.
+
+**A Server shall not put a non-content identifier in `Digest`.** A response fingerprint, a resource name, a storage entity tag and a repository commit identifier are all tempting, all stable, and none of them a digest of the artefact that ran. A client that verified against one would believe it had checked something it had not, which is the failure mode `DigestAlgorithm`'s strength rule exists to prevent — arrived at by a different route. `NotAvailable` is the answer.
+
+Where such an identifier **locates** the artefact or its provenance record, it belongs in `ArtifactUri` or `ProvenanceUri`, which promise nothing about content. Where it identifies something else — a serving configuration, a request route — this model has no member for it, and inventing one out of `Digest` is not the remedy. A datum with no home is better recorded as having none than filed somewhere a client will read it as an artefact digest.
+
+**Provenance does not strengthen by being forwarded.** Where a deployment's `Source` names another Server implementing this specification, that Server publishes a `Digest` and a `DigestProvenance` of its own, and both are readable. A Server **shall not** publish a `DigestProvenance` stronger than the one it read upstream, where the ordering is `NotAvailable` < `DeclaredBySource` < `ComputedByServer` < `VerifiedOnStage`. A digest received across a federation hop and not checked against bytes this Server holds is `DeclaredBySource` however the upstream Server obtained it: republishing its `VerifiedOnStage` would claim a verification this Server did not perform, and the claim would be indistinguishable from one it had. This is the composition rule §9.5 states for residency, applied to the other thing a federated deployment forwards.
+
+#### 12.1.2 Broken links
+
+The walk is: result → deployment → `ModelUsed` → `ModelType` → `Digest`, and — where the model was imported — `ImportedFrom` → the catalogue resource it came from.
+
+Two of those links can be broken by a Server that is otherwise behaving correctly:
+
+- Reading the **deployment's current model** instead of `ModelUsed` gives the wrong answer whenever a fallback served the call or a followed reference moved (§8.2.1). It is wrong silently and plausibly, which is the worst combination.
+- Trusting a **staged artefact whose digest was never checked** breaks it at the point where an artefact enters the system. §10.4 is where that check is required, and it is the only place in this model where a Server **shall** verify a digest rather than merely publish one.
+
+### 12.2 URI handling
+
+Every URI in this model is untrusted input.
+
+`ArtifactUri`, `ProvenanceUri` and `EndpointUri` are values a client may have written and a Server may resolve. A Server **shall** validate them against a configured policy before resolving, and **shall not** follow one to a scheme or host the policy does not permit.
+
+Where `InferenceLocation` is not `OnServer`, `EndpointUri` **shall** name a scheme that is authenticated and confidential. Inference off the Server means the input data leaves it, and the result comes back from something the Server did not compute — both directions need the channel to be trustworthy.
+
+The set of resolvable URIs grew with clauses 9 to 10, and every addition is a value some client may have written: `ModelSourceType.EndpointUri` and `EndpointDescriptionUri`, `ModelCardType.ContactUri`, `EvaluationRunType.ReportUri`, `ModelType.SafetyPolicyUri`, `EgressPolicyUri`, and the catalogue's inherited `ResourceUrl`. The same policy governs all of them. A Server that validates the ones it remembers and resolves the rest has a policy in name only.
+
+A staging import (§10.3) is the sharpest case, because it fetches bytes that will subsequently produce decisions. A Server **shall** apply the resolver policy to the artefact location **before** transferring, not after — a policy checked on the way out is not a control, and `SizeBytes` exists partly so that the decision can be made without starting.
+
+#### 12.2.1 Credential material
+
+A Server **shall not** expose credential material through any Attribute of any node in this model. `CredentialReference` names a credential in whatever store the Server uses; it never carries one, and `TokenAudience` states what a token is requested *for*, not what it is.
+
+This is stated as a prohibition rather than left to implementers' good sense because the address space is not merely readable. It is browsable by anything with a Session, subscribable so that a value is pushed as it changes, and historisable so that a value read once is retained. A secret placed there is not exposed once — it is published, distributed and archived.
+
+`WorkloadIdentity` is preferred wherever the platform offers it, for the reason that it is the only authentication kind under which there is no secret anywhere to be exposed by a future mistake.
+
+### 12.3 Promotion authorization
+
+Promotion needs an authorization of its own, distinct from the one that permits ordinary operation.
+
+A Server **shall** require an authorization for `PromoteModel` distinct from the one that permits reading this model or operating the equipment.
+
+Promotion changes behaviour without changing structure. Nothing in the address space looks different afterwards except a version string, so the usual defence — that a significant change is visible — does not apply here.
+
+#### 12.3.1 Followed references
+
+Promotion has a second door, and a control that guards only the first is misleading rather than merely weaker.
+
+`PromoteModel` is not the only way the model behind a deployment changes. A `FollowsRef` binding (§9.3) moves whenever whoever controls the reference repoints it, and nothing in this address space changes when they do.
+
+A Server **shall** treat repointing a followed reference as the same class of act as calling `PromoteModel`, and **shall** subject it to the same distinct authorization. A control that guards the front door while the side door stands open is not a weaker control — it is a misleading one, because the audit trail shows every promotion having been authorized.
+
+For the same reason `AiJobType.RequestedBy` records who started a job. An authorization check that leaves no record answers *was this allowed* but not *who did it*, and only the second question can be asked after the fact.
+
+#### 12.3.2 Fallback
+
+A fallback changes what answers, not who may ask.
+
+`FallBackTo` (§9.4) routes a call to a different deployment, and therefore a different model, without the caller asking for it.
+
+That is not a privilege escalation — the caller was already entitled to an answer — but it **is** a change in what produced the answer, and §8.2.1 requires it to be visible in `ModelUsed`. A Server **shall not** configure a fallback to a deployment whose `EgressPermitted` or `DataJurisdiction` is more permissive than the deployment falling back to it. Otherwise a network fault silently sends plant data somewhere policy forbids, which is precisely the moment nobody is watching.
+
+### 12.4 Digest and authorship
+
+A digest is not a signature, and the gap between the two is where an installation's real exposure sits.
+
+`Digest` establishes that an artefact is the one described. It does **not** establish who produced it or that they were entitled to. A Server **shall not** present digest verification as authorization, and an installation that needs provenance of authorship needs a signature, which this model does not define.
+
+The distinction sharpens once models arrive through a bridge (§10.2). A staging import verifies that the bytes it fetched match the digest the catalogue declared — so it detects corruption in transfer, and substitution by anyone who could not also edit the catalogue entry. It detects nothing at all about an attacker who could edit both, and the catalogue is the more attractive target precisely because it is the one that many machines read.
+
+So what `DigestVerified` means is narrow and worth stating plainly: **the artefact is the one this catalogue entry described**. Whether that entry described the right artefact is a question about the catalogue, answered by the catalogue's own access control and by whatever signing the publisher applies — neither of which this model can see.
+
+Two practical consequences:
+
+- A Server **shall not** treat `DigestVerified` as evidence that a model is approved for use. §11.1's card and §11.2's evaluation are what an installation reads for that, and `ProvenanceUri` is the hand-off to the system that actually decides.
+- An installation whose threat model includes a compromised catalogue **should** verify a publisher signature over the artefact out of band before promotion. This specification records where the artefact came from and what it hashes to, which is what makes such a check possible; it does not perform it.
+
+---
+
+## 13 Profiles and conformance units
+
+### 13.1 Declaring conformance
+
+A Server declares conformance by exposing `AiRootType` under the Server object with `SpecificationVersion` set to the release it implements.
+
+Facets are **additive and independent** except where a row states otherwise, and only one dependency exists: **AI-Import** requires **AI-Catalogue**, because an import job with nothing to import from is not implementable.
+
+The split matters more here than in a smaller model, because the plausible Servers differ enormously — a device running one fixed model, a gateway calling a hosted one, and a plant MLOps node that may never call `Invoke` at all are three different products rather than three degrees of completeness of one. §13.3 names them as profiles. A single monolithic conformance claim would have made two of the three unclaimable.
+
+**AI-Residency** is deliberately separate from **AI-Federation**. A Server can be perfectly capable of calling a remote model while being unable to state where the data goes, and an operator who needs the second guarantee needs to be able to ask for it by name rather than infer it from the first.
+
+### 13.2 Facets
+
+| Facet | Requires |
+|---|---|
+| **AI-Base** (mandatory) | `AiRootType` with `Models` and `Deployments`; at least one `ModelType` with `ModelId`, `Name`, `Version`, `Digest`, `DigestAlgorithm` and `DigestProvenance`; where the Server exposes any deployment, each carries `DeploymentId`, `InferenceLocation` and `State` and satisfies the exactly-one `UsesModel` rule of §6.5; the digest rules of §12.1 |
+| **AI-Dataset** | `DatasetType` instances with `DatasetId` and `SourceKind`, and `TrainedOn` from at least one model |
+| **AI-OffServer** | A deployment whose `InferenceLocation` is not `OnServer`, and §12.2's requirement that its `EndpointUri` name an authenticated, confidential scheme |
+| **AI-Signatures** | `Inputs` and `Outputs` populated on every model |
+| **AI-Learning** | `LearningJobType`, the §7 state model, every Method that drives a transition in it, and the distinct `PromoteModel` authorization of §12.3 |
+| **AI-Invoke** | `DeploymentType.Invoke` with `ModelUsed` and `FinishReason` populated on every response, and `Usage` returned on every response — its `UnitKind` empty where the execution site does not meter, per §8.2.3; the §6.4.2 requirement to publish `ApiDialect` where `Inputs`/`Outputs` do not describe the payload contract; the exactly-one `Payload`/`PayloadUri` rule of §8.6.1; and the §8.3 rule that an unsupported parameter is rejected rather than ignored |
+| **AI-InvokeAsync** | `InvokeAsync` and `InferenceJobType`, answering the same questions as `Invoke` including size (§8.6.1): the exactly-one `Payload`/`PayloadUri` rule, and `TransferRequired` with `Transfer` where a result outgrew the inline bound |
+| **AI-Transfer** | `BeginTransfer` and `InferenceTransferType`, `MaxInlinePayloadSize` on every deployment, and the §8.2.4 rule that `Invoke` reports `TransferRequired` rather than failing a call whose response outgrew the inline bound |
+| **AI-Stream** | Incremental results published over a data channel (§8.5). Entirely optional; a Server that answers only through `Invoke` is conformant without it |
+| **AI-Federation** | `ModelSourceType` with `ApiDialect`, `AuthenticationKind` and `Reachability`; the credential-secrecy prohibition of §9.2; `FallbackPolicy` on every deployment and the acyclicity rule of §9.4; `LastModifiedAt` on every model reached through a `FollowsRef` binding (§6.2.3); the composition rules of §9.5 and §12.1.1, which forbid a Server publishing residency or digest provenance stronger than what it read upstream |
+| **AI-Residency** | `DataJurisdiction`, `EgressPermitted` and `RetainsInput` on every deployment, with the §9.5 rules including the requirement to report `RetainsInput` true when it cannot be established |
+| **AI-Catalogue** | `ModelRegistryType`, `ModelPublisherType` and `ModelResourceType`, with the placeholders narrowed as §10.1 requires |
+| **AI-Import** | `ModelImportJobType`, the federate/stage/auto modes of §10.3, the exactly-one `Source`/`Registry` rule of §10.2, and the digest verification of §10.4. Requires **AI-Catalogue** |
+
+A Server **shall** publish the URI of every facet and profile it claims in `Server/ServerCapabilities/ServerProfileArray`, which is where a client discovers what it supports without browsing for members and guessing.
+
+### 13.3 Profiles
+
+A facet is a building block. A **profile** is a complete claim: a named set of facets describing one plausible Server, which is what a procurement document cites and what two vendors implementing the same shape agree they have built.
+
+Four are defined. Each includes **AI-Base**, and a Server **may** claim more than one — a gateway that also mirrors a catalogue claims two, which is the whole reason profiles are composed from facets rather than written out independently.
+
+| Profile | Facets | The Server it describes |
+|---|---|---|
+| **AI Inference Device Server** | AI-Base, AI-Invoke | Runs models itself. A camera, a controller or an industrial PC executing a model in its own process, describing what it runs and answering calls against it. |
+| **AI Inference Gateway Server** | AI-Base, AI-Invoke, AI-OffServer, AI-Federation, AI-Residency | Calls a model hosted elsewhere. The bridge between a plant address space and an external inference service. |
+| **AI Model Catalogue Server** | AI-Base, AI-Catalogue, AI-Import | Holds and distributes models without necessarily running any. A plant MLOps node mirroring a corporate registry and staging artefacts onto controllers. |
+| **AI Model Lifecycle Server** | AI-Base, AI-Dataset, AI-Learning, AI-Catalogue, AI-Import | Closes the loop of clause 7: accumulates a dataset from the line, trains a candidate, evaluates it, promotes it, and keeps the lineage that explains why. |
+
+**AI-Residency is inside the gateway profile rather than optional to it.** The moment inference leaves the Server the question *where does my data go* has an answer, and a gateway that cannot state it is precisely the arrangement §9.5 exists to prevent. A Server that federates and cannot answer claims the facets individually and not this profile.
+
+The **Inference Device** profile does not include AI-Signatures, though a tensor runtime can usually satisfy it, because a Server serving one model behind a documented request body is a legitimate device and `Inputs`/`Outputs` do not describe a JSON contract (§6.4.2). It is claimed alongside where it holds.
+
+The **Lifecycle** profile is the only one that requires AI-Learning, and it requires AI-Dataset with it. A learning loop whose training data is not described is a loop that cannot be audited, and §12.3's promotion authorization exists to be answerable about exactly that.
+
+None of the four is a subset of another, and that is the point. A device that runs one fixed model, a gateway that runs none, and a catalogue node that may never call `Invoke` are three different products, and a single monolithic profile would have made two of them unclaimable.
+
+### 13.4 Profile and facet URIs
+
+A profile name is for a human. `ServerProfileArray` holds URIs, and unless this specification states them two Servers implementing the same profile publish different strings and no client can match either.
+
+Profiles are published under `http://opcfoundation.org/UA-Profile/AI/Server/`:
+
+| Profile | URI suffix |
+|---|---|
+| AI Inference Device Server | `InferenceDevice` |
+| AI Inference Gateway Server | `InferenceGateway` |
+| AI Model Catalogue Server | `ModelCatalogue` |
+| AI Model Lifecycle Server | `ModelLifecycle` |
+
+Facets are published under `http://opcfoundation.org/UA-Profile/AI/Facet/`, with the suffix being the facet name after the `AI-` prefix: **AI-Base** is `Base`, **AI-InvokeAsync** is `InvokeAsync`, **AI-OffServer** is `OffServer`, and so on for every row of §13.2.
+
+These URIs are **provisional**, on the same terms as the namespace URI and the NodeIds: this is a working-group draft, and the OPC Foundation assigns the final values.
+
+---
+
+| Artifact | Path |
+|---|---|
+| This specification | `metaverse-specs/ai-model-management/OPC-UA-AI-Model-Management.md` |
+| Information model | `metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeSet2.xml` |
+| NodeId assignments | `metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeIds.csv` |
+| Generator | `metaverse-specs/extras/ai-model-management/tools/build_model.py` |
+| Validator | `metaverse-specs/extras/ai-model-management/tools/validate_local.py` |
+| Annex A (generated) | `metaverse-specs/extras/ai-model-management/tools/model-reference.md` |
+| Implementation guides (informative) | `metaverse-specs/extras/ai-model-management/examples/` |
+| Guide validator | `metaverse-specs/extras/ai-model-management/examples/tools/validate_examples.py` |
+
+The NodeSet, the CSV and Annex A are generated from a single in-code source of truth and are **deterministic**. The generator is edited; the generated files are not.
+
+The [implementation guides](../extras/ai-model-management/examples/index.md) are informative and introduce nothing. They map this model onto the systems an implementer is likely to be integrating — Azure AI Foundry, OpenAI, Amazon Bedrock and SageMaker, NVIDIA NIM and Triton, Google Vertex AI, Hugging Face, KServe, embedded runtimes, and another Server implementing this specification. Naming products there rather than here is what lets clause 9.2 name dialects for what they do: the normative document stays neutral and the informative folder beside it does not have to. Every literal of `ApiDialectEnum` and `AuthenticationKindEnum` is exercised by at least one guide, and `validate_examples.py` fails if a guide cites a member this model does not declare.
+
+```powershell
+python metaverse-specs\extras\ai-model-management\tools\build_model.py
+python metaverse-specs\extras\ai-model-management\tools\validate_local.py
+python metaverse-specs\extras\ai-model-management\examples\tools\validate_examples.py
+```
+
+---
+
+## Annex A — Information model (generated)
+
+Annex A is generated from the NodeSet and is authoritative for identifiers, DataTypes, ValueRanks, ModellingRules, structure fields, enumeration values and Method signatures. See [`../extras/ai-model-management/tools/model-reference.md`](../extras/ai-model-management/tools/model-reference.md).
+
+## Annex B — Informative alignments
+
+Not normative references, and no dependency. Recorded because this model borrowed from them deliberately.
+
+- **IDTA 02060** *AI Model Nameplate* — the member set of `ModelType`. Currently the only standardised description of an industrial AI model.
+- **IDTA 02058** *AI Dataset* — the member set of `DatasetType`.
+- **IDTA 02059** *AI Model Management* — the member set of `DeploymentType`, including the inference-location concept.
+- **OPC 30270** — the OPC UA ⇄ Asset Administration Shell bridge, over which the alignments above become a populated AAS.
+- **xRegistry** — [the CNCF specification](https://github.com/xregistry/spec) the OPC UA projection in this repository follows. Its `groups` / `resources` / `versions` structure is what clause 10 extends, and public proxies over model hubs already present exactly the arrangement adopted here: publisher as group, models and datasets as sibling resource types, versions immutable and identified by content, mutable branch and tag names as pointers rather than versions.
+- **OPC UA — Vision** in this repository is the first consuming specification. Its `InferencePipelineType.Deployment` is a `NodeId` Property naming a `DeploymentType` here, per §5.2, and neither NodeSet requires the other.
+
+---
+
+## Annex C — A worked arrangement (informative)
+
+This annex is **informative**. It shows one arrangement that satisfies clauses 8 to 10, to make the interaction between them concrete. No member here is introduced by this annex; every one is defined in Annex A.
+
+### C.1 The situation
+
+A plant runs a surface-inspection model on a finishing line. The model is published in a corporate catalogue. Two things are true at once and pull in opposite directions: the good model is large and runs on a GPU appliance nobody wants to put on every line, and the line must keep running when the network to that appliance does not.
+
+So the plant deploys twice. A **primary** deployment calls the appliance. A **secondary** deployment runs a smaller quantized model on the line controller itself. The primary falls back to the secondary.
+
+### C.2 Getting the models here
+
+Both start as one `ModelImportJobType` each, against a `ModelSourceType` naming the corporate catalogue.
+
+| | Primary | Secondary |
+|---|---|---|
+| `ModelReference` | `Publisher` = `plant-quality`, `Name` = `surface-defect`, `Version` = `4.2.0` | same publisher and name, `Version` = `4.2.0-int8` |
+| `Mode` | `Federate` | `Stage` |
+| Result | a `ModelType` describing an artefact that stays in the catalogue | a `ModelType` whose artefact is now on the controller |
+
+The second job fetches bytes, so `BytesTransferred` climbs and `DigestVerified` is the gate: the job compares what it fetched against the `Digest` the `ModelResourceType` declared, and refuses to deploy on mismatch (§10.4). The first job moves nothing, so `BytesTransferred` stays zero.
+
+Both resulting models carry `ImportedFrom` back to the catalogue resource, which is what makes the question *where did this come from* answerable next year rather than only today.
+
+The quantized model additionally carries `DerivedFrom` to the full-precision one and states `Quantization` = `int8`. That is not bookkeeping: it is the reason a reviewer knows the two will not agree on every part, and the reason the secondary needs its own `EvaluationRunType` rather than inheriting the primary's.
+
+### C.3 The two deployments
+
+| | Primary | Secondary |
+|---|---|---|
+| `InferenceLocation` | `EdgeOffServer` | `OnServer` |
+| `Source` | the appliance's `ModelSourceType` | null |
+| `VersionBinding` | `Pinned` | `Pinned` |
+| `FallbackPolicy` | `FallBackTo` | `Fail` |
+| `FallsBackTo` | the secondary | — |
+| `DataJurisdiction` | `plant-north` | `plant-north` |
+| `EgressPermitted` | `false` | `false` |
+| `RetainsInput` | `false` | `false` |
+
+The appliance is on the plant network, so nothing leaves the site and `EgressPermitted` is false for both. Had the plant chosen a hosted service instead, §9.5 would have required it to be `true` — and, if the operator could not establish what the provider did with the images, `RetainsInput` `true` as well.
+
+Both are `Pinned`. A `FollowsRef` primary would have been convenient and would have meant the artefact could change without anything else changing, which §9.3 treats as a promotion in disguise.
+
+### C.4 A normal call, and a link failure
+
+A client calls `Invoke` on the primary with an image as `Payload` and its media type as `ContentType`. The response carries `ModelUsed` naming the full-precision model, `Usage` with `UnitKind` `images` and `InputUnits` 1, and `FinishReason` `Stop`.
+
+Then the switch feeding the appliance fails.
+
+The Server's next attempt does not answer. `Reachability` on the primary goes `Unreachable` and `ConsecutiveFailures` climbs; `LastSuccessAt` stops advancing. Because `FallbackPolicy` is `FallBackTo`, the call is served by the secondary, and this is the part that matters: **the response says so.** `ModelUsed` now names the quantized model, not the one the primary still points at.
+
+A client that logged only the deployment would record that the full-precision model made every judgement that afternoon. A client that reads `ModelUsed` — as §8.2.1 requires — records what actually happened, which is what an audit a month later needs.
+
+Note what did **not** change: the client called the same Method with the same arguments throughout, and never learned that inference moved from an appliance to the local controller except by reading the outputs it was going to read anyway.
+
+### C.5 Throttling
+
+Had the appliance been saturated rather than unreachable, `Reachability` would have read `Throttled` and `RateLimit.RetryAfter` would have carried a wait.
+
+The distinction is the point of separating the two values. Failing over a throttled endpoint moves load onto the weaker model for no reason; the endpoint will serve again shortly. Failing over an unreachable one is exactly right. From the outside the two look identical, which is why the Server states which it is rather than leaving a client to infer it from a timeout.
+
+---
+
+---
+
+## Annex D — Deploying a classical model (informative)
+
+This annex is **informative**. Clause 8 is written around an envelope, and some of its vocabulary — capability names like `chat`, accounting in units that are often tokens — comes from the kind of model that made those terms familiar. Most industrial deployments run something else entirely: a fixed-shape tensor model, exported once, executed in-process, answering in microseconds.
+
+This annex works that case end to end to show that the same envelope carries it with nothing bent. Every member named here is defined in Annex A.
+
+### D.1 The model
+
+A gearbox condition classifier. Exported to **ONNX**, 4.8 MB, takes a window of vibration samples and returns a class distribution over four fault states. It runs on the line controller because a 20 ms budget does not survive a network hop, and because the plant does not permit raw vibration to leave the site.
+
+Nothing about that description needs a member this specification does not already have.
+
+### D.2 In the catalogue
+
+| Member | Value |
+|---|---|
+| `ModelResourceType` id | `gearbox-fault` under publisher `plant-reliability` |
+| `TaskKind` | `classification` |
+| `Framework` | `onnxruntime` |
+| `Digest` / `DigestAlgorithm` | SHA-256 of the `.onnx` file |
+| `SizeBytes` | `5033164` |
+| `Gated` | `false` |
+
+`TaskKind` is a String, and `classification` is not drawn from any list this specification publishes. That is the point of it being a String: a catalogue that also holds `regression`, `anomaly-detection` and `remaining-useful-life` needs no amendment here to say so.
+
+`SizeBytes` earns its place in this example. 4.8 MB is nothing; the same catalogue holds a vision model of 340 MB, and a controller with 64 MB of free storage needs to refuse **before** transferring rather than after.
+
+### D.3 Getting it onto the controller
+
+A `ModelImportJobType` with `Mode` = `Stage`, because `InferenceLocation` will be `OnServer` and an on-server deployment cannot reach the catalogue at inference time.
+
+The job fetches, computes SHA-256 over what arrived, compares it with the `Digest` the catalogue declared, and sets `DigestVerified`. This is the whole of the integrity story for a model that will now decide whether a gearbox is failing, and §10.4 is why it is a **shall** here and nowhere else.
+
+The resulting `ModelType` carries `ImportedFrom` back to the catalogue resource, so a year later *where did this come from* has an answer that does not depend on anyone having written it down.
+
+### D.4 The shape contract
+
+`Inputs` and `Outputs` carry `TensorSignatureDataType`, and for a classical model they are the **entire** interface description:
+
+| | `Name` | `ElementType` | `Shape` | `Layout` |
+|---|---|---|---|---|
+| Input | `window` | `float32` | `-1, 2048, 3` | `NWC` |
+| Output | `probabilities` | `float32` | `-1, 4` | |
+
+The leading `-1` is the batch axis, dynamic as ONNX exports usually leave it. `2048` is the window length and `3` the axis count, and both are fixed by the export — send 1024 samples and the runtime rejects the call.
+
+This is why §6.2 insists the signatures are the only machine-readable description of what a deployment accepts. A client that reads them establishes at configuration time that its window length matches; a client that does not discovers it as a rejected call at 3 a.m. And `LabelClasses` — `["healthy", "bearing-wear", "tooth-crack", "misalignment"]` — is what makes `probabilities[2]` mean something, which is exactly why §6.2 forbids reordering it in place.
+
+### D.5 The deployment
+
+| Member | Value |
+|---|---|
+| `InferenceLocation` | `OnServer` |
+| `Source` | null — nothing to reach |
+| `ApiDialect` | not applicable; where a source is named for a local runtime it is `EmbeddedRuntime` |
+| `AcceleratorKind` | `Cpu` |
+| `VersionBinding` | `Pinned` |
+| `FallbackPolicy` | `Fail` |
+| `DataJurisdiction` | `plant-north` |
+| `EgressPermitted` | `false` |
+| `RetainsInput` | `false` |
+| `LatencyBudget` | 20 ms |
+
+`FallbackPolicy` is `Fail` deliberately. There is no second model, and a condition classifier that quietly returns a stale verdict is worse than one that says it could not answer — the whole value of the reading is that it is current.
+
+`EgressPermitted` is `false` and means it: the bytes never leave the controller, let alone the site.
+
+### D.6 Calling it
+
+`Invoke` takes the tensor as `Payload` with a `ContentType` that says how it is encoded — for example `application/octet-stream` for a raw little-endian `float32` buffer in the declared layout, or a media type the deployment publishes for a framed encoding.
+
+**This specification does not standardise a tensor wire format, and that is deliberate.** The candidates — raw buffers, protobuf tensors, Arrow, npy — are each right in some deployment and wrong in others, and a Server that already speaks one to its runtime should not have to transcode into a format chosen here. `ContentType` names which one is in use, which is what a client actually needs, and the shape contract of §D.4 tells it what must be inside whichever it is.
+
+The response comes back through the same envelope:
+
+| Output | Value here |
+|---|---|
+| `ResponsePayload` | four `float32` probabilities |
+| `ModelUsed` | the staged `ModelType` |
+| `Usage` | `UnitKind` = `samples`, `InputUnits` = `2048`, `OutputUnits` = `4`, `TotalUnits` = `2048` |
+| `FinishReason` | `Stop` |
+| `SafetyAssessment` | empty — no policy applies |
+
+`Usage` is the member that would have been mis-modelled had the accounting been named in tokens. This model consumes samples. A field called `InputTokens` here would be either empty or a lie, and §8.2.3 is why it is not called that.
+
+`FinishReason` is `Stop` on every successful call, and a reader may reasonably ask what it is for on a model that cannot truncate. It is for the client, which does not know which kind of model is behind the deployment and should not have to — that is the same-envelope property doing its job.
+
+### D.7 Capabilities
+
+What this deployment does **not** advertise is as informative as what it does, and means nothing against it.
+
+`Capabilities` on this deployment names `tensor-inference` supported and nothing else. It does not name `chat`, `streaming` or `tool-call`.
+
+That is not a deficiency and does not make the deployment a partial implementation of anything. `Capabilities` is an **open list** (§8.4) precisely so that a deployment describes what it does rather than scoring itself against a menu — and a client that needs a chat capability finds it absent and looks elsewhere, which is the correct outcome and required no negotiation.
+
+The Server claims **AI-Base**, **AI-Invoke**, **AI-Signatures**, **AI-Residency**, **AI-Catalogue** and **AI-Import**. It claims neither **AI-Federation** — there is nothing remote — nor **AI-Learning**, because this model is retrained offline by the reliability team and promoted by a fresh import. Both absences are ordinary.
diff --git a/metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeIds.csv b/metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeIds.csv
new file mode 100644
index 0000000..0e38f27
--- /dev/null
+++ b/metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeIds.csv
@@ -0,0 +1,247 @@
+InferenceLocationEnum,3001,DataType
+InferenceLocationEnum_EnumStrings,3901,Variable
+AcceleratorKindEnum,3002,DataType
+AcceleratorKindEnum_EnumStrings,3902,Variable
+DeploymentStateEnum,3003,DataType
+DeploymentStateEnum_EnumStrings,3903,Variable
+DatasetSourceEnum,3004,DataType
+DatasetSourceEnum_EnumStrings,3904,Variable
+LearningJobStateEnum,3005,DataType
+LearningJobStateEnum_EnumStrings,3905,Variable
+FinishReasonEnum,3006,DataType
+FinishReasonEnum_EnumStrings,3906,Variable
+ApiDialectEnum,3007,DataType
+ApiDialectEnum_EnumStrings,3907,Variable
+AuthenticationKindEnum,3008,DataType
+AuthenticationKindEnum_EnumStrings,3908,Variable
+FallbackPolicyEnum,3009,DataType
+FallbackPolicyEnum_EnumStrings,3909,Variable
+VersionBindingEnum,3010,DataType
+VersionBindingEnum_EnumStrings,3910,Variable
+ImportModeEnum,3011,DataType
+ImportModeEnum_EnumStrings,3911,Variable
+SafetySeverityEnum,3012,DataType
+SafetySeverityEnum_EnumStrings,3912,Variable
+ReachabilityEnum,3013,DataType
+ReachabilityEnum_EnumStrings,3913,Variable
+TransferStateEnum,3014,DataType
+TransferStateEnum_EnumStrings,3914,Variable
+DigestProvenanceEnum,3015,DataType
+DigestProvenanceEnum_EnumStrings,3915,Variable
+TensorSignatureDataType,3050,DataType
+TensorSignatureDataType_Encoding_DefaultBinary,5001,Object
+ModelReferenceDataType,3051,DataType
+ModelReferenceDataType_Encoding_DefaultBinary,5002,Object
+UsageDataType,3052,DataType
+UsageDataType_Encoding_DefaultBinary,5003,Object
+CapabilityDataType,3053,DataType
+CapabilityDataType_Encoding_DefaultBinary,5004,Object
+SafetyAssessmentDataType,3054,DataType
+SafetyAssessmentDataType_Encoding_DefaultBinary,5005,Object
+EvaluationMetricDataType,3055,DataType
+EvaluationMetricDataType_Encoding_DefaultBinary,5006,Object
+RateLimitDataType,3056,DataType
+RateLimitDataType_Encoding_DefaultBinary,5007,Object
+UsesModel,4001,ReferenceType
+TrainedOn,4002,ReferenceType
+DerivedFrom,4003,ReferenceType
+FallsBackTo,4004,ReferenceType
+ImportedFrom,4005,ReferenceType
+EvaluatedBy,4006,ReferenceType
+AiRootType,1001,ObjectType
+AiRootType_Models,6001,Object
+AiRootType_Datasets,6002,Object
+AiRootType_Deployments,6003,Object
+AiRootType_LearningJobs,6004,Object
+AiRootType_SpecificationVersion,6005,Variable
+ModelType,1002,ObjectType
+ModelType_ModelId,6006,Variable
+ModelType_Name,6007,Variable
+ModelType_Version,6008,Variable
+ModelType_Framework,6009,Variable
+ModelType_Format,6010,Variable
+ModelType_TaskKind,6011,Variable
+ModelType_Digest,6012,Variable
+ModelType_DigestAlgorithm,6013,Variable
+ModelType_ArtifactUri,6014,Variable
+ModelType_ProvenanceUri,6015,Variable
+ModelType_LabelClasses,6016,Variable
+ModelType_Inputs,6017,Variable
+ModelType_Outputs,6018,Variable
+DatasetType,1003,ObjectType
+DatasetType_DatasetId,6019,Variable
+DatasetType_Name,6020,Variable
+DatasetType_Version,6021,Variable
+DatasetType_SourceKind,6022,Variable
+DatasetType_SampleCount,6023,Variable
+DatasetType_LabelClasses,6024,Variable
+DatasetType_CreatedAt,6025,Variable
+DatasetType_ArtifactUri,6026,Variable
+DatasetType_Digest,6027,Variable
+DeploymentType,1004,ObjectType
+DeploymentType_DeploymentId,6028,Variable
+DeploymentType_InferenceLocation,6029,Variable
+DeploymentType_AcceleratorKind,6030,Variable
+DeploymentType_AcceleratorName,6031,Variable
+DeploymentType_EndpointUri,6032,Variable
+DeploymentType_LatencyBudget,6033,Variable
+DeploymentType_BatchSize,6034,Variable
+DeploymentType_State,6035,Variable
+LearningJobType,1005,ObjectType
+LearningJobType_State,6036,Variable
+LearningJobType_Dataset,6037,Variable
+LearningJobType_BaseModel,6038,Variable
+LearningJobType_CandidateModel,6039,Variable
+LearningJobType_SamplesCollected,6040,Variable
+LearningJobType_StartCollection,6041,Method
+LearningJobType_StopCollection,6042,Method
+LearningJobType_TriggerTraining,6043,Method
+LearningJobType_TriggerTraining_OutputArguments,6044,Variable
+LearningJobType_PromoteModel,6045,Method
+LearningJobType_PromoteModel_InputArguments,6046,Variable
+LearningJobType_PromoteModel_OutputArguments,6047,Variable
+AiJobType,1006,ObjectType
+AiJobType_JobId,6048,Variable
+AiJobType_LastError,6049,Variable
+AiJobType_StartedAt,6050,Variable
+AiJobType_FinishedAt,6051,Variable
+AiJobType_Progress,6052,Variable
+AiJobType_RequestedBy,6053,Variable
+ModelImportJobType,1007,ObjectType
+ModelImportJobType_Source,6054,Variable
+ModelImportJobType_ModelReference,6055,Variable
+ModelImportJobType_Mode,6056,Variable
+ModelImportJobType_TargetDeployment,6057,Variable
+ModelImportJobType_ImportedModel,6058,Variable
+ModelImportJobType_BytesTransferred,6059,Variable
+ModelImportJobType_DigestVerified,6060,Variable
+ModelImportJobType_Cancel,6061,Method
+InferenceJobType,1008,ObjectType
+InferenceJobType_Deployment,6062,Variable
+InferenceJobType_RequestPayload,6063,Variable
+InferenceJobType_RequestContentType,6064,Variable
+InferenceJobType_ResponsePayload,6065,Variable
+InferenceJobType_ResponseContentType,6066,Variable
+InferenceJobType_ModelUsed,6067,Variable
+InferenceJobType_Usage,6068,Variable
+InferenceJobType_FinishReason,6069,Variable
+InferenceJobType_SafetyAssessment,6070,Variable
+ModelSourceType,1009,ObjectType
+ModelSourceType_SourceId,6071,Variable
+ModelSourceType_EndpointUri,6072,Variable
+ModelSourceType_ApiDialect,6073,Variable
+ModelSourceType_EndpointDescriptionUri,6074,Variable
+ModelSourceType_AuthenticationKind,6075,Variable
+ModelSourceType_CredentialReference,6076,Variable
+ModelSourceType_TokenAudience,6077,Variable
+ModelSourceType_Reachability,6078,Variable
+ModelSourceType_LastSuccessAt,6079,Variable
+ModelSourceType_ConsecutiveFailures,6080,Variable
+ModelSourceType_RateLimit,6081,Variable
+ModelSourceType_Capabilities,6082,Variable
+ModelSourceType_TestConnection,6083,Method
+ModelSourceType_TestConnection_OutputArguments,6084,Variable
+ModelSourceType_ListModels,6085,Method
+ModelSourceType_ListModels_InputArguments,6086,Variable
+ModelSourceType_ListModels_OutputArguments,6087,Variable
+EvaluationRunType,1014,ObjectType
+EvaluationRunType_RunId,6088,Variable
+EvaluationRunType_EvaluatedModel,6089,Variable
+EvaluationRunType_Dataset,6090,Variable
+EvaluationRunType_CompletedAt,6091,Variable
+EvaluationRunType_Metrics,6092,Variable
+EvaluationRunType_Passed,6093,Variable
+EvaluationRunType_ReportUri,6094,Variable
+ModelCardType,1015,ObjectType
+ModelCardType_IntendedUse,6095,Variable
+ModelCardType_Limitations,6096,Variable
+ModelCardType_OutOfScopeUse,6097,Variable
+ModelCardType_License,6098,Variable
+ModelCardType_TrainingDataCutoff,6099,Variable
+ModelCardType_EthicalConsiderations,6100,Variable
+ModelCardType_ContactUri,6101,Variable
+ModelRegistryType,1010,ObjectType
+ModelPublisherType,1011,ObjectType
+AiResourceType,1016,ObjectType
+ModelResourceType,1012,ObjectType
+ModelResourceType_TaskKind,6102,Variable
+ModelResourceType_Framework,6103,Variable
+ModelResourceType_Digest,6104,Variable
+ModelResourceType_DigestAlgorithm,6105,Variable
+ModelResourceType_SizeBytes,6106,Variable
+ModelResourceType_Gated,6107,Variable
+ModelResourceType_MutableRefs,6108,Variable
+DatasetResourceType,1013,ObjectType
+DatasetResourceType_SourceKind,6109,Variable
+DatasetResourceType_SampleCount,6110,Variable
+DatasetResourceType_Digest,6111,Variable
+DatasetResourceType_DigestAlgorithm,6112,Variable
+DatasetResourceType_SizeBytes,6113,Variable
+AiRootType_Sources,6114,Object
+AiRootType_Registries,6115,Object
+AiRootType_Evaluations,6116,Object
+AiRootType_Jobs,6117,Object
+ModelType_Card,6118,Object
+ModelType_Publisher,6119,Variable
+ModelType_ParameterCount,6120,Variable
+ModelType_Quantization,6121,Variable
+ModelType_SafetyPolicyUri,6122,Variable
+DeploymentType_Source,6123,Variable
+DeploymentType_VersionBinding,6124,Variable
+DeploymentType_BoundRef,6125,Variable
+DeploymentType_FallbackPolicy,6126,Variable
+DeploymentType_Reachability,6127,Variable
+DeploymentType_ConsecutiveFailures,6128,Variable
+DeploymentType_LastSuccessAt,6129,Variable
+DeploymentType_RateLimit,6130,Variable
+DeploymentType_Capabilities,6131,Variable
+DeploymentType_DataJurisdiction,6132,Variable
+DeploymentType_EgressPermitted,6133,Variable
+DeploymentType_RetainsInput,6134,Variable
+DeploymentType_EgressPolicyUri,6135,Variable
+DeploymentType_Invoke,6136,Method
+DeploymentType_Invoke_InputArguments,6137,Variable
+DeploymentType_Invoke_OutputArguments,6138,Variable
+DeploymentType_InvokeAsync,6139,Method
+DeploymentType_InvokeAsync_InputArguments,6140,Variable
+DeploymentType_InvokeAsync_OutputArguments,6141,Variable
+DeploymentType_GetCapabilities,6142,Method
+DeploymentType_GetCapabilities_OutputArguments,6143,Variable
+ModelRegistryType_Group,6144,Object
+ModelPublisherType_Resource,6145,Object
+DeploymentType_MaxInlinePayloadSize,6146,Variable
+DeploymentType_BeginTransfer,6147,Method
+DeploymentType_BeginTransfer_InputArguments,6148,Variable
+DeploymentType_BeginTransfer_OutputArguments,6149,Variable
+AiModelManagement,7001,Object
+InferenceTransferType,1017,ObjectType
+InferenceTransferType_TransferId,6150,Variable
+InferenceTransferType_State,6151,Variable
+InferenceTransferType_Request,6152,Object
+InferenceTransferType_Response,6153,Object
+InferenceTransferType_ContentType,6154,Variable
+InferenceTransferType_ResponseContentType,6155,Variable
+InferenceTransferType_ModelUsed,6156,Variable
+InferenceTransferType_Usage,6157,Variable
+InferenceTransferType_FinishReason,6158,Variable
+InferenceTransferType_SafetyAssessment,6159,Variable
+InferenceTransferType_LastError,6160,Variable
+InferenceTransferType_ExpiresAt,6161,Variable
+InferenceTransferType_Execute,6162,Method
+InferenceTransferType_Execute_OutputArguments,6163,Variable
+InferenceTransferType_Abort,6164,Method
+ModelType_DigestProvenance,6165,Variable
+ModelResourceType_DigestProvenance,6166,Variable
+ModelImportJobType_Registry,6167,Variable
+ModelType_PublishedAt,6168,Variable
+ModelType_LastModifiedAt,6169,Variable
+ModelCardType_DeprecatedFrom,6170,Variable
+ModelCardType_SupportedUntil,6171,Variable
+DeploymentType_ApiDialect,6172,Variable
+DeploymentType_EndpointDescriptionUri,6173,Variable
+DeploymentType_RuntimeIdentity,6174,Variable
+DeploymentType_ObservedLatency,6175,Variable
+InferenceJobType_RequestUri,6176,Variable
+InferenceJobType_ResponseUri,6177,Variable
+InferenceJobType_TransferRequired,6178,Variable
+InferenceJobType_Transfer,6179,Variable
diff --git a/metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeSet2.xml b/metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeSet2.xml
new file mode 100644
index 0000000..15f4d9b
--- /dev/null
+++ b/metaverse-specs/ai-model-management/Opc.Ua.AiModelManagement.NodeSet2.xml
@@ -0,0 +1,2458 @@
+
+
+
+
+ http://opcfoundation.org/UA/xRegistry/
+ http://opcfoundation.org/UA/AI/
+
+
+
+
+
+
+
+
+ i=1
+ i=6
+ i=7
+ i=9
+ i=11
+ i=12
+ i=14
+ i=15
+ i=10
+ i=8
+ i=13
+ i=17
+ i=20
+ i=21
+ i=294
+ i=290
+ i=296
+ i=887
+ i=14533
+ i=24
+ i=47
+ i=46
+ i=45
+ i=35
+ i=40
+ i=37
+ i=17603
+ i=38
+ i=78
+ i=80
+ i=11508
+ i=11510
+
+
+ InferenceLocationEnum
+ Where inference executes. The result contract is identical in every case; this property exists so a client can reason about latency, availability and the trust boundary without changing how it reads results.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3901
+
+ In the OPC UA Server process or on its host.On a separate edge node reached over the network.In a remote or cloud service.Inside a simulator that also produces the input.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3001
+
+ OnServerEdgeOffServerCloudInSimulator
+
+
+ AcceleratorKindEnum
+ Compute device executing the model.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3902
+
+
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3002
+
+ CpuGpuNpuFpgaTpuOther
+
+
+ DeploymentStateEnum
+ Runtime lifecycle state of a deployment.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3903
+
+ Declared but not serving.Able to serve; no work in progress.Serving at least one request.Serving below configured quality.Unable to serve.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3003
+
+ InactiveReadyActiveDegradedFaulted
+
+
+ DatasetSourceEnum
+ Provenance of the samples in a dataset.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3904
+
+ Captured from physical equipment.Generated or rendered by a simulator.Both, for example synthetic pre-training with real fine-tuning.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3004
+
+ RealSyntheticMixed
+
+
+ LearningJobStateEnum
+ State of a dataset-capture, retraining and promotion cycle.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3905
+
+ A candidate model is available for promotion.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3005
+
+ IdleCollectingLabellingTrainingValidatingReadyPromotedFailed
+
+
+ FinishReasonEnum
+ Why an inference call stopped producing output. A client that treats every non-error response as complete will silently accept a truncated one, which is why this is Mandatory on a response rather than a diagnostic.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3906
+
+ The model finished normally.Output was truncated by a length or budget limit. The result is incomplete and SHALL NOT be treated as final.The model requested a tool or function call and is waiting for its result.Output was withheld by a safety policy; see the SafetyAssessment.The caller or the Server cancelled the call.The call failed; the StatusCode carries the reason.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3006
+
+ StopLengthToolCallFilteredCancelledError
+
+
+ ApiDialectEnum
+ Wire contract a remote inference endpoint speaks. A Server needs this to call an endpoint it did not deploy; without it EndpointUri is a string nobody can act on. It describes the REMOTE endpoint and never affects how an OPC UA client calls this Server.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3907
+
+ Another OPC UA Server implementing this specification's Invoke Method.The de-facto REST contract for chat and embeddings that most serving runtimes expose, including ones that run on a single workstation.The Open Inference Protocol (KServe v2) predict contract.A tensor-oriented RPC contract such as those used by dedicated inference servers.An in-process runtime reached through a local library rather than a network protocol.A contract this specification does not name. EndpointDescriptionUri SHOULD then say where it is documented.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3007
+
+ OpcUaInferenceRestChatCompletionsOpenInferenceProtocolTensorRemoteProcedureEmbeddedRuntimeProprietary
+
+
+ AuthenticationKindEnum
+ How the Server authenticates ITSELF to a remote inference endpoint. This is not how a client authenticates to this Server, which is the ordinary OPC UA Session security and is unaffected.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3908
+
+ No credential. Permitted only where the endpoint is reachable solely from a trusted network segment.A shared secret presented as a key.A token obtained from an authorization service.An identity the hosting platform assigns to the Server, so no secret is stored at all. Preferred where the platform offers it.Both ends present certificates.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3008
+
+ AnonymousApiKeyBearerTokenWorkloadIdentityMutualTls
+
+
+ FallbackPolicyEnum
+ What the Server does when a deployment cannot serve. This is the question a plant asks that no cloud inference API answers, because a cloud API assumes the caller can simply wait.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3909
+
+ Report the failure to the caller and produce nothing. The safe default: a caller that is told nothing happened can decide for itself.Continue reporting the most recent successful result, marked stale. Legitimate only where a stale answer is safe, and the caller SHALL be able to see the staleness.Route to the deployment named by the FallsBackTo reference. The answer comes from a different model and the response SHALL say so.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3009
+
+ FailHoldLastFallBackTo
+
+
+ VersionBindingEnum
+ Whether a deployment is bound to one immutable model version or follows a moving pointer. Stated structurally rather than as an upgrade policy, because what a client needs to know is whether the artefact can change under it, not what schedule someone intends to change it on.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3910
+
+ Bound to one immutable version. The artefact behind this deployment cannot change without an observable change to the deployment.Bound to a mutable pointer such as a branch or channel. The artefact CAN change without any other change, which is why clause 12 requires the resulting promotion to be as authorized as an explicit one.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3010
+
+ PinnedFollowsRef
+
+
+ ImportModeEnum
+ Whether an import job brings the model's description or its bytes.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3911
+
+ Materialize the catalogue entry as a ModelType and leave the artefact where it is. Nothing is downloaded and inference runs at the source.Fetch the artefact, verify its Digest, and make it locally available so inference can run without the source.Federate, then stage if the target deployment's InferenceLocation is OnServer or EdgeOffServer - because those cannot reach the source at inference time.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3011
+
+ FederateStageAuto
+
+
+ SafetySeverityEnum
+ Severity of one safety finding. The scale is the convergent industry one; what each level means for a given category is the policy's business, not this specification's.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3912
+
+
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3012
+
+ NoneLowMediumHigh
+
+
+ ReachabilityEnum
+ Whether the Server can currently reach a deployment's execution site.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3913
+
+ Never attempted, or the Server does not probe.The most recent attempt succeeded.The most recent attempt failed.Reachable, but the endpoint is refusing work for capacity reasons. RetryAfter SHOULD be populated.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3013
+
+ UnknownReachableUnreachableThrottled
+
+
+ TransferStateEnum
+ Stage of a chunked inference exchange. A client reads this rather than inferring progress from which Methods have succeeded, because a transfer that failed mid-write and one that has not started look alike from outside.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3914
+
+ The request is being written and is not yet complete.The request is complete and inference has not started.Inference is running.The response is readable.The exchange failed; LastError carries the reason.The Server reclaimed the transfer before it completed.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3014
+
+ BuildingReadyExecutingCompletedFailedExpired
+
+
+ DigestProvenanceEnum
+ Where a Digest came from, or why there is none. Digest is Mandatory so that its absence is uniform and browsable rather than indistinguishable from a Server that does not implement digests - but 'empty' then carries two different meanings, and a client that must decide whether to trust an artefact needs them apart. This member is what tells them apart, and it does the same job for a digest that IS present: a value the source asserted and a value this Server computed over bytes are not the same evidence, and only one of them survives a substituted artefact.
+ AiModelManagement DataTypes
+
+ i=29
+ ns=2;i=3915
+
+ There is no digest and the source does not publish one. Digest is empty. This is the honest answer for an endpoint that names models but never their content, and it is what most hosted inference APIs require.Digest carries what the source declared. No party this Server can speak for has hashed the artefact, so the value is an assertion forwarded rather than evidence held.This Server hashed the artefact it holds. The value is evidence, but nothing independent agrees with it - a substitution that happened before the Server obtained the bytes is not detected.This Server hashed the artefact during a staging import (clause 10.4) and it matched what the source declared. Two independent parties agree, which is the strongest statement this model can carry.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=2;i=3015
+
+ NotAvailableDeclaredBySourceComputedByServerVerifiedOnStage
+
+
+ TensorSignatureDataType
+ Shape and element type of one model input or output tensor. This is what lets a client check that what it intends to send matches what the model expects, before it sends it.
+ AiModelManagement DataTypes
+
+ i=22
+ ns=2;i=5001
+
+ Tensor name as declared by the model.Element type, for example float32, uint8 or int64.Dimensions; -1 marks a dynamic axis.Optional axis layout hint, for example NCHW or NHWC.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=2;i=3050
+
+
+
+ ModelReferenceDataType
+ Identity of a model as a publisher, name and version triple. Every model catalogue in practice identifies a model this way, which is why an import job takes this rather than a URL: a URL says where a copy is today, the triple says which artefact is meant.
+ AiModelManagement DataTypes
+
+ i=22
+ ns=2;i=5002
+
+ Organisation or namespace that published the model.Model name within that publisher.Immutable version identifier, or a mutable pointer such as a branch or channel name. Which one it is is stated by VersionBinding, not guessable from the string.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=2;i=3051
+
+
+
+ UsageDataType
+ What one inference call consumed. Deliberately NOT named in tokens: a token is one accounting unit among several, and a model that consumes images, samples or seconds of audio needs the same accounting. UnitKind says which unit the counts are in.
+ AiModelManagement DataTypes
+
+ i=22
+ ns=2;i=5003
+
+ Unit the counts are expressed in, for example 'tokens', 'images', 'samples' or 'seconds'.Units consumed by the input.Units produced as output.Total units billed or metered for the call, which is not always the sum: cached or deduplicated input may be counted once.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=2;i=3052
+
+
+
+ CapabilityDataType
+ One capability a deployment does or does not have. An open list rather than an enumeration because the set of things a model can do is not closed, and a client that cannot recognise a capability name is no worse off than one that cannot recognise an enumeration value it has never seen.
+ AiModelManagement DataTypes
+
+ i=22
+ ns=2;i=5004
+
+ Capability name, for example 'chat', 'embeddings', 'streaming', 'tool-call' or 'structured-output'.Whether this deployment supports it.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=2;i=3053
+
+
+
+ SafetyAssessmentDataType
+ One finding from a safety policy applied to an inference call. Category is a String and not an enumeration because harm categories are set by the policy an installation adopts, and an industrial taxonomy looks nothing like a consumer one.
+ AiModelManagement DataTypes
+
+ i=22
+ ns=2;i=5005
+
+ Category the policy assessed, for example 'out-of-distribution-input' or a policy-defined name.Severity of the finding.True when the content was withheld or altered rather than merely flagged.Human-readable explanation. For a human; SHALL NOT be parsed.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=2;i=3054
+
+
+
+ EvaluationMetricDataType
+ One measured metric from an evaluation run, with the threshold it was judged against. The threshold travels with the metric because a metric without its acceptance criterion cannot be acted on, and a reviewer reading it a year later has no way to recover what 'good' meant.
+ AiModelManagement DataTypes
+
+ i=22
+ ns=2;i=5006
+
+ Metric name, for example 'accuracy' or 'false-negative-rate'.Measured value.Unit of the value, or empty when dimensionless.Acceptance threshold applied.How Value was compared with Threshold: one of '>=', '<=', '>', '<' or '=='.Outcome of that comparison.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=2;i=3055
+
+
+
+ RateLimitDataType
+ Capacity a remote endpoint is currently granting. Surfaced so a client can distinguish 'the model said no' from 'the quota said no', which are different faults with different remedies.
+ AiModelManagement DataTypes
+
+ i=22
+ ns=2;i=5007
+
+ Unit the limit is expressed in, matching UsageDataType.UnitKind, or 'requests'.Units permitted per interval, or 0 when not published.Units still available in the current interval.Length of the interval the limit applies to.How long to wait before retrying. Zero when the endpoint gave no guidance.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=2;i=3056
+
+
+
+ UsesModel
+ Links a Deployment to the Model it executes. Clause 6.5 requires exactly one such reference per deployment; it is the only defined path from a result to the model artefact and its Digest, on which the provenance requirement of clause 12 depends.
+ AiModelManagement ReferenceTypes
+ IsUsedByDeployment
+
+ i=32
+
+
+
+ TrainedOn
+ Links a Model to a Dataset it was trained or validated on. A model whose training data cannot be named is a model whose behaviour cannot be explained, which is why this reference exists rather than a string.
+ AiModelManagement ReferenceTypes
+ IsTrainingDataFor
+
+ i=32
+
+
+
+ DerivedFrom
+ Links a Model to the Model it was fine-tuned, distilled or quantized from. Lineage is a chain, not a field: a model three derivations from its base is answerable for all three, and a string naming the immediate parent cannot be walked.
+ AiModelManagement ReferenceTypes
+ IsBaseOfModel
+
+ i=32
+
+
+
+ FallsBackTo
+ Links a Deployment to the Deployment that serves in its place when it cannot. Clause 9 forbids a cycle, and requires the response to say which deployment actually answered.
+ AiModelManagement ReferenceTypes
+ IsFallbackFor
+
+ i=32
+
+
+
+ ImportedFrom
+ Links a Model to the catalogue resource an import job materialized it from. This is what makes 'where did this model come from' answerable after the fact, rather than only at the moment of import.
+ AiModelManagement ReferenceTypes
+ WasImportedAs
+
+ i=32
+
+
+
+ EvaluatedBy
+ Links a Model to an EvaluationRun that measured it. Optional and repeating: a model may be evaluated many times, and the run that gated its promotion is not necessarily the last one.
+ AiModelManagement ReferenceTypes
+ Evaluates
+
+ i=32
+
+
+
+ AiRootType
+ Server-level entry point. A client that has just connected browses here to find every model, dataset, deployment and learning job the Server describes, without knowing its layout.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6001
+ ns=2;i=6002
+ ns=2;i=6003
+ ns=2;i=6004
+ ns=2;i=6005
+ ns=2;i=6114
+ ns=2;i=6115
+ ns=2;i=6116
+ ns=2;i=6117
+
+
+
+ Models
+ ModelType instances.
+
+ i=78
+ i=61
+ ns=2;i=1001
+
+
+
+ Datasets
+ DatasetType instances.
+
+ i=80
+ i=61
+ ns=2;i=1001
+
+
+
+ Deployments
+ DeploymentType instances.
+
+ i=78
+ i=61
+ ns=2;i=1001
+
+
+
+ LearningJobs
+ LearningJobType instances.
+
+ i=80
+ i=61
+ ns=2;i=1001
+
+
+
+ SpecificationVersion
+ Release of this specification the Server implements, for example '0.1.0'.
+
+ i=78
+ i=68
+ ns=2;i=1001
+
+
+
+ ModelType
+ Nameplate of a trained model. The member set is deliberately aligned with the IDTA 02060 AI Model Nameplate submodel template, which is currently the only standardised description of an industrial AI model, so an Asset Administration Shell can be populated from this node without loss.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6006
+ ns=2;i=6007
+ ns=2;i=6008
+ ns=2;i=6009
+ ns=2;i=6010
+ ns=2;i=6011
+ ns=2;i=6012
+ ns=2;i=6013
+ ns=2;i=6014
+ ns=2;i=6015
+ ns=2;i=6016
+ ns=2;i=6017
+ ns=2;i=6018
+ ns=2;i=6118
+ ns=2;i=6119
+ ns=2;i=6120
+ ns=2;i=6121
+ ns=2;i=6122
+ ns=2;i=6165
+ ns=2;i=6168
+ ns=2;i=6169
+
+
+
+ ModelId
+ Identifier of the model.
+
+ i=78
+ i=68
+ ns=2;i=1002
+
+
+
+ Name
+ Human-readable model name. Its Text SHALL be the name the source system uses for the model, carried across unchanged. A LocalizedText because the base model types names that way and retyping it would break every implementation, but the localizable part is the presentation: a Server MAY add a translation for display and SHALL NOT translate, reformat or prettify the Text itself. Two Servers that fetched one model from two mirrors are meant to produce the same string, and a name adjusted for house style is a name that no longer matches.
+
+ i=78
+ i=68
+ ns=2;i=1002
+
+
+
+ Version
+ Model version.
+
+ i=78
+ i=68
+ ns=2;i=1002
+
+
+
+ Framework
+ Producing framework, for example PyTorch, TensorFlow or scikit-learn.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ Format
+ Serialization format, for example ONNX, TensorRT or OpenVINO IR.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ TaskKind
+ What the model does, for example Detection2D, Classification, Segmentation, Forecasting or AnomalyDetection. Free text because the set of tasks is not closed and a closed enumeration would date faster than the model does.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ Digest
+ Cryptographic digest of the model artefact, for provenance and integrity. Mandatory: clause 12 requires it for every model whose artefact is obtainable through ArtifactUri, and it is the terminus of the provenance chain that UsesModel keeps intact.
+
+ i=78
+ i=68
+ ns=2;i=1002
+
+
+
+ DigestAlgorithm
+ Hash function used for Digest. SHALL name a function with at least 256-bit output and no known collision weakness; SHA-256 is the default and is always acceptable. SHALL NOT be MD5, SHA-1 or a truncated variant - chosen-prefix collisions against those are practical, so a substituted artefact would pass verification. SHALL be non-empty where Digest is non-empty. See clause 12.
+
+ i=78
+ i=68
+ ns=2;i=1002
+
+
+
+ ArtifactUri
+ Where the model artefact can be obtained. Treated as untrusted input.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ ProvenanceUri
+ Training provenance or model card location.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ LabelClasses
+ Ordered class label set, where the model produces classified output. The INDEX is what a consuming specification's class identifier refers to, so the order is part of the contract and a Server SHALL NOT reorder it in place.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ Inputs
+ Input tensor signatures.
+
+ i=80
+ i=63
+ ns=2;i=1002
+
+
+
+ Outputs
+ Output tensor signatures.
+
+ i=80
+ i=63
+ ns=2;i=1002
+
+
+
+ DatasetType
+ A dataset used to train or validate a model. Aligned with the IDTA 02058 AI Dataset submodel template. SourceKind distinguishes real capture from simulator output, which is the provenance a reviewer needs when synthetic data is involved.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6019
+ ns=2;i=6020
+ ns=2;i=6021
+ ns=2;i=6022
+ ns=2;i=6023
+ ns=2;i=6024
+ ns=2;i=6025
+ ns=2;i=6026
+ ns=2;i=6027
+
+
+
+ DatasetId
+ Identifier of the dataset.
+
+ i=78
+ i=68
+ ns=2;i=1003
+
+
+
+ Name
+ Human-readable dataset name.
+
+ i=80
+ i=68
+ ns=2;i=1003
+
+
+
+ Version
+ Dataset version.
+
+ i=80
+ i=68
+ ns=2;i=1003
+
+
+
+ SourceKind
+ Whether samples are real, synthetic or mixed.
+
+ i=78
+ i=68
+ ns=2;i=1003
+
+
+
+ SampleCount
+ Number of samples.
+
+ i=80
+ i=68
+ ns=2;i=1003
+
+
+
+ LabelClasses
+ Class labels present.
+
+ i=80
+ i=68
+ ns=2;i=1003
+
+
+
+ CreatedAt
+ Creation time.
+
+ i=80
+ i=68
+ ns=2;i=1003
+
+
+
+ ArtifactUri
+ Where the dataset can be obtained. Treated as untrusted input.
+
+ i=80
+ i=68
+ ns=2;i=1003
+
+
+
+ Digest
+ Digest of the dataset artefact.
+
+ i=80
+ i=68
+ ns=2;i=1003
+
+
+
+ DeploymentType
+ A model made executable somewhere. Aligned with the IDTA 02059 AI Deployment submodel template. InferenceLocation is the on-server versus off-server switch: it changes where the computation happens and therefore the trust boundary, and it changes nothing else.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6028
+ ns=2;i=6029
+ ns=2;i=6030
+ ns=2;i=6031
+ ns=2;i=6032
+ ns=2;i=6033
+ ns=2;i=6034
+ ns=2;i=6035
+ ns=2;i=6123
+ ns=2;i=6124
+ ns=2;i=6125
+ ns=2;i=6126
+ ns=2;i=6127
+ ns=2;i=6128
+ ns=2;i=6129
+ ns=2;i=6130
+ ns=2;i=6131
+ ns=2;i=6132
+ ns=2;i=6133
+ ns=2;i=6134
+ ns=2;i=6135
+ ns=2;i=6136
+ ns=2;i=6139
+ ns=2;i=6142
+ ns=2;i=6146
+ ns=2;i=6147
+ ns=2;i=6172
+ ns=2;i=6173
+ ns=2;i=6174
+ ns=2;i=6175
+
+
+
+ DeploymentId
+ Identifier of the deployment.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ InferenceLocation
+ Where inference executes.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ AcceleratorKind
+ Compute device executing the model.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ AcceleratorName
+ Free-text accelerator identification, for example an NPU or GPU part name.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ EndpointUri
+ Inference endpoint when InferenceLocation is not OnServer. Treated as untrusted input and subject to the resolver policy of clause 12.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ LatencyBudget
+ Latency the deployment is expected to meet. Set by whoever commissioned the deployment; ObservedLatency is what it actually achieved, and clause 6.4.3 compares the two.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ BatchSize
+ Configured inference batch size.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ State
+ Runtime state of the deployment.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ LearningJobType
+ One turn of the capture, label, train and promote loop. It exists so that corrections arriving from a consuming application have somewhere to accumulate and a defined path into a new model version. A Server may implement only the capture stages and leave training to an external MLOps system - the state machine is the same either way.
+ AiModelManagement
+
+ ns=2;i=1006
+ ns=2;i=6036
+ ns=2;i=6037
+ ns=2;i=6038
+ ns=2;i=6039
+ ns=2;i=6040
+ ns=2;i=6041
+ ns=2;i=6042
+ ns=2;i=6043
+ ns=2;i=6045
+
+
+
+ State
+ Current stage of the loop. This is the PHASE, not the program lifecycle: the inherited CurrentState says whether the job is running, this says what it is doing. Clause 7 requires the two to agree.
+
+ i=78
+ i=68
+ ns=2;i=1005
+
+
+
+ Dataset
+ Dataset being accumulated or used.
+
+ i=80
+ i=68
+ ns=2;i=1005
+
+
+
+ BaseModel
+ Model the job starts from.
+
+ i=80
+ i=68
+ ns=2;i=1005
+
+
+
+ CandidateModel
+ Model produced by the job, awaiting promotion.
+
+ i=80
+ i=68
+ ns=2;i=1005
+
+
+
+ SamplesCollected
+ Samples accumulated so far, including corrections fed back.
+
+ i=80
+ i=68
+ ns=2;i=1005
+
+
+
+ StartCollection
+ Begin accumulating samples and corrections into the dataset.
+
+ i=80
+ ns=2;i=1005
+
+
+
+ StopCollection
+ Stop accumulating samples.
+
+ i=80
+ ns=2;i=1005
+
+
+
+ TriggerTraining
+ Request that a candidate model be trained from the collected dataset.
+
+ i=80
+ ns=2;i=1005
+ ns=2;i=6044
+
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6043
+
+ i=297Acceptedi=1-1True when the request was queued.
+
+
+ PromoteModel
+ Promote the candidate model so that deployments begin using it. A Server SHALL require a distinct authorization for this Method: it changes what the equipment does without changing anything a reader of the address space would notice, which is precisely the change that needs a separate permission.
+
+ i=80
+ ns=2;i=1005
+ ns=2;i=6046
+ ns=2;i=6047
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=2;i=6045
+
+ i=297Deploymenti=17-1Deployment to update, or null for all.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6045
+
+ i=297PromotedModeli=17-1The model now in use.
+
+
+ AiJobType
+ Abstract base of every long-running AI operation: learning, model import and asynchronous inference. It derives from the OPC 10000-10 ProgramStateMachineType, so the lifecycle - Ready, Running, Suspended, Halted - its transition events and its Start/Suspend/Resume/Halt Methods are inherited rather than reinvented, and every job in this model is auditable the same way.
+ AiModelManagement
+
+ i=2391
+ ns=2;i=6048
+ ns=2;i=6049
+ ns=2;i=6050
+ ns=2;i=6051
+ ns=2;i=6052
+ ns=2;i=6053
+
+
+
+ JobId
+ Identifier of the job, unique within the Server.
+
+ i=78
+ i=68
+ ns=2;i=1006
+
+
+
+ LastError
+ Diagnostic for the most recent failure. For a human; SHALL NOT be parsed.
+
+ i=80
+ i=68
+ ns=2;i=1006
+
+
+
+ StartedAt
+ When the job last entered Running.
+
+ i=80
+ i=68
+ ns=2;i=1006
+
+
+
+ FinishedAt
+ When the job last left Running, or null while it is running.
+
+ i=80
+ i=68
+ ns=2;i=1006
+
+
+
+ Progress
+ Fraction complete, 0.0 to 1.0, or null where the job cannot estimate it. A Server SHALL NOT report a value it is guessing: null is informative, a fabricated 0.5 is not.
+
+ i=80
+ i=68
+ ns=2;i=1006
+
+
+
+ RequestedBy
+ Identity that requested the job, recorded at the moment it started. Clause 12 requires this for any job that can promote a model.
+
+ i=80
+ i=68
+ ns=2;i=1006
+
+
+
+ ModelImportJobType
+ Brings a model from a catalogue into this Server. It federates by default - materializing the catalogue entry as a ModelType whose artefact stays where it is - and stages the artefact when the target deployment could not otherwise reach it. Staging is the moment a substituted artefact would enter, which is why clause 10 requires the Digest to be verified there and nowhere else.
+ AiModelManagement
+
+ ns=2;i=1006
+ ns=2;i=6054
+ ns=2;i=6055
+ ns=2;i=6056
+ ns=2;i=6057
+ ns=2;i=6058
+ ns=2;i=6059
+ ns=2;i=6060
+ ns=2;i=6061
+ ns=2;i=6167
+
+
+
+ Source
+ ModelSourceType instance the model is pulled from, where the import calls an endpoint. Null where the import reads a catalogue instead, in which case Registry names it. Exactly one of the two is non-null.
+
+ i=78
+ i=68
+ ns=2;i=1007
+
+
+
+ ModelReference
+ Publisher, name and version being imported.
+
+ i=78
+ i=68
+ ns=2;i=1007
+
+
+
+ Mode
+ Whether to federate, stage, or decide from the target's InferenceLocation.
+
+ i=78
+ i=68
+ ns=2;i=1007
+
+
+
+ TargetDeployment
+ Deployment to create or update on success, or null to import the model without deploying it.
+
+ i=80
+ i=68
+ ns=2;i=1007
+
+
+
+ ImportedModel
+ ModelType instance the job produced. Null until the job succeeds.
+
+ i=80
+ i=68
+ ns=2;i=1007
+
+
+
+ BytesTransferred
+ Artefact bytes fetched so far. Zero for a federating import, which moves none.
+
+ i=80
+ i=68
+ ns=2;i=1007
+
+
+
+ DigestVerified
+ Whether the staged artefact's computed digest matched the one the catalogue declared. False on a staging import means the artefact SHALL NOT be deployed.
+
+ i=80
+ i=68
+ ns=2;i=1007
+
+
+
+ Cancel
+ Abandon the import. A partially staged artefact SHALL be discarded rather than left where a later deployment could pick it up.
+
+ i=80
+ ns=2;i=1007
+
+
+
+ InferenceJobType
+ One asynchronous inference request. It exists because not every inference returns while the caller waits: a batch scored overnight and a long analysis over recorded data are ordinary industrial cases, and modelling them as a Method that blocks for hours is not.
+ AiModelManagement
+
+ ns=2;i=1006
+ ns=2;i=6062
+ ns=2;i=6063
+ ns=2;i=6064
+ ns=2;i=6065
+ ns=2;i=6066
+ ns=2;i=6067
+ ns=2;i=6068
+ ns=2;i=6069
+ ns=2;i=6070
+ ns=2;i=6176
+ ns=2;i=6177
+ ns=2;i=6178
+ ns=2;i=6179
+
+
+
+ Deployment
+ Deployment executing the request.
+
+ i=78
+ i=68
+ ns=2;i=1008
+
+
+
+ RequestPayload
+ Request body, encoded as RequestContentType states.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ RequestContentType
+ Media type of RequestPayload.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ ResponsePayload
+ Response body once the job succeeds.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ ResponseContentType
+ Media type of ResponsePayload.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ ModelUsed
+ Model that ACTUALLY executed the request, which is not always the one the deployment named when the job was submitted - a fallback or a followed reference can change it in between. The provenance chain of clause 12 walks this, not the deployment's current model.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ Usage
+ What the call consumed.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ FinishReason
+ Why the call stopped producing output.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ SafetyAssessment
+ Findings from the safety policy, if any were applied.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ ModelSourceType
+ An externally hosted inference or catalogue endpoint this Server can reach. It carries everything needed to actually call something the Server did not deploy - the wire contract, how to authenticate, what the endpoint can do and whether it is answering - because a URI on its own is a string nobody can act on.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6071
+ ns=2;i=6072
+ ns=2;i=6073
+ ns=2;i=6074
+ ns=2;i=6075
+ ns=2;i=6076
+ ns=2;i=6077
+ ns=2;i=6078
+ ns=2;i=6079
+ ns=2;i=6080
+ ns=2;i=6081
+ ns=2;i=6082
+ ns=2;i=6083
+ ns=2;i=6085
+
+
+
+ SourceId
+ Identifier of the source.
+
+ i=78
+ i=68
+ ns=2;i=1009
+
+
+
+ EndpointUri
+ Base URI of the endpoint. Untrusted input, subject to the resolver policy of clause 12.
+
+ i=78
+ i=68
+ ns=2;i=1009
+
+
+
+ ApiDialect
+ Wire contract the endpoint speaks.
+
+ i=78
+ i=68
+ ns=2;i=1009
+
+
+
+ EndpointDescriptionUri
+ Where the contract is documented. SHOULD be populated when ApiDialect is Proprietary, because otherwise nothing in the address space says how to call it.
+
+ i=80
+ i=68
+ ns=2;i=1009
+
+
+
+ AuthenticationKind
+ How the Server authenticates itself to the endpoint.
+
+ i=78
+ i=68
+ ns=2;i=1009
+
+
+
+ CredentialReference
+ Opaque handle naming the credential in whatever store the Server uses. It is a NAME, never a secret: clause 12 forbids a Server from exposing credential material through any Attribute of this model, and a client that can read this value learns only which credential is used, not what it is.
+
+ i=80
+ i=68
+ ns=2;i=1009
+
+
+
+ TokenAudience
+ Audience or scope a bearer token is requested for, where AuthenticationKind is BearerToken.
+
+ i=80
+ i=68
+ ns=2;i=1009
+
+
+
+ Reachability
+ Whether the Server can currently reach the endpoint.
+
+ i=78
+ i=68
+ ns=2;i=1009
+
+
+
+ LastSuccessAt
+ When the endpoint last answered successfully.
+
+ i=80
+ i=68
+ ns=2;i=1009
+
+
+
+ ConsecutiveFailures
+ Failures since the last success. Reset to zero on success.
+
+ i=80
+ i=68
+ ns=2;i=1009
+
+
+
+ RateLimit
+ Capacity the endpoint is currently granting.
+
+ i=80
+ i=68
+ ns=2;i=1009
+
+
+
+ Capabilities
+ What the endpoint reports it can do.
+
+ i=80
+ i=68
+ ns=2;i=1009
+
+
+
+ TestConnection
+ Probe the endpoint and update Reachability. Defined so that a commissioning engineer can establish that credentials and network policy are right BEFORE a deployment depends on them, rather than discovering it from a failed inference.
+
+ i=80
+ ns=2;i=1009
+ ns=2;i=6084
+
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6083
+
+ i=297Reachablei=1-1Whether the probe succeeded.i=297Detaili=21-1Diagnostic. For a human.
+
+
+ ListModels
+ Enumerate the models the source offers.
+
+ i=80
+ ns=2;i=1009
+ ns=2;i=6086
+ ns=2;i=6087
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=2;i=6085
+
+ i=297Filteri=12-1Optional substring or expression; empty for all.i=297MaxResultsi=7-1Upper bound on returned entries.i=297ContinuationPointi=15-1Empty on the first call; otherwise the value the previous call returned. A cap without a cursor bounds the response and puts every entry past it out of reach, which against a public catalogue means most of them.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6085
+
+ i=297Modelsns=2;i=305110Publisher, name and version of each model offered.i=297ContinuationPointi=15-1Pass to the next call to continue. Empty when the enumeration is complete, which is how a client knows to stop rather than by comparing counts.
+
+
+ EvaluationRunType
+ One measurement of a model against a dataset. It is a first-class object and not a field on the model because the same model is evaluated many times, and because the run that gated a promotion has to remain readable afterwards to answer why the promotion was allowed.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6088
+ ns=2;i=6089
+ ns=2;i=6090
+ ns=2;i=6091
+ ns=2;i=6092
+ ns=2;i=6093
+ ns=2;i=6094
+
+
+
+ RunId
+ Identifier of the run.
+
+ i=78
+ i=68
+ ns=2;i=1014
+
+
+
+ EvaluatedModel
+ Model that was measured.
+
+ i=78
+ i=68
+ ns=2;i=1014
+
+
+
+ Dataset
+ Dataset the model was measured against.
+
+ i=80
+ i=68
+ ns=2;i=1014
+
+
+
+ CompletedAt
+ When the run finished.
+
+ i=80
+ i=68
+ ns=2;i=1014
+
+
+
+ Metrics
+ Measured metrics, each with the threshold it was judged against.
+
+ i=78
+ i=68
+ ns=2;i=1014
+
+
+
+ Passed
+ Whether every metric met its threshold. A Server SHALL NOT report true while any entry in Metrics has Passed false - a summary that disagrees with its own detail is worse than no summary.
+
+ i=78
+ i=68
+ ns=2;i=1014
+
+
+
+ ReportUri
+ Where the full report lives. Untrusted input, subject to clause 12.
+
+ i=80
+ i=68
+ ns=2;i=1014
+
+
+
+ ModelCardType
+ What a human needs to decide whether a model may be used here: what it is for, where it stops working, and under what terms. Separate from the nameplate because a nameplate answers 'which artefact is this' and a card answers 'should this be running on my line'.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6095
+ ns=2;i=6096
+ ns=2;i=6097
+ ns=2;i=6098
+ ns=2;i=6099
+ ns=2;i=6100
+ ns=2;i=6101
+ ns=2;i=6170
+ ns=2;i=6171
+
+
+
+ IntendedUse
+ What the model is for.
+
+ i=78
+ i=68
+ ns=2;i=1015
+
+
+
+ Limitations
+ Where it is known not to work. Mandatory because a card that lists only capabilities is marketing, and the failure modes are the half a commissioning engineer needs.
+
+ i=78
+ i=68
+ ns=2;i=1015
+
+
+
+ OutOfScopeUse
+ Uses the supplier explicitly excludes.
+
+ i=80
+ i=68
+ ns=2;i=1015
+
+
+
+ License
+ Licence identifier or URI governing use of the artefact.
+
+ i=80
+ i=68
+ ns=2;i=1015
+
+
+
+ TrainingDataCutoff
+ Latest date represented in the training data. A model cannot know about anything after this, which is often the explanation for a field failure.
+
+ i=80
+ i=68
+ ns=2;i=1015
+
+
+
+ EthicalConsiderations
+ Risks the supplier records.
+
+ i=80
+ i=68
+ ns=2;i=1015
+
+
+
+ ContactUri
+ Where to report a problem with the model.
+
+ i=80
+ i=68
+ ns=2;i=1015
+
+
+
+ ModelRegistryType
+ A catalogue of models and the datasets they were trained on. It narrows the abstract registry's group placeholder to model publishers, so that a client browsing it knows what it will find rather than discovering it.
+ AiModelManagement
+
+ ns=1;i=63000
+ ns=2;i=6144
+
+
+
+ ModelPublisherType
+ One publisher's namespace within a model registry: the organisation or project that released the models it contains. Publisher is the first element of the publisher/name/version triple by which every catalogue in practice identifies a model.
+ AiModelManagement
+
+ ns=1;i=63001
+ ns=2;i=6145
+
+
+
+ AiResourceType
+ Abstract base of everything a model registry holds. It exists so that the inherited <Resource> placeholder can be narrowed ONCE to something that admits models and datasets and nothing else - a publisher holds both, and a placeholder can be overridden only by one declaration.
+ AiModelManagement
+
+ ns=1;i=63002
+
+
+
+ ModelResourceType
+ One model in a catalogue. Its versions are immutable and identified by content, so a version that has been seen cannot change meaning; mutable names such as a branch or a release channel are pointers AT versions, never versions themselves. Because the base type is a FileType, a Server that holds the artefact serves it through the inherited Open, Read and Close; one that only describes it leaves those unimplemented and points at the artefact instead.
+ AiModelManagement
+
+ ns=2;i=1016
+ ns=2;i=6102
+ ns=2;i=6103
+ ns=2;i=6104
+ ns=2;i=6105
+ ns=2;i=6106
+ ns=2;i=6107
+ ns=2;i=6108
+ ns=2;i=6166
+
+
+
+ TaskKind
+ What the model does, for example 'object-detection' or 'anomaly-detection'. A String and not an enumeration, for the same reason it is one on ModelType: the set is not closed, and every catalogue in practice uses a free tag here.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ Framework
+ Runtime or library the artefact targets.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ Digest
+ Digest of the artefact this version names, as the catalogue declares it. A staging import compares its own computed digest with this and refuses on mismatch.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ DigestAlgorithm
+ Algorithm of Digest. Subject to the strength requirement of clause 12.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ SizeBytes
+ Artefact size, so a staging import can decide whether it has room before it starts rather than after it fails.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ Gated
+ Whether obtaining the artefact requires an acceptance or entitlement beyond ordinary authentication. A client that ignores this discovers it as a failure part-way through a staging import.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ MutableRefs
+ Mutable pointers this resource publishes - branches, tags or channels - that a deployment may follow instead of pinning. Naming them is what makes VersionBinding FollowsRef checkable.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ DatasetResourceType
+ One dataset in a catalogue, a sibling of ModelResourceType rather than something beneath it: a dataset outlives the models trained on it and is cited by several.
+ AiModelManagement
+
+ ns=2;i=1016
+ ns=2;i=6109
+ ns=2;i=6110
+ ns=2;i=6111
+ ns=2;i=6112
+ ns=2;i=6113
+
+
+
+ SourceKind
+ Whether the samples are real, synthetic or mixed.
+
+ i=80
+ i=68
+ ns=2;i=1013
+
+
+
+ SampleCount
+ Samples in the dataset.
+
+ i=80
+ i=68
+ ns=2;i=1013
+
+
+
+ Digest
+ Digest of the dataset artefact as the catalogue declares it.
+
+ i=80
+ i=68
+ ns=2;i=1013
+
+
+
+ DigestAlgorithm
+ Algorithm of Digest.
+
+ i=80
+ i=68
+ ns=2;i=1013
+
+
+
+ SizeBytes
+ Dataset size.
+
+ i=80
+ i=68
+ ns=2;i=1013
+
+
+
+ Sources
+ ModelSourceType instances - the externally hosted endpoints and catalogues this Server can reach.
+
+ i=80
+ i=61
+ ns=2;i=1001
+
+
+
+ Registries
+ ModelRegistryType instances this Server serves or mirrors.
+
+ i=80
+ i=61
+ ns=2;i=1001
+
+
+
+ Evaluations
+ EvaluationRunType instances.
+
+ i=80
+ i=61
+ ns=2;i=1001
+
+
+
+ Jobs
+ Import and asynchronous inference jobs. Learning jobs remain under LearningJobs.
+
+ i=80
+ i=61
+ ns=2;i=1001
+
+
+
+ Card
+ What a human needs to decide whether this model may run here.
+
+ i=80
+ ns=2;i=1015
+ ns=2;i=1002
+
+
+
+ Publisher
+ Organisation or namespace that published the model. With Name and Version this is the triple every catalogue identifies a model by, and it is what makes the same model recognisable across two installations that fetched it from different mirrors.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ ParameterCount
+ Parameters in the model, or 0 where not published. A crude but universally available proxy for what it will cost to run.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ Quantization
+ Numeric precision the artefact is stored in, for example 'fp32', 'int8' or 'fp8'. A quantized model is a DIFFERENT artefact with different behaviour, not a packaging detail, which is why it is stated rather than left to the format string.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ SafetyPolicyUri
+ Safety or content policy applied to this model's output, where one is. Untrusted input, subject to clause 12.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ Source
+ ModelSourceType instance this deployment executes through, where inference is not local. Null when InferenceLocation is OnServer.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ VersionBinding
+ Whether the deployment is pinned to an immutable model version or follows a mutable pointer.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ BoundRef
+ The mutable pointer being followed, where VersionBinding is FollowsRef. Empty when Pinned.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ FallbackPolicy
+ What the Server does when this deployment cannot serve.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ Reachability
+ Whether the execution site is currently reachable. Always Reachable for an OnServer deployment that is not Faulted.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ ConsecutiveFailures
+ Failed calls since the last success.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ LastSuccessAt
+ When this deployment last answered successfully. With FallbackPolicy HoldLast this is how a caller judges whether the held answer is still worth having.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ RateLimit
+ Capacity the execution site is currently granting.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ Capabilities
+ What this deployment can do. A client checks here before calling a typed profile rather than discovering the answer from a rejection.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ DataJurisdiction
+ Where input data is processed, named in whatever scheme the operator uses - a site, a legal jurisdiction, or a named zone. This is the question a plant actually asks, and no amount of latency or accuracy data answers it.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ EgressPermitted
+ Whether calling this deployment sends input data outside the operator's boundary. A Server SHALL set this true for every deployment whose InferenceLocation is Cloud, and SHALL NOT set it false merely because the channel is encrypted - the question is where the data goes, not who can read it in flight.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ RetainsInput
+ Whether the execution site retains input beyond serving the request, for example for provider-side logging or training. Unknown is not a value: a Server that cannot establish this SHALL report true, because the safe assumption is the one that keeps data in.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ EgressPolicyUri
+ Where the governing data policy is documented.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ Invoke
+ Run inference and return the result. The payload is opaque here: what goes in and comes out is the consuming specification's vocabulary, and an envelope that tried to type it would have to be extended for every domain. What this Method fixes is everything AROUND the payload - routing, parameters, accounting, why it stopped, and which model actually ran.
+
+The signature does not change with InferenceLocation. A deployment served from the Server's own process and one served from a remote service are called identically; the location changes the trust boundary and the latency, and nothing else.
+
+ i=80
+ ns=2;i=1004
+ ns=2;i=6137
+ ns=2;i=6138
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=2;i=6136
+
+ i=297Payloadi=15-1Request body.i=297PayloadUrii=12-1Location the request body is read from, where it is supplied by reference rather than carried. A Server SHALL accept exactly one of Payload and PayloadUri and SHALL reject a call supplying both or neither. Untrusted input subject to clause 12.2, and named data the execution site will read, so clause 9.5 applies to it.i=297ContentTypei=12-1Media type of Payload.i=297Parametersi=1453310Call parameters such as a sampling temperature or an output length bound. A Server SHALL reject a parameter it does not support rather than ignore it: a caller whose parameter was silently dropped believes it took effect.i=297Timeouti=290-1How long the caller will wait. Zero means the Server's default.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6136
+
+ i=297ResponsePayloadi=15-1Response body.i=297ResponseContentTypei=12-1Media type of ResponsePayload.i=297ModelUsedi=17-1The model that ACTUALLY produced this response. Not necessarily the one the deployment names now: a fallback answered from a different deployment, and a FollowsRef binding may have moved. The provenance chain of clause 12 walks this.i=297Usagens=2;i=3052-1What the call consumed.i=297FinishReasonns=2;i=3006-1Why output stopped. A caller that ignores this will accept a truncated answer as a complete one.i=297SafetyAssessmentns=2;i=305410Findings from the safety policy, if any applied.i=297RetryAfteri=290-1How long to wait before retrying, where the failure was a capacity one. Zero when retrying immediately is as good as waiting, and meaningless when the failure was not retryable.i=297TransferRequiredi=1-1True when the deployment produced a response too large to return inline. ResponsePayload is then empty and the work is NOT lost - Transfer names where to read it. A client that ignores this reads an empty payload and concludes the model returned nothing.i=297Transferi=17-1InferenceTransferType instance holding the response, where TransferRequired is true. Null otherwise.
+
+
+ InvokeAsync
+ Submit inference to be completed later, returning immediately with the job that will carry the result. For work that does not finish while a caller waits - a batch scored overnight, an analysis over recorded data.
+
+ i=80
+ ns=2;i=1004
+ ns=2;i=6140
+ ns=2;i=6141
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=2;i=6139
+
+ i=297Payloadi=15-1Request body.i=297PayloadUrii=12-1Location the request body is read from, where it is supplied by reference rather than carried. Exactly one of Payload and PayloadUri on the same terms as Invoke. This is the argument that lets a batch already sitting in the plant's object store be scored without being copied through the Session first.i=297ContentTypei=12-1Media type of Payload.i=297Parametersi=1453310Call parameters.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6139
+
+ i=297Jobi=17-1InferenceJobType instance tracking the request. The caller subscribes to it rather than polling.
+
+
+ GetCapabilities
+ Report what this deployment can do, refreshed from the execution site rather than from cache. Defined because a remote endpoint's capabilities change without anything in this address space changing.
+
+ i=80
+ ns=2;i=1004
+ ns=2;i=6143
+
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6142
+
+ i=297Capabilitiesns=2;i=305310Current capabilities.
+
+
+ <Group>
+ A publisher namespace held by this registry. Narrows the inherited placeholder so a model registry admits ModelPublisherType and nothing else.
+
+ i=11508
+ ns=2;i=1011
+ ns=2;i=1010
+
+
+
+ <Resource>
+ A model or dataset published in this namespace. Narrows the inherited placeholder to this model's own resource types.
+
+ i=11508
+ ns=2;i=1016
+ ns=2;i=1011
+
+
+
+ MaxInlinePayloadSize
+ Largest request or response this deployment will carry inline through Invoke, in bytes. Zero means the deployment accepts no inline payload at all and BeginTransfer is the only way in.
+
+A client reads this BEFORE calling rather than discovering the bound from a rejection, and a Server SHALL NOT publish a value larger than its own MaxByteStringLength, the negotiated MaxMessageSize or the Session's MaxResponseMessageSize permit - the smallest of those is the real limit and a client cannot see all of them.
+
+ i=78
+ i=68
+ ns=2;i=1004
+
+
+
+ BeginTransfer
+ Opens a chunked exchange for a payload that will not fit inline, returning the InferenceTransferType instance to write into. This is the general path: Invoke is the shortcut that happens to work when everything is small.
+
+ i=80
+ ns=2;i=1004
+ ns=2;i=6148
+ ns=2;i=6149
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=2;i=6147
+
+ i=297ContentTypei=12-1Media type of the request body.i=297RequestSizei=9-1Expected request size in bytes, or 0 when not known in advance. A Server that cannot accommodate the stated size refuses here rather than after the client has uploaded it.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6147
+
+ i=297Transferi=17-1InferenceTransferType instance to write the request into.i=297Acceptedi=1-1False when the Server declined to open the exchange.
+
+
+ AiModelManagement
+ Entry point for the AI models this Server runs. A client browses Server/AiModelManagement/Models to find what this Server describes.
+
+ ns=2;i=1001
+ i=2253
+
+
+
+ InferenceTransferType
+ One chunked inference exchange. It exists because Invoke carries its payload as a ByteString, and a ByteString is bounded by MaxByteStringLength, the negotiated MaxMessageSize and the Session's MaxResponseMessageSize - none of which the model gets to choose. An image, a point cloud or a window of high-rate samples exceeds those routinely, and a call that cannot carry the input is not a call.
+
+Request and Response are Part 5 FileType objects: the client opens the request, writes it in chunks it selects, and closes it; after Execute the response is read the same way. Nothing here invents a transfer protocol, because OPC UA already has one and every client already implements it.
+ AiModelManagement
+
+ i=58
+ ns=2;i=6150
+ ns=2;i=6151
+ ns=2;i=6152
+ ns=2;i=6153
+ ns=2;i=6154
+ ns=2;i=6155
+ ns=2;i=6156
+ ns=2;i=6157
+ ns=2;i=6158
+ ns=2;i=6159
+ ns=2;i=6160
+ ns=2;i=6161
+ ns=2;i=6162
+ ns=2;i=6164
+
+
+
+ TransferId
+ Identifier of this exchange.
+
+ i=78
+ i=68
+ ns=2;i=1017
+
+
+
+ State
+ Stage the exchange has reached.
+
+ i=78
+ i=68
+ ns=2;i=1017
+
+
+
+ Request
+ The request body, written by the client in chunks of its own choosing. Inference does not begin until Execute is called, so a partially written request is never acted on.
+
+ i=78
+ i=11575
+ ns=2;i=1017
+
+
+
+ Response
+ The response body, readable once State is Completed. Empty before that.
+
+ i=78
+ i=11575
+ ns=2;i=1017
+
+
+
+ ContentType
+ Media type of the request body.
+
+ i=78
+ i=68
+ ns=2;i=1017
+
+
+
+ ResponseContentType
+ Media type of the response body.
+
+ i=80
+ i=68
+ ns=2;i=1017
+
+
+
+ ModelUsed
+ The model that ACTUALLY produced the response, on the same terms as Invoke: a fallback or a followed reference can change it between the call and the read.
+
+ i=80
+ i=68
+ ns=2;i=1017
+
+
+
+ Usage
+ What the call consumed.
+
+ i=80
+ i=68
+ ns=2;i=1017
+
+
+
+ FinishReason
+ Why output stopped.
+
+ i=80
+ i=68
+ ns=2;i=1017
+
+
+
+ SafetyAssessment
+ Findings from the safety policy, if any applied.
+
+ i=80
+ i=68
+ ns=2;i=1017
+
+
+
+ LastError
+ Diagnostic for the Failed state. For a human; SHALL NOT be parsed.
+
+ i=80
+ i=68
+ ns=2;i=1017
+
+
+
+ ExpiresAt
+ When the Server may reclaim this transfer if it has not completed. A client that abandons an exchange would otherwise hold Server resources until the Session ends, and a Server that never reclaimed them would be one denial of service away from unusable.
+
+ i=80
+ i=68
+ ns=2;i=1017
+
+
+
+ Execute
+ Runs inference over the written request. The Method returns as soon as the request is accepted; State and the envelope members carry the outcome, which is what lets one exchange span a payload too large to have been a single call in the first place.
+
+ i=78
+ ns=2;i=1017
+ ns=2;i=6163
+
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=2;i=6162
+
+ i=297Acceptedi=1-1False when the request was incomplete or already executed.
+
+
+ Abort
+ Abandons the exchange and releases what it holds. A client that has stopped caring about a response SHOULD say so rather than leaving the Server to wait out ExpiresAt.
+
+ i=80
+ ns=2;i=1017
+
+
+
+ DigestProvenance
+ Where Digest came from, or why there is none. NotAvailable is the only value permitted with an empty Digest, and it SHALL be used rather than leaving a client to guess whether the source publishes no digest or this Server declined to carry one.
+
+A Server SHALL NOT put a non-content identifier in Digest to avoid saying NotAvailable. A response fingerprint, a resource name, a storage entity tag and a repository commit identifier are none of them digests of the artefact that ran, and a client that verified against one would believe it had checked something it had not. Where such an identifier is worth publishing it belongs in ArtifactUri or ProvenanceUri, which promise nothing about content.
+
+ i=78
+ i=68
+ ns=2;i=1002
+
+
+
+ DigestProvenance
+ Where this resource's Digest came from, on the same terms as ModelType. A catalogue that declares a digest it did not compute is DeclaredBySource; one serving the artefact through the inherited Open, Read and Close can reach ComputedByServer.
+
+ i=80
+ i=68
+ ns=2;i=1012
+
+
+
+ Registry
+ ModelRegistryType instance the model is imported from, where the import reads a catalogue rather than calling an endpoint. Null otherwise.
+
+A Server SHALL populate exactly one of Source and Registry, and SHALL leave the other null. The two name the two things an import can read from, and a job that named both would not say which one produced the artefact whose digest clause 10.4 verifies.
+
+ i=80
+ i=68
+ ns=2;i=1007
+
+
+
+ PublishedAt
+ When the source first published this model, where the source states it. The same question DatasetType.CreatedAt answers for a dataset, and the same reason: a model trained before a process change may no longer represent the line it runs on, and Version is a vendor string that often cannot be ordered.
+
+This is the source's date, not when this Server learned of it - a Server SHALL NOT substitute its own acquisition time, which would make every model appear to date from the last restart.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ LastModifiedAt
+ When the artefact behind this model last changed at the source.
+
+It exists for the FollowsRef case of clause 9.3, where the artefact can change with nothing else changing. Clause 12.3.1 requires repointing to be treated as an authorization-bearing act and points at AiJobType.RequestedBy for the record - but a reference that moves AT THE SOURCE produces no job, so without this member the audit trail that clause demands cannot be constructed on the one path it exists to cover. A Server that follows a mutable reference SHALL populate it.
+
+ i=80
+ i=68
+ ns=2;i=1002
+
+
+
+ DeprecatedFrom
+ When the source stops treating this model as current while continuing to serve it. The date that starts a requalification, not the one that ends production.
+
+ i=80
+ i=68
+ ns=2;i=1015
+
+
+
+ SupportedUntil
+ When the source stops serving this model altogether.
+
+Its consequence is not degradation. On this date the deployment stops, Reachability goes Unreachable, and FallbackPolicy decides what happens next - which, where it is FallBackTo, means the line keeps producing and something outside the qualified configuration is answering. A date that was knowable a year in advance therefore becomes an unplanned change of model, and it is published by the serving system in machine-readable form.
+
+ i=80
+ i=68
+ ns=2;i=1015
+
+
+
+ ApiDialect
+ The contract a client's Payload must satisfy when calling Invoke on this deployment. RestChatCompletions means the Payload is a chat-completions request body; OpenInferenceProtocol means it is an OIP inference body; EmbeddedRuntime and TensorRemoteProcedure name the tensor contracts described by Inputs and Outputs; Proprietary means the contract is named only by EndpointDescriptionUri.
+
+This does not type the payload - clause 8.2 keeps it opaque and that is unchanged. It names WHICH contract the opaque bytes are expected to satisfy, which is what a client browsing an unfamiliar deployment needs before it can send anything at all.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ EndpointDescriptionUri
+ Where the request and response contract for this deployment is documented. Untrusted input, subject to clause 12.2. Required in practice wherever ApiDialect is Proprietary, because nothing else then says what to send.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ RuntimeIdentity
+ Opaque identifier of the serving configuration currently behind this deployment - a serving-stack fingerprint, an engine profile, a container image digest. Compared for equality and never parsed, on the same terms as Digest.
+
+It is not the model. The same artefact served by two runtime builds can produce different numbers, and where the execution site publishes such an identity it is the only thing that records the difference. A change to it under a Pinned binding IS the observable change to the deployment that clause 9.3 says a pinned artefact cannot move without.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ ObservedLatency
+ Most recent inference latency this Server measured for this deployment.
+
+LatencyBudget states what the deployment is expected to meet, and clause 6.4.3 makes Degraded the state of a deployment that is answering but missing it. Without a measurement the comparison has no published input, so the state transition could not be checked against a Server that claimed it. A Server that reports Degraded on latency grounds SHALL populate this.
+
+ i=80
+ i=68
+ ns=2;i=1004
+
+
+
+ RequestUri
+ Where the request body was read from, where it was supplied by reference rather than carried. Untrusted input under clause 12.2, and an egress path under clause 9.5.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ ResponseUri
+ Where the result was written, where the execution site returns a location rather than bytes. Empty when the response is carried inline or through Transfer.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ TransferRequired
+ True when the job produced a response too large to carry inline. ResponsePayload is then empty and the work is NOT lost - Transfer names where to read it.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
+ Transfer
+ InferenceTransferType instance holding the response, where TransferRequired is true. Null otherwise.
+
+Invoke carries the same pair, and the asymmetry would otherwise leave the jobs most likely to produce a large result - a batch scored overnight, an analysis over recorded data - bounded by exactly the three limits clause 8.2.4 says this model does not get to choose.
+
+ i=80
+ i=68
+ ns=2;i=1008
+
+
+
diff --git a/metaverse-specs/extras/ai-model-management/examples/aws-bedrock.md b/metaverse-specs/extras/ai-model-management/examples/aws-bedrock.md
new file mode 100644
index 0000000..6b67114
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/aws-bedrock.md
@@ -0,0 +1,241 @@
+# Amazon Bedrock
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+Amazon Bedrock is a managed AWS service for hosted foundation models from several
+publishers. The two inference surfaces that matter here are the Bedrock Runtime Converse
+API, which gives one request and response shape across model families, and InvokeModel,
+which sends a raw body whose schema depends on the selected model family.
+
+That distinction is the practical one for this specification. Converse is the surface to
+prefer for a Server implementing §8.2 because one payload shape across families lets `Invoke`
+stay opaque under §8.2 without the Server learning whether the target is Anthropic, Amazon,
+Meta or another family.
+
+## The `ModelSourceType`
+
+| Member | Amazon Bedrock |
+|---|---|
+| `SourceId` | your name for it, stable across restarts |
+| `EndpointUri` | the regional Bedrock Runtime endpoint, for example `https://bedrock-runtime.{region}.amazonaws.com/` |
+| `ApiDialect` | `Proprietary` |
+| `EndpointDescriptionUri` | the documentation for the exact Bedrock operation and payload shape you use |
+| `AuthenticationKind` | `WorkloadIdentity`, or `ApiKey` for static access keys |
+| `CredentialReference` | names the IAM role binding or key record — never the value |
+| `TokenAudience` | empty |
+| `Reachability` | maintained from `TestConnection` and from call outcomes |
+
+`ApiDialect` is `Proprietary`. Bedrock Converse is not OpenAI-shaped, not KServe-shaped and
+not the OPC UA inference Method. InvokeModel is even more vendor-specific: the body is raw
+JSON whose schema depends on the model family. §9.2 says a Server using `Proprietary`
+should populate `EndpointDescriptionUri`, and Bedrock is the concrete reason. Without that
+URI, nothing in the address space tells a client whether the Server expects the Converse
+shape or a family-specific InvokeModel body.
+
+AWS authenticates Bedrock calls with Signature Version 4. SigV4 is not one of the
+five `AuthenticationKindEnum` literals because §9.2 classifies the credential the Server
+stores, not the handshake it performs. SigV4 driven by an IAM role attached to the pod,
+task or instance is `WorkloadIdentity`, because no secret is stored anywhere. SigV4 driven
+by static access keys is `ApiKey`, because a secret is stored and must be rotated. If a
+reader needs the handshake recorded exactly, set `EndpointDescriptionUri` to documentation
+for the actual AWS SigV4 arrangement in use.
+
+## Identity
+
+`ListFoundationModels` returns `modelId`, `modelArn`, `modelName`, `providerName` and
+`modelLifecycle`, among other capability fields. `providerName` is the model producer that
+§6.2 says belongs in `Publisher` — Anthropic, Meta, Amazon and similar names — rather than
+merely the host written into an `owned_by` field.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | `providerName` | the model publisher, not merely AWS as host |
+| `Name` | `modelName` | display name from the Bedrock catalogue |
+| `Version` | parsed from `modelId` or `modelArn`, where the provider encodes one | a version identifier, not a digest |
+| `ModelId` | `modelId` | keep it verbatim; it is what you send back |
+| `PublishedAt` | `modelLifecycle.startOfLifeTime` | source publication time under §6.2.3 |
+| `DeprecatedFrom` | `modelLifecycle.legacyTime` | the date the source stops treating the model as current |
+| `SupportedUntil` | `modelLifecycle.endOfLifeTime` | the date the source stops serving the model |
+| `Framework`, `Format` | not exposed | leave empty |
+| `Digest`, `DigestAlgorithm` | **not exposed** | `modelArn` is not a weight hash |
+| `DigestProvenance` | `NotAvailable` | no artefact digest is exposed; `modelArn` is not one |
+
+`modelArn` is useful because it carries the AWS resource identity and includes a versioned
+model identifier. It still does not identify the bytes by content under §12.1.1, so it
+cannot populate `Digest`.
+
+The lifecycle dates are the strongest Bedrock-specific identity mapping. `startOfLifeTime`
+is the source's publication time for `PublishedAt`; `legacyTime` and `endOfLifeTime` belong
+on the model card as `DeprecatedFrom` and `SupportedUntil`. §11.1 is the reason to carry
+the retirement date: at `SupportedUntil` the deployment stops being served, and fallback
+can route production to a model outside the qualified configuration.
+
+## `Invoke`
+
+For Converse, the request body goes through as the caller supplied it. The Server is not
+required to understand the model family, which is exactly why Converse is preferable here:
+one body shape lets §8.2 remain true across families.
+
+| Deployment member | From |
+|---|---|
+| `ApiDialect` | `Proprietary` |
+| `EndpointDescriptionUri` | the documentation for Converse, or the family-specific InvokeModel contract |
+| `ObservedLatency` | `metrics.latencyMs` from Converse |
+
+The deployment's `ApiDialect` is the same as the source's when the Server passes the
+Bedrock payload through. §6.4.2 says to publish that value twice because the source value
+describes this Server's outward call and the deployment value tells an OPC UA client what
+shape its `Payload` must have. `EndpointDescriptionUri` is especially important for
+InvokeModel, where `Proprietary` covers a family-specific body. `metrics.latencyMs` is the
+measurement for `ObservedLatency`; under §6.4.3 a Server reporting `Degraded` on latency
+grounds must publish the measurement that makes the state checkable.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the Converse response body, verbatim |
+| `ResponseContentType` | `application/json` |
+| `ModelUsed` | the `ModelType` NodeId this deployment resolved to — not the Bedrock `modelId` string |
+| `Usage.UnitKind` | `tokens` |
+| `Usage.InputUnits` | `usage.inputTokens` |
+| `Usage.OutputUnits` | `usage.outputTokens` |
+| `Usage.TotalUnits` | `usage.totalTokens` |
+| `FinishReason` | `stopReason`, mapped below |
+| `SafetyAssessment` | populated when guardrails or filtering intervened |
+| `RetryAfter` | the `Retry-After` header, where the response carries one |
+
+`FinishReason` maps directly where Converse reports one: `end_turn` and `stop_sequence` to
+`Stop`, `max_tokens` to `Length`, `tool_use` to `ToolCall`, and `guardrail_intervened` or
+`content_filtered` to `Filtered`. The research directly verified `end_turn`, `max_tokens`
+and `stop_sequence` from the response schema and found the other values in AWS guidance;
+that uncertainty belongs in an implementation note if the Server treats unrecognised values
+as anything other than `Error`.
+
+InvokeModel is the escape hatch for model-specific bodies. It still maps to `Invoke`
+because §8.2 makes the payload opaque, but the Server cannot give a general mapping for
+usage or finish reason unless the selected family's response schema carries them. If you
+use InvokeModel, make `EndpointDescriptionUri` point to the family-specific contract. Where
+the response does not define usage counts, return `Usage` with empty `UnitKind` and zero
+counts; §8.2.3 defines that as "not metered" rather than as a measurement. A Server that
+does report a non-empty `UnitKind` must have obtained the corresponding counts from the
+execution site.
+
+## Asynchronous inference
+
+Bedrock has native batch inference through `CreateModelInvocationJob`. The job takes S3
+input, a `modelId`, an execution `roleArn` and S3 output configuration, returns a `jobArn`,
+and can use either InvokeModel or Converse invocation types. The documented timeout range
+is 24 to 168 hours.
+
+That is a genuine `InvokeAsync` mapping.
+
+| Member | From |
+|---|---|
+| `JobId` | the Bedrock `jobArn` |
+| `RequestUri` | the S3 input location |
+| `ResponseUri` | the S3 output location |
+
+The OPC UA job follows the Part 10 program lifecycle required by §8.6 while the Server
+observes the Bedrock job and exposes the result or failure through the `InferenceJobType`
+instance.
+
+## Large payloads
+
+The real-time Bedrock runtime takes the request body inline; the research notes an inline
+limit of about 4 MB. Bedrock batch inference uses S3 input and output locations, but that
+is a by-reference payload contract, not a chunked upload path for one synchronous call.
+
+| Member | From |
+|---|---|
+| `PayloadUri` | the S3 input location supplied for the batch request |
+| `RequestUri` | the S3 input location actually submitted |
+| `ResponseUri` | the S3 output location returned or configured for the job |
+
+`BeginTransfer` is the Server's own Part 5 `FileType` transfer path as §8.2 defines, for
+a payload too large to carry through the OPC UA call. §8.6.1 separates that case from data
+that already lives in S3. A `PayloadUri`, `RequestUri` or `ResponseUri` is untrusted input
+under §12.2, and it is also an egress decision under §9.5: a deployment whose
+`EgressPermitted` is false **shall not** accept a `PayloadUri` naming somewhere outside the
+operator's boundary.
+
+## The catalogue
+
+`ListFoundationModels` is the right source for `ListModels` on the `ModelSourceType`: it
+answers which foundation models Bedrock exposes in the region and provides `modelId`,
+`modelArn`, `modelName`, `providerName` and `modelLifecycle`.
+
+Within `modelLifecycle`, `status` is not a substitute for observing deployment state, and
+the research does not establish how `publicExtendedAccessTime` changes the card dates. The
+dates mapped above are the ones carried by the information model.
+
+It is not a catalogue in the §10.1 sense. The API lists hosted models, not content-addressed
+artefacts that a Server can fetch, hash and stage. `providerName` gives a useful publisher,
+and `modelArn` gives a stable AWS resource identity, but neither supplies `Digest` on a
+`ModelResourceType`.
+
+A Server can federate Bedrock-hosted models under §9.1. It cannot satisfy
+`AI-Catalogue` or `AI-Import` from `ListFoundationModels` alone, because there is no model
+artefact to stage and verify under §10.3 and §10.4.
+
+## Residency, egress and retention
+
+The region is visible in the Bedrock endpoint and the control-plane region used for
+listing. The retention and training-use position is a contractual AWS statement, not a
+field in an inference response. Treat the table as operator assertions.
+
+| Member | Amazon Bedrock |
+|---|---|
+| `InferenceLocation` | `Cloud` |
+| `EgressPermitted` | `true` |
+| `DataJurisdiction` | the AWS region used by the endpoint |
+| `RetainsInput` | asserted from the Bedrock data-use contract |
+| `EgressPolicyUri` | your policy document |
+
+`EgressPermitted` is `true` because the request leaves the Server for a cloud service. §9.5
+makes the point: SigV4 and TLS answer who may call and who can read the traffic in flight,
+not whether the payload left the site.
+
+## What this system does not tell you
+
+- **The exact handshake.** SigV4 is not in `AuthenticationKindEnum` because §9.2
+ classifies what is stored. Use `WorkloadIdentity` for role-based signing and `ApiKey`
+ for static access keys, and point `EndpointDescriptionUri` at the SigV4 handshake you
+ actually use.
+- **Which weights answered.** No digest is returned by Converse, InvokeModel or
+ `ListFoundationModels`. `DigestProvenance` is `NotAvailable` under §12.1.1.
+ `modelArn` carries a version identifier, not a weight hash.
+- **A universal request body.** Converse gives one Bedrock body shape; InvokeModel gives a
+ raw body whose schema depends on the provider family.
+- **What the model was trained on.** Nothing maps to `TrainedOn` or `DatasetType`. If
+ lineage matters, it comes from supplier documentation or a model card outside the
+ inference response.
+- **Where your data went, or whether it was kept.** Region and retention are recorded as
+ operator assertions in `DataJurisdiction`, `EgressPermitted` and `RetainsInput`.
+
+## Conformance units
+
+This arrangement is an **AI Inference Gateway Server**: it reaches the
+**AI-Base**, **AI-Invoke**, **AI-OffServer**, **AI-Federation** and
+**AI-Residency** facets that §13.3 bundles for a hosted inference Server.
+
+Reachable against Amazon Bedrock: **AI-Base**, **AI-Invoke**, **AI-InvokeAsync**,
+**AI-Transfer**, **AI-OffServer**, **AI-Federation**, **AI-Residency**.
+
+Converse is metered through `usage.inputTokens`, `usage.outputTokens` and
+`usage.totalTokens`. InvokeModel responses that do not carry counts satisfy
+**AI-Invoke** by returning `Usage` with empty `UnitKind` and zero counts; §13.2
+accommodates that.
+
+Out of reach without something else: **AI-Catalogue** and **AI-Import** need a
+content-addressed registry; **AI-Signatures** needs tensor shapes this contract does not
+carry; **AI-Learning** needs training, which is not what this is.
+
+## Sources
+
+- [Amazon Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html)
+- [Amazon Bedrock InvokeModel API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModel.html)
+- [Amazon Bedrock ListFoundationModels API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_ListFoundationModels.html)
+- [Amazon Bedrock CreateModelInvocationJob API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_CreateModelInvocationJob.html)
+- [Amazon SageMaker InvokeEndpoint API](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_runtime_InvokeEndpoint.html)
diff --git a/metaverse-specs/extras/ai-model-management/examples/aws-sagemaker.md b/metaverse-specs/extras/ai-model-management/examples/aws-sagemaker.md
new file mode 100644
index 0000000..dd1e5c4
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/aws-sagemaker.md
@@ -0,0 +1,240 @@
+# Amazon SageMaker real-time endpoints
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+Amazon SageMaker real-time endpoints run the container you deploy and route invocation
+bytes to it. That makes SageMaker different from hosted-model systems: the host defines the
+endpoint management plane, but the request and response contract at inference time is often
+your container's contract.
+
+That is the useful thing to model honestly. A container can be wholly proprietary, or it
+can serve an OpenAI-compatible surface through TGI, vLLM or a similar runtime. `ApiDialect`
+describes the contract the endpoint speaks, not who hosts it.
+
+## The `ModelSourceType`
+
+| Member | Amazon SageMaker endpoint |
+|---|---|
+| `SourceId` | your name for it, stable across restarts |
+| `EndpointUri` | the SageMaker Runtime endpoint for `EndpointName` |
+| `ApiDialect` | `Proprietary`, or `RestChatCompletions` for an OpenAI-compatible container |
+| `EndpointDescriptionUri` | the container contract documentation |
+| `AuthenticationKind` | `WorkloadIdentity`, or `ApiKey` for static access keys |
+| `CredentialReference` | names the IAM role binding or key record — never the value |
+| `TokenAudience` | empty |
+| `Reachability` | maintained from `TestConnection` and from call outcomes |
+
+`ApiDialect` is `Proprietary` for the default SageMaker case, and for a different reason
+from Bedrock. Bedrock's proprietary surface is vendor-defined; SageMaker's request and
+response bodies are container-defined. You built the container, so you define the contract.
+`EndpointDescriptionUri` matters even more here than it does for Bedrock because it is the
+only thing that can tell a client what the endpoint expects.
+
+A common SageMaker deployment is a container serving an OpenAI-compatible surface, such as
+TGI or vLLM. Where that is what you deployed, `RestChatCompletions` is the honest dialect
+rather than `Proprietary`. The dialect describes the contract this Server speaks to the
+endpoint and not who is hosting it.
+
+SageMaker uses AWS Signature Version 4. Apply §9.2 the same way as in the
+[Bedrock guide](aws-bedrock.md): SigV4 signing through an attached IAM role is
+`WorkloadIdentity`, SigV4 signing through stored access keys is `ApiKey`, and
+`EndpointDescriptionUri` records the actual signing arrangement when a client needs that
+handshake detail.
+
+## Identity
+
+The inference plane has no standard model-listing endpoint. The control plane can list and
+describe endpoints and models, including the model artefact S3 URI and container image, but
+that is not the same thing as a `/v1/models` response.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | operator assertion, or the container/model owner from control-plane metadata | not returned by invocation |
+| `Name` | endpoint name, model name or container-declared model id | choose the value the endpoint expects clients to use |
+| `Version` | model package version, image tag or container-declared version where one exists | no standard inference-plane field |
+| `ModelId` | endpoint name plus target model or variant where used | keep the routing identity needed to call it |
+| `Framework`, `Format` | control-plane or operator metadata | not returned by invocation |
+| `ArtifactUri` | S3 model artefact URI such as `s3://bucket/model.tar.gz` | available from the model configuration |
+| `Digest`, `DigestAlgorithm` | **not exposed by the inference plane** | see the S3 ETag warning below |
+| `DigestProvenance` | `NotAvailable` from the inference plane | `Stage` hashing can reach `ComputedByServer`; an S3 ETag is not an independent digest |
+
+SageMaker often gives the operator the artefact URI and control of the object behind it.
+`ArtifactUri` can therefore be filled from the S3 model artefact location.
+
+That still does not fill `Digest`. An S3 ETag is a de facto hash in common cases, but it is
+not surfaced in any inference-plane response and it is not an artefact digest under
+§12.1.1. Publish `ArtifactUri` when you have it; use `ComputedByServer` only if the Server
+hashes the artefact during import, and `VerifiedOnStage` only if an independent declared
+digest matched under §10.4.
+
+## `Invoke`
+
+`InvokeEndpoint` sends raw bytes to the container. `Content-Type` and `Accept` declare the
+media types, and SageMaker passes headers such as custom attributes, target model, target
+variant, inference component and session id to the container.
+
+| Deployment member | From |
+|---|---|
+| `ApiDialect` | `Proprietary`, or `RestChatCompletions` for an OpenAI-compatible container |
+| `EndpointDescriptionUri` | the container contract documentation |
+
+The deployment's `ApiDialect` is usually the same as the source's because the Server passes
+the payload through to the container. §6.4.2 makes that value the contract an OPC UA client
+must satisfy when it fills `Payload`. `EndpointDescriptionUri` is the place to publish the
+container contract when the dialect is `Proprietary`.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the container response body, verbatim |
+| `ResponseContentType` | the response media type the container returns |
+| `ModelUsed` | the `ModelType` NodeId this deployment resolved to — not merely the endpoint name |
+| `Usage.UnitKind` | container-defined; empty when the container response is not metered, per §8.2.3 |
+| `Usage.InputUnits` | container-defined; `0` when `UnitKind` is empty |
+| `Usage.OutputUnits` | container-defined; `0` when `UnitKind` is empty |
+| `Usage.TotalUnits` | container-defined; `0` when `UnitKind` is empty |
+| `FinishReason` | container-defined |
+| `SafetyAssessment` | container-defined |
+| `RetryAfter` | the `Retry-After` header, where the response carries one |
+
+For a plain container-defined endpoint, the Server cannot infer token counts or finish
+reasons from the SageMaker envelope. CloudWatch can record latency and throughput, but that
+is operational telemetry outside the response body. When the container response does not
+meter the call, `Usage` is returned with empty `UnitKind` and zero counts; §8.2.3 defines
+that as "not metered" rather than as a measurement. If the container emits
+OpenAI-compatible chat-completions JSON, use the `RestChatCompletions` mapping from the
+OpenAI-compatible guides: `usage.prompt_tokens`, `usage.completion_tokens`,
+`usage.total_tokens` and `choices[0].finish_reason` carry the values.
+
+`ModelUsed` still matters when SageMaker routes by target model, target variant or inference
+component. A caller needs the `ModelType` NodeId that actually answered, especially where a
+multi-model endpoint or variant route makes the endpoint name insufficient.
+
+## Asynchronous inference
+
+SageMaker has native Asynchronous Inference endpoints. `InvokeEndpointAsync` submits work to
+`/endpoints/{EndpointName}/async-invocations`; the input can be in the request body or named
+by `X-Amzn-SageMaker-InputLocation` as an S3 URI. The service returns 202 with an
+`OutputLocation` S3 URI and an `InferenceId`; completion can be observed from S3 or through
+SNS notification.
+
+That is a genuine `InvokeAsync` mapping. `InferenceId` goes in `JobId`, `OutputLocation`
+becomes `ResponseUri`, and the OPC UA job follows the Part 10 program lifecycle required
+by §8.6 while the Server observes the asynchronous invocation and exposes the container
+response through the `InferenceJobType` instance.
+
+| Member | From |
+|---|---|
+| `JobId` | `InferenceId` |
+| `RequestUri` | `X-Amzn-SageMaker-InputLocation`, where the request names S3 input |
+| `ResponseUri` | `OutputLocation` |
+
+Batch Transform is also a SageMaker batch mechanism, but it is a separate job API for
+offline inference over S3 datasets. Map it only if the Server intentionally exposes that
+control-plane job as `InvokeAsync`; it is not the real-time endpoint's asynchronous path.
+
+## Large payloads
+
+For asynchronous endpoints, `X-Amzn-SageMaker-InputLocation` can point at an S3 object
+containing the payload. That is a native way to avoid the inline request limit for the async
+path.
+
+That is a by-reference payload, not an OPC UA chunked transfer.
+
+| Member | From |
+|---|---|
+| `PayloadUri` | the S3 URI in `X-Amzn-SageMaker-InputLocation` |
+| `RequestUri` | the S3 input location actually submitted |
+| `ResponseUri` | the `OutputLocation` returned by `InvokeEndpointAsync` |
+
+`BeginTransfer` and `InferenceTransferType` are the Server's own Part 5 `FileType` path as
+§8.2 defines, for a payload too large to carry through the OPC UA call. §8.6.1 separates
+that case from data that already lives in S3. A `PayloadUri`, `RequestUri` or `ResponseUri`
+is untrusted input under §12.2, and it is also an egress decision under §9.5: a deployment
+whose `EgressPermitted` is false **shall not** accept a `PayloadUri` naming somewhere
+outside the operator's boundary. The Server may still write an assembled transfer to S3 and
+call `InvokeEndpointAsync` where that egress is permitted, or issue one ordinary real-time
+`InvokeEndpoint` request when the payload fits.
+
+## The catalogue
+
+SageMaker has useful control-plane inventory: endpoints, models, model artefact S3 URIs and
+container images. It has no standard inference-plane catalogue and no `/v1/models` shape.
+`ListModels` on the `ModelSourceType` can therefore be implemented only from the Server's
+own configuration or from SageMaker control-plane calls that the Server is authorized to
+make.
+
+SageMaker is the one system in this set where `Stage` import under §10.3 is often genuinely
+achievable. The operator commonly controls the S3 artefact, can fetch `s3://bucket/model.tar.gz`,
+compute a digest, compare it with a catalogue entry and then publish the resulting
+`ModelType`. That is different from hosted-model systems such as Bedrock, where the model
+bytes are not available to the Server.
+
+The distinction is important. `Federate` is enough when the Server only calls a SageMaker
+endpoint. `Stage` is appropriate when the Server obtains the artefact and can perform the
+§10.4 digest check before deployment.
+
+## Residency, egress and retention
+
+The SageMaker endpoint region is visible from the AWS endpoint and control plane. Whether
+payloads leave the site, whether inputs are retained by the container and what jurisdiction
+applies are still operator assertions.
+
+| Member | Amazon SageMaker endpoint |
+|---|---|
+| `InferenceLocation` | `Cloud`, or `OnServer` for SageMaker Local Mode |
+| `EgressPermitted` | `true` for AWS-hosted endpoints; `false` for local-only deployments |
+| `DataJurisdiction` | the AWS region, or the local site |
+| `RetainsInput` | asserted from the container and surrounding logging configuration |
+| `EgressPolicyUri` | your policy document |
+
+SageMaker Local Mode runs containers locally through the SageMaker Python SDK and Docker,
+with the same style of `/invocations` call against localhost. In that deployment the data
+boundary is the local machine or site rather than the AWS region, and the residency members
+should say that.
+
+## What this system does not tell you
+
+- **The request schema, unless your container does.** SageMaker passes bytes. If the
+ container contract is not documented through `EndpointDescriptionUri`, a client cannot
+ know what to send.
+- **Usage and finish reason in a standard envelope.** Token counts and `FinishReason` are
+ available only if the container chooses to return them. Otherwise `UsageDataType` uses
+ the §8.2.3 not-metered sentinel.
+- **A digest from the inference plane.** `ArtifactUri` can be filled from the S3 model
+ artefact URI, but `Digest` cannot be filled from `InvokeEndpoint` or
+ `InvokeEndpointAsync`. `DigestProvenance` is `NotAvailable` for that surface under
+ §12.1.1.
+- **That an S3 ETag is a model digest.** It may be a useful storage hint, but §12.1.1
+ treats storage entity tags as non-content identifiers. It is not the independent source
+ declaration needed for `VerifiedOnStage`.
+- **Where your data went, or whether it was kept.** Region, egress and retention depend on
+ the endpoint deployment, container logging and operator policy.
+
+## Conformance units
+
+This arrangement is an **AI Inference Gateway Server**: it reaches the
+**AI-Base**, **AI-Invoke**, **AI-OffServer**, **AI-Federation** and
+**AI-Residency** facets that §13.3 bundles for a hosted inference Server.
+
+Reachable against Amazon SageMaker endpoints: **AI-Base**, **AI-Invoke**,
+**AI-InvokeAsync**, **AI-Transfer**, **AI-OffServer**, **AI-Federation**,
+**AI-Residency**.
+
+For a container that does not meter, **AI-Invoke** is satisfied by returning `Usage`
+with empty `UnitKind` and zero counts; §13.2 accommodates that.
+
+Out of reach without something else: **AI-Catalogue** needs a registry projection with real
+resources, and **AI-Import** requires **AI-Catalogue** (§13) — an import job with nothing to
+import from is not implementable, so a Server staging artefacts from S3 URIs alone claims
+neither. Pair SageMaker with a content-addressed registry that declares a digest and both
+become reachable together; [the Hugging Face guide](hugging-face.md) is the worked case.
+**AI-Signatures** needs tensor shapes or schema metadata from the container;
+**AI-Learning** needs training, which is not what invocation is.
+
+## Sources
+
+- [Amazon SageMaker InvokeEndpoint API](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_runtime_InvokeEndpoint.html)
+- [Amazon SageMaker InvokeEndpointAsync API](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_runtime_InvokeEndpointAsync.html)
diff --git a/metaverse-specs/extras/ai-model-management/examples/azure-ai-foundry.md b/metaverse-specs/extras/ai-model-management/examples/azure-ai-foundry.md
new file mode 100644
index 0000000..3f5a5df
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/azure-ai-foundry.md
@@ -0,0 +1,244 @@
+# Azure AI Foundry
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+Azure AI Foundry serves models over an HTTP contract that is OpenAI-compatible in schema
+and Azure-hosted in everything else — authentication, regions, and the model catalogue
+behind it. **Foundry Local** runs the same contract on the machine, reached either over
+loopback or through an in-process SDK.
+
+Both are covered here because the interesting thing about them is that they are the same
+thing in two places, which is exactly the claim §8.1 makes: where inference runs does not
+change how it is called. What it changes is `InferenceLocation`, `EgressPermitted` and
+`DataJurisdiction` — the members that exist precisely to record that difference.
+
+## The `ModelSourceType`
+
+| Member | Cloud | Foundry Local |
+|---|---|---|
+| `SourceId` | your name for it, stable across restarts | as cloud |
+| `EndpointUri` | `https://{resource}.openai.azure.com/openai/v1/` | `http://localhost:{port}/v1/` |
+| `ApiDialect` | `RestChatCompletions` | `RestChatCompletions`, or `EmbeddedRuntime` through the SDK |
+| `EndpointDescriptionUri` | not required; the dialect names the contract | as cloud |
+| `AuthenticationKind` | `WorkloadIdentity`, or `ApiKey` | `Anonymous` |
+| `CredentialReference` | names the key or the token scope — never the value | empty |
+| `TokenAudience` | `https://ai.azure.com/.default` | empty |
+| `Reachability` | maintained from `TestConnection` and from call outcomes | as cloud |
+
+`AuthenticationKind` is `WorkloadIdentity` when the Server holds a Microsoft Entra managed
+identity and obtains tokens through it. That applies §9.2's storage rule: no secret is
+stored anywhere, so there is nothing to leak, rotate or archive. `ApiKey` is the fallback
+where a managed identity is not available.
+
+Foundry Local is `Anonymous` because it listens on loopback and there is nothing to
+authenticate to. That is a statement about the deployment, not a relaxation: an endpoint
+reachable only from the machine it runs on has the machine's own access control in front of
+it.
+
+The two SDK-hosted variants are worth distinguishing. Reached over its loopback HTTP
+server, Foundry Local is `RestChatCompletions` and looks like any other endpoint. Reached
+through the in-process SDK — `Microsoft.AI.Foundry.Local` and its siblings, which are
+library APIs rather than HTTP — it is `EmbeddedRuntime`, and `EndpointUri` is empty because
+there is no endpoint. Say which one you built, because the failure modes differ: one can be
+unreachable, the other can only be absent.
+
+## Identity
+
+The listing endpoint returns `id`, `object`, `created` and `owned_by`. A per-model
+`GET /model-info` adds a name, a type and capabilities. Neither carries a digest or
+structured provenance, and neither decomposes into the triple `ModelType` asks for.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | empty unless independent provenance identifies the producer | `owned_by` reports the serving host or account, not the model producer |
+| `Name` | `id`, with the trailing date removed | on a deployed model this is the *deployment* name, which is yours |
+| `Version` | the date suffix of `id`, where there is one | `gpt-4o-2024-08-06` yields `2024-08-06` |
+| `ModelId` | the whole `id` | keep it verbatim under §6.2; it is what you must send back |
+| `PublishedAt` | `created` | Unix timestamp from the source, not the Server's acquisition time |
+| `Framework`, `Format` | not exposed | leave empty |
+| `Digest`, `DigestAlgorithm` | **not exposed** | see below |
+| `DigestProvenance` | `NotAvailable` | no artefact digest is exposed; the model name is not one |
+
+Two traps here.
+
+The `owned_by` field reports the serving host or account. §6.2 says `Publisher` names the
+organisation that produced the model and is left empty where only the serving organisation
+is known. The full `id` still goes verbatim in `ModelId`, so two Servers can compare the
+source system's own identifier even when the `Publisher`, `Name`, `Version` triple is
+incomplete.
+
+The `id` on a cloud deployment is a **deployment name you chose**, not the model's identity.
+Two Servers in the same plant can call the same underlying model different things, and
+nothing in the API will tell you they are the same. If the provenance question matters to
+you — and §11 exists because it usually does — record the underlying model in `ModelId` and
+your deployment name in the deployment's `DeploymentId`, where it belongs.
+
+The date suffix is a **convention, not a field**. Splitting `gpt-4o-2024-08-06` on the last
+hyphen group works today and is not something the API promises. A model named without one
+leaves `Version` empty, which is honest, rather than being given a fabricated `1.0.0`.
+
+The `created` timestamp is the value for `PublishedAt`. §6.2.3 requires the source's
+publication time rather than the time this Server first saw the model, because the source
+time is what lets opaque identifiers be ordered.
+
+## `Invoke`
+
+The request body goes through as the caller supplied it. §8.2 makes the payload opaque to
+the Server, and the reason shows here: the `extra-parameters: pass-through` header exists so
+that model-specific fields can reach the model without the API version moving, and a Server
+that parsed and re-serialised the body would defeat it.
+
+| Deployment member | Cloud | Foundry Local |
+|---|---|---|
+| `ApiDialect` | `RestChatCompletions` | `RestChatCompletions`, or `EmbeddedRuntime` through the SDK |
+
+The deployment's `ApiDialect` is usually the same as the source's because the payload
+passes through unchanged. §6.4.2 says that is the honest answer given twice: the source
+value tells this Server what it speaks outward, and the deployment value tells a client
+what to put in `Payload`.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the response body, verbatim |
+| `ResponseContentType` | `application/json` |
+| `ModelUsed` | the `ModelType` NodeId this deployment resolved to — not the response's `model` string |
+| `Usage.UnitKind` | `tokens` |
+| `Usage.InputUnits` | `usage.prompt_tokens` |
+| `Usage.OutputUnits` | `usage.completion_tokens` |
+| `Usage.TotalUnits` | `usage.total_tokens` |
+| `FinishReason` | `choices[0].finish_reason`, mapped below |
+| `SafetyAssessment` | populated when the content filter fired |
+| `RetryAfter` | the `Retry-After` header, where the response carries one |
+
+`FinishReason` maps: `stop` to `Stop`, `length` to `Length`, `tool_calls` to `ToolCall`,
+`content_filter` to `Filtered`. There is no `Cancelled` on this contract — a cancelled call
+is cancelled by the Server, not by the endpoint — and `Error` covers a response that arrived
+but could not be understood.
+
+`ModelUsed` is a NodeId in this Server's address space, not the `model` string the endpoint
+echoed back. A caller can already see the string in the payload. What it cannot otherwise
+find out is *which of the models this Server publishes* answered, and that is the question
+`ModelUsed` exists to settle — see §8.2.1, and the fallback case in §9.4 where the two differ.
+
+## Asynchronous inference
+
+Azure OpenAI has a batch API — `POST /batches`, a job identifier, a 24-hour completion
+window — and it is reached on a different plane from the v1 inference surface. Treat it as
+unavailable from the inference endpoint unless you have checked for your resource.
+
+So `InvokeAsync` is generally the Server's own job: it accepts the call, returns an
+`InferenceJobType` NodeId, and runs the synchronous request itself. §8.6 permits exactly
+this, and the value is real even when nothing native backs it — the result survives the
+client that asked for it disconnecting, which a synchronous call cannot offer.
+
+Where you do wire it to the batch API, the mapping is direct: the batch identifier goes in
+`JobId` and the job's lifecycle follows the Part 10 program state machine as §8.6 requires.
+
+## Large payloads
+
+`POST /files` yields a `file_id` referenced from a later request, on the Azure OpenAI plane.
+It is a by-reference payload, not a chunked transfer.
+
+| Member | From |
+|---|---|
+| `PayloadUri` | the `file_id` named by the later request |
+| `RequestUri` | the file id actually submitted by the Server |
+
+A later Azure OpenAI request that names the uploaded file supplies that id through
+`PayloadUri`, and the job records what was submitted in `InferenceJobType.RequestUri`
+where asynchronous processing is used.
+
+`BeginTransfer` and `InferenceTransferType` are the Server's own, over Part 5 `FileType` as
+§8.2 defines, for a payload too large to carry through the OPC UA call. §8.6.1 separates
+that case from data that already lives elsewhere. A `PayloadUri` or `RequestUri` is
+untrusted input under §12.2, and it is also an egress decision under §9.5: a cloud
+deployment may accept an Azure file reference only where the operator has permitted that
+egress, and a deployment whose `EgressPermitted` is false **shall not** accept a
+`PayloadUri` naming somewhere outside the operator's boundary.
+
+## The catalogue
+
+The inference-plane `GET /v1/models` lists **what is deployed on this resource**, which is
+the right source for `ListModels` on the `ModelSourceType` — it answers what this source can
+actually serve.
+
+It is not a catalogue in the §10 sense. The Foundry model catalogue lives on the management
+plane, needs an Azure subscription, and has no notion of a content-addressed version. A
+`ModelRegistryType` over it would be a projection of a browsing experience, and one that
+could not populate `Digest` on any `ModelResourceType` in it.
+
+If you want §10 with real digests, the source has to be a registry that is content-addressed
+— see [the Hugging Face guide](hugging-face.md), which is the one in this set that is.
+
+## Residency, egress and retention
+
+Nothing in the API states any of these. All three are operator assertions.
+
+| Member | Cloud | Foundry Local |
+|---|---|---|
+| `InferenceLocation` | `Cloud` | `OnServer` |
+| `EgressPermitted` | `true` | `false` |
+| `DataJurisdiction` | the region the resource is in | the site |
+| `RetainsInput` | `false` under the standard contract | `false` |
+| `EgressPolicyUri` | your policy document | — |
+
+`DataJurisdiction` comes from the region you created the resource in, and someone has to
+write it down: the API will not tell you, and a resource created in the wrong region answers
+exactly as convincingly as one created in the right one.
+
+`EgressPermitted` is `true` for the cloud service and no encryption changes that. §9.5 makes
+the point and it is worth repeating because it is the mistake people make: TLS answers who
+can read the payload in flight, not whether it left the site. A client refusing to send
+process data off-premises needs the second answer, and needs it before it calls.
+
+`RetainsInput` asks whether the far end **keeps** the input after serving it, which is a
+wider question than whether it trains on it. Azure's contract states that customer data is
+not used to train foundation models; that is a contractual statement rather than a field in
+a response, and it does not by itself answer logging, abuse monitoring or evaluation.
+
+So publish `false` only where the operator has established that none of those retain input
+for this resource, and `true` where any of them does. It is the operator asserting it
+either way — there is no response field to read — and the member is worth nothing if it is
+filled in from the first sentence of a marketing page rather than from the configuration.
+
+## What this system does not tell you
+
+- **Which weights answered.** No digest, anywhere, on any call. `Digest` and
+ `DigestAlgorithm` stay empty, and `DigestProvenance` is `NotAvailable` under
+ §12.1.1. Do not hash the model name to fill them.
+- **What the model was trained on.** Nothing maps to `TrainedOn` or `DatasetType`. If
+ lineage matters, it comes from the model card or the supplier, by hand.
+- **Whether the model behind a name changed.** A `Pinned` deployment here is pinned to a
+ string. The provider's versioning policy is what holds it still; the Server cannot verify
+ it and should not imply otherwise.
+- **Where your data went, or whether it was kept.** Both are contract terms, recorded as
+ operator assertions in `DataJurisdiction`, `EgressPermitted` and `RetainsInput`.
+- **Its own health, before you call it.** There is no dedicated health endpoint on the
+ inference plane; `TestConnection` is a listing call whose success is the signal. That is
+ enough to distinguish a wrong credential from a wrong URL, which is what commissioning
+ needs it for.
+
+## Conformance units
+
+The cloud arrangement is an **AI Inference Gateway Server**: it reaches the
+**AI-Base**, **AI-Invoke**, **AI-OffServer**, **AI-Federation** and
+**AI-Residency** facets that §13.3 bundles for a hosted inference Server.
+Foundry Local is the same call shape on the same host, so that arrangement is an
+**AI Inference Device Server** instead.
+
+Reachable against Azure AI Foundry: **AI-Base**, **AI-Invoke**, **AI-InvokeAsync**,
+**AI-Transfer**, **AI-OffServer**, **AI-Federation**, **AI-Residency**.
+
+Out of reach without something else: **AI-Catalogue** and **AI-Import** need a
+content-addressed registry; **AI-Signatures** needs tensor shapes this contract does not
+carry; **AI-Learning** needs training, which is not what this is.
+
+## Sources
+
+- [Azure AI Foundry Model Inference REST API](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/modelinference/)
+- [API version lifecycle](https://learn.microsoft.com/en-us/azure/foundry/openai/api-version-lifecycle)
+- [Foundry Local — get started](https://learn.microsoft.com/en-us/azure/foundry-local/get-started)
+- [Azure OpenAI batch](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/batch)
diff --git a/metaverse-specs/extras/ai-model-management/examples/embedded-runtimes.md b/metaverse-specs/extras/ai-model-management/examples/embedded-runtimes.md
new file mode 100644
index 0000000..8591500
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/embedded-runtimes.md
@@ -0,0 +1,236 @@
+# Embedded runtimes
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+ONNX Runtime and llama.cpp are local execution stacks. The model artefact is a file on the
+machine, and inference is normally a library call in the Server's own process. llama.cpp also
+ships `llama-server`, an HTTP server with an OpenAI-compatible surface; that case is covered
+separately because the dialect follows the contract the Server speaks, not the software behind
+it.
+
+With a local artefact, the Server can compute a real `Digest` over the `.onnx` or `.gguf`
+file it holds. The §10.4 verification gate applies when staging an import against a digest
+declared by a catalogue. Hosted endpoints in this guide set name a model, but they do not
+expose a digest of the weights that answered.
+
+## The `ModelSourceType`
+
+| Member | ONNX Runtime / libllama | `llama-server` on loopback |
+|---|---|---|
+| `SourceId` | your name for the local runtime | your name for the loopback server |
+| `EndpointUri` | empty; there is no endpoint | `http://127.0.0.1:{port}/v1/` |
+| `ApiDialect` | `EmbeddedRuntime` | `RestChatCompletions` |
+| `EndpointDescriptionUri` | your documentation for the library binding, if useful | not required; the dialect names the contract |
+| `AuthenticationKind` | `Anonymous` | `Anonymous` |
+| `CredentialReference` | empty | empty |
+| `TokenAudience` | empty | empty |
+| `Reachability` | maintained from artefact-load and local-run outcomes | maintained from HTTP probes and call outcomes |
+| `ConsecutiveFailures` | consecutive load, missing-file or local execution failures | consecutive HTTP or execution failures |
+| `Capabilities` | what the wrapper exposes | chat, completions, embeddings and other enabled routes |
+
+`EmbeddedRuntime` means the Server reaches the runtime through a library rather than a
+socket. `EndpointUri` is therefore empty, and a source of this kind is not unreachable in
+the network sense. It is present or absent: the file exists and can be loaded, or it cannot.
+`TestConnection` is still useful, but it probes the configured artefact path and runtime
+initialisation rather than a port. `Reachability` and `ConsecutiveFailures` therefore record
+local readiness, not routing or firewall health.
+
+`llama-server` is different. The same llama.cpp weights reached through its HTTP process are
+`RestChatCompletions`, with `EndpointUri` on loopback and `AuthenticationKind` `Anonymous`.
+The dialect describes the contract this Server speaks to the execution site. It does not
+describe the product, the file format or the vendor. A Server that calls libllama directly
+and a Server that calls `llama-server` can execute the same `.gguf` file and still publish
+different `ApiDialect` values, because they speak different contracts.
+
+The deployment publishes the same caller-facing distinction. An in-process ONNX Runtime or
+libllama deployment whose payload is interpreted by the Server's wrapper is
+`EmbeddedRuntime`. A loopback `llama-server` deployment whose caller supplies the
+OpenAI-compatible request body is `RestChatCompletions`. §6.4.2 makes that a property of
+what the OPC UA caller sends, not of the file on disk.
+
+## Identity
+
+The local file is the identity anchor, not a provider-side deployment name.
+
+| Member | ONNX Runtime | llama.cpp |
+|---|---|---|
+| `Publisher` | operator or configured catalogue owner | operator, model family owner, or configured catalogue owner |
+| `Name` | configured name | configured name or filename-derived model name |
+| `Version` | configured version or catalogue revision | configured revision or file/catalogue revision |
+| `ModelId` | stable local identifier, often the file path plus digest | stable local identifier, often the file path plus digest |
+| `Framework` | ONNX / ONNX Runtime, where the file metadata supports it | llama.cpp |
+| `Format` | ONNX | GGUF |
+| `PublishedAt` | from a catalogue, or empty | from a catalogue, or empty |
+| `LastModifiedAt` | filesystem modification time for this file copy | filesystem modification time for this file copy |
+| `Digest`, `DigestAlgorithm` | hash of the `.onnx` artefact | hash of the `.gguf` artefact |
+| `DigestProvenance` | `ComputedByServer`; `VerifiedOnStage` after catalogue staging | `ComputedByServer`; `VerifiedOnStage` after catalogue staging |
+| `ArtifactUri` | local file URI | local file URI |
+| `RuntimeIdentity` | ONNX Runtime build version or container image digest | llama.cpp build version or container image digest |
+| `ParameterCount`, `Quantization` | fill where the operator has verified these facts for the artefact | fill where the operator has verified these facts for the artefact |
+
+`Digest` and `DigestAlgorithm` can be a genuine artefact hash, and `ArtifactUri` can point
+at the file the Server actually opens. A `Pinned` deployment is pinned to bytes only where
+the operator controls the immutability of the file behind the local path; otherwise the path
+can still name mutable bytes.
+
+An in-process runtime is the case where a real digest costs nothing: the Server already
+holds the local file. That is the opposite of hosted APIs that only name a model. Where the
+file was staged from a catalogue that declared a digest, the §10.4 match raises
+`DigestProvenance` to `VerifiedOnStage`.
+
+The same local evidence can populate `LastModifiedAt`. A filesystem modification time says
+when this copy of the artefact was written, so it is honest as a last-modified timestamp
+for the file the Server opens. It is not a substitute for `PublishedAt`: §6.2.3 forbids
+using the Server's acquisition time as the source publication time, and a file mtime has
+exactly that local-copy character unless a catalogue supplies the source date.
+
+`RuntimeIdentity` is genuinely available in this arrangement. A Server can record the ONNX
+Runtime or llama.cpp build version, or the container image digest when the runtime is
+containerised. §9.3.1 treats that value as opaque and compared only for equality, which is
+enough to show that the serving stack changed while the model bytes stayed the same.
+
+The research for this guide establishes ONNX Runtime in-process loading and inference, not a
+portable metadata or tensor-signature inspection contract. Publish `Inputs` and `Outputs`
+only where the runtime binding and model-format documentation you rely on supports reading
+them; otherwise this guide cannot establish **AI-Signatures** for ONNX models.
+
+The research for this guide does not establish GGUF header contents. Where the operator knows
+the quantization and parameter count of the file it deployed, `Quantization` and
+`ParameterCount` are theirs to populate; do not claim the file format surfaced those facts
+unless the implementation documentation you rely on says so.
+
+## `Invoke`
+
+For ONNX Runtime and libllama the mapping is a wrapper decision. The Server receives the
+opaque §8.2 payload, converts it to the runtime's input objects, runs the local call, and
+returns the runtime output as the response payload. There is no remote envelope to preserve.
+
+For `llama-server`, the request body goes through the OpenAI-compatible route the deployment
+uses, usually `/v1/chat/completions`, `/v1/completions` or `/v1/embeddings`.
+
+| Output | ONNX Runtime / libllama | `llama-server` |
+|---|---|---|
+| response payload | wrapper-defined bytes or JSON | HTTP response body, verbatim |
+| response content type | wrapper-defined | `application/json` |
+| model used | the `ModelType` NodeId for the local file | the `ModelType` NodeId for the served file |
+| `Usage.UnitKind` | empty when the wrapper does not meter; otherwise the wrapper's unit, per §8.2.3 | `tokens` |
+| `Usage.InputUnits` | `0` when `UnitKind` is empty; otherwise the measured input count | prompt token count |
+| `Usage.OutputUnits` | `0` when `UnitKind` is empty; otherwise the measured output count | completion token count |
+| `Usage.TotalUnits` | `0` when `UnitKind` is empty; otherwise the measured total count | total token count |
+
+Usage accounting is precise by case. `llama-server` uses an OpenAI-compatible response and
+returns token counts, so `UsageDataType` can be populated with `tokens`. ONNX Runtime
+in-process has no usage telemetry field in the research, so the Server returns `Usage`
+with empty `UnitKind` and zero counts unless its own wrapper has a metering rule it is
+prepared to document. That is the §8.2.3 not-metered sentinel, not a zero measurement.
+
+Finish reasons are also case-specific. The OpenAI-compatible llama.cpp surface includes a
+finish-reason field; ordinary values map the same way as the other chat-completions guides:
+`stop` to `Stop`, `length` to `Length`, tool calls to `ToolCall`, and filtering to
+`Filtered` where the wrapper can detect it. The research did not verify llama.cpp's exact
+values for unusual cases, so a Server should map unknown values to `Error` rather than
+inventing a literal.
+
+## Asynchronous inference
+
+Neither ONNX Runtime nor llama.cpp gives the Server a native batch job API to map onto.
+`InvokeAsync` is therefore the Server's own job, as §8.6 permits: it accepts the request,
+creates the job node, runs the local inference, and stores the result there.
+
+That is still worth implementing. A local run can be long even when it makes no network
+call, especially for large models, slow CPUs or large inputs. The job outlives the client
+Session that asked for it, which is the property synchronous `Invoke` cannot provide.
+
+## Large payloads
+
+Neither runtime provides a native file-upload or chunked-request facility that maps onto
+`BeginTransfer`. For embedded runtimes that is not a limitation of the execution site: the
+Server already owns the bytes once the OPC UA transfer completes.
+
+`BeginTransfer` is therefore the Server's own Part 5 file exchange described in §8.2.4. The
+client writes the request in chunks, the Server reassembles it, runs the local runtime, and
+exposes the response through the same transfer object if it is too large for the inline
+result. No ONNX Runtime or llama.cpp feature has to be arranged for that to work.
+
+## The catalogue
+
+There is no native ONNX Runtime catalogue API, and the research reports no llama.cpp
+catalogue with version and digest. `llama-server` has `GET /v1/models`, and router mode can
+list loaded or loadable models, but that is an execution listing rather than a §10 catalogue.
+
+The Server can nevertheless satisfy §10 with local artefacts because it holds the files. A
+configured directory, an internal registry or a staging area can be projected as catalogue
+entries whose `ArtifactUri`, `Digest` and `DigestAlgorithm` are computed from the local file.
+For staged imports, §10.4 is the digest-verification gate: where a catalogue declared a
+digest, staging computes the digest over the bytes received and refuses to deploy a mismatch.
+For a file the Server already holds, the computed `Digest` still identifies real bytes, but
+`Pinned` means pinned to bytes only where the operator makes the file immutable.
+
+That buys something concrete. A hosted deployment can tell a client which name answered; a
+local staged deployment can tell it which bytes answered. The cost is equally concrete:
+someone must place, update, remove and audit model files on every machine that may run them.
+
+## Residency, egress and retention
+
+| Member | ONNX Runtime / libllama | `llama-server` on loopback |
+|---|---|---|
+| `InferenceLocation` | `OnServer` | `OnServer` |
+| `EgressPermitted` | `false` | `false` |
+| `DataJurisdiction` | the site | the site |
+| `RetainsInput` | `false`, unless the wrapper logs it | `false`, unless the server or wrapper logs it |
+| `EgressPolicyUri` | optional local policy | optional local policy |
+
+Here `EgressPermitted` is not a contractual assertion about a provider. It is a property of
+the architecture: the Server runs the model in-process, or calls a loopback HTTP process on
+the same machine. There is no provider network call for input to take. If the wrapper writes
+prompts, tensors or outputs to logs, that is retention, not egress, and it belongs in
+`RetainsInput` and the operator's policy.
+
+`DataJurisdiction` is still worth filling. Local does not mean ungoverned; it means the
+processing location is the site that owns the machine, which is exactly the question §9.5
+asks a deployment to answer.
+
+## What this system does not tell you
+
+- **Who approved the file.** `DigestProvenance` is `ComputedByServer` when the Server hashes
+ the local file, and `VerifiedOnStage` only where a catalogue digest matched during §10.4
+ staging. A digest says which bytes are present, not that they are safe to run.
+ `ProvenanceUri`, `Card` and the governance material of §11 still have to come from the
+ operator's process or catalogue.
+- **What the model was trained on.** Neither ONNX Runtime nor llama.cpp exposes training
+ lineage. If lineage matters, it comes from a model card, a registry or manual governance.
+- **A remote health signal for embedded runtimes.** There is no endpoint to ask. The only
+ useful probe is loading the file and, where appropriate, running a small local check.
+- **Provider-managed scale.** Running on the Server keeps residency local and lets the Server
+ publish file-derived provenance, but someone owns the disk layout, accelerator drivers,
+ model updates and rollback plan on every machine.
+- **Native jobs or native transfers.** `InvokeAsync` and `BeginTransfer` are Server features
+ here. That is acceptable, but it means their lifecycle, expiry and cleanup are your design.
+
+## Conformance units
+
+This arrangement is an **AI Inference Device Server**: the model runs in the
+Server's own process, and the guide reaches the **AI-Base** and **AI-Invoke**
+facets that §13.3 bundles for an in-process inference Server.
+
+Reachable against embedded runtimes: **AI-Base**, **AI-Invoke**, **AI-InvokeAsync**,
+**AI-Transfer**, **AI-Residency**, **AI-Catalogue** and **AI-Import**. **AI-Signatures** is
+not established by this guide for ONNX Runtime or llama.cpp; claim it only where the Server
+has a verified way to publish tensor signatures.
+
+For in-process runtimes that do not meter, **AI-Invoke** is satisfied by returning
+`Usage` with empty `UnitKind` and zero counts; §13.2 accommodates that. `llama-server`
+responses are metered when they carry the OpenAI-compatible token counts shown above.
+
+Out of reach without something else: **AI-Learning** needs a training loop; **AI-OffServer**
+does not describe an `OnServer` deployment; **AI-Stream** needs a subscription or data-channel
+stream the Server chooses to implement.
+
+## Sources
+
+- [ONNX Runtime GenAI C++ API](https://onnxruntime.ai/docs/genai/api/cpp.html)
+- [ONNX Runtime GenAI Python API](https://onnxruntime.ai/docs/genai/api/python.html)
+- [llama.cpp server tools](https://github.com/ggml-org/llama.cpp/tree/master/tools/server)
diff --git a/metaverse-specs/extras/ai-model-management/examples/google-vertex-ai.md b/metaverse-specs/extras/ai-model-management/examples/google-vertex-ai.md
new file mode 100644
index 0000000..8943e18
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/google-vertex-ai.md
@@ -0,0 +1,255 @@
+# Google Vertex AI
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+Google Vertex AI serves hosted Gemini models, publisher models and custom models through
+regional Google Cloud endpoints. The native Gemini surface uses `:generateContent` and
+`:streamGenerateContent` on a fully qualified model resource, while custom and AutoML
+prediction use `:predict` on an endpoint resource.
+
+The important distinction for this specification is not what Vertex AI can offer in total.
+It is the contract this Server actually speaks to the endpoint. A Server using the native
+Vertex AI request body is different from one reaching Vertex AI through the beta
+OpenAI-compatible surface, even if both ultimately call a model hosted by Google.
+
+## The `ModelSourceType`
+
+| Member | Native Vertex AI | OpenAI-compatible Vertex AI |
+|---|---|---|
+| `SourceId` | your name for it, stable across restarts | as native |
+| `EndpointUri` | the regional Vertex AI endpoint and resource path, for example `https://aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/{publisher}/models/{model}:generateContent` | `https://{location}-aiplatform.googleapis.com/v1beta1/projects/{project}/locations/{location}/endpoints/openapi/` |
+| `ApiDialect` | `Proprietary` | `RestChatCompletions` |
+| `EndpointDescriptionUri` | the documentation for `:generateContent`, `:streamGenerateContent` or `:predict` | not required; the dialect names the contract |
+| `AuthenticationKind` | `WorkloadIdentity`, or `BearerToken` where the Server stores a token | as native |
+| `CredentialReference` | names the service account, workload binding or token record — never the value | as native |
+| `TokenAudience` | the Google OAuth scope used to obtain the access token | as native |
+| `Reachability` | maintained from `TestConnection` and from call outcomes | as native |
+
+`ApiDialect` is `Proprietary` for the native Vertex AI surfaces because the request body is
+neither chat-completions JSON nor KServe Open Inference Protocol. The Gemini body has
+`contents`, `systemInstruction`, `generationConfig` and `safetySettings`; the custom
+prediction body has `instances`. §9.2 says a Server using `Proprietary` should populate
+`EndpointDescriptionUri`, and Vertex AI is a clear case for doing so because even two
+native Vertex AI operations have different payload shapes.
+
+Vertex AI also has an OpenAI-compatible endpoint for some models, reached through an
+`openai` SDK base URL under `endpoints/openapi/`. The research flags its exact GA status as
+uncertain and describes it as beta. A Server that uses that surface is
+`RestChatCompletions`, not `Proprietary`, because the dialect records the wire contract
+this Server actually speaks, not every contract the provider could have served.
+
+The arrangement to reach for is `WorkloadIdentity`: Google service accounts, Workload
+Identity Federation, GKE workload identity and Cloud Run managed identity all let the
+Server obtain short-lived access tokens without storing a secret. Under §9.2, a directly
+stored bearer token is `BearerToken`. The research verifies OAuth and Application Default
+Credentials but does not state the literal OAuth scope; put the configured Google OAuth
+scope in `TokenAudience`, following the same pattern the Foundry guide uses for Azure.
+
+## Identity
+
+Vertex AI publisher model resource names are structured. For publisher models the name is a
+path such as
+`projects/{project}/locations/{location}/publishers/{publisher}/models/{model}`. That is
+much richer than a bare model id: the `publishers/{publisher}` segment gives `Publisher`
+directly, and the `locations/{location}` segment gives the deployment's
+`DataJurisdiction` from the resource identity itself.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | the `publishers/{publisher}` segment, or the `publisher` field returned by the publisher-model listing | available for publisher models |
+| `Name` | `displayName`, or the final `models/{model}` segment where no display name is used | keep the resource path separately in `ModelId` |
+| `Version` | the `version` field for publisher models, `versionId` for custom models, or `modelVersion` in a Gemini response | do not invent one where none is present |
+| `ModelId` | the full Vertex AI resource `name` | keep it verbatim; it is what identifies the hosted model |
+| `PublishedAt` | `createTime` | source publication or registry creation time under §6.2.3 |
+| `LastModifiedAt` | `updateTime` | source modification time under §6.2.3 |
+| `Framework`, `Format` | not exposed for hosted Gemini publisher models | leave empty |
+| `Digest`, `DigestAlgorithm` | **not exposed** | `artifactUri` is not a digest |
+| `DigestProvenance` | `NotAvailable` | no artefact digest is exposed; `artifactUri` is not one |
+
+This resource name carries provenance and residency fields the Server already has to use. A
+deployment pointed at
+`projects/p/locations/europe-west4/publishers/google/models/gemini-...` carries its
+publisher and region in the identifier the Server already has to call. A Server still
+needs to publish the OPC UA `DataJurisdiction` on the deployment, but it is not forced to
+recover that value from an out-of-band convention.
+
+Custom models are less uniform. The Model Registry lists
+`projects/{project}/locations/{location}/models/{model}` resources with `displayName`,
+`versionId`, `artifactUri` and `metadataSchemaUri`. The location remains structured, but
+there is no `publishers/{publisher}` segment in that resource path.
+
+The listing timestamps have direct homes. `createTime` populates `PublishedAt` and
+`updateTime` populates `LastModifiedAt`; §6.2.3 distinguishes those source times from the
+Server's own read timestamp.
+
+## `Invoke`
+
+For native Gemini `:generateContent`, the request body goes through as the caller supplied
+it. §8.2 makes the payload opaque to the Server, and this is the kind of vendor-shaped JSON
+that rule exists to preserve.
+
+| Deployment member | Native Vertex AI | OpenAI-compatible Vertex AI |
+|---|---|---|
+| `ApiDialect` | `Proprietary` | `RestChatCompletions` |
+| `EndpointDescriptionUri` | the documentation for `:generateContent`, `:streamGenerateContent` or `:predict` | not required; the dialect names the contract |
+
+The deployment's `ApiDialect` is the same as the source's when the Server passes the
+payload through. §6.4.2 says the deployment value answers the client's question — what
+shape to put in `Payload` — while the source value answers what this Server speaks onward.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the `GenerateContentResponse` body, verbatim |
+| `ResponseContentType` | `application/json` |
+| `ModelUsed` | the `ModelType` NodeId this deployment resolved to — not the response's `modelVersion` string |
+| `Usage.UnitKind` | `tokens` |
+| `Usage.InputUnits` | `usageMetadata.promptTokenCount` |
+| `Usage.OutputUnits` | `usageMetadata.candidatesTokenCount` |
+| `Usage.TotalUnits` | `usageMetadata.totalTokenCount` |
+| `FinishReason` | `candidates[0].finishReason`, mapped below |
+| `SafetyAssessment` | populated from `safetyRatings` when filtering or safety policy intervened |
+| `RetryAfter` | the `Retry-After` header, where the response carries one |
+
+The usage field names are not the OpenAI names. A Server that reads
+`usage.prompt_tokens` from a native Vertex AI response will publish empty usage even when
+the endpoint reported it. The verified native fields are `promptTokenCount`,
+`candidatesTokenCount` and `totalTokenCount` under `usageMetadata`.
+
+`FinishReason` maps: `FINISH_REASON_STOP` to `Stop`,
+`FINISH_REASON_MAX_TOKENS` to `Length`, and the safety-related values
+`FINISH_REASON_SAFETY`, `FINISH_REASON_RECITATION`, `FINISH_REASON_BLOCKLIST`,
+`FINISH_REASON_PROHIBITED_CONTENT`, `FINISH_REASON_IMAGE_PROHIBITED_CONTENT`,
+`FINISH_REASON_NO_IMAGE` and `FINISH_REASON_SPII` to `Filtered`.
+`FINISH_REASON_MALFORMED_FUNCTION_CALL`, `FINISH_REASON_OTHER` and
+`FINISH_REASON_UNSPECIFIED` are best reported as `Error` unless the Server has a more
+specific policy. The research did not verify a successful tool-call terminal value on the
+native Vertex AI response.
+
+For custom model `:predict`, the response shape depends on the model. It still maps to
+`Invoke` because §8.2 makes the payload opaque, but there is no general usage or finish
+reason mapping unless the selected model's response schema carries those fields.
+
+## Asynchronous inference
+
+Vertex AI has native Batch Prediction jobs. The research verifies
+`POST /v1/projects/{project}/locations/{location}/batchPredictionJobs`, with input from
+Google Cloud Storage or BigQuery, a returned job resource and status, and JSONL on GCS for
+Gemini batch input.
+
+That gives `InvokeAsync` a real remote job to map onto.
+
+| Member | From |
+|---|---|
+| `JobId` | the Vertex AI batch prediction job name |
+| `RequestUri` | the GCS or BigQuery input URI |
+| `ResponseUri` | the GCS or BigQuery output URI |
+
+The OPC UA job follows the Part 10 program lifecycle required by §8.6 while the Server
+observes the Vertex AI job and exposes the result or failure through the `InferenceJobType`
+instance.
+
+## Large payloads
+
+For large media, the Vertex AI pattern is to put the object in Google Cloud Storage and
+refer to it from the request as `fileUri` in a `fileData` part. That is useful for native
+Gemini requests, and it is a by-reference payload rather than a chunked upload path for one
+OPC UA call.
+
+| Member | From |
+|---|---|
+| `PayloadUri` | the `fileUri` in a `fileData` part, or the GCS or BigQuery batch input URI |
+| `RequestUri` | the input URI actually submitted |
+| `ResponseUri` | the output URI returned or configured for a batch prediction job |
+
+`BeginTransfer` is the Server's own Part 5 `FileType` transfer path as §8.2 defines, for
+a payload too large to carry through the OPC UA call. §8.6.1 separates that case from data
+that already lives in Cloud Storage or BigQuery. A `PayloadUri`, `RequestUri` or
+`ResponseUri` is untrusted input under §12.2, and it is also an egress decision under §9.5:
+a deployment whose `EgressPermitted` is false **shall not** accept a `PayloadUri` naming
+somewhere outside the operator's boundary. The Server may still reassemble an OPC UA
+transfer and then issue one ordinary Vertex AI request or write one ordinary batch input
+object where that egress is permitted.
+
+## The catalogue
+
+The publisher-model listing,
+`GET https://aiplatform.googleapis.com/v1beta1/publishers/*/models`, is a useful source
+for `ListModels` on the `ModelSourceType`. It returns `name`, `publisher`, `displayName`,
+`description`, `version`, `supportedGenerationMethods`, `createTime` and `updateTime`,
+which is enough to expose the hosted models this source can call.
+
+The custom Model Registry is also useful for federation. It lists project and location
+models with `name`, `displayName`, `versionId`, `createTime`, `updateTime`, `artifactUri`
+and `metadataSchemaUri`.
+
+Neither surface is a complete §10.1 catalogue by itself. The publisher-model list exposes
+hosted models, not content-addressed artefacts a Server can stage and verify. The custom
+registry exposes an `artifactUri`, but the research found no cryptographic digest. A Server
+can federate Vertex AI-hosted models under §9.1; it cannot satisfy `AI-Catalogue` or
+`AI-Import` from these APIs alone because §10.4 needs a digest to verify what was imported.
+
+## Residency, egress and retention
+
+The region is visible in the Vertex AI endpoint and in the model resource name. Retention
+and training use are contractual statements, not fields in the inference response. Treat
+the table as operator assertions, except for the region value carried by the resource path.
+
+| Member | Google Vertex AI |
+|---|---|
+| `InferenceLocation` | `Cloud` |
+| `EgressPermitted` | `true` |
+| `DataJurisdiction` | the `locations/{location}` segment used by the endpoint and model resource |
+| `RetainsInput` | asserted from the operator's Google Cloud data-use arrangement |
+| `EgressPolicyUri` | your policy document |
+
+`EgressPermitted` is `true` because the request leaves the Server for a cloud service.
+§9.5 makes the point: OAuth, service accounts and TLS answer who may call and who can read
+the traffic in flight, not whether the payload left the site.
+
+The region-in-identity point is genuinely useful. A Server configured with a Vertex AI
+publisher model can derive the jurisdiction it publishes from the same resource name it
+uses to invoke the model, and a commissioning review can compare that value against the
+endpoint URL. The API still does not state retention in the response; `RetainsInput` is the
+operator's assertion.
+
+## What this system does not tell you
+
+- **Which weights answered.** No digest is returned for hosted Gemini models, publisher
+ model listings or custom model registry entries. `DigestProvenance` is `NotAvailable`
+ under §12.1.1. `artifactUri` is a storage location, not a content hash.
+- **What the model was trained on.** Nothing maps to a dataset lineage in the information
+ model. If lineage matters, it comes from a model card or supplier documentation outside
+ the inference response.
+- **The exact OAuth scope in the research file.** The research verifies OAuth, ADC,
+ service accounts and workload identity, but the literal scope text is redacted or absent.
+ Publish the configured Google OAuth scope in `TokenAudience`; do not move it into
+ `CredentialReference`.
+- **The maturity of the OpenAI-compatible surface.** The research says the
+ OpenAI-compatible Vertex AI endpoint is beta and flags its exact GA status as uncertain.
+ A Server using it should document that endpoint choice explicitly.
+- **Retention in an inference response.** The response reports usage and safety fields, but
+ not whether input is retained or used for training. That remains an operator assertion in
+ `RetainsInput`.
+
+## Conformance units
+
+This arrangement is an **AI Inference Gateway Server**: it reaches the
+**AI-Base**, **AI-Invoke**, **AI-OffServer**, **AI-Federation** and
+**AI-Residency** facets that §13.3 bundles for a hosted inference Server.
+
+Reachable against Google Vertex AI: **AI-Base**, **AI-Invoke**, **AI-InvokeAsync**,
+**AI-Transfer**, **AI-OffServer**, **AI-Federation**, **AI-Residency**.
+
+Out of reach without something else: **AI-Catalogue** and **AI-Import** need a
+content-addressed registry; **AI-Signatures** needs tensor shapes this contract does not
+carry for Gemini; **AI-Learning** needs training lineage and promotion semantics beyond
+the inference response.
+
+## Sources
+
+- [Vertex AI Gemini model reference](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini)
+- [Vertex AI OpenAI migration guide](https://cloud.google.com/vertex-ai/generative-ai/docs/migrate/migrate-from-openai)
+- [Vertex AI publisher models REST reference](https://cloud.google.com/vertex-ai/docs/reference/rest/v1beta1/publishers.models)
diff --git a/metaverse-specs/extras/ai-model-management/examples/hugging-face.md b/metaverse-specs/extras/ai-model-management/examples/hugging-face.md
new file mode 100644
index 0000000..6898b04
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/hugging-face.md
@@ -0,0 +1,250 @@
+# Hugging Face
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+Hugging Face Hub is a catalogue, not a serving surface. It can be projected as a model
+registry, and it can tell a Server something the hosted inference systems in this guide set
+cannot: which immutable revision and which LFS-stored file digest describe the artefact.
+
+That distinction is the spine of this mapping. A Hugging Face commit SHA identifies a
+repository revision; a per-file LFS `sha256` identifies an artefact file. §10.4 verifies a
+fetched artefact against a declared digest, so the file digest is the value that can make
+that gate real.
+
+## The `ModelSourceType`
+
+Hugging Face Hub is not an inference endpoint. Most of the serving-oriented members are
+therefore either empty or operator-maintained, and the useful operation is catalogue
+listing rather than `Invoke`.
+
+| Member | Hugging Face Hub |
+|---|---|
+| `SourceId` | your name for this Hub catalogue source |
+| `EndpointUri` | `https://huggingface.co/api/` |
+| `ApiDialect` | `Proprietary` |
+| `EndpointDescriptionUri` | `https://huggingface.co/.well-known/openapi.json` |
+| `AuthenticationKind` | `Anonymous` for public repositories, `BearerToken` where private or gated repositories are accessed |
+| `CredentialReference` | names the Hugging Face token — never the value |
+| `TokenAudience` | empty |
+| `Reachability` | maintained from Hub API calls such as listing or repository detail |
+| `Capabilities` | catalogue capabilities, not inference capabilities |
+| `TestConnection` | a Hub API probe |
+| `ListModels` | backed by `GET /api/models` and filtered by the Server |
+
+§9.2 states the catalogue-only case directly: because the Hub speaks no inference
+contract, `ApiDialect` is `Proprietary` and `EndpointDescriptionUri` points at the Hub's
+own OpenAPI document. That is the accurate answer for this source, not an apology for a
+missing inference literal.
+
+The Hub's content still belongs in a `ModelRegistryType` under §10. Let a separate
+`ModelSourceType` describe whatever actually serves inference — Inference Endpoints, or a
+runtime you deployed the weights to.
+
+Keep the two dialect answers separate. The Hub catalogue source is `Proprietary`, because
+it is not an inference contract. A deployment created from an imported Hub model publishes
+the `ApiDialect` of the runtime that serves it: `RestChatCompletions` for a compatible
+chat endpoint, `EmbeddedRuntime` for an in-process runtime, `OpenInferenceProtocol` for an
+OIP server, or another value when that is the contract the OPC UA caller must send.
+
+The table above is for the case where a Server does want `ListModels` and `TestConnection`
+against the Hub itself, which is a reasonable thing to want and is why it is written out.
+
+`ListModels` pages naturally over the Hub listing. The Hub API takes `limit` and `skip`;
+§9.4 gives the OPC UA Method an opaque `ContinuationPoint` so a catalogue this size is not
+truncated by `MaxResults`. A Server can encode the next Hub offset, the filter it belongs
+to and any source-side cursor state into that opaque value. The first call supplies an
+empty `ContinuationPoint`, each following call returns the value supplied by the previous
+one, and an empty returned value means the enumeration is complete.
+
+`AuthenticationKind` is `BearerToken` only when the Server stores a Hugging Face token in
+its credential store. A public anonymous projection is `Anonymous`, and the public xrproxy
+adapter takes that stricter route: it reads the Hub without an auth token and rejects
+incoming authorization headers.
+
+Gated and private repositories matter at the catalogue boundary. A gated model may be
+visible as metadata while its artefacts require an entitlement; a private repository needs
+a token even to read. In both cases `CredentialReference` names the credential used by the
+Server, and §9.2's rule applies unchanged: the address space never carries the token.
+
+## Identity
+
+The Hub gives a better identity than a hosted inference endpoint because a repository has
+an owner, a name and immutable commits.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | `author` or the owner segment of `id` | maps to `ModelPublisherType` |
+| `Name` | the repository basename | maps to `ModelResourceType` |
+| `Version` | commit `sha` | immutable version identity |
+| `ModelId` | full repo id, such as `google-bert/bert-base-uncased` | keep it verbatim |
+| `TaskKind` | `pipeline_tag` | where the field is present |
+| `Framework` | `library_name` | where the field is present |
+| `Card` | `cardData` and the model card | structured metadata plus the human card |
+| `LastModifiedAt` | `lastModified` | ISO timestamp from the Hub listing |
+| `Digest`, `DigestAlgorithm` | per-file LFS `sha256` from the tree API | artefact-level digest, not the commit SHA |
+| `DigestProvenance` | `DeclaredBySource`; `VerifiedOnStage` after staging | the Hub declares the LFS digest; §10.4 verifies fetched bytes |
+| `ArtifactUri` | the selected file URL or Hub resource URL | choose the artefact being imported |
+| `ProvenanceUri` | the Hub repository or xRegistry resource URL | points back to the catalogue entry |
+
+The commit SHA is a Git object identity for the repository revision. It answers "which
+revision of this repository did I mean?" and is the right `Version` for the catalogue
+resource.
+
+It is not the hash of a weights file. `GET /api/models/{owner}/{repo}/tree/{rev}` returns
+file entries, and LFS-stored files include an `lfs` field with `sha256`. That is the value
+to publish as `Digest` with `DigestAlgorithm` set to the corresponding SHA-256 algorithm
+name when the model artefact is one of those LFS files.
+
+Branches and tags are mutable pointers to commits. A deployment pinned to a commit is
+`Pinned`; a deployment tracking `main` or another branch is `FollowsRef` with `BoundRef`
+set to that ref. §9.3 is exactly about this case, and §12.3 makes the consequence plain:
+repointing the followed ref changes what the equipment runs and must be treated as an
+authorization-bearing act, not as harmless configuration.
+
+The Hub listing's `lastModified` is the source-side timestamp for `LastModifiedAt`. It is
+especially important when a deployment follows a branch or tag, because §6.2.3 requires a
+Server following a mutable reference to populate `LastModifiedAt` rather than substituting
+its own acquisition time.
+
+## `Invoke`
+
+The Hub does not define `Invoke`. A Server importing from or federating to the Hub needs a
+separate runtime: Hugging Face Inference Endpoints, TGI, vLLM, transformers, llama.cpp or
+some other serving arrangement.
+
+Where the runtime is a TGI-backed Hugging Face Inference Endpoint in chat mode, the call can
+look like `RestChatCompletions` and follows the chat-completions mapping used by the other
+guides: response JSON is returned as `ResponsePayload`, token counts populate
+`UsageDataType`, and finish reasons map to `FinishReasonEnum` as that dialect defines.
+
+Where the runtime is the older task-specific endpoint shape, the request is task-specific
+JSON and the response usually has no token accounting. The Server can still return the
+payload under §8.2, but `UsageDataType` may be empty and `FinishReason` will usually be
+`Stop` for success or `Error` for a failed or uninterpretable response.
+
+Do not infer serving semantics from the catalogue entry. `pipeline_tag` is useful for
+`TaskKind`; it is not a complete request or response contract.
+
+## Asynchronous inference
+
+The Hub has no batch inference API. Hugging Face Inference Endpoints may offer asynchronous
+arrangements in some configurations, but the research did not verify a standard job-based
+API that all endpoints expose.
+
+So `InvokeAsync` is normally the Server's own job under §8.6. It accepts the request,
+returns an `InferenceJobType`, runs the chosen runtime, and records the same `ModelUsed`,
+`Usage`, `FinishReason` and result payload that `Invoke` would have returned.
+
+## Large payloads
+
+The Hub stores repository files through Git and Git LFS. That is catalogue storage, not an
+inference transfer protocol.
+
+`BeginTransfer` is therefore the Server's own OPC UA transfer path under §8.2.4. For a
+staged import, the Server fetches the selected artefact from the Hub, verifies the LFS file
+digest under §10.4 and then exposes the staged artefact or deployment locally. For an
+inference request whose payload is too large, the Server reassembles the request and sends
+one ordinary call to whatever runtime it uses.
+
+## The catalogue
+
+This is where Hugging Face fits the specification.
+
+The public xrproxy Hugging Face adapter already uses the xRegistry shape that Annex B cites:
+
+| Hugging Face concept | xRegistry concept | AI model-management type |
+|---|---|---|
+| owner or namespace | group | `ModelPublisherType` |
+| repository basename | resource | `ModelResourceType` |
+| commit SHA | version identity | `ModelReferenceDataType.Version` / `ModelType.Version` |
+| branches and tags | mutable refs | `MutableRefs`, `FollowsRef`, `BoundRef` |
+
+For `google-bert/bert-base-uncased`, the proxy path is
+`/huggingfaceregistries/google-bert/models/bert-base-uncased`, and a version path appends
+`/versions/{sha}`. The owner is the group; the repository is the resource; the commit SHA is
+the immutable version.
+
+That is the shape §10.1 requires. It is also the one case in this guide set where §10.4 can
+be an actual protection. Hosted inference platforms identify models by strings and return
+no weight hash. Hugging Face can give a Server both the immutable revision and, for
+LFS-stored artefacts, the per-file SHA-256 that the Server can recompute after staging.
+
+The import rule is precise:
+
+- populate the import job's `Registry` with the `ModelRegistryType` NodeId and leave
+ `Source` null, per §10.2;
+- use the commit SHA as the version identity;
+- use the selected LFS file's `sha256` as `Digest`;
+- set `DigestAlgorithm` to SHA-256;
+- fail the staging import if the fetched bytes do not match, as §10.4 requires.
+
+`cardData` and the model card relate to `Card` on `ModelType` and to §11.1. They are not a
+substitute for a digest, and the card's training-data information is not guaranteed to be a
+structured `DatasetType` lineage.
+
+## Residency, egress and retention
+
+The Hub API does not tell a Server the plant-level residency answer. The operator states
+it on the deployment that uses the model.
+
+| Member | Hugging Face catalogue import | Hugging Face-hosted inference |
+|---|---|---|
+| `InferenceLocation` | depends where the staged model runs | usually `Cloud` for managed endpoints |
+| `EgressPermitted` | `false` after a local staged import; `true` while fetching from the Hub if the fetch crosses the boundary | `true` |
+| `DataJurisdiction` | the operator's site or zone after staging | the contracted endpoint region or jurisdiction |
+| `RetainsInput` | `false` for local inference if no remote runtime receives input | operator assertion for managed inference |
+| `EgressPolicyUri` | your policy document | your policy document |
+
+A staged local deployment is the interesting outcome: model bytes came from the Hub, but
+inference data does not have to. §9.5 asks where input goes during invocation, not where the
+artefact was obtained.
+
+## What this system does not tell you
+
+- **Which file is the model artefact.** A repository can contain several large files. The
+ catalogue gives file digests; the Server or import policy must choose which file is the
+ deployable artefact.
+- **A single repository-wide artefact digest.** The commit SHA identifies a revision. The
+ LFS `sha256` identifies one file. §12.1.1 forbids publishing the commit SHA as the
+ artefact `Digest`; `DigestProvenance` is `DeclaredBySource` unless §10.4 staging makes it
+ `VerifiedOnStage`.
+- **Structured training lineage.** Model cards and `cardData` can describe training data,
+ but the research did not verify a structured API field that populates `TrainedOn` or a
+ `DatasetType` without human or policy interpretation.
+- **Inference-plane provenance.** Inference responses do not return the Hub commit or file
+ digest that answered. If that matters, bind the deployment to the imported `ModelType` and
+ return that node as `ModelUsed`.
+- **Residency and retention.** Hub and endpoint responses do not populate
+ `DataJurisdiction`, `EgressPermitted` or `RetainsInput`; the operator asserts them.
+
+## Conformance units
+
+This arrangement is an **AI Model Catalogue Server**: the Hub projection reaches
+**AI-Base**, **AI-Catalogue** and **AI-Import** without requiring the Server to
+call `Invoke`, which is the catalogue shape described in §13.3.
+
+Reachable against a Hugging Face catalogue projection: **AI-Base**, **AI-Catalogue** and
+**AI-Import**. **AI-Residency** is reachable for deployments the Server creates from the
+imported model, because the operator can state the invocation boundary.
+
+**AI-Base** is worth a sentence here because this is the shape §13.1 has in mind when it
+describes a plant node that "may never call `Invoke` at all". Its deployment requirements
+apply to each deployment a Server exposes, so a Server that exposes none satisfies them
+vacuously and claims **AI-Base** on its `AiRootType`, `SpecificationVersion` and
+`ModelType` obligations — which is what a catalogue Server actually has.
+
+Reachable only with a separate runtime: **AI-Invoke**, **AI-InvokeAsync**, **AI-Transfer**,
+**AI-OffServer**, **AI-Federation** and **AI-Signatures** depend on how the model is served
+after catalogue import. **AI-Learning** needs training workflow support, which the Hub
+catalogue does not provide.
+
+## Sources
+
+- [Hugging Face Hub API documentation](https://huggingface.co/docs/hub/api)
+- [Hugging Face Hub OpenAPI document](https://huggingface.co/.well-known/openapi.json)
+- [Hugging Face Inference Endpoints documentation](https://huggingface.co/docs/inference-endpoints/en/index)
+- xregistry/xrproxy Hugging Face adapter README, verified at SHA `ac3fa09ec72c851c62435da2104269f20b439640`
+- xregistry/xrproxy `huggingface/src/hf-client.ts`, verified at SHA `6e37565e75bfbfd023e9db8042d865484d132a77`
diff --git a/metaverse-specs/extras/ai-model-management/examples/index.md b/metaverse-specs/extras/ai-model-management/examples/index.md
new file mode 100644
index 0000000..a600b85
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/index.md
@@ -0,0 +1,223 @@
+# Implementing this specification against real systems
+
+Informative. Nothing here is normative, and nothing here introduces a member: every one is
+defined in [the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md).
+
+The specification is vendor-neutral on purpose. §9.2 names the `ApiDialectEnum` literals
+for what a contract *does* rather than for whoever published it first, because a literal in
+a standard should not be an advertisement. That is right for the normative document, and it
+leaves an implementer holding a question it does not answer: they are not integrating "a
+REST chat-completions contract", they are integrating Azure AI Foundry, or Amazon Bedrock,
+or the Triton server already running in the plant.
+
+These guides answer it. One per system, each saying which member takes which field, and —
+more usefully — which members the system gives you nothing to fill.
+
+## The guides
+
+| Guide | What it is | `ApiDialect` |
+|---|---|---|
+| [Azure AI Foundry](azure-ai-foundry.md) | hosted inference, and Foundry Local on the machine | `RestChatCompletions` |
+| [OpenAI](openai.md) | hosted inference | `RestChatCompletions` |
+| [Amazon Bedrock](aws-bedrock.md) | hosted inference | `Proprietary` |
+| [Amazon SageMaker](aws-sagemaker.md) | hosted endpoints serving your own container | `Proprietary` |
+| [NVIDIA NIM](nvidia-nim.md) | self-hosted microservices | `RestChatCompletions` |
+| [NVIDIA Triton](nvidia-triton.md) | self-hosted inference server | `OpenInferenceProtocol`, `TensorRemoteProcedure` |
+| [Google Vertex AI](google-vertex-ai.md) | hosted inference | `Proprietary` |
+| [Hugging Face](hugging-face.md) | a catalogue, not a serving surface | — |
+| [KServe / Open Inference Protocol](kserve-open-inference-protocol.md) | the vendor-neutral baseline | `OpenInferenceProtocol` |
+| [Embedded runtimes](embedded-runtimes.md) | ONNX Runtime and llama.cpp, in process | `EmbeddedRuntime`, `RestChatCompletions` |
+| [Another OPC UA Server](opc-ua-server.md) | federating to a Server implementing this specification | `OpcUaInference` |
+
+Every `ApiDialectEnum` literal is exercised by at least one guide, and
+`tools/validate_examples.py` checks that it stays that way. A dialect nobody could show an
+example of would be a literal worth removing.
+
+Read [Another OPC UA Server](opc-ua-server.md) against any of the others when you want the
+shortest statement of what this specification is for. It is the only guide in the set where
+nothing is lost, because the remote system has the same members — every other guide is an
+exercise in deciding what to do about the ones it does not have.
+
+## The shape every guide follows
+
+The same nine sections in the same order, so that the tables mean something and so that a
+reader who has read one guide can skim the next:
+
+1. What the system is.
+2. **The `ModelSourceType`** — the filled member table.
+3. **Identity** — how the endpoint's model naming becomes `Publisher`, `Name` and `Version`.
+4. **`Invoke`** — the request and response mapping, including `UsageDataType` and
+ `FinishReasonEnum`.
+5. **Asynchronous inference** — whether `InvokeAsync` has anything native to map onto.
+6. **Large payloads** — whether `BeginTransfer` has anything native to map onto.
+7. **The catalogue** — how §10 is satisfied, or why it cannot be.
+8. **Residency, egress and retention** — what the operator asserts because the API does not.
+9. **What this system does not tell you.**
+
+Section 9 is the one to read first. A mapping document that lists only what maps is
+marketing; what an implementer needs before committing is the list of things they will have
+to source from somewhere else, or decide to live without.
+
+## What none of them gives you
+
+Across the set, eight of the eleven arrangements reach the **AI Inference Gateway Server**
+profile, two reach **AI Inference Device Server**, one reaches **AI Model Catalogue
+Server**, and none reaches **AI Model Lifecycle Server**. That last is not a shortcoming of
+the profile: these eleven are inference and catalogue systems, and none of them is a plant
+that trains, which is the shape clause 7 was written for.
+
+**An artefact digest.** Not Azure AI Foundry, not OpenAI, not Bedrock, not SageMaker, not
+NIM, not Triton, not Vertex AI. Model identity on every hosted inference platform in this
+set is a name string, sometimes carrying a date — `gpt-4o-2024-08-06`,
+`meta/llama-3.1-8b-instruct`. None of them returns a cryptographic hash of the weights that
+answered.
+
+Two entries are exceptions, and neither is a hosted platform. Hugging Face is
+content-addressed as a catalogue, with an immutable commit SHA per revision and a `sha256`
+per LFS-stored file. An [embedded runtime](embedded-runtimes.md) holds the artefact on the
+machine, so the Server can hash the file itself — which is the only arrangement in the set
+where a digest is something this Server computed rather than something it was told.
+
+Three consequences worth taking seriously:
+
+- `Digest` and `DigestAlgorithm` are Mandatory members (§6.2) and stay empty against a
+ hosted endpoint. §12.1 requires a Server to populate `Digest` for every model **whose
+ artefact is obtainable through `ArtifactUri`**, and a hosted model's is not, so the
+ obligation does not bite. What the Server states instead is `DigestProvenance`
+ `NotAvailable`, which is Mandatory and always answerable: the absence is recorded rather
+ than merely left, and a client can tell a source that publishes no digest from a Server
+ that declined to carry one.
+- Filling `Digest` with something derived from the model's name is what would defeat §12.1
+ entirely. Every guide in this set met a different tempting value — a response
+ fingerprint, a resource name, a storage entity tag, a repository commit identifier — and
+ §12.1.1 prohibits all of them in one rule, because each looks like an artefact digest,
+ is not one, and will eventually be compared against a real one.
+- A `Pinned` deployment against a hosted endpoint is pinned to a **name**. The binding says
+ the artefact behind it cannot change without an observable change to the deployment, and
+ what actually enforces that is the provider's policy rather than anything the Server can
+ verify. That is a materially weaker guarantee than the same word carries against an
+ artefact the Server holds, and an audit process designed around it should know which one
+ it has.
+- The `Stage` import mode of §10.3 is the only path in this set that ends with a digest the
+ Server computed from bytes it fetched, and §10.4's verification — refusing to deploy on a
+ mismatch — is a real gate only there and on an embedded runtime. Those are also the only
+ two arrangements that reach `DigestProvenance` `VerifiedOnStage`.
+
+`DigestProvenance` is worth reading as the axis this whole set varies along, because it
+grades the evidence rather than merely recording its presence:
+
+| Value | Where it is reachable in this set |
+|---|---|
+| `NotAvailable` | Every hosted inference API: Foundry, OpenAI, Bedrock, SageMaker's inference plane, Vertex AI, NIM, Triton, base OIP |
+| `DeclaredBySource` | Hugging Face, whose tree API declares a per-file `sha256` the Server can forward without hashing anything; and a federated peer Server, whose digest is an assertion received |
+| `ComputedByServer` | An [embedded runtime](embedded-runtimes.md), where the artefact is a local file and hashing it costs nothing |
+| `VerifiedOnStage` | A `Stage` import from a catalogue that declared a digest — in practice Hugging Face, or a peer registry |
+
+Read down that column and the pattern is that **evidence tracks custody**. A Server can say
+something strong about an artefact exactly to the degree it has held the bytes, and no
+amount of vendor cooperation short of publishing a content hash changes that. It is also
+why the embedded case is not the poor relation it looks like: it is the only arrangement
+here where a Server can produce evidence entirely on its own.
+
+**Training lineage, data residency, and whether your input is retained.** No inference API
+in this set states any of them in a response. `TrainedOn`, `DataJurisdiction`,
+`EgressPermitted` and `RetainsInput` are therefore operator assertions: someone reads the
+contract and the region configuration and writes down what is true. The model gives them a
+place to write it down and a client a way to read it, which is the whole of what a protocol
+can do here.
+
+That these are assertions rather than measurements is not a weakness of the model. It is
+the situation, stated.
+
+There is one exception, and it is the case where an assertion can be checked against
+another: where a deployment federates to another Server implementing this specification,
+§9.5 obliges it to read that Server's declarations and forbids publishing anything more
+permissive. [The federation guide](opc-ua-server.md) works it through. It propagates
+honesty rather than establishing it — but it closes the case where every Server in a chain
+is truthful and the answer still comes out wrong because nobody was obliged to look up.
+
+**A model's age, and how long it has left.** These split the set in a way none of the other
+questions do, and the split runs the opposite way to the digest one.
+
+Six of the eleven publish a vintage — `created` on OpenAI, Azure and NIM, `createTime` and
+`updateTime` on Vertex AI, `lastModified` on Hugging Face, `startOfLifeTime` on Bedrock —
+so `PublishedAt` and `LastModifiedAt` (§6.2.3) are answerable more often than `Digest` is.
+`LastModifiedAt` matters more than it looks: a deployment with `VersionBinding` `FollowsRef`
+can have the artefact change beneath it with nothing else changing, and §12.3.1's audit
+trail points at a job record that a source-side move never produces. This is the member
+that makes the move visible at all.
+
+Exactly one system says when a model **stops**. Bedrock's `modelLifecycle` carries
+`legacyTime` and `endOfLifeTime`, and nothing else in the set has an equivalent. One vendor
+out of eleven is a thin basis for a member and it is in the model anyway, because on that
+date the deployment does not degrade — it stops, `FallbackPolicy` fires, and where that is
+`FallBackTo` the line keeps producing while something outside the qualified configuration
+answers. §11.1 sets out the reasoning. Every other availability facility here is a way of
+coping after the fact; `SupportedUntil` is the only one whose value is a date in the future.
+
+**Where the large data actually lives.** Every hosted platform in the set takes a storage
+URI in and out — OpenAI and Azure `file_id`, Bedrock's S3 input and output configuration,
+SageMaker's `InputLocation` header, Vertex's `fileUri` — and none of the self-hosted four
+does. That is not a coincidence: a hosted platform is on the far side of a network from
+your data, and a self-hosted runtime is not.
+
+The distinction §8.6.1 draws is worth carrying into every mapping. *A payload too large to
+carry* is a transport problem and `BeginTransfer` solves it. *Data that never needed to
+move* is not a transport problem at all, and chunking a batch that already sits in the
+plant's object store copies it twice for no benefit. `PayloadUri` is for the second, and it
+comes with an obligation these guides state repeatedly: a URI the execution site reads is a
+path the input data takes, so §9.5's `EgressPermitted` governs it exactly as it governs the
+endpoint.
+
+## Reading the mappings
+
+`AuthenticationKind` classifies **what is stored**, not which handshake is performed —
+§9.2 states the rule, and it is what makes the member answerable across systems whose
+handshakes have nothing in common. It is also why §9.2 prefers `WorkloadIdentity`: it is
+the one value under which no secret exists anywhere for an attacker to read.
+
+AWS is the case that shows the rule working. SigV4 is not one of the five literals and does
+not need to be: signed by an assigned IAM role it is `WorkloadIdentity`, because nothing is
+stored; signed by static access keys it is `ApiKey`, because something is. One scheme, two
+values, decided by what an attacker could steal. [The Bedrock guide](aws-bedrock.md) works
+it through, and points `EndpointDescriptionUri` at the handshake for a reader who needs it
+recorded exactly.
+
+`ApiDialect` is read the same way — it names the contract *this Server speaks to that
+endpoint*, not everything the endpoint could offer. The same runtime is `EmbeddedRuntime`
+in process and `RestChatCompletions` over its own loopback server; the same host is
+`Proprietary` through its native API and `RestChatCompletions` through its
+OpenAI-compatible one.
+
+## Throttling, on every one of them
+
+`Reachability` separates `Throttled` from `Unreachable` on purpose, and §9.4 gives the
+reason: the two look alike from outside and call for opposite responses. An unreachable
+endpoint should be failed over. A throttled one will serve again shortly, and failing it
+over merely moves the load onto a weaker model for nothing.
+
+Every hosted platform in this set throttles, and the mapping is the same for all of them
+because it is HTTP rather than anyone's API:
+
+| Model | From |
+|---|---|
+| `Reachability` = `Throttled` | HTTP 429, or a documented capacity refusal |
+| `RateLimit.RetryAfter` | a Duration, parsed from the `Retry-After` header — which carries either seconds or an HTTP date, so it is converted rather than copied |
+| `RateLimit.Limit`, `RateLimit.Remaining`, `RateLimit.Interval` | rate-limit response headers, where the platform returns them |
+| `RateLimit.UnitKind` | `requests` or `tokens`, depending on which quota bound |
+
+Do not count a 429 as a failure. `ConsecutiveFailures` answers *is this endpoint broken*,
+and a quota refusal is an endpoint working exactly as contracted. Folding the two together
+produces a deployment that reports itself as failing whenever it is busy, which is both
+wrong and the moment a supervisory client most needs the report to be right.
+
+Which headers a given platform returns, and under what names, is documented per platform
+and is not reproduced here — the shape above is what the model asks for, and the header
+names are the one part of this that changes without notice.
+
+## Currency
+
+Each guide records the date its vendor facts were verified and links to primary
+documentation. Vendor APIs move and nothing here can keep up with them automatically —
+`tools/validate_examples.py` checks these guides against the information model, which is
+the part that can be checked. Dating the rest is more honest than implying it is fresh.
diff --git a/metaverse-specs/extras/ai-model-management/examples/kserve-open-inference-protocol.md b/metaverse-specs/extras/ai-model-management/examples/kserve-open-inference-protocol.md
new file mode 100644
index 0000000..c87fd73
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/kserve-open-inference-protocol.md
@@ -0,0 +1,221 @@
+# KServe / Open Inference Protocol v2
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+Open Inference Protocol v2 is the vendor-neutral tensor inference contract associated with
+KServe. It is not a hosted product, and that is why the specification has an
+`OpenInferenceProtocol` literal: an installation that standardises on this contract can
+change serving implementations without changing any client of this Server.
+
+KServe and NVIDIA Triton both implement this shape. Triton has its own guide because it
+also exposes NVIDIA-specific surfaces; use this guide for the open protocol baseline and
+see [the NVIDIA Triton guide](nvidia-triton.md) when the implementation is Triton.
+
+## The `ModelSourceType`
+
+| Member | KServe / OIP v2 endpoint |
+|---|---|
+| `SourceId` | your name for the serving endpoint |
+| `EndpointUri` | the base URL that exposes `/v2` |
+| `ApiDialect` | `OpenInferenceProtocol` |
+| `EndpointDescriptionUri` | the OIP v2 specification or local endpoint documentation |
+| `AuthenticationKind` | implementation-defined: often `Anonymous`, `BearerToken`, `MutualTls` or `WorkloadIdentity` |
+| `CredentialReference` | names the configured credential — never the value |
+| `TokenAudience` | only where the chosen authentication scheme needs it |
+| `Reachability` | maintained from liveness, readiness and model-readiness probes |
+| `Capabilities` | tensor inference and any implementation extensions the Server chooses to publish |
+| `TestConnection` | maps to the OIP health and model-readiness probes |
+| `ListModels` | not defined by base OIP; implementation-specific if present |
+
+The protocol does not define authentication. Apply §9.2 to the credential the Server
+stores: no credential is `Anonymous`, a stored bearer credential is `BearerToken`, platform
+identity with no stored secret is `WorkloadIdentity`, and mutual TLS credentials are
+`MutualTls`.
+
+`ApiDialect` is the important member here. It names the wire contract rather than KServe as
+a product, so a Server can move from one OIP implementation to another without changing the
+OPC UA-facing deployment description.
+
+A deployment that accepts the OIP request body from an OPC UA caller publishes
+`DeploymentType.ApiDialect` as `OpenInferenceProtocol`. This is the clean case for
+§6.4.2: the payload contract genuinely is the OIP body, so the source dialect and the
+deployment dialect are the same when the Server passes that body through.
+
+The health endpoints make `TestConnection` real rather than a disguised inference call:
+server liveness, server readiness and model readiness distinguish a dead process, a server
+that is not ready to serve, and one model that is not ready. Those results are the natural
+source for `Reachability`, with call failures and throttling updating it afterwards as
+§9.4 describes.
+
+## Identity
+
+OIP names models by strings in the URL and optionally by version strings in the same path.
+It does not carry a content digest.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | operator assertion | not present in the protocol |
+| `Name` | `{MODEL_NAME}` | from `/v2/models/{MODEL_NAME}` |
+| `Version` | `{MODEL_VERSION}` or one entry from `versions` | optional |
+| `ModelId` | the model name, or name plus version | keep the value the endpoint expects |
+| `Framework` | `platform` | examples include serving platforms such as TensorRT plan |
+| `Inputs` | metadata `inputs` | names, datatypes and shapes |
+| `Outputs` | metadata `outputs` | names, datatypes and shapes |
+| `PublishedAt`, `LastModifiedAt` | not exposed | leave empty unless another catalogue supplies them |
+| `Digest`, `DigestAlgorithm` | not exposed | leave empty unless another catalogue supplies them |
+| `DigestProvenance` | `NotAvailable` | base OIP returns no artefact digest |
+
+`GET /v2/models/{name}[/versions/{version}]` returns model metadata: `name`, optional
+`versions`, `platform`, and `inputs` and `outputs` with tensor names, datatypes and shapes.
+Those `inputs` and `outputs` map directly onto `Inputs` and `Outputs` on `ModelType`.
+
+Base OIP exposes no source publication or modification timestamp. `PublishedAt` and
+`LastModifiedAt` therefore stay empty on the same terms as `Digest`: an implementation may
+populate them from another catalogue, but the OIP metadata response does not establish
+them.
+
+That makes **AI-Signatures** reachable. §6.2 says these tensor signatures are the
+machine-readable description of what a deployment accepts, and OIP is one of the few
+serving protocols in this guide set that actually exposes them.
+
+An explicit version in the URL is a `Pinned` binding. A deployment that follows a serving
+alias or other mutable name is `FollowsRef` with `BoundRef` naming that ref, and §9.3's
+warning applies: changing the ref changes what answers.
+
+## `Invoke`
+
+The standard inference call is:
+
+```http
+POST /v2/models/{MODEL_NAME}[/versions/{MODEL_VERSION}]/infer
+```
+
+The request carries an optional id, optional parameters, input tensors and requested output
+tensors. A tensor has a `name`, `shape`, `datatype` and data. The response mirrors the
+shape with output tensors.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the OIP response body, verbatim |
+| `ResponseContentType` | `application/json` for the HTTP/REST binding |
+| `ModelUsed` | the `ModelType` NodeId for the resolved model and version |
+| `Usage.UnitKind` | empty for an unmetered OIP response; otherwise the implementation extension's unit, per §8.2.3 |
+| `Usage.InputUnits` | `0` when `UnitKind` is empty; otherwise the measured input count |
+| `Usage.OutputUnits` | `0` when `UnitKind` is empty; otherwise the measured output count |
+| `Usage.TotalUnits` | `0` when `UnitKind` is empty; otherwise the measured total count |
+| `FinishReason` | normally `Stop` for success, `Error` for failed or invalid responses |
+| `SafetyAssessment` | not defined by OIP |
+| `RetryAfter` | only from implementation-specific throttling headers |
+
+This is a tensor protocol, not a chat protocol. It has no standard token accounting, no
+choice list and no text-generation finish reason. That is not a defect; it is exactly what
+a reader mapping a vision model, a vibration model or a classical tensor model will meet.
+§8.2.3 gives that absence an encoding: `Usage` is returned, with an empty `UnitKind` and
+zero counts, and clients treat those zeros as "not metered" rather than as measurements.
+
+The OIP datatypes verified in the research include `BOOL`, unsigned and signed integer
+widths, `FP16`, `FP32`, `FP64`, `BYTES` and `STRING`. The Server maps those into the tensor
+signature element types it publishes with `Inputs` and `Outputs`.
+
+## Asynchronous inference
+
+Base OIP v2 does not define a job or handle pattern. Server-side dynamic batching is an
+implementation detail, not an `InvokeAsync` contract.
+
+So `InvokeAsync` is normally implemented by the OPC UA Server under §8.6: it creates an
+`InferenceJobType`, submits or schedules the ordinary OIP inference request, and publishes
+the job lifecycle and result through OPC UA. If an implementation adds its own async API,
+that remains behind the same `InferenceJobType` surface.
+
+## Large payloads
+
+Base OIP v2 does not define chunked upload or a file-transfer API for inference. The HTTP
+body contains the tensors.
+
+`BeginTransfer` is therefore the Server's OPC UA transfer path under §8.2.4. The client
+writes the request through `InferenceTransferType`, the Server sends one OIP request when
+ready, and the response is read back through the same OPC UA exchange if it is too large to
+return inline.
+
+## The catalogue
+
+OIP model metadata is not a catalogue in the §10 sense. It tells a caller what one serving
+endpoint can execute and what tensor shapes it expects; it does not define publishers,
+resources, immutable content-addressed versions, model cards or artefact digests.
+
+That means `ListModels`, if an implementation provides it, is a serving inventory rather
+than a `ModelRegistryType`. It can create useful `ModelType` instances for deployments, and
+it can populate `Inputs` and `Outputs`, but it cannot by itself satisfy **AI-Catalogue** or
+**AI-Import**.
+
+Pair OIP with a real catalogue when import provenance matters. The catalogue supplies
+`Publisher`, `Name`, `Version`, `Digest` and `DigestAlgorithm`; OIP supplies the serving
+contract and the tensor signatures.
+
+## Residency, egress and retention
+
+OIP says nothing about geography or retention. Those are deployment facts supplied by the
+operator and by the platform that hosts the endpoint.
+
+| Member | KServe / OIP v2 endpoint |
+|---|---|
+| `InferenceLocation` | usually `EdgeOffServer`, `Cloud` or `OnServer`, depending where the endpoint runs |
+| `EgressPermitted` | `true` if input leaves the operator boundary, otherwise `false` |
+| `DataJurisdiction` | the site, cluster, region or jurisdiction the operator uses |
+| `RetainsInput` | operator assertion; report `true` if it cannot be established |
+| `EgressPolicyUri` | your policy document |
+
+The common on-premises KServe case is exactly why §9.5 separates location from egress. A
+remote endpoint on the plant network is off the Server, but it may still keep data inside
+the operator boundary.
+
+## What this system does not tell you
+
+- **Who published the model.** OIP has `name` and optional `version`, not a publisher
+ namespace. `Publisher` is supplied by the operator or by an external catalogue.
+- **Which bytes are running.** No content digest is returned. `Digest` and
+ `DigestAlgorithm` need another source, `DigestProvenance` is `NotAvailable`, and §10.4
+ cannot verify an import from OIP alone.
+- **Usage accounting.** There are no standard token, image, sample or byte counts in the
+ OIP response envelope. Without an implementation extension, `UsageDataType` uses the
+ §8.2.3 not-metered sentinel: empty `UnitKind` and zero counts.
+- **Chat finish semantics.** There is no standard equivalent of `length`, `tool_calls` or
+ `content_filter`. `FinishReasonEnum` has little to say beyond `Stop` and `Error`.
+- **Asynchronous jobs or large-object transfer.** Base OIP defines synchronous tensor
+ inference. `InvokeAsync` and `BeginTransfer` are OPC UA Server features unless the chosen
+ implementation adds something native.
+- **Residency and retention.** The protocol does not state where input is processed or
+ whether it is retained.
+
+## Conformance units
+
+Where the OIP endpoint is off-server and the operator states the residency boundary,
+this arrangement is an **AI Inference Gateway Server**: it reaches the
+**AI-Base**, **AI-Invoke**, **AI-OffServer**, **AI-Federation** and
+**AI-Residency** facets that §13.3 bundles for a gateway.
+
+Reachable against a conforming OIP endpoint: **AI-Base**, **AI-Invoke**,
+**AI-Federation** and **AI-Signatures**. **AI-OffServer** is reachable for a deployment
+whose `InferenceLocation` is not `OnServer`, which is what §13.2 asks for. What that
+deployment must additionally satisfy is §12.2, and it is not optional: where
+`InferenceLocation` is not `OnServer`, `EndpointUri` **shall** name an authenticated,
+confidential scheme. A bare `/v2` base URL over plain HTTP does not, so an OIP endpoint
+reached across a network is fronted with TLS and authentication before this facet is
+claimed. **AI-Residency** is reachable where the operator can state the deployment
+boundary required by §9.5.
+
+The absence of OIP usage counts does not weaken **AI-Invoke**: the Server returns `Usage`
+with empty `UnitKind` and zero counts for an unmetered call, and §13.2 accommodates that.
+
+Reachable through the OPC UA Server rather than the OIP protocol itself: **AI-InvokeAsync**
+and **AI-Transfer**. Out of reach without another system: **AI-Catalogue** and **AI-Import**
+need a catalogue with immutable versions and digests; **AI-Learning** needs a training
+workflow.
+
+## Sources
+
+- [KServe Predict Protocol v2 required API](https://github.com/kserve/kserve/blob/master/docs/predict-api/v2/required_api.md)
+- [KServe Predict Protocol v2 documentation](https://github.com/kserve/kserve/blob/master/docs/predict-api/v2/)
diff --git a/metaverse-specs/extras/ai-model-management/examples/nvidia-nim.md b/metaverse-specs/extras/ai-model-management/examples/nvidia-nim.md
new file mode 100644
index 0000000..6022da8
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/nvidia-nim.md
@@ -0,0 +1,204 @@
+# NVIDIA NIM
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+NVIDIA NIM Microservices serve models through the OpenAI-compatible `/v1/` contract, backed
+by vLLM for LLMs. The same contract can be reached on NVIDIA's hosted endpoint or from a
+self-hosted container on NVIDIA GPU infrastructure.
+
+That makes NIM the cleanest example of §8.1 in this set: where inference runs does not
+change how it is called. The difference is recorded in `InferenceLocation`,
+`EgressPermitted` and `DataJurisdiction`, not in the `Invoke` signature.
+
+## The `ModelSourceType`
+
+| Member | Self-hosted NIM | Hosted NIM |
+|---|---|---|
+| `SourceId` | your name for the NIM instance | your name for the NVIDIA endpoint |
+| `EndpointUri` | `http://{host}:8000/v1/` by default | `https://integrate.api.nvidia.com/v1/` |
+| `ApiDialect` | `RestChatCompletions` | `RestChatCompletions` |
+| `EndpointDescriptionUri` | not required; the dialect names the contract | as self-hosted |
+| `AuthenticationKind` | `Anonymous`, unless the operator adds one | `ApiKey` |
+| `CredentialReference` | empty, or the name of the proxy credential — never the value | names the stored key — never the value |
+| `TokenAudience` | empty | empty |
+| `Reachability` | maintained from `TestConnection` and call outcomes | as self-hosted |
+
+Self-hosted NIM does not enforce authentication by default. If the plant puts an
+authentication wrapper in front of it, §9.2 says `AuthenticationKind` describes the
+credential this Server stores for that wrapper. It is not a claim about what NIM itself
+implements.
+
+A NIM running on the same host as the OPC UA Server has `InferenceLocation` `OnServer`. A
+NIM running on a GPU appliance elsewhere on the plant network has `InferenceLocation`
+`EdgeOffServer`. Both are ordinary and honest arrangements; this member is what tells them
+apart.
+
+## Identity
+
+`GET /v1/models` returns the OpenAI-shaped listing: `id`, `object`, `created` and
+`owned_by`. NIM model ids are unusually helpful because an id such as
+`meta/llama-3.1-8b-instruct` carries the publisher namespace directly.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | the part before `/` in `id`, where present; otherwise empty unless independent provenance identifies the producer | `meta/llama-3.1-8b-instruct` yields `meta` |
+| `Name` | the part after `/`, or the whole `id` if there is no `/` | keep the model name as served |
+| `Version` | not exposed as a field | do not invent one from the name |
+| `ModelId` | the whole `id` | this is what the endpoint expects |
+| `PublishedAt` | `created` | Unix timestamp from the source, not the Server's acquisition time |
+| `Framework`, `Format` | not exposed by the listing | leave empty unless configured out of band |
+| `Digest`, `DigestAlgorithm` | **not exposed** | the manifest is not a weight hash |
+| `DigestProvenance` | `NotAvailable` | no artefact digest is exposed; manifest profile metadata is not one |
+
+The publisher split is worth noticing. An id such as `meta/llama-3.1-8b-instruct` carries
+the originator in the id itself, so `Publisher` can be `meta` — the organisation that
+trained the model — rather than the organisation hosting it, which is what a bare
+`owned_by` usually gives you. That is §6.2's rule: `Publisher` answers "who made this",
+not "who is serving it".
+
+The OpenAI-shaped `created` field belongs in `PublishedAt`. §6.2.3 requires the source's
+publication time rather than the time this Server first saw the model.
+
+NIM also exposes `GET /v1/manifest`, which returns model profile metadata. Precision maps
+to `Quantization`. GPU compatibility belongs on the deployment through `AcceleratorKind`
+and `AcceleratorName`, because it says what this instance can run on. `GET /v1/metadata`
+returns the active model profile identity, and that profile id belongs in
+`RuntimeIdentity`: the profile itself has a home instead of being reduced to its precision
+and accelerator fields. The manifest does not provide an artefact digest under §12.1.1, so
+it cannot populate `Digest`.
+
+## `Invoke`
+
+The request body goes through as the caller supplied it. §8.2 makes the payload opaque, and
+NIM gives the Server no reason to reinterpret it: the remote contract is the ordinary
+OpenAI-compatible chat, completions or embeddings request.
+
+| Deployment member | From |
+|---|---|
+| `ApiDialect` | `RestChatCompletions` |
+| `RuntimeIdentity` | the active model profile id from `GET /v1/metadata` |
+
+The deployment's `ApiDialect` is the same as the source's because the Server passes the
+OpenAI-compatible payload through. §6.4.2 says that value tells an OPC UA client what to
+put in `Payload`, while the source value tells this Server what to speak to NIM. §9.3.1
+uses `RuntimeIdentity` for the serving configuration identity, so an active profile change
+is observable on the deployment even when the model id stays the same.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the response body, verbatim |
+| `ResponseContentType` | `application/json` |
+| `ModelUsed` | the `ModelType` NodeId this deployment resolved to |
+| `Usage.UnitKind` | `tokens` |
+| `Usage.InputUnits` | `usage.prompt_tokens` |
+| `Usage.OutputUnits` | `usage.completion_tokens` |
+| `Usage.TotalUnits` | `usage.total_tokens` |
+| `FinishReason` | `choices[0].finish_reason`, mapped below |
+| `SafetyAssessment` | populated when filtering is reported |
+| `RetryAfter` | the `Retry-After` header, where the response carries one |
+
+`FinishReason` maps: `stop` to `Stop`, `length` to `Length`, `tool_calls` to `ToolCall`,
+`content_filter` to `Filtered`. `Cancelled` is produced by this Server when it cancels the
+call, and `Error` covers a response that arrived but could not be understood.
+
+`ModelUsed` is still a NodeId in this Server's address space, not the endpoint's `model`
+string. The endpoint string is useful, but it cannot answer the §8.2.1 question when a
+fallback deployment answered instead.
+
+## Asynchronous inference
+
+The standard NIM microservice API has no job-based batch or asynchronous inference surface.
+It is a real-time request/response service.
+
+`InvokeAsync` is therefore the Server's own job, as §8.6 permits. The Server accepts the
+request, returns an `InferenceJobType`, runs the NIM request itself and stores the same
+`ResponsePayload`, `ModelUsed`, `Usage` and `FinishReason` that a synchronous `Invoke`
+would have returned.
+
+## Large payloads
+
+NIM accepts the request body directly. The research found no `/v1/files` endpoint and no
+dedicated chunked upload API on the NIM surface; images are carried in the request body
+where the selected model accepts them.
+
+So `BeginTransfer` is an OPC UA-side facility, not a NIM feature. The Server uses the
+transfer path of §8.2 to collect a request that is too large for a `ByteString`, then
+issues one ordinary NIM request.
+
+## The catalogue
+
+`GET /v1/models` is a useful `ListModels` implementation for a `ModelSourceType`: it tells
+the Server which loaded models this NIM endpoint can serve.
+
+It is not a §10 catalogue. It does not expose immutable content-addressed versions,
+`ModelResourceType` entries, dataset resources or digests. `GET /v1/manifest` adds profile
+metadata, including precision and GPU compatibility, but it still does not identify model
+bytes by hash.
+
+If the operator controls the container image and model repository, a separate plant
+catalogue can describe those artefacts and make `Stage` imports meaningful. That catalogue
+is outside the NIM inference API.
+
+## Residency, egress and retention
+
+The NIM API does not state residency, egress or retention. The operator asserts them on the
+deployment.
+
+| Member | Self-hosted NIM in the plant | Hosted NIM |
+|---|---|---|
+| `InferenceLocation` | `OnServer` or `EdgeOffServer` | `Cloud` |
+| `EgressPermitted` | `false` | `true` |
+| `DataJurisdiction` | the site or plant network zone | the service region or contract jurisdiction |
+| `RetainsInput` | `false` if the operator controls logging accordingly | operator assertion |
+| `EgressPolicyUri` | your plant policy | your provider policy |
+
+`EgressPermitted` is `false` for a NIM in the plant, and that is one of the main reasons to
+run one. The model may sit on another GPU appliance, but the payload stays inside the
+operator boundary.
+
+The difference between `OnServer` and `EdgeOffServer` is not cosmetic. It says whether a
+host failure takes both the OPC UA Server and the model down together, or whether the
+network path to the appliance can fail independently.
+
+## What this system does not tell you
+
+- **A weight digest.** The manifest describes profiles and compatibility, not a
+ cryptographic hash. `Digest` and `DigestAlgorithm` stay empty, and `DigestProvenance` is
+ `NotAvailable` under §12.1.1 unless an external catalogue supplies a digest.
+- **Training lineage.** Nothing in the NIM API maps to `TrainedOn` or `DatasetType`.
+- **A structured version.** The served `id` is the operational identifier. If it contains a
+ version-like substring, that is still part of the name unless another source defines it.
+- **Residency or retention.** Self-hosting makes the answers controllable, not automatic.
+ Record the operator's policy in `DataJurisdiction`, `EgressPermitted`, `RetainsInput` and
+ `EgressPolicyUri`.
+- **A built-in health contract beyond ordinary calls.** `TestConnection` can use
+ `GET /v1/models` or a lightweight inference request, but NIM's OpenAI-compatible surface
+ does not add a dedicated health endpoint in the cited API surface.
+
+## Conformance units
+
+A NIM on a GPU appliance elsewhere is an **AI Inference Gateway Server** where
+the secure off-server endpoint and residency assertions below are present. NIM on
+the same host as the OPC UA Server is an **AI Inference Device Server** instead,
+because its `InferenceLocation` is `OnServer`.
+
+Reachable against self-hosted NIM: **AI-Base**, **AI-Invoke**, **AI-InvokeAsync**,
+**AI-Transfer**, **AI-Federation** and **AI-Residency**. **AI-OffServer** is reachable for
+a deployment whose `InferenceLocation` is not `OnServer`, which is what §13.2 asks for.
+What that deployment must additionally satisfy is §12.2, and it is not optional: where
+`InferenceLocation` is not `OnServer`, `EndpointUri` **shall** name an authenticated,
+confidential scheme. NIM's default `http://{host}:8000/v1/` listener does not, so a NIM on
+a GPU appliance elsewhere on the plant network is fronted with TLS and authentication
+before this facet is claimed. NIM on the same host is `OnServer`, where the question does
+not arise.
+
+Out of reach from NIM alone: **AI-Catalogue** and **AI-Import** need a separate catalogue
+with digests; **AI-Signatures** needs tensor signatures the OpenAI-compatible contract does
+not carry; **AI-Learning** needs training and promotion lifecycle support.
+
+## Sources
+
+- [NVIDIA NIM for LLMs API reference](https://docs.nvidia.com/nim/large-language-models/2.0.0/reference/api-reference.html)
diff --git a/metaverse-specs/extras/ai-model-management/examples/nvidia-triton.md b/metaverse-specs/extras/ai-model-management/examples/nvidia-triton.md
new file mode 100644
index 0000000..6071bbb
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/nvidia-triton.md
@@ -0,0 +1,215 @@
+# NVIDIA Triton Inference Server
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+NVIDIA Triton Inference Server is a self-hosted inference runtime for configured models.
+It serves the KServe v2 Open Inference Protocol over HTTP
+and gRPC, with readiness and metadata endpoints beside inference.
+
+Triton is the guide in this set where tensor signatures matter. Its metadata endpoint
+returns input and output names, datatypes and shapes, which map directly onto `Inputs` and
+`Outputs` on `ModelType` and make **AI-Signatures** reachable without guessing.
+
+## The `ModelSourceType`
+
+| Member | REST endpoint | gRPC endpoint |
+|---|---|---|
+| `SourceId` | your name for the Triton server | as REST |
+| `EndpointUri` | `http://{host}:8000/v2/` by default | `{host}:8001` by default |
+| `ApiDialect` | `OpenInferenceProtocol` | `OpenInferenceProtocol` |
+| `EndpointDescriptionUri` | the KServe v2 protocol documentation, if published | the Triton gRPC service definition, if published |
+| `AuthenticationKind` | `Anonymous`, unless the operator adds a wrapper | as REST |
+| `CredentialReference` | empty, or the name of the wrapper credential — never the value | as REST |
+| `TokenAudience` | empty unless the wrapper requires one | as REST |
+| `Reachability` | maintained from readiness endpoints and call outcomes | as REST |
+
+Triton is the one system here where the same contract naturally appears over two
+transports. Use `OpenInferenceProtocol` when this Server reaches Triton's OIP binding,
+whether that is HTTP `POST /v2/models/{name}/infer` or the gRPC `ModelInfer` RPC.
+`TensorRemoteProcedure` is reserved for a tensor-oriented RPC contract that is not OIP,
+such as a dedicated inference server speaking its own predict RPC. The literal names the
+contract this Server calls, not the transport.
+
+The same distinction applies on the deployment side. A deployment that expects an OPC UA
+caller to send an OIP body publishes `DeploymentType.ApiDialect` as
+`OpenInferenceProtocol`, whether this Server forwards that body over HTTP or over gRPC.
+The dialect tells the caller what payload contract it must satisfy; the transport is a
+separate integration choice.
+
+Triton does not include authentication in its core protocol. If an operator adds nginx,
+Envoy or another gateway, §9.2 says `AuthenticationKind` describes the credential this
+Server stores for that gateway.
+
+## Identity
+
+`GET /v2/models/{name}` returns model metadata: `name`, optional `versions`, `platform`,
+`inputs` and `outputs`. `GET /v2` returns server metadata rather than model identity.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | operator-chosen namespace | Triton does not expose a publisher field |
+| `Name` | `name` | the repository model name |
+| `Version` | the selected entry from `versions`, where used | Triton has a real version path segment |
+| `ModelId` | `name` plus version when pinned | keep enough to reconstruct the endpoint path |
+| `Framework` | `platform` | examples include backend platform strings |
+| `Format` | operator assertion | not a separate OIP field |
+| `PublishedAt`, `LastModifiedAt` | empty | OIP metadata exposes no model vintage timestamp |
+| `Digest`, `DigestAlgorithm` | **not exposed by OIP v2** | see catalogue and import below |
+| `DigestProvenance` | `NotAvailable` | OIP v2 returns no artefact digest |
+
+Triton model versions are explicit: inference can target
+`/v2/models/{name}/versions/{version}/infer`. That is a real version dimension, unlike
+systems that carry a date or revision in a name string. A deployment that calls a specific
+version is `Pinned`; a deployment that calls only `{name}` and accepts whichever version
+the repository policy serves is a candidate for `FollowsRef`.
+
+The trap is that an explicit version is still not a digest. It tells the Server which
+repository version it addressed; it does not prove which bytes the repository mounted for
+that version.
+
+The research establishes Triton metadata fields for name, versions, platform and tensor
+signatures. It does not establish a protocol field for the source publication time or for
+the time the repository loaded the model, so `PublishedAt` and `LastModifiedAt` stay empty
+unless a separate catalogue supplies them. §6.2.3 forbids using the Server's own discovery
+or acquisition time as `PublishedAt`.
+
+## `Invoke`
+
+For REST, `Invoke` maps to `POST /v2/models/{name}/infer` or
+`POST /v2/models/{name}/versions/{version}/infer`. For gRPC, it maps to `ModelInfer`.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the OIP response body or encoded gRPC response |
+| `ResponseContentType` | `application/json` for REST, an implementation media type for gRPC |
+| `ModelUsed` | the `ModelType` NodeId this deployment resolved to |
+| `Usage.UnitKind` | empty when the model exposes no accounting output; otherwise the declared unit, per §8.2.3 |
+| `Usage.InputUnits` | `0` when `UnitKind` is empty; otherwise the measured input count |
+| `Usage.OutputUnits` | `0` when `UnitKind` is empty; otherwise the measured output count |
+| `Usage.TotalUnits` | `0` when `UnitKind` is empty; otherwise the measured total count |
+| `FinishReason` | `Stop` for a complete successful tensor response, or `Error` |
+| `SafetyAssessment` | not provided by the protocol |
+| `RetryAfter` | empty unless a wrapper supplies it |
+
+OIP v2 returns named output tensors. It does not define token counts, a usage envelope or a
+standard finish reason. If a model backend exposes counts or stop reasons as output
+tensors, a Server may map them, but that is a model contract rather than a Triton protocol
+feature.
+
+Where it does not expose counts, §8.2.3 supplies the representation: `Usage` is returned
+with an empty `UnitKind` and zero counts, and those zeros are not measurements.
+
+The payload is still opaque to the OPC UA caller under §8.2. A Server may validate it
+against `Inputs` before forwarding, which is exactly why the signatures are valuable, but
+the domain meaning of the tensors belongs to the consuming specification.
+
+## Asynchronous inference
+
+OIP v2 has no handle-and-poll job API. Triton supports server-side dynamic batching, but
+the client still sends a synchronous inference request and receives a synchronous response.
+
+`InvokeAsync` is therefore an OPC UA-side job as §8.6 describes. The Server accepts the
+request, returns an `InferenceJobType`, runs the Triton call and records the same
+`ResponsePayload`, `ModelUsed`, `Usage` and `FinishReason` that `Invoke` would have
+returned.
+
+## Large payloads
+
+The REST protocol carries tensor data in the request body, including base64-encoded bytes
+for `BYTES` tensors. The research found no OIP v2 file upload or chunked transfer API.
+
+gRPC streaming can be used for very large inputs, but that is still the Triton transport
+chosen by this Server. It does not replace `BeginTransfer` for OPC UA clients. Under
+§8.2.4, `BeginTransfer` lets the OPC UA client write the request through Part 5 `FileType`;
+the Server then calls Triton by its chosen REST or gRPC route.
+
+## The catalogue
+
+Triton serves configured models, but OIP metadata is not a §10 catalogue entry and does
+not carry a digest. Repository configuration is outside what OIP exposes; read Triton's
+own documentation for how a deployment configures and manages the model repository.
+
+The valuable metadata is the shape contract. `GET /v2/models/{name}` returns `inputs` and
+`outputs` with tensor names, datatypes and shapes. Those map directly to `Inputs` and
+`Outputs` on `ModelType`, whose semantics in §6.2 are name, element type, shape with `-1`
+for a dynamic axis, and an optional layout hint.
+
+That makes **AI-Signatures** reachable here. A client can check at configuration time that
+the tensors it intends to send match what the model declares, instead of discovering a
+shape mismatch as a failed production call. Most hosted chat-style systems in this set do
+not expose enough information to do that.
+
+`Stage` import under §10.3 needs a separate catalogue projection alongside Triton. That
+projection supplies the `ModelRegistryType`, `ModelPublisherType`, `ModelResourceType` and
+declared digest that §10.4 verifies; Triton alone does not expose them.
+
+## Residency, egress and retention
+
+Triton is self-hosted, so these answers come from the deployment topology and repository
+policy rather than from the protocol.
+
+| Member | Typical Triton deployment |
+|---|---|
+| `InferenceLocation` | `OnServer` or `EdgeOffServer` |
+| `EgressPermitted` | `false` when Triton is inside the operator boundary |
+| `DataJurisdiction` | the site, zone or storage jurisdiction of the serving deployment |
+| `RetainsInput` | `false` unless logging or tracing stores request tensors |
+| `EgressPolicyUri` | your plant or platform policy |
+
+The readiness endpoints are a better operational source than the listing-call
+approximation many hosted systems need. `GET /v2/health/ready` tests server readiness, and
+`GET /v2/models/{name}/ready` or the versioned form tests model readiness. These are
+natural inputs to `TestConnection`, `Reachability`, `LastSuccessAt` and
+`ConsecutiveFailures`.
+
+The cited research establishes Triton's metrics surface, but not a specific protocol field
+that maps a per-deployment latency value into this model. A Server that needs
+`ObservedLatency` therefore measures the call itself, which §6.4.3 permits; if it reports
+`Degraded` on latency grounds, it publishes that measured value so the state can be
+checked.
+
+## What this system does not tell you
+
+- **A content digest.** The cited OIP metadata exposes name, versions, platform and tensor
+ signatures, not a hash of repository contents. `DigestProvenance` is `NotAvailable` for
+ that projection; any digest comes from a separate catalogue.
+- **A publisher.** The model repository names models; it does not state who published them.
+ `Publisher` is an operator namespace unless a separate catalogue supplies one.
+- **Usage accounting.** Token counts and finish reasons are not protocol fields. Treat them
+ as model outputs only where the model explicitly declares them; otherwise `UsageDataType`
+ uses the §8.2.3 not-metered sentinel.
+- **Training lineage.** Nothing in the Triton protocol maps to `TrainedOn` or `DatasetType`.
+- **Residency or retention.** Self-hosting gives the operator control of the answer; the
+ answer still has to be recorded explicitly.
+
+## Conformance units
+
+A Triton reached across the plant network is an **AI Inference Gateway Server**
+where the secure off-server endpoint and residency assertions below are present.
+Triton on the same host as the OPC UA Server is an **AI Inference Device Server**
+instead, because its `InferenceLocation` is `OnServer`.
+
+Reachable against Triton: **AI-Base**, **AI-Invoke**, **AI-InvokeAsync**, **AI-Transfer**,
+**AI-Federation**, **AI-Residency** and **AI-Signatures**. **AI-OffServer** is reachable
+for a deployment whose `InferenceLocation` is not `OnServer`, which is what §13.2 asks for.
+What that deployment must additionally satisfy is §12.2, and it is not optional: where
+`InferenceLocation` is not `OnServer`, `EndpointUri` **shall** name an authenticated,
+confidential scheme. Triton's default `http://{host}:8000/v2/` listener does not, so a
+Triton reached across the plant network is fronted with TLS and authentication before this
+facet is claimed. Triton on the same host is `OnServer`, where the question does not arise.
+
+The unmetered case satisfies **AI-Invoke**: the Server returns `Usage` with empty
+`UnitKind` and zero counts, and §13.2 accommodates that.
+
+Out of reach from Triton alone: **AI-Catalogue** and **AI-Import**, because they need a
+separate conforming registry projection with catalogue resources and declared digests, and
+**AI-Learning**, because training and promotion lifecycle support are outside OIP v2.
+
+## Sources
+
+- [Triton Inference Server protocol documentation](https://github.com/triton-inference-server/server/blob/main/docs/protocol/README.md)
+- [KServe v2 required inference API](https://github.com/kserve/kserve/blob/master/docs/predict-api/v2/required_api.md)
+- [Triton gRPC service definition](https://github.com/triton-inference-server/common/blob/main/protobuf/grpc_service.proto)
diff --git a/metaverse-specs/extras/ai-model-management/examples/opc-ua-server.md b/metaverse-specs/extras/ai-model-management/examples/opc-ua-server.md
new file mode 100644
index 0000000..810f163
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/opc-ua-server.md
@@ -0,0 +1,270 @@
+# Another OPC UA Server
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. No vendor facts to date: the contract is this specification.
+
+A `ModelSourceType` whose `ApiDialect` is `OpcUaInference` names **another Server
+implementing this specification**. A cell Server delegating to a line Server, a line Server
+delegating to a site Server, one plant borrowing capacity from another.
+
+This is the only entry in the set where nothing is lost in translation, because there is no
+translation. Every other guide is an exercise in deciding what to do about members the
+remote system has no answer for; here the remote system has the same members, defined the
+same way, because it is running the same model. Reading this guide against any of the
+others is the clearest available statement of what the specification is *for*.
+
+## The `ModelSourceType`
+
+| Member | Value |
+|---|---|
+| `SourceId` | your name for the upstream Server |
+| `EndpointUri` | its OPC UA endpoint — `opc.tcp://…` |
+| `ApiDialect` | `OpcUaInference` |
+| `EndpointDescriptionUri` | not required; the dialect names the contract exactly |
+| `AuthenticationKind` | `MutualTls` under the usual certificate arrangement |
+| `CredentialReference` | names the client certificate — never its private key |
+| `TokenAudience` | empty; there is no token |
+| `Reachability` | from `TestConnection`, which opens a Session |
+
+`MutualTls` is the honest value for the ordinary OPC UA arrangement, where both ends hold
+application instance certificates and each validates the other's. It is the one place in
+this whole set where the authentication is symmetric — the upstream Server authenticates
+the downstream one as surely as the reverse — and no other entry in
+`AuthenticationKindEnum` says that.
+
+`Anonymous` is what a Session with `SecurityMode` `None` deserves to be called. If that is
+what you built, say so rather than claiming `MutualTls` because certificates exist
+somewhere in the deployment.
+
+## Identity
+
+Nothing has to be reconstructed. The upstream Server publishes `ModelType` instances with
+`Publisher`, `Name`, `Version`, `Digest` and `DigestAlgorithm` already populated, and the
+downstream Server browses them.
+
+| Member | From |
+|---|---|
+| `Publisher`, `Name`, `Version` | the upstream `ModelType`, read directly |
+| `ModelId` | the upstream `ModelId` |
+| `PublishedAt`, `LastModifiedAt` | the upstream members of the same names |
+| `DeprecatedFrom`, `SupportedUntil` | the upstream `ModelCardType`, read directly |
+| `Digest`, `DigestAlgorithm` | **whatever the upstream Server has** |
+| `DigestProvenance` | read upstream, but publish no stronger value |
+| `Framework`, `Format`, `TaskKind` | the upstream members of the same names |
+| `Inputs`, `Outputs` | the upstream signature, so **AI-Signatures** carries through |
+
+The important word is *whatever*. A digest does not appear because the link is OPC UA; it
+appears if the upstream Server had one. An upstream Server that is itself federating to a
+hosted endpoint has an empty `Digest`, and the downstream Server's `Digest` is empty for
+the same reason. Nothing along the chain may fill it in — §12.1.1 forbids inventing a
+value, and a Server that manufactured one at the second hop would be laundering the
+absence of provenance into the appearance of it.
+
+The date members compose the same way. `PublishedAt`, `LastModifiedAt`, `DeprecatedFrom`
+and `SupportedUntil` are read from the upstream Server and forwarded rather than invented.
+`LastModifiedAt` is load-bearing in this guide: §6.2.3 requires a Server whose deployment
+follows a mutable reference to populate it, because a reference moving at the source
+produces no job and §12.3.1's audit trail cannot otherwise be built. In a federation the
+source is another Server, so that is exactly the case.
+
+`DigestProvenance` composes the same way §9.5 composes residency, and §12.1.1 states the
+rule: the downstream Server reads the upstream value and shall not publish a stronger one.
+Forwarding an upstream digest without staging and hashing the bytes is `DeclaredBySource`
+from the downstream Server's point of view, even if the upstream Server's own value was
+`VerifiedOnStage`. Publishing that upstream `VerifiedOnStage` as this Server's verification
+would claim a check this Server did not perform.
+
+`RuntimeIdentity` follows the same boundary. The upstream value describes the upstream
+Server's serving configuration. The downstream Server's deployment has its own serving
+configuration and shall not republish the upstream `RuntimeIdentity` as its own, for the
+same reason §12.1.1 forbids republishing an upstream `DigestProvenance` value that claims a
+check this Server did not perform.
+
+What does travel is the `ImportedFrom` reference. Follow it upstream and the chain
+terminates wherever the artefact actually came from, however many Servers back that is.
+That is the whole point of §11 being a walk rather than a field.
+
+## `Invoke`
+
+The downstream Server calls the upstream `Invoke` and returns what comes back. Every output
+maps to itself: `ResponsePayload`, `ResponseContentType`, `Usage`, `FinishReason`,
+`SafetyAssessment`, `RetryAfter`.
+
+The downstream deployment's `ApiDialect` is `OpcUaInference`, because the OPC UA caller
+sends the §8 Method contract to this Server. `EndpointDescriptionUri` is not required for
+that deployment; the dialect names this specification exactly. Availability members are
+also read from the upstream deployment where they describe the same execution path, and a
+latency-based `Degraded` state carries `ObservedLatency` under §6.4.3 rather than a bare
+assertion.
+
+`ModelUsed` needs care, and it is the one genuinely interesting mapping in this guide.
+
+The upstream Server returns a NodeId **in its own address space**. It means nothing
+downstream — the downstream client cannot resolve it, and passing it through would hand a
+caller an identifier that looks resolvable and is not. The downstream Server therefore
+returns the NodeId of *its own* `ModelType` representing the model that answered.
+
+Which means the downstream Server has to be able to tell which one that was. If it
+publishes one local model per upstream model, the mapping is a lookup. If the upstream
+Server fell back — its `FallbackPolicy` being `FallBackTo`, and it substituted — then the
+NodeId it returned is not the model the downstream Server asked for, and the downstream
+Server must resolve *that* one or admit it cannot.
+
+A downstream Server that cannot resolve the returned NodeId to a local `ModelType` has one
+correct move and one tempting wrong one. **AI-Invoke** requires `ModelUsed` populated on
+every response (§13), so a null is not an available answer for a call that succeeded: the
+downstream Server publishes a `ModelType` for the substitute and returns that, or it fails
+the call.
+
+Returning the model that was *requested* is the tempting wrong one. It converts a visible
+substitution into an invisible one, at exactly the point where a reader is furthest from
+the evidence. A Server that cannot keep up with what its upstream is doing should say so by
+failing, not by answering plausibly.
+
+Which means the practical arrangement is to mirror the upstream's models — including its
+fallbacks — rather than only the ones this Server expects to use. A fallback that has never
+been exercised is precisely the one whose `ModelType` will be missing on the day it is.
+
+## Asynchronous inference
+
+`InvokeAsync` maps to the upstream `InvokeAsync`, and this is the one guide in the set
+where that sentence is complete. Both ends have `AiJobType` on the Part 10 program state
+machine, so the downstream Server can mirror the upstream job's state rather than inventing
+a lifecycle over a polled REST call.
+
+Mirror it rather than proxy it. The downstream job is a job in the downstream address
+space, with its own `JobId`, and it reaches Halted when the upstream one does. A client
+that subscribes to the downstream job should not need a Session on the upstream Server to
+learn what happened.
+
+## Large payloads
+
+`BeginTransfer` maps to the upstream `BeginTransfer`, both over Part 5 `FileType`.
+
+`PayloadUri` is an egress path. A URI the downstream Server passes to the upstream Server
+is a location the upstream execution site may read, and §8.6.1 classifies that as the same
+§9.5 question as an inline payload. The composition rule already stated for residency
+therefore applies: the downstream Server cannot know whether the upstream Server will fetch
+that URI from somewhere further out, so it shall not publish a more permissive egress answer
+than the upstream deployment supports.
+
+For `InvokeAsync`, the downstream job mirrors the same large-payload members. `RequestUri`
+records the URI submitted, `ResponseUri` records a result location returned by the upstream
+execution path, and `TransferRequired` with `Transfer` is represented by a transfer object
+in the downstream address space rather than by passing through an upstream NodeId.
+
+Read `MaxInlinePayloadSize` from the upstream deployment and publish a value **no larger**
+downstream. Publishing a larger one produces a call that the downstream Server accepts and
+the upstream Server refuses, which turns a bound that §8.2.4 exists to make visible in
+advance into one discovered from a rejection — the exact failure the member was added to
+prevent.
+
+The same applies to a chain: the effective limit is the smallest along it, and each hop
+publishes the smallest it knows about.
+
+## The catalogue
+
+An upstream `ModelRegistryType` is browsable directly, so §10 needs nothing built. A
+downstream Server can present the upstream registry, or federate against it with a
+`Mode` of `Federate` — the mode that moves no bytes and leaves the artefact where it
+is (§10.3).
+
+`ListModels` carries its `ContinuationPoint` through the same composition. The downstream
+Server passes the client's opaque value to the upstream Server when it represents upstream
+state, returns the upstream continuation value in its own response, and returns an empty
+value when the upstream enumeration is complete. If it multiplexes several upstream
+catalogues, its continuation value is still opaque to the client and records whichever
+upstream cursor or local merge state is needed.
+
+`Stage` also works and means what it says: the downstream Server fetches the artefact and
+holds it. That is the arrangement worth choosing when the link is the thing you do not
+trust, because after it the downstream Server can serve with the link down — and it can
+verify the digest itself, having the bytes to verify it against.
+
+## Residency, egress and retention
+
+| Member | Value |
+|---|---|
+| `InferenceLocation` | `EdgeOffServer` for another Server on the plant network; `Cloud` if it is genuinely off-site |
+| `EgressPermitted` | whether the payload leaves *this* site — not whether it leaves this machine |
+| `DataJurisdiction` | the upstream Server's, which you have to read from it and record |
+| `RetainsInput` | whatever the upstream Server declares, propagated |
+
+These do not compose automatically, and §9.5 is where the rule for that lives.
+
+The members are end-to-end rather than next-hop: `EgressPermitted` states whether calling
+this deployment sends input outside the operator's boundary **by any path**. A cell Server
+calling a site Server over the plant network looks like `EgressPermitted` `false` — one
+local hop, no internet. If that site Server is itself federating to a hosted endpoint, the
+payload leaves the site, and `false` would be untrue about the only thing a caller wanted
+to know.
+
+So §9.5 obliges a Server whose `Source` names another Server implementing this
+specification to **read** `DataJurisdiction`, `EgressPermitted` and `RetainsInput` from the
+upstream deployment, and forbids publishing values more permissive than the ones it read.
+A Server that cannot read them publishes `EgressPermitted` and `RetainsInput` true, because
+the assumption that keeps data in is the one that is safe to be wrong about.
+
+This is the same rule §12.3.2 states for the `FallsBackTo` edge. A payload leaves a
+deployment along exactly two modelled edges, and both are guarded.
+
+What it does not do is verify anything. An upstream Server that declares something false
+makes its downstream neighbours wrong too, and no protocol fixes that. What it fixes is the
+case where every Server along the chain is honest and the answer still comes out wrong
+because nobody was obliged to look up.
+
+## What this system does not tell you
+
+Very little, and the exceptions are worth naming precisely because they are so few:
+
+- **Whether the upstream Server is telling the truth.** Every residency and provenance
+ member is an assertion at every hop. Federation propagates assertions; it does not
+ verify them. `DigestProvenance` is `DeclaredBySource` for a forwarded upstream digest,
+ and `VerifiedOnStage` only when a `Stage` import gave this Server the bytes to check.
+- **How deep the chain goes.** There is no hop count. Following `ImportedFrom` and
+ `Source` walks it, and a cycle is possible if two Servers are configured to federate to
+ each other — worth checking for at commissioning, because nothing in the model prevents
+ it.
+- **What the upstream Server is federating to.** Browsable if it publishes a
+ `ModelSourceType`, which it should, and absent if it does not.
+
+Against every other guide in this set the closing section is a list of what the platform
+withholds. Here it is a list of what federation cannot manufacture, which is a different
+thing: the model is not losing information at this hop. It is carrying forward exactly as
+much as was there to begin with.
+
+## Conformance units
+
+Where this Server proxies the upstream inference Method and publishes the local
+residency declarations described below, the federating arrangement is an
+**AI Inference Gateway Server**. The upstream peer may claim any profile; that
+claim is not inherited by this Server.
+
+A conformance unit describes what **this** Server exposes, not what it can reach. A link to
+a capable upstream Server does not make that Server's facets local, and this is the trap
+worth stating plainly: federating to a Server with a catalogue does not give this Server
+**AI-Catalogue**. Mirroring the upstream registry into this address space does.
+
+Reachable from the link itself: **AI-Federation** and **AI-OffServer**, the second where
+the Session is secured — §13 requires `EndpointUri` to name an authenticated, confidential
+scheme, which an ordinary `opc.tcp` Session with `SignAndEncrypt` and mutual certificate
+validation satisfies and an unsecured one does not.
+
+Reachable where this Server proxies the upstream Method and publishes the required outputs
+locally: **AI-Base**, **AI-Invoke**, **AI-InvokeAsync**, **AI-Transfer**.
+
+Reachable only where this Server mirrors the upstream nodes rather than pointing at them:
+**AI-Catalogue**, **AI-Import**, **AI-Dataset**, **AI-Signatures**, **AI-Stream**,
+**AI-Learning**, **AI-Residency**. Each needs nodes in this address space that a client can
+browse, read and subscribe to without a Session on the upstream Server — which is what
+mirroring means and why it is work rather than configuration.
+
+This is the only guide in the set whose limits come from what this Server chose to build
+rather than from what the far end withholds.
+
+## Sources
+
+None. The contract is
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md) — §8 for
+the inference Methods, §9 for federation, §10 for the catalogue, §11 for provenance.
diff --git a/metaverse-specs/extras/ai-model-management/examples/openai.md b/metaverse-specs/extras/ai-model-management/examples/openai.md
new file mode 100644
index 0000000..e425df1
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/openai.md
@@ -0,0 +1,240 @@
+# OpenAI platform API
+
+Informative. Every member named here is defined in
+[the specification](../../../ai-model-management/OPC-UA-AI-Model-Management.md); this guide
+introduces none. Vendor facts verified 2026-08-05 against the documentation linked at the
+end.
+
+The OpenAI platform API serves models over the HTTP `/v1` API, with
+`POST /v1/chat/completions` as the chat-completions surface mapped here. The same platform
+also has the newer `/v1/responses` surface and other endpoints, but this guide is about the
+chat-completions shape because that is what `RestChatCompletions` names.
+
+That naming matters. The specification names the dialect for the wire contract, not for
+OpenAI as a vendor, because the same request and response shape is served by Azure AI
+Foundry, NVIDIA NIM, llama.cpp-compatible servers and other systems. A Server can therefore
+say `RestChatCompletions` without claiming that the endpoint is OpenAI-operated.
+
+## The `ModelSourceType`
+
+| Member | OpenAI platform API |
+|---|---|
+| `SourceId` | your name for it, stable across restarts |
+| `EndpointUri` | `https://api.openai.com/v1/` |
+| `ApiDialect` | `RestChatCompletions` |
+| `EndpointDescriptionUri` | not required; the dialect names the contract |
+| `AuthenticationKind` | `BearerToken` |
+| `CredentialReference` | names the bearer credential — never the value |
+| `TokenAudience` | empty |
+| `Reachability` | maintained from `TestConnection` and from call outcomes |
+
+`AuthenticationKind` is `BearerToken` for the ordinary OpenAI platform arrangement because
+the Server presents a bearer credential in the standard authorization header.
+`CredentialReference` is the name by which the Server finds that credential in its own
+store. It is not a place to copy the token, and §9.2 is explicit that credentials are not
+exposed through the address space.
+
+The OpenAI documentation also describes workload identity federation for obtaining
+short-lived tokens. Apply §9.2: if a Server implements that arrangement without storing a
+secret, the deployment is `WorkloadIdentity`; if it stores a bearer credential, it is
+`BearerToken`.
+
+## Identity
+
+`GET /v1/models` returns `id`, `object`, `created` and `owned_by`. That is the same thin
+identity exposed by the other OpenAI-compatible systems in this set, and it does not carry
+the full `ModelType` identity.
+
+| Member | From | Note |
+|---|---|---|
+| `Publisher` | empty unless independent provenance identifies the producer | `owned_by` reports the serving host or account, not the model producer |
+| `Name` | `id`, with the trailing date removed | only where the name follows that convention |
+| `Version` | the date suffix of `id`, where there is one | `gpt-4o-2024-08-06` yields `2024-08-06` |
+| `ModelId` | the whole `id` | keep it verbatim under §6.2; it is what you send back |
+| `PublishedAt` | `created` | Unix timestamp from the source, not the Server's acquisition time |
+| `Framework`, `Format` | not exposed | leave empty |
+| `Digest`, `DigestAlgorithm` | **not exposed** | see the `system_fingerprint` warning below |
+| `DigestProvenance` | `NotAvailable` | no artefact digest is exposed; `system_fingerprint` is not one |
+
+The `owned_by` field reports the serving host or account. §6.2 says `Publisher` names the
+organisation that produced the model and is left empty where only the serving organisation
+is known. The full `id` still goes verbatim in `ModelId`, so two Servers can compare the
+source system's own identifier even when the `Publisher`, `Name`, `Version` triple is
+incomplete.
+
+The `created` timestamp belongs in `PublishedAt`. §6.2.3 uses the source's publication
+time to order opaque model identifiers and forbids substituting the Server's acquisition
+time.
+
+Pinned model ids such as `gpt-4o-2024-08-06` are immutable by OpenAI policy, not by content
+addressing. A `Pinned` deployment is pinned to a name whose behaviour the provider promises
+to hold stable. It is not pinned to a digest the Server can verify.
+
+The `system_fingerprint` field in chat-completions responses is the trap in this mapping.
+It is a real backend configuration fingerprint and is useful for repeatability
+investigations, but it is not an artefact digest under §12.1.1. It therefore populates
+`RuntimeIdentity`, not `Digest`: §9.3.1 treats a change to `RuntimeIdentity` as an
+observable change to the deployment, which is the assurance a `Pinned` deployment can give
+when no digest is available.
+
+## `Invoke`
+
+The request body goes through as the caller supplied it. §8.2 makes the payload opaque to
+the Server, and the OpenAI chat-completions request is exactly the kind of vendor-shaped
+JSON that rule exists to preserve.
+
+| Deployment member | From |
+|---|---|
+| `ApiDialect` | `RestChatCompletions` |
+| `RuntimeIdentity` | `system_fingerprint`, where the response carries one |
+
+The deployment's `ApiDialect` is the same as the source's in the ordinary pass-through
+arrangement. §6.4.2 makes that duplication intentional: the source value tells this Server
+what to send outward, and the deployment value tells an OPC UA client what to put in
+`Payload`.
+
+| Output | From |
+|---|---|
+| `ResponsePayload` | the response body, verbatim |
+| `ResponseContentType` | `application/json` |
+| `ModelUsed` | the `ModelType` NodeId this deployment resolved to — not the response's `model` string |
+| `Usage.UnitKind` | `tokens` |
+| `Usage.InputUnits` | `usage.prompt_tokens` |
+| `Usage.OutputUnits` | `usage.completion_tokens` |
+| `Usage.TotalUnits` | `usage.total_tokens` |
+| `FinishReason` | `choices[0].finish_reason`, mapped below |
+| `SafetyAssessment` | populated when the content filter fired |
+| `RetryAfter` | the `Retry-After` header, where the response carries one |
+
+`FinishReason` maps: `stop` to `Stop`, `length` to `Length`, `tool_calls` to `ToolCall`,
+`content_filter` to `Filtered`. The deprecated `function_call` value is an older form of a
+tool call and should be reported as `ToolCall` if the Server chooses to accept it. There is
+no `Cancelled` value on this contract — a cancelled call is cancelled by the Server, not by
+the endpoint — and `Error` covers a response that arrived but could not be understood.
+
+`ModelUsed` is a NodeId in this Server's address space, not the `model` string the endpoint
+echoed back. A caller can already see the string in the payload. What it cannot otherwise
+find out is *which of the models this Server publishes* answered, and that is the question
+`ModelUsed` exists to settle — see §8.2.1, and the fallback case in §9.4 where the two differ.
+
+## Asynchronous inference
+
+OpenAI has a native batch API: `POST /v1/batches` creates a batch job from a pre-uploaded
+JSONL file, returns a batch `id`, runs against endpoints including `/v1/chat/completions`,
+and uses a fixed `24h` completion window. Results are retrieved through the Files API by
+the returned output file id.
+
+That gives `InvokeAsync` something native to map onto.
+
+| Member | From |
+|---|---|
+| `JobId` | the OpenAI batch `id` |
+| `RequestUri` | the batch `input_file_id` |
+| `ResponseUri` | the output file id whose content the Files API returns |
+
+The OPC UA job follows the Part 10 program lifecycle required by §8.6 while the Server
+polls or observes the OpenAI batch status. OpenAI statuses such as `validating`,
+`in_progress`, `finalizing`, `completed`, `failed`, `expired`, `cancelling` and
+`cancelled` are endpoint details behind that lifecycle, not new OPC UA states.
+
+The Server still owns the OPC UA job object. It is the place where the client reads the
+state, result reference and failure reason, even though the work is being performed by the
+OpenAI batch service.
+
+## Large payloads
+
+OpenAI has a Files API: `POST /v1/files` uploads a file and returns a `file_id`. The batch
+API uses such a file as its input, and other OpenAI features can refer to files by id.
+
+That is a by-reference payload, not a chunked transfer. A client that wants the Server to
+submit an already uploaded file names the `file_id` through `PayloadUri`; the corresponding
+`InferenceJobType.RequestUri` records the batch `input_file_id`, and `ResponseUri` records
+the returned output file id.
+
+| Member | From |
+|---|---|
+| `PayloadUri` | the `file_id` named by the request |
+| `RequestUri` | the batch `input_file_id` actually submitted |
+| `ResponseUri` | the returned output file id whose content the Files API serves |
+
+`BeginTransfer` and `InferenceTransferType` are the Server's own, over Part 5 `FileType` as
+§8.2 defines, for a payload too large to carry through the OPC UA call. §8.6.1 separates
+that case from data that already lives elsewhere. A `PayloadUri`, `RequestUri` or
+`ResponseUri` is untrusted input under §12.2, and it is also an egress decision under §9.5:
+a deployment whose `EgressPermitted` is false **shall not** accept a `PayloadUri` naming
+somewhere outside the operator's boundary.
+
+## The catalogue
+
+`GET /v1/models` is a useful source for `ListModels` on the `ModelSourceType`: it answers
+which model ids the endpoint exposes, and it returns the `id`, `object`, `created` and
+`owned_by` fields that the identity mapping above draws on.
+
+It is not a catalogue in the §10 sense. It has no content-addressed artefact, no digest, no
+resource that can be staged and verified, and no model card or provenance document that the
+API exposes as a structured registry resource.
+
+If you want §10 with real digests, the source has to be a registry that is
+content-addressed. An OpenAI model id can be federated as an externally hosted deployment
+under clause 9, but it cannot by itself satisfy `AI-Catalogue` or `AI-Import`.
+
+## Residency, egress and retention
+
+Nothing in the API response states residency, egress or retention. All three are operator
+assertions.
+
+| Member | OpenAI platform API |
+|---|---|
+| `InferenceLocation` | `Cloud` |
+| `EgressPermitted` | `true` |
+| `DataJurisdiction` | the jurisdiction the operator has contracted for |
+| `RetainsInput` | asserted from the operator's retention arrangement |
+| `EgressPolicyUri` | your policy document |
+
+`EgressPermitted` is `true` because the request leaves the Server for a cloud service. §9.5
+makes the point and it is worth repeating because it is the mistake people make: TLS
+answers who can read the payload in flight, not whether it left the site.
+
+Retention is likewise not a response field. The OpenAI request has a `store` field that
+controls whether a completion is stored for distillation or evals, and separate policy
+arrangements such as Zero Data Retention can apply, but the response does not confirm the
+plant-level retention answer. The Server has to publish the operator's assertion in
+`RetainsInput`.
+
+## What this system does not tell you
+
+- **Which weights answered.** No digest, anywhere, on any call. `Digest` and
+ `DigestAlgorithm` stay empty, and `DigestProvenance` is `NotAvailable` under
+ §12.1.1. Do not hash the model name, and do not use `system_fingerprint` as a digest.
+- **What `system_fingerprint` means for provenance.** It is a backend configuration
+ fingerprint. It belongs in `RuntimeIdentity`, where §9.3.1 makes changes observable on
+ the deployment, but it is not a model artefact identity.
+- **What the model was trained on.** Nothing maps to `TrainedOn` or `DatasetType`. If
+ lineage matters, it comes from documentation or the supplier, by hand.
+- **Whether the model behind an unpinned name changed.** A `Pinned` deployment can bind to a
+ dated model id, but the pin is a name backed by provider policy, not a digest-backed
+ artefact.
+- **Where your data went, or whether it was kept.** Both are contract terms, recorded as
+ operator assertions in `DataJurisdiction`, `EgressPermitted` and `RetainsInput`.
+
+## Conformance units
+
+This arrangement is an **AI Inference Gateway Server**: it reaches the
+**AI-Base**, **AI-Invoke**, **AI-OffServer**, **AI-Federation** and
+**AI-Residency** facets that §13.3 bundles for a hosted inference Server.
+
+Reachable against the OpenAI platform API: **AI-Base**, **AI-Invoke**,
+**AI-InvokeAsync**, **AI-Transfer**, **AI-OffServer**, **AI-Federation**,
+**AI-Residency**.
+
+Out of reach without something else: **AI-Catalogue** and **AI-Import** need a
+content-addressed registry; **AI-Signatures** needs tensor shapes this contract does not
+carry; **AI-Learning** needs training, which is not what this is.
+
+## Sources
+
+- [OpenAI chat completions API](https://developers.openai.com/api/reference/resources/chat)
+- [OpenAI models list API](https://developers.openai.com/api/reference/resources/models/methods/list)
+- [OpenAI API overview](https://developers.openai.com/api/reference/overview)
+- [OpenAI batches API](https://developers.openai.com/api/reference/resources/batches)
+- [OpenAI workload identity federation guide](https://developers.openai.com/api/docs/guides/workload-identity-federation)
diff --git a/metaverse-specs/extras/ai-model-management/examples/tools/known-terms.txt b/metaverse-specs/extras/ai-model-management/examples/tools/known-terms.txt
new file mode 100644
index 0000000..6b17ad8
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/tools/known-terms.txt
@@ -0,0 +1,43 @@
+# Backticked PascalCase tokens the guides cite that this model does not declare.
+#
+# validate_examples.py reports any backticked PascalCase token that is neither declared by
+# Opc.Ua.AiModelManagement.NodeSet2.xml nor listed here. The list exists so that a guide
+# can name `InvokeModel` without that becoming a hole wide enough for a misspelt
+# `EgressPermited` to pass through unnoticed.
+#
+# Adding a term is a deliberate act and shows up in a diff. Before adding one, check it is
+# genuinely someone else's name and not a member of this model spelled wrongly.
+#
+# One term per line.
+
+# --- Defined by the base OPC UA specification -------------------------------
+# Cited legitimately: this model builds on them and the guides have to name them.
+ByteString
+FileType
+LocalizedText
+MessageSecurityMode
+SignAndEncrypt
+NodeId
+SecurityMode
+
+# --- Amazon Web Services ----------------------------------------------------
+Accept
+CreateModelInvocationJob
+DescribeEndpoint
+EndpointName
+InferenceId
+InvokeEndpoint
+InvokeEndpointAsync
+InvokeModel
+ListEndpoints
+ListFoundationModels
+OutputLocation
+
+# --- Amazon SageMaker -------------------------------------------------------
+InputLocation
+
+# --- Google Cloud -----------------------------------------------------------
+GenerateContentResponse
+
+# --- NVIDIA -----------------------------------------------------------------
+ModelInfer
diff --git a/metaverse-specs/extras/ai-model-management/examples/tools/validate_examples.py b/metaverse-specs/extras/ai-model-management/examples/tools/validate_examples.py
new file mode 100644
index 0000000..be677d7
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/examples/tools/validate_examples.py
@@ -0,0 +1,338 @@
+#!/usr/bin/env python3
+"""
+Validator for the vendor implementation guides beside this file.
+
+ python metaverse-specs/extras/ai-model-management/examples/tools/validate_examples.py
+
+The guides map real systems onto the information model, which means every one of them
+cites member names, enumeration literals and conformance units. Those citations are the
+part that rots: a member renamed in the model leaves a guide quietly describing something
+that no longer exists, and nothing about a markdown file fails when it does.
+
+Everything here is re-derived from Opc.Ua.AiModelManagement.NodeSet2.xml and from the
+specification. Nothing is read from the guides and compared against a second copy of
+itself, because a checker that asks the guides what the guides say validates nothing.
+
+Checks:
+
+ * Every model identifier a guide cites in backticks exists in the NodeSet. A backticked
+ PascalCase token that is neither a NodeSet name nor a listed vendor term is a typo or
+ a renamed member, and both are reported. known-terms.txt is what lets a guide say
+ `InvokeModel` without that becoming a hole big enough for `EgressPermited` to fit
+ through - adding a term is a deliberate act, visible in a diff.
+ * Every ApiDialectEnum and AuthenticationKindEnum literal is exercised by at least one
+ guide. The index claims the set is complete; this is that claim, checked. A dialect
+ nobody can show an example of is a literal worth reconsidering.
+ * Every conformance unit a guide names exists in the specification.
+ * Every guide cites the specification by relative path. This is load-bearing beyond
+ navigation: check_section_refs.py resolves a bare section reference against any
+ document cited by relative path, metaverse-specs/ is a strict tree, and a guide
+ without the link fails that check for every section reference it makes.
+ * The index lists every guide, every guide is listed by the index, and the dialect the
+ index attributes to a guide is one that guide actually assigns.
+
+Exit code 0 when everything holds, 1 otherwise.
+"""
+
+import os
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+EXAMPLES = os.path.dirname(HERE)
+EXTRAS = os.path.dirname(EXAMPLES)
+STD = os.path.normpath(os.path.join(EXTRAS, "..", "..", "ai-model-management"))
+
+NODESET = os.path.join(STD, "Opc.Ua.AiModelManagement.NodeSet2.xml")
+SPEC = os.path.join(STD, "OPC-UA-AI-Model-Management.md")
+INDEX = os.path.join(EXAMPLES, "index.md")
+KNOWN_TERMS = os.path.join(HERE, "known-terms.txt")
+
+UA = "{http://opcfoundation.org/UA/2011/03/UANodeSet.xsd}"
+UAX = "{http://opcfoundation.org/UA/2008/02/Types.xsd}"
+
+ERRORS: list[str] = []
+
+
+def err(msg: str) -> None:
+ ERRORS.append(msg)
+
+
+def load_nodeset_names() -> set[str]:
+ """Every BrowseName, enumeration literal, structure field and Method argument.
+
+ Method argument names live in the Value of an InputArguments or OutputArguments
+ Variable rather than in a BrowseName, so a guide naming `PayloadUri` is naming
+ something the model genuinely declares. Reading them here keeps the check honest
+ in both directions: a guide may cite an argument, and a typo in one still fails.
+ """
+ root = ET.parse(NODESET).getroot()
+ names: set[str] = set()
+
+ for node in root:
+ browse = node.get("BrowseName")
+ if browse:
+ names.add(browse.split(":", 1)[-1])
+
+ definition = node.find(f"{UA}Definition")
+ if definition is not None:
+ for field in definition.findall(f"{UA}Field"):
+ field_name = field.get("Name")
+ if field_name:
+ names.add(field_name)
+
+ if browse and browse.split(":", 1)[-1] in ("InputArguments", "OutputArguments"):
+ for arg_name in node.iter(f"{UAX}Name"):
+ if arg_name.text:
+ names.add(arg_name.text)
+
+ if not names:
+ err("the NodeSet yielded no names; the parse is wrong, not the guides")
+
+ return names
+
+
+def load_enum_literals(name: str) -> list[str]:
+ """The literals of one enumeration, in declaration order."""
+ root = ET.parse(NODESET).getroot()
+
+ for node in root.findall(f"{UA}UADataType"):
+ if (node.get("BrowseName") or "").split(":", 1)[-1] != name:
+ continue
+ definition = node.find(f"{UA}Definition")
+ if definition is None:
+ break
+ return [f.get("Name") or "" for f in definition.findall(f"{UA}Field")]
+
+ err(f"{name} is not an enumeration in the NodeSet")
+ return []
+
+
+def load_conformance_units() -> set[str]:
+ """Every conformance unit the specification declares."""
+ with open(SPEC, encoding="utf-8") as handle:
+ text = handle.read()
+ return set(re.findall(r"\*\*(AI-[A-Za-z]+)\*\*", text))
+
+
+def load_profiles() -> set[str]:
+ """Every profile the specification's profile table declares.
+
+ Read from the leading column of the clause 13.3 table rather than from prose, so a
+ profile a guide claims is checked against the one place that defines it. A guide that
+ names a Server shape the specification does not define is claiming something no test
+ lab can assess, which is the same failure a misspelled facet would be.
+ """
+ with open(SPEC, encoding="utf-8") as handle:
+ text = handle.read()
+ return set(re.findall(r"^\|\s*\*\*((?:AI|Robot) [^*|]*Server)\*\*\s*\|", text, re.M))
+
+
+def check_profiles(text_by_guide: dict[str, str]) -> None:
+ """A guide may name a profile only if the specification defines it.
+
+ This checks the NAME, not the claim. Whether a guide's reachable facets actually
+ cover the profile it names is a judgement over prose, and a checker that guessed at
+ it would fail on wording rather than on substance. What this does catch is the
+ failure that matters mechanically: a profile that does not exist, whether from a
+ typo or from an author inventing a Server shape the specification never defined.
+ """
+ declared = load_profiles()
+ if not declared:
+ err("no profiles parsed from the specification; the parse is wrong, not the guides")
+ return
+
+ for rel, text in text_by_guide.items():
+ for claimed in re.findall(r"\*\*((?:AI|Robot) [^*]*Server)\*\*", strip_fences(text)):
+ if claimed not in declared:
+ err(f"{rel}: `{claimed}` is not a profile the specification defines")
+
+
+def load_known_terms() -> set[str]:
+ """Backticked PascalCase tokens the guides may cite that this model does not declare."""
+ if not os.path.exists(KNOWN_TERMS):
+ err("known-terms.txt is missing; every vendor identifier would be reported")
+ return set()
+
+ terms: set[str] = set()
+ with open(KNOWN_TERMS, encoding="utf-8") as handle:
+ for line in handle:
+ line = line.split("#", 1)[0].strip()
+ if line:
+ terms.add(line)
+ return terms
+
+
+def guides() -> list[str]:
+ """Every guide beside the index."""
+ found = []
+ for name in sorted(os.listdir(EXAMPLES)):
+ if name.endswith(".md") and name != "index.md":
+ found.append(os.path.join(EXAMPLES, name))
+ return found
+
+
+# A backticked token. Fenced blocks are stripped first so that sample payloads do not
+# masquerade as citations.
+BACKTICKED = re.compile(r"`([^`\n]+)`")
+
+# The shape of a model identifier: PascalCase with at least one lower-case letter, so
+# that `AI-Base`, `application/json`, `stop` and `POST /v1/models` are not candidates.
+IDENTIFIER = re.compile(r"^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$")
+
+
+def strip_fences(text: str) -> str:
+ out, fenced = [], False
+ for line in text.splitlines():
+ if line.lstrip().startswith("```"):
+ fenced = not fenced
+ continue
+ if not fenced:
+ out.append(line)
+ return "\n".join(out)
+
+
+def citations(text: str) -> set[str]:
+ """Backticked tokens shaped like a model identifier.
+
+ A two-part dotted citation - `RateLimit.RetryAfter`, `Usage.InputUnits` - is checked
+ part by part, because that form is how a guide names a field of a structure and a typo
+ in the second half is exactly as wrong as one in the first. Both halves have to look
+ like identifiers for the token to be read that way, which is what keeps a package name
+ such as `Microsoft.AI.Foundry.Local` from being mistaken for one.
+ """
+ found = set()
+ for token in BACKTICKED.findall(strip_fences(text)):
+ token = token.strip()
+
+ if IDENTIFIER.match(token):
+ found.add(token)
+ continue
+
+ parts = token.split(".")
+ if len(parts) == 2 and all(IDENTIFIER.match(p) for p in parts):
+ found.update(parts)
+
+ return found
+
+
+def check_identifiers(names: set[str], known: set[str]) -> None:
+ for path in guides() + [INDEX]:
+ rel = os.path.basename(path)
+ with open(path, encoding="utf-8") as handle:
+ text = handle.read()
+
+ for token in sorted(citations(text)):
+ if token not in names and token not in known:
+ err(
+ f"{rel}: `{token}` is neither declared by this model nor "
+ f"listed in known-terms.txt"
+ )
+
+
+def check_enum_coverage(text_by_guide: dict[str, str]) -> None:
+ for enum in ("ApiDialectEnum", "AuthenticationKindEnum"):
+ for literal in load_enum_literals(enum):
+ if not literal:
+ continue
+ if not any(f"`{literal}`" in text for text in text_by_guide.values()):
+ err(
+ f"no guide exercises {enum}.{literal}; the index claims every literal "
+ f"is covered"
+ )
+
+
+def check_conformance_units(text_by_guide: dict[str, str]) -> None:
+ declared = load_conformance_units()
+
+ for rel, text in text_by_guide.items():
+ for unit in sorted(set(re.findall(r"\*\*(AI-[A-Za-z]+)\*\*", text))):
+ if unit not in declared:
+ err(f"{rel}: **{unit}** is not a conformance unit the specification declares")
+
+
+def check_spec_link(text_by_guide: dict[str, str]) -> None:
+ for rel, text in text_by_guide.items():
+ if "ai-model-management/OPC-UA-AI-Model-Management.md" not in text:
+ err(
+ f"{rel}: does not cite the specification by relative path, so every "
+ f"section reference in it resolves against nothing"
+ )
+
+
+def check_index(text_by_guide: dict[str, str]) -> None:
+ with open(INDEX, encoding="utf-8") as handle:
+ index_text = handle.read()
+
+ linked = set(re.findall(r"\]\((?!http)([a-z0-9-]+\.md)\)", index_text))
+ present = set(os.path.basename(p) for p in guides())
+
+ for missing in sorted(present - linked):
+ err(f"index.md: does not link {missing}")
+ for dangling in sorted(linked - present):
+ err(f"index.md: links {dangling}, which does not exist")
+
+ # The dialect the index attributes to a guide has to be one that guide assigns.
+ for line in index_text.splitlines():
+ if not line.startswith("| ["):
+ continue
+ cells = [c.strip() for c in line.strip("|").split("|")]
+ if len(cells) < 3:
+ continue
+ match = re.search(r"\]\(([a-z0-9-]+\.md)\)", cells[0])
+ if not match:
+ continue
+ guide = match.group(1)
+ text = text_by_guide.get(guide)
+ if text is None:
+ continue
+ for literal in re.findall(r"`([A-Za-z]+)`", cells[2]):
+ if f"`{literal}`" not in text:
+ err(
+ f"index.md: attributes `{literal}` to {guide}, which does not "
+ f"mention it"
+ )
+
+
+def main() -> int:
+ for required in (NODESET, SPEC, INDEX):
+ if not os.path.exists(required):
+ print(f"FAIL: {required} is missing")
+ return 1
+
+ found = guides()
+ if not found:
+ print("FAIL: no guides found beside the index")
+ return 1
+
+ text_by_guide = {}
+ for path in found:
+ with open(path, encoding="utf-8") as handle:
+ text_by_guide[os.path.basename(path)] = handle.read()
+
+ names = load_nodeset_names()
+ known = load_known_terms()
+
+ check_identifiers(names, known)
+ check_enum_coverage(text_by_guide)
+ check_conformance_units(text_by_guide)
+ check_profiles(text_by_guide)
+ check_spec_link(text_by_guide)
+ check_index(text_by_guide)
+
+ if ERRORS:
+ print(f"FAIL: {len(ERRORS)} problem(s) in the vendor guides")
+ for problem in ERRORS:
+ print(f" - {problem}")
+ return 1
+
+ print(
+ f"OK: {len(found)} vendor guide(s) agree with "
+ f"Opc.Ua.AiModelManagement.NodeSet2.xml"
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/metaverse-specs/extras/ai-model-management/tools/build_model.py b/metaverse-specs/extras/ai-model-management/tools/build_model.py
new file mode 100644
index 0000000..8285c58
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/tools/build_model.py
@@ -0,0 +1,1897 @@
+#!/usr/bin/env python3
+"""
+Generator for the OPC UA - AI Model Management and Inference companion specification.
+
+Emits, from a single in-code source of truth:
+ * ../../../ai-model-management/Opc.Ua.AiModelManagement.NodeSet2.xml - the model
+ * ../../../ai-model-management/Opc.Ua.AiModelManagement.NodeIds.csv - the NodeIds
+ * model-reference.md - the generated Annex A
+
+The model is a COMPANION specification in its OWN namespace
+(http://opcfoundation.org/UA/AI/). Its namespace index is DERIVED from NAMESPACE_URIS
+and is not 1: the xRegistry RequiredModel occupies index 1. Nodes use ns={OWN_NS} for
+both NodeIds and BrowseNames; references to base UA types use plain `i=`.
+
+It is deliberately STANDALONE and deliberately DOMAIN-NEUTRAL: the only
+ is the base UA namespace, and nothing here mentions a camera, a robot
+or any other kind of equipment. A model is trained on a dataset, deployed somewhere,
+and replaced by a better one; that story is the same whether the input is an image, a
+vibration spectrum or a process trace.
+
+NodeIds are PROVISIONAL (final IDs assigned by the OPC Foundation) and follow the repo
+convention: ObjectTypes/Interfaces 1001+, Enumerations 3001+ (EnumStrings = enum + 900),
+Structures 3050+, ReferenceTypes 4001+, DataType encodings 5001+, well-known instances
+7001+, and all remaining instance declarations sequentially from 6001. New members must
+be APPENDED so that previously published member NodeIds stay stable.
+
+Design notes:
+ * The model is domain-neutral by construction: nothing here names a camera, a robot
+ or a sensor, and validate_local.py fails the build if a type name acquires one.
+ * A consuming specification binds to this one through a NodeId Property, not a
+ reference and not a RequiredModel, so a Server can implement either alone.
+ * Digest and DigestAlgorithm are Mandatory because the provenance chain from a
+ published result back to the model artefact is the only reason several of the
+ other members are worth reading at all.
+"""
+from __future__ import annotations
+import os
+import re
+import xml.sax.saxutils as sx
+
+NAMESPACE = "http://opcfoundation.org/UA/AI/"
+VERSION = "0.4.0"
+PUBDATE = "2026-08-06T00:00:00Z"
+BASE_UA_VERSION = "1.05.04"
+BASE_UA_PUBDATE = "2023-12-15T00:00:00Z"
+
+# The model catalogue is a domain extension of OPC UA - xRegistry, so that a model
+# registry is the same shape as every other registry in this repository rather than a
+# private invention. See clause 10.
+XREG_NS = "http://opcfoundation.org/UA/xRegistry/"
+XREG_VERSION = "0.4.0"
+XREG_PUBDATE = "2026-07-31T00:00:00Z"
+
+# NamespaceUris order fixes the namespace indices for the whole file. Required-model
+# namespaces come first and the own namespace last, matching the Schema Registry
+# precedent. Both indices are DERIVED from this list - a hardcoded ns=N would not merely
+# go stale when a dependency is added, it would start pointing into a different model.
+NAMESPACE_URIS = [XREG_NS, NAMESPACE]
+OWN_NS = NAMESPACE_URIS.index(NAMESPACE) + 1
+XREG_IDX = NAMESPACE_URIS.index(XREG_NS) + 1
+
+# --- base UA NodeIds (namespace 0) -----------------------------------------
+HasComponent = "i=47"
+HasProperty = "i=46"
+HasSubtype = "i=45"
+Organizes = "i=35"
+HasTypeDefinition = "i=40"
+HasModellingRule = "i=37"
+HasInterface = "i=17603"
+HasEncoding = "i=38"
+
+MR_Mandatory = "i=78"
+MR_Optional = "i=80"
+MR_OptionalPlaceholder = "i=11508"
+MR_MandatoryPlaceholder = "i=11510"
+
+BaseObjectType = "i=58"
+FolderType = "i=61"
+PropertyType = "i=68"
+BaseDataVariableType = "i=63"
+BaseInterfaceType = "i=17602"
+DataTypeEncodingType = "i=76"
+Enumeration = "i=29"
+Structure = "i=22"
+NonHierarchicalReferences = "i=32"
+
+BaseDataType = "i=24"
+Boolean = "i=1"
+Int32 = "i=6"
+UInt32 = "i=7"
+UInt64 = "i=9"
+Double = "i=11"
+String = "i=12"
+Guid = "i=14"
+ByteString = "i=15"
+NodeId_ = "i=17"
+QualifiedName = "i=20"
+LocalizedText = "i=21"
+UtcTime = "i=294"
+Duration = "i=290"
+Argument = "i=296"
+EUInformation = "i=887"
+KeyValuePair = "i=14533"
+
+Server = "i=2253"
+
+# OPC 10000-10 Programs. A long-running AI job is a program instance, which is how
+# Robot Intent models its intents; the transition events and the auditability that a
+# hand-rolled state variable would have to reinvent come with the base type.
+ProgramStateMachineType = "i=2391"
+
+# OPC 10000-5 FileType. A large inference payload is a file in every respect that
+# matters - it is opened, written or read in bounded chunks, and closed.
+FileType = "i=11575"
+
+Float = "i=10"
+Int64 = "i=8"
+DateTime = "i=13"
+
+ALIASES = [
+ ("Boolean", Boolean), ("Int32", Int32), ("UInt32", UInt32), ("UInt64", UInt64),
+ ("Double", Double), ("String", String), ("Guid", Guid), ("ByteString", ByteString),
+ ("Float", Float), ("Int64", Int64), ("DateTime", DateTime),
+ ("NodeId", NodeId_), ("QualifiedName", QualifiedName), ("LocalizedText", LocalizedText),
+ ("UtcTime", UtcTime), ("Duration", Duration), ("Argument", Argument),
+ ("EUInformation", EUInformation), ("KeyValuePair", KeyValuePair),
+ ("BaseDataType", BaseDataType),
+ ("HasComponent", HasComponent), ("HasProperty", HasProperty),
+ ("HasSubtype", HasSubtype), ("Organizes", Organizes),
+ ("HasTypeDefinition", HasTypeDefinition), ("HasModellingRule", HasModellingRule),
+ ("HasInterface", HasInterface), ("HasEncoding", HasEncoding),
+ ("Mandatory", MR_Mandatory), ("Optional", MR_Optional),
+ ("OptionalPlaceholder", MR_OptionalPlaceholder),
+ ("MandatoryPlaceholder", MR_MandatoryPlaceholder),
+]
+
+REFTYPE_ALIAS = {v: k for k, v in ALIASES}
+DATATYPE_ALIAS = {v: k for k, v in ALIASES}
+
+
+# --- node registry ---------------------------------------------------------
+class Node:
+ __slots__ = ("nid", "cls", "bname", "symbolic", "display", "desc", "parent",
+ "attrs", "refs", "category", "definition", "value", "abstract",
+ "inverse")
+
+ def __init__(self, nid, cls, bname, symbolic, display, desc, parent, attrs,
+ category, abstract):
+ self.nid = nid
+ self.cls = cls
+ self.bname = bname
+ self.symbolic = symbolic
+ self.display = display or bname
+ self.desc = desc
+ self.parent = parent
+ self.attrs = attrs or {}
+ self.refs = []
+ self.category = category
+ self.definition = None
+ self.value = None
+ self.abstract = abstract
+ self.inverse = None
+
+
+NODES = {}
+ORDER = []
+_next_member = [6001]
+_next_encoding = [5001]
+
+
+def _mid():
+ v = _next_member[0]
+ _next_member[0] += 1
+ return v
+
+
+def T(nid):
+ """Own-namespace NodeId. The index is derived, never assumed."""
+ return f"ns={OWN_NS};i={nid}"
+
+
+def X(nid):
+ """A NodeId in the xRegistry namespace this model extends."""
+ return f"ns={XREG_IDX};i={nid}"
+
+
+def add(nid, cls, bname, symbolic, display=None, desc=None, parent=None,
+ attrs=None, category=None, abstract=False):
+ n = Node(nid, cls, bname, symbolic, display, desc, parent, attrs, category, abstract)
+ NODES[nid] = n
+ ORDER.append(nid)
+ return n
+
+
+def ref(nid, reftype, target, forward=True):
+ NODES[nid].refs.append((reftype, target, forward))
+
+
+# --- builders --------------------------------------------------------------
+def object_type(nid, name, base, desc, abstract=False):
+ add(nid, "UAObjectType", name, name, desc=desc, category=CAT, abstract=abstract)
+ ref(nid, HasSubtype, base, forward=False)
+ return nid
+
+
+def interface_type(nid, name, base, desc):
+ add(nid, "UAObjectType", name, name, desc=desc, category=CAT, abstract=True)
+ ref(nid, HasSubtype, base, forward=False)
+ return nid
+
+
+def reference_type(nid, name, inverse, desc, base=NonHierarchicalReferences):
+ n = add(nid, "UAReferenceType", name, name, desc=desc, category=CAT_RT)
+ n.inverse = inverse
+ ref(nid, HasSubtype, base, forward=False)
+ return nid
+
+
+def _member_var(owner, owner_sym, name, datatype, typedef, rule, reftype, desc,
+ valuerank="-1"):
+ nid = _mid()
+ attrs = {"DataType": datatype, "ValueRank": valuerank}
+ add(nid, "UAVariable", name, f"{owner_sym}_{name.strip('<>')}", desc=desc,
+ parent=T(owner), attrs=attrs)
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasTypeDefinition, typedef)
+ ref(nid, reftype, T(owner), forward=False)
+ ref(owner, reftype, T(nid))
+ return nid
+
+
+def prop_var(owner, owner_sym, name, datatype, desc, rule=MR_Optional, valuerank="-1"):
+ return _member_var(owner, owner_sym, name, datatype, PropertyType, rule,
+ HasProperty, desc, valuerank)
+
+
+def data_var(owner, owner_sym, name, datatype, desc, rule=MR_Optional, valuerank="-1"):
+ return _member_var(owner, owner_sym, name, datatype, BaseDataVariableType, rule,
+ HasComponent, desc, valuerank)
+
+
+def folder_member(owner, owner_sym, name, desc, rule=MR_Mandatory):
+ nid = _mid()
+ add(nid, "UAObject", name, f"{owner_sym}_{name}", desc=desc, parent=T(owner))
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasTypeDefinition, FolderType)
+ ref(nid, HasComponent, T(owner), forward=False)
+ ref(owner, HasComponent, T(nid))
+ return nid
+
+
+def obj_member(owner, owner_sym, name, typedef, desc, rule=MR_Optional,
+ reftype=HasComponent):
+ nid = _mid()
+ add(nid, "UAObject", name, f"{owner_sym}_{name.strip('<>')}", desc=desc,
+ parent=T(owner))
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasTypeDefinition, typedef)
+ ref(nid, reftype, T(owner), forward=False)
+ ref(owner, reftype, T(nid))
+ return nid
+
+
+def _args(method_nid, method_sym, bname, args):
+ """Emit an InputArguments / OutputArguments Property for a Method."""
+ nid = _mid()
+ add(nid, "UAVariable", bname, f"{method_sym}_{bname}", parent=T(method_nid),
+ attrs={"DataType": Argument, "ValueRank": "1",
+ "ArrayDimensions": str(len(args))})
+ ref(nid, HasModellingRule, MR_Mandatory)
+ ref(nid, HasTypeDefinition, PropertyType)
+ ref(nid, HasProperty, T(method_nid), forward=False)
+ ref(method_nid, HasProperty, T(nid))
+ parts = ['',
+ '']
+ for arg in args:
+ aname, adtype, adesc = arg[0], arg[1], arg[2]
+ arank = arg[3] if len(arg) > 3 else -1
+ parts.append("i=297"
+ "")
+ parts.append("")
+ parts.append(f"{sx.escape(aname)}")
+ parts.append(f"{adtype}"
+ "")
+ if arank is not None and arank >= 0:
+ parts.append(f"{arank}"
+ "0"
+ "")
+ else:
+ parts.append("-1"
+ "")
+ if adesc:
+ parts.append(""
+ f"{sx.escape(adesc)}")
+ parts.append("")
+ parts.append("")
+ NODES[nid].value = "".join(parts)
+ return nid
+
+
+def method(owner, owner_sym, name, desc, rule=MR_Optional, inargs=None, outargs=None):
+ nid = _mid()
+ add(nid, "UAMethod", name, f"{owner_sym}_{name}", desc=desc, parent=T(owner))
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasComponent, T(owner), forward=False)
+ ref(owner, HasComponent, T(nid))
+ if inargs:
+ _args(nid, f"{owner_sym}_{name}", "InputArguments", inargs)
+ if outargs:
+ _args(nid, f"{owner_sym}_{name}", "OutputArguments", outargs)
+ return nid
+
+
+def enum_type(nid, name, desc, fields):
+ add(nid, "UADataType", name, name, desc=desc, category=CAT_DT)
+ ref(nid, HasSubtype, Enumeration, forward=False)
+ dparts = [f'']
+ for (fname, val, fdesc) in fields:
+ if fdesc:
+ dparts.append(f'')
+ dparts.append(f'{sx.escape(fdesc)}')
+ else:
+ dparts.append(f'')
+ dparts.append("")
+ NODES[nid].definition = "".join(dparts)
+ es = nid + 900
+ ref(nid, HasProperty, T(es))
+ add(es, "UAVariable", "EnumStrings", f"{name}_EnumStrings", parent=T(nid),
+ attrs={"DataType": LocalizedText, "ValueRank": "1",
+ "ArrayDimensions": str(len(fields))})
+ ref(es, HasModellingRule, MR_Mandatory)
+ ref(es, HasTypeDefinition, PropertyType)
+ ref(es, HasProperty, T(nid), forward=False)
+ vp = ['',
+ '']
+ for (fname, _val, _fdesc) in fields:
+ vp.append(""
+ f"{sx.escape(fname)}")
+ vp.append("")
+ NODES[es].value = "".join(vp)
+ return nid
+
+
+ABSTRACT_STRUCTS = set()
+
+
+def struct_type(nid, name, desc, fields, base=Structure, abstract=False):
+ """A Structure DataType plus, unless it is abstract, its Default Binary encoding.
+
+ fields: list of (FieldName, DataType, Description[, ValueRank[, ArrayDimensions]])
+ ArrayDimensions 0 means "any length" and is omitted from the emitted
+ Definition, matching how the base UA NodeSet writes unbounded arrays.
+ base: the DataType this one extends. A subtype's Definition lists only the
+ fields it ADDS - the inherited ones are reached through HasSubtype, which
+ is how the base UA NodeSet does it.
+ abstract: an abstract structure gets no encoding, because nothing is ever encoded
+ as one; values are always of a concrete subtype.
+
+ A field whose DataType is an abstract structure declared here is emitted with
+ AllowSubTypes="true", so polymorphic members are self-describing rather than
+ relying on the reader to notice the DataType is abstract.
+ """
+ add(nid, "UADataType", name, name, desc=desc, category=CAT_DT, abstract=abstract)
+ ref(nid, HasSubtype, base, forward=False)
+ if abstract:
+ ABSTRACT_STRUCTS.add(T(nid))
+ dparts = [f'']
+ for f in fields:
+ fname, fdtype, fdesc = f[0], f[1], f[2]
+ frank = f[3] if len(f) > 3 else None
+ fdims = f[4] if len(f) > 4 else None
+ a = [f'Name="{sx.escape(fname)}"', f'DataType="{fdtype}"']
+ if frank is not None:
+ a.append(f'ValueRank="{frank}"')
+ if fdims:
+ a.append(f'ArrayDimensions="{fdims}"')
+ if fdtype in ABSTRACT_STRUCTS:
+ a.append('AllowSubTypes="true"')
+ attr = " ".join(a)
+ if fdesc:
+ dparts.append(f'')
+ dparts.append(f'{sx.escape(fdesc)}')
+ else:
+ dparts.append(f'')
+ dparts.append("")
+ NODES[nid].definition = "".join(dparts)
+ if abstract:
+ return nid
+ enc = _next_encoding[0]
+ _next_encoding[0] += 1
+ ref(nid, HasEncoding, T(enc))
+ add(enc, "UAObject", "Default Binary", f"{name}_Encoding_DefaultBinary",
+ desc="Default Binary encoding of the structure.",
+ attrs={"BrowseNameNamespace": 0, "SymbolicName": "DefaultBinary"})
+ ref(enc, HasTypeDefinition, DataTypeEncodingType)
+ ref(enc, HasEncoding, T(nid), forward=False)
+ return nid
+
+
+def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
+ add(nid, "UAObject", name, name, desc=desc, parent=parent_nodeid)
+ ref(nid, HasTypeDefinition, typedef)
+ ref(nid, reftype, parent_nodeid, forward=False)
+ return nid
+
+
+
+# ===========================================================================
+# ============================== MODEL DEFINITION =========================
+# ===========================================================================
+CAT = "AiModelManagement"
+CAT_DT = "AiModelManagement DataTypes"
+CAT_RT = "AiModelManagement ReferenceTypes"
+
+# ---------------------------------------------------------------------------
+# Enumerations (3001+)
+# ---------------------------------------------------------------------------
+enum_type(3001, "InferenceLocationEnum",
+ "Where inference executes. The result contract is identical in every case; "
+ "this property exists so a client can reason about latency, availability and "
+ "the trust boundary without changing how it reads results.",
+ [("OnServer", 0, "In the OPC UA Server process or on its host."),
+ ("EdgeOffServer", 1, "On a separate edge node reached over the network."),
+ ("Cloud", 2, "In a remote or cloud service."),
+ ("InSimulator", 3, "Inside a simulator that also produces the input.")])
+InferenceLocationEnum = T(3001)
+
+enum_type(3002, "AcceleratorKindEnum",
+ "Compute device executing the model.",
+ [("Cpu", 0, None), ("Gpu", 1, None), ("Npu", 2, None), ("Fpga", 3, None),
+ ("Tpu", 4, None), ("Other", 5, None)])
+AcceleratorKindEnum = T(3002)
+
+enum_type(3003, "DeploymentStateEnum",
+ "Runtime lifecycle state of a deployment.",
+ [("Inactive", 0, "Declared but not serving."),
+ ("Ready", 1, "Able to serve; no work in progress."),
+ ("Active", 2, "Serving at least one request."),
+ ("Degraded", 3, "Serving below configured quality."),
+ ("Faulted", 4, "Unable to serve.")])
+DeploymentStateEnum = T(3003)
+
+enum_type(3004, "DatasetSourceEnum",
+ "Provenance of the samples in a dataset.",
+ [("Real", 0, "Captured from physical equipment."),
+ ("Synthetic", 1, "Generated or rendered by a simulator."),
+ ("Mixed", 2, "Both, for example synthetic pre-training with real "
+ "fine-tuning.")])
+DatasetSourceEnum = T(3004)
+
+enum_type(3005, "LearningJobStateEnum",
+ "State of a dataset-capture, retraining and promotion cycle.",
+ [("Idle", 0, None), ("Collecting", 1, None), ("Labelling", 2, None),
+ ("Training", 3, None), ("Validating", 4, None),
+ ("Ready", 5, "A candidate model is available for promotion."),
+ ("Promoted", 6, None), ("Failed", 7, None)])
+LearningJobStateEnum = T(3005)
+
+enum_type(3006, "FinishReasonEnum",
+ "Why an inference call stopped producing output. A client that treats every "
+ "non-error response as complete will silently accept a truncated one, which "
+ "is why this is Mandatory on a response rather than a diagnostic.",
+ [("Stop", 0, "The model finished normally."),
+ ("Length", 1, "Output was truncated by a length or budget limit. The "
+ "result is incomplete and SHALL NOT be treated as final."),
+ ("ToolCall", 2, "The model requested a tool or function call and is "
+ "waiting for its result."),
+ ("Filtered", 3, "Output was withheld by a safety policy; see the "
+ "SafetyAssessment."),
+ ("Cancelled", 4, "The caller or the Server cancelled the call."),
+ ("Error", 5, "The call failed; the StatusCode carries the reason.")])
+FinishReasonEnum = T(3006)
+
+enum_type(3007, "ApiDialectEnum",
+ "Wire contract a remote inference endpoint speaks. A Server needs this to "
+ "call an endpoint it did not deploy; without it EndpointUri is a string "
+ "nobody can act on. It describes the REMOTE endpoint and never affects how "
+ "an OPC UA client calls this Server.",
+ [("OpcUaInference", 0, "Another OPC UA Server implementing this "
+ "specification's Invoke Method."),
+ ("RestChatCompletions", 1, "The de-facto REST contract for chat and "
+ "embeddings that most serving runtimes expose, "
+ "including ones that run on a single "
+ "workstation."),
+ ("OpenInferenceProtocol", 2, "The Open Inference Protocol (KServe v2) "
+ "predict contract."),
+ ("TensorRemoteProcedure", 3, "A tensor-oriented RPC contract such as those "
+ "used by dedicated inference servers."),
+ ("EmbeddedRuntime", 4, "An in-process runtime reached through a local "
+ "library rather than a network protocol."),
+ ("Proprietary", 5, "A contract this specification does not name. "
+ "EndpointDescriptionUri SHOULD then say where it is "
+ "documented.")])
+ApiDialectEnum = T(3007)
+
+enum_type(3008, "AuthenticationKindEnum",
+ "How the Server authenticates ITSELF to a remote inference endpoint. This is "
+ "not how a client authenticates to this Server, which is the ordinary OPC UA "
+ "Session security and is unaffected.",
+ [("Anonymous", 0, "No credential. Permitted only where the endpoint is "
+ "reachable solely from a trusted network segment."),
+ ("ApiKey", 1, "A shared secret presented as a key."),
+ ("BearerToken", 2, "A token obtained from an authorization service."),
+ ("WorkloadIdentity", 3, "An identity the hosting platform assigns to the "
+ "Server, so no secret is stored at all. Preferred "
+ "where the platform offers it."),
+ ("MutualTls", 4, "Both ends present certificates.")])
+AuthenticationKindEnum = T(3008)
+
+enum_type(3009, "FallbackPolicyEnum",
+ "What the Server does when a deployment cannot serve. This is the question a "
+ "plant asks that no cloud inference API answers, because a cloud API assumes "
+ "the caller can simply wait.",
+ [("Fail", 0, "Report the failure to the caller and produce nothing. The "
+ "safe default: a caller that is told nothing happened can "
+ "decide for itself."),
+ ("HoldLast", 1, "Continue reporting the most recent successful result, "
+ "marked stale. Legitimate only where a stale answer is "
+ "safe, and the caller SHALL be able to see the staleness."),
+ ("FallBackTo", 2, "Route to the deployment named by the FallsBackTo "
+ "reference. The answer comes from a different model and "
+ "the response SHALL say so.")])
+FallbackPolicyEnum = T(3009)
+
+enum_type(3010, "VersionBindingEnum",
+ "Whether a deployment is bound to one immutable model version or follows a "
+ "moving pointer. Stated structurally rather than as an upgrade policy, "
+ "because what a client needs to know is whether the artefact can change "
+ "under it, not what schedule someone intends to change it on.",
+ [("Pinned", 0, "Bound to one immutable version. The artefact behind this "
+ "deployment cannot change without an observable change to "
+ "the deployment."),
+ ("FollowsRef", 1, "Bound to a mutable pointer such as a branch or channel. "
+ "The artefact CAN change without any other change, which "
+ "is why clause 12 requires the resulting promotion to be "
+ "as authorized as an explicit one.")])
+VersionBindingEnum = T(3010)
+
+enum_type(3011, "ImportModeEnum",
+ "Whether an import job brings the model's description or its bytes.",
+ [("Federate", 0, "Materialize the catalogue entry as a ModelType and leave "
+ "the artefact where it is. Nothing is downloaded and "
+ "inference runs at the source."),
+ ("Stage", 1, "Fetch the artefact, verify its Digest, and make it locally "
+ "available so inference can run without the source."),
+ ("Auto", 2, "Federate, then stage if the target deployment's "
+ "InferenceLocation is OnServer or EdgeOffServer - because "
+ "those cannot reach the source at inference time.")])
+ImportModeEnum = T(3011)
+
+enum_type(3012, "SafetySeverityEnum",
+ "Severity of one safety finding. The scale is the convergent industry one; "
+ "what each level means for a given category is the policy's business, not "
+ "this specification's.",
+ [("None", 0, None), ("Low", 1, None), ("Medium", 2, None),
+ ("High", 3, None)])
+SafetySeverityEnum = T(3012)
+
+enum_type(3013, "ReachabilityEnum",
+ "Whether the Server can currently reach a deployment's execution site.",
+ [("Unknown", 0, "Never attempted, or the Server does not probe."),
+ ("Reachable", 1, "The most recent attempt succeeded."),
+ ("Unreachable", 2, "The most recent attempt failed."),
+ ("Throttled", 3, "Reachable, but the endpoint is refusing work for "
+ "capacity reasons. RetryAfter SHOULD be populated.")])
+ReachabilityEnum = T(3013)
+
+enum_type(3014, "TransferStateEnum",
+ "Stage of a chunked inference exchange. A client reads this rather than "
+ "inferring progress from which Methods have succeeded, because a transfer "
+ "that failed mid-write and one that has not started look alike from "
+ "outside.",
+ [("Building", 0, "The request is being written and is not yet complete."),
+ ("Ready", 1, "The request is complete and inference has not started."),
+ ("Executing", 2, "Inference is running."),
+ ("Completed", 3, "The response is readable."),
+ ("Failed", 4, "The exchange failed; LastError carries the reason."),
+ ("Expired", 5, "The Server reclaimed the transfer before it completed.")])
+TransferStateEnum = T(3014)
+
+enum_type(3015, "DigestProvenanceEnum",
+ "Where a Digest came from, or why there is none. Digest is Mandatory so "
+ "that its absence is uniform and browsable rather than indistinguishable "
+ "from a Server that does not implement digests - but 'empty' then carries "
+ "two different meanings, and a client that must decide whether to trust an "
+ "artefact needs them apart. This member is what tells them apart, and it "
+ "does the same job for a digest that IS present: a value the source "
+ "asserted and a value this Server computed over bytes are not the same "
+ "evidence, and only one of them survives a substituted artefact.",
+ [("NotAvailable", 0,
+ "There is no digest and the source does not publish one. Digest is "
+ "empty. This is the honest answer for an endpoint that names models but "
+ "never their content, and it is what most hosted inference APIs "
+ "require."),
+ ("DeclaredBySource", 1,
+ "Digest carries what the source declared. No party this Server can "
+ "speak for has hashed the artefact, so the value is an assertion "
+ "forwarded rather than evidence held."),
+ ("ComputedByServer", 2,
+ "This Server hashed the artefact it holds. The value is evidence, but "
+ "nothing independent agrees with it - a substitution that happened "
+ "before the Server obtained the bytes is not detected."),
+ ("VerifiedOnStage", 3,
+ "This Server hashed the artefact during a staging import (clause 10.4) "
+ "and it matched what the source declared. Two independent parties agree, "
+ "which is the strongest statement this model can carry.")])
+DigestProvenanceEnum = T(3015)
+
+# ---------------------------------------------------------------------------
+# Structured DataTypes (3050+)
+# ---------------------------------------------------------------------------
+struct_type(3050, "TensorSignatureDataType",
+ "Shape and element type of one model input or output tensor. This is what "
+ "lets a client check that what it intends to send matches what the model "
+ "expects, before it sends it.",
+ [("Name", String, "Tensor name as declared by the model."),
+ ("ElementType", String, "Element type, for example float32, uint8 or "
+ "int64."),
+ ("Shape", Int32, "Dimensions; -1 marks a dynamic axis.", 1),
+ ("Layout", String, "Optional axis layout hint, for example NCHW or "
+ "NHWC.")])
+TensorSignatureDataType = T(3050)
+
+struct_type(3051, "ModelReferenceDataType",
+ "Identity of a model as a publisher, name and version triple. Every model "
+ "catalogue in practice identifies a model this way, which is why an import "
+ "job takes this rather than a URL: a URL says where a copy is today, the "
+ "triple says which artefact is meant.",
+ [("Publisher", String, "Organisation or namespace that published the "
+ "model."),
+ ("Name", String, "Model name within that publisher."),
+ ("Version", String, "Immutable version identifier, or a mutable pointer "
+ "such as a branch or channel name. Which one it is "
+ "is stated by VersionBinding, not guessable from the "
+ "string.")])
+ModelReferenceDataType = T(3051)
+
+struct_type(3052, "UsageDataType",
+ "What one inference call consumed. Deliberately NOT named in tokens: a "
+ "token is one accounting unit among several, and a model that consumes "
+ "images, samples or seconds of audio needs the same accounting. UnitKind "
+ "says which unit the counts are in.",
+ [("UnitKind", String, "Unit the counts are expressed in, for example "
+ "'tokens', 'images', 'samples' or 'seconds'."),
+ ("InputUnits", UInt64, "Units consumed by the input."),
+ ("OutputUnits", UInt64, "Units produced as output."),
+ ("TotalUnits", UInt64, "Total units billed or metered for the call, "
+ "which is not always the sum: cached or "
+ "deduplicated input may be counted once.")])
+UsageDataType = T(3052)
+
+struct_type(3053, "CapabilityDataType",
+ "One capability a deployment does or does not have. An open list rather "
+ "than an enumeration because the set of things a model can do is not "
+ "closed, and a client that cannot recognise a capability name is no worse "
+ "off than one that cannot recognise an enumeration value it has never "
+ "seen.",
+ [("Name", String, "Capability name, for example 'chat', 'embeddings', "
+ "'streaming', 'tool-call' or 'structured-output'."),
+ ("Supported", Boolean, "Whether this deployment supports it.")])
+CapabilityDataType = T(3053)
+
+struct_type(3054, "SafetyAssessmentDataType",
+ "One finding from a safety policy applied to an inference call. Category "
+ "is a String and not an enumeration because harm categories are set by the "
+ "policy an installation adopts, and an industrial taxonomy looks nothing "
+ "like a consumer one.",
+ [("Category", String, "Category the policy assessed, for example "
+ "'out-of-distribution-input' or a policy-defined "
+ "name."),
+ ("Severity", SafetySeverityEnum, "Severity of the finding."),
+ ("Filtered", Boolean, "True when the content was withheld or altered "
+ "rather than merely flagged."),
+ ("Detail", String, "Human-readable explanation. For a human; SHALL NOT "
+ "be parsed.")])
+SafetyAssessmentDataType = T(3054)
+
+struct_type(3055, "EvaluationMetricDataType",
+ "One measured metric from an evaluation run, with the threshold it was "
+ "judged against. The threshold travels with the metric because a metric "
+ "without its acceptance criterion cannot be acted on, and a reviewer "
+ "reading it a year later has no way to recover what 'good' meant.",
+ [("Name", String, "Metric name, for example 'accuracy' or "
+ "'false-negative-rate'."),
+ ("Value", Double, "Measured value."),
+ ("Unit", String, "Unit of the value, or empty when dimensionless."),
+ ("Threshold", Double, "Acceptance threshold applied."),
+ ("Comparison", String, "How Value was compared with Threshold: one of "
+ "'>=', '<=', '>', '<' or '=='."),
+ ("Passed", Boolean, "Outcome of that comparison.")])
+EvaluationMetricDataType = T(3055)
+
+struct_type(3056, "RateLimitDataType",
+ "Capacity a remote endpoint is currently granting. Surfaced so a client "
+ "can distinguish 'the model said no' from 'the quota said no', which are "
+ "different faults with different remedies.",
+ [("UnitKind", String, "Unit the limit is expressed in, matching "
+ "UsageDataType.UnitKind, or 'requests'."),
+ ("Limit", UInt64, "Units permitted per interval, or 0 when not "
+ "published."),
+ ("Remaining", UInt64, "Units still available in the current interval."),
+ ("Interval", Duration, "Length of the interval the limit applies to."),
+ ("RetryAfter", Duration, "How long to wait before retrying. Zero when "
+ "the endpoint gave no guidance.")])
+RateLimitDataType = T(3056)
+
+# ---------------------------------------------------------------------------
+# ReferenceTypes (4001+)
+# ---------------------------------------------------------------------------
+reference_type(4001, "UsesModel", "IsUsedByDeployment",
+ "Links a Deployment to the Model it executes. Clause 6.5 requires "
+ "exactly one such reference per deployment; it is the only defined path "
+ "from a result to the model artefact and its Digest, on which the "
+ "provenance requirement of clause 12 depends.")
+UsesModel = T(4001)
+
+reference_type(4002, "TrainedOn", "IsTrainingDataFor",
+ "Links a Model to a Dataset it was trained or validated on. A model "
+ "whose training data cannot be named is a model whose behaviour cannot "
+ "be explained, which is why this reference exists rather than a string.")
+TrainedOn = T(4002)
+
+reference_type(4003, "DerivedFrom", "IsBaseOfModel",
+ "Links a Model to the Model it was fine-tuned, distilled or quantized "
+ "from. Lineage is a chain, not a field: a model three derivations from "
+ "its base is answerable for all three, and a string naming the "
+ "immediate parent cannot be walked.")
+DerivedFrom = T(4003)
+
+reference_type(4004, "FallsBackTo", "IsFallbackFor",
+ "Links a Deployment to the Deployment that serves in its place when it "
+ "cannot. Clause 9 forbids a cycle, and requires the response to say "
+ "which deployment actually answered.")
+FallsBackTo = T(4004)
+
+reference_type(4005, "ImportedFrom", "WasImportedAs",
+ "Links a Model to the catalogue resource an import job materialized it "
+ "from. This is what makes 'where did this model come from' answerable "
+ "after the fact, rather than only at the moment of import.")
+ImportedFrom = T(4005)
+
+reference_type(4006, "EvaluatedBy", "Evaluates",
+ "Links a Model to an EvaluationRun that measured it. Optional and "
+ "repeating: a model may be evaluated many times, and the run that "
+ "gated its promotion is not necessarily the last one.")
+EvaluatedBy = T(4006)
+
+# ---------------------------------------------------------------------------
+# ObjectTypes (1001+)
+# ---------------------------------------------------------------------------
+object_type(1001, "AiRootType", BaseObjectType,
+ "Server-level entry point. A client that has just connected browses here to "
+ "find every model, dataset, deployment and learning job the Server "
+ "describes, without knowing its layout.")
+RT_ = 1001
+folder_member(RT_, "AiRootType", "Models", "ModelType instances.")
+folder_member(RT_, "AiRootType", "Datasets", "DatasetType instances.", MR_Optional)
+folder_member(RT_, "AiRootType", "Deployments", "DeploymentType instances.")
+folder_member(RT_, "AiRootType", "LearningJobs", "LearningJobType instances.",
+ MR_Optional)
+prop_var(RT_, "AiRootType", "SpecificationVersion", String,
+ "Release of this specification the Server implements, for example '0.1.0'.",
+ MR_Mandatory)
+
+object_type(1002, "ModelType", BaseObjectType,
+ "Nameplate of a trained model. The member set is deliberately aligned with "
+ "the IDTA 02060 AI Model Nameplate submodel template, which is currently the "
+ "only standardised description of an industrial AI model, so an Asset "
+ "Administration Shell can be populated from this node without loss.")
+AM = 1002
+prop_var(AM, "ModelType", "ModelId", String, "Identifier of the model.", MR_Mandatory)
+prop_var(AM, "ModelType", "Name", LocalizedText,
+ "Human-readable model name. Its Text SHALL be the name the source system "
+ "uses for the model, carried across unchanged. A LocalizedText because the "
+ "base model types names that way and retyping it would break every "
+ "implementation, but the localizable part is the presentation: a Server MAY "
+ "add a translation for display and SHALL NOT translate, reformat or "
+ "prettify the Text itself. Two Servers that fetched one model from two "
+ "mirrors are meant to produce the same string, and a name adjusted for "
+ "house style is a name that no longer matches.",
+ MR_Mandatory)
+prop_var(AM, "ModelType", "Version", String, "Model version.", MR_Mandatory)
+prop_var(AM, "ModelType", "Framework", String,
+ "Producing framework, for example PyTorch, TensorFlow or scikit-learn.")
+prop_var(AM, "ModelType", "Format", String,
+ "Serialization format, for example ONNX, TensorRT or OpenVINO IR.")
+prop_var(AM, "ModelType", "TaskKind", String,
+ "What the model does, for example Detection2D, Classification, Segmentation, "
+ "Forecasting or AnomalyDetection. Free text because the set of tasks is not "
+ "closed and a closed enumeration would date faster than the model does.")
+prop_var(AM, "ModelType", "Digest", ByteString,
+ "Cryptographic digest of the model artefact, for provenance and integrity. "
+ "Mandatory: clause 12 requires it for every model whose artefact is obtainable "
+ "through ArtifactUri, and it is the terminus of the provenance chain that "
+ "UsesModel keeps intact.",
+ MR_Mandatory)
+prop_var(AM, "ModelType", "DigestAlgorithm", String,
+ "Hash function used for Digest. SHALL name a function with at least 256-bit "
+ "output and no known collision weakness; SHA-256 is the default and is always "
+ "acceptable. SHALL NOT be MD5, SHA-1 or a truncated variant - chosen-prefix "
+ "collisions against those are practical, so a substituted artefact would pass "
+ "verification. SHALL be non-empty where Digest is non-empty. See clause 12.",
+ MR_Mandatory)
+prop_var(AM, "ModelType", "ArtifactUri", String,
+ "Where the model artefact can be obtained. Treated as untrusted input.")
+prop_var(AM, "ModelType", "ProvenanceUri", String,
+ "Training provenance or model card location.")
+prop_var(AM, "ModelType", "LabelClasses", String,
+ "Ordered class label set, where the model produces classified output. The "
+ "INDEX is what a consuming specification's class identifier refers to, so the "
+ "order is part of the contract and a Server SHALL NOT reorder it in place.",
+ MR_Optional, valuerank="1")
+data_var(AM, "ModelType", "Inputs", TensorSignatureDataType,
+ "Input tensor signatures.", MR_Optional, valuerank="1")
+data_var(AM, "ModelType", "Outputs", TensorSignatureDataType,
+ "Output tensor signatures.", MR_Optional, valuerank="1")
+
+object_type(1003, "DatasetType", BaseObjectType,
+ "A dataset used to train or validate a model. Aligned with the IDTA 02058 AI "
+ "Dataset submodel template. SourceKind distinguishes real capture from "
+ "simulator output, which is the provenance a reviewer needs when synthetic "
+ "data is involved.")
+AD = 1003
+prop_var(AD, "DatasetType", "DatasetId", String, "Identifier of the dataset.",
+ MR_Mandatory)
+prop_var(AD, "DatasetType", "Name", LocalizedText, "Human-readable dataset name.")
+prop_var(AD, "DatasetType", "Version", String, "Dataset version.")
+prop_var(AD, "DatasetType", "SourceKind", DatasetSourceEnum,
+ "Whether samples are real, synthetic or mixed.", MR_Mandatory)
+prop_var(AD, "DatasetType", "SampleCount", UInt64, "Number of samples.")
+prop_var(AD, "DatasetType", "LabelClasses", String, "Class labels present.",
+ MR_Optional, valuerank="1")
+prop_var(AD, "DatasetType", "CreatedAt", UtcTime, "Creation time.")
+prop_var(AD, "DatasetType", "ArtifactUri", String,
+ "Where the dataset can be obtained. Treated as untrusted input.")
+prop_var(AD, "DatasetType", "Digest", ByteString, "Digest of the dataset artefact.")
+
+object_type(1004, "DeploymentType", BaseObjectType,
+ "A model made executable somewhere. Aligned with the IDTA 02059 AI "
+ "Deployment submodel template. InferenceLocation is the on-server versus "
+ "off-server switch: it changes where the computation happens and therefore "
+ "the trust boundary, and it changes nothing else.")
+AY = 1004
+prop_var(AY, "DeploymentType", "DeploymentId", String,
+ "Identifier of the deployment.", MR_Mandatory)
+prop_var(AY, "DeploymentType", "InferenceLocation", InferenceLocationEnum,
+ "Where inference executes.", MR_Mandatory)
+prop_var(AY, "DeploymentType", "AcceleratorKind", AcceleratorKindEnum,
+ "Compute device executing the model.")
+prop_var(AY, "DeploymentType", "AcceleratorName", String,
+ "Free-text accelerator identification, for example an NPU or GPU part name.")
+prop_var(AY, "DeploymentType", "EndpointUri", String,
+ "Inference endpoint when InferenceLocation is not OnServer. Treated as "
+ "untrusted input and subject to the resolver policy of clause 12.")
+prop_var(AY, "DeploymentType", "LatencyBudget", Duration,
+ "Latency the deployment is expected to meet. Set by whoever commissioned the "
+ "deployment; ObservedLatency is what it actually achieved, and clause 6.4.3 "
+ "compares the two.")
+prop_var(AY, "DeploymentType", "BatchSize", UInt32,
+ "Configured inference batch size.")
+prop_var(AY, "DeploymentType", "State", DeploymentStateEnum,
+ "Runtime state of the deployment.", MR_Mandatory)
+
+object_type(1005, "LearningJobType", T(1006),
+ "One turn of the capture, label, train and promote loop. It exists so that "
+ "corrections arriving from a consuming application have somewhere to "
+ "accumulate and a defined path into a new model version. A Server may "
+ "implement only the capture stages and leave training to an external MLOps "
+ "system - the state machine is the same either way.")
+LJ = 1005
+prop_var(LJ, "LearningJobType", "State", LearningJobStateEnum,
+ "Current stage of the loop. This is the PHASE, not the program lifecycle: "
+ "the inherited CurrentState says whether the job is running, this says what "
+ "it is doing. Clause 7 requires the two to agree.", MR_Mandatory)
+prop_var(LJ, "LearningJobType", "Dataset", NodeId_,
+ "Dataset being accumulated or used.")
+prop_var(LJ, "LearningJobType", "BaseModel", NodeId_, "Model the job starts from.")
+prop_var(LJ, "LearningJobType", "CandidateModel", NodeId_,
+ "Model produced by the job, awaiting promotion.")
+prop_var(LJ, "LearningJobType", "SamplesCollected", UInt64,
+ "Samples accumulated so far, including corrections fed back.")
+method(LJ, "LearningJobType", "StartCollection",
+ "Begin accumulating samples and corrections into the dataset.", MR_Optional)
+method(LJ, "LearningJobType", "StopCollection",
+ "Stop accumulating samples.", MR_Optional)
+method(LJ, "LearningJobType", "TriggerTraining",
+ "Request that a candidate model be trained from the collected dataset.",
+ MR_Optional,
+ outargs=[("Accepted", Boolean, "True when the request was queued.")])
+method(LJ, "LearningJobType", "PromoteModel",
+ "Promote the candidate model so that deployments begin using it. A Server SHALL "
+ "require a distinct authorization for this Method: it changes what the equipment "
+ "does without changing anything a reader of the address space would notice, "
+ "which is precisely the change that needs a separate permission.",
+ MR_Optional,
+ inargs=[("Deployment", NodeId_, "Deployment to update, or null for all.")],
+ outargs=[("PromotedModel", NodeId_, "The model now in use.")])
+
+# ---------------------------------------------------------------------------
+# ObjectTypes added in 0.2.0.
+#
+# Their MEMBERS necessarily sit at the end of the member id space even where the type
+# is conceptually a base of an earlier one: member ids are assigned in declaration
+# order and are append-only, so declaring AiJobType's members where the type "belongs"
+# would renumber everything after it.
+# ---------------------------------------------------------------------------
+object_type(1006, "AiJobType", ProgramStateMachineType,
+ "Abstract base of every long-running AI operation: learning, model import "
+ "and asynchronous inference. It derives from the OPC 10000-10 "
+ "ProgramStateMachineType, so the lifecycle - Ready, Running, Suspended, "
+ "Halted - its transition events and its Start/Suspend/Resume/Halt Methods "
+ "are inherited rather than reinvented, and every job in this model is "
+ "auditable the same way.",
+ abstract=True)
+AJ = 1006
+prop_var(AJ, "AiJobType", "JobId", String,
+ "Identifier of the job, unique within the Server.", MR_Mandatory)
+prop_var(AJ, "AiJobType", "LastError", LocalizedText,
+ "Diagnostic for the most recent failure. For a human; SHALL NOT be parsed.")
+prop_var(AJ, "AiJobType", "StartedAt", UtcTime, "When the job last entered Running.")
+prop_var(AJ, "AiJobType", "FinishedAt", UtcTime,
+ "When the job last left Running, or null while it is running.")
+prop_var(AJ, "AiJobType", "Progress", Double,
+ "Fraction complete, 0.0 to 1.0, or null where the job cannot estimate it. A "
+ "Server SHALL NOT report a value it is guessing: null is informative, a "
+ "fabricated 0.5 is not.")
+prop_var(AJ, "AiJobType", "RequestedBy", String,
+ "Identity that requested the job, recorded at the moment it started. Clause 12 "
+ "requires this for any job that can promote a model.")
+
+object_type(1007, "ModelImportJobType", T(1006),
+ "Brings a model from a catalogue into this Server. It federates by default "
+ "- materializing the catalogue entry as a ModelType whose artefact stays "
+ "where it is - and stages the artefact when the target deployment could "
+ "not otherwise reach it. Staging is the moment a substituted artefact "
+ "would enter, which is why clause 10 requires the Digest to be verified "
+ "there and nowhere else.")
+MI = 1007
+prop_var(MI, "ModelImportJobType", "Source", NodeId_,
+ "ModelSourceType instance the model is pulled from, where the import calls "
+ "an endpoint. Null where the import reads a catalogue instead, in which "
+ "case Registry names it. Exactly one of the two is non-null.", MR_Mandatory)
+prop_var(MI, "ModelImportJobType", "ModelReference", ModelReferenceDataType,
+ "Publisher, name and version being imported.", MR_Mandatory)
+prop_var(MI, "ModelImportJobType", "Mode", ImportModeEnum,
+ "Whether to federate, stage, or decide from the target's InferenceLocation.",
+ MR_Mandatory)
+prop_var(MI, "ModelImportJobType", "TargetDeployment", NodeId_,
+ "Deployment to create or update on success, or null to import the model "
+ "without deploying it.")
+prop_var(MI, "ModelImportJobType", "ImportedModel", NodeId_,
+ "ModelType instance the job produced. Null until the job succeeds.")
+prop_var(MI, "ModelImportJobType", "BytesTransferred", UInt64,
+ "Artefact bytes fetched so far. Zero for a federating import, which moves "
+ "none.")
+prop_var(MI, "ModelImportJobType", "DigestVerified", Boolean,
+ "Whether the staged artefact's computed digest matched the one the catalogue "
+ "declared. False on a staging import means the artefact SHALL NOT be "
+ "deployed.")
+method(MI, "ModelImportJobType", "Cancel",
+ "Abandon the import. A partially staged artefact SHALL be discarded rather "
+ "than left where a later deployment could pick it up.", MR_Optional)
+
+object_type(1008, "InferenceJobType", T(1006),
+ "One asynchronous inference request. It exists because not every inference "
+ "returns while the caller waits: a batch scored overnight and a long "
+ "analysis over recorded data are ordinary industrial cases, and modelling "
+ "them as a Method that blocks for hours is not.")
+IJ = 1008
+prop_var(IJ, "InferenceJobType", "Deployment", NodeId_,
+ "Deployment executing the request.", MR_Mandatory)
+prop_var(IJ, "InferenceJobType", "RequestPayload", ByteString,
+ "Request body, encoded as RequestContentType states.")
+prop_var(IJ, "InferenceJobType", "RequestContentType", String,
+ "Media type of RequestPayload.")
+prop_var(IJ, "InferenceJobType", "ResponsePayload", ByteString,
+ "Response body once the job succeeds.")
+prop_var(IJ, "InferenceJobType", "ResponseContentType", String,
+ "Media type of ResponsePayload.")
+prop_var(IJ, "InferenceJobType", "ModelUsed", NodeId_,
+ "Model that ACTUALLY executed the request, which is not always the one the "
+ "deployment named when the job was submitted - a fallback or a followed "
+ "reference can change it in between. The provenance chain of clause 12 walks "
+ "this, not the deployment's current model.")
+prop_var(IJ, "InferenceJobType", "Usage", UsageDataType,
+ "What the call consumed.")
+prop_var(IJ, "InferenceJobType", "FinishReason", FinishReasonEnum,
+ "Why the call stopped producing output.")
+prop_var(IJ, "InferenceJobType", "SafetyAssessment", SafetyAssessmentDataType,
+ "Findings from the safety policy, if any were applied.", valuerank="1")
+
+object_type(1009, "ModelSourceType", BaseObjectType,
+ "An externally hosted inference or catalogue endpoint this Server can "
+ "reach. It carries everything needed to actually call something the Server "
+ "did not deploy - the wire contract, how to authenticate, what the endpoint "
+ "can do and whether it is answering - because a URI on its own is a string "
+ "nobody can act on.")
+MS = 1009
+prop_var(MS, "ModelSourceType", "SourceId", String,
+ "Identifier of the source.", MR_Mandatory)
+prop_var(MS, "ModelSourceType", "EndpointUri", String,
+ "Base URI of the endpoint. Untrusted input, subject to the resolver policy of "
+ "clause 12.", MR_Mandatory)
+prop_var(MS, "ModelSourceType", "ApiDialect", ApiDialectEnum,
+ "Wire contract the endpoint speaks.", MR_Mandatory)
+prop_var(MS, "ModelSourceType", "EndpointDescriptionUri", String,
+ "Where the contract is documented. SHOULD be populated when ApiDialect is "
+ "Proprietary, because otherwise nothing in the address space says how to call "
+ "it.")
+prop_var(MS, "ModelSourceType", "AuthenticationKind", AuthenticationKindEnum,
+ "How the Server authenticates itself to the endpoint.", MR_Mandatory)
+prop_var(MS, "ModelSourceType", "CredentialReference", String,
+ "Opaque handle naming the credential in whatever store the Server uses. It is "
+ "a NAME, never a secret: clause 12 forbids a Server from exposing credential "
+ "material through any Attribute of this model, and a client that can read this "
+ "value learns only which credential is used, not what it is.")
+prop_var(MS, "ModelSourceType", "TokenAudience", String,
+ "Audience or scope a bearer token is requested for, where "
+ "AuthenticationKind is BearerToken.")
+prop_var(MS, "ModelSourceType", "Reachability", ReachabilityEnum,
+ "Whether the Server can currently reach the endpoint.", MR_Mandatory)
+prop_var(MS, "ModelSourceType", "LastSuccessAt", UtcTime,
+ "When the endpoint last answered successfully.")
+prop_var(MS, "ModelSourceType", "ConsecutiveFailures", UInt32,
+ "Failures since the last success. Reset to zero on success.")
+prop_var(MS, "ModelSourceType", "RateLimit", RateLimitDataType,
+ "Capacity the endpoint is currently granting.")
+prop_var(MS, "ModelSourceType", "Capabilities", CapabilityDataType,
+ "What the endpoint reports it can do.", valuerank="1")
+method(MS, "ModelSourceType", "TestConnection",
+ "Probe the endpoint and update Reachability. Defined so that a commissioning "
+ "engineer can establish that credentials and network policy are right BEFORE a "
+ "deployment depends on them, rather than discovering it from a failed "
+ "inference.", MR_Optional,
+ outargs=[("Reachable", Boolean, "Whether the probe succeeded."),
+ ("Detail", LocalizedText, "Diagnostic. For a human.")])
+method(MS, "ModelSourceType", "ListModels",
+ "Enumerate the models the source offers.", MR_Optional,
+ inargs=[("Filter", String, "Optional substring or expression; empty for all."),
+ ("MaxResults", UInt32, "Upper bound on returned entries."),
+ ("ContinuationPoint", ByteString,
+ "Empty on the first call; otherwise the value the previous call "
+ "returned. A cap without a cursor bounds the response and puts every "
+ "entry past it out of reach, which against a public catalogue means "
+ "most of them.")],
+ outargs=[("Models", ModelReferenceDataType,
+ "Publisher, name and version of each model offered.", 1),
+ ("ContinuationPoint", ByteString,
+ "Pass to the next call to continue. Empty when the enumeration is "
+ "complete, which is how a client knows to stop rather than by "
+ "comparing counts.")])
+
+object_type(1014, "EvaluationRunType", BaseObjectType,
+ "One measurement of a model against a dataset. It is a first-class object "
+ "and not a field on the model because the same model is evaluated many "
+ "times, and because the run that gated a promotion has to remain readable "
+ "afterwards to answer why the promotion was allowed.")
+ER = 1014
+prop_var(ER, "EvaluationRunType", "RunId", String, "Identifier of the run.",
+ MR_Mandatory)
+prop_var(ER, "EvaluationRunType", "EvaluatedModel", NodeId_,
+ "Model that was measured.", MR_Mandatory)
+prop_var(ER, "EvaluationRunType", "Dataset", NodeId_,
+ "Dataset the model was measured against.")
+prop_var(ER, "EvaluationRunType", "CompletedAt", UtcTime, "When the run finished.")
+prop_var(ER, "EvaluationRunType", "Metrics", EvaluationMetricDataType,
+ "Measured metrics, each with the threshold it was judged against.",
+ MR_Mandatory, valuerank="1")
+prop_var(ER, "EvaluationRunType", "Passed", Boolean,
+ "Whether every metric met its threshold. A Server SHALL NOT report true while "
+ "any entry in Metrics has Passed false - a summary that disagrees with its "
+ "own detail is worse than no summary.", MR_Mandatory)
+prop_var(ER, "EvaluationRunType", "ReportUri", String,
+ "Where the full report lives. Untrusted input, subject to clause 12.")
+
+object_type(1015, "ModelCardType", BaseObjectType,
+ "What a human needs to decide whether a model may be used here: what it is "
+ "for, where it stops working, and under what terms. Separate from the "
+ "nameplate because a nameplate answers 'which artefact is this' and a card "
+ "answers 'should this be running on my line'.")
+MC = 1015
+prop_var(MC, "ModelCardType", "IntendedUse", LocalizedText,
+ "What the model is for.", MR_Mandatory)
+prop_var(MC, "ModelCardType", "Limitations", LocalizedText,
+ "Where it is known not to work. Mandatory because a card that lists only "
+ "capabilities is marketing, and the failure modes are the half a commissioning "
+ "engineer needs.", MR_Mandatory)
+prop_var(MC, "ModelCardType", "OutOfScopeUse", LocalizedText,
+ "Uses the supplier explicitly excludes.")
+prop_var(MC, "ModelCardType", "License", String,
+ "Licence identifier or URI governing use of the artefact.")
+prop_var(MC, "ModelCardType", "TrainingDataCutoff", UtcTime,
+ "Latest date represented in the training data. A model cannot know about "
+ "anything after this, which is often the explanation for a field failure.")
+prop_var(MC, "ModelCardType", "EthicalConsiderations", LocalizedText,
+ "Risks the supplier records.")
+prop_var(MC, "ModelCardType", "ContactUri", String,
+ "Where to report a problem with the model.")
+
+# ---------------------------------------------------------------------------
+# The catalogue, as a domain extension of OPC UA - xRegistry (clause 10).
+#
+# A model catalogue IS a registry: publishers own namespaces, models and datasets are
+# resources within them, and versions are immutable. Subtyping the abstract registry
+# gets that structure, its browse and lifecycle behaviour, and - because ResourceType
+# is itself a Part 5 FileType - artefact streaming through the inherited Open/Read/
+# Close, which is what a staging import needs.
+# ---------------------------------------------------------------------------
+XRegistry_RegistryType = X(63000)
+XRegistry_GroupType = X(63001)
+XRegistry_ResourceType = X(63002)
+
+object_type(1010, "ModelRegistryType", XRegistry_RegistryType,
+ "A catalogue of models and the datasets they were trained on. It narrows "
+ "the abstract registry's group placeholder to model publishers, so that a "
+ "client browsing it knows what it will find rather than discovering it.")
+MR_ = 1010
+# An InstanceDeclaration is overridden only by one with the SAME BrowseName, so the
+# narrowing has to reuse the inherited and names and the inherited
+# Organizes. Declaring new placeholder names would leave the inherited ones fully open
+# - the subtype would look narrowed while still admitting any GroupType at all.
+
+object_type(1011, "ModelPublisherType", XRegistry_GroupType,
+ "One publisher's namespace within a model registry: the organisation or "
+ "project that released the models it contains. Publisher is the first "
+ "element of the publisher/name/version triple by which every catalogue in "
+ "practice identifies a model.")
+MP = 1011
+
+object_type(1016, "AiResourceType", XRegistry_ResourceType,
+ "Abstract base of everything a model registry holds. It exists so that the "
+ "inherited placeholder can be narrowed ONCE to something that "
+ "admits models and datasets and nothing else - a publisher holds both, and "
+ "a placeholder can be overridden only by one declaration.",
+ abstract=True)
+
+object_type(1012, "ModelResourceType", T(1016),
+ "One model in a catalogue. Its versions are immutable and identified by "
+ "content, so a version that has been seen cannot change meaning; mutable "
+ "names such as a branch or a release channel are pointers AT versions, "
+ "never versions themselves. Because the base type is a FileType, a Server "
+ "that holds the artefact serves it through the inherited Open, Read and "
+ "Close; one that only describes it leaves those unimplemented and points "
+ "at the artefact instead.")
+MRS = 1012
+prop_var(MRS, "ModelResourceType", "TaskKind", String,
+ "What the model does, for example 'object-detection' or "
+ "'anomaly-detection'. A String and not an enumeration, for the same reason it "
+ "is one on ModelType: the set is not closed, and every catalogue in practice "
+ "uses a free tag here.")
+prop_var(MRS, "ModelResourceType", "Framework", String,
+ "Runtime or library the artefact targets.")
+prop_var(MRS, "ModelResourceType", "Digest", ByteString,
+ "Digest of the artefact this version names, as the catalogue declares it. A "
+ "staging import compares its own computed digest with this and refuses on "
+ "mismatch.")
+prop_var(MRS, "ModelResourceType", "DigestAlgorithm", String,
+ "Algorithm of Digest. Subject to the strength requirement of clause 12.")
+prop_var(MRS, "ModelResourceType", "SizeBytes", UInt64,
+ "Artefact size, so a staging import can decide whether it has room before "
+ "it starts rather than after it fails.")
+prop_var(MRS, "ModelResourceType", "Gated", Boolean,
+ "Whether obtaining the artefact requires an acceptance or entitlement beyond "
+ "ordinary authentication. A client that ignores this discovers it as a "
+ "failure part-way through a staging import.")
+prop_var(MRS, "ModelResourceType", "MutableRefs", String,
+ "Mutable pointers this resource publishes - branches, tags or channels - that "
+ "a deployment may follow instead of pinning. Naming them is what makes "
+ "VersionBinding FollowsRef checkable.", valuerank="1")
+
+object_type(1013, "DatasetResourceType", T(1016),
+ "One dataset in a catalogue, a sibling of ModelResourceType rather than "
+ "something beneath it: a dataset outlives the models trained on it and is "
+ "cited by several.")
+DRS = 1013
+prop_var(DRS, "DatasetResourceType", "SourceKind", DatasetSourceEnum,
+ "Whether the samples are real, synthetic or mixed.")
+prop_var(DRS, "DatasetResourceType", "SampleCount", UInt64, "Samples in the dataset.")
+prop_var(DRS, "DatasetResourceType", "Digest", ByteString,
+ "Digest of the dataset artefact as the catalogue declares it.")
+prop_var(DRS, "DatasetResourceType", "DigestAlgorithm", String,
+ "Algorithm of Digest.")
+prop_var(DRS, "DatasetResourceType", "SizeBytes", UInt64, "Dataset size.")
+
+# ---------------------------------------------------------------------------
+# Members appended to types declared in 0.1.0. All append; nothing renumbers.
+# ---------------------------------------------------------------------------
+
+# --- AiRootType: the new collections ---------------------------------------
+folder_member(RT_, "AiRootType", "Sources",
+ "ModelSourceType instances - the externally hosted endpoints and "
+ "catalogues this Server can reach.", MR_Optional)
+folder_member(RT_, "AiRootType", "Registries",
+ "ModelRegistryType instances this Server serves or mirrors.",
+ MR_Optional)
+folder_member(RT_, "AiRootType", "Evaluations",
+ "EvaluationRunType instances.", MR_Optional)
+folder_member(RT_, "AiRootType", "Jobs",
+ "Import and asynchronous inference jobs. Learning jobs remain under "
+ "LearningJobs.", MR_Optional)
+
+# --- ModelType: provenance, card and lineage -------------------------------
+obj_member(AM, "ModelType", "Card", T(1015),
+ "What a human needs to decide whether this model may run here.", MR_Optional)
+prop_var(AM, "ModelType", "Publisher", String,
+ "Organisation or namespace that published the model. With Name and Version "
+ "this is the triple every catalogue identifies a model by, and it is what "
+ "makes the same model recognisable across two installations that fetched it "
+ "from different mirrors.")
+prop_var(AM, "ModelType", "ParameterCount", UInt64,
+ "Parameters in the model, or 0 where not published. A crude but universally "
+ "available proxy for what it will cost to run.")
+prop_var(AM, "ModelType", "Quantization", String,
+ "Numeric precision the artefact is stored in, for example 'fp32', 'int8' or "
+ "'fp8'. A quantized model is a DIFFERENT artefact with different behaviour, "
+ "not a packaging detail, which is why it is stated rather than left to the "
+ "format string.")
+prop_var(AM, "ModelType", "SafetyPolicyUri", String,
+ "Safety or content policy applied to this model's output, where one is. "
+ "Untrusted input, subject to clause 12.")
+
+# --- DeploymentType: federation --------------------------------------------
+prop_var(AY, "DeploymentType", "Source", NodeId_,
+ "ModelSourceType instance this deployment executes through, where inference "
+ "is not local. Null when InferenceLocation is OnServer.")
+prop_var(AY, "DeploymentType", "VersionBinding", VersionBindingEnum,
+ "Whether the deployment is pinned to an immutable model version or follows a "
+ "mutable pointer.", MR_Mandatory)
+prop_var(AY, "DeploymentType", "BoundRef", String,
+ "The mutable pointer being followed, where VersionBinding is FollowsRef. "
+ "Empty when Pinned.")
+prop_var(AY, "DeploymentType", "FallbackPolicy", FallbackPolicyEnum,
+ "What the Server does when this deployment cannot serve.", MR_Mandatory)
+prop_var(AY, "DeploymentType", "Reachability", ReachabilityEnum,
+ "Whether the execution site is currently reachable. Always Reachable for an "
+ "OnServer deployment that is not Faulted.")
+prop_var(AY, "DeploymentType", "ConsecutiveFailures", UInt32,
+ "Failed calls since the last success.")
+prop_var(AY, "DeploymentType", "LastSuccessAt", UtcTime,
+ "When this deployment last answered successfully. With FallbackPolicy "
+ "HoldLast this is how a caller judges whether the held answer is still worth "
+ "having.")
+prop_var(AY, "DeploymentType", "RateLimit", RateLimitDataType,
+ "Capacity the execution site is currently granting.")
+prop_var(AY, "DeploymentType", "Capabilities", CapabilityDataType,
+ "What this deployment can do. A client checks here before calling a typed "
+ "profile rather than discovering the answer from a rejection.", valuerank="1")
+
+# --- DeploymentType: data residency and egress -----------------------------
+prop_var(AY, "DeploymentType", "DataJurisdiction", String,
+ "Where input data is processed, named in whatever scheme the operator uses - "
+ "a site, a legal jurisdiction, or a named zone. This is the question a plant "
+ "actually asks, and no amount of latency or accuracy data answers it.",
+ MR_Mandatory)
+prop_var(AY, "DeploymentType", "EgressPermitted", Boolean,
+ "Whether calling this deployment sends input data outside the operator's "
+ "boundary. A Server SHALL set this true for every deployment whose "
+ "InferenceLocation is Cloud, and SHALL NOT set it false merely because the "
+ "channel is encrypted - the question is where the data goes, not who can "
+ "read it in flight.", MR_Mandatory)
+prop_var(AY, "DeploymentType", "RetainsInput", Boolean,
+ "Whether the execution site retains input beyond serving the request, for "
+ "example for provider-side logging or training. Unknown is not a value: a "
+ "Server that cannot establish this SHALL report true, because the safe "
+ "assumption is the one that keeps data in.")
+prop_var(AY, "DeploymentType", "EgressPolicyUri", String,
+ "Where the governing data policy is documented.")
+
+# --- DeploymentType: the invocation surface --------------------------------
+method(AY, "DeploymentType", "Invoke",
+ "Run inference and return the result. The payload is opaque here: what goes in "
+ "and comes out is the consuming specification's vocabulary, and an envelope "
+ "that tried to type it would have to be extended for every domain. What this "
+ "Method fixes is everything AROUND the payload - routing, parameters, "
+ "accounting, why it stopped, and which model actually ran.\n\n"
+ "The signature does not change with InferenceLocation. A deployment served from "
+ "the Server's own process and one served from a remote service are called "
+ "identically; the location changes the trust boundary and the latency, and "
+ "nothing else.", MR_Optional,
+ inargs=[("Payload", ByteString, "Request body."),
+ ("PayloadUri", String,
+ "Location the request body is read from, where it is supplied by "
+ "reference rather than carried. A Server SHALL accept exactly one of "
+ "Payload and PayloadUri and SHALL reject a call supplying both or "
+ "neither. Untrusted input subject to clause 12.2, and named data the "
+ "execution site will read, so clause 9.5 applies to it."),
+ ("ContentType", String, "Media type of Payload."),
+ ("Parameters", KeyValuePair,
+ "Call parameters such as a sampling temperature or an output length "
+ "bound. A Server SHALL reject a parameter it does not support rather "
+ "than ignore it: a caller whose parameter was silently dropped "
+ "believes it took effect.", 1),
+ ("Timeout", Duration,
+ "How long the caller will wait. Zero means the Server's default.")],
+ outargs=[("ResponsePayload", ByteString, "Response body."),
+ ("ResponseContentType", String, "Media type of ResponsePayload."),
+ ("ModelUsed", NodeId_,
+ "The model that ACTUALLY produced this response. Not necessarily the "
+ "one the deployment names now: a fallback answered from a different "
+ "deployment, and a FollowsRef binding may have moved. The provenance "
+ "chain of clause 12 walks this."),
+ ("Usage", UsageDataType, "What the call consumed."),
+ ("FinishReason", FinishReasonEnum,
+ "Why output stopped. A caller that ignores this will accept a "
+ "truncated answer as a complete one."),
+ ("SafetyAssessment", SafetyAssessmentDataType,
+ "Findings from the safety policy, if any applied.", 1),
+ ("RetryAfter", Duration,
+ "How long to wait before retrying, where the failure was a capacity "
+ "one. Zero when retrying immediately is as good as waiting, and "
+ "meaningless when the failure was not retryable."),
+ ("TransferRequired", Boolean,
+ "True when the deployment produced a response too large to return "
+ "inline. ResponsePayload is then empty and the work is NOT lost - "
+ "Transfer names where to read it. A client that ignores this reads "
+ "an empty payload and concludes the model returned nothing."),
+ ("Transfer", NodeId_,
+ "InferenceTransferType instance holding the response, where "
+ "TransferRequired is true. Null otherwise.")])
+method(AY, "DeploymentType", "InvokeAsync",
+ "Submit inference to be completed later, returning immediately with the job "
+ "that will carry the result. For work that does not finish while a caller "
+ "waits - a batch scored overnight, an analysis over recorded data.",
+ MR_Optional,
+ inargs=[("Payload", ByteString, "Request body."),
+ ("PayloadUri", String,
+ "Location the request body is read from, where it is supplied by "
+ "reference rather than carried. Exactly one of Payload and PayloadUri "
+ "on the same terms as Invoke. This is the argument that lets a batch "
+ "already sitting in the plant's object store be scored without being "
+ "copied through the Session first."),
+ ("ContentType", String, "Media type of Payload."),
+ ("Parameters", KeyValuePair, "Call parameters.", 1)],
+ outargs=[("Job", NodeId_,
+ "InferenceJobType instance tracking the request. The caller "
+ "subscribes to it rather than polling.")])
+method(AY, "DeploymentType", "GetCapabilities",
+ "Report what this deployment can do, refreshed from the execution site rather "
+ "than from cache. Defined because a remote endpoint's capabilities change "
+ "without anything in this address space changing.", MR_Optional,
+ outargs=[("Capabilities", CapabilityDataType, "Current capabilities.", 1)])
+
+obj_member(MR_, "ModelRegistryType", "", T(1011),
+ "A publisher namespace held by this registry. Narrows the inherited "
+ "placeholder so a model registry admits ModelPublisherType and nothing "
+ "else.", MR_OptionalPlaceholder, reftype=Organizes)
+obj_member(MP, "ModelPublisherType", "", T(1016),
+ "A model or dataset published in this namespace. Narrows the inherited "
+ "placeholder to this model's own resource types.", MR_OptionalPlaceholder,
+ reftype=Organizes)
+
+prop_var(AY, "DeploymentType", "MaxInlinePayloadSize", UInt32,
+ "Largest request or response this deployment will carry inline through "
+ "Invoke, in bytes. Zero means the deployment accepts no inline payload at "
+ "all and BeginTransfer is the only way in.\n\n"
+ "A client reads this BEFORE calling rather than discovering the bound from "
+ "a rejection, and a Server SHALL NOT publish a value larger than its own "
+ "MaxByteStringLength, the negotiated MaxMessageSize or the Session's "
+ "MaxResponseMessageSize permit - the smallest of those is the real limit "
+ "and a client cannot see all of them.", MR_Mandatory)
+method(AY, "DeploymentType", "BeginTransfer",
+ "Opens a chunked exchange for a payload that will not fit inline, returning "
+ "the InferenceTransferType instance to write into. This is the general path: "
+ "Invoke is the shortcut that happens to work when everything is small.",
+ MR_Optional,
+ inargs=[("ContentType", String, "Media type of the request body."),
+ ("RequestSize", UInt64,
+ "Expected request size in bytes, or 0 when not known in advance. A "
+ "Server that cannot accommodate the stated size refuses here rather "
+ "than after the client has uploaded it.")],
+ outargs=[("Transfer", NodeId_,
+ "InferenceTransferType instance to write the request into."),
+ ("Accepted", Boolean,
+ "False when the Server declined to open the exchange.")])
+# ---------------------------------------------------------------------------
+# Well-known instance (7001+)
+# ---------------------------------------------------------------------------
+well_known(7001, "AiModelManagement", T(1001), Server,
+ "Entry point for the AI models this Server runs. A client browses "
+ "Server/AiModelManagement/Models to find what this Server describes.")
+
+object_type(1017, "InferenceTransferType", BaseObjectType,
+ "One chunked inference exchange. It exists because Invoke carries its "
+ "payload as a ByteString, and a ByteString is bounded by "
+ "MaxByteStringLength, the negotiated MaxMessageSize and the Session's "
+ "MaxResponseMessageSize - none of which the model gets to choose. An "
+ "image, a point cloud or a window of high-rate samples exceeds those "
+ "routinely, and a call that cannot carry the input is not a call.\n\n"
+ "Request and Response are Part 5 FileType objects: the client opens the "
+ "request, writes it in chunks it selects, and closes it; after Execute "
+ "the response is read the same way. Nothing here invents a transfer "
+ "protocol, because OPC UA already has one and every client already "
+ "implements it.")
+TR = 1017
+prop_var(TR, "InferenceTransferType", "TransferId", String,
+ "Identifier of this exchange.", MR_Mandatory)
+prop_var(TR, "InferenceTransferType", "State", TransferStateEnum,
+ "Stage the exchange has reached.", MR_Mandatory)
+obj_member(TR, "InferenceTransferType", "Request", FileType,
+ "The request body, written by the client in chunks of its own choosing. "
+ "Inference does not begin until Execute is called, so a partially written "
+ "request is never acted on.", MR_Mandatory)
+obj_member(TR, "InferenceTransferType", "Response", FileType,
+ "The response body, readable once State is Completed. Empty before that.",
+ MR_Mandatory)
+prop_var(TR, "InferenceTransferType", "ContentType", String,
+ "Media type of the request body.", MR_Mandatory)
+prop_var(TR, "InferenceTransferType", "ResponseContentType", String,
+ "Media type of the response body.")
+prop_var(TR, "InferenceTransferType", "ModelUsed", NodeId_,
+ "The model that ACTUALLY produced the response, on the same terms as "
+ "Invoke: a fallback or a followed reference can change it between the call "
+ "and the read.")
+prop_var(TR, "InferenceTransferType", "Usage", UsageDataType,
+ "What the call consumed.")
+prop_var(TR, "InferenceTransferType", "FinishReason", FinishReasonEnum,
+ "Why output stopped.")
+prop_var(TR, "InferenceTransferType", "SafetyAssessment", SafetyAssessmentDataType,
+ "Findings from the safety policy, if any applied.", valuerank="1")
+prop_var(TR, "InferenceTransferType", "LastError", LocalizedText,
+ "Diagnostic for the Failed state. For a human; SHALL NOT be parsed.")
+prop_var(TR, "InferenceTransferType", "ExpiresAt", UtcTime,
+ "When the Server may reclaim this transfer if it has not completed. A "
+ "client that abandons an exchange would otherwise hold Server resources "
+ "until the Session ends, and a Server that never reclaimed them would be "
+ "one denial of service away from unusable.")
+method(TR, "InferenceTransferType", "Execute",
+ "Runs inference over the written request. The Method returns as soon as the "
+ "request is accepted; State and the envelope members carry the outcome, which "
+ "is what lets one exchange span a payload too large to have been a single "
+ "call in the first place.", MR_Mandatory,
+ outargs=[("Accepted", Boolean,
+ "False when the request was incomplete or already executed.")])
+method(TR, "InferenceTransferType", "Abort",
+ "Abandons the exchange and releases what it holds. A client that has stopped "
+ "caring about a response SHOULD say so rather than leaving the Server to wait "
+ "out ExpiresAt.", MR_Optional)
+
+# ---------------------------------------------------------------------------
+# Members appended in 0.3.0. All append; nothing renumbers.
+# ---------------------------------------------------------------------------
+
+# --- Why a Digest is what it is --------------------------------------------
+# Mandatory on ModelType for the reason Digest itself is: clause 12 depends on
+# it, and a rule that depends on an Optional member is one a conformant Server
+# can silently not satisfy. Optional on ModelResourceType, mirroring the Digest
+# it qualifies, which is Optional there.
+prop_var(1002, "ModelType", "DigestProvenance", DigestProvenanceEnum,
+ "Where Digest came from, or why there is none. NotAvailable is the only "
+ "value permitted with an empty Digest, and it SHALL be used rather than "
+ "leaving a client to guess whether the source publishes no digest or this "
+ "Server declined to carry one.\n\n"
+ "A Server SHALL NOT put a non-content identifier in Digest to avoid saying "
+ "NotAvailable. A response fingerprint, a resource name, a storage entity tag "
+ "and a repository commit identifier are none of them digests of the artefact "
+ "that ran, and a client that verified against one would believe it had "
+ "checked something it had not. Where such an identifier is worth publishing "
+ "it belongs in ArtifactUri or ProvenanceUri, which promise nothing about "
+ "content.",
+ MR_Mandatory)
+prop_var(1012, "ModelResourceType", "DigestProvenance", DigestProvenanceEnum,
+ "Where this resource's Digest came from, on the same terms as ModelType. A "
+ "catalogue that declares a digest it did not compute is DeclaredBySource; "
+ "one serving the artefact through the inherited Open, Read and Close can "
+ "reach ComputedByServer.")
+
+# --- The registry an import job read from ----------------------------------
+prop_var(1007, "ModelImportJobType", "Registry", NodeId_,
+ "ModelRegistryType instance the model is imported from, where the import "
+ "reads a catalogue rather than calling an endpoint. Null otherwise.\n\n"
+ "A Server SHALL populate exactly one of Source and Registry, and SHALL "
+ "leave the other null. The two name the two things an import can read from, "
+ "and a job that named both would not say which one produced the artefact "
+ "whose digest clause 10.4 verifies.")
+
+# ---------------------------------------------------------------------------
+# Members appended in 0.4.0. All append; nothing renumbers.
+#
+# Every one of these answers something a real system publishes and this model
+# had nowhere to put, found by mapping it onto eleven of them. None is
+# Mandatory: each is governed by a conditional SHALL in the specification
+# instead, so the obligation binds exactly where a Server can discharge it.
+# ---------------------------------------------------------------------------
+
+# --- ModelType: when the artefact appeared, and when it last moved ----------
+prop_var(1002, "ModelType", "PublishedAt", UtcTime,
+ "When the source first published this model, where the source states it. "
+ "The same question DatasetType.CreatedAt answers for a dataset, and the same "
+ "reason: a model trained before a process change may no longer represent the "
+ "line it runs on, and Version is a vendor string that often cannot be "
+ "ordered.\n\n"
+ "This is the source's date, not when this Server learned of it - a Server "
+ "SHALL NOT substitute its own acquisition time, which would make every model "
+ "appear to date from the last restart.")
+prop_var(1002, "ModelType", "LastModifiedAt", UtcTime,
+ "When the artefact behind this model last changed at the source.\n\n"
+ "It exists for the FollowsRef case of clause 9.3, where the artefact can "
+ "change with nothing else changing. Clause 12.3.1 requires repointing to be "
+ "treated as an authorization-bearing act and points at AiJobType.RequestedBy "
+ "for the record - but a reference that moves AT THE SOURCE produces no job, "
+ "so without this member the audit trail that clause demands cannot be "
+ "constructed on the one path it exists to cover. A Server that follows a "
+ "mutable reference SHALL populate it.")
+
+# --- ModelCardType: the dates that end a model's working life ---------------
+prop_var(1015, "ModelCardType", "DeprecatedFrom", UtcTime,
+ "When the source stops treating this model as current while continuing to "
+ "serve it. The date that starts a requalification, not the one that ends "
+ "production.")
+prop_var(1015, "ModelCardType", "SupportedUntil", UtcTime,
+ "When the source stops serving this model altogether.\n\n"
+ "Its consequence is not degradation. On this date the deployment stops, "
+ "Reachability goes Unreachable, and FallbackPolicy decides what happens next "
+ "- which, where it is FallBackTo, means the line keeps producing and "
+ "something outside the qualified configuration is answering. A date that was "
+ "knowable a year in advance therefore becomes an unplanned change of model, "
+ "and it is published by the serving system in machine-readable form.")
+
+# --- DeploymentType: what a client must send, and what is serving it --------
+prop_var(1004, "DeploymentType", "ApiDialect", ApiDialectEnum,
+ "The contract a client's Payload must satisfy when calling Invoke on this "
+ "deployment. RestChatCompletions means the Payload is a chat-completions "
+ "request body; OpenInferenceProtocol means it is an OIP inference body; "
+ "EmbeddedRuntime and TensorRemoteProcedure name the tensor contracts "
+ "described by Inputs and Outputs; Proprietary means the contract is named "
+ "only by EndpointDescriptionUri.\n\n"
+ "This does not type the payload - clause 8.2 keeps it opaque and that is "
+ "unchanged. It names WHICH contract the opaque bytes are expected to satisfy, "
+ "which is what a client browsing an unfamiliar deployment needs before it can "
+ "send anything at all.")
+prop_var(1004, "DeploymentType", "EndpointDescriptionUri", String,
+ "Where the request and response contract for this deployment is documented. "
+ "Untrusted input, subject to clause 12.2. Required in practice wherever "
+ "ApiDialect is Proprietary, because nothing else then says what to send.")
+prop_var(1004, "DeploymentType", "RuntimeIdentity", String,
+ "Opaque identifier of the serving configuration currently behind this "
+ "deployment - a serving-stack fingerprint, an engine profile, a container "
+ "image digest. Compared for equality and never parsed, on the same terms as "
+ "Digest.\n\n"
+ "It is not the model. The same artefact served by two runtime builds can "
+ "produce different numbers, and where the execution site publishes such an "
+ "identity it is the only thing that records the difference. A change to it "
+ "under a Pinned binding IS the observable change to the deployment that "
+ "clause 9.3 says a pinned artefact cannot move without.")
+prop_var(1004, "DeploymentType", "ObservedLatency", Duration,
+ "Most recent inference latency this Server measured for this deployment.\n\n"
+ "LatencyBudget states what the deployment is expected to meet, and clause "
+ "6.4.3 makes Degraded the state of a deployment that is answering but missing "
+ "it. Without a measurement the comparison has no published input, so the "
+ "state transition could not be checked against a Server that claimed it. A "
+ "Server that reports Degraded on latency grounds SHALL populate this.")
+
+# --- InferenceJobType: the large-payload path Invoke already had ------------
+prop_var(1008, "InferenceJobType", "RequestUri", String,
+ "Where the request body was read from, where it was supplied by reference "
+ "rather than carried. Untrusted input under clause 12.2, and an egress path "
+ "under clause 9.5.")
+prop_var(1008, "InferenceJobType", "ResponseUri", String,
+ "Where the result was written, where the execution site returns a location "
+ "rather than bytes. Empty when the response is carried inline or through "
+ "Transfer.")
+prop_var(1008, "InferenceJobType", "TransferRequired", Boolean,
+ "True when the job produced a response too large to carry inline. "
+ "ResponsePayload is then empty and the work is NOT lost - Transfer names "
+ "where to read it.")
+prop_var(1008, "InferenceJobType", "Transfer", NodeId_,
+ "InferenceTransferType instance holding the response, where TransferRequired "
+ "is true. Null otherwise.\n\n"
+ "Invoke carries the same pair, and the asymmetry would otherwise leave the "
+ "jobs most likely to produce a large result - a batch scored overnight, an "
+ "analysis over recorded data - bounded by exactly the three limits clause "
+ "8.2.4 says this model does not get to choose.")
+
+# ===========================================================================
+# ================================== EMIT =================================
+# ===========================================================================
+_PRIO = {HasModellingRule: 0, HasSubtype: 0, HasTypeDefinition: 1}
+
+
+def _sorted_refs(refs):
+ return sorted(range(len(refs)), key=lambda i: (_PRIO.get(refs[i][0], 2), i))
+
+
+def _fmt_reftype(t):
+ return REFTYPE_ALIAS.get(t, t)
+
+
+def _emit_node(n):
+ tag = n.cls
+ # A DataTypeEncoding browses as "Default Binary" in namespace 0: the BrowseName is
+ # standard, not model-defined. Emitting it as 1:Default Binary is what every real
+ # companion NodeSet avoids, and tooling that resolves encodings by BrowseName
+ # cannot find it.
+ prefix = "" if n.attrs.get("BrowseNameNamespace") == 0 else f"{OWN_NS}:"
+ a = [f'{tag} NodeId="{T(n.nid)}"', f'BrowseName="{prefix}{sx.escape(n.bname)}"']
+ if "SymbolicName" in n.attrs:
+ a.append(f'SymbolicName="{sx.escape(n.attrs["SymbolicName"])}"')
+ if n.parent is not None:
+ a.append(f'ParentNodeId="{n.parent}"')
+ for k in ("DataType", "ValueRank", "ArrayDimensions"):
+ if k in n.attrs:
+ v = n.attrs[k]
+ if k == "DataType":
+ v = DATATYPE_ALIAS.get(v, v)
+ a.append(f'{k}="{v}"')
+ if n.cls in ("UAObjectType", "UADataType") and n.abstract:
+ a.append('IsAbstract="true"')
+ lines = [" <" + " ".join(a) + ">"]
+ lines.append(f" {sx.escape(n.display)}")
+ if n.desc:
+ lines.append(f" {sx.escape(n.desc)}")
+ if n.category:
+ lines.append(f" {sx.escape(n.category)}")
+ if n.cls == "UAReferenceType" and n.inverse:
+ lines.append(f" {sx.escape(n.inverse)}")
+ lines.append(" ")
+ for i in _sorted_refs(n.refs):
+ rt, tgt, fwd = n.refs[i]
+ fwd_s = "" if fwd else ' IsForward="false"'
+ lines.append(f' '
+ f'{tgt}')
+ lines.append(" ")
+ if n.definition:
+ lines.append(" " + n.definition)
+ if n.value:
+ lines.append(" " + n.value)
+ lines.append(f" {tag}>")
+ return "\n".join(lines)
+
+
+def emit():
+ out = ['',
+ '',
+ '',
+ ' ']
+ out += [f' {u}' for u in NAMESPACE_URIS]
+ out += [' ',
+ ' ',
+ f' ',
+ f' ',
+ f' ',
+ ' ',
+ ' ',
+ ' ']
+ for name, val in ALIASES:
+ out.append(f' {val}')
+ out.append(' ')
+ for nid in ORDER:
+ out.append(_emit_node(NODES[nid]))
+ out.append('')
+ return "\n".join(out) + "\n"
+
+
+def emit_csv():
+ # OPC Foundation NodeIds.csv format: SymbolicName,NodeId,NodeClass
+ return "\n".join(f"{NODES[nid].symbolic},{nid},{NODES[nid].cls[2:]}"
+ for nid in ORDER) + "\n"
+
+
+def _rule_name(nid):
+ for rt, tgt, fwd in NODES[nid].refs:
+ if rt == HasModellingRule:
+ return {MR_Mandatory: "Mandatory", MR_Optional: "Optional",
+ MR_OptionalPlaceholder: "OptionalPlaceholder",
+ MR_MandatoryPlaceholder: "MandatoryPlaceholder"}.get(tgt, "")
+ return ""
+
+
+def _supertype(nid):
+ for rt, tgt, fwd in NODES[nid].refs:
+ if rt == HasSubtype and not fwd:
+ return tgt
+ return ""
+
+
+BASE_TYPE_NAMES = {
+ "i=22": "Structure", "i=29": "Enumeration", "i=58": "BaseObjectType",
+ "i=61": "FolderType", "i=68": "PropertyType", "i=63": "BaseDataVariableType",
+ "i=17602": "BaseInterfaceType", "i=32": "NonHierarchicalReferences",
+ "i=76": "DataTypeEncodingType", "i=24": "BaseDataType",
+ "i=2391": "ProgramStateMachineType",
+}
+
+
+def _dt_name(dt):
+ """Render a DataType or supertype NodeId as a readable name."""
+ if not dt:
+ return ""
+ if dt in BASE_TYPE_NAMES:
+ return BASE_TYPE_NAMES[dt]
+ if dt in DATATYPE_ALIAS:
+ return DATATYPE_ALIAS[dt]
+ if dt.startswith(f"ns={OWN_NS};i="):
+ n = NODES.get(int(dt.split("=")[-1]))
+ if n is not None:
+ return n.bname
+ return dt
+
+
+def _rank(vr):
+ return {"-1": "Scalar", "1": "Array"}.get(str(vr), str(vr))
+
+
+def _members_of(nid):
+ """Instance declarations owned by a type, in declaration order."""
+ out = []
+ for m in ORDER:
+ n = NODES[m]
+ if n.parent == T(nid) and n.cls in ("UAVariable", "UAObject", "UAMethod"):
+ out.append(m)
+ return out
+
+
+def _method_args(nid, which):
+ for m in _members_of(nid):
+ n = NODES[m]
+ if n.bname == which and n.value:
+ names = re.findall(r"([^<]*)", n.value)
+ types = re.findall(r"([^<]*)", n.value)
+ types = [t for t in types if t != "i=297"]
+ descs = re.findall(r"([^<]*)", n.value)
+ ranks = re.findall(r"(-?\d+)", n.value)
+ out = []
+ for i, nm in enumerate(names):
+ out.append((nm,
+ _dt_name(types[i]) if i < len(types) else "",
+ "Array" if i < len(ranks) and ranks[i] != "-1" else "Scalar",
+ descs[i] if i < len(descs) else ""))
+ return out
+ return []
+
+
+def _esc(s):
+ return (s or "").replace("|", "\\|")
+
+
+def _cell(s):
+ """A description as ONE table cell.
+
+ A member description may hold paragraphs, and a raw newline inside a row ends the
+ row: everything after it renders as prose and the following member starts a second
+ table. Paragraph breaks become
, which keeps the structure and keeps the row
+ on one line. MD033 is off for exactly this reason.
+ """
+ return _esc(s).replace("\n\n", "
").replace("\n", " ")
+
+
+def emit_md():
+ """Annex A. This is the authoritative node reference, so it must carry everything an
+ implementer needs: DataType, ValueRank and ModellingRule for every member, the field
+ list of every structure, the value of every enumeration literal, and the full
+ signature of every Method. A bare NodeId/BrowseName table is not sufficient."""
+ obj_types = [n for n in ORDER if NODES[n].cls == "UAObjectType"]
+ data_types = [n for n in ORDER if NODES[n].cls == "UADataType"]
+ ref_types = [n for n in ORDER if NODES[n].cls == "UAReferenceType"]
+
+ L = ["# OPC UA — AI Model Management and Inference — Annex A: Information model (generated)",
+ "",
+ "> Generated by `build_model.py`. Do not edit by hand. Namespace "
+ f"`{NAMESPACE}` (index {OWN_NS}). NodeIds are provisional.",
+ "",
+ "This annex is the authoritative node reference for the specification: it "
+ "carries the DataType, ValueRank and ModellingRule of every member, the field "
+ "list of every structure, the value of every enumeration literal, and the full "
+ "signature of every Method.",
+ ""]
+
+ L += ["## A.1 Type overview", "",
+ "| NodeId | BrowseName | NodeClass | Subtype of |", "|---|---|---|---|"]
+ for nid in ref_types + obj_types + data_types:
+ n = NODES[nid]
+ L.append(f"| {T(nid)} | {n.bname} | {n.cls[2:]} | "
+ f"{_dt_name(_supertype(nid))} |")
+ L.append("")
+
+ L += ["## A.2 ReferenceTypes", "",
+ "| NodeId | BrowseName | InverseName | Subtype of | Description |",
+ "|---|---|---|---|---|"]
+ for nid in ref_types:
+ n = NODES[nid]
+ L.append(f"| {T(nid)} | {n.bname} | {n.inverse} | "
+ f"{_dt_name(_supertype(nid))} | {_cell(n.desc)} |")
+ L.append("")
+
+ L += ["## A.3 ObjectTypes", ""]
+ for nid in obj_types:
+ n = NODES[nid]
+ abstract = " (abstract)" if n.abstract else ""
+ L.append(f"### {n.bname}{abstract} — `{T(nid)}`")
+ L.append("")
+ L.append(f"*Subtype of:* `{_dt_name(_supertype(nid))}`")
+ L.append("")
+ if n.desc:
+ L.append(_esc(n.desc))
+ L.append("")
+ members = _members_of(nid)
+ variables = [m for m in members if NODES[m].cls in ("UAVariable", "UAObject")]
+ methods = [m for m in members if NODES[m].cls == "UAMethod"]
+ if variables:
+ L.append("| BrowseName | NodeClass | DataType | ValueRank | ModellingRule "
+ "| Description |")
+ L.append("|---|---|---|---|---|---|")
+ for m in variables:
+ mn = NODES[m]
+ dt = _dt_name(mn.attrs.get("DataType", ""))
+ vr = _rank(mn.attrs.get("ValueRank", "-1")) if mn.cls == "UAVariable" else ""
+ L.append(f"| {mn.bname} | {mn.cls[2:]} | {dt} | {vr} | "
+ f"{_rule_name(m)} | {_cell(mn.desc)} |")
+ L.append("")
+ for m in methods:
+ mn = NODES[m]
+ L.append(f"**Method `{mn.bname}`** ({_rule_name(m)}) — {_esc(mn.desc)}")
+ L.append("")
+ for which, label in (("InputArguments", "In"),
+ ("OutputArguments", "Out")):
+ args = _method_args(m, which)
+ if not args:
+ continue
+ L.append(f"| {label} | DataType | ValueRank | Meaning |")
+ L.append("|---|---|---|---|")
+ for (an, at, ar, ad) in args:
+ L.append(f"| {an} | {at} | {ar} | {_cell(ad)} |")
+ L.append("")
+ if not _method_args(m, "InputArguments") and \
+ not _method_args(m, "OutputArguments"):
+ L.append("Takes no arguments and returns none.")
+ L.append("")
+
+ L += ["## A.4 DataTypes", ""]
+ for nid in data_types:
+ n = NODES[nid]
+ defn = n.definition or ""
+ is_enum = 'Value="' in defn
+ L.append(f"### {n.bname} — `{T(nid)}`")
+ L.append("")
+ L.append(f"*Subtype of:* `{_dt_name(_supertype(nid))}`")
+ L.append("")
+ if n.desc:
+ L.append(_esc(n.desc))
+ L.append("")
+ if is_enum:
+ L.append("| Name | Value | Description |")
+ L.append("|---|---|---|")
+ for mm in re.finditer(
+ r'|>'
+ r'(?:([^<]*))?)', defn):
+ L.append(f"| {mm.group(1)} | {mm.group(2)} | "
+ f"{_cell(mm.group(3) or '')} |")
+ else:
+ L.append("| Field | DataType | ValueRank | ArrayDimensions | Description |")
+ L.append("|---|---|---|---|---|")
+ for mm in re.finditer(
+ r']*?)(?:/>|>'
+ r'(?:([^<]*))?)', defn):
+ extra = mm.group(3) or ""
+ vr = re.search(r'ValueRank="(-?\d+)"', extra)
+ ad = re.search(r'ArrayDimensions="(\d+)"', extra)
+ L.append(f"| {mm.group(1)} | {_dt_name(mm.group(2))} | "
+ f"{_rank(vr.group(1)) if vr else 'Scalar'} | "
+ f"{ad.group(1) if ad else ''} | "
+ f"{_cell(mm.group(4) or '')} |")
+ L.append("")
+
+ return "\n".join(L).rstrip() + "\n"
+
+
+def main():
+ here = os.path.dirname(os.path.abspath(__file__))
+ std = os.path.normpath(os.path.join(here, "..", "..", "..", "ai-model-management"))
+ os.makedirs(std, exist_ok=True)
+ with open(os.path.join(std, "Opc.Ua.AiModelManagement.NodeSet2.xml"), "w",
+ encoding="utf-8", newline="\n") as f:
+ f.write(emit())
+ with open(os.path.join(std, "Opc.Ua.AiModelManagement.NodeIds.csv"), "w",
+ encoding="utf-8", newline="\n") as f:
+ f.write(emit_csv())
+ with open(os.path.join(here, "model-reference.md"), "w",
+ encoding="utf-8", newline="\n") as f:
+ f.write(emit_md())
+ n_types = sum(1 for k in NODES
+ if NODES[k].cls in ("UAObjectType", "UADataType", "UAReferenceType"))
+ print(f"Wrote NodeSet ({len(ORDER)} nodes, {n_types} types), NodeIds.csv, "
+ "model-reference.md")
+ print(f"Member id range: 6001..{_next_member[0] - 1}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/metaverse-specs/extras/ai-model-management/tools/model-reference.md b/metaverse-specs/extras/ai-model-management/tools/model-reference.md
new file mode 100644
index 0000000..6bd177e
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/tools/model-reference.md
@@ -0,0 +1,782 @@
+# OPC UA — AI Model Management and Inference — Annex A: Information model (generated)
+
+> Generated by `build_model.py`. Do not edit by hand. Namespace `http://opcfoundation.org/UA/AI/` (index 2). NodeIds are provisional.
+
+This annex is the authoritative node reference for the specification: it carries the DataType, ValueRank and ModellingRule of every member, the field list of every structure, the value of every enumeration literal, and the full signature of every Method.
+
+## A.1 Type overview
+
+| NodeId | BrowseName | NodeClass | Subtype of |
+|---|---|---|---|
+| ns=2;i=4001 | UsesModel | ReferenceType | NonHierarchicalReferences |
+| ns=2;i=4002 | TrainedOn | ReferenceType | NonHierarchicalReferences |
+| ns=2;i=4003 | DerivedFrom | ReferenceType | NonHierarchicalReferences |
+| ns=2;i=4004 | FallsBackTo | ReferenceType | NonHierarchicalReferences |
+| ns=2;i=4005 | ImportedFrom | ReferenceType | NonHierarchicalReferences |
+| ns=2;i=4006 | EvaluatedBy | ReferenceType | NonHierarchicalReferences |
+| ns=2;i=1001 | AiRootType | ObjectType | BaseObjectType |
+| ns=2;i=1002 | ModelType | ObjectType | BaseObjectType |
+| ns=2;i=1003 | DatasetType | ObjectType | BaseObjectType |
+| ns=2;i=1004 | DeploymentType | ObjectType | BaseObjectType |
+| ns=2;i=1005 | LearningJobType | ObjectType | AiJobType |
+| ns=2;i=1006 | AiJobType | ObjectType | ProgramStateMachineType |
+| ns=2;i=1007 | ModelImportJobType | ObjectType | AiJobType |
+| ns=2;i=1008 | InferenceJobType | ObjectType | AiJobType |
+| ns=2;i=1009 | ModelSourceType | ObjectType | BaseObjectType |
+| ns=2;i=1014 | EvaluationRunType | ObjectType | BaseObjectType |
+| ns=2;i=1015 | ModelCardType | ObjectType | BaseObjectType |
+| ns=2;i=1010 | ModelRegistryType | ObjectType | ns=1;i=63000 |
+| ns=2;i=1011 | ModelPublisherType | ObjectType | ns=1;i=63001 |
+| ns=2;i=1016 | AiResourceType | ObjectType | ns=1;i=63002 |
+| ns=2;i=1012 | ModelResourceType | ObjectType | AiResourceType |
+| ns=2;i=1013 | DatasetResourceType | ObjectType | AiResourceType |
+| ns=2;i=1017 | InferenceTransferType | ObjectType | BaseObjectType |
+| ns=2;i=3001 | InferenceLocationEnum | DataType | Enumeration |
+| ns=2;i=3002 | AcceleratorKindEnum | DataType | Enumeration |
+| ns=2;i=3003 | DeploymentStateEnum | DataType | Enumeration |
+| ns=2;i=3004 | DatasetSourceEnum | DataType | Enumeration |
+| ns=2;i=3005 | LearningJobStateEnum | DataType | Enumeration |
+| ns=2;i=3006 | FinishReasonEnum | DataType | Enumeration |
+| ns=2;i=3007 | ApiDialectEnum | DataType | Enumeration |
+| ns=2;i=3008 | AuthenticationKindEnum | DataType | Enumeration |
+| ns=2;i=3009 | FallbackPolicyEnum | DataType | Enumeration |
+| ns=2;i=3010 | VersionBindingEnum | DataType | Enumeration |
+| ns=2;i=3011 | ImportModeEnum | DataType | Enumeration |
+| ns=2;i=3012 | SafetySeverityEnum | DataType | Enumeration |
+| ns=2;i=3013 | ReachabilityEnum | DataType | Enumeration |
+| ns=2;i=3014 | TransferStateEnum | DataType | Enumeration |
+| ns=2;i=3015 | DigestProvenanceEnum | DataType | Enumeration |
+| ns=2;i=3050 | TensorSignatureDataType | DataType | Structure |
+| ns=2;i=3051 | ModelReferenceDataType | DataType | Structure |
+| ns=2;i=3052 | UsageDataType | DataType | Structure |
+| ns=2;i=3053 | CapabilityDataType | DataType | Structure |
+| ns=2;i=3054 | SafetyAssessmentDataType | DataType | Structure |
+| ns=2;i=3055 | EvaluationMetricDataType | DataType | Structure |
+| ns=2;i=3056 | RateLimitDataType | DataType | Structure |
+
+## A.2 ReferenceTypes
+
+| NodeId | BrowseName | InverseName | Subtype of | Description |
+|---|---|---|---|---|
+| ns=2;i=4001 | UsesModel | IsUsedByDeployment | NonHierarchicalReferences | Links a Deployment to the Model it executes. Clause 6.5 requires exactly one such reference per deployment; it is the only defined path from a result to the model artefact and its Digest, on which the provenance requirement of clause 12 depends. |
+| ns=2;i=4002 | TrainedOn | IsTrainingDataFor | NonHierarchicalReferences | Links a Model to a Dataset it was trained or validated on. A model whose training data cannot be named is a model whose behaviour cannot be explained, which is why this reference exists rather than a string. |
+| ns=2;i=4003 | DerivedFrom | IsBaseOfModel | NonHierarchicalReferences | Links a Model to the Model it was fine-tuned, distilled or quantized from. Lineage is a chain, not a field: a model three derivations from its base is answerable for all three, and a string naming the immediate parent cannot be walked. |
+| ns=2;i=4004 | FallsBackTo | IsFallbackFor | NonHierarchicalReferences | Links a Deployment to the Deployment that serves in its place when it cannot. Clause 9 forbids a cycle, and requires the response to say which deployment actually answered. |
+| ns=2;i=4005 | ImportedFrom | WasImportedAs | NonHierarchicalReferences | Links a Model to the catalogue resource an import job materialized it from. This is what makes 'where did this model come from' answerable after the fact, rather than only at the moment of import. |
+| ns=2;i=4006 | EvaluatedBy | Evaluates | NonHierarchicalReferences | Links a Model to an EvaluationRun that measured it. Optional and repeating: a model may be evaluated many times, and the run that gated its promotion is not necessarily the last one. |
+
+## A.3 ObjectTypes
+
+### AiRootType — `ns=2;i=1001`
+
+*Subtype of:* `BaseObjectType`
+
+Server-level entry point. A client that has just connected browses here to find every model, dataset, deployment and learning job the Server describes, without knowing its layout.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| Models | Object | | | Mandatory | ModelType instances. |
+| Datasets | Object | | | Optional | DatasetType instances. |
+| Deployments | Object | | | Mandatory | DeploymentType instances. |
+| LearningJobs | Object | | | Optional | LearningJobType instances. |
+| SpecificationVersion | Variable | String | Scalar | Mandatory | Release of this specification the Server implements, for example '0.1.0'. |
+| Sources | Object | | | Optional | ModelSourceType instances - the externally hosted endpoints and catalogues this Server can reach. |
+| Registries | Object | | | Optional | ModelRegistryType instances this Server serves or mirrors. |
+| Evaluations | Object | | | Optional | EvaluationRunType instances. |
+| Jobs | Object | | | Optional | Import and asynchronous inference jobs. Learning jobs remain under LearningJobs. |
+
+### ModelType — `ns=2;i=1002`
+
+*Subtype of:* `BaseObjectType`
+
+Nameplate of a trained model. The member set is deliberately aligned with the IDTA 02060 AI Model Nameplate submodel template, which is currently the only standardised description of an industrial AI model, so an Asset Administration Shell can be populated from this node without loss.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| ModelId | Variable | String | Scalar | Mandatory | Identifier of the model. |
+| Name | Variable | LocalizedText | Scalar | Mandatory | Human-readable model name. Its Text SHALL be the name the source system uses for the model, carried across unchanged. A LocalizedText because the base model types names that way and retyping it would break every implementation, but the localizable part is the presentation: a Server MAY add a translation for display and SHALL NOT translate, reformat or prettify the Text itself. Two Servers that fetched one model from two mirrors are meant to produce the same string, and a name adjusted for house style is a name that no longer matches. |
+| Version | Variable | String | Scalar | Mandatory | Model version. |
+| Framework | Variable | String | Scalar | Optional | Producing framework, for example PyTorch, TensorFlow or scikit-learn. |
+| Format | Variable | String | Scalar | Optional | Serialization format, for example ONNX, TensorRT or OpenVINO IR. |
+| TaskKind | Variable | String | Scalar | Optional | What the model does, for example Detection2D, Classification, Segmentation, Forecasting or AnomalyDetection. Free text because the set of tasks is not closed and a closed enumeration would date faster than the model does. |
+| Digest | Variable | ByteString | Scalar | Mandatory | Cryptographic digest of the model artefact, for provenance and integrity. Mandatory: clause 12 requires it for every model whose artefact is obtainable through ArtifactUri, and it is the terminus of the provenance chain that UsesModel keeps intact. |
+| DigestAlgorithm | Variable | String | Scalar | Mandatory | Hash function used for Digest. SHALL name a function with at least 256-bit output and no known collision weakness; SHA-256 is the default and is always acceptable. SHALL NOT be MD5, SHA-1 or a truncated variant - chosen-prefix collisions against those are practical, so a substituted artefact would pass verification. SHALL be non-empty where Digest is non-empty. See clause 12. |
+| ArtifactUri | Variable | String | Scalar | Optional | Where the model artefact can be obtained. Treated as untrusted input. |
+| ProvenanceUri | Variable | String | Scalar | Optional | Training provenance or model card location. |
+| LabelClasses | Variable | String | Array | Optional | Ordered class label set, where the model produces classified output. The INDEX is what a consuming specification's class identifier refers to, so the order is part of the contract and a Server SHALL NOT reorder it in place. |
+| Inputs | Variable | TensorSignatureDataType | Array | Optional | Input tensor signatures. |
+| Outputs | Variable | TensorSignatureDataType | Array | Optional | Output tensor signatures. |
+| Card | Object | | | Optional | What a human needs to decide whether this model may run here. |
+| Publisher | Variable | String | Scalar | Optional | Organisation or namespace that published the model. With Name and Version this is the triple every catalogue identifies a model by, and it is what makes the same model recognisable across two installations that fetched it from different mirrors. |
+| ParameterCount | Variable | UInt64 | Scalar | Optional | Parameters in the model, or 0 where not published. A crude but universally available proxy for what it will cost to run. |
+| Quantization | Variable | String | Scalar | Optional | Numeric precision the artefact is stored in, for example 'fp32', 'int8' or 'fp8'. A quantized model is a DIFFERENT artefact with different behaviour, not a packaging detail, which is why it is stated rather than left to the format string. |
+| SafetyPolicyUri | Variable | String | Scalar | Optional | Safety or content policy applied to this model's output, where one is. Untrusted input, subject to clause 12. |
+| DigestProvenance | Variable | DigestProvenanceEnum | Scalar | Mandatory | Where Digest came from, or why there is none. NotAvailable is the only value permitted with an empty Digest, and it SHALL be used rather than leaving a client to guess whether the source publishes no digest or this Server declined to carry one.
A Server SHALL NOT put a non-content identifier in Digest to avoid saying NotAvailable. A response fingerprint, a resource name, a storage entity tag and a repository commit identifier are none of them digests of the artefact that ran, and a client that verified against one would believe it had checked something it had not. Where such an identifier is worth publishing it belongs in ArtifactUri or ProvenanceUri, which promise nothing about content. |
+| PublishedAt | Variable | UtcTime | Scalar | Optional | When the source first published this model, where the source states it. The same question DatasetType.CreatedAt answers for a dataset, and the same reason: a model trained before a process change may no longer represent the line it runs on, and Version is a vendor string that often cannot be ordered.
This is the source's date, not when this Server learned of it - a Server SHALL NOT substitute its own acquisition time, which would make every model appear to date from the last restart. |
+| LastModifiedAt | Variable | UtcTime | Scalar | Optional | When the artefact behind this model last changed at the source.
It exists for the FollowsRef case of clause 9.3, where the artefact can change with nothing else changing. Clause 12.3.1 requires repointing to be treated as an authorization-bearing act and points at AiJobType.RequestedBy for the record - but a reference that moves AT THE SOURCE produces no job, so without this member the audit trail that clause demands cannot be constructed on the one path it exists to cover. A Server that follows a mutable reference SHALL populate it. |
+
+### DatasetType — `ns=2;i=1003`
+
+*Subtype of:* `BaseObjectType`
+
+A dataset used to train or validate a model. Aligned with the IDTA 02058 AI Dataset submodel template. SourceKind distinguishes real capture from simulator output, which is the provenance a reviewer needs when synthetic data is involved.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| DatasetId | Variable | String | Scalar | Mandatory | Identifier of the dataset. |
+| Name | Variable | LocalizedText | Scalar | Optional | Human-readable dataset name. |
+| Version | Variable | String | Scalar | Optional | Dataset version. |
+| SourceKind | Variable | DatasetSourceEnum | Scalar | Mandatory | Whether samples are real, synthetic or mixed. |
+| SampleCount | Variable | UInt64 | Scalar | Optional | Number of samples. |
+| LabelClasses | Variable | String | Array | Optional | Class labels present. |
+| CreatedAt | Variable | UtcTime | Scalar | Optional | Creation time. |
+| ArtifactUri | Variable | String | Scalar | Optional | Where the dataset can be obtained. Treated as untrusted input. |
+| Digest | Variable | ByteString | Scalar | Optional | Digest of the dataset artefact. |
+
+### DeploymentType — `ns=2;i=1004`
+
+*Subtype of:* `BaseObjectType`
+
+A model made executable somewhere. Aligned with the IDTA 02059 AI Deployment submodel template. InferenceLocation is the on-server versus off-server switch: it changes where the computation happens and therefore the trust boundary, and it changes nothing else.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| DeploymentId | Variable | String | Scalar | Mandatory | Identifier of the deployment. |
+| InferenceLocation | Variable | InferenceLocationEnum | Scalar | Mandatory | Where inference executes. |
+| AcceleratorKind | Variable | AcceleratorKindEnum | Scalar | Optional | Compute device executing the model. |
+| AcceleratorName | Variable | String | Scalar | Optional | Free-text accelerator identification, for example an NPU or GPU part name. |
+| EndpointUri | Variable | String | Scalar | Optional | Inference endpoint when InferenceLocation is not OnServer. Treated as untrusted input and subject to the resolver policy of clause 12. |
+| LatencyBudget | Variable | Duration | Scalar | Optional | Latency the deployment is expected to meet. Set by whoever commissioned the deployment; ObservedLatency is what it actually achieved, and clause 6.4.3 compares the two. |
+| BatchSize | Variable | UInt32 | Scalar | Optional | Configured inference batch size. |
+| State | Variable | DeploymentStateEnum | Scalar | Mandatory | Runtime state of the deployment. |
+| Source | Variable | NodeId | Scalar | Optional | ModelSourceType instance this deployment executes through, where inference is not local. Null when InferenceLocation is OnServer. |
+| VersionBinding | Variable | VersionBindingEnum | Scalar | Mandatory | Whether the deployment is pinned to an immutable model version or follows a mutable pointer. |
+| BoundRef | Variable | String | Scalar | Optional | The mutable pointer being followed, where VersionBinding is FollowsRef. Empty when Pinned. |
+| FallbackPolicy | Variable | FallbackPolicyEnum | Scalar | Mandatory | What the Server does when this deployment cannot serve. |
+| Reachability | Variable | ReachabilityEnum | Scalar | Optional | Whether the execution site is currently reachable. Always Reachable for an OnServer deployment that is not Faulted. |
+| ConsecutiveFailures | Variable | UInt32 | Scalar | Optional | Failed calls since the last success. |
+| LastSuccessAt | Variable | UtcTime | Scalar | Optional | When this deployment last answered successfully. With FallbackPolicy HoldLast this is how a caller judges whether the held answer is still worth having. |
+| RateLimit | Variable | RateLimitDataType | Scalar | Optional | Capacity the execution site is currently granting. |
+| Capabilities | Variable | CapabilityDataType | Array | Optional | What this deployment can do. A client checks here before calling a typed profile rather than discovering the answer from a rejection. |
+| DataJurisdiction | Variable | String | Scalar | Mandatory | Where input data is processed, named in whatever scheme the operator uses - a site, a legal jurisdiction, or a named zone. This is the question a plant actually asks, and no amount of latency or accuracy data answers it. |
+| EgressPermitted | Variable | Boolean | Scalar | Mandatory | Whether calling this deployment sends input data outside the operator's boundary. A Server SHALL set this true for every deployment whose InferenceLocation is Cloud, and SHALL NOT set it false merely because the channel is encrypted - the question is where the data goes, not who can read it in flight. |
+| RetainsInput | Variable | Boolean | Scalar | Optional | Whether the execution site retains input beyond serving the request, for example for provider-side logging or training. Unknown is not a value: a Server that cannot establish this SHALL report true, because the safe assumption is the one that keeps data in. |
+| EgressPolicyUri | Variable | String | Scalar | Optional | Where the governing data policy is documented. |
+| MaxInlinePayloadSize | Variable | UInt32 | Scalar | Mandatory | Largest request or response this deployment will carry inline through Invoke, in bytes. Zero means the deployment accepts no inline payload at all and BeginTransfer is the only way in.
A client reads this BEFORE calling rather than discovering the bound from a rejection, and a Server SHALL NOT publish a value larger than its own MaxByteStringLength, the negotiated MaxMessageSize or the Session's MaxResponseMessageSize permit - the smallest of those is the real limit and a client cannot see all of them. |
+| ApiDialect | Variable | ApiDialectEnum | Scalar | Optional | The contract a client's Payload must satisfy when calling Invoke on this deployment. RestChatCompletions means the Payload is a chat-completions request body; OpenInferenceProtocol means it is an OIP inference body; EmbeddedRuntime and TensorRemoteProcedure name the tensor contracts described by Inputs and Outputs; Proprietary means the contract is named only by EndpointDescriptionUri.
This does not type the payload - clause 8.2 keeps it opaque and that is unchanged. It names WHICH contract the opaque bytes are expected to satisfy, which is what a client browsing an unfamiliar deployment needs before it can send anything at all. |
+| EndpointDescriptionUri | Variable | String | Scalar | Optional | Where the request and response contract for this deployment is documented. Untrusted input, subject to clause 12.2. Required in practice wherever ApiDialect is Proprietary, because nothing else then says what to send. |
+| RuntimeIdentity | Variable | String | Scalar | Optional | Opaque identifier of the serving configuration currently behind this deployment - a serving-stack fingerprint, an engine profile, a container image digest. Compared for equality and never parsed, on the same terms as Digest.
It is not the model. The same artefact served by two runtime builds can produce different numbers, and where the execution site publishes such an identity it is the only thing that records the difference. A change to it under a Pinned binding IS the observable change to the deployment that clause 9.3 says a pinned artefact cannot move without. |
+| ObservedLatency | Variable | Duration | Scalar | Optional | Most recent inference latency this Server measured for this deployment.
LatencyBudget states what the deployment is expected to meet, and clause 6.4.3 makes Degraded the state of a deployment that is answering but missing it. Without a measurement the comparison has no published input, so the state transition could not be checked against a Server that claimed it. A Server that reports Degraded on latency grounds SHALL populate this. |
+
+**Method `Invoke`** (Optional) — Run inference and return the result. The payload is opaque here: what goes in and comes out is the consuming specification's vocabulary, and an envelope that tried to type it would have to be extended for every domain. What this Method fixes is everything AROUND the payload - routing, parameters, accounting, why it stopped, and which model actually ran.
+
+The signature does not change with InferenceLocation. A deployment served from the Server's own process and one served from a remote service are called identically; the location changes the trust boundary and the latency, and nothing else.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Payload | ByteString | Scalar | Request body. |
+| PayloadUri | String | Scalar | Location the request body is read from, where it is supplied by reference rather than carried. A Server SHALL accept exactly one of Payload and PayloadUri and SHALL reject a call supplying both or neither. Untrusted input subject to clause 12.2, and named data the execution site will read, so clause 9.5 applies to it. |
+| ContentType | String | Scalar | Media type of Payload. |
+| Parameters | KeyValuePair | Array | Call parameters such as a sampling temperature or an output length bound. A Server SHALL reject a parameter it does not support rather than ignore it: a caller whose parameter was silently dropped believes it took effect. |
+| Timeout | Duration | Scalar | How long the caller will wait. Zero means the Server's default. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| ResponsePayload | ByteString | Scalar | Response body. |
+| ResponseContentType | String | Scalar | Media type of ResponsePayload. |
+| ModelUsed | NodeId | Scalar | The model that ACTUALLY produced this response. Not necessarily the one the deployment names now: a fallback answered from a different deployment, and a FollowsRef binding may have moved. The provenance chain of clause 12 walks this. |
+| Usage | UsageDataType | Scalar | What the call consumed. |
+| FinishReason | FinishReasonEnum | Scalar | Why output stopped. A caller that ignores this will accept a truncated answer as a complete one. |
+| SafetyAssessment | SafetyAssessmentDataType | Array | Findings from the safety policy, if any applied. |
+| RetryAfter | Duration | Scalar | How long to wait before retrying, where the failure was a capacity one. Zero when retrying immediately is as good as waiting, and meaningless when the failure was not retryable. |
+| TransferRequired | Boolean | Scalar | True when the deployment produced a response too large to return inline. ResponsePayload is then empty and the work is NOT lost - Transfer names where to read it. A client that ignores this reads an empty payload and concludes the model returned nothing. |
+| Transfer | NodeId | Scalar | InferenceTransferType instance holding the response, where TransferRequired is true. Null otherwise. |
+
+**Method `InvokeAsync`** (Optional) — Submit inference to be completed later, returning immediately with the job that will carry the result. For work that does not finish while a caller waits - a batch scored overnight, an analysis over recorded data.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Payload | ByteString | Scalar | Request body. |
+| PayloadUri | String | Scalar | Location the request body is read from, where it is supplied by reference rather than carried. Exactly one of Payload and PayloadUri on the same terms as Invoke. This is the argument that lets a batch already sitting in the plant's object store be scored without being copied through the Session first. |
+| ContentType | String | Scalar | Media type of Payload. |
+| Parameters | KeyValuePair | Array | Call parameters. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Job | NodeId | Scalar | InferenceJobType instance tracking the request. The caller subscribes to it rather than polling. |
+
+**Method `GetCapabilities`** (Optional) — Report what this deployment can do, refreshed from the execution site rather than from cache. Defined because a remote endpoint's capabilities change without anything in this address space changing.
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Capabilities | CapabilityDataType | Array | Current capabilities. |
+
+**Method `BeginTransfer`** (Optional) — Opens a chunked exchange for a payload that will not fit inline, returning the InferenceTransferType instance to write into. This is the general path: Invoke is the shortcut that happens to work when everything is small.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| ContentType | String | Scalar | Media type of the request body. |
+| RequestSize | UInt64 | Scalar | Expected request size in bytes, or 0 when not known in advance. A Server that cannot accommodate the stated size refuses here rather than after the client has uploaded it. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Transfer | NodeId | Scalar | InferenceTransferType instance to write the request into. |
+| Accepted | Boolean | Scalar | False when the Server declined to open the exchange. |
+
+### LearningJobType — `ns=2;i=1005`
+
+*Subtype of:* `AiJobType`
+
+One turn of the capture, label, train and promote loop. It exists so that corrections arriving from a consuming application have somewhere to accumulate and a defined path into a new model version. A Server may implement only the capture stages and leave training to an external MLOps system - the state machine is the same either way.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| State | Variable | LearningJobStateEnum | Scalar | Mandatory | Current stage of the loop. This is the PHASE, not the program lifecycle: the inherited CurrentState says whether the job is running, this says what it is doing. Clause 7 requires the two to agree. |
+| Dataset | Variable | NodeId | Scalar | Optional | Dataset being accumulated or used. |
+| BaseModel | Variable | NodeId | Scalar | Optional | Model the job starts from. |
+| CandidateModel | Variable | NodeId | Scalar | Optional | Model produced by the job, awaiting promotion. |
+| SamplesCollected | Variable | UInt64 | Scalar | Optional | Samples accumulated so far, including corrections fed back. |
+
+**Method `StartCollection`** (Optional) — Begin accumulating samples and corrections into the dataset.
+
+Takes no arguments and returns none.
+
+**Method `StopCollection`** (Optional) — Stop accumulating samples.
+
+Takes no arguments and returns none.
+
+**Method `TriggerTraining`** (Optional) — Request that a candidate model be trained from the collected dataset.
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when the request was queued. |
+
+**Method `PromoteModel`** (Optional) — Promote the candidate model so that deployments begin using it. A Server SHALL require a distinct authorization for this Method: it changes what the equipment does without changing anything a reader of the address space would notice, which is precisely the change that needs a separate permission.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Deployment | NodeId | Scalar | Deployment to update, or null for all. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| PromotedModel | NodeId | Scalar | The model now in use. |
+
+### AiJobType (abstract) — `ns=2;i=1006`
+
+*Subtype of:* `ProgramStateMachineType`
+
+Abstract base of every long-running AI operation: learning, model import and asynchronous inference. It derives from the OPC 10000-10 ProgramStateMachineType, so the lifecycle - Ready, Running, Suspended, Halted - its transition events and its Start/Suspend/Resume/Halt Methods are inherited rather than reinvented, and every job in this model is auditable the same way.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| JobId | Variable | String | Scalar | Mandatory | Identifier of the job, unique within the Server. |
+| LastError | Variable | LocalizedText | Scalar | Optional | Diagnostic for the most recent failure. For a human; SHALL NOT be parsed. |
+| StartedAt | Variable | UtcTime | Scalar | Optional | When the job last entered Running. |
+| FinishedAt | Variable | UtcTime | Scalar | Optional | When the job last left Running, or null while it is running. |
+| Progress | Variable | Double | Scalar | Optional | Fraction complete, 0.0 to 1.0, or null where the job cannot estimate it. A Server SHALL NOT report a value it is guessing: null is informative, a fabricated 0.5 is not. |
+| RequestedBy | Variable | String | Scalar | Optional | Identity that requested the job, recorded at the moment it started. Clause 12 requires this for any job that can promote a model. |
+
+### ModelImportJobType — `ns=2;i=1007`
+
+*Subtype of:* `AiJobType`
+
+Brings a model from a catalogue into this Server. It federates by default - materializing the catalogue entry as a ModelType whose artefact stays where it is - and stages the artefact when the target deployment could not otherwise reach it. Staging is the moment a substituted artefact would enter, which is why clause 10 requires the Digest to be verified there and nowhere else.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| Source | Variable | NodeId | Scalar | Mandatory | ModelSourceType instance the model is pulled from, where the import calls an endpoint. Null where the import reads a catalogue instead, in which case Registry names it. Exactly one of the two is non-null. |
+| ModelReference | Variable | ModelReferenceDataType | Scalar | Mandatory | Publisher, name and version being imported. |
+| Mode | Variable | ImportModeEnum | Scalar | Mandatory | Whether to federate, stage, or decide from the target's InferenceLocation. |
+| TargetDeployment | Variable | NodeId | Scalar | Optional | Deployment to create or update on success, or null to import the model without deploying it. |
+| ImportedModel | Variable | NodeId | Scalar | Optional | ModelType instance the job produced. Null until the job succeeds. |
+| BytesTransferred | Variable | UInt64 | Scalar | Optional | Artefact bytes fetched so far. Zero for a federating import, which moves none. |
+| DigestVerified | Variable | Boolean | Scalar | Optional | Whether the staged artefact's computed digest matched the one the catalogue declared. False on a staging import means the artefact SHALL NOT be deployed. |
+| Registry | Variable | NodeId | Scalar | Optional | ModelRegistryType instance the model is imported from, where the import reads a catalogue rather than calling an endpoint. Null otherwise.
A Server SHALL populate exactly one of Source and Registry, and SHALL leave the other null. The two name the two things an import can read from, and a job that named both would not say which one produced the artefact whose digest clause 10.4 verifies. |
+
+**Method `Cancel`** (Optional) — Abandon the import. A partially staged artefact SHALL be discarded rather than left where a later deployment could pick it up.
+
+Takes no arguments and returns none.
+
+### InferenceJobType — `ns=2;i=1008`
+
+*Subtype of:* `AiJobType`
+
+One asynchronous inference request. It exists because not every inference returns while the caller waits: a batch scored overnight and a long analysis over recorded data are ordinary industrial cases, and modelling them as a Method that blocks for hours is not.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| Deployment | Variable | NodeId | Scalar | Mandatory | Deployment executing the request. |
+| RequestPayload | Variable | ByteString | Scalar | Optional | Request body, encoded as RequestContentType states. |
+| RequestContentType | Variable | String | Scalar | Optional | Media type of RequestPayload. |
+| ResponsePayload | Variable | ByteString | Scalar | Optional | Response body once the job succeeds. |
+| ResponseContentType | Variable | String | Scalar | Optional | Media type of ResponsePayload. |
+| ModelUsed | Variable | NodeId | Scalar | Optional | Model that ACTUALLY executed the request, which is not always the one the deployment named when the job was submitted - a fallback or a followed reference can change it in between. The provenance chain of clause 12 walks this, not the deployment's current model. |
+| Usage | Variable | UsageDataType | Scalar | Optional | What the call consumed. |
+| FinishReason | Variable | FinishReasonEnum | Scalar | Optional | Why the call stopped producing output. |
+| SafetyAssessment | Variable | SafetyAssessmentDataType | Array | Optional | Findings from the safety policy, if any were applied. |
+| RequestUri | Variable | String | Scalar | Optional | Where the request body was read from, where it was supplied by reference rather than carried. Untrusted input under clause 12.2, and an egress path under clause 9.5. |
+| ResponseUri | Variable | String | Scalar | Optional | Where the result was written, where the execution site returns a location rather than bytes. Empty when the response is carried inline or through Transfer. |
+| TransferRequired | Variable | Boolean | Scalar | Optional | True when the job produced a response too large to carry inline. ResponsePayload is then empty and the work is NOT lost - Transfer names where to read it. |
+| Transfer | Variable | NodeId | Scalar | Optional | InferenceTransferType instance holding the response, where TransferRequired is true. Null otherwise.
Invoke carries the same pair, and the asymmetry would otherwise leave the jobs most likely to produce a large result - a batch scored overnight, an analysis over recorded data - bounded by exactly the three limits clause 8.2.4 says this model does not get to choose. |
+
+### ModelSourceType — `ns=2;i=1009`
+
+*Subtype of:* `BaseObjectType`
+
+An externally hosted inference or catalogue endpoint this Server can reach. It carries everything needed to actually call something the Server did not deploy - the wire contract, how to authenticate, what the endpoint can do and whether it is answering - because a URI on its own is a string nobody can act on.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| SourceId | Variable | String | Scalar | Mandatory | Identifier of the source. |
+| EndpointUri | Variable | String | Scalar | Mandatory | Base URI of the endpoint. Untrusted input, subject to the resolver policy of clause 12. |
+| ApiDialect | Variable | ApiDialectEnum | Scalar | Mandatory | Wire contract the endpoint speaks. |
+| EndpointDescriptionUri | Variable | String | Scalar | Optional | Where the contract is documented. SHOULD be populated when ApiDialect is Proprietary, because otherwise nothing in the address space says how to call it. |
+| AuthenticationKind | Variable | AuthenticationKindEnum | Scalar | Mandatory | How the Server authenticates itself to the endpoint. |
+| CredentialReference | Variable | String | Scalar | Optional | Opaque handle naming the credential in whatever store the Server uses. It is a NAME, never a secret: clause 12 forbids a Server from exposing credential material through any Attribute of this model, and a client that can read this value learns only which credential is used, not what it is. |
+| TokenAudience | Variable | String | Scalar | Optional | Audience or scope a bearer token is requested for, where AuthenticationKind is BearerToken. |
+| Reachability | Variable | ReachabilityEnum | Scalar | Mandatory | Whether the Server can currently reach the endpoint. |
+| LastSuccessAt | Variable | UtcTime | Scalar | Optional | When the endpoint last answered successfully. |
+| ConsecutiveFailures | Variable | UInt32 | Scalar | Optional | Failures since the last success. Reset to zero on success. |
+| RateLimit | Variable | RateLimitDataType | Scalar | Optional | Capacity the endpoint is currently granting. |
+| Capabilities | Variable | CapabilityDataType | Array | Optional | What the endpoint reports it can do. |
+
+**Method `TestConnection`** (Optional) — Probe the endpoint and update Reachability. Defined so that a commissioning engineer can establish that credentials and network policy are right BEFORE a deployment depends on them, rather than discovering it from a failed inference.
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Reachable | Boolean | Scalar | Whether the probe succeeded. |
+| Detail | LocalizedText | Scalar | Diagnostic. For a human. |
+
+**Method `ListModels`** (Optional) — Enumerate the models the source offers.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Filter | String | Scalar | Optional substring or expression; empty for all. |
+| MaxResults | UInt32 | Scalar | Upper bound on returned entries. |
+| ContinuationPoint | ByteString | Scalar | Empty on the first call; otherwise the value the previous call returned. A cap without a cursor bounds the response and puts every entry past it out of reach, which against a public catalogue means most of them. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Models | ModelReferenceDataType | Array | Publisher, name and version of each model offered. |
+| ContinuationPoint | ByteString | Scalar | Pass to the next call to continue. Empty when the enumeration is complete, which is how a client knows to stop rather than by comparing counts. |
+
+### EvaluationRunType — `ns=2;i=1014`
+
+*Subtype of:* `BaseObjectType`
+
+One measurement of a model against a dataset. It is a first-class object and not a field on the model because the same model is evaluated many times, and because the run that gated a promotion has to remain readable afterwards to answer why the promotion was allowed.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| RunId | Variable | String | Scalar | Mandatory | Identifier of the run. |
+| EvaluatedModel | Variable | NodeId | Scalar | Mandatory | Model that was measured. |
+| Dataset | Variable | NodeId | Scalar | Optional | Dataset the model was measured against. |
+| CompletedAt | Variable | UtcTime | Scalar | Optional | When the run finished. |
+| Metrics | Variable | EvaluationMetricDataType | Array | Mandatory | Measured metrics, each with the threshold it was judged against. |
+| Passed | Variable | Boolean | Scalar | Mandatory | Whether every metric met its threshold. A Server SHALL NOT report true while any entry in Metrics has Passed false - a summary that disagrees with its own detail is worse than no summary. |
+| ReportUri | Variable | String | Scalar | Optional | Where the full report lives. Untrusted input, subject to clause 12. |
+
+### ModelCardType — `ns=2;i=1015`
+
+*Subtype of:* `BaseObjectType`
+
+What a human needs to decide whether a model may be used here: what it is for, where it stops working, and under what terms. Separate from the nameplate because a nameplate answers 'which artefact is this' and a card answers 'should this be running on my line'.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| IntendedUse | Variable | LocalizedText | Scalar | Mandatory | What the model is for. |
+| Limitations | Variable | LocalizedText | Scalar | Mandatory | Where it is known not to work. Mandatory because a card that lists only capabilities is marketing, and the failure modes are the half a commissioning engineer needs. |
+| OutOfScopeUse | Variable | LocalizedText | Scalar | Optional | Uses the supplier explicitly excludes. |
+| License | Variable | String | Scalar | Optional | Licence identifier or URI governing use of the artefact. |
+| TrainingDataCutoff | Variable | UtcTime | Scalar | Optional | Latest date represented in the training data. A model cannot know about anything after this, which is often the explanation for a field failure. |
+| EthicalConsiderations | Variable | LocalizedText | Scalar | Optional | Risks the supplier records. |
+| ContactUri | Variable | String | Scalar | Optional | Where to report a problem with the model. |
+| DeprecatedFrom | Variable | UtcTime | Scalar | Optional | When the source stops treating this model as current while continuing to serve it. The date that starts a requalification, not the one that ends production. |
+| SupportedUntil | Variable | UtcTime | Scalar | Optional | When the source stops serving this model altogether.
Its consequence is not degradation. On this date the deployment stops, Reachability goes Unreachable, and FallbackPolicy decides what happens next - which, where it is FallBackTo, means the line keeps producing and something outside the qualified configuration is answering. A date that was knowable a year in advance therefore becomes an unplanned change of model, and it is published by the serving system in machine-readable form. |
+
+### ModelRegistryType — `ns=2;i=1010`
+
+*Subtype of:* `ns=1;i=63000`
+
+A catalogue of models and the datasets they were trained on. It narrows the abstract registry's group placeholder to model publishers, so that a client browsing it knows what it will find rather than discovering it.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| | Object | | | OptionalPlaceholder | A publisher namespace held by this registry. Narrows the inherited placeholder so a model registry admits ModelPublisherType and nothing else. |
+
+### ModelPublisherType — `ns=2;i=1011`
+
+*Subtype of:* `ns=1;i=63001`
+
+One publisher's namespace within a model registry: the organisation or project that released the models it contains. Publisher is the first element of the publisher/name/version triple by which every catalogue in practice identifies a model.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| | Object | | | OptionalPlaceholder | A model or dataset published in this namespace. Narrows the inherited placeholder to this model's own resource types. |
+
+### AiResourceType (abstract) — `ns=2;i=1016`
+
+*Subtype of:* `ns=1;i=63002`
+
+Abstract base of everything a model registry holds. It exists so that the inherited placeholder can be narrowed ONCE to something that admits models and datasets and nothing else - a publisher holds both, and a placeholder can be overridden only by one declaration.
+
+### ModelResourceType — `ns=2;i=1012`
+
+*Subtype of:* `AiResourceType`
+
+One model in a catalogue. Its versions are immutable and identified by content, so a version that has been seen cannot change meaning; mutable names such as a branch or a release channel are pointers AT versions, never versions themselves. Because the base type is a FileType, a Server that holds the artefact serves it through the inherited Open, Read and Close; one that only describes it leaves those unimplemented and points at the artefact instead.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| TaskKind | Variable | String | Scalar | Optional | What the model does, for example 'object-detection' or 'anomaly-detection'. A String and not an enumeration, for the same reason it is one on ModelType: the set is not closed, and every catalogue in practice uses a free tag here. |
+| Framework | Variable | String | Scalar | Optional | Runtime or library the artefact targets. |
+| Digest | Variable | ByteString | Scalar | Optional | Digest of the artefact this version names, as the catalogue declares it. A staging import compares its own computed digest with this and refuses on mismatch. |
+| DigestAlgorithm | Variable | String | Scalar | Optional | Algorithm of Digest. Subject to the strength requirement of clause 12. |
+| SizeBytes | Variable | UInt64 | Scalar | Optional | Artefact size, so a staging import can decide whether it has room before it starts rather than after it fails. |
+| Gated | Variable | Boolean | Scalar | Optional | Whether obtaining the artefact requires an acceptance or entitlement beyond ordinary authentication. A client that ignores this discovers it as a failure part-way through a staging import. |
+| MutableRefs | Variable | String | Array | Optional | Mutable pointers this resource publishes - branches, tags or channels - that a deployment may follow instead of pinning. Naming them is what makes VersionBinding FollowsRef checkable. |
+| DigestProvenance | Variable | DigestProvenanceEnum | Scalar | Optional | Where this resource's Digest came from, on the same terms as ModelType. A catalogue that declares a digest it did not compute is DeclaredBySource; one serving the artefact through the inherited Open, Read and Close can reach ComputedByServer. |
+
+### DatasetResourceType — `ns=2;i=1013`
+
+*Subtype of:* `AiResourceType`
+
+One dataset in a catalogue, a sibling of ModelResourceType rather than something beneath it: a dataset outlives the models trained on it and is cited by several.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| SourceKind | Variable | DatasetSourceEnum | Scalar | Optional | Whether the samples are real, synthetic or mixed. |
+| SampleCount | Variable | UInt64 | Scalar | Optional | Samples in the dataset. |
+| Digest | Variable | ByteString | Scalar | Optional | Digest of the dataset artefact as the catalogue declares it. |
+| DigestAlgorithm | Variable | String | Scalar | Optional | Algorithm of Digest. |
+| SizeBytes | Variable | UInt64 | Scalar | Optional | Dataset size. |
+
+### InferenceTransferType — `ns=2;i=1017`
+
+*Subtype of:* `BaseObjectType`
+
+One chunked inference exchange. It exists because Invoke carries its payload as a ByteString, and a ByteString is bounded by MaxByteStringLength, the negotiated MaxMessageSize and the Session's MaxResponseMessageSize - none of which the model gets to choose. An image, a point cloud or a window of high-rate samples exceeds those routinely, and a call that cannot carry the input is not a call.
+
+Request and Response are Part 5 FileType objects: the client opens the request, writes it in chunks it selects, and closes it; after Execute the response is read the same way. Nothing here invents a transfer protocol, because OPC UA already has one and every client already implements it.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| TransferId | Variable | String | Scalar | Mandatory | Identifier of this exchange. |
+| State | Variable | TransferStateEnum | Scalar | Mandatory | Stage the exchange has reached. |
+| Request | Object | | | Mandatory | The request body, written by the client in chunks of its own choosing. Inference does not begin until Execute is called, so a partially written request is never acted on. |
+| Response | Object | | | Mandatory | The response body, readable once State is Completed. Empty before that. |
+| ContentType | Variable | String | Scalar | Mandatory | Media type of the request body. |
+| ResponseContentType | Variable | String | Scalar | Optional | Media type of the response body. |
+| ModelUsed | Variable | NodeId | Scalar | Optional | The model that ACTUALLY produced the response, on the same terms as Invoke: a fallback or a followed reference can change it between the call and the read. |
+| Usage | Variable | UsageDataType | Scalar | Optional | What the call consumed. |
+| FinishReason | Variable | FinishReasonEnum | Scalar | Optional | Why output stopped. |
+| SafetyAssessment | Variable | SafetyAssessmentDataType | Array | Optional | Findings from the safety policy, if any applied. |
+| LastError | Variable | LocalizedText | Scalar | Optional | Diagnostic for the Failed state. For a human; SHALL NOT be parsed. |
+| ExpiresAt | Variable | UtcTime | Scalar | Optional | When the Server may reclaim this transfer if it has not completed. A client that abandons an exchange would otherwise hold Server resources until the Session ends, and a Server that never reclaimed them would be one denial of service away from unusable. |
+
+**Method `Execute`** (Mandatory) — Runs inference over the written request. The Method returns as soon as the request is accepted; State and the envelope members carry the outcome, which is what lets one exchange span a payload too large to have been a single call in the first place.
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | False when the request was incomplete or already executed. |
+
+**Method `Abort`** (Optional) — Abandons the exchange and releases what it holds. A client that has stopped caring about a response SHOULD say so rather than leaving the Server to wait out ExpiresAt.
+
+Takes no arguments and returns none.
+
+## A.4 DataTypes
+
+### InferenceLocationEnum — `ns=2;i=3001`
+
+*Subtype of:* `Enumeration`
+
+Where inference executes. The result contract is identical in every case; this property exists so a client can reason about latency, availability and the trust boundary without changing how it reads results.
+
+| Name | Value | Description |
+|---|---|---|
+| OnServer | 0 | In the OPC UA Server process or on its host. |
+| EdgeOffServer | 1 | On a separate edge node reached over the network. |
+| Cloud | 2 | In a remote or cloud service. |
+| InSimulator | 3 | Inside a simulator that also produces the input. |
+
+### AcceleratorKindEnum — `ns=2;i=3002`
+
+*Subtype of:* `Enumeration`
+
+Compute device executing the model.
+
+| Name | Value | Description |
+|---|---|---|
+| Cpu | 0 | |
+| Gpu | 1 | |
+| Npu | 2 | |
+| Fpga | 3 | |
+| Tpu | 4 | |
+| Other | 5 | |
+
+### DeploymentStateEnum — `ns=2;i=3003`
+
+*Subtype of:* `Enumeration`
+
+Runtime lifecycle state of a deployment.
+
+| Name | Value | Description |
+|---|---|---|
+| Inactive | 0 | Declared but not serving. |
+| Ready | 1 | Able to serve; no work in progress. |
+| Active | 2 | Serving at least one request. |
+| Degraded | 3 | Serving below configured quality. |
+| Faulted | 4 | Unable to serve. |
+
+### DatasetSourceEnum — `ns=2;i=3004`
+
+*Subtype of:* `Enumeration`
+
+Provenance of the samples in a dataset.
+
+| Name | Value | Description |
+|---|---|---|
+| Real | 0 | Captured from physical equipment. |
+| Synthetic | 1 | Generated or rendered by a simulator. |
+| Mixed | 2 | Both, for example synthetic pre-training with real fine-tuning. |
+
+### LearningJobStateEnum — `ns=2;i=3005`
+
+*Subtype of:* `Enumeration`
+
+State of a dataset-capture, retraining and promotion cycle.
+
+| Name | Value | Description |
+|---|---|---|
+| Idle | 0 | |
+| Collecting | 1 | |
+| Labelling | 2 | |
+| Training | 3 | |
+| Validating | 4 | |
+| Ready | 5 | A candidate model is available for promotion. |
+| Promoted | 6 | |
+| Failed | 7 | |
+
+### FinishReasonEnum — `ns=2;i=3006`
+
+*Subtype of:* `Enumeration`
+
+Why an inference call stopped producing output. A client that treats every non-error response as complete will silently accept a truncated one, which is why this is Mandatory on a response rather than a diagnostic.
+
+| Name | Value | Description |
+|---|---|---|
+| Stop | 0 | The model finished normally. |
+| Length | 1 | Output was truncated by a length or budget limit. The result is incomplete and SHALL NOT be treated as final. |
+| ToolCall | 2 | The model requested a tool or function call and is waiting for its result. |
+| Filtered | 3 | Output was withheld by a safety policy; see the SafetyAssessment. |
+| Cancelled | 4 | The caller or the Server cancelled the call. |
+| Error | 5 | The call failed; the StatusCode carries the reason. |
+
+### ApiDialectEnum — `ns=2;i=3007`
+
+*Subtype of:* `Enumeration`
+
+Wire contract a remote inference endpoint speaks. A Server needs this to call an endpoint it did not deploy; without it EndpointUri is a string nobody can act on. It describes the REMOTE endpoint and never affects how an OPC UA client calls this Server.
+
+| Name | Value | Description |
+|---|---|---|
+| OpcUaInference | 0 | Another OPC UA Server implementing this specification's Invoke Method. |
+| RestChatCompletions | 1 | The de-facto REST contract for chat and embeddings that most serving runtimes expose, including ones that run on a single workstation. |
+| OpenInferenceProtocol | 2 | The Open Inference Protocol (KServe v2) predict contract. |
+| TensorRemoteProcedure | 3 | A tensor-oriented RPC contract such as those used by dedicated inference servers. |
+| EmbeddedRuntime | 4 | An in-process runtime reached through a local library rather than a network protocol. |
+| Proprietary | 5 | A contract this specification does not name. EndpointDescriptionUri SHOULD then say where it is documented. |
+
+### AuthenticationKindEnum — `ns=2;i=3008`
+
+*Subtype of:* `Enumeration`
+
+How the Server authenticates ITSELF to a remote inference endpoint. This is not how a client authenticates to this Server, which is the ordinary OPC UA Session security and is unaffected.
+
+| Name | Value | Description |
+|---|---|---|
+| Anonymous | 0 | No credential. Permitted only where the endpoint is reachable solely from a trusted network segment. |
+| ApiKey | 1 | A shared secret presented as a key. |
+| BearerToken | 2 | A token obtained from an authorization service. |
+| WorkloadIdentity | 3 | An identity the hosting platform assigns to the Server, so no secret is stored at all. Preferred where the platform offers it. |
+| MutualTls | 4 | Both ends present certificates. |
+
+### FallbackPolicyEnum — `ns=2;i=3009`
+
+*Subtype of:* `Enumeration`
+
+What the Server does when a deployment cannot serve. This is the question a plant asks that no cloud inference API answers, because a cloud API assumes the caller can simply wait.
+
+| Name | Value | Description |
+|---|---|---|
+| Fail | 0 | Report the failure to the caller and produce nothing. The safe default: a caller that is told nothing happened can decide for itself. |
+| HoldLast | 1 | Continue reporting the most recent successful result, marked stale. Legitimate only where a stale answer is safe, and the caller SHALL be able to see the staleness. |
+| FallBackTo | 2 | Route to the deployment named by the FallsBackTo reference. The answer comes from a different model and the response SHALL say so. |
+
+### VersionBindingEnum — `ns=2;i=3010`
+
+*Subtype of:* `Enumeration`
+
+Whether a deployment is bound to one immutable model version or follows a moving pointer. Stated structurally rather than as an upgrade policy, because what a client needs to know is whether the artefact can change under it, not what schedule someone intends to change it on.
+
+| Name | Value | Description |
+|---|---|---|
+| Pinned | 0 | Bound to one immutable version. The artefact behind this deployment cannot change without an observable change to the deployment. |
+| FollowsRef | 1 | Bound to a mutable pointer such as a branch or channel. The artefact CAN change without any other change, which is why clause 12 requires the resulting promotion to be as authorized as an explicit one. |
+
+### ImportModeEnum — `ns=2;i=3011`
+
+*Subtype of:* `Enumeration`
+
+Whether an import job brings the model's description or its bytes.
+
+| Name | Value | Description |
+|---|---|---|
+| Federate | 0 | Materialize the catalogue entry as a ModelType and leave the artefact where it is. Nothing is downloaded and inference runs at the source. |
+| Stage | 1 | Fetch the artefact, verify its Digest, and make it locally available so inference can run without the source. |
+| Auto | 2 | Federate, then stage if the target deployment's InferenceLocation is OnServer or EdgeOffServer - because those cannot reach the source at inference time. |
+
+### SafetySeverityEnum — `ns=2;i=3012`
+
+*Subtype of:* `Enumeration`
+
+Severity of one safety finding. The scale is the convergent industry one; what each level means for a given category is the policy's business, not this specification's.
+
+| Name | Value | Description |
+|---|---|---|
+| None | 0 | |
+| Low | 1 | |
+| Medium | 2 | |
+| High | 3 | |
+
+### ReachabilityEnum — `ns=2;i=3013`
+
+*Subtype of:* `Enumeration`
+
+Whether the Server can currently reach a deployment's execution site.
+
+| Name | Value | Description |
+|---|---|---|
+| Unknown | 0 | Never attempted, or the Server does not probe. |
+| Reachable | 1 | The most recent attempt succeeded. |
+| Unreachable | 2 | The most recent attempt failed. |
+| Throttled | 3 | Reachable, but the endpoint is refusing work for capacity reasons. RetryAfter SHOULD be populated. |
+
+### TransferStateEnum — `ns=2;i=3014`
+
+*Subtype of:* `Enumeration`
+
+Stage of a chunked inference exchange. A client reads this rather than inferring progress from which Methods have succeeded, because a transfer that failed mid-write and one that has not started look alike from outside.
+
+| Name | Value | Description |
+|---|---|---|
+| Building | 0 | The request is being written and is not yet complete. |
+| Ready | 1 | The request is complete and inference has not started. |
+| Executing | 2 | Inference is running. |
+| Completed | 3 | The response is readable. |
+| Failed | 4 | The exchange failed; LastError carries the reason. |
+| Expired | 5 | The Server reclaimed the transfer before it completed. |
+
+### DigestProvenanceEnum — `ns=2;i=3015`
+
+*Subtype of:* `Enumeration`
+
+Where a Digest came from, or why there is none. Digest is Mandatory so that its absence is uniform and browsable rather than indistinguishable from a Server that does not implement digests - but 'empty' then carries two different meanings, and a client that must decide whether to trust an artefact needs them apart. This member is what tells them apart, and it does the same job for a digest that IS present: a value the source asserted and a value this Server computed over bytes are not the same evidence, and only one of them survives a substituted artefact.
+
+| Name | Value | Description |
+|---|---|---|
+| NotAvailable | 0 | There is no digest and the source does not publish one. Digest is empty. This is the honest answer for an endpoint that names models but never their content, and it is what most hosted inference APIs require. |
+| DeclaredBySource | 1 | Digest carries what the source declared. No party this Server can speak for has hashed the artefact, so the value is an assertion forwarded rather than evidence held. |
+| ComputedByServer | 2 | This Server hashed the artefact it holds. The value is evidence, but nothing independent agrees with it - a substitution that happened before the Server obtained the bytes is not detected. |
+| VerifiedOnStage | 3 | This Server hashed the artefact during a staging import (clause 10.4) and it matched what the source declared. Two independent parties agree, which is the strongest statement this model can carry. |
+
+### TensorSignatureDataType — `ns=2;i=3050`
+
+*Subtype of:* `Structure`
+
+Shape and element type of one model input or output tensor. This is what lets a client check that what it intends to send matches what the model expects, before it sends it.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Name | String | Scalar | | Tensor name as declared by the model. |
+| ElementType | String | Scalar | | Element type, for example float32, uint8 or int64. |
+| Shape | Int32 | Array | | Dimensions; -1 marks a dynamic axis. |
+| Layout | String | Scalar | | Optional axis layout hint, for example NCHW or NHWC. |
+
+### ModelReferenceDataType — `ns=2;i=3051`
+
+*Subtype of:* `Structure`
+
+Identity of a model as a publisher, name and version triple. Every model catalogue in practice identifies a model this way, which is why an import job takes this rather than a URL: a URL says where a copy is today, the triple says which artefact is meant.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Publisher | String | Scalar | | Organisation or namespace that published the model. |
+| Name | String | Scalar | | Model name within that publisher. |
+| Version | String | Scalar | | Immutable version identifier, or a mutable pointer such as a branch or channel name. Which one it is is stated by VersionBinding, not guessable from the string. |
+
+### UsageDataType — `ns=2;i=3052`
+
+*Subtype of:* `Structure`
+
+What one inference call consumed. Deliberately NOT named in tokens: a token is one accounting unit among several, and a model that consumes images, samples or seconds of audio needs the same accounting. UnitKind says which unit the counts are in.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| UnitKind | String | Scalar | | Unit the counts are expressed in, for example 'tokens', 'images', 'samples' or 'seconds'. |
+| InputUnits | UInt64 | Scalar | | Units consumed by the input. |
+| OutputUnits | UInt64 | Scalar | | Units produced as output. |
+| TotalUnits | UInt64 | Scalar | | Total units billed or metered for the call, which is not always the sum: cached or deduplicated input may be counted once. |
+
+### CapabilityDataType — `ns=2;i=3053`
+
+*Subtype of:* `Structure`
+
+One capability a deployment does or does not have. An open list rather than an enumeration because the set of things a model can do is not closed, and a client that cannot recognise a capability name is no worse off than one that cannot recognise an enumeration value it has never seen.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Name | String | Scalar | | Capability name, for example 'chat', 'embeddings', 'streaming', 'tool-call' or 'structured-output'. |
+| Supported | Boolean | Scalar | | Whether this deployment supports it. |
+
+### SafetyAssessmentDataType — `ns=2;i=3054`
+
+*Subtype of:* `Structure`
+
+One finding from a safety policy applied to an inference call. Category is a String and not an enumeration because harm categories are set by the policy an installation adopts, and an industrial taxonomy looks nothing like a consumer one.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Category | String | Scalar | | Category the policy assessed, for example 'out-of-distribution-input' or a policy-defined name. |
+| Severity | SafetySeverityEnum | Scalar | | Severity of the finding. |
+| Filtered | Boolean | Scalar | | True when the content was withheld or altered rather than merely flagged. |
+| Detail | String | Scalar | | Human-readable explanation. For a human; SHALL NOT be parsed. |
+
+### EvaluationMetricDataType — `ns=2;i=3055`
+
+*Subtype of:* `Structure`
+
+One measured metric from an evaluation run, with the threshold it was judged against. The threshold travels with the metric because a metric without its acceptance criterion cannot be acted on, and a reviewer reading it a year later has no way to recover what 'good' meant.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Name | String | Scalar | | Metric name, for example 'accuracy' or 'false-negative-rate'. |
+| Value | Double | Scalar | | Measured value. |
+| Unit | String | Scalar | | Unit of the value, or empty when dimensionless. |
+| Threshold | Double | Scalar | | Acceptance threshold applied. |
+| Comparison | String | Scalar | | How Value was compared with Threshold: one of '>=', '<=', '>', '<' or '=='. |
+| Passed | Boolean | Scalar | | Outcome of that comparison. |
+
+### RateLimitDataType — `ns=2;i=3056`
+
+*Subtype of:* `Structure`
+
+Capacity a remote endpoint is currently granting. Surfaced so a client can distinguish 'the model said no' from 'the quota said no', which are different faults with different remedies.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| UnitKind | String | Scalar | | Unit the limit is expressed in, matching UsageDataType.UnitKind, or 'requests'. |
+| Limit | UInt64 | Scalar | | Units permitted per interval, or 0 when not published. |
+| Remaining | UInt64 | Scalar | | Units still available in the current interval. |
+| Interval | Duration | Scalar | | Length of the interval the limit applies to. |
+| RetryAfter | Duration | Scalar | | How long to wait before retrying. Zero when the endpoint gave no guidance. |
diff --git a/metaverse-specs/extras/ai-model-management/tools/validate_local.py b/metaverse-specs/extras/ai-model-management/tools/validate_local.py
new file mode 100644
index 0000000..dc0a42e
--- /dev/null
+++ b/metaverse-specs/extras/ai-model-management/tools/validate_local.py
@@ -0,0 +1,721 @@
+#!/usr/bin/env python3
+"""
+Local structural + modelling-rule validator for the OPC UA - AI Model Management and Inference NodeSet.
+
+Reproducible in-repo gate (mirrors the vision and openusd-binding validate_local.py
+convention). Everything is re-derived from the committed artifacts with the standard
+library alone; nothing here imports the generator, because a checker that asks the
+emitter what it emitted validates nothing.
+
+Structural checks, against Opc.Ua.AiModelManagement.NodeSet2.xml:
+ * XML well-formedness and a single whose ONLY is the base UA
+ namespace - this model is deliberately standalone.
+ * Unique NodeIds; every reference target resolves (own ns=1 node or a base-UA id).
+ * Every UAObjectType/UADataType/UAReferenceType has an inverse HasSubtype to a base.
+ * Every UAReferenceType carries an InverseName.
+ * Every instance-declaration member has a HasTypeDefinition (Objects/Variables) and a
+ HasModellingRule, unless it is the concrete well-known object under the Server.
+ * ParentNodeId is backed by an inverse hierarchical reference, and forward/inverse
+ hierarchical pairs are consistent.
+ * Enum EnumStrings ArrayDimensions equals the number of enum fields.
+ * A CONCRETE Structure has a Definition and a HasEncoding to a resolvable Default
+ Binary encoding Object; an ABSTRACT Structure has a Definition and NO encoding,
+ because nothing is ever encoded as an abstract type.
+ * Every Definition Field DataType resolves, and any field typed by an abstract
+ structure declared here carries AllowSubTypes="true" - polymorphic members must be
+ self-describing rather than leaving a reader to notice the DataType is abstract.
+ * Every Method's InputArguments/OutputArguments ArrayDimensions matches the number of
+ encoded Argument entries.
+ * Opc.Ua.AiModelManagement.NodeIds.csv and the NodeSet agree exactly - same id set in both
+ directions, same NodeClass, and the CSV name resolves to the NodeSet BrowseName.
+
+Specification invariants (the reason this file is not generic):
+ * ModelType.Digest and DigestAlgorithm MUST be Mandatory. The provenance chain from a
+ published result back to the model artefact is the only reason several of the other
+ members are worth reading, and an Optional digest breaks it silently.
+ * DeploymentType.InferenceLocation and State MUST be Mandatory: a deployment whose
+ location is unknown cannot be reasoned about, and clause 6 depends on the state.
+ * LearningJobStateEnum MUST carry exactly the eight states clause 6 tabulates.
+ * DatasetSourceEnum MUST number Real 0, Synthetic 1, Mixed 2.
+ * InferenceLocationEnum MUST number OnServer 0 - the on-server case is the default a
+ Server that says nothing else is describing.
+ * UsesModel MUST exist as a ReferenceType: it is the only defined path from a
+ deployment to the artefact its results depend on.
+ * NOTHING in this model may name a sensor, a camera, an image or a robot. This model
+ was factored out of a vision specification precisely so that it does not; a check
+ that fails loudly is what keeps it that way.
+
+Specification/model cross-checks, in BOTH directions:
+ * Every ObjectType, DataType and ReferenceType the model declares is named in
+ OPC-UA-AI-Model-Management.md, and every enumeration literal it declares appears there.
+ * Every `ns=1;i=` the specification cites exists in the NodeSet.
+
+Exit code 0 and "OK" on success; non-zero with an ERRORS list otherwise.
+"""
+from __future__ import annotations
+import csv
+import os
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+STD = os.path.normpath(os.path.join(HERE, "..", "..", "..", "ai-model-management"))
+NODESET = os.path.join(STD, "Opc.Ua.AiModelManagement.NodeSet2.xml")
+CSVFILE = os.path.join(STD, "Opc.Ua.AiModelManagement.NodeIds.csv")
+SPEC = os.path.join(STD, "OPC-UA-AI-Model-Management.md")
+
+NS = {"u": "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd"}
+UAX = {"uax": "http://opcfoundation.org/UA/2008/02/Types.xsd"}
+UA_NAMESPACE = "http://opcfoundation.org/UA/"
+NAMESPACE = "http://opcfoundation.org/UA/AI/"
+XREG_NS = "http://opcfoundation.org/UA/xRegistry/"
+PROGRAM_STATE_MACHINE = "i=2391"
+
+
+# Reference types that make a node a child of its ParentNodeId.
+HIERARCHICAL = {"HasComponent", "HasProperty", "Organizes", "HasSubtype",
+ "i=47", "i=46", "i=35", "i=45"}
+ALIAS_OF = {"HasComponent": "i=47", "HasProperty": "i=46", "Organizes": "i=35",
+ "HasSubtype": "i=45", "HasTypeDefinition": "i=40",
+ "HasModellingRule": "i=37", "HasEncoding": "i=38",
+ "HasInterface": "i=17603"}
+
+ERRORS: list[str] = []
+
+
+def err(msg: str) -> None:
+ ERRORS.append(msg)
+
+
+def canon(ref: str) -> str:
+ return ALIAS_OF.get(ref, ref)
+
+
+def local(tag: str) -> str:
+ return tag.rsplit("}", 1)[-1]
+
+
+class Model:
+ """The NodeSet, re-read from XML with no help from the generator."""
+
+ def __init__(self, path: str):
+ self.tree = ET.parse(path)
+ self.root = self.tree.getroot()
+ self.nodes: dict[str, ET.Element] = {}
+ self.order: list[str] = []
+ self.aliases: dict[str, str] = {}
+ for a in self.root.findall("u:Aliases/u:Alias", NS):
+ self.aliases[a.get("Alias", "")] = (a.text or "").strip()
+ for el in self.root:
+ t = local(el.tag)
+ if not t.startswith("UA") or t in ("UANodeSet",):
+ continue
+ nid = el.get("NodeId", "")
+ if not nid:
+ continue
+ if nid in self.nodes:
+ err(f"duplicate NodeId {nid}")
+ self.nodes[nid] = el
+ self.order.append(nid)
+
+ def cls(self, nid: str) -> str:
+ return local(self.nodes[nid].tag)
+
+ def bname(self, nid: str) -> str:
+ return (self.nodes[nid].get("BrowseName", "").split(":", 1)[-1])
+
+ def refs(self, nid: str):
+ out = []
+ for r in self.nodes[nid].findall("u:References/u:Reference", NS):
+ out.append((canon(r.get("ReferenceType", "")),
+ (r.text or "").strip(),
+ r.get("IsForward", "true") != "false"))
+ return out
+
+ def supertype(self, nid: str) -> str:
+ for rt, tgt, fwd in self.refs(nid):
+ if rt == "i=45" and not fwd:
+ return tgt
+ return ""
+
+ @property
+ def own(self) -> str:
+ """NodeId prefix of this model's OWN namespace.
+
+ Derived from NamespaceUris, never assumed: a RequiredModel puts its namespace
+ in that list too, so the own index moves when a dependency is added. A
+ validator that assumed ns=1 would resolve nothing and report success.
+ """
+ uris = [(u.text or "").strip()
+ for u in self.root.findall("u:NamespaceUris/u:Uri", NS)]
+ return "ns=%d;" % (uris.index(NAMESPACE) + 1) if NAMESPACE in uris else "ns=1;"
+
+ @property
+ def xreg(self) -> str:
+ """NodeId prefix of the xRegistry namespace this model extends."""
+ uris = [(u.text or "").strip()
+ for u in self.root.findall("u:NamespaceUris/u:Uri", NS)]
+ return "ns=%d;" % (uris.index(XREG_NS) + 1) if XREG_NS in uris else ""
+
+ def resolves(self, target: str) -> bool:
+ """Own-namespace targets must exist here; imported ids are taken on trust."""
+ if target in self.nodes:
+ return True
+ if self.xreg and target.startswith(self.xreg):
+ # Declared by the xRegistry model, which this one requires. Resolving it
+ # would mean parsing that NodeSet; the RequiredModel is the contract.
+ return True
+ if target.startswith(self.own):
+ return False
+ return bool(re.fullmatch(r"i=\d+", target)) or target in self.aliases
+
+ def definition(self, nid: str):
+ for child in self.nodes[nid]:
+ if local(child.tag) == "Definition":
+ return child
+ return None
+
+ def members_of(self, nid: str) -> list[str]:
+ want = (nid if nid.startswith(self.own)
+ else f"{self.own}i={nid.split('=')[-1]}")
+ return [m for m in self.order if self.nodes[m].get("ParentNodeId") == want]
+
+ def member_named(self, owner: str, name: str) -> str:
+ for m in self.members_of(owner):
+ if self.bname(m) == name:
+ return m
+ return ""
+
+ def modelling_rule(self, nid: str) -> str:
+ for rt, tgt, fwd in self.refs(nid):
+ if rt == "i=37":
+ return {"i=78": "Mandatory", "i=80": "Optional",
+ "i=11508": "OptionalPlaceholder",
+ "i=11510": "MandatoryPlaceholder"}.get(tgt, tgt)
+ return ""
+
+ def enum_fields(self, nid: str) -> list[tuple[str, int]]:
+ d = self.definition(nid)
+ if d is None:
+ return []
+ out = []
+ for f in d:
+ if local(f.tag) == "Field":
+ try:
+ out.append((f.get("Name", ""), int(f.get("Value", "0"))))
+ except ValueError:
+ err(f"enum {self.bname(nid)} field {f.get('Name')} has a "
+ "non-integer Value")
+ return out
+
+ def struct_fields(self, nid: str) -> list[ET.Element]:
+ d = self.definition(nid)
+ return [] if d is None else [f for f in d if local(f.tag) == "Field"]
+
+ def by_name(self, name: str) -> str:
+ for nid in self.order:
+ if self.bname(nid) == name:
+ return nid
+ return ""
+
+
+def check_model_header(m: Model) -> None:
+ models = m.root.findall("u:Models/u:Model", NS)
+ if len(models) != 1:
+ err(f"expected exactly one , found {len(models)}")
+ return
+ req = models[0].findall("u:RequiredModel", NS)
+ uris = [r.get("ModelUri") for r in req]
+ if uris != [UA_NAMESPACE, XREG_NS]:
+ err("RequiredModel must be exactly the base UA namespace and "
+ f"{XREG_NS}, in that order, found {uris}")
+ uris_declared = [(u.text or "").strip()
+ for u in m.root.findall("u:NamespaceUris/u:Uri", NS)]
+ # Order is load-bearing: it fixes every namespace index in the file. Required
+ # namespaces first, own namespace last, matching the Schema Registry precedent.
+ if uris_declared != [XREG_NS, NAMESPACE]:
+ err(f"NamespaceUris must be exactly [{XREG_NS}, {NAMESPACE}] in that order, "
+ f"found {uris_declared}")
+ elif uris_declared[-1] != models[0].get("ModelUri"):
+ err("the last NamespaceUris entry and Model ModelUri must be this model")
+
+
+def check_references(m: Model) -> None:
+ for nid in m.order:
+ for rt, tgt, fwd in m.refs(nid):
+ if not m.resolves(tgt):
+ err(f"{m.bname(nid)} ({nid}) references unresolvable target {tgt}")
+ if not m.resolves(rt):
+ err(f"{m.bname(nid)} ({nid}) uses unresolvable ReferenceType {rt}")
+
+
+def check_types(m: Model) -> None:
+ for nid in m.order:
+ c = m.cls(nid)
+ if c in ("UAObjectType", "UADataType", "UAReferenceType", "UAVariableType"):
+ if not m.supertype(nid):
+ err(f"{c} {m.bname(nid)} ({nid}) has no inverse HasSubtype")
+ if c == "UAReferenceType":
+ inv = m.nodes[nid].find("u:InverseName", NS)
+ if inv is None or not (inv.text or "").strip():
+ err(f"ReferenceType {m.bname(nid)} ({nid}) has no InverseName")
+
+
+def check_instance_declarations(m: Model) -> None:
+ for nid in m.order:
+ el = m.nodes[nid]
+ parent = el.get("ParentNodeId")
+ if not parent:
+ continue
+ c = m.cls(nid)
+ if c in ("UAObject", "UAVariable"):
+ if not any(rt == "i=40" for rt, _, _ in m.refs(nid)):
+ err(f"{m.bname(nid)} ({nid}) has no HasTypeDefinition")
+ external_root = parent not in m.nodes
+ if not external_root and not m.modelling_rule(nid):
+ # A node parented on a base-UA node (the well-known object under the
+ # Server) is a concrete instance, not an instance declaration, so it
+ # carries no ModellingRule.
+ err(f"{m.bname(nid)} ({nid}) has no HasModellingRule")
+ backed = any(canon(rt) in HIERARCHICAL and not fwd and tgt == parent
+ for rt, tgt, fwd in m.refs(nid))
+ if not backed:
+ err(f"{m.bname(nid)} ({nid}) ParentNodeId {parent} is not backed by an "
+ "inverse hierarchical reference")
+ # the forward half must exist on the parent
+ if parent in m.nodes:
+ fwd_ok = any(canon(rt) in HIERARCHICAL and fwd and tgt == nid
+ for rt, tgt, fwd in m.refs(parent))
+ if not fwd_ok:
+ err(f"parent {m.bname(parent)} ({parent}) has no forward hierarchical "
+ f"reference to {m.bname(nid)} ({nid})")
+
+
+def check_datatypes(m: Model) -> None:
+ abstract_structs = {
+ nid for nid in m.order
+ if m.cls(nid) == "UADataType"
+ and m.nodes[nid].get("IsAbstract") == "true"
+ }
+ for nid in m.order:
+ if m.cls(nid) != "UADataType":
+ continue
+ name = m.bname(nid)
+ d = m.definition(nid)
+ if d is None:
+ err(f"DataType {name} ({nid}) has no Definition")
+ continue
+ sup = m.supertype(nid)
+ is_enum = sup == "i=29"
+ encodings = [tgt for rt, tgt, fwd in m.refs(nid) if rt == "i=38" and fwd]
+ if is_enum:
+ fields = m.enum_fields(nid)
+ es = m.by_name("EnumStrings")
+ # EnumStrings is a property of THIS enum, found by parent
+ es = ""
+ for mm in m.members_of(nid):
+ if m.bname(mm) == "EnumStrings":
+ es = mm
+ if not es:
+ err(f"enum {name} ({nid}) has no EnumStrings property")
+ else:
+ dims = m.nodes[es].get("ArrayDimensions", "")
+ if dims != str(len(fields)):
+ err(f"enum {name} EnumStrings ArrayDimensions {dims!r} does not "
+ f"match its {len(fields)} fields")
+ if len({v for _, v in fields}) != len(fields):
+ err(f"enum {name} has duplicate field values")
+ else:
+ if nid in abstract_structs:
+ if encodings:
+ err(f"abstract DataType {name} ({nid}) must have no encoding, "
+ f"found {encodings}")
+ else:
+ if len(encodings) != 1:
+ err(f"concrete Structure {name} ({nid}) must have exactly one "
+ f"HasEncoding, found {encodings}")
+ elif encodings[0] not in m.nodes:
+ err(f"Structure {name} encoding {encodings[0]} does not resolve")
+ for f in m.struct_fields(nid):
+ fdt = f.get("DataType", "")
+ if not m.resolves(fdt):
+ err(f"Structure {name} field {f.get('Name')} has unresolvable "
+ f"DataType {fdt}")
+ if fdt in abstract_structs and f.get("AllowSubTypes") != "true":
+ err(f"Structure {name} field {f.get('Name')} is typed by the "
+ f"abstract {m.bname(fdt)} and must carry "
+ 'AllowSubTypes="true"')
+
+
+def check_method_arguments(m: Model) -> None:
+ for nid in m.order:
+ if m.cls(nid) != "UAMethod":
+ continue
+ for which in ("InputArguments", "OutputArguments"):
+ arg = ""
+ for mm in m.members_of(nid):
+ if m.bname(mm) == which:
+ arg = mm
+ if not arg:
+ continue
+ el = m.nodes[arg]
+ count = len(el.findall(".//uax:Argument", UAX))
+ dims = el.get("ArrayDimensions", "")
+ if dims != str(count):
+ err(f"{m.bname(nid)}.{which} ArrayDimensions {dims!r} does not match "
+ f"its {count} encoded Argument entries")
+ if el.get("ValueRank") != "1":
+ err(f"{m.bname(nid)}.{which} must have ValueRank 1")
+
+
+def check_csv(m: Model) -> None:
+ if not os.path.exists(CSVFILE):
+ err(f"missing {CSVFILE}")
+ return
+ rows = []
+ with open(CSVFILE, newline="", encoding="utf-8") as fh:
+ for row in csv.reader(fh):
+ if row and row[0].strip():
+ rows.append(row)
+ csv_ids = {}
+ for row in rows:
+ if len(row) != 3:
+ err(f"NodeIds.csv row is not 3 columns: {row}")
+ continue
+ name, sid, cls = row[0].strip(), row[1].strip(), row[2].strip()
+ if sid in csv_ids:
+ err(f"NodeIds.csv has duplicate id {sid}")
+ csv_ids[sid] = (name, cls)
+ ns_ids = {nid.split("=")[-1]: nid for nid in m.order}
+ for sid, (name, cls) in csv_ids.items():
+ nid = ns_ids.get(sid)
+ if nid is None:
+ err(f"NodeIds.csv lists {name} ({sid}) which is not in the NodeSet")
+ continue
+ if m.cls(nid)[2:] != cls:
+ err(f"NodeIds.csv says {name} ({sid}) is {cls}; NodeSet says "
+ f"{m.cls(nid)[2:]}")
+ bn = m.bname(nid)
+ # Members are qualified Owner_Member in the CSV, and a structure's encoding
+ # object browses as "Default Binary" but is published as Type_Encoding_...
+ if name.endswith("_Encoding_DefaultBinary"):
+ owner = name[: -len("_Encoding_DefaultBinary")]
+ if bn != "Default Binary":
+ err(f"NodeIds.csv {name!r} ({sid}) should be the Default Binary "
+ f"encoding object; NodeSet BrowseName is {bn!r}")
+ elif not m.by_name(owner):
+ err(f"NodeIds.csv {name!r} ({sid}) names an encoding of {owner!r}, "
+ "which is not a type in the NodeSet")
+ elif (name != bn and not name.endswith("_" + bn)
+ and name != bn.strip("<>") and not name.endswith("_" + bn.strip("<>"))):
+ err(f"NodeIds.csv name {name!r} ({sid}) does not resolve to NodeSet "
+ f"BrowseName {bn!r}")
+ for sid, nid in ns_ids.items():
+ if sid not in csv_ids:
+ err(f"NodeSet node {m.bname(nid)} ({nid}) is missing from NodeIds.csv")
+
+
+def _arg_names(m, method_nid, which):
+ """Argument names declared in a Method's InputArguments/OutputArguments node."""
+ for mem in m.members_of(method_nid):
+ if m.bname(mem) == which:
+ return [(el.text or "").strip() for el in m.nodes[mem].iter()
+ if local(el.tag) == "Name" and (el.text or "").strip()]
+ return []
+
+
+def _check_new_invariants(m, dt) -> None:
+ """Invariants for the 0.2.0 additions. Separate so each stays readable."""
+
+ # A BrowseName is namespace-qualified, and its index is INDEPENDENT of the NodeId's.
+ # Migrating one and not the other leaves every node named in a namespace it does not
+ # belong to - browse paths resolve against the wrong model and the names collide
+ # with whatever that model defines. Nothing else in this file would notice.
+ own_idx = m.own[3:-1]
+ for nid in m.order:
+ raw = m.nodes[nid].get("BrowseName", "")
+ idx = raw.split(":", 1)[0] if ":" in raw else "0"
+ if not idx.isdigit():
+ err(f"{raw!r} ({nid}) has a malformed BrowseName")
+ continue
+ if idx == "0":
+ continue # deliberate: base-UA names such as the encoding objects
+ if idx != own_idx:
+ err(f"{raw!r} ({nid}) is named in namespace index {idx} but its NodeId is "
+ f"in {m.own[:-1]}. A BrowseName index is not derived from the NodeId, "
+ "so a namespace change has to move both")
+
+ # Clause 8 is only enforceable if the members it turns on are Mandatory. A rule
+ # resting on an Optional member is a rule a conformant Server can silently not
+ # satisfy, which is the failure this whole file exists to prevent.
+ dep_t = dt("DeploymentType")
+ if dep_t:
+ for name in ("VersionBinding", "FallbackPolicy", "DataJurisdiction",
+ "EgressPermitted"):
+ mm = m.member_named(dep_t, name)
+ if not mm:
+ err(f"DeploymentType must declare {name}")
+ elif m.modelling_rule(mm) != "Mandatory":
+ err(f"DeploymentType.{name} must be Mandatory: clause 8 depends on "
+ "it, and a rule resting on an Optional member can be silently "
+ "not satisfied")
+
+ # Clause 7.2 - the outputs that make a response auditable and interpretable.
+ inv = m.member_named(dep_t, "Invoke")
+ if inv:
+ got = set(_arg_names(m, inv, "OutputArguments"))
+ for need in ("ModelUsed", "Usage", "FinishReason"):
+ if need not in got:
+ err(f"DeploymentType.Invoke must return {need}; without it a "
+ "caller cannot tell what answered, what it cost, or whether "
+ "the answer is complete")
+
+ # Clause 8.2 forbids credential material in the address space. A member NAMED like
+ # a secret is how that prohibition gets violated by accident - and the address
+ # space is browsable, subscribable and historisable, so a secret placed here is
+ # not merely readable, it is archived.
+ secretish = re.compile(r"Secret|Password|PrivateKey|ApiKeyValue|AccessToken"
+ r"|SharedKey|Passphrase")
+ for nid in m.order:
+ bn = m.bname(nid)
+ if secretish.search(bn):
+ err(f"{bn} ({nid}) is named like credential material. Clause 8.2 forbids "
+ "exposing it: CredentialReference names a credential, it never "
+ "carries one")
+
+ # Clause 9.1 - a domain extension that inherits the placeholders unchanged adds
+ # metadata while restricting nothing, and a client cannot then tell one kind of
+ # registry from another except by convention.
+ # The BrowseName is the whole mechanism: an InstanceDeclaration is overridden only
+ # by one with the SAME BrowseName. A subtype that invents a new placeholder name
+ # looks narrowed and is not - the inherited declaration stays fully open beside it.
+ for owner, placeholder, wanted in (
+ ("ModelRegistryType", "", "ModelPublisherType"),
+ ("ModelPublisherType", "", "AiResourceType")):
+ nid = dt(owner)
+ if not nid:
+ continue
+ mem = m.member_named(nid, placeholder)
+ if not mem:
+ err(f"{owner} must override the inherited {placeholder} placeholder. It "
+ f"declares {sorted(m.bname(x) for x in m.members_of(nid) if m.bname(x).startswith('<'))} "
+ "instead, which narrows nothing: a placeholder is overridden only by "
+ "one with the same BrowseName")
+ continue
+ td = [tgt for rt, tgt, fwd in m.refs(mem)
+ if fwd and rt in ("i=40", "HasTypeDefinition")]
+ got = m.bname(td[0]) if td and td[0] in m.nodes else (td[0] if td else "?")
+ if got != wanted:
+ err(f"{owner}.{placeholder} must be typed {wanted}, found {got}")
+ if not any(rt in ("i=35", "Organizes") for rt, _, fwd in m.refs(mem) if not fwd):
+ err(f"{owner}.{placeholder} must keep the inherited Organizes reference; "
+ "changing it means the declaration does not override")
+
+
+def check_spec_invariants(m: Model) -> None:
+ def dt(name: str) -> str:
+ nid = m.by_name(name)
+ if not nid:
+ err(f"required type {name} is missing from the model")
+ return nid
+
+ _check_new_invariants(m, dt)
+ # The provenance chain is the reason this model is worth reading, and an Optional
+ # digest breaks it without any Server appearing to be non-conformant.
+ model_t = dt("ModelType")
+ if model_t:
+ for name in ("ModelId", "Name", "Version", "Digest", "DigestAlgorithm"):
+ mm = m.member_named(model_t, name)
+ if not mm:
+ err(f"ModelType must declare {name}")
+ elif m.modelling_rule(mm) != "Mandatory":
+ err(f"ModelType.{name} must be Mandatory; found "
+ f"{m.modelling_rule(mm)!r}. The provenance rule of clause 7 cannot "
+ "depend on a member a conformant Server may omit.")
+
+ dep_t = dt("DeploymentType")
+ if dep_t:
+ for name in ("DeploymentId", "InferenceLocation", "State"):
+ mm = m.member_named(dep_t, name)
+ if not mm:
+ err(f"DeploymentType must declare {name}")
+ elif m.modelling_rule(mm) != "Mandatory":
+ err(f"DeploymentType.{name} must be Mandatory")
+
+ base_t = dt("AiJobType")
+ if base_t:
+ mm = m.member_named(base_t, "JobId")
+ if not mm or m.modelling_rule(mm) != "Mandatory":
+ err("AiJobType.JobId must be declared Mandatory")
+ if m.supertype(base_t) != PROGRAM_STATE_MACHINE:
+ err("AiJobType must derive from the Part 10 ProgramStateMachineType "
+ f"({PROGRAM_STATE_MACHINE}); a hand-rolled lifecycle would have to "
+ "reinvent its transition events")
+ for sub in ("LearningJobType", "ModelImportJobType", "InferenceJobType"):
+ st = dt(sub)
+ if st and m.supertype(st) != m.by_name("AiJobType"):
+ err(f"{sub} must derive from AiJobType so that every long-running "
+ "operation in this model is observed the same way")
+ job_t = dt("LearningJobType")
+ if job_t:
+ mm = m.member_named(job_t, "State")
+ if not mm or m.modelling_rule(mm) != "Mandatory":
+ err("LearningJobType.State must be declared Mandatory")
+
+ if not m.by_name("UsesModel"):
+ err("UsesModel must exist: it is the only defined path from a deployment to "
+ "the artefact its results depend on")
+
+ expect_values = {
+ "DatasetSourceEnum": {"Real": 0, "Synthetic": 1, "Mixed": 2},
+ "InferenceLocationEnum": {"OnServer": 0},
+ "DeploymentStateEnum": {"Inactive": 0},
+ }
+ for ename, wanted in expect_values.items():
+ nid = dt(ename)
+ if not nid:
+ continue
+ got = dict(m.enum_fields(nid))
+ for field, value in wanted.items():
+ if got.get(field) != value:
+ err(f"{ename}.{field} must be {value}; found {got.get(field)!r}")
+
+ job_states = dt("LearningJobStateEnum")
+ if job_states:
+ want = {"Idle", "Collecting", "Labelling", "Training", "Validating", "Ready",
+ "Promoted", "Failed"}
+ got = {f for f, _ in m.enum_fields(job_states)}
+ if got != want:
+ err(f"LearningJobStateEnum must carry exactly {sorted(want)}; "
+ f"found {sorted(got)}")
+
+ # This model was factored out of a vision specification so that it would be
+ # domain-neutral. Nothing enforces that but a check that fails loudly.
+ # Matched on CamelCase word boundaries, so "Framework" is not a hit for "Frame".
+ banned = ("Camera", "Image", "Pixel", "Sensor", "Vision", "Robot", "Frame",
+ "Detection", "Weld", "Grasp")
+ for nid in m.order:
+ name = m.bname(nid)
+ words = set(re.findall(r"[A-Z][a-z]*", name))
+ for word in banned:
+ if word in words:
+ err(f"{name} ({nid}) names '{word}', which is domain-specific. This "
+ "model is deliberately neutral: a consuming specification owns "
+ "that vocabulary, not this one.")
+
+
+def check_spec_crossref(m: Model) -> None:
+ if not os.path.exists(SPEC):
+ err(f"missing {SPEC}")
+ return
+ with open(SPEC, encoding="utf-8") as fh:
+ text = fh.read()
+
+ for nid in m.order:
+ if m.cls(nid) not in ("UAObjectType", "UADataType", "UAReferenceType"):
+ continue
+ name = m.bname(nid)
+ if name not in text:
+ err(f"model declares {m.cls(nid)[2:]} {name} but the specification never "
+ "names it")
+ if m.supertype(nid) == "i=29":
+ for field, _ in m.enum_fields(nid):
+ if not re.search(rf"\b{re.escape(field)}\b", text):
+ err(f"model declares {name}.{field} but the specification never "
+ "names it")
+
+ own = m.own
+ for cited in set(re.findall(r"ns=\d+;i=(\d+)", text)):
+ if f"{own}i={cited}" not in m.nodes:
+ err(f"specification cites {own}i={cited}, which is not in the NodeSet")
+ stale = sorted(set(re.findall(r"ns=(\d+);i=\d+", text)) - {own[3:-1]})
+ if stale:
+ err(f"specification writes NodeIds in namespace index {stale}; this model's "
+ f"own namespace is {own[:-1]} and a stale index does not merely go "
+ "stale, it points into a different model")
+
+ # Forward, at member granularity. Checking only type NAMES lets a whole Method or
+ # a Mandatory member ship undocumented - a Server is obliged to implement it and a
+ # client has nothing to read about it. Optional members are not required to be
+ # named: many are self-evident and demanding prose for each would produce padding.
+ for nid in m.order:
+ if m.cls(nid) not in ("UAObjectType",):
+ continue
+ owner = m.bname(nid)
+ for mem in m.members_of(nid):
+ name = m.bname(mem)
+ if name.startswith("<") or name in ("InputArguments", "OutputArguments"):
+ continue
+ is_method = m.cls(mem) == "UAMethod"
+ if not (is_method or m.modelling_rule(mem) == "Mandatory"):
+ continue
+ if not re.search(rf"`{re.escape(name)}`", text):
+ kind = "Method" if is_method else "Mandatory member"
+ err(f"{owner}.{name} is a {kind} the specification never names. A "
+ "Server is obliged to implement it and a client has nothing to "
+ "read about it")
+
+ # The other direction. Every `SomeType.SomeMember` the prose writes must exist,
+ # otherwise the document describes a member no Server can implement. Only
+ # qualified names are checked, because a bare backticked word is as likely to be
+ # an enumeration literal or a term of art as it is to be a member.
+ declared = {m.bname(nid) for nid in m.order}
+ members = set()
+ for nid in m.order:
+ owner = m.bname(nid)
+ for rt, tgt, fwd in m.refs(nid):
+ if fwd and rt in ("i=46", "i=47") and tgt in m.nodes:
+ members.add((owner, m.bname(tgt)))
+ # A structure's fields are Definition/Field, not references, but the prose
+ # writes them with the same `Type.Field` notation.
+ d = m.definition(nid)
+ if d is not None:
+ for f in d:
+ if local(f.tag) == "Field" and f.get("Name"):
+ members.add((owner, f.get("Name")))
+ for owner, member in set(re.findall(r"`([A-Z][A-Za-z0-9]*Type)\.([A-Za-z][A-Za-z0-9]*)`",
+ text)):
+ # An owner this model does not declare belongs to a consuming specification -
+ # 4.2 names one deliberately. Only a validator that loads BOTH models can tell
+ # an outside type from a nonexistent one, so that check lives in Vision's.
+ if owner not in declared:
+ continue
+ if (owner, member) not in members:
+ err(f"specification names {owner}.{member}, which the model does not declare")
+
+
+def main() -> int:
+ if not os.path.exists(NODESET):
+ print(f"ERROR: missing {NODESET}")
+ return 2
+ try:
+ m = Model(NODESET)
+ except ET.ParseError as exc:
+ print(f"ERROR: NodeSet is not well-formed XML: {exc}")
+ return 2
+
+ check_model_header(m)
+ check_references(m)
+ check_types(m)
+ check_instance_declarations(m)
+ check_datatypes(m)
+ check_method_arguments(m)
+ check_csv(m)
+ check_spec_invariants(m)
+ check_spec_crossref(m)
+
+ if ERRORS:
+ print(f"ERRORS ({len(ERRORS)}):")
+ for e in ERRORS:
+ print(f" - {e}")
+ return 1
+ n_types = sum(1 for nid in m.order
+ if m.cls(nid) in ("UAObjectType", "UADataType", "UAReferenceType"))
+ print(f"OK - ai-model-management: {len(m.order)} nodes, {n_types} types, "
+ "NodeSet/CSV/specification consistent")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/metaverse-specs/extras/robot-intent/tools/build_model.py b/metaverse-specs/extras/robot-intent/tools/build_model.py
new file mode 100644
index 0000000..df0f804
--- /dev/null
+++ b/metaverse-specs/extras/robot-intent/tools/build_model.py
@@ -0,0 +1,2017 @@
+#!/usr/bin/env python3
+"""
+Generator for the OPC UA - Robot Intent companion specification (WG draft).
+
+Emits, from a single in-code source of truth:
+ * ../../../robot-intent/Opc.Ua.RobotIntent.NodeSet2.xml - the information model
+ * ../../../robot-intent/Opc.Ua.RobotIntent.NodeIds.csv - the NodeId assignments
+ * model-reference.md - the generated Annex A
+
+The model is a COMPANION specification in its OWN namespace
+(http://opcfoundation.org/UA/RobotIntent/, namespace index 1). Nodes therefore use
+`ns=1;i=` NodeIds; references to base UA types use plain `i=`.
+
+It is deliberately STANDALONE: the only is the base UA namespace, so a
+Server can adopt it without pulling in DI, Machinery or Robotics. Binding to an OPC
+40010-1 MotionDeviceSystem is an optional profile carried by a ReferenceType, not by a
+NodeSet dependency.
+
+NodeIds are PROVISIONAL (final IDs assigned by the OPC Foundation) and follow the repo
+convention: ObjectTypes/Interfaces 1001+, Enumerations 3001+ (EnumStrings = enum + 900),
+Structures 3050+, ReferenceTypes 4001+, DataType encodings 5001+, well-known instances
+7001+, and all remaining instance declarations sequentially from 6001. New members must
+be APPENDED so that previously published member NodeIds stay stable.
+
+Design notes (see ../../../robot-intent/OPC-UA-Robot-Intent-Research.md for the
+evidence base):
+ * OPC 40010-1 describes robot TOPOLOGY and defines no motion verbs. This model
+ supplies the verbs, and nothing else, so the two compose rather than compete.
+ * An intent outlives an OPC UA Call, so the lifecycle is a Part 10 program instance:
+ submission returns a NodeId handle, progress arrives as ProgramTransitionEvents,
+ and the result survives in FinalResultData.
+ * Verbs are a DataType HIERARCHY rather than one Method each, so a single intent and
+ a mission step are the same shape and extension is a subtyping act.
+ * Queueing is PLCopen MC_BufferMode and concurrency is VDA 5050 blockingType, both
+ adopted unchanged because both are already implemented everywhere.
+ * Orientation is a unit quaternion (x, y, z, w). OPC UA defines no quaternion type,
+ and the Euler triple in ThreeDOrientation carries no convention of its own; Annex C
+ gives the normative conversion to and from ThreeDFrame.
+"""
+from __future__ import annotations
+import os
+import re
+import xml.sax.saxutils as sx
+
+NAMESPACE = "http://opcfoundation.org/UA/RobotIntent/"
+VERSION = "0.1.0"
+PUBDATE = "2026-08-02T00:00:00Z"
+BASE_UA_VERSION = "1.05.04"
+BASE_UA_PUBDATE = "2023-12-15T00:00:00Z"
+
+# --- base UA NodeIds (namespace 0) -----------------------------------------
+HasComponent = "i=47"
+HasProperty = "i=46"
+HasSubtype = "i=45"
+Organizes = "i=35"
+HasTypeDefinition = "i=40"
+HasModellingRule = "i=37"
+HasInterface = "i=17603"
+HasEncoding = "i=38"
+
+MR_Mandatory = "i=78"
+MR_Optional = "i=80"
+MR_OptionalPlaceholder = "i=11508"
+MR_MandatoryPlaceholder = "i=11510"
+
+BaseObjectType = "i=58"
+# Part 10 types used by the promotions at the end of the model.
+ProgramDiagnostic2DataType = "i=24033"
+ProgramDiagnostic2Type = "i=15383"
+FolderType = "i=61"
+PropertyType = "i=68"
+BaseDataVariableType = "i=63"
+BaseInterfaceType = "i=17602"
+DataTypeEncodingType = "i=76"
+Enumeration = "i=29"
+Structure = "i=22"
+NonHierarchicalReferences = "i=32"
+
+BaseDataType = "i=24"
+Boolean = "i=1"
+Int32 = "i=6"
+UInt32 = "i=7"
+UInt64 = "i=9"
+Double = "i=11"
+String = "i=12"
+Guid = "i=14"
+ByteString = "i=15"
+NodeId_ = "i=17"
+QualifiedName = "i=20"
+LocalizedText = "i=21"
+UtcTime = "i=294"
+Duration = "i=290"
+Argument = "i=296"
+EUInformation = "i=887"
+KeyValuePair = "i=14533"
+
+Server = "i=2253"
+
+ALIASES = [
+ ("Boolean", Boolean), ("Int32", Int32), ("UInt32", UInt32), ("UInt64", UInt64),
+ ("Double", Double), ("String", String), ("Guid", Guid), ("ByteString", ByteString),
+ ("NodeId", NodeId_), ("QualifiedName", QualifiedName), ("LocalizedText", LocalizedText),
+ ("UtcTime", UtcTime), ("Duration", Duration), ("Argument", Argument),
+ ("EUInformation", EUInformation), ("KeyValuePair", KeyValuePair),
+ ("BaseDataType", BaseDataType),
+ ("HasComponent", HasComponent), ("HasProperty", HasProperty),
+ ("HasSubtype", HasSubtype), ("Organizes", Organizes),
+ ("HasTypeDefinition", HasTypeDefinition), ("HasModellingRule", HasModellingRule),
+ ("HasInterface", HasInterface), ("HasEncoding", HasEncoding),
+ ("Mandatory", MR_Mandatory), ("Optional", MR_Optional),
+ ("OptionalPlaceholder", MR_OptionalPlaceholder),
+ ("MandatoryPlaceholder", MR_MandatoryPlaceholder),
+]
+
+REFTYPE_ALIAS = {v: k for k, v in ALIASES}
+DATATYPE_ALIAS = {v: k for k, v in ALIASES}
+
+
+# --- node registry ---------------------------------------------------------
+class Node:
+ __slots__ = ("nid", "cls", "bname", "symbolic", "display", "desc", "parent",
+ "attrs", "refs", "category", "definition", "value", "abstract",
+ "inverse")
+
+ def __init__(self, nid, cls, bname, symbolic, display, desc, parent, attrs,
+ category, abstract):
+ self.nid = nid
+ self.cls = cls
+ self.bname = bname
+ self.symbolic = symbolic
+ self.display = display or bname
+ self.desc = desc
+ self.parent = parent
+ self.attrs = attrs or {}
+ self.refs = []
+ self.category = category
+ self.definition = None
+ self.value = None
+ self.abstract = abstract
+ self.inverse = None
+
+
+NODES = {}
+ORDER = []
+_next_member = [6001]
+_next_encoding = [5001]
+
+
+def _mid():
+ v = _next_member[0]
+ _next_member[0] += 1
+ return v
+
+
+def T(nid):
+ """Own-namespace NodeId (ns=1)."""
+ return f"ns=1;i={nid}"
+
+
+def add(nid, cls, bname, symbolic, display=None, desc=None, parent=None,
+ attrs=None, category=None, abstract=False):
+ n = Node(nid, cls, bname, symbolic, display, desc, parent, attrs, category, abstract)
+ NODES[nid] = n
+ ORDER.append(nid)
+ return n
+
+
+def ref(nid, reftype, target, forward=True):
+ NODES[nid].refs.append((reftype, target, forward))
+
+
+# --- builders --------------------------------------------------------------
+def object_type(nid, name, base, desc, abstract=False):
+ add(nid, "UAObjectType", name, name, desc=desc, category=CAT, abstract=abstract)
+ ref(nid, HasSubtype, base, forward=False)
+ return nid
+
+
+def interface_type(nid, name, base, desc):
+ add(nid, "UAObjectType", name, name, desc=desc, category=CAT, abstract=True)
+ ref(nid, HasSubtype, base, forward=False)
+ return nid
+
+
+def reference_type(nid, name, inverse, desc, base=NonHierarchicalReferences):
+ n = add(nid, "UAReferenceType", name, name, desc=desc, category=CAT_RT)
+ n.inverse = inverse
+ ref(nid, HasSubtype, base, forward=False)
+ return nid
+
+
+def _member_var(owner, owner_sym, name, datatype, typedef, rule, reftype, desc,
+ valuerank="-1"):
+ nid = _mid()
+ attrs = {"DataType": datatype, "ValueRank": valuerank}
+ add(nid, "UAVariable", name, f"{owner_sym}_{name.strip('<>')}", desc=desc,
+ parent=T(owner), attrs=attrs)
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasTypeDefinition, typedef)
+ ref(nid, reftype, T(owner), forward=False)
+ ref(owner, reftype, T(nid))
+ return nid
+
+
+def prop_var(owner, owner_sym, name, datatype, desc, rule=MR_Optional, valuerank="-1"):
+ return _member_var(owner, owner_sym, name, datatype, PropertyType, rule,
+ HasProperty, desc, valuerank)
+
+
+def data_var(owner, owner_sym, name, datatype, desc, rule=MR_Optional, valuerank="-1"):
+ return _member_var(owner, owner_sym, name, datatype, BaseDataVariableType, rule,
+ HasComponent, desc, valuerank)
+
+
+def folder_member(owner, owner_sym, name, desc, rule=MR_Mandatory):
+ nid = _mid()
+ add(nid, "UAObject", name, f"{owner_sym}_{name}", desc=desc, parent=T(owner))
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasTypeDefinition, FolderType)
+ ref(nid, HasComponent, T(owner), forward=False)
+ ref(owner, HasComponent, T(nid))
+ return nid
+
+
+def obj_member(owner, owner_sym, name, typedef, desc, rule=MR_Optional,
+ reftype=HasComponent):
+ nid = _mid()
+ add(nid, "UAObject", name, f"{owner_sym}_{name.strip('<>')}", desc=desc,
+ parent=T(owner))
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasTypeDefinition, typedef)
+ ref(nid, reftype, T(owner), forward=False)
+ ref(owner, reftype, T(nid))
+ return nid
+
+
+def _args(method_nid, method_sym, bname, args):
+ """Emit an InputArguments / OutputArguments Property for a Method."""
+ nid = _mid()
+ add(nid, "UAVariable", bname, f"{method_sym}_{bname}", parent=T(method_nid),
+ attrs={"DataType": Argument, "ValueRank": "1",
+ "ArrayDimensions": str(len(args))})
+ ref(nid, HasModellingRule, MR_Mandatory)
+ ref(nid, HasTypeDefinition, PropertyType)
+ ref(nid, HasProperty, T(method_nid), forward=False)
+ ref(method_nid, HasProperty, T(nid))
+ parts = ['',
+ '']
+ for arg in args:
+ aname, adtype, adesc = arg[0], arg[1], arg[2]
+ arank = arg[3] if len(arg) > 3 else -1
+ parts.append("i=297"
+ "")
+ parts.append("")
+ parts.append(f"{sx.escape(aname)}")
+ parts.append(f"{adtype}"
+ "")
+ if arank is not None and arank >= 0:
+ parts.append(f"{arank}"
+ "0"
+ "")
+ else:
+ parts.append("-1"
+ "")
+ if adesc:
+ parts.append(""
+ f"{sx.escape(adesc)}")
+ parts.append("")
+ parts.append("")
+ NODES[nid].value = "".join(parts)
+ return nid
+
+
+def method(owner, owner_sym, name, desc, rule=MR_Optional, inargs=None, outargs=None):
+ nid = _mid()
+ add(nid, "UAMethod", name, f"{owner_sym}_{name}", desc=desc, parent=T(owner))
+ ref(nid, HasModellingRule, rule)
+ ref(nid, HasComponent, T(owner), forward=False)
+ ref(owner, HasComponent, T(nid))
+ if inargs:
+ _args(nid, f"{owner_sym}_{name}", "InputArguments", inargs)
+ if outargs:
+ _args(nid, f"{owner_sym}_{name}", "OutputArguments", outargs)
+ return nid
+
+
+def enum_type(nid, name, desc, fields):
+ add(nid, "UADataType", name, name, desc=desc, category=CAT_DT)
+ ref(nid, HasSubtype, Enumeration, forward=False)
+ dparts = [f'']
+ for (fname, val, fdesc) in fields:
+ if fdesc:
+ dparts.append(f'')
+ dparts.append(f'{sx.escape(fdesc)}')
+ else:
+ dparts.append(f'')
+ dparts.append("")
+ NODES[nid].definition = "".join(dparts)
+ es = nid + 900
+ ref(nid, HasProperty, T(es))
+ add(es, "UAVariable", "EnumStrings", f"{name}_EnumStrings", parent=T(nid),
+ attrs={"DataType": LocalizedText, "ValueRank": "1",
+ "ArrayDimensions": str(len(fields))})
+ ref(es, HasModellingRule, MR_Mandatory)
+ ref(es, HasTypeDefinition, PropertyType)
+ ref(es, HasProperty, T(nid), forward=False)
+ vp = ['',
+ '']
+ for (fname, _val, _fdesc) in fields:
+ vp.append(""
+ f"{sx.escape(fname)}")
+ vp.append("")
+ NODES[es].value = "".join(vp)
+ return nid
+
+
+ABSTRACT_STRUCTS = set()
+
+
+def struct_type(nid, name, desc, fields, base=Structure, abstract=False):
+ """A Structure DataType plus, unless it is abstract, its Default Binary encoding.
+
+ fields: list of (FieldName, DataType, Description[, ValueRank[, ArrayDimensions]])
+ ArrayDimensions 0 means "any length" and is omitted from the emitted
+ Definition, matching how the base UA NodeSet writes unbounded arrays.
+ base: the DataType this one extends. A subtype's Definition lists only the
+ fields it ADDS - the inherited ones are reached through HasSubtype, which
+ is how the base UA NodeSet does it.
+ abstract: an abstract structure gets no encoding, because nothing is ever encoded
+ as one; values are always of a concrete subtype.
+
+ A field whose DataType is an abstract structure declared here is emitted with
+ AllowSubTypes="true", so polymorphic members are self-describing rather than
+ relying on the reader to notice the DataType is abstract.
+ """
+ add(nid, "UADataType", name, name, desc=desc, category=CAT_DT, abstract=abstract)
+ ref(nid, HasSubtype, base, forward=False)
+ if abstract:
+ ABSTRACT_STRUCTS.add(T(nid))
+ dparts = [f'']
+ for f in fields:
+ fname, fdtype, fdesc = f[0], f[1], f[2]
+ frank = f[3] if len(f) > 3 else None
+ fdims = f[4] if len(f) > 4 else None
+ a = [f'Name="{sx.escape(fname)}"', f'DataType="{fdtype}"']
+ if frank is not None:
+ a.append(f'ValueRank="{frank}"')
+ if fdims:
+ a.append(f'ArrayDimensions="{fdims}"')
+ if fdtype in ABSTRACT_STRUCTS:
+ a.append('AllowSubTypes="true"')
+ attr = " ".join(a)
+ if fdesc:
+ dparts.append(f'')
+ dparts.append(f'{sx.escape(fdesc)}')
+ else:
+ dparts.append(f'')
+ dparts.append("")
+ NODES[nid].definition = "".join(dparts)
+ if abstract:
+ return nid
+ enc = _next_encoding[0]
+ _next_encoding[0] += 1
+ ref(nid, HasEncoding, T(enc))
+ add(enc, "UAObject", "Default Binary", f"{name}_Encoding_DefaultBinary",
+ desc="Default Binary encoding of the structure.",
+ attrs={"BrowseNameNamespace": 0, "SymbolicName": "DefaultBinary"})
+ ref(enc, HasTypeDefinition, DataTypeEncodingType)
+ ref(enc, HasEncoding, T(nid), forward=False)
+ return nid
+
+
+def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
+ add(nid, "UAObject", name, name, desc=desc, parent=parent_nodeid)
+ ref(nid, HasTypeDefinition, typedef)
+ ref(nid, reftype, parent_nodeid, forward=False)
+ return nid
+
+
+# ===========================================================================
+# ============================== MODEL DEFINITION =========================
+# ===========================================================================
+CAT = "RobotIntent"
+CAT_DT = "RobotIntent DataTypes"
+CAT_RT = "RobotIntent ReferenceTypes"
+
+# Part 10 program model - the lifecycle this specification builds on.
+ProgramStateMachineType = "i=2391"
+
+# ---------------------------------------------------------------------------
+# Enumerations (3001+)
+# ---------------------------------------------------------------------------
+enum_type(3001, "ExecutionStateEnum",
+ "Fine-grained execution state of an intent or a mission. This REFINES the "
+ "Part 10 program state machine rather than restating it: Queued, Cancelling "
+ "and the three terminal outcomes cannot be told apart from CurrentState "
+ "alone. Clause 6.3 fixes which ExecutionState may accompany which Part 10 "
+ "state, and a Server shall satisfy that table.",
+ [("Accepted", 0, "Admitted and validated, not yet queued or executing."),
+ ("Queued", 1, "Waiting behind another intent because BufferMode is "
+ "Buffered or a blending mode. Corresponds to PLCopen Busy "
+ "without Active."),
+ ("Executing", 2, "Commanding the robot now."),
+ ("Suspended", 3, "Paused by request; position is retained and execution "
+ "can resume."),
+ ("Cancelling", 4, "A cancel was accepted and the Server is bringing the "
+ "motion to a controlled end. Not yet terminal."),
+ ("Succeeded", 5, "Terminal. Completed as requested."),
+ ("Failed", 6, "Terminal. Did not complete; Failure carries the reason."),
+ ("Cancelled", 7, "Terminal. Ended early because a cancel was accepted."),
+ ("Retriable", 8, "Terminal for now, but the Server can re-attempt it on "
+ "Retry. A Server that does not offer Retry never enters "
+ "this state and reports Failed instead.")])
+ExecutionStateEnum = T(3001)
+
+enum_type(3002, "BufferModeEnum",
+ "How a newly submitted intent relates to the one already executing. The "
+ "values and their meanings are those of PLCopen Motion Control MC_BufferMode, "
+ "adopted unchanged because every motion runtime already implements them. In "
+ "all blending modes the robot does not decelerate to a stop at the boundary, "
+ "and the predecessor reaches Succeeded when blending begins rather than when "
+ "its target is exactly attained.",
+ [("Aborting", 0, "Abort what is executing and start immediately. The aborted "
+ "intent terminates as Cancelled. This is the default."),
+ ("Buffered", 1, "Queue; start when the predecessor succeeds."),
+ ("BlendingLow", 2, "Blend at the lower of the two boundary speeds."),
+ ("BlendingPrevious", 3, "Blend at the predecessor's boundary speed."),
+ ("BlendingNext", 4, "Blend at the successor's boundary speed."),
+ ("BlendingHigh", 5, "Blend at the higher of the two boundary speeds.")])
+BufferModeEnum = T(3002)
+
+enum_type(3003, "BlockingModeEnum",
+ "Whether an intent tolerates motion and other intents running alongside it. "
+ "The four values are the two-by-two matrix of VDA 5050 blockingType, adopted "
+ "because it is the only widely deployed concurrency annotation for robot "
+ "actions.",
+ [("None", 0, "Runs in the background; motion may continue and other intents "
+ "may run concurrently."),
+ ("Soft", 1, "Motion stops for the duration; other intents may still run."),
+ ("Single", 2, "Motion may continue; no other intent may run concurrently."),
+ ("Hard", 3, "Motion stops and no other intent may run concurrently. The "
+ "intent has exclusive use of the robot.")])
+BlockingModeEnum = T(3003)
+
+enum_type(3004, "TerminationModeEnum",
+ "Whether a motion ends exactly on its target or is blended into the next "
+ "one. This is the only distinction every vendor expresses identically; the "
+ "blend magnitude in BlendDataType.Radius is a request, not a guarantee.",
+ [("Exact", 0, "Come to rest on the target before the next motion begins. "
+ "ABB fine, FANUC FINE, Yaskawa PL=0, KUKA no approximation."),
+ ("Blend", 1, "Round the corner into the next motion without stopping.")])
+TerminationModeEnum = T(3004)
+
+enum_type(3005, "ReleaseModeEnum",
+ "How a held object is given up.",
+ [("Drop", 0, "Open the end effector where it is, without placing."),
+ ("Place", 1, "Set the object down under control at the target."),
+ ("Handover", 2, "Retain the object until the Server judges a receiving "
+ "party has taken it.")])
+ReleaseModeEnum = T(3005)
+
+enum_type(3006, "ApproachModeEnum",
+ "Direction from which an end effector approaches an object or a placement.",
+ [("Default", 0, "The Server chooses."),
+ ("ToolZ", 1, "Along the tool's own Z axis."),
+ ("Top", 2, "From above, in the frame the target is expressed in."),
+ ("Side", 3, "Laterally, in the frame the target is expressed in.")])
+ApproachModeEnum = T(3006)
+
+enum_type(3007, "FrameRoleEnum",
+ "Role of a coordinate frame, following the coordinate systems ISO 9787 "
+ "standardises. The roles say WHICH frames exist; no standard says how to "
+ "calibrate between them, which is why CoordinateFrameType carries the "
+ "transform explicitly.",
+ [("World", 0, "The cell-level reference frame."),
+ ("Base", 1, "The robot base frame."),
+ ("MechanicalInterface", 2, "The flange at the end of the last link, to "
+ "which an end effector is fitted."),
+ ("Tool", 3, "A tool frame, whose origin is a tool centre point."),
+ ("Object", 4, "A workpiece or work-object frame."),
+ ("Other", 5, "A frame whose role is none of the above.")])
+FrameRoleEnum = T(3007)
+
+enum_type(3008, "OperationalModeEnum",
+ "Operational mode of the robot system, as defined by ISO 10218-1 and "
+ "reported identically by OPC 40010-1. It is READ-ONLY here: mode selection "
+ "is a safety function performed by safety-rated means, and this "
+ "specification defines no way to command it. Clause 10 restricts intent "
+ "submission to Automatic and AutomaticExternal.",
+ [("Other", 0, "Booting, uncalibrated, or a safety system fault."),
+ ("ManualReducedSpeed", 1, "Teaching mode with a speed ceiling and a held "
+ "enabling device."),
+ ("ManualHighSpeed", 2, "Program verification with an enabling device."),
+ ("Automatic", 3, "Automatic operation with the safeguarded space secured."),
+ ("AutomaticExternal", 4, "Automatic operation commanded by an external "
+ "system. This is the mode this specification is "
+ "written for.")])
+OperationalModeEnum = T(3008)
+
+enum_type(3009, "IntentFailureEnum",
+ "Why an intent did not succeed. The set is deliberately small and "
+ "diagnosable: a client decides whether to retry, re-plan or escalate from "
+ "this value alone, and reads Message only to show a human.",
+ [("None", 0, "No failure. Reported on a successful outcome."),
+ ("Unreachable", 1, "The target lies outside the reachable workspace."),
+ ("Kinematics", 2, "No kinematic solution, or a singularity on the path."),
+ ("Collision", 3, "A collision was predicted or detected."),
+ ("JointLimit", 4, "A joint limit would be or was exceeded."),
+ ("SpeedLimit", 5, "The requested speed or acceleration is not permitted in "
+ "the active mode."),
+ ("ToolMissing", 6, "The required tool is not fitted or not identified."),
+ ("ObjectNotFound", 7, "The object to act on was not present."),
+ ("GraspFailed", 8, "The object was not acquired, or was lost in transit."),
+ ("Timeout", 9, "The intent did not complete within its permitted time."),
+ ("NotPermittedInMode", 10, "Refused because the operational mode does not "
+ "permit it. See clause 10."),
+ ("ControlNotOwned", 11, "Refused because the caller does not hold command "
+ "authority. See clause 8."),
+ ("CapabilityNotSupported", 12, "The Server does not implement this intent "
+ "type, or this combination of options."),
+ ("ParameterInvalid", 13, "A parameter was missing, malformed or out of "
+ "range."),
+ ("QueueFull", 14, "The queue is at MaxQueueDepth."),
+ ("Superseded", 15, "An Aborting submission or a mission update replaced it "
+ "before it could run."),
+ ("HardwareFault", 16, "A fault in the robot, the end effector or the "
+ "controller."),
+ ("SafetyStop", 17, "A safety function acted. The safety system, not this "
+ "interface, decided this."),
+ ("Other", 18, "A reason none of the above describes; see Message."),
+ ("SafetyLimitExceeded", 19,
+ "Refused because the request would exceed a limit the safety system is "
+ "enforcing. See clause 10.3.")])
+IntentFailureEnum = T(3009)
+
+enum_type(3010, "StopModeEnum",
+ "How urgently a cancellation should bring motion to an end. The values are "
+ "those of PossibleStopModes in OPC 40010-1, so a Server implementing both "
+ "reports one vocabulary. This is an APPLICATION-LEVEL request: it does not "
+ "select, imply or guarantee any IEC 60204-1 stop category, which only the "
+ "robot's safety system determines. See clause 10.",
+ [("OnPath", 1, "Decelerate along the programmed path."),
+ ("EndOfCycle", 2, "Stop when the current cycle completes."),
+ ("ProcessStop", 3, "Stop at a point the process defines as safe."),
+ ("QuickStop", 4, "Decelerate as quickly as the drives allow."),
+ ("EndOfInstruction", 5, "Stop when the current instruction completes.")])
+StopModeEnum = T(3010)
+
+enum_type(3011, "AxisKindEnum",
+ "Whether an axis rotates or translates. This fixes the unit of the "
+ "corresponding entry of JointMoveIntentDataType.JointTargets.",
+ [("Revolute", 0, "Rotates. Its joint target is in radians."),
+ ("Prismatic", 1, "Translates. Its joint target is in metres.")])
+AxisKindEnum = T(3011)
+
+enum_type(3012, "MissionUpdateResultEnum",
+ "Outcome of a mission update, reported so a client can tell a stale update "
+ "from a rejected one without parsing a StatusCode.",
+ [("Accepted", 0, "The horizon was replaced as requested."),
+ ("Outdated", 1, "MissionUpdateId was not greater than the current one."),
+ ("BaseConflict", 2, "The update would have altered a released step."),
+ ("UnknownMission", 3, "No mission with that MissionId is held."),
+ ("Rejected", 4, "Refused for a reason the Server states in a message.")])
+MissionUpdateResultEnum = T(3012)
+
+# ---------------------------------------------------------------------------
+# Structured DataTypes (3050+)
+# ---------------------------------------------------------------------------
+struct_type(3050, "Pose3DDataType",
+ "A rigid-body pose. Position is metres; Orientation is a UNIT QUATERNION "
+ "ordered (x, y, z, w). Quaternions are used because OPC UA defines no "
+ "quaternion type and the Euler triple in ThreeDOrientation is ambiguous "
+ "without an external convention; Annex C gives the normative conversion to "
+ "and from ThreeDFrame. All frames are right-handed. FrameId names the "
+ "CoordinateFrame the pose is expressed in; an empty FrameId means the "
+ "Server's default work frame.",
+ [("FrameId", String, "FrameId of the CoordinateFrame this pose is "
+ "expressed in; empty for the default work frame."),
+ ("Position", Double, "Translation (x, y, z) in metres.", 1, 3),
+ ("Orientation", Double, "Unit quaternion (x, y, z, w).", 1, 4)])
+Pose3DDataType = T(3050)
+
+struct_type(3051, "MotionConstraintsDataType",
+ "Limits a motion is to respect. Every field is a REQUEST bounded by what "
+ "the robot is configured to permit: a Server clamps rather than refuses, "
+ "except where clause 10 requires refusal. A value of zero or less means "
+ "the field is unspecified and the Server chooses.",
+ [("SpeedFraction", Double, "Fraction of the configured maximum speed, in "
+ "the range 0 to 1. This is the portable speed "
+ "control: every vendor supports it."),
+ ("CartesianSpeed", Double, "Tool centre point speed in metres per "
+ "second. Ignored by joint moves."),
+ ("CartesianAcceleration", Double, "Tool centre point acceleration in "
+ "metres per second squared."),
+ ("Jerk", Double, "Rate of change of acceleration in metres per second "
+ "cubed.")])
+MotionConstraintsDataType = T(3051)
+
+struct_type(3052, "BlendDataType",
+ "How a motion ends. Radius is interpreted only when Termination is Blend, "
+ "and is a request: controllers that expose a unitless blend scale rather "
+ "than a distance map it as best they can, and a Server that cannot honour "
+ "the exact radius still succeeds.",
+ [("Termination", TerminationModeEnum, "Exact stop, or blend into the next "
+ "motion."),
+ ("Radius", Double, "Blend radius in metres, measured from the target. "
+ "Zero or less means the Server chooses.")])
+BlendDataType = T(3052)
+
+struct_type(3053, "IntentDataType",
+ "Abstract base of every intent. An intent is a single task-level request; "
+ "it is what a client submits and what a mission step holds, so the two are "
+ "the same shape. Extension is by SUBTYPING this structure, which keeps new "
+ "intents discoverable through IntentCapabilitiesType rather than by "
+ "probing for BrowseNames.",
+ [("IntentId", String, "Client-assigned identifier, unique among the "
+ "intents the client has outstanding. Empty asks the "
+ "Server to assign one, which it returns."),
+ ("Label", LocalizedText, "Human-readable description. Never interpreted."),
+ ("BufferMode", BufferModeEnum, "How this intent relates to one already "
+ "executing."),
+ ("BlockingMode", BlockingModeEnum, "Whether motion and other intents may "
+ "proceed alongside it.")],
+ abstract=True)
+IntentDataType = T(3053)
+
+struct_type(3054, "MotionIntentDataType",
+ "Abstract base of the intents that move the robot. ToolFrame names the "
+ "frame whose origin is driven to the target - without it a pose target is "
+ "meaningless, and OPC 40010-1 defines no tool centre point at all.",
+ [("ToolFrame", NodeId_, "The CoordinateFrame, of role Tool, whose origin "
+ "is driven to the target. Null means the tool "
+ "currently fitted."),
+ ("Constraints", MotionConstraintsDataType, "Speed and acceleration "
+ "limits for this motion."),
+ ("Blend", BlendDataType, "How this motion ends.")],
+ base=IntentDataType, abstract=True)
+MotionIntentDataType = T(3054)
+
+struct_type(3055, "JointMoveIntentDataType",
+ "Move by interpolating in joint space. This is the fastest way between two "
+ "configurations and the path the tool centre point takes is not "
+ "controlled. It is the portable equivalent of PTP, MoveJ, J and MOVJ. "
+ "Giving a pose rather than joint values asks the Server to solve the "
+ "kinematics itself, which is the 'move to this pose, you choose how' case.",
+ [("HasJointTargets", Boolean, "True when JointTargets is meaningful; "
+ "False when TargetPose is."),
+ ("JointTargets", Double, "One value per axis, in the order the axes are "
+ "declared under the controller: radians for a "
+ "Revolute axis, metres for a Prismatic one.",
+ 1, 0),
+ ("TargetPose", Pose3DDataType, "Pose to reach, used when HasJointTargets "
+ "is False.")],
+ base=MotionIntentDataType)
+JointMoveIntentDataType = T(3055)
+
+struct_type(3056, "LinearMoveIntentDataType",
+ "Move the tool centre point along a straight line to the target. The "
+ "portable equivalent of LIN, MoveL, L and MOVL.",
+ [("Target", Pose3DDataType, "Pose to reach.")],
+ base=MotionIntentDataType)
+LinearMoveIntentDataType = T(3056)
+
+struct_type(3057, "CircularMoveIntentDataType",
+ "Move the tool centre point along the circular arc that passes through "
+ "ViaPoint and ends at Target. The portable equivalent of CIRC, MoveC, C "
+ "and MOVC. Only the position of ViaPoint defines the arc; its orientation "
+ "is ignored.",
+ [("ViaPoint", Pose3DDataType, "A pose on the arc between the start and "
+ "the target. Only its position is used."),
+ ("Target", Pose3DDataType, "Pose to reach.")],
+ base=MotionIntentDataType)
+CircularMoveIntentDataType = T(3057)
+
+struct_type(3058, "GraspIntentDataType",
+ "Close the end effector on an object. Force and Width are requests; an "
+ "end effector that cannot regulate force ignores Force and still succeeds.",
+ [("Tool", NodeId_, "The Tool to actuate. Null means the tool currently "
+ "fitted."),
+ ("Force", Double, "Grasp force in newtons. Zero or less means the Server "
+ "chooses."),
+ ("Width", Double, "Opening at which to close, in metres. Zero or less "
+ "means the Server chooses."),
+ ("Approach", ApproachModeEnum, "Direction of approach.")],
+ base=IntentDataType)
+GraspIntentDataType = T(3058)
+
+struct_type(3059, "ReleaseIntentDataType",
+ "Give up a held object.",
+ [("Tool", NodeId_, "The Tool to actuate. Null means the tool currently "
+ "fitted."),
+ ("Mode", ReleaseModeEnum, "How the object is given up."),
+ ("HasTarget", Boolean, "True when Target is meaningful."),
+ ("Target", Pose3DDataType, "Where to place the object, when Mode is "
+ "Place.")],
+ base=IntentDataType)
+ReleaseIntentDataType = T(3059)
+
+struct_type(3060, "PickIntentDataType",
+ "Take an object from a location. Source is a REFERENCE TO A LOCATION NODE, "
+ "not a name: the location's pose and its properties are then read from the "
+ "address space, so the station identity has exactly one definition.",
+ [("Source", NodeId_, "The Location to pick from."),
+ ("Tool", NodeId_, "The Tool to use. Null means the tool currently "
+ "fitted."),
+ ("ObjectClass", String, "What to pick, when the location can hold more "
+ "than one kind. Empty means whatever is there."),
+ ("Force", Double, "Grasp force in newtons. Zero or less means the Server "
+ "chooses."),
+ ("Approach", ApproachModeEnum, "Direction of approach."),
+ ("Attributes", KeyValuePair, "Further named parameters the Server "
+ "declares in its capabilities.", 1, 0)],
+ base=IntentDataType)
+PickIntentDataType = T(3060)
+
+struct_type(3061, "PlaceIntentDataType",
+ "Put a held object at a location. Destination is a reference to a Location "
+ "node, for the same reason as PickIntentDataType.Source.",
+ [("Destination", NodeId_, "The Location to place at."),
+ ("Tool", NodeId_, "The Tool to use. Null means the tool currently "
+ "fitted."),
+ ("Approach", ApproachModeEnum, "Direction of approach."),
+ ("Attributes", KeyValuePair, "Further named parameters the Server "
+ "declares in its capabilities.", 1, 0)],
+ base=IntentDataType)
+PlaceIntentDataType = T(3061)
+
+struct_type(3062, "ToolChangeIntentDataType",
+ "Exchange the fitted end effector.",
+ [("Tool", NodeId_, "The Tool to fit. Null means release the fitted tool "
+ "and fit nothing."),
+ ("DockStation", NodeId_, "The Location of the tool changer. Null lets "
+ "the Server choose.")],
+ base=IntentDataType)
+ToolChangeIntentDataType = T(3062)
+
+struct_type(3063, "SetOutputIntentDataType",
+ "Set a discrete or analogue output. Output references an OutputSignal "
+ "node, so the signal's meaning, range and unit are described once in the "
+ "address space instead of being implied by a string.",
+ [("Output", NodeId_, "The OutputSignal to write."),
+ ("Value", BaseDataType, "Value to write, of the signal's own DataType.")],
+ base=IntentDataType)
+SetOutputIntentDataType = T(3063)
+
+struct_type(3064, "CallProgramIntentDataType",
+ "Run a program that already exists on the controller. This is the escape "
+ "hatch for capability this specification does not model, and the bridge to "
+ "the OPC 40010-1 task control surface.",
+ [("Program", NodeId_, "The Program to run."),
+ ("Arguments", KeyValuePair, "Named arguments for the program.", 1, 0)],
+ base=IntentDataType)
+CallProgramIntentDataType = T(3064)
+
+struct_type(3065, "WaitIntentDataType",
+ "Do nothing for a while, or until released. A mission needs this to "
+ "express a rendezvous with something the robot does not control; without "
+ "it a client has to hold the queue open from outside.",
+ [("Duration", Duration, "How long to wait, in milliseconds. Zero or less "
+ "waits until Signal is released."),
+ ("Signal", NodeId_, "An OutputSignal under this controller, or another "
+ "Variable of DataType Boolean, whose becoming true "
+ "ends the wait. Null waits only for Duration. Clause "
+ "11.3 requires the Server to check that it resolves "
+ "to one of those two, under the controller being "
+ "commanded, and to refuse it otherwise.")],
+ base=IntentDataType)
+WaitIntentDataType = T(3065)
+
+struct_type(3066, "IntentResultDataType",
+ "The outcome of one intent, preserved after it terminates. AchievedPose "
+ "records where the tool centre point actually ended, which is what lets a "
+ "client tell a blended corner from an exact stop and audit a placement.",
+ [("IntentId", String, "The intent this describes."),
+ ("State", ExecutionStateEnum, "Terminal state reached."),
+ ("Failure", IntentFailureEnum, "Reason, or None on success."),
+ ("Message", LocalizedText, "Human-readable detail. Never parsed."),
+ ("HasAchievedPose", Boolean, "True when AchievedPose is meaningful."),
+ ("AchievedPose", Pose3DDataType, "Where the tool centre point came to "
+ "rest, or was when blending began."),
+ ("StartTime", UtcTime, "When execution began."),
+ ("EndTime", UtcTime, "When the terminal state was reached."),
+ ("Outputs", KeyValuePair, "Named results the intent produced, for "
+ "example the identity of a picked object.",
+ 1, 0)])
+IntentResultDataType = T(3066)
+
+struct_type(3067, "MissionStepDataType",
+ "One step of a mission. Released is what splits a mission into its "
+ "immutable base and its revisable horizon: a released step has been "
+ "committed and may already be executing, so an update may not touch it. "
+ "Status is a HINT - where Operation is not null, that IntentOperation's "
+ "state machine decides.",
+ [("StepId", String, "Identifier unique within the mission."),
+ ("SequenceId", UInt32, "Execution order within the mission, ascending."),
+ ("Released", Boolean, "True for a base step, which is committed and "
+ "immutable. False for a horizon step, which an "
+ "update may replace or remove."),
+ ("Intent", IntentDataType, "The intent this step executes."),
+ ("Status", ExecutionStateEnum, "Reported status. Where Operation is not "
+ "null its state machine is authoritative "
+ "and this reflects it."),
+ ("Operation", NodeId_, "The IntentOperation executing this step, or null "
+ "while the step has not begun."),
+ # ErrorPolicyEnum and MissionTransitionDataType are appended further
+ # down, so they are referenced by id here. A field DataType is emitted as
+ # a NodeId string; the node it names only has to exist in the finished
+ # NodeSet, which the validator checks.
+ ("ErrorPolicy", T(3016), "What the mission does when this step "
+ "does not succeed."),
+ ("FallbackStepId", String, "The step to continue at, when ErrorPolicy is "
+ "Fallback or Compensate.")])
+MissionStepDataType = T(3067)
+
+struct_type(3068, "MissionDataType",
+ "An ordered sequence of intents submitted and tracked as a unit. "
+ "MissionUpdateId increases with every update, so a Server can reject an "
+ "update that crossed with another in flight instead of applying it out of "
+ "order.",
+ [("MissionId", String, "Client-assigned identifier. Empty asks the Server "
+ "to assign one, which it returns."),
+ ("MissionUpdateId", UInt32, "Revision of this mission, increasing. The "
+ "first submission is 0."),
+ ("Label", LocalizedText, "Human-readable description. Never interpreted."),
+ ("Steps", MissionStepDataType, "The steps, in ascending SequenceId "
+ "order.", 1, 0),
+ ("Transitions", T(3083),
+ "The step graph. An empty array means the steps run in order, which is "
+ "the flat sequence a mission without branching has always been.", 1, 0)])
+MissionDataType = T(3068)
+
+struct_type(3069, "IntentCapabilityDataType",
+ "What the Server will accept for one intent type. This is the machine-"
+ "readable declaration that makes an intent surface discoverable: a client "
+ "reads it once and knows what it may submit, instead of submitting to find "
+ "out. It is the analogue of the VDA 5050 factsheet.",
+ [("IntentType", NodeId_, "The DataType of the intent, a subtype of "
+ "IntentDataType."),
+ ("Description", LocalizedText, "What this Server does with it."),
+ ("CancelSupported", Boolean, "True when Cancel is honoured for it. A "
+ "Server may still refuse a particular "
+ "cancel; see clause 6.5."),
+ ("PauseSupported", Boolean, "True when Pause and Resume are honoured for a "
+ "running intent. False when the Server can only stop its queue: "
+ "suspending execution means the robot stops, and Suspended is never "
+ "reported while it is still moving."),
+ ("RetrySupported", Boolean, "True when it can terminate Retriable and be "
+ "re-attempted."),
+ ("SupportedBufferModes", BufferModeEnum, "Buffer modes accepted for it. "
+ "Aborting is always accepted "
+ "and always listed.", 1, 0),
+ ("SupportedBlockingModes", BlockingModeEnum, "Blocking modes accepted "
+ "for it.", 1, 0),
+ ("Attributes", KeyValuePair, "Named parameters this Server recognises in "
+ "the intent's Attributes field.", 1, 0)])
+IntentCapabilityDataType = T(3069)
+
+# ---------------------------------------------------------------------------
+# ReferenceTypes (4001+)
+# ---------------------------------------------------------------------------
+reference_type(4001, "HasIntentController", "IntentControllerOf",
+ "Binds an intent surface to the thing it commands. This is how the "
+ "model attaches to a robot described by another specification - an OPC "
+ "40010-1 MotionDeviceSystem, say - without depending on it. Annex B "
+ "defines that binding.")
+HasIntentController = T(4001)
+
+reference_type(4002, "HasFrameParent", "FrameParentOf",
+ "From a CoordinateFrame to the frame its Transform is expressed in. "
+ "Frames form a tree, so a pose given in one frame can be re-expressed "
+ "in another by composing the transforms along the path between them.")
+HasFrameParent = T(4002)
+
+# ---------------------------------------------------------------------------
+# ObjectTypes (1001+)
+# ---------------------------------------------------------------------------
+object_type(1001, "RobotIntentRootType", BaseObjectType,
+ "Server-level entry point. A client that has just connected browses here "
+ "to find every robot it can command, without knowing the Server's layout.")
+RT_ = 1001
+folder_member(RT_, "RobotIntentRootType", "Controllers",
+ "The intent surfaces this Server offers, one per commandable robot.")
+prop_var(RT_, "RobotIntentRootType", "SpecificationVersion", String,
+ "Release of this specification the Server implements, for example '0.1.0'.",
+ MR_Mandatory)
+
+object_type(1002, "IntentControllerType", BaseObjectType,
+ "The intent surface for one robot: what it can be asked to do, the frames "
+ "and objects those requests refer to, and the intents and missions "
+ "currently outstanding. Everything a client needs in order to command the "
+ "robot hangs from here.")
+IC = 1002
+prop_var(IC, "IntentControllerType", "OperationalMode", OperationalModeEnum,
+ "Operational mode reported by the robot. Read-only: mode selection is a "
+ "safety function and this specification defines no way to command it. Clause "
+ "10 restricts submission to Automatic and AutomaticExternal.", MR_Mandatory)
+prop_var(IC, "IntentControllerType", "Ready", Boolean,
+ "True when the robot will accept intents now. A client checks this before "
+ "submitting rather than inferring readiness from OperationalMode alone.",
+ MR_Mandatory)
+prop_var(IC, "IntentControllerType", "ControlOwner", NodeId_,
+ "SessionId of the client that currently holds command authority, or null "
+ "when no client does. Only that client may submit; see clause 8.",
+ MR_Mandatory)
+prop_var(IC, "IntentControllerType", "MaxQueueDepth", UInt32,
+ "How many intents may be queued behind the executing one. Zero means the "
+ "Server accepts only Aborting submissions.", MR_Mandatory)
+data_var(IC, "IntentControllerType", "ActiveIntent", NodeId_,
+ "The IntentOperation executing now, or null.", MR_Mandatory)
+data_var(IC, "IntentControllerType", "ActiveMission", NodeId_,
+ "The Mission executing now, or null.")
+obj_member(IC, "IntentControllerType", "Capabilities", T(1005),
+ "What this robot will accept.", MR_Mandatory)
+folder_member(IC, "IntentControllerType", "Frames",
+ "The coordinate frames poses may be expressed in.")
+folder_member(IC, "IntentControllerType", "Tools",
+ "The end effectors this robot can use.")
+folder_member(IC, "IntentControllerType", "Locations",
+ "The named places intents refer to.")
+folder_member(IC, "IntentControllerType", "Axes",
+ "The axes, in the order JointMoveIntentDataType.JointTargets uses.")
+folder_member(IC, "IntentControllerType", "Outputs",
+ "The signals SetOutput can write.", MR_Optional)
+folder_member(IC, "IntentControllerType", "Programs",
+ "The controller programs CallProgram can run.", MR_Optional)
+folder_member(IC, "IntentControllerType", "Intents",
+ "Outstanding and recently completed intents, one Object each.")
+folder_member(IC, "IntentControllerType", "Missions",
+ "Outstanding and recently completed missions, one Object each.",
+ MR_Optional)
+
+method(IC, "IntentControllerType", "RequestControl",
+ "Take command authority. A Server grants it only when no other Session holds "
+ "it, or when the holder's Session has closed. Holding authority is a "
+ "precondition for submitting, and exists so that two clients cannot interleave "
+ "motion; it is NOT the single point of control that ISO 10218-2 requires, "
+ "which is enforced by safety-rated means outside this interface.",
+ MR_Mandatory,
+ outargs=[("Granted", Boolean, "True when the caller now holds authority."),
+ ("CurrentOwner", NodeId_,
+ "SessionId of the holder after the call.")])
+method(IC, "IntentControllerType", "ReleaseControl",
+ "Give up command authority. Outstanding intents are unaffected; use "
+ "CancelAll to stop them.", MR_Mandatory)
+method(IC, "IntentControllerType", "SubmitIntent",
+ "Submit one intent. Returns as soon as the intent is admitted - NOT when the "
+ "robot has finished, which may be minutes later. The returned Operation is a "
+ "node the client subscribes to for progress and reads for the result. This is "
+ "the whole reason the model is built on the Part 10 program lifecycle: an OPC "
+ "UA Call cannot stay open for the duration of a motion.",
+ MR_Mandatory,
+ inargs=[("Intent", IntentDataType, "The intent to execute.")],
+ outargs=[("Accepted", Boolean, "True when the intent was admitted. False is a "
+ "refusal, which clause 10.5 makes an ordinary "
+ "outcome rather than an error."),
+ ("IntentId", String, "Identifier of the intent, assigned by the "
+ "Server when the request left it empty. Empty on "
+ "a refusal."),
+ ("Operation", NodeId_, "The IntentOperation that tracks it. Null on a "
+ "refusal, because a refusal creates no "
+ "operation instance."),
+ ("Failure", IntentFailureEnum, "Why the intent was refused, or None "
+ "when it was admitted."),
+ ("Message", LocalizedText, "Human-readable detail on a refusal. For a "
+ "human; never parsed.")])
+method(IC, "IntentControllerType", "CancelIntent",
+ "Ask the Server to end an intent early. The Server MAY refuse, and says so in "
+ "Accepted, because some motions cannot be abandoned safely part-way. This is "
+ "not the OPC UA Cancel Service, which discards a pending response and leaves "
+ "the robot moving; see clause 6.5.",
+ MR_Mandatory,
+ inargs=[("IntentId", String, "The intent to cancel."),
+ ("StopMode", StopModeEnum, "How urgently to stop.")],
+ outargs=[("Accepted", Boolean, "True when the Server will act on it.")])
+method(IC, "IntentControllerType", "CancelAll",
+ "Ask the Server to end every outstanding intent and mission.", MR_Mandatory,
+ inargs=[("StopMode", StopModeEnum, "How urgently to stop.")],
+ outargs=[("Cancelled", UInt32, "How many were acted on.")])
+method(IC, "IntentControllerType", "Pause",
+ "Suspend execution, retaining position so it can be resumed.", MR_Optional,
+ outargs=[("Accepted", Boolean, "True when execution is suspending.")])
+method(IC, "IntentControllerType", "Resume",
+ "Continue execution suspended by Pause.", MR_Optional,
+ outargs=[("Accepted", Boolean, "True when execution is resuming.")])
+method(IC, "IntentControllerType", "Retry",
+ "Re-attempt an intent that terminated Retriable.", MR_Optional,
+ inargs=[("IntentId", String, "The intent to re-attempt.")],
+ outargs=[("Accepted", Boolean, "True when a new attempt was admitted."),
+ ("Operation", NodeId_, "The IntentOperation that tracks the new "
+ "attempt. Null on a refusal."),
+ ("Failure", IntentFailureEnum, "Why the re-attempt was refused, or "
+ "None when it was admitted."),
+ ("Message", LocalizedText, "Human-readable detail on a refusal.")])
+method(IC, "IntentControllerType", "SubmitMission",
+ "Submit an ordered sequence of intents as one unit. Steps marked Released form "
+ "the base and are committed; the rest form the horizon and may still be "
+ "revised by UpdateMission.", MR_Optional,
+ inargs=[("Mission", MissionDataType, "The mission to execute.")],
+ outargs=[("Accepted", Boolean, "True when the mission was admitted."),
+ ("MissionId", String, "Identifier of the mission, assigned by the "
+ "Server when the request left it empty. Empty "
+ "on a refusal."),
+ ("Operation", NodeId_, "The Mission that tracks it. Null on a "
+ "refusal."),
+ ("Failure", IntentFailureEnum, "Why the mission was refused, or None "
+ "when it was admitted."),
+ ("Message", LocalizedText, "Human-readable detail on a refusal.")])
+method(IC, "IntentControllerType", "UpdateMission",
+ "Replace the horizon of a mission already submitted. The base is untouchable: "
+ "it has been committed and may already have executed, so an update that would "
+ "alter a released step is refused rather than partly applied.", MR_Optional,
+ inargs=[("MissionId", String, "The mission to update."),
+ ("MissionUpdateId", UInt32, "Revision of the update. Must be greater "
+ "than the mission's current value."),
+ ("Steps", MissionStepDataType, "The steps that replace the horizon.",
+ 1)],
+ outargs=[("Result", MissionUpdateResultEnum, "Outcome of the update."),
+ ("Message", LocalizedText, "Human-readable detail on a refusal.")])
+method(IC, "IntentControllerType", "CancelMission",
+ "Ask the Server to end a mission and every intent belonging to it.",
+ MR_Optional,
+ inargs=[("MissionId", String, "The mission to cancel."),
+ ("StopMode", StopModeEnum, "How urgently to stop.")],
+ outargs=[("Accepted", Boolean, "True when the Server will act on it.")])
+
+object_type(1003, "IntentOperationType", ProgramStateMachineType,
+ "One submitted intent, tracked to completion. It is a Part 10 program "
+ "instance, so its lifecycle is the one OPC UA already defines for work "
+ "that outlives a service call: transitions raise ProgramTransitionEvents, "
+ "the terminal result survives in FinalResultData, and "
+ "ProgramDiagnostic2DataType records which Session commanded it without "
+ "this specification having to model provenance itself.")
+IO = 1003
+prop_var(IO, "IntentOperationType", "IntentId", String,
+ "Identifier of the intent this instance executes.", MR_Mandatory)
+data_var(IO, "IntentOperationType", "Intent", IntentDataType,
+ "The intent as admitted, after the Server applied its defaults. A client "
+ "reads this to learn what it actually asked for.", MR_Mandatory)
+prop_var(IO, "IntentOperationType", "ExecutionState", ExecutionStateEnum,
+ "Fine-grained state. It refines CurrentState rather than restating it; "
+ "clause 6.3 fixes which pairs are legal.", MR_Mandatory)
+prop_var(IO, "IntentOperationType", "Progress", Double,
+ "Fraction of the intent completed, 0 to 1, where the Server can estimate it. "
+ "Negative means it cannot.")
+data_var(IO, "IntentOperationType", "CurrentPose", Pose3DDataType,
+ "Where the driven tool centre point is now. Subscribe for tracking; this is "
+ "a status report at the sampling rate the client asks for, not a control "
+ "signal, and clause 4.3 explains why it must not be used as one.")
+data_var(IO, "IntentOperationType", "Result", IntentResultDataType,
+ "Outcome, meaningful once ExecutionState is terminal. The same value is "
+ "placed under FinalResultData so a Part 10 client finds it where Part 10 "
+ "says it will be.", MR_Mandatory)
+prop_var(IO, "IntentOperationType", "MissionId", String,
+ "The mission this intent belongs to, or empty when it was submitted alone.")
+prop_var(IO, "IntentOperationType", "QueuePosition", UInt32,
+ "Place in the queue while ExecutionState is Queued, 1 being next. Zero once "
+ "it is no longer queued.")
+
+object_type(1004, "MissionType", ProgramStateMachineType,
+ "One submitted mission, tracked to completion. It is a Part 10 program "
+ "instance for the same reasons an IntentOperation is, and it owns the "
+ "IntentOperations of its steps.")
+MI = 1004
+prop_var(MI, "MissionType", "MissionId", String,
+ "Identifier of the mission this instance executes.", MR_Mandatory)
+prop_var(MI, "MissionType", "MissionUpdateId", UInt32,
+ "Revision currently in force.", MR_Mandatory)
+data_var(MI, "MissionType", "Mission", MissionDataType,
+ "The mission as it now stands, base and horizon together.", MR_Mandatory)
+prop_var(MI, "MissionType", "ExecutionState", ExecutionStateEnum,
+ "Fine-grained state, refining CurrentState as clause 6.3 fixes.",
+ MR_Mandatory)
+prop_var(MI, "MissionType", "CurrentStepId", String,
+ "The step executing now, or empty.", MR_Mandatory)
+prop_var(MI, "MissionType", "ReleasedStepCount", UInt32,
+ "How many steps are in the base. The first this many steps of Mission.Steps "
+ "are committed; the rest are the horizon.", MR_Mandatory)
+
+object_type(1005, "IntentCapabilitiesType", BaseObjectType,
+ "What one robot will accept. A client reads this once, before it submits "
+ "anything, and knows what the robot can do and under what constraints.")
+CP = 1005
+data_var(CP, "IntentCapabilitiesType", "SupportedIntents", IntentCapabilityDataType,
+ "One entry per intent type this Server accepts.", MR_Mandatory, valuerank="1")
+prop_var(CP, "IntentCapabilitiesType", "MissionsSupported", Boolean,
+ "True when SubmitMission is implemented.", MR_Mandatory)
+prop_var(CP, "IntentCapabilitiesType", "MissionHorizonSupported", Boolean,
+ "True when UpdateMission can revise the horizon of a running mission.",
+ MR_Mandatory)
+prop_var(CP, "IntentCapabilitiesType", "BlendingSupported", Boolean,
+ "True when the blending buffer modes actually blend. A Server that treats "
+ "them as Buffered reports False, so a client is not misled about the path.",
+ MR_Mandatory)
+prop_var(CP, "IntentCapabilitiesType", "MaxBlendRadius", Double,
+ "Largest blend radius the robot will honour, in metres. Zero or less means "
+ "it is not bounded here.")
+prop_var(CP, "IntentCapabilitiesType", "AxisCount", UInt32,
+ "How many axes JointMoveIntentDataType.JointTargets must carry.",
+ MR_Mandatory)
+
+object_type(1006, "CoordinateFrameType", BaseObjectType,
+ "A named right-handed Cartesian frame. Frames form a tree through "
+ "HasFrameParent, so a client can compose a chain from a tool frame to a "
+ "world frame. Roles follow ISO 9787, which standardises which frames "
+ "exist; the transform between them is carried explicitly because no "
+ "standard says how to calibrate it.")
+CFR = 1006
+prop_var(CFR, "CoordinateFrameType", "FrameId", String,
+ "Identifier referenced by Pose3DDataType.FrameId. Unique among the frames "
+ "of one controller.", MR_Mandatory)
+prop_var(CFR, "CoordinateFrameType", "Role", FrameRoleEnum,
+ "Role of this frame.", MR_Mandatory)
+data_var(CFR, "CoordinateFrameType", "Transform", Pose3DDataType,
+ "Pose of this frame within its parent. Ignored for a root frame.")
+
+object_type(1007, "ToolType", BaseObjectType,
+ "An end effector. Its tool centre point is a CoordinateFrame of role "
+ "Tool, which is what a motion intent drives to a target - OPC 40010-1 "
+ "models robot topology in detail but has no tool centre point at all, so "
+ "this specification supplies one.")
+TL = 1007
+prop_var(TL, "ToolType", "ToolId", String, "Identifier unique within the controller.",
+ MR_Mandatory)
+prop_var(TL, "ToolType", "Name", LocalizedText, "Human-readable name.", MR_Mandatory)
+prop_var(TL, "ToolType", "Fitted", Boolean,
+ "True when this tool is on the robot now.", MR_Mandatory)
+prop_var(TL, "ToolType", "TcpFrame", NodeId_,
+ "The CoordinateFrame, of role Tool, that is this tool's centre point.",
+ MR_Mandatory)
+prop_var(TL, "ToolType", "Mass", Double, "Mass in kilograms. Zero or less when "
+ "not stated.")
+prop_var(TL, "ToolType", "MaxGraspForce", Double,
+ "Largest grasp force in newtons, or zero when the tool does not grasp.")
+prop_var(TL, "ToolType", "MaxOpening", Double,
+ "Largest opening in metres, or zero when the tool does not grasp.")
+
+object_type(1008, "LocationType", BaseObjectType,
+ "A named place an intent can refer to. Pick and Place reference these "
+ "nodes rather than naming a station in a string, so a location has one "
+ "definition that a client can read, subscribe to and reason about.")
+LC = 1008
+prop_var(LC, "LocationType", "LocationId", String,
+ "Identifier unique within the controller.", MR_Mandatory)
+prop_var(LC, "LocationType", "Name", LocalizedText, "Human-readable name.",
+ MR_Mandatory)
+data_var(LC, "LocationType", "Pose", Pose3DDataType,
+ "Where the location is.", MR_Mandatory)
+prop_var(LC, "LocationType", "Occupied", Boolean,
+ "True when the Server believes something is there.")
+prop_var(LC, "LocationType", "ObjectClass", String,
+ "What the location holds, when it holds one kind of thing. Empty otherwise.")
+prop_var(LC, "LocationType", "Capacity", UInt32,
+ "How many objects it can hold. Zero means unstated.")
+
+object_type(1009, "AxisType", BaseObjectType,
+ "One axis of the robot. The order of these nodes under the controller's "
+ "Axes folder fixes the order of JointMoveIntentDataType.JointTargets, and "
+ "Kind fixes each entry's unit.")
+AX = 1009
+prop_var(AX, "AxisType", "AxisId", String, "Identifier unique within the controller.",
+ MR_Mandatory)
+prop_var(AX, "AxisType", "Index", UInt32,
+ "Position in JointTargets, counting from zero.", MR_Mandatory)
+prop_var(AX, "AxisType", "Kind", AxisKindEnum,
+ "Whether it rotates or translates, which fixes the unit of its joint "
+ "target.", MR_Mandatory)
+prop_var(AX, "AxisType", "MinPosition", Double,
+ "Lower limit, in radians or metres by Kind.", MR_Mandatory)
+prop_var(AX, "AxisType", "MaxPosition", Double,
+ "Upper limit, in radians or metres by Kind.", MR_Mandatory)
+prop_var(AX, "AxisType", "MaxSpeed", Double,
+ "Largest speed, in radians or metres per second by Kind.")
+data_var(AX, "AxisType", "Position", Double,
+ "Where the axis is now, in radians or metres by Kind.")
+
+object_type(1010, "OutputSignalType", BaseObjectType,
+ "A signal SetOutput can write. Modelling it as a node means the range, "
+ "the unit and the meaning are described once, instead of every client "
+ "having to know what a line name implies.")
+OS = 1010
+prop_var(OS, "OutputSignalType", "SignalId", String,
+ "Identifier unique within the controller.", MR_Mandatory)
+prop_var(OS, "OutputSignalType", "Name", LocalizedText, "Human-readable name.",
+ MR_Mandatory)
+data_var(OS, "OutputSignalType", "Value", BaseDataType,
+ "Current value.", MR_Mandatory)
+prop_var(OS, "OutputSignalType", "Writable", Boolean,
+ "True when SetOutput may write it.", MR_Mandatory)
+prop_var(OS, "OutputSignalType", "EngineeringUnits", EUInformation,
+ "Unit of an analogue signal.")
+
+object_type(1011, "ProgramType", BaseObjectType,
+ "A program held on the controller that CallProgram can run. This is the "
+ "bridge to capability this specification does not model, and to the "
+ "programs an OPC 40010-1 task control already exposes.")
+PG = 1011
+prop_var(PG, "ProgramType", "ProgramId", String,
+ "Identifier unique within the controller.", MR_Mandatory)
+prop_var(PG, "ProgramType", "Name", LocalizedText, "Human-readable name.",
+ MR_Mandatory)
+prop_var(PG, "ProgramType", "Description", LocalizedText, "What the program does.")
+data_var(PG, "ProgramType", "Parameters", KeyValuePair,
+ "Named parameters it accepts, with their default values.", valuerank="1")
+
+# ---------------------------------------------------------------------------
+# Well-known instance (7001+)
+# ---------------------------------------------------------------------------
+well_known(7001, "RobotIntent", T(1001), Server,
+ "Entry point for robot intent on this Server. A client browses "
+ "Server/RobotIntent/Controllers to find every robot it can command.")
+
+
+
+# ===========================================================================
+# === APPENDED MEMBERS ====================================================
+# ===========================================================================
+# Everything below APPENDS. New enumerations take 3013+, structures 3070+,
+# encodings continue from 5019, ObjectTypes 1012+, and every instance
+# declaration takes the next free member id, so no previously assigned NodeId
+# moves. Members of types declared earlier are added here rather than beside
+# their siblings for exactly that reason.
+
+ContentFilter = "i=586"
+
+# ---------------------------------------------------------------------------
+# Enumerations (3013+)
+# ---------------------------------------------------------------------------
+enum_type(3013, "SafeMotionFunctionEnum",
+ "The safe motion function a safety system is enforcing, as defined by "
+ "IEC 61800-5-2. This is a REPORT. The safety system enforces these "
+ "independently of this interface, and a client reading them has not thereby "
+ "obtained any safety function - see clause 10.",
+ [("None", 0, "No safe motion function is active."),
+ ("Sto", 1, "Safe Torque Off: torque is removed."),
+ ("Ss1", 2, "Safe Stop 1: a controlled ramp to standstill, then Safe Torque "
+ "Off."),
+ ("Ss2", 3, "Safe Stop 2: a controlled ramp to standstill, which is then "
+ "held under power."),
+ ("Sos", 4, "Safe Operating Stop: standstill is monitored while the drive "
+ "remains energised."),
+ ("Sls", 5, "Safely Limited Speed: speed is monitored against a limit."),
+ ("Slp", 6, "Safely Limited Position: position is monitored against a "
+ "limit."),
+ ("Sdi", 7, "Safe Direction: motion is permitted in one direction only."),
+ ("Sbc", 8, "Safe Brake Control: a brake is commanded safely.")])
+SafeMotionFunctionEnum = T(3013)
+
+enum_type(3014, "RealTimeTransportEnum",
+ "The transport of a brokered real-time channel. This specification defines "
+ "none of these: it describes them so a client can find and open one, and the "
+ "samples never traverse this interface.",
+ [("Rtde", 0, "Universal Robots Real-Time Data Exchange."),
+ ("Egm", 1, "ABB Externally Guided Motion."),
+ ("Fri", 2, "KUKA Fast Research Interface."),
+ ("Rsi", 3, "KUKA Robot Sensor Interface."),
+ ("MotoRos2", 4, "Yaskawa MotoROS2."),
+ ("OpcUaFx", 5, "OPC UA FX (OPC 10000-80 to -84). The open path, and the "
+ "only one in this list that is an OPC Foundation "
+ "specification."),
+ ("Other", 6, "A transport identified by the channel's own descriptor.")])
+RealTimeTransportEnum = T(3014)
+
+enum_type(3015, "ChannelInitiatorEnum",
+ "Which end opens the transport connection of a brokered channel. Getting "
+ "this wrong is the usual reason a first connection attempt fails, so it is "
+ "stated rather than left to the reader.",
+ [("Server", 0, "The Server connects to an endpoint the client is listening "
+ "on."),
+ ("Client", 1, "The client connects to the endpoint the Server publishes.")])
+ChannelInitiatorEnum = T(3015)
+
+enum_type(3016, "ErrorPolicyEnum",
+ "What a mission does when one of its steps does not succeed. Without this a "
+ "mission can only abort, which forces every recovery out into the client.",
+ [("Abort", 0, "End the mission. This is the default and the behaviour of a "
+ "mission that declares no policy."),
+ ("Retry", 1, "Re-attempt the step. A Server bounds the attempts and reports "
+ "Failed when they are exhausted."),
+ ("Skip", 2, "Record the failure and continue with the next step."),
+ ("Fallback", 3, "Continue at FallbackStepId instead."),
+ ("Compensate", 4, "Run the fallback step to undo the work already done, "
+ "then end the mission.")])
+ErrorPolicyEnum = T(3016)
+
+enum_type(3017, "DivergenceKindEnum",
+ "How the transitions leaving one step relate to each other, following the "
+ "divergence of an IEC 61131-3 sequential function chart.",
+ [("Alternative", 0, "Exactly one transition is taken - the first whose "
+ "condition holds. An OR divergence."),
+ ("Parallel", 1, "Every transition is taken and the branches run "
+ "concurrently. An AND divergence.")])
+DivergenceKindEnum = T(3017)
+
+enum_type(3018, "WeaveShapeEnum",
+ "The oscillation applied across an arc weld seam.",
+ [("None", 0, "No weave."),
+ ("Sine", 1, "Sinusoidal oscillation."),
+ ("Zigzag", 2, "Triangular oscillation."),
+ ("Trapezoid", 3, "Trapezoidal oscillation, with a dwell at each edge.")])
+WeaveShapeEnum = T(3018)
+
+# ---------------------------------------------------------------------------
+# Structured DataTypes (3070+)
+# ---------------------------------------------------------------------------
+struct_type(3070, "TrajectoryPointDataType",
+ "One point of a time-parameterised path. Positions are per axis in the "
+ "order the axes are declared, in radians or metres by AxisKind. "
+ "TimeFromStart is measured from the start of the trajectory, which is what "
+ "makes the path a trajectory rather than a list of waypoints. Velocities "
+ "and Accelerations are optional and may be empty.",
+ [("TimeFromStart", Duration, "Milliseconds from the start of the "
+ "trajectory."),
+ ("Positions", Double, "One value per axis.", 1, 0),
+ ("Velocities", Double, "One value per axis, or empty.", 1, 0),
+ ("Accelerations", Double, "One value per axis, or empty.", 1, 0)])
+TrajectoryPointDataType = T(3070)
+
+struct_type(3071, "MotionToleranceDataType",
+ "How far execution may deviate before it is a failure. A tolerance of zero "
+ "or less means the Server applies its own.",
+ [("Position", Double, "Positional tolerance in metres."),
+ ("Orientation", Double, "Orientation tolerance in radians."),
+ ("Time", Duration, "Timing tolerance in milliseconds.")])
+MotionToleranceDataType = T(3071)
+
+struct_type(3072, "TrajectoryIntentDataType",
+ "Execute a time-parameterised path. The Server's own motion kernel runs "
+ "it; this interface hands the whole trajectory over in one submission and "
+ "does not stream it. That is what makes trajectory execution expressible "
+ "here when real-time control is not - see clause 4.3.",
+ [("Points", TrajectoryPointDataType, "The trajectory, in ascending "
+ "TimeFromStart order.", 1, 0),
+ ("PathTolerance", MotionToleranceDataType, "Permitted deviation while "
+ "following the path."),
+ ("GoalTolerance", MotionToleranceDataType, "Permitted deviation at the "
+ "final point."),
+ ("GoalTimeTolerance", Duration, "How much later than the final point's "
+ "TimeFromStart completion may be, in "
+ "milliseconds.")],
+ base=MotionIntentDataType)
+TrajectoryIntentDataType = T(3072)
+
+struct_type(3073, "PathWaypointDataType",
+ "One waypoint of a Cartesian path, with the blend that applies at it. "
+ "Per-waypoint blending is what distinguishes a path from a sequence of "
+ "separate linear moves: the robot need not stop between them.",
+ [("Pose", Pose3DDataType, "Where the tool centre point passes."),
+ ("Blend", BlendDataType, "How this waypoint is left.")])
+PathWaypointDataType = T(3073)
+
+struct_type(3074, "CartesianPathIntentDataType",
+ "Follow a list of Cartesian waypoints. This is the portable form of a "
+ "taught path, and unlike a trajectory it carries no timing: the Server "
+ "paces it from the motion constraints.",
+ [("Waypoints", PathWaypointDataType, "The path, in order.", 1, 0)],
+ base=MotionIntentDataType)
+CartesianPathIntentDataType = T(3074)
+
+struct_type(3075, "ForceIntentDataType",
+ "Move until contact. The portable subset of the force-controlled moves "
+ "every vendor offers: travel along a direction until a contact force is "
+ "reached or a distance is exhausted, whichever comes first. Reaching the "
+ "distance without contact is a failure, because the intent was to touch "
+ "something.",
+ [("Direction", Double, "Unit direction of travel (x, y, z) in the frame of "
+ "the target.", 1, 3),
+ ("FrameId", String, "The CoordinateFrame Direction is expressed in; empty "
+ "for the default work frame."),
+ ("ContactForce", Double, "The force in newtons at which contact is "
+ "declared."),
+ ("MaxDistance", Double, "How far to travel before giving up, in metres."),
+ ("HoldForce", Boolean, "True to keep pressing at ContactForce after "
+ "contact rather than stopping.")],
+ base=MotionIntentDataType)
+ForceIntentDataType = T(3075)
+
+struct_type(3076, "ProcessIntentDataType",
+ "Abstract base of the intents that run an application process along a "
+ "path. Every process needs the same two things beyond its own parameters: "
+ "a reference to the process program or procedure the equipment holds, and "
+ "room for the parameters this specification has not standardised.",
+ [("ProcessProgram", NodeId_, "The Program or equipment-side procedure to "
+ "run, or null when the parameters here are "
+ "sufficient."),
+ ("Attributes", KeyValuePair, "Further named parameters the Server "
+ "declares in its capabilities.", 1, 0)],
+ base=MotionIntentDataType, abstract=True)
+ProcessIntentDataType = T(3076)
+
+struct_type(3077, "ArcWeldIntentDataType",
+ "Lay an arc weld along the path. The parameters are the subset ABB "
+ "seamdata/welddata/weavedata, FANUC weld schedules and KUKA ArcTech all "
+ "carry. WeldProcedureRef points at a welding procedure specification "
+ "(ISO 15609) where the installation works to one; this specification does "
+ "not restate its content.",
+ [("Voltage", Double, "Arc voltage in volts."),
+ ("WireFeedSpeed", Double, "Wire feed speed in metres per minute."),
+ ("TravelSpeed", Double, "Tool centre point speed in metres per second."),
+ ("GasPreflowTime", Duration, "Shielding gas pre-flow, in milliseconds."),
+ ("GasPostflowTime", Duration, "Shielding gas post-flow, in milliseconds."),
+ ("ArcStartDelay", Duration, "Delay before travel begins, in milliseconds."),
+ ("CraterFillTime", Duration, "Crater fill at the end of the seam, in "
+ "milliseconds."),
+ ("WeaveShape", WeaveShapeEnum, "Oscillation across the seam."),
+ ("WeaveAmplitude", Double, "Peak-to-peak weave width in metres."),
+ ("WeaveFrequency", Double, "Weave frequency in hertz."),
+ ("SeamTrackingEnabled", Boolean, "True to enable seam tracking where the "
+ "equipment provides it."),
+ ("WeldProcedureRef", String, "Identifier of the welding procedure "
+ "specification this weld works to.")],
+ base=ProcessIntentDataType)
+ArcWeldIntentDataType = T(3077)
+
+struct_type(3078, "SpotWeldIntentDataType",
+ "Make a resistance spot weld at the target. WeldSchedule selects the weld "
+ "controller's own program: current and time are the weld controller's "
+ "business, and are carried here only where the installation drives them "
+ "from the robot.",
+ [("WeldSchedule", UInt32, "The weld controller program to run."),
+ ("GunForce", Double, "Electrode force in newtons."),
+ ("ApproachDistance", Double, "Gun open distance before the weld, in "
+ "metres."),
+ ("RetractDistance", Double, "Gun open distance after the weld, in "
+ "metres."),
+ ("MaterialThickness", Double, "Total stack thickness in metres, where the "
+ "schedule is chosen adaptively."),
+ ("TipDressRequested", Boolean, "True to dress the tips after this weld.")],
+ base=ProcessIntentDataType)
+SpotWeldIntentDataType = T(3078)
+
+struct_type(3079, "DispenseIntentDataType",
+ "Lay a bead of adhesive, sealant or paint along the path. The trigger "
+ "distances exist because material does not start and stop instantly: a "
+ "Server begins dispensing before the path and stops before its end.",
+ [("FlowRate", Double, "Nominal flow in millilitres per minute."),
+ ("TriggerOnDistance", Double, "Distance before the path start at which "
+ "dispensing begins, in metres."),
+ ("TriggerOffDistance", Double, "Distance before the path end at which "
+ "dispensing stops, in metres."),
+ ("BeadWidth", Double, "Target bead width in metres."),
+ ("MaterialTemperature", Double, "Material temperature in degrees Celsius, "
+ "for hot-melt work."),
+ ("PurgeCycles", UInt32, "Nozzle purge cycles before starting.")],
+ base=ProcessIntentDataType)
+DispenseIntentDataType = T(3079)
+
+struct_type(3080, "FastenIntentDataType",
+ "Drive a fastener at the target. This intent is deliberately THIN: "
+ "OPC 40450 and OPC 40451 already define joining and tightening in full, "
+ "so Joint references the joint in that model and the result belongs there. "
+ "Restating those parameters here would create a second definition of the "
+ "same fact.",
+ [("Joint", NodeId_, "The joint being fastened, in an OPC UA joining or "
+ "tightening model where one is implemented."),
+ ("ProgramNumber", UInt32, "The tightening program the tool is to run."),
+ ("TargetTorque", Double, "Target torque in newton metres, where the robot "
+ "supplies it rather than the tool."),
+ ("TargetAngle", Double, "Target angle in radians, for angle-controlled "
+ "strategies."),
+ ("SnugTorque", Double, "Torque at which angle counting begins, in newton "
+ "metres.")],
+ base=ProcessIntentDataType)
+FastenIntentDataType = T(3080)
+
+struct_type(3081, "PalletiseIntentDataType",
+ "Place an item into a pattern. The pattern itself is a Location, so its "
+ "geometry has one definition that a client can read rather than being "
+ "recomputed from indices on both sides.",
+ [("Pattern", NodeId_, "The Location describing the pallet or pattern."),
+ ("Layer", UInt32, "Layer index, counting from zero."),
+ ("Row", UInt32, "Row index within the layer, counting from zero."),
+ ("Column", UInt32, "Column index within the row, counting from zero."),
+ ("ItemOrientation", Double, "Rotation of the item about the pattern "
+ "normal, in radians.")],
+ base=ProcessIntentDataType)
+PalletiseIntentDataType = T(3081)
+
+struct_type(3082, "SurfaceFinishIntentDataType",
+ "Follow the path pressing into the surface - grinding, polishing, "
+ "deburring or sanding. ContactForce is what distinguishes it from a plain "
+ "path: the robot yields normal to the surface to hold that force.",
+ [("ContactForce", Double, "Force normal to the surface, in newtons."),
+ ("FeedRate", Double, "Travel speed along the surface, in metres per "
+ "second."),
+ ("ToolSpeed", Double, "Rotational speed of the tool, in revolutions per "
+ "minute."),
+ ("StepOver", Double, "Lateral offset between adjacent passes, in "
+ "metres.")],
+ base=ProcessIntentDataType)
+SurfaceFinishIntentDataType = T(3082)
+
+struct_type(3083, "MissionTransitionDataType",
+ "One edge of a mission's step graph, following the step-and-transition "
+ "form of an IEC 61131-3 sequential function chart. Condition is an OPC UA "
+ "ContentFilter - the base specification's own filter grammar, reused so "
+ "that this specification does not invent an expression language for "
+ "implementers to write a parser for.",
+ [("FromStepId", String, "The step this transition leaves."),
+ ("ToStepId", String, "The step this transition enters."),
+ ("Condition", ContentFilter, "The condition under which it is taken. An "
+ "empty filter is always true."),
+ ("DivergenceKind", DivergenceKindEnum, "How this transition relates to "
+ "the others leaving the same "
+ "step.")])
+MissionTransitionDataType = T(3083)
+
+struct_type(3084, "KinematicJointDataType",
+ "One joint of the kinematic chain: where it sits relative to its "
+ "predecessor and which way it acts. OPC 40010-1 describes a robot's "
+ "topology and its axes but defines no kinematic chain, so this is additive "
+ "rather than a second account of the same thing.",
+ [("AxisId", String, "The AxisType this joint corresponds to."),
+ ("Kind", AxisKindEnum, "Whether it rotates or translates."),
+ ("OriginTransform", Pose3DDataType, "Pose of this joint's frame within "
+ "its predecessor's, at zero "
+ "position."),
+ ("AxisVector", Double, "Unit vector (x, y, z) the joint rotates about or "
+ "translates along, in its own frame.", 1, 3)])
+KinematicJointDataType = T(3084)
+
+# ---------------------------------------------------------------------------
+# ObjectTypes (1012+)
+# ---------------------------------------------------------------------------
+object_type(1012, "SafetyStateType", BaseObjectType,
+ "What the robot's safety system is doing, reported so a client can act "
+ "sensibly around it. Every member is READ-ONLY and every one is a report: "
+ "the safety system enforces these independently, remains effective when "
+ "this interface is unreachable, and is not commanded from here. Clause 10 "
+ "states the boundary and this type does not cross it.")
+SS = 1012
+prop_var(SS, "SafetyStateType", "ActiveFunction", SafeMotionFunctionEnum,
+ "The safe motion function currently enforced, per IEC 61800-5-2.",
+ MR_Mandatory)
+prop_var(SS, "SafetyStateType", "EmergencyStopActive", Boolean,
+ "True while an emergency stop is asserted.", MR_Mandatory)
+prop_var(SS, "SafetyStateType", "ProtectiveStopActive", Boolean,
+ "True while a protective stop is asserted.", MR_Mandatory)
+prop_var(SS, "SafetyStateType", "SafeSpeedLimitActive", Boolean,
+ "True while a safely limited speed is being enforced.", MR_Mandatory)
+prop_var(SS, "SafetyStateType", "SafeSpeedLimit", Double,
+ "The tool centre point speed limit being enforced, in metres per second. "
+ "Meaningful only while SafeSpeedLimitActive. Clause 10.3 requires a Server to "
+ "refuse an intent that asks to exceed it.", MR_Mandatory)
+prop_var(SS, "SafetyStateType", "SafetyControllerOk", Boolean,
+ "False when the safety system reports its own fault. A Server accepts no "
+ "intent while it is false.", MR_Mandatory)
+prop_var(SS, "SafetyStateType", "LastStopReason", LocalizedText,
+ "Why the last stop occurred, for a human. Never parsed.")
+
+object_type(1013, "RealTimeChannelType", BaseObjectType,
+ "A high-rate channel this Server can offer, described so a client can open "
+ "it. The samples never traverse OPC UA: this brokers the endpoint and "
+ "nothing more, in the same way the Vision model brokers a media endpoint "
+ "rather than carrying pixels. Clause 4.3 explains why the alternative is "
+ "not available.")
+RC = 1013
+prop_var(RC, "RealTimeChannelType", "ChannelId", String,
+ "Identifier unique within the controller.", MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "Transport", RealTimeTransportEnum,
+ "The transport this channel speaks.", MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "EndpointUrl", String,
+ "Where the channel is reached. Its scheme and form are the transport's, not "
+ "this specification's.", MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "Initiator", ChannelInitiatorEnum,
+ "Which end opens the connection.", MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "NominalRate", Double,
+ "The rate the channel runs at, in hertz.", MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "PayloadDescriptor", String,
+ "The recipe, signal list or configuration the transport requires, in the "
+ "transport's own form.")
+prop_var(RC, "RealTimeChannelType", "RequiredMode", OperationalModeEnum,
+ "The operational mode the robot must be in before the channel will carry "
+ "motion.", MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "Available", Boolean,
+ "True when the channel can be opened now.", MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "LeaseHolder", NodeId_,
+ "SessionId of the client currently holding the channel, or null.",
+ MR_Mandatory)
+prop_var(RC, "RealTimeChannelType", "LeaseExpiry", UtcTime,
+ "When the current lease lapses. A lease that is not renewed frees the "
+ "channel, so a client that dies does not hold it for good.")
+
+object_type(1014, "RobotDescriptionType", BaseObjectType,
+ "Enough of the robot's construction for a client to plan against it "
+ "without a second specification: the kinematic chain, the space it can "
+ "reach, and what it can carry. OPC 40010-1 describes topology and axes and "
+ "defines no kinematic chain and no tool centre point, so this adds rather "
+ "than restates - Annex B says which side decides where both are present.")
+RD = 1014
+prop_var(RD, "RobotDescriptionType", "Manufacturer", LocalizedText,
+ "Who made the robot.")
+prop_var(RD, "RobotDescriptionType", "Model", String, "The robot's model designation.")
+data_var(RD, "RobotDescriptionType", "KinematicChain", KinematicJointDataType,
+ "The joints from the base outwards, in order.", MR_Mandatory, valuerank="1")
+data_var(RD, "RobotDescriptionType", "MountingPose", Pose3DDataType,
+ "Pose of the robot base within the world frame.")
+prop_var(RD, "RobotDescriptionType", "ReachRadius", Double,
+ "Radius of the reachable workspace from the base, in metres.", MR_Mandatory)
+prop_var(RD, "RobotDescriptionType", "PayloadLimit", Double,
+ "Largest payload at the mechanical interface, in kilograms.", MR_Mandatory)
+prop_var(RD, "RobotDescriptionType", "MaxCartesianSpeed", Double,
+ "Largest tool centre point speed the robot will produce, in metres per "
+ "second.", MR_Mandatory)
+prop_var(RD, "RobotDescriptionType", "MaxCartesianAcceleration", Double,
+ "Largest tool centre point acceleration, in metres per second squared.")
+
+# ---------------------------------------------------------------------------
+# Members appended to types declared earlier
+# ---------------------------------------------------------------------------
+obj_member(IC, "IntentControllerType", "SafetyState", T(1012),
+ "What the safety system is doing. A client reads this before it plans, and "
+ "subscribes to it so that it learns of a stop rather than inferring one "
+ "from a refusal.", MR_Mandatory)
+obj_member(IC, "IntentControllerType", "Description", T(1014),
+ "The robot's kinematics and limits.")
+folder_member(IC, "IntentControllerType", "RealTimeChannels",
+ "The high-rate channels this Server can broker.", MR_Optional)
+
+method(IC, "IntentControllerType", "OpenRealTimeChannel",
+ "Take a lease on a brokered real-time channel. The Server prepares the "
+ "transport and returns what the client needs in order to connect; it does not "
+ "carry the samples. A lease that is not renewed lapses, which is what stops a "
+ "dead client from holding the channel.", MR_Optional,
+ inargs=[("ChannelId", String, "The channel to open."),
+ ("RequestedLease", Duration, "How long the lease is wanted for, in "
+ "milliseconds.")],
+ outargs=[("Granted", Boolean, "True when the lease was taken."),
+ ("EndpointUrl", String, "Where to connect."),
+ ("PayloadDescriptor", String, "The transport's own configuration."),
+ ("LeaseExpiry", UtcTime, "When the lease lapses."),
+ ("Message", LocalizedText, "Human-readable detail on a refusal. For a "
+ "human; never parsed.")])
+method(IC, "IntentControllerType", "CloseRealTimeChannel",
+ "Give up a lease on a brokered channel.", MR_Optional,
+ inargs=[("ChannelId", String, "The channel to release.")],
+ outargs=[("Released", Boolean, "True when the lease was held and is now "
+ "released.")])
+
+prop_var(CP, "IntentCapabilitiesType", "TrajectorySupported", Boolean,
+ "True when trajectory and Cartesian path intents are accepted.", MR_Mandatory)
+prop_var(CP, "IntentCapabilitiesType", "ForceControlSupported", Boolean,
+ "True when force intents are accepted and the robot can actually regulate "
+ "force. A Server that would ignore the force reports false rather than "
+ "accepting an intent it cannot honour.", MR_Mandatory)
+prop_var(CP, "IntentCapabilitiesType", "RealTimeChannelsSupported", Boolean,
+ "True when the Server brokers real-time channels.", MR_Mandatory)
+prop_var(CP, "IntentCapabilitiesType", "MissionBranchingSupported", Boolean,
+ "True when mission transitions are evaluated. A Server that reports false "
+ "executes the steps in order and ignores any transitions supplied.",
+ MR_Mandatory)
+prop_var(CP, "IntentCapabilitiesType", "MaxTrajectoryPoints", UInt32,
+ "Largest number of points accepted in one trajectory. Zero means the Server "
+ "states no limit.")
+
+# Promotions of inherited Part 10 members.
+#
+# Clause 6.7 requires the result to be reachable under FinalResultData, and clauses 1.2
+# and 6.1 claim that every intent records which Session commanded it and with what
+# arguments. Both members are OPTIONAL in ProgramStateMachineType, so until they are
+# promoted here a fully conformant Server provides neither and the claims are false
+# against a legal implementation. Allocated at the end because member ids append.
+#
+# Both are emitted with a namespace-0 BrowseName. OPC 10000-3 says a subtype overrides
+# an inherited InstanceDeclaration by re-declaring a child with the same QualifiedName,
+# and a QualifiedName includes the namespace index - so 1:ProgramDiagnostic does not
+# override 0:ProgramDiagnostic, it declares a second member beside it and leaves the
+# inherited Optional one in place. That is the same failure the TypeDefinition and
+# reference type below are careful to avoid, reached by a different route, and it
+# defeats the entire point of promoting. Every other NodeSet in the reference stack
+# promotes inherited state-machine members unprefixed for this reason.
+_promoted = obj_member(
+ IO, "IntentOperationType", "FinalResultData", BaseObjectType,
+ "Part 10 result container. Carries the same IntentResultDataType value as "
+ "Result, so a Part 10 client finds the outcome where Part 10 says it will "
+ "be. Mandatory here because clause 6.7 requires it and an Optional member "
+ "cannot carry a SHALL.", MR_Mandatory)
+NODES[_promoted].attrs["BrowseNameNamespace"] = 0
+_promoted = _member_var(
+ IO, "IntentOperationType", "ProgramDiagnostic",
+ ProgramDiagnostic2DataType, ProgramDiagnostic2Type, MR_Mandatory,
+ HasComponent,
+ "Part 10 invocation diagnostics: which Session invoked the program, when, "
+ "with what arguments and to what outcome. Mandatory here because the "
+ "auditable-commanding property of clause 1.2 is exactly this member, and a "
+ "capability the specification advertises cannot rest on one a Server may "
+ "omit. Declared exactly as OPC 10000-10 declares it - a Variable of "
+ "ProgramDiagnostic2Type reached by HasComponent, with the inherited "
+ "namespace-0 BrowseName - because a promotion that altered the inherited "
+ "member's TypeDefinition, reference type or BrowseName namespace would "
+ "declare a second member rather than promote the inherited one.")
+NODES[_promoted].attrs["BrowseNameNamespace"] = 0
+
+# Clause 12 declares conformance in terms of facets but gave a Server nowhere to say
+# which ones it satisfies, so every client had to re-derive the whole of Table 12.2
+# from the address space. Several facet rows are behavioural - "accepted and honoured",
+# "the refusal rules of 6.2" - which no address-space read can settle, so two clients
+# deriving independently could disagree about whether the same Server conforms. This
+# member is the same answer OPC 10000-5 gives with ServerCapabilitiesType.
+# ServerProfileArray: the Server states its claim and clause 9 governs its honesty.
+prop_var(CP, "IntentCapabilitiesType", "SupportedFacets", String,
+ "The facets of Table 12.2 this controller claims, by the names given there. "
+ "A client reads this instead of re-deriving conformance from the address "
+ "space. Clause 12.2 binds the claim: a facet whose structural requirements "
+ "are unmet shall not appear, and the behavioural requirements are subject to "
+ "the honesty rules of clause 9.", MR_Mandatory, valuerank="1")
+
+# ===========================================================================
+# ================================== EMIT =================================
+# ===========================================================================
+_PRIO = {HasModellingRule: 0, HasSubtype: 0, HasTypeDefinition: 1}
+
+
+def _sorted_refs(refs):
+ return sorted(range(len(refs)), key=lambda i: (_PRIO.get(refs[i][0], 2), i))
+
+
+def _fmt_reftype(t):
+ return REFTYPE_ALIAS.get(t, t)
+
+
+def _emit_node(n):
+ tag = n.cls
+ # A DataTypeEncoding browses as "Default Binary" in namespace 0: the BrowseName is
+ # standard, not model-defined. Emitting it as 1:Default Binary is what every real
+ # companion NodeSet avoids, and tooling that resolves encodings by BrowseName
+ # cannot find it.
+ prefix = "" if n.attrs.get("BrowseNameNamespace") == 0 else "1:"
+ a = [f'{tag} NodeId="{T(n.nid)}"', f'BrowseName="{prefix}{sx.escape(n.bname)}"']
+ if "SymbolicName" in n.attrs:
+ a.append(f'SymbolicName="{sx.escape(n.attrs["SymbolicName"])}"')
+ if n.parent is not None:
+ a.append(f'ParentNodeId="{n.parent}"')
+ for k in ("DataType", "ValueRank", "ArrayDimensions"):
+ if k in n.attrs:
+ v = n.attrs[k]
+ if k == "DataType":
+ v = DATATYPE_ALIAS.get(v, v)
+ a.append(f'{k}="{v}"')
+ if n.cls in ("UAObjectType", "UADataType") and n.abstract:
+ a.append('IsAbstract="true"')
+ lines = [" <" + " ".join(a) + ">"]
+ lines.append(f" {sx.escape(n.display)}")
+ if n.desc:
+ lines.append(f" {sx.escape(n.desc)}")
+ if n.category:
+ lines.append(f" {sx.escape(n.category)}")
+ if n.cls == "UAReferenceType" and n.inverse:
+ lines.append(f" {sx.escape(n.inverse)}")
+ lines.append(" ")
+ for i in _sorted_refs(n.refs):
+ rt, tgt, fwd = n.refs[i]
+ fwd_s = "" if fwd else ' IsForward="false"'
+ lines.append(f' '
+ f'{tgt}')
+ lines.append(" ")
+ if n.definition:
+ lines.append(" " + n.definition)
+ if n.value:
+ lines.append(" " + n.value)
+ lines.append(f" {tag}>")
+ return "\n".join(lines)
+
+
+def emit():
+ out = ['',
+ '',
+ '',
+ ' ',
+ f' {NAMESPACE}',
+ ' ',
+ ' ',
+ f' ',
+ f' ',
+ ' ',
+ ' ',
+ ' ']
+ for name, val in ALIASES:
+ out.append(f' {val}')
+ out.append(' ')
+ for nid in ORDER:
+ out.append(_emit_node(NODES[nid]))
+ out.append('')
+ return "\n".join(out) + "\n"
+
+
+def emit_csv():
+ # OPC Foundation NodeIds.csv format: SymbolicName,NodeId,NodeClass
+ return "\n".join(f"{NODES[nid].symbolic},{nid},{NODES[nid].cls[2:]}"
+ for nid in ORDER) + "\n"
+
+
+def _rule_name(nid):
+ for rt, tgt, fwd in NODES[nid].refs:
+ if rt == HasModellingRule:
+ return {MR_Mandatory: "Mandatory", MR_Optional: "Optional",
+ MR_OptionalPlaceholder: "OptionalPlaceholder",
+ MR_MandatoryPlaceholder: "MandatoryPlaceholder"}.get(tgt, "")
+ return ""
+
+
+def _supertype(nid):
+ for rt, tgt, fwd in NODES[nid].refs:
+ if rt == HasSubtype and not fwd:
+ return tgt
+ return ""
+
+
+BASE_TYPE_NAMES = {
+ "i=22": "Structure", "i=29": "Enumeration", "i=58": "BaseObjectType",
+ "i=61": "FolderType", "i=68": "PropertyType", "i=63": "BaseDataVariableType",
+ "i=17602": "BaseInterfaceType", "i=32": "NonHierarchicalReferences",
+ "i=76": "DataTypeEncodingType", "i=24": "BaseDataType",
+ "i=2391": "ProgramStateMachineType",
+}
+
+
+def _dt_name(dt):
+ """Render a DataType or supertype NodeId as a readable name."""
+ if not dt:
+ return ""
+ if dt in BASE_TYPE_NAMES:
+ return BASE_TYPE_NAMES[dt]
+ if dt in DATATYPE_ALIAS:
+ return DATATYPE_ALIAS[dt]
+ if dt.startswith("ns=1;i="):
+ n = NODES.get(int(dt.split("=")[-1]))
+ if n is not None:
+ return n.bname
+ return dt
+
+
+def _rank(vr):
+ return {"-1": "Scalar", "1": "Array"}.get(str(vr), str(vr))
+
+
+def _members_of(nid):
+ """Instance declarations owned by a type, in declaration order."""
+ out = []
+ for m in ORDER:
+ n = NODES[m]
+ if n.parent == T(nid) and n.cls in ("UAVariable", "UAObject", "UAMethod"):
+ out.append(m)
+ return out
+
+
+def _method_args(nid, which):
+ for m in _members_of(nid):
+ n = NODES[m]
+ if n.bname == which and n.value:
+ names = re.findall(r"([^<]*)", n.value)
+ types = re.findall(r"([^<]*)", n.value)
+ types = [t for t in types if t != "i=297"]
+ descs = re.findall(r"([^<]*)", n.value)
+ ranks = re.findall(r"(-?\d+)", n.value)
+ out = []
+ for i, nm in enumerate(names):
+ out.append((nm,
+ _dt_name(types[i]) if i < len(types) else "",
+ "Array" if i < len(ranks) and ranks[i] != "-1" else "Scalar",
+ descs[i] if i < len(descs) else ""))
+ return out
+ return []
+
+
+def _esc(s):
+ return (s or "").replace("|", "\\|")
+
+
+def emit_md():
+ """Annex A. This is the authoritative node reference, so it must carry everything an
+ implementer needs: DataType, ValueRank and ModellingRule for every member, the field
+ list of every structure, the value of every enumeration literal, and the full
+ signature of every Method. A bare NodeId/BrowseName table is not sufficient."""
+ obj_types = [n for n in ORDER if NODES[n].cls == "UAObjectType"]
+ data_types = [n for n in ORDER if NODES[n].cls == "UADataType"]
+ ref_types = [n for n in ORDER if NODES[n].cls == "UAReferenceType"]
+
+ L = ["# OPC UA — Robot Intent — Annex A: Information model (generated)",
+ "",
+ "> Generated by `build_model.py`. Do not edit by hand. Namespace "
+ f"`{NAMESPACE}` (index 1). NodeIds are provisional.",
+ "",
+ "This annex is the authoritative node reference for the specification: it "
+ "carries the DataType, ValueRank and ModellingRule of every member, the field "
+ "list of every structure, the value of every enumeration literal, and the full "
+ "signature of every Method.",
+ ""]
+
+ L += ["## A.1 Type overview", "",
+ "| NodeId | BrowseName | NodeClass | Subtype of |", "|---|---|---|---|"]
+ for nid in ref_types + obj_types + data_types:
+ n = NODES[nid]
+ L.append(f"| ns=1;i={nid} | {n.bname} | {n.cls[2:]} | "
+ f"{_dt_name(_supertype(nid))} |")
+ L.append("")
+
+ L += ["## A.2 ReferenceTypes", "",
+ "| NodeId | BrowseName | InverseName | Subtype of | Description |",
+ "|---|---|---|---|---|"]
+ for nid in ref_types:
+ n = NODES[nid]
+ L.append(f"| ns=1;i={nid} | {n.bname} | {n.inverse} | "
+ f"{_dt_name(_supertype(nid))} | {_esc(n.desc)} |")
+ L.append("")
+
+ L += ["## A.3 ObjectTypes", ""]
+ for nid in obj_types:
+ n = NODES[nid]
+ abstract = " (abstract)" if n.abstract else ""
+ L.append(f"### {n.bname}{abstract} — `ns=1;i={nid}`")
+ L.append("")
+ L.append(f"*Subtype of:* `{_dt_name(_supertype(nid))}`")
+ L.append("")
+ if n.desc:
+ L.append(_esc(n.desc))
+ L.append("")
+ members = _members_of(nid)
+ variables = [m for m in members if NODES[m].cls in ("UAVariable", "UAObject")]
+ methods = [m for m in members if NODES[m].cls == "UAMethod"]
+ if variables:
+ L.append("| BrowseName | NodeClass | DataType | ValueRank | ModellingRule "
+ "| Description |")
+ L.append("|---|---|---|---|---|---|")
+ for m in variables:
+ mn = NODES[m]
+ dt = _dt_name(mn.attrs.get("DataType", ""))
+ vr = _rank(mn.attrs.get("ValueRank", "-1")) if mn.cls == "UAVariable" else ""
+ L.append(f"| {mn.bname} | {mn.cls[2:]} | {dt} | {vr} | "
+ f"{_rule_name(m)} | {_esc(mn.desc)} |")
+ L.append("")
+ for m in methods:
+ mn = NODES[m]
+ L.append(f"**Method `{mn.bname}`** ({_rule_name(m)}) — {_esc(mn.desc)}")
+ L.append("")
+ for which, label in (("InputArguments", "In"),
+ ("OutputArguments", "Out")):
+ args = _method_args(m, which)
+ if not args:
+ continue
+ L.append(f"| {label} | DataType | ValueRank | Meaning |")
+ L.append("|---|---|---|---|")
+ for (an, at, ar, ad) in args:
+ L.append(f"| {an} | {at} | {ar} | {_esc(ad)} |")
+ L.append("")
+ if not _method_args(m, "InputArguments") and \
+ not _method_args(m, "OutputArguments"):
+ L.append("Takes no arguments and returns none.")
+ L.append("")
+
+ L += ["## A.4 DataTypes", ""]
+ for nid in data_types:
+ n = NODES[nid]
+ defn = n.definition or ""
+ is_enum = 'Value="' in defn
+ L.append(f"### {n.bname} — `ns=1;i={nid}`")
+ L.append("")
+ L.append(f"*Subtype of:* `{_dt_name(_supertype(nid))}`")
+ L.append("")
+ if n.desc:
+ L.append(_esc(n.desc))
+ L.append("")
+ if is_enum:
+ L.append("| Name | Value | Description |")
+ L.append("|---|---|---|")
+ for mm in re.finditer(
+ r'|>'
+ r'(?:([^<]*))?)', defn):
+ L.append(f"| {mm.group(1)} | {mm.group(2)} | "
+ f"{_esc(mm.group(3) or '')} |")
+ else:
+ L.append("| Field | DataType | ValueRank | ArrayDimensions | Description |")
+ L.append("|---|---|---|---|---|")
+ for mm in re.finditer(
+ r']*?)(?:/>|>'
+ r'(?:([^<]*))?)', defn):
+ extra = mm.group(3) or ""
+ vr = re.search(r'ValueRank="(-?\d+)"', extra)
+ ad = re.search(r'ArrayDimensions="(\d+)"', extra)
+ L.append(f"| {mm.group(1)} | {_dt_name(mm.group(2))} | "
+ f"{_rank(vr.group(1)) if vr else 'Scalar'} | "
+ f"{ad.group(1) if ad else ''} | "
+ f"{_esc(mm.group(4) or '')} |")
+ L.append("")
+
+ return "\n".join(L).rstrip() + "\n"
+
+
+def main():
+ here = os.path.dirname(os.path.abspath(__file__))
+ std = os.path.normpath(os.path.join(here, "..", "..", "..", "robot-intent"))
+ os.makedirs(std, exist_ok=True)
+ with open(os.path.join(std, "Opc.Ua.RobotIntent.NodeSet2.xml"), "w",
+ encoding="utf-8", newline="\n") as f:
+ f.write(emit())
+ with open(os.path.join(std, "Opc.Ua.RobotIntent.NodeIds.csv"), "w",
+ encoding="utf-8", newline="\n") as f:
+ f.write(emit_csv())
+ with open(os.path.join(here, "model-reference.md"), "w",
+ encoding="utf-8", newline="\n") as f:
+ f.write(emit_md())
+ n_types = sum(1 for k in NODES
+ if NODES[k].cls in ("UAObjectType", "UADataType", "UAReferenceType"))
+ print(f"Wrote NodeSet ({len(ORDER)} nodes, {n_types} types), NodeIds.csv, "
+ "model-reference.md")
+ print(f"Member id range: 6001..{_next_member[0] - 1}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/metaverse-specs/extras/robot-intent/tools/model-reference.md b/metaverse-specs/extras/robot-intent/tools/model-reference.md
new file mode 100644
index 0000000..23430a0
--- /dev/null
+++ b/metaverse-specs/extras/robot-intent/tools/model-reference.md
@@ -0,0 +1,1177 @@
+# OPC UA — Robot Intent — Annex A: Information model (generated)
+
+> Generated by `build_model.py`. Do not edit by hand. Namespace `http://opcfoundation.org/UA/RobotIntent/` (index 1). NodeIds are provisional.
+
+This annex is the authoritative node reference for the specification: it carries the DataType, ValueRank and ModellingRule of every member, the field list of every structure, the value of every enumeration literal, and the full signature of every Method.
+
+## A.1 Type overview
+
+| NodeId | BrowseName | NodeClass | Subtype of |
+|---|---|---|---|
+| ns=1;i=4001 | HasIntentController | ReferenceType | NonHierarchicalReferences |
+| ns=1;i=4002 | HasFrameParent | ReferenceType | NonHierarchicalReferences |
+| ns=1;i=1001 | RobotIntentRootType | ObjectType | BaseObjectType |
+| ns=1;i=1002 | IntentControllerType | ObjectType | BaseObjectType |
+| ns=1;i=1003 | IntentOperationType | ObjectType | ProgramStateMachineType |
+| ns=1;i=1004 | MissionType | ObjectType | ProgramStateMachineType |
+| ns=1;i=1005 | IntentCapabilitiesType | ObjectType | BaseObjectType |
+| ns=1;i=1006 | CoordinateFrameType | ObjectType | BaseObjectType |
+| ns=1;i=1007 | ToolType | ObjectType | BaseObjectType |
+| ns=1;i=1008 | LocationType | ObjectType | BaseObjectType |
+| ns=1;i=1009 | AxisType | ObjectType | BaseObjectType |
+| ns=1;i=1010 | OutputSignalType | ObjectType | BaseObjectType |
+| ns=1;i=1011 | ProgramType | ObjectType | BaseObjectType |
+| ns=1;i=1012 | SafetyStateType | ObjectType | BaseObjectType |
+| ns=1;i=1013 | RealTimeChannelType | ObjectType | BaseObjectType |
+| ns=1;i=1014 | RobotDescriptionType | ObjectType | BaseObjectType |
+| ns=1;i=3001 | ExecutionStateEnum | DataType | Enumeration |
+| ns=1;i=3002 | BufferModeEnum | DataType | Enumeration |
+| ns=1;i=3003 | BlockingModeEnum | DataType | Enumeration |
+| ns=1;i=3004 | TerminationModeEnum | DataType | Enumeration |
+| ns=1;i=3005 | ReleaseModeEnum | DataType | Enumeration |
+| ns=1;i=3006 | ApproachModeEnum | DataType | Enumeration |
+| ns=1;i=3007 | FrameRoleEnum | DataType | Enumeration |
+| ns=1;i=3008 | OperationalModeEnum | DataType | Enumeration |
+| ns=1;i=3009 | IntentFailureEnum | DataType | Enumeration |
+| ns=1;i=3010 | StopModeEnum | DataType | Enumeration |
+| ns=1;i=3011 | AxisKindEnum | DataType | Enumeration |
+| ns=1;i=3012 | MissionUpdateResultEnum | DataType | Enumeration |
+| ns=1;i=3050 | Pose3DDataType | DataType | Structure |
+| ns=1;i=3051 | MotionConstraintsDataType | DataType | Structure |
+| ns=1;i=3052 | BlendDataType | DataType | Structure |
+| ns=1;i=3053 | IntentDataType | DataType | Structure |
+| ns=1;i=3054 | MotionIntentDataType | DataType | IntentDataType |
+| ns=1;i=3055 | JointMoveIntentDataType | DataType | MotionIntentDataType |
+| ns=1;i=3056 | LinearMoveIntentDataType | DataType | MotionIntentDataType |
+| ns=1;i=3057 | CircularMoveIntentDataType | DataType | MotionIntentDataType |
+| ns=1;i=3058 | GraspIntentDataType | DataType | IntentDataType |
+| ns=1;i=3059 | ReleaseIntentDataType | DataType | IntentDataType |
+| ns=1;i=3060 | PickIntentDataType | DataType | IntentDataType |
+| ns=1;i=3061 | PlaceIntentDataType | DataType | IntentDataType |
+| ns=1;i=3062 | ToolChangeIntentDataType | DataType | IntentDataType |
+| ns=1;i=3063 | SetOutputIntentDataType | DataType | IntentDataType |
+| ns=1;i=3064 | CallProgramIntentDataType | DataType | IntentDataType |
+| ns=1;i=3065 | WaitIntentDataType | DataType | IntentDataType |
+| ns=1;i=3066 | IntentResultDataType | DataType | Structure |
+| ns=1;i=3067 | MissionStepDataType | DataType | Structure |
+| ns=1;i=3068 | MissionDataType | DataType | Structure |
+| ns=1;i=3069 | IntentCapabilityDataType | DataType | Structure |
+| ns=1;i=3013 | SafeMotionFunctionEnum | DataType | Enumeration |
+| ns=1;i=3014 | RealTimeTransportEnum | DataType | Enumeration |
+| ns=1;i=3015 | ChannelInitiatorEnum | DataType | Enumeration |
+| ns=1;i=3016 | ErrorPolicyEnum | DataType | Enumeration |
+| ns=1;i=3017 | DivergenceKindEnum | DataType | Enumeration |
+| ns=1;i=3018 | WeaveShapeEnum | DataType | Enumeration |
+| ns=1;i=3070 | TrajectoryPointDataType | DataType | Structure |
+| ns=1;i=3071 | MotionToleranceDataType | DataType | Structure |
+| ns=1;i=3072 | TrajectoryIntentDataType | DataType | MotionIntentDataType |
+| ns=1;i=3073 | PathWaypointDataType | DataType | Structure |
+| ns=1;i=3074 | CartesianPathIntentDataType | DataType | MotionIntentDataType |
+| ns=1;i=3075 | ForceIntentDataType | DataType | MotionIntentDataType |
+| ns=1;i=3076 | ProcessIntentDataType | DataType | MotionIntentDataType |
+| ns=1;i=3077 | ArcWeldIntentDataType | DataType | ProcessIntentDataType |
+| ns=1;i=3078 | SpotWeldIntentDataType | DataType | ProcessIntentDataType |
+| ns=1;i=3079 | DispenseIntentDataType | DataType | ProcessIntentDataType |
+| ns=1;i=3080 | FastenIntentDataType | DataType | ProcessIntentDataType |
+| ns=1;i=3081 | PalletiseIntentDataType | DataType | ProcessIntentDataType |
+| ns=1;i=3082 | SurfaceFinishIntentDataType | DataType | ProcessIntentDataType |
+| ns=1;i=3083 | MissionTransitionDataType | DataType | Structure |
+| ns=1;i=3084 | KinematicJointDataType | DataType | Structure |
+
+## A.2 ReferenceTypes
+
+| NodeId | BrowseName | InverseName | Subtype of | Description |
+|---|---|---|---|---|
+| ns=1;i=4001 | HasIntentController | IntentControllerOf | NonHierarchicalReferences | Binds an intent surface to the thing it commands. This is how the model attaches to a robot described by another specification - an OPC 40010-1 MotionDeviceSystem, say - without depending on it. Annex B defines that binding. |
+| ns=1;i=4002 | HasFrameParent | FrameParentOf | NonHierarchicalReferences | From a CoordinateFrame to the frame its Transform is expressed in. Frames form a tree, so a pose given in one frame can be re-expressed in another by composing the transforms along the path between them. |
+
+## A.3 ObjectTypes
+
+### RobotIntentRootType — `ns=1;i=1001`
+
+*Subtype of:* `BaseObjectType`
+
+Server-level entry point. A client that has just connected browses here to find every robot it can command, without knowing the Server's layout.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| Controllers | Object | | | Mandatory | The intent surfaces this Server offers, one per commandable robot. |
+| SpecificationVersion | Variable | String | Scalar | Mandatory | Release of this specification the Server implements, for example '0.1.0'. |
+
+### IntentControllerType — `ns=1;i=1002`
+
+*Subtype of:* `BaseObjectType`
+
+The intent surface for one robot: what it can be asked to do, the frames and objects those requests refer to, and the intents and missions currently outstanding. Everything a client needs in order to command the robot hangs from here.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| OperationalMode | Variable | OperationalModeEnum | Scalar | Mandatory | Operational mode reported by the robot. Read-only: mode selection is a safety function and this specification defines no way to command it. Clause 10 restricts submission to Automatic and AutomaticExternal. |
+| Ready | Variable | Boolean | Scalar | Mandatory | True when the robot will accept intents now. A client checks this before submitting rather than inferring readiness from OperationalMode alone. |
+| ControlOwner | Variable | NodeId | Scalar | Mandatory | SessionId of the client that currently holds command authority, or null when no client does. Only that client may submit; see clause 8. |
+| MaxQueueDepth | Variable | UInt32 | Scalar | Mandatory | How many intents may be queued behind the executing one. Zero means the Server accepts only Aborting submissions. |
+| ActiveIntent | Variable | NodeId | Scalar | Mandatory | The IntentOperation executing now, or null. |
+| ActiveMission | Variable | NodeId | Scalar | Optional | The Mission executing now, or null. |
+| Capabilities | Object | | | Mandatory | What this robot will accept. |
+| Frames | Object | | | Mandatory | The coordinate frames poses may be expressed in. |
+| Tools | Object | | | Mandatory | The end effectors this robot can use. |
+| Locations | Object | | | Mandatory | The named places intents refer to. |
+| Axes | Object | | | Mandatory | The axes, in the order JointMoveIntentDataType.JointTargets uses. |
+| Outputs | Object | | | Optional | The signals SetOutput can write. |
+| Programs | Object | | | Optional | The controller programs CallProgram can run. |
+| Intents | Object | | | Mandatory | Outstanding and recently completed intents, one Object each. |
+| Missions | Object | | | Optional | Outstanding and recently completed missions, one Object each. |
+| SafetyState | Object | | | Mandatory | What the safety system is doing. A client reads this before it plans, and subscribes to it so that it learns of a stop rather than inferring one from a refusal. |
+| Description | Object | | | Optional | The robot's kinematics and limits. |
+| RealTimeChannels | Object | | | Optional | The high-rate channels this Server can broker. |
+
+**Method `RequestControl`** (Mandatory) — Take command authority. A Server grants it only when no other Session holds it, or when the holder's Session has closed. Holding authority is a precondition for submitting, and exists so that two clients cannot interleave motion; it is NOT the single point of control that ISO 10218-2 requires, which is enforced by safety-rated means outside this interface.
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Granted | Boolean | Scalar | True when the caller now holds authority. |
+| CurrentOwner | NodeId | Scalar | SessionId of the holder after the call. |
+
+**Method `ReleaseControl`** (Mandatory) — Give up command authority. Outstanding intents are unaffected; use CancelAll to stop them.
+
+Takes no arguments and returns none.
+
+**Method `SubmitIntent`** (Mandatory) — Submit one intent. Returns as soon as the intent is admitted - NOT when the robot has finished, which may be minutes later. The returned Operation is a node the client subscribes to for progress and reads for the result. This is the whole reason the model is built on the Part 10 program lifecycle: an OPC UA Call cannot stay open for the duration of a motion.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Intent | IntentDataType | Scalar | The intent to execute. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when the intent was admitted. False is a refusal, which clause 10.5 makes an ordinary outcome rather than an error. |
+| IntentId | String | Scalar | Identifier of the intent, assigned by the Server when the request left it empty. Empty on a refusal. |
+| Operation | NodeId | Scalar | The IntentOperation that tracks it. Null on a refusal, because a refusal creates no operation instance. |
+| Failure | IntentFailureEnum | Scalar | Why the intent was refused, or None when it was admitted. |
+| Message | LocalizedText | Scalar | Human-readable detail on a refusal. For a human; never parsed. |
+
+**Method `CancelIntent`** (Mandatory) — Ask the Server to end an intent early. The Server MAY refuse, and says so in Accepted, because some motions cannot be abandoned safely part-way. This is not the OPC UA Cancel Service, which discards a pending response and leaves the robot moving; see clause 6.5.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| IntentId | String | Scalar | The intent to cancel. |
+| StopMode | StopModeEnum | Scalar | How urgently to stop. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when the Server will act on it. |
+
+**Method `CancelAll`** (Mandatory) — Ask the Server to end every outstanding intent and mission.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| StopMode | StopModeEnum | Scalar | How urgently to stop. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Cancelled | UInt32 | Scalar | How many were acted on. |
+
+**Method `Pause`** (Optional) — Suspend execution, retaining position so it can be resumed.
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when execution is suspending. |
+
+**Method `Resume`** (Optional) — Continue execution suspended by Pause.
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when execution is resuming. |
+
+**Method `Retry`** (Optional) — Re-attempt an intent that terminated Retriable.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| IntentId | String | Scalar | The intent to re-attempt. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when a new attempt was admitted. |
+| Operation | NodeId | Scalar | The IntentOperation that tracks the new attempt. Null on a refusal. |
+| Failure | IntentFailureEnum | Scalar | Why the re-attempt was refused, or None when it was admitted. |
+| Message | LocalizedText | Scalar | Human-readable detail on a refusal. |
+
+**Method `SubmitMission`** (Optional) — Submit an ordered sequence of intents as one unit. Steps marked Released form the base and are committed; the rest form the horizon and may still be revised by UpdateMission.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Mission | MissionDataType | Scalar | The mission to execute. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when the mission was admitted. |
+| MissionId | String | Scalar | Identifier of the mission, assigned by the Server when the request left it empty. Empty on a refusal. |
+| Operation | NodeId | Scalar | The Mission that tracks it. Null on a refusal. |
+| Failure | IntentFailureEnum | Scalar | Why the mission was refused, or None when it was admitted. |
+| Message | LocalizedText | Scalar | Human-readable detail on a refusal. |
+
+**Method `UpdateMission`** (Optional) — Replace the horizon of a mission already submitted. The base is untouchable: it has been committed and may already have executed, so an update that would alter a released step is refused rather than partly applied.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| MissionId | String | Scalar | The mission to update. |
+| MissionUpdateId | UInt32 | Scalar | Revision of the update. Must be greater than the mission's current value. |
+| Steps | MissionStepDataType | Array | The steps that replace the horizon. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Result | MissionUpdateResultEnum | Scalar | Outcome of the update. |
+| Message | LocalizedText | Scalar | Human-readable detail on a refusal. |
+
+**Method `CancelMission`** (Optional) — Ask the Server to end a mission and every intent belonging to it.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| MissionId | String | Scalar | The mission to cancel. |
+| StopMode | StopModeEnum | Scalar | How urgently to stop. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Accepted | Boolean | Scalar | True when the Server will act on it. |
+
+**Method `OpenRealTimeChannel`** (Optional) — Take a lease on a brokered real-time channel. The Server prepares the transport and returns what the client needs in order to connect; it does not carry the samples. A lease that is not renewed lapses, which is what stops a dead client from holding the channel.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| ChannelId | String | Scalar | The channel to open. |
+| RequestedLease | Duration | Scalar | How long the lease is wanted for, in milliseconds. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Granted | Boolean | Scalar | True when the lease was taken. |
+| EndpointUrl | String | Scalar | Where to connect. |
+| PayloadDescriptor | String | Scalar | The transport's own configuration. |
+| LeaseExpiry | UtcTime | Scalar | When the lease lapses. |
+| Message | LocalizedText | Scalar | Human-readable detail on a refusal. For a human; never parsed. |
+
+**Method `CloseRealTimeChannel`** (Optional) — Give up a lease on a brokered channel.
+
+| In | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| ChannelId | String | Scalar | The channel to release. |
+
+| Out | DataType | ValueRank | Meaning |
+|---|---|---|---|
+| Released | Boolean | Scalar | True when the lease was held and is now released. |
+
+### IntentOperationType — `ns=1;i=1003`
+
+*Subtype of:* `ProgramStateMachineType`
+
+One submitted intent, tracked to completion. It is a Part 10 program instance, so its lifecycle is the one OPC UA already defines for work that outlives a service call: transitions raise ProgramTransitionEvents, the terminal result survives in FinalResultData, and ProgramDiagnostic2DataType records which Session commanded it without this specification having to model provenance itself.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| IntentId | Variable | String | Scalar | Mandatory | Identifier of the intent this instance executes. |
+| Intent | Variable | IntentDataType | Scalar | Mandatory | The intent as admitted, after the Server applied its defaults. A client reads this to learn what it actually asked for. |
+| ExecutionState | Variable | ExecutionStateEnum | Scalar | Mandatory | Fine-grained state. It refines CurrentState rather than restating it; clause 6.3 fixes which pairs are legal. |
+| Progress | Variable | Double | Scalar | Optional | Fraction of the intent completed, 0 to 1, where the Server can estimate it. Negative means it cannot. |
+| CurrentPose | Variable | Pose3DDataType | Scalar | Optional | Where the driven tool centre point is now. Subscribe for tracking; this is a status report at the sampling rate the client asks for, not a control signal, and clause 4.3 explains why it must not be used as one. |
+| Result | Variable | IntentResultDataType | Scalar | Mandatory | Outcome, meaningful once ExecutionState is terminal. The same value is placed under FinalResultData so a Part 10 client finds it where Part 10 says it will be. |
+| MissionId | Variable | String | Scalar | Optional | The mission this intent belongs to, or empty when it was submitted alone. |
+| QueuePosition | Variable | UInt32 | Scalar | Optional | Place in the queue while ExecutionState is Queued, 1 being next. Zero once it is no longer queued. |
+| FinalResultData | Object | | | Mandatory | Part 10 result container. Carries the same IntentResultDataType value as Result, so a Part 10 client finds the outcome where Part 10 says it will be. Mandatory here because clause 6.7 requires it and an Optional member cannot carry a SHALL. |
+| ProgramDiagnostic | Variable | i=24033 | Scalar | Mandatory | Part 10 invocation diagnostics: which Session invoked the program, when, with what arguments and to what outcome. Mandatory here because the auditable-commanding property of clause 1.2 is exactly this member, and a capability the specification advertises cannot rest on one a Server may omit. Declared exactly as OPC 10000-10 declares it - a Variable of ProgramDiagnostic2Type reached by HasComponent, with the inherited namespace-0 BrowseName - because a promotion that altered the inherited member's TypeDefinition, reference type or BrowseName namespace would declare a second member rather than promote the inherited one. |
+
+### MissionType — `ns=1;i=1004`
+
+*Subtype of:* `ProgramStateMachineType`
+
+One submitted mission, tracked to completion. It is a Part 10 program instance for the same reasons an IntentOperation is, and it owns the IntentOperations of its steps.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| MissionId | Variable | String | Scalar | Mandatory | Identifier of the mission this instance executes. |
+| MissionUpdateId | Variable | UInt32 | Scalar | Mandatory | Revision currently in force. |
+| Mission | Variable | MissionDataType | Scalar | Mandatory | The mission as it now stands, base and horizon together. |
+| ExecutionState | Variable | ExecutionStateEnum | Scalar | Mandatory | Fine-grained state, refining CurrentState as clause 6.3 fixes. |
+| CurrentStepId | Variable | String | Scalar | Mandatory | The step executing now, or empty. |
+| ReleasedStepCount | Variable | UInt32 | Scalar | Mandatory | How many steps are in the base. The first this many steps of Mission.Steps are committed; the rest are the horizon. |
+
+### IntentCapabilitiesType — `ns=1;i=1005`
+
+*Subtype of:* `BaseObjectType`
+
+What one robot will accept. A client reads this once, before it submits anything, and knows what the robot can do and under what constraints.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| SupportedIntents | Variable | IntentCapabilityDataType | Array | Mandatory | One entry per intent type this Server accepts. |
+| MissionsSupported | Variable | Boolean | Scalar | Mandatory | True when SubmitMission is implemented. |
+| MissionHorizonSupported | Variable | Boolean | Scalar | Mandatory | True when UpdateMission can revise the horizon of a running mission. |
+| BlendingSupported | Variable | Boolean | Scalar | Mandatory | True when the blending buffer modes actually blend. A Server that treats them as Buffered reports False, so a client is not misled about the path. |
+| MaxBlendRadius | Variable | Double | Scalar | Optional | Largest blend radius the robot will honour, in metres. Zero or less means it is not bounded here. |
+| AxisCount | Variable | UInt32 | Scalar | Mandatory | How many axes JointMoveIntentDataType.JointTargets must carry. |
+| TrajectorySupported | Variable | Boolean | Scalar | Mandatory | True when trajectory and Cartesian path intents are accepted. |
+| ForceControlSupported | Variable | Boolean | Scalar | Mandatory | True when force intents are accepted and the robot can actually regulate force. A Server that would ignore the force reports false rather than accepting an intent it cannot honour. |
+| RealTimeChannelsSupported | Variable | Boolean | Scalar | Mandatory | True when the Server brokers real-time channels. |
+| MissionBranchingSupported | Variable | Boolean | Scalar | Mandatory | True when mission transitions are evaluated. A Server that reports false executes the steps in order and ignores any transitions supplied. |
+| MaxTrajectoryPoints | Variable | UInt32 | Scalar | Optional | Largest number of points accepted in one trajectory. Zero means the Server states no limit. |
+| SupportedFacets | Variable | String | Array | Mandatory | The facets of Table 12.2 this controller claims, by the names given there. A client reads this instead of re-deriving conformance from the address space. Clause 12.2 binds the claim: a facet whose structural requirements are unmet shall not appear, and the behavioural requirements are subject to the honesty rules of clause 9. |
+
+### CoordinateFrameType — `ns=1;i=1006`
+
+*Subtype of:* `BaseObjectType`
+
+A named right-handed Cartesian frame. Frames form a tree through HasFrameParent, so a client can compose a chain from a tool frame to a world frame. Roles follow ISO 9787, which standardises which frames exist; the transform between them is carried explicitly because no standard says how to calibrate it.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| FrameId | Variable | String | Scalar | Mandatory | Identifier referenced by Pose3DDataType.FrameId. Unique among the frames of one controller. |
+| Role | Variable | FrameRoleEnum | Scalar | Mandatory | Role of this frame. |
+| Transform | Variable | Pose3DDataType | Scalar | Optional | Pose of this frame within its parent. Ignored for a root frame. |
+
+### ToolType — `ns=1;i=1007`
+
+*Subtype of:* `BaseObjectType`
+
+An end effector. Its tool centre point is a CoordinateFrame of role Tool, which is what a motion intent drives to a target - OPC 40010-1 models robot topology in detail but has no tool centre point at all, so this specification supplies one.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| ToolId | Variable | String | Scalar | Mandatory | Identifier unique within the controller. |
+| Name | Variable | LocalizedText | Scalar | Mandatory | Human-readable name. |
+| Fitted | Variable | Boolean | Scalar | Mandatory | True when this tool is on the robot now. |
+| TcpFrame | Variable | NodeId | Scalar | Mandatory | The CoordinateFrame, of role Tool, that is this tool's centre point. |
+| Mass | Variable | Double | Scalar | Optional | Mass in kilograms. Zero or less when not stated. |
+| MaxGraspForce | Variable | Double | Scalar | Optional | Largest grasp force in newtons, or zero when the tool does not grasp. |
+| MaxOpening | Variable | Double | Scalar | Optional | Largest opening in metres, or zero when the tool does not grasp. |
+
+### LocationType — `ns=1;i=1008`
+
+*Subtype of:* `BaseObjectType`
+
+A named place an intent can refer to. Pick and Place reference these nodes rather than naming a station in a string, so a location has one definition that a client can read, subscribe to and reason about.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| LocationId | Variable | String | Scalar | Mandatory | Identifier unique within the controller. |
+| Name | Variable | LocalizedText | Scalar | Mandatory | Human-readable name. |
+| Pose | Variable | Pose3DDataType | Scalar | Mandatory | Where the location is. |
+| Occupied | Variable | Boolean | Scalar | Optional | True when the Server believes something is there. |
+| ObjectClass | Variable | String | Scalar | Optional | What the location holds, when it holds one kind of thing. Empty otherwise. |
+| Capacity | Variable | UInt32 | Scalar | Optional | How many objects it can hold. Zero means unstated. |
+
+### AxisType — `ns=1;i=1009`
+
+*Subtype of:* `BaseObjectType`
+
+One axis of the robot. The order of these nodes under the controller's Axes folder fixes the order of JointMoveIntentDataType.JointTargets, and Kind fixes each entry's unit.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| AxisId | Variable | String | Scalar | Mandatory | Identifier unique within the controller. |
+| Index | Variable | UInt32 | Scalar | Mandatory | Position in JointTargets, counting from zero. |
+| Kind | Variable | AxisKindEnum | Scalar | Mandatory | Whether it rotates or translates, which fixes the unit of its joint target. |
+| MinPosition | Variable | Double | Scalar | Mandatory | Lower limit, in radians or metres by Kind. |
+| MaxPosition | Variable | Double | Scalar | Mandatory | Upper limit, in radians or metres by Kind. |
+| MaxSpeed | Variable | Double | Scalar | Optional | Largest speed, in radians or metres per second by Kind. |
+| Position | Variable | Double | Scalar | Optional | Where the axis is now, in radians or metres by Kind. |
+
+### OutputSignalType — `ns=1;i=1010`
+
+*Subtype of:* `BaseObjectType`
+
+A signal SetOutput can write. Modelling it as a node means the range, the unit and the meaning are described once, instead of every client having to know what a line name implies.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| SignalId | Variable | String | Scalar | Mandatory | Identifier unique within the controller. |
+| Name | Variable | LocalizedText | Scalar | Mandatory | Human-readable name. |
+| Value | Variable | BaseDataType | Scalar | Mandatory | Current value. |
+| Writable | Variable | Boolean | Scalar | Mandatory | True when SetOutput may write it. |
+| EngineeringUnits | Variable | EUInformation | Scalar | Optional | Unit of an analogue signal. |
+
+### ProgramType — `ns=1;i=1011`
+
+*Subtype of:* `BaseObjectType`
+
+A program held on the controller that CallProgram can run. This is the bridge to capability this specification does not model, and to the programs an OPC 40010-1 task control already exposes.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| ProgramId | Variable | String | Scalar | Mandatory | Identifier unique within the controller. |
+| Name | Variable | LocalizedText | Scalar | Mandatory | Human-readable name. |
+| Description | Variable | LocalizedText | Scalar | Optional | What the program does. |
+| Parameters | Variable | KeyValuePair | Array | Optional | Named parameters it accepts, with their default values. |
+
+### SafetyStateType — `ns=1;i=1012`
+
+*Subtype of:* `BaseObjectType`
+
+What the robot's safety system is doing, reported so a client can act sensibly around it. Every member is READ-ONLY and every one is a report: the safety system enforces these independently, remains effective when this interface is unreachable, and is not commanded from here. Clause 10 states the boundary and this type does not cross it.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| ActiveFunction | Variable | SafeMotionFunctionEnum | Scalar | Mandatory | The safe motion function currently enforced, per IEC 61800-5-2. |
+| EmergencyStopActive | Variable | Boolean | Scalar | Mandatory | True while an emergency stop is asserted. |
+| ProtectiveStopActive | Variable | Boolean | Scalar | Mandatory | True while a protective stop is asserted. |
+| SafeSpeedLimitActive | Variable | Boolean | Scalar | Mandatory | True while a safely limited speed is being enforced. |
+| SafeSpeedLimit | Variable | Double | Scalar | Mandatory | The tool centre point speed limit being enforced, in metres per second. Meaningful only while SafeSpeedLimitActive. Clause 10.3 requires a Server to refuse an intent that asks to exceed it. |
+| SafetyControllerOk | Variable | Boolean | Scalar | Mandatory | False when the safety system reports its own fault. A Server accepts no intent while it is false. |
+| LastStopReason | Variable | LocalizedText | Scalar | Optional | Why the last stop occurred, for a human. Never parsed. |
+
+### RealTimeChannelType — `ns=1;i=1013`
+
+*Subtype of:* `BaseObjectType`
+
+A high-rate channel this Server can offer, described so a client can open it. The samples never traverse OPC UA: this brokers the endpoint and nothing more, in the same way the Vision model brokers a media endpoint rather than carrying pixels. Clause 4.3 explains why the alternative is not available.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| ChannelId | Variable | String | Scalar | Mandatory | Identifier unique within the controller. |
+| Transport | Variable | RealTimeTransportEnum | Scalar | Mandatory | The transport this channel speaks. |
+| EndpointUrl | Variable | String | Scalar | Mandatory | Where the channel is reached. Its scheme and form are the transport's, not this specification's. |
+| Initiator | Variable | ChannelInitiatorEnum | Scalar | Mandatory | Which end opens the connection. |
+| NominalRate | Variable | Double | Scalar | Mandatory | The rate the channel runs at, in hertz. |
+| PayloadDescriptor | Variable | String | Scalar | Optional | The recipe, signal list or configuration the transport requires, in the transport's own form. |
+| RequiredMode | Variable | OperationalModeEnum | Scalar | Mandatory | The operational mode the robot must be in before the channel will carry motion. |
+| Available | Variable | Boolean | Scalar | Mandatory | True when the channel can be opened now. |
+| LeaseHolder | Variable | NodeId | Scalar | Mandatory | SessionId of the client currently holding the channel, or null. |
+| LeaseExpiry | Variable | UtcTime | Scalar | Optional | When the current lease lapses. A lease that is not renewed frees the channel, so a client that dies does not hold it for good. |
+
+### RobotDescriptionType — `ns=1;i=1014`
+
+*Subtype of:* `BaseObjectType`
+
+Enough of the robot's construction for a client to plan against it without a second specification: the kinematic chain, the space it can reach, and what it can carry. OPC 40010-1 describes topology and axes and defines no kinematic chain and no tool centre point, so this adds rather than restates - Annex B says which side decides where both are present.
+
+| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
+|---|---|---|---|---|---|
+| Manufacturer | Variable | LocalizedText | Scalar | Optional | Who made the robot. |
+| Model | Variable | String | Scalar | Optional | The robot's model designation. |
+| KinematicChain | Variable | KinematicJointDataType | Array | Mandatory | The joints from the base outwards, in order. |
+| MountingPose | Variable | Pose3DDataType | Scalar | Optional | Pose of the robot base within the world frame. |
+| ReachRadius | Variable | Double | Scalar | Mandatory | Radius of the reachable workspace from the base, in metres. |
+| PayloadLimit | Variable | Double | Scalar | Mandatory | Largest payload at the mechanical interface, in kilograms. |
+| MaxCartesianSpeed | Variable | Double | Scalar | Mandatory | Largest tool centre point speed the robot will produce, in metres per second. |
+| MaxCartesianAcceleration | Variable | Double | Scalar | Optional | Largest tool centre point acceleration, in metres per second squared. |
+
+## A.4 DataTypes
+
+### ExecutionStateEnum — `ns=1;i=3001`
+
+*Subtype of:* `Enumeration`
+
+Fine-grained execution state of an intent or a mission. This REFINES the Part 10 program state machine rather than restating it: Queued, Cancelling and the three terminal outcomes cannot be told apart from CurrentState alone. Clause 6.3 fixes which ExecutionState may accompany which Part 10 state, and a Server shall satisfy that table.
+
+| Name | Value | Description |
+|---|---|---|
+| Accepted | 0 | Admitted and validated, not yet queued or executing. |
+| Queued | 1 | Waiting behind another intent because BufferMode is Buffered or a blending mode. Corresponds to PLCopen Busy without Active. |
+| Executing | 2 | Commanding the robot now. |
+| Suspended | 3 | Paused by request; position is retained and execution can resume. |
+| Cancelling | 4 | A cancel was accepted and the Server is bringing the motion to a controlled end. Not yet terminal. |
+| Succeeded | 5 | Terminal. Completed as requested. |
+| Failed | 6 | Terminal. Did not complete; Failure carries the reason. |
+| Cancelled | 7 | Terminal. Ended early because a cancel was accepted. |
+| Retriable | 8 | Terminal for now, but the Server can re-attempt it on Retry. A Server that does not offer Retry never enters this state and reports Failed instead. |
+
+### BufferModeEnum — `ns=1;i=3002`
+
+*Subtype of:* `Enumeration`
+
+How a newly submitted intent relates to the one already executing. The values and their meanings are those of PLCopen Motion Control MC_BufferMode, adopted unchanged because every motion runtime already implements them. In all blending modes the robot does not decelerate to a stop at the boundary, and the predecessor reaches Succeeded when blending begins rather than when its target is exactly attained.
+
+| Name | Value | Description |
+|---|---|---|
+| Aborting | 0 | Abort what is executing and start immediately. The aborted intent terminates as Cancelled. This is the default. |
+| Buffered | 1 | Queue; start when the predecessor succeeds. |
+| BlendingLow | 2 | Blend at the lower of the two boundary speeds. |
+| BlendingPrevious | 3 | Blend at the predecessor's boundary speed. |
+| BlendingNext | 4 | Blend at the successor's boundary speed. |
+| BlendingHigh | 5 | Blend at the higher of the two boundary speeds. |
+
+### BlockingModeEnum — `ns=1;i=3003`
+
+*Subtype of:* `Enumeration`
+
+Whether an intent tolerates motion and other intents running alongside it. The four values are the two-by-two matrix of VDA 5050 blockingType, adopted because it is the only widely deployed concurrency annotation for robot actions.
+
+| Name | Value | Description |
+|---|---|---|
+| None | 0 | Runs in the background; motion may continue and other intents may run concurrently. |
+| Soft | 1 | Motion stops for the duration; other intents may still run. |
+| Single | 2 | Motion may continue; no other intent may run concurrently. |
+| Hard | 3 | Motion stops and no other intent may run concurrently. The intent has exclusive use of the robot. |
+
+### TerminationModeEnum — `ns=1;i=3004`
+
+*Subtype of:* `Enumeration`
+
+Whether a motion ends exactly on its target or is blended into the next one. This is the only distinction every vendor expresses identically; the blend magnitude in BlendDataType.Radius is a request, not a guarantee.
+
+| Name | Value | Description |
+|---|---|---|
+| Exact | 0 | Come to rest on the target before the next motion begins. ABB fine, FANUC FINE, Yaskawa PL=0, KUKA no approximation. |
+| Blend | 1 | Round the corner into the next motion without stopping. |
+
+### ReleaseModeEnum — `ns=1;i=3005`
+
+*Subtype of:* `Enumeration`
+
+How a held object is given up.
+
+| Name | Value | Description |
+|---|---|---|
+| Drop | 0 | Open the end effector where it is, without placing. |
+| Place | 1 | Set the object down under control at the target. |
+| Handover | 2 | Retain the object until the Server judges a receiving party has taken it. |
+
+### ApproachModeEnum — `ns=1;i=3006`
+
+*Subtype of:* `Enumeration`
+
+Direction from which an end effector approaches an object or a placement.
+
+| Name | Value | Description |
+|---|---|---|
+| Default | 0 | The Server chooses. |
+| ToolZ | 1 | Along the tool's own Z axis. |
+| Top | 2 | From above, in the frame the target is expressed in. |
+| Side | 3 | Laterally, in the frame the target is expressed in. |
+
+### FrameRoleEnum — `ns=1;i=3007`
+
+*Subtype of:* `Enumeration`
+
+Role of a coordinate frame, following the coordinate systems ISO 9787 standardises. The roles say WHICH frames exist; no standard says how to calibrate between them, which is why CoordinateFrameType carries the transform explicitly.
+
+| Name | Value | Description |
+|---|---|---|
+| World | 0 | The cell-level reference frame. |
+| Base | 1 | The robot base frame. |
+| MechanicalInterface | 2 | The flange at the end of the last link, to which an end effector is fitted. |
+| Tool | 3 | A tool frame, whose origin is a tool centre point. |
+| Object | 4 | A workpiece or work-object frame. |
+| Other | 5 | A frame whose role is none of the above. |
+
+### OperationalModeEnum — `ns=1;i=3008`
+
+*Subtype of:* `Enumeration`
+
+Operational mode of the robot system, as defined by ISO 10218-1 and reported identically by OPC 40010-1. It is READ-ONLY here: mode selection is a safety function performed by safety-rated means, and this specification defines no way to command it. Clause 10 restricts intent submission to Automatic and AutomaticExternal.
+
+| Name | Value | Description |
+|---|---|---|
+| Other | 0 | Booting, uncalibrated, or a safety system fault. |
+| ManualReducedSpeed | 1 | Teaching mode with a speed ceiling and a held enabling device. |
+| ManualHighSpeed | 2 | Program verification with an enabling device. |
+| Automatic | 3 | Automatic operation with the safeguarded space secured. |
+| AutomaticExternal | 4 | Automatic operation commanded by an external system. This is the mode this specification is written for. |
+
+### IntentFailureEnum — `ns=1;i=3009`
+
+*Subtype of:* `Enumeration`
+
+Why an intent did not succeed. The set is deliberately small and diagnosable: a client decides whether to retry, re-plan or escalate from this value alone, and reads Message only to show a human.
+
+| Name | Value | Description |
+|---|---|---|
+| None | 0 | No failure. Reported on a successful outcome. |
+| Unreachable | 1 | The target lies outside the reachable workspace. |
+| Kinematics | 2 | No kinematic solution, or a singularity on the path. |
+| Collision | 3 | A collision was predicted or detected. |
+| JointLimit | 4 | A joint limit would be or was exceeded. |
+| SpeedLimit | 5 | The requested speed or acceleration is not permitted in the active mode. |
+| ToolMissing | 6 | The required tool is not fitted or not identified. |
+| ObjectNotFound | 7 | The object to act on was not present. |
+| GraspFailed | 8 | The object was not acquired, or was lost in transit. |
+| Timeout | 9 | The intent did not complete within its permitted time. |
+| NotPermittedInMode | 10 | Refused because the operational mode does not permit it. See clause 10. |
+| ControlNotOwned | 11 | Refused because the caller does not hold command authority. See clause 8. |
+| CapabilityNotSupported | 12 | The Server does not implement this intent type, or this combination of options. |
+| ParameterInvalid | 13 | A parameter was missing, malformed or out of range. |
+| QueueFull | 14 | The queue is at MaxQueueDepth. |
+| Superseded | 15 | An Aborting submission or a mission update replaced it before it could run. |
+| HardwareFault | 16 | A fault in the robot, the end effector or the controller. |
+| SafetyStop | 17 | A safety function acted. The safety system, not this interface, decided this. |
+| Other | 18 | A reason none of the above describes; see Message. |
+| SafetyLimitExceeded | 19 | Refused because the request would exceed a limit the safety system is enforcing. See clause 10.3. |
+
+### StopModeEnum — `ns=1;i=3010`
+
+*Subtype of:* `Enumeration`
+
+How urgently a cancellation should bring motion to an end. The values are those of PossibleStopModes in OPC 40010-1, so a Server implementing both reports one vocabulary. This is an APPLICATION-LEVEL request: it does not select, imply or guarantee any IEC 60204-1 stop category, which only the robot's safety system determines. See clause 10.
+
+| Name | Value | Description |
+|---|---|---|
+| OnPath | 1 | Decelerate along the programmed path. |
+| EndOfCycle | 2 | Stop when the current cycle completes. |
+| ProcessStop | 3 | Stop at a point the process defines as safe. |
+| QuickStop | 4 | Decelerate as quickly as the drives allow. |
+| EndOfInstruction | 5 | Stop when the current instruction completes. |
+
+### AxisKindEnum — `ns=1;i=3011`
+
+*Subtype of:* `Enumeration`
+
+Whether an axis rotates or translates. This fixes the unit of the corresponding entry of JointMoveIntentDataType.JointTargets.
+
+| Name | Value | Description |
+|---|---|---|
+| Revolute | 0 | Rotates. Its joint target is in radians. |
+| Prismatic | 1 | Translates. Its joint target is in metres. |
+
+### MissionUpdateResultEnum — `ns=1;i=3012`
+
+*Subtype of:* `Enumeration`
+
+Outcome of a mission update, reported so a client can tell a stale update from a rejected one without parsing a StatusCode.
+
+| Name | Value | Description |
+|---|---|---|
+| Accepted | 0 | The horizon was replaced as requested. |
+| Outdated | 1 | MissionUpdateId was not greater than the current one. |
+| BaseConflict | 2 | The update would have altered a released step. |
+| UnknownMission | 3 | No mission with that MissionId is held. |
+| Rejected | 4 | Refused for a reason the Server states in a message. |
+
+### Pose3DDataType — `ns=1;i=3050`
+
+*Subtype of:* `Structure`
+
+A rigid-body pose. Position is metres; Orientation is a UNIT QUATERNION ordered (x, y, z, w). Quaternions are used because OPC UA defines no quaternion type and the Euler triple in ThreeDOrientation is ambiguous without an external convention; Annex C gives the normative conversion to and from ThreeDFrame. All frames are right-handed. FrameId names the CoordinateFrame the pose is expressed in; an empty FrameId means the Server's default work frame.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| FrameId | String | Scalar | | FrameId of the CoordinateFrame this pose is expressed in; empty for the default work frame. |
+| Position | Double | Array | 3 | Translation (x, y, z) in metres. |
+| Orientation | Double | Array | 4 | Unit quaternion (x, y, z, w). |
+
+### MotionConstraintsDataType — `ns=1;i=3051`
+
+*Subtype of:* `Structure`
+
+Limits a motion is to respect. Every field is a REQUEST bounded by what the robot is configured to permit: a Server clamps rather than refuses, except where clause 10 requires refusal. A value of zero or less means the field is unspecified and the Server chooses.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| SpeedFraction | Double | Scalar | | Fraction of the configured maximum speed, in the range 0 to 1. This is the portable speed control: every vendor supports it. |
+| CartesianSpeed | Double | Scalar | | Tool centre point speed in metres per second. Ignored by joint moves. |
+| CartesianAcceleration | Double | Scalar | | Tool centre point acceleration in metres per second squared. |
+| Jerk | Double | Scalar | | Rate of change of acceleration in metres per second cubed. |
+
+### BlendDataType — `ns=1;i=3052`
+
+*Subtype of:* `Structure`
+
+How a motion ends. Radius is interpreted only when Termination is Blend, and is a request: controllers that expose a unitless blend scale rather than a distance map it as best they can, and a Server that cannot honour the exact radius still succeeds.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Termination | TerminationModeEnum | Scalar | | Exact stop, or blend into the next motion. |
+| Radius | Double | Scalar | | Blend radius in metres, measured from the target. Zero or less means the Server chooses. |
+
+### IntentDataType — `ns=1;i=3053`
+
+*Subtype of:* `Structure`
+
+Abstract base of every intent. An intent is a single task-level request; it is what a client submits and what a mission step holds, so the two are the same shape. Extension is by SUBTYPING this structure, which keeps new intents discoverable through IntentCapabilitiesType rather than by probing for BrowseNames.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| IntentId | String | Scalar | | Client-assigned identifier, unique among the intents the client has outstanding. Empty asks the Server to assign one, which it returns. |
+| Label | LocalizedText | Scalar | | Human-readable description. Never interpreted. |
+| BufferMode | BufferModeEnum | Scalar | | How this intent relates to one already executing. |
+| BlockingMode | BlockingModeEnum | Scalar | | Whether motion and other intents may proceed alongside it. |
+
+### MotionIntentDataType — `ns=1;i=3054`
+
+*Subtype of:* `IntentDataType`
+
+Abstract base of the intents that move the robot. ToolFrame names the frame whose origin is driven to the target - without it a pose target is meaningless, and OPC 40010-1 defines no tool centre point at all.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| ToolFrame | NodeId | Scalar | | The CoordinateFrame, of role Tool, whose origin is driven to the target. Null means the tool currently fitted. |
+| Constraints | MotionConstraintsDataType | Scalar | | Speed and acceleration limits for this motion. |
+| Blend | BlendDataType | Scalar | | How this motion ends. |
+
+### JointMoveIntentDataType — `ns=1;i=3055`
+
+*Subtype of:* `MotionIntentDataType`
+
+Move by interpolating in joint space. This is the fastest way between two configurations and the path the tool centre point takes is not controlled. It is the portable equivalent of PTP, MoveJ, J and MOVJ. Giving a pose rather than joint values asks the Server to solve the kinematics itself, which is the 'move to this pose, you choose how' case.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| HasJointTargets | Boolean | Scalar | | True when JointTargets is meaningful; False when TargetPose is. |
+| JointTargets | Double | Array | | One value per axis, in the order the axes are declared under the controller: radians for a Revolute axis, metres for a Prismatic one. |
+| TargetPose | Pose3DDataType | Scalar | | Pose to reach, used when HasJointTargets is False. |
+
+### LinearMoveIntentDataType — `ns=1;i=3056`
+
+*Subtype of:* `MotionIntentDataType`
+
+Move the tool centre point along a straight line to the target. The portable equivalent of LIN, MoveL, L and MOVL.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Target | Pose3DDataType | Scalar | | Pose to reach. |
+
+### CircularMoveIntentDataType — `ns=1;i=3057`
+
+*Subtype of:* `MotionIntentDataType`
+
+Move the tool centre point along the circular arc that passes through ViaPoint and ends at Target. The portable equivalent of CIRC, MoveC, C and MOVC. Only the position of ViaPoint defines the arc; its orientation is ignored.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| ViaPoint | Pose3DDataType | Scalar | | A pose on the arc between the start and the target. Only its position is used. |
+| Target | Pose3DDataType | Scalar | | Pose to reach. |
+
+### GraspIntentDataType — `ns=1;i=3058`
+
+*Subtype of:* `IntentDataType`
+
+Close the end effector on an object. Force and Width are requests; an end effector that cannot regulate force ignores Force and still succeeds.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Tool | NodeId | Scalar | | The Tool to actuate. Null means the tool currently fitted. |
+| Force | Double | Scalar | | Grasp force in newtons. Zero or less means the Server chooses. |
+| Width | Double | Scalar | | Opening at which to close, in metres. Zero or less means the Server chooses. |
+| Approach | ApproachModeEnum | Scalar | | Direction of approach. |
+
+### ReleaseIntentDataType — `ns=1;i=3059`
+
+*Subtype of:* `IntentDataType`
+
+Give up a held object.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Tool | NodeId | Scalar | | The Tool to actuate. Null means the tool currently fitted. |
+| Mode | ReleaseModeEnum | Scalar | | How the object is given up. |
+| HasTarget | Boolean | Scalar | | True when Target is meaningful. |
+| Target | Pose3DDataType | Scalar | | Where to place the object, when Mode is Place. |
+
+### PickIntentDataType — `ns=1;i=3060`
+
+*Subtype of:* `IntentDataType`
+
+Take an object from a location. Source is a REFERENCE TO A LOCATION NODE, not a name: the location's pose and its properties are then read from the address space, so the station identity has exactly one definition.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Source | NodeId | Scalar | | The Location to pick from. |
+| Tool | NodeId | Scalar | | The Tool to use. Null means the tool currently fitted. |
+| ObjectClass | String | Scalar | | What to pick, when the location can hold more than one kind. Empty means whatever is there. |
+| Force | Double | Scalar | | Grasp force in newtons. Zero or less means the Server chooses. |
+| Approach | ApproachModeEnum | Scalar | | Direction of approach. |
+| Attributes | KeyValuePair | Array | | Further named parameters the Server declares in its capabilities. |
+
+### PlaceIntentDataType — `ns=1;i=3061`
+
+*Subtype of:* `IntentDataType`
+
+Put a held object at a location. Destination is a reference to a Location node, for the same reason as PickIntentDataType.Source.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Destination | NodeId | Scalar | | The Location to place at. |
+| Tool | NodeId | Scalar | | The Tool to use. Null means the tool currently fitted. |
+| Approach | ApproachModeEnum | Scalar | | Direction of approach. |
+| Attributes | KeyValuePair | Array | | Further named parameters the Server declares in its capabilities. |
+
+### ToolChangeIntentDataType — `ns=1;i=3062`
+
+*Subtype of:* `IntentDataType`
+
+Exchange the fitted end effector.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Tool | NodeId | Scalar | | The Tool to fit. Null means release the fitted tool and fit nothing. |
+| DockStation | NodeId | Scalar | | The Location of the tool changer. Null lets the Server choose. |
+
+### SetOutputIntentDataType — `ns=1;i=3063`
+
+*Subtype of:* `IntentDataType`
+
+Set a discrete or analogue output. Output references an OutputSignal node, so the signal's meaning, range and unit are described once in the address space instead of being implied by a string.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Output | NodeId | Scalar | | The OutputSignal to write. |
+| Value | BaseDataType | Scalar | | Value to write, of the signal's own DataType. |
+
+### CallProgramIntentDataType — `ns=1;i=3064`
+
+*Subtype of:* `IntentDataType`
+
+Run a program that already exists on the controller. This is the escape hatch for capability this specification does not model, and the bridge to the OPC 40010-1 task control surface.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Program | NodeId | Scalar | | The Program to run. |
+| Arguments | KeyValuePair | Array | | Named arguments for the program. |
+
+### WaitIntentDataType — `ns=1;i=3065`
+
+*Subtype of:* `IntentDataType`
+
+Do nothing for a while, or until released. A mission needs this to express a rendezvous with something the robot does not control; without it a client has to hold the queue open from outside.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Duration | Duration | Scalar | | How long to wait, in milliseconds. Zero or less waits until Signal is released. |
+| Signal | NodeId | Scalar | | An OutputSignal under this controller, or another Variable of DataType Boolean, whose becoming true ends the wait. Null waits only for Duration. Clause 11.3 requires the Server to check that it resolves to one of those two, under the controller being commanded, and to refuse it otherwise. |
+
+### IntentResultDataType — `ns=1;i=3066`
+
+*Subtype of:* `Structure`
+
+The outcome of one intent, preserved after it terminates. AchievedPose records where the tool centre point actually ended, which is what lets a client tell a blended corner from an exact stop and audit a placement.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| IntentId | String | Scalar | | The intent this describes. |
+| State | ExecutionStateEnum | Scalar | | Terminal state reached. |
+| Failure | IntentFailureEnum | Scalar | | Reason, or None on success. |
+| Message | LocalizedText | Scalar | | Human-readable detail. Never parsed. |
+| HasAchievedPose | Boolean | Scalar | | True when AchievedPose is meaningful. |
+| AchievedPose | Pose3DDataType | Scalar | | Where the tool centre point came to rest, or was when blending began. |
+| StartTime | UtcTime | Scalar | | When execution began. |
+| EndTime | UtcTime | Scalar | | When the terminal state was reached. |
+| Outputs | KeyValuePair | Array | | Named results the intent produced, for example the identity of a picked object. |
+
+### MissionStepDataType — `ns=1;i=3067`
+
+*Subtype of:* `Structure`
+
+One step of a mission. Released is what splits a mission into its immutable base and its revisable horizon: a released step has been committed and may already be executing, so an update may not touch it. Status is a HINT - where Operation is not null, that IntentOperation's state machine decides.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| StepId | String | Scalar | | Identifier unique within the mission. |
+| SequenceId | UInt32 | Scalar | | Execution order within the mission, ascending. |
+| Released | Boolean | Scalar | | True for a base step, which is committed and immutable. False for a horizon step, which an update may replace or remove. |
+| Intent | IntentDataType | Scalar | | The intent this step executes. |
+| Status | ExecutionStateEnum | Scalar | | Reported status. Where Operation is not null its state machine is authoritative and this reflects it. |
+| Operation | NodeId | Scalar | | The IntentOperation executing this step, or null while the step has not begun. |
+| ErrorPolicy | ErrorPolicyEnum | Scalar | | What the mission does when this step does not succeed. |
+| FallbackStepId | String | Scalar | | The step to continue at, when ErrorPolicy is Fallback or Compensate. |
+
+### MissionDataType — `ns=1;i=3068`
+
+*Subtype of:* `Structure`
+
+An ordered sequence of intents submitted and tracked as a unit. MissionUpdateId increases with every update, so a Server can reject an update that crossed with another in flight instead of applying it out of order.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| MissionId | String | Scalar | | Client-assigned identifier. Empty asks the Server to assign one, which it returns. |
+| MissionUpdateId | UInt32 | Scalar | | Revision of this mission, increasing. The first submission is 0. |
+| Label | LocalizedText | Scalar | | Human-readable description. Never interpreted. |
+| Steps | MissionStepDataType | Array | | The steps, in ascending SequenceId order. |
+| Transitions | MissionTransitionDataType | Array | | The step graph. An empty array means the steps run in order, which is the flat sequence a mission without branching has always been. |
+
+### IntentCapabilityDataType — `ns=1;i=3069`
+
+*Subtype of:* `Structure`
+
+What the Server will accept for one intent type. This is the machine-readable declaration that makes an intent surface discoverable: a client reads it once and knows what it may submit, instead of submitting to find out. It is the analogue of the VDA 5050 factsheet.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| IntentType | NodeId | Scalar | | The DataType of the intent, a subtype of IntentDataType. |
+| Description | LocalizedText | Scalar | | What this Server does with it. |
+| CancelSupported | Boolean | Scalar | | True when Cancel is honoured for it. A Server may still refuse a particular cancel; see clause 6.5. |
+| PauseSupported | Boolean | Scalar | | True when Pause and Resume are honoured for a running intent. False when the Server can only stop its queue: suspending execution means the robot stops, and Suspended is never reported while it is still moving. |
+| RetrySupported | Boolean | Scalar | | True when it can terminate Retriable and be re-attempted. |
+| SupportedBufferModes | BufferModeEnum | Array | | Buffer modes accepted for it. Aborting is always accepted and always listed. |
+| SupportedBlockingModes | BlockingModeEnum | Array | | Blocking modes accepted for it. |
+| Attributes | KeyValuePair | Array | | Named parameters this Server recognises in the intent's Attributes field. |
+
+### SafeMotionFunctionEnum — `ns=1;i=3013`
+
+*Subtype of:* `Enumeration`
+
+The safe motion function a safety system is enforcing, as defined by IEC 61800-5-2. This is a REPORT. The safety system enforces these independently of this interface, and a client reading them has not thereby obtained any safety function - see clause 10.
+
+| Name | Value | Description |
+|---|---|---|
+| None | 0 | No safe motion function is active. |
+| Sto | 1 | Safe Torque Off: torque is removed. |
+| Ss1 | 2 | Safe Stop 1: a controlled ramp to standstill, then Safe Torque Off. |
+| Ss2 | 3 | Safe Stop 2: a controlled ramp to standstill, which is then held under power. |
+| Sos | 4 | Safe Operating Stop: standstill is monitored while the drive remains energised. |
+| Sls | 5 | Safely Limited Speed: speed is monitored against a limit. |
+| Slp | 6 | Safely Limited Position: position is monitored against a limit. |
+| Sdi | 7 | Safe Direction: motion is permitted in one direction only. |
+| Sbc | 8 | Safe Brake Control: a brake is commanded safely. |
+
+### RealTimeTransportEnum — `ns=1;i=3014`
+
+*Subtype of:* `Enumeration`
+
+The transport of a brokered real-time channel. This specification defines none of these: it describes them so a client can find and open one, and the samples never traverse this interface.
+
+| Name | Value | Description |
+|---|---|---|
+| Rtde | 0 | Universal Robots Real-Time Data Exchange. |
+| Egm | 1 | ABB Externally Guided Motion. |
+| Fri | 2 | KUKA Fast Research Interface. |
+| Rsi | 3 | KUKA Robot Sensor Interface. |
+| MotoRos2 | 4 | Yaskawa MotoROS2. |
+| OpcUaFx | 5 | OPC UA FX (OPC 10000-80 to -84). The open path, and the only one in this list that is an OPC Foundation specification. |
+| Other | 6 | A transport identified by the channel's own descriptor. |
+
+### ChannelInitiatorEnum — `ns=1;i=3015`
+
+*Subtype of:* `Enumeration`
+
+Which end opens the transport connection of a brokered channel. Getting this wrong is the usual reason a first connection attempt fails, so it is stated rather than left to the reader.
+
+| Name | Value | Description |
+|---|---|---|
+| Server | 0 | The Server connects to an endpoint the client is listening on. |
+| Client | 1 | The client connects to the endpoint the Server publishes. |
+
+### ErrorPolicyEnum — `ns=1;i=3016`
+
+*Subtype of:* `Enumeration`
+
+What a mission does when one of its steps does not succeed. Without this a mission can only abort, which forces every recovery out into the client.
+
+| Name | Value | Description |
+|---|---|---|
+| Abort | 0 | End the mission. This is the default and the behaviour of a mission that declares no policy. |
+| Retry | 1 | Re-attempt the step. A Server bounds the attempts and reports Failed when they are exhausted. |
+| Skip | 2 | Record the failure and continue with the next step. |
+| Fallback | 3 | Continue at FallbackStepId instead. |
+| Compensate | 4 | Run the fallback step to undo the work already done, then end the mission. |
+
+### DivergenceKindEnum — `ns=1;i=3017`
+
+*Subtype of:* `Enumeration`
+
+How the transitions leaving one step relate to each other, following the divergence of an IEC 61131-3 sequential function chart.
+
+| Name | Value | Description |
+|---|---|---|
+| Alternative | 0 | Exactly one transition is taken - the first whose condition holds. An OR divergence. |
+| Parallel | 1 | Every transition is taken and the branches run concurrently. An AND divergence. |
+
+### WeaveShapeEnum — `ns=1;i=3018`
+
+*Subtype of:* `Enumeration`
+
+The oscillation applied across an arc weld seam.
+
+| Name | Value | Description |
+|---|---|---|
+| None | 0 | No weave. |
+| Sine | 1 | Sinusoidal oscillation. |
+| Zigzag | 2 | Triangular oscillation. |
+| Trapezoid | 3 | Trapezoidal oscillation, with a dwell at each edge. |
+
+### TrajectoryPointDataType — `ns=1;i=3070`
+
+*Subtype of:* `Structure`
+
+One point of a time-parameterised path. Positions are per axis in the order the axes are declared, in radians or metres by AxisKind. TimeFromStart is measured from the start of the trajectory, which is what makes the path a trajectory rather than a list of waypoints. Velocities and Accelerations are optional and may be empty.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| TimeFromStart | Duration | Scalar | | Milliseconds from the start of the trajectory. |
+| Positions | Double | Array | | One value per axis. |
+| Velocities | Double | Array | | One value per axis, or empty. |
+| Accelerations | Double | Array | | One value per axis, or empty. |
+
+### MotionToleranceDataType — `ns=1;i=3071`
+
+*Subtype of:* `Structure`
+
+How far execution may deviate before it is a failure. A tolerance of zero or less means the Server applies its own.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Position | Double | Scalar | | Positional tolerance in metres. |
+| Orientation | Double | Scalar | | Orientation tolerance in radians. |
+| Time | Duration | Scalar | | Timing tolerance in milliseconds. |
+
+### TrajectoryIntentDataType — `ns=1;i=3072`
+
+*Subtype of:* `MotionIntentDataType`
+
+Execute a time-parameterised path. The Server's own motion kernel runs it; this interface hands the whole trajectory over in one submission and does not stream it. That is what makes trajectory execution expressible here when real-time control is not - see clause 4.3.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Points | TrajectoryPointDataType | Array | | The trajectory, in ascending TimeFromStart order. |
+| PathTolerance | MotionToleranceDataType | Scalar | | Permitted deviation while following the path. |
+| GoalTolerance | MotionToleranceDataType | Scalar | | Permitted deviation at the final point. |
+| GoalTimeTolerance | Duration | Scalar | | How much later than the final point's TimeFromStart completion may be, in milliseconds. |
+
+### PathWaypointDataType — `ns=1;i=3073`
+
+*Subtype of:* `Structure`
+
+One waypoint of a Cartesian path, with the blend that applies at it. Per-waypoint blending is what distinguishes a path from a sequence of separate linear moves: the robot need not stop between them.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Pose | Pose3DDataType | Scalar | | Where the tool centre point passes. |
+| Blend | BlendDataType | Scalar | | How this waypoint is left. |
+
+### CartesianPathIntentDataType — `ns=1;i=3074`
+
+*Subtype of:* `MotionIntentDataType`
+
+Follow a list of Cartesian waypoints. This is the portable form of a taught path, and unlike a trajectory it carries no timing: the Server paces it from the motion constraints.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Waypoints | PathWaypointDataType | Array | | The path, in order. |
+
+### ForceIntentDataType — `ns=1;i=3075`
+
+*Subtype of:* `MotionIntentDataType`
+
+Move until contact. The portable subset of the force-controlled moves every vendor offers: travel along a direction until a contact force is reached or a distance is exhausted, whichever comes first. Reaching the distance without contact is a failure, because the intent was to touch something.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Direction | Double | Array | 3 | Unit direction of travel (x, y, z) in the frame of the target. |
+| FrameId | String | Scalar | | The CoordinateFrame Direction is expressed in; empty for the default work frame. |
+| ContactForce | Double | Scalar | | The force in newtons at which contact is declared. |
+| MaxDistance | Double | Scalar | | How far to travel before giving up, in metres. |
+| HoldForce | Boolean | Scalar | | True to keep pressing at ContactForce after contact rather than stopping. |
+
+### ProcessIntentDataType — `ns=1;i=3076`
+
+*Subtype of:* `MotionIntentDataType`
+
+Abstract base of the intents that run an application process along a path. Every process needs the same two things beyond its own parameters: a reference to the process program or procedure the equipment holds, and room for the parameters this specification has not standardised.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| ProcessProgram | NodeId | Scalar | | The Program or equipment-side procedure to run, or null when the parameters here are sufficient. |
+| Attributes | KeyValuePair | Array | | Further named parameters the Server declares in its capabilities. |
+
+### ArcWeldIntentDataType — `ns=1;i=3077`
+
+*Subtype of:* `ProcessIntentDataType`
+
+Lay an arc weld along the path. The parameters are the subset ABB seamdata/welddata/weavedata, FANUC weld schedules and KUKA ArcTech all carry. WeldProcedureRef points at a welding procedure specification (ISO 15609) where the installation works to one; this specification does not restate its content.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Voltage | Double | Scalar | | Arc voltage in volts. |
+| WireFeedSpeed | Double | Scalar | | Wire feed speed in metres per minute. |
+| TravelSpeed | Double | Scalar | | Tool centre point speed in metres per second. |
+| GasPreflowTime | Duration | Scalar | | Shielding gas pre-flow, in milliseconds. |
+| GasPostflowTime | Duration | Scalar | | Shielding gas post-flow, in milliseconds. |
+| ArcStartDelay | Duration | Scalar | | Delay before travel begins, in milliseconds. |
+| CraterFillTime | Duration | Scalar | | Crater fill at the end of the seam, in milliseconds. |
+| WeaveShape | WeaveShapeEnum | Scalar | | Oscillation across the seam. |
+| WeaveAmplitude | Double | Scalar | | Peak-to-peak weave width in metres. |
+| WeaveFrequency | Double | Scalar | | Weave frequency in hertz. |
+| SeamTrackingEnabled | Boolean | Scalar | | True to enable seam tracking where the equipment provides it. |
+| WeldProcedureRef | String | Scalar | | Identifier of the welding procedure specification this weld works to. |
+
+### SpotWeldIntentDataType — `ns=1;i=3078`
+
+*Subtype of:* `ProcessIntentDataType`
+
+Make a resistance spot weld at the target. WeldSchedule selects the weld controller's own program: current and time are the weld controller's business, and are carried here only where the installation drives them from the robot.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| WeldSchedule | UInt32 | Scalar | | The weld controller program to run. |
+| GunForce | Double | Scalar | | Electrode force in newtons. |
+| ApproachDistance | Double | Scalar | | Gun open distance before the weld, in metres. |
+| RetractDistance | Double | Scalar | | Gun open distance after the weld, in metres. |
+| MaterialThickness | Double | Scalar | | Total stack thickness in metres, where the schedule is chosen adaptively. |
+| TipDressRequested | Boolean | Scalar | | True to dress the tips after this weld. |
+
+### DispenseIntentDataType — `ns=1;i=3079`
+
+*Subtype of:* `ProcessIntentDataType`
+
+Lay a bead of adhesive, sealant or paint along the path. The trigger distances exist because material does not start and stop instantly: a Server begins dispensing before the path and stops before its end.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| FlowRate | Double | Scalar | | Nominal flow in millilitres per minute. |
+| TriggerOnDistance | Double | Scalar | | Distance before the path start at which dispensing begins, in metres. |
+| TriggerOffDistance | Double | Scalar | | Distance before the path end at which dispensing stops, in metres. |
+| BeadWidth | Double | Scalar | | Target bead width in metres. |
+| MaterialTemperature | Double | Scalar | | Material temperature in degrees Celsius, for hot-melt work. |
+| PurgeCycles | UInt32 | Scalar | | Nozzle purge cycles before starting. |
+
+### FastenIntentDataType — `ns=1;i=3080`
+
+*Subtype of:* `ProcessIntentDataType`
+
+Drive a fastener at the target. This intent is deliberately THIN: OPC 40450 and OPC 40451 already define joining and tightening in full, so Joint references the joint in that model and the result belongs there. Restating those parameters here would create a second definition of the same fact.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Joint | NodeId | Scalar | | The joint being fastened, in an OPC UA joining or tightening model where one is implemented. |
+| ProgramNumber | UInt32 | Scalar | | The tightening program the tool is to run. |
+| TargetTorque | Double | Scalar | | Target torque in newton metres, where the robot supplies it rather than the tool. |
+| TargetAngle | Double | Scalar | | Target angle in radians, for angle-controlled strategies. |
+| SnugTorque | Double | Scalar | | Torque at which angle counting begins, in newton metres. |
+
+### PalletiseIntentDataType — `ns=1;i=3081`
+
+*Subtype of:* `ProcessIntentDataType`
+
+Place an item into a pattern. The pattern itself is a Location, so its geometry has one definition that a client can read rather than being recomputed from indices on both sides.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| Pattern | NodeId | Scalar | | The Location describing the pallet or pattern. |
+| Layer | UInt32 | Scalar | | Layer index, counting from zero. |
+| Row | UInt32 | Scalar | | Row index within the layer, counting from zero. |
+| Column | UInt32 | Scalar | | Column index within the row, counting from zero. |
+| ItemOrientation | Double | Scalar | | Rotation of the item about the pattern normal, in radians. |
+
+### SurfaceFinishIntentDataType — `ns=1;i=3082`
+
+*Subtype of:* `ProcessIntentDataType`
+
+Follow the path pressing into the surface - grinding, polishing, deburring or sanding. ContactForce is what distinguishes it from a plain path: the robot yields normal to the surface to hold that force.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| ContactForce | Double | Scalar | | Force normal to the surface, in newtons. |
+| FeedRate | Double | Scalar | | Travel speed along the surface, in metres per second. |
+| ToolSpeed | Double | Scalar | | Rotational speed of the tool, in revolutions per minute. |
+| StepOver | Double | Scalar | | Lateral offset between adjacent passes, in metres. |
+
+### MissionTransitionDataType — `ns=1;i=3083`
+
+*Subtype of:* `Structure`
+
+One edge of a mission's step graph, following the step-and-transition form of an IEC 61131-3 sequential function chart. Condition is an OPC UA ContentFilter - the base specification's own filter grammar, reused so that this specification does not invent an expression language for implementers to write a parser for.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| FromStepId | String | Scalar | | The step this transition leaves. |
+| ToStepId | String | Scalar | | The step this transition enters. |
+| Condition | i=586 | Scalar | | The condition under which it is taken. An empty filter is always true. |
+| DivergenceKind | DivergenceKindEnum | Scalar | | How this transition relates to the others leaving the same step. |
+
+### KinematicJointDataType — `ns=1;i=3084`
+
+*Subtype of:* `Structure`
+
+One joint of the kinematic chain: where it sits relative to its predecessor and which way it acts. OPC 40010-1 describes a robot's topology and its axes but defines no kinematic chain, so this is additive rather than a second account of the same thing.
+
+| Field | DataType | ValueRank | ArrayDimensions | Description |
+|---|---|---|---|---|
+| AxisId | String | Scalar | | The AxisType this joint corresponds to. |
+| Kind | AxisKindEnum | Scalar | | Whether it rotates or translates. |
+| OriginTransform | Pose3DDataType | Scalar | | Pose of this joint's frame within its predecessor's, at zero position. |
+| AxisVector | Double | Array | 3 | Unit vector (x, y, z) the joint rotates about or translates along, in its own frame. |
diff --git a/metaverse-specs/extras/robot-intent/tools/validate_local.py b/metaverse-specs/extras/robot-intent/tools/validate_local.py
new file mode 100644
index 0000000..2affe83
--- /dev/null
+++ b/metaverse-specs/extras/robot-intent/tools/validate_local.py
@@ -0,0 +1,586 @@
+#!/usr/bin/env python3
+"""
+Local structural + modelling-rule validator for the OPC UA - Robot Intent NodeSet.
+
+Reproducible in-repo gate (mirrors the vision and openusd-binding validate_local.py
+convention). Everything is re-derived from the committed artifacts with the standard
+library alone; nothing here imports the generator, because a checker that asks the
+emitter what it emitted validates nothing.
+
+Structural checks, against Opc.Ua.RobotIntent.NodeSet2.xml:
+ * XML well-formedness and a single whose ONLY is the base UA
+ namespace - this model is deliberately standalone.
+ * Unique NodeIds; every reference target resolves (own ns=1 node or a base-UA id).
+ * Every UAObjectType/UADataType/UAReferenceType has an inverse HasSubtype to a base.
+ * Every UAReferenceType carries an InverseName.
+ * Every instance-declaration member has a HasTypeDefinition (Objects/Variables) and a
+ HasModellingRule, unless it is the concrete well-known object under the Server.
+ * ParentNodeId is backed by an inverse hierarchical reference, and forward/inverse
+ hierarchical pairs are consistent.
+ * Enum EnumStrings ArrayDimensions equals the number of enum fields.
+ * A CONCRETE Structure has a Definition and a HasEncoding to a resolvable Default
+ Binary encoding Object; an ABSTRACT Structure has a Definition and NO encoding,
+ because nothing is ever encoded as an abstract type.
+ * Every Definition Field DataType resolves, and any field typed by an abstract
+ structure declared here carries AllowSubTypes="true" - polymorphic members must be
+ self-describing rather than leaving a reader to notice the DataType is abstract.
+ * Every Method's InputArguments/OutputArguments ArrayDimensions matches the number of
+ encoded Argument entries.
+ * Opc.Ua.RobotIntent.NodeIds.csv and the NodeSet agree exactly - same id set in both
+ directions, same NodeClass, and the CSV name resolves to the NodeSet BrowseName.
+
+Specification invariants (the reason this file is not generic):
+ * IntentOperationType and MissionType MUST subtype ProgramStateMachineType (i=2391).
+ The whole lifecycle argument of clause 6 rests on this, so a refactor that quietly
+ reparented them would otherwise pass.
+ * IntentDataType and MotionIntentDataType MUST be abstract, and every other
+ *IntentDataType MUST descend from IntentDataType - the hierarchy is what makes a
+ mission step and a submission the same shape.
+ * BufferModeEnum.Aborting MUST be 0: it is the default and every Server accepts it.
+ * IntentFailureEnum.None MUST be 0, so that a default-initialised result reads as
+ "no failure".
+ * OperationalModeEnum MUST number Automatic 3 and AutomaticExternal 4, and
+ StopModeEnum MUST number OnPath 1 through EndOfInstruction 5, matching OPC 40010-1.
+ These are cited as interoperable in Annex B; renumbering them breaks that claim.
+ * ExecutionStateEnum MUST carry exactly the nine states clause 6.3 tabulates.
+ * Pose3DDataType MUST carry Position[3] and Orientation[4] - clause 5.2 fixes the
+ quaternion form.
+ * IntentControllerType MUST declare SubmitIntent, CancelIntent, CancelAll,
+ RequestControl and ReleaseControl as Mandatory: they are the RI-Base facet, and
+ SafetyState as Mandatory, because clause 10.4 drives refusals from it.
+ * ProcessIntentDataType MUST be abstract and all six process intents MUST descend
+ from it, so a client that understands the base can carry one it has never seen.
+ * MissionTransitionDataType.Condition MUST be the base UA ContentFilter. Replacing it
+ with a bespoke expression would oblige every implementer to write a parser.
+ * SafetyStateType's reported members MUST be Mandatory: a refusal rule cannot depend
+ on a member a conformant Server is allowed to omit.
+
+Specification/model cross-checks, in BOTH directions:
+ * Every ObjectType, DataType and ReferenceType the model declares is named in
+ OPC-UA-Robot-Intent.md, and every enumeration literal it declares appears there.
+ * Every `ns=1;i=` the specification cites exists in the NodeSet.
+
+Exit code 0 and "OK" on success; non-zero with an ERRORS list otherwise.
+"""
+from __future__ import annotations
+import csv
+import os
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+STD = os.path.normpath(os.path.join(HERE, "..", "..", "..", "robot-intent"))
+NODESET = os.path.join(STD, "Opc.Ua.RobotIntent.NodeSet2.xml")
+CSVFILE = os.path.join(STD, "Opc.Ua.RobotIntent.NodeIds.csv")
+SPEC = os.path.join(STD, "OPC-UA-Robot-Intent.md")
+
+NS = {"u": "http://opcfoundation.org/UA/2011/03/UANodeSet.xsd"}
+UAX = {"uax": "http://opcfoundation.org/UA/2008/02/Types.xsd"}
+UA_NAMESPACE = "http://opcfoundation.org/UA/"
+PROGRAM_STATE_MACHINE = "i=2391"
+CONTENT_FILTER = "i=586"
+
+# Reference types that make a node a child of its ParentNodeId.
+HIERARCHICAL = {"HasComponent", "HasProperty", "Organizes", "HasSubtype",
+ "i=47", "i=46", "i=35", "i=45"}
+ALIAS_OF = {"HasComponent": "i=47", "HasProperty": "i=46", "Organizes": "i=35",
+ "HasSubtype": "i=45", "HasTypeDefinition": "i=40",
+ "HasModellingRule": "i=37", "HasEncoding": "i=38",
+ "HasInterface": "i=17603"}
+
+ERRORS: list[str] = []
+
+
+def err(msg: str) -> None:
+ ERRORS.append(msg)
+
+
+def canon(ref: str) -> str:
+ return ALIAS_OF.get(ref, ref)
+
+
+def local(tag: str) -> str:
+ return tag.rsplit("}", 1)[-1]
+
+
+class Model:
+ """The NodeSet, re-read from XML with no help from the generator."""
+
+ def __init__(self, path: str):
+ self.tree = ET.parse(path)
+ self.root = self.tree.getroot()
+ self.nodes: dict[str, ET.Element] = {}
+ self.order: list[str] = []
+ self.aliases: dict[str, str] = {}
+ for a in self.root.findall("u:Aliases/u:Alias", NS):
+ self.aliases[a.get("Alias", "")] = (a.text or "").strip()
+ for el in self.root:
+ t = local(el.tag)
+ if not t.startswith("UA") or t in ("UANodeSet",):
+ continue
+ nid = el.get("NodeId", "")
+ if not nid:
+ continue
+ if nid in self.nodes:
+ err(f"duplicate NodeId {nid}")
+ self.nodes[nid] = el
+ self.order.append(nid)
+
+ def cls(self, nid: str) -> str:
+ return local(self.nodes[nid].tag)
+
+ def bname(self, nid: str) -> str:
+ return (self.nodes[nid].get("BrowseName", "").split(":", 1)[-1])
+
+ def refs(self, nid: str):
+ out = []
+ for r in self.nodes[nid].findall("u:References/u:Reference", NS):
+ out.append((canon(r.get("ReferenceType", "")),
+ (r.text or "").strip(),
+ r.get("IsForward", "true") != "false"))
+ return out
+
+ def supertype(self, nid: str) -> str:
+ for rt, tgt, fwd in self.refs(nid):
+ if rt == "i=45" and not fwd:
+ return tgt
+ return ""
+
+ def resolves(self, target: str) -> bool:
+ """Own-namespace targets must exist here; base-UA ids are taken on trust."""
+ if target in self.nodes:
+ return True
+ if target.startswith("ns=1;"):
+ return False
+ return bool(re.fullmatch(r"i=\d+", target)) or target in self.aliases
+
+ def definition(self, nid: str):
+ for child in self.nodes[nid]:
+ if local(child.tag) == "Definition":
+ return child
+ return None
+
+ def members_of(self, nid: str) -> list[str]:
+ want = f"ns=1;i={nid.split('=')[-1]}" if not nid.startswith("ns=1;") else nid
+ return [m for m in self.order if self.nodes[m].get("ParentNodeId") == want]
+
+ def member_named(self, owner: str, name: str) -> str:
+ for m in self.members_of(owner):
+ if self.bname(m) == name:
+ return m
+ return ""
+
+ def modelling_rule(self, nid: str) -> str:
+ for rt, tgt, fwd in self.refs(nid):
+ if rt == "i=37":
+ return {"i=78": "Mandatory", "i=80": "Optional",
+ "i=11508": "OptionalPlaceholder",
+ "i=11510": "MandatoryPlaceholder"}.get(tgt, tgt)
+ return ""
+
+ def enum_fields(self, nid: str) -> list[tuple[str, int]]:
+ d = self.definition(nid)
+ if d is None:
+ return []
+ out = []
+ for f in d:
+ if local(f.tag) == "Field":
+ try:
+ out.append((f.get("Name", ""), int(f.get("Value", "0"))))
+ except ValueError:
+ err(f"enum {self.bname(nid)} field {f.get('Name')} has a "
+ "non-integer Value")
+ return out
+
+ def struct_fields(self, nid: str) -> list[ET.Element]:
+ d = self.definition(nid)
+ return [] if d is None else [f for f in d if local(f.tag) == "Field"]
+
+ def by_name(self, name: str) -> str:
+ for nid in self.order:
+ if self.bname(nid) == name:
+ return nid
+ return ""
+
+
+def check_model_header(m: Model) -> None:
+ models = m.root.findall("u:Models/u:Model", NS)
+ if len(models) != 1:
+ err(f"expected exactly one , found {len(models)}")
+ return
+ req = models[0].findall("u:RequiredModel", NS)
+ uris = [r.get("ModelUri") for r in req]
+ if uris != [UA_NAMESPACE]:
+ err("this model is standalone: the only RequiredModel must be "
+ f"{UA_NAMESPACE}, found {uris}")
+ uris_declared = [(u.text or "").strip()
+ for u in m.root.findall("u:NamespaceUris/u:Uri", NS)]
+ if len(uris_declared) != 1:
+ err(f"expected exactly one NamespaceUri, found {uris_declared}")
+ elif uris_declared[0] != models[0].get("ModelUri"):
+ err("NamespaceUris entry and Model ModelUri disagree")
+
+
+def check_references(m: Model) -> None:
+ for nid in m.order:
+ for rt, tgt, fwd in m.refs(nid):
+ if not m.resolves(tgt):
+ err(f"{m.bname(nid)} ({nid}) references unresolvable target {tgt}")
+ if not m.resolves(rt):
+ err(f"{m.bname(nid)} ({nid}) uses unresolvable ReferenceType {rt}")
+
+
+def check_types(m: Model) -> None:
+ for nid in m.order:
+ c = m.cls(nid)
+ if c in ("UAObjectType", "UADataType", "UAReferenceType", "UAVariableType"):
+ if not m.supertype(nid):
+ err(f"{c} {m.bname(nid)} ({nid}) has no inverse HasSubtype")
+ if c == "UAReferenceType":
+ inv = m.nodes[nid].find("u:InverseName", NS)
+ if inv is None or not (inv.text or "").strip():
+ err(f"ReferenceType {m.bname(nid)} ({nid}) has no InverseName")
+
+
+def check_instance_declarations(m: Model) -> None:
+ for nid in m.order:
+ el = m.nodes[nid]
+ parent = el.get("ParentNodeId")
+ if not parent:
+ continue
+ c = m.cls(nid)
+ if c in ("UAObject", "UAVariable"):
+ if not any(rt == "i=40" for rt, _, _ in m.refs(nid)):
+ err(f"{m.bname(nid)} ({nid}) has no HasTypeDefinition")
+ external_root = parent not in m.nodes
+ if not external_root and not m.modelling_rule(nid):
+ # A node parented on a base-UA node (the well-known object under the
+ # Server) is a concrete instance, not an instance declaration, so it
+ # carries no ModellingRule.
+ err(f"{m.bname(nid)} ({nid}) has no HasModellingRule")
+ backed = any(canon(rt) in HIERARCHICAL and not fwd and tgt == parent
+ for rt, tgt, fwd in m.refs(nid))
+ if not backed:
+ err(f"{m.bname(nid)} ({nid}) ParentNodeId {parent} is not backed by an "
+ "inverse hierarchical reference")
+ # the forward half must exist on the parent
+ if parent in m.nodes:
+ fwd_ok = any(canon(rt) in HIERARCHICAL and fwd and tgt == nid
+ for rt, tgt, fwd in m.refs(parent))
+ if not fwd_ok:
+ err(f"parent {m.bname(parent)} ({parent}) has no forward hierarchical "
+ f"reference to {m.bname(nid)} ({nid})")
+
+
+def check_datatypes(m: Model) -> None:
+ abstract_structs = {
+ nid for nid in m.order
+ if m.cls(nid) == "UADataType"
+ and m.nodes[nid].get("IsAbstract") == "true"
+ }
+ for nid in m.order:
+ if m.cls(nid) != "UADataType":
+ continue
+ name = m.bname(nid)
+ d = m.definition(nid)
+ if d is None:
+ err(f"DataType {name} ({nid}) has no Definition")
+ continue
+ sup = m.supertype(nid)
+ is_enum = sup == "i=29"
+ encodings = [tgt for rt, tgt, fwd in m.refs(nid) if rt == "i=38" and fwd]
+ if is_enum:
+ fields = m.enum_fields(nid)
+ es = m.by_name("EnumStrings")
+ # EnumStrings is a property of THIS enum, found by parent
+ es = ""
+ for mm in m.members_of(nid):
+ if m.bname(mm) == "EnumStrings":
+ es = mm
+ if not es:
+ err(f"enum {name} ({nid}) has no EnumStrings property")
+ else:
+ dims = m.nodes[es].get("ArrayDimensions", "")
+ if dims != str(len(fields)):
+ err(f"enum {name} EnumStrings ArrayDimensions {dims!r} does not "
+ f"match its {len(fields)} fields")
+ if len({v for _, v in fields}) != len(fields):
+ err(f"enum {name} has duplicate field values")
+ else:
+ if nid in abstract_structs:
+ if encodings:
+ err(f"abstract DataType {name} ({nid}) must have no encoding, "
+ f"found {encodings}")
+ else:
+ if len(encodings) != 1:
+ err(f"concrete Structure {name} ({nid}) must have exactly one "
+ f"HasEncoding, found {encodings}")
+ elif encodings[0] not in m.nodes:
+ err(f"Structure {name} encoding {encodings[0]} does not resolve")
+ for f in m.struct_fields(nid):
+ fdt = f.get("DataType", "")
+ if not m.resolves(fdt):
+ err(f"Structure {name} field {f.get('Name')} has unresolvable "
+ f"DataType {fdt}")
+ if fdt in abstract_structs and f.get("AllowSubTypes") != "true":
+ err(f"Structure {name} field {f.get('Name')} is typed by the "
+ f"abstract {m.bname(fdt)} and must carry "
+ 'AllowSubTypes="true"')
+
+
+def check_method_arguments(m: Model) -> None:
+ for nid in m.order:
+ if m.cls(nid) != "UAMethod":
+ continue
+ for which in ("InputArguments", "OutputArguments"):
+ arg = ""
+ for mm in m.members_of(nid):
+ if m.bname(mm) == which:
+ arg = mm
+ if not arg:
+ continue
+ el = m.nodes[arg]
+ count = len(el.findall(".//uax:Argument", UAX))
+ dims = el.get("ArrayDimensions", "")
+ if dims != str(count):
+ err(f"{m.bname(nid)}.{which} ArrayDimensions {dims!r} does not match "
+ f"its {count} encoded Argument entries")
+ if el.get("ValueRank") != "1":
+ err(f"{m.bname(nid)}.{which} must have ValueRank 1")
+
+
+def check_csv(m: Model) -> None:
+ if not os.path.exists(CSVFILE):
+ err(f"missing {CSVFILE}")
+ return
+ rows = []
+ with open(CSVFILE, newline="", encoding="utf-8") as fh:
+ for row in csv.reader(fh):
+ if row and row[0].strip():
+ rows.append(row)
+ csv_ids = {}
+ for row in rows:
+ if len(row) != 3:
+ err(f"NodeIds.csv row is not 3 columns: {row}")
+ continue
+ name, sid, cls = row[0].strip(), row[1].strip(), row[2].strip()
+ if sid in csv_ids:
+ err(f"NodeIds.csv has duplicate id {sid}")
+ csv_ids[sid] = (name, cls)
+ ns_ids = {nid.split("=")[-1]: nid for nid in m.order}
+ for sid, (name, cls) in csv_ids.items():
+ nid = ns_ids.get(sid)
+ if nid is None:
+ err(f"NodeIds.csv lists {name} ({sid}) which is not in the NodeSet")
+ continue
+ if m.cls(nid)[2:] != cls:
+ err(f"NodeIds.csv says {name} ({sid}) is {cls}; NodeSet says "
+ f"{m.cls(nid)[2:]}")
+ bn = m.bname(nid)
+ # Members are qualified Owner_Member in the CSV, and a structure's encoding
+ # object browses as "Default Binary" but is published as Type_Encoding_...
+ if name.endswith("_Encoding_DefaultBinary"):
+ owner = name[: -len("_Encoding_DefaultBinary")]
+ if bn != "Default Binary":
+ err(f"NodeIds.csv {name!r} ({sid}) should be the Default Binary "
+ f"encoding object; NodeSet BrowseName is {bn!r}")
+ elif not m.by_name(owner):
+ err(f"NodeIds.csv {name!r} ({sid}) names an encoding of {owner!r}, "
+ "which is not a type in the NodeSet")
+ elif name != bn and not name.endswith("_" + bn):
+ err(f"NodeIds.csv name {name!r} ({sid}) does not resolve to NodeSet "
+ f"BrowseName {bn!r}")
+ for sid, nid in ns_ids.items():
+ if sid not in csv_ids:
+ err(f"NodeSet node {m.bname(nid)} ({nid}) is missing from NodeIds.csv")
+
+
+def check_spec_invariants(m: Model) -> None:
+ def dt(name: str) -> str:
+ nid = m.by_name(name)
+ if not nid:
+ err(f"required type {name} is missing from the model")
+ return nid
+
+ for name in ("IntentOperationType", "MissionType"):
+ nid = dt(name)
+ if nid and m.supertype(nid) != PROGRAM_STATE_MACHINE:
+ err(f"{name} must subtype ProgramStateMachineType "
+ f"({PROGRAM_STATE_MACHINE}); found {m.supertype(nid)!r}. Clause 6 "
+ "rests on the Part 10 lifecycle.")
+
+ for name in ("IntentDataType", "MotionIntentDataType"):
+ nid = dt(name)
+ if nid and m.nodes[nid].get("IsAbstract") != "true":
+ err(f"{name} must be abstract")
+
+ intent_base = m.by_name("IntentDataType")
+ if intent_base:
+ for nid in m.order:
+ n = m.bname(nid)
+ if m.cls(nid) != "UADataType" or not n.endswith("IntentDataType"):
+ continue
+ if n in ("IntentDataType",):
+ continue
+ seen, cur = set(), m.supertype(nid)
+ while cur.startswith("ns=1;") and cur not in seen:
+ seen.add(cur)
+ cur = m.supertype(cur)
+ if intent_base not in seen:
+ err(f"{n} does not descend from IntentDataType")
+
+ expect_values = {
+ "BufferModeEnum": {"Aborting": 0},
+ "IntentFailureEnum": {"None": 0},
+ "OperationalModeEnum": {"Automatic": 3, "AutomaticExternal": 4},
+ "StopModeEnum": {"OnPath": 1, "EndOfCycle": 2, "ProcessStop": 3,
+ "QuickStop": 4, "EndOfInstruction": 5},
+ "AxisKindEnum": {"Revolute": 0, "Prismatic": 1},
+ "TerminationModeEnum": {"Exact": 0, "Blend": 1},
+ # Abort is what a mission without a declared policy gets, so it must be zero.
+ "ErrorPolicyEnum": {"Abort": 0},
+ "DivergenceKindEnum": {"Alternative": 0, "Parallel": 1},
+ "SafeMotionFunctionEnum": {"None": 0},
+ }
+ for ename, wanted in expect_values.items():
+ nid = dt(ename)
+ if not nid:
+ continue
+ got = dict(m.enum_fields(nid))
+ for field, value in wanted.items():
+ if got.get(field) != value:
+ err(f"{ename}.{field} must be {value}; found {got.get(field)!r}")
+
+ exec_nid = dt("ExecutionStateEnum")
+ if exec_nid:
+ want = {"Accepted", "Queued", "Executing", "Suspended", "Cancelling",
+ "Succeeded", "Failed", "Cancelled", "Retriable"}
+ got = {f for f, _ in m.enum_fields(exec_nid)}
+ if got != want:
+ err(f"ExecutionStateEnum must carry exactly {sorted(want)}; "
+ f"found {sorted(got)}")
+
+ pose = dt("Pose3DDataType")
+ if pose:
+ dims = {f.get("Name"): f.get("ArrayDimensions")
+ for f in m.struct_fields(pose)}
+ if dims.get("Position") != "3":
+ err("Pose3DDataType.Position must be a 3-element array")
+ if dims.get("Orientation") != "4":
+ err("Pose3DDataType.Orientation must be a 4-element quaternion array")
+
+ ctl = dt("IntentControllerType")
+ if ctl:
+ for name in ("SubmitIntent", "CancelIntent", "CancelAll", "RequestControl",
+ "ReleaseControl"):
+ mm = m.member_named(ctl, name)
+ if not mm:
+ err(f"IntentControllerType must declare {name} (RI-Base facet)")
+ elif m.modelling_rule(mm) != "Mandatory":
+ err(f"IntentControllerType.{name} must be Mandatory for RI-Base; "
+ f"found {m.modelling_rule(mm)!r}")
+ # Clause 10.4 requires a Server to refuse on what the safety system reports,
+ # which it cannot do if the state is not there to read.
+ safety = m.member_named(ctl, "SafetyState")
+ if not safety:
+ err("IntentControllerType must declare SafetyState: clause 10.4 requires "
+ "refusals to be driven from it")
+ elif m.modelling_rule(safety) != "Mandatory":
+ err("IntentControllerType.SafetyState must be Mandatory")
+
+ # Every process intent descends from ProcessIntentDataType, so a client that
+ # understands the base can carry one it has never seen.
+ process_base = m.by_name("ProcessIntentDataType")
+ if process_base:
+ if m.nodes[process_base].get("IsAbstract") != "true":
+ err("ProcessIntentDataType must be abstract")
+ for name in ("ArcWeldIntentDataType", "SpotWeldIntentDataType",
+ "DispenseIntentDataType", "FastenIntentDataType",
+ "PalletiseIntentDataType", "SurfaceFinishIntentDataType"):
+ nid = m.by_name(name)
+ if not nid:
+ err(f"required process intent {name} is missing from the model")
+ elif m.supertype(nid) != process_base:
+ err(f"{name} must subtype ProcessIntentDataType")
+
+ # The transition condition reuses the base UA ContentFilter rather than inventing
+ # an expression language; a change here would oblige implementers to write a parser.
+ transition = m.by_name("MissionTransitionDataType")
+ if transition:
+ fields = {f.get("Name"): f.get("DataType") for f in m.struct_fields(transition)}
+ if fields.get("Condition") != CONTENT_FILTER:
+ err("MissionTransitionDataType.Condition must be the base UA ContentFilter "
+ f"({CONTENT_FILTER}); found {fields.get('Condition')!r}")
+
+ safety_state = m.by_name("SafetyStateType")
+ if safety_state:
+ for name in ("ActiveFunction", "EmergencyStopActive", "ProtectiveStopActive",
+ "SafeSpeedLimitActive", "SafeSpeedLimit", "SafetyControllerOk"):
+ mm = m.member_named(safety_state, name)
+ if not mm:
+ err(f"SafetyStateType must declare {name} (clause 10.4)")
+ elif m.modelling_rule(mm) != "Mandatory":
+ err(f"SafetyStateType.{name} must be Mandatory: a refusal rule cannot "
+ "depend on an optional member")
+
+
+def check_spec_crossref(m: Model) -> None:
+ if not os.path.exists(SPEC):
+ err(f"missing {SPEC}")
+ return
+ with open(SPEC, encoding="utf-8") as fh:
+ text = fh.read()
+
+ for nid in m.order:
+ if m.cls(nid) not in ("UAObjectType", "UADataType", "UAReferenceType"):
+ continue
+ name = m.bname(nid)
+ if name not in text:
+ err(f"model declares {m.cls(nid)[2:]} {name} but the specification never "
+ "names it")
+ if m.supertype(nid) == "i=29":
+ for field, _ in m.enum_fields(nid):
+ if not re.search(rf"\b{re.escape(field)}\b", text):
+ err(f"model declares {name}.{field} but the specification never "
+ "names it")
+
+ for cited in set(re.findall(r"ns=1;i=(\d+)", text)):
+ if f"ns=1;i={cited}" not in m.nodes:
+ err(f"specification cites ns=1;i={cited}, which is not in the NodeSet")
+
+
+def main() -> int:
+ if not os.path.exists(NODESET):
+ print(f"ERROR: missing {NODESET}")
+ return 2
+ try:
+ m = Model(NODESET)
+ except ET.ParseError as exc:
+ print(f"ERROR: NodeSet is not well-formed XML: {exc}")
+ return 2
+
+ check_model_header(m)
+ check_references(m)
+ check_types(m)
+ check_instance_declarations(m)
+ check_datatypes(m)
+ check_method_arguments(m)
+ check_csv(m)
+ check_spec_invariants(m)
+ check_spec_crossref(m)
+
+ if ERRORS:
+ print(f"ERRORS ({len(ERRORS)}):")
+ for e in ERRORS:
+ print(f" - {e}")
+ return 1
+ n_types = sum(1 for nid in m.order
+ if m.cls(nid) in ("UAObjectType", "UADataType", "UAReferenceType"))
+ print(f"OK - robot-intent: {len(m.order)} nodes, {n_types} types, "
+ "NodeSet/CSV/specification consistent")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/metaverse-specs/extras/vision/examples/machine-vision/Inspection.Vision.json b/metaverse-specs/extras/vision/examples/machine-vision/Inspection.Vision.json
index f79f060..080adc9 100644
--- a/metaverse-specs/extras/vision/examples/machine-vision/Inspection.Vision.json
+++ b/metaverse-specs/extras/vision/examples/machine-vision/Inspection.Vision.json
@@ -71,10 +71,10 @@
"fields": [
["Fx", "8310.2", "px"],
["Fy", "8309.6", "px"],
- ["Cx", "1295.4", "px, corner-datum per 5.10"],
- ["Cy", "971.2", "px, corner-datum per 5.10"],
+ ["Cx", "1295.4", "px, corner-datum per §5.12"],
+ ["Cy", "971.2", "px, corner-datum per §5.12"],
["Skew", "0.0", "px"],
- ["DistortionModel", "BrownConrady", "5.10 ordering: k1, k2, p1, p2, k3"],
+ ["DistortionModel", "BrownConrady", "§5.12 ordering: k1, k2, p1, p2, k3"],
["DistortionCoefficients", "[-0.0021, 0.0004, 0.0, 0.0, 0.0]", "dimensionless; a telecentric lens is close to distortion-free"],
["Width", "2592", "px"],
["Height", "1944", "px"]
@@ -93,10 +93,10 @@
"sourceFrame": "camera_insp_07",
"targetFrame": "station",
"fields": [
- ["FrameId", "station", "equals the TargetFrame's FrameId, per the 5.10 frame-precedence rule"],
+ ["FrameId", "station", "equals the TargetFrame's FrameId, per the §5.12 frame-precedence rule"],
["Position", "(0.0, 0.0, 0.320)", "metres, ordered (x, y, z)"],
["Orientation", "(1.0, 0.0, 0.0, 0.0)", "unit quaternion ordered (x, y, z, w); a 180 degree rotation about x, so the camera looks down at the station"],
- ["Covariance", "empty array", "not reported, per the 5.10 sentinel"]
+ ["Covariance", "empty array", "not reported, per the §5.12 sentinel"]
]
}
],
diff --git a/metaverse-specs/extras/vision/examples/robotics/Robotics.Vision.json b/metaverse-specs/extras/vision/examples/robotics/Robotics.Vision.json
index c2f1bd8..aa73640 100644
--- a/metaverse-specs/extras/vision/examples/robotics/Robotics.Vision.json
+++ b/metaverse-specs/extras/vision/examples/robotics/Robotics.Vision.json
@@ -58,9 +58,16 @@
},
{
"name": "FlangeFrame",
- "description": "Robot flange / tool centre point frame; the camera moves with it.",
+ "description": "Robot mechanical interface — the flange the camera and the gripper are both bolted to. This is what the hand-eye calibration resolves to, and it is deliberately NOT the tool centre point.",
"frameId": "flange",
"parentFrame": "robot_base",
+ "role": "MechanicalInterface"
+ },
+ {
+ "name": "GripperTcpFrame",
+ "description": "Tool centre point of the gripper. A pick pose has to reach THIS frame, not the flange, so the example carries the offset between them explicitly rather than leaving a consumer to assume it.",
+ "frameId": "gripper_tcp",
+ "parentFrame": "flange",
"role": "Tool"
},
{
@@ -84,10 +91,10 @@
"fields": [
["Fx", "2140.5", "px"],
["Fy", "2139.8", "px"],
- ["Cx", "1223.1", "px, corner-datum per 5.10"],
- ["Cy", "1021.7", "px, corner-datum per 5.10"],
+ ["Cx", "1223.1", "px, corner-datum per §5.12"],
+ ["Cy", "1021.7", "px, corner-datum per §5.12"],
["Skew", "0.0", "px"],
- ["DistortionModel", "BrownConrady", "5.10 ordering: k1, k2, p1, p2, k3"],
+ ["DistortionModel", "BrownConrady", "§5.12 ordering: k1, k2, p1, p2, k3"],
["DistortionCoefficients", "[-0.1721, 0.0934, 0.0002, -0.0001, -0.0188]", "dimensionless"],
["Width", "2448", "px"],
["Height", "2048", "px"]
@@ -96,7 +103,7 @@
{
"name": "HandEye",
"type": "ExtrinsicCalibrationType",
- "description": "Transform from the camera frame to the robot flange frame. Eye-in-hand: the camera moves with the tool.",
+ "description": "Transform from the camera frame to the robot mechanical interface. Eye-in-hand: the camera moves with the flange, so a pick pose is obtained by composing camera → flange → tool centre point.",
"calibrationId": "hand-eye-cam-eih-01",
"performedAt": "2026-06-14T10:40:00Z",
"valid": true,
@@ -106,10 +113,10 @@
"sourceFrame": "camera_eih",
"targetFrame": "flange",
"fields": [
- ["FrameId", "flange", "equals the TargetFrame's FrameId, per the 5.10 frame-precedence rule"],
+ ["FrameId", "flange", "equals the TargetFrame's FrameId, per the §5.12 frame-precedence rule"],
["Position", "(0.062, -0.031, 0.115)", "metres, ordered (x, y, z)"],
["Orientation", "(0.0, 0.0, 0.7071, 0.7071)", "unit quaternion ordered (x, y, z, w)"],
- ["Covariance", "empty array", "not reported, per the 5.10 sentinel"]
+ ["Covariance", "empty array", "not reported, per the §5.12 sentinel"]
]
}
],
@@ -238,7 +245,7 @@
"continuous": true
}
},
- "inferenceNote": "Inference runs **off-server** on a cell-side GPU appliance. The Server publishes results it did not compute. Nothing else in the model changes: a client reads `DetectionResultType` exactly as it would if `InferenceLocation` were `OnServer`, and consults that property only if it cares about the latency or trust boundary. Because the deployment is remote, base specification §12.6 applies: the channel to the inference service is authenticated and integrity-protected, and `AiModelType.Digest` lets a consumer confirm which artefact produced a result.",
- "resultsNote": "Each cycle produces a `DetectionResultType` whose `Detections` carry `ClassLabel`, `Confidence`, a `BoundingBox2D`, a `BoundingBox3D` and — the member that makes the result actionable — a 6-DoF `Pose`. Every pose names its `FrameId` (`camera_eih`), which is only meaningful because the `HandEye` calibration above relates that frame to the flange. A consumer composes camera → flange → base through the `CoordinateFrameType` tree to obtain a pose the robot controller can execute. `ResidualError` on the calibration is what tells the consumer how much to trust it.",
+ "inferenceNote": "Inference runs **off-server** on a cell-side GPU appliance. The Server publishes results it did not compute. Nothing else in the model changes: a client reads `DetectionResultType` exactly as it would if `InferenceLocation` were `OnServer`, and consults that property only if it cares about the latency or trust boundary. Because the deployment is remote, base specification §12.6 applies: the channel to the inference service is authenticated and integrity-protected, and `ModelType.Digest` lets a consumer confirm which artefact produced a result.",
+ "resultsNote": "Each cycle produces a `DetectionResultType` whose `Detections` carry `ClassLabel`, `Confidence`, a `BoundingBox2D`, a `BoundingBox3D` and — the member that makes the result actionable — a 6-DoF `Pose`. Every pose names its `FrameId` (`camera_eih`), which is only meaningful because the `HandEye` calibration above relates that frame to the flange. A consumer composes camera → flange → base through the `CoordinateFrameType` tree to obtain the pose in robot coordinates, and camera → flange → `gripper_tcp` to obtain what the gripper must actually reach. The two are distinct: the calibration resolves to the mechanical interface, while a grasp is executed at the tool centre point, and the frame tree carries the offset between them rather than leaving it to be assumed. `ResidualError` on the calibration is what tells the consumer how much to trust it.",
"feedbackNote": "Two feedback paths are exercised. During commissioning, the HMI calls `SubmitDetections` with `Purpose = Overlay` so the operator sees candidate grasps drawn on the RTSP stream. In production, a failed pick calls `SubmitCorrection` with `Purpose = GroundTruthLabel`, and the corrected pose is retained by the `LearningJobType` as a labelled sample — so the cases the model gets wrong are exactly the cases the next dataset contains. Feedback images are passed by reference through `SubmitImageReference`; this example does not enable inline feedback images."
}
diff --git a/metaverse-specs/extras/vision/tools/build_examples.py b/metaverse-specs/extras/vision/tools/build_examples.py
index 1c3324b..625397a 100644
--- a/metaverse-specs/extras/vision/tools/build_examples.py
+++ b/metaverse-specs/extras/vision/tools/build_examples.py
@@ -29,7 +29,18 @@
sys.path.insert(0, HERE)
import build_model as vm # noqa: E402 (path set above)
+# The AI Model Management model is a separate specification in a sibling extras tree. It is
+# loaded by path rather than imported as a package so that neither generator depends on
+# the other's location, and so a reader can see exactly which file is being read.
+import importlib.util as _ilu # noqa: E402
+_AI_GEN = os.path.normpath(
+ os.path.join(HERE, "..", "..", "ai-model-management", "tools", "build_model.py"))
+_spec = _ilu.spec_from_file_location("ai_build_model", _AI_GEN)
+am = _ilu.module_from_spec(_spec)
+_spec.loader.exec_module(am)
+
VISION_NS = vm.NAMESPACE
+AI_NS = am.NAMESPACE
# Base-UA NodeIds used by the overlays. Emitted through an block, as the
# base NodeSets in this repository do, so the XML stays readable.
@@ -89,12 +100,39 @@ def mandatory_members(type_name):
if n.cls in ("UAObjectType", "UADataType", "UAReferenceType")}
# The Vision ReferenceTypes the overlays use, aliased so the XML stays readable.
-for _rt in ("HasCalibration", "MountedOn", "UsesModel"):
+for _rt in ("HasCalibration", "MountedOn"):
ALIASES.append((_rt, f"ns=2;i={TYPE_ID[_rt]}"))
HasCalibration = "HasCalibration"
MountedOn = "MountedOn"
UsesModel = "UsesModel"
+# Type BrowseName -> NodeId in the AI Model Management namespace, taken from that model.
+AI_TYPE_ID = {n.bname: n.nid for n in am.NODES.values()
+ if n.cls in ("UAObjectType", "UADataType", "UAReferenceType")}
+ALIASES.append((UsesModel, f"ns=3;i={AI_TYPE_ID[UsesModel]}"))
+
+
+def aitype(name):
+ """Type NodeId in the AI Model Management namespace (index 3)."""
+ if name not in AI_TYPE_ID:
+ raise SystemExit(f"unknown AI type '{name}' - check ai build_model.py")
+ return f"ns=3;i={AI_TYPE_ID[name]}"
+
+
+def put_enum_ai(ov, parent, name, enum_name, literal, desc=None):
+ """An enum Property whose DataType is declared by the AI Model Management model."""
+ val = am_enum_value(enum_name, literal)
+ return ov.prop(name, aitype(enum_name), v_int32(val), parent, desc)
+
+
+def am_enum_value(enum_name, literal):
+ for n in am.NODES.values():
+ if n.bname == enum_name and n.definition:
+ m = re.search(rf'',
f' {sx.escape(self.example_uri)}',
f' {sx.escape(VISION_NS)}',
+ f' {sx.escape(AI_NS)}',
' ',
' ',
f' ',
+ f' ',
' ',
' ',
' ']
@@ -491,10 +533,25 @@ def build_overlay(d):
desc="Well-known Vision entry point for this example (§4.2).")
f_sensors = ov.folder("Sensors", root)
f_pipelines = ov.folder("Pipelines", root)
- f_models = ov.folder("Models", root)
f_frames = ov.folder("Frames", root)
- f_jobs = ov.folder("LearningJobs", root)
+
+ # Models, datasets, deployments and learning jobs belong to OPC UA - AI Model Management
+ # and Learning, whose own well-known object sits BESIDE the Vision one under the
+ # Server Object. Hanging them under the Vision root would put them in folders
+ # VisionRootType does not declare, and would leave the example unable to satisfy
+ # the AI-Base facet that the VIS-Inference-* and VIS-Learning facets require.
+ airoot = ov.obj("AiModelManagement", aitype("AiRootType"), external_parent=SERVER_OBJECT,
+ browse_ns=3,
+ desc="Well-known AI Model Management entry point for this example.")
+ ov.prop("SpecificationVersion", "String", v_string(am.VERSION), airoot,
+ "Release of the AI Model Management specification this example is built against.",
+ browse_ns=3)
+ f_models = ov.folder("Models", airoot, browse_ns=3)
+ f_datasets = ov.folder("Datasets", airoot, browse_ns=3)
+ f_deployments = ov.folder("Deployments", airoot, browse_ns=3)
+ f_jobs = ov.folder("LearningJobs", airoot, browse_ns=3)
ov.roots = dict(sensors=f_sensors, pipelines=f_pipelines, models=f_models,
+ datasets=f_datasets, deployments=f_deployments,
frames=f_frames, jobs=f_jobs)
sim = d.get("simulation") if s.get("realityKind") in ("Simulated", "Hybrid") else None
@@ -568,7 +625,7 @@ def build_overlay(d):
# --- AI -----------------------------------------------------------------
ai = d["ai"]
- model = ov.obj(ai["model"]["name"], vtype("AiModelType"), ov.roots["models"],
+ model = ov.obj(ai["model"]["name"], aitype("ModelType"), ov.roots["models"],
reftype=Organizes, desc=ai["model"].get("description"))
m = ai["model"]
put(ov, model, "ModelId", "String", m["modelId"])
@@ -580,23 +637,23 @@ def build_overlay(d):
put(ov, model, name, "String", m[key])
# §12.6 requires both for any model reachable through ArtifactUri.
put(ov, model, "Digest", "ByteString", m["digest"],
- desc="SHA-256 of the model artefact at ArtifactUri (base specification §12.6).")
+ desc="SHA-256 of the model artefact at ArtifactUri.")
put(ov, model, "DigestAlgorithm", "String", m.get("digestAlgorithm", "SHA-256"))
dep = ai["deployment"]
- deployment = ov.obj(dep["name"], vtype("AiDeploymentType"), ov.roots["models"],
+ deployment = ov.obj(dep["name"], aitype("DeploymentType"), ov.roots["models"],
reftype=Organizes, desc=dep.get("description"))
put(ov, deployment, "DeploymentId", "String", dep["deploymentId"])
- put_enum(ov, deployment, "InferenceLocation", "VisionInferenceLocationEnum",
+ put_enum_ai(ov, deployment, "InferenceLocation", "InferenceLocationEnum",
dep["inferenceLocation"])
if "acceleratorKind" in dep:
- put_enum(ov, deployment, "AcceleratorKind", "VisionAcceleratorKindEnum",
+ put_enum_ai(ov, deployment, "AcceleratorKind", "AcceleratorKindEnum",
dep["acceleratorKind"])
if "acceleratorName" in dep:
put(ov, deployment, "AcceleratorName", "String", dep["acceleratorName"])
if "endpointUri" in dep:
put(ov, deployment, "EndpointUri", "String", dep["endpointUri"])
- # §5.11 requires exactly one UsesModel per deployment. It is the only path from a
+ # The AI specification requires exactly one UsesModel per deployment. It is the only path from a
# result to the model artefact and its Digest, which §12.6 depends on.
ov.ref(deployment, UsesModel, f"ns=1;i={model}")
@@ -623,16 +680,16 @@ def build_overlay(d):
build_media(ov, twin, tw["stream"], tw["clip"])
if "learningJob" in tw:
lj = tw["learningJob"]
- dataset = ov.obj(lj["datasetName"], vtype("AiDatasetType"),
+ dataset = ov.obj(lj["datasetName"], aitype("DatasetType"),
ov.roots["models"], reftype=Organizes,
desc="Synthetic dataset produced from the twin.")
put(ov, dataset, "DatasetId", "String", lj["datasetId"])
- put_enum(ov, dataset, "SourceKind", "VisionDatasetSourceEnum",
+ put_enum_ai(ov, dataset, "SourceKind", "DatasetSourceEnum",
lj.get("sourceKind", "Synthetic"))
- job = ov.obj(lj["name"], vtype("LearningJobType"), ov.roots["jobs"],
+ job = ov.obj(lj["name"], aitype("LearningJobType"), ov.roots["jobs"],
reftype=Organizes, desc=lj.get("description"))
put(ov, job, "JobId", "String", lj["jobId"])
- put_enum(ov, job, "State", "VisionLearningJobStateEnum",
+ put_enum_ai(ov, job, "State", "LearningJobStateEnum",
lj.get("state", "Collecting"))
return ov
@@ -905,9 +962,9 @@ def head(title):
A(d["inferenceNote"])
A("")
A("The deployment carries exactly one `UsesModel` reference to the model above, as "
- "base specification §5.11 requires. That reference is the only defined path from a "
- "result to the model artefact and its `Digest`, so it is what makes the §12.6 "
- "provenance check possible.")
+ "*OPC UA — AI Model Management and Inference* requires. That reference is the only defined "
+ "path from a result to the model artefact and its `Digest`, so it is what makes the "
+ "base specification's §12.6 provenance check possible.")
A("")
head("Results")
A("")
diff --git a/metaverse-specs/extras/vision/tools/build_model.py b/metaverse-specs/extras/vision/tools/build_model.py
index 1823d14..b573d4c 100644
--- a/metaverse-specs/extras/vision/tools/build_model.py
+++ b/metaverse-specs/extras/vision/tools/build_model.py
@@ -41,7 +41,7 @@
NAMESPACE = "http://opcfoundation.org/UA/Vision/"
VERSION = "0.1.0"
-PUBDATE = "2026-07-26T00:00:00Z"
+PUBDATE = "2026-08-02T00:00:00Z"
BASE_UA_VERSION = "1.05.04"
BASE_UA_PUBDATE = "2023-12-15T00:00:00Z"
@@ -144,6 +144,31 @@ def _mid():
return v
+def _reserve_encoding_through(last):
+ """Same append-only rule as _reserve_through, for the encoding id counter.
+
+ Encoding ids are auto-assigned inside struct_type(), so deleting a structure
+ silently frees its id for the next structure added - a renumbering that is harder
+ to notice than a member one because no CSV row names the encoding directly.
+ """
+ assert _next_encoding[0] <= last + 1, (
+ f"encoding reservation {last} already passed at {_next_encoding[0]}")
+ _next_encoding[0] = last + 1
+
+
+def _reserve_through(last):
+ """Burn member ids up to and including `last`, so no surviving member moves.
+
+ The AI cluster was factored out into OPC UA - AI Model Management and Inference. Its
+ member ids are NOT reclaimed: reusing them would renumber every member declared
+ after them, which is exactly the churn the append-only rule exists to prevent.
+ A hole in the id space costs nothing and keeps the CSV diff at zero moved.
+ """
+ assert _next_member[0] <= last + 1, (
+ f"reservation {last} already passed at {_next_member[0]}")
+ _next_member[0] = last + 1
+
+
def T(nid):
"""Own-namespace NodeId (ns=1)."""
return f"ns=1;i={nid}"
@@ -337,7 +362,8 @@ def struct_type(nid, name, desc, fields):
_next_encoding[0] += 1
ref(nid, HasEncoding, T(enc))
add(enc, "UAObject", "Default Binary", f"{name}_Encoding_DefaultBinary",
- desc="Default Binary encoding of the structure.")
+ desc="Default Binary encoding of the structure.",
+ attrs={"BrowseNameNamespace": 0, "SymbolicName": "DefaultBinary"})
ref(enc, HasTypeDefinition, DataTypeEncodingType)
ref(enc, HasEncoding, T(nid), forward=False)
return nid
@@ -423,20 +449,6 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
"GetStreamEndpoint or GetClip."),
("MutualTls", 4, "Client certificate.")])
-enum_type(3007, "VisionInferenceLocationEnum",
- "Where inference executes. The result contract is identical in every case; this "
- "property exists so a client can reason about latency, availability and trust "
- "boundary without changing how it reads results.",
- [("OnServer", 0, "In the OPC UA Server process or on its host."),
- ("EdgeOffServer", 1, "On a separate edge node reached over the network."),
- ("Cloud", 2, "In a remote or cloud service."),
- ("InSimulator", 3, "Inside the simulator that also renders the sensor.")])
-
-enum_type(3008, "VisionAcceleratorKindEnum",
- "Compute device executing the model.",
- [("Cpu", 0, None), ("Gpu", 1, None), ("Npu", 2, None), ("Fpga", 3, None),
- ("Tpu", 4, None), ("Other", 5, None)])
-
enum_type(3009, "VisionResultEvaluationEnum",
"Overall verdict of a result. Value semantics are aligned with the "
"ResultEvaluationEnum of OPC 40001-101 so that a client already consuming "
@@ -468,10 +480,19 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
("Unknown", 3, None)])
enum_type(3013, "VisionFrameRoleEnum",
- "Role of a coordinate frame, following the ISO 9787 frame vocabulary.",
+ "Role of a coordinate frame, following the ISO 9787 frame vocabulary. The "
+ "mechanical interface and the tool are DISTINCT roles: a camera on a robot "
+ "flange is calibrated to the mechanical interface, while a pick pose has to "
+ "reach the tool centre point, and a model that cannot tell them apart cannot "
+ "express the offset between them.",
[("World", 0, None), ("Base", 1, None),
- ("Tool", 2, "Tool / tool centre point (TCP) frame."),
- ("Camera", 3, None), ("Object", 4, None), ("Other", 5, None)])
+ ("MechanicalInterface", 2,
+ "The flange at the end of the last link, to which an end effector is "
+ "fitted. This is what an eye-in-hand extrinsic calibration resolves to."),
+ ("Tool", 3, "A tool frame, whose origin is a tool centre point."),
+ ("Object", 4, None), ("Other", 5, None),
+ ("Camera", 6, "A camera frame. Numbered after the ISO 9787 roles because "
+ "it is not one of them.")])
enum_type(3014, "VisionDistortionModelEnum",
"Lens distortion model the coefficients belong to.",
@@ -488,27 +509,12 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
("Thermal", 3, None), ("Multispectral", 4, None),
("Event", 5, "Event / neuromorphic camera."), ("Other", 6, None)])
-enum_type(3016, "VisionLearningJobStateEnum",
- "State of a dataset-capture, retraining and promotion cycle.",
- [("Idle", 0, None), ("Collecting", 1, None), ("Labelling", 2, None),
- ("Training", 3, None), ("Validating", 4, None),
- ("Ready", 5, "A candidate model is available for promotion."),
- ("Promoted", 6, None), ("Failed", 7, None)])
-
-enum_type(3017, "VisionDatasetSourceEnum",
- "Provenance of the samples in a dataset.",
- [("Real", 0, "Captured from physical sensors."),
- ("Synthetic", 1, "Rendered by a simulator."),
- ("Mixed", 2, "Both, e.g. synthetic pre-training with real fine-tuning.")])
-
VisionRealityKindEnum = T(3001)
VisionStreamProtocolEnum = T(3002)
VisionClipFormatEnum = T(3003)
VisionVideoCodecEnum = T(3004)
VisionEndpointStateEnum = T(3005)
VisionEndpointAuthenticationEnum = T(3006)
-VisionInferenceLocationEnum = T(3007)
-VisionAcceleratorKindEnum = T(3008)
VisionResultEvaluationEnum = T(3009)
VisionToleranceStatusEnum = T(3010)
VisionFeedbackPurposeEnum = T(3011)
@@ -516,8 +522,6 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
VisionFrameRoleEnum = T(3013)
VisionDistortionModelEnum = T(3014)
VisionSensorModalityEnum = T(3015)
-VisionLearningJobStateEnum = T(3016)
-VisionDatasetSourceEnum = T(3017)
# ---------------------------------------------------------------------------
# Structured DataTypes (3050+)
@@ -628,13 +632,10 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
("ExpiresAt", UtcTime, "Expiry after which the Uri is no longer valid.")])
VisionStreamSessionDataType = T(3057)
-struct_type(3058, "VisionTensorSignatureDataType",
- "Shape and element type of one model input or output tensor.",
- [("Name", String, "Tensor name as declared by the model."),
- ("ElementType", String, "Element type, for example float32, uint8 or int64."),
- ("Shape", Int32, "Dimensions; -1 marks a dynamic axis.", 1),
- ("Layout", String, "Optional axis layout hint, for example NCHW or NHWC.")])
-VisionTensorSignatureDataType = T(3058)
+# Retired: encoding 5009 belonged to VisionTensorSignatureDataType, which moved to
+# OPC UA - AI Model Management and Inference. Not reclaimed, for the same reason member
+# ids are not.
+_reserve_encoding_through(5009)
# ---------------------------------------------------------------------------
# ReferenceTypes (4001+)
@@ -649,11 +650,6 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
"Server also implements OPC UA - OpenUSD Scene Materialization. The target "
"is expected to be a UsdGeomCameraType instance. Optional: PrimPath remains "
"the portable descriptor.")
-reference_type(4004, "UsesModel", "IsUsedByDeployment",
- "Links an AiDeploymentType instance to the AiModelType instance it "
- "executes. Clause 5.11 requires exactly one such reference per deployment; "
- "it is the only defined path from a result to the model artefact and its "
- "Digest, on which clause 12.6 depends.")
reference_type(4005, "ProducedBy", "Produces",
"Links a result to the inference pipeline that produced it.")
@@ -1028,92 +1024,6 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
"Seed of the active domain-randomization run, so a dataset can be reproduced.")
# ---- AI: model, dataset, deployment ----------------------------------------
-object_type(1015, "AiModelType", BaseObjectType,
- "Nameplate of a trained model. The member set is deliberately aligned with "
- "the IDTA 02060 AI Model Nameplate submodel template, which is currently the "
- "only standardised description of an industrial AI model, so an Asset "
- "Administration Shell can be populated from this node without loss.")
-AM = 1015
-prop_var(AM, "AiModelType", "ModelId", String, "Identifier of the model.", MR_Mandatory)
-prop_var(AM, "AiModelType", "Name", LocalizedText, "Human-readable model name.",
- MR_Mandatory)
-prop_var(AM, "AiModelType", "Version", String, "Model version.", MR_Mandatory)
-prop_var(AM, "AiModelType", "Framework", String,
- "Producing framework, for example PyTorch, TensorFlow or scikit-learn.")
-prop_var(AM, "AiModelType", "Format", String,
- "Serialization format, for example ONNX, TensorRT or OpenVINO IR.")
-prop_var(AM, "AiModelType", "TaskKind", String,
- "What the model does, for example Detection2D, Detection3D, Classification, "
- "Segmentation, PoseEstimation or AnomalyDetection.")
-prop_var(AM, "AiModelType", "Digest", ByteString,
- "Cryptographic digest of the model artefact, for provenance and integrity. "
- "Mandatory: clause 12.6 requires it for every model whose artefact is "
- "obtainable through ArtifactUri, and it is the terminus of the provenance "
- "chain that UsesModel keeps intact.",
- MR_Mandatory)
-prop_var(AM, "AiModelType", "DigestAlgorithm", String,
- "Hash function used for Digest. SHALL name a function with at least 256-bit "
- "output and no known collision weakness; SHA-256 is the default and is always "
- "acceptable. SHALL NOT be MD5, SHA-1 or a truncated variant - chosen-prefix "
- "collisions against those are practical, so a substituted artefact would pass "
- "verification. SHALL be non-empty where Digest is non-empty. See clause 12.6.",
- MR_Mandatory)
-prop_var(AM, "AiModelType", "ArtifactUri", String,
- "Where the model artefact can be obtained. Treated as untrusted input.")
-prop_var(AM, "AiModelType", "ProvenanceUri", String,
- "Training provenance or model card location.")
-prop_var(AM, "AiModelType", "LabelClasses", String,
- "Ordered class label set; the index corresponds to "
- "VisionDetectionDataType.ClassId.", MR_Optional, valuerank="1")
-data_var(AM, "AiModelType", "Inputs", VisionTensorSignatureDataType,
- "Input tensor signatures.", MR_Optional, valuerank="1")
-data_var(AM, "AiModelType", "Outputs", VisionTensorSignatureDataType,
- "Output tensor signatures.", MR_Optional, valuerank="1")
-
-object_type(1016, "AiDatasetType", BaseObjectType,
- "A dataset used to train or validate a model. Aligned with the IDTA 02058 AI "
- "Dataset submodel template. SourceKind distinguishes real capture from "
- "simulator output, which is the provenance a reviewer needs when synthetic "
- "data is involved.")
-AD = 1016
-prop_var(AD, "AiDatasetType", "DatasetId", String, "Identifier of the dataset.",
- MR_Mandatory)
-prop_var(AD, "AiDatasetType", "Name", LocalizedText, "Human-readable dataset name.")
-prop_var(AD, "AiDatasetType", "Version", String, "Dataset version.")
-prop_var(AD, "AiDatasetType", "SourceKind", VisionDatasetSourceEnum,
- "Whether samples are real, synthetic or mixed.", MR_Mandatory)
-prop_var(AD, "AiDatasetType", "SampleCount", UInt64, "Number of samples.")
-prop_var(AD, "AiDatasetType", "LabelClasses", String, "Class labels present.",
- MR_Optional, valuerank="1")
-prop_var(AD, "AiDatasetType", "CreatedAt", UtcTime, "Creation time.")
-prop_var(AD, "AiDatasetType", "ArtifactUri", String,
- "Where the dataset can be obtained.")
-prop_var(AD, "AiDatasetType", "Digest", ByteString, "Digest of the dataset artefact.")
-
-object_type(1017, "AiDeploymentType", BaseObjectType,
- "A model made executable somewhere. Aligned with the IDTA 02059 AI Deployment "
- "submodel template. InferenceLocation is the on-server versus off-server "
- "switch: it changes where the computation happens and therefore the trust "
- "boundary, but it does NOT change the result contract.")
-AY = 1017
-prop_var(AY, "AiDeploymentType", "DeploymentId", String,
- "Identifier of the deployment.", MR_Mandatory)
-prop_var(AY, "AiDeploymentType", "InferenceLocation", VisionInferenceLocationEnum,
- "Where inference executes.", MR_Mandatory)
-prop_var(AY, "AiDeploymentType", "AcceleratorKind", VisionAcceleratorKindEnum,
- "Compute device executing the model.")
-prop_var(AY, "AiDeploymentType", "AcceleratorName", String,
- "Free-text accelerator identification, for example an NPU or GPU part name.")
-prop_var(AY, "AiDeploymentType", "EndpointUri", String,
- "Inference endpoint when InferenceLocation is not OnServer. Treated as "
- "untrusted input and subject to the resolver policy of the security clause.")
-prop_var(AY, "AiDeploymentType", "LatencyBudget", Duration,
- "Latency the deployment is expected to meet, so a client can detect regression.")
-prop_var(AY, "AiDeploymentType", "BatchSize", UInt32,
- "Configured inference batch size.")
-prop_var(AY, "AiDeploymentType", "State", VisionEndpointStateEnum,
- "Runtime state of the deployment.")
-
# ---- Results ---------------------------------------------------------------
object_type(1020, "VisionResultType", BaseObjectType,
"Abstract base for a vision result. Unlike OPC 40100-1, whose ResultContent "
@@ -1123,6 +1033,10 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
"explanation lives.",
abstract=True)
VR = 1020
+# Retired: 6107..6136 held AiModelType, AiDatasetType and AiDeploymentType before they
+# moved to OPC UA - AI Model Management and Inference. See _reserve_through.
+_reserve_through(6136)
+
prop_var(VR, "VisionResultType", "ResultId", String,
"Identifier of the result, unique within the Server.", MR_Mandatory)
prop_var(VR, "VisionResultType", "CreationTime", UtcTime,
@@ -1240,14 +1154,21 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
"Binds a sensor to a deployment and publishes the results. The same type "
"serves on-server and off-server inference: when the deployment is remote "
"the Server publishes results it did not compute, and the only observable "
- "difference is AiDeployment.InferenceLocation.")
+ "difference is DeploymentType.InferenceLocation.")
IP = 1018
prop_var(IP, "InferencePipelineType", "PipelineId", String,
"Identifier of the pipeline.", MR_Mandatory)
prop_var(IP, "InferencePipelineType", "Sensor", NodeId_,
"Sensor supplying frames.", MR_Mandatory)
prop_var(IP, "InferencePipelineType", "Deployment", NodeId_,
- "Deployment executing inference.", MR_Mandatory)
+ "The deployment executing inference. This is a NodeId, not a reference, and "
+ "the node it names is NOT defined by this specification - see clause 8.2. "
+ "Where the Server also implements OPC UA - AI Model Management and Inference it names "
+ "a DeploymentType instance there, which is what clause 8's provenance argument "
+ "assumes; a Server that describes its deployment some other way names that "
+ "node instead. Nothing in this NodeSet references the other model's "
+ "identifiers, so adopting or ignoring it changes nothing about loading this "
+ "one.", MR_Mandatory)
prop_var(IP, "InferencePipelineType", "State", VisionEndpointStateEnum,
"Runtime state of the pipeline.", MR_Mandatory)
prop_var(IP, "InferencePipelineType", "Continuous", Boolean,
@@ -1269,56 +1190,23 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
"Stop continuous inference.", MR_Optional)
# ---- Learning --------------------------------------------------------------
-object_type(1019, "LearningJobType", BaseObjectType,
- "One turn of the capture, label, train and promote loop. It exists so that "
- "corrections arriving through VisionFeedbackType have somewhere to accumulate "
- "and a defined path into a new model version. A Server may implement only the "
- "capture stages and leave training to an external MLOps system - the state "
- "machine is the same either way.")
-LJ = 1019
-prop_var(LJ, "LearningJobType", "JobId", String, "Identifier of the job.", MR_Mandatory)
-prop_var(LJ, "LearningJobType", "State", VisionLearningJobStateEnum,
- "Current stage of the loop.", MR_Mandatory)
-prop_var(LJ, "LearningJobType", "Dataset", NodeId_,
- "Dataset being accumulated or used.")
-prop_var(LJ, "LearningJobType", "BaseModel", NodeId_, "Model the job starts from.")
-prop_var(LJ, "LearningJobType", "CandidateModel", NodeId_,
- "Model produced by the job, awaiting promotion.")
-prop_var(LJ, "LearningJobType", "SamplesCollected", UInt64,
- "Samples accumulated so far, including corrections fed back.")
-prop_var(LJ, "LearningJobType", "LastError", LocalizedText,
- "Diagnostic for the Failed state.")
-method(LJ, "LearningJobType", "StartCollection",
- "Begin accumulating samples and corrections into the dataset.", MR_Optional)
-method(LJ, "LearningJobType", "StopCollection",
- "Stop accumulating samples.", MR_Optional)
-method(LJ, "LearningJobType", "TriggerTraining",
- "Request that a candidate model be trained from the collected dataset.",
- MR_Optional,
- outargs=[("Accepted", Boolean, "True when the request was queued.")])
-method(LJ, "LearningJobType", "PromoteModel",
- "Promote the candidate model so that deployments begin using it. A Server SHOULD "
- "require a distinct authorization for this Method.",
- MR_Optional,
- inargs=[("Deployment", NodeId_, "Deployment to update, or null for all.")],
- outargs=[("PromotedModel", NodeId_, "The model now in use.")])
-
# ---- Root ------------------------------------------------------------------
object_type(1001, "VisionRootType", BaseObjectType,
"The single well-known entry point for everything in this model. A client "
"starts here, enumerates Sensors, and follows references outward. Mirrors "
"the discovery pattern of OPC UA - OpenUSD Bindings.")
VRT = 1001
+# Retired: 6177..6190 held LearningJobType.
+_reserve_through(6190)
+
folder_member(VRT, "VisionRootType", "Sensors",
"VisionSensorType instances known to this Server.", MR_Mandatory)
folder_member(VRT, "VisionRootType", "Pipelines",
"InferencePipelineType instances.", MR_Optional)
-folder_member(VRT, "VisionRootType", "Models",
- "AiModelType, AiDatasetType and AiDeploymentType instances.", MR_Optional)
+# Retired: 6193 held VisionRootType.Models.
+_reserve_through(6193)
folder_member(VRT, "VisionRootType", "Frames",
"CoordinateFrameType instances.", MR_Optional)
-folder_member(VRT, "VisionRootType", "LearningJobs",
- "LearningJobType instances.", MR_Optional)
# ---- Well-known instance ----------------------------------------------------
well_known(7001, "Vision", T(VRT), Server,
@@ -1338,6 +1226,8 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
# proposal, and this model deliberately takes NO dependency on it: nothing here
# references its provisional NodeIds, so the Vision NodeSet loads unchanged on a Server
# that has never heard of it.
+# Retired: 6195 held VisionRootType.LearningJobs.
+_reserve_through(6195)
prop_var(ME, "MediaEndpointType", "DataChannelSource", NodeId_,
"NodeId of the Object through which this endpoint's bytes can also be obtained "
"on an OPC UA data channel, per the OPC UA - Data Channels errata proposal. "
@@ -1353,6 +1243,18 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent):
"the payload type from this model alone, without the Data Channels model being "
"present. Meaningful only where DataChannelSource is non-null.")
+# Appended in the AI split. It belongs to InferencePipelineType, but its id is
+# allocated HERE because member ids are append-only: declaring it beside its type
+# renumbered InferencePipelineType.Stop.
+prop_var(IP, "InferencePipelineType", "LearningJob", NodeId_,
+ "LearningJobType instance that consumes GroundTruthLabel corrections "
+ "submitted through this pipeline's Feedback object, or null where the Server "
+ "retains none. A NodeId and not a reference, for the same reason Deployment "
+ "is: this model takes no dependency on the model that defines the job. "
+ "Section 9.5.1 requires this to be non-null wherever such a correction is "
+ "retained - without it a client cannot establish whether its label reached a "
+ "learning loop at all.")
+
# ===========================================================================
# ================================== EMIT =================================
@@ -1370,7 +1272,14 @@ def _fmt_reftype(t):
def _emit_node(n):
tag = n.cls
- a = [f'{tag} NodeId="{T(n.nid)}"', f'BrowseName="1:{sx.escape(n.bname)}"']
+ # A DataTypeEncoding browses as "Default Binary" in namespace 0: the BrowseName is
+ # standard, not model-defined. Emitting it as 1:Default Binary is what every real
+ # companion NodeSet avoids, and tooling that resolves encodings by BrowseName - the
+ # UA-.NETStandard model source generator among it - cannot find it.
+ prefix = "" if n.attrs.get("BrowseNameNamespace") == 0 else "1:"
+ a = [f'{tag} NodeId="{T(n.nid)}"', f'BrowseName="{prefix}{sx.escape(n.bname)}"']
+ if "SymbolicName" in n.attrs:
+ a.append(f'SymbolicName="{sx.escape(n.attrs["SymbolicName"])}"')
if n.parent is not None:
a.append(f'ParentNodeId="{n.parent}"')
for k in ("DataType", "ValueRank", "ArrayDimensions"):
diff --git a/metaverse-specs/extras/vision/tools/model-reference.md b/metaverse-specs/extras/vision/tools/model-reference.md
index b802951..3b76df1 100644
--- a/metaverse-specs/extras/vision/tools/model-reference.md
+++ b/metaverse-specs/extras/vision/tools/model-reference.md
@@ -11,7 +11,6 @@ This annex is the authoritative node reference for the specification: it carries
| ns=1;i=4001 | HasCalibration | ReferenceType | NonHierarchicalReferences |
| ns=1;i=4002 | MountedOn | ReferenceType | NonHierarchicalReferences |
| ns=1;i=4003 | HasScenePrim | ReferenceType | NonHierarchicalReferences |
-| ns=1;i=4004 | UsesModel | ReferenceType | NonHierarchicalReferences |
| ns=1;i=4005 | ProducedBy | ReferenceType | NonHierarchicalReferences |
| ns=1;i=1005 | OpticsType | ObjectType | BaseObjectType |
| ns=1;i=1006 | IlluminationType | ObjectType | BaseObjectType |
@@ -27,16 +26,12 @@ This annex is the authoritative node reference for the specification: it carries
| ns=1;i=1003 | ImageSensorType | ObjectType | VisionSensorType |
| ns=1;i=1004 | Depth3DSensorType | ObjectType | VisionSensorType |
| ns=1;i=1030 | IVisionSimulatedType | ObjectType | BaseInterfaceType |
-| ns=1;i=1015 | AiModelType | ObjectType | BaseObjectType |
-| ns=1;i=1016 | AiDatasetType | ObjectType | BaseObjectType |
-| ns=1;i=1017 | AiDeploymentType | ObjectType | BaseObjectType |
| ns=1;i=1020 | VisionResultType | ObjectType | BaseObjectType |
| ns=1;i=1021 | InspectionResultType | ObjectType | VisionResultType |
| ns=1;i=1022 | DetectionResultType | ObjectType | VisionResultType |
| ns=1;i=1023 | SegmentationResultType | ObjectType | VisionResultType |
| ns=1;i=1024 | VisionFeedbackType | ObjectType | BaseObjectType |
| ns=1;i=1018 | InferencePipelineType | ObjectType | BaseObjectType |
-| ns=1;i=1019 | LearningJobType | ObjectType | BaseObjectType |
| ns=1;i=1001 | VisionRootType | ObjectType | BaseObjectType |
| ns=1;i=3001 | VisionRealityKindEnum | DataType | Enumeration |
| ns=1;i=3002 | VisionStreamProtocolEnum | DataType | Enumeration |
@@ -44,8 +39,6 @@ This annex is the authoritative node reference for the specification: it carries
| ns=1;i=3004 | VisionVideoCodecEnum | DataType | Enumeration |
| ns=1;i=3005 | VisionEndpointStateEnum | DataType | Enumeration |
| ns=1;i=3006 | VisionEndpointAuthenticationEnum | DataType | Enumeration |
-| ns=1;i=3007 | VisionInferenceLocationEnum | DataType | Enumeration |
-| ns=1;i=3008 | VisionAcceleratorKindEnum | DataType | Enumeration |
| ns=1;i=3009 | VisionResultEvaluationEnum | DataType | Enumeration |
| ns=1;i=3010 | VisionToleranceStatusEnum | DataType | Enumeration |
| ns=1;i=3011 | VisionFeedbackPurposeEnum | DataType | Enumeration |
@@ -53,8 +46,6 @@ This annex is the authoritative node reference for the specification: it carries
| ns=1;i=3013 | VisionFrameRoleEnum | DataType | Enumeration |
| ns=1;i=3014 | VisionDistortionModelEnum | DataType | Enumeration |
| ns=1;i=3015 | VisionSensorModalityEnum | DataType | Enumeration |
-| ns=1;i=3016 | VisionLearningJobStateEnum | DataType | Enumeration |
-| ns=1;i=3017 | VisionDatasetSourceEnum | DataType | Enumeration |
| ns=1;i=3050 | VisionPose3DDataType | DataType | Structure |
| ns=1;i=3051 | VisionBoundingBox2DDataType | DataType | Structure |
| ns=1;i=3052 | VisionBoundingBox3DDataType | DataType | Structure |
@@ -63,7 +54,6 @@ This annex is the authoritative node reference for the specification: it carries
| ns=1;i=3055 | VisionDetectionDataType | DataType | Structure |
| ns=1;i=3056 | VisionCharacteristicDataType | DataType | Structure |
| ns=1;i=3057 | VisionStreamSessionDataType | DataType | Structure |
-| ns=1;i=3058 | VisionTensorSignatureDataType | DataType | Structure |
## A.2 ReferenceTypes
@@ -72,7 +62,6 @@ This annex is the authoritative node reference for the specification: it carries
| ns=1;i=4001 | HasCalibration | IsCalibrationOf | NonHierarchicalReferences | Links a sensor to a calibration currently valid for it. |
| ns=1;i=4002 | MountedOn | HasMounted | NonHierarchicalReferences | Links a sensor to the CoordinateFrame it is rigidly mounted on, for example a robot flange frame for an eye-in-hand camera. |
| ns=1;i=4003 | HasScenePrim | IsScenePrimOf | NonHierarchicalReferences | Links a sensor to the materialized USD prim representing it, when the Server also implements OPC UA - OpenUSD Scene Materialization. The target is expected to be a UsdGeomCameraType instance. Optional: PrimPath remains the portable descriptor. |
-| ns=1;i=4004 | UsesModel | IsUsedByDeployment | NonHierarchicalReferences | Links an AiDeploymentType instance to the AiModelType instance it executes. Clause 5.11 requires exactly one such reference per deployment; it is the only defined path from a result to the model artefact and its Digest, on which clause 12.6 depends. |
| ns=1;i=4005 | ProducedBy | Produces | NonHierarchicalReferences | Links a result to the inference pipeline that produced it. |
## A.3 ObjectTypes
@@ -350,63 +339,6 @@ Applied to a sensor whose RealityKind is Simulated or Hybrid. It names the simul
| GroundTruthAvailable | Variable | Boolean | Scalar | Optional | True when the simulator can emit annotator ground truth alongside imagery. |
| RandomizationSeed | Variable | UInt64 | Scalar | Optional | Seed of the active domain-randomization run, so a dataset can be reproduced. |
-### AiModelType — `ns=1;i=1015`
-
-*Subtype of:* `BaseObjectType`
-
-Nameplate of a trained model. The member set is deliberately aligned with the IDTA 02060 AI Model Nameplate submodel template, which is currently the only standardised description of an industrial AI model, so an Asset Administration Shell can be populated from this node without loss.
-
-| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
-|---|---|---|---|---|---|
-| ModelId | Variable | String | Scalar | Mandatory | Identifier of the model. |
-| Name | Variable | LocalizedText | Scalar | Mandatory | Human-readable model name. |
-| Version | Variable | String | Scalar | Mandatory | Model version. |
-| Framework | Variable | String | Scalar | Optional | Producing framework, for example PyTorch, TensorFlow or scikit-learn. |
-| Format | Variable | String | Scalar | Optional | Serialization format, for example ONNX, TensorRT or OpenVINO IR. |
-| TaskKind | Variable | String | Scalar | Optional | What the model does, for example Detection2D, Detection3D, Classification, Segmentation, PoseEstimation or AnomalyDetection. |
-| Digest | Variable | ByteString | Scalar | Mandatory | Cryptographic digest of the model artefact, for provenance and integrity. Mandatory: clause 12.6 requires it for every model whose artefact is obtainable through ArtifactUri, and it is the terminus of the provenance chain that UsesModel keeps intact. |
-| DigestAlgorithm | Variable | String | Scalar | Mandatory | Hash function used for Digest. SHALL name a function with at least 256-bit output and no known collision weakness; SHA-256 is the default and is always acceptable. SHALL NOT be MD5, SHA-1 or a truncated variant - chosen-prefix collisions against those are practical, so a substituted artefact would pass verification. SHALL be non-empty where Digest is non-empty. See clause 12.6. |
-| ArtifactUri | Variable | String | Scalar | Optional | Where the model artefact can be obtained. Treated as untrusted input. |
-| ProvenanceUri | Variable | String | Scalar | Optional | Training provenance or model card location. |
-| LabelClasses | Variable | String | Array | Optional | Ordered class label set; the index corresponds to VisionDetectionDataType.ClassId. |
-| Inputs | Variable | VisionTensorSignatureDataType | Array | Optional | Input tensor signatures. |
-| Outputs | Variable | VisionTensorSignatureDataType | Array | Optional | Output tensor signatures. |
-
-### AiDatasetType — `ns=1;i=1016`
-
-*Subtype of:* `BaseObjectType`
-
-A dataset used to train or validate a model. Aligned with the IDTA 02058 AI Dataset submodel template. SourceKind distinguishes real capture from simulator output, which is the provenance a reviewer needs when synthetic data is involved.
-
-| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
-|---|---|---|---|---|---|
-| DatasetId | Variable | String | Scalar | Mandatory | Identifier of the dataset. |
-| Name | Variable | LocalizedText | Scalar | Optional | Human-readable dataset name. |
-| Version | Variable | String | Scalar | Optional | Dataset version. |
-| SourceKind | Variable | VisionDatasetSourceEnum | Scalar | Mandatory | Whether samples are real, synthetic or mixed. |
-| SampleCount | Variable | UInt64 | Scalar | Optional | Number of samples. |
-| LabelClasses | Variable | String | Array | Optional | Class labels present. |
-| CreatedAt | Variable | UtcTime | Scalar | Optional | Creation time. |
-| ArtifactUri | Variable | String | Scalar | Optional | Where the dataset can be obtained. |
-| Digest | Variable | ByteString | Scalar | Optional | Digest of the dataset artefact. |
-
-### AiDeploymentType — `ns=1;i=1017`
-
-*Subtype of:* `BaseObjectType`
-
-A model made executable somewhere. Aligned with the IDTA 02059 AI Deployment submodel template. InferenceLocation is the on-server versus off-server switch: it changes where the computation happens and therefore the trust boundary, but it does NOT change the result contract.
-
-| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
-|---|---|---|---|---|---|
-| DeploymentId | Variable | String | Scalar | Mandatory | Identifier of the deployment. |
-| InferenceLocation | Variable | VisionInferenceLocationEnum | Scalar | Mandatory | Where inference executes. |
-| AcceleratorKind | Variable | VisionAcceleratorKindEnum | Scalar | Optional | Compute device executing the model. |
-| AcceleratorName | Variable | String | Scalar | Optional | Free-text accelerator identification, for example an NPU or GPU part name. |
-| EndpointUri | Variable | String | Scalar | Optional | Inference endpoint when InferenceLocation is not OnServer. Treated as untrusted input and subject to the resolver policy of the security clause. |
-| LatencyBudget | Variable | Duration | Scalar | Optional | Latency the deployment is expected to meet, so a client can detect regression. |
-| BatchSize | Variable | UInt32 | Scalar | Optional | Configured inference batch size. |
-| State | Variable | VisionEndpointStateEnum | Scalar | Optional | Runtime state of the deployment. |
-
### VisionResultType (abstract) — `ns=1;i=1020`
*Subtype of:* `BaseObjectType`
@@ -512,17 +444,18 @@ The return path into the vision system. It serves three purposes at once: drawin
*Subtype of:* `BaseObjectType`
-Binds a sensor to a deployment and publishes the results. The same type serves on-server and off-server inference: when the deployment is remote the Server publishes results it did not compute, and the only observable difference is AiDeployment.InferenceLocation.
+Binds a sensor to a deployment and publishes the results. The same type serves on-server and off-server inference: when the deployment is remote the Server publishes results it did not compute, and the only observable difference is DeploymentType.InferenceLocation.
| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
|---|---|---|---|---|---|
| PipelineId | Variable | String | Scalar | Mandatory | Identifier of the pipeline. |
| Sensor | Variable | NodeId | Scalar | Mandatory | Sensor supplying frames. |
-| Deployment | Variable | NodeId | Scalar | Mandatory | Deployment executing inference. |
+| Deployment | Variable | NodeId | Scalar | Mandatory | The deployment executing inference. This is a NodeId, not a reference, and the node it names is NOT defined by this specification - see clause 8.2. Where the Server also implements OPC UA - AI Model Management and Inference it names a DeploymentType instance there, which is what clause 8's provenance argument assumes; a Server that describes its deployment some other way names that node instead. Nothing in this NodeSet references the other model's identifiers, so adopting or ignoring it changes nothing about loading this one. |
| State | Variable | VisionEndpointStateEnum | Scalar | Mandatory | Runtime state of the pipeline. |
| Continuous | Variable | Boolean | Scalar | Optional | True while the pipeline runs on every frame. |
| Results | Object | | | Optional | Recent VisionResultType instances produced by this pipeline. |
| Feedback | Object | | | Optional | Feedback surface for pushing results back into the vision system. |
+| LearningJob | Variable | NodeId | Scalar | Optional | LearningJobType instance that consumes GroundTruthLabel corrections submitted through this pipeline's Feedback object, or null where the Server retains none. A NodeId and not a reference, for the same reason Deployment is: this model takes no dependency on the model that defines the job. Section 9.5.1 requires this to be non-null wherever such a correction is retained - without it a client cannot establish whether its label reached a learning loop at all. |
**Method `RunInference`** (Optional) — Run inference once, on the current or a specified frame, and return the identifier of the result that was produced.
@@ -542,46 +475,6 @@ Takes no arguments and returns none.
Takes no arguments and returns none.
-### LearningJobType — `ns=1;i=1019`
-
-*Subtype of:* `BaseObjectType`
-
-One turn of the capture, label, train and promote loop. It exists so that corrections arriving through VisionFeedbackType have somewhere to accumulate and a defined path into a new model version. A Server may implement only the capture stages and leave training to an external MLOps system - the state machine is the same either way.
-
-| BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description |
-|---|---|---|---|---|---|
-| JobId | Variable | String | Scalar | Mandatory | Identifier of the job. |
-| State | Variable | VisionLearningJobStateEnum | Scalar | Mandatory | Current stage of the loop. |
-| Dataset | Variable | NodeId | Scalar | Optional | Dataset being accumulated or used. |
-| BaseModel | Variable | NodeId | Scalar | Optional | Model the job starts from. |
-| CandidateModel | Variable | NodeId | Scalar | Optional | Model produced by the job, awaiting promotion. |
-| SamplesCollected | Variable | UInt64 | Scalar | Optional | Samples accumulated so far, including corrections fed back. |
-| LastError | Variable | LocalizedText | Scalar | Optional | Diagnostic for the Failed state. |
-
-**Method `StartCollection`** (Optional) — Begin accumulating samples and corrections into the dataset.
-
-Takes no arguments and returns none.
-
-**Method `StopCollection`** (Optional) — Stop accumulating samples.
-
-Takes no arguments and returns none.
-
-**Method `TriggerTraining`** (Optional) — Request that a candidate model be trained from the collected dataset.
-
-| Out | DataType | ValueRank | Meaning |
-|---|---|---|---|
-| Accepted | Boolean | Scalar | True when the request was queued. |
-
-**Method `PromoteModel`** (Optional) — Promote the candidate model so that deployments begin using it. A Server SHOULD require a distinct authorization for this Method.
-
-| In | DataType | ValueRank | Meaning |
-|---|---|---|---|
-| Deployment | NodeId | Scalar | Deployment to update, or null for all. |
-
-| Out | DataType | ValueRank | Meaning |
-|---|---|---|---|
-| PromotedModel | NodeId | Scalar | The model now in use. |
-
### VisionRootType — `ns=1;i=1001`
*Subtype of:* `BaseObjectType`
@@ -592,9 +485,7 @@ The single well-known entry point for everything in this model. A client starts
|---|---|---|---|---|---|
| Sensors | Object | | | Mandatory | VisionSensorType instances known to this Server. |
| Pipelines | Object | | | Optional | InferencePipelineType instances. |
-| Models | Object | | | Optional | AiModelType, AiDatasetType and AiDeploymentType instances. |
| Frames | Object | | | Optional | CoordinateFrameType instances. |
-| LearningJobs | Object | | | Optional | LearningJobType instances. |
## A.4 DataTypes
@@ -687,34 +578,6 @@ Authentication a client must present to the media endpoint. This is the media-pl
| Token | 3 | Bearer token, typically the time-limited token returned by GetStreamEndpoint or GetClip. |
| MutualTls | 4 | Client certificate. |
-### VisionInferenceLocationEnum — `ns=1;i=3007`
-
-*Subtype of:* `Enumeration`
-
-Where inference executes. The result contract is identical in every case; this property exists so a client can reason about latency, availability and trust boundary without changing how it reads results.
-
-| Name | Value | Description |
-|---|---|---|
-| OnServer | 0 | In the OPC UA Server process or on its host. |
-| EdgeOffServer | 1 | On a separate edge node reached over the network. |
-| Cloud | 2 | In a remote or cloud service. |
-| InSimulator | 3 | Inside the simulator that also renders the sensor. |
-
-### VisionAcceleratorKindEnum — `ns=1;i=3008`
-
-*Subtype of:* `Enumeration`
-
-Compute device executing the model.
-
-| Name | Value | Description |
-|---|---|---|
-| Cpu | 0 | |
-| Gpu | 1 | |
-| Npu | 2 | |
-| Fpga | 3 | |
-| Tpu | 4 | |
-| Other | 5 | |
-
### VisionResultEvaluationEnum — `ns=1;i=3009`
*Subtype of:* `Enumeration`
@@ -770,16 +633,17 @@ Physical relationship between a camera and the kinematic chain it is calibrated
*Subtype of:* `Enumeration`
-Role of a coordinate frame, following the ISO 9787 frame vocabulary.
+Role of a coordinate frame, following the ISO 9787 frame vocabulary. The mechanical interface and the tool are DISTINCT roles: a camera on a robot flange is calibrated to the mechanical interface, while a pick pose has to reach the tool centre point, and a model that cannot tell them apart cannot express the offset between them.
| Name | Value | Description |
|---|---|---|
| World | 0 | |
| Base | 1 | |
-| Tool | 2 | Tool / tool centre point (TCP) frame. |
-| Camera | 3 | |
+| MechanicalInterface | 2 | The flange at the end of the last link, to which an end effector is fitted. This is what an eye-in-hand extrinsic calibration resolves to. |
+| Tool | 3 | A tool frame, whose origin is a tool centre point. |
| Object | 4 | |
| Other | 5 | |
+| Camera | 6 | A camera frame. Numbered after the ISO 9787 roles because it is not one of them. |
### VisionDistortionModelEnum — `ns=1;i=3014`
@@ -811,35 +675,6 @@ What the sensor measures.
| Event | 5 | Event / neuromorphic camera. |
| Other | 6 | |
-### VisionLearningJobStateEnum — `ns=1;i=3016`
-
-*Subtype of:* `Enumeration`
-
-State of a dataset-capture, retraining and promotion cycle.
-
-| Name | Value | Description |
-|---|---|---|
-| Idle | 0 | |
-| Collecting | 1 | |
-| Labelling | 2 | |
-| Training | 3 | |
-| Validating | 4 | |
-| Ready | 5 | A candidate model is available for promotion. |
-| Promoted | 6 | |
-| Failed | 7 | |
-
-### VisionDatasetSourceEnum — `ns=1;i=3017`
-
-*Subtype of:* `Enumeration`
-
-Provenance of the samples in a dataset.
-
-| Name | Value | Description |
-|---|---|---|
-| Real | 0 | Captured from physical sensors. |
-| Synthetic | 1 | Rendered by a simulator. |
-| Mixed | 2 | Both, e.g. synthetic pre-training with real fine-tuning. |
-
### VisionPose3DDataType — `ns=1;i=3050`
*Subtype of:* `Structure`
@@ -965,16 +800,3 @@ A leased media session. The Uri may embed a single-use or time-limited credentia
| Uri | String | Scalar | | Media URI to open. |
| Protocol | VisionStreamProtocolEnum | Scalar | | Protocol of the returned URI. |
| ExpiresAt | UtcTime | Scalar | | Expiry after which the Uri is no longer valid. |
-
-### VisionTensorSignatureDataType — `ns=1;i=3058`
-
-*Subtype of:* `Structure`
-
-Shape and element type of one model input or output tensor.
-
-| Field | DataType | ValueRank | ArrayDimensions | Description |
-|---|---|---|---|---|
-| Name | String | Scalar | | Tensor name as declared by the model. |
-| ElementType | String | Scalar | | Element type, for example float32, uint8 or int64. |
-| Shape | Int32 | Array | | Dimensions; -1 marks a dynamic axis. |
-| Layout | String | Scalar | | Optional axis layout hint, for example NCHW or NHWC. |
diff --git a/metaverse-specs/extras/vision/tools/validate_local.py b/metaverse-specs/extras/vision/tools/validate_local.py
index 2907efc..1619a21 100644
--- a/metaverse-specs/extras/vision/tools/validate_local.py
+++ b/metaverse-specs/extras/vision/tools/validate_local.py
@@ -38,11 +38,73 @@
from __future__ import annotations
import csv
import os
+import re
import sys
import xml.etree.ElementTree as ET
NS = "{http://opcfoundation.org/UA/2011/03/UANodeSet.xsd}"
+# The AI Model Management model is a separate specification. This validator reads its
+# NodeSet rather than importing its generator, for the same reason it reads Vision's:
+# a checker that asks the emitter what it emitted validates nothing.
+AI_NS = "http://opcfoundation.org/UA/AI/"
+_HERE = os.path.dirname(os.path.abspath(__file__))
+AI_NODESET = os.path.normpath(os.path.join(
+ _HERE, "..", "..", "..", "ai-model-management", "Opc.Ua.AiModelManagement.NodeSet2.xml"))
+
+
+def _ai_prefix():
+ """The NodeId prefix the AI Model Management model uses for its OWN namespace.
+
+ Not necessarily ns=1: a NodeSet lists its RequiredModel namespaces in
+ NamespaceUris too, so adding a dependency shifts the model's own index. Reading
+ it from the file rather than assuming is the difference between this validator
+ noticing a change and silently resolving nothing, which would pass.
+ """
+ if not os.path.exists(AI_NODESET):
+ return None
+ root = ET.parse(AI_NODESET).getroot()
+ uris = [u.text for u in root.findall(f"{NS}NamespaceUris/{NS}Uri")]
+ if AI_NS not in uris:
+ return None
+ return "ns=%d;i=" % (uris.index(AI_NS) + 1)
+
+
+AI_PREFIX = _ai_prefix()
+
+
+def _load_ai_types():
+ """BrowseName -> numeric id for every type the AI Model Management model declares."""
+ out = {}
+ if not os.path.exists(AI_NODESET) or not AI_PREFIX:
+ return out
+ for el in ET.parse(AI_NODESET).getroot():
+ tag = el.tag[len(NS):] if el.tag.startswith(NS) else ""
+ if tag in ("UAObjectType", "UADataType", "UAReferenceType"):
+ bn = (el.get("BrowseName") or "").split(":", 1)[-1]
+ nid = el.get("NodeId", "")
+ if bn and nid.startswith(AI_PREFIX):
+ out[bn] = int(nid.split("i=")[1])
+ return out
+
+
+AI_TYPE_ID = _load_ai_types()
+
+
+def _load_ai_ids():
+ """Every numeric NodeId the AI Model Management model declares, for reference checking."""
+ out = set()
+ if not os.path.exists(AI_NODESET) or not AI_PREFIX:
+ return out
+ for el in ET.parse(AI_NODESET).getroot():
+ nid = el.get("NodeId", "") if el.tag.startswith(NS) else ""
+ if nid.startswith(AI_PREFIX):
+ out.add(int(nid.split("i=")[1]))
+ return out
+
+
+AI_IDS = _load_ai_ids()
+
# Base-UA NodeIds that this model legitimately references (namespace 0).
KNOWN_BASE = {
# built-in DataTypes
@@ -409,6 +471,69 @@ def is_concrete_instance(node):
err(f"OPC-UA-Vision.md '{marker}' region does not contain an "
f"'Annex {letter}' heading; regenerate with build_examples.py")
+ # ---- specification <-> model, in both directions -------------------
+ # Every type and enumeration literal the model declares must be named in the
+ # prose, and every `SomeType.SomeMember` the prose writes must exist in the
+ # model. Neither direction alone catches drift: the first misses a document
+ # that describes a member no Server can implement, the second misses a member
+ # that ships undocumented. Only qualified member names are checked in the
+ # reverse direction, because a bare backticked word is as likely to be an
+ # enumeration literal or a term of art as it is to be a member.
+ for n in nodes:
+ cls = n.tag[len(NS):]
+ if cls not in ("UAObjectType", "UAVariableType", "UADataType",
+ "UAReferenceType"):
+ continue
+ bn = simple_name(n)
+ if bn not in spec_text:
+ err(f"model declares {cls[2:]} {bn} but OPC-UA-Vision.md never "
+ "names it")
+ for f_el in n.findall(f"{NS}Definition/{NS}Field"):
+ fname = f_el.get("Name") or ""
+ if fname and not re.search(rf"\b{re.escape(fname)}\b", spec_text):
+ err(f"model declares {bn}.{fname} but OPC-UA-Vision.md never "
+ "names it")
+
+ member_of = set()
+ for n in nodes:
+ owner = simple_name(n)
+ for r in n.findall(f"{NS}References/{NS}Reference"):
+ rt = (r.get("ReferenceType") or "").strip()
+ tgt = (r.text or "").strip()
+ if r.get("IsForward", "true") != "false" and \
+ rt in ("HasComponent", "HasProperty", "i=47", "i=46") and \
+ tgt in by_id:
+ member_of.add((owner, simple_name(by_id[tgt])))
+ # A structure's fields are Definition/Field, not references, but the prose
+ # writes them with the same `Type.Field` notation.
+ for f_el in n.findall(f"{NS}Definition/{NS}Field"):
+ member_of.add((owner, f_el.get("Name") or ""))
+ declared = {simple_name(n) for n in nodes}
+ # AI_TYPE_ID holds every type the AI Model Management model declares. Between the two
+ # sets, a `SomeType.Member` whose owner appears in NEITHER names a type that
+ # exists nowhere - which is how a reference to a renamed or retired type
+ # survives. Skipping it, as the check first did, made exactly that invisible.
+ # Types defined by companion specifications this document cites but does not
+ # load. Listed explicitly rather than pattern-matched, so that adding a
+ # dependency on an outside type is a deliberate edit rather than a silent one.
+ EXTERNAL_TYPES = {
+ "ResultDataType", # OPC 40100-1
+ "UsdGeomCameraType", # OPC UA - OpenUSD Scene Materialization
+ "UsdApiSchemaType", # OPC UA - OpenUSD Scene Materialization
+ "DataChannelSourceType", # OPC UA - Data Channels (draft)
+ }
+ known_elsewhere = set(AI_TYPE_ID) | EXTERNAL_TYPES
+ for owner, member in set(re.findall(
+ r"`([A-Z][A-Za-z0-9]*Type)\.([A-Za-z][A-Za-z0-9]*)`", spec_text)):
+ if owner in declared:
+ if (owner, member) not in member_of:
+ err(f"OPC-UA-Vision.md names {owner}.{member}, which the model "
+ "does not declare")
+ elif owner not in known_elsewhere:
+ err(f"OPC-UA-Vision.md names {owner}.{member}, but neither this model "
+ f"nor the AI Model Management model declares {owner} - a type that "
+ "exists nowhere resolves to nothing")
+
# ---- example overlays --------------------------------------------------
# Each overlay instantiates the base model. Verify it is well-formed, declares the
# Vision namespace as a RequiredModel, and only references type NodeIds that this
@@ -487,10 +612,18 @@ def type_chain(tid):
if len(uris) < 2 or uris[1] != "http://opcfoundation.org/UA/Vision/":
err(f"{label}: expected the Vision namespace at NamespaceUris index 2 "
f"(ns=2); found {uris}")
+ # The worked examples show a camera whose inference runs on a described
+ # deployment, so they instantiate types from BOTH models. The base Vision
+ # NodeSet still requires only base UA; it is the example overlay that composes.
+ if len(uris) < 3 or uris[2] != AI_NS:
+ err(f"{label}: expected the AI Model Management namespace at NamespaceUris "
+ f"index 3 (ns=3); found {uris}")
req = [r.get("ModelUri")
for r in ov_root.findall(f"{NS}Models/{NS}Model/{NS}RequiredModel")]
if "http://opcfoundation.org/UA/Vision/" not in req:
err(f"{label}: missing for the Vision namespace")
+ if AI_NS not in req:
+ err(f"{label}: missing for the AI Model Management namespace")
ov_nodes = [e for e in ov_root
if e.tag.startswith(NS) and e.tag[len(NS):].startswith("UA")]
total_overlay_nodes += len(ov_nodes)
@@ -507,6 +640,10 @@ def type_chain(tid):
if tgt not in ov_ids:
err(f"{label}: {e.get('NodeId')} references {tgt}, which the "
"overlay does not define")
+ elif tgt.startswith("ns=3;i="):
+ if int(tgt.split("i=")[1]) not in AI_IDS:
+ err(f"{label}: {e.get('NodeId')} references {tgt}, which the "
+ "AI Model Management model does not define")
elif not tgt.startswith("i="):
err(f"{label}: {e.get('NodeId')} has malformed reference {tgt}")
if e.tag[len(NS):] in ("UAObject", "UAVariable"):
@@ -573,11 +710,17 @@ def type_named(name):
tid = own_by_name.get(name)
return f"ns=2;i={tid}" if tid else None
- # 5.9: an AiDeploymentType instance shall have exactly one UsesModel reference,
- # and it shall target an AiModelType instance.
- dep_td = type_named("AiDeploymentType")
- model_td = type_named("AiModelType")
- uses_model = type_named("UsesModel")
+ # The deployment-to-model rule moved with the types it constrains, into the
+ # AI Model Management specification. The overlays are still checked against it there,
+ # because they instantiate those types; what this validator keeps is the Vision
+ # side of the seam - that the pipeline names a deployment at all.
+ def ai_type_named(name):
+ tid = AI_TYPE_ID.get(name)
+ return f"ns=3;i={tid}" if tid else None
+
+ dep_td = ai_type_named("DeploymentType")
+ model_td = ai_type_named("ModelType")
+ uses_model = ai_type_named("UsesModel")
for e in ov_nodes:
if type_of.get(e.get("NodeId")) != dep_td:
continue
@@ -586,13 +729,14 @@ def type_named(name):
if r.get("ReferenceType") in ("UsesModel", uses_model)
and r.get("IsForward", "true") != "false"]
if len(targets) != 1:
- err(f"{label}: {e.get('NodeId')} is an AiDeploymentType with "
- f"{len(targets)} UsesModel references; clause 5.11 requires exactly "
- "one, and clause 12.6 depends on it")
+ err(f"{label}: {e.get('NodeId')} is a DeploymentType with "
+ f"{len(targets)} UsesModel references; the AI Model Management "
+ "specification requires exactly one, and its provenance rule "
+ "depends on it")
for t in targets:
if type_of.get(t) != model_td:
err(f"{label}: {e.get('NodeId')} UsesModel targets {t}, which is "
- "not an AiModelType instance (clause 5.11)")
+ "not a ModelType instance")
# Clause 11: VIS-Media-Inline is all four members or none.
clip_td = type_named("ClipEndpointType")
diff --git a/metaverse-specs/robot-intent/CHANGELOG.md b/metaverse-specs/robot-intent/CHANGELOG.md
new file mode 100644
index 0000000..42d04d1
--- /dev/null
+++ b/metaverse-specs/robot-intent/CHANGELOG.md
@@ -0,0 +1,125 @@
+# Changelog — OPC UA — Robot Intent
+
+All notable changes to this specification and its information model.
+
+## Unreleased
+
+### Conformance is machine-readable
+
+`IntentCapabilitiesType.SupportedFacets` (`String[]`, Mandatory, appended at `i=6139`; no existing NodeId moves) carries the facet names of Table 12.2 that a controller claims. Clause 12 defined conformance in terms of facets and gave a Server nowhere to state which it had, so a client had to re-derive the whole table from the address space — which is what the reference implementation did. Several rows are behavioural and cannot be settled by browsing at all, so two clients deriving independently could reach opposite conclusions about one Server and both be reading the specification correctly. §12.2 now separates structural requirements, which a client checks by reading, from behavioural ones, which are the Server's attestation under clause 9. **RI-Base** requires the member.
+
+### Implementation defects
+
+Defects found by implementing the specification in the OPC UA .NET Standard stack. Every change here
+makes an existing claim true; none adds capability, and no previously assigned NodeId moves.
+
+- **A refusal is now observable.** `SubmitIntent`, `SubmitMission` and `Retry` gained `Accepted`,
+ `Failure` (`IntentFailureEnum`) and `Message` output arguments, and §6.2 now states that a Server
+ returns `Good` and reports the refusal in those outputs rather than substituting a Bad `StatusCode`.
+ Before this, §6.2 ordered six distinct refusals and §5.8 called the failure set "small and
+ diagnosable on purpose" — and a client could observe neither, because the only outputs were
+ `IntentId` and `Operation`. A specification cannot require an ordering of refusals a conformant
+ client has no way to see. `OpenRealTimeChannel` gained a `Message` for the same reason: §6.9 lists
+ four grounds for refusal behind a single `Granted` boolean.
+
+- **The Part 10 promotions are now legal promotions.** `IntentOperationType.ProgramDiagnostic` was
+ declared as a Property of `PropertyType` reached by `HasProperty`. OPC 10000-10 declares that member
+ as a Variable of `ProgramDiagnostic2Type` reached by `HasComponent`, so the declaration added a
+ second member beside the inherited one instead of promoting it — which is what stopped a Part 10
+ client, and the first stack that tried to generate the model, from finding it. A promotion changes
+ the ModellingRule and nothing else, and §6.1 now says so.
+
+- **`WaitIntentDataType.Signal` is bounded.** It read "an OutputSignal **or other node**", which §11.3
+ cannot check: an unvalidated NodeId is precisely the surface §11.3 exists to close. It now resolves
+ to an `OutputSignalType` under the controller or to a Variable of DataType `Boolean` under it.
+
+- **§11.3 names the expected type of every NodeId-valued member.** "A node of the expected type" was
+ an instruction to guess. A table now fixes it for `Source`, `Destination`, `Pattern`, `ToolFrame`,
+ `FrameId`, `Tool`, `Output`, `Program`, `ProcessProgram`, `Signal` and `Joint`.
+
+- **§5.7.0 gives `Ready`, `ActiveIntent`, `ActiveMission` and `ControlOwner` normative meaning.** They
+ were in the model and in no clause, so what a Server had to publish in them was unstated.
+
+- **§6.9 bounds `RequestedLease`.** The lease rules never said what limits a request, so a client could
+ ask for a lease of any length and the Server's answer was unspecified.
+
+- **§9 applies its honesty rule to the Method surface.** Three rules already kept the capability
+ declaration honest, but nothing said that a declared capability must come with the Methods that make it
+ usable — and `SubmitMission`, `UpdateMission`, `CancelMission` and the two channel Methods are all
+ *Optional* on `IntentControllerType`. A Server could therefore advertise `MissionsSupported` true and
+ omit `SubmitMission` entirely, which is precisely what the first implementation did, and a client
+ discovered the contradiction only by calling something that was not there. A fourth rule and a table now
+ fix which Methods each declaration implies.
+
+- **§6.5 says what a Server that cannot differentiate `StopMode` must do.** The text gave `StopMode` the
+ `PossibleStopModes` vocabulary of OPC 40010-1 and then said nothing about a Server that treats every
+ value alike — so accepting the argument and discarding it looked conformant. A Server must now either
+ honour it or treat every value as its single stop behaviour, and should say which; a client that asks for
+ `OnPath` and silently gets a `QuickStop` has been told something untrue about how the cell stopped.
+
+- **§6.4 says which stop a superseded intent gets.** An `Aborting` submission carries no `StopMode`, so the
+ mode a superseded intent is stopped with was undefined. The Server chooses, should choose the most urgent
+ stop the cell tolerates since the successor is about to command motion, and should document it.
+
+### Profiles
+
+Clause 12 has been titled *Profiles and conformance units* since 0.1.0 and defined only facets. §12.3 defines four profiles and §12.4 gives their URIs. The information model does not change for this, so the release version does not move on its account: profiles are published through the base-UA `Server/ServerCapabilities/ServerProfileArray` and need no member.
+
+| Profile | Facets |
+|---|---|
+| Robot Motion Server | RI-Base, RI-Motion-Joint, RI-Motion-Linear, RI-Description, RI-Safety |
+| Robot Handling Server | Motion, plus RI-Motion-Circular, RI-Grasp, RI-PickPlace, RI-ToolChange, RI-Output, RI-Queue |
+| Robot Path Server | Motion, plus RI-Trajectory, RI-Path, RI-Blending |
+| Robot Mission Server | Motion, plus RI-Mission, RI-Program, RI-Wait, RI-Pause, RI-Retry |
+
+§1.2's use cases were already written about profiles without using the word. A mixed-fleet work cell — two robots from different manufacturers executing one mission definition — works only if both claim the same shape, and there was no name for the shape to claim.
+
+`RI-Safety` is in the baseline rather than optional to it, and that is the decision in this change most worth arguing about. Clause 10 is explicit that this specification is not safety-rated and that no Method here is a safety function. What it does impose is a duty: report what the safety system enforces, and refuse work that would exceed it. An integrator specifying a profile is entitled to assume a robot declines an intent its safety configuration forbids rather than attempting it, and a robot that cannot read its safety system claims facets individually instead.
+
+The process facets are deliberately in **no** profile. A welding robot is a **Robot Path Server** that additionally claims `RI-Process-ArcWeld`; bundling the process in would have produced one profile per process and no way to say the underlying motion is the same. `RI-Force`, `RI-RealTimeChannel` and the two interop facets stay outside all four for the same reason.
+
+§3 gains definitions for *conformance unit*, *facet* and *profile*, none of which the document defined while using the first two throughout.
+
+Facets and profiles are declared at different levels and §12.2 says how they relate: `SupportedFacets` is on `IntentCapabilitiesType` and is therefore per controller, `ServerProfileArray` is on the Server object and is therefore per Server. Where a Server publishes facet URIs as well as profile URIs, the two must agree.
+
+## 0.1.0 — 2026-08-02
+
+Initial working-group draft.
+
+### Why this specification exists
+
+OPC 40010-1 defines robot topology and no motion verbs. The gap was filled unilaterally in the .NET OPC UA stack by ten verbs in an application-owned namespace, resolved by BrowseName and explicitly self-described as a non-normative convention. That contribution established a vocabulary; it established no lifecycle. This release standardises the layer and supplies the lifecycle.
+
+### Decisions, and what forced them
+
+- **The lifecycle is Part 10, not a bespoke state machine.** OPC 10000-4 §5.12.2 discards a method result when the Session ends *"independent of the task actually performed at the Server"* — a synchronous motion method therefore loses the outcome of work that has already physically happened. OPC 10000-10 §4.1 gives the OPC Foundation's own resolution, and building on `ProgramStateMachineType` supplies transition events, a surviving result object and invocation diagnostics that would otherwise have to be invented.
+
+- **`ExecutionState` refines the Part 10 state rather than restating it.** `Queued`, `Cancelling` and the three terminal outcomes cannot be distinguished from `CurrentState` alone. The state machine remains authoritative and §6.3 tabulates every legal pairing, so the refinement is checkable rather than merely asserted.
+
+- **Verbs are a DataType hierarchy, not one Method each.** This makes a single submission and a mission step the same shape, makes extension a subtyping act, and makes discovery a read of `SupportedIntents` rather than a probe for BrowseNames.
+
+- **Orientation is a unit quaternion, not the core `ThreeDOrientation`.** OPC UA defines no quaternion type, and the `A`, `B`, `C` fields carry no convention of their own — the only normative assignment of meaning to them is external to the base specification. Annex C carries the bidirectional conversion, including the clamp that keeps a pole orientation from becoming a domain error, so the divergence costs no interoperability.
+
+- **`Pick` and `Place` reference Location nodes, not station strings.** A free-text station identifier would be a second definition of a fact the address space already holds, able to disagree with the first.
+
+- **Queueing is PLCopen `MC_BufferMode` and concurrency is VDA 5050 `blockingType`, both adopted unchanged.** No OPC UA specification defines motion-level queueing or blending; both of these are already implemented across the industry, and re-designing either would have produced something less implementable and no more correct.
+
+- **The specification declares itself non-safety-rated.** OPC 40010-1 §7.7.1 disclaims functional safety for its own safety states, and an OPC UA method call carries no safety rating. Clause 10 states the boundary, restricts submission to Automatic and Automatic External modes, and records that a stop request selects no IEC 60204-1 stop category.
+
+- **Command authority is arbitration, not the single point of control.** ISO 10218-2 requires mutual exclusion between remote command and local manual control by safety-rated means. Clause 8 says plainly that this model does not provide it, because a specification that implied otherwise would be dangerous.
+
+- **The NodeSet is standalone.** The only `RequiredModel` is the base UA namespace; OPC 40010-1 interop is an optional profile carried by `HasIntentController`. This follows the precedent set by *OPC UA — Vision* in this repository.
+
+- **Real-time is a division of labour, not an exclusion.** Trajectory *execution* is expressible over request/response — the whole path is handed over once and the robot's own motion kernel runs it, which is what `FollowJointTrajectory` and the PLCopen buffered path blocks do. Streaming *control* is not, so the model **brokers** a channel instead: it describes and leases RTDE, EGM, FRI, RSI, MotoROS2 or OPC UA FX, and the samples never traverse OPC UA. This is the same shape as the Vision model brokering a media endpoint rather than carrying pixels.
+
+- **Safety is awareness plus a refusal duty, and explicitly not a rating.** OPC 10000-15 carries cyclic safety data from a provider to a consumer, and the consumer's request holds an identifier, a monitoring number and one octet of explicitly non-safety flags — so a caller cannot supply safety-rated arguments, and no Method in any companion specification can be a safety function. Every safety fieldbus expresses a safety command as a continuously asserted cyclic signal, because the integrity argument rests on the fail-safe state that follows when assertion stops; a Method call has no behaviour when it stops being called. The model therefore *reports* what the safety system enforces and *refuses* work that would exceed it, and says plainly that neither makes anything safe.
+
+- **The kinematic chain is additive, not a second account.** OPC 40010-1 describes a robot's topology and axes in detail and defines no kinematic chain an IK solver could use, and no tool centre point. Annex B fixes which side decides where both are present.
+
+- **Fastening is thin on purpose.** OPC 40450 and OPC 40451 already define joining and tightening in full, so `FastenIntentDataType.Joint` references a joint there rather than restating torque strategies — the same rule that made `Pick` take a `Location` node instead of a station string.
+
+- **Mission branching follows IEC 61131-3, not a behaviour tree.** Steps and transitions with alternative and parallel divergence are the notation the controller audience already knows and has an IEC serialization; a behaviour tree needs a tick runtime controller vendors do not provide, and its serialization belongs to a library rather than a standard. Transition conditions reuse the base UA `ContentFilter` so that no implementer has to write a parser for an invented expression language. An empty transition array leaves the mission the flat sequence it was.
+
+### Not in this release
+
+Any real-time or servo-level facility carried *through* this interface, which §4.3 excludes as a normative limit; and any claim of functional safety, which clause 10.1 explains cannot be made.
diff --git a/metaverse-specs/robot-intent/OPC-UA-Robot-Intent-Research.md b/metaverse-specs/robot-intent/OPC-UA-Robot-Intent-Research.md
new file mode 100644
index 0000000..b76a9c0
--- /dev/null
+++ b/metaverse-specs/robot-intent/OPC-UA-Robot-Intent-Research.md
@@ -0,0 +1,720 @@
+# Research — An OPC UA Specification for Robot Control via an Intent API
+
+**Scope.** Source material for a specification to be drafted in `metaverse-specs/`, standardising a
+task-level *intent* interface for robots on OPC UA. Triggered by the robotics intent API merged into
+the .NET OPC UA stack ([`OPCFoundation/UA-.NETStandard#4127`](https://github.com/OPCFoundation/UA-.NETStandard/pull/4127),
+from [issue #3827](https://github.com/OPCFoundation/UA-.NETStandard/issues/3827)).
+
+**Status.** Research only. No design decisions are made here; §11 lists what must be decided before a plan.
+
+---
+
+## 1 Executive summary
+
+OPC 40010-1 *Robotics* defines a rich robot **topology** and **no motion verbs at all**. Its entire
+actuation surface is two state machines (`SystemOperation`, `TaskControl`) plus a controller `Programs`
+directory — you can start, stop and load a program, but you cannot say *"move to this pose"*. Everything
+above that line is vendor-proprietary. That gap is real, it is acknowledged in OPC 40010-1's own scope
+clause, and it is what the .NET stack filled unilaterally in an application-owned namespace.
+
+Five findings shape what the specification should be.
+
+1. **The .NET API is a synchronous request/response façade.** It has no operation handle, no progress,
+ no server-side cancel, no queueing. It is a *vocabulary* contribution, not a lifecycle contribution.
+ The lifecycle is the hard part and it is entirely unsolved there (§2.3).
+
+2. **A feared collision does not exist.** The widely repeated claim that OPC 40010 Parts 2 (Skills) and
+ 3 (Motion Program) are in development is **not supported by primary evidence** — the OPC Foundation
+ reference index lists only Part 1 (§9). What *does* exist is a stalled 2020 prototype from the VDMA
+ **SOArc** working group under a different namespace, `http://opcfoundation.org/UA/Skills/`. That is
+ prior art to reference, not an obstacle.
+
+3. **URML is not a standard.** It is a solo, pre-1.0, single-maintainer project. It is a legitimate
+ *consumer* of an OPC UA intent API and a useful sanity check on vocabulary, but it cannot be a
+ normative reference, and the specification must not be shaped to fit it (§4).
+
+4. **OPC UA already has the async pattern, and it is Part 10 `ProgramStateMachineType`.** Per-invocation
+ instances with NodeId handles, `ProgramTransitionEventType` for progress, `FinalResultData` for
+ results. The critical trap: the OPC UA `Cancel` **service** cancels a *request*, not an *operation* —
+ a robot commanded to move keeps moving (§6.4). This distinction must be stated normatively.
+
+5. **Safety is a hard boundary, not a caveat.** ISO 10218-1/-2:2025 (in force 1 April 2025) absorbed
+ ISO/TS 15066 and added cybersecurity requirements. An OPC UA method call carries **no** safety rating;
+ OPC 40010-1 §7.7.1 says so in its own text. The specification must declare itself non-safety-rated,
+ scope itself to Automatic / Automatic External modes, and state that a "stop" request implies **no**
+ IEC 60204-1 stop category (§8).
+
+**The shape that follows from this**: a *task-level* intent layer, explicitly above the vendor motion
+language and explicitly below the safety system, using Part 10 program semantics for lifecycle,
+`ThreeDFrame` for pose, PLCopen `BufferMode` for queueing, and OPC 40010-1 as an optional topology
+binding rather than a required model.
+
+---
+
+## 2 The trigger: what the .NET stack actually shipped
+
+### 2.1 Provenance
+
+PR #4096 was **closed and split** into #4123–#4127. The robotics slice **#4127 is merged**
+(SHA `ae241b6625a8580ec4b3b324ef110acd729df7b4`). Authored by `marcschier`.
+
+The PR body states the design position verbatim:
+
+> OPC 40010 1.02 defines a rich robot **topology** but **no motion verbs at all** — its actuation surface
+> is the SystemOperation and TaskControl state machines plus a Controller `Programs` directory.
+
+It closes this in **two tiers, and never presents the second as standard**:
+
+| Tier | Content | Namespace |
+|---|---|---|
+| 1 — normative | OPC 40010-1 state machines, topology, condition monitoring | `http://opcfoundation.org/UA/Robotics/` |
+| 2 — opt-in, non-normative | motion verbs | application-owned |
+
+Tier 2 members are resolved **by BrowseName**, deliberately, so that a consumer such as URML needs no
+NodeId mapping file. **The specification the user wants is the standardisation of Tier 2.**
+
+### 2.2 The verb set as implemented
+
+`src/Opc.Ua.Robotics/RoboticsOperationConventions.cs`, namespace `Opc.Ua.Robotics.Operations`:
+
+| Verb | Request record |
+|---|---|
+| `MoveTo` | `MoveToRequest(ThreeDFrame TargetFrame, double? SpeedFraction, double? BlendRadius, EUInformation? BlendRadiusUnits)` |
+| `MoveJ` | `JointMoveRequest(ArrayOf JointTargets, EUInformation JointUnits, double? SpeedFraction)` |
+| `MoveL` | `LinearMoveRequest(ThreeDFrame, double LinearSpeed, EUInformation LinearSpeedUnits, double? Acceleration, EUInformation? AccelerationUnits)` |
+| `Grasp` | `GraspRequest(double? ForceNewtons, double? Width, EUInformation? WidthUnits, RoboticsApproach Approach)` |
+| `Release` | `ReleaseRequest(RoboticsReleaseMode Mode, ThreeDFrame? TargetFrame)` |
+| `PickFrom` / `PlaceAt` | `PickPlaceRequest(string StationOrLocationIdentifier, string ObjectClass, ArrayOf Attributes, double? ForceNewtons)` |
+| `SwapTool` | `ToolChangeRequest` |
+| `SetOutput` | `OutputRequest(string, Variant)` |
+| `CallProgram` | `ProgramCallRequest(string, ArrayOf)` |
+
+Enums: `RoboticsReleaseMode { Drop, Place, Handover }`, `RoboticsApproach { Default, ToolZ, Top, Side }`.
+Universal result: `RoboticsOperationResult(ServiceResult, string? Message, ArrayOf? Outputs)`.
+Extension point: `AddOperation`.
+
+Server registration is `IRoboticsOperationsBuilder`
+(`src/Opc.Ua.Robotics.Server/Builders/RoboticsOperationsBuilders.cs`); client resolution is
+BrowseName-based (`src/Opc.Ua.Robotics.Client/Operations/RoboticsOperationsClient.cs`).
+
+### 2.3 What it does not have — the central design problem
+
+The model is **synchronous request/response**. There is:
+
+- no operation handle or ID returned to the caller;
+- no progress feedback;
+- no server-side cancellation (`CancellationToken` is client-side only — it abandons the *wait*, not the
+ *motion*);
+- no queueing, buffering or blending semantics between successive commands;
+- no notion of who currently owns the robot.
+
+A `MoveL` across a two-metre workcell takes seconds. A `PickFrom` may take a minute. Holding an OPC UA
+`Call` open for that duration collides with session timeouts (§6.1). **This is the single largest thing
+the specification must add**, and nothing in the .NET implementation constrains how.
+
+### 2.4 Issue #3827 — what it is and is not
+
+Opened by `@idoco2003` (the URML author) as **outreach**, not a requirements document. `marcschier`
+replied that specification changes go through OPC Foundation working groups via **Mantis** (Robot CS
+area), and proposed "an extension library (common, client, server) for robotics in this repo,
+implementing the standard, and then a simplified binding (AI accessible) in URML". Still open, assigned
+to `marcschier`.
+
+Conspicuously **not** discussed in the thread: ROS 2, VDA 5050, ISO 9787/8373, PackML, MTP, euROBIN.
+The thread is therefore weak evidence of requirements and should not be treated as a design input.
+
+---
+
+## 3 The gap in OPC 40010-1
+
+Published, v1.02, namespace `http://opcfoundation.org/UA/Robotics/`.
+
+**Actuation surface, in full:**
+
+| Type | NodeId | Methods |
+|---|---|---|
+| `OperationStateMachineType` | i=1006 | `Start() → Status:Int32`, `Stop(StopMode:Int32) → Status:Int32` |
+| `SystemOperationStateMachineType` | i=1021 | adds `GetReady()`, `StandDown()` |
+| `TaskControlStateMachineType` | i=1025 | adds `LoadByName`, `LoadByNodeId`, `UnloadByName` |
+
+States: **Idle(1) → Ready(2) → Executing(3)**.
+`PossibleStopModes`: OnPath(1), EndOfCycle(2), ProcessStop(3), QuickStop(4), EndOfInstruction(5).
+Status codes: 0 OK, 1 `E_SystemState`, 2 `E_UnexpectedError`, 3 `E_ActiveAlarm`, 4 `E_AcknowledgeRequired`.
+ReferenceTypes: `Controls`, `Moves`, `Requires`, `IsDrivenBy`, `IsConnectedTo`, `HasSafetyStates`, `HasSlave`.
+
+That is the whole of it. You can load a program by name and start it. **There is no pose, no target, no
+TCP concept, no tool frame, and no motion verb.**
+
+OPC 40010-1 §4.1 lists as out of scope for Part 1, in its own words, *"a state machine to inform about
+the status of task controls and to interact via methods"* — the gap is acknowledged, and deferred to
+"future parts" that (§9) do not demonstrably exist.
+
+---
+
+## 4 URML — a reality check
+
+[`URML-MARS/URML`](https://github.com/URML-MARS/URML), urml.dev, Apache-2.0.
+
+**Disambiguation matters**: it is **"Universal Robot Language"**, not "Universal Robot *Modeling*
+Language". At least three unrelated things share the acronym.
+
+| Property | Finding |
+|---|---|
+| Governance | `GOVERNANCE.md` states **"One person"** |
+| Version | v0.1.0, pre-1.0 |
+| Contributors | zero external |
+| Third-party runtimes | zero |
+| Serialization | YAML only (`.urml.yaml`) |
+| Primitives | 27 — core (12), profile-scoped (12), capability-gated (3) |
+| Pose | `{x, y, z?, yaw?, pitch?, roll?}` — **Euler, not quaternion**; SI units; force in Newtons |
+| Composition | `sequence`, `branch`, `parallel`, `retry`, `on_error` |
+| Validation | five passes: argument typing → capability → safety-envelope → variable-binding → compliance policy |
+
+**Two corrections to widely circulated claims:**
+
+- URML has **no `moveJ` / `moveL`**. It has only `move_to`; joint-vs-Cartesian is explicitly delegated to
+ the substrate. Web sources asserting otherwise are confusing URML with UR Script. The .NET stack's
+ `MoveJ`/`MoveL` therefore go **beyond** URML rather than implementing it.
+- URML's `call_program` explicitly names OPC UA `ControlProgram` method nodes as a motivating case — so
+ the interest is genuine and bidirectional.
+
+**Assessment.** URML is an aspiring standard with one maintainer. It is a valid *reference implementation
+target* and a useful cross-check that the verb vocabulary is AI-consumable. It must not be a normative
+reference, and its omissions (no joint/Cartesian distinction) must not propagate into the specification.
+
+---
+
+## 5 State of the art
+
+### 5.1 Comparison matrix
+
+| Dimension | VDA 5050 v3 | PLCopen MC | ROS 2 actions | OPC 10031-4 / 40001-3 Jobs | OPC UA Part 10 Programs | .NET intent API |
+|---|---|---|---|---|---|---|
+| Command identity | `actionId` (UUID) | FB instance | goal UUID | `JobOrderID` | Program instance NodeId | **none** |
+| Async | yes | yes (`Busy`/`Active`) | yes | yes | yes | **no** |
+| Progress | `actionStatus` in state msg | `Busy`/`Active` bits | `feedback` topic | status events | `ProgramTransitionEventType` | **no** |
+| Cancel | `cancelOrder` instant action | `MC_Stop`, `mcAborting` | `cancel_goal` → `CANCELING` | `Abort` / `Stop` / `Cancel` | `Halt()` | **no** |
+| Queueing | base/horizon graph | **`BufferMode`** | manual | `JobOrderList` + priority | — | **no** |
+| Blending | — | **`mcBlending*`** | `blend_radius` | — | — | `BlendRadius` param |
+| Concurrency | **`blockingType`** | `Active` ⊆ `Busy` | — | — | — | — |
+| Capability discovery | **factsheet** | — | — | `WorkMaster` | — | BrowseName presence |
+| Terminal result | `actionResult` string | `Done` pulse | result msg | `ISA95JobResponseDataType` | `FinalResultData` | return value |
+
+Each column contributes something the others lack. The specification should be assembled from the best
+of each rather than modelled on any one.
+
+### 5.2 VDA 5050 — the closest widely-adopted analogue
+
+AGV fleet protocol over MQTT. Its concurrency and capability models are the most directly reusable.
+
+**`blockingType`** — a 2×2 matrix of *driving allowed* × *parallel execution allowed*:
+
+| Value | Driving | Parallel | Meaning |
+|---|---|---|---|
+| `NONE` | yes | yes | pure background |
+| `SOFT` | no | yes | vehicle stops, other actions may overlap |
+| `SINGLE` | yes | no | drives on, but exclusive among actions |
+| `HARD` | no | no | exclusive access to the robot |
+
+Instant actions are **always** `NONE`.
+
+**`actionStatus`** lifecycle: `WAITING → INITIALIZING → RUNNING → PAUSED → FINISHED | FAILED | RETRIABLE`.
+`RETRIABLE` (v3) with `retry` / `skipRetry` instant actions is a genuinely good idea largely absent from
+OPC UA precedent.
+
+**Order model**: `orderId` + monotonically increasing `orderUpdateId`; a node/edge graph where each element
+carries a `released` flag splitting it into an immutable **base** and a mutable **horizon**. The base
+cannot be changed — the fleet controller must assume it is already executing. A lower `orderUpdateId`
+yields `OUTDATED_ORDER_UPDATE`. This is a well-tested answer to "how do you extend a running mission over
+an unreliable link".
+
+**Factsheet** (`protocolFeatures.mobileRobotActions[]`) declares, per action: `actionScopes`
+(`INSTANT|NODE|EDGE|ZONE`), `pauseAllowed`, `cancelAllowed`, permitted `blockingTypes`, and typed
+`actionParameters`. **This is machine-readable capability declaration and it is exactly what an intent API
+needs** — in OPC UA it becomes properties on the Method node rather than a JSON document.
+
+### 5.3 PLCopen Motion Control — the queueing model nothing else has
+
+`MC_BufferMode`:
+
+| Value | Name | Semantics |
+|---|---|---|
+| 0 | `mcAborting` | abort running command immediately; predecessor raises `CommandAborted` |
+| 1 | `mcBuffered` | queue; start when predecessor reaches `Done` |
+| 2 | `mcBlendingLow` | blend at the **lower** of the two velocities |
+| 3 | `mcBlendingPrevious` | blend at predecessor's endpoint velocity |
+| 4 | `mcBlendingNext` | blend at successor's start velocity |
+| 5 | `mcBlendingHigh` | blend at the **higher** — max throughput |
+
+In all blending modes the axis never decelerates to zero at the boundary; the predecessor fires `Done`
+when *blending begins*, and `CommandAborted` is **not** raised.
+
+The output-signal discipline is equally valuable: `Active ⊆ Busy` distinguishes **queued** from
+**commanding** — a distinction ROS 2 actions lack, and one an OPC UA state machine should expose as an
+explicit sub-state. `Done` and `CommandAborted` are one-scan pulses; `Error` latches.
+
+### 5.4 ROS 2 actions — the cancellation semantics
+
+Goal / result / feedback, over `send_goal`, `cancel_goal`, `get_result` services plus `feedback` and
+`status` topics. Client-generated goal UUID. States: `ACCEPTED → EXECUTING → SUCCEEDED | CANCELED | ABORTED`,
+with an explicit **`CANCELING`** state during cleanup, and the server permitted to **reject** a cancel
+(e.g. past a point of no return). That rejection right is important for a robot: some motions cannot be
+safely abandoned mid-flight.
+
+MoveIt contributes `MotionSequenceItem.blend_radius` [m] — `0` = buffered, `>0` = corner blending, the
+same abstraction as PLCopen `TransitionParameter`.
+
+### 5.5 ISA-95 job control — the best OPC UA precedent
+
+OPC 10031-4 / OPC 40001-3, `ISA95JobOrderReceiverObjectType`. Methods: `Store`, `StoreAndStart`, `Start`,
+`RevokeStart`, `Update`, `Pause`, `Resume`, `Abort`, `Stop`, `Cancel`, `Clear`. `JobOrderList` is a live
+queue with `Priority : Int16`; `MaxDownloadableJobOrders` advertises capacity.
+
+States: `NotAllowedToStart ↔ AllowedToStart → Running ↔ Interrupted → Ended`, plus `Aborted`;
+`Interrupted` splits into `Suspended` (external) and `Held` (automatic). Note the clean separation of
+`Cancel` (pre-execution, removes from queue) from `Abort` (during execution) from `Clear` (post-mortem
+cleanup) — three different verbs for three genuinely different acts, which most APIs conflate.
+
+Deployed in OPC 40501-1 Machine Tools, so it is proven at this granularity.
+
+### 5.6 MTP and the Capability–Skill–Service model
+
+**MTP** (VDI/VDE/NAMUR 2658) has a published OPC UA companion spec, **OPC 40081**, and is genuinely
+deployed in the process industry. Its service model — a parameterised service with an ISA-88/PackML
+state machine, typed in/out/config/report parameters — is the most mature industrial analogue to an
+intent API.
+
+**PackML** (OPC 30050) contributes the richest execution lifecycle in any OPC UA companion spec: 17
+states, with the distinction between `Held` (orderly operator pause, position preserved), `Suspended`
+(resource unavailability) and `Aborted` (emergency). Transitional *acting* states (`Starting`,
+`Completing`, `Stopping`) model the graceful handshake where a controller cannot transition instantly —
+directly applicable to a robot decelerating.
+
+**CSS** (Plattform Industrie 4.0) fixes the vocabulary:
+
+| Term | Meaning |
+|---|---|
+| **Capability** | implementation-independent specification of what can be done |
+| **Skill** | the executable implementation on a specific asset |
+| **Service** | the invocable interface through which the skill is offered |
+
+An intent API is, in this vocabulary, precisely a **service** layer over **skills**, discovered by
+**capability**. Using these three words correctly costs nothing and buys alignment with IDTA-02020
+(*Capability Description*, published) and the OPC 30270 AAS ↔ OPC UA bridge.
+
+Academic lineage: Bøgh et al. 2012 *"Does your Robot have Skills?"*; Pedersen et al. 2016
+(DOI `10.1016/j.rcim.2015.04.002`) — which found **~10 well-designed skills cover most logistics tasks**,
+a useful sanity check on the .NET stack's 10 verbs. SkiROS2 ([arXiv:2306.17030](https://arxiv.org/abs/2306.17030))
+is the most complete open implementation; skills carry `preConditions`, `holdConditions`, `postConditions`.
+
+### 5.7 Vendor motion languages — what must be implementable
+
+| Feature | UR | ABB | KUKA | FANUC | Yaskawa | Kawasaki |
+|---|---|---|---|---|---|---|
+| Joint move | `movej` | `MoveJ` / `MoveAbsJ` | `PTP` | `J` | `MOVJ` | `JMOVE` |
+| Linear move | `movel` | `MoveL` | `LIN` | `L` | `MOVL` | `LMOVE` |
+| Circular move | `movec` | `MoveC` | `CIRC` | `C` | `MOVC` | `CMOVE` |
+| Blend / zone | `r` [m] | `zonedata` fine/z0–z200 | `$APO` C_PTP/C_DIS/C_VEL/C_ORI | `CNT0–100`, `CR` [mm] | `PL=0..8` | `ACCURACY` |
+| Tool frame | `set_tcp()` | `tooldata` | `$TOOL` | `UT[n]` | `TOOL#` | `TOOL n` |
+| Work frame | offsets | `wobjdata` | `$BASE` | `UF[n]` | `BASE#` | `WORK n` |
+| Real-time channel | RTDE 500 Hz | EGM 250 Hz | FRI 1 kHz / RSI 250 Hz | — | MotoROS2 100 Hz | — |
+
+**Universal common denominator**: joint move, linear move, circular move, a speed, an acceleration, a
+blend/zone, a tool frame, a work frame, and an exact-stop option. Every vendor has all nine.
+
+**Blending is the awkward one.** ABB `zonedata` is a six-field structure decoupling TCP path zone from
+orientation zone; KUKA offers four different blend criteria; FANUC, Yaskawa and Kawasaki expose only a
+unitless relative scale. The portable abstraction is `termination: exact | blend` plus an optional
+`blendRadius` in millimetres, with vendors on relative scales requiring a speed-dependent mapping. The
+specification should say the radius is a **request, not a guarantee**.
+
+**Latency reality**: OPC UA runs at 10–100 Hz with 10–30 ms non-deterministic round-trip. Vendor
+real-time channels run at 250–1000 Hz. **The intent API cannot and must not attempt servo-level control**,
+trajectory streaming, force loops or conveyor tracking. It sits above the vendor language and is
+translated into it. This belongs in the "what this specification does not do" clause.
+
+Note also that **all major vendors now ship OPC 40010-1** — but supervisory only (program load/start/stop),
+confirming both the gap and the demand.
+
+---
+
+## 6 The hard problem: long-running operations in OPC UA
+
+### 6.1 The constraint
+
+Part 4 §5.12.2 sets **no** maximum method execution time, but:
+
+> If the Method is invoked in the context of a Session and the Session is terminated, the results of the
+> Method's execution cannot be returned to the Client and are discarded. **This is independent of the task
+> actually performed at the Server.**
+
+That last sentence is the whole problem in one line: the transport can give up while the robot keeps
+moving. `TimeoutHint` in `RequestHeader` is a *hint*, not an obligation. In practice, session watchdogs,
+SecureChannel re-keying and TCP timeouts stack such that a method blocking beyond ~30–60 s will fail the
+session. There is **no** mechanism for a method to emit progress within a single `Call`.
+
+Part 10 §4.1 states the OPC Foundation's own conclusion:
+
+> Programs model more complex and stateful functionality in the system. For example, a method call may be
+> used to perform a calculation or reset a counter. A Program is used to run and control a batch process,
+> execute a machine tool part program, or manage a domain download.
+
+### 6.2 `ProgramStateMachineType` (Part 10)
+
+```mermaid
+stateDiagram-v2
+ [*] --> Halted
+ Halted --> Ready: Reset
+ Ready --> Running: Start
+ Running --> Ready: internal success
+ Running --> Suspended: Suspend
+ Suspended --> Running: Resume
+ Running --> Halted: Halt or error
+ Suspended --> Halted: Halt
+ Suspended --> Ready: internal
+ Ready --> Halted: Halt
+```
+
+Nine transitions, each firing `ProgramTransitionEventType` **and** `AuditProgramTransitionEventType`.
+
+Structure: `Creatable`, `Deletable`, `AutoDelete`, `RecycleCount`, `InstanceCount`, `MaxInstanceCount`,
+`ProgramDiagnostic2DataType`, the four state objects, nine transition objects, five
+`OptionalPlaceholder` methods, and `FinalResultData`.
+
+- **Progress** — `ProgramTransitionEventType.IntermediateResult` is an Object aggregating arbitrary
+ Variables at the instant of transition (percent complete, current step, sensor readings). Subscribing to
+ it is the standard OPC UA progress mechanism. Any Variable on the instance can additionally be polled by
+ MonitoredItem for smooth progress.
+- **Result** — `FinalResultData` persists after termination until the instance is deleted. Results are
+ **not** the return value of `Start()`.
+- **Handle** — with `Creatable = True`, the client calls `ObjectCreate` and receives a unique NodeId per
+ invocation; `AutoDelete = False` keeps it alive for result inspection. This is OPC UA's native
+ "get a handle, watch it, collect the result" pattern.
+- **Diagnostics** — `ProgramDiagnostic2DataType` records `createSessionId`, `createClientName`,
+ `lastMethodCall`, `lastMethodSessionId`, input/output values and `lastMethodReturnStatus`. **This
+ provides command provenance for free** — which is exactly what a multi-client robot needs for arbitration
+ and audit.
+
+### 6.3 The queueing gap
+
+**No OPC UA companion specification defines motion-level command queuing or blending.** ISA-95 job control
+gives a coarse job-level queue with priority; nothing gives PLCopen `BufferMode`, motion blending, or
+VDA 5050 append-to-running-order semantics. Confirmed by direct search across published specs.
+
+If the intent specification adopts `BufferMode` as a first-class parameter, it is filling a documented,
+industry-wide gap — and one PLCopen has already solved well enough to be worth copying rather than
+redesigning.
+
+### 6.4 Cancellation — a trap that must be stated normatively
+
+| | `Cancel` **service** (Part 4 §5.7.5) | `Halt()` / `Abort()` **methods** |
+|---|---|---|
+| Layer | protocol / transport | application / information model |
+| Effect | discards the pending service response (`Bad_RequestCancelledByClient`) | stops the real-world operation |
+| Robot | **keeps moving** | stops |
+
+The `Cancel` service takes a `requestHandle` and cancels the *message transaction*. It is for abandoning a
+slow Read or Browse. **Calling it on a motion request abandons the answer, not the motion.** A
+specification that does not say this plainly will produce implementations that believe they have a stop
+button and do not.
+
+---
+
+## 7 Spatial foundations
+
+All core spatial types are in Part 5, namespace `http://opcfoundation.org/UA/`, added in v1.05.
+
+| Type | NodeId | Definition |
+|---|---|---|
+| `RationalNumber` | i=18806 | `Numerator:Int32`, `Denominator:UInt32` |
+| `Vector` | i=18807 | abstract |
+| `ThreeDVector` | i=18808 | `X`, `Y`, `Z` : Double |
+| `CartesianCoordinates` | i=18809 | abstract |
+| `ThreeDCartesianCoordinates` | i=18810 | `X`, `Y`, `Z` : Double |
+| `Orientation` | i=18811 | abstract |
+| `ThreeDOrientation` | i=18812 | `A`, `B`, `C` : Double |
+| `Frame` | i=18813 | abstract |
+| **`ThreeDFrame`** | **i=18814** | `CartesianCoordinates` + `Orientation` — 48 bytes, no optional fields |
+
+VariableTypes: `ThreeDCartesianCoordinatesType` i=18774, `ThreeDOrientationType` i=18779 (both confirmed
+from RSL NodeSet references).
+
+**Three findings that must become normative text:**
+
+1. **There is no quaternion type anywhere in OPC UA** — not in core, RSL, GPOS, or Robotics. Orientation is
+ three Euler doubles. Given ABB's `robtarget` carries a quaternion and most robotics stacks are
+ quaternion-internally, either a conversion rule or a new DataType is required. This is a real decision
+ (§11).
+
+2. **`A`/`B`/`C` are semantically undefined in core.** The only normative assignment is in RSL
+ (OPC 10000-210), per **ISO 9787**: `A` = roll about X, `B` = pitch about Y, `C` = yaw about Z, intrinsic
+ active rotation. A specification using `ThreeDOrientation` without restating this is ambiguous.
+
+3. **Units are not in the wire format.** `ThreeDFrame` carries no units. RSL attaches `LengthUnit` and
+ `AngleUnit` as `EUInformation` properties **on the VariableType**. A Method argument typed
+ `ThreeDFrame` therefore has **no** unit channel — units must be fixed normatively or carried
+ alongside. Relevant `EUInformation` (i=887) codes: metre `MTR`=5067858, millimetre `MMT`=5066068,
+ radian `C81`=4405297, degree `DD`=17476, metre/second `MTS`=5067859.
+
+**No handedness or axis convention is stated anywhere** in core OPC UA or OPC 40010. The specification
+must state it explicitly (right-handed, and the up-axis).
+
+**Critically: OPC 40010-1 has no TCP or tool-frame concept.** Every vendor has one; the companion spec
+does not. An intent API that says "move to this pose" is meaningless without defining *what* moves to it.
+Frame definition is therefore not optional groundwork — it is a prerequisite.
+
+Relevant vocabulary is fixed by **ISO 8373** (pose, path, trajectory, task, TCP, workspace, end effector)
+and coordinate systems by **ISO 9787** (world / base / mechanical-interface / tool / object). Both should
+be normative references.
+
+---
+
+## 8 Safety — the boundary conditions
+
+### 8.1 What changed in 2025
+
+ISO 10218-1:2025 and ISO 10218-2:2025 were published February 2025, in force **1 April 2025** — the first
+major revision since 2011.
+
+| | 2011 | 2025 |
+|---|---|---|
+| Collaborative requirements | separate ISO/TS 15066:2016 | **absorbed**; TS 15066 withdrawn as standalone |
+| Terminology | "collaborative robot" | **"collaborative application"** — robot + task + environment |
+| Cybersecurity | absent | **present, where it affects safety** |
+| Functional safety | general PL d / Cat 3 | per-function PLr/SIL table; deviation needs justification |
+
+Two consequences for a networked command API. First, only an *application* can be collaborative, so an
+intent API commands a collaborative application, not a "cobot". Second, **a networked external command
+interface is precisely the attack surface the new cybersecurity clauses address** — the specification's
+security clause is a safety-relevant artefact, not boilerplate.
+
+### 8.2 Operational modes and single point of control
+
+`OperationalModeEnumeration` (OPC 40010-1 §10.4, Table 120) — **read-only status**; OPC 40010-1 defines
+**no** method to command a mode change, because mode selection is a safety function executed by
+safety-rated means (key switch, interlock).
+
+| Mode | Value | Constraint |
+|---|---|---|
+| Other | 0 | boot, uncalibrated, safety failure |
+| Manual reduced speed (T1) | 1 | TCP ≤ **250 mm/s**, three-position enabling device held |
+| Manual high speed (T2) | 2 | program verification only, enabling device, guarding in place |
+| Automatic | 3 | no persons in safeguarded space |
+| **Automatic external** | 4 | *"with external control, e.g. by a PLC"* — **this is the intent API's mode** |
+
+**Single point of control** (ISO 10218-2 §5.2) requires that a remote command source be mutually exclusive
+with local manual control, by safety-rated means.
+
+### 8.3 Can OPC UA be safety-rated?
+
+A plain OPC UA method call: **no**, categorically. The only route is **OPC 10000-15 Safety /
+IEC 62541-15:2025** (published Feb 2025; OPC Foundation v1.05.04, 2024-10-15), a SafetyProvider /
+SafetyConsumer black-channel layer over ordinary OPC UA. Whether it certifies safety-rated *method
+invocation* (as opposed to variable exchange) is **not confirmed** — treat all method calls as
+non-safety-rated.
+
+OPC 40010-1 §7.7.1 states it directly:
+
+> The safety state is for informational purpose only and not intended for use with functional safety
+> applications as defined in IEC 61508.
+
+### 8.4 Draft normative text
+
+Adapted from the research, suitable as a starting point:
+
+> This specification defines a **non-safety-rated** application interface. The Methods defined herein are
+> processed by the robot controller as application-level requests and do not constitute, and **shall not**
+> be used as, safety functions as defined in IEC 61508 or safety communication as defined in
+> IEC 61784-3 / IEC 62541-15. The safety functions of the robot system — emergency stop, protective stop,
+> speed monitoring, force limiting and enabling device control — are implemented in safety-rated hardware
+> and firmware independent of this interface and remain effective regardless of its state.
+>
+> This interface is applicable only when the robot system is in **Automatic** or **Automatic External**
+> operational mode, with the safeguarded space secured or an appropriate collaborative technique active,
+> and with local manual control authority excluded by safety-rated means consistent with the
+> single-point-of-control requirement.
+>
+> A stop request issued via this interface is an application-level request. It does **not** guarantee any
+> IEC 60204-1 Stop Category behaviour. The stop category is determined solely by the robot controller's
+> safety system.
+
+**Repo-convention check**: every one of these is observable against a legal implementation — a server can
+be seen to refuse commands outside Automatic modes — so they satisfy the "executable normative statement"
+rule.
+
+---
+
+## 9 Is there a collision? — verification result
+
+The claim that **OPC 40010 Part 2 (Skills)** and **Part 3 (Motion Program)** are in development was
+tested against primary sources and **is not supported**.
+
+| Claim | Verdict | Evidence |
+|---|---|---|
+| OPC 40010-1 published v1.02 | ✅ **confirmed** | `reference.opcfoundation.org` index; NodeSet in `OPCFoundation/UA-Nodeset:Robotics/` |
+| OPC 40010-2 "Skills" | ❌ **no evidence** | absent from the OPC Foundation reference index; no NodeSet, branch, PR or issue in `UA-Nodeset`; not in VDMA public listings |
+| OPC 40010-3 "Motion Program" | ❌ **no evidence** | term does not appear in any OPC Foundation public robotics document |
+
+What exists instead is forward-looking boilerplate — OPC 40010-1 §4.1 *"in future parts…"* and the
+Foundation robotics page *"subsequent parts will extend…"* — plus a genuine but stalled prototype:
+
+**[`opcua-skills/skill-nodeset`](https://github.com/opcua-skills/skill-nodeset)** — from the **SOArc**
+VDMA/OPC Foundation joint working group, namespace `http://opcfoundation.org/UA/Skills/`, **version
+0.0.5, dated 2020-08-01, no commits since**. Its README calls itself *"a first draft of a possible skill
+model"*. Backed by Profanter et al., *"A Hardware-Agnostic OPC UA Skill Model for Robot Manipulators and
+Tools"*, IEEE ETFA 2019. It contributes a `SkillStateMachineType` with Start/Halt/Reset/Resume/Suspend.
+
+Sources asserting active Part 2/3 development trace back to an AI-generated blog with no primary
+citations. **Confidence that a numbered Part 2 is in formal development: LOW (15–20%).**
+
+**Consequences.** (a) No collision blocks this work. (b) The `http://opcfoundation.org/UA/Skills/`
+namespace is claimed — do not reuse it. (c) SOArc's prior art should be cited, since a future OPC UA
+skills effort would likely resume from it. (d) The one gap public research cannot close is whether an
+internal VDMA working draft exists; that is a question for the VDMA robotics contact, and is a
+prerequisite before proposing anything through Mantis.
+
+---
+
+## 10 Recommended shape (for the plan, not a decision)
+
+### 10.1 Layering
+
+```mermaid
+flowchart TB
+ A["AI planner / URML / fleet controller"] --> B
+ B["Intent API — this specification
task-level verbs, 10-100 Hz"] --> C
+ C["Vendor motion language
URScript / RAPID / KRL / TP / INFORM"] --> D
+ D["Real-time channel
RTDE / EGM / FRI / RSI, 250-1000 Hz"] --> E
+ E["Safety-rated controller
ISO 10218, independent of this interface"]
+ F["OPC 40010-1 topology
optional binding"] -.-> B
+```
+
+The specification occupies exactly one band. Everything below it is out of scope; the safety layer is
+beside and beneath it and is never mediated by it.
+
+### 10.2 Lifecycle
+
+Part 10 program semantics, extended with the two things Part 10 lacks — an explicit **Queued** sub-state
+(from PLCopen `Active ⊆ Busy`), and a **Cancelling** state with a server right of refusal (from ROS 2):
+
+```mermaid
+stateDiagram-v2
+ [*] --> Accepted
+ Accepted --> Queued: buffered
+ Accepted --> Executing: aborting or head of queue
+ Queued --> Executing: predecessor Done or blend begins
+ Executing --> Suspended: Suspend
+ Suspended --> Executing: Resume
+ Executing --> Cancelling: Cancel accepted
+ Cancelling --> Cancelled: cleanup complete
+ Executing --> Succeeded: complete
+ Executing --> Failed: error
+ Failed --> Queued: Retry
+ Succeeded --> [*]
+ Cancelled --> [*]
+ Failed --> [*]
+```
+
+### 10.3 Elements to assemble
+
+| Need | Borrow from | Form in OPC UA |
+|---|---|---|
+| Operation handle | Part 10 `Creatable` + `ObjectCreate` | per-invocation instance NodeId |
+| Progress | Part 10 | `ProgramTransitionEventType.IntermediateResult` |
+| Result | Part 10 | `FinalResultData` |
+| Provenance / arbitration | Part 10 | `ProgramDiagnostic2DataType` |
+| Queueing & blending | PLCopen `BufferMode` | enum argument on every motion Method |
+| Concurrency | VDA 5050 `blockingType` | property on the Method node |
+| Capability declaration | VDA 5050 factsheet | properties: `cancelAllowed`, `pauseAllowed`, permitted buffer modes |
+| Retry | VDA 5050 `RETRIABLE` | terminal-but-resumable state |
+| Cancel vs abort vs clear | ISA-95 job control | three distinct Methods |
+| Pose | Part 5 `ThreeDFrame` + RSL/ISO 9787 | with units and handedness fixed normatively |
+| Vocabulary | CSS + ISO 8373 | capability / skill / service used correctly |
+
+### 10.4 Fit with the repository
+
+The **`vision/` spec is the working template**: `OPC-UA-Vision.md` plus a companion
+`OPC-UA-Vision-Research.md` rationale document plus a `robotics/` addendum with a generated instance
+overlay. That is a strong precedent for exactly this deliverable shape — and notably, Vision references
+OPC 40010-1 *without* taking it as a `RequiredModel`, which is the right precedent for keeping the intent
+spec standalone.
+
+Conventions to follow: `/OPC-UA-.md` + `Opc.Ua..NodeSet2.xml` + `.NodeIds.csv`;
+generator at `metaverse-specs/extras//tools/build_model.py`; validator registered in
+`metaverse-specs/validate_all.py`; NodeId ranges by kind (ObjectTypes 1001+, DataTypes 3001+/3050+,
+ReferenceTypes 4001+, encodings 5001+, Variables 6001+, entry-point Object 7001).
+
+---
+
+## 11 Open questions — to resolve before planning
+
+1. **Verb set** — standardise the .NET stack's 10 verbs as-is, or redesign? `PickFrom`/`PlaceAt` take a
+ `StationOrLocationIdentifier` **string**, which is a second source of truth against any modelled
+ station node and conflicts with the repo's "one source of truth per fact" rule.
+2. **Orientation** — accept Euler `ThreeDOrientation` (ISO 9787 A/B/C) and require conversion, or define a
+ quaternion DataType? Interoperability versus alignment with existing OPC UA practice.
+3. **Units** — fix normatively (metres, radians) or carry `EUInformation` per argument as the .NET API
+ does? The former is simpler and testable; the latter matches RSL.
+4. **Lifecycle weight** — full Part 10 per-invocation instances (expressive, heavy: `ObjectCreate`,
+ node lifetime management) versus a lighter handle-plus-status-variable design?
+5. **Scope of composition** — single intents only, or sequences/graphs à la VDA 5050 base/horizon? The
+ latter is a large increase in scope.
+6. **Relationship to OPC 40010-1** — optional interop annex (Vision precedent) or hard `RequiredModel`?
+7. **Namespace** — must avoid `.../UA/Robotics/` and `.../UA/Skills/`.
+8. **VDMA** — is there a non-public working draft? Only the VDMA robotics contact can close this, and it
+ bears on whether this is a submission or an independent draft.
+9. **Gripper / tool scope** — `Grasp`/`Release` imply an end-effector model that OPC 40010-1 does not
+ provide. In scope, or deferred?
+10. **URML's role** — informative annex demonstrating a binding, or no mention at all? Given its
+ single-maintainer status, an informative annex is the most it can support.
+
+---
+
+## 12 Key sources
+
+| Source | Relevance |
+|---|---|
+| [`OPCFoundation/UA-.NETStandard#4127`](https://github.com/OPCFoundation/UA-.NETStandard/pull/4127) | the merged intent API; SHA `ae241b6625a8580ec4b3b324ef110acd729df7b4` |
+| [`OPCFoundation/UA-.NETStandard#3827`](https://github.com/OPCFoundation/UA-.NETStandard/issues/3827) | originating issue, still open |
+| [`OPCFoundation/UA-Nodeset:Robotics/`](https://github.com/OPCFoundation/UA-Nodeset/tree/master/Robotics) | OPC 40010-1 v1.02 NodeSet |
+| [`OPCFoundation/UA-Nodeset:Schema/Opc.Ua.Types.bsd`](https://github.com/OPCFoundation/UA-Nodeset/blob/master/Schema/Opc.Ua.Types.bsd) | `ThreeDFrame` and spatial type definitions |
+| [OPC 10000-10 Programs](https://reference.opcfoundation.org/specs/OPC-10000-10/4) | the async pattern |
+| [OPC 10000-4 §5.7.5 / §5.12.2](https://reference.opcfoundation.org/specs/OPC-10000-4/5.12.2) | Cancel service vs Call semantics |
+| [OPC 40010-1 §4.1, §7.7.1, §10.4](https://reference.opcfoundation.org/specs/OPC-40010-1/4) | scope exclusions, safety disclaimer, operational modes |
+| [OPC 10031-4 job control](https://reference.opcfoundation.org/specs/OPC-10031-4/6) | ISA-95 submit/track/abort precedent |
+| [OPC 40081 MTP](https://reference.opcfoundation.org/specs/OPC-40081/) | parameterised service invocation, deployed |
+| [`VDA5050/VDA5050`](https://github.com/VDA5050/VDA5050) | v3 schemas: `blockingType`, `actionStatus`, factsheet |
+| [ROS 2 actions design](https://design.ros2.org/articles/actions.html) | goal/feedback/cancel semantics |
+| [`URML-MARS/URML`](https://github.com/URML-MARS/URML) | the consuming DSL |
+| [`opcua-skills/skill-nodeset`](https://github.com/opcua-skills/skill-nodeset) | SOArc prior art, stalled at v0.0.5 |
+| [Plattform I4.0 CSS](https://www.plattform-i40.de/IP/Redaktion/EN/Downloads/Publikation/CapabilitiesSkillsServices.pdf) | capability / skill / service vocabulary |
+| [SkiROS2 arXiv:2306.17030](https://arxiv.org/abs/2306.17030) | reference skill implementation |
+| ISO 10218-1/-2:2025, ISO 9787, ISO 8373 | safety, coordinate systems, vocabulary |
+| `metaverse-specs/vision/` | structural template in this repository |
+
+---
+
+## 13 Confidence assessment
+
+| Finding | Confidence | Basis |
+|---|---|---|
+| .NET intent API surface and its synchronous limitation | **High** | source read directly at a known merge SHA |
+| OPC 40010-1 has no motion verbs | **High** | full type inventory enumerated; PR body concurs |
+| No OPC 40010-2/-3 in formal development | **Medium-High** | authoritative index + repo both negative; cannot rule out a non-public draft |
+| URML is single-maintainer, pre-1.0, no `moveJ`/`moveL` | **High** | repository and `GOVERNANCE.md` inspected; contradicts secondary sources, which were wrong |
+| Part 10 is the idiomatic async pattern | **High** | normative text quoted from Part 10 and Part 4 |
+| Cancel service ≠ operation cancellation | **High** | direct quotation from Part 4 |
+| No OPC UA spec defines motion queueing/blending | **Medium-High** | search across published specs; absence is harder to prove |
+| Spatial type NodeIds and `ThreeDFrame` layout | **High** | verified from `.bsd` and cross-checked against two NodeSets |
+| No quaternion type in OPC UA | **High** | searched core, RSL, GPOS, Robotics |
+| A/B/C = roll/pitch/yaw only via RSL / ISO 9787 | **Medium-High** | RSL is the only normative assignment found |
+| OPC UA method calls are non-safety-rated | **High** | OPC 40010-1 §7.7.1 quoted verbatim |
+| Whether OPC UA Safety certifies *method* invocation | **Low** | not resolved; conservative position adopted |
+| Vendor motion primitives and blending | **High** | vendor documentation; strong convergence |
+| OPC UA latency 10–30 ms, 10–100 Hz | **Medium** | consistent across sources, deployment-dependent |
+| ISO 10218:2025 content | **Medium** | standards are paywalled; from secondary summaries and cross-references |
+
+**Overall: Medium-High.** The technical foundations — API surface, OPC UA patterns, spatial types,
+vendor landscape — are verified against primary sources. The two soft spots are paywalled ISO content
+(summarised second-hand) and the unprovable negative about non-public VDMA drafts. Neither changes the
+recommended shape; both are flagged as pre-plan actions.
diff --git a/metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md b/metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md
new file mode 100644
index 0000000..cd05fb2
--- /dev/null
+++ b/metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md
@@ -0,0 +1,1027 @@
+# OPC UA — Robot Intent
+
+> Status: Working-group draft (Release 0.1.0). This document, together with `Opc.Ua.RobotIntent.NodeSet2.xml` and `Opc.Ua.RobotIntent.NodeIds.csv`, defines an OPC UA information model for **commanding a robot at the level of task intent** — move there, grasp that, pick from here, place at that — with a lifecycle that survives the minutes such work actually takes.
+>
+> Nothing here is normative, official, or endorsed by the OPC Foundation, VDMA or any robot manufacturer; namespace URIs and NodeIds are **provisional** and for prototyping only. The prior art, the gaps this model fills, and the decisions those gaps forced are recorded in the companion research report, [`OPC-UA-Robot-Intent-Research.md`](OPC-UA-Robot-Intent-Research.md).
+
+---
+
+## 1 Scope
+
+This specification defines an OPC UA information model that lets a Server describe:
+
+- **what a robot can be asked to do** — as a declared, machine-readable set of intents rather than a convention a client must know in advance;
+- **how to ask it** — one submission per intent, or an ordered mission of them;
+- **what happens next** — a lifecycle a client can observe from admission through queueing, execution and cancellation to a terminal result;
+- **what the request refers to** — the frames, tools, locations, axes, signals and programs that give a pose or a place its meaning;
+- **when the robot will refuse** — the operational modes, the command authority, and the boundary between this interface and the safety system that is never crossed.
+
+### 1.1 Motivation
+
+OPC 40010-1 describes robot **topology** in detail — the motion device system, its axes, its power trains, its controller, its safety states. It defines **no motion verbs at all**. Its entire actuation surface is `Start`, `Stop` and the loading of a named program over two state machines. A conformant client can discover everything about a robot's construction and cannot ask it to move anywhere.
+
+The consequence is that every robot integration above the level of "run program 7" is bespoke. Two Servers can be fully conformant to OPC 40010-1 and still share no way to express *move the tool to this pose*. The verbs exist — every vendor has joint, linear and circular moves, a speed, an acceleration, a blend, a tool frame and a work frame — but they exist only in vendor languages that do not interoperate.
+
+This specification supplies the verbs, and nothing else, so that the two compose rather than compete.
+
+There is a second gap, and it is the harder one. A motion takes seconds; a pick takes a minute. An OPC UA `Call` cannot stay open that long — Session timeouts, SecureChannel re-keying and transport timeouts all bound it, and OPC 10000-4 is explicit that if the Session ends the result is discarded *"independent of the task actually performed at the Server"*. A synchronous method that commands a robot is therefore not merely inelegant; it loses the outcome of work that has already physically happened. Clause 6 addresses this, and it is the reason the model is shaped the way it is.
+
+### 1.2 Motivating use cases
+
+- **Task-level cell control.** A cell controller sequences a robot, a fixture and a conveyor without embedding vendor motion code, because the robot's capability is declared and its verbs are portable.
+- **Planner and agent integration.** A planner — symbolic, learned, or a language model — emits intents against a declared vocabulary, and receives structured failures it can re-plan against rather than a vendor error string.
+- **Mixed-fleet work cells.** Two robots from different manufacturers execute the same mission definition, because the mission is expressed in intents and each Server translates into its own controller's language.
+- **Long-running supervised operation.** A supervisory system submits a mission, watches it progress, revises the part that has not yet been committed, and cancels it cleanly when the upstream process changes.
+- **Auditable commanding.** Every intent records which Session commanded it, when, with what arguments and to what outcome. Part 10 carries that in `ProgramDiagnostic`, which it leaves Optional; §6.1 promotes it to Mandatory here, because a capability a specification advertises cannot rest on a member a conformant Server may omit.
+
+### 1.3 What this specification does not do
+
+Two of these are permanent boundaries. Neither is a deferral.
+
+- It is **not** a real-time control channel. Trajectory *execution* is in scope — a client hands over a whole time-parameterised path and the robot's own motion kernel runs it (§6.8) — and where a high-rate channel is genuinely needed, this specification **brokers** it (§6.9). What it does not do is carry the samples: it defines no transport, closes no control loop, and runs at OPC UA rates. §4.3 draws that line as a normative limit rather than a caution.
+- It is **not** a safety function, and it is **not** safety-rated. This is not a scoping preference; it is what the technology permits. OPC 10000-15 carries cyclic safety data from a provider to a consumer, and a client calling a Method has no way to supply safety-rated arguments — so no Method in this or any other companion specification can be a safety function. What **is** in scope is *awareness*: the model reports what the safety system is enforcing, and a Server **shall** refuse work that would exceed it. Clause 10 states both halves precisely.
+
+Neither statement is about the importance of the omitted capability. The first is a division of labour with the layer beneath; the second is the boundary of what an OPC UA Method can be.
+
+### 1.4 Capabilities and versioning
+
+Release 0.1.0 covers the intent vocabulary — joint, linear and circular moves, trajectories, Cartesian paths, force-controlled moves, grasping, picking and placing, tool change, output, program call, waiting, and six application processes — together with the execution lifecycle, queueing and blending, cancellation, missions with a committed base, a revisable horizon and a step graph, command authority, safety awareness, the robot's kinematic description, real-time channel brokerage, and capability declaration.
+
+The NodeSet declares exactly one `RequiredModel` — the base OPC UA namespace — so a Server can adopt it without pulling in any companion model. A Server implements the facets it can honour and declares the rest false; only **RI-Base** is mandatory (§12.2).
+
+---
+
+## 2 Normative references
+
+- **OPC 10000-3, -4, -5** — Address Space Model, Services, Information Model. The base UA namespace is the only required model. OPC 10000-4 §5.12.2 bounds what a Method call can do, and OPC 10000-4 §5.7.5 defines the `Cancel` Service that §6.5 distinguishes this specification's cancellation from.
+- **OPC 10000-10** — Programs. `ProgramStateMachineType` is the base type of `IntentOperationType` and `MissionType`, and supplies the transition events, the terminal result object and the invocation diagnostics this specification relies on rather than re-inventing.
+- **OPC 10000-6** — Mappings. Structure subtyping and `ExtensionObject` encoding of the intent hierarchy (§5.3).
+- **ISO 8373** — *Robots and robotic devices — Vocabulary*. Source of the terms in clause 3; where this document uses *pose*, *path*, *trajectory*, *tool centre point*, *end effector* or *workspace*, it uses them as ISO 8373 defines them.
+- **ISO 9787:2013** — *Robots and robotic devices — Coordinate systems and motion nomenclatures*. Source of the frame roles in `FrameRoleEnum`, and of the roll-pitch-yaw convention used by the conversion in Annex C.
+- **ISO 10218-1** and **ISO 10218-2** — *Robotics — Safety requirements*. Source of the operational modes in `OperationalModeEnum` and of the single-point-of-control requirement discussed in clause 10.
+- **IEC 60204-1** — *Safety of machinery — Electrical equipment of machines*. Source of the stop categories that clause 10 states this interface does **not** select.
+- **IEC 61800-5-2** — *Adjustable speed electrical power drive systems — Safety requirements: Functional*. Source of the safe motion functions named by `SafeMotionFunctionEnum` (§5.7.2).
+- **IEC 61131-3** — *Programmable controllers — Programming languages*. Source of the step-and-transition divergence model the mission graph follows (§7.4).
+
+Informative alignments — PLCopen Motion Control, VDA 5050, ROS 2 actions, MoveIt, the vendor motion languages, the Capability–Skill–Service model, OPC UA FX, and the OPC UA joining and tightening specifications — are listed in Annex D. They are **not** normative references and impose no dependency, notwithstanding that `BufferModeEnum` and `BlockingModeEnum` adopt their vocabularies deliberately and unchanged.
+
+Two are worth naming here because a Server may resolve references into them, and neither is a `RequiredModel`:
+
+- **OPC 40450** and **OPC 40451** — *OPC UA for Industrial Joining Technologies*, base and tightening systems. `FastenIntentDataType.Joint` references a joint in that model where one is implemented (§5.4.2). Where it is not, the intent's own parameters stand alone.
+- **OPC 10000-80 to -84** — *OPC UA FX*. The open transport a real-time channel may name (§6.9). This specification defines none of its content.
+
+---
+
+## 3 Terms, definitions and abbreviations
+
+| Term | Definition |
+|---|---|
+| **Intent** | A single task-level request — one motion, one grasp, one pick. Modelled as a subtype of `IntentDataType`. It is what a client submits and what a mission step holds. |
+| **Intent operation** | One submitted intent, tracked to completion. Modelled as `IntentOperationType`, a Part 10 program instance created per submission. |
+| **Mission** | An ordered sequence of intents submitted and tracked as a unit. Modelled as `MissionType`. |
+| **Base** (of a mission) | The prefix of released steps. Committed, possibly already executing, and immutable (§7.2). |
+| **Horizon** (of a mission) | The suffix of unreleased steps. Provisional, and revisable by `UpdateMission` (§7.2). |
+| **Pose** | A rigid-body placement — three of position and three of orientation — expressed relative to a named frame. Carried by `Pose3DDataType`, whose orientation is a unit quaternion ordered (x, y, z, w) per §5.2. |
+| **Frame** | A named right-handed Cartesian coordinate system a pose is expressed relative to. Frames form a tree through `HasFrameParent`. Modelled as `CoordinateFrameType`. |
+| **Tool centre point (TCP)** | The origin of a tool frame; the point a motion intent drives to its target. OPC 40010-1 defines no such concept, so `ToolType.TcpFrame` supplies one. |
+| **Blending** | Rounding the corner between two motions rather than stopping on the first one's target. Requested by `BlendDataType`, and by the blending values of `BufferModeEnum`. |
+| **Buffer mode** | How a newly submitted intent relates to the one already executing — abort it, queue behind it, or blend into it. `BufferModeEnum`. |
+| **Blocking mode** | Whether an intent tolerates motion and other intents alongside it. `BlockingModeEnum`. |
+| **Command authority** | The exclusive right, held by at most one Session, to submit intents to one controller (clause 8). It is an arbitration mechanism between clients and **not** the single point of control that ISO 10218-2 requires. |
+| **Terminal state** | An `ExecutionStateEnum` value from which execution does not resume: `Succeeded`, `Failed`, `Cancelled`, or `Retriable`. |
+| **Conformance unit** | The smallest individually testable requirement of this specification. Grouped into facets rather than claimed one by one. |
+| **Facet** | A named, individually claimable set of conformance units — a building block, not a complete claim. Clause 12.2 defines them. |
+| **Profile** | A named set of facets describing one plausible robot Server, claimed by publishing its URI in `Server/ServerCapabilities/ServerProfileArray`. Clause 12.3 defines them. |
+
+---
+
+## 4 Overview and concepts
+
+### 4.1 The layered contract
+
+This specification occupies one band and touches neither the band above nor the two below.
+
+```mermaid
+flowchart TB
+ A["Planner, cell controller, agent, fleet manager"]
+ B["Robot Intent - this specification
task-level verbs, whole motions"]
+ C["Vendor motion language
URScript, RAPID, KRL, TP, INFORM"]
+ D["Real-time channel
RTDE, EGM, FRI, RSI"]
+ E["Safety-rated controller
independent of this interface"]
+ F["OPC 40010-1 topology
optional binding, Annex B"]
+ A --> B
+ B --> C
+ C --> D
+ F -.-> B
+ E -.-> C
+```
+
+A Server implementing this specification **shall** translate intents into whatever its controller actually executes. It **shall not** require a client to know which controller that is: the point of the model is that the same intent, submitted to two Servers driving robots from two manufacturers, produces the same physical outcome within the tolerance each robot is capable of.
+
+The safety layer is beside and beneath this interface and is never mediated by it. That relationship is normative and is stated in clause 10.
+
+### 4.2 Intent, not trajectory
+
+An intent says **what** is to be achieved and constrains **how far** the Server may go in achieving it. It does not say how. The Server owns path planning, inverse kinematics, singularity avoidance, collision checking and the choice of configuration.
+
+This is what makes the model portable. Every vendor already solves those problems, differently and well; a specification that dictated the solution would be implementable by nobody. `MotionConstraintsDataType` is therefore a set of **bounds**, not a plan, and a Server **shall** clamp a request to what the robot is configured to permit rather than refuse it — except where clause 10 requires refusal.
+
+The same reasoning applies to blending. `BlendDataType.Radius` is a request in metres because that is the only unit two vendors share; controllers that expose a unitless blend scale map it as best they can, and a Server that cannot honour the exact radius **shall** still succeed. A client that needs the exact path uses `Exact` termination, which every vendor implements identically.
+
+### 4.3 What this interface carries, and what it brokers
+
+OPC UA method invocation is not deterministic and, in the deployments this model is written for, completes in tens of milliseconds. Vendor real-time channels run two to four orders of magnitude faster on dedicated transports. That difference is not something a specification can argue away, so this model divides the work rather than pretending the gap is not there.
+
+**Carried here — one submission, executed by the robot.** A trajectory, a Cartesian path or a force-controlled move is handed over *whole* and run by the robot's own motion kernel (§6.8). The round trip happens once, at submission, so the transport's latency bounds how quickly work can be *started* and never how accurately it is *executed*. This is the same shape as `FollowJointTrajectory` in ROS and the buffered path function blocks of PLCopen, and it is why trajectory execution belongs here while trajectory streaming does not.
+
+**Brokered — described, leased, and left alone.** Where a client genuinely needs a high-rate channel — visual servoing, force tracking, conveyor following — the Server describes one and leases it (§6.9). The samples travel on that channel and never through this interface.
+
+A Server **shall not** present this interface as a real-time control channel, and a client **shall not** use it as one. In particular:
+
+- servo-level or joint-cyclic control **shall not** be attempted through repeated submission;
+- a closed force or impedance loop requiring a bounded control period **shall** use a brokered channel, not `ForceIntentDataType`, which commands a *move until contact* and not a continuous loop;
+- conveyor tracking, seam tracking and any other motion slaved to an external signal at rate **shall** use a brokered channel or the robot's own facility.
+
+`IntentOperationType.CurrentPose` exists so a client can *watch* a motion. It is a status report delivered at whatever rate the client's Subscription asks for, and using it to close a control loop is outside this specification.
+
+### 4.4 Architecture
+
+```mermaid
+flowchart LR
+ S["Server"] --> R["RobotIntent
well-known entry point"]
+ R --> CTL["IntentController
one per robot"]
+ CTL --> CAP["Capabilities"]
+ CTL --> REF["Frames, Tools, Locations,
Axes, Outputs, Programs"]
+ CTL --> OPS["Intents
IntentOperation per submission"]
+ CTL --> MIS["Missions
Mission per submission"]
+ MIS --> OPS
+```
+
+A client browses `Server/RobotIntent/Controllers` to find every robot it may command, reads that controller's `Capabilities` to learn what it accepts, resolves the frames and locations its intents will refer to, and then submits.
+
+---
+
+## 5 Information model
+
+### 5.1 Type hierarchy
+
+| Type | NodeId | Subtype of |
+|---|---|---|
+| `RobotIntentRootType` | `ns=1;i=1001` | `BaseObjectType` |
+| `IntentControllerType` | `ns=1;i=1002` | `BaseObjectType` |
+| `IntentOperationType` | `ns=1;i=1003` | `ProgramStateMachineType` (`i=2391`) |
+| `MissionType` | `ns=1;i=1004` | `ProgramStateMachineType` (`i=2391`) |
+| `IntentCapabilitiesType` | `ns=1;i=1005` | `BaseObjectType` |
+| `CoordinateFrameType` | `ns=1;i=1006` | `BaseObjectType` |
+| `ToolType` | `ns=1;i=1007` | `BaseObjectType` |
+| `LocationType` | `ns=1;i=1008` | `BaseObjectType` |
+| `AxisType` | `ns=1;i=1009` | `BaseObjectType` |
+| `OutputSignalType` | `ns=1;i=1010` | `BaseObjectType` |
+| `ProgramType` | `ns=1;i=1011` | `BaseObjectType` |
+| `SafetyStateType` | `ns=1;i=1012` | `BaseObjectType` |
+| `RealTimeChannelType` | `ns=1;i=1013` | `BaseObjectType` |
+| `RobotDescriptionType` | `ns=1;i=1014` | `BaseObjectType` |
+
+Annex A is the authoritative node reference and carries every member with its DataType, ValueRank and ModellingRule.
+
+### 5.2 Poses, frames and units (normative)
+
+`Pose3DDataType` (`ns=1;i=3050`) carries `FrameId`, a `Position` of three `Double` in **metres**, and an `Orientation` of four `Double` forming a **unit quaternion ordered (x, y, z, w)**.
+
+Four rules make this unambiguous, and a Server **shall** satisfy all of them.
+
+1. Every frame in this model is **right-handed**.
+2. Position is in **metres**; joint targets are in **radians** for a `Revolute` axis and **metres** for a `Prismatic` one; force is in **newtons**; time is in **milliseconds** where carried as `Duration`. These units are fixed by this specification and are **not** negotiable per-instance. `Pose3DDataType` appears as a Method argument, where no `EUInformation` property can reach it, so a per-instance unit would be undeliverable.
+3. `Orientation` **shall** be normalised. A Server receiving a quaternion whose norm differs from 1 by more than 1e-6 **shall** reject the intent with `ParameterInvalid`.
+4. `FrameId` names a `CoordinateFrameType` instance under the controller's `Frames` folder. An empty `FrameId` means the Server's default work frame.
+
+Quaternions are used because OPC UA defines no quaternion DataType anywhere, and because the `A`, `B`, `C` fields of the core `ThreeDOrientation` carry no convention of their own — the only normative assignment of meaning to them is external to the base specification. A quaternion has no such ambiguity, no gimbal degeneracy, and is what robot controllers and scene descriptions already hold internally. Annex C gives the normative bidirectional conversion to `ThreeDFrame`, so a Server that also speaks OPC 40010-1 or a spatial-location model can move between the two without inventing a convention.
+
+### 5.3 The intent hierarchy
+
+Intents are a **DataType hierarchy**, not one Method per verb.
+
+```mermaid
+flowchart TB
+ I["IntentDataType (abstract)"]
+ M["MotionIntentDataType (abstract)"]
+ P["ProcessIntentDataType (abstract)"]
+ I --> M
+ M --> JM["JointMoveIntentDataType"]
+ M --> LM["LinearMoveIntentDataType"]
+ M --> CM["CircularMoveIntentDataType"]
+ M --> TJ["TrajectoryIntentDataType"]
+ M --> CP2["CartesianPathIntentDataType"]
+ M --> FI["ForceIntentDataType"]
+ M --> P
+ P --> AW["ArcWeldIntentDataType"]
+ P --> SW["SpotWeldIntentDataType"]
+ P --> DI["DispenseIntentDataType"]
+ P --> FA["FastenIntentDataType"]
+ P --> PA["PalletiseIntentDataType"]
+ P --> SF["SurfaceFinishIntentDataType"]
+ I --> G["GraspIntentDataType"]
+ I --> R["ReleaseIntentDataType"]
+ I --> PK["PickIntentDataType"]
+ I --> PL["PlaceIntentDataType"]
+ I --> T["ToolChangeIntentDataType"]
+ I --> O["SetOutputIntentDataType"]
+ I --> C["CallProgramIntentDataType"]
+ I --> W["WaitIntentDataType"]
+```
+
+Three consequences follow, and each is the reason for the choice:
+
+- **A single intent and a mission step are the same shape.** `MissionStepDataType.Intent` is an `IntentDataType`, so nothing has to be expressed twice.
+- **Extension is subtyping.** A vendor or a later part adds an intent by deriving from `IntentDataType`. It is then carried, queued, cancelled and reported by the existing machinery without a new Method.
+- **Discovery is a read, not a probe.** `IntentCapabilitiesType.SupportedIntents` names each accepted DataType. A client learns what a robot accepts by reading one Variable, rather than by browsing for BrowseNames and inferring support from their presence.
+
+`IntentDataType` carries what every intent needs: `IntentId`, `Label`, `BufferMode` and `BlockingMode`. `MotionIntentDataType` adds `ToolFrame`, `Constraints` and `Blend` — the members that are meaningless for `SetOutput` and essential for a move.
+
+Fields whose DataType is one of the two abstract structures are emitted with `AllowSubTypes="true"`, so a client decoding a mission does not have to infer polymorphism from the abstractness of a DataType.
+
+### 5.4 Motion intents
+
+`JointMoveIntentDataType` (`ns=1;i=3055`) interpolates in joint space. The tool centre point's path is not controlled and **shall not** be relied on. It carries either explicit `JointTargets` or a `TargetPose`, and `HasJointTargets` decides which — a Boolean discriminator rather than a sentinel, so that neither field has to encode "unset".
+
+Giving a pose rather than joint values is the *"move there, you choose how"* case: the Server solves the kinematics and selects a configuration. A Server **shall** reject a `JointMoveIntentDataType` whose `HasJointTargets` is true and whose `JointTargets` length differs from `IntentCapabilitiesType.AxisCount`, with `ParameterInvalid`.
+
+`LinearMoveIntentDataType` (`ns=1;i=3056`) drives the tool centre point along a straight line. `CircularMoveIntentDataType` (`ns=1;i=3057`) drives it along the arc through `ViaPoint` to `Target`; only the **position** of `ViaPoint` defines the arc, and a Server **shall** ignore its orientation.
+
+The three correspond to the instructions every vendor already has, which is why they are three and not one:
+
+| This specification | UR | ABB | KUKA | FANUC | Yaskawa |
+|---|---|---|---|---|---|
+| `JointMoveIntentDataType` | `movej` | `MoveJ` / `MoveAbsJ` | `PTP` | `J` | `MOVJ` |
+| `LinearMoveIntentDataType` | `movel` | `MoveL` | `LIN` | `L` | `MOVL` |
+| `CircularMoveIntentDataType` | `movec` | `MoveC` | `CIRC` | `C` | `MOVC` |
+
+### 5.4.1 Paths, trajectories and contact
+
+Three further motion intents cover the work that a single target pose cannot express.
+
+`CartesianPathIntentDataType` (`ns=1;i=3074`) carries a list of `PathWaypointDataType` (`ns=1;i=3073`), each a pose with the blend that applies at it. It carries **no timing**: the Server paces it from `Constraints`. This is the portable form of a taught path, and per-waypoint blending is exactly what distinguishes it from a sequence of separate linear moves — the robot need not stop between waypoints.
+
+`TrajectoryIntentDataType` (`ns=1;i=3072`) carries `TrajectoryPointDataType` (`ns=1;i=3070`) points, each with `TimeFromStart` and per-axis positions, optionally velocities and accelerations. Timing is what makes it a trajectory rather than a path. It also carries a `PathTolerance` and a `GoalTolerance`, both `MotionToleranceDataType` (`ns=1;i=3071`), and a `GoalTimeTolerance`, which is a `Duration` — lateness is one number, not a pose deviation. Between them, "did it work?" has an answer the client set rather than one the Server chose.
+
+A Server **shall** reject a trajectory whose points are not in ascending `TimeFromStart` order, or whose `Positions` length differs from `AxisCount`, with `ParameterInvalid`. Where `MaxTrajectoryPoints` is non-zero, it **shall** reject a longer trajectory with `ParameterInvalid`.
+
+The whole trajectory is submitted in one call and executed by the robot's own motion kernel. That is the point: the transport is on the critical path once, at submission, and never during execution (§4.3).
+
+`ForceIntentDataType` (`ns=1;i=3075`) travels along `Direction` until `ContactForce` is reached or `MaxDistance` is exhausted. Exhausting the distance without contact **shall** be reported as `Failed` with `ObjectNotFound` — the intent was to touch something, and not touching it is not success. `HoldForce` keeps the robot pressing after contact instead of stopping.
+
+A Server **shall** report `ForceControlSupported` false unless the robot genuinely regulates force. Accepting a force intent and ignoring the force would tell a client its part was pressed when it was only approached.
+
+### 5.4.2 Process intents
+
+`ProcessIntentDataType` (`ns=1;i=3076`) is the abstract base of the intents that run an application process along a path. Beyond each process's own parameters it carries two things every process needs: `ProcessProgram`, referencing the procedure the equipment already holds, and `Attributes` for what this specification has not standardised.
+
+| Intent | NodeId | Covers |
+|---|---|---|
+| `ArcWeldIntentDataType` | `ns=1;i=3077` | voltage, wire feed and travel speed, gas pre- and post-flow, arc start delay, crater fill, weave, seam tracking |
+| `SpotWeldIntentDataType` | `ns=1;i=3078` | weld schedule, gun force, approach and retract distance, stack thickness, tip dress |
+| `DispenseIntentDataType` | `ns=1;i=3079` | flow rate, trigger-on and trigger-off distance, bead width, material temperature, purge |
+| `FastenIntentDataType` | `ns=1;i=3080` | joint reference, program number, torque, angle, snug torque |
+| `PalletiseIntentDataType` | `ns=1;i=3081` | pattern, layer, row, column, item orientation |
+| `SurfaceFinishIntentDataType` | `ns=1;i=3082` | contact force, feed rate, tool speed, step-over |
+
+Each parameter set is the subset the vendor languages agree on — ABB `seamdata`/`welddata`/`weavedata`, FANUC weld schedules and KUKA ArcTech for arc welding, and their equivalents elsewhere. Where an installation works to a welding procedure specification, `WeldProcedureRef` names it and this specification does not restate its content.
+
+Two of these deserve their reasoning stated.
+
+**Fastening is deliberately thin.** OPC 40450 and OPC 40451 already define industrial joining and tightening in full, including step-wise results and traces. `FastenIntentDataType.Joint` therefore references the joint in that model and the result belongs there. Restating torque strategies here would create a second definition of a fact that specification already owns — the same reason `PickIntentDataType.Source` references a `Location` node instead of naming a station in a string.
+
+**Palletising references a pattern, not a computed pose.** `Pattern` is a `LocationType`, so the geometry has one definition a client can read and subscribe to, rather than being recomputed from indices independently on both sides and disagreeing.
+
+`WeaveShapeEnum` (`ns=1;i=3018`) gives the oscillation across an arc weld seam: `None`, `Sine`, `Zigzag`, `Trapezoid`.
+
+### 5.5 Manipulation intents
+
+`GraspIntentDataType` and `ReleaseIntentDataType` actuate an end effector. `Force` and `Width` are requests: an end effector that cannot regulate force **shall** ignore `Force` and still succeed, because refusing would make the intent unusable on the majority of grippers that are open or closed and nothing else.
+
+`PickIntentDataType` and `PlaceIntentDataType` reference a `LocationType` **node** through `Source` and `Destination`. They do not name a station in a string. A location therefore has exactly one definition — its pose, its occupancy, what it holds and how much of it — which a client can read, subscribe to and reason about. A free-text station identifier would be a second definition of the same fact, able to disagree with the first.
+
+`ToolChangeIntentDataType` references the `ToolType` to fit; a null `Tool` releases the fitted tool and fits nothing.
+
+### 5.6 Auxiliary intents
+
+`SetOutputIntentDataType` writes an `OutputSignalType` node, so the signal's range, unit and meaning are described once in the address space instead of being implied by a line name. `Value` is `BaseDataType` and **shall** match the signal's own DataType.
+
+`CallProgramIntentDataType` runs a `ProgramType` held on the controller. It is the escape hatch for capability this model does not describe, and the bridge to the programs an OPC 40010-1 task control already exposes (Annex B).
+
+`WaitIntentDataType` waits for a duration, for a signal, or for both. A mission needs it to express a rendezvous with something the robot does not control; without it a client has to hold the queue open from outside, which defeats the point of submitting a mission at all.
+
+`Signal` is bounded so that §11.3 has something to check: it **shall** resolve either to an `OutputSignalType` instance under the controller being commanded, or to a Variable of DataType `Boolean` under it, and a Server **shall** refuse anything else with `ParameterInvalid`. A NodeId-valued member that no rule constrains cannot be validated, and an unvalidated NodeId is the surface §11.3 exists to close.
+
+### 5.7 Reference objects
+
+`CoordinateFrameType` instances form a tree through `HasFrameParent`, so a pose given in one frame can be re-expressed in another by composing the transforms along the path between them. `Role` follows ISO 9787, which standardises *which* frames exist; the transform between them is carried explicitly because no standard says how to calibrate it.
+
+`ToolType.TcpFrame` **shall** reference a `CoordinateFrameType` whose `Role` is `Tool`. At most one `ToolType` instance under a controller **shall** have `Fitted` true at any time.
+
+`AxisType.Index` fixes the position of that axis in `JointMoveIntentDataType.JointTargets`, and `Kind` fixes the unit of that entry. The indices of the axes under one controller **shall** be the contiguous range `0` to `AxisCount − 1`.
+
+### 5.7.0 What the controller itself reports
+
+Four members of `IntentControllerType` say what the robot is doing right now, and each is read-only and normative.
+
+- `OperationalMode` is the robot's own report of the mode in force. §10.2 gates submission on it and forbids commanding it.
+- `Ready` is true exactly when the Server would admit a well-formed intent from the Session that holds command authority. A Server **shall** report it false whenever §6.2 or §10.4 would refuse for a reason that does not depend on the intent — outside `Automatic` or `AutomaticExternal`, under an emergency or protective stop, or with `SafetyControllerOk` false. It exists so a client can see that submitting is pointless without submitting to find out; it is a **hint about the Server**, and a Server **shall not** treat a client's having read it as licence to skip any check of §6.2.
+- `ActiveIntent` references the `IntentOperationType` instance whose `ExecutionState` is `Executing` or `Cancelling`, and is null when none is. Where the executing intent belongs to a mission, `ActiveMission` references that mission's `MissionType` instance; otherwise `ActiveMission` is null.
+- `ControlOwner` reports the Session holding command authority, or null. Clause 8 governs it.
+
+None of these is a substitute for the per-operation state of §6.3. Two members may not report the state of one intent: the operation's own state machine decides, and these summarise it.
+
+### 5.7.1 `RobotDescriptionType`
+
+`RobotDescriptionType` (`ns=1;i=1014`) carries enough of the robot's construction for a client to plan against it without a second specification: a `KinematicChain` of `KinematicJointDataType` (`ns=1;i=3084`) from the base outwards, a `MountingPose`, a `ReachRadius`, a `PayloadLimit`, and ceilings on tool centre point speed and acceleration.
+
+Each `KinematicJointDataType` names the `AxisType` it corresponds to, its `Kind`, the `OriginTransform` of its frame within its predecessor's at zero position, and the unit `AxisVector` it rotates about or translates along.
+
+This is **additive, not duplicative**. OPC 40010-1 describes a robot's topology and its axes in detail and defines no kinematic chain an inverse-kinematics solver could use, and no tool centre point at all. Where OPC 40010-1 is also implemented, its axis description is the fuller one and Annex B fixes which side decides.
+
+### 5.7.2 `SafetyStateType`
+
+`SafetyStateType` (`ns=1;i=1012`) reports what the robot's safety system is doing: `ActiveFunction`, `EmergencyStopActive`, `ProtectiveStopActive`, `SafeSpeedLimitActive`, `SafeSpeedLimit`, `SafetyControllerOk` and a human-readable `LastStopReason`.
+
+`SafeMotionFunctionEnum` (`ns=1;i=3013`) names the function being enforced, using the vocabulary of IEC 61800-5-2: `None`, `Sto`, `Ss1`, `Ss2`, `Sos`, `Sls`, `Slp`, `Sdi`, `Sbc`.
+
+Every member is **read-only and a report**. The safety system enforces these independently of this interface and remains effective when the Server is unreachable. Clause 10 states what a Server must do with them and what a client may not conclude from them.
+
+### 5.7.3 `RealTimeChannelType`
+
+`RealTimeChannelType` (`ns=1;i=1013`) describes a high-rate channel the Server can offer, so a client can find and open one: `Transport`, `EndpointUrl`, `Initiator`, `NominalRate`, `PayloadDescriptor`, `RequiredMode`, `Available`, and the current `LeaseHolder` and `LeaseExpiry`.
+
+`RealTimeTransportEnum` (`ns=1;i=3014`) names the transport: `Rtde`, `Egm`, `Fri`, `Rsi`, `MotoRos2`, `OpcUaFx`, `Other`. Of these only `OpcUaFx` — OPC UA FX, OPC 10000-80 to -84 — is an OPC Foundation specification; the rest are vendor channels this model describes without defining.
+
+`ChannelInitiatorEnum` (`ns=1;i=3015`) says which end opens the connection: `Server` or `Client`. It is stated rather than left to the reader because getting it wrong is the usual reason a first connection attempt fails.
+
+§6.9 defines the lease.
+
+### 5.8 Enumerations
+
+The values below are normative. Where a value is cited as interoperable with another specification, renumbering it breaks that claim.
+
+**`ExecutionStateEnum`** (`ns=1;i=3001`) — tabulated with its Part 10 pairing in §6.3: `Accepted` 0, `Queued` 1, `Executing` 2, `Suspended` 3, `Cancelling` 4, `Succeeded` 5, `Failed` 6, `Cancelled` 7, `Retriable` 8.
+
+**`BufferModeEnum`** (`ns=1;i=3002`) — the values of PLCopen `MC_BufferMode`, adopted unchanged.
+
+| Value | | Meaning |
+|---|---|---|
+| `Aborting` | 0 | Abort what is executing and start immediately. The default, and always accepted. |
+| `Buffered` | 1 | Queue; start when the predecessor succeeds. |
+| `BlendingLow` | 2 | Blend at the lower of the two boundary speeds. |
+| `BlendingPrevious` | 3 | Blend at the predecessor's boundary speed. |
+| `BlendingNext` | 4 | Blend at the successor's boundary speed. |
+| `BlendingHigh` | 5 | Blend at the higher of the two boundary speeds. |
+
+**`BlockingModeEnum`** (`ns=1;i=3003`) — the VDA 5050 `blockingType` matrix.
+
+| Value | | Motion may continue | Other intents may run |
+|---|---|---|---|
+| `None` | 0 | yes | yes |
+| `Soft` | 1 | no | yes |
+| `Single` | 2 | yes | no |
+| `Hard` | 3 | no | no |
+
+**`TerminationModeEnum`** (`ns=1;i=3004`) — `Exact` 0 (come to rest on the target), `Blend` 1 (round the corner into the next motion).
+
+**`ReleaseModeEnum`** (`ns=1;i=3005`) — `Drop` 0 (open where it is), `Place` 1 (set down under control at the target), `Handover` 2 (retain until a receiving party has taken it).
+
+**`ApproachModeEnum`** (`ns=1;i=3006`) — `Default` 0 (the Server chooses), `ToolZ` 1 (along the tool's own Z axis), `Top` 2 (from above, in the target's frame), `Side` 3 (laterally, in the target's frame).
+
+**`FrameRoleEnum`** (`ns=1;i=3007`) — the coordinate systems ISO 9787 standardises: `World` 0, `Base` 1, `MechanicalInterface` 2 (the flange), `Tool` 3, `Object` 4 (a workpiece frame), `Other` 5.
+
+**`OperationalModeEnum`** (`ns=1;i=3008`) — the ISO 10218-1 modes, numbered as OPC 40010-1 numbers them: `Other` 0, `ManualReducedSpeed` 1, `ManualHighSpeed` 2, `Automatic` 3, `AutomaticExternal` 4. Clause 10 permits submission only in the last two.
+
+**`StopModeEnum`** (`ns=1;i=3010`) — the values of `PossibleStopModes` in OPC 40010-1: `OnPath` 1, `EndOfCycle` 2, `ProcessStop` 3, `QuickStop` 4, `EndOfInstruction` 5. §10.3 states what these do **not** mean.
+
+**`AxisKindEnum`** (`ns=1;i=3011`) — `Revolute` 0 (its joint target is in radians), `Prismatic` 1 (in metres).
+
+**`MissionUpdateResultEnum`** (`ns=1;i=3012`) — `Accepted` 0, `Outdated` 1 (the update identifier was not greater), `BaseConflict` 2 (it would have altered a released step), `UnknownMission` 3, `Rejected` 4 (refused for a reason the Server states in `Message`).
+
+**`IntentFailureEnum`** (`ns=1;i=3009`) — the reason an intent did not succeed. The set is small and diagnosable on purpose: a client decides whether to retry, re-plan or escalate from this value alone.
+
+| Value | | Meaning |
+|---|---|---|
+| `None` | 0 | No failure; reported on success. |
+| `Unreachable` | 1 | The target lies outside the reachable workspace. |
+| `Kinematics` | 2 | No kinematic solution, or a singularity on the path. |
+| `Collision` | 3 | A collision was predicted or detected. |
+| `JointLimit` | 4 | A joint limit would be or was exceeded. |
+| `SpeedLimit` | 5 | The requested speed is not permitted in the active mode. |
+| `ToolMissing` | 6 | The required tool is not fitted or not identified. |
+| `ObjectNotFound` | 7 | The object to act on was not present. |
+| `GraspFailed` | 8 | The object was not acquired, or was lost in transit. |
+| `Timeout` | 9 | Did not complete within its permitted time. |
+| `NotPermittedInMode` | 10 | Refused; the operational mode does not permit it (§10.2). |
+| `ControlNotOwned` | 11 | Refused; the caller does not hold command authority (clause 8). |
+| `CapabilityNotSupported` | 12 | Not implemented, or not in this combination (clause 9). |
+| `ParameterInvalid` | 13 | A parameter was missing, malformed or out of range. |
+| `QueueFull` | 14 | The queue is at `MaxQueueDepth`. |
+| `Superseded` | 15 | An `Aborting` submission or a mission update replaced it. |
+| `HardwareFault` | 16 | A fault in the robot, the end effector or the controller. |
+| `SafetyStop` | 17 | A safety function acted. The safety system decided this, not this interface. |
+| `Other` | 18 | A reason none of the above describes; see `Message`. |
+| `SafetyLimitExceeded` | 19 | Refused; the request would exceed a limit the safety system is enforcing (§10.3). |
+
+**`ErrorPolicyEnum`** (`ns=1;i=3016`) — what a mission does when a step does not succeed (§7.4): `Abort` 0 (the default), `Retry` 1, `Skip` 2, `Fallback` 3, `Compensate` 4.
+
+**`DivergenceKindEnum`** (`ns=1;i=3017`) — how the transitions leaving one step relate, following the divergence of an IEC 61131-3 sequential function chart: `Alternative` 0 (exactly one is taken — an OR divergence), `Parallel` 1 (all are taken and the branches run concurrently — an AND divergence).
+
+---
+
+## 6 Intent lifecycle (normative)
+
+### 6.1 Why an intent is a program instance
+
+A `Call` cannot outlive the Session that made it, and OPC 10000-4 §5.12.2 states that when a Session ends the method result is discarded *"independent of the task actually performed at the Server"*. A robot commanded by a synchronous method therefore keeps moving after the answer has been thrown away — and OPC 10000-10 §4.1 gives the OPC Foundation's own resolution: a Method performs a calculation, a **Program** runs a batch process or a machine tool part program.
+
+`SubmitIntent` accordingly returns as soon as the intent is **admitted**, not when the robot has finished. What it returns is a NodeId: an `IntentOperationType` instance created for that submission, which the client subscribes to for progress and reads for the result.
+
+Building on `ProgramStateMachineType` rather than defining a fresh state machine buys four things this specification then does not have to invent: transition events, a terminal result object that survives the operation, invocation diagnostics recording which Session commanded what, and a lifetime model for the instance itself.
+
+**Two of those are Optional in Part 10, and this specification promotes both.** `IntentOperationType` declares `FinalResultData` and `ProgramDiagnostic` **Mandatory**.
+
+Inheriting them would not have been enough. §6.7 requires the result to be reachable under `FinalResultData`, and §1.2 advertises auditable commanding, which *is* `ProgramDiagnostic` and nothing else. Both would have rested on members a fully conformant Server could omit — so a Server could pass every conformance test while providing neither, and the two claims would be false against a legal implementation. Promoting them is what makes the claims testable rather than aspirational.
+
+A promotion changes the ModellingRule and **nothing else**. Both members are therefore declared exactly as OPC 10000-10 declares them — `FinalResultData` an Object of `BaseObjectType` reached by `HasComponent`, and `ProgramDiagnostic` a Variable of `ProgramDiagnostic2Type` of DataType `ProgramDiagnostic2DataType`, also reached by `HasComponent`. Altering the reference type or the TypeDefinition would declare a *second* member beside the inherited one rather than promote it, and a client written against Part 10 would then find two.
+
+### 6.2 Submission
+
+On `SubmitIntent` a Server **shall**, in this order:
+
+1. Refuse with `ControlNotOwned` if the calling Session does not hold command authority (clause 8).
+2. Refuse with `NotPermittedInMode` if `OperationalMode` is not `Automatic` or `AutomaticExternal` (clause 10).
+3. Refuse with `CapabilityNotSupported` if the intent's DataType is not among `SupportedIntents`, or if its `BufferMode` or `BlockingMode` is not among those the capability entry permits.
+4. Refuse with `ParameterInvalid` if a parameter is missing, malformed or out of range.
+5. Refuse with `QueueFull` if admitting it would exceed `MaxQueueDepth`.
+6. Otherwise create an `IntentOperationType` instance, assign an `IntentId` if the request left it empty, and return both.
+
+A refusal at any of these steps **shall not** create an operation instance and **shall not** move the robot.
+
+**A refusal is an output, not a `StatusCode`.** §10.5 makes refusal an ordinary outcome, so a Server **shall** return `Good` from `SubmitIntent` and report the refusal in its output arguments: `Accepted` false, `Failure` set to the `IntentFailureEnum` value named above, `Message` carrying detail for a human, `IntentId` empty and `Operation` null. On admission it returns `Accepted` true, `Failure` `None`, and the assigned `IntentId` and `Operation`. `SubmitMission` and `Retry` report a refusal the same way.
+
+This is what makes the ordered rules above observable. A Server that signalled a refusal with a Bad `StatusCode` would tell a client only that something went wrong, and the whole point of a small, diagnosable failure set (§5.8) is that a client decides whether to retry, re-plan or escalate from that value alone. A Server **shall not** substitute a Bad `StatusCode` for one of these refusals. `Bad_` codes remain what they always were — the transport, the Session and the Service layer failing — and a client **shall** distinguish the two.
+
+An `IntentId` returned by the Server **shall** be unique among the intents that Server currently holds. A client-supplied `IntentId` that collides with an outstanding one **shall** be refused with `ParameterInvalid`.
+
+### 6.3 States
+
+The Part 10 state machine carries the coarse lifecycle. `ExecutionState` refines it, because `Queued`, `Cancelling` and the distinction between the three terminal outcomes cannot be told apart from `CurrentState` alone.
+
+**The state machine is authoritative** for the coarse state and is what generates events. `ExecutionState` **shall** at all times be consistent with it according to the following table, which is exhaustive: a pairing not listed here is not legal.
+
+| `ExecutionState` | Part 10 state | Meaning |
+|---|---|---|
+| `Accepted` | `Ready` | Admitted and validated; not yet queued or executing. |
+| `Queued` | `Ready` | Waiting behind another intent. `QueuePosition` is non-zero. |
+| `Executing` | `Running` | Commanding the robot now. |
+| `Suspended` | `Suspended` | Paused; position retained. |
+| `Cancelling` | `Running` | A cancel was accepted; motion is being brought to a controlled end. |
+| `Succeeded` | `Halted` | Terminal. Completed as requested. |
+| `Failed` | `Halted` | Terminal. `Result.Failure` carries the reason. |
+| `Cancelled` | `Halted` | Terminal. Ended early because a cancel was accepted. |
+| `Retriable` | `Halted` | Terminal for now; `Retry` may re-attempt it. |
+
+```mermaid
+stateDiagram-v2
+ [*] --> Accepted
+ Accepted --> Queued
+ Accepted --> Executing
+ Queued --> Executing
+ Queued --> Cancelled
+ Executing --> Suspended
+ Suspended --> Executing
+ Executing --> Cancelling
+ Cancelling --> Cancelled
+ Executing --> Succeeded
+ Executing --> Failed
+ Executing --> Retriable
+ Retriable --> [*]
+ Succeeded --> [*]
+ Failed --> [*]
+ Cancelled --> [*]
+```
+
+The distinction between `Accepted` and `Queued` is the one PLCopen draws between an axis command that is busy and one that is actively commanding. A client that cannot see it cannot tell "the robot has not started your work yet" from "the robot is working on it".
+
+### 6.4 Queueing and blending
+
+`BufferMode` on the submitted intent decides how it relates to what is already executing.
+
+`Aborting` is the default and **shall** be accepted by every Server. It terminates the executing intent as `Cancelled` — with `Result.Failure` set to `Superseded` — and begins the new one.
+
+A supersede carries no client-chosen `StopMode`, because the submission that caused it names none. The Server chooses, and **should** choose the most urgent stop the cell tolerates, since the successor is about to command motion of its own and the two must not overlap. A Server **should** document which mode a superseded intent is stopped with, so the behaviour is predictable rather than discovered.
+
+`Buffered` queues the intent; it begins when its predecessor reaches `Succeeded`.
+
+The four blending modes queue the intent and additionally ask that the robot not decelerate to a stop at the boundary. Where blending occurs, the predecessor reaches `Succeeded` **when blending begins**, not when its target is exactly attained, and its `Result.AchievedPose` **shall** record where the tool centre point was at that moment. This is the behaviour PLCopen defines, and reporting it any other way would tell a client the robot stopped somewhere it never was.
+
+A Server that accepts a blending mode but executes it as `Buffered` **shall** report `BlendingSupported` false. A client can then tell a robot that blends from one that merely tolerates being asked to.
+
+`MaxQueueDepth` bounds the queue. A Server with `MaxQueueDepth` zero accepts only `Aborting` submissions.
+
+`BlockingMode` is orthogonal to `BufferMode` and constrains concurrency rather than ordering: whether motion may continue during the intent, and whether other intents may run alongside it. A Server **shall not** begin an intent whose `BlockingMode` is `Single` or `Hard` while any other intent is executing.
+
+### 6.5 Cancellation — and what the `Cancel` Service is not
+
+> The OPC UA `Cancel` Service defined in OPC 10000-4 §5.7.5 cancels an **outstanding service request**. It does not stop the robot. Invoking it against a submission returns `Bad_RequestCancelledByClient` for that request and leaves the motion running.
+
+Stopping a robot is `CancelIntent`, `CancelMission` or `CancelAll` — Methods on the information model, with real-world effect. This distinction is normative because a specification that leaves it implicit produces implementations that believe they have a stop button and do not.
+
+A Server **may refuse** a cancel, and reports that in the `Accepted` output. Some motions cannot be abandoned part-way without leaving the cell in a worse state than completing them — a tool change mid-exchange, a placement mid-release. A capability entry whose `CancelSupported` is false declares this in advance for a whole intent type; `Accepted` false reports it for one occasion.
+
+Where a cancel is accepted, the operation enters `Cancelling` and then `Cancelled`. `Cancelling` is not terminal: a client that treats the acceptance of a cancel as the end of motion will act too early.
+
+`StopMode` says how urgently to stop. It carries the values of `PossibleStopModes` in OPC 40010-1 so that a Server implementing both reports one vocabulary. It selects **no** IEC 60204-1 stop category; clause 10 explains why it cannot.
+
+A Server **shall** either honour the requested `StopMode` or treat every value as its single stop behaviour; it **shall not** vary silently between the two. Where it cannot differentiate, it **should** say so — the capability declaration is the natural place — because a client that asks for `OnPath` and silently receives a `QuickStop` has been told something untrue about how the cell came to rest. A Server that does differentiate **should** make the difference observable in `Result.AchievedPose`, which records where the tool centre point actually stopped.
+
+### 6.6 Pause, resume and retry
+
+`Pause` suspends execution retaining position, and `Resume` continues it. Both are optional, and a capability entry declares per intent type whether they are honoured.
+
+Suspending execution means the robot stops. A Server **shall not** report `Suspended` for an operation whose motion is still in progress: Part 10 defines that state as position retained, and a client or operator that reads it will act on the robot having come to rest. Since this specification defines no channel through which a Server can instruct an actuator to pause mid-motion, whether a Server can honour `Pause` for a running intent depends entirely on the underlying controller.
+
+A Server that cannot suspend a running intent **shall** declare `PauseSupported` false for that intent type. It **may** still stop its queue on `Pause` - refusing to start further work is useful on its own - but the executing operation remains `Executing` until it finishes, and `Ready` **shall** reflect that no new work will be admitted. This is the same rule clause 9 applies to `BlendingSupported`: declare false rather than accept work that will not be performed.
+
+`Retriable` is a terminal state a Server uses where it judges an intent worth another attempt — a grasp that closed on nothing, a location that was momentarily blocked. `Retry` creates a **new** `IntentOperationType` instance for the new attempt; the original remains, terminal, with its own result. A Server that does not offer `Retry` never enters `Retriable` and reports `Failed` instead.
+
+`Retry` refuses like a submission does, and reports it the same way (§6.2): `Accepted` false with a `Failure` and a `Message`. A named intent that is not in `Retriable` is refused with `ParameterInvalid`, and one whose capability entry declares `RetrySupported` false with `CapabilityNotSupported`.
+
+### 6.7 Results
+
+When an operation reaches a terminal state its `Result` **shall** be complete and **shall not** change thereafter. The same `IntentResultDataType` value **shall** also be reachable under the `FinalResultData` object, which this specification promotes to **Mandatory** on `IntentOperationType` for exactly this reason — Part 10 leaves it Optional, and a **shall** that rests on a member a conformant Server may omit is not a requirement, so that a client written against Part 10 finds the result where Part 10 says it will be.
+
+`Result.AchievedPose` records where the driven tool centre point came to rest, or was when blending began. It is what lets a client audit a placement and distinguish a blended corner from an exact stop.
+
+`Result.Failure` is a small, diagnosable set precisely so that a client can decide from it alone whether to retry, re-plan or escalate. `Message` is for a human and **shall not** be parsed.
+
+A Server **shall** retain a terminated operation for long enough that a client which was disconnected at the moment of completion can still read its result on reconnection. It **may** then delete it; `AutoDelete` and `RecycleCount`, inherited from Part 10, describe what it does.
+
+### 6.8 Trajectory execution (normative)
+
+A trajectory is submitted like any other intent and tracked by the same lifecycle. Two things are particular to it.
+
+**It is handed over whole.** A Server **shall** accept the entire trajectory at submission and execute it without further exchange. It **shall not** require a client to feed points during execution, because a transport that cannot guarantee the next point arrives in time cannot be part of the control loop (§4.3).
+
+**Tolerance decides success.** While executing, a Server **shall** report `Failed` with `Kinematics` if deviation exceeds `PathTolerance`. At the end it **shall** report `Failed` with `Kinematics` if the final deviation exceeds `GoalTolerance`, and `Failed` with `Timeout` if completion is later than the final point's `TimeFromStart` by more than `GoalTimeTolerance`. A tolerance of zero or less means the Server applies its own, and a Server that applies its own **should** publish it in `Result.Outputs` so the client can learn what was actually enforced.
+
+`Progress` is meaningful for a trajectory in a way it is not for a single move: a Server **should** report the fraction of `TimeFromStart` elapsed.
+
+### 6.9 Brokering a real-time channel (normative)
+
+Where a client needs a rate this interface cannot carry, the Server describes a channel and leases it. The samples travel on that channel; this specification defines no transport and inspects no payload.
+
+`OpenRealTimeChannel` takes a lease. A Server **shall** refuse it — returning `Granted` false, with `Message` saying which of these it was — when the channel is not `Available`, when another Session holds the lease, when `OperationalMode` is not the channel's `RequiredMode`, or when the caller does not hold command authority. On success it returns the `EndpointUrl`, the `PayloadDescriptor` and a `LeaseExpiry`.
+
+A Server **shall** bound `RequestedLease` to what it is willing to grant and report the bounded value in `LeaseExpiry`; a `RequestedLease` of zero or less asks for the Server's own default.
+
+A lease **shall** lapse at `LeaseExpiry` unless renewed by a further `OpenRealTimeChannel` from the holding Session, and **shall** be released when that Session closes. This is the same reasoning as command authority in clause 8: a client that dies must not hold a resource for good.
+
+`CloseRealTimeChannel` releases the lease explicitly.
+
+Two rules keep the division of labour honest:
+
+- While a channel lease is held, a Server **shall** refuse motion intents with `CapabilityNotSupported` unless it can genuinely arbitrate between the two sources. Two things commanding one robot with no arbitration is the failure this rule exists to prevent.
+- A Server **shall not** represent a brokered channel as being under this interface's control. Its behaviour, its guarantees and its failure modes are the transport's.
+
+Of the transports named, only `OpcUaFx` — OPC UA FX, OPC 10000-80 to -84 — is an OPC Foundation specification. It is the open path, and a Server that offers it gives a client something it can implement from published documents rather than from a vendor SDK.
+
+---
+
+## 7 Missions (normative)
+
+### 7.1 Purpose
+
+A mission is an ordered sequence of intents submitted and tracked as a unit. It exists so that a supervisor can commit work in advance — the robot keeps moving through a sequence without a round trip per step — while retaining the ability to change what has not yet been committed.
+
+`MissionDataType` (`ns=1;i=3068`) carries the mission: a client-assigned `MissionId`, a monotonically increasing `MissionUpdateId`, a `Label`, and the ordered `Steps`. `MissionType` (`ns=1;i=1004`) is the instance that tracks one, and like `IntentOperationType` it is a Part 10 program instance for the reasons §6.1 gives.
+
+Missions are optional. `IntentCapabilitiesType.MissionsSupported` declares whether a Server implements them.
+
+### 7.2 Base and horizon
+
+Every step carries `Released`. The released steps form a prefix called the **base**; the rest form the **horizon**.
+
+```mermaid
+flowchart LR
+ S0["Step 0
released"] --> S1["Step 1
released"]
+ S1 --> S2["Step 2
released"]
+ S2 --> S3["Step 3
horizon"]
+ S3 --> S4["Step 4
horizon"]
+```
+
+The base is **committed and immutable**. A Server **shall** assume every released step is executing or already executed, and **shall** refuse any update that would alter, remove or reorder one.
+
+The horizon is provisional. `UpdateMission` replaces it wholly and **may** release some or all of its steps in doing so, extending the base.
+
+`ReleasedStepCount` on the mission instance states how many steps are in the base, so a client does not have to scan the array to find the boundary.
+
+A Server **shall** enforce all of the following, and **shall** apply an update atomically — an update either takes effect entirely or not at all:
+
+1. `MissionUpdateId` **shall** be strictly greater than the mission's current value. An update that is not is refused with `Outdated`. This is what makes two updates that crossed in flight safe: the later one wins and the earlier one is rejected rather than applied out of order.
+2. An update that would alter a released step is refused with `BaseConflict`.
+3. `SequenceId` **shall** ascend across the steps of a mission, and `StepId` **shall** be unique within it.
+
+### 7.3 Execution
+
+A mission executes its steps in ascending `SequenceId`. Each step, when it begins, gets an `IntentOperationType` instance, and `MissionStepDataType.Operation` references it.
+
+`MissionStepDataType.Status` is a **hint**. Where `Operation` is not null, that operation's state machine **decides**, and `Status` **shall** reflect it. Two members can report the state of one step; this sentence says which one is right.
+
+A step that terminates `Failed` **shall** cause the mission to terminate `Failed` without beginning any later step, **unless** the step declares an error policy that says otherwise (§7.4).
+
+`CancelMission` cancels the mission and every intent belonging to it, subject to the same right of refusal as `CancelIntent`.
+
+### 7.4 The step graph (normative)
+
+A mission may carry `Transitions`, an array of `MissionTransitionDataType` (`ns=1;i=3083`). Where it is **empty**, the mission is the ordered sequence §7.3 describes and nothing else applies — which is what makes the graph an addition rather than a replacement.
+
+The model is the step-and-transition form of an IEC 61131-3 sequential function chart, chosen over a behaviour tree because it is the notation the audience implementing this already knows, it has an IEC serialization, and it needs no tick loop.
+
+Each transition carries `FromStepId`, `ToStepId`, a `Condition` and a `DivergenceKind`.
+
+`Condition` is an OPC UA `ContentFilter` — the base specification's own filter grammar, reused rather than invented. A specification that defined its own expression language would oblige every implementer to write a parser for it, and would then have to say what happens when two parsers disagree. An empty filter is always true.
+
+`DivergenceKind` says how the transitions leaving one step relate:
+
+- `Alternative` — exactly one is taken, the first whose `Condition` holds, in `Transitions` order. A Server **shall** evaluate them in that order so that two clients reading the same mission predict the same branch.
+- `Parallel` — all are taken, and the branches execute concurrently. A Server **shall** report `MissionBranchingSupported` false if it cannot run branches concurrently, rather than silently serialising them.
+
+A Server **shall** refuse a mission whose transitions name a `FromStepId` or `ToStepId` that is not a step of that mission, and one that mixes `Alternative` and `Parallel` on transitions leaving the same step.
+
+**Error policies.** `MissionStepDataType.ErrorPolicy` says what happens when the step does not succeed:
+
+| Policy | Behaviour |
+|---|---|
+| `Abort` | End the mission. The default, and what a mission without policies does. |
+| `Retry` | Re-attempt the step. The Server bounds the attempts and reports `Failed` when they are exhausted. |
+| `Skip` | Record the failure and continue with the next step. |
+| `Fallback` | Continue at `FallbackStepId`. |
+| `Compensate` | Run `FallbackStepId` to undo what has been done, then end the mission. |
+
+A Server **shall** refuse a mission where `ErrorPolicy` is `Fallback` or `Compensate` and `FallbackStepId` does not name a step of that mission. `Compensate` differs from `Fallback` in what happens afterwards, and only in that: both run the fallback step, but `Compensate` then ends the mission rather than continuing.
+
+Where a step's policy is honoured, the *mission* does not terminate `Failed` even though the *step* did — which is the whole reason the policies exist, and why §7.3's rule is stated as a default rather than an invariant.
+
+---
+
+## 8 Command authority (normative)
+
+At most one Session at a time holds command authority over a controller, and only that Session **may** submit intents or missions. `ControlOwner` reports which.
+
+`RequestControl` grants authority when no other Session holds it, or when the holder's Session has closed. A Server **shall** release authority automatically when the holding Session closes, so that a crashed client does not lock a robot permanently. `ReleaseControl` gives it up explicitly; outstanding intents are unaffected, and a client that wants them stopped calls `CancelAll` first.
+
+Reading, browsing and subscribing require no authority. Observation is always permitted.
+
+> Command authority arbitrates between OPC UA clients. It is **not** the single point of control required by ISO 10218-2, which concerns the mutual exclusion of remote command and local manual control and is enforced by safety-rated means outside this interface. A Server **shall not** present command authority as satisfying that requirement.
+
+---
+
+## 9 Capabilities and discovery (normative)
+
+`IntentCapabilitiesType` is what makes an intent surface self-describing. A conformant Server **shall** populate it to reflect what it will actually accept — it is a contract, not documentation.
+
+`SupportedIntents` carries one `IntentCapabilityDataType` per accepted intent type, naming the DataType and declaring whether cancel, pause and retry are honoured for it, which buffer and blocking modes it accepts, and which named `Attributes` this Server recognises.
+
+Four rules keep the declaration honest, and each is checkable against a running Server:
+
+1. A Server **shall** refuse an intent whose DataType is not listed, with `CapabilityNotSupported`.
+2. Every entry's `SupportedBufferModes` **shall** include `Aborting`.
+3. `BlendingSupported` **shall** be false unless the blending buffer modes actually blend.
+4. `PauseSupported` **shall** be false unless `Pause` actually suspends a *running* intent, per §6.6. Stopping only the queue is not suspending execution, and `Suspended` **shall not** be reported while the robot is still moving.
+
+A fifth rule applies the same honesty to the **Method surface**, because a declaration a client cannot act on is worse than no declaration. Where a Server declares a capability, the Methods that make it usable **shall** be present on the controller and callable:
+
+| Declaration | Methods that **shall** be present |
+|---|---|
+| `MissionsSupported` true | `SubmitMission`, `CancelMission` |
+| `MissionHorizonSupported` true | `UpdateMission` |
+| `RealTimeChannelsSupported` true | `OpenRealTimeChannel`, `CloseRealTimeChannel` |
+| a capability entry with `PauseSupported` true | `Pause`, `Resume` |
+| a capability entry with `RetrySupported` true | `Retry` |
+
+These Methods are Optional on `IntentControllerType`, which is what makes the rule necessary: a Server can otherwise advertise missions while omitting `SubmitMission` entirely, and a client discovers the contradiction only by calling something that is not there. Like the others, this is observable against a running Server — browse the controller and compare what it offers with what it claims.
+
+`AxisCount` states how many entries `JointMoveIntentDataType.JointTargets` must carry, and **shall** equal the number of `AxisType` instances under the controller.
+
+Five further declarations cover the capability added beyond single moves. `TrajectorySupported` and `ForceControlSupported` say whether trajectories and force-controlled moves are accepted; `RealTimeChannelsSupported` whether channels are brokered; `MissionBranchingSupported` whether `Transitions` are evaluated at all; and `MaxTrajectoryPoints` bounds a trajectory, zero meaning the Server states no limit.
+
+Each follows the same rule as `BlendingSupported`: a Server declares false rather than accepting work it will not actually perform. A Server that reports `MissionBranchingSupported` false executes the steps in order and ignores any transitions supplied, and a client reading that declaration knows not to express a branch it needs.
+
+`SupportedFacets` is the same contract at the level of whole facets, and it is bound by every rule above. A facet is not a summary of the declaration a client has already read: some of what Table 12.2 requires — that blending modes are honoured, that the refusal rules of §6.2 are followed, that a mission base is immutable — cannot be established by reading the address space at all. Listing such a facet is therefore an attestation, and a Server that lists **RI-Blending** while treating the buffer modes as `Buffered` has made a false statement of exactly the kind rule 3 forbids, whatever `BlendingSupported` says. Clause 12.2 sets out which requirements are structural and which are attested.
+
+---
+
+## 10 Safety (normative)
+
+### 10.1 This interface is not safety-rated
+
+**This specification defines a non-safety-rated application interface.** The Methods defined here are processed by the robot controller as application-level requests. They do not constitute, and **shall not** be used as, safety functions as defined in IEC 61508, nor safety communication as defined in IEC 61784-3 or IEC 62541-15.
+
+The safety functions of the robot system — emergency stop, protective stop, speed and separation monitoring, force limiting, and enabling device control — are implemented in safety-rated hardware and firmware independent of this interface, and **shall** remain effective regardless of its state, including when the Server is unreachable, the Session has closed, or a client is submitting intents as fast as the Server will accept them.
+
+A Server **shall not** claim, and a client **shall not** assume, any safety integrity level or performance level for any part of this model.
+
+This is a property of the technology, not a choice this working group made. OPC 10000-15 carries cyclic safety data from a SafetyProvider to a SafetyConsumer; the consumer's request carries an identifier, a monitoring number and one octet of explicitly **non-safety** flags, so a caller has no channel through which to supply safety-rated arguments. Every safety fieldbus — PROFIsafe, CIP Safety, FSoE, openSAFETY — expresses a safety command as a **continuously asserted cyclic signal**, because the integrity argument rests on the fail-safe state that follows when assertion stops. A Method call has no defined behaviour when it stops being called, and therefore cannot be a safety function however it is labelled.
+
+A specification that ignored this would not be merely wrong; it would encourage an integrator to rely on something that fails silently. §10.4 says what can honestly be done instead.
+
+### 10.2 Operational mode gates submission
+
+A Server **shall** refuse `SubmitIntent` and `SubmitMission` with `NotPermittedInMode` unless `OperationalMode` is `Automatic` or `AutomaticExternal`.
+
+`OperationalMode` is read-only. This specification defines **no** way to command a mode change, because mode selection is a safety function performed by safety-rated means — a key switch, an interlock — and an interface that could change it from the network would defeat the arrangement it is reporting.
+
+Where a person may be within the safeguarded space, the applicable safety standard, not this interface, decides what motion is permitted. A Server **shall not** rely on a client to observe such limits.
+
+### 10.3 A stop request is not a stop category
+
+`StopMode` expresses urgency. It **shall not** be interpreted as selecting, implying or guaranteeing any IEC 60204-1 stop category. Category 0, 1 and 2 stops are determined solely by the robot controller's safety system in response to the active operational mode and the risk assessment for the installation.
+
+A client that requires a category-rated stop **shall** obtain it from the safety system. It cannot be obtained here.
+
+### 10.4 Safety awareness, and its limits
+
+What this specification *can* do is see what the safety system is enforcing and refuse work that would contradict it. `SafetyStateType` (§5.7.2) reports it, and the following are normative.
+
+A Server **shall** refuse `SubmitIntent` and `SubmitMission`:
+
+- with `SafetyLimitExceeded`, when `SafeSpeedLimitActive` is true and the intent's `Constraints.CartesianSpeed` exceeds `SafeSpeedLimit`;
+- with `NotPermittedInMode`, when `EmergencyStopActive` or `ProtectiveStopActive` is true;
+- with `NotPermittedInMode`, when `SafetyControllerOk` is false.
+
+A Server **shall** publish `ActiveFunction`, `EmergencyStopActive`, `ProtectiveStopActive`, `SafeSpeedLimitActive`, `SafeSpeedLimit` and `SafetyControllerOk` as the safety system reports them, and **shall not** publish a value the safety system has not asserted.
+
+Each of these is observable against a running Server: assert a protective stop and a conformant Server refuses; lower `SafeSpeedLimit` below a submitted speed and it refuses.
+
+**What none of this makes true.** These refusals are an application-layer courtesy performed by non-safety-rated software. They reduce the number of requests the safety system has to reject; they are not a protective measure and nothing may be assumed from their presence. In particular:
+
+- a client **shall not** treat a Server's acceptance of an intent as evidence that the motion is safe;
+- a client **shall not** treat `SafeSpeedLimit` as a limit *this interface* enforces — the safety system enforces it, and would enforce it identically if this model did not exist;
+- a Server **shall not** offer any Method that commands a safe motion function, changes an operational mode, or clears a stop. Those are safety functions, and clause 10.1 says why no Method here can be one.
+
+The asymmetry is deliberate: this model may **observe** the safety system and **refuse** on what it sees, and may never **instruct** it.
+
+### 10.5 Refusal is a normal outcome
+
+Every rule above produces a refusal rather than a degraded execution, and each is observable against a running Server: a conformant Server can be seen to refuse a submission outside Automatic mode, and to refuse one from a Session that does not hold authority. A client **shall** treat refusal as an expected outcome and not as an error condition to be retried blindly.
+
+---
+
+## 11 Security
+
+### 11.1 Commanding is a privileged operation
+
+Every Method in this model moves a machine that can injure people and destroy property. A Server **shall** require an authenticated Session and **should** restrict the Methods of `IntentControllerType` by Role, distinctly from read access to the same address space. Observing a robot and commanding one are different privileges and **shall not** be conflated.
+
+A Server **should** apply `UserExecutable` to the Methods it exposes so that a client discovers what it is permitted to invoke before invoking it.
+
+### 11.2 Command authority is not authorisation
+
+Command authority (clause 8) prevents two authorised clients from interleaving motion. It grants nothing. A Server **shall** apply its access control independently: a Session that holds authority but lacks the necessary Role **shall** still be refused.
+
+### 11.3 NodeIds in intents are untrusted input
+
+`PickIntentDataType.Source`, `SetOutputIntentDataType.Output`, `CallProgramIntentDataType.Program` and the other NodeId-valued members carry references chosen by the client. A Server **shall** validate that each resolves to a node of the expected type **under the controller being commanded**, and **shall** refuse with `ParameterInvalid` otherwise. A NodeId that resolves to a node belonging to a different controller, or to no node at all, **shall not** be acted on.
+
+The expected type is fixed for every such member, so that "the expected type" is a checkable statement rather than an instruction to guess:
+
+| Member | Resolves to |
+|---|---|
+| `PickIntentDataType.Source`, `PlaceIntentDataType.Destination`, `PalletiseIntentDataType.Pattern` | a `LocationType` instance under the controller |
+| `MotionIntentDataType.ToolFrame`, `ForceIntentDataType.FrameId` | a `CoordinateFrameType` instance under the controller; `ToolFrame` additionally of `Role` `Tool` |
+| `ToolChangeIntentDataType.Tool` | a `ToolType` instance under the controller, or null to release the fitted tool |
+| `SetOutputIntentDataType.Output` | an `OutputSignalType` instance under the controller; `Value` **shall** match that signal's own DataType |
+| `CallProgramIntentDataType.Program`, `ProcessIntentDataType.ProcessProgram` | a `ProgramType` instance under the controller |
+| `WaitIntentDataType.Signal` | an `OutputSignalType` instance under the controller, or a Variable of DataType `Boolean` under it |
+| `FastenIntentDataType.Joint` | a joint in an OPC 40450 / OPC 40451 model where one is implemented; otherwise the intent's own parameters stand alone and the member is null |
+
+`CallProgramIntentDataType` deserves particular care: it runs code the Server holds. A Server **shall** restrict it to programs it has published as `ProgramType` instances, and **shall not** accept a program identifier that names anything else.
+
+### 11.4 Cybersecurity is in scope of the safety case
+
+ISO 10218-1 addresses cybersecurity where a vulnerability could compromise robot safety. A networked command interface is exactly such a surface. The measures above are therefore not merely good practice; where this interface is deployed on a robot subject to that standard, they form part of the case that the installation is safe.
+
+---
+
+## 12 Profiles and conformance units
+
+### 12.1 Declaring conformance
+
+A Server declares conformance by exposing `RobotIntentRootType` under the Server object with `SpecificationVersion` set to the release it implements, by populating `IntentCapabilitiesType` truthfully, and by listing in `SupportedFacets` the facets of Table 12.2 that each controller satisfies.
+
+`SupportedFacets` carries the facet names of Table 12.2 verbatim. It exists because conformance is otherwise not machine-readable: a client would have to re-derive the whole table from the address space, and since several rows are behavioural, two clients deriving independently could reach different conclusions about the same Server. This mirrors `ServerCapabilitiesType.ServerProfileArray` in OPC 10000-5, where a Server states its profiles rather than leaving them to be inferred.
+
+A Server shall not list a facet whose structural requirements are unmet. The behavioural requirements are the Server's own attestation and are subject to the honesty rules of clause 9 — a Server that lists **RI-Blending** while treating the blending buffer modes as `Buffered` is making a false statement in exactly the sense clause 9 forbids, and is no more conformant than one that reports `BlendingSupported` true under the same conditions.
+
+### 12.2 Facets
+
+Requirements are of two kinds. **Structural** requirements are settled by reading the address space and the capability declaration: a client can check them, and so can a compliance tool, without commanding the robot. **Behavioural** requirements — written below as accepting, honouring, maintaining or observing a rule — cannot be settled by reading, only by exercising the Server, and are the Server's attestation under clause 9.
+
+| Facet | Requires |
+|---|---|
+| **RI-Base** (mandatory) | `RobotIntentRootType`; at least one `IntentControllerType` with `Capabilities`, `Frames`, `Tools`, `Locations`, `Axes` and `Intents`; `SubmitIntent`, `CancelIntent`, `CancelAll`, `RequestControl`, `ReleaseControl`; `IntentOperationType` instances with the state model of §6.3; the refusal rules of §6.2 and clause 10. |
+| **RI-Motion-Joint** | `JointMoveIntentDataType`, with `AxisType` instances covering `0` to `AxisCount − 1`. |
+| **RI-Motion-Linear** | `LinearMoveIntentDataType`. |
+| **RI-Motion-Circular** | `CircularMoveIntentDataType`. |
+| **RI-Trajectory** | `TrajectoryIntentDataType`, `TrajectorySupported` true, and the tolerance rules of §6.8. |
+| **RI-Path** | `CartesianPathIntentDataType` and `TrajectorySupported` true. |
+| **RI-Force** | `ForceIntentDataType` and `ForceControlSupported` true — the robot genuinely regulates force. |
+| **RI-RealTimeChannel** | `RealTimeChannelsSupported` true; the `RealTimeChannels` folder with at least one `RealTimeChannelType`; `OpenRealTimeChannel` and `CloseRealTimeChannel` with the lease rules of §6.9. |
+| **RI-Safety** | `SafetyState` populated from the safety system, and the refusals of §10.4. |
+| **RI-Description** | `Description` with a `KinematicChain` covering every axis, `ReachRadius`, `PayloadLimit` and `MaxCartesianSpeed`. |
+| **RI-Process-ArcWeld** | `ArcWeldIntentDataType`. |
+| **RI-Process-SpotWeld** | `SpotWeldIntentDataType`. |
+| **RI-Process-Dispense** | `DispenseIntentDataType`. |
+| **RI-Process-Fasten** | `FastenIntentDataType`. Where an OPC UA joining or tightening model is implemented, `Joint` resolves into it. |
+| **RI-Process-Palletise** | `PalletiseIntentDataType` and at least one `LocationType` describing a pattern. |
+| **RI-Process-SurfaceFinish** | `SurfaceFinishIntentDataType` and **RI-Force**. |
+| **RI-Grasp** | `GraspIntentDataType` and `ReleaseIntentDataType`, and at least one `ToolType` with a `TcpFrame`. |
+| **RI-PickPlace** | `PickIntentDataType`, `PlaceIntentDataType`, and at least one `LocationType`. |
+| **RI-ToolChange** | `ToolChangeIntentDataType`, and more than one `ToolType`. |
+| **RI-Output** | `SetOutputIntentDataType` and the `Outputs` folder. |
+| **RI-Program** | `CallProgramIntentDataType` and the `Programs` folder. |
+| **RI-Wait** | `WaitIntentDataType`. |
+| **RI-Queue** | `MaxQueueDepth` greater than zero; `Buffered` accepted; `QueuePosition` maintained. |
+| **RI-Blending** | `BlendingSupported` true; the four blending buffer modes accepted and honoured; `Result.AchievedPose` reported at the blend point per §6.4. |
+| **RI-Pause** | `Pause` and `Resume`. |
+| **RI-Retry** | `Retry`, and `Retriable` reachable. |
+| **RI-Mission** | `MissionsSupported` true; `SubmitMission`, `CancelMission`; `MissionType` instances. |
+| **RI-Mission-Horizon** | **RI-Mission**, plus `MissionHorizonSupported` true, `UpdateMission`, and the base immutability rules of §7.2. |
+| **RI-Mission-Branching** | **RI-Mission**, plus `MissionBranchingSupported` true, `Transitions` evaluated per §7.4, and the error policies honoured. |
+| **RI-Interop-40010** | Annex B. |
+| **RI-Interop-Vision** | Annex E. |
+
+A facet other than **RI-Base** is claimed only where every intent type it names appears in `SupportedIntents`.
+
+**RI-Base** additionally requires `SupportedFacets`, since a conformance claim that cannot be read is not a claim.
+
+Conformance is therefore declared at two levels, and they answer different questions. `SupportedFacets` is a member of `IntentCapabilitiesType`, so it is stated **per controller**: a Server hosting two robots of different capability has two answers, and a client asking whether *this* controller blends must read *this* controller's list. `ServerProfileArray` is a member of the Server object, so it is stated **once for the Server**, which is the right granularity for a profile (§12.3) — a named shape an integrator specifies and a supplier builds to.
+
+A Server **shall** publish the URI of every profile it claims in `Server/ServerCapabilities/ServerProfileArray`, and **may** publish facet URIs there as well. Where it does, the two **shall** agree: a facet URI on the Server that no controller lists in `SupportedFacets` is a claim nothing in the address space backs, and a client that read only one of them would be told something untrue by the other.
+
+### 12.3 Profiles
+
+A facet is a building block. A **profile** is a complete claim: a named set of facets describing one plausible robot Server, which is what an integrator specifies and what two manufacturers implementing the same shape agree they have built. §1.2's use cases are written about profiles even though they do not use the word — a mixed-fleet cell works only because two robots claim the same one.
+
+Four are defined. Each includes the **Robot Motion Server** set, and a Server **may** claim more than one: a robot that both follows paths and executes missions claims two.
+
+Claiming a profile is claiming every facet in it, on the terms §12.2 sets out — structural requirements a client can check by reading, behavioural ones the Server attests to under clause 9. A profile is a shorter way to say the same thing, not a weaker one.
+
+| Profile | Facets | The Server it describes |
+|---|---|---|
+| **Robot Motion Server** | RI-Base, RI-Motion-Joint, RI-Motion-Linear, RI-Description, RI-Safety | The baseline. A robot that can be commanded to a joint configuration or a Cartesian pose, that describes its own kinematics and limits, and that reports what its safety system is enforcing. |
+| **Robot Handling Server** | Motion, plus RI-Motion-Circular, RI-Grasp, RI-PickPlace, RI-ToolChange, RI-Output, RI-Queue | Material handling. Picking, placing, changing tools and driving the discrete outputs a gripper needs, with a queue so a cell controller can stay ahead of the robot. |
+| **Robot Path Server** | Motion, plus RI-Trajectory, RI-Path, RI-Blending | Continuous-path work. A whole path is handed over once and the robot's own motion kernel runs it, blending between segments rather than stopping at each. |
+| **Robot Mission Server** | Motion, plus RI-Mission, RI-Program, RI-Wait, RI-Pause, RI-Retry | Long-running supervised operation. A mission is submitted, watched, paused, retried and cancelled, which is §1.2's fourth use case stated as a claim. |
+
+**RI-Safety is in the baseline rather than optional to it.** Clause 10 is explicit that this specification is not safety-rated and that no Method here is a safety function. What it does require is a duty: the Server *reports* what the safety system enforces and *refuses* work that would exceed it. Every profiled Server owes that duty, because an integrator specifying a profile is entitled to assume a robot will decline an intent its safety configuration forbids rather than attempt it. A robot that cannot read its safety system claims facets individually and not a profile.
+
+The process facets — **RI-Process-ArcWeld** and its siblings — are deliberately in no profile. A welding robot is a **Robot Path Server** that additionally claims **RI-Process-ArcWeld**, and bundling the process into a profile would have produced one profile per process and no way to say that the underlying motion is the same. The same reasoning keeps **RI-Force**, **RI-RealTimeChannel** and the two interop facets outside all four.
+
+### 12.4 Profile and facet URIs
+
+A profile name is for a human. `ServerProfileArray` holds URIs, and unless this specification states them two Servers implementing the same profile publish different strings and no client can match either.
+
+Profiles are published under `http://opcfoundation.org/UA-Profile/RobotIntent/Server/`:
+
+| Profile | URI suffix |
+|---|---|
+| Robot Motion Server | `Motion` |
+| Robot Handling Server | `Handling` |
+| Robot Path Server | `Path` |
+| Robot Mission Server | `Mission` |
+
+Facets are published under `http://opcfoundation.org/UA-Profile/RobotIntent/Facet/`, with the suffix being the facet name after the `RI-` prefix: **RI-Base** is `Base`, **RI-Motion-Joint** is `Motion-Joint`, **RI-Process-ArcWeld** is `Process-ArcWeld`, and so on for every row of §12.2. These URIs exist so a generic OPC UA tool that reads `ServerProfileArray` and knows nothing about robots can still recognise a facet; the authority on which facets a given controller satisfies is that controller's `SupportedFacets`, because only it is stated per controller.
+
+These URIs are **provisional**, on the same terms as the namespace URI and the NodeIds: this is a working-group draft, and the OPC Foundation assigns the final values.
+
+---
+
+## 13 Deliverables and reproducibility
+
+| Artifact | Path |
+|---|---|
+| This specification | `metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md` |
+| Research report | `metaverse-specs/robot-intent/OPC-UA-Robot-Intent-Research.md` |
+| Information model | `metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeSet2.xml` |
+| NodeId assignments | `metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeIds.csv` |
+| Generator | `metaverse-specs/extras/robot-intent/tools/build_model.py` |
+| Validator | `metaverse-specs/extras/robot-intent/tools/validate_local.py` |
+| Annex A (generated) | `metaverse-specs/extras/robot-intent/tools/model-reference.md` |
+
+The NodeSet, the CSV and Annex A are generated from a single in-code source of truth and are **deterministic**: regenerating reproduces them byte for byte. The generator is edited; the generated files are not.
+
+```powershell
+python metaverse-specs\extras\robot-intent\tools\build_model.py
+python metaverse-specs\extras\robot-intent\tools\validate_local.py
+```
+
+---
+
+## Annex A — Information model (generated)
+
+Annex A is generated from the NodeSet and is authoritative for identifiers, DataTypes, ValueRanks, ModellingRules, structure fields, enumeration values and Method signatures. See [`../extras/robot-intent/tools/model-reference.md`](../extras/robot-intent/tools/model-reference.md).
+
+---
+
+## Annex B — OPC 40010 interop profile (normative for RI-Interop-40010)
+
+OPC 40010-1 describes the robot; this specification commands it. The two are joined by one reference and are otherwise independent — this model takes no dependency on the Robotics NodeSet, and a Server implementing only this specification is fully conformant.
+
+A Server claiming **RI-Interop-40010** **shall**:
+
+1. Expose a `HasIntentController` reference from the `MotionDeviceSystemType` instance describing the robot to the `IntentControllerType` instance that commands it. The inverse, `IntentControllerOf`, leads from the intent surface back to the machine.
+2. Report `IntentControllerType.OperationalMode` with the same value as the corresponding OPC 40010-1 operational mode of that motion device system. The two **shall not** disagree; where they would, the OPC 40010-1 value is the robot's own report and decides.
+3. Publish, as `ProgramType` instances under the controller's `Programs` folder, exactly those programs the OPC 40010-1 task control can load, so that `CallProgramIntentDataType` and the OPC 40010-1 task control name the same things.
+4. Express the poses it publishes in frames whose transforms are consistent with the mounting and geometry the OPC 40010-1 model describes.
+
+A Server **shall not** duplicate OPC 40010-1's topology in this model. `AxisType` exists here only to fix the order, kind and limits that a joint target needs; where OPC 40010-1 is also implemented, its axis description is the fuller one and this model's `AxisType` instances **shall** agree with it. The same rule governs `RobotDescriptionType`: its `KinematicChain` is additive — OPC 40010-1 defines no kinematic chain — but where the two describe the same axis, **OPC 40010-1 decides** and this model reflects it.
+
+Note that OPC 40010-1 defines no tool centre point. `ToolType.TcpFrame` supplies the concept, and there is nothing in OPC 40010-1 for it to contradict.
+
+A Server claiming **RI-Interop-40010** together with **RI-Safety** **shall** report `SafetyStateType` consistently with the OPC 40010-1 safety state of the same motion device system. Where they would disagree, the OPC 40010-1 value is the robot's own report and decides — and neither is safety-rated, as both specifications say of themselves.
+
+---
+
+## Annex C — Pose conversion (normative)
+
+A Server that exchanges poses with a model using the core `ThreeDFrame` — OPC 40010-1, or a spatial-location model — **shall** convert as follows.
+
+**Position.** `ThreeDCartesianCoordinates` `X`, `Y`, `Z` are metres, as is `Pose3DDataType.Position`. The mapping is the identity.
+
+**Orientation.** The `A`, `B`, `C` fields of `ThreeDOrientation` are, per ISO 9787, rotations about the **X**, **Y** and **Z** axes of the reference frame — roll, pitch and yaw — applied as an **intrinsic** rotation in the order Z, then Y, then X. Angles are in radians.
+
+Given `A` (roll), `B` (pitch), `C` (yaw), let
+
+```text
+cr = cos(A/2) sr = sin(A/2)
+cp = cos(B/2) sp = sin(B/2)
+cy = cos(C/2) sy = sin(C/2)
+```
+
+then the unit quaternion ordered (x, y, z, w) is
+
+```text
+x = sr*cp*cy - cr*sp*sy
+y = cr*sp*cy + sr*cp*sy
+z = cr*cp*sy - sr*sp*cy
+w = cr*cp*cy + sr*sp*sy
+```
+
+and the inverse is
+
+```text
+A = atan2( 2*(w*x + y*z), 1 - 2*(x*x + y*y) )
+B = asin ( clamp( 2*(w*y - z*x), -1, +1 ) )
+C = atan2( 2*(w*z + x*y), 1 - 2*(y*y + z*z) )
+```
+
+Three properties of this conversion are normative:
+
+- The clamp in the expression for `B` is required. Floating-point error can carry the argument of `asin` outside `[−1, +1]`, and without the clamp a conversion that should yield a pole orientation yields a domain error instead.
+- `q` and `−q` denote the same orientation. A Server **should** emit the representative whose `w` is non-negative, so that two Servers describing one orientation produce the same four numbers.
+- The conversion is lossy in one direction only at the poles, where `B` is ±π/2 and roll and yaw are not separately recoverable. A Server converting a pose that originated as a quaternion **shall not** round-trip it through `ThreeDOrientation` when it can avoid doing so.
+
+---
+
+## Annex D — Informative alignments
+
+These are **not** normative references and impose no dependency. They are recorded because this model borrowed from them deliberately, and an implementer who knows them will recognise what was borrowed.
+
+- **PLCopen Motion Control** — `BufferModeEnum` adopts `MC_BufferMode` unchanged, including the rule that the predecessor completes when blending begins. The `Accepted` / `Queued` distinction is PLCopen's `Busy` without `Active`.
+- **VDA 5050** — `BlockingModeEnum` is the `blockingType` matrix. The base/horizon mission model, the monotonic update identifier and the rejection of an outdated update are that specification's order model. `IntentCapabilitiesType` plays the role of its factsheet.
+- **ROS 2 actions** — the goal handle returned on submission, the server's right to refuse a cancel, and the explicit `Cancelling` state before `Cancelled`.
+- **MoveIt** — `MotionSequenceItem.blend_radius` is the same abstraction as `BlendDataType.Radius`.
+- **OPC 10031-4 job control** — the separation of cancelling work that has not started from aborting work that has.
+- **Capability–Skill–Service** (Plattform Industrie 4.0) — in that vocabulary this model is the **service** layer over a robot's **skills**, discovered by **capability**. `IntentCapabilitiesType` is the capability declaration.
+- **Vendor motion languages** — URScript, RAPID, KRL, TP, INFORM and AS. The three motion intents are their common denominator, and `BlendDataType` is the portable form of `r`, `zonedata`, `$APO`, `CNT` and `PL`. The process intents are drawn the same way: `ArcWeldIntentDataType` from ABB `seamdata`/`welddata`/`weavedata`, FANUC weld schedules and KUKA ArcTech; `SpotWeldIntentDataType` from ABB `SpotL` and FANUC SpotTool; `DispenseIntentDataType` from ABB `DispL`.
+- **ROS `control_msgs/FollowJointTrajectory`** — the shape of `TrajectoryIntentDataType`, including the separation of path, goal and goal-time tolerance. Every ROS robot driver already accepts it, which is the point.
+- **OPC UA FX** (OPC 10000-80 to -84) — the open transport a brokered channel may name, and the only one in `RealTimeTransportEnum` that is an OPC Foundation specification.
+- **OPC 40450 / OPC 40451** (Industrial Joining Technologies) — where fastening results and joint definitions belong. `FastenIntentDataType` references them rather than restating them.
+- **IEC 61131-3 sequential function charts** — the step, transition and divergence model of §7.4. Behaviour trees were considered and not adopted: their tick semantics need a runtime that controller vendors do not provide, and their serialization is a library's rather than a standard's.
+- **ISO 15609** — welding procedure specifications, named by `ArcWeldIntentDataType.WeldProcedureRef` and not restated here.
+- **The OPC UA robot skill model** developed in the VDMA SOArc working group (`http://opcfoundation.org/UA/Skills/`) is prior art in this area. This specification uses a different namespace and does not extend it.
+
+---
+
+## Annex E — Vision interop profile (normative for RI-Interop-Vision)
+
+A vision model that publishes a grasp pose and this model that executes it are deployed on the same cell, and each defines its own `CoordinateFrameType`. Without a rule the flange is described twice, with two `FrameId` strings and two transforms that can disagree.
+
+This annex imposes **no** NodeSet dependency in either direction. Both models keep the base OPC UA namespace as their only `RequiredModel`, and a Server implementing only this one is unaffected.
+
+**E.1 This model's frame tree decides.** Where a Server implements both for the same robot, the frames here are authoritative. This model owns `ToolType.TcpFrame` and is what the robot actually moves to; a pose that disagrees with it is wrong however carefully it was measured.
+
+**E.2 `FrameId` corresponds by value.** A frame present in both models **shall** carry the same `FrameId` string in each. That string, not the NodeId, is what `Pose3DDataType` names.
+
+**E.3 Roles correspond by name, never by number.** The two vocabularies agree on `World`, `Base`, `MechanicalInterface`, `Tool`, `Object` and `Other`. A vision model may additionally define a camera role, which this model does not; such a frame **shall** appear here as `Other`. A gateway **shall** map by literal name and **shall not** cast the integer between the two enumerations, because each is decoded against the DataType of the Variable carrying it.
+
+**E.4 Poses transcode explicitly.** A vision pose may carry a covariance field this model's `Pose3DDataType` does not. A boundary **shall** drop it inbound and **shall not** fabricate one outbound. Both sides use metres and a unit quaternion ordered (x, y, z, w) in a right-handed frame, so no numeric conversion is required — but §5.2 rule 3 still applies, and an inbound pose whose quaternion is not normalised **shall** be rejected with `ParameterInvalid` rather than renormalised.
+
+**E.5 An empty `FrameId` is not passed outward.** §5.2 rule 4 reads an empty `FrameId` as this Server's default work frame. A vision model may forbid an empty value entirely, so a boundary publishing a pose outward **shall** substitute the named frame explicitly.
+
+**E.6 A grasp pose is resolved to the tool centre point.** A pose received for execution **shall** be resolved, through the frame tree, to the `Tool` frame named by the intent's `ToolFrame`. A hand-eye calibration resolves to the mechanical interface, and the offset from there to the tool centre point is exactly what it does not measure — so a Server **shall not** execute a pose that resolves only to `MechanicalInterface`, and **shall** refuse it with `ParameterInvalid`.
diff --git a/metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeIds.csv b/metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeIds.csv
new file mode 100644
index 0000000..08bc4ca
--- /dev/null
+++ b/metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeIds.csv
@@ -0,0 +1,259 @@
+ExecutionStateEnum,3001,DataType
+ExecutionStateEnum_EnumStrings,3901,Variable
+BufferModeEnum,3002,DataType
+BufferModeEnum_EnumStrings,3902,Variable
+BlockingModeEnum,3003,DataType
+BlockingModeEnum_EnumStrings,3903,Variable
+TerminationModeEnum,3004,DataType
+TerminationModeEnum_EnumStrings,3904,Variable
+ReleaseModeEnum,3005,DataType
+ReleaseModeEnum_EnumStrings,3905,Variable
+ApproachModeEnum,3006,DataType
+ApproachModeEnum_EnumStrings,3906,Variable
+FrameRoleEnum,3007,DataType
+FrameRoleEnum_EnumStrings,3907,Variable
+OperationalModeEnum,3008,DataType
+OperationalModeEnum_EnumStrings,3908,Variable
+IntentFailureEnum,3009,DataType
+IntentFailureEnum_EnumStrings,3909,Variable
+StopModeEnum,3010,DataType
+StopModeEnum_EnumStrings,3910,Variable
+AxisKindEnum,3011,DataType
+AxisKindEnum_EnumStrings,3911,Variable
+MissionUpdateResultEnum,3012,DataType
+MissionUpdateResultEnum_EnumStrings,3912,Variable
+Pose3DDataType,3050,DataType
+Pose3DDataType_Encoding_DefaultBinary,5001,Object
+MotionConstraintsDataType,3051,DataType
+MotionConstraintsDataType_Encoding_DefaultBinary,5002,Object
+BlendDataType,3052,DataType
+BlendDataType_Encoding_DefaultBinary,5003,Object
+IntentDataType,3053,DataType
+MotionIntentDataType,3054,DataType
+JointMoveIntentDataType,3055,DataType
+JointMoveIntentDataType_Encoding_DefaultBinary,5004,Object
+LinearMoveIntentDataType,3056,DataType
+LinearMoveIntentDataType_Encoding_DefaultBinary,5005,Object
+CircularMoveIntentDataType,3057,DataType
+CircularMoveIntentDataType_Encoding_DefaultBinary,5006,Object
+GraspIntentDataType,3058,DataType
+GraspIntentDataType_Encoding_DefaultBinary,5007,Object
+ReleaseIntentDataType,3059,DataType
+ReleaseIntentDataType_Encoding_DefaultBinary,5008,Object
+PickIntentDataType,3060,DataType
+PickIntentDataType_Encoding_DefaultBinary,5009,Object
+PlaceIntentDataType,3061,DataType
+PlaceIntentDataType_Encoding_DefaultBinary,5010,Object
+ToolChangeIntentDataType,3062,DataType
+ToolChangeIntentDataType_Encoding_DefaultBinary,5011,Object
+SetOutputIntentDataType,3063,DataType
+SetOutputIntentDataType_Encoding_DefaultBinary,5012,Object
+CallProgramIntentDataType,3064,DataType
+CallProgramIntentDataType_Encoding_DefaultBinary,5013,Object
+WaitIntentDataType,3065,DataType
+WaitIntentDataType_Encoding_DefaultBinary,5014,Object
+IntentResultDataType,3066,DataType
+IntentResultDataType_Encoding_DefaultBinary,5015,Object
+MissionStepDataType,3067,DataType
+MissionStepDataType_Encoding_DefaultBinary,5016,Object
+MissionDataType,3068,DataType
+MissionDataType_Encoding_DefaultBinary,5017,Object
+IntentCapabilityDataType,3069,DataType
+IntentCapabilityDataType_Encoding_DefaultBinary,5018,Object
+HasIntentController,4001,ReferenceType
+HasFrameParent,4002,ReferenceType
+RobotIntentRootType,1001,ObjectType
+RobotIntentRootType_Controllers,6001,Object
+RobotIntentRootType_SpecificationVersion,6002,Variable
+IntentControllerType,1002,ObjectType
+IntentControllerType_OperationalMode,6003,Variable
+IntentControllerType_Ready,6004,Variable
+IntentControllerType_ControlOwner,6005,Variable
+IntentControllerType_MaxQueueDepth,6006,Variable
+IntentControllerType_ActiveIntent,6007,Variable
+IntentControllerType_ActiveMission,6008,Variable
+IntentControllerType_Capabilities,6009,Object
+IntentControllerType_Frames,6010,Object
+IntentControllerType_Tools,6011,Object
+IntentControllerType_Locations,6012,Object
+IntentControllerType_Axes,6013,Object
+IntentControllerType_Outputs,6014,Object
+IntentControllerType_Programs,6015,Object
+IntentControllerType_Intents,6016,Object
+IntentControllerType_Missions,6017,Object
+IntentControllerType_RequestControl,6018,Method
+IntentControllerType_RequestControl_OutputArguments,6019,Variable
+IntentControllerType_ReleaseControl,6020,Method
+IntentControllerType_SubmitIntent,6021,Method
+IntentControllerType_SubmitIntent_InputArguments,6022,Variable
+IntentControllerType_SubmitIntent_OutputArguments,6023,Variable
+IntentControllerType_CancelIntent,6024,Method
+IntentControllerType_CancelIntent_InputArguments,6025,Variable
+IntentControllerType_CancelIntent_OutputArguments,6026,Variable
+IntentControllerType_CancelAll,6027,Method
+IntentControllerType_CancelAll_InputArguments,6028,Variable
+IntentControllerType_CancelAll_OutputArguments,6029,Variable
+IntentControllerType_Pause,6030,Method
+IntentControllerType_Pause_OutputArguments,6031,Variable
+IntentControllerType_Resume,6032,Method
+IntentControllerType_Resume_OutputArguments,6033,Variable
+IntentControllerType_Retry,6034,Method
+IntentControllerType_Retry_InputArguments,6035,Variable
+IntentControllerType_Retry_OutputArguments,6036,Variable
+IntentControllerType_SubmitMission,6037,Method
+IntentControllerType_SubmitMission_InputArguments,6038,Variable
+IntentControllerType_SubmitMission_OutputArguments,6039,Variable
+IntentControllerType_UpdateMission,6040,Method
+IntentControllerType_UpdateMission_InputArguments,6041,Variable
+IntentControllerType_UpdateMission_OutputArguments,6042,Variable
+IntentControllerType_CancelMission,6043,Method
+IntentControllerType_CancelMission_InputArguments,6044,Variable
+IntentControllerType_CancelMission_OutputArguments,6045,Variable
+IntentOperationType,1003,ObjectType
+IntentOperationType_IntentId,6046,Variable
+IntentOperationType_Intent,6047,Variable
+IntentOperationType_ExecutionState,6048,Variable
+IntentOperationType_Progress,6049,Variable
+IntentOperationType_CurrentPose,6050,Variable
+IntentOperationType_Result,6051,Variable
+IntentOperationType_MissionId,6052,Variable
+IntentOperationType_QueuePosition,6053,Variable
+MissionType,1004,ObjectType
+MissionType_MissionId,6054,Variable
+MissionType_MissionUpdateId,6055,Variable
+MissionType_Mission,6056,Variable
+MissionType_ExecutionState,6057,Variable
+MissionType_CurrentStepId,6058,Variable
+MissionType_ReleasedStepCount,6059,Variable
+IntentCapabilitiesType,1005,ObjectType
+IntentCapabilitiesType_SupportedIntents,6060,Variable
+IntentCapabilitiesType_MissionsSupported,6061,Variable
+IntentCapabilitiesType_MissionHorizonSupported,6062,Variable
+IntentCapabilitiesType_BlendingSupported,6063,Variable
+IntentCapabilitiesType_MaxBlendRadius,6064,Variable
+IntentCapabilitiesType_AxisCount,6065,Variable
+CoordinateFrameType,1006,ObjectType
+CoordinateFrameType_FrameId,6066,Variable
+CoordinateFrameType_Role,6067,Variable
+CoordinateFrameType_Transform,6068,Variable
+ToolType,1007,ObjectType
+ToolType_ToolId,6069,Variable
+ToolType_Name,6070,Variable
+ToolType_Fitted,6071,Variable
+ToolType_TcpFrame,6072,Variable
+ToolType_Mass,6073,Variable
+ToolType_MaxGraspForce,6074,Variable
+ToolType_MaxOpening,6075,Variable
+LocationType,1008,ObjectType
+LocationType_LocationId,6076,Variable
+LocationType_Name,6077,Variable
+LocationType_Pose,6078,Variable
+LocationType_Occupied,6079,Variable
+LocationType_ObjectClass,6080,Variable
+LocationType_Capacity,6081,Variable
+AxisType,1009,ObjectType
+AxisType_AxisId,6082,Variable
+AxisType_Index,6083,Variable
+AxisType_Kind,6084,Variable
+AxisType_MinPosition,6085,Variable
+AxisType_MaxPosition,6086,Variable
+AxisType_MaxSpeed,6087,Variable
+AxisType_Position,6088,Variable
+OutputSignalType,1010,ObjectType
+OutputSignalType_SignalId,6089,Variable
+OutputSignalType_Name,6090,Variable
+OutputSignalType_Value,6091,Variable
+OutputSignalType_Writable,6092,Variable
+OutputSignalType_EngineeringUnits,6093,Variable
+ProgramType,1011,ObjectType
+ProgramType_ProgramId,6094,Variable
+ProgramType_Name,6095,Variable
+ProgramType_Description,6096,Variable
+ProgramType_Parameters,6097,Variable
+RobotIntent,7001,Object
+SafeMotionFunctionEnum,3013,DataType
+SafeMotionFunctionEnum_EnumStrings,3913,Variable
+RealTimeTransportEnum,3014,DataType
+RealTimeTransportEnum_EnumStrings,3914,Variable
+ChannelInitiatorEnum,3015,DataType
+ChannelInitiatorEnum_EnumStrings,3915,Variable
+ErrorPolicyEnum,3016,DataType
+ErrorPolicyEnum_EnumStrings,3916,Variable
+DivergenceKindEnum,3017,DataType
+DivergenceKindEnum_EnumStrings,3917,Variable
+WeaveShapeEnum,3018,DataType
+WeaveShapeEnum_EnumStrings,3918,Variable
+TrajectoryPointDataType,3070,DataType
+TrajectoryPointDataType_Encoding_DefaultBinary,5019,Object
+MotionToleranceDataType,3071,DataType
+MotionToleranceDataType_Encoding_DefaultBinary,5020,Object
+TrajectoryIntentDataType,3072,DataType
+TrajectoryIntentDataType_Encoding_DefaultBinary,5021,Object
+PathWaypointDataType,3073,DataType
+PathWaypointDataType_Encoding_DefaultBinary,5022,Object
+CartesianPathIntentDataType,3074,DataType
+CartesianPathIntentDataType_Encoding_DefaultBinary,5023,Object
+ForceIntentDataType,3075,DataType
+ForceIntentDataType_Encoding_DefaultBinary,5024,Object
+ProcessIntentDataType,3076,DataType
+ArcWeldIntentDataType,3077,DataType
+ArcWeldIntentDataType_Encoding_DefaultBinary,5025,Object
+SpotWeldIntentDataType,3078,DataType
+SpotWeldIntentDataType_Encoding_DefaultBinary,5026,Object
+DispenseIntentDataType,3079,DataType
+DispenseIntentDataType_Encoding_DefaultBinary,5027,Object
+FastenIntentDataType,3080,DataType
+FastenIntentDataType_Encoding_DefaultBinary,5028,Object
+PalletiseIntentDataType,3081,DataType
+PalletiseIntentDataType_Encoding_DefaultBinary,5029,Object
+SurfaceFinishIntentDataType,3082,DataType
+SurfaceFinishIntentDataType_Encoding_DefaultBinary,5030,Object
+MissionTransitionDataType,3083,DataType
+MissionTransitionDataType_Encoding_DefaultBinary,5031,Object
+KinematicJointDataType,3084,DataType
+KinematicJointDataType_Encoding_DefaultBinary,5032,Object
+SafetyStateType,1012,ObjectType
+SafetyStateType_ActiveFunction,6098,Variable
+SafetyStateType_EmergencyStopActive,6099,Variable
+SafetyStateType_ProtectiveStopActive,6100,Variable
+SafetyStateType_SafeSpeedLimitActive,6101,Variable
+SafetyStateType_SafeSpeedLimit,6102,Variable
+SafetyStateType_SafetyControllerOk,6103,Variable
+SafetyStateType_LastStopReason,6104,Variable
+RealTimeChannelType,1013,ObjectType
+RealTimeChannelType_ChannelId,6105,Variable
+RealTimeChannelType_Transport,6106,Variable
+RealTimeChannelType_EndpointUrl,6107,Variable
+RealTimeChannelType_Initiator,6108,Variable
+RealTimeChannelType_NominalRate,6109,Variable
+RealTimeChannelType_PayloadDescriptor,6110,Variable
+RealTimeChannelType_RequiredMode,6111,Variable
+RealTimeChannelType_Available,6112,Variable
+RealTimeChannelType_LeaseHolder,6113,Variable
+RealTimeChannelType_LeaseExpiry,6114,Variable
+RobotDescriptionType,1014,ObjectType
+RobotDescriptionType_Manufacturer,6115,Variable
+RobotDescriptionType_Model,6116,Variable
+RobotDescriptionType_KinematicChain,6117,Variable
+RobotDescriptionType_MountingPose,6118,Variable
+RobotDescriptionType_ReachRadius,6119,Variable
+RobotDescriptionType_PayloadLimit,6120,Variable
+RobotDescriptionType_MaxCartesianSpeed,6121,Variable
+RobotDescriptionType_MaxCartesianAcceleration,6122,Variable
+IntentControllerType_SafetyState,6123,Object
+IntentControllerType_Description,6124,Object
+IntentControllerType_RealTimeChannels,6125,Object
+IntentControllerType_OpenRealTimeChannel,6126,Method
+IntentControllerType_OpenRealTimeChannel_InputArguments,6127,Variable
+IntentControllerType_OpenRealTimeChannel_OutputArguments,6128,Variable
+IntentControllerType_CloseRealTimeChannel,6129,Method
+IntentControllerType_CloseRealTimeChannel_InputArguments,6130,Variable
+IntentControllerType_CloseRealTimeChannel_OutputArguments,6131,Variable
+IntentCapabilitiesType_TrajectorySupported,6132,Variable
+IntentCapabilitiesType_ForceControlSupported,6133,Variable
+IntentCapabilitiesType_RealTimeChannelsSupported,6134,Variable
+IntentCapabilitiesType_MissionBranchingSupported,6135,Variable
+IntentCapabilitiesType_MaxTrajectoryPoints,6136,Variable
+IntentOperationType_FinalResultData,6137,Object
+IntentOperationType_ProgramDiagnostic,6138,Variable
+IntentCapabilitiesType_SupportedFacets,6139,Variable
diff --git a/metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeSet2.xml b/metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeSet2.xml
new file mode 100644
index 0000000..0a9ca25
--- /dev/null
+++ b/metaverse-specs/robot-intent/Opc.Ua.RobotIntent.NodeSet2.xml
@@ -0,0 +1,2503 @@
+
+
+
+
+ http://opcfoundation.org/UA/RobotIntent/
+
+
+
+
+
+
+
+ i=1
+ i=6
+ i=7
+ i=9
+ i=11
+ i=12
+ i=14
+ i=15
+ i=17
+ i=20
+ i=21
+ i=294
+ i=290
+ i=296
+ i=887
+ i=14533
+ i=24
+ i=47
+ i=46
+ i=45
+ i=35
+ i=40
+ i=37
+ i=17603
+ i=38
+ i=78
+ i=80
+ i=11508
+ i=11510
+
+
+ ExecutionStateEnum
+ Fine-grained execution state of an intent or a mission. This REFINES the Part 10 program state machine rather than restating it: Queued, Cancelling and the three terminal outcomes cannot be told apart from CurrentState alone. Clause 6.3 fixes which ExecutionState may accompany which Part 10 state, and a Server shall satisfy that table.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3901
+
+ Admitted and validated, not yet queued or executing.Waiting behind another intent because BufferMode is Buffered or a blending mode. Corresponds to PLCopen Busy without Active.Commanding the robot now.Paused by request; position is retained and execution can resume.A cancel was accepted and the Server is bringing the motion to a controlled end. Not yet terminal.Terminal. Completed as requested.Terminal. Did not complete; Failure carries the reason.Terminal. Ended early because a cancel was accepted.Terminal for now, but the Server can re-attempt it on Retry. A Server that does not offer Retry never enters this state and reports Failed instead.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3001
+
+ AcceptedQueuedExecutingSuspendedCancellingSucceededFailedCancelledRetriable
+
+
+ BufferModeEnum
+ How a newly submitted intent relates to the one already executing. The values and their meanings are those of PLCopen Motion Control MC_BufferMode, adopted unchanged because every motion runtime already implements them. In all blending modes the robot does not decelerate to a stop at the boundary, and the predecessor reaches Succeeded when blending begins rather than when its target is exactly attained.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3902
+
+ Abort what is executing and start immediately. The aborted intent terminates as Cancelled. This is the default.Queue; start when the predecessor succeeds.Blend at the lower of the two boundary speeds.Blend at the predecessor's boundary speed.Blend at the successor's boundary speed.Blend at the higher of the two boundary speeds.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3002
+
+ AbortingBufferedBlendingLowBlendingPreviousBlendingNextBlendingHigh
+
+
+ BlockingModeEnum
+ Whether an intent tolerates motion and other intents running alongside it. The four values are the two-by-two matrix of VDA 5050 blockingType, adopted because it is the only widely deployed concurrency annotation for robot actions.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3903
+
+ Runs in the background; motion may continue and other intents may run concurrently.Motion stops for the duration; other intents may still run.Motion may continue; no other intent may run concurrently.Motion stops and no other intent may run concurrently. The intent has exclusive use of the robot.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3003
+
+ NoneSoftSingleHard
+
+
+ TerminationModeEnum
+ Whether a motion ends exactly on its target or is blended into the next one. This is the only distinction every vendor expresses identically; the blend magnitude in BlendDataType.Radius is a request, not a guarantee.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3904
+
+ Come to rest on the target before the next motion begins. ABB fine, FANUC FINE, Yaskawa PL=0, KUKA no approximation.Round the corner into the next motion without stopping.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3004
+
+ ExactBlend
+
+
+ ReleaseModeEnum
+ How a held object is given up.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3905
+
+ Open the end effector where it is, without placing.Set the object down under control at the target.Retain the object until the Server judges a receiving party has taken it.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3005
+
+ DropPlaceHandover
+
+
+ ApproachModeEnum
+ Direction from which an end effector approaches an object or a placement.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3906
+
+ The Server chooses.Along the tool's own Z axis.From above, in the frame the target is expressed in.Laterally, in the frame the target is expressed in.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3006
+
+ DefaultToolZTopSide
+
+
+ FrameRoleEnum
+ Role of a coordinate frame, following the coordinate systems ISO 9787 standardises. The roles say WHICH frames exist; no standard says how to calibrate between them, which is why CoordinateFrameType carries the transform explicitly.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3907
+
+ The cell-level reference frame.The robot base frame.The flange at the end of the last link, to which an end effector is fitted.A tool frame, whose origin is a tool centre point.A workpiece or work-object frame.A frame whose role is none of the above.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3007
+
+ WorldBaseMechanicalInterfaceToolObjectOther
+
+
+ OperationalModeEnum
+ Operational mode of the robot system, as defined by ISO 10218-1 and reported identically by OPC 40010-1. It is READ-ONLY here: mode selection is a safety function performed by safety-rated means, and this specification defines no way to command it. Clause 10 restricts intent submission to Automatic and AutomaticExternal.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3908
+
+ Booting, uncalibrated, or a safety system fault.Teaching mode with a speed ceiling and a held enabling device.Program verification with an enabling device.Automatic operation with the safeguarded space secured.Automatic operation commanded by an external system. This is the mode this specification is written for.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3008
+
+ OtherManualReducedSpeedManualHighSpeedAutomaticAutomaticExternal
+
+
+ IntentFailureEnum
+ Why an intent did not succeed. The set is deliberately small and diagnosable: a client decides whether to retry, re-plan or escalate from this value alone, and reads Message only to show a human.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3909
+
+ No failure. Reported on a successful outcome.The target lies outside the reachable workspace.No kinematic solution, or a singularity on the path.A collision was predicted or detected.A joint limit would be or was exceeded.The requested speed or acceleration is not permitted in the active mode.The required tool is not fitted or not identified.The object to act on was not present.The object was not acquired, or was lost in transit.The intent did not complete within its permitted time.Refused because the operational mode does not permit it. See clause 10.Refused because the caller does not hold command authority. See clause 8.The Server does not implement this intent type, or this combination of options.A parameter was missing, malformed or out of range.The queue is at MaxQueueDepth.An Aborting submission or a mission update replaced it before it could run.A fault in the robot, the end effector or the controller.A safety function acted. The safety system, not this interface, decided this.A reason none of the above describes; see Message.Refused because the request would exceed a limit the safety system is enforcing. See clause 10.3.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3009
+
+ NoneUnreachableKinematicsCollisionJointLimitSpeedLimitToolMissingObjectNotFoundGraspFailedTimeoutNotPermittedInModeControlNotOwnedCapabilityNotSupportedParameterInvalidQueueFullSupersededHardwareFaultSafetyStopOtherSafetyLimitExceeded
+
+
+ StopModeEnum
+ How urgently a cancellation should bring motion to an end. The values are those of PossibleStopModes in OPC 40010-1, so a Server implementing both reports one vocabulary. This is an APPLICATION-LEVEL request: it does not select, imply or guarantee any IEC 60204-1 stop category, which only the robot's safety system determines. See clause 10.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3910
+
+ Decelerate along the programmed path.Stop when the current cycle completes.Stop at a point the process defines as safe.Decelerate as quickly as the drives allow.Stop when the current instruction completes.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3010
+
+ OnPathEndOfCycleProcessStopQuickStopEndOfInstruction
+
+
+ AxisKindEnum
+ Whether an axis rotates or translates. This fixes the unit of the corresponding entry of JointMoveIntentDataType.JointTargets.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3911
+
+ Rotates. Its joint target is in radians.Translates. Its joint target is in metres.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3011
+
+ RevolutePrismatic
+
+
+ MissionUpdateResultEnum
+ Outcome of a mission update, reported so a client can tell a stale update from a rejected one without parsing a StatusCode.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3912
+
+ The horizon was replaced as requested.MissionUpdateId was not greater than the current one.The update would have altered a released step.No mission with that MissionId is held.Refused for a reason the Server states in a message.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3012
+
+ AcceptedOutdatedBaseConflictUnknownMissionRejected
+
+
+ Pose3DDataType
+ A rigid-body pose. Position is metres; Orientation is a UNIT QUATERNION ordered (x, y, z, w). Quaternions are used because OPC UA defines no quaternion type and the Euler triple in ThreeDOrientation is ambiguous without an external convention; Annex C gives the normative conversion to and from ThreeDFrame. All frames are right-handed. FrameId names the CoordinateFrame the pose is expressed in; an empty FrameId means the Server's default work frame.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5001
+
+ FrameId of the CoordinateFrame this pose is expressed in; empty for the default work frame.Translation (x, y, z) in metres.Unit quaternion (x, y, z, w).
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3050
+
+
+
+ MotionConstraintsDataType
+ Limits a motion is to respect. Every field is a REQUEST bounded by what the robot is configured to permit: a Server clamps rather than refuses, except where clause 10 requires refusal. A value of zero or less means the field is unspecified and the Server chooses.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5002
+
+ Fraction of the configured maximum speed, in the range 0 to 1. This is the portable speed control: every vendor supports it.Tool centre point speed in metres per second. Ignored by joint moves.Tool centre point acceleration in metres per second squared.Rate of change of acceleration in metres per second cubed.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3051
+
+
+
+ BlendDataType
+ How a motion ends. Radius is interpreted only when Termination is Blend, and is a request: controllers that expose a unitless blend scale rather than a distance map it as best they can, and a Server that cannot honour the exact radius still succeeds.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5003
+
+ Exact stop, or blend into the next motion.Blend radius in metres, measured from the target. Zero or less means the Server chooses.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3052
+
+
+
+ IntentDataType
+ Abstract base of every intent. An intent is a single task-level request; it is what a client submits and what a mission step holds, so the two are the same shape. Extension is by SUBTYPING this structure, which keeps new intents discoverable through IntentCapabilitiesType rather than by probing for BrowseNames.
+ RobotIntent DataTypes
+
+ i=22
+
+ Client-assigned identifier, unique among the intents the client has outstanding. Empty asks the Server to assign one, which it returns.Human-readable description. Never interpreted.How this intent relates to one already executing.Whether motion and other intents may proceed alongside it.
+
+
+ MotionIntentDataType
+ Abstract base of the intents that move the robot. ToolFrame names the frame whose origin is driven to the target - without it a pose target is meaningless, and OPC 40010-1 defines no tool centre point at all.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+
+ The CoordinateFrame, of role Tool, whose origin is driven to the target. Null means the tool currently fitted.Speed and acceleration limits for this motion.How this motion ends.
+
+
+ JointMoveIntentDataType
+ Move by interpolating in joint space. This is the fastest way between two configurations and the path the tool centre point takes is not controlled. It is the portable equivalent of PTP, MoveJ, J and MOVJ. Giving a pose rather than joint values asks the Server to solve the kinematics itself, which is the 'move to this pose, you choose how' case.
+ RobotIntent DataTypes
+
+ ns=1;i=3054
+ ns=1;i=5004
+
+ True when JointTargets is meaningful; False when TargetPose is.One value per axis, in the order the axes are declared under the controller: radians for a Revolute axis, metres for a Prismatic one.Pose to reach, used when HasJointTargets is False.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3055
+
+
+
+ LinearMoveIntentDataType
+ Move the tool centre point along a straight line to the target. The portable equivalent of LIN, MoveL, L and MOVL.
+ RobotIntent DataTypes
+
+ ns=1;i=3054
+ ns=1;i=5005
+
+ Pose to reach.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3056
+
+
+
+ CircularMoveIntentDataType
+ Move the tool centre point along the circular arc that passes through ViaPoint and ends at Target. The portable equivalent of CIRC, MoveC, C and MOVC. Only the position of ViaPoint defines the arc; its orientation is ignored.
+ RobotIntent DataTypes
+
+ ns=1;i=3054
+ ns=1;i=5006
+
+ A pose on the arc between the start and the target. Only its position is used.Pose to reach.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3057
+
+
+
+ GraspIntentDataType
+ Close the end effector on an object. Force and Width are requests; an end effector that cannot regulate force ignores Force and still succeeds.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5007
+
+ The Tool to actuate. Null means the tool currently fitted.Grasp force in newtons. Zero or less means the Server chooses.Opening at which to close, in metres. Zero or less means the Server chooses.Direction of approach.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3058
+
+
+
+ ReleaseIntentDataType
+ Give up a held object.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5008
+
+ The Tool to actuate. Null means the tool currently fitted.How the object is given up.True when Target is meaningful.Where to place the object, when Mode is Place.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3059
+
+
+
+ PickIntentDataType
+ Take an object from a location. Source is a REFERENCE TO A LOCATION NODE, not a name: the location's pose and its properties are then read from the address space, so the station identity has exactly one definition.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5009
+
+ The Location to pick from.The Tool to use. Null means the tool currently fitted.What to pick, when the location can hold more than one kind. Empty means whatever is there.Grasp force in newtons. Zero or less means the Server chooses.Direction of approach.Further named parameters the Server declares in its capabilities.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3060
+
+
+
+ PlaceIntentDataType
+ Put a held object at a location. Destination is a reference to a Location node, for the same reason as PickIntentDataType.Source.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5010
+
+ The Location to place at.The Tool to use. Null means the tool currently fitted.Direction of approach.Further named parameters the Server declares in its capabilities.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3061
+
+
+
+ ToolChangeIntentDataType
+ Exchange the fitted end effector.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5011
+
+ The Tool to fit. Null means release the fitted tool and fit nothing.The Location of the tool changer. Null lets the Server choose.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3062
+
+
+
+ SetOutputIntentDataType
+ Set a discrete or analogue output. Output references an OutputSignal node, so the signal's meaning, range and unit are described once in the address space instead of being implied by a string.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5012
+
+ The OutputSignal to write.Value to write, of the signal's own DataType.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3063
+
+
+
+ CallProgramIntentDataType
+ Run a program that already exists on the controller. This is the escape hatch for capability this specification does not model, and the bridge to the OPC 40010-1 task control surface.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5013
+
+ The Program to run.Named arguments for the program.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3064
+
+
+
+ WaitIntentDataType
+ Do nothing for a while, or until released. A mission needs this to express a rendezvous with something the robot does not control; without it a client has to hold the queue open from outside.
+ RobotIntent DataTypes
+
+ ns=1;i=3053
+ ns=1;i=5014
+
+ How long to wait, in milliseconds. Zero or less waits until Signal is released.An OutputSignal under this controller, or another Variable of DataType Boolean, whose becoming true ends the wait. Null waits only for Duration. Clause 11.3 requires the Server to check that it resolves to one of those two, under the controller being commanded, and to refuse it otherwise.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3065
+
+
+
+ IntentResultDataType
+ The outcome of one intent, preserved after it terminates. AchievedPose records where the tool centre point actually ended, which is what lets a client tell a blended corner from an exact stop and audit a placement.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5015
+
+ The intent this describes.Terminal state reached.Reason, or None on success.Human-readable detail. Never parsed.True when AchievedPose is meaningful.Where the tool centre point came to rest, or was when blending began.When execution began.When the terminal state was reached.Named results the intent produced, for example the identity of a picked object.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3066
+
+
+
+ MissionStepDataType
+ One step of a mission. Released is what splits a mission into its immutable base and its revisable horizon: a released step has been committed and may already be executing, so an update may not touch it. Status is a HINT - where Operation is not null, that IntentOperation's state machine decides.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5016
+
+ Identifier unique within the mission.Execution order within the mission, ascending.True for a base step, which is committed and immutable. False for a horizon step, which an update may replace or remove.The intent this step executes.Reported status. Where Operation is not null its state machine is authoritative and this reflects it.The IntentOperation executing this step, or null while the step has not begun.What the mission does when this step does not succeed.The step to continue at, when ErrorPolicy is Fallback or Compensate.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3067
+
+
+
+ MissionDataType
+ An ordered sequence of intents submitted and tracked as a unit. MissionUpdateId increases with every update, so a Server can reject an update that crossed with another in flight instead of applying it out of order.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5017
+
+ Client-assigned identifier. Empty asks the Server to assign one, which it returns.Revision of this mission, increasing. The first submission is 0.Human-readable description. Never interpreted.The steps, in ascending SequenceId order.The step graph. An empty array means the steps run in order, which is the flat sequence a mission without branching has always been.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3068
+
+
+
+ IntentCapabilityDataType
+ What the Server will accept for one intent type. This is the machine-readable declaration that makes an intent surface discoverable: a client reads it once and knows what it may submit, instead of submitting to find out. It is the analogue of the VDA 5050 factsheet.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5018
+
+ The DataType of the intent, a subtype of IntentDataType.What this Server does with it.True when Cancel is honoured for it. A Server may still refuse a particular cancel; see clause 6.5.True when Pause and Resume are honoured for a running intent. False when the Server can only stop its queue: suspending execution means the robot stops, and Suspended is never reported while it is still moving.True when it can terminate Retriable and be re-attempted.Buffer modes accepted for it. Aborting is always accepted and always listed.Blocking modes accepted for it.Named parameters this Server recognises in the intent's Attributes field.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3069
+
+
+
+ HasIntentController
+ Binds an intent surface to the thing it commands. This is how the model attaches to a robot described by another specification - an OPC 40010-1 MotionDeviceSystem, say - without depending on it. Annex B defines that binding.
+ RobotIntent ReferenceTypes
+ IntentControllerOf
+
+ i=32
+
+
+
+ HasFrameParent
+ From a CoordinateFrame to the frame its Transform is expressed in. Frames form a tree, so a pose given in one frame can be re-expressed in another by composing the transforms along the path between them.
+ RobotIntent ReferenceTypes
+ FrameParentOf
+
+ i=32
+
+
+
+ RobotIntentRootType
+ Server-level entry point. A client that has just connected browses here to find every robot it can command, without knowing the Server's layout.
+ RobotIntent
+
+ i=58
+ ns=1;i=6001
+ ns=1;i=6002
+
+
+
+ Controllers
+ The intent surfaces this Server offers, one per commandable robot.
+
+ i=78
+ i=61
+ ns=1;i=1001
+
+
+
+ SpecificationVersion
+ Release of this specification the Server implements, for example '0.1.0'.
+
+ i=78
+ i=68
+ ns=1;i=1001
+
+
+
+ IntentControllerType
+ The intent surface for one robot: what it can be asked to do, the frames and objects those requests refer to, and the intents and missions currently outstanding. Everything a client needs in order to command the robot hangs from here.
+ RobotIntent
+
+ i=58
+ ns=1;i=6003
+ ns=1;i=6004
+ ns=1;i=6005
+ ns=1;i=6006
+ ns=1;i=6007
+ ns=1;i=6008
+ ns=1;i=6009
+ ns=1;i=6010
+ ns=1;i=6011
+ ns=1;i=6012
+ ns=1;i=6013
+ ns=1;i=6014
+ ns=1;i=6015
+ ns=1;i=6016
+ ns=1;i=6017
+ ns=1;i=6018
+ ns=1;i=6020
+ ns=1;i=6021
+ ns=1;i=6024
+ ns=1;i=6027
+ ns=1;i=6030
+ ns=1;i=6032
+ ns=1;i=6034
+ ns=1;i=6037
+ ns=1;i=6040
+ ns=1;i=6043
+ ns=1;i=6123
+ ns=1;i=6124
+ ns=1;i=6125
+ ns=1;i=6126
+ ns=1;i=6129
+
+
+
+ OperationalMode
+ Operational mode reported by the robot. Read-only: mode selection is a safety function and this specification defines no way to command it. Clause 10 restricts submission to Automatic and AutomaticExternal.
+
+ i=78
+ i=68
+ ns=1;i=1002
+
+
+
+ Ready
+ True when the robot will accept intents now. A client checks this before submitting rather than inferring readiness from OperationalMode alone.
+
+ i=78
+ i=68
+ ns=1;i=1002
+
+
+
+ ControlOwner
+ SessionId of the client that currently holds command authority, or null when no client does. Only that client may submit; see clause 8.
+
+ i=78
+ i=68
+ ns=1;i=1002
+
+
+
+ MaxQueueDepth
+ How many intents may be queued behind the executing one. Zero means the Server accepts only Aborting submissions.
+
+ i=78
+ i=68
+ ns=1;i=1002
+
+
+
+ ActiveIntent
+ The IntentOperation executing now, or null.
+
+ i=78
+ i=63
+ ns=1;i=1002
+
+
+
+ ActiveMission
+ The Mission executing now, or null.
+
+ i=80
+ i=63
+ ns=1;i=1002
+
+
+
+ Capabilities
+ What this robot will accept.
+
+ i=78
+ ns=1;i=1005
+ ns=1;i=1002
+
+
+
+ Frames
+ The coordinate frames poses may be expressed in.
+
+ i=78
+ i=61
+ ns=1;i=1002
+
+
+
+ Tools
+ The end effectors this robot can use.
+
+ i=78
+ i=61
+ ns=1;i=1002
+
+
+
+ Locations
+ The named places intents refer to.
+
+ i=78
+ i=61
+ ns=1;i=1002
+
+
+
+ Axes
+ The axes, in the order JointMoveIntentDataType.JointTargets uses.
+
+ i=78
+ i=61
+ ns=1;i=1002
+
+
+
+ Outputs
+ The signals SetOutput can write.
+
+ i=80
+ i=61
+ ns=1;i=1002
+
+
+
+ Programs
+ The controller programs CallProgram can run.
+
+ i=80
+ i=61
+ ns=1;i=1002
+
+
+
+ Intents
+ Outstanding and recently completed intents, one Object each.
+
+ i=78
+ i=61
+ ns=1;i=1002
+
+
+
+ Missions
+ Outstanding and recently completed missions, one Object each.
+
+ i=80
+ i=61
+ ns=1;i=1002
+
+
+
+ RequestControl
+ Take command authority. A Server grants it only when no other Session holds it, or when the holder's Session has closed. Holding authority is a precondition for submitting, and exists so that two clients cannot interleave motion; it is NOT the single point of control that ISO 10218-2 requires, which is enforced by safety-rated means outside this interface.
+
+ i=78
+ ns=1;i=1002
+ ns=1;i=6019
+
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6018
+
+ i=297Grantedi=1-1True when the caller now holds authority.i=297CurrentOwneri=17-1SessionId of the holder after the call.
+
+
+ ReleaseControl
+ Give up command authority. Outstanding intents are unaffected; use CancelAll to stop them.
+
+ i=78
+ ns=1;i=1002
+
+
+
+ SubmitIntent
+ Submit one intent. Returns as soon as the intent is admitted - NOT when the robot has finished, which may be minutes later. The returned Operation is a node the client subscribes to for progress and reads for the result. This is the whole reason the model is built on the Part 10 program lifecycle: an OPC UA Call cannot stay open for the duration of a motion.
+
+ i=78
+ ns=1;i=1002
+ ns=1;i=6022
+ ns=1;i=6023
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6021
+
+ i=297Intentns=1;i=3053-1The intent to execute.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6021
+
+ i=297Acceptedi=1-1True when the intent was admitted. False is a refusal, which clause 10.5 makes an ordinary outcome rather than an error.i=297IntentIdi=12-1Identifier of the intent, assigned by the Server when the request left it empty. Empty on a refusal.i=297Operationi=17-1The IntentOperation that tracks it. Null on a refusal, because a refusal creates no operation instance.i=297Failurens=1;i=3009-1Why the intent was refused, or None when it was admitted.i=297Messagei=21-1Human-readable detail on a refusal. For a human; never parsed.
+
+
+ CancelIntent
+ Ask the Server to end an intent early. The Server MAY refuse, and says so in Accepted, because some motions cannot be abandoned safely part-way. This is not the OPC UA Cancel Service, which discards a pending response and leaves the robot moving; see clause 6.5.
+
+ i=78
+ ns=1;i=1002
+ ns=1;i=6025
+ ns=1;i=6026
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6024
+
+ i=297IntentIdi=12-1The intent to cancel.i=297StopModens=1;i=3010-1How urgently to stop.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6024
+
+ i=297Acceptedi=1-1True when the Server will act on it.
+
+
+ CancelAll
+ Ask the Server to end every outstanding intent and mission.
+
+ i=78
+ ns=1;i=1002
+ ns=1;i=6028
+ ns=1;i=6029
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6027
+
+ i=297StopModens=1;i=3010-1How urgently to stop.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6027
+
+ i=297Cancelledi=7-1How many were acted on.
+
+
+ Pause
+ Suspend execution, retaining position so it can be resumed.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6031
+
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6030
+
+ i=297Acceptedi=1-1True when execution is suspending.
+
+
+ Resume
+ Continue execution suspended by Pause.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6033
+
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6032
+
+ i=297Acceptedi=1-1True when execution is resuming.
+
+
+ Retry
+ Re-attempt an intent that terminated Retriable.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6035
+ ns=1;i=6036
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6034
+
+ i=297IntentIdi=12-1The intent to re-attempt.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6034
+
+ i=297Acceptedi=1-1True when a new attempt was admitted.i=297Operationi=17-1The IntentOperation that tracks the new attempt. Null on a refusal.i=297Failurens=1;i=3009-1Why the re-attempt was refused, or None when it was admitted.i=297Messagei=21-1Human-readable detail on a refusal.
+
+
+ SubmitMission
+ Submit an ordered sequence of intents as one unit. Steps marked Released form the base and are committed; the rest form the horizon and may still be revised by UpdateMission.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6038
+ ns=1;i=6039
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6037
+
+ i=297Missionns=1;i=3068-1The mission to execute.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6037
+
+ i=297Acceptedi=1-1True when the mission was admitted.i=297MissionIdi=12-1Identifier of the mission, assigned by the Server when the request left it empty. Empty on a refusal.i=297Operationi=17-1The Mission that tracks it. Null on a refusal.i=297Failurens=1;i=3009-1Why the mission was refused, or None when it was admitted.i=297Messagei=21-1Human-readable detail on a refusal.
+
+
+ UpdateMission
+ Replace the horizon of a mission already submitted. The base is untouchable: it has been committed and may already have executed, so an update that would alter a released step is refused rather than partly applied.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6041
+ ns=1;i=6042
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6040
+
+ i=297MissionIdi=12-1The mission to update.i=297MissionUpdateIdi=7-1Revision of the update. Must be greater than the mission's current value.i=297Stepsns=1;i=306710The steps that replace the horizon.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6040
+
+ i=297Resultns=1;i=3012-1Outcome of the update.i=297Messagei=21-1Human-readable detail on a refusal.
+
+
+ CancelMission
+ Ask the Server to end a mission and every intent belonging to it.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6044
+ ns=1;i=6045
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6043
+
+ i=297MissionIdi=12-1The mission to cancel.i=297StopModens=1;i=3010-1How urgently to stop.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6043
+
+ i=297Acceptedi=1-1True when the Server will act on it.
+
+
+ IntentOperationType
+ One submitted intent, tracked to completion. It is a Part 10 program instance, so its lifecycle is the one OPC UA already defines for work that outlives a service call: transitions raise ProgramTransitionEvents, the terminal result survives in FinalResultData, and ProgramDiagnostic2DataType records which Session commanded it without this specification having to model provenance itself.
+ RobotIntent
+
+ i=2391
+ ns=1;i=6046
+ ns=1;i=6047
+ ns=1;i=6048
+ ns=1;i=6049
+ ns=1;i=6050
+ ns=1;i=6051
+ ns=1;i=6052
+ ns=1;i=6053
+ ns=1;i=6137
+ ns=1;i=6138
+
+
+
+ IntentId
+ Identifier of the intent this instance executes.
+
+ i=78
+ i=68
+ ns=1;i=1003
+
+
+
+ Intent
+ The intent as admitted, after the Server applied its defaults. A client reads this to learn what it actually asked for.
+
+ i=78
+ i=63
+ ns=1;i=1003
+
+
+
+ ExecutionState
+ Fine-grained state. It refines CurrentState rather than restating it; clause 6.3 fixes which pairs are legal.
+
+ i=78
+ i=68
+ ns=1;i=1003
+
+
+
+ Progress
+ Fraction of the intent completed, 0 to 1, where the Server can estimate it. Negative means it cannot.
+
+ i=80
+ i=68
+ ns=1;i=1003
+
+
+
+ CurrentPose
+ Where the driven tool centre point is now. Subscribe for tracking; this is a status report at the sampling rate the client asks for, not a control signal, and clause 4.3 explains why it must not be used as one.
+
+ i=80
+ i=63
+ ns=1;i=1003
+
+
+
+ Result
+ Outcome, meaningful once ExecutionState is terminal. The same value is placed under FinalResultData so a Part 10 client finds it where Part 10 says it will be.
+
+ i=78
+ i=63
+ ns=1;i=1003
+
+
+
+ MissionId
+ The mission this intent belongs to, or empty when it was submitted alone.
+
+ i=80
+ i=68
+ ns=1;i=1003
+
+
+
+ QueuePosition
+ Place in the queue while ExecutionState is Queued, 1 being next. Zero once it is no longer queued.
+
+ i=80
+ i=68
+ ns=1;i=1003
+
+
+
+ MissionType
+ One submitted mission, tracked to completion. It is a Part 10 program instance for the same reasons an IntentOperation is, and it owns the IntentOperations of its steps.
+ RobotIntent
+
+ i=2391
+ ns=1;i=6054
+ ns=1;i=6055
+ ns=1;i=6056
+ ns=1;i=6057
+ ns=1;i=6058
+ ns=1;i=6059
+
+
+
+ MissionId
+ Identifier of the mission this instance executes.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ MissionUpdateId
+ Revision currently in force.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ Mission
+ The mission as it now stands, base and horizon together.
+
+ i=78
+ i=63
+ ns=1;i=1004
+
+
+
+ ExecutionState
+ Fine-grained state, refining CurrentState as clause 6.3 fixes.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ CurrentStepId
+ The step executing now, or empty.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ ReleasedStepCount
+ How many steps are in the base. The first this many steps of Mission.Steps are committed; the rest are the horizon.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ IntentCapabilitiesType
+ What one robot will accept. A client reads this once, before it submits anything, and knows what the robot can do and under what constraints.
+ RobotIntent
+
+ i=58
+ ns=1;i=6060
+ ns=1;i=6061
+ ns=1;i=6062
+ ns=1;i=6063
+ ns=1;i=6064
+ ns=1;i=6065
+ ns=1;i=6132
+ ns=1;i=6133
+ ns=1;i=6134
+ ns=1;i=6135
+ ns=1;i=6136
+ ns=1;i=6139
+
+
+
+ SupportedIntents
+ One entry per intent type this Server accepts.
+
+ i=78
+ i=63
+ ns=1;i=1005
+
+
+
+ MissionsSupported
+ True when SubmitMission is implemented.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ MissionHorizonSupported
+ True when UpdateMission can revise the horizon of a running mission.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ BlendingSupported
+ True when the blending buffer modes actually blend. A Server that treats them as Buffered reports False, so a client is not misled about the path.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ MaxBlendRadius
+ Largest blend radius the robot will honour, in metres. Zero or less means it is not bounded here.
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ AxisCount
+ How many axes JointMoveIntentDataType.JointTargets must carry.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ CoordinateFrameType
+ A named right-handed Cartesian frame. Frames form a tree through HasFrameParent, so a client can compose a chain from a tool frame to a world frame. Roles follow ISO 9787, which standardises which frames exist; the transform between them is carried explicitly because no standard says how to calibrate it.
+ RobotIntent
+
+ i=58
+ ns=1;i=6066
+ ns=1;i=6067
+ ns=1;i=6068
+
+
+
+ FrameId
+ Identifier referenced by Pose3DDataType.FrameId. Unique among the frames of one controller.
+
+ i=78
+ i=68
+ ns=1;i=1006
+
+
+
+ Role
+ Role of this frame.
+
+ i=78
+ i=68
+ ns=1;i=1006
+
+
+
+ Transform
+ Pose of this frame within its parent. Ignored for a root frame.
+
+ i=80
+ i=63
+ ns=1;i=1006
+
+
+
+ ToolType
+ An end effector. Its tool centre point is a CoordinateFrame of role Tool, which is what a motion intent drives to a target - OPC 40010-1 models robot topology in detail but has no tool centre point at all, so this specification supplies one.
+ RobotIntent
+
+ i=58
+ ns=1;i=6069
+ ns=1;i=6070
+ ns=1;i=6071
+ ns=1;i=6072
+ ns=1;i=6073
+ ns=1;i=6074
+ ns=1;i=6075
+
+
+
+ ToolId
+ Identifier unique within the controller.
+
+ i=78
+ i=68
+ ns=1;i=1007
+
+
+
+ Name
+ Human-readable name.
+
+ i=78
+ i=68
+ ns=1;i=1007
+
+
+
+ Fitted
+ True when this tool is on the robot now.
+
+ i=78
+ i=68
+ ns=1;i=1007
+
+
+
+ TcpFrame
+ The CoordinateFrame, of role Tool, that is this tool's centre point.
+
+ i=78
+ i=68
+ ns=1;i=1007
+
+
+
+ Mass
+ Mass in kilograms. Zero or less when not stated.
+
+ i=80
+ i=68
+ ns=1;i=1007
+
+
+
+ MaxGraspForce
+ Largest grasp force in newtons, or zero when the tool does not grasp.
+
+ i=80
+ i=68
+ ns=1;i=1007
+
+
+
+ MaxOpening
+ Largest opening in metres, or zero when the tool does not grasp.
+
+ i=80
+ i=68
+ ns=1;i=1007
+
+
+
+ LocationType
+ A named place an intent can refer to. Pick and Place reference these nodes rather than naming a station in a string, so a location has one definition that a client can read, subscribe to and reason about.
+ RobotIntent
+
+ i=58
+ ns=1;i=6076
+ ns=1;i=6077
+ ns=1;i=6078
+ ns=1;i=6079
+ ns=1;i=6080
+ ns=1;i=6081
+
+
+
+ LocationId
+ Identifier unique within the controller.
+
+ i=78
+ i=68
+ ns=1;i=1008
+
+
+
+ Name
+ Human-readable name.
+
+ i=78
+ i=68
+ ns=1;i=1008
+
+
+
+ Pose
+ Where the location is.
+
+ i=78
+ i=63
+ ns=1;i=1008
+
+
+
+ Occupied
+ True when the Server believes something is there.
+
+ i=80
+ i=68
+ ns=1;i=1008
+
+
+
+ ObjectClass
+ What the location holds, when it holds one kind of thing. Empty otherwise.
+
+ i=80
+ i=68
+ ns=1;i=1008
+
+
+
+ Capacity
+ How many objects it can hold. Zero means unstated.
+
+ i=80
+ i=68
+ ns=1;i=1008
+
+
+
+ AxisType
+ One axis of the robot. The order of these nodes under the controller's Axes folder fixes the order of JointMoveIntentDataType.JointTargets, and Kind fixes each entry's unit.
+ RobotIntent
+
+ i=58
+ ns=1;i=6082
+ ns=1;i=6083
+ ns=1;i=6084
+ ns=1;i=6085
+ ns=1;i=6086
+ ns=1;i=6087
+ ns=1;i=6088
+
+
+
+ AxisId
+ Identifier unique within the controller.
+
+ i=78
+ i=68
+ ns=1;i=1009
+
+
+
+ Index
+ Position in JointTargets, counting from zero.
+
+ i=78
+ i=68
+ ns=1;i=1009
+
+
+
+ Kind
+ Whether it rotates or translates, which fixes the unit of its joint target.
+
+ i=78
+ i=68
+ ns=1;i=1009
+
+
+
+ MinPosition
+ Lower limit, in radians or metres by Kind.
+
+ i=78
+ i=68
+ ns=1;i=1009
+
+
+
+ MaxPosition
+ Upper limit, in radians or metres by Kind.
+
+ i=78
+ i=68
+ ns=1;i=1009
+
+
+
+ MaxSpeed
+ Largest speed, in radians or metres per second by Kind.
+
+ i=80
+ i=68
+ ns=1;i=1009
+
+
+
+ Position
+ Where the axis is now, in radians or metres by Kind.
+
+ i=80
+ i=63
+ ns=1;i=1009
+
+
+
+ OutputSignalType
+ A signal SetOutput can write. Modelling it as a node means the range, the unit and the meaning are described once, instead of every client having to know what a line name implies.
+ RobotIntent
+
+ i=58
+ ns=1;i=6089
+ ns=1;i=6090
+ ns=1;i=6091
+ ns=1;i=6092
+ ns=1;i=6093
+
+
+
+ SignalId
+ Identifier unique within the controller.
+
+ i=78
+ i=68
+ ns=1;i=1010
+
+
+
+ Name
+ Human-readable name.
+
+ i=78
+ i=68
+ ns=1;i=1010
+
+
+
+ Value
+ Current value.
+
+ i=78
+ i=63
+ ns=1;i=1010
+
+
+
+ Writable
+ True when SetOutput may write it.
+
+ i=78
+ i=68
+ ns=1;i=1010
+
+
+
+ EngineeringUnits
+ Unit of an analogue signal.
+
+ i=80
+ i=68
+ ns=1;i=1010
+
+
+
+ ProgramType
+ A program held on the controller that CallProgram can run. This is the bridge to capability this specification does not model, and to the programs an OPC 40010-1 task control already exposes.
+ RobotIntent
+
+ i=58
+ ns=1;i=6094
+ ns=1;i=6095
+ ns=1;i=6096
+ ns=1;i=6097
+
+
+
+ ProgramId
+ Identifier unique within the controller.
+
+ i=78
+ i=68
+ ns=1;i=1011
+
+
+
+ Name
+ Human-readable name.
+
+ i=78
+ i=68
+ ns=1;i=1011
+
+
+
+ Description
+ What the program does.
+
+ i=80
+ i=68
+ ns=1;i=1011
+
+
+
+ Parameters
+ Named parameters it accepts, with their default values.
+
+ i=80
+ i=63
+ ns=1;i=1011
+
+
+
+ RobotIntent
+ Entry point for robot intent on this Server. A client browses Server/RobotIntent/Controllers to find every robot it can command.
+
+ ns=1;i=1001
+ i=2253
+
+
+
+ SafeMotionFunctionEnum
+ The safe motion function a safety system is enforcing, as defined by IEC 61800-5-2. This is a REPORT. The safety system enforces these independently of this interface, and a client reading them has not thereby obtained any safety function - see clause 10.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3913
+
+ No safe motion function is active.Safe Torque Off: torque is removed.Safe Stop 1: a controlled ramp to standstill, then Safe Torque Off.Safe Stop 2: a controlled ramp to standstill, which is then held under power.Safe Operating Stop: standstill is monitored while the drive remains energised.Safely Limited Speed: speed is monitored against a limit.Safely Limited Position: position is monitored against a limit.Safe Direction: motion is permitted in one direction only.Safe Brake Control: a brake is commanded safely.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3013
+
+ NoneStoSs1Ss2SosSlsSlpSdiSbc
+
+
+ RealTimeTransportEnum
+ The transport of a brokered real-time channel. This specification defines none of these: it describes them so a client can find and open one, and the samples never traverse this interface.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3914
+
+ Universal Robots Real-Time Data Exchange.ABB Externally Guided Motion.KUKA Fast Research Interface.KUKA Robot Sensor Interface.Yaskawa MotoROS2.OPC UA FX (OPC 10000-80 to -84). The open path, and the only one in this list that is an OPC Foundation specification.A transport identified by the channel's own descriptor.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3014
+
+ RtdeEgmFriRsiMotoRos2OpcUaFxOther
+
+
+ ChannelInitiatorEnum
+ Which end opens the transport connection of a brokered channel. Getting this wrong is the usual reason a first connection attempt fails, so it is stated rather than left to the reader.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3915
+
+ The Server connects to an endpoint the client is listening on.The client connects to the endpoint the Server publishes.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3015
+
+ ServerClient
+
+
+ ErrorPolicyEnum
+ What a mission does when one of its steps does not succeed. Without this a mission can only abort, which forces every recovery out into the client.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3916
+
+ End the mission. This is the default and the behaviour of a mission that declares no policy.Re-attempt the step. A Server bounds the attempts and reports Failed when they are exhausted.Record the failure and continue with the next step.Continue at FallbackStepId instead.Run the fallback step to undo the work already done, then end the mission.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3016
+
+ AbortRetrySkipFallbackCompensate
+
+
+ DivergenceKindEnum
+ How the transitions leaving one step relate to each other, following the divergence of an IEC 61131-3 sequential function chart.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3917
+
+ Exactly one transition is taken - the first whose condition holds. An OR divergence.Every transition is taken and the branches run concurrently. An AND divergence.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3017
+
+ AlternativeParallel
+
+
+ WeaveShapeEnum
+ The oscillation applied across an arc weld seam.
+ RobotIntent DataTypes
+
+ i=29
+ ns=1;i=3918
+
+ No weave.Sinusoidal oscillation.Triangular oscillation.Trapezoidal oscillation, with a dwell at each edge.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3018
+
+ NoneSineZigzagTrapezoid
+
+
+ TrajectoryPointDataType
+ One point of a time-parameterised path. Positions are per axis in the order the axes are declared, in radians or metres by AxisKind. TimeFromStart is measured from the start of the trajectory, which is what makes the path a trajectory rather than a list of waypoints. Velocities and Accelerations are optional and may be empty.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5019
+
+ Milliseconds from the start of the trajectory.One value per axis.One value per axis, or empty.One value per axis, or empty.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3070
+
+
+
+ MotionToleranceDataType
+ How far execution may deviate before it is a failure. A tolerance of zero or less means the Server applies its own.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5020
+
+ Positional tolerance in metres.Orientation tolerance in radians.Timing tolerance in milliseconds.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3071
+
+
+
+ TrajectoryIntentDataType
+ Execute a time-parameterised path. The Server's own motion kernel runs it; this interface hands the whole trajectory over in one submission and does not stream it. That is what makes trajectory execution expressible here when real-time control is not - see clause 4.3.
+ RobotIntent DataTypes
+
+ ns=1;i=3054
+ ns=1;i=5021
+
+ The trajectory, in ascending TimeFromStart order.Permitted deviation while following the path.Permitted deviation at the final point.How much later than the final point's TimeFromStart completion may be, in milliseconds.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3072
+
+
+
+ PathWaypointDataType
+ One waypoint of a Cartesian path, with the blend that applies at it. Per-waypoint blending is what distinguishes a path from a sequence of separate linear moves: the robot need not stop between them.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5022
+
+ Where the tool centre point passes.How this waypoint is left.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3073
+
+
+
+ CartesianPathIntentDataType
+ Follow a list of Cartesian waypoints. This is the portable form of a taught path, and unlike a trajectory it carries no timing: the Server paces it from the motion constraints.
+ RobotIntent DataTypes
+
+ ns=1;i=3054
+ ns=1;i=5023
+
+ The path, in order.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3074
+
+
+
+ ForceIntentDataType
+ Move until contact. The portable subset of the force-controlled moves every vendor offers: travel along a direction until a contact force is reached or a distance is exhausted, whichever comes first. Reaching the distance without contact is a failure, because the intent was to touch something.
+ RobotIntent DataTypes
+
+ ns=1;i=3054
+ ns=1;i=5024
+
+ Unit direction of travel (x, y, z) in the frame of the target.The CoordinateFrame Direction is expressed in; empty for the default work frame.The force in newtons at which contact is declared.How far to travel before giving up, in metres.True to keep pressing at ContactForce after contact rather than stopping.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3075
+
+
+
+ ProcessIntentDataType
+ Abstract base of the intents that run an application process along a path. Every process needs the same two things beyond its own parameters: a reference to the process program or procedure the equipment holds, and room for the parameters this specification has not standardised.
+ RobotIntent DataTypes
+
+ ns=1;i=3054
+
+ The Program or equipment-side procedure to run, or null when the parameters here are sufficient.Further named parameters the Server declares in its capabilities.
+
+
+ ArcWeldIntentDataType
+ Lay an arc weld along the path. The parameters are the subset ABB seamdata/welddata/weavedata, FANUC weld schedules and KUKA ArcTech all carry. WeldProcedureRef points at a welding procedure specification (ISO 15609) where the installation works to one; this specification does not restate its content.
+ RobotIntent DataTypes
+
+ ns=1;i=3076
+ ns=1;i=5025
+
+ Arc voltage in volts.Wire feed speed in metres per minute.Tool centre point speed in metres per second.Shielding gas pre-flow, in milliseconds.Shielding gas post-flow, in milliseconds.Delay before travel begins, in milliseconds.Crater fill at the end of the seam, in milliseconds.Oscillation across the seam.Peak-to-peak weave width in metres.Weave frequency in hertz.True to enable seam tracking where the equipment provides it.Identifier of the welding procedure specification this weld works to.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3077
+
+
+
+ SpotWeldIntentDataType
+ Make a resistance spot weld at the target. WeldSchedule selects the weld controller's own program: current and time are the weld controller's business, and are carried here only where the installation drives them from the robot.
+ RobotIntent DataTypes
+
+ ns=1;i=3076
+ ns=1;i=5026
+
+ The weld controller program to run.Electrode force in newtons.Gun open distance before the weld, in metres.Gun open distance after the weld, in metres.Total stack thickness in metres, where the schedule is chosen adaptively.True to dress the tips after this weld.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3078
+
+
+
+ DispenseIntentDataType
+ Lay a bead of adhesive, sealant or paint along the path. The trigger distances exist because material does not start and stop instantly: a Server begins dispensing before the path and stops before its end.
+ RobotIntent DataTypes
+
+ ns=1;i=3076
+ ns=1;i=5027
+
+ Nominal flow in millilitres per minute.Distance before the path start at which dispensing begins, in metres.Distance before the path end at which dispensing stops, in metres.Target bead width in metres.Material temperature in degrees Celsius, for hot-melt work.Nozzle purge cycles before starting.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3079
+
+
+
+ FastenIntentDataType
+ Drive a fastener at the target. This intent is deliberately THIN: OPC 40450 and OPC 40451 already define joining and tightening in full, so Joint references the joint in that model and the result belongs there. Restating those parameters here would create a second definition of the same fact.
+ RobotIntent DataTypes
+
+ ns=1;i=3076
+ ns=1;i=5028
+
+ The joint being fastened, in an OPC UA joining or tightening model where one is implemented.The tightening program the tool is to run.Target torque in newton metres, where the robot supplies it rather than the tool.Target angle in radians, for angle-controlled strategies.Torque at which angle counting begins, in newton metres.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3080
+
+
+
+ PalletiseIntentDataType
+ Place an item into a pattern. The pattern itself is a Location, so its geometry has one definition that a client can read rather than being recomputed from indices on both sides.
+ RobotIntent DataTypes
+
+ ns=1;i=3076
+ ns=1;i=5029
+
+ The Location describing the pallet or pattern.Layer index, counting from zero.Row index within the layer, counting from zero.Column index within the row, counting from zero.Rotation of the item about the pattern normal, in radians.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3081
+
+
+
+ SurfaceFinishIntentDataType
+ Follow the path pressing into the surface - grinding, polishing, deburring or sanding. ContactForce is what distinguishes it from a plain path: the robot yields normal to the surface to hold that force.
+ RobotIntent DataTypes
+
+ ns=1;i=3076
+ ns=1;i=5030
+
+ Force normal to the surface, in newtons.Travel speed along the surface, in metres per second.Rotational speed of the tool, in revolutions per minute.Lateral offset between adjacent passes, in metres.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3082
+
+
+
+ MissionTransitionDataType
+ One edge of a mission's step graph, following the step-and-transition form of an IEC 61131-3 sequential function chart. Condition is an OPC UA ContentFilter - the base specification's own filter grammar, reused so that this specification does not invent an expression language for implementers to write a parser for.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5031
+
+ The step this transition leaves.The step this transition enters.The condition under which it is taken. An empty filter is always true.How this transition relates to the others leaving the same step.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3083
+
+
+
+ KinematicJointDataType
+ One joint of the kinematic chain: where it sits relative to its predecessor and which way it acts. OPC 40010-1 describes a robot's topology and its axes but defines no kinematic chain, so this is additive rather than a second account of the same thing.
+ RobotIntent DataTypes
+
+ i=22
+ ns=1;i=5032
+
+ The AxisType this joint corresponds to.Whether it rotates or translates.Pose of this joint's frame within its predecessor's, at zero position.Unit vector (x, y, z) the joint rotates about or translates along, in its own frame.
+
+
+ Default Binary
+ Default Binary encoding of the structure.
+
+ i=76
+ ns=1;i=3084
+
+
+
+ SafetyStateType
+ What the robot's safety system is doing, reported so a client can act sensibly around it. Every member is READ-ONLY and every one is a report: the safety system enforces these independently, remains effective when this interface is unreachable, and is not commanded from here. Clause 10 states the boundary and this type does not cross it.
+ RobotIntent
+
+ i=58
+ ns=1;i=6098
+ ns=1;i=6099
+ ns=1;i=6100
+ ns=1;i=6101
+ ns=1;i=6102
+ ns=1;i=6103
+ ns=1;i=6104
+
+
+
+ ActiveFunction
+ The safe motion function currently enforced, per IEC 61800-5-2.
+
+ i=78
+ i=68
+ ns=1;i=1012
+
+
+
+ EmergencyStopActive
+ True while an emergency stop is asserted.
+
+ i=78
+ i=68
+ ns=1;i=1012
+
+
+
+ ProtectiveStopActive
+ True while a protective stop is asserted.
+
+ i=78
+ i=68
+ ns=1;i=1012
+
+
+
+ SafeSpeedLimitActive
+ True while a safely limited speed is being enforced.
+
+ i=78
+ i=68
+ ns=1;i=1012
+
+
+
+ SafeSpeedLimit
+ The tool centre point speed limit being enforced, in metres per second. Meaningful only while SafeSpeedLimitActive. Clause 10.3 requires a Server to refuse an intent that asks to exceed it.
+
+ i=78
+ i=68
+ ns=1;i=1012
+
+
+
+ SafetyControllerOk
+ False when the safety system reports its own fault. A Server accepts no intent while it is false.
+
+ i=78
+ i=68
+ ns=1;i=1012
+
+
+
+ LastStopReason
+ Why the last stop occurred, for a human. Never parsed.
+
+ i=80
+ i=68
+ ns=1;i=1012
+
+
+
+ RealTimeChannelType
+ A high-rate channel this Server can offer, described so a client can open it. The samples never traverse OPC UA: this brokers the endpoint and nothing more, in the same way the Vision model brokers a media endpoint rather than carrying pixels. Clause 4.3 explains why the alternative is not available.
+ RobotIntent
+
+ i=58
+ ns=1;i=6105
+ ns=1;i=6106
+ ns=1;i=6107
+ ns=1;i=6108
+ ns=1;i=6109
+ ns=1;i=6110
+ ns=1;i=6111
+ ns=1;i=6112
+ ns=1;i=6113
+ ns=1;i=6114
+
+
+
+ ChannelId
+ Identifier unique within the controller.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ Transport
+ The transport this channel speaks.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ EndpointUrl
+ Where the channel is reached. Its scheme and form are the transport's, not this specification's.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ Initiator
+ Which end opens the connection.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ NominalRate
+ The rate the channel runs at, in hertz.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ PayloadDescriptor
+ The recipe, signal list or configuration the transport requires, in the transport's own form.
+
+ i=80
+ i=68
+ ns=1;i=1013
+
+
+
+ RequiredMode
+ The operational mode the robot must be in before the channel will carry motion.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ Available
+ True when the channel can be opened now.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ LeaseHolder
+ SessionId of the client currently holding the channel, or null.
+
+ i=78
+ i=68
+ ns=1;i=1013
+
+
+
+ LeaseExpiry
+ When the current lease lapses. A lease that is not renewed frees the channel, so a client that dies does not hold it for good.
+
+ i=80
+ i=68
+ ns=1;i=1013
+
+
+
+ RobotDescriptionType
+ Enough of the robot's construction for a client to plan against it without a second specification: the kinematic chain, the space it can reach, and what it can carry. OPC 40010-1 describes topology and axes and defines no kinematic chain and no tool centre point, so this adds rather than restates - Annex B says which side decides where both are present.
+ RobotIntent
+
+ i=58
+ ns=1;i=6115
+ ns=1;i=6116
+ ns=1;i=6117
+ ns=1;i=6118
+ ns=1;i=6119
+ ns=1;i=6120
+ ns=1;i=6121
+ ns=1;i=6122
+
+
+
+ Manufacturer
+ Who made the robot.
+
+ i=80
+ i=68
+ ns=1;i=1014
+
+
+
+ Model
+ The robot's model designation.
+
+ i=80
+ i=68
+ ns=1;i=1014
+
+
+
+ KinematicChain
+ The joints from the base outwards, in order.
+
+ i=78
+ i=63
+ ns=1;i=1014
+
+
+
+ MountingPose
+ Pose of the robot base within the world frame.
+
+ i=80
+ i=63
+ ns=1;i=1014
+
+
+
+ ReachRadius
+ Radius of the reachable workspace from the base, in metres.
+
+ i=78
+ i=68
+ ns=1;i=1014
+
+
+
+ PayloadLimit
+ Largest payload at the mechanical interface, in kilograms.
+
+ i=78
+ i=68
+ ns=1;i=1014
+
+
+
+ MaxCartesianSpeed
+ Largest tool centre point speed the robot will produce, in metres per second.
+
+ i=78
+ i=68
+ ns=1;i=1014
+
+
+
+ MaxCartesianAcceleration
+ Largest tool centre point acceleration, in metres per second squared.
+
+ i=80
+ i=68
+ ns=1;i=1014
+
+
+
+ SafetyState
+ What the safety system is doing. A client reads this before it plans, and subscribes to it so that it learns of a stop rather than inferring one from a refusal.
+
+ i=78
+ ns=1;i=1012
+ ns=1;i=1002
+
+
+
+ Description
+ The robot's kinematics and limits.
+
+ i=80
+ ns=1;i=1014
+ ns=1;i=1002
+
+
+
+ RealTimeChannels
+ The high-rate channels this Server can broker.
+
+ i=80
+ i=61
+ ns=1;i=1002
+
+
+
+ OpenRealTimeChannel
+ Take a lease on a brokered real-time channel. The Server prepares the transport and returns what the client needs in order to connect; it does not carry the samples. A lease that is not renewed lapses, which is what stops a dead client from holding the channel.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6127
+ ns=1;i=6128
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6126
+
+ i=297ChannelIdi=12-1The channel to open.i=297RequestedLeasei=290-1How long the lease is wanted for, in milliseconds.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6126
+
+ i=297Grantedi=1-1True when the lease was taken.i=297EndpointUrli=12-1Where to connect.i=297PayloadDescriptori=12-1The transport's own configuration.i=297LeaseExpiryi=294-1When the lease lapses.i=297Messagei=21-1Human-readable detail on a refusal. For a human; never parsed.
+
+
+ CloseRealTimeChannel
+ Give up a lease on a brokered channel.
+
+ i=80
+ ns=1;i=1002
+ ns=1;i=6130
+ ns=1;i=6131
+
+
+
+ InputArguments
+
+ i=78
+ i=68
+ ns=1;i=6129
+
+ i=297ChannelIdi=12-1The channel to release.
+
+
+ OutputArguments
+
+ i=78
+ i=68
+ ns=1;i=6129
+
+ i=297Releasedi=1-1True when the lease was held and is now released.
+
+
+ TrajectorySupported
+ True when trajectory and Cartesian path intents are accepted.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ ForceControlSupported
+ True when force intents are accepted and the robot can actually regulate force. A Server that would ignore the force reports false rather than accepting an intent it cannot honour.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ RealTimeChannelsSupported
+ True when the Server brokers real-time channels.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ MissionBranchingSupported
+ True when mission transitions are evaluated. A Server that reports false executes the steps in order and ignores any transitions supplied.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ MaxTrajectoryPoints
+ Largest number of points accepted in one trajectory. Zero means the Server states no limit.
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ FinalResultData
+ Part 10 result container. Carries the same IntentResultDataType value as Result, so a Part 10 client finds the outcome where Part 10 says it will be. Mandatory here because clause 6.7 requires it and an Optional member cannot carry a SHALL.
+
+ i=78
+ i=58
+ ns=1;i=1003
+
+
+
+ ProgramDiagnostic
+ Part 10 invocation diagnostics: which Session invoked the program, when, with what arguments and to what outcome. Mandatory here because the auditable-commanding property of clause 1.2 is exactly this member, and a capability the specification advertises cannot rest on one a Server may omit. Declared exactly as OPC 10000-10 declares it - a Variable of ProgramDiagnostic2Type reached by HasComponent, with the inherited namespace-0 BrowseName - because a promotion that altered the inherited member's TypeDefinition, reference type or BrowseName namespace would declare a second member rather than promote the inherited one.
+
+ i=78
+ i=15383
+ ns=1;i=1003
+
+
+
+ SupportedFacets
+ The facets of Table 12.2 this controller claims, by the names given there. A client reads this instead of re-deriving conformance from the address space. Clause 12.2 binds the claim: a facet whose structural requirements are unmet shall not appear, and the behavioural requirements are subject to the honesty rules of clause 9.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
diff --git a/metaverse-specs/robot-intent/README.md b/metaverse-specs/robot-intent/README.md
new file mode 100644
index 0000000..e22ad93
--- /dev/null
+++ b/metaverse-specs/robot-intent/README.md
@@ -0,0 +1,50 @@
+# OPC UA — Robot Intent
+
+**Release 0.1.0 — Draft.** An OPC UA information model for commanding a robot at the level of task intent, with a lifecycle that survives the minutes such work actually takes.
+
+Nothing here is normative, official, or endorsed by the OPC Foundation, VDMA or any robot manufacturer. Namespace URIs and NodeIds are **provisional** and for prototyping only.
+
+## What it is for
+
+OPC 40010-1 *Robotics* describes robot **topology** in detail and defines **no motion verbs at all** — its whole actuation surface is `Start`, `Stop` and loading a named program. A conformant client can discover everything about a robot's construction and cannot ask it to move anywhere.
+
+This model supplies the verbs, and nothing else, so the two compose rather than compete. It sits **above** the vendor motion language and **beside and beneath nothing** in the safety system:
+
+- task-level intents — joint, linear and circular moves, trajectories, Cartesian paths, force-controlled moves, grasp, release, pick, place, tool change, output, program call, wait, and six application processes;
+- a lifecycle built on the Part 10 program model, because an OPC UA `Call` cannot stay open for the length of a real motion;
+- PLCopen buffer modes for queueing and blending, and VDA 5050 blocking modes for concurrency;
+- missions with an immutable committed base, a revisable horizon, and an IEC 61131-3 step graph with per-step error policies;
+- **safety awareness** — what the safety system is enforcing, and a duty to refuse work that would exceed it;
+- **real-time channel brokerage** — describe and lease RTDE, EGM, FRI, RSI, MotoROS2 or OPC UA FX, without carrying a single sample;
+- the robot's kinematic chain, reach and payload, which OPC 40010-1 does not define;
+- a machine-readable capability declaration, so a client reads what a robot accepts instead of probing for it.
+
+## Layout
+
+| File | What it is |
+|---|---|
+| [`OPC-UA-Robot-Intent.md`](OPC-UA-Robot-Intent.md) | The specification |
+| [`OPC-UA-Robot-Intent-Research.md`](OPC-UA-Robot-Intent-Research.md) | The prior art, the gaps, and the decisions those gaps forced |
+| `Opc.Ua.RobotIntent.NodeSet2.xml` | The information model — **generated** |
+| `Opc.Ua.RobotIntent.NodeIds.csv` | The NodeId assignments — **generated** |
+
+Tooling lives in [`../extras/robot-intent/tools/`](../extras/robot-intent/tools/): `build_model.py` is the single source of truth for all three generated artifacts, `validate_local.py` is the gate, and `model-reference.md` is the generated Annex A.
+
+## Build and validate
+
+```powershell
+python metaverse-specs\extras\robot-intent\tools\build_model.py
+python metaverse-specs\extras\robot-intent\tools\validate_local.py
+```
+
+The generator is **deterministic**: rebuilding reproduces the committed NodeSet byte for byte. Edit the generator, never the generated NodeSet, CSV or Annex A.
+
+NodeId assignment is **append-only** — new members take the next free id, because inserting one mid-file silently renumbers everything after it.
+
+The validator re-derives everything from the committed artifacts rather than asking the generator what it emitted, and cross-checks the specification against the model **in both directions**: every type and every enumeration literal the model declares must be named in the prose, and every `ns=1;i=` the prose cites must exist in the model.
+
+## Namespace
+
+`http://opcfoundation.org/UA/RobotIntent/`
+
+The NodeSet declares exactly one `RequiredModel` — the base OPC UA namespace. Binding to an OPC 40010-1 `MotionDeviceSystem` is an optional profile carried by a ReferenceType (Annex B), not a NodeSet dependency, so a Server can adopt this model without pulling in any companion specification.
diff --git a/metaverse-specs/validate_all.py b/metaverse-specs/validate_all.py
index d599c8f..96971fe 100644
--- a/metaverse-specs/validate_all.py
+++ b/metaverse-specs/validate_all.py
@@ -22,11 +22,20 @@
# release-spec-validator:ICAgICJleHRyYXMvb3BlbnVzZC1hcnRpZmFjdHMvdG9vbHMvdmFsaWRhdGVfbG9jYWwucHkiLA==
# release-spec-validator:ICAgICJleHRyYXMvb3BlbnVzZC1hcnRpZmFjdHMvdG9vbHMvdmFsaWRhdGVfc3BlYy5weSIs
"extras/vision/tools/validate_local.py",
+ "extras/robot-intent/tools/validate_local.py",
+ "extras/ai-model-management/tools/validate_local.py",
+ "extras/ai-model-management/examples/tools/validate_examples.py",
# openusd-scene added in the Scene Materialization (Part 2) work.
# openusd-artifacts added with the xRegistry artifact registry (Bindings 0.4.0).
# validate_spec added with the submittable xRegistry domain spec; it keeps
# xRegistry-OpenUsd.md and xRegistry-OpenUsd.model.json from drifting apart.
# vision added in the OPC UA - Vision work.
+ # robot-intent added in the OPC UA - Robot Intent work; it also cross-checks the
+ # specification against the model in both directions.
+ # ai-model-management is a separate specification, so vision's validator also reads
+ # that NodeSet to resolve the overlay references that cross between them.
+ # validate_examples keeps the vendor implementation guides beside that specification
+ # from citing members the model no longer declares.
]
# Validators that additionally need untracked base data (none yet).
diff --git a/metaverse-specs/vision/OPC-UA-Vision.md b/metaverse-specs/vision/OPC-UA-Vision.md
index d0678b2..edd70ff 100644
--- a/metaverse-specs/vision/OPC-UA-Vision.md
+++ b/metaverse-specs/vision/OPC-UA-Vision.md
@@ -49,7 +49,7 @@ Neither list is a statement that the omitted capability is unimportant — only
### 1.4 Capabilities and versioning
-Release 0.1.0 covers sensors, media endpoints, frames and calibration, AI model/dataset/deployment/pipeline, results, feedback, and the learning loop. The NodeSet declares exactly one `RequiredModel` — the base OPC UA namespace — so a Server can adopt it without pulling in any companion model.
+Release 0.1.0 covers sensors, the media they emit, coordinate frames and calibration, inference pipelines, results, and the feedback path back in. The AI models those pipelines run are described by *OPC UA — AI Model Management and Inference* (§8.1). The NodeSet declares exactly one `RequiredModel` — the base OPC UA namespace — so a Server can adopt it without pulling in any companion model.
---
@@ -64,12 +64,18 @@ Release 0.1.0 covers sensors, media endpoints, frames and calibration, AI model/
Informative alignments — GenICam SFNC and PFNC, QIF (ISO 23952), ROS 2 `vision_msgs`, IDTA 02058/02059/02060 — are listed in Annex E. They are **not** normative references and impose no dependency.
-One further informative reference is called out here rather than in Annex E, because §6.7 defines an optional facet against it:
+- **OPC UA — AI Model Management and Inference** — [`../ai-model-management/OPC-UA-AI-Model-Management.md`](../ai-model-management/OPC-UA-AI-Model-Management.md). A **working draft in this repository**. It defines the model nameplate, the dataset, the deployment and the learning job that clauses 8 and 9 use. It is a **conditional** reference: this NodeSet declares no `RequiredModel` on it, `InferencePipelineType.Deployment` is a plain `NodeId`, and a Server claiming only **VIS-Base** need not implement it. The **VIS-Inference-\*** and **VIS-Learning** facets do require it (clause 11), because without it there is no auditable path from a verdict to the artefact that produced it.
+
+Two further informative references are called out here rather than in Annex E, because §6.7 defines an optional facet against the first:
- **OPC UA — Data Channels** — [`../../core-specs/data-channels/OPC-UA-Data-Channels.md`](../../core-specs/data-channels/OPC-UA-Data-Channels.md). A **working draft in this repository**, written as an errata package against OPC 10000-3, -4 and -6. It is **not a released OPC UA specification**: its NodeIds (`65000+`), its `STR` MessageType, its ALPN identifier and its StatusCodes are all provisional, and it may change or be abandoned. It is **informative and non-normative here, and imposes no dependency** — §6.7 defines an entirely optional facet against it, this model references none of its identifiers, and a Server that has never heard of it is fully conformant to this specification.
---
+### 2.1 Names taken from other specifications
+
+`ModelType`, `DatasetType`, `DeploymentType`, `LearningJobType`, `TensorSignatureDataType`, `InferenceLocationEnum`, `AcceleratorKindEnum`, `DatasetSourceEnum`, `LearningJobStateEnum` and the `UsesModel` and `TrainedOn` ReferenceTypes are defined by *OPC UA — AI Model Management and Inference* and are used here unqualified. They are **not** in this specification's namespace, and a Server conforming to **VIS-Base** need not implement any of them — see §8.1 and clause 11.
+
## 3 Terms, definitions and abbreviations
| Term | Definition |
@@ -83,7 +89,7 @@ One further informative reference is called out here rather than in Annex E, bec
| **Characteristic** | One measured property of an inspected part, with nominal, actual, tolerance and uncertainty. |
| **Detection** | One detected instance, with a class, a score, and geometry. |
| **Deployment** | A model made executable at a stated location. |
-| **On-server / off-server inference** | Whether the computation happens in the Server's process or elsewhere. Distinguished by `VisionInferenceLocationEnum`, and by nothing else. |
+| **On-server / off-server inference** | Whether the computation happens in the Server's process or elsewhere. Distinguished by `InferenceLocationEnum`, and by nothing else. |
| **Sim/real symmetry** | The property that a physical and a simulated sensor expose the same members with the same meaning, so a client works against either without change (§4.3). |
| **Point cloud** | An unordered set of 3-D points, each at least an (x, y, z) coordinate in a named frame and optionally carrying intensity, colour or a normal. It is the native output of a depth or laser-triangulation sensor. A single frame is routinely megabytes, so this specification obtains one through a media endpoint and never as an OPC UA array (§5.6). |
| **6-DoF pose** | A rigid-body placement with **six degrees of freedom** — three of position (x, y, z) and three of orientation — expressed relative to a named frame. It is what a robot controller needs in order to move to an object: position alone cannot say how to approach or grip it. Carried by `VisionPose3DDataType`, whose orientation is a unit quaternion ordered (x, y, z, w) per §5.12. |
@@ -123,7 +129,9 @@ A Server implementing this model almost always uses GenICam internally to talk t
A conforming Server **shall** expose exactly one well-known Object `Vision` of type `VisionRootType` as a component of the Server Object (`i=2253`), with BrowseName `Vision` qualified by the namespace `http://opcfoundation.org/UA/Vision/`. A client **shall** resolve that namespace's index from `Server.NamespaceArray` rather than assuming a fixed index. It contains:
- `Sensors` (Mandatory) — every `VisionSensorType` instance;
-- `Pipelines`, `Models`, `Frames`, `LearningJobs` (Optional).
+- `Pipelines` and `Frames` (Optional).
+
+Models, deployments and learning jobs are **not** here. They are reached through `AiRootType` in *OPC UA — AI Model Management and Inference*, whose own well-known object sits beside this one under the Server object. A client looking for what AI a Server runs browses there, not here.
A client therefore starts at `Server/Vision/Sensors` and follows references outward. This mirrors the discovery pattern of *OPC UA — OpenUSD Bindings*.
@@ -153,12 +161,12 @@ graph TD
CAM --> CAL["Calibrations"]
PIPES --> PIPE["InferencePipelineType"]
- PIPE --> DEPLOY["AiDeploymentType"]
- DEPLOY --> MODEL["AiModelType"]
+ PIPE -.NodeId.-> DEPLOY["DeploymentType
AI Model Management spec"]
+ DEPLOY --> MODEL["ModelType
AI Model Management spec"]
PIPE --> RESULT["InspectionResultType / DetectionResultType"]
PIPE --> FB["Feedback : VisionFeedbackType"]
- FB --> LEARN["LearningJobType"]
- LEARN --> DATASET["AiDatasetType"]
+ FB --> LEARN["LearningJobType
AI Model Management spec"]
+ LEARN --> DATASET["DatasetType
AI Model Management spec"]
CAM -.HasScenePrim.-> PRIM["UsdGeomCameraType (Part 2)"]
```
@@ -167,14 +175,14 @@ graph TD
## 5 Information model
-The model has **25 ObjectTypes**, and they exist in five groups, each answering one question a vision integration has to answer:
+The model has **21 ObjectTypes**, and they exist in five groups, each answering one question a vision integration has to answer:
| Group | Question it answers | Types | Clause |
|---|---|---|---|
| **Sensing** | What is the device, and what did it see it with? | `VisionSensorType`, `ImageSensorType`, `Depth3DSensorType`, `OpticsType`, `IlluminationType` | §5.4–5.5 |
| **Media** | How do I get the imagery, without putting it in OPC UA? | `VisionMediaManagementType`, `MediaEndpointType`, `StreamEndpointType`, `ClipEndpointType` | §6 |
| **Spatial** | Where is the sensor, and what is a pose *relative to*? | `CoordinateFrameType`, `VisionCalibrationType`, `IntrinsicCalibrationType`, `ExtrinsicCalibrationType` | §5.8 |
-| **AI** | What computed the answer, and can I audit it? | `AiModelType`, `AiDatasetType`, `AiDeploymentType`, `InferencePipelineType`, `LearningJobType` | §8, §9 |
+| **AI** | What computed the answer, and can I audit it? | `InferencePipelineType` here; `ModelType`, `DatasetType`, `DeploymentType` and `LearningJobType` in *OPC UA — AI Model Management and Inference* | §8, §9 |
| **Outcome** | What is the answer, and how do I correct it? | `VisionResultType`, `InspectionResultType`, `DetectionResultType`, `SegmentationResultType`, `VisionFeedbackType` | §7, §9 |
Plus two structural types: `VisionRootType`, the entry point (§4.2), and `IVisionSimulatedType`, the interface that makes a synthetic sensor addressable (§5.9).
@@ -214,11 +222,12 @@ This is the shape of a populated address space. Solid arrows are hierarchical (`
```mermaid
graph TD
ROOT["Vision : VisionRootType"]
+ AIROOT["AiModelManagement : AiRootType
AI Model Management spec, beside this one
under the Server object"]
+ AIROOT --> FM["Models"]
+ AIROOT --> FJ["LearningJobs"]
ROOT --> FS["Sensors"]
ROOT --> FP["Pipelines"]
- ROOT --> FM["Models"]
ROOT --> FF["Frames"]
- ROOT --> FJ["LearningJobs"]
FS --> SENSOR["ImageSensorType
SensorId, RealityKind, Modality
Width, Height, PixelFormat"]
SENSOR --> MEDIA["Media : VisionMediaManagementType
GetStreamEndpoint, GetClip"]
@@ -232,15 +241,15 @@ graph TD
FF --> FRAME["CoordinateFrameType
FrameId, Role"]
- FM --> MODEL["AiModelType
ModelId, Version, Digest"]
- FM --> DSET["AiDatasetType
SourceKind"]
- FM --> DEPL["AiDeploymentType
InferenceLocation, EndpointUri"]
+ FM --> MODEL["ModelType
ModelId, Version, Digest
AI Model Management spec"]
+ FM --> DSET["DatasetType
SourceKind
AI Model Management spec"]
+ FM --> DEPL["DeploymentType
InferenceLocation, EndpointUri
AI Model Management spec"]
FP --> PIPE["InferencePipelineType
State, Continuous"]
PIPE --> RES["Results/
InspectionResultType | DetectionResultType"]
PIPE --> FB["Feedback : VisionFeedbackType
SubmitCorrection"]
- FJ --> JOB["LearningJobType
State, SamplesCollected"]
+ FJ --> JOB["LearningJobType
State, SamplesCollected
AI Model Management spec"]
SENSOR -.HasCalibration.-> ICAL
SENSOR -.HasCalibration.-> ECAL
@@ -267,11 +276,11 @@ The three chains worth tracing are:
### 5.3 `VisionRootType : BaseObjectType`
-The single entry point (§4.2). Holds the five folders and nothing else.
+The single entry point (§4.2). Holds the three folders and nothing else.
This type exists because discovery has to be deterministic. Without a well-known root a client would have to search the address space for anything that looks like a camera, and two Servers would place them differently. A Server instantiates exactly one, as a component of the Server Object.
-`Sensors` is Mandatory; `Pipelines`, `Models`, `Frames` and `LearningJobs` are Optional, and their absence is meaningful — a Server with no `Models` folder is not doing AI, and a client can determine that in one Browse rather than by inference.
+`Sensors` is Mandatory; `Pipelines` and `Frames` are Optional, and their absence is meaningful — a Server with no `Models` folder is not doing AI, and a client can determine that in one Browse rather than by inference.
### 5.4 `VisionSensorType : BaseObjectType` (abstract)
@@ -364,42 +373,37 @@ The enumerations are closed: each is contiguous from 0, and the repository valid
| Enumeration | What it states |
|---|---|
| `VisionRealityKindEnum` | Whether a sensor is `Physical`, `Simulated` or `Hybrid` (§4.3). |
-| `VisionSensorModalityEnum` | What the sensor senses — `Area2D`, `Line2D`, `Depth3D`, `Thermal` and so on. |
+| `VisionSensorModalityEnum` | What the sensor senses — `Area2D`, `Line2D`, `Depth3D`, `Thermal`, `Multispectral`, `Event`, `Other`. |
| `VisionStreamProtocolEnum` | Wire protocol of a continuous stream. `Rtsp` is value 0 and the mandatory default (§6.2); `DataChannel` is the optional in-band path of §6.7. |
| `VisionClipFormatEnum` | Encoding of a single still. `Jpeg` is value 0 and the mandatory default. |
-| `VisionVideoCodecEnum` | Codec carried by a stream endpoint, for example H.264. |
+| `VisionVideoCodecEnum` | Codec carried by a stream endpoint — `H264`, `H265`, `Mjpeg`, `Av1`, `Raw`, `Other`. |
| `VisionEndpointStateEnum` | Lifecycle state shared by media endpoints, deployments and pipelines (§6.6). |
-| `VisionEndpointAuthenticationEnum` | How the media plane authenticates, independently of the OPC UA session (§12.1). |
-| `VisionInferenceLocationEnum` | Where inference runs — `OnServer`, `EdgeOffServer`, `Cloud`, `InSimulator`. Changes the trust boundary and nothing else (§8.2). |
-| `VisionAcceleratorKindEnum` | Class of hardware executing the model, for example GPU or NPU. |
+| `VisionEndpointAuthenticationEnum` | How the media plane authenticates, independently of the OPC UA session — `None`, `Basic`, `Digest`, `Token`, `MutualTls` (§12.1). |
| `VisionResultEvaluationEnum` | Overall inspection verdict — `Undefined`, `Ok`, `NotOk`, `NotDecidable`. Value semantics reused from OPC 40001-101. |
-| `VisionToleranceStatusEnum` | Per-characteristic outcome, including `Indeterminate` when uncertainty crosses a tolerance limit (§7.2). |
-| `VisionFeedbackPurposeEnum` | Why a client is submitting feedback — to draw an overlay, to reconcile a record, or to supply a ground-truth label (§9). |
-| `VisionCalibrationMountEnum` | The camera-to-robot arrangement a hand-eye calibration applies to: `EyeInHand`, `EyeToHand` or `Fixed`. |
-| `VisionFrameRoleEnum` | The role a coordinate frame plays, from the ISO 9787 vocabulary — world, base, tool, camera. |
+| `VisionToleranceStatusEnum` | Per-characteristic outcome — `InTolerance`, `OutOfTolerance`, `Indeterminate`, the last when uncertainty crosses a tolerance limit (§7.2). |
+| `VisionFeedbackPurposeEnum` | Why a client is submitting feedback — `Overlay`, `Reconciliation`, `GroundTruthLabel` or `Trigger` (§9.2). |
+| `VisionCalibrationMountEnum` | The camera-to-robot arrangement a hand-eye calibration applies to — `EyeInHand`, `EyeToHand`, `Fixed`, or `Unknown` where the Server cannot tell. |
+| `VisionFrameRoleEnum` | The role a coordinate frame plays, from the ISO 9787 vocabulary — world, base, mechanical interface, tool, object — plus `Camera`, which ISO 9787 does not define. The mechanical interface and the tool are separate roles: an eye-in-hand calibration resolves to the flange, while a grasp is executed at the tool centre point. |
| `VisionDistortionModelEnum` | Which lens-distortion model the coefficients follow; §5.12 fixes their ordering per model. |
-| `VisionLearningJobStateEnum` | Where a learning job is in its lifecycle (§9.6). |
-| `VisionDatasetSourceEnum` | Whether a dataset is `Real`, `Synthetic` or `Mixed` — the provenance a reviewer needs when synthetic data is involved. |
The structures are structures, not folders of Variables, because each is read as a unit or not at all. Splitting `VisionPose3DDataType` into seven Variables would let a client read a position from one acquisition and an orientation from the next, and would multiply the MonitoredItem count on a busy line by an order of magnitude. It also makes the array cases — `Detections`, `Characteristics` — a single value change rather than a variable-length subtree that has to be re-browsed whenever the part changes.
| Structure | What it carries |
|---|---|
| `VisionPose3DDataType` | A 6-DoF pose (§3): position in metres, orientation as a unit quaternion, the `FrameId` it is relative to, and an optional covariance. |
-| `VisionBoundingBox2DDataType` | An axis-aligned or rotated box in pixel coordinates, for a detection in the image plane. |
-| `VisionBoundingBox3DDataType` | An oriented box in metres, for a detection localised in space. |
+| `VisionBoundingBox2DDataType` | An axis-aligned or rotated box in pixel coordinates, for a detection in the image plane: `CenterX`, `CenterY`, `Width`, `Height`, `Rotation`. |
+| `VisionBoundingBox3DDataType` | An oriented box in metres, for a detection localised in space: `Center` and `Size`. |
| `VisionImageReferenceDataType` | A descriptor for an image the client fetches elsewhere: `Uri`, `Timestamp`, `Digest` and format. The correlation key of §6.4 rule 4. |
| `VisionIntrinsicsDataType` | Camera intrinsics — focal lengths, principal point, skew, distortion model and coefficients, and the resolution they were computed at. |
-| `VisionDetectionDataType` | One detected instance: class label and id, confidence, optional 2-D and 3-D geometry, optional pose, optional track id. Shaped on ROS 2 `vision_msgs`. |
-| `VisionCharacteristicDataType` | One measured property of a part: nominal, actual, deviation, tolerances, unit, **uncertainty** and status. Shaped on QIF (ISO 23952) Results. |
+| `VisionDetectionDataType` | One detected instance: `DetectionId`, `ClassLabel`, `ClassId`, `Confidence`, optional 2-D and 3-D geometry, optional pose, optional `TrackId`. Shaped on ROS 2 `vision_msgs`. |
+| `VisionCharacteristicDataType` | One measured property of a part: `CharacteristicId`, `Name`, nominal, actual, deviation, tolerances, unit, **uncertainty** and status. Shaped on QIF (ISO 23952) Results. |
| `VisionStreamSessionDataType` | A granted media lease: the `Uri`, its expiry, and the protocol actually served. Returned by `GetStreamEndpoint`, never published as a Variable (§12.2). |
-| `VisionTensorSignatureDataType` | The shape, element type and layout of one model input or output, so a client can tell whether it can feed the model. |
Full field-level detail — DataType, ValueRank, ModellingRule, structure fields, enumeration values and Method signatures — is in the generated Annex A. Units and orderings for every quantity are fixed normatively in §5.12.
### 5.11 ReferenceTypes
-Each ReferenceType subtypes `NonHierarchicalReferences`. They exist alongside the hierarchy because the hierarchy answers *what is part of this sensor*, whereas these answer *what does this node depend on*, and the two are not the same shape. A calibration is listed under its sensor, but a frame is not part of any one sensor and a model is not part of any one pipeline — both are shared and live in their own folders. A NodeId Property could express such a link, but a reference is browsable in **both** directions, which is what lets a client ask the reverse question — *which deployments use this model?* — the question that is asked the moment a model is found to be defective.
+Each ReferenceType subtypes `NonHierarchicalReferences`. They exist alongside the hierarchy because the hierarchy answers *what is part of this sensor*, whereas these answer *what does this node depend on*, and the two are not the same shape. A calibration is listed under its sensor, but a frame is not part of any one sensor — it is shared and lives in its own folder. A NodeId Property could express such a link, but a reference is browsable in **both** directions, which is what lets a client ask the reverse question — *which sensors does this calibration affect?* — the question that is asked the moment a calibration is found to be wrong.
The following constraints are **normative**; a Server **shall not** use these ReferenceTypes with other SourceNode or TargetNode types.
@@ -408,18 +412,17 @@ The following constraints are **normative**; a Server **shall not** use these Re
| `HasCalibration` | `IsCalibrationOf` | `VisionSensorType` | `VisionCalibrationType` | 0..n, at most one *valid* per calibration kind |
| `MountedOn` | `HasMounted` | `VisionSensorType` | `CoordinateFrameType` | 0..1 |
| `HasScenePrim` | `IsScenePrimOf` | `VisionSensorType` | a materialized camera prim (Annex C) | 0..1 |
-| `UsesModel` | `IsUsedByDeployment` | `AiDeploymentType` | `AiModelType` | **exactly 1** |
| `ProducedBy` | `Produces` | `VisionResultType` | `InferencePipelineType` | 0..1 |
- **`HasCalibration`** links a sensor to a calibration that applies to it. Following it forward answers *how do I interpret this sensor's output*; following `IsCalibrationOf` back answers *which sensors does this calibration affect*, which is what a maintenance client asks after re-calibrating. The cardinality allows a history of superseded calibrations to remain browsable, so long as only one per kind is `Valid`.
-- **`MountedOn`** links a sensor to the coordinate frame it is physically attached to — a robot flange for an eye-in-hand camera, a station frame for a fixed one. It is the structural statement of what the extrinsic calibration measures numerically, and it lets a client find the mounting frame without parsing a calibration.
+- **`MountedOn`** links a sensor to the coordinate frame it is physically attached to — a frame of role `MechanicalInterface` for an eye-in-hand camera, a station frame for a fixed one. It is the structural statement of what the extrinsic calibration measures numerically, and it lets a client find the mounting frame without parsing a calibration.
- **`HasScenePrim`** links a sensor to the camera prim it corresponds to in a materialized OpenUSD stage. It exists so a client can navigate from sensor to scene without resolving `PrimPath` as a string. Required only where the Server claims *VIS-Interop-Scene* (Annex C).
-- **`UsesModel`** links a deployment to the single model artefact it executes. This is the only defined path from a published result to the artefact and its `Digest`, so the §12.6 provenance check depends on it entirely — which is why the cardinality is exactly one rather than 0..1. `IsUsedByDeployment` is how an operator finds every deployment affected by a recalled model.
- **`ProducedBy`** links a result to the pipeline that computed it. It duplicates the `Pipeline` Property deliberately: the Property is convenient to read with the result, the reference is browsable in reverse so a client can enumerate everything one pipeline produced.
+The deployment-to-model link is **not** here. `UsesModel` is defined by *OPC UA — AI Model Management and Inference*, which also states its exactly-one cardinality; §12.6's provenance check walks it, and the **VIS-Inference-\*** facets require that specification for exactly that reason (§11.2).
+
The following are **normative**:
-- An `AiDeploymentType` instance **shall** have exactly one `UsesModel` reference to an `AiModelType` instance. This is the only defined path from a result to the model artefact and its `Digest`, and §12.6 depends on it.
- A `VisionResultType` instance **shall** identify its producer either by the `Pipeline` Property or by a `ProducedBy` reference. Where both are present they **shall** designate the same `InferencePipelineType` instance; a client **shall** treat the `ProducedBy` reference as authoritative.
- Where a sensor is calibrated, it **shall** carry a `HasCalibration` reference to each applicable calibration in addition to listing it under `Calibrations`.
@@ -452,6 +455,14 @@ Every physical quantity in this model is fixed here. A Server **shall** use thes
**Measurement uncertainty.** `VisionCharacteristicDataType.Uncertainty` is the **expanded** uncertainty at **coverage factor k = 2** (approximately 95 %), per ISO 14253-1, expressed in the same unit as `Actual`. A value of `0` means uncertainty is not reported, and a Server that does not evaluate uncertainty **shall** report `0` rather than a guess. Without a fixed coverage factor the §7.2 `NotDecidable` rule would not be reproducible between Servers, so a Server **shall not** report uncertainty at another coverage factor.
+**Frame and pose conventions.** Three further rules make a pose unambiguous, and a Server **shall** satisfy all of them.
+
+1. Every frame in this model is **right-handed**. The table above fixes the units of a pose; handedness is what fixes its meaning, and neither the base OPC UA specification nor ISO 9787 states it for you.
+2. `VisionPose3DDataType.Orientation` **shall** be normalised. A Server publishing a quaternion whose norm differs from 1 by more than 1e-6 is not describing a rotation, and a client **shall** treat such a pose as invalid rather than renormalising it silently — the error is more likely to be a wrong field order than a rounding artefact.
+3. `FrameId` **shall** be non-empty wherever a pose is published (§7.3). This model defines **no** default frame: a pose whose frame is not named is not actionable, and §5.8 explains what happens when the two ends disagree about what the numbers were relative to.
+
+> Rule 3 differs deliberately from specifications that treat an empty `FrameId` as a default working frame. Where poses are exchanged with such a model, the boundary **shall** substitute the named frame explicitly rather than passing the empty value through, because the same empty field means opposite things on either side.
+
**Distortion coefficient ordering.** `VisionIntrinsicsDataType.DistortionCoefficients` **shall** be ordered per `DistortionModel`:
| `DistortionModel` | Ordering |
@@ -586,7 +597,7 @@ Where the selected endpoint is a data-channel endpoint (§6.7), `Session.Uri` **
### 6.6 Endpoint state model (normative)
-`VisionEndpointStateEnum` is used by `MediaEndpointType`, `AiDeploymentType` and `InferencePipelineType`. All transitions are **Server-driven**; no Method sets `State` directly.
+`VisionEndpointStateEnum` is used by `MediaEndpointType` and `InferencePipelineType`. *OPC UA — AI Model Management and Inference* defines its own `DeploymentStateEnum` with the same five literals and the same transitions, so a client can apply one rule to both without this specification imposing a dependency. All transitions are **Server-driven**; no Method sets `State` directly.
```mermaid
stateDiagram-v2
@@ -681,29 +692,28 @@ Mandatory `Mask`, a `VisionImageReferenceDataType`. Masks are images and follow
## 8 AI integration (normative)
-### 8.1 Model, dataset, deployment
+### 8.1 The model this pipeline runs
-`AiModelType` is a model nameplate: identity, version, framework, format, task kind, digest, provenance, label classes, and input/output tensor signatures. `AiDatasetType` describes training or validation data, including `SourceKind` — `Real`, `Synthetic` or `Mixed` — which is the provenance a reviewer needs when synthetic data is involved. `AiDeploymentType` makes a model executable somewhere.
+The model itself, the data it was trained on and the deployment that executes it are **not** defined here. They are defined by *OPC UA — AI Model Management and Inference*, which is domain-neutral: nothing about a model nameplate, a dataset's provenance or an inference endpoint is specific to a camera, and a specification that defined them here would oblige every other domain either to depend on a vision model or to define them again.
-All three align member-for-member with the IDTA submodel templates **02060** (AI Model Nameplate), **02058** (AI Dataset) and **02059** (AI Deployment), which are currently the only standardised description of an industrial AI model. An Asset Administration Shell can therefore be populated from these nodes without loss (Annex E).
+`InferencePipelineType.Deployment` is a **`NodeId` Property** naming that deployment. It is a NodeId and not a reference precisely so that this NodeSet takes no dependency: a Server implementing this specification alone is fully conformant, and a Server that describes its deployment some other way names that node instead.
-#### 8.1.1 A model is a business artefact, not device firmware
+Where the Server implements both, the deployment is a `DeploymentType` instance and the chain in §12.6 — result → deployment → model → digest — is available end to end. Where it does not, §12.6's provenance guarantee is unavailable, which is why the **VIS-Inference-\*** facets require it (§11.2).
-This is the assumption the whole clause rests on, and it is why the model is separated from the sensor that uses it.
+#### 8.1.1 A model is a business artefact, not device firmware
-An AI model is **supplied and governed by the end-user**, not baked into the device by its manufacturer. In practice it is trained, approved and released by the operator, a system integrator, or a model vendor, against that organisation's own quality, safety and regulatory obligations. The same physical camera runs different models over its life; the same model runs on many cameras and on off-server hardware the camera vendor never sees. A device-embedded model would be indistinguishable from firmware and would need none of `AiModelType`.
+This assumption is why the model is separated from the sensor that uses it, and it is what makes the separation of specifications the right shape rather than merely a tidy one.
-Three consequences are normative:
+An AI model is **supplied and governed by the end-user**, not baked into the device by its manufacturer. The same physical camera runs different models over its life; the same model runs on many cameras and on off-server hardware the camera vendor never sees.
-1. **Lifecycles are independent.** A Server **shall not** require a device firmware change to change the deployed model, and **shall not** tie `AiModelType.Version` to any device or firmware version. Replacing a model **shall** be observable as a change to the `UsesModel` reference of an `AiDeploymentType`, or to that model's `Version` and `Digest` — not as a change to the sensor.
-2. **The Server describes; it does not govern.** This model carries *what is deployed and where it came from* — `ModelId`, `Version`, `Digest`, `ProvenanceUri`, `TrainedOn` — so that a business process can audit it. Approval workflow, release criteria, sign-off and retention policy are **out of scope**: they belong to the organisation's governance system, and a Server **shall not** be assumed to enforce them. `ProvenanceUri` is the defined hand-off point to that system.
-3. **Authority to change a model is separate from authority to operate the device.** A client authorized to browse a sensor, view its stream or trigger inference is **not** thereby authorized to promote or replace a model. §12.5 requires that authorization to be distinct, because substituting a model silently changes every verdict the line produces.
+Two consequences are normative here, and the rest belong to the specification that owns the model:
-Where a vendor *does* ship a fixed model with a device, that is expressible — a single `AiModelType` whose `ProvenanceUri` names the vendor — but it is one case of the general arrangement, not the assumption behind it.
+1. **Lifecycles are independent.** A Server **shall not** require a device firmware change to change the deployed model, and **shall not** tie a model's version to any device or firmware version. Replacing a model **shall** be observable as a change to the deployment, not as a change to the sensor.
+2. **Authority to change a model is separate from authority to operate the device.** A client authorized to browse a sensor, view its stream or trigger inference is **not** thereby authorized to promote or replace a model — see §12.5.
### 8.2 On-server and off-server inference
-`AiDeploymentType.InferenceLocation` is mandatory and takes one of `OnServer`, `EdgeOffServer`, `Cloud`, `InSimulator`.
+The deployment's inference location is mandatory in the model that defines it and takes one of `OnServer`, `EdgeOffServer`, `Cloud`, `InSimulator`. It is restated here because it is what a vision client reasons about when it decides whether to trust a latency budget.
**This property changes where computation happens and therefore the trust boundary. It changes nothing else.** A Server **shall** publish results through the same types, with the same members and the same meaning, regardless of its value. When inference is off-server the Server publishes results it did not compute; a client that does not care where inference ran does not have to look.
@@ -731,7 +741,7 @@ sequenceDiagram
C->>S: Read Results/
S-->>C: InspectionResultType / DetectionResultType
C->>S: Browse result -> ProducedBy -> Deployment -> UsesModel
- S-->>C: AiModelType (Version, Digest)
+ S-->>C: ModelType (Version, Digest)
```
**`OnServer`** — the Server computes the result itself, so the only failure mode is its own:
@@ -852,15 +862,16 @@ sequenceDiagram
S-->>C: Good, or Bad_NotSupported if it will not retain it
```
-### 9.2 The three purposes
+### 9.2 The four purposes
-`VisionFeedbackType` serves three purposes with one surface:
+`VisionFeedbackType` serves four purposes with one surface. `VisionFeedbackPurposeEnum` states which applies:
-- **Overlay** — submitted geometry is drawn onto the outgoing stream, governed by `OverlayEnabled`, `OverlayStyle` and `OverlayTtl`. Used during commissioning and for operator confidence; it changes what a human sees and nothing else.
-- **Reconciliation** — a downstream verdict is recorded against a result, so what the line concluded can be compared with what the vision system reported. It changes the record, not the model.
-- **Ground-truth labelling** — a correction is retained as labelled training data. It is the only one of the three that can change what the system decides in future, which is why §12.7 gates it.
+- **`Overlay`** — submitted geometry is drawn onto the outgoing stream, governed by `OverlayEnabled`, `OverlayStyle` and `OverlayTtl`. Used during commissioning and for operator confidence; it changes what a human sees and nothing else.
+- **`Reconciliation`** — a downstream verdict is recorded against a result, so what the line concluded can be compared with what the vision system reported. It changes the record, not the model.
+- **`GroundTruthLabel`** — a correction is retained as labelled training data. It is the only one of the four that can change what the system decides in future, which is why §12.7 gates it.
+- **`Trigger`** — the submitted payload is an acquisition or processing request rather than a report: a client that already knows where to look tells the sensor to look there. It changes neither the record nor the model, and a Server that does not accept externally triggered acquisition returns `Bad_NotSupported`.
-`VisionFeedbackPurposeEnum` states which applies. The Methods are `SubmitDetections`, `SubmitInspectionResult`, `SubmitCorrection` and `SubmitImageReference`.
+The Methods are `SubmitDetections`, `SubmitInspectionResult`, `SubmitCorrection` and `SubmitImageReference`.
### 9.3 Feedback images
@@ -870,18 +881,18 @@ Any `Uri` in a submitted `VisionImageReferenceDataType` is a location the Server
### 9.4 Closing the loop
-`LearningJobType` is where corrections accumulate and become a new model version. Its `State` moves through `Idle`, `Collecting`, `Labelling`, `Training`, `Validating`, `Ready`, `Promoted` or `Failed`, and it links a `Dataset`, a `BaseModel` and a `CandidateModel`.
+`LearningJobType` is where corrections accumulate and become a new model version. Its state model, its Methods and its StatusCodes are defined by *OPC UA — AI Model Management and Inference* and are not restated here (§9.5.1).
```mermaid
graph LR
R["Result published"] --> OP["Operator or downstream
disagrees"]
OP --> C["SubmitCorrection
Purpose = GroundTruthLabel"]
- C --> D["AiDatasetType
SamplesCollected++"]
+ C --> D["DatasetType
SamplesCollected++"]
D --> T["TriggerTraining"]
T --> CM["CandidateModel"]
CM --> V["Validating"]
V --> P["PromoteModel"]
- P --> DEP["AiDeploymentType
now serving new version"]
+ P --> DEP["DeploymentType
now serving new version"]
DEP --> R
```
@@ -914,52 +925,15 @@ Any `Uri` inside a submitted `VisionImageReferenceDataType` is a client-supplied
A Server that accepts a correction with `Purpose = GroundTruthLabel` **shall** either retain it for the associated `LearningJobType` or return `Bad_NotSupported`; it **shall not** return `Good` and discard it, because a client has no other way to learn that its label was dropped. Retention is not acceptance as truth — §12.7 states what a Server **shall** record alongside the sample and what **shall** gate its admission to a training run.
-**`StartCollection()`**, **`StopCollection()`**, **`TriggerTraining() → (Accepted)`**, **`PromoteModel(Deployment) → (PromotedModel)`**
+#### 9.5.1 The learning Methods are not defined here
-| StatusCode | Condition |
-|---|---|
-| `Bad_InvalidState` | `StartCollection` when `State` is not `Idle` or `Collecting`; `TriggerTraining` when `State` is not `Collecting` or `Labelling`; `PromoteModel` when `State` is not `Ready` |
-| `Bad_NothingToDo` | `TriggerTraining` when `SamplesCollected` is 0 |
-| `Bad_NotFound` | `PromoteModel` when `Deployment` is non-null and does not resolve, or `CandidateModel` is null |
-| `Bad_UserAccessDenied` | the caller is not authorized; `PromoteModel` requires the distinct authorization of §12.5 |
+`StartCollection`, `StopCollection`, `TriggerTraining` and `PromoteModel` belong to `LearningJobType`, which *OPC UA — AI Model Management and Inference* defines together with its state model, its StatusCodes and the requirement that `PromoteModel` carry an authorization distinct from every other Method on the job. This specification does not restate them: two documents stating the same transition table is two places for it to be wrong, and the one that is wrong is discovered by an implementer, not by a validator.
-`StartCollection` and `StopCollection` are idempotent. `TriggerTraining` returns `Accepted = false`, with `Good`, when the Server queued nothing but the request was otherwise valid — for example because an external MLOps system declined it; `LastError` **shall** then carry the reason.
+What *is* stated here is the part that is specific to vision — the join between a correction submitted through `VisionFeedbackType` and the job that consumes it:
-`PromoteModel` moves `CandidateModel` into service. A null `Deployment` means *every* deployment fed by this job: the Server **shall** promote the candidate to all of them, or to none, and **shall not** promote a subset. `PromotedModel` returns the NodeId of the `AiModelType` instance that was promoted, which is the same node in either case — it identifies the model, not the deployment — so a caller that needs to know which deployments changed browses their `UsesModel` references afterwards.
-
-### 9.6 Learning job state model (normative)
-
-```mermaid
-stateDiagram-v2
- [*] --> Idle
- Idle --> Collecting: StartCollection
- Collecting --> Labelling: StopCollection
- Collecting --> Training: TriggerTraining (accepted)
- Labelling --> Training: TriggerTraining (accepted)
- Training --> Validating: Server, training finished
- Validating --> Ready: Server, candidate accepted
- Validating --> Failed: Server, candidate rejected
- Ready --> Promoted: PromoteModel
- Promoted --> Collecting: StartCollection
- Training --> Failed: Server, error
- Validating --> Failed: Server, error
- Failed --> Collecting: StartCollection
-```
-
-| From | Trigger | To |
-|---|---|---|
-| `Idle` | `StartCollection` | `Collecting` |
-| `Collecting` | `StopCollection` | `Labelling` |
-| `Collecting`, `Labelling` | `TriggerTraining` (accepted) | `Training` |
-| `Training` | Server: training finished | `Validating` |
-| `Validating` | Server: candidate met acceptance criteria | `Ready` |
-| `Validating` | Server: candidate rejected | `Failed` |
-| `Ready` | `PromoteModel` | `Promoted` |
-| `Promoted` | `StartCollection` | `Collecting` |
-| `Training`, `Validating` | Server: error | `Failed` |
-| `Failed` | `StartCollection` | `Collecting` |
-
-Transitions marked *Server* are driven by the Server or its MLOps backend; the rest are Method-driven. A Server **shall not** perform a transition not in this table, and **shall** populate `LastError` on entry to `Failed`. `CandidateModel` **shall** be non-null on entry to `Ready`.
+1. A Server that retains a `GroundTruthLabel` correction **shall** populate `InferencePipelineType.LearningJob` with the job that will consume it, so a client can determine whether its label reached a learning loop at all. A Server that retains nothing leaves it null, which is the honest answer and a different one from an unpopulated Optional member on a Server that does retain.
+2. A Server **shall not** report a job as `Collecting` on the strength of corrections it discarded. Where a correction was accepted with `Good` and retained, `SamplesCollected` **shall** account for it; the two statements are the same fact and a client that trusts one is entitled to the other.
+3. Promotion changes what every downstream verdict means. §12.5 requires its authorization to be distinct from the authorization for any `VisionFeedbackType` Method, and that requirement is stated in both documents deliberately — it is the one rule where a reader of either specification alone would otherwise reach the wrong conclusion.
---
@@ -1000,15 +974,18 @@ Where a facet's row names members, a Server claiming it **shall** instantiate ev
| **VIS-Result-Inspection** | `InspectionResultType` with `Evaluation` and `Characteristics`, and the §7.2 uncertainty rule including its uniform-reporting requirement |
| **VIS-Result-Detection** | `DetectionResultType` with `Detections`, the §5.12 pose conventions, and the §7.3 `FrameId` rule |
| **VIS-Feedback** | `VisionFeedbackType` with at least `SubmitImageReference`, the §9.3 and §9.5 rules, the §12.3 inbound-URI validation, and the §12.7 feedback-integrity rules |
-| **VIS-Inference-OnServer** | `InferencePipelineType` with a deployment whose `InferenceLocation` is `OnServer`, and the §5.11 `UsesModel` constraint. Where `RunInference` is implemented, `Results` (§8.4). `AiModelType.Digest` and `DigestAlgorithm` per §12.6 |
+| **VIS-Inference-OnServer** | `InferencePipelineType` with a deployment whose `InferenceLocation` is `OnServer`, and the `UsesModel` constraint. Where `RunInference` is implemented, `Results` (§8.4). `ModelType.Digest` and `DigestAlgorithm` per §12.6 |
| **VIS-Inference-OffServer** | As above with any other `InferenceLocation`, plus `EndpointUri` naming an authenticated, confidential scheme (§12.6) |
| **VIS-Simulation** | `IVisionSimulatedType` on every sensor whose `RealityKind` is `Simulated` or `Hybrid` (§4.3, §10). **Required** of any Server that reports either value. |
-| **VIS-Learning** | `LearningJobType`, `SubmitCorrection`, the §9.6 state model, every Method that drives a transition in it — `StartCollection`, `StopCollection`, `TriggerTraining`, `PromoteModel` — and the **distinct `PromoteModel` authorization** of §12.5 |
+| **VIS-Learning** | `VisionFeedbackType.SubmitCorrection` accepting `GroundTruthLabel`, the §9.5.1 join rules, and the **AI-Learning** facet of *OPC UA — AI Model Management and Inference*, which carries `LearningJobType`, its state model and the **distinct `PromoteModel` authorization** this specification also requires in §12.5 |
| **VIS-Interop-Scene** | The numbered requirements of Annex C, which are normative for a Server claiming this facet |
| **VIS-Interop-40100** | The numbered requirements of Annex D, which are normative for a Server claiming this facet |
+| **VIS-Interop-RobotIntent** | The numbered requirements of Annex I, which are normative for a Server claiming this facet |
Facets are independent and additive except where a row states a dependency. Two dependencies exist: *VIS-Base* requires *VIS-Media-Rtsp* and *VIS-Media-Jpeg*, and *VIS-Simulation* is required — not merely permitted — of any Server that reports `RealityKind` as `Simulated` or `Hybrid`. A facet is claimed only when every member and rule it lists is satisfied.
+**Three facets require a second specification.** *VIS-Inference-OnServer*, *VIS-Inference-OffServer* and *VIS-Learning* each name a type defined by *OPC UA — AI Model Management and Inference* — `DeploymentType`, `ModelType`, `LearningJobType` — so a Server claiming any of them **shall** also implement that specification's **AI-Base** facet, and **AI-Learning** for *VIS-Learning*. This is the only place either specification depends on the other, and it is stated as a facet precondition rather than a `RequiredModel` deliberately: a Server that publishes cameras, calibration and results and never mentions a model is fully conformant to **VIS-Base** with this NodeSet alone.
+
*VIS-Media-DataChannel* is the only facet defined against a document that is not a released specification. It is marked as such in its row and in §6.7, and it is deliberately structured so that its withdrawal would cost nothing: the two members it uses become permanently null, the enumeration literal goes unused, and every other facet is unaffected.
---
@@ -1067,26 +1044,26 @@ Inline delivery amplifies payload size by orders of magnitude relative to ordina
Every `VisionFeedbackType` Method mutates state: overlays change what operators see, reconciliation changes the record, and corrections change what the next model learns. A Server **shall** require explicit authorization for each.
-`LearningJobType.PromoteModel` changes what the system *decides*, on every deployment fed by the job (§9.5). A Server **shall** require an authorization for `PromoteModel` that is **distinct from, and not implied by**, the authorization required for any `VisionFeedbackType` Method or for `StartCollection`, `StopCollection` or `TriggerTraining`. A principal able to submit corrections **shall not** thereby be able to promote a model. This is the requirement §8.1.1 consequence 3 refers to, and clause 11 makes it a condition of *VIS-Learning* so that it is testable.
+`LearningJobType.PromoteModel` changes what the system *decides*, on every deployment fed by the job. A Server **shall** require an authorization for `PromoteModel` that is **distinct from, and not implied by**, the authorization required for any `VisionFeedbackType` Method or for `StartCollection`, `StopCollection` or `TriggerTraining`. A principal able to submit corrections **shall not** thereby be able to promote a model. This is the requirement §8.1.1 consequence 2 refers to, and clause 11 makes it a condition of *VIS-Learning* so that it is testable.
A Server **shall** retain an audit record of every correction and promotion, including the authenticated caller identity and the timestamp, and **shall not** include a credential-bearing URI in it (§12.2). Where the deployment falls under a high-risk regulatory regime, this record and the §7.1 trust members are what make the decision chain reconstructible.
### 12.6 Off-server inference crosses a trust boundary
-When `InferenceLocation` is not `OnServer`, results were computed by a system the OPC UA client cannot inspect. A Server **shall** establish an authenticated, integrity-protected channel to that service. `AiDeploymentType.EndpointUri` **shall** name a scheme that provides authentication and confidentiality — for example `https` or `grpcs`, not their plaintext counterparts — and a Server **shall not** publish a plaintext scheme for a deployment it claims conformance for.
+When `InferenceLocation` is not `OnServer`, results were computed by a system the OPC UA client cannot inspect. A Server **shall** establish an authenticated, integrity-protected channel to that service. `DeploymentType.EndpointUri` **shall** name a scheme that provides authentication and confidentiality — for example `https` or `grpcs`, not their plaintext counterparts — and a Server **shall not** publish a plaintext scheme for a deployment it claims conformance for.
-**Artefact integrity.** A Server **shall** publish `AiModelType.Digest` together with `AiModelType.DigestAlgorithm` for every model whose artefact is obtainable through `ArtifactUri`; both are Mandatory for this reason, and clause 11 makes the pair a condition of the inference facets. `DigestAlgorithm` **shall** name a hash function with at least 256-bit output and no known collision weakness — SHA-256 is the default and is always acceptable. A Server **shall not** publish `MD5`, `SHA-1`, or a truncated variant of any function; chosen-prefix collisions against those are practical, so a digest computed with them would let a substituted artefact pass verification. A client **shall** refuse a digest whose algorithm it does not recognise, or that does not meet this bar, rather than skipping verification and reporting success. Where `Digest` is non-empty, `DigestAlgorithm` **shall** be non-empty.
+**Artefact integrity.** A model artefact fetched out of band is bytes this Server did not serve, so the only thing that ties it to the answer is the digest. *OPC UA — AI Model Management and Inference* makes the model's `Digest` and `DigestAlgorithm` Mandatory, bars weak and truncated hash functions, and requires a client to refuse an algorithm it does not recognise rather than skip verification and report success. This specification does not restate those rules — it makes them a **condition of the inference facets** (clause 11): a Server claiming **VIS-Inference-OffServer** without a verifiable digest has published an unauditable verdict, which is the whole failure this clause exists to prevent.
-Digest verification is the only integrity control this specification defines for bytes fetched out of band, and it is the terminus of the provenance chain that §5.11 requires `UsesModel` to keep intact.
+Digest verification is the terminus of the provenance chain, and `UsesModel` is what keeps that chain intact.
### 12.7 Feedback is untrusted training data
-§12.5 governs *permission to call* a feedback Method. This clause governs what may then be *believed*, which is a separate question: §9.4 routes a submitted `GroundTruthLabel` into `AiDatasetType`, then into a training run, a `CandidateModel` and — after promotion — into every verdict the line produces. A single misused credential on the feedback surface is therefore a path to influencing safety-relevant decisions, and authorization alone does not bound it.
+§12.5 governs *permission to call* a feedback Method. This clause governs what may then be *believed*, which is a separate question: §9.4 routes a submitted `GroundTruthLabel` into `DatasetType`, then into a training run, a `CandidateModel` and — after promotion — into every verdict the line produces. A single misused credential on the feedback surface is therefore a path to influencing safety-relevant decisions, and authorization alone does not bound it.
A Server **shall**:
1. record the authenticated caller identity with every retained sample whose `Purpose` is `GroundTruthLabel`, and make it available to the `LearningJobType` that consumes the dataset — an unattributable label set cannot be reviewed or retracted;
-2. distinguish, within `AiDatasetType`, samples originating from client feedback from samples originating from capture, so that a reviewer can weigh them differently; and
+2. distinguish, within `DatasetType`, samples originating from client feedback from samples originating from capture, so that a reviewer can weigh them differently; and
3. require a distinct authorization or an explicit approval step before client-submitted labels are admitted to a training run, mirroring the `PromoteModel` gate of §12.5.
A Server **should** bound the proportion of any dataset contributed by a single principal, and **should** support retracting all samples attributed to one identity.
@@ -1180,7 +1157,7 @@ Replicator attaches annotators to a render product. These are simulation outputs
| `pointcloud` | `Depth3DSensorType` output, via a media endpoint |
| `normals`, `motion_vectors` | auxiliary channels, out of scope |
-Class labels come from the `Semantics` applied API schema on prims, which Part 2 materializes as a `UsdApiSchemaType` AddIn. A client can therefore read a stage's label set over OPC UA and know which classes a generated dataset will contain **before** running the simulation — and those labels are the same strings that appear in `AiModelType.LabelClasses` and `VisionDetectionDataType.ClassLabel`.
+Class labels come from the `Semantics` applied API schema on prims, which Part 2 materializes as a `UsdApiSchemaType` AddIn. A client can therefore read a stage's label set over OPC UA and know which classes a generated dataset will contain **before** running the simulation — and those labels are the same strings that appear in `ModelType.LabelClasses` and `VisionDetectionDataType.ClassLabel`.
Because these are ground truth rather than prediction, §10 requires a Server to make them distinguishable from inference output.
@@ -1188,11 +1165,11 @@ Because these are ground truth rather than prediction, §10 requires a Server to
| This specification | Isaac Sim |
|---|---|
-| `AiDatasetType` with `SourceKind = Synthetic` | Replicator writer output (BasicWriter, COCO, KITTI) |
-| `AiDatasetType.SampleCount` | frames written |
+| `DatasetType` with `SourceKind = Synthetic` | Replicator writer output (BasicWriter, COCO, KITTI) |
+| `DatasetType.SampleCount` | frames written |
| `IVisionSimulatedType.RandomizationSeed` | domain randomization seed |
| `LearningJobType` states `Collecting` → `Training` | a randomization run, then Isaac Lab or an external trainer |
-| `AiDeploymentType` with `InferenceLocation = InSimulator` | inference inside the simulator, for closed-loop evaluation |
+| `DeploymentType` with `InferenceLocation = InSimulator` | inference inside the simulator, for closed-loop evaluation |
### B.5 Streaming from a simulator
@@ -1203,7 +1180,7 @@ A simulated sensor still needs a `StreamEndpointType` with `StreamProtocol = Rts
1. Part 2 materializes the cell — geometry, semantic labels, and one or more `UsdGeomCameraType` prims.
2. Part 1 bindings drive live plant state into the stage, so the simulated cell tracks the real one.
3. A `VisionSensorType` with `RealityKind = Simulated` points at the camera prim through `PrimPath` and `HasScenePrim`.
-4. Replicator renders and emits annotators; the Server publishes them as results and accumulates an `AiDatasetType` with `SourceKind = Synthetic`.
+4. Replicator renders and emits annotators; the Server publishes them as results and accumulates a `DatasetType` with `SourceKind = Synthetic`.
5. `LearningJobType` trains a `CandidateModel` and promotes it.
6. The promoted model is deployed against the **physical** sensor — same types, same members, `RealityKind = Physical`.
7. Operator corrections from the line arrive through `SubmitCorrection` and seed the next dataset, now `Mixed`.
@@ -1257,7 +1234,7 @@ The alignment table below records the correspondence the requirements above rest
| OPC 40100-2 `ILensType` | `OpticsType`, member names already aligned |
| OPC 40100-2 `ILampType`, `ILightingControllerType` | `IlluminationType`, member names already aligned |
| OPC 40100-2 `VisionImageSensorType` (no members) | `ImageSensorType` supplies the imaging parameters it lacks |
-| OPC 40100-2 `SoftwareComponents` | `AiModelType` for the model specifically |
+| OPC 40100-2 `SoftwareComponents` | `ModelType` for the model specifically |
The intended division is that OPC 40100 answers *"what job is the system running"* and this specification answers *"what did it see, how, and with what model"*. Neither requires the other, and a Server is fully conformant to this specification without this facet.
@@ -1320,9 +1297,9 @@ The `CameraInfo` mapping is **not** a copy. Two adjustments are required, both f
| This specification | IDTA template |
|---|---|
-| `AiModelType` | **IDTA 02060** AI Model Nameplate |
-| `AiDatasetType` | **IDTA 02058** AI Dataset |
-| `AiDeploymentType` | **IDTA 02059** AI Deployment |
+| `ModelType` | **IDTA 02060** AI Model Nameplate |
+| `DatasetType` | **IDTA 02058** AI Dataset |
+| `DeploymentType` | **IDTA 02059** AI Model Management |
There is no IDTA submodel template for machine vision, so `VisionSensorType` and the result types have no counterpart. The OPC UA bridge to the AAS, OPC 30270, currently maps AAS V2.0.1 and is slated for replacement; this model therefore aligns by field name rather than depending on that bridge.
@@ -1330,7 +1307,7 @@ There is no IDTA submodel template for machine vision, so `VisionSensorType` and
| This specification | Standard |
|---|---|
-| `VisionFrameRoleEnum` | ISO 9787:2013 coordinate systems, including the tool centre point |
+| `VisionFrameRoleEnum` | ISO 9787:2013 coordinate systems, distinguishing the mechanical interface from the tool centre point |
| `VisionCharacteristicDataType.Uncertainty` | ISO 14253 |
| `ExtrinsicCalibrationType` | no standard defines the hand-eye procedure; only the result is portable |
| Terminology | ISO 8373:2021 robotics vocabulary |
@@ -1401,7 +1378,8 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
|---|---|---|---|
| `WorldFrame` | `world` | `World` | none (tree root) |
| `RobotBaseFrame` | `robot_base` | `Base` | `world` |
-| `FlangeFrame` | `flange` | `Tool` | `robot_base` |
+| `FlangeFrame` | `flange` | `MechanicalInterface` | `robot_base` |
+| `GripperTcpFrame` | `gripper_tcp` | `Tool` | `flange` |
| `CameraFrame` | `camera_eih` | `Camera` | `flange` |
**`Intrinsics2448x2048`** (`IntrinsicCalibrationType`) — Pinhole intrinsics with Brown-Conrady distortion at full resolution.
@@ -1420,15 +1398,15 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
|---|---|---|
| `Fx` | `2140.5` | px |
| `Fy` | `2139.8` | px |
-| `Cx` | `1223.1` | px, corner-datum per 5.10 |
-| `Cy` | `1021.7` | px, corner-datum per 5.10 |
+| `Cx` | `1223.1` | px, corner-datum per §5.12 |
+| `Cy` | `1021.7` | px, corner-datum per §5.12 |
| `Skew` | `0.0` | px |
-| `DistortionModel` | `BrownConrady` | 5.10 ordering: k1, k2, p1, p2, k3 |
+| `DistortionModel` | `BrownConrady` | §5.12 ordering: k1, k2, p1, p2, k3 |
| `DistortionCoefficients` | `[-0.1721, 0.0934, 0.0002, -0.0001, -0.0188]` | dimensionless |
| `Width` | `2448` | px |
| `Height` | `2048` | px |
-**`HandEye`** (`ExtrinsicCalibrationType`) — Transform from the camera frame to the robot flange frame. Eye-in-hand: the camera moves with the tool.
+**`HandEye`** (`ExtrinsicCalibrationType`) — Transform from the camera frame to the robot mechanical interface. Eye-in-hand: the camera moves with the flange, so a pick pose is obtained by composing camera → flange → tool centre point.
| Member | Value |
|---|---|
@@ -1445,10 +1423,10 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
| Field | Value | Unit / convention |
|---|---|---|
-| `FrameId` | `flange` | equals the TargetFrame's FrameId, per the 5.10 frame-precedence rule |
+| `FrameId` | `flange` | equals the TargetFrame's FrameId, per the §5.12 frame-precedence rule |
| `Position` | `(0.062, -0.031, 0.115)` | metres, ordered (x, y, z) |
| `Orientation` | `(0.0, 0.0, 0.7071, 0.7071)` | unit quaternion ordered (x, y, z, w) |
-| `Covariance` | `empty array` | not reported, per the 5.10 sentinel |
+| `Covariance` | `empty array` | not reported, per the §5.12 sentinel |
Each calibration is reachable from the sensor by a `HasCalibration` reference, as base specification §5.11 requires.
@@ -1486,13 +1464,13 @@ The twin additionally implements `IVisionSimulatedType`:
| `AcceleratorKind` | `Gpu` |
| `EndpointUri` | `grpcs://192.0.2.60:8001/graspposenet` |
-Inference runs **off-server** on a cell-side GPU appliance. The Server publishes results it did not compute. Nothing else in the model changes: a client reads `DetectionResultType` exactly as it would if `InferenceLocation` were `OnServer`, and consults that property only if it cares about the latency or trust boundary. Because the deployment is remote, base specification §12.6 applies: the channel to the inference service is authenticated and integrity-protected, and `AiModelType.Digest` lets a consumer confirm which artefact produced a result.
+Inference runs **off-server** on a cell-side GPU appliance. The Server publishes results it did not compute. Nothing else in the model changes: a client reads `DetectionResultType` exactly as it would if `InferenceLocation` were `OnServer`, and consults that property only if it cares about the latency or trust boundary. Because the deployment is remote, base specification §12.6 applies: the channel to the inference service is authenticated and integrity-protected, and `ModelType.Digest` lets a consumer confirm which artefact produced a result.
-The deployment carries exactly one `UsesModel` reference to the model above, as base specification §5.11 requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the §12.6 provenance check possible.
+The deployment carries exactly one `UsesModel` reference to the model above, as *OPC UA — AI Model Management and Inference* requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the base specification's §12.6 provenance check possible.
### F.8 Results
-Each cycle produces a `DetectionResultType` whose `Detections` carry `ClassLabel`, `Confidence`, a `BoundingBox2D`, a `BoundingBox3D` and — the member that makes the result actionable — a 6-DoF `Pose`. Every pose names its `FrameId` (`camera_eih`), which is only meaningful because the `HandEye` calibration above relates that frame to the flange. A consumer composes camera → flange → base through the `CoordinateFrameType` tree to obtain a pose the robot controller can execute. `ResidualError` on the calibration is what tells the consumer how much to trust it.
+Each cycle produces a `DetectionResultType` whose `Detections` carry `ClassLabel`, `Confidence`, a `BoundingBox2D`, a `BoundingBox3D` and — the member that makes the result actionable — a 6-DoF `Pose`. Every pose names its `FrameId` (`camera_eih`), which is only meaningful because the `HandEye` calibration above relates that frame to the flange. A consumer composes camera → flange → base through the `CoordinateFrameType` tree to obtain the pose in robot coordinates, and camera → flange → `gripper_tcp` to obtain what the gripper must actually reach. The two are distinct: the calibration resolves to the mechanical interface, while a grasp is executed at the tool centre point, and the frame tree carries the offset between them rather than leaving it to be assumed. `ResidualError` on the calibration is what tells the consumer how much to trust it.
### F.9 Feedback
@@ -1582,10 +1560,10 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
|---|---|---|
| `Fx` | `8310.2` | px |
| `Fy` | `8309.6` | px |
-| `Cx` | `1295.4` | px, corner-datum per 5.10 |
-| `Cy` | `971.2` | px, corner-datum per 5.10 |
+| `Cx` | `1295.4` | px, corner-datum per §5.12 |
+| `Cy` | `971.2` | px, corner-datum per §5.12 |
| `Skew` | `0.0` | px |
-| `DistortionModel` | `BrownConrady` | 5.10 ordering: k1, k2, p1, p2, k3 |
+| `DistortionModel` | `BrownConrady` | §5.12 ordering: k1, k2, p1, p2, k3 |
| `DistortionCoefficients` | `[-0.0021, 0.0004, 0.0, 0.0, 0.0]` | dimensionless; a telecentric lens is close to distortion-free |
| `Width` | `2592` | px |
| `Height` | `1944` | px |
@@ -1607,10 +1585,10 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
| Field | Value | Unit / convention |
|---|---|---|
-| `FrameId` | `station` | equals the TargetFrame's FrameId, per the 5.10 frame-precedence rule |
+| `FrameId` | `station` | equals the TargetFrame's FrameId, per the §5.12 frame-precedence rule |
| `Position` | `(0.0, 0.0, 0.320)` | metres, ordered (x, y, z) |
| `Orientation` | `(1.0, 0.0, 0.0, 0.0)` | unit quaternion ordered (x, y, z, w); a 180 degree rotation about x, so the camera looks down at the station |
-| `Covariance` | `empty array` | not reported, per the 5.10 sentinel |
+| `Covariance` | `empty array` | not reported, per the §5.12 sentinel |
Each calibration is reachable from the sensor by a `HasCalibration` reference, as base specification §5.11 requires.
@@ -1625,7 +1603,7 @@ Each calibration is reachable from the sensor by a `HasCalibration` reference, a
Inference runs **on-server**: `InferenceLocation = OnServer`, on an NPU in the station industrial PC. A client consuming the results cannot distinguish this from the off-server robotics example except by reading that one property — which is the intent of base specification §8.2. Because the pipeline is not continuous, `RunInference` is called per part by the station PLC and returns the `ResultId` it produced.
-The deployment carries exactly one `UsesModel` reference to the model above, as base specification §5.11 requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the §12.6 provenance check possible.
+The deployment carries exactly one `UsesModel` reference to the model above, as *OPC UA — AI Model Management and Inference* requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the base specification's §12.6 provenance check possible.
### G.7 Results
@@ -1715,3 +1693,32 @@ A client that does not recognise a `PixelFormat` **shall not** guess: it obtains
- **Writing features.** Nothing in this model configures a camera through GenICam. `ConfigureStreamEndpoint` (§6.5) configures the *encoder* of a media endpoint, not the sensor.
There is no published GenICam-to-OPC-UA mapping specification. This annex is a binding for this model only, and does not claim to be one.
+
+---
+
+## Annex I — Robot Intent interop profile (normative for *VIS-Interop-RobotIntent*)
+
+A camera that guides a robot and an interface that commands one are deployed on the same cell, and each defines its own `CoordinateFrameType`. Without a rule the flange is described twice, in two namespaces, with two `FrameId` strings and two `Transform` values that can disagree — the failure §5.8 warns about, arrived at by integration rather than by miscalibration.
+
+This annex fixes the correspondence. It imposes **no** NodeSet dependency in either direction: both models keep the base OPC UA namespace as their only `RequiredModel`, and a Server implementing one of them is unaffected by the other.
+
+**I.1 One frame tree is authoritative.** Where a Server implements both models for the same physical robot, the commanding model's frame tree **shall** decide. It owns the tool centre point, and a pose that disagrees with the frame the robot actually moves to is wrong however carefully it was measured. This model's frames **shall** then describe the same physical frames with the same transforms.
+
+**I.2 `FrameId` corresponds by value.** A frame present in both models **shall** carry the **same** `FrameId` string in each. That string, not the NodeId, is what a pose names, so it is the only correspondence a pose can carry.
+
+**I.3 Roles correspond by name.** The two role vocabularies agree on `World`, `Base`, `MechanicalInterface`, `Tool`, `Object` and `Other`; a frame present in both **shall** carry the same role. `Camera` exists only here, and a camera frame published to the commanding model **shall** be given the role `Other` there, because that model defines no camera role and misusing `Tool` would put a grasp at the lens.
+
+> The numeric values of the two enumerations are **not** interchangeable across models: each is decoded against the DataType of the Variable that carries it. A gateway **shall** map by literal name and **shall not** cast the integer.
+
+**I.4 Poses transcode explicitly.** The two pose structures are not wire-compatible — this model's carries a fourth field, `Covariance`. A boundary **shall** transcode rather than pass through:
+
+| From | To | Rule |
+|---|---|---|
+| `VisionPose3DDataType` | commanding pose | drop `Covariance`; `FrameId`, `Position` and `Orientation` transfer unchanged |
+| commanding pose | `VisionPose3DDataType` | set `Covariance` to an **empty array**, which §5.12 defines as *not reported* — a Server **shall not** fabricate one |
+
+Both use metres and a unit quaternion ordered (x, y, z, w) in a right-handed frame, so the numbers themselves need no conversion.
+
+**I.5 An empty `FrameId` is never passed through.** §5.12 rule 3 requires a named frame here, while a commanding model may read an empty `FrameId` as its default working frame. A boundary **shall** substitute the named frame explicitly in that direction, and **shall** reject a pose it cannot name rather than guessing.
+
+**I.6 A grasp pose reaches the tool centre point.** A pose published for a robot to act on **shall** be resolvable, through the frame tree, to a frame of role `Tool`. Resolving only to `MechanicalInterface` is not sufficient: the offset between the flange and the tool centre point is exactly what a hand-eye calibration does not measure, and Annex F carries it as a distinct frame for this reason.
diff --git a/metaverse-specs/vision/Opc.Ua.Vision.NodeIds.csv b/metaverse-specs/vision/Opc.Ua.Vision.NodeIds.csv
index baf2da1..5a4eae0 100644
--- a/metaverse-specs/vision/Opc.Ua.Vision.NodeIds.csv
+++ b/metaverse-specs/vision/Opc.Ua.Vision.NodeIds.csv
@@ -10,10 +10,6 @@ VisionEndpointStateEnum,3005,DataType
VisionEndpointStateEnum_EnumStrings,3905,Variable
VisionEndpointAuthenticationEnum,3006,DataType
VisionEndpointAuthenticationEnum_EnumStrings,3906,Variable
-VisionInferenceLocationEnum,3007,DataType
-VisionInferenceLocationEnum_EnumStrings,3907,Variable
-VisionAcceleratorKindEnum,3008,DataType
-VisionAcceleratorKindEnum_EnumStrings,3908,Variable
VisionResultEvaluationEnum,3009,DataType
VisionResultEvaluationEnum_EnumStrings,3909,Variable
VisionToleranceStatusEnum,3010,DataType
@@ -28,10 +24,6 @@ VisionDistortionModelEnum,3014,DataType
VisionDistortionModelEnum_EnumStrings,3914,Variable
VisionSensorModalityEnum,3015,DataType
VisionSensorModalityEnum_EnumStrings,3915,Variable
-VisionLearningJobStateEnum,3016,DataType
-VisionLearningJobStateEnum_EnumStrings,3916,Variable
-VisionDatasetSourceEnum,3017,DataType
-VisionDatasetSourceEnum_EnumStrings,3917,Variable
VisionPose3DDataType,3050,DataType
VisionPose3DDataType_Encoding_DefaultBinary,5001,Object
VisionBoundingBox2DDataType,3051,DataType
@@ -48,12 +40,9 @@ VisionCharacteristicDataType,3056,DataType
VisionCharacteristicDataType_Encoding_DefaultBinary,5007,Object
VisionStreamSessionDataType,3057,DataType
VisionStreamSessionDataType_Encoding_DefaultBinary,5008,Object
-VisionTensorSignatureDataType,3058,DataType
-VisionTensorSignatureDataType_Encoding_DefaultBinary,5009,Object
HasCalibration,4001,ReferenceType
MountedOn,4002,ReferenceType
HasScenePrim,4003,ReferenceType
-UsesModel,4004,ReferenceType
ProducedBy,4005,ReferenceType
OpticsType,1005,ObjectType
OpticsType_FocalLength,6001,Variable
@@ -175,39 +164,6 @@ IVisionSimulatedType_StageIdentifier,6103,Variable
IVisionSimulatedType_PrimPath,6104,Variable
IVisionSimulatedType_GroundTruthAvailable,6105,Variable
IVisionSimulatedType_RandomizationSeed,6106,Variable
-AiModelType,1015,ObjectType
-AiModelType_ModelId,6107,Variable
-AiModelType_Name,6108,Variable
-AiModelType_Version,6109,Variable
-AiModelType_Framework,6110,Variable
-AiModelType_Format,6111,Variable
-AiModelType_TaskKind,6112,Variable
-AiModelType_Digest,6113,Variable
-AiModelType_DigestAlgorithm,6114,Variable
-AiModelType_ArtifactUri,6115,Variable
-AiModelType_ProvenanceUri,6116,Variable
-AiModelType_LabelClasses,6117,Variable
-AiModelType_Inputs,6118,Variable
-AiModelType_Outputs,6119,Variable
-AiDatasetType,1016,ObjectType
-AiDatasetType_DatasetId,6120,Variable
-AiDatasetType_Name,6121,Variable
-AiDatasetType_Version,6122,Variable
-AiDatasetType_SourceKind,6123,Variable
-AiDatasetType_SampleCount,6124,Variable
-AiDatasetType_LabelClasses,6125,Variable
-AiDatasetType_CreatedAt,6126,Variable
-AiDatasetType_ArtifactUri,6127,Variable
-AiDatasetType_Digest,6128,Variable
-AiDeploymentType,1017,ObjectType
-AiDeploymentType_DeploymentId,6129,Variable
-AiDeploymentType_InferenceLocation,6130,Variable
-AiDeploymentType_AcceleratorKind,6131,Variable
-AiDeploymentType_AcceleratorName,6132,Variable
-AiDeploymentType_EndpointUri,6133,Variable
-AiDeploymentType_LatencyBudget,6134,Variable
-AiDeploymentType_BatchSize,6135,Variable
-AiDeploymentType_State,6136,Variable
VisionResultType,1020,ObjectType
VisionResultType_ResultId,6137,Variable
VisionResultType_CreationTime,6138,Variable
@@ -254,27 +210,11 @@ InferencePipelineType_RunInference_InputArguments,6173,Variable
InferencePipelineType_RunInference_OutputArguments,6174,Variable
InferencePipelineType_StartContinuous,6175,Method
InferencePipelineType_Stop,6176,Method
-LearningJobType,1019,ObjectType
-LearningJobType_JobId,6177,Variable
-LearningJobType_State,6178,Variable
-LearningJobType_Dataset,6179,Variable
-LearningJobType_BaseModel,6180,Variable
-LearningJobType_CandidateModel,6181,Variable
-LearningJobType_SamplesCollected,6182,Variable
-LearningJobType_LastError,6183,Variable
-LearningJobType_StartCollection,6184,Method
-LearningJobType_StopCollection,6185,Method
-LearningJobType_TriggerTraining,6186,Method
-LearningJobType_TriggerTraining_OutputArguments,6187,Variable
-LearningJobType_PromoteModel,6188,Method
-LearningJobType_PromoteModel_InputArguments,6189,Variable
-LearningJobType_PromoteModel_OutputArguments,6190,Variable
VisionRootType,1001,ObjectType
VisionRootType_Sensors,6191,Object
VisionRootType_Pipelines,6192,Object
-VisionRootType_Models,6193,Object
VisionRootType_Frames,6194,Object
-VisionRootType_LearningJobs,6195,Object
Vision,7001,Object
MediaEndpointType_DataChannelSource,6196,Variable
MediaEndpointType_DataChannelContentType,6197,Variable
+InferencePipelineType_LearningJob,6198,Variable
diff --git a/metaverse-specs/vision/Opc.Ua.Vision.NodeSet2.xml b/metaverse-specs/vision/Opc.Ua.Vision.NodeSet2.xml
index c6dd314..cbe1e35 100644
--- a/metaverse-specs/vision/Opc.Ua.Vision.NodeSet2.xml
+++ b/metaverse-specs/vision/Opc.Ua.Vision.NodeSet2.xml
@@ -5,7 +5,7 @@
http://opcfoundation.org/UA/Vision/
-
+
@@ -153,44 +153,6 @@
NoneBasicDigestTokenMutualTls
-
- VisionInferenceLocationEnum
- Where inference executes. The result contract is identical in every case; this property exists so a client can reason about latency, availability and trust boundary without changing how it reads results.
- Vision DataTypes
-
- i=29
- ns=1;i=3907
-
- In the OPC UA Server process or on its host.On a separate edge node reached over the network.In a remote or cloud service.Inside the simulator that also renders the sensor.
-
-
- EnumStrings
-
- i=78
- i=68
- ns=1;i=3007
-
- OnServerEdgeOffServerCloudInSimulator
-
-
- VisionAcceleratorKindEnum
- Compute device executing the model.
- Vision DataTypes
-
- i=29
- ns=1;i=3908
-
-
-
-
- EnumStrings
-
- i=78
- i=68
- ns=1;i=3008
-
- CpuGpuNpuFpgaTpuOther
-
VisionResultEvaluationEnum
Overall verdict of a result. Value semantics are aligned with the ResultEvaluationEnum of OPC 40001-101 so that a client already consuming Machinery results needs no new interpretation rules.
@@ -269,22 +231,22 @@
VisionFrameRoleEnum
- Role of a coordinate frame, following the ISO 9787 frame vocabulary.
+ Role of a coordinate frame, following the ISO 9787 frame vocabulary. The mechanical interface and the tool are DISTINCT roles: a camera on a robot flange is calibrated to the mechanical interface, while a pick pose has to reach the tool centre point, and a model that cannot tell them apart cannot express the offset between them.
Vision DataTypes
i=29
ns=1;i=3913
- Tool / tool centre point (TCP) frame.
+ The flange at the end of the last link, to which an end effector is fitted. This is what an eye-in-hand extrinsic calibration resolves to.A tool frame, whose origin is a tool centre point.A camera frame. Numbered after the ISO 9787 roles because it is not one of them.
-
+
EnumStrings
i=78
i=68
ns=1;i=3013
- WorldBaseToolCameraObjectOther
+ WorldBaseMechanicalInterfaceToolObjectOtherCamera
VisionDistortionModelEnum
@@ -324,44 +286,6 @@
Area2DLine2DDepth3DThermalMultispectralEventOther
-
- VisionLearningJobStateEnum
- State of a dataset-capture, retraining and promotion cycle.
- Vision DataTypes
-
- i=29
- ns=1;i=3916
-
- A candidate model is available for promotion.
-
-
- EnumStrings
-
- i=78
- i=68
- ns=1;i=3016
-
- IdleCollectingLabellingTrainingValidatingReadyPromotedFailed
-
-
- VisionDatasetSourceEnum
- Provenance of the samples in a dataset.
- Vision DataTypes
-
- i=29
- ns=1;i=3917
-
- Captured from physical sensors.Rendered by a simulator.Both, e.g. synthetic pre-training with real fine-tuning.
-
-
- EnumStrings
-
- i=78
- i=68
- ns=1;i=3017
-
- RealSyntheticMixed
-
VisionPose3DDataType
A rigid-body pose expressed in a named coordinate frame. Position is metres; Orientation is a unit quaternion ordered (x, y, z, w). Covariance is an optional row-major 6x6 matrix over (x, y, z, rx, ry, rz); an empty array means the uncertainty is not reported.
@@ -372,7 +296,7 @@
FrameId of the CoordinateFrame this pose is expressed in.Translation (x, y, z) in metres.Unit quaternion (x, y, z, w).Row-major 6x6 covariance, or empty.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -390,7 +314,7 @@
Box centre x, in pixels.Box centre y, in pixels.Box width, in pixels.Box height, in pixels.Rotation about the centre, in degrees.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -408,7 +332,7 @@
Centre pose of the box.Extent (x, y, z) in metres.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -426,7 +350,7 @@
Location of the encoded image.Cryptographic digest of the referenced bytes.Digest algorithm; default SHA-256.Container or encoding of the image.Pixel format using GenICam PFNC naming, for example Mono8, BayerRG12 or RGB8.Image width in pixels.Image height in pixels.Encoded size, so a client can decide whether to fetch.Acquisition time of the frame.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -444,7 +368,7 @@
Focal length in pixels, x.Focal length in pixels, y.Principal point x, in pixels.Principal point y, in pixels.Axis skew; 0 for square pixels.Model the coefficients belong to.Coefficients, model-ordered.Image width these intrinsics are valid for.Image height these intrinsics are valid for.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -462,7 +386,7 @@
Identifier unique within the result.Human-readable class name.Numeric class identifier within the model label set.Score in the range 0.0 to 1.0.True when BoundingBox2D is meaningful.Image-space box.True when BoundingBox3D is meaningful.Object-space box.True when Pose is meaningful.6-DoF pose, for example a grasp pose.Stable identity across frames, when tracking is done.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -480,7 +404,7 @@
Identifier of the characteristic measured.Human-readable characteristic name.Design or target value.Measured value.Actual minus Nominal.Lower tolerance limit, relative to Nominal.Upper tolerance limit, relative to Nominal.Expanded measurement uncertainty (ISO 14253), in the same unit as Actual. 0 means not reported.Engineering unit of Nominal, Actual and Deviation.Per-characteristic outcome.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -498,7 +422,7 @@
Opaque token identifying the lease.Media URI to open.Protocol of the returned URI.Expiry after which the Uri is no longer valid.
-
+
Default Binary
Default Binary encoding of the structure.
@@ -506,24 +430,6 @@
ns=1;i=3057
-
- VisionTensorSignatureDataType
- Shape and element type of one model input or output tensor.
- Vision DataTypes
-
- i=22
- ns=1;i=5009
-
- Tensor name as declared by the model.Element type, for example float32, uint8 or int64.Dimensions; -1 marks a dynamic axis.Optional axis layout hint, for example NCHW or NHWC.
-
-
- Default Binary
- Default Binary encoding of the structure.
-
- i=76
- ns=1;i=3058
-
-
HasCalibration
Links a sensor to a calibration currently valid for it.
@@ -551,15 +457,6 @@
i=32
-
- UsesModel
- Links an AiDeploymentType instance to the AiModelType instance it executes. Clause 5.11 requires exactly one such reference per deployment; it is the only defined path from a result to the model artefact and its Digest, on which clause 12.6 depends.
- Vision ReferenceTypes
- IsUsedByDeployment
-
- i=32
-
-
ProducedBy
Links a result to the inference pipeline that produced it.
@@ -1738,330 +1635,6 @@
ns=1;i=1030
-
- AiModelType
- Nameplate of a trained model. The member set is deliberately aligned with the IDTA 02060 AI Model Nameplate submodel template, which is currently the only standardised description of an industrial AI model, so an Asset Administration Shell can be populated from this node without loss.
- Vision
-
- i=58
- ns=1;i=6107
- ns=1;i=6108
- ns=1;i=6109
- ns=1;i=6110
- ns=1;i=6111
- ns=1;i=6112
- ns=1;i=6113
- ns=1;i=6114
- ns=1;i=6115
- ns=1;i=6116
- ns=1;i=6117
- ns=1;i=6118
- ns=1;i=6119
-
-
-
- ModelId
- Identifier of the model.
-
- i=78
- i=68
- ns=1;i=1015
-
-
-
- Name
- Human-readable model name.
-
- i=78
- i=68
- ns=1;i=1015
-
-
-
- Version
- Model version.
-
- i=78
- i=68
- ns=1;i=1015
-
-
-
- Framework
- Producing framework, for example PyTorch, TensorFlow or scikit-learn.
-
- i=80
- i=68
- ns=1;i=1015
-
-
-
- Format
- Serialization format, for example ONNX, TensorRT or OpenVINO IR.
-
- i=80
- i=68
- ns=1;i=1015
-
-
-
- TaskKind
- What the model does, for example Detection2D, Detection3D, Classification, Segmentation, PoseEstimation or AnomalyDetection.
-
- i=80
- i=68
- ns=1;i=1015
-
-
-
- Digest
- Cryptographic digest of the model artefact, for provenance and integrity. Mandatory: clause 12.6 requires it for every model whose artefact is obtainable through ArtifactUri, and it is the terminus of the provenance chain that UsesModel keeps intact.
-
- i=78
- i=68
- ns=1;i=1015
-
-
-
- DigestAlgorithm
- Hash function used for Digest. SHALL name a function with at least 256-bit output and no known collision weakness; SHA-256 is the default and is always acceptable. SHALL NOT be MD5, SHA-1 or a truncated variant - chosen-prefix collisions against those are practical, so a substituted artefact would pass verification. SHALL be non-empty where Digest is non-empty. See clause 12.6.
-
- i=78
- i=68
- ns=1;i=1015
-
-
-
- ArtifactUri
- Where the model artefact can be obtained. Treated as untrusted input.
-
- i=80
- i=68
- ns=1;i=1015
-
-
-
- ProvenanceUri
- Training provenance or model card location.
-
- i=80
- i=68
- ns=1;i=1015
-
-
-
- LabelClasses
- Ordered class label set; the index corresponds to VisionDetectionDataType.ClassId.
-
- i=80
- i=68
- ns=1;i=1015
-
-
-
- Inputs
- Input tensor signatures.
-
- i=80
- i=63
- ns=1;i=1015
-
-
-
- Outputs
- Output tensor signatures.
-
- i=80
- i=63
- ns=1;i=1015
-
-
-
- AiDatasetType
- A dataset used to train or validate a model. Aligned with the IDTA 02058 AI Dataset submodel template. SourceKind distinguishes real capture from simulator output, which is the provenance a reviewer needs when synthetic data is involved.
- Vision
-
- i=58
- ns=1;i=6120
- ns=1;i=6121
- ns=1;i=6122
- ns=1;i=6123
- ns=1;i=6124
- ns=1;i=6125
- ns=1;i=6126
- ns=1;i=6127
- ns=1;i=6128
-
-
-
- DatasetId
- Identifier of the dataset.
-
- i=78
- i=68
- ns=1;i=1016
-
-
-
- Name
- Human-readable dataset name.
-
- i=80
- i=68
- ns=1;i=1016
-
-
-
- Version
- Dataset version.
-
- i=80
- i=68
- ns=1;i=1016
-
-
-
- SourceKind
- Whether samples are real, synthetic or mixed.
-
- i=78
- i=68
- ns=1;i=1016
-
-
-
- SampleCount
- Number of samples.
-
- i=80
- i=68
- ns=1;i=1016
-
-
-
- LabelClasses
- Class labels present.
-
- i=80
- i=68
- ns=1;i=1016
-
-
-
- CreatedAt
- Creation time.
-
- i=80
- i=68
- ns=1;i=1016
-
-
-
- ArtifactUri
- Where the dataset can be obtained.
-
- i=80
- i=68
- ns=1;i=1016
-
-
-
- Digest
- Digest of the dataset artefact.
-
- i=80
- i=68
- ns=1;i=1016
-
-
-
- AiDeploymentType
- A model made executable somewhere. Aligned with the IDTA 02059 AI Deployment submodel template. InferenceLocation is the on-server versus off-server switch: it changes where the computation happens and therefore the trust boundary, but it does NOT change the result contract.
- Vision
-
- i=58
- ns=1;i=6129
- ns=1;i=6130
- ns=1;i=6131
- ns=1;i=6132
- ns=1;i=6133
- ns=1;i=6134
- ns=1;i=6135
- ns=1;i=6136
-
-
-
- DeploymentId
- Identifier of the deployment.
-
- i=78
- i=68
- ns=1;i=1017
-
-
-
- InferenceLocation
- Where inference executes.
-
- i=78
- i=68
- ns=1;i=1017
-
-
-
- AcceleratorKind
- Compute device executing the model.
-
- i=80
- i=68
- ns=1;i=1017
-
-
-
- AcceleratorName
- Free-text accelerator identification, for example an NPU or GPU part name.
-
- i=80
- i=68
- ns=1;i=1017
-
-
-
- EndpointUri
- Inference endpoint when InferenceLocation is not OnServer. Treated as untrusted input and subject to the resolver policy of the security clause.
-
- i=80
- i=68
- ns=1;i=1017
-
-
-
- LatencyBudget
- Latency the deployment is expected to meet, so a client can detect regression.
-
- i=80
- i=68
- ns=1;i=1017
-
-
-
- BatchSize
- Configured inference batch size.
-
- i=80
- i=68
- ns=1;i=1017
-
-
-
- State
- Runtime state of the deployment.
-
- i=80
- i=68
- ns=1;i=1017
-
-
VisionResultType
Abstract base for a vision result. Unlike OPC 40100-1, whose ResultContent is BaseDataType[] and explicitly not defined, the subtypes of this type define their content. The trust members exist so that a high-risk deployment can log which model version produced a decision and where its explanation lives.
@@ -2380,7 +1953,7 @@
InferencePipelineType
- Binds a sensor to a deployment and publishes the results. The same type serves on-server and off-server inference: when the deployment is remote the Server publishes results it did not compute, and the only observable difference is AiDeployment.InferenceLocation.
+ Binds a sensor to a deployment and publishes the results. The same type serves on-server and off-server inference: when the deployment is remote the Server publishes results it did not compute, and the only observable difference is DeploymentType.InferenceLocation.
Vision
i=58
@@ -2394,6 +1967,7 @@
ns=1;i=6172
ns=1;i=6175
ns=1;i=6176
+ ns=1;i=6198
@@ -2416,7 +1990,7 @@
Deployment
- Deployment executing inference.
+ The deployment executing inference. This is a NodeId, not a reference, and the node it names is NOT defined by this specification - see clause 8.2. Where the Server also implements OPC UA - AI Model Management and Inference it names a DeploymentType instance there, which is what clause 8's provenance argument assumes; a Server that describes its deployment some other way names that node instead. Nothing in this NodeSet references the other model's identifiers, so adopting or ignoring it changes nothing about loading this one.
i=78
i=68
@@ -2503,150 +2077,6 @@
ns=1;i=1018
-
- LearningJobType
- One turn of the capture, label, train and promote loop. It exists so that corrections arriving through VisionFeedbackType have somewhere to accumulate and a defined path into a new model version. A Server may implement only the capture stages and leave training to an external MLOps system - the state machine is the same either way.
- Vision
-
- i=58
- ns=1;i=6177
- ns=1;i=6178
- ns=1;i=6179
- ns=1;i=6180
- ns=1;i=6181
- ns=1;i=6182
- ns=1;i=6183
- ns=1;i=6184
- ns=1;i=6185
- ns=1;i=6186
- ns=1;i=6188
-
-
-
- JobId
- Identifier of the job.
-
- i=78
- i=68
- ns=1;i=1019
-
-
-
- State
- Current stage of the loop.
-
- i=78
- i=68
- ns=1;i=1019
-
-
-
- Dataset
- Dataset being accumulated or used.
-
- i=80
- i=68
- ns=1;i=1019
-
-
-
- BaseModel
- Model the job starts from.
-
- i=80
- i=68
- ns=1;i=1019
-
-
-
- CandidateModel
- Model produced by the job, awaiting promotion.
-
- i=80
- i=68
- ns=1;i=1019
-
-
-
- SamplesCollected
- Samples accumulated so far, including corrections fed back.
-
- i=80
- i=68
- ns=1;i=1019
-
-
-
- LastError
- Diagnostic for the Failed state.
-
- i=80
- i=68
- ns=1;i=1019
-
-
-
- StartCollection
- Begin accumulating samples and corrections into the dataset.
-
- i=80
- ns=1;i=1019
-
-
-
- StopCollection
- Stop accumulating samples.
-
- i=80
- ns=1;i=1019
-
-
-
- TriggerTraining
- Request that a candidate model be trained from the collected dataset.
-
- i=80
- ns=1;i=1019
- ns=1;i=6187
-
-
-
- OutputArguments
-
- i=78
- i=68
- ns=1;i=6186
-
- i=297Acceptedi=1-1True when the request was queued.
-
-
- PromoteModel
- Promote the candidate model so that deployments begin using it. A Server SHOULD require a distinct authorization for this Method.
-
- i=80
- ns=1;i=1019
- ns=1;i=6189
- ns=1;i=6190
-
-
-
- InputArguments
-
- i=78
- i=68
- ns=1;i=6188
-
- i=297Deploymenti=17-1Deployment to update, or null for all.
-
-
- OutputArguments
-
- i=78
- i=68
- ns=1;i=6188
-
- i=297PromotedModeli=17-1The model now in use.
-
VisionRootType
The single well-known entry point for everything in this model. A client starts here, enumerates Sensors, and follows references outward. Mirrors the discovery pattern of OPC UA - OpenUSD Bindings.
@@ -2655,9 +2085,7 @@
i=58
ns=1;i=6191
ns=1;i=6192
- ns=1;i=6193
ns=1;i=6194
- ns=1;i=6195
@@ -2678,15 +2106,6 @@
ns=1;i=1001
-
- Models
- AiModelType, AiDatasetType and AiDeploymentType instances.
-
- i=80
- i=61
- ns=1;i=1001
-
-
Frames
CoordinateFrameType instances.
@@ -2696,15 +2115,6 @@
ns=1;i=1001
-
- LearningJobs
- LearningJobType instances.
-
- i=80
- i=61
- ns=1;i=1001
-
-
Vision
The well-known Vision entry point, a component of the Server object. A conformant Server exposes exactly one.
@@ -2731,4 +2141,13 @@
ns=1;i=1007
+
+ LearningJob
+ LearningJobType instance that consumes GroundTruthLabel corrections submitted through this pipeline's Feedback object, or null where the Server retains none. A NodeId and not a reference, for the same reason Deployment is: this model takes no dependency on the model that defines the job. Section 9.5.1 requires this to be non-null wherever such a correction is retained - without it a client cannot establish whether its label reached a learning loop at all.
+
+ i=80
+ i=68
+ ns=1;i=1018
+
+
diff --git a/metaverse-specs/vision/machine-vision/OPC-UA-Inspection-Vision-Addendum.md b/metaverse-specs/vision/machine-vision/OPC-UA-Inspection-Vision-Addendum.md
index 2a11e4e..3f04edc 100644
--- a/metaverse-specs/vision/machine-vision/OPC-UA-Inspection-Vision-Addendum.md
+++ b/metaverse-specs/vision/machine-vision/OPC-UA-Inspection-Vision-Addendum.md
@@ -71,10 +71,10 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
|---|---|---|
| `Fx` | `8310.2` | px |
| `Fy` | `8309.6` | px |
-| `Cx` | `1295.4` | px, corner-datum per 5.10 |
-| `Cy` | `971.2` | px, corner-datum per 5.10 |
+| `Cx` | `1295.4` | px, corner-datum per §5.12 |
+| `Cy` | `971.2` | px, corner-datum per §5.12 |
| `Skew` | `0.0` | px |
-| `DistortionModel` | `BrownConrady` | 5.10 ordering: k1, k2, p1, p2, k3 |
+| `DistortionModel` | `BrownConrady` | §5.12 ordering: k1, k2, p1, p2, k3 |
| `DistortionCoefficients` | `[-0.0021, 0.0004, 0.0, 0.0, 0.0]` | dimensionless; a telecentric lens is close to distortion-free |
| `Width` | `2592` | px |
| `Height` | `1944` | px |
@@ -96,10 +96,10 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
| Field | Value | Unit / convention |
|---|---|---|
-| `FrameId` | `station` | equals the TargetFrame's FrameId, per the 5.10 frame-precedence rule |
+| `FrameId` | `station` | equals the TargetFrame's FrameId, per the §5.12 frame-precedence rule |
| `Position` | `(0.0, 0.0, 0.320)` | metres, ordered (x, y, z) |
| `Orientation` | `(1.0, 0.0, 0.0, 0.0)` | unit quaternion ordered (x, y, z, w); a 180 degree rotation about x, so the camera looks down at the station |
-| `Covariance` | `empty array` | not reported, per the 5.10 sentinel |
+| `Covariance` | `empty array` | not reported, per the §5.12 sentinel |
Each calibration is reachable from the sensor by a `HasCalibration` reference, as base specification §5.11 requires.
@@ -114,7 +114,7 @@ Each calibration is reachable from the sensor by a `HasCalibration` reference, a
Inference runs **on-server**: `InferenceLocation = OnServer`, on an NPU in the station industrial PC. A client consuming the results cannot distinguish this from the off-server robotics example except by reading that one property — which is the intent of base specification §8.2. Because the pipeline is not continuous, `RunInference` is called per part by the station PLC and returns the `ResultId` it produced.
-The deployment carries exactly one `UsesModel` reference to the model above, as base specification §5.11 requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the §12.6 provenance check possible.
+The deployment carries exactly one `UsesModel` reference to the model above, as *OPC UA — AI Model Management and Inference* requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the base specification's §12.6 provenance check possible.
## 7 Results
diff --git a/metaverse-specs/vision/machine-vision/Opc.Ua.Inspection.Vision.NodeSet2.xml b/metaverse-specs/vision/machine-vision/Opc.Ua.Inspection.Vision.NodeSet2.xml
index f1ed44e..c387312 100644
--- a/metaverse-specs/vision/machine-vision/Opc.Ua.Inspection.Vision.NodeSet2.xml
+++ b/metaverse-specs/vision/machine-vision/Opc.Ua.Inspection.Vision.NodeSet2.xml
@@ -4,11 +4,13 @@
http://opcfoundation.org/UA/Vision/Examples/Inspection/
http://opcfoundation.org/UA/Vision/
+ http://opcfoundation.org/UA/AI/
-
+
-
+
+
@@ -29,7 +31,7 @@
i=15
ns=2;i=4001
ns=2;i=4002
- ns=2;i=4004
+ ns=3;i=4001
Vision
@@ -40,8 +42,6 @@
ns=1;i=5002
ns=1;i=5003
ns=1;i=5004
- ns=1;i=5005
- ns=1;i=5006
@@ -49,7 +49,7 @@
i=61
ns=1;i=5001
- ns=1;i=5007
+ ns=1;i=5011
@@ -57,44 +57,76 @@
i=61
ns=1;i=5001
- ns=1;i=5102
+ ns=1;i=5106
-
- Models
+
+ Frames
i=61
ns=1;i=5001
- ns=1;i=5087
- ns=1;i=5097
+ ns=1;i=5065
+ ns=1;i=5068
-
- Frames
+
+ AiModelManagement
+ Well-known AI Model Management entry point for this example.
+
+ ns=3;i=1001
+ i=2253
+ ns=1;i=5006
+ ns=1;i=5007
+ ns=1;i=5008
+ ns=1;i=5009
+ ns=1;i=5010
+
+
+
+ SpecificationVersion
+ Release of the AI Model Management specification this example is built against.
+
+ i=68
+ ns=1;i=5005
+
+ 0.4.0
+
+
+ Models
i=61
- ns=1;i=5001
- ns=1;i=5061
- ns=1;i=5064
+ ns=1;i=5005
+ ns=1;i=5091
+ ns=1;i=5101
+
+
+
+ Datasets
+
+ i=61
+ ns=1;i=5005
-
+
+ Deployments
+
+ i=61
+ ns=1;i=5005
+
+
+
LearningJobs
i=61
- ns=1;i=5001
+ ns=1;i=5005
-
+
SealInspectionCamera
Fixed monochrome area-scan camera over the sealing-surface inspection station.
ns=2;i=1003
ns=1;i=5002
- ns=1;i=5008
- ns=1;i=5009
- ns=1;i=5010
- ns=1;i=5011
ns=1;i=5012
ns=1;i=5013
ns=1;i=5014
@@ -107,183 +139,183 @@
ns=1;i=5021
ns=1;i=5022
ns=1;i=5023
- ns=1;i=5024
- ns=1;i=5053
- ns=1;i=5061
- ns=1;i=5069
- ns=1;i=5070
- ns=1;i=5077
+ ns=1;i=5024
+ ns=1;i=5025
+ ns=1;i=5026
+ ns=1;i=5027
+ ns=1;i=5028
+ ns=1;i=5057
+ ns=1;i=5065
+ ns=1;i=5073
+ ns=1;i=5074
+ ns=1;i=5081
-
+
SensorId
i=68
- ns=1;i=5007
+ ns=1;i=5011
cam-insp-07
-
+
RealityKind
VisionRealityKindEnum.Physical
i=68
- ns=1;i=5007
+ ns=1;i=5011
0
-
+
Modality
VisionSensorModalityEnum.Area2D
i=68
- ns=1;i=5007
+ ns=1;i=5011
0
-
+
Manufacturer
i=68
- ns=1;i=5007
+ ns=1;i=5011
Example Vision GmbH
-
+
Model
i=68
- ns=1;i=5007
+ ns=1;i=5011
EV-M-0520
-
+
SerialNumber
i=68
- ns=1;i=5007
+ ns=1;i=5011
SN-9083-1174
-
+
DeviceUri
i=68
- ns=1;i=5007
+ ns=1;i=5011
u3v://0x2A0B/0x0410/SN-9083-1174
-
+
FrameId
i=68
- ns=1;i=5007
+ ns=1;i=5011
camera_insp_07
-
+
Width
i=68
- ns=1;i=5007
+ ns=1;i=5011
2592
-
+
Height
i=68
- ns=1;i=5007
+ ns=1;i=5011
1944
-
+
PixelFormat
i=68
- ns=1;i=5007
+ ns=1;i=5011
Mono8
-
+
ExposureTime
i=68
- ns=1;i=5007
+ ns=1;i=5011
1200.0
-
+
Gain
i=68
- ns=1;i=5007
+ ns=1;i=5011
1.0
-
+
AcquisitionFrameRate
i=68
- ns=1;i=5007
+ ns=1;i=5011
8.0
-
+
TriggerMode
i=68
- ns=1;i=5007
+ ns=1;i=5011
On
-
+
TriggerSource
i=68
- ns=1;i=5007
+ ns=1;i=5011
Line0
-
+
Media
Media endpoints and their control surface.
ns=2;i=1010
- ns=1;i=5007
- ns=1;i=5025
- ns=1;i=5026
- ns=1;i=5050
- ns=1;i=5051
- ns=1;i=5052
+ ns=1;i=5011
+ ns=1;i=5029
+ ns=1;i=5030
+ ns=1;i=5054
+ ns=1;i=5055
+ ns=1;i=5056
-
+
StreamEndpoints
i=61
- ns=1;i=5024
- ns=1;i=5027
+ ns=1;i=5028
+ ns=1;i=5031
-
+
ClipEndpoints
i=61
- ns=1;i=5024
- ns=1;i=5039
+ ns=1;i=5028
+ ns=1;i=5043
-
+
LiveRtsp
Live MJPEG stream for operator setup and focus checking.
ns=2;i=1008
- ns=1;i=5025
- ns=1;i=5028
- ns=1;i=5029
- ns=1;i=5030
- ns=1;i=5031
+ ns=1;i=5029
ns=1;i=5032
ns=1;i=5033
ns=1;i=5034
@@ -291,737 +323,739 @@
ns=1;i=5036
ns=1;i=5037
ns=1;i=5038
+ ns=1;i=5039
+ ns=1;i=5040
+ ns=1;i=5041
+ ns=1;i=5042
-
+
EndpointId
i=68
- ns=1;i=5027
+ ns=1;i=5031
stream-rtsp-setup
-
+
EndpointUri
i=68
- ns=1;i=5027
+ ns=1;i=5031
rtsp://192.0.2.77:554/setup
-
+
StreamProtocol
VisionStreamProtocolEnum.Rtsp
i=68
- ns=1;i=5027
+ ns=1;i=5031
0
-
+
ProtocolVersion
i=68
- ns=1;i=5027
+ ns=1;i=5031
1.0
-
+
State
VisionEndpointStateEnum.Ready
i=68
- ns=1;i=5027
+ ns=1;i=5031
1
-
+
Authentication
VisionEndpointAuthenticationEnum.Digest
i=68
- ns=1;i=5027
+ ns=1;i=5031
2
-
+
SecureTransport
Derived from the endpoint scheme; see base specification §12.2.
i=68
- ns=1;i=5027
+ ns=1;i=5031
false
-
+
Codec
VisionVideoCodecEnum.Mjpeg
i=68
- ns=1;i=5027
+ ns=1;i=5031
2
-
+
Width
i=68
- ns=1;i=5027
+ ns=1;i=5031
1296
-
+
Height
i=68
- ns=1;i=5027
+ ns=1;i=5031
972
-
+
FrameRate
i=68
- ns=1;i=5027
+ ns=1;i=5031
8.0
-
+
PartFrames
JPEG still per inspected part. Publishes a reduced-size thumbnail inline and the full image by URI.
ns=2;i=1009
- ns=1;i=5026
- ns=1;i=5040
- ns=1;i=5041
- ns=1;i=5042
- ns=1;i=5043
+ ns=1;i=5030
ns=1;i=5044
ns=1;i=5045
ns=1;i=5046
ns=1;i=5047
- ns=1;i=5048
- ns=1;i=5049
+ ns=1;i=5048
+ ns=1;i=5049
+ ns=1;i=5050
+ ns=1;i=5051
+ ns=1;i=5052
+ ns=1;i=5053
-
+
EndpointId
i=68
- ns=1;i=5039
+ ns=1;i=5043
clip-jpeg-part
-
+
EndpointUri
i=68
- ns=1;i=5039
+ ns=1;i=5043
https://192.0.2.77/clips/{resultId}.jpg
-
+
ClipFormat
VisionClipFormatEnum.Jpeg
i=68
- ns=1;i=5039
+ ns=1;i=5043
0
-
+
State
VisionEndpointStateEnum.Active
i=68
- ns=1;i=5039
+ ns=1;i=5043
2
-
+
Authentication
VisionEndpointAuthenticationEnum.Token
i=68
- ns=1;i=5039
+ ns=1;i=5043
3
-
+
SecureTransport
Derived from the endpoint scheme; see base specification §12.2.
i=68
- ns=1;i=5039
+ ns=1;i=5043
true
-
+
InlineDeliveryEnabled
i=68
- ns=1;i=5039
+ ns=1;i=5043
true
-
+
MaxInlineClipSize
i=68
- ns=1;i=5039
+ ns=1;i=5043
262144
-
+
LatestClip
Most recent clip, published inline within MaxInlineClipSize. Subscribable; see §6.4 rules 3 to 5 for the overflow, correlation and initial-state behaviour.
i=63
- ns=1;i=5039
+ ns=1;i=5043
-
+
LatestClipMetadata
Descriptor for LatestClip, carrying the Uri that remains valid when the inline payload does not fit, and the Timestamp and Digest that correlate the two (§6.4 rule 4).
i=63
- ns=1;i=5039
+ ns=1;i=5043
-
+
GetStreamEndpoint
GetStreamEndpoint as declared by VisionMediaManagementType.
- ns=1;i=5024
+ ns=1;i=5028
-
+
ReleaseStreamEndpoint
ReleaseStreamEndpoint as declared by VisionMediaManagementType.
- ns=1;i=5024
+ ns=1;i=5028
-
+
GetClip
GetClip as declared by VisionMediaManagementType.
- ns=1;i=5024
+ ns=1;i=5028
-
+
Optics
ns=2;i=1005
- ns=1;i=5007
- ns=1;i=5054
- ns=1;i=5055
- ns=1;i=5056
- ns=1;i=5057
+ ns=1;i=5011
ns=1;i=5058
ns=1;i=5059
ns=1;i=5060
+ ns=1;i=5061
+ ns=1;i=5062
+ ns=1;i=5063
+ ns=1;i=5064
-
+
FocalLength
i=68
- ns=1;i=5053
+ ns=1;i=5057
25.0
-
+
Aperture
i=68
- ns=1;i=5053
+ ns=1;i=5057
5.6
-
+
WorkingDistance
i=68
- ns=1;i=5053
+ ns=1;i=5057
0.32
-
+
Magnification
i=68
- ns=1;i=5053
+ ns=1;i=5057
0.14
-
+
OpticalFormat
i=68
- ns=1;i=5053
+ ns=1;i=5057
1/1.8"
-
+
MountType
i=68
- ns=1;i=5053
+ ns=1;i=5057
C
-
+
LensType
i=68
- ns=1;i=5053
+ ns=1;i=5057
Telecentric
-
+
StationFrame
Inspection station world frame.
ns=2;i=1011
- ns=1;i=5005
- ns=1;i=5062
- ns=1;i=5063
+ ns=1;i=5004
+ ns=1;i=5066
ns=1;i=5067
- ns=1;i=5007
+ ns=1;i=5071
+ ns=1;i=5011
-
+
FrameId
i=68
- ns=1;i=5061
+ ns=1;i=5065
station
-
+
Role
VisionFrameRoleEnum.World
i=68
- ns=1;i=5061
+ ns=1;i=5065
0
-
+
CameraFrame
Optical frame of the fixed inspection camera.
ns=2;i=1011
- ns=1;i=5005
- ns=1;i=5065
- ns=1;i=5066
- ns=1;i=5068
+ ns=1;i=5004
+ ns=1;i=5069
+ ns=1;i=5070
+ ns=1;i=5072
-
+
FrameId
i=68
- ns=1;i=5064
+ ns=1;i=5068
camera_insp_07
-
+
Role
VisionFrameRoleEnum.Camera
i=68
- ns=1;i=5064
+ ns=1;i=5068
- 3
+ 6
-
+
ParentFrame
i=68
- ns=1;i=5061
+ ns=1;i=5065
i=0
-
+
ParentFrame
i=68
- ns=1;i=5064
+ ns=1;i=5068
- ns=1;i=5061
+ ns=1;i=5065
-
+
Calibrations
i=61
- ns=1;i=5007
- ns=1;i=5070
- ns=1;i=5077
+ ns=1;i=5011
+ ns=1;i=5074
+ ns=1;i=5081
-
+
Intrinsics2592x1944
Pinhole intrinsics at full resolution; a telecentric lens leaves very little residual distortion.
ns=2;i=1013
- ns=1;i=5069
- ns=1;i=5071
- ns=1;i=5072
- ns=1;i=5073
- ns=1;i=5074
+ ns=1;i=5073
ns=1;i=5075
- ns=1;i=5007
- ns=1;i=5076
+ ns=1;i=5076
+ ns=1;i=5077
+ ns=1;i=5078
+ ns=1;i=5079
+ ns=1;i=5011
+ ns=1;i=5080
-
+
CalibrationId
i=68
- ns=1;i=5070
+ ns=1;i=5074
intr-cam-insp-07
-
+
PerformedAt
i=68
- ns=1;i=5070
+ ns=1;i=5074
2026-05-02T07:55:00Z
-
+
Valid
i=68
- ns=1;i=5070
+ ns=1;i=5074
true
-
+
ResidualError
i=68
- ns=1;i=5070
+ ns=1;i=5074
0.08
-
+
Method
i=68
- ns=1;i=5070
+ ns=1;i=5074
Zhang
-
+
Intrinsics
Intrinsic parameters. The field values are tabulated in the addendum's calibration clause.
i=63
- ns=1;i=5070
+ ns=1;i=5074
-
+
StationMounting
Transform from the camera frame to the station world frame. The camera is fixed, so there is no kinematic chain.
ns=2;i=1014
- ns=1;i=5069
- ns=1;i=5078
- ns=1;i=5079
- ns=1;i=5080
- ns=1;i=5081
+ ns=1;i=5073
ns=1;i=5082
- ns=1;i=5007
ns=1;i=5083
ns=1;i=5084
ns=1;i=5085
- ns=1;i=5086
+ ns=1;i=5086
+ ns=1;i=5011
+ ns=1;i=5087
+ ns=1;i=5088
+ ns=1;i=5089
+ ns=1;i=5090
-
+
CalibrationId
i=68
- ns=1;i=5077
+ ns=1;i=5081
extr-cam-insp-07
-
+
PerformedAt
i=68
- ns=1;i=5077
+ ns=1;i=5081
2026-05-02T08:20:00Z
-
+
Valid
i=68
- ns=1;i=5077
+ ns=1;i=5081
true
-
+
ResidualError
i=68
- ns=1;i=5077
+ ns=1;i=5081
0.00015
-
+
Method
i=68
- ns=1;i=5077
+ ns=1;i=5081
TargetPlate
-
+
Mount
VisionCalibrationMountEnum.Fixed
i=68
- ns=1;i=5077
+ ns=1;i=5081
2
-
+
SourceFrame
i=68
- ns=1;i=5077
+ ns=1;i=5081
- ns=1;i=5064
+ ns=1;i=5068
-
+
TargetFrame
i=68
- ns=1;i=5077
+ ns=1;i=5081
- ns=1;i=5061
+ ns=1;i=5065
-
+
Transform
Pose of SourceFrame expressed in TargetFrame. The field values are tabulated in the addendum's calibration clause.
i=63
- ns=1;i=5077
+ ns=1;i=5081
-
+
SealDefectNet
Segmentation and measurement of sealing-surface defects.
- ns=2;i=1015
- ns=1;i=5004
- ns=1;i=5088
- ns=1;i=5089
- ns=1;i=5090
- ns=1;i=5091
+ ns=3;i=1002
+ ns=1;i=5007
ns=1;i=5092
ns=1;i=5093
ns=1;i=5094
ns=1;i=5095
ns=1;i=5096
- ns=1;i=5097
+ ns=1;i=5097
+ ns=1;i=5098
+ ns=1;i=5099
+ ns=1;i=5100
+ ns=1;i=5101
-
+
ModelId
i=68
- ns=1;i=5087
+ ns=1;i=5091
sealdefectnet
-
+
Name
i=68
- ns=1;i=5087
+ ns=1;i=5091
SealDefectNet
-
+
Version
i=68
- ns=1;i=5087
+ ns=1;i=5091
1.4.1
-
+
Framework
i=68
- ns=1;i=5087
+ ns=1;i=5091
PyTorch
-
+
Format
i=68
- ns=1;i=5087
+ ns=1;i=5091
ONNX
-
+
TaskKind
i=68
- ns=1;i=5087
+ ns=1;i=5091
Segmentation
-
+
ArtifactUri
i=68
- ns=1;i=5087
+ ns=1;i=5091
https://models.example.com/sealdefectnet/1.4.1.onnx
-
+
Digest
- SHA-256 of the model artefact at ArtifactUri (base specification §12.6).
+ SHA-256 of the model artefact at ArtifactUri.
i=68
- ns=1;i=5087
+ ns=1;i=5091
3f8a1c4e9b2d7f60a5c3e81d4f92b6a7c0d5e83f1a2b4c6d8e0f3a5b7c9d1e2f
-
+
DigestAlgorithm
i=68
- ns=1;i=5087
+ ns=1;i=5091
SHA-256
-
+
OnServerDeployment
Inference in the Server's own process, on the station industrial PC.
- ns=2;i=1017
- ns=1;i=5004
- ns=1;i=5098
- ns=1;i=5099
- ns=1;i=5100
- ns=1;i=5101
- ns=1;i=5087
+ ns=3;i=1004
+ ns=1;i=5007
+ ns=1;i=5102
+ ns=1;i=5103
+ ns=1;i=5104
+ ns=1;i=5105
+ ns=1;i=5091
-
+
DeploymentId
i=68
- ns=1;i=5097
+ ns=1;i=5101
deploy-onserver-01
-
+
InferenceLocation
- VisionInferenceLocationEnum.OnServer
i=68
- ns=1;i=5097
+ ns=1;i=5101
0
-
+
AcceleratorKind
- VisionAcceleratorKindEnum.Npu
i=68
- ns=1;i=5097
+ ns=1;i=5101
2
-
+
AcceleratorName
i=68
- ns=1;i=5097
+ ns=1;i=5101
Example NPU 8-core
-
+
SealInspectionPipeline
One inference per part, triggered by the station PLC.
ns=2;i=1018
ns=1;i=5003
- ns=1;i=5103
- ns=1;i=5104
- ns=1;i=5105
- ns=1;i=5106
ns=1;i=5107
- ns=1;i=5108
- ns=1;i=5109
+ ns=1;i=5108
+ ns=1;i=5109
+ ns=1;i=5110
+ ns=1;i=5111
+ ns=1;i=5112
+ ns=1;i=5113
-
+
PipelineId
i=68
- ns=1;i=5102
+ ns=1;i=5106
pipe-seal-01
-
+
Sensor
i=68
- ns=1;i=5102
+ ns=1;i=5106
- ns=1;i=5007
+ ns=1;i=5011
-
+
Deployment
i=68
- ns=1;i=5102
+ ns=1;i=5106
- ns=1;i=5097
+ ns=1;i=5101
-
+
State
VisionEndpointStateEnum.Active
i=68
- ns=1;i=5102
+ ns=1;i=5106
2
-
+
Continuous
i=68
- ns=1;i=5102
+ ns=1;i=5106
false
-
+
Results
i=61
- ns=1;i=5102
+ ns=1;i=5106
-
+
Feedback
Feedback surface for corrections and overlays.
ns=2;i=1024
- ns=1;i=5102
+ ns=1;i=5106
diff --git a/metaverse-specs/vision/robotics/OPC-UA-Robotics-Vision-Addendum.md b/metaverse-specs/vision/robotics/OPC-UA-Robotics-Vision-Addendum.md
index dc5e307..a03ea2e 100644
--- a/metaverse-specs/vision/robotics/OPC-UA-Robotics-Vision-Addendum.md
+++ b/metaverse-specs/vision/robotics/OPC-UA-Robotics-Vision-Addendum.md
@@ -65,7 +65,8 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
|---|---|---|---|
| `WorldFrame` | `world` | `World` | none (tree root) |
| `RobotBaseFrame` | `robot_base` | `Base` | `world` |
-| `FlangeFrame` | `flange` | `Tool` | `robot_base` |
+| `FlangeFrame` | `flange` | `MechanicalInterface` | `robot_base` |
+| `GripperTcpFrame` | `gripper_tcp` | `Tool` | `flange` |
| `CameraFrame` | `camera_eih` | `Camera` | `flange` |
**`Intrinsics2448x2048`** (`IntrinsicCalibrationType`) — Pinhole intrinsics with Brown-Conrady distortion at full resolution.
@@ -84,15 +85,15 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
|---|---|---|
| `Fx` | `2140.5` | px |
| `Fy` | `2139.8` | px |
-| `Cx` | `1223.1` | px, corner-datum per 5.10 |
-| `Cy` | `1021.7` | px, corner-datum per 5.10 |
+| `Cx` | `1223.1` | px, corner-datum per §5.12 |
+| `Cy` | `1021.7` | px, corner-datum per §5.12 |
| `Skew` | `0.0` | px |
-| `DistortionModel` | `BrownConrady` | 5.10 ordering: k1, k2, p1, p2, k3 |
+| `DistortionModel` | `BrownConrady` | §5.12 ordering: k1, k2, p1, p2, k3 |
| `DistortionCoefficients` | `[-0.1721, 0.0934, 0.0002, -0.0001, -0.0188]` | dimensionless |
| `Width` | `2448` | px |
| `Height` | `2048` | px |
-**`HandEye`** (`ExtrinsicCalibrationType`) — Transform from the camera frame to the robot flange frame. Eye-in-hand: the camera moves with the tool.
+**`HandEye`** (`ExtrinsicCalibrationType`) — Transform from the camera frame to the robot mechanical interface. Eye-in-hand: the camera moves with the flange, so a pick pose is obtained by composing camera → flange → tool centre point.
| Member | Value |
|---|---|
@@ -109,10 +110,10 @@ The frame tree. `ParentFrame` is what makes it composable: a client walks from t
| Field | Value | Unit / convention |
|---|---|---|
-| `FrameId` | `flange` | equals the TargetFrame's FrameId, per the 5.10 frame-precedence rule |
+| `FrameId` | `flange` | equals the TargetFrame's FrameId, per the §5.12 frame-precedence rule |
| `Position` | `(0.062, -0.031, 0.115)` | metres, ordered (x, y, z) |
| `Orientation` | `(0.0, 0.0, 0.7071, 0.7071)` | unit quaternion ordered (x, y, z, w) |
-| `Covariance` | `empty array` | not reported, per the 5.10 sentinel |
+| `Covariance` | `empty array` | not reported, per the §5.12 sentinel |
Each calibration is reachable from the sensor by a `HasCalibration` reference, as base specification §5.11 requires.
@@ -150,13 +151,13 @@ The twin additionally implements `IVisionSimulatedType`:
| `AcceleratorKind` | `Gpu` |
| `EndpointUri` | `grpcs://192.0.2.60:8001/graspposenet` |
-Inference runs **off-server** on a cell-side GPU appliance. The Server publishes results it did not compute. Nothing else in the model changes: a client reads `DetectionResultType` exactly as it would if `InferenceLocation` were `OnServer`, and consults that property only if it cares about the latency or trust boundary. Because the deployment is remote, base specification §12.6 applies: the channel to the inference service is authenticated and integrity-protected, and `AiModelType.Digest` lets a consumer confirm which artefact produced a result.
+Inference runs **off-server** on a cell-side GPU appliance. The Server publishes results it did not compute. Nothing else in the model changes: a client reads `DetectionResultType` exactly as it would if `InferenceLocation` were `OnServer`, and consults that property only if it cares about the latency or trust boundary. Because the deployment is remote, base specification §12.6 applies: the channel to the inference service is authenticated and integrity-protected, and `ModelType.Digest` lets a consumer confirm which artefact produced a result.
-The deployment carries exactly one `UsesModel` reference to the model above, as base specification §5.11 requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the §12.6 provenance check possible.
+The deployment carries exactly one `UsesModel` reference to the model above, as *OPC UA — AI Model Management and Inference* requires. That reference is the only defined path from a result to the model artefact and its `Digest`, so it is what makes the base specification's §12.6 provenance check possible.
## 8 Results
-Each cycle produces a `DetectionResultType` whose `Detections` carry `ClassLabel`, `Confidence`, a `BoundingBox2D`, a `BoundingBox3D` and — the member that makes the result actionable — a 6-DoF `Pose`. Every pose names its `FrameId` (`camera_eih`), which is only meaningful because the `HandEye` calibration above relates that frame to the flange. A consumer composes camera → flange → base through the `CoordinateFrameType` tree to obtain a pose the robot controller can execute. `ResidualError` on the calibration is what tells the consumer how much to trust it.
+Each cycle produces a `DetectionResultType` whose `Detections` carry `ClassLabel`, `Confidence`, a `BoundingBox2D`, a `BoundingBox3D` and — the member that makes the result actionable — a 6-DoF `Pose`. Every pose names its `FrameId` (`camera_eih`), which is only meaningful because the `HandEye` calibration above relates that frame to the flange. A consumer composes camera → flange → base through the `CoordinateFrameType` tree to obtain the pose in robot coordinates, and camera → flange → `gripper_tcp` to obtain what the gripper must actually reach. The two are distinct: the calibration resolves to the mechanical interface, while a grasp is executed at the tool centre point, and the frame tree carries the offset between them rather than leaving it to be assumed. `ResidualError` on the calibration is what tells the consumer how much to trust it.
## 9 Feedback
diff --git a/metaverse-specs/vision/robotics/Opc.Ua.Robotics.Vision.NodeSet2.xml b/metaverse-specs/vision/robotics/Opc.Ua.Robotics.Vision.NodeSet2.xml
index 19ca54a..ca61589 100644
--- a/metaverse-specs/vision/robotics/Opc.Ua.Robotics.Vision.NodeSet2.xml
+++ b/metaverse-specs/vision/robotics/Opc.Ua.Robotics.Vision.NodeSet2.xml
@@ -4,11 +4,13 @@
http://opcfoundation.org/UA/Vision/Examples/Robotics/
http://opcfoundation.org/UA/Vision/
+ http://opcfoundation.org/UA/AI/
-
+
-
+
+
@@ -29,7 +31,7 @@
i=15
ns=2;i=4001
ns=2;i=4002
- ns=2;i=4004
+ ns=3;i=4001
Vision
@@ -40,8 +42,6 @@
ns=1;i=5002
ns=1;i=5003
ns=1;i=5004
- ns=1;i=5005
- ns=1;i=5006
@@ -49,8 +49,8 @@
i=61
ns=1;i=5001
- ns=1;i=5007
- ns=1;i=5124
+ ns=1;i=5011
+ ns=1;i=5132
@@ -58,48 +58,81 @@
i=61
ns=1;i=5001
- ns=1;i=5116
+ ns=1;i=5124
-
- Models
+
+ Frames
i=61
ns=1;i=5001
- ns=1;i=5100
- ns=1;i=5110
- ns=1;i=5170
+ ns=1;i=5070
+ ns=1;i=5073
+ ns=1;i=5076
+ ns=1;i=5079
+ ns=1;i=5082
-
- Frames
+
+ AiModelManagement
+ Well-known AI Model Management entry point for this example.
+
+ ns=3;i=1001
+ i=2253
+ ns=1;i=5006
+ ns=1;i=5007
+ ns=1;i=5008
+ ns=1;i=5009
+ ns=1;i=5010
+
+
+
+ SpecificationVersion
+ Release of the AI Model Management specification this example is built against.
+
+ i=68
+ ns=1;i=5005
+
+ 0.4.0
+
+
+ Models
i=61
- ns=1;i=5001
- ns=1;i=5066
- ns=1;i=5069
- ns=1;i=5072
- ns=1;i=5075
+ ns=1;i=5005
+ ns=1;i=5108
+ ns=1;i=5118
+ ns=1;i=5178
+
+
+
+ Datasets
+
+ i=61
+ ns=1;i=5005
-
+
+ Deployments
+
+ i=61
+ ns=1;i=5005
+
+
+
LearningJobs
i=61
- ns=1;i=5001
- ns=1;i=5173
+ ns=1;i=5005
+ ns=1;i=5181
-
+
BinPickingCamera
Eye-in-hand colour camera on the robot flange, used for bin picking.
ns=2;i=1003
ns=1;i=5002
- ns=1;i=5008
- ns=1;i=5009
- ns=1;i=5010
- ns=1;i=5011
ns=1;i=5012
ns=1;i=5013
ns=1;i=5014
@@ -112,1068 +145,1098 @@
ns=1;i=5021
ns=1;i=5022
ns=1;i=5023
- ns=1;i=5024
- ns=1;i=5059
- ns=1;i=5072
- ns=1;i=5082
- ns=1;i=5083
- ns=1;i=5090
+ ns=1;i=5024
+ ns=1;i=5025
+ ns=1;i=5026
+ ns=1;i=5027
+ ns=1;i=5028
+ ns=1;i=5063
+ ns=1;i=5076
+ ns=1;i=5090
+ ns=1;i=5091
+ ns=1;i=5098
-
+
SensorId
i=68
- ns=1;i=5007
+ ns=1;i=5011
cam-eih-01
-
+
RealityKind
VisionRealityKindEnum.Physical
i=68
- ns=1;i=5007
+ ns=1;i=5011
0
-
+
Modality
VisionSensorModalityEnum.Area2D
i=68
- ns=1;i=5007
+ ns=1;i=5011
0
-
+
Manufacturer
i=68
- ns=1;i=5007
+ ns=1;i=5011
Example Vision GmbH
-
+
Model
i=68
- ns=1;i=5007
+ ns=1;i=5011
EV-3D-2400
-
+
SerialNumber
i=68
- ns=1;i=5007
+ ns=1;i=5011
SN-4417-2291
-
+
DeviceUri
i=68
- ns=1;i=5007
+ ns=1;i=5011
gev://192.0.2.41/0
-
+
FrameId
i=68
- ns=1;i=5007
+ ns=1;i=5011
camera_eih
-
+
Width
i=68
- ns=1;i=5007
+ ns=1;i=5011
2448
-
+
Height
i=68
- ns=1;i=5007
+ ns=1;i=5011
2048
-
+
PixelFormat
i=68
- ns=1;i=5007
+ ns=1;i=5011
BayerRG8
-
+
ExposureTime
i=68
- ns=1;i=5007
+ ns=1;i=5011
4000.0
-
+
Gain
i=68
- ns=1;i=5007
+ ns=1;i=5011
2.5
-
+
AcquisitionFrameRate
i=68
- ns=1;i=5007
+ ns=1;i=5011
15.0
-
+
TriggerMode
i=68
- ns=1;i=5007
+ ns=1;i=5011
On
-
+
TriggerSource
i=68
- ns=1;i=5007
+ ns=1;i=5011
Line1
-
+
Media
Media endpoints and their control surface.
ns=2;i=1010
- ns=1;i=5007
- ns=1;i=5025
- ns=1;i=5026
- ns=1;i=5056
- ns=1;i=5057
- ns=1;i=5058
+ ns=1;i=5011
+ ns=1;i=5029
+ ns=1;i=5030
+ ns=1;i=5060
+ ns=1;i=5061
+ ns=1;i=5062
-
+
StreamEndpoints
i=61
- ns=1;i=5024
- ns=1;i=5027
+ ns=1;i=5028
+ ns=1;i=5031
-
+
ClipEndpoints
i=61
- ns=1;i=5024
- ns=1;i=5042
+ ns=1;i=5028
+ ns=1;i=5046
-
+
LiveRtsp
Live H.264 stream, used by the cell HMI and for overlay of detections.
ns=2;i=1008
- ns=1;i=5025
- ns=1;i=5028
- ns=1;i=5029
- ns=1;i=5030
- ns=1;i=5031
+ ns=1;i=5029
ns=1;i=5032
ns=1;i=5033
ns=1;i=5034
- ns=1;i=5035
+ ns=1;i=5035
ns=1;i=5036
ns=1;i=5037
ns=1;i=5038
- ns=1;i=5039
+ ns=1;i=5039
ns=1;i=5040
ns=1;i=5041
+ ns=1;i=5042
+ ns=1;i=5043
+ ns=1;i=5044
+ ns=1;i=5045
-
+
EndpointId
i=68
- ns=1;i=5027
+ ns=1;i=5031
stream-rtsp-main
-
+
EndpointUri
i=68
- ns=1;i=5027
+ ns=1;i=5031
rtsp://192.0.2.41:554/main
-
+
StreamProtocol
VisionStreamProtocolEnum.Rtsp
i=68
- ns=1;i=5027
+ ns=1;i=5031
0
-
+
ProtocolVersion
i=68
- ns=1;i=5027
+ ns=1;i=5031
1.0
-
+
State
VisionEndpointStateEnum.Active
i=68
- ns=1;i=5027
+ ns=1;i=5031
2
-
+
Authentication
VisionEndpointAuthenticationEnum.Digest
i=68
- ns=1;i=5027
+ ns=1;i=5031
2
-
+
SecureTransport
Derived from the endpoint scheme; see base specification §12.2.
i=68
- ns=1;i=5027
+ ns=1;i=5031
false
-
+
H264DataChannelSource
Stands in for the Server-created data channel source. On a Server implementing the OPC UA - Data Channels draft this Object implements IDataChannelSourceType; this overlay does not reference that draft's provisional NodeIds, so it loads unchanged on a Server without it.
i=58
- ns=1;i=5027
+ ns=1;i=5031
-
+
DataChannelSource
The Object on which a client opens the data channel (§6.7).
i=68
- ns=1;i=5027
+ ns=1;i=5031
- ns=1;i=5035
+ ns=1;i=5039
-
+
DataChannelContentType
IANA media type carried on the data channel (§6.7).
i=68
- ns=1;i=5027
+ ns=1;i=5031
video/H264
-
+
Codec
VisionVideoCodecEnum.H264
i=68
- ns=1;i=5027
+ ns=1;i=5031
0
-
+
Width
i=68
- ns=1;i=5027
+ ns=1;i=5031
1224
-
+
Height
i=68
- ns=1;i=5027
+ ns=1;i=5031
1024
-
+
FrameRate
i=68
- ns=1;i=5027
+ ns=1;i=5031
15.0
-
+
PickFrames
JPEG still for the frame each pick decision was computed from.
ns=2;i=1009
- ns=1;i=5026
- ns=1;i=5043
- ns=1;i=5044
- ns=1;i=5045
- ns=1;i=5046
+ ns=1;i=5030
ns=1;i=5047
ns=1;i=5048
- ns=1;i=5049
+ ns=1;i=5049
ns=1;i=5050
ns=1;i=5051
ns=1;i=5052
- ns=1;i=5053
- ns=1;i=5054
- ns=1;i=5055
+ ns=1;i=5053
+ ns=1;i=5054
+ ns=1;i=5055
+ ns=1;i=5056
+ ns=1;i=5057
+ ns=1;i=5058
+ ns=1;i=5059
-
+
EndpointId
i=68
- ns=1;i=5042
+ ns=1;i=5046
clip-jpeg-pick
-
+
EndpointUri
i=68
- ns=1;i=5042
+ ns=1;i=5046
https://192.0.2.41/clips/{resultId}.jpg
-
+
ClipFormat
VisionClipFormatEnum.Jpeg
i=68
- ns=1;i=5042
+ ns=1;i=5046
0
-
+
State
VisionEndpointStateEnum.Ready
i=68
- ns=1;i=5042
+ ns=1;i=5046
1
-
+
Authentication
VisionEndpointAuthenticationEnum.Token
i=68
- ns=1;i=5042
+ ns=1;i=5046
3
-
+
SecureTransport
Derived from the endpoint scheme; see base specification §12.2.
i=68
- ns=1;i=5042
+ ns=1;i=5046
true
-
+
JpegDataChannelSource
Stands in for the Server-created data channel source. On a Server implementing the OPC UA - Data Channels draft this Object implements IDataChannelSourceType; this overlay does not reference that draft's provisional NodeIds, so it loads unchanged on a Server without it.
i=58
- ns=1;i=5042
+ ns=1;i=5046
-
+
DataChannelSource
The Object on which a client opens the data channel (§6.7).
i=68
- ns=1;i=5042
+ ns=1;i=5046
- ns=1;i=5049
+ ns=1;i=5053
-
+
DataChannelContentType
IANA media type carried on the data channel (§6.7).
i=68
- ns=1;i=5042
+ ns=1;i=5046
image/jpeg
-
+
InlineDeliveryEnabled
i=68
- ns=1;i=5042
+ ns=1;i=5046
false
-
+
MaxInlineClipSize
i=68
- ns=1;i=5042
+ ns=1;i=5046
0
-
+
LatestClip
Most recent clip, published inline within MaxInlineClipSize. Subscribable; see §6.4 rules 3 to 5 for the overflow, correlation and initial-state behaviour.
i=63
- ns=1;i=5042
+ ns=1;i=5046
-
+
LatestClipMetadata
Descriptor for LatestClip, carrying the Uri that remains valid when the inline payload does not fit, and the Timestamp and Digest that correlate the two (§6.4 rule 4).
i=63
- ns=1;i=5042
+ ns=1;i=5046
-
+
GetStreamEndpoint
GetStreamEndpoint as declared by VisionMediaManagementType.
- ns=1;i=5024
+ ns=1;i=5028
-
+
ReleaseStreamEndpoint
ReleaseStreamEndpoint as declared by VisionMediaManagementType.
- ns=1;i=5024
+ ns=1;i=5028
-
+
GetClip
GetClip as declared by VisionMediaManagementType.
- ns=1;i=5024
+ ns=1;i=5028
-
+
Optics
ns=2;i=1005
- ns=1;i=5007
- ns=1;i=5060
- ns=1;i=5061
- ns=1;i=5062
- ns=1;i=5063
+ ns=1;i=5011
ns=1;i=5064
ns=1;i=5065
+ ns=1;i=5066
+ ns=1;i=5067
+ ns=1;i=5068
+ ns=1;i=5069
-
+
FocalLength
i=68
- ns=1;i=5059
+ ns=1;i=5063
12.0
-
+
Aperture
i=68
- ns=1;i=5059
+ ns=1;i=5063
4.0
-
+
WorkingDistance
i=68
- ns=1;i=5059
+ ns=1;i=5063
0.85
-
+
OpticalFormat
i=68
- ns=1;i=5059
+ ns=1;i=5063
2/3"
-
+
MountType
i=68
- ns=1;i=5059
+ ns=1;i=5063
C
-
+
LensType
i=68
- ns=1;i=5059
+ ns=1;i=5063
Entocentric
-
+
WorldFrame
Cell world frame; the root of the frame tree.
ns=2;i=1011
- ns=1;i=5005
- ns=1;i=5067
- ns=1;i=5068
- ns=1;i=5078
+ ns=1;i=5004
+ ns=1;i=5071
+ ns=1;i=5072
+ ns=1;i=5085
-
+
FrameId
i=68
- ns=1;i=5066
+ ns=1;i=5070
world
-
+
Role
VisionFrameRoleEnum.World
i=68
- ns=1;i=5066
+ ns=1;i=5070
0
-
+
RobotBaseFrame
Robot base frame, fixed in the cell.
ns=2;i=1011
- ns=1;i=5005
- ns=1;i=5070
- ns=1;i=5071
- ns=1;i=5079
+ ns=1;i=5004
+ ns=1;i=5074
+ ns=1;i=5075
+ ns=1;i=5086
-
+
FrameId
i=68
- ns=1;i=5069
+ ns=1;i=5073
robot_base
-
+
Role
VisionFrameRoleEnum.Base
i=68
- ns=1;i=5069
+ ns=1;i=5073
1
-
+
FlangeFrame
- Robot flange / tool centre point frame; the camera moves with it.
+ Robot mechanical interface — the flange the camera and the gripper are both bolted to. This is what the hand-eye calibration resolves to, and it is deliberately NOT the tool centre point.
ns=2;i=1011
- ns=1;i=5005
- ns=1;i=5073
- ns=1;i=5074
- ns=1;i=5080
- ns=1;i=5007
+ ns=1;i=5004
+ ns=1;i=5077
+ ns=1;i=5078
+ ns=1;i=5087
+ ns=1;i=5011
-
+
FrameId
i=68
- ns=1;i=5072
+ ns=1;i=5076
flange
-
+
Role
- VisionFrameRoleEnum.Tool
+ VisionFrameRoleEnum.MechanicalInterface
i=68
- ns=1;i=5072
+ ns=1;i=5076
2
-
+
+ GripperTcpFrame
+ Tool centre point of the gripper. A pick pose has to reach THIS frame, not the flange, so the example carries the offset between them explicitly rather than leaving a consumer to assume it.
+
+ ns=2;i=1011
+ ns=1;i=5004
+ ns=1;i=5080
+ ns=1;i=5081
+ ns=1;i=5088
+
+
+
+ FrameId
+
+ i=68
+ ns=1;i=5079
+
+ gripper_tcp
+
+
+ Role
+ VisionFrameRoleEnum.Tool
+
+ i=68
+ ns=1;i=5079
+
+ 3
+
+
CameraFrame
Optical frame of the eye-in-hand camera; detection poses are expressed here.
ns=2;i=1011
- ns=1;i=5005
- ns=1;i=5076
- ns=1;i=5077
- ns=1;i=5081
+ ns=1;i=5004
+ ns=1;i=5083
+ ns=1;i=5084
+ ns=1;i=5089
-
+
FrameId
i=68
- ns=1;i=5075
+ ns=1;i=5082
camera_eih
-
+
Role
VisionFrameRoleEnum.Camera
i=68
- ns=1;i=5075
+ ns=1;i=5082
- 3
+ 6
-
+
ParentFrame
i=68
- ns=1;i=5066
+ ns=1;i=5070
i=0
-
+
+ ParentFrame
+
+ i=68
+ ns=1;i=5073
+
+ ns=1;i=5070
+
+
ParentFrame
i=68
- ns=1;i=5069
+ ns=1;i=5076
- ns=1;i=5066
+ ns=1;i=5073
-
+
ParentFrame
i=68
- ns=1;i=5072
+ ns=1;i=5079
- ns=1;i=5069
+ ns=1;i=5076
-
+
ParentFrame
i=68
- ns=1;i=5075
+ ns=1;i=5082
- ns=1;i=5072
+ ns=1;i=5076
-
+
Calibrations
i=61
- ns=1;i=5007
- ns=1;i=5083
- ns=1;i=5090
+ ns=1;i=5011
+ ns=1;i=5091
+ ns=1;i=5098
-
+
Intrinsics2448x2048
Pinhole intrinsics with Brown-Conrady distortion at full resolution.
ns=2;i=1013
- ns=1;i=5082
- ns=1;i=5084
- ns=1;i=5085
- ns=1;i=5086
- ns=1;i=5087
- ns=1;i=5088
- ns=1;i=5007
- ns=1;i=5089
+ ns=1;i=5090
+ ns=1;i=5092
+ ns=1;i=5093
+ ns=1;i=5094
+ ns=1;i=5095
+ ns=1;i=5096
+ ns=1;i=5011
+ ns=1;i=5097
-
+
CalibrationId
i=68
- ns=1;i=5083
+ ns=1;i=5091
intr-cam-eih-01-2448
-
+
PerformedAt
i=68
- ns=1;i=5083
+ ns=1;i=5091
2026-06-14T09:12:00Z
-
+
Valid
i=68
- ns=1;i=5083
+ ns=1;i=5091
true
-
+
ResidualError
i=68
- ns=1;i=5083
+ ns=1;i=5091
0.21
-
+
Method
i=68
- ns=1;i=5083
+ ns=1;i=5091
Zhang
-
+
Intrinsics
Intrinsic parameters. The field values are tabulated in the addendum's calibration clause.
i=63
- ns=1;i=5083
+ ns=1;i=5091
-
+
HandEye
- Transform from the camera frame to the robot flange frame. Eye-in-hand: the camera moves with the tool.
+ Transform from the camera frame to the robot mechanical interface. Eye-in-hand: the camera moves with the flange, so a pick pose is obtained by composing camera → flange → tool centre point.
ns=2;i=1014
- ns=1;i=5082
- ns=1;i=5091
- ns=1;i=5092
- ns=1;i=5093
- ns=1;i=5094
- ns=1;i=5095
- ns=1;i=5007
- ns=1;i=5096
- ns=1;i=5097
- ns=1;i=5098
- ns=1;i=5099
+ ns=1;i=5090
+ ns=1;i=5099
+ ns=1;i=5100
+ ns=1;i=5101
+ ns=1;i=5102
+ ns=1;i=5103
+ ns=1;i=5011
+ ns=1;i=5104
+ ns=1;i=5105
+ ns=1;i=5106
+ ns=1;i=5107
-
+
CalibrationId
i=68
- ns=1;i=5090
+ ns=1;i=5098
hand-eye-cam-eih-01
-
+
PerformedAt
i=68
- ns=1;i=5090
+ ns=1;i=5098
2026-06-14T10:40:00Z
-
+
Valid
i=68
- ns=1;i=5090
+ ns=1;i=5098
true
-
+
ResidualError
i=68
- ns=1;i=5090
+ ns=1;i=5098
0.0008
-
+
Method