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,795 @@
1
+ """SQLite-backed vector store for semantic search.
2
+
3
+ Stores embeddings as packed float32 BLOBs in SQLite. Uses numpy-accelerated
4
+ batch cosine similarity with in-memory caching for fast retrieval.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import math
11
+ import os
12
+ import sqlite3
13
+ import struct
14
+ import threading
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from typing import Any
18
+
19
+ try:
20
+ import numpy as np
21
+
22
+ _HAS_NUMPY = True
23
+ except ImportError:
24
+ np = None # type: ignore[assignment]
25
+ _HAS_NUMPY = False
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Schema version for the vectors DB. Bump this when adding columns so the
30
+ # store detects upgrades automatically and runs the appropriate ALTER
31
+ # scripts at startup.
32
+ VECTOR_STORE_SCHEMA_VERSION = "2"
33
+
34
+
35
+ class VectorStoreDimensionMismatchError(RuntimeError):
36
+ """Raised when the running embedding provider's dimension does not match
37
+ the vectors already persisted on disk.
38
+
39
+ The old behavior in this store was to silently DELETE every row on
40
+ dim mismatch. That was the local-side sibling of the server-side
41
+ ``UPDATE embeddings SET vector = NULL`` footgun — quietly destroying
42
+ user data because the system couldn't figure out the right thing to do.
43
+
44
+ Now: loud error. Caller (``SemanticSearchManager``) catches it once,
45
+ logs a visible warning, and puts the store into a degraded read-only
46
+ mode until the user resolves it (via ``clear_embeddings`` once that
47
+ tool lands in phase 2, or by picking a provider that matches).
48
+ """
49
+
50
+ def __init__(self, *, stored: int, expected: int, db_path: str) -> None:
51
+ self.stored = stored
52
+ self.expected = expected
53
+ self.db_path = db_path
54
+ super().__init__(
55
+ f"vector store at {db_path} has stored dim {stored} but provider "
56
+ f"expects dim {expected}. Refusing to wipe the existing vectors. "
57
+ f"Resolve by choosing a provider with matching dim, or (phase 2) "
58
+ f"run embeddings_rotate_model() or clear_embeddings(confirm=True)."
59
+ )
60
+
61
+
62
+ class VectorStoreRotationActiveError(RuntimeError):
63
+ """Raised when a destructive write hits the vectors table while an
64
+ embedding rotation is still active.
65
+
66
+ Mid-rotation writes to the primary ``vectors`` table would be lost
67
+ after cutover. Any destructive write during ``pending``,
68
+ ``backfilling``, ``ready_to_cutover``, or ``cutover_done`` raises
69
+ rather than silently corrupting the rotation source data.
70
+ """
71
+
72
+ def __init__(self, *, rotation_state: str, db_path: str) -> None:
73
+ self.rotation_state = rotation_state
74
+ self.db_path = db_path
75
+ super().__init__(
76
+ f"vector store at {db_path} has an active rotation "
77
+ f"(state={rotation_state}). Refusing destructive writes "
78
+ f"until the rotation completes or is aborted. Use "
79
+ f"embeddings_rotate_abort() to cancel, or wait for "
80
+ f"embeddings_rotate_cutover() + embeddings_rotate_gc_old()."
81
+ )
82
+
83
+
84
+ @dataclass(slots=True)
85
+ class VectorEntry:
86
+ """A stored vector with metadata."""
87
+
88
+ id: str
89
+ file_path: str
90
+ chunk_type: str # "file", "function", "class"
91
+ name: str
92
+ text: str
93
+ vector: list[float]
94
+ # Provenance — optional; empty defaults keep callers that don't care
95
+ # working. New indexers should fill these in.
96
+ blob_oid: str = ""
97
+ action_hash: str = ""
98
+
99
+
100
+ @dataclass(slots=True)
101
+ class SearchResult:
102
+ """A search result with similarity score."""
103
+
104
+ id: str
105
+ file_path: str
106
+ chunk_type: str
107
+ name: str
108
+ text: str
109
+ score: float
110
+
111
+
112
+ def _pack_vector(vec: list[float]) -> bytes:
113
+ """Pack a float vector into bytes."""
114
+ return struct.pack(f"{len(vec)}f", *vec)
115
+
116
+
117
+ def _unpack_vector(data: bytes, dim: int) -> list[float]:
118
+ """Unpack bytes into a float vector."""
119
+ return list(struct.unpack(f"{dim}f", data))
120
+
121
+
122
+ def _cosine_similarity(a: list[float], b: list[float]) -> float:
123
+ """Compute cosine similarity between two vectors (pure Python fallback)."""
124
+ if len(a) != len(b) or not a:
125
+ return 0.0
126
+ dot = sum(x * y for x, y in zip(a, b, strict=False))
127
+ norm_a = math.sqrt(sum(x * x for x in a))
128
+ norm_b = math.sqrt(sum(x * x for x in b))
129
+ if norm_a == 0.0 or norm_b == 0.0:
130
+ return 0.0
131
+ return dot / (norm_a * norm_b)
132
+
133
+
134
+ def _batch_cosine_similarity(
135
+ query: np.ndarray,
136
+ matrix: np.ndarray,
137
+ ) -> np.ndarray:
138
+ """Vectorized cosine similarity: query (D,) vs matrix (N, D) → scores (N,).
139
+
140
+ Uses numpy BLAS for a single matrix-vector multiply instead of N Python loops.
141
+ """
142
+ query_norm = np.linalg.norm(query)
143
+ if query_norm == 0.0 or len(matrix) == 0:
144
+ return np.zeros(len(matrix), dtype=np.float32)
145
+
146
+ query_normed = query / query_norm
147
+ norms = np.linalg.norm(matrix, axis=1)
148
+
149
+ scores = np.zeros(len(matrix), dtype=np.float32)
150
+ mask = norms > 0
151
+ scores[mask] = (matrix[mask] @ query_normed) / norms[mask]
152
+ return scores
153
+
154
+
155
+ @dataclass(slots=True)
156
+ class VectorStore:
157
+ """SQLite-backed vector store with numpy-accelerated search.
158
+
159
+ Usage::
160
+
161
+ store = VectorStore(db_path="vectors.db", dimension=384)
162
+ store.upsert(VectorEntry(id="f1", ...))
163
+ results = store.search(query_vector, top_k=10)
164
+ """
165
+
166
+ db_path: str
167
+ dimension: int
168
+ # Optional provenance for new inserts. If set, every upsert records these
169
+ # fields into the vectors row so we can later answer "which model made
170
+ # this?" and detect silent drift.
171
+ model_name: str = ""
172
+ model_version: str = ""
173
+ # When True, a dimension mismatch puts the store in degraded read-only
174
+ # mode instead of raising. Used by callers that want to show a nice
175
+ # error in the UI rather than crash on import.
176
+ strict_dimension: bool = True
177
+ # Set by _validate_dimension on mismatch so callers can detect degraded
178
+ # mode without catching the exception.
179
+ degraded: bool = False
180
+ degraded_reason: str = ""
181
+ _conn: sqlite3.Connection | None = field(default=None, repr=False)
182
+ _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
183
+ # In-memory vector cache for numpy batch search
184
+ _vec_matrix: Any = field(default=None, repr=False) # np.ndarray (N, D)
185
+ _vec_meta: list[tuple] | None = field(default=None, repr=False)
186
+ _vec_cache_version: int = field(default=0, repr=False)
187
+ _vec_loaded_version: int = field(default=-1, repr=False)
188
+ # Codex M5: last observed value of store_metadata._rotator_cache_ver.
189
+ # Checked before each search so a cutover in another process
190
+ # correctly invalidates this open store's in-memory cache.
191
+ _last_external_cache_ver: int = field(default=0, repr=False)
192
+
193
+ def __post_init__(self) -> None:
194
+ # Ensure directory exists
195
+ os.makedirs(os.path.dirname(self.db_path) or ".", exist_ok=True)
196
+ self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
197
+ self._conn.execute("PRAGMA journal_mode=WAL")
198
+ self._create_tables()
199
+ self._migrate_schema()
200
+ self._validate_dimension()
201
+
202
+ def _get_conn(self) -> sqlite3.Connection:
203
+ """Return the active connection or raise if closed."""
204
+ if self._conn is None:
205
+ raise RuntimeError("VectorStore connection is closed")
206
+ return self._conn
207
+
208
+ def _create_tables(self) -> None:
209
+ conn = self._get_conn()
210
+ # Schema v2 — includes provenance columns. Pre-v2 stores have the
211
+ # same base shape and are migrated in _migrate_schema().
212
+ conn.execute("""
213
+ CREATE TABLE IF NOT EXISTS vectors (
214
+ id TEXT PRIMARY KEY,
215
+ file_path TEXT NOT NULL,
216
+ chunk_type TEXT NOT NULL,
217
+ name TEXT NOT NULL,
218
+ text TEXT NOT NULL,
219
+ vector BLOB NOT NULL,
220
+ model_name TEXT NOT NULL DEFAULT '',
221
+ model_version TEXT NOT NULL DEFAULT '',
222
+ dimension INTEGER NOT NULL DEFAULT 0,
223
+ produced_at REAL NOT NULL DEFAULT 0,
224
+ blob_oid TEXT NOT NULL DEFAULT '',
225
+ action_hash TEXT NOT NULL DEFAULT ''
226
+ )
227
+ """)
228
+ conn.execute(
229
+ "CREATE INDEX IF NOT EXISTS idx_vectors_file ON vectors(file_path)"
230
+ )
231
+ # The model/blob indexes reference columns that a pre-v2 table may not
232
+ # have yet — they're created in ``_migrate_schema`` after the columns
233
+ # are guaranteed to exist.
234
+ # Metadata table for tracking index freshness per file
235
+ conn.execute("""
236
+ CREATE TABLE IF NOT EXISTS file_metadata (
237
+ file_path TEXT PRIMARY KEY,
238
+ last_indexed_at REAL NOT NULL,
239
+ file_mtime REAL NOT NULL,
240
+ chunk_count INTEGER NOT NULL DEFAULT 0
241
+ )
242
+ """)
243
+ # Store-level metadata (dimension, schema_version, active model, etc.)
244
+ conn.execute("""
245
+ CREATE TABLE IF NOT EXISTS store_metadata (
246
+ key TEXT PRIMARY KEY,
247
+ value TEXT NOT NULL
248
+ )
249
+ """)
250
+ conn.commit()
251
+
252
+ def _migrate_schema(self) -> None:
253
+ """Additively migrate an older vectors table to the current schema.
254
+
255
+ Pre-v2 stores lack the provenance columns (``model_name``,
256
+ ``model_version``, ``dimension``, ``produced_at``, ``blob_oid``,
257
+ ``action_hash``). We ADD COLUMN each missing one with a safe default;
258
+ existing rows get a 'legacy' marker so later tools can identify
259
+ them for re-indexing.
260
+
261
+ Idempotent: running on a fresh v2 store is a no-op.
262
+ """
263
+ conn = self._get_conn()
264
+ columns = {
265
+ row[1]
266
+ for row in conn.execute("PRAGMA table_info(vectors)").fetchall()
267
+ }
268
+ added: list[str] = []
269
+ additive_columns = [
270
+ ("model_name", "TEXT NOT NULL DEFAULT ''"),
271
+ ("model_version", "TEXT NOT NULL DEFAULT ''"),
272
+ ("dimension", "INTEGER NOT NULL DEFAULT 0"),
273
+ ("produced_at", "REAL NOT NULL DEFAULT 0"),
274
+ ("blob_oid", "TEXT NOT NULL DEFAULT ''"),
275
+ ("action_hash", "TEXT NOT NULL DEFAULT ''"),
276
+ ]
277
+ for col, ddl in additive_columns:
278
+ if col not in columns:
279
+ conn.execute(f"ALTER TABLE vectors ADD COLUMN {col} {ddl}")
280
+ added.append(col)
281
+
282
+ if added:
283
+ # Mark pre-v2 rows so callers can find and re-index them later.
284
+ conn.execute(
285
+ "UPDATE vectors SET model_name = 'legacy-pre-v2' "
286
+ "WHERE model_name = '' AND length(vector) > 0"
287
+ )
288
+ logger.info(
289
+ "vector_store: migrated v1 schema -> v2 (added columns: %s)",
290
+ ", ".join(added),
291
+ )
292
+
293
+ # Ensure indexes exist on newly-added columns.
294
+ conn.execute(
295
+ "CREATE INDEX IF NOT EXISTS idx_vectors_model ON vectors(model_name, model_version)"
296
+ )
297
+ conn.execute(
298
+ "CREATE INDEX IF NOT EXISTS idx_vectors_blob ON vectors(blob_oid)"
299
+ )
300
+
301
+ # Record schema version.
302
+ conn.execute(
303
+ "INSERT OR REPLACE INTO store_metadata (key, value) VALUES ('schema_version', ?)",
304
+ (VECTOR_STORE_SCHEMA_VERSION,),
305
+ )
306
+ conn.commit()
307
+
308
+ def _validate_dimension(self) -> None:
309
+ """Check stored dimension matches current.
310
+
311
+ Behavior change vs the old code: a mismatch NO LONGER silently wipes
312
+ ``vectors`` + ``file_metadata``. Instead:
313
+
314
+ - If ``strict_dimension`` is True (the default): raise
315
+ :class:`VectorStoreDimensionMismatchError`. The caller is expected
316
+ to handle this by switching providers, running a rotation, or
317
+ invoking the (phase-2) ``clear_embeddings`` tool.
318
+ - If ``strict_dimension`` is False: put the store into degraded
319
+ read-only mode by setting ``self.degraded`` and
320
+ ``self.degraded_reason``, and bail out of the constructor. Writes
321
+ then refuse to land.
322
+
323
+ Why change: silent wipe meant every `attocode` invocation with a
324
+ different env-var / model flag deleted the user's embedding index
325
+ without ever telling them. That's the exact footgun this whole
326
+ work item is fixing.
327
+ """
328
+ conn = self._get_conn()
329
+ row = conn.execute(
330
+ "SELECT value FROM store_metadata WHERE key = 'dimension'"
331
+ ).fetchone()
332
+ if row:
333
+ stored_dim = int(row[0])
334
+ if stored_dim != self.dimension:
335
+ # Non-destructive: refuse to proceed.
336
+ msg = (
337
+ f"embedding dimension mismatch (stored={stored_dim}, "
338
+ f"expected={self.dimension})"
339
+ )
340
+ if self.strict_dimension:
341
+ logger.error(
342
+ "%s; refusing to wipe vectors. Use embeddings_rotate_model "
343
+ "or clear_embeddings(confirm=true) to resolve.", msg,
344
+ )
345
+ raise VectorStoreDimensionMismatchError(
346
+ stored=stored_dim,
347
+ expected=self.dimension,
348
+ db_path=self.db_path,
349
+ )
350
+ logger.warning(
351
+ "%s; store is in DEGRADED read-only mode. "
352
+ "Existing vectors preserved.", msg,
353
+ )
354
+ self.degraded = True
355
+ self.degraded_reason = "dimension_mismatch"
356
+ return
357
+ else:
358
+ conn.execute(
359
+ "INSERT OR REPLACE INTO store_metadata (key, value) VALUES ('dimension', ?)",
360
+ (str(self.dimension),),
361
+ )
362
+ conn.commit()
363
+
364
+ def _guard_writes(self) -> None:
365
+ """Refuse writes when the store is in degraded mode."""
366
+ if self.degraded:
367
+ raise VectorStoreDimensionMismatchError(
368
+ stored=0,
369
+ expected=self.dimension,
370
+ db_path=self.db_path,
371
+ )
372
+ self._check_rotation_lock()
373
+
374
+ def _check_rotation_lock(self) -> None:
375
+ """Reject destructive writes while a rotation is mid-flight.
376
+
377
+ ``clear_*`` / ``upsert*`` must not run while an embedding rotation
378
+ is still reading from the primary ``vectors`` table. Reads
379
+ ``store_metadata.rotation_state`` and raises
380
+ :class:`VectorStoreRotationActiveError` for active states.
381
+ Pre-rotation (``none``), aborted, failed, and completed-and-GCed
382
+ states pass through.
383
+ """
384
+ conn = self._get_conn()
385
+ try:
386
+ row = conn.execute(
387
+ "SELECT value FROM store_metadata WHERE key = 'rotation_state'"
388
+ ).fetchone()
389
+ except sqlite3.OperationalError:
390
+ # Pre-v2 store without a store_metadata row yet — treat as
391
+ # no rotation.
392
+ return
393
+ if row is None:
394
+ return
395
+ state = str(row[0])
396
+ active_states = {
397
+ "pending", "backfilling", "ready_to_cutover", "cutover_done",
398
+ }
399
+ if state in active_states:
400
+ raise VectorStoreRotationActiveError(
401
+ rotation_state=state, db_path=self.db_path,
402
+ )
403
+
404
+ def _check_external_cache_bump(self) -> None:
405
+ """Pick up cache invalidations from other processes.
406
+
407
+ ``EmbeddingRotator.cutover()`` writes ``_rotator_cache_ver`` into
408
+ ``store_metadata`` to signal already-open ``VectorStore`` instances
409
+ to reload their in-memory numpy matrix. We compare the stored
410
+ value against the last seen and bump the local
411
+ ``_vec_cache_version`` on drift so the next search triggers a
412
+ reload.
413
+ """
414
+ conn = self._get_conn()
415
+ try:
416
+ row = conn.execute(
417
+ "SELECT value FROM store_metadata WHERE key = '_rotator_cache_ver'"
418
+ ).fetchone()
419
+ except sqlite3.OperationalError:
420
+ return
421
+ if row is None:
422
+ return
423
+ try:
424
+ external = int(row[0])
425
+ except (TypeError, ValueError):
426
+ return
427
+ if external > self._last_external_cache_ver:
428
+ self._last_external_cache_ver = external
429
+ # Bump the local version so _search_numpy reloads.
430
+ self._vec_cache_version += 1
431
+
432
+ def upsert(self, entry: VectorEntry) -> None:
433
+ """Insert or update a vector entry."""
434
+ self._guard_writes()
435
+ conn = self._get_conn()
436
+ packed = _pack_vector(entry.vector)
437
+ now = time.time()
438
+ with self._lock:
439
+ conn.execute(
440
+ """INSERT OR REPLACE INTO vectors
441
+ (id, file_path, chunk_type, name, text, vector,
442
+ model_name, model_version, dimension, produced_at,
443
+ blob_oid, action_hash)
444
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
445
+ (
446
+ entry.id, entry.file_path, entry.chunk_type, entry.name,
447
+ entry.text, packed,
448
+ self.model_name, self.model_version, self.dimension,
449
+ now, entry.blob_oid, entry.action_hash,
450
+ ),
451
+ )
452
+ conn.commit()
453
+ self._vec_cache_version += 1
454
+
455
+ def upsert_batch(self, entries: list[VectorEntry]) -> None:
456
+ """Batch insert/update vector entries."""
457
+ self._guard_writes()
458
+ conn = self._get_conn()
459
+ now = time.time()
460
+ rows = [
461
+ (
462
+ e.id, e.file_path, e.chunk_type, e.name, e.text,
463
+ _pack_vector(e.vector),
464
+ self.model_name, self.model_version, self.dimension,
465
+ now, e.blob_oid, e.action_hash,
466
+ )
467
+ for e in entries
468
+ ]
469
+ with self._lock:
470
+ conn.executemany(
471
+ """INSERT OR REPLACE INTO vectors
472
+ (id, file_path, chunk_type, name, text, vector,
473
+ model_name, model_version, dimension, produced_at,
474
+ blob_oid, action_hash)
475
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
476
+ rows,
477
+ )
478
+ conn.commit()
479
+ self._vec_cache_version += 1
480
+
481
+ def delete_by_file(self, file_path: str) -> int:
482
+ """Delete all entries for a file. Returns count deleted."""
483
+ self._guard_writes()
484
+ conn = self._get_conn()
485
+ with self._lock:
486
+ cursor = conn.execute(
487
+ "DELETE FROM vectors WHERE file_path = ?", (file_path,),
488
+ )
489
+ conn.commit()
490
+ self._vec_cache_version += 1
491
+ return cursor.rowcount
492
+
493
+ def clear_all(self) -> int:
494
+ """Delete every vector + file-metadata row. Returns count deleted.
495
+
496
+ Preserves the ``store_metadata`` table (schema version + stored
497
+ dimension) so a subsequent reopen doesn't trip the dim-mismatch
498
+ check. That's the whole point of clear_all vs ``rm -f embeddings.db``:
499
+ it's the safe reset path.
500
+
501
+ Codex B3: refuses to run during an active rotation.
502
+ """
503
+ self._guard_writes()
504
+ conn = self._get_conn()
505
+ with self._lock:
506
+ cursor = conn.execute("DELETE FROM vectors")
507
+ conn.execute("DELETE FROM file_metadata")
508
+ conn.commit()
509
+ self._vec_cache_version += 1
510
+ return cursor.rowcount
511
+
512
+ def clear_by_model(self, model_name: str, model_version: str = "") -> int:
513
+ """Delete only vectors for a given model (+ optional version).
514
+
515
+ Returns count deleted. Used during embedding rotation's GC phase —
516
+ drops old-model rows once the new model is live.
517
+
518
+ Codex B3: refuses to run during an active rotation.
519
+ """
520
+ self._guard_writes()
521
+ conn = self._get_conn()
522
+ with self._lock:
523
+ if model_version:
524
+ cursor = conn.execute(
525
+ "DELETE FROM vectors WHERE model_name = ? AND model_version = ?",
526
+ (model_name, model_version),
527
+ )
528
+ else:
529
+ cursor = conn.execute(
530
+ "DELETE FROM vectors WHERE model_name = ?",
531
+ (model_name,),
532
+ )
533
+ conn.commit()
534
+ self._vec_cache_version += 1
535
+ return cursor.rowcount
536
+
537
+ # ------------------------------------------------------------------
538
+ # In-memory vector cache for numpy batch search
539
+ # ------------------------------------------------------------------
540
+
541
+ def _load_vector_cache(self) -> None:
542
+ """Load all vectors from SQLite into a numpy matrix."""
543
+ conn = self._get_conn()
544
+ with self._lock:
545
+ rows = conn.execute(
546
+ "SELECT id, file_path, chunk_type, name, text, vector FROM vectors",
547
+ ).fetchall()
548
+
549
+ if not rows:
550
+ self._vec_matrix = np.empty((0, self.dimension), dtype=np.float32)
551
+ self._vec_meta = []
552
+ self._vec_loaded_version = self._vec_cache_version
553
+ return
554
+
555
+ meta: list[tuple] = []
556
+ vecs: list[np.ndarray] = []
557
+ for row in rows:
558
+ try:
559
+ v = np.frombuffer(row[5], dtype=np.float32)
560
+ if len(v) == self.dimension:
561
+ vecs.append(v)
562
+ meta.append((row[0], row[1], row[2], row[3], row[4]))
563
+ except (ValueError, struct.error):
564
+ logger.warning("Skipping corrupt vector row id=%s", row[0])
565
+
566
+ if vecs:
567
+ self._vec_matrix = np.vstack(vecs)
568
+ else:
569
+ self._vec_matrix = np.empty((0, self.dimension), dtype=np.float32)
570
+ self._vec_meta = meta
571
+ self._vec_loaded_version = self._vec_cache_version
572
+ logger.debug(
573
+ "Vector cache loaded: %d vectors (%d dim)",
574
+ len(meta), self.dimension,
575
+ )
576
+
577
+ # ------------------------------------------------------------------
578
+ # Search
579
+ # ------------------------------------------------------------------
580
+
581
+ def search(
582
+ self,
583
+ query_vector: list[float],
584
+ top_k: int = 10,
585
+ file_filter: str = "",
586
+ existing_files: set[str] | None = None,
587
+ ) -> list[SearchResult]:
588
+ """Search for similar vectors.
589
+
590
+ Uses numpy batch cosine similarity when available (100-1000x faster
591
+ than pure Python), with automatic in-memory caching of the vector
592
+ matrix. Falls back to per-vector Python loop if numpy is unavailable.
593
+
594
+ Args:
595
+ query_vector: Query embedding vector.
596
+ top_k: Number of results to return.
597
+ file_filter: Optional glob pattern to filter files (e.g. "*.py").
598
+ existing_files: Optional set of file paths that exist on disk.
599
+ When provided, vectors whose file_path is not in this set
600
+ are skipped (filters out stale branch data in local mode).
601
+
602
+ Returns:
603
+ Top-k results sorted by similarity score (highest first).
604
+ """
605
+ if not query_vector:
606
+ return []
607
+
608
+ # Pick up any cache invalidation signal written by another
609
+ # process (e.g. EmbeddingRotator.cutover in a separate MCP
610
+ # worker) before consulting the in-memory cache.
611
+ self._check_external_cache_bump()
612
+
613
+ if _HAS_NUMPY:
614
+ return self._search_numpy(query_vector, top_k, file_filter, existing_files)
615
+ return self._search_python(query_vector, top_k, file_filter, existing_files)
616
+
617
+ def _search_numpy(
618
+ self,
619
+ query_vector: list[float],
620
+ top_k: int,
621
+ file_filter: str,
622
+ existing_files: set[str] | None,
623
+ ) -> list[SearchResult]:
624
+ """Numpy-accelerated vector search with in-memory cache."""
625
+ # Ensure cache is current
626
+ if self._vec_loaded_version != self._vec_cache_version or self._vec_matrix is None:
627
+ self._load_vector_cache()
628
+
629
+ if self._vec_matrix is None or len(self._vec_matrix) == 0:
630
+ return []
631
+
632
+ query_np = np.array(query_vector, dtype=np.float32)
633
+
634
+ # Batch cosine similarity — single BLAS matmul
635
+ scores = _batch_cosine_similarity(query_np, self._vec_matrix)
636
+
637
+ # Apply filters by zeroing out excluded entries
638
+ if file_filter or existing_files is not None:
639
+ import fnmatch
640
+
641
+ for i, meta in enumerate(self._vec_meta):
642
+ fp = meta[1] # file_path
643
+ if existing_files is not None and fp not in existing_files: # noqa: SIM114 — clearer as two branches than one combined or
644
+ scores[i] = -1.0
645
+ elif file_filter and not fnmatch.fnmatch(fp, file_filter):
646
+ scores[i] = -1.0
647
+
648
+ # Top-k selection: argpartition is O(N) vs O(N log N) for full sort
649
+ n = len(scores)
650
+ if n <= top_k:
651
+ top_indices = np.argsort(scores)[::-1]
652
+ else:
653
+ top_indices = np.argpartition(scores, -top_k)[-top_k:]
654
+ top_indices = top_indices[np.argsort(scores[top_indices])[::-1]]
655
+
656
+ results: list[SearchResult] = []
657
+ for idx in top_indices:
658
+ s = float(scores[idx])
659
+ if s <= 0:
660
+ break
661
+ m = self._vec_meta[idx]
662
+ results.append(SearchResult(
663
+ id=m[0], file_path=m[1], chunk_type=m[2],
664
+ name=m[3], text=m[4], score=round(s, 4),
665
+ ))
666
+ return results
667
+
668
+ def _search_python(
669
+ self,
670
+ query_vector: list[float],
671
+ top_k: int,
672
+ file_filter: str,
673
+ existing_files: set[str] | None,
674
+ ) -> list[SearchResult]:
675
+ """Pure Python fallback when numpy is unavailable."""
676
+ conn = self._get_conn()
677
+
678
+ if file_filter:
679
+ import fnmatch
680
+
681
+ def _file_filter_fn(fp: str) -> bool:
682
+ return fnmatch.fnmatch(fp, file_filter)
683
+ else:
684
+ _file_filter_fn = None
685
+
686
+ with self._lock:
687
+ rows = conn.execute(
688
+ "SELECT id, file_path, chunk_type, name, text, vector FROM vectors",
689
+ ).fetchall()
690
+
691
+ results: list[SearchResult] = []
692
+ for row in rows:
693
+ if existing_files is not None and row[1] not in existing_files:
694
+ continue
695
+ if _file_filter_fn and not _file_filter_fn(row[1]):
696
+ continue
697
+ try:
698
+ vec = _unpack_vector(row[5], self.dimension)
699
+ except struct.error:
700
+ logger.warning("Skipping corrupt vector row id=%s", row[0])
701
+ continue
702
+ score = _cosine_similarity(query_vector, vec)
703
+ results.append(SearchResult(
704
+ id=row[0],
705
+ file_path=row[1],
706
+ chunk_type=row[2],
707
+ name=row[3],
708
+ text=row[4],
709
+ score=round(score, 4),
710
+ ))
711
+
712
+ results.sort(key=lambda r: r.score, reverse=True)
713
+ return results[:top_k]
714
+
715
+ def count(self) -> int:
716
+ """Return total number of stored vectors."""
717
+ conn = self._get_conn()
718
+ with self._lock:
719
+ row = conn.execute("SELECT COUNT(*) FROM vectors").fetchone()
720
+ return row[0] if row else 0
721
+
722
+ def set_file_metadata(
723
+ self, file_path: str, mtime: float, chunk_count: int,
724
+ ) -> None:
725
+ """Record when a file was last indexed and its mtime."""
726
+ conn = self._get_conn()
727
+ import time as _time
728
+ with self._lock:
729
+ conn.execute(
730
+ """INSERT OR REPLACE INTO file_metadata
731
+ (file_path, last_indexed_at, file_mtime, chunk_count)
732
+ VALUES (?, ?, ?, ?)""",
733
+ (file_path, _time.time(), mtime, chunk_count),
734
+ )
735
+ conn.commit()
736
+
737
+ def get_file_metadata(self, file_path: str) -> dict[str, Any] | None:
738
+ """Get index metadata for a file. Returns None if not indexed."""
739
+ conn = self._get_conn()
740
+ with self._lock:
741
+ row = conn.execute(
742
+ "SELECT last_indexed_at, file_mtime, chunk_count "
743
+ "FROM file_metadata WHERE file_path = ?",
744
+ (file_path,),
745
+ ).fetchone()
746
+ if row:
747
+ return {"last_indexed_at": row[0], "file_mtime": row[1], "chunk_count": row[2]}
748
+ return None
749
+
750
+ def get_stale_files(self, file_mtimes: dict[str, float]) -> list[str]:
751
+ """Return files whose mtime is newer than last indexed mtime.
752
+
753
+ Args:
754
+ file_mtimes: Dict of relative_path -> current mtime.
755
+
756
+ Returns:
757
+ List of file paths that need re-indexing.
758
+ """
759
+ conn = self._get_conn()
760
+ with self._lock:
761
+ rows = conn.execute(
762
+ "SELECT file_path, file_mtime FROM file_metadata"
763
+ ).fetchall()
764
+ indexed = {row[0]: row[1] for row in rows}
765
+ stale: list[str] = []
766
+ for fpath, current_mtime in file_mtimes.items():
767
+ stored_mtime = indexed.get(fpath)
768
+ if stored_mtime is None or current_mtime > stored_mtime:
769
+ stale.append(fpath)
770
+ return stale
771
+
772
+ def get_all_indexed_files(self) -> list[str]:
773
+ """Return all file paths that have metadata in the store."""
774
+ conn = self._get_conn()
775
+ with self._lock:
776
+ rows = conn.execute("SELECT file_path FROM file_metadata").fetchall()
777
+ return [r[0] for r in rows]
778
+
779
+ def delete_file_metadata(self, file_path: str) -> None:
780
+ """Remove metadata for a file."""
781
+ conn = self._get_conn()
782
+ with self._lock:
783
+ conn.execute(
784
+ "DELETE FROM file_metadata WHERE file_path = ?", (file_path,),
785
+ )
786
+ conn.commit()
787
+
788
+ def close(self) -> None:
789
+ """Close the database connection."""
790
+ if self._conn:
791
+ self._conn.close()
792
+ self._conn = None
793
+
794
+ def __del__(self) -> None:
795
+ self.close()