From 3ba0dfe60f83acc2ab99293aa0d940cb43a2f491 Mon Sep 17 00:00:00 2001 From: Miyamura80 Date: Wed, 25 Mar 2026 00:04:12 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=A8=20add=20file=20length=20CI=20c?= =?UTF-8?q?heck=20to=20prevent=20monolithic=20AI-generated=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 500-line limit on Python files enforced via `make file_len_check` (included in `make ci`) and the linter GitHub Actions workflow. Files exceeding the limit can be added to [tool.file_length] exclude in pyproject.toml. Includes a Cursor rule documenting the rationale. Co-Authored-By: Claude Opus 4.6 (1M context) --- .cursor/rules/file_length.mdc | 26 +++++++++ .github/workflows/linter_require_ruff.yaml | 2 + Makefile | 7 ++- pyproject.toml | 6 +++ scripts/check_file_length.py | 63 ++++++++++++++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 .cursor/rules/file_length.mdc create mode 100644 scripts/check_file_length.py diff --git a/.cursor/rules/file_length.mdc b/.cursor/rules/file_length.mdc new file mode 100644 index 0000000..6c4b236 --- /dev/null +++ b/.cursor/rules/file_length.mdc @@ -0,0 +1,26 @@ +--- +description: File length limits for Python files +globs: *.py +alwaysApply: false +--- +## File Length Limit + +Python files must not exceed 500 lines. This is enforced by `make file_len_check` (part of `make ci`) and in the linter GitHub Actions workflow. + +This limit exists to prevent AI-generated code from becoming monolithic and forces refactoring into smaller, more modular components. When a file approaches 500 lines: + +1. Extract related functions into a new module in the same package +2. Use the existing package structure (src/, utils/, common/) to organize extracted code +3. Prefer composition over inheritance when splitting classes + +If a file genuinely needs to exceed 500 lines, add it to the `exclude` list in `pyproject.toml` under `[tool.file_length]`: + +```toml +[tool.file_length] +max_lines = 500 +exclude = [ + "onboard.py", +] +``` + +Do not add files to the exclude list without a clear justification. diff --git a/.github/workflows/linter_require_ruff.yaml b/.github/workflows/linter_require_ruff.yaml index c9c7f89..e21aa94 100644 --- a/.github/workflows/linter_require_ruff.yaml +++ b/.github/workflows/linter_require_ruff.yaml @@ -38,3 +38,5 @@ jobs: run: uv run python scripts/check_ai_writing.py - name: Run import-linter run: make import_lint + - name: Run file length check + run: uv run python scripts/check_file_length.py diff --git a/Makefile b/Makefile index 6435c37..7fbdeb9 100644 --- a/Makefile +++ b/Makefile @@ -248,7 +248,12 @@ check_deps: install_tools ## Check for unused dependencies @uv run deptry . @echo "$(GREEN)✅Dependency check completed.$(RESET)" -ci: ruff vulture import_lint ty docs_lint lint_links check_deps ## Run all CI checks (ruff, vulture, import_lint, ty, docs_lint, lint_links) +file_len_check: check_uv ## Check Python files don't exceed max line count + @echo "$(YELLOW)🔍Checking file lengths...$(RESET)" + @uv run python scripts/check_file_length.py + @echo "$(GREEN)✅File length check completed.$(RESET)" + +ci: ruff vulture import_lint ty docs_lint lint_links check_deps file_len_check ## Run all CI checks (ruff, vulture, import_lint, ty, docs_lint, lint_links, file_len_check) @echo "$(GREEN)✅CI checks completed.$(RESET)" ######################################################## diff --git a/pyproject.toml b/pyproject.toml index d4a1f37..97525f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,12 @@ exclude = [ "onboard.py" ] +[tool.file_length] +max_lines = 500 +exclude = [ + "onboard.py", +] + [tool.coverage.run] branch = true source = ["src", "common", "utils"] diff --git a/scripts/check_file_length.py b/scripts/check_file_length.py new file mode 100644 index 0000000..d520493 --- /dev/null +++ b/scripts/check_file_length.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import pathlib +import tomllib + +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent +ROOT_SKIP_DIRS = { + ".git", + ".venv", + ".uv_cache", + ".uv-cache", + ".uv_tools", + ".uv-tools", + ".cache", + "node_modules", + ".next", +} +RECURSIVE_SKIP_DIRS = {"__pycache__", ".pytest_cache"} + + +def load_config() -> tuple[int, set[str]]: + pyproject = REPO_ROOT / "pyproject.toml" + with open(pyproject, "rb") as f: + data = tomllib.load(f) + cfg = data.get("tool", {}).get("file_length", {}) + max_lines = cfg.get("max_lines", 500) + exclude = set(cfg.get("exclude", [])) + return max_lines, exclude + + +def main() -> int: + max_lines, exclude = load_config() + violations: list[tuple[pathlib.PurePosixPath, int]] = [] + + for path in REPO_ROOT.rglob("*.py"): + rel = path.relative_to(REPO_ROOT) + parts = rel.parts + if parts[0] in ROOT_SKIP_DIRS: + continue + if any(part in RECURSIVE_SKIP_DIRS for part in parts[:-1]): + continue + if str(rel) in exclude: + continue + line_count = len(path.read_text(encoding="utf-8", errors="ignore").splitlines()) + if line_count > max_lines: + violations.append((rel, line_count)) + + if violations: + print(f"File length check failed: {len(violations)} file(s) exceed {max_lines} lines") + for rel_path, count in sorted(violations): + print(f" {rel_path}: {count} lines") + print( + "Refactor large files into smaller modules, " + "or add to [tool.file_length] exclude in pyproject.toml." + ) + return 1 + + print(f"File length check passed (all files <= {max_lines} lines).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From dc68c4782a798adaedb52f6548d6ff3ef8ea4718 Mon Sep 17 00:00:00 2001 From: Miyamura80 Date: Wed, 25 Mar 2026 00:07:39 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20address=20PR=20review=20feed?= =?UTF-8?q?back:=20cross-platform=20path=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use pathlib.Path instead of PurePosixPath in type annotation - Use rel.as_posix() for exclude matching to handle Windows paths Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/check_file_length.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check_file_length.py b/scripts/check_file_length.py index d520493..29f024a 100644 --- a/scripts/check_file_length.py +++ b/scripts/check_file_length.py @@ -30,7 +30,7 @@ def load_config() -> tuple[int, set[str]]: def main() -> int: max_lines, exclude = load_config() - violations: list[tuple[pathlib.PurePosixPath, int]] = [] + violations: list[tuple[pathlib.Path, int]] = [] for path in REPO_ROOT.rglob("*.py"): rel = path.relative_to(REPO_ROOT) @@ -39,7 +39,7 @@ def main() -> int: continue if any(part in RECURSIVE_SKIP_DIRS for part in parts[:-1]): continue - if str(rel) in exclude: + if rel.as_posix() in exclude: continue line_count = len(path.read_text(encoding="utf-8", errors="ignore").splitlines()) if line_count > max_lines: From 450568c874cb2b33aa1328c1676ec861bda45420 Mon Sep 17 00:00:00 2001 From: Miyamura80 Date: Wed, 25 Mar 2026 08:53:01 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20remove=20refactoring?= =?UTF-8?q?=20guidance=20from=20cursor=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .cursor/rules/file_length.mdc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.cursor/rules/file_length.mdc b/.cursor/rules/file_length.mdc index 6c4b236..d01369c 100644 --- a/.cursor/rules/file_length.mdc +++ b/.cursor/rules/file_length.mdc @@ -7,11 +7,7 @@ alwaysApply: false Python files must not exceed 500 lines. This is enforced by `make file_len_check` (part of `make ci`) and in the linter GitHub Actions workflow. -This limit exists to prevent AI-generated code from becoming monolithic and forces refactoring into smaller, more modular components. When a file approaches 500 lines: - -1. Extract related functions into a new module in the same package -2. Use the existing package structure (src/, utils/, common/) to organize extracted code -3. Prefer composition over inheritance when splitting classes +This limit exists to prevent AI-generated code from becoming monolithic and forces refactoring into smaller, more modular components. If a file genuinely needs to exceed 500 lines, add it to the `exclude` list in `pyproject.toml` under `[tool.file_length]`: