diff --git a/README.md b/README.md
index 19eca65f..2f9d46d2 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 e2432ec3..92dc8231 100644
--- a/metaverse-specs/README.md
+++ b/metaverse-specs/README.md
@@ -22,16 +22,18 @@ 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 two standalone models
+## The three standalone models
-| | [`vision/`](vision/) | [`robot-intent/`](robot-intent/) |
-|---|---|---|
-| Question | *What does this camera see, what AI reads it, and what did it conclude?* | *How do I tell this robot what to do?* |
-| 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** |
-| Namespace | `http://opcfoundation.org/UA/Vision/` | `http://opcfoundation.org/UA/RobotIntent/` |
-| Release | 0.1.0 | 0.1.0 |
+| | [`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.
-Both are self-contained on base OPC UA — neither takes a companion specification as a `RequiredModel` — and both express interop with OPC 40010-1 as an optional annex rather than a dependency.
+**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
@@ -39,12 +41,14 @@ Both are self-contained on base OPC UA — neither takes a companion specificati
- *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).*
- `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).
- `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 00000000..10960058
--- /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 00000000..b1483011
--- /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 00000000..0e38f27f
--- /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 00000000..15f4d9b2
--- /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 00000000..6b671147
--- /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 00000000..dd1e5c4c
--- /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 00000000..3f5a5dfe
--- /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 00000000..8591500a
--- /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 00000000..8943e188
--- /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 00000000..6898b04d
--- /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 00000000..a600b85a
--- /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 00000000..c87fd732
--- /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 00000000..6022da81
--- /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 00000000..6071bbba
--- /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 00000000..810f1633
--- /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 00000000..e425df12
--- /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 00000000..6b17ad85
--- /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 00000000..be677d7c
--- /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 00000000..8285c587
--- /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 00000000..6bd177ec
--- /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 00000000..dc0a42e9
--- /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/vision/examples/machine-vision/Inspection.Vision.json b/metaverse-specs/extras/vision/examples/machine-vision/Inspection.Vision.json
index f79f0609..080adc99 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 c2f1bd8f..aa736407 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 1c3324b5..625397a9 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 e26467f7..b573d4c2 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}"
@@ -424,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 "
@@ -469,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.",
@@ -489,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)
@@ -517,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+)
@@ -629,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+)
@@ -650,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.")
@@ -1029,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 "
@@ -1124,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,
@@ -1241,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,
@@ -1270,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,
@@ -1339,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. "
@@ -1354,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 =================================
diff --git a/metaverse-specs/extras/vision/tools/model-reference.md b/metaverse-specs/extras/vision/tools/model-reference.md
index b802951f..3b76df17 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 2907efc8..1619a213 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
index 3ca03a9b..42d04d1b 100644
--- a/metaverse-specs/robot-intent/CHANGELOG.md
+++ b/metaverse-specs/robot-intent/CHANGELOG.md
@@ -4,6 +4,12 @@ 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.
@@ -55,6 +61,27 @@ makes an existing claim true; none adds capability, and no previously assigned N
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.
diff --git a/metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md b/metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md
index d5057cea..cd05fb2a 100644
--- a/metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md
+++ b/metaverse-specs/robot-intent/OPC-UA-Robot-Intent.md
@@ -89,6 +89,9 @@ Two are worth naming here because a Server may resolve references into them, and
| **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. |
---
@@ -850,11 +853,52 @@ Requirements are of two kinds. **Structural** requirements are settled by readin
| **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
@@ -961,3 +1005,23 @@ These are **not** normative references and impose no dependency. They are record
- **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/validate_all.py b/metaverse-specs/validate_all.py
index 9c13e893..96971feb 100644
--- a/metaverse-specs/validate_all.py
+++ b/metaverse-specs/validate_all.py
@@ -23,6 +23,8 @@
# 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
@@ -30,6 +32,10 @@
# 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 d0678b26..edd70ff9 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 baf2da1d..5a4eae03 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 aa3bbb6c..cbe1e351 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.
@@ -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 2a11e4e5..3f04edc7 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 f1ed44e1..c3873120 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 dc5e3075..a03ea2e2 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 19ca54a7..ca615899 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
i=68
- ns=1;i=5090
+ ns=1;i=5098
Daniilidis
-
+
Mount
VisionCalibrationMountEnum.EyeInHand
i=68
- ns=1;i=5090
+ ns=1;i=5098
0
-
+
SourceFrame
i=68
- ns=1;i=5090
+ ns=1;i=5098
- ns=1;i=5075
+ ns=1;i=5082
-
+
TargetFrame
i=68
- ns=1;i=5090
+ ns=1;i=5098
- ns=1;i=5072
+ ns=1;i=5076
-
+
Transform
Pose of SourceFrame expressed in TargetFrame. The field values are tabulated in the addendum's calibration clause.
i=63
- ns=1;i=5090
+ ns=1;i=5098
-
+
GraspPoseNet
6-DoF grasp pose estimation over a bin of mixed parts.
- ns=2;i=1015
- ns=1;i=5004
- ns=1;i=5101
- ns=1;i=5102
- 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=3;i=1002
+ ns=1;i=5007
ns=1;i=5109
- ns=1;i=5110
+ ns=1;i=5110
+ ns=1;i=5111
+ ns=1;i=5112
+ ns=1;i=5113
+ ns=1;i=5114
+ ns=1;i=5115
+ ns=1;i=5116
+ ns=1;i=5117
+ ns=1;i=5118
-
+
ModelId
i=68
- ns=1;i=5100
+ ns=1;i=5108
graspposenet
-
+
Name
i=68
- ns=1;i=5100
+ ns=1;i=5108
GraspPoseNet
-
+
Version
i=68
- ns=1;i=5100
+ ns=1;i=5108
3.2.0
-
+
Framework
i=68
- ns=1;i=5100
+ ns=1;i=5108
PyTorch
-
+
Format
i=68
- ns=1;i=5100
+ ns=1;i=5108
TensorRT
-
+
TaskKind
i=68
- ns=1;i=5100
+ ns=1;i=5108
PoseEstimation
-
+
ArtifactUri
i=68
- ns=1;i=5100
+ ns=1;i=5108
https://models.example.com/graspposenet/3.2.0.plan
-
+
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=5100
+ ns=1;i=5108
a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0fedcba9876543210
-
+
DigestAlgorithm
i=68
- ns=1;i=5100
+ ns=1;i=5108
SHA-256
-
+
EdgeGpuDeployment
Inference on a cell-side GPU appliance, off the OPC UA Server.
- ns=2;i=1017
- ns=1;i=5004
- ns=1;i=5111
- ns=1;i=5112
- ns=1;i=5113
- ns=1;i=5114
- ns=1;i=5115
- ns=1;i=5100
+ ns=3;i=1004
+ ns=1;i=5007
+ ns=1;i=5119
+ ns=1;i=5120
+ ns=1;i=5121
+ ns=1;i=5122
+ ns=1;i=5123
+ ns=1;i=5108
-
+
DeploymentId
i=68
- ns=1;i=5110
+ ns=1;i=5118
deploy-edge-gpu-01
-
+
InferenceLocation
- VisionInferenceLocationEnum.EdgeOffServer
i=68
- ns=1;i=5110
+ ns=1;i=5118
1
-
+
AcceleratorKind
- VisionAcceleratorKindEnum.Gpu
i=68
- ns=1;i=5110
+ ns=1;i=5118
1
-
+
AcceleratorName
i=68
- ns=1;i=5110
+ ns=1;i=5118
Example RTX A2000
-
+
EndpointUri
i=68
- ns=1;i=5110
+ ns=1;i=5118
grpcs://192.0.2.60:8001/graspposenet
-
+
BinPickingPipeline
Runs on every acquired frame while the cell is picking.
ns=2;i=1018
ns=1;i=5003
- ns=1;i=5117
- ns=1;i=5118
- ns=1;i=5119
- ns=1;i=5120
- ns=1;i=5121
- ns=1;i=5122
- ns=1;i=5123
+ ns=1;i=5125
+ ns=1;i=5126
+ ns=1;i=5127
+ ns=1;i=5128
+ ns=1;i=5129
+ ns=1;i=5130
+ ns=1;i=5131
-
+
PipelineId
i=68
- ns=1;i=5116
+ ns=1;i=5124
pipe-binpick-01
-
+
Sensor
i=68
- ns=1;i=5116
+ ns=1;i=5124
- ns=1;i=5007
+ ns=1;i=5011
-
+
Deployment
i=68
- ns=1;i=5116
+ ns=1;i=5124
- ns=1;i=5110
+ ns=1;i=5118
-
+
State
VisionEndpointStateEnum.Active
i=68
- ns=1;i=5116
+ ns=1;i=5124
2
-
+
Continuous
i=68
- ns=1;i=5116
+ ns=1;i=5124
true
-
+
Results
i=61
- ns=1;i=5116
+ ns=1;i=5124
-
+
Feedback
Feedback surface for corrections and overlays.
ns=2;i=1024
- ns=1;i=5116
+ ns=1;i=5124
-
+
BinPickingCameraTwin
Synthetic twin of the eye-in-hand camera, rendered by Isaac Sim from the cell stage.
ns=2;i=1003
ns=1;i=5002
ns=2;i=1030
- ns=1;i=5125
- ns=1;i=5126
- ns=1;i=5127
- ns=1;i=5128
- ns=1;i=5129
- ns=1;i=5130
- ns=1;i=5131
- ns=1;i=5132
ns=1;i=5133
ns=1;i=5134
ns=1;i=5135
@@ -1182,454 +1245,460 @@
ns=1;i=5138
ns=1;i=5139
ns=1;i=5140
- ns=1;i=5141
+ ns=1;i=5141
+ ns=1;i=5142
+ ns=1;i=5143
+ ns=1;i=5144
+ ns=1;i=5145
+ ns=1;i=5146
+ ns=1;i=5147
+ ns=1;i=5148
+ ns=1;i=5149
-
+
SensorId