1use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CodeBlock {
16 pub path: PathBuf,
18 pub language: Language,
20 pub content: String,
22 pub line_range: (usize, usize),
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum Language {
29 Rust,
30 Python,
31 JavaScript,
32 TypeScript,
33 Go,
34 Cpp,
35 C,
36 Unknown,
37}
38
39impl Language {
40 pub fn from_extension(ext: &str) -> Self {
41 match ext.to_lowercase().as_str() {
42 "rs" => Language::Rust,
43 "py" => Language::Python,
44 "js" | "mjs" | "cjs" => Language::JavaScript,
45 "ts" | "mts" | "cts" => Language::TypeScript,
46 "go" => Language::Go,
47 "cpp" | "cc" | "cxx" => Language::Cpp,
48 "c" | "h" => Language::C,
49 _ => Language::Unknown,
50 }
51 }
52
53 pub fn extension(&self) -> Option<&'static str> {
54 match self {
55 Language::Rust => Some("rs"),
56 Language::Python => Some("py"),
57 Language::JavaScript => Some("js"),
58 Language::TypeScript => Some("ts"),
59 Language::Go => Some("go"),
60 Language::Cpp => Some("cpp"),
61 Language::C => Some("c"),
62 Language::Unknown => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CommitMessage {
70 pub message: String,
72 pub subject: String,
74 pub body: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct StyleProfile {
81 pub naming_convention: NamingConvention,
83 pub comment_style: CommentStyle,
85 pub max_line_length: Option<usize>,
87 pub indentation: IndentationStyle,
89 pub commit_format: CommitFormat,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub enum NamingConvention {
96 SnakeCase,
97 CamelCase,
98 PascalCase,
99 KebabCase,
100 Mixed,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum CommentStyle {
106 None,
107 InlineOnly,
108 BlockOnly,
109 Both,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub enum IndentationStyle {
115 Spaces(u8),
116 Tabs,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct CommitFormat {
122 pub conventional: bool,
124 pub subject_length: Option<usize>,
126 pub has_body: bool,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct StealthScore {
133 pub overall: f32,
135 pub ai_probability: f32,
137 pub pattern_score: f32,
139 pub style_score: f32,
141}
142
143impl StealthScore {
144 pub fn new(ai_probability: f32, pattern_score: f32, style_score: f32) -> Self {
145 let overall = 1.0 - (ai_probability * 0.5 + pattern_score * 0.3 + style_score * 0.2);
146 Self {
147 overall: overall.max(0.0).min(1.0),
148 ai_probability,
149 pattern_score,
150 style_score,
151 }
152 }
153
154 pub fn is_likely_ai(&self, threshold: f32) -> bool {
156 self.ai_probability > threshold
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct DetectionResult {
163 pub score: StealthScore,
165 pub patterns: Vec<Pattern>,
167 pub sections: Vec<SectionResult>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Pattern {
174 pub pattern_type: PatternType,
176 pub confidence: f32,
178 pub location: Option<(usize, usize)>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub enum PatternType {
185 Watermark,
187 ExcessiveEmojis,
189 UniformComments,
191 PredictableNaming,
193 LowEntropy,
195 Other(String),
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct SectionResult {
202 pub section_id: String,
204 pub score: StealthScore,
206 pub patterns: Vec<Pattern>,
208}
209
210pub trait Detector: Send + Sync {
212 fn detect(&self, code: &CodeBlock) -> anyhow::Result<DetectionResult>;
214
215 fn detect_batch(&self, codes: &[CodeBlock]) -> anyhow::Result<Vec<DetectionResult>> {
217 codes.iter().map(|c| self.detect(c)).collect()
218 }
219}
220
221pub trait Humanizer: Send + Sync {
223 fn humanize(&self, code: &CodeBlock, profile: &StyleProfile) -> anyhow::Result<String>;
225
226 fn learn_style(&self, repo_path: &PathBuf) -> anyhow::Result<StyleProfile>;
228}
229
230pub trait JitterEngine: Send + Sync {
232 fn apply_jitter(&self, operation: JitterOperation) -> anyhow::Result<JitterSchedule>;
234
235 fn current_schedule(&self) -> Option<JitterSchedule>;
237}
238
239#[derive(Debug, Clone)]
241pub enum JitterOperation {
242 Stage { files: Vec<PathBuf> },
244 Commit { message: String },
246 Push,
248}
249
250#[derive(Debug, Clone)]
252pub struct JitterSchedule {
253 pub execute_at: chrono::DateTime<chrono::Utc>,
255 pub operation: JitterOperation,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct Config {
262 pub detection: DetectionConfig,
264 pub humanization: HumanizationConfig,
266 pub jitter: JitterConfig,
268 pub api: ApiConfig,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct DetectionConfig {
275 pub threshold: f32,
277 pub use_local: bool,
279 pub use_cloud_fallback: bool,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct HumanizationConfig {
286 pub auto_humanize: bool,
288 pub entropy_level: f32,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct JitterConfig {
295 pub enabled: bool,
297 pub min_delay_secs: u64,
299 pub max_delay_secs: u64,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct ApiConfig {
306 pub openrouter_key: Option<String>,
308 pub anthropic_key: Option<String>,
310 pub base_url: Option<String>,
312}
313
314impl Default for Config {
315 fn default() -> Self {
316 Self {
317 detection: DetectionConfig {
318 threshold: 0.15,
319 use_local: true,
320 use_cloud_fallback: true,
321 },
322 humanization: HumanizationConfig {
323 auto_humanize: false,
324 entropy_level: 0.5,
325 },
326 jitter: JitterConfig {
327 enabled: false,
328 min_delay_secs: 60,
329 max_delay_secs: 300,
330 },
331 api: ApiConfig {
332 openrouter_key: None,
333 anthropic_key: None,
334 base_url: None,
335 },
336 }
337 }
338}
339
340impl Config {
341 pub fn load(path: &PathBuf) -> anyhow::Result<Self> {
343 let content = std::fs::read_to_string(path)?;
344 let config: Config = toml::from_str(&content)?;
345 Ok(config)
346 }
347
348 pub fn save(&self, path: &PathBuf) -> anyhow::Result<()> {
350 let content = toml::to_string_pretty(self)?;
351 std::fs::write(path, content)?;
352 Ok(())
353 }
354
355 pub fn load_or_default(path: &PathBuf) -> anyhow::Result<Self> {
357 if path.exists() {
358 Self::load(path)
359 } else {
360 let config = Self::default();
361 config.save(path)?;
362 Ok(config)
363 }
364 }
365}