505 lines
36 KiB
Python
505 lines
36 KiB
Python
#!/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}")
|