textmachine/eval/dovodka/ja6.py

505 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 = """<!-- ОБВЯЗКА СЕМЕЙСТВА SOL. Отличается от обвязки другого семейства ТОЛЬКО каталогом
заданий и ответов. Паритет обвязок — норма Д0.13 П-4: в паке 23 у харнесса Sol в промте были
правила плотности счёта, которых не было у Claude, и часть расхождения семейств в разы могла
быть куплена ИНСТРУКЦИЕЙ, а не моделью. -->
Ты — литературный редактор-эксперт. Твоя работа — оценить качество русских художественных
переводов С ЯПОНСКОГО по заданиям, которые лежат готовыми файлами.
Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом
задании есть исходник на японском, несколько русских переводов этого же фрагмента под слепыми
метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан
ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой.
Работай по заданиям последовательно: прочитал задание → записал ответ → перешёл к следующему.
## Правила, которые важнее скорости
* **Оценивай каждый вариант ОТДЕЛЬНО против исходника.** Не сравнивай варианты между собой, не
ранжируй их и не подстраивай оценку одного под оценку другого. Порядок меток перемешан намеренно.
* **Каждая ненулевая оценка требует обоснования ЦИТАТОЙ** из соответствующего варианта, в формате,
заданном в задании. Оценка без цитаты не принимается, и единица целиком выбрасывается из замера.
* **Числа — это СЧЁТ ошибок, а не оценка по шкале.** Две одинаковые ошибки в одном варианте — два.
* ⚠ **НОЛЬ — УТВЕРЖДЕНИЕ, А НЕ МОЛЧАНИЕ.** Ставя ноль, ты заявляешь «я сверил и не нашёл». Поле
ПОЧЕМУ обязательно для КАЖДОЙ метки, включая нулевые: при всех нулях так и пиши — «прочитано,
дефектов не найдено». Пустое ПОЧЕМУ разбор отбрасывает. Прошлый проход развалился именно на
молчаливых нулях: 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}")