anhtld commited on
Commit
33dff72
·
verified ·
1 Parent(s): ce68b1e

Add LCB dominance safety calibration metrics

Browse files
workspace/scripts/eval_dominance_selector.py CHANGED
@@ -17,7 +17,11 @@ if str(PROJECT_ROOT) not in sys.path:
17
 
18
  import torch # noqa: E402
19
 
20
- from cil.metrics import macro_micro_summary # noqa: E402
 
 
 
 
21
  from cil.models import CTTConfig, ChartEncoder, TangentNormalizer, UtilityEnergy # noqa: E402
22
  from scripts.eval_ctt_generated_rollout import load_chart_items # noqa: E402
23
 
@@ -103,17 +107,29 @@ def main(argv: list[str] | None = None) -> int:
103
  else float(args.tau)
104
  )
105
 
 
 
 
 
 
 
106
  evaluated_cases = [
107
  _evaluate_case(
108
- _dominance_case(row, eval_charts, scorer=calibrator, k=args.k),
109
  residual_quantile=residual_quantile,
110
  tau=tau,
 
111
  )
112
- for row in eval_rows
113
  ]
114
  calibration_eval_cases = [
115
- _evaluate_case(case, residual_quantile=residual_quantile, tau=tau)
116
- for case in calibration_cases
 
 
 
 
 
117
  ]
118
  metric_names = sorted(
119
  {
@@ -155,8 +171,12 @@ def main(argv: list[str] | None = None) -> int:
155
  "eval_target_split_hash": eval_index.get("split_hash"),
156
  "num_calibration_rows": len(calibration_cases),
157
  "num_eval_rows": len(evaluated_cases),
158
- "calibration_summary": _simple_summary(calibration_eval_cases),
159
- "eval_summary": _simple_summary(evaluated_cases),
 
 
 
 
160
  "summary": summary,
161
  "rows": evaluated_cases,
162
  }
@@ -292,14 +312,23 @@ def _dominance_case(
292
  if chart_id not in charts:
293
  raise KeyError(f"chart_id {chart_id!r} not found in target index")
294
  predicted_scores = scorer.candidate_scores(row, charts[chart_id], k=k)
 
 
 
 
295
  if not generated_utilities or not predicted_scores:
296
  raise ValueError("dominance evaluation requires generated utilities and predicted scores")
 
297
  top_index = max(range(len(predicted_scores)), key=lambda index: predicted_scores[index])
298
  base_score = scorer.base_score(row, charts[chart_id])
299
  base_utility = float(row["base_utility"])
300
  base_success = float(bool(row.get("base_success", False)))
 
301
  selected_generated_utility = generated_utilities[top_index]
302
  selected_generated_success = candidate_success[top_index]
 
 
 
303
  proposal_oracle_utility = max(generated_utilities)
304
  proposal_oracle_success = float(any(candidate_success))
305
  hidden = [float(value) for value in row.get("hidden_chart_utilities", [])]
@@ -315,12 +344,20 @@ def _dominance_case(
315
  "top_index": top_index,
316
  "base_predicted_score": base_score,
317
  "top_predicted_score": predicted_scores[top_index],
 
318
  "predicted_margin": predicted_margin,
319
  "measured_margin": measured_margin,
320
  "base_utility": base_utility,
321
  "base_success": base_success,
 
 
322
  "top_generated_utility": selected_generated_utility,
323
  "top_generated_success": selected_generated_success,
 
 
 
 
 
324
  "proposal_oracle_utility": proposal_oracle_utility,
325
  "proposal_oracle_success": proposal_oracle_success,
326
  "hidden_chart_oracle_utility": hidden_oracle_utility,
@@ -329,7 +366,13 @@ def _dominance_case(
329
  }
330
 
331
 
332
- def _evaluate_case(case: dict[str, Any], *, residual_quantile: float, tau: float) -> dict[str, Any]:
 
 
 
 
 
 
333
  lcb = float(case["predicted_margin"]) - float(residual_quantile)
334
  execute_generated = lcb > float(tau)
335
  selected_utility = (
@@ -342,6 +385,11 @@ def _evaluate_case(case: dict[str, Any], *, residual_quantile: float, tau: float
342
  proposal_oracle_success = float(case["proposal_oracle_success"])
343
  hidden_utility = float(case["hidden_chart_oracle_utility"])
344
  hidden_success = float(case["hidden_chart_oracle_success"])
 
 
 
 
 
345
  output = dict(case)
346
  output.update(
347
  {
@@ -352,6 +400,10 @@ def _evaluate_case(case: dict[str, Any], *, residual_quantile: float, tau: float
352
  "fallback_rate": float(not execute_generated),
353
  "selected_utility": selected_utility,
354
  "selected_success": selected_success,
 
 
 
 
355
  "selected_utility_gain_over_base": selected_utility - float(case["base_utility"]),
356
  "selected_success_gain_over_base": selected_success - float(case["base_success"]),
357
  "selector_regret": max(0.0, proposal_oracle_utility - selected_utility),
@@ -368,6 +420,8 @@ def _evaluate_case(case: dict[str, Any], *, residual_quantile: float, tau: float
368
  else math.nan,
369
  }
370
  )
 
 
371
  return output
372
 
373
 
@@ -434,6 +488,16 @@ def _simple_summary(rows: list[dict[str, Any]]) -> dict[str, float | None]:
434
  "selected_success_gain_over_base",
435
  "coverage",
436
  "fallback_rate",
 
 
 
 
 
 
 
 
 
 
437
  "outcome_ptr",
438
  "success_support_gap",
439
  "success_selector_gap",
@@ -447,6 +511,96 @@ def _simple_summary(rows: list[dict[str, Any]]) -> dict[str, float | None]:
447
  return {key: _mean([row.get(key) for row in rows]) for key in keys}
448
 
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  def _group_means(
451
  rows: list[dict[str, Any]],
452
  key: str,
@@ -475,19 +629,71 @@ def _mean(values: list[Any]) -> float | None:
475
  return sum(clean) / len(clean) if clean else None
476
 
477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  def _table(metrics: dict[str, Any]) -> str:
479
  summary = metrics["eval_summary"]
480
  lines = [
481
  "% Auto-generated by scripts/eval_dominance_selector.py",
482
- "\\begin{tabular}{lrrrrrrrr}",
483
  "\\toprule",
484
- "Rows & Coverage & Fallback & Base succ. & Selected succ. & Oracle succ. & OutcomePTR & Succ. support gap & Succ. selector gap \\\\",
485
  "\\midrule",
486
  f"{metrics['num_eval_rows']} & {_fmt(summary.get('coverage'))} & "
487
- f"{_fmt(summary.get('fallback_rate'))} & {_fmt(summary.get('base_success'))} & "
 
488
  f"{_fmt(summary.get('selected_success'))} & {_fmt(summary.get('proposal_oracle_success'))} & "
489
  f"{_fmt(summary.get('outcome_ptr'))} & {_fmt(summary.get('success_support_gap'))} & "
490
- f"{_fmt(summary.get('success_selector_gap'))} \\\\",
 
491
  "\\bottomrule",
492
  "\\end{tabular}",
493
  ]
@@ -508,18 +714,22 @@ def _report(metrics: dict[str, Any]) -> str:
508
  "",
509
  "The threshold is fit on calibration rows only. Eval outcomes are used only for reporting.",
510
  "",
511
- "| Split | Coverage | Fallback | Base success | Selected success | Proposal oracle | OutcomePTR | Success support gap | Success selector gap |",
512
- "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
513
  f"| calibration | {_fmt(calibration.get('coverage'))} | {_fmt(calibration.get('fallback_rate'))} | "
 
514
  f"{_fmt(calibration.get('base_success'))} | {_fmt(calibration.get('selected_success'))} | "
515
  f"{_fmt(calibration.get('proposal_oracle_success'))} | {_fmt(calibration.get('outcome_ptr'))} | "
516
- f"{_fmt(calibration.get('success_support_gap'))} | {_fmt(calibration.get('success_selector_gap'))} |",
 
517
  f"| eval | {_fmt(summary.get('coverage'))} | {_fmt(summary.get('fallback_rate'))} | "
 
518
  f"{_fmt(summary.get('base_success'))} | {_fmt(summary.get('selected_success'))} | "
519
  f"{_fmt(summary.get('proposal_oracle_success'))} | {_fmt(summary.get('outcome_ptr'))} | "
520
- f"{_fmt(summary.get('success_support_gap'))} | {_fmt(summary.get('success_selector_gap'))} |",
 
521
  "",
522
- "This is a calibrated fallback diagnostic. It is not a final safety claim because unsafe-contact labels are not measured yet.",
523
  ]
524
  return "\n".join(lines)
525
 
 
17
 
18
  import torch # noqa: E402
19
 
20
+ from cil.metrics import ( # noqa: E402
21
+ macro_micro_summary,
22
+ outcome_safety_violation,
23
+ pairwise_causal_dominance_ece,
24
+ )
25
  from cil.models import CTTConfig, ChartEncoder, TangentNormalizer, UtilityEnergy # noqa: E402
26
  from scripts.eval_ctt_generated_rollout import load_chart_items # noqa: E402
27
 
 
107
  else float(args.tau)
108
  )
109
 
110
+ eval_cases = [
111
+ _dominance_case(row, eval_charts, scorer=calibrator, k=args.k)
112
+ for row in eval_rows
113
+ ]
114
+ eval_pairwise = _pairwise_calibration_summary(eval_cases)
115
+ calibration_pairwise = _pairwise_calibration_summary(calibration_cases)
116
  evaluated_cases = [
117
  _evaluate_case(
118
+ case,
119
  residual_quantile=residual_quantile,
120
  tau=tau,
121
+ pairwise_calibration=eval_pairwise["rows"].get(index, {}),
122
  )
123
+ for index, case in enumerate(eval_cases)
124
  ]
125
  calibration_eval_cases = [
126
+ _evaluate_case(
127
+ case,
128
+ residual_quantile=residual_quantile,
129
+ tau=tau,
130
+ pairwise_calibration=calibration_pairwise["rows"].get(index, {}),
131
+ )
132
+ for index, case in enumerate(calibration_cases)
133
  ]
134
  metric_names = sorted(
135
  {
 
171
  "eval_target_split_hash": eval_index.get("split_hash"),
172
  "num_calibration_rows": len(calibration_cases),
173
  "num_eval_rows": len(evaluated_cases),
174
+ "calibration_summary": _summary_with_pairwise(calibration_eval_cases, calibration_pairwise),
175
+ "eval_summary": _summary_with_pairwise(evaluated_cases, eval_pairwise),
176
+ "pairwise_causal_calibration": {
177
+ "calibration": _pairwise_calibration_global(calibration_pairwise),
178
+ "eval": _pairwise_calibration_global(eval_pairwise),
179
+ },
180
  "summary": summary,
181
  "rows": evaluated_cases,
182
  }
 
312
  if chart_id not in charts:
313
  raise KeyError(f"chart_id {chart_id!r} not found in target index")
314
  predicted_scores = scorer.candidate_scores(row, charts[chart_id], k=k)
315
+ count = min(len(generated_utilities), len(predicted_scores), len(candidate_success))
316
+ generated_utilities = generated_utilities[:count]
317
+ predicted_scores = predicted_scores[:count]
318
+ candidate_success = candidate_success[:count]
319
  if not generated_utilities or not predicted_scores:
320
  raise ValueError("dominance evaluation requires generated utilities and predicted scores")
321
+ candidate_safety_labels = _candidate_safety_labels(row, count=count)
322
  top_index = max(range(len(predicted_scores)), key=lambda index: predicted_scores[index])
323
  base_score = scorer.base_score(row, charts[chart_id])
324
  base_utility = float(row["base_utility"])
325
  base_success = float(bool(row.get("base_success", False)))
326
+ base_safety_label = _base_safety_label(row)
327
  selected_generated_utility = generated_utilities[top_index]
328
  selected_generated_success = candidate_success[top_index]
329
+ top_safety_label = (
330
+ candidate_safety_labels[top_index] if top_index < len(candidate_safety_labels) else None
331
+ )
332
  proposal_oracle_utility = max(generated_utilities)
333
  proposal_oracle_success = float(any(candidate_success))
334
  hidden = [float(value) for value in row.get("hidden_chart_utilities", [])]
 
344
  "top_index": top_index,
345
  "base_predicted_score": base_score,
346
  "top_predicted_score": predicted_scores[top_index],
347
+ "predicted_scores": predicted_scores,
348
  "predicted_margin": predicted_margin,
349
  "measured_margin": measured_margin,
350
  "base_utility": base_utility,
351
  "base_success": base_success,
352
+ "base_safety_label_known": float(base_safety_label is not None),
353
+ "base_unsafe_known": _label_to_float(base_safety_label),
354
  "top_generated_utility": selected_generated_utility,
355
  "top_generated_success": selected_generated_success,
356
+ "top_candidate_safety_label_known": float(top_safety_label is not None),
357
+ "top_candidate_unsafe_known": _label_to_float(top_safety_label),
358
+ "candidate_safety_label_coverage": _safety_label_coverage(candidate_safety_labels),
359
+ "candidate_unsafe_rate_known": _unsafe_rate_known(candidate_safety_labels),
360
+ "generated_utilities": generated_utilities,
361
  "proposal_oracle_utility": proposal_oracle_utility,
362
  "proposal_oracle_success": proposal_oracle_success,
363
  "hidden_chart_oracle_utility": hidden_oracle_utility,
 
366
  }
367
 
368
 
369
+ def _evaluate_case(
370
+ case: dict[str, Any],
371
+ *,
372
+ residual_quantile: float,
373
+ tau: float,
374
+ pairwise_calibration: dict[str, Any] | None = None,
375
+ ) -> dict[str, Any]:
376
  lcb = float(case["predicted_margin"]) - float(residual_quantile)
377
  execute_generated = lcb > float(tau)
378
  selected_utility = (
 
385
  proposal_oracle_success = float(case["proposal_oracle_success"])
386
  hidden_utility = float(case["hidden_chart_oracle_utility"])
387
  hidden_success = float(case["hidden_chart_oracle_success"])
388
+ selected_safety = (
389
+ _float_to_label(case.get("top_candidate_unsafe_known"))
390
+ if execute_generated
391
+ else _float_to_label(case.get("base_unsafe_known"))
392
+ )
393
  output = dict(case)
394
  output.update(
395
  {
 
400
  "fallback_rate": float(not execute_generated),
401
  "selected_utility": selected_utility,
402
  "selected_success": selected_success,
403
+ "selected_safety_label_known": float(selected_safety is not None),
404
+ "selected_unsafe_known": _label_to_float(selected_safety),
405
+ "unsafe_execution_label_known": float(selected_safety is not None),
406
+ "unsafe_execution_known": _label_to_float(selected_safety),
407
  "selected_utility_gain_over_base": selected_utility - float(case["base_utility"]),
408
  "selected_success_gain_over_base": selected_success - float(case["base_success"]),
409
  "selector_regret": max(0.0, proposal_oracle_utility - selected_utility),
 
420
  else math.nan,
421
  }
422
  )
423
+ if pairwise_calibration is not None:
424
+ output.update(_pairwise_calibration_scalars(pairwise_calibration))
425
  return output
426
 
427
 
 
488
  "selected_success_gain_over_base",
489
  "coverage",
490
  "fallback_rate",
491
+ "base_safety_label_known",
492
+ "base_unsafe_known",
493
+ "candidate_safety_label_coverage",
494
+ "candidate_unsafe_rate_known",
495
+ "top_candidate_safety_label_known",
496
+ "top_candidate_unsafe_known",
497
+ "selected_safety_label_known",
498
+ "selected_unsafe_known",
499
+ "unsafe_execution_label_known",
500
+ "unsafe_execution_known",
501
  "outcome_ptr",
502
  "success_support_gap",
503
  "success_selector_gap",
 
511
  return {key: _mean([row.get(key) for row in rows]) for key in keys}
512
 
513
 
514
+ def _pairwise_calibration_summary(cases: list[dict[str, Any]], *, n_bins: int = 10) -> dict[str, Any]:
515
+ bins = [
516
+ {
517
+ "count": 0,
518
+ "accuracy_sum": 0.0,
519
+ "confidence_sum": 0.0,
520
+ "lower": index / n_bins,
521
+ "upper": (index + 1) / n_bins,
522
+ }
523
+ for index in range(n_bins)
524
+ ]
525
+ rows: dict[int, dict[str, Any]] = {}
526
+ total_pairs = 0
527
+ correct_sum = 0.0
528
+ confidence_sum = 0.0
529
+ for index, case in enumerate(cases):
530
+ row_metrics = pairwise_causal_dominance_ece(
531
+ case.get("predicted_scores", []),
532
+ case.get("generated_utilities", []),
533
+ n_bins=n_bins,
534
+ )
535
+ rows[index] = row_metrics
536
+ row_pairs = int(row_metrics.get("num_pairs") or 0)
537
+ if row_pairs <= 0:
538
+ continue
539
+ total_pairs += row_pairs
540
+ correct_sum += float(row_metrics.get("accuracy") or 0.0) * row_pairs
541
+ confidence_sum += float(row_metrics.get("mean_confidence") or 0.0) * row_pairs
542
+ for bin_index, row_bin in enumerate(row_metrics.get("bins", [])):
543
+ if bin_index >= len(bins):
544
+ break
545
+ count = int(row_bin.get("count") or 0)
546
+ bins[bin_index]["count"] += count
547
+ bins[bin_index]["accuracy_sum"] += float(row_bin.get("accuracy") or 0.0) * count
548
+ bins[bin_index]["confidence_sum"] += float(row_bin.get("confidence") or 0.0) * count
549
+
550
+ ece = 0.0
551
+ rendered_bins: list[dict[str, float | int]] = []
552
+ for bucket in bins:
553
+ count = int(bucket["count"])
554
+ accuracy = bucket["accuracy_sum"] / count if count else 0.0
555
+ confidence = bucket["confidence_sum"] / count if count else 0.0
556
+ if total_pairs:
557
+ ece += (count / total_pairs) * abs(accuracy - confidence)
558
+ rendered_bins.append(
559
+ {
560
+ "lower": float(bucket["lower"]),
561
+ "upper": float(bucket["upper"]),
562
+ "count": count,
563
+ "accuracy": accuracy,
564
+ "confidence": confidence,
565
+ "abs_gap": abs(accuracy - confidence),
566
+ }
567
+ )
568
+ return {
569
+ "n_bins": int(n_bins),
570
+ "num_rows": len(cases),
571
+ "ece": ece if total_pairs else math.nan,
572
+ "num_pairs": int(total_pairs),
573
+ "accuracy": correct_sum / total_pairs if total_pairs else math.nan,
574
+ "mean_confidence": confidence_sum / total_pairs if total_pairs else math.nan,
575
+ "bins": rendered_bins,
576
+ "rows": rows,
577
+ }
578
+
579
+
580
+ def _pairwise_calibration_scalars(calibration: dict[str, Any]) -> dict[str, float]:
581
+ return {
582
+ "pairwise_causal_calibration_ece": _finite_or_nan(calibration.get("ece")),
583
+ "pairwise_causal_calibration_pairs": float(calibration.get("num_pairs") or 0),
584
+ "pairwise_causal_calibration_accuracy": _finite_or_nan(calibration.get("accuracy")),
585
+ "pairwise_causal_calibration_confidence": _finite_or_nan(
586
+ calibration.get("mean_confidence")
587
+ ),
588
+ }
589
+
590
+
591
+ def _pairwise_calibration_global(calibration: dict[str, Any]) -> dict[str, Any]:
592
+ return {key: value for key, value in calibration.items() if key != "rows"}
593
+
594
+
595
+ def _summary_with_pairwise(
596
+ rows: list[dict[str, Any]],
597
+ pairwise_calibration: dict[str, Any],
598
+ ) -> dict[str, float | None]:
599
+ summary = _simple_summary(rows)
600
+ summary.update(_pairwise_calibration_scalars(pairwise_calibration))
601
+ return summary
602
+
603
+
604
  def _group_means(
605
  rows: list[dict[str, Any]],
606
  key: str,
 
629
  return sum(clean) / len(clean) if clean else None
630
 
631
 
632
+ def _finite_or_nan(value: Any) -> float:
633
+ return float(value) if isinstance(value, (int, float)) and math.isfinite(float(value)) else math.nan
634
+
635
+
636
+ def _base_safety_label(row: dict[str, Any]) -> bool | None:
637
+ label = outcome_safety_violation(row.get("base_outcome"))
638
+ if label is not None:
639
+ return label
640
+ if "base_safety_violation" in row:
641
+ return outcome_safety_violation({"safety_violation": row.get("base_safety_violation")})
642
+ return None
643
+
644
+
645
+ def _candidate_safety_labels(row: dict[str, Any], *, count: int) -> list[bool | None]:
646
+ outcomes = row.get("candidate_outcomes", [])
647
+ flags = row.get("candidate_safety_violation", [])
648
+ labels: list[bool | None] = []
649
+ for index in range(count):
650
+ label = None
651
+ if isinstance(outcomes, list) and index < len(outcomes):
652
+ label = outcome_safety_violation(outcomes[index])
653
+ if label is None and isinstance(flags, list) and index < len(flags):
654
+ label = outcome_safety_violation({"safety_violation": flags[index]})
655
+ labels.append(label)
656
+ return labels
657
+
658
+
659
+ def _safety_label_coverage(labels: list[bool | None]) -> float:
660
+ if not labels:
661
+ return math.nan
662
+ return sum(label is not None for label in labels) / len(labels)
663
+
664
+
665
+ def _unsafe_rate_known(labels: list[bool | None]) -> float:
666
+ known = [label for label in labels if label is not None]
667
+ if not known:
668
+ return math.nan
669
+ return sum(float(label) for label in known) / len(known)
670
+
671
+
672
+ def _label_to_float(label: bool | None) -> float:
673
+ return math.nan if label is None else float(label)
674
+
675
+
676
+ def _float_to_label(value: Any) -> bool | None:
677
+ if not isinstance(value, (int, float)) or not math.isfinite(float(value)):
678
+ return None
679
+ return bool(float(value))
680
+
681
+
682
  def _table(metrics: dict[str, Any]) -> str:
683
  summary = metrics["eval_summary"]
684
  lines = [
685
  "% Auto-generated by scripts/eval_dominance_selector.py",
686
+ "\\begin{tabular}{lrrrrrrrrrr}",
687
  "\\toprule",
688
+ "Rows & Coverage & Fallback & Unsafe exec. & Base succ. & Selected succ. & Oracle succ. & OutcomePTR & Succ. support gap & Succ. selector gap & Cal. ECE \\\\",
689
  "\\midrule",
690
  f"{metrics['num_eval_rows']} & {_fmt(summary.get('coverage'))} & "
691
+ f"{_fmt(summary.get('fallback_rate'))} & {_fmt(summary.get('unsafe_execution_known'))} & "
692
+ f"{_fmt(summary.get('base_success'))} & "
693
  f"{_fmt(summary.get('selected_success'))} & {_fmt(summary.get('proposal_oracle_success'))} & "
694
  f"{_fmt(summary.get('outcome_ptr'))} & {_fmt(summary.get('success_support_gap'))} & "
695
+ f"{_fmt(summary.get('success_selector_gap'))} & "
696
+ f"{_fmt(summary.get('pairwise_causal_calibration_ece'))} \\\\",
697
  "\\bottomrule",
698
  "\\end{tabular}",
699
  ]
 
714
  "",
715
  "The threshold is fit on calibration rows only. Eval outcomes are used only for reporting.",
716
  "",
717
+ "| Split | Coverage | Fallback | Unsafe exec. | Safety label coverage | Base success | Selected success | Proposal oracle | OutcomePTR | Success support gap | Success selector gap | Calibration ECE |",
718
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
719
  f"| calibration | {_fmt(calibration.get('coverage'))} | {_fmt(calibration.get('fallback_rate'))} | "
720
+ f"{_fmt(calibration.get('unsafe_execution_known'))} | {_fmt(calibration.get('unsafe_execution_label_known'))} | "
721
  f"{_fmt(calibration.get('base_success'))} | {_fmt(calibration.get('selected_success'))} | "
722
  f"{_fmt(calibration.get('proposal_oracle_success'))} | {_fmt(calibration.get('outcome_ptr'))} | "
723
+ f"{_fmt(calibration.get('success_support_gap'))} | {_fmt(calibration.get('success_selector_gap'))} | "
724
+ f"{_fmt(calibration.get('pairwise_causal_calibration_ece'))} |",
725
  f"| eval | {_fmt(summary.get('coverage'))} | {_fmt(summary.get('fallback_rate'))} | "
726
+ f"{_fmt(summary.get('unsafe_execution_known'))} | {_fmt(summary.get('unsafe_execution_label_known'))} | "
727
  f"{_fmt(summary.get('base_success'))} | {_fmt(summary.get('selected_success'))} | "
728
  f"{_fmt(summary.get('proposal_oracle_success'))} | {_fmt(summary.get('outcome_ptr'))} | "
729
+ f"{_fmt(summary.get('success_support_gap'))} | {_fmt(summary.get('success_selector_gap'))} | "
730
+ f"{_fmt(summary.get('pairwise_causal_calibration_ece'))} |",
731
  "",
732
+ "This is a calibrated fallback diagnostic over already measured candidates; unsafe rates use available action-bound safety labels only.",
733
  ]
734
  return "\n".join(lines)
735