#!/usr/bin/env python3 """ЯПОНСКАЯ НОГА H-4 — слепой проход `J0` против `J6`. $0 (агент-сессии + харнесс владельца). Гипотеза H-4: перевес `deepseek-v4-pro` в редакторской роли есть свойство пары zh→ru, и на другой паре ПОРЯДОК ЖИЛЬЦОВ может смениться. Порядок нельзя увидеть, имея на паре одного редактора: ось Д6 меряла «покупает ли редактор что-нибудь вообще», а не «какой редактор лучше». На zh панель есть (эксп-22), на en второй редактор куплен (`E6`), на ja его не было — требование заказа (:115) печатать сравнение порядка между zh/en/ja поэтому не исполнялось. ⚠ СТАТУС РЕЗУЛЬТАТА — ОПИСАТЕЛЬНЫЙ, и это не смягчается никаким исходом: ось объявлена разведочной ДО денег (`power.FORCED_DESCRIPTIVE`, n=9, MDE 2.90 против цели 2.00). ⚠ ЧЕМУ НАУЧИЛ `tier` И ЧТО ЗДЕСЬ СДЕЛАНО ИНАЧЕ (пре-регистрируется ДО первого ответа): 1. ПОЛОВИНА ПОЛА НЕ ЯВЛЯЕТСЯ БОЕВЫМ АРМОМ. В паке 23 `CTRLfloorA` побайтно равнялся арму `T1` в 16/16 — контроль сравнивал `T1` сам с собой. Здесь пара пола — две генерации БОЕВОГО РЕДАКТОРА (`J0` и `JFLO`), и они РАЗВЕДЕНЫ ПО НАБОРАМ: `J0` живёт в наборе p1, `JFLO` — в p2. Судья никогда не видит обе половины в одной пачке, а порог несёт межсессионную компоненту. 2. ПОЛ СНЯТ С ТОЙ ОПЕРАЦИИ, КОТОРАЯ В ПАНЕЛИ. Пол оси Д6 снят с точечного фиксера — операции почти детерминированной (4 побайтных близнеца из 8 пар). Панель здесь — панель РЕДАКТОРОВ, поэтому и шум мерится повторной генерацией редактора. 3. ДОНОР ДЕКОЯ УРАВНЕН: в паке 23 донором во всех 16 единицах был `R0`, то есть чувствительность сертифицировалась в окрестности одного арма. Здесь донор чередуется `J0`/`J6` по чётности. 4. МАРЖЕВЫЙ ДЕКОЙ ЕСТЬ (норма П-2) — без него «не различимо» неотличимо от слепоты прибора. 5а. ⚠ ОГРАНИЧЕНИЕ ГЕЙТА ЧУВСТВИТЕЛЬНОСТИ ОБЪЯВЛЯЕТСЯ ДО ПРОГОНА: маржевый декой садится лишь на 5 единиц из 9 — регексы посадок не находят на этой книге нужных мест. Расширенный список (`--wide-plants`, который на другой японской выборке давал 8/9) проверен и НЕ помогает: те же 5 из 9. Значит гейт П-2 на этой ноге снят пятью точками, и право говорить «не хуже» он подпирает слабее, чем на осях с полным покрытием. Сказано ДО ответов, а не после. 5. МОЛЧАЛИВЫЙ НОЛЬ ЗАПРЕЩЁН заданием, а рубрика несёт классы, которые пачка `tier` пропускала. 6. КЛЮЧ СВОЙ, соль своя. Ключи осей Д6 и `tier` не трогаются: раскладка меток есть функция соли, uid и НАБОРА армов, и эмиссия поверх переписала бы отсуженную раскладку. ⚠ ПАРИТЕТ ОБВЯЗОК (норма П-4): оба семейства получают ПОБАЙТНО ОДНО задание и одни правила счёта. Расхождение прохода `tier` в 12 раз частично куплено тем, что харнессу Sol дали правило плотности («две одинаковые ошибки — это два»), которого агенты claude не получали. Здесь это правило стоит в САМОМ задании, то есть достаётся обоим. Запуск: ja6.py --emit | --score | --sol | --score-sol | --selftest """ from __future__ import annotations import collections import hashlib import importlib.util import json import re import statistics as st import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") ZONE = Path(__file__).resolve().parent for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"): sys.path.insert(0, str(REPO / "eval" / d)) OUT = Path.home() / "books" / "dovodka" WORK = Path.home() / "books" / "judging" / "ja6" KEYD = OUT / "blind-keys-ja6" AX = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА") CARRY = ("ВЕРНОСТЬ", "ЯЗЫК") SALT = "ja6-2026-08-15" PER_SESSION = 4 SETS = ("p1", "p2") NL = chr(10) # перевод строки как имя: файл собирается генератором внутри f-строк _axre = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE) _whyre = re.compile(r"^[ТT](\d+)-ПОЧЕМУ:(.*)$", re.MULTILINE) def _load(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) sys.modules[name] = mod spec.loader.exec_module(mod) return mod def _sud(): return sys.modules.get("sud") or _load("sud", ZONE / "sud.py") def cell(tag: str) -> str: f = OUT / f"{tag}.json" if not f.exists(): return "" d = json.loads(f.read_text(encoding="utf-8")) return (d.get("content") or "") if d.get("finish") == "stop" else "" def units() -> list[dict]: MJ = sys.modules.get("mat_ja6") or _load("mat_ja6", ZONE / "material_ja.py") return MJ.units() def panel(u: dict, half: str, idx: int) -> dict[str, str]: """Армы набора. `J0` только в p1, `JFLO` только в p2 — половины пола не встречаются в пачке.""" S = _sud() uid = u["uid"] j0, j6, d0 = cell(f"dv-c-j0-{uid}"), cell(f"dv-h-j6-{uid}"), cell(f"dv-c-d0-{uid}") jflo = cell(f"dv-h-jflo-{uid}") base = j0 if half == "p1" else jflo if not (base.strip() and j6.strip() and d0.strip()): return {} donor = j0 if idx % 2 == 0 else j6 # донор декоя уравнен по армам out = {("J0" if half == "p1" else "JFLO"): base, "J6": j6, "D0": d0} dec, n_d = S.AJ._plant(donor) if hasattr(S, "AJ") else (None, 0) if dec and n_d >= 2: out["CTRLdecoy"] = dec mg, n_m = S.margin_plant(donor) if n_m >= 2: out["CTRLmargin"] = mg return out def layout(uid: str, half: str, arms) -> dict[str, str]: order = sorted(arms) seed = hashlib.sha256(f"{SALT}|{uid}|{half}|{'|'.join(order)}".encode()).hexdigest() rank = sorted(order, key=lambda a: hashlib.sha256(f"{seed}|{a}".encode()).hexdigest()) return {f"Т{i}": a for i, a in enumerate(rank, 1)} HEAD = """Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на японском и {n} независимых русских перевода этого же фрагмента под слепыми метками. Посчитай ОШИБКИ в каждом варианте по типам: ВЕРНОСТЬ — сказано не то, что в исходнике: искажение, инверсия, выдумка, пропуск факта. Сюда же входят, и их пропускают чаще всего: · инверсия адресата или участника реплики (кто кому говорит, кто кого бьёт); · потеря или подмена разряда, ранга, числа, единицы измерения; · потеря вежливостного или social-регистра там, где он несёт смысл; · состояние действия: совершилось против ещё не совершилось; · снятое или, наоборот, добавленное отрицание. ТЕРМИН — имя или термин передан не так, как в других местах того же текста. ЯЗЫК — то, чего носитель не напишет: калька, канцелярит, несуществующее слово, кривой порядок слов, рассогласование. ФОРМА — вёрстка и пунктуация: оформление реплик, кавычки, тире, абзацы. Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника — не сравнивая варианты между собой. Порядок вариантов ничего не значит и специально перемешан. Числа — это СЧЁТ ошибок, а не оценка по шкале. Две одинаковые ошибки в одном варианте — это два. ⚠ НОЛЬ — ЭТО УТВЕРЖДЕНИЕ, А НЕ ОТСУТСТВИЕ ОТВЕТА. Ноль означает «я сверил этот вариант с исходником по этой оси и дефектов не нашёл». Поэтому ПОЧЕМУ обязательно для КАЖДОЙ метки, в том числе при нулях: если по всем осям ноль, напиши «прочитано, дефектов не найдено». Пустое ПОЧЕМУ не принимается и на диск не попадает. Ответ ЗАПИШИ ФАЙЛОМ в {path} — ровно в таком виде и ничем больше, блок на каждую метку: {skeleton} ПОЧЕМУ для НЕНУЛЕВОЙ оси: короткая ЦИТАТА из текста через « | ». Число без цитаты не принимается и на диск не попадает. """ def emit() -> None: key: dict[str, dict] = {"_проход": "ja6", "_семейство": ["J6/J0"], "_контроль": ["J0/D0"], "_пол": ["J0(p1) против JFLO(p2)"]} made = 0 for half in SETS: (WORK / f"{half}-tasks").mkdir(parents=True, exist_ok=True) (WORK / f"{half}-answers").mkdir(parents=True, exist_ok=True) for idx, u in enumerate(units()): for half in SETS: texts = panel(u, half, idx) if not texts: print(f" ⚠ {u['uid']}/{half}: панель неполна, пропущено") continue lay = layout(u["uid"], half, tuple(texts)) tok = hashlib.sha256(f"{SALT}|{u['uid']}|{half}".encode()).hexdigest()[:10] skel = "\n\n".join("\n".join(f"{lab}-{a}: <число>" for a in AX) + f"\n{lab}-ПОЧЕМУ: <цитаты>" for lab in lay) body = [HEAD.format(n=len(lay), path=WORK / f"{half}-answers" / f"{tok}.txt", skeleton=skel), "", "=" * 60, "ИСХОДНИК:", u["source"]] for lab, arm in lay.items(): body += ["", "=" * 60, f"{lab}:", texts[arm]] (WORK / f"{half}-tasks" / f"{tok}.txt").write_text("\n".join(body), encoding="utf-8") key[tok] = {lab: {"arm": a, "uid": u["uid"], "half": half, "kind": "контроль" if a.startswith("CTRL") else "боевой", "sig": hashlib.sha256(texts[a].encode()).hexdigest()[:12]} for lab, a in lay.items()} made += 1 KEYD.mkdir(parents=True, exist_ok=True) (KEYD / "ja6-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8") (KEYD / "SALT-ja6.txt").write_text(SALT + "\n", encoding="utf-8") toks = [t for t in key if not t.startswith("_")] print(f"заданий {made} → {WORK}/{{p1,p2}}-tasks") print(f"ключ → {KEYD} (судье НЕ давать)") for i in range(0, len(toks), PER_SESSION): print(f" сессия {i // PER_SESSION + 1}: " + " · ".join(toks[i:i + PER_SESSION])) def read(root: Path) -> tuple[dict, list]: key = json.loads((KEYD / "ja6-KEY.json").read_text(encoding="utf-8")) out, bad = {}, [] # ⚠ Две раскладки ответов, и обе законны: своё семейство пишет в `p1-answers`/`p2-answers` # (наборы разведены каталогами), внешний харнесс — в ПЛОСКИЙ `answers/`, потому что его # разводит не каталог, а оркестраторский промт «по одному агенту на промт». Счётчик обязан # читать обе: первая редакция знала только про первую и на пакете Sol печатала «ответов нет». dirs = [root / f"{half}-answers" for half in SETS] if (root / "answers").exists(): dirs.append(root / "answers") for d in dirs: if not d.exists(): continue for f in sorted(d.glob("*.txt")): km = key.get(f.stem) if not isinstance(km, dict): continue t = f.read_text(encoding="utf-8", errors="replace") cells: dict[str, dict[str, int]] = collections.defaultdict(dict) for m in _axre.finditer(t): cells["Т" + m.group(1)][m.group(2)] = int(m.group(3)) why = {"Т" + m.group(1): m.group(2).strip() for m in _whyre.finditer(t)} for lab, ax in cells.items(): meta = km.get(lab) if not isinstance(meta, dict): continue if len(ax) < len(AX): bad.append(f"{f.stem}/{lab}: не все четыре оси") continue if not why.get(lab): bad.append(f"{f.stem}/{lab}: ПУСТОЕ обоснование (запрещено заданием)") out[(meta["uid"], meta["arm"])] = sum(ax.get(a, 0) for a in CARRY) return out, bad def _sign_p(diffs: list[float]) -> float: import math nz = [d for d in diffs if d != 0] n = len(nz) if not n: return 1.0 k = sum(1 for d in nz if d > 0) tail = sum(math.comb(n, i) for i in range(min(k, n - k) + 1)) / 2 ** n return min(1.0, 2 * tail) def score(root: Path | None = None) -> int: root = root or WORK sc, bad = read(root) if not sc: print(f"ответов в {root} нет") return 1 uids = sorted({u for u, _ in sc}) print(f"единиц {len(uids)} · клеток {len(sc)} · замечаний годности {len(bad)}") for b in bad[:8]: print(" ⚠", b) vals = [v for (u, a), v in sc.items() if not a.startswith("CTRL")] zeros = sum(1 for v in vals if v == 0) / max(len(vals), 1) print(f"\nГЕЙТ ПЛОТНОСТИ: ошибок/клетку {st.mean(vals):.2f} · доля нулей {zeros:.0%} " f"→ {'ГОДНО' if zeros < 0.25 else '⛔ ПОЛ ШКАЛЫ'}") fl = [sc[(u, "J0")] - sc[(u, "JFLO")] for u in uids if (u, "J0") in sc and (u, "JFLO") in sc] sd = st.pstdev(fl) if len(fl) > 1 else 0.0 thr = 2.8 * sd / max(len(fl), 1) ** 0.5 if fl else 0.0 print(f"СВОЙ ПОЛ (две генерации редактора, половины в РАЗНЫХ наборах): пар {len(fl)} · " f"sd {sd:.2f} → ПОРОГ {thr:.2f}") for name, arm in (("ГРУБЫЙ ДЕКОЙ", "CTRLdecoy"), ("МАРЖЕВЫЙ ДЕКОЙ (гейт П-2)", "CTRLmargin")): d = [sc[(u, arm)] - sc[(u, "J0")] for u in uids if (u, arm) in sc and (u, "J0") in sc] if d: v = "ПРОЙДЕН" if st.mean(d) > thr else "⛔ НЕ ПРОЙДЕН" print(f"{name}: n={len(d)} среднее {st.mean(d):+.2f} против порога {thr:.2f} → {v}") print(f"\n{'контраст':14s}{'ед.':>5s}{'перевес':>9s}{'p':>9s} вердикт") for a, b in (("J6", "J0"), ("J0", "D0")): d = [sc[(u, b)] - sc[(u, a)] for u in uids if (u, a) in sc and (u, b) in sc] if not d: continue m = st.mean(d) v = "ПЕРЕШЁЛ ПОРОГ (описательно)" if abs(m) > thr else "ниже порога" print(f"{a + ' vs ' + b:14s}{len(d):5d}{m:+9.2f}{_sign_p(d):9.4f} {v}") print(f"\n{'арм':10s}{'ошибок/ед.':>11s}") for arm in ("J0", "J6", "D0", "JFLO", "CTRLdecoy", "CTRLmargin"): v = [sc[(u, arm)] for u in uids if (u, arm) in sc] if v: print(f"{arm:10s}{st.mean(v):11.2f}") print("\n⚠ Ось РАЗВЕДОЧНАЯ: вывод описательный при любом исходе (n=9, MDE 2.90 против цели 2.00).") return 0 ORCH = """# Sol-оркестратору: судейство ЯПОНСКОЙ НОГИ H-4 (проход `ja6`) Ты раскладываешь готовые судейские задания по своим агентам. **Ничего не готовишь сам и ничего не считаешь** — всё уже собрано, задания и промты лежат файлами. ## Что сделать В каталоге `{prompts}` лежат **{k} промтов сессий**: {names} Запусти **по одному агенту на каждый промт**, {k} агентов. Каждому дай ровно одну инструкцию: > Прочитай файл `{prompts}/<имя>.md` и выполни ровно то, что в нём написано, ничего сверх. Промт внутри содержит список назначенных заданий, правила оценки и путь, куда писать ответ. Больше агенту знать ничего не нужно. ## Жёсткие требования (нарушение обесценивает замер) 1. **Один агент — один промт.** Не объединяй сессии и не дроби их. Состав сессии — часть прибора. 2. ⚠⚠ **Наборы `p1` и `p2` — РАЗНЫМ агентам, и это здесь важнее всего.** Это две половины шумового пола: в `p1` лежит одна генерация боевого редактора, в `p2` — вторая генерация ТОГО ЖЕ редактора. Из разницы их оценок и строится порог различимости всего прохода. Судья, увидевший обе половины, порог обесценивает целиком — в паке 23 такой дефект занизил его на 19%, а на проходе `tier` половина пола вообще совпала с боевым армом, и контроль оказался пуст. **Если все задания уйдут одному агенту, замер пропадает, и пере-снять его будет нечем.** 3. **Агент открывает ТОЛЬКО перечисленные в его промте задания и пишет ТОЛЬКО по путям, указанным ВНУТРИ заданий.** Ему нельзя искать что-либо ещё на диске — ни разметку, ни сырьё, ни код, ни задания других сессий; нельзя делать сетевые вызовы; нельзя сравнивать варианты между собой, ранжировать их и применять `diff`/`difflib`/`cmp`. Ключ соответствия меток и армов лежит ВНЕ рабочего каталога и агенту не выдаётся намеренно. Заглянет — сессия аннулируется целиком. 4. **Не правь промты и задания.** Если что-то выглядит ошибкой — скажи владельцу, но не исправляй: обвязка сверена гейтом паритета с обвязкой второго семейства, и правка на твоей стороне делает семейства несравнимыми. 5. **Не досуживай и не переоценивай.** Не справился с частью — так и скажи, сколько выполнено. Частичный возврат честнее выдуманного и засчитывается. 6. **Агент НЕ запускает своих агентов.** Сессия — это ОДИН агент, читающий свои задания. Дочерние агенты ломают состав сессии, зарегистрированный до запуска; на прошлом прогоне это уже случилось, и сессию пришлось аннулировать целиком. ## Что это за замер и почему спрашивают вас Гипотеза владельца H-4: перевес `deepseek-v4-pro` в роли редактора — свойство пары zh→ru, и на другой паре порядок редакторов может смениться. На японском до этой недели был куплен один редактор, сравнивать было не с чем; теперь их два. Вопрос ровно один: **меняется ли порядок**. Ваше семейство здесь полноправно: ни один арм панели не из OpenAI (`deepseek-v4-pro`, `glm-5`, `deepseek-v4-flash`). На соседнем проходе `tier` конфликт был — там судился `gpt-5.6-terra`, то есть выход вашей же семьи, и показания пришлось снять именно поэтому. Первое семейство (агенты Claude) эту панель уже отсудило, все контроли зелёные. Вы — второе мнение о ЕГО судействе и его слепоте, а не первый свидетель. ## Как считается результат eval/.venv/bin/python eval/dovodka/ja6.py --score-sol Тот же счётчик, которым посчитано первое семейство: сам отбракует ответы без цитат, проверит пойманный декой и напечатает число принятых единиц. ## Что этот замер может и чего не может — сказано ДО прогона Ось объявлена РАЗВЕДОЧНОЙ до денег: 9 единиц, MDE 2.90 против цели 2.00. Несущего вывода она не даст ни при каком исходе. Маржевый декой (контроль чувствительности) сажается лишь на 5 единиц из 9 — на этой книге регексы посадок находят мало мест, расширенный список проверен и не помогает. Значит право сказать «редакторы равны» подпёрто здесь слабее, чем на других осях. """ SESSION = """ Ты — литературный редактор-эксперт. Твоя работа — оценить качество русских художественных переводов С ЯПОНСКОГО по заданиям, которые лежат готовыми файлами. Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом задании есть исходник на японском, несколько русских переводов этого же фрагмента под слепыми метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой. Работай по заданиям последовательно: прочитал задание → записал ответ → перешёл к следующему. ## Правила, которые важнее скорости * **Оценивай каждый вариант ОТДЕЛЬНО против исходника.** Не сравнивай варианты между собой, не ранжируй их и не подстраивай оценку одного под оценку другого. Порядок меток перемешан намеренно. * **Каждая ненулевая оценка требует обоснования ЦИТАТОЙ** из соответствующего варианта, в формате, заданном в задании. Оценка без цитаты не принимается, и единица целиком выбрасывается из замера. * **Числа — это СЧЁТ ошибок, а не оценка по шкале.** Две одинаковые ошибки в одном варианте — два. * ⚠ **НОЛЬ — УТВЕРЖДЕНИЕ, А НЕ МОЛЧАНИЕ.** Ставя ноль, ты заявляешь «я сверил и не нашёл». Поле ПОЧЕМУ обязательно для КАЖДОЙ метки, включая нулевые: при всех нулях так и пиши — «прочитано, дефектов не найдено». Пустое ПОЧЕМУ разбор отбрасывает. Прошлый проход развалился именно на молчаливых нулях: 38% клеток были нулями, треть из них без единого слова, и вердикт оказался не «варианты равны», а «прибор ничего не показывал». * Читай ВНИМАТЕЛЬНО и сверяй смысл с японским исходником. Смысловые искажения — инверсия, подмена адресата реплики, потерянное отрицание, изменённое число, потерянный вежливостный регистр, выдуманный факт — незаметны при беглом чтении, потому что русский текст остаётся грамотным и связным. Именно их и надо ловить. * Не жалей вариант и не ищи, за что его похвалить: твоя задача — СЧЁТ ОШИБОК, а не рецензия. ## Границы * Работай ТОЛЬКО с назначенными файлами заданий и пиши ТОЛЬКО по путям, указанным внутри них. * **Не ищи и не открывай ничего другого на диске.** В частности: не выясняй, откуда взялся вариант, какой моделью он сделан и что означают метки. Эта информация к задаче отношения не имеет, а её поиск делает оценку недействительной. * Не применяй `diff`, `difflib`, `cmp` и любое механическое сличение вариантов между собой. * **Не запускай своих агентов.** Эта сессия — ты один. * Не переводи сам и не предлагай свою версию. Не оценивай «литературность в целом» — только оси, названные в задании. * Книга новая (первая публикация 30.07.2026), в претрейне её нет: не ищи её в сети и не опирайся на память. Единственный источник истины — исходник в задании. * Ничего не покупай, никаких сетевых вызовов. Файлы с ключами и секретами не читать. ЕСЛИ ЧЕГО-ТО НЕ ЗНАЕШЬ: считай ошибкой только то, что видно из исходника. Сомневаешься — не засчитывай. Заниженный счёт честнее выдуманного. ## ТВОИ ЗАДАНИЯ ({n}) {files} """ def sol() -> None: """Пакет для внешнего харнесса — ТРЁХУРОВНЕВЫЙ, как пакеты фазы Д, а не двухдокументный. ⚠ ПОЧЕМУ ИМЕННО ТАК, И ЧЕМ ЭТО ОПЛАЧЕНО. Первая редакция этого пакета копировала структуру эксп-23 (`editor-tier/sol-tier/`: ИНСТРУКЦИЯ + ПРОМТ-ДЛЯ-ХАРНЕССА) и просила «работай по 4 задания за сессию» ПРОЗОЙ. Владелец указал, что его харнесс так не работает: получив задание, Sol идёт и читает всё сам, одной сессией. Для этого прохода такая раскладка убийственна — половины шумового пола лежат в РАЗНЫХ наборах (`J0` в p1, `JFLO` в p2), и один судья, увидевший обе, обесценивает порог целиком. Пакеты фазы Д (`sol-d3-work`, `sol-d4-work`, `sol-d6-work`) решают это слоем ОРКЕСТРАТОРА: `ORCHESTRATOR.md` велит запустить по агенту на промт, а `prompts/*.md` жёстко разводят наборы. Здесь воспроизводится тот же слой. ⚠ Прочее, учтённое из прошлых пакетов, каждое — оплаченная ошибка: · путь к ключу НЕ НАЗЫВАЕТСЯ (пак 23 написал «не открывай blind-keys/» — это показать пальцем); · путь записи ведёт в СВОЙ каталог (пак 23 велел писать в боевой, поверх отсуженного); · правила счёта стоят В САМОМ ЗАДАНИИ и в промте сессии — паритет П-4; · запрет на дочерних агентов вписан явно (на прошлом прогоне сессия их породила); · все армы единицы — в одной пачке (урок эксп-22 §16). """ src, dst = WORK, Path.home() / "books" / "judging" / "ja6-sol" (dst / "tasks").mkdir(parents=True, exist_ok=True) (dst / "answers").mkdir(parents=True, exist_ok=True) (dst / "prompts").mkdir(parents=True, exist_ok=True) by_half: dict[str, list[str]] = {} for half in SETS: d = src / f"{half}-tasks" if not d.exists(): continue for f in sorted(d.glob("*.txt")): body = f.read_text(encoding="utf-8") body = body.replace(str(src / f"{half}-answers" / f.name), str(dst / "answers" / f.name)) (dst / "tasks" / f.name).write_text(body, encoding="utf-8") by_half.setdefault(half, []).append(f.name) if not by_half: print("заданий нет — сначала --emit") return names = [] for half, toks in sorted(by_half.items()): for i in range(0, len(toks), PER_SESSION): grp = toks[i:i + PER_SESSION] nm = f"{half}-session-{i // PER_SESSION + 1:02d}" files = NL.join(f"* `{dst / 'tasks' / x}`" for x in grp) (dst / "prompts" / f"{nm}.md").write_text( SESSION.format(n=len(grp), files=files), encoding="utf-8") names.append(nm) (dst / "ORCHESTRATOR.md").write_text( ORCH.format(prompts=dst / "prompts", k=len(names), names=NL.join(f"* `{n}.md`" for n in names)), encoding="utf-8") print(f"пакет Sol → {dst}") print(f" заданий {sum(len(v) for v in by_half.values())} · промтов сессий {len(names)}") print(f" ОТДАТЬ ВЛАДЕЛЬЦУ: {dst / 'ORCHESTRATOR.md'}") print(" ⚠ p1 и p2 — РАЗНЫМ агентам: это две половины шумового пола") def selftest() -> int: fails = [] ok = lambda c, m: fails.append(m) if not c else None us = units() ok(len(us) == 9, f"единиц {len(us)}, ожидалось 9") for half in SETS: p = panel(us[0], half, 0) ok(bool(p), f"{half}: панель пуста") if p: sigs = [hashlib.sha256(v.encode()).hexdigest() for v in p.values()] ok(len(sigs) == len(set(sigs)), f"{half}: в панели побайтные дубли") ok(("J0" in p) == (half == "p1"), f"{half}: J0 стоит не в своём наборе") ok(("JFLO" in p) == (half == "p2"), f"{half}: JFLO стоит не в своём наборе") lay = layout(us[0]["uid"], half, tuple(p)) ok(len(lay) == len(set(lay.values())), "раскладка не биективна") ok(layout(us[0]["uid"], half, tuple(sorted(lay.values()))) == lay, "раскладка не воспроизводима") d = [panel(u, "p1", i).get("CTRLdecoy", "") for i, u in enumerate(us)] ok(sum(1 for x in d if x) >= 7, f"грубый декой сажается лишь на {sum(1 for x in d if x)} из 9") ok(_sign_p([1, 1, 1, 1, 1]) < 0.07, "знаковый тест сломан") for m in fails: print("ПРОВАЛ:", m) print(f"селфтест ja6: провалов {len(fails)}") return 1 if fails else 0 if __name__ == "__main__": a = sys.argv[1:] or ["--selftest"] if a[0] == "--emit": emit() elif a[0] == "--score": sys.exit(score()) elif a[0] == "--selftest": sys.exit(selftest()) elif a[0] == "--sol": sol() elif a[0] == "--score-sol": sys.exit(score(Path.home() / "books" / "judging" / "ja6-sol")) else: raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")