diff options
Diffstat (limited to 'docs/design')
| -rw-r--r-- | docs/design/2026-07-10-curated-help-menu-survey.org | 96 | ||||
| -rw-r--r-- | docs/design/2026-07-10-ledger-config-audit.org | 146 | ||||
| -rw-r--r-- | docs/design/2026-07-10-org-module-ownership.org | 138 |
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. |
