A Bun-first CLI that detects contract drift before it breaks downstream consumers.
Turns markdown contracts into a dependency graph, classifies drift, and blocks unsafe changes in local dev and CI.
- 2026-05-04 π Released v0.6.0 β
ferret scannow auto-infersstablestatus when a contract'ssourceresolves clean (NOOP upward drift). No manualstatus: activeneeded. See CHANGELOG. - 2026-05-04 π Released v0.5.0 β
sourcefield ondefineContract()for cross-file upward drift (S63) androadmapβpendingstatus rename withcontext.jsonv3.0 (S62). See CHANGELOG. - 2026-04-26 π Released v0.4.2 β dogfooding bug fixes: zod@4 compatibility, tree-sitter native module dedup,
ContractReftype forconsumes/dependsOn, anddefineContractnow returnsschema: ZodObject<T>for safe composition. See release notes. - 2026-04-14 π Released v0.4.0 β
ferret watch,ferret audit, and exported watch/audit APIs in@specferret/core. See CHANGELOG. - 2026-04-14 π Released v0.3.0 β TypeScript-native
.contract.tsformat withdefineContract(), Zod-based schemas,ferret statuscommand, and automatic.contract.tsdiscovery. See CHANGELOG. - 2026-04-14 π Released v0.2.0 β bidirectional drift enforcement (code β spec), upward drift detection in
ferret lint --ci,source:blocks, and guided resolution for code-origin drift. See CHANGELOG. - 2026-04-09 π Released v0.1.4 β agent mode scaffolding (
--agent-targets), default tree-sitter TypeScript extraction (no annotations required), context schema versioning, diagnostics command, and perf budget hardening. See release notes. - 2026-04-04 π Released v0.1.3 β publish-safe Bun exports, smoke-fixture hardening, and npm packages refreshed. See release notes.
- 2026-03-15 π Released v0.1.0 β initial public release.
ferret init,ferret scan,ferret lint,ferret review. Full BMAD and spec-kit validation runs passing.
- Key Features
- Architecture
- Install
- Quick Start
- Your First Contract
- CLI Reference
- TypeScript-Native Contracts
- Contract Types
- Changelog
- How Drift Resolution Works
- CI Integration
- BMAD & spec-kit Integration
- Use Cases
- Project Structure
- Roadmap
- Validation Evidence
- Development
- Star History
π Drift Detection β Parses .contract.md and .contract.ts files and detects shape changes automatically on every ferret lint.
π₯ Breaking vs Non-Breaking Classification β Missing required fields and removed properties are BREAKING. Optional additions are NON-BREAKING. You know exactly what needs review.
πΈοΈ Dependency Graph β Contracts declare dependencies with imports (markdown) or consumes (TypeScript). SpecFerret computes the full direct and transitive impact graph so you know every consumer of a drifted contract.
β‘ Fast β ferret lint on a clean project completes in under 500ms. SQLite-backed, no external service.
π Pre-commit Enforcement β ferret init installs a .git/hooks/pre-commit hook that blocks commits when breaking drift is detected.
π€ CI Mode β ferret lint --ci exits non-zero on drift, with JSON output for downstream tooling and agents.
π οΈ TypeScript Extraction β ferret extract infers contracts from exported TypeScript declarations by default, with // @ferret-contract: available as an override for explicit id and type.
π TypeScript-Native Contracts β Define contracts as .contract.ts files using defineContract() and Zod schemas. Full type safety, runtime invariants, and zero markdown.
SpecFerret is built in five strict layers. No layer reaches into another layer's domain.
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLI β
β ferret.ts Β· init Β· scan Β· lint Β· review β
β extract Β· status Β· watch Β· audit β
β Reads config, calls core, prints, exits. β
β Max 50 lines per command file. β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Reconciler β
β BFS traversal Β· flags dirty nodes Β· report β
β Never parses files. Never formats output. β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Validator β
β JSON Schema subset Β· breaking / non-breaking β
β Pure function. No side effects. No I/O. β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Extractor β
β .contract.md Β· .contract.ts Β· .ts (tree-sitter)β
β Never talks to the store or the graph. β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Store β
β SQLite (default) Β· Postgres (roadmap) β
β Parameterised SQL only. No business logic. β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Global install (recommended)
bun install -g @specferret/cliVerify
ferret --versionTip
Bun 1.0 or later is required. Install it at bun.sh.
Install @specferret/core as a library
bun add @specferret/core
# or
npm install @specferret/coreUse @specferret/core directly when you want to integrate the extractor, validator, or reconciler into your own tooling or agent workflow.
1. Install
bun install -g @specferret/cli2. Initialise your project
cd your-project
ferret initThis creates .ferret/ state, ferret.config.json, and installs a pre-commit hook.
Tip
Use ferret init --no-hook to skip the pre-commit hook.
3. Bootstrap contracts (choose one path)
Manual contract-first path:
mkdir -p contractsCode-first TypeScript path:
ferret extractferret extract uses Tree-sitter to infer contracts from exported TypeScript declarations and scaffolds deterministic .contract.md files with no annotations required. @ferret-contract remains supported as a compatibility override for explicit id and type.
Example summary from inferred extraction:
ferret extract created=7 updated=0 skipped=0 failed=0 inferred=12 annotated=2 143ms
4. Lint (default daily command)
ferret lintferret lint runs scan/reconcile checks and reports drift. Use ferret scan directly for advanced/manual graph refresh workflows.
Clean output:
β ferret 12 contracts 0 drift 9ms
Drift detected:
ferret 3 contracts need review
BREAKING auth.jwt
βββ contracts/search.contract.md imports this directly
βββ contracts/recommendations.contract.md imports this directly
βββ contracts/analytics.contract.md imports this transitively (depth 2)
NON-BREAKING tables.document
βββ contracts/search.contract.md optional field added β no action needed
2 breaking 1 non-breaking
β Run ferret review to resolve
5. Resolve
ferret reviewCreate a file under contracts/ with a ferret: frontmatter block:
---
ferret:
id: api.GET/users
type: api
shape:
type: array
items:
type: object
properties:
id:
type: string
email:
type: string
required: [id, email]
---
# GET /users
Returns the list of registered users.Then run:
ferret lintNow change required: [id, email] to required: [id] β removing email from required β and run lint again to see breaking drift detected.
Contract with imports (dependency)
Declare that one contract depends on another using imports:
---
ferret:
id: api.POST/search
type: api
imports:
- auth.jwt
shape:
type: object
properties:
query:
type: string
results:
type: array
required: [query, results]
---
# POST /search
Authenticated search endpoint. Depends on the JWT auth contract.Now if auth.jwt shape changes, api.POST/search is flagged as an impacted consumer.
Code-first: extract contracts from TypeScript
SpecFerret extracts contracts from TypeScript by default, so no annotations are required for normal workflows. Use @ferret-contract only when you need an explicit id or type override:
// @ferret-contract: api.GET/users api
export interface GetUsersResponse {
id: string;
email: string;
}Then run:
ferret extractThis scaffolds .contract.md files under contracts/ using deterministic mapping. Summary output includes created=<n>, updated=<n>, skipped=<n>, and failed=<n>. The command exits non-zero for hard extraction errors, when --perf-budget-ms is exceeded, and with exit code 2 when --perf-budget-ms is invalid.
Inferred output example:
ferret extract created=5 updated=1 skipped=0 failed=0 inferred=5 annotated=1 87ms
Migration from annotation-first repositories:
- Keep existing
@ferret-contractannotations in place initially. - Run
ferret extractand verify generated ids/types match expected contracts. - Remove annotations incrementally where inferred ids/types are already correct.
- Retain annotations only for files that need explicit overrides.
Mixed repository mode is fully supported: inferred and annotated contracts can coexist in the same run.
| Command | Purpose |
|---|---|
ferret init |
Scaffold .ferret/ state, config, and pre-commit hook |
ferret init --no-hook |
Scaffold without installing the pre-commit hook |
ferret scan |
Parse contracts and refresh .ferret/context.json |
ferret lint |
Detect and classify contract drift |
ferret lint --ci |
CI mode β JSON output, exits non-zero on drift |
ferret review |
Resolve blocking drift interactively |
ferret review --json |
Emit versioned review JSON with suggested actions and dependency context |
ferret extract |
Generate contracts from exported TypeScript (annotation override compatible) |
ferret status |
Report current contract drift state (read-only) |
ferret status --json |
Machine-readable status JSON (always exits 0) |
ferret status --export |
Write STATUS.md to the project root |
ferret watch |
Watch contract files and re-lint on change |
ferret audit |
Full bidirectional drift report (downward + upward + integrity) |
ferret diagnostics |
Print import graph diagnostics |
Pre-commit hook behaviour
ferret init writes .git/hooks/pre-commit (when .git/hooks/ exists).
When you git commit, the hook runs ferret lint. If breaking drift is detected, the commit is blocked:
ferret checking staged files...
BREAKING auth.jwt shape changed
βββ 3 downstream contracts need review
commit blocked β run ferret review
Fix drift with ferret review, re-stage, and commit again.
ferret lint output formats
Default (human-readable)
β ferret 12 contracts 0 drift 9ms
CI mode (--ci)
Exits with code 1 when drift exists. Emits a JSON summary to stdout for downstream tools:
{
"drift": true,
"breaking": 2,
"nonBreaking": 1,
"diagnosticsSchemaVersion": "1.0.0",
"diagnostics": [
{
"code": "FERRET_DRIFT_BREAKING",
"severity": "error",
"message": "auth.jwt impacts contracts/search/results.contract.md (direct, depth 1).",
"location": {
"contractId": "auth.jwt",
"filePath": "contracts/search/results.contract.md",
"depth": 1,
"impact": "direct"
},
"remediation": "Run ferret review and resolve downstream drift before merge."
}
],
"contracts": [...]
}Machine-readable diagnostics contract:
diagnosticsSchemaVersionidentifies the stable diagnostics schema version for parsersdiagnostics[].codeis a stable machine key (safe for switch/case routing)diagnostics[].severityis one oferror,warning, orinfodiagnostics[].locationcarries file/contract/node/import context when availablediagnostics[].remediationis human-readable guidance for safe resolution
Parsing guidance:
- Branch your parser by
diagnosticsSchemaVersionfirst - Treat unknown
codevalues as forward-compatible and fall back tomessage - Prefer
code + locationfor automation andremediationfor user-facing hints
Reference sample payloads:
docs/ci-templates/ferret-lint-ci-sample.jsondocs/ci-templates/ferret-review-json-sample.json
End-to-end agent example (ferret review --json):
- Run
ferret lint --ciand collect diagnostics. - If drift is detected, run
ferret review --json. - For each
reviewable[]item:
- Use
suggestedActions[]ordered byconfidenceto pick a default action. - Use
dependencyContext.directDependentsanddependencyContext.transitiveDependentsto scope impacted files.
- Apply the selected action with
ferret review --contract <id> --action <accept|update|reject> --json. - Re-run
ferret lint --ciand confirmconsistent: truebefore merge.
Instead of markdown frontmatter, define contracts directly in TypeScript with full type safety:
// contracts/auth.contract.ts
import { z } from 'zod';
import { defineContract } from '@specferret/core';
export const jwtPayload = defineContract({
id: 'auth.jwt',
value: 'JWT authentication payload',
output: {
sub: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'viewer']),
iat: z.number(),
},
invariants: [(r) => r.role !== ''],
status: 'active',
});Cross-file upward drift with source (v0.5.0)
Add a source field to point upward drift tracking at a named TypeScript type in your src/ directory. SpecFerret will compare the Zod output schema against the live implementation type via tree-sitter on every ferret lint:
// contracts/keywords.contract.ts
export const apiGetKeywords = defineContract({
id: 'api.getKeywords',
value: 'Keywords endpoint',
output: {
keywords: z.array(z.object({ keyword: z.string(), created_at: z.string() })),
limit: z.number(),
remaining: z.number(),
},
source: { file: 'src/routes/keywords.ts', symbol: 'KeywordsResponse' },
});// src/routes/keywords.ts
export type KeywordsResponse = {
keywords: Array<{ keyword: string; created_at: string }>;
limit: number;
remaining: number;
};Now ferret lint detects drift between the Zod contract and the actual KeywordsResponse type β two representations, one enforcement check, no markdown spec needed.
**Why `.contract.ts`?**
- **`tsc` enforces your contract** β shape errors are compile errors, not runtime surprises
- **Zod schemas** β converted to JSON Schema automatically via `zod-to-json-schema`
- **Runtime invariants** β typed functions, not text descriptions
- **`consumes` references** β object references, not string IDs. Upstream shape changes break downstream contracts at compile time
- **`contract.schema`** β `defineContract` returns a `ZodObject<T>` on every contract. Use it for composition: `z.array(jwtPayload.schema)`, `z.object({ token: jwtPayload.schema })`. Do not compose with `contract.output` β that is the raw shape, not a schema.
> [!NOTE]
> v0.4.2 requires **zod@4**. If your project is on zod@3, upgrade: `bun add zod@4`.
**Using it:**
```bash
# .contract.ts files are discovered automatically β no config needed
ferret lint
# Disable if needed:
# ferret.config.json β "contractParsers": { "typescript": false }
Both .contract.md and .contract.ts work in the same project. Mixed format is fully supported.
Contract with consumes (typed dependency)
// contracts/search.contract.ts
import { z } from 'zod';
import { defineContract } from '@specferret/core';
import { jwtPayload } from './auth.contract.js';
export const searchResults = defineContract({
value: 'Search results endpoint',
output: {
query: z.string(),
results: z.array(z.object({ id: z.string(), title: z.string() })),
},
consumes: [jwtPayload],
});The consumes reference is resolved by object identity β no string matching. If jwtPayload changes shape, searchResults is flagged as impacted.
Check contract status
ferret statusferret status 12 contracts
stable 10
needs-review 2
NEEDS REVIEW
auth.jwt breaking β 3 dependents
tables.document non-breaking β 1 dependent
Machine-readable: ferret status --json
Export to markdown: ferret status --export (writes STATUS.md)
| Type | Use for |
|---|---|
api |
REST endpoints, GraphQL operations, RPC methods |
table |
Database tables, collections, document schemas |
type |
Shared TypeScript types, interfaces, enums |
event |
Domain events, webhooks, message queue payloads |
flow |
User flows and multi-step process contracts |
config |
Configuration shapes, environment schemas, flags |
Each type uses the same frontmatter convention. The type field is used for grouping and reporting.
Type semantics are strict at runtime:
ferret.typemust be exactly one of:api,table,type,event,flow,config- Unknown values are rejected during extraction with an error that lists allowed values
- Custom type namespaces are not currently supported as runtime extensions
Extension model:
- If a contract does not fit one of the six core types, use the closest core type and open an issue for model expansion
Migration note for legacy repos:
- Replace legacy values like
schema,service, ormodelwith the closest supported core type before runningferret scanorferret lint
See CHANGELOG.md for versioned changes and migration notes.
When ferret lint detects drift it classifies every violation:
| Classification | Trigger | Action required |
|---|---|---|
BREAKING |
Required field removed, property type changed | Must resolve before merge |
NON-BREAKING |
Optional field added, description changed only | No action required |
The impact report shows:
- The drifting contract id and source file
- Every downstream contract that
importsit β direct and transitive - Depth of the transitive chain
Run ferret review to step through each breaking drift, choose an action (accept / update / reject), and write resolution notes back to the contract file.
End-to-end agent loop (`ferret review --json`)
- Ask your agent to run:
ferret review --json- Agent reads each review item and prioritizes
suggestedActionsbyconfidence. - Agent uses
dependencyContextand the direct/transitiveimpactlists to update downstream files in blast-radius order. - Agent re-runs:
ferret lint --ci- If clean, agent proceeds with commit/PR. If drift remains, it repeats the review loop with the new JSON payload.
Minimal GitHub Actions step (reusable action)
- name: SpecFerret CI
uses: BenGardiner123/action@v1
with:
baseline-mode: committedFull job example
jobs:
ferret:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: SpecFerret CI
uses: BenGardiner123/action@v1
with:
baseline-mode: committed
artifact-name: ferret-lint-ciStarter templates:
docs/ci-templates/ferret-action-single-package.ymldocs/ci-templates/ferret-action-monorepo.ymldocs/ci-templates/ferret-action-pr-only.yml
Tip
Commit .ferret/context.json to your repo. The default --ci-baseline committed mode reads from it, so CI passes on the first run without a rebuild step.
Baseline modes
| Mode | When to use |
|---|---|
--ci-baseline committed |
Default. Requires committed context.json. |
--ci-baseline rebuild |
Ephemeral runners or fresh checkouts. |
SpecFerret is not a planning tool. It enforces contract consistency downstream of planning tools.
- Produce PRD, architecture, and stories as usual
- Capture concrete shapes in
.contract.mdfiles undercontracts/ - Run
ferret lintto validate and enforce consistency across the team
Example repo layout
your-project/
_bmad-output/
PRD.md
architecture.md
contracts/
auth/
jwt.contract.md
tables/
user.contract.md
api/
get-users.contract.md
.ferret/
context.json
ferret.config.json
Tip
Ask your AI agent to ferret lint --ci as a sub-step whenever it generates or edits code that might touch a contract boundary. This catches AI-introduced drift before it reaches review.
API teams preventing breaking changes
Map every REST endpoint, GraphQL operation, or RPC method to a .contract.md file. Declare which other contracts import each API surface. When a shape change breaks a required field on a consumer, ferret lint catches it in seconds β not days.
Multi-team monorepos
Teams working in the same repo often change shared types without realising the downstream blast radius. SpecFerret's dependency graph shows every contract that transitively depends on a changed type β and blocks the commit until drift is resolved or accepted.
AI-assisted development
AI code generation accelerates implementation but outpaces documentation. Add ferret lint --ci to your CI pipeline so that every AI-generated PR is checked for contract drift. Use ferret review --json to feed the impact report back to your agent for automated resolution.
Schema governance
Long-lived product surfaces β database tables, event payloads, config shapes β drift over years. SpecFerret's SQLite graph persists the contract state so you always have a before/after diff of every shape change, not just a snapshot.
specferret/
βββ packages/
β βββ core/ @specferret/core
β β βββ src/
β β βββ store/ # DBStore interface Β· SQLite Β· Postgres Β· factory
β β βββ extractor/ # frontmatter.ts Β· typescript.ts Β· typescript-contract.ts Β· validator.ts Β· upward-classifier.ts Β· hash.ts
β β βββ reconciler/ # BFS engine β dirty node flagging
β β βββ context/ # context.json writer
β β βββ watch/ # file watcher
β β βββ audit/ # bidirectional audit report
β β βββ config.ts # project config loader
β β βββ index.ts # public re-exports
β βββ cli/ @specferret/cli
β βββ bin/
β βββ commands/ # init Β· scan Β· lint Β· review Β· extract Β· status Β· watch Β· audit
β βββ ferret.ts # CLI entrypoint
βββ apps/
β βββ site/ specferret.dev (Astro)
βββ spec/ Architecture, stories, contract schema
βββ docs/ Quickstart, demo runbook, images
βββ scripts/ Build helpers
PRs welcome. The codebase is intentionally small and readable.
- Postgres store β production-grade persistence without SQLite
-
ferret auditβ bidirectional drift report across all contracts (shipped v0.4.0) -
roadmapβpendingstatus rename β contract lifecycle usespendingeverywhere;ferret lintshowsβonly when pending count is zero;context.jsonv3.0 (shipped v0.5.0 / S62) -
sourcefield ondefineContract()β cross-file upward drift for.contract.tsprojects without the three-representation trap (shipped v0.5.0 / S63) - Auto-infer
stablefromsourceduringferret scanβ contracts auto-promote frompendingβstablewhen source resolves clean; no manual declaration needed (shipped v0.6.0) -
ferret upgradeβ SQLite β Postgres migration command -
ferret placeβ AI-powered feature placement against the graph -
ferret benchmarkβ provider benchmarking for AI-assisted review - GitHub Action adoption (Sprint 6) β one-line CI enforcement via
uses: BenGardiner123/action@v1 - Upward code-to-spec drift (Sprint 7) β catch implementation drift when specs are not updated
- TypeScript-native contracts (Sprint 8) β
.contract.tswithdefineContract(), Zod schemas,ferret status - Tree-sitter extraction β TypeScript shape extraction without annotations (shipped)
- Multi-language support β Go, Python, OpenAPI (post Phase 5)
- Hosted dashboard β team-wide contract health, analytics, SSO/RBAC
- Branch-matrix dogfooding (Sprint 5) β scenario branches across spec-kit and BMAD validation repos
See spec/ROADMAP.MD for the full plan.
Latest validated evidence (post explicit S40/S41/S42 assertions):
| Validation | Link |
|---|---|
| spec-kit matrix run | actions/runs/23962649755 |
| BMAD matrix run | actions/runs/23962652352 |
Release evidence (v0.1.3 publish window):
| Validation | Link |
|---|---|
| spec-kit validation run | actions/runs/23962649755 |
| BMAD validation run | actions/runs/23962652352 |
@specferret/core@0.1.3 on npm |
npmjs.com/package/@specferret/core |
@specferret/cli@0.1.3 on npm |
npmjs.com/package/@specferret/cli |
Sprint 5 expands this from a single smoke path to a branch matrix. Operational details live in spec/GA-VALIDATION-REPOS.MD and spec/GA-VALIDATION-RUNBOOK.MD.
bun install
bun test
bun run buildPerformance benchmark guardrails
Use explicit performance budgets when validating contributor changes:
# Lint clean-run budget (project baseline target)
ferret lint --perf-budget-ms 500
# Extract runtime budget (tune per fixture/repo size in CI)
ferret extract --perf-budget-ms 1000If a budget is exceeded, the command exits non-zero and prints an actionable diagnostic.
Documentation safety rule
- Never include PII or secrets in docs, contracts, runbooks, screenshots, logs, or release evidence.
- Always use synthetic placeholders (for example
user@example.test,REDACTED_TOKEN). - Redact sensitive values before commit.
Monorepo packages
| Package | Path | npm |
|---|---|---|
@specferret/core |
packages/core |
|
@specferret/cli |
packages/cli |
|
specferret.dev (Astro) |
apps/site |
β |
Docs
MIT License Β· specferret.dev Β· "SpecFerret keeps your specs honest."