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,282 @@
1
+ """Embedding provider abstraction for semantic search.
2
+
3
+ Tries providers in order:
4
+ 1. Local: sentence-transformers with code-optimized model (no API cost)
5
+ 2. Local: sentence-transformers with general model (smaller fallback)
6
+ 3. API: OpenAI text-embedding-3-small (if OPENAI_API_KEY set)
7
+ 4. None: graceful degradation (returns empty vectors)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ from abc import ABC, abstractmethod
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class EmbeddingProvider(ABC):
20
+ """Abstract embedding provider."""
21
+
22
+ @abstractmethod
23
+ def embed(self, texts: list[str]) -> list[list[float]]:
24
+ """Embed a batch of texts. Returns list of vectors."""
25
+ ...
26
+
27
+ def embed_query(self, texts: list[str]) -> list[list[float]]:
28
+ """Embed query texts. Symmetric providers reuse embed(); asymmetric
29
+ providers (e.g. nomic) override with a query-specific prefix."""
30
+ return self.embed(texts)
31
+
32
+ @abstractmethod
33
+ def dimension(self) -> int:
34
+ """Return the embedding dimension."""
35
+ ...
36
+
37
+ @property
38
+ @abstractmethod
39
+ def name(self) -> str:
40
+ """Provider name for logging."""
41
+ ...
42
+
43
+
44
+ class LocalEmbeddingProvider(EmbeddingProvider):
45
+ """Local embeddings via sentence-transformers (all-MiniLM-L6-v2)."""
46
+
47
+ def __init__(self) -> None:
48
+ from sentence_transformers import SentenceTransformer # type: ignore[import-untyped]
49
+ self._model = SentenceTransformer("all-MiniLM-L6-v2")
50
+ self._dim = 384
51
+
52
+ def embed(self, texts: list[str]) -> list[list[float]]:
53
+ embeddings = self._model.encode(texts, convert_to_numpy=True)
54
+ return [e.tolist() for e in embeddings]
55
+
56
+ def dimension(self) -> int:
57
+ return self._dim
58
+
59
+ @property
60
+ def name(self) -> str:
61
+ return "local:all-MiniLM-L6-v2"
62
+
63
+
64
+ class OpenAIEmbeddingProvider(EmbeddingProvider):
65
+ """OpenAI embeddings via text-embedding-3-small."""
66
+
67
+ def __init__(self, api_key: str | None = None) -> None:
68
+ import openai # type: ignore[import-untyped]
69
+ self._client = openai.OpenAI(api_key=api_key or os.environ.get("OPENAI_API_KEY"))
70
+ self._dim = 1536
71
+
72
+ def embed(self, texts: list[str]) -> list[list[float]]:
73
+ response = self._client.embeddings.create(
74
+ model="text-embedding-3-small",
75
+ input=texts,
76
+ )
77
+ return [item.embedding for item in response.data]
78
+
79
+ def dimension(self) -> int:
80
+ return self._dim
81
+
82
+ @property
83
+ def name(self) -> str:
84
+ return "openai:text-embedding-3-small"
85
+
86
+
87
+ class NomicEmbeddingProvider(EmbeddingProvider):
88
+ """Local embeddings via nomic-embed-text (137M params, better code understanding)."""
89
+
90
+ def __init__(self) -> None:
91
+ from sentence_transformers import SentenceTransformer # type: ignore[import-untyped]
92
+ logger.warning(
93
+ "Loading nomic-embed-text with trust_remote_code=True — "
94
+ "this executes code from the model repository"
95
+ )
96
+ self._model = SentenceTransformer(
97
+ "nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True,
98
+ )
99
+ self._dim = 768
100
+
101
+ def embed(self, texts: list[str]) -> list[list[float]]:
102
+ # nomic-embed-text recommends prefixing with task type
103
+ prefixed = [f"search_document: {t}" for t in texts]
104
+ embeddings = self._model.encode(prefixed, convert_to_numpy=True)
105
+ return [e.tolist() for e in embeddings]
106
+
107
+ def embed_query(self, texts: list[str]) -> list[list[float]]:
108
+ # Queries use a different prefix than documents (asymmetric model).
109
+ prefixed = [f"search_query: {t}" for t in texts]
110
+ embeddings = self._model.encode(prefixed, convert_to_numpy=True)
111
+ return [e.tolist() for e in embeddings]
112
+
113
+ def dimension(self) -> int:
114
+ return self._dim
115
+
116
+ @property
117
+ def name(self) -> str:
118
+ return "local:nomic-embed-text-v1.5"
119
+
120
+
121
+ class CodeEmbeddingProvider(EmbeddingProvider):
122
+ """Local embeddings via BAAI/bge-base-en-v1.5 (768-dim, ~440MB model).
123
+
124
+ Significantly outperforms all-MiniLM-L6-v2 on code search tasks due to
125
+ larger model capacity and better handling of code tokens (identifiers,
126
+ camelCase, snake_case, function signatures).
127
+
128
+ Note: Switching models requires reindexing (different vector dimensions).
129
+ """
130
+
131
+ def __init__(self) -> None:
132
+ from sentence_transformers import SentenceTransformer # type: ignore[import-untyped]
133
+ self._model = SentenceTransformer("BAAI/bge-base-en-v1.5")
134
+ self._dim = 768
135
+
136
+ def embed(self, texts: list[str]) -> list[list[float]]:
137
+ embeddings = self._model.encode(texts, convert_to_numpy=True)
138
+ return [e.tolist() for e in embeddings]
139
+
140
+ def dimension(self) -> int:
141
+ return self._dim
142
+
143
+ @property
144
+ def name(self) -> str:
145
+ return "local:bge-base-en-v1.5"
146
+
147
+
148
+ class NullEmbeddingProvider(EmbeddingProvider):
149
+ """Fallback provider that returns empty vectors."""
150
+
151
+ def embed(self, texts: list[str]) -> list[list[float]]:
152
+ return [[] for _ in texts]
153
+
154
+ def dimension(self) -> int:
155
+ return 0
156
+
157
+ @property
158
+ def name(self) -> str:
159
+ return "none"
160
+
161
+
162
+ _provider_cache: dict[str, EmbeddingProvider] = {}
163
+
164
+
165
+ def create_embedding_provider(
166
+ model: str = "",
167
+ ) -> EmbeddingProvider:
168
+ """Create the best available embedding provider.
169
+
170
+ Args:
171
+ model: Preferred model name. Options:
172
+ - "bge" (768-dim, recommended for code search)
173
+ - "all-MiniLM-L6-v2" (384-dim, fast, smaller)
174
+ - "nomic-embed-text" (768-dim, better code understanding)
175
+ - "openai" (API-based, requires OPENAI_API_KEY)
176
+ - "" (auto-detect best available)
177
+
178
+ Auto-detect order: bge (code-optimized) > MiniLM > OpenAI API > null.
179
+ Note: switching models requires reindexing (different vector dims).
180
+ """
181
+ model = model or os.environ.get("ATTOCODE_EMBEDDING_MODEL", "")
182
+
183
+ if model in _provider_cache:
184
+ return _provider_cache[model]
185
+
186
+ # Explicit BGE request (code-optimized)
187
+ if model == "bge":
188
+ try:
189
+ provider = CodeEmbeddingProvider()
190
+ logger.info("Using embedding provider: %s", provider.name)
191
+ _provider_cache[model] = provider
192
+ return provider
193
+ except ImportError:
194
+ raise ImportError(
195
+ f"Embedding model '{model}' requires sentence-transformers. "
196
+ "Install with: pip install attocode[semantic]"
197
+ )
198
+
199
+ # Explicit MiniLM request
200
+ if model == "all-MiniLM-L6-v2":
201
+ try:
202
+ provider = LocalEmbeddingProvider()
203
+ logger.info("Using embedding provider: %s", provider.name)
204
+ _provider_cache[model] = provider
205
+ return provider
206
+ except ImportError:
207
+ raise ImportError(
208
+ f"Embedding model '{model}' requires sentence-transformers. "
209
+ "Install with: pip install attocode[semantic]"
210
+ )
211
+
212
+ # Explicit nomic request
213
+ if model == "nomic-embed-text":
214
+ try:
215
+ provider = NomicEmbeddingProvider()
216
+ logger.info("Using embedding provider: %s", provider.name)
217
+ _provider_cache[model] = provider
218
+ return provider
219
+ except ImportError:
220
+ raise ImportError(
221
+ f"Embedding model '{model}' requires sentence-transformers and einops. "
222
+ "Install with: pip install attocode[semantic-nomic]"
223
+ )
224
+
225
+ # Explicit OpenAI request
226
+ if model == "openai":
227
+ try:
228
+ provider = OpenAIEmbeddingProvider()
229
+ logger.info("Using embedding provider: %s", provider.name)
230
+ _provider_cache[model] = provider
231
+ return provider
232
+ except ImportError:
233
+ raise ImportError(
234
+ "OpenAI embeddings require the openai package. "
235
+ "Install with: pip install attocode[openai]"
236
+ )
237
+ except Exception as e:
238
+ raise RuntimeError(f"OpenAI embedding provider failed: {e}") from e
239
+
240
+ # Auto-detect: code-trained nomic first, then BGE, then MiniLM (no API cost)
241
+ try:
242
+ provider = NomicEmbeddingProvider()
243
+ logger.info("Using code-trained embedding provider: %s", provider.name)
244
+ _provider_cache[model] = provider
245
+ return provider
246
+ except Exception as exc:
247
+ logger.info("nomic unavailable (%s), trying BGE", exc)
248
+ try:
249
+ provider = CodeEmbeddingProvider()
250
+ logger.info("Using code-optimized embedding provider: %s", provider.name)
251
+ _provider_cache[model] = provider
252
+ return provider
253
+ except ImportError:
254
+ logger.debug("sentence-transformers not installed, trying OpenAI")
255
+ except Exception as exc:
256
+ logger.info("BGE model unavailable (%s), falling back to MiniLM", exc)
257
+ try:
258
+ provider = LocalEmbeddingProvider()
259
+ logger.info("Using local embedding provider: %s", provider.name)
260
+ _provider_cache[model] = provider
261
+ return provider
262
+ except ImportError:
263
+ logger.debug("sentence-transformers not installed, trying OpenAI")
264
+ except Exception as exc2:
265
+ logger.warning("Local embedding provider unavailable: %s", exc2)
266
+ logger.debug("Local embedding provider traceback", exc_info=True)
267
+
268
+ # Try OpenAI API
269
+ if os.environ.get("OPENAI_API_KEY"):
270
+ try:
271
+ provider = OpenAIEmbeddingProvider()
272
+ logger.info("Using OpenAI embedding provider: %s", provider.name)
273
+ _provider_cache[model] = provider
274
+ return provider
275
+ except ImportError:
276
+ logger.debug("openai package not installed")
277
+ except Exception:
278
+ logger.debug("OpenAI embedding provider failed", exc_info=True)
279
+
280
+ # Graceful degradation — don't cache so retry picks up newly installed packages
281
+ logger.info("No embedding provider available; semantic search will use keyword fallback")
282
+ return NullEmbeddingProvider()
@@ -0,0 +1,427 @@
1
+ """Frecency tracker for intelligent file ranking based on access patterns.
2
+
3
+ Frecency combines "frequency" and "recency" to rank files that are both
4
+ often accessed AND recently accessed. This is similar to browser bookmark
5
+ scoring or the fff.nvim frecency algorithm.
6
+
7
+ Algorithm:
8
+ - 10-day half-life (or 3-day for AI mode)
9
+ - 30-day lookback window
10
+ - Score = Σ e^(-ln(2)/10 * days_ago) with diminishing returns
11
+ - Modification bonus: +16 for <2min, +8 for <15min, etc.
12
+
13
+ Usage:
14
+ tracker = FrecencyTracker(".attocode/frecency")
15
+ tracker.track_access("src/main.py")
16
+ score = tracker.get_score("src/main.py")
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import math
24
+ import sqlite3
25
+ import threading
26
+ import time
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Decay constants (ln(2)/10 for 10-day half-life)
33
+ _DECAY_CONSTANT: float = 0.069314718 # ln(2)/10
34
+ _SECONDS_PER_DAY: float = 86400.0
35
+ _MAX_HISTORY_DAYS: float = 30.0
36
+
37
+ # AI mode: faster decay since AI sessions are shorter
38
+ _AI_DECAY_CONSTANT: float = 0.231 # ln(2)/3 for 3-day half-life
39
+ _AI_MAX_HISTORY_DAYS: float = 7.0
40
+
41
+ # Modification score thresholds (seconds, points)
42
+ _MODIFICATION_THRESHOLDS: list[tuple[int, int]] = [
43
+ (120, 16), # < 2 minutes: 16 points
44
+ (900, 8), # < 15 minutes: 8 points
45
+ (3600, 4), # < 1 hour: 4 points
46
+ (86400, 2), # < 1 day: 2 points
47
+ (604800, 1), # < 1 week: 1 point
48
+ ]
49
+
50
+ # AI mode: compressed thresholds since AI edits happen in rapid bursts
51
+ _AI_MODIFICATION_THRESHOLDS: list[tuple[int, int]] = [
52
+ (30, 16), # < 30 seconds: 16 points
53
+ (300, 8), # < 5 minutes: 8 points
54
+ (900, 4), # < 15 minutes: 4 points
55
+ (3600, 2), # < 1 hour: 2 points
56
+ (14400, 1), # < 4 hours: 1 point
57
+ ]
58
+
59
+ # SQLite schema version
60
+ _SCHEMA_VERSION: int = 1
61
+
62
+
63
+ @dataclass
64
+ class FrecencyResult:
65
+ """Result of a frecency query."""
66
+ score: int
67
+ accesses: int
68
+ last_access: float | None
69
+ is_ai_mode: bool
70
+
71
+
72
+ class FrecencyTracker:
73
+ """Tracks file access patterns for intelligent ranking.
74
+
75
+ Uses SQLite to store access timestamps per file path (keyed by path hash).
76
+ Implements exponential decay with a 10-day half-life (or 3-day for AI mode).
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ db_path: str | Path = ".attocode/frecency",
82
+ *,
83
+ ai_mode: bool = False,
84
+ ) -> None:
85
+ self._db_path = Path(db_path)
86
+ self._ai_mode = ai_mode
87
+ self._lock = threading.Lock()
88
+ self._conn: sqlite3.Connection | None = None
89
+ self._ensure_db()
90
+
91
+ def _ensure_db(self) -> None:
92
+ """Ensure the database directory and tables exist."""
93
+ self._db_path.mkdir(parents=True, exist_ok=True)
94
+ db_file = self._db_path / "frecency.db"
95
+
96
+ conn = sqlite3.connect(
97
+ str(db_file),
98
+ timeout=5.0,
99
+ check_same_thread=False,
100
+ )
101
+ conn.execute("PRAGMA journal_mode=WAL")
102
+ conn.execute("PRAGMA foreign_keys=ON")
103
+
104
+ # Create tables
105
+ conn.execute("""
106
+ CREATE TABLE IF NOT EXISTS frecency_accesses (
107
+ path_hash TEXT PRIMARY KEY,
108
+ timestamps TEXT NOT NULL DEFAULT '[]',
109
+ updated_at REAL NOT NULL DEFAULT 0
110
+ )
111
+ """)
112
+ conn.execute("""
113
+ CREATE TABLE IF NOT EXISTS meta (
114
+ key TEXT PRIMARY KEY,
115
+ value TEXT NOT NULL
116
+ )
117
+ """)
118
+
119
+ # Initialize schema version if not set
120
+ cursor = conn.execute(
121
+ "SELECT value FROM meta WHERE key = 'version'",
122
+ )
123
+ row = cursor.fetchone()
124
+ if row is None:
125
+ conn.execute(
126
+ "INSERT INTO meta (key, value) VALUES ('version', ?)",
127
+ (str(_SCHEMA_VERSION),),
128
+ )
129
+ conn.commit()
130
+
131
+ self._conn = conn
132
+
133
+ def close(self) -> None:
134
+ """Close the database connection."""
135
+ with self._lock:
136
+ if self._conn is not None:
137
+ self._conn.close()
138
+ self._conn = None
139
+
140
+ def __del__(self) -> None:
141
+ try:
142
+ self.close()
143
+ except Exception:
144
+ pass
145
+
146
+ def track_access(self, file_path: str | Path) -> None:
147
+ """Record that a file was accessed.
148
+
149
+ Args:
150
+ file_path: Path to the file that was accessed.
151
+ """
152
+ path = str(file_path)
153
+ now = time.time()
154
+
155
+ with self._lock:
156
+ if self._conn is None:
157
+ return
158
+
159
+ cursor = self._conn.execute(
160
+ "SELECT timestamps FROM frecency_accesses WHERE path_hash = ?",
161
+ (path,),
162
+ )
163
+ row = cursor.fetchone()
164
+
165
+ if row is None:
166
+ # New entry
167
+ timestamps = [now]
168
+ self._conn.execute(
169
+ "INSERT INTO frecency_accesses (path_hash, timestamps, updated_at) VALUES (?, ?, ?)",
170
+ (path, json.dumps(timestamps), now),
171
+ )
172
+ else:
173
+ # Update existing
174
+ timestamps: list[float] = json.loads(row[0])
175
+ timestamps.append(now)
176
+
177
+ # Prune old timestamps (beyond retention window)
178
+ max_history = _AI_MAX_HISTORY_DAYS if self._ai_mode else _MAX_HISTORY_DAYS
179
+ cutoff = now - (max_history * _SECONDS_PER_DAY)
180
+ timestamps = [ts for ts in timestamps if ts >= cutoff]
181
+
182
+ self._conn.execute(
183
+ "UPDATE frecency_accesses SET timestamps = ?, updated_at = ? WHERE path_hash = ?",
184
+ (json.dumps(timestamps), now, path),
185
+ )
186
+
187
+ self._conn.commit()
188
+
189
+ def get_score(
190
+ self,
191
+ file_path: str | Path,
192
+ *,
193
+ modified_time: float | None = None,
194
+ is_modified_git: bool = False,
195
+ ai_mode: bool | None = None,
196
+ ) -> FrecencyResult:
197
+ """Calculate the frecency score for a file.
198
+
199
+ Args:
200
+ file_path: Path to the file.
201
+ modified_time: Unix timestamp of last modification (for git score).
202
+ is_modified_git: Whether the file has uncommitted changes.
203
+ ai_mode: Override AI mode (default: use instance setting).
204
+
205
+ Returns:
206
+ FrecencyResult with score and metadata.
207
+ """
208
+ path = str(file_path)
209
+ now = time.time()
210
+
211
+ # Determine mode
212
+ use_ai = self._ai_mode if ai_mode is None else ai_mode
213
+
214
+ decay_constant = _AI_DECAY_CONSTANT if use_ai else _DECAY_CONSTANT
215
+ max_history_days = _AI_MAX_HISTORY_DAYS if use_ai else _MAX_HISTORY_DAYS
216
+ cutoff_time = now - (max_history_days * _SECONDS_PER_DAY)
217
+
218
+ with self._lock:
219
+ if self._conn is None:
220
+ return FrecencyResult(score=0, accesses=0, last_access=None, is_ai_mode=use_ai)
221
+
222
+ cursor = self._conn.execute(
223
+ "SELECT timestamps FROM frecency_accesses WHERE path_hash = ?",
224
+ (path,),
225
+ )
226
+ row = cursor.fetchone()
227
+
228
+ if row is None:
229
+ timestamps: list[float] = []
230
+ else:
231
+ timestamps = json.loads(row[0])
232
+
233
+ # Filter to within retention window
234
+ timestamps = [ts for ts in timestamps if ts >= cutoff_time]
235
+
236
+ if not timestamps:
237
+ accesses = 0
238
+ total_frecency = 0.0
239
+ else:
240
+ accesses = len(timestamps)
241
+ total_frecency = 0.0
242
+
243
+ for access_time in timestamps:
244
+ days_ago = (now - access_time) / _SECONDS_PER_DAY
245
+ decay_factor = math.exp(-decay_constant * days_ago)
246
+ total_frecency += decay_factor
247
+
248
+ # Apply diminishing returns normalization
249
+ # if total > 10: return 10 + sqrt(total - 10)
250
+ if total_frecency <= 10.0:
251
+ normalized = total_frecency
252
+ else:
253
+ normalized = 10.0 + math.sqrt(total_frecency - 10.0)
254
+
255
+ frecency_score = round(normalized)
256
+
257
+ # Add modification bonus
258
+ if modified_time is not None and is_modified_git:
259
+ thresholds = _AI_MODIFICATION_THRESHOLDS if use_ai else _MODIFICATION_THRESHOLDS
260
+ duration_since = now - modified_time
261
+
262
+ for threshold_seconds, points in thresholds:
263
+ if duration_since <= threshold_seconds:
264
+ frecency_score += points
265
+ break
266
+
267
+ last_access = timestamps[-1] if timestamps else None
268
+
269
+ return FrecencyResult(
270
+ score=frecency_score,
271
+ accesses=accesses,
272
+ last_access=last_access,
273
+ is_ai_mode=use_ai,
274
+ )
275
+
276
+ def get_scores_batch(
277
+ self,
278
+ file_paths: list[str | Path],
279
+ *,
280
+ modified_times: dict[str, float] | None = None,
281
+ git_status: dict[str, bool] | None = None,
282
+ ai_mode: bool | None = None,
283
+ ) -> dict[str, FrecencyResult]:
284
+ """Get frecency scores for multiple files efficiently.
285
+
286
+ Args:
287
+ file_paths: List of file paths to score.
288
+ modified_times: Dict mapping path -> modification timestamp.
289
+ git_status: Dict mapping path -> is_modified bool.
290
+ ai_mode: Override AI mode.
291
+
292
+ Returns:
293
+ Dict mapping path -> FrecencyResult.
294
+ """
295
+ modified_times = modified_times or {}
296
+ git_status = git_status or {}
297
+
298
+ results = {}
299
+ for path in file_paths:
300
+ path_str = str(path)
301
+ results[path_str] = self.get_score(
302
+ path_str,
303
+ modified_time=modified_times.get(path_str),
304
+ is_modified_git=git_status.get(path_str, False),
305
+ ai_mode=ai_mode,
306
+ )
307
+ return results
308
+
309
+ def clear(self, file_path: str | Path | None = None) -> int:
310
+ """Clear frecency data.
311
+
312
+ Args:
313
+ file_path: If provided, clear only this file. Otherwise clear all.
314
+
315
+ Returns:
316
+ Number of entries cleared.
317
+ """
318
+ with self._lock:
319
+ if self._conn is None:
320
+ return 0
321
+
322
+ if file_path is None:
323
+ self._conn.execute("DELETE FROM frecency_accesses")
324
+ self._conn.commit()
325
+ return -1 # Indicates all cleared
326
+
327
+ path = str(file_path)
328
+ cursor = self._conn.execute(
329
+ "DELETE FROM frecency_accesses WHERE path_hash = ?",
330
+ (path,),
331
+ )
332
+ self._conn.commit()
333
+ return cursor.rowcount
334
+
335
+ def get_stats(self) -> dict:
336
+ """Get frecency statistics."""
337
+ with self._lock:
338
+ if self._conn is None:
339
+ return {"entries": 0, "ai_mode": self._ai_mode}
340
+
341
+ cursor = self._conn.execute(
342
+ "SELECT COUNT(*) FROM frecency_accesses",
343
+ )
344
+ row = cursor.fetchone()
345
+ count = row[0] if row else 0
346
+
347
+ return {
348
+ "entries": count,
349
+ "ai_mode": self._ai_mode,
350
+ "db_path": str(self._db_path),
351
+ }
352
+
353
+ def get_leaderboard(
354
+ self,
355
+ top_n: int = 20,
356
+ *,
357
+ ai_mode: bool | None = None,
358
+ ) -> list[tuple[str, FrecencyResult]]:
359
+ """Get the top N files by frecency score.
360
+
361
+ Args:
362
+ top_n: Number of top files to return.
363
+ ai_mode: Override AI mode (default: use instance setting).
364
+
365
+ Returns:
366
+ List of (path, FrecencyResult) tuples sorted by score descending.
367
+ """
368
+ with self._lock:
369
+ if self._conn is None:
370
+ return []
371
+ cursor = self._conn.execute(
372
+ "SELECT path_hash FROM frecency_accesses",
373
+ )
374
+ all_paths = [row[0] for row in cursor.fetchall()]
375
+
376
+ # Score each path (get_score handles its own locking)
377
+ scored: list[tuple[str, FrecencyResult]] = []
378
+ for path in all_paths:
379
+ result = self.get_score(path, ai_mode=ai_mode)
380
+ if result.score > 0:
381
+ scored.append((path, result))
382
+
383
+ scored.sort(key=lambda x: x[1].score, reverse=True)
384
+ return scored[:top_n]
385
+
386
+ def vacuum(self) -> None:
387
+ """Compact the database."""
388
+ with self._lock:
389
+ if self._conn is not None:
390
+ self._conn.execute("VACUUM")
391
+
392
+
393
+ # Global singleton instance
394
+ _tracker: FrecencyTracker | None = None
395
+ _tracker_lock = threading.Lock()
396
+
397
+
398
+ def get_tracker(
399
+ db_path: str | Path = ".attocode/frecency",
400
+ *,
401
+ ai_mode: bool = False,
402
+ ) -> FrecencyTracker:
403
+ """Get or create the global FrecencyTracker instance.
404
+
405
+ Args:
406
+ db_path: Path to the frecency database.
407
+ ai_mode: Whether to use AI mode (faster decay).
408
+
409
+ Returns:
410
+ FrecencyTracker singleton.
411
+ """
412
+ global _tracker
413
+
414
+ with _tracker_lock:
415
+ if _tracker is None:
416
+ _tracker = FrecencyTracker(db_path=db_path, ai_mode=ai_mode)
417
+ return _tracker
418
+
419
+
420
+ def reset_tracker() -> None:
421
+ """Reset the global tracker instance."""
422
+ global _tracker
423
+
424
+ with _tracker_lock:
425
+ if _tracker is not None:
426
+ _tracker.close()
427
+ _tracker = None