Files
alkhttp/docs/architecture/decisions/045-to-openapi-gateway-spec-versioning.md
T
glm-5.3-flash 4a825d33e7 feat(infra): full-surface integration suite + docs sync + publish prep
Full-surface integration suite (tests/full_surface.rs, mcp feature):
- one HttpAdapter over real TCP (ProtocolHandler::handle path) serving
  gateway endpoints, /openapi.json, /mcp, and the WS channels session
- gateway: search/schema/call/subscribe/batch/publish presence,
  envelope shapes, error fidelity end-to-end
- from_openapi import -> Internal-by-default invisible from the wire ->
  External facade composes it via env.invoke -> upstream HTTP API
  called end-to-end (ADR-015 composition model exercised)
- to_openapi 6-path doc validated against openapiv3 over the wire
- to_mcp: MCP client connects to /mcp on the served adapter, lists the
  4 gateway tools, search returns ACL-filtered ops (Sub excluded)

Production fix: the WS upgrade route was reserved but never wired into
HttpAdapter's router (the ws-upgrade-session tests built their own
router). Now wired with ws_bearer_auth (401 without a resolvable
token) around ws_upgrade_handler.

Docs sync: all 28 'Port notes' sections/blockquotes stripped from
ported ADRs/specs; OQ-01/OQ-02 statuses corrected to resolved in
overview.md, websocket.md, and the README table (open-questions.md was
already current).

Publish prep: cargo publish --dry-run --allow-dirty succeeds;
cargo doc --no-deps warning-free (ADR link targets fixed); feature
combinations (default / test-support / mcp / wss / all) compile
warning-free under clippy -D warnings.

Verified: cargo test (182 lib default), --all-features (227 lib + 29
integration), clippy -D warnings x3 feature sets, fmt, doc,
publish --dry-run.
2026-08-28 16:07:56 +00:00

200 lines
9.8 KiB
Markdown

# ADR-045: to_openapi Gateway-Spec Versioning
*Ported from alknet ADR-045 (to_openapi Gateway-Spec Versioning); re-targeted to alkhttp.*
## Status
Proposed
## Context
OQ-39 asked how the published `to_openapi` spec is versioned. [ADR-017](017-call-protocol-client-and-adapter-contract.md)
Consequences established that a published `to_*` spec is a compatibility
contract: once external clients build against it, the mapping semantics
become a de facto contract and changing them breaks every client.
The original framing of OQ-39 assumed `to_openapi` generated a
traditional per-operation-paths OpenAPI doc — one path per `External`
operation, changing whenever an operation is added, removed, or has its
schema modified. Under that model the versioning surface is large and
churns constantly, and the doc is a static full-surface dump (the Gitea
failure mode: admin ops shown to every caller, no per-caller filtering).
[ADR-042](042-openapi-gateway-pattern.md) replaced that model with the
**gateway pattern**: `to_openapi`
generates a doc describing **5 fixed gateway endpoints**
(`/search`, `/schema`, `/call`, `/batch`, `/subscribe`), and the
per-caller operation surface is discovered at runtime through
`AccessControl`-filtered `/search` — not preloaded into the static doc.
This is the same mechanic as the MCP gateway (ADR-041), with `subscribe`
added because OpenAPI/SSE supports streaming where MCP tool calls are
request/response.
The consequence for versioning: the published doc is now a small, stable
surface that changes only when the gateway endpoint set or an endpoint's
request/response shape changes. Per-caller operation changes
(adding/removing/modifying operations, changing an operation's schema)
do **not** change the published doc — those operations are not in the
doc; they are discovered via `/search`. This dissolves most of the
churn the original OQ-39 was concerned about.
What remains is the narrow versioning question: how does the published
gateway doc signal its version so consumers can detect breaking changes?
This is one-way after first publication — once external clients build
against the gateway doc, renaming `/call` or changing its request shape
breaks them.
A note on door-type framing: ADR-009 classifies doors by reversal cost
in the codebase. The "published artifact is a contract" case is a blind
spot in that framework — the published doc's reversal cost is paid by
external consumers, not in the codebase. [ADR-017](017-call-protocol-client-and-adapter-contract.md)
Consequences captures
this (published `to_*` specs are compatibility contracts); this ADR
honors the constraint without changing ADR-009's framework. The door is
two-way before first publication (the gateway shape can be revised
freely while no external client depends on it) and one-way after
(revising requires a major version bump that signals breakage to
consumers).
## Decision
### 1. The published gateway doc carries a semver `info.version`
`to_openapi` emits `info.version` as a semver string. The version
reflects the **gateway endpoint contract** (the 5 endpoints + their
request/response shapes), not the operation set:
- **Major bump** — breaking change to the gateway contract: an endpoint
removed or renamed, a required field added to a gateway endpoint's
request, a response shape changed in a backward-incompatible way
(including removing or retyping an existing response field, or
tightening an optional field to required),
the error-mapping semantics ([ADR-023](023-operation-error-schemas.md)) changed.
- **Minor bump** — additive change: a new gateway endpoint added
(e.g., a future `/subscribe-batch`), a new optional request field, a
new response field. Additive changes do not break existing clients.
- **Patch bump** — description/wording changes, documentation, no shape
change.
Cases not enumerated above follow **standard semver**: a change is a
major bump if it could break a client built against the prior version,
a minor bump if it is purely additive, a patch bump otherwise. The
enumerated triggers above are the common cases, not an exhaustive list.
Per-caller operation changes (registering a new operation, removing one,
changing an operation's input schema) **do not bump the version** — the
operation set is not part of the published doc; it is discovered via
`/search` at runtime. This is the key simplification the gateway pattern
buys: the operation surface can evolve freely without touching the
published contract version.
**alkhttp note:** the gateway contract here is the 5-endpoint set,
extended with `/publish` in alkhttp (ADR-068). That addition is a minor
bump under this ADR's rules (a new gateway endpoint added, additive —
existing clients are unaffected; the versioning rationale is unchanged),
and the contract this ADR governs in alkhttp is the resulting 6-endpoint
set.
### 2. The version is bumped on change to the gateway shape, not on regeneration
A deployment that regenerates the doc (e.g., on restart) gets the same
`info.version` unless the gateway shape changed. The version is a
function of the gateway contract, not of when the doc was generated.
### 3. Consumers detect breaking changes via the major version
A client reading the doc compares `info.version`'s major component to
the version it built against. A major bump signals "re-read the doc,
something broke." The minor/patch components are informational. This is
the standard OpenAPI/semver convention — no alkhttp-specific detection
mechanism.
### 4. The traditional per-operation-paths projection (additive, ADR-042 §5) versions independently
A deployment that builds the additive traditional REST projection
(ADR-042 §5) versions that doc on its own schedule — its surface
*does* change with the operation set, so its versioning is the
per-operation churn OQ-39 originally worried about. That projection is
opt-in and out of scope for this ADR; the gateway doc is the default
published contract and the one this ADR governs.
## Consequences
**Positive:**
- The published contract is a 5-endpoint surface (6 in alkhttp with
`/publish`, ADR-068) that rarely changes.
Versioning is bump-on-change, not bump-on-every-operation-change. The
original OQ-39 concern (constant churn) is dissolved by the gateway
pattern — the operation set is not in the doc.
- Consumers use standard semver/OpenAPI `info.version` — no
alkhttp-specific version-detection mechanism to learn.
- Per-caller operation evolution (the common case) is decoupled from the
published-contract version. A node can add/remove operations freely
without bumping the doc version or breaking clients built against the
gateway doc.
- The Gitea failure mode stays structurally impossible (ADR-042 §3):
`/search` is `AccessControl`-filtered, so the doc never exposes ops
the caller can't call. Versioning inherits this — the doc describes
the gateway, not the operations.
**Negative:**
- A client cannot tell from the doc version alone *which* operations are
available — it must call `/search`. This is by design (per-caller,
runtime), but a client expecting a static operation list from the doc
must learn the gateway pattern.
- The version only signals gateway-contract changes. An operation
changing its input schema (a breaking change for callers of that
operation) does not bump the doc version — that change is surfaced via
`/schema` per-operation, not via the doc version. Clients that cache
operation schemas must re-fetch `/schema` to detect per-operation
changes; the doc version does not track them.
## Assumptions
1. **The 5-endpoint gateway set is stable.** ADR-042 Assumption 1. Adding
endpoints is additive (minor bump); removing/renaming is a major bump.
The initial 5-endpoint set is the first published contract (alkhttp
extends it with `/publish`, ADR-068 — an additive minor-bump
addition).
2. **Per-operation schema changes are detected via `/schema`, not the
doc version.** The doc version tracks the gateway contract only. A
client that caches an operation's `OperationSpec` re-fetches `/schema`
to detect changes to that operation. This is the standard
discovery-then-invoke pattern; the doc version is not a per-operation
change tracker.
3. **`info.version` is the single source of truth for the published
contract version.** No separate `x-alknet-version` extension or
content-hash header. Standard OpenAPI field, standard semver
interpretation. A content-hash would be more precise but adds an
alkhttp-specific mechanism for no real gain over semver-on-shape-
change.
## References
- alknet ADR-009: One-Way Door Decision Framework (now alkcall ADR-032)
— door-type
framework (classifies by codebase reversal cost; the
published-artifact-as-contract case is the blind spot this ADR honors
without changing the framework)
- [ADR-017](017-call-protocol-client-and-adapter-contract.md) — published
`to_*` specs are compatibility contracts (the one-way-after-
publication constraint; the decision record is alkcall ADR-022)
- [ADR-023](023-operation-error-schemas.md) — error-mapping semantics
are part of the gateway contract (a change to them is a major bump;
the decision record is alkcall ADR-016)
- [ADR-036](036-http-to-call-operation-mapping.md) — the SSE projection
for `/subscribe` (part of the gateway contract)
- [ADR-042](042-openapi-gateway-pattern.md) — the gateway pattern that
makes the published doc a 5-endpoint surface instead of a per-
operation surface; §4 explicitly deferred versioning to OQ-39
- [ADR-068](068-gateway-publish-endpoint.md) — the alkhttp `/publish`
gateway endpoint; its addition bumps the gateway contract version as
a minor bump under this ADR's rules
- alknet OQ-39 — `to_openapi` published-spec versioning (resolved by
this ADR)
- `http-adapters.md` (in this crate's `docs/architecture/`) — the spec
that emits `info.version` (alknet original:
`crates/http/http-adapters.md`)