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,587 @@
1
+ """Compile raw dashboard artifacts into a claim-first diagnostic report."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from evalrx.reporting.model import (
8
+ Claim,
9
+ DiagnosticReport,
10
+ Evidence,
11
+ ReaderFinding,
12
+ ReaderReport,
13
+ ReportStep,
14
+ )
15
+ from evalrx.viz.labels import display_name
16
+
17
+
18
+ def compile_diagnostic_report(
19
+ story: dict[str, Any] | None,
20
+ explore_report: dict[str, Any] | None,
21
+ ) -> DiagnosticReport:
22
+ """Build a semantic report from a loop story plus the Step-1 explore report.
23
+
24
+ This compiler is deterministic. Agent-authored fields such as ``claims`` or
25
+ ``chart_readings`` are preserved when present, but host-confirmed signal
26
+ verdicts and loop outcomes remain the authoritative backbone.
27
+ """
28
+ story = story or {}
29
+ explore_report = explore_report or {}
30
+ signals = _candidate_signals(explore_report)
31
+ evidence = _compile_evidence(explore_report, story)
32
+ claims = _compile_claims(explore_report, story, signals)
33
+
34
+ # Analysis-phase (descriptive) mode: M2 ran with the e-BH validity verdict
35
+ # DEFERRED to the confirm phase, so the dashboard must NOT show
36
+ # supported/not-supported claims — only distributions, charts, and proposed
37
+ # hypotheses. Demote every signal claim to descriptive until confirmation.
38
+ descriptive_only = _is_descriptive_only(story)
39
+ caveats = [str(c) for c in (explore_report.get("caveats") or [])]
40
+ if descriptive_only:
41
+ claims = [_demote_to_descriptive(c) for c in claims]
42
+ caveats.insert(0, _DESCRIPTIVE_BANNER)
43
+
44
+ answer = _answer(explore_report, claims) if not descriptive_only else _DESCRIPTIVE_ANSWER
45
+ confidence = _confidence(claims)
46
+
47
+ return DiagnosticReport(
48
+ question=_fallback_question(explore_report, story),
49
+ answer=answer,
50
+ confidence=confidence,
51
+ claims=claims,
52
+ evidence=evidence,
53
+ timeline=_compile_timeline(story, explore_report),
54
+ visual_decisions=list(explore_report.get("visual_plan") or []),
55
+ chart_readings=list(explore_report.get("chart_readings") or []),
56
+ dashboard_storyboard=_dashboard_storyboard(explore_report, story, claims),
57
+ critique=_critique(explore_report, signals),
58
+ caveats=caveats,
59
+ next_actions=_next_actions(explore_report, claims),
60
+ )
61
+
62
+
63
+ def compile_reader_report(data: dict[str, Any]) -> ReaderReport:
64
+ """Compile a conservative, plain-language summary from a loaded run.
65
+
66
+ This function deliberately does not infer causality from a correlation. It
67
+ makes the verification state explicit so the renderer cannot accidentally
68
+ present a promising pattern as an established root cause.
69
+ """
70
+ run = data.get("run") or {}
71
+ m1 = data.get("m1") or {}
72
+ m2 = data.get("m2") or {}
73
+ m3 = data.get("m3") or {}
74
+ m4 = data.get("m4") or {}
75
+ m5_fix = data.get("m5_fix") or {}
76
+ explore = m2.get("explore") or {}
77
+ n_cases = int(run.get("n_cases") or 0)
78
+ model = str(run.get("model") or "the model")
79
+ benchmark = str(run.get("benchmark_name") or "this benchmark")
80
+ question = _reader_question(run.get("protocol"))
81
+
82
+ m4_ran = bool(m4.get("ran"))
83
+ hypotheses = m3.get("hypotheses") or []
84
+ repair_ran = bool(m5_fix.get("ran"))
85
+ repair_effect = (m5_fix.get("confirm") or {}).get("effect")
86
+ if m4_ran:
87
+ headline = "A possible explanation was checked on new cases."
88
+ confidence = "Independently checked"
89
+ elif hypotheses:
90
+ headline = "We found a lead that still needs an independent check."
91
+ confidence = "Needs independent checking"
92
+ else:
93
+ headline = "We found patterns, not a proven cause."
94
+ confidence = "Early evidence"
95
+ if repair_ran and isinstance(repair_effect, (int, float)) and repair_effect > 0:
96
+ headline = "A repair was tried and improved this run."
97
+
98
+ what_we_did = [
99
+ f"{model} was evaluated on {n_cases:,} {benchmark} cases." if n_cases else f"{model} was evaluated on {benchmark}.",
100
+ f"{len(m1.get('analyzers') or [])} behavior checks were compared between successful and unsuccessful cases.",
101
+ ]
102
+
103
+ findings = _reader_findings(explore, m2, verified=m4_ran)
104
+ if not findings:
105
+ findings = [ReaderFinding(
106
+ title="No clear pattern",
107
+ summary="This run did not produce a finding safe to summarize.",
108
+ why_it_matters="",
109
+ evidence_level="No conclusion yet",
110
+ )]
111
+
112
+ open_questions = [
113
+ "Whether the leading pattern explains the outcome on new cases."
114
+ ] if not m4_ran else []
115
+ next_steps = [
116
+ "Test the leading pattern on new cases."
117
+ ] if not m4_ran else []
118
+
119
+ answer = (
120
+ "The findings below are leads, not proven causes."
121
+ if not m4_ran else "This report separates observed patterns from independently checked results."
122
+ )
123
+ return ReaderReport(
124
+ headline=headline,
125
+ question=question,
126
+ answer=answer,
127
+ confidence=confidence,
128
+ what_we_did=what_we_did,
129
+ key_findings=findings,
130
+ open_questions=open_questions,
131
+ next_steps=next_steps,
132
+ )
133
+
134
+
135
+ def _reader_findings(explore: dict[str, Any], m2: dict[str, Any], *, verified: bool) -> list[ReaderFinding]:
136
+ signals = [signal for signal in (explore.get("candidate_signals") or []) if isinstance(signal, dict)]
137
+ findings: list[ReaderFinding] = []
138
+ for signal in signals[:2]:
139
+ name = str(signal.get("name") or "")
140
+ title, summary, why = _plain_signal(name, signal)
141
+ level = "Independently checked" if verified else "Pattern seen in this run"
142
+ findings.append(ReaderFinding(
143
+ title=title,
144
+ summary=summary,
145
+ why_it_matters=why,
146
+ evidence_level=level,
147
+ limitation="This pattern alone does not prove what caused the mistake." if not verified else "Interpret alongside the independent-check details.",
148
+ ))
149
+ if findings:
150
+ return findings
151
+ for observation in (explore.get("observations") or [])[:2]:
152
+ if isinstance(observation, str) and observation.strip():
153
+ findings.append(ReaderFinding(
154
+ title="A pattern worth checking",
155
+ summary=observation,
156
+ why_it_matters="",
157
+ evidence_level="Pattern seen in this run",
158
+ limitation="It is not proof of a root cause.",
159
+ ))
160
+ return findings
161
+
162
+
163
+ def _plain_signal(name: str, signal: dict[str, Any]) -> tuple[str, str, str]:
164
+ """Return only text supplied by an arbitrary task's structured artifacts.
165
+
166
+ The renderer owns the evidence-status wording. It must not encode domain
167
+ assumptions (for example, multiple-choice or audio-specific explanations)
168
+ in order to work for every evaluator that emits candidate signals.
169
+ """
170
+ display = str(signal.get("display_name") or name.replace("_", " ").strip().title() or "Observed pattern")
171
+ summary = str(
172
+ signal.get("reader_summary")
173
+ or signal.get("summary")
174
+ or signal.get("rationale")
175
+ or "This behavior differed between the outcome groups in this run."
176
+ )
177
+ return display, summary, ""
178
+
179
+
180
+ def _reader_question(protocol: Any) -> str:
181
+ """Extract a compact task question without assuming a protocol schema."""
182
+ if isinstance(protocol, dict):
183
+ for key in ("question", "description", "name"):
184
+ value = protocol.get(key)
185
+ if value:
186
+ return str(value)
187
+ if protocol:
188
+ return str(protocol)
189
+ return "What patterns are linked to the observed outcomes?"
190
+
191
+
192
+ _DESCRIPTIVE_BANNER = (
193
+ "ANALYSIS PHASE (descriptive): distributions, charts, and proposed hypotheses "
194
+ "only — signal/hypothesis VALIDITY (e-BH + M4) is deferred to the confirm phase "
195
+ "and is not shown here."
196
+ )
197
+ _DESCRIPTIVE_ANSWER = (
198
+ "Analysis phase — candidate signals and proposed hypotheses are shown without a "
199
+ "validity verdict; run the confirm phase to adjudicate them."
200
+ )
201
+
202
+
203
+ def _is_descriptive_only(story: dict[str, Any]) -> bool:
204
+ """True when every M2 analysis in the story deferred its validity verdict and
205
+ no confirmation (M4/surgery) has run yet — i.e. the loaded run is analysis-only.
206
+
207
+ A single confirmatory M2 (``descriptive_only=False``, logged by the confirm
208
+ phase or the all-in-one loop) or any recorded surgery flips this off, so the
209
+ full validity rendering returns once the run is confirmed."""
210
+ analyses = story.get("analyses") or []
211
+ if not analyses:
212
+ return False
213
+ if not all(a.get("descriptive_only") for a in analyses):
214
+ return False
215
+ return not (story.get("surgeries") or [])
216
+
217
+
218
+ def _demote_to_descriptive(claim: Claim) -> Claim:
219
+ """Strip a claim's validity verdict (supported/inconclusive/refuted) to
220
+ descriptive for the analysis-phase view, preserving its text and evidence."""
221
+ if claim.status == "descriptive":
222
+ return claim
223
+ note = "Association shown descriptively; validity is deferred to the confirm phase."
224
+ interp = (claim.interpretation + " " if claim.interpretation else "") + note
225
+ return Claim(
226
+ id=claim.id,
227
+ text=claim.text,
228
+ status="descriptive",
229
+ evidence_ids=list(claim.evidence_ids),
230
+ counter_evidence_ids=list(claim.counter_evidence_ids),
231
+ interpretation=interp.strip(),
232
+ do_not_infer=claim.do_not_infer,
233
+ downstream=list(claim.downstream),
234
+ )
235
+
236
+
237
+ _GENERIC_QUESTION = "What distinguishes failures from passes?"
238
+
239
+
240
+ def _fallback_question(explore_report: dict[str, Any], story: dict[str, Any]) -> str:
241
+ """The explore report's own question wins; a loop/agentic run with no
242
+ explore_report artifact falls back to the protocol description logged at
243
+ run_start — otherwise a run with no explore report shows a question with
244
+ no connection to what it actually investigated."""
245
+ question = str(explore_report.get("question") or "").strip()
246
+ if question:
247
+ return question
248
+ protocol = (story.get("run_start") or {}).get("protocol") or {}
249
+ return str(protocol.get("description") or "").strip() or _GENERIC_QUESTION
250
+
251
+
252
+ def _dashboard_storyboard(
253
+ explore_report: dict[str, Any],
254
+ story: dict[str, Any],
255
+ claims: list[Claim],
256
+ ) -> list[dict[str, Any]]:
257
+ raw = explore_report.get("dashboard_storyboard") or explore_report.get("ui_panels")
258
+ if isinstance(raw, list) and all(isinstance(p, dict) for p in raw):
259
+ return [dict(p) for p in raw]
260
+
261
+ supported = next((c.text for c in claims if c.status == "supported"), "")
262
+ observations = [str(x) for x in (explore_report.get("observations") or [])[:3]]
263
+ readings = [
264
+ str(r.get("reading"))
265
+ for r in (explore_report.get("chart_readings") or [])
266
+ if isinstance(r, dict) and r.get("reading")
267
+ ][:3]
268
+ hypotheses = []
269
+ for diag in story.get("diagnoses") or []:
270
+ for h in diag.get("hypotheses") or []:
271
+ hypotheses.append(str(h.get("statement") or h))
272
+
273
+ return [
274
+ {
275
+ "id": "problem_setting",
276
+ "title": "Problem Setting",
277
+ "stages": ["M1"],
278
+ "summary": _fallback_question(explore_report, story),
279
+ "items": observations,
280
+ "artifact_refs": ["data_profile", "candidate_signals"],
281
+ },
282
+ {
283
+ "id": "analysis",
284
+ "title": "Analysis",
285
+ "stages": ["M2"],
286
+ "summary": supported or str(explore_report.get("conclusion") or ""),
287
+ "items": readings,
288
+ "artifact_refs": ["candidate_signals", "charts", "chart_readings"],
289
+ },
290
+ {
291
+ "id": "hypotheses_artifacts",
292
+ "title": "Hypotheses & Artifacts",
293
+ "stages": ["M3", "M4", "M5"],
294
+ "summary": "Hypotheses and downstream decisions generated from the analysis.",
295
+ "items": hypotheses[:5],
296
+ "artifact_refs": ["diagnoses", "surgeries", "fixes"],
297
+ },
298
+ ]
299
+
300
+
301
+ def _candidate_signals(explore_report: dict[str, Any]) -> list[dict[str, Any]]:
302
+ return [
303
+ s for s in (explore_report.get("candidate_signals") or [])
304
+ if isinstance(s, dict)
305
+ ]
306
+
307
+
308
+ def _compile_evidence(explore_report: dict[str, Any], story: dict[str, Any]) -> list[Evidence]:
309
+ evidence: list[Evidence] = []
310
+ for idx, signal in enumerate(_candidate_signals(explore_report), start=1):
311
+ name = str(signal.get("name") or f"signal_{idx}")
312
+ evidence.append(Evidence(
313
+ id=f"signal:{name}",
314
+ kind="confirmed_signal",
315
+ title=str(signal.get("display_name") or display_name(name)),
316
+ summary=_signal_summary(signal),
317
+ artifact=signal,
318
+ ))
319
+ for idx, chart in enumerate(explore_report.get("charts") or [], start=1):
320
+ if not isinstance(chart, dict):
321
+ continue
322
+ raw_title = str(chart.get("display_name") or chart.get("title") or chart.get("name") or f"Chart {idx}")
323
+ title = display_name(raw_title)
324
+ evidence.append(Evidence(
325
+ id=f"chart:{_slug(title)}",
326
+ kind="chart",
327
+ title=title,
328
+ summary=str(chart.get("description") or chart.get("kind") or ""),
329
+ artifact=chart,
330
+ ))
331
+ for idx, analysis in enumerate(story.get("analyses") or [], start=1):
332
+ summary = str(analysis.get("conclusion") or analysis.get("narrative") or "")
333
+ evidence.append(Evidence(
334
+ id=f"analysis:{idx}",
335
+ kind="m2_analysis",
336
+ title=f"M2 analysis cycle {analysis.get('cycle', idx)}",
337
+ summary=summary,
338
+ artifact=analysis,
339
+ ))
340
+ return evidence
341
+
342
+
343
+ def _compile_claims(
344
+ explore_report: dict[str, Any],
345
+ story: dict[str, Any],
346
+ signals: list[dict[str, Any]],
347
+ ) -> list[Claim]:
348
+ explicit = [
349
+ _claim_from_agent(c, i)
350
+ for i, c in enumerate(explore_report.get("claims") or [], start=1)
351
+ if isinstance(c, dict)
352
+ ]
353
+ if explicit:
354
+ return explicit
355
+
356
+ claims: list[Claim] = []
357
+ for idx, signal in enumerate(signals, start=1):
358
+ name = str(signal.get("name") or f"signal_{idx}")
359
+ status = _signal_status(signal)
360
+ downstream = _downstream_for_signal(name, story)
361
+ claims.append(Claim(
362
+ id=f"C{idx}",
363
+ text=_claim_text(signal),
364
+ status=status,
365
+ evidence_ids=[f"signal:{name}", *_chart_evidence_for_signal(name, explore_report)],
366
+ interpretation=_signal_interpretation(signal),
367
+ do_not_infer=_do_not_infer(signal),
368
+ downstream=downstream,
369
+ ))
370
+ if not claims:
371
+ claims.append(Claim(
372
+ id="C0",
373
+ text="No candidate signal was confirmed in the loaded report.",
374
+ status="inconclusive",
375
+ interpretation="The run may still contain exploratory observations, but no supported "
376
+ "claim can be made from the available confirmation layer.",
377
+ do_not_infer="Do not treat exploratory plots as confirmed root causes.",
378
+ ))
379
+ return _sort_claims(claims)
380
+
381
+
382
+ def _claim_from_agent(raw: dict[str, Any], idx: int) -> Claim:
383
+ status = str(raw.get("status") or "descriptive").lower()
384
+ if status not in {"supported", "inconclusive", "refuted", "descriptive"}:
385
+ status = "descriptive"
386
+ return Claim(
387
+ id=str(raw.get("id") or f"C{idx}"),
388
+ text=str(raw.get("text") or raw.get("claim") or ""),
389
+ status=status, # type: ignore[arg-type]
390
+ evidence_ids=[str(x) for x in (raw.get("evidence_ids") or [])],
391
+ counter_evidence_ids=[str(x) for x in (raw.get("counter_evidence_ids") or [])],
392
+ interpretation=str(raw.get("interpretation") or ""),
393
+ do_not_infer=str(raw.get("do_not_infer") or ""),
394
+ downstream=[str(x) for x in (raw.get("downstream") or [])],
395
+ )
396
+
397
+
398
+ def _compile_timeline(story: dict[str, Any], explore_report: dict[str, Any]) -> list[ReportStep]:
399
+ steps = [
400
+ ReportStep(
401
+ stage="Explore",
402
+ title="Agent explored patterns and proposed visuals/signals",
403
+ summary=f"{len(explore_report.get('observations') or [])} observation(s), "
404
+ f"{len(explore_report.get('charts') or [])} chart(s), "
405
+ f"{len(explore_report.get('candidate_signals') or [])} signal(s).",
406
+ )
407
+ ]
408
+ analyses = story.get("analyses") or []
409
+ if analyses:
410
+ steps.append(ReportStep(
411
+ stage="M2",
412
+ title="Host confirmed signal associations",
413
+ summary=str(analyses[-1].get("conclusion") or analyses[-1].get("narrative") or ""),
414
+ artifact_ids=[f"analysis:{len(analyses)}"],
415
+ ))
416
+ diagnoses = story.get("diagnoses") or []
417
+ if diagnoses:
418
+ n_h = sum(len(d.get("hypotheses") or []) for d in diagnoses)
419
+ steps.append(ReportStep(
420
+ stage="M3",
421
+ title="Agent formed falsifiable hypotheses",
422
+ summary=f"{n_h} hypothesis/hypotheses proposed from confirmed and exploratory context.",
423
+ ))
424
+ surgeries = story.get("surgeries") or []
425
+ if surgeries:
426
+ steps.append(ReportStep(
427
+ stage="M4/M5",
428
+ title="Hypotheses were tested by interventions",
429
+ summary=f"{len(surgeries)} test/intervention event(s) recorded.",
430
+ ))
431
+ fixes = story.get("fixes") or []
432
+ if fixes:
433
+ steps.append(ReportStep(
434
+ stage="Fix",
435
+ title="Fix candidates were adjudicated",
436
+ summary=f"{len(fixes)} fix event(s) recorded.",
437
+ ))
438
+ return steps
439
+
440
+
441
+ def _answer(explore_report: dict[str, Any], claims: list[Claim]) -> str:
442
+ supported = [c.text for c in claims if c.status == "supported"]
443
+ if supported:
444
+ leaky = sum(1 for c in claims if "sanity check" in c.do_not_infer.lower())
445
+ suffix = f" ({leaky} {_plural(leaky, 'sanity check')} demoted.)" if leaky else ""
446
+ return supported[0] + suffix
447
+ conclusion = str(explore_report.get("conclusion") or "").strip()
448
+ if conclusion:
449
+ return conclusion
450
+ return "No supported diagnostic claim is available in the loaded report."
451
+
452
+
453
+ def _confidence(claims: list[Claim]) -> str:
454
+ if any(c.status == "supported" for c in claims):
455
+ return "medium"
456
+ if any(c.status == "descriptive" for c in claims):
457
+ return "low"
458
+ return "unknown"
459
+
460
+
461
+ def _signal_status(signal: dict[str, Any]) -> str:
462
+ if _is_leaky_signal(signal):
463
+ return "descriptive"
464
+ if signal.get("reject") is True:
465
+ return "supported"
466
+ if signal.get("reject") is False:
467
+ return "inconclusive"
468
+ return "descriptive"
469
+
470
+
471
+ def _signal_summary(signal: dict[str, Any]) -> str:
472
+ parts = []
473
+ if signal.get("effect") is not None:
474
+ parts.append(f"effect={_fmt(signal.get('effect'))}")
475
+ ci = signal.get("ci")
476
+ if isinstance(ci, list | tuple) and len(ci) == 2:
477
+ parts.append(f"CI={_fmt(ci[0])}..{_fmt(ci[1])}")
478
+ if signal.get("e_value") is not None:
479
+ parts.append(f"e={_fmt(signal.get('e_value'))}")
480
+ verdict = "supported" if signal.get("reject") is True else "not supported"
481
+ return f"{verdict}" + (f" ({', '.join(parts)})" if parts else "")
482
+
483
+
484
+ def _claim_text(signal: dict[str, Any]) -> str:
485
+ name = str(signal.get("display_name") or display_name(signal.get("name") or "signal"))
486
+ if _is_leaky_signal(signal):
487
+ return f"{name} tracks the failure label and is treated as descriptive plumbing."
488
+ if signal.get("reject") is True:
489
+ return f"{name} is associated with FAIL cases on the confirmation split."
490
+ if signal.get("reject") is False:
491
+ return f"{name} did not produce a supported FAIL/PASS association."
492
+ return f"{name} is an exploratory signal without a host-confirmed verdict."
493
+
494
+
495
+ def _signal_interpretation(signal: dict[str, Any]) -> str:
496
+ if _is_leaky_signal(signal):
497
+ return "This is useful for auditing the pipeline, not for explaining the failure."
498
+ if signal.get("reject") is True:
499
+ return "Use this as a confirmed association that can motivate M3 hypotheses."
500
+ if signal.get("reject") is False:
501
+ return "Treat this as a negative or underpowered result unless new data changes it."
502
+ return "Treat this as descriptive only."
503
+
504
+
505
+ def _do_not_infer(signal: dict[str, Any]) -> str:
506
+ if _is_leaky_signal(signal):
507
+ return "Sanity check only: do not rank this as a root cause."
508
+ return "Do not infer causality from association without M4/M5 support."
509
+
510
+
511
+ def _plural(count: int, singular: str, plural: str | None = None) -> str:
512
+ return singular if count == 1 else (plural or f"{singular}s")
513
+
514
+
515
+ def _downstream_for_signal(name: str, story: dict[str, Any]) -> list[str]:
516
+ out = []
517
+ needle = name.lower()
518
+ for diag in story.get("diagnoses") or []:
519
+ refs = " ".join(str(x) for x in (diag.get("referenced_charts") or [])).lower()
520
+ hyps = diag.get("hypotheses") or []
521
+ if needle in refs or any(needle in str(h).lower() for h in hyps):
522
+ out.append(f"M3 cycle {diag.get('cycle')}: referenced in diagnosis context")
523
+ return out
524
+
525
+
526
+ def _chart_evidence_for_signal(name: str, explore_report: dict[str, Any]) -> list[str]:
527
+ out = []
528
+ lname = name.lower()
529
+ for chart in explore_report.get("charts") or []:
530
+ if not isinstance(chart, dict):
531
+ continue
532
+ title = str(chart.get("title") or chart.get("name") or "")
533
+ blob = " ".join(str(chart.get(k, "")) for k in ("name", "title", "data")).lower()
534
+ if lname in blob or any(part and part in blob for part in lname.split("_")):
535
+ out.append(f"chart:{_slug(title)}")
536
+ return out[:3]
537
+
538
+
539
+ def _critique(explore_report: dict[str, Any], signals: list[dict[str, Any]]) -> list[str]:
540
+ raw = explore_report.get("critique")
541
+ if isinstance(raw, list):
542
+ return [str(x) for x in raw]
543
+ notes = [str(c) for c in (explore_report.get("caveats") or [])]
544
+ if any(_is_leaky_signal(s) for s in signals):
545
+ notes.append("One or more signals are sanity checks and are demoted to descriptive evidence.")
546
+ if not notes:
547
+ notes.append("No explicit critique was recorded; inspect raw artifacts before over-claiming.")
548
+ return notes
549
+
550
+
551
+ def _next_actions(explore_report: dict[str, Any], claims: list[Claim]) -> list[str]:
552
+ actions = [str(x) for x in (explore_report.get("recommended_confirmatory_tests") or [])]
553
+ if any(c.status == "supported" for c in claims):
554
+ actions.append("Inspect the linked M3/M4 outcomes before treating supported signals as causes.")
555
+ if not actions:
556
+ actions.append("Re-run with a larger held-out split or richer probes if no claim is supported.")
557
+ return actions
558
+
559
+
560
+ def _sort_claims(claims: list[Claim]) -> list[Claim]:
561
+ order = {"supported": 0, "inconclusive": 1, "refuted": 2, "descriptive": 3}
562
+ return sorted(claims, key=lambda c: (order.get(c.status, 9), c.id))
563
+
564
+
565
+ def _is_leaky_signal(signal: dict[str, Any]) -> bool:
566
+ name = str(signal.get("name", "")).lower()
567
+ if name.startswith("probe1") or "false_detection" in name:
568
+ return True
569
+ eff = signal.get("effect")
570
+ ci = signal.get("ci") or [None, None]
571
+ try:
572
+ lo, hi = float(ci[0]), float(ci[1])
573
+ return eff is not None and abs(float(eff)) >= 0.999 and (hi - lo) <= 1e-6
574
+ except (TypeError, ValueError, IndexError):
575
+ return False
576
+
577
+
578
+ def _fmt(value: Any) -> str:
579
+ try:
580
+ return f"{float(value):+.3f}"
581
+ except (TypeError, ValueError):
582
+ return str(value)
583
+
584
+
585
+ def _slug(value: str) -> str:
586
+ text = "".join(ch.lower() if ch.isalnum() else "_" for ch in value).strip("_")
587
+ return text or "artifact"