evalrx 0.1.2__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 (339) hide show
  1. evalrx/__init__.py +139 -0
  2. evalrx/agent_assets/__init__.py +2 -0
  3. evalrx/agent_assets/skills/README.md +28 -0
  4. evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
  5. evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
  6. evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
  7. evalrx/agent_assets/skills/nature-figure/README.md +412 -0
  8. evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
  9. evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
  10. evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
  11. evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
  12. evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
  13. evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
  14. evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
  15. evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
  16. evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
  17. evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
  18. evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
  19. evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
  20. evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
  21. evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
  22. evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
  23. evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
  24. evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
  25. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
  26. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
  27. evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
  28. evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
  29. evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
  30. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
  31. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
  32. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
  33. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
  34. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
  35. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
  36. evalrx/agent_assets/skills.py +27 -0
  37. evalrx/agent_runtime/__init__.py +78 -0
  38. evalrx/agent_runtime/_docker_runner.py +89 -0
  39. evalrx/agent_runtime/cli_runtime.py +103 -0
  40. evalrx/agent_runtime/cli_transcript.py +138 -0
  41. evalrx/agent_runtime/cli_types.py +68 -0
  42. evalrx/agent_runtime/codegen/__init__.py +5 -0
  43. evalrx/agent_runtime/codegen/runner.py +94 -0
  44. evalrx/agent_runtime/experiment_harness.py +117 -0
  45. evalrx/agent_runtime/factory.py +102 -0
  46. evalrx/agent_runtime/json_shape.py +44 -0
  47. evalrx/agent_runtime/judges/__init__.py +28 -0
  48. evalrx/agent_runtime/judges/agy.py +179 -0
  49. evalrx/agent_runtime/judges/autodetect.py +135 -0
  50. evalrx/agent_runtime/judges/claude.py +159 -0
  51. evalrx/agent_runtime/judges/codex.py +120 -0
  52. evalrx/agent_runtime/providers/__init__.py +21 -0
  53. evalrx/agent_runtime/providers/antigravity.py +31 -0
  54. evalrx/agent_runtime/providers/base.py +145 -0
  55. evalrx/agent_runtime/providers/claude_code.py +49 -0
  56. evalrx/agent_runtime/providers/codex.py +37 -0
  57. evalrx/agent_runtime/providers/gemini_cli.py +26 -0
  58. evalrx/agent_runtime/providers/kimi_cli.py +27 -0
  59. evalrx/agent_runtime/providers/opencode.py +27 -0
  60. evalrx/agent_runtime/providers/registry.py +58 -0
  61. evalrx/agent_runtime/sandbox.py +517 -0
  62. evalrx/agent_runtime/skill_audit.py +143 -0
  63. evalrx/agent_runtime/skills/__init__.py +19 -0
  64. evalrx/agent_runtime/skills/installer.py +68 -0
  65. evalrx/agent_runtime/skills/prompt_policy.py +86 -0
  66. evalrx/agent_runtime/skills/resolver.py +19 -0
  67. evalrx/analysis/__init__.py +132 -0
  68. evalrx/analysis/adjudicate.py +154 -0
  69. evalrx/analysis/analysis_module.py +361 -0
  70. evalrx/analysis/api.py +171 -0
  71. evalrx/analysis/case_studio.py +651 -0
  72. evalrx/analysis/cli.py +114 -0
  73. evalrx/analysis/dashboard.py +350 -0
  74. evalrx/analysis/eval_case_matrix.py +118 -0
  75. evalrx/analysis/eval_viz_theme.py +833 -0
  76. evalrx/analysis/explore_run.py +333 -0
  77. evalrx/analysis/explorer.py +1276 -0
  78. evalrx/analysis/failure_modes.py +607 -0
  79. evalrx/analysis/fused_pipeline.py +489 -0
  80. evalrx/analysis/holdout.py +300 -0
  81. evalrx/analysis/hypothesis_agent.py +230 -0
  82. evalrx/analysis/narration.py +177 -0
  83. evalrx/analysis/operationalize.py +442 -0
  84. evalrx/analysis/plain_language.py +42 -0
  85. evalrx/analysis/planner.py +283 -0
  86. evalrx/analysis/probe_search.py +203 -0
  87. evalrx/analysis/profile.py +268 -0
  88. evalrx/analysis/prompts/__init__.py +0 -0
  89. evalrx/analysis/prompts/explorer.py +417 -0
  90. evalrx/analysis/prompts/failure_modes.py +33 -0
  91. evalrx/analysis/prompts/holdout.py +27 -0
  92. evalrx/analysis/prompts/hypothesis_agent.py +78 -0
  93. evalrx/analysis/prompts/run_codebase.py +47 -0
  94. evalrx/analysis/prompts/stats_agent.py +72 -0
  95. evalrx/analysis/prompts/stats_tool_generator.py +43 -0
  96. evalrx/analysis/result_marker.py +47 -0
  97. evalrx/analysis/run_codebase.py +242 -0
  98. evalrx/analysis/run_view.py +205 -0
  99. evalrx/analysis/stage_views.py +93 -0
  100. evalrx/analysis/stats_agent.py +944 -0
  101. evalrx/analysis/stats_tool_agent.py +261 -0
  102. evalrx/analysis/stats_tool_generator.py +415 -0
  103. evalrx/analysis/stats_tools.py +1153 -0
  104. evalrx/analysis/trajectory_records.py +193 -0
  105. evalrx/analysis/workbench.py +431 -0
  106. evalrx/analyzers/__init__.py +42 -0
  107. evalrx/analyzers/agent/__init__.py +25 -0
  108. evalrx/analyzers/agent/counterfactual.py +84 -0
  109. evalrx/analyzers/agent/first_error_judge.py +96 -0
  110. evalrx/analyzers/agent/ignored_obs.py +81 -0
  111. evalrx/analyzers/agent/loop_detect.py +79 -0
  112. evalrx/analyzers/agent/reliability.py +165 -0
  113. evalrx/analyzers/agent/tool_shap.py +225 -0
  114. evalrx/analyzers/agent/trajectory_rubric.py +168 -0
  115. evalrx/analyzers/attention/__init__.py +19 -0
  116. evalrx/analyzers/attention/relative_attn.py +610 -0
  117. evalrx/analyzers/attention/rollout.py +73 -0
  118. evalrx/analyzers/attention/sink.py +56 -0
  119. evalrx/analyzers/attention/summary.py +190 -0
  120. evalrx/analyzers/attribution/__init__.py +6 -0
  121. evalrx/analyzers/attribution/generic_attn.py +31 -0
  122. evalrx/analyzers/attribution/gradcam.py +30 -0
  123. evalrx/analyzers/base.py +12 -0
  124. evalrx/analyzers/geometry/__init__.py +6 -0
  125. evalrx/analyzers/geometry/cka.py +70 -0
  126. evalrx/analyzers/geometry/linear_probe.py +157 -0
  127. evalrx/analyzers/hallucination/__init__.py +9 -0
  128. evalrx/analyzers/hallucination/chair.py +78 -0
  129. evalrx/analyzers/hallucination/opera.py +29 -0
  130. evalrx/analyzers/hallucination/pope.py +119 -0
  131. evalrx/analyzers/hallucination/selfcheck.py +155 -0
  132. evalrx/analyzers/hallucination/vcd.py +29 -0
  133. evalrx/analyzers/lens/__init__.py +7 -0
  134. evalrx/analyzers/lens/layer_contrast.py +133 -0
  135. evalrx/analyzers/lens/logit_lens.py +138 -0
  136. evalrx/analyzers/lens/tuned_lens.py +30 -0
  137. evalrx/analyzers/patching/__init__.py +5 -0
  138. evalrx/analyzers/patching/causal_trace.py +30 -0
  139. evalrx/analyzers/perturbation/__init__.py +23 -0
  140. evalrx/analyzers/perturbation/_shapley.py +54 -0
  141. evalrx/analyzers/perturbation/context_shap.py +174 -0
  142. evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
  143. evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
  144. evalrx/analyzers/perturbation/mm_shap.py +146 -0
  145. evalrx/analyzers/perturbation/modality_ablation.py +196 -0
  146. evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
  147. evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
  148. evalrx/analyzers/perturbation/rise.py +94 -0
  149. evalrx/analyzers/perturbation/vl_shap.py +102 -0
  150. evalrx/analyzers/reasoning/__init__.py +33 -0
  151. evalrx/analyzers/reasoning/_text.py +328 -0
  152. evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
  153. evalrx/analyzers/reasoning/arith_audit.py +226 -0
  154. evalrx/analyzers/reasoning/contamination.py +214 -0
  155. evalrx/analyzers/reasoning/knowledge_split.py +253 -0
  156. evalrx/analyzers/reasoning/self_repair.py +246 -0
  157. evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
  158. evalrx/analyzers/reasoning/termination_audit.py +258 -0
  159. evalrx/analyzers/uncertainty/__init__.py +18 -0
  160. evalrx/analyzers/uncertainty/calibration.py +174 -0
  161. evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
  162. evalrx/analyzers/uncertainty/entropy.py +90 -0
  163. evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
  164. evalrx/analyzers/uncertainty/self_consistency.py +204 -0
  165. evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
  166. evalrx/cli.py +411 -0
  167. evalrx/config.py +77 -0
  168. evalrx/contract/__init__.py +179 -0
  169. evalrx/contract/common.py +452 -0
  170. evalrx/contract/emit.py +948 -0
  171. evalrx/contract/export.py +237 -0
  172. evalrx/contract/m1.py +325 -0
  173. evalrx/contract/m2.py +317 -0
  174. evalrx/contract/m3.py +165 -0
  175. evalrx/contract/m4.py +130 -0
  176. evalrx/contract/m5.py +292 -0
  177. evalrx/contract/methodology.py +76 -0
  178. evalrx/contract/pre_m1.py +58 -0
  179. evalrx/contract/typescript.py +140 -0
  180. evalrx/core/__init__.py +85 -0
  181. evalrx/core/analyzer.py +174 -0
  182. evalrx/core/capability.py +54 -0
  183. evalrx/core/case.py +443 -0
  184. evalrx/core/experiment.py +106 -0
  185. evalrx/core/model.py +198 -0
  186. evalrx/core/pipeline.py +42 -0
  187. evalrx/core/registry.py +142 -0
  188. evalrx/core/result.py +64 -0
  189. evalrx/core/spec.py +173 -0
  190. evalrx/core/tokentype.py +165 -0
  191. evalrx/core/tool.py +92 -0
  192. evalrx/datasets/__init__.py +41 -0
  193. evalrx/datasets/base.py +68 -0
  194. evalrx/datasets/gui_os.py +52 -0
  195. evalrx/datasets/llm_qa.py +57 -0
  196. evalrx/datasets/pure_qa.py +12 -0
  197. evalrx/datasets/vlm_qa.py +695 -0
  198. evalrx/datasets/web_search_qa.py +52 -0
  199. evalrx/eval_agent/__init__.py +341 -0
  200. evalrx/eval_agent/_tools.py +81 -0
  201. evalrx/eval_agent/ab_runner.py +50 -0
  202. evalrx/eval_agent/agentic/__init__.py +43 -0
  203. evalrx/eval_agent/agentic/actions.py +216 -0
  204. evalrx/eval_agent/agentic/board.py +107 -0
  205. evalrx/eval_agent/agentic/loop.py +190 -0
  206. evalrx/eval_agent/agentic/tools.py +538 -0
  207. evalrx/eval_agent/checkpoint.py +57 -0
  208. evalrx/eval_agent/cli_agent.py +59 -0
  209. evalrx/eval_agent/cli_skills.py +5 -0
  210. evalrx/eval_agent/evolution.py +396 -0
  211. evalrx/eval_agent/git_manager.py +215 -0
  212. evalrx/eval_agent/hypothesis.py +172 -0
  213. evalrx/eval_agent/label_quarantine.py +209 -0
  214. evalrx/eval_agent/legacy.py +530 -0
  215. evalrx/eval_agent/log_schema.py +497 -0
  216. evalrx/eval_agent/loop.py +2159 -0
  217. evalrx/eval_agent/loop_reports.py +116 -0
  218. evalrx/eval_agent/model_instrumentation.py +282 -0
  219. evalrx/eval_agent/narration.py +193 -0
  220. evalrx/eval_agent/nl_runner.py +460 -0
  221. evalrx/eval_agent/orchestrator.py +61 -0
  222. evalrx/eval_agent/preregister.py +93 -0
  223. evalrx/eval_agent/prompts/__init__.py +1 -0
  224. evalrx/eval_agent/prompts/agentic.py +46 -0
  225. evalrx/eval_agent/prompts/case_discovery.py +25 -0
  226. evalrx/eval_agent/prompts/diagnosis.py +125 -0
  227. evalrx/eval_agent/prompts/experiment_writer.py +265 -0
  228. evalrx/eval_agent/prompts/explore_step.py +37 -0
  229. evalrx/eval_agent/prompts/fix_agent.py +257 -0
  230. evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
  231. evalrx/eval_agent/prompts/nl_runner.py +38 -0
  232. evalrx/eval_agent/prompts/probe_agent.py +25 -0
  233. evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
  234. evalrx/eval_agent/prompts/probe_generator.py +35 -0
  235. evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
  236. evalrx/eval_agent/report.py +58 -0
  237. evalrx/eval_agent/run_context.py +354 -0
  238. evalrx/eval_agent/run_log.schema.json +1215 -0
  239. evalrx/eval_agent/run_logger_v2.py +1764 -0
  240. evalrx/eval_agent/run_metadata.py +208 -0
  241. evalrx/eval_agent/stages/__init__.py +56 -0
  242. evalrx/eval_agent/stages/case_discovery.py +293 -0
  243. evalrx/eval_agent/stages/diagnosis.py +1017 -0
  244. evalrx/eval_agent/stages/experiment_writer.py +1634 -0
  245. evalrx/eval_agent/stages/fix_agent.py +3916 -0
  246. evalrx/eval_agent/stages/fix_internals.py +499 -0
  247. evalrx/eval_agent/stages/fix_pipeline.py +725 -0
  248. evalrx/eval_agent/stages/fix_tiers.py +187 -0
  249. evalrx/eval_agent/stages/fix_tools.py +1034 -0
  250. evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
  251. evalrx/eval_agent/stages/probe.py +439 -0
  252. evalrx/eval_agent/stages/probe_agent.py +1079 -0
  253. evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
  254. evalrx/eval_agent/stages/probe_generator.py +326 -0
  255. evalrx/eval_agent/stages/probe_search_agent.py +106 -0
  256. evalrx/eval_agent/stages/protocol.py +112 -0
  257. evalrx/eval_agent/stages/repair_catalog.py +273 -0
  258. evalrx/eval_agent/stages/surgery.py +524 -0
  259. evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
  260. evalrx/eval_agent/store.py +231 -0
  261. evalrx/logging_utils.py +112 -0
  262. evalrx/models/__init__.py +161 -0
  263. evalrx/models/_discover.py +101 -0
  264. evalrx/models/agent.py +380 -0
  265. evalrx/models/backends/__init__.py +58 -0
  266. evalrx/models/backends/api.py +169 -0
  267. evalrx/models/backends/base.py +57 -0
  268. evalrx/models/backends/gemini_compat.py +579 -0
  269. evalrx/models/backends/hf_local.py +2074 -0
  270. evalrx/models/backends/openai_compat.py +301 -0
  271. evalrx/models/backends/vllm_offline.py +116 -0
  272. evalrx/models/base.py +24 -0
  273. evalrx/models/blackbox/__init__.py +4 -0
  274. evalrx/models/blackbox/agent.py +31 -0
  275. evalrx/models/blackbox/base.py +29 -0
  276. evalrx/models/blackbox/gemini.py +279 -0
  277. evalrx/models/blackbox/llm_api.py +17 -0
  278. evalrx/models/blackbox/vlm_api.py +17 -0
  279. evalrx/models/compose.py +66 -0
  280. evalrx/models/inference.py +88 -0
  281. evalrx/models/paper_methods/__init__.py +8 -0
  282. evalrx/models/paper_methods/aad.py +53 -0
  283. evalrx/models/paper_methods/ifcd.py +204 -0
  284. evalrx/models/paper_methods/pai.py +164 -0
  285. evalrx/models/paper_methods/tcd.py +202 -0
  286. evalrx/models/paper_methods/vcd.py +45 -0
  287. evalrx/models/paper_methods/vicrop.py +137 -0
  288. evalrx/models/toolcodec.py +143 -0
  289. evalrx/models/tools/__init__.py +20 -0
  290. evalrx/models/tools/perception.py +300 -0
  291. evalrx/models/tools/visual.py +174 -0
  292. evalrx/models/whitebox/__init__.py +26 -0
  293. evalrx/models/whitebox/agent.py +31 -0
  294. evalrx/models/whitebox/base.py +24 -0
  295. evalrx/models/whitebox/qwen.py +61 -0
  296. evalrx/models/whitebox/qwen2_5_omni.py +29 -0
  297. evalrx/models/whitebox/qwen2_audio.py +25 -0
  298. evalrx/models/whitebox/qwen_omni.py +53 -0
  299. evalrx/models/whitebox/qwen_vl.py +62 -0
  300. evalrx/observability/__init__.py +21 -0
  301. evalrx/observability/envelope.py +122 -0
  302. evalrx/observability/outbox.py +111 -0
  303. evalrx/observability/tracer.py +882 -0
  304. evalrx/reporting/__init__.py +28 -0
  305. evalrx/reporting/case_study.py +947 -0
  306. evalrx/reporting/compiler.py +587 -0
  307. evalrx/reporting/dynamic.py +1882 -0
  308. evalrx/reporting/html_report.py +2225 -0
  309. evalrx/reporting/langfuse_exporter.py +38 -0
  310. evalrx/reporting/langfuse_source.py +155 -0
  311. evalrx/reporting/model.py +151 -0
  312. evalrx/reporting/run_events.py +184 -0
  313. evalrx/reporting/server.py +557 -0
  314. evalrx/reporting/stages.py +58 -0
  315. evalrx/reporting/static_export.py +142 -0
  316. evalrx/reporting/web_dist/index.html +146 -0
  317. evalrx/specs.py +727 -0
  318. evalrx/stats/__init__.py +47 -0
  319. evalrx/stats/api.py +192 -0
  320. evalrx/stats/bootstrap.py +86 -0
  321. evalrx/stats/ebh.py +27 -0
  322. evalrx/stats/evalue.py +98 -0
  323. evalrx/stats/friedman.py +138 -0
  324. evalrx/stats/mcnemar.py +40 -0
  325. evalrx/stats/multiplicity.py +159 -0
  326. evalrx/stats/subset_sampling.py +55 -0
  327. evalrx/term_links.py +43 -0
  328. evalrx/viz/__init__.py +7 -0
  329. evalrx/viz/labels.py +77 -0
  330. evalrx/viz/prompts.py +39 -0
  331. evalrx/viz/renderer.py +590 -0
  332. evalrx/viz/schema.py +36 -0
  333. evalrx/viz/style.py +134 -0
  334. evalrx-0.1.2.dist-info/METADATA +532 -0
  335. evalrx-0.1.2.dist-info/RECORD +339 -0
  336. evalrx-0.1.2.dist-info/WHEEL +5 -0
  337. evalrx-0.1.2.dist-info/entry_points.txt +3 -0
  338. evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
  339. evalrx-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,283 @@
1
+ """Generic planning utilities for M2 analysis.
2
+
3
+ The planner consumes a profile of the available data and emits a deterministic
4
+ tool plan. It is intentionally conservative: discovery may inspect everything,
5
+ but confirmatory plans should make tested families explicit and avoid silently
6
+ dropping columns because of their original order.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from typing import Any
15
+
16
+ from evalrx.analysis.profile import DatasetProfile, profile_stats_input
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class AnalysisPlanItem:
23
+ """One planned statistical analysis."""
24
+
25
+ tool: str
26
+ config: dict[str, Any] = field(default_factory=dict)
27
+ rationale: str = ""
28
+ estimand: str = ""
29
+ family: str = "confirmatory"
30
+ priority: float = 0.0
31
+
32
+ def as_legacy_tuple(self) -> tuple[str, dict[str, Any], str]:
33
+ return self.tool, self.config, self.rationale
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ return {
37
+ "tool": self.tool,
38
+ "config": self.config,
39
+ "rationale": self.rationale,
40
+ "estimand": self.estimand,
41
+ "family": self.family,
42
+ "priority": self.priority,
43
+ }
44
+
45
+
46
+ def _signal_priority(inp: Any, signal: str, profile: DatasetProfile) -> float:
47
+ col = profile.columns.get(signal)
48
+ sigmap = (getattr(inp, "per_case", {}) or {}).get(signal, {})
49
+ labels = getattr(inp, "labels", {}) or {}
50
+ if not sigmap:
51
+ return 0.0
52
+ n_labeled = max(1, len(labels))
53
+ coverage = min(1.0, len(sigmap) / n_labeled)
54
+ values = [float(v) for v in sigmap.values()]
55
+ unique = len(set(values))
56
+ variance = 0.0
57
+ if values:
58
+ mean = sum(values) / len(values)
59
+ variance = sum((v - mean) ** 2 for v in values) / len(values)
60
+ kind_bonus = 0.2 if col and col.is_binary else 0.1 if col and col.dtype == "numeric" else 0.0
61
+ sparse_binary_bonus = 0.15 if col and col.is_binary and coverage < 0.95 else 0.0
62
+ constant_penalty = -1.0 if unique <= 1 else 0.0
63
+ return coverage + min(1.0, variance) + kind_bonus + sparse_binary_bonus + constant_penalty
64
+
65
+
66
+ #: Agreement above which a BINARY signal is the label wearing another name.
67
+ #: Not 1.0: one flipped case out of dozens is still a restatement, not a finding.
68
+ _LABEL_RESTATEMENT_AGREEMENT = 0.95
69
+ #: Below this many shared cases, perfect agreement is cheap and means nothing.
70
+ _MIN_CASES_TO_JUDGE_RESTATEMENT = 10
71
+ #: Names of fields an analyzer produces BY GRADING — i.e. by asking the same
72
+ #: question the label answers. Matched on the last dotted segment.
73
+ _CORRECTNESS_NAME = re.compile(
74
+ r"(?:^|_)(?:correct|incorrect|label|labelled|passed|is_pass)(?:_|$)"
75
+ r"|_match$|^match$"
76
+ )
77
+ # These answer-audit fields are defined using the gold and/or the stored
78
+ # PASS/FAIL label. A sparse flag can have low whole-batch agreement while
79
+ # still making ``P(FAIL | flag)=1`` by construction, so an association test is
80
+ # circular. They remain useful descriptive sanity signals, never candidate
81
+ # discriminators for M2/M4.
82
+ _LABEL_DERIVED_SUFFIXES = frozenset({
83
+ "answer_extraction_audit.extraction_suspect",
84
+ "answer_extraction_audit.extraction_point_miss",
85
+ "answer_extraction_audit.gold_in_output",
86
+ "answer_extraction_audit.gold_in_answer_region",
87
+ "answer_extraction_audit.strict_match",
88
+ "answer_extraction_audit.label_disagrees",
89
+ "answer_extraction_audit.labelled_fail",
90
+ # gold x pred conjunctions: P(FAIL | flag) = 1 by construction. Sparse
91
+ # enough to slip past label_leak_score, named nothing like "correct", so
92
+ # they sat in the VLM family as guaranteed BH survivors. The marginals
93
+ # (pope.answered_yes / pope.gold_yes) stay testable.
94
+ "pope.false_positive",
95
+ "pope.false_negative",
96
+ })
97
+
98
+
99
+ def restates_label(inp: Any, signal: str) -> bool:
100
+ """True when *signal* reproduces the PASS/FAIL label, or its complement.
101
+
102
+ Such a signal always rejects H0, always survives FDR, and carries no
103
+ information — "does the label predict the label". Left in the pool it also
104
+ CROWDS OUT the real ones, because the survivor list is what M4 draws on to
105
+ verify a hypothesis. Measured on qwen3.5-2b / bbh_causal_judgement: of 10
106
+ rejecting signals, 6 survived FDR and every one of those 6 was the label
107
+ (``labelled_fail``, ``strict_match``, ``calibration.correct``,
108
+ ``self_repair.baseline_correct`` / ``revised_correct``), while the two
109
+ genuinely informative extraction flags did not survive. M2's own narrative
110
+ called them out as "the label copied under another name"; M4 then verified a
111
+ hypothesis about label REPRODUCIBILITY using ``labelled_fail``, at
112
+ confidence 0.73.
113
+
114
+ Requires BOTH a correctness-shaped NAME and label agreement in the DATA,
115
+ because neither alone is safe and each fixes the other's failure:
116
+
117
+ * Data alone suppresses real findings. A composite discovered over
118
+ independent measurements — ``(obj_size < 40) and (attention < 0.3)`` —
119
+ can separate the labels perfectly, and that is the most valuable result
120
+ the loop can produce, not a tautology. It is statistically identical to
121
+ ``calibration.correct``; only its provenance differs.
122
+ * Name alone suppresses honest fields. A column called ``correct`` that
123
+ tracks something other than this batch's label is a legitimate signal.
124
+
125
+ Two further things this deliberately does NOT do:
126
+
127
+ * It does not judge arbitrary signals by the conditional rate. A sparse,
128
+ independently measured flag may legitimately have
129
+ ``P(FAIL | signal)=1``. The explicit answer-audit exceptions above are
130
+ removed by provenance because their definitions use gold/labels, not
131
+ because of their observed conditional rate.
132
+ * It does not touch continuous signals. Restatement is an identity claim,
133
+ and a continuous measure binarised at some threshold can drift into high
134
+ agreement without being the label at all.
135
+ """
136
+ labels = getattr(inp, "labels", {}) or {}
137
+ if str(signal).lower() in _LABEL_DERIVED_SUFFIXES:
138
+ return bool(labels)
139
+ if not _CORRECTNESS_NAME.search(str(signal).rsplit(".", 1)[-1].lower()):
140
+ return False
141
+ sigmap = (getattr(inp, "per_case", {}) or {}).get(signal, {})
142
+ shared = [key for key in sigmap if key in labels]
143
+ if len(shared) < _MIN_CASES_TO_JUDGE_RESTATEMENT:
144
+ return False
145
+ try:
146
+ values = {float(sigmap[key]) for key in shared}
147
+ except (TypeError, ValueError):
148
+ return False
149
+ if not values <= {0.0, 1.0} or len(values) < 2:
150
+ return False
151
+ agree = sum(1 for key in shared
152
+ if bool(float(sigmap[key])) == bool(labels[key]))
153
+ rate = agree / len(shared)
154
+ return (rate >= _LABEL_RESTATEMENT_AGREEMENT
155
+ or rate <= 1.0 - _LABEL_RESTATEMENT_AGREEMENT)
156
+
157
+
158
+ def label_restating_signals(inp: Any) -> list[str]:
159
+ """The signals :func:`ranked_signal_names` drops, so callers can report them.
160
+
161
+ Dropped rather than tested-and-flagged on purpose: the harm is not that the
162
+ result is wrong, it is that the result is RIGHT and meaningless, and every
163
+ such test consumes a slot in the FDR family that a real signal needed.
164
+ """
165
+ return sorted(
166
+ signal for signal in (getattr(inp, "per_case", {}) or {})
167
+ if restates_label(inp, signal)
168
+ )
169
+
170
+
171
+ def ranked_signal_names(inp: Any, *, max_signals: int | None = None) -> list[str]:
172
+ """Rank per-case signals by testability rather than insertion order."""
173
+ profile = profile_stats_input(inp)
174
+ scored = [
175
+ (_signal_priority(inp, signal, profile), signal)
176
+ for signal in (getattr(inp, "per_case", {}) or {})
177
+ if not restates_label(inp, signal)
178
+ ]
179
+ scored.sort(key=lambda item: (-item[0], item[1]))
180
+ names = [name for score, name in scored if score > 0]
181
+ if max_signals is not None:
182
+ names = names[:max(0, int(max_signals))]
183
+ return names
184
+
185
+
186
+ def plan_stats_input(
187
+ inp: Any,
188
+ *,
189
+ max_signals: int | None = None,
190
+ ) -> list[AnalysisPlanItem]:
191
+ """Create a deterministic confirmatory plan for a ``StatsInput`` object."""
192
+ labels = getattr(inp, "labels", {}) or {}
193
+ groups = getattr(inp, "groups", None) or {}
194
+ vectors = getattr(inp, "per_case_vectors", {}) or {}
195
+ n_fail = sum(1 for value in labels.values() if value)
196
+ n_pass = len(labels) - n_fail
197
+ plan: list[AnalysisPlanItem] = []
198
+
199
+ if n_pass > 0 and n_fail > 0 and getattr(inp, "per_case", None):
200
+ # Dropped, not silently: a suppressed signal that turns out to be real
201
+ # has to leave a trace somewhere, and this is the only place that knows.
202
+ dropped = label_restating_signals(inp)
203
+ if dropped:
204
+ logger.info(
205
+ "planner: dropped %d signal(s) that restate the PASS/FAIL label "
206
+ "and would crowd the FDR family: %s",
207
+ len(dropped), ", ".join(dropped))
208
+ ranked = ranked_signal_names(inp, max_signals=max_signals)
209
+ for key in ranked:
210
+ plan.append(AnalysisPlanItem(
211
+ tool="signal_label_assoc",
212
+ config={"signal": key},
213
+ rationale=f"test whether per-case signal '{key}' predicts FAIL",
214
+ estimand=f"P(FAIL | {key}=present/high) - P(FAIL | {key}=absent/low)",
215
+ priority=1.0,
216
+ ))
217
+ continuous = [
218
+ key for key in ranked
219
+ if key in (getattr(inp, "per_case", {}) or {})
220
+ and len({float(v) for v in inp.per_case[key].values()}) > 2
221
+ ]
222
+ for key in continuous:
223
+ plan.append(AnalysisPlanItem(
224
+ tool="rank_corr",
225
+ config={"signal": key},
226
+ rationale=f"monotonic association between continuous '{key}' and FAIL",
227
+ estimand=f"Kendall tau({key}, FAIL)",
228
+ family="descriptive",
229
+ priority=0.5,
230
+ ))
231
+
232
+ if n_pass > 0 and n_fail > 0 and vectors:
233
+ vector_names = list(vectors)
234
+ if max_signals is not None:
235
+ vector_names = vector_names[:max(0, int(max_signals))]
236
+ for key in vector_names:
237
+ plan.append(AnalysisPlanItem(
238
+ tool="attention_decoding",
239
+ config={"signal": key},
240
+ rationale=f"omnibus: do FAIL/PASS per-case maps '{key}' differ?",
241
+ estimand=f"distributional difference in vector signal {key}",
242
+ priority=0.8,
243
+ ))
244
+
245
+ if labels:
246
+ plan.append(AnalysisPlanItem(
247
+ tool="single_rate_evalue",
248
+ config={},
249
+ rationale="describe the overall FAIL rate against an explicit baseline if provided",
250
+ estimand="P(FAIL) - p0",
251
+ family="descriptive",
252
+ priority=0.1,
253
+ ))
254
+
255
+ n_groups = len(groups)
256
+ if n_groups >= 3:
257
+ plan.append(AnalysisPlanItem(
258
+ tool="friedman_nemenyi",
259
+ config={},
260
+ rationale="rank 3+ strategies on shared cases",
261
+ estimand="strategy rank differences",
262
+ priority=0.9,
263
+ ))
264
+ names = list(groups)
265
+ base = names[0]
266
+ for variant in names[1:]:
267
+ plan.append(AnalysisPlanItem(
268
+ tool="mcnemar_evalue",
269
+ config={"strategies": [base, variant]},
270
+ rationale=f"paired contrast: does '{variant}' repair '{base}' failures?",
271
+ estimand=f"{variant} success - {base} success on paired cases",
272
+ priority=1.0,
273
+ ))
274
+ elif n_groups == 2:
275
+ plan.append(AnalysisPlanItem(
276
+ tool="mcnemar_evalue",
277
+ config={},
278
+ rationale="paired two-strategy comparison",
279
+ estimand="strategy success difference on paired cases",
280
+ priority=1.0,
281
+ ))
282
+
283
+ return plan
@@ -0,0 +1,203 @@
1
+ """Hierarchical MCTS probing search (ProbeLLM-style adaptive test-case discovery).
2
+
3
+ Standalone tree-search skeleton: it knows nothing about models, judges, or
4
+ datasets — those are injected as three callables (``generate_macro``,
5
+ ``generate_micro``, ``verify``). This mirrors the paper's two-level design
6
+ (arXiv 2602.12966, "ProbeLLM"): a global choice between Macro (broad
7
+ coverage — diversify topics) and Micro (local refinement — densify evidence
8
+ around a seed) regimes, each running its own UCB-guided MCTS to pick which
9
+ node to expand next, and a shared node/backup bookkeeping (Eqs. 5-10 in the
10
+ paper).
11
+
12
+ Failure cases discovered here are plain ``FailureCase`` objects — the result
13
+ feeds directly into :func:`evalrx.analysis.failure_modes.cluster_failures`
14
+ for failure-mode synthesis.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import math
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, Callable
22
+
23
+ from evalrx.core.case import CaseBatch, FailureCase, Label
24
+
25
+ Regime = str # "macro" | "micro"
26
+
27
+ GenerateMacroFn = Callable[["ProbeNode", "list[ProbeNode]"], "FailureCase | None"]
28
+ GenerateMicroFn = Callable[["ProbeNode"], "FailureCase | None"]
29
+ VerifyFn = Callable[[FailureCase], FailureCase]
30
+
31
+
32
+ @dataclass
33
+ class ProbeNode:
34
+ """One evaluated test case in the search tree (paper Eq.5: (x, y*, y, fail)).
35
+
36
+ ``N``/``E`` are *this node's* expansion stats (paper Eq.6): how many times
37
+ a child was proposed+evaluated from here, and how many of those children
38
+ were failures. They are updated on nodes along the *selection path*
39
+ (root..this node), not on the freshly created child itself — a brand-new
40
+ node always starts at N=E=0 until it is later expanded.
41
+ """
42
+
43
+ case: FailureCase
44
+ regime: Regime
45
+ parent: "ProbeNode | None" = None
46
+ children: list["ProbeNode"] = field(default_factory=list)
47
+ N: int = 0
48
+ E: int = 0
49
+
50
+ def p_hat(self) -> float:
51
+ """Empirical failure rate of expansions from this node (Eq.6)."""
52
+ return self.E / max(1, self.N)
53
+
54
+ def is_fail(self) -> bool:
55
+ return self.case.label == Label.FAIL
56
+
57
+
58
+ @dataclass
59
+ class ProbeSearchResult:
60
+ """Output of one :meth:`ProbeSearch.run` call."""
61
+
62
+ macro_root: ProbeNode
63
+ micro_root: ProbeNode
64
+ n_simulations: int
65
+ n_macro: int
66
+ n_micro: int
67
+ failure_cases: CaseBatch
68
+ all_cases: CaseBatch
69
+
70
+ @property
71
+ def error_rate(self) -> float:
72
+ return len(self.failure_cases) / self.n_simulations if self.n_simulations else 0.0
73
+
74
+ def to_dict(self) -> dict[str, Any]:
75
+ return {
76
+ "n_simulations": self.n_simulations,
77
+ "n_macro": self.n_macro,
78
+ "n_micro": self.n_micro,
79
+ "error_rate": self.error_rate,
80
+ "n_failures": len(self.failure_cases),
81
+ }
82
+
83
+
84
+ def _ucb(node: ProbeNode, parent_n: int, beta: float) -> float:
85
+ """UCB(v|u) = p_hat(v) + beta*sqrt(log(max(1,N(u))) / max(1,N(v))) (Eq.7)."""
86
+ return node.p_hat() + beta * math.sqrt(math.log(max(1, parent_n)) / max(1, node.N))
87
+
88
+
89
+ def _select_expandable(root: ProbeNode, *, beta: float, w_max: int) -> ProbeNode:
90
+ """Descend via UCB while the current node is already at max width,
91
+ stopping at the first node with room for a new child (paper Eq.7-8, the
92
+ "continue this selection step until reaching an expandable node")."""
93
+ u = root
94
+ while u.children and len(u.children) >= w_max:
95
+ u = max(u.children, key=lambda v: _ucb(v, u.N, beta))
96
+ return u
97
+
98
+
99
+ def _choose_regime(macro_root: ProbeNode, micro_root: ProbeNode, beta: float) -> Regime:
100
+ """Top-level Macro/Micro choice, via the *same* UCB rule applied to the
101
+ two regime roots — the global root's two children in the paper's
102
+ hierarchy (Fig.2/Sec.3.2), rather than a fixed or alternating split."""
103
+ total_n = macro_root.N + micro_root.N
104
+ ucb_macro = _ucb(macro_root, total_n, beta)
105
+ ucb_micro = _ucb(micro_root, total_n, beta)
106
+ return "macro" if ucb_macro >= ucb_micro else "micro"
107
+
108
+
109
+ class ProbeSearch:
110
+ """Hierarchical MCTS driver.
111
+
112
+ Args:
113
+ generate_macro: ``(node, explored_macro_nodes) -> FailureCase | None``.
114
+ Proposes one new, *unevaluated* candidate case aimed
115
+ at broad topical coverage; receives every macro-tree
116
+ node explored so far so it can pick its own
117
+ diversity/frontier heuristic (e.g. embed + cluster
118
+ medoids, paper Eq.11) — this module stays agnostic
119
+ to how that is done.
120
+ generate_micro: ``(node) -> FailureCase | None``. Proposes one local
121
+ perturbation of ``node.case`` (paper's controlled
122
+ entity/attribute substitutions).
123
+ verify: ``(case) -> case``. Runs the target model + scorer,
124
+ returning the same case with ``.observed``/``.label``
125
+ populated (paper Eq.1's verifier V). A thin wrapper
126
+ around ``CaseDiscoveryAgent.discover`` in practice.
127
+ seeds: Initial seed cases (from an existing benchmark) used
128
+ to anchor both regime roots.
129
+ budget: Total number of simulations (T_max in Eq.4).
130
+ beta: UCB exploration constant (Eq.7).
131
+ w_max: Max children per node before progressive widening
132
+ forces a deeper UCB descent instead of a new sibling.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ *,
138
+ generate_macro: GenerateMacroFn,
139
+ generate_micro: GenerateMicroFn,
140
+ verify: VerifyFn,
141
+ seeds: "list[FailureCase] | CaseBatch",
142
+ budget: int = 20,
143
+ beta: float = 1.0,
144
+ w_max: int = 3,
145
+ ) -> None:
146
+ seeds = list(seeds)
147
+ if not seeds:
148
+ raise ValueError("ProbeSearch requires at least one seed case")
149
+ self._generate_macro = generate_macro
150
+ self._generate_micro = generate_micro
151
+ self._verify = verify
152
+ self.budget = budget
153
+ self.beta = beta
154
+ self.w_max = max(1, w_max)
155
+
156
+ self.macro_root = ProbeNode(case=seeds[0], regime="macro")
157
+ self.micro_root = ProbeNode(case=seeds[0], regime="micro")
158
+ self._macro_nodes: list[ProbeNode] = [self.macro_root]
159
+ self._evaluated: list[FailureCase] = []
160
+ self._n_macro = 0
161
+ self._n_micro = 0
162
+
163
+ def run(self) -> ProbeSearchResult:
164
+ for _ in range(self.budget):
165
+ regime = _choose_regime(self.macro_root, self.micro_root, self.beta)
166
+ root = self.macro_root if regime == "macro" else self.micro_root
167
+ u = _select_expandable(root, beta=self.beta, w_max=self.w_max)
168
+
169
+ new_case = (
170
+ self._generate_macro(u, self._macro_nodes)
171
+ if regime == "macro"
172
+ else self._generate_micro(u)
173
+ )
174
+ if new_case is None:
175
+ continue # generator exhausted for this node; budget step still spent
176
+
177
+ evaluated = self._verify(new_case)
178
+ child = ProbeNode(case=evaluated, regime=regime, parent=u)
179
+ u.children.append(child)
180
+ if regime == "macro":
181
+ self._macro_nodes.append(child)
182
+ self._n_macro += 1
183
+ else:
184
+ self._n_micro += 1
185
+ self._evaluated.append(evaluated)
186
+
187
+ fail = 1 if evaluated.label == Label.FAIL else 0
188
+ node: "ProbeNode | None" = u
189
+ while node is not None:
190
+ node.N += 1
191
+ node.E += fail
192
+ node = node.parent
193
+
194
+ failures = CaseBatch([c for c in self._evaluated if c.label == Label.FAIL])
195
+ return ProbeSearchResult(
196
+ macro_root=self.macro_root,
197
+ micro_root=self.micro_root,
198
+ n_simulations=len(self._evaluated),
199
+ n_macro=self._n_macro,
200
+ n_micro=self._n_micro,
201
+ failure_cases=failures,
202
+ all_cases=CaseBatch(self._evaluated),
203
+ )