@@ -23,6 +23,12 @@ pub struct Config {
2323 #[ serde( default = "default_min_confidence" ) ]
2424 pub min_confidence : f32 ,
2525
26+ #[ serde( default = "default_strictness" ) ]
27+ pub strictness : u8 ,
28+
29+ #[ serde( default = "default_comment_types" ) ]
30+ pub comment_types : Vec < String > ,
31+
2632 #[ serde( default ) ]
2733 pub review_profile : Option < String > ,
2834
@@ -74,6 +80,9 @@ pub struct Config {
7480
7581 #[ serde( default ) ]
7682 pub paths : HashMap < String , PathConfig > ,
83+
84+ #[ serde( default ) ]
85+ pub custom_context : Vec < CustomContextConfig > ,
7786}
7887
7988#[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
@@ -95,6 +104,18 @@ pub struct PathConfig {
95104 pub severity_overrides : HashMap < String , String > ,
96105}
97106
107+ #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
108+ pub struct CustomContextConfig {
109+ #[ serde( default ) ]
110+ pub scope : Option < String > ,
111+
112+ #[ serde( default ) ]
113+ pub notes : Vec < String > ,
114+
115+ #[ serde( default ) ]
116+ pub files : Vec < String > ,
117+ }
118+
98119#[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
99120pub struct PluginConfig {
100121 #[ serde( default = "default_true" ) ]
@@ -116,6 +137,8 @@ impl Default for Config {
116137 max_context_chars : default_max_context_chars ( ) ,
117138 max_diff_chars : default_max_diff_chars ( ) ,
118139 min_confidence : default_min_confidence ( ) ,
140+ strictness : default_strictness ( ) ,
141+ comment_types : default_comment_types ( ) ,
119142 review_profile : None ,
120143 review_instructions : None ,
121144 smart_review_summary : true ,
@@ -135,6 +158,7 @@ impl Default for Config {
135158 plugins : PluginConfig :: default ( ) ,
136159 exclude_patterns : Vec :: new ( ) ,
137160 paths : HashMap :: new ( ) ,
161+ custom_context : Vec :: new ( ) ,
138162 }
139163 }
140164}
@@ -225,6 +249,13 @@ impl Config {
225249 } else if !( 0.0 ..=1.0 ) . contains ( & self . min_confidence ) {
226250 self . min_confidence = self . min_confidence . clamp ( 0.0 , 1.0 ) ;
227251 }
252+ if self . strictness == 0 {
253+ self . strictness = default_strictness ( ) ;
254+ } else if self . strictness > 3 {
255+ self . strictness = 3 ;
256+ }
257+
258+ self . comment_types = normalize_comment_types ( & self . comment_types ) ;
228259
229260 if let Some ( profile) = & self . review_profile {
230261 let normalized = profile. trim ( ) . to_lowercase ( ) ;
@@ -242,6 +273,37 @@ impl Config {
242273 self . review_instructions = None ;
243274 }
244275 }
276+
277+ let mut normalized_custom_context = Vec :: new ( ) ;
278+ for mut entry in std:: mem:: take ( & mut self . custom_context ) {
279+ entry. scope = entry. scope . and_then ( |scope| {
280+ let trimmed = scope. trim ( ) . to_string ( ) ;
281+ if trimmed. is_empty ( ) {
282+ None
283+ } else {
284+ Some ( trimmed)
285+ }
286+ } ) ;
287+
288+ entry. notes = entry
289+ . notes
290+ . into_iter ( )
291+ . map ( |note| note. trim ( ) . to_string ( ) )
292+ . filter ( |note| !note. is_empty ( ) )
293+ . collect ( ) ;
294+ entry. files = entry
295+ . files
296+ . into_iter ( )
297+ . map ( |file| file. trim ( ) . to_string ( ) )
298+ . filter ( |file| !file. is_empty ( ) )
299+ . collect ( ) ;
300+
301+ if entry. notes . is_empty ( ) && entry. files . is_empty ( ) {
302+ continue ;
303+ }
304+ normalized_custom_context. push ( entry) ;
305+ }
306+ self . custom_context = normalized_custom_context;
245307 }
246308
247309 pub fn get_path_config ( & self , file_path : & Path ) -> Option < & PathConfig > {
@@ -284,6 +346,26 @@ impl Config {
284346 false
285347 }
286348
349+ pub fn matching_custom_context ( & self , file_path : & Path ) -> Vec < & CustomContextConfig > {
350+ let file_path_str = file_path. to_string_lossy ( ) ;
351+ self . custom_context
352+ . iter ( )
353+ . filter ( |entry| match entry. scope . as_deref ( ) {
354+ Some ( scope) => self . path_matches ( & file_path_str, scope) ,
355+ None => true ,
356+ } )
357+ . collect ( )
358+ }
359+
360+ pub fn effective_min_confidence ( & self ) -> f32 {
361+ let strictness_floor = match self . strictness {
362+ 1 => 0.85 ,
363+ 2 => 0.65 ,
364+ _ => 0.45 ,
365+ } ;
366+ self . min_confidence . max ( strictness_floor) . clamp ( 0.0 , 1.0 )
367+ }
368+
287369 fn path_matches ( & self , path : & str , pattern : & str ) -> bool {
288370 // Simple glob matching
289371 if pattern. contains ( '*' ) {
@@ -310,6 +392,7 @@ mod tests {
310392 config. temperature = 5.0 ;
311393 config. max_tokens = 0 ;
312394 config. min_confidence = 2.0 ;
395+ config. strictness = 0 ;
313396 config. review_profile = Some ( "ASSERTIVE" . to_string ( ) ) ;
314397
315398 config. normalize ( ) ;
@@ -318,8 +401,24 @@ mod tests {
318401 assert_eq ! ( config. temperature, default_temperature( ) ) ;
319402 assert_eq ! ( config. max_tokens, default_max_tokens( ) ) ;
320403 assert_eq ! ( config. min_confidence, 1.0 ) ;
404+ assert_eq ! ( config. strictness, default_strictness( ) ) ;
321405 assert_eq ! ( config. review_profile. as_deref( ) , Some ( "assertive" ) ) ;
322406 }
407+
408+ #[ test]
409+ fn normalize_comment_types_filters_unknown_values ( ) {
410+ let mut config = Config :: default ( ) ;
411+ config. comment_types = vec ! [
412+ " LOGIC " . to_string( ) ,
413+ "style" . to_string( ) ,
414+ "unknown" . to_string( ) ,
415+ "STYLE" . to_string( ) ,
416+ ] ;
417+
418+ config. normalize ( ) ;
419+
420+ assert_eq ! ( config. comment_types, vec![ "logic" , "style" ] ) ;
421+ }
323422}
324423
325424fn default_model ( ) -> String {
@@ -346,6 +445,19 @@ fn default_min_confidence() -> f32 {
346445 0.0
347446}
348447
448+ fn default_strictness ( ) -> u8 {
449+ 2
450+ }
451+
452+ fn default_comment_types ( ) -> Vec < String > {
453+ vec ! [
454+ "logic" . to_string( ) ,
455+ "syntax" . to_string( ) ,
456+ "style" . to_string( ) ,
457+ "informational" . to_string( ) ,
458+ ]
459+ }
460+
349461fn default_symbol_index_max_files ( ) -> usize {
350462 500
351463}
@@ -375,3 +487,29 @@ fn default_feedback_path() -> PathBuf {
375487fn default_true ( ) -> bool {
376488 true
377489}
490+
491+ fn normalize_comment_types ( values : & [ String ] ) -> Vec < String > {
492+ if values. is_empty ( ) {
493+ return default_comment_types ( ) ;
494+ }
495+
496+ let mut normalized = Vec :: new ( ) ;
497+ for value in values {
498+ let value = value. trim ( ) . to_lowercase ( ) ;
499+ if !matches ! (
500+ value. as_str( ) ,
501+ "logic" | "syntax" | "style" | "informational"
502+ ) {
503+ continue ;
504+ }
505+ if !normalized. contains ( & value) {
506+ normalized. push ( value) ;
507+ }
508+ }
509+
510+ if normalized. is_empty ( ) {
511+ default_comment_types ( )
512+ } else {
513+ normalized
514+ }
515+ }
0 commit comments