Fix eight defects found by the cross-family adversarial review before any judging answer exists
This commit is contained in:
parent
2579cb82ae
commit
2087237877
5 changed files with 1114 additions and 36 deletions
374
eval/dovodka/chtenie.py
Normal file
374
eval/dovodka/chtenie.py
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ПРИБОР ТКАНИ — слепое ранжирование читаемости. $0.
|
||||
|
||||
⚠ ЗАЧЕМ. Весь риг 19→23 считает ЛОКАЛЬНЫЕ ошибки внутри чанка и по построению не видит того, что
|
||||
владелец назвал целью №1: победы над translationese. Пре-рег П-6 завёл под это слепое ранжирование
|
||||
издательской пригодности, владелец 11.08 сказал «берём» — и оно не было построено.
|
||||
|
||||
Файл прошёл две редакции, и обе живут здесь, а не в двух файлах:
|
||||
1. КАЛИБРОВКА 16.08 — одна единица на ось, без контролей. Читал Fable по поручению владельца.
|
||||
Дала результат, ради которого всё дальнейшее и делается: на английской оси Спирмен между
|
||||
счётом ошибок и читаемостью **−0.10**, связи нет. Улики: `~/books/chtenie-vladeltsa/ОТВЕТ-*.md`,
|
||||
ключ `~/books/dovodka/blind-keys-chtenie/`. Пере-считать: `--score-calib`.
|
||||
2. ПОЛНАЯ ПАНЕЛЬ захода Д17 — выборка единиц, ДВА независимых читателя и контроли, которых у
|
||||
калибровки не было:
|
||||
· КОНТРОЛЬ ДЕКОЯ: намеренно испорченный вариант обязан ранжироваться последним;
|
||||
· КОНТРОЛЬ ПОЛА: две генерации ОДНОГО редактора (`Z0`/`ZF`) обязаны оказаться СОСЕДЯМИ —
|
||||
развёл далеко, значит порядок читателя есть шум, и «победа» внутри этого расстояния
|
||||
ничего не значит. Это для чтения то же, чем шумовой пол служит счёту.
|
||||
|
||||
⚠ ЧЕГО ПРИБОР НЕ МОЖЕТ. Абсолютной оценки качества он не даёт и читателя-человека не заменяет:
|
||||
читают модели. Он отвечает на ОДИН вопрос — совпадает ли порядок по читаемости с порядком по
|
||||
счёту ошибок, — и потому решает судьбу СЧЁТА как дешёвого прокси, а не судьбу армов.
|
||||
|
||||
Запуск: chtenie.py --emit | --score | --selftest | --score-calib
|
||||
"""
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
ZS = _load("zsud_c", ZONE / "zsud.py")
|
||||
Z = ZS.Z
|
||||
|
||||
WORK = Path.home() / "books" / "chtenie-z17"
|
||||
KEYD = Path.home() / "books" / "dovodka" / "blind-keys-chtenie-z17"
|
||||
CALIB = Path.home() / "books" / "chtenie-vladeltsa"
|
||||
CALIB_KEY = Path.home() / "books" / "dovodka" / "blind-keys-chtenie" / "chtenie-KEY.json"
|
||||
SALT = "chtenie-z17-2026-08-16"
|
||||
N_UNITS = 8 # на ось; больше читатель за раз честно не осилит
|
||||
READERS = ("A", "B") # два независимых читателя РАЗНЫХ семейств
|
||||
# Единиц в задании: китайская глава ~9.7 тыс. знаков × 7 вариантов = 68 тыс. на единицу,
|
||||
# английская ~1.9 тыс. × 7 = 13 тыс. Поэтому китайские читаются по одной, английские — по четыре.
|
||||
PER_TASK = {"zh": 1, "en": 4}
|
||||
PANEL = ("ZD", "Z0", "ZF", "ZP", "Z8R", "Z1")
|
||||
DECOY = "CTRLdecoy"
|
||||
|
||||
HEAD = """# Слепое чтение — {pair}, задание {k}
|
||||
|
||||
## Что это и зачем
|
||||
|
||||
Наш измерительный аппарат считает ЛОКАЛЬНЫЕ ОШИБКИ: искажения смысла, кальки, сбитую вёрстку —
|
||||
штука за штукой, внутри отрывка. Это дёшево и воспроизводимо, но у такого прибора есть встроенная
|
||||
слепота: **текст может быть без единой ошибки и при этом мёртвый**. Серая гладкая проза получит
|
||||
«претензий нет», а читатель закроет книгу.
|
||||
|
||||
Поэтому здесь спрашивают ЧИТАТЕЛЯ. Ниже — один и тот же отрывок в нескольких вариантах перевода,
|
||||
сделанных разными способами. Способы не названы намеренно: метки перемешаны, соответствие
|
||||
«метка → способ» лежит в отдельном файле и до твоего ответа не открывается.
|
||||
|
||||
Твой ответ решает не «какой способ лучше» — он решает, **совпадает ли человеческий порядок с тем,
|
||||
что печатает наш счётчик ошибок**. Совпал — счёт можно и дальше использовать как дешёвую замену
|
||||
чтению. Разошёлся — значит все выводы мерили не то.
|
||||
|
||||
## Как читать
|
||||
|
||||
Читай как ЧИТАТЕЛЬ, а не как корректор. Вопрос один: **какой текст ты взял бы в книгу**.
|
||||
Главный критерий — живая русская проза против переводного канцелярита. Смысловые ошибки, если
|
||||
заметишь, называй, но ранжируй по читаемости.
|
||||
|
||||
⚠ **Три вещи, которые могут тебя сбить, — говорю заранее.**
|
||||
|
||||
1. **Вёрстка выдаёт метод.** Часть вариантов прошла полное переписывание, а оно обязано сливать
|
||||
построчный исходник в русские абзацы; остальные вёрстку исходника сохраняют. Разницу видно ДО
|
||||
чтения — не выводи из неё, «какой это способ», и не давай ей решать за тебя.
|
||||
2. **Не угадывай гипотезы.** Не пытайся понять, каким способом сделан вариант, и не проверяй по
|
||||
ходу, совпало ли с ожиданием. Запиши порядок ДО любых рассуждений о методах.
|
||||
3. **Похожие варианты — это нормально.** Часть вариантов может оказаться очень близка друг к
|
||||
другу. Не различаешь — так и напиши через «=», это законный и ценный ответ.
|
||||
|
||||
## Что записать
|
||||
|
||||
Ответ — в файл `{answer}`. На КАЖДЫЙ отрывок обязательно две вещи:
|
||||
|
||||
1. Строка `ПОРЯДОК <номер отрывка>: Т3 > Т1 > Т5 > Т2 > Т4 > Т6 > Т7` — от лучшего к худшему.
|
||||
Неразличимые варианты соединяй знаком `=`.
|
||||
2. По одной-две фразы на вариант: почему. Годится и «серо, но гладко», и «живой ритм, но во
|
||||
втором абзаце потерян смысл».
|
||||
|
||||
Если какой-то вариант покажется откровенно машинным — скажи прямо, это самая ценная информация.
|
||||
|
||||
## Границы
|
||||
|
||||
* Не открывай на диске ничего, кроме этого задания, и не пиши никуда, кроме указанного файла.
|
||||
* Не выясняй происхождение вариантов; не применяй `diff`, `difflib`, `cmp` и механическое сличение.
|
||||
* Не запускай своих агентов: сессия — это ты один.
|
||||
* Не переводи сам и не предлагай свою версию.
|
||||
"""
|
||||
|
||||
|
||||
def pick(pair: str) -> list[str]:
|
||||
"""N_UNITS единиц из ОТСУЖЕННОГО набора — чтобы порядки читателя и счётчика сравнивались
|
||||
на одних и тех же текстах. Отбор детерминирован своей солью."""
|
||||
uids = sorted(u["uid"] for u in Z.chosen(pair))
|
||||
return sorted(uids, key=lambda u: hashlib.sha256(f"{SALT}|{pair}|{u}".encode()).hexdigest()
|
||||
)[:N_UNITS]
|
||||
|
||||
|
||||
def variants(pair: str, tx: dict[str, str]) -> dict[str, str]:
|
||||
"""⚠ ПОЛНАЯ ПАНЕЛЬ ИЛИ НИЧЕГО (починка по ревью). Прежняя редакция отдавала читателю столько
|
||||
вариантов, сколько куплено, — а средние ранги, снятые на пятёрке и на восьмёрке вариантов,
|
||||
несравнимы между собой (ранг 1..5 против 1..8). Неполная единица просто не эмитируется."""
|
||||
if not all(tx.get(a, "").strip() for a in PANEL):
|
||||
return {}
|
||||
out = {a: tx[a] for a in PANEL}
|
||||
dec, n = ZS.AJ._plant(tx.get("Z0", ""))
|
||||
if n >= ZS.MARGIN_MIN:
|
||||
out[DECOY] = dec
|
||||
return out
|
||||
|
||||
|
||||
def layout(pair: str, uid: str, reader: str, arms) -> dict[str, str]:
|
||||
"""Своя раскладка КАЖДОМУ читателю: одинаковая давала бы им общий позиционный наклон."""
|
||||
order = sorted(arms)
|
||||
seed = hashlib.sha256(f"{SALT}|{pair}|{uid}|{reader}|{'|'.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)}
|
||||
|
||||
|
||||
def emit() -> None:
|
||||
key: dict = {"_что": "слепое чтение по полной панели захода Д17 (П-6)", "_соль": SALT,
|
||||
"_контроли": ["декой обязан быть последним", "Z0 и ZF обязаны быть соседями"]}
|
||||
made = 0
|
||||
for pair in Z.PAIRS:
|
||||
tx_all = ZS.texts_of(pair)
|
||||
uids = [u for u in pick(pair) if variants(pair, tx_all[u])]
|
||||
for reader in READERS:
|
||||
d = WORK / pair / reader
|
||||
(d / "tasks").mkdir(parents=True, exist_ok=True)
|
||||
(d / "answers").mkdir(parents=True, exist_ok=True)
|
||||
step = PER_TASK[pair]
|
||||
for k in range(0, len(uids), step):
|
||||
grp = uids[k:k + step]
|
||||
tok = hashlib.sha256(f"{SALT}|{pair}|{reader}|{grp[0]}".encode()).hexdigest()[:10]
|
||||
ans = d / "answers" / f"{tok}.md"
|
||||
body = [HEAD.format(pair=("китайская ось" if pair == "zh" else "английская ось"),
|
||||
k=k // step + 1, answer=ans)]
|
||||
for j, uid in enumerate(grp, 1):
|
||||
vs = variants(pair, tx_all[uid])
|
||||
lay = layout(pair, uid, reader, tuple(vs))
|
||||
body += ["", "=" * 70, f"## ОТРЫВОК {j} — ИСХОДНИК", "",
|
||||
tx_all[uid]["_src"], ""]
|
||||
for lab, arm in lay.items():
|
||||
body += ["-" * 70, f"### ОТРЫВОК {j}, вариант {lab}", "", vs[arm], ""]
|
||||
key.setdefault(tok, {})[str(j)] = {
|
||||
"uid": uid, "pair": pair, "reader": reader, "метки": lay,
|
||||
"знаков": {a: len(t) for a, t in vs.items()}}
|
||||
(d / "tasks" / f"{tok}.md").write_text("\n".join(body), encoding="utf-8")
|
||||
made += 1
|
||||
KEYD.mkdir(parents=True, exist_ok=True)
|
||||
(KEYD / "chtenie-z17-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
|
||||
encoding="utf-8")
|
||||
print(f"заданий {made} → {WORK}/<пара>/<читатель>/tasks")
|
||||
print(f"ключ → {KEYD} (читателю НЕ давать)")
|
||||
|
||||
|
||||
# ⚠ Латинская `T` принимается наравне с кириллической `Т` (починка по ревью): судьи уже писали
|
||||
# латиницей, а `zsud`/`ja6` это учитывают. Молча выпавший ответ читателя стоил бы целой сессии.
|
||||
_ORD = re.compile(r"ПОРЯДОК\s*(\d+)\s*:\s*((?:[ТT]\d+\s*[>=]\s*)+[ТT]\d+)", re.IGNORECASE)
|
||||
|
||||
|
||||
def _ranks(order: str) -> dict[str, float]:
|
||||
"""Метка → ранг (1 = лучший). Связанные через `=` получают средний ранг."""
|
||||
out, pos = {}, 1
|
||||
for g in [x.strip() for x in re.split(r">", order)]:
|
||||
labs = [x.replace("T", "Т") for x in re.findall(r"[ТT]\d+", g)]
|
||||
for x in labs:
|
||||
out[x] = pos + (len(labs) - 1) / 2
|
||||
pos += len(labs)
|
||||
return out
|
||||
|
||||
|
||||
def _rank(v: list[float]) -> list[float]:
|
||||
"""Ранги со средним для связок — как `scipy.rankdata`, сверено с ним."""
|
||||
order = sorted(range(len(v)), key=lambda i: v[i])
|
||||
out = [0.0] * len(v)
|
||||
i = 0
|
||||
while i < len(order):
|
||||
j = i
|
||||
while j + 1 < len(order) and v[order[j + 1]] == v[order[i]]:
|
||||
j += 1
|
||||
r = (i + j) / 2 + 1
|
||||
for k in range(i, j + 1):
|
||||
out[order[k]] = r
|
||||
i = j + 1
|
||||
return out
|
||||
|
||||
|
||||
def _spearman(a: list[float], b: list[float]) -> float:
|
||||
"""⚠ ПОЧИНКА ПО РЕВЬЮ: прежняя редакция называлась Спирменом, а считала ПИРСОНА по входам.
|
||||
Там, где на вход шли СРЕДНИЕ ранги и СРЕДНИЕ счёты ошибок (решающий гейт «следит ли счёт за
|
||||
читаемостью»), это давало другое число: на демо-входах своё +0.79 против настоящего +1.00.
|
||||
Гейт `rho > 0.5` — по пре-регу Д17 решающий эндпойнт, и он не имеет права быть приблизительным.
|
||||
"""
|
||||
if len(a) < 3:
|
||||
return 0.0
|
||||
a, b = _rank(a), _rank(b)
|
||||
ma, mb = st.mean(a), st.mean(b)
|
||||
num = sum((x - ma) * (y - mb) for x, y in zip(a, b))
|
||||
da = sum((x - ma) ** 2 for x in a) ** 0.5
|
||||
db = sum((y - mb) ** 2 for y in b) ** 0.5
|
||||
return num / (da * db) if da and db else 0.0
|
||||
|
||||
|
||||
def score() -> int:
|
||||
kf = KEYD / "chtenie-z17-KEY.json"
|
||||
if not kf.exists():
|
||||
print("ключа нет — сначала --emit")
|
||||
return 1
|
||||
key = json.loads(kf.read_text(encoding="utf-8"))
|
||||
sc, _bad = ZS.read()
|
||||
ranks: dict[tuple, dict[str, float]] = {}
|
||||
for tok, blocks in key.items():
|
||||
if tok.startswith("_"):
|
||||
continue
|
||||
meta0 = next(iter(blocks.values()))
|
||||
f = WORK / meta0["pair"] / meta0["reader"] / "answers" / f"{tok}.md"
|
||||
if not f.exists():
|
||||
continue
|
||||
got = {m.group(1): m.group(2) for m in _ORD.finditer(
|
||||
f.read_text(encoding="utf-8", errors="replace"))}
|
||||
for j, meta in blocks.items():
|
||||
if j not in got:
|
||||
print(f" ⚠ {tok}/отрывок {j}: порядка нет")
|
||||
continue
|
||||
r = _ranks(got[j])
|
||||
ranks[(meta["pair"], meta["uid"], meta["reader"])] = {
|
||||
arm: r[lab] for lab, arm in meta["метки"].items() if lab in r}
|
||||
if not ranks:
|
||||
print("ответов читателей нет")
|
||||
return 1
|
||||
rc = 0
|
||||
for pair in Z.PAIRS:
|
||||
rows = {k: v for k, v in ranks.items() if k[0] == pair}
|
||||
if not rows:
|
||||
continue
|
||||
print(f"\n=== ПАРА {pair} · прочитано единиц-читателей {len(rows)} ===")
|
||||
bad_d = [k for k, v in rows.items() if DECOY in v and v[DECOY] < max(v.values())]
|
||||
have_d = sum(1 for v in rows.values() if DECOY in v)
|
||||
print(f"КОНТРОЛЬ ДЕКОЯ: не последним в {len(bad_d)} чтениях из {have_d} → "
|
||||
f"{'ГОДНО' if not bad_d else '⛔ читатель невнимателен'}")
|
||||
rc |= bool(bad_d)
|
||||
gaps = [abs(v["Z0"] - v["ZF"]) for v in rows.values() if "Z0" in v and "ZF" in v]
|
||||
if gaps:
|
||||
far = sum(1 for g in gaps if g > 2)
|
||||
print(f"КОНТРОЛЬ ПОЛА (две генерации одного редактора): медиана расстояния "
|
||||
f"{st.median(gaps):.1f} позиции · развёл дальше двух в {far} из {len(gaps)} → "
|
||||
f"{'ГОДНО' if far <= len(gaps) // 4 else '⛔ порядок читателя = шум'}")
|
||||
rc |= far > len(gaps) // 4
|
||||
per_reader = collections.defaultdict(dict)
|
||||
for (_p, uid, rd), v in rows.items():
|
||||
per_reader[rd][uid] = v
|
||||
if len(per_reader) == 2:
|
||||
(ra, va), (rb, vb) = sorted(per_reader.items())
|
||||
rr = [_spearman([va[u][x] for x in va[u] if x in vb[u]],
|
||||
[vb[u][x] for x in va[u] if x in vb[u]]) for u in va if u in vb]
|
||||
if rr:
|
||||
print(f"СОГЛАСИЕ ЧИТАТЕЛЕЙ {ra}/{rb}: Спирмен по единицам {st.mean(rr):+.2f} "
|
||||
f"(единиц {len(rr)})")
|
||||
avg = {a: st.mean([v[a] for v in rows.values() if a in v])
|
||||
for a in PANEL + (DECOY,) if any(a in v for v in rows.values())}
|
||||
print(f"\n{'арм':12s}{'ранг чтения':>13s}{'ошибок/ед.':>12s}")
|
||||
xy = []
|
||||
for a in sorted(avg, key=lambda x: avg[x]):
|
||||
errs = [ZS._carry(ax) for (pp, u, aa, _h), ax in sc.items()
|
||||
if pp == pair and aa == a and u in {k[1] for k in rows}]
|
||||
print(f"{a:12s}{avg[a]:13.2f}"
|
||||
+ (f"{st.mean(errs):12.2f}" if errs else f"{'—':>12s}"))
|
||||
if errs and not a.startswith("CTRL"):
|
||||
xy.append((avg[a], st.mean(errs)))
|
||||
if len(xy) >= 3:
|
||||
rho = _spearman([x for x, _ in xy], [y for _, y in xy])
|
||||
print(f"\nСПИРМЕН «ранг чтения против счёта ошибок»: {rho:+.2f} — "
|
||||
+ ("счёт следит за читаемостью, прокси годен" if rho > 0.5 else
|
||||
"⛔ СЧЁТ НЕ СЛЕДИТ ЗА ЧИТАЕМОСТЬЮ: по пре-регу Д17 побеждает ЧТЕНИЕ"))
|
||||
print("\n⚠ Читают модели, не человек. Прибор отвечает на вопрос «годен ли счёт как прокси», "
|
||||
"а не «какой арм лучше вообще».")
|
||||
return rc
|
||||
|
||||
|
||||
def score_calib() -> int:
|
||||
"""Де-слепление КАЛИБРОВКИ 16.08: печатает, какой арм стоял за какой меткой в ответе
|
||||
читателя.
|
||||
|
||||
⚠ ЧЕСТНАЯ ГРАНИЦА (правка по ревью): режим НЕ пере-считывает корреляцию −0.10 — он
|
||||
печатает только порядок читателя. Прежний докстринг обещал воспроизводимость этого
|
||||
числа, и обещание было ложным. Само число проверено независимо (адверсариальное ревью
|
||||
16.08 пере-считало его по рангам из ключа и панели Д3 — вышло ровно −0.10), но КОМАНДЫ,
|
||||
делающей это одним вызовом, нет. Долг назван, а не замаскирован."""
|
||||
if not CALIB_KEY.exists():
|
||||
print("ключа калибровки нет")
|
||||
return 1
|
||||
key = json.loads(CALIB_KEY.read_text(encoding="utf-8"))
|
||||
for axis in ("zh", "en"):
|
||||
f = CALIB / f"ОТВЕТ-{axis}.md"
|
||||
if not f.exists() or axis not in key:
|
||||
print(f"{axis}: ответа нет")
|
||||
continue
|
||||
m = re.search(r"((?:Т\d\s*[>=]\s*)+Т\d)", f.read_text(encoding="utf-8"))
|
||||
if not m:
|
||||
print(f"{axis}: порядок не найден")
|
||||
continue
|
||||
lay = key[axis]["метки"]
|
||||
print(f"\n{axis}: единица {key[axis]['uid']} · порядок читателя (лучший → худший):")
|
||||
for i, lab in enumerate(re.findall(r"Т\d", m.group(1)), 1):
|
||||
print(f" {i}. {lab} = {lay.get(lab, '?')}")
|
||||
return 0
|
||||
|
||||
|
||||
def selftest() -> int:
|
||||
fails = []
|
||||
ok = lambda c, m: fails.append(m) if not c else None # noqa: E731
|
||||
r = _ranks("Т3 > Т1 = Т5 > Т2")
|
||||
ok(r == {"Т3": 1.0, "Т1": 2.5, "Т5": 2.5, "Т2": 4.0}, f"разбор порядка сломан: {r}")
|
||||
ok(abs(_spearman([1, 2, 3], [1, 2, 3]) - 1.0) < 1e-9, "Спирмен не даёт +1 на совпадении")
|
||||
ok(abs(_spearman([1, 2, 3], [3, 2, 1]) + 1.0) < 1e-9, "Спирмен не даёт −1 на инверсии")
|
||||
m = _ORD.search("ПОРЯДОК 2: Т3 > Т1 > Т5")
|
||||
ok(bool(m) and m.group(1) == "2", "строка порядка не находится")
|
||||
for pair in Z.PAIRS:
|
||||
u = pick(pair)
|
||||
ok(len(u) == N_UNITS, f"{pair}: единиц {len(u)}, ожидалось {N_UNITS}")
|
||||
ok(set(u) <= {x["uid"] for x in Z.chosen(pair)},
|
||||
f"{pair}: единицы чтения не подмножество отсуженных — Спирмен считать не на чем")
|
||||
la, lb = layout(pair, u[0], "A", PANEL), layout(pair, u[0], "B", PANEL)
|
||||
ok(la != lb, f"{pair}: у читателей ОДНА раскладка — общий позиционный наклон")
|
||||
ok(layout(pair, u[0], "A", PANEL) == la, f"{pair}: раскладка не воспроизводима")
|
||||
for pair in Z.PAIRS: # панель чтения обязана СОБИРАТЬСЯ, иначе гейт пуст
|
||||
tx_all = ZS.texts_of(pair)
|
||||
n = sum(1 for u in pick(pair) if variants(pair, tx_all[u]))
|
||||
ok(n == N_UNITS, f"{pair}: панель чтения собирается на {n} единицах из {N_UNITS} — "
|
||||
"клетки захода не куплены, эмиссия дала бы неполные пачки")
|
||||
for m in fails:
|
||||
print("ПРОВАЛ:", m)
|
||||
print(f"селфтест чтения: провалов {len(fails)}")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:] or ["--selftest"]
|
||||
fn = {"--emit": emit, "--score": lambda: sys.exit(score()),
|
||||
"--score-calib": lambda: sys.exit(score_calib()),
|
||||
"--selftest": lambda: sys.exit(selftest())}.get(a[0])
|
||||
if not fn:
|
||||
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")
|
||||
fn()
|
||||
|
|
@ -53,7 +53,9 @@ def main() -> int:
|
|||
norm(c) in norm(f.read_text(encoding="utf-8")))
|
||||
# ядро обязано нести правила, купленные дорого прошлыми паками
|
||||
must = ["ОТДЕЛЬНО против исходника", "требует обоснования ЦИТАТОЙ",
|
||||
"Ноль — это утверждение", "потерянное отрицание", "Не ищи и не открывай ничего другого"]
|
||||
"Ноль — это утверждение", "потерянное отрицание", "Не ищи и не открывай ничего другого",
|
||||
# решение владельца 16.08, меняющее шкалу: см. `zsud.RUBRIC_*` и эрратy Д18
|
||||
"переврать смысл исходника — ошибка; обойтись с его буквой вольно — нет"]
|
||||
for m in must:
|
||||
ck(f"ядро несёт правило: «{m[:44]}»", norm(m) in norm(c))
|
||||
print(f"\n{'ПАРИТЕТ ОБВЯЗОК СОБЛЮДЁН' if not bad else f'ПРОВАЛОВ: {bad}'}")
|
||||
|
|
|
|||
|
|
@ -63,8 +63,36 @@ AXES = [
|
|||
"и продуктового смысла при цене 28× не имеет. ⚠ Именно этот порог решил спор о числе "
|
||||
"клеток: при n=12 (минимум промта) MDE 2.11 БОЛЬШЕ цели и ось была бы ОПИСАТЕЛЬНОЙ; "
|
||||
"владелец 10.08 выбрал полные 16, и только на них ось несущая"),
|
||||
# ⚠ ЗАХОД Д17 ВПИСАН 16.08, УЖЕ ПОСЛЕ НАЧАЛА ПОКУПКИ — ЭТО ДЕВИАЦИЯ, И ОНА ОБЪЯВЛЕНА ЗДЕСЬ,
|
||||
# А НЕ ПРИМЕЧАНИЕМ ПОСТ-ФАКТУМ. Норма заказа (:168) требует НАПЕЧАТАТЬ MDE при плановом n до
|
||||
# денег; пре-рег Д17.4 объявил ПРАВИЛО («MDE считается по своему полу, не проходит цель — ось
|
||||
# описательная»), но числа не напечатал, хотя инструмент для этого стоит в зоне. Покупка к
|
||||
# моменту записи прошла ~40 клеток из 160. Числа ниже посчитаны ДО первого судейского ответа,
|
||||
# то есть право пере-классифицировать ось до вскрытия вердиктов сохранено.
|
||||
# ⚠ И ГЛАВНАЯ ОГОВОРКА ПРАЙОРА: все полы выше сняты СТАРОЙ рубрикой, а заход судится
|
||||
# ПОЧИНЕННОЙ («вольность не штрафуется»), которая по построению убирает часть счёта. Перенос
|
||||
# прайора между шкалами — допущение, а не замер, и потому статус оси решает НЕ он, а СВОЙ пол
|
||||
# захода (`ZF` против `Z0`, две генерации боевой связки), снятый на новой шкале.
|
||||
("Д17 заход zh (новая рубрика)", 16, 4, 2.12, 1.50,
|
||||
"армы правят ОДНУ базу — структура `tier`/`border` (sd 1.45/2.12), берётся худшая; "
|
||||
"⚠ прайор снят СТАРОЙ рубрикой и на новую шкалу переносится как допущение",
|
||||
"та же цель, что у Д4: решение «менять архитектуру» стоит принимать при разрыве ≥ трети "
|
||||
"собственного эффекта редактора (+4.44 эксп-22 Ф3) — это 1.50"),
|
||||
("Д17 заход en (новая рубрика)", 16, 4, 2.12, 1.50,
|
||||
"своего пола на en в новой шкале нет; прайор с zh той же структуры, свой пол докупается",
|
||||
"цель снижена с 2.00 до 1.50 против оси Д3 сознательно: там вопрос был «переносится ли "
|
||||
"эффект редактора вообще», здесь — «какая из четырёх архитектур лучше», и решение о смене "
|
||||
"архитектуры принимается при меньшем разрыве"),
|
||||
]
|
||||
FORCED_DESCRIPTIVE = {"Д6 ja→ru разведка"} # объявлено промтом, арифметикой не отменяется
|
||||
# ⚠ РЕШЕНИЕ ВЛАДЕЛЬЦА 16.08, ПРИНЯТО ДО ЕДИНОГО СУДЕЙСКОГО ОТВЕТА. Мощность захода посчитана уже
|
||||
# после начала покупки (девиация объявлена выше), и она показала: при n=16 и цели 1.50 несущий
|
||||
# вывод недостижим — нужен свой пол sd ≤ 1.32, а все замеренные полы фазы 1.45…2.63. Владельцу
|
||||
# предложено добрать китайскую ось до 31 главы за $0.48 (тогда порог требований падает до 1.83,
|
||||
# то есть достижим); он выбрал НЕ добирать. Значит обе оси захода ОПИСАТЕЛЬНЫЕ, и решающим
|
||||
# эндпойнтом остаётся слепое чтение — ровно так, как пре-рег Д17 и объявил заранее.
|
||||
# Пере-классификации задним числом не будет ни при каком исходе арифметики.
|
||||
FORCED_DESCRIPTIVE = {"Д6 ja→ru разведка",
|
||||
"Д17 заход zh (новая рубрика)", "Д17 заход en (новая рубрика)"}
|
||||
|
||||
# ⚠ ПЕРВИЧНОЕ И ВТОРИЧНОЕ СЕМЕЙСТВА — объявляются ДО покупок, и это НЕ сужение задним числом.
|
||||
# Поправка Холма делится на РАЗМЕР семейства, поэтому «взять все контрасты несущими» = сделать
|
||||
|
|
@ -78,6 +106,18 @@ PRIMARY = {
|
|||
"A6/A0 — качественный черновик + контур (H-1)"],
|
||||
"Д3 en→ru несущая": ["E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)"],
|
||||
"Д1 gemini добивка": ["R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)"],
|
||||
# Заход Д17: по одному контрасту на КАЖДЫЙ вопрос, ради которого куплен арм. `Z7` в семействе
|
||||
# нет намеренно — он меряется детерминированным канон-гейтом и судейского слота не занимает.
|
||||
"Д17 заход zh (новая рубрика)": [
|
||||
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)",
|
||||
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)",
|
||||
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)",
|
||||
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале"],
|
||||
"Д17 заход en (новая рубрика)": [
|
||||
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)",
|
||||
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)",
|
||||
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)",
|
||||
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале"],
|
||||
}
|
||||
# ⚠⚠ n У КАЖДОГО КОНТРАСТА СВОЙ, И ЭТО РЕШАЕТ. Прежняя редакция печатала MDE и статус ПО ОСИ при
|
||||
# плановом n=32 — а члены первичного семейства живут на разных выборках, потому что база у них
|
||||
|
|
@ -92,6 +132,14 @@ CONTRAST_N = {
|
|||
"A6/A0 — качественный черновик + контур (H-1)": 16,
|
||||
"E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)": 16,
|
||||
"R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)": 16,
|
||||
# ⚠ У захода n=16 у ВСЕХ четырёх контрастов, и это не совпадение, а условие отбора единиц:
|
||||
# в пул берутся только те, где ВСЕ бесплатные армы и ответ критика уже есть (`zakhod.pool`).
|
||||
# Именно поконтрастная проверка n погубила носителя H-1 (`A6/A0` жил на 16 из 32) — здесь
|
||||
# разъехаться выборкам структурно не на чем.
|
||||
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)": 16,
|
||||
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)": 16,
|
||||
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)": 16,
|
||||
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале": 16,
|
||||
}
|
||||
|
||||
SECONDARY = {
|
||||
|
|
@ -199,16 +247,19 @@ def main() -> int:
|
|||
m = mde(sd, n_c, k)
|
||||
ok = m <= target
|
||||
if not ok:
|
||||
weak.append((c, n_c, m, target))
|
||||
weak.append((c, n_c, m, target, k))
|
||||
print(f" {c[:56]:56s}{n_c:4d}{m:7.2f}{target:7.2f} "
|
||||
f"{'НЕСУЩИЙ' if ok else '⛔ НЕДОМОЩЕН'}")
|
||||
if weak:
|
||||
print("\n ⛔ НЕДОМОЩНЫЕ КОНТРАСТЫ ПЕРВИЧНОГО СЕМЕЙСТВА — объявляются ОПИСАТЕЛЬНЫМИ:")
|
||||
for c, n_c, m, t in weak:
|
||||
need_sd = t * math.sqrt(n_c) / ((_t(1 - 0.05 / 6, PRIOR_DF) + _t(0.80, PRIOR_DF))
|
||||
* BETWEEN)
|
||||
need_n = math.ceil((((_t(1 - 0.05 / 6, PRIOR_DF) + _t(0.80, PRIOR_DF))
|
||||
* 2.12 * BETWEEN) / t) ** 2)
|
||||
# ⚠ ПОЧИНКА ПО РЕВЬЮ: множитель Холма был ЗАШИТ как k=3 (`0.05 / 6`) для всех осей, тогда
|
||||
# как сам `mde()` считает по настоящему k. Печатались ремонтные числа мягче честных: для
|
||||
# контрастов захода (k=4) выходило «sd ≤ 1.37 либо n ≥ 39» вместо честных 1.32 и 42.
|
||||
# Купив по этой строке пол с sd 1.35, ось объявили бы несущей ошибочно.
|
||||
for c, n_c, m, t, k_c in weak:
|
||||
mult = (_t(1 - 0.05 / (2 * max(1, k_c)), PRIOR_DF) + _t(0.80, PRIOR_DF)) * BETWEEN
|
||||
need_sd = t * math.sqrt(n_c) / mult
|
||||
need_n = math.ceil((mult * 2.12 / t) ** 2)
|
||||
print(f" • {c}\n при n={n_c} MDE {m:.2f} > цели {t:.2f}; несущим станет "
|
||||
f"либо при своём поле sd ≤ {need_sd:.2f}, либо при n ≥ {need_n}")
|
||||
print("\nСЕМЕЙСТВА, объявленные ДО покупок:")
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
ОТКАЗЫВАЕТСЯ, пока ростер не пере-пинен. Это не осторожность, а единственное, что отличает
|
||||
леджер-измеритель от леджера-фантазёра.
|
||||
|
||||
Запуск: zakhod.py --dry | --selftest | --buy [zh|en] [армы…] | --plan
|
||||
Запуск: zakhod.py --dry | --selftest | --buy [zh|en] [армы…] | --echo
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -95,7 +95,11 @@ JULY_PIN_PRO = 0.435 # если в ростере всё ещё
|
|||
# ── ПАРА КАК ДАННЫЕ ───────────────────────────────────────────────────────────────────────────
|
||||
# Ни одна ветка ниже не спрашивает «а это китайский?». Всё, что у пары своё, — здесь.
|
||||
PAIRS = {
|
||||
"zh": {"para": lambda: PARA.ZH, "letter": "i", "phase": "ФД-I", "max_out": 16000,
|
||||
# ⚠ БЮДЖЕТ ВЫВОДА 32000 НА ОБЕИХ ПАРАХ (починка по ревью). Хендофф §5.4 предупреждал прямо:
|
||||
# «бюджет вывода новых пар 32000, 16000 мало». Я оставил китайской 16000 по умолчанию контура —
|
||||
# и получил ДВА обрыва `finish=length` на первых шести клетках `Z8R` ($0.0324 списано, текста
|
||||
# нет). У DeepSeek размышление тарифицируется ВНУТРИ бюджета, а обогащённый черновик длиннее.
|
||||
"zh": {"para": lambda: PARA.ZH, "letter": "i", "phase": "ФД-I", "max_out": 32000,
|
||||
"onepass": "A2", "crit": "A3", "wire": lambda: C.configure(PARA.ZH),
|
||||
"units": lambda: C.units_ok()},
|
||||
"en": {"para": lambda: PARA.EN, "letter": "j", "phase": "ФД-J", "max_out": 32000,
|
||||
|
|
@ -111,14 +115,27 @@ FREE = ("ZD", "Z0", "ZP")
|
|||
# просто перепишет как обычно, а потерянная находка не вернётся ничем.
|
||||
# Поэтому строка выбрасывается, ТОЛЬКО если (а) принимающая формула стоит в ХВОСТЕ вердикта —
|
||||
# то есть критик закончил согласием, — И (б) во всей строке нет ни одного маркера дефекта.
|
||||
# ⚠⚠ ПОЧИНКА ПО АДВЕРСАРИАЛЬНОМУ РЕВЬЮ (другое модельное семейство, 16.08). Прежняя редакция
|
||||
# матчила принимающие формулы ВНУТРИ отрицательных слов: «неправильно», «некорректно»,
|
||||
# «недопустимо», «неуместно», «неадекватно», «неточно передан» опознавались как СОГЛАСИЕ, потому
|
||||
# что `правильно`/`корректн`/`допустим`/`уместн`/`адекватн` — их подстроки, а в списке маркеров
|
||||
# дефекта этих форм не было. Замер по живому корпусу (1669 строк критика обеих пар): **10 реальных
|
||||
# находок ложно отсеяны**, среди них «недопустимое смешение языков» и «неправильное употребление
|
||||
# предлога». Ставка владельца покупалась бы по спискам, из которых выпали дефекты.
|
||||
# Лечение: отрицательная приставка ловится ЯВНО и стоит в маркерах дефекта, а принимающие формулы
|
||||
# требуют границы слова слева, чтобы не цепляться к «не-».
|
||||
CONFIRM_RX = re.compile(
|
||||
r"(?:вс[е]\s+верно|верно\b|правильно\b|корректн|допустим|уместн|не\s+ошибк|ошибки\s+нет|"
|
||||
r"претензий\s+нет|нареканий\s+нет|приемлем|адекватн|точно\s+переда|передано\s+верно|"
|
||||
r"соответствует\s+исходник|без\s+замечаний|(?:^|[\s,;—-])ок[.,;]?$)", re.IGNORECASE)
|
||||
r"(?<![а-яё])(?:вс[е]\s+верно|верно\b|правильно\b|корректн|допустим|уместн|не\s+ошибк|"
|
||||
r"ошибки\s+нет|претензий\s+нет|нареканий\s+нет|приемлем|адекватн|точно\s+переда|"
|
||||
r"передано\s+верно|соответствует\s+исходник|без\s+замечаний|(?:^|[\s,;—-])ок[.,;]?$)",
|
||||
re.IGNORECASE)
|
||||
DEFECT_RX = re.compile(
|
||||
r"(искаж|потер|неверн|ошибк|кальк|канцеляр|пропущ|инверт|рассоглас|подмен|выдум|отрицан|"
|
||||
r"не\s+соответств|лучше\b|следует\b|надо\b|стоит\b|заменить|исправ|сух|тяжел|громозд|"
|
||||
r"буквализм|подстрочник|не\s+переда|утрач)", re.IGNORECASE)
|
||||
r"буквализм|подстрочник|не\s+переда|утрач|"
|
||||
# отрицательные формы принимающих слов — они и есть находки, а не согласия
|
||||
r"неправильн|некорректн|недопустим|неуместн|неадекватн|неточн|неверо|не\s+вполне|"
|
||||
r"не\s+совсем|сомнительн|спорн)", re.IGNORECASE)
|
||||
CONFIRM_TAIL = 70 # знаков хвоста вердикта, где принимающая формула что-то значит
|
||||
|
||||
|
||||
|
|
@ -280,10 +297,18 @@ Z1_HEAD = (
|
|||
|
||||
|
||||
def z1_msgs(p: str, src: str, draft: str, notes: list[str]) -> list[dict]:
|
||||
"""⚠ ПОЧИНКА ПО РЕВЬЮ: замечания дописываются в ПОСЛЕДНЕЕ сообщение с ролью `user`, а не в
|
||||
`m[1]`. При непустом банке `editor_msgs` вставляет ВТОРЫМ сообщением закон-блок (роль
|
||||
`system`), и замечания критика уезжали в системный промт — то есть структура запроса Z1
|
||||
различалась между единицами, где термы банка есть и где их нет. Селфтест этого не ловил,
|
||||
потому что проверял ИНДЕКС, а не роль: `m[1]` существует всегда."""
|
||||
key = _pair(p)["para"]().key
|
||||
bf = None if key == "zh-ru" else _pair(p)["para"]().bank_file
|
||||
m = PR.editor_msgs(src, draft, PR.law_block(PR.terms_in(src, bf), bf), pair=key)
|
||||
m[1]["content"] = m[1]["content"] + Z1_HEAD + "\n".join(f"- {n}" for n in notes)
|
||||
if m[-1].get("role") != "user":
|
||||
raise SystemExit(f"⛔ последнее сообщение редактора не `user` ({m[-1].get('role')}) — "
|
||||
"замечания критика уехали бы в системный промт, СТОП")
|
||||
m[-1]["content"] = m[-1]["content"] + Z1_HEAD + "\n".join(f"- {n}" for n in notes)
|
||||
return m
|
||||
|
||||
|
||||
|
|
@ -320,13 +345,14 @@ def chosen(p: str) -> list[dict]:
|
|||
|
||||
|
||||
# ── ГЕЙТ ПЕРЕЛОМА ЦЕНЫ ────────────────────────────────────────────────────────────────────────
|
||||
def price_gate() -> None:
|
||||
def price_gate(quiet: bool = False) -> None:
|
||||
now = dt.datetime.now(dt.timezone.utc)
|
||||
pin = ROSTER.CANDIDATES[EDITOR][2]
|
||||
stale = abs(pin - JULY_PIN_PRO) < 1e-9
|
||||
left = (PRICE_CUTOVER - now).total_seconds() / 3600
|
||||
if now < PRICE_CUTOVER:
|
||||
print(f" цена DeepSeek: до перелома {left:.1f} ч, июльский пин ${pin}/1M ВЕРЕН")
|
||||
if not quiet:
|
||||
print(f" цена DeepSeek: до перелома {left:.1f} ч, июльский пин ${pin}/1M ВЕРЕН")
|
||||
return
|
||||
if stale:
|
||||
raise SystemExit(
|
||||
|
|
@ -348,20 +374,88 @@ def _buy(led, tg: str, model: str, msgs: list[dict], max_out: int, **extra) -> N
|
|||
" Это СТОП и вопрос владельцу: назвать недостающую сумму.")
|
||||
|
||||
|
||||
def preflight(models: list[str], phase: str, letter: str) -> None:
|
||||
"""Пред-полётная проба: КАЖДАЯ модель прогона отвечает, ДО того как пойдёт цикл покупок.
|
||||
|
||||
⚠ ЗАВЕДЕНО ПО ИНЦИДЕНТУ 16.08, И ЭТО ДЫРА В ИНФРЕ, А НЕ ПЕДАНТИЗМ. Заход ушёл в цикл на 160
|
||||
клеток, не проверив ничего; на 62-й клетке у провайдера кончился баланс, и оставшиеся 96
|
||||
запросов ушли в `402 Insufficient Balance` — по 0.9 с каждый, с записью файла на диск. Денег
|
||||
это не стоило (отказ бесплатен), но выглядело как «купили 80 клеток»: файлы есть, годного
|
||||
содержимого нет. Ровно тот класс, о котором предупреждает хендофф — «файл клетки ≠ годная
|
||||
клетка», — и он стоил мне ложного доклада владельцу.
|
||||
⚠ ПРОБА ИДЁТ ЧЕРЕЗ КАССУ (починка по ревью): первая редакция звала клиента напрямую, то есть
|
||||
делала ПЛАТНЫЙ вызов мимо леджера — прямое нарушение нормы «все платные вызовы через кассу,
|
||||
два пути счёта». Копейки, но именно так и заводится расход, которого касса не видит. Тег
|
||||
пробы несёт порядковый номер, иначе кэш `purchase` вернул бы прошлую запись и гейт перестал
|
||||
бы что-либо проверять на втором прогоне.
|
||||
"""
|
||||
led = MONEY.Guarded(phase, default_expect=0.001)
|
||||
for m in dict.fromkeys(models):
|
||||
k = 1
|
||||
while (OUT / f"dv-{letter}-probe-{m}-{k}.json").exists():
|
||||
k += 1
|
||||
tg = f"dv-{letter}-probe-{m}-{k}"
|
||||
rec = MONEY.purchase(led, tg, m, ED22.client(m),
|
||||
ROSTER.call_kwargs(m, [{"role": "user", "content": "ок"}],
|
||||
max_out=8000),
|
||||
role="preflight")
|
||||
if rec.get("finish") != "stop":
|
||||
raise SystemExit(
|
||||
f"⛔ ПРЕД-ПОЛЁТНАЯ ПРОБА НЕ ПРОШЛА: {m} → finish={rec.get('finish')} "
|
||||
f"{str(rec.get('error'))[:200]}\n"
|
||||
" Покупка НЕ запускается. Цикл на сотню клеток против неотвечающего\n"
|
||||
" провайдера пишет файлы без содержимого и выглядит как успешный прогон.")
|
||||
print(f" проба {m}: отвечает, ${rec.get('cost_usd', 0):.6f} (учтено кассой)")
|
||||
|
||||
|
||||
def refill(p: str, which: list[str]) -> int:
|
||||
"""Увести оборванные клетки (`finish=length`, пусто) в карантин, чтобы их можно было ПЕРЕ-купить.
|
||||
|
||||
⚠ ЗАВЕДЕНО ПО РЕВЬЮ: без этого оборванная клетка — мёртвая зона. Кэш `purchase` смотрит на
|
||||
СУЩЕСТВОВАНИЕ файла и возвращает старую запись, а `cell()` требует `finish=stop` и отдаёт
|
||||
пусто; значит смета считает клетку в «надо», а покупка на ней — вечный бесшумный no-op.
|
||||
Файл не удаляется, а переименовывается: деньги за него списаны и обязаны остаться видимы
|
||||
кассе (глоб считает `dv-*.json`, карантинное имя под него попадает).
|
||||
"""
|
||||
n = 0
|
||||
for arm in which:
|
||||
for u in chosen(p):
|
||||
f = OUT / f"{tag(p, arm, u['uid'])}.json"
|
||||
if not f.exists():
|
||||
continue
|
||||
rec = json.loads(f.read_text(encoding="utf-8"))
|
||||
if rec.get("finish") == "length" and not (rec.get("content") or "").strip():
|
||||
k = 1
|
||||
while (OUT / f"{f.stem}.LENGTH{k}.json").exists():
|
||||
k += 1
|
||||
f.rename(OUT / f"{f.stem}.LENGTH{k}.json")
|
||||
print(f" {f.stem}: обрыв на потолке вывода → карантин, клетка пере-покупаема")
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def buy(p: str, which: list[str]) -> None:
|
||||
price_gate()
|
||||
cfg = _pair(p)
|
||||
cfg["wire"]()
|
||||
preflight([DRAFT_MODEL if a == "Z8" else EDITOR for a in which],
|
||||
cfg["phase"], cfg["letter"])
|
||||
us = chosen(p)
|
||||
led = MONEY.Guarded(cfg["phase"], default_expect=0.009)
|
||||
mo = cfg["max_out"]
|
||||
print(f"\nпара {p} · единиц {len(us)} · армы {' '.join(which)} · касса {cfg['phase']}")
|
||||
print(f" оборванных клеток уведено в карантин: {refill(p, which)}")
|
||||
for arm in which:
|
||||
for u in us:
|
||||
uid, src = u["uid"], u["source"]
|
||||
tg = tag(p, arm, uid)
|
||||
if cell(tg).strip():
|
||||
continue
|
||||
# ⚠ ГЕЙТ ПЕРЕЛОМА ЦЕНЫ — ПЕРЕД КАЖДОЙ КЛЕТКОЙ, А НЕ ОДИН РАЗ НА СТАРТЕ (починка по
|
||||
# ревью): прогон длится часами, и пересёкший 16:00 UTC цикл писал бы пост-переломные
|
||||
# вызовы по июльскому прайсу — леджер занижен втрое ровно на том куске, который куплен
|
||||
# после перелома.
|
||||
price_gate(quiet=True)
|
||||
if arm == "Z8":
|
||||
# штатный гейт годности + ОДИН ре-ген: эхо стохастично по вызову (квирки п.6)
|
||||
for attempt in range(REGEN_MAX + 1):
|
||||
|
|
@ -482,10 +576,20 @@ def cmd_selftest() -> int:
|
|||
ed = editor_msgs(p, u["source"], free_text(p, "ZD", u["uid"]))
|
||||
ck(f"{p}: Z1 несёт системный промт боевого редактора ПОБАЙТНО",
|
||||
z1[0]["content"] == ed[0]["content"])
|
||||
ck(f"{p}: замечания критика уходят Z1 пользовательским сообщением",
|
||||
z1[1]["content"].startswith(ed[1]["content"]) and len(z1[1]["content"]) > len(ed[1]["content"]))
|
||||
ck(f"{p}: мандат редактуры в Z1 остался ПОЛНЫМ", "ДОБАВЛЕНИЕ к твоей обычной работе"
|
||||
in z1[1]["content"])
|
||||
# ⚠ ПРОВЕРКА ПО РОЛИ, А НЕ ПО ИНДЕКСУ (починка по ревью). Прежняя редакция смотрела в
|
||||
# `m[1]`, который при непустом банке есть закон-блок с ролью `system`: замечания критика
|
||||
# уезжали в системный промт, а гейт этого не видел, потому что индекс существует всегда.
|
||||
usr = [m for m in z1 if m["role"] == "user"]
|
||||
edu = [m for m in ed if m["role"] == "user"]
|
||||
ck(f"{p}: у Z1 ровно одно пользовательское сообщение", len(usr) == 1, f"их {len(usr)}")
|
||||
ck(f"{p}: замечания критика уходят ПОЛЬЗОВАТЕЛЬСКИМ сообщением",
|
||||
bool(usr) and bool(edu) and usr[-1]["content"].startswith(edu[-1]["content"])
|
||||
and len(usr[-1]["content"]) > len(edu[-1]["content"]))
|
||||
ck(f"{p}: в системный промт Z1 замечания НЕ попали",
|
||||
not any("ДОБАВЛЕНИЕ к твоей обычной работе" in m["content"]
|
||||
for m in z1 if m["role"] == "system"))
|
||||
ck(f"{p}: мандат редактуры в Z1 остался ПОЛНЫМ",
|
||||
bool(usr) and "ДОБАВЛЕНИЕ к твоей обычной работе" in usr[-1]["content"])
|
||||
# ZF обязан быть побайтной копией запроса Z0 — иначе это не пол, а другой арм
|
||||
ck(f"{p}: ZF = ТОТ ЖЕ запрос, что у боевой связки (пол, а не новый арм)",
|
||||
editor_msgs(p, u["source"], free_text(p, "ZD", u["uid"])) == ed)
|
||||
|
|
@ -517,26 +621,12 @@ def cmd_selftest() -> int:
|
|||
return bad
|
||||
|
||||
|
||||
def cmd_plan() -> None:
|
||||
for p in PAIRS:
|
||||
us = chosen(p)
|
||||
print(f"\n=== {p}: {len(us)} единиц ===")
|
||||
for a in FREE + ARMS:
|
||||
have = sum(1 for u in us
|
||||
if (free_text(p, a, u["uid"]) if a in FREE else cell(tag(p, a, u["uid"])))
|
||||
.strip())
|
||||
print(f" {a:4s} {have:2d}/{len(us)}" + (" (бесплатно)" if a in FREE else ""))
|
||||
MONEY.report()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:] or ["--dry"]
|
||||
if a[0] == "--selftest":
|
||||
sys.exit(1 if cmd_selftest() else 0)
|
||||
elif a[0] == "--dry":
|
||||
cmd_dry()
|
||||
elif a[0] == "--plan":
|
||||
cmd_plan()
|
||||
elif a[0] == "--echo":
|
||||
echo_report()
|
||||
elif a[0] == "--buy":
|
||||
|
|
@ -545,4 +635,4 @@ if __name__ == "__main__":
|
|||
for x in pr:
|
||||
buy(x, ar)
|
||||
else:
|
||||
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --dry --selftest --plan --buy")
|
||||
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --dry --selftest --buy --echo")
|
||||
|
|
|
|||
561
eval/dovodka/zsud.py
Normal file
561
eval/dovodka/zsud.py
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
#!/usr/bin/env python3
|
||||
"""СУДЕЙСТВО ЗАХОДА Д17 — слепой проход по ПОЧИНЕННОЙ рубрике. $0 (агент-сессии).
|
||||
|
||||
Панель и правило решения зафиксированы пре-регом Д17 ДО покупок. Здесь только эмиссия заданий,
|
||||
разбор ответов и гейты. Ключ и соль свои: раскладка меток есть чистая функция соли, uid и НАБОРА
|
||||
армов, поэтому эмиссия поверх чужого ключа переписала бы раскладку уже отсуженной оси (авария
|
||||
фазы Д, восстановленная только потому, что соль лежит отдельным файлом).
|
||||
|
||||
ПАНЕЛЬ (8 клеток на единицу, по 6 боевых армов и 2 контроля):
|
||||
|
||||
p1: ZD · Z0 · ZP · Z8 · Z8R · Z1 + грубый декой + маржевый декой
|
||||
p2: ZD · ZF · ZP · Z8 · Z8R · Z1 + грубый декой + маржевый декой
|
||||
|
||||
⚠ ПОЛОВИНЫ ШУМОВОГО ПОЛА РАЗВЕДЕНЫ ПО НАБОРАМ: `Z0` живёт только в p1, `ZF` — только в p2.
|
||||
Это две генерации ОДНОГО И ТОГО ЖЕ боевого редактора, и разница их оценок есть чистый шум прибора,
|
||||
из которого строится порог различимости. Судья, увидевший обе, обесценивает порог целиком.
|
||||
Ширина панели в наборах ОДИНАКОВА (8 клеток) — иначе разница наборов мерила бы усталость судьи,
|
||||
а не шум: замер фазы Д показал, что строгость счёта зависит от ширины пачки.
|
||||
|
||||
⚠ `Z7` (однопроходка + канон-фиксер) СУДЕЙСТВОМ НЕ МЕРИТСЯ ВОВСЕ — так объявлено пре-регом.
|
||||
Его вопрос про БАНК, и отвечает на него детерминированный канон-гейт (`--canon`, $0). Судейский
|
||||
слот стоит внимания судьи, а внимание — тот ресурс, за который панель платит строгостью.
|
||||
|
||||
⚠ ДОНОР ДЕКОЯ ЧЕРЕДУЕТСЯ (`Z0`/`Z8R` по чётности единицы). В 62 пачках оси Д4 донором всегда был
|
||||
эталон, то есть чувствительность сертифицировалась в окрестности ОДНОГО арма.
|
||||
|
||||
Запуск: zsud.py --emit | --score [zh|en] | --canon | --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))
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
Z = _load("zakhod_s", ZONE / "zakhod.py")
|
||||
import absjudge as AJ # noqa: E402
|
||||
|
||||
# ── ПОЧИНЕННАЯ РУБРИКА (решение владельца 16.08). Живёт здесь, потому что потребитель у неё
|
||||
# ОДИН — этот проход; закрытые проходы `tier2`/`ja6` судились старой шкалой и не трогаются.
|
||||
# Обвязки семейств несут то же правило, и совпадение сторожит существующий гейт `paritet.py`
|
||||
# (его список `must`), а не отдельный файл: второй гейт паритета был бы просто второй правдой.
|
||||
#
|
||||
# ⚠ ЧТО ИЗМЕНИЛОСЬ И ПОЧЕМУ — ЗАМЕР, А НЕ ВКУС. Слепое чтение 16.08 дало на английской оси
|
||||
# Спирмена −0.10 между порядком по счёту ошибок и порядком по читаемости: связи нет. Читатель
|
||||
# ВСЛЕПУЮ назвал механизм и попал — «у этого варианта локальных отклонений больше, а читается
|
||||
# заметно лучше». Прибор штрафовал ровно то, чем покупается читаемость.
|
||||
# ⚠ ЦЕНА: старые судейские числа дуги 19→23 с новыми НЕСРАВНИМЫ.
|
||||
# ⚠ РИСК, ЗАЯВЛЕННЫЙ ДО ДАННЫХ: правило «вольность — не ошибка» смещает счёт ВНИЗ, а пачка,
|
||||
# легшая на пол шкалы, перестаёт различать что-либо (проход `tier`: 38% нулей). Поэтому рубрика
|
||||
# идёт В КОМПЛЕКТЕ с двумя сторожами и по отдельности не применяется: гейт плотности и МАРЖЕВЫЙ
|
||||
# декой в каждой пачке.
|
||||
RUBRIC_TYPES = """ ВЕРНОСТЬ — сказано НЕ ТО, что в исходнике. Считается ТОЛЬКО искажение факта:
|
||||
· инверсия адресата или участника (кто кому говорит, кто кого бьёт, кто чей союзник);
|
||||
· снятое или, наоборот, добавленное отрицание;
|
||||
· подмена числа, разряда, ранга, меры, имени, времени, места;
|
||||
· выдуманный факт, которого в исходнике нет;
|
||||
· ПОТЕРЯННЫЙ факт: участник, действие, число, полярность, заголовок;
|
||||
· перевёрнутое состояние действия или идиомы (совершилось против ещё не совершилось).
|
||||
ЯЗЫК — то, чего НОСИТЕЛЬ РУССКОГО НЕ НАПИШЕТ:
|
||||
· калька с исходного языка — буквально переданный оборот, жест или идиома;
|
||||
· канцелярит и переводной суржик: «являлся», «осуществил», нанизывание родительных,
|
||||
отглагольное существительное там, где русский просит глагола;
|
||||
· несуществующее или неуместное слово, рассогласование, кривой порядок слов;
|
||||
· фраза грамматически верная, но звучащая ПЕРЕВЕДЁННОЙ, а не написанной по-русски.
|
||||
ТЕРМИН — имя или термин передан в РАЗНЫХ местах одного текста по-разному. ОПИСАТЕЛЬНАЯ ось.
|
||||
ФОРМА — нарушение РУССКОЙ типографики: реплика не с нового абзаца через тире, прямые кавычки
|
||||
вместо «ёлочек», дефис вместо тире. ОПИСАТЕЛЬНАЯ ось."""
|
||||
|
||||
RUBRIC_FREEDOM = """⛔ ЧТО ОШИБКОЙ НЕ ЯВЛЯЕТСЯ — ПРОЧТИ ДО ТОГО, КАК НАЧНЁШЬ СЧИТАТЬ.
|
||||
Живой русский язык — приоритет этого продукта, и переводчик ВПРАВЕ обращаться с буквой исходника
|
||||
свободно. НЕ штрафуй и не считай ошибкой:
|
||||
· перестановку предложений и частей предложения;
|
||||
· слияние или дробление абзацев; число абзацев и предложений совпадать с исходником НЕ обязано;
|
||||
· замену оборота на более живой русский, синонимическую замену, добавленную связку;
|
||||
· распутывание чужого синтаксиса в русскую фразу — это ОБЯЗАННОСТЬ переводчика, а не вольность;
|
||||
· убранную англоязычность или буквализм исходного языка там, где смысл сохранён;
|
||||
· вариант, который просто НРАВИТСЯ тебе меньше соседнего.
|
||||
Ошибка — только то, что ПЕРЕВИРАЕТ СМЫСЛ ИСХОДНИКА (ось ВЕРНОСТЬ) либо ЧЕГО НОСИТЕЛЬ РУССКОГО НЕ
|
||||
НАПИШЕТ (ось ЯЗЫК). Колеблешься между «вольность» и «искажение» — вернись к исходнику и проверь
|
||||
факт: не нашёл искажения факта, значит это вольность и она НЕ считается."""
|
||||
|
||||
RUBRIC_ZERO = """⚠ НОЛЬ — ЭТО УТВЕРЖДЕНИЕ, А НЕ ОТСУТСТВИЕ ОТВЕТА. Ноль означает «я сверил этот
|
||||
вариант с исходником по этой оси и дефектов не нашёл». Поэтому ПОЧЕМУ обязательно для КАЖДОЙ
|
||||
метки, в том числе при нулях: если по всем осям ноль, напиши «прочитано, дефектов не найдено».
|
||||
Пустое ПОЧЕМУ не принимается и на диск не попадает.
|
||||
⚠ И обратное так же важно: правило про вольность НЕ означает «ставь нули». Искажения факта бывают
|
||||
незаметны, потому что русский текст остаётся грамотным и связным, — именно их и надо ловить,
|
||||
сверяя с исходником, а не читая один перевод."""
|
||||
|
||||
|
||||
def rubric() -> str:
|
||||
return (f"Посчитай ОШИБКИ в каждом варианте по типам:\n\n{RUBRIC_TYPES}\n\n"
|
||||
f"{RUBRIC_FREEDOM}\n\n{RUBRIC_ZERO}")
|
||||
|
||||
OUT = Path.home() / "books" / "dovodka"
|
||||
WORK = Path.home() / "books" / "judging" / "z17"
|
||||
KEYD = OUT / "blind-keys-z17"
|
||||
SALT = "z17-2026-08-16"
|
||||
PER_SESSION = 4
|
||||
SETS = ("p1", "p2")
|
||||
AX = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА")
|
||||
CARRY = ("ВЕРНОСТЬ", "ЯЗЫК") # несущая сумма (эррата П-5); ТЕРМИН отдан банк-гейту, ФОРМА описательна
|
||||
NL = chr(10)
|
||||
|
||||
# ⚠ Посадки грубого декоя берутся ТЕ ЖЕ, что видели судьи фазы Д, — иначе гейт «грубый декой
|
||||
# грубее маржевого» сертифицировал бы не тот текст. Файл копируется, а не читается из чужого
|
||||
# каталога: чужой ключ трогать нельзя даже на чтение-по-ссылке.
|
||||
KEYD.mkdir(parents=True, exist_ok=True)
|
||||
_SRC_PLANTS = OUT / "blind-keys-d4" / "PLANTS.json"
|
||||
if _SRC_PLANTS.exists() and not (KEYD / "PLANTS.json").exists():
|
||||
(KEYD / "PLANTS.json").write_text(_SRC_PLANTS.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
AJ.KEYS = KEYD
|
||||
|
||||
BATTLE = ("ZD", "ZP", "Z8", "Z8R", "Z1") # армы, стоящие в ОБОИХ наборах
|
||||
FLOOR = {"p1": "Z0", "p2": "ZF"} # половины пола, разведённые по наборам
|
||||
CTRL = ("CTRLdecoy", "CTRLmargin")
|
||||
SRC_LANG = {"zh": "китайском", "en": "английском"}
|
||||
MARGIN_MIN = 2
|
||||
|
||||
_axre = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE)
|
||||
_whyre = re.compile(r"^[ТT](\d+)-ПОЧЕМУ:(.*)$", re.MULTILINE)
|
||||
|
||||
|
||||
def _sud(pair: str):
|
||||
"""`sud` разбирает ось из argv на уровне модуля — грузится ОДИН раз под нужную ось.
|
||||
|
||||
⚠ `--wide-plants` ОБЯЗАТЕЛЕН И ПЕРЕДАЁТСЯ ЯВНО (починка по ревью). Хендофф §5.8: классы
|
||||
маржевых посадок ПАР-ЗАВИСИМЫ, узкий (китайский) список на английском ложится 5 из 16, а
|
||||
рабочий класс «подмена принадлежности» даёт 11 из 16. Флаг разрешён «только для новых
|
||||
эмиссий» — заход и есть новая эмиссия. Без него гейт чувствительности П-2, то самое право
|
||||
сказать «не хуже», встал бы на пять точек вместо одиннадцати, а ширина пачек прыгала бы 7/8.
|
||||
"""
|
||||
axis = "d4" if pair == "zh" else "d3"
|
||||
key = f"sud_{axis}"
|
||||
if key in sys.modules:
|
||||
return sys.modules[key]
|
||||
saved = list(sys.argv)
|
||||
sys.argv = [saved[0], f"--axis={axis}", "--wide-plants"]
|
||||
try:
|
||||
m = _load(key, ZONE / "sud.py")
|
||||
finally:
|
||||
sys.argv = saved
|
||||
AJ.KEYS = KEYD # `sud` перенастраивает риг на СВОЙ ключ — вернуть
|
||||
return m
|
||||
|
||||
|
||||
def texts_of(pair: str) -> dict[str, dict[str, str]]:
|
||||
"""{uid: {арм: текст}} по выбранным единицам пары. Пара включается ОДИН раз, здесь."""
|
||||
us = Z.chosen(pair) # внутри вызывает wire() своей пары
|
||||
out = {}
|
||||
for u in us:
|
||||
uid = u["uid"]
|
||||
d = {a: Z.free_text(pair, a, uid) for a in ("ZD", "Z0", "ZP")}
|
||||
for a in ("Z8", "Z8R", "Z1", "ZF"):
|
||||
d[a] = Z.cell(Z.tag(pair, a, uid))
|
||||
d["_src"] = u["source"]
|
||||
out[uid] = d
|
||||
return out
|
||||
|
||||
|
||||
def donor_arm(half: str, idx: int) -> str:
|
||||
"""Арм-донор контролей ЭТОЙ пачки.
|
||||
|
||||
⚠⚠ ПОЧИНКА ПО РЕВЬЮ, И ЭТО БЫЛ САМЫЙ ДОРОГОЙ ИЗ ЕГО НАХОДОК. Прежняя редакция брала донором
|
||||
`Z0` по чётности единицы — включая пачки НАБОРА p2, где `Z0` стоять не должен вовсе: он
|
||||
половина шумового пола, живущая в p1. Проверено исполнением: декой в p2 совпадал с `Z0` на
|
||||
0.9973 и с `ZF` на 0.138. То есть судья p2 читал `ZF` рядом с текстом, на 99.7% совпадающим
|
||||
со второй половиной пола, — ровно то, что этот же файл в своей шапке называет обесцениванием
|
||||
порога целиком. Порог, а на нём оба вердикта «не различимо», стояли бы на испорченном контроле.
|
||||
Лечение: донор всегда берётся ИЗ АРМОВ СВОЕЙ ПОЛОВИНЫ; чередование по чётности сохранено, но
|
||||
между `Z8R` и половиной пола ЭТОГО набора.
|
||||
"""
|
||||
return "Z8R" if idx % 2 else FLOOR[half]
|
||||
|
||||
|
||||
def panel(pair: str, uid: str, tx: dict[str, str], half: str, idx: int) -> dict[str, str]:
|
||||
S = _sud(pair)
|
||||
need = list(BATTLE) + [FLOOR[half]]
|
||||
if not all(tx.get(a, "").strip() for a in need):
|
||||
return {}
|
||||
out = {a: tx[a] for a in need}
|
||||
donor = tx[donor_arm(half, idx)]
|
||||
dec, n_d = AJ._plant(donor)
|
||||
if n_d >= MARGIN_MIN:
|
||||
out["CTRLdecoy"] = dec
|
||||
mg, n_m = S.margin_plant(donor)
|
||||
if n_m >= MARGIN_MIN:
|
||||
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 = """Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на {lang} и {n} независимых русских перевода этого же фрагмента под слепыми метками.
|
||||
|
||||
{rubric}
|
||||
|
||||
Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника — не сравнивая варианты между собой.
|
||||
Порядок вариантов ничего не значит и специально перемешан.
|
||||
|
||||
Числа — это СЧЁТ ошибок, а не оценка по шкале. Две одинаковые ошибки в одном варианте — это два.
|
||||
|
||||
Ответ ЗАПИШИ ФАЙЛОМ в {path} — ровно в таком виде и ничем больше, блок на каждую метку:
|
||||
|
||||
{skeleton}
|
||||
ПОЧЕМУ для НЕНУЛЕВОЙ оси: короткая ЦИТАТА из текста через « | ».
|
||||
Число без цитаты не принимается и на диск не попадает.
|
||||
"""
|
||||
|
||||
|
||||
def emit() -> None:
|
||||
# ⚠ ЭМИССИЯ НЕ ПЕРЕПИСЫВАЕТ КЛЮЧ, ЗА КОТОРЫМ УЖЕ ЕСТЬ ОТВЕТЫ (починка по ревью). Раскладка
|
||||
# меток — чистая функция соли, uid и НАБОРА армов; повторная эмиссия при изменившемся составе
|
||||
# панели переписала бы ключ, и разбор сопоставил бы ответы с ЧУЖИМИ армами. Ровно так в фазе Д
|
||||
# вердикт `E0/D0` съехал с +1.56 на +0.38. Файл защищался от этого только словами в докстринге.
|
||||
got = [f for d in list(WORK.glob("*/p?-answers")) + list(WORK.glob("*/answers"))
|
||||
for f in d.glob("*.txt")]
|
||||
if got and (KEYD / "z17-KEY.json").exists():
|
||||
raise SystemExit(
|
||||
f"⛔ ЭМИССИЯ ОТКАЗАНА: за ключом `{KEYD / 'z17-KEY.json'}` уже лежит {len(got)} "
|
||||
"ответов.\n Переэмиссия перепишет раскладку меток, и разбор сопоставит эти ответы\n"
|
||||
" с ДРУГИМИ армами. Новый состав панели = НОВАЯ соль и новый ключ.")
|
||||
# ⚠ ГЕЙТ БЛИЗНЕЦА ДЛЯ ПОЛА (починка по ревью). Половины `Z0`/`ZF` лежат в РАЗНЫХ пачках,
|
||||
# поэтому панельная проверка побайтных дублей их не видит по построению. Побайтно равная пара
|
||||
# даёт нулевую разность, ЗАНИЖАЕТ sd и с ним порог различимости — то есть делает вердикты
|
||||
# смелее, чем данные позволяют. Ровно этот класс («близнец») уже стоил фазе перекупки
|
||||
# японского пола. Такие единицы из пола исключаются и называются числом.
|
||||
twins: dict[str, list[str]] = {}
|
||||
key: dict[str, dict] = {
|
||||
"_проход": "z17", "_рубрика": "починенная 16.08: вольность не штрафуется",
|
||||
"_семейство": ["Z8/ZD", "Z8R/Z0", "Z1/Z0", "ZP/Z0"],
|
||||
"_контроль": ["Z0/ZD"], "_пол": ["Z0(p1) против ZF(p2)"]}
|
||||
made = collections.Counter()
|
||||
for pair in Z.PAIRS:
|
||||
tx_all = texts_of(pair)
|
||||
twins[pair] = [u for u, tx in tx_all.items()
|
||||
if tx.get("Z0", "").strip() and tx["Z0"] == tx.get("ZF", "")]
|
||||
if twins[pair]:
|
||||
print(f" ⚠ {pair}: половины пола ПОБАЙТНО РАВНЫ на {len(twins[pair])} единицах — "
|
||||
"они исключаются из пола (иначе порог занижен)")
|
||||
for half in SETS:
|
||||
(WORK / pair / f"{half}-tasks").mkdir(parents=True, exist_ok=True)
|
||||
(WORK / pair / f"{half}-answers").mkdir(parents=True, exist_ok=True)
|
||||
for idx, (uid, tx) in enumerate(sorted(tx_all.items())):
|
||||
for half in SETS:
|
||||
p = panel(pair, uid, tx, half, idx)
|
||||
if not p:
|
||||
miss = [a for a in list(BATTLE) + [FLOOR[half]] if not tx.get(a, "").strip()]
|
||||
print(f" ⚠ {pair}/{uid}/{half}: панель неполна ({', '.join(miss)}) — пропуск")
|
||||
continue
|
||||
lay = layout(uid, half, tuple(p))
|
||||
tok = hashlib.sha256(f"{SALT}|{pair}|{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(lang=SRC_LANG[pair], n=len(lay), rubric=rubric(),
|
||||
path=WORK / pair / f"{half}-answers" / f"{tok}.txt",
|
||||
skeleton=skel),
|
||||
"", "=" * 60, "ИСХОДНИК:", tx["_src"]]
|
||||
for lab, arm in lay.items():
|
||||
body += ["", "=" * 60, f"{lab}:", p[arm]]
|
||||
(WORK / pair / f"{half}-tasks" / f"{tok}.txt").write_text("\n".join(body),
|
||||
encoding="utf-8")
|
||||
key[tok] = {lab: {"arm": a, "uid": uid, "half": half, "pair": pair,
|
||||
# донор нужен разбору: гейт декоя обязан сравнивать контроль
|
||||
# с ТЕМ, из чего он сделан, а не с эталоном (починка по ревью)
|
||||
"donor": donor_arm(half, idx) if a.startswith("CTRL") else "",
|
||||
"kind": "контроль" if a.startswith("CTRL") else "боевой",
|
||||
"sig": hashlib.sha256(p[a].encode()).hexdigest()[:12]}
|
||||
for lab, a in lay.items()}
|
||||
made[pair] += 1
|
||||
key["_пол_близнецы"] = twins
|
||||
(KEYD / "z17-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
|
||||
encoding="utf-8")
|
||||
(KEYD / "SALT-z17.txt").write_text(SALT + "\n", encoding="utf-8")
|
||||
print(f"заданий: " + " · ".join(f"{p} {n}" for p, n in made.items()))
|
||||
print(f"ключ → {KEYD} (судье НЕ давать)")
|
||||
for pair in made:
|
||||
toks = [t for t, v in key.items() if not t.startswith("_") and v
|
||||
and next(iter(v.values()))["pair"] == pair]
|
||||
for half in SETS:
|
||||
ht = [t for t in toks if next(iter(key[t].values()))["half"] == half]
|
||||
for i in range(0, len(ht), PER_SESSION):
|
||||
print(f" {pair}/{half} сессия {i // PER_SESSION + 1}: "
|
||||
f"{WORK / pair / f'{half}-tasks'} → " + " ".join(ht[i:i + PER_SESSION]))
|
||||
|
||||
|
||||
def read(root: Path | None = None) -> tuple[dict, list]:
|
||||
root = root or WORK
|
||||
key = json.loads((KEYD / "z17-KEY.json").read_text(encoding="utf-8"))
|
||||
out, bad = {}, []
|
||||
dirs = list(root.glob("*/p?-answers")) + list(root.glob("*/answers")) + [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}: ПУСТОЕ обоснование — клетка снята")
|
||||
continue
|
||||
# ⚠ КЛЮЧ НЕСЁТ НАБОР. Прежде ключом было `(пара, единица, арм)`, а боевые армы
|
||||
# стоят в ОБЕИХ пачках: вторая прочитанная молча затирала первую, и какая из двух
|
||||
# оценок выживет, решал порядок файловой системы. Проверено исполнением: из пары
|
||||
# оценок 7 и 1 выживала семёрка. Свой же риг фазы (`itog_d4`) ключует с набором.
|
||||
out[(meta["pair"], meta["uid"], meta["arm"], meta["half"])] = dict(
|
||||
ax, _donor=meta.get("donor", ""))
|
||||
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 _carry(ax: dict) -> int:
|
||||
return sum(ax.get(a, 0) for a in CARRY)
|
||||
|
||||
|
||||
def score(pairs: list[str] | None = None) -> int:
|
||||
"""Свод захода.
|
||||
|
||||
⚠ ВСЕ КОНТРАСТЫ СЧИТАЮТСЯ ВНУТРИ ОДНОГО НАБОРА (починка по ревью). Боевые армы стоят в обеих
|
||||
пачках единицы, и раньше вторая прочитанная затирала первую — вердикт зависел от порядка
|
||||
файловой системы. Теперь для каждой единицы берётся разность армов в ТОЙ ЖЕ пачке (значит
|
||||
сдвиг уровня сессии сокращается — главный конфаундер фазы), и по единице усредняются те
|
||||
наборы, где есть оба арма. Пол — единственная величина, которая МЕЖсессионна намеренно:
|
||||
`Z0` живёт в p1, `ZF` в p2, и их разность как раз и несёт межсессионную компоненту.
|
||||
"""
|
||||
sc, bad = read()
|
||||
if not sc:
|
||||
print("ответов нет — сначала --emit и судейство")
|
||||
return 1
|
||||
rc = 0
|
||||
for pair in (pairs or list(Z.PAIRS)):
|
||||
cells = {(u, a, h): ax for (p, u, a, h), ax in sc.items() if p == pair}
|
||||
if not cells:
|
||||
print(f"\n=== {pair}: ответов нет ===")
|
||||
continue
|
||||
uids = sorted({u for u, _, _ in cells})
|
||||
mine = [b for b in bad if True]
|
||||
print(f"\n=== ПАРА {pair} · единиц {len(uids)} · клеток {len(cells)} · "
|
||||
f"замечаний годности {len(mine)} ===")
|
||||
for b in mine[:6]:
|
||||
print(" ⚠", b)
|
||||
|
||||
def val(u, a, h):
|
||||
return _carry(cells[(u, a, h)]) if (u, a, h) in cells else None
|
||||
|
||||
def paired(a, b):
|
||||
"""Разности по единицам, усреднённые по наборам, где присутствуют ОБА арма."""
|
||||
out = []
|
||||
for u in uids:
|
||||
d = [val(u, a, h) - val(u, b, h) for h in SETS
|
||||
if val(u, a, h) is not None and val(u, b, h) is not None]
|
||||
if d:
|
||||
out.append(st.mean(d))
|
||||
return out
|
||||
|
||||
vals = [_carry(ax) for (u, a, h), ax in cells.items() if not a.startswith("CTRL")]
|
||||
zeros = sum(1 for v in vals if v == 0) / max(len(vals), 1)
|
||||
ok_d = zeros < 0.25
|
||||
rc |= not ok_d
|
||||
print(f"ГЕЙТ ПЛОТНОСТИ: ошибок/клетку {st.mean(vals):.2f} · доля нулей {zeros:.0%} "
|
||||
f"→ {'ГОДНО' if ok_d else '⛔ ПОЛ ШКАЛЫ — пачка не несёт «не различимо»'}")
|
||||
|
||||
twin = set(json.loads((KEYD / "z17-KEY.json").read_text(encoding="utf-8"))
|
||||
.get("_пол_близнецы", {}).get(pair, []))
|
||||
if twin:
|
||||
print(f" ⚠ из пола исключены {len(twin)} единиц: половины побайтно равны")
|
||||
fl = [val(u, "Z0", "p1") - val(u, "ZF", "p2") for u in uids
|
||||
if u not in twin
|
||||
and val(u, "Z0", "p1") is not None and val(u, "ZF", "p2") is not None]
|
||||
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}")
|
||||
if not fl:
|
||||
print(" ⛔ ПОЛА НЕТ — порога различимости не существует, и ни один вердикт "
|
||||
"«различимо / не различимо» этой пачкой не выносится")
|
||||
rc = 1
|
||||
|
||||
# ⚠ ДЕКОЙ СРАВНИВАЕТСЯ СО СВОИМ ДОНОРОМ, А НЕ С ЭТАЛОНОМ (починка по ревью): донор
|
||||
# чередуется по чётности единицы, и разность «контроль минус эталон» подмешивала бы в
|
||||
# сертификат чувствительности разницу самих армов.
|
||||
for name, arm in (("ГРУБЫЙ ДЕКОЙ", "CTRLdecoy"),
|
||||
("МАРЖЕВЫЙ ДЕКОЙ (гейт П-2)", "CTRLmargin")):
|
||||
d, skipped = [], 0
|
||||
for u in uids:
|
||||
for h in SETS:
|
||||
if (u, arm, h) not in cells:
|
||||
continue
|
||||
dn = cells[(u, arm, h)].get("_donor") or ""
|
||||
if val(u, dn, h) is None:
|
||||
skipped += 1
|
||||
continue
|
||||
d.append(_carry(cells[(u, arm, h)]) - val(u, dn, h))
|
||||
if d:
|
||||
good = st.mean(d) > thr and thr > 0
|
||||
rc |= (arm == "CTRLmargin") and not good
|
||||
print(f"{name}: n={len(d)} среднее над СВОИМ донором {st.mean(d):+.2f} против "
|
||||
f"порога {thr:.2f} → {'ПРОЙДЕН' if good else '⛔ НЕ ПРОЙДЕН'}"
|
||||
+ (f" · пропущено без донора {skipped}" if skipped else ""))
|
||||
else:
|
||||
print(f"{name}: не сажался ни в одной пачке — гейт чувствительности ПУСТ")
|
||||
rc |= arm == "CTRLmargin"
|
||||
|
||||
print(f"\n{'контраст':14s}{'ед.':>5s}{'перевес':>9s}{'p':>9s} вердикт")
|
||||
for a, b in (("Z8", "ZD"), ("Z8R", "Z0"), ("Z1", "Z0"), ("ZP", "Z0"), ("Z0", "ZD")):
|
||||
d = paired(b, a) # положительный перевес = арм `a` ЛУЧШЕ (ошибок меньше)
|
||||
if not d:
|
||||
continue
|
||||
m = st.mean(d)
|
||||
v = ("РАЗЛИЧИМО" if thr and abs(m) > thr and _sign_p(d) < 0.05 else "не различимо")
|
||||
print(f"{a + ' vs ' + b:14s}{len(d):5d}{m:+9.2f}{_sign_p(d):9.4f} {v}")
|
||||
|
||||
print(f"\n{'арм':12s}{'несущая':>9s}" + "".join(f"{x:>11s}" for x in AX))
|
||||
for arm in list(BATTLE) + ["Z0", "ZF", *CTRL]:
|
||||
v = [ax for (u, a, h), ax in cells.items() if a == arm]
|
||||
if not v:
|
||||
continue
|
||||
print(f"{arm:12s}{st.mean([_carry(x) for x in v]):9.2f}"
|
||||
+ "".join(f"{st.mean([x.get(y, 0) for x in v]):11.2f}" for y in AX))
|
||||
print("⚠ Вывод ОПИСАТЕЛЬНЫЙ: при n=16 MDE 2.41 против цели 1.50 (`power.py`), "
|
||||
"решение владельца 16.08 — не добирать. Числа НЕСРАВНИМЫ с дугой 19→23: рубрика другая.")
|
||||
return rc
|
||||
|
||||
|
||||
def canon() -> int:
|
||||
"""Канон-гейт — им и только им меряется `Z7`. $0, детерминированно, без судьи."""
|
||||
print("ПОКРЫТИЕ БАНКА (доля терминов исходника, переданных канонной формой)\n")
|
||||
for pair in Z.PAIRS:
|
||||
tx_all = texts_of(pair)
|
||||
rows = collections.defaultdict(list)
|
||||
for uid, tx in tx_all.items():
|
||||
src = tx["_src"]
|
||||
for arm in ("ZD", "Z0", "ZP", "Z8", "Z8R", "Z1"):
|
||||
if tx.get(arm, "").strip():
|
||||
rows[arm].append(len(Z.C.canon_gaps(src, tx[arm])))
|
||||
z7 = Z.cell(Z.tag(pair, "Z7", uid))
|
||||
if z7.strip():
|
||||
rows["Z7"].append(len(Z.C.canon_gaps(src, z7)))
|
||||
print(f"=== {pair} ===")
|
||||
for arm in ("ZD", "Z0", "ZP", "Z7", "Z8", "Z8R", "Z1"):
|
||||
if rows[arm]:
|
||||
print(f" {arm:5s} потерянных терминов на единицу {st.mean(rows[arm]):5.2f} "
|
||||
f"(единиц {len(rows[arm])})")
|
||||
if rows["ZP"] and rows["Z7"]:
|
||||
print(f" ⇒ канон-фиксер поверх однопроходки: "
|
||||
f"{st.mean(rows['ZP']):.2f} → {st.mean(rows['Z7']):.2f} потерь на единицу")
|
||||
return 0
|
||||
|
||||
|
||||
def selftest() -> int:
|
||||
fails = []
|
||||
ok = lambda c, m: fails.append(m) if not c else None # noqa: E731
|
||||
PAR = _load("paritet_s", ZONE / "paritet.py")
|
||||
ok(PAR.main() == 0, "гейт паритета обвязок красный — рубрика разъехалась между семействами")
|
||||
for pair in Z.PAIRS:
|
||||
tx_all = texts_of(pair)
|
||||
ok(len(tx_all) == Z.N_UNITS, f"{pair}: единиц {len(tx_all)}, ожидалось {Z.N_UNITS}")
|
||||
uid, tx = sorted(tx_all.items())[0]
|
||||
for half in SETS:
|
||||
p = panel(pair, uid, tx, half, 0)
|
||||
# ⚠ ПУСТАЯ ПАНЕЛЬ — ЭТО ПРОВАЛ, А НЕ ПРОПУСК. Прежняя редакция делала `continue`, и
|
||||
# пока клетки захода не куплены, ВСЕ проверки панели молча не исполнялись, а селфтест
|
||||
# печатал «провалов 0». Гейт, который не может упасть, ничего не сторожит.
|
||||
ok(bool(p), f"{pair}/{half}: панель не собирается — клетки захода ещё не куплены "
|
||||
f"(нет: {', '.join(a for a in list(BATTLE) + [FLOOR[half]] if not tx.get(a, '').strip())})")
|
||||
if not p:
|
||||
continue
|
||||
sigs = [hashlib.sha256(v.encode()).hexdigest() for v in p.values()]
|
||||
ok(len(sigs) == len(set(sigs)), f"{pair}/{half}: в панели побайтные дубли")
|
||||
ok(("Z0" in p) == (half == "p1"), f"{pair}/{half}: Z0 стоит не в своём наборе")
|
||||
ok(("ZF" in p) == (half == "p2"), f"{pair}/{half}: ZF стоит не в своём наборе")
|
||||
ok("Z7" not in p, f"{pair}/{half}: Z7 попал в судейство, а пре-рег это запрещает")
|
||||
lay = layout(uid, half, tuple(p))
|
||||
ok(len(lay) == len(set(lay.values())), "раскладка не биективна")
|
||||
ok(layout(uid, half, tuple(sorted(lay.values()))) == lay, "раскладка не воспроизводима")
|
||||
p1 = panel(pair, uid, tx, "p1", 0)
|
||||
p2 = panel(pair, uid, tx, "p2", 0)
|
||||
if p1 and p2:
|
||||
ok(len(p1) == len(p2), f"{pair}: ширина наборов расходится ({len(p1)}/{len(p2)}) — "
|
||||
"разница наборов начнёт мерить усталость судьи, а не шум")
|
||||
# шапка задания обязана нести правило владельца — паритет П-4 требует его В ЗАДАНИИ
|
||||
head = HEAD.format(lang=SRC_LANG[pair], n=8, rubric=rubric(), path="x", skeleton="y")
|
||||
ok("ОШИБКОЙ НЕ ЯВЛЯЕТСЯ" in head and "перестановку предложений" in head,
|
||||
f"{pair}: правило владельца о вольности не в шапке ЗАДАНИЯ (норма паритета П-4: "
|
||||
"правила счёта в обвязке до одного из семейств в паке 23 не доехали)")
|
||||
ok("НЕ означает «ставь нули»" in head, f"{pair}: нет сторожа против сползания в нули")
|
||||
ok(_sign_p([1, 1, 1, 1, 1]) < 0.07, "знаковый тест сломан")
|
||||
ok("_пол_близнецы" in emit.__code__.co_consts or True, "")
|
||||
for pair in Z.PAIRS: # донор контролей обязан быть в СВОЕЙ половине
|
||||
for h in SETS:
|
||||
for idx in (0, 1):
|
||||
ok(donor_arm(h, idx) in (list(BATTLE) + [FLOOR[h]]),
|
||||
f"{pair}/{h}: донор декоя {donor_arm(h, idx)} не из армов своей половины")
|
||||
ok((KEYD / "PLANTS.json").exists(), "посадки грубого декоя не заведены")
|
||||
for m in fails:
|
||||
print("ПРОВАЛ:", m)
|
||||
print(f"селфтест z17: провалов {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([x for x in a[1:] if x in Z.PAIRS] or None))
|
||||
elif a[0] == "--canon":
|
||||
sys.exit(canon())
|
||||
elif a[0] == "--selftest":
|
||||
sys.exit(selftest())
|
||||
else:
|
||||
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")
|
||||
Loading…
Add table
Reference in a new issue