diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-15 23:03:10 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-15 23:03:10 -0500 |
| commit | dc1661c222304dddd797bece882bb2501d2b6e76 (patch) | |
| tree | 58ebf60dc43980fef72ed3730941f68674cf1db9 /claude-templates/.ai/scripts/cj-scan.py | |
| parent | 8577a880bbb84879a9297ebd28d6370c5e62727d (diff) | |
| download | rulesets-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.
Diffstat (limited to 'claude-templates/.ai/scripts/cj-scan.py')
| -rw-r--r-- | claude-templates/.ai/scripts/cj-scan.py | 25 |
1 files changed, 25 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({ |
