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,382 @@
1
+ """Semgrep rule importer — converts Semgrep YAML rules to attocode format.
2
+
3
+ Handles Semgrep's rule format including:
4
+ - ``pattern``, ``patterns``, ``pattern-either``, ``pattern-not``
5
+ - ``metavariable-regex`` and ``metavariable-comparison``
6
+ - ``fix`` (autofix)
7
+ - ``metadata`` (CWE, confidence, OWASP)
8
+
9
+ Limitations:
10
+ - Semgrep patterns that use ``$X`` metavariables in AST-aware mode
11
+ cannot be directly converted to regex. These are flagged as needing
12
+ the structural tier (ast-grep).
13
+ - ``pattern-inside`` with full AST scope is approximated as line-based.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import re
20
+ from typing import Any
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Semgrep severity -> attocode severity
25
+ _SEVERITY_MAP = {
26
+ "ERROR": "high",
27
+ "WARNING": "medium",
28
+ "INFO": "low",
29
+ "INVENTORY": "info",
30
+ }
31
+
32
+ # Semgrep category hints -> attocode category
33
+ _CATEGORY_MAP = {
34
+ "security": "security",
35
+ "correctness": "correctness",
36
+ "best-practice": "style",
37
+ "performance": "performance",
38
+ "maintainability": "complexity",
39
+ }
40
+
41
+
42
+ def _has_ast_metavars(pattern: str) -> bool:
43
+ """Check if a Semgrep pattern uses AST-aware metavariables.
44
+
45
+ Semgrep metavars like ``$X`` in AST patterns work differently from
46
+ regex patterns — they match entire AST nodes. We can only convert
47
+ simple patterns where ``$X`` maps to ``\\w+`` or similar.
48
+ """
49
+ # Patterns with ellipsis operators (... in Semgrep) are AST-aware
50
+ if "..." in pattern and "$" in pattern:
51
+ return True
52
+ # Patterns with type constraints or deep patterns
53
+ if "<..." in pattern or "...>" in pattern:
54
+ return True
55
+ return False
56
+
57
+
58
+ def _extract_cwe(metadata: dict) -> str:
59
+ """Extract CWE ID from Semgrep metadata."""
60
+ # Semgrep stores CWE in various places
61
+ cwe = metadata.get("cwe", "")
62
+ if isinstance(cwe, list):
63
+ cwe = cwe[0] if cwe else ""
64
+ cwe = str(cwe)
65
+
66
+ # Normalize to "CWE-NNN" format
67
+ m = re.search(r"CWE-?(\d+)", cwe, re.IGNORECASE)
68
+ if m:
69
+ return f"CWE-{m.group(1)}"
70
+ return ""
71
+
72
+
73
+ def _extract_confidence(metadata: dict) -> float:
74
+ """Extract confidence from Semgrep metadata."""
75
+ conf = metadata.get("confidence", "")
76
+ if isinstance(conf, (int, float)):
77
+ return float(conf) if 0 <= conf <= 1 else conf / 100
78
+ conf_str = str(conf).lower()
79
+ if conf_str == "high":
80
+ return 0.9
81
+ elif conf_str == "medium":
82
+ return 0.7
83
+ elif conf_str == "low":
84
+ return 0.5
85
+ return 0.8
86
+
87
+
88
+ def convert_semgrep_rule(rule: dict[str, Any]) -> dict[str, Any] | None:
89
+ """Convert a single Semgrep rule to attocode YAML format.
90
+
91
+ Args:
92
+ rule: A single rule dict from Semgrep YAML.
93
+
94
+ Returns:
95
+ Attocode-format rule dict, or None if unconvertible.
96
+ """
97
+ rule_id = rule.get("id", "")
98
+ if not rule_id:
99
+ return None
100
+
101
+ metadata = rule.get("metadata", {})
102
+ severity = _SEVERITY_MAP.get(rule.get("severity", "WARNING"), "medium")
103
+
104
+ # Determine category
105
+ category = "security" # default for Semgrep rules
106
+ for tag in metadata.get("category", []) if isinstance(metadata.get("category"), list) else [str(metadata.get("category", ""))]:
107
+ tag_lower = tag.lower()
108
+ if tag_lower in _CATEGORY_MAP:
109
+ category = _CATEGORY_MAP[tag_lower]
110
+ break
111
+
112
+ # Extract pattern(s)
113
+ attocode_rule: dict[str, Any] = {
114
+ "id": rule_id,
115
+ "name": rule_id.replace("-", " ").replace("_", " ").title()[:60],
116
+ "message": rule.get("message", ""),
117
+ "severity": severity,
118
+ "category": category,
119
+ "confidence": _extract_confidence(metadata),
120
+ }
121
+
122
+ # CWE
123
+ cwe = _extract_cwe(metadata)
124
+ if cwe:
125
+ attocode_rule["cwe"] = cwe
126
+
127
+ # Languages
128
+ languages = rule.get("languages", [])
129
+ if languages:
130
+ attocode_rule["languages"] = languages
131
+
132
+ # Tags
133
+ tags = metadata.get("tags", [])
134
+ if isinstance(tags, list):
135
+ attocode_rule["tags"] = tags
136
+
137
+ # Handle pattern types
138
+ needs_structural = False
139
+
140
+ if "pattern" in rule:
141
+ pattern = str(rule["pattern"])
142
+ if _has_ast_metavars(pattern):
143
+ needs_structural = True
144
+ attocode_rule["structural_pattern"] = pattern
145
+ # Still provide regex approximation
146
+ regex_approx = _semgrep_to_regex(pattern)
147
+ if regex_approx:
148
+ attocode_rule["pattern"] = regex_approx
149
+ else:
150
+ regex = _semgrep_to_regex(pattern)
151
+ if regex:
152
+ attocode_rule["pattern"] = regex
153
+ else:
154
+ return None # can't convert
155
+
156
+ elif "patterns" in rule:
157
+ # Semgrep composite patterns -> attocode patterns list
158
+ patterns_list = _convert_patterns(rule["patterns"])
159
+ if patterns_list:
160
+ attocode_rule["patterns"] = patterns_list
161
+ else:
162
+ return None
163
+
164
+ elif "pattern-either" in rule:
165
+ either = rule["pattern-either"]
166
+ converted = []
167
+ for p in either:
168
+ if isinstance(p, str):
169
+ regex = _semgrep_to_regex(p)
170
+ if regex:
171
+ converted.append(regex)
172
+ elif isinstance(p, dict) and "pattern" in p:
173
+ regex = _semgrep_to_regex(str(p["pattern"]))
174
+ if regex:
175
+ converted.append(regex)
176
+ if converted:
177
+ attocode_rule["pattern-either"] = converted
178
+ else:
179
+ return None
180
+
181
+ else:
182
+ return None # no pattern to convert
183
+
184
+ # Metavariable constraints — convert Semgrep format to attocode format
185
+ # Semgrep: {"metavariable": "$VAR", "regex": "..."} or {"metavariable": "$VAR", "comparison": "..."}
186
+ # Attocode: {"VAR": "..."} (key is var name without $)
187
+ for item in rule.get("patterns", []):
188
+ if isinstance(item, dict):
189
+ if "metavariable-regex" in item:
190
+ mv = item["metavariable-regex"]
191
+ if isinstance(mv, dict) and "metavariable" in mv and "regex" in mv:
192
+ var = str(mv["metavariable"]).lstrip("$")
193
+ attocode_rule.setdefault("metavariable-regex", {})[var] = str(mv["regex"])
194
+ elif isinstance(mv, dict):
195
+ # Already in attocode format (key: regex pairs)
196
+ attocode_rule["metavariable-regex"] = mv
197
+ if "metavariable-comparison" in item:
198
+ mv = item["metavariable-comparison"]
199
+ if isinstance(mv, dict) and "metavariable" in mv and "comparison" in mv:
200
+ var = str(mv["metavariable"]).lstrip("$")
201
+ attocode_rule.setdefault("metavariable-comparison", {})[var] = str(mv["comparison"])
202
+ elif isinstance(mv, dict):
203
+ attocode_rule["metavariable-comparison"] = mv
204
+
205
+ # Fix
206
+ fix = rule.get("fix", "")
207
+ if fix:
208
+ attocode_rule["fix"] = {
209
+ "search": rule.get("pattern", ""),
210
+ "replace": fix,
211
+ }
212
+
213
+ # Generate explanation stub from message
214
+ message = rule.get("message", "")
215
+ if message and "explanation" not in attocode_rule:
216
+ attocode_rule["explanation"] = message
217
+ if message and "recommendation" not in attocode_rule:
218
+ attocode_rule["recommendation"] = f"Address: {message[:100]}"
219
+
220
+ # Mark as needing structural tier
221
+ if needs_structural:
222
+ attocode_rule.setdefault("tags", []).append("needs-structural-tier")
223
+
224
+ return attocode_rule
225
+
226
+
227
+ def _semgrep_to_regex(pattern: str) -> str | None:
228
+ """Convert a Semgrep pattern to a regex pattern.
229
+
230
+ Simple Semgrep patterns (without deep AST matching) can often
231
+ be converted to regex with metavariable support.
232
+ """
233
+ if not pattern.strip():
234
+ return None
235
+
236
+ # Remove leading/trailing whitespace and ellipsis
237
+ p = pattern.strip()
238
+
239
+ # Replace Semgrep ellipsis with regex wildcard
240
+ p = p.replace("...", ".*?")
241
+
242
+ # Escape regex special chars EXCEPT those in metavar patterns
243
+ # We do a targeted escape: escape chars that are regex-special
244
+ # but preserve $VAR metavariables
245
+ result = []
246
+ i = 0
247
+ while i < len(p):
248
+ if p[i] == "$" and i + 1 < len(p) and (p[i + 1].isalpha() or p[i + 1] == "_"):
249
+ # Keep $VAR as-is (metavar)
250
+ j = i + 1
251
+ while j < len(p) and (p[j].isalnum() or p[j] == "_"):
252
+ j += 1
253
+ result.append(p[i:j])
254
+ i = j
255
+ elif p[i] in r"\.+*?[]{}()^|":
256
+ # Escape regex special chars
257
+ if p[i] == "." and i + 2 < len(p) and p[i:i+3] == ".*?":
258
+ result.append(".*?")
259
+ i += 3
260
+ else:
261
+ result.append("\\" + p[i])
262
+ i += 1
263
+ else:
264
+ result.append(p[i])
265
+ i += 1
266
+
267
+ regex = "".join(result)
268
+
269
+ # Verify it compiles
270
+ try:
271
+ re.compile(regex)
272
+ return regex
273
+ except re.error:
274
+ return None
275
+
276
+
277
+ def _convert_patterns(patterns: list) -> list[dict[str, str]] | None:
278
+ """Convert Semgrep patterns list to attocode patterns list."""
279
+ result: list[dict[str, str]] = []
280
+
281
+ for item in patterns:
282
+ if not isinstance(item, dict):
283
+ continue
284
+
285
+ if "pattern" in item:
286
+ regex = _semgrep_to_regex(str(item["pattern"]))
287
+ if regex:
288
+ result.append({"pattern": regex})
289
+
290
+ elif "pattern-not" in item:
291
+ regex = _semgrep_to_regex(str(item["pattern-not"]))
292
+ if regex:
293
+ result.append({"pattern-not": regex})
294
+
295
+ elif "pattern-inside" in item:
296
+ regex = _semgrep_to_regex(str(item["pattern-inside"]))
297
+ if regex:
298
+ result.append({"pattern-inside": regex})
299
+
300
+ elif "pattern-not-inside" in item:
301
+ regex = _semgrep_to_regex(str(item["pattern-not-inside"]))
302
+ if regex:
303
+ result.append({"pattern-not-inside": regex})
304
+
305
+ elif "pattern-either" in item:
306
+ either_list = item["pattern-either"]
307
+ converted = []
308
+ for p in either_list:
309
+ if isinstance(p, str):
310
+ regex = _semgrep_to_regex(p)
311
+ if regex:
312
+ converted.append(regex)
313
+ elif isinstance(p, dict) and "pattern" in p:
314
+ regex = _semgrep_to_regex(str(p["pattern"]))
315
+ if regex:
316
+ converted.append(regex)
317
+ if converted:
318
+ result.append({"pattern-either": converted})
319
+
320
+ return result if result else None
321
+
322
+
323
+ # ---------------------------------------------------------------------------
324
+ # Batch conversion
325
+ # ---------------------------------------------------------------------------
326
+
327
+
328
+ def convert_semgrep_file(yaml_content: str) -> tuple[list[dict], list[str]]:
329
+ """Convert a Semgrep YAML file (possibly with multiple rules) to attocode format.
330
+
331
+ Args:
332
+ yaml_content: Raw YAML content from a Semgrep rule file.
333
+
334
+ Returns:
335
+ (converted_rules, errors) — list of attocode rule dicts and error messages.
336
+ """
337
+ try:
338
+ import yaml # type: ignore[import-untyped]
339
+ data = yaml.safe_load(yaml_content)
340
+ except Exception as exc:
341
+ return [], [f"Failed to parse YAML: {exc}"]
342
+
343
+ if not isinstance(data, dict) or "rules" not in data:
344
+ return [], ["Expected Semgrep format with top-level 'rules' key"]
345
+
346
+ converted: list[dict] = []
347
+ errors: list[str] = []
348
+
349
+ for i, rule in enumerate(data["rules"]):
350
+ if not isinstance(rule, dict):
351
+ errors.append(f"Rule at index {i}: expected dict")
352
+ continue
353
+ result = convert_semgrep_rule(rule)
354
+ if result:
355
+ converted.append(result)
356
+ else:
357
+ errors.append(f"Rule '{rule.get('id', f'index-{i}')}': could not convert")
358
+
359
+ return converted, errors
360
+
361
+
362
+ def convert_semgrep_to_yaml(yaml_content: str) -> str:
363
+ """Convert Semgrep YAML to attocode YAML string.
364
+
365
+ Convenience function for the MCP tool.
366
+ """
367
+ converted, errors = convert_semgrep_file(yaml_content)
368
+
369
+ try:
370
+ import yaml # type: ignore[import-untyped]
371
+ except ImportError:
372
+ return "Error: PyYAML not installed"
373
+
374
+ parts: list[str] = []
375
+ if converted:
376
+ parts.append(yaml.dump(converted, default_flow_style=False, sort_keys=False))
377
+ if errors:
378
+ parts.append("\n# Conversion errors:")
379
+ for e in errors:
380
+ parts.append(f"# {e}")
381
+
382
+ return "\n".join(parts) if parts else "No rules converted."
@@ -0,0 +1,292 @@
1
+ """Rule loader — loads rules from builtins, YAML files, and packs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from attocode_intel.rules.metavar import compile_metavar_pattern, has_metavars
10
+ from attocode_intel.rules.model import (
11
+ AutoFix,
12
+ FewShotExample,
13
+ RuleCategory,
14
+ RuleSeverity,
15
+ RuleSource,
16
+ RuleTier,
17
+ UnifiedRule,
18
+ from_bug_pattern,
19
+ from_security_pattern,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Category mapping for YAML rules
25
+ _YAML_CATEGORY_MAP: dict[str, RuleCategory] = {
26
+ "correctness": RuleCategory.CORRECTNESS,
27
+ "suspicious": RuleCategory.SUSPICIOUS,
28
+ "complexity": RuleCategory.COMPLEXITY,
29
+ "performance": RuleCategory.PERFORMANCE,
30
+ "style": RuleCategory.STYLE,
31
+ "security": RuleCategory.SECURITY,
32
+ "deprecated": RuleCategory.DEPRECATED,
33
+ # Legacy compat (must match from_bug_pattern and from_security_pattern mappings)
34
+ "anti_pattern": RuleCategory.SUSPICIOUS,
35
+ "secret": RuleCategory.SECURITY,
36
+ "logic_error": RuleCategory.CORRECTNESS,
37
+ "error_handling": RuleCategory.CORRECTNESS,
38
+ "edge_case": RuleCategory.SUSPICIOUS,
39
+ "concurrency": RuleCategory.SUSPICIOUS,
40
+ "resource_leak": RuleCategory.CORRECTNESS,
41
+ "type_safety": RuleCategory.CORRECTNESS,
42
+ }
43
+
44
+ _VALID_SEVERITIES = frozenset(s.value for s in RuleSeverity)
45
+
46
+
47
+ def load_builtin_rules() -> list[UnifiedRule]:
48
+ """Load builtin rules from bug_finder and security patterns."""
49
+ rules: list[UnifiedRule] = []
50
+
51
+ # 1. Bug finder patterns
52
+ try:
53
+ from attocode_intel.bug_finder import _PATTERNS
54
+
55
+ for regex, cat, sev, desc, conf in _PATTERNS:
56
+ try:
57
+ rule = from_bug_pattern(regex, cat.value, sev.value, desc, conf)
58
+ rules.append(rule)
59
+ except Exception as exc:
60
+ logger.warning("Failed to adapt builtin bug pattern '%s': %s", desc[:40], exc)
61
+ except ImportError:
62
+ logger.debug("bug_finder not available, skipping builtin bug rules")
63
+
64
+ # 2. Security patterns (secrets + anti-patterns)
65
+ try:
66
+ from attocode_intel._internal.integrations.security.patterns import (
67
+ ANTI_PATTERNS,
68
+ SECRET_PATTERNS,
69
+ )
70
+
71
+ for pat in SECRET_PATTERNS:
72
+ try:
73
+ rules.append(from_security_pattern(pat))
74
+ except Exception as exc:
75
+ logger.warning("Failed to adapt security pattern '%s': %s", getattr(pat, "name", "?"), exc)
76
+ for pat in ANTI_PATTERNS:
77
+ try:
78
+ rules.append(from_security_pattern(pat))
79
+ except Exception as exc:
80
+ logger.warning("Failed to adapt security pattern '%s': %s", getattr(pat, "name", "?"), exc)
81
+ except ImportError:
82
+ logger.debug("security patterns not available, skipping builtin security rules")
83
+
84
+ logger.info("Loaded %d builtin rules", len(rules))
85
+ return rules
86
+
87
+
88
+ def load_yaml_rules(
89
+ rules_dir: str | Path,
90
+ *,
91
+ source: RuleSource = RuleSource.USER,
92
+ pack: str = "",
93
+ ) -> list[UnifiedRule]:
94
+ """Load rules from YAML files in a directory.
95
+
96
+ Supports the existing .attocode/rules/*.yaml format plus the
97
+ expanded format with explanation and examples fields.
98
+
99
+ Args:
100
+ rules_dir: Directory containing \\*.yaml rule files.
101
+ source: RuleSource tag for loaded rules.
102
+ pack: Pack name to associate with loaded rules.
103
+
104
+ Returns:
105
+ List of parsed UnifiedRule instances. Invalid rules are skipped.
106
+ """
107
+ rules_path = Path(rules_dir)
108
+ if not rules_path.is_dir():
109
+ return []
110
+
111
+ try:
112
+ import yaml # type: ignore[import-untyped]
113
+ except ImportError:
114
+ logger.debug("PyYAML not installed — skipping YAML rules from %s", rules_dir)
115
+ return []
116
+
117
+ rules: list[UnifiedRule] = []
118
+ for yaml_file in sorted(rules_path.glob("*.yaml")):
119
+ try:
120
+ content = yaml_file.read_text(encoding="utf-8")
121
+ data = yaml.safe_load(content)
122
+ if data is None:
123
+ continue
124
+ items = [data] if isinstance(data, dict) else data if isinstance(data, list) else []
125
+ for i, item in enumerate(items):
126
+ try:
127
+ rule = _parse_yaml_rule(
128
+ item, source=source, pack=pack, origin=f"{yaml_file.name}[{i}]",
129
+ )
130
+ if rule is not None:
131
+ rules.append(rule)
132
+ except Exception as exc:
133
+ logger.warning("Invalid rule in %s at index %d: %s", yaml_file.name, i, exc)
134
+ except Exception as exc:
135
+ logger.warning("Failed to load rules from %s: %s", yaml_file, exc)
136
+
137
+ if rules:
138
+ logger.info("Loaded %d YAML rules from %s", len(rules), rules_dir)
139
+ return rules
140
+
141
+
142
+ def _parse_yaml_rule(
143
+ data: dict,
144
+ *,
145
+ source: RuleSource = RuleSource.USER,
146
+ pack: str = "",
147
+ origin: str = "",
148
+ ) -> UnifiedRule | None:
149
+ """Parse a single YAML rule dict into a UnifiedRule."""
150
+ # ``pattern`` is required unless one of: ``patterns``/``pattern-either``
151
+ # provides a composite, OR ``structural_pattern`` provides a Tier-2
152
+ # ast-grep matcher (message-only structural rules are valid).
153
+ has_composite = "patterns" in data or "pattern-either" in data
154
+ has_structural = bool(data.get("structural_pattern"))
155
+ # Allow ``description`` as a synonym for ``message`` so structural-only
156
+ # rules don't have to repeat themselves.
157
+ if "message" not in data and "description" in data:
158
+ data = {**data, "message": data["description"]}
159
+ required = ["id", "message", "severity"]
160
+ if not (has_composite or has_structural):
161
+ required.append("pattern")
162
+ missing = [f for f in required if f not in data]
163
+ if missing:
164
+ logger.warning("Rule %s missing required fields: %s", origin, ", ".join(missing))
165
+ return None
166
+
167
+ sev_str = str(data["severity"]).lower()
168
+ if sev_str not in _VALID_SEVERITIES:
169
+ logger.warning("Rule %s has invalid severity '%s'", origin, data["severity"])
170
+ return None
171
+
172
+ # Category (default to security for backward compat)
173
+ cat_str = str(data.get("category", "security")).lower()
174
+ category = _YAML_CATEGORY_MAP.get(cat_str, RuleCategory.SECURITY)
175
+
176
+ # Build composite pattern if "patterns" or "pattern-either" is present
177
+ composite_pattern = None
178
+ if has_composite:
179
+ from attocode_intel.rules.combinators import build_composite_from_yaml
180
+ if "patterns" in data:
181
+ composite_src = data["patterns"] # list of dicts
182
+ else:
183
+ # Top-level pattern-either: wrap as dict for build_composite_from_yaml
184
+ composite_src = {"pattern-either": data["pattern-either"]}
185
+ composite_pattern = build_composite_from_yaml(composite_src)
186
+ if composite_pattern is None:
187
+ logger.warning("Rule %s has invalid composite patterns", origin)
188
+ return None
189
+
190
+ # Compile primary pattern (may be absent for composite-only rules)
191
+ compiled: re.Pattern[str] | None = None
192
+ metavar_names: list[str] = []
193
+ if "pattern" in data:
194
+ pattern_str = str(data["pattern"])
195
+ try:
196
+ if has_metavars(pattern_str):
197
+ compiled, metavar_names = compile_metavar_pattern(pattern_str)
198
+ else:
199
+ compiled = re.compile(pattern_str)
200
+ except re.error as exc:
201
+ logger.warning("Rule %s has invalid regex: %s", origin, exc)
202
+ return None
203
+
204
+ tier = RuleTier.STRUCTURAL if data.get("structural_pattern") else RuleTier.REGEX
205
+
206
+ # ast-grep's inline-rule YAML accepts a ``pattern`` object with either
207
+ # a bare string OR ``{context, selector}``. Specifying ``context``
208
+ # without ``selector`` produces an invalid rule that ast-grep rejects
209
+ # at parse time, which silently no-ops the rule. Catch it at load.
210
+ if data.get("structural_context") and not data.get("structural_selector"):
211
+ logger.warning(
212
+ "Rule %s sets structural_context without structural_selector; "
213
+ "ast-grep needs both — skipping this rule",
214
+ origin,
215
+ )
216
+ return None
217
+
218
+ # Parse metavariable constraints
219
+ metavar_regex: dict[str, str] = {}
220
+ raw_mv_regex = data.get("metavariable-regex") or data.get("metavariable_regex")
221
+ if isinstance(raw_mv_regex, dict):
222
+ metavar_regex = {str(k): str(v) for k, v in raw_mv_regex.items()}
223
+
224
+ metavar_comparison: dict[str, str] = {}
225
+ raw_mv_cmp = data.get("metavariable-comparison") or data.get("metavariable_comparison")
226
+ if isinstance(raw_mv_cmp, dict):
227
+ metavar_comparison = {str(k): str(v) for k, v in raw_mv_cmp.items()}
228
+
229
+ fix = None
230
+ fix_data = data.get("fix")
231
+ if isinstance(fix_data, dict) and "search" in fix_data and "replace" in fix_data:
232
+ fix_search = str(fix_data["search"])
233
+ fix_replace = str(fix_data["replace"])
234
+ uses_metavars = has_metavars(fix_search) or has_metavars(fix_replace)
235
+ fix = AutoFix(search=fix_search, replace=fix_replace, uses_metavars=uses_metavars)
236
+
237
+ examples: list[FewShotExample] = []
238
+ for ex in data.get("examples", []):
239
+ if isinstance(ex, dict) and "bad" in ex and "good" in ex:
240
+ examples.append(FewShotExample(
241
+ bad_code=str(ex["bad"]),
242
+ good_code=str(ex["good"]),
243
+ explanation=str(ex.get("explanation", "")),
244
+ ))
245
+
246
+ # Normalize languages and tags to lists (YAML allows string shorthand)
247
+ raw_langs = data.get("languages", [])
248
+ languages = [raw_langs] if isinstance(raw_langs, str) else list(raw_langs)
249
+ raw_tags = data.get("tags", [])
250
+ tags = [raw_tags] if isinstance(raw_tags, str) else list(raw_tags)
251
+
252
+ # External references (CWE links, blog posts, etc.) — preserved on import
253
+ raw_refs = data.get("references", [])
254
+ references = [raw_refs] if isinstance(raw_refs, str) else list(raw_refs)
255
+
256
+ return UnifiedRule(
257
+ id=str(data["id"]),
258
+ name=str(data.get("name", data["id"])),
259
+ description=str(data["message"]),
260
+ severity=RuleSeverity(sev_str),
261
+ category=category,
262
+ languages=languages,
263
+ pattern=compiled,
264
+ structural_pattern=str(data.get("structural_pattern", "")),
265
+ structural_context=str(data.get("structural_context", "")),
266
+ structural_selector=str(data.get("structural_selector", "")),
267
+ cwe=str(data.get("cwe", "")),
268
+ tags=tags,
269
+ source=source,
270
+ tier=tier,
271
+ confidence=float(data.get("confidence", 0.8)),
272
+ fix=fix,
273
+ enabled=bool(data.get("enabled", True)),
274
+ pack=pack,
275
+ explanation=str(data.get("explanation", "")),
276
+ examples=examples,
277
+ recommendation=str(data.get("recommendation", "")),
278
+ scan_comments=bool(data.get("scan_comments", False)),
279
+ metavars=metavar_names,
280
+ metavar_regex=metavar_regex,
281
+ metavar_comparison=metavar_comparison,
282
+ composite_pattern=composite_pattern,
283
+ references=references,
284
+ )
285
+
286
+
287
+ def load_user_rules(project_dir: str) -> list[UnifiedRule]:
288
+ """Load user-defined rules from .attocode/rules/*.yaml."""
289
+ return load_yaml_rules(
290
+ Path(project_dir) / ".attocode" / "rules",
291
+ source=RuleSource.USER,
292
+ )