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

# Diagnostics configuration

## Tuning diagnostics

The ruleset above is the default. A `diagnostics` block in [`glotto.yml`](/docs/glotto-yml) tunes
it without touching the rules engine — it is a pure post-pass over the findings:

```yaml
# glotto.yml
diagnostics:
  rules:
    no-error-response: error          # promote a warning to a release-blocking error
    mutation-no-idempotency-key: off  # suppress a rule entirely
  max_warnings: 10                    # fail (exit 1) when warnings exceed this budget
```

- **`diagnostics.rules`** maps a rule id to a severity override — `off` suppresses the rule, while
  `warn` / `error` remap its severity. Promoting a `warning` to `error` makes it release-blocking;
  setting a rule to `off` drops its findings (and they no longer count toward `max_warnings`). An unknown rule id is almost certainly a typo, so it's reported as a
  non-fatal warning on stderr and otherwise ignored.
- **`diagnostics.max_warnings`** is a release-gating threshold: after overrides are applied, the
  remaining warning count exceeding this number fails the gate — even with zero errors. Start loose and ratchet it toward `0` as you approach `1.0` (see
  [Release-gating guidance](/docs/diagnostics#release-gating-guidance)).

Both keys are optional and additive — a config with no `diagnostics` block lints exactly as the
defaults above.

The valid rule ids are the ones enumerated on this page, plus two that tune a **config**
diagnostic rather than a spec rule:

- **`config-entry-matched-nothing`** — the severity of `GLOTTO_CONFIG_ENTRY_MATCHED_NOTHING`
  (below). `off` silences it, `error` makes it blocking. It's tunable because
  a keyed entry naming an operation your spec no longer carries is *legal by design* — a
  `glotto.yml` is meant to outlive a spec change — so a team deliberately carrying one can say so
  here instead of dropping `--strict`.
- **`config-auth-scheme-inert`** — the severity of the two *inert* halves of
  `GLOTTO_CONFIG_AUTH_SCHEME_REFUSED` (below): a top-level `client_settings.auth.header_prefix` on an
  API whose security schemes cannot format one, and a `client_settings.auth.schemes` block on an API
  that declares a single security scheme. Both are the same kind of *legal by design* — a
  `glotto.yml` outliving a change to your spec's auth **shape** — so one off-switch covers them.
  It deliberately does **not** cover the third half, a `header_prefix` on an `apikey`, `basic` or
  `custom` scheme: no spec change makes that field applicable to those kinds, so there is nothing
  deliberate to declare.

The two are **independent switches**: they say different things (*"my config outlives my spec's
operations"* versus *"my config outlives my spec's auth shape"*), so silencing one leaves the other
reporting. The remaining config warnings report a directive Glotto structurally declined, which has
no legal-by-design reading, and aren't overridable.

## Config diagnostics

The rules above lint your **OpenAPI spec**. Problems in your **`glotto.yml`** are reported by a
separate gate that validates the config against the schema, and it runs first — an invalid config
stops the pipeline before any spec rule is evaluated. `glotto generate` reports these on stderr.
Unlike the spec rules, they are keyed by a stable **code** and carry a source position
(`file:line:col`) into your `glotto.yml`:

| Code | Severity | Meaning |
| --- | --- | --- |
| `GLOTTO_CONFIG_REQUIRED_FIELD_MISSING` | error | A required key is absent (e.g. `organization.name`). |
| `GLOTTO_CONFIG_INVALID_TYPE` | error | A value has the wrong type (e.g. a string where an object is expected). |
| `GLOTTO_CONFIG_INVALID_ENUM_VALUE` | error | A value is outside the allowed set (e.g. `react_native.secure_storage`, `mcp.modes`). |
| `GLOTTO_CONFIG_INVALID_VALUE` | error | A value fails a field-specific rule (e.g. `max_delay` < `initial_delay`; an operationId in both `skip` and `only`, or in both `exclude` and `only`). |
| `GLOTTO_CONFIG_EXCLUDE_ONLY_CONTRADICTION` | error | An [`exclude`](/docs/glotto-yml-api-surface#exclude) entry and an [`only`](/docs/glotto-yml-api-surface#skip--only) key address the **same** operation using the two different spellings `exclude` accepts — e.g. `exclude: ["post /pets"]` alongside `only: { createPet: … }`. Contradictory intent, exactly as naming it the same way in both is: `only` restricts the operation to the listed targets, `exclude` withholds it from every artifact. `exclude` is applied first, so the operation is withheld everywhere and the `only` entry has no effect. Reported at generate time, where the spec is available to resolve the positional spelling. |
| `GLOTTO_CONFIG_INPUT_SOURCE` | error | Not exactly one of `openapi` / `asyncapi` / `graphql` is present. |
| `GLOTTO_CONFIG_OPENAPI_SOURCE_NOT_FOUND` | error | A local `openapi.source` path doesn't resolve on disk. |
| `GLOTTO_CONFIG_OPENAPI_COMMAND_OUTPUT_PATH` | error | A `command`-source file output is absolute or escapes its working directory. Reported for a `command` source at **any** input site — `openapi.source`, `asyncapi.source`, `graphql.source`, `graphql.operations`, and each `docs.versions[]` snapshot — with the message naming the offending path; the code keeps its historical `OPENAPI` spelling so a config matching on it keeps working. |
| `GLOTTO_CONFIG_INTROSPECT_SOURCE_OPENAPI_ONLY` | error | An [`introspect`](/docs/glotto-yml#input-source--exactly-one-of-openapi--asyncapi--graphql) source was written somewhere other than `openapi.source`. Introspection reads a framework's source and synthesizes an **OpenAPI** document, so it cannot be what an `asyncapi` or `graphql` key means. Point the key at the document it describes, or move the `introspect` source to `openapi.source`. |
| `GLOTTO_CONFIG_DEFAULT_ENVIRONMENT` | error | [`default_environment`](/docs/glotto-yml#default_environment) names an environment that isn't declared under [`environments`](/docs/glotto-yml#environments). The message lists the names you did declare. This is an error rather than a fall-back-and-warn because there is no fallback that keeps what you meant: resolving it any other way would point your SDK, your docs playground and your MCP server at a host you wrote the key specifically to avoid. Fix the spelling, or add the environment. |
| `GLOTTO_CONFIG_DEFAULT_ENVIRONMENT_INFERRED` | warning | Your config declares two or more [`environments`](/docs/glotto-yml#environments), none of them named `production`, and no [`default_environment`](/docs/glotto-yml#default_environment) — so Glotto used the **first by sorted name**, and the message says which. That rule exists so generated code never depends on the order you typed your YAML in; it is deliberately not a guess at which host you meant. Set `default_environment` to the one your SDKs, their README quickstarts, your docs playground and your MCP server should all point at. You will not see this for a single environment (nothing to choose between) or when one is named `production` (that one wins). |
| `GLOTTO_CONFIG_DOCS_VERSIONS` | error | A `docs.versions` entry has a non-route-safe slug, a duplicate slug, more than one `default: true`, or declares other than exactly one snapshot source (`openapi` / `asyncapi` / `graphql`). A slug is also rejected when the URL layer would rewrite it — the generated layout finds the current version by comparing the slug against a percent-encoded pathname segment, so a slug containing a space, a `%`, or a non-ASCII character never matches its own pages and every page under it silently falls back to the default version. Characters a URL path keeps verbatim (letters, digits, and `-._~!$&'()*+,;=:@`) are fine. |
| `GLOTTO_CONFIG_DOCS_REDIRECTS` | warning | A `docs.redirects` entry the generator will refuse to emit: an empty or non-string `from`/`to`, a `from` that is not site-absolute (it must start with `/`), a `from` or `to` containing whitespace (the emitted `_redirects` rule file is whitespace-delimited, so a host would read only the fragment before the space — percent-encode it), a `from` equal to its own `to` (a no-op loop), or a `from` an earlier entry already claims (the later one is unreachable). Also raised when your `docs.deploy.target` names a host whose own redirect config cannot express a `from` exactly — only a **trailing** `*` has an exact equivalent on Vercel, so an interior or repeated one is left out of the emitted `vercel.json` rather than rewritten into a rule that would match different URLs than you wrote. **Only that entry is dropped** — your other redirects still emit. |
| `GLOTTO_CONFIG_DOCS_STRUCTURED_DATA` | warning | A customer-authored [`docs.structured_data.extra_nodes`](/docs/glotto-yml-project-settings#docs) / `extra_nodes_by_route` JSON-LD node the generator will refuse to carry: one that isn’t a JSON object, is an empty object, has no `@type` (a consumer dispatches on the type, so an untyped node is read by nothing), has a `@type` that isn’t a non-empty string — nor a non-empty array of them, JSON-LD’s multiple-type form — has an `@id` that isn’t a non-empty string or a `@context` that is neither a non-empty string nor an object, carries a nested `@graph` (that makes it a *document* rather than a node, and its members would be read as siblings of the derived ones), or exactly duplicates an earlier node in the same list. **Only that node is dropped** — your other nodes, and every node Glotto derives from your spec, still emit. This is a **structural** check: a misspelled `@type`, or a property that doesn’t belong to the type it sits on, is not validated here. |
| `GLOTTO_CONFIG_DOCS_OG_TEMPLATE` | error | A [`docs.og_images.template.colors`](/docs/glotto-yml-project-settings#docs) value isn't a colour the OG-card renderer can paint. Cards are rasterized by `pureimage` during your site's `astro build`, not by a browser, so the accepted forms are `#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`, `rgb(r,g,b)` or `rgba(r,g,b,a)` with **comma-separated** numbers, and CSS named colours (`rebeccapurple`). `hsl()`, `color()`, gradients, `var(--x)`, and the space-separated `rgb(1 2 3)` form are not supported — they either fail your build or paint a colour you didn't ask for. |
| `GLOTTO_CONFIG_DOCS_DEPLOY_DOMAIN` | error | [`docs.deploy.custom_domain`](/docs/glotto-yml-project-settings#docs) isn't a bare DNS hostname. It becomes the authority of your docs site's canonical URLs *and* the domain handed to your deploy provider's binding, so write the hostname alone — `docs.acme.com`, not `https://docs.acme.com`, and no path, port, wildcard, or trailing dot. Labels are letters, digits, and hyphens, at most 63 characters each and 253 overall. Internationalized domains are supported in their punycode form (`xn--caf-dma.example`). |
| `GLOTTO_CONFIG_MCP_PACKAGE_NAME` | error | [`mcp.package_name`](/docs/glotto-yml-project-settings#mcp) isn't a valid npm package name. It becomes the generated `package.json` `"name"` and — with any `@scope/` stripped — the `bin` command name, so it must be at most 214 characters, lowercase, and made of letters, digits, and `-._~`, optionally with a single `@scope/` prefix whose scope and name are each non-empty. Leave it unset to derive `<your-org>-mcp` instead. |
| `GLOTTO_CONFIG_CODE_OWNERS` | error | A [`code_owners`](/docs/glotto-yml-project-settings#code_owners) rule the emission would have to guess about: a block that is declared but names no rules, a blank pattern, a rule with an empty owner list, an empty owner string, or an owner the forge your target's repo lives on could not resolve. The message names the offending provider and the spellings it accepts — GitHub takes `@login`, `@org/team-slug` (exactly two segments) or an email address; GitLab additionally allows `_` and `.` inside a segment and nests `@group/subgroup/…` without limit; Bitbucket and Azure Repos resolve an owner against your workspace directory, so anything non-empty is accepted here and checked when you apply it. This is an error rather than a dropped-and-warn because a CODEOWNERS line the forge cannot resolve is **ignored in full** — the path is left unowned, the file still exists, and nothing anywhere says so. |
| `GLOTTO_CONFIG_CODE_OWNERS_NOT_APPLIED` | warning | You declared [`code_owners`](/docs/glotto-yml-project-settings#code_owners), and this target does not receive a CODEOWNERS file. Two causes, both about the forge rather than your config. **Bitbucket and Azure Repos read no such file** — their equivalents are default reviewers and a required-reviewers branch policy, so Glotto applies the block through their APIs instead of emitting a file. **A target with a `repo_path`** lives in a subtree of a shared repo, and GitHub and GitLab read CODEOWNERS only from the repository **root** — a file emitted into the subtree would never be opened, so none is emitted; declare the ownership in the CODEOWNERS at that repository's root instead. Reported by `glotto generate`. |
| `GLOTTO_CONFIG_DOCS_AUTH` | error | A [`docs.auth`](/docs/glotto-yml-project-settings#docs) block omits a field its tier requires — `password` needs `secret_env`, `sso` needs both `provider` and `login_url` — or sets a non-positive `session_hours`. Supply the missing field (or drop the block to leave the docs site public). |
| `GLOTTO_CONFIG_DOCS_AUDIENCES` | error | A `docs.audiences` block names an audience in `scopes` or `default` that isn't declared in `members`, or its `members` list is empty or has duplicates. Declare every audience in `members` first, and make `default` one of them. |
| `GLOTTO_CONFIG_DOCS_PERSONALIZATION` | error | A `docs.personalization` block has an empty `fields` allow-list, or a `prefill_key_field` that isn't one of `fields`. Only fields on the allow-list are ever rendered into a page, so the key field has to be among them. |
| `GLOTTO_CONFIG_DOCS_I18N` | error | A `docs.i18n` block has a locale token that isn't route-safe, a `locales` list that is empty or has duplicates, or a `locales` list that omits `default_locale`. Every locale becomes a URL segment, and the default has to be one of the locales you build. |
| `GLOTTO_CONFIG_CUSTOM_CASINGS` | error | A [`custom_casings`](/docs/glotto-yml-client-behavior#custom_casings) key is not a lowercase alphanumeric word (identifier words are lowercased before matching, so `API: API` could never match — write `api: API`), or its rendering is not a pure re-casing of the key. Only letter case may differ; to change the word itself use `naming` (model members) or `parameter_naming` (method parameters). |
| `GLOTTO_CONFIG_RENAME_NOT_APPLIED` | warning | A [`naming`](/docs/glotto-yml-client-behavior#naming--parameter_naming) member-rename targets an engine that doesn't apply member renames — TypeScript and React Native (their DTOs are transparent, so the interface *is* the wire shape) or Python in the default non-pydantic mode — or an `enums` nominal-shaping directive targets an engine with no type-alias construct. The directive is accepted but the emitted member keeps its wire name. Drop it for that target, or use `parameter_naming`, which those engines do honor. |
| `GLOTTO_CONFIG_NAMESPACE_NOT_APPLIED` | warning | A [`targets.<slug>.namespace`](/docs/glotto-yml#targets) is set on one of the four SDKs whose **published package name *is* its code identity** — TypeScript and React Native (the npm package is the module you import), Dart (the pub package is the library you import), and Swift (the SwiftPM package is the module you import). There is no second name underneath it for `namespace` to set, so the key has no effect and the SDK keeps the identity `package_name` gives it. Set [`package_name`](/docs/glotto-yml#targets) for that target instead — for these four it *is* the code identity, and it also stays the name you publish under. `namespace` is honored for `csharp`, `elixir`, `go`, `java`, `kotlin`, `php`, `python`, `ruby`, and `rust`, where the two identities are genuinely separate (the Python import package vs. the PyPI distribution name; the Rust `[lib] name` vs. Cargo's `[package] name`). |
| `GLOTTO_CONFIG_CASING_NOT_APPLIED` | warning | `custom_casings` is applied to model members, method parameters, and enum constants, but **not yet** to method names, class/type names, or resource accessors — those keep their default casing. Informational: no config change resolves it, and the warning clears when the remaining surfaces land. |
| `GLOTTO_SPEC_UNION_DEGRADED` | warning | A discriminated union won't generate as a narrowed union (ADR-0041 eligibility); the SDK falls back to an open type. |
| `GLOTTO_CONFIG_MODEL_SHAPING_REFUSED` | warning | A [`models`](/docs/glotto-yml-model-shaping#models) directive was declined — a rename onto a name another model holds, or an `inline: true` on a self-referencing model or a union member. The model stays as it was. |
| `GLOTTO_CONFIG_AUTH_SCHEME_REFUSED` | warning | A [`client_settings.auth`](/docs/glotto-yml-client-behavior#client_settings) directive was declined. Either a `header_prefix` was set on a scheme whose kind cannot carry one (`apikey`, `basic`, `custom` — only `bearer` and `oauth2` format an `Authorization` value), or a top-level `header_prefix` was set on an API whose schemes are all of those kinds so it formats nothing, or a per-scheme `schemes:` block was written for an API that has a **single** security scheme, which is configured through `client_settings.auth` directly. A scheme name your spec no longer defines is *not* reported — a `glotto.yml` outlives a spec change. The last two are tunable with [`diagnostics.rules.config-auth-scheme-inert`](#tuning-diagnostics); the kind mismatch is not, because no spec change makes that field applicable. |
| `GLOTTO_CONFIG_MODEL_RENAMED` | warning | One of your models emits under a **different type name for one target**, because the name it would otherwise take is one that target's own generated SDK source refers to — a schema named `Data` in a Swift SDK would land in the same module as the client's own `Data` references and take them over, and Swift has no import-qualification escape for a same-module declaration. The generated name appends the target's model suffix, and adds a number if that is taken too: `Data` → `DataModel` → `DataModel2`. Nothing is missing from the output and every reference to the model follows the new name; **only the named target is renamed**, so your other SDKs are unaffected. To pick the name yourself, set the key the warning opens with — a [`name.<target>`](/docs/glotto-yml-model-shaping#models) entry under `models`. An explicit name is always honored and this warning stops. The key it names is the one **you** wrote, even when another `models` entry has already renamed that model, so it is always one your `glotto.yml` can carry. |
| `GLOTTO_CONFIG_SOFT_REQUIRED_SPLIT` | warning | A [`soft_required`](/docs/glotto-yml-model-shaping#soft_required) `body_fields` promotion generated a `<Model>Request` variant, because the operation's request body is a model your API also **returns** — demanding the field on it would make it mandatory when decoding a response too. The shared model is unchanged; only this operation's body uses the variant. |
| `GLOTTO_CONFIG_AUTO_POPULATE_REFUSED` | warning | An [`auto_populate`](/docs/glotto-yml-model-shaping#auto_populate) position was declined — its schema doesn't permit exactly one value, its one value has no sendable wire form, or the request body isn't a JSON object. The input stays in the method signature. All four parameter locations (`path`, `query`, `header`, `cookie`) are supported. |
| `GLOTTO_CONFIG_DUAL_MODE_REFUSED` | warning | A [`streaming.dual_mode`](/docs/streaming#dual-mode-endpoints-stream-true) entry was declined, so the endpoint was **not** split into buffered and streaming methods — the operation isn't in your spec, the spec never marks it as streaming, it takes no JSON-object request body, `param_discriminator` names a field that body doesn't have, `stream_event_model` names a model your spec doesn't define, the per-event type couldn't be resolved (name it with `stream_event_model`), or the variant's method name is already taken by a real operation (pick a different `method_suffix`). The operation keeps the single method it had. |
| `GLOTTO_CONFIG_ENTRY_MATCHED_NOTHING` | warning | An entry in one of the six operationId-keyed blocks — `skip`, `only`, `exclude`, `parameter_naming`, `client_methods`, `aliases` — names an operation your spec doesn't carry, so it had no effect. Usually a typo; sometimes an operation renamed upstream since the config was written. The entry is still ignored rather than rejected (a `glotto.yml` outlives a spec change), so this is a warning you can tune or silence with [`diagnostics.rules.config-entry-matched-nothing`](#tuning-diagnostics). |
| `GLOTTO_CONFIG_POSITIONAL_PARAMS_REFUSED` | warning | A [`positional_params`](/docs/glotto-yml-client-behavior#positional_params) entry was declined — it names a parameter the operation doesn't have, lists one twice, or places the `$body` token where it can't be honored (the operation has no body, the body isn't positional, taking it positionally would drag query parameters along, an optional body would precede a path parameter, or the token collides with a reserved name). The order is refused **whole**, so the method keeps its derived argument order — a partly-applied order would be a signature you never reviewed. |
| `GLOTTO_CONFIG_RESOURCE_METHOD_NAME` | error | A [`resources.<r>.methods.<key>`](/docs/glotto-yml#resources) declaration of the emitted method name could not be honored, so `glotto generate` stops. Either the endpoint beside it **matches no operation** in your spec (the verb and path must match exactly, placeholder names included — when exactly one operation serves the same route under a *different placeholder name*, `get /pets/{id}` against `paths: /pets/{petId}`, the message names the spelling your spec uses, which is usually a one-character fix), or the name it declares is **already emitted by another operation on the same resource** — in which case both operations are named and neither is renamed. Not suppressible: the alternative to stopping is shipping an SDK whose method names are not the ones you asked for, and a refused declaration would otherwise look exactly like one you never wrote. |
| `GLOTTO_SPEC_README_UNKNOWN_OPERATION` | warning | A [`readme.example_requests`](/docs/glotto-yml-project-settings#readme) slot names an operation that is not an `operationId` in your spec. The README example falls back to the default selection. Usually a typo, or an operation renamed upstream. |
| `GLOTTO_SPEC_README_UNKNOWN_PARAM` | warning | A `readme.example_requests.<slot>.params` key matches no wire parameter of the operation it names — neither a path/query parameter nor a request-body field. The key is omitted from the rendered example. |
| `GLOTTO_CONFIG_UNKNOWN_TARGET` | error | A [`targets`](/docs/glotto-yml#targets) entry names no codegen engine, so `glotto generate` would refuse the config. The message lists the engines that exist. The usual cause is an emitted artifact that is **not** a target: `mcp`, `docs` and `mock` are configured by a top-level block of that name — see [the MCP server guide](/docs/mcp-server#generating-the-server) — and the message says so for those three. Publishing is a different surface from the codegen `targets:` map: Glotto publishes the generated MCP server to npm for you, and that surface does accept `mcp` where `targets:` does not. |
| `GLOTTO_SPEC_README_SLOT_KIND_MISMATCH` | warning | A `readme.example_requests` pagination or streaming slot names an operation of the wrong kind (a non-paginated operation in the pagination slot, a non-streaming one in the streaming slot). That example falls back to the default selection. |

A config `error` blocks generation; the warnings don't, but a strict gate turns them
(and any warning) into a non-zero exit — as does setting `config-entry-matched-nothing: error`, the
one severity you can raise on its own. Every warning below `GLOTTO_SPEC_UNION_DEGRADED` is computed
from your spec rather than from the config text, so they name the config path
(`models.Address.name`, `soft_required.updateInvoice.body_fields.note`,
`parameter_naming.createPett`) instead of a `file:line:col` position, and `glotto generate` prints
all of them on stderr as well — you don't need a separate `validate` run to see a directive that
didn't take. See the [`glotto.yml` reference](/docs/glotto-yml) for the configuration surface
itself.
