#!/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 chtenie.py --emit-owner | --score-owner — пакет ВЛАДЕЛЬЦУ (4 варианта, «жирная глава») """ 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 единиц из ОТСУЖЕННОГО набора — чтобы порядки читателя и счётчика сравнивались на одних и тех же текстах. Отбор детерминирован своей солью. ⚠ ОТБОР ИДЁТ ТОЛЬКО ПО ЕДИНИЦАМ С ПОЛНОЙ ПАНЕЛЬЮ. На китайской оси четыре обогащённых черновика забракованы эхо-гейтом, значит арма «обогащённый черновик + перепис» на них нет вовсе. Прежняя редакция брала восемь единиц из всех отсуженных и одна приходила неполной — а ранги, снятые на панелях разной ширины, между собой несравнимы. Это сужение ВЫБОРКИ ЧТЕНИЯ, а не панели: сама панель не режется, просто читаются те главы, где она есть целиком. """ tx_all = ZS.texts_of(pair) uids = sorted(u for u in tx_all if all(tx_all[u].get(a, "").strip() for a in PANEL)) 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` это учитывают. Молча выпавший ответ читателя стоил бы целой сессии. # ⚠ ФОРМА ОТВЕТА ЧЕЛОВЕКА ШИРЕ, ЧЕМ Я ПРЕДПИСАЛ, И ЭТО ДЕФЕКТ РАЗБОРЩИКА, А НЕ ОТВЕТА. # Владелец 17.08 ответил «Т3 > Т1 > Т2 > Т4» под заголовком «Порядок читаемости» и # «**ПОРЯДОК ЧИТАЕМОСТИ:** `Т1 > Т4 = Т3 > Т2`» — обе формы законны и обе прежним регексом # не читались: он требовал НОМЕРА отрывка сразу после слова. Инструмент, который не берёт # честный ответ, обесценивает работу читателя, а не читателя. # Теперь номер необязателен (нет — нумеруем по порядку появления), markdown-разметка и # обратные кавычки снимаются, латинская T принимается наравне с кириллической. _ORD_NUM = re.compile(r"ПОРЯДОК[^\n:]*?(\d+)\s*:", re.IGNORECASE) _ORD = re.compile(r"((?:[ТT]\d+\s*[>=]\s*)+[ТT]\d+)") def orders(txt: str) -> dict[str, str]: """{номер отрывка: строка порядка}. Номер берётся из подписи «ПОРЯДОК … N:», если она есть; если её нет — порядки нумеруются по появлению в тексте. ⚠ Заведено 17.08 после того, как разборщик не взял ЧЕСТНЫЙ ответ владельца: он написал порядок под заголовком «## 1. Порядок читаемости», без обязательного номера сразу за словом. Требовать от человека машинной формы и молча терять его работу — дефект инструмента. ⚠ Строки порядка ищутся ПО ВСЕМУ тексту, но берутся только те, что стоят ПОСЛЕ слова «ПОРЯДОК» либо первыми в файле: иначе в разбор попали бы упоминания меток из комментариев вроде «Т3 и Т1 стоят близко». ⚠ ВТОРАЯ ПОЧИНКА ТОГО ЖЕ ДНЯ: порядок ВЕРНОСТИ отбрасывается. Владелец дал по английской паре две строки — «ПОРЯДОК ЧИТАЕМОСТИ» и «ПОРЯДОК ВЕРНОСТИ», — а разборщик принял вторую за порядок второго отрывка и молча отсудил ею чужую главу. Прибор мерит ЧИТАЕМОСТЬ; верность считается детерминированно и в этот разбор не входит. """ out: dict[str, str] = {} for i, m in enumerate(_ORD.finditer(txt), 1): head = txt[max(0, m.start() - 200):m.start()] if "ВЕРНОСТ" in head.upper(): continue nm = _ORD_NUM.findall(head) key = nm[-1] if nm else str(len(out) + 1) out.setdefault(key, m.group(1)) return out 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 = orders(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 # ── ПАКЕТ ДЛЯ ВЛАДЕЛЬЦА: он читает сам, человеческим глазом ───────────────────────────────────── # ⚠ ЗАЧЕМ ОТДЕЛЬНЫЙ РЕЖИМ, А НЕ ТОТ ЖЕ ПАКЕТ. Агентские пачки собраны под МАШИННОГО читателя: # восемь глав на ось, семь вариантов, двадцать заданий. Владелец дважды не стал такое читать и # сказал прямо: «чтоб не слишком много было, давай какую-нибудь жирную главу на язык». Значит # ограничение здесь — ЕГО ВРЕМЯ, и панель режется под него, а не под статистику. # # ЧТО ОСТАВЛЕНО И ПОЧЕМУ РОВНО ЭТО (четыре варианта вместо семи): # Z0 боевая связка — то, что стоит в продакшене; без неё сравнивать не с чем; # ZF ВТОРАЯ генерация той же связки — контроль шума. Если владелец разведёт их далеко, его # порядок есть шум, и «победа» любого арма внутри этого расстояния ничего не значит. # Это единственный способ узнать, различает ли он вообще что-то, — и он же самый дешёвый; # ZP однопроходка — один вызов вместо двух, и по банку с канон-фиксером она впереди связки; # Z1 критик → полный перепис — ставка владельца в сильнейшей форме. # Голый черновик НЕ включён намеренно: оба прибора и оба машинных читателя ставят его последним # с огромным отрывом, спора нет, а глава черновика — это лишние 7 тысяч знаков его чтения. # Обогащённый черновик с переписом тоже снят: от боевой связки он неотличим ни одним прибором. # # ⚠ ДВА ВОПРОСА, КАК ПРОСИЛ ВЛАДЕЛЕЦ («лучше по художке и при этом лучше держит канон смысл»), но # ЧЕСТНО РАЗВЕДЁННЫЕ. Читаемость может назвать только он. Верность исходнику на КИТАЙСКОМ он # назвать не может — для этого надо читать китайский, — поэтому там его просят отметить лишь то, # что видно без исходника: провалы связности, непонятные места, оборванные мысли. На английском # исходник приложен, и сверка возможна, но объявлена необязательной. Канон терминов при этом уже # посчитан детерминированно и в пакет НЕ кладётся до его ответа — иначе это подсказка. OWNER_PANEL = ("Z0", "ZF", "ZP", "Z1") OWNER_DIR = Path.home() / "books" / "chtenie-vladeltsa-z17" OWNER_KEY = KEYD / "chtenie-vladeltsa-z17-KEY.json" OWNER_SALT = "vladelets-z17-2026-08-17" OWNER_N = {"zh": 1, "en": 2} # «жирная глава на язык»; английские главы втрое короче OWNER_HEAD = """# Что ты читаешь и зачем — коротко Ниже {n} перевода одного и того же отрывка. Сделаны они разными способами: где-то один вызов модели, где-то два, где-то с промежуточным критиком. **Какой чем сделан — не сказано намеренно.** Метки перемешаны, расшифровка лежит в отдельном файле и до твоего ответа не открывается. **Зачем это нужно.** Наш измеритель считает локальные ошибки — искажения смысла, кальки, сбитую вёрстку. Он дёшев и воспроизводим, но у него встроенная слепота: текст может быть без единой ошибки и при этом мёртвый. На последнем прогоне это подтвердилось числом — порядок по счёту ошибок и порядок по читаемости совпали лишь на треть, а вариант с наименьшим числом ошибок читался почти хуже всех. Твой ответ решает, верить ли дальше счётчику или чтению. **Одна из четырёх версий — контроль.** Две из них сделаны ОДНИМ И ТЕМ ЖЕ способом, просто модель вызвана дважды. Какие именно — не скажу. Если ты поставишь их рядом, значит ты различаешь способы; если разведёшь далеко, значит разница между способами меньше собственного разброса модели, и тогда никакая «победа» в этом замере ничего не стоит. Специально их искать не надо — просто читай. Метки сквозные: `Т1` в первом отрывке и `Т1` во втором — ОДИН И ТОТ ЖЕ способ. Поэтому порядок можно называть и по каждому отрывку, и один общий на всю пару. ⚠ **Что может сбить.** Вёрстка выдаёт метод: часть вариантов сливает построчный исходник в русские абзацы, часть сохраняет разбивку. Это видно ДО чтения — не давай форме решать за тебя. --- ## Что записать — в файл `{answer}` 1. **ПОРЯДОК ЧИТАЕМОСТИ** — строкой: `Т3 > Т1 > Т2 > Т4`, от лучшего к худшему. Неразличимые соединяй через `=`. Это главный вопрос: какой текст ты взял бы в книгу. 2. **ПОРЯДОК ВЕРНОСТИ** — {fidelity} 3. **По фразе на вариант**: почему. Годится и «серо, но гладко», и «живой ритм, но во втором абзаце теряется, кто кому говорит». Если какой-то вариант покажется откровенно машинным — скажи прямо, это самое ценное. --- """ # ⚠ Верность исходнику владелец может назвать только там, где читает исходный язык. На китайском # он его не читает, и просить «сверь смысл» значило бы просить невыполнимого — а невыполнимая # просьба в задании обесценивает и выполнимую часть. Поэтому на zh просят то, что видно БЕЗ # исходника: провалы связности. Это не подмена вопроса, а его честная граница. OWNER_FID = { "zh": ("исходник китайский, и сверять его тебе нечем — поэтому этот пункт ПРОПУСТИ.\n" " Вместо него отметь то, что видно и без исходника: где текст теряет связность,\n" " где непонятно, кто что делает, где мысль обрывается. Такие места почти всегда\n" " и оказываются враньём про исходник."), "en": ("исходник приложен, английский. Если станешь сверять — назови порядок по верности\n" " отдельной строкой. Не станешь — пропусти, это необязательно."), } def emit_owner(only: list[str] | None = None) -> None: """Пакет владельцу. `only` — пересобрать ТОЛЬКО названные пары. ⚠⚠ ПЕРЕ-ЭМИССИЯ НЕ ИМЕЕТ ПРАВА ТРОНУТЬ ПАРУ, КОТОРУЮ ВЛАДЕЛЕЦ УЖЕ ПРОЧЁЛ. Раскладка меток есть чистая функция соли и набора армов; пересборка ЦЕЛОГО пакета переписала бы ключ и китайской пары тоже — а её ответ уже дан и расшифрован. Тогда та же расшифровка при следующем запуске дала бы ДРУГИЕ армы, и работа владельца превратилась бы в мусор молча. Ровно так фаза Д однажды потеряла вердикт `E0/D0` (+1.56 → +0.38). Поэтому ключ здесь ДОГРУЖАЕТСЯ, а не создаётся заново, и пары вне `only` не пересчитываются. ⚠ Прежний ответ по пересобираемой паре не удаляется, а уводится в архив: это улика — работа владельца, испорченная дефектом МОЕЙ сборки, а не его чтением. """ key: dict = {"_что": "слепое чтение ВЛАДЕЛЬЦЕМ, панель захода Д17", "_соль": OWNER_SALT, "_панель": list(OWNER_PANEL), "_контроль": "Z0 и ZF — две генерации ОДНОЙ боевой связки; развёл далеко = шум"} if OWNER_KEY.exists(): key = {**json.loads(OWNER_KEY.read_text(encoding="utf-8")), **key} OWNER_DIR.mkdir(parents=True, exist_ok=True) for pair, langname, srcname in (("zh", "КИТАЙСКАЯ", "китайский"), ("en", "АНГЛИЙСКАЯ", "английский")): if only and pair not in only: print(f" {pair}: не в списке пересборки — ключ и задание НЕ трогаются") continue prev = OWNER_DIR / f"ОТВЕТ-{pair}.md" if prev.exists() and _ORD.search(prev.read_text(encoding="utf-8")): arch = OWNER_DIR / f"ОТВЕТ-{pair}.ИСПОРЧЕННЫЙ-ПАКЕТ.md" arch.write_text(prev.read_text(encoding="utf-8"), encoding="utf-8") prev.unlink() print(f" {pair}: прежний ответ уведён в {arch.name} (улика, не удалён)") tx_all = ZS.texts_of(pair) # «жирная глава»: самая длинная из тех, где панель полна — длинная честнее короткой cand = [u for u in tx_all if all(tx_all[u].get(a, "").strip() for a in OWNER_PANEL)] cand.sort(key=lambda u: -sum(len(tx_all[u][a]) for a in OWNER_PANEL)) take = cand[:OWNER_N[pair]] ans = OWNER_DIR / f"ОТВЕТ-{pair}.md" body = [f"# {langname} ПАРА — слепое чтение\n", OWNER_HEAD.format(n=len(OWNER_PANEL), answer=ans, fidelity=OWNER_FID[pair])] for j, uid in enumerate(take, 1): # ⚠ РАСКЛАДКА ОДНА НА ВСЮ ПАРУ, А НЕ СВОЯ У КАЖДОГО ОТРЫВКА (починка 17.08). # Первая редакция солила раскладку номером главы, и метка `Т1` означала РАЗНЫЕ # армы в первом и втором английском отрывке. Владелец — как поступил бы любой # читатель — дал ОДИН сводный порядок на пару и говорил о «Т1» как об одном # варианте; сопоставить это с армами оказалось нечем, и английская половина # его работы пропала не по его вине. Машинным читателям своя раскладка на # единицу нужна (они судят поединично), человеку — вредна. lay = layout(pair, "OWNER-ALL-v2", "OWNER", OWNER_PANEL) body += [f"\n# ОТРЫВОК {j}\n", f"## Исходник ({srcname})\n", tx_all[uid]["_src"], ""] for lab, arm in lay.items(): body += ["\n---\n", f"## Отрывок {j}, вариант {lab}\n", tx_all[uid][arm], ""] key.setdefault(pair, {})[str(j)] = { "uid": uid, "метки": lay, "знаков": {a: len(tx_all[uid][a]) for a in OWNER_PANEL}} (OWNER_DIR / f"ЧТЕНИЕ-{pair}.md").write_text("\n".join(body), encoding="utf-8") if not ans.exists(): ans.write_text( f"# Ответ — {langname.lower()} пара\n\n" + "".join(f"ПОРЯДОК ЧИТАЕМОСТИ {j}: \n" for j in range(1, len(take) + 1)) + "\nЗаметки по вариантам:\n", encoding="utf-8") n = sum(len(tx_all[u][a]) for u in take for a in OWNER_PANEL) print(f" {pair}: глав {len(take)} · вариантов {len(OWNER_PANEL)} · {n // 1000} тыс. знаков") OWNER_KEY.write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8") print(f"\nпакет → {OWNER_DIR}") print(f"ключ → {OWNER_KEY} (НЕ открывать до ответа)") def score_owner() -> int: if not OWNER_KEY.exists(): print("ключа нет — сначала --emit-owner") return 1 key = json.loads(OWNER_KEY.read_text(encoding="utf-8")) sc, _bad = ZS.read() for pair in ("zh", "en"): f = OWNER_DIR / f"ОТВЕТ-{pair}.md" if not f.exists() or pair not in key: continue txt = f.read_text(encoding="utf-8") got = orders(txt) if not got: print(f"{pair}: порядка нет — ждём ответа владельца") continue print(f"\n=== {pair} ===") # ⚠ СТОРОЖ НЕОДНОЗНАЧНОЙ РАСШИФРОВКИ (заведён 17.08 по факту). Если в ключе у отрывков # РАЗНЫЕ раскладки меток, а читатель дал ОДИН сводный порядок, то механическое применение # этого порядка к каждой раскладке даёт столько же РАЗНЫХ расшифровок, сколько отрывков, и # все они одинаково «правдоподобны». Так и вышло с английской парой: один и тот же ответ # владельца дал «однопроходка первая» на первом отрывке и «однопроходка ПОСЛЕДНЯЯ» на # втором. Печатать это как результат — значит выдать артефакт разметки за замер. maps = [tuple(sorted(m["метки"].items())) for m in key[pair].values()] if len(set(maps)) > 1 and len(got) < len(key[pair]): print(" ⛔ РАСШИФРОВКА НЕОДНОЗНАЧНА И НЕ ВЫВОДИТСЯ. У отрывков РАЗНЫЕ раскладки") print(" меток, а порядок дан ОДИН сводный: одна и та же метка означает в них") print(" разные армы. Это дефект СБОРКИ ПАКЕТА, а не ответа читателя.") print(" Что уцелело — устойчивое к раскладке: какие метки читатель поставил") print(" РЯДОМ или связал знаком «=». Ниже только это.") for j, meta in key[pair].items(): if j not in got: continue r = _ranks(got[j]) pairs = [(a, b) for a in r for b in r if a < b and abs(r[a] - r[b]) <= 1] for a, b in pairs: arms = {meta["метки"].get(a), meta["метки"].get(b)} print(f" {a} и {b} рядом → в этом отрывке это {' и '.join(sorted(x for x in arms if x))}") continue for j, meta in key[pair].items(): if j not in got: continue r = _ranks(got[j]) rows = [(meta["метки"][lab], r[lab]) for lab in meta["метки"] if lab in r] print(f" отрывок {j} (глава {meta['uid']}):") for arm, rank in sorted(rows, key=lambda x: x[1]): errs = [ZS._carry(ax) for (pp, u, aa, _h), ax in sc.items() if pp == pair and aa == arm and u == meta["uid"]] e = f"{st.mean(errs):.1f}" if errs else "—" print(f" {rank:.1f} {arm:4s} ошибок по судье {e}") d = {a: rk for a, rk in rows} if "Z0" in d and "ZF" in d: gap = abs(d["Z0"] - d["ZF"]) print(f" КОНТРОЛЬ ШУМА: две генерации одной связки разведены на {gap:.1f} " f"позиции → {'порядок несёт сигнал' if gap <= 1 else 'ПОРЯДОК = ШУМ'}") 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 на инверсии") ok(orders("ПОРЯДОК 2: Т3 > Т1 > Т5") == {"2": "Т3 > Т1 > Т5"}, "нумерованный порядок") ok(list(orders("## Порядок читаемости\n\nТ3 > Т1 > Т2 > Т4").values()) == ["Т3 > Т1 > Т2 > Т4"], "ЧЕЛОВЕЧЕСКАЯ форма ответа без номера не читается — на ней уже терялась работа владельца") ok(list(orders("**ПОРЯДОК ЧИТАЕМОСТИ:** `Т1 > Т4 = Т3 > Т2`").values()) == ["Т1 > Т4 = Т3 > Т2"], "порядок в markdown-разметке не читается") 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()), "--emit-owner": lambda: emit_owner([x for x in a[1:] if x in ("zh", "en")] or None), "--score-owner": lambda: sys.exit(score_owner()), "--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()