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,695 @@
1
+ """Composite tools for the code-intel MCP server.
2
+
3
+ Tools: review_change, explain_impact, suggest_tests.
4
+
5
+ These tools combine multiple analysis passes into single, agent-optimized
6
+ calls that reduce round-trips and produce richer context than calling
7
+ individual tools sequentially.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import subprocess
15
+
16
+ from attocode_intel._shared import (
17
+ _get_context_mgr,
18
+ _get_project_dir,
19
+ _get_service,
20
+ mcp,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Helpers
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ def _git_modified_files(project_dir: str) -> list[str]:
32
+ """Return list of git-modified file paths (relative to project root)."""
33
+ try:
34
+ result = subprocess.run(
35
+ ["git", "diff", "--name-only", "HEAD"],
36
+ cwd=project_dir,
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=15,
40
+ )
41
+ files = [f.strip() for f in result.stdout.splitlines() if f.strip()]
42
+ if not files:
43
+ # Also check staged files
44
+ result = subprocess.run(
45
+ ["git", "diff", "--name-only", "--cached"],
46
+ cwd=project_dir,
47
+ capture_output=True,
48
+ text=True,
49
+ timeout=15,
50
+ )
51
+ files = [f.strip() for f in result.stdout.splitlines() if f.strip()]
52
+ return files
53
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as exc:
54
+ logger.debug("Failed to get git-modified files: %s", exc)
55
+ return []
56
+
57
+
58
+ def _find_test_files_by_convention(file_path: str, project_dir: str) -> list[str]:
59
+ """Find test files matching naming conventions for a source file.
60
+
61
+ Checks for: test_X.py, X_test.py, __tests__/X.test.ts, X.spec.ts, etc.
62
+ """
63
+ from pathlib import Path
64
+
65
+ basename = Path(file_path).stem
66
+ ext = Path(file_path).suffix
67
+ parent = str(Path(file_path).parent)
68
+
69
+ candidates = []
70
+
71
+ if ext in (".py",):
72
+ # Python conventions: test_X.py, X_test.py
73
+ candidates.extend([
74
+ os.path.join(parent, f"test_{basename}.py"),
75
+ os.path.join(parent, f"{basename}_test.py"),
76
+ # tests/ mirror directory
77
+ file_path.replace("src/", "tests/", 1).replace(
78
+ f"{basename}.py", f"test_{basename}.py"
79
+ ),
80
+ # tests/unit/ mirror
81
+ "tests/unit/" + file_path.removeprefix("src/").replace(
82
+ f"{basename}.py", f"test_{basename}.py"
83
+ ),
84
+ # tests/test_X.py flat layout
85
+ f"tests/test_{basename}.py",
86
+ ])
87
+ elif ext in (".ts", ".tsx", ".js", ".jsx"):
88
+ # JS/TS conventions
89
+ test_ext = ".test" + ext
90
+ spec_ext = ".spec" + ext
91
+ candidates.extend([
92
+ os.path.join(parent, f"{basename}{test_ext}"),
93
+ os.path.join(parent, f"{basename}{spec_ext}"),
94
+ os.path.join(parent, "__tests__", f"{basename}{test_ext}"),
95
+ os.path.join(parent, "__tests__", f"{basename}{spec_ext}"),
96
+ ])
97
+ elif ext in (".go",):
98
+ # Go convention: X_test.go in same directory
99
+ candidates.append(os.path.join(parent, f"{basename}_test.go"))
100
+ elif ext in (".rs",):
101
+ # Rust: tests/X.rs or inline #[cfg(test)] (can't find inline from path)
102
+ candidates.append(os.path.join(parent, "tests", f"{basename}.rs"))
103
+ elif ext in (".java", ".kt"):
104
+ # Java/Kotlin: mirror src/main -> src/test
105
+ candidates.append(
106
+ file_path.replace("src/main/", "src/test/", 1).replace(
107
+ f"{basename}{ext}", f"{basename}Test{ext}"
108
+ )
109
+ )
110
+
111
+ # Deduplicate and filter to existing files
112
+ seen: set[str] = set()
113
+ existing: list[str] = []
114
+ for c in candidates:
115
+ c = os.path.normpath(c)
116
+ if c in seen:
117
+ continue
118
+ seen.add(c)
119
+ full = os.path.join(project_dir, c)
120
+ if os.path.isfile(full):
121
+ existing.append(c)
122
+
123
+ return existing
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Tools
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ @mcp.tool()
132
+ def review_change(
133
+ files: list[str] | None = None,
134
+ mode: str = "full",
135
+ ) -> str:
136
+ """Review changed files for security issues, rule violations and convention problems in one report.
137
+
138
+ Args:
139
+ files: Files to review (default: the files that git reports as changed).
140
+ mode: "quick" (security only) or "full".
141
+ """
142
+ project_dir = _get_project_dir()
143
+
144
+ # Resolve files to review
145
+ if files is None:
146
+ files = _git_modified_files(project_dir)
147
+ if not files:
148
+ return "No files to review. Provide file paths or ensure git has modified files."
149
+
150
+ if mode not in ("quick", "full"):
151
+ return f"Error: Invalid mode '{mode}'. Use 'quick' or 'full'."
152
+
153
+ svc = _get_service()
154
+ report_sections: list[str] = []
155
+ total_findings = 0
156
+
157
+ # --- Security scan ---
158
+ security_text = ""
159
+ try:
160
+ # Scan each file's directory to scope the results
161
+ scanned_paths: set[str] = set()
162
+ for f in files:
163
+ scan_path = os.path.dirname(f) or ""
164
+ if scan_path not in scanned_paths:
165
+ scanned_paths.add(scan_path)
166
+
167
+ # Run a single security scan (scoped to project)
168
+ security_text = svc.security_scan(mode="full", path="")
169
+ except Exception as exc:
170
+ security_text = f"Security scan error: {exc}"
171
+
172
+ if security_text:
173
+ # Filter findings to only mention changed files
174
+ relevant_lines: list[str] = []
175
+ for line in security_text.splitlines():
176
+ # Include header/summary lines and lines mentioning changed files
177
+ if any(f in line for f in files) or not line.startswith(" "):
178
+ relevant_lines.append(line)
179
+ filtered_security = "\n".join(relevant_lines) if relevant_lines else security_text
180
+
181
+ # Count findings (lines with severity indicators)
182
+ finding_markers = ("HIGH", "MEDIUM", "LOW", "CRITICAL", "WARNING")
183
+ sec_findings = sum(
184
+ 1 for line in filtered_security.splitlines()
185
+ if any(m in line.upper() for m in finding_markers)
186
+ )
187
+ total_findings += sec_findings
188
+ report_sections.append(f"## Security ({sec_findings} finding(s))\n\n{filtered_security}")
189
+
190
+ # --- Rule-based analysis (full mode only) ---
191
+ if mode == "full":
192
+ rules_text = ""
193
+ try:
194
+ rules_text = svc.analyze(
195
+ files=files, min_confidence=0.5, max_findings=20,
196
+ )
197
+ except Exception as exc:
198
+ rules_text = f"Rule analysis error: {exc}"
199
+
200
+ if rules_text and "No findings" not in rules_text:
201
+ rule_findings = sum(
202
+ 1 for line in rules_text.splitlines()
203
+ if any(m in line for m in ("[CRITICAL]", "[HIGH]", "[MEDIUM]", "[LOW]", "[INFO]"))
204
+ )
205
+ total_findings += rule_findings
206
+ report_sections.append(f"## Rule Analysis ({rule_findings} finding(s))\n\n{rules_text}")
207
+
208
+ # --- Conventions check (full mode only) ---
209
+ if mode == "full":
210
+ conventions_text = ""
211
+ try:
212
+ conventions_text = svc.conventions(sample_size=25, path="")
213
+ except Exception as exc:
214
+ conventions_text = f"Conventions check error: {exc}"
215
+
216
+ if conventions_text:
217
+ report_sections.append(f"## Conventions\n\n{conventions_text}")
218
+
219
+ # --- Build report ---
220
+ file_list = "\n".join(f" - {f}" for f in files)
221
+ header = (
222
+ f"# Change Review Report ({mode} mode)\n\n"
223
+ f"Files reviewed ({len(files)}):\n{file_list}\n"
224
+ )
225
+
226
+ body = "\n\n".join(report_sections) if report_sections else "No findings."
227
+
228
+ # Analysis failures must not be presented as successful verification.
229
+ incomplete = any("error:" in section.lower() for section in report_sections)
230
+ # Assessment
231
+ if incomplete:
232
+ assessment = "Review incomplete: one or more analysis passes failed. Resolve the reported errors before relying on this review."
233
+ elif total_findings == 0:
234
+ assessment = "No findings were reported by the completed checks. This does not establish that the change is safe."
235
+ elif total_findings <= 3:
236
+ assessment = f"{total_findings} finding(s) detected. Review recommended before merging."
237
+ else:
238
+ assessment = (
239
+ f"{total_findings} finding(s) detected. "
240
+ "Careful review required -- multiple issues found."
241
+ )
242
+
243
+ summary = f"\n\n## Summary\n\n{assessment}"
244
+
245
+ return header + "\n" + body + summary
246
+
247
+
248
+ @mcp.tool()
249
+ def explain_impact(
250
+ files: list[str],
251
+ depth: int = 3,
252
+ ) -> str:
253
+ """Explain the blast radius of changing files with rich context.
254
+
255
+ Combines impact analysis, community detection, and temporal coupling
256
+ to provide a comprehensive understanding of what would be affected
257
+ by changes to the specified files.
258
+
259
+ Args:
260
+ files: File paths to analyze (relative to project root).
261
+ depth: Maximum depth for dependency traversal (default 3).
262
+
263
+ Returns:
264
+ Narrative explanation of the change impact.
265
+ """
266
+ if not files:
267
+ return "Error: No files specified for impact analysis."
268
+
269
+ if depth < 1:
270
+ depth = 1
271
+ elif depth > 5:
272
+ depth = 5
273
+
274
+ svc = _get_service()
275
+ sections: list[str] = []
276
+
277
+ # --- Impact analysis ---
278
+ impact_text = ""
279
+ try:
280
+ impact_text = svc.impact_analysis(files)
281
+ except Exception as exc:
282
+ impact_text = f"Impact analysis error: {exc}"
283
+
284
+ if impact_text:
285
+ sections.append(f"## Direct Impact\n\n{impact_text}")
286
+
287
+ # --- Community detection ---
288
+ community_text = ""
289
+ try:
290
+ community_text = svc.community_detection(
291
+ min_community_size=2, max_communities=10,
292
+ )
293
+ except Exception as exc:
294
+ community_text = f"Community detection error: {exc}"
295
+
296
+ if community_text:
297
+ # Extract relevant community info for the target files
298
+ relevant_community_lines: list[str] = []
299
+ in_relevant_community = False
300
+ for line in community_text.splitlines():
301
+ if any(f in line for f in files):
302
+ in_relevant_community = True
303
+ if in_relevant_community:
304
+ relevant_community_lines.append(line)
305
+ # Reset at community boundaries
306
+ if in_relevant_community and line.strip() == "":
307
+ in_relevant_community = False
308
+
309
+ if relevant_community_lines:
310
+ filtered_community = "\n".join(relevant_community_lines)
311
+ sections.append(
312
+ f"## Module Community\n\n"
313
+ f"The changed files belong to these communities:\n\n"
314
+ f"{filtered_community}"
315
+ )
316
+ else:
317
+ sections.append(f"## Module Community\n\n{community_text}")
318
+
319
+ # --- Temporal coupling ---
320
+ coupling_sections: list[str] = []
321
+ high_coupling_files: list[tuple[str, str]] = [] # (file, coupling_info)
322
+ for f in files:
323
+ try:
324
+ coupling_text = svc.change_coupling(
325
+ file=f, days=90, min_coupling=0.3, top_k=10,
326
+ )
327
+ if coupling_text and "no " not in coupling_text.lower():
328
+ coupling_sections.append(f"### {f}\n{coupling_text}")
329
+ # Track high-coupling pairs for risk assessment
330
+ for line in coupling_text.splitlines():
331
+ if any(
332
+ marker in line
333
+ for marker in ("0.8", "0.9", "1.0", "score: 0.7")
334
+ ):
335
+ high_coupling_files.append((f, line.strip()))
336
+ except Exception as exc:
337
+ coupling_sections.append(f"### {f}\nTemporal coupling error: {exc}")
338
+
339
+ if coupling_sections:
340
+ sections.append(
341
+ "## Temporal Coupling (git co-change history)\n\n"
342
+ + "\n\n".join(coupling_sections)
343
+ )
344
+
345
+ # --- Risk assessment ---
346
+ risk_level = "LOW"
347
+ risk_reasons: list[str] = []
348
+
349
+ # Check impact breadth
350
+ if impact_text:
351
+ affected_count = impact_text.count("\n")
352
+ if affected_count > 20:
353
+ risk_level = "HIGH"
354
+ risk_reasons.append(
355
+ f"Large blast radius: {affected_count}+ files potentially affected"
356
+ )
357
+ elif affected_count > 10:
358
+ risk_level = "MEDIUM"
359
+ risk_reasons.append(
360
+ f"Moderate blast radius: ~{affected_count} files potentially affected"
361
+ )
362
+
363
+ # Check temporal coupling
364
+ if high_coupling_files:
365
+ if risk_level == "LOW":
366
+ risk_level = "MEDIUM"
367
+ risk_reasons.append(
368
+ f"{len(high_coupling_files)} file(s) with high temporal coupling "
369
+ "(often change together -- verify they don't need updates too)"
370
+ )
371
+
372
+ # Check if multiple communities are affected
373
+ if len(files) > 1:
374
+ risk_reasons.append(
375
+ f"Changes span {len(files)} files -- cross-cutting changes "
376
+ "may affect multiple modules"
377
+ )
378
+
379
+ risk_summary = (
380
+ f"## Risk Assessment: {risk_level}\n\n"
381
+ + ("\n".join(f" - {r}" for r in risk_reasons) if risk_reasons else " - No elevated risk factors detected.")
382
+ )
383
+ sections.append(risk_summary)
384
+
385
+ # --- Compose narrative ---
386
+ file_list = ", ".join(f"`{f}`" for f in files)
387
+ header = f"# Impact Analysis for {file_list}\n"
388
+
389
+ return header + "\n\n".join(sections)
390
+
391
+
392
+ @mcp.tool()
393
+ def suggest_tests(files: list[str], symbol_name: str | None = None, task_hint: str | None = None) -> str:
394
+ """Suggest candidate tests using imports and syntax; optionally focus on a symbol and task."""
395
+ if not files:
396
+ return "Error: No files specified. Provide a list of changed files."
397
+ return format_test_suggestions(suggest_tests_data(files, symbol_name=symbol_name, task_hint=task_hint))
398
+
399
+
400
+ def suggest_tests_data(files: list[str], *, symbol_name: str | None = None,
401
+ task_hint: str | None = None, linked_reference_files=None) -> dict:
402
+ """Reusable evidence; callers format or budget it without repeating graph traversal."""
403
+ project_dir = _get_project_dir()
404
+
405
+ # Collect test suggestions with priorities
406
+ # Priority 1: Direct test files (by naming convention)
407
+ # Priority 2: Test files that import the changed module
408
+ # Priority 3: Tests for dependent modules
409
+ suggestions: dict[str, dict] = {} # path -> {priority, reasons}
410
+ distances: dict[str, int] = {}
411
+
412
+ def _add_suggestion(path: str, priority: int, reason: str) -> None:
413
+ """Add or update a test suggestion."""
414
+ if path in suggestions:
415
+ existing = suggestions[path]
416
+ existing["priority"] = min(existing["priority"], priority)
417
+ if reason not in existing["reasons"]:
418
+ existing["reasons"].append(reason)
419
+ else:
420
+ suggestions[path] = {"priority": priority, "reasons": [reason]}
421
+
422
+ # --- Priority 1: Convention-based test files ---
423
+ for f in files:
424
+ convention_tests = _find_test_files_by_convention(f, project_dir)
425
+ for test_file in convention_tests:
426
+ _add_suggestion(
427
+ test_file,
428
+ priority=1,
429
+ reason=f"Direct test file for `{f}`",
430
+ )
431
+
432
+ # --- Priority 2: Import-based discovery ---
433
+ try:
434
+ ctx = _get_context_mgr()
435
+ dep_graph = getattr(ctx, "dependency_graph", None) or getattr(ctx, "_dep_graph", None)
436
+
437
+ if dep_graph is not None:
438
+ for f in files:
439
+ # Find files that import the changed file (dependents)
440
+ try:
441
+ importers = dep_graph.get_importers(f)
442
+ except (AttributeError, KeyError):
443
+ try:
444
+ importers = dep_graph.get_reverse_deps(f)
445
+ except (AttributeError, KeyError):
446
+ importers = set()
447
+
448
+ for importer in importers:
449
+ # Check if the importer is a test file
450
+ importer_lower = importer.lower()
451
+ is_test = (
452
+ "test" in importer_lower
453
+ or "spec" in importer_lower
454
+ or "__tests__" in importer_lower
455
+ )
456
+ if is_test:
457
+ _add_suggestion(
458
+ importer,
459
+ priority=2,
460
+ reason=f"Imports changed module `{f}`",
461
+ )
462
+
463
+ # --- Priority 3: Tests for dependent modules ---
464
+ for f in files:
465
+ try:
466
+ importers = dep_graph.get_importers(f)
467
+ except (AttributeError, KeyError):
468
+ try:
469
+ importers = dep_graph.get_reverse_deps(f)
470
+ except (AttributeError, KeyError):
471
+ importers = set()
472
+
473
+ for importer in importers:
474
+ importer_lower = importer.lower()
475
+ is_test = (
476
+ "test" in importer_lower
477
+ or "spec" in importer_lower
478
+ or "__tests__" in importer_lower
479
+ )
480
+ if not is_test:
481
+ # Find tests for this dependent module
482
+ dep_tests = _find_test_files_by_convention(
483
+ importer, project_dir
484
+ )
485
+ for test_file in dep_tests:
486
+ _add_suggestion(
487
+ test_file,
488
+ priority=3,
489
+ reason=f"Tests dependent module `{importer}` (which imports `{f}`)",
490
+ )
491
+ except Exception as exc:
492
+ logger.debug("Import-based test discovery failed: %s", exc)
493
+
494
+ # Follow actual reverse-import paths beyond immediate importers.
495
+ try:
496
+ from collections import deque
497
+ from pathlib import Path
498
+ ctx = _get_context_mgr()
499
+ graph = ctx.dependency_graph
500
+ for changed in files:
501
+ queue = deque([(changed, 0)])
502
+ seen = {changed}
503
+ while queue:
504
+ source, depth = queue.popleft()
505
+ for target in sorted(graph.get_importers(source)) if graph else []:
506
+ if target in seen:
507
+ continue
508
+ seen.add(target)
509
+ queue.append((target, depth + 1))
510
+ parts = Path(target).parts
511
+ filename = Path(target).name
512
+ if (any(p in {"test", "tests", "__tests__", "spec"} for p in parts)
513
+ or filename.startswith("test_") or ".test." in filename
514
+ or ".spec." in filename or filename.endswith("_test.go")):
515
+ _add_suggestion(target, 2 if depth == 0 else 3,
516
+ f"Reverse import path from `{changed}` ({depth + 1} edges)")
517
+ distances[target] = min(distances.get(target, depth + 1), depth + 1)
518
+ path = Path(project_dir) / changed
519
+ if path.suffix == ".rs" and path.is_file():
520
+ content = path.read_text(encoding="utf-8")
521
+ if "#[test]" in content or "#[cfg(test)]" in content:
522
+ _add_suggestion(changed, 1, "Contains Rust inline test declarations; select the owning Cargo package")
523
+ except (OSError, AttributeError) as exc:
524
+ logger.debug("Indirect test discovery incomplete: %s", exc)
525
+
526
+ if symbol_name or task_hint:
527
+ from attocode_intel._shared import _get_ast_service, _get_service
528
+ from attocode_intel.test_ranking import rank_symbol_tests
529
+ service = _get_service()
530
+ task_scores = (service._task_file_scores(task_hint)
531
+ if task_hint and hasattr(service, "_task_file_scores") else {})
532
+ return {"files": files, "symbol_name": symbol_name, "task_hint": task_hint,
533
+ "candidates": rank_symbol_tests(_get_ast_service(), suggestions, files, symbol_name,
534
+ task_hint, distances, linked_reference_files,
535
+ relevance_by_file=task_scores),
536
+ "ranking": "Selected-symbol links, reverse-import distance, then lexical test names; verify candidates",
537
+ "absence_proven": False}
538
+
539
+ # Prefer tests with concrete syntax references to changed definitions.
540
+ symbol_hits: dict[str, int] = {}
541
+ representatives: set[str] = set()
542
+ try:
543
+ from attocode_intel._shared import _get_ast_service
544
+ ast = _get_ast_service()
545
+ for changed in files:
546
+ for symbol in ast.get_file_symbols(changed):
547
+ if symbol.kind == "variable":
548
+ continue
549
+ matches = {ref.file_path for ref in ast.get_callers(symbol.qualified_name)
550
+ if ref.file_path in suggestions}
551
+ if matches:
552
+ representatives.add(min(matches, key=lambda path: (
553
+ symbol.name.lower() not in path.lower(), len(path), path)))
554
+ for path in matches:
555
+ symbol_hits[path] = symbol_hits.get(path, 0) + 1
556
+ _add_suggestion(path, suggestions[path]["priority"],
557
+ f"Syntax reference to `{symbol.qualified_name}`; verify ambiguous matches")
558
+ except (AttributeError, OSError) as exc:
559
+ logger.debug("Test reference ranking incomplete: %s", exc)
560
+
561
+ # Sort by evidence, then put repository tests before documentation examples.
562
+ sorted_suggestions = sorted(
563
+ suggestions.items(),
564
+ key=lambda kv: (kv[1]["priority"], kv[0] not in representatives, -symbol_hits.get(kv[0], 0), kv[0].startswith("docs"), kv[0]),
565
+ )
566
+
567
+ return {"files": files, "candidates": [
568
+ {"file_path": path, "priority": info["priority"], "reasons": info["reasons"][:3]}
569
+ for path, info in sorted_suggestions
570
+ ], "absence_proven": False}
571
+
572
+
573
+ def format_test_suggestions(data: dict) -> str:
574
+ files = data["files"]
575
+ suggestions = {row["file_path"]: row for row in data["candidates"]}
576
+ sorted_suggestions = list(suggestions.items())
577
+ # --- Format output ---
578
+ if not suggestions:
579
+ # Fallback: suggest running all tests
580
+ lines = [
581
+ "# Test Suggestions\n",
582
+ "No specific test files found for the changed files.",
583
+ "",
584
+ "Changed files:",
585
+ ]
586
+ for f in files:
587
+ lines.append(f" - {f}")
588
+ lines.extend([
589
+ "",
590
+ "Recommendations:",
591
+ " - Run the full test suite",
592
+ " - Check if these files have inline tests (e.g., Rust #[cfg(test)])",
593
+ " - Consider creating test files for untested modules",
594
+ ])
595
+ return "\n".join(lines)
596
+
597
+ priority_labels = {
598
+ 1: "DIRECT",
599
+ 2: "IMPORTS",
600
+ 3: "INDIRECT",
601
+ }
602
+
603
+ lines = [
604
+ "# Test Suggestions\n",
605
+ f"Found {len(sorted_suggestions)} test file(s) for "
606
+ f"{len(files)} changed file(s).\n",
607
+ ]
608
+
609
+ current_priority = 0
610
+ for test_path, info in sorted_suggestions:
611
+ priority = info["priority"]
612
+ if priority != current_priority:
613
+ current_priority = priority
614
+ label = priority_labels.get(priority, f"P{priority}")
615
+ lines.append(f"\n## Priority {priority} ({label})\n")
616
+
617
+ lines.append(f" {test_path}")
618
+ for reason in info["reasons"][:3]:
619
+ lines.append(f" - {reason}")
620
+
621
+ # Summary
622
+ by_priority: dict[int, int] = {}
623
+ for _, info in sorted_suggestions:
624
+ by_priority[info["priority"]] = by_priority.get(info["priority"], 0) + 1
625
+
626
+ lines.append("\n## Summary\n")
627
+ for p in sorted(by_priority):
628
+ label = priority_labels.get(p, f"P{p}")
629
+ lines.append(f" {label}: {by_priority[p]} test file(s)")
630
+
631
+ lines.append(
632
+ "\nThese are test candidates based on naming and static relationships. "
633
+ "They are not complete coverage or proof that a change is safe."
634
+ )
635
+
636
+ return "\n".join(lines)
637
+
638
+
639
+ @mcp.tool()
640
+ def architecture_drift(
641
+ config_path: str = "",
642
+ ) -> str:
643
+ """Detect architecture boundary violations in the codebase.
644
+
645
+ Compares actual file dependencies against rules defined in
646
+ .attocode/architecture.yaml. Reports layering violations,
647
+ unauthorized imports, and circular dependencies.
648
+
649
+ Args:
650
+ config_path: Path to architecture config (default: .attocode/architecture.yaml).
651
+ """
652
+ from attocode_intel._internal.integrations.context.architecture_drift import (
653
+ check_drift,
654
+ format_report,
655
+ )
656
+
657
+ project_dir = _get_project_dir()
658
+
659
+ # If a custom config_path is given, verify it exists
660
+ if config_path and not os.path.isfile(config_path):
661
+ return (
662
+ f"Error: Architecture config not found at '{config_path}'.\n"
663
+ "Create a .attocode/architecture.yaml file defining your layers and rules.\n"
664
+ "See documentation for the expected YAML format."
665
+ )
666
+
667
+ # Check that the default config exists when no override is given
668
+ if not config_path:
669
+ default_path = os.path.join(project_dir, ".attocode", "architecture.yaml")
670
+ if not os.path.isfile(default_path):
671
+ return (
672
+ "No architecture config found at .attocode/architecture.yaml.\n\n"
673
+ "Create this file to define your architecture boundaries. Example:\n\n"
674
+ " layers:\n"
675
+ ' - name: presentation\n'
676
+ ' paths: ["src/api/", "src/routes/"]\n'
677
+ ' - name: business\n'
678
+ ' paths: ["src/services/", "src/domain/"]\n'
679
+ ' - name: data\n'
680
+ ' paths: ["src/models/", "src/repositories/"]\n\n'
681
+ " rules:\n"
682
+ " - from: presentation\n"
683
+ " to: [business]\n"
684
+ " deny: [data]\n"
685
+ " - from: business\n"
686
+ " to: [data]\n"
687
+ " deny: [presentation]\n"
688
+ )
689
+
690
+ try:
691
+ report = check_drift(project_dir)
692
+ return format_report(report)
693
+ except Exception as exc:
694
+ logger.exception("Architecture drift check failed")
695
+ return f"Error running architecture drift check: {exc}"