Files
spine/spine-spec.md
T

156 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:` 15, 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.