aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-15 23:03:10 -0500
committerCraig Jennings <c@cjennings.net>2026-05-15 23:03:10 -0500
commitdc1661c222304dddd797bece882bb2501d2b6e76 (patch)
tree58ebf60dc43980fef72ed3730941f68674cf1db9
parent8577a880bbb84879a9297ebd28d6370c5e62727d (diff)
downloadrulesets-dc1661c222304dddd797bece882bb2501d2b6e76.tar.gz
rulesets-dc1661c222304dddd797bece882bb2501d2b6e76.zip
fix(cj-scan): suppress detection inside nested non-cj begin_* blocks
cj-scan.py matched =#+begin_src cj:= / =#+end_src= line-by-line without awareness of enclosing block scopes. A cj fence embedded inside =#+begin_example= (typical when documenting what the <cj yasnippet emits) or =#+begin_src snippet= (the yasnippet definition itself) was misclassified as a live cj annotation. Two false positives surfaced from a /respond-to-cj-comments run against an org file with yasnippet docs. Track an active wrapper_type. When the scanner sees =#+begin_<type>= for any type other than cj: (the cj-open regex is checked first), enter a wrapper state where every line is content until the matching =#+end_<type>= closer fires. Inside a wrapper, both fence patterns and legacy inline cj: lines stay suppressed. Added the TestCjScanNestedFencesIgnored class with 6 tests: nesting inside example, src <other-lang>, and quote; regression guards for clean wrapper close and unclosed-wrapper non-swallow. Canonical pytest: 302 passed, 1 skipped.
-rw-r--r--claude-templates/.ai/scripts/cj-scan.py25
-rw-r--r--claude-templates/.ai/scripts/tests/test_cj_scan.py115
-rw-r--r--todo.org27
3 files changed, 167 insertions, 0 deletions
diff --git a/claude-templates/.ai/scripts/cj-scan.py b/claude-templates/.ai/scripts/cj-scan.py
index 54e2bf9..275f5ca 100644
--- a/claude-templates/.ai/scripts/cj-scan.py
+++ b/claude-templates/.ai/scripts/cj-scan.py
@@ -30,6 +30,7 @@ VALID_VERIFY_DEPTHS = {2, 3}
HEADING_RE = re.compile(r"^(\*+)\s+(.*)$")
SRC_OPEN_RE = re.compile(r"^\s*#\+begin_src\s+cj:\s*(\S*)\s*$", re.IGNORECASE)
SRC_CLOSE_RE = re.compile(r"^\s*#\+end_src\s*$", re.IGNORECASE)
+BLOCK_OPEN_RE = re.compile(r"^\s*#\+begin_(\w+)(?:\s.*)?$", re.IGNORECASE)
LEGACY_CJ_RE = re.compile(r"^\s*cj:\s*(.*)$")
VERIFY_KEYWORD_RE = re.compile(r"^VERIFY(\s|\[|$)")
@@ -66,6 +67,13 @@ def scan_file(path: Path) -> dict[str, object]:
block_label: str | None = None
block_body: list[str] = []
+ # Tracks a non-cj `#+begin_<type>` wrapper currently in scope. Inside a
+ # wrapper, cj fence patterns are *content* (documentation examples,
+ # quoted prose, snippet definitions) -- not annotations -- so we
+ # suppress matching until the wrapper closes. The closer is type-keyed:
+ # `#+end_example` for example, `#+end_src` for src, etc.
+ wrapper_type: str | None = None
+
file_str = str(path)
lines = path.read_text().splitlines()
@@ -90,6 +98,15 @@ def scan_file(path: Path) -> dict[str, object]:
block_body.append(line)
continue
+ if wrapper_type is not None:
+ wrapper_close_re = re.compile(
+ rf"^\s*#\+end_{re.escape(wrapper_type)}\s*$",
+ re.IGNORECASE,
+ )
+ if wrapper_close_re.match(line):
+ wrapper_type = None
+ continue
+
m_heading = HEADING_RE.match(line)
if m_heading:
depth = len(m_heading.group(1))
@@ -110,6 +127,9 @@ def scan_file(path: Path) -> dict[str, object]:
})
continue
+ # cj-open must be checked before the generic begin-block match: a
+ # `#+begin_src cj: ...` line matches both patterns, and cj-open is
+ # the more specific intent.
m_src_open = SRC_OPEN_RE.match(line)
if m_src_open:
in_cj_block = True
@@ -118,6 +138,11 @@ def scan_file(path: Path) -> dict[str, object]:
block_body = []
continue
+ m_block_open = BLOCK_OPEN_RE.match(line)
+ if m_block_open:
+ wrapper_type = m_block_open.group(1).lower()
+ continue
+
m_legacy = LEGACY_CJ_RE.match(line)
if m_legacy:
cj_blocks.append({
diff --git a/claude-templates/.ai/scripts/tests/test_cj_scan.py b/claude-templates/.ai/scripts/tests/test_cj_scan.py
index 7844474..22bd467 100644
--- a/claude-templates/.ai/scripts/tests/test_cj_scan.py
+++ b/claude-templates/.ai/scripts/tests/test_cj_scan.py
@@ -248,3 +248,118 @@ class TestCjScanVerifyPlacement:
)
result = run_scan(content)
assert result["verify_tasks"] == []
+
+
+# ----------------------------------------------------------------------
+# Nested-fence suppression (no false positives inside wrapper blocks)
+# ----------------------------------------------------------------------
+
+class TestCjScanNestedFencesIgnored:
+ """A cj fence nested inside another #+begin_<type> block is content, not an annotation.
+
+ Documentation patterns frequently embed the cj marker inside `#+begin_example`
+ blocks or inside `#+begin_src snippet` yasnippet definitions to *show* what
+ the marker looks like. A naive line-by-line scanner that only watches for
+ `#+begin_src cj:` and `#+end_src` matches those literally and misclassifies
+ the documentation as live annotations.
+ """
+
+ def test_cj_scan_fence_inside_begin_example_ignored(self, run_scan):
+ """Normal: cj fence inside #+begin_example ... #+end_example is documentation, not annotation."""
+ content = (
+ "* Section\n"
+ "Here is what the cj marker looks like:\n"
+ "#+begin_example\n"
+ "#+begin_src cj: comment\n"
+ "I am documentation, not a real annotation\n"
+ "#+end_src\n"
+ "#+end_example\n"
+ )
+ result = run_scan(content)
+ assert result["cj_blocks"] == []
+ assert result["unclosed_blocks"] == []
+
+ def test_cj_scan_fence_inside_begin_src_other_lang_ignored(self, run_scan):
+ """Normal: cj fence inside #+begin_src snippet (or any non-cj src block) is content.
+
+ The outer #+begin_src snippet block claims everything until the FIRST
+ matching #+end_src, so the inner `#+begin_src cj:` line is literal text
+ inside that block — not a nested cj annotation.
+ """
+ content = (
+ "* Section\n"
+ "#+begin_src snippet\n"
+ "# name: cj-comment-block\n"
+ "# --\n"
+ "#+begin_src cj: comment\n"
+ "$0\n"
+ "#+end_src\n"
+ "#+end_src\n"
+ )
+ result = run_scan(content)
+ assert result["cj_blocks"] == []
+
+ def test_cj_scan_fence_inside_begin_quote_ignored(self, run_scan):
+ """Boundary: cj fence inside #+begin_quote ... #+end_quote is quoted prose, not annotation."""
+ content = (
+ "* Section\n"
+ "#+begin_quote\n"
+ "#+begin_src cj: comment\n"
+ "quoted, not active\n"
+ "#+end_src\n"
+ "#+end_quote\n"
+ )
+ result = run_scan(content)
+ assert result["cj_blocks"] == []
+
+ def test_cj_scan_real_cj_after_example_block_still_detected(self, run_scan):
+ """Normal: after a wrapper block closes, a subsequent real cj fence is detected.
+
+ Regression guard: the wrapper-tracking state must reset when the
+ wrapper closes; otherwise everything after the first example block
+ would be silently swallowed.
+ """
+ content = (
+ "* Section\n"
+ "#+begin_example\n"
+ "#+begin_src cj: comment\n"
+ "doc, ignored\n"
+ "#+end_src\n"
+ "#+end_example\n"
+ "#+begin_src cj: comment\n"
+ "this one is real\n"
+ "#+end_src\n"
+ )
+ result = run_scan(content)
+ assert len(result["cj_blocks"]) == 1
+ assert result["cj_blocks"][0]["body"] == "this one is real"
+
+ def test_cj_scan_legacy_inline_inside_wrapper_ignored(self, run_scan):
+ """Boundary: legacy `cj: ...` line inside a wrapper block is content, not annotation."""
+ content = (
+ "* Section\n"
+ "#+begin_example\n"
+ "cj: this is a documentation example, not a real annotation\n"
+ "#+end_example\n"
+ )
+ result = run_scan(content)
+ assert result["cj_blocks"] == []
+
+ def test_cj_scan_unclosed_wrapper_does_not_swallow_rest_of_file_silently(self, run_scan):
+ """Error: an unclosed wrapper consumes everything after it — cj_blocks empty, not unclosed_blocks.
+
+ Pinning current behaviour: an unclosed `#+begin_example` is a separate
+ org-level malformation that this scanner doesn't currently report. The
+ guard here is that the unclosed-wrapper case doesn't produce
+ false-positive cj_blocks downstream.
+ """
+ content = (
+ "* Section\n"
+ "#+begin_example\n"
+ "#+begin_src cj: comment\n"
+ "should be content of the unclosed example\n"
+ "#+end_src\n"
+ "more content here, no end_example\n"
+ )
+ result = run_scan(content)
+ assert result["cj_blocks"] == []
diff --git a/todo.org b/todo.org
index 2160558..aea4ab1 100644
--- a/todo.org
+++ b/todo.org
@@ -1800,6 +1800,33 @@ Scope:
Origin: came up while authoring =triage-intake.org= on 2026-05-11.
* Rulesets Resolved
+** DONE [#C] Fix =cj-scan= false positives on cj fences nested inside other =#+begin_*= blocks :bug:
+CLOSED: [2026-05-15 Fri]
+
+=cj-scan.py= was matching =#+begin_src cj:= / =#+end_src= line-by-line
+without awareness of enclosing block scopes. A cj fence embedded inside a
+=#+begin_example= block (typically when documenting what the =<cj= yasnippet
+emits) or inside =#+begin_src snippet= (the yasnippet definition itself) was
+misclassified as a live cj annotation. Surfaced from a /respond-to-cj-comments
+run against the dotemacs =todo.org= that reported two false positives in the
+=<cj= yasnippet documentation.
+
+Fix: track an active =wrapper_type= state. When the scanner sees =#+begin_<type>=
+(for any =<type>= other than =cj:= via the more-specific cj-open regex, which
+is checked first), it enters a wrapper state where every line is treated as
+content until the matching =#+end_<type>= closer fires. Inside a wrapper, cj
+fence patterns and legacy inline =cj:= lines are both suppressed.
+
+Tests: added =TestCjScanNestedFencesIgnored= (6 tests) to
+=claude-templates/.ai/scripts/tests/test_cj_scan.py= covering nesting inside
+=#+begin_example=, =#+begin_src <other-lang>=, and =#+begin_quote=, plus
+regression guards that a wrapper closes cleanly (a subsequent real cj fence
+is still detected) and that an unclosed wrapper doesn't silently swallow
+later content into false-positive cj blocks.
+
+Full =make test-scripts= equivalent (=python3 -m pytest=): 302 passed, 1
+skipped, 0 failures.
+
** DONE [#A] Add =make doctor= — verify ~/.claude/ matches repo + settings.json :feature:
A drift detector that scans =~/.claude/= and reports anything inconsistent with what the repo expects. Single-command answer to "is my machine consistent with rulesets?"