Download

docs/api-contract/ — the seam, written down

Contract docs/api-contract/README.md

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

FileWhat it isHow it changes
openapi.jsonapp.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.mdThe 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.mdThe 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.mdThe 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:

  1. Change the route. The guard goes red, naming the routes that were added, removed or reshaped.

  2. Regenerate, from resmon_scripts/:

    python -m implementation_scripts.api_contract --write
  3. 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.

  4. Regenerate the Atlas renderer’s types from the new description, from resmon_scripts/frontend/:

    npm run gen:atlas-api

    and commit atlas/src/api/schema.d.ts with 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_model has its success body described in openapi.json and the model named in http.md’s Response column; a route that does not shows — and the empty schema {}. The split is counted in http.md from the application, not written down, and the undescribed half is described in prose in the root README.md and in the five feature contracts. Nothing here guesses. A declared model is a lower bound: every one is additionalProperties: 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.py explains the hazard and verification_scripts/test_response_models.py is 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 by jsonable_encoder, which changes a Decimal into a string, raises on a PurePath where the concrete Path beside it is fine, and turns inf/nan into a quiet null where the unmodeled route answered 500. None of the three is reachable in the routes modeled today, and response_models.py carries 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.md gives 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.