Skip to main content

spec_drift/
lib.rs

1//! `spec-drift` — semantic coherence analysis between a project's
2//! specification surfaces (README, AGENTS.md, examples, CI) and its Rust code.
3//!
4//! The library exposes the domain model, analyzers, and reporters so they can
5//! be embedded in editors or other tools. The `spec-drift` binary is a thin
6//! CLI wrapper over [`run`].
7
8pub mod analyzers;
9pub mod baseline;
10pub mod blame;
11pub mod config;
12pub mod context;
13pub mod domain;
14pub mod error;
15pub mod llm;
16pub mod parsers;
17pub mod reporters;
18pub mod sources;
19pub mod suppress;
20pub mod workspace;
21
22pub use config::Config;
23pub use context::ProjectContext;
24pub use domain::{
25    Attribution, ClaimKind, CodeFact, Confidence, Divergence, FactKind, Location, RuleId,
26    Severity, SpecClaim,
27};
28pub use error::SpecDriftError;
29
30use analyzers::DriftAnalyzer;
31use rayon::prelude::*;
32
33/// Execute every analyzer in parallel and return divergences sorted
34/// deterministically by `(file, line, rule)` so output can be diffed between
35/// runs.
36///
37/// Analyzers are independent by construction — see the `DriftAnalyzer` trait
38/// docstring — so `par_iter` is safe and parallelizes nicely on multi-pillar
39/// runs where one analyzer (e.g. `ExamplesAnalyzer`) spends most of its time
40/// blocked on `cargo`.
41pub fn run(ctx: &ProjectContext, analyzers: &[Box<dyn DriftAnalyzer>]) -> Vec<Divergence> {
42    let mut all: Vec<Divergence> = analyzers
43        .par_iter()
44        .flat_map_iter(|a| a.analyze(ctx))
45        .collect();
46
47    all.sort_by(|a, b| {
48        a.location
49            .file
50            .cmp(&b.location.file)
51            .then_with(|| a.location.line.cmp(&b.location.line))
52            .then_with(|| a.rule.as_str().cmp(b.rule.as_str()))
53    });
54    all
55}
56
57/// Apply a [`Config`] to a divergence set: drop suppressed items and apply
58/// severity overrides.
59pub fn apply_config(
60    mut divs: Vec<Divergence>,
61    cfg: &Config,
62    root: &std::path::Path,
63) -> Vec<Divergence> {
64    divs.retain(|d| !cfg.is_suppressed(d, root));
65    cfg.apply_severity_overrides(&mut divs);
66    divs
67}
68
69/// Promote every non-deterministic divergence one severity level. Mirrors the
70/// `--strict` CLI flag — deterministic rules stay untouched because they
71/// already carry unambiguous verdicts.
72pub fn apply_strict(divs: &mut [Divergence]) {
73    for d in divs.iter_mut() {
74        if d.rule.confidence() != Confidence::Deterministic {
75            d.severity = d.severity.promoted();
76        }
77    }
78}