Bidirectional spec-to-code validation with cross-project references. Written in Rust. Single binary. 9 languages.
Quick Start • Spec Format • CLI • Cross-Project Refs • GitHub Action • Config • Docs Site
SpecSync validates markdown module specs (*.spec.md) against your source code in both directions:
| Direction | Severity | Meaning |
|---|---|---|
| Code exports something not in the spec | Warning | Undocumented export |
| Spec documents something missing from code | Error | Stale/phantom entry |
| Source file in spec was deleted | Error | Missing file |
| DB table in spec missing from schema | Error | Phantom table |
| Required markdown section missing | Error | Incomplete spec |
Auto-detected from file extensions. Same spec format for all.
| 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 (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 (no _ prefix), class/mixin/enum/typedef |
*_test.dart |
- uses: CorvidLabs/spec-sync@v2
with:
strict: 'true'
require-coverage: '100'cargo install specsyncDownload from GitHub Releases:
# macOS (Apple Silicon)
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-macos-aarch64.tar.gz | tar xz
sudo mv specsync-macos-aarch64 /usr/local/bin/specsync
# macOS (Intel)
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-macos-x86_64.tar.gz | tar xz
sudo mv specsync-macos-x86_64 /usr/local/bin/specsync
# Linux (x86_64)
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-linux-x86_64.tar.gz | tar xz
sudo mv specsync-linux-x86_64 /usr/local/bin/specsync
# Linux (aarch64)
curl -sL https://github.com/CorvidLabs/spec-sync/releases/latest/download/specsync-linux-aarch64.tar.gz | tar xz
sudo mv specsync-linux-aarch64 /usr/local/bin/specsyncWindows: download specsync-windows-x86_64.exe.zip from the releases page.
cargo install --git https://github.com/CorvidLabs/spec-syncspecsync init # Create specsync.json config
specsync check # Validate specs against code
specsync check --fix # Auto-add undocumented exports as stubs
specsync diff # Show exports added/removed since HEAD
specsync diff HEAD~5 # Compare against a specific ref
specsync coverage # Show file/module coverage
specsync generate # Scaffold specs for unspecced modules
specsync generate --provider auto # AI-powered specs (auto-detect provider)
specsync add-spec auth # Add a single spec + companion files
specsync init-registry # Generate specsync-registry.toml
specsync resolve # Verify spec cross-references
specsync resolve --remote # Verify cross-project refs via GitHub
specsync score # Quality-score your spec files (0–100)
specsync hooks install # Install agent instructions + git hooks
specsync hooks status # Check what's installed
specsync mcp # Start MCP server for AI agent integration
specsync watch # Re-validate on every file changeSpecs are markdown files (*.spec.md) with YAML frontmatter in your specs directory.
---
module: auth # Module name (required)
version: 3 # Spec version (required)
status: stable # draft | review | stable | deprecated (required)
files: # Source files covered (required, non-empty)
- src/auth/service.ts
- src/auth/middleware.ts
db_tables: # Validated against schema dir (optional)
- users
- sessions
depends_on: # Other spec paths, validated for existence (optional)
- specs/database/database.spec.md
- corvid-labs/algochat@messaging # Cross-project ref (owner/repo@module)
---Every spec must include these ## sections (configurable in specsync.json):
Purpose, Public API, Invariants, Behavioral Examples, Error Cases, Dependencies, Change Log
SpecSync extracts the first backtick-quoted name per row and cross-references it against code exports:
## Public API
| Function | Parameters | Returns | Description |
|----------|-----------|---------|-------------|
| `authenticate` | `(token: string)` | `User \| null` | Validates bearer token |
| `refreshSession` | `(sessionId: string)` | `Session` | Extends session TTL |Full spec example
---
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
---
# Auth
## Purpose
Handles authentication and session management. Validates bearer tokens,
manages session lifecycle, provides middleware for route protection.
## Public API
### Exported Functions
| Function | Parameters | Returns | Description |
|----------|-----------|---------|-------------|
| `authenticate` | `(token: string)` | `User \| null` | Validates a token |
| `refreshSession` | `(sessionId: string)` | `Session` | Extends session TTL |
### Exported Types
| Type | Description |
|------|-------------|
| `User` | Authenticated user object |
| `Session` | Active session record |
## Invariants
1. Sessions expire after 24 hours
2. Failed auth attempts rate-limited to 5/minute
3. Tokens validated cryptographically, never by string comparison
## Behavioral Examples
### Scenario: Valid token
- **Given** a valid JWT token
- **When** `authenticate()` is called
- **Then** returns the corresponding User object
### Scenario: Expired token
- **Given** an expired JWT token
- **When** `authenticate()` is called
- **Then** returns null and logs a warning
## Error Cases
| Condition | Behavior |
|-----------|----------|
| Expired token | Returns null, logs warning |
| Malformed token | Returns null |
| DB unavailable | Throws `ServiceUnavailableError` |
## Dependencies
| Module | Usage |
|--------|-------|
| database | `query()` for user lookups |
| crypto | `verifyJwt()` for token validation |
## Change Log
| Date | Change |
|------|--------|
| 2026-03-18 | Initial spec |specsync [command] [flags]
| Command | Description |
|---|---|
check |
Validate all specs against source code (default). --fix auto-adds missing exports as stubs |
diff |
Show exports added/removed since a git ref (default: HEAD) |
coverage |
File and module coverage report |
generate |
Scaffold specs for modules missing one (--provider for AI-powered content) |
add-spec <name> |
Scaffold a single spec + companion files (tasks.md, context.md) |
resolve |
Verify depends_on references exist. --remote fetches registries from GitHub |
init-registry |
Generate specsync-registry.toml from existing specs |
score |
Quality-score spec files (0–100) with improvement suggestions |
hooks |
Install/uninstall agent instructions and git hooks (install, uninstall, status) |
mcp |
Start MCP server for AI agent integration (Claude Code, Cursor, etc.) |
init |
Create default specsync.json |
watch |
Live validation on file changes (500ms debounce) |
| Flag | Description |
|---|---|
--strict |
Warnings become errors (recommended for CI) |
--require-coverage N |
Fail if file coverage < N% |
--root <path> |
Project root (default: cwd) |
--provider <name> |
AI provider: auto, anthropic, openai, or command. auto detects installed provider. Without --provider, generate uses templates only. |
--fix |
Auto-add undocumented exports as stub rows in spec Public API tables |
--json |
Structured JSON output |
| Code | Meaning |
|---|---|
0 |
All checks passed |
1 |
Errors, strict warnings, or coverage below threshold |
Specs can declare dependencies on modules in other repositories using owner/repo@module syntax in depends_on:
depends_on:
- specs/database/database.spec.md # Local reference
- corvid-labs/algochat@messaging # Cross-project referenceEach project publishes a specsync-registry.toml at its root to declare available spec modules:
[registry]
name = "myapp"
[specs]
auth = "specs/auth/auth.spec.md"
messaging = "specs/messaging/messaging.spec.md"
database = "specs/db/database.spec.md"Generate one automatically from existing specs:
specsync init-registry # Uses project folder name
specsync init-registry --name myapp # Custom registry namespecsync resolve # Verify local refs exist
specsync resolve --remote # Also verify cross-project refs via GitHubRemote resolution fetches specsync-registry.toml from each referenced repo and validates that the module exists. Requests are grouped by repo to minimize HTTP calls.
Zero CI cost by default — specsync check validates local refs only (no network). Use --remote explicitly when you want cross-project verification.
When you run specsync generate or specsync add-spec, two companion files are created alongside each spec:
tasks.md — Multi-role checkpoint tracking:
---
spec: auth.spec.md
---
## Tasks
- [ ] <!-- Implementation checklist -->
## Gaps
<!-- Uncovered areas, missing edge cases -->
## Review Sign-offs
- **Product**: pending
- **QA**: pending
- **Design**: n/a
- **Dev**: pendingcontext.md — Agent briefing document:
---
spec: auth.spec.md
---
## Key Decisions
<!-- Architectural or design decisions -->
## Files to Read First
<!-- Most important files for understanding this module -->
## Current Status
<!-- What's done, in progress, blocked -->
## Notes
<!-- Free-form notes, links, context -->These files are designed for team coordination and AI agent context — they give any contributor (human or AI) the full picture of where a module stands.
Available on the GitHub Marketplace. Auto-detects OS/arch, downloads the binary, runs validation.
| Input | Default | Description |
|---|---|---|
version |
latest |
Release version to download |
strict |
false |
Treat warnings as errors |
require-coverage |
0 |
Minimum file coverage % |
root |
. |
Project root directory |
args |
'' |
Extra CLI arguments |
name: Spec Check
on: [push, pull_request]
jobs:
specsync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: CorvidLabs/spec-sync@v2
with:
strict: 'true'
require-coverage: '100'Create specsync.json or .specsync.toml in your project root (or run specsync init):
{
"specsDir": "specs",
"sourceDirs": ["src"],
"schemaDir": "db/migrations",
"requiredSections": ["Purpose", "Public API", "Invariants", "Behavioral Examples", "Error Cases", "Dependencies", "Change Log"],
"excludeDirs": ["__tests__"],
"excludePatterns": ["**/__tests__/**", "**/*.test.ts", "**/*.spec.ts"],
"sourceExtensions": [],
"aiCommand": "claude -p --output-format text",
"aiTimeout": 120
}| Option | Type | Default | Description |
|---|---|---|---|
specsDir |
string |
"specs" |
Directory containing *.spec.md files |
sourceDirs |
string[] |
["src"] |
Source directories for coverage analysis |
schemaDir |
string? |
— | SQL schema dir for db_tables validation |
schemaPattern |
string? |
CREATE TABLE regex |
Custom regex for table name extraction |
requiredSections |
string[] |
7 defaults | Markdown sections every spec must include |
excludeDirs |
string[] |
["__tests__"] |
Directories excluded from coverage |
excludePatterns |
string[] |
Common test globs | File patterns excluded from coverage |
sourceExtensions |
string[] |
All supported | Restrict to specific extensions (e.g., ["ts", "rs"]) |
aiCommand |
string? |
claude -p ... |
Command for generate --provider command (reads stdin prompt, writes stdout markdown) |
aiProvider |
string? |
— | Default AI provider (auto, claude, anthropic, openai, ollama) |
aiTimeout |
number? |
120 |
Seconds before AI command times out per module |
# .specsync.toml
specs_dir = "specs"
source_dirs = ["src", "lib"]
required_sections = ["Purpose", "Public API", "Invariants", "Behavioral Examples", "Error Cases", "Dependencies", "Change Log"]
ai_provider = "claude"
ai_timeout = 120Config resolution order: specsync.json → .specsync.toml → defaults with auto-detected source dirs.
specsync generate scans your source directories, finds modules without spec files, and scaffolds *.spec.md files for each one.
specsync generate # Scaffold template specs for all unspecced modules
specsync generate --provider auto # Use AI to generate filled-in specs from source code
specsync coverage # See what's still missingUses your custom template (specs/_template.spec.md) or the built-in default. Generates frontmatter + stubbed sections with TODOs.
Reads your source code, sends it to an LLM, and generates specs with real content — Purpose, Public API tables, Invariants, Error Cases, all filled in from the code. No manual filling required.
The AI command is resolved in order:
"aiCommand"inspecsync.jsonSPECSYNC_AI_COMMANDenvironment variableclaude -p --output-format text(default, requires Claude CLI)
Any command that reads a prompt from stdin and writes markdown to stdout works:
{ "aiCommand": "claude -p --output-format text" }
{ "aiCommand": "ollama run llama3" }Set "aiTimeout" in specsync.json to control per-module timeout (default: 120 seconds).
The generate command is the entry point for LLM-powered spec workflows:
specsync generate --provider auto # AI writes specs from source code
specsync check --fix # auto-add any missing exports as stubs
specsync check --json # validate, get structured feedback
# LLM fixes errors from JSON output # iterate until clean
specsync check --strict --require-coverage 100 # enforce full coverage in CIEvery output format is designed for machine consumption:
--jsonon any command → structured JSON, no ANSI codes- Exit code 0/1 → pass/fail, no parsing needed
- Spec files are plain markdown → any LLM can read and write them
- Public API tables use backtick-quoted names → unambiguous to extract
// specsync check --json
{ "passed": false, "errors": ["..."], "warnings": ["..."], "specs_checked": 12 }
// specsync coverage --json
{ "file_coverage": 85.33, "files_covered": 23, "files_total": 27, "loc_coverage": 79.12, "loc_covered": 4200, "loc_total": 5308, "modules": [...] }
// specsync diff HEAD~3 --json
{ "added": ["newFunction", "NewType"], "removed": ["oldHelper"], "spec": "specs/auth/auth.spec.md" }specsync check --fix # Add undocumented exports as stub rows
specsync check --fix --json # Same, with structured JSON outputWhen --fix is used, any export found in code but missing from the spec gets appended as a stub row (| \name` | | | TODO |) to the Public API table. If no ## Public API` section exists, one is created. Already-documented exports are never duplicated.
This turns spec maintenance from manual table editing into a review-and-refine workflow — run --fix, then fill in the descriptions.
specsync diff # Changes since HEAD (staged + unstaged)
specsync diff HEAD~5 # Changes since 5 commits ago
specsync diff v2.1.0 # Changes since a tagShows exports added and removed per spec file since the given git ref. Useful for code review, release notes, and CI drift detection.
src/
├── main.rs CLI entry + output formatting
├── ai.rs AI-powered spec generation (prompt builder + command runner)
├── mcp.rs MCP server for AI agent integration (JSON-RPC stdio)
├── registry.rs Registry loading, generation, and remote fetching
├── scoring.rs Spec quality scoring (0–100, weighted rubric)
├── types.rs Data types + config schema
├── config.rs specsync.json / .specsync.toml loading
├── parser.rs Frontmatter + spec body parsing
├── validator.rs Validation + coverage + cross-project ref detection
├── generator.rs Spec + companion file scaffolding
├── hooks.rs Agent instruction + git hook management
├── watch.rs File watcher (notify, 500ms debounce)
└── exports/
├── mod.rs Language dispatch
├── typescript.rs TS/JS exports
├── rust_lang.rs Rust pub items
├── go.rs Go uppercase identifiers
├── python.rs Python __all__ / top-level
├── swift.rs Swift public/open items
├── kotlin.rs Kotlin top-level
├── java.rs Java public items
├── csharp.rs C# public items
└── dart.rs Dart public items
Design: Single binary, no runtime deps. Frontmatter parsed with regex (no YAML library). Language backends use regex, not ASTs — works without compilers installed. Release builds use LTO + strip + opt-level 3.
- Fork, branch (
git checkout -b feat/my-feature), implement cargo test+cargo clippy- Open a PR
- Create
src/exports/yourlang.rs— returnVec<String>of exported names - Add variant to
Languageenum intypes.rs - Wire extension detection + dispatch in
src/exports/mod.rs - Add tests for common patterns