attocode-code-intel 0.1.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 (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. attocode_intel/workers/settings.py +61 -0
@@ -0,0 +1,712 @@
1
+ """Synthesis helpers for the code-intel MCP server.
2
+
3
+ Contains data classes, constants, and pure-logic helpers used by
4
+ multiple tool modules. Extracted from server.py for maintainability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ from collections import Counter
12
+ from dataclasses import dataclass, field
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Constants
16
+ # ---------------------------------------------------------------------------
17
+
18
+ FRAMEWORK_PATTERNS: dict[str, str] = {
19
+ "flask": "Flask",
20
+ "django": "Django",
21
+ "fastapi": "FastAPI",
22
+ "starlette": "Starlette",
23
+ "uvicorn": "Uvicorn",
24
+ "gunicorn": "Gunicorn",
25
+ "celery": "Celery",
26
+ "sqlalchemy": "SQLAlchemy",
27
+ "pydantic": "Pydantic",
28
+ "pytest": "pytest",
29
+ "unittest": "unittest",
30
+ "click": "Click",
31
+ "typer": "Typer",
32
+ "textual": "Textual",
33
+ "rich": "Rich",
34
+ "httpx": "httpx",
35
+ "requests": "requests",
36
+ "aiohttp": "aiohttp",
37
+ "asyncio": "asyncio",
38
+ "numpy": "NumPy",
39
+ "pandas": "pandas",
40
+ "torch": "PyTorch",
41
+ "tensorflow": "TensorFlow",
42
+ "transformers": "Transformers",
43
+ "anthropic": "Anthropic SDK",
44
+ "openai": "OpenAI SDK",
45
+ "mcp": "MCP",
46
+ "react": "React",
47
+ "express": "Express",
48
+ "next": "Next.js",
49
+ "vue": "Vue",
50
+ "angular": "Angular",
51
+ "svelte": "Svelte",
52
+ "dataclasses": "dataclasses",
53
+ "attrs": "attrs",
54
+ "marshmallow": "Marshmallow",
55
+ "alembic": "Alembic",
56
+ "boto3": "AWS SDK (boto3)",
57
+ "google.cloud": "Google Cloud SDK",
58
+ }
59
+
60
+ _SNAKE_RE = re.compile(r"^[a-z_][a-z0-9_]*$")
61
+ _CAMEL_RE = re.compile(r"^[a-z][a-zA-Z0-9]*$")
62
+
63
+ _ENTRY_BASENAMES = {
64
+ "main.py", "cli.py", "app.py", "server.py",
65
+ "index.ts", "index.js", "main.ts", "main.go", "lib.rs",
66
+ }
67
+ _CONFIG_BASENAMES = {
68
+ "pyproject.toml", "package.json", "Cargo.toml",
69
+ "setup.py", "setup.cfg", "go.mod",
70
+ }
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Data classes
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ @dataclass(slots=True)
79
+ class _FileMetrics:
80
+ path: str
81
+ line_count: int = 0
82
+ symbol_count: int = 0
83
+ public_symbols: int = 0
84
+ fan_in: int = 0
85
+ fan_out: int = 0
86
+ density: float = 0.0
87
+ composite: float = 0.0
88
+ categories: list[str] = field(default_factory=list)
89
+
90
+
91
+ @dataclass(slots=True)
92
+ class _FunctionMetrics:
93
+ file_path: str
94
+ name: str
95
+ line_count: int
96
+ param_count: int
97
+ is_public: bool
98
+ has_return_type: bool
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Helper functions
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ def _detect_project_name(project_dir: str) -> str:
107
+ """Detect project name from config files or directory basename."""
108
+ for cfg_name, pattern in [
109
+ ("pyproject.toml", r'name\s*=\s*"([^"]+)"'),
110
+ ("package.json", r'"name"\s*:\s*"([^"]+)"'),
111
+ ("Cargo.toml", r'name\s*=\s*"([^"]+)"'),
112
+ ("setup.cfg", r'name\s*=\s*(.+)'),
113
+ ]:
114
+ cfg_path = os.path.join(project_dir, cfg_name)
115
+ if os.path.isfile(cfg_path):
116
+ try:
117
+ with open(cfg_path, encoding="utf-8", errors="replace") as f:
118
+ content = f.read(4096)
119
+ m = re.search(pattern, content)
120
+ if m:
121
+ return m.group(1).strip()
122
+ except OSError:
123
+ pass
124
+ return os.path.basename(os.path.abspath(project_dir))
125
+
126
+
127
+ def _find_entry_points(files: list, index) -> list[tuple[str, str]]:
128
+ """Find likely entry points. Returns list of (path, reason)."""
129
+ results: list[tuple[str, str]] = []
130
+ seen = set()
131
+ for fi in files:
132
+ rel = fi.relative_path
133
+ basename = os.path.basename(rel)
134
+ if fi.importance >= 0.8 and rel not in seen:
135
+ results.append((rel, f"importance={fi.importance:.1f}"))
136
+ seen.add(rel)
137
+ elif basename in _ENTRY_BASENAMES and rel not in seen:
138
+ results.append((rel, "entry filename"))
139
+ seen.add(rel)
140
+ return results[:15]
141
+
142
+
143
+ def _find_hub_files(files: list, index, top_n: int = 10) -> list[tuple[str, int, int]]:
144
+ """Find most-depended-on files. Returns (path, fan_in, fan_out)."""
145
+ metrics = []
146
+ for fi in files:
147
+ rel = fi.relative_path
148
+ fan_in = len(index.file_dependents.get(rel, set()))
149
+ fan_out = len(index.file_dependencies.get(rel, set()))
150
+ if fan_in > 0:
151
+ metrics.append((rel, fan_in, fan_out))
152
+ metrics.sort(key=lambda x: x[1], reverse=True)
153
+ return metrics[:top_n]
154
+
155
+
156
+ def _summarize_directories(files: list) -> list[tuple[str, int, int]]:
157
+ """Group files by top-level directory. Returns (dir, file_count, total_lines)."""
158
+ dir_stats: dict[str, tuple[int, int]] = {}
159
+ for fi in files:
160
+ parts = fi.relative_path.split("/")
161
+ top_dir = parts[0] if len(parts) > 1 else "(root)"
162
+ count, lines = dir_stats.get(top_dir, (0, 0))
163
+ dir_stats[top_dir] = (count + 1, lines + fi.line_count)
164
+ result = [(d, c, loc) for d, (c, loc) in dir_stats.items()]
165
+ result.sort(key=lambda x: x[1], reverse=True)
166
+ return result
167
+
168
+
169
+ def _classify_layers(files: list, index) -> dict[str, list[str]]:
170
+ """Classify non-test files into dependency layers."""
171
+ layers: dict[str, list[str]] = {"foundation": [], "integration": [], "leaf": []}
172
+ for fi in files:
173
+ if fi.is_test or fi.is_config:
174
+ continue
175
+ rel = fi.relative_path
176
+ fan_in = len(index.file_dependents.get(rel, set()))
177
+ fan_out = len(index.file_dependencies.get(rel, set()))
178
+ if fan_in >= 5 and fan_out <= 3:
179
+ layers["foundation"].append(rel)
180
+ elif fan_in >= 2 and fan_out >= 2:
181
+ layers["integration"].append(rel)
182
+ elif fan_in <= 1 and fan_out >= 3:
183
+ layers["leaf"].append(rel)
184
+ return layers
185
+
186
+
187
+ def _detect_tech_stack(ast_cache: dict) -> list[str]:
188
+ """Detect frameworks/libraries from import statements."""
189
+ found: Counter = Counter()
190
+ for file_ast in ast_cache.values():
191
+ for imp in file_ast.imports:
192
+ mod = imp.module.split(".")[0] if imp.module else ""
193
+ if mod in FRAMEWORK_PATTERNS:
194
+ found[FRAMEWORK_PATTERNS[mod]] += 1
195
+ # Return sorted by frequency
196
+ return [name for name, _ in found.most_common()]
197
+
198
+
199
+ def _detect_build_system(files: list) -> str:
200
+ """Detect build system from config file presence."""
201
+ basenames = {os.path.basename(fi.relative_path) for fi in files}
202
+ systems = []
203
+ if "pyproject.toml" in basenames:
204
+ systems.append("pyproject.toml")
205
+ if "setup.py" in basenames or "setup.cfg" in basenames:
206
+ systems.append("setuptools")
207
+ if "package.json" in basenames:
208
+ systems.append("npm/node")
209
+ if "Cargo.toml" in basenames:
210
+ systems.append("Cargo (Rust)")
211
+ if "go.mod" in basenames:
212
+ systems.append("Go modules")
213
+ if "Makefile" in basenames or "makefile" in basenames:
214
+ systems.append("Make")
215
+ if "CMakeLists.txt" in basenames:
216
+ systems.append("CMake")
217
+ if "Dockerfile" in basenames:
218
+ systems.append("Docker")
219
+ return ", ".join(systems) if systems else "unknown"
220
+
221
+
222
+ def _is_snake_case(name: str) -> bool:
223
+ return bool(_SNAKE_RE.match(name)) and "_" in name
224
+
225
+
226
+ def _is_camel_case(name: str) -> bool:
227
+ return bool(_CAMEL_RE.match(name)) and name != name.lower()
228
+
229
+
230
+ def _percentile_ranks(values: list[float]) -> list[float]:
231
+ """Return percentile rank (0.0-1.0) for each value in the input list.
232
+
233
+ Uses average-rank method for ties. Returns all zeros for empty/single-value lists.
234
+ """
235
+ n = len(values)
236
+ if n <= 1:
237
+ return [0.0] * n
238
+ indexed = sorted(range(n), key=lambda i: values[i])
239
+ ranks = [0.0] * n
240
+ i = 0
241
+ while i < n:
242
+ j = i
243
+ while j < n - 1 and values[indexed[j + 1]] == values[indexed[i]]:
244
+ j += 1
245
+ avg_rank = (i + j) / 2.0
246
+ for k in range(i, j + 1):
247
+ ranks[indexed[k]] = avg_rank / (n - 1) if n > 1 else 0.0
248
+ i = j + 1
249
+ return ranks
250
+
251
+
252
+ def _compute_function_hotspots(ast_cache: dict, top_n: int = 10) -> list[_FunctionMetrics]:
253
+ """Find the longest/most complex functions across the codebase."""
254
+ all_fns: list[_FunctionMetrics] = []
255
+ for rel, file_ast in ast_cache.items():
256
+ for fn in file_ast.functions:
257
+ lc = fn.end_line - fn.start_line + 1
258
+ all_fns.append(_FunctionMetrics(
259
+ file_path=rel,
260
+ name=fn.name,
261
+ line_count=lc,
262
+ param_count=len(fn.parameters),
263
+ is_public=fn.visibility == "public",
264
+ has_return_type=bool(fn.return_type),
265
+ ))
266
+ for cls in file_ast.classes:
267
+ for method in cls.methods:
268
+ lc = method.end_line - method.start_line + 1
269
+ all_fns.append(_FunctionMetrics(
270
+ file_path=rel,
271
+ name=f"{cls.name}.{method.name}",
272
+ line_count=lc,
273
+ param_count=len(method.parameters),
274
+ is_public=method.visibility == "public",
275
+ has_return_type=bool(method.return_type),
276
+ ))
277
+
278
+ def _composite(f: _FunctionMetrics) -> float:
279
+ return (f.line_count / 50) * 0.5 + f.param_count * 0.3 + (0.0 if f.has_return_type else 0.2)
280
+
281
+ all_fns.sort(key=_composite, reverse=True)
282
+ return all_fns[:top_n]
283
+
284
+
285
+ def _get_churn_scores(project_dir: str, files: list) -> dict[str, float] | None:
286
+ """Compute churn scores for files via TemporalCouplingAnalyzer.
287
+
288
+ Returns a dict of relative_path -> churn_score (0.0-1.0), or None
289
+ if temporal coupling is unavailable (e.g. not a git repo).
290
+ """
291
+ try:
292
+ from attocode_intel._internal.integrations.context.temporal_coupling import (
293
+ TemporalCouplingAnalyzer,
294
+ )
295
+
296
+ analyzer = TemporalCouplingAnalyzer(project_dir=project_dir)
297
+ scores: dict[str, float] = {}
298
+ for fi in files:
299
+ score = analyzer.get_churn_score(fi.relative_path, days=90)
300
+ if score > 0:
301
+ scores[fi.relative_path] = score
302
+ return scores if scores else None
303
+ except Exception:
304
+ return None
305
+
306
+
307
+ def _compute_file_metrics(
308
+ files: list,
309
+ index,
310
+ ast_cache: dict,
311
+ churn_scores: dict[str, float] | None = None,
312
+ ) -> list[_FileMetrics]:
313
+ """Compute composite metrics for all files using percentile-based scoring.
314
+
315
+ Args:
316
+ files: List of FileInfo objects.
317
+ index: CrossRefIndex instance.
318
+ ast_cache: Dict of relative_path -> FileAST.
319
+ churn_scores: Optional dict of relative_path -> churn_score (0.0-1.0).
320
+ When provided, churn is included in the composite score.
321
+ """
322
+ # First pass: collect raw values for all eligible files
323
+ raw: list[dict] = []
324
+ for fi in files:
325
+ if fi.is_config:
326
+ continue
327
+ rel = fi.relative_path
328
+ lc = fi.line_count
329
+ if lc == 0:
330
+ continue
331
+
332
+ file_ast = ast_cache.get(rel)
333
+ if file_ast is not None:
334
+ sym_count = file_ast.symbol_count
335
+ # Count public symbols (visibility == "public")
336
+ pub = sum(1 for fn in file_ast.functions if fn.visibility == "public")
337
+ pub += sum(1 for cls in file_ast.classes if not cls.name.startswith("_"))
338
+ else:
339
+ sym_count = len(index.file_symbols.get(rel, set()))
340
+ pub = sym_count # assume all public without AST
341
+
342
+ fan_in = len(index.file_dependents.get(rel, set()))
343
+ fan_out = len(index.file_dependencies.get(rel, set()))
344
+ density = sym_count / lc * 100 if lc > 0 else 0.0
345
+ churn = churn_scores.get(rel, 0.0) if churn_scores else 0.0
346
+
347
+ raw.append({
348
+ "path": rel,
349
+ "line_count": lc,
350
+ "symbol_count": sym_count,
351
+ "public_symbols": pub,
352
+ "fan_in": fan_in,
353
+ "fan_out": fan_out,
354
+ "density": density,
355
+ "churn": churn,
356
+ })
357
+
358
+ if not raw:
359
+ return []
360
+
361
+ has_churn = churn_scores is not None and any(r["churn"] > 0 for r in raw)
362
+
363
+ # Second pass: compute percentile ranks for each metric
364
+ lines_pct = _percentile_ranks([r["line_count"] for r in raw])
365
+ sym_pct = _percentile_ranks([r["symbol_count"] for r in raw])
366
+ fan_in_pct = _percentile_ranks([r["fan_in"] for r in raw])
367
+ fan_out_pct = _percentile_ranks([r["fan_out"] for r in raw])
368
+ density_pct = _percentile_ranks([r["density"] for r in raw])
369
+ churn_pct = _percentile_ranks([r["churn"] for r in raw]) if has_churn else [0.0] * len(raw)
370
+
371
+ # Adaptive thresholds: P90 with minimum floors
372
+ n = len(raw)
373
+ p90_idx = int(0.9 * (n - 1))
374
+
375
+ sorted_lines = sorted(r["line_count"] for r in raw)
376
+ sorted_syms = sorted(r["symbol_count"] for r in raw)
377
+ sorted_pub = sorted(r["public_symbols"] for r in raw)
378
+ sorted_fan_in = sorted(r["fan_in"] for r in raw)
379
+ sorted_fan_out = sorted(r["fan_out"] for r in raw)
380
+
381
+ thresh_lines = max(sorted_lines[p90_idx], 200)
382
+ thresh_syms = max(sorted_syms[p90_idx], 10)
383
+ thresh_pub = max(sorted_pub[p90_idx], 10)
384
+ thresh_fan_in = max(sorted_fan_in[p90_idx], 3)
385
+ thresh_fan_out = max(sorted_fan_out[p90_idx], 5)
386
+
387
+ # Third pass: build results with percentile composite and adaptive categories
388
+ # Weights: with churn data, rebalance to include temporal dimension
389
+ if has_churn:
390
+ w_lines, w_sym, w_fan_in, w_fan_out, w_density, w_churn = (
391
+ 0.20, 0.15, 0.25, 0.10, 0.10, 0.20,
392
+ )
393
+ else:
394
+ w_lines, w_sym, w_fan_in, w_fan_out, w_density, w_churn = (
395
+ 0.25, 0.20, 0.30, 0.15, 0.10, 0.0,
396
+ )
397
+
398
+ results: list[_FileMetrics] = []
399
+ for i, r in enumerate(raw):
400
+ composite = (
401
+ lines_pct[i] * w_lines
402
+ + sym_pct[i] * w_sym
403
+ + fan_in_pct[i] * w_fan_in
404
+ + fan_out_pct[i] * w_fan_out
405
+ + density_pct[i] * w_density
406
+ + churn_pct[i] * w_churn
407
+ )
408
+
409
+ cats: list[str] = []
410
+ if r["line_count"] >= thresh_lines and r["symbol_count"] >= thresh_syms:
411
+ cats.append("god-file")
412
+ if r["fan_in"] >= thresh_fan_in:
413
+ cats.append("hub")
414
+ if r["fan_out"] >= thresh_fan_out:
415
+ cats.append("coupling-magnet")
416
+ if r["public_symbols"] >= thresh_pub:
417
+ cats.append("wide-api")
418
+ if has_churn and churn_pct[i] >= 0.9:
419
+ cats.append("churn-hotspot")
420
+
421
+ results.append(_FileMetrics(
422
+ path=r["path"],
423
+ line_count=r["line_count"],
424
+ symbol_count=r["symbol_count"],
425
+ public_symbols=r["public_symbols"],
426
+ fan_in=r["fan_in"],
427
+ fan_out=r["fan_out"],
428
+ density=round(r["density"], 1),
429
+ composite=round(composite, 3),
430
+ categories=cats,
431
+ ))
432
+ return results
433
+
434
+
435
+ def _analyze_conventions(ast_cache: dict, sample_rels: list[str]) -> dict:
436
+ """Analyze coding conventions across sampled files."""
437
+ stats: dict = {
438
+ "total_functions": 0,
439
+ "snake_names": 0,
440
+ "camel_names": 0,
441
+ "typed_return": 0,
442
+ "typed_params": 0,
443
+ "total_params": 0,
444
+ "has_docstring_fn": 0,
445
+ "has_docstring_cls": 0,
446
+ "total_classes": 0,
447
+ "async_count": 0,
448
+ "from_imports": 0,
449
+ "plain_imports": 0,
450
+ "relative_imports": 0,
451
+ "total_imports": 0,
452
+ "decorator_counts": Counter(),
453
+ "dataclass_count": 0,
454
+ "slots_dataclass_count": 0,
455
+ "frozen_dataclass_count": 0,
456
+ "abstract_count": 0,
457
+ "base_classes": Counter(),
458
+ "file_sizes": [],
459
+ "files_per_dir": Counter(),
460
+ "exception_classes": [], # list of (name, bases)
461
+ "all_exports_count": 0,
462
+ "private_functions": 0,
463
+ "staticmethod_count": 0,
464
+ "classmethod_count": 0,
465
+ "property_count": 0,
466
+ }
467
+
468
+ for rel in sample_rels:
469
+ file_ast = ast_cache.get(rel)
470
+ if file_ast is None:
471
+ continue
472
+
473
+ stats["file_sizes"].append(file_ast.line_count)
474
+ parts = rel.split("/")
475
+ dir_name = parts[0] if len(parts) > 1 else "(root)"
476
+ stats["files_per_dir"][dir_name] += 1
477
+
478
+ # Check for __all__ in top-level vars
479
+ if "__all__" in file_ast.top_level_vars:
480
+ stats["all_exports_count"] += 1
481
+
482
+ # Functions
483
+ all_fns = list(file_ast.functions)
484
+ for cls in file_ast.classes:
485
+ all_fns.extend(cls.methods)
486
+
487
+ for fn in all_fns:
488
+ stats["total_functions"] += 1
489
+ if _is_snake_case(fn.name):
490
+ stats["snake_names"] += 1
491
+ elif _is_camel_case(fn.name):
492
+ stats["camel_names"] += 1
493
+ if fn.return_type:
494
+ stats["typed_return"] += 1
495
+ for p in fn.parameters:
496
+ stats["total_params"] += 1
497
+ if p.type_annotation:
498
+ stats["typed_params"] += 1
499
+ if fn.docstring:
500
+ stats["has_docstring_fn"] += 1
501
+ if fn.is_async:
502
+ stats["async_count"] += 1
503
+ if fn.visibility != "public":
504
+ stats["private_functions"] += 1
505
+ if fn.is_staticmethod:
506
+ stats["staticmethod_count"] += 1
507
+ if fn.is_classmethod:
508
+ stats["classmethod_count"] += 1
509
+ if fn.is_property:
510
+ stats["property_count"] += 1
511
+ for dec in fn.decorators:
512
+ stats["decorator_counts"][dec] += 1
513
+
514
+ # Classes
515
+ for cls in file_ast.classes:
516
+ stats["total_classes"] += 1
517
+ if cls.docstring:
518
+ stats["has_docstring_cls"] += 1
519
+ if cls.is_abstract:
520
+ stats["abstract_count"] += 1
521
+ for dec in cls.decorators:
522
+ stats["decorator_counts"][dec] += 1
523
+ if "dataclass" in dec:
524
+ stats["dataclass_count"] += 1
525
+ if "slots=True" in dec:
526
+ stats["slots_dataclass_count"] += 1
527
+ if "frozen=True" in dec:
528
+ stats["frozen_dataclass_count"] += 1
529
+ for base in cls.bases:
530
+ if base not in ("object",):
531
+ stats["base_classes"][base] += 1
532
+ # Detect exception subclasses
533
+ is_exception = any(
534
+ "Exception" in b or "Error" in b
535
+ for b in cls.bases
536
+ )
537
+ if is_exception:
538
+ stats["exception_classes"].append((cls.name, list(cls.bases)))
539
+
540
+ # Imports
541
+ for imp in file_ast.imports:
542
+ stats["total_imports"] += 1
543
+ if imp.is_from:
544
+ stats["from_imports"] += 1
545
+ if imp.module.startswith("."):
546
+ stats["relative_imports"] += 1
547
+ else:
548
+ stats["plain_imports"] += 1
549
+
550
+ return stats
551
+
552
+
553
+ def _format_conventions(stats: dict, dir_stats: dict[str, dict] | None = None) -> str:
554
+ """Format convention stats into readable output."""
555
+ sections: list[str] = []
556
+
557
+ # Naming
558
+ total_named = stats["snake_names"] + stats["camel_names"]
559
+ if total_named > 0:
560
+ snake_pct = stats["snake_names"] / total_named * 100
561
+ sections.append(
562
+ f"Naming: {snake_pct:.0f}% snake_case, {100 - snake_pct:.0f}% camelCase "
563
+ f"({total_named} functions analyzed)"
564
+ )
565
+
566
+ # Type hints
567
+ total_fn = stats["total_functions"]
568
+ if total_fn > 0:
569
+ ret_pct = stats["typed_return"] / total_fn * 100
570
+ lines = [f"Type hints: {ret_pct:.0f}% functions have return types"]
571
+ if stats["total_params"] > 0:
572
+ param_pct = stats["typed_params"] / stats["total_params"] * 100
573
+ lines.append(f" {param_pct:.0f}% parameters have type annotations")
574
+ sections.append("\n".join(lines))
575
+
576
+ # Docstrings
577
+ if total_fn > 0:
578
+ fn_pct = stats["has_docstring_fn"] / total_fn * 100
579
+ line = f"Docstrings: {fn_pct:.0f}% of functions"
580
+ if stats["total_classes"] > 0:
581
+ cls_pct = stats["has_docstring_cls"] / stats["total_classes"] * 100
582
+ line += f", {cls_pct:.0f}% of classes"
583
+ sections.append(line)
584
+
585
+ # Visibility
586
+ if total_fn > 0 and stats.get("private_functions", 0) > 0:
587
+ pub_count = total_fn - stats["private_functions"]
588
+ pub_pct = pub_count / total_fn * 100
589
+ priv_pct = stats["private_functions"] / total_fn * 100
590
+ sections.append(f"Visibility: {pub_pct:.0f}% public, {priv_pct:.0f}% private")
591
+
592
+ # Method types
593
+ method_parts = []
594
+ if stats.get("staticmethod_count", 0) > 0:
595
+ method_parts.append(f"{stats['staticmethod_count']} @staticmethod")
596
+ if stats.get("classmethod_count", 0) > 0:
597
+ method_parts.append(f"{stats['classmethod_count']} @classmethod")
598
+ if stats.get("property_count", 0) > 0:
599
+ method_parts.append(f"{stats['property_count']} @property")
600
+ if method_parts:
601
+ sections.append("Method types: " + ", ".join(method_parts))
602
+
603
+ # Async
604
+ if total_fn > 0 and stats["async_count"] > 0:
605
+ async_pct = stats["async_count"] / total_fn * 100
606
+ async_n = stats["async_count"]
607
+ sections.append(
608
+ f"Async: {async_pct:.0f}% of functions are async"
609
+ f" ({async_n}/{total_fn})"
610
+ )
611
+
612
+ # Imports
613
+ total_imp = stats["total_imports"]
614
+ if total_imp > 0:
615
+ from_pct = stats["from_imports"] / total_imp * 100
616
+ line = f"Imports: {from_pct:.0f}% use 'from X import Y' style"
617
+ if stats["relative_imports"] > 0:
618
+ rel_pct = stats["relative_imports"] / total_imp * 100
619
+ line += f", {rel_pct:.0f}% relative imports"
620
+ sections.append(line)
621
+
622
+ # Module exports (__all__)
623
+ if stats.get("all_exports_count", 0) > 0:
624
+ sections.append(f"Module exports: {stats['all_exports_count']} files define __all__")
625
+
626
+ # Decorators
627
+ top_decs = stats["decorator_counts"].most_common(8)
628
+ if top_decs:
629
+ dec_lines = ["Popular decorators:"]
630
+ for dec, count in top_decs:
631
+ dec_lines.append(f" @{dec} ({count}x)")
632
+ sections.append("\n".join(dec_lines))
633
+
634
+ # Class patterns
635
+ cls_lines = []
636
+ if stats["dataclass_count"] > 0:
637
+ dc_line = f" dataclasses: {stats['dataclass_count']}"
638
+ extras = []
639
+ if stats.get("slots_dataclass_count", 0) > 0:
640
+ extras.append(f"{stats['slots_dataclass_count']} with slots=True")
641
+ if stats.get("frozen_dataclass_count", 0) > 0:
642
+ extras.append(f"{stats['frozen_dataclass_count']} frozen")
643
+ if extras:
644
+ dc_line += f" ({', '.join(extras)})"
645
+ cls_lines.append(dc_line)
646
+ if stats["abstract_count"] > 0:
647
+ cls_lines.append(f" abstract classes: {stats['abstract_count']}")
648
+ top_bases = stats["base_classes"].most_common(5)
649
+ if top_bases:
650
+ cls_lines.append(" common bases: " + ", ".join(f"{b} ({c}x)" for b, c in top_bases))
651
+ if cls_lines:
652
+ sections.append("Class patterns:\n" + "\n".join(cls_lines))
653
+
654
+ # Error hierarchy
655
+ exc_classes = stats.get("exception_classes", [])
656
+ if exc_classes:
657
+ exc_line = f"Error hierarchy: {len(exc_classes)} exception classes"
658
+ # Find root exceptions (those whose bases don't appear as names of other exceptions)
659
+ exc_names = {name for name, _ in exc_classes}
660
+ roots = [name for name, bases in exc_classes if not any(b in exc_names for b in bases)]
661
+ subtypes = [name for name, bases in exc_classes if any(b in exc_names for b in bases)]
662
+ if roots:
663
+ exc_line += f", root: {', '.join(roots[:3])}"
664
+ if subtypes:
665
+ exc_line += f", subtypes: {', '.join(subtypes[:5])}"
666
+ if len(subtypes) > 5:
667
+ exc_line += f" (+{len(subtypes) - 5} more)"
668
+ sections.append(exc_line)
669
+
670
+ # Module organization
671
+ sizes = stats["file_sizes"]
672
+ if sizes:
673
+ avg_size = sum(sizes) / len(sizes)
674
+ sorted_sizes = sorted(sizes)
675
+ median_size = sorted_sizes[len(sorted_sizes) // 2]
676
+ sections.append(
677
+ f"Module size: avg {avg_size:.0f} lines, median {median_size}"
678
+ f" lines ({len(sizes)} files)"
679
+ )
680
+
681
+ # Per-directory convention divergence
682
+ if dir_stats:
683
+ global_type_pct = (
684
+ stats["typed_return"] / total_fn * 100 if total_fn > 0 else 0
685
+ )
686
+ global_doc_pct = (
687
+ stats["has_docstring_fn"] / total_fn * 100 if total_fn > 0 else 0
688
+ )
689
+ divergences: list[str] = []
690
+ for dirname, ds in sorted(dir_stats.items()):
691
+ dir_fn = ds.get("total_functions", 0)
692
+ if dir_fn < 3:
693
+ continue
694
+ dir_type_pct = ds.get("typed_return", 0) / dir_fn * 100
695
+ dir_doc_pct = ds.get("has_docstring_fn", 0) / dir_fn * 100
696
+ parts = []
697
+ if abs(dir_type_pct - global_type_pct) > 20:
698
+ parts.append(
699
+ f"{dir_type_pct:.0f}% type hints"
700
+ f" vs {global_type_pct:.0f}% project-wide"
701
+ )
702
+ if abs(dir_doc_pct - global_doc_pct) > 20:
703
+ parts.append(
704
+ f"{dir_doc_pct:.0f}% docstrings"
705
+ f" vs {global_doc_pct:.0f}% project-wide"
706
+ )
707
+ if parts:
708
+ divergences.append(f" {dirname}/: " + "; ".join(parts))
709
+ if divergences:
710
+ sections.append("Convention divergence:\n" + "\n".join(divergences))
711
+
712
+ return "\n\n".join(sections) if sections else "No conventions detected."