@@ -36,7 +36,7 @@ impl ConventionPattern {
3636 let z = 1.96 ; // 95% confidence
3737 let denominator = 1.0 + z * z / n;
3838 let center = p + z * z / ( 2.0 * n) ;
39- let spread = z * ( ( p * ( 1.0 - p) + z * z / ( 4.0 * n) ) / n ) . sqrt ( ) ;
39+ let spread = z * ( ( p * ( 1.0 - p) / n ) + ( z * z / ( 4.0 * n * n ) ) ) . sqrt ( ) ;
4040 ( ( center - spread) / denominator) . clamp ( 0.0 , 1.0 )
4141 }
4242
@@ -259,9 +259,10 @@ impl ConventionStore {
259259/// Normalize comment text into a pattern key (lowercased, stopwords removed).
260260fn normalize_pattern ( text : & str ) -> String {
261261 let lower = text. to_lowercase ( ) ;
262- let tokens: Vec < & str > = lower
263- . split_whitespace ( )
262+ let tokens: Vec < String > = lower
263+ . split ( | c : char | !c . is_alphanumeric ( ) && c != '_' )
264264 . filter ( |w| w. len ( ) > 2 && !STOPWORDS . contains ( w) )
265+ . map ( |w| w. to_string ( ) )
265266 . collect ( ) ;
266267 tokens. join ( " " )
267268}
@@ -525,4 +526,109 @@ mod tests {
525526 let suppressed = store. suppression_patterns ( ) ;
526527 assert_eq ! ( suppressed. len( ) , 1 ) ;
527528 }
529+
530+ #[ test]
531+ fn test_normalize_all_stopwords_returns_empty ( ) {
532+ let result = normalize_pattern ( "the and for are but not" ) ;
533+ assert ! ( result. is_empty( ) ) ;
534+ }
535+
536+ #[ test]
537+ fn test_normalize_short_words_filtered ( ) {
538+ let result = normalize_pattern ( "a b c do be" ) ;
539+ assert ! ( result. is_empty( ) ) ;
540+ }
541+
542+ #[ test]
543+ fn test_normalize_strips_punctuation ( ) {
544+ let a = normalize_pattern ( "Missing error-handling for API calls" ) ;
545+ let b = normalize_pattern ( "Missing error handling for API calls" ) ;
546+ // Hyphen should be treated as separator, matching split behavior of extract_tokens
547+ assert ! ( a. contains( "missing" ) ) ;
548+ assert ! ( a. contains( "error" ) ) ;
549+ assert ! ( a. contains( "handling" ) ) ;
550+ assert_eq ! ( a, b) ;
551+ }
552+
553+ #[ test]
554+ fn test_confidence_single_observation ( ) {
555+ let pattern = ConventionPattern {
556+ pattern_text : "test" . to_string ( ) ,
557+ category : "Bug" . to_string ( ) ,
558+ accepted_count : 1 ,
559+ rejected_count : 0 ,
560+ file_patterns : Vec :: new ( ) ,
561+ first_seen : "2024-01-01" . to_string ( ) ,
562+ last_seen : "2024-01-01" . to_string ( ) ,
563+ } ;
564+ // Single observation should return 0 confidence (not enough data)
565+ assert_eq ! ( pattern. confidence( ) , 0.0 ) ;
566+ }
567+
568+ #[ test]
569+ fn test_confidence_all_accepted ( ) {
570+ let pattern = ConventionPattern {
571+ pattern_text : "test" . to_string ( ) ,
572+ category : "Bug" . to_string ( ) ,
573+ accepted_count : 10 ,
574+ rejected_count : 0 ,
575+ file_patterns : Vec :: new ( ) ,
576+ first_seen : "2024-01-01" . to_string ( ) ,
577+ last_seen : "2024-01-01" . to_string ( ) ,
578+ } ;
579+ let conf = pattern. confidence ( ) ;
580+ assert ! ( conf > 0.5 , "High acceptance should yield high confidence: {conf}" ) ;
581+ }
582+
583+ #[ test]
584+ fn test_confidence_all_rejected ( ) {
585+ let pattern = ConventionPattern {
586+ pattern_text : "test" . to_string ( ) ,
587+ category : "Bug" . to_string ( ) ,
588+ accepted_count : 0 ,
589+ rejected_count : 10 ,
590+ file_patterns : Vec :: new ( ) ,
591+ first_seen : "2024-01-01" . to_string ( ) ,
592+ last_seen : "2024-01-01" . to_string ( ) ,
593+ } ;
594+ let conf = pattern. confidence ( ) ;
595+ assert ! ( conf < 0.1 , "All rejected should yield low confidence: {conf}" ) ;
596+ }
597+
598+ #[ test]
599+ fn test_score_comment_pattern_fallthrough ( ) {
600+ // Pattern exists, category matches, but neither suppress nor boost threshold met
601+ let mut store = ConventionStore :: new ( ) ;
602+ // 2 accepted, 1 rejected = 66% acceptance (below boost 75%, above suppress 25%)
603+ store. record_feedback ( "borderline pattern" , "Bug" , true , None , "2024-01-01" ) ;
604+ store. record_feedback ( "borderline pattern" , "Bug" , true , None , "2024-01-02" ) ;
605+ store. record_feedback ( "borderline pattern" , "Bug" , false , None , "2024-01-03" ) ;
606+
607+ let score = store. score_comment ( "borderline pattern" , "Bug" ) ;
608+ // Should fall through to token-based scoring, not hit suppress/boost early returns
609+ assert ! ( score. abs( ) <= 0.3 ) ;
610+ }
611+
612+ #[ test]
613+ fn test_record_feedback_empty_comment ( ) {
614+ let mut store = ConventionStore :: new ( ) ;
615+ store. record_feedback ( "" , "Bug" , true , None , "2024-01-01" ) ;
616+ // Empty comment normalizes to empty key, should be rejected
617+ assert ! ( store. patterns. is_empty( ) ) ;
618+ }
619+
620+ #[ test]
621+ fn test_extract_tokens_consistency ( ) {
622+ // Verify extract_tokens and normalize_pattern produce compatible results
623+ let text = "Missing error handling for API call" ;
624+ let normalized = normalize_pattern ( text) ;
625+ let tokens = extract_tokens ( text) ;
626+ // Every token should appear in the normalized pattern
627+ for token in & tokens {
628+ assert ! (
629+ normalized. contains( token. as_str( ) ) ,
630+ "Token '{token}' not found in normalized '{normalized}'"
631+ ) ;
632+ }
633+ }
528634}
0 commit comments