Your PRD is a test. DriftGuard runs it.
DriftGuard scans your OpenAPI specs and Markdown PRDs against your TypeScript codebase, then tells you exactly where your code has drifted from your design.
Entirely offline. Deterministic. No LLMs.
Requires Node.js >= 22.
npm install -g driftguardOr run without installing:
npx driftguard scanRun from your repository root with the default scan paths (spec: docs, code: src):
npx driftguard scanScan with explicit paths and JSON output:
npx driftguard scan --repo . --spec specs/openapi.yml --code src --jsonScan using a config file:
npx driftguard scan --repo . --config .driftguard.yml --jsonScan against a Foundation project (pulls specs dynamically via MCP):
npx driftguard scan --repo . --foundation-mcp my-project-id --foundation-token $FOUNDATION_TOKEN --jsonCompare against a historical git ref:
npx driftguard scan --repo . --since HEAD~5 --jsonAdd DriftGuard to your workflow with the composite action included in this repository:
name: DriftGuard
on: [push, pull_request]
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/driftguard
id: driftguard
with:
spec: docs
code: src
fail-on: high
# Optional: save a baseline first with `driftguard baseline save --name default`
# baseline: default
# Optional: pull specs from Foundation
# foundation-mcp: my-project-id
# foundation-token: ${{ secrets.FOUNDATION_TOKEN }}
- uses: github/codeql-action/upload-sarif@v3
if: steps.driftguard.outputs.sarif-path != ''
with:
sarif_file: ${{ steps.driftguard.outputs.sarif-path }}See .github/actions/driftguard/README.md for full input/output reference.
Instead of pointing at local spec files, DriftGuard can pull specs directly from a Foundation project via the foundationworks-mcp server.
npx driftguard scan --repo . --foundation-mcp my-project-id --foundation-token $FOUNDATION_TOKEN --jsonHow it works:
- Connects to
foundationworks-mcpusing the provided token - Calls
foundation_menuto discover available spec sections (epics, user stories, API design, etc.) - Calls
foundation_fetchwith those sections to pull the actual specs - Hydrates the Unified Spec IR from Foundation documents
- Validates against your code as usual
Configuration via .driftguard.yml:
foundation:
enabled: true
projectId: my-project-id
authToken: ${FOUNDATION_TOKEN}
writeBack: true # sync deviation reports back to FoundationDriftGuard auto-discovers .driftguard.yml at <repo>/.driftguard.yml. If the config file is absent, it falls back to the built-in defaults.
# .driftguard.yml
spec:
- docs
- specs/openapi.yml
code:
- src
plugins:
- ./custom-rules/drift-rule.js
output:
json: false
report:
format: textspec- List of spec files or directories to scan (default:docs). Supports OpenAPI JSON/YAML and Markdown files.code- List of code directories or files to index (default:src). Only.ts,.tsx,.js,.jsx,.mts, and.ctsare indexed.plugins- List of local plugin files to load (optional). Each plugin must export arun(input)function.output.json- Whether to emit JSON output (default:false).report.format- Output format. Currentlytextorjson(default:text).
- CLI flags (e.g.
--spec,--code,--json,--plugin) .driftguard.yml- Default values (
repo: .,spec: ["docs"],code: ["src"])
CLI --spec, --code, and --plugin values can be supplied multiple times and will override config values completely.
| Code | Meaning |
|---|---|
0 |
No drift detected or warnings only. |
1 |
Drift found (missing route, model, rule reference, uncovered story, etc.). |
2 |
Input, config, or runtime error (missing repo, invalid spec file, bad config, etc.). |
DriftGuard indexes these TS/JS route declaration patterns:
router.get('/path', handler)app.get('/path', handler)fastify.get('/path', handler)
Also supported for post, put, patch, and delete.
NestJS controller decorators are supported with literal string arguments:
@Controller('users')@Get(':id'),@Post(),@Put(),@Patch(),@Delete()
Paths are combined into full routes (users + :id becomes GET /users/{id}). Path parameters such as :id are normalized to {id} for comparison against OpenAPI specs.
DriftGuard extracts TypeScript interface and type alias declarations from code and compares them against components.schemas defined in OpenAPI specs. This is a presence check only (name matching), not property-level validation.
When an OpenAPI operation has no matching route in code, DriftGuard creates a finding:
npx driftguard scan --repo . --spec specs/openapi.yml --code srcExample finding:
{
"id": "openapi-route-exists:GET|/users|specs/openapi.yml",
"summary": "OpenAPI operation is not implemented by an indexed route",
"severity": "high"
}Code routes that have no matching OpenAPI operation are reported as formal findings:
npx driftguard scan --repo . --spec specs/openapi.yml --code srcExample finding:
{
"id": "extra-route-not-in-spec:POST|/orders|src/routes.ts",
"summary": "Route implementation exists with no matching spec operation",
"severity": "medium"
}DriftGuard checks whether every components.schemas entry in your OpenAPI spec has a matching interface or type declaration in code:
npx driftguard scan --repo . --spec specs/openapi.yml --code srcExample finding when Order is declared in the spec but missing from code:
{
"id": "data-model-exists:Order|specs/openapi.yml",
"summary": "Data model 'Order' is declared in spec but not found in code",
"severity": "high"
}Markdown headings that declare business rules (e.g. ## BR-001: Payment Validation) are scanned for references in code. If no reference is found, a finding is created:
npx driftguard scan --repo . --spec docs --code srcExample finding:
{
"id": "business-rule-referenced:BR-001|docs/rules.md",
"summary": "Business rule 'BR-001 Require manager approval' is documented but not referenced in code",
"severity": "medium"
}User stories declared in Markdown (e.g. ## US-1: Process Order) can list dependencies such as other rules, models, or routes. DriftGuard checks whether each dependency is covered by code and reports uncovered dependencies:
npx driftguard scan --repo . --spec docs --code srcExample finding when a story depends on a data model that is not implemented:
{
"id": "story-uncovered:US-1|docs/stories.md",
"summary": "Story 'US-1 Place an order' depends on uncovered entities: Order",
"severity": "low"
}You can write local JavaScript plugins that receive the spec IR and repository index, then return custom findings.
Example plugin (custom-rule.js):
export function run({ repository }) {
return repository.files
.filter((f) => /TODO-[A-Za-z0-9_-]+/.test(f.filePath.split('/').pop() ?? ''))
.map((f) => ({
id: 'temp-file',
summary: `Temp file found: ${f.filePath}`,
severity: 'low',
confidence: 'high',
affectedFiles: [f.filePath],
}));
}Run it:
npx driftguard scan --repo . --plugin ./custom-rule.jsPlugin findings appear with a namespaced ID:
{
"id": "plugin:custom-rule:temp-file",
"summary": "Temp file found: src/TODO-cleanup.ts",
"severity": "low"
}Plugins can also be declared in .driftguard.yml:
plugins:
- ./custom-rules/drift-rule.js
- ./another-rule.mjsCompare the current scan against a previous git commit to see which findings are new, resolved, or persistent:
npx driftguard scan --repo . --since HEAD~5 --jsonThe JSON output includes a historical section:
{
"historical": {
"sinceRef": "HEAD~5",
"newFindings": [...],
"resolvedFindings": [...],
"persistedFindings": [...]
}
}DriftGuard creates a temporary snapshot of the repository at the specified ref. It never modifies your working tree.
DriftGuard runs entirely on your machine. No network calls, no remote services, no LLM matching. It is deterministic and designed to run in CI or locally.
The current release does not:
- Compare request or response schemas at the property level
- Validate authentication or permissions
- Detect semantic drift or business logic changes
- Host a UI or generate PDF/HTML reports
These features are planned for future releases.
*Note: Foundation sync via MCP, CI integration, and pre-commit hooks are now supported. See GitHub Actions and Foundation MCP.
git clone <repo-url>
cd driftguard
npm installnpm run buildThis compiles src/ into dist/. The CLI entry point is dist/cli.js.
# After building
node dist/cli.js --help
# Example scan on the repo itself
node dist/cli.js scan --repo . --spec docs --code src
# Scan with JSON output
node dist/cli.js scan --repo . --spec docs --code src --json
# Run in CI mode with SARIF output
node dist/cli.js scan --repo . --spec docs --code src --ci --sarif report.sarif
# Load a custom plugin
node dist/cli.js scan --repo . --spec docs --code src --plugin ./custom-rule.js
# Compare against a historical commit
node dist/cli.js scan --repo . --spec docs --code src --since HEAD~3 --json
# Baseline operations
node dist/cli.js baseline save --repo . --spec docs --code src --name default
node dist/cli.js baseline list --repo .
# Foundation project selection (requires token)
node dist/cli.js foundation auth --token <your-token>
node dist/cli.js foundation projects --token <your-token>
node dist/cli.js foundation select --project-id <your-project-id>mkdir -p /tmp/driftguard-test/docs /tmp/driftguard-test/src
cat > /tmp/driftguard-test/.driftguard.yml <<'EOF'
spec:
- docs
code:
- src
EOF
cat > /tmp/driftguard-test/docs/openapi.yml <<'EOF'
openapi: "3.0.0"
info:
version: 1.0.0
title: Test API
paths:
/users:
get:
summary: List users
EOF
cat > /tmp/driftguard-test/src/routes.ts <<'EOF'
import express from 'express';
const router = express.Router();
router.get('/users', (_req, res) => res.json([]));
export default router;
EOF
# Run the scan
node dist/cli.js scan --repo /tmp/driftguard-test --json# All tests
npm test
# Unit tests only
npm run test:unit
# CLI/integration tests only
npm run test:clinpm run typecheck# Re-run tests on file changes
npx vitest
# Run a specific test file in watch mode
npx vitest tests/unit/config/resolver.test.tsMIT