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,141 @@
1
+ """Small evidence bundles composed from one refreshed workspace."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+
7
+ def select_definitions(service, symbol_name, file_path=None, line=None):
8
+ root = Path(service.project_dir).resolve()
9
+ if file_path is not None:
10
+ path = (root / file_path).resolve()
11
+ if not path.is_relative_to(root):
12
+ raise ValueError("Path must remain inside the selected repository")
13
+ file_path = path.relative_to(root).as_posix()
14
+ if line is not None and line < 1:
15
+ raise ValueError("line must be a positive definition start line")
16
+ ast = service._get_ast_service()
17
+ if (file_path is not None and file_path not in ast._ast_cache
18
+ and any(fi.relative_path == file_path for fi in ast._context_mgr._files)):
19
+ ast.ensure_file_parsed(file_path)
20
+ if file_path is not None:
21
+ # A top-level exact name elsewhere must not hide a method in this file.
22
+ definitions = ast.get_file_symbols(file_path)
23
+ definitions = [d for d in definitions if d.file_path == file_path
24
+ and (d.qualified_name == symbol_name or d.name == symbol_name
25
+ or d.qualified_name.endswith(f".{symbol_name}"))]
26
+ else:
27
+ definitions = ast.find_symbol(symbol_name, include_shadowed=line is not None)
28
+ return [d for d in definitions if line is None or d.start_line == line]
29
+
30
+
31
+ def location(definition):
32
+ return {key: getattr(definition, key) for key in
33
+ ("name", "qualified_name", "kind", "file_path", "start_line", "end_line")}
34
+
35
+
36
+ def scoped_references(service, symbol_name, file_path=None, line=None):
37
+ data = service.cross_references_data(symbol_name)
38
+ if file_path is not None or line is not None:
39
+ definitions = select_definitions(service, symbol_name, file_path, line)
40
+ data["definitions"] = [location(d) for d in definitions]
41
+ data["selection_unique"] = len(definitions) == 1
42
+ if not definitions:
43
+ data["references"] = []
44
+ data["total_references"] = 0
45
+ data["reference_candidates"] = []
46
+ data["total_reference_candidates"] = 0
47
+ elif len(definitions) == 1:
48
+ from attocode_intel.symbol_links import classify_symbol_references
49
+ linked, candidates = classify_symbol_references(service._get_ast_service(), definitions[0])
50
+ data["references"] = linked
51
+ data["total_references"] = len(linked)
52
+ data["reference_candidates"] = candidates[:20]
53
+ data["total_reference_candidates"] = len(candidates)
54
+ data["reference_caveat"] = (
55
+ "References are linked by same-file identity, explicit import bindings, or a language server. "
56
+ "Same-name syntax matches without that evidence remain reference_candidates."
57
+ )
58
+ data["references"] = sorted(data["references"], key=lambda r: (
59
+ r["file_path"], r["line"], r["ref_kind"], r["source"], r.get("symbol", "")))
60
+ seen = set()
61
+ unique = []
62
+ for ref in data["references"]:
63
+ identity = tuple(sorted((key, str(value)) for key, value in ref.items()))
64
+ if identity not in seen:
65
+ seen.add(identity)
66
+ unique.append(ref)
67
+ data["references"] = unique
68
+ return data
69
+
70
+
71
+ def inspect_symbol_data(service, symbol_name, file_path=None, line=None, source_start_line=None, task_hint=None):
72
+ definitions = select_definitions(service, symbol_name, file_path, line)
73
+ if len(definitions) != 1:
74
+ return {"status": "ambiguous" if definitions else "not_found", "symbol": symbol_name,
75
+ "definitions": [location(d) for d in definitions[:10]],
76
+ "total_definitions": len(definitions), "ambiguous": len(definitions) > 1,
77
+ "follow_up": {"tool": "inspect_symbol", "select": "file_path and start_line as line"}}
78
+ definition = definitions[0]
79
+ path = definition.file_path
80
+ root = Path(service.project_dir).resolve()
81
+ source_path = (root / path).resolve()
82
+ if not source_path.is_relative_to(root):
83
+ raise ValueError("Path must remain inside the selected repository")
84
+ # Check file identity across reading so an outside editor cannot pair stale lines with new text.
85
+ before = source_path.stat()
86
+ from attocode_intel.request_context import current_request
87
+ request = current_request.get()
88
+ tracker = request.stores.get("freshness") if request else None
89
+ expected = (tracker.manifest or {}).get(path) if tracker else None
90
+ if expected is not None and expected != (before.st_mtime_ns, before.st_ctime_ns, before.st_size):
91
+ raise ValueError("Source changed after indexing; retry inspection")
92
+ source = source_path.read_text(encoding="utf-8", errors="replace").splitlines()
93
+ after = source_path.stat()
94
+ if (before.st_mtime_ns, before.st_ctime_ns, before.st_size) != (
95
+ after.st_mtime_ns, after.st_ctime_ns, after.st_size):
96
+ raise ValueError("Source changed during inspection; retry")
97
+ from attocode_intel.tools.composite_tools import suggest_tests_data
98
+
99
+ references = scoped_references(service, symbol_name, path, definition.start_line)
100
+ imports = service.dependencies_data(path)
101
+ linked_test_files = {ref["file_path"] for ref in references["references"]}
102
+ tests = suggest_tests_data([path], symbol_name=definition.qualified_name, task_hint=task_hint,
103
+ linked_reference_files=linked_test_files)["candidates"]
104
+ if source_start_line is not None and (isinstance(source_start_line, bool) or int(source_start_line) != source_start_line):
105
+ raise ValueError("source_start_line must be an integer inside the selected definition")
106
+ start = max(1, definition.start_line) if source_start_line is None else int(source_start_line)
107
+ if not definition.start_line <= start <= min(len(source), definition.end_line):
108
+ raise ValueError("source_start_line must be inside the selected definition")
109
+ focused_preview = task_hint and source_start_line is None and definition.end_line - definition.start_line >= 60
110
+ end = min(len(source), definition.end_line, start + (11 if focused_preview else 59))
111
+ selection = {"symbol_name": definition.qualified_name, "file_path": path,
112
+ "line": definition.start_line}
113
+ relationships = ([{"file_path": p, "kind": "imports"} for p in imports["imports"]]
114
+ + [{"file_path": p, "kind": "imported_by"} for p in imports["imported_by"]])
115
+ result = {
116
+ "status": "found", "definition": location(definition),
117
+ "source": {"start_line": start, "end_line": end,
118
+ "text": "\n".join(source[start - 1:end]),
119
+ "truncated": start > definition.start_line or end < definition.end_line},
120
+ "references": references["references"][:8], "total_references": references["total_references"],
121
+ "reference_candidates": references.get("reference_candidates", [])[:5],
122
+ "total_reference_candidates": references.get("total_reference_candidates", 0),
123
+ "ambiguous": references["ambiguous"],
124
+ "reference_caveat": references.get("reference_caveat", "Syntax references are candidates; verify source."),
125
+ "imports": relationships[:5], "total_imports": len(relationships),
126
+ "tests": tests[:5], "total_tests": len(tests), "absence_proven": False,
127
+ "follow_up": {
128
+ "references": {"tool": "cross_references", **selection},
129
+ "imports": {"tool": "dependencies", "path": path},
130
+ "tests": {"tool": "suggest_tests", "files": [path], "symbol_name": definition.qualified_name,
131
+ **({"task_hint": task_hint} if task_hint else {})},
132
+ "source": {"file_path": path, "start_line": definition.start_line, "end_line": definition.end_line},
133
+ "next_source": {"tool": "inspect_symbol", **selection, "source_start_line": end + 1}
134
+ if end < definition.end_line else None,
135
+ },
136
+ }
137
+ if task_hint:
138
+ from attocode_intel.focused_evidence import source_excerpts
139
+ result["source_excerpts"] = source_excerpts(source, definition, task_hint, preview_end=end)
140
+ result["excerpt_selection"] = "Identifiers and executable literals ranked with enclosing syntax context; not exhaustive behavior coverage or data flow. Context ranges belong to their excerpt; cite each range separately."
141
+ return result
@@ -0,0 +1,167 @@
1
+ """Resolve selected-symbol evidence through local import bindings."""
2
+ from __future__ import annotations
3
+
4
+ from collections import deque
5
+ from pathlib import Path, PurePosixPath
6
+
7
+ from attocode_intel._internal.integrations.context.cross_references import SymbolRef
8
+ from attocode_intel._internal.integrations.context.syntax_evidence import reference_candidates
9
+
10
+
11
+ def _file_index(ast):
12
+ try:
13
+ from attocode_intel._internal.integrations.context.codebase_context import _build_file_index
14
+ return _build_file_index(ast._context_mgr._files, ast._root_dir)
15
+ except AttributeError:
16
+ return None
17
+
18
+
19
+ def _imports_from(ast, source, target, index):
20
+ """Return bindings in *source* whose module resolves to *target*."""
21
+ if index is None:
22
+ return []
23
+ try:
24
+ from attocode_intel._internal.integrations.context.codebase_context import IMPORT_RESOLVERS
25
+ file_ast = ast._ast_cache.get(source)
26
+ if file_ast is None:
27
+ ast.ensure_file_parsed(source)
28
+ file_ast = ast._ast_cache.get(source)
29
+ resolver = IMPORT_RESOLVERS.get(file_ast.language) if file_ast else None
30
+ return [imp for imp in (file_ast.imports if file_ast else [])
31
+ if resolver and resolver(imp.module, source, index) == target]
32
+ except (AttributeError, OSError, ValueError):
33
+ return []
34
+
35
+
36
+ def linked_names_by_file(ast, definition):
37
+ """Map importers to local names explicitly bound to one definition.
38
+
39
+ Reexports propagate the public name through TypeScript export clauses and
40
+ Python package initializers. Ordinary imports establish a binding only in
41
+ the importing file.
42
+ """
43
+ names = {definition.file_path: {definition.name, definition.qualified_name}}
44
+ index = _file_index(ast)
45
+ queue = deque([(definition.file_path, definition.name)])
46
+ visited = set(queue)
47
+ while queue:
48
+ target, exported_name = queue.popleft()
49
+ try:
50
+ dependents = ast.get_dependents(target)
51
+ except AttributeError:
52
+ break
53
+ for source in dependents:
54
+ for imp in _imports_from(ast, source, target, index):
55
+ imported = imp.names[-1] if imp.names else ("*" if imp.is_reexport else "")
56
+ if imported not in {exported_name, "*"}:
57
+ continue
58
+ local = imp.alias or (exported_name if imported == "*" else imported)
59
+ bound = names.setdefault(source, set())
60
+ bound.update({exported_name, imported, local})
61
+ reexport = imp.is_reexport or PurePosixPath(source).name == "__init__.py"
62
+ state = (source, local)
63
+ if reexport and local and state not in visited:
64
+ visited.add(state)
65
+ queue.append(state)
66
+ return names
67
+
68
+
69
+ def classify_symbol_references(ast, definition):
70
+ """Partition references into linked evidence and unresolved candidates."""
71
+ names_by_file = linked_names_by_file(ast, definition)
72
+ query_names = {definition.name, definition.qualified_name}
73
+ for names in names_by_file.values():
74
+ query_names.update(names)
75
+ for path in names_by_file:
76
+ try:
77
+ ast.ensure_references_indexed(path)
78
+ except AttributeError:
79
+ pass
80
+ candidates = []
81
+ seen = set()
82
+ for name in query_names:
83
+ try:
84
+ refs = ast.get_callers(name)
85
+ except AttributeError:
86
+ refs = []
87
+ for ref in refs:
88
+ identity = (ref.file_path, ref.line, ref.ref_kind, ref.source, ref.symbol_name,
89
+ getattr(ref, "syntax_name", ""))
90
+ if identity in seen:
91
+ continue
92
+ seen.add(identity)
93
+ candidates.append(ref)
94
+
95
+ # Calls through a public alias may not be in the global reference index:
96
+ # the alias itself is not a definition, so the normal indexer deliberately
97
+ # skips it. Scan only files that an explicit binding connected to the
98
+ # selected definition, keeping this bounded and structurally scoped.
99
+ try:
100
+ root = Path(ast._root_dir)
101
+ original_names = {definition.name, definition.qualified_name}
102
+ for path, local_names in names_by_file.items():
103
+ if not local_names - original_names:
104
+ continue
105
+ file_ast = ast._ast_cache.get(path)
106
+ if file_ast is None:
107
+ continue
108
+ content = (root / path).read_text(encoding="utf-8", errors="replace")
109
+ for written, kind, line in reference_candidates(str(root / path), content, file_ast.language) or []:
110
+ parts = written.split(".")
111
+ if not ({written, parts[0], parts[-1]} & local_names):
112
+ continue
113
+ ref = SymbolRef(symbol_name=written, ref_kind=kind, file_path=path,
114
+ line=line, source="tree-sitter")
115
+ identity = (ref.file_path, ref.line, ref.ref_kind, ref.source, ref.symbol_name,
116
+ getattr(ref, "syntax_name", ""))
117
+ if identity not in seen:
118
+ seen.add(identity)
119
+ candidates.append(ref)
120
+ except (AttributeError, OSError):
121
+ pass
122
+
123
+ try:
124
+ same_name_definitions = ast.find_symbol(definition.name)
125
+ definition_count = len({(item.file_path, item.qualified_name)
126
+ for item in same_name_definitions})
127
+ except AttributeError:
128
+ same_name_definitions = []
129
+ definition_count = 2
130
+ same_file_count = len({item.qualified_name for item in same_name_definitions
131
+ if item.file_path == definition.file_path})
132
+ selected_parent = (definition.qualified_name.rsplit(".", 1)[0]
133
+ if "." in definition.qualified_name else "")
134
+ linked, unresolved = [], []
135
+ for ref in candidates:
136
+ local_names = names_by_file.get(ref.file_path, set())
137
+ syntax_name = getattr(ref, "syntax_name", "") or ref.symbol_name
138
+ caller = getattr(ref, "caller_qualified_name", "")
139
+ if (ref.file_path == definition.file_path
140
+ and (same_file_count <= 1
141
+ or (selected_parent and caller.rsplit(".", 1)[0] == selected_parent))):
142
+ resolution = "same_file"
143
+ elif {syntax_name, syntax_name.split(".", 1)[0]} & local_names:
144
+ resolution = "import_binding"
145
+ elif ref.source == "lsp" and definition_count == 1:
146
+ resolution = "language_server"
147
+ else:
148
+ resolution = "same_name_candidate"
149
+ row = {"ref_kind": ref.ref_kind, "file_path": ref.file_path, "line": ref.line,
150
+ "source": ref.source, "symbol": ref.symbol_name, "resolution": resolution}
151
+ if syntax_name != ref.symbol_name:
152
+ row["syntax"] = syntax_name
153
+ (linked if resolution != "same_name_candidate" else unresolved).append(row)
154
+
155
+ by_site = {}
156
+ for row in unresolved + linked:
157
+ site = row["file_path"], row["line"], row["ref_kind"], row["source"]
158
+ existing = by_site.get(site)
159
+ if (existing is None or existing["resolution"] == "same_name_candidate"
160
+ or row["resolution"] != "same_name_candidate"):
161
+ by_site[site] = row
162
+ linked = [row for row in by_site.values() if row["resolution"] != "same_name_candidate"]
163
+ unresolved = [row for row in by_site.values() if row["resolution"] == "same_name_candidate"]
164
+ def order(row):
165
+ return row["file_path"], row["line"], row["ref_kind"], row["source"], row["symbol"]
166
+
167
+ return sorted(linked, key=order), sorted(unresolved, key=order)
@@ -0,0 +1,42 @@
1
+ """Opt-in timings outside model responses; never writes source or arguments."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ import os
7
+ import time
8
+ from contextlib import contextmanager
9
+ from contextvars import ContextVar
10
+ from pathlib import Path
11
+
12
+ timings: ContextVar[dict | None] = ContextVar("intelligence_timings", default=None)
13
+
14
+
15
+ @contextmanager
16
+ def measure(name):
17
+ start = time.monotonic()
18
+ try:
19
+ yield
20
+ finally:
21
+ current = timings.get()
22
+ if current is not None:
23
+ current[name] = round(current.get(name, 0) + (time.monotonic() - start) * 1000, 2)
24
+
25
+
26
+ def enabled():
27
+ return bool(os.environ.get("ATTOCODE_INTEL_TRACE"))
28
+
29
+
30
+ def emit(event: dict) -> None:
31
+ destination = os.environ.get("ATTOCODE_INTEL_TRACE")
32
+ if not destination:
33
+ return
34
+ try:
35
+ path = Path(destination).expanduser()
36
+ if not path.is_absolute():
37
+ return
38
+ fd = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
39
+ with os.fdopen(fd, "a") as stream:
40
+ stream.write(json.dumps(event, separators=(",", ":")) + "\n")
41
+ except OSError:
42
+ logging.getLogger(__name__).warning("Could not write intelligence timing trace")
@@ -0,0 +1,73 @@
1
+ """Symbol-focused test evidence, reusing the parsed index instead of scanning file bodies."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import PurePosixPath
5
+
6
+ from attocode_intel.focused_evidence import task_terms, terms
7
+
8
+
9
+ def is_test_path(path):
10
+ parts = PurePosixPath(path).parts
11
+ name = parts[-1] if parts else ""
12
+ return (any(p in {"test", "tests", "__tests__", "spec", "specs"} for p in parts)
13
+ or name.startswith("test_") or ".test." in name or ".spec." in name
14
+ or name.endswith(("_test.py", "_test.go", "Test.java", "Test.kt")))
15
+
16
+
17
+ def rank_symbol_tests(ast, suggestions, files, symbol_name, task_hint, distances,
18
+ linked_reference_files=None, relevance_by_file=None):
19
+ selected = [s for f in files for s in ast.get_file_symbols(f)
20
+ if s.file_path == f and (s.name == symbol_name or s.qualified_name == symbol_name)]
21
+ symbol_terms = terms(symbol_name or "")
22
+ hint_terms, excluded_terms = task_terms(task_hint or "")
23
+ relevance_by_file = relevance_by_file or {}
24
+ direct = set(linked_reference_files or ())
25
+ if linked_reference_files is None:
26
+ for symbol in selected:
27
+ from attocode_intel.symbol_links import classify_symbol_references
28
+ linked, _ = classify_symbol_references(ast, symbol)
29
+ direct.update(r["file_path"] for r in linked if is_test_path(r["file_path"]))
30
+ # Search indexed test names as well as paths. A generic suite filename can
31
+ # contain a focused regression; filename matching alone should not hide it.
32
+ matches = {}
33
+ for path in set(ast.index.file_symbols) | set(suggestions) | direct:
34
+ inline = path in files and any("Rust inline test" in r for r in suggestions.get(path, {}).get("reasons", []))
35
+ if not is_test_path(path) and not inline:
36
+ continue
37
+ path_terms = terms(PurePosixPath(path).stem)
38
+ names = set().union(*(terms(s.name) for s in ast.get_file_symbols(path) if s.file_path == path))
39
+ symbol_hits = symbol_terms & (path_terms | names)
40
+ hint_hits = hint_terms & (path_terms | names)
41
+ excluded_hits = excluded_terms & (path_terms | names)
42
+ if path not in suggestions and path not in direct and not symbol_hits and not hint_hits:
43
+ continue
44
+ reasons = []
45
+ if path in direct:
46
+ reasons.append(f"Reference linked to selected symbol `{symbol_name}` by import or language-server evidence")
47
+ if symbol_hits:
48
+ reasons.append("Test name/path matches symbol terms: " + ", ".join(sorted(symbol_hits)))
49
+ if hint_hits:
50
+ reasons.append("Test name/path matches task terms: " + ", ".join(sorted(hint_hits)))
51
+ info = suggestions.get(path, {"priority": 3, "reasons": []})
52
+ matches[path] = {"file_path": path, "priority": 1 if path in direct else info["priority"],
53
+ "reasons": (reasons + info["reasons"])[:3],
54
+ "evidence": {"selected_symbol_reference": path in direct,
55
+ "reference_resolution": "linked" if path in direct else None,
56
+ "symbol_terms": sorted(symbol_hits), "task_terms": sorted(hint_hits),
57
+ "import_distance": distances.get(path)},
58
+ "_excluded_hits": len(excluded_hits)}
59
+ def order(row):
60
+ evidence = row["evidence"]
61
+ # Selected-symbol references are strongest, followed by an actual reverse
62
+ # import path. Lexical matches remain useful candidates after graph evidence.
63
+ return (not evidence["selected_symbol_reference"],
64
+ evidence["import_distance"] is None,
65
+ row["_excluded_hits"] > 0,
66
+ -relevance_by_file.get(row["file_path"], 0.0),
67
+ -(len(evidence["task_terms"]) + len(evidence["symbol_terms"])),
68
+ row["priority"], evidence["import_distance"] if evidence["import_distance"] is not None else float("inf"),
69
+ row["file_path"].startswith("docs/"), row["file_path"])
70
+ ranked = sorted(matches.values(), key=order)
71
+ for row in ranked:
72
+ row.pop("_excluded_hits")
73
+ return ranked
@@ -0,0 +1,43 @@
1
+ """Testing utilities for attocode-code-intel.
2
+
3
+ This module provides standardized fixtures, mocks, and helpers for
4
+ testing code intelligence tools and MCP integrations.
5
+
6
+ Example usage in tests::
7
+
8
+ import pytest
9
+ from attocode_intel.testing import (
10
+ code_intel_service,
11
+ ast_service,
12
+ sample_project,
13
+ MockServiceFactory,
14
+ create_sample_project,
15
+ )
16
+
17
+ class TestSearchTools:
18
+ @pytest.fixture(autouse=True)
19
+ def setup(self, code_intel_service, ast_service):
20
+ self.service = code_intel_service
21
+ self.ast = ast_service
22
+
23
+ def test_symbol_search(self):
24
+ result = search_tools.symbol_search(symbol_name="foo")
25
+ assert "foo" in result
26
+ """
27
+
28
+ from attocode_intel.testing.fixtures import (
29
+ ast_service,
30
+ code_intel_service,
31
+ sample_project,
32
+ )
33
+ from attocode_intel.testing.mocks import MockServiceFactory
34
+ from attocode_intel.testing.helpers import create_sample_project, get_tool_names
35
+
36
+ __all__ = [
37
+ "ast_service",
38
+ "code_intel_service",
39
+ "sample_project",
40
+ "MockServiceFactory",
41
+ "create_sample_project",
42
+ "get_tool_names",
43
+ ]
@@ -0,0 +1,122 @@
1
+ """Standardized pytest fixtures for code intelligence testing.
2
+
3
+ Provides reusable fixtures for CodeIntelService, ASTService, and
4
+ related components. These fixtures handle initialization, cleanup,
5
+ and reset of singletons to ensure test isolation.
6
+
7
+ For real repos, set ATTOCODE_PROJECT_DIR env var before running tests.
8
+
9
+ Usage::
10
+
11
+ import pytest
12
+ from attocode_intel.testing.fixtures import code_intel_service, ast_service
13
+
14
+ class TestMyTool:
15
+ def test_something(self, code_intel_service, ast_service):
16
+ assert code_intel_service is not None
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING
23
+
24
+ import pytest
25
+
26
+ if TYPE_CHECKING:
27
+ from attocode_intel.service import CodeIntelService
28
+ from attocode_intel._internal.integrations.context.ast_service import ASTService
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Service fixtures
33
+ # ---------------------------------------------------------------------------
34
+
35
+
36
+ @pytest.fixture
37
+ def sample_project(tmp_path: Path) -> Path:
38
+ """Create a minimal Python project in tmp_path for testing.
39
+
40
+ Creates:
41
+ - src/main.py, src/utils.py
42
+ - tests/test_main.py
43
+ - pyproject.toml
44
+ """
45
+ src = tmp_path / "src"
46
+ tests = tmp_path / "tests"
47
+
48
+ src.mkdir(parents=True, exist_ok=True)
49
+ tests.mkdir(parents=True, exist_ok=True)
50
+
51
+ (src / "__init__.py").write_text("")
52
+ (src / "main.py").write_text(
53
+ 'import os\nfrom src.utils import helper\n\ndef main():\n """Entry point."""\n return helper(42)\n\ndef cli(args: list) -> int:\n """CLI interface."""\n return 0\n'
54
+ )
55
+ (src / "utils.py").write_text(
56
+ 'import os\n\ndef helper(value: int) -> str:\n """Process a value."""\n return str(value)\n\nclass BaseProcessor:\n """Base class."""\n def process(self) -> None: pass\n'
57
+ )
58
+ (tests / "__init__.py").write_text("")
59
+ (tests / "test_main.py").write_text(
60
+ 'import pytest\nfrom src.main import main\n\ndef test_main():\n assert main() == "42"\n'
61
+ )
62
+ (tmp_path / "pyproject.toml").write_text(
63
+ '[project]\nname = "test"\nversion = "0.1.0"\n'
64
+ )
65
+
66
+ return tmp_path
67
+
68
+
69
+ @pytest.fixture
70
+ def code_intel_service(
71
+ sample_project: Path,
72
+ monkeypatch: pytest.MonkeyPatch,
73
+ ) -> "CodeIntelService":
74
+ """Provide a CodeIntelService instance for the sample project.
75
+
76
+ Resets all singletons before and after the test to ensure isolation.
77
+ """
78
+ import attocode_intel._shared as ci_shared
79
+ from attocode_intel.service import CodeIntelService
80
+
81
+ monkeypatch.setenv("ATTOCODE_PROJECT_DIR", str(sample_project))
82
+
83
+ CodeIntelService._reset_instances()
84
+ ci_shared._service = None
85
+ ci_shared._remote_service = None
86
+
87
+ service = CodeIntelService.get_instance(str(sample_project))
88
+
89
+ yield service
90
+
91
+ CodeIntelService._reset_instances()
92
+ ci_shared._service = None
93
+ ci_shared._remote_service = None
94
+ monkeypatch.delenv("ATTOCODE_PROJECT_DIR", raising=False)
95
+
96
+
97
+ @pytest.fixture
98
+ def ast_service(
99
+ sample_project: Path,
100
+ monkeypatch: pytest.MonkeyPatch,
101
+ ) -> "ASTService":
102
+ """Provide an ASTService instance for the sample project.
103
+
104
+ Initializes skeleton indexing for fast test execution.
105
+ """
106
+ import attocode_intel._shared as ci_shared
107
+ from attocode_intel._internal.integrations.context.ast_service import ASTService
108
+
109
+ monkeypatch.setenv("ATTOCODE_PROJECT_DIR", str(sample_project))
110
+
111
+ ASTService.clear_instances()
112
+ ci_shared._ast_service = None
113
+
114
+ service = ASTService.get_instance(str(sample_project))
115
+ if not service.initialized:
116
+ service.initialize_skeleton(indexing_depth="minimal")
117
+
118
+ yield service
119
+
120
+ ASTService.clear_instances()
121
+ ci_shared._ast_service = None
122
+ monkeypatch.delenv("ATTOCODE_PROJECT_DIR", raising=False)
@@ -0,0 +1,75 @@
1
+ """Helper utilities for code intelligence testing.
2
+
3
+ Provides utility functions for creating test projects and
4
+ common test patterns. For real repos, use ATTOCODE_PROJECT_DIR.
5
+
6
+ Usage::
7
+
8
+ from attocode_intel.testing.helpers import create_sample_project
9
+
10
+ # Create a project with custom files
11
+ project = create_sample_project(tmp_path, {
12
+ "src/main.py": "def foo(): pass",
13
+ "src/utils.py": "def bar(): pass",
14
+ })
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+
21
+
22
+ def create_sample_project(
23
+ tmp_path: Path,
24
+ files: dict[str, str],
25
+ *,
26
+ with_git: bool = False,
27
+ ) -> Path:
28
+ """Create a sample project with custom file structure.
29
+
30
+ Args:
31
+ tmp_path: The parent temporary directory.
32
+ files: Dict mapping file paths (relative to project root)
33
+ to their contents.
34
+ with_git: Whether to create a .git directory.
35
+
36
+ Returns:
37
+ Path to the created project root.
38
+
39
+ Example::
40
+
41
+ project = create_sample_project(tmp_path, {
42
+ "src/main.py": "def main(): pass",
43
+ "src/utils.py": "def helper(): pass",
44
+ "tests/test_main.py": "def test_main(): pass",
45
+ })
46
+ """
47
+ for file_path, content in files.items():
48
+ full_path = tmp_path / file_path
49
+ full_path.parent.mkdir(parents=True, exist_ok=True)
50
+ full_path.write_text(content)
51
+
52
+ if with_git:
53
+ git_dir = tmp_path / ".git"
54
+ git_dir.mkdir(parents=True, exist_ok=True)
55
+ (git_dir / "config").write_text("[core]\n\tautocrlf = true\n")
56
+
57
+ return tmp_path
58
+
59
+
60
+ def get_tool_names() -> list[str]:
61
+ """Return the list of MCP tool module names.
62
+
63
+ Returns:
64
+ List of tool module names (without .py extension).
65
+ """
66
+ from attocode_intel import tools as tools_module
67
+
68
+ tool_dir = Path(tools_module.__file__).parent
69
+ return [f.stem for f in tool_dir.glob("*.py") if not f.stem.startswith("_")]
70
+
71
+
72
+ __all__ = [
73
+ "create_sample_project",
74
+ "get_tool_names",
75
+ ]