tapps-agents 3.5.40__py3-none-any.whl → 3.6.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 (705) hide show
  1. tapps_agents/__init__.py +2 -2
  2. tapps_agents/agents/__init__.py +22 -22
  3. tapps_agents/agents/analyst/__init__.py +5 -5
  4. tapps_agents/agents/architect/__init__.py +5 -5
  5. tapps_agents/agents/architect/agent.py +1033 -1033
  6. tapps_agents/agents/architect/pattern_detector.py +75 -75
  7. tapps_agents/agents/cleanup/__init__.py +7 -7
  8. tapps_agents/agents/cleanup/agent.py +445 -445
  9. tapps_agents/agents/debugger/__init__.py +7 -7
  10. tapps_agents/agents/debugger/agent.py +310 -310
  11. tapps_agents/agents/debugger/error_analyzer.py +437 -437
  12. tapps_agents/agents/designer/__init__.py +5 -5
  13. tapps_agents/agents/designer/agent.py +786 -786
  14. tapps_agents/agents/designer/visual_designer.py +638 -638
  15. tapps_agents/agents/documenter/__init__.py +7 -7
  16. tapps_agents/agents/documenter/agent.py +531 -531
  17. tapps_agents/agents/documenter/doc_generator.py +472 -472
  18. tapps_agents/agents/documenter/doc_validator.py +393 -393
  19. tapps_agents/agents/documenter/framework_doc_updater.py +493 -493
  20. tapps_agents/agents/enhancer/__init__.py +7 -7
  21. tapps_agents/agents/evaluator/__init__.py +7 -7
  22. tapps_agents/agents/evaluator/agent.py +443 -443
  23. tapps_agents/agents/evaluator/priority_evaluator.py +641 -641
  24. tapps_agents/agents/evaluator/quality_analyzer.py +147 -147
  25. tapps_agents/agents/evaluator/report_generator.py +344 -344
  26. tapps_agents/agents/evaluator/usage_analyzer.py +192 -192
  27. tapps_agents/agents/evaluator/workflow_analyzer.py +189 -189
  28. tapps_agents/agents/implementer/__init__.py +7 -7
  29. tapps_agents/agents/implementer/agent.py +798 -798
  30. tapps_agents/agents/implementer/auto_fix.py +1119 -1119
  31. tapps_agents/agents/implementer/code_generator.py +73 -73
  32. tapps_agents/agents/improver/__init__.py +1 -1
  33. tapps_agents/agents/improver/agent.py +753 -753
  34. tapps_agents/agents/ops/__init__.py +1 -1
  35. tapps_agents/agents/ops/agent.py +619 -619
  36. tapps_agents/agents/ops/dependency_analyzer.py +600 -600
  37. tapps_agents/agents/orchestrator/__init__.py +5 -5
  38. tapps_agents/agents/orchestrator/agent.py +522 -522
  39. tapps_agents/agents/planner/__init__.py +7 -7
  40. tapps_agents/agents/planner/agent.py +1127 -1127
  41. tapps_agents/agents/reviewer/__init__.py +24 -24
  42. tapps_agents/agents/reviewer/agent.py +3513 -3513
  43. tapps_agents/agents/reviewer/aggregator.py +213 -213
  44. tapps_agents/agents/reviewer/batch_review.py +448 -448
  45. tapps_agents/agents/reviewer/cache.py +443 -443
  46. tapps_agents/agents/reviewer/context7_enhancer.py +630 -630
  47. tapps_agents/agents/reviewer/context_detector.py +203 -203
  48. tapps_agents/agents/reviewer/docker_compose_validator.py +158 -158
  49. tapps_agents/agents/reviewer/dockerfile_validator.py +176 -176
  50. tapps_agents/agents/reviewer/error_handling.py +126 -126
  51. tapps_agents/agents/reviewer/feedback_generator.py +490 -490
  52. tapps_agents/agents/reviewer/influxdb_validator.py +316 -316
  53. tapps_agents/agents/reviewer/issue_tracking.py +169 -169
  54. tapps_agents/agents/reviewer/library_detector.py +295 -295
  55. tapps_agents/agents/reviewer/library_patterns.py +268 -268
  56. tapps_agents/agents/reviewer/maintainability_scorer.py +593 -593
  57. tapps_agents/agents/reviewer/metric_strategies.py +276 -276
  58. tapps_agents/agents/reviewer/mqtt_validator.py +160 -160
  59. tapps_agents/agents/reviewer/output_enhancer.py +105 -105
  60. tapps_agents/agents/reviewer/pattern_detector.py +241 -241
  61. tapps_agents/agents/reviewer/performance_scorer.py +357 -357
  62. tapps_agents/agents/reviewer/phased_review.py +516 -516
  63. tapps_agents/agents/reviewer/progressive_review.py +435 -435
  64. tapps_agents/agents/reviewer/react_scorer.py +331 -331
  65. tapps_agents/agents/reviewer/score_constants.py +228 -228
  66. tapps_agents/agents/reviewer/score_validator.py +507 -507
  67. tapps_agents/agents/reviewer/scorer_registry.py +373 -373
  68. tapps_agents/agents/reviewer/scoring.py +1566 -1566
  69. tapps_agents/agents/reviewer/service_discovery.py +534 -534
  70. tapps_agents/agents/reviewer/tools/__init__.py +41 -41
  71. tapps_agents/agents/reviewer/tools/parallel_executor.py +581 -581
  72. tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -250
  73. tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -284
  74. tapps_agents/agents/reviewer/typescript_scorer.py +1142 -1142
  75. tapps_agents/agents/reviewer/validation.py +208 -208
  76. tapps_agents/agents/reviewer/websocket_validator.py +132 -132
  77. tapps_agents/agents/tester/__init__.py +7 -7
  78. tapps_agents/agents/tester/accessibility_auditor.py +309 -309
  79. tapps_agents/agents/tester/agent.py +1080 -1080
  80. tapps_agents/agents/tester/batch_generator.py +54 -54
  81. tapps_agents/agents/tester/context_learner.py +51 -51
  82. tapps_agents/agents/tester/coverage_analyzer.py +386 -386
  83. tapps_agents/agents/tester/coverage_test_generator.py +290 -290
  84. tapps_agents/agents/tester/debug_enhancer.py +238 -238
  85. tapps_agents/agents/tester/device_emulator.py +241 -241
  86. tapps_agents/agents/tester/integration_generator.py +62 -62
  87. tapps_agents/agents/tester/network_recorder.py +300 -300
  88. tapps_agents/agents/tester/performance_monitor.py +320 -320
  89. tapps_agents/agents/tester/test_fixer.py +316 -316
  90. tapps_agents/agents/tester/test_generator.py +632 -632
  91. tapps_agents/agents/tester/trace_manager.py +234 -234
  92. tapps_agents/agents/tester/visual_regression.py +291 -291
  93. tapps_agents/analysis/pattern_detector.py +36 -36
  94. tapps_agents/beads/hydration.py +213 -213
  95. tapps_agents/beads/parse.py +32 -32
  96. tapps_agents/beads/specs.py +206 -206
  97. tapps_agents/cli/__init__.py +9 -9
  98. tapps_agents/cli/__main__.py +8 -8
  99. tapps_agents/cli/base.py +478 -478
  100. tapps_agents/cli/command_classifier.py +72 -72
  101. tapps_agents/cli/commands/__init__.py +2 -2
  102. tapps_agents/cli/commands/analyst.py +173 -173
  103. tapps_agents/cli/commands/architect.py +109 -109
  104. tapps_agents/cli/commands/cleanup_agent.py +92 -92
  105. tapps_agents/cli/commands/common.py +126 -126
  106. tapps_agents/cli/commands/debugger.py +90 -90
  107. tapps_agents/cli/commands/designer.py +112 -112
  108. tapps_agents/cli/commands/documenter.py +136 -136
  109. tapps_agents/cli/commands/enhancer.py +110 -110
  110. tapps_agents/cli/commands/evaluator.py +255 -255
  111. tapps_agents/cli/commands/health.py +665 -665
  112. tapps_agents/cli/commands/implementer.py +301 -301
  113. tapps_agents/cli/commands/improver.py +91 -91
  114. tapps_agents/cli/commands/knowledge.py +111 -111
  115. tapps_agents/cli/commands/learning.py +172 -172
  116. tapps_agents/cli/commands/observability.py +283 -283
  117. tapps_agents/cli/commands/ops.py +135 -135
  118. tapps_agents/cli/commands/orchestrator.py +116 -116
  119. tapps_agents/cli/commands/planner.py +237 -237
  120. tapps_agents/cli/commands/reviewer.py +1872 -1872
  121. tapps_agents/cli/commands/status.py +285 -285
  122. tapps_agents/cli/commands/task.py +227 -219
  123. tapps_agents/cli/commands/tester.py +191 -191
  124. tapps_agents/cli/commands/top_level.py +3586 -3586
  125. tapps_agents/cli/feedback.py +936 -936
  126. tapps_agents/cli/formatters.py +608 -608
  127. tapps_agents/cli/help/__init__.py +7 -7
  128. tapps_agents/cli/help/static_help.py +425 -425
  129. tapps_agents/cli/network_detection.py +110 -110
  130. tapps_agents/cli/output_compactor.py +274 -274
  131. tapps_agents/cli/parsers/__init__.py +2 -2
  132. tapps_agents/cli/parsers/analyst.py +186 -186
  133. tapps_agents/cli/parsers/architect.py +167 -167
  134. tapps_agents/cli/parsers/cleanup_agent.py +228 -228
  135. tapps_agents/cli/parsers/debugger.py +116 -116
  136. tapps_agents/cli/parsers/designer.py +182 -182
  137. tapps_agents/cli/parsers/documenter.py +134 -134
  138. tapps_agents/cli/parsers/enhancer.py +113 -113
  139. tapps_agents/cli/parsers/evaluator.py +213 -213
  140. tapps_agents/cli/parsers/implementer.py +168 -168
  141. tapps_agents/cli/parsers/improver.py +132 -132
  142. tapps_agents/cli/parsers/ops.py +159 -159
  143. tapps_agents/cli/parsers/orchestrator.py +98 -98
  144. tapps_agents/cli/parsers/planner.py +145 -145
  145. tapps_agents/cli/parsers/reviewer.py +462 -462
  146. tapps_agents/cli/parsers/tester.py +124 -124
  147. tapps_agents/cli/progress_heartbeat.py +254 -254
  148. tapps_agents/cli/streaming_progress.py +336 -336
  149. tapps_agents/cli/utils/__init__.py +6 -6
  150. tapps_agents/cli/utils/agent_lifecycle.py +48 -48
  151. tapps_agents/cli/utils/error_formatter.py +82 -82
  152. tapps_agents/cli/utils/error_recovery.py +188 -188
  153. tapps_agents/cli/utils/output_handler.py +59 -59
  154. tapps_agents/cli/utils/prompt_enhancer.py +319 -319
  155. tapps_agents/cli/validators/__init__.py +9 -9
  156. tapps_agents/cli/validators/command_validator.py +81 -81
  157. tapps_agents/context7/__init__.py +112 -112
  158. tapps_agents/context7/agent_integration.py +869 -869
  159. tapps_agents/context7/analytics.py +382 -382
  160. tapps_agents/context7/analytics_dashboard.py +299 -299
  161. tapps_agents/context7/async_cache.py +681 -681
  162. tapps_agents/context7/backup_client.py +958 -958
  163. tapps_agents/context7/cache_locking.py +194 -194
  164. tapps_agents/context7/cache_metadata.py +214 -214
  165. tapps_agents/context7/cache_prewarm.py +488 -488
  166. tapps_agents/context7/cache_structure.py +168 -168
  167. tapps_agents/context7/cache_warming.py +604 -604
  168. tapps_agents/context7/circuit_breaker.py +376 -376
  169. tapps_agents/context7/cleanup.py +461 -461
  170. tapps_agents/context7/commands.py +858 -858
  171. tapps_agents/context7/credential_validation.py +276 -276
  172. tapps_agents/context7/cross_reference_resolver.py +168 -168
  173. tapps_agents/context7/cross_references.py +424 -424
  174. tapps_agents/context7/doc_manager.py +225 -225
  175. tapps_agents/context7/fuzzy_matcher.py +369 -369
  176. tapps_agents/context7/kb_cache.py +404 -404
  177. tapps_agents/context7/language_detector.py +219 -219
  178. tapps_agents/context7/library_detector.py +725 -725
  179. tapps_agents/context7/lookup.py +738 -738
  180. tapps_agents/context7/metadata.py +258 -258
  181. tapps_agents/context7/refresh_queue.py +300 -300
  182. tapps_agents/context7/security.py +373 -373
  183. tapps_agents/context7/staleness_policies.py +278 -278
  184. tapps_agents/context7/tiles_integration.py +47 -47
  185. tapps_agents/continuous_bug_fix/__init__.py +20 -20
  186. tapps_agents/continuous_bug_fix/bug_finder.py +306 -306
  187. tapps_agents/continuous_bug_fix/bug_fix_coordinator.py +177 -177
  188. tapps_agents/continuous_bug_fix/commit_manager.py +178 -178
  189. tapps_agents/continuous_bug_fix/continuous_bug_fixer.py +322 -322
  190. tapps_agents/continuous_bug_fix/proactive_bug_finder.py +285 -285
  191. tapps_agents/core/__init__.py +298 -298
  192. tapps_agents/core/adaptive_cache_config.py +432 -432
  193. tapps_agents/core/agent_base.py +647 -647
  194. tapps_agents/core/agent_cache.py +466 -466
  195. tapps_agents/core/agent_learning.py +1865 -1865
  196. tapps_agents/core/analytics_dashboard.py +563 -563
  197. tapps_agents/core/analytics_enhancements.py +597 -597
  198. tapps_agents/core/anonymization.py +274 -274
  199. tapps_agents/core/artifact_context_builder.py +293 -0
  200. tapps_agents/core/ast_parser.py +228 -228
  201. tapps_agents/core/async_file_ops.py +402 -402
  202. tapps_agents/core/best_practice_consultant.py +299 -299
  203. tapps_agents/core/brownfield_analyzer.py +299 -299
  204. tapps_agents/core/brownfield_review.py +541 -541
  205. tapps_agents/core/browser_controller.py +513 -513
  206. tapps_agents/core/capability_registry.py +418 -418
  207. tapps_agents/core/change_impact_analyzer.py +190 -190
  208. tapps_agents/core/checkpoint_manager.py +377 -377
  209. tapps_agents/core/code_generator.py +329 -329
  210. tapps_agents/core/code_validator.py +276 -276
  211. tapps_agents/core/command_registry.py +327 -327
  212. tapps_agents/core/config.py +33 -0
  213. tapps_agents/core/context_gathering/__init__.py +2 -2
  214. tapps_agents/core/context_gathering/repository_explorer.py +28 -28
  215. tapps_agents/core/context_intelligence/__init__.py +2 -2
  216. tapps_agents/core/context_intelligence/relevance_scorer.py +24 -24
  217. tapps_agents/core/context_intelligence/token_budget_manager.py +27 -27
  218. tapps_agents/core/context_manager.py +240 -240
  219. tapps_agents/core/cursor_feedback_monitor.py +146 -146
  220. tapps_agents/core/cursor_verification.py +290 -290
  221. tapps_agents/core/customization_loader.py +280 -280
  222. tapps_agents/core/customization_schema.py +260 -260
  223. tapps_agents/core/customization_template.py +238 -238
  224. tapps_agents/core/debug_logger.py +124 -124
  225. tapps_agents/core/design_validator.py +298 -298
  226. tapps_agents/core/diagram_generator.py +226 -226
  227. tapps_agents/core/docker_utils.py +232 -232
  228. tapps_agents/core/document_generator.py +617 -617
  229. tapps_agents/core/domain_detector.py +30 -30
  230. tapps_agents/core/error_envelope.py +454 -454
  231. tapps_agents/core/error_handler.py +270 -270
  232. tapps_agents/core/estimation_tracker.py +189 -189
  233. tapps_agents/core/eval_prompt_engine.py +116 -116
  234. tapps_agents/core/evaluation_base.py +119 -119
  235. tapps_agents/core/evaluation_models.py +320 -320
  236. tapps_agents/core/evaluation_orchestrator.py +225 -225
  237. tapps_agents/core/evaluators/__init__.py +7 -7
  238. tapps_agents/core/evaluators/architectural_evaluator.py +205 -205
  239. tapps_agents/core/evaluators/behavioral_evaluator.py +160 -160
  240. tapps_agents/core/evaluators/performance_profile_evaluator.py +160 -160
  241. tapps_agents/core/evaluators/security_posture_evaluator.py +148 -148
  242. tapps_agents/core/evaluators/spec_compliance_evaluator.py +181 -181
  243. tapps_agents/core/exceptions.py +107 -107
  244. tapps_agents/core/expert_config_generator.py +293 -293
  245. tapps_agents/core/export_schema.py +202 -202
  246. tapps_agents/core/external_feedback_models.py +102 -102
  247. tapps_agents/core/external_feedback_storage.py +213 -213
  248. tapps_agents/core/fallback_strategy.py +314 -314
  249. tapps_agents/core/feedback_analyzer.py +162 -162
  250. tapps_agents/core/feedback_collector.py +178 -178
  251. tapps_agents/core/git_operations.py +445 -445
  252. tapps_agents/core/hardware_profiler.py +151 -151
  253. tapps_agents/core/instructions.py +324 -324
  254. tapps_agents/core/io_guardrails.py +69 -69
  255. tapps_agents/core/issue_manifest.py +249 -249
  256. tapps_agents/core/issue_schema.py +139 -139
  257. tapps_agents/core/json_utils.py +128 -128
  258. tapps_agents/core/knowledge_graph.py +446 -446
  259. tapps_agents/core/language_detector.py +296 -296
  260. tapps_agents/core/learning_confidence.py +242 -242
  261. tapps_agents/core/learning_dashboard.py +246 -246
  262. tapps_agents/core/learning_decision.py +384 -384
  263. tapps_agents/core/learning_explainability.py +578 -578
  264. tapps_agents/core/learning_export.py +287 -287
  265. tapps_agents/core/learning_integration.py +228 -228
  266. tapps_agents/core/llm_behavior.py +232 -232
  267. tapps_agents/core/long_duration_support.py +786 -786
  268. tapps_agents/core/mcp_setup.py +106 -106
  269. tapps_agents/core/memory_integration.py +396 -396
  270. tapps_agents/core/meta_learning.py +666 -666
  271. tapps_agents/core/module_path_sanitizer.py +199 -199
  272. tapps_agents/core/multi_agent_orchestrator.py +382 -382
  273. tapps_agents/core/network_errors.py +125 -125
  274. tapps_agents/core/nfr_validator.py +336 -336
  275. tapps_agents/core/offline_mode.py +158 -158
  276. tapps_agents/core/output_contracts.py +300 -300
  277. tapps_agents/core/output_formatter.py +300 -300
  278. tapps_agents/core/path_normalizer.py +174 -174
  279. tapps_agents/core/path_validator.py +322 -322
  280. tapps_agents/core/pattern_library.py +250 -250
  281. tapps_agents/core/performance_benchmark.py +301 -301
  282. tapps_agents/core/performance_monitor.py +184 -184
  283. tapps_agents/core/playwright_mcp_controller.py +771 -771
  284. tapps_agents/core/policy_loader.py +135 -135
  285. tapps_agents/core/progress.py +166 -166
  286. tapps_agents/core/project_profile.py +354 -354
  287. tapps_agents/core/project_type_detector.py +454 -454
  288. tapps_agents/core/prompt_base.py +223 -223
  289. tapps_agents/core/prompt_learning/__init__.py +2 -2
  290. tapps_agents/core/prompt_learning/learning_loop.py +24 -24
  291. tapps_agents/core/prompt_learning/project_prompt_store.py +25 -25
  292. tapps_agents/core/prompt_learning/skills_prompt_analyzer.py +35 -35
  293. tapps_agents/core/prompt_optimization/__init__.py +6 -6
  294. tapps_agents/core/prompt_optimization/ab_tester.py +114 -114
  295. tapps_agents/core/prompt_optimization/correlation_analyzer.py +160 -160
  296. tapps_agents/core/prompt_optimization/progressive_refiner.py +129 -129
  297. tapps_agents/core/prompt_optimization/prompt_library.py +37 -37
  298. tapps_agents/core/requirements_evaluator.py +431 -431
  299. tapps_agents/core/resource_aware_executor.py +449 -449
  300. tapps_agents/core/resource_monitor.py +343 -343
  301. tapps_agents/core/resume_handler.py +298 -298
  302. tapps_agents/core/retry_handler.py +197 -197
  303. tapps_agents/core/review_checklists.py +479 -479
  304. tapps_agents/core/role_loader.py +201 -201
  305. tapps_agents/core/role_template_loader.py +201 -201
  306. tapps_agents/core/runtime_mode.py +60 -60
  307. tapps_agents/core/security_scanner.py +342 -342
  308. tapps_agents/core/skill_agent_registry.py +194 -194
  309. tapps_agents/core/skill_integration.py +208 -208
  310. tapps_agents/core/skill_loader.py +492 -492
  311. tapps_agents/core/skill_template.py +341 -341
  312. tapps_agents/core/skill_validator.py +478 -478
  313. tapps_agents/core/stack_analyzer.py +35 -35
  314. tapps_agents/core/startup.py +174 -174
  315. tapps_agents/core/storage_manager.py +397 -397
  316. tapps_agents/core/storage_models.py +166 -166
  317. tapps_agents/core/story_evaluator.py +410 -410
  318. tapps_agents/core/subprocess_utils.py +170 -170
  319. tapps_agents/core/task_duration.py +296 -296
  320. tapps_agents/core/task_memory.py +582 -582
  321. tapps_agents/core/task_state.py +226 -226
  322. tapps_agents/core/tech_stack_priorities.py +208 -208
  323. tapps_agents/core/temp_directory.py +194 -194
  324. tapps_agents/core/template_merger.py +600 -600
  325. tapps_agents/core/template_selector.py +280 -280
  326. tapps_agents/core/test_generator.py +286 -286
  327. tapps_agents/core/tiered_context.py +253 -253
  328. tapps_agents/core/token_monitor.py +345 -345
  329. tapps_agents/core/traceability.py +254 -254
  330. tapps_agents/core/trajectory_tracker.py +50 -50
  331. tapps_agents/core/unicode_safe.py +143 -143
  332. tapps_agents/core/unified_cache_config.py +170 -170
  333. tapps_agents/core/unified_state.py +324 -324
  334. tapps_agents/core/validate_cursor_setup.py +237 -237
  335. tapps_agents/core/validation_registry.py +136 -136
  336. tapps_agents/core/validators/__init__.py +4 -4
  337. tapps_agents/core/validators/python_validator.py +87 -87
  338. tapps_agents/core/verification_agent.py +90 -90
  339. tapps_agents/core/visual_feedback.py +644 -644
  340. tapps_agents/core/workflow_validator.py +197 -197
  341. tapps_agents/core/worktree.py +367 -367
  342. tapps_agents/docker/__init__.py +10 -10
  343. tapps_agents/docker/analyzer.py +186 -186
  344. tapps_agents/docker/debugger.py +229 -229
  345. tapps_agents/docker/error_patterns.py +216 -216
  346. tapps_agents/epic/__init__.py +22 -22
  347. tapps_agents/epic/beads_sync.py +115 -115
  348. tapps_agents/epic/markdown_sync.py +105 -105
  349. tapps_agents/epic/models.py +96 -96
  350. tapps_agents/experts/__init__.py +163 -163
  351. tapps_agents/experts/agent_integration.py +243 -243
  352. tapps_agents/experts/auto_generator.py +331 -331
  353. tapps_agents/experts/base_expert.py +536 -536
  354. tapps_agents/experts/builtin_registry.py +261 -261
  355. tapps_agents/experts/business_metrics.py +565 -565
  356. tapps_agents/experts/cache.py +266 -266
  357. tapps_agents/experts/confidence_breakdown.py +306 -306
  358. tapps_agents/experts/confidence_calculator.py +336 -336
  359. tapps_agents/experts/confidence_metrics.py +236 -236
  360. tapps_agents/experts/domain_config.py +311 -311
  361. tapps_agents/experts/domain_detector.py +550 -550
  362. tapps_agents/experts/domain_utils.py +84 -84
  363. tapps_agents/experts/expert_config.py +113 -113
  364. tapps_agents/experts/expert_engine.py +465 -465
  365. tapps_agents/experts/expert_registry.py +744 -744
  366. tapps_agents/experts/expert_synthesizer.py +70 -70
  367. tapps_agents/experts/governance.py +197 -197
  368. tapps_agents/experts/history_logger.py +312 -312
  369. tapps_agents/experts/knowledge/README.md +180 -180
  370. tapps_agents/experts/knowledge/accessibility/accessible-forms.md +331 -331
  371. tapps_agents/experts/knowledge/accessibility/aria-patterns.md +344 -344
  372. tapps_agents/experts/knowledge/accessibility/color-contrast.md +285 -285
  373. tapps_agents/experts/knowledge/accessibility/keyboard-navigation.md +332 -332
  374. tapps_agents/experts/knowledge/accessibility/screen-readers.md +282 -282
  375. tapps_agents/experts/knowledge/accessibility/semantic-html.md +355 -355
  376. tapps_agents/experts/knowledge/accessibility/testing-accessibility.md +369 -369
  377. tapps_agents/experts/knowledge/accessibility/wcag-2.1.md +296 -296
  378. tapps_agents/experts/knowledge/accessibility/wcag-2.2.md +211 -211
  379. tapps_agents/experts/knowledge/agent-learning/best-practices.md +715 -715
  380. tapps_agents/experts/knowledge/agent-learning/pattern-extraction.md +282 -282
  381. tapps_agents/experts/knowledge/agent-learning/prompt-optimization.md +320 -320
  382. tapps_agents/experts/knowledge/ai-frameworks/model-optimization.md +90 -90
  383. tapps_agents/experts/knowledge/ai-frameworks/openvino-patterns.md +260 -260
  384. tapps_agents/experts/knowledge/api-design-integration/api-gateway-patterns.md +309 -309
  385. tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +521 -521
  386. tapps_agents/experts/knowledge/api-design-integration/api-versioning.md +421 -421
  387. tapps_agents/experts/knowledge/api-design-integration/async-protocol-patterns.md +61 -61
  388. tapps_agents/experts/knowledge/api-design-integration/contract-testing.md +221 -221
  389. tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +489 -489
  390. tapps_agents/experts/knowledge/api-design-integration/fastapi-patterns.md +360 -360
  391. tapps_agents/experts/knowledge/api-design-integration/fastapi-testing.md +262 -262
  392. tapps_agents/experts/knowledge/api-design-integration/graphql-patterns.md +582 -582
  393. tapps_agents/experts/knowledge/api-design-integration/grpc-best-practices.md +499 -499
  394. tapps_agents/experts/knowledge/api-design-integration/mqtt-patterns.md +455 -455
  395. tapps_agents/experts/knowledge/api-design-integration/rate-limiting.md +507 -507
  396. tapps_agents/experts/knowledge/api-design-integration/restful-api-design.md +618 -618
  397. tapps_agents/experts/knowledge/api-design-integration/websocket-patterns.md +480 -480
  398. tapps_agents/experts/knowledge/cloud-infrastructure/cloud-native-patterns.md +175 -175
  399. tapps_agents/experts/knowledge/cloud-infrastructure/container-health-checks.md +261 -261
  400. tapps_agents/experts/knowledge/cloud-infrastructure/containerization.md +222 -222
  401. tapps_agents/experts/knowledge/cloud-infrastructure/cost-optimization.md +122 -122
  402. tapps_agents/experts/knowledge/cloud-infrastructure/disaster-recovery.md +153 -153
  403. tapps_agents/experts/knowledge/cloud-infrastructure/dockerfile-patterns.md +285 -285
  404. tapps_agents/experts/knowledge/cloud-infrastructure/infrastructure-as-code.md +187 -187
  405. tapps_agents/experts/knowledge/cloud-infrastructure/kubernetes-patterns.md +253 -253
  406. tapps_agents/experts/knowledge/cloud-infrastructure/multi-cloud-strategies.md +155 -155
  407. tapps_agents/experts/knowledge/cloud-infrastructure/serverless-architecture.md +200 -200
  408. tapps_agents/experts/knowledge/code-quality-analysis/README.md +16 -16
  409. tapps_agents/experts/knowledge/code-quality-analysis/code-metrics.md +137 -137
  410. tapps_agents/experts/knowledge/code-quality-analysis/complexity-analysis.md +181 -181
  411. tapps_agents/experts/knowledge/code-quality-analysis/technical-debt-patterns.md +191 -191
  412. tapps_agents/experts/knowledge/data-privacy-compliance/anonymization.md +313 -313
  413. tapps_agents/experts/knowledge/data-privacy-compliance/ccpa.md +255 -255
  414. tapps_agents/experts/knowledge/data-privacy-compliance/consent-management.md +282 -282
  415. tapps_agents/experts/knowledge/data-privacy-compliance/data-minimization.md +275 -275
  416. tapps_agents/experts/knowledge/data-privacy-compliance/data-retention.md +297 -297
  417. tapps_agents/experts/knowledge/data-privacy-compliance/data-subject-rights.md +383 -383
  418. tapps_agents/experts/knowledge/data-privacy-compliance/encryption-privacy.md +285 -285
  419. tapps_agents/experts/knowledge/data-privacy-compliance/gdpr.md +344 -344
  420. tapps_agents/experts/knowledge/data-privacy-compliance/hipaa.md +385 -385
  421. tapps_agents/experts/knowledge/data-privacy-compliance/privacy-by-design.md +280 -280
  422. tapps_agents/experts/knowledge/database-data-management/acid-vs-cap.md +164 -164
  423. tapps_agents/experts/knowledge/database-data-management/backup-and-recovery.md +182 -182
  424. tapps_agents/experts/knowledge/database-data-management/data-modeling.md +172 -172
  425. tapps_agents/experts/knowledge/database-data-management/database-design.md +187 -187
  426. tapps_agents/experts/knowledge/database-data-management/flux-query-optimization.md +342 -342
  427. tapps_agents/experts/knowledge/database-data-management/influxdb-connection-patterns.md +432 -432
  428. tapps_agents/experts/knowledge/database-data-management/influxdb-patterns.md +442 -442
  429. tapps_agents/experts/knowledge/database-data-management/migration-strategies.md +216 -216
  430. tapps_agents/experts/knowledge/database-data-management/nosql-patterns.md +259 -259
  431. tapps_agents/experts/knowledge/database-data-management/scalability-patterns.md +184 -184
  432. tapps_agents/experts/knowledge/database-data-management/sql-optimization.md +175 -175
  433. tapps_agents/experts/knowledge/database-data-management/time-series-modeling.md +444 -444
  434. tapps_agents/experts/knowledge/development-workflow/README.md +16 -16
  435. tapps_agents/experts/knowledge/development-workflow/automation-best-practices.md +216 -216
  436. tapps_agents/experts/knowledge/development-workflow/build-strategies.md +198 -198
  437. tapps_agents/experts/knowledge/development-workflow/deployment-patterns.md +205 -205
  438. tapps_agents/experts/knowledge/development-workflow/git-workflows.md +205 -205
  439. tapps_agents/experts/knowledge/documentation-knowledge-management/README.md +16 -16
  440. tapps_agents/experts/knowledge/documentation-knowledge-management/api-documentation-patterns.md +231 -231
  441. tapps_agents/experts/knowledge/documentation-knowledge-management/documentation-standards.md +191 -191
  442. tapps_agents/experts/knowledge/documentation-knowledge-management/knowledge-management.md +171 -171
  443. tapps_agents/experts/knowledge/documentation-knowledge-management/technical-writing-guide.md +192 -192
  444. tapps_agents/experts/knowledge/observability-monitoring/alerting-patterns.md +461 -461
  445. tapps_agents/experts/knowledge/observability-monitoring/apm-tools.md +459 -459
  446. tapps_agents/experts/knowledge/observability-monitoring/distributed-tracing.md +367 -367
  447. tapps_agents/experts/knowledge/observability-monitoring/logging-strategies.md +478 -478
  448. tapps_agents/experts/knowledge/observability-monitoring/metrics-and-monitoring.md +510 -510
  449. tapps_agents/experts/knowledge/observability-monitoring/observability-best-practices.md +492 -492
  450. tapps_agents/experts/knowledge/observability-monitoring/open-telemetry.md +573 -573
  451. tapps_agents/experts/knowledge/observability-monitoring/slo-sli-sla.md +419 -419
  452. tapps_agents/experts/knowledge/performance/anti-patterns.md +284 -284
  453. tapps_agents/experts/knowledge/performance/api-performance.md +256 -256
  454. tapps_agents/experts/knowledge/performance/caching.md +327 -327
  455. tapps_agents/experts/knowledge/performance/database-performance.md +252 -252
  456. tapps_agents/experts/knowledge/performance/optimization-patterns.md +327 -327
  457. tapps_agents/experts/knowledge/performance/profiling.md +297 -297
  458. tapps_agents/experts/knowledge/performance/resource-management.md +293 -293
  459. tapps_agents/experts/knowledge/performance/scalability.md +306 -306
  460. tapps_agents/experts/knowledge/security/owasp-top10.md +209 -209
  461. tapps_agents/experts/knowledge/security/secure-coding-practices.md +207 -207
  462. tapps_agents/experts/knowledge/security/threat-modeling.md +220 -220
  463. tapps_agents/experts/knowledge/security/vulnerability-patterns.md +342 -342
  464. tapps_agents/experts/knowledge/software-architecture/docker-compose-patterns.md +314 -314
  465. tapps_agents/experts/knowledge/software-architecture/microservices-patterns.md +379 -379
  466. tapps_agents/experts/knowledge/software-architecture/service-communication.md +316 -316
  467. tapps_agents/experts/knowledge/testing/best-practices.md +310 -310
  468. tapps_agents/experts/knowledge/testing/coverage-analysis.md +293 -293
  469. tapps_agents/experts/knowledge/testing/mocking.md +256 -256
  470. tapps_agents/experts/knowledge/testing/test-automation.md +276 -276
  471. tapps_agents/experts/knowledge/testing/test-data.md +271 -271
  472. tapps_agents/experts/knowledge/testing/test-design-patterns.md +280 -280
  473. tapps_agents/experts/knowledge/testing/test-maintenance.md +236 -236
  474. tapps_agents/experts/knowledge/testing/test-strategies.md +311 -311
  475. tapps_agents/experts/knowledge/user-experience/information-architecture.md +325 -325
  476. tapps_agents/experts/knowledge/user-experience/interaction-design.md +363 -363
  477. tapps_agents/experts/knowledge/user-experience/prototyping.md +293 -293
  478. tapps_agents/experts/knowledge/user-experience/usability-heuristics.md +337 -337
  479. tapps_agents/experts/knowledge/user-experience/usability-testing.md +311 -311
  480. tapps_agents/experts/knowledge/user-experience/user-journeys.md +296 -296
  481. tapps_agents/experts/knowledge/user-experience/user-research.md +373 -373
  482. tapps_agents/experts/knowledge/user-experience/ux-principles.md +340 -340
  483. tapps_agents/experts/knowledge_freshness.py +321 -321
  484. tapps_agents/experts/knowledge_ingestion.py +438 -438
  485. tapps_agents/experts/knowledge_need_detector.py +93 -93
  486. tapps_agents/experts/knowledge_validator.py +382 -382
  487. tapps_agents/experts/observability.py +440 -440
  488. tapps_agents/experts/passive_notifier.py +238 -238
  489. tapps_agents/experts/proactive_orchestrator.py +32 -32
  490. tapps_agents/experts/rag_chunker.py +205 -205
  491. tapps_agents/experts/rag_embedder.py +152 -152
  492. tapps_agents/experts/rag_evaluation.py +299 -299
  493. tapps_agents/experts/rag_index.py +303 -303
  494. tapps_agents/experts/rag_metrics.py +293 -293
  495. tapps_agents/experts/rag_safety.py +263 -263
  496. tapps_agents/experts/report_generator.py +296 -296
  497. tapps_agents/experts/setup_wizard.py +441 -441
  498. tapps_agents/experts/simple_rag.py +431 -431
  499. tapps_agents/experts/vector_rag.py +354 -354
  500. tapps_agents/experts/weight_distributor.py +304 -304
  501. tapps_agents/health/__init__.py +24 -24
  502. tapps_agents/health/base.py +75 -75
  503. tapps_agents/health/checks/__init__.py +22 -22
  504. tapps_agents/health/checks/automation.py +127 -127
  505. tapps_agents/health/checks/context7_cache.py +210 -210
  506. tapps_agents/health/checks/environment.py +116 -116
  507. tapps_agents/health/checks/execution.py +170 -170
  508. tapps_agents/health/checks/knowledge_base.py +187 -187
  509. tapps_agents/health/checks/outcomes.py +324 -324
  510. tapps_agents/health/collector.py +280 -280
  511. tapps_agents/health/dashboard.py +137 -137
  512. tapps_agents/health/metrics.py +151 -151
  513. tapps_agents/health/orchestrator.py +271 -271
  514. tapps_agents/health/registry.py +166 -166
  515. tapps_agents/hooks/__init__.py +33 -33
  516. tapps_agents/hooks/config.py +140 -140
  517. tapps_agents/hooks/events.py +135 -135
  518. tapps_agents/hooks/executor.py +128 -128
  519. tapps_agents/hooks/manager.py +143 -143
  520. tapps_agents/integration/__init__.py +8 -8
  521. tapps_agents/integration/service_integrator.py +121 -121
  522. tapps_agents/integrations/__init__.py +10 -10
  523. tapps_agents/integrations/clawdbot.py +525 -525
  524. tapps_agents/integrations/memory_bridge.py +356 -356
  525. tapps_agents/mcp/__init__.py +18 -18
  526. tapps_agents/mcp/gateway.py +112 -112
  527. tapps_agents/mcp/servers/__init__.py +13 -13
  528. tapps_agents/mcp/servers/analysis.py +204 -204
  529. tapps_agents/mcp/servers/context7.py +198 -198
  530. tapps_agents/mcp/servers/filesystem.py +218 -218
  531. tapps_agents/mcp/servers/git.py +201 -201
  532. tapps_agents/mcp/tool_registry.py +115 -115
  533. tapps_agents/quality/__init__.py +54 -54
  534. tapps_agents/quality/coverage_analyzer.py +379 -379
  535. tapps_agents/quality/enforcement.py +82 -82
  536. tapps_agents/quality/gates/__init__.py +37 -37
  537. tapps_agents/quality/gates/approval_gate.py +255 -255
  538. tapps_agents/quality/gates/base.py +84 -84
  539. tapps_agents/quality/gates/exceptions.py +43 -43
  540. tapps_agents/quality/gates/policy_gate.py +195 -195
  541. tapps_agents/quality/gates/registry.py +239 -239
  542. tapps_agents/quality/gates/security_gate.py +156 -156
  543. tapps_agents/quality/quality_gates.py +369 -369
  544. tapps_agents/quality/secret_scanner.py +335 -335
  545. tapps_agents/session/__init__.py +19 -19
  546. tapps_agents/session/manager.py +256 -256
  547. tapps_agents/simple_mode/__init__.py +66 -66
  548. tapps_agents/simple_mode/agent_contracts.py +357 -357
  549. tapps_agents/simple_mode/beads_hooks.py +151 -151
  550. tapps_agents/simple_mode/code_snippet_handler.py +382 -382
  551. tapps_agents/simple_mode/documentation_manager.py +395 -395
  552. tapps_agents/simple_mode/documentation_reader.py +187 -187
  553. tapps_agents/simple_mode/file_inference.py +292 -292
  554. tapps_agents/simple_mode/framework_change_detector.py +268 -268
  555. tapps_agents/simple_mode/intent_parser.py +510 -510
  556. tapps_agents/simple_mode/learning_progression.py +358 -358
  557. tapps_agents/simple_mode/nl_handler.py +700 -700
  558. tapps_agents/simple_mode/onboarding.py +253 -253
  559. tapps_agents/simple_mode/orchestrators/__init__.py +38 -38
  560. tapps_agents/simple_mode/orchestrators/base.py +185 -185
  561. tapps_agents/simple_mode/orchestrators/breakdown_orchestrator.py +49 -49
  562. tapps_agents/simple_mode/orchestrators/brownfield_orchestrator.py +135 -135
  563. tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2700 -2667
  564. tapps_agents/simple_mode/orchestrators/deliverable_checklist.py +349 -349
  565. tapps_agents/simple_mode/orchestrators/enhance_orchestrator.py +53 -53
  566. tapps_agents/simple_mode/orchestrators/epic_orchestrator.py +122 -122
  567. tapps_agents/simple_mode/orchestrators/explore_orchestrator.py +184 -184
  568. tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +723 -723
  569. tapps_agents/simple_mode/orchestrators/plan_analysis_orchestrator.py +206 -206
  570. tapps_agents/simple_mode/orchestrators/pr_orchestrator.py +237 -237
  571. tapps_agents/simple_mode/orchestrators/refactor_orchestrator.py +222 -222
  572. tapps_agents/simple_mode/orchestrators/requirements_tracer.py +262 -262
  573. tapps_agents/simple_mode/orchestrators/resume_orchestrator.py +210 -210
  574. tapps_agents/simple_mode/orchestrators/review_orchestrator.py +161 -161
  575. tapps_agents/simple_mode/orchestrators/test_orchestrator.py +82 -82
  576. tapps_agents/simple_mode/output_aggregator.py +340 -340
  577. tapps_agents/simple_mode/result_formatters.py +598 -598
  578. tapps_agents/simple_mode/step_dependencies.py +382 -382
  579. tapps_agents/simple_mode/step_results.py +276 -276
  580. tapps_agents/simple_mode/streaming.py +388 -388
  581. tapps_agents/simple_mode/variations.py +129 -129
  582. tapps_agents/simple_mode/visual_feedback.py +238 -238
  583. tapps_agents/simple_mode/zero_config.py +274 -274
  584. tapps_agents/suggestions/__init__.py +8 -8
  585. tapps_agents/suggestions/inline_suggester.py +52 -52
  586. tapps_agents/templates/__init__.py +8 -8
  587. tapps_agents/templates/microservice_generator.py +274 -274
  588. tapps_agents/utils/env_validator.py +291 -291
  589. tapps_agents/workflow/__init__.py +171 -171
  590. tapps_agents/workflow/acceptance_verifier.py +132 -132
  591. tapps_agents/workflow/agent_handlers/__init__.py +41 -41
  592. tapps_agents/workflow/agent_handlers/analyst_handler.py +75 -75
  593. tapps_agents/workflow/agent_handlers/architect_handler.py +107 -107
  594. tapps_agents/workflow/agent_handlers/base.py +84 -84
  595. tapps_agents/workflow/agent_handlers/debugger_handler.py +100 -100
  596. tapps_agents/workflow/agent_handlers/designer_handler.py +110 -110
  597. tapps_agents/workflow/agent_handlers/documenter_handler.py +94 -94
  598. tapps_agents/workflow/agent_handlers/implementer_handler.py +235 -235
  599. tapps_agents/workflow/agent_handlers/ops_handler.py +62 -62
  600. tapps_agents/workflow/agent_handlers/orchestrator_handler.py +43 -43
  601. tapps_agents/workflow/agent_handlers/planner_handler.py +98 -98
  602. tapps_agents/workflow/agent_handlers/registry.py +119 -119
  603. tapps_agents/workflow/agent_handlers/reviewer_handler.py +119 -119
  604. tapps_agents/workflow/agent_handlers/tester_handler.py +69 -69
  605. tapps_agents/workflow/analytics_accessor.py +337 -337
  606. tapps_agents/workflow/analytics_alerts.py +416 -416
  607. tapps_agents/workflow/analytics_dashboard_cursor.py +281 -281
  608. tapps_agents/workflow/analytics_dual_write.py +103 -103
  609. tapps_agents/workflow/analytics_integration.py +119 -119
  610. tapps_agents/workflow/analytics_query_parser.py +278 -278
  611. tapps_agents/workflow/analytics_visualizer.py +259 -259
  612. tapps_agents/workflow/artifact_helper.py +204 -204
  613. tapps_agents/workflow/audit_logger.py +263 -263
  614. tapps_agents/workflow/auto_execution_config.py +340 -340
  615. tapps_agents/workflow/auto_progression.py +586 -586
  616. tapps_agents/workflow/branch_cleanup.py +349 -349
  617. tapps_agents/workflow/checkpoint.py +256 -256
  618. tapps_agents/workflow/checkpoint_manager.py +178 -178
  619. tapps_agents/workflow/code_artifact.py +179 -179
  620. tapps_agents/workflow/common_enums.py +96 -96
  621. tapps_agents/workflow/confirmation_handler.py +130 -130
  622. tapps_agents/workflow/context_analyzer.py +222 -222
  623. tapps_agents/workflow/context_artifact.py +230 -230
  624. tapps_agents/workflow/cursor_chat.py +94 -94
  625. tapps_agents/workflow/cursor_executor.py +2337 -2196
  626. tapps_agents/workflow/cursor_skill_helper.py +516 -516
  627. tapps_agents/workflow/dependency_resolver.py +244 -244
  628. tapps_agents/workflow/design_artifact.py +156 -156
  629. tapps_agents/workflow/detector.py +751 -751
  630. tapps_agents/workflow/direct_execution_fallback.py +301 -301
  631. tapps_agents/workflow/docs_artifact.py +168 -168
  632. tapps_agents/workflow/enforcer.py +389 -389
  633. tapps_agents/workflow/enhancement_artifact.py +142 -142
  634. tapps_agents/workflow/error_recovery.py +806 -806
  635. tapps_agents/workflow/event_bus.py +183 -183
  636. tapps_agents/workflow/event_log.py +612 -612
  637. tapps_agents/workflow/events.py +63 -63
  638. tapps_agents/workflow/exceptions.py +43 -43
  639. tapps_agents/workflow/execution_graph.py +498 -498
  640. tapps_agents/workflow/execution_plan.py +126 -126
  641. tapps_agents/workflow/file_utils.py +186 -186
  642. tapps_agents/workflow/gate_evaluator.py +182 -182
  643. tapps_agents/workflow/gate_integration.py +200 -200
  644. tapps_agents/workflow/graph_visualizer.py +130 -130
  645. tapps_agents/workflow/health_checker.py +206 -206
  646. tapps_agents/workflow/logging_helper.py +243 -243
  647. tapps_agents/workflow/manifest.py +582 -582
  648. tapps_agents/workflow/marker_writer.py +250 -250
  649. tapps_agents/workflow/message_formatter.py +188 -188
  650. tapps_agents/workflow/messaging.py +325 -325
  651. tapps_agents/workflow/metadata_models.py +91 -91
  652. tapps_agents/workflow/metrics_integration.py +226 -226
  653. tapps_agents/workflow/migration_utils.py +116 -116
  654. tapps_agents/workflow/models.py +148 -111
  655. tapps_agents/workflow/nlp_config.py +198 -198
  656. tapps_agents/workflow/nlp_error_handler.py +207 -207
  657. tapps_agents/workflow/nlp_executor.py +163 -163
  658. tapps_agents/workflow/nlp_parser.py +528 -528
  659. tapps_agents/workflow/observability_dashboard.py +451 -451
  660. tapps_agents/workflow/observer.py +170 -170
  661. tapps_agents/workflow/ops_artifact.py +257 -257
  662. tapps_agents/workflow/output_passing.py +214 -214
  663. tapps_agents/workflow/parallel_executor.py +463 -463
  664. tapps_agents/workflow/planning_artifact.py +179 -179
  665. tapps_agents/workflow/preset_loader.py +285 -285
  666. tapps_agents/workflow/preset_recommender.py +270 -270
  667. tapps_agents/workflow/progress_logger.py +145 -145
  668. tapps_agents/workflow/progress_manager.py +303 -303
  669. tapps_agents/workflow/progress_monitor.py +186 -186
  670. tapps_agents/workflow/progress_updates.py +423 -423
  671. tapps_agents/workflow/quality_artifact.py +158 -158
  672. tapps_agents/workflow/quality_loopback.py +101 -101
  673. tapps_agents/workflow/recommender.py +387 -387
  674. tapps_agents/workflow/remediation_loop.py +166 -166
  675. tapps_agents/workflow/result_aggregator.py +300 -300
  676. tapps_agents/workflow/review_artifact.py +185 -185
  677. tapps_agents/workflow/schema_validator.py +522 -522
  678. tapps_agents/workflow/session_handoff.py +178 -178
  679. tapps_agents/workflow/skill_invoker.py +648 -648
  680. tapps_agents/workflow/state_manager.py +756 -756
  681. tapps_agents/workflow/state_persistence_config.py +331 -331
  682. tapps_agents/workflow/status_monitor.py +449 -449
  683. tapps_agents/workflow/step_checkpoint.py +314 -314
  684. tapps_agents/workflow/step_details.py +201 -201
  685. tapps_agents/workflow/story_models.py +147 -147
  686. tapps_agents/workflow/streaming.py +416 -416
  687. tapps_agents/workflow/suggestion_engine.py +552 -552
  688. tapps_agents/workflow/testing_artifact.py +186 -186
  689. tapps_agents/workflow/timeline.py +158 -158
  690. tapps_agents/workflow/token_integration.py +209 -209
  691. tapps_agents/workflow/validation.py +217 -217
  692. tapps_agents/workflow/visual_feedback.py +391 -391
  693. tapps_agents/workflow/workflow_chain.py +95 -95
  694. tapps_agents/workflow/workflow_summary.py +219 -219
  695. tapps_agents/workflow/worktree_manager.py +724 -724
  696. {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/METADATA +672 -672
  697. tapps_agents-3.6.0.dist-info/RECORD +758 -0
  698. {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/licenses/LICENSE +22 -22
  699. tapps_agents/health/checks/outcomes.backup_20260204_064058.py +0 -324
  700. tapps_agents/health/checks/outcomes.backup_20260204_064256.py +0 -324
  701. tapps_agents/health/checks/outcomes.backup_20260204_064600.py +0 -324
  702. tapps_agents-3.5.40.dist-info/RECORD +0 -760
  703. {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/WHEEL +0 -0
  704. {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/entry_points.txt +0 -0
  705. {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/top_level.txt +0 -0
@@ -1,1566 +1,1566 @@
1
- """
2
- Code Scoring System - Calculates objective quality metrics
3
- """
4
-
5
- import ast
6
- import json as json_lib
7
- import logging
8
- import shutil
9
- import subprocess # nosec B404 - used with fixed args, no shell
10
- import sys
11
- from pathlib import Path
12
- from typing import Any, Protocol
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- from ...core.config import ProjectConfig, ScoringWeightsConfig
17
- from ...core.language_detector import Language
18
- from ...core.subprocess_utils import wrap_windows_cmd_shim
19
- from .score_constants import ComplexityConstants, SecurityConstants
20
- from .validation import validate_code_input
21
-
22
- # Import analysis libraries
23
- try:
24
- from radon.complexity import cc_visit
25
- from radon.metrics import mi_visit
26
-
27
- HAS_RADON = True
28
- except ImportError:
29
- HAS_RADON = False
30
-
31
- try:
32
- import bandit
33
- from bandit.core import config as bandit_config
34
- from bandit.core import manager
35
-
36
- HAS_BANDIT = True
37
- except ImportError:
38
- HAS_BANDIT = False
39
-
40
- # Check if ruff is available in PATH or via python -m ruff
41
- def _check_ruff_available() -> bool:
42
- """Check if ruff is available via 'ruff' command or 'python -m ruff'"""
43
- # Check for ruff command directly
44
- if shutil.which("ruff"):
45
- return True
46
- # Check for python -m ruff
47
- try:
48
- result = subprocess.run( # nosec B603 - fixed args
49
- [sys.executable, "-m", "ruff", "--version"],
50
- capture_output=True,
51
- timeout=5,
52
- check=False,
53
- )
54
- return result.returncode == 0
55
- except (subprocess.TimeoutExpired, FileNotFoundError):
56
- return False
57
-
58
-
59
- HAS_RUFF = _check_ruff_available()
60
-
61
- # Check if mypy is available in PATH
62
- HAS_MYPY = shutil.which("mypy") is not None
63
-
64
-
65
- # Check if jscpd is available (via npm/npx)
66
- def _check_jscpd_available() -> bool:
67
- """Check if jscpd is available via jscpd command or npx jscpd"""
68
- # Check for jscpd command directly
69
- if shutil.which("jscpd"):
70
- return True
71
- # Check for npx (Node.js package runner)
72
- npx_path = shutil.which("npx")
73
- if npx_path:
74
- try:
75
- cmd = wrap_windows_cmd_shim([npx_path, "--yes", "jscpd", "--version"])
76
- result = subprocess.run( # nosec B603 - fixed args
77
- cmd,
78
- capture_output=True,
79
- timeout=5,
80
- check=False,
81
- )
82
- return result.returncode == 0
83
- except (subprocess.TimeoutExpired, FileNotFoundError):
84
- return False
85
- return False
86
-
87
-
88
- HAS_JSCPD = _check_jscpd_available()
89
-
90
- # Import coverage tools
91
- try:
92
- from coverage import Coverage
93
-
94
- HAS_COVERAGE = True
95
- except ImportError:
96
- HAS_COVERAGE = False
97
-
98
-
99
- class BaseScorer:
100
- """
101
- Base class for all scorers.
102
- Defines the interface and shared helpers: _find_project_root, _calculate_structure_score, _calculate_devex_score (7-category §3.2).
103
- """
104
-
105
- def score_file(self, file_path: Path, code: str) -> dict[str, Any]:
106
- """Score a file and return quality metrics. Subclasses must implement."""
107
- raise NotImplementedError("Subclasses must implement score_file")
108
-
109
- @staticmethod
110
- def _find_project_root(file_path: Path) -> Path | None:
111
- """Find project root by common markers (.git, pyproject.toml, package.json, .tapps-agents, etc.)."""
112
- current = file_path.resolve().parent
113
- markers = [".git", "pyproject.toml", "setup.py", "requirements.txt", ".tapps-agents", "package.json"]
114
- for _ in range(10):
115
- for m in markers:
116
- if (current / m).exists():
117
- return current
118
- if current.parent == current:
119
- break
120
- current = current.parent
121
- return None
122
-
123
- @classmethod
124
- def _calculate_structure_score(cls, file_path: Path) -> float:
125
- """Structure score (0-10). Project layout, key files. 7-category §3.2."""
126
- root = BaseScorer._find_project_root(file_path)
127
- if root is None:
128
- return 5.0
129
- pts = 0.0
130
- if (root / "pyproject.toml").exists() or (root / "package.json").exists():
131
- pts += 2.5
132
- if (root / "README").exists() or (root / "README.md").exists() or (root / "README.rst").exists():
133
- pts += 2.0
134
- if (root / "tests").exists() or (root / "test").exists():
135
- pts += 2.0
136
- if (root / ".tapps-agents").exists() or (root / ".git").exists():
137
- pts += 1.0
138
- if (root / "setup.py").exists() or (root / "requirements.txt").exists() or (root / "package-lock.json").exists():
139
- pts += 1.5
140
- return min(10.0, pts * 2.0)
141
-
142
- @classmethod
143
- def _calculate_devex_score(cls, file_path: Path) -> float:
144
- """DevEx score (0-10). Docs, config, tooling. 7-category §3.2."""
145
- root = BaseScorer._find_project_root(file_path)
146
- if root is None:
147
- return 5.0
148
- pts = 0.0
149
- if (root / "AGENTS.md").exists() or (root / "CLAUDE.md").exists():
150
- pts += 3.0
151
- if (root / "docs").exists() and (root / "docs").is_dir():
152
- pts += 2.0
153
- if (root / ".tapps-agents").exists() or (root / ".cursor").exists():
154
- pts += 2.0
155
- pyproject, pkg = root / "pyproject.toml", root / "package.json"
156
- if pyproject.exists():
157
- try:
158
- t = pyproject.read_text(encoding="utf-8", errors="replace")
159
- if "[tool.ruff]" in t or "[tool.mypy]" in t or "pytest" in t:
160
- pts += 1.5
161
- except Exception:
162
- pass
163
- if pkg.exists():
164
- try:
165
- import json as _j
166
- d = _j.loads(pkg.read_text(encoding="utf-8", errors="replace"))
167
- dev = (d.get("devDependencies") or {}) if isinstance(d, dict) else {}
168
- if any(k in dev for k in ("eslint", "jest", "vitest", "mypy")):
169
- pts += 1.5
170
- except Exception:
171
- pass
172
- return min(10.0, pts * 2.0)
173
-
174
-
175
- class CodeScorer(BaseScorer):
176
- """Calculate code quality scores for Python files"""
177
-
178
- def __init__(
179
- self,
180
- weights: ScoringWeightsConfig | None = None,
181
- ruff_enabled: bool = True,
182
- mypy_enabled: bool = True,
183
- jscpd_enabled: bool = True,
184
- duplication_threshold: float = 3.0,
185
- min_duplication_lines: int = 5,
186
- ):
187
- self.has_radon = HAS_RADON
188
- self.has_bandit = HAS_BANDIT
189
- self.has_coverage = HAS_COVERAGE
190
- self.has_ruff = HAS_RUFF and ruff_enabled
191
- self.has_mypy = HAS_MYPY and mypy_enabled
192
- self.has_jscpd = HAS_JSCPD and jscpd_enabled
193
- self.duplication_threshold = duplication_threshold
194
- self.min_duplication_lines = min_duplication_lines
195
- self.weights = weights # Will use defaults if None
196
-
197
- def score_file(self, file_path: Path, code: str) -> dict[str, Any]:
198
- """
199
- Calculate scores for a code file.
200
-
201
- Returns:
202
- {
203
- "complexity_score": float (0-10),
204
- "security_score": float (0-10),
205
- "maintainability_score": float (0-10),
206
- "overall_score": float (0-100),
207
- "metrics": {...}
208
- }
209
- """
210
- metrics: dict[str, float] = {}
211
- scores: dict[str, Any] = {
212
- "complexity_score": 0.0,
213
- "security_score": 0.0,
214
- "maintainability_score": 0.0,
215
- "test_coverage_score": 0.0,
216
- "performance_score": 0.0,
217
- "structure_score": 0.0, # 7-category: project layout, key files (MCP_SYSTEMS_IMPROVEMENT_RECOMMENDATIONS §3.2)
218
- "devex_score": 0.0, # 7-category: docs, config, tooling (MCP_SYSTEMS_IMPROVEMENT_RECOMMENDATIONS §3.2)
219
- "linting_score": 0.0, # Phase 6.1: Ruff linting score
220
- "type_checking_score": 0.0, # Phase 6.2: mypy type checking score
221
- "duplication_score": 0.0, # Phase 6.4: jscpd duplication score
222
- "metrics": metrics,
223
- }
224
-
225
- # Complexity Score (0-10, lower is better)
226
- scores["complexity_score"] = self._calculate_complexity(code)
227
- metrics["complexity"] = float(scores["complexity_score"])
228
-
229
- # Security Score (0-10, higher is better)
230
- scores["security_score"] = self._calculate_security(file_path, code)
231
- metrics["security"] = float(scores["security_score"])
232
-
233
- # Maintainability Score (0-10, higher is better)
234
- scores["maintainability_score"] = self._calculate_maintainability(code)
235
- metrics["maintainability"] = float(scores["maintainability_score"])
236
-
237
- # Test Coverage Score (0-10, higher is better)
238
- scores["test_coverage_score"] = self._calculate_test_coverage(file_path)
239
- metrics["test_coverage"] = float(scores["test_coverage_score"])
240
-
241
- # Performance Score (0-10, higher is better)
242
- # Phase 3.2: Use context-aware performance scorer
243
- from .performance_scorer import PerformanceScorer
244
- from ...core.language_detector import Language
245
-
246
- performance_scorer = PerformanceScorer()
247
- scores["performance_score"] = performance_scorer.calculate(
248
- code, Language.PYTHON, file_path, context=None
249
- )
250
- metrics["performance"] = float(scores["performance_score"])
251
-
252
- # Linting Score (0-10, higher is better) - Phase 6.1
253
- scores["linting_score"] = self._calculate_linting_score(file_path)
254
- metrics["linting"] = float(scores["linting_score"])
255
-
256
- # Get actual linting issues for transparency (P1 Improvement)
257
- linting_issues = self.get_ruff_issues(file_path)
258
- scores["linting_issues"] = self._format_ruff_issues(linting_issues)
259
- scores["linting_issue_count"] = len(linting_issues)
260
-
261
- # Type Checking Score (0-10, higher is better) - Phase 6.2
262
- scores["type_checking_score"] = self._calculate_type_checking_score(file_path)
263
- metrics["type_checking"] = float(scores["type_checking_score"])
264
-
265
- # Get actual type checking issues for transparency (P1 Improvement)
266
- type_issues = self.get_mypy_errors(file_path)
267
- scores["type_issues"] = type_issues # Already formatted
268
- scores["type_issue_count"] = len(type_issues)
269
-
270
- # Duplication Score (0-10, higher is better) - Phase 6.4
271
- scores["duplication_score"] = self._calculate_duplication_score(file_path)
272
- metrics["duplication"] = float(scores["duplication_score"])
273
-
274
- # Structure Score (0-10, higher is better) - 7-category §3.2
275
- scores["structure_score"] = self._calculate_structure_score(file_path)
276
- metrics["structure"] = float(scores["structure_score"])
277
-
278
- # DevEx Score (0-10, higher is better) - 7-category §3.2
279
- scores["devex_score"] = self._calculate_devex_score(file_path)
280
- metrics["devex"] = float(scores["devex_score"])
281
-
282
- class _Weights(Protocol):
283
- complexity: float
284
- security: float
285
- maintainability: float
286
- test_coverage: float
287
- performance: float
288
- structure: float
289
- devex: float
290
-
291
- # Overall Score (weighted average, 7-category)
292
- if self.weights is not None:
293
- w: _Weights = self.weights
294
- else:
295
- class DefaultWeights:
296
- complexity = 0.18
297
- security = 0.27
298
- maintainability = 0.24
299
- test_coverage = 0.13
300
- performance = 0.08
301
- structure = 0.05
302
- devex = 0.05
303
-
304
- w = DefaultWeights()
305
-
306
- scores["overall_score"] = (
307
- (10 - scores["complexity_score"]) * w.complexity
308
- + scores["security_score"] * w.security
309
- + scores["maintainability_score"] * w.maintainability
310
- + scores["test_coverage_score"] * w.test_coverage
311
- + scores["performance_score"] * w.performance
312
- + scores["structure_score"] * w.structure
313
- + scores["devex_score"] * w.devex
314
- ) * 10 # Scale from 0-10 weighted sum to 0-100
315
-
316
- # Phase 3.3: Validate all scores before returning
317
- from .score_validator import ScoreValidator
318
- from ...core.language_detector import Language
319
-
320
- validator = ScoreValidator()
321
- validation_results = validator.validate_all_scores(
322
- scores, language=Language.PYTHON, context=None
323
- )
324
-
325
- # Update scores with validated/clamped values and add explanations
326
- validated_scores = {}
327
- score_explanations = {}
328
- for category, result in validation_results.items():
329
- if result.valid and result.calibrated_score is not None:
330
- validated_scores[category] = result.calibrated_score
331
- if result.explanation:
332
- score_explanations[category] = {
333
- "explanation": result.explanation,
334
- "suggestions": result.suggestions,
335
- }
336
- else:
337
- validated_scores[category] = scores.get(category, 0.0)
338
-
339
- # Add explanations to result if any
340
- if score_explanations:
341
- validated_scores["_explanations"] = score_explanations
342
-
343
- # Merge: keep all scores (incl. structure_score, devex_score), overlay validated
344
- merged = {**scores, **validated_scores}
345
- return merged
346
-
347
- def _calculate_complexity(self, code: str) -> float:
348
- """Calculate cyclomatic complexity (0-10 scale)"""
349
- # Validate input
350
- validate_code_input(code, method_name="_calculate_complexity")
351
-
352
- if not self.has_radon:
353
- return 5.0 # Default neutral score
354
-
355
- try:
356
- tree = ast.parse(code)
357
- complexities = cc_visit(tree)
358
-
359
- if not complexities:
360
- return 1.0
361
-
362
- # Get max complexity
363
- max_complexity = max(cc.complexity for cc in complexities)
364
-
365
- # Scale to 0-10 using constants
366
- return min(
367
- max_complexity / ComplexityConstants.SCALING_FACTOR,
368
- ComplexityConstants.MAX_SCORE
369
- )
370
- except SyntaxError:
371
- return 10.0 # Syntax errors = max complexity
372
-
373
- def _calculate_security(self, file_path: Path | None, code: str) -> float:
374
- """Calculate security score (0-10 scale, higher is better)"""
375
- # Validate inputs
376
- validate_code_input(code, method_name="_calculate_security")
377
- if file_path is not None and not isinstance(file_path, Path):
378
- raise ValueError(f"_calculate_security: file_path must be Path or None, got {type(file_path).__name__}")
379
-
380
- if not self.has_bandit:
381
- # Basic heuristic check
382
- insecure_patterns = [
383
- "eval(",
384
- "exec(",
385
- "__import__",
386
- "pickle.loads",
387
- "subprocess.call",
388
- "os.system",
389
- ]
390
- issues = sum(1 for pattern in insecure_patterns if pattern in code)
391
- return max(
392
- 0.0,
393
- SecurityConstants.MAX_SCORE - (issues * SecurityConstants.INSECURE_PATTERN_PENALTY)
394
- )
395
-
396
- try:
397
- # Use bandit for proper security analysis
398
- # BanditManager expects a BanditConfig, not a dict. Passing a dict can raise ValueError,
399
- # which would silently degrade scoring to a neutral 5.0.
400
- b_conf = bandit_config.BanditConfig()
401
- b_mgr = manager.BanditManager(
402
- config=b_conf,
403
- agg_type="file",
404
- debug=False,
405
- verbose=False,
406
- quiet=True,
407
- profile=None,
408
- ignore_nosec=False,
409
- )
410
- b_mgr.discover_files([str(file_path)], False)
411
- b_mgr.run_tests()
412
-
413
- # Count high/medium severity issues
414
- issues = b_mgr.get_issue_list()
415
- high_severity = sum(1 for i in issues if i.severity == bandit.HIGH)
416
- medium_severity = sum(1 for i in issues if i.severity == bandit.MEDIUM)
417
-
418
- # Score: 10 - (high*3 + medium*1)
419
- score = 10.0 - (high_severity * 3.0 + medium_severity * 1.0)
420
- return max(0.0, score)
421
- except (FileNotFoundError, PermissionError, ValueError) as e:
422
- # Specific exceptions for file/system errors
423
- logger.warning(f"Security scoring failed for {file_path}: {e}")
424
- return 5.0 # Default neutral on error
425
- except Exception as e:
426
- # Catch-all for unexpected errors (should be rare)
427
- logger.warning(f"Unexpected error during security scoring for {file_path}: {e}", exc_info=True)
428
- return 5.0 # Default neutral on error
429
-
430
- def _calculate_maintainability(self, code: str) -> float:
431
- """
432
- Calculate maintainability index (0-10 scale, higher is better).
433
-
434
- Phase 3.1: Enhanced with context-aware scoring using MaintainabilityScorer.
435
- Phase 2 (P0): Maintainability issues are captured separately via get_maintainability_issues().
436
- """
437
- from .maintainability_scorer import MaintainabilityScorer
438
- from ...core.language_detector import Language
439
-
440
- # Use context-aware maintainability scorer
441
- scorer = MaintainabilityScorer()
442
- return scorer.calculate(code, Language.PYTHON, file_path=None, context=None)
443
-
444
- def get_maintainability_issues(
445
- self, code: str, file_path: Path | None = None
446
- ) -> list[dict[str, Any]]:
447
- """
448
- Get specific maintainability issues (Phase 2 - P0).
449
-
450
- Returns list of issues with details like missing docstrings, long functions, etc.
451
-
452
- Args:
453
- code: Source code content
454
- file_path: Optional path to the file
455
-
456
- Returns:
457
- List of maintainability issues with details
458
- """
459
- from .maintainability_scorer import MaintainabilityScorer
460
- from ...core.language_detector import Language
461
-
462
- scorer = MaintainabilityScorer()
463
- return scorer.get_issues(code, Language.PYTHON, file_path=file_path, context=None)
464
-
465
- def _calculate_test_coverage(self, file_path: Path) -> float:
466
- """
467
- Calculate test coverage score (0-10 scale, higher is better).
468
-
469
- Attempts to read coverage data from:
470
- 1. coverage.xml file in project root or .coverage file
471
- 2. Falls back to heuristic if no coverage data available
472
-
473
- Args:
474
- file_path: Path to the file being scored
475
-
476
- Returns:
477
- Coverage score (0-10 scale)
478
- """
479
- if not self.has_coverage:
480
- # No coverage tool available, use heuristic
481
- return self._coverage_heuristic(file_path)
482
-
483
- try:
484
- # Try to find and parse coverage data
485
- project_root = self._find_project_root(file_path)
486
- if project_root is None:
487
- return 5.0 # Neutral if can't find project root
488
-
489
- # Look for coverage.xml first (pytest-cov output)
490
- coverage_xml = project_root / "coverage.xml"
491
- if coverage_xml.exists():
492
- return self._parse_coverage_xml(coverage_xml, file_path)
493
-
494
- # Look for .coverage database file
495
- coverage_db = project_root / ".coverage"
496
- if coverage_db.exists():
497
- return self._parse_coverage_db(coverage_db, file_path)
498
-
499
- # No coverage data found, use heuristic
500
- return self._coverage_heuristic(file_path)
501
-
502
- except Exception:
503
- # Fallback to heuristic on any error
504
- return self._coverage_heuristic(file_path)
505
-
506
- def _parse_coverage_xml(self, coverage_xml: Path, file_path: Path) -> float:
507
- """Parse coverage.xml and return coverage percentage for file_path"""
508
- try:
509
- # coverage.xml is locally generated, but use defusedxml to reduce XML attack risk.
510
- from defusedxml import ElementTree as ET
511
-
512
- tree = ET.parse(coverage_xml)
513
- root = tree.getroot()
514
-
515
- # Get relative path from project root
516
- project_root = coverage_xml.parent
517
- try:
518
- rel_path = file_path.relative_to(project_root)
519
- file_path_str = str(rel_path).replace("\\", "/")
520
- except ValueError:
521
- # File not in project root
522
- return 5.0
523
-
524
- # Find coverage for this file
525
- for package in root.findall(".//package"):
526
- for class_elem in package.findall(".//class"):
527
- file_name = class_elem.get("filename", "")
528
- if file_name == file_path_str or file_path.name in file_name:
529
- # Get line-rate (coverage percentage)
530
- line_rate = float(class_elem.get("line-rate", "0.0"))
531
- # Convert 0-1 scale to 0-10 scale
532
- return line_rate * 10.0
533
-
534
- # File not found in coverage report
535
- return 0.0
536
- except Exception:
537
- return 5.0 # Default on error
538
-
539
- def _parse_coverage_db(self, coverage_db: Path, file_path: Path) -> float:
540
- """Parse .coverage database and return coverage percentage"""
541
- try:
542
- cov = Coverage()
543
- cov.load()
544
-
545
- # Get coverage data
546
- data = cov.get_data()
547
-
548
- # Try to find file in coverage data
549
- try:
550
- rel_path = file_path.relative_to(coverage_db.parent)
551
- file_path_str = str(rel_path).replace("\\", "/")
552
- except ValueError:
553
- return 5.0
554
-
555
- # Get coverage for this file
556
- if file_path_str in data.measured_files():
557
- # Calculate coverage percentage
558
- lines = data.lines(file_path_str)
559
- if not lines:
560
- return 0.0
561
-
562
- # Count covered vs total lines (simplified)
563
- # In practice, we'd need to check which lines are executable
564
- # For now, return neutral score
565
- return 5.0
566
-
567
- return 0.0 # File not covered
568
- except Exception:
569
- return 5.0
570
-
571
- def _coverage_heuristic(self, file_path: Path) -> float:
572
- """
573
- Heuristic-based coverage estimate.
574
-
575
- Phase 1 (P0): Fixed to return 0.0 when no test files exist.
576
-
577
- Checks for:
578
- - Test file existence (actual test files, not just directories)
579
- - Test directory structure
580
- - Test naming patterns
581
-
582
- Returns:
583
- - 0.0 if no test files exist (no tests written yet)
584
- - 5.0 if test files exist but no coverage data (tests exist but not run)
585
- - 10.0 if both test files and coverage data exist (not used here, handled by caller)
586
- """
587
- project_root = self._find_project_root(file_path)
588
- if project_root is None:
589
- return 0.0 # No project root = assume no tests (Phase 1 fix)
590
-
591
- # Look for test files with comprehensive patterns
592
- test_dirs = ["tests", "test", "tests/unit", "tests/integration", "tests/test", "test/test"]
593
- test_patterns = [
594
- f"test_{file_path.stem}.py",
595
- f"{file_path.stem}_test.py",
596
- f"test_{file_path.name}",
597
- # Also check for module-style test files
598
- f"test_{file_path.stem.replace('_', '')}.py",
599
- f"test_{file_path.stem.replace('-', '_')}.py",
600
- ]
601
-
602
- # Also check if the file itself is a test file
603
- if file_path.name.startswith("test_") or file_path.name.endswith("_test.py"):
604
- # File is a test file, assume it has coverage if it exists
605
- return 5.0 # Tests exist but no coverage data available
606
-
607
- # Check if any test files actually exist
608
- test_file_found = False
609
- for test_dir in test_dirs:
610
- test_dir_path = project_root / test_dir
611
- if test_dir_path.exists() and test_dir_path.is_dir():
612
- for pattern in test_patterns:
613
- test_file = test_dir_path / pattern
614
- if test_file.exists() and test_file.is_file():
615
- test_file_found = True
616
- break
617
- if test_file_found:
618
- break
619
-
620
- # Phase 1 fix: Return 0.0 if no test files exist (no tests written yet)
621
- if not test_file_found:
622
- return 0.0
623
-
624
- # Test files exist but not run yet
625
- return 5.0
626
-
627
- def _find_project_root(self, file_path: Path) -> Path | None:
628
- """Delegate to BaseScorer. Override for CodeScorer-specific markers if needed."""
629
- return BaseScorer._find_project_root(file_path)
630
-
631
- def get_performance_issues(
632
- self, code: str, file_path: Path | None = None
633
- ) -> list[dict[str, Any]]:
634
- """
635
- Get specific performance issues with line numbers (Phase 4 - P1).
636
-
637
- Returns list of performance bottlenecks with details like nested loops, expensive operations, etc.
638
-
639
- Args:
640
- code: Source code content
641
- file_path: Optional path to the file
642
-
643
- Returns:
644
- List of performance issues with line numbers
645
- """
646
- from .issue_tracking import PerformanceIssue
647
- import ast
648
-
649
- issues: list[PerformanceIssue] = []
650
- code_lines = code.splitlines()
651
-
652
- try:
653
- tree = ast.parse(code)
654
- except SyntaxError:
655
- return [PerformanceIssue(
656
- issue_type="syntax_error",
657
- message="File contains syntax errors - cannot analyze performance",
658
- severity="high"
659
- ).__dict__]
660
-
661
- # Analyze functions for performance issues
662
- for node in ast.walk(tree):
663
- if isinstance(node, ast.FunctionDef):
664
- # Check for nested loops
665
- for child in ast.walk(node):
666
- if isinstance(child, ast.For):
667
- # Check if this loop contains another loop
668
- for nested_child in ast.walk(child):
669
- if isinstance(nested_child, ast.For) and nested_child != child:
670
- # Found nested loop
671
- issues.append(PerformanceIssue(
672
- issue_type="nested_loops",
673
- message=f"Nested for loops detected in function '{node.name}' - potential O(n²) complexity",
674
- line_number=child.lineno,
675
- severity="high",
676
- operation_type="loop",
677
- context=f"Nested in function '{node.name}'",
678
- suggestion="Consider using itertools.product() or list comprehensions to flatten nested loops"
679
- ))
680
-
681
- # Check for expensive operations in loops
682
- if isinstance(child, ast.For):
683
- for loop_child in ast.walk(child):
684
- if isinstance(loop_child, ast.Call):
685
- # Check for expensive function calls in loops
686
- if isinstance(loop_child.func, ast.Name):
687
- func_name = loop_child.func.id
688
- expensive_operations = ["time.fromisoformat", "datetime.fromisoformat", "re.compile", "json.loads"]
689
- if any(exp_op in func_name for exp_op in expensive_operations):
690
- issues.append(PerformanceIssue(
691
- issue_type="expensive_operation_in_loop",
692
- message=f"Expensive operation '{func_name}' called in loop at line {loop_child.lineno} - parse once before loop",
693
- line_number=loop_child.lineno,
694
- severity="medium",
695
- operation_type=func_name,
696
- context=f"In loop at line {child.lineno}",
697
- suggestion=f"Move '{func_name}' call outside the loop and cache the result"
698
- ))
699
-
700
- # Check for list comprehensions with function calls
701
- if isinstance(node, ast.ListComp):
702
- func_calls_in_comp = sum(1 for n in ast.walk(node) if isinstance(n, ast.Call))
703
- if func_calls_in_comp > 5:
704
- issues.append(PerformanceIssue(
705
- issue_type="expensive_comprehension",
706
- message=f"List comprehension at line {node.lineno} contains {func_calls_in_comp} function calls - consider using generator or loop",
707
- line_number=node.lineno,
708
- severity="medium",
709
- operation_type="comprehension",
710
- suggestion="Consider using a generator expression or a loop for better performance"
711
- ))
712
-
713
- # Convert to dict format
714
- return [
715
- {
716
- "issue_type": issue.issue_type,
717
- "message": issue.message,
718
- "line_number": issue.line_number,
719
- "severity": issue.severity,
720
- "suggestion": issue.suggestion,
721
- "operation_type": issue.operation_type,
722
- "context": issue.context,
723
- }
724
- for issue in issues
725
- ]
726
-
727
- def _calculate_performance(self, code: str) -> float:
728
- """
729
- Calculate performance score using static analysis (0-10 scale, higher is better).
730
-
731
- Checks for:
732
- - Function size (number of lines)
733
- - Nesting depth
734
- - Inefficient patterns (N+1 queries, nested loops, etc.)
735
- - Large list/dict comprehensions
736
- """
737
- try:
738
- tree = ast.parse(code)
739
- issues = []
740
-
741
- # Analyze functions
742
- for node in ast.walk(tree):
743
- if isinstance(node, ast.FunctionDef):
744
- # Check function size
745
- # Use end_lineno if available (Python 3.8+), otherwise estimate
746
- if hasattr(node, "end_lineno") and node.end_lineno is not None:
747
- func_lines = node.end_lineno - node.lineno
748
- else:
749
- # Estimate: count lines in function body
750
- func_lines = (
751
- len(code.split("\n")[node.lineno - 1 : node.lineno + 49])
752
- if len(code.split("\n")) > node.lineno
753
- else 50
754
- )
755
-
756
- if func_lines > 50:
757
- issues.append("large_function") # > 50 lines
758
- if func_lines > 100:
759
- issues.append("very_large_function") # > 100 lines
760
-
761
- # Check nesting depth
762
- max_depth = self._get_max_nesting_depth(node)
763
- if max_depth > 4:
764
- issues.append("deep_nesting") # > 4 levels
765
- if max_depth > 6:
766
- issues.append("very_deep_nesting") # > 6 levels
767
-
768
- # Check for nested loops (potential N^2 complexity)
769
- if isinstance(node, ast.For):
770
- for child in ast.walk(node):
771
- if isinstance(child, ast.For) and child != node:
772
- issues.append("nested_loops")
773
- break
774
-
775
- # Check for list comprehensions with function calls
776
- if isinstance(node, ast.ListComp):
777
- # Count function calls in comprehension
778
- func_calls = sum(
779
- 1 for n in ast.walk(node) if isinstance(n, ast.Call)
780
- )
781
- if func_calls > 5:
782
- issues.append("expensive_comprehension")
783
-
784
- # Calculate score based on issues
785
- # Start with 10, deduct points for issues
786
- score = 10.0
787
- penalty_map = {
788
- "large_function": 0.5,
789
- "very_large_function": 1.5,
790
- "deep_nesting": 1.0,
791
- "very_deep_nesting": 2.0,
792
- "nested_loops": 1.5,
793
- "expensive_comprehension": 0.5,
794
- }
795
-
796
- seen_issues = set()
797
- for issue in issues:
798
- if issue not in seen_issues:
799
- score -= penalty_map.get(issue, 0.5)
800
- seen_issues.add(issue)
801
-
802
- return max(0.0, min(10.0, score))
803
-
804
- except SyntaxError:
805
- return 0.0 # Syntax errors = worst performance score
806
- except Exception:
807
- return 5.0 # Default on error
808
-
809
- def _get_max_nesting_depth(self, node: ast.AST, current_depth: int = 0) -> int:
810
- """Calculate maximum nesting depth in an AST node"""
811
- max_depth = current_depth
812
-
813
- for child in ast.iter_child_nodes(node):
814
- # Count nesting for control structures
815
- if isinstance(child, (ast.If, ast.For, ast.While, ast.Try, ast.With)):
816
- child_depth = self._get_max_nesting_depth(child, current_depth + 1)
817
- max_depth = max(max_depth, child_depth)
818
- else:
819
- child_depth = self._get_max_nesting_depth(child, current_depth)
820
- max_depth = max(max_depth, child_depth)
821
-
822
- return max_depth
823
-
824
- def _calculate_linting_score(self, file_path: Path) -> float:
825
- """
826
- Calculate linting score using Ruff (0-10 scale, higher is better).
827
-
828
- Phase 6: Modern Quality Analysis - Ruff Integration
829
-
830
- Returns:
831
- Linting score (0-10), where 10 = no issues, 0 = many issues
832
- """
833
- if not self.has_ruff:
834
- return 5.0 # Neutral score if Ruff not available
835
-
836
- # Only check Python files
837
- if file_path.suffix != ".py":
838
- return 10.0 # Perfect score for non-Python files (can't lint)
839
-
840
- try:
841
- # Run ruff check with JSON output
842
- result = subprocess.run( # nosec B603
843
- [
844
- sys.executable,
845
- "-m",
846
- "ruff",
847
- "check",
848
- "--output-format=json",
849
- str(file_path),
850
- ],
851
- capture_output=True,
852
- text=True,
853
- encoding="utf-8",
854
- errors="replace",
855
- timeout=30, # 30 second timeout
856
- cwd=file_path.parent if file_path.parent.exists() else None,
857
- )
858
-
859
- # Parse JSON output
860
- if result.returncode == 0 and not result.stdout.strip():
861
- # No issues found
862
- return 10.0
863
-
864
- try:
865
- # Ruff JSON format: list of diagnostic objects
866
- diagnostics = (
867
- json_lib.loads(result.stdout) if result.stdout.strip() else []
868
- )
869
-
870
- if not diagnostics:
871
- return 10.0
872
-
873
- # Count issues by severity
874
- # Ruff severity levels: E (Error), W (Warning), F (Fatal), I (Info)
875
- error_count = sum(
876
- 1
877
- for d in diagnostics
878
- if d.get("code", {}).get("name", "").startswith("E")
879
- )
880
- warning_count = sum(
881
- 1
882
- for d in diagnostics
883
- if d.get("code", {}).get("name", "").startswith("W")
884
- )
885
- fatal_count = sum(
886
- 1
887
- for d in diagnostics
888
- if d.get("code", {}).get("name", "").startswith("F")
889
- )
890
-
891
- # Calculate score: Start at 10, deduct points
892
- # Errors (E): -2 points each
893
- # Fatal (F): -3 points each
894
- # Warnings (W): -0.5 points each
895
- score = 10.0
896
- score -= error_count * 2.0
897
- score -= fatal_count * 3.0
898
- score -= warning_count * 0.5
899
-
900
- return max(0.0, min(10.0, score))
901
-
902
- except json_lib.JSONDecodeError:
903
- # If JSON parsing fails, check stderr for errors
904
- if result.stderr:
905
- return 5.0 # Neutral on parsing error
906
- return 10.0 # No output = no issues
907
-
908
- except subprocess.TimeoutExpired:
909
- return 5.0 # Neutral on timeout
910
- except FileNotFoundError:
911
- # Ruff not found in PATH
912
- return 5.0
913
- except Exception:
914
- # Any other error
915
- return 5.0
916
-
917
- def get_ruff_issues(self, file_path: Path) -> list[dict[str, Any]]:
918
- """
919
- Get detailed Ruff linting issues for a file.
920
-
921
- Phase 6: Modern Quality Analysis - Ruff Integration
922
-
923
- Returns:
924
- List of diagnostic dictionaries with code, message, location, etc.
925
- """
926
- if not self.has_ruff or file_path.suffix != ".py":
927
- return []
928
-
929
- try:
930
- result = subprocess.run( # nosec B603
931
- [
932
- sys.executable,
933
- "-m",
934
- "ruff",
935
- "check",
936
- "--output-format=json",
937
- str(file_path),
938
- ],
939
- capture_output=True,
940
- text=True,
941
- encoding="utf-8",
942
- errors="replace",
943
- timeout=30,
944
- cwd=file_path.parent if file_path.parent.exists() else None,
945
- )
946
-
947
- if result.returncode == 0 and not result.stdout.strip():
948
- return []
949
-
950
- try:
951
- diagnostics = (
952
- json_lib.loads(result.stdout) if result.stdout.strip() else []
953
- )
954
- return diagnostics
955
- except json_lib.JSONDecodeError:
956
- return []
957
-
958
- except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
959
- return []
960
-
961
- def _calculate_type_checking_score(self, file_path: Path) -> float:
962
- """
963
- Calculate type checking score using mypy (0-10 scale, higher is better).
964
-
965
- Phase 6.2: Modern Quality Analysis - mypy Integration
966
- Phase 5 (P1): Fixed to actually run mypy and return real scores (not static 5.0).
967
- ENH-002-S2: Prefer ScopedMypyExecutor (--follow-imports=skip, <10s) with fallback to full mypy.
968
- """
969
- if not self.has_mypy:
970
- logger.debug("mypy not available - returning neutral score")
971
- return 5.0 # Neutral score if mypy not available
972
-
973
- # Only check Python files
974
- if file_path.suffix != ".py":
975
- return 10.0 # Perfect score for non-Python files (can't type check)
976
-
977
- # ENH-002-S2: Try scoped mypy first (faster)
978
- try:
979
- from .tools.scoped_mypy import ScopedMypyExecutor
980
- executor = ScopedMypyExecutor()
981
- result = executor.run_scoped_sync(file_path, timeout=10)
982
- if result.files_checked == 1 or result.issues:
983
- error_count = len(result.issues)
984
- if error_count == 0:
985
- return 10.0
986
- score = 10.0 - (error_count * 0.5)
987
- logger.debug(
988
- "mypy (scoped) found %s errors for %s, score: %s/10",
989
- error_count, file_path, score,
990
- )
991
- return max(0.0, min(10.0, score))
992
- except Exception as e:
993
- logger.debug("scoped mypy not used, falling back to full mypy: %s", e)
994
-
995
- try:
996
- result = subprocess.run( # nosec B603
997
- [
998
- sys.executable,
999
- "-m",
1000
- "mypy",
1001
- "--show-error-codes",
1002
- "--no-error-summary",
1003
- "--no-color-output",
1004
- "--no-incremental",
1005
- str(file_path),
1006
- ],
1007
- capture_output=True,
1008
- text=True,
1009
- encoding="utf-8",
1010
- errors="replace",
1011
- timeout=30,
1012
- cwd=file_path.parent if file_path.parent.exists() else None,
1013
- )
1014
- if result.returncode == 0:
1015
- logger.debug("mypy found no errors for %s", file_path)
1016
- return 10.0
1017
- output = result.stdout.strip()
1018
- if not output:
1019
- logger.debug("mypy returned non-zero but no output for %s", file_path)
1020
- return 10.0
1021
- error_lines = [
1022
- line
1023
- for line in output.split("\n")
1024
- if "error:" in line.lower() and file_path.name in line
1025
- ]
1026
- error_count = len(error_lines)
1027
- if error_count == 0:
1028
- logger.debug("mypy returned non-zero but no parseable errors for %s", file_path)
1029
- return 10.0
1030
- score = 10.0 - (error_count * 0.5)
1031
- logger.debug("mypy found %s errors for %s, score: %s/10", error_count, file_path, score)
1032
- return max(0.0, min(10.0, score))
1033
- except subprocess.TimeoutExpired:
1034
- logger.warning("mypy timed out for %s", file_path)
1035
- return 5.0
1036
- except FileNotFoundError:
1037
- logger.debug("mypy not found in PATH for %s", file_path)
1038
- self.has_mypy = False
1039
- return 5.0
1040
- except Exception as e:
1041
- logger.warning("mypy failed for %s: %s", file_path, e, exc_info=True)
1042
- return 5.0
1043
-
1044
- def get_mypy_errors(self, file_path: Path) -> list[dict[str, Any]]:
1045
- """
1046
- Get detailed mypy type checking errors for a file.
1047
-
1048
- Phase 6.2: Modern Quality Analysis - mypy Integration
1049
- ENH-002-S2: Prefer ScopedMypyExecutor with fallback to full mypy.
1050
- """
1051
- if not self.has_mypy or file_path.suffix != ".py":
1052
- return []
1053
-
1054
- try:
1055
- from .tools.scoped_mypy import ScopedMypyExecutor
1056
- executor = ScopedMypyExecutor()
1057
- result = executor.run_scoped_sync(file_path, timeout=10)
1058
- if result.files_checked == 1 or result.issues:
1059
- return [
1060
- {
1061
- "filename": str(i.file_path),
1062
- "line": i.line,
1063
- "message": i.message,
1064
- "error_code": i.error_code,
1065
- "severity": i.severity,
1066
- }
1067
- for i in result.issues
1068
- ]
1069
- except Exception:
1070
- pass
1071
-
1072
- try:
1073
- result = subprocess.run( # nosec B603
1074
- [
1075
- sys.executable,
1076
- "-m",
1077
- "mypy",
1078
- "--show-error-codes",
1079
- "--no-error-summary",
1080
- "--no-incremental",
1081
- str(file_path),
1082
- ],
1083
- capture_output=True,
1084
- text=True,
1085
- encoding="utf-8",
1086
- errors="replace",
1087
- timeout=30,
1088
- cwd=file_path.parent if file_path.parent.exists() else None,
1089
- )
1090
- if result.returncode == 0 or not result.stdout.strip():
1091
- return []
1092
- errors = []
1093
- for line in result.stdout.strip().split("\n"):
1094
- if "error:" not in line.lower():
1095
- continue
1096
- parts = line.split(":", 3)
1097
- if len(parts) >= 4:
1098
- filename = parts[0]
1099
- try:
1100
- line_num = int(parts[1])
1101
- except ValueError:
1102
- continue
1103
- error_msg = parts[3].strip()
1104
- error_code = None
1105
- if "[" in error_msg and "]" in error_msg:
1106
- start = error_msg.rfind("[")
1107
- end = error_msg.rfind("]")
1108
- if start < end:
1109
- error_code = error_msg[start + 1 : end]
1110
- error_msg = error_msg[:start].strip()
1111
- errors.append({
1112
- "filename": filename,
1113
- "line": line_num,
1114
- "message": error_msg,
1115
- "error_code": error_code,
1116
- "severity": "error",
1117
- })
1118
- return errors
1119
- except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
1120
- return []
1121
-
1122
- def _format_ruff_issues(self, diagnostics: list[dict[str, Any]]) -> list[dict[str, Any]]:
1123
- """
1124
- Format raw ruff diagnostics into a cleaner structure for output.
1125
-
1126
- P1 Improvement: Include actual lint errors in score output.
1127
-
1128
- Args:
1129
- diagnostics: Raw ruff JSON diagnostics
1130
-
1131
- Returns:
1132
- List of formatted issues with line, code, message, severity
1133
- """
1134
- formatted = []
1135
- for diag in diagnostics:
1136
- # Extract code info (ruff format: {"code": {"name": "F401", ...}})
1137
- code_info = diag.get("code", {})
1138
- if isinstance(code_info, dict):
1139
- code = code_info.get("name", "")
1140
- else:
1141
- code = str(code_info)
1142
-
1143
- # Determine severity from code prefix
1144
- severity = "warning"
1145
- if code.startswith("E") or code.startswith("F"):
1146
- severity = "error"
1147
- elif code.startswith("W"):
1148
- severity = "warning"
1149
- elif code.startswith("I"):
1150
- severity = "info"
1151
-
1152
- # Get location info
1153
- location = diag.get("location", {})
1154
- line = location.get("row", 0) if isinstance(location, dict) else 0
1155
- column = location.get("column", 0) if isinstance(location, dict) else 0
1156
-
1157
- formatted.append({
1158
- "code": code,
1159
- "message": diag.get("message", ""),
1160
- "line": line,
1161
- "column": column,
1162
- "severity": severity,
1163
- })
1164
-
1165
- # Sort by line number
1166
- formatted.sort(key=lambda x: (x.get("line", 0), x.get("column", 0)))
1167
-
1168
- return formatted
1169
-
1170
- def _group_ruff_issues_by_code(self, issues: list[dict[str, Any]]) -> dict[str, Any]:
1171
- """
1172
- Group ruff issues by rule code for cleaner, more actionable reports.
1173
-
1174
- ENH-002 Story #18: Ruff Output Grouping
1175
-
1176
- Args:
1177
- issues: List of ruff diagnostic dictionaries
1178
-
1179
- Returns:
1180
- Dictionary with grouped issues:
1181
- {
1182
- "total_count": int,
1183
- "groups": [
1184
- {
1185
- "code": "UP006",
1186
- "count": 17,
1187
- "description": "Use dict/list instead of Dict/List",
1188
- "severity": "info",
1189
- "issues": [...]
1190
- },
1191
- ...
1192
- ],
1193
- "summary": "UP006 (17), UP045 (10), UP007 (2), F401 (1)"
1194
- }
1195
- """
1196
- if not issues:
1197
- return {
1198
- "total_count": 0,
1199
- "groups": [],
1200
- "summary": "No issues found"
1201
- }
1202
-
1203
- # Group issues by code
1204
- groups_dict: dict[str, list[dict[str, Any]]] = {}
1205
- for issue in issues:
1206
- code_info = issue.get("code", {})
1207
- if isinstance(code_info, dict):
1208
- code = code_info.get("name", "UNKNOWN")
1209
- else:
1210
- code = str(code_info) if code_info else "UNKNOWN"
1211
-
1212
- if code not in groups_dict:
1213
- groups_dict[code] = []
1214
- groups_dict[code].append(issue)
1215
-
1216
- # Create grouped structure with metadata
1217
- groups = []
1218
- for code, code_issues in groups_dict.items():
1219
- # Get first message as description (they're usually the same for same code)
1220
- description = code_issues[0].get("message", "") if code_issues else ""
1221
-
1222
- # Determine severity from code
1223
- severity = "info"
1224
- if code.startswith("E") or code.startswith("F"):
1225
- severity = "error"
1226
- elif code.startswith("W"):
1227
- severity = "warning"
1228
-
1229
- groups.append({
1230
- "code": code,
1231
- "count": len(code_issues),
1232
- "description": description,
1233
- "severity": severity,
1234
- "issues": code_issues
1235
- })
1236
-
1237
- # Sort by count (descending) then by code
1238
- groups.sort(key=lambda x: (-x["count"], x["code"]))
1239
-
1240
- # Create summary string: "UP006 (17), UP045 (10), ..."
1241
- summary_parts = [f"{g['code']} ({g['count']})" for g in groups]
1242
- summary = ", ".join(summary_parts)
1243
-
1244
- return {
1245
- "total_count": len(issues),
1246
- "groups": groups,
1247
- "summary": summary
1248
- }
1249
-
1250
- def _calculate_duplication_score(self, file_path: Path) -> float:
1251
- """
1252
- Calculate duplication score using jscpd (0-10 scale, higher is better).
1253
-
1254
- Phase 6.4: Modern Quality Analysis - jscpd Integration
1255
-
1256
- Note: jscpd works on directories/files, so we analyze the parent directory
1257
- or file directly. For single file, we analyze just that file.
1258
-
1259
- Returns:
1260
- Duplication score (0-10), where 10 = no duplication, 0 = high duplication
1261
- Score formula: 10 - (duplication_pct / 10)
1262
- """
1263
- if not self.has_jscpd:
1264
- return 5.0 # Neutral score if jscpd not available
1265
-
1266
- # jscpd works best on directories or multiple files
1267
- # For single file analysis, we'll analyze the file directly
1268
- try:
1269
- # Determine target (file or directory)
1270
- target = str(file_path)
1271
- if file_path.is_dir():
1272
- target_dir = str(file_path)
1273
- else:
1274
- target_dir = str(file_path.parent)
1275
-
1276
- # Build jscpd command
1277
- # Use npx if jscpd not directly available
1278
- jscpd_path = shutil.which("jscpd")
1279
- if jscpd_path:
1280
- cmd = [jscpd_path]
1281
- else:
1282
- npx_path = shutil.which("npx")
1283
- if not npx_path:
1284
- return 5.0 # jscpd not available
1285
- cmd = [npx_path, "--yes", "jscpd"]
1286
-
1287
- # Add jscpd arguments
1288
- cmd.extend(
1289
- [
1290
- target,
1291
- "--format",
1292
- "json",
1293
- "--min-lines",
1294
- str(self.min_duplication_lines),
1295
- "--reporters",
1296
- "json",
1297
- "--output",
1298
- ".", # Output to current directory
1299
- ]
1300
- )
1301
-
1302
- # Run jscpd
1303
- result = subprocess.run( # nosec B603 - fixed args
1304
- wrap_windows_cmd_shim(cmd),
1305
- capture_output=True,
1306
- text=True,
1307
- encoding="utf-8",
1308
- errors="replace",
1309
- timeout=120, # 2 minute timeout (jscpd can be slow on large codebases)
1310
- cwd=target_dir if Path(target_dir).exists() else None,
1311
- )
1312
-
1313
- # jscpd outputs JSON to stdout when using --reporters json
1314
- # But it might also create a file, so check both
1315
- json_output = result.stdout.strip()
1316
-
1317
- # Try to parse JSON from stdout
1318
- try:
1319
- if json_output:
1320
- report_data = json_lib.loads(json_output)
1321
- else:
1322
- # Check for jscpd-report.json in output directory
1323
- output_file = Path(target_dir) / "jscpd-report.json"
1324
- if output_file.exists():
1325
- with open(output_file, encoding="utf-8") as f:
1326
- report_data = json_lib.load(f)
1327
- else:
1328
- # No duplication found (exit code 0 typically means no issues or success)
1329
- if result.returncode == 0:
1330
- return 10.0 # Perfect score (no duplication)
1331
- return 5.0 # Neutral on parse failure
1332
- except json_lib.JSONDecodeError:
1333
- # JSON parse error - might be text output
1334
- # Try to extract duplication percentage from text output
1335
- # Format: "Found X% duplicated lines out of Y total lines"
1336
- lines = result.stdout.split("\n") + result.stderr.split("\n")
1337
- for line in lines:
1338
- if "%" in line and "duplicate" in line.lower():
1339
- # Try to extract percentage
1340
- try:
1341
- pct_str = line.split("%")[0].split()[-1]
1342
- duplication_pct = float(pct_str)
1343
- score = 10.0 - (duplication_pct / 10.0)
1344
- return max(0.0, min(10.0, score))
1345
- except (ValueError, IndexError):
1346
- pass
1347
-
1348
- # If we can't parse, default behavior
1349
- if result.returncode == 0:
1350
- return 10.0 # No duplication found
1351
- return 5.0 # Neutral on parse failure
1352
-
1353
- # Extract duplication percentage from JSON report
1354
- # jscpd JSON structure: { "percentage": X.X, ... }
1355
- duplication_pct = report_data.get("percentage", 0.0)
1356
-
1357
- # Calculate score: 10 - (duplication_pct / 10)
1358
- # This means:
1359
- # - 0% duplication = 10.0 score
1360
- # - 3% duplication (threshold) = 9.7 score
1361
- # - 10% duplication = 9.0 score
1362
- # - 30% duplication = 7.0 score
1363
- # - 100% duplication = 0.0 score
1364
- score = 10.0 - (duplication_pct / 10.0)
1365
- return max(0.0, min(10.0, score))
1366
-
1367
- except subprocess.TimeoutExpired:
1368
- return 5.0 # Neutral on timeout
1369
- except FileNotFoundError:
1370
- # jscpd not found
1371
- return 5.0
1372
- except Exception:
1373
- # Any other error
1374
- return 5.0
1375
-
1376
- def get_duplication_report(self, file_path: Path) -> dict[str, Any]:
1377
- """
1378
- Get detailed duplication report from jscpd.
1379
-
1380
- Phase 6.4: Modern Quality Analysis - jscpd Integration
1381
-
1382
- Returns:
1383
- Dictionary with duplication report data including:
1384
- - percentage: Duplication percentage
1385
- - duplicates: List of duplicate code blocks
1386
- - files: File-level duplication stats
1387
- """
1388
- if not self.has_jscpd:
1389
- return {
1390
- "available": False,
1391
- "percentage": 0.0,
1392
- "duplicates": [],
1393
- "files": [],
1394
- }
1395
-
1396
- try:
1397
- # Determine target
1398
- if file_path.is_dir():
1399
- target_dir = str(file_path)
1400
- target = str(file_path)
1401
- else:
1402
- target_dir = str(file_path.parent)
1403
- target = str(file_path)
1404
-
1405
- # Build jscpd command
1406
- jscpd_path = shutil.which("jscpd")
1407
- if jscpd_path:
1408
- cmd = [jscpd_path]
1409
- else:
1410
- npx_path = shutil.which("npx")
1411
- if not npx_path:
1412
- return {
1413
- "available": False,
1414
- "percentage": 0.0,
1415
- "duplicates": [],
1416
- "files": [],
1417
- }
1418
- cmd = [npx_path, "--yes", "jscpd"]
1419
-
1420
- cmd.extend(
1421
- [
1422
- target,
1423
- "--format",
1424
- "json",
1425
- "--min-lines",
1426
- str(self.min_duplication_lines),
1427
- "--reporters",
1428
- "json",
1429
- ]
1430
- )
1431
-
1432
- # Run jscpd
1433
- result = subprocess.run( # nosec B603 - fixed args
1434
- wrap_windows_cmd_shim(cmd),
1435
- capture_output=True,
1436
- text=True,
1437
- encoding="utf-8",
1438
- errors="replace",
1439
- timeout=120,
1440
- cwd=target_dir if Path(target_dir).exists() else None,
1441
- )
1442
-
1443
- # Parse JSON output
1444
- json_output = result.stdout.strip()
1445
- try:
1446
- if json_output:
1447
- report_data = json_lib.loads(json_output)
1448
- else:
1449
- # Check for output file
1450
- output_file = Path(target_dir) / "jscpd-report.json"
1451
- if output_file.exists():
1452
- with open(output_file, encoding="utf-8") as f:
1453
- report_data = json_lib.load(f)
1454
- else:
1455
- return {
1456
- "available": True,
1457
- "percentage": 0.0,
1458
- "duplicates": [],
1459
- "files": [],
1460
- }
1461
- except json_lib.JSONDecodeError:
1462
- return {
1463
- "available": True,
1464
- "error": "Failed to parse jscpd output",
1465
- "percentage": 0.0,
1466
- "duplicates": [],
1467
- "files": [],
1468
- }
1469
-
1470
- # Extract relevant data from jscpd report
1471
- # jscpd JSON format varies, but typically has:
1472
- # - percentage: overall duplication percentage
1473
- # - duplicates: array of duplicate pairs
1474
- # - files: file-level statistics
1475
-
1476
- return {
1477
- "available": True,
1478
- "percentage": report_data.get("percentage", 0.0),
1479
- "duplicates": report_data.get("duplicates", []),
1480
- "files": (
1481
- report_data.get("statistics", {}).get("files", [])
1482
- if "statistics" in report_data
1483
- else []
1484
- ),
1485
- "total_lines": (
1486
- report_data.get("statistics", {}).get("total", {}).get("lines", 0)
1487
- if "statistics" in report_data
1488
- else 0
1489
- ),
1490
- "duplicated_lines": (
1491
- report_data.get("statistics", {})
1492
- .get("duplicated", {})
1493
- .get("lines", 0)
1494
- if "statistics" in report_data
1495
- else 0
1496
- ),
1497
- }
1498
-
1499
- except subprocess.TimeoutExpired:
1500
- return {
1501
- "available": True,
1502
- "error": "jscpd timeout",
1503
- "percentage": 0.0,
1504
- "duplicates": [],
1505
- "files": [],
1506
- }
1507
- except FileNotFoundError:
1508
- return {
1509
- "available": False,
1510
- "percentage": 0.0,
1511
- "duplicates": [],
1512
- "files": [],
1513
- }
1514
- except Exception as e:
1515
- return {
1516
- "available": True,
1517
- "error": str(e),
1518
- "percentage": 0.0,
1519
- "duplicates": [],
1520
- "files": [],
1521
- }
1522
-
1523
-
1524
- class ScorerFactory:
1525
- """
1526
- Factory to provide appropriate scorer based on language (Strategy Pattern).
1527
-
1528
- Phase 1.2: Language-Specific Scorers
1529
-
1530
- Now uses ScorerRegistry for extensible language support.
1531
- """
1532
-
1533
- @staticmethod
1534
- def get_scorer(language: Language, config: ProjectConfig | None = None) -> BaseScorer:
1535
- """
1536
- Get the appropriate scorer for a given language.
1537
-
1538
- Uses ScorerRegistry for extensible language support with fallback chains.
1539
-
1540
- Args:
1541
- language: Detected language enum
1542
- config: Optional project configuration
1543
-
1544
- Returns:
1545
- BaseScorer instance appropriate for the language
1546
-
1547
- Raises:
1548
- ValueError: If no scorer is available for the language (even with fallbacks)
1549
- """
1550
- from .scorer_registry import ScorerRegistry
1551
-
1552
- try:
1553
- return ScorerRegistry.get_scorer(language, config)
1554
- except ValueError:
1555
- # If no scorer found, fall back to Python scorer as last resort
1556
- # This maintains backward compatibility but may not work well for non-Python code
1557
- # TODO: In the future, create a GenericScorer that uses metric strategies
1558
- if language != Language.PYTHON:
1559
- # Try Python scorer as absolute last resort
1560
- try:
1561
- return ScorerRegistry.get_scorer(Language.PYTHON, config)
1562
- except ValueError:
1563
- pass
1564
-
1565
- # If even Python scorer isn't available, raise the original error
1566
- raise
1
+ """
2
+ Code Scoring System - Calculates objective quality metrics
3
+ """
4
+
5
+ import ast
6
+ import json as json_lib
7
+ import logging
8
+ import shutil
9
+ import subprocess # nosec B404 - used with fixed args, no shell
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any, Protocol
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ from ...core.config import ProjectConfig, ScoringWeightsConfig
17
+ from ...core.language_detector import Language
18
+ from ...core.subprocess_utils import wrap_windows_cmd_shim
19
+ from .score_constants import ComplexityConstants, SecurityConstants
20
+ from .validation import validate_code_input
21
+
22
+ # Import analysis libraries
23
+ try:
24
+ from radon.complexity import cc_visit
25
+ from radon.metrics import mi_visit
26
+
27
+ HAS_RADON = True
28
+ except ImportError:
29
+ HAS_RADON = False
30
+
31
+ try:
32
+ import bandit
33
+ from bandit.core import config as bandit_config
34
+ from bandit.core import manager
35
+
36
+ HAS_BANDIT = True
37
+ except ImportError:
38
+ HAS_BANDIT = False
39
+
40
+ # Check if ruff is available in PATH or via python -m ruff
41
+ def _check_ruff_available() -> bool:
42
+ """Check if ruff is available via 'ruff' command or 'python -m ruff'"""
43
+ # Check for ruff command directly
44
+ if shutil.which("ruff"):
45
+ return True
46
+ # Check for python -m ruff
47
+ try:
48
+ result = subprocess.run( # nosec B603 - fixed args
49
+ [sys.executable, "-m", "ruff", "--version"],
50
+ capture_output=True,
51
+ timeout=5,
52
+ check=False,
53
+ )
54
+ return result.returncode == 0
55
+ except (subprocess.TimeoutExpired, FileNotFoundError):
56
+ return False
57
+
58
+
59
+ HAS_RUFF = _check_ruff_available()
60
+
61
+ # Check if mypy is available in PATH
62
+ HAS_MYPY = shutil.which("mypy") is not None
63
+
64
+
65
+ # Check if jscpd is available (via npm/npx)
66
+ def _check_jscpd_available() -> bool:
67
+ """Check if jscpd is available via jscpd command or npx jscpd"""
68
+ # Check for jscpd command directly
69
+ if shutil.which("jscpd"):
70
+ return True
71
+ # Check for npx (Node.js package runner)
72
+ npx_path = shutil.which("npx")
73
+ if npx_path:
74
+ try:
75
+ cmd = wrap_windows_cmd_shim([npx_path, "--yes", "jscpd", "--version"])
76
+ result = subprocess.run( # nosec B603 - fixed args
77
+ cmd,
78
+ capture_output=True,
79
+ timeout=5,
80
+ check=False,
81
+ )
82
+ return result.returncode == 0
83
+ except (subprocess.TimeoutExpired, FileNotFoundError):
84
+ return False
85
+ return False
86
+
87
+
88
+ HAS_JSCPD = _check_jscpd_available()
89
+
90
+ # Import coverage tools
91
+ try:
92
+ from coverage import Coverage
93
+
94
+ HAS_COVERAGE = True
95
+ except ImportError:
96
+ HAS_COVERAGE = False
97
+
98
+
99
+ class BaseScorer:
100
+ """
101
+ Base class for all scorers.
102
+ Defines the interface and shared helpers: _find_project_root, _calculate_structure_score, _calculate_devex_score (7-category §3.2).
103
+ """
104
+
105
+ def score_file(self, file_path: Path, code: str) -> dict[str, Any]:
106
+ """Score a file and return quality metrics. Subclasses must implement."""
107
+ raise NotImplementedError("Subclasses must implement score_file")
108
+
109
+ @staticmethod
110
+ def _find_project_root(file_path: Path) -> Path | None:
111
+ """Find project root by common markers (.git, pyproject.toml, package.json, .tapps-agents, etc.)."""
112
+ current = file_path.resolve().parent
113
+ markers = [".git", "pyproject.toml", "setup.py", "requirements.txt", ".tapps-agents", "package.json"]
114
+ for _ in range(10):
115
+ for m in markers:
116
+ if (current / m).exists():
117
+ return current
118
+ if current.parent == current:
119
+ break
120
+ current = current.parent
121
+ return None
122
+
123
+ @classmethod
124
+ def _calculate_structure_score(cls, file_path: Path) -> float:
125
+ """Structure score (0-10). Project layout, key files. 7-category §3.2."""
126
+ root = BaseScorer._find_project_root(file_path)
127
+ if root is None:
128
+ return 5.0
129
+ pts = 0.0
130
+ if (root / "pyproject.toml").exists() or (root / "package.json").exists():
131
+ pts += 2.5
132
+ if (root / "README").exists() or (root / "README.md").exists() or (root / "README.rst").exists():
133
+ pts += 2.0
134
+ if (root / "tests").exists() or (root / "test").exists():
135
+ pts += 2.0
136
+ if (root / ".tapps-agents").exists() or (root / ".git").exists():
137
+ pts += 1.0
138
+ if (root / "setup.py").exists() or (root / "requirements.txt").exists() or (root / "package-lock.json").exists():
139
+ pts += 1.5
140
+ return min(10.0, pts * 2.0)
141
+
142
+ @classmethod
143
+ def _calculate_devex_score(cls, file_path: Path) -> float:
144
+ """DevEx score (0-10). Docs, config, tooling. 7-category §3.2."""
145
+ root = BaseScorer._find_project_root(file_path)
146
+ if root is None:
147
+ return 5.0
148
+ pts = 0.0
149
+ if (root / "AGENTS.md").exists() or (root / "CLAUDE.md").exists():
150
+ pts += 3.0
151
+ if (root / "docs").exists() and (root / "docs").is_dir():
152
+ pts += 2.0
153
+ if (root / ".tapps-agents").exists() or (root / ".cursor").exists():
154
+ pts += 2.0
155
+ pyproject, pkg = root / "pyproject.toml", root / "package.json"
156
+ if pyproject.exists():
157
+ try:
158
+ t = pyproject.read_text(encoding="utf-8", errors="replace")
159
+ if "[tool.ruff]" in t or "[tool.mypy]" in t or "pytest" in t:
160
+ pts += 1.5
161
+ except Exception:
162
+ pass
163
+ if pkg.exists():
164
+ try:
165
+ import json as _j
166
+ d = _j.loads(pkg.read_text(encoding="utf-8", errors="replace"))
167
+ dev = (d.get("devDependencies") or {}) if isinstance(d, dict) else {}
168
+ if any(k in dev for k in ("eslint", "jest", "vitest", "mypy")):
169
+ pts += 1.5
170
+ except Exception:
171
+ pass
172
+ return min(10.0, pts * 2.0)
173
+
174
+
175
+ class CodeScorer(BaseScorer):
176
+ """Calculate code quality scores for Python files"""
177
+
178
+ def __init__(
179
+ self,
180
+ weights: ScoringWeightsConfig | None = None,
181
+ ruff_enabled: bool = True,
182
+ mypy_enabled: bool = True,
183
+ jscpd_enabled: bool = True,
184
+ duplication_threshold: float = 3.0,
185
+ min_duplication_lines: int = 5,
186
+ ):
187
+ self.has_radon = HAS_RADON
188
+ self.has_bandit = HAS_BANDIT
189
+ self.has_coverage = HAS_COVERAGE
190
+ self.has_ruff = HAS_RUFF and ruff_enabled
191
+ self.has_mypy = HAS_MYPY and mypy_enabled
192
+ self.has_jscpd = HAS_JSCPD and jscpd_enabled
193
+ self.duplication_threshold = duplication_threshold
194
+ self.min_duplication_lines = min_duplication_lines
195
+ self.weights = weights # Will use defaults if None
196
+
197
+ def score_file(self, file_path: Path, code: str) -> dict[str, Any]:
198
+ """
199
+ Calculate scores for a code file.
200
+
201
+ Returns:
202
+ {
203
+ "complexity_score": float (0-10),
204
+ "security_score": float (0-10),
205
+ "maintainability_score": float (0-10),
206
+ "overall_score": float (0-100),
207
+ "metrics": {...}
208
+ }
209
+ """
210
+ metrics: dict[str, float] = {}
211
+ scores: dict[str, Any] = {
212
+ "complexity_score": 0.0,
213
+ "security_score": 0.0,
214
+ "maintainability_score": 0.0,
215
+ "test_coverage_score": 0.0,
216
+ "performance_score": 0.0,
217
+ "structure_score": 0.0, # 7-category: project layout, key files (MCP_SYSTEMS_IMPROVEMENT_RECOMMENDATIONS §3.2)
218
+ "devex_score": 0.0, # 7-category: docs, config, tooling (MCP_SYSTEMS_IMPROVEMENT_RECOMMENDATIONS §3.2)
219
+ "linting_score": 0.0, # Phase 6.1: Ruff linting score
220
+ "type_checking_score": 0.0, # Phase 6.2: mypy type checking score
221
+ "duplication_score": 0.0, # Phase 6.4: jscpd duplication score
222
+ "metrics": metrics,
223
+ }
224
+
225
+ # Complexity Score (0-10, lower is better)
226
+ scores["complexity_score"] = self._calculate_complexity(code)
227
+ metrics["complexity"] = float(scores["complexity_score"])
228
+
229
+ # Security Score (0-10, higher is better)
230
+ scores["security_score"] = self._calculate_security(file_path, code)
231
+ metrics["security"] = float(scores["security_score"])
232
+
233
+ # Maintainability Score (0-10, higher is better)
234
+ scores["maintainability_score"] = self._calculate_maintainability(code)
235
+ metrics["maintainability"] = float(scores["maintainability_score"])
236
+
237
+ # Test Coverage Score (0-10, higher is better)
238
+ scores["test_coverage_score"] = self._calculate_test_coverage(file_path)
239
+ metrics["test_coverage"] = float(scores["test_coverage_score"])
240
+
241
+ # Performance Score (0-10, higher is better)
242
+ # Phase 3.2: Use context-aware performance scorer
243
+ from .performance_scorer import PerformanceScorer
244
+ from ...core.language_detector import Language
245
+
246
+ performance_scorer = PerformanceScorer()
247
+ scores["performance_score"] = performance_scorer.calculate(
248
+ code, Language.PYTHON, file_path, context=None
249
+ )
250
+ metrics["performance"] = float(scores["performance_score"])
251
+
252
+ # Linting Score (0-10, higher is better) - Phase 6.1
253
+ scores["linting_score"] = self._calculate_linting_score(file_path)
254
+ metrics["linting"] = float(scores["linting_score"])
255
+
256
+ # Get actual linting issues for transparency (P1 Improvement)
257
+ linting_issues = self.get_ruff_issues(file_path)
258
+ scores["linting_issues"] = self._format_ruff_issues(linting_issues)
259
+ scores["linting_issue_count"] = len(linting_issues)
260
+
261
+ # Type Checking Score (0-10, higher is better) - Phase 6.2
262
+ scores["type_checking_score"] = self._calculate_type_checking_score(file_path)
263
+ metrics["type_checking"] = float(scores["type_checking_score"])
264
+
265
+ # Get actual type checking issues for transparency (P1 Improvement)
266
+ type_issues = self.get_mypy_errors(file_path)
267
+ scores["type_issues"] = type_issues # Already formatted
268
+ scores["type_issue_count"] = len(type_issues)
269
+
270
+ # Duplication Score (0-10, higher is better) - Phase 6.4
271
+ scores["duplication_score"] = self._calculate_duplication_score(file_path)
272
+ metrics["duplication"] = float(scores["duplication_score"])
273
+
274
+ # Structure Score (0-10, higher is better) - 7-category §3.2
275
+ scores["structure_score"] = self._calculate_structure_score(file_path)
276
+ metrics["structure"] = float(scores["structure_score"])
277
+
278
+ # DevEx Score (0-10, higher is better) - 7-category §3.2
279
+ scores["devex_score"] = self._calculate_devex_score(file_path)
280
+ metrics["devex"] = float(scores["devex_score"])
281
+
282
+ class _Weights(Protocol):
283
+ complexity: float
284
+ security: float
285
+ maintainability: float
286
+ test_coverage: float
287
+ performance: float
288
+ structure: float
289
+ devex: float
290
+
291
+ # Overall Score (weighted average, 7-category)
292
+ if self.weights is not None:
293
+ w: _Weights = self.weights
294
+ else:
295
+ class DefaultWeights:
296
+ complexity = 0.18
297
+ security = 0.27
298
+ maintainability = 0.24
299
+ test_coverage = 0.13
300
+ performance = 0.08
301
+ structure = 0.05
302
+ devex = 0.05
303
+
304
+ w = DefaultWeights()
305
+
306
+ scores["overall_score"] = (
307
+ (10 - scores["complexity_score"]) * w.complexity
308
+ + scores["security_score"] * w.security
309
+ + scores["maintainability_score"] * w.maintainability
310
+ + scores["test_coverage_score"] * w.test_coverage
311
+ + scores["performance_score"] * w.performance
312
+ + scores["structure_score"] * w.structure
313
+ + scores["devex_score"] * w.devex
314
+ ) * 10 # Scale from 0-10 weighted sum to 0-100
315
+
316
+ # Phase 3.3: Validate all scores before returning
317
+ from .score_validator import ScoreValidator
318
+ from ...core.language_detector import Language
319
+
320
+ validator = ScoreValidator()
321
+ validation_results = validator.validate_all_scores(
322
+ scores, language=Language.PYTHON, context=None
323
+ )
324
+
325
+ # Update scores with validated/clamped values and add explanations
326
+ validated_scores = {}
327
+ score_explanations = {}
328
+ for category, result in validation_results.items():
329
+ if result.valid and result.calibrated_score is not None:
330
+ validated_scores[category] = result.calibrated_score
331
+ if result.explanation:
332
+ score_explanations[category] = {
333
+ "explanation": result.explanation,
334
+ "suggestions": result.suggestions,
335
+ }
336
+ else:
337
+ validated_scores[category] = scores.get(category, 0.0)
338
+
339
+ # Add explanations to result if any
340
+ if score_explanations:
341
+ validated_scores["_explanations"] = score_explanations
342
+
343
+ # Merge: keep all scores (incl. structure_score, devex_score), overlay validated
344
+ merged = {**scores, **validated_scores}
345
+ return merged
346
+
347
+ def _calculate_complexity(self, code: str) -> float:
348
+ """Calculate cyclomatic complexity (0-10 scale)"""
349
+ # Validate input
350
+ validate_code_input(code, method_name="_calculate_complexity")
351
+
352
+ if not self.has_radon:
353
+ return 5.0 # Default neutral score
354
+
355
+ try:
356
+ tree = ast.parse(code)
357
+ complexities = cc_visit(tree)
358
+
359
+ if not complexities:
360
+ return 1.0
361
+
362
+ # Get max complexity
363
+ max_complexity = max(cc.complexity for cc in complexities)
364
+
365
+ # Scale to 0-10 using constants
366
+ return min(
367
+ max_complexity / ComplexityConstants.SCALING_FACTOR,
368
+ ComplexityConstants.MAX_SCORE
369
+ )
370
+ except SyntaxError:
371
+ return 10.0 # Syntax errors = max complexity
372
+
373
+ def _calculate_security(self, file_path: Path | None, code: str) -> float:
374
+ """Calculate security score (0-10 scale, higher is better)"""
375
+ # Validate inputs
376
+ validate_code_input(code, method_name="_calculate_security")
377
+ if file_path is not None and not isinstance(file_path, Path):
378
+ raise ValueError(f"_calculate_security: file_path must be Path or None, got {type(file_path).__name__}")
379
+
380
+ if not self.has_bandit:
381
+ # Basic heuristic check
382
+ insecure_patterns = [
383
+ "eval(",
384
+ "exec(",
385
+ "__import__",
386
+ "pickle.loads",
387
+ "subprocess.call",
388
+ "os.system",
389
+ ]
390
+ issues = sum(1 for pattern in insecure_patterns if pattern in code)
391
+ return max(
392
+ 0.0,
393
+ SecurityConstants.MAX_SCORE - (issues * SecurityConstants.INSECURE_PATTERN_PENALTY)
394
+ )
395
+
396
+ try:
397
+ # Use bandit for proper security analysis
398
+ # BanditManager expects a BanditConfig, not a dict. Passing a dict can raise ValueError,
399
+ # which would silently degrade scoring to a neutral 5.0.
400
+ b_conf = bandit_config.BanditConfig()
401
+ b_mgr = manager.BanditManager(
402
+ config=b_conf,
403
+ agg_type="file",
404
+ debug=False,
405
+ verbose=False,
406
+ quiet=True,
407
+ profile=None,
408
+ ignore_nosec=False,
409
+ )
410
+ b_mgr.discover_files([str(file_path)], False)
411
+ b_mgr.run_tests()
412
+
413
+ # Count high/medium severity issues
414
+ issues = b_mgr.get_issue_list()
415
+ high_severity = sum(1 for i in issues if i.severity == bandit.HIGH)
416
+ medium_severity = sum(1 for i in issues if i.severity == bandit.MEDIUM)
417
+
418
+ # Score: 10 - (high*3 + medium*1)
419
+ score = 10.0 - (high_severity * 3.0 + medium_severity * 1.0)
420
+ return max(0.0, score)
421
+ except (FileNotFoundError, PermissionError, ValueError) as e:
422
+ # Specific exceptions for file/system errors
423
+ logger.warning(f"Security scoring failed for {file_path}: {e}")
424
+ return 5.0 # Default neutral on error
425
+ except Exception as e:
426
+ # Catch-all for unexpected errors (should be rare)
427
+ logger.warning(f"Unexpected error during security scoring for {file_path}: {e}", exc_info=True)
428
+ return 5.0 # Default neutral on error
429
+
430
+ def _calculate_maintainability(self, code: str) -> float:
431
+ """
432
+ Calculate maintainability index (0-10 scale, higher is better).
433
+
434
+ Phase 3.1: Enhanced with context-aware scoring using MaintainabilityScorer.
435
+ Phase 2 (P0): Maintainability issues are captured separately via get_maintainability_issues().
436
+ """
437
+ from .maintainability_scorer import MaintainabilityScorer
438
+ from ...core.language_detector import Language
439
+
440
+ # Use context-aware maintainability scorer
441
+ scorer = MaintainabilityScorer()
442
+ return scorer.calculate(code, Language.PYTHON, file_path=None, context=None)
443
+
444
+ def get_maintainability_issues(
445
+ self, code: str, file_path: Path | None = None
446
+ ) -> list[dict[str, Any]]:
447
+ """
448
+ Get specific maintainability issues (Phase 2 - P0).
449
+
450
+ Returns list of issues with details like missing docstrings, long functions, etc.
451
+
452
+ Args:
453
+ code: Source code content
454
+ file_path: Optional path to the file
455
+
456
+ Returns:
457
+ List of maintainability issues with details
458
+ """
459
+ from .maintainability_scorer import MaintainabilityScorer
460
+ from ...core.language_detector import Language
461
+
462
+ scorer = MaintainabilityScorer()
463
+ return scorer.get_issues(code, Language.PYTHON, file_path=file_path, context=None)
464
+
465
+ def _calculate_test_coverage(self, file_path: Path) -> float:
466
+ """
467
+ Calculate test coverage score (0-10 scale, higher is better).
468
+
469
+ Attempts to read coverage data from:
470
+ 1. coverage.xml file in project root or .coverage file
471
+ 2. Falls back to heuristic if no coverage data available
472
+
473
+ Args:
474
+ file_path: Path to the file being scored
475
+
476
+ Returns:
477
+ Coverage score (0-10 scale)
478
+ """
479
+ if not self.has_coverage:
480
+ # No coverage tool available, use heuristic
481
+ return self._coverage_heuristic(file_path)
482
+
483
+ try:
484
+ # Try to find and parse coverage data
485
+ project_root = self._find_project_root(file_path)
486
+ if project_root is None:
487
+ return 5.0 # Neutral if can't find project root
488
+
489
+ # Look for coverage.xml first (pytest-cov output)
490
+ coverage_xml = project_root / "coverage.xml"
491
+ if coverage_xml.exists():
492
+ return self._parse_coverage_xml(coverage_xml, file_path)
493
+
494
+ # Look for .coverage database file
495
+ coverage_db = project_root / ".coverage"
496
+ if coverage_db.exists():
497
+ return self._parse_coverage_db(coverage_db, file_path)
498
+
499
+ # No coverage data found, use heuristic
500
+ return self._coverage_heuristic(file_path)
501
+
502
+ except Exception:
503
+ # Fallback to heuristic on any error
504
+ return self._coverage_heuristic(file_path)
505
+
506
+ def _parse_coverage_xml(self, coverage_xml: Path, file_path: Path) -> float:
507
+ """Parse coverage.xml and return coverage percentage for file_path"""
508
+ try:
509
+ # coverage.xml is locally generated, but use defusedxml to reduce XML attack risk.
510
+ from defusedxml import ElementTree as ET
511
+
512
+ tree = ET.parse(coverage_xml)
513
+ root = tree.getroot()
514
+
515
+ # Get relative path from project root
516
+ project_root = coverage_xml.parent
517
+ try:
518
+ rel_path = file_path.relative_to(project_root)
519
+ file_path_str = str(rel_path).replace("\\", "/")
520
+ except ValueError:
521
+ # File not in project root
522
+ return 5.0
523
+
524
+ # Find coverage for this file
525
+ for package in root.findall(".//package"):
526
+ for class_elem in package.findall(".//class"):
527
+ file_name = class_elem.get("filename", "")
528
+ if file_name == file_path_str or file_path.name in file_name:
529
+ # Get line-rate (coverage percentage)
530
+ line_rate = float(class_elem.get("line-rate", "0.0"))
531
+ # Convert 0-1 scale to 0-10 scale
532
+ return line_rate * 10.0
533
+
534
+ # File not found in coverage report
535
+ return 0.0
536
+ except Exception:
537
+ return 5.0 # Default on error
538
+
539
+ def _parse_coverage_db(self, coverage_db: Path, file_path: Path) -> float:
540
+ """Parse .coverage database and return coverage percentage"""
541
+ try:
542
+ cov = Coverage()
543
+ cov.load()
544
+
545
+ # Get coverage data
546
+ data = cov.get_data()
547
+
548
+ # Try to find file in coverage data
549
+ try:
550
+ rel_path = file_path.relative_to(coverage_db.parent)
551
+ file_path_str = str(rel_path).replace("\\", "/")
552
+ except ValueError:
553
+ return 5.0
554
+
555
+ # Get coverage for this file
556
+ if file_path_str in data.measured_files():
557
+ # Calculate coverage percentage
558
+ lines = data.lines(file_path_str)
559
+ if not lines:
560
+ return 0.0
561
+
562
+ # Count covered vs total lines (simplified)
563
+ # In practice, we'd need to check which lines are executable
564
+ # For now, return neutral score
565
+ return 5.0
566
+
567
+ return 0.0 # File not covered
568
+ except Exception:
569
+ return 5.0
570
+
571
+ def _coverage_heuristic(self, file_path: Path) -> float:
572
+ """
573
+ Heuristic-based coverage estimate.
574
+
575
+ Phase 1 (P0): Fixed to return 0.0 when no test files exist.
576
+
577
+ Checks for:
578
+ - Test file existence (actual test files, not just directories)
579
+ - Test directory structure
580
+ - Test naming patterns
581
+
582
+ Returns:
583
+ - 0.0 if no test files exist (no tests written yet)
584
+ - 5.0 if test files exist but no coverage data (tests exist but not run)
585
+ - 10.0 if both test files and coverage data exist (not used here, handled by caller)
586
+ """
587
+ project_root = self._find_project_root(file_path)
588
+ if project_root is None:
589
+ return 0.0 # No project root = assume no tests (Phase 1 fix)
590
+
591
+ # Look for test files with comprehensive patterns
592
+ test_dirs = ["tests", "test", "tests/unit", "tests/integration", "tests/test", "test/test"]
593
+ test_patterns = [
594
+ f"test_{file_path.stem}.py",
595
+ f"{file_path.stem}_test.py",
596
+ f"test_{file_path.name}",
597
+ # Also check for module-style test files
598
+ f"test_{file_path.stem.replace('_', '')}.py",
599
+ f"test_{file_path.stem.replace('-', '_')}.py",
600
+ ]
601
+
602
+ # Also check if the file itself is a test file
603
+ if file_path.name.startswith("test_") or file_path.name.endswith("_test.py"):
604
+ # File is a test file, assume it has coverage if it exists
605
+ return 5.0 # Tests exist but no coverage data available
606
+
607
+ # Check if any test files actually exist
608
+ test_file_found = False
609
+ for test_dir in test_dirs:
610
+ test_dir_path = project_root / test_dir
611
+ if test_dir_path.exists() and test_dir_path.is_dir():
612
+ for pattern in test_patterns:
613
+ test_file = test_dir_path / pattern
614
+ if test_file.exists() and test_file.is_file():
615
+ test_file_found = True
616
+ break
617
+ if test_file_found:
618
+ break
619
+
620
+ # Phase 1 fix: Return 0.0 if no test files exist (no tests written yet)
621
+ if not test_file_found:
622
+ return 0.0
623
+
624
+ # Test files exist but not run yet
625
+ return 5.0
626
+
627
+ def _find_project_root(self, file_path: Path) -> Path | None:
628
+ """Delegate to BaseScorer. Override for CodeScorer-specific markers if needed."""
629
+ return BaseScorer._find_project_root(file_path)
630
+
631
+ def get_performance_issues(
632
+ self, code: str, file_path: Path | None = None
633
+ ) -> list[dict[str, Any]]:
634
+ """
635
+ Get specific performance issues with line numbers (Phase 4 - P1).
636
+
637
+ Returns list of performance bottlenecks with details like nested loops, expensive operations, etc.
638
+
639
+ Args:
640
+ code: Source code content
641
+ file_path: Optional path to the file
642
+
643
+ Returns:
644
+ List of performance issues with line numbers
645
+ """
646
+ from .issue_tracking import PerformanceIssue
647
+ import ast
648
+
649
+ issues: list[PerformanceIssue] = []
650
+ code_lines = code.splitlines()
651
+
652
+ try:
653
+ tree = ast.parse(code)
654
+ except SyntaxError:
655
+ return [PerformanceIssue(
656
+ issue_type="syntax_error",
657
+ message="File contains syntax errors - cannot analyze performance",
658
+ severity="high"
659
+ ).__dict__]
660
+
661
+ # Analyze functions for performance issues
662
+ for node in ast.walk(tree):
663
+ if isinstance(node, ast.FunctionDef):
664
+ # Check for nested loops
665
+ for child in ast.walk(node):
666
+ if isinstance(child, ast.For):
667
+ # Check if this loop contains another loop
668
+ for nested_child in ast.walk(child):
669
+ if isinstance(nested_child, ast.For) and nested_child != child:
670
+ # Found nested loop
671
+ issues.append(PerformanceIssue(
672
+ issue_type="nested_loops",
673
+ message=f"Nested for loops detected in function '{node.name}' - potential O(n²) complexity",
674
+ line_number=child.lineno,
675
+ severity="high",
676
+ operation_type="loop",
677
+ context=f"Nested in function '{node.name}'",
678
+ suggestion="Consider using itertools.product() or list comprehensions to flatten nested loops"
679
+ ))
680
+
681
+ # Check for expensive operations in loops
682
+ if isinstance(child, ast.For):
683
+ for loop_child in ast.walk(child):
684
+ if isinstance(loop_child, ast.Call):
685
+ # Check for expensive function calls in loops
686
+ if isinstance(loop_child.func, ast.Name):
687
+ func_name = loop_child.func.id
688
+ expensive_operations = ["time.fromisoformat", "datetime.fromisoformat", "re.compile", "json.loads"]
689
+ if any(exp_op in func_name for exp_op in expensive_operations):
690
+ issues.append(PerformanceIssue(
691
+ issue_type="expensive_operation_in_loop",
692
+ message=f"Expensive operation '{func_name}' called in loop at line {loop_child.lineno} - parse once before loop",
693
+ line_number=loop_child.lineno,
694
+ severity="medium",
695
+ operation_type=func_name,
696
+ context=f"In loop at line {child.lineno}",
697
+ suggestion=f"Move '{func_name}' call outside the loop and cache the result"
698
+ ))
699
+
700
+ # Check for list comprehensions with function calls
701
+ if isinstance(node, ast.ListComp):
702
+ func_calls_in_comp = sum(1 for n in ast.walk(node) if isinstance(n, ast.Call))
703
+ if func_calls_in_comp > 5:
704
+ issues.append(PerformanceIssue(
705
+ issue_type="expensive_comprehension",
706
+ message=f"List comprehension at line {node.lineno} contains {func_calls_in_comp} function calls - consider using generator or loop",
707
+ line_number=node.lineno,
708
+ severity="medium",
709
+ operation_type="comprehension",
710
+ suggestion="Consider using a generator expression or a loop for better performance"
711
+ ))
712
+
713
+ # Convert to dict format
714
+ return [
715
+ {
716
+ "issue_type": issue.issue_type,
717
+ "message": issue.message,
718
+ "line_number": issue.line_number,
719
+ "severity": issue.severity,
720
+ "suggestion": issue.suggestion,
721
+ "operation_type": issue.operation_type,
722
+ "context": issue.context,
723
+ }
724
+ for issue in issues
725
+ ]
726
+
727
+ def _calculate_performance(self, code: str) -> float:
728
+ """
729
+ Calculate performance score using static analysis (0-10 scale, higher is better).
730
+
731
+ Checks for:
732
+ - Function size (number of lines)
733
+ - Nesting depth
734
+ - Inefficient patterns (N+1 queries, nested loops, etc.)
735
+ - Large list/dict comprehensions
736
+ """
737
+ try:
738
+ tree = ast.parse(code)
739
+ issues = []
740
+
741
+ # Analyze functions
742
+ for node in ast.walk(tree):
743
+ if isinstance(node, ast.FunctionDef):
744
+ # Check function size
745
+ # Use end_lineno if available (Python 3.8+), otherwise estimate
746
+ if hasattr(node, "end_lineno") and node.end_lineno is not None:
747
+ func_lines = node.end_lineno - node.lineno
748
+ else:
749
+ # Estimate: count lines in function body
750
+ func_lines = (
751
+ len(code.split("\n")[node.lineno - 1 : node.lineno + 49])
752
+ if len(code.split("\n")) > node.lineno
753
+ else 50
754
+ )
755
+
756
+ if func_lines > 50:
757
+ issues.append("large_function") # > 50 lines
758
+ if func_lines > 100:
759
+ issues.append("very_large_function") # > 100 lines
760
+
761
+ # Check nesting depth
762
+ max_depth = self._get_max_nesting_depth(node)
763
+ if max_depth > 4:
764
+ issues.append("deep_nesting") # > 4 levels
765
+ if max_depth > 6:
766
+ issues.append("very_deep_nesting") # > 6 levels
767
+
768
+ # Check for nested loops (potential N^2 complexity)
769
+ if isinstance(node, ast.For):
770
+ for child in ast.walk(node):
771
+ if isinstance(child, ast.For) and child != node:
772
+ issues.append("nested_loops")
773
+ break
774
+
775
+ # Check for list comprehensions with function calls
776
+ if isinstance(node, ast.ListComp):
777
+ # Count function calls in comprehension
778
+ func_calls = sum(
779
+ 1 for n in ast.walk(node) if isinstance(n, ast.Call)
780
+ )
781
+ if func_calls > 5:
782
+ issues.append("expensive_comprehension")
783
+
784
+ # Calculate score based on issues
785
+ # Start with 10, deduct points for issues
786
+ score = 10.0
787
+ penalty_map = {
788
+ "large_function": 0.5,
789
+ "very_large_function": 1.5,
790
+ "deep_nesting": 1.0,
791
+ "very_deep_nesting": 2.0,
792
+ "nested_loops": 1.5,
793
+ "expensive_comprehension": 0.5,
794
+ }
795
+
796
+ seen_issues = set()
797
+ for issue in issues:
798
+ if issue not in seen_issues:
799
+ score -= penalty_map.get(issue, 0.5)
800
+ seen_issues.add(issue)
801
+
802
+ return max(0.0, min(10.0, score))
803
+
804
+ except SyntaxError:
805
+ return 0.0 # Syntax errors = worst performance score
806
+ except Exception:
807
+ return 5.0 # Default on error
808
+
809
+ def _get_max_nesting_depth(self, node: ast.AST, current_depth: int = 0) -> int:
810
+ """Calculate maximum nesting depth in an AST node"""
811
+ max_depth = current_depth
812
+
813
+ for child in ast.iter_child_nodes(node):
814
+ # Count nesting for control structures
815
+ if isinstance(child, (ast.If, ast.For, ast.While, ast.Try, ast.With)):
816
+ child_depth = self._get_max_nesting_depth(child, current_depth + 1)
817
+ max_depth = max(max_depth, child_depth)
818
+ else:
819
+ child_depth = self._get_max_nesting_depth(child, current_depth)
820
+ max_depth = max(max_depth, child_depth)
821
+
822
+ return max_depth
823
+
824
+ def _calculate_linting_score(self, file_path: Path) -> float:
825
+ """
826
+ Calculate linting score using Ruff (0-10 scale, higher is better).
827
+
828
+ Phase 6: Modern Quality Analysis - Ruff Integration
829
+
830
+ Returns:
831
+ Linting score (0-10), where 10 = no issues, 0 = many issues
832
+ """
833
+ if not self.has_ruff:
834
+ return 5.0 # Neutral score if Ruff not available
835
+
836
+ # Only check Python files
837
+ if file_path.suffix != ".py":
838
+ return 10.0 # Perfect score for non-Python files (can't lint)
839
+
840
+ try:
841
+ # Run ruff check with JSON output
842
+ result = subprocess.run( # nosec B603
843
+ [
844
+ sys.executable,
845
+ "-m",
846
+ "ruff",
847
+ "check",
848
+ "--output-format=json",
849
+ str(file_path),
850
+ ],
851
+ capture_output=True,
852
+ text=True,
853
+ encoding="utf-8",
854
+ errors="replace",
855
+ timeout=30, # 30 second timeout
856
+ cwd=file_path.parent if file_path.parent.exists() else None,
857
+ )
858
+
859
+ # Parse JSON output
860
+ if result.returncode == 0 and not result.stdout.strip():
861
+ # No issues found
862
+ return 10.0
863
+
864
+ try:
865
+ # Ruff JSON format: list of diagnostic objects
866
+ diagnostics = (
867
+ json_lib.loads(result.stdout) if result.stdout.strip() else []
868
+ )
869
+
870
+ if not diagnostics:
871
+ return 10.0
872
+
873
+ # Count issues by severity
874
+ # Ruff severity levels: E (Error), W (Warning), F (Fatal), I (Info)
875
+ error_count = sum(
876
+ 1
877
+ for d in diagnostics
878
+ if d.get("code", {}).get("name", "").startswith("E")
879
+ )
880
+ warning_count = sum(
881
+ 1
882
+ for d in diagnostics
883
+ if d.get("code", {}).get("name", "").startswith("W")
884
+ )
885
+ fatal_count = sum(
886
+ 1
887
+ for d in diagnostics
888
+ if d.get("code", {}).get("name", "").startswith("F")
889
+ )
890
+
891
+ # Calculate score: Start at 10, deduct points
892
+ # Errors (E): -2 points each
893
+ # Fatal (F): -3 points each
894
+ # Warnings (W): -0.5 points each
895
+ score = 10.0
896
+ score -= error_count * 2.0
897
+ score -= fatal_count * 3.0
898
+ score -= warning_count * 0.5
899
+
900
+ return max(0.0, min(10.0, score))
901
+
902
+ except json_lib.JSONDecodeError:
903
+ # If JSON parsing fails, check stderr for errors
904
+ if result.stderr:
905
+ return 5.0 # Neutral on parsing error
906
+ return 10.0 # No output = no issues
907
+
908
+ except subprocess.TimeoutExpired:
909
+ return 5.0 # Neutral on timeout
910
+ except FileNotFoundError:
911
+ # Ruff not found in PATH
912
+ return 5.0
913
+ except Exception:
914
+ # Any other error
915
+ return 5.0
916
+
917
+ def get_ruff_issues(self, file_path: Path) -> list[dict[str, Any]]:
918
+ """
919
+ Get detailed Ruff linting issues for a file.
920
+
921
+ Phase 6: Modern Quality Analysis - Ruff Integration
922
+
923
+ Returns:
924
+ List of diagnostic dictionaries with code, message, location, etc.
925
+ """
926
+ if not self.has_ruff or file_path.suffix != ".py":
927
+ return []
928
+
929
+ try:
930
+ result = subprocess.run( # nosec B603
931
+ [
932
+ sys.executable,
933
+ "-m",
934
+ "ruff",
935
+ "check",
936
+ "--output-format=json",
937
+ str(file_path),
938
+ ],
939
+ capture_output=True,
940
+ text=True,
941
+ encoding="utf-8",
942
+ errors="replace",
943
+ timeout=30,
944
+ cwd=file_path.parent if file_path.parent.exists() else None,
945
+ )
946
+
947
+ if result.returncode == 0 and not result.stdout.strip():
948
+ return []
949
+
950
+ try:
951
+ diagnostics = (
952
+ json_lib.loads(result.stdout) if result.stdout.strip() else []
953
+ )
954
+ return diagnostics
955
+ except json_lib.JSONDecodeError:
956
+ return []
957
+
958
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
959
+ return []
960
+
961
+ def _calculate_type_checking_score(self, file_path: Path) -> float:
962
+ """
963
+ Calculate type checking score using mypy (0-10 scale, higher is better).
964
+
965
+ Phase 6.2: Modern Quality Analysis - mypy Integration
966
+ Phase 5 (P1): Fixed to actually run mypy and return real scores (not static 5.0).
967
+ ENH-002-S2: Prefer ScopedMypyExecutor (--follow-imports=skip, <10s) with fallback to full mypy.
968
+ """
969
+ if not self.has_mypy:
970
+ logger.debug("mypy not available - returning neutral score")
971
+ return 5.0 # Neutral score if mypy not available
972
+
973
+ # Only check Python files
974
+ if file_path.suffix != ".py":
975
+ return 10.0 # Perfect score for non-Python files (can't type check)
976
+
977
+ # ENH-002-S2: Try scoped mypy first (faster)
978
+ try:
979
+ from .tools.scoped_mypy import ScopedMypyExecutor
980
+ executor = ScopedMypyExecutor()
981
+ result = executor.run_scoped_sync(file_path, timeout=10)
982
+ if result.files_checked == 1 or result.issues:
983
+ error_count = len(result.issues)
984
+ if error_count == 0:
985
+ return 10.0
986
+ score = 10.0 - (error_count * 0.5)
987
+ logger.debug(
988
+ "mypy (scoped) found %s errors for %s, score: %s/10",
989
+ error_count, file_path, score,
990
+ )
991
+ return max(0.0, min(10.0, score))
992
+ except Exception as e:
993
+ logger.debug("scoped mypy not used, falling back to full mypy: %s", e)
994
+
995
+ try:
996
+ result = subprocess.run( # nosec B603
997
+ [
998
+ sys.executable,
999
+ "-m",
1000
+ "mypy",
1001
+ "--show-error-codes",
1002
+ "--no-error-summary",
1003
+ "--no-color-output",
1004
+ "--no-incremental",
1005
+ str(file_path),
1006
+ ],
1007
+ capture_output=True,
1008
+ text=True,
1009
+ encoding="utf-8",
1010
+ errors="replace",
1011
+ timeout=30,
1012
+ cwd=file_path.parent if file_path.parent.exists() else None,
1013
+ )
1014
+ if result.returncode == 0:
1015
+ logger.debug("mypy found no errors for %s", file_path)
1016
+ return 10.0
1017
+ output = result.stdout.strip()
1018
+ if not output:
1019
+ logger.debug("mypy returned non-zero but no output for %s", file_path)
1020
+ return 10.0
1021
+ error_lines = [
1022
+ line
1023
+ for line in output.split("\n")
1024
+ if "error:" in line.lower() and file_path.name in line
1025
+ ]
1026
+ error_count = len(error_lines)
1027
+ if error_count == 0:
1028
+ logger.debug("mypy returned non-zero but no parseable errors for %s", file_path)
1029
+ return 10.0
1030
+ score = 10.0 - (error_count * 0.5)
1031
+ logger.debug("mypy found %s errors for %s, score: %s/10", error_count, file_path, score)
1032
+ return max(0.0, min(10.0, score))
1033
+ except subprocess.TimeoutExpired:
1034
+ logger.warning("mypy timed out for %s", file_path)
1035
+ return 5.0
1036
+ except FileNotFoundError:
1037
+ logger.debug("mypy not found in PATH for %s", file_path)
1038
+ self.has_mypy = False
1039
+ return 5.0
1040
+ except Exception as e:
1041
+ logger.warning("mypy failed for %s: %s", file_path, e, exc_info=True)
1042
+ return 5.0
1043
+
1044
+ def get_mypy_errors(self, file_path: Path) -> list[dict[str, Any]]:
1045
+ """
1046
+ Get detailed mypy type checking errors for a file.
1047
+
1048
+ Phase 6.2: Modern Quality Analysis - mypy Integration
1049
+ ENH-002-S2: Prefer ScopedMypyExecutor with fallback to full mypy.
1050
+ """
1051
+ if not self.has_mypy or file_path.suffix != ".py":
1052
+ return []
1053
+
1054
+ try:
1055
+ from .tools.scoped_mypy import ScopedMypyExecutor
1056
+ executor = ScopedMypyExecutor()
1057
+ result = executor.run_scoped_sync(file_path, timeout=10)
1058
+ if result.files_checked == 1 or result.issues:
1059
+ return [
1060
+ {
1061
+ "filename": str(i.file_path),
1062
+ "line": i.line,
1063
+ "message": i.message,
1064
+ "error_code": i.error_code,
1065
+ "severity": i.severity,
1066
+ }
1067
+ for i in result.issues
1068
+ ]
1069
+ except Exception:
1070
+ pass
1071
+
1072
+ try:
1073
+ result = subprocess.run( # nosec B603
1074
+ [
1075
+ sys.executable,
1076
+ "-m",
1077
+ "mypy",
1078
+ "--show-error-codes",
1079
+ "--no-error-summary",
1080
+ "--no-incremental",
1081
+ str(file_path),
1082
+ ],
1083
+ capture_output=True,
1084
+ text=True,
1085
+ encoding="utf-8",
1086
+ errors="replace",
1087
+ timeout=30,
1088
+ cwd=file_path.parent if file_path.parent.exists() else None,
1089
+ )
1090
+ if result.returncode == 0 or not result.stdout.strip():
1091
+ return []
1092
+ errors = []
1093
+ for line in result.stdout.strip().split("\n"):
1094
+ if "error:" not in line.lower():
1095
+ continue
1096
+ parts = line.split(":", 3)
1097
+ if len(parts) >= 4:
1098
+ filename = parts[0]
1099
+ try:
1100
+ line_num = int(parts[1])
1101
+ except ValueError:
1102
+ continue
1103
+ error_msg = parts[3].strip()
1104
+ error_code = None
1105
+ if "[" in error_msg and "]" in error_msg:
1106
+ start = error_msg.rfind("[")
1107
+ end = error_msg.rfind("]")
1108
+ if start < end:
1109
+ error_code = error_msg[start + 1 : end]
1110
+ error_msg = error_msg[:start].strip()
1111
+ errors.append({
1112
+ "filename": filename,
1113
+ "line": line_num,
1114
+ "message": error_msg,
1115
+ "error_code": error_code,
1116
+ "severity": "error",
1117
+ })
1118
+ return errors
1119
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
1120
+ return []
1121
+
1122
+ def _format_ruff_issues(self, diagnostics: list[dict[str, Any]]) -> list[dict[str, Any]]:
1123
+ """
1124
+ Format raw ruff diagnostics into a cleaner structure for output.
1125
+
1126
+ P1 Improvement: Include actual lint errors in score output.
1127
+
1128
+ Args:
1129
+ diagnostics: Raw ruff JSON diagnostics
1130
+
1131
+ Returns:
1132
+ List of formatted issues with line, code, message, severity
1133
+ """
1134
+ formatted = []
1135
+ for diag in diagnostics:
1136
+ # Extract code info (ruff format: {"code": {"name": "F401", ...}})
1137
+ code_info = diag.get("code", {})
1138
+ if isinstance(code_info, dict):
1139
+ code = code_info.get("name", "")
1140
+ else:
1141
+ code = str(code_info)
1142
+
1143
+ # Determine severity from code prefix
1144
+ severity = "warning"
1145
+ if code.startswith("E") or code.startswith("F"):
1146
+ severity = "error"
1147
+ elif code.startswith("W"):
1148
+ severity = "warning"
1149
+ elif code.startswith("I"):
1150
+ severity = "info"
1151
+
1152
+ # Get location info
1153
+ location = diag.get("location", {})
1154
+ line = location.get("row", 0) if isinstance(location, dict) else 0
1155
+ column = location.get("column", 0) if isinstance(location, dict) else 0
1156
+
1157
+ formatted.append({
1158
+ "code": code,
1159
+ "message": diag.get("message", ""),
1160
+ "line": line,
1161
+ "column": column,
1162
+ "severity": severity,
1163
+ })
1164
+
1165
+ # Sort by line number
1166
+ formatted.sort(key=lambda x: (x.get("line", 0), x.get("column", 0)))
1167
+
1168
+ return formatted
1169
+
1170
+ def _group_ruff_issues_by_code(self, issues: list[dict[str, Any]]) -> dict[str, Any]:
1171
+ """
1172
+ Group ruff issues by rule code for cleaner, more actionable reports.
1173
+
1174
+ ENH-002 Story #18: Ruff Output Grouping
1175
+
1176
+ Args:
1177
+ issues: List of ruff diagnostic dictionaries
1178
+
1179
+ Returns:
1180
+ Dictionary with grouped issues:
1181
+ {
1182
+ "total_count": int,
1183
+ "groups": [
1184
+ {
1185
+ "code": "UP006",
1186
+ "count": 17,
1187
+ "description": "Use dict/list instead of Dict/List",
1188
+ "severity": "info",
1189
+ "issues": [...]
1190
+ },
1191
+ ...
1192
+ ],
1193
+ "summary": "UP006 (17), UP045 (10), UP007 (2), F401 (1)"
1194
+ }
1195
+ """
1196
+ if not issues:
1197
+ return {
1198
+ "total_count": 0,
1199
+ "groups": [],
1200
+ "summary": "No issues found"
1201
+ }
1202
+
1203
+ # Group issues by code
1204
+ groups_dict: dict[str, list[dict[str, Any]]] = {}
1205
+ for issue in issues:
1206
+ code_info = issue.get("code", {})
1207
+ if isinstance(code_info, dict):
1208
+ code = code_info.get("name", "UNKNOWN")
1209
+ else:
1210
+ code = str(code_info) if code_info else "UNKNOWN"
1211
+
1212
+ if code not in groups_dict:
1213
+ groups_dict[code] = []
1214
+ groups_dict[code].append(issue)
1215
+
1216
+ # Create grouped structure with metadata
1217
+ groups = []
1218
+ for code, code_issues in groups_dict.items():
1219
+ # Get first message as description (they're usually the same for same code)
1220
+ description = code_issues[0].get("message", "") if code_issues else ""
1221
+
1222
+ # Determine severity from code
1223
+ severity = "info"
1224
+ if code.startswith("E") or code.startswith("F"):
1225
+ severity = "error"
1226
+ elif code.startswith("W"):
1227
+ severity = "warning"
1228
+
1229
+ groups.append({
1230
+ "code": code,
1231
+ "count": len(code_issues),
1232
+ "description": description,
1233
+ "severity": severity,
1234
+ "issues": code_issues
1235
+ })
1236
+
1237
+ # Sort by count (descending) then by code
1238
+ groups.sort(key=lambda x: (-x["count"], x["code"]))
1239
+
1240
+ # Create summary string: "UP006 (17), UP045 (10), ..."
1241
+ summary_parts = [f"{g['code']} ({g['count']})" for g in groups]
1242
+ summary = ", ".join(summary_parts)
1243
+
1244
+ return {
1245
+ "total_count": len(issues),
1246
+ "groups": groups,
1247
+ "summary": summary
1248
+ }
1249
+
1250
+ def _calculate_duplication_score(self, file_path: Path) -> float:
1251
+ """
1252
+ Calculate duplication score using jscpd (0-10 scale, higher is better).
1253
+
1254
+ Phase 6.4: Modern Quality Analysis - jscpd Integration
1255
+
1256
+ Note: jscpd works on directories/files, so we analyze the parent directory
1257
+ or file directly. For single file, we analyze just that file.
1258
+
1259
+ Returns:
1260
+ Duplication score (0-10), where 10 = no duplication, 0 = high duplication
1261
+ Score formula: 10 - (duplication_pct / 10)
1262
+ """
1263
+ if not self.has_jscpd:
1264
+ return 5.0 # Neutral score if jscpd not available
1265
+
1266
+ # jscpd works best on directories or multiple files
1267
+ # For single file analysis, we'll analyze the file directly
1268
+ try:
1269
+ # Determine target (file or directory)
1270
+ target = str(file_path)
1271
+ if file_path.is_dir():
1272
+ target_dir = str(file_path)
1273
+ else:
1274
+ target_dir = str(file_path.parent)
1275
+
1276
+ # Build jscpd command
1277
+ # Use npx if jscpd not directly available
1278
+ jscpd_path = shutil.which("jscpd")
1279
+ if jscpd_path:
1280
+ cmd = [jscpd_path]
1281
+ else:
1282
+ npx_path = shutil.which("npx")
1283
+ if not npx_path:
1284
+ return 5.0 # jscpd not available
1285
+ cmd = [npx_path, "--yes", "jscpd"]
1286
+
1287
+ # Add jscpd arguments
1288
+ cmd.extend(
1289
+ [
1290
+ target,
1291
+ "--format",
1292
+ "json",
1293
+ "--min-lines",
1294
+ str(self.min_duplication_lines),
1295
+ "--reporters",
1296
+ "json",
1297
+ "--output",
1298
+ ".", # Output to current directory
1299
+ ]
1300
+ )
1301
+
1302
+ # Run jscpd
1303
+ result = subprocess.run( # nosec B603 - fixed args
1304
+ wrap_windows_cmd_shim(cmd),
1305
+ capture_output=True,
1306
+ text=True,
1307
+ encoding="utf-8",
1308
+ errors="replace",
1309
+ timeout=120, # 2 minute timeout (jscpd can be slow on large codebases)
1310
+ cwd=target_dir if Path(target_dir).exists() else None,
1311
+ )
1312
+
1313
+ # jscpd outputs JSON to stdout when using --reporters json
1314
+ # But it might also create a file, so check both
1315
+ json_output = result.stdout.strip()
1316
+
1317
+ # Try to parse JSON from stdout
1318
+ try:
1319
+ if json_output:
1320
+ report_data = json_lib.loads(json_output)
1321
+ else:
1322
+ # Check for jscpd-report.json in output directory
1323
+ output_file = Path(target_dir) / "jscpd-report.json"
1324
+ if output_file.exists():
1325
+ with open(output_file, encoding="utf-8") as f:
1326
+ report_data = json_lib.load(f)
1327
+ else:
1328
+ # No duplication found (exit code 0 typically means no issues or success)
1329
+ if result.returncode == 0:
1330
+ return 10.0 # Perfect score (no duplication)
1331
+ return 5.0 # Neutral on parse failure
1332
+ except json_lib.JSONDecodeError:
1333
+ # JSON parse error - might be text output
1334
+ # Try to extract duplication percentage from text output
1335
+ # Format: "Found X% duplicated lines out of Y total lines"
1336
+ lines = result.stdout.split("\n") + result.stderr.split("\n")
1337
+ for line in lines:
1338
+ if "%" in line and "duplicate" in line.lower():
1339
+ # Try to extract percentage
1340
+ try:
1341
+ pct_str = line.split("%")[0].split()[-1]
1342
+ duplication_pct = float(pct_str)
1343
+ score = 10.0 - (duplication_pct / 10.0)
1344
+ return max(0.0, min(10.0, score))
1345
+ except (ValueError, IndexError):
1346
+ pass
1347
+
1348
+ # If we can't parse, default behavior
1349
+ if result.returncode == 0:
1350
+ return 10.0 # No duplication found
1351
+ return 5.0 # Neutral on parse failure
1352
+
1353
+ # Extract duplication percentage from JSON report
1354
+ # jscpd JSON structure: { "percentage": X.X, ... }
1355
+ duplication_pct = report_data.get("percentage", 0.0)
1356
+
1357
+ # Calculate score: 10 - (duplication_pct / 10)
1358
+ # This means:
1359
+ # - 0% duplication = 10.0 score
1360
+ # - 3% duplication (threshold) = 9.7 score
1361
+ # - 10% duplication = 9.0 score
1362
+ # - 30% duplication = 7.0 score
1363
+ # - 100% duplication = 0.0 score
1364
+ score = 10.0 - (duplication_pct / 10.0)
1365
+ return max(0.0, min(10.0, score))
1366
+
1367
+ except subprocess.TimeoutExpired:
1368
+ return 5.0 # Neutral on timeout
1369
+ except FileNotFoundError:
1370
+ # jscpd not found
1371
+ return 5.0
1372
+ except Exception:
1373
+ # Any other error
1374
+ return 5.0
1375
+
1376
+ def get_duplication_report(self, file_path: Path) -> dict[str, Any]:
1377
+ """
1378
+ Get detailed duplication report from jscpd.
1379
+
1380
+ Phase 6.4: Modern Quality Analysis - jscpd Integration
1381
+
1382
+ Returns:
1383
+ Dictionary with duplication report data including:
1384
+ - percentage: Duplication percentage
1385
+ - duplicates: List of duplicate code blocks
1386
+ - files: File-level duplication stats
1387
+ """
1388
+ if not self.has_jscpd:
1389
+ return {
1390
+ "available": False,
1391
+ "percentage": 0.0,
1392
+ "duplicates": [],
1393
+ "files": [],
1394
+ }
1395
+
1396
+ try:
1397
+ # Determine target
1398
+ if file_path.is_dir():
1399
+ target_dir = str(file_path)
1400
+ target = str(file_path)
1401
+ else:
1402
+ target_dir = str(file_path.parent)
1403
+ target = str(file_path)
1404
+
1405
+ # Build jscpd command
1406
+ jscpd_path = shutil.which("jscpd")
1407
+ if jscpd_path:
1408
+ cmd = [jscpd_path]
1409
+ else:
1410
+ npx_path = shutil.which("npx")
1411
+ if not npx_path:
1412
+ return {
1413
+ "available": False,
1414
+ "percentage": 0.0,
1415
+ "duplicates": [],
1416
+ "files": [],
1417
+ }
1418
+ cmd = [npx_path, "--yes", "jscpd"]
1419
+
1420
+ cmd.extend(
1421
+ [
1422
+ target,
1423
+ "--format",
1424
+ "json",
1425
+ "--min-lines",
1426
+ str(self.min_duplication_lines),
1427
+ "--reporters",
1428
+ "json",
1429
+ ]
1430
+ )
1431
+
1432
+ # Run jscpd
1433
+ result = subprocess.run( # nosec B603 - fixed args
1434
+ wrap_windows_cmd_shim(cmd),
1435
+ capture_output=True,
1436
+ text=True,
1437
+ encoding="utf-8",
1438
+ errors="replace",
1439
+ timeout=120,
1440
+ cwd=target_dir if Path(target_dir).exists() else None,
1441
+ )
1442
+
1443
+ # Parse JSON output
1444
+ json_output = result.stdout.strip()
1445
+ try:
1446
+ if json_output:
1447
+ report_data = json_lib.loads(json_output)
1448
+ else:
1449
+ # Check for output file
1450
+ output_file = Path(target_dir) / "jscpd-report.json"
1451
+ if output_file.exists():
1452
+ with open(output_file, encoding="utf-8") as f:
1453
+ report_data = json_lib.load(f)
1454
+ else:
1455
+ return {
1456
+ "available": True,
1457
+ "percentage": 0.0,
1458
+ "duplicates": [],
1459
+ "files": [],
1460
+ }
1461
+ except json_lib.JSONDecodeError:
1462
+ return {
1463
+ "available": True,
1464
+ "error": "Failed to parse jscpd output",
1465
+ "percentage": 0.0,
1466
+ "duplicates": [],
1467
+ "files": [],
1468
+ }
1469
+
1470
+ # Extract relevant data from jscpd report
1471
+ # jscpd JSON format varies, but typically has:
1472
+ # - percentage: overall duplication percentage
1473
+ # - duplicates: array of duplicate pairs
1474
+ # - files: file-level statistics
1475
+
1476
+ return {
1477
+ "available": True,
1478
+ "percentage": report_data.get("percentage", 0.0),
1479
+ "duplicates": report_data.get("duplicates", []),
1480
+ "files": (
1481
+ report_data.get("statistics", {}).get("files", [])
1482
+ if "statistics" in report_data
1483
+ else []
1484
+ ),
1485
+ "total_lines": (
1486
+ report_data.get("statistics", {}).get("total", {}).get("lines", 0)
1487
+ if "statistics" in report_data
1488
+ else 0
1489
+ ),
1490
+ "duplicated_lines": (
1491
+ report_data.get("statistics", {})
1492
+ .get("duplicated", {})
1493
+ .get("lines", 0)
1494
+ if "statistics" in report_data
1495
+ else 0
1496
+ ),
1497
+ }
1498
+
1499
+ except subprocess.TimeoutExpired:
1500
+ return {
1501
+ "available": True,
1502
+ "error": "jscpd timeout",
1503
+ "percentage": 0.0,
1504
+ "duplicates": [],
1505
+ "files": [],
1506
+ }
1507
+ except FileNotFoundError:
1508
+ return {
1509
+ "available": False,
1510
+ "percentage": 0.0,
1511
+ "duplicates": [],
1512
+ "files": [],
1513
+ }
1514
+ except Exception as e:
1515
+ return {
1516
+ "available": True,
1517
+ "error": str(e),
1518
+ "percentage": 0.0,
1519
+ "duplicates": [],
1520
+ "files": [],
1521
+ }
1522
+
1523
+
1524
+ class ScorerFactory:
1525
+ """
1526
+ Factory to provide appropriate scorer based on language (Strategy Pattern).
1527
+
1528
+ Phase 1.2: Language-Specific Scorers
1529
+
1530
+ Now uses ScorerRegistry for extensible language support.
1531
+ """
1532
+
1533
+ @staticmethod
1534
+ def get_scorer(language: Language, config: ProjectConfig | None = None) -> BaseScorer:
1535
+ """
1536
+ Get the appropriate scorer for a given language.
1537
+
1538
+ Uses ScorerRegistry for extensible language support with fallback chains.
1539
+
1540
+ Args:
1541
+ language: Detected language enum
1542
+ config: Optional project configuration
1543
+
1544
+ Returns:
1545
+ BaseScorer instance appropriate for the language
1546
+
1547
+ Raises:
1548
+ ValueError: If no scorer is available for the language (even with fallbacks)
1549
+ """
1550
+ from .scorer_registry import ScorerRegistry
1551
+
1552
+ try:
1553
+ return ScorerRegistry.get_scorer(language, config)
1554
+ except ValueError:
1555
+ # If no scorer found, fall back to Python scorer as last resort
1556
+ # This maintains backward compatibility but may not work well for non-Python code
1557
+ # TODO: In the future, create a GenericScorer that uses metric strategies
1558
+ if language != Language.PYTHON:
1559
+ # Try Python scorer as absolute last resort
1560
+ try:
1561
+ return ScorerRegistry.get_scorer(Language.PYTHON, config)
1562
+ except ValueError:
1563
+ pass
1564
+
1565
+ # If even Python scorer isn't available, raise the original error
1566
+ raise