Skip to content

Let a Server actually use a model, not just describe one - #50

Merged
marcschier merged 6 commits into
vision-restructurefrom
ai-expansion
Aug 3, 2026
Merged

Let a Server actually use a model, not just describe one#50
marcschier merged 6 commits into
vision-restructurefrom
ai-expansion

Conversation

@marcschier

Copy link
Copy Markdown
Owner

Stacked on #49 — review that first, or read this diff against vision-restructure.

#49 gave the AI model its own specification. It was descriptive only: it 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, what proved it was entitled to send them, or what to do when the far end stopped answering.

So the property the whole model is organised around — InferenceLocation, where the model runs — pointed at a capability that could not be implemented from anything the model said.

Three clauses fix that. A fourth covers what the nameplate never answered.


The invariant the invocation surface is built on

Invoke's 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 not an aspiration. It is how model serving already works: runtimes that run on a single workstation and the hosted services they mirror expose the same contract, differing only in addressing and authentication. The specification already claimed that InferenceLocation "changes the trust boundary and nothing else"; this makes the claim load-bearing rather than decorative.

Clause 7 — Inference

The payload stays opaque (ByteString + ContentType). What goes into a model and comes out is domain vocabulary — an image and detections, a spectrum and a fault class — and an envelope that typed it would need extending by every domain that ever adopted it.

What is fixed is everything around the payload, because none of it is domain-specific and all of it gets got wrong when left to implementers:

ModelUsed reports the model that actually answered. Not the one the deployment names when you look. A fallback answers from a different deployment, and a FollowsRef binding can be repointed — both move it between the call and the read. The deployment's current model is then the wrong answer to "what produced this result", and wrong in the direction that looks plausible. The provenance chain walks ModelUsed.

FinishReason distinguishes finished from truncated. A client branching only on the StatusCode accepts a Length response as final, because nothing failed.

An unsupported parameter is rejected, never ignored. This costs implementers something and is worth it: a caller whose determinism seed was silently dropped believes its results are reproducible, and there is no later point at which it can discover otherwise.

Accounting is not in tokens. UsageDataType carries UnitKind plus input/output/total. A token is one unit among several; a field called InputTokens on a model that consumes images or audio seconds is either empty or lying.

Plus InvokeAsync and InferenceJobType for work that does not finish while the caller waits — a batch scored overnight, an analysis over recorded data. Modelling that as a blocking Method would hold a Session open for hours and lose the work if it dropped.

Clause 8 — Consuming a model hosted elsewhere (the priority)

ModelSourceType carries what a URI cannot: ApiDialect, AuthenticationKind, Capabilities, Reachability.

CredentialReference is a name, never a secret. The Server may not expose credential material through any Attribute. Stated as a prohibition because the address space is browsable, subscribable and historisable — a secret placed there is not merely readable, it is archived.

What the line does when the WAN drops. No inference API answers this, because an inference API can assume its caller will wait. A line cannot. FallbackPolicy is Mandatory: Fail (the safe default — a caller told nothing happened can decide for itself), HoldLast (only where stale is safe, and staleness must be visible), or FallBackTo. A fallback must report the substituted model in ModelUsed; one that answered silently would break provenance exactly when it matters most. Cycles are rejected at configuration time rather than discovered at the moment of failure.

Throttled is separated from Unreachable deliberately. They look identical from outside and call for opposite responses — failing over a throttled endpoint moves load onto a weaker model for no reason.

Where the data goes. Encryption answers who can read data in flight, not where it went, and the second is what a plant is actually asking. DataJurisdiction and EgressPermitted are Mandatory. RetainsInput has no "unknown": a Server that cannot establish the answer must report true, because the assumption that keeps data in is the one that is safe to be wrong about.

Clause 9 — The catalogue and the bridge

Every model catalogue has the same shape: publishers own namespaces, models and datasets are sibling resources, versions are immutable, mutable names point at versions. That shape is a registry — so this does not invent one. The catalogue types are domain extensions of OPC UA — xRegistry.

Two things then fall out of the base type instead of being designed:

  1. ResourceType is a FileType — a staged artefact streams through the inherited Open/Read/Close. Staging needs no new transport.
  2. It already carries ExternalReference — 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's InferenceLocation could not otherwise reach the source. Staging is the one moment a substituted artefact would enter the system, so that is where the digest is verified — and where Cancel must discard a partial transfer rather than leave an unverified file with a plausible name.

Clause 10 — Whether it may be used at all

ModelCardType with IntendedUse and Limitations both Mandatory — a card listing only capabilities is marketing, and the failure modes are the half a commissioning engineer needs. EvaluationRunType where the threshold travels with the metric: an accuracy of 0.94 means nothing alone, and a reviewer a year later cannot recover what "good" meant. DerivedFrom as a reference, because lineage is walked — a defect in a base is a defect in every descendant.


Costs, stated plainly

This model now requires OPC UA — xRegistry. That spends the "exactly one RequiredModel" property #49 advertised, and it moves the own namespace from ns=1 to ns=2. Taken deliberately: a model catalogue is a registry, and defining a second one would leave two incompatible ways to describe the same artefact. The dependency does not reach consuming specifications — Vision still binds by a plain NodeId Property and is unaffected.

Both validators hardcoded ns=1 while reading this NodeSet. Vision's would have resolved nothing and reported success — a silent pass, the worst failure mode. Both now derive the index from NamespaceUris, and that is one of the mutations tested below.

No NodeId moved. JobId and LastError moved up to AiJobType; their ids are reserved, not reclaimed. Against #49: 0 moved, 141 added, 2 removed in place.

Verification

Seven mutations, each caught, artifacts restored byte-exactly:

Mutation Caught by
A Mandatory federation member downgraded clause 8 invariant
The egress flag downgraded clause 8 invariant
Invoke stops reporting which model answered clause 7.2 invariant
A secret smuggled into the address space clause 8.2 name check
A registry placeholder left unnarrowed clause 9.1 invariant
The job base detached from Part 10 job invariant
The namespace order changed header check

Determinism byte-identical; all three tree validators, links, section refs and YAML/JSON green.

On vendor neutrality

A commercial AI platform's API surface was researched to shape the client-facing contract, and is named nowhere in the document. Only convergent practice was adopted — the (publisher, name, version) triple, usage accounting, a finish reason, retry-after. Explicitly rejected: resource-manager URL patterns, vendor request-id headers, SKU strings, a pass-through extension header, vendor geography concepts.

ApiDialectEnum.OpenAiCompatible was renamed RestChatCompletions for the same reason: the literal named a company, and an enumeration in a standard should describe the contract rather than advertise whoever published it first.

Version pinning is modelled structurally — an immutable version id versus a mutable ref, as xRegistry does — rather than as the vendor's upgrade-policy enum. 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 checkable.

marcschier and others added 2 commits August 3, 2026 11:42
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
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
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
@marcschier

Copy link
Copy Markdown
Owner Author

Second pass pushed — the clauses that already existed were left describing a smaller model than the one that now exists, so they have been deepened rather than left as-is. The document is now ~10,300 words.

It also caught a defect I introduced in the first pass: §5.6 had landed between §5.1 and §5.2. That is the out-of-document-order failure this repository has been bitten by before, and no CI check catches it — check_section_refs.py verifies that references resolve, not that headings ascend. Fixed, and every heading is now verified in order.

Substance added, rather than padding:

  • §5.2Quantization is not a packaging detail. A quantized model 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 — 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 — why State and Reachability are separate values. Ready + Unreachable is a network fault; Faulted + Reachable points at credentials or a contract mismatch. Collapsing them loses exactly the distinction a commissioning engineer needs.
  • §6 — the joins to the consuming half, and the plain statement 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 resolver policy now governs seven URIs, and applying it to a staging import after transfer is not a control. FollowsRef is promotion's second door. 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 — 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.

All 9 checks green; determinism byte-identical; no NodeId moved.

marcschier and others added 3 commits August 3, 2026 13:17
Critical: the namespace migration moved NodeIds and forgot BrowseNames.
A BrowseName is namespace-qualified, and its index is independent of the
NodeId's. T()/OWN_NS was threaded through every NodeId, but the
BrowseName prefix stayed hardcoded "1:" - which after the xRegistry
RequiredModel means index 1 is xRegistry. So all 201 own nodes browsed as
xRegistry:ModelType while their NodeIds said ns=2: browse paths resolve
against the wrong model, and the names collide with a namespace in which
none of them are defined. Schema Registry, which this generator names as
its precedent and has the identical layout, uses 2: throughout.

Nothing in the validator noticed, so it now checks BrowseName indices
directly - mutation-confirmed to catch the exact regression.

Annex A had the same half-migration and is declared authoritative for
identifiers, so the two normative sources disagreed: one row printed
ns=1;i=1005 and ns=2;i=1006 for the same model. A dead ns=1 branch in
_dt_name also meant DataType columns rendered raw ids instead of names.

The xRegistry narrowing did not narrow. ModelRegistryType declared a NEW
<ModelPublisher> placeholder rather than overriding the inherited
<Group>, and an InstanceDeclaration is overridden only by one with the
SAME BrowseName - so the inherited placeholder stayed fully open beside
it and a conformant registry could still organize any GroupType at all.
The reference type had silently become HasComponent too. Now overridden
properly, by name and with Organizes.

That forced a real modelling decision rather than a rename: a publisher
holds both models and datasets, but <Resource> can be overridden only
once. AiResourceType is an abstract base of both, so the single override
admits exactly the two. The validator's check was BrowseName-blind, which
is why it passed a narrowing that narrowed nothing; it now checks the
name, the type and the reference, and all three mutations are caught.

Also: a dangling 5.10 introduced by this PR, invisible because a bare
section reference resolves against any document the file cites.

Merged from vision-restructure, keeping the expanded overview figure with
the corrected arrow directions, and restoring the StatusCode table and
transition table that the merge dropped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
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
@marcschier

Copy link
Copy Markdown
Owner Author

Review response — Critical + 3 High fixed

Critical — the namespace migration moved NodeIds and forgot BrowseNames. A BrowseName is namespace-qualified and its index is independent of the NodeId's. T()/OWN_NS was threaded through every NodeId, but the BrowseName prefix stayed hardcoded "1:" — and after the xRegistry RequiredModel, index 1 is xRegistry. So all 201 own nodes browsed as xRegistry:ModelType while their NodeIds said ns=2: browse paths resolve against the wrong model, and the names collide with a namespace in which none of them are defined. Schema Registry — which this generator names as its precedent, with the identical layout — uses 2: throughout.

Nothing in the validator noticed, so it now checks BrowseName indices directly, mutation-confirmed to catch that exact regression.

Annex A had the same half-migration and is declared authoritative for identifiers, so the two normative sources disagreed — one row printed ns=1;i=1005 and ns=2;i=1006 for the same model. A dead ns=1 branch in _dt_name also meant DataType columns rendered raw ids instead of names.

The xRegistry narrowing did not narrow. ModelRegistryType declared a new <ModelPublisher> placeholder rather than overriding the inherited <Group> — and an InstanceDeclaration is overridden only by one with the same BrowseName, so the inherited placeholder stayed fully open beside it and a conformant registry could still organize any GroupType at all. The reference type had silently become HasComponent too.

That forced a real modelling decision rather than a rename: a publisher holds both models and datasets, but <Resource> can be overridden only once. AiResourceType is an abstract base of both, so the single override admits exactly the two. My check was BrowseName-blind, which is why it passed a narrowing that narrowed nothing; it now checks the name, the type and the reference — all three mutations caught.

A dangling §5.10 introduced by this PR, invisible because a bare section reference resolves against any document the file cites.

The both-directions check only compared type names, so a whole Method and three Mandatory members shipped undocumented — ListModels, ModelSourceType.SourceId, and EvaluationRunType's RunId/EvaluatedModel/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.

9/9 checks green; determinism byte-identical; 0 NodeIds moved.

Not fixed

Six Medium findings remain for a follow-up: §9.4's digest gate resting on Optional members; §11.1 and §11.1.1 describing two different provenance chains; §5.6 citing a §11.3 requirement §11.3 does not make; §11.3.2's fallback rule needing an ordering DataJurisdiction does not define; §9.3's Auto rationale sitting awkwardly against §5.4.1 and §8.2; and some duplicated prose. None is a correctness bug in the model, but several are real gaps in testability and worth closing before publication.

@marcschier
marcschier merged commit 96ff766 into vision-restructure Aug 3, 2026
9 checks passed
@marcschier
marcschier deleted the ai-expansion branch August 3, 2026 12:02
@marcschier

Copy link
Copy Markdown
Owner Author

Polish pass — and one thing a reviewer should not discover on their own

⚠ The NodeIds were renumbered deliberately

The member id space had gaps at 6036, 6042 and 61046106. Those existed only as residue: two members moved up to AiJobType, and three placeholders that turned out to narrow nothing were withdrawn. The generator explained them with comments like "6036 held JobId, now inherited".

A reader implementing this specification today should not be able to tell any of that happened. The model is unpublished, so the ids are now contiguous — 6001..6145, 145 members, none lost.

The diff against the previous commit is a full renumber (110 moved), not the usual 0-moved. No released artefact is affected, and no member was added or removed — but it is a deliberate departure from the append-only rule this stack has otherwise held to throughout, so it is called out rather than left to be found.

§1.2 "Why this is a separate specification" is gone

Its third paragraph duplicated §4.2 outright — the same rule stated twice. Its second defended the document existing, which a standalone specification does not need to do. Only the first said anything about the model, and that is now in §1.1 where the motivation lives.

While reading it I also found §1.3 carrying two bullets that both said training is out of scope. Merged.

§11.1.1 was titled "The chain has more links than it used to"

A changelog sentence in a heading. It now says what the chain is.

Five flows gained diagrams

A call and what comes back (§7.2), async submit and subscribe (§7.6), the far end failing and a fallback answering (§8.4), federate-vs-stage with the digest gate (§9.3/9.4), and the provenance walk (§11.1).

Every arrow direction was checked against the generator's ReferenceType definitions rather than against intuition — the review of #49 found both arrows in the overview figure drawn backwards against their own definitions, which is a mistake that is very easy to make and very hard to notice.

Annex D — an ONNX model, end to end

Clause 7 had acquired vocabulary from generative models — a capability called chat, accounting that is usually tokens — while what industry actually runs is a fixed-shape tensor model executed in-process. The annex works a gearbox condition classifier end to end to show the same envelope carries it with nothing bent:

  • TensorSignatureDataType as the entire interface description, and why a client that reads it finds a shape mismatch at configuration time rather than at 3 a.m.
  • a staging import with the digest verified — the whole integrity story for a model that will now decide whether a gearbox is failing
  • usage metered in samples, not tokens; an InputTokens field here would be either empty or a lie
  • Capabilities naming no chat capability at all — an absence that means nothing, because the list is open so a deployment describes what it does instead of scoring itself against a menu

It also states why no tensor wire format is standardised: raw buffers, protobuf, Arrow and npy are each right in some deployment, and a Server already speaking one to its runtime should not transcode into a format chosen here. ContentType names which is in use, which is what a client actually needs.

Also

A table of contents, 69 entries, all resolving. Document is now ~13,000 words.

9/9 checks green — including Mermaid, which is the only gate for the new diagrams since mmdc is not available locally. Determinism byte-identical; the three critical guards (BrowseName namespace, placeholder override, domain neutrality) re-confirmed by mutation after the renumber.

marcschier added a commit that referenced this pull request Aug 3, 2026
#50 was squash-merged into this branch at 12:02, which flattened
everything ai-expansion held at that moment. Two commits pushed after
that point were left behind:

  - the member-granularity cross-check, and the Method and three
    Mandatory members it found undocumented
  - the polish pass: contiguous NodeIds, 1.2 dropped, the delta voice
    removed from 11.1.1, five flow diagrams, the ONNX annex and the
    table of contents

The squash and the branch describe the same model by different histories,
so every AI-spec file conflicted; ai-expansion is strictly ahead on all
of them and its version is taken, then regenerated from the generator
rather than trusted as merged text.

Verified after the merge: member ids contiguous 6001..6145, all three
tree validators green, 772 internal links resolve, 0 unresolved clause
references.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
marcschier added a commit that referenced this pull request Aug 7, 2026
…#49)

* Give Vision a frame role for the flange

Vision's frame vocabulary was World, Base, Tool, Camera, Object, Other. It
had no value for the mechanical interface, so its own eye-in-hand picking
example labelled the flange `Tool` and the frame tree contained no tool
centre point at all.

That is not a naming quibble. The hand-eye calibration in Annex F resolves
camera to flange, and a pick has to reach the TCP. With one `Tool` role the
example could not carry the offset between them: adding a second frame for
the real TCP would have made `Tool` ambiguous inside one tree. Vision 5.8
describes this exact failure - "the pose is correct and the robot moves to
the wrong place, because the two ends disagreed about what the numbers were
relative to" - and the annex reproduced it.

`MechanicalInterface` is added and the literals are renumbered to match Robot
Intent, which had it right. Renumbering is free today and permanent after
publication: enum values live in EnumStrings, so no NodeId moves - the CSV
diff is 0 changed, 0 removed, 0 added for both models.

The example descriptor gains a `GripperTcpFrame` so the picking example
carries the flange-to-TCP offset explicitly rather than leaving a consumer to
assume it, and says why the two frames are distinct. Annex F, both addenda
and the overlays regenerate from it.

Vision 5.12 gains the three pose rules it was missing beside Robot Intent's:
right-handedness, quaternion normalisation, and the statement that this model
defines no default frame. The last one matters most - the same empty
`FrameId` means "not actionable" here and "default work frame" there, so a
transcoder written by someone who read one specification would have produced
a pose in the wrong frame.

Ten bare "5.10" references cited DataTypes when the rules they name - corner
datum, distortion ordering, frame precedence, the covariance sentinel - are
all in 5.12. They passed CI because check_section_refs.py requires a literal
section sign to see a reference at all.

Both specifications now carry a mutual interop annex, which neither did: they
never referenced each other despite being deployed on the same robot. Vision
Annex I and Robot Intent Annex E fix which frame tree decides, how FrameId
values correspond, that roles map by name and never by integer, how poses
transcode across the covariance field, and that a grasp pose resolving only
to the mechanical interface is refused. Annexes only - no NodeIds, no NodeSet
dependency, and both models keep base OPC UA as their only RequiredModel.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Give the AI model its own specification

Vision had grown a model nameplate, a dataset, a deployment and a learning
job - 30 members and ten types - none of which mention a camera. A process
soft sensor needs all of it and none of a lens, so leaving it there would
have obliged every other domain either to declare a vision model as a
RequiredModel or to define the whole thing again, at which point the
provenance question stops having one answer.

metaverse-specs/ai-deployment/ is domain-neutral by construction: its
validator fails the build if a type name acquires a domain term, and the
check is CamelCase word-boundary matched so Framework is not a false hit.
DeploymentStateEnum replaces the VisionEndpointStateEnum the deployment
had borrowed - same five literals, no dependency.

The join is a facet precondition, not a RequiredModel.
InferencePipelineType.Deployment was already a plain NodeId, so Vision's
namespace index does not shift and a Server that publishes cameras and
verdicts without ever naming a model is still conformant to VIS-Base.
VIS-Inference-* and VIS-Learning require AI-Base, and say so.

No Vision NodeId moves. The vacated blocks - 6107..6136, 6177..6190,
6193, 6195 - are reserved rather than reclaimed, because reusing them
would renumber all 45 members declared after them. The CSV diff is
61 removed, 0 added, 0 moved.

Both validators now cross-check specification against model in both
directions. That found 16 real gaps in Vision: enum literals and
structure fields the model declared and the prose had truncated with
"and so on". Mutation-tested in both directions on both models.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Stop restating the learning loop in two places

Clause 9.5 carried StartCollection, StopCollection, TriggerTraining and
PromoteModel with their StatusCodes, and 9.6 carried the state model -
all of which LearningJobType owns and OPC UA - AI Deployment and Learning
now states. Two documents stating one transition table is two places for
it to be wrong, and the wrong one is found by an implementer rather than
by a validator.

9.5.1 keeps only the vision-specific half: the join between a correction
submitted through VisionFeedbackType and the job that consumes it. The
distinct PromoteModel authorization is deliberately stated in both
documents, because it is the one rule where a reader of either alone
would reach the wrong conclusion.

The both-directions cross-check then found a real defect the old prose
had hidden. Clause 9.2 was titled "The three purposes" and described
three; VisionFeedbackPurposeEnum declares four. Trigger had never been
documented - it only appeared to be, because a "| From | Trigger | To |"
table header in the deleted state model satisfied the word match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* 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

* 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

* 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

* Fix what the review found in the AI split

Critical: the prose described a root object the model does not have.
VisionRootType lost Models and LearningJobs when the AI cluster moved,
but 4.2 still listed them normatively, 5.3 still said "five folders", and
the 5.2 figure still drew them. A Server following 4.2 literally would
instantiate two folders no ModellingRule permits, and a client following
5.3 to decide "is this Server doing AI" would browse for a folder that
can never exist. The figure now shows the AI cluster where it actually
lives - under AiRootType, beside the Vision object, not inside it.

Both worked examples had the same fault and a worse consequence: they
parked AI instances in those folders and never instantiated AiRootType at
all, so the only executable demonstration of the new seam could not
satisfy the AI-Base facet that VIS-Inference-* and VIS-Learning now
require. They now build a proper AI root under the Server object.

The learning loop lost content in the move that landed nowhere: the
StatusCode table, the idempotency of StartCollection/StopCollection,
TriggerTraining returning Accepted=false with Good, and the all-or-none
rule for a null Deployment. Worse, two transitions vanished - Promoted
had NO outgoing edge, so the loop could run exactly once, contradicting
the AI spec's own "the cycle repeats" two clauses earlier. Restored, with
the transition table it needs to be testable.

Both ReferenceType arrows in the AI overview figure were drawn backwards
against their own definitions. UsesModel is Deployment->Model and
TrainedOn is Model->Dataset; the figure said the opposite, so an
implementer following it would emit references that fail the exactly-one
rule. Vision's equivalent figure had them right, so the two documents
disagreed.

Vision still owned a rule it had handed over: 5.11 restated the
exactly-one UsesModel constraint verbatim, over a ReferenceType it no
longer defines - visible in the mangled "An DeploymentType", which is
what a mechanical rename leaves behind. Also fixed: a stale AiModelType
in a descriptor that a generated annex was publishing, an AiDeployment
reference shipping inside the NodeSet itself, an ObjectType count that
said 25 against an actual 21, and a scope sentence still claiming Vision
covers AI.

9.5.1 rule 1 required something no member could express - there was no
modelled path from a pipeline to a learning job, and the folder that used
to anchor one had just been removed. InferencePipelineType.LearningJob
makes it executable. Writing it beside its type renumbered
InferencePipelineType.Stop, so it is allocated at the true end of the id
space instead; 0 moved.

The encoding id counter was unguarded, so the 5009 freed by the moved
structure would have been silently reclaimed by the next structure added
- the same append-only violation the member ids were carefully protected
from.

Two validator blind spots, both proved by the reviewers and both now
mutation-tested: the AI validator rejected any struct field documented as
Type.Field, and the reverse cross-check silently SKIPPED a backticked
owner that no model declares - so a reference to a retired type passed.
That check now lives in Vision's validator, which is the only one with
both models loaded and therefore the only one able to tell an outside
type from a nonexistent one. Types from companion specifications are
allowlisted explicitly, so taking a new outside dependency is a
deliberate edit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* 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

* Let a Server actually use a model, not just describe one (#50)

* 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

* 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

* 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

* 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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Make the document read as a statement of the model, not its history

Five changes, none of which alter what the model can do.

The NodeId space had gaps at 6036, 6042 and 6104..6106 - residue of two
members that moved to a base type and three placeholders that narrowed
nothing - and the generator explained them with comments like "6036 held
JobId, now inherited". A reader implementing this today should not be
able to tell any of that happened. The model is unpublished, so the ids
are now contiguous: 6001..6145, 145 members, none lost. The diff against
the previous commit is a deliberate full renumber rather than the usual
0-moved.

1.2 "Why this is a separate specification" is gone. Its third paragraph
duplicated 4.2 outright, its second defended the document existing, and
only the first said anything about the model - which is now in 1.1 where
the motivation lives. 1.3 also carried two bullets that both said
training is out of scope.

11.1.1 was titled "The chain has more links than it used to", which is a
changelog sentence in a heading. It says what the chain is.

Five flows had no diagram: a call and what comes back, async submit and
subscribe, the far end failing and a fallback answering, federate vs
stage with the digest gate, and the provenance walk. Every arrow
direction was checked against the generator's ReferenceType definitions
rather than against intuition, because the last review found both arrows
in the overview figure drawn backwards against their own.

Annex D deploys an ONNX gearbox classifier end to end. Clause 7 had
acquired some vocabulary from generative models - a capability called
chat, accounting that is often tokens - while what industry runs is a
fixed-shape tensor model executed in-process. The annex shows the same
envelope carries it with nothing bent: tensor signatures as the entire
interface description, a staging import with the digest verified, usage
metered in samples rather than tokens, and Capabilities naming no chat
capability at all - an absence that means nothing, because the list is
open so a deployment describes what it does instead of scoring itself
against a menu.

It also says why no tensor wire format is standardised: raw buffers,
protobuf, Arrow and npy are each right somewhere, and a Server already
speaking one to its runtime should not transcode into a format chosen
here. ContentType names which is in use, which is what a client needs.

Plus a table of contents, 69 entries, all resolving.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Retitle: AI Model Management and Inference

"AI Deployment and Learning" named two of thirteen clauses. The document
also covers what a model is, calling it, calling one hosted elsewhere,
the catalogue and bridge, and governance and provenance. Management
carries the catalogue, import, lifecycle and governance; Inference
carries the call, which is the largest clause and the first thing an
implementer reaches for.

The type system was already name-neutral, so almost nothing in the model
changes. The namespace stays http://opcfoundation.org/UA/AI/ - it was
never named for the old title, and changing it would invalidate every
RequiredModel pin and every overlay reference for nothing. AiRootType,
AiJobType, AiResourceType and the AI-* facets are all prefixed Ai, not
AiDeployment, so they stand.

Exactly one identifier carried the old title: the well-known browse entry
point. AiDeployment becomes AiModelManagement, which is a BrowseName
change - NodeId 7001 does not move. The CSV diff is 0 moved, with that
one symbolic name replaced and nothing else touched.

Folders moved with git mv so history follows the document rather than
showing a delete and an add.

The riskiest part was not the rename but what reaches into this tree by
path: both Vision generators, the Vision validator, validate_all.py, the
tree README and sixteen references in the Vision specification. A missed
path there would not have failed loudly - the Vision validator returns
empty sets when the AI NodeSet is absent and then passes - so AI_TYPE_ID
was checked to be populated rather than inferring it from a green run.
It resolves 42 types and 208 ids.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Address PR #49 review feedback

Headings stated conclusions instead of naming topics, so a reader
scanning the contents met forty arguments rather than a structure.
Forty-three are now topic names - "Provenance", "Lineage", "Version
binding", "Data residency and egress" - and every statement a heading was
carrying has been restored as the opening sentence of the clause it
belonged to, so nothing was lost in demoting them.

The information model sections read as clipped one-liners. 5.2, 5.3 and
5.4 now open by saying what the type is for, who reads it and when, and
the member descriptions say what each is needed for rather than only what
it holds. The 5.4.1 table literally prefixed a "The question" column
before each answer; it is prose now.

5.4.2 compared State with Reachability, which clause 8 does not introduce
until much later - the reader met a comparison against something
undefined. It now describes State on its own terms, including why
Degraded sits between Active and Faulted, and defers the combination to
8.4 where the other half exists.

"The bridge" was doing two jobs. A Server reaches an external AI system
either by calling one per request (clause 8) or by obtaining a model from
a catalogue once (clause 9), and both get called bridging. 9.2 now
separates them before defining the import job, and the clause is titled
"The catalogue and model import".

"A card that lists only what a model can do is marketing" was polemic for
a specification; it now says the same thing about behaviour and
commissioning without the jab.

The decision record explaining why this was a separate specification was
already removed in an earlier commit; the section is gone rather than
rewritten, since a standalone document does not argue for its own
existence.

Contents regenerated for the renamed headings: 69 entries, all resolving.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Address PR #49 review feedback: payload size and scenarios

Invoke carried its payload as a ByteString, which is bounded three times
over - by MaxByteStringLength, by the negotiated MaxMessageSize and by
the Session's MaxResponseMessageSize - none of which this model gets to
choose. An image, a point cloud or a window of high-rate samples exceeds
them routinely, so the one call the specification defined could not carry
the input it was defined for.

Invoke is now the shortcut and BeginTransfer is the general path.
InferenceTransferType carries one chunked exchange whose Request and
Response are Part 5 FileType objects, so a client opens, writes or reads
in chunks of its own choosing, and closes. Nothing here invents a
transfer protocol: OPC UA already has one, every client implements it,
and a bespoke chunking scheme would be a second thing to get wrong.

MaxInlinePayloadSize is Mandatory and says the bound before a client
calls rather than after a rejection, because the three limits above are
not all visible from the client side and the smallest of them is the real
one. Clients that do not know their payload sizes in advance are told to
start with the transfer path.

The awkward case is not a large request - a client knows its own input -
but a request that fits producing a response that does not. Invoke
returns TransferRequired and Transfer for that, and a Server shall NOT
fail the call: the inference already ran, and failing would discard the
work and tell the caller nothing, leaving it to read an empty payload and
conclude the model returned nothing.

Streaming stays where it was. A transfer delivers one complete response
however large; 8.5 covers output a client wants as it forms, over a
Subscription or optionally a data channel.

Clause 4 Scenarios is new, and informative: eight arrangements this
specification supports, each with the flow that realises it, so a reader
can find the one resembling their installation before reading the
normative clauses that define its parts. They are not alternatives - a
plant running a model locally, calling a hosted one for a second opinion
and importing both from a catalogue is doing three at once.

Clauses 5 to 14 shift by one to make room. Every clause reference was
re-checked by hand rather than trusting the section-ref gate, which
resolves a bare reference against any cited document and would not have
caught a stale one.

0 NodeIds moved; 22 added.

* Show how the AI model maps onto the systems people actually integrate (#54)

* Add vendor implementation guides for the AI Model Management specification

The specification names ApiDialectEnum literals for what a contract does
rather than for whoever published it first, because a literal in a
standard should not be an advertisement. That is right for the normative
document and leaves an implementer holding a question it does not answer:
they are not integrating a REST chat-completions contract, they are
integrating Azure AI Foundry, or Bedrock, or the Triton server already in
the plant.

Eleven informative guides under extras, one per system, each with the
same nine sections so the index's coverage table means something. The
ninth - what this system does not tell you - is the one worth reading
first; a mapping document that lists only what maps is marketing.

Two findings the set makes hard to avoid noticing.

No hosted inference platform in it exposes an artefact digest. Not
Foundry, OpenAI, Bedrock, SageMaker, NIM, Triton or Vertex. Identity
everywhere is a name string. Hugging Face is the only content-addressed
system here and it serves no inference. So Digest stays empty against a
hosted endpoint, a Pinned deployment is pinned to a NAME with the
provider's policy rather than a hash holding it still, and the Stage
import mode is the only path in the set that ends with a digest the
Server computed itself.

AuthenticationKindEnum cannot express AWS SigV4. The guides map it by
what is STORED, which is what makes the member answerable across systems
whose handshakes have nothing in common: an IAM role is WorkloadIdentity
because no secret exists anywhere, static access keys are ApiKey. Whether
the enumeration should gain a literal is a separate decision - it would
be a NodeSet change - and is not taken here.

validate_examples.py re-derives from the NodeSet and the specification,
never from the guides, and is wired into validate_all.py because a
validator outside the aggregate silently stops running. It checks that
cited members exist, that every dialect and authentication literal is
exercised, that conformance units are real, that each guide links the
specification by relative path - which is also what makes a bare section
reference resolve, metaverse-specs being a strict tree - and that the
index agrees with the guides.

It earned its place twice while being written: it found that no guide
covered OpcUaInference, which is why there is now one for federating to
another Server implementing this specification, and it caught a member
this author had cited as ImportMode when the model calls it Mode.

Mutation-tested against all five checks. No model file is touched: no
NodeSet, no NodeIds CSV, no generator, so no version bump.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Correct the vendor guides against an audit of all eleven

Nine of the guides were written by sub-agents and only spot-checked. A
three-way audit against the sanctioned research and the specification
found errors in every category the audit looked for, including in the two
documents the lead wrote.

Conformance claims that were simply wrong:

AI-OffServer requires EndpointUri to name an authenticated, confidential
scheme. The NIM, Triton and KServe guides all claimed it while showing a
plain http:// listener as the default - which is the ordinary way a
self-hosted inference server sits on a plant network, so this was wrong
in exactly the case a reader would be in.

AI-Import requires AI-Catalogue; an import job with nothing to import
from is not implementable. SageMaker and Triton claimed AI-Import from
S3 URIs and a model repository respectively, neither of which is a
catalogue that declares a digest to verify against.

The federation guide claimed every conformance unit an upstream Server
implements is reachable through the link. A conformance unit describes
what THIS Server exposes; reaching a Server with a catalogue does not
give this Server AI-Catalogue, mirroring it does. Rewritten as three
lists by what the downstream Server actually has to build.

Model errors:

ApiDialect is Mandatory, and the Hugging Face guide left it empty on the
grounds that a catalogue speaks no inference contract. True, and not
available: it is Proprietary with EndpointDescriptionUri doing the work,
and the guide now says the cleaner arrangement is to model the Hub as a
ModelRegistryType under clause 10 rather than bending a ModelSourceType
around it.

AI-Invoke requires ModelUsed populated on every response, so the
federation guide's suggestion that a downstream Server may return null
when it cannot resolve the upstream model was not one of the options. It
publishes a ModelType for the substitute or it fails the call.

RateLimit.RetryAfter is a Duration; Retry-After carries seconds or an
HTTP date. Converted, not copied.

Citations: ModelUsed is 8.2.1 and not 8.5, which is Incremental results;
the payload bound and the transfer path are 8.2.4 and not the whole of
8.2. The index attributed the digest rules to clause 11 when 12.1 states
them, and asserted a shall-not the specification does not contain - what
12.1 actually requires is that a Server populate Digest where the
artefact is obtainable through ArtifactUri, which for a hosted model it
is not.

Unsupported vendor facts, removed rather than softened: ONNX file
metadata fields, GGUF header contents, Triton's storage backends and
repository-management extension, and Retry-After attributed to specific
platforms rather than to HTTP.

Triton's dialect split was a transport split. Clause 9.2 names the
literals for the contract, and Triton serves OIP over both HTTP and
gRPC, so both are OpenInferenceProtocol. TensorRemoteProcedure stays,
described as what it is for.

The index also called Hugging Face the only exception to no artefact
digest, omitting embedded runtimes - the one arrangement in the set where
the digest is something the Server computed rather than was told.

Comparative praise replaced with description throughout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Restore the blank line before a heading in the federation guide

Lost while rewriting the catalogue section for the conformance-unit
audit. MD022, and the only markdownlint finding across the set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Releases 0.3.0 and 0.4.0: what eleven real systems taught the AI specification (#55)

* Integrate what the vendor mappings taught the AI specification

Mapping one information model onto eleven real systems was a natural
experiment, and it found places where the model was unsatisfiable,
unguarded or ambiguous. None of them needs a NodeSet change.

AI-Invoke was unsatisfiable for every non-language model. The facet
required Usage "populated on every response" while no clause imposed
that obligation, and "empty" was unencodable: the three counts are
UInt64 and the model declares no optional fields, so leaving them
empty encodes as zero, indistinguishable from a metered zero. Five
guides claimed the facet while documenting empty Usage. 8.2.3 now
makes an empty UnitKind the not-metered sentinel, with the counts set
to zero and clients forbidden from reading them as measurements. The
unit carries the sentinel because the counts cannot.

Residency did not compose across a federation hop. A payload leaves a
deployment along exactly two modelled edges, FallsBackTo and Source,
and only the first was guarded; EgressPermitted's only shall binds on
InferenceLocation Cloud, so a cell-to-site EdgeOffServer hop escaped
it. 9.5 states the members are end-to-end rather than next-hop, and
obliges a Server federating to a peer to read its declarations and
never publish anything more permissive, defaulting fail-safe where it
cannot read them. This is the rule 12.3.2 already states for the
other edge.

Clause 13.1 described a plant MLOps node that "may never call Invoke
at all" while AI-Base required at least one deployment. The facet's
deployment requirements are now conditional on the Server exposing
one. AI-OffServer cites 12.2's scheme requirement instead of
restating it, so the two cannot drift.

Three ambiguities are resolved where the guides had each invented an
answer. ModelId carries the source's identifier verbatim, and
Publisher names who produced the model rather than who serves it --
two Servers were reading owned_by into Publisher and publishing
"azure" for a model Meta trained. AuthenticationKind classifies what
is stored rather than the handshake performed, which is what makes
SigV4 answerable without a new literal. ApiDialect classifies the
contract this Server speaks, and a catalogue-only source is
Proprietary with an EndpointDescriptionUri -- correct, not a
shortcoming.

The eleven guides are updated to cite these rules rather than derive
them, and three that had softened 12.2's shall into an operator
choice are corrected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Release 0.3.0: state why a digest is absent, and reach a registry from an import

Three changes the vendor mappings argued for that prose alone could not
make. All three append; NodeId churn is 0 changed, 0 removed, 5 added.

DigestProvenanceEnum, Mandatory on ModelType and Optional on
ModelResourceType. Digest is Mandatory so that its absence is uniform
and browsable, and that stays right -- seven of eleven systems cannot
fill it, and Optional would make "no digest" indistinguishable from
"does not implement digests". But empty then carried two meanings and
populated carried three, and the missing answer was doing visible harm:
five guides each prohibited a DIFFERENT wrong value -- a response
fingerprint, a resource name, a storage entity tag, a manifest hash, a
commit identifier -- and none could say what to publish instead. 12.1.1
states the prohibition once and supplies NotAvailable. Mandatory for the
reason Digest is, which 6.2 already gives: clause 12 depends on it, and
a rule that depends on an Optional member is one a conformant Server can
silently not satisfy. A StatusCode cannot carry it -- DeclaredBySource
and VerifiedOnStage are both Good.

12.1.1 also states that provenance does not strengthen by being
forwarded, on the ordering NotAvailable < DeclaredBySource <
ComputedByServer < VerifiedOnStage. Republishing an upstream peer's
VerifiedOnStage would claim a check this Server did not perform, and the
claim would be indistinguishable from one it had. Same composition
argument as 9.5 residency, applied to the other thing a federated
deployment forwards.

ModelImportJobType.Registry, with 10.2 requiring exactly one of Source
and Registry non-null. Source was Mandatory and typed ModelSourceType,
so nothing connected an import job to a ModelRegistryType -- the edge
the 4.4 and 5.1 diagrams draw. A Server importing from a catalogue had
to invent a source wrapper around its own registry. Source keeps its
Mandatory rule and takes null on the registry path, the idiom
TargetDeployment already uses, so no existing member changes.

ModelType.Name is constrained rather than retyped: its Text shall be the
source's name carried across unchanged, translations may be added for
display, and the Text is not reformatted. Retyping to String would have
been cleaner and is breaking; the property wanted -- two Servers
fetching one model from two mirrors produce the same string -- is
obtainable by constraining the field.

Version and PublicationDate bump to 0.3.0 / 2026-08-05, picked up by the
vision overlays automatically because build_examples.py derives both
from the AI generator rather than pinning them.

The eleven guides state their DigestProvenance value and cite 12.1.1
instead of each re-arguing the prohibition. The index reads the member
as the axis the set varies along: evidence tracks custody, which is why
an embedded runtime -- a local file the Server can simply hash -- is the
one arrangement here that produces evidence without vendor cooperation.

Adds CHANGELOG.md, where this repository keeps release history so the
specification does not have to narrate it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Stop sending a homeless identifier to a member that cannot hold it

12.1.1 correctly forbids a non-content identifier in Digest and then
disposed of it into ArtifactUri or ProvenanceUri. Those are scalar
String Properties on ModelType -- one artefact, one value. A serving
configuration fingerprint is per-response and per-deployment, so the
disposition is unimplementable for the case it was most needed in, and
five guides were left telling an implementer not to publish a value the
specification had just promised a home for.

The narrowed rule says what is true: an identifier that LOCATES the
artefact or its provenance record belongs in those members, and one
that identifies something else has no member in this model. Recording
that a datum has no home is better than filing it where a client reads
it as an artefact digest.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Release 0.4.0: model what eleven real systems publish and this had no member for

The same eleven mappings, read with a different question. 0.3.0 asked
which rules were wrong; this asks what these systems do that the model
cannot represent at all. The answer had a consistent signature: a guide
NAMES a vendor field and then maps it nowhere, and the omission is
invisible because nothing is missing from the guide's own tables.

Twelve appended Properties, no new DataTypes. NodeId churn is 0 changed,
0 removed, 12 added.

A model had no time on it. Six of eleven return a vintage in the call a
Server populates ModelType from, and every guide dropped it. The
asymmetry made it plain: DatasetType.CreatedAt exists with 6.3
defending it, and a catalogue resource inherits CreatedAt/ModifiedAt
from xRegistry, so a model known through a catalogue had a vintage and
the same model federated from an endpoint did not. LastModifiedAt is
the load-bearing one: 9.3's FollowsRef lets the artefact change with
nothing else changing, and 12.3.1 requires repointing to be an
authorization-bearing act while pointing at a job record that a
source-side move never produces.

A model had no end to it. Bedrock alone publishes legacyTime and
endOfLifeTime -- one vendor of eleven, and in the model anyway, because
vendor count is the wrong measure for an industrial specification. On
that date the deployment does not degrade, it stops; FallbackPolicy
fires, and where that is FallBackTo the line keeps producing while
something outside the qualified configuration answers. Every other
availability facility here copes after the fact. This is the only one
whose value is a date in the future.

The asynchronous path could not carry a large payload. InferenceJobType
had no TransferRequired and no Transfer, so InvokeAsync was bounded by
exactly the limits 8.2.4 says this model does not choose -- while 8.6
motivated it with the requests most likely to exceed them, and 13.2
claimed a parity that did not exist. 8.6.1 separates two problems that
had been collapsed: a result too large to carry, which the transfer
pair solves, and data that never needed to move, which it does not.
Five guides had discarded the vendor URI mechanism on the ground that
it is not a chunked transfer; they were right, and there was nothing
else to compare it to.

A deployment did not say what to send it. Ten of eleven publish a
request contract; the model captured the tensor half only, which is why
AI-Signatures was unclaimable by every hosted platform. The vocabulary
existed and was out of reach behind an explicit assertion in 9.2 that
these members never affect how a client calls this Server. 6.4.2 puts
ApiDialect and EndpointDescriptionUri on DeploymentType and 9.2's
sentence is rewritten rather than quietly dropped: naming which
contract opaque bytes must satisfy is not typing them, and 8.2's
argument for opacity is about contents.

A pinned deployment could move without anything changing. 12.1.1
forbade putting a serving-configuration fingerprint in Digest and
offered ArtifactUri or ProvenanceUri, which are scalar Strings on
ModelType and cannot hold a per-deployment value, so the datum was
thrown away. RuntimeIdentity holds it, and 9.3.1 states what makes it
matter: a change to it IS the observable change to the deployment that
9.3 says a pinned artefact cannot move without.

Three defects found by reading the specification against itself.
6.4.3 made Degraded normative for a deployment missing its
LatencyBudget while no measured latency existed anywhere, so the rule
could not be observed to be satisfied or violated by any legal Server;
ObservedLatency supplies the input. 13.2's AI-InvokeAsync row claimed
size parity it did not have. 9.4's ListModels answered catalogue size
with a cap and no cursor, putting everything past MaxResults
permanently out of reach.

Nothing added is Mandatory. Each is governed by a conditional shall in
the clause that defines it, because a blanket Mandatory would oblige
nine of eleven Servers to publish a RuntimeIdentity they cannot obtain,
and a requirement no conformant implementation can satisfy is not a
requirement.

Breaking: Invoke and InvokeAsync gain a PayloadUri input and ListModels
gains a ContinuationPoint in and out. Argument lists are the Value of
an existing Variable, so no NodeId moves, but a client that calls
positionally will break.

Two tooling fixes fall out. validate_examples.py now reads Method
argument names from the NodeSet, so a guide citing PayloadUri is
checked rather than suppressed -- mutation-tested against a typo and an
invented name. And a member description holding paragraphs was ending
its own table row in the generated model reference, which stayed
invisible only while such a member happened to be last in its table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Define the profiles both clause titles have promised since 0.1.0

Clause 13 of the AI specification and clause 12 of Robot Intent are both
titled "Profiles and conformance units" and both defined facets only.
Neither information model changes, so neither version moves: profiles
publish through the base-UA ServerProfileArray and need no member.

A facet is a building block; a profile is a complete claim. The
distinction is commercial rather than technical -- a plant writes a
profile name into a purchase order, and enumerating nine facet names
correctly is not something a procurement document does reliably. Robot
Intent's 1.2 already depends on this without a word for it: a
mixed-fleet cell works only if two robots claim the same shape.

Four AI profiles. Inference Device, Inference Gateway, Model Catalogue,
Model Lifecycle. The first three were already in 13.1 as prose examples
-- a device running one fixed model, a gateway calling a hosted one, a
plant MLOps node that may never call Invoke -- so naming them cost
nothing but a name, and 13.1 now points at 13.3 instead of restating
them.

AI-Residency is INSIDE the gateway profile rather than optional to it.
Once inference leaves the Server, where does my data go has an answer,
and a gateway that cannot state it is the arrangement 9.5 exists to
prevent.

The Lifecycle profile is witnessed by none of the eleven mapped systems
and is defined anyway. All eleven are inference or catalogue systems;
none is a plant that trains, which is what clause 7 was written for.
Its absence was also why AI-Learning, AI-Dataset and AI-Stream looked
orphaned -- they belonged to a Server shape the document described
nowhere. AI-Stream stays in no profile, because 13.2 already says a
Server answering only through Invoke is conformant without it and a
facet genuinely optional to every shape should not be bundled into one.

Four Robot Intent profiles: Motion, Handling, Path, Mission. RI-Safety
sits in the baseline, which is the decision here most worth arguing
about. Clause 10 is explicit that nothing here is safety-rated and no
Method is a safety function; what it imposes is a duty to report what
the safety system enforces and refuse work exceeding it. An integrator
specifying a profile is entitled to assume a robot declines an intent
its safety configuration forbids rather than attempting it. The process
facets are deliberately in no profile: a welding robot is a Path Server
that additionally claims RI-Process-ArcWeld, and bundling the process in
would have produced one profile per process with no way to say the
motion underneath is the same.

Both documents gain URI tables. A profile name is for a human and
ServerProfileArray holds URIs, so without stated values two Servers
implementing one profile publish different strings and no client matches
either.

Both gain definitions for conformance unit, facet and profile. Each used
"facet" throughout and defined it nowhere, and in the AI document
"profile" collides with the typed profile of a consuming specification,
which is a payload vocabulary and unrelated to conformance.

Profiles are added AFTER the facet tables. 13.2 is cited fourteen times;
moving it would have churned every reference for nothing.

validate_examples.py gates profile names against the profile table, so a
guide naming a shape the specification does not define fails the way a
misspelled facet does. Mutation-tested against a typo and an invented
name. It checks the name and not the claim, which its docstring says
plainly: whether a guide's facets cover the profile it names is a
judgement over prose, and a checker guessing at that would fail on
wording rather than substance.

The eleven guides name their profile. Eight reach Gateway, two reach
Device, one reaches Catalogue, and none reaches Lifecycle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Separate a paragraph from the table it followed

The ServerProfileArray obligation sat on the line after the last facet
row, so markdownlint read it as a fourteenth row with one cell. My
pre-push check tested for a table glued after text and not for text
glued after a table; it now tests both directions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Say what this repository actually contains

The README explained how to contribute well and what exists badly. The
inventory was a Layout list of single-paragraph bullets -- the Data
Channels one ran to 1,900 characters -- carrying no version, no status,
and no link to any specification document. A reader could not get from
the README to a specification in one click, could not tell which of the
sixteen documents were under OPC Foundation review, and had no single
place saying what the whole effort is.

It is now one table per folder prefix, covering this repository and the
private submodule together, so the grouping is complete rather than
split by which repository a document happens to sit in today. That is
why wot appears at all: both its specifications are under review and the
tree has left, so a prefix-grouped list is the only place the reader
learns they exist. Columns are Specification, what it is and why it
exists, status, and documents -- markdown first, then Word.

Three specifications have no Word rendering and their cell says so with
an em dash rather than omitting the column, because the gap is worth
seeing: Vision, Robot Intent and AI Model Management have no clause map
under word-drafts/tools/specs/, which is what drives that build.

release_spec.py gains a third repair pass. It maintains this file when a
specification moves private -- capsuling the describing list item with
its original base64-encoded so a return restores it, and emptying links
into the moved tree -- and it understood list items and links, not table
rows. Without the pass the next release would leave a row advertising a
document that is no longer here.

Three things had to be right and were verified rather than assumed. The
row pass runs BEFORE the link pass, because afterwards every row already
holds a capsule around its own links and would be skipped, exactly as
the bullet pass skips such lines. The replacement keeps the row's cell
count, since collapsing four cells into two fails MD056. And only tables
whose last two columns are Status and Documents are touched, so a table
of another shape is left alone rather than rewritten into something the
function guessed at.

Return needed no new code: capsule_belongs_to already matches a capsule
by a link resolving into the returning set, and every row carries one.
Round trips for six specifications across four trees restore the file
byte-for-byte.

The five capsules for the specifications already under review encode the
table row they should become, not the bullet they used to be. Restoring
a 1,900-character bullet into a table cell would be worse than the gap
it filled.

The five reviewed specifications link to github.com/OPCF-Members rather
than to spec-drafts/ paths. The submodule is empty for everyone who is
not a member, so a relative link would fail check_links.py for them and
still open nothing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Keep the capsule markers inside the table row's outer pipes

markdownlint counts a row's cells without stripping HTML comments, so a
closing marker after the final pipe leaves the row ending in content:
MD055 read it as a missing trailing pipe and MD056 counted a fifth cell.
Both markers now sit between the outer pipes, which is invisible to a
reader and correct to the parser.

My pre-push check stripped the comments before counting, which is
exactly why it passed locally and failed in CI. It now counts them the
way markdownlint does.

Round trips still restore byte-for-byte; the encoded original is the row
between its outer pipes rather than after the first one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Trim the blank line at the end of the README

MD012 counts the newline after the final paragraph plus the file's own
terminator as two blank lines. My check read the file through
Get-Content, which drops that distinction, so it saw one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa
marcschier added a commit that referenced this pull request Aug 7, 2026
* Supply the motion verbs OPC 40010 leaves undefined

OPC 40010-1 describes robot topology in detail and defines no motion verbs
at all: its whole actuation surface is Start, Stop and loading a named
program. A client can discover everything about a robot's construction and
cannot ask it to move anywhere.

This adds OPC UA - Robot Intent, which supplies the verbs and nothing else,
so the two compose rather than compete.

The lifecycle is the harder half. A motion takes seconds and a pick takes a
minute, while OPC 10000-4 clause 5.12.2 discards a method result when the
Session ends "independent of the task actually performed at the Server" - so
a synchronous motion method loses the outcome of work that has already
physically happened. Submission therefore returns a Part 10 program instance
and the client watches it, which is the resolution OPC 10000-10 clause 4.1
already reaches for exactly this case.

Verbs are a DataType hierarchy rather than one Method each, so a single
submission and a mission step are the same shape and extension is subtyping.
Queueing is PLCopen MC_BufferMode and concurrency is VDA 5050 blockingType,
both adopted unchanged because both are already implemented everywhere and
no OPC UA specification defines either. Missions carry an immutable
committed base and a revisable horizon.

Orientation is a unit quaternion: OPC UA defines no quaternion type, and the
A/B/C fields of ThreeDOrientation carry no convention of their own. Annex C
carries the bidirectional conversion, including the clamp that stops a pole
orientation becoming a domain error.

Pick and Place reference Location nodes rather than naming a station in a
string, which would be a second definition of a fact the address space
already holds.

Clause 10 declares the interface non-safety-rated, restricts submission to
Automatic and Automatic External, and records that a stop request selects no
IEC 60204-1 stop category. Clause 8 says plainly that command authority
arbitrates between clients and is not the single point of control ISO
10218-2 requires.

The NodeSet is standalone on base OPC UA, following the Vision precedent;
OPC 40010-1 interop is an optional profile carried by a ReferenceType.

The validator re-derives from the committed artifacts rather than asking the
generator what it emitted, and cross-checks the specification against the
model in both directions. It was mutation-tested against thirteen defects,
including one introduced in the generator and fully regenerated so that the
NodeSet, the CSV and Annex A all agree with each other - which only the
specification invariants can catch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Browse encodings where the standard says they are

A DataTypeEncoding browses as "Default Binary" in namespace 0. The BrowseName
is standard, not model-defined, and every published companion NodeSet writes
it that way. Both generators were emitting 1:Default Binary.

This is not cosmetic. Tooling that resolves encodings by BrowseName cannot
find them: the UA-.NETStandard model source generator fails outright on such
a NodeSet, and the failure surfaces as a NullReferenceException with no
indication of which node caused it - it took a binary search over the node
list to find.

Both models are regenerated. No NodeId moves, because only the BrowseName and
the added SymbolicName change: Vision is nine lines, Robot Intent eighteen.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Bring the deferred and excluded capability into scope

Five things the specification did not do. Four are now defined; the fifth
turns out not to be a scoping choice at all.

Real-time was an exclusion and is now a division of labour. Trajectory
execution is expressible over request/response - the whole path is handed
over once and the robot's own motion kernel runs it, which is what
FollowJointTrajectory and the PLCopen buffered path blocks already do - so
TrajectoryIntentDataType, CartesianPathIntentDataType and ForceIntentDataType
are defined here. Streaming control is not expressible, so the model brokers
a channel instead: RealTimeChannelType describes and leases RTDE, EGM, FRI,
RSI, MotoROS2 or OPC UA FX and the samples never traverse OPC UA. That is the
same shape as the Vision model brokering a media endpoint rather than
carrying pixels.

Safety cannot be brought into scope as a rating, and the reason is
structural rather than editorial. OPC 10000-15 carries cyclic safety data
from a provider to a consumer; the consumer's request holds an identifier, a
monitoring number and one octet of explicitly non-safety flags, so a caller
has no channel through which to supply safety-rated arguments. Every safety
fieldbus expresses a safety command as a continuously asserted cyclic signal,
because the integrity argument rests on the fail-safe state that follows when
assertion stops - and a Method call has no defined behaviour when it stops
being called. So safety is in scope as awareness plus a duty: SafetyStateType
reports what the safety system enforces, clause 10.4 requires a Server to
refuse work that would exceed it, and the same clause says plainly that
neither of those makes anything safe. A Server may observe the safety system
and refuse on what it sees; it may never instruct it.

Robot construction was excluded on the grounds that OPC 40010 covers it. It
does not cover this part: OPC 40010-1 describes topology and axes and defines
no kinematic chain an IK solver could use, and no tool centre point at all.
RobotDescriptionType is therefore additive, and Annex B fixes which side
decides where both are present.

Process models are defined for all six applications, with fastening left
deliberately thin: OPC 40450 and OPC 40451 already own joining and
tightening, so FastenIntentDataType references a joint there rather than
restating torque strategies - the rule that already made Pick take a Location
node instead of a station string.

Mission branching follows IEC 61131-3 rather than a behaviour tree. Steps and
transitions with alternative and parallel divergence are the notation the
controller audience knows and have an IEC serialization; a behaviour tree
needs a tick runtime controller vendors do not provide. Transition conditions
reuse the base UA ContentFilter, so nobody has to write a parser for an
invented expression language, and an empty transition array leaves a mission
the flat sequence it was.

83 nodes added, 0 removed, 0 renumbered. The validator gained invariants for
the process hierarchy, the ContentFilter reuse and the mandatory safety
members, and the mutation suite grew from 13 defects to 17 - all caught, with
the tree restored byte-exactly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Make three claims true that the model did not support

The prose said TrajectoryIntentDataType carries PathTolerance,
GoalTolerance and GoalTimeTolerance "all MotionToleranceDataType".
GoalTimeTolerance is a Duration. An implementer decoding the structure
from the prose gets the wire format wrong at that field, and the
structure is not self-describing there. Lateness is one number, not a
pose deviation.

The 6.3 state diagram had Retriable --> Queued and gave Retriable no exit
to terminal, so execution resumed on the same instance. Clause 3 defines
Retriable as a state "from which execution does not resume", 6.6 says
Retry creates a NEW operation with the original left terminal, and 6.7
says a terminal Result shall not change thereafter. The model cannot
distinguish the two readings - Retriable to Queued is Halted to Ready,
which Part 10 permits - so a conformance test built from the model alone
rejects neither, and two implementations get incompatible operation
lifetimes.

FinalResultData and ProgramDiagnostic are Optional in
ProgramStateMachineType and were not promoted. So 6.7's "shall also be
reachable under FinalResultData" rested on a member a conformant Server
may omit, and 1.2's headline "auditable commanding" claim - which IS
ProgramDiagnostic and nothing else - was false against a legal
implementation. Both are now Mandatory on IntentOperationType, which is
the repository's stated remedy: a conformance unit that depends on an
inherited member promotes that member.

Written beside their type the promotions renumbered 39 members; they are
allocated at the end of the id space instead. 0 moved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Make refusal observable and the Part 10 promotions legal

Implementing this specification in the OPC UA .NET Standard stack surfaced six
defects. Each is a claim the document already made that the model did not let a
conformant Server keep.

Clause 6.2 orders six refusals and clause 5.8 calls the failure set small and
diagnosable on purpose, but SubmitIntent returned only IntentId and Operation.
A client could see that something had been refused and never which of the six it
was, so the ordering was unobservable and the failure taxonomy unreachable.
SubmitIntent, SubmitMission and Retry now return Accepted, Failure and Message,
clause 6.2 states that a Server returns Good and reports the refusal there
rather than substituting a Bad StatusCode, and OpenRealTimeChannel gains a
Message because clause 6.9 hid four grounds for refusal behind one boolean.

IntentOperationType declared ProgramDiagnostic as a Property of PropertyType
reached by HasProperty. OPC 10000-10 declares it as a Variable of
ProgramDiagnostic2Type reached by HasComponent, so the declaration added a
second member beside the inherited one instead of promoting it - which is why a
Part 10 client would not have found it where Part 10 says it is, and why the
first stack to generate the model deleted it. A promotion changes the
ModellingRule and nothing else; clause 6.1 now says so.

WaitIntentDataType.Signal read "an OutputSignal or other node", which clause
11.3 cannot check, and an unvalidated NodeId is the surface clause 11.3 exists
to close. It is now bounded to an OutputSignal under the controller or a
Boolean Variable under it, and clause 11.3 tabulates the expected type of every
NodeId-valued member rather than leaving "the expected type" to the reader.

Ready, ActiveIntent, ActiveMission and ControlOwner were in the model and in no
clause; clause 5.7.0 gives them normative meaning. Clause 6.9 now bounds
RequestedLease, which the lease rules never limited.

The NodeSet, the CSV and Annex A are regenerated; no previously assigned NodeId
moves.

* Make the capability declaration honest about its Method surface

Implementing the specification a second time surfaced three more places where
a conformant Server could tell a client something untrue.

Clause 9 already had three rules keeping the capability declaration honest, but
none of them said that a declared capability must come with the Methods that
make it usable. SubmitMission, UpdateMission, CancelMission and both real-time
channel Methods are Optional on IntentControllerType, so a Server could report
MissionsSupported true and omit SubmitMission entirely - which is exactly what
the first implementation did, and the contradiction only surfaced when a client
called something that was not there. A fourth rule and a table now fix which
Methods each declaration implies, and like the other three it is checkable by
browsing a running Server.

Clause 6.5 gave StopMode the PossibleStopModes vocabulary of OPC 40010-1 and
then said nothing about a Server that treats every value alike, so accepting the
argument and discarding it looked conformant. A Server must now either honour it
or treat every value as its single stop behaviour, and should say which - a
client that asks for OnPath and silently receives a QuickStop has been told
something untrue about how the cell came to rest.

Clause 6.4 left the stop mode of a superseded intent undefined, because an
Aborting submission names none. The Server chooses, should choose the most
urgent stop the cell tolerates since the successor is about to command motion of
its own, and should document which.

Prose only; the model is unchanged and regenerates identically.

* Let a Server state which facets it satisfies

Clause 12 defined conformance in terms of facets and then gave a Server
nowhere to say which ones it had. A client that wanted to know whether a
controller supported blending had to re-derive the whole of Table 12.2
from the address space itself, which is what the reference implementation
ended up doing.

That is worse than verbose. Several rows of the table are behavioural --
"the four blending buffer modes accepted and honoured", "the refusal
rules of 6.2", "the base immutability rules of 7.2" -- and no amount of
browsing settles them. Two clients deriving conformance independently
could therefore reach opposite conclusions about the same Server and both
be reading the specification correctly.

OPC 10000-5 already answers this with ServerCapabilitiesType.
ServerProfileArray: the Server states its profiles rather than leaving
them to be inferred. IntentCapabilitiesType now carries SupportedFacets
on the same principle, Mandatory, naming the facets of Table 12.2
verbatim.

Clause 12.2 now separates the two kinds of requirement it had been
mixing. Structural requirements are settled by reading -- the Outputs
folder, more than one ToolType, axes covering 0 to AxisCount - 1 -- and a
Server shall not list a facet whose structural requirements are unmet.
Behavioural requirements are the Server's attestation, and clause 9 says
what that means: listing RI-Blending while treating the buffer modes as
Buffered is the same false statement rule 3 already forbids, whatever
BlendingSupported says.

RI-Base requires SupportedFacets, since a conformance claim that cannot
be read is not a claim.

The member appends at i=6139; no existing NodeId moves.

* Make the Part 10 promotions real, and say what Pause means

Two defects, both found by implementing the draft and reviewing the
result.

The promotions of FinalResultData and ProgramDiagnostic were emitted with
namespace-1 BrowseNames. OPC 10000-3 says a subtype overrides an
inherited InstanceDeclaration by re-declaring a child with the same
QualifiedName, and a QualifiedName includes the namespace index -- so
1:ProgramDiagnostic never overrode 0:ProgramDiagnostic, it declared a
second member beside it and left the inherited Optional one in place.
That is the same failure the TypeDefinition and reference type were
already careful to avoid, reached by a different route, and it defeated
the entire point of promoting: clause 6.7's SHALL and the clause 1.2
auditable-commanding claim still rested on members a conformant Server
could omit. The generated client made it visible -- its accessor used
`new` to hide the base Part 10 one. Both now carry namespace-0
BrowseNames, using the same mechanism already applied to DataTypeEncoding
browse names for the same reason, and every other NodeSet in the
reference stack promotes inherited state-machine members unprefixed.

Clause 6.6 required Pause to suspend execution retaining position, but
the model defines no channel through which a Server can tell an actuator
to pause mid-motion. A Server whose controller offers no pause could
therefore only publish Suspended and hope -- and Part 10 defines that
state as position retained, so an operator reading the HMI sees the arm
at rest while it finishes the move. The reference implementation did
exactly that until a review caught it.

The requirement stays, because stopping the robot is what Pause should
mean. What is added is the honesty rule the rest of clause 9 already
applies elsewhere: a Server that cannot suspend a running intent declares
PauseSupported false, may still stop its queue, and leaves the executing
operation Executing with Ready reflecting that nothing new is admitted.
Suspended is never reported while the robot is still moving. That makes
clause 9 four rules rather than three, alongside the BlendingSupported
rule it is modelled on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b

* Give the AI models their own specification, and let a Server use them (#49)

* Give Vision a frame role for the flange

Vision's frame vocabulary was World, Base, Tool, Camera, Object, Other. It
had no value for the mechanical interface, so its own eye-in-hand picking
example labelled the flange `Tool` and the frame tree contained no tool
centre point at all.

That is not a naming quibble. The hand-eye calibration in Annex F resolves
camera to flange, and a pick has to reach the TCP. With one `Tool` role the
example could not carry the offset between them: adding a second frame for
the real TCP would have made `Tool` ambiguous inside one tree. Vision 5.8
describes this exact failure - "the pose is correct and the robot moves to
the wrong place, because the two ends disagreed about what the numbers were
relative to" - and the annex reproduced it.

`MechanicalInterface` is added and the literals are renumbered to match Robot
Intent, which had it right. Renumbering is free today and permanent after
publication: enum values live in EnumStrings, so no NodeId moves - the CSV
diff is 0 changed, 0 removed, 0 added for both models.

The example descriptor gains a `GripperTcpFrame` so the picking example
carries the flange-to-TCP offset explicitly rather than leaving a consumer to
assume it, and says why the two frames are distinct. Annex F, both addenda
and the overlays regenerate from it.

Vision 5.12 gains the three pose rules it was missing beside Robot Intent's:
right-handedness, quaternion normalisation, and the statement that this model
defines no default frame. The last one matters most - the same empty
`FrameId` means "not actionable" here and "default work frame" there, so a
transcoder written by someone who read one specification would have produced
a pose in the wrong frame.

Ten bare "5.10" references cited DataTypes when the rules they name - corner
datum, distortion ordering, frame precedence, the covariance sentinel - are
all in 5.12. They passed CI because check_section_refs.py requires a literal
section sign to see a reference at all.

Both specifications now carry a mutual interop annex, which neither did: they
never referenced each other despite being deployed on the same robot. Vision
Annex I and Robot Intent Annex E fix which frame tree decides, how FrameId
values correspond, that roles map by name and never by integer, how poses
transcode across the covariance field, and that a grasp pose resolving only
to the mechanical interface is refused. Annexes only - no NodeIds, no NodeSet
dependency, and both models keep base OPC UA as their only RequiredModel.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Give the AI model its own specification

Vision had grown a model nameplate, a dataset, a deployment and a learning
job - 30 members and ten types - none of which mention a camera. A process
soft sensor needs all of it and none of a lens, so leaving it there would
have obliged every other domain either to declare a vision model as a
RequiredModel or to define the whole thing again, at which point the
provenance question stops having one answer.

metaverse-specs/ai-deployment/ is domain-neutral by construction: its
validator fails the build if a type name acquires a domain term, and the
check is CamelCase word-boundary matched so Framework is not a false hit.
DeploymentStateEnum replaces the VisionEndpointStateEnum the deployment
had borrowed - same five literals, no dependency.

The join is a facet precondition, not a RequiredModel.
InferencePipelineType.Deployment was already a plain NodeId, so Vision's
namespace index does not shift and a Server that publishes cameras and
verdicts without ever naming a model is still conformant to VIS-Base.
VIS-Inference-* and VIS-Learning require AI-Base, and say so.

No Vision NodeId moves. The vacated blocks - 6107..6136, 6177..6190,
6193, 6195 - are reserved rather than reclaimed, because reusing them
would renumber all 45 members declared after them. The CSV diff is
61 removed, 0 added, 0 moved.

Both validators now cross-check specification against model in both
directions. That found 16 real gaps in Vision: enum literals and
structure fields the model declared and the prose had truncated with
"and so on". Mutation-tested in both directions on both models.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Stop restating the learning loop in two places

Clause 9.5 carried StartCollection, StopCollection, TriggerTraining and
PromoteModel with their StatusCodes, and 9.6 carried the state model -
all of which LearningJobType owns and OPC UA - AI Deployment and Learning
now states. Two documents stating one transition table is two places for
it to be wrong, and the wrong one is found by an implementer rather than
by a validator.

9.5.1 keeps only the vision-specific half: the join between a correction
submitted through VisionFeedbackType and the job that consumes it. The
distinct PromoteModel authorization is deliberately stated in both
documents, because it is the one rule where a reader of either alone
would reach the wrong conclusion.

The both-directions cross-check then found a real defect the old prose
had hidden. Clause 9.2 was titled "The three purposes" and described
three; VisionFeedbackPurposeEnum declares four. Trigger had never been
documented - it only appeared to be, because a "| From | Trigger | To |"
table header in the deleted state model satisfied the word match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* 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

* 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

* 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

* Fix what the review found in the AI split

Critical: the prose described a root object the model does not have.
VisionRootType lost Models and LearningJobs when the AI cluster moved,
but 4.2 still listed them normatively, 5.3 still said "five folders", and
the 5.2 figure still drew them. A Server following 4.2 literally would
instantiate two folders no ModellingRule permits, and a client following
5.3 to decide "is this Server doing AI" would browse for a folder that
can never exist. The figure now shows the AI cluster where it actually
lives - under AiRootType, beside the Vision object, not inside it.

Both worked examples had the same fault and a worse consequence: they
parked AI instances in those folders and never instantiated AiRootType at
all, so the only executable demonstration of the new seam could not
satisfy the AI-Base facet that VIS-Inference-* and VIS-Learning now
require. They now build a proper AI root under the Server object.

The learning loop lost content in the move that landed nowhere: the
StatusCode table, the idempotency of StartCollection/StopCollection,
TriggerTraining returning Accepted=false with Good, and the all-or-none
rule for a null Deployment. Worse, two transitions vanished - Promoted
had NO outgoing edge, so the loop could run exactly once, contradicting
the AI spec's own "the cycle repeats" two clauses earlier. Restored, with
the transition table it needs to be testable.

Both ReferenceType arrows in the AI overview figure were drawn backwards
against their own definitions. UsesModel is Deployment->Model and
TrainedOn is Model->Dataset; the figure said the opposite, so an
implementer following it would emit references that fail the exactly-one
rule. Vision's equivalent figure had them right, so the two documents
disagreed.

Vision still owned a rule it had handed over: 5.11 restated the
exactly-one UsesModel constraint verbatim, over a ReferenceType it no
longer defines - visible in the mangled "An DeploymentType", which is
what a mechanical rename leaves behind. Also fixed: a stale AiModelType
in a descriptor that a generated annex was publishing, an AiDeployment
reference shipping inside the NodeSet itself, an ObjectType count that
said 25 against an actual 21, and a scope sentence still claiming Vision
covers AI.

9.5.1 rule 1 required something no member could express - there was no
modelled path from a pipeline to a learning job, and the folder that used
to anchor one had just been removed. InferencePipelineType.LearningJob
makes it executable. Writing it beside its type renumbered
InferencePipelineType.Stop, so it is allocated at the true end of the id
space instead; 0 moved.

The encoding id counter was unguarded, so the 5009 freed by the moved
structure would have been silently reclaimed by the next structure added
- the same append-only violation the member ids were carefully protected
from.

Two validator blind spots, both proved by the reviewers and both now
mutation-tested: the AI validator rejected any struct field documented as
Type.Field, and the reverse cross-check silently SKIPPED a backticked
owner that no model declares - so a reference to a retired type passed.
That check now lives in Vision's validator, which is the only one with
both models loaded and therefore the only one able to tell an outside
type from a nonexistent one. Types from companion specifications are
allowlisted explicitly, so taking a new outside dependency is a
deliberate edit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* 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

* Let a Server actually use a model, not just describe one (#50)

* 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

* 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

* 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

* 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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Make the document read as a statement of the model, not its history

Five changes, none of which alter what the model can do.

The NodeId space had gaps at 6036, 6042 and 6104..6106 - residue of two
members that moved to a base type and three placeholders that narrowed
nothing - and the generator explained them with comments like "6036 held
JobId, now inherited". A reader implementing this today should not be
able to tell any of that happened. The model is unpublished, so the ids
are now contiguous: 6001..6145, 145 members, none lost. The diff against
the previous commit is a deliberate full renumber rather than the usual
0-moved.

1.2 "Why this is a separate specification" is gone. Its third paragraph
duplicated 4.2 outright, its second defended the document existing, and
only the first said anything about the model - which is now in 1.1 where
the motivation lives. 1.3 also carried two bullets that both said
training is out of scope.

11.1.1 was titled "The chain has more links than it used to", which is a
changelog sentence in a heading. It says what the chain is.

Five flows had no diagram: a call and what comes back, async submit and
subscribe, the far end failing and a fallback answering, federate vs
stage with the digest gate, and the provenance walk. Every arrow
direction was checked against the generator's ReferenceType definitions
rather than against intuition, because the last review found both arrows
in the overview figure drawn backwards against their own.

Annex D deploys an ONNX gearbox classifier end to end. Clause 7 had
acquired some vocabulary from generative models - a capability called
chat, accounting that is often tokens - while what industry runs is a
fixed-shape tensor model executed in-process. The annex shows the same
envelope carries it with nothing bent: tensor signatures as the entire
interface description, a staging import with the digest verified, usage
metered in samples rather than tokens, and Capabilities naming no chat
capability at all - an absence that means nothing, because the list is
open so a deployment describes what it does instead of scoring itself
against a menu.

It also says why no tensor wire format is standardised: raw buffers,
protobuf, Arrow and npy are each right somewhere, and a Server already
speaking one to its runtime should not transcode into a format chosen
here. ContentType names which is in use, which is what a client needs.

Plus a table of contents, 69 entries, all resolving.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Retitle: AI Model Management and Inference

"AI Deployment and Learning" named two of thirteen clauses. The document
also covers what a model is, calling it, calling one hosted elsewhere,
the catalogue and bridge, and governance and provenance. Management
carries the catalogue, import, lifecycle and governance; Inference
carries the call, which is the largest clause and the first thing an
implementer reaches for.

The type system was already name-neutral, so almost nothing in the model
changes. The namespace stays http://opcfoundation.org/UA/AI/ - it was
never named for the old title, and changing it would invalidate every
RequiredModel pin and every overlay reference for nothing. AiRootType,
AiJobType, AiResourceType and the AI-* facets are all prefixed Ai, not
AiDeployment, so they stand.

Exactly one identifier carried the old title: the well-known browse entry
point. AiDeployment becomes AiModelManagement, which is a BrowseName
change - NodeId 7001 does not move. The CSV diff is 0 moved, with that
one symbolic name replaced and nothing else touched.

Folders moved with git mv so history follows the document rather than
showing a delete and an add.

The riskiest part was not the rename but what reaches into this tree by
path: both Vision generators, the Vision validator, validate_all.py, the
tree README and sixteen references in the Vision specification. A missed
path there would not have failed loudly - the Vision validator returns
empty sets when the AI NodeSet is absent and then passes - so AI_TYPE_ID
was checked to be populated rather than inferring it from a green run.
It resolves 42 types and 208 ids.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Address PR #49 review feedback

Headings stated conclusions instead of naming topics, so a reader
scanning the contents met forty arguments rather than a structure.
Forty-three are now topic names - "Provenance", "Lineage", "Version
binding", "Data residency and egress" - and every statement a heading was
carrying has been restored as the opening sentence of the clause it
belonged to, so nothing was lost in demoting them.

The information model sections read as clipped one-liners. 5.2, 5.3 and
5.4 now open by saying what the type is for, who reads it and when, and
the member descriptions say what each is needed for rather than only what
it holds. The 5.4.1 table literally prefixed a "The question" column
before each answer; it is prose now.

5.4.2 compared State with Reachability, which clause 8 does not introduce
until much later - the reader met a comparison against something
undefined. It now describes State on its own terms, including why
Degraded sits between Active and Faulted, and defers the combination to
8.4 where the other half exists.

"The bridge" was doing two jobs. A Server reaches an external AI system
either by calling one per request (clause 8) or by obtaining a model from
a catalogue once (clause 9), and both get called bridging. 9.2 now
separates them before defining the import job, and the clause is titled
"The catalogue and model import".

"A card that lists only what a model can do is marketing" was polemic for
a specification; it now says the same thing about behaviour and
commissioning without the jab.

The decision record explaining why this was a separate specification was
already removed in an earlier commit; the section is gone rather than
rewritten, since a standalone document does not argue for its own
existence.

Contents regenerated for the renamed headings: 69 entries, all resolving.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Address PR #49 review feedback: payload size and scenarios

Invoke carried its payload as a ByteString, which is bounded three times
over - by MaxByteStringLength, by the negotiated MaxMessageSize and by
the Session's MaxResponseMessageSize - none of which this model gets to
choose. An image, a point cloud or a window of high-rate samples exceeds
them routinely, so the one call the specification defined could not carry
the input it was defined for.

Invoke is now the shortcut and BeginTransfer is the general path.
InferenceTransferType carries one chunked exchange whose Request and
Response are Part 5 FileType objects, so a client opens, writes or reads
in chunks of its own choosing, and closes. Nothing here invents a
transfer protocol: OPC UA already has one, every client implements it,
and a bespoke chunking scheme would be a second thing to get wrong.

MaxInlinePayloadSize is Mandatory and says the bound before a client
calls rather than after a rejection, because the three limits above are
not all visible from the client side and the smallest of them is the real
one. Clients that do not know their payload sizes in advance are told to
start with the transfer path.

The awkward case is not a large request - a client knows its own input -
but a request that fits producing a response that does not. Invoke
returns TransferRequired and Transfer for that, and a Server shall NOT
fail the call: the inference already ran, and failing would discard the
work and tell the caller nothing, leaving it to read an empty payload and
conclude the model returned nothing.

Streaming stays where it was. A transfer delivers one complete response
however large; 8.5 covers output a client wants as it forms, over a
Subscription or optionally a data channel.

Clause 4 Scenarios is new, and informative: eight arrangements this
specification supports, each with the flow that realises it, so a reader
can find the one resembling their installation before reading the
normative clauses that define its parts. They are not alternatives - a
plant running a model locally, calling a hosted one for a second opinion
and importing both from a catalogue is doing three at once.

Clauses 5 to 14 shift by one to make room. Every clause reference was
re-checked by hand rather than trusting the section-ref gate, which
resolves a bare reference against any cited document and would not have
caught a stale one.

0 NodeIds moved; 22 added.

* Show how the AI model maps onto the systems people actually integrate (#54)

* Add vendor implementation guides for the AI Model Management specification

The specification names ApiDialectEnum literals for what a contract does
rather than for whoever published it first, because a literal in a
standard should not be an advertisement. That is right for the normative
document and leaves an implementer holding a question it does not answer:
they are not integrating a REST chat-completions contract, they are
integrating Azure AI Foundry, or Bedrock, or the Triton server already in
the plant.

Eleven informative guides under extras, one per system, each with the
same nine sections so the index's coverage table means something. The
ninth - what this system does not tell you - is the one worth reading
first; a mapping document that lists only what maps is marketing.

Two findings the set makes hard to avoid noticing.

No hosted inference platform in it exposes an artefact digest. Not
Foundry, OpenAI, Bedrock, SageMaker, NIM, Triton or Vertex. Identity
everywhere is a name string. Hugging Face is the only content-addressed
system here and it serves no inference. So Digest stays empty against a
hosted endpoint, a Pinned deployment is pinned to a NAME with the
provider's policy rather than a hash holding it still, and the Stage
import mode is the only path in the set that ends with a digest the
Server computed itself.

AuthenticationKindEnum cannot express AWS SigV4. The guides map it by
what is STORED, which is what makes the member answerable across systems
whose handshakes have nothing in common: an IAM role is WorkloadIdentity
because no secret exists anywhere, static access keys are ApiKey. Whether
the enumeration should gain a literal is a separate decision - it would
be a NodeSet change - and is not taken here.

validate_examples.py re-derives from the NodeSet and the specification,
never from the guides, and is wired into validate_all.py because a
validator outside the aggregate silently stops running. It checks that
cited members exist, that every dialect and authentication literal is
exercised, that conformance units are real, that each guide links the
specification by relative path - which is also what makes a bare section
reference resolve, metaverse-specs being a strict tree - and that the
index agrees with the guides.

It earned its place twice while being written: it found that no guide
covered OpcUaInference, which is why there is now one for federating to
another Server implementing this specification, and it caught a member
this author had cited as ImportMode when the model calls it Mode.

Mutation-tested against all five checks. No model file is touched: no
NodeSet, no NodeIds CSV, no generator, so no version bump.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Correct the vendor guides against an audit of all eleven

Nine of the guides were written by sub-agents and only spot-checked. A
three-way audit against the sanctioned research and the specification
found errors in every category the audit looked for, including in the two
documents the lead wrote.

Conformance claims that were simply wrong:

AI-OffServer requires EndpointUri to name an authenticated, confidential
scheme. The NIM, Triton and KServe guides all claimed it while showing a
plain http:// listener as the default - which is the ordinary way a
self-hosted inference server sits on a plant network, so this was wrong
in exactly the case a reader would be in.

AI-Import requires AI-Catalogue; an import job with nothing to import
from is not implementable. SageMaker and Triton claimed AI-Import from
S3 URIs and a model repository respectively, neither of which is a
catalogue that declares a digest to verify against.

The federation guide claimed every conformance unit an upstream Server
implements is reachable through the link. A conformance unit describes
what THIS Server exposes; reaching a Server with a catalogue does not
give this Server AI-Catalogue, mirroring it does. Rewritten as three
lists by what the downstream Server actually has to build.

Model errors:

ApiDialect is Mandatory, and the Hugging Face guide left it empty on the
grounds that a catalogue speaks no inference contract. True, and not
available: it is Proprietary with EndpointDescriptionUri doing the work,
and the guide now says the cleaner arrangement is to model the Hub as a
ModelRegistryType under clause 10 rather than bending a ModelSourceType
around it.

AI-Invoke requires ModelUsed populated on every response, so the
federation guide's suggestion that a downstream Server may return null
when it cannot resolve the upstream model was not one of the options. It
publishes a ModelType for the substitute or it fails the call.

RateLimit.RetryAfter is a Duration; Retry-After carries seconds or an
HTTP date. Converted, not copied.

Citations: ModelUsed is 8.2.1 and not 8.5, which is Incremental results;
the payload bound and the transfer path are 8.2.4 and not the whole of
8.2. The index attributed the digest rules to clause 11 when 12.1 states
them, and asserted a shall-not the specification does not contain - what
12.1 actually requires is that a Server populate Digest where the
artefact is obtainable through ArtifactUri, which for a hosted model it
is not.

Unsupported vendor facts, removed rather than softened: ONNX file
metadata fields, GGUF header contents, Triton's storage backends and
repository-management extension, and Retry-After attributed to specific
platforms rather than to HTTP.

Triton's dialect split was a transport split. Clause 9.2 names the
literals for the contract, and Triton serves OIP over both HTTP and
gRPC, so both are OpenInferenceProtocol. TensorRemoteProcedure stays,
described as what it is for.

The index also called Hugging Face the only exception to no artefact
digest, omitting embedded runtimes - the one arrangement in the set where
the digest is something the Server computed rather than was told.

Comparative praise replaced with description throughout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Restore the blank line before a heading in the federation guide

Lost while rewriting the catalogue section for the conformance-unit
audit. MD022, and the only markdownlint finding across the set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Releases 0.3.0 and 0.4.0: what eleven real systems taught the AI specification (#55)

* Integrate what the vendor mappings taught the AI specification

Mapping one information model onto eleven real systems was a natural
experiment, and it found places where the model was unsatisfiable,
unguarded or ambiguous. None of them needs a NodeSet change.

AI-Invoke was unsatisfiable for every non-language model. The facet
required Usage "populated on every response" while no clause imposed
that obligation, and "empty" was unencodable: the three counts are
UInt64 and the model declares no optional fields, so leaving them
empty encodes as zero, indistinguishable from a metered zero. Five
guides claimed the facet while documenting empty Usage. 8.2.3 now
makes an empty UnitKind the not-metered sentinel, with the counts set
to zero and clients forbidden from reading them as measurements. The
unit carries the sentinel because the counts cannot.

Residency did not compose across a federation hop. A payload leaves a
deployment along exactly two modelled edges, FallsBackTo and Source,
and only the first was guarded; EgressPermitted's only shall binds on
InferenceLocation Cloud, so a cell-to-site EdgeOffServer hop escaped
it. 9.5 states the members are end-to-end rather than next-hop, and
obliges a Server federating to a peer to read its declarations and
never publish anything more permissive, defaulting fail-safe where it
cannot read them. This is the rule 12.3.2 already states for the
other edge.

Clause 13.1 described a plant MLOps node that "may never call Invoke
at all" while AI-Base required at least one deployment. The facet's
deployment requirements are now conditional on the Server exposing
one. AI-OffServer cites 12.2's scheme requirement instead of
restating it, so the two cannot drift.

Three ambiguities are resolved where the guides had each invented an
answer. ModelId carries the source's identifier verbatim, and
Publisher names who produced the model rather than who serves it --
two Servers were reading owned_by into Publisher and publishing
"azure" for a model Meta trained. AuthenticationKind classifies what
is stored rather than the handshake performed, which is what makes
SigV4 answerable without a new literal. ApiDialect classifies the
contract this Server speaks, and a catalogue-only source is
Proprietary with an EndpointDescriptionUri -- correct, not a
shortcoming.

The eleven guides are updated to cite these rules rather than derive
them, and three that had softened 12.2's shall into an operator
choice are corrected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Release 0.3.0: state why a digest is absent, and reach a registry from an import

Three changes the vendor mappings argued for that prose alone could not
make. All three append; NodeId churn is 0 changed, 0 removed, 5 added.

DigestProvenanceEnum, Mandatory on ModelType and Optional on
ModelResourceType. Digest is Mandatory so that its absence is uniform
and browsable, and that stays right -- seven of eleven systems cannot
fill it, and Optional would make "no digest" indistinguishable from
"does not implement digests". But empty then carried two meanings and
populated carried three, and the missing answer was doing visible harm:
five guides each prohibited a DIFFERENT wrong value -- a response
fingerprint, a resource name, a storage entity tag, a manifest hash, a
commit identifier -- and none could say what to publish instead. 12.1.1
states the prohibition once and supplies NotAvailable. Mandatory for the
reason Digest is, which 6.2 already gives: clause 12 depends on it, and
a rule that depends on an Optional member is one a conformant Server can
silently not satisfy. A StatusCode cannot carry it -- DeclaredBySource
and VerifiedOnStage are both Good.

12.1.1 also states that provenance does not strengthen by being
forwarded, on the ordering NotAvailable < DeclaredBySource <
ComputedByServer < VerifiedOnStage. Republishing an upstream peer's
VerifiedOnStage would claim a check this Server did not perform, and the
claim would be indistinguishable from one it had. Same composition
argument as 9.5 residency, applied to the other thing a federated
deployment forwards.

ModelImportJobType.Registry, with 10.2 requiring exactly one of Source
and Registry non-null. Source was Mandatory and typed ModelSourceType,
so nothing connected an import job to a ModelRegistryType -- the edge
the 4.4 and 5.1 diagrams draw. A Server importing from a catalogue had
to invent a source wrapper around its own registry. Source keeps its
Mandatory rule and takes null on the registry path, the idiom
TargetDeployment already uses, so no existing member changes.

ModelType.Name is constrained rather than retyped: its Text shall be the
source's name carried across unchanged, translations may be added for
display, and the Text is not reformatted. Retyping to String would have
been cleaner and is breaking; the property wanted -- two Servers
fetching one model from two mirrors produce the same string -- is
obtainable by constraining the field.

Version and PublicationDate bump to 0.3.0 / 2026-08-05, picked up by the
vision overlays automatically because build_examples.py derives both
from the AI generator rather than pinning them.

The eleven guides state their DigestProvenance value and cite 12.1.1
instead of each re-arguing the prohibition. The index reads the member
as the axis the set varies along: evidence tracks custody, which is why
an embedded runtime -- a local file the Server can simply hash -- is the
one arrangement here that produces evidence without vendor cooperation.

Adds CHANGELOG.md, where this repository keeps release history so the
specification does not have to narrate it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Stop sending a homeless identifier to a member that cannot hold it

12.1.1 correctly forbids a non-content identifier in Digest and then
disposed of it into ArtifactUri or ProvenanceUri. Those are scalar
String Properties on ModelType -- one artefact, one value. A serving
configuration fingerprint is per-response and per-deployment, so the
disposition is unimplementable for the case it was most needed in, and
five guides were left telling an implementer not to publish a value the
specification had just promised a home for.

The narrowed rule says what is true: an identifier that LOCATES the
artefact or its provenance record belongs in those members, and one
that identifies something else has no member in this model. Recording
that a datum has no home is better than filing it where a client reads
it as an artefact digest.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1dab1302-19c5-4a9b-a50c-d97d389713aa

* Release 0.4.0: model what eleven real systems publish and this had no member for

The same eleven mappings, read with a different question. 0.3.0 asked
which rules were wrong; this asks what these systems do that the model
cannot represent at all. The answer had a consistent signature: a guide
NAMES a vendor field and then maps it nowhere, and the omission is
invisible because nothing is missing from the guide's own tables.

Twelve appended Properties, no new DataTypes. NodeId churn is 0 changed,
0 removed, 12 added.

A model had no time on it. Six of eleven return a vintage in the call a
Server populates ModelType from, and every guide dropped it. The
asymmetry made it plain: DatasetType.CreatedAt exists with 6.3
defending it, and a catalogue resource inherits CreatedAt/ModifiedAt
from xRegistry, so a model known through a ca…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant