feat: render index via Mustache template with spine-index-model

This commit is contained in:
2026-06-21 07:38:32 -04:00
parent 67e41ad110
commit 7faf0cd8ac
7 changed files with 722 additions and 124 deletions
+155
View File
@@ -0,0 +1,155 @@
# 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.