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,428 @@
1
+ """Authentication endpoints (service mode only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import secrets
6
+ import time
7
+ import uuid
8
+ from urllib.parse import urlencode
9
+
10
+ from fastapi import APIRouter, Depends, HTTPException, Query
11
+ from pydantic import BaseModel
12
+ from sqlalchemy import select
13
+ from sqlalchemy.ext.asyncio import AsyncSession
14
+ from starlette.responses import RedirectResponse
15
+
16
+ from attocode_intel.api.auth import resolve_auth
17
+ from attocode_intel.api.auth.context import AuthContext
18
+ from attocode_intel.api.auth.jwt import create_access_token, create_refresh_token, decode_token
19
+ from attocode_intel.api.auth.passwords import hash_password, verify_password
20
+ from attocode_intel.api.deps import get_config, get_db_session
21
+ from attocode_intel.db.models import OrgMembership, User
22
+
23
+ router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
24
+
25
+
26
+ # --- OAuth state store (single-instance, 10 min TTL) ---
27
+
28
+ _oauth_states: dict[str, float] = {}
29
+
30
+
31
+ def _store_state(state: str) -> None:
32
+ """Store an OAuth state token with the current timestamp."""
33
+ now = time.time()
34
+ expired = [k for k, v in _oauth_states.items() if now - v > 600]
35
+ for k in expired:
36
+ del _oauth_states[k]
37
+ _oauth_states[state] = now
38
+
39
+
40
+ def _validate_state(state: str) -> bool:
41
+ """Validate and consume an OAuth state token (single-use)."""
42
+ ts = _oauth_states.pop(state, None)
43
+ if ts is None:
44
+ return False
45
+ return time.time() - ts < 600
46
+
47
+
48
+ # --- Request/Response models ---
49
+
50
+
51
+ class RegisterRequest(BaseModel):
52
+ email: str
53
+ password: str
54
+ name: str = ""
55
+ registration_key: str = ""
56
+
57
+
58
+ class LoginRequest(BaseModel):
59
+ email: str
60
+ password: str
61
+
62
+
63
+ class TokenResponse(BaseModel):
64
+ access_token: str
65
+ refresh_token: str
66
+ token_type: str = "bearer"
67
+
68
+
69
+ class RefreshRequest(BaseModel):
70
+ refresh_token: str
71
+
72
+
73
+ class UserProfile(BaseModel):
74
+ id: str
75
+ email: str
76
+ name: str
77
+ avatar_url: str | None = None
78
+ auth_provider: str
79
+ orgs: list[dict]
80
+
81
+
82
+ # --- Endpoints ---
83
+
84
+
85
+ @router.post("/register", response_model=TokenResponse)
86
+ async def register(
87
+ req: RegisterRequest,
88
+ session: AsyncSession = Depends(get_db_session),
89
+ ) -> TokenResponse:
90
+ """Register a new user with email + password."""
91
+ config = get_config()
92
+ if config.registration_key and req.registration_key != config.registration_key:
93
+ raise HTTPException(status_code=403, detail="Invalid registration key")
94
+
95
+ # Check if email already exists
96
+ existing = await session.execute(select(User).where(User.email == req.email))
97
+ if existing.scalar_one_or_none():
98
+ raise HTTPException(status_code=409, detail="Email already registered")
99
+
100
+ user = User(
101
+ email=req.email,
102
+ name=req.name or req.email.split("@")[0],
103
+ password_hash=hash_password(req.password),
104
+ auth_provider="email",
105
+ )
106
+ session.add(user)
107
+ await session.commit()
108
+ await session.refresh(user)
109
+
110
+ config = get_config()
111
+ return TokenResponse(
112
+ access_token=create_access_token(user.id, expires_minutes=config.jwt_expiry_minutes),
113
+ refresh_token=create_refresh_token(user.id, expires_days=config.refresh_expiry_days),
114
+ )
115
+
116
+
117
+ @router.post("/login", response_model=TokenResponse)
118
+ async def login(
119
+ req: LoginRequest,
120
+ session: AsyncSession = Depends(get_db_session),
121
+ ) -> TokenResponse:
122
+ """Login with email + password, returns JWT tokens."""
123
+ result = await session.execute(select(User).where(User.email == req.email))
124
+ user = result.scalar_one_or_none()
125
+ if user is None or not user.password_hash or not verify_password(req.password, user.password_hash):
126
+ raise HTTPException(status_code=401, detail="Invalid email or password")
127
+
128
+ config = get_config()
129
+ return TokenResponse(
130
+ access_token=create_access_token(user.id, expires_minutes=config.jwt_expiry_minutes),
131
+ refresh_token=create_refresh_token(user.id, expires_days=config.refresh_expiry_days),
132
+ )
133
+
134
+
135
+ @router.post("/refresh", response_model=TokenResponse)
136
+ async def refresh(
137
+ req: RefreshRequest,
138
+ session: AsyncSession = Depends(get_db_session),
139
+ ) -> TokenResponse:
140
+ """Exchange a refresh token for a new access token."""
141
+ payload = decode_token(req.refresh_token)
142
+ if payload is None or payload.get("type") != "refresh":
143
+ raise HTTPException(status_code=401, detail="Invalid refresh token")
144
+
145
+ user_id = uuid.UUID(payload["sub"])
146
+ result = await session.execute(select(User).where(User.id == user_id))
147
+ user = result.scalar_one_or_none()
148
+ if user is None:
149
+ raise HTTPException(status_code=401, detail="User not found")
150
+
151
+ config = get_config()
152
+ return TokenResponse(
153
+ access_token=create_access_token(user.id, expires_minutes=config.jwt_expiry_minutes),
154
+ refresh_token=create_refresh_token(user.id, expires_days=config.refresh_expiry_days),
155
+ )
156
+
157
+
158
+ @router.get("/github")
159
+ async def github_authorize() -> dict:
160
+ """Redirect URL for GitHub OAuth authorization."""
161
+ from attocode_intel.api.auth.oauth import get_github_auth_url
162
+
163
+ config = get_config()
164
+ if not config.github_client_id:
165
+ raise HTTPException(status_code=501, detail="GitHub OAuth not configured")
166
+
167
+ state = secrets.token_urlsafe(32)
168
+ _store_state(state)
169
+ url = await get_github_auth_url(
170
+ client_id=config.github_client_id,
171
+ redirect_uri=f"{config.effective_base_url}/api/v1/auth/github/callback",
172
+ state=state,
173
+ )
174
+ return {"authorize_url": url, "state": state}
175
+
176
+
177
+ @router.get("/github/callback")
178
+ async def github_callback(
179
+ code: str = Query(...),
180
+ state: str = Query(""),
181
+ session: AsyncSession = Depends(get_db_session),
182
+ ) -> RedirectResponse:
183
+ """Handle GitHub OAuth callback — create/link user, redirect with JWT."""
184
+ if not _validate_state(state):
185
+ raise HTTPException(status_code=400, detail="Invalid or expired OAuth state")
186
+
187
+ from attocode_intel.api.auth.oauth import exchange_github_code
188
+
189
+ config = get_config()
190
+ if not config.github_client_id or not config.github_client_secret:
191
+ raise HTTPException(status_code=501, detail="GitHub OAuth not configured")
192
+
193
+ gh_user = await exchange_github_code(
194
+ code=code,
195
+ client_id=config.github_client_id,
196
+ client_secret=config.github_client_secret,
197
+ )
198
+
199
+ # Try to find existing user by github_id
200
+ result = await session.execute(
201
+ select(User).where(User.github_id == gh_user["github_id"])
202
+ )
203
+ user = result.scalar_one_or_none()
204
+
205
+ if user is None:
206
+ # Try by email
207
+ result = await session.execute(
208
+ select(User).where(User.email == gh_user["email"])
209
+ )
210
+ user = result.scalar_one_or_none()
211
+ if user:
212
+ # Link GitHub to existing account
213
+ user.github_id = gh_user["github_id"]
214
+ user.avatar_url = user.avatar_url or gh_user["avatar_url"]
215
+ else:
216
+ # Create new user
217
+ user = User(
218
+ email=gh_user["email"],
219
+ name=gh_user["name"],
220
+ github_id=gh_user["github_id"],
221
+ avatar_url=gh_user["avatar_url"],
222
+ auth_provider="github",
223
+ )
224
+ session.add(user)
225
+
226
+ await session.commit()
227
+ await session.refresh(user)
228
+
229
+ access_token = create_access_token(user.id, expires_minutes=config.jwt_expiry_minutes)
230
+ refresh_token = create_refresh_token(user.id, expires_days=config.refresh_expiry_days)
231
+ fragment = urlencode({"access_token": access_token, "refresh_token": refresh_token})
232
+ return RedirectResponse(url=f"/auth/callback#{fragment}", status_code=302)
233
+
234
+
235
+ @router.get("/google")
236
+ async def google_authorize() -> dict:
237
+ """Redirect URL for Google OAuth authorization."""
238
+ from attocode_intel.api.auth.oauth import get_google_auth_url
239
+
240
+ config = get_config()
241
+ if not config.google_client_id:
242
+ raise HTTPException(status_code=501, detail="Google OAuth not configured")
243
+
244
+ state = secrets.token_urlsafe(32)
245
+ _store_state(state)
246
+ url = await get_google_auth_url(
247
+ client_id=config.google_client_id,
248
+ redirect_uri=f"{config.effective_base_url}/api/v1/auth/google/callback",
249
+ state=state,
250
+ )
251
+ return {"authorize_url": url, "state": state}
252
+
253
+
254
+ @router.get("/google/callback")
255
+ async def google_callback(
256
+ code: str = Query(...),
257
+ state: str = Query(""),
258
+ session: AsyncSession = Depends(get_db_session),
259
+ ) -> RedirectResponse:
260
+ """Handle Google OAuth callback — create/link user, redirect with JWT."""
261
+ if not _validate_state(state):
262
+ raise HTTPException(status_code=400, detail="Invalid or expired OAuth state")
263
+
264
+ from attocode_intel.api.auth.oauth import exchange_google_code
265
+
266
+ config = get_config()
267
+ if not config.google_client_id or not config.google_client_secret:
268
+ raise HTTPException(status_code=501, detail="Google OAuth not configured")
269
+
270
+ g_user = await exchange_google_code(
271
+ code=code,
272
+ client_id=config.google_client_id,
273
+ client_secret=config.google_client_secret,
274
+ redirect_uri=f"{config.effective_base_url}/api/v1/auth/google/callback",
275
+ )
276
+
277
+ # Lookup by google_id -> email -> create new
278
+ result = await session.execute(select(User).where(User.google_id == g_user["google_id"]))
279
+ user = result.scalar_one_or_none()
280
+
281
+ if user is None:
282
+ result = await session.execute(select(User).where(User.email == g_user["email"]))
283
+ user = result.scalar_one_or_none()
284
+ if user:
285
+ user.google_id = g_user["google_id"]
286
+ user.avatar_url = user.avatar_url or g_user["avatar_url"]
287
+ else:
288
+ user = User(
289
+ email=g_user["email"],
290
+ name=g_user["name"],
291
+ google_id=g_user["google_id"],
292
+ avatar_url=g_user["avatar_url"],
293
+ auth_provider="google",
294
+ )
295
+ session.add(user)
296
+
297
+ await session.commit()
298
+ await session.refresh(user)
299
+
300
+ access_token = create_access_token(user.id, expires_minutes=config.jwt_expiry_minutes)
301
+ refresh_token = create_refresh_token(user.id, expires_days=config.refresh_expiry_days)
302
+ fragment = urlencode({"access_token": access_token, "refresh_token": refresh_token})
303
+ return RedirectResponse(url=f"/auth/callback#{fragment}", status_code=302)
304
+
305
+
306
+ @router.get("/providers")
307
+ async def list_providers() -> dict:
308
+ """List available auth providers. No auth required — called from login page."""
309
+ config = get_config()
310
+ providers = ["email"]
311
+ if config.github_client_id:
312
+ providers.append("github")
313
+ if config.google_client_id:
314
+ providers.append("google")
315
+ return {
316
+ "providers": providers,
317
+ "registration_enabled": True,
318
+ "registration_key_required": bool(config.registration_key),
319
+ }
320
+
321
+
322
+ class UpdateProfileRequest(BaseModel):
323
+ name: str | None = None
324
+ avatar_url: str | None = None
325
+
326
+
327
+ class ChangePasswordRequest(BaseModel):
328
+ current_password: str
329
+ new_password: str
330
+
331
+
332
+ @router.patch("/me", response_model=UserProfile)
333
+ async def update_profile(
334
+ req: UpdateProfileRequest,
335
+ auth: AuthContext = Depends(resolve_auth),
336
+ session: AsyncSession = Depends(get_db_session),
337
+ ) -> UserProfile:
338
+ """Update current user's profile (name, avatar_url)."""
339
+ if not auth.user_id:
340
+ raise HTTPException(status_code=401, detail="Authentication required")
341
+
342
+ result = await session.execute(select(User).where(User.id == auth.user_id))
343
+ user = result.scalar_one_or_none()
344
+ if user is None:
345
+ raise HTTPException(status_code=404, detail="User not found")
346
+
347
+ if req.name is not None:
348
+ user.name = req.name
349
+ if req.avatar_url is not None:
350
+ user.avatar_url = req.avatar_url
351
+
352
+ await session.commit()
353
+ await session.refresh(user)
354
+
355
+ memberships = await session.execute(
356
+ select(OrgMembership).where(OrgMembership.user_id == user.id)
357
+ )
358
+ orgs = [{"org_id": str(m.org_id), "role": m.role} for m in memberships.scalars()]
359
+
360
+ return UserProfile(
361
+ id=str(user.id),
362
+ email=user.email,
363
+ name=user.name,
364
+ avatar_url=user.avatar_url,
365
+ auth_provider=user.auth_provider,
366
+ orgs=orgs,
367
+ )
368
+
369
+
370
+ @router.post("/me/password")
371
+ async def change_password(
372
+ req: ChangePasswordRequest,
373
+ auth: AuthContext = Depends(resolve_auth),
374
+ session: AsyncSession = Depends(get_db_session),
375
+ ) -> dict:
376
+ """Change password (requires current password)."""
377
+ if not auth.user_id:
378
+ raise HTTPException(status_code=401, detail="Authentication required")
379
+
380
+ result = await session.execute(select(User).where(User.id == auth.user_id))
381
+ user = result.scalar_one_or_none()
382
+ if user is None:
383
+ raise HTTPException(status_code=404, detail="User not found")
384
+
385
+ if not user.password_hash:
386
+ raise HTTPException(status_code=400, detail="Account uses OAuth — no password to change")
387
+
388
+ if not verify_password(req.current_password, user.password_hash):
389
+ raise HTTPException(status_code=401, detail="Current password is incorrect")
390
+
391
+ if len(req.new_password) < 8:
392
+ raise HTTPException(status_code=400, detail="New password must be at least 8 characters")
393
+
394
+ user.password_hash = hash_password(req.new_password)
395
+ await session.commit()
396
+ return {"detail": "Password changed"}
397
+
398
+
399
+ @router.get("/me", response_model=UserProfile)
400
+ async def me(
401
+ auth: AuthContext = Depends(resolve_auth),
402
+ session: AsyncSession = Depends(get_db_session),
403
+ ) -> UserProfile:
404
+ """Get current user profile with org memberships."""
405
+ if not auth.user_id:
406
+ raise HTTPException(status_code=401, detail="Not authenticated")
407
+
408
+ result = await session.execute(select(User).where(User.id == auth.user_id))
409
+ user = result.scalar_one_or_none()
410
+ if user is None:
411
+ raise HTTPException(status_code=404, detail="User not found")
412
+
413
+ # Get org memberships
414
+ memberships = await session.execute(
415
+ select(OrgMembership).where(OrgMembership.user_id == user.id)
416
+ )
417
+ orgs = []
418
+ for m in memberships.scalars():
419
+ orgs.append({"org_id": str(m.org_id), "role": m.role})
420
+
421
+ return UserProfile(
422
+ id=str(user.id),
423
+ email=user.email,
424
+ name=user.name,
425
+ avatar_url=user.avatar_url,
426
+ auth_provider=user.auth_provider,
427
+ orgs=orgs,
428
+ )
@@ -0,0 +1,262 @@
1
+ """Branch management and comparison endpoints (service mode only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException, Query
8
+ from pydantic import BaseModel
9
+ from sqlalchemy import func, select
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from attocode_intel.api.auth import resolve_auth
13
+ from attocode_intel.api.auth.context import AuthContext
14
+ from attocode_intel.api.deps import get_db_session
15
+ from attocode_intel.db.models import Branch, Repository
16
+
17
+ router = APIRouter(prefix="/api/v1/repos/{repo_id}/branches", tags=["branches"])
18
+
19
+
20
+ class BranchResponse(BaseModel):
21
+ id: str
22
+ name: str
23
+ head_commit: str | None = None
24
+ is_default: bool
25
+ last_indexed_at: str | None = None
26
+ overlay_stats: dict = {}
27
+
28
+
29
+ class BranchListResponse(BaseModel):
30
+ branches: list[BranchResponse]
31
+ total: int = 0
32
+ limit: int = 20
33
+ offset: int = 0
34
+ has_more: bool = False
35
+
36
+
37
+ class BranchCompareRequest(BaseModel):
38
+ base_branch: str
39
+ compare_branch: str
40
+
41
+
42
+ class BranchCompareResponse(BaseModel):
43
+ base: str
44
+ compare: str
45
+ files_changed: dict # path → change_type
46
+
47
+
48
+ async def _get_repo(repo_id: uuid.UUID, session: AsyncSession) -> Repository:
49
+ result = await session.execute(select(Repository).where(Repository.id == repo_id))
50
+ repo = result.scalar_one_or_none()
51
+ if repo is None:
52
+ raise HTTPException(status_code=404, detail="Repository not found")
53
+ return repo
54
+
55
+
56
+ @router.get("", response_model=BranchListResponse)
57
+ async def list_branches(
58
+ repo_id: uuid.UUID,
59
+ limit: int = Query(20, ge=1, le=100),
60
+ offset: int = Query(0, ge=0),
61
+ auth: AuthContext = Depends(resolve_auth),
62
+ session: AsyncSession = Depends(get_db_session),
63
+ ) -> BranchListResponse:
64
+ """List branches with overlay stats."""
65
+ await _get_repo(repo_id, session)
66
+
67
+ from attocode_intel.storage.branch_overlay import BranchOverlay
68
+
69
+ overlay = BranchOverlay(session)
70
+
71
+ count_result = await session.execute(
72
+ select(func.count()).select_from(
73
+ select(Branch).where(Branch.repo_id == repo_id).subquery()
74
+ )
75
+ )
76
+ total = count_result.scalar() or 0
77
+
78
+ result = await session.execute(
79
+ select(Branch).where(Branch.repo_id == repo_id)
80
+ .offset(offset).limit(limit)
81
+ )
82
+ branches = []
83
+ for b in result.scalars():
84
+ stats = await overlay.get_overlay_stats(b.id)
85
+ branches.append(BranchResponse(
86
+ id=str(b.id),
87
+ name=b.name,
88
+ head_commit=b.head_commit,
89
+ is_default=b.is_default,
90
+ last_indexed_at=b.last_indexed_at.isoformat() if b.last_indexed_at else None,
91
+ overlay_stats=stats,
92
+ ))
93
+ return BranchListResponse(
94
+ branches=branches, total=total, limit=limit, offset=offset,
95
+ has_more=(offset + limit < total),
96
+ )
97
+
98
+
99
+ @router.get("/{branch_name}", response_model=BranchResponse)
100
+ async def get_branch(
101
+ repo_id: uuid.UUID,
102
+ branch_name: str,
103
+ auth: AuthContext = Depends(resolve_auth),
104
+ session: AsyncSession = Depends(get_db_session),
105
+ ) -> BranchResponse:
106
+ """Get branch detail."""
107
+ await _get_repo(repo_id, session)
108
+
109
+ result = await session.execute(
110
+ select(Branch).where(Branch.repo_id == repo_id, Branch.name == branch_name)
111
+ )
112
+ branch = result.scalar_one_or_none()
113
+ if branch is None:
114
+ raise HTTPException(status_code=404, detail=f"Branch '{branch_name}' not found")
115
+
116
+ from attocode_intel.storage.branch_overlay import BranchOverlay
117
+
118
+ overlay = BranchOverlay(session)
119
+ stats = await overlay.get_overlay_stats(branch.id)
120
+
121
+ return BranchResponse(
122
+ id=str(branch.id),
123
+ name=branch.name,
124
+ head_commit=branch.head_commit,
125
+ is_default=branch.is_default,
126
+ last_indexed_at=branch.last_indexed_at.isoformat() if branch.last_indexed_at else None,
127
+ overlay_stats=stats,
128
+ )
129
+
130
+
131
+ @router.post("/compare", response_model=BranchCompareResponse)
132
+ async def compare_branches(
133
+ repo_id: uuid.UUID,
134
+ req: BranchCompareRequest,
135
+ auth: AuthContext = Depends(resolve_auth),
136
+ session: AsyncSession = Depends(get_db_session),
137
+ ) -> BranchCompareResponse:
138
+ """Compare two branches — returns file-level diff."""
139
+ await _get_repo(repo_id, session)
140
+
141
+ base_result = await session.execute(
142
+ select(Branch).where(Branch.repo_id == repo_id, Branch.name == req.base_branch)
143
+ )
144
+ base = base_result.scalar_one_or_none()
145
+ if base is None:
146
+ raise HTTPException(status_code=404, detail=f"Branch '{req.base_branch}' not found")
147
+
148
+ compare_result = await session.execute(
149
+ select(Branch).where(Branch.repo_id == repo_id, Branch.name == req.compare_branch)
150
+ )
151
+ compare = compare_result.scalar_one_or_none()
152
+ if compare is None:
153
+ raise HTTPException(status_code=404, detail=f"Branch '{req.compare_branch}' not found")
154
+
155
+ from attocode_intel.storage.branch_overlay import BranchOverlay
156
+
157
+ overlay = BranchOverlay(session)
158
+ diff = await overlay.diff_branches(base.id, compare.id)
159
+
160
+ return BranchCompareResponse(
161
+ base=req.base_branch,
162
+ compare=req.compare_branch,
163
+ files_changed=diff,
164
+ )
165
+
166
+
167
+ class BranchMergeRequest(BaseModel):
168
+ source_branch: str
169
+ target_branch: str
170
+ delete_source: bool = True
171
+
172
+
173
+ class BranchMergeResponse(BaseModel):
174
+ source: str
175
+ target: str
176
+ added: int = 0
177
+ modified: int = 0
178
+ deleted: int = 0
179
+ total: int = 0
180
+
181
+
182
+ @router.post("/merge", response_model=BranchMergeResponse)
183
+ async def merge_branches(
184
+ repo_id: uuid.UUID,
185
+ req: BranchMergeRequest,
186
+ auth: AuthContext = Depends(resolve_auth),
187
+ session: AsyncSession = Depends(get_db_session),
188
+ ) -> BranchMergeResponse:
189
+ """Merge source branch overlay into target branch."""
190
+ await _get_repo(repo_id, session)
191
+
192
+ source_result = await session.execute(
193
+ select(Branch).where(Branch.repo_id == repo_id, Branch.name == req.source_branch)
194
+ )
195
+ source = source_result.scalar_one_or_none()
196
+ if source is None:
197
+ raise HTTPException(status_code=404, detail=f"Branch '{req.source_branch}' not found")
198
+
199
+ target_result = await session.execute(
200
+ select(Branch).where(Branch.repo_id == repo_id, Branch.name == req.target_branch)
201
+ )
202
+ target = target_result.scalar_one_or_none()
203
+ if target is None:
204
+ raise HTTPException(status_code=404, detail=f"Branch '{req.target_branch}' not found")
205
+
206
+ if source.is_default:
207
+ raise HTTPException(status_code=400, detail="Cannot merge the default branch as source")
208
+
209
+ from attocode_intel.storage.branch_overlay import BranchOverlay
210
+
211
+ overlay = BranchOverlay(session)
212
+ stats = await overlay.merge_branch(
213
+ source.id, target.id, delete_source=req.delete_source,
214
+ )
215
+
216
+ from attocode_intel.audit import log_event
217
+
218
+ repo = await _get_repo(repo_id, session)
219
+ await log_event(
220
+ session, repo.org_id, "branch.merged", repo_id=repo_id,
221
+ detail={"source": req.source_branch, "target": req.target_branch, "stats": stats},
222
+ )
223
+
224
+ await session.commit()
225
+
226
+ return BranchMergeResponse(
227
+ source=req.source_branch,
228
+ target=req.target_branch,
229
+ **stats,
230
+ )
231
+
232
+
233
+ @router.delete("/{branch_name}")
234
+ async def delete_branch(
235
+ repo_id: uuid.UUID,
236
+ branch_name: str,
237
+ auth: AuthContext = Depends(resolve_auth),
238
+ session: AsyncSession = Depends(get_db_session),
239
+ ) -> dict:
240
+ """Delete a branch overlay (not the git branch)."""
241
+ await _get_repo(repo_id, session)
242
+
243
+ result = await session.execute(
244
+ select(Branch).where(Branch.repo_id == repo_id, Branch.name == branch_name)
245
+ )
246
+ branch = result.scalar_one_or_none()
247
+ if branch is None:
248
+ raise HTTPException(status_code=404, detail=f"Branch '{branch_name}' not found")
249
+
250
+ if branch.is_default:
251
+ raise HTTPException(status_code=400, detail="Cannot delete the default branch overlay")
252
+
253
+ repo = await _get_repo(repo_id, session)
254
+
255
+ await session.delete(branch)
256
+
257
+ from attocode_intel.audit import log_event
258
+
259
+ await log_event(session, repo.org_id, "branch.deleted", repo_id=repo_id, detail={"branch_name": branch_name})
260
+
261
+ await session.commit()
262
+ return {"detail": f"Branch overlay '{branch_name}' deleted"}