aboutsummaryrefslogtreecommitdiff
path: root/docs/design
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-30 13:50:22 -0400
committerCraig Jennings <c@cjennings.net>2026-06-30 13:50:22 -0400
commitd9d8ce79da82b8c0fbbc8d6090548cd1f508b4c0 (patch)
tree29e90ba96bb227fd7c4d8d0bf1c24aa863f6a331 /docs/design
parentf67e72430845236ab5ed4ca00ba13afe87eda53a (diff)
downloadrulesets-d9d8ce79da82b8c0fbbc8d6090548cd1f508b4c0.tar.gz
rulesets-d9d8ce79da82b8c0fbbc8d6090548cd1f508b4c0.zip
feat(lint-org): add four structural heading checkers org-lint misses
org-lint validates links, drawers, blocks, and babel, but not heading well-formedness. These four catch hand-edit defects it stays silent on: an indented heading demoted to body text (the task vanishes from the agenda and never archives), bare stars with no title, a malformed priority cookie org rejected, and a level-2 DONE/CANCELLED with no CLOSED line. All judgment-only and regex-based, wired in after the existing dated-header check. The last one pairs with the new aging step, which archives an undated completed task immediately. I tightened the indented-heading check to two-or-more stars. The proposed one-or-more-stars regex flagged indented single-star lines, but an indented single * is a valid plain-list bullet, not a lost heading, so it false-positived on legitimate lists (confirmed: three valid bullets flagged). A ** is never a bullet, so an indented one is unambiguously a demoted heading. Added a test that a single-star list stays silent.
Diffstat (limited to 'docs/design')
-rw-r--r--docs/design/2026-06-29-lint-org-structural-checkers-proposal.org55
1 files changed, 55 insertions, 0 deletions
diff --git a/docs/design/2026-06-29-lint-org-structural-checkers-proposal.org b/docs/design/2026-06-29-lint-org-structural-checkers-proposal.org
new file mode 100644
index 0000000..c464aca
--- /dev/null
+++ b/docs/design/2026-06-29-lint-org-structural-checkers-proposal.org
@@ -0,0 +1,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).