aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/.ai/scripts
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-14 01:27:17 -0500
committerCraig Jennings <c@cjennings.net>2026-07-14 01:27:17 -0500
commit951b6fc63129577a80e1d46f46eeafff005fe636 (patch)
treeb5ecd33ae60971ad23448d4b20ef1e3edf488923 /claude-templates/.ai/scripts
parentf573c35885583900e3e319cea384606c4e0321cf (diff)
downloadrulesets-951b6fc63129577a80e1d46f46eeafff005fe636.tar.gz
rulesets-951b6fc63129577a80e1d46f46eeafff005fe636.zip
fix(scripts): stop org table helpers rewriting block content
Both helpers treated any pipe-led line as a table row, so ASCII art in an example block got mangled into a bordered table (a work file took a 1949-line reformat on 2026-07-09). The scanners now track the open block's type and skip its content. Only the matching end marker closes a block, so a literal end_src quoted inside an example block can't re-expose it. The deeper cause was wrap-org-table's load-time dispatch: it fired when lint-org merely required the library, running the reformatter over files lint-org was only asked to report on. It now dispatches only when named as the entry script. lint-org's CLI is also report-only by default now. Writes require --fix, and the wrap-up workflow and lint command pass it.
Diffstat (limited to 'claude-templates/.ai/scripts')
-rw-r--r--claude-templates/.ai/scripts/lint-org.el72
-rw-r--r--claude-templates/.ai/scripts/tests/lint-org-cli.bats18
-rw-r--r--claude-templates/.ai/scripts/tests/test-lint-org.el23
-rw-r--r--claude-templates/.ai/scripts/tests/test-wrap-org-table.el42
-rw-r--r--claude-templates/.ai/scripts/wrap-org-table.el58
5 files changed, 178 insertions, 35 deletions
diff --git a/claude-templates/.ai/scripts/lint-org.el b/claude-templates/.ai/scripts/lint-org.el
index 90b1b1d..55727ef 100644
--- a/claude-templates/.ai/scripts/lint-org.el
+++ b/claude-templates/.ai/scripts/lint-org.el
@@ -2,16 +2,19 @@
;;
;; Usage:
;; emacs --batch -q -l lint-org.el FILE.org [FILE.org ...]
+;; report only (the default) — categorize without modifying the file.
+;; A linter reports, it doesn't write; mutation requires --fix.
+;; --check is accepted as an explicit alias of this default.
+;;
+;; emacs --batch -q -l lint-org.el --fix FILE.org [FILE.org ...]
;; apply mechanical fixes in place, emit judgment items on stdout for the
;; command layer to walk
;;
-;; emacs --batch -q -l lint-org.el --check FILE.org [FILE.org ...]
-;; report only — categorize without modifying the file
-;;
-;; emacs --batch -q -l lint-org.el --followups-file=PATH FILE.org
+;; emacs --batch -q -l lint-org.el --fix --followups-file=PATH FILE.org
;; apply mechanical fixes; if any judgment items remain, append them to
;; PATH as an org section dated today. Used by wrap-it-up to defer the
;; judgment walk to the next morning's review without blocking the wrap.
+;; (--followups-file only writes in --fix mode.)
;;
;; Mechanical categories (auto-fixed):
;; item-number add [@N] directive to drifted bullets
@@ -66,7 +69,9 @@
Each plist has :kind (mechanical-fixed | judgment), :line, :checker, :msg.
Mechanical entries from --check mode also carry :preview t.")
(defvar lo-check-only nil
- "Non-nil means run in report-only mode — no buffer writes.")
+ "Non-nil means run in report-only mode — no buffer writes.
+The CLI defaults this to t (a linter reports, it doesn't write);
+`--fix' is what enables writes on a command-line run.")
(defvar lo-current-file nil
"Path of the file currently being processed.")
(defvar lo-followups-file nil
@@ -355,24 +360,40 @@ logical row, matching wrap-org-table.el's grouping."
(defun lo--check-tables ()
"Scan the current buffer for org tables violating the table standard.
-Emits one judgment item per violating table."
+Emits one judgment item per violating table. Pipe-led lines inside
+#+begin_/#+end_ blocks are content (ASCII art, shell pipes), not tables,
+and are skipped — the same block rule `wot-process-file' applies."
(save-excursion
(goto-char (point-min))
- (while (re-search-forward "^[ \t]*|" nil t)
- (let ((start-line (line-number-at-pos))
- (lines nil))
- (beginning-of-line)
- (while (and (not (eobp)) (looking-at "[ \t]*|"))
- (push (buffer-substring-no-properties (line-beginning-position)
- (line-end-position))
- lines)
+ (let ((in-block nil)) ; the open block's type, e.g. "example" — nil outside
+ (while (not (eobp))
+ (cond
+ ;; Type-matched close only: literal #+end_src quoted inside an
+ ;; example block must not clear the flag (see wot-process-file).
+ ((and (not in-block)
+ (looking-at "^[ \t]*#\\+begin_\\([^ \t\n]+\\)"))
+ (setq in-block (downcase (match-string 1)))
+ (forward-line 1))
+ ((and in-block
+ (looking-at-p (format "^[ \t]*#\\+end_%s\\([ \t]\\|$\\)"
+ (regexp-quote in-block))))
+ (setq in-block nil)
(forward-line 1))
- (let ((violations (lo--table-violations (nreverse lines))))
- (when violations
- (lo--emit-judgment
- 'org-table-standard start-line
- (format "table violates the org-table standard: %s — wrap-org-table.el reflows it"
- (string-join violations "; ")))))))))
+ ((and (not in-block) (looking-at-p "^[ \t]*|"))
+ (let ((start-line (line-number-at-pos))
+ (lines nil))
+ (while (and (not (eobp)) (looking-at "[ \t]*|"))
+ (push (buffer-substring-no-properties (line-beginning-position)
+ (line-end-position))
+ lines)
+ (forward-line 1))
+ (let ((violations (lo--table-violations (nreverse lines))))
+ (when violations
+ (lo--emit-judgment
+ 'org-table-standard start-line
+ (format "table violates the org-table standard: %s — wrap-org-table.el reflows it"
+ (string-join violations "; ")))))))
+ (t (forward-line 1)))))))
;;; ---------------------------------------------------------------------------
;;; level-2 dated-header check (claude-rules/todo-format.md)
@@ -677,6 +698,13 @@ After printing, also append judgments to `lo-followups-file' when set."
;;; CLI
(defun lo-main ()
+ ;; Report-only is the CLI default; --fix is the only way a command-line run
+ ;; writes to disk. The old mutate-by-default reformatted five files in one
+ ;; pass before anyone confirmed anything (work project, 2026-07-09).
+ (setq lo-check-only t)
+ (when (member "--fix" command-line-args-left)
+ (setq lo-check-only nil)
+ (setq command-line-args-left (delete "--fix" command-line-args-left)))
(when (member "--check" command-line-args-left)
(setq lo-check-only t)
(setq command-line-args-left (delete "--check" command-line-args-left)))
@@ -688,7 +716,7 @@ After printing, also append judgments to `lo-followups-file' when set."
(setq command-line-args-left (delete followups command-line-args-left))))
(if (null command-line-args-left)
(progn
- (princ "Usage: emacs --batch -q -l lint-org.el [--check] [--followups-file=PATH] FILE.org ...\n")
+ (princ "Usage: emacs --batch -q -l lint-org.el [--fix] [--check] [--followups-file=PATH] FILE.org ...\n")
(kill-emacs 1))
(let ((files command-line-args-left))
(setq command-line-args-left nil)
@@ -707,7 +735,7 @@ this file without firing the CLI dispatch — under `ert-run-tests-batch-and-exi
the trailing args are things like `-f ert-run-tests-batch-and-exit'."
(and command-line-args-left
(cl-every (lambda (a)
- (cond ((member a '("--check")) t)
+ (cond ((member a '("--check" "--fix")) t)
((string-prefix-p "--followups-file=" a) t)
((string-prefix-p "-" a) nil)
(t (file-readable-p a))))
diff --git a/claude-templates/.ai/scripts/tests/lint-org-cli.bats b/claude-templates/.ai/scripts/tests/lint-org-cli.bats
index d457696..b9faef6 100644
--- a/claude-templates/.ai/scripts/tests/lint-org-cli.bats
+++ b/claude-templates/.ai/scripts/tests/lint-org-cli.bats
@@ -20,6 +20,24 @@ teardown() {
[[ "$output" == *"lint-org: file="* ]]
}
+@test "lint-org.el default invocation is report-only — file untouched" {
+ # bare #+begin_src is a mechanical fix (→ #+begin_example) that the old
+ # default applied on disk; a linter reports, it doesn't write
+ printf '* H\n\n#+begin_src\nx\n#+end_src\n' > "$TMPFILE"
+ before="$(cat "$TMPFILE")"
+ run emacs --batch -q -l "$SCRIPTS_DIR/lint-org.el" "$TMPFILE"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"would-fix"* ]]
+ [ "$(cat "$TMPFILE")" = "$before" ]
+}
+
+@test "lint-org.el --fix applies mechanical fixes on disk" {
+ printf '* H\n\n#+begin_src\nx\n#+end_src\n' > "$TMPFILE"
+ run emacs --batch -q -l "$SCRIPTS_DIR/lint-org.el" --fix "$TMPFILE"
+ [ "$status" -eq 0 ]
+ grep -q '#+begin_example' "$TMPFILE"
+}
+
@test "wrap-org-table.el loads and runs without -L on the load path" {
run emacs --batch -q -l "$SCRIPTS_DIR/wrap-org-table.el" --width=120 "$TMPFILE"
[ "$status" -eq 0 ]
diff --git a/claude-templates/.ai/scripts/tests/test-lint-org.el b/claude-templates/.ai/scripts/tests/test-lint-org.el
index d14879f..8e3e190 100644
--- a/claude-templates/.ai/scripts/tests/test-lint-org.el
+++ b/claude-templates/.ai/scripts/tests/test-lint-org.el
@@ -620,6 +620,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))
diff --git a/claude-templates/.ai/scripts/tests/test-wrap-org-table.el b/claude-templates/.ai/scripts/tests/test-wrap-org-table.el
index 8d1ecb6..0b3b375 100644
--- a/claude-templates/.ai/scripts/tests/test-wrap-org-table.el
+++ b/claude-templates/.ai/scripts/tests/test-wrap-org-table.el
@@ -186,3 +186,45 @@
(should (string-match-p "Prose before\\." content))
(should (string-match-p "Prose after\\." content))))
(delete-file file))))
+
+;;; ---------------------------------------------------------------------------
+;;; block safety — pipe lines inside #+begin_/#+end_ blocks are never tables
+
+(defconst wot-test--block-content
+ "#+begin_example
+| client |----->| server |
+| box | | box |
+#+end_example
+"
+ "An example block whose ASCII-art lines start with pipes.")
+
+(defun wot-test--process-content (content budget)
+ "Write CONTENT to a temp file, run `wot-process-file' at BUDGET, return result."
+ (let ((file (make-temp-file "wot-test" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file file (insert content))
+ (wot-process-file file budget)
+ (with-temp-buffer (insert-file-contents file) (buffer-string)))
+ (delete-file file))))
+
+(ert-deftest wot-process-file-leaves-example-block-byte-identical ()
+ (let ((content (concat "* Diagram\n\n" wot-test--block-content)))
+ (should (equal (wot-test--process-content content 120) content))))
+
+(ert-deftest wot-process-file-reformats-table-but-not-block ()
+ (let* ((content (concat "* Doc\n\n" wot-test--block-content "\n"
+ wot-test--wide-input))
+ (result (wot-test--process-content content 40)))
+ (should (string-match-p (regexp-quote wot-test--block-content) result))
+ (should (string-match-p (regexp-quote wot-test--wide-expected) result))))
+
+(ert-deftest wot-process-file-skips-pipes-in-src-block ()
+ (let ((content "* Pipeline\n\n#+begin_src sh\n| sort\n| uniq -c\n#+end_src\n"))
+ (should (equal (wot-test--process-content content 120) content))))
+
+(ert-deftest wot-process-file-literal-inner-end-marker-stays-in-block ()
+ "A literal #+end_src quoted inside an example block must not close it."
+ (let ((content (concat "* Doc\n\n#+begin_example\n#+begin_src sh\nx\n"
+ "#+end_src\n| art |----| art |\n#+end_example\n")))
+ (should (equal (wot-test--process-content content 120) content))))
diff --git a/claude-templates/.ai/scripts/wrap-org-table.el b/claude-templates/.ai/scripts/wrap-org-table.el
index ddbea65..173e44d 100644
--- a/claude-templates/.ai/scripts/wrap-org-table.el
+++ b/claude-templates/.ai/scripts/wrap-org-table.el
@@ -228,22 +228,40 @@ continuation lines merge back into their logical row before re-wrapping."
;;; file layer
(defun wot-process-file (file &optional budget)
- "Reformat every org table in FILE in place to BUDGET width."
+ "Reformat every org table in FILE in place to BUDGET width.
+Pipe-led lines inside #+begin_/#+end_ blocks (example, src, quote, …) are
+content, not tables — ASCII art in an example block once got mangled into a
+bordered table — so block regions are skipped verbatim."
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
- (while (re-search-forward "^[ \t]*|" nil t)
- (let ((start (line-beginning-position)))
- (while (and (not (eobp))
- (save-excursion (beginning-of-line)
- (looking-at "[ \t]*|")))
+ (let ((in-block nil)) ; the open block's type, e.g. "example" — nil outside
+ (while (not (eobp))
+ (cond
+ ;; Only the matching #+end_<type> closes a block: an example block
+ ;; often quotes literal #+begin_src/#+end_src lines, and a boolean
+ ;; flag would let that inner literal end-marker re-expose the rest
+ ;; of the block to reformatting.
+ ((and (not in-block)
+ (looking-at "^[ \t]*#\\+begin_\\([^ \t\n]+\\)"))
+ (setq in-block (downcase (match-string 1)))
(forward-line 1))
- (let* ((end (point))
- (table (buffer-substring-no-properties start end))
- (reformatted (wot-reformat-table-string table budget)))
- (delete-region start end)
- (goto-char start)
- (insert reformatted))))
+ ((and in-block
+ (looking-at-p (format "^[ \t]*#\\+end_%s\\([ \t]\\|$\\)"
+ (regexp-quote in-block))))
+ (setq in-block nil)
+ (forward-line 1))
+ ((and (not in-block) (looking-at-p "^[ \t]*|"))
+ (let ((start (point)))
+ (while (and (not (eobp)) (looking-at-p "^[ \t]*|"))
+ (forward-line 1))
+ (let* ((end (point))
+ (table (buffer-substring-no-properties start end))
+ (reformatted (wot-reformat-table-string table budget)))
+ (delete-region start end)
+ (goto-char start)
+ (insert reformatted))))
+ (t (forward-line 1)))))
(write-region (point-min) (point-max) file)))
;;; ---------------------------------------------------------------------------
@@ -289,7 +307,21 @@ so the ERT suite can `require' this file without firing the CLI dispatch."
(t (file-readable-p a))))
command-line-args-left)))
-(when (and noninteractive (wot--cli-invocation-p))
+(defun wot--entry-script-p ()
+ "Non-nil when wrap-org-table.el itself was named on the command line.
+lint-org.el `require's this file, and a load-triggered dispatch would run
+the table reformatter over lint-org's file arguments — that's how a lint
+invocation once reformatted the files it was only supposed to report on.
+Only dispatch when a -l/--load argument names this very file."
+ (and load-file-name
+ (cl-loop for (flag arg) on command-line-args
+ thereis (and (member flag '("-l" "--load"))
+ (stringp arg)
+ (file-exists-p arg)
+ (string= (file-truename (expand-file-name arg))
+ (file-truename load-file-name))))))
+
+(when (and noninteractive (wot--entry-script-p) (wot--cli-invocation-p))
(wot-main))
(provide 'wrap-org-table)