Skip to main content

ito_core/validate_repo/
rule.rs

1//! [`Rule`] trait and supporting types.
2//!
3//! A [`Rule`] is a single named, config-gated check that runs against the
4//! repository and produces zero or more [`crate::validate::ValidationIssue`]
5//! values. The engine in [`super::run_repo_validation`] dispatches each
6//! active rule and merges results into a single
7//! [`crate::validate::ValidationReport`].
8//!
9//! Rules are deliberately small: they own their identifier, severity, an
10//! activation predicate over [`ito_config::types::ItoConfig`], and a check
11//! function. They do **not** own their own report builder — they just emit
12//! raw issues.
13
14use std::fmt;
15use std::path::Path;
16
17use ito_config::types::ItoConfig;
18
19use crate::errors::CoreError;
20use crate::process::ProcessRunner;
21use crate::validate::ValidationIssue;
22
23use super::staged::StagedFiles;
24
25/// Stable identifier for a [`Rule`].
26///
27/// Identifiers follow a `category/kebab-case-name` convention
28/// (e.g. `coordination/symlinks-wired`). They are stored as
29/// `&'static str` because every built-in rule is known at compile time.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct RuleId(&'static str);
32
33impl RuleId {
34    /// Construct a [`RuleId`] from a static string.
35    ///
36    /// Callers are expected to pass a literal that follows the
37    /// `category/kebab-case-name` convention.
38    #[must_use]
39    pub const fn new(id: &'static str) -> Self {
40        Self(id)
41    }
42
43    /// Return the underlying string slice.
44    #[must_use]
45    pub const fn as_str(self) -> &'static str {
46        self.0
47    }
48}
49
50impl fmt::Display for RuleId {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str(self.0)
53    }
54}
55
56impl AsRef<str> for RuleId {
57    fn as_ref(&self) -> &str {
58        self.0
59    }
60}
61
62/// Nominal severity declared by a [`Rule`].
63///
64/// This is metadata used by [`super::list_active_rules`] and hook output.
65/// The severity actually applied to emitted issues is determined by the rule
66/// itself when constructing the [`ValidationIssue`] (via the
67/// [`crate::validate::error`] / [`crate::validate::warning`] /
68/// [`crate::validate::info`] helpers). Strict mode applied at the report
69/// layer can promote warnings to errors but never weakens an existing error.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum RuleSeverity {
72    /// The rule fails the repository when violated.
73    Error,
74    /// The rule reports a violation but does not fail the repository unless
75    /// `--strict` is set.
76    Warning,
77    /// The rule reports informational findings only.
78    Info,
79}
80
81impl RuleSeverity {
82    /// Return the uppercase string form used by
83    /// [`crate::validate::ValidationLevel`] (`"ERROR"`, `"WARNING"`,
84    /// `"INFO"`).
85    #[must_use]
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Self::Error => "ERROR",
89            Self::Warning => "WARNING",
90            Self::Info => "INFO",
91        }
92    }
93}
94
95impl fmt::Display for RuleSeverity {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.write_str(self.as_str())
98    }
99}
100
101/// Inputs available to a [`Rule`] during a single check.
102///
103/// The context is borrowed for the duration of the check and never mutated
104/// by rules. Rules SHOULD treat all fields as read-only; in particular they
105/// MUST NOT spawn background tasks holding any of these references.
106///
107/// Marked `#[non_exhaustive]` so future fields (for example a resolved
108/// worktree root for layout rules) can be added without breaking external
109/// constructors.
110#[non_exhaustive]
111pub struct RuleContext<'a> {
112    /// Resolved Ito configuration.
113    pub config: &'a ItoConfig,
114    /// Absolute path to the project root.
115    pub project_root: &'a Path,
116    /// Snapshot of files currently staged in the git index.
117    pub staged: &'a StagedFiles,
118    /// Process runner for shelling out to `git` and other tools.
119    pub runner: &'a dyn ProcessRunner,
120}
121
122impl<'a> RuleContext<'a> {
123    /// Construct a [`RuleContext`].
124    #[must_use]
125    pub fn new(
126        config: &'a ItoConfig,
127        project_root: &'a Path,
128        staged: &'a StagedFiles,
129        runner: &'a dyn ProcessRunner,
130    ) -> Self {
131        Self {
132            config,
133            project_root,
134            staged,
135            runner,
136        }
137    }
138}
139
140/// A repository validation rule.
141///
142/// Implementations are typically zero-sized structs — all per-call state
143/// lives in the [`RuleContext`].
144pub trait Rule: Send + Sync {
145    /// Stable identifier for this rule (e.g. `coordination/symlinks-wired`).
146    fn id(&self) -> RuleId;
147
148    /// Nominal severity for this rule.
149    fn severity(&self) -> RuleSeverity;
150
151    /// Short human-readable description of what the rule checks.
152    ///
153    /// Used by `ito validate repo --list-rules` and `--explain`.
154    fn description(&self) -> &'static str;
155
156    /// Optional human-readable description of the activation gate, suitable
157    /// for surfacing in `--list-rules` output.
158    ///
159    /// Examples:
160    /// - `"changes.coordination_branch.storage == worktree"`
161    /// - `"worktrees.enabled == true"`
162    /// - `None` for rules that are always active.
163    ///
164    /// Defaults to `None`. Rules that gate themselves on configuration
165    /// SHOULD override this so introspection output is informative.
166    fn gate(&self) -> Option<&'static str> {
167        None
168    }
169
170    /// Whether this rule is active for the given config.
171    ///
172    /// Rules SHOULD return `false` quickly when they have nothing to check
173    /// (for example, coordination rules return `false` when storage is
174    /// embedded). Inactive rules are skipped before [`Rule::check`] runs.
175    fn is_active(&self, config: &ItoConfig) -> bool;
176
177    /// Run the rule against the given [`RuleContext`] and return any issues.
178    ///
179    /// Returning an empty `Vec` means the rule passed. Rules SHOULD construct
180    /// issues using the helpers in [`crate::validate`] so the rule id is
181    /// consistently attached.
182    ///
183    /// # Errors
184    ///
185    /// Rules that need to invoke external tooling (for example
186    /// `git check-ignore`) MAY return [`CoreError`] when the tool itself
187    /// fails. The engine surfaces such errors as `ERROR`-level issues
188    /// pointing at the rule, rather than aborting the whole validation run.
189    fn check(&self, ctx: &RuleContext<'_>) -> Result<Vec<ValidationIssue>, CoreError>;
190}
191
192#[cfg(test)]
193#[path = "rule_tests.rs"]
194mod rule_tests;