﻿# API PARITY — every GUI capability has an API twin

## 1. The rule

**Anything a user can do in the web GUI can be done through `/api/v1` with the same semantics.**
The API may offer more than the GUI (`api-only` rows in `FEATURE-REGISTRY.md`); the GUI may never
offer more than the API.

"Same semantics" means, for the same feature:

| Dimension | Must match |
|-----------|------------|
| Capability | Same operations, same optional parts (e.g. scheduling, photo by URL vs by file) |
| Validation | Same limits, same allow-lists, same required fields |
| Defaults | Same default when a field is omitted (GUI checkbox default = API omitted-field default) |
| Data types | Same date format (ISO-8601), same boolean parsing |
| Errors | Same error codes for the same conditions; GUI shows the message of the code the API returns |
| Authorisation | Same guard strength; the API must not be a way around a GUI confirmation |
| Documentation | The endpoint exists in `DOC/API.md` with parameters, response, error codes, one example |

The only allowed exceptions are things that are **browser concepts**: the OAuth consent redirect,
the browser session (active account, logout), page navigation. Those rows carry `n/a: <reason>` in
the registry, and the reason must be one of these three.

## 2. How the rule is enforced

1. **Structurally**: GUI route and API route are two thin adapters over **one** service or
   facade function (ADR-002). If the logic exists once, it cannot drift. Adapter = parse input,
   call the function, render (HTML or JSON envelope). No `if`/`else` about the domain in an adapter.
2. **By the fragment**: every change whose `gui_routes` is non-empty must have `api_endpoints`
   non-empty or a `parity_exception`. `check_rules.py --fragments` fails otherwise.
3. **By the registry**: every GUI route in code must be in `FEATURE-REGISTRY.md` with an API
   twin. `check_rules.py --parity` fails otherwise, and also fails when an API endpoint in code is
   missing from `DOC/API.md`.
4. **By review**: the reviewer walks the table in §1 for the feature and writes "parity: verified"
   in the review. The script proves existence; the human proves semantics.

## 3. What to do when …

**… you add a GUI feature.** Add the service function first (or the facade function), then the
API endpoint, then the GUI page. Test the service directly, then each adapter. Add the registry
row and the `DOC/API.md` section in the same change.

**… you change a GUI feature** (new field, new validation, new default). Ask: does the API
already accept this? If not, extend the API in the same change. Record the analysis in the
fragment's `## API impact` section even when the answer is "API already covers it because both
call `services.posts.publish_now()`".

**… you change an API endpoint.** Additive changes (new optional field, new endpoint) are fine
inside `v1`. A breaking change (renamed field, removed endpoint, changed default, stricter
validation) requires `breaking: true` in the fragment, a deprecation note in `DOC/API.md`, and
either a `v2` prefix or a documented sunset date of at least 90 days. The GUI is updated in the
same change so it keeps working.

**… a GUI capability cannot have an API twin.** Write the `n/a:` reason in the registry. If it is
not one of the three browser concepts in §1, it is a design smell: discuss before merging.

## 4. Known drift (must be closed)

None at birth: every GUI capability has an API twin or a reasoned browser exception, and both
adapters call the same service or facade function. Drift that appears later is recorded in
`FEATURE-REGISTRY.md` rows marked `drift` and must reference the change ID that will close it.

## 5. Contract testing (target)

`tests/routes/` already exercises both adapters with the same fakes. The remaining step is a
parametrised parity test per registry row of status `ok` that calls the service through both
adapters with the same input and asserts the same outcome. Until the test exists, the review in
§2.4 is the control.

