Forward-looking companion to
ROADMAP.md(which covers the shipped M0–M6). Last verified: 2026-06-06 · Recheck cadence: per phase kickoff.Status (2026-06-06): N1–N6 shipped. All phases below are implemented with
make verifygreen (167 tests @ ~96% coverage,mypy --strict, lint,pip-audit0 vulns, a11y 0 violations, recommender beats popularity). Deeper follow-ups remain open and are noted inline: a real (still-local) embedding model, Lighthouse/k6 in CI, and sidecar highlight-text import. Live-network contract cassettes shipped 2026-07-03 (see §0).
Guiding constraint. Every item below must hold the four hard guardrails or it does not ship:
- Read-only / snapshot-first access to Calibre
metadata.db+ KOReaderstatistics.sqlite— never a write handle to a real library. - Reading data never leaves the instance — no third-party analytics, no reading history sent to any catalog; egress confined to the user's own kosync server and public-metadata GETs.
- No Goodreads / no gatekept catalogs — sources stay on the ethical allowlist with provenance.
- Describe books via sourced tags; never auto-label authors.
The shipped build is demo-driven. Close the gap between "demo works" and "runs on your real library" before adding features.
- Real-library config + first-run wizard — paths for Calibre/KOReader, kosync
host/user/key, storage dir; a
stacks doctorthat validates paths, confirms read-only access, and reports detected schema versions. (Phase N1) - Persisted derived state — a
data/SQLite app-state store with astacks refreshjob and an "ingest only if the source mtime changed" guard; surface a "data as of …" freshness stamp. (Phase N1) - Live-path contract tests — DONE (2026-07-03). Recorded-cassette tests
(
tests/test_live_clients_cassettes.py, bodies undertests/cassettes/) now exerciseKosyncClient,OpenLibraryClient, andBookwyrmClient— request building, header/URL construction, 404 handling, and theResponseCacheput/get path — against real response shapes, withrequests.getstubbed (no network in CI). Thepragma: no covermarkers on all three client classes are removed;make teststays green at ≥85% coverage. (N2) - Coverage honesty — add TestClient route tests so
app/server.pywiring is covered, not just the auth path. (N1)
Performance (Quality §2) — k6/Locust smoke (p95 < 500 ms dashboard route) + Lighthouse-CI on the rendered HTML; make merge-blocking.Done (2026-07-03).tests/perf/locustfile.py+scripts/perf-smoke.sh(make perf-load) boot the demo app and fail the build if the aggregated p95 on/is >= 500ms;.lighthouserc.json+make lighthouserun Lighthouse-CI against the builtdocs/audits/dashboard.htmlwithcategories:performance >= 0.9andcategories:accessibility >= 1.0as error-level (merge-blocking) assertions. Both are non-conditional steps in.github/workflows/ci.yml. (N5)Reliability (Quality §5) — restart-recovery test (reads persisted state) and a chaos test for "kosync down → degrade to KOReader-only".Done.tests/test_reliability.py(test_restart_recovery,test_kosync_down_degrades_to_stats), run merge-blocking as part ofmake test/ the CITestsstep. (N5)- Manual a11y sign-off — perform + commit the dated VoiceOver/NVDA walkthrough. (N1–N6, before first release)
- More read-only sources, same guardrails — Readest progress, Kobo's native
KoboReader.sqlite, Calibre-Web read-state, sideloaded EPUB/PDF. Each is a new adapter behind the existingunifyjoin. Kobo native (ingest/kobo.py) done — snapshot-firstKoboReader.sqlitereader over thecontenttable (ContentType = 6, chapter-row dedup, schema-drift tolerant), merged through the existingunifyjoin with zero changes toingest/unify.py; wired intoingest/config.py([kobo]/STACKS_KOBO_DB) andingest/refresh.py. Readest, Calibre-Web read-state, and sideloaded EPUB/PDF remain open. - Annotations & highlights — surface a private, searchable "commonplace book" from KOReader highlights; never synced anywhere.
- Series & TBR intelligence — "next in a series you own," progress through a series, a prioritized to-read shelf.
- Robust matching — ISBN/OLID resolution + a fuzzy-match review queue for ambiguous Calibre↔KOReader joins (translations/editions).
- Hybrid model — add a non-surveillance collaborative signal: co-occurrence of books across public curated lists, blended with the content model.
- Local semantic embeddings — local-only, optional (decided 2026-06-05). A small embedding model running on the seedbox over sourced theme tags + descriptions, for semantic similarity beyond exact-tag overlap. Strictly no egress; feature-flagged off by default. Parked until the list-co-occurrence hybrid proves itself. (N3)
- Diversity / aperture controls — a boost-only "lean into small-press / own-voices / translated / underread" slider (mirrors the values-lens pattern; "unknown" stays first-class, never penalized).
- Gentle negative signals — opt-in DNF / low-dwell down-weighting, explained.
- Richer eval — nDCG, catalog coverage, intra-list diversity, plus a real temporal hold-out on actual finishes; track metric drift across runs.
- Hardened live adapters — OpenLibrary, Hardcover (GraphQL), Bookwyrm federation behind the allowlist, with caching, rate-limit respect, and provenance on every field. (N2)
- Reusable "ethical book-data sources" list — ship the roadmap-promised, versioned data file others can reuse. (N2)
- Curated-list ingestion pipeline — import named community lists (citation +
retrieved_at); a refresh job flags rotted links. (N2)
- Progressive-enhancement UI — keep the static-render a11y contract; layer unobtrusive JS for filter/sort that degrades to the existing data tables.
- Wrapped, expanded — monthly timelines, pace, theme evolution across years, an opt-in local PNG/PDF export (nothing auto-published).
- Search & browse by sourced theme/genre, author, series, status.
- Goal tracking — pages/books/streak goals, computed locally.
- EXP-01 — OPDS feed of your own shelves. Shipped 2026-07-03. An
auth-gated, read-only OPDS 1.2 catalog (
app/opds.py, wired at/opdsand/opds/{to-read,currently-reading,series-next,recommendations}inapp/server.py) rendered from the sameDashboardViewthe dashboard uses, so it's browsable straight from KOReader/Readest. Recommendation entries carry the sourced why/explanation, never an inferred label; a Calibre-Web alternate link is config-driven (STACKS_CALIBRE_WEB_URL) and omitted when unset. GET-only, no new write surface. Seetests/test_opds.py.
- Containerize + one-command compose for the seedbox next to Calibre-Web; documented reverse-proxy + auth.
- Auth upgrade path — bearer token → optional OIDC/forward-auth; rate-limit the auth endpoint.
- Backups of
data/app state + a restore drill. - Preservation-grade export — done.
stacks export --archive/stacks import --archiveround-trip the full derived state (states + activity + count-only highlight Web Annotations) through a versioned, self-describing, stdlib-only JSON bundle for decades-scale, tool-independent preservation. Seedocs/ideation/03-expansions.md(EXP-13). - Observability without telemetry — local structured logs with a hard "no reading-content in logs" lint (extend the no-egress test family).
- Schema-drift CI — a matrix of recorded Calibre/KOReader schema versions the
parsers must handle. *(Done:
tests/schemas/{calibre,koreader}/*.sqlfixturestests/schemas/MATRIX.md, parametrized intests/test_schema_drift.py, run bymake testin CI.)*
| Phase | Theme | Rationale |
|---|---|---|
| N1 | Real-library config + persisted state + stacks doctor |
Turns the demo into a tool you actually run |
| N2 | Hardened live catalog adapters + curated-list pipeline | Real recs, real provenance |
| N3 | Hybrid + (optional, local) embedding recommender + richer eval | The differentiated core |
| N4 | Annotations, series/TBR, search/browse | Daily-driver depth |
| N5 | Containerize, backups, perf + reliability gates | Production-ready on the seedbox |
| N6 | Interactive UI, expanded Wrapped, goals | Polish |
Each phase ends with make verify green and any new responsible-tech artifact
committed under docs/audits/.