ref-agents 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
@@ -0,0 +1,436 @@
1
+ """Complexity Scanner - Multi-language complexity analysis.
2
+
3
+ Supports Python (AST-based) and TypeScript/JavaScript/Java (AST or regex-based).
4
+ Uses ConfigLoader to load language-specific thresholds.
5
+ Calculates LOC, cyclomatic/cognitive complexity, function/class counts.
6
+ Implements dual-threshold system: function-level + file-level.
7
+ """
8
+
9
+ import ast
10
+ import fnmatch
11
+ import re
12
+ from pathlib import Path
13
+
14
+ import structlog
15
+
16
+ from ref_agents.core.config_loader import ConfigLoader
17
+ from ref_agents.core.language_detector import LanguageDetector
18
+
19
+ logger = structlog.get_logger(__name__)
20
+
21
+
22
+ class ComplexityVisitor(ast.NodeVisitor):
23
+ """AST visitor for Python complexity analysis."""
24
+
25
+ def __init__(self):
26
+ """Initialize visitor."""
27
+ self.complexity = 0
28
+ self.functions = 0
29
+ self.classes = 0
30
+
31
+ def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
32
+ """Count function and base complexity."""
33
+ self.functions += 1
34
+ self.complexity += 1 # Base complexity
35
+ self.generic_visit(node)
36
+
37
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
38
+ """Count async function."""
39
+ self.visit_FunctionDef(node)
40
+
41
+ def visit_ClassDef(self, node) -> None:
42
+ """Count class."""
43
+ self.classes += 1
44
+ self.generic_visit(node)
45
+
46
+ def visit_If(self, node) -> None:
47
+ """Count if statement."""
48
+ self.complexity += 1
49
+ self.generic_visit(node)
50
+
51
+ def visit_For(self, node) -> None:
52
+ """Count for loop."""
53
+ self.complexity += 1
54
+ self.generic_visit(node)
55
+
56
+ def visit_AsyncFor(self, node) -> None:
57
+ """Count async for loop."""
58
+ self.complexity += 1
59
+ self.generic_visit(node)
60
+
61
+ def visit_While(self, node) -> None:
62
+ """Count while loop."""
63
+ self.complexity += 1
64
+ self.generic_visit(node)
65
+
66
+ def visit_Try(self, node) -> None:
67
+ """Count try block."""
68
+ self.complexity += 1
69
+ self.generic_visit(node)
70
+
71
+ def visit_ExceptHandler(self, node) -> None:
72
+ """Count exception handler."""
73
+ self.complexity += 1
74
+ self.generic_visit(node)
75
+
76
+
77
+ def _analyze_python_ast(content: str) -> dict:
78
+ """Analyze Python file using AST.
79
+
80
+ Args:
81
+ content: File content.
82
+
83
+ Returns:
84
+ Dict with complexity metrics.
85
+ """
86
+ tree = ast.parse(content)
87
+ visitor = ComplexityVisitor()
88
+ visitor.visit(tree)
89
+
90
+ return {
91
+ "complexity": visitor.complexity,
92
+ "functions": visitor.functions,
93
+ "classes": visitor.classes,
94
+ }
95
+
96
+
97
+ def _analyze_regex_based(content: str, language: str) -> dict:
98
+ """Analyze file using regex patterns (TypeScript/JavaScript/Java).
99
+
100
+ Args:
101
+ content: File content.
102
+ language: Language name.
103
+
104
+ Returns:
105
+ Dict with complexity metrics.
106
+ """
107
+ complexity = 0
108
+ functions = 0
109
+ classes = 0
110
+
111
+ # Count functions/methods
112
+ if language in ("typescript", "javascript"):
113
+ # Function patterns: function name(), arrow functions, async functions
114
+ function_patterns = [
115
+ r"\bfunction\s+\w+\s*\(",
116
+ r"\bconst\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>",
117
+ r"\bconst\s+\w+\s*=\s*(async\s+)?function",
118
+ r"\bexport\s+(async\s+)?function",
119
+ r"\bexport\s+const\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>",
120
+ ]
121
+ for pattern in function_patterns:
122
+ functions += len(re.findall(pattern, content, re.MULTILINE))
123
+
124
+ # Class patterns
125
+ class_pattern = r"\b(class|interface|type)\s+\w+"
126
+ classes = len(re.findall(class_pattern, content, re.MULTILINE))
127
+
128
+ elif language == "java":
129
+ # Method patterns: public/private/protected returnType methodName(
130
+ method_pattern = r"\b(public|private|protected|static)\s+.*?\s+\w+\s*\("
131
+ functions = len(re.findall(method_pattern, content, re.MULTILINE))
132
+
133
+ # Class patterns
134
+ class_pattern = r"\b(public\s+)?(abstract\s+)?(final\s+)?class\s+\w+"
135
+ classes = len(re.findall(class_pattern, content, re.MULTILINE))
136
+
137
+ elif language == "csharp":
138
+ # Method patterns: access modifier + return type + name(
139
+ method_pattern = (
140
+ r"\b(public|private|protected|internal|static|override|virtual|abstract)"
141
+ r"\s+.*?\s+\w+\s*\("
142
+ )
143
+ functions = len(re.findall(method_pattern, content, re.MULTILINE))
144
+
145
+ # Class, interface, struct, record declarations
146
+ class_pattern = (
147
+ r"\b(public|internal|private|protected|abstract|sealed|partial|static)?"
148
+ r"\s*(partial\s+)?(class|interface|struct|record)\s+\w+"
149
+ )
150
+ classes = len(re.findall(class_pattern, content, re.MULTILINE))
151
+
152
+ # Count complexity contributors (if, for, while, switch, catch, ternary)
153
+ complexity_patterns = [
154
+ r"\bif\s*\(",
155
+ r"\bfor\s*\(",
156
+ r"\bwhile\s*\(",
157
+ r"\bswitch\s*\(",
158
+ r"\bcatch\s*\(",
159
+ r"\?\s*.*\s*:",
160
+ ]
161
+ for pattern in complexity_patterns:
162
+ complexity += len(re.findall(pattern, content, re.MULTILINE))
163
+
164
+ # Base complexity = number of functions
165
+ complexity += functions
166
+
167
+ return {
168
+ "complexity": complexity,
169
+ "functions": functions,
170
+ "classes": classes,
171
+ }
172
+
173
+
174
+ def analyze_file(file_path: Path, config_loader: ConfigLoader | None = None) -> dict:
175
+ """Analyze file for complexity metrics.
176
+
177
+ Supports Python (AST), TypeScript, JavaScript, Java (regex).
178
+ Uses ConfigLoader to get language-specific thresholds.
179
+
180
+ Args:
181
+ file_path: Path to file.
182
+ config_loader: Optional ConfigLoader instance. Creates default if None.
183
+
184
+ Returns:
185
+ Dict with loc, complexity, functions, classes, thresholds, error.
186
+ """
187
+ if config_loader is None:
188
+ config_loader = ConfigLoader()
189
+
190
+ try:
191
+ if config_loader is None:
192
+ config_loader = ConfigLoader()
193
+
194
+ # Detect language first
195
+ detector = LanguageDetector()
196
+ lang_info = detector.detect(file_path)
197
+
198
+ if lang_info.language == "unknown":
199
+ # Can't check exclusions without language, proceed with basic analysis
200
+ content = file_path.read_text(encoding="utf-8")
201
+ loc = len(content.splitlines())
202
+ return {
203
+ "loc": loc,
204
+ "complexity": 0,
205
+ "functions": 0,
206
+ "classes": 0,
207
+ "error": f"Unknown language for file: {file_path}",
208
+ }
209
+
210
+ # Load config to check exclusions (before reading file content)
211
+ try:
212
+ config = config_loader.load(lang_info.language, lang_info.framework)
213
+ except FileNotFoundError:
214
+ logger.warning(
215
+ "config_not_found",
216
+ language=lang_info.language,
217
+ file=str(file_path),
218
+ )
219
+ from ref_agents.core.config_models import LanguageConfig, Thresholds
220
+
221
+ config = LanguageConfig(
222
+ language=lang_info.language, thresholds=Thresholds()
223
+ )
224
+
225
+ # Check skip_analysis exclusions (before reading file)
226
+ if config.exclusions and "skip_analysis" in config.exclusions:
227
+ file_str = str(file_path)
228
+ file_name = file_path.name
229
+ for pattern in config.exclusions["skip_analysis"]:
230
+ # Check both full path and filename
231
+ if fnmatch.fnmatch(file_str, pattern) or fnmatch.fnmatch(
232
+ file_name, pattern
233
+ ):
234
+ logger.debug(
235
+ "file_excluded_from_analysis",
236
+ file=str(file_path),
237
+ pattern=pattern,
238
+ )
239
+ return {
240
+ "loc": 0,
241
+ "complexity": 0,
242
+ "functions": 0,
243
+ "classes": 0,
244
+ "error": None,
245
+ "excluded": True,
246
+ "exclusion_reason": f"Matches skip pattern: {pattern}",
247
+ }
248
+
249
+ # Read file content after exclusion check
250
+ content = file_path.read_text(encoding="utf-8")
251
+ loc = len(content.splitlines())
252
+
253
+ # Get thresholds
254
+ thresholds = config.thresholds
255
+
256
+ # Analyze based on language and parser type
257
+ if lang_info.language == "python":
258
+ metrics = _analyze_python_ast(content)
259
+ per_function = None # Python doesn't track per-function yet
260
+ elif lang_info.language in ("typescript", "javascript"):
261
+ # Use AST parser if configured, else regex fallback
262
+ if config.parser == "ast":
263
+ from ref_agents.tools.complexity_ast import analyze_typescript_ast
264
+
265
+ ast_result = analyze_typescript_ast(file_path, content)
266
+ if ast_result.error:
267
+ logger.warning(
268
+ "ast_fallback_to_regex",
269
+ error=ast_result.error,
270
+ file=str(file_path),
271
+ )
272
+ metrics = _analyze_regex_based(content, lang_info.language)
273
+ per_function = None
274
+ else:
275
+ # Convert AST result to metrics format
276
+ metrics = {
277
+ "complexity": ast_result.file_complexity,
278
+ "functions": len(ast_result.functions),
279
+ "classes": ast_result.classes,
280
+ }
281
+ per_function = [
282
+ {"name": f.name, "complexity": f.complexity, "line": f.line}
283
+ for f in ast_result.functions
284
+ ]
285
+ else:
286
+ metrics = _analyze_regex_based(content, lang_info.language)
287
+ per_function = None
288
+ elif lang_info.language == "java":
289
+ metrics = _analyze_regex_based(content, lang_info.language)
290
+ per_function = None
291
+ elif lang_info.language == "csharp":
292
+ metrics = _analyze_regex_based(content, lang_info.language)
293
+ per_function = None
294
+ else:
295
+ return {
296
+ "loc": loc,
297
+ "complexity": 0,
298
+ "functions": 0,
299
+ "classes": 0,
300
+ "error": f"Unsupported language: {lang_info.language}",
301
+ }
302
+
303
+ # Check exclusions for file-level complexity
304
+ skip_file_complexity = False
305
+ if config.exclusions and "file_complexity" in config.exclusions:
306
+ for pattern in config.exclusions["file_complexity"]:
307
+ if fnmatch.fnmatch(file_path.name, pattern):
308
+ skip_file_complexity = True
309
+ logger.debug(
310
+ "file_excluded_from_complexity",
311
+ file=str(file_path),
312
+ pattern=pattern,
313
+ )
314
+ break
315
+
316
+ return {
317
+ "loc": loc,
318
+ "complexity": metrics["complexity"],
319
+ "functions": metrics["functions"],
320
+ "classes": metrics["classes"],
321
+ "per_function": per_function, # List of {name, complexity, line} if available
322
+ "thresholds": {
323
+ "complexity": thresholds.complexity, # Legacy
324
+ "function_complexity_warn": thresholds.function_complexity_warn,
325
+ "function_complexity_block": thresholds.function_complexity_block,
326
+ "file_complexity_warn": thresholds.file_complexity_warn,
327
+ "file_complexity_block": thresholds.file_complexity_block,
328
+ "file_length": thresholds.file_length,
329
+ },
330
+ "skip_file_complexity": skip_file_complexity,
331
+ "error": None,
332
+ }
333
+ except SyntaxError as e:
334
+ return {"error": f"Syntax error: {e}"}
335
+ except Exception as e:
336
+ logger.error("analyze_file_failed", error=str(e))
337
+ raise
338
+
339
+
340
+ def scan_complexity_tool(files: str) -> str:
341
+ """Scan files for complexity metrics (LOC, Cognitive/Cyclomatic).
342
+
343
+ Uses dual-threshold system:
344
+ - Function-level: Warn 15, Block 25 (cognitive complexity)
345
+ - File-level: Warn 80, Block 150 (aggregate)
346
+
347
+ Automatically excludes:
348
+ - Autogenerated files (*.generated.ts, *.auto.ts)
349
+ - Hidden directories (.*/)
350
+ - Coverage/build directories (coverage/, dist/, build/)
351
+ - Vendor directories (node_modules/, vendor/, third-party/)
352
+
353
+ Usage: scan_complexity("file1.py,file2.ts")
354
+ """
355
+ file_list = [f.strip() for f in files.split(",") if f.strip()]
356
+ report = ["🔍 **Code Complexity Scan**\n"]
357
+
358
+ for fname in file_list:
359
+ p = Path(fname)
360
+ if not p.exists():
361
+ report.append(f"❌ `{fname}`: File not found")
362
+ continue
363
+
364
+ metrics = analyze_file(p)
365
+
366
+ # Skip excluded files silently (autogenerated, vendor, etc.)
367
+ if metrics.get("excluded"):
368
+ continue
369
+
370
+ if metrics.get("error"):
371
+ report.append(f"❌ `{fname}`: Parse Error: {metrics['error']}")
372
+ continue
373
+
374
+ thresholds = metrics.get("thresholds", {})
375
+ skip_file = metrics.get("skip_file_complexity", False)
376
+
377
+ # File-level checks
378
+ loc_status = (
379
+ "🔴" if metrics["loc"] > thresholds.get("file_length", 500) else "🟢"
380
+ )
381
+ file_comp = metrics["complexity"]
382
+
383
+ report.append(f"### `{p.name}`")
384
+ report.append(
385
+ f"- LOC: {metrics['loc']} {loc_status} (Limit: {thresholds.get('file_length', 500)})"
386
+ )
387
+ report.append(f"- Functions: {metrics['functions']}")
388
+ report.append(f"- Classes: {metrics['classes']}")
389
+
390
+ # File-level complexity (if not excluded)
391
+ if not skip_file:
392
+ file_warn = thresholds.get("file_complexity_warn", 80)
393
+ file_block = thresholds.get("file_complexity_block", 150)
394
+
395
+ if file_comp >= file_block:
396
+ file_status = "🔴 BLOCKER"
397
+ report.append(
398
+ f"- File Complexity: {file_comp} {file_status} (Block: {file_block}, Warn: {file_warn})"
399
+ )
400
+ elif file_comp >= file_warn:
401
+ file_status = "🟡 WARNING"
402
+ report.append(
403
+ f"- File Complexity: {file_comp} {file_status} (Block: {file_block}, Warn: {file_warn})"
404
+ )
405
+ else:
406
+ file_status = "🟢 OK"
407
+ report.append(
408
+ f"- File Complexity: {file_comp} {file_status} (Block: {file_block}, Warn: {file_warn})"
409
+ )
410
+ else:
411
+ report.append(f"- File Complexity: {file_comp} ⚪ EXCLUDED")
412
+
413
+ # Per-function complexity (if available)
414
+ per_function = metrics.get("per_function")
415
+ if per_function:
416
+ func_warn = thresholds.get("function_complexity_warn", 15)
417
+ func_block = thresholds.get("function_complexity_block", 25)
418
+
419
+ violations = [f for f in per_function if f["complexity"] >= func_warn]
420
+
421
+ if violations:
422
+ report.append("")
423
+ report.append("**Function-Level Violations:**")
424
+ for func in violations:
425
+ if func["complexity"] >= func_block:
426
+ status = "🔴 BLOCKER"
427
+ else:
428
+ status = "🟡 WARNING"
429
+ report.append(
430
+ f"- `{func['name']}` (line {func['line']}): "
431
+ f"{func['complexity']} {status} (Block: {func_block}, Warn: {func_warn})"
432
+ )
433
+
434
+ report.append("")
435
+
436
+ return "\n".join(report)