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,594 @@
1
+ """Embedding storage — content-SHA-keyed with model tracking, pgvector-ready."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ import uuid
10
+
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class EmbeddingDimensionMismatchError(RuntimeError):
17
+ """Raised when the running embedding provider's dimension does not match
18
+ the vectors already persisted in the ``embeddings`` table.
19
+
20
+ Migration 016 replaced the old behavior — which silently ran
21
+ ``UPDATE embeddings SET vector = NULL`` and dropped every stored
22
+ embedding — with a loud refusal. Callers must now explicitly rotate
23
+ to the new dimension via the (phase-4) embeddings rotation endpoint,
24
+ or clear the table manually. Silent data loss is never OK.
25
+ """
26
+
27
+ def __init__(self, *, stored: int, expected: int) -> None:
28
+ self.stored = stored
29
+ self.expected = expected
30
+ super().__init__(
31
+ f"embedding dimension mismatch: stored={stored} expected={expected}. "
32
+ f"Refusing to wipe existing vectors. "
33
+ f"Resolve by starting an embedding rotation (POST /embeddings/rotate), "
34
+ f"or explicitly re-dimensioning the column after backing up."
35
+ )
36
+
37
+
38
+ async def ensure_vector_columns(
39
+ session: AsyncSession,
40
+ primary_dim: int,
41
+ shadow_dim: int | None = None,
42
+ ) -> None:
43
+ """Ensure ``embeddings.vector`` exists at ``primary_dim`` without destroying
44
+ data, and optionally ensure a ``vector_b`` shadow column at ``shadow_dim``.
45
+
46
+ Behavior:
47
+ - If ``vector`` does not exist: CREATE at ``primary_dim``, build HNSW index.
48
+ - If ``vector`` exists at ``primary_dim``: no-op.
49
+ - If ``vector`` exists at a DIFFERENT dim: **raise** ``EmbeddingDimensionMismatchError``.
50
+ Old code silently NULLed every row here; that was a footgun.
51
+ - If ``shadow_dim`` is given and differs from ``primary_dim``: ensure a
52
+ ``vector_b`` column at ``shadow_dim`` (used during rotation dual-write).
53
+ Never touches the primary column.
54
+
55
+ ``shadow_dim`` is accepted now so the rotation machinery in a later phase
56
+ doesn't have to re-touch this function.
57
+ """
58
+ from sqlalchemy import text
59
+
60
+ result = await session.execute(
61
+ text(
62
+ "SELECT atttypmod FROM pg_attribute "
63
+ "WHERE attrelid = 'embeddings'::regclass AND attname = 'vector'"
64
+ )
65
+ )
66
+ row = result.first()
67
+
68
+ if row is None:
69
+ # Column doesn't exist — safe to add.
70
+ await session.execute(
71
+ text(f"ALTER TABLE embeddings ADD COLUMN vector vector({primary_dim})")
72
+ )
73
+ await session.execute(
74
+ text(
75
+ "CREATE INDEX IF NOT EXISTS idx_embeddings_vector_hnsw "
76
+ "ON embeddings USING hnsw (vector vector_cosine_ops) "
77
+ "WITH (m = 16, ef_construction = 64)"
78
+ )
79
+ )
80
+ logger.info(
81
+ "Created vector column with dimension %d and HNSW index", primary_dim
82
+ )
83
+ else:
84
+ stored_dim = row[0] - 4 # pgvector stores dim+4 in atttypmod
85
+ if stored_dim != primary_dim:
86
+ logger.error(
87
+ "Embedding dimension mismatch: stored=%d, expected=%d. "
88
+ "Refusing destructive UPDATE. Use embedding rotation flow.",
89
+ stored_dim, primary_dim,
90
+ )
91
+ raise EmbeddingDimensionMismatchError(
92
+ stored=stored_dim, expected=primary_dim,
93
+ )
94
+ logger.debug("Vector column already has correct dimension %d", primary_dim)
95
+
96
+ # Shadow column handling — used during a live rotation where new writes
97
+ # target vector_b with the new dimension while reads still hit vector.
98
+ if shadow_dim is not None and shadow_dim != primary_dim:
99
+ result_b = await session.execute(
100
+ text(
101
+ "SELECT atttypmod FROM pg_attribute "
102
+ "WHERE attrelid = 'embeddings'::regclass AND attname = 'vector_b'"
103
+ )
104
+ )
105
+ row_b = result_b.first()
106
+ if row_b is None:
107
+ await session.execute(
108
+ text(f"ALTER TABLE embeddings ADD COLUMN vector_b vector({shadow_dim})")
109
+ )
110
+ await session.execute(
111
+ text(
112
+ "CREATE INDEX IF NOT EXISTS idx_embeddings_vector_b_hnsw "
113
+ "ON embeddings USING hnsw (vector_b vector_cosine_ops) "
114
+ "WITH (m = 16, ef_construction = 64)"
115
+ )
116
+ )
117
+ logger.info("Created shadow vector_b column at dimension %d", shadow_dim)
118
+ else:
119
+ stored_shadow = row_b[0] - 4
120
+ if stored_shadow != shadow_dim:
121
+ raise EmbeddingDimensionMismatchError(
122
+ stored=stored_shadow, expected=shadow_dim,
123
+ )
124
+
125
+ await session.commit()
126
+
127
+
128
+ class EmbeddingStore:
129
+ """Content-SHA-keyed embedding storage for semantic search.
130
+
131
+ Embeddings are keyed by (content_sha, embedding_model, embedding_model_version)
132
+ for deduplication. If content_sha+model+version already exists, skip — same
133
+ content always produces the same embeddings for a given model version.
134
+
135
+ Branch-aware queries resolve through BranchOverlay.
136
+ """
137
+
138
+ def __init__(self, session: AsyncSession) -> None:
139
+ self._session = session
140
+
141
+ async def upsert_embeddings(
142
+ self,
143
+ content_sha: str,
144
+ embeddings: list[dict],
145
+ ) -> int:
146
+ """Store embeddings for a content hash.
147
+
148
+ Each embedding dict: {chunk_text, chunk_type, embedding_model,
149
+ embedding_model_version?, embedding_dim?, embedding_provenance?, vector?}
150
+
151
+ Idempotent within a (content_sha, model, model_version) triple —
152
+ replaces all rows for that triple. Other (model, version) pairs
153
+ coexist peacefully (needed for the dual-write rotation flow).
154
+
155
+ Codex fix M7: every insert is mirrored into the ``provenance``
156
+ table so downstream tools can join on ``action_hash`` /
157
+ ``input_blob_oid``. The provenance write is batched alongside
158
+ the embedding insert and does not commit separately — the
159
+ caller's existing session lifecycle controls atomicity.
160
+
161
+ Returns count of embeddings stored.
162
+ """
163
+ from sqlalchemy import delete
164
+
165
+ from attocode_intel.db.models import Embedding
166
+ from attocode_intel.storage.provenance_store import (
167
+ embedding_provenance_dict,
168
+ write_provenance_rows,
169
+ )
170
+
171
+ if not embeddings:
172
+ return 0
173
+
174
+ # Determine (model, version) pairs being upserted. Rows at other
175
+ # versions stay put — this is the key invariant enabling rotation.
176
+ pairs_in_batch: set[tuple[str, str]] = {
177
+ (
178
+ e.get("embedding_model", "default"),
179
+ e.get("embedding_model_version", ""),
180
+ )
181
+ for e in embeddings
182
+ }
183
+ for model, version in pairs_in_batch:
184
+ await self._session.execute(
185
+ delete(Embedding).where(
186
+ Embedding.content_sha == content_sha,
187
+ Embedding.embedding_model == model,
188
+ Embedding.embedding_model_version == version,
189
+ )
190
+ )
191
+
192
+ # Insert new + collect provenance rows in parallel so they land
193
+ # in the same unit of work.
194
+ provenance_batch: list[dict] = []
195
+ for emb_data in embeddings:
196
+ model = emb_data.get("embedding_model", "default")
197
+ version = emb_data.get("embedding_model_version", "")
198
+ dim = emb_data.get("embedding_dim")
199
+ provenance = emb_data.get("embedding_provenance", {}) or {}
200
+ action_hash = provenance.get("action_hash", "") if isinstance(provenance, dict) else ""
201
+ kwargs = {
202
+ "content_sha": content_sha,
203
+ "embedding_model": model,
204
+ "embedding_model_version": version,
205
+ "embedding_dim": dim,
206
+ "embedding_provenance": provenance,
207
+ "chunk_text": emb_data.get("chunk_text", ""),
208
+ "chunk_type": emb_data.get("chunk_type", "file"),
209
+ }
210
+ # Include vector if provided and pgvector is available
211
+ if "vector" in emb_data and emb_data["vector"] is not None:
212
+ kwargs["vector"] = emb_data["vector"]
213
+ emb = Embedding(**kwargs)
214
+ self._session.add(emb)
215
+
216
+ # One provenance row per chunk. Empty action_hash is fine —
217
+ # it just means no dedup key was computed upstream; the row
218
+ # still provides the indexer + blob lineage needed for
219
+ # later audits.
220
+ provenance_batch.append(embedding_provenance_dict(
221
+ action_hash=action_hash,
222
+ content_sha=content_sha,
223
+ model_name=model,
224
+ model_version=version,
225
+ dimension=int(dim) if dim else None,
226
+ config_digest=(
227
+ provenance.get("config_digest", "")
228
+ if isinstance(provenance, dict)
229
+ else ""
230
+ ),
231
+ chunk_type=emb_data.get("chunk_type", "file"),
232
+ ))
233
+
234
+ if provenance_batch:
235
+ await write_provenance_rows(self._session, provenance_batch)
236
+
237
+ await self._session.flush()
238
+ return len(embeddings)
239
+
240
+ async def batch_has_embeddings(self, content_shas: set[str], model: str = "default") -> set[str]:
241
+ """Check which content_shas have embeddings. Returns the subset that do."""
242
+ if not content_shas:
243
+ return set()
244
+
245
+ from sqlalchemy import select
246
+
247
+ from attocode_intel.db.models import Embedding
248
+
249
+ stmt = select(Embedding.content_sha).where(
250
+ Embedding.content_sha.in_(content_shas),
251
+ Embedding.embedding_model == model,
252
+ )
253
+ if hasattr(Embedding, 'vector'):
254
+ stmt = stmt.where(Embedding.vector.isnot(None))
255
+ result = await self._session.execute(stmt.distinct())
256
+ return {row[0] for row in result}
257
+
258
+ async def batch_embedding_stats(
259
+ self, content_shas: set[str], model: str = "default",
260
+ ) -> dict[str, dict]:
261
+ """Get chunk count + last embedded time for content SHAs with embeddings."""
262
+ if not content_shas:
263
+ return {}
264
+
265
+ from sqlalchemy import func, select
266
+
267
+ from attocode_intel.db.models import Embedding
268
+
269
+ stmt = select(
270
+ Embedding.content_sha,
271
+ func.count().label("chunk_count"),
272
+ func.max(Embedding.created_at).label("last_embedded"),
273
+ ).where(
274
+ Embedding.content_sha.in_(content_shas),
275
+ Embedding.embedding_model == model,
276
+ )
277
+ if hasattr(Embedding, 'vector'):
278
+ stmt = stmt.where(Embedding.vector.isnot(None))
279
+ stmt = stmt.group_by(Embedding.content_sha)
280
+ result = await self._session.execute(stmt)
281
+ return {
282
+ row.content_sha: {
283
+ "chunk_count": row.chunk_count,
284
+ "last_embedded": row.last_embedded,
285
+ }
286
+ for row in result
287
+ }
288
+
289
+ async def has_embeddings(self, content_sha: str, model: str = "default") -> bool:
290
+ """Check if embeddings exist for a content_sha+model. Used for dedup gating."""
291
+ from sqlalchemy import select
292
+
293
+ from attocode_intel.db.models import Embedding
294
+
295
+ stmt = select(Embedding.id).where(
296
+ Embedding.content_sha == content_sha,
297
+ Embedding.embedding_model == model,
298
+ )
299
+ if hasattr(Embedding, 'vector'):
300
+ stmt = stmt.where(Embedding.vector.isnot(None))
301
+ result = await self._session.execute(stmt.limit(1))
302
+ return result.scalar_one_or_none() is not None
303
+
304
+ async def find_similar_by_sha(
305
+ self,
306
+ branch_id: uuid.UUID,
307
+ content_sha: str,
308
+ top_k: int = 10,
309
+ model: str = "default",
310
+ ) -> list[dict]:
311
+ """Find files similar to the given content_sha within a branch.
312
+
313
+ Retrieves the embedding vector(s) for the source content_sha, averages
314
+ them if multiple chunks exist, then runs a cosine similarity search
315
+ excluding the source itself.
316
+ """
317
+ from sqlalchemy import text
318
+
319
+ from attocode_intel.storage.branch_overlay import BranchOverlay
320
+
321
+ # 1. Get vectors for the source content_sha
322
+ result = await self._session.execute(
323
+ text("""
324
+ SELECT vector::text FROM embeddings
325
+ WHERE content_sha = :sha AND embedding_model = :model AND vector IS NOT NULL
326
+ """),
327
+ {"sha": content_sha, "model": model},
328
+ )
329
+ rows = result.fetchall()
330
+ if not rows:
331
+ return []
332
+
333
+ # 2. Parse and average vectors
334
+ def parse_vector(vec_str: str) -> list[float]:
335
+ return [float(x) for x in vec_str.strip("[]").split(",")]
336
+
337
+ vectors = [parse_vector(row[0]) for row in rows]
338
+ dim = len(vectors[0])
339
+ if len(vectors) == 1:
340
+ avg_vector = vectors[0]
341
+ else:
342
+ avg_vector = [sum(v[i] for v in vectors) / len(vectors) for i in range(dim)]
343
+
344
+ # 3. Resolve branch manifest
345
+ overlay = BranchOverlay(self._session)
346
+ manifest = await overlay.resolve_manifest(branch_id)
347
+ content_shas = set(manifest.values())
348
+ content_shas.discard(content_sha) # exclude source
349
+
350
+ if not content_shas:
351
+ return []
352
+
353
+ sha_to_path = {sha: path for path, sha in manifest.items()}
354
+
355
+ # 4. Run cosine similarity search
356
+ result = await self._session.execute(
357
+ text("""
358
+ SELECT content_sha, chunk_text, chunk_type, embedding_model,
359
+ 1 - (vector <=> CAST(:qv AS vector)) AS score
360
+ FROM embeddings
361
+ WHERE content_sha = ANY(:shas)
362
+ AND embedding_model = :model
363
+ AND vector IS NOT NULL
364
+ ORDER BY vector <=> CAST(:qv AS vector)
365
+ LIMIT :top_k
366
+ """),
367
+ {
368
+ "qv": "[" + ",".join(str(v) for v in avg_vector) + "]",
369
+ "shas": list(content_shas),
370
+ "model": model,
371
+ "top_k": top_k,
372
+ },
373
+ )
374
+
375
+ results = []
376
+ for row in result:
377
+ results.append({
378
+ "file": sha_to_path.get(row.content_sha, "unknown"),
379
+ "content_sha": row.content_sha,
380
+ "chunk_text": row.chunk_text,
381
+ "score": float(row.score),
382
+ })
383
+ return results
384
+
385
+ async def similarity_search(
386
+ self,
387
+ branch_id: uuid.UUID,
388
+ query_vector: list[float],
389
+ top_k: int = 10,
390
+ model: str = "default",
391
+ file_filter: str = "",
392
+ ) -> list[dict]:
393
+ """Find most similar content within a branch context using pgvector cosine distance.
394
+
395
+ Args:
396
+ branch_id: Branch to scope results to.
397
+ query_vector: Embedded query vector.
398
+ top_k: Number of results to return.
399
+ model: Embedding model name to filter by.
400
+
401
+ Returns:
402
+ List of dicts with file, content_sha, chunk_text, chunk_type, model, score.
403
+ """
404
+ from sqlalchemy import text
405
+
406
+ from attocode_intel.storage.branch_overlay import BranchOverlay
407
+
408
+ overlay = BranchOverlay(self._session)
409
+ manifest = await overlay.resolve_manifest(branch_id)
410
+
411
+ if file_filter:
412
+ import fnmatch
413
+ manifest = {p: s for p, s in manifest.items() if fnmatch.fnmatch(p, file_filter)}
414
+
415
+ content_shas = set(manifest.values())
416
+
417
+ if not content_shas:
418
+ return []
419
+
420
+ sha_to_path = {sha: path for path, sha in manifest.items()}
421
+
422
+ # pgvector cosine distance query — score = 1 - distance (higher = more similar)
423
+ result = await self._session.execute(
424
+ text("""
425
+ SELECT content_sha, chunk_text, chunk_type, embedding_model,
426
+ 1 - (vector <=> CAST(:qv AS vector)) AS score
427
+ FROM embeddings
428
+ WHERE content_sha = ANY(:shas)
429
+ AND embedding_model = :model
430
+ AND vector IS NOT NULL
431
+ ORDER BY vector <=> CAST(:qv AS vector)
432
+ LIMIT :top_k
433
+ """),
434
+ {
435
+ "qv": "[" + ",".join(str(v) for v in query_vector) + "]",
436
+ "shas": list(content_shas),
437
+ "model": model,
438
+ "top_k": top_k,
439
+ },
440
+ )
441
+
442
+ results = []
443
+ for row in result:
444
+ results.append({
445
+ "file": sha_to_path.get(row.content_sha, "unknown"),
446
+ "content_sha": row.content_sha,
447
+ "chunk_text": row.chunk_text,
448
+ "chunk_type": row.chunk_type,
449
+ "model": row.embedding_model,
450
+ "score": float(row.score),
451
+ })
452
+ return results
453
+
454
+ async def multi_branch_similarity_search(
455
+ self,
456
+ session: AsyncSession,
457
+ branch_manifests: dict[str, dict[str, str]],
458
+ query_vector: list[float],
459
+ top_k: int = 20,
460
+ model: str = "",
461
+ file_filter: str = "",
462
+ ) -> list[dict]:
463
+ """Find most similar content across multiple repo branch manifests.
464
+
465
+ Args:
466
+ session: Async DB session (uses self._session if same, but kept
467
+ for API symmetry with callers that pass it explicitly).
468
+ branch_manifests: Mapping of repo_id (str) → manifest (path → content_sha).
469
+ query_vector: Embedded query vector.
470
+ top_k: Number of results to return globally.
471
+ model: Embedding model name to filter by.
472
+ file_filter: Optional glob pattern to filter file paths.
473
+
474
+ Returns:
475
+ List of dicts with repo_id, file, content_sha, chunk_text,
476
+ chunk_type, model, score — sorted by score descending.
477
+ """
478
+ import fnmatch
479
+
480
+ from sqlalchemy import text
481
+
482
+ # 1. Union all content_shas across repos, building reverse lookup
483
+ all_content_shas: set[str] = set()
484
+ # sha → (repo_id, path) for mapping results back
485
+ sha_to_context: dict[str, tuple[str, str]] = {}
486
+
487
+ for repo_id, manifest in branch_manifests.items():
488
+ for path, sha in manifest.items():
489
+ if file_filter and not fnmatch.fnmatch(path, file_filter):
490
+ continue
491
+ all_content_shas.add(sha)
492
+ # First repo wins for a given sha (content-addressed dedup)
493
+ if sha not in sha_to_context:
494
+ sha_to_context[sha] = (repo_id, path)
495
+
496
+ if not all_content_shas:
497
+ return []
498
+
499
+ # 2. Single pgvector query across all repos
500
+ result = await self._session.execute(
501
+ text("""
502
+ SELECT content_sha, chunk_text, chunk_type, embedding_model,
503
+ 1 - (vector <=> CAST(:qv AS vector)) AS score
504
+ FROM embeddings
505
+ WHERE content_sha = ANY(:shas)
506
+ AND embedding_model = :model
507
+ AND vector IS NOT NULL
508
+ ORDER BY vector <=> CAST(:qv AS vector)
509
+ LIMIT :top_k
510
+ """),
511
+ {
512
+ "qv": "[" + ",".join(str(v) for v in query_vector) + "]",
513
+ "shas": list(all_content_shas),
514
+ "model": model,
515
+ "top_k": top_k,
516
+ },
517
+ )
518
+
519
+ # 3. Map results back to repo context
520
+ results = []
521
+ for row in result:
522
+ ctx = sha_to_context.get(row.content_sha)
523
+ repo_id = ctx[0] if ctx else "unknown"
524
+ file_path = ctx[1] if ctx else "unknown"
525
+ results.append({
526
+ "repo_id": repo_id,
527
+ "file": file_path,
528
+ "content_sha": row.content_sha,
529
+ "chunk_text": row.chunk_text,
530
+ "chunk_type": row.chunk_type,
531
+ "model": row.embedding_model,
532
+ "score": float(row.score),
533
+ })
534
+ return results
535
+
536
+ async def gc_orphaned(
537
+ self,
538
+ min_age_minutes: int = 60,
539
+ *,
540
+ repo_id: uuid.UUID | None = None,
541
+ ) -> int:
542
+ """Delete embeddings whose content_sha is not in any branch manifest.
543
+
544
+ Only deletes embeddings older than *min_age_minutes* to avoid racing
545
+ with concurrent indexers that are still writing references.
546
+
547
+ When ``repo_id`` is provided, restricts the DELETE to embeddings
548
+ whose content_sha was ever tracked by a branch of the target
549
+ repo AND is no longer tracked anywhere. ``repo_id=None`` runs
550
+ global GC (used by background workers).
551
+
552
+ Returns count of deleted rows.
553
+ """
554
+ from sqlalchemy import text
555
+
556
+ if repo_id is None:
557
+ result = await self._session.execute(
558
+ text("""
559
+ DELETE FROM embeddings
560
+ WHERE created_at < NOW() - make_interval(mins => :age)
561
+ AND content_sha NOT IN (
562
+ SELECT DISTINCT content_sha FROM branch_files WHERE content_sha IS NOT NULL
563
+ )
564
+ """).bindparams(age=min_age_minutes)
565
+ )
566
+ else:
567
+ result = await self._session.execute(
568
+ text("""
569
+ DELETE FROM embeddings
570
+ WHERE created_at < NOW() - make_interval(mins => :age)
571
+ AND content_sha IN (
572
+ SELECT DISTINCT bf.content_sha
573
+ FROM branch_files bf
574
+ JOIN branches b ON b.id = bf.branch_id
575
+ WHERE b.repo_id = :repo_id AND bf.content_sha IS NOT NULL
576
+ )
577
+ AND content_sha NOT IN (
578
+ SELECT DISTINCT content_sha FROM branch_files WHERE content_sha IS NOT NULL
579
+ )
580
+ """).bindparams(age=min_age_minutes, repo_id=str(repo_id))
581
+ )
582
+ count = result.rowcount
583
+ if count:
584
+ if repo_id is None:
585
+ logger.info(
586
+ "GC: removed %d orphaned embeddings (older than %dm, global)",
587
+ count, min_age_minutes,
588
+ )
589
+ else:
590
+ logger.info(
591
+ "GC: removed %d orphaned embeddings (older than %dm, repo=%s)",
592
+ count, min_age_minutes, repo_id,
593
+ )
594
+ return count