Decision Register¶
Every architectural decision recorded in this manual, in the order it appears in the body — so the register reads in the same sequence as the manual rather than by ID. The Page column points at the full entry, which gives the context, the alternatives considered, and the consequences.
IDs are allocated in blocks by subject area (engine, board, motion, application, services, fleet) with gaps left inside each block, so a decision recorded later sits beside its neighbours instead of at the end. An ID is therefore a stable name, not a position.
IDs are assigned once and never reused. A decision that has been overtaken is not edited: a new ID is allocated and the old entry’s status becomes superseded, so the register reads as a history rather than a snapshot. The Origin of each decision — the dated plan document it was first argued in — is given in the body entry; those plans remain in the repository under docs/*-docs/ exactly where the source code cites them.
ID |
Claim |
Decided |
Status |
|---|---|---|---|
The manual is canonical for current behaviour; the plan documents stay in place as the dated record of each decision as made |
2026-07-22 |
in force |
|
The difficulty ladder is one shared NNUE plus a parameterized weakening layer, not one net per rung |
2026-06-29 |
in force |
|
The node budget is the primary difficulty dial; deliberate imperfect move selection supplies the low end |
2026-06-29 |
in force |
|
The ceiling and the ladder are independent tracks — the top rung is full strength and rises with it |
2026-06-29 |
in force |
|
Any change that could affect playing strength must be validated by SPRT before it ships |
2026-06 |
in force |
|
There is one logical engine instance; `LinkedEngine` wraps its globals and access is serialized by the session worker |
— |
in force |
|
Engine-consuming work outside the game loop runs on the session worker thread, not a background thread |
2026-07-13, reaffirming a 2026-07-12 correction |
in force |
|
`Max Managed Move Time` defaults to 0 (no cap), not 1500 ms |
2026-06-29 |
in force |
|
The target of a physical setup is a `PieceTracker` identity snapshot, never a FEN |
2026-07-12 |
in force |
|
Promotion is physically faked — the pawn stays on the square and keeps its `SLOT_Px` identity |
— |
in force |
|
An arbitrary FEN is assigned to physical pieces by optimal min-cost matching, and promoted pieces are represented by stand-ins |
2026-07-13 |
in force |
|
Never tear the board down between puzzles; plan each setup as a delta, and order a batch by setup distance |
2026-07-13 |
in force |
|
LEDs are driven directly by the Pi over SPI MOSI with DMA, on a second SPI bus |
2026-07-12 |
in force, supersedes D-30 |
|
LEDs driven from GPIO18 via the PWM peripheral |
2026-07-11 |
superseded by D-29 |
|
A lit square always lights all 8 of its ring LEDs, and no more than 16 squares may be lit at once |
2026-07-12 |
in force |
|
When a lifted piece has more legal targets than budget, show its single best destination rather than a truncated set |
2026-07-12 |
in force |
|
Legal-move highlighting triggers on a physical piece lift, not on a completed move |
2026-07-11 |
in force |
|
One continuous snake chain, and the chain order is the firmware pixel index |
2026-07-11 |
in force |
|
Desync recovery is a pure decision function, separated from the physical integration that acts on it |
2026-07-03 |
in force |
|
Persist only the colours that deviate from the factory palette, and stamp a schema version |
2026-07 |
in force |
|
Motion control is FluidNC on an ESP32 board with 4$\times$ TMC2209 drivers in UART mode |
2026-07-01 |
in force |
|
Driver configuration lives in a boot-time YAML file; motion parameters stay live `$`-writes |
2026-07-01 |
in force |
|
The controller class and the `$`/G-code protocol layer are unchanged by the firmware swap |
2026-07-01 |
in force |
|
The GUI links the engine and hardware libraries in-process — no IPC, no daemon |
— |
in force |
|
A personality is data, not code: a named preset of existing UCI options |
— |
in force |
|
Game orchestration is extracted out of `main.cpp` into a frontend-agnostic `GameSession` |
— |
in force |
|
The backend is chosen at startup by capability detection, in a Qt-free factory, with environment overrides |
2026-07-11 |
in force |
|
QML stays logic-free; all logic lives in C++ bridges |
— |
in force |
|
Restart prompts Auto vs Hand board reset on every use |
— |
in force |
|
Game sharing is opt-in, off by default, and never retroactive |
2026-07-12 |
in force |
|
The board-local store is one JSON file per game, and it is the source of truth for solo games |
— |
in force |
|
Store the minimum — `startFen` plus the UCI move list — and derive everything else |
— |
in force |
|
The board analyzes its own games; the cloud renders a precomputed artifact |
— |
in force |
|
Analysis lives in a separate table, not a column on `games` |
— |
in force |
|
Boards do not upload networked games at all |
2026-07-12 |
in force |
|
The server attributes the human seat from the device’s linked account, ignoring any client-claimed user id |
2026-07-12 |
in force |
|
Completed puzzles are filed in a dedicated `PuzzleHistoryStore`, not in `GameStore` |
2026-07-17 |
in force |
|
Idle sleep blanks the panel backlight and wakes on touch — it is not system suspend |
2026-07 |
in force |
|
Accounts ship in v1, and the product is architected toward a public service |
2026-07-03 |
in force |
|
Cloudflare is the backend: Workers, one Durable Object per game, D1 and KV |
2026-07-03 |
in force |
|
A networked game is the existing loop with the opponent swapped for a socket |
2026-07-03 |
in force |
|
The wire carries only UCI move strings and a few control messages |
2026-07-03 |
in force |
|
Authoritative game state lives on the server; the Durable Object re-validates every move |
2026-07-03 |
in force |
|
The 6-digit join code is kept permanently as the zero-friction path |
2026-07-03 |
in force |
|
Track H (the hardware loop) and Track A (accounts) run in parallel, with H first |
2026-07-03 |
in force |
|
Player data lives in a separate D1 from the corporate database |
2026-07-03 |
in force |
|
An unprovisioned board is seated anonymous, not rejected — but a wrong token is a 401 |
2026-07-16 |
in force |
|
The device token rides the upgrade as an `X-Device-Token` header, not a query parameter |
2026-07-16 |
in force |
|
The live feed is a `BoardFeed` Durable Object keyed per device, and its socket state is presence |
2026-07-09 |
in force |
|
No permission is required, but the board must be fully functional with no Lichess account |
2026-07-13 |
in force |
|
Fair play is structural: no hint source is supplied at all during Lichess-rated activity |
2026-07-13 |
in force |
|
HTTP stays in the Qt layer; `kirin_app` gains no HTTP dependency |
2026-07-13 |
in force |
|
Kirin keeps its own rating, authoritative for the device; the Lichess rating is displayed alongside |
2026-07-13 |
in force |
|
Keep `clocks=true`; the tokenizer extracts `[%clk]` rather than skipping it |
2026-07-19 |
in force |
|
Broadcast mirroring composes `ReviewSession` for its bidirectional cursor |
2026-07-19 |
in force |
|
Broadcast needs no auth at all, and cannot share the Lichess HTTP client |
2026-07-19 |
in force |
|
The account link is OAuth PKCE on the player’s phone, and we are a courier, not a custodian |
2026-07-13 |
in force |
|
Telemetry is live-only, and is sent now or dropped — never queued |
2026-07-18 |
in force |
|
The golden image locks three properties that cannot be changed on already-flashed boards |
2026-07-18 |
in force |
|
v1 OTA ships a prebuilt, self-contained bundle and swaps the application, not the OS |
2026-07-17 |
in force |
|
Bundles are signed with minisign, the public key is baked into the image, and nothing unpacks before verification |
2026-07-17 |
in force |
|
The board never installs on its own — a background check stages, and the owner taps Install |
2026-07-17 |
in force |
|
A boot-count guard backs up the heartbeat window for auto-rollback |
2026-07-17 |
in force |
|
The OTA admin console is built in the corporate admin backend, proxying to the player Worker |
2026-07-17 |
in force |
|
The wizard stores no step cursor and derives no resume point — it always opens at Welcome |
2026-07-17 |
in force |
|
The dev bench deliberately does not use the golden image, and the two paths must not merge |
2026-07-18 |
in force |
|
Tokens are hashed at rest, the email-sending endpoints are rate-limited, and the board service runs unprivileged |
2026-07-22 |
in force |
|
The launcher runs a fixed canonical deploy path |
2026-07-16 |
in force |
|
Unit serials are four-digit zero-padded decimal, with `0001`– `0099` reserved for internal boards |
2026-07-19 |
in force |