# graphql-schema-drift

Shared PR-time guard that validates an MFE's **inline** GraphQL query /
mutation / subscription documents (hand-rolled template literals, `gql`-tagged
or not) against a configured schema snapshot, and fails the build on any
mismatch.

## Why this exists (BOFF-3922)

MFEs that write GraphQL as `.graphql` files under `src/operations/**` already
get this check for free — `graphql-codegen`'s `documents:` glob parses and
validates those against `schema:` as part of generating types. But several
MFEs (and more will follow) hand-roll queries as plain template literals
inside `.ts`/`.tsx` hooks — those never touch `documents:`, so `codegen`
silently no-ops on them (`ignoreNoDocuments: true`), and nothing else in CI
looks at them. A backend rename or a flat-list→Connection pagination change
then breaks the MFE in prod with zero CI signal. This happened three times
independently before this tool existed:

- **BOFF-3915** (labsofscience) — root incident that opened BOFF-3922.
- **BOFF-4703** (manufacturedops) — `useDowntimeEvents`/`useInspectionPlans`.
- **BOFF-3529** (planmagnet) — 44 pages' inline `gql` blocks post field-rename.
- **movethewheels** `useOrders.ts` (2026-07-25) — hand-rolled queries against
  the pre-rename `Order`/`orderStats` schema, weeks after the backend renamed
  the domain to `DeliveryJob`.

manufacturedops and planmagnet each independently wrote a bespoke
`scripts/validate-schema-drift.ts` to guard against a repeat. This package
generalizes both into one configurable tool so future repos don't reinvent it
a fourth, fifth, sixth time.

## What it does NOT (yet) do

- **Doesn't check `.graphql`-file-based MFEs.** Those already get schema
  validation from `bun run codegen` today. What they're missing is a
  guarantee that the _committed schema snapshot itself_ is fresh — codegen
  will happily validate a correct-looking query against a stale local
  `graphql/wspace.graphql` and pass. That's a different problem (snapshot
  freshness, not missing validation) and needs a different fix — see
  "Fleet-wide follow-up" below.
- **Doesn't fetch schema from Hive CDN.** Every `schemaGroups[].files` entry
  is a file already committed to the consuming repo. This avoids needing new
  CI secrets to land the tool in its first 3 repos, but it means the schema
  source can itself go stale (exactly the manufacturedops/planmagnet root
  cause) unless someone keeps it refreshed. See "Fleet-wide follow-up".
- **Doesn't check the version an app-shell actually pins.** BOFF-3922 also
  flags that a shell can pin an MFE version older than the deployed schema.
  Out of scope here — this only checks the repo's own HEAD.

## Usage

1. Add `@burdenoff/fe-libs` as a dependency (every MFE already has it).
2. Create `schema-drift.config.json` at the repo root:

```jsonc
{
  "sourceRoots": [{ "dir": "src/myproduct" }],
  "fragmentDirs": ["src/operations/wspace/myproduct/fragments"],
  "schemaGroups": [
    { "name": "subgraph", "files": ["graphql/myproduct.graphql"] },
    { "name": "gateway-fallback", "files": ["graphql/wspace.graphql"] },
  ],
}
```

3. Add a script and wire it into `sanity`:

```jsonc
"scripts": {
  "validate:schema-drift": "graphql-schema-drift",
  "sanity": "bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run build"
}
```

4. Keep the referenced schema file(s) fresh — same discipline as
   `graphql/wspace.graphql` / `graphql/global.graphql` already require. E.g.:

```bash
cp ../../wspace/myproduct/wspace-myproduct-svc/.hive-schema.graphql graphql/myproduct.graphql
```

## Config reference

See `types.ts` for the full, documented shape (`SchemaDriftConfig`). Summary:

| Field                                 | Purpose                                                                                                                                                                                                                                                                                                                      |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sourceRoots[]`                       | Dirs scanned recursively for inline `query`/`mutation`/`subscription`/`fragment` template literals.                                                                                                                                                                                                                          |
| `fragmentDirs[]`                      | Optional dirs of standalone `.graphql`/`.gql` fragment files, resolved for `...Spread`s (in addition to inline `fragment` blocks, which are always collected automatically).                                                                                                                                                 |
| `schemaGroups[]`                      | ≥1 named schema targets. A document is clean if it validates against **any one** group — this covers both "try my subgraph, fall back to the gateway snapshot for foreign root fields" (multiple groups) and "concatenate a base SDL + an `extend type Query` roots file into one schema" (`mergeExtends` within one group). |
| `treatUnresolvedInterpolationAsError` | Default `true`. An operation with a `${...}` this tool can't resolve against a same-file `const` fails loudly rather than silently validating a truncated document.                                                                                                                                                          |

## Extraction rules

- Matches backtick blocks whose content starts with `query`, `mutation`,
  `subscription`, or `fragment` — with or without a preceding `gql` tag.
- `${NAME}` interpolations are expanded against same-file top-level
  `const NAME = \`...\`;` constants (transitively, up to 10 passes).
- `fragment ... on ... { }` blocks (inline or from `fragmentDirs`) are pooled
  and spliced in wherever `...FragmentName` is spread, transitively.

## Fleet-wide follow-up (not done in this pass)

BOFF-3922 asks for a fleet-wide rollout across all 63 `microfe-*` repos. That
is deliberately **not** attempted here — this PR proves the tool on 3 real
repos (2 migrations off bespoke scripts + 1 fresh installation covering a
guard gap that a real incident had already exposed) and leaves the rest as
scoped follow-up:

1. **Wire into the remaining MFEs with inline/hand-rolled documents.** Audit
   the fleet for `.ts`/`.tsx` files containing bare `` `query `` / `` `mutation ``
   backtick blocks outside any `documents:` glob (grep
   ``grep -rlE '`\s*(query|mutation)\s' src --include=*.ts --include=*.tsx``
   per repo) and add a `schema-drift.config.json` + wire into `sanity` for
   each hit.
2. **Solve schema-source freshness**, either by:
   - a scheduled job per repo that re-runs `schema:pull`-equivalent and opens
     a PR when the snapshot drifts from the live subgraph, or
   - a `schemaGroups[].source: 'hive-cdn'` mode that fetches
     `https://cdn.graphql-hive.com/artifacts/v1/{target}/sdl` with
     `X-Hive-CDN-Key` at validation time instead of reading a committed file
     (precedent: `microfe-groups/.graphqlrc.yml`'s `schema-wspace` codegen
     project already pulls schema this way, just not wired to this tool).
     This needs a CDN token provisioned per repo/CI — real effort, not a
     one-line change.
3. **Extend coverage to `.graphql`-file-based MFEs** (the majority of the
   fleet) once (2) is solved — at that point the value-add over what codegen
   already does is real (freshness), not just redundant re-validation of the
   same local file codegen already checked.
4. **Pin-aware validation** — validate the _published_ MFE version an
   app-shell pins, not just the repo's own HEAD, per the original ticket's
   "worth considering" list.

## Reference incidents this tool is proof against

Run `bun test scripts/graphql-schema-drift/__tests__` in this repo for the
unit-test coverage of the extraction/fragment/schema-group logic itself.
