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,411 @@
1
+ """Middleware for CORS, request logging, rate limiting, and metrics collection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import threading
8
+ import time
9
+ from collections import defaultdict, deque
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from starlette.middleware.base import BaseHTTPMiddleware
13
+ from starlette.responses import JSONResponse
14
+
15
+ if TYPE_CHECKING:
16
+ from starlette.requests import Request
17
+ from starlette.responses import Response
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Metrics Collector (module-level singleton)
24
+ # ---------------------------------------------------------------------------
25
+
26
+ _MAX_ENTRIES = 10_000
27
+
28
+
29
+ def _get_metrics_category(path: str) -> str:
30
+ """Categorize request path into a metrics bucket.
31
+
32
+ Returns one of: "search", "analysis", "graph", "auth", "notify", "health",
33
+ "lsp", "learning", "files", "projects", "embeddings", "other".
34
+ """
35
+ if path in ("/health", "/ready") or path.startswith("/api/v1/metrics"):
36
+ return "health"
37
+ if path.startswith("/api/v1/auth") or path.startswith("/api/v1/register"):
38
+ return "auth"
39
+ if path.startswith("/api/v1/notify") or path.startswith("/api/v2/notify"):
40
+ return "notify"
41
+ if "/search" in path or "/semantic" in path or "/symbols" in path:
42
+ return "search"
43
+ if "/analysis" in path or "/impact" in path or "/hotspots" in path or "/cross-ref" in path:
44
+ return "analysis"
45
+ if "/graph" in path or "/dependencies" in path or "/deps" in path:
46
+ return "graph"
47
+ if "/lsp" in path:
48
+ return "lsp"
49
+ if "/learning" in path:
50
+ return "learning"
51
+ if "/files" in path:
52
+ return "files"
53
+ if "/projects" in path or "/repos" in path:
54
+ return "projects"
55
+ if "/embeddings" in path:
56
+ return "embeddings"
57
+ return "other"
58
+
59
+
60
+ class MetricsCollector:
61
+ """Thread-safe, in-memory metrics collector using a ring buffer.
62
+
63
+ Tracks request latencies, search statistics, and tool call metrics.
64
+ All data is stored in-memory using ``collections.deque`` with a max
65
+ size of 10,000 entries per category.
66
+ """
67
+
68
+ def __init__(self, max_entries: int = _MAX_ENTRIES) -> None:
69
+ self._lock = threading.Lock()
70
+ self._max_entries = max_entries
71
+
72
+ # Request metrics: deque of (path_category, method, status, duration_ms, timestamp)
73
+ self._requests: deque[tuple[str, str, int, float, float]] = deque(maxlen=max_entries)
74
+
75
+ # Search metrics: deque of (query, result_count, duration_ms, cache_hit, timestamp)
76
+ self._searches: deque[tuple[str, int, float, bool, float]] = deque(maxlen=max_entries)
77
+
78
+ # Tool call metrics: deque of (tool_name, duration_ms, success, timestamp)
79
+ self._tool_calls: deque[tuple[str, float, bool, float]] = deque(maxlen=max_entries)
80
+
81
+ # -- Recording methods ---------------------------------------------------
82
+
83
+ def record_request(
84
+ self, path: str, method: str, status: int, duration_ms: float
85
+ ) -> None:
86
+ """Record an HTTP request metric."""
87
+ category = _get_metrics_category(path)
88
+ with self._lock:
89
+ self._requests.append((category, method, status, duration_ms, time.time()))
90
+
91
+ def record_search(
92
+ self, query: str, result_count: int, duration_ms: float, cache_hit: bool
93
+ ) -> None:
94
+ """Record a search query metric."""
95
+ with self._lock:
96
+ self._searches.append((query, result_count, duration_ms, cache_hit, time.time()))
97
+
98
+ def record_tool_call(
99
+ self, tool_name: str, duration_ms: float, success: bool
100
+ ) -> None:
101
+ """Record an MCP tool call metric."""
102
+ with self._lock:
103
+ self._tool_calls.append((tool_name, duration_ms, success, time.time()))
104
+
105
+ # -- Percentile computation ----------------------------------------------
106
+
107
+ @staticmethod
108
+ def _compute_percentiles(values: list[float]) -> dict[str, float]:
109
+ """Compute p50, p95, and p99 from a list of values.
110
+
111
+ Returns dict with keys "p50", "p95", "p99". Returns all zeros if
112
+ *values* is empty.
113
+ """
114
+ if not values:
115
+ return {"p50": 0.0, "p95": 0.0, "p99": 0.0}
116
+ sorted_vals = sorted(values)
117
+ n = len(sorted_vals)
118
+
119
+ def _pct(p: float) -> float:
120
+ idx = int(p / 100.0 * (n - 1))
121
+ return sorted_vals[min(idx, n - 1)]
122
+
123
+ return {
124
+ "p50": round(_pct(50), 2),
125
+ "p95": round(_pct(95), 2),
126
+ "p99": round(_pct(99), 2),
127
+ }
128
+
129
+ # -- Aggregation ---------------------------------------------------------
130
+
131
+ def get_metrics(self, fmt: str = "json") -> dict[str, Any] | str:
132
+ """Return aggregated metrics.
133
+
134
+ Args:
135
+ fmt: ``"json"`` returns a dict, ``"prometheus"`` returns
136
+ Prometheus text exposition format.
137
+ """
138
+ with self._lock:
139
+ requests = list(self._requests)
140
+ searches = list(self._searches)
141
+ tool_calls = list(self._tool_calls)
142
+
143
+ # -- Request metrics -------------------------------------------------
144
+ requests_by_category: dict[str, list[float]] = defaultdict(list)
145
+ status_counts: dict[int, int] = defaultdict(int)
146
+ total_requests = len(requests)
147
+
148
+ for category, _method, status, duration_ms, _ts in requests:
149
+ requests_by_category[category].append(duration_ms)
150
+ status_counts[status] += 1
151
+
152
+ request_latency: dict[str, dict[str, Any]] = {}
153
+ for category, durations in requests_by_category.items():
154
+ request_latency[category] = {
155
+ "count": len(durations),
156
+ "avg_ms": round(sum(durations) / len(durations), 2) if durations else 0.0,
157
+ **self._compute_percentiles(durations),
158
+ }
159
+
160
+ # -- Search metrics --------------------------------------------------
161
+ search_durations = [d for _, _, d, _, _ in searches]
162
+ search_result_counts = [rc for _, rc, _, _, _ in searches]
163
+ cache_hits = sum(1 for _, _, _, hit, _ in searches if hit)
164
+ cache_misses = len(searches) - cache_hits
165
+
166
+ search_metrics: dict[str, Any] = {
167
+ "total": len(searches),
168
+ "cache_hits": cache_hits,
169
+ "cache_misses": cache_misses,
170
+ "cache_hit_rate": round(cache_hits / len(searches), 4) if searches else 0.0,
171
+ "latency": self._compute_percentiles(search_durations),
172
+ "avg_result_count": (
173
+ round(sum(search_result_counts) / len(search_result_counts), 2)
174
+ if search_result_counts
175
+ else 0.0
176
+ ),
177
+ }
178
+
179
+ # -- Tool call metrics -----------------------------------------------
180
+ tools_by_name: dict[str, dict[str, Any]] = defaultdict(
181
+ lambda: {"count": 0, "success": 0, "failure": 0, "durations": []}
182
+ )
183
+ for tool_name, duration_ms, success, _ts in tool_calls:
184
+ entry = tools_by_name[tool_name]
185
+ entry["count"] += 1
186
+ if success:
187
+ entry["success"] += 1
188
+ else:
189
+ entry["failure"] += 1
190
+ entry["durations"].append(duration_ms)
191
+
192
+ tool_metrics: dict[str, dict[str, Any]] = {}
193
+ for tool_name, data in tools_by_name.items():
194
+ durations = data["durations"]
195
+ tool_metrics[tool_name] = {
196
+ "count": data["count"],
197
+ "success": data["success"],
198
+ "failure": data["failure"],
199
+ "avg_ms": round(sum(durations) / len(durations), 2) if durations else 0.0,
200
+ **self._compute_percentiles(durations),
201
+ }
202
+
203
+ result: dict[str, Any] = {
204
+ "requests": {
205
+ "total": total_requests,
206
+ "by_category": request_latency,
207
+ "by_status": dict(status_counts),
208
+ },
209
+ "search": search_metrics,
210
+ "tools": tool_metrics,
211
+ }
212
+
213
+ if fmt == "prometheus":
214
+ return self._format_prometheus(result)
215
+ return result
216
+
217
+ # -- Prometheus formatting -----------------------------------------------
218
+
219
+ @staticmethod
220
+ def _format_prometheus(metrics: dict[str, Any]) -> str:
221
+ """Format metrics as Prometheus text exposition."""
222
+ lines: list[str] = []
223
+
224
+ # Request metrics
225
+ lines.append("# HELP code_intel_requests_total Total HTTP requests")
226
+ lines.append("# TYPE code_intel_requests_total counter")
227
+ lines.append(f"code_intel_requests_total {metrics['requests']['total']}")
228
+
229
+ for category, data in metrics["requests"]["by_category"].items():
230
+ lines.append(f'code_intel_requests_by_category{{category="{category}"}} {data["count"]}')
231
+
232
+ lines.append("")
233
+ lines.append("# HELP code_intel_request_duration_ms Request latency in milliseconds")
234
+ lines.append("# TYPE code_intel_request_duration_ms summary")
235
+ for category, data in metrics["requests"]["by_category"].items():
236
+ for pct in ("p50", "p95", "p99"):
237
+ quantile = {"p50": "0.5", "p95": "0.95", "p99": "0.99"}[pct]
238
+ lines.append(
239
+ f'code_intel_request_duration_ms{{category="{category}",quantile="{quantile}"}} {data[pct]}'
240
+ )
241
+
242
+ for status, count in metrics["requests"]["by_status"].items():
243
+ lines.append(f'code_intel_requests_by_status{{status="{status}"}} {count}')
244
+
245
+ # Search metrics
246
+ lines.append("")
247
+ lines.append("# HELP code_intel_search_total Total search queries")
248
+ lines.append("# TYPE code_intel_search_total counter")
249
+ lines.append(f"code_intel_search_total {metrics['search']['total']}")
250
+ lines.append(f"code_intel_search_cache_hits {metrics['search']['cache_hits']}")
251
+ lines.append(f"code_intel_search_cache_misses {metrics['search']['cache_misses']}")
252
+ lines.append(f"code_intel_search_cache_hit_rate {metrics['search']['cache_hit_rate']}")
253
+
254
+ lines.append("")
255
+ lines.append("# HELP code_intel_search_duration_ms Search latency in milliseconds")
256
+ lines.append("# TYPE code_intel_search_duration_ms summary")
257
+ for pct in ("p50", "p95", "p99"):
258
+ quantile = {"p50": "0.5", "p95": "0.95", "p99": "0.99"}[pct]
259
+ lines.append(
260
+ f'code_intel_search_duration_ms{{quantile="{quantile}"}} {metrics["search"]["latency"][pct]}'
261
+ )
262
+
263
+ # Tool metrics
264
+ lines.append("")
265
+ lines.append("# HELP code_intel_tool_calls_total Total MCP tool calls")
266
+ lines.append("# TYPE code_intel_tool_calls_total counter")
267
+ for tool_name, data in metrics["tools"].items():
268
+ lines.append(f'code_intel_tool_calls_total{{tool="{tool_name}"}} {data["count"]}')
269
+ lines.append(f'code_intel_tool_calls_success{{tool="{tool_name}"}} {data["success"]}')
270
+ lines.append(f'code_intel_tool_calls_failure{{tool="{tool_name}"}} {data["failure"]}')
271
+ for pct in ("p50", "p95", "p99"):
272
+ quantile = {"p50": "0.5", "p95": "0.95", "p99": "0.99"}[pct]
273
+ lines.append(
274
+ f'code_intel_tool_duration_ms{{tool="{tool_name}",quantile="{quantile}"}} {data[pct]}'
275
+ )
276
+
277
+ lines.append("")
278
+ return "\n".join(lines)
279
+
280
+
281
+ # Module-level singleton
282
+ metrics_collector = MetricsCollector()
283
+
284
+
285
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
286
+ """Log request method, path, status, and duration."""
287
+
288
+ async def dispatch(self, request: Request, call_next) -> Response:
289
+ start = time.monotonic()
290
+ response = await call_next(request)
291
+ duration_ms = (time.monotonic() - start) * 1000
292
+ logger.info(
293
+ "%s %s → %d (%.1fms)",
294
+ request.method,
295
+ request.url.path,
296
+ response.status_code,
297
+ duration_ms,
298
+ )
299
+ return response
300
+
301
+
302
+ def _get_category(path: str) -> str:
303
+ """Categorize request path for tiered rate limiting.
304
+
305
+ Returns: "auth" | "notify" | "query"
306
+ """
307
+ if path.startswith("/api/v1/auth") or path.startswith("/api/v1/register"):
308
+ return "auth"
309
+ if path.startswith("/api/v1/notify"):
310
+ return "notify"
311
+ return "query"
312
+
313
+
314
+ class RateLimitMiddleware(BaseHTTPMiddleware):
315
+ """In-memory sliding window rate limiter with tiered limits.
316
+
317
+ Tiers:
318
+ - auth endpoints: 30 RPM (brute-force prevention)
319
+ - notify endpoints: 10x base (initial sync, watch)
320
+ - query endpoints: base RPM (default 300)
321
+
322
+ Keyed by org_id (from auth header) or client IP for unauthenticated requests.
323
+ """
324
+
325
+ def __init__(self, app, requests_per_minute: int = 300) -> None:
326
+ super().__init__(app)
327
+ # Read from env, fallback to constructor arg
328
+ self.base_rpm = int(os.environ.get("ATTOCODE_RATE_LIMIT_RPM", str(requests_per_minute)))
329
+ self._tier_multipliers = {
330
+ "auth": 0.1, # 30 RPM at base=300
331
+ "notify": 10.0, # 3000 RPM at base=300
332
+ "query": 1.0, # 300 RPM at base=300
333
+ }
334
+ self._windows: dict[str, list[float]] = defaultdict(list)
335
+
336
+ def _get_rpm(self, category: str) -> int:
337
+ mult = self._tier_multipliers.get(category, 1.0)
338
+ return max(1, int(self.base_rpm * mult))
339
+
340
+ def _get_key(self, request: Request, category: str) -> str:
341
+ """Extract rate limit key from request, scoped by category."""
342
+ # Try to extract org from auth header (best-effort, pre-auth)
343
+ auth = request.headers.get("authorization", "")
344
+ base = f"auth:{hash(auth)}" if auth else f"ip:{request.client.host if request.client else 'unknown'}"
345
+ return f"{category}:{base}"
346
+
347
+ def _check_and_record(self, key: str, now: float, rpm: int | None = None) -> tuple[bool, int, int]:
348
+ """Check rate limit and record request.
349
+
350
+ Returns (allowed, remaining, reset_seconds).
351
+ """
352
+ if rpm is None:
353
+ rpm = self.base_rpm
354
+ window = self._windows[key]
355
+ cutoff = now - 60.0
356
+ # Prune expired entries
357
+ self._windows[key] = window = [t for t in window if t > cutoff]
358
+
359
+ remaining = max(0, rpm - len(window))
360
+ reset_seconds = int(cutoff + 60 - now) if window else 60
361
+
362
+ if len(window) >= rpm:
363
+ return False, 0, max(1, reset_seconds)
364
+
365
+ window.append(now)
366
+ return True, remaining - 1, max(1, reset_seconds)
367
+
368
+ async def dispatch(self, request: Request, call_next) -> Response:
369
+ # Skip health endpoints
370
+ if request.url.path in ("/health", "/ready"):
371
+ return await call_next(request)
372
+
373
+ category = _get_category(request.url.path)
374
+ rpm = self._get_rpm(category)
375
+ key = self._get_key(request, category)
376
+ now = time.monotonic()
377
+ allowed, remaining, reset_seconds = self._check_and_record(key, now, rpm)
378
+
379
+ if not allowed:
380
+ return JSONResponse(
381
+ status_code=429,
382
+ content={"detail": "Rate limit exceeded"},
383
+ headers={
384
+ "X-RateLimit-Limit": str(rpm),
385
+ "X-RateLimit-Remaining": "0",
386
+ "X-RateLimit-Reset": str(reset_seconds),
387
+ "Retry-After": str(reset_seconds),
388
+ },
389
+ )
390
+
391
+ response = await call_next(request)
392
+ response.headers["X-RateLimit-Limit"] = str(rpm)
393
+ response.headers["X-RateLimit-Remaining"] = str(remaining)
394
+ response.headers["X-RateLimit-Reset"] = str(reset_seconds)
395
+ return response
396
+
397
+
398
+ class MetricsMiddleware(BaseHTTPMiddleware):
399
+ """Record request metrics via the module-level ``metrics_collector``."""
400
+
401
+ async def dispatch(self, request: Request, call_next) -> Response:
402
+ start = time.monotonic()
403
+ response = await call_next(request)
404
+ duration_ms = (time.monotonic() - start) * 1000
405
+ metrics_collector.record_request(
406
+ path=request.url.path,
407
+ method=request.method,
408
+ status=response.status_code,
409
+ duration_ms=duration_ms,
410
+ )
411
+ return response