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,333 @@
1
+ """Single-shot exploratory analysis run (the ``evalrx explore`` entry).
2
+
3
+ One call = one pipeline = one artifact directory. This is the non-interactive
4
+ replacement for the retired chat REPL: the explorer writes EDA code, the host
5
+ adjudicates any host-checkable candidate statistics, charts are rendered
6
+ deterministically, and everything is persisted (optionally a dashboard opens).
7
+
8
+ Flow::
9
+
10
+ ExploratoryAnalysisAgent.explore_path # M2: free-form EDA (a CLI coding agent)
11
+ -> adjudicate_report # host recomputes verdicts (in-sample)
12
+ -> HypothesisAgent.propose # M3: falsifiable hypotheses from M2's takeaways
13
+ -> render_chart_specs # spec + CSV -> PNG, host-side
14
+ -> write_report_artifacts # report.json + figures/ + tables/ + code
15
+ -> launch_dashboard (optional)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import shutil
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from evalrx.analysis.api import explore
26
+ from evalrx.analysis.explorer import RECORDS_FILENAME, load_records_from_path
27
+ from evalrx.analysis.holdout import holdout_confirm as run_holdout_confirm
28
+ from evalrx.analysis.holdout import split_records
29
+ from evalrx.viz.renderer import render_chart_specs
30
+
31
+ # Appended to the analysis question when a held-out confirm phase will follow:
32
+ # recipes must be frozen and threshold-explicit or there is nothing to re-test.
33
+ HOLDOUT_QUESTION_SUFFIX = (
34
+ " This is the EXPLORATION HALF of a held-out design: a disjoint validate "
35
+ "split exists and will re-test whatever you propose. Make every candidate "
36
+ "signal a FROZEN, threshold-explicit recipe (explicit numeric thresholds, "
37
+ "no re-fitting) so it can be re-evaluated verbatim on the held-out half, "
38
+ "and propose hypotheses precise enough to be graded against held-out "
39
+ "statistics."
40
+ )
41
+
42
+
43
+ def _build_judge(model: str):
44
+ """Judge for the held-out confirm phase (separate so tests can stub it)."""
45
+ from evalrx.agent_runtime.judges import ClaudeModel
46
+
47
+ return ClaudeModel(model=model, effort="low")
48
+
49
+
50
+ def run_explore(
51
+ path: str | Path,
52
+ *,
53
+ question: str = "Explore this dataset and surface the patterns that matter.",
54
+ out: str | Path = "evalrx_explore_output",
55
+ coder_provider: str = "antigravity",
56
+ coder_model: str = "",
57
+ coder_binary: str = "",
58
+ max_rows: int = 2000,
59
+ max_files: int = 200,
60
+ include_tool_calls: bool = False,
61
+ timeout_sec: int = 120,
62
+ max_attempts: int = 2,
63
+ dashboard: bool = False,
64
+ dashboard_port: int | None = None,
65
+ skills: "list[str] | tuple[str, ...]" = (),
66
+ allow_skills: bool = False,
67
+ use_bundled_skills: bool = True,
68
+ outcome_col: str | None = None,
69
+ propose_hypotheses: bool = True,
70
+ holdout_frac: float = 0.0,
71
+ holdout_seed: int = 0,
72
+ holdout_confirm: bool = False,
73
+ judge_model: str = "claude-opus-4-8",
74
+ progress_sink: Any = None,
75
+ ) -> int:
76
+ """Run one exploratory analysis and persist its artifacts.
77
+
78
+ Returns a process exit code: ``0`` if the exploration succeeded, else ``1``.
79
+ When *dashboard* is set, the return value is the dashboard process's code.
80
+
81
+ Skills style the agent-authored figures (e.g. the bundled eval-chart-style
82
+ and nature-figure skills). By default (*use_bundled_skills*) the package's
83
+ bundled skills are applied on the claude/agy/codex backends; *skills* adds
84
+ more dirs and *allow_skills* also enables globally-installed
85
+ (`~/.claude/skills`) skills.
86
+
87
+ *outcome_col* optionally names the target/label column explicitly (M1
88
+ passes ``"label"``); omit it to let the agent auto-detect an outcome by
89
+ name heuristics, or fall back to unsupervised EDA when there is none.
90
+
91
+ *propose_hypotheses* runs M3 (``HypothesisAgent``) on M2's takeaways after
92
+ a successful explore, using the same coding-agent backend; set False to
93
+ skip it (e.g. to save the extra LLM call).
94
+
95
+ *holdout_frac* > 0 splits the loaded records BEFORE exploration
96
+ (deterministic, outcome-stratified; the explorer only sees the explore
97
+ share). With *holdout_confirm* the held-out rows then re-test the frozen
98
+ recipes + hypotheses (``analysis.holdout``) and ``confirm_report.json``
99
+ lands next to the exploratory report — the dashboard's Held-out Verdicts
100
+ tab fills in. Without it the rows are simply reserved on disk.
101
+ """
102
+ out_dir = Path(out).resolve()
103
+ out_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ source: Any = path
106
+ holdout_rows: "list[dict[str, Any]] | None" = None
107
+ if holdout_frac and holdout_frac > 0:
108
+ records = load_records_from_path(
109
+ path, max_rows=max_rows, max_files=max_files,
110
+ include_tool_calls=include_tool_calls,
111
+ )
112
+ explore_share, holdout_rows = split_records(
113
+ records, holdout_frac, seed=holdout_seed, outcome_col=outcome_col
114
+ )
115
+ if holdout_rows is None:
116
+ print(f"holdout: split impossible (n={len(records)}, frac={holdout_frac}) "
117
+ "— falling back to a plain in-sample run")
118
+ else:
119
+ source = explore_share
120
+ print(f"holdout: exploring {len(explore_share)} rows, "
121
+ f"{len(holdout_rows)} held out "
122
+ + ("(verification follows)" if holdout_confirm
123
+ else "(reserved, not verified in this mode)"))
124
+ if holdout_confirm:
125
+ question = question.rstrip() + HOLDOUT_QUESTION_SUFFIX
126
+
127
+ result = explore(
128
+ source,
129
+ question=question,
130
+ out=out_dir,
131
+ provider=coder_provider,
132
+ model=coder_model,
133
+ binary=coder_binary,
134
+ outcome_col=outcome_col,
135
+ propose_hypotheses=propose_hypotheses,
136
+ skills=skills,
137
+ allow_skills=allow_skills,
138
+ use_bundled_skills=use_bundled_skills,
139
+ max_rows=max_rows,
140
+ max_files=max_files,
141
+ include_tool_calls=include_tool_calls,
142
+ timeout_sec=timeout_sec,
143
+ max_attempts=max_attempts,
144
+ progress_sink=progress_sink,
145
+ )
146
+ report = result.report
147
+
148
+ print(f"ok: {report.ok}")
149
+ print(f"attempts: {report.attempts}")
150
+ print(f"rows loaded: {report.data_profile.get('loaded_rows', report.data_profile.get('n_rows'))}")
151
+ print(f"output: {out_dir}")
152
+ if report.error:
153
+ print(f"error: {report.error}")
154
+ if report.hypotheses:
155
+ print("\nhypotheses (M3, proposed only — not validated):")
156
+ for h in report.hypotheses[:8]:
157
+ print(f"- {h.get('statement')}")
158
+ if report.observations:
159
+ print("\nobservations:")
160
+ for obs in report.observations[:8]:
161
+ print(f"- {obs}")
162
+ if report.candidate_signals:
163
+ print("\ncandidate signals:")
164
+ for signal in report.candidate_signals[:8]:
165
+ print(f"- {signal.name}: {signal.rationale}{_verdict_suffix(signal)}")
166
+ if report.adjudication:
167
+ adj = report.adjudication
168
+ print(
169
+ f"\nhost adjudication ({adj.get('split')}): "
170
+ f"{adj.get('n_rejected', 0)}/{adj.get('n_host_adjudicated', 0)} reject "
171
+ f"(e-BH family n={adj.get('n_in_family', 0)}, alpha={adj.get('alpha')})"
172
+ )
173
+
174
+ if holdout_rows is not None:
175
+ # Audit trail either way: the exact rows the explorer never saw.
176
+ (out_dir / "holdout_records.json").write_text(
177
+ json.dumps(holdout_rows, default=str), encoding="utf-8"
178
+ )
179
+ split_meta = {
180
+ "n_explore": len(source) if isinstance(source, list) else None,
181
+ "n_holdout": len(holdout_rows),
182
+ "holdout_frac": holdout_frac,
183
+ "seed": holdout_seed,
184
+ "stratified_by": outcome_col or "label",
185
+ }
186
+ if holdout_confirm:
187
+ judge = None
188
+ judge_meta = None
189
+ try:
190
+ judge = _build_judge(judge_model)
191
+ judge_meta = {"model": judge_model, "effort": "low"}
192
+ except Exception as exc: # noqa: BLE001 — verdicts degrade to not_judged
193
+ print(f"judge unavailable ({exc}) — hypotheses will be not_judged")
194
+ if progress_sink is not None:
195
+ progress_sink.emit("m4", "started", "Verifying hypotheses on the held-out split")
196
+ confirm = run_holdout_confirm(
197
+ report.to_dict(), holdout_rows,
198
+ outcome_col=outcome_col or "label",
199
+ judge=judge, judge_meta=judge_meta,
200
+ )
201
+ confirm["split_meta"] = split_meta
202
+ (out_dir / "confirm_report.json").write_text(
203
+ json.dumps(confirm, indent=1, default=str), encoding="utf-8"
204
+ )
205
+ cadj = confirm.get("adjudication") or {}
206
+ if progress_sink is not None:
207
+ progress_sink.emit(
208
+ "m4", "completed", "Held-out verification complete",
209
+ metrics={
210
+ "n_rows": confirm.get("n_validate_rows"),
211
+ "n_rejected": cadj.get("n_rejected", 0),
212
+ "n_adjudicated": cadj.get("n_host_adjudicated", 0),
213
+ },
214
+ )
215
+ print(
216
+ f"\nheld-out adjudication: {cadj.get('n_rejected', 0)}/"
217
+ f"{cadj.get('n_host_adjudicated', 0)} reject "
218
+ f"(n={confirm.get('n_validate_rows')} rows, "
219
+ f"{confirm.get('n_validate_fail')} failures)"
220
+ )
221
+ for v in confirm.get("hypothesis_verdicts", []):
222
+ print(f" * [{v.get('verdict')}] {str(v.get('statement', ''))[:90]}")
223
+ else:
224
+ (out_dir / "holdout_split.json").write_text(
225
+ json.dumps(split_meta, indent=1), encoding="utf-8"
226
+ )
227
+
228
+ # The HTML report is the canonical visualization artifact. Generate it
229
+ # even when the caller does not immediately open a browser so every
230
+ # completed exploratory run is portable and reviewable.
231
+ from evalrx.reporting.html_report import build_html_report
232
+
233
+ build_html_report(out_dir, out_path=out_dir / "report.html")
234
+
235
+ if dashboard:
236
+ from evalrx.analysis.dashboard import launch_dashboard
237
+
238
+ return launch_dashboard(out_dir, port=dashboard_port)
239
+ return 0 if report.ok else 1
240
+
241
+
242
+ def _verdict_suffix(signal: Any) -> str:
243
+ """One-line host verdict tag for a candidate signal, or '' if not adjudicated."""
244
+ if not getattr(signal, "host_adjudicated", False):
245
+ return " [descriptive]" if getattr(signal, "sufficient", None) else ""
246
+ verdict = "REJECT H0" if signal.reject else "inconclusive"
247
+ parts = [verdict]
248
+ if signal.e_value is not None:
249
+ # In the e-BH family (has an e-value); reject already reflects survival.
250
+ parts.append(f"e={signal.e_value:.2f}")
251
+ parts.append("e-BH family")
252
+ elif signal.ci is not None:
253
+ parts.append(f"CI={signal.ci[0]:+.3f}..{signal.ci[1]:+.3f} (not FDR-corrected)")
254
+ return " [host: " + ", ".join(parts) + "]"
255
+
256
+
257
+ def write_report_artifacts(
258
+ report: Any, out_dir: Path, *, report_filename: str = "exploratory_report.json"
259
+ ) -> None:
260
+ """Persist one explore run's artifacts: figures/, tables/, rendered charts,
261
+ the report JSON, and the generated code/stdout/stderr.
262
+
263
+ Charts are rendered here (host-side, from spec + CSV) so the persisted
264
+ ``exploratory_report.json`` carries each chart's ``figure_path``.
265
+ """
266
+ out_dir = Path(out_dir)
267
+ out_dir.mkdir(parents=True, exist_ok=True)
268
+ _copy_artifact_dirs(report, out_dir)
269
+
270
+ # Render the explorer's chart specs to PNG from the copied tables/ CSVs.
271
+ try:
272
+ report.charts = render_chart_specs(
273
+ getattr(report, "charts", None), out_dir / "tables", out_dir
274
+ )
275
+ except Exception: # rendering is best-effort, never blocks persistence
276
+ pass
277
+
278
+ (out_dir / report_filename).write_text(
279
+ json.dumps(report.to_dict(), indent=2, default=str),
280
+ encoding="utf-8",
281
+ )
282
+ if report.code:
283
+ (out_dir / "analysis.py").write_text(report.code, encoding="utf-8")
284
+ if report.stdout:
285
+ (out_dir / "stdout.txt").write_text(report.stdout, encoding="utf-8")
286
+ if report.stderr:
287
+ (out_dir / "stderr.txt").write_text(report.stderr, encoding="utf-8")
288
+ if report.raw_outputs:
289
+ (out_dir / "agent_raw_output.txt").write_text(
290
+ "\n\n--- attempt ---\n\n".join(report.raw_outputs),
291
+ encoding="utf-8",
292
+ )
293
+ # The UNTRUNCATED raw CLI streams (one per attempt) — the truncated
294
+ # rendering above is for compact UIs; this is the full audit trail.
295
+ raw_streams = getattr(report, "raw_streams", None) or []
296
+ if any(s.strip() for s in raw_streams):
297
+ (out_dir / "agent_raw_streams.txt").write_text(
298
+ "\n\n--- attempt (full stream) ---\n\n".join(raw_streams),
299
+ encoding="utf-8",
300
+ )
301
+ if report.agent_audits:
302
+ (out_dir / "agent_audit.json").write_text(
303
+ json.dumps({"schema_version": 1, "attempts": report.agent_audits}, indent=2),
304
+ encoding="utf-8",
305
+ )
306
+
307
+ # Clean up redundant nested sandbox directory so out_dir is the single clean source of truth
308
+ workdir = Path(getattr(report, "workdir", "") or "")
309
+ try:
310
+ if workdir.exists() and workdir.resolve() != out_dir.resolve() and workdir.is_relative_to(out_dir):
311
+ shutil.rmtree(workdir)
312
+ except Exception:
313
+ pass
314
+
315
+
316
+ def _copy_artifact_dirs(report: Any, out_dir: Path) -> None:
317
+ workdir = Path(getattr(report, "workdir", "") or "")
318
+ if not workdir.exists():
319
+ return
320
+ for name in ("figures", "tables"):
321
+ src = workdir / name
322
+ dest = out_dir / name
323
+ if src.exists() and src.is_dir():
324
+ if dest.exists():
325
+ shutil.rmtree(dest)
326
+ shutil.copytree(src, dest)
327
+ # Persist the raw loaded records alongside the report so the dashboard can
328
+ # offer a "browse raw data" view — otherwise they only live in the sandbox
329
+ # workdir, which isn't guaranteed to still exist by the time someone opens
330
+ # the dashboard.
331
+ records_src = workdir / RECORDS_FILENAME
332
+ if records_src.exists():
333
+ shutil.copy2(records_src, out_dir / RECORDS_FILENAME)