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///
56/// `staged_files` carries the absolute paths of files in the current group's
57/// scope when running in monorepo mode (i.e. the subset of staged files that
58/// belong to the `klasp.toml` group that owns this invocation). An **empty
59/// Vec means "no scoping; the check sees the whole repo"** — this is the
60/// back-compat value used by the single-config fallback path and by callers
61/// that do not dispatch per-group. Per-source consumption of `staged_files`
62/// for fine-grained scoping is deferred to issue #34 (rayon / named recipes);
63/// the field is present now so that data is available to checks without a
64/// further struct-breaking change.
65#[derive(Debug, Clone)]
66pub struct RepoState {
67 pub root: PathBuf,
68 pub git_event: GitEvent,
69 pub base_ref: String,
70 /// Staged files scoped to this group's `klasp.toml`. Empty = whole-repo
71 /// (single-config fallback or unscoped callers).
72 pub staged_files: Vec<PathBuf>,
73}
74
75/// Outcome of a single `CheckSource::run` invocation.
76#[derive(Debug, Clone)]
77pub struct CheckResult {
78 pub source_id: String,
79 pub check_name: String,
80 pub verdict: Verdict,
81 pub raw_stdout: Option<String>,
82 pub raw_stderr: Option<String>,
83}
84
85/// Object-safe trait. Implementations are stored as `Box<dyn CheckSource>`
86/// in the source registry.
87pub trait CheckSource: Send + Sync {
88 /// Stable identifier for this source (e.g. `"shell"`, `"pre_commit"`,
89 /// `"plugin:klasp-plugin-foo"`). Tied to `&self` lifetime — see module
90 /// docs for why this isn't `&'static str`.
91 fn source_id(&self) -> &str;
92
93 /// Pre-flight check: does this source know how to handle the given
94 /// `CheckConfig`? Used by the registry to dispatch a check to the
95 /// right source.
96 fn supports_config(&self, config: &CheckConfig) -> bool;
97
98 /// Execute the check and return a structured result.
99 ///
100 /// Errors here are runtime failures (process spawn errors, malformed
101 /// output) — semantic failures (lint hits, test failures) ride inside
102 /// `Verdict::Fail`. Use `CheckSourceError::Other` for impl-specific
103 /// errors that don't fit the predefined variants.
104 fn run(&self, config: &CheckConfig, state: &RepoState)
105 -> Result<CheckResult, CheckSourceError>;
106}