11.6.6 The DRIFT GUARD in CI — a field added on one surface and forgotten on the other fails the build
The story's headline: the mechanism that makes "the two surfaces cannot drift" a fact CI holds rather than a property people maintain. Every tool is derived by now — this is what stops the next one from not being.
What to build
- The guard, running over the DERIVED shared-resource set (from
lib/api/v1/openapi/registry.ts, per 11.6.2) rather than a hand-written list: for each resource exposed by both surfaces, assert the MCP payload and the REST response validate against the SAME schema. - The failure is the deliverable. Prove the guard bites: add a field to one surface's schema in a fixture and assert the guard FAILS. A guard nobody has watched fail is a guard nobody knows works — and this one's entire value is what it does on a day nobody is looking.
- Coverage is asserted, not assumed. The guard reports which resources it checked, and a resource in the derived set with no corresponding check fails rather than being skipped. Same for tools: derived or explicitly exempt, nothing in between.
- Where it runs. Wire it into the CI lane that already runs the MCP and v1 suites, with real Postgres where the comparison needs real rows. It is a required check, not an advisory one — an advisory drift guard is a report nobody reads.
What the guard does NOT freeze
Say this in the code, prominently, because the next prompt-tuning pass will read the guard before it reads this card: tool names, tools/list descriptions, argument names and scopes are NOT frozen and SHOULD churn. The guard covers the DATA SHAPE only — the half with a second consumer. A contributor who reads a red check as "I may not reword a tool description" will route around the guard, and that is a worse outcome than the drift it prevents.
Scope BOUNDARY
Ends at the guard and its wiring. It does NOT re-base any tool — every family landed already (11.6.3 / 11.6.4 / 11.6.5). It does NOT change a schema on either surface: if the guard finds a real divergence when it first runs, that is a defect to REPORT against the owning card, not to silence by widening a schema until both sides fit. It does NOT gate on tool prose, arguments or scopes.
Acceptance criteria
- For every resource in the derived shared set, the guard validates the MCP payload and the REST response against one schema, and the run names the resources it covered.
- The guard FAILS on a deliberately introduced one-sided field — asserted in both directions (added on REST only, added on MCP only), because a one-directional guard is half a guard.
- A resource in the derived set with no check, or a registered tool that is neither derived nor exempt, fails the run rather than passing silently.
- The guard is a REQUIRED check in the CI lane, not advisory.
- The code states, at the guard's definition, exactly what is frozen (data shape) and what is deliberately free (names, descriptions, arguments, scopes) — and points at the reason.
- If the guard's first real run surfaces an existing divergence, it is filed as a card against the owning story and cited here; it is not resolved by relaxing the guard.
- The per-file coverage floor (≥90%) holds on every new file.
Context refs
- 11.6.2 — the derived shared-resource set and the exemption registry this guard reads.
lib/api/v1/openapi/registry.ts·lib/mcp/registry.ts— the two registries whose intersection the guard walks.tests/api/v1/openapi-operations-coverage.test.ts— the shipped precedent for a registry-walking totality test; copy its shape.tests/helpers/v1RouteAudit.ts— the other shipped guard over this boundary, and the model for stating a rule's intent beside its assertion.tests/mcp/tool-coverage.test.ts— the existing MCP-side coverage test this must not duplicate or contradict.- Story: 11.6.