> For the complete documentation index, see [llms.txt](/llms.txt)

# Drift detection

Glotto **commits generated code to Git** and guards it with drift detection. The check
regenerates your SDKs/docs/MCP server in memory and **fails the PR if the committed output
no longer matches** what the generator produces from the current spec + `glotto.yml`.

## Why commit generated code

- **No slow-install problem** — consumers pull ready-to-use code; there's no toolchain
  (JVM, Python, Go) running during their install.
- **Reviewable output** — code review reads the generator's *actual* output, not an
  inference of it. A generator change shows up as a concrete diff.

## How the gate works

drift detection regenerates and diffs against the committed tree; on drift it exits
non-zero and reports the diff. Glotto emits a per-VCS-provider CI workflow that gates every PR
with [verification report](/docs/verification-report) — the check fails on drift **or** a hand-edited
managed file, and the [**verification report**](/docs/verification-report) (pinned spec/config
hashes, per-target drift and custom-code integrity) is published to the job summary and CI
artifacts — so "someone changed
the generator but forgot to regenerate" can't slip through, and the proof is visible on the PR
itself.

See [drift detection](/docs/drift-detection) and
[verification report](/docs/verification-report) in the CLI reference for the full flag set —
comparing against the directories a [workspace file](/docs/cli#the-workspace-file) declares (no
flag), a local tree (`--against`) or a remote branch (`--provider`), and emitting the CI workflow
(`--emit-workflow`).

## What the gate actually prints

Nothing below is typed by hand. Both panes are what `renderDriftReport` emitted over
[the committed fixture this repo dogfoods](/docs/pipeline) — the same tree the drift gate
regenerates on every one of our own pull requests.

**the committed SDK, regenerated and compared — the state every green PR is in**

The state every green PR is in: 66 committed files, regenerated and compared.

**regenerated + byte-diffed in CI** `renderDriftReport (@glotto/core-vcs)` `packages/cli/tests/fixtures/drift-gate` `0410e9dad464`

files compared 81

out of sync 0

```markdown
✓ SDK in sync — no drift detected.
```

Change one word of the spec and leave the committed SDK alone, and the same check says so —
across every target, not just the SDK:

**one word changed in the spec — `` `summary: List pets` `` became `` `summary: List every pet` ``**

One summary reworded in the spec. Eleven files out of sync, reported as a diff you can read.

**regenerated + byte-diffed in CI** `renderDriftReport (@glotto/core-vcs)` `packages/cli/tests/fixtures/drift-gate` `0560947c783e`

files compared 81

out of sync 11

````markdown
## SDK drift detected

11 files out of sync with the committed SDK.

### changed: `cli/README.md`
```diff
- | `pets listPets` | `GET /pets` | List pets |
+ | `pets listPets` | `GET /pets` | List every pet |
```

### changed: `cli/cli.js`
```diff
-     "summary": "List pets",
+     "summary": "List every pet",
```

### changed: `graph/graph.json`
```diff
-         "summary": "List pets"
+         "summary": "List every pet"
```

### changed: `mcp/src/server.ts`
```diff
- // @glotto:generated-checksum e30acd0f04fffbf5e8122b85c3c76c9dbe50cece7ff42c7e47803ef9227fb641
- // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file.
- import { McpServer } from '@modelcontextprotocol/server';
- import { z } from 'zod';
- import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js';
- import { registerDynamicTools, registerOperationTools } from './dynamic.js';
- import type { ServerDescription } from './debug.js';
- import { parseFilters, shouldRegister } from './filters.js';
- import { parseMode, type ServerMode } from './mode.js';
- import { browseOperations, type OperationEntry } from './operations.js';
- import { forwardedCredentialSource } from './passthrough.js';
- import { clampText, errorResult, projectText } from './results.js';
- import { createSandbox, type SandboxSdkBinding } from './sandbox.js';
- import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js';
- import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js';
- 
- const SERVER_NAME = "petstore-mcp";
- const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example";
- const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] };
- // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default.
- const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"];
- 
- // Named models referenced by the tool schemas; z.lazy so reference cycles resolve.
- const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() }));
- 
- const SEARCH_INDEX_MODELS: Record<string, TypeDescriptor> = {
-   "Pet": {
-     "kind": "object",
-     "fields": {
-       "id": {
-         "kind": "integer"
-       },
-       "name": {
-         "kind": "string"
-       }
-     },
-     "required": [
-       "id",
-       "name"
-     ]
-   }
- };
- const SEARCH_INDEX_BASELINE: SearchRecord[] = [
-   {
-     "kind": "reference",
-     "title": "Create a pet",
-     "url": "/reference/pets/createPet",
-     "resource": "pets",
-     "method": "createPet",
-     "httpMethod": "POST",
-     "path": "/pets",
-     "tool": "pets_createPet",
-     "sdkCall": "client.pets.createPet",
-     "parameters": [],
-     "text": "Create a pet",
-     "requestBody": {
-       "required": true,
-       "contentType": "application/json",
-       "schema": {
-         "kind": "model",
-         "model": "Pet"
-       }
-     },
-     "response": {
-       "kind": "model",
-       "model": "Pet"
-     },
-     "sampleInput": {
-       "id": 0,
-       "name": "<name>"
-     }
-   },
-   {
-     "kind": "reference",
-     "title": "List pets",
-     "url": "/reference/pets/listPets",
-     "resource": "pets",
-     "method": "listPets",
-     "httpMethod": "GET",
-     "path": "/pets",
-     "tool": "pets_listPets",
-     "sdkCall": "client.pets.listPets",
-     "parameters": [],
-     "text": "List pets",
-     "response": {
-       "kind": "array",
-       "items": {
-         "kind": "model",
-         "model": "Pet"
-       }
-     }
-   }
- ];
- const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX'];
- // Loaded once at startup; the search_docs handler awaits this cached merged index.
- const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION);
- 
- // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded
- // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this
- // caller's own upstream OAuth token when that flow is on (#3351), else the operator's
- // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the
- // credential HERE, per request, so the guest's client and the Tools Mode handlers can
- // never disagree about which credential this caller reaches the API with.
- function applyAuth(headers: Record<string, string>): void {
-   const forwarded = forwardedCredentialSource();
-   const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"];
-   if (token) headers['Authorization'] = "Bearer " + token;
- }
- 
- // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order:
- // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries
- // through the list_tools/describe_tools/invoke_tool meta-tools.
- const OPERATIONS: readonly OperationEntry[] = [
-   {
-     name: "pets_createPet",
-     description: "Create a pet",
-     resource: "pets",
-     method: "createPet",
-     httpMethod: "POST",
-     path: "/pets",
-     meta: { resource: "pets", tags: ["pets"] },
-     jqInjected: true,
-     inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }),
-     outputSchema: z.object({ "id": z.number().int(), "name": z.string() }),
-     handler: async (input, jqFilter) => {
-       const path = "/pets";
-       const url = new URL(BASE_URL + path);
-       const headers: Record<string, string> = {};
-       headers['content-type'] = 'application/json';
-       applyAuth(headers);
-       const response = await fetch(url, {
-         method: "POST",
-         headers,
-         body: JSON.stringify(input['body']),
-       });
-       const text = await response.text();
-       if (!response.ok) return errorResult(response.status, text);
-       let structured: unknown;
-       try {
-         structured = JSON.parse(text);
-       } catch {
-         structured = undefined;
-       }
-       if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) {
-         return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true };
-       }
-       const projected = projectText(text, jqFilter);
-       if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true };
-       return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record<string, unknown> };
-     },
-   },
-   {
-     name: "pets_listPets",
-     description: "List pets",
+ // @glotto:generated-checksum 53232a1a568b12e4d6b606365ec19a3c1375664757cc1611e9edecef1a193b61
+ // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file.
+ import { McpServer } from '@modelcontextprotocol/server';
+ import { z } from 'zod';
+ import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js';
+ import { registerDynamicTools, registerOperationTools } from './dynamic.js';
+ import type { ServerDescription } from './debug.js';
+ import { parseFilters, shouldRegister } from './filters.js';
+ import { parseMode, type ServerMode } from './mode.js';
+ import { browseOperations, type OperationEntry } from './operations.js';
+ import { forwardedCredentialSource } from './passthrough.js';
+ import { clampText, errorResult, projectText } from './results.js';
+ import { createSandbox, type SandboxSdkBinding } from './sandbox.js';
+ import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js';
+ import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js';
+ 
+ const SERVER_NAME = "petstore-mcp";
+ const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example";
+ const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] };
+ // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default.
+ const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"];
+ 
+ // Named models referenced by the tool schemas; z.lazy so reference cycles resolve.
+ const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() }));
+ 
+ const SEARCH_INDEX_MODELS: Record<string, TypeDescriptor> = {
+   "Pet": {
+     "kind": "object",
+     "fields": {
+       "id": {
+         "kind": "integer"
+       },
+       "name": {
+         "kind": "string"
+       }
+     },
+     "required": [
+       "id",
+       "name"
+     ]
+   }
+ };
+ const SEARCH_INDEX_BASELINE: SearchRecord[] = [
+   {
+     "kind": "reference",
+     "title": "Create a pet",
+     "url": "/reference/pets/createPet",
+     "resource": "pets",
+     "method": "createPet",
+     "httpMethod": "POST",
+     "path": "/pets",
+     "tool": "pets_createPet",
+     "sdkCall": "client.pets.createPet",
+     "parameters": [],
+     "text": "Create a pet",
+     "requestBody": {
+       "required": true,
+       "contentType": "application/json",
+       "schema": {
+         "kind": "model",
+         "model": "Pet"
+       }
+     },
+     "response": {
+       "kind": "model",
+       "model": "Pet"
+     },
+     "sampleInput": {
+       "id": 0,
+       "name": "<name>"
+     }
+   },
+   {
+     "kind": "reference",
+     "title": "List every pet",
+     "url": "/reference/pets/listPets",
+     "resource": "pets",
+     "method": "listPets",
+     "httpMethod": "GET",
+     "path": "/pets",
+     "tool": "pets_listPets",
+     "sdkCall": "client.pets.listPets",
+     "parameters": [],
+     "text": "List every pet",
+     "response": {
+       "kind": "array",
+       "items": {
+         "kind": "model",
+         "model": "Pet"
+       }
+     }
+   }
+ ];
+ const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX'];
+ // Loaded once at startup; the search_docs handler awaits this cached merged index.
+ const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION);
+ 
+ // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded
+ // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this
+ // caller's own upstream OAuth token when that flow is on (#3351), else the operator's
+ // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the
+ // credential HERE, per request, so the guest's client and the Tools Mode handlers can
+ // never disagree about which credential this caller reaches the API with.
+ function applyAuth(headers: Record<string, string>): void {
+   const forwarded = forwardedCredentialSource();
+   const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"];
+   if (token) headers['Authorization'] = "Bearer " + token;
+ }
+ 
+ // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order:
+ // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries
+ // through the list_tools/describe_tools/invoke_tool meta-tools.
+ const OPERATIONS: readonly OperationEntry[] = [
+   {
+     name: "pets_createPet",
+     description: "Create a pet",
+     resource: "pets",
+     method: "createPet",
+     httpMethod: "POST",
+     path: "/pets",
+     meta: { resource: "pets", tags: ["pets"] },
+     jqInjected: true,
+     inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }),
+     outputSchema: z.object({ "id": z.number().int(), "name": z.string() }),
+     handler: async (input, jqFilter) => {
+       const path = "/pets";
+       const url = new URL(BASE_URL + path);
+       const headers: Record<string, string> = {};
+       headers['content-type'] = 'application/json';
+       applyAuth(headers);
+       const response = await fetch(url, {
+         method: "POST",
+         headers,
+         body: JSON.stringify(input['body']),
+       });
+       const text = await response.text();
+       if (!response.ok) return errorResult(response.status, text);
+       let structured: unknown;
+       try {
+         structured = JSON.parse(text);
+       } catch {
+         structured = undefined;
+       }
+       if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) {
+         return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true };
+       }
+       const projected = projectText(text, jqFilter);
+       if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true };
+       return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record<string, unknown> };
+     },
+   },
+   {
+     name: "pets_listPets",
+     description: "List every pet",
```

### changed: `openapi.decorated.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.base.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.base.yaml`
```diff
-       summary: List pets
+       summary: List every pet
```

### changed: `spec_repo/spec.with-code-samples.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.with-code-samples.yaml`
```diff
-       summary: List pets
+       summary: List every pet
```

### changed: `spec_repo/spec.with-transforms.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.with-transforms.yaml`
```diff
-       summary: List pets
+       summary: List every pet
```
````

This only works because the [pipeline](/docs/pipeline) is deterministic — a byte-stable
canonical spec means regeneration is reproducible, so a diff means a *real* change, not
noise.
