a graphql document is never checked against the upstream's schema, so a wrong field name passes lint and fails at runtime #309

Open
opened 2026-08-19 09:37:02 +00:00 by coilyco-ops · 0 comments
Member

Filed by Angie (engineer, claude seat) on Kai's call, to carry forward the one question mcp-beaver#70 left unresolved rather than let it die inside a closed epic.

That epic's director comment recorded it as:

Whether lint validates the document against a fetched schema or only parses it. That is a scoping call for the children rather than a blocker on starting.

#306 shipped the "only parses it" half. This is the other half, filed so the gap is tracked rather than assumed.

What is checked today, and what is not

parseGraphQLDocument is a signature reader. It reads the operation keyword, the operation name, and the ($x: Type!) variable definitions, and cross-checks those against the guardfile's variable decorations. It never looks at the selection set, which is sent to the upstream verbatim.

So this passes lint and fails at AniList:

graphql {
    document "query ($search: String!) { Page { media(search: $search) { id titel { romaji } } } }"
}

titel is a typo. Everything the parser checks is correct: one operation, a declared variable, a used variable, a POST verb. The upstream returns a GraphQL error at runtime, on a deployed pod, to a model that asked a reasonable question.

That is the failure mode the fail-closed-at-build posture exists to prevent everywhere else in this grammar.

The constraint that shapes the whole design

ParseInline cannot do this. It runs at pod startup, in the container, on every boot. It must stay offline, dependency-free, and fast. A schema fetch there would make a pod's readiness depend on the upstream being reachable, which is strictly worse than the bug being fixed.

So this is a lint-time capability, separate from the parse path, and that is the first thing to settle rather than an implementation detail.

Open questions, none of which I would guess at

  • Where does it live? Beside the document parser here, called by a lint tool, or in mcp-beaver lint where the guardfile-facing lint surface already is. The grammar is umbra's, which argues for here; the lint UX is mcp-beaver's, which argues for there.
  • Where does the schema come from? An introspection query against the live endpoint, or a committed SDL file the guardfile points at. Introspection is zero-setup and many public APIs disable it. SDL is offline-friendly and is another artifact to keep in sync.
  • What does an air-gapped CI do? A lint that needs network is a lint that fails in a sandbox. It probably has to be opt-in, or skip-with-a-statement rather than pass silently, since a check that quietly does nothing is worse than no check.
  • What does it cost in dependencies? Real selection-set validation wants a GraphQL schema library (gqlparser, graphql-go). #306 deliberately shipped with none, and #60 treated dependency footprint as a deciding factor. This is the first thing here that genuinely needs one, so it deserves the same scrutiny rather than a reflex import.

Not urgent, and worth saying so

Nothing is blocked on this. coilyco-bridge/deploy#467 AniList works today, and a guardfile author gets a clear upstream error rather than silence. The cost is that the error arrives at runtime on a pod instead of at lint on a laptop.

Filed at low priority as a real remaining capability, not a defect in #306.

Refs #306, mcp-beaver#70, coilyco-bridge/deploy#467.

**Filed by Angie (engineer, `claude` seat)** on Kai's call, to carry forward the one question `mcp-beaver#70` left unresolved rather than let it die inside a closed epic. That epic's director comment recorded it as: > Whether lint validates the document against a fetched schema or only parses it. That is a scoping call for the children rather than a blocker on starting. `#306` shipped the "only parses it" half. This is the other half, filed so the gap is tracked rather than assumed. ## What is checked today, and what is not `parseGraphQLDocument` is a signature reader. It reads the operation keyword, the operation name, and the `($x: Type!)` variable definitions, and cross-checks those against the guardfile's `variable` decorations. It never looks at the selection set, which is sent to the upstream verbatim. So this passes lint and fails at AniList: ```kdl graphql { document "query ($search: String!) { Page { media(search: $search) { id titel { romaji } } } }" } ``` `titel` is a typo. Everything the parser checks is correct: one operation, a declared variable, a used variable, a POST verb. The upstream returns a GraphQL error at runtime, on a deployed pod, to a model that asked a reasonable question. That is the failure mode the fail-closed-at-build posture exists to prevent everywhere else in this grammar. ## The constraint that shapes the whole design **`ParseInline` cannot do this.** It runs at pod startup, in the container, on every boot. It must stay offline, dependency-free, and fast. A schema fetch there would make a pod's readiness depend on the upstream being reachable, which is strictly worse than the bug being fixed. So this is a **lint-time** capability, separate from the parse path, and that is the first thing to settle rather than an implementation detail. ## Open questions, none of which I would guess at * **Where does it live?** Beside the document parser here, called by a lint tool, or in `mcp-beaver lint` where the guardfile-facing lint surface already is. The grammar is umbra's, which argues for here; the lint UX is mcp-beaver's, which argues for there. * **Where does the schema come from?** An introspection query against the live endpoint, or a committed SDL file the guardfile points at. Introspection is zero-setup and many public APIs disable it. SDL is offline-friendly and is another artifact to keep in sync. * **What does an air-gapped CI do?** A lint that needs network is a lint that fails in a sandbox. It probably has to be opt-in, or skip-with-a-statement rather than pass silently, since a check that quietly does nothing is worse than no check. * **What does it cost in dependencies?** Real selection-set validation wants a GraphQL schema library (`gqlparser`, `graphql-go`). `#306` deliberately shipped with none, and `#60` treated dependency footprint as a deciding factor. This is the first thing here that genuinely needs one, so it deserves the same scrutiny rather than a reflex import. ## Not urgent, and worth saying so Nothing is blocked on this. `coilyco-bridge/deploy#467` AniList works today, and a guardfile author gets a clear upstream error rather than silence. The cost is that the error arrives at runtime on a pod instead of at lint on a laptop. Filed at low priority as a real remaining capability, not a defect in `#306`. Refs `#306`, `mcp-beaver#70`, `coilyco-bridge/deploy#467`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/umbra#309
No description provided.