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,1106 @@
1
+ """Service-mode providers — query Postgres DB directly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import uuid
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+
13
+ from attocode_intel.api.deps import BranchContext
14
+
15
+ from attocode_intel.api.models import (
16
+ CodeChunkItem,
17
+ CommunityItem,
18
+ CommunityResponse,
19
+ ConventionsResponse,
20
+ ConventionStats,
21
+ CrossRefResponse,
22
+ DependencyGraphEdge,
23
+ DependencyGraphNode,
24
+ DependencyGraphResponse,
25
+ DependencyResponse,
26
+ FileAnalysisResponse,
27
+ FileMetricsItem,
28
+ HotspotsResponse,
29
+ ImpactAnalysisResponse,
30
+ ReferenceItem,
31
+ SearchResultItem,
32
+ SearchResultsResponse,
33
+ SecurityFinding,
34
+ SecurityScanResponse,
35
+ SymbolItem,
36
+ SymbolListResponse,
37
+ SymbolSearchResponse,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class DbAnalysisProvider:
44
+ """DB-backed analysis provider for service mode."""
45
+
46
+ __slots__ = ("_project_id",)
47
+
48
+ def __init__(self, project_id: str) -> None:
49
+ self._project_id = project_id
50
+
51
+ async def symbols(self, path: str, branch: str) -> SymbolListResponse:
52
+ from sqlalchemy import select
53
+
54
+ from attocode_intel.api.deps import get_branch_context
55
+ from attocode_intel.db.engine import get_session
56
+ from attocode_intel.db.models import Symbol
57
+
58
+ async for session in get_session():
59
+ repo_id = uuid.UUID(self._project_id)
60
+ branch_ctx = await get_branch_context(repo_id, branch, session)
61
+ manifest = branch_ctx.manifest
62
+
63
+ if path:
64
+ content_sha = manifest.get(path)
65
+ if not content_sha:
66
+ return SymbolListResponse(path=path, symbols=[])
67
+ result = await session.execute(
68
+ select(Symbol).where(Symbol.content_sha == content_sha)
69
+ )
70
+ else:
71
+ shas = set(manifest.values())
72
+ if not shas:
73
+ return SymbolListResponse(path="", symbols=[])
74
+ result = await session.execute(
75
+ select(Symbol).where(Symbol.content_sha.in_(shas))
76
+ )
77
+
78
+ rows = result.scalars().all()
79
+ sha_to_path = branch_ctx.sha_to_path
80
+ items = [
81
+ SymbolItem(
82
+ name=s.name,
83
+ kind=s.kind,
84
+ file_path=sha_to_path.get(s.content_sha, ""),
85
+ start_line=s.line_start or 0,
86
+ end_line=s.line_end or 0,
87
+ signature=s.signature or "",
88
+ )
89
+ for s in rows
90
+ ]
91
+ return SymbolListResponse(path=path, symbols=items)
92
+
93
+ return SymbolListResponse(path=path, symbols=[])
94
+
95
+ async def search_symbols(self, name: str, branch: str, directory: str = "") -> SymbolSearchResponse:
96
+ from sqlalchemy import select
97
+
98
+ from attocode_intel.api.deps import get_branch_context
99
+ from attocode_intel.db.engine import get_session
100
+ from attocode_intel.db.models import Symbol
101
+
102
+ async for session in get_session():
103
+ repo_id = uuid.UUID(self._project_id)
104
+ branch_ctx = await get_branch_context(repo_id, branch, session)
105
+ shas = branch_ctx.content_shas
106
+ if not shas:
107
+ return SymbolSearchResponse(query=name, definitions=[])
108
+
109
+ result = await session.execute(
110
+ select(Symbol)
111
+ .where(
112
+ Symbol.content_sha.in_(shas),
113
+ Symbol.name.ilike(f"%{name}%"),
114
+ )
115
+ .limit(100)
116
+ )
117
+ rows = result.scalars().all()
118
+ sha_to_path = branch_ctx.sha_to_path
119
+ items = [
120
+ SymbolItem(
121
+ name=s.name,
122
+ kind=s.kind,
123
+ file_path=sha_to_path.get(s.content_sha, ""),
124
+ start_line=s.line_start or 0,
125
+ end_line=s.line_end or 0,
126
+ signature=s.signature or "",
127
+ )
128
+ for s in rows
129
+ ]
130
+ if directory:
131
+ items = [s for s in items if s.file_path.startswith(directory)]
132
+ return SymbolSearchResponse(query=name, definitions=items)
133
+
134
+ return SymbolSearchResponse(query=name, definitions=[])
135
+
136
+ async def cross_references(self, symbol_name: str, branch: str) -> CrossRefResponse:
137
+ from sqlalchemy import or_, select
138
+
139
+ from attocode_intel.api.deps import get_branch_context
140
+ from attocode_intel.db.engine import get_session
141
+ from attocode_intel.db.models import Symbol, SymbolReference
142
+
143
+ async for session in get_session():
144
+ repo_id = uuid.UUID(self._project_id)
145
+ branch_ctx = await get_branch_context(repo_id, branch, session)
146
+ content_shas = branch_ctx.content_shas
147
+ if not content_shas:
148
+ return CrossRefResponse(
149
+ symbol=symbol_name, definitions=[], references=[], total_references=0
150
+ )
151
+
152
+ result = await session.execute(
153
+ select(Symbol).where(Symbol.content_sha.in_(content_shas))
154
+ )
155
+ rows = result.scalars().all()
156
+ sha_to_path = branch_ctx.sha_to_path
157
+
158
+ definitions = [
159
+ SymbolItem(
160
+ name=s.name,
161
+ kind=s.kind,
162
+ file_path=sha_to_path.get(s.content_sha, ""),
163
+ start_line=s.line_start or 0,
164
+ end_line=s.line_end or 0,
165
+ signature=s.signature or "",
166
+ )
167
+ for s in rows
168
+ if s.name == symbol_name or s.name.endswith(f".{symbol_name}")
169
+ ]
170
+
171
+ ref_result = await session.execute(
172
+ select(SymbolReference)
173
+ .where(
174
+ SymbolReference.content_sha.in_(content_shas),
175
+ or_(
176
+ SymbolReference.symbol_name == symbol_name,
177
+ SymbolReference.symbol_name.endswith(f".{symbol_name}"),
178
+ ),
179
+ )
180
+ .limit(500)
181
+ )
182
+ ref_rows = ref_result.scalars().all()
183
+ references = [
184
+ ReferenceItem(
185
+ ref_kind=r.ref_kind,
186
+ file_path=sha_to_path.get(r.content_sha, ""),
187
+ line=r.line,
188
+ )
189
+ for r in ref_rows
190
+ ]
191
+
192
+ return CrossRefResponse(
193
+ symbol=symbol_name,
194
+ definitions=definitions,
195
+ references=references,
196
+ total_references=len(references),
197
+ )
198
+
199
+ return CrossRefResponse(
200
+ symbol=symbol_name, definitions=[], references=[], total_references=0
201
+ )
202
+
203
+ async def impact_analysis(self, files: list[str], branch: str) -> ImpactAnalysisResponse:
204
+ from attocode_intel.api.deps import get_branch_context
205
+ from attocode_intel.api.models import ImpactLayer
206
+ from attocode_intel.db.engine import get_session
207
+ from attocode_intel.storage.dependency_store import DependencyStore
208
+
209
+ async for session in get_session():
210
+ repo_id = uuid.UUID(self._project_id)
211
+ branch_ctx = await get_branch_context(repo_id, branch, session)
212
+ manifest = branch_ctx.manifest
213
+ sha_to_path = branch_ctx.sha_to_path
214
+ dep_store = DependencyStore(session)
215
+
216
+ changed_shas: set[str] = set()
217
+ for f in files:
218
+ sha = manifest.get(f)
219
+ if sha:
220
+ changed_shas.add(sha)
221
+
222
+ visited: set[str] = set(changed_shas)
223
+ frontier = set(changed_shas)
224
+ layers: list[ImpactLayer] = []
225
+ current_depth = 0
226
+ while frontier:
227
+ current_depth += 1
228
+ next_frontier: set[str] = set()
229
+ for sha in frontier:
230
+ reverse = await dep_store.get_reverse(sha)
231
+ for r in reverse:
232
+ src = r["source_sha"]
233
+ if src not in visited and src in branch_ctx.content_shas:
234
+ visited.add(src)
235
+ next_frontier.add(src)
236
+ if next_frontier:
237
+ layer_files = sorted(
238
+ sha_to_path[sha]
239
+ for sha in next_frontier
240
+ if sha in sha_to_path
241
+ )
242
+ if layer_files:
243
+ layers.append(ImpactLayer(depth=current_depth, files=layer_files))
244
+ frontier = next_frontier
245
+
246
+ impacted_paths = sorted(
247
+ sha_to_path[sha]
248
+ for sha in visited - changed_shas
249
+ if sha in sha_to_path
250
+ )
251
+
252
+ return ImpactAnalysisResponse(
253
+ changed_files=files,
254
+ impacted_files=impacted_paths,
255
+ total_impacted=len(impacted_paths),
256
+ layers=layers,
257
+ )
258
+
259
+ return ImpactAnalysisResponse(changed_files=files, impacted_files=[], total_impacted=0)
260
+
261
+ async def dependency_graph(
262
+ self, start_file: str, depth: int, branch: str, directory: str = "",
263
+ ) -> DependencyGraphResponse:
264
+ from attocode_intel.api.deps import get_branch_context
265
+ from attocode_intel.db.engine import get_session
266
+ from attocode_intel.storage.dependency_store import DependencyStore
267
+
268
+ async for session in get_session():
269
+ repo_id = uuid.UUID(self._project_id)
270
+ dep_store = DependencyStore(session)
271
+ branch_ctx = await get_branch_context(repo_id, branch, session)
272
+ raw = await dep_store.get_graph_for_branch(branch_ctx.branch_id)
273
+
274
+ raw_nodes = raw.get("nodes", [])
275
+ raw_edges = raw.get("edges", [])
276
+
277
+ # Fallback: compute edges on-the-fly from import extraction
278
+ # when the Dependency table is empty but we have files
279
+ if not raw_edges and raw_nodes:
280
+ raw_edges = await self._compute_import_edges(session, branch_ctx)
281
+
282
+ if start_file:
283
+ adj: dict[str, list[str]] = {}
284
+ for e in raw_edges:
285
+ adj.setdefault(e["source"], []).append(e["target"])
286
+ adj.setdefault(e["target"], []).append(e["source"])
287
+
288
+ # Resolve start_file: exact match or directory prefix
289
+ if start_file in adj:
290
+ seeds = {start_file}
291
+ else:
292
+ prefix = start_file.rstrip("/") + "/"
293
+ seeds = {n["path"] for n in raw_nodes if n["path"].startswith(prefix)}
294
+
295
+ if not seeds:
296
+ return DependencyGraphResponse(nodes=[], edges=[])
297
+
298
+ reachable: set[str] = set()
299
+ bfs_frontier = seeds
300
+ for _ in range(depth):
301
+ next_frontier: set[str] = set()
302
+ for f in bfs_frontier:
303
+ if f not in reachable:
304
+ reachable.add(f)
305
+ next_frontier.update(adj.get(f, []))
306
+ bfs_frontier = next_frontier - reachable
307
+ reachable.update(bfs_frontier)
308
+
309
+ raw_nodes = [n for n in raw_nodes if n["path"] in reachable]
310
+ raw_edges = [
311
+ e for e in raw_edges
312
+ if e["source"] in reachable and e["target"] in reachable
313
+ ]
314
+
315
+ nodes = [
316
+ DependencyGraphNode(id=n["path"], label=n["path"], type="file")
317
+ for n in raw_nodes
318
+ ]
319
+ edges = [
320
+ DependencyGraphEdge(
321
+ source=e["source"], target=e["target"],
322
+ type=e.get("type", "import"),
323
+ )
324
+ for e in raw_edges
325
+ ]
326
+
327
+ # Filter by directory prefix if specified
328
+ if directory:
329
+ node_ids = {n.id for n in nodes if n.id.startswith(directory)}
330
+ nodes = [n for n in nodes if n.id in node_ids]
331
+ edges = [e for e in edges if e.source in node_ids and e.target in node_ids]
332
+
333
+ return DependencyGraphResponse(nodes=nodes, edges=edges)
334
+
335
+ return DependencyGraphResponse(nodes=[], edges=[])
336
+
337
+ async def _compute_import_edges(self, session: AsyncSession, branch_ctx: BranchContext) -> list[dict]:
338
+ """Fallback: compute edges on-the-fly from import extraction when Dependency table is empty."""
339
+ import logging
340
+
341
+ from sqlalchemy import select
342
+
343
+ from attocode_intel.db.models import FileContent
344
+ from attocode_intel.indexing.full_indexer import _resolve_import_path
345
+ from attocode_intel.indexing.parser import detect_language, extract_imports
346
+
347
+ logger = logging.getLogger(__name__)
348
+ manifest = branch_ctx.manifest # path -> sha
349
+ known_paths = set(manifest.keys())
350
+ sha_to_path = {sha: path for path, sha in manifest.items()}
351
+ content_shas = list(set(manifest.values()))
352
+
353
+ if not content_shas:
354
+ return []
355
+
356
+ # Batch-fetch file content (limit to avoid memory issues)
357
+ MAX_FILES = 500
358
+ if len(content_shas) > MAX_FILES:
359
+ # Only process files matching start_file prefix if too many
360
+ content_shas = content_shas[:MAX_FILES]
361
+
362
+ result = await session.execute(
363
+ select(FileContent.sha256, FileContent.content).where(
364
+ FileContent.sha256.in_(content_shas)
365
+ )
366
+ )
367
+ rows = result.all()
368
+
369
+ edges: list[dict] = []
370
+ for sha, content in rows:
371
+ source_path = sha_to_path.get(sha)
372
+ if not source_path or content is None:
373
+ continue
374
+ language = detect_language(source_path)
375
+ if not language:
376
+ continue
377
+ try:
378
+ raw_content = content if isinstance(content, bytes) else content.encode("utf-8")
379
+ imports = extract_imports(raw_content, source_path, language)
380
+ for imp in imports:
381
+ target = _resolve_import_path(imp, source_path, known_paths, language)
382
+ if target and target != source_path:
383
+ edges.append({
384
+ "source": source_path,
385
+ "target": target,
386
+ "type": "import",
387
+ "weight": 1.0,
388
+ })
389
+ except Exception as e:
390
+ logger.debug("Import extraction failed for %s: %s", source_path, e)
391
+
392
+ logger.info("Computed %d import edges on-the-fly for %d files", len(edges), len(rows))
393
+ return edges
394
+
395
+ async def hotspots(self, branch: str, top_n: int) -> HotspotsResponse:
396
+ from sqlalchemy import func as sa_func
397
+ from sqlalchemy import select
398
+
399
+ from attocode_intel.api.deps import get_branch_context
400
+ from attocode_intel.db.engine import get_session
401
+ from attocode_intel.db.models import Dependency, FileContent, Symbol
402
+
403
+ async for session in get_session():
404
+ repo_id = uuid.UUID(self._project_id)
405
+ branch_ctx = await get_branch_context(repo_id, branch, session)
406
+ content_shas = branch_ctx.content_shas
407
+ sha_to_path = branch_ctx.sha_to_path
408
+
409
+ if not content_shas:
410
+ return HotspotsResponse(file_hotspots=[], function_hotspots=[], orphan_files=[])
411
+
412
+ sym_result = await session.execute(
413
+ select(Symbol.content_sha, sa_func.count())
414
+ .where(Symbol.content_sha.in_(content_shas))
415
+ .group_by(Symbol.content_sha)
416
+ )
417
+ sym_counts: dict[str, int] = dict(sym_result.all())
418
+
419
+ fan_out_result = await session.execute(
420
+ select(Dependency.source_sha, sa_func.count())
421
+ .where(Dependency.source_sha.in_(content_shas))
422
+ .group_by(Dependency.source_sha)
423
+ )
424
+ fan_out: dict[str, int] = dict(fan_out_result.all())
425
+
426
+ fan_in_result = await session.execute(
427
+ select(Dependency.target_sha, sa_func.count())
428
+ .where(Dependency.target_sha.in_(content_shas))
429
+ .group_by(Dependency.target_sha)
430
+ )
431
+ fan_in: dict[str, int] = dict(fan_in_result.all())
432
+
433
+ size_result = await session.execute(
434
+ select(FileContent.sha256, FileContent.size_bytes)
435
+ .where(FileContent.sha256.in_(content_shas))
436
+ )
437
+ file_sizes: dict[str, int] = dict(size_result.all())
438
+
439
+ items: list[FileMetricsItem] = []
440
+ for sha, path in sha_to_path.items():
441
+ sc = sym_counts.get(sha, 0)
442
+ fi = fan_in.get(sha, 0)
443
+ fo = fan_out.get(sha, 0)
444
+ size = file_sizes.get(sha, 0)
445
+ lines = max(size // 40, 1) if size > 0 else 0
446
+ max_sym = max(sym_counts.values()) if sym_counts else 1
447
+ max_conn = max(
448
+ (fi_ + fo_ for fi_, fo_ in zip(fan_in.values(), fan_out.values())),
449
+ default=1,
450
+ ) or 1
451
+ max_size = max(file_sizes.values()) if file_sizes else 1
452
+ max_lines = max(max_size // 40, 1)
453
+
454
+ composite = (
455
+ 0.3 * (sc / max(max_sym, 1))
456
+ + 0.3 * ((fi + fo) / max(max_conn, 1))
457
+ + 0.2 * (lines / max(max_lines, 1))
458
+ + 0.2 * (size / max(max_size, 1))
459
+ )
460
+ items.append(FileMetricsItem(
461
+ path=path,
462
+ line_count=lines,
463
+ symbol_count=sc,
464
+ fan_in=fi,
465
+ fan_out=fo,
466
+ density=round(sc / max(lines, 1), 3),
467
+ composite=round(composite, 4),
468
+ ))
469
+
470
+ items.sort(key=lambda x: x.composite, reverse=True)
471
+ return HotspotsResponse(
472
+ file_hotspots=items[:top_n],
473
+ function_hotspots=[],
474
+ orphan_files=[],
475
+ )
476
+
477
+ return HotspotsResponse(file_hotspots=[], function_hotspots=[], orphan_files=[])
478
+
479
+ async def dependencies(self, path: str, branch: str) -> DependencyResponse:
480
+ """Falls back to CodeIntelService (no DB-specific implementation)."""
481
+ from attocode_intel.api.deps import get_service_or_404
482
+
483
+ svc = await get_service_or_404(self._project_id)
484
+ data = svc.dependencies_data(path)
485
+ return DependencyResponse(**data)
486
+
487
+ async def file_analysis(self, path: str, branch: str) -> FileAnalysisResponse:
488
+ """Falls back to CodeIntelService (no DB-specific implementation)."""
489
+ from attocode_intel.api.deps import get_service_or_404
490
+
491
+ svc = await get_service_or_404(self._project_id)
492
+ data = svc.file_analysis_data(path)
493
+ return FileAnalysisResponse(
494
+ path=data["path"],
495
+ language=data["language"],
496
+ line_count=data["line_count"],
497
+ imports=data["imports"],
498
+ exports=data["exports"],
499
+ chunks=[CodeChunkItem(**c) for c in data["chunks"]],
500
+ )
501
+
502
+ async def conventions(
503
+ self, branch: str, sample_size: int, path: str,
504
+ ) -> ConventionsResponse:
505
+ from sqlalchemy import select
506
+
507
+ from attocode_intel.api.deps import get_branch_context
508
+ from attocode_intel.db.engine import get_session
509
+ from attocode_intel.db.models import Symbol
510
+
511
+ snake_re = re.compile(r"^[a-z][a-z0-9_]*$")
512
+ camel_re = re.compile(r"^[a-z][a-zA-Z0-9]*$")
513
+
514
+ async for session in get_session():
515
+ repo_id = uuid.UUID(self._project_id)
516
+ branch_ctx = await get_branch_context(repo_id, branch, session)
517
+ manifest = branch_ctx.manifest
518
+ content_shas = branch_ctx.content_shas
519
+
520
+ if not content_shas:
521
+ return ConventionsResponse(sample_size=0, path=path, stats=ConventionStats())
522
+
523
+ if path:
524
+ filtered_shas = {sha for p, sha in manifest.items() if p.startswith(path)}
525
+ else:
526
+ filtered_shas = content_shas
527
+
528
+ if not filtered_shas:
529
+ return ConventionsResponse(sample_size=0, path=path, stats=ConventionStats())
530
+
531
+ result = await session.execute(
532
+ select(Symbol).where(Symbol.content_sha.in_(filtered_shas))
533
+ )
534
+ symbols = result.scalars().all()
535
+
536
+ total_functions = 0
537
+ total_classes = 0
538
+ snake_names = 0
539
+ camel_names = 0
540
+ typed_return = 0
541
+ async_count = 0
542
+
543
+ for s in symbols:
544
+ if s.kind in ("function", "method"):
545
+ total_functions += 1
546
+ if snake_re.match(s.name):
547
+ snake_names += 1
548
+ elif camel_re.match(s.name) and any(c.isupper() for c in s.name[1:]):
549
+ camel_names += 1
550
+ if s.signature and "->" in s.signature:
551
+ typed_return += 1
552
+ meta = s.metadata_ or {}
553
+ if meta.get("async"):
554
+ async_count += 1
555
+ elif s.kind == "class":
556
+ total_classes += 1
557
+
558
+ stats = ConventionStats(
559
+ total_functions=total_functions,
560
+ total_classes=total_classes,
561
+ snake_names=snake_names,
562
+ camel_names=camel_names,
563
+ typed_return=typed_return,
564
+ async_count=async_count,
565
+ )
566
+ return ConventionsResponse(
567
+ sample_size=len(filtered_shas),
568
+ path=path,
569
+ stats=stats,
570
+ )
571
+
572
+ return ConventionsResponse(sample_size=0, path=path, stats=ConventionStats())
573
+
574
+
575
+ class DbSearchProvider:
576
+ """DB-backed search provider for service mode."""
577
+
578
+ __slots__ = ("_project_id",)
579
+
580
+ def __init__(self, project_id: str) -> None:
581
+ self._project_id = project_id
582
+
583
+ async def semantic_search(
584
+ self, query: str, top_k: int, file_filter: str, branch: str,
585
+ ) -> SearchResultsResponse:
586
+ """Run pgvector cosine similarity search."""
587
+ import uuid as _uuid
588
+
589
+ from attocode_intel.api.deps import get_branch_context, get_config
590
+ from attocode_intel.db.engine import get_session
591
+ from attocode_intel.storage.embedding_store import EmbeddingStore
592
+ from attocode_intel._internal.integrations.context.embeddings import create_embedding_provider
593
+
594
+ config = get_config()
595
+ provider = create_embedding_provider(config.embedding_model)
596
+ if provider.name == "none":
597
+ raise RuntimeError("No embedding provider available")
598
+
599
+ query_vectors = provider.embed([query])
600
+ query_vector = query_vectors[0]
601
+
602
+ async for session in get_session():
603
+ branch_ctx = await get_branch_context(
604
+ _uuid.UUID(self._project_id), branch, session,
605
+ )
606
+
607
+ store = EmbeddingStore(session)
608
+ results = await store.similarity_search(
609
+ branch_id=branch_ctx.branch_id,
610
+ query_vector=query_vector,
611
+ top_k=top_k,
612
+ model=provider.name,
613
+ file_filter=file_filter,
614
+ )
615
+
616
+ items = [
617
+ SearchResultItem(
618
+ file_path=r["file"],
619
+ score=r.get("score", 0.0),
620
+ snippet=r.get("chunk_text", ""),
621
+ )
622
+ for r in results
623
+ ]
624
+
625
+ # Best-effort line number enrichment from symbols
626
+ query_words = [w for w in query.split() if len(w) >= 3]
627
+ if query_words:
628
+ from sqlalchemy import select as sa_select
629
+
630
+ from attocode_intel.db.models import BranchFile, Symbol
631
+
632
+ sha_by_file: dict[str, str] = {}
633
+ for r in results:
634
+ bf_result = await session.execute(
635
+ sa_select(BranchFile.content_sha).where(
636
+ BranchFile.branch_id == branch_ctx.branch_id,
637
+ BranchFile.path == r["file"],
638
+ ).limit(1)
639
+ )
640
+ bf_row = bf_result.first()
641
+ if bf_row:
642
+ sha_by_file[r["file"]] = bf_row[0]
643
+
644
+ for item in items:
645
+ sha = sha_by_file.get(item.file_path)
646
+ if not sha:
647
+ continue
648
+ for word in query_words:
649
+ sym_result = await session.execute(
650
+ sa_select(Symbol.line_start)
651
+ .where(Symbol.content_sha == sha)
652
+ .where(Symbol.name.ilike(f"%{word}%"))
653
+ .limit(1)
654
+ )
655
+ row = sym_result.first()
656
+ if row:
657
+ item.line = row[0]
658
+ break
659
+
660
+ # Codex M4: populate the ``pin_id`` + ``manifest_hash`` fields
661
+ # so search responses carry the same state-snapshot identifier
662
+ # the local stdio MCP emits via ``_stamp_pin``.
663
+ from attocode_intel.api.routes._pin_helper import (
664
+ build_retrieval_pin,
665
+ )
666
+ try:
667
+ server_pin = await build_retrieval_pin(
668
+ session,
669
+ repo_id=_uuid.UUID(self._project_id),
670
+ branch_id=branch_ctx.branch_id,
671
+ )
672
+ pin_id = server_pin.pin_id
673
+ manifest_hash = server_pin.manifest_hash
674
+ except Exception:
675
+ pin_id = ""
676
+ manifest_hash = ""
677
+
678
+ return SearchResultsResponse(
679
+ query=query,
680
+ results=items,
681
+ total=len(items),
682
+ pin_id=pin_id,
683
+ manifest_hash=manifest_hash,
684
+ )
685
+
686
+ raise RuntimeError("No database session available")
687
+
688
+
689
+ async def security_scan(
690
+ self, mode: str, path: str, branch: str,
691
+ ) -> SecurityScanResponse:
692
+ """DB-backed security scan — runs regex patterns on stored content."""
693
+ import uuid as _uuid
694
+
695
+ from attocode_intel.api.deps import get_branch_context
696
+ from attocode_intel.db.engine import get_session
697
+ from attocode_intel.storage.security_scanner_db import db_security_scan
698
+
699
+ async for session in get_session():
700
+ repo_id = _uuid.UUID(self._project_id)
701
+ branch_ctx = await get_branch_context(repo_id, branch, session)
702
+
703
+ result = await db_security_scan(
704
+ session,
705
+ branch_ctx.branch_id,
706
+ branch_ctx.manifest,
707
+ mode=mode,
708
+ path_filter=path,
709
+ )
710
+
711
+ return SecurityScanResponse(
712
+ mode=result["mode"],
713
+ path=result["path"],
714
+ findings=[SecurityFinding(**f) for f in result["findings"]],
715
+ total_findings=result["total_findings"],
716
+ summary=result.get("summary", {}),
717
+ )
718
+
719
+ return SecurityScanResponse(
720
+ mode=mode, path=path, findings=[], total_findings=0,
721
+ summary={"error": "No database session available"},
722
+ )
723
+
724
+
725
+ class DbGraphProvider:
726
+ """DB-backed graph provider for service mode."""
727
+
728
+ __slots__ = ("_project_id",)
729
+
730
+ def __init__(self, project_id: str) -> None:
731
+ self._project_id = project_id
732
+
733
+ async def graph_query(
734
+ self, file: str, edge_type: str, direction: str, depth: int, branch: str,
735
+ ) -> GraphQueryResponse:
736
+ """Falls back to CodeIntelService (no DB-specific implementation)."""
737
+ from attocode_intel.api.deps import get_service_or_404
738
+ from attocode_intel.api.models import GraphQueryHop, GraphQueryResponse
739
+
740
+ svc = await get_service_or_404(self._project_id)
741
+ data = svc.graph_query_data(
742
+ file=file, edge_type=edge_type, direction=direction, depth=depth,
743
+ )
744
+ return GraphQueryResponse(
745
+ root=data["root"],
746
+ direction=data["direction"],
747
+ depth=data["depth"],
748
+ hops=[GraphQueryHop(**h) for h in data["hops"]],
749
+ total_reachable=data["total_reachable"],
750
+ )
751
+
752
+ async def find_related(
753
+ self, file: str, top_k: int, branch: str,
754
+ ) -> FindRelatedResponse:
755
+ """Falls back to CodeIntelService (no DB-specific implementation)."""
756
+ from attocode_intel.api.deps import get_service_or_404
757
+ from attocode_intel.api.models import FindRelatedResponse, RelatedFileItem
758
+
759
+ svc = await get_service_or_404(self._project_id)
760
+ data = svc.find_related_data(file=file, top_k=top_k)
761
+ return FindRelatedResponse(
762
+ file=data["file"],
763
+ related=[RelatedFileItem(**r) for r in data["related"]],
764
+ )
765
+
766
+ async def community_detection(
767
+ self, branch: str, min_community_size: int, max_communities: int,
768
+ ) -> CommunityResponse:
769
+ import os
770
+ from collections import Counter
771
+
772
+ from attocode_intel.api.deps import get_branch_context
773
+ from attocode_intel.api.models import CommunityBridge
774
+ from attocode_intel.community import bfs_connected_components, louvain_communities
775
+ from attocode_intel.db.engine import get_session
776
+ from attocode_intel.storage.dependency_store import DependencyStore
777
+
778
+ async for session in get_session():
779
+ repo_id = uuid.UUID(self._project_id)
780
+ branch_ctx = await get_branch_context(repo_id, branch, session)
781
+ dep_store = DependencyStore(session)
782
+ raw = await dep_store.get_graph_for_branch(branch_ctx.branch_id)
783
+
784
+ raw_edges = raw.get("edges", [])
785
+ all_files: set[str] = {n["path"] for n in raw.get("nodes", [])}
786
+
787
+ adj: dict[str, set[str]] = {}
788
+ weights: dict[tuple[str, str], float] = {}
789
+ for e in raw_edges:
790
+ src, tgt = e["source"], e["target"]
791
+ adj.setdefault(src, set()).add(tgt)
792
+ adj.setdefault(tgt, set()).add(src)
793
+ key = (min(src, tgt), max(src, tgt))
794
+ weights[key] = weights.get(key, 0.0) + e.get("weight", 1.0)
795
+
796
+ try:
797
+ communities, modularity = louvain_communities(all_files, adj, weights)
798
+ method = "louvain"
799
+ except ImportError:
800
+ communities, modularity = bfs_connected_components(all_files, adj)
801
+ method = "connected_components"
802
+
803
+ communities = [c for c in communities if len(c) >= min_community_size]
804
+ communities.sort(key=len, reverse=True)
805
+ communities = communities[:max_communities]
806
+
807
+ # Detect trivial results and use directory-based fallback
808
+ is_trivial = len(communities) <= 1 or modularity < 0.05
809
+ if is_trivial:
810
+ # Group files by top-level directory as modules
811
+ dir_groups: dict[str, list[str]] = {}
812
+ for f in all_files:
813
+ parts = f.split("/")
814
+ top_dir = parts[0] if len(parts) > 1 else "(root)"
815
+ dir_groups.setdefault(top_dir, []).append(f)
816
+
817
+ fallback_items: list[CommunityItem] = []
818
+ for idx_fb, (dir_name, file_list) in enumerate(
819
+ sorted(dir_groups.items(), key=lambda x: len(x[1]), reverse=True),
820
+ ):
821
+ fallback_items.append(CommunityItem(
822
+ id=idx_fb,
823
+ files=sorted(file_list),
824
+ size=len(file_list),
825
+ theme=dir_name,
826
+ internal_edges=0,
827
+ external_edges=0,
828
+ hub=sorted(file_list)[0] if file_list else "",
829
+ hub_internal_degree=0,
830
+ top_dirs=[dir_name],
831
+ ))
832
+
833
+ return CommunityResponse(
834
+ method=f"{method}+directory-fallback",
835
+ modularity=round(modularity, 4),
836
+ communities=fallback_items,
837
+ bridges=[],
838
+ )
839
+
840
+ # Build file-to-community-index mapping
841
+ file_to_comm: dict[str, int] = {}
842
+ items: list[CommunityItem] = []
843
+ for idx, comm in enumerate(communities):
844
+ for f in comm:
845
+ file_to_comm[f] = idx
846
+
847
+ internal = 0
848
+ external = 0
849
+ degree: dict[str, int] = {f: 0 for f in comm}
850
+ for f in comm:
851
+ for neighbor in adj.get(f, set()):
852
+ if neighbor in comm:
853
+ internal += 1
854
+ degree[f] += 1
855
+ else:
856
+ external += 1
857
+ internal //= 2
858
+
859
+ hub = max(degree, key=degree.get) if degree else "" # type: ignore[arg-type]
860
+ hub_deg = degree.get(hub, 0)
861
+
862
+ sorted_files = sorted(comm)
863
+ if sorted_files:
864
+ prefix = sorted_files[0]
865
+ for f in sorted_files[1:]:
866
+ while not f.startswith(prefix):
867
+ prefix = prefix.rsplit("/", 1)[0] if "/" in prefix else ""
868
+ if not prefix:
869
+ break
870
+ theme = prefix.rstrip("/") or "mixed"
871
+ else:
872
+ theme = ""
873
+
874
+ # Compute top directories
875
+ dirs = [os.path.dirname(f) or "(root)" for f in sorted_files]
876
+ top_dirs = [d for d, _ in Counter(dirs).most_common(3)]
877
+
878
+ items.append(CommunityItem(
879
+ id=idx,
880
+ files=sorted_files,
881
+ size=len(comm),
882
+ theme=theme,
883
+ internal_edges=internal,
884
+ external_edges=external,
885
+ hub=hub,
886
+ hub_internal_degree=hub_deg,
887
+ top_dirs=top_dirs,
888
+ ))
889
+
890
+ # Compute bridges between communities
891
+ bridge_counts: dict[tuple[int, int], int] = {}
892
+ bridge_samples: dict[tuple[int, int], list[str]] = {}
893
+ for e in raw_edges:
894
+ src_comm = file_to_comm.get(e["source"])
895
+ tgt_comm = file_to_comm.get(e["target"])
896
+ if src_comm is None or tgt_comm is None or src_comm == tgt_comm:
897
+ continue
898
+ key = (min(src_comm, tgt_comm), max(src_comm, tgt_comm))
899
+ bridge_counts[key] = bridge_counts.get(key, 0) + 1
900
+ samples = bridge_samples.setdefault(key, [])
901
+ if len(samples) < 3:
902
+ samples.append(e["source"])
903
+
904
+ bridges = [
905
+ CommunityBridge(
906
+ source_id=k[0], target_id=k[1],
907
+ edge_count=v, sample_files=bridge_samples.get(k, []),
908
+ )
909
+ for k, v in sorted(bridge_counts.items(), key=lambda x: x[1], reverse=True)
910
+ ]
911
+
912
+ return CommunityResponse(
913
+ method=method, modularity=round(modularity, 4),
914
+ communities=items, bridges=bridges,
915
+ )
916
+
917
+ return CommunityResponse(method="none", modularity=0.0, communities=[])
918
+
919
+
920
+ class DbLSPProvider:
921
+ """DB-backed LSP provider for remote repos without a language server.
922
+
923
+ Provides best-effort definition/references/hover using the symbol and
924
+ symbol_reference tables. Diagnostics returns empty (requires real LS).
925
+ """
926
+
927
+ __slots__ = ("_project_id",)
928
+
929
+ def __init__(self, project_id: str) -> None:
930
+ self._project_id = project_id
931
+
932
+ async def definition(self, file: str, line: int, col: int, branch: str) -> dict:
933
+ """Find definition of symbol at position."""
934
+ from sqlalchemy import select
935
+
936
+ from attocode_intel.api.deps import get_branch_context
937
+ from attocode_intel.db.engine import get_session
938
+ from attocode_intel.db.models import Symbol
939
+
940
+ async for session in get_session():
941
+ repo_id = uuid.UUID(self._project_id)
942
+ branch_ctx = await get_branch_context(repo_id, branch, session)
943
+ sha = branch_ctx.manifest.get(file)
944
+ if not sha:
945
+ return {"definitions": []}
946
+
947
+ # Find symbol at the given line
948
+ result = await session.execute(
949
+ select(Symbol).where(
950
+ Symbol.content_sha == sha,
951
+ Symbol.line_start <= line,
952
+ Symbol.line_end >= line,
953
+ )
954
+ )
955
+ source_sym = result.scalars().first()
956
+ if source_sym is None:
957
+ return {"definitions": []}
958
+
959
+ # Search for a definition with the same name across the branch
960
+ all_shas = branch_ctx.content_shas
961
+ def_result = await session.execute(
962
+ select(Symbol).where(
963
+ Symbol.content_sha.in_(all_shas),
964
+ Symbol.name == source_sym.name,
965
+ Symbol.kind.in_(("class", "function", "method", "variable")),
966
+ ).limit(10)
967
+ )
968
+ definitions = []
969
+ sha_to_path = branch_ctx.sha_to_path
970
+ for s in def_result.scalars():
971
+ definitions.append({
972
+ "file": sha_to_path.get(s.content_sha, ""),
973
+ "line": s.line_start or 0,
974
+ "name": s.name,
975
+ "kind": s.kind,
976
+ "signature": s.signature or "",
977
+ })
978
+ return {"definitions": definitions}
979
+
980
+ return {"definitions": []}
981
+
982
+ async def references(self, file: str, line: int, col: int, branch: str) -> dict:
983
+ """Find references to symbol at position."""
984
+ from sqlalchemy import select
985
+
986
+ from attocode_intel.api.deps import get_branch_context
987
+ from attocode_intel.db.engine import get_session
988
+ from attocode_intel.db.models import Symbol, SymbolReference
989
+
990
+ async for session in get_session():
991
+ repo_id = uuid.UUID(self._project_id)
992
+ branch_ctx = await get_branch_context(repo_id, branch, session)
993
+ sha = branch_ctx.manifest.get(file)
994
+ if not sha:
995
+ return {"references": []}
996
+
997
+ # Find symbol at position
998
+ result = await session.execute(
999
+ select(Symbol).where(
1000
+ Symbol.content_sha == sha,
1001
+ Symbol.line_start <= line,
1002
+ Symbol.line_end >= line,
1003
+ )
1004
+ )
1005
+ source_sym = result.scalars().first()
1006
+ if source_sym is None:
1007
+ return {"references": []}
1008
+
1009
+ # Query references table
1010
+ all_shas = branch_ctx.content_shas
1011
+ ref_result = await session.execute(
1012
+ select(SymbolReference).where(
1013
+ SymbolReference.content_sha.in_(all_shas),
1014
+ SymbolReference.symbol_name == source_sym.name,
1015
+ ).limit(500)
1016
+ )
1017
+ sha_to_path = branch_ctx.sha_to_path
1018
+ references = [
1019
+ {
1020
+ "file": sha_to_path.get(r.content_sha, ""),
1021
+ "line": r.line,
1022
+ "ref_kind": r.ref_kind,
1023
+ }
1024
+ for r in ref_result.scalars()
1025
+ ]
1026
+ return {"references": references}
1027
+
1028
+ return {"references": []}
1029
+
1030
+ async def hover(self, file: str, line: int, col: int, branch: str) -> dict:
1031
+ """Get hover info for symbol at position."""
1032
+ from sqlalchemy import select
1033
+
1034
+ from attocode_intel.api.deps import get_branch_context
1035
+ from attocode_intel.db.engine import get_session
1036
+ from attocode_intel.db.models import Symbol
1037
+
1038
+ async for session in get_session():
1039
+ repo_id = uuid.UUID(self._project_id)
1040
+ branch_ctx = await get_branch_context(repo_id, branch, session)
1041
+ sha = branch_ctx.manifest.get(file)
1042
+ if not sha:
1043
+ return {"hover": None}
1044
+
1045
+ result = await session.execute(
1046
+ select(Symbol).where(
1047
+ Symbol.content_sha == sha,
1048
+ Symbol.line_start <= line,
1049
+ Symbol.line_end >= line,
1050
+ )
1051
+ )
1052
+ sym = result.scalars().first()
1053
+ if sym is None:
1054
+ return {"hover": None}
1055
+
1056
+ return {
1057
+ "hover": {
1058
+ "name": sym.name,
1059
+ "kind": sym.kind,
1060
+ "signature": sym.signature or "",
1061
+ "exported": sym.exported,
1062
+ "line_start": sym.line_start,
1063
+ "line_end": sym.line_end,
1064
+ }
1065
+ }
1066
+
1067
+ return {"hover": None}
1068
+
1069
+ async def diagnostics(self, file: str, branch: str) -> dict:
1070
+ """Return empty diagnostics — requires a real language server."""
1071
+ return {
1072
+ "diagnostics": [],
1073
+ "message": "Diagnostics require a running language server (not available for remote repos)",
1074
+ }
1075
+
1076
+ async def completions(self, file: str, line: int, col: int, branch: str, limit: int = 20) -> dict:
1077
+ """Completions require a real language server."""
1078
+ return {
1079
+ "file": file, "line": line, "col": col,
1080
+ "completions": [], "total": 0,
1081
+ "error": "Completions require a running language server (not available for remote repos)",
1082
+ }
1083
+
1084
+ async def workspace_symbol(self, query: str, branch: str, limit: int = 30) -> dict:
1085
+ """Workspace symbol search requires a real language server."""
1086
+ return {
1087
+ "query": query,
1088
+ "symbols": [], "total": 0,
1089
+ "error": "Workspace symbol search requires a running language server (not available for remote repos)",
1090
+ }
1091
+
1092
+ async def incoming_calls(self, file: str, line: int, col: int, branch: str) -> dict:
1093
+ """Incoming call hierarchy requires a real language server."""
1094
+ return {
1095
+ "symbol": "", "file": file, "line": line, "col": col,
1096
+ "callers": [], "total": 0,
1097
+ "error": "Call hierarchy requires a running language server (not available for remote repos)",
1098
+ }
1099
+
1100
+ async def outgoing_calls(self, file: str, line: int, col: int, branch: str) -> dict:
1101
+ """Outgoing call hierarchy requires a real language server."""
1102
+ return {
1103
+ "symbol": "", "file": file, "line": line, "col": col,
1104
+ "callees": [], "total": 0,
1105
+ "error": "Call hierarchy requires a running language server (not available for remote repos)",
1106
+ }