Add AGENTS.md to make repo context auto-discoverable

Nothing in this repo was auto-read by an agent at the start of a session.
The .github/instructions files only attach once a matching file is edited,
which is too late to steer strategy, and the canonical docs set is not
discoverable without already knowing to look for it.

AGENTS.md routes rather than duplicates: it states the authority order
(docs/* first, docs/reviews/** explicitly non-canonical), the uv-only
command set, the test-enforced boundaries, and the change protocol.

The Traps section records failure modes this codebase has actually produced
rather than generic advice: the message/detail split that leaked paths in one
direction and degraded provenance in the other, the two competing atomicity
invariants in workflows.py where the obvious simplification breaks multi-page
durability, and the habit of changing a shared symbol without enumerating its
consumers.

Verified: cited test paths exist, full suite passes, ruff/ty clean.

Co-authored-by: Copilot App <[email protected]>
This commit is contained in:
Jim Lancaster
2026-08-23 19:23:26 -05:00
co-authored by Copilot App
parent 5ff66a8c40
commit 2c59cbd2c7
+107
View File
@@ -0,0 +1,107 @@
# 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.
- **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.