Skip to content

Latest commit

 

History

History
140 lines (120 loc) · 42.5 KB

STATUS.md

File metadata and controls

140 lines (120 loc) · 42.5 KB

Status

Last updated: 2026-11-11. Where the project is now: what is live, half-finished, known-broken or deliberately not done. What the product does: docs/README.md. What changed and when: the changelog page (merged PRs by week); before #1220, CHANGELOG.md and changelog-archive/. Closed items are deleted from here, not struck through; git history keeps them.

Keeping it honest: a PR that finishes a line here deletes or moves that line in the same PR. New open items go to GitHub issues with a label; the items below move to an issue when someone touches them.


Shipped and live

Area State
Reader (web + mobile) EPUB + PDF (original-first, ADR-012), page-based progress, highlights, TTS, vocabulary SRS, one chapter at a time. Both clients read through packages/reader-engine (ADR-025, Phases 0–6 done 2026-10-15, old readers deleted). Position rules: ADR-019; re-ingest keeps readers' data (ADR-018). The free dictionary is gone (2026-10-03).
Sync Progress last-write-wins per client clock; offline highlights replay with a three-way merge; reading sessions queued on both clients.
Security & ops All P0/P1 items of the architecture review 2026-10 fixed. /me and /internal fail closed by path (ADR-024); every ungated route is in tests/TextStack.UnitTests/Routes/routes.public.txt. Only the migrator migrates (ADR-021).
Backups Nightly off-site copy to Cloudflare R2 (restic) and a monthly restore drill. backup.md.
AI platform Translate and Explain/enrichment on OpenAI; vocabulary distractors, tags and fallback metadata on local Ollama; SEO publishing crews; Tutor study planner. Reader-facing chat surfaces were deleted 2026-09-10; the conversation happens in the reader's own assistant.
Assistant handoff (MCP) 21 tools (backend/src/Contracts/Mcp/McpManifest.cs) over stdio + streamable HTTP; OAuth sign-in (ADR-017) or connect key for ChatGPT; conclusions come back as BookInsight; chapter review (ADR-016). assistant-handoff.md, mcp.md.
Observability Sentry on API, Worker, MCP server, ssg-worker and mobile; OpenTelemetry → Aspire locally only. delivery.md.
Entitlements & guests UserTier { Guest, Free, Supporter, Staff }, config-driven quotas; guests get a real server session that registering promotes in place (ADR-014).
SEO / SSG Prerendered pages, sitemap, IndexNow; frozen since #992 (no nightly rebuild, no age alarm; manual rebuilds only, future undecided in #992). Four incidents since 2026-08-11 (list); now watched two ways (worker refuses a rebuild that lost files, crawler health check every 5 min). Hung rebuilds end Failed after 5 min without a rendered route.
Mobile Android is in production on Google Play (owner, 2026-10-23); Internal and Closed tracks stay for testing. Expo SDK 57 / RN 0.86. OTA via expo-updates on merge; a moved runtime fingerprint builds and submits to Internal instead. Offline by default: own uploads stored on the device (2 GB LRU), library downloads on Wi-Fi, chapter loaders read the device first. Release runbook: play-store-release.md.
Delivery Build once on GitHub, deploy by digest (ADR-020, delivery.md); secret-scanned images; pre-deploy dump; pinned actions. One Node version in .nvmrc, one pnpm workspace with a version catalog, weekly dependency refresh PR.
Catalog Curated Popular shelf (Edition.FeaturedRank; admin or make featured). Search is Postgres FTS over chapters only.

In flight

Focus (owner): no new side work. Anything new goes into Known-broken below and is not started, unless it is a production outage or a security problem.

  • Reader engine follow-ups (ADR-025 done): owner step EAS variables after the bake (#867); owner pass of web Phase 4 on prod; open #829, #835, #850, #851, #856 (bundle budget), #846 (test review), #824 (e2e blind spots), #857 (nginx security headers).
  • Reader after R4: split web ReaderPage.tsx (~900 lines) and ReaderHighlights.tsx (661) by job, as #731 did for mobile. Every R1–R4 mobile fix is unit-tested only: the owner's Android phone checklist is still owed (QA-007).
  • Delivery, owner steps (ADR-020): repo settings (require actions pinned to a full SHA, allowed actions = GitHub + verified + listed, turn off "Actions can approve PRs", delete the unused CLAUDE_CODE_OAUTH_TOKEN secret, enable non-provider secret patterns). Then measure an images run after the slim Dockerfiles and delete Free disk space if there is room.
  • Assistant handoff, owner checks: that the mobile Claude and ChatGPT apps accept a custom connector at all, and one live end-to-end conversation on a phone.
  • Chunked upload: 1 of 8 steps (tiers, #449). Files over ~100 MB still fail at Cloudflare's body cap with Upload failed: 413.
  • Two-audience onboarding ("I work in tech" / "I'm learning English"), planned, not started. Was deferred until Play production, which is now live. The mobile card still says "Learn a language by reading real books". Plan. Prod nit: 10 arXiv editions have their abstract inside "About this edition".
  • Later, in 2–3 bundled PRs: the rest of review 2026-10 (#19 admin roles/audit, #20 book models, #21 LLM jobs on the owner's Claude login, #24 web api/ duplicate, #25 two SEO engines, #26 GDPR traces, #31 ADR numbering, #32 housekeeping) and the ADR-022/023/024 remainder (/storage covers #956, delete the book quality queue, reconciler enrichment).
  • Article: Sentry write-up, draft on vasyl.blog; needs edits, image, publish, then a DEV cross-post.

Known-broken / open follow-ups

Each is a real defect, known and not yet fixed.

Ingestion, SSG, ops

  • Upload covers are readable by URL without auth (/storage, ADR-024 step 2) — #956.
  • /internal trusts a forwarded address from the home LAN (ADR-024): ForwardedHeaders trusts X-Forwarded-For from 192.168/16. nginx refuses it from outside. Fix: shared-secret header (ADR-024 step 3, after #19).
  • Six user routes live outside /me and rely on their handler's check: GET /auth/me, POST /auth/device/approve|deny, POST /oauth/authorize/approve|deny, GET /oauth/token-status. No plan to move them.
  • Mobile OTA targets one runtime only (#762, issue #914): it publishes to the runtime of the newest finished production build. If production runs an older build than Closed testing, production stops receiving OTAs. Fix before the second store release. Runbook.
  • Deploy-interrupted uploads (ADR-022, fixed #769) were not yet watched through a real deploy: check the next deploy's Worker log for "interrupted by shutdown; returned to queue" and that no upload ends Failed.
  • Interrupted ingestion can leave orphan image files (disk only). Fix: deterministic image names, or sweep assets/ against book_assets.
  • Vocabulary words promoted by the hourly reconciler are never enriched (DailyCapService.ReconcileUserAsync does not call QueueEnrichment).
  • Cancelling a Running SSG job does not stop it: ssg-worker never re-reads the row and its final status overwrites Cancelled.
  • Unverified: does ssg-worker's Host header reach the API? Node fetch to http://127.0.0.1:<port> with host: 'localhost' got a 400 locally; prod works (API_URL=http://api:8080), but not checked why.
  • SSG stall detection ignores first-pass failures (SSG_JOB_STALL_MS): a good build with ~160 consecutive failures is stopped; a moving job can outlast the deploy's 40-min SSG wait.
  • SSG treats any failed API call as a failed page, including secondary calls the app hides; one broken endpoint fails every rebuild (loud: Failed jobs, Sentry).
  • Soft-404s to crawlers: the catch-all nginx location / returns 200 for non-SSG paths. Dead nginx block: location /ssg/ aliases a directory that does not exist. X-SEO-Render lies: set from map $is_bot, reports "you look like a bot".
  • GitHub scheduled backups run hours late (backup.yml cron 03:00 UTC started 10:12 on 2026-10-05). Consider triggering from the server's cron, GitHub schedule as a fallback.
  • Server hygiene: three .env.bak* files with live secrets; the server's node_modules does not match the lockfile (delete root and apps/*/node_modules once, by hand).
  • Review 2026-10 P2 still open: #15 one claim rule for the 11 polled queues, #16 background services inside the API and fire-and-forget vocab enrichment, #18 auth opt-in per endpoint, #19 admin roles unchecked / no audit log / admin API on the public host, #20 one BookRef rule, #24, #25, #26 (GDPR delete leaves LLM trace text, caches, backups). Also claude-isolated.sh under env -i is untested on the server.
  • llm_traces grows without bound (prompts and book excerpts, no cleanup job; account deletion anonymises). Check the size before writing a retention job.
  • Four dependency advisories have no fix to apply (decode-uri-component ×2 and uuid inside Expo's tree; zod 3 via @expo/cli). Listed in KNOWN in scripts/check-advisories.mjs; none ships in the app. node-forge left the list 2026-11-01 (1.4.2 fixes both of its advisories, in range of @expo/cli). The check also warns, non-blocking, on deepmerge, glob and minimatch (Dependabot opens the fix).
  • @sentry/react-native is ahead of the Expo SDK pin (8.24 vs ~7.11); recorded in expo.install.exclude as deliberate, but nobody remembers whether it was.
  • Play Console (#915, #916): the Data Safety form may still hold the pre-2026-08-20 answers, which contradict the privacy policy; the correct answers are in play-store-release.md. The store listing text was updated 2026-10-24 (in review); the feature graphic and one screenshot still show "Ask this book", which no longer exists.
  • One permission group still requested without need: 20 OEM launcher/badge permissions from ShortcutBadger via expo-notifications (WATCH list in apps/mobile/scripts/check-android-permissions.mjs). Confirm in Play Console → App content that the foreground-service declaration is gone (removed in 1.1.0).
  • Notification permission dialog (P2-5) from the 2026-08-26 pass: no code path in the app can produce it (targetSdkVersion: 36). Reproduce with adb logcat on an emulator before changing anything.

Reader and mobile

  • Shared nearestOccurrence tie-break overrides a context-unique match (ADR-025 spike): the engine's resolve.ts breakTie fixes it; apps got it by switching to the engine. Verify nothing else still uses the old one.
  • Mobile: Continue on a catalog PDF read in Original layout (page:N, no chapter) opens the detail screen; guest Library empty online after a download without "Save to Library"; Library "Continue" opens book detail, not the reader; native selection handles stay after highlighting; dev-build toast "injectJs failed: markVocabWords is …" (check whether vocab marks can miss on first paint); highlight anchors over a saved word include its inline gloss (resolve only fuzzily).
  • Gloss backfill translates saved words without their sentence and uses the open book's language for words saved from other books (mobile useReaderVocabMap, web useReaderVocabulary).
  • Web: a new reader who confirms a native language different from the browser's guess while a word popup is open can save that word with the guessed-language translation. Web has no service worker, so "Download" is not real offline in a fresh tab.
  • Web useReaderScrollSync ponytail: ceiling (#729): after an open the server has not answered, a layout shift above the reading line larger than 48 px reads as the reader moving, and the stale local place is saved.
  • ADR-019 owner questions (end of the ADR): after a TOC jump, should a newer same-chapter position from another device move the reader silently on mobile? Should web get the "read further on another device" toast?
  • Anchors: web's anchor creator skips .vocab-inline-translation text, the phone's getRangeAnchor does not (only the PDF body fallback); the mcp.json anchor fixture is copied by hand from SynthesizeAnchor; an MCP highlight of a repeated quote lands on the first occurrence.
  • PDF: highlights have no context and never will (PDF-rect anchors carry only exact); the mobile viewer measures against innerHeight including any strip RN chrome overlays.
  • A word tap can still dispatch two messages one character apart (reader bridge tap plus Android selectionchange; suspected WORD_RE straight vs curly apostrophe). Measure both strings before changing anything.
  • Offline by default, not verified on a device: the automatic Wi-Fi sweep fetching a library it does not hold, the 2 GB budget and its eviction order, the full-disk stop.
  • Mobile tests: reader hooks have no tests of their own (wiring guards readerR3Wiring.test.ts, readerR4Wiring.test.ts); Lane A e2e is 17 tests in 6 specs that CI does not run (need a live backend), see MOBILE-TEST-PLAN.md; useBookLibrary and #782/#783 review leftovers are cleanup, not bugs.
  • A new user is shown nothing that explains the app (onboarding/language only asks the native language; guests are not asked). A guest can finish the loop, but nothing on Discover says so.
  • An abandoned guest upload is permanent disk: GuestCleanupWorker preserves any guest holding an upload. Deliberately not solved until the real occupancy figure is known.
  • iOS Universal Links were never configured (no associatedDomains, empty entitlements).
  • Login answers 500, not 401, when a stored password hash is malformed (BCrypt throws in AuthService); only reachable with bad data.

Data and product

  • BookInsight.Source is a constant ("mcp", never updated on replace), and the Edition FK cascades: deleting a catalog edition hard-deletes every reader's insights about it.
  • Highlight "revisit" has no recall model: Highlight carries only LastReviewedAt and a 24-hour cooldown; making it real needs new columns and an SRS path.
  • ReviewCardDto.reviewMode is a dead field (no client reads it; 'context' cannot reach the wire), and selfAssessment is captured and discarded (the three-button card sends it, no server code reads it).
  • RetrievedCard carries no review history, so the tutor's grounded re-projection relies on the prompt rule alone.
  • t() has no plural rules; plural() in packages/shared handles English one-vs-many in code.
  • Four QA accounts and their guest rows sit on production (qa-guest-{a,b,c,d}-20260906@textstack.app plus abandoned guest rows holding vocabulary); the findings they were created for are closed, so they can go.

V2 — in flight

  • Map: v2-architecture.md (start here) → v2-plan.md — two databases, one cutover, reader track in parallel; slices by milestone in §8, one issue each under #1013. ADR-027 stays Proposed as research input.
  • Decided (Proposed ADRs): ADR-030 V2 lives in a second database textstack_v2, Dapper-only, one rehearsed cutover; ADR-031 the V2 schema (24 tables + 2 transitional) and the fate of every legacy table, open items 1–7 decided 2026-10-30; ADR-029 foliate-js renders V2 local EPUBs. Accepted 2026-11-07: ADR-032 one engine for every V2 format, a fork of foliate-js (PDF after a gate; packages/reader-engine retires after R8) — #1128.
  • Landed: M1.1 — the textstack_v2 baseline, its runner and migrator-v2 (#1014); T — typography rewrites prose only (#1041); M1.2 — V2DataSource + ILibraryRepository + IReadingStateRepository, Testcontainers-tested, nothing in api/worker reads V2 yet (#1015).
  • Decided (PDD): M2.1 — the V2 sync contract, v2-sync-protocol.md: payloads, revision, operation_id, 409 bodies, file sync in parts, change feeds, the guest→account merge; two schema deltas for migration 0001; 13 owner questions with defaults at its end (#1019).
  • Landed: M1.3 — IAnnotationRepository + IVocabularyRepository, and the shared RevisionedWrite<T>/WriteResult<T> (#1016); M1.4 — ISharedChapterRepository, ICheckpointRepository, IBookInsightRepository (the V2 twin of IInsightRepository; legacy callers switch at M2.4), IReviewQuestionRepository (#1017).
  • Landed: migration 0001 — the sync PDD's and the owner's schema deltas (reset_revision, ON UPDATE CASCADE for the merge, the share FK's SET NULL, retired_at, books.revision, content_removed_at, …); the round trip runs a real down; what the merge must do itself is pinned in Schema/MergeReparentTests (#1086). Applying it on production is make migrate-v2 (V2 is in backups since #1043; the owner deferred the backups-first rule while V2 held nothing).
  • Landed: M1.5 — the identity, ops and transitional repositories; account delete is one DELETE FROM users with the cascade pinned over all 21 user tables; the M1 repositories are complete (#1018). Open from its reviews: #1089 (provenance FKs vs immediate constraints), #1092 (a reset vs credentials created concurrently, M2.5).
  • Landed: M2.2 — the first V2 API, /me/v2/* library, reading and annotation sync behind Features:V2Api (off; off in production until M2.5, #1095); V2 readiness gates that group only and /health/ready reports a soft v2 component (#1053, #1044); the explicit V2 unit of work (#1067) and the replay-safe import (#1065); CI runs the V2 integration classes against migrator-v2's database (#1020). Before the flag goes on for real users: #1095, #1094.
  • Landed: M2.3 — V2 vocabulary sync (/me/v2/vocabulary/*: save by key, idempotent examples, server-side SRS on the locked card, retire ≠ delete, due + change feed) and the MCP vocabulary tools re-pointed behind TEXTSTACK_MCP_V2 (off; renamed by M2.4) with a required operationId (#1021).
  • Landed: M2.4 — V2 shares and checkpoints (/me/v2/books/{id}/shares|checkpoints, checkpoint-saving) and the three MCP discussion tools behind the bridge's one TEXTSTACK_MCP_V2 switch (21 tools off, 24 on); the MCP manifest is one list with a V2Only flag (#1022). Ceilings: #1098.
  • Landed: M2.5a — the V2 guest→account merge (IAccountMergeRepository, per aggregate, one lock statement, 6 round trips) and the mirror that runs it, the account delete and the register promotion on V2 after the legacy commit, best-effort behind the flag (10 s budget, one retry, Error logs); OAuth grants and MCP keys die with the guest; #1089 closed by contract (#1023). The MCP manifest follows Features:V2Api (21 off / 24 on), tied to the bridge's switch in compose (#1101). Owner decisions 2026-11-02 are in the PDD's merge section: no real users on V2 before M5 (no outbox); a failed mirror loses guest-only V2 rows, visibly; the loser insight preserved in full, language und; no identity-only runtime switch.
  • Landed: M2.5b — IAuthService/IAdminAuthService, JwtTokens/ProviderTokens out of the legacy services (endpoints untouched), V2AuthService/V2AdminAuthService over the identity ports — unwired; production stays legacy until the fully wired Database:Mode at M5.1 (#1023; M2 complete). The Apple verification path is reviewed later (owner-deferred 2026-11-02).
  • Landed: M2.6 — the V2 file store on its own volume (Storage:LibraryRoot = /library, keys library/…, parts uploads/…, outside the public /storage root; startup refuses nested roots, the /storage denylist stays, #1110), upload parts and content with the quota decided inside the attach (SUM of stored files, per-user advisory lock, 413 quota_exceeded), Range downloads, covers, "stop syncing", transitional chapter reads; bytes released row-first by replace, merge and stop-syncing with a daily orphan sweep as backstop; the transitional extraction worker replaces a file version's book_chapters whole (#1024; #1091 settled).
  • Landed: migration 0002 on textstack_v2 — credentials_reset_at (every credential minted from a pre-reset access token refused, OAuth consent included), session kind + family_id (families revoked and swept whole; the migration signs every V2 session out), the feed/sweep/device/checkpoint indexes (#1107; #1092, #1047 closed). Not applied anywhere deployed: migrator-v2 is profile-gated; make migrate-v2 on prod is the owner's (after #1043).
  • Landed: M2.7a — V2 accepts the legacy reader's textstack-* locators on reading-state and annotation writes under the owner's six constraints of 2026-11-02 (chapter must exist on the user's file version, checked in the write SQL; structure validated; never converted to a CFI; revision rules unchanged, mixed schemes ordered by fraction; old-reader round trip tested; removal = the V2 reader on every client and the old-client window closed → R8 #1040) (#1112). The rules live in v2-sync-protocol.md "Transitional locators".
  • Landed: V2 in the nightly backup, the pre-deploy dump, make backup/restore-v2, the restore drill and health-check.yml; make migrate-v2-drop CONFIRM=<db> is the baseline's scripted rollback (#1043).
  • Landed: #1100 — every identity path (OAuth, MCP keys + middleware, device, guest activity, profile, account delete, create-admin, the retention sweeps, the guest reaper) on the M1.5 ports / M2.5b services with legacy EF adapters, no runtime switch; contract tests on both databases; the iat reader; one V2AccountDeletion; the reaper asks V2 before reaping and pauses when a configured V2 cannot answer (#1100). M5.1 re-binds the ports with Database:Mode.
  • Landed: M2.7 web — the web app speaks V2 behind the flag (probe per app start, V2 feed shelf, sha256 → parts → complete upload, the transitional reader with textstack-position-v1 state and the full 409 rule set, annotations, vocabulary as flashcards, the merge re-key, V2_FEATURES hiding what V2 lacks, a blocking @v2 e2e pass in CI); packages/shared/src/api/v2.ts and the locator mapping shared with mobile (part of #1025). Ceilings: #1117 (#1118 closed in wave 8).
  • Landed: M2.7 mobile — the Expo app speaks V2 behind the flag on the shared module (probe re-asked after a network failure or 503, the V2 feed shelf cached in SQLite with file_id, upload by streamed sha256 → readBytes parts ≤ 8 MB → complete, the reader SQLite-first then the transitional chapters, the tagged write outcomes, annotations pulled once per open, vocabulary as flashcards with practice never posted, the merge map re-keying SQLite/AsyncStorage/queued sessions, V2_FEATURES hiding what V2 lacks) (#1025 — M2 complete on both clients, wave 6 closed). Device-only checks left for the owner: large-book hash time, a content:// pick, a PDF over the V2 content route, the file_replaced redirect, an offline cold start, a real guest→account merge.
  • Wave 7 (M3 ETL): M3.1 etl-v2 CLI skeleton done (#1026; ledger 0003/0004; runbook docs/internal/ops/v2-etl.md); M3.2 mappers done (users/admin + uploads, #1027; takedown restore gap → #1138); M3.3 mappers done (catalog pairs, vocabulary case-merge, insights, sessions, unknown-users, #1028); #1110 done (library on its own volume). M3.4 done (validate-etl.sql + T1–T4, #1029); 0005_book_formats merged with the owner's explicit yes (fb2/mobi/azw3/cbz; ETL carries fb2). First isolated rehearsal accepted 2026-11-09 (restored pre-deploy backup → throwaway pair on the server, prod read-only: dry → real → validate-etl.sql exit 0 → re-run 0 writes → validate; 677 MB bytes; it caught and the owner settled one legacy quote drift — whitespace before punctuation now ignored). That was 1 of plan §5.1's 3 rehearsals.
  • Wave 8 (2026-11-09): mobile/web V2 client hardening — anchors always carry offsets and edits echo the stored locator (#1139–#1143, #1150–#1151 fixed; emulator checklist all PASS, true-hardware rows stay with the owner); the ETL computes offsets for legacy offset-less anchors and reports every anchor V2 refuses; #1094 closed (was implemented in M3.2, now pinned by tests); V2 feed carries the file summary (#1118). M3.5 rehearsals #2 and #3 passed via restore-drill.yml etl_rehearsal=true on GitHub runners (latest + pinned 2026-11-08 nightly; dry → real → validate exit 0 → re-run 0 writes → validate again; reports on #1030); owner accepted 2026-11-09 — M3.5 done. Decisions 2026-11-09: #1117 closed (B — no V2 bookmarks until R5/R6), #1109 closed (transitional images wait for R5). Open: web epoch twin #1155, offline console noise #1156; shared client factories #1121 with R6.
  • Server, repaired 2026-10-30: the production cluster could not create any database — template1 and postgres recorded collation version 2.41 against the image's 2.36, since its own initdb. bt_index_check passed on 48 of 48 text btree indexes first (so textstack_prod had been recreated by a restore, not relabelled, and needed no reindex), then both were refreshed and CREATE DATABASE works again (#1056). /health/ready now reports a collation component over every database and health-check.yml alarms on it. migrator-v2 stays behind compose profile v2 and gates nothing, after it took the API down for 44 minutes that day (postmortem, re-gate #1053).
  • Reader: R1 landed as a drift-checked artifact; R2 — the __DEV__-only Android dev route hosting it, emulator-verified import → kill → resume (#1034); R3 — the sandbox check asserts and proves it can fail, in CI (#1035); R4 agent half done on emulators — 87 MB book pass, ENOSPC partial (host leftovers fixed), the page's real WebView floor is Chrome 117, hostile book 30/30 on Android, file:// recommendation written (#1036); the physical-device row, the WebView-screen decision and the file:// decision are the owner's; dev-only until then. R5 — the page's web host: dev-only /dev/reader-v2 (iframe + nonce'd shim, ArrayBuffer in, localStorage only), prod bundle unchanged and asserted at every build, Playwright-covered (#1037). The page builds from the fork pin mrviduus/foliate-js@b3386ef9 (textstack-pin; #1202); every further fork push or pin change needs the owner's yes. Open host findings: #1080, #1081.
  • Wave 10 (2026-11-10, 8 PRs, all deployed): the reader track's client milestone. #1168 fixed (stale relocate; one open-seq mechanism, both hosts); mobile error states #1163/#1164 (residue → #1174–#1176); one copy of the V2 client in packages/shared (#1121 — mobile takes web's epoch rules, web takes mobile's clock clamp); sync-PDD prereqs #1078 (v4 UUID ids, code-point quotes, selectedText; ADR-031 amended); #1128 format work — backend (#1181: uploads accept mobi/azw3/fb2/cbz, epub-cfi fits any non-PDF) and the page (#1183: MOBI6/FB2/FBZ/CBZ read under the unchanged CSP, scrolled flow + telemetry, DRM/entity/KF8 refusals) — only the PDF gate remains on #1128; R6 landed (#1038, #1185): SQLite textstack-v2 library + outbox sync client per the PDD, opt-in file sync, legacy migration — deferrals #1186–#1189 (#1188 CBZ positions is the owner's rule call), device matrix owed. Process: the mobile CI job is NOT a required check and auto-merge landed it red twice (#1184 fixed; making it required is the owner's branch-protection click).
  • Wave 11 (2026-11-11, 5 PRs, all merged): pre-M5 gaps and the reader's last formats. A transitional (textstack-*) position restores by bookFraction, and opening a book writes nothing until the reader moves (#1187, #1197; anchors stay undrawn → #1195, pre-paginated EPUB saves no position → #1196); a server-deleted upload is remembered on the device (#1174, #1194; residue #1191–#1193); the outbox counts failures — 5 per session, then kept and skipped, never parked or dropped (#1186, #1199; residue #1198); fork pin bump, six commits: AZW3 (KF8) reads, on a synthetic fixture only (#1182 closed, #1202); the page reads PDF on its dev hosts (#1205): pdf.js on the page's thread under the unchanged CSP, pdf-page positions, build-before-teardown opens. The ADR-032 PDF gate has its desktop half only (55 real uploads: all open, page, resume; index-only report in the archive) — no device rows, so #1128's PDF box and R7 #1039 stay open and the ADR-012 viewer stays; fork-side gaps #1203, owner decisions #1204. #1138 deferred. Nothing in this wave was run on a device; the checklists are in the five PRs.
  • Wave 12 (2026-11-11): sized R8 and closed small gaps. R8 parity audit — r8-parity-audit.md: what the legacy readers do that the V2 page does not, a proposed R8 DoD, 7 owner questions with recommended answers, 6 waves to R8 (A hosting → F deletion); the calendar is bound by M5 and the old-client window (#1040, #1208). Merged: a V2 store reset never lets a listing delete downloads (#1191, #1207); R7 tests — pdf-page through the Android host path and the V2 API, no code change needed (#1209; #1039 stays open: no device gate, no web host sync, no PDF highlights); CI no longer goes red on an apt/DNS outage and builds the web app once per e2e run (#1211, after a runner DNS outage blocked #1210 three times). Open for the owner's review: sign-out clears both id sets (#1192, #1212); two legacy reader fixes (#1213). Not started: #1193, #1196 (waits for the #1188 rule), #1198. Nothing in this wave was run on a device. From this wave on the owner reviews and merges every PR.
  • Web reader hosting (wave 13 / wave A): v2-reader-web-hosting.md — threat model, options, slices. Owner decided 2026-11-11: same origin, inside the app, for now; a second origin is revisited before the flag goes on (M5). The production-CSP check runs first. Slice 1 (#1219): the dev host embeds the page as its own same-origin document (measured: a srcdoc frame never becomes ready under the production CSP), with prodCsp.check.mjs gating it in CI. Slice 2 (#1225): the production route /:lang/library/my/:id/read-v2 behind Features:V2Api (bytes from /me/v2/…/content, one V2ReaderHost for both routes); the frame is a no-store shell + the content-hashed, immutable page script (ADR-029 Decision 4 amended 2026-11-11); nginx serves both, app CSP frame-src 'self'. Slice 3 (#1226): that route's position and highlights live on /me/v2 (shared mapping v2PageEntry, online-first, "not saved" until a write lands; this tab wins a conflict for now; CBZ positions local until #1188); the dev route keeps localStorage. Slice 4 (#1227): loading/error states and the ready/opened deadlines (deadlines.mjs) on both hosts, Retry where it helps; Android error lines per #1080 item 6; #1081's corrupt library.json kept aside. Slice 5 (#1228): Android WebView older than 117 → an "update Android System WebView or Chrome" screen (user agent read before the page parses); book bytes without file://, built on the owner's decision — the page loads under https://appassets.androidplatform.net/ and a pnpm patch of react-native-webview serves exactly the retained or staged book (CSP connect-src drops file:; v2-reader-android-bytes.md). Also in wave 13: a server-deleted book keeps its bytes until the reader removes them (#1218). Wave 13 closed 2026-11-11: slices 1–5 merged and deployed dark (#1221, #1231, #1234, #1235, #1241, #1249); the device checks in #1235, #1241 and #1249 are the owner's; next is the reader chrome and the word layer (R8 audit waves B and C).
  • Decided 2026-10-30 by the owner: the 13 sync-PDD questions (recorded in v2-sync-protocol.md "Owner decisions": retired ≠ deleted, revision-guarded metadata, client covers, stop-syncing, merge keeps both insights, operation_id per logical operation, …) and the ADR-029 file:// direction (native limited file serving over https).
  • Decisions pending from the owner: dump-archive location outside restic retention (before the production ETL); cutover date; Android book bytes: three defaults taken in #1228 (directory-scoped serving, staged picks served from staging/, main-frame bridge; v2-reader-android-bytes.md §4), vetoable in its PR; the R4 device run and the #1152/#1153 true-hardware rows; the wave-11 device checklists (#1194, #1197, #1199, #1202, #1205); two ADR-032 sentences (KF8 accepted on synthetic evidence; the PDF gate applies from the PR that ships the PDF module); the PDF CSP calls in #1204 (Worker, JPEG 2000 wasm); whether Mobile OTA (auto) keeps shipping changes nobody ran on a device; making the mobile CI job a required check.

Under discussion (no decision)

Nothing at the moment.

Deliberately not doing

Recorded so they stop being re-proposed:

  • Z-Library integration. The legal public-domain alternative (Gutendex import) is planned instead: ~/.claude/plans/textstack-public-domain-discovery.md.
  • Billing / Stripe / tier upgrade from the UI. Tiers exist; monetization does not.
  • An admin console over User. Staff is a config allowlist; the population is 1–3 people.
  • Mobile chunked upload for the legacy endpoint. RN cannot slice a picker's opaque content://; the legacy endpoint stays. (V2 file sync does slice the app-owned retained copy — expo-file-system 57 File.open() — per v2-sync-protocol.md.)
  • A UserBooks/Editions shared abstraction — assessed as false parallelism during the R1–R6 sweep.
  • Python anywhere. Distillation is TorchSharp + a synthetic teacher.
  • Audiobooks. A recurring per-book narration cost against a product whose thesis is reading; TTS already reads any selection aloud.
  • A bot that merges its own dependency updates. A merge deploys production; an automatic update is safe exactly as far as its constraints are machine-checkable (expo install --check in CI, a fingerprint report on every PR), and the rest are why nothing merges itself.
  • Majors, applied automatically. deps-refresh.yml raises patch and minor in the catalog and reports majors without touching them.
  • Two progress-path "fixes": LocatorKind on the catalog path would refuse the mark-as-read sentinel it was meant to protect (catalog books have no second coordinate space). MaxChapterNumber has a reader again (ChapterFrontier, chapter review's spoiler gate), so its missing remap on re-ingest is a real defect, written up in assistant-handoff.md.