* docs(agents): AGENTS.md is the single guide, contract first, no state snapshots CLAUDE.md now only imports AGENTS.md so Claude Code, Codex and Cursor read one file. AGENTS.md is rewritten around what an agent cannot learn from the code: - Non-negotiables move to the top and are corrected against the code. "Own key always wins, never metered, never routed or overflowed" is promoted to rule 1. The relay rule now scopes to plain /call/ and names routed endpoints and overflow as wrappers, since route.py injects `_treg` into the body and overflow adds X-Treg-Served-Via; the old wording contradicted both. The hold rule defines "hold"; the pool rule says why reserve and settle are two transactions; the ledger rule records that there is deliberately no refund entry. - The dataplane write allowlist becomes guidance that points at tests/test_call_architecture.py as the authority instead of a prose copy. - Enforcement gap inventories, per-file commit lists, contract-by-contract recaps and the CLI-module list are removed: they duplicate pyproject, the import-boundaries fragment and the tests, and rot without a drift check. - Endpoint and provider counts are removed (three files carried three values). - Duplicated statements (own-key, faithful relay, keep-four-in-step, money-only path, relay guard, secrets) are stated once each. - Sections reordered: contract, where the truth lives, architecture, development, working agreement, and a closing section for user-facing copy. * build(uv): pin required-version instead of banning `uv lock` uv.lock is revision 3, first written by uv 0.8.4. An older uv reads it fine but rewrites it to revision 2 on any touch, dropping every upload-time field: the ~650-line no-op diff the old "always --frozen, never uv sync or uv lock" rule worked around. That rule also told agents to hand-edit the lock, which --frozen would then install unchecked. - pyproject.toml: `[tool.uv] required-version = ">=0.12"`, so an old uv refuses to run instead of rewriting the lock. - ci.yml: `--frozen` becomes `--locked`, so a stale lock fails CI instead of being installed silently. The comment about the team's older uv is gone. - CONTRIBUTING.md names the floor; AGENTS.md replaces the ban with "change dependencies through uv add or uv lock, never by hand". - docs/context/architecture/import-boundaries.md describes the CI step as it now runs. Verified: uv lock --check, uv sync --locked and uv run --locked lint-imports (12 kept, 0 broken) on uv 0.12.3.
8.6 KiB
title, status, sources, related
| title | status | sources | related | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Enforced import boundaries | shipped |
|
|
Enforced import boundaries
Import Linter reads the contracts under tool.importlinter in pyproject.toml. The main CI test
job installs the lock with uv sync --locked (failing on a stale lock), then runs
uv run --locked lint-imports before the test suite. Keeping the check in that job reuses the
development environment and avoids a second install for a fast static architecture check.
The separate test-postgres job runs its database-sensitive subset serially against Postgres 16;
it uses unbuffered Python output and a 15-minute job budget so a slow test remains diagnosable. The
subset includes agent attribution, credential health, local-run reporting and ads-conversion coverage
so naive-UTC assumptions are exercised by asyncpg rather than hidden by SQLite's permissive adapter.
Stage 1 activated the first two contracts:
- The explicit lightweight CLI module list cannot directly import any server-extra package, including
FastAPI, SQLModel, SQLAlchemy, Alembic, database drivers, MCP, Stripe, or cryptography. Imports guarded
by
TYPE_CHECKINGare excluded globally because they cannot load at runtime. Indirect imports are allowed by this contract because optional proxy dependencies may appear in lazily executed internal modules; the named CLI modules themselves must remain free of direct server imports. treg.domain.moneycannot importtreg.audit. Money correctness never flows through the best-effort audit path, whose writes may be shed under load.
Stage 2 adds a third contract: the complete treg.routers package cannot import treg.api, directly or
indirectly. as_packages = true makes the source cover every current and future router submodule.
api.py remains the compatibility exporter and ordered route-table host, so the allowed direction is
API to routers.
The async-task domain has the same inward-only boundary as capacity: it may not import API, routers,
application orchestration or best-effort audit. tests/test_call_architecture.py pins the rule and a
mutation proving the guard rejects a forbidden edge.
Stage 3 adds domain contracts as packages appear. The complete treg.domain.identity package cannot import
treg.api, treg.routers, or treg.application. Identity now owns session signing and validation,
MCP token and grant-family primitives, and caller/access resolution as a leaf. Sibling-domain
edges are added when the sibling appears; identity therefore also forbids governance. Governance may
import identity but cannot import the API, routers, or application layer. Future sibling contracts remain
absent until their packages exist, so no placeholder domain makes a future boundary look active.
Governance owns shared tool/project ACLs, tag-budget rules, and public-demo rate policy. The package also
forbids direct FastAPI and Starlette imports; semantic policy errors are translated by each HTTP interface.
The call application package owns the framework-neutral staged use case: request intake, idempotency
state, target resolution, catalog pricing and access decisions, authorization, reservation, relay
orchestration, and finalization. The HTTP adapter captures a CallInput, translates typed failures, and
wraps the returned UpstreamResponse. Client-name normalization lives in a neutral leaf so the application path does not
load the Request-aware caller metadata adapter.
The complete treg.domain.connections package is also an inward-facing domain package. It owns
provider-neutral authorization-method selection, consent URL construction, and refresh state changes.
It cannot import the API, bootstrap, routers, application layer, FastAPI, or Starlette. Provider registry
data can call these rules through the legacy compatibility modules, while HTTP exchange adapters stay in
treg.infra and workflow coordination stays in treg.application.
Two runtime contracts keep that boundary executable. treg.application.call cannot import the legacy
API, bootstrap, routers, FastAPI, or Starlette. treg.infra.upstream cannot import those HTTP adapters
or frameworks. Direct imports of the application-owned request and response DTOs remain the port shared
by the use case and relay. Mutation tests inject representative forbidden edges and assert detection.
The same test module also pins the money transaction boundary: all five domain/money primitives
(the reserve/settle/release staged bodies plus grant and topup) are scanned for db.commit /
db.rollback and must stage only, with a mutation self-check that an injected commit is detected;
the lazy stale-hold reap keeps its documented independent committing boundary.
Two direct edges are precise exceptions. cli.ensure_proxy_dependency imports cryptography only after
the user invokes the optional proxy feature and offers to install the proxy extra first.
localrun.render_grant imports SQLModel only when the server executes the grant path. Import Linter treats
function-local imports as ordinary direct edges, so both appear in ignore_imports; unmatched ignores are
errors, ensuring a removed or renamed edge cannot leave a stale exception behind.
An ignore covers an entire module edge and therefore cannot detect someone moving either lazy import to
module scope. tests.test_import_lightness closes that gap by starting an isolated Python subprocess,
importing every lightweight module, and asserting that no server dependency root appears in sys.modules.
Base dependencies such as httpx and questionary remain allowed.
The async task domain (treg.domain.asynctasks) is a stdlib-only leaf shared with the light
CLI: cli.await_async_task imports its json_path, classify_terminal and artifact so the
awaiter and the settlement worker can never disagree about what "done" means. Two guards keep it
light: the module is on test_import_lightness's list, and an import-linter contract forbids it
every server root (treg.models, treg.infra, treg.config, SQLModel, pydantic, yaml, httpx).
The capacity domain (treg.domain.capacity, plan step B) is a leaf like identity: it cannot import
treg.api, treg.routers, treg.application, treg.bootstrap, treg.audit, FastAPI or Starlette.
It reads config and writes only its own tables and ratestore keys, from worker-profile commands
(treg-worker, a separate console script so the light treg CLI never gains a DB import). The call
application imports the capacity domain inward (resolve → view, settle → signatures/marks);
the domain never imports back; application.call.overflow composes the capacity domain, the
aggregator envelopes and the money primitives, and the aggregator adapters stay pure envelope code; application.call.route composes the pure
domain.catalog.routing package (contracts, adapters, ranking) with the call use case itself. The
aggregator envelopes live under treg.infra.upstream.aggregators and inherit the upstream contract
(no HTTP adapters, no routers); the capacity domain's verify module may import them because they are
pure envelope code, not a web framework.