Ctrl K
ComunidadGuías…

scripts/ai_guard/file_size_check.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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())