Expand description
§Sashité Sanki Session
The session kernel of the Sanki game suite, built for
Sashité. The L2 layer over
sashite-sanki-engine: given a
session’s public events, it computes the verdict the rule system yields at a
given instant — and therefore whether a player’s Conclusion (kind 3425),
which is binding by correctness, actually terminates the session. Published
under the Apache-2.0 license.
Until 0.12 this crate was sashite-sanki-arbiter: the same replay, run by a
designated arbiter that signed the verdict. Since the suite’s 2026-09-04
revision (ADR-0033) there is no arbiter — either player concludes, and every
consumer recomputes. The crate is the reference implementation of the session
part of the sashite.sanki.kernel/1 rule system.
The event model is abstract and carries no Nostr dependency: the same kernel can be driven from any context able to supply the events — a rating authority, a client, a bot, an indexer. Identity, transport, signature verification and cross-event structural validation are the caller’s responsibility.
§Event model
The kernel reasons over plain values the caller has already received, signature-verified, and parsed — there is no cryptography and no I/O here:
Ply— a move played at a step (the kind-3423 content);Attestation— a timestamper’s attestation of an event (kind 3410);Conclusion— a player’s verdict on the session (kind 3425): who concludes, when (its canonical timing is the cutoff), and what it claims — aVerdict, a termination status and an outcome, coherent by construction (Verdict::newrefuses a decisive status with a draw, or the converse: a claim the rule system can never yield is refused before any replay);EventId/PublicKey— opaque 32-byte identities (event ids unique, as Nostr guarantees — the kernel tells candidates apart by id).
Timing depends on the session’s mode: in attested mode (a designated
timestamper) each event’s canonical timing is its attestation’s created_at,
the event’s own being an informational self-claim; in self-timed mode
(designated timing relays, no timestamper — what Sashité deploys for sanki)
the event’s own created_at, as accepted by a designated timing relay, is the
canonical timing. Ply and Conclusion carry their created_at for the
self-timed case — on the precondition that the caller offers the kernel only
events whose acceptance by a designated timing relay is established; an
event without that is pending, not a candidate for anything, and this crate
has no way to tell (it sees no relay).
§Evaluation
verdict_at(params, plies, attestations, invoker, cutoff) -> Result<Verdict, NoVerdict>
is the primitive: the verdict the rule system yields at cutoff, with invoker
as the concluding side. expected_verdict(params, plies, attestations, conclusion)
wraps it, resolving the invoker and cutoff from a Conclusion (its signer and its
canonical timing, cutoff_of) — what that Conclusion is expected to claim:
- canonical timing gives each Ply its instant (its attestation’s
created_atin attested mode — smallest attestation as tiebreaker — its owncreated_atwhen self-timed) —stepbeing each signer’s own move ordinal; - the natural state replays the interleaved play order (within each step
value, side
firstbefore sidesecond), selecting each(session, signer, step)slot’s canonical Ply by the two-window forgiving rule against the slot’s boundaryT(the maximum canonical timing among the preceding half-moves — never rewound by a selected premove — t₀ for the first slot): among anterior candidates (timed beforeT— premoves) the latest legal wins; failing that, among informed candidates (timed at/afterT— live moves) the earliest legal wins; each within a per-window capK, legality probed lazily on the capped windows only (≤ 2K full-rule probes per slot, the normative anti-flooding bound;Kis the session’scandidate_cap, the rule-system document’s, at least 1 by type). A Ply timed before t₀ is invalid and never enters a slot (one timed at t₀ does); identical candidates (same content, samedrawflag) count once against the cap, represented by the one the window’s scan reaches first, so the collapse never changes the selection. An illegal candidate — premove or live — is always skipped, never a loss (there is noillegalmove); - the verdict is entirely play-derived (there is no equivocation sanction): a termination reached during replay — a rule-system ending or a played-Ply timeout — otherwise, on a still-ongoing position, the invocation resolved in order: draw acceptance, abandonment timeout, residual resignation (decisive against the concluding player, whatever the turn). Concluding is at the signer’s risk: they choose when, never what.
expected_verdict returns Err(NoVerdict) — a typed reason, not a bare None
— when no verdict is defined: OtherSession or NotAPlayer (kind 3425
§Semantic constraints, items 2–3), BeforeStart (a cutoff before t₀ — item 9;
the kernel is not invoked before the session is playable), Pending (no
canonical timing yet — the one transient reason, NoVerdict::is_transient),
or Inconsistent (a broken internal invariant, never a silently wrong
verdict). A consumer must tell the transient case from the final ones: a
pending Conclusion is re-examined once timed, an invalid one is ignored for
good.
Two functions turn the verdict into the suite’s protocol rules:
check(params, plies, attestations, conclusion) -> Check— item 8 of the Conclusion’s semantic constraints:Conforming(verdict)when the claim equals the verdict the rule system yields,Wrong { claimed, expected }when it differs (a non-conforming Conclusion does not terminate the session, whatever its timing), orNoVerdict(reason).conforms(…) -> boolischeckreduced to “is itConforming”;select_conclusion(params, plies, attestations, conclusions) -> Result<Option<CanonicalConclusion>, NoVerdict>— the session’s canonical Conclusion (kind 3425 §Idempotence and finality): the earliest conforming one by canonical timing, smallest event id as tiebreaker, returned with its cutoff and verdict so no second replay is needed;Ok(None)while none conforms with established timing. Two conforming Conclusions with different cutoffs may carry different verdicts (a premature claim resolving to its signer’s resignation, a later win on time); the earliest rules. An inconsistent replay isErr: the session is unresolved for this consumer, and no other Conclusion is promoted around it.
To decide whether to conclude, a client or bot calls verdict_at with its own
side and the present instant — no synthetic event — and publishes exactly that
verdict; Verdict::scores(params) gives the two result tags to carry.
§Design guarantees
- Panic-free by construction. Crate lints forbid
unsafe, and denyunwrap/expect/panic, slice indexing, and overflowing arithmetic. - Deterministic. The result is a pure function of the events; identity, transport, and signature verification are the caller’s responsibility.
- In lockstep with the reference module. The vendored conformance corpus
(
tests/conformance/, v10) is the shared test suite of the Sanki kernel — this crate replays it natively, and the reference WebAssembly module built from this crate replays it through its ABI (ADR-0034): the two cannot drift on which Ply is canonical, on how a chain ends, nor — the vectors carrying aninvokerand anexpectedVerdict— on the post-chain verdict. - Reported, never guessed. A Conclusion out of reach gets a typed reason;
a broken internal invariant during replay is
NoVerdict::Inconsistent, not a wrong resignation — andselect_conclusionpropagates it rather than promoting the next Conclusion in line. - Refused at the boundary, not in the replay. What the rule system can
never yield — a self-play
Seats, a founding position withoutfirstto move, a cap of 0, a status and an outcome of different kinds, a split other than 100/0, 50/50, 0/100 — is refused by the constructors (Option, or the type), so the kernel only ever evaluates sessions and claims it has a defined answer for.
§Usage
[dependencies]
sashite-sanki-session = "0.14"use sashite_sanki_session::event::{Attestation, Conclusion, EventId, Ply, PublicKey};
use sashite_sanki_session::session::{Seats, SessionParams};
use sashite_sanki_session::verdict::{check, Check, Verdict};
use sashite_sanki_engine::domain::side::Side;
use sashite_sanki_engine::domain::status::{Outcome3, Status};
use sashite_sanki_engine::domain::time::{Duration, Timestamp};
use sashite_sanki_engine::domain::time_control::{Period, TimeControl};
use sashite_sanki_engine::position::Position;
// Identities (opaque 32-byte values; the caller maps them from its own source).
let session = EventId::from_bytes([50; 32]);
let timestamper = PublicKey::from_bytes([99; 32]);
let first = PublicKey::from_bytes([10; 32]);
let second = PublicKey::from_bytes([20; 32]);
// The session's invariant parameters, including the initial position (FEEN,
// `first` to move — as every position a Sanki rule-system document
// prescribes). A designated timestamper puts the session in attested mode;
// `None` would make it self-timed (each event's own `created_at`, as accepted
// by a designated timing relay, authoritative).
let period = Period::new(Duration::from_secs(600), None, None).expect("valid period");
let params = SessionParams::new(
session,
Some(timestamper),
Seats::new(first, second).expect("distinct players"),
TimeControl::new(period, Vec::new()),
Position::parse("7k^/6pp/8/8/8/8/8/R3K^3 / W/w").expect("valid FEEN"),
Timestamp::from_unix(0),
)
.expect("first to move");
// One ply: the first player plays Ra1-a8, a back-rank mate. In attested mode
// the ply's own `created_at` (last argument) is an informational self-claim;
// the timestamper's attestation below is authoritative.
let plies = [Ply::new(
EventId::from_bytes([1; 32]),
first,
session,
1,
false,
r#"["a1","a8",null]"#.to_owned(),
Timestamp::from_unix(90),
)];
// The second player concludes, claiming checkmate for the first player (a
// coherent claim: a decisive status with a decisive outcome); the timestamper
// has attested both the ply (t=100) and the Conclusion (t=1000, the cutoff).
let conclusion = Conclusion::new(
EventId::from_bytes([170; 32]),
second,
session,
Verdict::new(Status::Checkmate, Outcome3::FirstWins).expect("coherent"),
Timestamp::from_unix(900),
);
let attestations = [
Attestation::new(
EventId::from_bytes([101; 32]),
timestamper,
EventId::from_bytes([1; 32]),
Timestamp::from_unix(100),
),
Attestation::new(
EventId::from_bytes([171; 32]),
timestamper,
EventId::from_bytes([170; 32]),
Timestamp::from_unix(1000),
),
];
// Checking the Conclusion on the rules axis: the claim it carries equals the
// verdict the rule system yields at its cutoff, so it is Conforming and
// terminates the session.
match check(¶ms, &plies, &attestations, &conclusion) {
Check::Conforming(verdict) => {
assert_eq!(verdict, conclusion.claim);
assert_eq!(verdict.status(), Status::Checkmate);
assert_eq!(verdict.outcome(), Outcome3::FirstWins);
assert_eq!(verdict.score(Side::First), 100);
}
other => panic!("expected a conforming Conclusion, got {other:?}"),
}§Built on
sashite-sanki-engine (the rules
engine), which it uses under the full rule system — ōgi’s uchifuzume included:
candidate legality is probed via the façade’s validate, and the selected Ply
is applied via the kernel’s per-ply step. The two agree by construction
(pinned by an exhaustive oracle test across every variant pairing); should
they ever disagree, the replay reports ChainEnd::Inconsistent and no verdict
is derived.
§Minimum supported Rust version
Rust 1.81.
§License
Licensed under the Apache License, Version 2.0. See NOTICE.
Modules§
- event
- Typed Nostr event models the session kernel reasons about.
- implicit
- Implicit draw by agreement (Statuses — Sanki §Implicit draw by agreement).
- natural_
state - The natural state of events at the cutoff (kind
3425§Natural state of events at the cutoff; Kernel — Sanki §II.3–II.5). - selection
- Two-window forgiving-premove selection — the per-slot rule that picks a slot’s canonical Ply (Move Encoding — Sanki §Slot candidates and selection).
- session
SessionParams— the session-constant configuration the kernel evaluates.- timing
- Canonical timing — the instant every rule of the kernel reads an event at.
- verdict
- The verdict — what the rule system yields at a cutoff, and what a Conclusion
claims — and the three questions consumers ask of it: what a Conclusion
should claim (
expected_verdict), whether a Conclusion conforms (check,conforms), and which Conclusion rules the session (select_conclusion).