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,623 @@
1
+ """File change notification endpoint — hook integration for incremental indexing.
2
+
3
+ This endpoint receives file change notifications from Claude Code post-tooluse
4
+ hooks (or any other client) and feeds them into the debounced incremental
5
+ indexing pipeline.
6
+
7
+ Protocol:
8
+ POST /api/v1/notify/file-changed
9
+ Body: {"paths": ["src/a.py"], "project": "...", "branch": "feat/x"}
10
+ Response: 202 Accepted (processing is async)
11
+
12
+ Hook integration example (.claude/hooks.toml):
13
+ [hooks.post_tool_use]
14
+ command = "curl -s -X POST http://localhost:8080/api/v1/notify/file-changed ..."
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import uuid
21
+ from datetime import datetime, timezone
22
+ from typing import TYPE_CHECKING
23
+
24
+ from fastapi import APIRouter, Depends, HTTPException
25
+ from pydantic import BaseModel, Field
26
+
27
+ from attocode_intel.api.auth import resolve_auth
28
+
29
+ if TYPE_CHECKING:
30
+ from attocode_intel.indexing.debouncer import FileChangeDebouncer
31
+
32
+ logger = logging.getLogger(__name__)
33
+ router = APIRouter(tags=["notify"])
34
+
35
+
36
+ # --- Request/Response models ---
37
+
38
+
39
+ class FileChangedRequest(BaseModel):
40
+ paths: list[str] = Field(..., description="List of changed file paths (relative to project root)")
41
+ files: dict[str, str] = Field(default_factory=dict, description="Optional: path → base64-encoded file content (for remote servers that can't read client filesystem)")
42
+ project: str = Field("", description="Project ID or path (local mode: ignored, uses default)")
43
+ branch: str = Field("", description="Branch name (optional, auto-detected if empty)")
44
+ idempotency_key: str = Field("", description="Optional idempotency key to prevent duplicate processing")
45
+ if_match: int | None = Field(None, description="Expected branch version (optimistic concurrency)")
46
+
47
+
48
+ class FileChangedResponse(BaseModel):
49
+ accepted: int = Field(..., description="Number of paths accepted for processing")
50
+ message: str = "Accepted"
51
+ version: int | None = Field(None, description="Current branch version after update")
52
+
53
+
54
+ class FlushRequest(BaseModel):
55
+ project: str = Field("", description="Project ID to flush")
56
+ branch: str = Field("", description="Branch to flush")
57
+
58
+
59
+ # --- Module-level debouncer singleton ---
60
+
61
+ _debouncer: FileChangeDebouncer | None = None
62
+ _pipeline_initialized = False
63
+
64
+ # --- Idempotency cache ---
65
+
66
+ _idempotency_cache: dict[str, float] = {}
67
+ _IDEMPOTENCY_TTL = 300 # 5 minutes
68
+
69
+
70
+ def _get_debouncer() -> FileChangeDebouncer:
71
+ """Get or create the module-level debouncer singleton."""
72
+ global _debouncer
73
+ if _debouncer is None:
74
+ from attocode_intel.indexing.debouncer import FileChangeDebouncer
75
+
76
+ _debouncer = FileChangeDebouncer(handler=_handle_debounced_batch)
77
+ return _debouncer
78
+
79
+
80
+ async def _handle_debounced_batch(project_id: str, branch: str, paths: list[str], files: dict[str, str] | None = None, request_if_match: int | None = None) -> None:
81
+ """Handle a debounced batch of file changes.
82
+
83
+ This runs after the debounce window closes. It processes files through
84
+ the content-hash-gated incremental pipeline.
85
+ """
86
+ from attocode_intel.api import deps
87
+
88
+ config = deps.get_config()
89
+
90
+ if config.is_service_mode:
91
+ await _handle_service_mode(project_id, branch, paths, files=files or {}, request_if_match=request_if_match)
92
+ else:
93
+ await _handle_local_mode(project_id, paths)
94
+
95
+
96
+ async def _handle_local_mode(project_id: str, paths: list[str]) -> None:
97
+ """Process file changes in local mode using existing CodeIntelService."""
98
+ from attocode_intel.api import deps
99
+
100
+ try:
101
+ svc = deps.get_service(project_id)
102
+ except ValueError:
103
+ logger.warning("Notify: project %s not found", project_id)
104
+ return
105
+
106
+ # Use existing notify_file_changed for each path
107
+ for path in paths:
108
+ try:
109
+ await _call_sync(svc.notify_file_changed, path)
110
+ except Exception as e:
111
+ logger.warning("Error notifying file change %s: %s", path, e)
112
+
113
+
114
+ async def _handle_service_mode(project_id: str, branch: str, paths: list[str], *, files: dict[str, str] | None = None, request_if_match: int | None = None) -> None:
115
+ """Process file changes in service mode using the incremental pipeline.
116
+
117
+ M2 note: In production, the caller (notify_file_changed route) already requires
118
+ auth via resolve_auth. The project_id should be validated against org scoping
119
+ in a future iteration when full multi-tenant isolation is implemented.
120
+ """
121
+ import base64
122
+
123
+ from sqlalchemy import select
124
+
125
+ from attocode_intel.db.engine import get_session
126
+ from attocode_intel.db.models import Branch, Repository
127
+ from attocode_intel.indexing.incremental import IncrementalPipeline
128
+ from attocode_intel.pubsub import publish_event
129
+
130
+ # Decode base64 file contents into bytes
131
+ file_contents: dict[str, bytes] | None = None
132
+ if files:
133
+ file_contents = {}
134
+ for path, b64_data in files.items():
135
+ try:
136
+ file_contents[path] = base64.b64decode(b64_data)
137
+ except Exception:
138
+ logger.warning("Failed to decode base64 content for %s", path)
139
+
140
+ async for session in get_session():
141
+ try:
142
+ # Resolve repository — validate it exists
143
+ repo_result = await session.execute(
144
+ select(Repository).where(Repository.id == uuid.UUID(project_id))
145
+ )
146
+ repo = repo_result.scalar_one_or_none()
147
+ if repo is None:
148
+ logger.warning("Notify: repository %s not found", project_id)
149
+ return
150
+
151
+ branch_result = await session.execute(
152
+ select(Branch).where(
153
+ Branch.repo_id == repo.id,
154
+ Branch.name == branch,
155
+ )
156
+ )
157
+ branch_obj = branch_result.scalar_one_or_none()
158
+ if branch_obj is None:
159
+ # Auto-create branch entry, parented to default
160
+ parent_result = await session.execute(
161
+ select(Branch).where(
162
+ Branch.repo_id == repo.id,
163
+ Branch.is_default == True, # noqa: E712
164
+ )
165
+ )
166
+ parent_branch = parent_result.scalar_one_or_none()
167
+
168
+ # If no default branch exists, this becomes the default
169
+ is_first_branch = parent_branch is None
170
+
171
+ branch_obj = Branch(
172
+ repo_id=repo.id,
173
+ name=branch,
174
+ parent_branch_id=parent_branch.id if parent_branch else None,
175
+ is_default=is_first_branch,
176
+ )
177
+ session.add(branch_obj)
178
+
179
+ # Update repo.default_branch to match the actual data branch
180
+ if is_first_branch:
181
+ repo.default_branch = branch
182
+
183
+ await session.flush()
184
+
185
+ # Advisory lock: prevent concurrent indexing of the same branch
186
+ await IncrementalPipeline.acquire_branch_lock(session, branch_obj.id)
187
+
188
+ # If-Match optimistic concurrency check
189
+ if request_if_match is not None:
190
+ from attocode_intel.storage.branch_overlay import BranchOverlay
191
+
192
+ overlay = BranchOverlay(session)
193
+ try:
194
+ await overlay.check_version(branch_obj.id, request_if_match)
195
+ except ValueError:
196
+ current_version = await overlay.get_version(branch_obj.id)
197
+ raise Exception(f"VERSION_MISMATCH:{current_version}")
198
+
199
+ # Run incremental pipeline
200
+ pipeline = IncrementalPipeline(session)
201
+ base_dir = repo.clone_path or repo.local_path
202
+ stats = await pipeline.process_file_changes(
203
+ branch_id=branch_obj.id,
204
+ paths=paths,
205
+ base_dir=base_dir,
206
+ file_contents=file_contents,
207
+ )
208
+
209
+ # Transition repo to "indexed" on first successful incremental update
210
+ if stats["processed"] > 0:
211
+ repo.index_status = "indexed"
212
+ repo.last_indexed_at = datetime.now(timezone.utc)
213
+
214
+ await session.commit()
215
+
216
+ # Reliably trigger embedding generation after batch completes
217
+ if stats["processed"] > 0:
218
+ from attocode_intel.workers.job_utils import enqueue_embedding_job
219
+ await enqueue_embedding_job(project_id, branch)
220
+
221
+ # Publish event
222
+ await publish_event(
223
+ str(repo.id),
224
+ "index.updated",
225
+ {"paths": paths, "branch": branch, "stats": stats},
226
+ )
227
+
228
+ # Log audit event
229
+ from attocode_intel.audit import log_event
230
+
231
+ await log_event(session, repo.org_id, "commit.notified", repo_id=repo.id, detail={"paths_count": len(paths), "branch": branch})
232
+
233
+ logger.info(
234
+ "Incremental update for %s/%s: %d processed, %d unchanged",
235
+ project_id, branch, stats["processed"], stats["skipped_unchanged"],
236
+ )
237
+
238
+ except Exception as exc:
239
+ error_msg = str(exc)
240
+ if error_msg.startswith("VERSION_MISMATCH:"):
241
+ current_version = int(error_msg.split(":")[1])
242
+ logger.info("Version mismatch for %s/%s: current=%d", project_id, branch, current_version)
243
+ await session.rollback()
244
+ return # Caller should check version
245
+ logger.exception("Error in service mode notify handler")
246
+ await session.rollback()
247
+
248
+
249
+ async def _call_sync(func, *args):
250
+ """Call a sync function from async context."""
251
+ import asyncio
252
+ loop = asyncio.get_running_loop()
253
+ return await loop.run_in_executor(None, func, *args)
254
+
255
+
256
+ # --- Routes ---
257
+
258
+
259
+ @router.post(
260
+ "/api/v1/notify/file-changed",
261
+ response_model=FileChangedResponse,
262
+ status_code=202,
263
+ )
264
+ async def notify_file_changed(
265
+ request: FileChangedRequest,
266
+ auth: dict = Depends(resolve_auth),
267
+ ) -> FileChangedResponse:
268
+ """Notify that files have changed. Returns 202 immediately.
269
+
270
+ The actual processing happens asynchronously after debouncing.
271
+ Multiple rapid notifications are batched into a single update.
272
+ """
273
+ if not request.paths:
274
+ raise HTTPException(status_code=422, detail="No paths provided")
275
+
276
+ # Idempotency check
277
+ if request.idempotency_key:
278
+ import time
279
+
280
+ now = time.monotonic()
281
+ # Clean expired entries
282
+ expired = [k for k, t in _idempotency_cache.items() if now - t > _IDEMPOTENCY_TTL]
283
+ for k in expired:
284
+ _idempotency_cache.pop(k, None)
285
+ # Check for duplicate
286
+ if request.idempotency_key in _idempotency_cache:
287
+ return FileChangedResponse(accepted=0, message="Duplicate (idempotency key already processed)")
288
+ _idempotency_cache[request.idempotency_key] = now
289
+
290
+ from attocode_intel.api import deps
291
+
292
+ config = deps.get_config()
293
+
294
+ # Resolve project ID
295
+ project_id = request.project or deps.get_default_project_id()
296
+
297
+ # Resolve branch (auto-detect from git if not provided)
298
+ branch = request.branch
299
+ if not branch:
300
+ branch = await _detect_current_branch(config.project_dir) if config.project_dir else "main"
301
+
302
+ debouncer = _get_debouncer()
303
+ await debouncer.notify(project_id, branch, request.paths, files=request.files)
304
+
305
+ return FileChangedResponse(accepted=len(request.paths))
306
+
307
+
308
+ @router.post(
309
+ "/api/v1/notify/flush",
310
+ response_model=FileChangedResponse,
311
+ status_code=200,
312
+ )
313
+ async def flush_pending(
314
+ request: FlushRequest,
315
+ auth: dict = Depends(resolve_auth),
316
+ ) -> FileChangedResponse:
317
+ """Force-flush any pending debounced notifications.
318
+
319
+ Useful for testing or when you need immediate consistency.
320
+ """
321
+ from attocode_intel.api import deps
322
+
323
+ project_id = request.project or deps.get_default_project_id()
324
+ branch = request.branch
325
+
326
+ debouncer = _get_debouncer()
327
+ await debouncer.flush(project_id, branch)
328
+
329
+ return FileChangedResponse(accepted=0, message="Flushed")
330
+
331
+
332
+ class CommitPushRequest(BaseModel):
333
+ commits: list[dict] = Field(..., description="List of commit dicts: {oid, message, author_name, author_email, timestamp, parent_oids}")
334
+ project: str = Field(..., description="Repository ID")
335
+ branch: str = Field("main", description="Branch name")
336
+
337
+
338
+ class CommitPushResponse(BaseModel):
339
+ stored: int = 0
340
+ skipped: int = 0
341
+
342
+
343
+ @router.post(
344
+ "/api/v1/notify/commits",
345
+ response_model=CommitPushResponse,
346
+ status_code=200,
347
+ )
348
+ async def push_commits(
349
+ request: CommitPushRequest,
350
+ auth: dict = Depends(resolve_auth),
351
+ ) -> CommitPushResponse:
352
+ """Push commit metadata for DB-backed commit history (remote repos)."""
353
+ from sqlalchemy import select
354
+ from sqlalchemy.dialects.postgresql import insert as pg_insert
355
+
356
+ from attocode_intel.db.engine import get_session
357
+ from attocode_intel.db.models import Commit, Repository
358
+
359
+ if not request.commits:
360
+ return CommitPushResponse()
361
+
362
+ stored = 0
363
+ skipped = 0
364
+
365
+ async for session in get_session():
366
+ try:
367
+ repo_result = await session.execute(
368
+ select(Repository).where(Repository.id == uuid.UUID(request.project))
369
+ )
370
+ repo = repo_result.scalar_one_or_none()
371
+ if repo is None:
372
+ logger.warning("push_commits: repository %s not found", request.project)
373
+ return CommitPushResponse()
374
+
375
+ for c in request.commits:
376
+ try:
377
+ values = {
378
+ "repo_id": repo.id,
379
+ "oid": c["oid"],
380
+ "message": c.get("message", ""),
381
+ "author_name": c.get("author_name", ""),
382
+ "author_email": c.get("author_email", ""),
383
+ "timestamp": int(c.get("timestamp", 0)),
384
+ "parent_oids": c.get("parent_oids", []),
385
+ "branch_name": request.branch,
386
+ }
387
+ if c.get("changed_files"):
388
+ values["changed_files"] = c["changed_files"]
389
+ stmt = pg_insert(Commit.__table__).values(
390
+ **values,
391
+ ).on_conflict_do_nothing(
392
+ constraint="uq_commits_repo_oid",
393
+ )
394
+ result = await session.execute(stmt)
395
+ if result.rowcount:
396
+ stored += 1
397
+ else:
398
+ skipped += 1
399
+ except Exception as e:
400
+ logger.warning("Error storing commit %s: %s", c.get("oid", "?"), e)
401
+ skipped += 1
402
+
403
+ await session.commit()
404
+ except Exception:
405
+ logger.exception("Error in push_commits handler")
406
+ await session.rollback()
407
+
408
+ return CommitPushResponse(stored=stored, skipped=skipped)
409
+
410
+
411
+ class BulkSyncRequest(BaseModel):
412
+ files: dict[str, str] = Field(..., description="Map of path → base64-encoded content (up to 500 files)")
413
+ project: str = Field(..., description="Repository ID")
414
+ branch: str = Field("main", description="Branch name")
415
+
416
+
417
+ class BulkSyncResponse(BaseModel):
418
+ processed: int = 0
419
+ skipped_unchanged: int = 0
420
+
421
+
422
+ @router.post(
423
+ "/api/v1/notify/bulk-sync",
424
+ response_model=BulkSyncResponse,
425
+ status_code=200,
426
+ )
427
+ async def bulk_sync(
428
+ request: BulkSyncRequest,
429
+ auth: dict = Depends(resolve_auth),
430
+ ) -> BulkSyncResponse:
431
+ """Bulk sync files — bypasses debouncer for fast initial sync.
432
+
433
+ Accepts up to 500 files per request. Direct pipeline call in single transaction.
434
+ """
435
+ import base64
436
+
437
+ from sqlalchemy import select
438
+
439
+ from attocode_intel.db.engine import get_session
440
+ from attocode_intel.db.models import Branch, Repository
441
+ from attocode_intel.indexing.incremental import IncrementalPipeline
442
+
443
+ if not request.files:
444
+ return BulkSyncResponse()
445
+
446
+ if len(request.files) > 500:
447
+ raise HTTPException(status_code=422, detail="Maximum 500 files per bulk-sync request")
448
+
449
+ async for session in get_session():
450
+ try:
451
+ repo_result = await session.execute(
452
+ select(Repository).where(Repository.id == uuid.UUID(request.project))
453
+ )
454
+ repo = repo_result.scalar_one_or_none()
455
+ if repo is None:
456
+ raise HTTPException(status_code=404, detail="Repository not found")
457
+
458
+ branch_result = await session.execute(
459
+ select(Branch).where(
460
+ Branch.repo_id == repo.id,
461
+ Branch.name == request.branch,
462
+ )
463
+ )
464
+ branch_obj = branch_result.scalar_one_or_none()
465
+ if branch_obj is None:
466
+ # Auto-create branch
467
+ parent_result = await session.execute(
468
+ select(Branch).where(
469
+ Branch.repo_id == repo.id,
470
+ Branch.is_default == True, # noqa: E712
471
+ )
472
+ )
473
+ parent_branch = parent_result.scalar_one_or_none()
474
+ is_first = parent_branch is None
475
+ branch_obj = Branch(
476
+ repo_id=repo.id,
477
+ name=request.branch,
478
+ parent_branch_id=parent_branch.id if parent_branch else None,
479
+ is_default=is_first,
480
+ )
481
+ session.add(branch_obj)
482
+ if is_first:
483
+ repo.default_branch = request.branch
484
+ await session.flush()
485
+
486
+ # Advisory lock
487
+ await IncrementalPipeline.acquire_branch_lock(session, branch_obj.id)
488
+
489
+ # Decode file contents
490
+ file_contents: dict[str, bytes] = {}
491
+ for path, b64_data in request.files.items():
492
+ try:
493
+ file_contents[path] = base64.b64decode(b64_data)
494
+ except Exception:
495
+ logger.warning("Failed to decode base64 content for %s", path)
496
+
497
+ # Run pipeline directly (no debounce)
498
+ pipeline = IncrementalPipeline(session)
499
+ base_dir = repo.clone_path or repo.local_path
500
+ stats = await pipeline.process_file_changes(
501
+ branch_id=branch_obj.id,
502
+ paths=list(file_contents.keys()),
503
+ base_dir=base_dir,
504
+ file_contents=file_contents,
505
+ )
506
+
507
+ if stats["processed"] > 0:
508
+ repo.index_status = "indexed"
509
+ repo.last_indexed_at = datetime.now(timezone.utc)
510
+
511
+ await session.commit()
512
+
513
+ # Trigger embeddings
514
+ if stats["processed"] > 0:
515
+ from attocode_intel.workers.job_utils import enqueue_embedding_job
516
+ await enqueue_embedding_job(request.project, request.branch)
517
+
518
+ return BulkSyncResponse(
519
+ processed=stats["processed"],
520
+ skipped_unchanged=stats["skipped_unchanged"],
521
+ )
522
+
523
+ except HTTPException:
524
+ raise
525
+ except Exception:
526
+ logger.exception("Error in bulk sync handler")
527
+ await session.rollback()
528
+ raise HTTPException(status_code=500, detail="Internal error during bulk sync")
529
+
530
+ raise HTTPException(status_code=503, detail="No database session available")
531
+
532
+
533
+ class BlamePushRequest(BaseModel):
534
+ hunks: list[dict] = Field(..., description="List of blame hunk dicts")
535
+ project: str = Field(..., description="Repository ID")
536
+ branch: str = Field("main", description="Branch name")
537
+ path: str = Field(..., description="File path for blame data")
538
+
539
+
540
+ class BlamePushResponse(BaseModel):
541
+ stored: int = 0
542
+
543
+
544
+ @router.post(
545
+ "/api/v1/notify/blame",
546
+ response_model=BlamePushResponse,
547
+ status_code=200,
548
+ )
549
+ async def push_blame(
550
+ request: BlamePushRequest,
551
+ auth: dict = Depends(resolve_auth),
552
+ ) -> BlamePushResponse:
553
+ """Push blame data for DB-backed blame (remote repos)."""
554
+ from sqlalchemy import select
555
+
556
+ from attocode_intel.db.engine import get_session
557
+ from attocode_intel.db.models import BlameHunk, Repository
558
+
559
+ if not request.hunks:
560
+ return BlamePushResponse()
561
+
562
+ stored = 0
563
+
564
+ async for session in get_session():
565
+ try:
566
+ repo_result = await session.execute(
567
+ select(Repository).where(Repository.id == uuid.UUID(request.project))
568
+ )
569
+ repo = repo_result.scalar_one_or_none()
570
+ if repo is None:
571
+ return BlamePushResponse()
572
+
573
+ # Delete existing blame for this file/branch (replace)
574
+ from sqlalchemy import delete
575
+
576
+ await session.execute(
577
+ delete(BlameHunk).where(
578
+ BlameHunk.repo_id == repo.id,
579
+ BlameHunk.path == request.path,
580
+ BlameHunk.branch_name == request.branch,
581
+ )
582
+ )
583
+
584
+ for h in request.hunks:
585
+ hunk = BlameHunk(
586
+ repo_id=repo.id,
587
+ path=request.path,
588
+ branch_name=request.branch,
589
+ commit_oid=h.get("commit_oid", ""),
590
+ author_name=h.get("author_name", ""),
591
+ author_email=h.get("author_email", ""),
592
+ timestamp=int(h.get("timestamp", 0)),
593
+ start_line=int(h.get("start_line", 0)),
594
+ end_line=int(h.get("end_line", 0)),
595
+ )
596
+ session.add(hunk)
597
+ stored += 1
598
+
599
+ await session.commit()
600
+ except Exception:
601
+ logger.exception("Error in push_blame handler")
602
+ await session.rollback()
603
+
604
+ return BlamePushResponse(stored=stored)
605
+
606
+
607
+ def _detect_current_branch(project_dir: str) -> str:
608
+ """Detect the current git branch from the project directory."""
609
+ import subprocess
610
+
611
+ try:
612
+ result = subprocess.run(
613
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
614
+ cwd=project_dir,
615
+ capture_output=True,
616
+ text=True,
617
+ timeout=5,
618
+ )
619
+ if result.returncode == 0:
620
+ return result.stdout.strip()
621
+ except Exception:
622
+ pass
623
+ return "main"
@@ -0,0 +1,27 @@
1
+ """Structured operations used by both the dashboard and MCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException, Request
8
+ from jsonschema import ValidationError
9
+
10
+ from attocode_intel.api.auth import resolve_auth
11
+ from attocode_intel.remote import remote_identity
12
+
13
+ router = APIRouter(prefix="/api/v2/operations", tags=["intelligence"])
14
+
15
+
16
+ @router.post("/{name}")
17
+ async def execute_operation(
18
+ name: str, arguments: dict, request: Request, auth: Annotated[object, Depends(resolve_auth)]
19
+ ):
20
+ token = remote_identity.set(auth)
21
+ try:
22
+ result = await request.app.state.mcp_transport.gateway.execute(name, arguments)
23
+ return result.structuredContent
24
+ except (ValueError, ValidationError) as exc:
25
+ raise HTTPException(422, str(exc)) from exc
26
+ finally:
27
+ remote_identity.reset(token)