From 5a1b944931157cd91be83512f893b482a4a323ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Thu, 7 May 2026 23:31:37 +0900 Subject: [PATCH] Add librarian Stage 1 oversize check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third Stage 1 piece: scripts/librarian_check_oversize.py walks non-archive markdown files and flags any over the 500-line soft cap declared in AGENTS.md. Wired into scripts/librarian.sh. Caught one real finding on first run: docs/performance.md at 914 lines. Splitting it into sub-topic notes is a separate cleanup task — surfaced for the user, not auto-applied. Updates docs/plans/librarian.md Progress + sets the next concrete step to a stale-plan checker. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/plans/librarian.md | 16 +++-- scripts/librarian.sh | 6 ++ scripts/librarian_check_oversize.py | 92 +++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 scripts/librarian_check_oversize.py diff --git a/docs/plans/librarian.md b/docs/plans/librarian.md index b30d990..1880c0d 100644 --- a/docs/plans/librarian.md +++ b/docs/plans/librarian.md @@ -134,6 +134,13 @@ operator applies the patch. - ✅ Stage 1 orchestrator: `scripts/librarian.sh`. Runs every Stage 1 check in order, aggregates exit code, prints findings inline. Single entry point for users and (future) cron. +- ✅ AGENTS.md mentions `scripts/librarian.sh` as the doc-lint entry + point in "Notes For Future Agents" (commit `0b363b5`). +- ✅ Stage 1, piece 3: `scripts/librarian_check_oversize.py`. Flags + any non-archive markdown file over the 500-line soft cap declared + in AGENTS.md. Caught one real finding on first run: + `docs/performance.md` at 914 lines — split into sub-topics deferred + as a separate task. ## Stage 1 Remaining Checks @@ -142,11 +149,10 @@ operator applies the patch. judgment is the open question). - MEMORY.md drift (index lines vs target file `description:` frontmatter). - Duplicate prose (high-overlap pairs across archive vs research). -- Oversize files (>500-line soft cap from AGENTS.md). ## Next Concrete Step -Mention `scripts/librarian.sh` in AGENTS.md so agents and humans know -it exists as the canonical Stage 1 entry point. After that, pick one -of the remaining checks above to add as the next checker (oversize -files is the cheapest; stale plans is the most useful). +Stale plan check (`scripts/librarian_check_stale_plans.py`). Walks +`docs/plans/*.md` and flags files whose mtime is older than N days +AND whose path hasn't appeared in `git log` over the same window. +This catches plans that drift out of mind without being archived. diff --git a/scripts/librarian.sh b/scripts/librarian.sh index ab212c2..e800626 100755 --- a/scripts/librarian.sh +++ b/scripts/librarian.sh @@ -21,6 +21,12 @@ if ! uv run python scripts/librarian_check_citations.py; then fi echo +echo "→ File size (500-line soft cap from AGENTS.md)" +if ! uv run python scripts/librarian_check_oversize.py; then + overall=1 +fi +echo + if [ "$overall" -eq 0 ]; then echo "All Stage 1 checks passed." else diff --git a/scripts/librarian_check_oversize.py b/scripts/librarian_check_oversize.py new file mode 100644 index 0000000..0fabb13 --- /dev/null +++ b/scripts/librarian_check_oversize.py @@ -0,0 +1,92 @@ +"""Oversize file check for librarian Stage 1. + +Walks markdown files and flags any whose line count exceeds the +soft cap declared in AGENTS.md ("Docs & Experiment Workflow" +section). Skips read-only archive doc directories — splitting them +isn't an option. + +Usage: + uv run python scripts/librarian_check_oversize.py +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +SOFT_CAP_LINES = 500 + +EXCLUDED_DIRS = { + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".venv", + "__pycache__", + "build", + "dist", + "node_modules", + "runs", + "target", + "tools", + "wheels", +} + +EXCLUDED_DOC_PREFIXES = ( + Path("docs/archive"), + Path("docs/plans/archive"), +) + + +def _repo_root() -> Path: + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "pyproject.toml").is_file(): + return parent + raise RuntimeError("pyproject.toml을 찾을 수 없어 repository root를 판정할 수 없습니다.") + + +def _is_excluded(rel: Path) -> bool: + if any(part in EXCLUDED_DIRS for part in rel.parts): + return True + return any(prefix in rel.parents for prefix in EXCLUDED_DOC_PREFIXES) + + +def _markdown_files(root: Path) -> list[Path]: + files: list[Path] = [] + for path in root.rglob("*.md"): + rel = path.relative_to(root) + if not _is_excluded(rel): + files.append(path) + return sorted(set(files)) + + +def _line_count(path: Path) -> int: + with path.open(encoding="utf-8", errors="replace") as f: + return sum(1 for _ in f) + + +def main() -> int: + root = _repo_root() + files = _markdown_files(root) + if not files: + print("검사할 Markdown 파일이 없습니다.", file=sys.stderr) + return 1 + + over = [(path, _line_count(path)) for path in files] + over = [(p, n) for p, n in over if n > SOFT_CAP_LINES] + + if not over: + print(f"All markdown files under {SOFT_CAP_LINES}-line soft cap.") + return 0 + + over.sort(key=lambda item: -item[1]) + print(f"Files over {SOFT_CAP_LINES}-line soft cap:") + for path, n in over: + rel = path.relative_to(root) + print(f" {n:5d} {rel}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())