aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/lint-org.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-02 21:31:37 -0500
committerCraig Jennings <c@cjennings.net>2026-06-02 21:31:37 -0500
commitac693a6b7fa7abe88f7778f8e793d5ddfd32f24e (patch)
treeac0e8babf829ffd3ab64bb3b2bb5e9498d1f2f13 /.ai/scripts/lint-org.el
parent291103803495cd1937244dc7c993faaaf00023ab (diff)
downloadrulesets-ac693a6b7fa7abe88f7778f8e793d5ddfd32f24e.tar.gz
rulesets-ac693a6b7fa7abe88f7778f8e793d5ddfd32f24e.zip
feat(lint-org): reconcile follow-ups on write instead of appending
Every run appended a fresh dated "lint-org follow-ups" section with line-number-keyed entries, so the follow-ups file grew an unbounded pile of near-duplicate sections, kept entries whose finding had since resolved, and broke whenever the target file's line numbers shifted. Running an audit against a large todo.org surfaced exactly that drift: dead-link flags pointing at docs that now exist, and three stacked dated runs for one file. Now lint-org rewrites the current file's section from the current run. Findings that no longer reproduce simply are not re-emitted, re-runs dedupe to one section, and entries key on checker plus message with the line as a trailing annotation, so a finding survives line shifts as the same entry. Other files' sections are left intact, and the strip step tolerates the old dated-header shape so existing follow-ups files migrate on first run. This changes the follow-ups file from an append-only log to the current outstanding findings per file. task-audit's Phase C link-hygiene step now also reaps a matching dead-link entry when it fixes or verifies the link, scoped strictly to dead-link entries, so the audit and the follow-ups file stop drifting between lint runs. Five follow-ups tests cover record-by-content, dedupe across runs, drop-on-resolve, and preserve-other-files. Mirrors synced.
Diffstat (limited to '.ai/scripts/lint-org.el')
-rw-r--r--.ai/scripts/lint-org.el85
1 files changed, 69 insertions, 16 deletions
diff --git a/.ai/scripts/lint-org.el b/.ai/scripts/lint-org.el
index 2e97db0..85886af 100644
--- a/.ai/scripts/lint-org.el
+++ b/.ai/scripts/lint-org.el
@@ -322,27 +322,80 @@ left unmodified and mechanical entries are recorded with :preview t."
;;; ---------------------------------------------------------------------------
;;; Reporting
+(defun lo--followups-section (file judgments)
+ "Org section text for FILE's JUDGMENTS, keyed by checker + message.
+Empty string when there are no judgments. The line number is a trailing
+annotation, not the entry's identity, so a finding that shifts lines after an
+edit is still recognized as the same finding."
+ (if (null judgments)
+ ""
+ (concat
+ (format "* lint-org follow-ups — %s (%s)\n"
+ (file-name-nondirectory file)
+ (format-time-string "%Y-%m-%d"))
+ (mapconcat
+ (lambda (i)
+ (format "** TODO %s — %s (line %d)\n"
+ (plist-get i :checker)
+ (plist-get i :msg)
+ (plist-get i :line)))
+ judgments
+ ""))))
+
+(defun lo--strip-followups-section (content file)
+ "Return CONTENT with FILE's existing follow-ups section(s) removed.
+A section runs from its `* lint-org follow-ups — FILE ...' header to the next
+top-level `* ' heading or end of buffer. Matched on the file name, so it
+survives line-number churn in the target file, and tolerates the older
+`* DATE lint-org follow-ups — FILE' header shape so legacy runs migrate cleanly."
+ (with-temp-buffer
+ (insert content)
+ (let ((header-re
+ (format "^\\* \\(?:[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} \\)?lint-org follow-ups — %s "
+ (regexp-quote (file-name-nondirectory file)))))
+ (while (progn (goto-char (point-min))
+ (re-search-forward header-re nil t))
+ (let ((start (line-beginning-position))
+ (end (if (re-search-forward "^\\* " nil t)
+ (line-beginning-position)
+ (point-max))))
+ (delete-region start end))))
+ (buffer-string)))
+
+(defun lo--reconciled-followups (existing file judgments)
+ "Reconcile EXISTING follow-ups content for FILE against the current JUDGMENTS.
+FILE's prior section is replaced by the current findings (dropped entirely when
+nothing reproduces); other files' sections are left intact. This is what makes
+re-runs dedupe and resolved findings disappear instead of piling up."
+ (let ((stripped (string-trim (lo--strip-followups-section existing file)))
+ (section (string-trim (lo--followups-section file judgments))))
+ (cond
+ ((and (string= stripped "") (string= section "")) "")
+ ((string= section "") (concat stripped "\n"))
+ ((string= stripped "") (concat section "\n"))
+ (t (concat stripped "\n\n" section "\n")))))
+
(defun lo--append-followups ()
- "Append any judgment items from the current run to `lo-followups-file' as a
-dated org section. No-op when the file path is unset or there are no
-judgment items."
+ "Reconcile the current run's judgment items into `lo-followups-file'.
+Rewrites FILE's section from the current findings: entries are keyed by content
+(checker + message) rather than line number, findings that no longer reproduce
+are dropped, and re-runs dedupe instead of appending a fresh dated section.
+No-op when the file path is unset, or when there are no judgments and no file to
+reconcile."
(when lo-followups-file
(let ((judgments (cl-remove-if-not
(lambda (i) (eq (plist-get i :kind) 'judgment))
(reverse lo-issues))))
- (when judgments
- (let ((dir (file-name-directory (expand-file-name lo-followups-file))))
- (when dir (make-directory dir t)))
- (with-temp-buffer
- (insert (format "\n* %s lint-org follow-ups — %s\n"
- (format-time-string "%Y-%m-%d")
- (file-name-nondirectory lo-current-file)))
- (dolist (i judgments)
- (insert (format "** TODO line %d — %s — %s\n"
- (plist-get i :line)
- (plist-get i :checker)
- (plist-get i :msg))))
- (append-to-file (point-min) (point-max) lo-followups-file))))))
+ (when (or judgments (file-exists-p lo-followups-file))
+ (let ((existing (if (file-exists-p lo-followups-file)
+ (with-temp-buffer
+ (insert-file-contents lo-followups-file)
+ (buffer-string))
+ ""))
+ (dir (file-name-directory (expand-file-name lo-followups-file))))
+ (when dir (make-directory dir t))
+ (with-temp-file lo-followups-file
+ (insert (lo--reconciled-followups existing lo-current-file judgments))))))))
(defun lo-emit-report ()
"Print the per-file summary line plus each issue as a readable plist.