#!/usr/bin/env python3
"""記事 I の「書き手として（Round 1）」の指標を、お題単位のクラスター・ブートストラップの区間つきで出す。

  python3 headless-experiments/tools/i_r1_numbers.py \
      --set "本実験（記事 A・GPT-6.1 Sol Pro）=headless-results/exp1v2-final::headless-results/exp1v2-final/claims-final.jsonl" \
      --set "参考（Gemini 4 Argon 版）=headless-results/expB-argon-s1,headless-results/expB-argon-s2,headless-results/expB-argon-s3::<claims-typed.jsonl>" \
      --out headless-experiments/exp1v2-final-inputs/i-r1-numbers-2026-10-15.md

- 本文は討論 JSON の Round 1（roundNo=1、status=completed）。数え方は記事 I の初版の集計（2026-10-14、inline）と同じ正規表現。
- 主張の数・対象外の率は、主張ファイル（claims-final.jsonl の target=r1、または claims-typed.jsonl）がある書き手だけ。
- 区間: 15 テーマを復元抽出（10,000 回、seed 1）。文章ごとの値はテーマ内の文章を全部使って平均、率（対象外の率）は合算比。
"""
import argparse, glob, json, os, random, re
from collections import defaultdict

NUM = re.compile(r"\d[\d,\.]*\s*(%|％|年|月|日|円|万|億|件|人|歳|時間|条|字)")
HEDGE = re.compile(r"確認が必要|確認してください|可能性があります|と考えられ|場合があります|とされています|一般的に|注意が必要")
TARGET_TYPES = ("fact", "absence")  # 誤り率の対象。それ以外（推測・解釈・存在しない・空の否定）は対象外
SHORT = {
    "anthropic/claude-sonnet-5.5": "Claude Sonnet 5.5",
    "openai/gpt-6.1-sol-pro": "GPT-6.1 Sol Pro",
    "openai/gpt-6.1-sol": "GPT-6.1 Sol",
    "google/gemini-3.8-flash": "Gemini 3.8 Flash",
    "google/gemini-4-argon": "Gemini 4 Argon",
}
FAMILY_ORDER = ["anthropic", "openai", "google"]


def load_texts(dirs):
    """(dir名, file, model) → (questionId, 本文)"""
    out = {}
    for d in dirs:
        dn = os.path.basename(d.rstrip("/"))
        for f in sorted(glob.glob(os.path.join(d, "[0-9][0-9][0-9]-*.json"))):
            sj = json.load(open(f, encoding="utf-8"))
            for a in sj["attempts"]:
                if a["status"] == "completed" and a["roundNo"] == 1:
                    out[(dn, os.path.basename(f), a["model"])] = (sj["questionId"], a["content"])
    return out


def load_claims(path, dirs):
    """(dir名, file, model) → [種類…]。claims-final は dir を持たないので、dir が 1 つのときはその名前を使う。"""
    out = defaultdict(list)
    one = os.path.basename(dirs[0].rstrip("/")) if len(dirs) == 1 else None
    for l in open(path, encoding="utf-8"):
        c = json.loads(l)
        if c.get("target") != "r1":
            continue
        dn = c.get("dir") or one
        out[(dn, c["file"], c["model"])].append(c.get("claimType"))
    return out


def boot(units, stat, n=10000, seed=1):
    """units: テーマ → 値のリスト。stat: テーマのリストのリスト → 値"""
    topics = sorted(units)
    rng = random.Random(seed)
    vals = []
    for _ in range(n):
        pick = [units[rng.choice(topics)] for _ in topics]
        v = stat(pick)
        if v is not None:
            vals.append(v)
    vals.sort()
    return vals[int(0.025 * len(vals))], vals[int(0.975 * len(vals)) - 1]


def mean_stat(groups):
    xs = [x for g in groups for x in g]
    return sum(xs) / len(xs) if xs else None


def ratio_stat(groups):
    num = sum(a for g in groups for a, _ in g)
    den = sum(b for g in groups for _, b in g)
    return num / den if den else None


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--set", action="append", required=True, help="名前=dir1,dir2[::claims.jsonl]")
    ap.add_argument("--out", required=True)
    a = ap.parse_args()
    lines = ["# 記事 I：書き手として（Round 1）の指標と区間（自動生成・i_r1_numbers.py）", "",
             "- 値は「点推定 [95% 区間]」。区間はテーマ単位のクラスター・ブートストラップ（15 テーマを復元抽出、10,000 回、seed 1）。",
             "- 数字の書き込み: 「数字＋単位（% 年 月 日 円 万 億 件 人 歳 時間 条 字）」の数 ÷ 字数 × 1,000 の文章平均。",
             "- 留保の言い回し: 「確認が必要」「確認してください」「可能性があります」「と考えられ」「場合があります」「とされています」「一般的に」「注意が必要」の数 ÷ 字数 × 1,000 の文章平均。",
             "- 対象外の率: 主張のうち、種類が fact・absence 以外（推測・解釈・存在しないものの説明・空の否定）の割合（合算比）。", ""]
    for spec in a.set:
        name, rest = spec.split("=", 1)
        dirs_s, _, claims_path = rest.partition("::")
        dirs = [d for d in dirs_s.split(",") if d]
        texts = load_texts(dirs)
        claims = load_claims(claims_path, dirs) if claims_path else {}
        models = sorted({k[2] for k in texts}, key=lambda m: FAMILY_ORDER.index(m.split("/")[0]))
        lines += [f"## {name}", "", f"- 討論のフォルダ: {', '.join(dirs)}" + (f"／主張: {claims_path}" if claims_path else ""), ""]
        rows = [("文章の本数", None), ("平均の長さ（字）", "len"), ("指定字数（800〜1,200 字）に収めた割合", "within"),
                ("数字の書き込み（千字あたり）", "num"), ("留保の言い回し（千字あたり）", "hedge"),
                ("主張の数／本", "claims"), ("対象外（推測・解釈など）の率", "oos")]
        table = {m: {} for m in models}
        for m in models:
            per = defaultdict(lambda: defaultdict(list))
            for (dn, fn, mm), (q, tx) in texts.items():
                if mm != m:
                    continue
                k = len(tx) / 1000
                per["len"][q].append(len(tx))
                per["within"][q].append(1.0 if 800 <= len(tx) <= 1200 else 0.0)
                per["num"][q].append(len(NUM.findall(tx)) / k)
                per["hedge"][q].append(len(HEDGE.findall(tx)) / k)
                cs = claims.get((dn, fn, mm))
                if cs is not None:
                    per["claims"][q].append(len(cs))
                    per["oos"][q].append((sum(1 for t in cs if t not in TARGET_TYPES), len(cs)))
            n = sum(len(v) for v in per["len"].values())
            table[m]["文章の本数"] = f"{n}"
            for label, key in rows[1:]:
                if not per[key]:
                    table[m][label] = "—"
                    continue
                stat = ratio_stat if key == "oos" else mean_stat
                v = stat(list(per[key].values()))
                lo, hi = boot(per[key], stat)
                if key == "len":
                    table[m][label] = f"{v:,.0f} [{lo:,.0f}, {hi:,.0f}]"
                elif key in ("within", "oos"):
                    table[m][label] = f"{v:.1%} [{lo:.1%}, {hi:.1%}]"
                elif key == "claims":
                    table[m][label] = f"{v:.1f} [{lo:.1f}, {hi:.1f}]"
                else:
                    table[m][label] = f"{v:.2f} [{lo:.2f}, {hi:.2f}]"
        lines.append("| 指標 | " + " | ".join(SHORT.get(m, m) for m in models) + " |")
        lines.append("|---|" + "---|" * len(models))
        for label, _ in rows:
            lines.append(f"| {label} | " + " | ".join(table[m][label] for m in models) + " |")
        lines.append("")
    open(a.out, "w", encoding="utf-8").write("\n".join(lines) + "\n")
    print("\n".join(lines))


if __name__ == "__main__":
    main()
