Kernel — Sanki (`sashite.sanki.kernel/1`)
==========================================

`reference` `rule-system` `normative`

This document specifies the **kernel** that the Sanki rule-system document names — the fixed rules that the document's data (the GGN movement tables and the parameters of [Rule System — Sanki](https://blobs.sanki.app/eee64155f8b70c63e46acd7fe6179e3ef872137cee7f2352f268bccdd129e5c3.md)) parameterise. It is the document whose SHA-256 the manifest carries as `kernel.digest`, and it is written to be the load-bearing half of an **executable contract**: everything a session's verdict depends on is either stated here, incorporated here by digest, or carried by the rule-system document; nothing is left to an arbiter, because under ADR-0033 there is none.

The kernel has two layers. The **position kernel** (Part I) is the per-ply transition `legality → apply → canonicalize`: given a position, a ply and the rule-system data, it either rejects the ply or yields the next canonical position. The **session kernel** (Part II) is the natural-state function: given the public events of a session — its founding, its Plies with their canonical timings, and the invocation that ends it — it yields the verdict. The position kernel is a pure function of a position and a ply; the session kernel is a pure function of events. Neither has any other input.

## Identification and contract

| Field of the manifest | Value | Meaning |
|---|---|---|
| `kernel.id` | `sashite.sanki.kernel/1` | the name of this set of rules; a consumer that does not implement it MUST refuse the manifest |
| `kernel.digest` | the SHA-256 of this document as published | pins the text — two parties agreeing on the identifier agree on these words |
| `kernel.conformance` | the SHA-256 of the conformance corpus (§Conformance) | pins the executable form of the same rules |
| `kernel.reference` (optional) | the SHA-256 of a reference implementation | pins one program whose output *is* the verdict |

**Incorporated documents.** Four documents of the Sashité suite are part of this kernel by incorporation; their digests at the time of publication are listed here, so that `kernel.digest` transitively pins them. They are normative for what they specify; this document restates their rules only as far as needed to name them.

| Document | Specifies | SHA-256 at publication |
|---|---|---|
| [Time Accounting — Sanki](https://blobs.sanki.app/171f71cbccf471c5952fef31688a8bf58d6d159bd2abf59f5efeb255e450c65b.md) | the clock arithmetic (§II.4) | `171f71cbccf471c5952fef31688a8bf58d6d159bd2abf59f5efeb255e450c65b` |
| [Move Encoding — Sanki](https://blobs.sanki.app/ed49e170861d0c4fadc9c9789579479d7250e5ef9653227d5a7fb998ef4bf756.md) | the ply content, the slot structure, the selection rule and the cap `K` (§II.2–II.3) | `ed49e170861d0c4fadc9c9789579479d7250e5ef9653227d5a7fb998ef4bf756` |
| [Statuses — Sanki](https://blobs.sanki.app/82eb9648a58fcc511198758fe39d1cdcb00c292401deece80f6394c9247380e7.md) | the status vocabulary, the implicit conventions and the verdict resolution (§II.5–II.6) | `82eb9648a58fcc511198758fe39d1cdcb00c292401deece80f6394c9247380e7` |
| [GGN v1.0.0](https://sashite.dev/specs/ggn/1.0.0/) | the movement-table format the position kernel reads | `c054ef693a1f6c3b367d94390c512e581730a39ac85c0a8f40bba921cce175b4` |

**The corpus rules over the prose.** An implementation that passes the conformance corpus is a conforming implementation of this kernel. Where the prose of this document or of an incorporated document admits two readings and the corpus decides between them, the corpus is right. Where the corpus is silent and two conforming implementations diverge, the kernel is **defective**: the defect is repaired by publishing a new kernel identifier with a corpus that covers the case — never by editing this document or its corpus under the same identifier, since sessions founded under it have signed these exact digests.

**No discretion.** Nothing in this kernel is "MAY" for the party computing a verdict. The one place a rule-system document could leave a choice open — a non-deterministic initial position — is excluded: a Sanki rule-system document MUST prescribe a deterministic initial position for every pairing it defines ([Rule System — Sanki](https://blobs.sanki.app/eee64155f8b70c63e46acd7fe6179e3ef872137cee7f2352f268bccdd129e5c3.md) §Variants).

---

## Part I — The position kernel

### I.1 Inputs and output

The position kernel takes:

- a **position** in canonical [FEEN](https://sashite.dev/specs/feen/1.0.0/) form — board, two hands, the active side and the two styles;
- a **ply** in the Sanki content form `[from, to, actor]` ([Move Encoding — Sanki](https://blobs.sanki.app/ed49e170861d0c4fadc9c9789579479d7250e5ef9653227d5a7fb998ef4bf756.md) §Content format): `from` a square or `null` (a drop), `to` a square, `actor` a piece name or `null`;
- the **rule-system data**: the GGN document of each side's variant and the manifest's parameters.

It returns either **Illegal** (with a reason, informational) or the **next canonical position** together with one bit, *irreversible*, consumed by the session kernel's move-limit counter (§I.8).

The pipeline is fixed: **resolve** (§I.4) → **uchifuzume** (§I.5) → **apply** (§I.6) → **capture mutation and hand canonicalisation** (§I.6) → **canonicalize** (§I.7). A rejection at any stage leaves the position untouched.

### I.2 Data the kernel reads

The GGN documents enumerate, for every [QPI](https://sashite.dev/specs/qpi/1.0.0/) the variant uses, every source (a square, or the hand `*`) and every destination, the **possibilities** — each a set of [LCN](https://sashite.dev/specs/lcn/1.0.0/) pre-conditions (`must`/`deny`) on named squares. A possibility is **satisfied** in a position when every one of its conditions holds. A movement absent from the documents is not possible (GGN §13.2, exhaustiveness).

**Condition matching (normative).** `"empty"` is satisfied by an unoccupied square; `"enemy"` by a square holding a piece whose Side differs from the Side of the QPI under which the possibility is listed (GGN §7.2); a QPI value by a piece whose style letter (cased by its Side), state marker, letter and terminal marker all equal the value's — exact token equality, the EPIN derivation marker `'` (not part of QPI) being ignored. A `must` object is a conjunction, a `deny` object a disjunction (GGN §7.3).

The manifest's parameters the position kernel reads are listed in Appendix B. Everything else in this Part is a fixed rule.

### I.3 Derived relations

**A1 — Attack.** A square *s* is *attacked by side X* in a position if and only if some piece of X has, from its square, a satisfied possibility with destination *s* whose destination condition is `enemy`, the possibility being evaluated as though *s* were occupied by an enemy piece. The relation is derived from the GGN alone; it therefore excludes foot-soldier pushes, the xiongqi General's step to an empty square and castling (destination `empty`), and includes the General's Chariot-range capture. Every rule below that mentions attack, check or safety is phrased in this relation.

**In check.** A side is *in check* when the square of a piece of that side bearing the terminal marker `^` is attacked by the other side.

**Pseudo-legal set.** The pseudo-legal moves of the active side are the satisfied possibilities of its pieces (board and hand). **Legal set.** The pseudo-legal moves that survive §I.4 (royal safety) and §I.5 (uchifuzume).

### I.4 Resolution of a ply (A8, A6, E)

A ply `[from, to, actor]` is **admitted** if and only if all of the following hold; otherwise it is **rejected** with the first failing condition as reason:

1. **Possibility.** `from` is a square holding a piece of the active side, or `null` with `actor` naming a piece the active side holds in hand (*hand availability*, a conservation fact read on the FEEN hands); and some possibility of that piece's QPI from that source to `to` is satisfied.
2. **Actor.** `actor` obeys the promotion table (parameter): for a board move that promotes — the piece is a promoting letter of its variant and `to` lies on its promotion rank, `from` not already on it — `actor` MUST name one of the table's targets when the table lists several, and MUST be `null` when it lists one (automatic promotion); for every other board move `actor` MUST be `null`; for a drop `actor` names the dropped piece.
3. **Royal safety (A6).** The canonical position resulting from the ply (after §I.6–I.7) does not leave the mover in check. This single condition covers pins, moving into check, failing to parry, and castling out of, through or into check — the last through §I.7 A5, which withholds `+` from a rook whose castling would cross an attacked square.

**A8 — Rejection is not a sanction.** A rejected ply changes nothing: the position, the hands, the clocks and the turn are as before. The session kernel treats it as a non-event (§II.3). There is no `illegalmove` termination.

**Effect recognition (E).** An admitted ply is one of exactly five effects, recognised on the *form* of the satisfied possibility, never on an annotation:

| Effect | Recognised when | Applied as |
|---|---|---|
| **E1** move / ordinary capture | the default | the piece leaves `from` and occupies `to`; an occupant of `to` is captured |
| **E2** castling | the possibility's `must` names a square holding a rook-class QPI of the mover marked `+` (the only possibility shape of this form in the whole GGN) | E1 for the royal; that rook is relocated to the square of the straight segment `from→to` adjacent to `to` |
| **E3** en passant | the possibility's `must` names a square holding an enemy foot-soldier marked `-` | E1 for the mover; that foot-soldier is captured although it does not stand on `to` |
| **E4** promotion | condition 2 above identifies a promotion | E1, then the piece at `to` is replaced by the target letter (from `actor`, or the single target) in the mover's case, without state marker |
| **E5** drop | `from` is `null` | the hand token is placed on `to` exactly as held (no marker, no capture, no promotion), and removed from the hand |

The turn passes to the other side after every effect (a protocol invariant, not a rule of this kernel).

### I.5 Drop restrictions (A7)

Two restrictions apply to drops (E5) above the GGN's own conditions (an empty destination; for a letter the document keeps off a rank, its omission there):

- **Nifu** (parameter `drops.nifu`, `["F"]` for ōgi): a piece whose letter is listed may not be dropped on a file where the same side already has an unpromoted piece of that letter, *whatever its state marker* (`F`, `+F` or `-F` alike). This rule is the kernel's and not the document's because GGN matches a QPI value exactly and a `deny` object admits one value per square, so the three states cannot be denied together (GGN §7.3, *Limitation*).
- **Uchifuzume** (parameter `drops.uchifuzume`, `["F"]` for ōgi): a drop of a listed letter is rejected when the resulting position would be **checkmate** for the opponent (§I.8, evaluated one ply ahead). A drop that merely gives check is admitted.

Both lists may be empty.

### I.6 Capture mutation and hand canonicalisation

When an effect captures (E1 with an occupant, E3), the captured token goes to the **capturer's** hand after the variant's capture mutation (parameter, keyed by the *capturer's* variant):

1. **A2′ — Hand canonicalisation (fixed rule).** A token in hand never bears a state marker: `+` and `-` are stripped. This is a recomposition of the position, not an action of the move; it is why [PMN](https://sashite.dev/specs/pmn/1.0.0/) writes `e5~f6` bare.
2. **Demotion (parameter).** The letter is replaced per the `demote` table (`T → F` in ōgi; empty elsewhere).
3. **Side and foreign pieces (parameter).** `side: "flip"` puts the token in the capturer's case (ōgi capturing ōgi); `side: "keep"` leaves it in the victim's case, an *inert* token the capturer can never drop (chess and xiongqi capturing anything). `foreign` says what a piece of *another* variant becomes: kept as is, or replaced by a letter of the capturer's variant (ōgi captures chess or xiongqi pieces as `F`).

A token is **droppable** by a side if and only if its case is that side's (§I.2: an inert token has no `*` entry in the GGN and so no possibility).

### I.7 Canonicalisation

After apply, four recompositions are performed on the resulting position, in this order, each a pure function of the board (and, for A5, of the ply's effect). They are mutually independent — none reads a marker another writes — and together they define **position identity**: two positions are the same for repetition (§I.8) if and only if their canonical FEEN strings are equal, markers and hands included.

| | Recomposition | Rule |
|---|---|---|
| **A2** | check marker | every piece marked `^` bears `-` if and only if its square is attacked (A1) by the other side; every other `-` on a `^` piece is removed |
| **A3** | double-step marker (parameter: foot-soldier letter and home rank per side) | `+` is removed from every foot-soldier not on its home rank; `+` is never added |
| **A4** | en passant window (parameter: foot-soldier letter, marching direction) | `-` is removed from every foot-soldier; then `-` is set on the foot-soldier that has just made a double step **if and only if** an en passant capture of it is *legal* for the side now to move — a capturer stands in position and the capture passes A6 — so that the marker is present exactly when the capture is possible. This is the kernel's only arrow from canonicalisation back into legality (a one-ply look-ahead on a tentative position). |
| **A5** | castling rights (no parameter — derived from the GGN) | a rook-class piece marked `R` (bare) stays bare forever. A rook marked `+R` or `-R` becomes bare `R` when the ply moved its royal (every marked rook of that side) or moved that rook, or when any piece arrives on the rook's square by capture. Otherwise it is marked `+R` if the royal's castling possibility that references it (the possibility whose `must` names its square with value `+R`) would be satisfied with, in addition, no square of the royal's transit — the straight segment `from→to`, source excluded — attacked (A1) and the royal not in check; and `-R` if not. The transit is read on the geometry of the possibility; the manifest carries no castling vocabulary. |

The initial position of a pairing carries no `-` markers and carries `+` only on the pieces the rule-system document lists (playing-principles §10, condition 4).

### I.8 Terminal classification (A9)

After canonicalisation, the resulting position is classified for the side now to move. Facts: *has a pseudo-legal move*, *has a legal move* (§I.3), *in check*, and the four background conditions below. The classification order is fixed:

1. if there is **no legal move**: `nomove` (a draw) when there is not even a pseudo-legal move; else `checkmate` (the side to move loses) when in check; else `stalemate` (outcome: parameter `session.stalemate`, `draw` for Sanki);
2. otherwise, in this order: `insufficient` (a draw) when one of the **dead-position predicates** the pairing enables holds (parameter `pairings.<pair>.dead_positions`, names from this fixed catalogue: `lone_royals` — only the two royals on the board; `royal_and_bishop_vs_royal` and `royal_and_knight_vs_royal` — a lone royal against a royal and one minor, either orientation; `royals_and_same_colour_bishops` — royals and any number of bishops all standing on squares of one colour; `royals_only_with_inert_hands` — only the two royals on the board and no hand holding a token droppable by its holder); `repetition` when the canonical position has now occurred `session.repetition` times (3) in the session, initial position included; `movelimit` when `session.move_limit` (100) half-moves have passed without an irreversible move; `movecap` when the session has reached `session.move_cap` (600) half-moves;
3. otherwise the session is **ongoing**.

**Irreversible** (parameter `session.irreversible`): a ply is irreversible when it captures, or when it is a board move of an unpromoted foot-soldier (read on the source piece, so a promotion counts); a drop and a castling are not. The move-limit counter resets on an irreversible ply and counts every other one.

A delivered checkmate takes precedence over any background draw the same ply would produce, by the order above.

---

## Part II — The session kernel

### II.1 Inputs and output

The session kernel takes the **founding** (the Game Session: players, seats, variants, `rules`, `time_control`, t₀), the session's **Plies** with their **canonical timings** ([Canonical Timing](https://github.com/sashite/nostr/blob/main/nips/canonical-timing.md)), and an **invocation**: the pubkey of the player ending the session and a **cutoff** timing. The protocol layer supplies the invocation — before ADR-0033 the Adjudication Request, under it the Conclusion event itself, whose signer and canonical timing are the invocation. Plies with canonical timing strictly after the cutoff, and Plies without canonical timing, are not inputs.

It returns a **verdict**: a status of the [Statuses — Sanki](https://blobs.sanki.app/82eb9648a58fcc511198758fe39d1cdcb00c292401deece80f6394c9247380e7.md) vocabulary and a result split — `100/0`, `50/50` or `0/100`, seats being the axis. A conforming invocation always yields a verdict (§II.6).

### II.2 t₀, slots and play order

t₀ is the Game Session's `start_at` when present, otherwise its canonical timing. A Ply's slot is `(session, signer, step)`, `step` being the signer's own move ordinal; the play order interleaves the two sequences by strict alternation, seat `first` before seat `second` within each step ([Move Encoding — Sanki](https://blobs.sanki.app/ed49e170861d0c4fadc9c9789579479d7250e5ef9653227d5a7fb998ef4bf756.md) §Within-step ordering).

### II.3 The canonical chain

Slots are consumed in the play order. At each slot the **selection rule** picks the canonical Ply among the slot's candidates: the boundary `T` is the maximum canonical timing among the preceding half-moves (t₀ for the first), never rewound by a selected premove; candidates timed before `T` form the *anterior* window (premoves), the others the *informed* window (live moves); the canonical Ply is the **latest legal** anterior candidate among the `K` most recent, else the **earliest legal** informed candidate among the `K` earliest, else the slot is **unfilled** and the chain stops there. `K = 8` (parameter of the deployment, identical for every consumer of a session — it is part of the `rules` document's `session` object). Legality is the position kernel's (§I.4–I.5); an illegal candidate is skipped in either window and is never a termination.

### II.4 Clocks

Each selected Ply charges its mover `elapsed = max(0, t(ply) − anchor)`, the anchor being the boundary `T` of its slot; the charge is spent per the session's `time_control` under the rules of [Time Accounting — Sanki](https://blobs.sanki.app/171f71cbccf471c5952fef31688a8bf58d6d159bd2abf59f5efeb255e450c65b.md) (fixed bank / Fischer, quota periods, period transitions). A selected premove charges zero. If the mover's clock flags on the charge, the session terminates `timeout` against the mover at that Ply (**played-Ply timeout**); the position is still the one the Ply produces.

### II.5 Chain replay and termination

Starting from the pairing's initial position, each selected Ply is applied by the position kernel; after each, the clocks are charged (§II.4) and then the position is classified (§I.8). The first Ply that terminates — by `timeout` or by any status of §I.8 — ends the session at that Ply; later slots are irrelevant.

### II.6 Post-chain resolution

If the chain replays to an ongoing position — because a slot is unfilled, or because the cutoff cuts the candidates short — the invocation is resolved at the cutoff, in this order:

1. **`agreement`** (`50/50`): the last selected Ply carries the `draw` flag and the invoker is its signer's opponent;
2. **abandonment `timeout`**: the player on move has let their clock, charged from the chain's last anchor (t₀ for an empty chain) to the cutoff, expire — decisive against them;
3. **`resignation`**: otherwise, decisive against the invoker — the residual reading of an invocation with no objective cause.

The order is normative: an invocation by the offeree while a draw offer stands is an acceptance; an invocation while the on-move player's clock has expired is a win-on-time claim (or a loss on time if the invoker is that player); anything else is a resignation. A conforming invocation therefore always yields a verdict.

**Which invocation.** When several conforming invocations exist, the one with the smallest canonical timing (then the smallest event id) is the session's; the verdict is a function of that cutoff. Under ADR-0033 this is the slot rule of the Conclusion event, and a Conclusion whose stated verdict differs from the kernel's for its own cutoff is invalid.

---

## Part III — Conformance

### III.1 The corpus

The conformance corpus is the executable form of this kernel. It is published as **one JSON object** merging its categories under their names (`legality`, `selection`, `time`, `scenarios`, `clock`, `puzzle`), serialised with sorted keys and no insignificant whitespace; the SHA-256 of that document is `kernel.conformance`. It is composed of:

- **legality vectors** — position (FEEN) + ply → *admitted with the next canonical FEEN and the irreversible bit*, or *rejected*; they cover every effect E1–E5, every recomposition A2–A5 including the en passant look-ahead and the three castling refusals, nifu and uchifuzume, hand availability and inert tokens, promotion `actor` errors, and every cross-variant capture mutation;
- **classification vectors** — position (+ history for repetition and counters) → status; they cover `nomove` before `checkmate`/`stalemate`, the dead-position tables per pairing including the mixed-pairing rule, and the precedence of a delivered mate over a background draw;
- **clock vectors** — time control + sequence of charges → clocks and flags, per period type and transition;
- **scenario vectors** — complete sessions as event sets (founding, Plies with timings, invocation) → verdict; they cover premove selection and the non-rewinding anchor, the cap `K`, both timeout flavours, the three post-chain resolutions and their order, and cutoff exclusion;
- **differential vectors** — random positions → the full legal set, for the GGN documents to be checked against the generator they were produced from (the reference engine's `tests/ggn_differential.rs` performs this check on ~67 000 positions of random play across the nine pairings, on both the legal set and the attack relation).

Every vector carries the digests of the rule-system document it was computed under. A vector is never edited: a wrong vector is a kernel defect (§Identification and contract).

### III.2 Reference implementation

Sashité's reference implementation of this kernel is the `sashite-sanki-engine` crate (position kernel and clocks) with its session layer. When the manifest carries `kernel.reference`, that digest names a build of it (a WASM module with a fixed ABI: `natural_state(events) → verdict`), and the verdict of a session is by definition that program's output. Publishing the reference digest removes even the residual risk of prose ambiguity, at the price that a defect of the build is the rule until a new kernel is published.

---

## Appendix A — The atoms and where they live

| Atom | Fixed rule | Parameterised by | Reference module |
|---|---|---|---|
| E | the five effects, recognised on the form of the possibility | promotion table; capture mutation | `apply.rs`, `legality/resolve.rs`, `capture.rs` |
| A1 | attack, derived from the GGN | — | `movement/attack.rs` |
| A2 | check marker | — | `canonicalize/check_marker.rs` |
| A2′ | no marker in hand | — | `capture.rs` (to move under `canonicalize`) |
| A3 | double-step marker strip | foot-soldier letter, home rank | `canonicalize/double_step.rs` |
| A4 | en passant window iff capture legal | foot-soldier letter, direction | `canonicalize/en_passant_flag.rs` |
| A5 | castling rights, derived from the GGN | — | `canonicalize/castling_rights.rs` |
| A6 | royal safety | — | `legality/self_check.rs` |
| A7 | drop restrictions: nifu, uchifuzume | the letters concerned by each (may be empty) | `legality/drops.rs`, `terminal/uchifuzume.rs` |
| A8 | rejection, never a sanction | — | `kernel/step.rs` |
| A9 | terminations and their order | stalemate outcome; dead-position tables; repetition, move-limit, move-cap thresholds; irreversible class | `terminal/*` |
| S1 | slots, play order, selection, cap | `K` | session layer |
| S2 | clock accounting, two timeout flavours | `time_control` (a founding term) | `clock.rs`, session layer |
| S3 | post-chain resolution and its order; which invocation | — | session layer |

Changing anything in the "Fixed rule" column is a new `kernel.id`. Changing anything in the "Parameterised by" column, or any GGN table, is a new rule-system document under the same kernel.

## Appendix B — Manifest members the kernel reads

`variants.<v>.style`, `.pieces`, `.ggn`, `.initial`, `.foot_soldier`, `.promotion`, `.drops` (`.letters`, `.nifu`, `.uchifuzume`), `.capture`; `pairings.<pair>.dead_positions`; `session.stalemate`, `.repetition`, `.move_limit`, `.move_cap`, `.irreversible`, `.candidate_cap` (`K`). Any other member is ignored by the kernel but MUST be known to the manifest format ([Rule System — Sanki](https://blobs.sanki.app/eee64155f8b70c63e46acd7fe6179e3ef872137cee7f2352f268bccdd129e5c3.md) §Structure: unknown members are rejected).

## References

- [Rule System — Sanki](https://blobs.sanki.app/eee64155f8b70c63e46acd7fe6179e3ef872137cee7f2352f268bccdd129e5c3.md) — the manifest this kernel executes
- ADR-0033 — why the kernel is an executable contract
- `_reviews/2026-09-03/inventaire-atomes-rule-system.md` — the inventory the atoms come from, with the decisions of 2026-09-03/04
- Playing Principles §10 — the pluggable-arbiter clause this kernel mechanises
- [Game Protocol](https://sashite.dev/game-protocol/) — the protocol invariants the kernel assumes (turn passing, conservation of pieces)
