The repo is layered — every Python module sits in exactly one layer, and layer flow is one-way. Reverse imports are a CI failure (lint-imports job).
┌──────────────┐ ┌──────────────┐
│ src.api │ │ src.eval │ request handlers + eval runner
│ /api/v1/* │ │ pytest eval │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────────────────────┐
│ src.agent │ the LLM loop (tool-calling, CoT)
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ src.tools │ typed tool registry + implementations
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ src.data │ ingestion + queries (DB, files, …)
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ src.observability │ tracing / logging / spans
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ src.models │ Pydantic contracts (StrictModel)
│ (depends on nothing in src)│
└──────────────────────────────┘
Defined in pyproject.toml [tool.importlinter]:
-
Layered contract —
api | eval → agent → tools → data → observability → models. Modules at level N may import modules at level N+1 and below. Anything else fails. -
Forbidden contract —
src.modelsimports nothing fromsrc/. Models are leaf data; they neither know about the API surface nor reach back into observability. Keeping them isolated means schema bugs surface at construction, not via stack traces from deeper modules.
When the project grows a new layer (cache, queue, persistence-DTO mapper):
- Add the package under
src/. - Add it to the
layerslist in[tool.importlinter]in the right position. - Add a
tests/test_<layer>.pywith at least the unhappy-path tests. - Update the diagram above.
- Update
EXEMPT_WORKFLOWSin.github/scripts/check_required_contexts.pyonly if the layer ships its own CI job that should NOT be required.
The frontend (frontend/) is its own tree with its own quality gates (ESLint flat config + Prettier + tsc + Vitest); cross-tree imports are forbidden by build (Vite has no Python module resolver) and reviewed by hand.