utils/file_length/exemptions.py
Krzysztof kuhy Rudnicki 085d08b5c9 Split the two shell scripts and the gate's own config
aur-publish/publish.sh   439 -> 148 + lib/aur_registration 107
                                      + lib/aur_verify 122 + lib/aur_publish_one 107
  guard-lib/lib/file_guard.sh  350 -> 140 + file_guard_unlock 214
  file_length/config.py        252 -> 45 + _tables 202 + exemptions 77

The libs are sourced, never executed: they read the readonly config the entry
script declares, and a `readonly` re-run inside a twice-sourced lib aborts
under `set -e`. Verified by function parity rather than by eye — publish.sh
resolves the same 17 functions as before the split, guardctl the same 15 —
plus `shellcheck -x` clean on every file and a real `--help` / bad-option run.

file_length is the gate's own code, so a broken import there turns the gate
into a no-op. Split three ways to avoid an import cycle without a `# noqa`:
_tables holds the data, exemptions the rules that read it, config re-exports
both so `from file_length.config import is_generated` still resolves. The
first attempt was wrong — EXCLUDED_DIRS was left behind and every invocation
raised — caught because the gate was re-run in both modes rather than
assumed. Now verified: exit 0 on a clean file, exit 1 on an over-cap one,
--explain intact, and the same 61 exemption decisions as HEAD.

Also deletes baseline-2026-08-14.json and SURVEY-2026-08-14.txt: the task
forbids a baseline file, and nothing in the tree read either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:59:27 +02:00

83 lines
2.8 KiB
Python

"""Predicates deciding whether a file is exempt from the 250-line cap.
Split from :mod:`file_length.config`, which holds the tables these read.
Keeping the rules separate from the data they consult means a new exemption
is a table entry, not a new branch, and the gate and the survey keep sharing
one definition of "exempt".
Re-exported from :mod:`file_length.config`, so existing
``from file_length.config import is_generated`` imports keep working.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from file_length._tables import (
DATA_TXT_MAX_MEAN_LINE,
EXCLUDED_DIRS,
GENERATED_MARKERS,
GENERATED_PATTERN,
SESSION_ARTIFACTS_ANYWHERE,
THIRD_PARTY_REPOS,
VENDORED_ANYWHERE,
VENDORED_SUBPATHS,
)
if TYPE_CHECKING:
from pathlib import Path
def is_generated(path: Path) -> bool:
"""True if the file is tool-generated and must not be split by hand."""
if GENERATED_PATTERN.search(path.as_posix()):
return True
try:
with path.open("r", encoding="utf-8", errors="replace") as handle:
head = "".join(next(handle, "") for _ in range(5))
except OSError:
return False
return any(marker in head for marker in GENERATED_MARKERS)
def is_data_text(path: Path, lines: int, size: int) -> bool:
"""True for wordlist-style .txt: many lines, very short ones."""
if path.suffix.lower() != ".txt" or lines <= 0:
return False
return (size / lines) < DATA_TXT_MAX_MEAN_LINE
def is_vendored(path: Path) -> bool:
"""True if the path sits under a known vendored subtree or excluded dir.
Callers must pass an absolute path (see `check.absolutize`). Every rule
below needs the repo-name context that a repo-relative path has already
thrown away: '/.claude/skills/' cannot match 'skills/x.md', so a relative
path would quietly skip the exemption instead of applying it.
"""
parts = path.parts
if any(part in EXCLUDED_DIRS for part in parts):
return True
posix = path.as_posix()
if any(marker in posix for marker in VENDORED_ANYWHERE):
return True
for repo, subs in VENDORED_SUBPATHS.items():
for sub in subs:
if f"/{repo}/{sub}/" in posix:
return True
return any(f"/{repo}/" in posix for repo in THIRD_PARTY_REPOS)
def is_session_artifact(path: Path) -> bool:
"""True for a frozen agent-session record (a plan or a session log).
Kept separate from `is_vendored` so the gate reports why a file is exempt:
these are kuhy's own files, not third-party code.
Callers must pass an absolute path (see `check.absolutize`), for the same
reason `is_vendored` does -- a repo-relative path has already dropped the
leading directory these markers match on.
"""
posix = path.as_posix()
return any(marker in posix for marker in SESSION_ARTIFACTS_ANYWHERE)