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,341 @@
1
+ """Shared dependencies for code-intel MCP tool modules.
2
+
3
+ Extracted from server.py to break the circular import:
4
+ server.py → tool modules → server.py
5
+
6
+ Tool modules should import from here instead of server.py.
7
+ server.py re-exports these symbols for backward compatibility.
8
+
9
+ Note: Existing tests set ``server._ast_service = mock`` directly on the
10
+ server module's ``__dict__``. The ``_check_server_override`` helper
11
+ ensures the getters respect those overrides without requiring tests to
12
+ change.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os
19
+ import sys
20
+ from typing import TYPE_CHECKING
21
+
22
+ try:
23
+ from mcp.server.fastmcp import FastMCP
24
+ except ImportError:
25
+ print(
26
+ "Error: 'mcp' package not installed. "
27
+ "Reinstall with: uv tool install --force --reinstall --from . attocode",
28
+ file=sys.stderr,
29
+ )
30
+ sys.exit(1)
31
+
32
+ if TYPE_CHECKING:
33
+ from attocode_intel.api.providers.remote_provider import RemoteTextService
34
+ from attocode_intel.service import CodeIntelService
35
+ from attocode_intel._internal.integrations.context.ast_service import ASTService
36
+ from attocode_intel._internal.integrations.context.code_analyzer import CodeAnalyzer
37
+ from attocode_intel._internal.integrations.context.codebase_context import CodebaseContextManager
38
+ from attocode_intel._internal.integrations.context.frecency import FrecencyTracker
39
+ from attocode_intel._internal.integrations.context.hierarchical_explorer import HierarchicalExplorer
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # FastMCP instance (singleton)
45
+ # ---------------------------------------------------------------------------
46
+
47
+ mcp = FastMCP("attocode-code-intel")
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Lazily initialized singletons
51
+ # ---------------------------------------------------------------------------
52
+
53
+ _ast_service: ASTService | None = None
54
+ _context_mgr: CodebaseContextManager | None = None
55
+ _code_analyzer: CodeAnalyzer | None = None
56
+ _semantic_search: object | None = None # Backward compat: tests may set this directly
57
+ _memory_store: object | None = None # Backward compat: tests may set this directly
58
+ _explorer: HierarchicalExplorer | None = None
59
+ _service: CodeIntelService | None = None
60
+ _remote_service: RemoteTextService | None = None
61
+ _remote_degraded_reason: str = ""
62
+
63
+
64
+ def _check_server_override(name: str) -> object | None:
65
+ """Return the value of *name* from ``server.__dict__`` if set there.
66
+
67
+ Tests (and some internal code) set e.g. ``server._ast_service = mock``
68
+ directly. Since getters live here, we must check the server module's
69
+ __dict__ so those overrides are respected. Uses sys.modules to avoid
70
+ a circular import.
71
+ """
72
+ srv = sys.modules.get("attocode_intel.server")
73
+ if srv is not None and name in srv.__dict__:
74
+ return srv.__dict__[name] # type: ignore[no-any-return]
75
+ return None
76
+
77
+
78
+ # Project-directory discovery lives in the neutral ``project_dir``
79
+ # module so HTTP providers can import it without dragging in the MCP
80
+ # runtime. Re-export here for backward compatibility with the existing
81
+ # ~25 call sites that do ``from attocode_intel._shared import
82
+ # _get_project_dir`` (or access it as ``_shared._get_project_dir``).
83
+ from attocode_intel.project_dir import ( # noqa: F401, E402
84
+ _get_project_dir,
85
+ _walk_up,
86
+ )
87
+
88
+
89
+ def _get_ast_service() -> ASTService:
90
+ """Lazily initialize and return the ASTService singleton."""
91
+ from attocode_intel.request_context import current_request
92
+ if (request := current_request.get()) is not None:
93
+ return request.service._get_ast_service()
94
+ global _ast_service
95
+ override = _check_server_override("_ast_service")
96
+ if override is not None:
97
+ return override # type: ignore[return-value]
98
+ if _ast_service is None:
99
+ from attocode_intel._internal.integrations.context.ast_service import ASTService
100
+
101
+ project_dir = _get_project_dir()
102
+ _ast_service = ASTService.get_instance(project_dir)
103
+ if not _ast_service.initialized:
104
+ logger.info("Initializing ASTService (skeleton) for %s...", project_dir)
105
+ _ast_service.initialize_skeleton(indexing_depth="auto")
106
+ if (
107
+ _ast_service._hydration_state
108
+ and _ast_service._hydration_state.phase != "ready"
109
+ ):
110
+ _ast_service.start_hydration()
111
+ logger.info(
112
+ "ASTService ready: %d files indexed (skeleton/hydration)",
113
+ len(_ast_service._ast_cache),
114
+ )
115
+ return _ast_service
116
+
117
+
118
+ def _get_context_mgr() -> CodebaseContextManager:
119
+ """Lazily initialize and return the CodebaseContextManager."""
120
+ from attocode_intel.request_context import current_request
121
+ if (request := current_request.get()) is not None:
122
+ return request.service._get_context_mgr()
123
+ global _context_mgr
124
+ override = _check_server_override("_context_mgr")
125
+ if override is not None:
126
+ return override # type: ignore[return-value]
127
+ if _context_mgr is None:
128
+ from attocode_intel._internal.integrations.context.codebase_context import CodebaseContextManager
129
+
130
+ project_dir = _get_project_dir()
131
+ _context_mgr = CodebaseContextManager(root_dir=project_dir)
132
+ _context_mgr.discover_files()
133
+ return _context_mgr
134
+
135
+
136
+ def _get_code_analyzer() -> CodeAnalyzer:
137
+ """Lazily initialize and return the CodeAnalyzer."""
138
+ from attocode_intel.request_context import current_request
139
+ if (request := current_request.get()) is not None:
140
+ return request.service._get_code_analyzer()
141
+ global _code_analyzer
142
+ override = _check_server_override("_code_analyzer")
143
+ if override is not None:
144
+ return override # type: ignore[return-value]
145
+ if _code_analyzer is None:
146
+ from attocode_intel._internal.integrations.context.code_analyzer import CodeAnalyzer
147
+
148
+ _code_analyzer = CodeAnalyzer()
149
+ return _code_analyzer
150
+
151
+
152
+ def _get_service() -> CodeIntelService | RemoteTextService:
153
+ """Lazily initialize and return the CodeIntelService singleton."""
154
+ from attocode_intel.request_context import current_request
155
+ if (request := current_request.get()) is not None:
156
+ return request.service
157
+ global _service
158
+ srv = sys.modules.get("attocode_intel.server")
159
+ if srv is not None and "_service" in srv.__dict__:
160
+ override = srv.__dict__["_service"]
161
+ if override is None:
162
+ _service = None
163
+ else:
164
+ return override # type: ignore[no-any-return]
165
+ if _remote_service is not None:
166
+ return _remote_service
167
+ if _service is not None:
168
+ from attocode_intel import service as code_intel_service_module
169
+
170
+ if _service.project_dir not in code_intel_service_module._instances:
171
+ _service = None
172
+ if _service is None:
173
+ from attocode_intel.service import CodeIntelService
174
+
175
+ project_dir = _get_project_dir()
176
+ _service = CodeIntelService.get_instance(project_dir)
177
+ return _service
178
+
179
+
180
+ def _get_local_service() -> CodeIntelService:
181
+ """Return the local CodeIntelService, bypassing any remote proxy.
182
+
183
+ Some tools (e.g. ``regex_search``, ``call_graph``) have no server-side
184
+ HTTP route, so they cannot be proxied to a remote. They call this to run
185
+ against the local index even when a remote is configured, instead of
186
+ crashing with ``AttributeError`` on the missing remote method.
187
+ """
188
+ from attocode_intel.request_context import current_request
189
+ if (request := current_request.get()) is not None:
190
+ return request.service
191
+ global _service
192
+ srv = sys.modules.get("attocode_intel.server")
193
+ if srv is not None and "_service" in srv.__dict__:
194
+ override = srv.__dict__["_service"]
195
+ if override is not None and not _is_remote_service(override):
196
+ return override # type: ignore[return-value]
197
+ if override is None:
198
+ _service = None
199
+ if _service is not None:
200
+ from attocode_intel import service as code_intel_service_module
201
+
202
+ if _service.project_dir not in code_intel_service_module._instances:
203
+ _service = None
204
+ if _service is None:
205
+ from attocode_intel.service import CodeIntelService
206
+
207
+ _service = CodeIntelService.get_instance(_get_project_dir())
208
+ return _service
209
+
210
+
211
+ def _is_remote_service(obj: object) -> bool:
212
+ """True when *obj* is a remote proxy rather than the local service."""
213
+ return type(obj).__name__ == "RemoteTextService"
214
+
215
+
216
+ def _get_remote_service() -> RemoteTextService | None:
217
+ """Return the configured remote text service, if any."""
218
+ from attocode_intel.request_context import current_request
219
+ if current_request.get() is not None:
220
+ return None
221
+ srv = sys.modules.get("attocode_intel.server")
222
+ if srv is not None and "_remote_service" in srv.__dict__:
223
+ return srv.__dict__["_remote_service"] # type: ignore[no-any-return]
224
+ return _remote_service
225
+
226
+
227
+ def configure_remote_service(remote_url: str, remote_token: str, remote_repo_id: str) -> None:
228
+ """Proxy the shared service through a remote HTTP server **if it is usable**.
229
+
230
+ Gracefully degrades to local mode (leaving ``_remote_service`` unset) when
231
+ the JWT is expired or the server is unreachable, recording a human-readable
232
+ reason in :func:`get_remote_degraded_reason`. This prevents a stale or dead
233
+ ``[remote]`` config from making every tool fail with "Connection refused".
234
+ """
235
+ global _remote_service, _service, _remote_degraded_reason
236
+ from attocode_intel.api.providers.remote_provider import RemoteTextService
237
+ from attocode_intel.config import token_is_expired
238
+
239
+ # Drop any previously-configured client (avoid a socket leak on reconfigure).
240
+ if _remote_service is not None:
241
+ try:
242
+ _remote_service.close()
243
+ except Exception:
244
+ pass
245
+ _remote_service = None
246
+ _service = None
247
+ _remote_degraded_reason = ""
248
+
249
+ if token_is_expired(remote_token):
250
+ _remote_degraded_reason = (
251
+ f"remote token expired — using local mode (server={remote_url})"
252
+ )
253
+ logger.warning("Code-intel %s", _remote_degraded_reason)
254
+ return
255
+
256
+ candidate = RemoteTextService(remote_url, remote_token, remote_repo_id)
257
+ if not candidate.ping():
258
+ _remote_degraded_reason = (
259
+ f"remote server unreachable at {remote_url} — using local mode"
260
+ )
261
+ logger.warning("Code-intel %s", _remote_degraded_reason)
262
+ try:
263
+ candidate.close()
264
+ except Exception:
265
+ pass
266
+ return
267
+
268
+ _remote_service = candidate
269
+ logger.info("Code-intel remote mode active: %s (repo=%s)", remote_url, remote_repo_id)
270
+
271
+
272
+ def clear_remote_service() -> None:
273
+ """Reset remote service wiring (back to local mode)."""
274
+ global _remote_service, _remote_degraded_reason
275
+ if _remote_service is not None:
276
+ try:
277
+ _remote_service.close()
278
+ except Exception:
279
+ pass
280
+ _remote_service = None
281
+ _remote_degraded_reason = ""
282
+
283
+
284
+ def get_remote_degraded_reason() -> str:
285
+ """Why remote mode is inactive despite being configured ('' when N/A)."""
286
+ return _remote_degraded_reason
287
+
288
+
289
+ def enable_remote_if_configured(
290
+ project_dir: str,
291
+ *,
292
+ local_only: bool = False,
293
+ remote_url: str = "",
294
+ remote_token: str = "",
295
+ remote_repo_id: str = "",
296
+ ) -> None:
297
+ """Enable remote mode from explicit args / config / env, else stay local.
298
+
299
+ Single source of truth for the startup wiring previously duplicated (and
300
+ divergent) between ``server.main`` and ``cli._cmd_serve``. Explicit
301
+ ``remote_*`` args win; otherwise ``.attocode/config.toml`` + env are read
302
+ via :func:`load_remote_config`. ``configure_remote_service`` then health/
303
+ expiry-gates the remote and falls back to local on any problem.
304
+ """
305
+ clear_remote_service()
306
+ if local_only:
307
+ return
308
+ if not remote_url:
309
+ from attocode_intel.config import load_remote_config
310
+
311
+ rc = load_remote_config(project_dir)
312
+ if rc.is_configured:
313
+ remote_url, remote_token, remote_repo_id = rc.server, rc.token, rc.repo_id
314
+ if remote_url:
315
+ configure_remote_service(remote_url, remote_token, remote_repo_id)
316
+
317
+
318
+ def _get_explorer() -> HierarchicalExplorer:
319
+ """Lazily initialize the hierarchical explorer."""
320
+ from attocode_intel.request_context import current_request
321
+ if (request := current_request.get()) is not None:
322
+ return request.service._get_explorer()
323
+ global _explorer
324
+ override = _check_server_override("_explorer")
325
+ if override is not None:
326
+ return override # type: ignore[return-value]
327
+ if _explorer is None:
328
+ from attocode_intel._internal.integrations.context.hierarchical_explorer import HierarchicalExplorer
329
+ ctx = _get_context_mgr()
330
+ ast_svc = _get_ast_service()
331
+ _explorer = HierarchicalExplorer(ctx, ast_service=ast_svc)
332
+ return _explorer
333
+
334
+
335
+ def _get_frecency_tracker() -> FrecencyTracker:
336
+ """Get shared frecency tracker (thread-safe via get_tracker lock)."""
337
+ from attocode_intel._internal.integrations.context.frecency import get_tracker
338
+
339
+ project_dir = _get_project_dir()
340
+ db_path = os.path.join(project_dir, ".attocode", "frecency")
341
+ return get_tracker(db_path=db_path)
@@ -0,0 +1,56 @@
1
+ """Report analysis capabilities independently of index progress."""
2
+ from __future__ import annotations
3
+
4
+ import shutil
5
+ from collections import Counter
6
+
7
+
8
+ def language_capabilities(verified_languages=()):
9
+ from ._internal.integrations.context.codebase_ast import LANG_EXTENSIONS
10
+ from ._internal.integrations.context.codebase_context import IMPORT_RESOLVERS
11
+ from ._internal.integrations.context.ts_parser import is_available, supported_languages
12
+ from ._internal.integrations.lsp.client import BUILTIN_SERVERS
13
+
14
+ result = {}
15
+ for language in sorted(set(LANG_EXTENSIONS.values()) | set(supported_languages())):
16
+ server = BUILTIN_SERVERS.get("typescript" if language == "javascript" else language)
17
+ parser = is_available(language)
18
+ result[language] = {
19
+ "search": "available",
20
+ "definitions": "syntax" if parser else "fallback",
21
+ "references": "syntax_candidates" if parser else "text_candidates",
22
+ "dependencies": "partial" if language in IMPORT_RESOLVERS else "unavailable",
23
+ "test_relationships": "heuristic",
24
+ "precision_server": server.command if server else None,
25
+ "precision_installed": bool(server and shutil.which(server.command)),
26
+ "precision_verified": language in verified_languages,
27
+ }
28
+ return result
29
+
30
+
31
+ def analysis_report(service, precision=None):
32
+ ast = service._ast_service
33
+ if ast is None:
34
+ return {"status": "not_started", "absence_proven": False}
35
+ cache = ast._ast_cache.copy()
36
+ tiers = Counter(getattr(a, "parsing_tier", "unknown") or "unknown" for a in cache.values())
37
+ graph = ast._context_mgr._dep_graph
38
+ unresolved = graph.unresolved if graph else {}
39
+ limitations = [
40
+ "Syntax matches are candidates; dynamic dispatch and external dependencies may be unresolved.",
41
+ "No matches do not prove absence of references or impact.",
42
+ ]
43
+ return {
44
+ "status": "partial",
45
+ "absence_proven": False,
46
+ "sources": sorted(tiers),
47
+ "files_by_parser": dict(tiers),
48
+ "unresolved_imports": sum(len(v) for v in unresolved.values()),
49
+ "unresolved_import_samples": [
50
+ {"file": path, "module": module}
51
+ for path, modules in sorted(unresolved.items())[:5] for module in modules[:3]
52
+ ],
53
+ "unresolved_includes_external": True,
54
+ "precision": precision or {"status": "not_requested"},
55
+ "limitations": limitations,
56
+ }
@@ -0,0 +1 @@
1
+ """FastAPI HTTP API for Attocode Code Intelligence."""
@@ -0,0 +1,32 @@
1
+ """Apply repository authorization before legacy routes can consult cached services."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from fastapi import HTTPException
8
+ from starlette.middleware.base import BaseHTTPMiddleware
9
+ from starlette.responses import JSONResponse
10
+
11
+
12
+ class RepositoryAccessMiddleware(BaseHTTPMiddleware):
13
+ async def dispatch(self, request, call_next):
14
+ config = request.app.state.config
15
+ if config.is_service_mode:
16
+ match = re.match(r"/api/v[12]/(?:projects|repos)/([^/]+)", request.url.path)
17
+ if match:
18
+ from attocode_intel.api.auth import resolve_auth
19
+ from attocode_intel.remote import authorized_repo
20
+
21
+ try:
22
+ auth = await resolve_auth(request.headers.get("authorization"))
23
+ await authorized_repo(
24
+ match.group(1), auth, write=request.method not in {"GET", "HEAD", "OPTIONS"}
25
+ )
26
+ except HTTPException as exc:
27
+ return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
28
+ elif request.url.path.rstrip("/") == "/api/v1/projects":
29
+ return JSONResponse(
30
+ {"detail": "Use organization-scoped repository management"}, status_code=403
31
+ )
32
+ return await call_next(request)
@@ -0,0 +1,247 @@
1
+ """FastAPI application factory for Attocode Code Intelligence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from contextlib import asynccontextmanager
7
+ from typing import TYPE_CHECKING
8
+
9
+ from attocode_intel import __version__
10
+ from attocode_intel.config import CodeIntelConfig
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import AsyncGenerator
14
+
15
+ from fastapi import FastAPI
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ async def _run_migrations(database_url: str) -> None:
21
+ """Run Alembic migrations programmatically."""
22
+ import asyncio
23
+ from pathlib import Path
24
+
25
+ from alembic import command
26
+ from alembic.config import Config
27
+
28
+ migrations_dir = Path(__file__).resolve().parent.parent / "migrations"
29
+ alembic_cfg = Config(str(migrations_dir / "alembic.ini"))
30
+ alembic_cfg.set_main_option("script_location", str(migrations_dir))
31
+ alembic_cfg.set_main_option("sqlalchemy.url", database_url)
32
+
33
+ # Alembic is sync — run in thread to avoid blocking the event loop
34
+ loop = asyncio.get_running_loop()
35
+ await loop.run_in_executor(None, command.upgrade, alembic_cfg, "head")
36
+
37
+
38
+ @asynccontextmanager
39
+ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
40
+ """Startup/shutdown lifecycle — initialize DB engine in service mode."""
41
+ config: CodeIntelConfig = app.state.config
42
+ if config.is_service_mode:
43
+ from attocode_intel.db.engine import dispose_engine, init_engine
44
+
45
+ init_engine(config.database_url)
46
+ logger.info("Service mode: database engine initialized")
47
+
48
+ # Auto-run migrations on startup
49
+ try:
50
+ await _run_migrations(config.database_url)
51
+ logger.info("Service mode: migrations applied")
52
+ except Exception:
53
+ logger.exception("Failed to run migrations on startup")
54
+ raise
55
+ async with app.state.mcp_transport.manager.run():
56
+ yield
57
+ await app.state.mcp_transport.gateway.close()
58
+
59
+ # Shutdown debouncer
60
+ from attocode_intel.api.routes.notify import _debouncer
61
+
62
+ if _debouncer is not None:
63
+ await _debouncer.shutdown()
64
+
65
+ if config.is_service_mode:
66
+ from attocode_intel.db.engine import dispose_engine
67
+
68
+ await dispose_engine()
69
+ logger.info("Service mode: database engine disposed")
70
+
71
+
72
+ def create_app(config: CodeIntelConfig | None = None) -> FastAPI:
73
+ """Create and configure the FastAPI application.
74
+
75
+ Args:
76
+ config: Service configuration. If None, reads from environment.
77
+ """
78
+ from fastapi import FastAPI
79
+ from fastapi.middleware.cors import CORSMiddleware
80
+
81
+ from attocode_intel.api import deps
82
+ from attocode_intel.api.middleware import MetricsMiddleware, RequestLoggingMiddleware
83
+ from attocode_intel.api.routes import (
84
+ adr,
85
+ analysis,
86
+ files,
87
+ graph,
88
+ graph_viz,
89
+ health,
90
+ history,
91
+ learning,
92
+ lsp,
93
+ notify,
94
+ projects,
95
+ search,
96
+ )
97
+
98
+ if config is None:
99
+ config = CodeIntelConfig.from_env()
100
+
101
+ deps.configure(config)
102
+
103
+ app = FastAPI(
104
+ title="Attocode Code Intelligence",
105
+ description=(
106
+ "Code intelligence service with AST parsing, dependency graphs, "
107
+ "semantic search, impact analysis, and more. "
108
+ "v1 endpoints return text (MCP-compatible), "
109
+ "v2 endpoints return structured JSON (UI-ready)."
110
+ ),
111
+ version=__version__,
112
+ docs_url="/docs",
113
+ redoc_url="/redoc",
114
+ lifespan=_lifespan,
115
+ )
116
+ app.state.config = config
117
+ from attocode_intel.api.mcp_transport import MCPTransport
118
+ from starlette.routing import Mount, Route
119
+
120
+ transport = MCPTransport(config)
121
+ app.state.mcp_transport = transport
122
+ app.router.routes.append(Route("/mcp", endpoint=transport, methods=["GET", "POST", "DELETE"]))
123
+ app.router.routes.append(Mount("/mcp", app=transport))
124
+
125
+ # Without configured origins, no other web origin can call the API, so no CORS headers.
126
+ # credentials=True is invalid with origins=["*"] per the CORS spec.
127
+ if config.cors_origins:
128
+ app.add_middleware(
129
+ CORSMiddleware,
130
+ allow_origins=config.cors_origins,
131
+ allow_credentials="*" not in config.cors_origins,
132
+ allow_methods=["*"],
133
+ allow_headers=["*"],
134
+ )
135
+ from attocode_intel.api.access import RepositoryAccessMiddleware
136
+
137
+ app.add_middleware(RepositoryAccessMiddleware)
138
+ app.add_middleware(RequestLoggingMiddleware)
139
+ app.add_middleware(MetricsMiddleware)
140
+
141
+ # Rate limiting (service mode only)
142
+ if config.is_service_mode:
143
+ from attocode_intel.api.middleware import RateLimitMiddleware
144
+
145
+ app.add_middleware(RateLimitMiddleware, requests_per_minute=300)
146
+
147
+ # Core routes — each module has router_v1 (text) + router_v2 (JSON)
148
+ from attocode_intel.api.routes.operations import router as operations_router
149
+
150
+ app.include_router(operations_router)
151
+ app.include_router(health.router)
152
+ app.include_router(projects.router)
153
+ app.include_router(adr.router_v1)
154
+ app.include_router(analysis.router_v1)
155
+ app.include_router(analysis.router_v2)
156
+ app.include_router(search.router_v1)
157
+ app.include_router(search.router_v2)
158
+ app.include_router(graph.router_v1)
159
+ app.include_router(graph.router_v2)
160
+ app.include_router(lsp.router_v1)
161
+ app.include_router(lsp.router_v2)
162
+ app.include_router(learning.router)
163
+ app.include_router(learning.router_v2)
164
+ app.include_router(notify.router)
165
+ app.include_router(files.router)
166
+ app.include_router(history.router_v1)
167
+ app.include_router(history.router_v2)
168
+ app.include_router(graph_viz.router)
169
+
170
+ # Service mode routes
171
+ if config.is_service_mode:
172
+ from attocode_intel.api.routes import (
173
+ activity,
174
+ api_keys,
175
+ auth,
176
+ branches,
177
+ cross_repo_search,
178
+ embeddings,
179
+ files_v2,
180
+ gc,
181
+ git_v2,
182
+ jobs,
183
+ orgs,
184
+ preferences,
185
+ presence,
186
+ repos,
187
+ snapshots,
188
+ webhooks,
189
+ websocket,
190
+ )
191
+
192
+ app.include_router(auth.router)
193
+ app.include_router(orgs.router)
194
+ app.include_router(repos.router)
195
+ app.include_router(api_keys.router)
196
+ app.include_router(branches.router)
197
+ app.include_router(webhooks.router)
198
+ app.include_router(websocket.router)
199
+ app.include_router(jobs.router)
200
+ # v2 service-mode routes
201
+ app.include_router(files_v2.router)
202
+ app.include_router(git_v2.router)
203
+ app.include_router(embeddings.router)
204
+ app.include_router(presence.router)
205
+ app.include_router(activity.router)
206
+ app.include_router(preferences.router)
207
+ app.include_router(cross_repo_search.router)
208
+ app.include_router(gc.router)
209
+ app.include_router(snapshots.router)
210
+
211
+ # Graph visualization — serve the bundled HTML page at /graph
212
+ import os
213
+ from pathlib import Path
214
+
215
+ from starlette.responses import FileResponse as _FileResponse
216
+
217
+ _graph_html = Path(__file__).resolve().parent / "static" / "graph.html"
218
+
219
+ @app.get("/graph", include_in_schema=False)
220
+ async def graph_visualization_page() -> _FileResponse:
221
+ """Serve the D3 dependency-graph visualization."""
222
+ return _FileResponse(str(_graph_html), media_type="text/html")
223
+
224
+ # SPA fallback — serve frontend static files (registered LAST so it
225
+ # doesn't shadow /api/*, /docs, /redoc, /openapi.json).
226
+
227
+ static_dir = os.environ.get("ATTOCODE_STATIC_DIR", "/app/static")
228
+ if os.path.isdir(static_dir):
229
+ from starlette.responses import FileResponse
230
+
231
+ @app.get("/{full_path:path}", include_in_schema=False)
232
+ async def spa_fallback(full_path: str) -> FileResponse:
233
+ from pathlib import Path
234
+
235
+ file_path = (Path(static_dir) / full_path).resolve()
236
+ if file_path.is_relative_to(Path(static_dir).resolve()) and file_path.is_file():
237
+ return FileResponse(file_path)
238
+ return FileResponse(os.path.join(static_dir, "index.html"))
239
+
240
+ logger.info("SPA fallback enabled from %s", static_dir)
241
+
242
+ logger.info(
243
+ "FastAPI app created for %s (service_mode=%s)",
244
+ config.project_dir or "(no project)",
245
+ config.is_service_mode,
246
+ )
247
+ return app