Kernel ABI — Sanki
==================

`reference` `suite-support`

This is a supporting document for the Sashité chess-family NIP suite. It does not define a new event kind or protocol mechanism; it specifies **`sashite.sanki.kernel-abi/1`**, the interface of the WebAssembly module a Rule System event (kind `3417`) names for the `sanki` game — the contract through which every consumer of a session asks the rules their questions and receives, byte for byte, the same answers.

Under [ADR-0034](../../adr/adr-0034-reference-build.md) a rule system **is** its module: the verdict of a session, and every rule answer on the way to it, is by definition what the module returns. This document fixes what a consumer may ask and what the module must answer, so that a module built today is still callable, and still answering identically, for as long as its bytes exist. The NIPs deliberately leave the interface to the application layer (kind `3417` §The module); this is that application-layer specification for Sashité's Sanki game.

## Scope

This document covers:

- the identity and versioning of the ABI, and what a **conforming module** is;
- the **shape** of a conforming module — its WebAssembly profile, its exports, its memory protocol, its bounds;
- the **encodings** every request and response uses;
- the **operations**, their requests, their responses and their errors;
- which answers are **normative** (they define the rules for a session) and which are documentary or test-only;
- the **determinism and totality** requirements a conforming build satisfies, and how a build is verified before publication;
- what the **consumer** does on its side of the boundary — the checks and filters that are its own, and what it does with the answers.

This document does NOT define:

- the rules themselves — the kernel the module implements is specified in [Kernel — Sanki](../../rules/kernel-sanki.md) and the documents it incorporates ([Move Encoding — Sanki](move-encoding-sanki.md), [Statuses — Sanki](statuses-sanki.md), [Time Accounting — Sanki](time-accounting-sanki.md), [Position Encoding — Sanki](position-encoding-sanki.md)); this document only carries their vocabulary across the boundary;
- the Rule System event (kind `3417`), its retrieval and its provenance;
- how the reference module is built and published ([Rule System — Sanki](rule-system-sanki.md)).

## Identity and versioning

The ABI is identified by the string **`sashite.sanki.kernel-abi/1`**. A Rule System event carries it in its `abi` tag, and a conforming module states it through its `abi` export (§Exports). The two MUST be equal, and a consumer that implements this ABI refuses a module stating any other identifier (kind `3417` §Retrieval and verification).

The identifier names **this whole document**: the exports, the memory protocol, the encodings, the operation set and every schema below. Any change to any of them — an operation added or removed, a member renamed, a bound moved, an encoding altered — is a new identifier (`sashite.sanki.kernel-abi/2`), never a revision of this one: a module published under `/1` must remain callable by a `/1` consumer forever. A consumer keeps every ABI version it has ever accepted a session under; revisions are therefore rare.

A module **conforms** to this ABI if and only if it has the shape of §The module, answers every operation of §Operations with the encodings of §Encodings, and satisfies §Determinism and totality. Nothing else is required of it; in particular the rules it answers with are its own — that is the point.

## The module

### WebAssembly profile

A conforming module is a **core WebAssembly module** (binary format, version 1) using the following features and no other: the MVP instruction set, mutable globals, sign-extension operators, non-trapping float-to-int conversions (present in toolchain output, unused by the kernel), multi-value, reference types, bulk memory operations. It MUST NOT use threads and atomics, SIMD, memory64, multiple memories, exception handling, garbage collection, tail calls or the component model, and it has **no start section**: nothing runs at instantiation. A build is validated against exactly this feature set before publication (§Conformance of a build).

The module's **import section is empty**: no WASI, no host functions, no imported memory, table or global. Everything the module computes, it computes from its request.

The module defines exactly one memory, exported as `memory`. It is used from one thread at a time, its `alloc` and `call` strictly sequential (§Lifetimes): the protocol is not reentrant.

### Exports

| Export | Type | Meaning |
|---|---|---|
| `memory` | memory, `min = max = 2048` pages (128 MiB) | the module's single linear memory, fixed at instantiation — it never grows |
| `abi` | `() → i32` | the address of a **buffer** (§Buffers) holding the ABI identifier, `sashite.sanki.kernel-abi/1`, as UTF-8 |
| `alloc` | `(len: i32) → i32` | resets the arena (§Lifetimes), reserves `len` bytes for a request and returns their address — or `0` when `len` exceeds the request bound (§Bounds) |
| `call` | `(ptr: i32, len: i32) → i32` | runs the request in `memory[ptr, ptr + len)` and returns the address of the response buffer |

Every `i32` of these signatures is read as **unsigned** (`len` of `0x8000_0000` is simply above the bound). No other export is required; a module MAY export more, and a consumer ignores what it does not know.

### Buffers

A **buffer** is a 4-byte little-endian unsigned length followed by that many bytes. The `abi` and `call` exports return the address of a buffer; the consumer reads the length, then the bytes. A buffer is never at address `0`: a returned `0` means the module produced none — from `alloc`, a length above the bound (§Bounds), which the consumer records as the error `oversized`; from `call`, a response it could not place, which is a build defect and no answer (§The request and the response).

The `abi` buffer of a conforming module is the 30 bytes `1a 00 00 00` followed by the 26 ASCII bytes of `sashite.sanki.kernel-abi/1`; it is the first buffer every consumer reads.

### Lifetimes

The **arena** is the region of memory the module hands out for requests and responses. It is reset by every `alloc`, so that no answer ever depends on a previous request (ADR-0034 §4): every request is self-contained. The protocol of one call is:

1. the consumer calls `alloc(len)` with the request's byte length and writes the request at the returned address;
2. the consumer calls `call(ptr, len)` with that address and a length at most the one reserved — the request MUST be the one the last `alloc` reserved: a `call` whose `[ptr, ptr + len)` is not within the last reservation (an address from an earlier `alloc`, a length beyond it, a sum overflowing `u32` or exceeding the memory) has no defined answer and MAY trap, which is the consumer's defect, not the build's — except that a `len` above the request bound is answered `oversized` before any memory is read (§Bounds);
3. the consumer reads the response buffer at the returned address, **before** its next `alloc`, which invalidates it.

The buffer returned by `abi` lives outside the arena and is valid for the instance's life. A consumer MAY keep one instance per module for as long as it likes, and MAY discard and re-instantiate it at any point: an instance holds nothing a later answer depends on.

### Bounds

- A **request** is at most **32 MiB** (33 554 432 bytes). `alloc` returns `0` for a larger length, and a `call` whose `len` exceeds the bound (reachable only by a consumer bypassing `alloc`) answers the error `oversized` without reading memory. The consumer MUST NOT submit a larger request.
- A **response** is at most **4 MiB** (4 194 304 bytes). A conforming module never produces a larger one for a request within bound (the largest, a `ggn` table, is about 1.2 MB). A length prefix above the bound, or a buffer extending past the end of memory, is a build defect: the consumer treats it as no answer.
- The **memory** is 128 MiB, fixed. The consumer MUST refuse a module whose `memory` export declares another size or no maximum.

The request bound is chosen so that **every session under a conforming timing authority fits**, whatever its players publish. Two ceilings make that true: the session's one timing authority — the designated timing relay or the designated timestamper ([Canonical Timing](https://github.com/sashite/nostr/blob/main/nips/canonical-timing.md) §Timing modes and mode selection) — confers canonical timing on at most `M = 32` Plies of one signer for one `step`, and attests an event once (ADR-0034 §5; [Canonical Timing](https://github.com/sashite/nostr/blob/main/nips/canonical-timing.md) §Bounding a signer's candidates for one slot, `M` being four times the selection cap `K`), and no Ply beyond the module's `max_step` (§`describe`) can ever enter a chain, so the consumer offers none (§The consumer's side). Under them, measured on the wire model of §The events in the compact form a consumer emits (no insignificant whitespace, the minimal escaping of §JSON, timings of thirteen digits):

| Request | Size |
|---|---|
| a typical session — 80 half-moves, one candidate each, attested | 45 KB |
| 600 half-moves, `M` candidates in every slot, 20-character contents, self-timed | 5.7 MiB |
| the same, attested (one attestation per Ply) | 10.3 MiB |
| the extreme — 600 half-moves, `M` candidates in every slot, every content 256 four-byte characters, attested | 28.7 MiB |

The extreme is two players flooding their own windows with the longest and widest contents kind `3423` admits — 256 code points, none of them a control character, so none costs more than four bytes on the wire (kind `3423` §Content); it fits, so a flood harms no one's ability to evaluate — a consumer never meets `oversized` on a session whose timing authority applied the ceiling. A module whose `max_step` is larger than the reference's MUST still keep the extreme within the bound (§Conformance of a build, item 5). A consumer that inflates its requests — pretty-printing, escaping non-ASCII characters — spends its own margin; so does a timestamper that attests an event more than once.

## Encodings

### JSON

Requests and responses are **UTF-8 JSON texts** (RFC 8259) whose value is an object. A request is parsed **leniently as to form** — any member order, any insignificant whitespace — and **strictly as to content**: a duplicate member name, an unknown member name at any depth, a missing member, a value of another shape than its schema's, a number not matching `^-?(0|[1-9][0-9]*)$` (a fraction, an exponent, a leading zero, a negative zero), a string containing an unpaired surrogate escape, a leading byte-order mark, bytes after the top-level object other than JSON whitespace, or nesting deeper than **eight** levels (the deepest schema below has four), is `malformed` — so that a consumer never believes an option was honoured when it was ignored, and no request can drive the parser into depth.

A **response** is emitted in **canonical form**: object members sorted by key, no insignificant whitespace, integers written without exponent or leading zeros (a minus sign for a negative value), strings escaped as RFC 8259 requires and no more (only `"`, `\`, and the C0 controls are escaped, the last as `\u00xx` with lowercase hexadecimal digits except `\n`, `\r`, `\t`, `\b`, `\f`; every other character, non-ASCII included, is emitted raw). Every object key a module emits is **ASCII** — the keys of this ABI's schemas, and those of the module-shaped members (`describe.parameters`, `ggn`) alike — so that sorting by code point, by UTF-8 byte and by RFC 8785's UTF-16 order coincide. A response a module generates from its own tables (`ggn`) is in this form too. Two conforming runtimes therefore return byte-identical responses, which is what the conformance run compares (§Conformance of a build).

**Numbers are integers.** No request or response carries a fraction or an exponent. Every integer of a request lies in the range its value class gives (§Values); those ranges are chosen so that every integer of this ABI's response schemas lies, by construction, in the safe range `[−(2^53 − 1), 2^53 − 1]` — the largest a response can carry, a clock's `remaining`, is a sum of at most 601 durations below 2^40.

The examples in this document are pretty-printed, unsorted and abbreviated for readability; the wire form of a response is the canonical form above.

### Values

| Value | Encoding |
|---|---|
| event id, pubkey | 64 lowercase hexadecimal characters (`^[0-9a-f]{64}$`); a request carrying uppercase or another length is `malformed` |
| timing (a canonical timing, a cutoff, t₀) | an integer number of seconds (a Nostr `created_at`) in `[0, 2^40)` |
| duration | an integer number of seconds in `[0, 2^40)` |
| side | `"first"` or `"second"` (the seat names of kind `3420` §Match-terms tags) |
| position | a **FEEN** string ([Position Encoding — Sanki](position-encoding-sanki.md)); one the module cannot parse — a style letter it does not define included — is `invalid_position` |
| move | the `content` string of a Ply (kind `3423`) — the `[source, destination, actor]` JSON array *as a string*, exactly as published ([Move Encoding — Sanki](move-encoding-sanki.md) §Content format); the module parses it, and an unparseable one is an illegal move, never `malformed` |
| status | a token of [Statuses — Sanki](statuses-sanki.md): `checkmate`, `stalemate`, `nomove`, `insufficient`, `repetition`, `movelimit`, `movecap`, `timeout`, `resignation`, `agreement`; position answers add **`ongoing`**, which is this ABI's, not a status, and never appears in a verdict |
| result | `{ "first": <score>, "second": <score> }`, each score `0`, `50` or `100`, the two summing to `100` — the seat-axis form of a Conclusion's `result` tags (kind `3425` §Tags) |
| verdict | `{ "result": <result>, "status": <status> }` |
| claim | what a Conclusion states, in kind `3425`'s own domain: a `status` matching `^[a-z]{1,32}$` and a `result` of two non-negative integers summing to `100` — a claim that is not a verdict the kernel can yield is `wrong` (§`check`), never `malformed` |
| time control | an array of one or more period triples `[duration, increment, plies]` in kind-`3420` order, `null` for an absent option, valid per kind `3420` §Match-terms tags (`plies` only with `increment`, `plies` in `[1, 2^31)`, `duration` `0` only in the three-element form) — the same encoding as the conformance corpus; an invalid one is `malformed`. The number of periods is bounded by the founding event alone; the module walks them iteratively (§Determinism and totality). `[300, 0, null]` and `[300, null, null]` are distinct time controls, as kind `3420` says, though arithmetically equal |
| clock | `{ "period": <int>, "plies_in_period": <int>, "remaining": <duration> }` — a player's clock as [Time Accounting — Sanki](time-accounting-sanki.md) describes it: the 0-based period index, the plies played in it, the seconds remaining; `period` and `plies_in_period` in `[0, 2^31)` |
| step | a positive integer in `[1, 2^31)`; a Ply whose `step` exceeds the module's `max_step` (§`describe`) is ignored (§The events) |
| variant | a variant identifier the module defines (`chess`, `ogi`, `xiongqi` for the reference module) |

### The session terms

Every session operation carries the session's **terms** — what the founding events fixed — in one `session` object:

```json
"session": {
  "id": "<game session event id>",
  "first": "<pubkey>",
  "second": "<pubkey>",
  "timestamper": "<pubkey>" | null,
  "time_control": [[300, 3, null]],
  "position": "<FEEN>",
  "start": 1700000000
}
```

- `id` — the Game Session's event id (kind `3422`); the Plies and Conclusions offered MUST reference it — the module ignores a Ply that does not and answers `other_session` for a Conclusion that does not (§The events);
- `first`, `second` — the two players by seat (the Game Session's `seat` tags); equal pubkeys are `malformed`;
- `timestamper` — the designated timestamper in attested mode, `null` in self-timed mode ([Canonical Timing](https://github.com/sashite/nostr/blob/main/nips/canonical-timing.md) §Timing modes and mode selection);
- `time_control` — the session's time control;
- `position` — the initial position (the Game Session's `content`), with `first` to move; the module refuses a position with `second` to move (`invalid_position`); whether it is the position the rule system prescribes is the consumer's check (kind `3422` §Semantic constraints, item 7, against `describe`);
- `start` — t₀, the canonical session start (kind `3422` §Canonical session start).

### The events

The session operations receive the session's public events in the abstract model of the session kernel — what remains of a Nostr event once the consumer has verified its signature, applied its kind's structural constraints, scoped it to the session and established its timing (or, for the one Conclusion `check` examines, not yet — §The consumer's side):

```json
"plies": [
  { "id": "<event id>", "signer": "<pubkey>", "session": "<game session event id>", "step": 7,
    "draw": false, "content": "[\"e2\",\"e4\",null]", "created_at": 1700000100 }
],
"attestations": [
  { "id": "<event id>", "signer": "<pubkey>", "attests": "<event id>", "created_at": 1700000101 }
],
"conclusions": [
  { "id": "<event id>", "signer": "<pubkey>", "session": "<game session event id>",
    "status": "checkmate", "result": { "first": 100, "second": 0 }, "created_at": 1700005000 }
]
```

- a **ply** is a Ply (kind `3423`): its id, its signer, the Game Session it references (its `game_session`-marked `e` tag), its `step`, whether it carries the `draw` flag, its `content` verbatim, and its own `created_at` — the canonical timing in self-timed mode, ignored in attested mode;
- an **attestation** is an Event Timestamp Attestation (kind `3410`) of a Ply or of a Conclusion — one array holds both: its id, its signer, the event it attests (its `attests`-marked `e` tag) and its `created_at`; only those signed by the session's timestamper count, and the module applies that filter itself, whatever the consumer offered;
- a **conclusion** is a Conclusion (kind `3425`): its id, its signer, the Game Session it references, its claim — the status it states (its `content`) and the result it states on the seat axis (its `result` tags mapped by the session's seats) — and its own `created_at`.

The module is total over these inputs, and defensive where a consumer could mix or overfill them: a Ply whose `session` is not the request's, whose `signer` is neither player, or whose `step` exceeds `max_step` (it could enter no chain) is **ignored**; a Conclusion whose `session` is not the request's, or whose signer is not a player, is out of reach (`no_verdict`, §`check`). Event ids are unique, as Nostr guarantees; the module does not re-verify it.

## The request and the response

A request is an object with an `op` member naming the operation and the operation's own members. A response is either the operation's result object, or an **error**:

```json
{ "error": { "code": "<code>", "message": "<free text>" } }
```

| Code | Meaning |
|---|---|
| `oversized` | the request exceeds the bound (§Bounds): `alloc` returned `0`, or a `call` bypassing it was refused before parsing |
| `malformed` | the request is not a JSON object of this ABI (§JSON): invalid JSON, a missing, unknown or duplicate member, a value of the wrong shape or out of its range, a malformed id, an invalid time control |
| `unsupported_op` | the `op` is not one of this ABI's |
| `unknown_variant` | (`ggn` only) a variant identifier the module does not define |
| `invalid_position` | a FEEN string the module cannot parse, or a session `position` with `second` to move |
| `illegal` | (`apply` only) the move is not legal in the position; `message` names the reason, documentary |
| `inconsistent` | the replay hit a broken internal invariant (the session kernel's `ChainEnd::Inconsistent` in `sashite-sanki-session`): no answer is defined; a build defect, never a verdict |

`message` is free text for humans; a consumer MUST NOT key behaviour on it. A conforming module never traps on a request within bound offered per §Lifetimes: a trap is a build defect (§Determinism and totality).

**No answer.** An `oversized` request, an `inconsistent` response, a trap, a `0` from `call`, a response buffer beyond the bound — each leaves the question unanswered for that consumer. The session is then **unverified** for it, never non-conforming (kind `3417` §Security considerations), and nothing about the session is inferred from the failure.

## Operations

### Rule system

#### `describe`

Request: `{ "op": "describe" }`.

Response:

```json
{
  "abi": "sashite.sanki.kernel-abi/1",
  "game": "sanki",
  "kernel": "sashite.sanki.kernel/1",
  "max_step": 300,
  "parameters": {},
  "positions": { "chess/chess": "<FEEN>", "chess/ogi": "<FEEN>" },
  "variants": {
    "chess": { "initial": { "first": "<FEEN fragment>", "second": "<FEEN fragment>" },
               "pieces": { "K": { "name": "king", "royal": true } },
               "style": "W" },
    "ogi": {}, "xiongqi": {}
  }
}
```

- `abi`, `game`, `kernel` — what the module is: this ABI's identifier, the game identifier a Rule System event naming this module MUST carry, and the identifier of the kernel specification it implements (documentary — the prose it was built to);
- `max_step` — the largest `step` a Ply can enter a chain with: `300` for the reference module, whose sessions end at 600 half-moves (`movecap`, [Kernel — Sanki](../../rules/kernel-sanki.md) §I.8) under the suite's alternation of seats;
- `parameters` — the parameters the kernel reads (thresholds, tables, the cap `K`, the dead-position predicates per pairing, the capture mutations, the promotion and drop rules), in the shape the module's kernel specification gives them. Its shape is the module's, not this ABI's;
- `positions` — the **initial position** of every ordered pairing, keyed `"<first variant>/<second variant>"`: the FEEN a Game Session MUST carry for those variants and seats (kind `3422` §Content);
- `variants` — the variant identifiers the module defines, each with its style letter ([SIN](https://sashite.dev/specs/sin/1.0.0/)), its **piece vocabulary** (the actor names a Ply's `content` may carry, [Move Encoding — Sanki](move-encoding-sanki.md) §Piece-name format, with the royal flagged) and its initial fragments.

`abi`, `game`, `max_step`, `positions`, and the identifiers and piece vocabulary of `variants` are **normative**: they are what the Rule System event's tags, the consumer's step filter, kind `3422`'s initial-position constraint and a Ply's actor names are checked against. `kernel`, `parameters`, and the `style` and `initial` members of a variant are documentary.

#### `ggn`

Request: `{ "op": "ggn", "variant": "<variant>" }`.

Response: `{ "ggn": { … }, "variant": "<variant>" }` — the variant's complete pseudo-legal movement table as a [GGN v1.0.0](https://sashite.dev/specs/ggn/1.0.0/) object, the geometry the module moves by, generated from the module's own tables in canonical form. Documentary: the geometry is readable and auditable, but a consumer computes nothing from it — legality is `legal_moves` and `apply`. `unknown_variant` for a variant the module does not define.

### Position

The position operations answer on a position alone, without a session's history: they are the kernel's Part I ([Kernel — Sanki](../../rules/kernel-sanki.md) §I).

#### `legal_moves`

Request: `{ "op": "legal_moves", "position": "<FEEN>" }`.

Response: `{ "moves": ["<move>", "…"] }` — every legal move of the side to move, under the full rule system (royal safety, castling conditions, uchifuzume, drop restrictions, promotion rules included), each as the `content` string a Ply carrying it would have, sorted as byte sequences of their UTF-8 form. Empty for a position with no legal move.

#### `apply`

Request: `{ "op": "apply", "position": "<FEEN>", "move": "<move>" }`.

Response: `{ "irreversible": true, "position": "<FEEN>", "status": "<status>" }` — the canonical resulting position, whether the move is **irreversible** for the move-limit counter (the bit of [Kernel — Sanki](../../rules/kernel-sanki.md) §I.1 — a capture or a foot-soldier move under the reference parameters), and the resulting position's **intrinsic** status (`ongoing`, `checkmate`, `stalemate`, `nomove`, `insufficient`). `illegal` when the move is not legal, with the reason in `message` (documentary — the reason vocabulary is the module's).

#### `classify`

Request: `{ "op": "classify", "position": "<FEEN>" }`.

Response: `{ "status": "<status>" }` — the position's intrinsic status: `ongoing`, `checkmate`, `stalemate`, `nomove` or `insufficient`, in the kernel's order. The background terminations that depend on history — `repetition`, `movelimit`, `movecap` — are session questions, answered by `natural_state`.

### Session

The session operations replay a session's public events under the kernel's Part II: the natural state at a cutoff and the verdict it yields. They all take `session`, `plies` and `attestations` (§The session terms, §The events).

#### `natural_state`

Request: `{ "op": "natural_state", "session": {}, "plies": [], "attestations": [], "cutoff": <timing> }`.

Response:

```json
{
  "chain": [ { "at": <timing>, "id": "<ply event id>" } ],
  "end": { "terminal": { "at": <timing>, "verdict": <verdict> } }
              | { "ongoing": { "anchor": <timing>,
                               "clocks": { "first": <clock>, "second": <clock> },
                               "half_move": <int>, "position": "<FEEN>" } }
}
```

The **chain** is the selected canonical Plies in play order, each with its canonical timing — the natural state of kind `3425` §Natural state of events at the cutoff under the two-window selection of [Move Encoding — Sanki](move-encoding-sanki.md). The **end** is either **terminal** — the replay reached a rule-system ending or a played-Ply timeout, with the verdict and the canonical timing of the Ply that reached it — or **ongoing**: the end position, the play-order index of the next half-move, the chain's anchor (the maximum canonical timing among the selected Plies, t₀ for an empty chain — the value the abandonment clock runs from), and both clocks as the replay left them. `before_start` is not an answer here: the natural state is defined at any cutoff, and a cutoff before t₀ simply admits no Ply.

#### `verdict_at`

Request: `{ "op": "verdict_at", "session": {}, "plies": [], "attestations": [], "invoker": <side>, "cutoff": <timing> }`.

Response: `{ "verdict": <verdict> }` — the verdict the rule system yields at `cutoff` with `invoker` as the concluding side: the natural state's terminal verdict if the replay reached one, else the invocation resolved on the ongoing end position in the normative order — `agreement`, abandonment `timeout`, residual `resignation` ([Statuses — Sanki](statuses-sanki.md) §Verdict resolution). Or `{ "no_verdict": "before_start" }` when `cutoff` precedes `start`: the session is not playable before t₀, and a client probing ahead of a scheduled start is told so, not told it would resign.

This is what a client calls, with its own side and the present instant, before publishing a Conclusion: the answer is the claim the Conclusion MUST carry to conform.

#### `check`

Request: `{ "op": "check", "session": {}, "plies": [], "attestations": [], "conclusion": <conclusion> }`.

Response, one of:

```json
{ "conforming": <verdict> }
{ "wrong": { "claimed": <claim>, "expected": <verdict> } }
{ "no_verdict": "other_session" | "not_a_player" | "pending" | "before_start" }
```

Item 8 of kind `3425` §Semantic constraints on the rules axis. The module first decides whether the Conclusion is **in reach**, in this order: it references this session (`other_session` otherwise), its signer is a player (`not_a_player`), it has canonical timing (`pending` — the one transient reason: re-examine once timed), and its cutoff — that timing — is not before t₀ (`before_start`, final). Then it judges the claim: **`conforming`** — the Conclusion claims exactly the verdict the rule system yields at its cutoff with its signer as the invoker; the verdict is returned. **`wrong`** — anything else: a different verdict, a status the kernel never yields, a result the kernel never yields, a status and a result of different kinds (a decisive status with a draw, or the converse); `claimed` echoes the claim as offered and `expected` is what a conforming Conclusion at that cutoff would carry.

#### `select_conclusion`

Request: `{ "op": "select_conclusion", "session": {}, "plies": [], "attestations": [], "conclusions": [<conclusion>] }`.

Response: `{ "canonical": { "cutoff": <timing>, "id": "<event id>", "verdict": <verdict> } }` or `{ "canonical": null }`.

The session's **canonical Conclusion** (kind `3425` §Idempotence and finality): among the offered Conclusions that are conforming with established timing, the earliest by canonical timing, smallest event id as tiebreaker, returned with its cutoff and its verdict. `null` while none conforms: the session is open for this consumer — still played, or awaiting a pending Conclusion's timing.

The selection is **decomposable**: each Conclusion is judged on its own (`check`), and the canonical one of a union of lists is the earliest, by `(cutoff, id)`, of the canonical ones of the lists. A consumer holding more Conclusions than one request carries MAY therefore partition them and keep the earliest of the answers — provided every part answered.

### Corpus primitives

The four operations below expose, in isolation, the primitives the conformance corpus pins vector by vector ([Sanki conformance vectors](../conformance/README.md), categories B, C and E). They exist so that every category of the corpus but `puzzle` runs *through the module* (§Conformance of a build). They are **not normative for a session**: a consumer computes no chain, no charge and no clock of its own with them — the session operations are the only source of those answers.

#### `select`

Request: `{ "op": "select", "boundary": <timing>, "cap": <int>, "candidates": [ { "id": "<string>", "created_at": <timing>, "legal": true } ] }`.

Response: `{ "result": "applied" | "unfilled", "selected": "<id>" | null }` — the two-window rule ([Move Encoding — Sanki](move-encoding-sanki.md) §Slot candidates and selection) against the given boundary and cap, with legality taken as given. `cap` is in `[1, 2^31)`. Candidate ids are arbitrary strings here, compared as byte sequences of their UTF-8 form for the tiebreak, as in the corpus.

#### `elapsed`

Request: `{ "op": "elapsed", "anchor": <timing>, "timing": <timing> }`.

Response: `{ "elapsed": <duration> }` — `max(0, timing − anchor)` ([Time Accounting — Sanki](time-accounting-sanki.md) §Elapsed time).

#### `charges`

Request: `{ "op": "charges", "start": <timing>, "timings": [<timing>] }`.

Response: `{ "anchor": <timing>, "first": [<duration>], "on_move": <side>, "second": [<duration>] }` — the per-side charges of a chain given its per-Ply timings in play order from t₀ (`start`), the anchor advancing to `max(anchor, timing)` after each half-move and never rewinding, the final anchor, and the side on move after the chain. `timings` holds at most `2 × max_step` entries — a chain is no longer — and more is `malformed`.

#### `clock`

Request: `{ "op": "clock", "time_control": <time control>, "clock": <clock>, "elapsed": <duration> }`.

Response: `{ "kind": "flagged" }` or `{ "clock": <clock>, "kind": "continued" }` — one tick of the mover's clock: the elapsed seconds spent, the increment applied per the period's kind, `plies_in_period` advanced, the period advanced or repeated, a bank overspend rolled into the next period, or the flag ([Time Accounting — Sanki](time-accounting-sanki.md)). A `clock` whose `period` names no period of the time control is `flagged`, never trusted.

## Normativity

For a session, the rules are the answers of `natural_state`, `verdict_at`, `check` and `select_conclusion`, and of `legal_moves`, `apply` and `classify` on its positions; the initial position, the piece vocabulary and `max_step` are those `describe` reports. Those answers **define** the rule system the module is: a consumer that computes any of them by other means — its own kernel, another module, a reading of the prose — is not evaluating the session under its rules, whatever it obtains.

`describe.kernel`, `describe.parameters`, a variant's `style` and `initial`, `ggn` and every `message` are **documentary**: readable, auditable, never computed from. The **corpus primitives** are test-only. An error is never a verdict.

## Determinism and totality

A conforming module is a pure function of its request. It satisfies, by construction (ADR-0034 §5):

- **integers only** — the kernel computes with integers alone (no floating-point arithmetic in its sources), no number crosses the boundary with a fraction, and no answer depends on a floating-point value: the only float instructions the module contains are its JSON library's, which a request within this ABI never reaches with a fraction — a Ply's `content` carrying one is parsed to an illegal candidate, and the value plays no part in the answer;
- **no clock, no randomness, no threads** — every instant is an input (the import section being empty, none is available anyway);
- **no iteration over unordered containers** whose order could depend on anything but the request;
- **bounded loops and recursion** — the kernel is bounded by 64 squares, `max_step` and the cap `K`, every other loop by the request's size; the parser accepts eight levels of nesting and no more (§JSON), and no recursion follows an input-sized structure (a time control's periods, a candidate list, a JSON array — all walked iteratively), so that the module's call depth is bounded independently of its input and no request exhausts a host's stack;
- **fixed memory** — no `memory.grow`; the memory is sized for the largest request, the largest response and the replay of the extreme session of §Bounds;
- **totality** — every request within bound receives a response; a trap on such a request is a build defect, and a session on which a published build traps cannot be concluded, which is why the conformance run below exists.

There is **no instruction budget**: a request within bound completes. A consumer MAY place a wall-clock watchdog on a module from a publisher it does not recognise; that is a liveness guard for the consumer's own sake, never a verdict (kind `3417` §Security considerations).

### Conformance of a build

Before a module is published under this ABI, its publisher verifies, and SHOULD publish the evidence that:

1. the module validates against the WebAssembly profile of §The module, its import section is empty, it has no start section, and its `memory` is `min = max = 2048` pages; the build's own checks enforce the constructive properties above — no floating-point arithmetic and no iteration over hash containers in the sources (lints), no recursion over input-sized structures;
2. `abi` returns `sashite.sanki.kernel-abi/1`;
3. the whole conformance corpus but its `puzzle` category runs through the module — `legality` through `apply` and `classify`, `selection` through `select`, `time` through `elapsed` and `charges`, `scenarios` through `natural_state` and `verdict_at`, `clock` through `clock` — in **two runtimes** (a browser's WebAssembly engine and a pure-Rust interpreter) with byte-identical responses; the `puzzle` category runs through the puzzle validator with the module underneath;
4. a random-session differential against the native reference engine finds no divergence — the legal set of every position reached (`legal_moves`), the position and status after every move (`apply`), and the chain and verdict of every session (`natural_state`, `verdict_at`);
5. the extreme session of §Bounds — 600 half-moves, `M` candidates in every slot, every content 256 four-byte characters, attested — completes in both runtimes, within the request bound and the memory.

The corpus's vectors become requests under one fixed mapping, so that two harnesses obtain the same answers: a vector id becomes a 64-hex id by an injective, **order-preserving** map (the reference harness zero-pads the id's ASCII bytes to 32 bytes and hex-encodes them, so that the ids' string order — which the equal-timing race vectors rely on — is the hex order); a `seat` becomes the session pubkey of that seat, the two pubkeys distinct; a `move` array becomes the `content` string, its compact JSON text (no whitespace); `t0` becomes `session.start` and `timedAt` a Ply's `created_at`; an absent `draw` is `false`; a scenario runs **self-timed** — `timestamper` `null`, no attestations — and MAY additionally run attested, with one synthetic attestation per Ply carrying `timedAt` under ids disjoint from the Plies', the two answers being required equal; an absent `timeControl` is `[[3600, null, null]]`; the remaining camelCase names map one to one onto this ABI's snake_case members (`createdAt` → `created_at`, `pliesInPeriod` → `plies_in_period`, `onMove` → `on_move`). The mapping is stated in full in [Sanki conformance vectors](../conformance/README.md) §Evaluation contract.

The reference module's build is reproducible from the sources its Rule System event names (`source`), in a pinned container; reproducibility is auditability — the bytes rule regardless ([Rule System — Sanki](rule-system-sanki.md)).

## The consumer's side

**Before calling.** The consumer maps Nostr events to the objects of §The events and hands the module only what has passed the checks that are the consumer's — and none of the checks that are the module's. The filters below are of two kinds, and both keep consumers in agreement: the **conformance filters** drop events that do not exist for the protocol, and every consumer drops the same ones because the kinds define them; the **relevance filters** drop only events the module would ignore anyway, so applying them changes no answer. Together they are what keeps requests within the bound.

- **Conformance** — signatures (NIP-01) and each kind's structural constraints: for a Ply (kind `3423` §Semantic constraints) items 1–4, 6 and the transport half of item 5 (a non-empty string of at most 256 code points, free of control and bidirectional formatting characters); for a Conclusion (kind `3425` §Semantic constraints) items 1–7; for an attestation, its own kind's. The consumer MUST NOT judge a Ply's `content` against the move encoding, nor a Conclusion's claim against the rules: an unparseable content is an illegal candidate that still counts against the cap, and a claim is judged by `check`. Both are the module's.
- **Scope** — only Plies and Conclusions referencing the session's canonical Game Session; an attestation is scoped by the event it attests, not by a session tag of its own.
- **Steps** — only Plies whose `step` is at most the module's `max_step` (§`describe`); a larger one can enter no chain and would only spend the bound.
- **Timing, self-timed mode** — only Plies and Conclusions whose **acceptance by a designated timing relay** is established ([Canonical Timing](https://github.com/sashite/nostr/blob/main/nips/canonical-timing.md) §The pending state); an event whose acceptance is not established is pending and MUST NOT be offered, since the module would time it by its self-declared `created_at`.
- **Timing, attested mode** — only attestations signed by the session's timestamper, and only Plies that have at least one; the module resolves canonical timing from them itself (meta-resolution included), ignoring the events' own `created_at`. A pending Ply MUST NOT be offered. A pending Conclusion MUST NOT be offered to `select_conclusion`; it MAY be offered to `check`, which answers `pending`.

Under these filters and the timing authority's ceiling `M`, a session's request is within the bound of §Bounds whatever its players publish.

**Running it.** The consumer instantiates the module in a sandbox with no host capabilities, checks `abi`, and keeps one instance per module digest for as long as it likes. A browser uses its WebAssembly engine; Sashité's Rust consumers use `wasmi` (ADR-0034 §5). The consumer emits its requests compactly and bounds every one it submits (§Bounds).

**With the answers.** A client displays the natural state, predicts the verdict it would carry by concluding (`verdict_at` with its own side and the present instant), publishes exactly that, and checks the opponent's Conclusion (`check`); a rating authority rates the canonical Conclusion (`select_conclusion`) and nothing else; a puzzle validator walks certificates with `legal_moves`, `apply` and `classify`. A consumer caches the module by digest beside the Rule System event, so that the sessions it holds stay evaluable without the network (kind `3417` §Retrieval and verification).

## References

- [ADR-0034](../../adr/adr-0034-reference-build.md) — the rule system is a signed event naming an executable module; §5 fixes the per-slot ceiling `M`
- [Rule System (kind `3417`)](https://github.com/sashite/nostr/blob/main/nips/3417-rule-system.md) — the event that names the module and states its ABI
- [Kernel — Sanki](../../rules/kernel-sanki.md) — the kernel the reference module implements
- [Move Encoding — Sanki](move-encoding-sanki.md), [Statuses — Sanki](statuses-sanki.md), [Time Accounting — Sanki](time-accounting-sanki.md), [Position Encoding — Sanki](position-encoding-sanki.md) — the vocabularies this ABI carries
- [Rule System — Sanki](rule-system-sanki.md) — the reference module: what it contains, how it is built and published
- [Sanki conformance vectors](../conformance/README.md) — the corpus a build runs through, and the mapping of its vectors onto this ABI
- [Canonical Timing](https://github.com/sashite/nostr/blob/main/nips/canonical-timing.md) — timing modes, meta-resolution, the pending state, the per-slot ceiling `M`
- [WebAssembly Core Specification](https://webassembly.github.io/spec/core/) — the execution model
- [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259) — JSON; [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) — JSON Canonicalization Scheme (the member order of responses coincides with it)
