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,531 @@
1
+ """Rule executor — runs rules against source files to produce findings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ from attocode_intel.rules.combinators import MatchContext
13
+ from attocode_intel.rules.metavar import (
14
+ apply_metavar_fix,
15
+ check_metavar_constraints,
16
+ interpolate_message,
17
+ )
18
+ from attocode_intel.rules.model import (
19
+ EnrichedFinding,
20
+ RuleTier,
21
+ UnifiedRule,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Per-rule subprocess timeout for ast-grep (seconds). Structural patterns
27
+ # can be expensive on huge files; cap to keep ``analyze`` responsive.
28
+ _AST_GREP_TIMEOUT = 30.0
29
+
30
+ # Languages where ast-grep ships a tree-sitter grammar. When a structural
31
+ # rule targets a language outside this set we either skip the rule (no
32
+ # fallback regex) or fall back to ``rule.pattern`` if the YAML defined one.
33
+ _AST_GREP_LANGS = frozenset({
34
+ "bash", "c", "cpp", "csharp", "css", "elixir", "go", "html", "java",
35
+ "javascript", "json", "kotlin", "lua", "php", "python", "ruby", "rust",
36
+ "scala", "shell", "swift", "tsx", "typescript", "yaml",
37
+ })
38
+
39
+ # Extension to language mapping
40
+ _EXT_LANG: dict[str, str] = {
41
+ ".py": "python", ".pyi": "python",
42
+ ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
43
+ ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
44
+ ".go": "go",
45
+ ".rs": "rust",
46
+ ".java": "java",
47
+ ".kt": "kotlin", ".kts": "kotlin",
48
+ ".rb": "ruby",
49
+ ".php": "php",
50
+ ".swift": "swift",
51
+ ".cs": "csharp",
52
+ ".c": "c", ".h": "c",
53
+ ".cpp": "cpp", ".hpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
54
+ ".lua": "lua",
55
+ ".sh": "shell", ".bash": "shell", ".zsh": "shell",
56
+ ".scala": "scala",
57
+ ".ex": "elixir", ".exs": "elixir",
58
+ }
59
+
60
+ # Comment prefix detection per language
61
+ _COMMENT_PREFIX: dict[str, list[str]] = {
62
+ "python": ["#"],
63
+ "javascript": ["//"],
64
+ "typescript": ["//"],
65
+ "go": ["//"],
66
+ "rust": ["//"],
67
+ "java": ["//"],
68
+ "kotlin": ["//"],
69
+ "ruby": ["#"],
70
+ "php": ["//", "#"],
71
+ "swift": ["//"],
72
+ "csharp": ["//"],
73
+ "c": ["//"],
74
+ "cpp": ["//"],
75
+ "lua": ["--"],
76
+ "shell": ["#"],
77
+ "scala": ["//"],
78
+ "elixir": ["#"],
79
+ }
80
+
81
+
82
+ def detect_language(file_path: str) -> str:
83
+ """Detect language from file extension."""
84
+ ext = os.path.splitext(file_path)[1].lower()
85
+ return _EXT_LANG.get(ext, "")
86
+
87
+
88
+ def _is_comment_line(line: str, language: str) -> bool:
89
+ stripped = line.lstrip()
90
+ for prefix in _COMMENT_PREFIX.get(language, []):
91
+ if stripped.startswith(prefix):
92
+ return True
93
+ return False
94
+
95
+
96
+ def _ast_grep_available() -> bool:
97
+ """Return True when an ``sg`` or ``ast-grep`` binary is on PATH."""
98
+ return shutil.which("sg") is not None or shutil.which("ast-grep") is not None
99
+
100
+
101
+ def _ast_grep_binary() -> str:
102
+ """Resolve the ast-grep binary, preferring the short ``sg`` alias."""
103
+ return shutil.which("sg") or shutil.which("ast-grep") or "sg"
104
+
105
+
106
+ # ast-grep's ``language`` field uses TitleCase ("Go", "Python") whereas
107
+ # our internal language tags are lowercase. Mapping covers the cases that
108
+ # differ from a naive ``.title()``.
109
+ _AST_GREP_LANG_NAMES: dict[str, str] = {
110
+ "javascript": "JavaScript",
111
+ "typescript": "TypeScript",
112
+ "tsx": "Tsx",
113
+ "csharp": "CSharp",
114
+ "cpp": "Cpp",
115
+ "html": "Html",
116
+ "css": "Css",
117
+ "json": "Json",
118
+ "yaml": "Yaml",
119
+ }
120
+
121
+
122
+ def _ast_grep_lang_name(lang: str) -> str:
123
+ return _AST_GREP_LANG_NAMES.get(lang, lang.title())
124
+
125
+
126
+ def _yaml_quote(value: str) -> str:
127
+ """Render ``value`` as a YAML double-quoted scalar with escapes.
128
+
129
+ Double-quoted scalars handle every printable + control character via
130
+ backslash escapes. Single-quoted scalars cannot represent literal
131
+ newlines or tabs, so synthesised / evolved patterns containing those
132
+ (which are legal ast-grep input) would corrupt the inline-rules YAML.
133
+ """
134
+ escaped = (
135
+ value.replace("\\", "\\\\")
136
+ .replace('"', '\\"')
137
+ .replace("\n", "\\n")
138
+ .replace("\r", "\\r")
139
+ .replace("\t", "\\t")
140
+ )
141
+ return '"' + escaped + '"'
142
+
143
+
144
+ def _build_inline_rule_yaml(rule: UnifiedRule, lang: str) -> str:
145
+ """Build the minimal ast-grep YAML rule for ``sg scan --inline-rules``.
146
+
147
+ Used when the rule provides ``structural_context`` and/or
148
+ ``structural_selector`` to disambiguate the parse.
149
+
150
+ ast-grep accepts two pattern shapes under ``rule.pattern``:
151
+
152
+ * a bare string — the simple pattern, no disambiguation
153
+ * an object with ``context`` (the full snippet that parses correctly)
154
+ and ``selector`` (the AST kind to extract from inside it). The plain
155
+ ``pattern`` field on the object is *not* used when context drives
156
+ matching; including it makes the YAML invalid.
157
+ """
158
+ parts: list[str] = [
159
+ f"id: {rule.id or 'inline'}",
160
+ f"language: {_ast_grep_lang_name(lang)}",
161
+ "rule:",
162
+ ]
163
+ if rule.structural_context:
164
+ # Context without selector produces invalid ast-grep YAML — the
165
+ # parser needs a selector to know which AST kind to extract from
166
+ # the wrapping context. Loader-level validation rejects pack rules
167
+ # in this shape; this guard catches programmatically-constructed
168
+ # rules (synthesis / evolution / direct UnifiedRule(...) callers)
169
+ # so they fail loudly instead of silently no-op'ing.
170
+ if not rule.structural_selector:
171
+ raise ValueError(
172
+ f"rule {rule.qualified_id!r} has structural_context but no "
173
+ "structural_selector; ast-grep needs both to extract a "
174
+ "node from the wrapping context"
175
+ )
176
+ parts.append(" pattern:")
177
+ parts.append(f" context: {_yaml_quote(rule.structural_context)}")
178
+ parts.append(f" selector: {rule.structural_selector}")
179
+ elif rule.structural_selector:
180
+ # Selector without context: pair it with the pattern as object.
181
+ parts.append(" pattern:")
182
+ parts.append(f" pattern: {_yaml_quote(rule.structural_pattern)}")
183
+ parts.append(f" selector: {rule.structural_selector}")
184
+ else:
185
+ parts.append(f" pattern: {_yaml_quote(rule.structural_pattern)}")
186
+ return "\n".join(parts) + "\n"
187
+
188
+
189
+ def _execute_structural_rule(
190
+ rule: UnifiedRule,
191
+ files_by_lang: dict[str, list[str]],
192
+ *,
193
+ project_dir: str = "",
194
+ ) -> list[EnrichedFinding]:
195
+ """Run a single structural rule across applicable files via ast-grep.
196
+
197
+ Per language: shells out to ``sg run --pattern <pat> --lang <lang>
198
+ --json=stream <files...>`` and parses one match per JSON line. Files
199
+ in languages without an ast-grep grammar are silently skipped — the
200
+ caller is responsible for supplying ``rule.pattern`` if they want a
201
+ Tier-1 fallback for those languages.
202
+
203
+ Returns the list of (partially populated) ``EnrichedFinding`` objects.
204
+ """
205
+ if not rule.structural_pattern:
206
+ return []
207
+
208
+ # Determine which languages this rule applies to. An empty
209
+ # ``rule.languages`` means "all languages we have files for".
210
+ target_langs: list[str]
211
+ if rule.languages:
212
+ target_langs = [lang for lang in rule.languages if lang in files_by_lang]
213
+ else:
214
+ target_langs = list(files_by_lang.keys())
215
+
216
+ binary = _ast_grep_binary()
217
+ findings: list[EnrichedFinding] = []
218
+
219
+ for lang in target_langs:
220
+ if lang not in _AST_GREP_LANGS:
221
+ continue
222
+ files = files_by_lang.get(lang, [])
223
+ if not files:
224
+ continue
225
+
226
+ # When the rule needs context/selector to disambiguate parsing
227
+ # (e.g. ``fmt.Println($X)`` in Go), shell out to ``sg scan
228
+ # --inline-rules`` with a YAML rule body. Otherwise the simpler
229
+ # ``sg run --pattern`` path is faster.
230
+ if rule.structural_context or rule.structural_selector:
231
+ inline_yaml = _build_inline_rule_yaml(rule, lang)
232
+ cmd = [
233
+ binary, "scan",
234
+ "--inline-rules", inline_yaml,
235
+ "--json=stream",
236
+ *files,
237
+ ]
238
+ else:
239
+ cmd = [
240
+ binary, "run",
241
+ "--pattern", rule.structural_pattern,
242
+ "--lang", lang,
243
+ "--json=stream",
244
+ *files,
245
+ ]
246
+ try:
247
+ result = subprocess.run(
248
+ cmd,
249
+ capture_output=True,
250
+ text=True,
251
+ timeout=_AST_GREP_TIMEOUT,
252
+ check=False,
253
+ )
254
+ except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
255
+ logger.warning(
256
+ "ast-grep failed for rule %s lang=%s: %s",
257
+ rule.qualified_id, lang, exc,
258
+ )
259
+ continue
260
+
261
+ # ast-grep returns non-zero when *no* matches are found. Only treat
262
+ # stderr-with-actual-content as an error signal.
263
+ if result.returncode not in (0, 1) and result.stderr.strip():
264
+ logger.warning(
265
+ "ast-grep error for rule %s lang=%s: %s",
266
+ rule.qualified_id, lang, result.stderr.strip()[:200],
267
+ )
268
+ continue
269
+
270
+ for line in result.stdout.splitlines():
271
+ line = line.strip()
272
+ if not line:
273
+ continue
274
+ try:
275
+ match = json.loads(line)
276
+ except json.JSONDecodeError:
277
+ continue
278
+
279
+ file_path = match.get("file", "")
280
+ start = match.get("range", {}).get("start", {})
281
+ line_no = int(start.get("line", 0)) + 1 # ast-grep is 0-based
282
+ snippet = (match.get("lines") or "").rstrip()[:200]
283
+
284
+ captures: dict[str, str] = {}
285
+ metavars = match.get("metaVariables") or match.get("metavariables") or {}
286
+ singles = metavars.get("single") or {}
287
+ for var_name, info in singles.items():
288
+ text = info.get("text") if isinstance(info, dict) else None
289
+ if isinstance(text, str):
290
+ captures[var_name] = text
291
+ # ast-grep emits ``$$$X`` (variadic) bindings under ``multi``.
292
+ # Concatenate the segment texts so description templates that
293
+ # reference ``$X`` still resolve to something readable.
294
+ multis = metavars.get("multi") or {}
295
+ for var_name, segments in multis.items():
296
+ if var_name in captures or not isinstance(segments, list):
297
+ continue
298
+ parts = [
299
+ s.get("text", "") for s in segments
300
+ if isinstance(s, dict) and isinstance(s.get("text"), str)
301
+ ]
302
+ if parts:
303
+ captures[var_name] = "".join(parts)
304
+
305
+ description = rule.description
306
+ if captures:
307
+ description = interpolate_message(description, captures)
308
+
309
+ suggested_fix = ""
310
+ if rule.fix:
311
+ if rule.fix.uses_metavars and captures:
312
+ sf_search, sf_replace = apply_metavar_fix(
313
+ rule.fix.search, rule.fix.replace, captures,
314
+ )
315
+ suggested_fix = f"{sf_search} → {sf_replace}"
316
+ else:
317
+ suggested_fix = f"{rule.fix.search} → {rule.fix.replace}"
318
+
319
+ rel_path = file_path
320
+ if project_dir:
321
+ try:
322
+ rel_path = os.path.relpath(file_path, project_dir)
323
+ except ValueError:
324
+ pass
325
+
326
+ findings.append(EnrichedFinding(
327
+ rule_id=rule.qualified_id,
328
+ rule_name=rule.name,
329
+ severity=rule.severity,
330
+ category=rule.category,
331
+ confidence=rule.confidence,
332
+ file=rel_path,
333
+ line=line_no,
334
+ code_snippet=snippet,
335
+ description=description,
336
+ explanation=rule.explanation,
337
+ recommendation=rule.recommendation,
338
+ examples=list(rule.examples),
339
+ suggested_fix=suggested_fix,
340
+ captures=captures,
341
+ cwe=rule.cwe,
342
+ pack=rule.pack,
343
+ tags=list(rule.tags),
344
+ ))
345
+ return findings
346
+
347
+
348
+ def execute_rules(
349
+ files: list[str],
350
+ rules: list[UnifiedRule],
351
+ *,
352
+ project_dir: str = "",
353
+ ) -> list[EnrichedFinding]:
354
+ """Execute rules against a list of files.
355
+
356
+ Args:
357
+ files: Absolute file paths to scan.
358
+ rules: Rules to execute (should be pre-filtered to enabled only).
359
+ project_dir: Project root for relative path computation.
360
+
361
+ Returns:
362
+ List of EnrichedFinding (partially populated — use enricher for full context).
363
+ """
364
+ findings: list[EnrichedFinding] = []
365
+
366
+ # ----- Tier 2: structural rules via ast-grep --------------------------
367
+ structural_rules = [
368
+ r for r in rules
369
+ if r.tier == RuleTier.STRUCTURAL and r.structural_pattern
370
+ ]
371
+ if structural_rules:
372
+ if _ast_grep_available():
373
+ files_by_lang: dict[str, list[str]] = {}
374
+ for fp in files:
375
+ lang = detect_language(fp)
376
+ if lang:
377
+ files_by_lang.setdefault(lang, []).append(fp)
378
+ for rule in structural_rules:
379
+ findings.extend(_execute_structural_rule(
380
+ rule, files_by_lang, project_dir=project_dir,
381
+ ))
382
+ else:
383
+ logger.info(
384
+ "ast-grep not on PATH; skipping %d structural rule(s). "
385
+ "Install with: cargo install ast-grep || brew install ast-grep",
386
+ len(structural_rules),
387
+ )
388
+
389
+ # Pre-group rules by language for fast lookup. Tier-1 rules always run
390
+ # here; Tier-2 (structural) rules also run here as a regex fallback
391
+ # for languages where ast-grep can't help — either because the language
392
+ # has no ast-grep grammar, or because the binary is missing entirely.
393
+ # Roadmap Phase 1: "Fallback to regex Tier 1 for languages without
394
+ # ast-grep grammars".
395
+ ast_grep_ok = _ast_grep_available()
396
+ lang_rules: dict[str, list[UnifiedRule]] = {}
397
+ universal_rules: list[UnifiedRule] = []
398
+
399
+ for rule in rules:
400
+ if rule.pattern is None and rule.composite_pattern is None:
401
+ continue # no executable pattern
402
+
403
+ regex_langs: list[str]
404
+ if rule.tier == RuleTier.REGEX:
405
+ regex_langs = list(rule.languages)
406
+ regex_universal = not rule.languages
407
+ elif rule.tier == RuleTier.STRUCTURAL:
408
+ # Only run the regex fallback when ast-grep can't cover the rule.
409
+ if ast_grep_ok:
410
+ if not rule.languages:
411
+ continue # universal structural — ast-grep handled it
412
+ regex_langs = [
413
+ lang for lang in rule.languages
414
+ if lang not in _AST_GREP_LANGS
415
+ ]
416
+ regex_universal = False
417
+ if not regex_langs:
418
+ continue
419
+ else:
420
+ # ast-grep missing — fall back to regex everywhere the rule
421
+ # was meant to apply.
422
+ regex_langs = list(rule.languages)
423
+ regex_universal = not rule.languages
424
+ else:
425
+ continue # plugin tier
426
+
427
+ if regex_universal:
428
+ universal_rules.append(rule)
429
+ else:
430
+ for lang in regex_langs:
431
+ lang_rules.setdefault(lang, []).append(rule)
432
+
433
+ for file_path in files:
434
+ lang = detect_language(file_path)
435
+ applicable = list(universal_rules)
436
+ if lang:
437
+ applicable.extend(lang_rules.get(lang, []))
438
+ if not applicable:
439
+ continue
440
+
441
+ try:
442
+ content = Path(file_path).read_text(encoding="utf-8", errors="replace")
443
+ except OSError:
444
+ continue
445
+
446
+ lines = content.splitlines()
447
+ rel_path = file_path
448
+ if project_dir:
449
+ try:
450
+ rel_path = os.path.relpath(file_path, project_dir)
451
+ except ValueError:
452
+ pass
453
+
454
+ for i, line in enumerate(lines):
455
+ line_no = i + 1
456
+ is_comment = _is_comment_line(line, lang)
457
+
458
+ for rule in applicable:
459
+ if is_comment and not rule.scan_comments:
460
+ continue
461
+
462
+ captures: dict[str, str] = {}
463
+
464
+ # --- Composite pattern path ---
465
+ if rule.composite_pattern is not None:
466
+ ctx = MatchContext(
467
+ line=line, line_no=line_no, all_lines=lines,
468
+ )
469
+ if not rule.composite_pattern.evaluate(ctx):
470
+ continue
471
+ captures = ctx.captures
472
+
473
+ # --- Simple pattern path ---
474
+ elif rule.pattern is not None:
475
+ match = rule.pattern.search(line)
476
+ if not match:
477
+ continue
478
+ if rule.metavars:
479
+ captures = {
480
+ k: v for k, v in match.groupdict().items()
481
+ if v is not None
482
+ }
483
+ if not check_metavar_constraints(
484
+ captures, rule.metavar_regex, rule.metavar_comparison,
485
+ ):
486
+ continue
487
+ else:
488
+ continue
489
+
490
+ # Build description with metavar interpolation
491
+ description = rule.description
492
+ if captures:
493
+ description = interpolate_message(description, captures)
494
+
495
+ # Build suggested fix
496
+ suggested_fix = ""
497
+ if rule.fix:
498
+ if rule.fix.uses_metavars and captures:
499
+ sf_search, sf_replace = apply_metavar_fix(
500
+ rule.fix.search, rule.fix.replace, captures,
501
+ )
502
+ suggested_fix = f"{sf_search} \u2192 {sf_replace}"
503
+ else:
504
+ suggested_fix = f"{rule.fix.search} \u2192 {rule.fix.replace}"
505
+
506
+ findings.append(EnrichedFinding(
507
+ rule_id=rule.qualified_id,
508
+ rule_name=rule.name,
509
+ severity=rule.severity,
510
+ category=rule.category,
511
+ confidence=rule.confidence,
512
+ file=rel_path,
513
+ line=line_no,
514
+ # Provider redaction must see the complete literal before
515
+ # imposing a payload budget (a truncated key may not match).
516
+ code_snippet=line.rstrip(),
517
+ description=description,
518
+ explanation=rule.explanation,
519
+ recommendation=rule.recommendation,
520
+ examples=list(rule.examples),
521
+ suggested_fix=suggested_fix,
522
+ captures=dict(captures),
523
+ cwe=rule.cwe,
524
+ pack=rule.pack,
525
+ tags=list(rule.tags),
526
+ ))
527
+
528
+ # Sort: severity first, then file, then line
529
+ _sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
530
+ findings.sort(key=lambda f: (_sev_order.get(f.severity, 9), f.file, f.line))
531
+ return findings
@@ -0,0 +1,5 @@
1
+ """Deterministic pre-filter pipeline for findings.
2
+
3
+ Reduces noise before the connected agent sees findings.
4
+ No LLM needed — these are all mechanical, deterministic filters.
5
+ """
@@ -0,0 +1,151 @@
1
+ """Pre-filter pipeline — orchestrates deterministic finding filters.
2
+
3
+ Reduces noise before the connected coding agent sees findings:
4
+ 1. Dedup — remove exact duplicates and merge overlapping findings
5
+ 2. Test file adjuster — lower severity for test/spec files
6
+ 3. Confidence threshold — drop findings below minimum confidence
7
+
8
+ Deterministic by default. Setting ATTOCODE_FLAG_CONFIDENCE adds one model call
9
+ per finding between stages 2 and 3, which second-guesses the constants.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ import re
17
+
18
+ from attocode_intel.rules.model import EnrichedFinding, RuleSeverity
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Test file patterns
23
+ _TEST_FILE_PATTERNS = [
24
+ re.compile(r"test[_/]"),
25
+ re.compile(r"_test\.\w+$"),
26
+ re.compile(r"\.test\.\w+$"),
27
+ re.compile(r"\.spec\.\w+$"),
28
+ re.compile(r"__tests__/"),
29
+ re.compile(r"tests?/"),
30
+ re.compile(r"conftest\.py$"),
31
+ re.compile(r"fixtures?/"),
32
+ ]
33
+
34
+
35
+ def _is_test_file(file_path: str) -> bool:
36
+ normalized = file_path.replace(os.sep, "/")
37
+ return any(p.search(normalized) for p in _TEST_FILE_PATTERNS)
38
+
39
+
40
+ # Severity demotion map for test files
41
+ _SEVERITY_DEMOTION = {
42
+ RuleSeverity.CRITICAL: RuleSeverity.HIGH,
43
+ RuleSeverity.HIGH: RuleSeverity.MEDIUM,
44
+ RuleSeverity.MEDIUM: RuleSeverity.LOW,
45
+ RuleSeverity.LOW: RuleSeverity.INFO,
46
+ }
47
+
48
+
49
+ def dedup_findings(findings: list[EnrichedFinding]) -> list[EnrichedFinding]:
50
+ """Remove exact duplicates and merge overlapping findings.
51
+
52
+ Two findings overlap if they're on the same file:line with the same
53
+ category. Keep the one with higher confidence.
54
+ """
55
+ seen: dict[tuple[str, int, str], EnrichedFinding] = {}
56
+ for f in findings:
57
+ key = (f.file, f.line, str(f.category))
58
+ existing = seen.get(key)
59
+ if existing is None or f.confidence >= existing.confidence:
60
+ if existing is not None and existing.rule_id != f.rule_id:
61
+ f.related_findings = list(set(f.related_findings + [existing.rule_id]))
62
+ seen[key] = f
63
+ elif existing.rule_id != f.rule_id:
64
+ # f lost the confidence race — record its existence on the winner
65
+ existing.related_findings = list(set(existing.related_findings + [f.rule_id]))
66
+
67
+ return list(seen.values())
68
+
69
+
70
+ def adjust_test_file_severity(
71
+ findings: list[EnrichedFinding],
72
+ ) -> list[EnrichedFinding]:
73
+ """Lower severity for findings in test files."""
74
+ for f in findings:
75
+ if _is_test_file(f.file):
76
+ new_sev = _SEVERITY_DEMOTION.get(f.severity)
77
+ if new_sev:
78
+ f.severity = new_sev
79
+ f.confidence *= 0.8 # reduce confidence for test files
80
+ return findings
81
+
82
+
83
+ def filter_by_confidence(
84
+ findings: list[EnrichedFinding],
85
+ min_confidence: float = 0.5,
86
+ ) -> list[EnrichedFinding]:
87
+ """Drop findings below confidence threshold."""
88
+ return [f for f in findings if f.confidence >= min_confidence]
89
+
90
+
91
+ def run_pipeline(
92
+ findings: list[EnrichedFinding],
93
+ *,
94
+ min_confidence: float = 0.5,
95
+ adjust_test_severity: bool = True,
96
+ project_dir: str = "",
97
+ ) -> list[EnrichedFinding]:
98
+ """Run the full deterministic pre-filter pipeline.
99
+
100
+ Pipeline stages:
101
+ 1. Dedup overlapping findings
102
+ 2. Adjust severity for test files
103
+ 2b. Estimate p(true positive), if ATTOCODE_FLAG_CONFIDENCE is set
104
+ 3. Apply confidence threshold
105
+
106
+ Args:
107
+ findings: Raw findings from executor.
108
+ min_confidence: Minimum confidence to keep (default 0.5).
109
+ adjust_test_severity: Whether to demote test file severity.
110
+
111
+ Returns:
112
+ Filtered findings ready for the agent.
113
+ """
114
+ original_count = len(findings)
115
+
116
+ # 1. Dedup
117
+ findings = dedup_findings(findings)
118
+ dedup_removed = original_count - len(findings)
119
+
120
+ # 2. Test file severity adjustment
121
+ if adjust_test_severity:
122
+ findings = adjust_test_file_severity(findings)
123
+
124
+ # 2b. Second-guess the constants (no-op unless ATTOCODE_FLAG_CONFIDENCE is set).
125
+ # Before the threshold on purpose: in live mode the threshold gates on
126
+ # the estimate rather than on the constant.
127
+ from attocode_intel import confidence
128
+ from attocode_intel.confidence.settings import workspace
129
+
130
+ with workspace(project_dir):
131
+ if confidence.settings.selected() != "off" and project_dir:
132
+ from attocode_intel.rules.enricher import enrich_findings
133
+
134
+ enrich_findings(findings, project_dir=project_dir)
135
+ confidence.score(findings, min_confidence=min_confidence)
136
+
137
+ # 3. Confidence threshold
138
+ findings = filter_by_confidence(findings, min_confidence)
139
+ conf_removed = original_count - dedup_removed - len(findings)
140
+
141
+ if dedup_removed > 0 or conf_removed > 0:
142
+ logger.info(
143
+ "Pre-filter: %d → %d findings (-%d dedup, -%d low-confidence)",
144
+ original_count, len(findings), dedup_removed, max(0, conf_removed),
145
+ )
146
+
147
+ # Re-sort after filtering
148
+ _sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
149
+ findings.sort(key=lambda f: (_sev_order.get(f.severity, 9), f.file, f.line))
150
+
151
+ return findings