6.3 KiB
Spine — design brief
A personal library and reading tracker. Tracks books I want to read, am reading, have read, and what I think about them; captures notes repeatedly through a read; and records who recommended a book and why.
This document is a starting point for a coding agent. Several decisions are deliberately left open (see "Open decisions"). Do not invent answers for those — scaffold around them or ask.
Core features
- Track each book through a reading lifecycle: want → reading → read (or abandoned).
- Capture multiple timestamped notes across a single read, not one review at the end.
- Record recommendations: who recommended a book and a short "why it's on my radar" note. This is context shown inline, not a search/analytics concern.
- Flag the format consumed: hardcover, ebook, or audiobook.
- Rate finished books.
- Browse "on deck" (want-to-read) and optionally filter by category.
Architecture
- Source of truth is a single Org file. No database.
- Emacs owns the file. All reads and writes go through a running Emacs via
emacsclient, soorg-elementdoes the parsing and serialization canonically. This avoids third-party Org parser fidelity problems on write. - No read/search index initially. Queries are simple status + tag filters
answered live by
org-ql. A derived SQLite index can be added later if the recommendation data ever needs real querying; it is explicitly out of scope now. - The web layer is a thin client over emacsclient. Its language and framework are undecided (see Open decisions).
Data model (Org)
Each book is a top-level headline whose TODO state is its lifecycle status.
#+TITLE: Spine
#+TODO: WANT(w) READING(r) | READ(d) ABANDONED(a)
#+STARTUP: logdrawer
* READING Use of Weapons
:PROPERTIES:
:AUTHOR: Iain M. Banks
:FORMAT: audiobook
:REC_BY: Priya
:REC_NOTE: If you liked Player of Games, this one will wreck you in a better way.
:RATING:
:ADDED: [2026-02-20]
:ID: 8c1e-uow
:END:
:LOGBOOK:
- State "READING" from "WANT" [2026-02-20]
:END:
- [2026-03-02] The two-track structure is doing something I can't name yet.
- [2026-03-07] Zakalwe's competence reads as a wound. Banks never lets a skill be free.
- [2026-03-09] The chapter numbering. Oh.
Field notes:
- Status: the TODO keyword.
WANT READING | READ ABANDONED. - Status history: comes free from the LOGBOOK drawer when
org-log-into-draweris enabled. Distinct from reading notes. - Reading notes: a plain Org list in the headline body, each item prefixed
with an inactive timestamp
[YYYY-MM-DD]. Appending a note is inserting one list item. Notes are not individually queried. - Format: single
:FORMAT:property —hardcover|ebook|audiobook. (See Open decisions for the multi-format case.) - Recommendation:
:REC_BY:plus optional:REC_NOTE:. Surfaced inline as context, never ranked or searched. - Category: Org tags on the headline (
:scifi:literary:), not a property — tags filter natively and inherit. - Rating:
:RATING:1–5, set when moved to READ.
emacsclient integration
Read: on-deck list
(require 'org-ql)
(require 'json)
(defun spine-ondeck (&optional tag)
"JSON of WANT books, optionally filtered by TAG."
(json-serialize
(vconcat
(org-ql-select "~/spine/books.org"
(if tag `(and (todo "WANT") (tags ,tag)) '(todo "WANT"))
:action
(lambda ()
(list :title (org-get-heading t t t t)
:author (org-entry-get nil "AUTHOR")
:format (org-entry-get nil "FORMAT")
:rec_by (org-entry-get nil "REC_BY")
:tags (vconcat (org-get-tags))))))))
(defun spine-ondeck-file (path &optional tag)
"Write `spine-ondeck' output to PATH (avoids emacsclient quoting)."
(with-temp-file path (insert (spine-ondeck tag))))
Invoke: emacsclient --eval '(spine-ondeck-file "/tmp/spine.json")', then read
the file from the web layer. (emacsclient --eval prints values with prin1, so
returning a JSON string directly comes back quoted/escaped — the temp-file hop
sidesteps that. A small simple-httpd endpoint is the alternative; undecided.)
Write: to implement
spine-add-note— append a[today] textlist item under a book's headline.spine-add-book— insert a new WANT headline with initial properties.spine-set-status/spine-set-rating/spine-set-format.
These set state through org-element / standard Org commands, not text munging.
Not yet written.
UI
Two directions are mocked as standalone HTML (spine-mockup-a-agenda.html,
spine-mockup-b-journal.html). They render the same Org record through different
front doors.
-
Direction A — agenda (TUI). An org-agenda-style surface grouped by status. Format shows as a leading glyph, the recommendation rides on the row as "why it's here," the selected book expands inline to show its reading log, and capture happens in a minibuffer rather than a form. This is the home surface.
-
Direction B — journal card. A book detail card. Format is a segmented control, the reading log is a thread you append to with an inline composer, and the recommendation has an explicit "who / why" capture form. This is the drill-in view.
Recommended composition: hybrid. Direction A as the home/browse surface, Direction B as the detail view when a book is selected. Agenda answers "what now," card answers "where am I with this one." Not locked in — building one alone is fine.
Open decisions
- Glue language + web framework — undecided. Possibly elisp end-to-end (Emacs serves the UI too), possibly an external thin client shelling out to emacsclient. Leave this unbound; don't hardwire a stack.
- Read transport: temp-file hop vs
simple-httpdendpoint in Emacs. - Per-note format: book-level
:FORMAT:is the default. If a book is regularly consumed across media (hardcover at home, audio on a commute), format may need to move to per-note. Single-value for now. - Hybrid vs single UI direction — see UI section.
- Search index: deferred. Not now.
Out of scope (for now)
- Recommender ranking / "hit rate" analytics.
- Full-text search across notes.
- Any external book-metadata API (covers, ISBN lookup) — placeholder cover blocks in the mockups are decoration only.