Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0a8fbe1
Test the v2-layers guard (failing)
anth-volk Nov 4, 2026
0240ece
Assign every v2 file to a layer and enforce the v2 layer rules
anth-volk Nov 4, 2026
0af97a1
Test the review findings on the v2-layers guard (failing)
anth-volk Nov 4, 2026
92c10f8
Check every v2 layer's calls and tighten the v2-layers guard
anth-volk Nov 4, 2026
a21780f
Describe the new pure-modules rule in the changelog fragment
anth-volk Nov 4, 2026
c1e8e66
Test the second review's findings on the v2-layers guard (failing)
anth-volk Nov 4, 2026
5ff9ea6
Close the second review's findings on the v2-layers guard
anth-volk Nov 4, 2026
51005e0
Test the third review's findings on the v2-layers guard (failing)
anth-volk Nov 4, 2026
efb919d
Close the third review's findings on the v2-layers guard
anth-volk Nov 4, 2026
a4b2b79
Test layers taken from file names only, with registry exceptions (fai…
anth-volk Nov 5, 2026
36fda35
Take v2 layers from file names only, and rename the files that don't fit
anth-volk Nov 5, 2026
1780783
Test that proofs name each finding's rule and exceptions load cleanly…
anth-volk Nov 5, 2026
8b441b8
Name each proof finding's rule and complete the exception guidance
anth-volk Nov 5, 2026
e8ce312
Test that the exception guidance asks for an expires date only (failing)
anth-volk Nov 5, 2026
9b621b1
Ask for an expires date on exceptions and say which checks they give
anth-volk Nov 5, 2026
e19f0d5
Merge remote-tracking branch 'origin/master' into v2-architecture-lay…
anth-volk Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,8 @@ To add or change a rule, read `docs/engineering/skills/rules-registry.md`.

### API v2 layer boundaries

- Checked: `api.v2.connectors-mutations-no-reads`, `api.v2.connectors-no-cross-calls`, `api.v2.connectors-reads-no-mutations`, `api.v2.crud-checks-cover-every-resource`, `api.v2.import-layers`, `api.v2.pure-modules-no-database`, `api.v2.routes-import-no-database-access`, `api.v2.session-modules-no-queries`, `api.v2.types-transformations-no-validators`, `api.v2.user-services-no-sql`, `api.v2.validators-transformations-no-db`
- Checked: `api.v2.connectors-mutations-no-reads`, `api.v2.connectors-never-commit`, `api.v2.connectors-no-cross-calls`, `api.v2.connectors-reads-no-mutations`, `api.v2.crud-checks-cover-every-resource`, `api.v2.files-assigned-to-layers`, `api.v2.import-layers`, `api.v2.pure-modules-no-database`, `api.v2.pure-modules-no-session-calls`, `api.v2.routes-import-no-database-access`, `api.v2.routes-no-sessions-or-sql`, `api.v2.services-no-sql`, `api.v2.session-modules-no-data-access`, `api.v2.session-modules-no-queries`, `api.v2.types-transformations-no-validators`, `api.v2.user-services-no-sql`, `api.v2.validators-transformations-no-db`
- `api.routes.shared-query-parameters`: Routes that accept query parameters define them through the shared typed query models, never with a second manual parser. (doc-only, not checked: follow it yourself)
- `api.v2.connectors-never-commit`: v2 connectors receive a session from the service and never open, commit, roll back or close it. (doc-only, not checked: follow it yourself)
- `api.v2.routes-no-sessions-or-sql`: v2 route modules never open database sessions or construct SQL; they call one service method and map typed failures to HTTP. (doc-only, not checked: follow it yourself)
- `api.v2.services-no-sql`: v2 services sequence work but never construct SQL expressions, call Session.exec, or define HTTP response models. (doc-only, not checked: follow it yourself)

### Runtime persistence and route boundaries

Expand Down
1 change: 1 addition & 0 deletions changelog.d/4190.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Check that every API v2 route and service file is named after its layer, and that v2 route modules open no session and build no SQL, services construct no SQL, connectors never open, commit, roll back or close a session, session modules do no data access, and validators, transformations and types make no session calls, in a new v2-layers quality guard. Eight v2 modules are renamed to fit (for example `fastapi_routes/v2/errors.py` is now `errors_support.py`).
107 changes: 85 additions & 22 deletions docs/engineering/skills/v2-code-organization.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,59 @@
# API v2 Code Organization

Organize API v2 code first by resource and then by one explicit technical
responsibility. Route modules must not open database sessions or construct SQL.
Service modules must sequence work but must not construct or execute SQL.
responsibility. Route modules must not open database sessions or construct SQL
(rule `api.v2.routes-no-sessions-or-sql`). Service modules must sequence work
but must not construct or execute SQL (rule `api.v2.services-no-sql`).


## File layers

Every `.py` file under `policyengine_api/services/v2` and
`policyengine_api/fastapi_routes/v2` has a layer, which the `v2-layers` quality
guard (`scripts/rules/v2_layers.py`) takes from the file's name alone:

- under `services/v2/<resource>/`: `services.py` (service), `validators.py`,
`transformations.py`, `types.py`, `process_state.py` (process-wide state),
`database_session.py` (directly in the resource package) and
`database_connectors/<creates|reads|updates|deletes>[_name].py`;
- under `fastapi_routes/v2/`: `routes.py` and `<name>_routes.py` (routes),
`<name>_models.py` (request, response and document schemas) and
`<name>_support.py` (HTTP code the route modules share);
- `__init__.py` anywhere.

A file with another name fails the guard (rule
`api.v2.files-assigned-to-layers`): rename it. A file that can't be renamed
needs a `[[rule.exception]]` for that rule in `rules/registry.toml`, with its
`path`, a `reason` and an `expires` date, by which the file should be renamed.
An exception chooses no layer: the file gets the route checks under
`fastapi_routes/v2`, and the validator, transformation and types checks
(rule `api.v2.pure-modules-no-session-calls`) under `services/v2`. The guard
fails on an exception for a file that no longer exists or whose name already
gives it a layer.

The guard counts a method call as a session call when its receiver is named
`session` or `<something>_session`, but not a request's `.session` or an HTTP
or client session (`http_session`, `requests_session`). Route modules and the
pure layers may call no method on a session at all. In routes, services and
the pure layers a call into SQLAlchemy or SQLModel counts, except to its
exception classes and, in services, to a session factory; a builder called on
a model's `__table__` counts too. A session factory is any call into
`policyengine_api.data.orm` or `policyengine_api.data.v2.database`,
SQLAlchemy's `Session`, `sessionmaker` or `create_engine`, or an injected
`*session_factory` or its `begin()`; only services and `database_session`
modules may use one. The guard doesn't
recognise a session bound to another name, an alias of a model's table, or a
name re-exported through another module. A ratcheted rule's baseline lists one
line per tolerated occurrence.

## HTTP adapters

Resource-specific FastAPI code lives under `policyengine_api/fastapi_routes/v2/`:

```text
errors.py
errors_support.py
user_auth_support.py
user_resources_support.py
policies/
request_models.py
response_models.py
Expand All @@ -20,27 +64,39 @@ user_policies/
routes.py
metadata/
response_models.py
common.py
query_dependencies.py
response_cache.py
route_support.py
query_support.py
response_cache_support.py
search_file_support.py
*_routes.py
```

`common.py` also holds the route class that gives metadata responses their
caching headers. `query_dependencies.py` defines the typed FastAPI
`route_support.py` runs the metadata reads for the route modules, maps their
failures to HTTP, and holds the route class that gives metadata responses
their caching headers. `query_support.py` defines the typed FastAPI
dependencies for the shared query models once, for every metadata route
module to import. `response_cache.py` keeps serialized whole-version
module to import. `response_cache_support.py` keeps serialized whole-version
responses (bodies and ETags) for the process; it is HTTP-level, so it lives
with the routes.

`errors.py` defines the strict error response and serialization function shared
by every API v2 resource. Application-wide exception handling distinguishes
with the routes. `search_file_support.py` serves a stored parameter search
file with its content encoding.

`errors_support.py` defines the strict error response and serialization
function shared by every API v2 resource. `user_auth_support.py` turns
request credentials into an account principal, and
`user_resources_support.py` holds the HTTP conventions the user-owned
resource routes share. Application-wide exception handling distinguishes
API v2 from non-v2 requests but must not select error behavior by matching
individual resource paths.

Request models describe HTTP bodies. Response models describe public response
response structures and OpenAPI output. Route functions handle HTTP-only conditions,
invoke one service method, and convert typed failures to HTTP responses.
invoke one service method, and convert typed failures to HTTP responses. No module
under `fastapi_routes/v2` imports SQLAlchemy, SQLModel, a connector, a
`database_session` module, the v1 ORM module `policyengine_api.data.orm` or
`policyengine_api.data.v2.database`, or calls a SQLAlchemy or SQLModel
function, a session method or a session factory (rule
`api.v2.routes-no-sessions-or-sql`; today's `sqlalchemy.exc` imports are in its
baseline).

## Resource service packages

Expand All @@ -61,10 +117,9 @@ services/v2/<resource>/
deletes.py
```

A resource may also add a module for process-wide state its service reuses
across requests, named for what it keeps, such as the metadata package's
`selected_catalogs.py` (each catalog version a request has selected, which
never changes once published). Such a module holds plain data, constructs no
A resource may also add a `process_state.py` module for process-wide state its
service reuses across requests, such as the metadata package's (each catalog
version a request has selected, which never changes once published). Such a module holds plain data, constructs no
SQL and owns no session; the composition code creates one instance per process
and injects it into the service.

Expand All @@ -86,14 +141,20 @@ or more connector functions. A service may expose an entrypoint that accepts an
existing session when several resources must change atomically in one caller-
owned transaction.

Do not construct SQL expressions, call `Session.exec`, or define HTTP response
models in this module.
Do not construct SQL expressions, call `Session.exec` or another data-access
session method, or define HTTP response models in this module (rule
`api.v2.services-no-sql`, which also forbids importing FastAPI or Starlette).

### `validators.py`

Define functions that inspect already-available values and either return a
validated value or raise a typed exception. Validators must not load records,
open sessions, execute SQL, mutate database rows, or construct HTTP responses.
Validators, transformations, types, process-state modules and package
`__init__` files import nothing from SQLAlchemy or SQLModel but the `Session`
type, import no connector or session module, and call no session method,
SQLAlchemy or SQLModel function or session factory (rule
`api.v2.pure-modules-no-session-calls`).

When validation depends on stored state, a database connector loads the
required rows and the service passes those rows to a validator. For example,
Expand Down Expand Up @@ -129,7 +190,9 @@ baseline).
Define the resource's database-session lifetime container. It may open and
close sessions, begin transactions, commit, or roll back. It must not construct
SQL, choose records, validate business rules, transform records into response
types, or determine multi-step operation order.
types, or determine multi-step operation order. It calls no data-access
session method (`add`, `get`, `merge`, `exec`, `execute` and so on) and
imports no connector (rule `api.v2.session-modules-no-data-access`).

Composition code should construct this container and inject it into the
service. A request-scoped read service may wrap one already-open session; a
Expand All @@ -147,7 +210,7 @@ ORM rows belongs in this package. Organize connector files by CRUD operation:
- `deletes.py` removes rows.

Connector functions receive a session from the service. They do not open,
commit, roll back, or close it. They do not call route functions, perform
commit, roll back, or close it (rule `api.v2.connectors-never-commit`). They do not call route functions, perform
request-level or business validation, or convert rows into public response
types. Connector modules do not call one another to sequence a workflow; the
service makes that ordering explicit. They never import one another (rule
Expand Down
Loading
Loading