aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-06-29-lint-org-structural-checkers-proposal.org
blob: c464acad1daa6ac42bdee20f134b87f6108f743e (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
#+TITLE: lint-org.el — four structural heading checkers org-lint doesn't cover

* What changed (from .emacs.d, 2026-06-29)

Added four custom judgment checkers to =lint-org.el=, following the existing
=lo--check-tables= / =lo--check-level2-dated-headers= pattern (custom scans run
after the org-lint pass, emitting judgment items, never auto-fixed):

- =indented-heading= — a line of whitespace + stars + space OUTSIDE any block.
  org parses a heading only at column 0, so leading whitespace silently demotes
  a would-be heading to body text: the task vanishes from the agenda and never
  archives. The worst defect class (an invisible task) and entirely silent
  today. Skips indented stars inside =#+begin_/#+end_= blocks (legit content).
- =empty-heading= — a line of bare stars with no title.
- =malformed-priority-cookie= — a =[#x]=-shaped token org rejected (lowercase,
  multi-char, non-letter) left stranded where a cookie would be. Checks only the
  first cookie token per heading; skips verbatim-wrapped =[#D]= in dated-log
  titles.
- =level2-done-without-closed= — a level-2 DONE/CANCELLED with no CLOSED line.
  Directly supports the todo-cleanup aging step (sent separately today): an
  undated completed task gets force-archived immediately, so flagging it lets
  the human add CLOSED first.

Two attached files (edited canonical candidates): =lint-org.el=,
=tests/test-lint-org.el=.

* Why

org-lint validates links, drawers, blocks, and babel — but NOT heading
well-formedness. On Craig's .emacs.d todo.org a missing org-bullet in the live
buffer prompted the question "is the file structurally okay?", and org-lint
(even unfiltered, all checkers) reported nothing actionable. These four close
the gap. They are general (any org file), not project-specific.

* Design notes for the canonical

- All four are regex-based, NOT org-element/keyword-based, so they don't depend
  on which TODO keywords the batch Emacs happens to recognize (lint-org.el does
  not set =org-todo-keywords=). The =level2-done-without-closed= done set is a
  defconst =lo-done-keywords= (DONE/CANCELLED) for easy extension.
- *Gotcha worth carrying in the canonical:* =case-fold-search= defaults to t, so
  a naive =[A-Z]= cookie check accepts =[#a]= as valid and =\(DONE\|CANCELLED\)=
  matches the title words "done"/"cancelled". Both letter-sensitive checkers
  bind =case-fold-search nil=. (Caught by a failing test before it shipped.)
- Wired into =lo-process-file= after =lo--check-level2-dated-headers=. Judgment
  output already flows through the existing report + followups-file machinery.
- 8 new ERT tests (good-input-silent + bad-input-flagged for each, plus
  block-skip and verbatim-skip boundary cases). 44/44 green. Zero false
  positives on a real 5600-line todo.org.

* Note

=make task-sorted= in .emacs.d now runs =lint-org.el todo.org= after the
archive, so these checkers also gate the task-hygiene target. Makefiles aren't
template-synced; that wiring is project-local (noted for context).