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,770 @@
1
+ """Search and security tools for the code-intel MCP server.
2
+
3
+ Tools: semantic_search, semantic_search_status, security_scan, fast_search,
4
+ regex_search.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import threading
11
+ from typing import TYPE_CHECKING
12
+
13
+ from attocode_intel._shared import (
14
+ _get_ast_service,
15
+ _get_context_mgr,
16
+ _get_frecency_tracker,
17
+ _get_remote_service,
18
+ _get_service,
19
+ mcp,
20
+ )
21
+ from attocode_intel.tools.pin_tools import pin_stamped
22
+ from attocode_intel._internal.integrations.context.frecency import FrecencyResult
23
+
24
+ if TYPE_CHECKING:
25
+ from attocode_intel._internal.integrations.context.semantic_search import SemanticSearchManager
26
+ from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
27
+ from attocode_intel._internal.integrations.security.scanner import SecurityScanner
28
+
29
+
30
+ def _get_project_dir() -> str:
31
+ """Dynamic dispatch to :func:`attocode_intel._shared._get_project_dir`.
32
+
33
+ Previously this module did ``from ..._shared import _get_project_dir``
34
+ which cached the original function reference at import time. That
35
+ made it invisible to test fixtures that monkeypatch
36
+ ``_shared._get_project_dir`` — the fixture's new function would be
37
+ installed in ``_shared`` but ``search_tools._get_project_dir`` would
38
+ keep returning the original.
39
+
40
+ Looking the function up fresh on every call keeps cross-module
41
+ monkeypatching consistent AND avoids a whole class of test
42
+ pollution where a fixture-scoped patch of ``_shared`` leaks into
43
+ search_tools via some other path.
44
+ """
45
+ from attocode_intel import _shared
46
+ return _shared._get_project_dir()
47
+
48
+
49
+ def _empty_frecency(ai_mode: bool) -> FrecencyResult:
50
+ """Create a zero-score FrecencyResult."""
51
+ return FrecencyResult(score=0, accesses=0, last_access=None, is_ai_mode=ai_mode)
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Lazy singletons
55
+ # ---------------------------------------------------------------------------
56
+
57
+ _semantic_search: SemanticSearchManager | None = None
58
+ _semantic_search_lock = threading.Lock()
59
+
60
+ _trigram_index: TrigramIndex | None = None
61
+ _trigram_index_lock = threading.Lock()
62
+
63
+
64
+ def _get_semantic_search() -> SemanticSearchManager:
65
+ """Lazily initialize the semantic search manager (thread-safe)."""
66
+ from attocode_intel.request_context import current_request
67
+ if (request := current_request.get()) is not None:
68
+ return request.service._get_semantic_search()
69
+ global _semantic_search
70
+ if _semantic_search is None:
71
+ with _semantic_search_lock:
72
+ if _semantic_search is None:
73
+ from attocode_intel._internal.integrations.context.semantic_search import SemanticSearchManager
74
+ project_dir = _get_project_dir()
75
+ _semantic_search = SemanticSearchManager(root_dir=project_dir)
76
+ return _semantic_search
77
+
78
+
79
+ _security_scanner: SecurityScanner | None = None
80
+
81
+
82
+ def _get_security_scanner() -> SecurityScanner:
83
+ """Lazily initialize the security scanner."""
84
+ from attocode_intel.request_context import current_request
85
+ if (request := current_request.get()) is not None:
86
+ return request.service._get_security_scanner()
87
+ global _security_scanner
88
+ if _security_scanner is None:
89
+ from attocode_intel._internal.integrations.security.scanner import SecurityScanner
90
+ project_dir = _get_project_dir()
91
+ _security_scanner = SecurityScanner(root_dir=project_dir)
92
+ return _security_scanner
93
+
94
+
95
+ def _get_trigram_index() -> TrigramIndex | None:
96
+ """Lazily initialize the trigram index (thread-safe).
97
+
98
+ Loads an existing index from disk if available. Returns None
99
+ if no index has been built yet.
100
+ """
101
+ from attocode_intel.request_context import current_request, scoped_store
102
+ if current_request.get() is not None:
103
+ def build(root):
104
+ import os
105
+ from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
106
+ index = TrigramIndex(index_dir=os.path.join(root, ".attocode", "index"))
107
+ return index if index.load() else None
108
+ return scoped_store("trigrams", build)
109
+ global _trigram_index
110
+ if _trigram_index is None:
111
+ with _trigram_index_lock:
112
+ if _trigram_index is None:
113
+ import os
114
+ project_dir = _get_project_dir()
115
+ index_dir = os.path.join(project_dir, ".attocode", "index")
116
+ if os.path.isdir(index_dir):
117
+ try:
118
+ from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
119
+ idx = TrigramIndex(index_dir=index_dir)
120
+ if idx.load():
121
+ _trigram_index = idx
122
+ except Exception:
123
+ pass
124
+ return _trigram_index
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Tools
129
+ # ---------------------------------------------------------------------------
130
+
131
+
132
+ @mcp.tool()
133
+ @pin_stamped
134
+ def semantic_search(
135
+ query: str,
136
+ top_k: int = 10,
137
+ file_filter: str = "",
138
+ branch: str = "",
139
+ mode: str = "auto",
140
+ ) -> str:
141
+ """Search the code by meaning with a natural-language query. Returns files, functions and classes, best first.
142
+
143
+ Args:
144
+ query: For example "authentication middleware".
145
+ file_filter: Optional glob, for example "*.py".
146
+ branch: Remote service only. Local search reads the working tree.
147
+ mode: "auto" (default), "keyword", or "vector" (waits for embeddings).
148
+ """
149
+ return _get_service().semantic_search(
150
+ query=query,
151
+ top_k=top_k,
152
+ file_filter=file_filter,
153
+ branch=branch,
154
+ mode=mode,
155
+ )
156
+
157
+
158
+ @mcp.tool()
159
+ def semantic_search_status() -> str:
160
+ """Get the status of the semantic search index.
161
+
162
+ Returns: provider name, coverage percentage, files indexed/total,
163
+ indexing status, and whether vector search is active.
164
+ """
165
+ remote = _get_remote_service()
166
+ if remote is not None:
167
+ return remote.semantic_search_status()
168
+
169
+ mgr = _get_semantic_search()
170
+ progress = mgr.get_index_progress()
171
+ discovered_files = 0
172
+ ast_indexed_files = 0
173
+ degradation_reasons: list[str] = []
174
+
175
+ try:
176
+ ctx = _get_context_mgr()
177
+ discovered = getattr(ctx, "_files", None)
178
+ if isinstance(discovered, list):
179
+ discovered_files = len(discovered)
180
+ if discovered_files == 0:
181
+ discovered_files = len(ctx.discover_files())
182
+ except Exception as exc:
183
+ degradation_reasons.append(f"context_error:{type(exc).__name__}")
184
+
185
+ try:
186
+ ast_svc = _get_ast_service()
187
+ stats = ast_svc._store.stats() if hasattr(ast_svc, "_store") else {}
188
+ ast_indexed_files = int(
189
+ stats.get("files", 0)
190
+ or stats.get("files_indexed", 0)
191
+ or len(getattr(ast_svc, "_ast_cache", {}))
192
+ )
193
+ except Exception as exc:
194
+ degradation_reasons.append(f"ast_error:{type(exc).__name__}")
195
+
196
+ if discovered_files == 0:
197
+ degradation_reasons.append("no_files_discovered")
198
+ if ast_indexed_files == 0:
199
+ degradation_reasons.append("no_files_indexed")
200
+
201
+ lines = [
202
+ "Semantic search status:",
203
+ f" Provider: {mgr.provider_name}",
204
+ f" Available: {mgr.is_available}",
205
+ f" Status: {progress.status}",
206
+ f" Coverage: {progress.coverage:.0%} ({progress.indexed_files}/{progress.total_files} files)",
207
+ f" Failed: {progress.failed_files}",
208
+ f" Vector search active: {mgr.is_index_ready()}",
209
+ f" Discovery count: {discovered_files}",
210
+ f" AST indexed files: {ast_indexed_files}",
211
+ f" Health: {'degraded' if degradation_reasons else 'healthy'}",
212
+ ]
213
+ try:
214
+ _store = getattr(mgr, "_store", None)
215
+ _store_model = getattr(_store, "model_name", "") if _store else ""
216
+ if _store_model and _store_model != mgr.provider_name:
217
+ lines.append(
218
+ f" Model mismatch: index built with {_store_model}, active "
219
+ f"{mgr.provider_name} — run reindex to rebuild vectors."
220
+ )
221
+ except Exception:
222
+ pass
223
+ if degradation_reasons:
224
+ lines.append(f" Degradation reason: {', '.join(degradation_reasons)}")
225
+ if getattr(progress, "degraded_reason", ""):
226
+ lines.append(f" Backend degradation: {progress.degraded_reason}")
227
+ if getattr(progress, "last_error", ""):
228
+ lines.append(f" Last error: {progress.last_error}")
229
+ if progress.elapsed_seconds > 0:
230
+ lines.append(f" Elapsed: {progress.elapsed_seconds:.1f}s")
231
+ return "\n".join(lines)
232
+
233
+
234
+ @mcp.tool()
235
+ def security_scan(
236
+ mode: str = "full",
237
+ path: str = "",
238
+ ) -> str:
239
+ """Scan the codebase for security issues.
240
+
241
+ Detects hardcoded secrets, code anti-patterns, and dependency
242
+ pinning issues. All scanning is local (no external API calls).
243
+ Returns a compliance score (0-100) and categorized findings.
244
+
245
+ Args:
246
+ mode: Scan mode -- 'quick' (secrets), 'full' (all), 'secrets', 'patterns', 'dependencies'.
247
+ path: Subdirectory to scan (relative to project root, empty for all).
248
+ """
249
+ return _get_service().security_scan(mode=mode, path=path)
250
+
251
+
252
+ @mcp.tool()
253
+ def dataflow_scan(
254
+ files: list[str] | None = None,
255
+ path: str = "",
256
+ ) -> str:
257
+ """Scan for data flow vulnerabilities using taint analysis.
258
+
259
+ Tracks user-controlled data (request parameters, input, argv) through
260
+ variable assignments to dangerous sinks (SQL execution, shell commands,
261
+ file operations, HTML output). Detects SQL injection (CWE-89), command
262
+ injection (CWE-78), XSS (CWE-79), path traversal (CWE-22), and SSRF
263
+ (CWE-918).
264
+
265
+ Currently supports Python and JavaScript/TypeScript. Analysis is
266
+ intra-procedural (within individual functions).
267
+
268
+ Args:
269
+ files: Specific files to analyze (relative paths). Default: all.
270
+ path: Subdirectory to restrict analysis to.
271
+ """
272
+ from attocode_intel._internal.integrations.security.dataflow import analyze_project, format_report
273
+
274
+ project_dir = _get_project_dir()
275
+ if path and not files:
276
+ import os
277
+ scan_dir = os.path.join(project_dir, path)
278
+ if not os.path.isdir(scan_dir):
279
+ return f"Error: Path not found: {path}"
280
+ # Discover files in subdirectory
281
+ _EXTS = {".py", ".js", ".ts", ".jsx", ".tsx"}
282
+ files = []
283
+ for dirpath, _, filenames in os.walk(scan_dir):
284
+ for fname in filenames:
285
+ if os.path.splitext(fname)[1].lower() in _EXTS:
286
+ files.append(os.path.relpath(
287
+ os.path.join(dirpath, fname), project_dir,
288
+ ))
289
+
290
+ report = analyze_project(project_dir, paths=files or None)
291
+ return format_report(report)
292
+
293
+
294
+ @mcp.tool()
295
+ @pin_stamped
296
+ def fast_search(
297
+ pattern: str,
298
+ path: str = "",
299
+ max_results: int = 50,
300
+ case_insensitive: bool = False,
301
+ selectivity_threshold: float = 0.10,
302
+ explain: bool = False,
303
+ ) -> str:
304
+ """Search file contents with a regular expression. A trigram index selects the candidate files first.
305
+
306
+ Args:
307
+ pattern: A regular expression, for example "def process_.*event".
308
+ path: Optional subdirectory relative to the project root.
309
+ selectivity_threshold: Skip the index when more than this share of files can match (default 0.10).
310
+ explain: Add search diagnostics to the output.
311
+ """
312
+ remote = _get_remote_service()
313
+ if remote is not None:
314
+ return remote.fast_search(
315
+ pattern=pattern,
316
+ path=path,
317
+ max_results=max_results,
318
+ case_insensitive=case_insensitive,
319
+ selectivity_threshold=selectivity_threshold,
320
+ explain=explain,
321
+ )
322
+
323
+ import re
324
+ from pathlib import Path
325
+
326
+ project_dir = _get_project_dir()
327
+ root = Path(project_dir)
328
+ if path:
329
+ root = root / path
330
+ root = root.resolve()
331
+
332
+ if not root.exists():
333
+ return f"Error: Path not found: {root}"
334
+
335
+ flags = re.IGNORECASE if case_insensitive else 0
336
+ try:
337
+ regex = re.compile(pattern, flags)
338
+ except re.error as e:
339
+ return f"Error: Invalid regex pattern: {e}"
340
+
341
+ # Try trigram pre-filtering
342
+ trigram_idx = _get_trigram_index()
343
+ candidates: list[str] | None = None
344
+ index_status = "no index"
345
+ query_result = None # QueryResult when explain=True
346
+
347
+ if trigram_idx is not None and trigram_idx.is_ready():
348
+ if explain:
349
+ from attocode_intel._internal.integrations.context.trigram_index import QueryResult
350
+
351
+ query_result = trigram_idx.query(
352
+ pattern,
353
+ case_insensitive=case_insensitive,
354
+ selectivity_threshold=selectivity_threshold,
355
+ explain=True,
356
+ )
357
+ candidates = query_result.candidates
358
+ index_status = query_result.mode
359
+ else:
360
+ candidates = trigram_idx.query(
361
+ pattern,
362
+ case_insensitive=case_insensitive,
363
+ selectivity_threshold=selectivity_threshold,
364
+ )
365
+ if candidates is not None:
366
+ index_status = f"trigram filter: {len(candidates)} candidates"
367
+ else:
368
+ index_status = "no trigrams extractable or threshold exceeded, full scan"
369
+ else:
370
+ # Try to build the index on first use
371
+ try:
372
+ from attocode_intel._internal.integrations.context.trigram_index import TrigramIndex
373
+ index_dir = os.path.join(project_dir, ".attocode", "index")
374
+ idx = TrigramIndex(index_dir=index_dir)
375
+ stats = idx.build(project_dir)
376
+ global _trigram_index
377
+ _trigram_index = idx
378
+ if explain:
379
+ query_result = idx.query(
380
+ pattern,
381
+ case_insensitive=case_insensitive,
382
+ selectivity_threshold=selectivity_threshold,
383
+ explain=True,
384
+ )
385
+ candidates = query_result.candidates
386
+ index_status = (
387
+ f"built index ({stats['files_indexed']} files, "
388
+ f"{stats['build_time_ms']}ms), mode: {query_result.mode}"
389
+ )
390
+ else:
391
+ candidates = idx.query(
392
+ pattern,
393
+ case_insensitive=case_insensitive,
394
+ selectivity_threshold=selectivity_threshold,
395
+ )
396
+ index_status = (
397
+ f"built index ({stats['files_indexed']} files, "
398
+ f"{stats['build_time_ms']}ms), "
399
+ f"{len(candidates) if candidates is not None else 'N/A'} candidates"
400
+ )
401
+ except Exception:
402
+ index_status = "index build failed, full scan"
403
+ if explain:
404
+ from attocode_intel._internal.integrations.context.trigram_index import QueryResult
405
+
406
+ query_result = QueryResult(
407
+ candidates=None, trigram_literals=[], posting_sizes=[],
408
+ total_files=0, candidate_count=0, selectivity=0.0,
409
+ threshold=selectivity_threshold, threshold_triggered=False,
410
+ mode="index-build-failed",
411
+ )
412
+
413
+ # Determine files to search
414
+ if candidates is not None:
415
+ files = sorted(root / c for c in candidates)
416
+ else:
417
+ files = sorted(root.rglob("*"))
418
+
419
+ matches: list[str] = []
420
+ for file in files:
421
+ if not file.is_file() or file.name.startswith("."):
422
+ continue
423
+ try:
424
+ content = file.read_text(encoding="utf-8", errors="strict")
425
+ except (UnicodeDecodeError, OSError):
426
+ continue
427
+ for i, line in enumerate(content.splitlines(), 1):
428
+ if regex.search(line):
429
+ try:
430
+ rel = file.relative_to(Path(project_dir))
431
+ except ValueError:
432
+ rel = file.name
433
+ matches.append(f"{rel}:{i}: {line.strip()}")
434
+ if len(matches) >= max_results:
435
+ break
436
+ if len(matches) >= max_results:
437
+ break
438
+
439
+ if not matches:
440
+ result = f"No matches found ({index_status})"
441
+ else:
442
+ result = "\n".join(matches)
443
+ if len(matches) >= max_results:
444
+ result += f"\n... (limited to {max_results} results)"
445
+ result += f"\n({index_status})"
446
+
447
+ # Append diagnostics when explain=True
448
+ if explain and query_result is not None:
449
+ qr = query_result
450
+ sel_pct = f"{qr.selectivity * 100:.2f}%"
451
+ threshold_pct = f"{qr.threshold * 100:.1f}%"
452
+ verdict = "SKIP (threshold exceeded)" if qr.threshold_triggered else "PASS"
453
+ diag_lines = [
454
+ "",
455
+ "--- Search Diagnostics ---",
456
+ f"Pattern: {pattern}",
457
+ f"Trigrams: {qr.trigram_literals!r}",
458
+ f"Posting sizes: {qr.posting_sizes!r}",
459
+ f"Candidates: {qr.candidate_count} / {qr.total_files} files ({sel_pct})",
460
+ f"Selectivity: {verdict} (threshold: {threshold_pct})",
461
+ f"Mode: {qr.mode}",
462
+ ]
463
+ result += "\n".join(diag_lines)
464
+
465
+ return result
466
+
467
+
468
+ @mcp.tool()
469
+ @pin_stamped
470
+ def regex_search(
471
+ pattern: str,
472
+ path: str = "",
473
+ max_results: int = 50,
474
+ case_insensitive: bool = False,
475
+ ) -> str:
476
+ """Search code with regex, accelerated by trigram index.
477
+
478
+ A straightforward regex search tool that leverages the trigram inverted
479
+ index for 10-100x speedup over brute-force grep. Useful for finding
480
+ specific code patterns like function signatures, import statements,
481
+ or configuration values.
482
+
483
+ Args:
484
+ pattern: Regex pattern to search for.
485
+ path: Subdirectory to restrict search to (relative to project root).
486
+ max_results: Maximum number of matching lines to return.
487
+ case_insensitive: Whether to match case-insensitively.
488
+ """
489
+ # NOTE: regex_search has no server-side HTTP route, so it cannot be
490
+ # proxied to a remote. It always runs locally (against the trigram index /
491
+ # local files), even when a remote is configured.
492
+ import re
493
+ from pathlib import Path
494
+
495
+ project_dir = _get_project_dir()
496
+ root = Path(project_dir)
497
+ if path:
498
+ root = root / path
499
+ root = root.resolve()
500
+
501
+ if not root.exists():
502
+ return f"Error: Path not found: {root}"
503
+
504
+ flags = re.IGNORECASE if case_insensitive else 0
505
+ try:
506
+ regex = re.compile(pattern, flags)
507
+ except re.error as e:
508
+ return f"Error: Invalid regex pattern: {e}"
509
+
510
+ # Try trigram pre-filtering
511
+ _SELECTIVITY_THRESHOLD = 0.10
512
+ trigram_idx = _get_trigram_index()
513
+ candidates: list[str] | None = None
514
+ used_index = False
515
+
516
+ if trigram_idx is not None and trigram_idx.is_ready():
517
+ candidates = trigram_idx.query(
518
+ pattern,
519
+ case_insensitive=case_insensitive,
520
+ selectivity_threshold=_SELECTIVITY_THRESHOLD,
521
+ )
522
+ if candidates is not None:
523
+ used_index = True
524
+
525
+ # Determine files to search
526
+ if candidates is not None:
527
+ files = sorted(root / c for c in candidates)
528
+ else:
529
+ files = sorted(root.rglob("*"))
530
+
531
+ matches: list[str] = []
532
+ for file in files:
533
+ if not file.is_file() or file.name.startswith("."):
534
+ continue
535
+ try:
536
+ content = file.read_text(encoding="utf-8", errors="strict")
537
+ except (UnicodeDecodeError, OSError):
538
+ continue
539
+ for i, line in enumerate(content.splitlines(), 1):
540
+ if regex.search(line):
541
+ try:
542
+ rel = file.relative_to(Path(project_dir))
543
+ except ValueError:
544
+ rel = file.name
545
+ matches.append(f"{rel}:{i}: {line.strip()}")
546
+ if len(matches) >= max_results:
547
+ break
548
+ if len(matches) >= max_results:
549
+ break
550
+
551
+ total = len(matches)
552
+ if not matches:
553
+ return "No matches found."
554
+
555
+ result = "\n".join(matches)
556
+ if total >= max_results:
557
+ result += f"\n... (limited to {max_results} results)"
558
+ index_note = "trigram-indexed" if used_index else "brute-force"
559
+ result += f"\n{total} match(es) found ({index_note})."
560
+ return result
561
+
562
+
563
+ @mcp.tool()
564
+ @pin_stamped
565
+ def frecency_search(
566
+ pattern: str,
567
+ path: str = "",
568
+ max_results: int = 50,
569
+ case_insensitive: bool = False,
570
+ selectivity_threshold: float = 0.10,
571
+ use_frecency: bool = True,
572
+ ai_mode: bool = True,
573
+ ) -> str:
574
+ """Frecency-boosted regex search for AI agents.
575
+
576
+ Like fast_search but boosts results by frecency score (how often and
577
+ recently files were accessed). This helps prioritize commonly-used files
578
+ when there are many matches.
579
+
580
+ The frecency score is calculated from:
581
+ - Access frequency (exponential decay with 10-day or 3-day half-life)
582
+ - Recency (more recent = higher score)
583
+ - Git modification bonus (recently modified files get +1-16 points)
584
+
585
+ Args:
586
+ pattern: Regex pattern to search for.
587
+ path: Subdirectory to search (relative to project root, empty for all).
588
+ max_results: Maximum number of matching lines to return (default 50).
589
+ case_insensitive: Whether to match case-insensitively.
590
+ selectivity_threshold: Skip trigram index when matching files exceeds
591
+ this fraction (0.0-1.0, default 0.10).
592
+ use_frecency: Whether to apply frecency boosting (default True).
593
+ ai_mode: Use AI mode decay (3-day half-life) vs human mode (10-day).
594
+
595
+ Returns:
596
+ Search results with frecency-boosted ranking.
597
+ """
598
+ import re
599
+ from collections import defaultdict
600
+ from pathlib import Path
601
+
602
+ project_dir = _get_project_dir()
603
+ root = Path(project_dir)
604
+ if path:
605
+ root = root / path
606
+ root = root.resolve()
607
+
608
+ if not root.exists():
609
+ return f"Error: Path not found: {root}"
610
+
611
+ flags = re.IGNORECASE if case_insensitive else 0
612
+ try:
613
+ regex = re.compile(pattern, flags)
614
+ except re.error as e:
615
+ return f"Error: Invalid regex pattern: {e}"
616
+
617
+ # Get trigram candidates
618
+ trigram_idx = _get_trigram_index()
619
+ candidates: list[str] | None = None
620
+ index_status = "no index"
621
+
622
+ if trigram_idx is not None and trigram_idx.is_ready():
623
+ candidates = trigram_idx.query(
624
+ pattern,
625
+ case_insensitive=case_insensitive,
626
+ selectivity_threshold=selectivity_threshold,
627
+ )
628
+ if candidates is not None:
629
+ index_status = f"trigram filter: {len(candidates)} candidates"
630
+ else:
631
+ index_status = "no trigrams, full scan"
632
+ else:
633
+ index_status = "no index, full scan"
634
+
635
+ # Get frecency scores
636
+ frecency_tracker = _get_frecency_tracker() if use_frecency else None
637
+
638
+ # Determine files to search
639
+ if candidates is not None:
640
+ files = sorted(root / c for c in candidates)
641
+ else:
642
+ all_files = sorted(root.rglob("*"))
643
+ try:
644
+ from attocode_intel._internal.integrations.utilities.ignore import IgnoreManager
645
+
646
+ ignore_mgr = IgnoreManager(root=Path(project_dir))
647
+ files = [
648
+ f
649
+ for f in all_files
650
+ if f.is_file()
651
+ and not f.name.startswith(".")
652
+ and not ignore_mgr.is_ignored(
653
+ str(f.relative_to(Path(project_dir)))
654
+ )
655
+ ]
656
+ except (ImportError, ValueError):
657
+ files = [
658
+ f
659
+ for f in all_files
660
+ if f.is_file() and not f.name.startswith(".")
661
+ ]
662
+
663
+ # Phase 1: Collect candidate file paths
664
+ candidate_paths: list[Path] = []
665
+ for file in files:
666
+ if not file.is_file() or file.name.startswith("."):
667
+ continue
668
+ candidate_paths.append(file)
669
+
670
+ # Phase 2: Pre-sort by frecency so high-value files are searched first
671
+ _FRECENCY_PRESORT_LIMIT = 10_000
672
+ file_scores: dict[str, FrecencyResult] = {}
673
+ if (
674
+ frecency_tracker is not None
675
+ and use_frecency
676
+ and len(candidate_paths) <= _FRECENCY_PRESORT_LIMIT
677
+ ):
678
+ path_strs: list[str] = []
679
+ path_map: dict[str, Path] = {}
680
+ for f in candidate_paths:
681
+ try:
682
+ rel = str(f.relative_to(Path(project_dir)))
683
+ except ValueError:
684
+ rel = str(f)
685
+ path_strs.append(rel)
686
+ path_map[rel] = f
687
+
688
+ file_scores = frecency_tracker.get_scores_batch(path_strs, ai_mode=ai_mode)
689
+
690
+ # Build reverse lookup to avoid recomputing relative_to() in sort
691
+ path_to_rel: dict[Path, str] = {v: k for k, v in path_map.items()}
692
+
693
+ # Sort: high-frecency files first, then alphabetical as tiebreaker
694
+ candidate_paths.sort(
695
+ key=lambda f: (
696
+ -file_scores.get(
697
+ path_to_rel.get(f, str(f)),
698
+ _empty_frecency(ai_mode),
699
+ ).score,
700
+ str(f),
701
+ ),
702
+ )
703
+
704
+ # Phase 3: Read content from frecency-sorted files
705
+ file_matches: dict[str, list[tuple[int, str]]] = defaultdict(list)
706
+
707
+ for file in candidate_paths:
708
+ try:
709
+ content = file.read_text(encoding="utf-8", errors="strict")
710
+ except (UnicodeDecodeError, OSError):
711
+ continue
712
+ for i, line in enumerate(content.splitlines(), 1):
713
+ if regex.search(line):
714
+ try:
715
+ rel = str(file.relative_to(Path(project_dir)))
716
+ except ValueError:
717
+ rel = file.name
718
+ file_matches[rel].append((i, line.strip()))
719
+ if sum(len(v) for v in file_matches.values()) >= max_results * 3:
720
+ break
721
+ if sum(len(v) for v in file_matches.values()) >= max_results * 3:
722
+ break
723
+
724
+ if not file_matches:
725
+ return f"No matches found ({index_status})"
726
+
727
+ # Fetch scores for any files not yet scored (e.g. from trigram path)
728
+ unscored = [p for p in file_matches if p not in file_scores]
729
+ if unscored and frecency_tracker is not None:
730
+ file_scores.update(
731
+ frecency_tracker.get_scores_batch(unscored, ai_mode=ai_mode)
732
+ )
733
+ # Fill in defaults for unscored files
734
+ for p in file_matches:
735
+ if p not in file_scores:
736
+ file_scores[p] = _empty_frecency(ai_mode)
737
+
738
+ # Sort files by frecency score (descending), then by match count
739
+ sorted_files = sorted(
740
+ file_matches.items(),
741
+ key=lambda kv: (
742
+ -file_scores.get(kv[0], _empty_frecency(ai_mode)).score,
743
+ -len(kv[1]),
744
+ ),
745
+ )
746
+
747
+ # Build results respecting max_results
748
+ results: list[str] = []
749
+ for file_path, matches in sorted_files:
750
+ score = file_scores.get(file_path, _empty_frecency(ai_mode)).score
751
+ for line_num, line_content in matches[:5]: # Max 5 matches per file
752
+ results.append(f"{file_path}:{line_num}: [{score}] {line_content}")
753
+ if len(results) >= max_results:
754
+ break
755
+ if len(results) >= max_results:
756
+ break
757
+
758
+ if not results:
759
+ return f"No matches found ({index_status})"
760
+
761
+ result = "\n".join(results)
762
+ if len(results) >= max_results:
763
+ result += f"\n... (limited to {max_results} results)"
764
+
765
+ # Add frecency stats header
766
+ total_files = len(file_matches)
767
+ avg_score = sum(s.score for s in file_scores.values()) / max(len(file_scores), 1)
768
+ result += f"\n({index_status}, {total_files} files matched, avg frecency: {avg_score:.1f})"
769
+
770
+ return result