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,992 @@
1
+ """Rule-based analysis tools for the code-intel MCP server.
2
+
3
+ Tools:
4
+ Core: analyze, list_rules, list_packs, install_pack, register_rule
5
+ Testing: test_rules
6
+ CI: ci_scan
7
+ Feedback: rule_stats, rule_feedback
8
+ Community: search_community_packs, install_community_pack, validate_pack_tool
9
+ Import: import_rules
10
+
11
+ These expose the pluggable rule engine to the connected coding agent,
12
+ providing rich, pre-filtered, context-laden findings that enable the
13
+ agent to do LLMCheck-quality validation natively.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import os
20
+ import threading
21
+ from typing import TYPE_CHECKING
22
+
23
+ from attocode_intel._shared import _get_project_dir, mcp
24
+
25
+ if TYPE_CHECKING:
26
+ from attocode_intel.rules.registry import RuleRegistry
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Lazy singleton for the rule registry
31
+ _registry: RuleRegistry | None = None
32
+ _registry_loaded = False
33
+ _registry_lock = threading.Lock()
34
+
35
+
36
+ def _get_registry() -> RuleRegistry:
37
+ """Get or create the global rule registry (lazy singleton)."""
38
+ from attocode_intel.request_context import current_request, scoped_store
39
+ if current_request.get() is not None:
40
+ return scoped_store("rules", _build_scoped_registry)
41
+ global _registry, _registry_loaded
42
+ # Read-only fast path (safe: both reads are atomic in CPython;
43
+ # the lock below re-checks to avoid races with install_pack resets)
44
+ reg = _registry
45
+ if reg is not None and _registry_loaded:
46
+ return reg
47
+ with _registry_lock:
48
+ if _registry is not None and _registry_loaded:
49
+ return _registry
50
+
51
+ from attocode_intel.rules.registry import RuleRegistry
52
+ from attocode_intel.rules.loader import load_builtin_rules, load_user_rules
53
+ from attocode_intel.rules.packs.pack_loader import load_all_packs
54
+ from attocode_intel.rules.plugins.plugin_loader import load_all_plugins
55
+ from attocode_intel.rules.hygiene import apply_persistent_disable
56
+ from attocode_intel.rules.profiling import FeedbackStore
57
+
58
+ reg = RuleRegistry()
59
+ project_dir = _get_project_dir()
60
+
61
+ # Load builtins
62
+ builtins = load_builtin_rules()
63
+ reg.register_many(builtins)
64
+
65
+ # Load language packs (builtin + user packs from .attocode/packs/)
66
+ pack_manifests, pack_rules = load_all_packs(project_dir)
67
+ reg.register_many(pack_rules)
68
+
69
+ # Load user plugins (from .attocode/plugins/)
70
+ plugin_count = 0
71
+ if project_dir:
72
+ plugin_manifests, plugin_rules = load_all_plugins(project_dir)
73
+ reg.register_many(plugin_rules)
74
+ plugin_count = len(plugin_rules)
75
+
76
+ # Load user rules (from .attocode/rules/)
77
+ user_count = 0
78
+ if project_dir:
79
+ user_rules = load_user_rules(project_dir)
80
+ reg.register_many(user_rules)
81
+ user_count = len(user_rules)
82
+
83
+ # Re-apply previously persisted hygiene auto-disable decisions so
84
+ # dead/noisy rules pruned in earlier sessions stay disabled.
85
+ hygiene_disabled = 0
86
+ if project_dir:
87
+ hygiene_disabled = apply_persistent_disable(reg, FeedbackStore(project_dir))
88
+
89
+ # Assign pointer LAST — fully populated before visible
90
+ _registry = reg
91
+ _registry_loaded = True
92
+
93
+ logger.info(
94
+ "Rule registry: %d rules (%d builtin, %d from %d packs, %d plugins, %d user); "
95
+ "%d hygiene-disabled",
96
+ reg.count,
97
+ len(builtins),
98
+ len(pack_rules),
99
+ len(pack_manifests),
100
+ plugin_count,
101
+ user_count,
102
+ hygiene_disabled,
103
+ )
104
+ return _registry
105
+
106
+
107
+ def _collect_files(
108
+ files: list[str] | None,
109
+ path: str,
110
+ project_dir: str,
111
+ ) -> list[str]:
112
+ """Resolve file list from explicit files or path glob.
113
+
114
+ All resolved paths are checked for containment within project_dir
115
+ to prevent path traversal attacks via the HTTP API.
116
+ """
117
+ root = os.path.realpath(project_dir)
118
+
119
+ def _is_within_project(abs_path: str) -> bool:
120
+ real = os.path.realpath(abs_path)
121
+ return real == root or real.startswith(root + os.sep)
122
+
123
+ if files:
124
+ result = []
125
+ for f in files:
126
+ abs_f = os.path.realpath(
127
+ f if os.path.isabs(f) else os.path.join(project_dir, f)
128
+ )
129
+ if not _is_within_project(abs_f):
130
+ logger.warning("Skipping path outside project root: %s", f)
131
+ continue
132
+ if os.path.isfile(abs_f):
133
+ result.append(abs_f)
134
+ return result
135
+
136
+ # Walk path (or whole project)
137
+ scan_dir = os.path.realpath(
138
+ os.path.join(project_dir, path) if path else project_dir
139
+ )
140
+ if not _is_within_project(scan_dir):
141
+ logger.warning("Scan path escapes project root: %s", path)
142
+ return []
143
+ if not os.path.isdir(scan_dir):
144
+ return []
145
+
146
+ from attocode_intel.rules.executor import _EXT_LANG
147
+
148
+ _SKIP_DIRS = frozenset({
149
+ ".git", "node_modules", "__pycache__", ".venv", "venv",
150
+ ".tox", "dist", "build", ".next", ".nuxt", ".attocode",
151
+ })
152
+
153
+ result = []
154
+ for dirpath, dirnames, filenames in os.walk(scan_dir):
155
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
156
+ for fname in filenames:
157
+ ext = os.path.splitext(fname)[1].lower()
158
+ if ext in _EXT_LANG:
159
+ result.append(os.path.join(dirpath, fname))
160
+ return result
161
+
162
+
163
+ def _analyze_impl(
164
+ files: list[str] | None = None,
165
+ path: str = "",
166
+ language: str = "",
167
+ category: str = "",
168
+ severity: str = "",
169
+ pack: str = "",
170
+ min_confidence: float = 0.5,
171
+ max_findings: int = 50,
172
+ project_dir: str = "",
173
+ ) -> str:
174
+ """Internal implementation — accepts explicit project_dir for service layer."""
175
+ if not project_dir:
176
+ project_dir = _get_project_dir()
177
+ reg = _get_registry()
178
+
179
+ # Query applicable rules
180
+ from attocode_intel import confidence
181
+ from attocode_intel.confidence.settings import workspace
182
+
183
+ with workspace(project_dir):
184
+ threshold = confidence.rule_threshold(min_confidence)
185
+ rules = reg.query(
186
+ language=language,
187
+ category=category,
188
+ severity=severity,
189
+ pack=pack,
190
+ min_confidence=threshold,
191
+ )
192
+
193
+ if not rules:
194
+ return "No rules match the given filters."
195
+
196
+ # Collect files
197
+ file_list = _collect_files(files, path, project_dir)
198
+ if not file_list:
199
+ return "No scannable source files found."
200
+
201
+ # Execute
202
+ from attocode_intel.rules.executor import execute_rules
203
+ from attocode_intel.rules.enricher import enrich_findings
204
+ from attocode_intel.rules.filters.pipeline import run_pipeline
205
+ from attocode_intel.rules.formatter import format_findings
206
+
207
+ findings = execute_rules(file_list, rules, project_dir=project_dir)
208
+
209
+ # Persist per-rule scan/match counters so the hygiene tool can
210
+ # identify dead rules across sessions. Counts pre-filter findings —
211
+ # post-filter would under-count dead rules whose only matches got
212
+ # dropped by the test-file/confidence pipeline.
213
+ if project_dir:
214
+ from attocode_intel.rules.profiling import FeedbackStore
215
+
216
+ match_counts: dict[str, int] = {}
217
+ for f in findings:
218
+ match_counts[f.rule_id] = match_counts.get(f.rule_id, 0) + 1
219
+ FeedbackStore(project_dir).record_session(
220
+ rule_ids=[r.qualified_id for r in rules],
221
+ matches=match_counts,
222
+ files_scanned=len(file_list),
223
+ )
224
+
225
+ # Pre-filter pipeline (dedup, test-file adjustment, confidence threshold)
226
+ findings = run_pipeline(findings, min_confidence=min_confidence, project_dir=project_dir)
227
+
228
+ # Enrich with context
229
+ enrich_findings(findings, project_dir=project_dir)
230
+
231
+ # Format for agent
232
+ text = format_findings(findings, max_findings=max_findings)
233
+ return text + ("\n\n" + confidence.summary() if confidence.summary() else "")
234
+
235
+
236
+ @mcp.tool()
237
+ def analyze(
238
+ files: list[str] | None = None,
239
+ path: str = "",
240
+ language: str = "",
241
+ category: str = "",
242
+ severity: str = "",
243
+ pack: str = "",
244
+ min_confidence: float = 0.5,
245
+ max_findings: int = 50,
246
+ ) -> str:
247
+ """Run rule-based analysis on source files with rich context for reasoning.
248
+
249
+ Returns structured findings with: code context (10 lines before/after),
250
+ antipattern explanations, few-shot examples, suggested fixes, confidence
251
+ scores, and CWE references. Designed to give you everything needed to
252
+ triage, fix, and explain issues.
253
+
254
+ Language-specific rules require installing a pack first via install_pack().
255
+ Builtin security rules (secrets, CWE patterns) are always active.
256
+
257
+ Args:
258
+ files: Specific file paths to analyze (relative or absolute).
259
+ If empty, scans all source files under path.
260
+ path: Directory to scan (relative to project root). Default: entire project.
261
+ language: Filter rules to this language (e.g. "python", "go").
262
+ category: Filter by category: correctness, suspicious, complexity,
263
+ performance, style, security, deprecated.
264
+ severity: Filter by minimum severity: critical, high, medium, low, info.
265
+ pack: Filter rules from a specific language pack.
266
+ min_confidence: Minimum confidence threshold (0.0-1.0). Default 0.5.
267
+ max_findings: Maximum findings to return. Default 50.
268
+ """
269
+ return _analyze_impl(
270
+ files=files, path=path, language=language, category=category,
271
+ severity=severity, pack=pack, min_confidence=min_confidence,
272
+ max_findings=max_findings,
273
+ )
274
+
275
+
276
+ @mcp.tool()
277
+ def list_rules(
278
+ language: str = "",
279
+ category: str = "",
280
+ severity: str = "",
281
+ pack: str = "",
282
+ verbose: bool = False,
283
+ ) -> str:
284
+ """List available analysis rules with optional filters.
285
+
286
+ Shows all registered rules grouped by category, with severity tags.
287
+ Use verbose=True to include rule descriptions.
288
+
289
+ Args:
290
+ language: Filter to rules for this language.
291
+ category: Filter by category (correctness, security, performance, etc.).
292
+ severity: Filter by severity level.
293
+ pack: Filter to rules from a specific language pack.
294
+ verbose: Include rule descriptions in output.
295
+ """
296
+ reg = _get_registry()
297
+ rules = reg.query(
298
+ language=language,
299
+ category=category,
300
+ severity=severity,
301
+ pack=pack,
302
+ enabled_only=False,
303
+ )
304
+
305
+ from attocode_intel.rules.formatter import format_rules_list
306
+ output = format_rules_list(rules, verbose=verbose)
307
+
308
+ # Append stats
309
+ stats = reg.stats()
310
+ output += f"\n---\nTotal: {stats['total']} rules | "
311
+ output += f"Enabled: {stats['enabled']} | "
312
+ output += f"Languages: {stats['languages']} | "
313
+ output += f"Packs: {stats['packs']}"
314
+
315
+ return output
316
+
317
+
318
+ @mcp.tool()
319
+ def list_packs() -> str:
320
+ """List installed and available example language packs.
321
+
322
+ Shows installed packs (active, from .attocode/packs/) and available
323
+ example packs that can be installed with install_pack().
324
+
325
+ Language packs provide language-specific rules for performance
326
+ antipatterns, correctness bugs, security issues, and idiomatic style.
327
+ They are NOT auto-loaded — install the ones relevant to your project.
328
+ """
329
+ from attocode_intel.rules.packs.pack_loader import list_example_packs
330
+ from attocode_intel.rules.formatter import format_packs_list
331
+
332
+ reg = _get_registry()
333
+ pack_names = reg.packs()
334
+
335
+ lines: list[str] = []
336
+
337
+ # Installed packs
338
+ if pack_names:
339
+ packs = []
340
+ for name in pack_names:
341
+ pack_rules = reg.query(pack=name, enabled_only=False)
342
+ packs.append({
343
+ "name": name,
344
+ "languages": sorted(set(
345
+ lang for r in pack_rules for lang in r.languages
346
+ )),
347
+ "rules_count": len(pack_rules),
348
+ "description": "",
349
+ })
350
+ lines.append(format_packs_list(packs))
351
+ else:
352
+ lines.append("## Installed Packs: none\n")
353
+ lines.append("No packs installed. Use `install_pack(name)` to activate one.\n")
354
+
355
+ # Available examples
356
+ examples = list_example_packs()
357
+ if examples:
358
+ lines.append("## Available Example Packs\n")
359
+ lines.append("These ship with attocode but are NOT auto-loaded.")
360
+ lines.append("Install with `install_pack(name)` to activate.\n")
361
+ for ex in examples:
362
+ langs = ", ".join(ex.languages)
363
+ lines.append(f"- **{ex.name}** ({langs}) — {ex.description}")
364
+ lines.append("")
365
+
366
+ return "\n".join(lines)
367
+
368
+
369
+ @mcp.tool()
370
+ def install_pack(name: str) -> str:
371
+ """Install an example language pack into this project.
372
+
373
+ Copies the pack from attocode's shipped examples into
374
+ .attocode/packs/<name>/ where it becomes active. You can then
375
+ customize the rules in .attocode/packs/<name>/rules/*.yaml.
376
+
377
+ Available packs: go, python, typescript, rust, java.
378
+
379
+ Args:
380
+ name: Pack name to install (e.g. "go", "python").
381
+ """
382
+ from attocode_intel.rules.packs.pack_loader import install_pack as _install
383
+
384
+ project_dir = _get_project_dir()
385
+ result = _install(name, project_dir)
386
+
387
+ # Reload registry to pick up new pack
388
+ global _registry, _registry_loaded
389
+ with _registry_lock:
390
+ _registry = None
391
+ _registry_loaded = False
392
+
393
+ return result
394
+
395
+
396
+ @mcp.tool()
397
+ def register_rule(yaml_content: str) -> str:
398
+ """Register a custom analysis rule at runtime from YAML.
399
+
400
+ The rule is added to the registry immediately and will be used
401
+ by subsequent analyze() calls. Rules persist for the session.
402
+
403
+ For permanent rules, add YAML files to .attocode/rules/.
404
+
405
+ Format:
406
+ id: my-rule-name
407
+ pattern: "regex_pattern"
408
+ message: "What was detected"
409
+ severity: high
410
+ category: performance
411
+ languages: [python]
412
+ recommendation: "How to fix"
413
+ explanation: "Why this matters"
414
+ examples:
415
+ - bad: "slow_code()"
416
+ good: "fast_code()"
417
+ explanation: "Why good is better"
418
+
419
+ Args:
420
+ yaml_content: YAML rule definition (single rule or list).
421
+ """
422
+ try:
423
+ import yaml # type: ignore[import-untyped]
424
+ except ImportError:
425
+ return "Error: PyYAML not installed. Run: pip install pyyaml"
426
+
427
+ from attocode_intel.rules.loader import _parse_yaml_rule
428
+ from attocode_intel.rules.model import RuleSource
429
+
430
+ data = yaml.safe_load(yaml_content)
431
+ if data is None:
432
+ return "Error: Empty YAML content."
433
+
434
+ items = [data] if isinstance(data, dict) else data if isinstance(data, list) else []
435
+ if not items:
436
+ return "Error: YAML must be a dict (single rule) or list (multiple rules)."
437
+
438
+ reg = _get_registry()
439
+ registered = 0
440
+ errors: list[str] = []
441
+
442
+ for i, item in enumerate(items):
443
+ if not isinstance(item, dict):
444
+ errors.append(f"Rule at index {i}: expected dict, got {type(item).__name__}")
445
+ continue
446
+ rule = _parse_yaml_rule(item, source=RuleSource.USER, origin=f"runtime[{i}]")
447
+ if rule is None:
448
+ errors.append(f"Rule at index {i}: invalid (check id, pattern, message, severity)")
449
+ else:
450
+ reg.register(rule)
451
+ registered += 1
452
+
453
+ parts = [f"Registered {registered} rule(s)."]
454
+ if errors:
455
+ parts.append("Errors:\n" + "\n".join(f" - {e}" for e in errors))
456
+ parts.append(f"Registry now has {reg.count} rules total.")
457
+
458
+ return "\n".join(parts)
459
+
460
+
461
+ @mcp.tool()
462
+ def test_rules(
463
+ fixtures_dir: str = "",
464
+ rule_id: str = "",
465
+ ) -> str:
466
+ """Run rule tests against fixture files with expect/ok/todoruleid annotations.
467
+
468
+ Fixture files should contain inline annotations:
469
+ # expect: rule-id — rule MUST fire on this line
470
+ # ok: rule-id — rule must NOT fire on this line (false-positive guard)
471
+ # todoruleid: rule-id — rule SHOULD fire but is known-missing
472
+
473
+ Args:
474
+ fixtures_dir: Directory containing annotated test files.
475
+ Default: .attocode/test_fixtures/ or tests/fixtures/rule_violations/.
476
+ rule_id: Optional — only test this specific rule.
477
+ """
478
+ from attocode_intel.rules.testing import (
479
+ RuleTestRunner,
480
+ format_test_report,
481
+ )
482
+
483
+ reg = _get_registry()
484
+ project_dir = _get_project_dir()
485
+
486
+ # Resolve fixtures directory
487
+ if not fixtures_dir:
488
+ import os
489
+ for candidate in [
490
+ os.path.join(project_dir, ".attocode", "test_fixtures"),
491
+ os.path.join(project_dir, "tests", "fixtures", "rule_violations"),
492
+ ]:
493
+ if os.path.isdir(candidate):
494
+ fixtures_dir = candidate
495
+ break
496
+ if not fixtures_dir:
497
+ return "No test fixtures directory found. Provide fixtures_dir or create .attocode/test_fixtures/."
498
+
499
+ if rule_id:
500
+ rule = reg.get(rule_id)
501
+ if rule is None:
502
+ return f"Rule '{rule_id}' not found in registry."
503
+ rules = [rule]
504
+ else:
505
+ rules = reg.all_rules(enabled_only=False)
506
+
507
+ runner = RuleTestRunner(rules, project_dir=project_dir)
508
+ suite = runner.run_test_suite(fixtures_dir)
509
+
510
+ if not suite.file_results:
511
+ return f"No annotated test files found in {fixtures_dir}."
512
+
513
+ return format_test_report(suite)
514
+
515
+
516
+ @mcp.tool()
517
+ def ci_scan(
518
+ path: str = "",
519
+ language: str = "",
520
+ category: str = "",
521
+ fail_on: str = "high",
522
+ diff_only: bool = False,
523
+ output_format: str = "summary",
524
+ ) -> str:
525
+ """Run CI-style rule scan with exit-code semantics and SARIF output.
526
+
527
+ Designed for CI/CD pipelines: scans source files, applies rules,
528
+ and reports findings with pass/fail status. Supports diff-only mode
529
+ to scan only changed lines since a baseline git ref.
530
+
531
+ Args:
532
+ path: Directory to scan (relative to project root). Default: entire project.
533
+ language: Filter rules to this language.
534
+ category: Filter by category (correctness, security, etc.).
535
+ fail_on: Minimum severity to trigger failure: critical, high, medium, low.
536
+ diff_only: Only report findings on lines changed since baseline.
537
+ output_format: Output format — "summary" (human-readable),
538
+ "sarif" (SARIF JSON), "annotations" (GitHub Actions format).
539
+ """
540
+ from attocode_intel.rules.ci import (
541
+ CIConfig, CIRunner, format_ci_summary, format_github_annotations,
542
+ )
543
+ from attocode_intel.rules.sarif import findings_to_sarif, sarif_to_json
544
+ from attocode_intel.rules.model import RuleSeverity
545
+
546
+ project_dir = _get_project_dir()
547
+
548
+ config = CIConfig()
549
+ sev_str = fail_on.lower()
550
+ if sev_str in {s.value for s in RuleSeverity}:
551
+ config.fail_on = RuleSeverity(sev_str)
552
+
553
+ runner = CIRunner(project_dir, config=config)
554
+ result = runner.run(path=path, language=language, category=category, diff_only=diff_only)
555
+
556
+ if output_format == "sarif":
557
+ sarif = findings_to_sarif(result.findings)
558
+ return sarif_to_json(sarif)
559
+ elif output_format == "annotations":
560
+ annotations = format_github_annotations(result.findings)
561
+ status = "PASS" if result.passed else "FAIL"
562
+ return f"Status: {status}\n{annotations}"
563
+ else:
564
+ return format_ci_summary(result)
565
+
566
+
567
+ @mcp.tool()
568
+ def rule_stats(rule_id: str = "") -> str:
569
+ """Show profiling stats and confidence calibration for rules.
570
+
571
+ Shows execution time, match count, true/false positive feedback,
572
+ and calibrated confidence from persistent feedback data.
573
+
574
+ Args:
575
+ rule_id: Specific rule ID, or empty for all rules with feedback.
576
+ """
577
+ from attocode_intel.rules.profiling import (
578
+ FeedbackStore, RuleStats, format_rule_stats,
579
+ )
580
+
581
+ project_dir = _get_project_dir()
582
+ store = FeedbackStore(project_dir)
583
+ feedback = store.all_feedback()
584
+
585
+ if not feedback:
586
+ return "No rule feedback recorded yet. Use rule_feedback() to record TP/FP observations."
587
+
588
+ def _as_int(value: object) -> int:
589
+ return int(value) if isinstance(value, (int, float)) else 0
590
+
591
+ # Build stats from feedback data
592
+ stats: dict[str, RuleStats] = {}
593
+ for rid, fb in feedback.items():
594
+ if rule_id and rid != rule_id:
595
+ continue
596
+ s = RuleStats(
597
+ rule_id=rid,
598
+ true_positives=_as_int(fb.get("tp", 0)),
599
+ false_positives=_as_int(fb.get("fp", 0)),
600
+ )
601
+ stats[rid] = s
602
+
603
+ if rule_id and not stats:
604
+ return f"No feedback for rule '{rule_id}'."
605
+
606
+ return format_rule_stats(stats, feedback)
607
+
608
+
609
+ @mcp.tool()
610
+ def rule_feedback(
611
+ rule_id: str,
612
+ is_true_positive: bool,
613
+ finding_line: int = 0,
614
+ ) -> str:
615
+ """Record whether a finding was a true or false positive.
616
+
617
+ This feedback is used to calibrate rule confidence scores over time.
618
+ After 5+ observations, a calibrated confidence replaces the rule's
619
+ default confidence for more accurate triage.
620
+
621
+ Args:
622
+ rule_id: The rule that produced the finding.
623
+ is_true_positive: True if the finding was a real issue, False if it was a false positive.
624
+ finding_line: Optional line number for context (not used in calibration).
625
+ """
626
+ from attocode_intel.rules.profiling import FeedbackStore
627
+
628
+ project_dir = _get_project_dir()
629
+ store = FeedbackStore(project_dir)
630
+ store.record(rule_id, is_true_positive=is_true_positive)
631
+
632
+ fb = store.get_feedback(rule_id)
633
+ tp, fp = fb.get("tp", 0), fb.get("fp", 0)
634
+ total = tp + fp
635
+ cal = store.get_calibrated_confidence(rule_id)
636
+
637
+ parts = [f"Recorded {'TP' if is_true_positive else 'FP'} for `{rule_id}`."]
638
+ parts.append(f"Total: {tp} TP, {fp} FP ({total} observations)")
639
+ if cal is not None:
640
+ parts.append(f"Calibrated confidence: {cal:.1%}")
641
+ else:
642
+ remaining = 5 - total
643
+ parts.append(f"Need {remaining} more observation(s) for calibration.")
644
+
645
+ return "\n".join(parts)
646
+
647
+
648
+ @mcp.tool()
649
+ def evolve_rules(
650
+ target_rule_ids: list[str],
651
+ fixtures_dir: str = "",
652
+ generations: int = 20,
653
+ population_size: int = 12,
654
+ plateau_gens: int = 5,
655
+ ) -> str:
656
+ """Evolutionary optimisation of one or more rules against a labelled corpus.
657
+
658
+ Uses tournament selection (k=3) + 10% elitism + per-generation
659
+ mutation/crossover. Fitness is precision + recall + speed bonus.
660
+ Stops on ``max_generations`` or when the best composite plateaus
661
+ for ``plateau_gens`` consecutive generations.
662
+
663
+ Args:
664
+ target_rule_ids: Qualified rule IDs to seed the population with.
665
+ Each must be in the active registry.
666
+ fixtures_dir: Directory of inline-annotated source files (same
667
+ format as ``test_rules``). Defaults to
668
+ ``.attocode/test_fixtures/`` if empty.
669
+ generations: Maximum generations to run.
670
+ population_size: Total population per generation (incl. elites).
671
+ plateau_gens: Stop early if best composite is flat across this
672
+ many recent generations.
673
+ """
674
+ from attocode_intel.rules.evolution import EvolutionState, evolve
675
+
676
+ if not target_rule_ids:
677
+ return "Error: provide at least one target_rule_id."
678
+
679
+ project_dir = _get_project_dir()
680
+ if not fixtures_dir:
681
+ candidate = os.path.join(project_dir or ".", ".attocode", "test_fixtures")
682
+ if os.path.isdir(candidate):
683
+ fixtures_dir = candidate
684
+ else:
685
+ return (
686
+ "Error: fixtures_dir not provided and "
687
+ ".attocode/test_fixtures/ does not exist."
688
+ )
689
+
690
+ registry = _get_registry()
691
+ seeds: list = []
692
+ missing: list[str] = []
693
+ for qid in target_rule_ids:
694
+ rule = registry.get(qid)
695
+ if rule is None:
696
+ missing.append(qid)
697
+ else:
698
+ seeds.append(rule)
699
+ if missing:
700
+ return f"Error: rule(s) not found: {missing}"
701
+ if not seeds:
702
+ return "Error: no usable seed rules."
703
+
704
+ log_path = (
705
+ os.path.join(project_dir, ".attocode", "evolution_log.jsonl")
706
+ if project_dir else ""
707
+ )
708
+
709
+ state: EvolutionState = evolve(
710
+ seeds,
711
+ fixtures_dir=fixtures_dir,
712
+ project_dir=project_dir,
713
+ max_generations=generations,
714
+ population_size=population_size,
715
+ plateau_gens=plateau_gens,
716
+ log_path=log_path,
717
+ )
718
+
719
+ best = state.best_rule
720
+ fit = state.best_fitness
721
+ parts = [
722
+ f"Evolution complete: {state.generations_run} generation(s), "
723
+ f"stopped on {state.stopped_reason}.",
724
+ "",
725
+ f"Best rule: {best.qualified_id}",
726
+ f" pattern: {best.pattern.pattern if best.pattern else '(none)'}",
727
+ f" severity: {best.severity}, confidence: {best.confidence}",
728
+ f" precision: {fit.precision:.3f}, recall: {fit.recall:.3f}, "
729
+ f"f1: {fit.f1:.3f}, speed: {fit.speed_ms:.1f}ms",
730
+ f" composite: {fit.composite:.3f}",
731
+ "",
732
+ f"History (composite per generation): {state.history}",
733
+ ]
734
+ if log_path:
735
+ parts.append(f"\nFull audit log: {log_path}")
736
+ return "\n".join(parts)
737
+
738
+
739
+ @mcp.tool()
740
+ def synthesize_rule(
741
+ positive_samples: list[str],
742
+ negative_samples: list[str] | None = None,
743
+ language: str = "",
744
+ rule_id: str = "synth-rule",
745
+ description: str = "",
746
+ mode: str = "auto",
747
+ ) -> str:
748
+ """Generate a candidate rule from positive / negative code samples.
749
+
750
+ Tries deterministic regex synthesis first (longest common substring +
751
+ word boundaries, validated against negatives). Falls back to an
752
+ LLM-assisted synthesis when no clean anchor exists and ``mode`` is
753
+ ``"auto"`` or ``"llm"``. The returned YAML can be pasted into a pack
754
+ file or registered at runtime via ``register_rule``.
755
+
756
+ Args:
757
+ positive_samples: Lines of code the rule MUST match (one per entry).
758
+ negative_samples: Lines of code the rule MUST NOT match. Optional.
759
+ language: Target language tag (e.g. "python", "go").
760
+ rule_id: Suggested ID for the synthesized rule.
761
+ description: What the rule should detect — used by the LLM prompt
762
+ and as the rule's ``message`` field.
763
+ mode: ``"auto"`` (default), ``"regex"``, or ``"llm"``.
764
+
765
+ Returns:
766
+ YAML rule body when synthesis succeeded, otherwise a diagnostic
767
+ report explaining what was tried.
768
+ """
769
+ from attocode_intel.rules.synthesis import (
770
+ render_rule_yaml,
771
+ synthesize_rule as _synth,
772
+ )
773
+
774
+ result = _synth(
775
+ positives=positive_samples,
776
+ negatives=list(negative_samples or []),
777
+ language=language,
778
+ rule_id=rule_id,
779
+ description=description,
780
+ mode=mode,
781
+ )
782
+
783
+ if result.rule is None:
784
+ diag_lines = "\n - ".join(result.diagnostics) if result.diagnostics else "(no diagnostics)"
785
+ return (
786
+ f"Synthesis failed (mode={mode}, method={result.method}):\n - {diag_lines}"
787
+ )
788
+
789
+ yaml_body = render_rule_yaml(result.rule)
790
+ diag_lines = "\n# - ".join(result.diagnostics)
791
+ return f"# synthesised via {result.method}\n# - {diag_lines}\n{yaml_body}"
792
+
793
+
794
+ @mcp.tool()
795
+ def rule_hygiene(
796
+ apply: bool = False,
797
+ min_scans: int = 10,
798
+ min_samples: int = 10,
799
+ fp_threshold: float = 0.5,
800
+ drift_threshold: float = 0.2,
801
+ ) -> str:
802
+ """Report (and optionally auto-disable) dead, noisy, and drifting rules.
803
+
804
+ Categories:
805
+ dead — rule applied across `min_scans` analyze sessions with 0 matches.
806
+ noisy — at least `min_samples` TP/FP observations and FP rate > `fp_threshold`.
807
+ drift — calibrated confidence diverges from the rule's declared
808
+ confidence by at least `drift_threshold`. Reported only.
809
+
810
+ With `apply=True`, dead and noisy rules are disabled in the in-memory
811
+ registry and the decision persists in `.attocode/rule_feedback.json` so
812
+ they stay disabled in future sessions until manually re-enabled.
813
+
814
+ Args:
815
+ apply: When True, disables dead and noisy rules.
816
+ min_scans: Sessions a rule must have run before it can be marked dead.
817
+ min_samples: TP/FP observations required before a rule can be marked noisy.
818
+ fp_threshold: FP rate above which a rule is noisy.
819
+ drift_threshold: Confidence delta above which drift is reported.
820
+ """
821
+ from attocode_intel.rules.hygiene import (
822
+ apply_hygiene,
823
+ compute_hygiene,
824
+ format_hygiene_report,
825
+ )
826
+ from attocode_intel.rules.profiling import FeedbackStore
827
+
828
+ project_dir = _get_project_dir()
829
+ if not project_dir:
830
+ return "Error: No project directory detected. Open a project first."
831
+
832
+ store = FeedbackStore(project_dir)
833
+ registry = _get_registry()
834
+ report = compute_hygiene(
835
+ store,
836
+ registry,
837
+ min_scans=min_scans,
838
+ min_samples=min_samples,
839
+ fp_threshold=fp_threshold,
840
+ drift_threshold=drift_threshold,
841
+ )
842
+
843
+ output = format_hygiene_report(report)
844
+ if apply and report.total_actionable:
845
+ disabled = apply_hygiene(report, registry, store)
846
+ output += f"\n\nDisabled {disabled} rule(s) (dead + noisy)."
847
+ return output
848
+
849
+
850
+ @mcp.tool()
851
+ def search_community_packs(
852
+ query: str = "",
853
+ language: str = "",
854
+ ) -> str:
855
+ """Search the community rule pack registry.
856
+
857
+ Finds rule packs shared by the community, filterable by keyword
858
+ and language. Install found packs with install_community_pack().
859
+
860
+ Args:
861
+ query: Search by name, description, or tags.
862
+ language: Filter to packs supporting this language.
863
+ """
864
+ from attocode_intel.rules.marketplace import search_packs, format_registry_search
865
+
866
+ entries = search_packs(query=query, language=language)
867
+ return format_registry_search(entries)
868
+
869
+
870
+ @mcp.tool()
871
+ def install_community_pack(
872
+ name: str,
873
+ url: str = "",
874
+ ) -> str:
875
+ """Install a community rule pack from a Git repository.
876
+
877
+ Downloads and validates the pack, then installs to .attocode/packs/.
878
+
879
+ Args:
880
+ name: Pack name (used as directory name).
881
+ url: Git clone URL. If empty, looks up from community registry.
882
+ """
883
+ from attocode_intel.rules.marketplace import (
884
+ install_remote_pack,
885
+ search_packs,
886
+ )
887
+
888
+ project_dir = _get_project_dir()
889
+ if not project_dir:
890
+ return "Error: No project directory detected. Open a project first."
891
+
892
+ if not url:
893
+ entries = search_packs(query=name)
894
+ matching = [e for e in entries if e.name == name]
895
+ if matching:
896
+ url = matching[0].url
897
+ else:
898
+ return f"Pack '{name}' not found in registry. Provide a URL directly."
899
+
900
+ result = install_remote_pack(url, project_dir, pack_name=name)
901
+
902
+ # Reload registry
903
+ global _registry, _registry_loaded
904
+ with _registry_lock:
905
+ _registry = None
906
+ _registry_loaded = False
907
+
908
+ return result
909
+
910
+
911
+ @mcp.tool()
912
+ def validate_pack_tool(pack_path: str = "") -> str:
913
+ """Validate a rule pack for correctness before publishing.
914
+
915
+ Checks manifest, rule syntax, regex compilation, and inline tests.
916
+
917
+ Args:
918
+ pack_path: Path to pack directory. Default: validates all installed packs.
919
+ """
920
+ from attocode_intel.rules.marketplace import validate_pack, prepare_pack_for_publish
921
+
922
+ project_dir = _get_project_dir()
923
+ if not project_dir and not pack_path:
924
+ return "Error: No project directory detected. Provide pack_path or open a project."
925
+
926
+ if pack_path:
927
+ import os
928
+ if not os.path.isabs(pack_path):
929
+ pack_path = os.path.join(project_dir, pack_path)
930
+ return prepare_pack_for_publish(pack_path)
931
+
932
+ import os
933
+ packs_dir = os.path.join(project_dir, ".attocode", "packs")
934
+ if not os.path.isdir(packs_dir):
935
+ return "No packs installed (.attocode/packs/ not found)."
936
+
937
+ results: list[str] = []
938
+ for entry in sorted(os.listdir(packs_dir)):
939
+ pack_dir = os.path.join(packs_dir, entry)
940
+ if os.path.isdir(pack_dir):
941
+ errors = validate_pack(pack_dir)
942
+ status = "PASS" if not errors else "FAIL"
943
+ results.append(f"**{entry}**: {status}")
944
+ if errors:
945
+ for e in errors:
946
+ results.append(f" - {e}")
947
+
948
+ return "\n".join(results) if results else "No packs found."
949
+
950
+
951
+ @mcp.tool()
952
+ def import_rules(
953
+ source_format: str,
954
+ content: str,
955
+ ) -> str:
956
+ """Import rules from another tool's format into attocode YAML.
957
+
958
+ Converts rules from Semgrep or other formats to attocode YAML.
959
+ Output can be saved to .attocode/rules/ or registered via register_rule().
960
+
961
+ Supported formats: semgrep
962
+
963
+ Args:
964
+ source_format: Source format — "semgrep".
965
+ content: YAML content in the source format.
966
+ """
967
+ if source_format.lower() == "semgrep":
968
+ from attocode_intel.rules.importers.semgrep import convert_semgrep_to_yaml
969
+ return convert_semgrep_to_yaml(content)
970
+ else:
971
+ return f"Unsupported format '{source_format}'. Supported: semgrep"
972
+
973
+
974
+ def _build_scoped_registry(project_dir):
975
+ from attocode_intel.rules.registry import RuleRegistry
976
+ from attocode_intel.rules.loader import load_builtin_rules, load_user_rules
977
+ from attocode_intel.rules.packs.pack_loader import load_all_packs
978
+ from attocode_intel.rules.hygiene import apply_persistent_disable
979
+ from attocode_intel.rules.profiling import FeedbackStore
980
+ registry = RuleRegistry()
981
+ registry.register_many(load_builtin_rules())
982
+ _, rules = load_all_packs(project_dir)
983
+ registry.register_many(rules)
984
+ registry.register_many(load_user_rules(project_dir))
985
+ from attocode_intel.request_context import current_request
986
+ request = current_request.get()
987
+ if request and request.source == "local":
988
+ from attocode_intel.rules.plugins.plugin_loader import load_all_plugins
989
+ _, plugin_rules = load_all_plugins(project_dir)
990
+ registry.register_many(plugin_rules)
991
+ apply_persistent_disable(registry, FeedbackStore(project_dir))
992
+ return registry