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,172 @@
1
+ """Hypotheses — the unit the self-evolving agent proposes, tests, and mutates.
2
+
3
+ A hypothesis is a falsifiable claim about *when/why* a model fails (e.g. "Qwen
4
+ mis-binds entities when two names share a surname"). The loop turns it into
5
+ cases + an experiment, runs it, and updates its status from the findings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from typing import Any
14
+
15
+
16
+ class HypothesisStatus(str, Enum):
17
+ PROPOSED = "proposed" # generated, not yet tested
18
+ TESTING = "testing" # experiment in flight
19
+ SUPPORTED = "supported" # evidence backs it
20
+ REFUTED = "refuted" # evidence contradicts it
21
+ INCONCLUSIVE = "inconclusive"
22
+
23
+
24
+ @dataclass
25
+ class Hypothesis:
26
+ """A falsifiable claim about a model's failure behaviour.
27
+
28
+ Attributes:
29
+ statement: Natural-language claim (LLM-generated/readable).
30
+ target_model: Registered model name the claim is about.
31
+ predicted_failure_mode: Tag/description of the expected failure.
32
+ plain_statement: Plain-language explanation understandable by a layperson.
33
+ test_design: How to verify this claim — analyzer / per-case
34
+ signal / strategy-contrast keywords proposed by
35
+ M3 (e.g. ``"relative_attention.max_relative_weight"``,
36
+ ``"prompt_contrast describe_first"``). M4 uses
37
+ it to route evidence deterministically and M1
38
+ uses it for cycle-2 targeted probing.
39
+ expected_association: Pre-registered direction for the named test:
40
+ ``higher_on_failures`` or ``lower_on_failures``.
41
+ This prevents a protective-valued signal (for
42
+ example ``n_correct``) from being interpreted
43
+ backwards after its effect is observed.
44
+ status: Lifecycle state (:class:`HypothesisStatus`).
45
+ parent_id: Hypothesis this one was mutated from, if any.
46
+ id: Stable identifier.
47
+ evidence: Result/case ids accumulated while testing.
48
+ metadata: Free-form extras.
49
+ """
50
+
51
+ statement: str
52
+ target_model: str
53
+ predicted_failure_mode: str
54
+ plain_statement: str = ""
55
+ test_design: str = ""
56
+ expected_association: str = ""
57
+ status: HypothesisStatus = HypothesisStatus.PROPOSED
58
+ parent_id: str | None = None
59
+ id: str = ""
60
+ evidence: list[str] = field(default_factory=list)
61
+ metadata: dict[str, Any] = field(default_factory=dict)
62
+
63
+
64
+ class HypothesisGenerator:
65
+ """Proposes new hypotheses — from scratch or by mutating prior ones.
66
+
67
+ Stage 2: an LLM-backed generator that reads the store (past supported/refuted
68
+ hypotheses and their evidence) and proposes the next ones to test.
69
+ """
70
+
71
+ def propose(self, context: Any = None) -> list[Hypothesis]:
72
+ raise NotImplementedError("HypothesisGenerator is planned for Stage 2.")
73
+
74
+ def mutate(self, hypothesis: Hypothesis, feedback: Any = None) -> list[Hypothesis]:
75
+ """Derive refined/adjacent hypotheses from one that was tested."""
76
+ raise NotImplementedError("HypothesisGenerator.mutate is planned for Stage 2.")
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Serialization helpers (needed by JsonlStore and loop checkpointing)
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ def hypothesis_to_dict(h: Hypothesis) -> dict[str, Any]:
85
+ """Serialize a Hypothesis to a JSON-compatible dict."""
86
+ plain = h.plain_statement or (h.metadata.get("plain_statement", "") if h.metadata else "")
87
+ return {
88
+ "statement": h.statement,
89
+ "plain_statement": plain,
90
+ "target_model": h.target_model,
91
+ "predicted_failure_mode": h.predicted_failure_mode,
92
+ "test_design": h.test_design,
93
+ "expected_association": h.expected_association,
94
+ "status": h.status.value if h.status else HypothesisStatus.PROPOSED.value,
95
+ "parent_id": h.parent_id,
96
+ "id": h.id,
97
+ "evidence": list(h.evidence),
98
+ "metadata": dict(h.metadata),
99
+ }
100
+
101
+
102
+ def hypothesis_from_dict(data: dict[str, Any]) -> Hypothesis:
103
+ """Deserialize a Hypothesis from a dict (e.g., loaded from JSONL)."""
104
+ raw_status = data.get("status", HypothesisStatus.PROPOSED.value)
105
+ try:
106
+ status = HypothesisStatus(raw_status)
107
+ except ValueError:
108
+ status = HypothesisStatus.PROPOSED
109
+ plain = data.get("plain_statement") or (data.get("metadata", {}).get("plain_statement", "") if isinstance(data.get("metadata"), dict) else "")
110
+ return Hypothesis(
111
+ statement=str(data.get("statement", "")),
112
+ target_model=str(data.get("target_model", "")),
113
+ predicted_failure_mode=str(data.get("predicted_failure_mode", "")),
114
+ plain_statement=plain,
115
+ test_design=str(data.get("test_design", "")),
116
+ expected_association=str(data.get("expected_association", "")),
117
+ status=status,
118
+ parent_id=data.get("parent_id"),
119
+ id=data.get("id", ""),
120
+ evidence=list(data.get("evidence", [])),
121
+ metadata=dict(data.get("metadata", {})),
122
+ )
123
+
124
+
125
+ def _hyp_val(obj: Any, name: str, default: Any = None) -> Any:
126
+ """Read an attribute or mapping key, whichever *obj* is."""
127
+ if isinstance(obj, dict):
128
+ return obj.get(name, default)
129
+ return getattr(obj, name, default)
130
+
131
+
132
+ def hypothesis_id(hypothesis: Any) -> str:
133
+ """Stable join key for one hypothesis, shared by every M3/M4/M5 log entry.
134
+
135
+ ``Hypothesis.id`` defaults to ``""`` and nothing in the M1-M5 loop fills it
136
+ in, so every stage that names a hypothesis has to derive one — deriving it
137
+ per call site is how the same object ends up with two different names from
138
+ two different stages, and the join between a claim and its verdict goes
139
+ silently empty. Falls back to a hash of the statement, mirroring
140
+ ``eval_agent.loop._hyp_key`` — deterministic, so two stages holding the
141
+ same hypothesis always agree. ``evalrx.contract.emit.hypothesis_id``
142
+ delegates here so the optional contract sidecar and the primary log never
143
+ disagree on the id.
144
+ """
145
+ hid = str(_hyp_val(hypothesis, "id", "") or "").strip()
146
+ if hid:
147
+ return hid
148
+ statement = str(_hyp_val(hypothesis, "statement", "") or "").strip()
149
+ if statement:
150
+ return "h-" + hashlib.sha1(statement.encode("utf-8")).hexdigest()[:12]
151
+ return "unknown"
152
+
153
+
154
+ class ManualHypothesisGenerator(HypothesisGenerator):
155
+ """A non-LLM generator: drains a fixed queue, or calls an injected ``proposer``.
156
+
157
+ Lets the loop run + be unit-tested without an LLM; swap in an LLM-backed
158
+ generator later without touching the loop.
159
+ """
160
+
161
+ def __init__(self, hypotheses: list[Hypothesis] | None = None, proposer: Any = None) -> None:
162
+ self._queue: list[Hypothesis] = list(hypotheses or [])
163
+ self._proposer = proposer
164
+
165
+ def propose(self, context: Any = None) -> list[Hypothesis]:
166
+ if self._proposer is not None:
167
+ return list(self._proposer(context))
168
+ out, self._queue = self._queue, []
169
+ return out
170
+
171
+ def mutate(self, hypothesis: Hypothesis, feedback: Any = None) -> list[Hypothesis]:
172
+ return []
@@ -0,0 +1,209 @@
1
+ """Hold a run directory's label-bearing artifacts in memory while the fix
2
+ stage runs, so nothing the repair coder or its sandbox can read from disk
3
+ reveals gold answers or per-case pass/fail.
4
+
5
+ Why this exists
6
+ ---------------
7
+ The fix stage already withholds labels at the *prompt* level: the proposer sees
8
+ EXPLORE cases only, the coded-pipeline payload carries ``id`` / ``prompt`` /
9
+ ``baseline_output`` and nothing else, and every FixAgent prompt says so. But the
10
+ coder CLI runs with ``Bash Edit Write Read`` and the pipeline sandbox is a plain
11
+ subprocess, both rooted *inside* the run directory — where, by the time the fix
12
+ stage starts, M1..M4 have written the same labels many times over:
13
+
14
+ * ``baseline.json`` and ``logs/report/discovery_cases.json`` — every case with
15
+ ``expected`` and ``label``, CONFIRM cases included;
16
+ * ``logs/run.json`` — ``case_record`` events with ``expected``, ``label``
17
+ and ``metadata.gold``;
18
+ * ``logs/artifacts/*.result.json``, ``logs/contract/c*.m1.json``,
19
+ ``explore/records.json`` — per-case analyzer signals, among them
20
+ ``gold_yes``, which *is* the gold answer on a yes/no task;
21
+ * ``logs/workspace/post_m5/cases.json`` — the M5 experiment's cases with gold.
22
+
23
+ ``cat ../../../baseline.json`` from a fix workspace is two directory levels
24
+ away. A candidate that joins its cases to any of these by id can hard-code the
25
+ held-out answers; the frozen-model control catches the crude lookup-table form
26
+ but not one that also manufactures consensus support through the bridge.
27
+
28
+ What it does
29
+ ------------
30
+ :func:`quarantine_run_dir` is a context manager. On entry it reads every
31
+ regular file under the run directory into memory and unlinks it (directory
32
+ names stay; they carry nothing). Files handed in as ``append_logs`` — the
33
+ structured event log, which a :class:`logging.FileHandler` holds open in
34
+ append mode — are truncated in place instead, so events the fix stage writes
35
+ still land in the same file. On exit (also on an exception) everything is put
36
+ back byte-for-byte; an append log becomes ``old + what the fix stage wrote``;
37
+ a path the fix stage re-created with new content keeps the new content and
38
+ the pre-fix bytes are saved beside it as ``<name>.pre_fix`` — nothing
39
+ is ever lost. A ``fix_quarantine.json`` manifest is written on exit so the run
40
+ records exactly which files were hidden.
41
+
42
+ What it cannot do
43
+ -----------------
44
+ It only governs what *this run wrote*. A dataset manifest on a bind mount
45
+ (``/app/work/data/<dataset>/manifest.json`` in the benchmark containers)
46
+ carries ``gold`` too and cannot be hidden from a root process in the same
47
+ container — that needs uid separation or host-side label delivery, which is
48
+ out of scope here. The gap is deliberate and documented, not forgotten.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import json
54
+ import logging
55
+ import os
56
+ from collections.abc import Iterable, Iterator
57
+ from contextlib import contextmanager
58
+ from dataclasses import dataclass, field
59
+ from datetime import datetime, timezone
60
+ from pathlib import Path
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ MANIFEST_NAME = "fix_quarantine.json"
65
+
66
+
67
+ @dataclass
68
+ class QuarantineReport:
69
+ """What was hidden and how it was restored (also written as JSON)."""
70
+
71
+ root: Path
72
+ hidden: list[str] = field(default_factory=list)
73
+ truncated: list[str] = field(default_factory=list)
74
+ kept: list[str] = field(default_factory=list)
75
+ restored: list[str] = field(default_factory=list)
76
+ merged: list[str] = field(default_factory=list)
77
+ conflicts: list[str] = field(default_factory=list)
78
+ n_bytes: int = 0
79
+
80
+ def to_json(self) -> dict:
81
+ return {
82
+ "root": str(self.root),
83
+ "n_hidden": len(self.hidden),
84
+ "n_bytes": self.n_bytes,
85
+ "hidden": sorted(self.hidden),
86
+ "truncated_in_place": sorted(self.truncated),
87
+ "kept_visible": sorted(self.kept),
88
+ "merged_append_logs": sorted(self.merged),
89
+ "conflicts_saved_as_pre_fix": sorted(self.conflicts),
90
+ }
91
+
92
+
93
+ def _under(rel: str, prefixes: tuple[str, ...]) -> bool:
94
+ return any(rel == p or rel.startswith(p.rstrip("/") + "/") for p in prefixes)
95
+
96
+
97
+ def _rel(path: Path, root: Path) -> str:
98
+ return path.relative_to(root).as_posix()
99
+
100
+
101
+ @contextmanager
102
+ def quarantine_run_dir(
103
+ run_dir: "str | os.PathLike[str]",
104
+ *,
105
+ append_logs: Iterable["str | os.PathLike[str]"] = (),
106
+ rewrite_logs: Iterable["str | os.PathLike[str]"] = (),
107
+ keep: Iterable[str] = (),
108
+ write_manifest: bool = True,
109
+ ) -> Iterator[QuarantineReport]:
110
+ """Hide every file under *run_dir* for the duration of the block.
111
+
112
+ Args:
113
+ run_dir: The run directory (``baseline.json``, ``logs/``, ...).
114
+ append_logs: Files that a live handler holds open in append mode.
115
+ They are truncated to zero length instead of unlinked
116
+ and restored as ``old + new``. RunLoggerV2 rewrites its
117
+ documents atomically instead of appending, so its
118
+ callers pass ``rewrite_logs`` (see
119
+ ``RunLoggerV2.managed_json_paths``), not this.
120
+ keep: Relative path prefixes (POSIX style) to leave visible,
121
+ e.g. ``("logs/fixes",)`` when resuming a fix search.
122
+ write_manifest: Write ``fix_quarantine.json`` into *run_dir* on exit.
123
+
124
+ Yields the :class:`QuarantineReport`, filled in as the block runs.
125
+ """
126
+ root = Path(run_dir).resolve()
127
+ keep_prefixes = tuple(k.strip("/") for k in keep if k.strip("/"))
128
+ append_rel = set()
129
+ for p in append_logs:
130
+ p = Path(p).resolve()
131
+ try:
132
+ append_rel.add(_rel(p, root))
133
+ except ValueError:
134
+ logger.warning("label quarantine: append log %s is outside %s; ignored", p, root)
135
+ rewrite_rel = set()
136
+ for p in rewrite_logs:
137
+ p = Path(p).resolve()
138
+ try:
139
+ rewrite_rel.add(_rel(p, root))
140
+ except ValueError:
141
+ logger.warning("label quarantine: rewrite log %s is outside %s; ignored", p, root)
142
+
143
+ report = QuarantineReport(root=root)
144
+ stash: dict[str, bytes] = {}
145
+
146
+ if root.is_dir():
147
+ for dirpath, _dirnames, filenames in os.walk(root):
148
+ for name in filenames:
149
+ path = Path(dirpath) / name
150
+ if path.is_symlink():
151
+ continue
152
+ rel = _rel(path, root)
153
+ if rel == MANIFEST_NAME or _under(rel, keep_prefixes):
154
+ report.kept.append(rel)
155
+ continue
156
+ data = path.read_bytes()
157
+ stash[rel] = data
158
+ report.n_bytes += len(data)
159
+ if rel in append_rel:
160
+ # A logging.FileHandler appends through an O_APPEND fd:
161
+ # after truncation its next write lands at offset 0.
162
+ with path.open("r+b") as handle:
163
+ handle.truncate(0)
164
+ report.truncated.append(rel)
165
+ else:
166
+ path.unlink()
167
+ report.hidden.append(rel)
168
+ logger.info(
169
+ "label quarantine: hid %d file(s), %d bytes, under %s for the fix stage",
170
+ len(report.hidden), report.n_bytes, root,
171
+ )
172
+
173
+ try:
174
+ yield report
175
+ finally:
176
+ for rel, old in stash.items():
177
+ path = root / rel
178
+ path.parent.mkdir(parents=True, exist_ok=True)
179
+ if rel in append_rel:
180
+ new = path.read_bytes() if path.exists() else b""
181
+ path.write_bytes(old + new)
182
+ report.merged.append(rel)
183
+ elif rel in rewrite_rel and path.exists() and path.stat().st_size > 0:
184
+ # Atomic JSON loggers rewrite a complete in-memory document;
185
+ # that new document already includes everything in `old`.
186
+ # Keeping it avoids a redundant, non-JSON `.pre_fix` sibling.
187
+ report.merged.append(rel)
188
+ elif path.exists() and path.stat().st_size > 0:
189
+ # Appended AFTER the extension so a `*.json` scan (the contract
190
+ # index) never mistakes the copy for a payload.
191
+ side = path.with_name(path.name + ".pre_fix")
192
+ side.write_bytes(old)
193
+ report.conflicts.append(rel)
194
+ logger.warning(
195
+ "label quarantine: %s was rewritten during the fix stage; "
196
+ "pre-fix content saved as %s", rel, side.name,
197
+ )
198
+ else:
199
+ path.write_bytes(old)
200
+ report.restored.append(rel)
201
+ if write_manifest and root.is_dir():
202
+ payload = report.to_json()
203
+ payload["restored_at"] = datetime.now(timezone.utc).isoformat()
204
+ (root / MANIFEST_NAME).write_text(
205
+ json.dumps(payload, indent=2) + "\n", encoding="utf-8"
206
+ )
207
+
208
+
209
+ __all__ = ["MANIFEST_NAME", "QuarantineReport", "quarantine_run_dir"]