aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/.ai/scripts/tests/test-lint-org.el
diff options
context:
space:
mode:
Diffstat (limited to 'claude-templates/.ai/scripts/tests/test-lint-org.el')
-rw-r--r--claude-templates/.ai/scripts/tests/test-lint-org.el437
1 files changed, 437 insertions, 0 deletions
diff --git a/claude-templates/.ai/scripts/tests/test-lint-org.el b/claude-templates/.ai/scripts/tests/test-lint-org.el
index 3a83602..ceee209 100644
--- a/claude-templates/.ai/scripts/tests/test-lint-org.el
+++ b/claude-templates/.ai/scripts/tests/test-lint-org.el
@@ -193,6 +193,65 @@ real suspicious-language warning here
#+end_src
")
+;; invalid-block, false-positive case — a correctly paired example block whose
+;; body holds a heading-shaped line. org's parser reads the `** ' inside the
+;; verbatim body as a structural break, loses the open block, and flags BOTH
+;; delimiters as "Possible incomplete block".
+(defconst lo-test--verbatim-heading-block "\
+* Heading
+
+#+begin_example
+** Feature Name or Topic
+Body line.
+#+end_example
+
+Trailing prose.
+")
+
+;; invalid-block, literal-delimiter case — a paired src block whose body holds
+;; a literal `#+end_example' plus a heading-shaped line. Only `#+end_src'
+;; closes a src block, so all three findings here are false.
+(defconst lo-test--literal-end-in-src "\
+* Heading
+
+#+begin_src text
+#+end_example
+** heading shaped
+#+end_src
+")
+
+;; invalid-block, uppercase-delimiter case — org accepts #+BEGIN_/#+END_ in
+;; either case, and the pre-fix script flagged both delimiters here too.
+(defconst lo-test--uppercase-verbatim-block "\
+* Heading
+
+#+BEGIN_EXAMPLE
+** heading shaped
+#+END_EXAMPLE
+")
+
+;; invalid-block, genuine case — a block that really is never closed. The
+;; suppression must not reach this one.
+(defconst lo-test--unterminated-block "\
+* Heading
+
+#+begin_example
+truly unterminated block body
+")
+
+;; A genuinely unterminated block *after* a correctly paired one — verifies the
+;; suppression is scoped per block rather than per file.
+(defconst lo-test--paired-then-unterminated "\
+* Heading
+
+#+begin_example
+** heading shaped
+#+end_example
+
+#+begin_example
+never closed
+")
+
;; Mixed fixture — each category once.
(defconst lo-test--mixed "\
* Mixed
@@ -392,6 +451,55 @@ suspicious-language judgment."
(should (= 1 suspicious))))
;;; ---------------------------------------------------------------------------
+;;; invalid-block — false positives on correctly paired verbatim blocks
+
+(ert-deftest lo-verbatim-heading-block-emits-no-invalid-block ()
+ "Normal: a paired example block containing a heading-shaped body line emits
+no invalid-block judgment. Both delimiters are flagged by org-lint because the
+parser treats the `** ' inside the verbatim body as a structural break."
+ (let* ((out (lo-test--run lo-test--verbatim-heading-block))
+ (res (plist-get out :result))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ ;; File untouched, no fixes applied — suppression only, never a rewrite.
+ (should (equal lo-test--verbatim-heading-block res))
+ (should (= 0 (plist-get out :fixes)))
+ (should-not (member 'invalid-block (lo-test--checkers judgments)))))
+
+(ert-deftest lo-literal-end-delimiter-in-src-emits-no-invalid-block ()
+ "Boundary: a paired src block whose body holds a literal `#+end_example' and
+a heading-shaped line emits no invalid-block judgment. Only `#+end_src' closes
+a src block, so the interior delimiter is body text."
+ (let* ((out (lo-test--run lo-test--literal-end-in-src))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'invalid-block (lo-test--checkers judgments)))))
+
+(ert-deftest lo-uppercase-verbatim-block-emits-no-invalid-block ()
+ "Boundary: block delimiters are case-insensitive in org, so an uppercase
+`#+BEGIN_EXAMPLE' pair is suppressed the same as a lowercase one."
+ (let* ((out (lo-test--run lo-test--uppercase-verbatim-block))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'invalid-block (lo-test--checkers judgments)))))
+
+(ert-deftest lo-unterminated-block-still-emits-invalid-block ()
+ "Error: a block that is never closed still emits its invalid-block judgment.
+This is the finding the checker exists for — the suppression must not mask it."
+ (let* ((out (lo-test--run lo-test--unterminated-block))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should (member 'invalid-block (lo-test--checkers judgments)))))
+
+(ert-deftest lo-invalid-block-suppression-is-scoped-per-block ()
+ "Boundary: a paired block and an unterminated block in the same file — the
+paired one is suppressed and the unterminated one still reports. Exactly one
+invalid-block judgment, and it points at the unterminated opener (line 7)."
+ (let* ((out (lo-test--run lo-test--paired-then-unterminated))
+ (judgments (lo-test--judgments (plist-get out :issues)))
+ (invalid (cl-remove-if-not
+ (lambda (i) (eq (plist-get i :checker) 'invalid-block))
+ judgments)))
+ (should (= 1 (length invalid)))
+ (should (= 7 (plist-get (car invalid) :line)))))
+
+;;; ---------------------------------------------------------------------------
;;; --check mode
(ert-deftest lo-check-mode-does-not-modify-file ()
@@ -620,6 +728,29 @@ followups file on the next run."
;;; ---------------------------------------------------------------------------
;;; org-table-standard check (width budget + rules between rows)
+(ert-deftest lo-table-inside-example-block-not-flagged ()
+ "Pipe-led ASCII art inside an example block is not a table; no judgment."
+ (let* ((run (lo-test--run
+ "* H\n\n#+begin_example\n| client |----->| server |\n| box | | box |\n#+end_example\n"
+ 1 t))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should-not (memq 'org-table-standard (lo-test--checkers judgments)))))
+
+(ert-deftest lo-table-inside-src-block-not-flagged ()
+ "Shell pipes inside a src block are not a table; no judgment."
+ (let* ((run (lo-test--run
+ "* H\n\n#+begin_src sh\n| sort\n| uniq -c\n#+end_src\n" 1 t))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should-not (memq 'org-table-standard (lo-test--checkers judgments)))))
+
+(ert-deftest lo-real-table-after-block-still-flagged ()
+ "Block safety must not mask a genuine violation later in the file."
+ (let* ((run (lo-test--run
+ "* H\n\n#+begin_example\n| art |\n#+end_example\n\n| a | b |\n| 1 | 2 |\n"
+ 1 t))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should (memq 'org-table-standard (lo-test--checkers judgments)))))
+
(ert-deftest lo-table-over-budget-emits-judgment ()
"A table line rendering wider than 120 surfaces as an org-table-standard judgment."
(let* ((wide (make-string 130 ?x))
@@ -659,5 +790,311 @@ missing-rules violation."
(judgments (lo-test--judgments (plist-get run :issues))))
(should-not (memq 'org-table-standard (lo-test--checkers judgments)))))
+;;; ---------------------------------------------------------------------------
+;;; level-2 dated-header check (claude-rules/todo-format.md)
+
+(ert-deftest lo-level2-dated-header-is-judgment ()
+ "A level-2 heading beginning with a YYYY-MM-DD date is flagged."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** 2026-06-20 Sat @ 10:00:00 -0500 Something resolved\nBody.\n"))
+ (res (plist-get out :result))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should (= 0 (plist-get out :fixes))) ; judgment-only, never auto-fixed
+ (should (member 'level-2-dated-header (lo-test--checkers judgments)))))
+
+(ert-deftest lo-level2-done-task-not-flagged ()
+ "A level-2 task closed with a terminal keyword + CLOSED: is fine."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** DONE [#B] Something resolved\nCLOSED: [2026-06-20 Sat]\nBody.\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'level-2-dated-header (lo-test--checkers judgments)))))
+
+(ert-deftest lo-level3-dated-entry-not-flagged ()
+ "A dated event-log entry at level 3 is the correct sub-task shape, not a defect."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent task\n*** 2026-06-20 Sat @ 10:00:00 -0500 sub-entry landed\nBody.\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'level-2-dated-header (lo-test--checkers judgments)))))
+
+;;; subtask-done-not-dated check (the inverse: level-3+ done keyword)
+
+(ert-deftest lo-subtask-done-not-dated-flags-level3 ()
+ "A level-3 DONE sub-task still carrying the keyword is flagged for conversion."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent\n*** DONE [#C] Sub-task done\nCLOSED: [2026-06-20 Sat 10:00]\nBody.\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should (= 0 (plist-get out :fixes))) ; judgment-only, never auto-fixed
+ (should (member 'subtask-done-not-dated (lo-test--checkers judgments)))))
+
+(ert-deftest lo-subtask-done-not-dated-flags-level4-cancelled ()
+ "A level-4 CANCELLED sub-task is flagged too."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** PROJECT [#B] Parent\n*** TODO Mid\n**** CANCELLED Deep abandoned\nCLOSED: [2026-06-20 Sat]\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should (member 'subtask-done-not-dated (lo-test--checkers judgments)))))
+
+(ert-deftest lo-subtask-done-not-dated-ignores-level2 ()
+ "A level-2 DONE task is a top-level task, not a sub-task — this checker skips it."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** DONE [#B] Top-level\nCLOSED: [2026-06-20 Sat]\nBody.\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'subtask-done-not-dated (lo-test--checkers judgments)))))
+
+(ert-deftest lo-subtask-done-not-dated-ignores-dated-and-lowercase ()
+ "An already-dated level-3 entry, and the word done in a title, are not flagged."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0400 landed\n*** TODO wrap the done cleanup\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'subtask-done-not-dated (lo-test--checkers judgments)))))
+
+;;; dated-log-heading-active-timestamp check (stale SCHEDULED/DEADLINE on a
+;;; completed dated-log entry — the home 2026-07-17 agenda-pollution bug)
+
+(ert-deftest lo-dated-log-active-scheduled-is-flagged ()
+ "A dated-log entry still carrying an active SCHEDULED is flagged: org renders
+it on the agenda forever despite the missing keyword."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 trip booked\nSCHEDULED: <2026-06-18 Thu>\nBody.\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should (= 0 (plist-get out :fixes))) ; judgment-only, never auto-fixed
+ (should (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments)))))
+
+(ert-deftest lo-dated-log-active-deadline-is-flagged ()
+ "An active DEADLINE on a dated-log entry is flagged too."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 shipped\nDEADLINE: <2026-06-25 Thu>\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments)))))
+
+(ert-deftest lo-dated-log-clean-entry-not-flagged ()
+ "A dated-log entry with no active planning timestamp is correct — not flagged."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 done cleanly\nBody only.\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments)))))
+
+(ert-deftest lo-dated-log-inactive-timestamp-not-flagged ()
+ "An inactive [..] timestamp doesn't render on the agenda, so it isn't flagged —
+only active <..> planning timestamps are the defect."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 recorded\nSCHEDULED: [2026-06-18 Thu]\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments)))))
+
+(ert-deftest lo-dated-log-active-scheduled-on-live-todo-not-flagged ()
+ "A live TODO (keyword present) that legitimately carries an active SCHEDULED is
+not a dated-log heading, so this checker leaves it alone."
+ (let* ((out (lo-test--run
+ "* Open Work\n\n** TODO [#B] Parent\n*** TODO [#C] real upcoming task\nSCHEDULED: <2026-06-18 Thu>\n"))
+ (judgments (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments)))))
+
+;;; ---------------------------------------------------------------------------
+;;; structural heading checks (org-lint gaps)
+
+(defun lo-test--checker-lines (issues checker)
+ "Lines of judgment ISSUES whose :checker is CHECKER, document order."
+ (mapcar (lambda (i) (plist-get i :line))
+ (cl-remove-if-not
+ (lambda (i) (and (eq (plist-get i :kind) 'judgment)
+ (eq (plist-get i :checker) checker)))
+ (reverse issues))))
+
+(ert-deftest lo-indented-heading-flags-leading-whitespace ()
+ "Error: a heading indented off column 0 is flagged (org demotes it to body)."
+ (let* ((out (lo-test--run "* Open\n ** TODO indented and lost\n** TODO fine\n"))
+ (j (lo-test--judgments (plist-get out :issues))))
+ (should (member 'indented-heading (lo-test--checkers j)))
+ (should (= 1 (length (lo-test--checker-lines (plist-get out :issues)
+ 'indented-heading))))))
+
+(ert-deftest lo-indented-heading-skips-stars-inside-blocks ()
+ "Boundary: indented stars inside a #+begin_/#+end_ block are legitimate content."
+ (let* ((out (lo-test--run "* Open\n#+begin_example\n ** not a heading\n#+end_example\n"))
+ (j (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'indented-heading (lo-test--checkers j)))))
+
+(ert-deftest lo-indented-heading-skips-single-star-list-bullets ()
+ "Normal: an indented single `*' is a valid plain-list bullet, not a demoted
+heading, so it is not flagged — only two-or-more indented stars are."
+ (let* ((out (lo-test--run "* Open\nintro line\n * first bullet\n * second bullet\n * nested bullet\n"))
+ (j (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'indented-heading (lo-test--checkers j)))))
+
+(ert-deftest lo-empty-heading-flags-bare-stars ()
+ "Error: a line of bare stars with no title is flagged."
+ (let* ((out (lo-test--run "* Open\n** \n** TODO real\n"))
+ (j (lo-test--judgments (plist-get out :issues))))
+ (should (member 'empty-heading (lo-test--checkers j)))))
+
+(ert-deftest lo-malformed-priority-flags-lowercase-and-skips-valid ()
+ "Error + Normal: a lowercase/oversized cookie flags; a valid [#B] stays silent."
+ (let* ((bad (lo-test--run "* Open\n** TODO [#a] lowercase cookie\n** TODO [#BB] oversized\n"))
+ (ok (lo-test--run "* Open\n** TODO [#B] valid cookie\n"))
+ (jo (lo-test--judgments (plist-get ok :issues))))
+ (should (= 2 (length (lo-test--checker-lines (plist-get bad :issues)
+ 'malformed-priority-cookie))))
+ (should-not (member 'malformed-priority-cookie (lo-test--checkers jo)))))
+
+(ert-deftest lo-malformed-priority-skips-verbatim-cookie-in-title ()
+ "Boundary: a dated-log title quoting =[#D]= verbatim is not a real cookie."
+ (let* ((out (lo-test--run "* Open\n** TODO [#B] parent\n*** 2026-05-14 reprioritized =[#D]= -> =[#B]=\n"))
+ (j (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'malformed-priority-cookie (lo-test--checkers j)))))
+
+(ert-deftest lo-done-without-closed-flags-undated-level2 ()
+ "Error: a level-2 DONE with no CLOSED line is flagged; a dated one is not."
+ (let* ((bad (lo-test--run "* Resolved\n** DONE undated finished\nbody\n"))
+ (jb (lo-test--judgments (plist-get bad :issues)))
+ (ok (lo-test--run "* Resolved\n** DONE dated\nCLOSED: [2026-06-29 Mon]\n"))
+ (jo (lo-test--judgments (plist-get ok :issues))))
+ (should (member 'level2-done-without-closed (lo-test--checkers jb)))
+ (should-not (member 'level2-done-without-closed (lo-test--checkers jo)))))
+
+(ert-deftest lo-done-without-closed-ignores-deeper-levels ()
+ "Boundary: a level-3 DONE (a dated-log sub-entry) need not carry CLOSED."
+ (let* ((out (lo-test--run "* Resolved\n** DONE parent\nCLOSED: [2026-06-29 Mon]\n*** DONE nested no-closed\n"))
+ (j (lo-test--judgments (plist-get out :issues))))
+ (should-not (member 'level2-done-without-closed (lo-test--checkers j)))))
+
+(ert-deftest lo-structural-checks-silent-on-clean-file ()
+ "Normal: a well-formed file trips none of the four structural checkers."
+ (let* ((out (lo-test--run "* Open Work\n** TODO [#A] a task :tag:\n** DOING [#B] another\n* Resolved\n** DONE [#C] done\nCLOSED: [2026-06-29 Mon]\n"))
+ (checkers (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (dolist (c '(indented-heading empty-heading malformed-priority-cookie
+ level2-done-without-closed))
+ (should-not (member c checkers)))))
+
(provide 'test-lint-org)
;;; test-lint-org.el ends here
+
+;;; ---------------------------------------------------------------------------
+;;; task-missing-last-reviewed (claude-rules/todo-format.md)
+
+(ert-deftest lo-task-without-last-reviewed-is-judgment ()
+ "An open level-2 task with no :LAST_REVIEWED: is flagged."
+ (let* ((out (lo-test--run "* Open Work\n** TODO [#B] A task :feature:\nBody.\n"))
+ (js (lo-test--judgments (plist-get out :issues))))
+ (should (memq 'task-missing-last-reviewed (lo-test--checkers js)))))
+
+(ert-deftest lo-task-with-last-reviewed-is-clean ()
+ "A task carrying the property is not flagged."
+ (let* ((out (lo-test--run (concat "* Open Work\n** TODO [#B] A task :feature:\n"
+ ":PROPERTIES:\n:LAST_REVIEWED: 2026-07-23\n:END:\n"
+ "Body.\n")))
+ (js (lo-test--judgments (plist-get out :issues))))
+ (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js)))))
+
+(ert-deftest lo-task-last-reviewed-accepts-org-timestamp ()
+ "The org-native [YYYY-MM-DD Day] form counts, matching the staleness script."
+ (let* ((out (lo-test--run (concat "* Open Work\n** TODO [#B] A task :feature:\n"
+ ":PROPERTIES:\n:LAST_REVIEWED: [2026-07-23 Thu]\n:END:\n")))
+ (js (lo-test--judgments (plist-get out :issues))))
+ (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js)))))
+
+(ert-deftest lo-done-task-without-last-reviewed-is-clean ()
+ "Completed tasks leave the review pool, so they are never flagged."
+ (let* ((out (lo-test--run (concat "* Open Work\n** DONE [#B] A task :feature:\n"
+ "CLOSED: [2026-07-23 Thu]\n")))
+ (js (lo-test--judgments (plist-get out :issues))))
+ (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js)))))
+
+(ert-deftest lo-subtask-without-last-reviewed-is-clean ()
+ "Only level-2 tasks are in the review pool; deeper headings are not."
+ (let* ((out (lo-test--run (concat "* Open Work\n** TODO [#B] Parent :feature:\n"
+ ":PROPERTIES:\n:LAST_REVIEWED: 2026-07-23\n:END:\n"
+ "*** TODO A sub-task\n")))
+ (js (lo-test--judgments (plist-get out :issues))))
+ (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js)))))
+
+(ert-deftest lo-cookieless-task-without-last-reviewed-is-clean ()
+ "The staleness script selects on a priority cookie, so match that scope."
+ (let* ((out (lo-test--run "* Open Work\n** TODO Manual testing and validation\n"))
+ (js (lo-test--judgments (plist-get out :issues))))
+ (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js)))))
+
+(ert-deftest lo-verify-task-without-last-reviewed-is-judgment ()
+ "VERIFY is in the review pool too."
+ (let* ((out (lo-test--run "* Open Work\n** VERIFY [#B] Waiting on Craig\n"))
+ (js (lo-test--judgments (plist-get out :issues))))
+ (should (memq 'task-missing-last-reviewed (lo-test--checkers js)))))
+
+;;; ---------------------------------------------------------------------------
+;;; todo-format checkers skip docs/specs/ files (claude-rules/todo-format.md)
+;;
+;; The four todo-format-family checkers encode todo.org completion conventions.
+;; A spec legitimately uses ** DONE <decision> with no CLOSED cookie and
+;; ** <dated> — <who> review-history headings, so those checkers misfire on
+;; every spec. They must skip any file under a docs/specs/ path segment.
+
+(defun lo-test--run-at (relpath content)
+ "Write CONTENT to <tmpdir>/RELPATH, run lint on it, return :issues.
+RELPATH is a relative path (may contain slashes) so a docs/specs/ segment
+can be exercised — the checkers key on the file's path, not just its name."
+ (let* ((root (make-temp-file "lo-test-root-" t))
+ (file (expand-file-name relpath root)))
+ (make-directory (file-name-directory file) t)
+ (unwind-protect
+ (progn
+ (with-temp-file file (insert content))
+ (lo-test--reset)
+ (lo-process-file file)
+ (prog1 (list :issues lo-issues)
+ (lo-test--drop-buffer file)))
+ (delete-directory root t))))
+
+(defconst lo-test--spec-decisions
+ "* Decisions [1/1]\n** DONE Some decision\n- Context: x\n"
+ "A spec Decisions section: a level-2 DONE with no CLOSED cookie.")
+
+(defconst lo-test--spec-history
+ "* Review history\n** 2026-07-14 Tue @ 02:03:28 -0500 — Claude — responder\n- What: x\n"
+ "A spec review-history section: a level-2 dated header.")
+
+(ert-deftest lo-todo-checkers-fire-on-a-normal-org-file ()
+ "Baseline: the checkers DO fire on a non-spec path (the bug is scope, not silence)."
+ (let* ((out (lo-test--run-at "todo.org" lo-test--spec-decisions))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should (memq 'level2-done-without-closed cs))))
+
+(ert-deftest lo-level2-done-without-closed-skips-specs ()
+ (let* ((out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" lo-test--spec-decisions))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should-not (memq 'level2-done-without-closed cs))))
+
+(ert-deftest lo-level2-dated-header-skips-specs ()
+ (let* ((out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" lo-test--spec-history))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should-not (memq 'level-2-dated-header cs))))
+
+(ert-deftest lo-dated-log-active-timestamp-skips-specs ()
+ (let* ((c "* History\n** 2026-07-14 Tue @ 02:03:28 -0500 — did a thing\nSCHEDULED: <2026-07-20 Mon>\n")
+ (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should-not (memq 'dated-log-heading-active-timestamp cs))))
+
+(ert-deftest lo-subtask-done-not-dated-skips-specs ()
+ (let* ((c "* Work\n** TODO Parent\n*** DONE A sub-decision\n")
+ (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should-not (memq 'subtask-done-not-dated cs))))
+
+(ert-deftest lo-link-checks-still-fire-on-specs ()
+ "Only the todo-format family is scoped out; a broken link in a spec still flags."
+ (let* ((c "* X\n[[file:does-not-exist-xyz.org][link]]\n")
+ (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should (memq 'link-to-local-file cs))))
+
+(ert-deftest lo-task-missing-last-reviewed-skips-specs ()
+ "The fifth todo-format checker (added 2026-07-23) skips specs too — a spec's
+phases section may carry ** TODO [#x] items that aren't backlog tasks."
+ (let* ((c "* Implementation phases\n** TODO [#B] Phase one\nBody.\n")
+ (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should-not (memq 'task-missing-last-reviewed cs)))
+ ;; And still fires on a normal file.
+ (let* ((c "* Work\n** TODO [#B] Real backlog task\nBody.\n")
+ (out (lo-test--run-at "todo.org" c))
+ (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))
+ (should (memq 'task-missing-last-reviewed cs))))