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,614 @@
1
+ """Repository snapshot endpoints — server-side point-in-time manifests.
2
+
3
+ Phase 3a ships CRUD + create over the ``repo_snapshots`` +
4
+ ``repo_snapshot_components`` tables. A snapshot is a content-addressed
5
+ record of everything it takes to reproduce a repo's code-intel state
6
+ *right now*: the branch manifest, the symbol + dependency + embedding
7
+ coverage, and aggregate stats.
8
+
9
+ The manifest hash is the SHA-256 of a canonical JSON document shaped
10
+ like an OCI v1.1 image manifest — so Phase 3b's OCI adapter can push it
11
+ to a registry without changing the database schema.
12
+
13
+ Limitations of Phase 3a:
14
+
15
+ - The snapshot describes but does NOT materialize a downloadable tarball
16
+ of the underlying content/symbol/embedding blobs. That's Phase 3b.
17
+ - Restore is also Phase 3b. Right now this endpoint set gives you
18
+ create / list / get / delete — the "durable record of state" half.
19
+
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import json
26
+ import logging
27
+ import uuid
28
+ from datetime import datetime
29
+
30
+ from fastapi import APIRouter, Depends, HTTPException, Query
31
+ from pydantic import BaseModel
32
+ from sqlalchemy import func, select
33
+ from sqlalchemy.ext.asyncio import AsyncSession
34
+
35
+ from attocode_intel.api.auth import resolve_auth
36
+ from attocode_intel.api.auth.context import AuthContext
37
+ from attocode_intel.api.deps import get_db_session
38
+ from attocode_intel.api.routes.orgs import _require_membership
39
+ from attocode_intel.api.utils import get_repo_by_org as _load_repo
40
+ from attocode_intel.db.models import (
41
+ Branch,
42
+ Dependency,
43
+ Embedding,
44
+ FileContent,
45
+ Repository,
46
+ RepoSnapshot,
47
+ RepoSnapshotComponent,
48
+ Symbol,
49
+ )
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+ router = APIRouter(prefix="/api/v1/orgs", tags=["snapshots"])
54
+
55
+
56
+ # Snapshot / component schema + media type strings match Phase 2a's
57
+ # ``snapshot_tools`` local format so a Phase 3b adapter can merge them.
58
+ SNAPSHOT_SCHEMA = "atto.snapshot.v1"
59
+ _MEDIA_CONTENT = "application/vnd.attocode.content-manifest.v1+json"
60
+ _MEDIA_SYMBOLS = "application/vnd.attocode.symbols-manifest.v1+json"
61
+ _MEDIA_DEPS = "application/vnd.attocode.deps-manifest.v1+json"
62
+ _MEDIA_EMBED = "application/vnd.attocode.embeddings-manifest.v1+json"
63
+
64
+
65
+ # --- Request / response models ---
66
+
67
+
68
+ class SnapshotCreateRequest(BaseModel):
69
+ name: str
70
+ description: str = ""
71
+ branch: str = "" # empty means "use repo default_branch"
72
+ commit_oid: str = "" # optional explicit commit pin
73
+ dry_run: bool = False
74
+
75
+
76
+ class SnapshotComponentModel(BaseModel):
77
+ name: str
78
+ media_type: str
79
+ digest: str
80
+ size_bytes: int
81
+ extra: dict = {}
82
+
83
+
84
+ class SnapshotResponse(BaseModel):
85
+ id: str
86
+ repo_id: str
87
+ org_id: str
88
+ branch_id: str | None = None
89
+ name: str
90
+ description: str
91
+ manifest_hash: str
92
+ total_bytes: int
93
+ component_count: int
94
+ commit_oid: str | None = None
95
+ created_at: str
96
+ components: list[SnapshotComponentModel] = []
97
+
98
+
99
+ class SnapshotListResponse(BaseModel):
100
+ snapshots: list[SnapshotResponse]
101
+ total: int = 0
102
+ limit: int = 20
103
+ offset: int = 0
104
+ has_more: bool = False
105
+
106
+
107
+ class SnapshotDryRunResponse(BaseModel):
108
+ repo_id: str
109
+ branch_id: str | None
110
+ manifest_hash: str
111
+ total_bytes: int
112
+ components: list[SnapshotComponentModel]
113
+ dry_run: bool = True
114
+
115
+
116
+ # --- Helpers ---
117
+
118
+
119
+ def _canonical_json_bytes(obj: object) -> bytes:
120
+ return json.dumps(
121
+ obj,
122
+ sort_keys=True,
123
+ ensure_ascii=True,
124
+ separators=(",", ":"),
125
+ default=str,
126
+ ).encode("ascii")
127
+
128
+
129
+ def _sha256_of(data: bytes) -> str:
130
+ return "sha256:" + hashlib.sha256(data).hexdigest()
131
+
132
+
133
+ async def _resolve_branch(
134
+ repo: Repository,
135
+ branch_name: str,
136
+ session: AsyncSession,
137
+ ) -> Branch | None:
138
+ """Return the Branch row matching ``branch_name``, or the repo default.
139
+
140
+ Returns None if the repo has no matching branch row — callers should
141
+ treat that as a snapshottable state with empty overlays.
142
+ """
143
+ target = branch_name or repo.default_branch or "main"
144
+ result = await session.execute(
145
+ select(Branch).where(
146
+ Branch.repo_id == repo.id, Branch.name == target,
147
+ )
148
+ )
149
+ return result.scalar_one_or_none()
150
+
151
+
152
+ async def _compute_components(
153
+ session: AsyncSession,
154
+ branch: Branch | None,
155
+ ) -> tuple[list[SnapshotComponentModel], int]:
156
+ """Compute the content-addressed component list for a snapshot.
157
+
158
+ Returns ``(components, total_bytes)``.
159
+
160
+ For each component, digest is the SHA-256 of a canonical JSON
161
+ document describing the component contents at this moment. The
162
+ component bodies themselves are NOT embedded in the response — they
163
+ live in the existing storage tables and can be reassembled from the
164
+ digests + the content_store / symbol / dependency / embedding tables
165
+ on read.
166
+ """
167
+ components: list[SnapshotComponentModel] = []
168
+ total_bytes = 0
169
+
170
+ # --- Content component ---
171
+ #
172
+ # Every path in the branch overlay manifest mapped to its content
173
+ # SHA. This is the smallest possible representation of "what files
174
+ # are in the snapshot and what's in each one". The digest changes
175
+ # if any path moves, is added, or any content SHA changes.
176
+ content_shas: list[str] = []
177
+ if branch is not None:
178
+ from attocode_intel.storage.branch_overlay import BranchOverlay
179
+ overlay = BranchOverlay(session)
180
+ manifest = await overlay.resolve_manifest(branch.id)
181
+ content_shas = sorted(set(manifest.values()))
182
+ # Sum sizes from file_contents — lets the caller plan disk usage.
183
+ if content_shas:
184
+ result = await session.execute(
185
+ select(func.coalesce(func.sum(FileContent.size_bytes), 0))
186
+ .where(FileContent.sha256.in_(content_shas))
187
+ )
188
+ content_bytes = int(result.scalar_one() or 0)
189
+ total_bytes += content_bytes
190
+ content_body = {
191
+ "manifest": manifest,
192
+ "content_shas": content_shas,
193
+ "total_bytes": content_bytes,
194
+ }
195
+ else:
196
+ content_bytes = 0
197
+ content_body = {"manifest": {}, "content_shas": [], "total_bytes": 0}
198
+ components.append(SnapshotComponentModel(
199
+ name="content",
200
+ media_type=_MEDIA_CONTENT,
201
+ digest=_sha256_of(_canonical_json_bytes(content_body)),
202
+ size_bytes=content_bytes,
203
+ extra={"file_count": len(manifest), "sha_count": len(content_shas)},
204
+ ))
205
+ else:
206
+ components.append(SnapshotComponentModel(
207
+ name="content",
208
+ media_type=_MEDIA_CONTENT,
209
+ digest=_sha256_of(b"{}"),
210
+ size_bytes=0,
211
+ extra={"file_count": 0, "sha_count": 0},
212
+ ))
213
+
214
+ # --- Symbols component ---
215
+ #
216
+ # Convention: size_bytes is ALWAYS actual bytes. Row counts and
217
+ # other cardinalities go into ``extra``. Symbols / deps / embeddings
218
+ # components report ``size_bytes=0`` because there is no meaningful
219
+ # on-disk byte count for them yet (the OCI adapter will backfill
220
+ # these with real artifact sizes once it ships).
221
+ if content_shas:
222
+ sym_result = await session.execute(
223
+ select(Symbol.content_sha, func.count()).where(
224
+ Symbol.content_sha.in_(content_shas),
225
+ ).group_by(Symbol.content_sha)
226
+ )
227
+ symbol_counts = sorted(
228
+ (row[0], int(row[1])) for row in sym_result
229
+ )
230
+ symbol_total = sum(c for _, c in symbol_counts)
231
+ else:
232
+ symbol_counts = []
233
+ symbol_total = 0
234
+ components.append(SnapshotComponentModel(
235
+ name="symbols",
236
+ media_type=_MEDIA_SYMBOLS,
237
+ digest=_sha256_of(_canonical_json_bytes({"counts": symbol_counts})),
238
+ size_bytes=0,
239
+ extra={"row_count": symbol_total},
240
+ ))
241
+
242
+ # --- Dependencies component ---
243
+ if content_shas:
244
+ dep_result = await session.execute(
245
+ select(
246
+ Dependency.source_sha,
247
+ Dependency.target_sha,
248
+ Dependency.dep_type,
249
+ ).where(
250
+ Dependency.source_sha.in_(content_shas),
251
+ ).order_by(
252
+ Dependency.source_sha, Dependency.target_sha, Dependency.dep_type,
253
+ )
254
+ )
255
+ dep_rows = [(r[0], r[1], r[2]) for r in dep_result]
256
+ else:
257
+ dep_rows = []
258
+ components.append(SnapshotComponentModel(
259
+ name="dependencies",
260
+ media_type=_MEDIA_DEPS,
261
+ digest=_sha256_of(_canonical_json_bytes({"edges": dep_rows})),
262
+ size_bytes=0,
263
+ extra={"edge_count": len(dep_rows)},
264
+ ))
265
+
266
+ # --- Embeddings components ---
267
+ #
268
+ # Component name includes ``model_version`` so two versions of the
269
+ # same model (during a rotation) live under distinct names instead
270
+ # of colliding on ``embeddings.{model}``. Sort deterministically by
271
+ # (model, version) so DB iteration order can't leak into the
272
+ # manifest hash.
273
+ if content_shas:
274
+ emb_result = await session.execute(
275
+ select(
276
+ Embedding.embedding_model,
277
+ Embedding.embedding_model_version,
278
+ func.count(),
279
+ ).where(
280
+ Embedding.content_sha.in_(content_shas),
281
+ ).group_by(
282
+ Embedding.embedding_model, Embedding.embedding_model_version,
283
+ ).order_by(
284
+ Embedding.embedding_model, Embedding.embedding_model_version,
285
+ )
286
+ )
287
+ for model, version, count in emb_result:
288
+ version_tag = version or "unversioned"
289
+ components.append(SnapshotComponentModel(
290
+ name=f"embeddings.{model}:{version_tag}",
291
+ media_type=_MEDIA_EMBED,
292
+ digest=_sha256_of(_canonical_json_bytes({
293
+ "model": model,
294
+ "version": version,
295
+ "chunk_count": int(count),
296
+ })),
297
+ size_bytes=0,
298
+ extra={
299
+ "model": model,
300
+ "model_version": version or "",
301
+ "chunk_count": int(count),
302
+ },
303
+ ))
304
+
305
+ return components, total_bytes
306
+
307
+
308
+ def _compute_manifest_hash(
309
+ *,
310
+ repo_id: uuid.UUID,
311
+ branch_id: uuid.UUID | None,
312
+ commit_oid: str,
313
+ components: list[SnapshotComponentModel],
314
+ ) -> str:
315
+ """Top-level manifest hash.
316
+
317
+ Deliberately excludes transient metadata (name, description,
318
+ created_at, created_by) so two snapshots of identical state under
319
+ different names produce the same hash — required for OCI dedup.
320
+
321
+ Sorts by ``(name, digest)`` so identical-name components can't
322
+ reorder based on DB iteration. ``(name, digest)`` is stable even
323
+ when two components share the same logical name — distinct digests
324
+ still produce a deterministic ordering.
325
+ """
326
+ body = {
327
+ "schema": SNAPSHOT_SCHEMA,
328
+ "repo_id": str(repo_id),
329
+ "branch_id": str(branch_id) if branch_id else None,
330
+ "commit_oid": commit_oid or "",
331
+ "components": [
332
+ {
333
+ "name": c.name,
334
+ "media_type": c.media_type,
335
+ "digest": c.digest,
336
+ "size_bytes": c.size_bytes,
337
+ }
338
+ for c in sorted(components, key=lambda x: (x.name, x.digest))
339
+ ],
340
+ }
341
+ return _sha256_of(_canonical_json_bytes(body))
342
+
343
+
344
+ def _iso(dt: datetime | None) -> str | None:
345
+ return dt.isoformat() if dt else None
346
+
347
+
348
+ def _to_response(
349
+ snapshot: RepoSnapshot,
350
+ components: list[RepoSnapshotComponent],
351
+ ) -> SnapshotResponse:
352
+ return SnapshotResponse(
353
+ id=str(snapshot.id),
354
+ repo_id=str(snapshot.repo_id),
355
+ org_id=str(snapshot.org_id),
356
+ branch_id=str(snapshot.branch_id) if snapshot.branch_id else None,
357
+ name=snapshot.name,
358
+ description=snapshot.description,
359
+ manifest_hash=snapshot.manifest_hash,
360
+ total_bytes=snapshot.total_bytes,
361
+ component_count=snapshot.component_count,
362
+ commit_oid=snapshot.commit_oid,
363
+ created_at=snapshot.created_at.isoformat(),
364
+ components=[
365
+ SnapshotComponentModel(
366
+ name=c.name,
367
+ media_type=c.media_type,
368
+ digest=c.digest,
369
+ size_bytes=c.size_bytes,
370
+ extra=dict(c.extra or {}),
371
+ )
372
+ for c in components
373
+ ],
374
+ )
375
+
376
+
377
+ # --- Endpoints ---
378
+
379
+
380
+ @router.post(
381
+ "/{org_id}/repos/{repo_id}/snapshots",
382
+ response_model=SnapshotResponse | SnapshotDryRunResponse,
383
+ )
384
+ async def create_snapshot(
385
+ org_id: uuid.UUID,
386
+ repo_id: uuid.UUID,
387
+ req: SnapshotCreateRequest,
388
+ auth: AuthContext = Depends(resolve_auth),
389
+ session: AsyncSession = Depends(get_db_session),
390
+ ) -> SnapshotResponse | SnapshotDryRunResponse:
391
+ """Create a content-addressed snapshot of the repo's current state.
392
+
393
+ ``dry_run=true`` returns the computed manifest hash + component list
394
+ without inserting anything — useful for diffing against an existing
395
+ snapshot before committing.
396
+ """
397
+ await _require_membership(org_id, auth, session)
398
+ repo = await _load_repo(org_id, repo_id, session)
399
+
400
+ if not req.name:
401
+ raise HTTPException(status_code=422, detail="name is required")
402
+
403
+ # Resolve branch (may be None if the repo hasn't been indexed yet).
404
+ branch = await _resolve_branch(repo, req.branch, session)
405
+ components, total_bytes = await _compute_components(session, branch)
406
+ manifest_hash = _compute_manifest_hash(
407
+ repo_id=repo_id,
408
+ branch_id=branch.id if branch else None,
409
+ commit_oid=req.commit_oid,
410
+ components=components,
411
+ )
412
+
413
+ if req.dry_run:
414
+ return SnapshotDryRunResponse(
415
+ repo_id=str(repo_id),
416
+ branch_id=str(branch.id) if branch else None,
417
+ manifest_hash=manifest_hash,
418
+ total_bytes=total_bytes,
419
+ components=components,
420
+ )
421
+
422
+ # Uniqueness: (repo_id, name). IntegrityError from the DB unique
423
+ # constraint would raise 500 — check explicitly for a nice 409.
424
+ existing = await session.execute(
425
+ select(RepoSnapshot).where(
426
+ RepoSnapshot.repo_id == repo_id, RepoSnapshot.name == req.name,
427
+ )
428
+ )
429
+ if existing.scalar_one_or_none():
430
+ raise HTTPException(
431
+ status_code=409,
432
+ detail=f"Snapshot named {req.name!r} already exists for this repo",
433
+ )
434
+
435
+ snapshot = RepoSnapshot(
436
+ org_id=org_id,
437
+ repo_id=repo_id,
438
+ branch_id=branch.id if branch else None,
439
+ name=req.name,
440
+ description=req.description,
441
+ manifest_hash=manifest_hash,
442
+ total_bytes=total_bytes,
443
+ component_count=len(components),
444
+ commit_oid=req.commit_oid or None,
445
+ created_by_user_id=auth.user_id,
446
+ extra={},
447
+ )
448
+ session.add(snapshot)
449
+ await session.flush()
450
+
451
+ component_rows: list[RepoSnapshotComponent] = []
452
+ for c in components:
453
+ row = RepoSnapshotComponent(
454
+ snapshot_id=snapshot.id,
455
+ name=c.name,
456
+ media_type=c.media_type,
457
+ digest=c.digest,
458
+ size_bytes=c.size_bytes,
459
+ extra=dict(c.extra or {}),
460
+ )
461
+ session.add(row)
462
+ component_rows.append(row)
463
+
464
+ from attocode_intel.audit import log_event
465
+
466
+ await log_event(
467
+ session,
468
+ org_id,
469
+ "snapshot.created",
470
+ repo_id=repo_id,
471
+ user_id=auth.user_id,
472
+ detail={
473
+ "snapshot_id": str(snapshot.id),
474
+ "name": snapshot.name,
475
+ "manifest_hash": snapshot.manifest_hash,
476
+ "component_count": snapshot.component_count,
477
+ "total_bytes": snapshot.total_bytes,
478
+ },
479
+ )
480
+
481
+ await session.commit()
482
+ await session.refresh(snapshot)
483
+
484
+ return _to_response(snapshot, component_rows)
485
+
486
+
487
+ @router.get(
488
+ "/{org_id}/repos/{repo_id}/snapshots",
489
+ response_model=SnapshotListResponse,
490
+ )
491
+ async def list_snapshots(
492
+ org_id: uuid.UUID,
493
+ repo_id: uuid.UUID,
494
+ limit: int = Query(20, ge=1, le=100),
495
+ offset: int = Query(0, ge=0),
496
+ auth: AuthContext = Depends(resolve_auth),
497
+ session: AsyncSession = Depends(get_db_session),
498
+ ) -> SnapshotListResponse:
499
+ """List snapshots for a repository (most recent first)."""
500
+ await _require_membership(org_id, auth, session)
501
+ await _load_repo(org_id, repo_id, session)
502
+
503
+ count_result = await session.execute(
504
+ select(func.count()).select_from(
505
+ select(RepoSnapshot).where(RepoSnapshot.repo_id == repo_id).subquery()
506
+ )
507
+ )
508
+ total = int(count_result.scalar() or 0)
509
+
510
+ rows_result = await session.execute(
511
+ select(RepoSnapshot)
512
+ .where(RepoSnapshot.repo_id == repo_id)
513
+ .order_by(RepoSnapshot.created_at.desc())
514
+ .offset(offset).limit(limit)
515
+ )
516
+ snapshots: list[RepoSnapshot] = list(rows_result.scalars())
517
+
518
+ if not snapshots:
519
+ return SnapshotListResponse(
520
+ snapshots=[], total=total, limit=limit, offset=offset, has_more=False,
521
+ )
522
+
523
+ # Batch-fetch components for every snapshot in this page.
524
+ snapshot_ids = [s.id for s in snapshots]
525
+ comp_result = await session.execute(
526
+ select(RepoSnapshotComponent).where(
527
+ RepoSnapshotComponent.snapshot_id.in_(snapshot_ids),
528
+ )
529
+ )
530
+ comps_by_snapshot: dict[uuid.UUID, list[RepoSnapshotComponent]] = {}
531
+ for c in comp_result.scalars():
532
+ comps_by_snapshot.setdefault(c.snapshot_id, []).append(c)
533
+
534
+ return SnapshotListResponse(
535
+ snapshots=[
536
+ _to_response(s, comps_by_snapshot.get(s.id, []))
537
+ for s in snapshots
538
+ ],
539
+ total=total,
540
+ limit=limit,
541
+ offset=offset,
542
+ has_more=(offset + limit < total),
543
+ )
544
+
545
+
546
+ @router.get(
547
+ "/{org_id}/repos/{repo_id}/snapshots/{snapshot_id}",
548
+ response_model=SnapshotResponse,
549
+ )
550
+ async def get_snapshot(
551
+ org_id: uuid.UUID,
552
+ repo_id: uuid.UUID,
553
+ snapshot_id: uuid.UUID,
554
+ auth: AuthContext = Depends(resolve_auth),
555
+ session: AsyncSession = Depends(get_db_session),
556
+ ) -> SnapshotResponse:
557
+ """Fetch a single snapshot by id with its full component list."""
558
+ await _require_membership(org_id, auth, session)
559
+ await _load_repo(org_id, repo_id, session)
560
+
561
+ result = await session.execute(
562
+ select(RepoSnapshot).where(
563
+ RepoSnapshot.id == snapshot_id,
564
+ RepoSnapshot.repo_id == repo_id,
565
+ )
566
+ )
567
+ snapshot = result.scalar_one_or_none()
568
+ if snapshot is None:
569
+ raise HTTPException(status_code=404, detail="Snapshot not found")
570
+
571
+ comp_result = await session.execute(
572
+ select(RepoSnapshotComponent).where(
573
+ RepoSnapshotComponent.snapshot_id == snapshot.id,
574
+ )
575
+ )
576
+ return _to_response(snapshot, list(comp_result.scalars()))
577
+
578
+
579
+ @router.delete("/{org_id}/repos/{repo_id}/snapshots/{snapshot_id}")
580
+ async def delete_snapshot(
581
+ org_id: uuid.UUID,
582
+ repo_id: uuid.UUID,
583
+ snapshot_id: uuid.UUID,
584
+ auth: AuthContext = Depends(resolve_auth),
585
+ session: AsyncSession = Depends(get_db_session),
586
+ ) -> dict:
587
+ """Delete a snapshot. Requires admin+ role (destructive)."""
588
+ await _require_membership(org_id, auth, session, min_role="admin")
589
+ await _load_repo(org_id, repo_id, session)
590
+
591
+ result = await session.execute(
592
+ select(RepoSnapshot).where(
593
+ RepoSnapshot.id == snapshot_id,
594
+ RepoSnapshot.repo_id == repo_id,
595
+ )
596
+ )
597
+ snapshot = result.scalar_one_or_none()
598
+ if snapshot is None:
599
+ raise HTTPException(status_code=404, detail="Snapshot not found")
600
+
601
+ await session.delete(snapshot)
602
+
603
+ from attocode_intel.audit import log_event
604
+
605
+ await log_event(
606
+ session,
607
+ org_id,
608
+ "snapshot.deleted",
609
+ repo_id=repo_id,
610
+ user_id=auth.user_id,
611
+ detail={"snapshot_id": str(snapshot_id), "name": snapshot.name},
612
+ )
613
+ await session.commit()
614
+ return {"detail": "Snapshot deleted"}