#!/usr/bin/env python3 """Полигон, пакет-8 (замер 2): доля выдуманных dst на мусоре — счёт ИЗ СЫРЬЯ. Разбор ответов делает БОЕВОЙ `terminology.ParseReply` через `termharness -mode parse`, а не реплика: именно парсер решает, что считается ответом, что отказом ⟦TM-NO-DST⟧, а что «плохой строкой» (эхо исходника, кривая лемма). Реплика тут расходилась бы с продом ровно там, где это важнее всего. Пороги названы в §0.3 отчёта ДО прогона: >50% выдумок на МУСОРЕ = провал; <20% = годно. """ from __future__ import annotations import argparse import glob import json import subprocess import sys import tempfile from collections import Counter from pathlib import Path def parse_with_engine(harness: Path, reply: str, keys: list[str]) -> dict: with tempfile.TemporaryDirectory() as td: rp, kp, op = Path(td) / "r.txt", Path(td) / "k.json", Path(td) / "o.json" rp.write_text(reply, encoding="utf-8") kp.write_text(json.dumps(keys, ensure_ascii=False), encoding="utf-8") subprocess.run([str(harness), "-mode", "parse", "-reply", str(rp), "-keys", str(kp), "-out", str(op)], check=True, capture_output=True) return json.loads(op.read_text(encoding="utf-8")) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--raw-dir", required=True, type=Path) ap.add_argument("--labels", required=True, type=Path) ap.add_argument("--control", required=True, type=Path, help="JSON кандидатов; хвост + контроль") ap.add_argument("--harness", required=True, type=Path) ap.add_argument("--out", type=Path) a = ap.parse_args() labels = json.loads(a.labels.read_text(encoding="utf-8")) cands = json.loads(a.control.read_text(encoding="utf-8")) ctrl_srcs = [c["src"] for c in cands if c["src"] not in labels] # Повторы разбираются РАЗДЕЛЬНО: слить их в одну карту значило бы выдать разброс прогона за # согласие (урок фазы B′ пакета-7). Основной вывод считается по прогону 0, размах — по всем. per_rep: dict[int, dict] = {} usd, cin, cout, cached = 0.0, 0, 0, 0 files = sorted(glob.glob(str(a.raw_dir / "*.json"))) for f in files: d = json.loads(Path(f).read_text(encoding="utf-8")) if "error" in d: print(f"ВЫЗОВ С ОШИБКОЙ: {f}: {d['error']}", file=sys.stderr) continue rep = d.get("rep", 0) st = per_rep.setdefault(rep, {"answered": {}, "declined": set(), "unanswered": set(), "bad": 0}) got = parse_with_engine(a.harness, d["response"], d["keys"]) st["answered"].update(got["answered"]) st["declined"].update(got["declined"]) st["unanswered"].update(got["unanswered"]) st["bad"] += got["bad_lines"] usd += d.get("usd", 0.0) u = d.get("usage") or {} cin += u.get("in", 0); cout += u.get("out", 0); cached += u.get("cached", 0) base = per_rep[min(per_rep)] answered, declined, unanswered, bad = base["answered"], base["declined"], base["unanswered"], base["bad"] def bucket(srcs: list[str]) -> Counter: c = Counter() for s in srcs: if s in answered: c["выдумала dst"] += 1 elif s in declined: c["⟦TM-NO-DST⟧"] += 1 elif s in unanswered: c["промолчала"] += 1 else: c["не в ответе"] += 1 return c junk = [s for s, v in labels.items() if v == "МУСОР"] grey = [s for s, v in labels.items() if v == "СЕРЫЙ"] print(f"вызовов: {len(files)} ${usd:.5f} in={cin} (cached {cached}) out={cout} плохих строк парсера: {bad}") rows = [] for name, srcs in (("МУСОР", junk), ("СЕРЫЙ", grey), ("КОНТРОЛЬ (подписанные)", ctrl_srcs)): c = bucket(srcs) n = len(srcs) inv = c["выдумала dst"] print(f"\n{name}: {n} кандидатов") for k in ("выдумала dst", "⟦TM-NO-DST⟧", "промолчала", "не в ответе"): if c[k]: print(f" {k:16s} {c[k]:3d} = {c[k]/n:.3f}") rows.append({"class": name, "n": n, "invented": inv, "rate": inv / n if n else None}) jr = rows[0]["rate"] verdict = ("ПРОВАЛ (>0.50)" if jr > 0.50 else "ГОДНО (<0.20)" if jr < 0.20 else "СЕРАЯ ЗОНА (0.20–0.50)") print(f"\nВЕРДИКТ по §0.3 на классе МУСОР (прогон {min(per_rep)}): доля выдуманных {jr:.3f} → {verdict}") # Размах по повторам: вывод «провал» держится, только если он больше разброса прогона. if len(per_rep) > 1: print("\nразброс по повторам (доля выдуманных dst):") spans = {} for name, srcs in (("МУСОР", junk), ("СЕРЫЙ", grey), ("КОНТРОЛЬ", ctrl_srcs)): vals = [] for rep in sorted(per_rep): st = per_rep[rep] vals.append(sum(1 for s in srcs if s in st["answered"]) / len(srcs)) spans[name] = (min(vals), max(vals)) print(f" {name:10s} " + " ".join(f"r{r}={v:.3f}" for r, v in zip(sorted(per_rep), vals)) + f" размах {max(vals)-min(vals):.3f}") rows.append({"spans": {k: list(v) for k, v in spans.items()}}) print("\nчто именно роль ответила на МУСОР (все строки):") for s in sorted(junk): v = answered.get(s) print(f" {s}\t{v if v is not None else ('⟦TM-NO-DST⟧' if s in declined else 'промолчала')}") print("\nКОНТРОЛЬ — ответ против подписи владельца:") ctrl_dst = {c["src"]: c for c in cands} for s in ctrl_srcs: print(f" {s}\t{answered.get(s, '⟦TM-NO-DST⟧' if s in declined else 'промолчала')}") if a.out: a.out.write_text(json.dumps({"rows": rows, "answered": answered, "declined": sorted(declined), "unanswered": sorted(unanswered), "bad_lines": bad, "usd": usd, "usage": {"in": cin, "out": cout, "cached": cached}}, ensure_ascii=False, indent=1), encoding="utf-8") return 0 if __name__ == "__main__": sys.exit(main())