From f3b5af51028cffe591b197967a2304dde80e3139 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 3 Aug 2026 11:42:14 +0200 Subject: [PATCH 1/4] Let a Server actually use a model, not just describe one The model said what a model IS and never let anyone use one. EndpointUri was a bare String: no wire contract, no credential, no capabilities, no failure semantics. A Server holding one knew where to send bytes and nothing about what shape they should take or what to do when the far end stopped answering - so "consume a model hosted elsewhere", which is the whole point of the InferenceLocation property, was unimplementable from what this model told it. Three additions. Clause 7 defines one invocation surface whose signature does NOT change with InferenceLocation. That 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 addressing and auth. The payload stays opaque - it is domain vocabulary - and what gets fixed is the envelope, where ModelUsed reports the model that ACTUALLY answered. A fallback or a followed reference moves it between the call and the read, so the deployment's current model is the wrong answer to "what produced this result", and wrong in the direction that looks plausible. Clause 8 makes remote consumption real: dialect, auth kind, a credential REFERENCE that is never a secret, capabilities, reachability, and the question no inference API answers because it can assume its caller will wait - what the line does when the WAN drops. FallbackPolicy is Mandatory. So is DataJurisdiction and EgressPermitted, because encryption answers who can read data in flight and not where it went. Clause 9 makes the catalogue an xRegistry domain extension rather than a private invention. Two things then fall out of the base type instead of being designed: ResourceType IS a FileType, so a staged artefact streams through the inherited Open/Read/Close; and it already carries ExternalReference, so a federated entry whose bytes live elsewhere is expressible without pretending to hold them. The import job federates by default and stages when the target could not otherwise reach the source, and verifies the digest at staging - the one moment a substituted artefact would enter. Jobs now derive from the Part 10 ProgramStateMachineType through a shared AiJobType, as Robot Intent does, so the transition events and auditability are inherited rather than reinvented. Costs, stated plainly rather than buried: this model now requires OPC UA - xRegistry, which spends the "exactly one RequiredModel" claim and moves the own namespace from ns=1 to ns=2. Both validators hardcoded ns=1 while reading it - Vision's would have resolved nothing and passed. Both now derive the index from NamespaceUris. No NodeId moved. JobId and LastError moved up to AiJobType and their ids are reserved, not reclaimed: 0 removed in place, 141 added, 0 moved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --- .../ai-deployment/OPC-UA-AI-Deployment.md | 367 +++- .../Opc.Ua.AiDeployment.NodeIds.csv | 143 +- .../Opc.Ua.AiDeployment.NodeSet2.xml | 1749 +++++++++++++++-- .../extras/ai-deployment/tools/build_model.py | 699 ++++++- .../ai-deployment/tools/model-reference.md | 479 ++++- .../ai-deployment/tools/validate_local.py | 86 +- .../extras/vision/tools/validate_local.py | 28 +- .../Opc.Ua.Inspection.Vision.NodeSet2.xml | 2 +- .../Opc.Ua.Robotics.Vision.NodeSet2.xml | 2 +- 9 files changed, 3260 insertions(+), 295 deletions(-) diff --git a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md index 971cbc1..fcf4733 100644 --- a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md +++ b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md @@ -1,6 +1,6 @@ # OPC UA — AI Deployment and Learning -> Status: Working-group draft (Release 0.1.0). This document, together with `Opc.Ua.AiDeployment.NodeSet2.xml` and `Opc.Ua.AiDeployment.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. +> Status: Working-group draft (Release 0.2.0). This document, together with `Opc.Ua.AiDeployment.NodeSet2.xml` and `Opc.Ua.AiDeployment.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. > @@ -15,6 +15,10 @@ This specification defines an OPC UA information model that lets a Server descri - **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 7); +- **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 8); +- **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 9); +- **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 10); - **how a model is replaced** — the capture, label, train and promote loop, and who is allowed to complete it. ### 1.1 Motivation @@ -34,20 +38,28 @@ Consuming specifications join to this one through a plain `NodeId`, not a `Requi ### 1.3 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 inference **invocation**. A consuming specification owns that, 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. This model describes the deployment; the caller describes the call. +- It does **not** define what an inference payload *contains*. Clause 7 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; clause 6 is explicit that a Server may implement only the capture stages. - It does **not** define model training. `TriggerTraining` requests it; where the training happens is out of scope, and clause 6 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.4 Capabilities and versioning -Release 0.1.0 covers models, datasets, deployments 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, and a consuming specification binds to it without taking a NodeSet dependency (§4.2). +Release 0.2.0 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 9 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. The base UA namespace is the only required model. -- **OPC 10000-6** — Mappings. Structure encoding of `TensorSignatureDataType`. +- **OPC 10000-3, -4, -5** — Address Space Model, Services, Information Model. +- **OPC 10000-6** — Mappings. Structure encoding of the DataTypes in §5.10. +- **OPC 10000-10** — Programs. `ProgramStateMachineType` is the base type of `AiJobType` (§5.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` (§9.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 9). 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. @@ -69,18 +81,31 @@ Informative alignments — IDTA 02058, IDTA 02059, IDTA 02060, and the OPC UA ## 4 Overview and concepts -### 4.1 The four objects and what joins them +### 4.1 The objects and what joins them ```mermaid flowchart LR + CAT["ModelRegistryType
catalogue"] -->|ModelImportJobType| M D["DatasetType"] -->|TrainedOn| M["ModelType"] M -->|UsesModel| P["DeploymentType"] - J["LearningJobType"] -.->|BaseModel| M - J -.->|CandidateModel| M2["ModelType (candidate)"] + P -->|Source| S["ModelSourceType
somewhere else"] + P -.->|FallsBackTo| P2["DeploymentType
fallback"] + C["Client"] -->|Invoke| P + J["LearningJobType"] -.->|CandidateModel| M J -.->|Dataset| D - M2 -.->|PromoteModel| 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`, §11.1 | +| How do I run it? | `DeploymentType.Invoke` (§7) | +| What if it is not here, or stops answering? | `ModelSourceType`, `FallbackPolicy` (§8) | +| How did it get here, and may it be used? | `ModelImportJobType` (§9), `ModelCardType` (§10) | + 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. @@ -89,13 +114,13 @@ A dataset trains a model; a deployment executes one. A learning job accumulates 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 §7 is only available where both are implemented, which is why it is stated as a conformance condition rather than assumed. +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 §11 is only available where both are implemented, which is why it is stated as a conformance condition rather than assumed. ### 4.3 A model is a business artefact, not device firmware 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 (§7.3). +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 (§11.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. @@ -107,11 +132,37 @@ The consequence for a reader: every member of `ModelType` is about *this* artefa | Type | NodeId | Subtype of | |---|---|---| -| `AiRootType` | `ns=1;i=1001` | `BaseObjectType` | -| `ModelType` | `ns=1;i=1002` | `BaseObjectType` | -| `DatasetType` | `ns=1;i=1003` | `BaseObjectType` | -| `DeploymentType` | `ns=1;i=1004` | `BaseObjectType` | -| `LearningJobType` | `ns=1;i=1005` | `BaseObjectType` | +| `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. + +### 5.6 `AiJobType` and the three jobs + +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. §11.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. @@ -119,37 +170,37 @@ Annex A is the authoritative node reference and carries every member with its Da Identity, provenance and interface of a trained model. Aligned with IDTA 02060. -`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 §7 depends on them, and a rule that depends on an Optional member is a rule a conformant Server can silently not satisfy. +`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 11 depends on them, and a rule that depends on an Optional member is a rule a conformant Server can silently not satisfy. `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. -`Inputs` and `Outputs` carry `TensorSignatureDataType` (`ns=1;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. +`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. ### 5.3 `DatasetType` What a model was trained or validated on. Aligned with IDTA 02058. -`SourceKind` (`DatasetSourceEnum`, `ns=1;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. +`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. ### 5.4 `DeploymentType` A model made executable. Aligned with IDTA 02059. -`InferenceLocation` (`InferenceLocationEnum`, `ns=1;i=3001`) is `OnServer` 0, `EdgeOffServer` 1, `Cloud` 2 or `InSimulator` 3, and is **Mandatory**. +`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=1;i=3002`) is `Cpu`, `Gpu`, `Npu`, `Fpga`, `Tpu` or `Other`. `State` (`DeploymentStateEnum`, `ns=1;i=3003`) is `Inactive` 0, `Ready` 1, `Active` 2, `Degraded` 3 or `Faulted` 4, and is **Mandatory** because §6 and any consuming specification's availability logic depend on it. +`AcceleratorKind` (`AcceleratorKindEnum`, `ns=2;i=3002`) is `Cpu`, `Gpu`, `Npu`, `Fpga`, `Tpu` or `Other`. `State` (`DeploymentStateEnum`, `ns=2;i=3003`) is `Inactive` 0, `Ready` 1, `Active` 2, `Degraded` 3 or `Faulted` 4, and is **Mandatory** because §6 and any consuming specification's availability logic depend on it. -`EndpointUri` is meaningful when `InferenceLocation` is not `OnServer`. It is **untrusted input** and subject to §7.2. +`EndpointUri` is meaningful when `InferenceLocation` is not `OnServer`. It is **untrusted input** and subject to §11.2. ### 5.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 §7.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. +This is the only defined path from a running deployment to the artefact its results depend on, and §11.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. @@ -173,19 +224,254 @@ stateDiagram-v2 Failed --> Idle ``` -`LearningJobStateEnum` (`ns=1;i=3005`) carries exactly these eight states. +`LearningJobStateEnum` (`ns=2;i=3005`) carries exactly these eight states. **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 (§7.3). +**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 (§11.3). + +--- + +## 7 Inference (normative) + +### 7.1 One call, wherever the model runs + +`DeploymentType.Invoke` runs inference and returns the result. + +**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 8's subject. + +### 7.2 The payload is opaque, the envelope is not + +`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 | + +#### 7.2.1 `ModelUsed` is the one a client must read + +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 (§8.4) answers from a different deployment entirely, and a `FollowsRef` binding (§8.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 §11.1 therefore walks `ModelUsed`, not the deployment. + +#### 7.2.2 A truncated answer is not a complete one + +`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. + +#### 7.2.3 Accounting is not 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. + +### 7.3 Parameters, and why an ignored one is worse than a rejected one + +`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. + +### 7.4 Capabilities are asked, not assumed + +`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. + +### 7.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 (§12.2) and it is entirely optional. A Server that implements neither answers only through `Invoke`, and is fully conformant. + +### 7.6 Work that does not finish while the caller waits + +`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` (§5.6), so it is observed exactly like every other long-running operation here. + +`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. + +--- + +## 8 Consuming a model hosted elsewhere (normative) + +### 8.1 What a URI does not tell you + +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 | + +### 8.2 The wire contract, and the credential that is never a secret + +`ApiDialect` (`ApiDialectEnum`, `ns=2;i=3007`) is `OpcUaInference`, `OpenAiCompatible`, `OpenInferenceProtocol`, `TensorRemoteProcedure`, `EmbeddedRuntime` or `Proprietary`. + +These name **the contract the remote endpoint speaks**. They never affect how an OPC UA client calls this Server, which is always §7. `OpcUaInference` is another Server implementing this specification; `OpenAiCompatible` is the de-facto chat and embeddings contract that most serving runtimes expose, including ones that run on a single workstation; `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. + +`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. + +**`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. + +### 8.3 Pinned, or following something that moves + +`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 (§6) 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 §11.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. + +### 8.4 When the far end stops answering + +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. + +`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". + +`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. + +### 8.5 Where the data goes + +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. + +--- + +## 9 The catalogue and the bridge (normative) + +### 9.1 A model catalogue is a registry + +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 (§9.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: a `ModelRegistryType` holds `ModelPublisherType` groups and nothing else, and a `ModelPublisherType` holds `ModelResourceType` and `DatasetResourceType` resources and nothing else. A subtype that inherited the placeholders unchanged would add metadata while restricting nothing, and a client could not tell one kind of registry from another except by convention. + +`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 §8.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. + +### 9.2 The bridge + +`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. + +`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. + +### 9.3 Federate or stage + +`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. + +### 9.4 Staging is where the digest matters + +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 §11.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**. + +--- + +## 10 Governance and provenance (normative) + +### 10.1 The nameplate does not say whether it may be used + +`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 lists only what a model can do is marketing, and the failure modes are the half a commissioning engineer actually needs. `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. + +### 10.2 A metric without its threshold 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. + +`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. + +### 10.3 Lineage is a chain + +`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. + +### 10.4 Safety findings + +`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. --- -## 7 Security +## 11 Security -### 7.1 Provenance is the point of the digest +### 11.1 Provenance is the point of the digest A published result is traceable to the artefact that produced it by: result → deployment (the consuming specification's `NodeId` Property) → `UsesModel` → `ModelType` → `Digest`. @@ -193,43 +479,50 @@ Every link is required for the chain to hold, which is why `UsesModel` is exactl `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. -### 7.2 URIs are untrusted input +### 11.2 URIs are 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. -### 7.3 Promotion needs its own authorization +### 11.3 Promotion needs its own authorization 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. -### 7.4 A digest is not a signature +### 11.4 A digest is not a signature `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. --- -## 8 Profiles and conformance units +## 12 Profiles and conformance units -### 8.1 Declaring conformance +### 12.1 Declaring conformance A Server declares conformance by exposing `AiRootType` under the Server object with `SpecificationVersion` set to the release it implements. -### 8.2 Facets +### 12.2 Facets | Facet | Requires | |---|---| -| **AI-Base** (mandatory) | `AiRootType` with `Models` and `Deployments`; at least one `ModelType` with `ModelId`, `Name`, `Version`, `Digest` and `DigestAlgorithm`; at least one `DeploymentType` with `DeploymentId`, `InferenceLocation` and `State`; the exactly-one `UsesModel` rule of §5.5; the digest rules of §7.1 | +| **AI-Base** (mandatory) | `AiRootType` with `Models` and `Deployments`; at least one `ModelType` with `ModelId`, `Name`, `Version`, `Digest` and `DigestAlgorithm`; at least one `DeploymentType` with `DeploymentId`, `InferenceLocation` and `State`; the exactly-one `UsesModel` rule of §5.5; the digest rules of §11.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`, with `EndpointUri` naming an authenticated, confidential scheme (§7.2) | +| **AI-OffServer** | A deployment whose `InferenceLocation` is not `OnServer`, with `EndpointUri` naming an authenticated, confidential scheme (§11.2) | | **AI-Signatures** | `Inputs` and `Outputs` populated on every model | -| **AI-Learning** | `LearningJobType`, the §6 state model, every Method that drives a transition in it, and the distinct `PromoteModel` authorization of §7.3 | +| **AI-Learning** | `LearningJobType`, the §6 state model, every Method that drives a transition in it, and the distinct `PromoteModel` authorization of §11.3 | +| **AI-Invoke** | `DeploymentType.Invoke` with `ModelUsed`, `Usage` and `FinishReason` populated on every response, and the §7.3 rule that an unsupported parameter is rejected rather than ignored | +| **AI-InvokeAsync** | `InvokeAsync` and `InferenceJobType`, answering the same questions as `Invoke` (§7.6) | +| **AI-Stream** | Incremental results published over a data channel (§7.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 §8.2; `FallbackPolicy` on every deployment and the acyclicity rule of §8.4 | +| **AI-Residency** | `DataJurisdiction`, `EgressPermitted` and `RetainsInput` on every deployment, with the §8.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 §9.1 requires | +| **AI-Import** | `ModelImportJobType`, the federate/stage/auto modes of §9.3, and the digest verification of §9.4. Requires **AI-Catalogue** | --- -## 9 Deliverables and reproducibility +## 13 Deliverables and reproducibility | Artifact | Path | |---|---| diff --git a/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeIds.csv b/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeIds.csv index 0331245..065cd65 100644 --- a/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeIds.csv +++ b/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeIds.csv @@ -8,10 +8,42 @@ 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 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 @@ -52,13 +84,11 @@ DeploymentType_LatencyBudget,6033,Variable DeploymentType_BatchSize,6034,Variable DeploymentType_State,6035,Variable LearningJobType,1005,ObjectType -LearningJobType_JobId,6036,Variable LearningJobType_State,6037,Variable LearningJobType_Dataset,6038,Variable LearningJobType_BaseModel,6039,Variable LearningJobType_CandidateModel,6040,Variable LearningJobType_SamplesCollected,6041,Variable -LearningJobType_LastError,6042,Variable LearningJobType_StartCollection,6043,Method LearningJobType_StopCollection,6044,Method LearningJobType_TriggerTraining,6045,Method @@ -66,4 +96,113 @@ LearningJobType_TriggerTraining_OutputArguments,6046,Variable LearningJobType_PromoteModel,6047,Method LearningJobType_PromoteModel_InputArguments,6048,Variable LearningJobType_PromoteModel_OutputArguments,6049,Variable +AiJobType,1006,ObjectType +AiJobType_JobId,6050,Variable +AiJobType_LastError,6051,Variable +AiJobType_StartedAt,6052,Variable +AiJobType_FinishedAt,6053,Variable +AiJobType_Progress,6054,Variable +AiJobType_RequestedBy,6055,Variable +ModelImportJobType,1007,ObjectType +ModelImportJobType_Source,6056,Variable +ModelImportJobType_ModelReference,6057,Variable +ModelImportJobType_Mode,6058,Variable +ModelImportJobType_TargetDeployment,6059,Variable +ModelImportJobType_ImportedModel,6060,Variable +ModelImportJobType_BytesTransferred,6061,Variable +ModelImportJobType_DigestVerified,6062,Variable +ModelImportJobType_Cancel,6063,Method +InferenceJobType,1008,ObjectType +InferenceJobType_Deployment,6064,Variable +InferenceJobType_RequestPayload,6065,Variable +InferenceJobType_RequestContentType,6066,Variable +InferenceJobType_ResponsePayload,6067,Variable +InferenceJobType_ResponseContentType,6068,Variable +InferenceJobType_ModelUsed,6069,Variable +InferenceJobType_Usage,6070,Variable +InferenceJobType_FinishReason,6071,Variable +InferenceJobType_SafetyAssessment,6072,Variable +ModelSourceType,1009,ObjectType +ModelSourceType_SourceId,6073,Variable +ModelSourceType_EndpointUri,6074,Variable +ModelSourceType_ApiDialect,6075,Variable +ModelSourceType_EndpointDescriptionUri,6076,Variable +ModelSourceType_AuthenticationKind,6077,Variable +ModelSourceType_CredentialReference,6078,Variable +ModelSourceType_TokenAudience,6079,Variable +ModelSourceType_Reachability,6080,Variable +ModelSourceType_LastSuccessAt,6081,Variable +ModelSourceType_ConsecutiveFailures,6082,Variable +ModelSourceType_RateLimit,6083,Variable +ModelSourceType_Capabilities,6084,Variable +ModelSourceType_TestConnection,6085,Method +ModelSourceType_TestConnection_OutputArguments,6086,Variable +ModelSourceType_ListModels,6087,Method +ModelSourceType_ListModels_InputArguments,6088,Variable +ModelSourceType_ListModels_OutputArguments,6089,Variable +EvaluationRunType,1014,ObjectType +EvaluationRunType_RunId,6090,Variable +EvaluationRunType_EvaluatedModel,6091,Variable +EvaluationRunType_Dataset,6092,Variable +EvaluationRunType_CompletedAt,6093,Variable +EvaluationRunType_Metrics,6094,Variable +EvaluationRunType_Passed,6095,Variable +EvaluationRunType_ReportUri,6096,Variable +ModelCardType,1015,ObjectType +ModelCardType_IntendedUse,6097,Variable +ModelCardType_Limitations,6098,Variable +ModelCardType_OutOfScopeUse,6099,Variable +ModelCardType_License,6100,Variable +ModelCardType_TrainingDataCutoff,6101,Variable +ModelCardType_EthicalConsiderations,6102,Variable +ModelCardType_ContactUri,6103,Variable +ModelRegistryType,1010,ObjectType +ModelRegistryType_ModelPublisher,6104,Object +ModelPublisherType,1011,ObjectType +ModelPublisherType_Model,6105,Object +ModelPublisherType_Dataset,6106,Object +ModelResourceType,1012,ObjectType +ModelResourceType_TaskKind,6107,Variable +ModelResourceType_Framework,6108,Variable +ModelResourceType_Digest,6109,Variable +ModelResourceType_DigestAlgorithm,6110,Variable +ModelResourceType_SizeBytes,6111,Variable +ModelResourceType_Gated,6112,Variable +ModelResourceType_MutableRefs,6113,Variable +DatasetResourceType,1013,ObjectType +DatasetResourceType_SourceKind,6114,Variable +DatasetResourceType_SampleCount,6115,Variable +DatasetResourceType_Digest,6116,Variable +DatasetResourceType_DigestAlgorithm,6117,Variable +DatasetResourceType_SizeBytes,6118,Variable +AiRootType_Sources,6119,Object +AiRootType_Registries,6120,Object +AiRootType_Evaluations,6121,Object +AiRootType_Jobs,6122,Object +ModelType_Card,6123,Object +ModelType_Publisher,6124,Variable +ModelType_ParameterCount,6125,Variable +ModelType_Quantization,6126,Variable +ModelType_SafetyPolicyUri,6127,Variable +DeploymentType_Source,6128,Variable +DeploymentType_VersionBinding,6129,Variable +DeploymentType_BoundRef,6130,Variable +DeploymentType_FallbackPolicy,6131,Variable +DeploymentType_Reachability,6132,Variable +DeploymentType_ConsecutiveFailures,6133,Variable +DeploymentType_LastSuccessAt,6134,Variable +DeploymentType_RateLimit,6135,Variable +DeploymentType_Capabilities,6136,Variable +DeploymentType_DataJurisdiction,6137,Variable +DeploymentType_EgressPermitted,6138,Variable +DeploymentType_RetainsInput,6139,Variable +DeploymentType_EgressPolicyUri,6140,Variable +DeploymentType_Invoke,6141,Method +DeploymentType_Invoke_InputArguments,6142,Variable +DeploymentType_Invoke_OutputArguments,6143,Variable +DeploymentType_InvokeAsync,6144,Method +DeploymentType_InvokeAsync_InputArguments,6145,Variable +DeploymentType_InvokeAsync_OutputArguments,6146,Variable +DeploymentType_GetCapabilities,6147,Method +DeploymentType_GetCapabilities_OutputArguments,6148,Variable AiDeployment,7001,Object diff --git a/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml b/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml index 55691da..b7871cd 100644 --- a/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml +++ b/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml @@ -2,11 +2,13 @@ + http://opcfoundation.org/UA/xRegistry/ http://opcfoundation.org/UA/AI/ - + + @@ -18,6 +20,9 @@ i=12 i=14 i=15 + i=10 + i=8 + i=13 i=17 i=20 i=21 @@ -40,129 +45,389 @@ 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. AiDeployment DataTypes i=29 - ns=1;i=3901 + 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=1;i=3001 + ns=2;i=3001 OnServerEdgeOffServerCloudInSimulator - + AcceleratorKindEnum Compute device executing the model. AiDeployment DataTypes i=29 - ns=1;i=3902 + ns=2;i=3902 - + EnumStrings i=78 i=68 - ns=1;i=3002 + ns=2;i=3002 CpuGpuNpuFpgaTpuOther - + DeploymentStateEnum Runtime lifecycle state of a deployment. AiDeployment DataTypes i=29 - ns=1;i=3903 + 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=1;i=3003 + ns=2;i=3003 InactiveReadyActiveDegradedFaulted - + DatasetSourceEnum Provenance of the samples in a dataset. AiDeployment DataTypes i=29 - ns=1;i=3904 + 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=1;i=3004 + ns=2;i=3004 RealSyntheticMixed - + LearningJobStateEnum State of a dataset-capture, retraining and promotion cycle. AiDeployment DataTypes i=29 - ns=1;i=3905 + ns=2;i=3905 A candidate model is available for promotion. - + EnumStrings i=78 i=68 - ns=1;i=3005 + 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. + AiDeployment 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. + AiDeployment DataTypes + + i=29 + ns=2;i=3907 + + Another OPC UA Server implementing this specification's Invoke Method.The de-facto chat/embeddings REST contract that most serving runtimes, including on-device ones, expose.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 + + OpcUaInferenceOpenAiCompatibleOpenInferenceProtocolTensorRemoteProcedureEmbeddedRuntimeProprietary + + + 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. + AiDeployment 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. + AiDeployment 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. + AiDeployment 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 11 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. + AiDeployment 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. + AiDeployment 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. + AiDeployment 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 + + 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. AiDeployment DataTypes i=22 - ns=1;i=5001 + 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. + AiDeployment 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. + AiDeployment 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. + AiDeployment 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. + AiDeployment 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. + AiDeployment 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. + AiDeployment 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=1;i=3050 + ns=2;i=3056 - + UsesModel - Links a Deployment to the Model it executes. Clause 5.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 7 depends. + Links a Deployment to the Model it executes. Clause 5.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 11 depends. AiDeployment 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. AiDeployment ReferenceTypes @@ -171,537 +436,1619 @@ 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. + AiDeployment ReferenceTypes + IsBaseOfModel + + i=32 + + + + FallsBackTo + Links a Deployment to the Deployment that serves in its place when it cannot. Clause 8 forbids a cycle, and requires the response to say which deployment actually answered. + AiDeployment 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. + AiDeployment 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. + AiDeployment 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. AiDeployment i=58 - ns=1;i=6001 - ns=1;i=6002 - ns=1;i=6003 - ns=1;i=6004 - ns=1;i=6005 + ns=2;i=6001 + ns=2;i=6002 + ns=2;i=6003 + ns=2;i=6004 + ns=2;i=6005 + ns=2;i=6119 + ns=2;i=6120 + ns=2;i=6121 + ns=2;i=6122 - + Models ModelType instances. i=78 i=61 - ns=1;i=1001 + ns=2;i=1001 - + Datasets DatasetType instances. i=80 i=61 - ns=1;i=1001 + ns=2;i=1001 - + Deployments DeploymentType instances. i=78 i=61 - ns=1;i=1001 + ns=2;i=1001 - + LearningJobs LearningJobType instances. i=80 i=61 - ns=1;i=1001 + ns=2;i=1001 - + SpecificationVersion Release of this specification the Server implements, for example '0.1.0'. i=78 i=68 - ns=1;i=1001 + 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. AiDeployment i=58 - ns=1;i=6006 - ns=1;i=6007 - ns=1;i=6008 - ns=1;i=6009 - ns=1;i=6010 - ns=1;i=6011 - ns=1;i=6012 - ns=1;i=6013 - ns=1;i=6014 - ns=1;i=6015 - ns=1;i=6016 - ns=1;i=6017 - ns=1;i=6018 + ns=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=6123 + ns=2;i=6124 + ns=2;i=6125 + ns=2;i=6126 + ns=2;i=6127 - + ModelId Identifier of the model. i=78 i=68 - ns=1;i=1002 + ns=2;i=1002 - + Name Human-readable model name. i=78 i=68 - ns=1;i=1002 + ns=2;i=1002 - + Version Model version. i=78 i=68 - ns=1;i=1002 + ns=2;i=1002 - + Framework Producing framework, for example PyTorch, TensorFlow or scikit-learn. i=80 i=68 - ns=1;i=1002 + ns=2;i=1002 - + Format Serialization format, for example ONNX, TensorRT or OpenVINO IR. i=80 i=68 - ns=1;i=1002 + 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=1;i=1002 + ns=2;i=1002 - + Digest - Cryptographic digest of the model artefact, for provenance and integrity. Mandatory: clause 7 requires it for every model whose artefact is obtainable through ArtifactUri, and it is the terminus of the provenance chain that UsesModel keeps intact. + Cryptographic digest of the model artefact, for provenance and integrity. Mandatory: clause 11 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=1002 + 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 7. + 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 11. i=78 i=68 - ns=1;i=1002 + ns=2;i=1002 - + ArtifactUri Where the model artefact can be obtained. Treated as untrusted input. i=80 i=68 - ns=1;i=1002 + ns=2;i=1002 - + ProvenanceUri Training provenance or model card location. i=80 i=68 - ns=1;i=1002 + 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=1;i=1002 + ns=2;i=1002 - + Inputs Input tensor signatures. i=80 i=63 - ns=1;i=1002 + ns=2;i=1002 - + Outputs Output tensor signatures. i=80 i=63 - ns=1;i=1002 + 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. AiDeployment i=58 - ns=1;i=6019 - ns=1;i=6020 - ns=1;i=6021 - ns=1;i=6022 - ns=1;i=6023 - ns=1;i=6024 - ns=1;i=6025 - ns=1;i=6026 - ns=1;i=6027 + 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=1;i=1003 + ns=2;i=1003 - + Name Human-readable dataset name. i=80 i=68 - ns=1;i=1003 + ns=2;i=1003 - + Version Dataset version. i=80 i=68 - ns=1;i=1003 + ns=2;i=1003 - + SourceKind Whether samples are real, synthetic or mixed. i=78 i=68 - ns=1;i=1003 + ns=2;i=1003 - + SampleCount Number of samples. i=80 i=68 - ns=1;i=1003 + ns=2;i=1003 - + LabelClasses Class labels present. i=80 i=68 - ns=1;i=1003 + ns=2;i=1003 - + CreatedAt Creation time. i=80 i=68 - ns=1;i=1003 + ns=2;i=1003 - + ArtifactUri Where the dataset can be obtained. Treated as untrusted input. i=80 i=68 - ns=1;i=1003 + ns=2;i=1003 - + Digest Digest of the dataset artefact. i=80 i=68 - ns=1;i=1003 + 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. AiDeployment i=58 - ns=1;i=6028 - ns=1;i=6029 - ns=1;i=6030 - ns=1;i=6031 - ns=1;i=6032 - ns=1;i=6033 - ns=1;i=6034 - ns=1;i=6035 + 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=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=6137 + ns=2;i=6138 + ns=2;i=6139 + ns=2;i=6140 + ns=2;i=6141 + ns=2;i=6144 + ns=2;i=6147 - + DeploymentId Identifier of the deployment. i=78 i=68 - ns=1;i=1004 + ns=2;i=1004 - + InferenceLocation Where inference executes. i=78 i=68 - ns=1;i=1004 + ns=2;i=1004 - + AcceleratorKind Compute device executing the model. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + AcceleratorName Free-text accelerator identification, for example an NPU or GPU part name. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + EndpointUri - Inference endpoint when InferenceLocation is not OnServer. Treated as untrusted input and subject to the resolver policy of clause 7. + Inference endpoint when InferenceLocation is not OnServer. Treated as untrusted input and subject to the resolver policy of clause 11. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + LatencyBudget Latency the deployment is expected to meet, so a client can detect regression. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + BatchSize Configured inference batch size. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + State Runtime state of the deployment. i=78 i=68 - ns=1;i=1004 + 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. AiDeployment - i=58 - ns=1;i=6036 - ns=1;i=6037 - ns=1;i=6038 - ns=1;i=6039 - ns=1;i=6040 - ns=1;i=6041 - ns=1;i=6042 - ns=1;i=6043 - ns=1;i=6044 - ns=1;i=6045 - ns=1;i=6047 + ns=2;i=1006 + ns=2;i=6037 + ns=2;i=6038 + ns=2;i=6039 + ns=2;i=6040 + ns=2;i=6041 + ns=2;i=6043 + ns=2;i=6044 + ns=2;i=6045 + ns=2;i=6047 - - JobId - Identifier of the job. - - i=78 - i=68 - ns=1;i=1005 - - - + State - Current stage of the loop. + 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 6 requires the two to agree. i=78 i=68 - ns=1;i=1005 + ns=2;i=1005 - + Dataset Dataset being accumulated or used. i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + BaseModel Model the job starts from. i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + CandidateModel Model produced by the job, awaiting promotion. i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + SamplesCollected Samples accumulated so far, including corrections fed back. i=80 i=68 - ns=1;i=1005 - - - - LastError - Diagnostic for the Failed state. - - i=80 - i=68 - ns=1;i=1005 + ns=2;i=1005 - + StartCollection Begin accumulating samples and corrections into the dataset. i=80 - ns=1;i=1005 + ns=2;i=1005 - + StopCollection Stop accumulating samples. i=80 - ns=1;i=1005 + ns=2;i=1005 - + TriggerTraining Request that a candidate model be trained from the collected dataset. i=80 - ns=1;i=1005 - ns=1;i=6046 + ns=2;i=1005 + ns=2;i=6046 - + OutputArguments i=78 i=68 - ns=1;i=6045 + ns=2;i=6045 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=1;i=1005 - ns=1;i=6048 - ns=1;i=6049 + ns=2;i=1005 + ns=2;i=6048 + ns=2;i=6049 - + InputArguments i=78 i=68 - ns=1;i=6047 + ns=2;i=6047 i=297Deploymenti=17-1Deployment to update, or null for all. - + OutputArguments i=78 i=68 - ns=1;i=6047 + ns=2;i=6047 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. + AiDeployment + + i=2391 + ns=2;i=6050 + ns=2;i=6051 + ns=2;i=6052 + ns=2;i=6053 + ns=2;i=6054 + ns=2;i=6055 + + + + 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 11 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 9 requires the Digest to be verified there and nowhere else. + AiDeployment + + ns=2;i=1006 + 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=6062 + ns=2;i=6063 + + + + Source + ModelSourceType instance the model is pulled from. + + 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. + AiDeployment + + ns=2;i=1006 + 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=6071 + ns=2;i=6072 + + + + 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 11 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. + AiDeployment + + i=58 + 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=6084 + ns=2;i=6085 + ns=2;i=6087 + + + + 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 11. + + 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 11 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=6086 + + + + OutputArguments + + i=78 + i=68 + ns=2;i=6085 + + 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=6088 + ns=2;i=6089 + + + + InputArguments + + i=78 + i=68 + ns=2;i=6087 + + i=297Filteri=12-1Optional substring or expression; empty for all.i=297MaxResultsi=7-1Upper bound on returned entries. + + + OutputArguments + + i=78 + i=68 + ns=2;i=6087 + + i=297Modelsns=2;i=305110Publisher, name and version of each model offered. + + + 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. + AiDeployment + + i=58 + ns=2;i=6090 + ns=2;i=6091 + ns=2;i=6092 + ns=2;i=6093 + ns=2;i=6094 + ns=2;i=6095 + ns=2;i=6096 + + + + 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 11. + + 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'. + AiDeployment + + i=58 + ns=2;i=6097 + ns=2;i=6098 + ns=2;i=6099 + ns=2;i=6100 + ns=2;i=6101 + ns=2;i=6102 + ns=2;i=6103 + + + + 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. + AiDeployment + + ns=1;i=63000 + ns=2;i=6104 + + + + <ModelPublisher> + A publisher namespace held by this registry. + + i=11508 + ns=2;i=1011 + ns=2;i=1010 + + + + 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. + AiDeployment + + ns=1;i=63001 + ns=2;i=6105 + ns=2;i=6106 + + + + <Model> + A model published in this namespace. + + i=11508 + ns=2;i=1012 + ns=2;i=1011 + + + + <Dataset> + A dataset published in this namespace. + + i=11508 + ns=2;i=1013 + ns=2;i=1011 + + + + 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. + AiDeployment + + ns=1;i=63002 + ns=2;i=6107 + ns=2;i=6108 + ns=2;i=6109 + ns=2;i=6110 + ns=2;i=6111 + ns=2;i=6112 + ns=2;i=6113 + + + + 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 11. + + 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. + AiDeployment + + ns=1;i=63002 + ns=2;i=6114 + ns=2;i=6115 + ns=2;i=6116 + ns=2;i=6117 + ns=2;i=6118 + + + + 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 11. + + 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=6142 + ns=2;i=6143 + + + + InputArguments + + i=78 + i=68 + ns=2;i=6141 + + i=297Payloadi=15-1Request body.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=6141 + + 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 11 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. + + + 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=6145 + ns=2;i=6146 + + + + InputArguments + + i=78 + i=68 + ns=2;i=6144 + + i=297Payloadi=15-1Request body.i=297ContentTypei=12-1Media type of Payload.i=297Parametersi=1453310Call parameters. + + + OutputArguments + + i=78 + i=68 + ns=2;i=6144 + + 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=6148 + + + + OutputArguments + + i=78 + i=68 + ns=2;i=6147 + + i=297Capabilitiesns=2;i=305310Current capabilities. + + AiDeployment Entry point for AI deployment and learning on this Server. A client browses Server/AiDeployment/Models to find what this Server describes. - ns=1;i=1001 + ns=2;i=1001 i=2253 diff --git a/metaverse-specs/extras/ai-deployment/tools/build_model.py b/metaverse-specs/extras/ai-deployment/tools/build_model.py index be40018..1bf5e54 100644 --- a/metaverse-specs/extras/ai-deployment/tools/build_model.py +++ b/metaverse-specs/extras/ai-deployment/tools/build_model.py @@ -40,11 +40,26 @@ import xml.sax.saxutils as sx NAMESPACE = "http://opcfoundation.org/UA/AI/" -VERSION = "0.1.0" -PUBDATE = "2026-08-02T00:00:00Z" +VERSION = "0.2.0" +PUBDATE = "2026-08-03T00: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 9. +XREG_NS = "http://opcfoundation.org/UA/xRegistry/" +XREG_VERSION = "0.3.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" @@ -90,9 +105,19 @@ 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" + +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), @@ -146,9 +171,24 @@ def _mid(): return v +def _reserve(count, why): + """Burn `count` member ids without declaring anything. + + Used where a member moved to a base type. Its id is NOT reclaimed: reusing it + would renumber every member declared after it, which is exactly the churn the + append-only rule exists to prevent. A hole costs nothing. + """ + _next_member[0] += count + + def T(nid): - """Own-namespace NodeId (ns=1).""" - return f"ns=1;i={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, @@ -425,6 +465,114 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): ("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."), + ("OpenAiCompatible", 1, "The de-facto chat/embeddings REST contract that " + "most serving runtimes, including on-device ones, " + "expose."), + ("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 11 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) + # --------------------------------------------------------------------------- # Structured DataTypes (3050+) # --------------------------------------------------------------------------- @@ -440,6 +588,89 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "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+) # --------------------------------------------------------------------------- @@ -447,7 +678,7 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "Links a Deployment to the Model it executes. Clause 5.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 7 depends.") + "provenance requirement of clause 11 depends.") UsesModel = T(4001) reference_type(4002, "TrainedOn", "IsTrainingDataFor", @@ -456,6 +687,31 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "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 8 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+) # --------------------------------------------------------------------------- @@ -493,7 +749,7 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "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 7 requires it for every model whose artefact is obtainable " + "Mandatory: clause 11 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) @@ -502,7 +758,7 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "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 7.", + "verification. SHALL be non-empty where Digest is non-empty. See clause 11.", MR_Mandatory) prop_var(AM, "ModelType", "ArtifactUri", String, "Where the model artefact can be obtained. Treated as untrusted input.") @@ -554,7 +810,7 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "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 7.") + "untrusted input and subject to the resolver policy of clause 11.") prop_var(AY, "DeploymentType", "LatencyBudget", Duration, "Latency the deployment is expected to meet, so a client can detect " "regression.") @@ -563,16 +819,18 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): prop_var(AY, "DeploymentType", "State", DeploymentStateEnum, "Runtime state of the deployment.", MR_Mandatory) -object_type(1005, "LearningJobType", BaseObjectType, +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", "JobId", String, "Identifier of the job.", MR_Mandatory) +_reserve(1, "6036 held JobId, now inherited from AiJobType.") prop_var(LJ, "LearningJobType", "State", LearningJobStateEnum, - "Current stage of the loop.", MR_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 6 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.") @@ -580,8 +838,7 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "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.") +_reserve(1, "6042 held LastError, now inherited from AiJobType.") method(LJ, "LearningJobType", "StartCollection", "Begin accumulating samples and corrections into the dataset.", MR_Optional) method(LJ, "LearningJobType", "StopCollection", @@ -599,6 +856,414 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): 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 11 " + "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 9 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.", 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 11 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 11.", 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 11 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.")], + outargs=[("Models", ModelReferenceDataType, + "Publisher, name and version of each model offered.", 1)]) + +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 11.") + +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 9). +# +# 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 +obj_member(MR_, "ModelRegistryType", "", T(1011), + "A publisher namespace held by this registry.", MR_OptionalPlaceholder) + +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 +obj_member(MP, "ModelPublisherType", "", T(1012), + "A model published in this namespace.", MR_OptionalPlaceholder) +obj_member(MP, "ModelPublisherType", "", T(1013), + "A dataset published in this namespace.", MR_OptionalPlaceholder) + +object_type(1012, "ModelResourceType", XRegistry_ResourceType, + "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 11.") +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", XRegistry_ResourceType, + "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 11.") + +# --- 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."), + ("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 11 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.")]) +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."), + ("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)]) + # --------------------------------------------------------------------------- # Well-known instance (7001+) # --------------------------------------------------------------------------- @@ -672,14 +1337,16 @@ def emit(): 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' 'xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd" ' 'xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">', - ' ', - f' {NAMESPACE}', - ' ', + ' '] + out += [f' {u}' for u in NAMESPACE_URIS] + out += [' ', ' ', f' ', f' ', + f' ', ' ', ' ', ' '] diff --git a/metaverse-specs/extras/ai-deployment/tools/model-reference.md b/metaverse-specs/extras/ai-deployment/tools/model-reference.md index 45b5e61..261a70b 100644 --- a/metaverse-specs/extras/ai-deployment/tools/model-reference.md +++ b/metaverse-specs/extras/ai-deployment/tools/model-reference.md @@ -10,24 +10,56 @@ This annex is the authoritative node reference for the specification: it carries |---|---|---|---| | ns=1;i=4001 | UsesModel | ReferenceType | NonHierarchicalReferences | | ns=1;i=4002 | TrainedOn | ReferenceType | NonHierarchicalReferences | +| ns=1;i=4003 | DerivedFrom | ReferenceType | NonHierarchicalReferences | +| ns=1;i=4004 | FallsBackTo | ReferenceType | NonHierarchicalReferences | +| ns=1;i=4005 | ImportedFrom | ReferenceType | NonHierarchicalReferences | +| ns=1;i=4006 | EvaluatedBy | ReferenceType | NonHierarchicalReferences | | ns=1;i=1001 | AiRootType | ObjectType | BaseObjectType | | ns=1;i=1002 | ModelType | ObjectType | BaseObjectType | | ns=1;i=1003 | DatasetType | ObjectType | BaseObjectType | | ns=1;i=1004 | DeploymentType | ObjectType | BaseObjectType | -| ns=1;i=1005 | LearningJobType | ObjectType | BaseObjectType | +| ns=1;i=1005 | LearningJobType | ObjectType | ns=2;i=1006 | +| ns=1;i=1006 | AiJobType | ObjectType | ProgramStateMachineType | +| ns=1;i=1007 | ModelImportJobType | ObjectType | ns=2;i=1006 | +| ns=1;i=1008 | InferenceJobType | ObjectType | ns=2;i=1006 | +| ns=1;i=1009 | ModelSourceType | ObjectType | BaseObjectType | +| ns=1;i=1014 | EvaluationRunType | ObjectType | BaseObjectType | +| ns=1;i=1015 | ModelCardType | ObjectType | BaseObjectType | +| ns=1;i=1010 | ModelRegistryType | ObjectType | ns=1;i=63000 | +| ns=1;i=1011 | ModelPublisherType | ObjectType | ns=1;i=63001 | +| ns=1;i=1012 | ModelResourceType | ObjectType | ns=1;i=63002 | +| ns=1;i=1013 | DatasetResourceType | ObjectType | ns=1;i=63002 | | ns=1;i=3001 | InferenceLocationEnum | DataType | Enumeration | | ns=1;i=3002 | AcceleratorKindEnum | DataType | Enumeration | | ns=1;i=3003 | DeploymentStateEnum | DataType | Enumeration | | ns=1;i=3004 | DatasetSourceEnum | DataType | Enumeration | | ns=1;i=3005 | LearningJobStateEnum | DataType | Enumeration | +| ns=1;i=3006 | FinishReasonEnum | DataType | Enumeration | +| ns=1;i=3007 | ApiDialectEnum | DataType | Enumeration | +| ns=1;i=3008 | AuthenticationKindEnum | DataType | Enumeration | +| ns=1;i=3009 | FallbackPolicyEnum | DataType | Enumeration | +| ns=1;i=3010 | VersionBindingEnum | DataType | Enumeration | +| ns=1;i=3011 | ImportModeEnum | DataType | Enumeration | +| ns=1;i=3012 | SafetySeverityEnum | DataType | Enumeration | +| ns=1;i=3013 | ReachabilityEnum | DataType | Enumeration | | ns=1;i=3050 | TensorSignatureDataType | DataType | Structure | +| ns=1;i=3051 | ModelReferenceDataType | DataType | Structure | +| ns=1;i=3052 | UsageDataType | DataType | Structure | +| ns=1;i=3053 | CapabilityDataType | DataType | Structure | +| ns=1;i=3054 | SafetyAssessmentDataType | DataType | Structure | +| ns=1;i=3055 | EvaluationMetricDataType | DataType | Structure | +| ns=1;i=3056 | RateLimitDataType | DataType | Structure | ## A.2 ReferenceTypes | NodeId | BrowseName | InverseName | Subtype of | Description | |---|---|---|---|---| -| ns=1;i=4001 | UsesModel | IsUsedByDeployment | NonHierarchicalReferences | Links a Deployment to the Model it executes. Clause 5.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 7 depends. | +| ns=1;i=4001 | UsesModel | IsUsedByDeployment | NonHierarchicalReferences | Links a Deployment to the Model it executes. Clause 5.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 11 depends. | | ns=1;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=1;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=1;i=4004 | FallsBackTo | IsFallbackFor | NonHierarchicalReferences | Links a Deployment to the Deployment that serves in its place when it cannot. Clause 8 forbids a cycle, and requires the response to say which deployment actually answered. | +| ns=1;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=1;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 @@ -44,6 +76,10 @@ Server-level entry point. A client that has just connected browses here to find | 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=1;i=1002` @@ -59,13 +95,18 @@ Nameplate of a trained model. The member set is deliberately aligned with the ID | 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 7 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 7. | +| Digest | Variable | ByteString | Scalar | Mandatory | Cryptographic digest of the model artefact, for provenance and integrity. Mandatory: clause 11 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 11. | | 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. | +| Inputs | Variable | ns=2;i=3050 | Array | Optional | Input tensor signatures. | +| Outputs | Variable | ns=2;i=3050 | 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 11. | ### DatasetType — `ns=1;i=1003` @@ -78,7 +119,7 @@ A dataset used to train or validate a model. Aligned with the IDTA 02058 AI Data | 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. | +| SourceKind | Variable | ns=2;i=3004 | 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. | @@ -94,29 +135,79 @@ A model made executable somewhere. Aligned with the IDTA 02059 AI Deployment sub | 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. | +| InferenceLocation | Variable | ns=2;i=3001 | Scalar | Mandatory | Where inference executes. | +| AcceleratorKind | Variable | ns=2;i=3002 | 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 7. | +| EndpointUri | Variable | String | Scalar | Optional | Inference endpoint when InferenceLocation is not OnServer. Treated as untrusted input and subject to the resolver policy of clause 11. | | 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 | DeploymentStateEnum | Scalar | Mandatory | Runtime state of the deployment. | +| State | Variable | ns=2;i=3003 | 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 | ns=2;i=3010 | 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 | ns=2;i=3009 | Scalar | Mandatory | What the Server does when this deployment cannot serve. | +| Reachability | Variable | ns=2;i=3013 | 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 | ns=2;i=3056 | Scalar | Optional | Capacity the execution site is currently granting. | +| Capabilities | Variable | ns=2;i=3053 | 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. | + +**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. | +| 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 11 walks this. | +| Usage | ns=2;i=3052 | Scalar | What the call consumed. | +| FinishReason | ns=2;i=3006 | Scalar | Why output stopped. A caller that ignores this will accept a truncated answer as a complete one. | +| SafetyAssessment | ns=2;i=3054 | 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. | + +**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. | +| 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 | ns=2;i=3053 | Array | Current capabilities. | ### LearningJobType — `ns=1;i=1005` -*Subtype of:* `BaseObjectType` +*Subtype of:* `ns=2;i=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. | BrowseName | NodeClass | DataType | ValueRank | ModellingRule | Description | |---|---|---|---|---|---| -| JobId | Variable | String | Scalar | Mandatory | Identifier of the job. | -| State | Variable | LearningJobStateEnum | Scalar | Mandatory | Current stage of the loop. | +| State | Variable | ns=2;i=3005 | 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 6 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. | -| LastError | Variable | LocalizedText | Scalar | Optional | Diagnostic for the Failed state. | **Method `StartCollection`** (Optional) — Begin accumulating samples and corrections into the dataset. @@ -142,6 +233,181 @@ Takes no arguments and returns none. |---|---|---|---| | PromotedModel | NodeId | Scalar | The model now in use. | +### AiJobType (abstract) — `ns=1;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 11 requires this for any job that can promote a model. | + +### ModelImportJobType — `ns=1;i=1007` + +*Subtype of:* `ns=2;i=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 9 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. | +| ModelReference | Variable | ns=2;i=3051 | Scalar | Mandatory | Publisher, name and version being imported. | +| Mode | Variable | ns=2;i=3011 | 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. | + +**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=1;i=1008` + +*Subtype of:* `ns=2;i=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. + +| 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 11 walks this, not the deployment's current model. | +| Usage | Variable | ns=2;i=3052 | Scalar | Optional | What the call consumed. | +| FinishReason | Variable | ns=2;i=3006 | Scalar | Optional | Why the call stopped producing output. | +| SafetyAssessment | Variable | ns=2;i=3054 | Array | Optional | Findings from the safety policy, if any were applied. | + +### ModelSourceType — `ns=1;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 11. | +| ApiDialect | Variable | ns=2;i=3007 | 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 | ns=2;i=3008 | 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 11 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 | ns=2;i=3013 | 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 | ns=2;i=3056 | Scalar | Optional | Capacity the endpoint is currently granting. | +| Capabilities | Variable | ns=2;i=3053 | 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. | + +| Out | DataType | ValueRank | Meaning | +|---|---|---|---| +| Models | ns=2;i=3051 | Array | Publisher, name and version of each model offered. | + +### EvaluationRunType — `ns=1;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 | ns=2;i=3055 | 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 11. | + +### ModelCardType — `ns=1;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. | + +### ModelRegistryType — `ns=1;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. | + +### ModelPublisherType — `ns=1;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 published in this namespace. | +| | Object | | | OptionalPlaceholder | A dataset published in this namespace. | + +### ModelResourceType — `ns=1;i=1012` + +*Subtype of:* `ns=1;i=63002` + +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 11. | +| 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. | + +### DatasetResourceType — `ns=1;i=1013` + +*Subtype of:* `ns=1;i=63002` + +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 | ns=2;i=3004 | 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. | + ## A.4 DataTypes ### InferenceLocationEnum — `ns=1;i=3001` @@ -215,6 +481,111 @@ State of a dataset-capture, retraining and promotion cycle. | Promoted | 6 | | | Failed | 7 | | +### FinishReasonEnum — `ns=1;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=1;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. | +| OpenAiCompatible | 1 | The de-facto chat/embeddings REST contract that most serving runtimes, including on-device ones, expose. | +| 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=1;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=1;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=1;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 11 requires the resulting promotion to be as authorized as an explicit one. | + +### ImportModeEnum — `ns=1;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=1;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=1;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. | + ### TensorSignatureDataType — `ns=1;i=3050` *Subtype of:* `Structure` @@ -227,3 +598,81 @@ Shape and element type of one model input or output tensor. This is what lets a | 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=1;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=1;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=1;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=1;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 | ns=2;i=3012 | 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=1;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=1;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-deployment/tools/validate_local.py b/metaverse-specs/extras/ai-deployment/tools/validate_local.py index cfb0168..acceb58 100644 --- a/metaverse-specs/extras/ai-deployment/tools/validate_local.py +++ b/metaverse-specs/extras/ai-deployment/tools/validate_local.py @@ -68,6 +68,9 @@ 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. @@ -136,11 +139,34 @@ def supertype(self, nid: str) -> str: 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; base-UA ids are taken on trust.""" + """Own-namespace targets must exist here; imported ids are taken on trust.""" if target in self.nodes: return True - if target.startswith("ns=1;"): + 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 @@ -151,7 +177,8 @@ def definition(self, nid: str): return None def members_of(self, nid: str) -> list[str]: - want = f"ns=1;i={nid.split('=')[-1]}" if not nid.startswith("ns=1;") else nid + 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: @@ -200,15 +227,18 @@ def check_model_header(m: Model) -> None: return req = models[0].findall("u:RequiredModel", NS) uris = [r.get("ModelUri") for r in req] - if uris != [UA_NAMESPACE]: - err("this model is standalone: the only RequiredModel must be " - f"{UA_NAMESPACE}, found {uris}") + 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)] - if len(uris_declared) != 1: - err(f"expected exactly one NamespaceUri, found {uris_declared}") - elif uris_declared[0] != models[0].get("ModelUri"): - err("NamespaceUris entry and Model ModelUri disagree") + # 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: @@ -377,7 +407,8 @@ def check_csv(m: Model) -> None: 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): + 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(): @@ -414,12 +445,25 @@ def dt(name: str) -> str: 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: - for name in ("JobId", "State"): - mm = m.member_named(job_t, name) - if not mm or m.modelling_rule(mm) != "Mandatory": - err(f"LearningJobType.{name} must be declared Mandatory") + 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 " @@ -483,9 +527,15 @@ def check_spec_crossref(m: Model) -> None: err(f"model declares {name}.{field} but the specification never " "names it") - for cited in set(re.findall(r"ns=1;i=(\d+)", text)): - if f"ns=1;i={cited}" not in m.nodes: - err(f"specification cites ns=1;i={cited}, which is not in the NodeSet") + 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") # The other direction. Every `SomeType.SomeMember` the prose writes must exist, # otherwise the document describes a member no Server can implement. Only diff --git a/metaverse-specs/extras/vision/tools/validate_local.py b/metaverse-specs/extras/vision/tools/validate_local.py index 8e99519..998bbb9 100644 --- a/metaverse-specs/extras/vision/tools/validate_local.py +++ b/metaverse-specs/extras/vision/tools/validate_local.py @@ -53,17 +53,37 @@ _HERE, "..", "..", "..", "ai-deployment", "Opc.Ua.AiDeployment.NodeSet2.xml")) +def _ai_prefix(): + """The NodeId prefix the AI Deployment 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 Deployment model declares.""" out = {} - if not os.path.exists(AI_NODESET): + 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("ns=1;i="): + if bn and nid.startswith(AI_PREFIX): out[bn] = int(nid.split("i=")[1]) return out @@ -74,11 +94,11 @@ def _load_ai_types(): def _load_ai_ids(): """Every numeric NodeId the AI Deployment model declares, for reference checking.""" out = set() - if not os.path.exists(AI_NODESET): + 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("ns=1;i="): + if nid.startswith(AI_PREFIX): out.add(int(nid.split("i=")[1])) return out 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 ca24764..ac21d30 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 @@ -10,7 +10,7 @@ - + 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 a79511e..683f5b7 100644 --- a/metaverse-specs/vision/robotics/Opc.Ua.Robotics.Vision.NodeSet2.xml +++ b/metaverse-specs/vision/robotics/Opc.Ua.Robotics.Vision.NodeSet2.xml @@ -10,7 +10,7 @@ - + From 9f50ad1f76f35a0176fd022a27205c2af338435b Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 3 Aug 2026 11:47:44 +0200 Subject: [PATCH 2/4] Enforce the new rules, and show what they add up to The invariants that make clauses 7 to 10 executable rather than aspirational, each mutation-tested: - The four Mandatory members clause 8 rests on. A rule resting on an Optional member is a rule a conformant Server can silently not satisfy. - Invoke returns ModelUsed, Usage and FinishReason. - Nothing in the address space is NAMED like credential material. That is how the clause 8.2 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. - The catalogue types narrow their inherited placeholders. A domain extension that leaves them open adds metadata while restricting nothing. - AiJobType still derives from the Part 10 base. - NamespaceUris order, because it fixes every namespace index in the file. All seven mutations caught; artifacts restored byte-exactly. Annex C works one arrangement end to end: a federated cloud model and a staged edge model, the WAN dropping, and the fallback answering. The point of it is the sentence a table cannot make - the client called the same Method with the same arguments throughout and never learned that inference had moved, except by reading ModelUsed, which is exactly what an audit a month later needs and what a client logging the deployment would have got wrong. ApiDialectEnum.OpenAiCompatible became RestChatCompletions. The literal named a company. A standard's enumeration should describe the contract, not advertise whoever published it first - and the prose says what it is compatible with without needing the name. No vendor is named anywhere in the document. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --- metaverse-specs/README.md | 6 +- .../ai-deployment/OPC-UA-AI-Deployment.md | 72 +++++++++++++++++- .../Opc.Ua.AiDeployment.NodeSet2.xml | 4 +- .../extras/ai-deployment/tools/build_model.py | 7 +- .../ai-deployment/tools/model-reference.md | 2 +- .../ai-deployment/tools/validate_local.py | 73 +++++++++++++++++++ 6 files changed, 151 insertions(+), 13 deletions(-) diff --git a/metaverse-specs/README.md b/metaverse-specs/README.md index 00cbac9..8c6ca39 100644 --- a/metaverse-specs/README.md +++ b/metaverse-specs/README.md @@ -29,9 +29,9 @@ Pick Part 1 when you already have an artist-authored USD asset and want to drive | 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.1.0 | +| Release | 0.1.0 | 0.1.0 | 0.2.0 | -All three are self-contained on base OPC UA — none takes a companion specification as a `RequiredModel`. +Vision and Robot Intent are self-contained on base OPC UA. AI Deployment additionally requires *OPC UA — xRegistry*, because its model catalogue is a domain extension of that abstract registry rather than a private invention — a model catalogue **is** a registry, and defining a second one would leave two incompatible ways to describe the same artefact. **They compose without coupling.** Vision's `InferencePipelineType.Deployment` is a plain `NodeId`, so a Server can publish cameras and verdicts with no AI model at all; where it does describe one, the chain *result → deployment → model → digest* is available end to end. Vision and Robot Intent share a frame vocabulary with identical literals and numbering. In every case the join is a **facet precondition**, never a `RequiredModel` — which is what lets a domain adopt one model without inheriting the others. @@ -41,7 +41,7 @@ All three are self-contained on base OPC UA — none takes a companion specifica - *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-deployment/` — **OPC UA — AI Deployment and Learning**: the model nameplate, the dataset it was trained on, the deployment that executes it and the learning job that produces the next version. Deliberately **domain-neutral** — it names no camera, no robot and no sensor — so any domain can build on it. Aligned member-for-member with IDTA 02058/02059/02060. +- `ai-deployment/` — **OPC UA — AI Deployment and Learning**: 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. diff --git a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md index fcf4733..303193c 100644 --- a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md +++ b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md @@ -334,9 +334,9 @@ A deployment whose `InferenceLocation` is not `OnServer` executes somewhere the ### 8.2 The wire contract, and the credential that is never a secret -`ApiDialect` (`ApiDialectEnum`, `ns=2;i=3007`) is `OpcUaInference`, `OpenAiCompatible`, `OpenInferenceProtocol`, `TensorRemoteProcedure`, `EmbeddedRuntime` or `Proprietary`. +`ApiDialect` (`ApiDialectEnum`, `ns=2;i=3007`) is `OpcUaInference`, `RestChatCompletions`, `OpenInferenceProtocol`, `TensorRemoteProcedure`, `EmbeddedRuntime` or `Proprietary`. -These name **the contract the remote endpoint speaks**. They never affect how an OPC UA client calls this Server, which is always §7. `OpcUaInference` is another Server implementing this specification; `OpenAiCompatible` is the de-facto chat and embeddings contract that most serving runtimes expose, including ones that run on a single workstation; `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. +These name **the contract the remote endpoint speaks**. They never affect how an OPC UA client calls this Server, which is always §7. `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. `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. @@ -546,8 +546,6 @@ python metaverse-specs\extras\ai-deployment\tools\validate_local.py 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-deployment/tools/model-reference.md`](../extras/ai-deployment/tools/model-reference.md). ---- - ## Annex B — Informative alignments Not normative references, and no dependency. Recorded because this model borrowed from them deliberately. @@ -556,4 +554,70 @@ Not normative references, and no dependency. Recorded because this model borrowe - **IDTA 02058** *AI Dataset* — the member set of `DatasetType`. - **IDTA 02059** *AI Deployment* — 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 9 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 §4.2, and neither NodeSet requires the other. + +--- + +## Annex C — A worked arrangement (informative) + +This annex is **informative**. It shows one arrangement that satisfies clauses 7 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 (§9.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, §8.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 §8.3 treats as a promotion in disguise. + +### C.4 A normal call, and a bad afternoon + +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 §7.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 What a throttle would have done instead + +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. + +--- diff --git a/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml b/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml index b7871cd..05d75be 100644 --- a/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml +++ b/metaverse-specs/ai-deployment/Opc.Ua.AiDeployment.NodeSet2.xml @@ -167,7 +167,7 @@ i=29 ns=2;i=3907 - Another OPC UA Server implementing this specification's Invoke Method.The de-facto chat/embeddings REST contract that most serving runtimes, including on-device ones, expose.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. + 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 @@ -176,7 +176,7 @@ i=68 ns=2;i=3007 - OpcUaInferenceOpenAiCompatibleOpenInferenceProtocolTensorRemoteProcedureEmbeddedRuntimeProprietary + OpcUaInferenceRestChatCompletionsOpenInferenceProtocolTensorRemoteProcedureEmbeddedRuntimeProprietary AuthenticationKindEnum diff --git a/metaverse-specs/extras/ai-deployment/tools/build_model.py b/metaverse-specs/extras/ai-deployment/tools/build_model.py index 1bf5e54..42005fe 100644 --- a/metaverse-specs/extras/ai-deployment/tools/build_model.py +++ b/metaverse-specs/extras/ai-deployment/tools/build_model.py @@ -487,9 +487,10 @@ def well_known(nid, name, typedef, parent_nodeid, desc, reftype=HasComponent): "an OPC UA client calls this Server.", [("OpcUaInference", 0, "Another OPC UA Server implementing this " "specification's Invoke Method."), - ("OpenAiCompatible", 1, "The de-facto chat/embeddings REST contract that " - "most serving runtimes, including on-device ones, " - "expose."), + ("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 " diff --git a/metaverse-specs/extras/ai-deployment/tools/model-reference.md b/metaverse-specs/extras/ai-deployment/tools/model-reference.md index 261a70b..8bae36e 100644 --- a/metaverse-specs/extras/ai-deployment/tools/model-reference.md +++ b/metaverse-specs/extras/ai-deployment/tools/model-reference.md @@ -505,7 +505,7 @@ Wire contract a remote inference endpoint speaks. A Server needs this to call an | Name | Value | Description | |---|---|---| | OpcUaInference | 0 | Another OPC UA Server implementing this specification's Invoke Method. | -| OpenAiCompatible | 1 | The de-facto chat/embeddings REST contract that most serving runtimes, including on-device ones, expose. | +| 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. | diff --git a/metaverse-specs/extras/ai-deployment/tools/validate_local.py b/metaverse-specs/extras/ai-deployment/tools/validate_local.py index acceb58..3a2304c 100644 --- a/metaverse-specs/extras/ai-deployment/tools/validate_local.py +++ b/metaverse-specs/extras/ai-deployment/tools/validate_local.py @@ -416,6 +416,78 @@ def check_csv(m: Model) -> None: 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.""" + + # 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. + for owner, wanted in (("ModelRegistryType", {"ModelPublisherType"}), + ("ModelPublisherType", {"ModelResourceType", + "DatasetResourceType"})): + nid = dt(owner) + if not nid: + continue + narrowed = set() + for mem in m.members_of(nid): + if not m.bname(mem).startswith("<"): + continue + for rt, tgt, fwd in m.refs(mem): + if rt in ("i=40", "HasTypeDefinition") and fwd and tgt in m.nodes: + narrowed.add(m.bname(tgt)) + missing = wanted - narrowed + if missing: + err(f"{owner} must narrow its inherited placeholder to {sorted(missing)}; " + "a subtype that leaves it open restricts nothing") + + def check_spec_invariants(m: Model) -> None: def dt(name: str) -> str: nid = m.by_name(name) @@ -423,6 +495,7 @@ def dt(name: str) -> str: 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") From cce9495cc6447c991838f5b57589ac03e9645fe8 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 3 Aug 2026 11:57:45 +0200 Subject: [PATCH 3/4] Deepen the clauses that were already there Adding three clauses left the existing ones describing a smaller model than the one that now exists, and put 5.6 between 5.1 and 5.2 - the out-of-document-order defect this repository has been bitten by before and which no check catches. 5.6 moved to where it belongs. Every heading is now in order. 5.2 documents what a model carries beyond its artefact: the publisher that completes the identity triple, and Quantization, which is not a packaging detail - a quantized model is a different artefact that gives different answers, and treating it as a variant is how a model evaluated at full precision ends up deployed at reduced precision with nobody re-measuring it. 5.3 says why Mixed is not a hedge, and that a dataset whose class list disagrees in ORDER with its model is undetectable: every identifier resolves, every count is plausible, every label is off by one. 5.4 gains a table of everything that makes a deployment callable, and the reason State and Reachability are separate. Ready + Unreachable is a network fault; Faulted + Reachable points at credentials. Collapsing them loses exactly what a commissioning engineer needs. 6 gains the joins to the consuming half - a trained candidate is not privileged over an imported model and owes the same lineage - and says plainly that almost no Server implements the whole loop, which is why State is read rather than inferred from which Methods exist. 11 grew because the attack surface did. The chain has more links; two break while a Server looks correct. The resolver policy now governs seven URIs, and applying it to a staging import AFTER transfer is not a control. Credential material is unaddressable, argued from the fact that the address space is subscribable and historisable - a secret there is not exposed once, it is published and archived. FollowsRef is promotion's second door and needs the same authorization, or the audit trail shows every promotion having been authorized. And a fallback may not be more permissive on egress than what falls back to it, or a network fault quietly sends plant data where policy forbids. 11.4 now says what DigestVerified actually means: the artefact is the one this catalogue entry described. An attacker who can edit both artefact and entry defeats it, and the catalogue is the better target because many machines read it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --- .../ai-deployment/OPC-UA-AI-Deployment.md | 149 ++++++++++++++++-- 1 file changed, 133 insertions(+), 16 deletions(-) diff --git a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md index 303193c..7f66fce 100644 --- a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md +++ b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md @@ -150,39 +150,47 @@ The consequence for a reader: every member of `ModelType` is about *this* artefa `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. -### 5.6 `AiJobType` and the three jobs +### 5.2 `ModelType` -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`. +Identity, provenance and interface of a trained model. Aligned with IDTA 02060. -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. +`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 11 depends on them, and a rule that depends on an Optional member is a rule a conformant Server can silently not satisfy. -`AiJobType` adds `JobId`, `LastError`, `StartedAt`, `FinishedAt`, `Progress` and `RequestedBy`. +`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. -`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. +`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. -`RequestedBy` records the identity that started the job, at the moment it started. §11.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". +`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. -**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`. +Clause 7 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. -Annex A is the authoritative node reference and carries every member with its DataType, ValueRank and ModellingRule. +#### 5.2.1 Identity beyond the artefact -### 5.2 `ModelType` +`Publisher` completes the `Publisher`, `Name`, `Version` triple by which every catalogue in practice identifies a model (§9.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. -Identity, provenance and interface of a trained model. Aligned with IDTA 02060. +`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. -`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 11 depends on them, and a rule that depends on an Optional member is a rule a conformant Server can silently not satisfy. +#### 5.2.2 What it costs, and at what precision -`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. +`ParameterCount` is a crude proxy for what a model will cost to run, and is the one such figure that is universally published. -`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. +`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. §10.3 requires the derivation to be stated as well. -`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. +`SafetyPolicyUri` names the policy applied to this model's output, where one is. Like every URI here it is untrusted input (§11.2). + +`Card` reaches the `ModelCardType` of §10.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. ### 5.3 `DatasetType` What a model was trained or validated on. Aligned with IDTA 02058. -`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. +`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`, `CreatedAt` and `LabelClasses` describe the contents; `ArtifactUri` and `Digest` describe where the data is and how to know it is the data meant. + +`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` (§5.5) is a repeating reference and why the catalogue gives datasets their own resource type (§9.1). ### 5.4 `DeploymentType` @@ -194,7 +202,30 @@ A model made executable. Aligned with IDTA 02059. `AcceleratorKind` (`AcceleratorKindEnum`, `ns=2;i=3002`) is `Cpu`, `Gpu`, `Npu`, `Fpga`, `Tpu` or `Other`. `State` (`DeploymentStateEnum`, `ns=2;i=3003`) is `Inactive` 0, `Ready` 1, `Active` 2, `Degraded` 3 or `Faulted` 4, and is **Mandatory** because §6 and any consuming specification's availability logic depend on it. -`EndpointUri` is meaningful when `InferenceLocation` is not `OnServer`. It is **untrusted input** and subject to §11.2. +`AcceleratorName` is free text for the specific part, because the enumeration deliberately does not attempt to name every accelerator ever shipped. + +`LatencyBudget` states the latency the deployment is expected to meet. It exists so a client can **detect** regression rather than merely experience it: without a declared expectation, a deployment that has become three times slower looks exactly like one that was always slow. + +`BatchSize` is the configured inference batch size, which a client needs to interpret latency — a large batch trades per-item latency for throughput, and a budget breach on a batched deployment may mean nothing is wrong. + +#### 5.4.1 The members that make it callable + +`EndpointUri` is meaningful when `InferenceLocation` is not `OnServer`. It is **untrusted input** and subject to §11.2. On its own it is not enough to call anything, which is what `Source` and clause 8 are for. + +Everything a client needs in order to *use* a deployment is added by later clauses, and it is worth seeing the whole set in one place: + +| Members | Clause | The question | +|---|---|---| +| `Invoke`, `InvokeAsync`, `GetCapabilities`, `Capabilities` | 7 | How do I call it, and what can it do? | +| `Source`, `VersionBinding`, `BoundRef` | 8.2, 8.3 | Where does it execute, and can the artefact move under me? | +| `FallbackPolicy`, `Reachability`, `ConsecutiveFailures`, `LastSuccessAt`, `RateLimit` | 8.4 | Is it answering, and what happens when it is not? | +| `DataJurisdiction`, `EgressPermitted`, `RetainsInput`, `EgressPolicyUri` | 8.5 | Does calling it send my data off site? | + +#### 5.4.2 `State` and `Reachability` answer different questions + +`State` is about the deployment as this Server configured it; `Reachability` is about whether the far end is currently answering. They are independent, and the combinations are meaningful rather than redundant. + +A deployment can be `Ready` and `Unreachable` — correctly configured, network down — which is the case §8.4's fallback exists for. It can be `Faulted` and `Reachable` — the endpoint answers, but rejects everything this Server sends, which points at credentials or a contract mismatch rather than at the network. Collapsing the two into one value would lose exactly the distinction a commissioning engineer needs. ### 5.5 `UsesModel` and `TrainedOn` @@ -204,6 +235,22 @@ This is the only defined path from a running deployment to the artefact its resu `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. +### 5.6 `AiJobType` and the three jobs + +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. §11.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. + --- ## 6 The learning loop (normative) @@ -232,6 +279,26 @@ stateDiagram-v2 **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 (§11.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. + +### 6.1 Where the loop meets the rest of the model + +The loop is the **producing** half of this specification; clauses 7 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 — §10.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` (§10.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` (§8.3), promotion and repointing are two routes to the same outcome. §11.3.1 requires both to be authorized alike. + +### 6.2 A Server may implement very little of this + +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 §6. + +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. + --- ## 7 Inference (normative) @@ -479,22 +546,66 @@ Every link is required for the chain to hold, which is why `UsesModel` is exactl `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. +#### 11.1.1 The chain has more links than it used to + +The walk is now: 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 (§7.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. §9.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. + ### 11.2 URIs are 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 8 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 (§9.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. + +#### 11.2.1 Credential material is not addressable + +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. + ### 11.3 Promotion needs its own authorization 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. +#### 11.3.1 Promotion has a second door + +`PromoteModel` is not the only way the model behind a deployment changes. A `FollowsRef` binding (§8.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. + +#### 11.3.2 Fallback changes what answers, not who may ask + +`FallBackTo` (§8.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 §7.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. + ### 11.4 A digest is not a signature `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 (§9.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. §10.1's card and §10.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. + --- ## 12 Profiles and conformance units @@ -503,6 +614,12 @@ Promotion changes behaviour without changing structure. Nothing in the address s 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 that runs one fixed model and describes it claims **AI-Base** and stops. A gateway that calls a hosted model claims **AI-Base**, **AI-Invoke** and **AI-Federation**. A plant MLOps node that mirrors a corporate catalogue and stages models onto controllers claims **AI-Catalogue** and **AI-Import** and may never call `Invoke` at all. None of these is a partial implementation of the others, and 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. + ### 12.2 Facets | Facet | Requires | From 77d68ff81ffec7b2b146abaf39b2b9b77a976373 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 3 Aug 2026 13:25:38 +0200 Subject: [PATCH 4/4] Require a Mandatory member to be documented, not just its type The both-directions cross-check only compared type NAMES, so a whole Method and three Mandatory members shipped undocumented: a Server is obliged to implement them and a client has nothing to read. ListModels, ModelSourceType.SourceId, and EvaluationRunType's RunId, EvaluatedModel and Metrics. The forward direction now checks member granularity for Methods and Mandatory members, and stops there: demanding prose for every Optional member would produce padding rather than documentation. All five are now documented, including why ListModels takes a filter - a public catalogue holds more models than any client wants to page through, so a Method that could only return everything would be unusable against exactly the sources clause 8 exists to reach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa --- .../ai-deployment/OPC-UA-AI-Deployment.md | 6 ++++++ .../ai-deployment/tools/validate_local.py | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md index 3c0a958..dae20e5 100644 --- a/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md +++ b/metaverse-specs/ai-deployment/OPC-UA-AI-Deployment.md @@ -433,6 +433,8 @@ A deployment whose `InferenceLocation` is not `OnServer` executes somewhere the | `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. + ### 8.2 The wire contract, and the credential that is never a secret `ApiDialect` (`ApiDialectEnum`, `ns=2;i=3007`) is `OpcUaInference`, `RestChatCompletions`, `OpenInferenceProtocol`, `TensorRemoteProcedure`, `EmbeddedRuntime` or `Proprietary`. @@ -469,6 +471,8 @@ This is the question a plant asks that an inference API does not answer, because `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. + `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. ### 8.5 Where the data goes @@ -550,6 +554,8 @@ This is the point at which §11.1's requirement that `Digest` be Mandatory stops `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. diff --git a/metaverse-specs/extras/ai-deployment/tools/validate_local.py b/metaverse-specs/extras/ai-deployment/tools/validate_local.py index c93ca59..b949d21 100644 --- a/metaverse-specs/extras/ai-deployment/tools/validate_local.py +++ b/metaverse-specs/extras/ai-deployment/tools/validate_local.py @@ -635,6 +635,27 @@ def check_spec_crossref(m: Model) -> None: 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