SpecSync
Bidirectional spec-to-code validation — keep your docs honest.
Written in Rust. Language-agnostic. Blazing fast.
Quick Start • Spec Format • CLI Reference • GitHub Action • Configuration • Documentation Site
The Problem
Documentation drifts. Engineers add new exports but forget to update the spec. Specs reference functions that were renamed months ago. Nobody notices until a new team member reads the docs and gets confused.
SpecSync catches this automatically.
What It Does
SpecSync validates your markdown module specs against actual source code — in both directions:
| Situation | Severity | Message |
|---|---|---|
| Code exports something not in the spec | Warning | Undocumented export |
| Spec documents something that doesn't exist | Error | Stale/phantom spec entry |
| Source file referenced in spec was deleted | Error | Missing file |
| DB table in spec doesn't exist in schema | Error | Phantom table |
| Required section missing from spec | Error | Incomplete spec |
Supported Languages
SpecSync auto-detects the language from file extensions. The same spec format works for all of them.
| Language | What Gets Detected | Test Files Excluded |
|---|---|---|
| TypeScript / JavaScript | export function, export class, export type, export const, export enum, re-exports |
.test.ts, .spec.ts, .d.ts |
| Rust | pub fn, pub struct, pub enum, pub trait, pub type, pub const, pub static, pub mod |
Inline #[cfg(test)] modules |
| Go | Uppercase identifiers: func, type, var, const, methods |
_test.go |
| Python | __all__ list, or top-level def/class (excluding _-prefixed) |
test_*.py, *_test.py |
| Swift | public/open func, class, struct, enum, protocol, typealias, actor, init |
*Tests.swift, *Test.swift |
| Kotlin | Top-level declarations (public by default), excludes private/internal/protected |
*Test.kt, *Spec.kt |
| Java | public class, interface, enum, record, methods, fields |
*Test.java, *Tests.java |
| C# | public class, struct, interface, enum, record, delegate, methods |
*Test.cs, *Tests.cs |
| Dart | Top-level declarations (no _ prefix), class, mixin, enum, typedef |
*_test.dart |
Install
GitHub Action (recommended)
Available on the GitHub Marketplace. Add to any workflow:
- uses: CorvidLabs/spec-sync@v1
with:
strict: 'true'
require-coverage: '100'
No binary download or Rust toolchain needed — the action handles everything.
Pre-built binaries
Download from GitHub Releases:
# macOS (Apple Silicon)
|
# macOS (Intel)
|
# Linux (x86_64)
|
# Linux (aarch64)
|
Windows: download specsync-windows-x86_64.exe.zip from the releases page.
From source
# Or clone and build
&&
Crates.io
Quick Start
# 1. Initialize config in your project root
# 2. Validate all specs
# 3. See what's covered and what's missing
# 4. Auto-generate specs for unspecced modules
# 5. Watch mode — re-validates on every file change
Spec Format
Specs are markdown files (*.spec.md) with YAML frontmatter. Place them in your specs directory (default: specs/).
Frontmatter
---
module: auth # Module name (required)
version: 3 # Spec version (required)
status: stable # draft | review | stable | deprecated (required)
files: # Source files this spec covers (required, non-empty)
- src/auth/service.ts
- src/auth/middleware.ts
db_tables: # DB tables used (optional, validated against schema)
- users
- sessions
depends_on: # Other specs this module depends on (optional)
- specs/database/database.spec.md
---
Required Sections
By default, every spec must include these markdown sections (configurable):
| Section | Purpose |
|---|---|
## Purpose |
What this module does and why it exists |
## Public API |
Tables listing exported symbols — this is what gets validated against code |
## Invariants |
Rules that must always hold true |
## Behavioral Examples |
Given/When/Then scenarios |
## Error Cases |
How the module handles failures |
## Dependencies |
What this module consumes from other modules |
## Change Log |
History of spec changes |
Public API Tables
The Public API section uses markdown tables with backtick-quoted symbol names. SpecSync extracts these and cross-references them against actual code exports.
Full Example
module: auth
version: 3
status: stable
files:
- - - - -
Handles authentication and session management. Validates bearer tokens,
manages session lifecycle, and provides middleware for route protection.
1. 2.3.
- --
- --
CLI Reference
specsync [command] [flags]
Commands
| Command | Description |
|---|---|
check |
Validate all specs against source code (default) |
coverage |
Show file and module coverage report |
generate |
Scaffold spec files for modules that don't have one |
init |
Create a default specsync.json config file |
watch |
Live validation — re-runs on file changes (500ms debounce) |
Flags
| Flag | Description |
|---|---|
--strict |
Treat warnings as errors (recommended for CI) |
--require-coverage N |
Fail if file coverage is below N% |
--root <path> |
Set project root directory (default: current directory) |
--json |
Output structured JSON instead of colored text |
Examples
# Basic validation
# Strict mode for CI — warnings fail the build
# Enforce 100% spec coverage
# JSON output for tooling integration
# Override project root
# Watch mode for development
Exit Codes
| Code | Meaning |
|---|---|
0 |
All checks passed |
1 |
Errors found, or warnings found with --strict, or coverage below threshold |
GitHub Action
Available on the GitHub Marketplace. The easiest way to run SpecSync in CI — no manual binary download needed.
Basic usage
- uses: CorvidLabs/spec-sync@v1
with:
strict: 'true'
require-coverage: '100'
Inputs
| Input | Default | Description |
|---|---|---|
version |
latest |
SpecSync release version to use |
strict |
false |
Treat warnings as errors |
require-coverage |
0 |
Minimum file coverage percentage |
root |
. |
Project root directory |
args |
'' |
Additional CLI arguments |
Full workflow example
name: Spec Check
on:
jobs:
specsync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: CorvidLabs/spec-sync@v1
with:
strict: 'true'
require-coverage: '100'
Multi-platform matrix
jobs:
specsync:
strategy:
matrix:
os:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: CorvidLabs/spec-sync@v1
with:
strict: 'true'
Manual CI setup
If you prefer not to use the action:
- name: Install specsync
run: |
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
- name: Spec check
run: specsync check --strict --require-coverage 100
Configuration
Create specsync.json in your project root (or run specsync init):
Options
| Option | Type | Default | Description |
|---|---|---|---|
specsDir |
string |
"specs" |
Directory containing *.spec.md files |
sourceDirs |
string[] |
["src"] |
Source directories to analyze for coverage |
schemaDir |
string? |
— | SQL schema directory for db_tables validation |
schemaPattern |
string? |
CREATE TABLE regex |
Custom regex to extract table names from schema files |
requiredSections |
string[] |
See above | Markdown sections every spec must include |
excludeDirs |
string[] |
["__tests__"] |
Directories excluded from coverage scanning |
excludePatterns |
string[] |
Common test patterns | Glob patterns for files to exclude from coverage |
sourceExtensions |
string[] |
All supported | Restrict analysis to specific file extensions (e.g., ["ts", "rs"]) |
How It Works
*.spec.md files
|
[1] Discover
|
[2] Parse frontmatter
|
+----------+----------+
| | |
[3] Structure [4] API [5] Dependencies
| | |
- Required - Detect - depends_on exists?
fields language - db_tables in schema?
- File - Extract - Consumed By refs?
exists? exports
- Required - Compare
sections? with spec
| | |
+----------+----------+
|
[6] Report
|
+----------+----------+
| | |
Errors Warnings Coverage %
- Discover all
*.spec.mdfiles in your specs directory - Parse YAML frontmatter using a zero-dependency regex parser (no YAML library needed)
- Validate structure — required fields, required markdown sections, file existence
- Validate API surface — auto-detect language from extensions, extract exports, cross-reference against the spec's Public API tables
- Validate dependencies — check
depends_onspecs exist,db_tablesare in schema - Report errors, warnings, and coverage metrics
Architecture
src/
├── main.rs CLI entry point (clap) + output formatting
├── types.rs Core data types + config schema
├── config.rs specsync.json loading
├── parser.rs YAML frontmatter + spec body parsing
├── validator.rs Spec validation + coverage computation
├── generator.rs Spec scaffolding for new modules
├── watch.rs File watcher (notify crate, 500ms debounce)
└── exports/
├── mod.rs Language dispatch + file utilities
├── typescript.rs TypeScript/JS export extraction
├── rust_lang.rs Rust pub item extraction
├── go.rs Go exported identifier extraction
├── python.rs Python __all__ / top-level extraction
├── swift.rs Swift public/open item extraction
├── kotlin.rs Kotlin public item extraction
├── java.rs Java public item extraction
├── csharp.rs C# public item extraction
└── dart.rs Dart public item extraction
Design Principles
- Single binary — no runtime dependencies, no package managers, just download and run
- Zero YAML dependencies — frontmatter is parsed with regex, keeping the binary small
- Language-agnostic architecture — adding a new language means adding one file in
exports/ - Release-optimized — LTO, symbol stripping, opt-level 3 for maximum performance
For AI Agents
SpecSync is designed to work well with AI coding agents. Here's what you need to know:
--jsonflag outputs structured results that are easy to parse programmatically- Exit code 1 means something needs attention; exit code 0 means all clear
specsync generatecan scaffold specs automatically — useful for bootstrapping docs on an existing codebase- Spec files are plain markdown — any LLM can read and write them
- The Public API table format uses backtick-quoted names that are unambiguous to parse
JSON output shape
Recommended AI workflow
# 1. Check current state
# 2. Fix any errors (update specs or code)
# 3. Generate specs for new modules
# 4. Verify everything passes
Contributing
- Fork the repo
- Create a feature branch (
git checkout -b feat/my-feature) - Make your changes
- Run tests:
cargo test - Run lints:
cargo clippy - Open a PR
Adding a new language
- Create
src/exports/yourlang.rsimplementing export extraction - Add the language variant to
Languageenum intypes.rs - Wire it up in
src/exports/mod.rs - Add tests for common export patterns