System Architecture & Foundation
Lichess Mobile GPL-3.0 base, 4-tier layer isolation, component boundaries, and SQLite local persistence.
Technical Architecture
Foundation reset: this application is built on a fork of
lichess-org/mobile (GPL-3.0).
The previous standalone Flutter implementation is archived at git tag
legacy/pre-reset. It is reference material only — the product and domain
knowledge lives in the Markdown specifications, not in the old code.
1. What this application is
A local-first chess repertoire + spaced-repetition trainer built on the Lichess Mobile application foundation. It is not "Lichess with modifications": Lichess Mobile provides the technical/UI foundation (board, theme, navigation, state management, persistence patterns); our product provides the domain (repertoire, review, SRS).
Lichess Mobile foundation (GPL-3.0 fork)
|
+-- application/product modules (Riverpod providers)
|
+-- OUR domain boundary (pure Dart: repertoire, review, SRS)
| |
| +-- Listudy-derived training semantics
| +-- chessrs-derived SRS/review semantics
|
+-- local persistence / import infrastructure (sqflite, dartchess PGN)
2. Layer boundaries
┌────────────────────────────────────────────────────────────┐
│ Presentation — Lichess Mobile shell (Flutter + Riverpod) │
│ Review scene (primary) · Repertoire selector · Import · │
│ Settings (Lichess settings framework) │
└──────────────────────────┬─────────────────────────────────┘
│ talks only to application providers
┌──────────────────────────▼─────────────────────────────────┐
│ Application — Riverpod providers/services orchestrating │
│ use-cases (import study, start review, submit move) │
└──────────────────────────┬─────────────────────────────────┘
│ repository interfaces only
┌──────────────────────────▼─────────────────────────────────┐
│ DOMAIN (ours) — pure Dart, no Flutter/chessground imports │
│ Study · Chapter · repertoire tree · RepertoireDecision │
│ ReviewSession engine · ReviewState · Scheduler contract │
└──────────────────────────┬─────────────────────────────────┘
│ adapters only
┌──────────────────────────▼─────────────────────────────────┐
│ Infrastructure — Lichess-derived: │
│ dartchess (rules, FEN, SAN, PGN) · chessground widgets │
│ sqflite local DB · settings/preferences · file import │
│ (no network on any critical path) │
└────────────────────────────────────────────────────────────┘
Boundary invariants
- Pure domain: our domain code (
lib/src/domain/, 23 files) contains pure Dart logic and entities. Zero imports ofpackage:flutter/...widgets, chessground, or sqflite — enforced by inspection and by the banned-API gate (scripts/gates/banned_apis_check.py). Testable without a widget tree. The module's public surface is the barreldomain/domain.dart. - One chess representation:
dartchessis the chess rules engine. Do not introduce a second chess library or reimplement move generation. Domain code may consume dartchess core types (Position,Move,San) through thin adapters; UI consumes chessground. Never create competing representations of moves/positions/games. - Lichess UI is not our domain model: Lichess
model/classes for online features are presentation/application concerns of the foundation, not domain entities of our product. Our entities live in our domain module. - Local-first critical path: user move → legality (dartchess) → repertoire lookup → SRS update → board update runs entirely in memory, locally. Network access is never on this path.
- Isolated integrations: Listudy-derived and chessrs-derived behavior
live in isolated domain modules behind our contracts (see
docs/INTEGRATION_MAP.md). No cross-cutting merges. - Imported repertoires vs. online studies: Repertoires imported into
ChessSRS (from PGN or Lichess) are pure, local-first snapshots stored in
SQLite. They have no live socket connection and never auto-sync with
remote changes. Remote Lichess study browsing (
StudyScreen/StudyController) operates strictly in-memory over WebSockets for live viewing; it never writes to the local database.
3. Component responsibilities
Domain (ours)
Study/Chapter: repertoire content containers (source PGN file → study; PGN game → chapter, honoring FEN headers).- Repertoire tree: branching move tree of positions. Reuses the
Lichess
Node/Branchtree pattern where practical, but decision semantics are ours. RepertoireDecision: THE scheduled unit — a position (from the player's perspective) plus the expected repertoire move(s). Every branch the player must recall is a decision; opponent moves are not scheduled independently.ReviewState/ReviewEvent: SRS tracking — first/last review, next due, repetition count, lapses, stability — plus immutable review log.Scheduler(contract): interval computation, implemented three times and selected at runtime from settings (SchedulerTypeinmodel/study/study_preferences.dart, resolved byschedulerProviderinreview/review_service.dart):SimpleScheduler(exponential ladder, the default),EaseScalingScheduler(chessrs-style ease and scaling), andChessFsrsScheduler(binary FSRS,targetRetention, default 0.88). Adding a fourth means implementing the contract and extending theswitch— nothing else in the domain knows which one is in use.ReviewSessionengine: due-decision selection, move validation against repertoire (not engine truth), auto-traversal of non-due material, opponent-reply auto-play, correct/incorrect feedback semantics.Clockabstraction: deterministic time in tests.
Infrastructure (Lichess-derived)
- dartchess: legality, SAN/UCI, FEN, and PGN parsing (
PgnParser) — used by the import pipeline; PGN is an input format, never the domain model. - chessground: board rendering, drag & drop, animation — presentation only.
- sqflite (
db/): local persistence for studies, trees, decisions, review states, events. Incremental writes; full study tree never rewritten per move. - settings/preferences: board theme, piece set, sound — Lichess framework.
- import: PGN file import (multi-game, RAV variations, comments, NAGs, starting FENs) with graceful, structured degradation — never silent corruption.
Presentation (Lichess-derived)
- App shell, tab navigation (reduced to: Review primary, settings in More),
theming (
styles/), reusablewidgets/. - Review scene: board-dominant screen; the board is the product.
- Review modes:
ReviewMode.srs: standard spaced-repetition training updating review states and logging events.ReviewMode.practice: non-destructive rehearsal (cram mode) allowing active board testing without altering SRS intervals.
- Review scopes (
domain/review/review_scope.dart, six constructors):ReviewScope.all(): all active studies in the review pool.ReviewScope.white()/ReviewScope.black(): one side of every study — the two sides of a repertoire carry separate review memory.ReviewScope.study(id): specific study.ReviewScope.chapter({studyId, chapterId}): one chapter.ReviewScope.opening(name): virtual cross-study opening hub.
- Review order (
domain/review/review_order.dart):dueDate(most overdue first),byLine(study → chapter → tree order, the default), orrandom. The due set is identical in all three; only presentation order changes.
4. Review state machine (preserved product semantics)
┌───────────────┐
│ Select Scope │ (all active / one study / one opening)
└───────┬───────┘
▼
┌───────────────┐
│ Due Decision? ├─── No ──► calm idle state (option: Explore / Practice)
└───────┬───────┘
│ Yes
▼
┌───────────────┐
│ Show Position │ (board oriented; move comments strictly hidden)
└───────┬───────┘
▼
User plays move
│
┌─────────────┴─────────────┐
▼ ▼
[Repertoire move] [Other move]
│ │
• record success (if SRS) • record lapse (if SRS)
• reveal move comment • reveal expected move & comment
• auto-traverse non-due • keep board interactive for reguess
• opponent auto-reply • re-queue failed item
│ │
└─────────────┬─────────────┘
▼
Next Due Decision (until nothing due)
Key semantics (from PRODUCT/QUALITY + reference projects):
- Any valid repertoire branch is accepted; the played branch is followed.
- Auto-traversal is never permanent exclusion — learned moves return when due.
- No session-complete screen; review is an ongoing utility.
- Move comments are withheld during recall to prevent spoilers, then displayed post-move.
- Wrong-move feedback leaves the board interactive so the user can immediately reguess.
- Practice mode traverses lines identically to SRS mode but performs zero database writes.
5. Identity & determinism
- Entities use stable unique IDs (sync-ready).
- Position identity = normalized 4-field FEN (placement, side to move, castling, en-passant) — the "clean FEN" concept.
- All time-dependent logic takes an abstract
Clockfor deterministic tests.
6. Licensing constraints (binding)
- The whole application remains GPL-3.0 as a fork of Lichess Mobile.
Preserve
LICENSE,COPYING.md, and copyright notices verbatim when trimming code. - chessrs (GPL-3.0): adaptation permitted with attribution; default is reimplementation.
- listudy (AGPL-3.0): no code copying; behavioral reference only.
- See
docs/INTEGRATION_MAP.mdfor the extraction rules.
7. Related documents
PRODUCT.md— product definition (authoritative)MVP.md— current scopeQUALITY.md— invariantsTEST_STRATEGY.md— how invariants are provenCUT_PROPOSALS.md— Lichess foundation trim map & statusdocs/INTEGRATION_MAP.md— Listudy/chessrs extraction plandocs/decisions.md— durable decision logChess_Repertoire_SRS_Product_Blueprint.md— deep product philosophy (consult on demand)
8. Module map
Sizes are hand-written Dart (generated *.freezed.dart / *.g.dart are
gitignored and absent from a fresh clone). "Depends on" lists inward edges
only, i.e. what the module imports.
| Module | Files / lines | Role | Entry points | Depends on |
|---|---|---|---|---|
lib/src/domain/ |
23 / 3,153 | Pure-Dart domain: repertoire tree, decisions, SRS state, review engine, scheduler contract, clock | barrel domain.dart; ReviewSession, ReviewEngine, Scheduler, PositionKnowledgeState |
dartchess, meta, logging, uuid, crypto — no Flutter, chessground or sqflite |
lib/src/review/ |
2 / 2,182 | Application layer: orchestrates sessions, persistence and SRS updates; Riverpod providers | reviewServiceProvider, ReviewService.startSession / submitMove / retryMove / getDueSummary; ReviewController |
domain, persistence, study_preferences |
lib/src/persistence/ |
7 / 1,746 | SQLite schema, StudyRepository contract and its sqflite implementation, JSON adapters, two one-shot data migrations |
study_repository.dart (contract), sqlite_study_repository.dart, srs_schema.dart |
domain, sqflite, dartchess |
lib/src/db/ |
3 / 410 | Opens the one database, bundles the openings DB, secure storage | database.dart (openDatabase, schema v15) |
sqflite, sqflite_common_ffi (desktop/tests) |
lib/src/import/ |
4 / 1,047 | PGN import/export, Lichess study import, opening-name derivation | importPgn / importPgnAsync → ImportResult; pgn_exporter.dart |
dartchess, domain |
lib/src/network/ |
5 / 2,736 | Lichess HTTP, WebSocket, connectivity, server status — Lichess-derived | http.dart (HttpClientFactory), socket.dart, server_status.dart |
http, cronet_http, cupertino_http, web_socket_channel |
lib/src/model/ |
111 / 22,504 | Lichess-derived feature models (games, studies, auth, engine, users) | model/**/*_providers.dart |
everything above |
lib/src/view/ |
76 / 23,784 | Screens. review/ is ours; the rest is the inherited Lichess surface |
screen widgets | application layer, widgets, model |
lib/src/widgets/, styles/, design/ |
65 / 12,827 | Reusable widgets, tokens, and the design package | — | material_ui, chessground |
lib/ total |
329 / 73,557 | lib/main.dart → lib/src/app.dart |
||
test/ |
188 / 51,543 | 1,598 test declarations | see §13 |
Dependency direction is inward-only with three known exceptions: three
screens import the persistence layer directly instead of going through an
application provider —
view/settings/srs_settings_screen.dart (db/database.dart),
view/review/study_chapters_screen.dart (persistence/study_repository.dart)
and view/analysis/analysis_hub_screen.dart (persistence/persistence.dart).
This is tracked as item 14 in docs/tech-debt-inventory.md, not an intended
shortcut. Five screens also import domain/ directly, which is allowed for
value types but should not grow into orchestration.
9. Data flow
Flow A — import a PGN, then review it
- Pick a file (
view/more/import_pgn_screen.dart,file_picker). - Parse:
ReviewControllercallsimportPgnAsync/importPgn(import/pgn_importer.dart:198,358), which uses dartchess for legality, SAN and FEN. Multi-game PGN, RAV variations, comments, NAGs and starting FENs are handled; malformed input yields a structuredImportErrorand is reported, never silently dropped. - Persist:
StudyRepository.saveImportResult(persistence/study_repository.dart:21) writessrs_study,srs_chapterand onesrs_decisionrow per recallable branch — the whole tree goes intosrs_chapter.treeJson, and per-move writes never rewrite it. - Start a session:
ReviewController→ReviewService.startSession(review/review_service.dart:157) reads active studies, filters decisions by the chosenReviewScope, and constructs aReviewSessionwith the selectedSchedulerandClock.
Flow B — answering one move (the critical path)
ReviewControllerforwards the board's move toReviewService.submitMove(review_service.dart:270).- The service takes a
checkpoint()of the session and callsReviewSession.submitMove(domain/review/review_session.dart:302) — pure Dart, no I/O: legality against dartchess, lookup in the repertoire tree, branch selection, and the SRS transition computed by the injectedScheduler. - It returns a
ReviewStepResult(correct / incorrect, the expected move, the comment to reveal, plussideEffectStatesfor graph effects such as contagion and sibling updates). - In SRS mode only, the service maps each updated state to a
PositionKnowledgeStateand writes it with the review event — incremental, per answer. Inpracticemode this step is skipped entirely and nothing is written. - The controller updates the board and prompts the next due decision.
No step on this path performs network I/O. This is the invariant that makes review work offline, and it is why the scheduler and clock are injectable: tests drive the whole path deterministically with a fixed clock.
10. Data model
One SQLite database (Lichess schema v15, opened in db/database.dart). SRS
tables are created by createSrsTables (persistence/srs_schema.dart):
| Table | Key columns | Notes |
|---|---|---|
srs_study |
id, title, createdAt, updatedAt, isActive, pgnHash |
pgnHash (indexed) deduplicates re-imports |
srs_chapter |
id, studyId FK→study (cascade), sourceOrder, title, startingFen, treeJson, opening, orientation |
one row per chapter; the move tree lives in treeJson so a single move never rewrites the tree |
srs_decision |
id, studyId, chapterId, nodeId, expectedMoves, canonicalStateId |
the scheduled unit: one row per branch the player must recall. expectedMoves holds multiple accepted moves; canonicalStateId links a node to a position's knowledge state |
position_knowledge_state |
nextDueAt (indexed), … |
knowledge keyed by canonical position, so the same position reached by different move orders shares one schedule |
srs_review_state |
nextDueAt (indexed), … |
per-decision SRS counters (first/last review, interval, lapses) |
srs_review_event |
decisionId (indexed), whenTimestamp (indexed) |
append-only review log |
Relationships: study 1→N chapters 1→N decisions; decisions reference a canonical position state; review events reference the decision they came from. Foreign keys cascade on study/chapter delete.
Position identity is the normalised four-field FEN (placement, side to
move, castling rights, en-passant) — the "clean FEN" of §5, which is what makes
cross-move-order knowledge sharing and transposition handling possible. A
decision's canonical knowledge ID is sha1("<fenKey>|<every accepted move, sorted>"): the answer set is part of the identity, so two different questions
asked at the same position keep separate schedules instead of being merged.
(The earlier format used only the first accepted move and could not tell them
apart — see the migration below.)
Two one-shot data migrations run after the schema upgrade and read/write rows, so they run outside the schema batch:
canonical_rekey_migration.dart— rewrites persistedcanonicalStateIdvalues andposition_knowledge_staterows from the old first-move key format to the complete-answer-set format, recomputing from each chapter's stored tree and reading the answer set back fromsrs_decision.expectedMovesso the rewrite cannot drift from what was actually persisted. Without it every position would look never-reviewed and a user's accumulated intervals, due dates and history would silently reset. Idempotent.opening_name_repair_migration.dart— erases chapter "openings" that were really just titles mentioning an opening, keeping genuine ones.
11. External dependencies and integration points
| Integration | Where | Failure handling |
|---|---|---|
Lichess HTTP API — lichess.org, lichess1.org (constants.dart, overridable via --dart-define) |
network/http.dart; HttpClientFactory prefers Cronet on Android, CupertinoClient on iOS/macOS, and falls back to dart:io HttpClient in a catch |
offline-first: nothing on the review path calls it; failures surface as errors in the Lichess-derived screens, and the factory degrades to the plain HTTP client |
Lichess WebSocket — socket.lichess.org |
network/socket.dart |
study browsing is strictly in-memory and read-only; it never writes to SQLite (§2 invariant 6) |
| Lichess OAuth (PKCE) | flutter_appauth, model/auth/auth_repository.dart |
app remains fully usable signed-out; only the Lichess-specific screens need it |
| Lichess explorer / tablebase | explorer.lichess.org, tablebase.lichess.org |
advisory lookups; absence degrades to "no data", not an error state |
| Server availability | network/server_status.dart derives ServerStatus from HTTP 503 = maintenance, 502 = down |
status is informational; the local review loop is unaffected. connectivity_plus gates network-only affordances |
| Chess engine | model/engine/ — native Stockfish and Fairy-Stockfish via multistockfish, Leela via lc0 (both git-pinned, see docs/dependency-audit.md §4) |
advisory only, never the Review judge (decision D005); engine_failure.dart, engine_budget.dart and a slot model keep a missing or crashing engine from breaking review |
| Bundled assets | assets/chess_openings.db (1.1 MB, copied out of the bundle on first use), assets/maia/, assets/images/stockfish/, positions.json, endgames.json |
no network needed; the openings DB is versioned by filename and replaced on mismatch |
12. Design decisions and tradeoffs
The durable log is docs/decisions.md (D001–D017) and it is the authority;
this is a map of the ones that shape the code most:
- D002 local-first — the review path never touches the network (§9). Tradeoff: no cross-device sync yet (D009 scopes it as future work).
- D003 / D013 — PGN is an input format and dartchess is the only chess representation. Tradeoff: domain code may not use chessground types, so adapters exist at the boundary.
- D005 — the engine is advisory: a position is judged against the repertoire, not engine truth.
- D007 / D008 — "learned" is never permanent exclusion, and there is no session-complete screen; review is an ongoing utility.
- D015 — FSRS is implemented as one of three schedulers rather than replacing the ladder outright, which keeps the algorithm a setting (§3, verified in code).
- D016 — visual identity is deliberately independent of Lichess Mobile,
which is why
design/exists alongside the inheritedstyles/.
Inferred (from code, not written down): the schema stores the move tree as
treeJson on the chapter row and writes decisions separately, which trades
queryability of the tree for the guarantee that a move never rewrites the whole
study — consistent with the incremental-persistence invariant in QUALITY.md.
Marked as inferred because no document states the choice explicitly.
13. Known limitations and technical debt
Full detail lives elsewhere; this is the signpost.
docs/tech-debt-inventory.md— the scanned inventory, with impact/effort/risk and what was deliberately left out.docs/dependency-audit.md— every dependency, advisory check, and the staged upgrade plan. Both engine git pins are behind the branches they were waiting for.- Boundary erosion: the three presentation→persistence imports listed in §8 (item 14), and the four remaining duplications item 21 did not collapse.
- Verification is CI's job by design (
AGENTS.md§3.1):flutter analyzeandflutter testare the authority, and the gates (scripts/gates.sh t1) cover what tests do not. Anything documented here as "verified" was checked against the tree in this repository, not against a running build.
Appendix: Subsystem Cuts & Upstream Discipline
Lichess Mobile Cut Proposals
Status: ACTIVE — Phase 1 F5+ staged cuts in progress.
This document is the authoritative record of what is removed, what is kept, and why. Update it before executing any cut. Future agents: read this entire file before touching anything.
The guiding rule:
Trim the Lichess product. Preserve the Lichess technical foundation. When uncertain: keep it. Delete less, verify more.
Execution status key
[ ]not started[~]in progress[x]DONE — verified (analyze 0 + all tests pass + app launches)[K]KEEP — owner decision: do not remove[G]GREY — owner undecided; leave untouched until explicit approval[T]TRIM-LATER — defer to Phase 6 refinement
Owner decisions (recorded 2026-09-14)
These are the authoritative decisions from the owner. Do not override them.
| # | Decision | Rationale |
|---|---|---|
| C1 Firebase | [x] DONE |
Removed 2026-09-30 (Option B) — firebase_core, crashlytics, messaging dropped; app is 100% offline-first with zero cloud dependencies |
| C2 Notifications | [x] DONE |
Removed 2026-09-30 — FCM path and Lichess push models removed; app has no server-push dependencies |
| C3 Auth/login | [K] KEEP |
Login is optional but enables importing Lichess/chess.com studies; app is 100% functional offline without it |
| C4 Online play | [x] DONE |
Removed 2026-09-15 — lobby, seeks, challenges, view/play, model/lobby, model/challenge |
| C5 Server games | [x] DONE |
Removed 2026-09-15 — server game lifecycle, correspondence, GameScreen, ongoing games |
| C6 Puzzles tab | [x] DONE |
Removed 2026-09-15 — tab, model/view dirs, deep links, quick action; shared FeedbackTile/SideToPlayPiece moved to widgets/feedback.dart |
| C7 Watch tab | [x] DONE |
Removed (commit dd996ee74) — docs marked late in C6 commit |
| C8 Social | [x] DONE |
Removed 2026-09-15 — removed Community/Players/Friends, Inbox from More tab & account menu, Home friends carousel, and message service poller |
| C9 Learn tab + coord training | [x] DONE |
Removed 2026-09-14 |
| C10 Blog/recap/announce | [x] DONE |
Removed 2026-09-15 — models, services, home carousel, test |
| C11 Over-the-board game | [x] DONE |
Removed 2026-09-14 |
| C11 Chess clock tool | [x] DONE |
Removed by owner request 2026-09-14 |
| C12 Offline computer play | [G] GREY |
Owner undecided; leave untouched until explicit approval |
| C13 Engine (Stockfish) | [G] GREY |
May be useful as optional import advisor — undecided |
| C14 Opening explorer | [K] KEEP |
Owner decision |
| C15 Analysis screen | [G] GREY |
Undecided — shares widgets with study; leave untouched |
| C16 Board editor | [K] KEEP |
Owner decision |
| C17 WebSocket | [K] KEEP |
Owner decision (2026-09-16) — kept for study sync, cloud Stockfish evaluation, and future sync |
| C18 HTTP repos | [K] KEEP |
Owner decision (2026-09-16) — base HTTP, auth, study import, opening explorer, and tablebase kept; dead online repos trimmed |
| UI-A Donate / patron links | [x] DONE |
Removed (commit 1af79eab3) |
| UI-B "About Lichess" in More tab | [x] DONE |
Removed (commit 1af79eab3) |
| UI-C "Lichess is a free…" message (LichessMessage widget) | [x] DONE |
Removed (commit 1af79eab3) |
| UI-D "Welcome to the Lichess app" card | [x] DONE |
Removed (commit 1af79eab3) |
| UI-E "Not all features available" text | [x] DONE |
Removed (commit 1af79eab3) |
| UI-F App background theming | [x] DONE |
Removed 2026-09-27 — see the note below |
C8 Social note: Friends list, inbox, player search, and relations navigation entries were removed from the More tab and Account menu; the Home screen friends carousel was removed; the background message service was silenced; and the relation repository, following_user model, and follow/block actions were pruned in Step 15. Base user/account models remain for C3 (auth).
UI-F App background note: The setting was removed because it could not work, not because it was judged unimportant. Two independent facts, each verified in the code:
- Its only consumer was unreachable.
makeAppThemeinlib/src/theme.dartwas defined and never called —app.dartbuilds the Diagram theme throughsrsThemeData(srsColors). The background fed_makeBackgroundImageTheme, a translucent-surface theme whose whole purpose was letting a background show through, and that in turn was reachable only frommakeAppTheme. The preference was stored and read by nothing that ran. Separately,scaffoldBackgroundColor: c.groundis opaque intheme_bridge.dart, so even the dead path could not have shown through. - The Diagram palette is specified against a fixed
ground.design/docs/02-tokens.mdtabulates "Contrast on ground (light / dark)" andtokens.dartstates "Every value passes 4.5:1 against ground and surface in its own theme".groundis#F1F3F4/#0D0F13, defined as "App background". A user-chosen image behindinktext invalidates the one guarantee the token system rests on, and nothing tests it against an arbitrary background.
Making the feature work was therefore not a small fix: it meant either dropping that contrast guarantee or building image-luminance sampling to pick light or dark per picture, which is feature creep the feature policy forbids by default. Leaving a picker that visibly selects a background and changes nothing was the worst of the three options, so it went.
Reversal, if the owner disagrees: do not set scaffoldBackgroundColor in
theme_bridge.dart and restore FullScreenBackground in buildScreenRoute. The
deleted code is in this branch's parent commits.
Removed with it: lib/src/widgets/background.dart,
lib/src/view/settings/background_theme_choice_screen.dart, the BackgroundColor
enum, the BackgroundImage sealed class and its JSON converter, setBackground, the
isForcedDarkMode forced-dark escape hatch in app.dart, and the
usingCustomBackground branch in game_summary_table.dart. lib/src/theme.dart
survives at 50 lines holding only CustomTheme, which two explorer views read through
context.lichessTheme.
Safe execution order
Each step is ONE logical change, ONE commit. Gate: fvm flutter analyze (0
issues) + fvm flutter test (all pass) + fvm flutter run -d linux (app
launches, manual spot-check) before proceeding to the next step.
NEVER batch multiple steps into one commit.
Step 1 [x] C9 — Learn tab + coordinate training DONE
Step 2 [x] UI — Lichess branding strings from home + more screens DONE
Step 3 [x] C7 — Watch tab (TV / tournaments / broadcasts) DONE (dd996ee74)
Step 4 [x] C6 — Puzzles tab DONE
Step 5 [x] C11 — Over-the-board game & Clock tool DONE
Step 6 [x] C10 — Blog / recap / announce (home carousels + model) DONE
Step 7 [x] C4 — Online play: lobby / seek / challenges DONE
Step 8 [x] C5 — Server game lifecycle + correspondence DONE
Step 9 [x] C8 — Social navigation & Home carousel & Message poller DONE
Step 10 [K] C17 — WebSocket (KEPT by owner decision for study sync / cloud eval)
Step 11 [K] C18 — HTTP network & core repositories (KEPT for auth / study / explorer)
Step 12 [x] Tab reduction: clean 2-tab shell (Review + More) — Home tab removed DONE
Step 13 [x] C8 — Dead social views & message models (b8664182d) DONE
Step 14 [x] User— Dead leaderboard & online bot screens/providers (b0ad26284) DONE
Step 15 [x] C8 — Dead relation repository, following_user & follow/block actions DONE
Step 16 [x] C4 — Dead playban dialog, notification model & account service monitoring DONE
Step 17 [x] UI/Widgets — Dead orphaned widgets, views & assets (UserContextMenu, ServerOutageDisplay, ExpandedSection, SideIndicator, TextBadge, BrightnessNotifier) DONE
Step 18 [x] C5/C6 — Dead Android system gestures exclusion utility & method channel handler DONE
Steps beyond 12 (C12 offline computer, C13 engine) are blocked on owner GREY decisions and are not started until explicit approval.
C1/C2 decided 2026-09-29 — keep, and what "keep" actually means here
Owner decision: keep Firebase, because notifications need it. Recorded here because the decision and its consequences are easy to conflate, and only one of the two is a decision.
What is actually running. The FCM path is live and started at launch
(lib/src/app.dart:123). It requests permission, listens for token refresh, and
registers on connectivity regain. It is real code doing real work.
What it delivers is not SRS reminders. CUT_PROPOSALS.md C2 was written as
"may be needed for SRS review reminders (like Anki)". That feature does not exist.
There is no zonedSchedule and no topic subscription anywhere in lib/; the only
display calls initialize the plugin (lib/src/init.dart:71). What the plumbing
delivers today is Lichess's own push, registered against Lichess:
// lib/src/model/notifications/notification_service.dart:475
client.post(Uri(path: '/mobile/register/firebase/$token'))
That request also returns early when no user is signed in (:471), so the whole
notification path is gated behind sign-in — which is why the unreachable account
menu (fixed in PR #84) was suppressing notifications as well as study import.
The consequence that is not a decision. Keeping Firebase keeps
lib/firebase_options.dart pointed at Lichess's project — lichessv2,
org.lichess.mobileV2. So:
- FCM device tokens are registered on Lichess's backend, not one we control.
- Crashlytics non-fatals, including sign-in failures
(
lib/src/model/auth/sign_in_failure_reporter.dart), go to Lichess's console.
lib/firebase_options.dart is generated, so this is a console-and-regeneration
task, not a code edit. It stays open because it is a consequence of the decision,
not a separate decision.
What would change the answer. Building the Anki-style daily review reminder that C2 was written for would need a backend we own, and at that point owning the Firebase project stops being optional. That is the decision to revisit first if notifications are ever built for real.
Detailed cut plans
Step 2 — Lichess branding strings (UI-A through UI-E)
Why: These are Lichess product strings that appear in the ChessSRS UI. They confuse users and misrepresent the product.
What to remove (surgical edits, no file deletions):
lib/src/view/home/home_tab_screen.dart- Remove
_WelcomeMessageCardwidget render (the "Welcome to the Lichess app / not all features available" card shown on first launch). The_WelcomeMessageCardclass and its state can be deleted. - Remove the donate (
https://lichess.org/patron)FilledButton.tonalblock in the welcome screen branch. - Remove the "About Lichess" (
https://lichess.org/about)FilledButton.tonalblock in the welcome screen branch. - Remove
_LichessMessageBanner(the unread Lichess server message banner) — this is a Lichess inbox feature that will be removed with C8 anyway; safe to remove from the home screen display now. - Remove
LichessMessagewidget usage from the welcome branch. - Keep
unreadMessagesProviderwatch for now (C8 will clean it up).
- Remove
lib/src/view/more/more_tab_screen.dart- Remove the Android-only
ListSectioncontaining patron/donate tile and "About" tile (lines 184–205). - Remove
LichessMessagewidget at the bottom of the More tab list. - Keep
AboutScreenimport removal if the tile is the only caller — check before deleting.
- Remove the Android-only
Shared dependencies that must remain: LichessMessage widget class in
widgets/misc.dart — other callers may exist; remove references not the
widget itself. AboutScreen — check if any other caller exists before
deciding whether to delete the screen.
Risk: LOW — pure UI text/widget removal, no state or routing changes.
Tests to update: test/app_test.dart — if it asserts the welcome message
text, update accordingly.
Step 3 — C7: Watch tab
Why: TV, tournaments, broadcasts, and streamers are Lichess server content with no relevance to repertoire training.
Files — FEATURE-SPECIFIC (delete):
lib/src/view/watch/(entire directory)lib/src/model/tv/(entire directory)lib/src/model/tournament/(entire directory)lib/src/model/broadcast/(entire directory)test/view/watch/(if exists)test/model/tv/,test/model/tournament/,test/model/broadcast/(if exist)
Files — SHARED (edit only):
lib/src/tab_scaffold.dart— remove Watch case, reindex More tablib/src/tab_navigation.dart— removeBottomTab.watch+ its globalslib/src/view/home/home_tab_screen.dart— removeFeaturedTournamentsWidget,featuredTournamentsProviderusage,tournament_list_screenimport,tournament_providersimporttest/app_test.dart— remove 'Watch' bottom-nav assertion
Shared dependencies to check before deleting:
model/broadcast/broadcast_preferences.dart— referenced inapp.dart_screenSizeBasedInitialization; remove that call too.model/broadcast/broadcast_service.dart— started inapp.dart; remove.- Tournament model types used in home screen — remove those usages first.
Risk: MEDIUM — home screen references tournament widgets; must clean those before deleting model files.
Step 4 — C6: Puzzles tab
Why: Tactics training is an explicit non-goal of ChessSRS.
Files — FEATURE-SPECIFIC (delete):
lib/src/view/puzzle/(entire directory)lib/src/model/puzzle/(entire directory)test/view/puzzle/(if exists)test/model/puzzle/(if exists)
Files — SHARED (edit only):
lib/src/tab_scaffold.dart— remove Puzzles case, reindex remaining tabslib/src/tab_navigation.dart— removeBottomTab.puzzles+ its globalslib/src/view/home/home_tab_screen.dart— remove any puzzle references (home_widgets puzzle entry if present)lib/src/view/more/more_tab_screen.dart— remove puzzle entry if presentlib/src/app_links_service.dart— remove puzzle deep-link handlingtest/app_test.dart— remove 'Puzzles' bottom-nav assertionpubspec.yaml— check if any puzzle-only packages can be removed
Risk: MEDIUM — puzzle tab is a whole tab; check home screen home_widgets enum for puzzle references.
Step 5 — C11: Over-the-board game & Clock tool — COMPLETED
Why: Pass-and-play local two-player chess and standalone physical clock simulator are not relevant to local-first SRS.
Status: Completed. Removed view/over_the_board, model/over_the_board, over_the_board_game.dart, OverTheBoardGameResultDialog, OTB navigation entries in play_menu.dart, board_editor_screen.dart, and analysis_actions.dart. Removed standalone clock tool view/clock, model/clock/clock_tool_*, clock_tool_controller_test.dart, and More tab entry. All quality gates (./verify) passed with 0 warnings.
Files — FEATURE-SPECIFIC (deleted):
lib/src/view/over_the_board/(entire directory)lib/src/model/over_the_board/(entire directory)lib/src/model/game/over_the_board_game.dart+ generated fileslib/src/view/clock/(entire directory)lib/src/model/clock/clock_tool_*test/view/over_the_board/test/model/clock/clock_tool_controller_test.dart
Files — SHARED (edited):
lib/src/view/play/play_menu.dart— removed OTB entry + importlib/src/view/analysis/analysis_actions.dart— removedOverTheBoardScreen.buildRoute()call + importlib/src/view/board_editor/board_editor_screen.dart— removed OTB "play from position" action + importlib/src/view/game/game_result_dialog.dart— removed unusedOverTheBoardGameResultDialoglib/src/view/more/more_tab_screen.dart— removed Clock tool entry + importlib/src/model/settings/preferences_storage.dart— removedPrefCategory.clockTooltest/model/game/game_test.dart,test/view/board_editor/board_editor_screen_test.dart,test/view/analysis/analysis_screen_test.dart— updated tests
Game clock stays (lib/src/model/clock/chess_clock.dart & test/model/clock/chess_clock_test.dart — kept; used by in-game countdown clocks).
Step 6 — C10: Blog / recap / announce
Why: Server-pushed content feeds with no relevance to local-first SRS.
Files — FEATURE-SPECIFIC (delete):
lib/src/model/blog/(entire directory — 3 files)lib/src/model/recap/recap_service.dartlib/src/model/announce/announce_service.dartlib/src/view/home/blog_carousel.dart
Files — SHARED (edit only):
lib/src/app.dart— removerecapServiceProvider.start()andannounceServiceProvider.start()frominitState; remove their importslib/src/view/home/home_tab_screen.dart— removeblogCarouselProviderwatch,_BlogCarouselWidgetusage,HomeEditableWidget.blogCarouselusage,blog_carousel.dartimport,blog.dart+blog_repository.dartimportslib/src/model/account/home_widgets.dart— removeblogCarouselfromHomeEditableWidgetenum and its references
Risk: MEDIUM — home_tab_screen has multiple blog reference sites; methodical line-by-line removal required.
Steps 7–9 — C4 / C5 / C8: Online play, server games, social entries
These are larger cuts with more cross-cutting references. Detailed plans will be written immediately before execution. Key constraint: C3 (auth) stays, so the auth model and HTTP client infrastructure is preserved.
High-level scope:
- C4: Remove
view/play/lobby/seek UI;model/challenge/;model/lobby/ - C5: Remove online game lifecycle from
view/game/andmodel/game/; removemodel/correspondence/; preserve game-frame board widgets - C8: Remove navigation entry points to friends/inbox/players from More tab; model/social code stays for now
Steps 10–11 — C17 / C18: WebSocket + HTTP repositories
Execute only after C4–C9 are done and confirmed to have no remaining socket or HTTP consumers outside auth and study-import paths.
What is explicitly NOT cut
dartchess+chessground+ piece/board assetsmodel/common/(chess.dart, node.dart, eval, id, uci, perf, etc.)styles/,widgets/reusable set,db/sqflite,model/settings/
Amended by D016: "not cut" here means these directories are not deleted wholesale; it does not mean
their visual contents (Lichess colours, Material-styled components, Lichess-specific widgets) are frozen.
The visual identity in design/docs/ supersedes the Lichess-styled parts of styles/ and widgets/.
Structural/non-visual code in these directories (layout math, platform adapters unrelated to look) may still
be kept and reused where it doesn't conflict with the new design.
- l10n pipeline
model/auth/(C3 kept — optional login for study import)view/study/,model/study/(D1 — closest to our review scene)view/analysis/,model/analysis/(C15 grey — shared with study)view/board_editor/,model/board_editor/(C16 kept)model/explorer/,view/explorer/(C14 kept)view/clock/,model/clock/(clock tool kept)network/http.dartbase infra (needed for auth + study import)- GPL-3.0 LICENSE, COPYING.md, copyright notices — preserved forever
Completed cuts
| Step | Feature | Commit | Date | Tests before → after |
|---|---|---|---|---|
| 1 / C9 | Learn tab + coordinate training | f74627873 |
2026-09-14 | 1570 → 1564 |
| 2 / UI | Lichess branding (donate, about, LichessMessage, welcome card) | 1af79eab3 |
2026-09-14 | 1564 → 1564 |
| 3 / C7 | Watch tab (TV / tournaments / broadcasts) | dd996ee74 |
2026-09-15 | — |
| 4 / C6 | Puzzles tab (model/view, nav, deep links, quick action) | fb4847be2 |
2026-09-15 | 1242 passing, analyze 0, linux build ok |
| 5 / C11 | Over-the-board game & Clock tool | 4d1c4b211 & 1b7e18a03 |
2026-09-14 | — |
| 6 / C10 | Blog / recap / announce (models, services, home carousel) | 156d760fa |
2026-09-15 | 1242 → 1235 passing, analyze 0, linux build ok |
| 7 / C4 | Online play: lobby / seek / challenges | c2094c070 |
2026-09-15 | 1235 → 1182 passing, analyze 0, linux build ok |
| 8 / C5 | Server game lifecycle + correspondence | bc7b4dcc4 |
2026-09-15 | 1182 → 1084 passing, analyze 0, linux build ok |
| 9 / C8 | Social navigation entries from More tab | 790bb99c2 |
2026-09-15 | 1182 passing, analyze 0, linux build ok |
| 12 / Home | Home tab + home widgets/prefs (clean 2-tab shell: Review + More) | 29c3db19e |
2026-09-16 | 1188 → 1158 passing, analyze 0, linux build ok |
| 13 / C8-Views | Dead social views & models (messages, conversations, friend/player screens) | b8664182d |
2026-09-18 | 1267 → 1252 passing, analyze 0, linux build ok |
| 14 / User-Views | Dead leaderboard & online bot screens, models, and providers | b0ad26284 |
2026-09-18 | 1252 → 1248 passing, analyze 0, linux build ok |
| 15 / C8-Relation | Dead relation repository, following_user model, and follow/block actions | db6409c31 |
2026-09-24 | 1248 → 1246 passing, analyze 0, linux build ok |
| 16 / C4-Playban | Dead playban dialog, notification model & account service monitoring | c3e91b183 |
2026-09-24 | 1246 passing, analyze 0, linux build ok |
| 17 / UI-Residue | Dead orphaned widgets, views & assets (UserContextMenu, ServerOutageDisplay, ExpandedSection, SideIndicator, TextBadge, BrightnessNotifier) | 8f0e8f7d3 |
2026-09-24 | 1246 passing, analyze 0, linux build ok |
| 18 / Gestures | Dead Android system gestures exclusion utility & method channel handler | bd4fa6c97 |
2026-09-24 | 1246 passing, analyze 0, linux build ok |
| 19 / Android-Widgets | Dead Android broadcast widget (BroadcastWidgetProvider, layouts, preview drawables, strings, receiver) | 3ed89a5b7 |
2026-09-30 | analyze 0 |
| 20 / Home-Widget-Plugin | Drop home_widget plugin dependency, board preference listener, AGP 9 workarounds | f7ff59df9 |
2026-09-30 | analyze 0 |
| 21 / iOS-Widgets | Dead iOS LichessWidgets extension (1830 LOC Swift, targets, schemes, FeedKit/flutter-chessground SPM deps) | b8777bef8 |
2026-09-30 | pbxproj syntax valid |
| 22 / Firebase-FCM | Strip Firebase, Crashlytics, FCM push models & local notifications (Option B) | pending | 2026-09-30 | analyze 0 |