aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/todo-cleanup.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-06 21:59:52 -0500
committerCraig Jennings <c@cjennings.net>2026-05-06 21:59:52 -0500
commitd81b23ad6b6e437dfe3c338a00a4be39bc555146 (patch)
tree2d4b0d7890fd1fc70d81282b81fed2808c28a106 /.ai/scripts/todo-cleanup.el
parent201377f57430ef28d02e703a2191434bbee55c75 (diff)
downloadrulesets-d81b23ad6b6e437dfe3c338a00a4be39bc555146.tar.gz
rulesets-d81b23ad6b6e437dfe3c338a00a4be39bc555146.zip
chore(ai): initialize project notes and Claude tooling surfaces
Replace the seed notes.org with project-specific context (layout, install modes, task tracker location, recent inflection point). Bring in the synced template surfaces (protocols, workflows, scripts, references, retrospectives, someday-maybe) as tracked content for this content/documentation project.
Diffstat (limited to '.ai/scripts/todo-cleanup.el')
-rw-r--r--.ai/scripts/todo-cleanup.el149
1 files changed, 149 insertions, 0 deletions
diff --git a/.ai/scripts/todo-cleanup.el b/.ai/scripts/todo-cleanup.el
new file mode 100644
index 0000000..c4231f4
--- /dev/null
+++ b/.ai/scripts/todo-cleanup.el
@@ -0,0 +1,149 @@
+;;; todo-cleanup.el --- Auto-fix and audit for todo.org hygiene
+;;
+;; Usage:
+;; emacs --batch -q -l todo-cleanup.el todo.org # apply fixes in place
+;; emacs --batch -q -l todo-cleanup.el --check todo.org # report-only
+;;
+;; What it does:
+;;
+;; 1. Auto-deletes "bogus state-log" lines of the form
+;; - State "X" from "X" [date]
+;; where the state didn't actually change. Org sometimes logs these when
+;; `org-log-into-drawer' is unset and a state-change toggle lands on the
+;; same state. They carry no information and they break org's planning-line
+;; parser by sitting between the heading and DEADLINE/SCHEDULED.
+;;
+;; 2. Detects "orphan planning lines" — entries whose body contains
+;; `^DEADLINE:' or `^SCHEDULED:' that org-entry-get can't read because the
+;; line isn't in canonical position. Reports these for manual fix; doesn't
+;; auto-rewrite (preserving real state-log history is judgement work).
+;;
+;; Designed for the wrap-it-up workflow: cheap (~0.4s on a 3700-line todo.org),
+;; idempotent, and safe to run every session. Any fixes show up in the
+;; wrap-up commit's diff for review.
+
+(require 'org)
+(require 'cl-lib)
+
+(setq org-todo-keywords
+ '((sequence "TODO" "DOING" "WAITING" "NEXT" "|" "DONE" "CANCELLED")))
+
+(defvar tc-fixes 0)
+(defvar tc-issues nil)
+(defvar tc-check-only nil)
+(defvar tc-current-file nil)
+
+(defun tc-fix-bogus-state-log-in-entry ()
+ "Delete bogus state-log lines within the entry at point.
+A bogus log line matches `- State \"X\" from \"X\" [date]' where the two
+states are identical."
+ (save-excursion
+ (let ((end (save-excursion
+ (or (outline-next-heading) (goto-char (point-max)))
+ (point))))
+ (while (re-search-forward
+ "^[[:space:]]*- State \"\\([^\"]+\\)\"[[:space:]]+from \"\\1\"[[:space:]]+\\[[^]]+\\][[:space:]]*\n"
+ end t)
+ (let ((line (line-number-at-pos (match-beginning 0))))
+ (if tc-check-only
+ (push (list :kind 'bogus-log
+ :file tc-current-file
+ :line line
+ :detail (string-trim (match-string 0)))
+ tc-issues)
+ (delete-region (match-beginning 0) (match-end 0))
+ (cl-incf tc-fixes)
+ (push (list :kind 'bogus-log-fixed
+ :file tc-current-file
+ :line line
+ :detail (string-trim (match-string 0)))
+ tc-issues)))))))
+
+(defun tc-detect-orphan-planning-in-entry ()
+ "Flag entries where DEADLINE/SCHEDULED is in the body but org-entry-get returns nil.
+This means the planning line isn't in canonical position, so org-mode's
+agenda + scheduling machinery won't see it."
+ (let* ((line (line-number-at-pos))
+ (heading (org-get-heading t t t t))
+ (dl-canonical (org-entry-get (point) "DEADLINE"))
+ (sc-canonical (org-entry-get (point) "SCHEDULED"))
+ (start (save-excursion (org-end-of-meta-data t) (point)))
+ (end (save-excursion
+ (or (outline-next-heading) (goto-char (point-max)))
+ (point)))
+ (body (buffer-substring-no-properties start end)))
+ (when (and (not dl-canonical)
+ (string-match "^[[:space:]]*DEADLINE:[[:space:]]*\\(<[^>]+>\\)" body))
+ (push (list :kind 'orphan-deadline
+ :file tc-current-file
+ :line line
+ :heading heading
+ :detail (match-string 1 body))
+ tc-issues))
+ (when (and (not sc-canonical)
+ (string-match "^[[:space:]]*SCHEDULED:[[:space:]]*\\(<[^>]+>\\)" body))
+ (push (list :kind 'orphan-scheduled
+ :file tc-current-file
+ :line line
+ :heading heading
+ :detail (match-string 1 body))
+ tc-issues))))
+
+(defun tc-process-file (file)
+ (setq tc-current-file (file-name-nondirectory file))
+ (with-current-buffer (find-file-noselect file)
+ (org-mode)
+ ;; Pass 1: auto-fix bogus state logs (or report under --check).
+ (org-map-entries #'tc-fix-bogus-state-log-in-entry nil 'file)
+ ;; Pass 2: detect orphan planning lines (always report-only).
+ (org-map-entries #'tc-detect-orphan-planning-in-entry nil 'file)
+ (when (and (not tc-check-only) (buffer-modified-p))
+ (save-buffer))))
+
+(defun tc-emit-report ()
+ (princ (format "todo-cleanup: %d fix(es) applied%s\n"
+ tc-fixes
+ (if tc-check-only " — CHECK MODE (no writes)" "")))
+ (let ((orphans (cl-remove-if-not (lambda (i) (memq (plist-get i :kind)
+ '(orphan-deadline
+ orphan-scheduled)))
+ tc-issues))
+ (logs (cl-remove-if-not (lambda (i) (memq (plist-get i :kind)
+ '(bogus-log
+ bogus-log-fixed)))
+ tc-issues)))
+ (when logs
+ (princ (format " Bogus state-log lines (%s):\n"
+ (if tc-check-only "would delete" "deleted")))
+ (dolist (i (nreverse logs))
+ (princ (format " %s:%d: %s\n"
+ (plist-get i :file)
+ (plist-get i :line)
+ (plist-get i :detail)))))
+ (when orphans
+ (princ (format " Orphan planning lines needing manual fix (%d):\n" (length orphans)))
+ (dolist (i (nreverse orphans))
+ (princ (format " %s:%d: %s — %s in body\n"
+ (plist-get i :file)
+ (plist-get i :line)
+ (plist-get i :heading)
+ (plist-get i :detail)))))))
+
+(when noninteractive
+ ;; Mutate `command-line-args-left' so emacs's own arg parser doesn't see
+ ;; --check after our script returns.
+ (when (member "--check" command-line-args-left)
+ (setq tc-check-only t)
+ (setq command-line-args-left (delete "--check" command-line-args-left)))
+ (if (null command-line-args-left)
+ (progn (princ "Usage: emacs --batch -q -l todo-cleanup.el [--check] FILE...\n")
+ (kill-emacs 1))
+ (let ((files command-line-args-left))
+ (setq command-line-args-left nil)
+ (dolist (file files)
+ (when (file-readable-p file)
+ (tc-process-file file)))
+ (tc-emit-report))))
+
+(provide 'todo-cleanup)
+;;; todo-cleanup.el ends here