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,489 @@
|
|
|
1
|
+
"""Fused explore -> confirm pipeline (DESIGN §4).
|
|
2
|
+
|
|
3
|
+
One pass that DISCOVERS freely and CONFIRMS rigorously, with the two split apart:
|
|
4
|
+
|
|
5
|
+
1. ``_split_records`` — deterministic, stratified (by label) EXPLORE / CONFIRM
|
|
6
|
+
partition (mirrors ``VLDiagnoseLoop._split_explore_confirm``). The explorer and
|
|
7
|
+
catalog planner see ONLY explore rows; every e-value/CI is computed only on the
|
|
8
|
+
disjoint confirm rows. That is the double-dip firewall.
|
|
9
|
+
2. Discovery on EXPLORE — TWO sources, blind to each other:
|
|
10
|
+
- the LAMBDA explorer (free codegen EDA) -> observations / charts /
|
|
11
|
+
candidate_signals, each optionally carrying a deterministic ``recipe``;
|
|
12
|
+
- the catalog planner -> the per-case signal columns already present.
|
|
13
|
+
Their union is deduped by signal name (estimand proxy; value-level estimand
|
|
14
|
+
identity is an open question, see DESIGN §10).
|
|
15
|
+
3. Operationalization bridge — each explorer recipe is compiled on the CONFIRM rows
|
|
16
|
+
into a frozen per-case column (``compile_recipe``) and merged into the confirm
|
|
17
|
+
``StatsInput.per_case`` so it competes like any analyzer signal.
|
|
18
|
+
4. Confirmation on CONFIRM — the real M2 firewall (``StatsAnalysisAgent``) runs the
|
|
19
|
+
validated catalog over the EXPANDED signal family and produces effect/CI/e-value
|
|
20
|
+
verdicts + e-BH ``corrected_rejections`` + conclusion. The explorer never decides.
|
|
21
|
+
5. Output assembly — per-signal host verdicts (provenance-tagged), and a graceful
|
|
22
|
+
``recommended_confirmatory_tests`` channel for candidates that could not be
|
|
23
|
+
operationalized / were underpowered / had no confirm data. Nothing is silently
|
|
24
|
+
dropped and nothing is over-claimed.
|
|
25
|
+
|
|
26
|
+
Multiplicity note: paired/e-value tools use e-BH and marginal p-value tools use
|
|
27
|
+
BH. The report exposes both legacy ``rejected_tools`` and precise
|
|
28
|
+
``rejected_result_keys`` so M1-M4 remains compatible while standalone M2 can
|
|
29
|
+
audit the exact tested family.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from evalrx.analysis.operationalize import (
|
|
38
|
+
RecipeError,
|
|
39
|
+
SignalRecipe,
|
|
40
|
+
compile_recipe,
|
|
41
|
+
)
|
|
42
|
+
from evalrx.analysis.stats_tools import build_stats_input_from_records
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Output types
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class FusedSignal:
|
|
50
|
+
"""One candidate signal confirmed (or attempted) on the held-out CONFIRM split."""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
source: str # "explorer" | "catalog" | "both"
|
|
54
|
+
description: str = ""
|
|
55
|
+
suggested_test: str = ""
|
|
56
|
+
recipe: dict[str, Any] | None = None # the SignalRecipe dict (bridged signals); replayable in-loop
|
|
57
|
+
operationalized: bool = True # became a per-case column on CONFIRM
|
|
58
|
+
# host verdict (from the M2 firewall on CONFIRM) — authoritative
|
|
59
|
+
effect: float | None = None
|
|
60
|
+
ci: tuple[float, float] | None = None
|
|
61
|
+
e_value: float | None = None
|
|
62
|
+
p_value: float | None = None
|
|
63
|
+
reject: bool | None = None
|
|
64
|
+
underpowered: bool = False
|
|
65
|
+
host_adjudicated: bool = False
|
|
66
|
+
fdr_corrected: bool = False
|
|
67
|
+
correction_method: str | None = None
|
|
68
|
+
confirmed_on: str = "" # "held_out" | "in_sample"
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"name": self.name,
|
|
73
|
+
"source": self.source,
|
|
74
|
+
"description": self.description,
|
|
75
|
+
"suggested_test": self.suggested_test,
|
|
76
|
+
"recipe": self.recipe,
|
|
77
|
+
"operationalized": self.operationalized,
|
|
78
|
+
"effect": self.effect,
|
|
79
|
+
"ci": list(self.ci) if self.ci is not None else None,
|
|
80
|
+
"e_value": self.e_value,
|
|
81
|
+
"p_value": self.p_value,
|
|
82
|
+
"reject": self.reject,
|
|
83
|
+
"underpowered": self.underpowered,
|
|
84
|
+
"host_adjudicated": self.host_adjudicated,
|
|
85
|
+
"fdr_corrected": self.fdr_corrected,
|
|
86
|
+
"correction_method": self.correction_method,
|
|
87
|
+
"confirmed_on": self.confirmed_on,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class FusedReport:
|
|
93
|
+
"""Output of :func:`run_fused_analysis` — explore (descriptive) + confirm (verdicts)."""
|
|
94
|
+
|
|
95
|
+
question: str = ""
|
|
96
|
+
ok: bool = False
|
|
97
|
+
observations: list[str] = field(default_factory=list)
|
|
98
|
+
visual_plan: list[dict[str, Any]] = field(default_factory=list)
|
|
99
|
+
chart_readings: list[dict[str, Any]] = field(default_factory=list)
|
|
100
|
+
dashboard_storyboard: list[dict[str, Any]] = field(default_factory=list)
|
|
101
|
+
claims: list[dict[str, Any]] = field(default_factory=list)
|
|
102
|
+
candidate_signals: list[FusedSignal] = field(default_factory=list)
|
|
103
|
+
charts: list[dict[str, Any]] = field(default_factory=list)
|
|
104
|
+
recommended_confirmatory_tests: list[str] = field(default_factory=list)
|
|
105
|
+
caveats: list[str] = field(default_factory=list)
|
|
106
|
+
critique: list[str] = field(default_factory=list)
|
|
107
|
+
adjudication: dict[str, Any] = field(default_factory=dict)
|
|
108
|
+
split: dict[str, Any] = field(default_factory=dict)
|
|
109
|
+
conclusion: str = ""
|
|
110
|
+
explore_report: dict[str, Any] = field(default_factory=dict)
|
|
111
|
+
confirm_stats: dict[str, Any] = field(default_factory=dict)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def confirmed_signal_names(self) -> list[str]:
|
|
115
|
+
return [s.name for s in self.candidate_signals if s.reject]
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict[str, Any]:
|
|
118
|
+
return {
|
|
119
|
+
"question": self.question,
|
|
120
|
+
"ok": self.ok,
|
|
121
|
+
"observations": self.observations,
|
|
122
|
+
"visual_plan": self.visual_plan,
|
|
123
|
+
"chart_readings": self.chart_readings,
|
|
124
|
+
"dashboard_storyboard": self.dashboard_storyboard,
|
|
125
|
+
"claims": self.claims,
|
|
126
|
+
"candidate_signals": [s.to_dict() for s in self.candidate_signals],
|
|
127
|
+
"charts": self.charts,
|
|
128
|
+
"recommended_confirmatory_tests": self.recommended_confirmatory_tests,
|
|
129
|
+
"caveats": self.caveats,
|
|
130
|
+
"critique": self.critique,
|
|
131
|
+
"adjudication": self.adjudication,
|
|
132
|
+
"split": self.split,
|
|
133
|
+
"conclusion": self.conclusion,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Deterministic stratified EXPLORE / CONFIRM split (records level)
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def _split_records(
|
|
142
|
+
records: list[dict[str, Any]],
|
|
143
|
+
*,
|
|
144
|
+
frac: float,
|
|
145
|
+
seed: int,
|
|
146
|
+
label_col: str,
|
|
147
|
+
) -> "tuple[list[dict], list[dict] | None]":
|
|
148
|
+
"""Deterministic, label-stratified ``(explore, confirm)`` split.
|
|
149
|
+
|
|
150
|
+
Returns ``(explore, confirm)``. Like the in-loop split, returns
|
|
151
|
+
``(records, None)`` when ``frac <= 0`` or the batch is too small to hold out —
|
|
152
|
+
a no-op, so the caller falls back to in-sample confirmation with a caveat.
|
|
153
|
+
"""
|
|
154
|
+
from evalrx.stats.subset_sampling import stratified_subset
|
|
155
|
+
|
|
156
|
+
rows = list(records)
|
|
157
|
+
if frac <= 0.0 or len(rows) < 4:
|
|
158
|
+
return rows, None
|
|
159
|
+
n_confirm = round(len(rows) * frac)
|
|
160
|
+
if n_confirm <= 0 or n_confirm >= len(rows):
|
|
161
|
+
return rows, None
|
|
162
|
+
|
|
163
|
+
def _key(row: dict[str, Any]) -> Any:
|
|
164
|
+
return str(row.get(label_col))
|
|
165
|
+
|
|
166
|
+
confirm = stratified_subset(rows, _key, n_confirm, seed=seed)
|
|
167
|
+
confirm_ids = {id(r) for r in confirm}
|
|
168
|
+
explore = [r for r in rows if id(r) not in confirm_ids]
|
|
169
|
+
if not explore or not confirm:
|
|
170
|
+
return rows, None
|
|
171
|
+
return explore, confirm
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Orchestrator
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def run_fused_analysis(
|
|
179
|
+
records: list[dict[str, Any]],
|
|
180
|
+
*,
|
|
181
|
+
question: str = "What distinguishes failures from passes?",
|
|
182
|
+
explorer: Any,
|
|
183
|
+
stats_agent: Any | None = None,
|
|
184
|
+
confirm_split: float = 0.3,
|
|
185
|
+
seed: int = 0,
|
|
186
|
+
id_col: str = "case_id",
|
|
187
|
+
label_col: str = "label",
|
|
188
|
+
) -> FusedReport:
|
|
189
|
+
"""Run discovery on EXPLORE, confirmation on the held-out CONFIRM split.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
records: list of row dicts (id/label/signal columns).
|
|
193
|
+
explorer: an object with ``explore_records(rows, question=...) ->
|
|
194
|
+
ExploratoryAnalysisReport`` (e.g. :class:`ExploratoryAnalysisAgent`).
|
|
195
|
+
stats_agent: optional :class:`StatsAnalysisAgent`; one is built with a
|
|
196
|
+
high signal cap when omitted so the whole family is tested.
|
|
197
|
+
confirm_split: fraction held out for confirmation (0 = in-sample fallback).
|
|
198
|
+
"""
|
|
199
|
+
rows = list(records or [])
|
|
200
|
+
explore_rows, confirm_rows = _split_records(
|
|
201
|
+
rows, frac=confirm_split, seed=seed, label_col=label_col
|
|
202
|
+
)
|
|
203
|
+
held_out = confirm_rows is not None
|
|
204
|
+
confirm_label = "held_out" if held_out else "in_sample"
|
|
205
|
+
if not held_out:
|
|
206
|
+
confirm_rows = explore_rows # in-sample fallback (small batch)
|
|
207
|
+
|
|
208
|
+
report = FusedReport(question=question)
|
|
209
|
+
report.split = {
|
|
210
|
+
"mode": confirm_label,
|
|
211
|
+
"n_total": len(rows),
|
|
212
|
+
"n_explore": len(explore_rows),
|
|
213
|
+
"n_confirm": len(confirm_rows),
|
|
214
|
+
"frac": confirm_split,
|
|
215
|
+
"seed": seed,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# ── 1. Discovery source A: the LAMBDA explorer (EXPLORE only) ──
|
|
219
|
+
explore_report = explorer.explore_records(explore_rows, question=question)
|
|
220
|
+
report.ok = bool(getattr(explore_report, "ok", False))
|
|
221
|
+
report.observations = list(getattr(explore_report, "observations", []) or [])
|
|
222
|
+
report.visual_plan = list(getattr(explore_report, "visual_plan", []) or [])
|
|
223
|
+
report.chart_readings = list(getattr(explore_report, "chart_readings", []) or [])
|
|
224
|
+
report.dashboard_storyboard = list(getattr(explore_report, "dashboard_storyboard", []) or [])
|
|
225
|
+
report.claims = list(getattr(explore_report, "claims", []) or [])
|
|
226
|
+
report.charts = list(getattr(explore_report, "charts", []) or [])
|
|
227
|
+
report.caveats = list(getattr(explore_report, "caveats", []) or [])
|
|
228
|
+
report.critique = list(getattr(explore_report, "critique", []) or [])
|
|
229
|
+
report.explore_report = (
|
|
230
|
+
explore_report.to_dict() if hasattr(explore_report, "to_dict") else {}
|
|
231
|
+
)
|
|
232
|
+
explorer_candidates = list(getattr(explore_report, "candidate_signals", []) or [])
|
|
233
|
+
explorer_named = {c.name: c for c in explorer_candidates if getattr(c, "name", "")}
|
|
234
|
+
|
|
235
|
+
# ── 2. Discovery source B: catalog columns present on EXPLORE ──
|
|
236
|
+
explore_inp = build_stats_input_from_records(
|
|
237
|
+
explore_rows, id_col=id_col, label_col=label_col
|
|
238
|
+
)
|
|
239
|
+
catalog_names = set(explore_inp.per_case)
|
|
240
|
+
|
|
241
|
+
# ── 3. Operationalization bridge: compile explorer recipes on CONFIRM ──
|
|
242
|
+
bridged: dict[str, dict[str, float]] = {}
|
|
243
|
+
recipe_by_name: dict[str, dict[str, Any]] = {} # recipe name -> recipe dict (for replay)
|
|
244
|
+
bridge_failed: list[Any] = []
|
|
245
|
+
for c in explorer_candidates:
|
|
246
|
+
recipe_data = getattr(c, "recipe", None)
|
|
247
|
+
if not isinstance(recipe_data, dict):
|
|
248
|
+
continue
|
|
249
|
+
recipe = SignalRecipe.from_dict(recipe_data)
|
|
250
|
+
if not recipe.name:
|
|
251
|
+
recipe.name = c.name
|
|
252
|
+
try:
|
|
253
|
+
values = compile_recipe(recipe, confirm_rows, id_col=id_col)
|
|
254
|
+
except (RecipeError, NotImplementedError):
|
|
255
|
+
values = {}
|
|
256
|
+
if values:
|
|
257
|
+
bridged[recipe.name] = values
|
|
258
|
+
recipe_by_name[recipe.name] = {**recipe_data, "name": recipe.name}
|
|
259
|
+
else:
|
|
260
|
+
bridge_failed.append(c)
|
|
261
|
+
|
|
262
|
+
# ── 4. Confirmation: build confirm StatsInput, inject bridged signals, run M2 ──
|
|
263
|
+
confirm_inp = build_stats_input_from_records(
|
|
264
|
+
confirm_rows, id_col=id_col, label_col=label_col
|
|
265
|
+
)
|
|
266
|
+
catalog_confirm = set(confirm_inp.per_case) # real catalog columns on CONFIRM
|
|
267
|
+
bridged_keys: set[str] = set() # final per_case keys for bridged signals
|
|
268
|
+
bridged_origin: dict[str, str] = {} # final key -> originating recipe name
|
|
269
|
+
collisions: list[str] = []
|
|
270
|
+
for name, values in bridged.items():
|
|
271
|
+
key = name
|
|
272
|
+
if key in catalog_confirm:
|
|
273
|
+
# A bridged recipe must NEVER silently overwrite a real catalog column —
|
|
274
|
+
# that would test a DIFFERENT estimand under the same name. Namespace it.
|
|
275
|
+
key = f"bridged.{name}"
|
|
276
|
+
while key in confirm_inp.per_case:
|
|
277
|
+
key = f"_{key}"
|
|
278
|
+
collisions.append(name)
|
|
279
|
+
confirm_inp.per_case[key] = values
|
|
280
|
+
bridged_keys.add(key)
|
|
281
|
+
bridged_origin[key] = name
|
|
282
|
+
if collisions:
|
|
283
|
+
report.caveats.append(
|
|
284
|
+
"bridged recipe name(s) collided with catalog column(s) and were "
|
|
285
|
+
f"namespaced to avoid overwriting a real signal: {sorted(collisions)}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# The deferred "leak-1" check: a bridged recipe (or catalog column) whose
|
|
289
|
+
# value RECONSTRUCTS the FAIL label is the label in disguise — it would
|
|
290
|
+
# trivially "confirm" with effect→1 and pollute confirmed_recipes.json. Route
|
|
291
|
+
# such columns to the sanity lane so they never enter the confirmatory family
|
|
292
|
+
# or get reported as discriminators; record a caveat for the audit trail.
|
|
293
|
+
# (build_stats_input_from_records already isolated raw leaks; this re-runs
|
|
294
|
+
# over the just-injected bridged signals.)
|
|
295
|
+
from evalrx.analysis.stats_tools import isolate_label_leaks
|
|
296
|
+
|
|
297
|
+
leaked = isolate_label_leaks(confirm_inp)
|
|
298
|
+
for key in leaked:
|
|
299
|
+
bridged_keys.discard(key)
|
|
300
|
+
if leaked:
|
|
301
|
+
report.caveats.append(
|
|
302
|
+
"label-reconstructing signal(s) held out of the confirmatory family "
|
|
303
|
+
"(sanity/plumbing, not discriminators): "
|
|
304
|
+
+ ", ".join(f"{k} [{v}]" for k, v in sorted(leaked.items()))
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if stats_agent is None:
|
|
308
|
+
from evalrx.analysis.stats_agent import StatsAnalysisAgent
|
|
309
|
+
|
|
310
|
+
stats_agent = StatsAnalysisAgent(
|
|
311
|
+
max_signal_tools=max(8, len(confirm_inp.per_case))
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
confirm_report = stats_agent.analyze_input(confirm_inp)
|
|
315
|
+
report.confirm_stats = (
|
|
316
|
+
confirm_report.to_dict() if hasattr(confirm_report, "to_dict") else {}
|
|
317
|
+
)
|
|
318
|
+
report.conclusion = getattr(confirm_report, "conclusion", "")
|
|
319
|
+
corrected = getattr(confirm_report, "corrected_rejections", {}) or {}
|
|
320
|
+
rejected_tools = set(corrected.get("rejected_tools", []))
|
|
321
|
+
rejected_result_keys = set(corrected.get("rejected_result_keys", []))
|
|
322
|
+
|
|
323
|
+
# ── 5. Assemble per-signal verdicts (provenance-tagged) ──
|
|
324
|
+
verdict_by_signal = _verdicts_by_signal(getattr(confirm_report, "stats_results", []))
|
|
325
|
+
for name in confirm_inp.per_case:
|
|
326
|
+
result = verdict_by_signal.get(name)
|
|
327
|
+
origin = bridged_origin.get(name, name) # recipe name for a (possibly renamed) bridged key
|
|
328
|
+
report.candidate_signals.append(
|
|
329
|
+
_fused_signal(
|
|
330
|
+
name=name,
|
|
331
|
+
source=_source_of(name, catalog_names, bridged_keys, explorer_named),
|
|
332
|
+
description=_describe(name, origin, explorer_named, bridged_keys),
|
|
333
|
+
suggested_test=_suggested_test(origin, explorer_named),
|
|
334
|
+
recipe=recipe_by_name.get(origin),
|
|
335
|
+
result=result,
|
|
336
|
+
rejected_tools=rejected_tools,
|
|
337
|
+
rejected_result_keys=rejected_result_keys,
|
|
338
|
+
confirmed_on=confirm_label,
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# ── 6. Graceful degradation channel: nothing silently dropped ──
|
|
343
|
+
rec = list(getattr(explore_report, "recommended_confirmatory_tests", []) or [])
|
|
344
|
+
for c in bridge_failed:
|
|
345
|
+
rec.append(
|
|
346
|
+
f"{c.name}: {getattr(c, 'rationale', '')} "
|
|
347
|
+
"(recipe could not be operationalized on the confirm split)"
|
|
348
|
+
)
|
|
349
|
+
for c in explorer_candidates:
|
|
350
|
+
if (
|
|
351
|
+
not getattr(c, "recipe", None)
|
|
352
|
+
and c.name not in catalog_names
|
|
353
|
+
and c.name not in bridged
|
|
354
|
+
):
|
|
355
|
+
rec.append(
|
|
356
|
+
f"{c.name}: {getattr(c, 'rationale', '')} "
|
|
357
|
+
"(descriptive only — no host-testable recipe)"
|
|
358
|
+
)
|
|
359
|
+
for fs in report.candidate_signals:
|
|
360
|
+
if fs.underpowered:
|
|
361
|
+
rec.append(f"{fs.name}: underpowered on the confirm split")
|
|
362
|
+
report.recommended_confirmatory_tests = rec
|
|
363
|
+
|
|
364
|
+
# ── 7. Family metadata + honest caveats ──
|
|
365
|
+
report.adjudication = {
|
|
366
|
+
"method": corrected.get("method", "e-BH"),
|
|
367
|
+
"alpha": corrected.get("alpha"),
|
|
368
|
+
"n_in_family": corrected.get("n_tested", 0),
|
|
369
|
+
"rejected_tools": sorted(rejected_tools),
|
|
370
|
+
"rejected_result_keys": sorted(rejected_result_keys),
|
|
371
|
+
"families": corrected.get("families", {}),
|
|
372
|
+
"n_signals_tested": len(confirm_inp.per_case),
|
|
373
|
+
"n_signals_rejected": sum(1 for s in report.candidate_signals if s.reject),
|
|
374
|
+
"split": confirm_label,
|
|
375
|
+
}
|
|
376
|
+
if not held_out:
|
|
377
|
+
report.caveats.append(
|
|
378
|
+
"CONFIRM split is IN-SAMPLE (batch too small to hold out); verdicts are "
|
|
379
|
+
"not double-dip-protected — enlarge the batch or lower confirm_split"
|
|
380
|
+
)
|
|
381
|
+
if any(s.host_adjudicated and s.reject and not s.fdr_corrected
|
|
382
|
+
for s in report.candidate_signals):
|
|
383
|
+
report.caveats.append(
|
|
384
|
+
"some rejected signal verdicts were not FDR-corrected; treat them as "
|
|
385
|
+
"descriptive until rerun with a correction-capable test"
|
|
386
|
+
)
|
|
387
|
+
return report
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Helpers
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
def _verdicts_by_signal(stats_results: list[Any]) -> dict[str, Any]:
|
|
395
|
+
"""Map ``signal name -> StatsToolResult`` from the per-case catalog tools.
|
|
396
|
+
|
|
397
|
+
Prefers ``signal_label_assoc`` (carries a reject) over ``rank_corr``.
|
|
398
|
+
"""
|
|
399
|
+
out: dict[str, Any] = {}
|
|
400
|
+
for r in stats_results or []:
|
|
401
|
+
if not getattr(r, "ok", False):
|
|
402
|
+
continue
|
|
403
|
+
signal = (getattr(r, "config", {}) or {}).get("signal")
|
|
404
|
+
if not signal:
|
|
405
|
+
continue
|
|
406
|
+
if getattr(r, "tool", "") == "signal_label_assoc":
|
|
407
|
+
out[signal] = r
|
|
408
|
+
elif signal not in out:
|
|
409
|
+
out[signal] = r
|
|
410
|
+
return out
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _fused_signal(
|
|
414
|
+
*,
|
|
415
|
+
name: str,
|
|
416
|
+
source: str,
|
|
417
|
+
description: str,
|
|
418
|
+
suggested_test: str,
|
|
419
|
+
recipe: dict[str, Any] | None,
|
|
420
|
+
result: Any,
|
|
421
|
+
rejected_tools: set[str],
|
|
422
|
+
rejected_result_keys: set[str],
|
|
423
|
+
confirmed_on: str,
|
|
424
|
+
) -> FusedSignal:
|
|
425
|
+
fs = FusedSignal(
|
|
426
|
+
name=name,
|
|
427
|
+
source=source,
|
|
428
|
+
description=description,
|
|
429
|
+
suggested_test=suggested_test,
|
|
430
|
+
recipe=recipe,
|
|
431
|
+
confirmed_on=confirmed_on,
|
|
432
|
+
)
|
|
433
|
+
if result is None:
|
|
434
|
+
# Still a per-case column, but no usable test ran on CONFIRM (e.g. a
|
|
435
|
+
# degenerate one-sided split). Left host_adjudicated=False, reject=None.
|
|
436
|
+
return fs
|
|
437
|
+
fs.host_adjudicated = True
|
|
438
|
+
fs.effect = getattr(result, "effect", None)
|
|
439
|
+
fs.ci = getattr(result, "ci", None)
|
|
440
|
+
fs.e_value = getattr(result, "e_value", None)
|
|
441
|
+
fs.p_value = getattr(result, "p_value", None)
|
|
442
|
+
fs.underpowered = bool(getattr(result, "underpowered", False))
|
|
443
|
+
fs.reject = bool(getattr(result, "reject", False))
|
|
444
|
+
fs.correction_method = getattr(result, "correction_method", None)
|
|
445
|
+
result_key = getattr(result, "analysis_key", None)
|
|
446
|
+
fs.fdr_corrected = bool(getattr(result, "fdr_corrected", False)) or (
|
|
447
|
+
(result_key in rejected_result_keys)
|
|
448
|
+
or (getattr(result, "tool", "") in rejected_tools and fs.e_value is not None)
|
|
449
|
+
)
|
|
450
|
+
if fs.fdr_corrected:
|
|
451
|
+
fs.reject = True
|
|
452
|
+
return fs
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _source_of(
|
|
456
|
+
name: str,
|
|
457
|
+
catalog_names: set[str],
|
|
458
|
+
bridged_keys: set[str],
|
|
459
|
+
explorer_named: dict[str, Any],
|
|
460
|
+
) -> str:
|
|
461
|
+
in_bridged = name in bridged_keys
|
|
462
|
+
in_catalog = name in catalog_names
|
|
463
|
+
pointed = name in explorer_named
|
|
464
|
+
# A bridged key is namespaced away from catalog columns, so a bridged signal is
|
|
465
|
+
# purely explorer-sourced; "both" is reserved for the explorer naming a real column.
|
|
466
|
+
if in_bridged:
|
|
467
|
+
return "both" if in_catalog else "explorer"
|
|
468
|
+
if in_catalog and pointed:
|
|
469
|
+
return "both"
|
|
470
|
+
return "catalog"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _describe(
|
|
474
|
+
name: str,
|
|
475
|
+
origin: str,
|
|
476
|
+
explorer_named: dict[str, Any],
|
|
477
|
+
bridged_keys: set[str],
|
|
478
|
+
) -> str:
|
|
479
|
+
c = explorer_named.get(origin) or explorer_named.get(name)
|
|
480
|
+
if c is not None and getattr(c, "rationale", ""):
|
|
481
|
+
return c.rationale
|
|
482
|
+
if name in bridged_keys:
|
|
483
|
+
return "bridged explorer signal"
|
|
484
|
+
return "existing per-case column"
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _suggested_test(name: str, explorer_named: dict[str, Any]) -> str:
|
|
488
|
+
c = explorer_named.get(name)
|
|
489
|
+
return getattr(c, "suggested_test", "") if c is not None else ""
|