Skip to main content

tokmd_sensor/
lib.rs

1//! # tokmd-sensor
2//!
3//! **Tier 1 (Sensor Contract)**
4//!
5//! Defines the `EffortlessSensor` trait and provides the substrate builder
6//! that runs the tokei scan + git diff once.
7//!
8//! ## What belongs here
9//! * `EffortlessSensor` trait
10//! * `build_substrate()` function
11//!
12//! ## What does NOT belong here
13//! * Sensor implementations (those go in their respective crates)
14//! * CLI parsing
15
16pub mod substrate_builder;
17
18use anyhow::Result;
19use serde::{Serialize, de::DeserializeOwned};
20use tokmd_envelope::SensorReport;
21use tokmd_substrate::RepoSubstrate;
22
23/// Trait for effortless code quality sensors.
24///
25/// A sensor receives a pre-built `RepoSubstrate` (shared context from
26/// a single tokei scan + git diff) and produces a `SensorReport`.
27///
28/// # Design
29///
30/// - **Settings**: Each sensor defines its own settings type.
31/// - **Substrate**: Shared context eliminates redundant I/O across sensors.
32/// - **Report**: Standardized envelope for cross-fleet aggregation.
33pub trait EffortlessSensor {
34    /// Settings type for this sensor (must be JSON-serializable).
35    type Settings: Serialize + DeserializeOwned;
36
37    /// Sensor name (e.g., "tokmd", "coverage-bot").
38    fn name(&self) -> &str;
39
40    /// Sensor version (e.g., "1.5.0").
41    fn version(&self) -> &str;
42
43    /// Run the sensor with the given settings and substrate.
44    fn run(&self, settings: &Self::Settings, substrate: &RepoSubstrate) -> Result<SensorReport>;
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use std::collections::BTreeMap;
51    use tokmd_envelope::{SensorReport, ToolMeta, Verdict};
52    use tokmd_substrate::{LangSummary, RepoSubstrate, SubstrateFile};
53
54    /// A trivial test sensor for verifying the trait.
55    struct DummySensor;
56
57    #[derive(serde::Serialize, serde::Deserialize)]
58    struct DummySettings {
59        threshold: usize,
60    }
61
62    impl EffortlessSensor for DummySensor {
63        type Settings = DummySettings;
64
65        fn name(&self) -> &str {
66            "dummy"
67        }
68
69        fn version(&self) -> &str {
70            "0.1.0"
71        }
72
73        fn run(&self, settings: &DummySettings, substrate: &RepoSubstrate) -> Result<SensorReport> {
74            let verdict = if substrate.total_code_lines > settings.threshold {
75                Verdict::Warn
76            } else {
77                Verdict::Pass
78            };
79            Ok(SensorReport::new(
80                ToolMeta::new(self.name(), self.version(), "check"),
81                "2024-01-01T00:00:00Z".to_string(),
82                verdict,
83                format!(
84                    "{} code lines (threshold: {})",
85                    substrate.total_code_lines, settings.threshold
86                ),
87            ))
88        }
89    }
90
91    fn sample_substrate() -> RepoSubstrate {
92        RepoSubstrate {
93            repo_root: ".".to_string(),
94            files: vec![SubstrateFile {
95                path: "src/lib.rs".to_string(),
96                lang: "Rust".to_string(),
97                code: 100,
98                lines: 120,
99                bytes: 3000,
100                tokens: 750,
101                module: "src".to_string(),
102                in_diff: false,
103            }],
104            lang_summary: BTreeMap::from([(
105                "Rust".to_string(),
106                LangSummary {
107                    files: 1,
108                    code: 100,
109                    lines: 120,
110                    bytes: 3000,
111                    tokens: 750,
112                },
113            )]),
114            diff_range: None,
115            total_tokens: 750,
116            total_bytes: 3000,
117            total_code_lines: 100,
118        }
119    }
120
121    #[test]
122    fn dummy_sensor_pass() {
123        let sensor = DummySensor;
124        let settings = DummySettings { threshold: 200 };
125        let substrate = sample_substrate();
126        let report = sensor.run(&settings, &substrate).unwrap();
127        assert_eq!(report.verdict, Verdict::Pass);
128    }
129
130    #[test]
131    fn dummy_sensor_warn() {
132        let sensor = DummySensor;
133        let settings = DummySettings { threshold: 50 };
134        let substrate = sample_substrate();
135        let report = sensor.run(&settings, &substrate).unwrap();
136        assert_eq!(report.verdict, Verdict::Warn);
137    }
138
139    #[test]
140    fn sensor_name_and_version() {
141        let sensor = DummySensor;
142        assert_eq!(sensor.name(), "dummy");
143        assert_eq!(sensor.version(), "0.1.0");
144    }
145}