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,7 @@
1
+ """Git repository cloning and management.
2
+
3
+ These modules require pygit2 (service mode dependency).
4
+ Import them directly when needed:
5
+ from attocode_intel.git.manager import GitRepoManager
6
+ from attocode_intel.git.models import BranchInfo, DiffEntry, TreeEntry
7
+ """
@@ -0,0 +1,75 @@
1
+ """Git credential management — SSH keys, deploy tokens, PATs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ import pygit2
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class Credential:
18
+ """Resolved credential for git remote operations."""
19
+
20
+ cred_type: str # ssh_key|deploy_token|pat
21
+ value: str
22
+
23
+
24
+ class CredentialStore:
25
+ """Resolves credentials for git remote operations.
26
+
27
+ Supports SSH keys, deploy tokens, and personal access tokens.
28
+ Returns pygit2-compatible RemoteCallbacks.
29
+ """
30
+
31
+ def __init__(self, ssh_key_path: str = "") -> None:
32
+ self._ssh_key_path = ssh_key_path
33
+
34
+ def resolve(
35
+ self, credential: Credential | None = None
36
+ ) -> pygit2.RemoteCallbacks | None:
37
+ """Resolve a credential into pygit2 RemoteCallbacks.
38
+
39
+ Returns None for public repos (no auth needed).
40
+ """
41
+ try:
42
+ import pygit2
43
+ except ImportError:
44
+ logger.warning("pygit2 not installed — git operations unavailable")
45
+ return None
46
+
47
+ if credential is None:
48
+ return None
49
+
50
+ callbacks = pygit2.RemoteCallbacks()
51
+
52
+ if credential.cred_type == "ssh_key":
53
+ key_path = credential.value or self._ssh_key_path
54
+ if key_path and os.path.exists(key_path):
55
+ pub_path = f"{key_path}.pub"
56
+ keypair = pygit2.Keypair(
57
+ "git",
58
+ pub_path if os.path.exists(pub_path) else key_path,
59
+ key_path,
60
+ "",
61
+ )
62
+ callbacks.credentials = lambda *_args: keypair
63
+ elif credential.cred_type in ("deploy_token", "pat"):
64
+ user_pass = pygit2.UserPass(credential.value, "x-oauth-basic")
65
+ callbacks.credentials = lambda *_args: user_pass
66
+
67
+ return callbacks
68
+
69
+ def resolve_from_url(self, url: str) -> None:
70
+ """Try to auto-detect credentials from URL format.
71
+
72
+ Returns None — callers should use explicit credentials.
73
+ """
74
+ # Public repos don't need auth
75
+ return None
@@ -0,0 +1,493 @@
1
+ """Git repository manager — clone, fetch, branches, diffs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from attocode_intel.git.credentials import Credential, CredentialStore
11
+ from attocode_intel.git.models import (
12
+ BlameHunk,
13
+ BranchInfo,
14
+ CommitInfo,
15
+ DiffEntry,
16
+ DiffHunk,
17
+ DiffLine,
18
+ PatchEntry,
19
+ TreeEntry,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ pass
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class GitRepoManager:
29
+ """Manages git operations on bare repositories.
30
+
31
+ All operations are designed for bare repos to minimize disk usage.
32
+ """
33
+
34
+ def __init__(self, clone_dir: str, ssh_key_path: str = "") -> None:
35
+ self._clone_dir = Path(clone_dir)
36
+ self._cred_store = CredentialStore(ssh_key_path)
37
+ self._path_overrides: dict[str, Path] = {}
38
+ self._clone_dir.mkdir(parents=True, exist_ok=True)
39
+
40
+ def register_path(self, repo_id: str, path: str | Path) -> None:
41
+ """Register a path override for a repo (e.g. local directory imports)."""
42
+ self._path_overrides[repo_id] = Path(path)
43
+
44
+ def _repo_path(self, repo_id: str) -> Path:
45
+ """Return the filesystem path for a repo's bare clone."""
46
+ if repo_id in self._path_overrides:
47
+ return self._path_overrides[repo_id]
48
+ return self._clone_dir / f"{repo_id}.git"
49
+
50
+ def clone(
51
+ self,
52
+ url: str,
53
+ repo_id: str,
54
+ credential: Credential | None = None,
55
+ ) -> str:
56
+ """Clone a remote repository as a bare repo.
57
+
58
+ Returns the local clone path.
59
+ """
60
+ import pygit2
61
+
62
+ dest = self._repo_path(repo_id)
63
+ if dest.exists():
64
+ logger.info("Repo %s already cloned at %s", repo_id, dest)
65
+ return str(dest)
66
+
67
+ callbacks = self._cred_store.resolve(credential)
68
+ logger.info("Cloning %s → %s (bare)", url, dest)
69
+
70
+ pygit2.clone_repository(
71
+ url,
72
+ str(dest),
73
+ bare=True,
74
+ callbacks=callbacks,
75
+ )
76
+ return str(dest)
77
+
78
+ def fetch(
79
+ self,
80
+ repo_id: str,
81
+ credential: Credential | None = None,
82
+ ) -> None:
83
+ """Fetch updates from the remote origin."""
84
+ import pygit2
85
+
86
+ path = self._repo_path(repo_id)
87
+ if not path.exists():
88
+ raise FileNotFoundError(f"Repository {repo_id} not found at {path}")
89
+
90
+ repo = pygit2.Repository(str(path))
91
+ remote = repo.remotes["origin"]
92
+ callbacks = self._cred_store.resolve(credential)
93
+
94
+ logger.info("Fetching updates for %s", repo_id)
95
+ remote.fetch(callbacks=callbacks)
96
+
97
+ def list_branches(self, repo_id: str) -> list[BranchInfo]:
98
+ """List all branches in the repository."""
99
+ import pygit2
100
+
101
+ path = self._repo_path(repo_id)
102
+ if not path.exists():
103
+ raise FileNotFoundError(f"Repository {repo_id} not found")
104
+
105
+ repo = pygit2.Repository(str(path))
106
+ default_branch = "main"
107
+
108
+ # Try to detect default branch from HEAD
109
+ try:
110
+ head_ref = repo.references.get("HEAD")
111
+ if head_ref and head_ref.target:
112
+ target = str(head_ref.target)
113
+ if target.startswith("refs/heads/"):
114
+ default_branch = target[len("refs/heads/"):]
115
+ except Exception:
116
+ pass
117
+
118
+ branches = []
119
+ for ref_name in repo.references:
120
+ if not ref_name.startswith("refs/heads/") and not ref_name.startswith("refs/remotes/origin/"):
121
+ continue
122
+
123
+ name = ref_name
124
+ if name.startswith("refs/heads/"):
125
+ name = name[len("refs/heads/"):]
126
+ elif name.startswith("refs/remotes/origin/"):
127
+ name = name[len("refs/remotes/origin/"):]
128
+ if name == "HEAD":
129
+ continue
130
+
131
+ ref = repo.references[ref_name]
132
+ try:
133
+ commit = ref.peel(pygit2.Commit)
134
+ except Exception:
135
+ continue
136
+
137
+ branches.append(BranchInfo(
138
+ name=name,
139
+ commit=str(commit.id),
140
+ is_default=(name == default_branch),
141
+ ))
142
+
143
+ return branches
144
+
145
+ def get_diff(
146
+ self,
147
+ repo_id: str,
148
+ from_ref: str,
149
+ to_ref: str,
150
+ ) -> list[DiffEntry]:
151
+ """Get the file diff between two refs (branches, commits)."""
152
+ import pygit2
153
+
154
+ path = self._repo_path(repo_id)
155
+ repo = pygit2.Repository(str(path))
156
+
157
+ from_commit = self._resolve_ref(repo, from_ref)
158
+ to_commit = self._resolve_ref(repo, to_ref)
159
+
160
+ diff = repo.diff(from_commit, to_commit)
161
+ entries = []
162
+ for patch in diff:
163
+ delta = patch.delta
164
+ status_map = {
165
+ pygit2.GIT_DELTA_ADDED: "added",
166
+ pygit2.GIT_DELTA_DELETED: "deleted",
167
+ pygit2.GIT_DELTA_MODIFIED: "modified",
168
+ pygit2.GIT_DELTA_RENAMED: "renamed",
169
+ }
170
+ status = status_map.get(delta.status, "modified")
171
+ entries.append(DiffEntry(
172
+ path=delta.new_file.path,
173
+ status=status,
174
+ old_path=delta.old_file.path if status == "renamed" else None,
175
+ additions=patch.line_stats[1],
176
+ deletions=patch.line_stats[2],
177
+ ))
178
+ return entries
179
+
180
+ def read_file(
181
+ self,
182
+ repo_id: str,
183
+ ref: str,
184
+ file_path: str,
185
+ ) -> bytes:
186
+ """Read a file's content at a specific ref."""
187
+ import pygit2
188
+
189
+ path = self._repo_path(repo_id)
190
+ repo = pygit2.Repository(str(path))
191
+ commit = self._resolve_ref(repo, ref)
192
+ tree = commit.tree
193
+
194
+ # Walk the tree path
195
+ entry = tree[file_path]
196
+ blob = repo.get(entry.id)
197
+ if blob is None or not isinstance(blob, pygit2.Blob):
198
+ raise FileNotFoundError(f"File not found: {file_path} at {ref}")
199
+ return blob.data
200
+
201
+ def get_tree(
202
+ self,
203
+ repo_id: str,
204
+ ref: str,
205
+ path: str = "",
206
+ ) -> list[TreeEntry]:
207
+ """List entries in a directory at a specific ref."""
208
+ import pygit2
209
+
210
+ repo_path = self._repo_path(repo_id)
211
+ repo = pygit2.Repository(str(repo_path))
212
+ commit = self._resolve_ref(repo, ref)
213
+ tree = commit.tree
214
+
215
+ if path:
216
+ entry = tree[path]
217
+ subtree = repo.get(entry.id)
218
+ if not isinstance(subtree, pygit2.Tree):
219
+ raise ValueError(f"Path is not a directory: {path}")
220
+ tree = subtree
221
+
222
+ entries = []
223
+ for entry in tree:
224
+ obj = repo.get(entry.id)
225
+ entry_type = "blob" if isinstance(obj, pygit2.Blob) else "tree"
226
+ size = len(obj.data) if isinstance(obj, pygit2.Blob) else 0
227
+ entry_path = f"{path}/{entry.name}" if path else entry.name
228
+ entries.append(TreeEntry(
229
+ name=entry.name,
230
+ path=entry_path,
231
+ type=entry_type,
232
+ size=size,
233
+ oid=str(entry.id),
234
+ ))
235
+ return entries
236
+
237
+ def get_commit_log(
238
+ self,
239
+ repo_id: str,
240
+ ref: str,
241
+ path: str | None = None,
242
+ limit: int = 50,
243
+ offset: int = 0,
244
+ ) -> list[CommitInfo]:
245
+ """Walk commit history from a ref, optionally filtered by path."""
246
+ import pygit2
247
+
248
+ repo_path = self._repo_path(repo_id)
249
+ if not repo_path.exists():
250
+ raise FileNotFoundError(f"Repository {repo_id} not found")
251
+
252
+ repo = pygit2.Repository(str(repo_path))
253
+ commit = self._resolve_ref(repo, ref)
254
+
255
+ commits = []
256
+ seen = 0
257
+ for c in repo.walk(commit.id, pygit2.GIT_SORT_TIME):
258
+ if path:
259
+ # Filter: only include commits that touch the specified path
260
+ if not self._commit_touches_path(repo, c, path):
261
+ continue
262
+ if seen < offset:
263
+ seen += 1
264
+ continue
265
+ commits.append(CommitInfo(
266
+ oid=str(c.id),
267
+ message=c.message.strip(),
268
+ author_name=c.author.name,
269
+ author_email=c.author.email,
270
+ timestamp=c.commit_time,
271
+ parent_oids=[str(p) for p in c.parent_ids],
272
+ ))
273
+ if len(commits) >= limit:
274
+ break
275
+ seen += 1
276
+
277
+ return commits
278
+
279
+ def _commit_touches_path(self, repo, commit, path: str) -> bool:
280
+ """Check if a commit modified the given path compared to its first parent."""
281
+ try:
282
+ entry = commit.tree[path]
283
+ except KeyError:
284
+ # Path doesn't exist in this commit
285
+ if commit.parents:
286
+ try:
287
+ commit.parents[0].tree[path]
288
+ return True # Deleted in this commit
289
+ except KeyError:
290
+ return False
291
+ return False
292
+
293
+ if not commit.parents:
294
+ return True # Initial commit
295
+
296
+ try:
297
+ parent_entry = commit.parents[0].tree[path]
298
+ return entry.id != parent_entry.id
299
+ except KeyError:
300
+ return True # Added in this commit
301
+
302
+ def get_commit_detail(
303
+ self,
304
+ repo_id: str,
305
+ commit_sha: str,
306
+ ) -> tuple[CommitInfo, list[PatchEntry]]:
307
+ """Get full commit info + diff against parent."""
308
+ import pygit2
309
+
310
+ path = self._repo_path(repo_id)
311
+ if not path.exists():
312
+ raise FileNotFoundError(f"Repository {repo_id} not found")
313
+
314
+ repo = pygit2.Repository(str(path))
315
+ commit = self._resolve_ref(repo, commit_sha)
316
+
317
+ info = CommitInfo(
318
+ oid=str(commit.id),
319
+ message=commit.message.strip(),
320
+ author_name=commit.author.name,
321
+ author_email=commit.author.email,
322
+ timestamp=commit.commit_time,
323
+ parent_oids=[str(p) for p in commit.parent_ids],
324
+ )
325
+
326
+ # Diff against first parent (or empty tree for root commit)
327
+ if commit.parents:
328
+ diff = repo.diff(commit.parents[0], commit)
329
+ else:
330
+ diff = commit.tree.diff_to_tree()
331
+
332
+ patches = self._diff_to_patches(diff)
333
+ return info, patches
334
+
335
+ def get_blame(
336
+ self,
337
+ repo_id: str,
338
+ ref: str,
339
+ file_path: str,
340
+ ) -> list[BlameHunk]:
341
+ """Get line-level blame for a file at a ref."""
342
+ import pygit2
343
+
344
+ path = self._repo_path(repo_id)
345
+ if not path.exists():
346
+ raise FileNotFoundError(f"Repository {repo_id} not found")
347
+
348
+ repo = pygit2.Repository(str(path))
349
+ commit = self._resolve_ref(repo, ref)
350
+
351
+ blame = repo.blame(file_path, newest_commit=commit.id)
352
+ hunks = []
353
+ for hunk in blame:
354
+ hunks.append(BlameHunk(
355
+ commit_oid=str(hunk.final_commit_id),
356
+ author_name=hunk.final_committer.name if hunk.final_committer else "",
357
+ author_email=hunk.final_committer.email if hunk.final_committer else "",
358
+ timestamp=hunk.final_committer.time if hunk.final_committer else 0,
359
+ start_line=hunk.final_start_line_number,
360
+ end_line=hunk.final_start_line_number + hunk.lines_in_hunk - 1,
361
+ ))
362
+
363
+ return hunks
364
+
365
+ def get_patch(
366
+ self,
367
+ repo_id: str,
368
+ from_ref: str,
369
+ to_ref: str,
370
+ path: str | None = None,
371
+ ) -> list[PatchEntry]:
372
+ """Get diff with full patch content between two refs."""
373
+ import pygit2
374
+
375
+ repo_path = self._repo_path(repo_id)
376
+ if not repo_path.exists():
377
+ raise FileNotFoundError(f"Repository {repo_id} not found")
378
+
379
+ repo = pygit2.Repository(str(repo_path))
380
+ from_commit = self._resolve_ref(repo, from_ref)
381
+ to_commit = self._resolve_ref(repo, to_ref)
382
+
383
+ diff = repo.diff(from_commit, to_commit)
384
+ patches = self._diff_to_patches(diff)
385
+
386
+ if path:
387
+ patches = [p for p in patches if p.path == path or p.old_path == path]
388
+
389
+ return patches
390
+
391
+ def _diff_to_patches(self, diff) -> list[PatchEntry]:
392
+ """Convert a pygit2 Diff to structured PatchEntry list."""
393
+ import pygit2
394
+
395
+ status_map = {
396
+ pygit2.GIT_DELTA_ADDED: "added",
397
+ pygit2.GIT_DELTA_DELETED: "deleted",
398
+ pygit2.GIT_DELTA_MODIFIED: "modified",
399
+ pygit2.GIT_DELTA_RENAMED: "renamed",
400
+ }
401
+
402
+ patches = []
403
+ for patch in diff:
404
+ delta = patch.delta
405
+ status = status_map.get(delta.status, "modified")
406
+
407
+ hunks = []
408
+ for hunk in patch.hunks:
409
+ lines = []
410
+ for line in hunk.lines:
411
+ lines.append(DiffLine(
412
+ origin=line.origin,
413
+ content=line.content.rstrip("\n"),
414
+ old_lineno=line.old_lineno if line.old_lineno >= 0 else None,
415
+ new_lineno=line.new_lineno if line.new_lineno >= 0 else None,
416
+ ))
417
+ hunks.append(DiffHunk(
418
+ old_start=hunk.old_start,
419
+ old_lines=hunk.old_lines,
420
+ new_start=hunk.new_start,
421
+ new_lines=hunk.new_lines,
422
+ header=hunk.header.strip(),
423
+ lines=lines,
424
+ ))
425
+
426
+ patches.append(PatchEntry(
427
+ path=delta.new_file.path,
428
+ status=status,
429
+ old_path=delta.old_file.path if status == "renamed" else None,
430
+ additions=patch.line_stats[1],
431
+ deletions=patch.line_stats[2],
432
+ hunks=hunks,
433
+ ))
434
+
435
+ return patches
436
+
437
+ def get_disk_usage(self, repo_id: str) -> int:
438
+ """Get total disk usage of a repo clone in bytes."""
439
+ path = self._repo_path(repo_id)
440
+ if not path.exists():
441
+ return 0
442
+ total = 0
443
+ for dirpath, dirnames, filenames in os.walk(path):
444
+ for f in filenames:
445
+ fp = os.path.join(dirpath, f)
446
+ if os.path.isfile(fp):
447
+ total += os.path.getsize(fp)
448
+ return total
449
+
450
+ def delete(self, repo_id: str) -> None:
451
+ """Delete a repo's bare clone from disk.
452
+
453
+ Skips deletion for repos with path overrides (local directory imports)
454
+ to avoid nuking the user's project directory.
455
+ """
456
+ if repo_id in self._path_overrides:
457
+ logger.info("Skipping delete for local repo %s (path override)", repo_id)
458
+ self._path_overrides.pop(repo_id, None)
459
+ return
460
+
461
+ import shutil
462
+
463
+ path = self._repo_path(repo_id)
464
+ if path.exists():
465
+ shutil.rmtree(path)
466
+ logger.info("Deleted clone for %s", repo_id)
467
+
468
+ def _resolve_ref(self, repo, ref: str):
469
+ """Resolve a ref string to a pygit2 Commit object."""
470
+ import pygit2
471
+
472
+ # Try as branch name
473
+ for prefix in ("refs/heads/", "refs/remotes/origin/", ""):
474
+ full_ref = f"{prefix}{ref}" if prefix else ref
475
+ try:
476
+ reference = repo.references[full_ref]
477
+ obj = repo.get(reference.target)
478
+ if isinstance(obj, pygit2.Commit):
479
+ return obj
480
+ except (KeyError, ValueError):
481
+ pass
482
+
483
+ # Try as commit hash
484
+ try:
485
+ obj = repo.revparse_single(ref)
486
+ if isinstance(obj, pygit2.Commit):
487
+ return obj
488
+ if isinstance(obj, pygit2.Tag):
489
+ return obj.get_object()
490
+ except (KeyError, ValueError):
491
+ pass
492
+
493
+ raise ValueError(f"Cannot resolve ref: {ref}")
@@ -0,0 +1,96 @@
1
+ """Data models for git operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class BranchInfo:
10
+ """Information about a git branch."""
11
+
12
+ name: str
13
+ commit: str
14
+ is_default: bool = False
15
+ ahead: int = 0
16
+ behind: int = 0
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class DiffEntry:
21
+ """A single file change in a diff."""
22
+
23
+ path: str
24
+ status: str # added|modified|deleted|renamed
25
+ old_path: str | None = None # For renames
26
+ additions: int = 0
27
+ deletions: int = 0
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class TreeEntry:
32
+ """An entry in a git tree (file or directory)."""
33
+
34
+ name: str
35
+ path: str
36
+ type: str # blob|tree
37
+ size: int = 0
38
+ oid: str = ""
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class CommitInfo:
43
+ """Information about a git commit."""
44
+
45
+ oid: str
46
+ message: str
47
+ author_name: str
48
+ author_email: str
49
+ timestamp: int
50
+ parent_oids: list[str] = field(default_factory=list)
51
+
52
+
53
+ @dataclass(slots=True)
54
+ class BlameHunk:
55
+ """A contiguous block of lines attributed to one commit."""
56
+
57
+ commit_oid: str
58
+ author_name: str
59
+ author_email: str
60
+ timestamp: int
61
+ start_line: int
62
+ end_line: int
63
+
64
+
65
+ @dataclass(slots=True)
66
+ class DiffLine:
67
+ """A single line in a diff hunk."""
68
+
69
+ origin: str # '+'/'-'/' '
70
+ content: str
71
+ old_lineno: int | None = None
72
+ new_lineno: int | None = None
73
+
74
+
75
+ @dataclass(slots=True)
76
+ class DiffHunk:
77
+ """A hunk within a patch."""
78
+
79
+ old_start: int
80
+ old_lines: int
81
+ new_start: int
82
+ new_lines: int
83
+ header: str
84
+ lines: list[DiffLine] = field(default_factory=list)
85
+
86
+
87
+ @dataclass(slots=True)
88
+ class PatchEntry:
89
+ """A file change with full diff hunks."""
90
+
91
+ path: str
92
+ status: str
93
+ old_path: str | None = None
94
+ additions: int = 0
95
+ deletions: int = 0
96
+ hunks: list[DiffHunk] = field(default_factory=list)