4
[2026-08-08] · src build/orrery.el
build/orrery.el is the entry point: five require calls wire the other five
files together (orrery-model, orrery-feed, orrery-ir, orrery-render-org,
orrery-render-html), and orrery-build chains model+feed to ir to two
renderers to dist/. Every stage in the pipeline is named exactly once, in
under forty lines.
The Lisp idea: require/provide is Elisp's entire module system, and it is
smaller than it looks. (provide 'orrery-ir) at the bottom of orrery-ir.el
does one thing — pushes the symbol orrery-ir onto the global features
list. (require 'orrery-ir) at the top of orrery.el checks whether that
symbol is already in features; if not, it searches load-path for a file
named orrery-ir.el (or .elc) and loads it, which runs its own provide
and satisfies the check. There is no namespacing and no export list — every
defun becomes globally visible the moment its file loads, which is exactly
why the orrery-ir-- double-dash-means-internal convention in this repo's
CLAUDE.md is the only thing marking a function private. Elisp itself does not
enforce it.
orrery-root (orrery.el:23-29) is the other half of "finding yourself": it
derives the repo root from load-file-name — the path Emacs is loading
this very file from — instead of a hardcoded path or an env var. That is
what lets (require 'orrery) produce a correct orrery-root identically from
a git checkout, a nix store path, or a CI runner's checkout, with zero
configuration.
Notice orrery-build-ir (orrery.el:38) writes nothing and returns a value,
while orrery-build (orrery.el:48) is the only function that touches disk.
That split is what a later lesson on determinism will unpack in full — it is
what lets make golden build an IR from test fixtures and diff it with no
dist/ involved at all. For now, just notice the seam exists before reading
past it.
Exercise (verifiable): from the repo root, run:
emacs -Q --batch -L build --eval "(progn (require 'orrery) (princ orrery-root))"Confirm it prints your checkout's absolute path, ending in a slash. Then copy
the checkout to a different directory and run the same command again with no
edit to orrery.el — the root changes to match. That is load-file-name
doing the work the docstring describes.
Review scheduled: first review of build-shape due 2026-08-09. No reviews
were due this run — data/state/srs.json did not exist before it, so there
is nothing yet to surface.
[2026-08-08] · src build/orrery-ir.el, build/orrery-render-html.el, build/orrery-feed.el
None of the six files under build/ require cl-lib. Every sequence
operation goes through seq.el instead: seq-take (orrery-ir.el:86),
seq-filter (orrery-ir.el:111), seq-doseq (orrery-ir.el:142,154),
seq-map-indexed (orrery-render-html.el:210), seq-empty-p
(orrery-render-org.el:54). seq.el's functions are generic over sequences —
the same seq-take works on a list or a vector, dispatching internally —
where the cl-lib equivalents (cl-subseq, cl-remove-if) mostly assume
lists and exist as a Common Lisp compatibility shim.
This codebase's JSON-shaping layer deliberately mixes lists and vectors (see
the first item above), so a sequence function that only worked on one shape
would force a type check at every call site. seq's genericity is what lets
orrery-ir.el pass a list here and a vector there without caring. Reach for
cl-lib only for what seq doesn't cover — cl-defstruct, cl-case,
&key arguments — and reach for pcase-let (orrery-feed.el:115,
destructuring (panel . items) off a cons) before cl-lib's
destructuring-bind. On sequences specifically, seq wins by default; this
repo's own zero cl-lib requires is the evidence.
[2026-08-08] · src build/orrery-model.el
Both are just lists of conses; the difference is what you do with the keys.
A plist ((:id "x" :weight 3)) suits a small fixed vocabulary you know at
write-time — plist-get/plist-put compare keys with eq, and keyword
symbols are interned, so that's a pointer check. An alist
(((key . val) ...)) suits keys you don't control — assoc compares with
equal by default, so arbitrary strings work, and duplicate keys shadow
instead of erroring.
orrery-model--node-properties (orrery-model.el:47-58) builds alists exactly
where that matters: an Org PROPERTIES drawer has keys Greg typed by hand, and
orrery-model--prop! looks them up with (assoc (upcase key) props)
(orrery-model.el:64,72). Everywhere else — panel plists, item plists, the IR
itself — the key set is fixed and known, so it's plists via plist-get. The
split isn't incidental either: as the JSON item above shows, json-serialize
wants plists for objects, so keeping downstream data as plists means
orrery-ir.el hands it over with zero conversion. Alist at the boundary where
keys are foreign text, plist everywhere the keys are your own vocabulary —
this repo holds that line consistently.
[2026-08-08] · src build/orrery-ir.el
Emacs's native json-serialize treats any Lisp list you hand it as a JSON
object, never an array: nil is ambiguous between "false", "null" and
"empty object", and a non-empty list gets read as alist/plist key-value
pairs. A vector is the one shape json-serialize always turns into a JSON
array regardless of contents. That's orrery-ir--strings (orrery-ir.el:51-55):
it wraps (or list []) in vconcat purely to flip list to vector, because an
item's :tags is a real Lisp list of strings, and handed over unchanged it
would be misread as an object.
orrery-ir--object (orrery-ir.el:40) builds its output as a flat plist and
hands that to json-serialize too — and that one is meant to become an
object. Same underlying Lisp type, two different JSON shapes, decided entirely
by which function built the list and whether it got vconcat'd. Miss that on a
new list-valued field and you get a corrupted digest, not an error.