aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/2026-07-10-org-workflow-doctor-spec.org
blob: b7f5db6cf4f604d70a2ef27d1b69f2dd328370c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
#+TITLE: Org workflow doctor — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-07-10
#+TODO: TODO | DONE
#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED

* CANCELLED Org workflow doctor
:PROPERTIES:
:ID:       c0e06025-a3b8-4238-a9a0-07f9e55913f4
:END:
- 2026-07-10 Fri @ 05:58:00 -0500 — CANCELLED. The feature has no job. Craig asked what the doctor buys when it refuses to install anything, and the answer is nothing that isn't already there. Every external binary is already guarded at its point of use, with a clear message: hugo-config guards hugo and the file-manager opener, org-webclipper guards pandoc, org-export-config guards zathura, and ox-pandoc guards itself upstream (ox-pandoc.el:1533, 1970). Package availability surfaces at load or first use. Path checks were the only genuinely new capability, and a startup warning about a missing org-dir contradicts this spec's own goal of keeping startup quiet, while an on-demand check nobody remembers to run is worth roughly nothing. DRAFT -> CANCELLED.
- 2026-07-10 Fri @ 00:29:10 -0500 — drafted.

* Postmortem

The spec cleared spec-create's Phase 0 bar narrowly, and that was the signal to stop
and ask what the feature bought over the mechanisms already in the tree. It wasn't
taken.

The =Reuse & lost opportunities= dimension exists to catch exactly this. It was
filled in, it named =cj/executable-find-or-warn=, and it dismissed the helper in one
clause for warning as a side effect. Warning as a side effect is the feature. The
dimension was answered without being used.

Three claims in the surrounding audit came from grepping for a helper's *name* rather
than reading for the *behavior*, and all three were wrong: the org modules were said
not to guard their binaries when they guard them at the point of use, which is the
better place. Reading corrected the claim each time.

The one real finding this line of work produced landed elsewhere and stands: ledger
buffers were never linted, fixed in 55b85754.

* Metadata
| Status   | cancelled                                                |
|----------+----------------------------------------------------------|
| Owner    | Craig Jennings                                           |
|----------+----------------------------------------------------------|
| Reviewer | Craig Jennings                                            |
|----------+----------------------------------------------------------|
| Related  | [[file:../../todo.org][todo.org]] — "Add an Org workflow health check command" |
|----------+----------------------------------------------------------|

* Summary

A single on-demand command, =cj/org-workflow-doctor=, that checks whether the Org
workflow's prerequisites are actually present: the files and directories it reads
and writes, the external programs it shells out to, and the optional packages it
defers loading. It reports what it found and never changes anything.

Today a missing prerequisite surfaces at command time, in whatever shape the first
module to trip over it happens to produce. The doctor moves that discovery to a
moment the user chose.

* Problem / Context

#+begin_quote
*Correction, 2026-07-10.* The premise below is false and the body is left intact as
the record of the mistake. Every external binary *is* checked, at its point of use:
=hugo-config= guards hugo and the file-manager opener, =org-webclipper= guards
pandoc, =org-export-config= guards zathura, and =ox-pandoc= guards itself upstream.
The claim came from grepping for =cj/executable-find-or-warn= by name instead of
reading the modules for the behavior. See the Postmortem above.
#+end_quote

The Org workflow spans many modules, and each depends on some mix of a personal
path (=org-dir=, =roam-dir=), an external binary (pandoc, hugo), and an optional
package that loads lazily (=org-noter=, =org-web-tools=). None of those
dependencies is checked anywhere.

When one is missing, the failure appears wherever the first module happens to hit
it. A missing =contacts-file= surfaces as a capture template erroring mid-capture.
An absent pandoc surfaces as a shell command returning nothing useful. The user
learns about a broken prerequisite at the least convenient moment, and the message
rarely names the prerequisite.

Nothing about this is hard. It just isn't anywhere, and the checks are scattered
across modules that each know only their own corner.

* Goals and Non-Goals

** Goals
- One command reports the health of every Org workflow prerequisite.
- The check never mutates user data. It reads, it does not create or repair.
- The result is structured data, so it is unit-testable and can be rendered to
  either the echo area or a buffer without recomputing.
- Startup stays quiet. Nothing runs unless asked.

** Non-Goals
- It will not fix anything. No creating a missing directory, no installing a
  package, no offering to. A doctor that repairs is a different, riskier command.
- It will not check every package in the config, only the Org workflow's.
- It will not run on a timer, a hook, or at startup.
- It will not be a general config linter. The scope is the Org workflow.

** Scope tiers
- v1: the seven paths, the two external binaries, the six optional packages, a
  structured result, and a rendering to a buffer.
- Out of scope: repair actions, non-Org prerequisites, scheduled runs.
- vNext: a =--fix= variant that offers to create missing directories after
  confirmation; checking that =org-agenda-files= entries all resolve.

* Design

** For the caller

=M-x cj/org-workflow-doctor= opens a report buffer listing every prerequisite with
its status. A prerequisite is =ok=, =missing=, or =skipped= (checked something the
user hasn't configured). With a prefix argument the command reports a one-line
summary to the echo area instead, for a quick "is anything broken?" glance.

Nothing on disk changes. Running it twice produces the same report.

** For the implementer

The command splits in two, per the interactive-versus-internal rule.

=cj/org-workflow--check= is pure with respect to user data: it probes the
environment and returns a list of plists, one per prerequisite, each carrying
=:name=, =:kind= (=path= / =executable= / =package=), =:status= and =:detail=. It
takes no arguments and prompts for nothing, so a test can call it directly against
a temp =user-emacs-directory= and assert on the structure.

=cj/org-workflow-doctor= is the thin interactive wrapper: call the internal,
render the result, done.

*** The probe each kind uses

Paths are checked with =file-exists-p= against the variable's value, and reported
=skipped= when the variable is unbound or nil rather than =missing= — an unset
=cj/hugo-content-org-dir= means the user doesn't publish with Hugo, which is not a
fault.

Executables are checked with =executable-find=.

Packages are the one place a naive implementation gets it wrong, and this is the
decision the spec exists to record. The obvious probe is =featurep=, and it is
incorrect here: this config defers loading, so =featurep= returns nil for a package
that is installed and perfectly healthy. Probed on 2026-07-10, =org-noter= and
=org-web-tools= both report =(featurep) => nil= while =(locate-library) => t=. A
doctor built on =featurep= would report a false failure for precisely the packages
the config is designed to load lazily, which is worse than no doctor: it teaches
the user to ignore it.

=locate-library= answers the question actually being asked, which is "can this load
when something needs it?" rather than "has it loaded already?".

* Alternatives considered

*** Probe packages with =featurep=
- Good, because: it is the first thing that comes to mind and costs nothing.
- Bad, because: it reports false failures for every lazily-loaded package, which is
  most of them. Verified, not theorised.
- Neutral, because: it would be correct in a config that loads everything eagerly.

*** Check prerequisites at startup instead of on demand
- Good, because: the user learns about a broken prerequisite before they need it.
- Bad, because: it costs startup time on every launch to answer a question asked a
  few times a year, and it puts warnings in front of a user who didn't ask.
- Neutral, because: a doctor command can be run from a startup hook later if the
  cost turns out to be trivial.

*** Have each module check its own prerequisites
- Good, because: the check lives next to the thing that needs it.
- Bad, because: this is the status quo, and the problem is that the checks don't
  compose into an answer to "is my Org workflow healthy?".
- Neutral, because: the doctor doesn't prevent a module from also checking.

* Decisions [2/2]

** DONE Probe optional packages with =locate-library=, not =featurep=
Context: the config loads Org packages lazily, so a healthy package is routinely
unloaded. Probed 2026-07-10: =org-noter= and =org-web-tools= are both
=featurep=-nil and =locate-library=-non-nil.

Decision: we will probe package availability with =locate-library=.

Consequences: the doctor answers "can this load?", which is the question that
matters, and it stops reporting false failures for deferred packages. Harder: the
doctor cannot distinguish "installed but broken on load" from "installed and fine",
because it deliberately does not load anything. That is the right trade for a
read-only check, and a load error surfaces at use time anyway.

** DONE An unset optional path reports =skipped=, not =missing=
Context: not every user of this config publishes with Hugo or uses reveal.js. An
unbound or nil =cj/hugo-content-org-dir= is a configuration choice, not a fault.

Decision: we will report =skipped= when a path variable is unbound or nil, and
=missing= only when it holds a value that does not resolve on disk.

Consequences: the report stays honest, so a clean report means something. Harder:
the status vocabulary grows a third value, and the renderer has to distinguish
three states rather than two.

* Implementation phases

1. *The internal, with tests.* =cj/org-workflow--check= plus its three probe
   helpers (path, executable, package). Tests drive real state: a temp directory
   that exists and one that doesn't, an executable that resolves and a nonsense
   name, a library that locates and one that doesn't. Tree is working; nothing is
   bound to a key yet.
2. *The renderer and the command.* =cj/org-workflow-doctor=, the report buffer, and
   the prefix-argument echo-area summary. Tests cover the rendering of a synthetic
   result list, not the environment.

* Acceptance criteria

- =cj/org-workflow--check= returns one plist per prerequisite, each with =:name=,
  =:kind=, =:status= and =:detail=.
- A path variable holding a resolving directory reports =ok=; one holding a
  nonexistent path reports =missing=; one unbound or nil reports =skipped=.
- =org-noter= and =org-web-tools= report =ok= on this machine despite being
  unloaded. This is the regression the spec exists to prevent.
- Running the command twice leaves the filesystem byte-identical.
- The command is absent from every hook and timer.

* Readiness dimensions

- *Data model & ownership* — the result is generated, ephemeral, and owned by the
  command. Nothing persists.
- *Errors, empty states & failure* — a probe that throws is caught per-prerequisite
  and reported as =missing= with the error text as =:detail=, so one bad probe
  cannot abort the report.
- *Security & privacy* — the report prints paths from the user's config. It stays
  in a local buffer and is never written to disk or transmitted.
- *Observability* — the report is the observability.
- *Performance & scale* — fifteen probes, all local filesystem stats. No concern.
- *Reuse & lost opportunities* — =executable-find= and =locate-library= are the
  platform's answers; nothing is reimplemented. =cj/executable-find-or-warn=
  (=system-lib.el=) exists but warns as a side effect, which a read-only check must
  not do, so the doctor calls =executable-find= directly.
- *Architecture fit* — a new module, =modules/org-workflow-doctor.el=, requiring
  nothing but the variables it probes. It must not require the Org modules, or
  probing them would load them and defeat the lazy-loading it is checking.
- *Config surface* — none in v1. The prerequisite list is a defconst.
- *Documentation plan* — module commentary, plus the keybinding if one is added.
- *Dev tooling* — the existing =make test= covers it. No new target.
- *Rollout, compatibility & rollback* — additive, read-only, deletable. N/A.
- *External APIs & deps* — none. Both binaries were verified present on 2026-07-10
  (=/usr/bin/pandoc=, =/usr/bin/hugo=), and their absence is the case under test.

* Risks, rabbit holes, and drawbacks

The rabbit hole is scope. "Check the Org workflow's prerequisites" slides easily
into "lint the whole config", and from there into "offer to fix what it finds". The
non-goals exist to hold that line. If the doctor is useful, a =--fix= variant is a
separate spec with a separate risk profile, because a command that creates
directories is no longer read-only.

The smaller risk is the prerequisite list going stale as modules change. A doctor
that checks the wrong things is worse than none, since it reports health that isn't
real. The defconst lives next to the probes so it is at least easy to find.

* Review and iteration history

** 2026-07-10 Fri @ 00:29:10 -0500 — Craig Jennings — Author
What: drafted the spec.
Why: the task is feature-level, so the speedrun's per-item disposition rule
delivers a spec rather than an implementation.
Artifacts: this file. Probed the live daemon for the seven paths, both binaries,
and the six packages; the =featurep= finding drove the first decision.