@@ -99,7 +99,9 @@ async def analyze_contributing_guidelines(state: RepositoryAnalysisState) -> Non
9999 )
100100
101101
102- def _get_language_specific_patterns (language : str | None ) -> tuple [list [str ], list [str ]]:
102+ def _get_language_specific_patterns (
103+ language : str | None ,
104+ ) -> tuple [list [str ], list [str ]]:
103105 """
104106 Get source and test patterns based on repository language.
105107
@@ -140,13 +142,70 @@ def _get_language_specific_patterns(language: str | None) -> tuple[list[str], li
140142 # Default fallback patterns for unknown languages
141143 return (
142144 ["**/*.py" , "**/*.ts" , "**/*.tsx" , "**/*.js" , "**/*.go" ],
143- ["**/tests/**" , "**/*_test.py" , "**/*.spec.ts" , "**/*.test.js" , "**/*.test.ts" , "**/*.test.jsx" ],
145+ [
146+ "**/tests/**" ,
147+ "**/*_test.py" ,
148+ "**/*.spec.ts" ,
149+ "**/*.test.js" ,
150+ "**/*.test.ts" ,
151+ "**/*.test.jsx" ,
152+ ],
144153 )
145154
146155
147- def _default_recommendations (state : RepositoryAnalysisState ) -> list [RuleRecommendation ]:
156+ def _analyze_pr_bad_habits (state : RepositoryAnalysisState ) -> dict [str , Any ]:
157+ """
158+ Analyze PR history to detect bad habits and patterns.
159+
160+ Returns a dict with detected issues like:
161+ - missing_tests: PRs without test files (estimated based on changed_files)
162+ - short_titles: PRs with very short titles (< 10 characters)
163+ - no_reviews: PRs merged without reviews (always 0, as we can't determine this from list API)
164+
165+ Note: We can't analyze PR diffs/descriptions from the basic PR list API.
166+ This would require fetching individual PR details which is expensive.
167+ We analyze what we can from the PR list metadata.
168+ """
169+ if not state .pr_samples :
170+ return {}
171+
172+ issues : dict [str , Any ] = {
173+ "missing_tests" : 0 ,
174+ "short_titles" : 0 ,
175+ "no_reviews" : 0 ,
176+ "total_analyzed" : len (state .pr_samples ),
177+ }
178+
179+ # Analyze PR titles for very short ones (likely missing context)
180+ # A title < 10 characters is likely too short to be meaningful
181+ short_title_threshold = 10
182+ for pr in state .pr_samples :
183+ if pr .title and len (pr .title .strip ()) < short_title_threshold :
184+ issues ["short_titles" ] += 1
185+
186+ # Estimate missing tests: if PR has changed_files but no test-related patterns
187+ # This is a heuristic - we can't know for sure without fetching diffs
188+ # For now, we'll use a simple heuristic: if changed_files > 0 and title doesn't mention tests
189+ if pr .changed_files and pr .changed_files > 0 :
190+ title_lower = (pr .title or "" ).lower ()
191+ # If PR has code changes but title doesn't mention tests/test/tested/testing
192+ if not any (word in title_lower for word in ["test" , "tests" , "tested" , "testing" , "spec" ]):
193+ # This is a weak signal, but we'll count it
194+ issues ["missing_tests" ] += 1
195+
196+ return issues
197+
198+
199+ def _default_recommendations (
200+ state : RepositoryAnalysisState ,
201+ ) -> list [RuleRecommendation ]:
148202 """
149- Return a minimal, deterministic set of diff-aware rules.
203+ Return a minimal, deterministic set of diff-aware rules based on repository analysis.
204+
205+ Rules are generated based on:
206+ 1. Repository language (for test patterns)
207+ 2. PR history analysis (for bad habits)
208+ 3. Contributing guidelines (if present)
150209
151210 Note: These recommendations use repository-specific patterns when available.
152211 For more advanced use cases like restricting specific authors from specific paths
@@ -161,30 +220,47 @@ def _default_recommendations(state: RepositoryAnalysisState) -> list[RuleRecomme
161220 # Get language-specific patterns based on repository analysis
162221 source_patterns , test_patterns = _get_language_specific_patterns (state .repository_features .language )
163222
223+ # Analyze PR history for bad habits
224+ pr_issues = _analyze_pr_bad_habits (state )
225+
164226 # Require tests when source code changes.
227+ # This is especially important if we detect missing tests in PR history
228+ test_reasoning = f"Default guardrail for code changes without tests. Patterns adapted for { state .repository_features .language or 'multi-language' } repository."
229+ if pr_issues .get ("missing_tests" , 0 ) > 0 :
230+ test_reasoning += f" Detected { pr_issues ['missing_tests' ]} recent PRs without test files."
231+
232+ # Build YAML rule with proper indentation
233+ # parameters: is at column 0, source_patterns: at column 2, list items at column 4
234+ source_patterns_yaml = "\n " .join (f' - "{ pattern } "' for pattern in source_patterns )
235+ test_patterns_yaml = "\n " .join (f' - "{ pattern } "' for pattern in test_patterns )
236+
237+ yaml_content = f"""description: "Require tests when code changes"
238+ enabled: true
239+ severity: medium
240+ event_types:
241+ - pull_request
242+ parameters:
243+ source_patterns:
244+ { source_patterns_yaml }
245+ test_patterns:
246+ { test_patterns_yaml }
247+ """
248+
165249 recommendations .append (
166250 RuleRecommendation (
167- yaml_rule = textwrap .dedent (
168- f"""
169- description: "Require tests when code changes"
170- enabled: true
171- severity: medium
172- event_types:
173- - pull_request
174- parameters:
175- source_patterns:
176- { chr (10 ).join (f' - "{ pattern } "' for pattern in source_patterns )}
177- test_patterns:
178- { chr (10 ).join (f' - "{ pattern } "' for pattern in test_patterns )}
179- """
180- ).strip (),
181- confidence = 0.74 ,
182- reasoning = f"Default guardrail for code changes without tests. Patterns adapted for { state .repository_features .language or 'multi-language' } repository." ,
251+ yaml_rule = yaml_content .strip (),
252+ confidence = 0.74 if pr_issues .get ("missing_tests" , 0 ) == 0 else 0.85 ,
253+ reasoning = test_reasoning ,
183254 strategy_used = "hybrid" ,
184255 )
185256 )
186257
187258 # Require description in PR body.
259+ # Increase confidence if we detect short titles in PR history (indicator of missing context)
260+ desc_reasoning = "Encourage context for reviewers; lightweight default."
261+ if pr_issues .get ("short_titles" , 0 ) > 0 :
262+ desc_reasoning += f" Detected { pr_issues ['short_titles' ]} PRs with very short titles (likely missing context)."
263+
188264 recommendations .append (
189265 RuleRecommendation (
190266 yaml_rule = textwrap .dedent (
@@ -198,15 +274,19 @@ def _default_recommendations(state: RepositoryAnalysisState) -> list[RuleRecomme
198274 min_description_length: 50
199275 """
200276 ).strip (),
201- confidence = 0.68 ,
202- reasoning = "Encourage context for reviewers; lightweight default." ,
277+ confidence = 0.68 if pr_issues . get ( "short_titles" , 0 ) == 0 else 0.80 ,
278+ reasoning = desc_reasoning ,
203279 strategy_used = "static" ,
204280 )
205281 )
206282
207- # If no CODEOWNERS, suggest one for shared ownership signals.
208- # Note: This is informational only - we can't enforce CODEOWNERS creation via validators
209- # but we can encourage it through the recommendation reasoning.
283+ # If contributing guidelines require tests, increase confidence
284+ if state .contributing_analysis .content is not None and state .contributing_analysis .requires_tests :
285+ # Find the test rule and boost its confidence
286+ for rec in recommendations :
287+ if "tests" in rec .yaml_rule .lower ():
288+ rec .confidence = min (0.95 , rec .confidence + 0.1 )
289+ rec .reasoning += " Contributing guidelines explicitly require tests."
210290
211291 return recommendations
212292
0 commit comments