# 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`, so `org-element` does 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. ```org #+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-drawer` is 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 ```elisp (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] text` list 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-httpd` endpoint 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.