Drift Detection

Drift is when your documentation says one thing but your code does another. Drift diffs JSDoc, examples, markdown, and option tables against the real API spec (TypeScript via Openpkg, or OpenAPI / Clarity via adapters).

Who This Is For

  • Maintainers debugging why drift failed.
  • Teams defining a shared policy for docs quality issues.
  • Engineers deciding which drift classes should block merge.

Why This Matters

  • It converts vague "docs look wrong" feedback into concrete, typed issues.
  • It gives file/line-level diagnostics that can be fixed quickly.
  • It helps teams separate high-risk drift (signature mismatch) from lower-risk gaps.

How To Use This Page

  1. Learn the drift categories below.
  2. Map your common failures to a category.
  3. Set team expectations for which categories must be fixed before merge.

How It Works

  1. Extract -- Openpkg (TypeScript) or an adapter (OpenAPI, Clarity) produces an ApiSpec.
  2. Compare -- JSDoc, examples, markdown, and option tables vs that spec.
  3. Report -- file:line findings. Exit 1 on issues. No model.

The 4 Drift Categories

Structural

Signature and type mismatches between JSDoc and code.

Drift TypeDescription
param-mismatch@param name doesn't match any parameter in the signature
param-type-mismatch@param type doesn't match actual parameter type
return-type-mismatch@returns type doesn't match actual return type
optionality-mismatch@param marks a required param as optional or vice versa
generic-constraint-mismatch@template constraint doesn't match actual generic constraint
property-type-driftDocumented property type doesn't match actual type
async-mismatchJSDoc says sync but function is async, or vice versa

Semantic

Metadata and visibility mismatches.

Drift TypeDescription
deprecated-mismatch@deprecated tag present but export isn't deprecated, or vice versa
visibility-mismatchJSDoc visibility (@internal, @private, etc.) conflicts with code visibility
broken-link{@link SomeExport} in JSDoc references a non-existent export

Example

Issues with @example code blocks.

Drift TypeDescription
example-driftExample imports or references non-existent exports
example-syntax-errorExample has syntax errors

Prose

Broken references in markdown documentation.

Drift TypeDescription
prose-broken-referenceMarkdown code block imports a name that doesn't exist in package exports
prose-unresolved-memberMarkdown code block calls a method that doesn't exist on any exported type
prose-deprecated-referenceMarkdown references a deprecated export/member with no deprecation note nearby

Using drift

drift runs drift detection and reports issues:

drift

Output includes file path and line number for each issue:

  3 issues found

  parseConfig    @param 'options' type mismatch: documented as 'object', actual 'ParseOptions'
                 src/config.ts:42

  createClient   @returns type mismatch: documented as 'Client', actual 'Promise<Client>'
                 src/client.ts:15

  README.md:28   Import 'formatJSON' from 'my-lib' does not exist in package exports
                 Did you mean 'formatJson'?

JSON output:

{
  "ok": true,
  "data": {
    "lint": {
      "count": 2,
      "issues": [
        {
          "export": "parseConfig",
          "issue": "@param 'options' type mismatch: documented as 'object', actual 'ParseOptions'",
          "filePath": "src/config.ts",
          "line": 42
        }
      ]
    },
    "pass": false
  },
  "meta": { "command": "scan", "duration": 450, "version": "1.15.1" },
  "next": { "suggested": "drift get <name>", "reason": "2 issues found" }
}

Exit 1 when issues are found. See CLI Reference.

Prose Drift Detection

Prose drift scans your markdown files for code blocks that import from your package. If an imported name doesn't exist in the package's exports, it's flagged as prose-broken-reference. Method calls in those code blocks are also checked: if a called method doesn't exist on any exported type, it's flagged as prose-unresolved-member. References to APIs the spec marks deprecated are flagged as prose-deprecated-reference — unless the surrounding prose (±5 lines) already acknowledges the deprecation. The check is on the resolved reference, never a bare name: z.url() is the export url, not the deprecated method ZodString.url; a member counts only when its receiver is bound to the type (const s = z.string(); s.url(), or the chain z.string().url()). A name an earlier code block of the page declared at its top level (const useStore = create(...)) is the reader's own in later blocks, not the export of the same name: it is not flagged as deprecated and a variable assigned from it is not typed by the export, until a block imports that name from the package again. A printed signature (function f(a: T): R with no body) is not such a declaration.

Drift includes fuzzy matching -- if you import formatJSON but the actual export is formatJson, the suggestion will say "Did you mean 'formatJson'?". Same for member calls, among the members of the receiver's own type.

Configuring Markdown Discovery

By default, drift scans:

  • README.md
  • docs/**/*.md
  • docs/**/*.mdx

And excludes:

  • node_modules/**
  • dist/**
  • .git/**

Override with the docs config key:

{
  "docs": {
    "include": ["README.md", "docs/**/*.md", "guides/**/*.md"],
    "exclude": ["node_modules/**", "dist/**"]
  }
}

See Configuration for config file locations.

Override at the command line instead with --docs <patterns...> (globs or directories) — useful for pointing at a hosted docs site pulled down locally, without touching the config file. Runs for any language when passed explicitly, not just TypeScript:

drift --docs guides/**/*.md

Monorepo Mode

drift --all
drift --all --private

Batch output shows per-package issue counts:

{
  "packages": [
    { "name": "@scope/core", "exports": 45, "issues": 3 },
    { "name": "@scope/utils", "exports": 12, "issues": 0 }
  ],
  "aggregate": { "count": 3 }
}