klasp_core/source.rs
1//! `CheckSource` — abstraction over a runnable check.
2//!
3//! Design: [docs/design.md §3.2]. v0.1 ships exactly one impl (`Shell`,
4//! living in the `klasp` binary). The trait exists at v0.1 so v0.2's named
5//! recipes (`pre_commit`, `fallow`, `pytest`, `cargo`) and v0.3's
6//! subprocess plugins land as new impls without touching the trait.
7//!
8//! **Lifetime note**: `source_id(&self) -> &str` returns a `&str` tied to
9//! `&self`, *not* `&'static str`. v0.3 subprocess plugins have IDs derived
10//! from binary filenames discovered at runtime — a `'static` lifetime
11//! cannot represent that. This is the v0.3 plugin commitment locked in
12//! at v0.1. See issue [klasp#1] for the explicit callout.
13
14use std::path::PathBuf;
15
16use crate::config::CheckConfig;
17use crate::trigger::GitEvent;
18use crate::verdict::Verdict;
19
20/// Typed error for `CheckSource::run`. Covers runtime-level failures only;
21/// semantic failures (lint hits, test failures) ride inside `Verdict::Fail`.
22/// Use `CheckSourceError::Other` for impl-specific errors that don't fit the
23/// predefined variants.
24#[derive(Debug, thiserror::Error)]
25pub enum CheckSourceError {
26 #[error("failed to spawn check process: {source}")]
27 Spawn {
28 #[source]
29 source: std::io::Error,
30 },
31
32 #[error("check produced unparseable output: {0}")]
33 Output(String),
34
35 #[error("check exceeded {secs}s timeout")]
36 Timeout { secs: u64 },
37
38 #[error(transparent)]
39 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
40}
41
42/// Snapshot of repo metadata passed to every check execution.
43///
44/// `base_ref` is the merge-base ref between `HEAD` and the upstream tracking
45/// branch — the "branch divergence point" diff-aware tools (`pre-commit
46/// run --from-ref`, `fallow audit --base`) want. The gate runtime exposes it
47/// to shell checks via the `KLASP_BASE_REF` env var; sources that talk to
48/// other check tools (named recipes in v0.2, subprocess plugins in v0.3) read
49/// it directly off this struct.
50///
51/// Falls back to `HEAD~1` when no upstream is configured (a fresh checkout,
52/// a detached HEAD, or a branch that has never been pushed). The fallback is
53/// best-effort — diff-aware tools that don't recognise the ref simply lint
54/// the whole tree, which is the same behaviour they'd have without klasp.
55#[derive(Debug, Clone)]
56pub struct RepoState {
57 pub root: PathBuf,
58 pub git_event: GitEvent,
59 pub base_ref: String,
60}
61
62/// Outcome of a single `CheckSource::run` invocation.
63#[derive(Debug, Clone)]
64pub struct CheckResult {
65 pub source_id: String,
66 pub check_name: String,
67 pub verdict: Verdict,
68 pub raw_stdout: Option<String>,
69 pub raw_stderr: Option<String>,
70}
71
72/// Object-safe trait. Implementations are stored as `Box<dyn CheckSource>`
73/// in the source registry.
74pub trait CheckSource: Send + Sync {
75 /// Stable identifier for this source (e.g. `"shell"`, `"pre_commit"`,
76 /// `"plugin:klasp-plugin-foo"`). Tied to `&self` lifetime — see module
77 /// docs for why this isn't `&'static str`.
78 fn source_id(&self) -> &str;
79
80 /// Pre-flight check: does this source know how to handle the given
81 /// `CheckConfig`? Used by the registry to dispatch a check to the
82 /// right source.
83 fn supports_config(&self, config: &CheckConfig) -> bool;
84
85 /// Execute the check and return a structured result.
86 ///
87 /// Errors here are runtime failures (process spawn errors, malformed
88 /// output) — semantic failures (lint hits, test failures) ride inside
89 /// `Verdict::Fail`. Use `CheckSourceError::Other` for impl-specific
90 /// errors that don't fit the predefined variants.
91 fn run(&self, config: &CheckConfig, state: &RepoState)
92 -> Result<CheckResult, CheckSourceError>;
93}