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,132 @@
1
+ """File content and tree endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import mimetypes
7
+ import os
8
+
9
+ from fastapi import APIRouter, Depends, HTTPException
10
+
11
+ from attocode_intel.api.auth import verify_auth
12
+ from attocode_intel.api.deps import BranchParam, ensure_branch_supported, get_service_or_404
13
+ from attocode_intel.api.models import (
14
+ FileContentResponse,
15
+ FileTreeResponse,
16
+ TreeEntry,
17
+ )
18
+ from attocode_intel.api.utils import LANG_MAP, MAX_FILE_SIZE, detect_language
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ router = APIRouter(
23
+ prefix="/api/v1/projects/{project_id}",
24
+ tags=["files"],
25
+ dependencies=[Depends(verify_auth)],
26
+ )
27
+
28
+
29
+ def _resolve_path(project_dir: str, rel_path: str) -> str:
30
+ """Resolve and validate a relative path within the project."""
31
+ abs_path = os.path.realpath(os.path.join(project_dir, rel_path))
32
+ real_root = os.path.realpath(project_dir)
33
+ if not abs_path.startswith(real_root + os.sep) and abs_path != real_root:
34
+ raise HTTPException(status_code=400, detail="Path traversal not allowed")
35
+ return abs_path
36
+
37
+
38
+ @router.get("/files/{path:path}", response_model=FileContentResponse)
39
+ async def get_file_content(
40
+ project_id: str,
41
+ path: str,
42
+ branch: BranchParam = "",
43
+ ) -> FileContentResponse:
44
+ """Read file content with metadata."""
45
+ ensure_branch_supported(branch)
46
+ svc = await get_service_or_404(project_id)
47
+ abs_path = _resolve_path(svc.project_dir, path)
48
+
49
+ if not os.path.isfile(abs_path):
50
+ raise HTTPException(status_code=404, detail=f"File not found: {path}")
51
+
52
+ stat = os.stat(abs_path)
53
+ if stat.st_size > MAX_FILE_SIZE:
54
+ raise HTTPException(
55
+ status_code=413,
56
+ detail=f"File too large ({stat.st_size} bytes, max {MAX_FILE_SIZE})",
57
+ )
58
+
59
+ # Check if binary
60
+ mime_type, _ = mimetypes.guess_type(abs_path)
61
+ if mime_type and not mime_type.startswith("text/") and mime_type != "application/json":
62
+ # Allow known text formats
63
+ ext = os.path.splitext(abs_path)[1].lower()
64
+ if ext not in LANG_MAP and ext not in {".cfg", ".ini", ".env", ".gitignore"}:
65
+ raise HTTPException(status_code=415, detail="Binary file — content not available via this endpoint")
66
+
67
+ try:
68
+ with open(abs_path, encoding="utf-8", errors="replace") as f:
69
+ content = f.read()
70
+ except OSError as exc:
71
+ logger.exception("Error reading %s", abs_path)
72
+ raise HTTPException(status_code=500, detail="Error reading file") from exc
73
+
74
+ line_count = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
75
+
76
+ return FileContentResponse(
77
+ path=path,
78
+ content=content,
79
+ language=detect_language(path),
80
+ size_bytes=stat.st_size,
81
+ line_count=line_count,
82
+ )
83
+
84
+
85
+ @router.get("/tree/{path:path}", response_model=FileTreeResponse)
86
+ async def get_file_tree(
87
+ project_id: str,
88
+ path: str = "",
89
+ branch: BranchParam = "",
90
+ ) -> FileTreeResponse:
91
+ """List directory entries."""
92
+ ensure_branch_supported(branch)
93
+ svc = await get_service_or_404(project_id)
94
+ abs_path = _resolve_path(svc.project_dir, path) if path else svc.project_dir
95
+
96
+ if not os.path.isdir(abs_path):
97
+ raise HTTPException(status_code=404, detail=f"Directory not found: {path}")
98
+
99
+ entries: list[TreeEntry] = []
100
+ try:
101
+ for entry in sorted(os.scandir(abs_path), key=lambda e: (not e.is_dir(), e.name)):
102
+ # Skip hidden files and common noise
103
+ if entry.name.startswith(".") or entry.name in {"__pycache__", "node_modules", ".git"}:
104
+ continue
105
+ if entry.is_dir(follow_symlinks=False):
106
+ entries.append(TreeEntry(name=entry.name, type="dir"))
107
+ elif entry.is_file(follow_symlinks=False):
108
+ try:
109
+ size = entry.stat().st_size
110
+ except OSError:
111
+ size = None
112
+ entries.append(TreeEntry(
113
+ name=entry.name,
114
+ type="file",
115
+ size_bytes=size,
116
+ language=detect_language(entry.name),
117
+ ))
118
+ except OSError as exc:
119
+ logger.exception("Error reading directory %s", abs_path)
120
+ raise HTTPException(status_code=500, detail="Error reading directory") from exc
121
+
122
+ return FileTreeResponse(path=path or ".", entries=entries)
123
+
124
+
125
+ @router.get("/tree", response_model=FileTreeResponse)
126
+ async def get_root_tree(
127
+ project_id: str,
128
+ branch: BranchParam = "",
129
+ ) -> FileTreeResponse:
130
+ """List root directory entries."""
131
+ ensure_branch_supported(branch)
132
+ return await get_file_tree(project_id, path="", branch=branch)
@@ -0,0 +1,388 @@
1
+ """Service-mode file/tree browsing via bare git repos (v2).
2
+
3
+ Supports two backends:
4
+ 1. Bare git clone on disk (repos with clone_path) — uses GitManager
5
+ 2. DB-backed manifest (remote-connected repos without clone_path) — uses BranchOverlay + ContentStore
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import uuid
12
+
13
+ from fastapi import APIRouter, Depends, HTTPException, Query
14
+ from pydantic import BaseModel
15
+ from sqlalchemy import func, select
16
+ from sqlalchemy.ext.asyncio import AsyncSession
17
+
18
+ from attocode_intel.api.auth import resolve_auth
19
+ from attocode_intel.api.auth.context import AuthContext
20
+ from attocode_intel.api.deps import get_branch_context, get_db_session, get_git_manager
21
+ from attocode_intel.api.utils import (
22
+ MAX_FILE_SIZE,
23
+ detect_language,
24
+ is_binary,
25
+ )
26
+ from attocode_intel.api.utils import get_repo_by_id as _get_repo
27
+
28
+ router = APIRouter(prefix="/api/v2/projects/{project_id}", tags=["files-v2"])
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ # --- Response models ---
33
+
34
+
35
+ class TreeEntryV2(BaseModel):
36
+ name: str
37
+ path: str
38
+ type: str # file|directory
39
+ size: int = 0
40
+ language: str = ""
41
+ children: list[TreeEntryV2] | None = None
42
+
43
+
44
+ class TreeResponse(BaseModel):
45
+ path: str
46
+ ref: str
47
+ entries: list[TreeEntryV2]
48
+
49
+
50
+ class FileContentV2Response(BaseModel):
51
+ path: str
52
+ ref: str
53
+ content: str
54
+ language: str
55
+ size_bytes: int
56
+ line_count: int
57
+
58
+
59
+ class RepoStatsResponse(BaseModel):
60
+ total_files: int = 0
61
+ total_symbols: int = 0
62
+ languages: dict[str, int] = {}
63
+ embedded_files: int = 0
64
+ total_size_bytes: int = 0
65
+
66
+
67
+ # --- Helpers ---
68
+
69
+
70
+ async def _resolve_ref(repo, ref: str) -> str:
71
+ """Resolve ref to use — defaults to repo's default_branch."""
72
+ return ref if ref else repo.default_branch
73
+
74
+
75
+ async def _build_tree_from_manifest(
76
+ session: AsyncSession,
77
+ repo_id: uuid.UUID,
78
+ ref: str,
79
+ path: str = "",
80
+ ) -> list[TreeEntryV2]:
81
+ """Build nested tree from BranchFile manifest (for repos without bare git clones).
82
+
83
+ Resolves the manifest once, then builds the tree in-memory.
84
+ """
85
+ from attocode_intel.db.models import FileContent
86
+
87
+ branch_ctx = await get_branch_context(repo_id, ref, session)
88
+ manifest = branch_ctx.manifest # dict[str, str] path → sha
89
+
90
+ # Fetch file sizes in bulk
91
+ shas = list(set(manifest.values()))
92
+ sizes: dict[str, int] = {}
93
+ if shas:
94
+ result = await session.execute(
95
+ select(FileContent.sha256, FileContent.size_bytes)
96
+ .where(FileContent.sha256.in_(shas))
97
+ )
98
+ sizes = {row[0]: row[1] for row in result}
99
+
100
+ return _assemble_tree(manifest, sizes, path)
101
+
102
+
103
+ def _assemble_tree(
104
+ manifest: dict[str, str],
105
+ sizes: dict[str, int],
106
+ path: str = "",
107
+ ) -> list[TreeEntryV2]:
108
+ """Build a nested TreeEntryV2 list from a flat manifest (pure in-memory, no DB calls)."""
109
+ prefix = (path.rstrip("/") + "/") if path else ""
110
+
111
+ dirs: dict[str, list[str]] = {}
112
+ files: list[tuple[str, str, int]] = []
113
+
114
+ for file_path, sha in manifest.items():
115
+ if prefix and not file_path.startswith(prefix):
116
+ continue
117
+ relative = file_path[len(prefix):]
118
+ parts = relative.split("/")
119
+ if len(parts) == 1:
120
+ files.append((parts[0], file_path, sizes.get(sha, 0)))
121
+ else:
122
+ dir_name = parts[0]
123
+ if dir_name not in dirs:
124
+ dirs[dir_name] = []
125
+ dirs[dir_name].append(file_path)
126
+
127
+ entries: list[TreeEntryV2] = []
128
+
129
+ for dir_name in sorted(dirs.keys(), key=str.lower):
130
+ dir_path = f"{prefix}{dir_name}" if prefix else dir_name
131
+ children = _assemble_tree(manifest, sizes, dir_path)
132
+ entries.append(TreeEntryV2(
133
+ name=dir_name,
134
+ path=dir_path,
135
+ type="directory",
136
+ size=0,
137
+ language="",
138
+ children=children,
139
+ ))
140
+
141
+ for name, full_path, size in sorted(files, key=lambda x: x[0].lower()):
142
+ entries.append(TreeEntryV2(
143
+ name=name,
144
+ path=full_path,
145
+ type="file",
146
+ size=size,
147
+ language=detect_language(name),
148
+ ))
149
+
150
+ return entries
151
+
152
+
153
+ # --- Endpoints ---
154
+
155
+
156
+ def _build_tree_recursive(
157
+ git, project_id: str, ref: str, path: str = "", max_depth: int = 10,
158
+ ) -> list[TreeEntryV2]:
159
+ """Build a nested tree by recursively walking git entries."""
160
+ if max_depth <= 0:
161
+ return []
162
+ try:
163
+ entries = git.get_tree(project_id, ref, path) if path else git.get_tree(project_id, ref)
164
+ except (FileNotFoundError, ValueError, KeyError):
165
+ return []
166
+
167
+ result = []
168
+ for e in entries:
169
+ node_type = "directory" if e.type == "tree" else "file"
170
+ children = None
171
+ if e.type == "tree":
172
+ children = _build_tree_recursive(git, project_id, ref, e.path, max_depth - 1)
173
+ result.append(TreeEntryV2(
174
+ name=e.name,
175
+ path=e.path,
176
+ type=node_type,
177
+ size=e.size,
178
+ language=detect_language(e.name) if e.type == "blob" else "",
179
+ children=children,
180
+ ))
181
+ # Sort: directories first, then files, alphabetically
182
+ result.sort(key=lambda x: (0 if x.type == "directory" else 1, x.name.lower()))
183
+ return result
184
+
185
+
186
+ @router.get("/tree", response_model=TreeResponse)
187
+ async def get_root_tree(
188
+ project_id: uuid.UUID,
189
+ ref: str = Query("", description="Git ref (branch, tag, commit). Defaults to default branch."),
190
+ recursive: bool = Query(True, description="Return nested tree with children"),
191
+ auth: AuthContext = Depends(resolve_auth),
192
+ session: AsyncSession = Depends(get_db_session),
193
+ ) -> TreeResponse:
194
+ """List root tree at a ref. With recursive=true, returns full nested tree."""
195
+ repo = await _get_repo(project_id, session, auth)
196
+ resolved_ref = await _resolve_ref(repo, ref)
197
+
198
+ # DB-backed fallback for repos without a bare git clone (remote-connected)
199
+ if not repo.clone_path:
200
+ entries = await _build_tree_from_manifest(session, project_id, resolved_ref)
201
+ return TreeResponse(path="", ref=resolved_ref, entries=entries)
202
+
203
+ git = get_git_manager()
204
+
205
+ if recursive:
206
+ import asyncio
207
+ loop = asyncio.get_running_loop()
208
+ entries = await loop.run_in_executor(
209
+ None, _build_tree_recursive, git, str(project_id), resolved_ref, "", 10,
210
+ )
211
+ return TreeResponse(path="", ref=resolved_ref, entries=entries)
212
+
213
+ try:
214
+ entries = git.get_tree(str(project_id), resolved_ref)
215
+ except (FileNotFoundError, ValueError) as e:
216
+ raise HTTPException(status_code=404, detail=str(e))
217
+
218
+ return TreeResponse(
219
+ path="",
220
+ ref=resolved_ref,
221
+ entries=[
222
+ TreeEntryV2(
223
+ name=e.name,
224
+ path=e.path,
225
+ type="directory" if e.type == "tree" else "file",
226
+ size=e.size,
227
+ language=detect_language(e.name) if e.type == "blob" else "",
228
+ )
229
+ for e in entries
230
+ ],
231
+ )
232
+
233
+
234
+ @router.get("/tree/{path:path}", response_model=TreeResponse)
235
+ async def get_subtree(
236
+ project_id: uuid.UUID,
237
+ path: str,
238
+ ref: str = Query("", description="Git ref. Defaults to default branch."),
239
+ auth: AuthContext = Depends(resolve_auth),
240
+ session: AsyncSession = Depends(get_db_session),
241
+ ) -> TreeResponse:
242
+ """List subtree at a path and ref."""
243
+ repo = await _get_repo(project_id, session, auth)
244
+ resolved_ref = await _resolve_ref(repo, ref)
245
+
246
+ # Validate path
247
+ if ".." in path.split("/"):
248
+ raise HTTPException(status_code=400, detail="Path traversal not allowed")
249
+
250
+ # DB-backed fallback for repos without a bare git clone
251
+ if not repo.clone_path:
252
+ entries = await _build_tree_from_manifest(session, project_id, resolved_ref, path)
253
+ return TreeResponse(path=path, ref=resolved_ref, entries=entries)
254
+
255
+ git = get_git_manager()
256
+
257
+ try:
258
+ entries = git.get_tree(str(project_id), resolved_ref, path)
259
+ except (FileNotFoundError, ValueError, KeyError):
260
+ raise HTTPException(status_code=404, detail=f"Path not found: {path}")
261
+
262
+ return TreeResponse(
263
+ path=path,
264
+ ref=resolved_ref,
265
+ entries=[
266
+ TreeEntryV2(
267
+ name=e.name,
268
+ path=e.path,
269
+ type="directory" if e.type == "tree" else "file",
270
+ size=e.size,
271
+ language=detect_language(e.name) if e.type == "blob" else "",
272
+ )
273
+ for e in entries
274
+ ],
275
+ )
276
+
277
+
278
+ @router.get("/files/{path:path}", response_model=FileContentV2Response)
279
+ async def get_file_content(
280
+ project_id: uuid.UUID,
281
+ path: str,
282
+ ref: str = Query("", description="Git ref. Defaults to default branch."),
283
+ auth: AuthContext = Depends(resolve_auth),
284
+ session: AsyncSession = Depends(get_db_session),
285
+ ) -> FileContentV2Response:
286
+ """Read file content at a specific ref."""
287
+ repo = await _get_repo(project_id, session, auth)
288
+ resolved_ref = await _resolve_ref(repo, ref)
289
+
290
+ # Validate path
291
+ if ".." in path.split("/"):
292
+ raise HTTPException(status_code=400, detail="Path traversal not allowed")
293
+
294
+ # DB-backed fallback: read from ContentStore via manifest SHA
295
+ if not repo.clone_path:
296
+ from attocode_intel.storage.content_store import ContentStore
297
+
298
+ branch_ctx = await get_branch_context(project_id, resolved_ref, session)
299
+ sha = branch_ctx.manifest.get(path)
300
+ if sha is None:
301
+ raise HTTPException(status_code=404, detail=f"File not found: {path}")
302
+ content_store = ContentStore(session)
303
+ data = await content_store.get(sha)
304
+ if data is None:
305
+ raise HTTPException(status_code=404, detail=f"Content not found for {path}")
306
+ else:
307
+ git = get_git_manager()
308
+ try:
309
+ data = git.read_file(str(project_id), resolved_ref, path)
310
+ except (FileNotFoundError, KeyError):
311
+ raise HTTPException(status_code=404, detail=f"File not found: {path}")
312
+
313
+ if len(data) > MAX_FILE_SIZE:
314
+ raise HTTPException(
315
+ status_code=413,
316
+ detail=f"File too large ({len(data)} bytes, max {MAX_FILE_SIZE})",
317
+ )
318
+
319
+ if is_binary(data):
320
+ raise HTTPException(status_code=415, detail="Binary file — content not available via this endpoint")
321
+
322
+ content = data.decode("utf-8", errors="replace")
323
+ line_count = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
324
+
325
+ return FileContentV2Response(
326
+ path=path,
327
+ ref=resolved_ref,
328
+ content=content,
329
+ language=detect_language(path),
330
+ size_bytes=len(data),
331
+ line_count=line_count,
332
+ )
333
+
334
+
335
+ @router.get("/stats", response_model=RepoStatsResponse)
336
+ async def get_repo_stats(
337
+ project_id: uuid.UUID,
338
+ ref: str = Query("", description="Git ref. Defaults to default branch."),
339
+ auth: AuthContext = Depends(resolve_auth),
340
+ session: AsyncSession = Depends(get_db_session),
341
+ ) -> RepoStatsResponse:
342
+ """Get repository file/symbol/language/embedding stats."""
343
+ from attocode_intel.db.models import Embedding, Symbol
344
+
345
+ repo = await _get_repo(project_id, session, auth)
346
+ resolved_ref = ref if ref else repo.default_branch
347
+
348
+ try:
349
+ branch_ctx = await get_branch_context(project_id, resolved_ref, session)
350
+ except HTTPException:
351
+ return RepoStatsResponse()
352
+
353
+ manifest = branch_ctx.manifest
354
+ content_shas = branch_ctx.content_shas
355
+
356
+ # Count files and aggregate languages
357
+ languages: dict[str, int] = {}
358
+ for path in manifest:
359
+ lang = detect_language(path)
360
+ if lang:
361
+ languages[lang] = languages.get(lang, 0) + 1
362
+
363
+ # Count symbols
364
+ symbol_count = 0
365
+ if content_shas:
366
+ result = await session.execute(
367
+ select(func.count()).select_from(
368
+ select(Symbol.id).where(Symbol.content_sha.in_(content_shas)).subquery()
369
+ )
370
+ )
371
+ symbol_count = result.scalar() or 0
372
+
373
+ # Count embedded files
374
+ embedded_count = 0
375
+ if content_shas:
376
+ result = await session.execute(
377
+ select(func.count(func.distinct(Embedding.content_sha))).where(
378
+ Embedding.content_sha.in_(content_shas)
379
+ )
380
+ )
381
+ embedded_count = result.scalar() or 0
382
+
383
+ return RepoStatsResponse(
384
+ total_files=len(manifest),
385
+ total_symbols=symbol_count,
386
+ languages=languages,
387
+ embedded_files=embedded_count,
388
+ )