The contract layer
Specs as
CI-enforced contracts.
Markdown *.spec.md files are the
contract. spec-sync checks that the code matches the spec, in
both directions, and fails CI when
they drift. 12 languages, one format, a single Rust binary.
$ cargo install specsync
# Verify
$ specsync --version
specsync 4.5.0
# Validate specs against code
$ specsync check
✓ greeter (v1, draft): 7/7 sections
1 spec checked, 0 errors, 0 warnings
The idea
A spec only helps if it cannot lie.
A README rots the day it is written. spec-sync treats every
*.spec.md as a contract and
diffs it against the source, both ways. Code with no spec entry is a warning;
a spec entry with no code is an error. Neither side can quietly fall behind,
because the diff runs in CI on every pull request.
Auto-detected from file extensions. The same markdown contract covers Rust, TypeScript, Go, Python, Swift, and seven more.
Code that drifts from the spec, and a spec that drifts from the code. Severity is calibrated so each gets the right signal.
Purpose through Change Log. A spec missing any of them is an error, so contracts stay complete by construction.
Three powers
Detect, fix, and prevent spec drift.
From discovery to enforcement. Each power is a focused subcommand you wire into any CI. Drift is the gap between what your specs say and what your code actually does.
Step 1
Detect drift
Find the gap between what your specs say and what your code does: undocumented exports, stale entries, broken cross-references. 12 languages, auto-detected from file extensions.
Step 2
Fix drift
Patch undocumented exports and stale spec entries with AI-assisted generation. Reads source, writes real Purpose, Public API, and Invariants. Ten providers, or local Ollama keyless.
Step 3
Prevent drift
Make drift impossible to merge. Inline squiggles in VS Code on save, a strict gate on every PR via the GitHub Action. Drift becomes a build failure, not a review nag.
The spec format
One markdown file is the whole contract.
A spec is markdown with YAML frontmatter. The frontmatter declares the module, its source files, and any dependencies; the body has seven required sections. spec-sync extracts the first backtick-quoted name in each Public API row and matches it to a code export.
auth.spec.md
module: auth
version: 3
status: stable
files:
- src/auth/service.ts
- src/auth/middleware.ts
db_tables:
- users
- sessions
depends_on:
- specs/database/database.spec.md
- corvid-labs/algochat@messaging
---
## Public API
| Function | Returns |
| `authenticate` | `User | null` |
Seven required sections
- Purpose. What the module does, in one paragraph.
- Public API. Every exported function, type, and class. Each row's backticked name is matched to a code export.
- Invariants. Properties that must always hold.
- Behavioral Examples. Inputs paired with their expected outputs.
- Error Cases. What can go wrong and how it surfaces.
- Dependencies. Other modules and external services.
- Change Log. Append-only history, one entry per version.
Key concepts
The vocabulary; everything else composes from these.
- spec
- A
*.spec.mdmarkdown file with YAML frontmatter. It is the contract for one module. - module
- A named unit declared by
module:in frontmatter, covering one or more sourcefiles:. - drift
- The gap between what a spec documents and what the code actually exports.
- coverage
- The share of source files claimed by a spec.
require-coverage: '100'gates on it. - cross-project ref
owner/repo@moduleindepends_on:, resolved withspecsync resolve --remote.- provider
- An AI backend for
generate:anthropic,openai,ollama, and seven more, orauto.
Twelve languages
Same contract, every stack.
spec-sync detects the language from the file extension and parses exactly the symbols that count as public, skipping test files automatically. The language reference has the per-language detail.
| Language | Exports detected |
|---|---|
| TypeScript / JS | export function / class / type / const / enum, re-exports, export * wildcard resolution |
| Rust | pub fn / struct / enum / trait / type / const / static / mod |
| Go | Uppercase func / type / var / const, methods |
| Python | __all__, or top-level def / class with no _ prefix |
| Swift | public / open func / class / struct / enum / protocol / actor |
| Kotlin | Top-level declarations, excludes private / internal |
| Java | public class / interface / enum / record / methods |
| C# | public class / struct / interface / enum / record / delegate |
| Dart | Top-level with no _ prefix: class / mixin / enum / typedef |
| PHP | class / interface / trait / enum, public function / const |
| Ruby | class / module, public methods, attr_accessor / reader / writer, constants |
| YAML | Top-level mapping keys, anchors and aliases supported |
Calibrated severities
What counts as drift.
Errors fail CI; warnings annotate. The split is deliberate: a spec that promises code that does not exist is a broken contract (error), while code that has outrun its docs is a nudge (warning). Schema-aware validation tracks DB tables and columns across migrations the same way.
| Drift | Severity |
|---|---|
| Code exports something not in the spec | warning |
| Spec documents something missing from code | error |
| Source file in spec was deleted | error |
| DB table in spec missing from schema | error |
| Column in spec missing from migrations | error |
| Column in schema not documented in spec | warning |
| Column type mismatch (spec vs schema) | warning |
| Required markdown section missing | error |
Make drift unmergeable
A gate in CI, a squiggle in your editor.
The CorvidLabs/spec-sync@v4
Action auto-detects OS and arch, downloads the binary, and runs the check on
every push and PR. The VS Code extension mirrors the CLI exactly, so dev and CI
agree. When drift is real, AI-assisted generation patches it.
.github/workflows/spec.yml
jobs:
specsync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: CorvidLabs/spec-sync@v4
with:
strict: 'true'
require-coverage: '100'
comment: 'true'
Action inputs
| Input | Default |
|---|---|
version | latest Release version of the binary to download. |
strict | false Treat warnings as errors, so undocumented exports also fail CI. |
require-coverage | 0 Minimum file coverage percent the run must meet. |
root | . Project root directory to validate. |
comment | false Post the drift summary as a PR comment (needs a pull_request event). |
args | '' Extra whitespace-separated CLI arguments; shell quoting is not supported. |
The VS Code extension
- Inline diagnostics. Errors and warnings map to the spec file, on save, with a 500ms debounce.
- CodeLens scores. A 0 to 100 quality score is shown inline above each spec file.
- Coverage webview. A rich panel with per-file and per-line coverage.
- Status bar. A persistent pass, fail, or error indicator for the workspace.
$ code --install-extension corvidlabs.specsync
AI-assisted fix
$ specsync generate
# Auto-detect provider, write real content
$ specsync generate --provider auto
# Local Ollama, zero config, keyless
$ specsync generate --model llama3.3
Calls go through the shared corvid-ai
client over plain HTTP. Providers: anthropic, openai, openrouter, gemini,
deepseek, groq, mistral, xai, together, and ollama. Resolution is
flag, then env, then config; with no key set it falls back to local Ollama.
Where it ships
- Language
- Rust, single static binary
- Crate
- specsync on crates.io
- Action
- CorvidLabs/spec-sync@v4 (GitHub Marketplace)
- Editor
- corvidlabs.specsync (VS Code Marketplace)
- AI client
- corvid-ai, plain HTTP, no CLI tool required
- Latest
- v4.5.0
- License
- MIT
- Languages
- 12, auto-detected
Where it's used
Every CorvidLabs repo with a spec runs
specsync check as a CI gate,
typically inside fledge's
verify lane.
- → corvid-chat 23 specs across proto, crypto, server, federation, invites, admin, interactions, and merlin-bridge
- → Merlin spec-sync is the agent contract; drift equals a failed task
- → fledge ↗ the spec-check lane runs specsync check
v4.5.0 is live
v4.5.0 shipped 13 days ago (Jun 11, 2026).
The 5-minute quickstart is a real walkthrough: a fresh crate to a green check in five copy-paste steps.