# Contract and OpenAPI
contract/openapi.json is generated from real responses and checked on every pull request; the golden vectors beside it pin what every route returns. How both are made, enforced and changed.
- Page:
- Updated: 2026-10-11
- Verified against: `d2f2e41` on 2026-09-27
- From the Sri Guru Granth Sahib Ji — Knowledge Base wiki. Sri Guru Granth Sahib Ji is quoted verbatim, cited by Ang, and never altered.
---
The API's shape is not written by hand. `tools/gen_openapi.py` declares each route once (tag,
a one-sentence description, parameters, limits, sample requests, plus a short verb-first
`summary` in `SUMMARIES` — "Search the Granth" — that the reference's sidebar and headings show)
and then **infers the response schemas from real
responses** of the in-process API on the pinned database, so the spec cannot describe a field the
server does not return. The result, `contract/openapi.json`, drives the [API reference](https://docs.gurbanisoul.com/api/reference/)
on this site and the [routes page](https://docs.gurbanisoul.com/api/routes/).
## Two tables that must agree
The dispatcher (`webapp/serve.py:ROUTES`) says which handler and which bounded context serve a
route; the generator (`tools/gen_openapi.py:ROUTES`) says what a route takes and means. A unit
test fails if a route is on one side and not the other, and `tools/gen_route_table.py --check`
fails the docs build if the [routes page](https://docs.gurbanisoul.com/api/routes/) was not regenerated after either changed.
```python
# The description is the operation's full sentence; its short sidebar/heading label is in SUMMARIES.
LENS = [
q("author", STR, "Lens: only verses by this author."),
q("raag", STR, "Lens: only verses in this raag."),
q("raags", STR, "Lens: only verses in these raags (comma-separated; the \"This hour\" lens)."),
q("rahao", {"type": "string", "enum": ["1", "true"]}, "Lens: refrains (Rahao lines) only."),
q("min_themes", dict(INT, minimum=0, maximum=8, default=0), "Lens: verses carrying at least this many themes."),
q("ang_min", dict(INT, minimum=1, maximum=1430, default=1), "Lens: first Ang."),
q("ang_max", dict(INT, minimum=1, maximum=1430, default=1430), "Lens: last Ang."),
]
ROUTES = {
"/api/meta": ("Meta", "Build identity and corpus statistics (version, commit, counts).", [],
[("/api/meta", {})], []),
"/api/health": ("Meta", "Integrity self-test: line/Ang counts, FTS, verbatim Mool Mantar, ੴ count, live verify, bani registry.", [],
[("/api/health", {})], []),
"/api/search": ("Search", "Search the Granth through the ranked waterfall (exact FTS, seeker lexicon, variants, English, passage, fold).", [
q("q", {"type": "string", "maxLength": 300}, "Query in Gurmukhi, romanised Gurmukhi or English.", True),
q("mode", {"type": "string", "enum": ["auto", "gurmukhi", "roman", "english", "first", "theme"], "default": "auto"}, "Search mode."),
q("limit", dict(INT, minimum=0, maximum=200, default=50), "Page size (clamped)."),
```
*`tools/gen_openapi.py`, lines 40–60, read at `c3d9360`: *
Source: [`tools/gen_openapi.py`](https://github.com/Algorythmos-AI/sggs-platform/blob/integration/tools/gen_openapi.py) — the head of the declarations table, read at build time.
## How the spec is checked
```bash
python3 tools/gen_openapi.py # regenerate contract/openapi.json (needs db/sggs.sqlite)
python3 tools/gen_openapi.py --check # what CI runs: regenerate in memory, exit 1 on any difference
```
`web-ci · python` runs the check on every pull request, and asserts the spec covers every
dispatcher route. Response schemas are inferred with types merged across the sample requests, so a
field that is sometimes `null` is typed as nullable rather than guessed.
## The golden vectors beside it
`contract/` also holds the golden vectors — recorded outputs of the real functions — and
`_meta.json` with their counts and the `db_sha256` they were built against. They pin *values*,
where the OpenAPI spec pins *shape*:
| File | What it fixes |
|---|---|
| `openapi.json` | every route's parameters and response schema |
| `golden_search.ndjson`, `golden_verify.ndjson` | what search and verification return for recorded inputs |
| `golden_reader.ndjson`, `golden_timing.ndjson`, `golden_banis.ndjson`, `golden_analytics.ndjson` | the reader, knowledge, Nitnem and insight projections |
| `golden_roman_norm.ndjson`, `golden_difflib.ndjson` | the two pure functions the iOS port must reproduce byte for byte |
`make contract` regenerates the vectors and fails on drift; `tools/contract_http.py` replays them
over HTTP against a running API, which every deploy does before promoting. Details on
[Harnesses and golden vectors](https://docs.gurbanisoul.com/search/harnesses-and-golden-vectors/).
## The API contract version
`API_CONTRACT_VERSION` in `tools/gen_openapi.py` is the spec's own version, bumped only for an API
change; a breaking change gets a new major and a new path prefix. The **product** version
(`APP_VERSION`, shown by `/api/meta` and `/api/health`) is a separate number shared by web, API and
app — see [One version number](https://docs.gurbanisoul.com/adr/0004-unified-semver/).
## Changing the API honestly
1. Add or change the handler in the bounded context module and its entry in `serve.py:ROUTES`.
2. Declare it in `tools/gen_openapi.py:ROUTES` with parameters, limits and at least one sample
request, and give it a short label in `SUMMARIES` (at most 32 characters; a test holds both
tables to the same routes); regenerate the spec.
3. Regenerate the golden vectors (`make contract`) and review the diff line by line.
4. Regenerate the [routes page](https://docs.gurbanisoul.com/api/routes/) (`python3 tools/gen_route_table.py`) and, if the route
belongs to a context that runs as its own function on staging, the gateway
(`tools/gen_gateway.py`).
5. Ship through the normal pipeline; the deploy replays the contract through the gateway before
promoting. The app re-vendors the contract at the next release tag.