"""Shared configuration for the 250-line file-length cap. Single source of truth for the cap and its exemptions, imported by both the pre-commit gate (`check.py`) and the survey. Duplicating these lists in shell is what lets a gate and a survey silently disagree. """ from __future__ import annotations from pathlib import Path import re #: Hard cap. Files above this fail the gate. Code and prose alike. MAX_LINES = 250 #: Extensions the cap applies to: real programming languages plus prose. #: Markup (.html/.css/.scss) is deliberately absent -- templates split into #: partials, not functions, so a line cap does not express anything useful. CAPPED_EXTENSIONS = { # code ".py", ".dart", ".ts", ".tsx", ".js", ".jsx", ".sh", ".bash", ".zsh", ".c", ".h", ".cpp", ".cc", ".hpp", ".go", ".rs", ".java", ".kt", ".kts", ".swift", ".rb", ".lua", ".sql", ".vue", ".cs", ".php", # prose ".md", ".markdown", ".txt", ".rst", ".tex", ".org", } #: Generated files: regenerated by a tool, so splitting them is meaningless #: (the tool would just rewrite the original next run). GENERATED_PATTERN = re.compile( r"\.g\.dart$" r"|\.freezed\.dart$" r"|\.gr\.dart$" r"|\.config\.dart$" r"|\.mocks\.dart$" r"|\.min\.(js|css)$" r"|\.pb\.(go|py)$" r"|_pb2(_grpc)?\.py$" r"|\.generated\.ts$" r"|\.d\.ts$" r"|/l10n/generated/" r"|/generated/" ) #: A marker in the first few lines also marks a file generated. Covers tools #: that do not use a distinguishing extension. GENERATED_MARKERS = ( "GENERATED CODE - DO NOT MODIFY", "@generated", "DO NOT EDIT", "Autogenerated", "AUTOGENERATED", "generated by", ) #: Directory names never descended into: dependencies, build output, caches. EXCLUDED_DIRS = frozenset( { ".git", "node_modules", ".dart_tool", "build", ".venv", "venv", "env", "site-packages", "target", "ephemeral", "__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache", "dist", ".next", "vendor", "third_party", "Pods", ".gradle", ".idea", ".cxx", "coverage", "htmlcov", ".tox", "Library", "Temp", "obj", ".terraform", ".fvm", ".angular", ".cache", ".svelte-kit", "migrations", ".ci-mirror-venv", ".hippo", ".build-deps", # Where CI checks out the shared gate itself (kuhyx/utils). Without # this, a repo's lint job fails on utils' own long files -- a gate # that fails for reasons the repo being gated cannot fix. ".utils", } ) #: Third-party content living *inside* a repo you own, keyed by repo dir name. #: Verified by git remote / vendor manifest, not guessed. VENDORED_SUBPATHS = { "mcp-servers": ("servers",), "dufs-cloud": ("app/assets/pdfjs", "web/assets/pdfjs"), ".claude": ( "projects", "shell-snapshots", "cache", "backups", "todos", "statsig", "history", "chrome", "downloads", "plugins", "ide", ), # Agent skill bundles vendored from Anthropic (todo: commit f91311f). # A SKILL.md is a fixed-name file with frontmatter; detail belongs in a # sibling references/ dir, so splitting one into SKILL.part2.md would # break the format rather than improve readability. "todo": (".agents/skills",), } #: Directory names that mark a vendored agent-skill tree wherever they appear. #: Keyed by name rather than repo because these bundles get dropped into any #: repo that wants them, not just the one they were first seen in. VENDORED_ANYWHERE = ("/.agents/skills/", "/.claude/skills/") #: Frozen records of past agent sessions: an implementation plan or an #: append-only session log describes what was done at one moment. Splitting one #: rewrites a historical record for no reading benefit -- nobody navigates them, #: they are read whole or not at all. Same category as generated files. #: #: Deliberately NOT the whole of docs/superpowers/: `specs/` and `contracts/` #: stay capped, because those are living documents that get re-read and edited. SESSION_ARTIFACTS_ANYWHERE = ( "/docs/superpowers/plans/", "/docs/superpowers/sessions/", ) #: Whole repos under ~ that are third-party clones, not kuhy's work. #: Verified 2026-08-14 by `git remote get-url origin` + commit authors. THIRD_PARTY_REPOS = frozenset( { "StringTheory", # github.com/AnthonySf/TheStringTheory "reaper-mcp", # github.com/TwelveTake-Studios/reaper-mcp "freesound-mcp", # github.com/timjrobinson/FreesoundMCPServer "llama_cpp_dart_local", # generated FFI bindings "aseprite-mcp", "Mapperatorinator", # github.com/OliBomby/Mapperatorinator "aur", "build-your-own-x", "build_your_x", "byox-ladder", "warriorjs", "untools", "awesome-mcp-explorer", "runnerup", "depotdownloader", "actions-runner", } ) #: A `.txt` whose mean line is shorter than this is data (a wordlist, a #: coordinate dump), not prose, so the cap does not apply. DATA_TXT_MAX_MEAN_LINE = 25 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)