#!/usr/bin/env python3 """G6: 文件大小守卫。 检查 .py 文件是否超过项目约定的行数上限: - >800 行:警告(项目约定应拆分) - >1200 行:阻止(必须拆分才能提交) 使用方式: # 检查所有已 stage 的 .py 文件 python3 scripts/ai_guard/file_size_check.py --staged # 检查指定文件 python3 scripts/ai_guard/file_size_check.py path/to/file.py # 检查整个 butler/ 目录(CI 模式) python3 scripts/ai_guard/file_size_check.py --ci 退出码: 0 = 通过(或仅有警告) 1 = 有文件超过 1200 行(硬限制) """ from __future__ import annotations import argparse import os import subprocess import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent.parent # 项目约定:>800 行警告,>1200 行必须拆分 WARN_LINES = 800 BLOCK_LINES = 1200 # 已知的大文件白名单(历史遗留,不在本次拆分范围) WHITELIST: set[str] = { # 这些是大型 legacy 文件,已被列入待拆分 backlog } def _count_lines(file_path: Path) -> int: """统计文件行数(包括空行)。""" try: with file_path.open("r", encoding="utf-8", errors="replace") as f: return sum(1 for _ in f) except OSError: return 0 def _get_staged_py_files() -> list[Path]: """获取已 stage 的 .py 文件。""" try: result = subprocess.run( ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"], cwd=REPO_ROOT, capture_output=True, text=True, check=True, ) except (subprocess.CalledProcessError, FileNotFoundError): return [] files: list[Path] = [] for line in result.stdout.splitlines(): if line.endswith(".py"): path = REPO_ROOT / line if path.exists(): files.append(path) return files def _get_all_butler_py_files() -> list[Path]: """获取 butler/ 下所有 .py 文件(CI 模式)。""" return sorted((REPO_ROOT / "butler").rglob("*.py")) def _check_files(files: list[Path]) -> tuple[list[tuple[Path, int]], list[tuple[Path, int]]]: """检查文件列表,返回 (warnings, blocks)。""" warnings: list[tuple[Path, int]] = [] blocks: list[tuple[Path, int]] = [] for f in files: try: rel = f.relative_to(REPO_ROOT) rel_str = rel.as_posix() except ValueError: continue if rel_str in WHITELIST: continue line_count = _count_lines(f) if line_count > BLOCK_LINES: blocks.append((rel, line_count)) elif line_count > WARN_LINES: warnings.append((rel, line_count)) return warnings, blocks def main() -> int: parser = argparse.ArgumentParser(description="文件大小守卫") group = parser.add_mutually_exclusive_group() group.add_argument("--staged", action="store_true", help="检查已 stage 的文件") group.add_argument("--ci", action="store_true", help="CI 模式:检查整个 butler/ 目录") parser.add_argument("files", nargs="*", help="指定文件路径") args = parser.parse_args() if args.staged: files = _get_staged_py_files() elif args.ci: files = _get_all_butler_py_files() elif args.files: files = [Path(f).resolve() for f in args.files] else: parser.print_help() return 0 if not files: return 0 warnings, blocks = _check_files(files) if warnings: print(f"⚠️ 以下文件超过 {WARN_LINES} 行(建议拆分):", file=sys.stderr) for path, n in warnings: print(f" {n:5d} 行 {path}", file=sys.stderr) if blocks: print(f"❌ 以下文件超过 {BLOCK_LINES} 行(必须拆分):", file=sys.stderr) for path, n in blocks: print(f" {n:5d} 行 {path}", file=sys.stderr) print("", file=sys.stderr) print( f"项目约定 .py 文件不超过 {BLOCK_LINES} 行。请将大文件拆分为包:", file=sys.stderr, ) print(" 1. 创建同名包目录", file=sys.stderr) print(" 2. 将代码拆分为逻辑子模块", file=sys.stderr) print(" 3. 原文件改为 shim(保留 __all__ + DeprecationWarning)", file=sys.stderr) print(" 4. 运行测试验证功能不变", file=sys.stderr) return 1 if not warnings: print(f"✅ 文件大小守卫通过({len(files)} 个文件)", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())