#!/usr/bin/env python3 """РАСКЛАДКА СУДЕЙСКИХ ПАЧЕК ПО СЕССИЯМ и печать готовых промтов. $0. Зачем скриптом, а не руками. Состав сессии — часть прибора, а не оформление: от него зависит, (1) не попадут ли обе половины одной пары шумового пола к ОДНОМУ судье — тогда пол не поймает межсессионную компоненту, и порог будет занижен, как в паке 23 на 19%; (2) одинаково ли нагружены семейства — если Sol судит по 10 пачек за сессию, а Claude по 4, разница усталости станет разницей семейств; (3) воспроизводима ли раскладка при пере-прогоне. Обвязка берётся из `judge-prompts/{claude,sol}.md`, которые собраны из ОДНОГО ядра и сверяются гейтом `paritet.py` (норма Д0.13 П-4). Здесь подставляется только список файлов. ⚠ Наборы `p1` и `p2` НИКОГДА не попадают в одну сессию: это разные половины пары пола. Запуск: sessii.py план раскладки sessii.py --write записать готовые промты файлами """ from __future__ import annotations import sys from pathlib import Path ZONE = Path(__file__).resolve().parent PROMPTS = ZONE / "judge-prompts" # ⚠ Ось — параметр. Пачки разных осей НЕЛЬЗЯ класть в один каталог и нельзя отдавать одной # сессии: судья, увидевший обе, перестаёт быть слепым к паре. AXIS = next((a.split("=", 1)[1] for a in sys.argv if a.startswith("--axis=")), "d4") # ⚠ КОРЕНЬ — ТОЖЕ ПАРАМЕТР (заведено 16.08 под заход Д17). У осей фазы раскладка каталогов # `judging/sud-<ось>`, а у захода она на уровень глубже: `judging/z17/<пара>`, потому что пара # там своя у каждой панели. Прежде корень выводился из имени оси, и для захода пришлось бы # заводить ВТОРОЙ такой же скрипт — то есть размножить будущие расхождения раскладки. Флаг # `--root=` даёт тот же механизм чужой раскладке, не копируя его. _ROOT = next((a.split("=", 1)[1] for a in sys.argv if a.startswith("--root=")), "") FAM = { "claude": dict(root=(Path(_ROOT).expanduser() if _ROOT else Path.home() / "books" / "judging" / f"sud-{AXIS}"), tpl=PROMPTS / "claude.md"), "sol": dict(root=Path.home() / "books" / "judging" / f"sol-{AXIS}-work", tpl=PROMPTS / "sol.md"), } if _ROOT: FAM.pop("sol") # внешнее семейство пакет получает своим сборщиком PER_SESSION = 4 # как у харнесса Sol в паке 23; одинаково для обоих семейств MISSING_ONLY = "--missing" in sys.argv def batches(root: Path, half: str) -> list[list[str]]: """Пачки набора, разложенные по сессиям. Порядок — от имени файла, значит воспроизводим. ⚠ `--missing` РАСКЛАДЫВАЕТ ТОЛЬКО НЕОТСУЖЕННОЕ, и это не удобство, а защита данных. Сессия может оборваться на середине (16.08: семь из четырнадцати упали на лимите харнесса, успев записать часть ответов). Пере-запуск ЦЕЛОЙ сессии переписал бы уже полученные оценки новыми — то есть тихо подменил бы данные замера теми, что судья выдал во второй раз. Добор идёт только по пачкам без ответа; уже отсуженное не трогается вовсе. """ toks = sorted(p.stem for p in (root / f"{half}-tasks").glob("*.txt")) if MISSING_ONLY: done = {p.stem for p in (root / f"{half}-answers").glob("*.txt")} toks = [t for t in toks if t not in done] return [toks[i:i + PER_SESSION] for i in range(0, len(toks), PER_SESSION)] # Язык исходника — из провайдера пары, а не из текста шаблона: судья английской пачки, которому # сказано «сверяй с китайским исходником», сверяет не с тем, что видит. LANG = {"d4": "китайском", "d3": "английском", "d6": "японском", "d1": "китайском", "z17-zh": "китайском", "z17-en": "английском"} # ⚠ Творительный падеж — ОТДЕЛЬНАЯ подстановка. Первая редакция просто вычёркивала язык из фразы # «сверяй смысл с китайским исходником», потому что склонение не подставлялось. Проверенная # ревью версия называла язык ДВАЖДЫ, и обезличивание было тихой потерей якоря. LANG_INS = {"d4": "китайским", "d3": "английским", "d6": "японским", "d1": "китайским", "z17-zh": "китайским", "z17-en": "английским"} def render(fam: str, half: str, toks: list[str]) -> str: m = FAM[fam] body = m["tpl"].read_text(encoding="utf-8") body = (body.replace("{ЯЗЫКЕ}", LANG[AXIS]).replace("{КАТАЛОГ}", m["root"].name) .replace("{НАБОР}", half).replace("{ЯЗЫКОМ}", LANG_INS[AXIS])) body = body.replace(f"~/{m['root'].name}/", f"{m['root']}/") lst = "\n".join(f" {m['root']}/{half}-tasks/{t}.txt" for t in toks) return body.replace("{СПИСОК ФАЙЛОВ}", lst) def main() -> int: write = "--write" in sys.argv total = 0 for fam, m in FAM.items(): root = m["root"] if not root.exists(): print(f"{fam}: каталога {root} нет — пропуск") continue out = root / "prompts" if write: out.mkdir(exist_ok=True) print(f"\n=== {fam.upper()} ({root}) ===") n = 0 for half in ("p1", "p2"): for i, toks in enumerate(batches(root, half), 1): n += 1 name = f"{half}-session-{i:02d}.md" print(f" {name:22s} пачек {len(toks)}: {' '.join(toks)}") if write: (out / name).write_text(render(fam, half, toks), encoding="utf-8") total += n print(f" сессий у семейства: {n}" + (f" · записано в {out}" if write else "")) print(f"\nвсего сессий: {total} · по {PER_SESSION} пачки · кап агент-сессий 80") print("⚠ наборы p1 и p2 — разные половины пары пола, одному судье их отдавать НЕЛЬЗЯ") return 0 if __name__ == "__main__": sys.exit(main())