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,597 @@
1
+ """Embedding rotation state machine for the local ``vectors`` store.
2
+
3
+ Migrates every stored embedding from one model to another without ever
4
+ touching (let alone wiping) the live rows — the opposite of the
5
+ destructive-NULL path Phase 1 killed.
6
+
7
+ States::
8
+
9
+ none
10
+ │ embeddings_rotate_start(new_model, new_dim)
11
+ ▼
12
+ pending ─── staging table created, total row count captured
13
+ │
14
+ │ embeddings_rotate_step(batch_size) (call repeatedly)
15
+ ▼
16
+ backfilling ── each batch: read N old rows, embed with new provider,
17
+ │ insert into vectors_rotating
18
+ │
19
+ │ last batch processed (cursor exhausted)
20
+ ▼
21
+ ready_to_cutover
22
+ │
23
+ │ embeddings_rotate_cutover(confirm=True)
24
+ ▼ ┌── rename vectors → vectors_archive
25
+ cutover_done └── rename vectors_rotating → vectors
26
+ │ update store_metadata.dimension
27
+ │ embeddings_rotate_gc_old(confirm=True)
28
+ ▼
29
+ gc_done ──── vectors_archive dropped; rotation metadata cleared
30
+ │
31
+ ▼
32
+ none
33
+
34
+ At any pre-cutover state, ``embeddings_rotate_abort(confirm=True)`` drops
35
+ ``vectors_rotating`` and resets state to ``none`` — the primary ``vectors``
36
+ table is untouched.
37
+
38
+ **Known limitation:** new writes to ``vectors`` that happen DURING rotation
39
+ (e.g. ``notify_file_changed`` triggering a reindex mid-rotation) land in
40
+ the old table and never reach ``vectors_rotating``. After cutover they're
41
+ effectively missing. For Phase 2b the recommendation is to avoid concurrent
42
+ indexing during a rotation, or run a normal ``semantic_search`` reindex
43
+ afterwards to catch up.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import logging
49
+ import os
50
+ import sqlite3
51
+ import struct
52
+ import time
53
+ from dataclasses import dataclass
54
+ from enum import StrEnum
55
+ from typing import TYPE_CHECKING
56
+
57
+ if TYPE_CHECKING:
58
+ from attocode_intel._internal.integrations.context.embeddings import EmbeddingProvider
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+ # Staging table name used for the rotation target.
63
+ STAGING_TABLE = "vectors_rotating"
64
+ # Archive table name used to hold the pre-rotation rows until GC.
65
+ ARCHIVE_TABLE = "vectors_archive"
66
+
67
+ # All store_metadata keys the rotator owns. Clearing these drops all
68
+ # rotation state back to a clean slate.
69
+ _ROTATION_KEYS: tuple[str, ...] = (
70
+ "rotation_state",
71
+ "rotation_from_model",
72
+ "rotation_from_version",
73
+ "rotation_from_dim",
74
+ "rotation_to_model",
75
+ "rotation_to_version",
76
+ "rotation_to_dim",
77
+ "rotation_started_at",
78
+ "rotation_cursor",
79
+ "rotation_processed_rows",
80
+ "rotation_total_rows",
81
+ "rotation_last_error",
82
+ )
83
+
84
+
85
+ class RotationState(StrEnum):
86
+ """Finite state machine for one embedding rotation."""
87
+
88
+ NONE = "none"
89
+ PENDING = "pending"
90
+ BACKFILLING = "backfilling"
91
+ READY_TO_CUTOVER = "ready_to_cutover"
92
+ CUTOVER_DONE = "cutover_done"
93
+ GC_DONE = "gc_done"
94
+ ABORTED = "aborted"
95
+ FAILED = "failed"
96
+
97
+
98
+ @dataclass(slots=True)
99
+ class RotationStatus:
100
+ """Public snapshot of the current rotation (or lack thereof)."""
101
+
102
+ state: RotationState
103
+ from_model: str
104
+ from_version: str
105
+ from_dim: int
106
+ to_model: str
107
+ to_version: str
108
+ to_dim: int
109
+ started_at: float
110
+ processed_rows: int
111
+ total_rows: int
112
+ last_error: str
113
+
114
+ def is_active(self) -> bool:
115
+ return self.state not in (
116
+ RotationState.NONE,
117
+ RotationState.GC_DONE,
118
+ RotationState.ABORTED,
119
+ RotationState.FAILED,
120
+ )
121
+
122
+ def progress_pct(self) -> float:
123
+ if self.total_rows <= 0:
124
+ return 0.0
125
+ return 100.0 * self.processed_rows / self.total_rows
126
+
127
+ def to_dict(self) -> dict[str, object]:
128
+ return {
129
+ "state": self.state.value,
130
+ "from": {
131
+ "model": self.from_model,
132
+ "version": self.from_version,
133
+ "dim": self.from_dim,
134
+ },
135
+ "to": {
136
+ "model": self.to_model,
137
+ "version": self.to_version,
138
+ "dim": self.to_dim,
139
+ },
140
+ "started_at": self.started_at,
141
+ "processed_rows": self.processed_rows,
142
+ "total_rows": self.total_rows,
143
+ "progress_pct": round(self.progress_pct(), 2),
144
+ "last_error": self.last_error,
145
+ }
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # EmbeddingRotator
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ class EmbeddingRotator:
154
+ """Rotation driver operating directly on a ``vectors.db`` file.
155
+
156
+ Deliberately bypasses ``VectorStore`` so that a dimension mismatch
157
+ between the old and new models doesn't trip ``_validate_dimension``.
158
+ All reads / writes go through raw ``sqlite3``; the rotator is
159
+ responsible for its own locking.
160
+ """
161
+
162
+ def __init__(self, db_path: str) -> None:
163
+ self.db_path = db_path
164
+ self._conn: sqlite3.Connection | None = None
165
+ if os.path.exists(db_path):
166
+ self._conn = sqlite3.connect(db_path, check_same_thread=False)
167
+ self._conn.execute("PRAGMA journal_mode=WAL")
168
+
169
+ def _get_conn(self) -> sqlite3.Connection:
170
+ if self._conn is None:
171
+ raise RuntimeError(
172
+ f"EmbeddingRotator: no vectors.db at {self.db_path}"
173
+ )
174
+ return self._conn
175
+
176
+ def close(self) -> None:
177
+ if self._conn:
178
+ self._conn.close()
179
+ self._conn = None
180
+
181
+ # ------------------------------------------------------------------
182
+ # Metadata helpers
183
+ # ------------------------------------------------------------------
184
+
185
+ def _set_meta(self, key: str, value: str) -> None:
186
+ conn = self._get_conn()
187
+ conn.execute(
188
+ "INSERT OR REPLACE INTO store_metadata (key, value) VALUES (?, ?)",
189
+ (key, str(value)),
190
+ )
191
+
192
+ def _get_meta(self, key: str, default: str = "") -> str:
193
+ conn = self._get_conn()
194
+ row = conn.execute(
195
+ "SELECT value FROM store_metadata WHERE key = ?", (key,),
196
+ ).fetchone()
197
+ return row[0] if row else default
198
+
199
+ def _clear_rotation_meta(self) -> None:
200
+ conn = self._get_conn()
201
+ for key in _ROTATION_KEYS:
202
+ conn.execute("DELETE FROM store_metadata WHERE key = ?", (key,))
203
+
204
+ # ------------------------------------------------------------------
205
+ # Status
206
+ # ------------------------------------------------------------------
207
+
208
+ def status(self) -> RotationStatus:
209
+ """Read the current rotation state from ``store_metadata``."""
210
+ if self._conn is None:
211
+ return RotationStatus(
212
+ state=RotationState.NONE,
213
+ from_model="", from_version="", from_dim=0,
214
+ to_model="", to_version="", to_dim=0,
215
+ started_at=0.0,
216
+ processed_rows=0, total_rows=0,
217
+ last_error="vectors.db not found",
218
+ )
219
+ state_raw = self._get_meta("rotation_state", RotationState.NONE.value)
220
+ try:
221
+ state = RotationState(state_raw)
222
+ except ValueError:
223
+ state = RotationState.NONE
224
+ return RotationStatus(
225
+ state=state,
226
+ from_model=self._get_meta("rotation_from_model"),
227
+ from_version=self._get_meta("rotation_from_version"),
228
+ from_dim=int(self._get_meta("rotation_from_dim") or "0"),
229
+ to_model=self._get_meta("rotation_to_model"),
230
+ to_version=self._get_meta("rotation_to_version"),
231
+ to_dim=int(self._get_meta("rotation_to_dim") or "0"),
232
+ started_at=float(self._get_meta("rotation_started_at") or "0"),
233
+ processed_rows=int(self._get_meta("rotation_processed_rows") or "0"),
234
+ total_rows=int(self._get_meta("rotation_total_rows") or "0"),
235
+ last_error=self._get_meta("rotation_last_error"),
236
+ )
237
+
238
+ # ------------------------------------------------------------------
239
+ # start
240
+ # ------------------------------------------------------------------
241
+
242
+ def start(
243
+ self,
244
+ *,
245
+ to_model: str,
246
+ to_version: str,
247
+ to_dim: int,
248
+ ) -> RotationStatus:
249
+ """Begin a new rotation.
250
+
251
+ Raises RuntimeError if a rotation is already active or if the
252
+ main ``vectors`` table is empty.
253
+ """
254
+ conn = self._get_conn()
255
+
256
+ current = self.status()
257
+ if current.is_active():
258
+ raise RuntimeError(
259
+ f"rotation already active (state={current.state.value}); "
260
+ "abort it before starting a new one"
261
+ )
262
+
263
+ # Capture the current "from" metadata from the active store_metadata.
264
+ from_dim = int(self._get_meta("dimension") or "0")
265
+ # "from" model is approximated from the most common model_name on
266
+ # existing rows. If the column was never populated (legacy rows
267
+ # from Phase 0), we fall back to "legacy".
268
+ from_model = self._dominant_model_name(conn) or "legacy"
269
+ from_version = self._dominant_model_version(conn)
270
+
271
+ row_count = conn.execute("SELECT COUNT(*) FROM vectors").fetchone()[0]
272
+ if row_count == 0:
273
+ raise RuntimeError("no vectors to rotate — table is empty")
274
+
275
+ # Clean any prior staging table from a failed rotation.
276
+ conn.execute(f"DROP TABLE IF EXISTS {STAGING_TABLE}")
277
+ # Mirror the vectors table schema. Using a literal CREATE keeps
278
+ # the PK + NOT NULL / DEFAULT constraints that a
279
+ # ``CREATE TABLE x AS SELECT`` would strip.
280
+ conn.execute(
281
+ f"""
282
+ CREATE TABLE {STAGING_TABLE} (
283
+ id TEXT PRIMARY KEY,
284
+ file_path TEXT NOT NULL,
285
+ chunk_type TEXT NOT NULL,
286
+ name TEXT NOT NULL,
287
+ text TEXT NOT NULL,
288
+ vector BLOB NOT NULL,
289
+ model_name TEXT NOT NULL DEFAULT '',
290
+ model_version TEXT NOT NULL DEFAULT '',
291
+ dimension INTEGER NOT NULL DEFAULT 0,
292
+ produced_at REAL NOT NULL DEFAULT 0,
293
+ blob_oid TEXT NOT NULL DEFAULT '',
294
+ action_hash TEXT NOT NULL DEFAULT ''
295
+ )
296
+ """
297
+ )
298
+ # Matching indexes on the staging table so they're in place for
299
+ # search as soon as the cutover rename swap completes.
300
+ conn.execute(
301
+ f"CREATE INDEX IF NOT EXISTS idx_rot_vectors_file "
302
+ f"ON {STAGING_TABLE}(file_path)"
303
+ )
304
+ conn.execute(
305
+ f"CREATE INDEX IF NOT EXISTS idx_rot_vectors_model "
306
+ f"ON {STAGING_TABLE}(model_name, model_version)"
307
+ )
308
+ conn.execute(
309
+ f"CREATE INDEX IF NOT EXISTS idx_rot_vectors_blob "
310
+ f"ON {STAGING_TABLE}(blob_oid)"
311
+ )
312
+
313
+ self._clear_rotation_meta()
314
+ self._set_meta("rotation_state", RotationState.PENDING.value)
315
+ self._set_meta("rotation_from_model", from_model)
316
+ self._set_meta("rotation_from_version", from_version)
317
+ self._set_meta("rotation_from_dim", str(from_dim))
318
+ self._set_meta("rotation_to_model", to_model)
319
+ self._set_meta("rotation_to_version", to_version)
320
+ self._set_meta("rotation_to_dim", str(to_dim))
321
+ self._set_meta("rotation_started_at", str(time.time()))
322
+ self._set_meta("rotation_cursor", "")
323
+ self._set_meta("rotation_processed_rows", "0")
324
+ self._set_meta("rotation_total_rows", str(row_count))
325
+ self._set_meta("rotation_last_error", "")
326
+ conn.commit()
327
+
328
+ return self.status()
329
+
330
+ def _dominant_model_name(self, conn: sqlite3.Connection) -> str:
331
+ """Return the most common ``model_name`` in the vectors table.
332
+
333
+ Returns an empty string if no rows have a non-empty name (e.g.
334
+ a fresh v2 store where rows were inserted before provenance was
335
+ wired through).
336
+ """
337
+ try:
338
+ row = conn.execute(
339
+ "SELECT model_name, COUNT(*) as c FROM vectors "
340
+ "WHERE model_name != '' "
341
+ "GROUP BY model_name ORDER BY c DESC LIMIT 1"
342
+ ).fetchone()
343
+ except sqlite3.OperationalError:
344
+ return ""
345
+ return row[0] if row else ""
346
+
347
+ def _dominant_model_version(self, conn: sqlite3.Connection) -> str:
348
+ try:
349
+ row = conn.execute(
350
+ "SELECT model_version, COUNT(*) as c FROM vectors "
351
+ "WHERE model_version != '' "
352
+ "GROUP BY model_version ORDER BY c DESC LIMIT 1"
353
+ ).fetchone()
354
+ except sqlite3.OperationalError:
355
+ return ""
356
+ return row[0] if row else ""
357
+
358
+ # ------------------------------------------------------------------
359
+ # step — advance the backfill
360
+ # ------------------------------------------------------------------
361
+
362
+ def step(
363
+ self,
364
+ provider: EmbeddingProvider,
365
+ batch_size: int = 32,
366
+ ) -> int:
367
+ """Embed one batch of rows with ``provider`` and write them to
368
+ the staging table.
369
+
370
+ Returns the number of rows processed. Returns 0 when the
371
+ rotation is done (state transitions to ``ready_to_cutover``).
372
+
373
+ Raises RuntimeError if there's no active rotation or if
374
+ ``provider.dimension()`` doesn't match the target dim recorded
375
+ at ``start``.
376
+ """
377
+ conn = self._get_conn()
378
+ status = self.status()
379
+
380
+ # Idempotent no-op once the backfill is already done — callers
381
+ # that drive step() in a while-loop would otherwise have to
382
+ # duplicate state-check logic.
383
+ if status.state == RotationState.READY_TO_CUTOVER:
384
+ return 0
385
+
386
+ if status.state not in (RotationState.PENDING, RotationState.BACKFILLING):
387
+ raise RuntimeError(
388
+ f"step() requires state pending|backfilling, got {status.state.value}"
389
+ )
390
+
391
+ provider_dim = provider.dimension()
392
+ if provider_dim != status.to_dim:
393
+ self._set_meta(
394
+ "rotation_last_error",
395
+ f"provider dim {provider_dim} != target dim {status.to_dim}",
396
+ )
397
+ self._set_meta("rotation_state", RotationState.FAILED.value)
398
+ conn.commit()
399
+ raise RuntimeError(
400
+ f"provider {provider.name} has dim {provider_dim}, "
401
+ f"but rotation target dim is {status.to_dim}"
402
+ )
403
+
404
+ last_cursor = self._get_meta("rotation_cursor", "")
405
+ rows = conn.execute(
406
+ "SELECT id, file_path, chunk_type, name, text, blob_oid, action_hash "
407
+ "FROM vectors WHERE id > ? ORDER BY id LIMIT ?",
408
+ (last_cursor, batch_size),
409
+ ).fetchall()
410
+
411
+ if not rows:
412
+ # Nothing left to process.
413
+ self._set_meta("rotation_state", RotationState.READY_TO_CUTOVER.value)
414
+ conn.commit()
415
+ return 0
416
+
417
+ # Promote to BACKFILLING on the first processed batch.
418
+ if status.state == RotationState.PENDING:
419
+ self._set_meta("rotation_state", RotationState.BACKFILLING.value)
420
+
421
+ texts = [r[4] for r in rows]
422
+ try:
423
+ new_vectors = provider.embed(texts)
424
+ except Exception as exc: # noqa: BLE001 — surface provider errors loudly
425
+ self._set_meta("rotation_last_error", f"embed failed: {exc}")
426
+ self._set_meta("rotation_state", RotationState.FAILED.value)
427
+ conn.commit()
428
+ raise
429
+
430
+ if len(new_vectors) != len(rows):
431
+ self._set_meta(
432
+ "rotation_last_error",
433
+ f"embed returned {len(new_vectors)} vectors for {len(rows)} inputs",
434
+ )
435
+ self._set_meta("rotation_state", RotationState.FAILED.value)
436
+ conn.commit()
437
+ raise RuntimeError("embed() length mismatch")
438
+
439
+ produced_at = time.time()
440
+ provider_name = provider.name # property, not callable
441
+ to_version = status.to_version
442
+ to_dim = status.to_dim
443
+
444
+ insert_sql = (
445
+ f"INSERT OR REPLACE INTO {STAGING_TABLE} "
446
+ "(id, file_path, chunk_type, name, text, vector, "
447
+ "model_name, model_version, dimension, produced_at, "
448
+ "blob_oid, action_hash) "
449
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
450
+ )
451
+ for (row, vec) in zip(rows, new_vectors, strict=True):
452
+ if len(vec) != to_dim:
453
+ self._set_meta(
454
+ "rotation_last_error",
455
+ f"provider returned vec of len {len(vec)}, expected {to_dim}",
456
+ )
457
+ self._set_meta("rotation_state", RotationState.FAILED.value)
458
+ conn.commit()
459
+ raise RuntimeError(
460
+ f"provider returned vec of wrong length: {len(vec)} != {to_dim}"
461
+ )
462
+ packed = struct.pack(f"{to_dim}f", *vec)
463
+ conn.execute(
464
+ insert_sql,
465
+ (
466
+ row[0], # id
467
+ row[1], # file_path
468
+ row[2], # chunk_type
469
+ row[3], # name
470
+ row[4], # text
471
+ packed, # new vector
472
+ provider_name, # model_name
473
+ to_version, # model_version
474
+ to_dim, # dimension
475
+ produced_at, # produced_at
476
+ row[5], # blob_oid (carried over)
477
+ row[6], # action_hash (carried over)
478
+ ),
479
+ )
480
+
481
+ # Update cursor + processed count.
482
+ last_id = rows[-1][0]
483
+ processed = status.processed_rows + len(rows)
484
+ self._set_meta("rotation_cursor", last_id)
485
+ self._set_meta("rotation_processed_rows", str(processed))
486
+ conn.commit()
487
+
488
+ # If we drained the table, transition to ready_to_cutover.
489
+ if len(rows) < batch_size:
490
+ # We got fewer rows than requested, so the table is exhausted.
491
+ self._set_meta("rotation_state", RotationState.READY_TO_CUTOVER.value)
492
+ conn.commit()
493
+
494
+ return len(rows)
495
+
496
+ # ------------------------------------------------------------------
497
+ # cutover
498
+ # ------------------------------------------------------------------
499
+
500
+ def cutover(self) -> RotationStatus:
501
+ """Atomically swap ``vectors`` and ``vectors_rotating``.
502
+
503
+ The existing ``vectors`` becomes ``vectors_archive`` (still
504
+ present until ``gc_old``). Subsequent reads / writes hit the
505
+ new table. ``store_metadata.dimension`` is updated to the new
506
+ target dim.
507
+
508
+ Requires state ``ready_to_cutover``.
509
+ """
510
+ conn = self._get_conn()
511
+ status = self.status()
512
+ if status.state != RotationState.READY_TO_CUTOVER:
513
+ raise RuntimeError(
514
+ f"cutover() requires state ready_to_cutover, got {status.state.value}"
515
+ )
516
+
517
+ try:
518
+ conn.execute("BEGIN IMMEDIATE")
519
+ conn.execute(f"DROP TABLE IF EXISTS {ARCHIVE_TABLE}")
520
+ conn.execute(f"ALTER TABLE vectors RENAME TO {ARCHIVE_TABLE}")
521
+ conn.execute(f"ALTER TABLE {STAGING_TABLE} RENAME TO vectors")
522
+ # Update stored dimension pointer so next VectorStore open
523
+ # doesn't trip the mismatch check.
524
+ conn.execute(
525
+ "INSERT OR REPLACE INTO store_metadata (key, value) VALUES ('dimension', ?)",
526
+ (str(status.to_dim),),
527
+ )
528
+ conn.execute(
529
+ "INSERT OR REPLACE INTO store_metadata (key, value) VALUES (?, ?)",
530
+ ("rotation_state", RotationState.CUTOVER_DONE.value),
531
+ )
532
+ # Invalidate the vector cache version so open VectorStores
533
+ # reload. Uses a monotonic bump stored in store_metadata.
534
+ prev_ver = int(self._get_meta("_rotator_cache_ver") or "0")
535
+ conn.execute(
536
+ "INSERT OR REPLACE INTO store_metadata (key, value) VALUES (?, ?)",
537
+ ("_rotator_cache_ver", str(prev_ver + 1)),
538
+ )
539
+ conn.commit()
540
+ except sqlite3.Error:
541
+ conn.execute("ROLLBACK")
542
+ self._set_meta("rotation_state", RotationState.FAILED.value)
543
+ self._set_meta("rotation_last_error", "cutover SQL failed")
544
+ conn.commit()
545
+ raise
546
+
547
+ return self.status()
548
+
549
+ # ------------------------------------------------------------------
550
+ # gc_old
551
+ # ------------------------------------------------------------------
552
+
553
+ def gc_old(self) -> RotationStatus:
554
+ """Drop the archive table and clear rotation metadata.
555
+
556
+ Requires state ``cutover_done``. Transitions through ``gc_done``
557
+ back to ``none``.
558
+ """
559
+ conn = self._get_conn()
560
+ status = self.status()
561
+ if status.state != RotationState.CUTOVER_DONE:
562
+ raise RuntimeError(
563
+ f"gc_old() requires state cutover_done, got {status.state.value}"
564
+ )
565
+ conn.execute(f"DROP TABLE IF EXISTS {ARCHIVE_TABLE}")
566
+ self._set_meta("rotation_state", RotationState.GC_DONE.value)
567
+ conn.commit()
568
+ # Then clean the rotation metadata entirely so state goes back to NONE.
569
+ self._clear_rotation_meta()
570
+ conn.commit()
571
+ return self.status()
572
+
573
+ # ------------------------------------------------------------------
574
+ # abort
575
+ # ------------------------------------------------------------------
576
+
577
+ def abort(self) -> RotationStatus:
578
+ """Cancel a pre-cutover rotation.
579
+
580
+ Drops ``vectors_rotating`` and clears rotation metadata. The
581
+ primary ``vectors`` table is untouched. Legal from any state
582
+ other than ``cutover_done`` / ``gc_done``.
583
+ """
584
+ conn = self._get_conn()
585
+ status = self.status()
586
+ if status.state in (RotationState.CUTOVER_DONE, RotationState.GC_DONE):
587
+ raise RuntimeError(
588
+ f"abort() would corrupt state {status.state.value}; "
589
+ "use gc_old instead or restore from a snapshot"
590
+ )
591
+ conn.execute(f"DROP TABLE IF EXISTS {STAGING_TABLE}")
592
+ self._set_meta("rotation_state", RotationState.ABORTED.value)
593
+ conn.commit()
594
+ # Clear metadata back to NONE so a fresh rotation can begin.
595
+ self._clear_rotation_meta()
596
+ conn.commit()
597
+ return self.status()