docs/api-contract/ — the seam, written down
The backend and the renderer talk only over HTTP on 127.0.0.1. That boundary is the one place
where work on either side can break the other, so it is the one place worth freezing. This folder
is what “frozen” means in practice.
What is here
| File | What it is | How it changes |
|---|---|---|
openapi.json | app.openapi() from resmon_scripts/resmon.py, keys sorted and info.version removed. Every route’s method, path, parameters and request body. | Generated. Never edited by hand. |
http.md | The index: every route against the user journey it serves, with its auth class, whether it streams, and a one-line purpose; then the closed vocabularies and the admission 429. | Generated. Never edited by hand. |
mcp.md | The MCP tool contract, 1.0. What an external harness may call and what it gets back. | Hand-written, frozen. |
assistant-conversations.md · assistant-choices.md · library.md · evidence.md · selected-evidence.md | The five feature contracts: the written agreement the backend and the renderer are each built to. | Hand-written, frozen. |
openapi.json and http.md are generated by implementation_scripts/api_contract.py and guarded by
verification_scripts/test_api_contract.py, which regenerates both in-process and fails on any
difference. The other six are prose, written by hand.
The security model the auth classes come from is ../local-api-security.md:
what a request must carry, where the token lives, and — just as plainly — what none of it defends.
How to change it
A change to the surface takes its own pull request. That is the whole mechanism:
-
Change the route. The guard goes red, naming the routes that were added, removed or reshaped.
-
Regenerate, from
resmon_scripts/:python -m implementation_scripts.api_contract --write -
Commit the regenerated files in that pull request, and say in the body which routes changed and why. A reviewer reading
git diff docs/api-contract/should be able to see the whole of what moved without reading the implementation. -
Regenerate the Atlas renderer’s types from the new description, from
resmon_scripts/frontend/:npm run gen:atlas-apiand commit
atlas/src/api/schema.d.tswith the rest.npm run typecheck(which CI runs) fails while that file is stale, and a renderer call to a route that changed shape stops compiling.
A new route also needs a line in api_contract.ROUTE_JOURNEYS saying which register row it serves.
Until it has one the guard stays red: a route nobody can name a journey for is a route the renderer
has no reason to call.
A route that declares a response_model needs one more thing: a case in
verification_scripts/test_response_models.py. That file fails when a modeled route is not driven
and compared against its own handler’s output, so a model cannot be added on trust.
Running the generator to make a red test go green, without looking at what changed, defeats the point of having it. The failure message is the review.
How the renderer uses it
A screen may call what is in here and nothing else. The renderer is measured against this
file: for each of the 49 register rows, http.md lists the routes that carry it, so a screen
can be checked against the routes its journey actually needs rather than against somebody’s
memory of the page. A screen that needs something not in here is asking for
a backend change, which is a pull request of its own and arrives before the screen does.
Three things the index is careful about, because a contract that overclaims is worse than none:
- Response schemas, where there are any. A route that declares a
response_modelhas its success body described inopenapi.jsonand the model named inhttp.md’s Response column; a route that does not shows—and the empty schema{}. The split is counted inhttp.mdfrom the application, not written down, and the undescribed half is described in prose in the rootREADME.mdand in the five feature contracts. Nothing here guesses. A declared model is a lower bound: every one isadditionalProperties: true, because FastAPI filters a reply against its model and a key nobody declared would otherwise vanish from the body with a 200.implementation_scripts/response_models.pyexplains the hazard andverification_scripts/test_response_models.pyis what holds the models to it, by comparing each route’s body against what its handler returned on the same request. Rely on a declared field being there; do not rely on there being no others. While an undeclared key survives, its value does not always survive unchanged: an extra is re-serialized by pydantic rather than byjsonable_encoder, which changes aDecimalinto a string, raises on aPurePathwhere the concretePathbeside it is fine, and turnsinf/naninto a quietnullwhere the unmodeled route answered 500. None of the three is reachable in the routes modeled today, andresponse_models.pycarries the measured table for whoever models the next one. - Purposes are quoted, not written. A route’s one-line purpose is the first line of its
docstring. The routes that have no docstring show FastAPI’s generated summary in italics — the
function name with the underscores taken out — rather than a sentence somebody invented for them.
http.mdgives the count both ways. - Streaming is marked. Three routes answer
text/event-stream. A client that fetches one of them and waits for a JSON body waits for ever.