#+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).