refactor: sort scripts/ by who runs it

Top level is now only what the runbook or a re-run touches: validate_persona_axes,
bounded_thinking_judge, template_catalog, export_selections, parse_stage_a, run_axis,
export_steering_selection. Corpus ingestion and publishing moved to scripts/corpus/,
plotting and stats to scripts/report/.

Moved files needed parents[1] -> parents[2]; the two corpus scripts that import
template_catalog use the sys.path shim bounded_thinking_judge_liveproof already used.

Also completes the export_steering_selection rename: an earlier git reset had dropped
the staged deletion, leaving both filenames tracked.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-07-25 10:23:22 +08:00
co-authored by Claudypoo
parent 3593189142
commit b8e22f42fb
14 changed files with 10 additions and 182 deletions
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
import json
import math
from pathlib import Path
import statistics
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
STATS = ROOT / "data/results/stats"
MODEL_MATRIX = ROOT / "data/results/model_matrix"
DOCS_MODEL_MATRIX = ROOT / "docs/results/model_matrix"
NORMAL_TEMPLATE_PAIR_STATS = STATS / "v2_pilot_seed24_template_pair_stats.jsonl"
ENGINEERED_TEMPLATE_PAIR_STATS = STATS / "engineered_baseline_seed24_template_pair_stats.jsonl"
CONTROL_TEMPLATE_PAIR_STATS = STATS / "control_baseline_seed24_template_pair_stats.jsonl"
REFUSAL_MODEL_PAIR_STATS = [
MODEL_MATRIX / "stats/refusal_probe_seed24_n1_google_gemma-2-27b-it_template_pair_stats.jsonl",
MODEL_MATRIX / "stats/refusal_probe_seed24_n1_google_gemma-3-4b-it_template_pair_stats.jsonl",
MODEL_MATRIX / "stats/refusal_probe_seed24_n1_qwen_qwen3.6-flash_template_pair_stats.jsonl",
MODEL_MATRIX / "stats/refusal_probe_seed24_n1_ibm-granite_granite-4.1-8b_template_pair_stats.jsonl",
]
REFUSAL_MODEL_PREFIX = MODEL_MATRIX / "refusal_probe_seed24_n1"
REFUSAL_MODEL_MARKDOWN = DOCS_MODEL_MATRIX / "refusal_probe_seed24_n1_model_matrix_summary.md"
ANTHROPIC_IF2_COMMENT = "<!-- instruction following eval, Anthropic/if-2 -->"
ANTHROPIC_IF2_LABEL = "Anthropic/if-2 instruction-following eval:"
def read_jsonl(path: Path) -> list[dict[str, Any]]:
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
def display_template_text(text: str) -> str:
return text.replace(ANTHROPIC_IF2_COMMENT, ANTHROPIC_IF2_LABEL)
def clamp01(x: float) -> float:
return max(0.0, min(1.0, x))
def mean(xs: list[float]) -> float:
return sum(xs) / len(xs)
def std(xs: list[float]) -> float:
if len(xs) == 1:
return 0.0
return statistics.stdev(xs)
def score(row: dict[str, Any]) -> float:
on_axis = clamp01(float(row["mean_axis_delta"]) / 8.0)
off_axis = clamp01((float(row["mean_off_axis_problem"]) - 1.0) / 6.0)
return 100.0 * on_axis * (1.0 - off_axis)
def score_t(scores: list[float]) -> float:
sem = std(scores) / math.sqrt(len(scores))
mean_score = mean(scores)
if sem == 0.0:
return 0.0 if mean_score == 0.0 else 1_000_000.0
return mean_score / sem
def mean_template_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
grouped: dict[str, list[dict[str, Any]]] = {}
for row in rows:
grouped.setdefault(row["template"], []).append({**row, "score": score(row)})
out = []
for template, rs in grouped.items():
scores = [float(row["score"]) for row in rs]
out.append({
"template": template,
"score_t": round(score_t(scores), 2),
"score": round(mean(scores), 1),
"score_mean": round(mean(scores), 2),
"on_axis": clamp01(mean([float(row["mean_axis_delta"]) for row in rs]) / 8.0),
"off_axis": clamp01(
(mean([float(row["mean_off_axis_problem"]) for row in rs]) - 1.0) / 6.0),
"axis_delta": round(mean([float(row["mean_axis_delta"]) for row in rs]), 2),
"off_axis_problem": round(mean([float(row["mean_off_axis_problem"]) for row in rs]), 2),
"judge_std": round(mean([float(row["mean_axis_delta_judge_std"]) for row in rs]), 2),
"n_cells": len(rs),
})
return sorted(out, key=lambda row: row["score_t"], reverse=True)
@@ -0,0 +1,253 @@
"""Export upload-friendly stats from persona template validation artifacts.
Input is the JSON written by scripts/validate_persona_axes.py.
Outputs:
<out-prefix>_template_stats.jsonl one row per template
<out-prefix>_template_pair_stats.jsonl one row per template × persona pair
<out-prefix>_examples.jsonl one row per generated pair
<out-prefix>_template_stats.csv compact table for spreadsheets
"""
from __future__ import annotations
import argparse
import csv
import json
from collections import defaultdict
from pathlib import Path
from statistics import mean
def _rows(paths: list[Path]) -> list[dict]:
rows = []
for path in paths:
data = json.loads(path.read_text())
meta = {
"artifact": str(path),
"generator_model": data.get("generator_model"),
"judge_model": data.get("judge_model"),
"axis_judge_models": data["axis_judge_models"],
"style_judge_model": data["style_judge_model"],
"gen_temperature": data.get("gen_temperature"),
"seed": data.get("seed"),
"family": data.get("family"),
}
for rec in data.get("results", []):
row = {**meta, **rec}
rows.append(row)
return rows
def _m(vals: list[float]) -> float | None:
return round(mean(vals), 4) if vals else None
def _aggregate(rows: list[dict], keys: tuple[str, ...]) -> list[dict]:
groups: dict[tuple, list[dict]] = defaultdict(list)
for r in rows:
if "error" not in r:
groups[tuple(r[k] for k in keys)].append(r)
out = []
for key, rs in groups.items():
n = len(rs)
strict = [bool(r.get("strict_pass")) for r in rs]
style_dims = sorted(rs[0].get("style_deltas_pos_minus_neg", {}))
off_axis_dims = sorted(rs[0].get("off_axis_category_likerts", {}))
row = {k: v for k, v in zip(keys, key)}
row.update({
"n": n,
"strict_pass_rate": round(sum(strict) / n, 4),
"n_strict_pass": sum(strict),
"mean_axis_delta": _m([float(r["axis_delta"]) for r in rs]),
"mean_axis_delta_judge_mean": _m([
float(r["axis_delta_judge_mean"]) for r in rs
]),
"mean_axis_delta_judge_std": _m([
float(r["axis_delta_judge_std"]) for r in rs
]),
"mean_axis_judge_abs_disagreement": _m([
float(r["axis_judge_mean_abs_disagreement"]) for r in rs
]),
"mean_delta_pos_vs_base": _m([float(r["delta_pos_vs_base"]) for r in rs]),
"mean_delta_base_vs_neg": _m([float(r["delta_base_vs_neg"]) for r in rs]),
"mean_min_side_delta": _m([float(r["min_side_delta"]) for r in rs]),
"mean_off_axis_problem": _m([
float(r["confound_judgment"]["off_axis_problem_likert"]) for r in rs
]),
"mean_max_off_axis_category_likert": _m([
float(r.get("max_off_axis_category_likert", 7)) for r in rs
]),
"usable_rate": round(
sum(bool(r["confound_judgment"]["usable_for_training"]) for r in rs) / n, 4),
"mean_max_style_abs_delta": _m([float(r["max_style_abs_delta"]) for r in rs]),
"mean_abs_word_delta_frac": _m([abs(float(r["word_delta_frac"])) for r in rs]),
"mean_response_token_jaccard": _m([
float(r.get("response_token_jaccard", 0.0)) for r in rs
]),
"mean_pos_repeated_token_frac": _m([
float(r.get("pos_repeated_token_frac", 0.0)) for r in rs
]),
"mean_neg_repeated_token_frac": _m([
float(r.get("neg_repeated_token_frac", 0.0)) for r in rs
]),
"persona_echo_rate": round(sum(bool(r["persona_echo"]) for r in rs) / n, 4),
"judge_persona_echo_rate": round(
sum(bool(r.get("judge_persona_echo")) for r in rs) / n, 4),
"refusal_or_ai_break_rate": round(
sum(bool(r["refusal_or_ai_break"]) for r in rs) / n, 4),
"judge_refusal_or_ai_break_rate": round(
sum(bool(r.get("judge_refusal_or_ai_break")) for r in rs) / n, 4),
"strict_pass_persona_pairs": sorted({
r["axis"]["id"] for r in rs if r.get("strict_pass")
}),
"common_spurious_axes": sorted({
r["confound_judgment"].get("likely_spurious_axis", "")
for r in rs
if r["confound_judgment"].get("likely_spurious_axis")
}),
})
for dim in style_dims:
row[f"mean_style_delta_{dim}_pos_minus_neg"] = _m([
float(r["style_deltas_pos_minus_neg"][dim]) for r in rs
])
for dim in off_axis_dims:
row[f"mean_off_axis_{dim}"] = _m([
float(r["off_axis_category_likerts"][dim]) for r in rs
])
row["recommended"] = (
n >= 4
and row["strict_pass_rate"] >= 0.5
and row["mean_axis_delta"] >= 3
and row["mean_off_axis_problem"] <= 2
and row["mean_max_style_abs_delta"] <= 2
and row["persona_echo_rate"] == 0
and row["refusal_or_ai_break_rate"] == 0
)
out.append(row)
out.sort(key=lambda r: (
r["recommended"],
r["strict_pass_rate"],
r["mean_min_side_delta"],
r["mean_axis_delta"],
-r["mean_off_axis_problem"],
-r["mean_max_style_abs_delta"],
), reverse=True)
return out
def _example_rows(rows: list[dict]) -> list[dict]:
out = []
for r in rows:
axis = r.get("axis", {})
rec = {
"artifact": r.get("artifact"),
"eval_id": r.get("eval_id"),
"template": r.get("template"),
"persona_pair": axis.get("id"),
"scenario_id": r.get("scenario_id"),
"pos_persona": axis.get("pos_descriptor"),
"neg_persona": axis.get("neg_descriptor"),
"row": r.get("row"),
"source": r.get("source"),
"config": r.get("config"),
"prompt": r.get("prompt"),
"pos_generation_prompt": r.get("pos_generation_prompt"),
"neg_generation_prompt": r.get("neg_generation_prompt"),
"error": r.get("error"),
}
if "error" not in r:
rec.update({
"strict_pass": r.get("strict_pass"),
"axis_judge_models": r.get("axis_judge_models"),
"axis_judgments": r.get("axis_judgments"),
"axis_delta": r.get("axis_delta"),
"axis_delta_judge_mean": r.get("axis_delta_judge_mean"),
"axis_delta_judge_std": r.get("axis_delta_judge_std"),
"axis_judge_mean_abs_disagreement": r.get("axis_judge_mean_abs_disagreement"),
"delta_pos_vs_base": r.get("delta_pos_vs_base"),
"delta_base_vs_neg": r.get("delta_base_vs_neg"),
"min_side_delta": r.get("min_side_delta"),
"off_axis_problem": r["confound_judgment"].get("off_axis_problem_likert"),
"max_off_axis_category_likert": r.get("max_off_axis_category_likert"),
"usable_for_training": r["confound_judgment"].get("usable_for_training"),
"likely_spurious_axis": r["confound_judgment"].get("likely_spurious_axis"),
"max_style_abs_delta": r.get("max_style_abs_delta"),
"word_delta_frac": r.get("word_delta_frac"),
"response_token_jaccard": r.get("response_token_jaccard"),
"pos_repeated_token_frac": r.get("pos_repeated_token_frac"),
"neg_repeated_token_frac": r.get("neg_repeated_token_frac"),
"persona_echo": r.get("persona_echo"),
"judge_persona_echo": r.get("judge_persona_echo"),
"pos_persona_echo_hits": r.get("pos_persona_echo_hits"),
"neg_persona_echo_hits": r.get("neg_persona_echo_hits"),
"pos_persona_overlap_tokens": r.get("pos_persona_overlap_tokens"),
"neg_persona_overlap_tokens": r.get("neg_persona_overlap_tokens"),
"refusal_or_ai_break": r.get("refusal_or_ai_break"),
"judge_refusal_or_ai_break": r.get("judge_refusal_or_ai_break"),
"pos_refusal_phrase_hits": r.get("pos_refusal_phrase_hits"),
"neg_refusal_phrase_hits": r.get("neg_refusal_phrase_hits"),
"pos_response": r.get("pos_response"),
"neg_response": r.get("neg_response"),
"base_response": r.get("base_response"),
})
for dim, val in r.get("style_deltas_pos_minus_neg", {}).items():
rec[f"style_delta_{dim}_pos_minus_neg"] = val
for dim, val in r.get("off_axis_category_likerts", {}).items():
rec[f"off_axis_{dim}"] = val
out.append(rec)
return out
def _write_jsonl(path: Path, rows: list[dict]) -> None:
with path.open("w") as fh:
for row in rows:
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
def _write_csv(path: Path, rows: list[dict]) -> None:
if not rows:
path.write_text("")
return
fieldnames = sorted({k for row in rows for k in row})
with path.open("w", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=fieldnames, lineterminator="\n")
writer.writeheader()
for row in rows:
writer.writerow({
k: json.dumps(v, ensure_ascii=False) if isinstance(v, (list, dict)) else v
for k, v in row.items()
})
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("artifacts", nargs="+", type=Path)
ap.add_argument("--out-prefix", default="data/results/stats/persona_template_library")
args = ap.parse_args()
rows = _rows(args.artifacts)
for r in rows:
if "axis" in r and "error" not in r:
r["persona_pair"] = r["axis"]["id"]
template_stats = _aggregate(rows, ("template",))
pair_stats = _aggregate(rows, ("template", "persona_pair"))
examples = _example_rows(rows)
prefix = Path(args.out_prefix)
prefix.parent.mkdir(parents=True, exist_ok=True)
_write_jsonl(prefix.with_name(prefix.name + "_template_stats.jsonl"), template_stats)
_write_jsonl(prefix.with_name(prefix.name + "_template_pair_stats.jsonl"), pair_stats)
_write_jsonl(prefix.with_name(prefix.name + "_examples.jsonl"), examples)
_write_csv(prefix.with_name(prefix.name + "_template_stats.csv"), template_stats)
_write_csv(prefix.with_name(prefix.name + "_template_pair_stats.csv"), pair_stats)
print(f"examples={len(examples)} template_stats={len(template_stats)} pair_stats={len(pair_stats)}")
print("top templates:")
for row in template_stats[:10]:
print(
f"{row['strict_pass_rate']:.2f} pass axis={row['mean_axis_delta']:.2f} "
f"off={row['mean_off_axis_problem']:.2f} style={row['mean_max_style_abs_delta']:.2f} "
f"{row['template']}"
)
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
"""Write the previous-results Plotly figure as PNG and SVG."""
from __future__ import annotations
import readme_plot
def main() -> None:
readme_plot.write_main_plot_assets()
print(readme_plot.MAIN_PNG)
print(readme_plot.MAIN_SVG)
if __name__ == "__main__":
main()
+98
View File
@@ -0,0 +1,98 @@
from __future__ import annotations
import html
from pathlib import Path
import textwrap
from typing import Any
import plotly.graph_objects as go
import docs_results
MAIN_PNG = docs_results.ROOT / "docs/results/assets/on_off_axis.png"
MAIN_SVG = docs_results.ROOT / "docs/results/assets/on_off_axis.svg"
def _wrap_hover(text: str, width: int = 62) -> str:
text = docs_results.display_template_text(text)
escaped = html.escape(" ".join(text.split()))
return "<br>".join(
textwrap.wrap(escaped, width=width, break_long_words=True, break_on_hyphens=False))
def main_plot_rows(path: Path = docs_results.NORMAL_TEMPLATE_PAIR_STATS) -> list[dict[str, Any]]:
return docs_results.mean_template_rows(docs_results.read_jsonl(path))
def template_scatter(rows: list[dict[str, Any]] | None = None, width: int | None = None) -> go.Figure:
rows = main_plot_rows() if rows is None else rows
top_rank = {row["template"]: i for i, row in enumerate(rows[:10], start=1)}
text = [str(top_rank[row["template"]]) if row["template"] in top_rank else "" for row in rows]
hover = [
"<br>".join([
f"<b>{_wrap_hover(row['template'])}</b>",
f"rank: {i}",
f"score t: {row['score_t']:.2f}",
f"score mean: {row['score_mean']:.2f}",
f"axis delta: {row['axis_delta']:.2f}",
f"off-axis problem: {row['off_axis_problem']:.2f}",
f"judge std: {row['judge_std']:.2f}",
f"cells: {row['n_cells']}",
])
for i, row in enumerate(rows, start=1)
]
fig = go.Figure(
data=go.Scatter(
x=[row["on_axis"] for row in rows],
y=[row["off_axis"] for row in rows],
mode="markers+text",
text=text,
textposition="middle center",
textfont={"size": 9, "color": "white"},
customdata=hover,
hovertemplate="%{customdata}<extra></extra>",
marker={
"size": 10,
"color": [row["score_t"] for row in rows],
"colorscale": "Cividis",
"showscale": True,
"colorbar": {"title": "score t"},
"line": {"width": 0.5, "color": "white"},
"opacity": 0.9,
},
)
)
fig.update_layout(
autosize=True,
width=width,
height=620,
template="plotly_white",
margin={"l": 68, "r": 24, "t": 28, "b": 66},
xaxis={
"title": "on-axis movement, higher is better",
"range": [-0.02, 1.02],
"gridcolor": "rgba(0,0,0,0.08)",
},
yaxis={
"title": "off-axis confounding, lower is better",
"range": [-0.02, 1.02],
"gridcolor": "rgba(0,0,0,0.08)",
},
annotations=[{
"text": "normal pilot scenarios; one point per measured template",
"xref": "paper",
"yref": "paper",
"x": 1.0,
"y": -0.13,
"showarrow": False,
"font": {"size": 11, "color": "rgba(0,0,0,0.62)"},
}],
)
return fig
def write_main_plot_assets() -> None:
fig = template_scatter(width=960)
MAIN_PNG.parent.mkdir(parents=True, exist_ok=True)
fig.write_image(MAIN_PNG, width=960, height=620, scale=2)
fig.write_image(MAIN_SVG, width=960, height=620)
+216
View File
@@ -0,0 +1,216 @@
from __future__ import annotations
import argparse
import csv
import json
import math
from pathlib import Path
import statistics
from typing import Any
from tabulate import tabulate
import docs_results
ROOT = Path(__file__).resolve().parents[2]
DEFAULT_PAIR_STATS = docs_results.REFUSAL_MODEL_PAIR_STATS
DEFAULT_OUT_PREFIX = docs_results.REFUSAL_MODEL_PREFIX
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
def _model_name(path: Path) -> str:
name = path.name
name = name.removeprefix("refusal_probe_seed24_n1_")
name = name.removesuffix("_template_pair_stats.jsonl")
return name
def _clamp01(x: float) -> float:
return max(0.0, min(1.0, x))
def _score(row: dict[str, Any]) -> float:
on_axis = _clamp01(float(row["mean_axis_delta"]) / 8.0)
off_axis = _clamp01((float(row["mean_off_axis_problem"]) - 1.0) / 6.0)
return 100.0 * on_axis * (1.0 - off_axis)
def _mean(xs: list[float]) -> float:
return sum(xs) / len(xs)
def _std(xs: list[float]) -> float:
if len(xs) == 1:
return 0.0
return statistics.stdev(xs)
def _p25(xs: list[float]) -> float:
return statistics.quantiles(xs, n=4, method="inclusive")[0]
def _sem(xs: list[float]) -> float:
return _std(xs) / math.sqrt(len(xs))
def _t_stat(mean: float, sem: float) -> float:
if sem == 0.0:
return 0.0 if mean == 0.0 else 1_000_000.0
return mean / sem
def _round(x: float, digits: int = 3) -> float:
if math.isnan(x):
raise ValueError("nan in model matrix summary")
return round(x, digits)
def _write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows))
def _write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
def _template_mean_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
groups: dict[tuple[str, str], list[dict[str, Any]]] = {}
for row in rows:
groups.setdefault((row["model"], row["template"]), []).append(row)
out = []
for (model, template), rs in groups.items():
out.append({
"model": model,
"template": template,
"score": _mean([row["score"] for row in rs]),
"strict_pass_rate": _mean([float(row["strict_pass_rate"]) for row in rs]),
"mean_axis_delta": _mean([float(row["mean_axis_delta"]) for row in rs]),
"mean_off_axis_problem": _mean([float(row["mean_off_axis_problem"]) for row in rs]),
"mean_axis_delta_judge_std": _mean([float(row["mean_axis_delta_judge_std"]) for row in rs]),
"mean_max_style_abs_delta": _mean([float(row["mean_max_style_abs_delta"]) for row in rs]),
"persona_echo_rate": _mean([float(row["persona_echo_rate"]) for row in rs]),
"refusal_or_ai_break_rate": _mean([float(row["refusal_or_ai_break_rate"]) for row in rs]),
"n_axes": len(rs),
})
return out
def _summarize(rows: list[dict[str, Any]], group_cols: list[str]) -> list[dict[str, Any]]:
groups: dict[tuple[Any, ...], list[dict[str, Any]]] = {}
for row in rows:
groups.setdefault(tuple(row[col] for col in group_cols), []).append(row)
out = []
for key, rs in groups.items():
models = sorted({row["model"] for row in rs})
base = dict(zip(group_cols, key, strict=True))
model_count = len(models)
scores = [float(row["score"]) for row in rs]
score_mean = _mean(scores)
score_sem = _sem(scores)
out.append({
"model_count": model_count,
"score_t": _round(_t_stat(score_mean, score_sem), 2),
"score_p25": _round(_p25(scores), 2),
"score_mean": _round(score_mean, 2),
"score_std": _round(_std(scores), 2),
"strict_pass_rate_mean": _round(_mean([float(row["strict_pass_rate"]) for row in rs]), 3),
"strict_pass_rate_std": _round(_std([float(row["strict_pass_rate"]) for row in rs]), 3),
"axis_delta_mean": _round(_mean([float(row["mean_axis_delta"]) for row in rs]), 3),
"axis_delta_std": _round(_std([float(row["mean_axis_delta"]) for row in rs]), 3),
"off_axis_problem_mean": _round(_mean([float(row["mean_off_axis_problem"]) for row in rs]), 3),
"off_axis_problem_std": _round(_std([float(row["mean_off_axis_problem"]) for row in rs]), 3),
"judge_std_mean": _round(_mean([float(row["mean_axis_delta_judge_std"]) for row in rs]), 3),
"style_delta_mean": _round(_mean([float(row["mean_max_style_abs_delta"]) for row in rs]), 3),
"persona_echo_rate_mean": _round(_mean([float(row["persona_echo_rate"]) for row in rs]), 3),
"refusal_or_ai_break_rate_mean": _round(
_mean([float(row["refusal_or_ai_break_rate"]) for row in rs]), 3),
"models": ",".join(models),
**base,
})
return sorted(out, key=lambda row: row["score_t"], reverse=True)
def _markdown_text(text: str) -> str:
text = docs_results.display_template_text(text)
text = text.replace("{persona}", "`{persona}`")
text = text.replace("&", "&amp;")
text = text.replace("<", "&lt;")
text = text.replace(">", "&gt;")
text = text.replace("\\", "&#92;")
text = text.replace("|", "&#124;")
return text.replace("\n", "<br>")
def _write_markdown(path: Path, template_rows: list[dict[str, Any]], pair_rows: list[dict[str, Any]], top_n: int) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
top_template_rows = [
{
"score t": f"{row['score_t']:.2f}",
"score mean": f"{row['score_mean']:.2f}",
"score std": f"{row['score_std']:.2f}",
"pass": f"{row['strict_pass_rate_mean']:.3f}",
"echo": f"{row['persona_echo_rate_mean']:.3f}",
"refusal": f"{row['refusal_or_ai_break_rate_mean']:.3f}",
"template": _markdown_text(row["template"]),
}
for row in template_rows[:top_n]
]
lines = [
"# Refusal-pole probe",
"",
"Scores are model-equal. Each model first averages the two refusal-probe axes per template, then the table reports reliability-sorted template rows across clean model artifacts.",
"",
"## All templates",
"",
"`score t` is mean score divided by standard error across the four clean model artifacts. `pass` is strict-pass rate; `echo` is explicit persona echo; `refusal` is refusal or AI-role break. Rows are sorted by `score t`.",
"",
tabulate(top_template_rows, headers="keys", tablefmt="github", disable_numparse=True),
]
path.write_text("\n".join(lines) + "\n")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--pair-stats", nargs="+", type=Path, default=DEFAULT_PAIR_STATS)
ap.add_argument("--out-prefix", type=Path, default=DEFAULT_OUT_PREFIX)
ap.add_argument("--top-n", type=int, default=999)
args = ap.parse_args()
rows = []
for path in args.pair_stats:
model = _model_name(path)
model_rows = []
for row in _read_jsonl(path):
model_rows.append({**row, "model": model, "score": _score(row)})
if len(model_rows) != 190:
raise ValueError(f"{path} has {len(model_rows)} rows, expected 190")
rows.extend(model_rows)
template_rows = _summarize(_template_mean_rows(rows), ["template"])
pair_rows = _summarize(rows, ["template", "persona_pair"])
expected_models = len(args.pair_stats)
if any(row["model_count"] != expected_models for row in template_rows + pair_rows):
raise ValueError("at least one summary row is missing a model")
prefix = args.out_prefix
_write_jsonl(prefix.with_name(prefix.name + "_template_model_summary.jsonl"), template_rows)
_write_csv(prefix.with_name(prefix.name + "_template_model_summary.csv"), template_rows)
_write_jsonl(prefix.with_name(prefix.name + "_template_pair_model_summary.jsonl"), pair_rows)
_write_csv(prefix.with_name(prefix.name + "_template_pair_model_summary.csv"), pair_rows)
_write_markdown(docs_results.REFUSAL_MODEL_MARKDOWN, template_rows, pair_rows, args.top_n)
print(f"models={expected_models} templates={len(template_rows)} template_pairs={len(pair_rows)}")
print(docs_results.REFUSAL_MODEL_MARKDOWN)
if __name__ == "__main__":
main()