diff --git a/AGENTS.md b/AGENTS.md index de5c2753..6579f6db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,34 @@ ask first.** Writing to *another* repository is not an exception either. Do it only when told to work there. +### Do not add top-level files or folders + +**Never add a new directory or file at the repository root on your own.** The root layout is the +maintainer's. Work belongs inside an existing module or directory — Python under `api/`, the client +under `app/`, documents under `docs//`, CI under `.github/`, developer scripts under `tools/`, +temporary files under the git-ignored `.tmp/`. If a new top-level entry seems necessary, propose it +(what, why, and which existing directories you ruled out) and wait for approval; then add it to the +list below in the same change. + +Approved tracked root entries (`api/tests/test_repo_layout.py` fails on anything else): + +| Entry | Purpose | +|---|---| +| `api/`, `app/` | The model-serving API (Python) and the client (Kotlin) | +| `docs/` | Documents and the GitHub Pages guide | +| `tools/` | Developer tooling (release sync, web bundle sync) | +| `benchmarks/` | Measurement scripts | +| `script/` | Desktop UI driving script for the native-image build (`script/drive-desktop.ps1`) | +| `gradle/`, `build.gradle.kts`, `settings.gradle.kts`, `gradle.properties`, `gradlew`, `gradlew.bat` | Gradle build | +| `pyproject.toml` | Python project (uv) | +| `.github/` | CI workflows | +| `.idea/` | Shared IDE settings (partly tracked) | +| `README.md`, `AGENTS.md`, `PROJECT.md`, `LICENSE.md` | Root documents | +| `.gitignore`, `.gitattributes`, `.env.example` | Git and environment templates | + +Local, git-ignored entries that tools create are fine: `.tmp/`, `.worktrees/`, `.venv/`, `.gradle/`, +`.kotlin/`, `.env`, `local.properties`, `uv.lock`, `.claude/`, `.DS_Store`. + ## 3. Worktrees link large artefacts instead of copying them A worktree is a full checkout. Copying large untracked artefacts (prebuilt runtimes, vendored trees, diff --git a/api/tests/test_repo_layout.py b/api/tests/test_repo_layout.py new file mode 100644 index 00000000..123852aa --- /dev/null +++ b/api/tests/test_repo_layout.py @@ -0,0 +1,40 @@ +""" +The repository root holds only the entries AGENTS.md §2 approves; anything else is a new top-level +file or folder that was added without approval. +""" +import re +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def approved_entries(): + agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8") + table = agents.split("Approved tracked root entries", 1)[1].split("\n\n", 2)[1] + entries = set() + for row in table.splitlines()[2:]: # skip the header and the separator + first_cell = row.split("|")[1] + entries.update(name.rstrip("/") for name in re.findall(r"`([^`]+)`", first_cell)) + return entries + + +def tracked_root_entries(): + names = subprocess.run( + ["git", "ls-files"], cwd=ROOT, capture_output=True, text=True, check=True + ).stdout.splitlines() + return {name.split("/", 1)[0] for name in names} + + +def test_every_tracked_root_entry_is_approved(): + unapproved = tracked_root_entries() - approved_entries() + + assert unapproved == set(), ( + f"top-level entries not approved in AGENTS.md §2: {sorted(unapproved)} " + "— propose them and add them to the list, or move the work into an existing directory" + ) + + +def test_the_approved_list_is_read(): + assert {"api", "app", "docs", "AGENTS.md", "pyproject.toml"} <= approved_entries()