Skip to main content

fallow_config/config/
similar_code.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Deserializer, Serialize};
3
4const fn default_threshold() -> f64 {
5    0.80
6}
7
8const fn default_min_lines() -> usize {
9    3
10}
11
12fn deserialize_threshold<'de, D>(deserializer: D) -> Result<f64, D::Error>
13where
14    D: Deserializer<'de>,
15{
16    let value = f64::deserialize(deserializer)?;
17    if !value.is_finite() || !(0.0..=1.0).contains(&value) {
18        return Err(serde::de::Error::custom(format!(
19            "similarCode.threshold must be finite and between 0 and 1 (got {value})"
20        )));
21    }
22    Ok(value)
23}
24
25fn deserialize_min_lines<'de, D>(deserializer: D) -> Result<usize, D::Error>
26where
27    D: Deserializer<'de>,
28{
29    let value = usize::deserialize(deserializer)?;
30    if value == 0 {
31        return Err(serde::de::Error::custom(
32            "similarCode.minLines must be at least 1",
33        ));
34    }
35    Ok(value)
36}
37
38/// Project-owned tuning for the explicit `fallow similar-code` workflow.
39///
40/// Provider identity, executable discovery, model setup, credentials, and
41/// consent are intentionally absent. Project configuration cannot select code
42/// destinations or authorize model downloads.
43#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
44#[serde(rename_all = "camelCase", deny_unknown_fields)]
45pub struct SimilarCodeConfig {
46    /// Minimum model-specific cosine similarity retained as an unverified
47    /// candidate. It is not a probability or refactor-safety verdict.
48    #[serde(
49        default = "default_threshold",
50        deserialize_with = "deserialize_threshold"
51    )]
52    #[schemars(range(min = 0.0, max = 1.0))]
53    pub threshold: f64,
54
55    /// Minimum source line count for a function to enter model inference.
56    #[serde(
57        default = "default_min_lines",
58        deserialize_with = "deserialize_min_lines"
59    )]
60    #[schemars(range(min = 1))]
61    pub min_lines: usize,
62
63    /// Additional project-root-relative globs excluded only from similar-code
64    /// extraction. Global `ignorePatterns` remain authoritative first.
65    /// A single `*` stays within one path segment; use `**` to match across directories.
66    #[serde(default)]
67    pub ignore: Vec<String>,
68}
69
70impl Default for SimilarCodeConfig {
71    fn default() -> Self {
72        Self {
73            threshold: default_threshold(),
74            min_lines: default_min_lines(),
75            ignore: Vec::new(),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn defaults_are_candidate_only_calibration() {
86        let config = SimilarCodeConfig::default();
87        assert!((config.threshold - 0.80).abs() < f64::EPSILON);
88        assert_eq!(config.min_lines, 3);
89        assert!(config.ignore.is_empty());
90    }
91
92    #[test]
93    fn invalid_threshold_and_line_floor_fail_loud() {
94        let threshold = serde_json::from_str::<SimilarCodeConfig>(r#"{"threshold":1.1}"#)
95            .unwrap_err()
96            .to_string();
97        assert!(threshold.contains("between 0 and 1"));
98        let lines = serde_json::from_str::<SimilarCodeConfig>(r#"{"minLines":0}"#)
99            .unwrap_err()
100            .to_string();
101        assert!(lines.contains("at least 1"));
102    }
103}