# JSON-RPC integration contract

This is the shared design for Smithy value validation, JSON-RPC modeling/message checks, optional Ion
validation and OpenRPC export. Direct JSON value validation is implemented in this module and the
`validate value` CLI. The `rpc` package implements opt-in JSON-RPC trait definitions, prepared
methods, request/notification and contextual response/error validation through `smithy validate rpc`.
Explicit 2.0 batches are implemented; exchanges and the Ion adapter remain follow-on capabilities. The separate
`moonrockz/smithy-openrpc` module implements description export through a library and build plugin.
See the [message validator](rpc/README.mbt.md) for executable examples and implemented boundaries.

The goal is to define an API in Smithy and check actual messages against it. A description export is
a separate deliverable. Model assembly validates definitions; instance validation checks application
data; envelope validation checks protocol structure; exchange validation checks supplied relationships.
No isolated message check can establish transport delivery or prove that a peer remained silent.

## Dependencies and delivery

| Work | Deliverable | Prerequisite |
|---|---|---|
| smithy-2qe.34.1 | Shared method/value/format contracts and rule matrix | Design review |
| smithy-2qe.34.3 | Direct JSON value validation, reusable API and CLI | Shared value contract |
| smithy-2qe.34.4 | Requests and notifications | Generic validator and prepared methods |
| smithy-2qe.34.6 | Contextual successful/error responses | Request/method contract |
| smithy-2qe.34.7 | Versioned batches | Request and response validators |
| smithy-2qe.34.8 | Explicit exchanges/transcripts | Response and batch validators |
| smithy-2qe.34.2 | OpenRPC export | Shared method/schema contract, independently of runtime validation |
| smithy-2qe.34.5 | Native Ion value validation | Generic engine and adapter contract |

`moonrockz/smithy-validation` depends inward on the model/syntax modules and core libraries.
JSON-RPC and OpenRPC consumers must not acquire Ion, WIT, WAC or wasm-devkit dependencies.
The Ion adapter belongs in its own publishable module with an Ion dependency; WIT conversion belongs
in another module using the separately published `moonrockz/wit`. Subpackages within one module do
not isolate module dependencies. The CLI may compose optional capabilities at the app layer.

Qualify these boundaries with isolated packaged consumers, not workspace imports: JSON validation
without Ion/WIT, later Ion validation without WIT, and later WIT conversion without Ion. Keep
cross-integration tests out of the core published manifest. New modules follow shared-version and
publication conventions.

## Shared value contract

See [the executable library examples and policies](README.mbt.md) and [engine design](DESIGN.md).
The generic validator selects a typed ShapeId, snapshots reachable contracts and supports repeated
read-only validation without a service or schema conversion. Findings preserve classification,
severity, semantic member IDs and structured instance paths. Numbers remain exact; input is never
silently coerced, normalized, defaulted or mutated. Limits produce an explicitly incomplete result.

For a trait ID, an explicit member trait replaces that target trait as a whole; otherwise it inherits
the target trait. Required/null/unknown/default/sparse/union/naming policies are explicit. Selected
result and error-data members must use their effective member contract, including overrides, rather
than validating only the unconstrained target shape.

ValueView inspects one source node at a time. Its neutral NativeScalar payloads distinguish typed
null, binary float, exact decimal token, native timestamp, symbol and binary data. Annotated values
and sexps remain distinct. Object fields retain occurrence identity. LocatedObject explicitly maps
logical members onto original source locations; a positional parameter can therefore report
`/params/0` while naming `example#Input$count`. Base paths compose structurally, not by rewriting
rendered pointer strings. Unsupported mappings produce findings, not a JSON round trip.

## Method modeling

ProtocolVersion has exactly V1_0 and V2_0, corresponding to the published JSON-RPC 1.0 and 2.0
specifications. Do not infer a version or imply support for 1.1 drafts. Keep these versions distinct
from Smithy RPC v2 JSON, OpenRPC document versions and JSON Schema dialects.

The opt-in `smithy.rpc` trait model supplies:

| Trait | Contract |
|---|---|
| Service `jsonRpc` | Explicit supported versions; `undeclaredServerErrors` is `accept` (default), `advisory` or `error`. |
| Operation `method` | Nonempty wire name, parameter mode named/positional/either, explicit positional input-member order, and result selection. |
| Error `rpcError` | Integer application or implementation server code and error-data selection. |

Inputs and outputs remain Smithy structures. Result selection is OutputStructure, OutputMember or
Null. `resultMember` selects a member contract so its wire result may be scalar, array, object or
null. Without a selection, output is the whole output structure. A conflicting result selector is
invalid. Error data similarly selects the whole error structure by default, a named member, or an
explicit absent-data mode. A data member selector cannot coexist with absent-data mode.

Request preparation rejects duplicate wire names, undeclared result member selectors, incomplete
or repeated positional ordering and mappings incompatible with a declared protocol version.
Preparation also rejects duplicate codes, standard/reserved future codes and invalid error-data
selectors. Application methods cannot use reserved `rpc.` names. Supported system
extensions require explicit preparation rather than automatic acceptance of a reserved name.

Named parameters use the chosen Smithy/jsonName policy. Positional order is a complete explicit
list, never map iteration order. Missing trailing optional arguments are permitted; required ones
must be present. Interior omissions require explicit values satisfying the selected null policy.
For 2.0, omitted params means no arguments. For 1.0, params is always a positional array, including
an empty one. A named-only method cannot claim a usable 1.0 mapping.

The trait syntax is:

```smithy
$version: "2"
namespace example
use smithy.rpc#jsonRpc
use smithy.rpc#method

@jsonRpc(versions: ["1.0", "2.0"])
service Calculator { version: "1", operations: [Subtract] }

@method(name: "subtract", params: "either",
    positional: ["minuend", "subtrahend"], resultMember: "value")
operation Subtract { input: SubtractInput, output: SubtractOutput }
structure SubtractInput {
    @required
    minuend: Integer
    @required
    subtrahend: Integer
}
structure SubtractOutput { @required value: Integer }
```

With that contract, `[42,23]` is valid positional input in either version. In 2.0,
`{"minuend":42,"subtrahend":23}` is also valid. `[42,"23"]` fails at `/params/1` on
`example#SubtractInput$subtrahend`. Result `19` validates against `SubtractOutput$value`; result
`"19"` fails that member. `@rpcError(code: 1001, dataMember: "detail")` selects a modeled error's
detail member for application error data. A Null result selection validates a null success payload
independently of whether a request is a notification.

## Message and exchange APIs

PreparedService snapshots wire method names to operation IDs, input validators, result selections
and the chosen version/profile, together with result validators and per-operation application/server error
mappings including service errors. It returns findings; it does not invoke handlers or transmit
protocol errors. Parameters, results and error data delegate to the generic value engine.

RequestId distinguishes Absent from Present(NodeValue). Numbers remain exact; legacy structured IDs
retain structural identity. A 1.0 present-null ID is a notification. A 2.0 absent ID is a notification;
a 2.0 present-null ID remains a call, with a recommendation advisory. Fractional 2.0 IDs are also
permitted with an advisory. Advisories do not invalidate an otherwise valid message.

ResponseContext explicitly provides an operation or original request. Operation-only context can
validate payload shapes but cannot prove ID correlation. A bare response identifies no method;
envelope-only checks must state that limited scope. Original-request context permits exact ID checks.
For exchanges, `ProtocolDescription` names the two peers of a `@jsonRpcProtocol` structure and the
service each one accepts, so each transcript entry has an explicit direction. `TranscriptChecker`
correlates calls, notifications and replies per sender (see the
[RPC guide](rpc/README.mbt.md#transcripts)). Transcript completeness must be declared before reporting
missing responses. Duplicate IDs may leave otherwise
well-formed calls ambiguous for correlation; distinguish that finding from an invented envelope MUST.

Unknown envelope extension fields are ignored by the base profile unless an explicit extension
policy checks them. The payload's unknown-member policy is separate. Duplicate JSON keys are
rejected as an explicit deterministic-input compatibility policy.

The request/notification milestone also fixes the shared JSON parser's error offsets, as reported
in [PR #38](https://github.com/moonrockz/smithy/pull/38#discussion_r4337569571).
Balanced malformed input such as `{"a":truX}` must report the parser's failure location instead of
the completed preflight scan's EOF position. Regression coverage must verify the library offset
and its propagation through structured CLI diagnostics for value and RPC validation.

The request validator is an `rpc` subcommand of `smithy validate`.
The CLI is `smithy validate rpc --version 2.0 --service example#Calculator --message call.json
model.smithy`. Responses use `--response reply.json` with one of `--method subtract`,
`--operation example#Subtract`, `--request call.json` or `--envelope-only`. Missing response
context fails. JSON diagnostics are one complete stdout report. The scope is explicit; method-only
checks cannot prove correlation. Legacy opaque errors remain distinct from the opt-in
`--legacy-errors code-message-data` mapping. Add `--batch` for explicit 2.0 request/response arrays.
Original batch context declares the complete supplied batch association, with unordered exact-ID
matching and notification omissions. `--batch --no-response --request` checks absence separately
from an invalid empty response array. Duplicate call IDs remain request advisories but fail
ambiguous correlation. Identical invalid-element errors form a multiset. See the RPC guide for
typed batch contexts, indexed results, single invalid-whole-batch errors and resource policies.
`smithy validate rpc transcript --protocol example.peers#Editor peers.smithy editor.json` checks a
JSON transcript between the two peers of a protocol, with explicit senders. `--version` is needed only
when the document gives none, and must agree with it. `--complete` or `--partial` overrides the
document; partial is the default. Text findings name the file, line, column, entry and sender; JSON
diagnostics are one report with the summary, the findings and the expectation result. The exit
status is 0 for no error findings, or for an `expect` list that matches, 1 otherwise, and 2 for usage
and contract errors. `--expect <file.json>` gives the expect list for any input format
(`read_expected_findings`); it conflicts with an `expect` list in the document. `--legacy-errors`
passes the 1.0 error mapping to `run_transcript` and `TranscriptChecker::new`. Markdown transcripts (`examples/json-rpc/transcripts/*.md`) are read with the same command. JSONL logs (`--input-format jsonl`, `.jsonl`, `.ndjson` or stdin) need `--version`, and take
`--log-preset raw|dir-message`, `--sender`, repeatable `--direction value=peer` or `--log-mapping`. The
command reads a log file or stdin in 64 KiB chunks, decodes them at the CLI boundary, carrying an incomplete UTF-8 sequence to the next
chunk and replacing invalid bytes with U+FFFD, and passes each chunk through `LogReader` and
`TranscriptChecker`. It writes reader and checker findings in line order as they are known, then a
summary with the `LogCounts`; JSON diagnostics are JSON Lines. When `single_sender()` is true the
checker runs with correlation off. Native and WASI read files and stdin live, so a FIFO streams;
js and wasm-gc read all of the input first and give the same findings. A read error part-way through
still gives the summary, then the error (exit 1). A read error before the first byte, such as a
directory path on POSIX, gives the `InputRead` report of a missing file instead (exit 1). With `--expect`, the match runs at the end of the
log and the summary has the expectation result.

## Two-peer protocols

`smithy.rpc#jsonRpcProtocol` marks a structure as a protocol between exactly two named peers. Each peer
may `accept` one JSON-RPC service, which is the service that peer receives calls and notifications for.
`ProtocolDescription::from_smithy(model, protocol, version)` rejects a missing shape or trait, an
undeclared version, equal peer names and an accepted service that does not declare the version. It
prepares one `PreparedService` per accepting peer; `service_for` gives `None` for a peer that accepts
nothing. Transcript checking builds on this description.

## Transcript correlation

`TranscriptChecker` takes a `ProtocolDescription` and checks entries, each a sender and the original
message text, one at a time or in groups. Findings are `TranscriptFinding` values with a
`TranscriptCode`: `Message(FindingCode)` wraps a message-level finding, and the other codes are
correlation and limit findings. `DuplicateReply`, `MissingReply` and `AmbiguousReplyId` render as the
stable codes `DuplicateResponse`, `MissingResponse` and `AmbiguousId`, and their names differ from the
`FindingCode` constructors so that unqualified uses stay unambiguous. `FindingSeverity` stays `Error | Advisory`; the outstanding calls of a
partial transcript are advisories. Each sender has its own ID space, and a reply matches a call by
caller, responder and exact ID, with the batch ID normalization for scalars and structural equality for
structured 1.0 IDs. A matched reply is validated with `OriginalRequest` context. A null-ID reply answers
the earliest unreadable entry of the other peer, then an explicit-null 2.0 call, with an `AmbiguousReplyId`
advisory when both exist. Other replies are duplicate, wrong-responder, reply-to-notification or
unsolicited findings. Completeness is declared: `finish` reports missing replies only for a `Complete`
transcript. Tables are bounded by `TranscriptLimits`, so memory does not grow with the length of a
stream. A 2.0 request batch is one unit that waits for one reply batch. A reply batch answers the
earliest pending batch of the other peer whose IDs include every reply ID, and is checked with
`OriginalBatch` context; one that fits no batch is unsolicited. A single reply to a call inside a
pending batch is `WrongReplyForm`. An empty batch waits for one null-ID `-32600` error object, and an
all-notification batch needs no reply. At `finish`, each pending batch counts as one unanswered unit.
A 1.0 array entry stays `UnsupportedMessage`.


### JSON transcript documents

`read_json_transcript` reads `{version?, complete?, entries, expect?}` or a bare entry array. A
small scanner finds each `message` value's exact source text; nothing is re-serialized. Problems
are `InvalidTranscript` findings with a line and column; a bad entry is dropped. `ExpectedFinding`
names a code by its rendered `to_string` (for example `DuplicateResponse`), and `match_expected`
matches items to distinct findings (maximum bipartite matching, so order does not matter). A match
needs no missing item and no unlisted error. `read_expected_findings` reads a standalone array of
the same items with the same item parser.

### Markdown transcript documents

`read_markdown_transcript` reads fenced `jsonrpc A -> B` (or `jsonrpc A`) blocks as entries, `jsonrpc-expect`
blocks as the expect list, and optional front matter keys `version` and `complete`. It ignores other
fences and prose. A message is the block body as written, located at its first line. A `jsonrpc` fence with
no direction, a receiver equal to the sender, and an unterminated fence are `InvalidTranscript` findings.
The receiver of `A -> B` is stored in `TranscriptEntry.receiver` (the JSON reader leaves it `None`); when it is not `other(A)`, `TranscriptChecker::feed` reports `UnknownPeer` and skips the entry.
`run_transcript` is the shared read-check-match pipeline of the CLI and the BDD world.

### JSONL logs

`LogReader` turns log text into entries as it arrives. `push_text` takes UTF-16 chunks of any size
and holds a split line, including a split surrogate pair or CRLF, until its newline; `end` reads a
final line with no newline. The reader removes one leading BOM and treats CRLF as LF. Memory is
bounded by `line_bytes` (1 MiB of UTF-8): a longer line is one `InvalidTranscript` finding and is
skipped to its newline. Decoding bytes, and carrying an incomplete UTF-8 sequence to the next chunk,
is the caller's job. A `LogMapping` is a fixed rule, not a pattern language: a `LogPrefix` (none, to
the first brace, or N delimited fields), a `LogSelector` (any JSON-RPC record, a field that exists, or
a string field with a value), a `MessageSource` (the whole record, the exact source text of a field
through the JSON transcript scanner, or a decoded string field) and a `SenderSource` (a fixed peer,
or a field mapped to peers). Paths are JSON Pointers. `LogMapping::new`, `dir_message`, `from_json`
and `LogReader::new` raise `ConfigurationError` for a bad configuration, such as an empty direction map
or a `line_bytes` below 1; the reader never raises on its input. Lines that are not JSON
and records the selector rejects are counted, not reported. A sender value that the map lacks and a
missing message field are `InvalidTranscript` findings, and the line is dropped. The `raw` preset has
a fixed sender, so `single_sender()` tells the caller to turn correlation off; `dir-message` reads
`{"dir", "message"}` records. A quickcheck law checks that random chunk boundaries, CRLF, a BOM and a
missing final newline give the same entries, findings and counts, and a table test checks that
`editor.json`, `editor.md` and `editor-dir.ndjson` give the same findings apart from locations.

### Session order

The order layer is opt-in. `@smithy.rpc#jsonRpcSession` on a protocol structure declares the session
states (the first is the initial state) and the capability sources: a name, the method whose
successful result advertises it, and a JSON Pointer into that result. A value that is absent, `null`
or `false` does not advertise. `@jsonRpcLifecycle` on an operation gives `allowedIn` (absent means
every state), the transitions `onCall`, `onSuccess` and `onError`, and the `requires` capabilities.
`@jsonRpcCancels(idMember, code)` names the input member with the ID of the cancelled call and the
error code that the call may then answer with. The rules are declarative, with no expression
language, so that they express the initialize-first and shutdown-then-exit order of MEP, LSP and MCP.

`from_smithy` prepares the rules only for a protocol with `jsonRpcSession`, so a protocol without it
gives the findings it gave before, even when its operations carry the lifecycle traits. A state that
the session does not declare, an unknown capability, a capability method that no peer accepts, an
invalid pointer and an `idMember` that is not an input member are `InvalidContract`. The checker
keeps one session state for both peers. The rules apply only to a method that the receiving peer
models. A request that the receiver does not model is an `UnknownMethod` finding already, so it
applies no rule, takes no transition and advertises nothing. When a request or notification arrives,
its method must be allowed in the state (`StateViolation`, with the entry of the last transition in
`related`) and its required capabilities must be advertised (`CapabilityNotAdvertised`, with the
last accepted call of the capability source in `related` when there is one). A rejected message
causes no transition, and its reply moves the session nowhere, but correlation still tracks its
call. A call that correlation refuses (`ActiveIdReused` or `ActiveCallLimit`) is not checked against
the session, so it takes no transition and cancels nothing; the same holds for every element of a
refused request batch, and for an unreadable entry (a request without a usable ID) that
`ActiveCallLimit` refuses. An accepted message takes its `onCall` transition. A reply to an accepted
call takes `onSuccess` or `onError` and adds the capabilities that its result advertises. Request
batch elements are checked in array order, and a reply batch settles each answered call.

A cancellation names a call of its own sender: an active single call, or a call in an earlier
pending request batch. Otherwise it is a `CancelInactiveCall` advisory, because a cancellation can
race the reply. A cancellation that names a call in its own request batch gives no finding and
excuses nothing: the receiver may process the elements of a batch in any order, so it proves
nothing. A null ID never names a batch call: in a batch the null ID also answers each invalid
element, so it does not name one call. A null-id cancellation names only an active null-id single
call, and otherwise it is the `CancelInactiveCall` advisory. The reply of a cancelled call may use
the declared code: the `UnknownError` finding for that code on that reply is dropped. In a reply
batch, only the first element that answers the cancelled call is excused, never another element and
never a call with the same ID outside the batch. The session holds only the state, the advertised
capability names, the entry of the last call of each capability source, the entry of the last
transition and the cancelled calls. A cancelled single call is always an active call, so
`active_calls` bounds that table, and the call leaves it when it is answered. A cancelled batch call
is kept in its pending batch, so the keys of that batch bound it, and it leaves with the batch reply.
The layer needs correlation; with `correlation=false` it does nothing.

## Rule and original-example coverage

The table separates request and single-response coverage from follow-on batch/exchange work. R = requests/notifications,
S = responses/errors, B = batches, E = exchanges, T = transport-only, C = explicit compatibility policy.

| Source | Class | Required check or classified boundary |
|---|---|---|
| 2.0 §§1–3 | R/S/C | Explicit version; JSON types and case-sensitive matching; 1.0 objects do not silently become 2.0. |
| 2.0 §4 version/method | R | Literal 2.0, string method, reserved rpc. prefix and modeled method lookup. |
| 2.0 §§4/4.2 params | R | Omitted/object/array only; named case, explicit positional order, parameter mode and value constraints. |
| 2.0 §4 IDs | R/S/C | String/number/null only when present; exact identity; null/fractional recommendations stay advisories. |
| 2.0 §4.1 notifications | R/E | Absent ID; no supplied response even within batches. Single messages cannot prove silence. |
| 2.0 §5 responses | S/E | Version and ID required; exactly one result/error; contextual payload shape and ID correlation; null unknown ID for parse/invalid-request errors. |
| 2.0 §5.1 errors | S/C | Integer code, string message, optional data. Models may declare -32099..-32000 implementation server errors with the normal data contract. Undeclared server errors follow the service's accept/advisory/error policy, default accept. Standard and reserved future codes cannot be modeled. No invented hard message-length limit. |
| 2.0 §6 batches | B/E | Nonempty request array, element checks, unordered exact-ID association and no scheduling claims. Invalid batch yields a single error object. |
| 2.0 §6 notification handling | B/E | Invalid elements and mixed batches; all-notification batches have no response; empty response arrays are forbidden. |
| 2.0 §7 subtract positional examples | R/S/E | Both argument orders, numeric results and original IDs. |
| 2.0 §7 subtract named examples | R/S | Both object orders; names determine arguments. |
| 2.0 §7 update/foobar notifications | R/E | Notification classification independently of modeled method existence. |
| 2.0 §7 nonexistent method | R/S/E | Unknown method and contextual -32601 error. |
| 2.0 §7 invalid JSON | R/S/E | Parse failure and -32700 response with null unknown ID. |
| 2.0 §7 invalid request | R/S | Wrong method/params types and contextual -32600. |
| 2.0 §7 invalid batch JSON/empty batch | B/S | Single protocol error object. |
| 2.0 §7 [1] and [1,2,3] | B/S | Error for each invalid element, with null IDs. |
| 2.0 §7 mixed batch | B/E | Calls, notification omissions, invalid elements, unknown method and unordered responses. In a transcript, a reply batch answers the pending request batch of the other peer (`transcripts/batches.json`, `batch#Link`). |
| 2.0 §7 all-notification batch | B/E | No response. |
| 2.0 §8 extensions | C | Only supported explicit extensions, without inferred negotiation. |
| 1.0 overview/§1.1 | R | Object, method string, required positional params and arbitrary required JSON ID. This profile rejects a jsonrpc marker. |
| 1.0 §1.2 | S/E | result/error/id required; error null on success, result null on error, matching arbitrary ID. Null success allowed. Legacy error mapping is explicit, not a required 2.0 error envelope. |
| 1.0 §1.3 | R/E | Present-null notification; absent ID invalid; no supplied response. |
| 1.0 §2.1 bidirectional calls | E | Explicit peer directions and per-sender ID spaces; partial transcripts list outstanding calls (advisory) and complete ones report missing replies; unsolicited, duplicate and wrong-responder replies and replies to notifications fail exchange checks (`TranscriptChecker`, `smithy validate rpc transcript`). |
| 1.0 §2.1 stream lifecycle | T | Framing, close behavior and pending-call exceptions need a transport implementation. |
| 1.0 §2.2 HTTP | T/C | POST framing, polling and unspecified sessions remain transport concerns. Multiple serialized objects are not standardized 2.0-style batch arrays. |
| 1.0 §3 class hints | C | __jsonclass__ is ordinary modeled data; never execute constructors. |
| 1.0 §4 echo | R/S/E | String argument/result and matching numeric ID. |
| 1.0 §4 chat transcript | R/S/E | postMessage calls 99/101, reverse handleMessage/userLeft null-ID notifications and matching results. The echo and chat exchanges are checked as transcripts (`transcripts/echo.json`, `transcripts/chat.json`). |
| Adversarial fixtures | R/S/B/E | Exact large/fractional IDs, wrong case, absent/null, arbitrary scalar/array/null results, malformed errors, ambiguous IDs, duplicate/unsolicited/wrong-responder replies, partial and complete transcripts, expected findings (`transcripts/editor-negative.json`) and transcript limits. |

## OpenRPC mapping

Pin document version 1.3.2 and JSON Schema draft-07 independently of JSON-RPC wire versions. The
official metaschema release 1.14.9 artifact permits 1.3.2; its SHA-256 is
`e5f534fb0dd9dd3598d26414e278a8c96e923ee7b54bef7009c499f4c71d0f82`.
Vendor the released `open-rpc-meta-schema.json`, not the repository's generation template.

The library is `moonrockz/smithy-openrpc`: `Config::new(ServiceId, ...)` selects a typed service,
and `convert_result(model, config)` returns the document and structured conversion warnings.
A build plugin named `openrpc` writes deterministic `openrpc.json`. Settings select the service,
info metadata, naming and extension policy. Use the existing JSON Schema converter/reference index
with `#/components/schemas`, preserving effective member constraints and valid references.
`PreparedService::contracts()` supplies detached, sorted method/error mappings to the exporter.
JSON Schema's opt-in `SchemaIndex::for_values` and `convert_values` APIs preserve runtime member replacement semantics,
intrinsic numeric bounds, closed structures, union cardinality and sparse collections. Recursive
constrained members receive collision-checked shared definitions rather than recursive expansion.

Export service metadata, method names/docs, ordered Content Descriptors and requiredness, results,
application errors, examples and reusable schemas. Map parameter mode to by-name/by-position/either.
Result selection supports scalar, array, object or null schemas. Error data schemas use documented
x-smithy metadata because OpenRPC's error data field is not itself a schema keyword. Notification-only
conventions, batch and exchange rules require documented extensions or explicit loss diagnostics.
Reject conflicting names, invalid selections and unresolved refs instead of silently losing meaning.

OpenRPC standard output covers 2.0 only. Preserve 1.0 validation independently; do not invent standard
1.x OpenRPC. Consumers may serve a generated document via rpc.discover; a discovery server is not
required. Acceptance validates deterministic documents against the pinned official metaschema and
shares representative method/schema fixtures with message validation.
The [exporter guide](../smithy-openrpc/README.mbt.md) documents exact settings, error-data/example
extensions and schema limitations. `mise run test:openrpc` qualifies actual CLI/library artifacts
against the vendored official metaschema and compares representative schemas with runtime results.

## Ion profile and released API audit

The inspected `moonrockz/ion v0.6.0` API exposes immutable Ion, alias IonValue, IonValueKind,
SymbolToken and ordered struct Fields. It exposes exact BigInt, decimal coefficient/exponent/signed
zero, native timestamp precision/offset, annotations, binary data and distinct list/sexp kinds.
Field names may be unresolved symbols. Select and verify a compatible published pin when implementing
the adapter; reuse its codecs rather than writing an Ion parser or generating Ion Schema.

| Native kind | Explicit mapping policy for the later adapter |
|---|---|
| Null/bool/string | Generic presence/type rules; typed null retains its kind and requires nullable/sparse/document support. |
| Int/decimal | Exact comparison; preserve scale and signed zero. Integral decimal-to-integer mapping requires an explicit policy. |
| Float | Preserve binary value, signed zero and nonfinite status. Exact range comparison or an unsupported finding; no lossy decimal conversion. |
| Timestamp | Native Smithy timestamp profile, initially known offset and second precision. Unsupported precision/ranges report findings. |
| Symbol | Explicit text-symbol mapping only; unresolved symbol names fail. |
| Blob/clob | Blob uses bytes and byte lengths. Clob is unsupported without a separately named policy. |
| Struct | Structure/map with occurrence-aware fields; duplicate keys fail by default. |
| List/sexp | List maps to list; sexp remains distinct unless explicitly enabled. |
| Annotations | Reject by default; explicit ignore policy retains source annotations. Never infer a union from annotations. |
| Document | JSON-shaped by default; native open-content support requires an explicit profile. |

Take one in-memory Ion value first. A later CLI selects text/binary plus explicit single-value or
datagram behavior. Equivalent decoded text/binary inputs must yield equivalent findings, while
retaining native types and source values. This does not define an Ion-encoded JSON-RPC protocol.

## Sources

- [JSON-RPC 1.0](https://www.jsonrpc.org/specification_v1)
- [JSON-RPC 2.0](https://www.jsonrpc.org/specification)
- [OpenRPC specification](https://spec.open-rpc.org/)
- [Released metaschema](https://github.com/open-rpc/meta-schema/releases/download/1.14.9/open-rpc-meta-schema.json)
- [Ion v0.6.0 public API](https://github.com/moonrockz/ion/blob/v0.6.0/pkgs/ion/pkg.generated.mbti)
