aboutsummaryrefslogtreecommitdiff
path: root/docs/design
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design')
-rw-r--r--docs/design/2026-07-10-curated-help-menu-survey.org96
-rw-r--r--docs/design/2026-07-10-ledger-config-audit.org146
-rw-r--r--docs/design/2026-07-10-org-module-ownership.org138
3 files changed, 380 insertions, 0 deletions
diff --git a/docs/design/2026-07-10-curated-help-menu-survey.org b/docs/design/2026-07-10-curated-help-menu-survey.org
new file mode 100644
index 00000000..a7469ac1
--- /dev/null
+++ b/docs/design/2026-07-10-curated-help-menu-survey.org
@@ -0,0 +1,96 @@
+#+TITLE: Survey — where a curated "?" help menu makes sense
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-10
+
+* What this is
+
+The mechanical half of the "=?= = curated help menu" convention task. It reports,
+per mode, whether =?= is free, whether =H= is free, and what the package already
+ships. It changes no keybinding and makes no per-mode recommendation, because which
+commands belong in a curated menu is a taste call.
+
+The reference implementation is calibredb (=modules/calibredb-epub-config.el:129-131=):
+=?= runs =cj/calibredb-menu=, a curated transient of the frequent workflows, and =H=
+runs the package's own =calibredb-dispatch=.
+
+* Method
+
+Bindings were read from the live keymaps, not from source, because a package's
+=define-key= calls and its =:bind= forms don't always agree with what ends up in the
+map.
+
+The probe ran in a throwaway =emacs --batch= with =package-initialize=, not in the
+running daemon, for two reasons. Probing =calibredb-search-mode-map= or
+=mu4e-headers-mode-map= in the daemon would force-load those packages into a live
+session. And a batch Emacs reports the *package's* defaults rather than the defaults
+plus Craig's config, which is the question being asked: what does the package ship?
+
+=mu4e= is not an ELPA package. It installs with =mu= into
+=/usr/share/emacs/site-lisp/mu4e=, so it needed an explicit =load-path= entry.
+
+* The table
+
+=FREE= means the key is unbound in that mode's map.
+
+| Mode | =?= | =H= | Ships a dispatch? |
+|---------------+--------------------------------+---------------------------+------------------------|
+| calibredb | =cj/calibredb-menu= (curated) | =calibredb-dispatch= | yes, moved to =H= |
+|---------------+--------------------------------+---------------------------+------------------------|
+| nov | FREE | FREE | no |
+|---------------+--------------------------------+---------------------------+------------------------|
+| eat | FREE | FREE | no |
+|---------------+--------------------------------+---------------------------+------------------------|
+| elfeed-search | FREE | FREE | no |
+|---------------+--------------------------------+---------------------------+------------------------|
+| elfeed-show | FREE | FREE | no |
+|---------------+--------------------------------+---------------------------+------------------------|
+| dired/dirvish | =dired-summary= | =dired-do-hardlink= | no (summary is a |
+| | | | one-line echo) |
+|---------------+--------------------------------+---------------------------+------------------------|
+| magit-status | =magit-dispatch= | =magit-describe-section= | yes, already on =?= |
+|---------------+--------------------------------+---------------------------+------------------------|
+| pdf-view | =describe-mode= | =pdf-view-fit-height-...= | no |
+|---------------+--------------------------------+---------------------------+------------------------|
+| org-agenda | =org-agenda-show-the-flagging- | =org-agenda-holidays= | no |
+| | note= | | |
+|---------------+--------------------------------+---------------------------+------------------------|
+| mu4e-headers | =mu4e-headers-mark-for-unread= | =mu4e-display-manual= | no |
+|---------------+--------------------------------+---------------------------+------------------------|
+
+* What the table shows
+
+Four modes have both keys free: nov, eat, elfeed-search, elfeed-show. Nothing has to
+be displaced to adopt the convention there. These are the cheap ones.
+
+Three modes bind =?= to something small. =dired-summary= echoes a one-line key
+reminder. =pdf-view= sends =?= to =describe-mode=. =org-agenda= puts a
+flagging-note command on it. In each case the convention would displace a command of
+low value, but it is a displacement and needs a decision.
+
+=mu4e-headers= is the awkward one. Its =?= is =mu4e-headers-mark-for-unread=, a real
+editing command in the marking family (=u=, =d=, =!=). Rebinding it would break
+muscle memory for an operation that mutates mail state. Its =H= is already
+=mu4e-display-manual=.
+
+=magit= is the counterexample worth noting. It already does exactly what the
+convention proposes: =?= opens =magit-dispatch=, its own curated transient. The
+convention is not new, it is magit's habit, generalized. Magit needs no change.
+
+=dirvish= inherits =dired-mode-map=, so a change there lands on plain dired too.
+
+* Caveats
+
+The probe reads package defaults. Craig's own config may rebind =?= in a mode after
+the package loads, and the batch probe would not see it. A grep for =?=-rebinding in
+=modules/*.el= found only the calibredb case, so the table should hold, but a mode
+whose config binds =?= inside a =:config= block reached by a path the grep missed
+would not show up.
+
+=signel= and =pearl= are in the task's candidate list and are absent from the table.
+Neither was probeable: they are local checkouts, not ELPA packages, and loading them
+in batch pulls in dependencies the probe deliberately avoided.
+
+* The decisions this leaves
+
+For each mode, whether a curated =?= menu is worth its cost, and if so, which
+commands go in it. That is the taste call, and the reason this survey stops here.
diff --git a/docs/design/2026-07-10-ledger-config-audit.org b/docs/design/2026-07-10-ledger-config-audit.org
new file mode 100644
index 00000000..ff969d78
--- /dev/null
+++ b/docs/design/2026-07-10-ledger-config-audit.org
@@ -0,0 +1,146 @@
+#+TITLE: Ledger config audit — correctness and guardrail gaps
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-10
+
+* Scope
+
+The correctness half of the ledger guardrail work. It audits
+=modules/ledger-config.el= (71 lines, no test file before today) and reports what's
+wrong and what's missing. It designs no guardrails, because those choices are
+Craig's.
+
+Characterization tests for the behavior that exists now landed alongside this note
+at =tests/test-ledger-config.el=.
+
+The stakes set the bar. This is a financial file, and Craig's stated goal for the
+follow-on work is "enough guardrails that it's hard to make a costly mistake."
+
+* Findings
+
+** F1. The linting doesn't run. Nothing checks unbalanced transactions.
+
+FIXED 2026-07-10. =flycheck-config.el= now hooks =flycheck-mode= to =ledger-mode=.
+Verified end to end on an unbalanced fixture: flycheck reports "Transaction does not
+balance" with the =$10.00= remainder. The finding is kept below as the record of
+what was wrong.
+
+The module's own commentary says it provides "flycheck linting."
+
+=flycheck-ledger= is loaded (=ledger-config.el:59-60=) and registers the =ledger=
+checker. But =flycheck-mode= is never enabled in a ledger buffer.
+=flycheck-config.el:47= hooks =flycheck-mode= to exactly two modes:
+
+#+begin_src elisp
+:hook ((sh-mode emacs-lisp-mode) . flycheck-mode)
+#+end_src
+
+There is no =global-flycheck-mode= anywhere in the config. Verified against a real
+=ledger-mode= buffer in the running daemon: =major-mode= is =ledger-mode=,
+=flycheck-checkers= contains =ledger=, and =flycheck-mode= is =nil=.
+
+So an unbalanced transaction, a malformed date, and a typo'd account name all
+produce no warning. The guardrail Craig believes he has is not connected.
+
+This is the finding that matters. Everything below is smaller.
+
+** F2. Every save silently reorders the entire file.
+
+=cj/ledger--clean-before-save= runs =ledger-mode-clean-buffer= on
+=before-save-hook= (=ledger-config.el:32-40=), with =cj/ledger-clean-on-save=
+defaulting to =t=.
+
+=ledger-mode-clean-buffer= (=ledger-mode.el:226=) is not a formatter. Its own
+docstring says "Indent, remove multiple line feeds and sort the buffer," and its
+body calls, in order: =untabify=, =ledger-sort-buffer=, =ledger-post-align-postings=,
+=ledger-mode-remove-extra-lines=.
+
+=ledger-sort-buffer= (=ledger-sort.el:106=) sorts the whole buffer by date, from
+=point-min= to =point-max= unless the file carries explicit sort markers.
+
+So every =C-x C-s= rewrites the transaction order of a financial file. The task
+asked whether clean-on-save "ever reorders or rewrites in a surprising way." It
+reorders, every time, by design of the upstream function.
+
+*Accepted 2026-07-10, on inspection of the sort key.* The reordering is
+chronological and nothing else. =ledger-sort-startkey= (=ledger-sort.el:62=) builds
+its key from the first ten characters of a transaction's opening line, the ISO date,
+through =ledger-parse-iso-date= and =float-time=. Payee, amount, and account never
+enter the key. =sort-subr= uses Emacs's stable sort, so transactions sharing a date
+keep the order they were typed in.
+
+The scope is bounded on request: =ledger-sort-buffer= narrows to the region between
+=; Ledger-mode: Start sort= and =; Ledger-mode: End sort= marker comments when the
+file carries them, and only falls back to the whole buffer when it doesn't.
+
+So a save sorts by date, stably, over a region the file can choose. Craig accepts
+that. The finding stays recorded because "clean-on-save" still reads as whitespace
+tidying, and the next person to meet this function should know it sorts.
+
+** F3. The demoted error hides a partial rewrite, not just a message.
+
+The clean is wrapped in =with-demoted-errors= so "a malformed buffer still saves"
+(=ledger-config.el:34-37=).
+
+The comment is accurate about the save. It is misleading about the buffer. The
+operations inside =ledger-mode-clean-buffer= run in sequence and mutate as they go.
+An error raised by =ledger-post-align-postings= or by the =search-forward= that
+restores point (=ledger-mode.el:239=) happens *after* =ledger-sort-buffer= has
+already reordered everything. Nothing rolls back.
+
+The result is a file that saved, in a state neither the user nor the cleaner
+intended, with a message in the echo area that scrolls away.
+
+** F4. Reconcile has no confirmation, and clears whole transactions.
+
+=ledger-clear-whole-transactions= is =t= (=ledger-config.el:43=), so a reconcile
+marks entire transactions cleared rather than individual postings. Combined with no
+confirmation step anywhere in the module, a stray keypress in a reconcile buffer
+mutates the ledger file.
+
+This is upstream behavior, not a bug in this module. It is named here because the
+follow-on guardrail work asked about "reconcile safety" and this is what reconcile
+safety currently amounts to.
+
+* What is not a problem
+
+Worth recording so the guardrail work doesn't chase it.
+
+- *company-ledger's global backend is correctly scoped.* =ledger-config.el:68= does
+ =(add-to-list 'company-backends 'company-ledger)= globally, which looks like it
+ would offer ledger completions everywhere. It doesn't: the backend's own =prefix=
+ command (=company-ledger.el:110-111=) returns nil unless the buffer is
+ =beancount-mode= or derives from =ledger-mode=.
+- *The reports pass =--strict=.* All five entries in =ledger-reports=
+ (=ledger-config.el:48-52=) use =--strict=, so a report over a file with an
+ undeclared account errors rather than silently inventing one. This is a real
+ guardrail and it is on.
+- *The missing-binary check is at the right place.* =cj/executable-find-or-warn=
+ runs at =:config= (=ledger-config.el:54=), so a missing =ledger= CLI warns at load
+ rather than failing cryptically inside a report.
+- *The clean-on-save hook is installed buffer-locally* (=ledger-config.el:40=), not
+ globally. Pinned by a test.
+
+* Gaps, as a list
+
+1. +No linting reaches ledger buffers (F1).+ Fixed 2026-07-10.
+2. No confirmation before the date-sort on save (F2). Accepted: the sort is
+ chronological and stable.
+3. No rollback when that rewrite fails partway (F3).
+4. No confirmation before reconcile mutates the file (F4).
+5. No validation that a save leaves the file balanced.
+6. No test coverage before today.
+
+* What this note deliberately does not do
+
+It designs no guardrails. Whether a clean that fails should roll back, and what
+shape a reconcile confirmation takes, are preference calls about Craig's own
+accounting workflow. Those belong to the ledger guardrail UX task.
+
+The one finding that wasn't a preference call was F1: turning flycheck on in ledger
+buffers restored a guardrail the module already claimed to have. That was a defect,
+not a design choice, and it is fixed.
+
+F2 was resolved by reading the sort key rather than by changing code. What remains
+for the guardrail task is F3 (no rollback when a clean fails partway) and F4 (no
+confirmation before reconcile), plus the open question of whether a save should
+verify the file still balances.
diff --git a/docs/design/2026-07-10-org-module-ownership.org b/docs/design/2026-07-10-org-module-ownership.org
new file mode 100644
index 00000000..5717b66f
--- /dev/null
+++ b/docs/design/2026-07-10-org-module-ownership.org
@@ -0,0 +1,138 @@
+#+TITLE: Org module ownership and load boundaries
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-10
+
+* Why this note exists
+
+The Org workflow is spread across sixteen modules with overlapping
+responsibilities. The code works. What's missing is a map: which module owns which
+behavior, what each one adds to a shared variable, and in what order they load.
+
+Future load-order work needs that map. Four modules add capture templates, four
+touch =org-agenda-files=, and five register org-protocol handlers. None of that is
+wrong, but none of it is written down either, so the only way to answer "who sets
+this?" is to grep.
+
+This is a description of what is, not a proposal. Where the map turned up something
+that looks like a defect, it's flagged at the bottom rather than fixed here.
+
+* The modules
+
+Every Org module declares =Layer=, =Category=, and =Load shape= in its commentary
+header. The layer numbers below come from those headers, not from this note.
+
+| Module | Layer | Owns |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-faces-config.el= | 2 | Agenda keyword and priority faces, plus their -dim |
+| | | variants for unfocused windows |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-config.el= | 3 | Base org-mode settings, the org keymap, tag alignment |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-agenda-config.el= | 3 | Agenda views, task tracking, notifications; loads |
+| | | =org-agenda-config-debug= on demand |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-capture-config.el= | 3 | The base capture templates and their dispatch keys |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-refile-config.el= | 3 | Refile targets and the cached target list |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-roam-config.el= | 3 | Roam database, capture, and its agenda-file additions |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-contacts-config.el= | 3 | Contacts storage and its capture template |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-babel-config.el= | 3 | Source-block languages and tempo templates |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-export-config.el= | 3 | Export backends and their defaults |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-spec-links.el= | 3 | Resolves =[[id:]]= links into project spec docs |
+|------------------------------+-------+---------------------------------------------------------|
+| =hugo-config.el= | 3 | ox-hugo blog publishing (=C-; h=) |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-drill-config.el= | 4 | Spaced repetition, its own refile targets |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-noter-config.el= | 4 | PDF and EPUB annotation |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-reveal-config.el= | 4 | reveal.js presentations (=C-; p=) |
+|------------------------------+-------+---------------------------------------------------------|
+| =org-webclipper.el= | 4 | org-protocol web clipping into roam |
+|------------------------------+-------+---------------------------------------------------------|
+| =quick-video-capture.el= | 4 | Video download via an org-protocol bookmark |
+|------------------------------+-------+---------------------------------------------------------|
+
+* Shared variables and who writes to them
+
+This is the part grep answers slowly and a table answers fast. Each of these
+variables is appended to by more than one module.
+
+** =org-capture-templates= — four writers
+
+=org-capture-config.el= establishes the base set. =org-contacts-config.el=,
+=org-webclipper.el=, and =quick-video-capture.el= each append their own. Dispatch
+keys must not collide across all four, which is why
+=tests/test-org-capture-templates-integrity.el= exists: it loads the cleanly
+loadable capture modules, applies their additions, and asserts no two templates
+share a key.
+
+That test is the enforcement mechanism for a boundary no single module can see.
+
+** =org-agenda-files= — four writers
+
+=org-agenda-config.el= sets the base list. =org-roam-config.el= adds roam files.
+=config-utilities.el= manipulates it, and =org-agenda-config-debug.el= reads it for
+diagnostics.
+
+** org-protocol handlers — five registrars
+
+=org-config.el=, =org-capture-config.el=, =org-drill-config.el=,
+=org-webclipper.el=, and =quick-video-capture.el= all touch org-protocol. The
+handlers are keyed by protocol name, so collisions are possible in principle and
+nothing checks for them today.
+
+** =org-refile-targets= — two writers
+
+=org-refile-config.el= owns the general targets. =org-drill-config.el= adds its
+own, because drill files are refile destinations but not agenda files.
+
+* Load order
+
+=init.el= loads the Org stack at lines 130 through 145, in this order: base config,
+faces, agenda, babel, capture, contacts, drill, export, hugo, reveal, refile, roam,
+spec-links, webclipper, noter.
+
+The order is deliberate in two places. =org-config= precedes everything because it
+establishes the keymap the rest bind into. =org-faces-config= precedes
+=org-agenda-config= because the agenda renders with those faces.
+
+One module sits outside that block. =quick-video-capture.el= loads at line 106,
+twenty-four lines before =org-config=, because it is grouped with the media modules
+rather than the Org modules. It nonetheless appends to =org-capture-templates=. It
+works because the append is a plain =add-to-list= against a variable org-mode has
+not yet claimed, but the grouping means a reader looking for "who adds capture
+templates" in the Org block will not find it.
+
+* Load shape
+
+Every Org module declares =Load shape: eager=. Nothing in the Org stack defers.
+
+This is the single largest fact about the Org workflow's startup cost, and it is
+worth stating plainly rather than leaving implicit across sixteen headers. Layer-4
+modules (drill, noter, reveal, webclipper, video capture) are optional features by
+their own classification, and all five load eagerly on every launch.
+
+Whether that matters is a measurement question, not an argument, and this note does
+not make it. It records the fact so the question can be asked.
+
+* Observations for follow-up
+
+These turned up while mapping. None is fixed here.
+
+1. =modules/org-config.el.faces.bak= is tracked in git (25KB). It appears to be a
+ leftover from the 2026-06-14 face-stripping work. A =.bak= file in =modules/=
+ is not loaded, but it is on the load-path's directory and will confuse the next
+ reader.
+2. =org-agenda-config-debug.el= carries no =Layer= / =Category= / =Load shape=
+ header, unlike every other module. It loads conditionally from
+ =org-agenda-config.el:42= when =cj/debug-modules= names it, so its absence from
+ =init.el= is by design, not an orphan.
+3. Nothing checks org-protocol handler collisions the way the capture-template test
+ checks key collisions. Five modules register handlers.
+4. Every Layer-4 Org module loads eagerly despite being classified optional.