# AGENTS.md Orientation for AI agents working in this repository. This file is a **router**, not a spec: it points at canonical authority and flags the traps that are expensive to discover by trial. Where this file and `docs/*` disagree, `docs/*` wins. ## What This Is A document transcription system that preserves durable archival records (Documents, Sources, People) and executes page transcription asynchronously through vision/LLM providers. Its defining constraint is **evidence**: every machine attempt is recorded append-only with request/response provenance. Features that would lose, mutate, or obscure that history are wrong regardless of how convenient they are. Stack: Python 3.12+ · FastAPI + NiceGUI · SQLModel/SQLAlchemy (SQLite-first, PostgreSQL- compatible) · Pydantic V2 · asyncio worker · OpenRouter adapter. ## Commands This is a `uv` project. **Nothing is on `PATH`** — `ruff`, `ty`, and `pytest` all require `uv run`. Bare invocations fail with command-not-found. ```bash uv run ruff check . # lint (blocking in pre-commit) uv run ruff format --check . # format (blocking in pre-commit) uv run ty check # types (blocking in pre-commit) uv run pytest -q -m "not external" # default verification run ``` `external` marks tests that hit live services; always exclude it unless explicitly asked. All four commands are expected to pass clean — there is no tolerated baseline of failures. If `ty` reports something, fix it or suppress it inline *with a rationale comment*; a bare `ignore` will not survive review. ## Authority Order Resolve every question in this order, and stop at the first that answers it: 1. **`docs/*`** — canonical. Start at [`docs/index.md`](docs/index.md), which defines the reading order. `docs/invariant/*` holds cross-version rules that outlive any release. 2. **`.github/instructions/*.md`** — active steering, auto-attached when you edit matching paths. Covers services, UI, error handling, and documentation sync. 3. **`.github/skills/*`** — periodic audit procedures (code review, provenance, test effectiveness). 4. **`tests/`** — deterministic enforcement. A guard test is the ground truth for whatever rule it encodes. `docs/reviews/**` is **not** canonical. Those are dated, opinionated snapshots that were accurate when written and may since have been fixed, superseded, or found wrong. ## Layout | Path | Role | | :--- | :--- | | `src/transcription/ui/**`, `api/**` | Interface. No direct persistence access. | | `src/transcription/services/**` | Domain logic and transaction ownership. | | `src/transcription/db/**` | Models and persistence. | | `src/transcription/providers/**` | Provider adapters; provider details stop here. | | `src/transcription/worker.py` | Asyncio worker loop. | | `tests/` | Includes boundary/contract guards, not just behavior tests. | ## Enforced Boundaries These are not conventions — a test fails if you break them: - **No service-to-service imports** (`test_service_boundaries.py`). Compose in the caller. - **No persistence access from pages/components** (`test_ui_boundaries.py`, allowlist-based). - **No hand-rolled error notifications in UI** — use the shared error presenter. - **No stringly-typed status literals** — use the enums (`test_model_contract_guards.py`). - **Attempt history is append-only** (`test_v42_evidence.py`). - **`docs/schema.md` stays field-accurate** with `db/models.py`. - **Orphans are tracked, not tolerated** — `test_orphan_sweep.py` records each retained orphan with rationale in `KNOWN_ORPHANS`. ## Traps Non-obvious things that have already caused real bugs here: - **`AppError.message` vs `AppError.detail`.** `message` is user/API-facing and must stay generic — never put exception text or filesystem paths in it. `detail` is internal-only and is what reaches logs and `ExecutionAttempt.error_detail`. Putting root-cause data in `message` leaks; removing it from `detail` silently degrades provenance. See `docs/error_handling.md`. - **Two competing atomicity invariants in `services/workflows.py`.** Intermediate pages must commit individually (durability across a long multi-page job); the *final* page must commit atomically with the terminal job status. Collapsing the batch into one transaction satisfies the second and destroys the first. Both are guarded — `test_workflows_reliability.py` and `tests/integration/test_pipeline_atomicity.py`. - **Shared symbols have more consumers than the obvious one.** Before changing a model field, exception attribute, or helper return value, grep for every consumer including tests. Evidence and logging paths frequently read the same fields the UI does. - **`Tag` is owned by two services, on purpose.** Every other model has exactly one owning service, so the ownership rule reads as absolute — it isn't. `Tag` is a single table reached through two `RegistryService[Tag]` facades, `TagRegistry` (documents) and `PersonTagRegistry` (people), which count usage through `DocumentTag` and `PersonTag` respectively. Changing tag semantics through one facade silently changes the other. Consolidating them under one service is not a cleanup; it makes the other side a cross-aggregate writer. - **Import style:** ruff `isort` runs with `force-single-line = true`. One import per line. - **Latent defects have ordering constraints.** Some code is unreachable only because of a current setting or single-instance deployment. Fix it *before* the change that unblocks it, not after. ## Change Protocol - **Write the failing test first** for behavioral fixes, and confirm it actually fails for the reason you think. Several bugs here were subtle enough that a test written afterward would have passed against the broken code. - **Update docs in the same change** when you alter a contract, behavior, or scope — see `.github/instructions/documentation-sync.instructions.md`. - **Do not commit unless asked.** Making a requested change is not consent to commit it. - **Do not push or open PRs on your own initiative.** - **Scope discipline:** fix what was asked plus what your change genuinely breaks. Pre-existing unrelated issues are a separate conversation.