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]>
5.6 KiB
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.
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:
docs/*— canonical. Start atdocs/index.md, which defines the reading order.docs/invariant/*holds cross-version rules that outlive any release..github/instructions/*.md— active steering, auto-attached when you edit matching paths. Covers services, UI, error handling, and documentation sync..github/skills/*— periodic audit procedures (code review, provenance, test effectiveness).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.mdstays field-accurate withdb/models.py.- Orphans are tracked, not tolerated —
test_orphan_sweep.pyrecords each retained orphan with rationale inKNOWN_ORPHANS.
Traps
Non-obvious things that have already caused real bugs here:
AppError.messagevsAppError.detail.messageis user/API-facing and must stay generic — never put exception text or filesystem paths in it.detailis internal-only and is what reaches logs andExecutionAttempt.error_detail. Putting root-cause data inmessageleaks; removing it fromdetailsilently degrades provenance. Seedocs/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.pyandtests/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
isortruns withforce-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.