|
| 1 | +"""Validate the PyAutoMemory repository layout. |
| 2 | +
|
| 3 | +The repo has exactly two content homes — `wiki/` (sub-wikis) and |
| 4 | +`bibliography/` (canonical BibTeX) — plus a small allowlisted set of root |
| 5 | +files and tooling folders. This lint fails on the historical failure modes: |
| 6 | +loose `.bib` files outside `bibliography/`, committed papers (PDF/HTML blobs, |
| 7 | +with or without a file extension), and unrecognised top-level entries. |
| 8 | +""" |
| 9 | + |
| 10 | +from __future__ import annotations |
| 11 | + |
| 12 | +import argparse |
| 13 | +import sys |
| 14 | +from pathlib import Path |
| 15 | + |
| 16 | +ROOT = Path(__file__).resolve().parents[1] |
| 17 | + |
| 18 | +ALLOWED_TOP_DIRS = { |
| 19 | + ".git", |
| 20 | + ".github", |
| 21 | + "bibliography", |
| 22 | + "scripts", |
| 23 | + "tests", |
| 24 | + "wiki", |
| 25 | +} |
| 26 | +ALLOWED_TOP_FILES = { |
| 27 | + ".gitignore", |
| 28 | + "AGENTS.md", |
| 29 | + "CLAUDE.md", |
| 30 | + "CONTRIBUTING.md", |
| 31 | + "LICENSE", |
| 32 | + "Makefile", |
| 33 | + "README.md", |
| 34 | + "index.md", |
| 35 | + "logo.png", |
| 36 | + "reading-queue.md", |
| 37 | +} |
| 38 | +BANNED_SUFFIXES = {".pdf", ".htm", ".html"} |
| 39 | +BINARY_MAGIC = (b"%PDF-",) |
| 40 | +BINARY_EXEMPT = {"logo.png"} |
| 41 | + |
| 42 | + |
| 43 | +def iter_repo_files(root: Path): |
| 44 | + for path in sorted(root.rglob("*")): |
| 45 | + if ".git" in path.relative_to(root).parts: |
| 46 | + continue |
| 47 | + if path.is_file(): |
| 48 | + yield path |
| 49 | + |
| 50 | + |
| 51 | +def validate_structure(root: Path) -> list[str]: |
| 52 | + errors: list[str] = [] |
| 53 | + |
| 54 | + for entry in sorted(root.iterdir()): |
| 55 | + name = entry.name |
| 56 | + if name == ".git": # a directory in a normal checkout, a file in a worktree |
| 57 | + continue |
| 58 | + if entry.is_dir(): |
| 59 | + if name not in ALLOWED_TOP_DIRS: |
| 60 | + errors.append( |
| 61 | + f"unexpected top-level directory: {name}/ " |
| 62 | + "(content belongs under wiki/ or bibliography/)" |
| 63 | + ) |
| 64 | + elif name not in ALLOWED_TOP_FILES: |
| 65 | + errors.append( |
| 66 | + f"unexpected top-level file: {name} " |
| 67 | + "(allowlist lives in scripts/validate_structure.py)" |
| 68 | + ) |
| 69 | + |
| 70 | + for path in iter_repo_files(root): |
| 71 | + rel = path.relative_to(root) |
| 72 | + if path.suffix == ".bib" and rel.parts[0] != "bibliography": |
| 73 | + errors.append(f"loose .bib file outside bibliography/: {rel}") |
| 74 | + if path.suffix.lower() in BANNED_SUFFIXES: |
| 75 | + errors.append(f"committed paper artifact (source PDFs live off-repo): {rel}") |
| 76 | + elif rel.name not in BINARY_EXEMPT: |
| 77 | + try: |
| 78 | + head = path.open("rb").read(8) |
| 79 | + except OSError: |
| 80 | + head = b"" |
| 81 | + if head.startswith(BINARY_MAGIC): |
| 82 | + errors.append(f"committed PDF content (source PDFs live off-repo): {rel}") |
| 83 | + |
| 84 | + return errors |
| 85 | + |
| 86 | + |
| 87 | +def main(argv: list[str] | None = None) -> int: |
| 88 | + parser = argparse.ArgumentParser(description=__doc__) |
| 89 | + parser.add_argument("--root", type=Path, default=ROOT) |
| 90 | + args = parser.parse_args(argv) |
| 91 | + |
| 92 | + errors = validate_structure(args.root.resolve()) |
| 93 | + for error in errors: |
| 94 | + print(f"ERROR: {error}") |
| 95 | + if errors: |
| 96 | + print(f"Repository structure is invalid ({len(errors)} problem(s)).") |
| 97 | + return 1 |
| 98 | + print("Repository structure is valid.") |
| 99 | + return 0 |
| 100 | + |
| 101 | + |
| 102 | +if __name__ == "__main__": |
| 103 | + sys.exit(main()) |
0 commit comments