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.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- 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"
|