Semantic Coherence Analysis between Specification and Implementation
spec-drift operates on the principle of Single Source of Truth (SSOT) Verification. It treats documentation, examples, and CI configs as "executable specifications." When the code changes, the specification must evolve, or it becomes a "lie."
spec-drift detects these "lies" by cross-referencing the semantic claims made in natural language (Markdown) against the structural reality of the Rust codebase.
spec-drift analyzes four primary surfaces to detect divergence:
- The Check: Extracts mentioned function names, types, and architectural constraints from Markdown.
- Drift Detection:
- Symbol Absence: The
READMEmentionsfn connect_to_db(), but that function was renamed tofn init_connection(). - Constraint Violation:
AGENTS.mdstates "All API handlers must return aResult<T, ApiError>", but a new handler is found returningOption<T>. - Outdated Logic: The docs describe a 3-step auth flow, but the code now implements a 2-step flow.
- Symbol Absence: The
- The Check: This is the "Hard Truth" check. It attempts to compile examples against the current library version.
- Drift Detection:
- Compilation Failure: An example fails to compile because a public API changed.
- Deprecated Usage: The example uses a function marked with
#[deprecated]. - Logic Gap: The example demonstrates a feature that has been removed or fundamentally altered in the core logic.
- The Check: Compares the intent described in test names/doc-comments with the assertion logic.
- Drift Detection:
- The "Lying Test": A test named
test_user_cannot_access_admin_panelexists, but the assertion inside is commented out or merely checks for a200 OKinstead of a403 Forbidden. - Missing Coverage: The
READMEclaims the tool "supports concurrent writes," but no tests in thetests/directory exercise concurrency.
- The "Lying Test": A test named
- The Check: Matches the build/test commands in CI against the actual project structure.
- Drift Detection:
- Ghost Commands: CI runs
cargo test --package legacy_crate, butlegacy_cratewas merged into the main crate. - Environment Mismatch: The
READMEsays the project requireslibssl-dev, but the CI workflow is using a container that providesopenssl-devel.
- Ghost Commands: CI runs
spec-drift follows a ports-and-adapters (hexagonal) architecture. The domain model knows nothing about filesystems, parsers, or output formats — every I/O concern lives behind a trait at the edge.
┌──────────────────────────────────────────────────────────────────┐
│ spec-drift │
│ │
│ Sources ──▶ Parsers ──▶ Domain Model ──▶ Analyzers │
│ (adapters) (adapters) (core, pure) (use cases) │
│ │ │
│ ▼ │
│ Reporters │
│ (adapters) │
└──────────────────────────────────────────────────────────────────┘
| Layer | Responsibility | Key types / crates |
|---|---|---|
| Sources | Enumerate project files; optionally diff against git HEAD. |
FsWalker, GitHistory (ignore, git2) |
| Parsers | Translate raw bytes into structured facts. Pure, cached per file. | syn (full), pulldown-cmark, serde_yaml, regex |
| Domain | Pure types. No I/O, no globals. | SpecClaim, CodeFact, Divergence, Severity, RuleId, Location |
| Analyzers | Implement trait DriftAnalyzer. Independent, parallel-safe under rayon. |
DocsAnalyzer, ExamplesAnalyzer, TestsAnalyzer, CiAnalyzer |
| Reporters | Serialize Vec<Divergence> to an output format. |
HumanReporter, JsonReporter, SarifReporter, FixPromptReporter |
pub trait DriftAnalyzer {
fn id(&self) -> &'static str;
fn analyze(&self, ctx: &ProjectContext) -> Vec<Divergence>;
}
pub struct Divergence {
pub rule: RuleId,
pub severity: Severity,
pub location: Location,
pub stated: String,
pub reality: String,
pub risk: String,
}Analyzers are independent by construction and run in parallel. Parsed ASTs are cached on ProjectContext so each source file is parsed at most once per run. Errors use a single SpecDriftError enum (thiserror) at library boundaries; the CLI crate wraps with anyhow.
Not every drift check is deterministic. The matrix below states the mechanism and confidence level of each rule, so users and CI gates can calibrate trust.
| Pillar | Rule | Mechanism | Confidence |
|---|---|---|---|
| Docs | symbol_absence |
syn AST lookup for symbols mentioned in Markdown code spans. |
Deterministic |
| Docs | constraint_violation |
User-authored rule DSL (e.g. "all handlers return Result<_, ApiError>") checked on AST. |
Heuristic |
| Docs | outdated_logic |
LLM summarization + structural comparison. | Experimental (LLM) |
| Examples | compile_failure |
Thin wrapper over cargo check --examples --message-format=json. |
Deterministic |
| Examples | deprecated_usage |
cargo clippy deprecated lint re-framed as drift. |
Deterministic |
| Examples | logic_gap |
LLM comparison of example narrative vs. current API surface. | Experimental (LLM) |
| Tests | lying_test |
Parse test-name intent (negative / positive / status class) vs. assert!/assert_eq! bodies. |
Heuristic |
| Tests | missing_coverage |
README capability claims vs. test-corpus symbol coverage. | Heuristic |
| CI | ghost_command |
Parse workflows / Makefile / justfile; cross-reference cargo metadata. |
Deterministic |
| CI | env_mismatch |
Normalize named deps (e.g. libssl-dev ≡ openssl-devel) and match CI image manifest. |
Heuristic |
Confidence levels map to defaults:
- Deterministic — enabled by default, failures are actionable verdicts.
- Heuristic — enabled by default, reported at
warningor lower. False positives are expected; inline ignores exist for a reason. - Experimental (LLM) — opt-in only. Requires
[llm] enabled = trueand network/credential access.--no-llmdisables globally.
--strict promotes every heuristic rule one severity level.
# Analyze the entire project for coherence
spec-drift
# Focus a single pillar
spec-drift --docs
spec-drift --examples
# Generate a "Correction Prompt" for the AI to fix the drift
spec-drift --fix-prompt
# CI integration
spec-drift --format sarif --deny warning --baseline .spec-drift.baseline.json| Flag | Purpose |
|---|---|
--docs, --examples, --tests, --ci |
Run a single pillar. |
--format {human,json,sarif} |
Output format. Default human. |
--deny <severity> |
Exit non-zero when divergences at or above <severity> exist. |
--baseline <file> |
Accept existing divergences; fail only on new drift. |
--config <path> |
Path to spec-drift.toml. Default: walk up from CWD. |
--fix-prompt |
Emit a structured correction prompt instead of a report. |
--strict |
Promote heuristic rules one severity level. |
--diff <ref> |
Focus on files changed since the given git ref and drift induced by changed code. |
--blame |
Attribute each divergence to the commit/author/date that wrote the line. |
--package <name> |
Restrict analysis to one member of a cargo workspace. |
--no-llm |
Disable all LLM-backed checks regardless of config. |
0— no divergences at or above--denythreshold.1— divergences found.2— tool error (bad config, parse failure, I/O).
📉 SPEC DRIFT REPORT: [3 Divergences Found]
❌ CRITICAL: symbol_absence
- File: README.md (Line 42)
- Stated: `Client::new` exists in the codebase
- Reality: no symbol named `new` found in the parsed Rust sources
- Risk: New developers and AI agents will reach for a non-existent API.
- Blame: abc1234 Ada Lovelace (2024-01-02): Initial README
⚠️ WARNING: ghost_command
- File: .github/workflows/ci.yml (Line 14)
- Stated: CI runs `cargo` against package `legacy_crate`
- Reality: `legacy_crate` is not a member of the workspace
- Risk: CI exercises a target that no longer exists; the step is a no-op at best.
🟡 NOTICE: missing_coverage
- File: README.md (Line 8)
- Stated: `place_order` is a capability the project exposes
- Reality: no test references `place_order` by name
- Risk: Capability claimed in the docs has no guard-rail in tests.
Run spec-drift in CI and publish results to GitHub code scanning:
name: spec-drift
on: [push, pull_request]
jobs:
drift:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # --blame needs full history
- uses: asmuelle/spec-drift@main
with:
format: sarif
output: spec-drift.sarif
args: --blame --deny warning
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: spec-drift.sarif
category: spec-drift| Input | Default | Purpose |
|---|---|---|
version |
v0.2.0 |
git ref (branch, tag, or SHA) to install. |
format |
human |
Output format: human, json, or sarif. |
output |
(stdout) | File path to write output to. |
deny |
notice |
Fail the step when divergences at or above this severity exist. |
args |
(empty) | Extra arguments forwarded to spec-drift (e.g. --blame --strict). |
working-directory |
. |
Directory the scan runs in. |
anthropic-api-key |
(empty) | Enables LLM-backed rules when combined with [llm] enabled = true. |
Project-level config lives in spec-drift.toml at the project root. Every rule can be silenced in config, or inline at the source.
[severity]
# Deterministic
symbol_absence = "critical"
compile_failure = "critical"
ghost_command = "warning"
deprecated_usage = "warning"
# Heuristic
lying_test = "critical"
constraint_violation = "warning"
env_mismatch = "notice"
missing_coverage = "notice"
# Experimental (LLM)
outdated_logic = "notice"
logic_gap = "notice"
[ignore]
rules = ["outdated_logic"]
paths = ["docs/legacy/**", "examples/archived/**"]
symbols = ["internal_*", "*_deprecated"]
[llm]
enabled = false # opt-in; --no-llm always wins
provider = "anthropic" # anthropic | openai | local
model = "claude-sonnet-4-6"
max_calls = 50 # per run; fail closed when exceeded
timeout_s = 30
# User-authored structural rules parsed from AGENTS.md or declared here.
[[rules.constraint_violation]]
name = "handlers_return_result"
glob = "src/handlers/**"
return_type = "Result<_, ApiError>"Inline ignores must name the rule. A blanket ignore with no rule ID is rejected — silence must be specific and auditable.
// Rust: accept a specific divergence on the next item.
#[allow(spec_drift::symbol_absence)]
pub fn legacy_shim() {}<!-- spec-drift: ignore-rule symbol_absence -->
Use `Client::new()` to connect.# .github/workflows/ci.yml
# spec-drift: ignore-rule ghost_command
- run: cargo test --package legacy_crate--baseline <file> snapshots the current set of divergences. Subsequent runs only fail on new drift. Use this to adopt spec-drift on a legacy repo without a flag-day cleanup:
spec-drift --format json > .spec-drift.baseline.json
spec-drift --baseline .spec-drift.baseline.json --deny warningspec-drift is the final stage of a complete AI-Native Development Lifecycle (AIDL). Each tool owns one failure mode of AI-assisted coding.
┌──────────────┐ ┌────────────┐ ┌──────────────┐ ┌──────────────┐
intent ─▶ │ cargo-context│─▶ │ diff-risk │─▶ │ cargo-impact │─▶ │ spec-drift │ ─▶ merge
└──────────────┘ └────────────┘ └──────────────┘ └──────────────┘
Load smarts Block bad Prove correct Keep honest
cargo-contextgives the agent the right information.diff-riskvetoes the wrong change.cargo-impactconfirms the right result.spec-driftkeeps the story the repo tells about itself true.
Skip any stage and drift creeps back in somewhere else. spec-drift is the last line of defense: once the code is right, it makes sure the rest of the repo agrees.
