Skip to content

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.

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.

12 languages, one format

Auto-detected from file extensions. The same markdown contract covers Rust, TypeScript, Go, Python, Swift, and seven more.

2 directions, checked

Code that drifts from the spec, and a spec that drifts from the code. Severity is calibrated so each gets the right signal.

7 required sections

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.

$ specsync check

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.

$ specsync generate --provider auto

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.

uses: CorvidLabs/spec-sync@v4

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

  1. Purpose. What the module does, in one paragraph.
  2. Public API. Every exported function, type, and class. Each row's backticked name is matched to a code export.
  3. Invariants. Properties that must always hold.
  4. Behavioral Examples. Inputs paired with their expected outputs.
  5. Error Cases. What can go wrong and how it surfaces.
  6. Dependencies. Other modules and external services.
  7. Change Log. Append-only history, one entry per version.

Key concepts

The vocabulary; everything else composes from these.

spec
A *.spec.md markdown file with YAML frontmatter. It is the contract for one module.
module
A named unit declared by module: in frontmatter, covering one or more source files:.
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@module in depends_on:, resolved with specsync resolve --remote.
provider
An AI backend for generate: anthropic, openai, ollama, and seven more, or auto.

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 Test exclusions
TypeScript / JS export function / class / type / const / enum, re-exports, export * wildcard resolution .test.ts, .spec.ts, .d.ts
Rust pub fn / struct / enum / trait / type / const / static / mod #[cfg(test)] modules
Go Uppercase func / type / var / const, methods _test.go
Python __all__, or top-level def / class with no _ prefix test_*.py, *_test.py
Swift public / open func / class / struct / enum / protocol / actor *Tests.swift
Kotlin Top-level declarations, excludes private / internal *Test.kt, *Spec.kt
Java public class / interface / enum / record / methods *Test.java, *Tests.java
C# public class / struct / interface / enum / record / delegate *Test.cs, *Tests.cs
Dart Top-level with no _ prefix: class / mixin / enum / typedef *_test.dart
PHP class / interface / trait / enum, public function / const *Test.php
Ruby class / module, public methods, attr_accessor / reader / writer, constants *_test.rb, *_spec.rb
YAML Top-level mapping keys, anchors and aliases supported none

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 specwarning
Spec documents something missing from codeerror
Source file in spec was deletederror
DB table in spec missing from schemaerror
Column in spec missing from migrationserror
Column in schema not documented in specwarning
Column type mismatch (spec vs schema)warning
Required markdown section missingerror

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

on: [push, pull_request]
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

# Template-only, no provider needed
$ 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.

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.