From f6dde4e0fe21022966196e19d535f2bb7abcfcdb Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 20 Jun 2026 23:28:47 -0400 Subject: feat(lint-org): flag level-2 dated headers as a completion defect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `** …` heading carries no keyword, so todo-cleanup's --archive-done can never archive it and task-review drops it from selection. The new level-2-dated-header check (custom, like org-table-standard) emits a judgment item per offending heading so the wrap-up sweep routes it to the next morning's review. Judgment-only, never auto-fixed: the repair needs a DONE-vs-CANCELLED call and the original heading text. Three ERT cases cover it (flagged at level 2, clean for DONE+CLOSED, clean for a level-3 dated entry). --- .ai/scripts/lint-org.el | 27 ++++++++++++++++++++++ .ai/scripts/tests/test-lint-org.el | 26 +++++++++++++++++++++ .claude/commands/lint-org.md | 1 + claude-templates/.ai/scripts/lint-org.el | 27 ++++++++++++++++++++++ .../.ai/scripts/tests/test-lint-org.el | 26 +++++++++++++++++++++ 5 files changed, 107 insertions(+) diff --git a/.ai/scripts/lint-org.el b/.ai/scripts/lint-org.el index 8f55cc6..3633dba 100644 --- a/.ai/scripts/lint-org.el +++ b/.ai/scripts/lint-org.el @@ -367,6 +367,31 @@ Emits one judgment item per violating table." (format "table violates the org-table standard: %s — wrap-org-table.el reflows it" (string-join violations "; "))))))))) +;;; --------------------------------------------------------------------------- +;;; level-2 dated-header check (claude-rules/todo-format.md) +;; +;; A completed task or resolved VERIFY at level 2 must carry a terminal +;; keyword (DONE/CANCELLED + CLOSED:), never a dated heading. A `** ' +;; header has no keyword, so todo-cleanup's --archive-done can never archive +;; it (it accumulates in Open Work forever) and task-review drops it from +;; selection. Judgment-only, never auto-fixed: the repair needs a +;; DONE-vs-CANCELLED call and the original heading text, which is a judgment +;; the sweep can't make. Targets todo/task files; a dated-log-format org +;; file using `** ' headings intentionally will false-positive here, in +;; which case the human dismisses the judgment item. + +(defun lo--check-level2-dated-headers () + "Flag level-2 headings whose text begins with a YYYY-MM-DD date. +Emits one judgment item per offending heading (checker +`level-2-dated-header')." + (save-excursion + (goto-char (point-min)) + (while (re-search-forward + "^\\*\\* \\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" nil t) + (lo--emit-judgment + 'level-2-dated-header (line-number-at-pos) + "level-2 dated header is a completion defect (todo-format.md): a ** task or VERIFY closes with DONE/CANCELLED + CLOSED:, not a dated heading — convert it so --archive-done can archive it")))) + ;;; --------------------------------------------------------------------------- ;;; File processing @@ -401,6 +426,8 @@ left unmodified and mechanical entries are recorded with :preview t." ;; After org-lint items: the custom table-standard scan. Runs on the ;; post-fix buffer; judgment-only, so order doesn't perturb fixes. (lo--check-tables) + ;; Same shape: flag level-2 dated headers (completion defects). + (lo--check-level2-dated-headers) (when (and (not lo-check-only) (buffer-modified-p)) (save-buffer))) (with-current-buffer buf (set-buffer-modified-p nil)) diff --git a/.ai/scripts/tests/test-lint-org.el b/.ai/scripts/tests/test-lint-org.el index 3a83602..242c35c 100644 --- a/.ai/scripts/tests/test-lint-org.el +++ b/.ai/scripts/tests/test-lint-org.el @@ -659,5 +659,31 @@ 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))))) + (provide 'test-lint-org) ;;; test-lint-org.el ends here diff --git a/.claude/commands/lint-org.md b/.claude/commands/lint-org.md index 953629c..64ec967 100644 --- a/.claude/commands/lint-org.md +++ b/.claude/commands/lint-org.md @@ -50,6 +50,7 @@ Out of scope (refuse, don't try to lint): | `invalid-fuzzy-link` | (1) Repair to a `[[*Heading]]` ref if a similar heading exists. (2) Drop to `=verbatim label=` text. (3) Skip. | | `misplaced-heading` *(verbatim-asterisk case)* | (1) Strip asterisks and rephrase to preserve semantics. (2) Convert surrounding markup to `~code~` style. (3) Skip. | | `suspicious-language-in-src-block` | (1) Emit an Emacs init one-liner that registers the language. (2) Change the block label to `text` or `example`. (3) Skip. | +| `level-2-dated-header` *(custom check, not org-lint)* | A `** …` heading is a completion defect per `todo-format.md` (no keyword, so `--archive-done` can't archive it). (1) Convert to `DONE`/`CANCELLED` + `CLOSED:`, keeping the heading text — the usual fix. (2) Demote to `***` if it's really a mis-leveled sub-entry. (3) Skip (a dated-log-format org file where `**` dates are intentional). | | anything else | Surface the raw `org-lint` message and ask the user how to proceed. | ## Phase A — Run the script diff --git a/claude-templates/.ai/scripts/lint-org.el b/claude-templates/.ai/scripts/lint-org.el index 8f55cc6..3633dba 100644 --- a/claude-templates/.ai/scripts/lint-org.el +++ b/claude-templates/.ai/scripts/lint-org.el @@ -367,6 +367,31 @@ Emits one judgment item per violating table." (format "table violates the org-table standard: %s — wrap-org-table.el reflows it" (string-join violations "; "))))))))) +;;; --------------------------------------------------------------------------- +;;; level-2 dated-header check (claude-rules/todo-format.md) +;; +;; A completed task or resolved VERIFY at level 2 must carry a terminal +;; keyword (DONE/CANCELLED + CLOSED:), never a dated heading. A `** ' +;; header has no keyword, so todo-cleanup's --archive-done can never archive +;; it (it accumulates in Open Work forever) and task-review drops it from +;; selection. Judgment-only, never auto-fixed: the repair needs a +;; DONE-vs-CANCELLED call and the original heading text, which is a judgment +;; the sweep can't make. Targets todo/task files; a dated-log-format org +;; file using `** ' headings intentionally will false-positive here, in +;; which case the human dismisses the judgment item. + +(defun lo--check-level2-dated-headers () + "Flag level-2 headings whose text begins with a YYYY-MM-DD date. +Emits one judgment item per offending heading (checker +`level-2-dated-header')." + (save-excursion + (goto-char (point-min)) + (while (re-search-forward + "^\\*\\* \\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\)" nil t) + (lo--emit-judgment + 'level-2-dated-header (line-number-at-pos) + "level-2 dated header is a completion defect (todo-format.md): a ** task or VERIFY closes with DONE/CANCELLED + CLOSED:, not a dated heading — convert it so --archive-done can archive it")))) + ;;; --------------------------------------------------------------------------- ;;; File processing @@ -401,6 +426,8 @@ left unmodified and mechanical entries are recorded with :preview t." ;; After org-lint items: the custom table-standard scan. Runs on the ;; post-fix buffer; judgment-only, so order doesn't perturb fixes. (lo--check-tables) + ;; Same shape: flag level-2 dated headers (completion defects). + (lo--check-level2-dated-headers) (when (and (not lo-check-only) (buffer-modified-p)) (save-buffer))) (with-current-buffer buf (set-buffer-modified-p nil)) diff --git a/claude-templates/.ai/scripts/tests/test-lint-org.el b/claude-templates/.ai/scripts/tests/test-lint-org.el index 3a83602..242c35c 100644 --- a/claude-templates/.ai/scripts/tests/test-lint-org.el +++ b/claude-templates/.ai/scripts/tests/test-lint-org.el @@ -659,5 +659,31 @@ 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))))) + (provide 'test-lint-org) ;;; test-lint-org.el ends here -- cgit v1.2.3