This phase is mandatory before the spec can be locked or any real feature
work starts. See CLAUDE.md for the full brief and RESEARCH.md for
findings so far.
- Pin down KOReader's real
statistics.sqlite3schema from source. Done 2026-07-03 straight from the device's own plugin source (research/koreader-plugin-src/statistics.koplugin/main.lua); seeRESEARCH.md§1 for the full schema, thepage_statrescaling view, and the font-size/pagination handling. - Get a real sample database. Copied from
/mnt/Kindle2026-07-03, refreshed 2026-07-05:research/samples/statistics.sqlite3(9 books, 750 page-stat rows as of the refresh, latest event 2026-07-05; Royal Assassin now carries its 1 highlight) andresearch/samples/vocabulary_builder.sqlite3(still empty; feature unused so far). Both gitignored. - Clone existing third-party KOReader stats tools for reference into
~/.gitrepos/.studyrepos/:KoInsight,KoShelf,Kodashboard,readingstreak.koplugin. SeeRESEARCH.md§4. - Actually read those four tools' source for (a) what KOReader's own
built-in stats screen already shows and (b) their full derived-metric
catalogues. Done 2026-07-03: KOReader's UI surveyed from the plugin
source (
RESEARCH.md§4), all four tools catalogued (§5), converged conventions extracted (§6), underexplored territory mapped (§8). - Fifth tool researched 2026-07-05: Tome (
bndct-devops/tome, a self-hosted FastAPI+React server). Full dossierRESEARCH.md§5.5, ranked steal-list §8.1. The original four re-swept for residual value (§5.6). All five reference clones then deleted (re-clonable; upstreams in §9). Key delta: a word-count axis and a handful of cheap stats-DB-feasible cards (author affinity, personal records, finish estimates), now phased below. - Track down per-book
.sdrsidecar metadata. Structure, location, and thepartial_md5_checksum↔book.md5linkage fully documented from KoShelf/Kodashboard source (RESEARCH.md§7). Real sample copied 2026-07-05 from/mnt/Kindle:research/samples/Royal Assassin - Robin Hobb (1705).sdr/metadata.epub.lua(a finished book with a highlight:summary.status="complete",percent_finished=1,partial_md5_checksumpresent) plus the Jingo dup-title sidecar. Both gitignored (real book data). Content stays Tier C. - Update
spec.md's widget/chart list from a brainstorm to a commitment. Done 2026-07-03: normative derived-metric definitions plus a three-tier catalogue (differentiators / table stakes / deferred), each item checked against KOReader's UI and the four tools.
- Lock the real schema into
colophon-core(the placeholdertable_names()probe is gone;db.rsspeaks the confirmed schema). - Typed query layer over the confirmed schema: books (md5-merged,
junk-filter helper), raw events, and the
page_statview. - Derived-metric layer implementing
spec.md's normative definitions: sessions (300 s gap), day buckets + daily totals, streaks (today-or-yesterday rule), distinct-pages-read, capped and uncapped totals, interval-union coverage/progress, reading-speed series (day/week/month), completion detection (78 % / 20 % / 2 % with the restart-split heuristic). All pure functions, timezone-generic. -
db::snapshot(): plain filesystem copy of the db (+ WAL/SHM sidecars), then the copy is checkpointed; no SQLite connection ever touches the source./mnt/Kindleauto-detect deferred to the app layer (Phase 2+). - Test fixtures built programmatically (verbatim KOReader DDL,
including the
numberstable andpage_statview, in a std-only temp dir). 42 tests: 32 unit, 9 fixture-integration, plus a live-sample reconciliation test that runs against the gitignored Kindle copy when present and skips cleanly when not.
- Real
adw::ApplicationWindowlayout:NavigationSplitViewsidebar (library) + content pane (Phase 3's detail slot), per-paneToolbarView/HeaderBar, breakpoint collapse, toast overlay. Composite templates in the Viaduct house shape. - Database loading flow, stricter than planned: imports always snapshot (staging dir → validate → promote), so no user-chosen file is ever opened in place and no heuristic is needed; a bad pick can't clobber the good snapshot. Source path remembered in GSettings for Refresh (Ctrl+R / F5); startup auto-opens the canonical copy.
- Schema-version guard:
adw::Bannerwarning on unfamiliaruser_version, never a refusal. - Ingestion → UI proven end to end: library list with title, author,
total time, interval-union unique pages, relative last-open; data
loads off the main thread (
gio::spawn_blocking, no tokio). - Junk filter as a stateful window action in the primary menu (default on, 5-minute threshold), GSettings-persisted, live refilter on change.
- Same-title/author grouping (two files of one work): header row + inset member rows disambiguated by page count and short md5; display-only, data never merged.
- Kanagawa Dragon theming: full Dragon sheet on dark via adw 1.6+ CSS variables, accent-only sheet on light (Dragon has no light variant), swapped live on the system preference; palette exported as consts for Phase 3 chart ramps.
Verified against the real sample: 53 tests (11 app-side), plus a headless smoke run with screenshots (empty state, filtered/unfiltered library, live junk-toggle via gsettings).
The catalogue is locked in spec.md; this phase builds it. Definitions
land in spec.md first, colophon-core grows the queries second, the
widget renders third.
UI shape (Brandon, 2026-07-03): the sidebar gets an "All Books" entry above the book list. Selecting it shows library-wide widgets in the content pane; selecting a book shows that book's stats. Both surfaces grow widgets over the phase.
- "All Books" overview surface (v0.3.0): totals tiles (time, pages, books, active days, busiest day), streak tiles with date ranges, year heatmap, weekday averages. Respects the junk filter (recomputes live on toggle). Time-window selector (30/90/365/all) still to come.
- Per-book surface, first cut (v0.3.0): the Tier B stat card set (capped total labelled "as shown on device" with uncapped alongside, days reading, averages, sessions, KOReader-math time-left and finish-date estimates, interval-union progress bar). The Tier A per-book charts (velocity, page activity) are still open below.
Charting decision (first, it gates everything):
- Decided: custom cairo drawing on
GtkDrawingArea, no charting crate. Validated 2026-07-03 by building the year heatmap and the weekday bar chart as production widgets (colophon/src/charts/): both shapes came out clean, theme-reactive, and dependency-free (cairo toy text for short labels keeps pangocairo out too). - Shared chart scaffolding (
charts/mod.rs): Kanagawa ramps for light/dark, KoShelf-style discrete intensity quantizer (explicitly not Kodashboard's continuous alpha), tooltip plumbing, dark-notify redraw wiring.
Tier A widgets (the differentiators; nobody ships these):
- Reading-speed trend, library-wide (v0.4.0): pages/hour on a cairo line/area chart, daily buckets under ~10 weeks of history and weekly past that, nearest-point tooltips. - [x] Per-book overlay (v0.5.0): the book page shows its own trend with the library baseline muted behind it, same bucket so the series stay commensurable.
- When-do-I-read heatmap (v0.4.0): weekday × hour grid over the
whole history (
metrics::hourly_profile, attribution bystart_timelike KOReader's calendar histograms), per-cell tooltips. - [x] Time-window filtering (v0.5.0, via the overview window selector). - Session analytics, first cut (v0.4.0): session-length histogram (<5m … >2h buckets) + caption records (count, median, longest with date). - [x] Sessions per active day + starts-by-hour chart (v0.5.0).
- Per-page activity strip (v0.4.0): per-page total time and read
count from the rescaled view, sqrt scaling with a 90th-percentile
cap (KoShelf's numbers), pixel binning for long books, per-range
tooltips. This is also the "did it drag in the middle" velocity
view (Tier A #4's page axis).
- [x] Annotation markers (v0.19.0). Highlights/notes/bookmarks from a
provided
.sdrsidecar, drawn on the activity strip at each annotation's fractional position through the book (itspagenoover the sidecar's own page count, rescaled onto the current axis, never rawpageno; RESEARCH §7.1). Three-way bookmark/highlight/note classification in the parser; highlights and notes accent-coloured, bookmarks muted. Books without a provided sidecar show none.colophon-core::sidecar::Annotation. - Book velocity, remaining piece (v0.5.0): read-through cards now carry pages/day over the calendar span (the page axis was already covered by the activity strip).
- Per-completion cards (v0.4.0): inferred read-throughs on the book
page (dates, time, sessions, pages/hour, coverage), hidden for
books with none.
- [x] Overview completions timeline / books finished per
year/month (v0.18.0). A "Finished books" section listing each
finished work by finish date (detected read-through end, else
last reading day so a sidecar-only finish still places), time
per book, most recent first.
stats::finished_timeline.
Tier B widgets (expected furniture, done correctly):
- Year heatmap calendar (v0.3.0): GitHub-style Monday-start grid, quantized levels, per-day tooltips (date, time, pages); the grid shrinks for young histories instead of rendering a year of blanks.
- Streak tiles (v0.3.0): current/longest with date ranges
(
metrics::streaks). - Library totals windowing (v0.5.0): 30/90/365/all-time selector on the overview, scoping the totals tiles and the behaviour charts (hourly, speed, sessions, weekday) while streaks, the year heatmap, and monthly stay whole-history. Windows are calendar windows, not "last N days that had data" (Kodashboard's KPI bug; noted in RESEARCH §5.3).
- Per-book stat cards with device parity (v0.3.0): capped total
labelled "as shown on device", uncapped alongside, avg time/page
and time-left/finish-date estimates using KOReader's own capped
avg_timemath so Colophon never contradicts the Kindle. - Weekday distribution (v0.3.0): averages normalized by weekdays elapsed (KoInsight's raw-sum skew is the anti-pattern).
- Monthly distribution (v0.4.0): totals with empty months rendered, not skipped; January labels carry the year.
- Icon pass. A colophon press-mark: a Kanagawa-Dragon gold "C" with a
copper fleuron (the end-of-book flourish a colophon is) in its mouth,
pure vector paths so it depends on no font. Ships as the scalable app
icon, a monochrome symbolic variant, and
logo.svg. - Performance pass on a realistic future db (v0.6.0). A deterministic
synthetic fixture (200 books, four years, 222k
page_stat_datarows) and an ignored measurement harness (colophon-core/tests/ perf.rs) gave the baseline. Two findings drove the work: thepage_statview was materialized whole (367k fanned-out rows held in memory), and the overview re-ran its whole-history aggregation on every window toggle. Fixes: (a) the view is consumed as a per-pageGROUP BYreduction (StatsDb::page_totals) plus a Rust rescale for the last page (metrics::rescaled_last_page), never the fanned-out rows, parity-locked against the old path; resident set 27 MB → 19 MB; (b) the overview caches its window-independent aggregates (stats::OverviewBase) and recomputes only the windowed charts on a window toggle: narrowing a window 20 ms → 3 ms, all-time 44 ms → 23 ms. Timezone/DST math stayed in chrono (no SQLlocaltime), so the metric functions and their tests are untouched. Load time is unchanged (both paths compute the view once); the win is memory and per-interaction render cost. - Meson wrapper + desktop entry + AppStream metainfo + Flatpak
manifest, matching the Atrium/Conservatory/Viaduct pattern. Top-level
meson.buildorchestratescargo build --releaseand installs the GNOME-shaped layout (binary, gschema + compiled cache,.desktop,.metainfo.xml, hicolor icon); the gschema moved fromcolophon/datato a shared top-leveldata/(dev-run schema shim inmain.rsandbuild.rsupdated to match).org.virinvictus.Colophon.jsonbuilds it against the GNOME 49 runtime with a read-only host sandbox (--filesystem=host:ro, fitting the read-only ethos so Refresh can re-read a device path). Verified end to end:meson setup/compile/ installto a temp prefix lays the tree out correctly, anddesktop-file-validate+appstreamcli validatepass. The installed icon is still the placeholderlogo.svgpending the icon pass. -
VERSION→1.0.0(2026-07-05). Phases 4.5 and 4.6 all shipped, so the milestone is earned; the spec is fully built.
Colophon reframed from "ship the spec" to "a cool, useful, evolving
reading-stats companion." Four features greenlit, sequenced; every new
widget's metric still lands in spec.md first. 1.0 waits until these are
in.
- Honest per-book progress (v0.7.0). The progress bar was
interval-union coverage shown as a left-anchored fraction, so a book
read partly outside KOReader (Brandon's own case: jailbroke a Kindle
mid-read, KOReader logged only ~29 %→100 %) looked half-done though
it was finished. Now a positional span bar (
charts/span_bar.rs,stats::progress, corecoverage_spans+furthest_position) draws where reading was logged with a furthest-position marker, plus a Finished marker at furthest ≥ 0.98. The.sdrdeclared-finished flag will override the inference once sidecars are in scope. - Themes (v0.8.0). Eight palettes (Kanagawa Dragon/Wave/Lotus,
Gruvbox Dark/Light, Nord, Rosé Pine, Solarized Light) plus a
Follow-system mode. One
Themedrives both the generated adwaita CSS and the chart colours; a Preferences dialog (Ctrl+comma) switches live. The two static CSS sheets are gone; new GSettingsthemekey. - Mine today's data for new cards. No new deps.
- [x] Reader-profile (v0.9.0): "Reading personality" on the overview,
three synthesised traits (chronotype, session style, weekly
rhythm) classified from the existing hourly/session/weekday data.
(v0.16.0: session style now classifies on a time-weighted typical
session, not the plain median, so a pile of tiny device-tinkering
sessions no longer mislabels a steady reader a "Sipper".)
- [x] Series aggregation (v0.10.0): overview "Series" section grouping
books by the
seriesfield ("Name #index" parsed, "N/A" skipped, files of one work deduped), with finished counts. - [x] Re-read detection (v0.10.0): "Pages revisited" per-book stat frompage_totals(reads > 1). - [ ] Language breakdown deferred: Brandon's library is single-language ("en"), so it would render dull; revisit if that changes. - Completions / year timeline (v0.18.0). A "Finished books" section
on the overview: each finished work by finish date, most recent first,
with per-book time. Finish date is a detected read-through's end, else
the last reading day (so a sidecar-only finish still places). Grows
into a year-over-year record as Brandon finishes more.
stats::finished_timeline. -
.sdrfinished-flag reconciliation (v0.15.0). Brandon chose the sandboxedmluaroute over a stdlib parser.colophon-core::sidecarparses eachmetadata.*.luain a locked-down Lua VM (StdLib::NONE, text-only, UTF-8-lossy) and joinspartial_md5_checksumtobook.md5; the declaredsummary.statusbecomes authoritative through the singleLibraryEntry::is_finishedused by every finished count and marker. The book page shows the device's status. Verified by a round-trip test that reconciles the real Royal Assassin sidecar (status="complete") against the live stats DB. Reading theannotationsarray (highlight content, annotation markers) is the remaining sidecar work, now that the parser and dep are in place. - v0.17.0 rework: discovery changed from a device-folder scan (the original "KOReader library folder" setting, now removed) to per-book, user-provided files, matching how the stats DB is imported. Each book's page has an "Add file" action; the sidecar is verified against the book's md5, copied into an app-owned cache (<data>/sidecars/<md5>.lua), and looked up per book on load. General principle: Colophon never reads the device; any stat needing a file the user has not provided stays hidden until they add it.
Data-provisioning errands: the .sdr sidecar sample and a fresh
statistics.sqlite3 were pulled 2026-07-05 (750 events). Standing errand,
not urgent: re-import statistics.sqlite3 after finishing each book so the
finished-books features (completions timeline, year rollups,
estimate-accuracy) grow richer over time.
The Tome dossier surfaced a cluster of cards that are feasible from the
statistics.sqlite3 Colophon already reads: no new deps, no library-file
access, no sidecars. They fit the "make it a joy" reframing (more useful
cards from data already in hand) and slot ahead of 1.0. Each metric's
normative definition lands in spec.md first, per the standing rule; the
pure aggregation goes in colophon-core/stats.rs, the widget renders
third. Ordered cheapest-first.
- Author affinity (v0.11.0). Reading time and finished-book count
rolled up per author (top 10), an overview "Authors" card ranked by
time. Whole-library like Series; files of one work count once.
stats::author_breakdown. - Personal records (v0.12.0). Longest single session, biggest
reading day by time, most pages in a day, each with its date, as an
all-time "Records" card. Whole-history (does not move with the
window).
stats::personal_records. - Forgotten books (v0.12.0). A "Set aside" rail of books with
logged reading whose furthest read is not "finished" and whose last
reading-day is > 30 days ago, most-neglected first. Files of one work
collapse; reading either copy recently keeps the work off the list.
stats::forgotten_books. - Period-over-period delta (v0.13.0). The total-time tile shows the
current window against the immediately preceding equal-length window
(% change with an up/down/flat arrow), returning nothing rather than a
fake ∞% when the prior window is empty.
stats::PeriodDelta. - Recap card (v0.13.0). A whole-history composite (books finished,
total time, longest streak, sessions, most-active month) assembled from
numbers the overview already computes. Always all-time, so it stays put
when a shorter window is selected; Tome's top-genre facet dropped
(needs catalogue metadata Colophon lacks).
stats::Recap. (Renamed from "year-in-review": the data is single-year for now, so a whole- history recap is the honest framing; a per-year variant can come with the completions timeline once the data spans years.) - Per-book finish estimate + reading momentum (v0.14.0). The
existing KOReader-parity time-left/finish estimate now carries a
high/medium/low confidence from the number of reading days behind it,
and the book page gains a momentum read (last 7 days vs the prior 7:
picking up / slowing down / holding steady), shown only while the book
is currently active.
stats::reading_momentum,BookDetail. - Completion / abandonment rate (v0.14.0). A completion figure on
the Recap card: finished works over started works (both distinct by
title). "Finished" is the inferred furthest-position heuristic, not
user-declared, until §4.5 sidecar reconciliation lands.
stats::Recap::completion_rate. - HHI Variety trait for the reading-personality card (v0.11.0). A
fourth synthesised trait (focused ↔ eclectic) from an author-diversity
Herfindahl index (
1 − HHIover read time), shown once the library holds three or more distinct authors. Whole-library (author identity does not window). The book-type half stays blocked on catalogue metadata.stats::variety_trait.
Ordered roughly by likelihood. None are commitments.
Shipped from this list's spirit (new stats-DB-only overview charts, no new deps, on-contract):
- Speed by hour of day (v2.1.0, 2026-07-16): pages/hour by clock
hour, a 24-bar companion to the speed trend, windowed.
speed_by_hourincolophon-core::metrics::speed; overview sub-section under Reading speed. - Cumulative reading curve (v2.1.0, 2026-07-16): whole-history
running total of reading time, one point per active day, an odometer
line.
stats::cumulative_timeonOverviewBase; "Reading over time" overview section.
Still open:
- Word-count axis (the big one; needs a scope decision). Tome's
largest capability delta (
RESEARCH.md§5.5): true words-per-minute (pagination-independent, unlike Colophon's pages/hour), lifetime words-read, and a book-length distribution, and it unlocks two more reading-personality axes (Length, Pace). Off Colophon's stats-DB-only contract: word counts come from the EPUB files, not KOReader, so this means (a) reaching the library files at all, and (b) an EPUB word counter: either a new dep (ebooklib) or a stdlibzipfile+ regex path (Tome falls back to exactly that). Both are deliberate go/no-go calls, not slip-ins: it changes what Colophon reads. High value if the answer is yes. (DECIDED 2026-09-12 (Brandon): GO, with a new scoped zip/epub dependency for the counter; reads stay explicit-path, so the never-discovers rule holds and the library-files grant rides this decision.) - Reading goals ({books|minutes|pages} per {day|week|month|year}, with a prorated on-pace line), stats-DB-feasible. Deliberately declined at research time (Colophon is a stats viewer, not a tracker); listed only so the decision is on the record. Re-raise on request.
-
.sdrhighlight browser: browsing parsed highlight/note content in the UI. (Re-scoped 2026-09-05: most of this box shipped and the old text understated it. Themluadependency ask cleared (vendored,Cargo.toml), the sandboxed sidecar parser ships (colophon-core/src/sidecar.rs), the declared-finished reconciliation shipped v0.15.0, per-book sidecar attach v0.17.0, and annotation markers v0.19.0. What remains is the browser itself.) (Shipped v2.4.0, 2026-09-06: spec "Annotation browser" defined the surface first;sidecar::Annotationnow carries the page, excerpt, and note it was already reading for classification,stats::annotations_in_book_orderfixes the ordering, and the book page lists entries (kind, rescaled position, wrapped excerpt, dim note) hidden without a provided sidecar. Brandon's hands-on confirmation with the real sample is roadmap 629.) - Vocabulary-builder widgets ("words looked up per book", lookup
timeline). Schema already documented (RESEARCH §2); Brandon's
vocabulary_builder.sqlite3is empty, so parked until the feature sees real use. - "Day starts at HH:MM" shift for night owls (KOReader and KoShelf
both offer one; Colophon's day bucketing is already
timezone-generic, so this is a small
TimeConfig-style addition). (Shipped v2.3.0, 2026-09-06: spec "Day" amended first to define the logical reading day; coreDayStart+logical_dateplumbed throughdaily_totals,hourly_profile's weekday,speed_series, and every app-side date derivation; GSettingsday-start-minutes+ a Preferences HH:MM spin row that applies live. Hour-of-day attribution stays real clock.) - Multi-device merge, only if a second KOReader device ever exists:
KoInsight's
(md5, device, page, start_time)upsert is the reference design; same-device re-imports are naturally idempotent thanks to the schema'sUNIQUE (id_book, page, start_time). - Reference pages (manual canonical page count per book, KoInsight's feature): only if cross-layout comparison starts to matter beyond what interval-union progress already absorbs.
- Hardcover cross-reference:
hardcoversync_settings.luaon the device implies a Hardcover plugin is in use; a read-only "also on Hardcover" link-out is the most this should ever be (local-first rule).
Brandon's ask: stop hand-refreshing after every reading stretch. Colophon
auto-imports whenever the data it was already given becomes reachable.
Normative definition in spec.md ("Device auto-pull"); the read-access
principle is restated there and in CLAUDE.md. Independent of Phase 6
sequencing (it landed first).
- Sidecar origins remembered at attach time (
<md5>.originbeside the cached copy) and re-copied on auto-pull after the same md5 verification as attach; failures skip silently, the cache keeps its last good copy (autopull.rs, std-only, unit-tested). - Auto-pull on startup when the remembered source path is readable (after the canonical snapshot paints, so launch stays instant).
- Auto-pull on mount:
gio::UnixMountMonitorwatches the kernel mount table; an absent→present transition of the source path triggers the existing staging → validate → promote import. No polling, no gvfs dependency, no new crate.
Brandon moved his desktop from GNOME Shell to Hyprland (a Wayland tiling compositor) in 2026-07. The first cut of this phase (2026-07-08) kept libadwaita and scoped everything to additive polish. Brandon superseded that on 2026-07-09: the goal is now an app that fully belongs on Hyprland, which means dropping libadwaita, not GTK4. GTK4 stays (it is Wayland-native and the cairo chart layer lives on it); libadwaita (the GNOME stylesheet, the adaptive widgets, the GNOME design language) goes, replaced by plain GTK4 widgets and a stylesheet Colophon owns outright.
Colophon is the portfolio pilot for this move: it is the smallest shipped GTK app in the workspace, and the patterns proven here (widget replacements, the generated owned stylesheet, portal-based dark/light) become the template for Atrium, Conservatory, Viaduct, and Framework, each of which carries its own de-adwaita phase in its own roadmap, gated on this one landing.
Guardrails, restated because "never break userspace" binds hardest here:
- No feature regressions. Every surface (import, refresh, junk filter, themes, preferences, sidecar attach, every chart and card) works the same after the migration as before it.
- The app keeps running fine under GNOME; plain GTK4 does. "Hyprland-native design" means the look stops being GNOME's, not that the app stops working elsewhere.
colophon-coreand the cairo chart internals are untouched; they only brush adwaita as a trivialadw::Binparent.- The read-only contract is untouched.
- Design decisions land in
spec.mdbefore code, per the standing rule.
- Decoration posture. Decided 2026-07-09 (spec "Design language"):
slim flat toolbar, no window buttons. A thin flat bar keeps the
title and the Import / Refresh / primary-menu buttons over a 1px
rule; closing is
Ctrl+Qplus the compositor's own binds on either desktop. - The look itself. Flat, square, hard 1px borders, no shadows,
denser spacing; the eight palettes unchanged. The reference sheet
shipped as a
COLOPHON_FLAT-gated override block intheme.rs(square corners, 1px card borders, flat thin toolbar, window controls hidden) and Brandon approved the direction live on Hyprland 2026-07-09. Known spike gaps (toast still a pill, spacing not yet densified) are absorbed by the real owned sheet in 6c; the spike is deleted when 6c lands. - Layout. Decided 2026-07-09 (spec "Design language"): plain
GtkPanedwith a manual sidebar toggle (F9), paned position persisted in GSettings, no auto-collapse; the app never reshuffles its own layout on resize. - Follow-system dark/light without
adw::StyleManager. Decided 2026-07-09 (spec "Design language"): keep Follow-system by readingorg.freedesktop.portal.Settingsdirectly over D-Bus via gio (already a dependency through GTK, zero new crates); degrades to the dark default with no portal backend, never a failure.xdg-desktop-portal-hyprlandor-gtkis the documented runtime dependency for a non-GNOME session.
The full adwaita surface, inventoried 2026-07-09: 17 adw:: types across
roughly 3,000 lines of UI layer (ui/*.rs, ui/*.ui, theme.rs). No
replacement below needs a new dependency.
All shipped 2026-07-10 (v2.0.0), in commit-sized steps that stayed green.
-
Application/ApplicationWindow→gtk::Application/gtk::ApplicationWindow(the final toolkit-cut commit: template parent, the adwcontentproperty renamed tochild, and the headerbar promoted to a real titlebar, which AdwApplicationWindow forbade). -
NavigationSplitView/NavigationPage/Breakpoint→GtkPaned, per the 6a layout decision. Newsidebar-widthGSettings key, saved on close (not per notify::position, which fires every pixel of a drag); F9win.toggle-sidebaraction, also in the primary menu. -
ToolbarView/HeaderBar→ one flatGtkHeaderBar(show-title-buttonsoff) whose title label follows the content pane; the window title follows too, so compositor bars stay useful. -
Clamp→ ownedui/clamp.rs. The width-capped-box idea didn't survive contact: GTK CSS has no max-width, so it's a smallgtk::Widgetsubclass with a measure override (caps natural width, answers height-for-width at the clamped width) and a centering allocate. The tightening-threshold easing was deliberately dropped. -
StatusPage(empty states) → inline title + description composites (no icon was in use, so no new widget type). -
Banner(schema-version warning) →GtkRevealer+ styled label. -
Toast/ToastOverlay→GtkOverlay+ auto-hiding revealer, newest-wins with the pending hide cancelled on re-show (auto-pull emits two back-to-back; the import result must survive). -
Bin(page-widget parents) →gtk::Widgetsubclass withBinLayout+ dispose unparenting. (The charts never were adw::Bin; their only tie wasStyleManager::connect_dark_notify, replaced by a weak-ref redraw registry intheme.rsthat also fixes a listener leak: every chart used to add a permanent closure to the singleton.) -
ActionRow/ComboRow/PreferencesDialog/PreferencesPage/PreferencesGroup→ ownedui/rows.rs(row/value_row, shared by both pages) and a plaingtk::Windowpreferences surface with aGtkDropDown; Escape closes it via an explicit key controller. -
AboutDialog→gtk::AboutDialog(a toplevel rather than adwaita's in-window sheet). -
StyleManager/ColorScheme→ the 6a portal decision:org.freedesktop.portal.SettingsReadOne (with the deprecated double-wrapped Read as fallback) + a SettingChanged subscription, dark default when no portal answers; fixed themes force polarity viagtk-application-prefer-dark-themeso stock-widget internals follow.
Shipped 2026-07-10 (v2.0.0).
- The owned sheet:
theme.rsemits a:rootblock of owned--c-*custom properties per palette (GTK 4.16, whyv4_16is pinned) plus a palette-independent structural sheet implementing the 6a look, including the GTK-default gaps (menu popovers/modelbuttons, tooltips, scrollbars, text selection, a visible focus outline). TheCOLOPHON_FLATspike is deleted. Nofont-familyanywhere, enforced by a unit test. - Adwaita style classes: kept the class names (zero
.uichurn) but owned their definitions;pilldeleted (contradicts square). - Chart parity holds by construction (same
Themefeeds vars and cairo). Discovery that matters portfolio-wide: a global~/.config/gtk-4.0/gtk.cssskin loads at USER priority (800) and outranks APPLICATION (600), silently half-overriding in-app themes on themed systems, invisible when both are Kanagawa Dragon. The provider now registers just above USER; the sibling apps must do the same when they take this template.
Closed 2026-07-10 (v2.0.0); both "evaluate" items resolved with a no.
- Flatpak runtime: evaluated, staying on the GNOME runtime. GTK4 ships in the GNOME runtime and does not ship in org.freedesktop.Platform, so moving would mean building and maintaining GTK4 as manifest modules for the sake of a name. Revisit only if a gtk4 freedesktop BaseApp/extension appears. Metainfo reworded (no more libadwaita) and the 2.0.0 release entry added; the metainfo carries no screenshots, so nothing to retake there.
- Filesystem grant: evaluated, keeping
--filesystem=host:ro. Refresh and device auto-pull re-read the rememberedsource-path(and sidecar origins) directly across app restarts; portal FileDialog grants do not persist for that, so dropping the grant breaks both features in the Flatpak. Narrowing to/run/media;/media;/mntwas considered and rejected: the source is legitimately arbitrary (a synced folder,~/backups/...). Read-only matches the contract. - Meson,
.desktop, metainfo, and app-id unchanged;APP_IDstill matches the.desktopbasename and the Flatpakapp-idon both build paths, so Hyprlandwindowrulev2matching stays stable. CI droppedlibadwaita-devel(the Fedora container stays for GTK >= 4.16).
Written against the keep-adwaita frame but toolkit-agnostic; they ran after the migration as its verification pass (2026-07-10, v2.0.0).
- Tiling geometry audit at ~480px content width. The tile FlowBox
reflows cleanly to two columns and cards stay intact (verified at a
455px sidebar forcing a ~480px content pane). The two heatmap
scrollers now set
overlay-scrolling=false, so their scrollbar is a steady gutter rather than a hover-only fading overlay. The remaining eyeball items (heatmap tooltips under clipping, book-page strip reflow on a true quarter tile) are in the hands-on pass below. - Width-adaptive label thinning.
BarChart::drawnow thins the label row against its live allocation (stride from the widest label's measured width per slot), and the session-starts prep passes all 24 hour labels instead of pre-hiding five in six; a wide window now labels every hour, a narrow tile labels every few. Weekday and monthly bars get the same behaviour for free; the hour-heatmap headers are fixed-pitch cells and never crowd, so they needed nothing. - Minimum-height audit. Fixed chart heights are unchanged and the overview's list rows remain focusable, so PageDown/arrows scroll the outer scroller once anything has focus; tiles leaving the Tab order (below) makes that focus land on useful widgets sooner.
- Fractional-scaling hairline check. Not reproducible on current hardware: the only display runs scale 1.00, where 1px strokes sit on the pixel grid by construction. Recorded as audited-not-applicable; re-open if a fractionally scaled display ever joins the setup rather than shipping unverifiable snapping code now.
- Keyboard-first pass. A hand-built shortcuts window (owned rows in
a modal,
gtk::ShortcutsWindowbeing deprecated) onCtrl+question/F1and in the primary menu;Escapein the main window returns to the library (reshowing the sidebar if hidden and focusing it), and Escape closes the Preferences and Shortcuts windows via a sharedclose_on_escapehelper; the overview's tile FlowBox children arefocusable=falseso Tab skips the read-only tiles and reaches lists and controls. - Font guardrail. Charts stay on cairo's generic
sans-serif, and the owned sheet contains nofont-family, enforced by a unit test since 6c. - Hands-on confirmation pass (Brandon, keyboard in hand): real keypresses for F9 / Escape / Ctrl+question, heatmap tooltips and the book page on a genuine quarter tile, a theme live-flip from Preferences, a sidecar attach, and a GNOME-session sanity check. Everything scriptable was verified live during the migration (D-Bus-driven actions, screenshots across three palettes, single-instance re-summon, refresh toast).
- Keyboard sidebar resize (optional, not scheduled). The shortcuts window advertised "F8, then arrows" but no accelerator ever existed, and GTK 4.22's paned handle is not Tab-reachable (verified live 2026-09-15: a full Tab cycle never focuses the divider). The false row is dropped; actually wiring keyboard resize means an action that focuses the paned handle, verified by the hands-on pass.
- Accent focus-flash on a bare modifier press (found via Conservatory,
2026-07-12).
theme.rs's*:focus-visible { outline: 1px solid var(--c-accent) }is universal: pressing a bare modifier (e.g. a tiling-WM workspace-switch chord like Fn+Win / Ctrl+Super) flips GTK into keyboard-focus-visible mode, and the*selector then outlines every widget in the focus chain at once, flashing the accent across the window before it fades. It does not show in a screenshot (grabbing one changes input state and clears it), which makes it slippery to spot. Under an animated system GTK theme the flash is more pronounced. Fix (as shipped in Conservatory v0.3.7): scope the focus ring to the discrete interactive controls (button:focus-visible, entry:focus-visible, switch:focus-visible, scale:focus-visible, …) and drop the universal*; list/grid position is already shown by the selection background. Fixed in v2.0.1 (2026-07-16), porting Conservatory's scoped rule verbatim.
Separate from 6d's closed "stay on GNOME" verdict, which still stands: GTK4
ships in the GNOME runtime and not in org.freedesktop.Platform, so leaving
is not on the table. This is a version bump inside the GNOME runtime.
-
Bump
org.virinvictus.Colophon.jsonfrom GNOME 49 to GNOME 50. (Done 2026-07-23.) Colophon was the only Flatpak in the portfolio still on 49; Atrium, Hermitage, and Framework all target 50, so 49 existed on the build machine solely for this manifest. Found during a 2026-07-22 disk audit:org.gnome.Sdk 49was 2.3 GB of otherwise-dead weight, and it was pinned, soflatpak uninstall --unusedwould not touch it while the manifest asked for it. GNOME 50 ships GTK 4.20 against 49's 4.18 and the app requires only 4.16, so no API work was needed.org.gnome.Platform 49stays installed regardless (MissionCenter is the only thing still using it), so the reclaim was the SDK alone, and it has been taken.CLAUDE.mdandREADME.mdpackaging lines updated in the same commit.**⚠ The Flatpak build was NOT verified, by decision.** The check this item asked for turned out to be impossible as written: the manifest needs `org.freedesktop.Sdk.Extension.rust-stable`, and **no branch of it is installed** (not `25.08` for GNOME 50, and not `24.08` for the old 49 either). So the Flatpak has not been locally buildable for some time, independent of this bump; the item's premise that a passing build existed to re-run was wrong. Pulling `rust-stable//25.08` costs 556 MB down / 2.0 GB installed, which would have consumed almost the entire 2.3 GB this bump exists to reclaim, so it was skipped deliberately. What *was* verified: the manifest is valid JSON, `org.gnome.Platform` and `org.gnome.Sdk` 50 are both present, the GTK `v4_16` feature is well under 50's 4.20, and `cargo build --release` is green. -
Verify the Flatpak build. (Done 2026-07-23.)
org.freedesktop.Sdk.Extension.rust-stable//25.08installed (556 MB down / 2.0 GB on disk), thenflatpak-builderrun against the GNOME 50 manifest: build succeeded and the built binary runs under the runtime (flatpak-builder --run ... colophon --helpexits 0). The bump is now verified, not merely reasoned about.**One manifest bug found and fixed in the process:** the build had never worked, independently of the runtime version. `flatpak-builder` sandboxes builds without network, and this manifest carries no vendored cargo sources, so cargo could not resolve `index.crates.io` and the build died at the first dependency. Added `build-args: --share=network` to `build-options`, matching what Atrium already does and documents. **This is a local-developer convenience, not Flathub-ready:** Flathub forbids network access during build, so shipping there still needs vendored cargo sources generated by `flatpak-cargo-generator.py`. Atrium tracks that same gap as its own Phase 9 task; Colophon needs it before any Flathub submission. -
Vendor the cargo sources for an offline Flathub build. Generate
cargo-sources.jsonvia flatpak-builder-tools'flatpak-cargo-generator.pyand drop the--share=networkbuild arg. Shared shape with Atrium's Phase 9 task and with Viaduct, which has the same manifest gap. Only blocks Flathub submission; local builds work. *(Done 2026-09-06 as v2.3.1: upstream generator vendored atscripts/flatpak-cargo-generator.py, 127 crates fromCargo.lockintogenerated-sources.json, manifest includes it in the module sources,--share=networkdeleted. Verified for real: a localflatpak-builderrun against the GNOME 50 manifest succeeded and the built binary runs (--run ... colophon --helpexits 0); the build sandbox has no network, so cargo demonstrably resolved everything from the vendored sources. App-id migrates to io.github.virinvictus.Colophon per the 2026-09-12 workspace decision (Brandon); the rename and the Flathub prep ride the next Colophon lane.
(All three closed in v2.2.0; the text below is the 2026-08-09 record,
kept for what moved and why. The pushed v2.2.0 tag itself keeps its
recorded message, quirks included (single-# heading, the stray
"pyright strict" tail), as history: no force-push, decided 2026-09-12.)
Three latent correctness bugs found in the 2026-08-09 full-repo sweep and
deliberately not fixed in that pass. All three are latent rather than
live: none of them is reachable on the current sample database
(research/samples/statistics.sqlite3, 9 books, 1,075 events), which was
checked directly for every trigger condition below and has zero md5-merged
books, zero NULL/zero pages, and zero empty titles.
They are recorded here rather than fixed because D1 and D2 change numbers
in the KOReader-parity path, and the standing rule is that a metric's
definition lands in spec.md before the code moves. Each needs that spec
amendment as its first step. The live bugs from the same sweep were fixed
and are in patchnotes.md.
-
D1 — Merged books conflate two page axes.
StatsDb::books(Fixed v2.2.0: canonical-axis rescale in SQL: the canonical page count is bound as a parameter and the per-row book JOIN dropped. Fixture: one md5, two rows, pages 300/350; the old row's page 30 fans to canonical pages 34-35, no phantom 30.) (colophon-core/src/db.rs:210,merge_by_md5) merges KOReaderbookrows sharing an md5, keeping every underlying row id inBook::all_ids.page_totals(db.rs:143) then queries thepage_statview withid_book IN (…) GROUP BY page, andrescaled_events(db.rs:165) loops the same ids and concatenates. The trigger: the view rescales every row against its ownbook.id'spagescolumn, viaJOIN book ON book.id = id_book(RESEARCH.md§1). So when two merged rows recorded different page counts (ordinary after a font-size or margin change between metadata edits),GROUP BY pagesums positions from two different pagination axes into one bucket, while the canonicalBook::pagesis only the most-recently-opened row's. Wrong output:capped_secsandview_pages(colophon/src/loader.rs, the "as shown on device" totals),revisited_pagesinstats.rs(two unrelated pages sharing a number read as one page read twice), and the per-page activity strip. Not affected, so don't widen the fix:coverage,coverage_spans,furthest_position,completions,daily_totals, andspeed_seriesall read rawpage_stat_datathroughdb.events()and normalise each event against its own recordedtotal_pages, so they are immune by construction. Fix shape: rescale per row id against that id's ownpages, then map onto the canonical axis before aggregating, i.e. do the per-axis reduction in Rust rather than letting one SQLGROUP BYflatten both. Cheaper alternative worth costing first: restrict the parity path to the canonical id and treat the older rows' events as contributing time but not page positions. Verify with: a fixture with one md5 and twobookrows whosepagesdiffer (say 300 and 350), asserting the strip andrevisited_pagesdo not merge the two axes. The synthetic-fixture builder incolophon-core/tests/common/mod.rsalready emits the verbatim KOReader DDL, so the fixture is a few rows, not a new harness. -
D2 — A NULL
pagessilently reads as zero and zeroes a book's (Fixed v2.2.0:Book::pagesisOption<i64>; an unknown count hides unique pages, rescaled positions, completions, and page-derived totals, while time-derived stats keep rendering. Spec amended first.) stats. KOReader'sbooktable declarespages integerwith noNOT NULL(RESEARCH.md§1; unlikepage_stat_data'spage/duration/total_pages, which are allNOT NULL DEFAULT 0).db.rs:89reads it asOption<i64>andunwrap_or(0), and nothing downstream distinguishes "0 pages" from "page count unknown". Wrong output, three ways:unique_pages_read(colophon-core/src/metrics/progress.rs:81) multiplies coverage bypages, so it returns 0 for a fully-read book;rescaled_last_page(progress.rs:92) returns 1 for every event regardless of how far the book was read; and on the SQL side the NULL propagates through the view'sJOIN book, making theidx <= (last_page - first_page + 1)predicate NULL, so the view emits no rows at all for that book andpage_totalsreturns an emptyVec. The result iscapped_secs = 0beside adb.events()history showing hours of real reading, with no error anywhere. Fix shape: carry the unknown through the model (pages: Option<i64>, or a sentinel with one accessor) and let the existing "a stat that needs a file the user has not provided stays hidden" principle (spec.md, restated inCLAUDE.md) cover it: hide the page-derived stats for such a book rather than printing a confident zero. The time-derived stats stay valid and should keep rendering. Note the existing half-guard:loader.rs's round-trip test assertsunique_pages <= book.pages.max(1), which shows the0case was known but only its downstream invariant was defended, not the number itself. -
D3 — Three library-wide aggregations dedup on title alone. (Fixed v2.2.0: all three key on
library::group_key's(title, authors); the title-keyed sites inside series/author buckets are correct and untouched, with the Jingo collapse asserted.)colophon/src/library.rs:68defines a work as(title, authors)andgrouped()uses it;spec.md's "Book identity" says the same. Three places instats.rsdo not follow it and key on the trimmed title only:forgotten_books(stats.rs:343),finished_timeline(stats.rs:410), and the Recap'sfinished_works/started_workssets (stats.rs:510and:515). The trigger: two different works sharing a title. Most reachable via untitled books:db.rs:85turns a NULL title into"", so every book whose metadata never resolved collides under one empty key. Wrong output:finished_timelinereplaces rather than merges (.and_modify(|f| if finish_date > f.finish_date …)), so the earlier book's finish silently vanishes from the timeline along with its time and author;forgotten_booksORs the finished flag across colliding titles and then filters finished works out, so a genuinely abandoned book disappears from "Set aside"; and the Recap's two sets undercount distinct works, skewingRecap::completion_rate. Fix shape: small and mechanical: key all three onlibrary::group_key's(title, authors)instead of the title. Makegroup_keypub(crate)and reuse it rather than writing a fourth copy of the tuple. Check while doing it:stats.rs:108and:169also key on title, but those sit inside a series or author bucket where the other half of the identity is already fixed, so they are correct as they stand and must not be "fixed" too. Verify with: two entries, same title, different authors, one finished and one abandoned, asserting both survivefinished_timeline/forgotten_booksand thatcompletion_ratereads 1 of 2 rather than 1 of 1. The Jingo pair in the real sample is the opposite case (one work, two files, one author) and must keep collapsing to one; worth an assertion so the fix doesn't overshoot.
- The v2.2.0 leave-recorded call lives only in a commit message: add one parenthetical to the Known-defects block ("the pushed v2.2.0 tag keeps its recorded message as history; no force-push, decided 2026-09-12") so the decision survives outside git log.
- CLAUDE.md staleness (sync in the word-count lane's first commit): CLAUDE.md:40-42 still says the word-count axis needs a decision (it was GO'd 2026-09-12, roadmap Phase 5); CLAUDE.md:88-89 still says Flathub is blocked on the app-id split (decided).
- Rename-lane riders: DBusActivatable=true with no D-Bus service file installed (drop the key or install the .service); scan_sidecars is public API unused by the app (#[cfg(test)] or delete); restore the blank line before ## Known defects.
- Blitz candidates: the word-count lane (spec amendment first; the scoped zip/epub dep regenerates generated-sources.json; explicit-path provision reuses the sidecar/remembered-origin pattern; deliverables: WPM, lifetime words, length distribution, Length+Pace axes); the app-id rename (~12 sites + the GSettings path reset note); the hands-on pass closes Phase 6e. (2026-09-15: word-count shipped as v2.6.0 in the final blitz; the rename train and the hands-on pass remain.)
- GitHub presentation (workspace batch): description truncated
mid-word at the 350 cap (replacement drafted); zero Releases -
gh release create v2.4.0 --notes-from-tag(tag messages carry full notes; repeat for v2.3.x/v2.2.0); topics swap (drop gtk4-no-libadwaita-since-v200/mit/rust-2024; add gtk/reading).
Final audit 2026-09-14 (THE FINAL AUDIT: NEW findings, one line each; full detail in audit-final/Colophon/FINAL-REPORT.md)
Eight lenses + slop-reader at v2.4.0 (f21dd78). Tally after dedup: 2 HIGH / 8 MEDIUM / ~30 LOW + 11 feature proposals. Cleanest repo of the audit so far on the code side: read-only DB contract verified exemplary, threading and chart math clean, nothing to remove, version-sync unanimous.
(EXECUTED 2026-09-15 in the final blitz: every box below shipped in v2.4.1 unless the note says otherwise.)
- [HIGH] Zero GitHub Releases despite four annotated tags; create all four with
--notes-from-tag, then add a release.yml onpush: tags: ['v*']so the state cannot recur. - [HIGH] Repo description truncated mid-word at the 350 cap; apply the drafted replacement.
- [MEDIUM] Panic guard: raw
start_timereachestimestamp_opt(...).expecton the GTK main thread (colophon-core/src/metrics/days.rs:20, :104, metrics/speed.rs:92); a corrupt/foreign .db (import accepts any *.db) panics the app. Bound timestamps once inload_snapshot. - [MEDIUM] Comment-hygiene commit: nine-site D1 drift family still describing the
page_statview as the data source (model.rs:77, :89; lib.rs:9; loader.rs:26, :39, :48; library.rs:24; stats.rs:6) plus the stale sidecars future tense (stats.rs:990). - [MEDIUM] CLAUDE.md sync commit: word-count GO framing (:40-42), vendoring contradiction (:44-46 vs :85-87), stale 6e remainder (:37-39), dead ~/.claude pointer (:3).
- [MEDIUM] Pin CI:
actions/checkout@v5,Swatinem/rust-cache@v2,dtolnay/rust-toolchain@stableto full SHAs; version-pinfedora:latest(ci.yml:19,31,34,38). - [MEDIUM] Known-defects block: add the v2.2.0 leave-recorded parenthetical and restore the missing blank line (roadmap.md:731-735).
- [MEDIUM] Staged-import gap: validate with full
load_snapshotbefore the rename, or addnumbersto the required-table check (loader.rs:117-141); a promoted-then-failed import clobbers the good snapshot. - [MEDIUM] Spec/doc truth fixes: junk threshold "configurable" is a fixed 300 s constant (spec.md:170); heatmap tooltip lacks the promised books count (spec.md:295); Tier A records/weekday catalogue items never shipped (spec.md:268); README default theme is Follow-system, not Dragon (README.md:81); stale sample-pending claims in spec.md:343 and RESEARCH.md:609/:758; Tier B numbering collides with Tier A (spec.md:295).
- [LOW] Main-thread
p.exists()on possibly-hung FUSE mounts at startup and on every mounts_changed (window.rs:242); completion rows ignore the configured day start (book_page.rs:258); Shortcuts window documents a nonexistent F8 accelerator (shortcuts.rs:13). - [LOW] Removal dispositions:
scan_sidecarsoff the public API;all_eventsremoved (fold live_sample onto flattened per-book events);rescaled_eventsKEPT, marked baseline-only (it is the D1/D2 oracle + perf comparison arm); dedupe the D1 SQL subquery shared by page_totals/rescaled_events. - [LOW] Housekeeping: MSRV
rust-versionkey; rust-toolchain.toml or a recorded no-pin decision; generator provenance header;generated-sources.json linguist-generatedin .gitattributes; CI badge; SECURITY.md; dependabot (actions-only, deliberately not cargo); optional justfile and cargo-deny gate (Brandon's call). - [LOW] Em-dash pass: RESEARCH.md 29, roadmap.md 14, spec.md 6, CLAUDE.md 3 live in prose; future defect IDs use "D1:" not "D1 —"; historical patchnotes and the pushed v2.2.0 tag stay untouched per the 2026-09-12 decision.
CONFIRMED-prior (all verified still present, none superseded; the 09-12 block above remains the authority): CLAUDE.md decision-gating staleness, v2.2.0 parenthetical, DBusActivatable=true with no service file, unused public scan_sidecars, missing Known-defects blank line, GitHub description/Releases/topics. Audit-side correction: the audit sheet's "113 test attrs" is stale; the tree has 120 attrs / 119 runnable, matching the repo's own docs.
Feature candidates (RE-RANKED or NEW, grounded in audit-final/Colophon/FINAL-REPORT.md lens 4): word-count lane (GO'd, rank 1) vs rename+Flathub train (decided, rank 5) vs the S-sized wins: rollup click-through navigation (rank 2), library search Ctrl+F (rank 3); then heatmap year pager, per-year recap, cross-book annotation browser, estimate-accuracy card; export and dragon-themer integration noted but NOT recommended.
Slop-reader: no AI-slop in substance anywhere (patchnotes among the best release prose in the workspace); the systemic finding is ~52 live em-dashes concentrated in RESEARCH.md and roadmap.md (see the LOW em-dash box above).
- Year heatmap pager + per-year recap (v2.5.0, GO'd). Arrows page the year calendar back through past years instead of the silent 52-week rolling window; the recap card gains a per-year variant.
- Cross-book annotation browser (v2.5.0, GO'd). An overview surface listing highlights, notes, and bookmarks across every provided sidecar, not only per book.
- Word-count axis (v2.6.0; GO'd 2026-09-12; lane order: fixes first, then this). Spec amendment first; the scoped zip/epub dependency regenerates generated-sources.json (provenance header goes on the generator then, not before); explicit-path provisioning on the sidecar/remembered-origin pattern; deliverables: true WPM, lifetime words, book-length distribution, Length + Pace personality axes.
- Declined, recorded so no future audit re-raises them: export PNG/CSV (grim covers the one known consumer); dragon-themer integration and custom palettes (protects the pilot's theme.rs); reading goals remain declined from research time.
- Recorded, not scheduled: estimate-accuracy card is the next Tier A candidate (after word-count); keyboard sidebar resize stays optional (6e box above); the app-id rename train and the S-sized wins (rollup click-through navigation, library search) keep their recorded places for their own sessions; Flathub submission follows the word-count lane, in a screenshot session with Brandon (the metainfo screenshot slots prep rides the lane); the Phase 6e hands-on pass stays Brandon's. GitHub social preview: composed from logo.svg, one manual upload left (Settings -> Social preview).