Freeze phase D pre-reg erratum and harness repair: A2 arm built, battery wired with violation whitelist, per-contrast power with Student quantile, guard is a real stop
This commit is contained in:
parent
6594d3bd28
commit
8950352fa2
6 changed files with 761 additions and 79 deletions
|
|
@ -894,3 +894,88 @@ A6 dspro-ЧЕРНОВИК + оба контура ← носител
|
||||||
Топология «черновик → редактор» (решена эксп-21) · банк как фактор · выбор жильца черновой роли
|
Топология «черновик → редактор» (решена эксп-21) · банк как фактор · выбор жильца черновой роли
|
||||||
(решён эксп-22) · качество прозы `gemini` как продуктовый выбор — при $221 за книгу против $8 у
|
(решён эксп-22) · качество прозы `gemini` как продуктовый выбор — при $221 за книгу против $8 у
|
||||||
боевого он невозможен ни при каком исходе, меряется только гипотеза «аутлаер прозы» эксп-04.
|
боевого он невозможен ни при каком исходе, меряется только гипотеза «аутлаер прозы» эксп-04.
|
||||||
|
|
||||||
|
### Д0.11 ЭРРАТА ПРЕ-РЕГА (11.08, ДО докупок; история не переписывается)
|
||||||
|
|
||||||
|
Ревью 82 находок двумя направлениями плюс опровергатель на каждую находку (67 выжили) вскрыло, что
|
||||||
|
зафризенный Д0 расходится с кодом и что три арма куплены не теми, какими объявлены. Ниже — каждое
|
||||||
|
расхождение и его диспозиция. **Ни одно число выше НЕ правится задним числом**; правится код, а
|
||||||
|
таблицы Д0.3/Д0.5 читать через эту эррату.
|
||||||
|
|
||||||
|
**Э-1. Таблица Д0.3 напечатана ДО починки D-1 и коду не соответствует.** В отчёте `Д4 k=5 MDE 1.29`
|
||||||
|
и «Холм по пяти» (:805, :810); `power.py` держит `k=3` (первичное семейство по одному контрасту на
|
||||||
|
гипотезу). Ни одна версия кода числа 1.29 не даёт: это множитель Холма БЕЗ поправки ×1.23.
|
||||||
|
Действительна раскладка кода, а не таблицы отчёта.
|
||||||
|
|
||||||
|
**Э-2. ⛔ MDE считался по ОСИ при плановом n, а контрасты живут на РАЗНЫХ n.** `A6/A0` — носитель
|
||||||
|
H-1 — стоит на 16 единицах по построению: черновик dspro эксп-22 есть только у 16 старых единиц.
|
||||||
|
Мощность на него никто не считал. Это дословный класс провала en-оси эксп-22, ради недопущения
|
||||||
|
которого написан `power.py`. Введена поконтрастная таблица; статус объявляется по контрасту.
|
||||||
|
|
||||||
|
**Э-3. ⛔ Множитель мощности брал квантиль НОРМАЛИ, тогда как sd — ОЦЕНКА** (пол прошлого пака,
|
||||||
|
n=16, df=15). При честном t всё меняется: ось Д4 даёт MDE 1.64 против цели 1.50, а все три
|
||||||
|
первичных контраста — 1.67 · 1.67 · 2.32. **При заимствованном прайоре sd 2.12 ось Д4 ОПИСАТЕЛЬНАЯ.**
|
||||||
|
Несущей её делает только СВОЙ пол: при sd ≤ 1.91 контрасты `A1/A0` и `A3/A0` становятся несущими
|
||||||
|
(замеренный пол `tier` эксп-23 — 1.45, то есть шанс реален). `A6/A0` требует sd ≤ 1.37 и при n=16
|
||||||
|
не спасается ничем — H-1 остаётся описательной, пока база dspro не расширена на новые единицы.
|
||||||
|
⇒ **Порядок покупок изменён: СВОЙ ПОЛ ПЕРВЫМ.** Покупать армы, не зная пола, значит рисковать всей
|
||||||
|
сметой оси, которая может не дать вывода ни при каком исходе. Объявлено до покупки пола.
|
||||||
|
|
||||||
|
**Э-4. Арм A1 куплен БЕЗ батареи** (30 клеток, $0.2269). `battery.run_unit` получал `dict` вместо
|
||||||
|
`battery.Unit` и падал `AttributeError` на 32/32, ошибку глотал голый `except`. Флаги A1 = 100%
|
||||||
|
канон-гейт при объявленном «батарея + канон-гейт». Починено; при живой батарее мест 181 против 74.
|
||||||
|
⚠ Наивная починка была бы ХУЖЕ поломки: значения проверок — словари, и прежний фильтр
|
||||||
|
`isinstance(v,(int,float))` вымел бы в список мест ЗНАМЕНАТЕЛИ («проверка sentences дала 90»).
|
||||||
|
Введён явный whitelist полей-нарушений. **Старые 30 клеток пере-покупаются.**
|
||||||
|
|
||||||
|
**Э-5. Арм A2 не существовал.** `PR.translator_msgs(src)` звался с одним аргументом при сигнатуре
|
||||||
|
`(src, block, en=False)` → TypeError; сторож `hasattr` проверяет имя, а не арность, поэтому ветка
|
||||||
|
«арм пропущен, объявить в отчёте» была НЕДОСТИЖИМА. Вдобавок `block=None` снял бы банк-закон
|
||||||
|
D39.104. Собран уравненный мандат (строка 153 бэклога): промт переводчика ПЛЮС мандатные части
|
||||||
|
промта редактора, включая дискурс-перевёрстку и правило чэнъюй, плюс банк-закон.
|
||||||
|
|
||||||
|
**Э-6. «A6 = оба контура» — один арм двух контуров нести не может.** Куплённый `A6` есть СМЫСЛОВОЙ
|
||||||
|
контур; флаговый заводится армом `A6F` на той же базе.
|
||||||
|
|
||||||
|
**Э-7. `places[:40]` молча резал список критика.** A3 потерял 512 замечаний из 1491 (34%), A6 — 374
|
||||||
|
из 886; резался КОНЕЦ главы, не случайная выборка. Кап поднят до 200 (максимум найденного — 150),
|
||||||
|
в запись клетки добавлено поле `places_found` рядом с `places`.
|
||||||
|
|
||||||
|
**Э-8. Единица `63ab55ac5c` исключена из оси.** Её якорный черновик — эхо исходника; штатный гейт
|
||||||
|
проекта `bakeoff.draft_reject_reason` его бракует, но ветка новых 16 единиц (`PAN23.draft_b`) гейт
|
||||||
|
не применяла вовсе. Клетки A1/A3 на ней оплачены ($0.0231) и в судейскую пачку НЕ идут. n оси 31.
|
||||||
|
|
||||||
|
**Э-9. Две клетки A4 с `finish=length`** (41599f30df, f6da9f76b2) недопустимы в пачку по норме Д0.6
|
||||||
|
и пере-покупаются. ⚠ Поправка к промежуточному чтению: они же фабриковали 12 из 13 «новых
|
||||||
|
канон-потерь» арма A4 — без них A4 чинит 19 и заводит 1.
|
||||||
|
|
||||||
|
**Э-10. Проекционный гард не был СТОПом.** При отказе `buy.purchase` возвращает `skipped=True` и
|
||||||
|
файла не пишет, а `contour.py` возврат выбрасывал: цикл шёл дальше по единицам И по следующим
|
||||||
|
армам, прогон завершался кодом 0. Так A4 остался на 22 клетках из 32, а объявленная норма
|
||||||
|
«срабатывание гарда = СТОП и вопрос владельцу» существовала только в прозе. Механизирована.
|
||||||
|
|
||||||
|
**Э-11. Клетка «мест не нашлось» больше не выбрасывается.** Прежде единица молча выпадала, и A1
|
||||||
|
приходил на судейство с n=30 против 32 у A3 — контрасты переставали быть парными, причём выпадали
|
||||||
|
ровно те единицы, где контур не даёт ничего, то есть смещение шло В ПОЛЬЗУ арма. Теперь выход арма
|
||||||
|
на такой единице = его вход, клетка пишется с нулевой ценой и полем `free`.
|
||||||
|
|
||||||
|
**Э-12. Режим `--floor` был объявлен в шапке и не существовал** (разбор аргументов знал только
|
||||||
|
`--selftest` и `--run`, прочее молча уходило в `--plan` с кодом 0). Реализован парой независимых
|
||||||
|
генераций одного лечения на единицу, как требует норма Д0.6.
|
||||||
|
|
||||||
|
**Э-13. Смета промахнулась структурно.** `plan.py` кладёт фиксера в 0.60× переписа — замерено 2.41×;
|
||||||
|
критика в 2.14× — замерено 0.99× (он выдаёт короткий список, а не текст; множитель 2.14 из заказа
|
||||||
|
описывал стоимость ПОИСКА как задачи, а не длину ответа). Потолок фазы поднят владельцем 11.08 до
|
||||||
|
**$5.40**. ⚠ Резерв при этом $0.00, и это объявлено риском ВТОРОЙ раз: честный остаток $4.277 плюс
|
||||||
|
потраченные $1.140 дают $5.417. Д1 добирает **15 клеток gemini из 16** именно поэтому, а пре-рег
|
||||||
|
Д0.3 говорит, что несущей ось Д1 делают полные 16.
|
||||||
|
|
||||||
|
**Э-14. Гейт чужого пака `verify22.py` красный** (EXIT=1) — в хендоффе значился зелёным. Причина не
|
||||||
|
в отчёте: число «верхняя граница двойной оплаты» выводится из mtime файлов, а дерево `~/books`
|
||||||
|
пере-штамповано, поэтому граница считается нулевой. Чужая зона: не правлю, объявляю пингом.
|
||||||
|
|
||||||
|
**Что ревью ОПРОВЕРГЛО (снято, чтобы не осталось в обороте).** Канон-гейт `contour.py` подозревался
|
||||||
|
в том, что меряет не то: 96% его флагов — не пропажа термина, а меньшая частота. Прогнан контроль,
|
||||||
|
которого не было: срабатывание на выходе ЧУЖОЙ единицы 67.1% против 28.5% на своём — прибор
|
||||||
|
специфичен в 2.4 раза и нулевую модель проходит. Счётная сверка ПОКРЫТИЯ — дословно то, что
|
||||||
|
заказано (промт :91-94). Подозрение снято; остаётся мягкая правка формулировки флага.
|
||||||
|
|
|
||||||
|
|
@ -69,18 +69,59 @@ OUT = MONEY.OUT
|
||||||
ANCHOR = "deepseek-v4-flash" # якорный черновик
|
ANCHOR = "deepseek-v4-flash" # якорный черновик
|
||||||
FIXER = "deepseek-v4-pro" # фиксер и критик — боевой редакторский движок
|
FIXER = "deepseek-v4-pro" # фиксер и критик — боевой редакторский движок
|
||||||
DRAFT2 = "deepseek-v4-pro" # «качественный черновик» H-1 (арм D2 эксп-22)
|
DRAFT2 = "deepseek-v4-pro" # «качественный черновик» H-1 (арм D2 эксп-22)
|
||||||
ARMS = ("A1", "A2", "A3", "A4", "A6")
|
ARMS = ("A1", "A2", "A3", "A4", "A6", "A6F")
|
||||||
|
|
||||||
|
# ⚠ ПОПРАВКА ФАЗЫ Д (11.08, после ревью 82 находок). Пре-рег Д0.5 называет A6 армом «dspro-черновик
|
||||||
|
# + ОБА контура». Один арм двух контуров нести не может: контур — это способ получить выход, и
|
||||||
|
# выходов у двух контуров два. Куплённый `A6` есть СМЫСЛОВОЙ контур; флаговый заводится отдельным
|
||||||
|
# армом `A6F` на той же базе. Эррата объявлена ДО покупки A6F, старые клетки не трогаются.
|
||||||
|
PLACES_CAP = 200 # было 40 — резало 34% находок критика (см. эрратy Д0.11)
|
||||||
|
|
||||||
|
|
||||||
|
# ⚠ ЕДИНИЦЫ, ИСКЛЮЧЁННЫЕ ИЗ ОСИ, — ПЕРЕЧИСЛЕНЫ ЗДЕСЬ С ПРИЧИНОЙ И ЦЕНОЙ.
|
||||||
|
# Исключение — не сокращение выборки решением сессии, а применение ШТАТНОГО гейта проекта, который
|
||||||
|
# на этой ветке не применялся вовсе (см. `base_draft`). Объявляется до судейства, а не после.
|
||||||
|
EXCLUDED = {
|
||||||
|
"63ab55ac5c": "якорный черновик — эхо исходника (гейт bakeoff.draft_reject_reason); "
|
||||||
|
"клетки A1/A3 на ней уже оплачены ($0.0231) и в судейскую пачку НЕ идут",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── единицы: 16 эксп-22 + 16 эксп-23, база у каждой своя и уже куплена ────────────────────────
|
# ── единицы: 16 эксп-22 + 16 эксп-23, база у каждой своя и уже куплена ────────────────────────
|
||||||
def units() -> list[dict]:
|
def units() -> list[dict]:
|
||||||
|
"""ВСЕ единицы среза, включая исключённые, — для учёта и кассы."""
|
||||||
return P22.M.units_zh() + MAT.units_new()
|
return P22.M.units_zh() + MAT.units_new()
|
||||||
|
|
||||||
|
|
||||||
|
def units_ok() -> list[dict]:
|
||||||
|
"""Единицы, ДОПУСТИМЫЕ к покупке и судейству: без брака базы."""
|
||||||
|
return [u for u in units() if u["uid"] not in EXCLUDED]
|
||||||
|
|
||||||
|
|
||||||
|
def draft_defect(text: str) -> str:
|
||||||
|
"""Штатный гейт годности черновика проекта. Пусто = годен."""
|
||||||
|
import bakeoff as BO # noqa: PLC0415
|
||||||
|
return BO.draft_reject_reason(text or "")
|
||||||
|
|
||||||
|
|
||||||
def base_draft(uid: str) -> str:
|
def base_draft(uid: str) -> str:
|
||||||
"""Якорный черновик единицы. У старых — из сырья эксп-22, у новых — из фазы B эксп-23."""
|
"""Якорный черновик единицы. У старых — из сырья эксп-22, у новых — из фазы B эксп-23.
|
||||||
|
|
||||||
|
⚠ ГЕЙТ ГОДНОСТИ НА ОБЕИХ ВЕТКАХ. Старая ветка `P22.draft_text` фильтрует брак сама
|
||||||
|
(`tenant_panel/panel.py:130` — `if c and not BO.draft_reject_reason(c)`), а `PAN23.draft_b`
|
||||||
|
для 16 НОВЫХ единиц не фильтрует ничего. Через эту щель в фазу Д прошла эхо-мина
|
||||||
|
`63ab55ac5c`: на ней куплены клетки армов A1 и A3 поверх черновика, который штатный гейт
|
||||||
|
проекта бракует. Здесь ветки уравнены; брак не возвращается молча, а называется вслух.
|
||||||
|
"""
|
||||||
t = P22.draft_text(ANCHOR, uid)
|
t = P22.draft_text(ANCHOR, uid)
|
||||||
return t if t.strip() else PAN23.draft_b(uid) # новые единицы — фаза B пака 23
|
if t.strip():
|
||||||
|
return t
|
||||||
|
t = PAN23.draft_b(uid)
|
||||||
|
why = draft_defect(t)
|
||||||
|
if why:
|
||||||
|
print(f" ⚠ {uid}: черновик БРАКОВАН гейтом проекта ({why}) — единица непригодна")
|
||||||
|
return ""
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
def base_a0(uid: str) -> str:
|
def base_a0(uid: str) -> str:
|
||||||
|
|
@ -108,20 +149,61 @@ def canon_gaps(src: str, out: str) -> list[str]:
|
||||||
return gaps
|
return gaps
|
||||||
|
|
||||||
|
|
||||||
|
# ⚠ ПОЛЯ БАТАРЕИ, КОТОРЫЕ ОЗНАЧАЮТ НАРУШЕНИЕ, — ПЕРЕЧИСЛЕНЫ ЯВНО, И ЭТО НЕ ПЕДАНТИЗМ.
|
||||||
|
# Первая редакция брала из `run_unit` всё, что `isinstance(v, (int, float)) and v`. Дефект был
|
||||||
|
# двойной и оба слоя молчали: (1) на вход шёл `dict`, а не `battery.Unit`, и `run_unit` падал
|
||||||
|
# `AttributeError: 'dict' object has no attribute 'final'` на 32/32 единицах — ошибку глотал голый
|
||||||
|
# `except`, так что ВСЕ 30 оплаченных клеток A1 куплены на одном канон-гейте, хотя арм объявлен
|
||||||
|
# «батарея + канон-гейт»; (2) значения проверок — СЛОВАРИ, и наивный фильтр, будь он даже
|
||||||
|
# достижим, вымел бы в список мест ЗНАМЕНАТЕЛИ: «проверка sentences дала 90», «words дала 1154».
|
||||||
|
# То есть починка одного типа сделала бы арм хуже, чем сломанный. Поэтому whitelist, а не эвристика.
|
||||||
|
BATTERY_VIOLATIONS = {
|
||||||
|
"palladius": ("nonconformant_bank",),
|
||||||
|
"cjk_leak": ("over_probe_threshold", "over_prod_threshold"),
|
||||||
|
"typography": ("violations", "ascii_quotes", "hyphen_as_dash"),
|
||||||
|
"numbers": ("lost_n", "invented_n"),
|
||||||
|
"ty_vy": ("mixed_in_unit",),
|
||||||
|
"gender": ("mismatched",),
|
||||||
|
}
|
||||||
|
BATTERY_SAYS = {
|
||||||
|
"palladius.nonconformant_bank": "имена/термины переданы мимо банка ({v} шт.)",
|
||||||
|
"cjk_leak.over_probe_threshold": "в переводе остались иероглифы исходника",
|
||||||
|
"cjk_leak.over_prod_threshold": "в переводе остались иероглифы исходника (сверх прод-порога)",
|
||||||
|
"typography.violations": "нарушения типографики ({v} шт.)",
|
||||||
|
"typography.ascii_quotes": "прямые кавычки вместо «ёлочек» ({v} шт.)",
|
||||||
|
"typography.hyphen_as_dash": "дефис вместо тире ({v} шт.)",
|
||||||
|
"numbers.lost_n": "числа исходника потеряны в переводе ({v} шт.)",
|
||||||
|
"numbers.invented_n": "в переводе появились числа, которых нет в исходнике ({v} шт.)",
|
||||||
|
"ty_vy.mixed_in_unit": "внутри единицы смешаны «ты» и «вы»",
|
||||||
|
"gender.mismatched": "род персонажа расходится с карточкой ({v} шт.)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def battery_flags(u: dict, draft: str) -> list[str]:
|
||||||
|
"""Нарушения батареи на тексте. Пустой список — законный ответ; ИСКЛЮЧЕНИЕ — не законный."""
|
||||||
|
import battery as B # noqa: PLC0415
|
||||||
|
bank = BANK.PR.bank()
|
||||||
|
unit = B.Unit(source=u["source"], draft=draft, final=draft,
|
||||||
|
bank_dst={v["dst"] for v in bank.values()},
|
||||||
|
term_pairs=[(t, v["dst"]) for t, v in bank.items() if u["source"].count(t)])
|
||||||
|
r = B.run_unit(unit) # падение = дефект, НЕ глотаем
|
||||||
|
out = []
|
||||||
|
for check, fields in BATTERY_VIOLATIONS.items():
|
||||||
|
val = r.get(check)
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
continue
|
||||||
|
for f in fields:
|
||||||
|
v = val.get(f)
|
||||||
|
if isinstance(v, (int, float, bool)) and v:
|
||||||
|
out.append("БАТАРЕЯ: " + BATTERY_SAYS[f"{check}.{f}"].format(v=int(v)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def flags_of(u: dict, draft: str) -> list[str]:
|
def flags_of(u: dict, draft: str) -> list[str]:
|
||||||
"""Список мест для A1. Только ДЕТЕРМИНИРОВАННОЕ: канон-гейт + батарея, без судьи и без LLM."""
|
"""Список мест для A1. Только ДЕТЕРМИНИРОВАННОЕ: канон-гейт + батарея, без судьи и без LLM."""
|
||||||
import battery as B # noqa: PLC0415
|
out = [f"КАНОН: термин {g} передан не канонной формой либо пропущен"
|
||||||
out = []
|
for g in canon_gaps(u["source"], draft)]
|
||||||
for g in canon_gaps(u["source"], draft):
|
return out + battery_flags(u, draft)
|
||||||
out.append(f"КАНОН: термин {g} передан не канонной формой либо пропущен")
|
|
||||||
try:
|
|
||||||
r = B.run_unit(dict(uid=u["uid"], source=u["source"], text=draft))
|
|
||||||
for k, v in (r or {}).items():
|
|
||||||
if isinstance(v, (int, float)) and v and k not in ("uid", "len"):
|
|
||||||
out.append(f"БАТАРЕЯ: проверка «{k}» дала {v}")
|
|
||||||
except Exception: # noqa: BLE001, S110
|
|
||||||
pass # батарея не обязана знать наш вход
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
# ── промты армов: данные, не код ──────────────────────────────────────────────────────────────
|
# ── промты армов: данные, не код ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -158,8 +240,60 @@ def crit_msgs(src: str, draft: str) -> list[dict]:
|
||||||
{"role": "user", "content": f"ИСХОДНИК:\n{src}\n\nЧЕРНОВОЙ ПЕРЕВОД:\n{draft}"}]
|
{"role": "user", "content": f"ИСХОДНИК:\n{src}\n\nЧЕРНОВОЙ ПЕРЕВОД:\n{draft}"}]
|
||||||
|
|
||||||
|
|
||||||
|
# ── A2: однопроходка УРАВНЕННОГО мандата (строка 153 бэклога, долг с эксп-21) ─────────────────
|
||||||
|
# ⚠ ЗАЧЕМ УРАВНИВАТЬ. Контраст «однопроходка против связки» в эксп-21 был загрязнён: у арма без
|
||||||
|
# редактора системный промт вдвое короче (×1.92), и замеренная разница могла быть разницей ОБЪЁМА
|
||||||
|
# ИНСТРУКЦИИ, а не топологии. Строка 153 бэклога держит этот долг с тех пор и здесь закрывается:
|
||||||
|
# A2 получает СУММУ мандатов боевой связки — промт переводчика ПЛЮС мандатные части промта
|
||||||
|
# редактора — в ОДИН вызов. Тогда `A2/A0` меряет топологию, а не количество указаний.
|
||||||
|
#
|
||||||
|
# ⚠ ПРЕЖНЯЯ РЕДАКЦИЯ ЭТОГО МЕСТА НЕ РАБОТАЛА ВОВСЕ: `PR.translator_msgs(src)` звался с одним
|
||||||
|
# аргументом при сигнатуре `(src, block, en=False)` → TypeError, а сторож `hasattr` проверяет
|
||||||
|
# существование имени, а не арность, поэтому ветка «арм пропущен, объявить в отчёте» была
|
||||||
|
# НЕДОСТИЖИМА. Арм не куплен ни разу; вдобавок `block=None` снял бы банк-закон D39.104, который
|
||||||
|
# заказ требует на ВСЕХ армах. Оба дефекта закрыты здесь.
|
||||||
|
#
|
||||||
|
# Преобразование промта редактора — ДАННЫЕ, не код: таблицы ниже читаются глазами и сторожатся
|
||||||
|
# гейтом `--selftest`. Строки, предполагающие наличие черновика, переписываются, а не выбрасываются:
|
||||||
|
# выбросить их значило бы снять мандат, ради уравнивания которого арм и заводится.
|
||||||
|
A2_DROP = (
|
||||||
|
"Ты — литературный редактор", # роль уже задана промтом переводчика
|
||||||
|
"Жанр книги:", # дубль строки переводчика
|
||||||
|
"Хонорифики:", # дубль строки переводчика
|
||||||
|
"Выведи ТОЛЬКО отредактированный", # у переводчика своя строка вывода
|
||||||
|
)
|
||||||
|
A2_SUBST = (
|
||||||
|
("Твой мандат — художественная редактура черновика со сверкой по исходнику:",
|
||||||
|
"Отдельного редактора после тебя НЕ БУДЕТ, поэтому мандат редактуры входит в твой:"),
|
||||||
|
("Сверяй смысл черновика с исходным текстом", "Сверяй смысл перевода с исходным текстом"),
|
||||||
|
("Сохраняй ПОЛНОТУ черновика", "Сохраняй ПОЛНОТУ исходника"),
|
||||||
|
("редактуры черновика", "редактуры перевода"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def a2_mandate() -> str:
|
||||||
|
"""Мандатная часть боевого редактора, переписанная под однопроходку. $0, детерминирована."""
|
||||||
|
core, _few, _user = PR.load_template(PR.BATTLE / "editor.md")
|
||||||
|
core = PR.render(core, PR.BRIEF_ZH, "", "")
|
||||||
|
out = []
|
||||||
|
for ln in core.splitlines():
|
||||||
|
if any(ln.strip().startswith(d) for d in A2_DROP):
|
||||||
|
continue
|
||||||
|
for a, b in A2_SUBST:
|
||||||
|
ln = ln.replace(a, b)
|
||||||
|
out.append(ln)
|
||||||
|
return "\n".join(out).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def a2_msgs(src: str) -> list[dict]:
|
||||||
|
"""Однопроходка: перевод исходника ОДНИМ вызовом, мандат уравнен со связкой, банк-закон на месте."""
|
||||||
|
m = PR.translator_msgs(src, PR.law_block(PR.terms_in(src)))
|
||||||
|
m[0]["content"] = m[0]["content"] + "\n\n" + a2_mandate()
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
def tag(arm: str, uid: str, part: str = "") -> str:
|
def tag(arm: str, uid: str, part: str = "") -> str:
|
||||||
ph = "f" if arm == "A6" else "a"
|
ph = "f" if arm.startswith("A6") else "a"
|
||||||
return f"dv-{ph}-{arm.lower()}{('-' + part) if part else ''}-{uid}"
|
return f"dv-{ph}-{arm.lower()}{('-' + part) if part else ''}-{uid}"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -175,9 +309,19 @@ def cmd_plan() -> None:
|
||||||
old = {u["uid"] for u in P22.M.units_zh()}
|
old = {u["uid"] for u in P22.M.units_zh()}
|
||||||
print(f"единиц {len(us)} (старых {len(old)} + новых {len(us) - len(old)})")
|
print(f"единиц {len(us)} (старых {len(old)} + новых {len(us) - len(old)})")
|
||||||
have = {a: sum(1 for u in us if text_of(a, u["uid"]).strip()) for a in ARMS}
|
have = {a: sum(1 for u in us if text_of(a, u["uid"]).strip()) for a in ARMS}
|
||||||
|
# ⚠ Печатать «куплено N из M» без второй колонки — значит сертифицировать полноту арма его
|
||||||
|
# же счётчиком: клетка, записанная БЕСПЛАТНО (мест не нашлось), в первой колонке неотличима
|
||||||
|
# от оплаченной, а именно на этой неразличимости прошлая редакция объявила A1 «30/30 платных».
|
||||||
for a in ARMS:
|
for a in ARMS:
|
||||||
n = len(old) if a == "A6" else len(us)
|
n = len(old) if a.startswith("A6") else len(us)
|
||||||
print(f" {a}: куплено {have[a]:2d} из {n}")
|
free = sum(1 for u in us
|
||||||
|
if (OUT / f"{tag(a, u['uid'])}.json").exists()
|
||||||
|
and json.loads((OUT / f"{tag(a, u['uid'])}.json").read_text()).get("free"))
|
||||||
|
print(f" {a}: клеток {have[a]:2d} из {n}" + (f" (из них БЕСПЛАТНЫХ {free})" if free else ""))
|
||||||
|
flo = sum(1 for u in P22.M.units_zh() for h in ("flo1", "flo2")
|
||||||
|
if (OUT / f"dv-a-{h}-{u['uid']}.json").exists())
|
||||||
|
print(f" свой шумовой пол: клеток {flo} из {2 * len(old)} (ПАРАМИ, обе половины — "
|
||||||
|
"отдельные генерации)")
|
||||||
base = sum(1 for u in us if base_draft(u["uid"]).strip())
|
base = sum(1 for u in us if base_draft(u["uid"]).strip())
|
||||||
a0 = sum(1 for u in us if base_a0(u["uid"]).strip())
|
a0 = sum(1 for u in us if base_a0(u["uid"]).strip())
|
||||||
d2 = sum(1 for u in us if base_draft2(u["uid"]).strip())
|
d2 = sum(1 for u in us if base_draft2(u["uid"]).strip())
|
||||||
|
|
@ -195,23 +339,31 @@ def cmd_selftest() -> int:
|
||||||
bad += not ok
|
bad += not ok
|
||||||
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else ""))
|
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else ""))
|
||||||
|
|
||||||
us = units()
|
us = units_ok()
|
||||||
ck("единиц ровно 32", len(us) == 32, str(len(us)))
|
ck("единиц ровно 32 минус исключённые", len(us) == 32 - len(EXCLUDED),
|
||||||
|
f"{len(us)} из 32; исключено {len(EXCLUDED)}")
|
||||||
|
for uid, why in EXCLUDED.items():
|
||||||
|
print(f" ⚠ ИСКЛЮЧЕНА {uid}: {why}")
|
||||||
ck("uid уникальны", len({u["uid"] for u in us}) == len(us))
|
ck("uid уникальны", len({u["uid"] for u in us}) == len(us))
|
||||||
miss = [u["uid"] for u in us if not base_draft(u["uid"]).strip()]
|
miss = [u["uid"] for u in us if not base_draft(u["uid"]).strip()]
|
||||||
ck("якорный черновик есть у всех", not miss, f"нет у {miss[:3]}")
|
ck("якорный черновик есть у всех ДОПУСТИМЫХ", not miss, f"нет у {miss[:3]}")
|
||||||
miss0 = [u["uid"] for u in us if not base_a0(u["uid"]).strip()]
|
miss0 = [u["uid"] for u in us if not base_a0(u["uid"]).strip()]
|
||||||
ck("арм A0 (боевой перепис) есть у всех", not miss0, f"нет у {miss0[:3]}")
|
ck("арм A0 (боевой перепис) есть у всех", not miss0, f"нет у {miss0[:3]}")
|
||||||
old = P22.M.units_zh()
|
old = P22.M.units_zh()
|
||||||
miss2 = [u["uid"] for u in old if not base_draft2(u["uid"]).strip()]
|
miss2 = [u["uid"] for u in old if not base_draft2(u["uid"]).strip()]
|
||||||
ck("черновик-dspro для H-1 есть у всех 16 старых", not miss2, f"нет у {miss2[:3]}")
|
ck("черновик-dspro для H-1 есть у всех 16 старых", not miss2, f"нет у {miss2[:3]}")
|
||||||
# ИЗОЛЯЦИЯ: у A1 и A3 вход ОДИН И ТОТ ЖЕ черновик; различаться обязан только список мест
|
# ИЗОЛЯЦИЯ: у A1 и A3 вход ОДИН И ТОТ ЖЕ черновик; различаться обязан только список мест.
|
||||||
|
# ⚠ Прежняя редакция сверяла `fix_msgs(...)` САМА С СОБОЙ на одном и том же черновике —
|
||||||
|
# тавтология, которая не могла провалиться и потому ничего не сторожила. Теперь входы армов
|
||||||
|
# берутся ТЕМ ЖЕ кодом, что и в бою (`cmd_run`), и сверяются между собой.
|
||||||
u = us[0]
|
u = us[0]
|
||||||
d = base_draft(u["uid"])
|
d = base_draft(u["uid"])
|
||||||
m1 = fix_msgs(u["source"], d, ["КАНОН: тест"])
|
m1 = fix_msgs(u["source"], d, flags_of(u, d) or ["КАНОН: тест"])
|
||||||
m3 = fix_msgs(u["source"], d, ["СМЫСЛ: тест"])
|
m3 = fix_msgs(u["source"], d, critic_places("A3", u["uid"])[0] or ["СМЫСЛ: тест"])
|
||||||
ck("A1 и A3 делят вход, различаясь только списком мест",
|
ck("A1 и A3 делят вход, различаясь только списком мест",
|
||||||
m1[0]["content"] == m3[0]["content"] and m1[1]["content"] != m3[1]["content"])
|
m1[0]["content"] == m3[0]["content"] and m1[1]["content"] != m3[1]["content"])
|
||||||
|
ck("вход A4 — это A0, а не черновик (позиция «ПОСЛЕ редактора»)",
|
||||||
|
base_a0(u["uid"]).strip() != d.strip())
|
||||||
ck("в промте фиксера стоит запрет трогать остальное",
|
ck("в промте фиксера стоит запрет трогать остальное",
|
||||||
"ПОБАЙТНО как есть" in m1[0]["content"])
|
"ПОБАЙТНО как есть" in m1[0]["content"])
|
||||||
# ⚠ Банк-закон D39.104 обязателен на ВСЕХ армах с переводом/редактурой. Проверяется ДОСЛОВНО
|
# ⚠ Банк-закон D39.104 обязателен на ВСЕХ армах с переводом/редактурой. Проверяется ДОСЛОВНО
|
||||||
|
|
@ -220,12 +372,38 @@ def cmd_selftest() -> int:
|
||||||
"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in m1[0]["content"], m1[0]["content"][-90:].replace("\n", " "))
|
"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in m1[0]["content"], m1[0]["content"][-90:].replace("\n", " "))
|
||||||
ck("банк-закон есть и в промте КРИТИКА — иначе он флагует канон не по банку",
|
ck("банк-закон есть и в промте КРИТИКА — иначе он флагует канон не по банку",
|
||||||
"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in crit_msgs(u["source"], d)[0]["content"])
|
"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in crit_msgs(u["source"], d)[0]["content"])
|
||||||
fl = flags_of(u, d)
|
# ⚠ БАТАРЕЯ. Прежняя проверка `isinstance(fl, list)` не могла быть ложной: `flags_of` заводит
|
||||||
ck("канон-гейт выдаёт места (иначе A1 нечего чинить)", isinstance(fl, list), f"мест {len(fl)}")
|
# `out = []` и имеет единственный выход `return out`. Она и пропустила смерть батареи на 32/32.
|
||||||
|
bf = battery_flags(u, d)
|
||||||
|
ck("батарея ЖИВА (падение = дефект, а не пустой список)", isinstance(bf, list))
|
||||||
|
tot_b = sum(len(battery_flags(x, base_draft(x["uid"]))) for x in us if base_draft(x["uid"]))
|
||||||
|
ck("батарея вносит места в флаговый контур, а не молчит", tot_b > 0, f"нарушений {tot_b}")
|
||||||
|
ck("в места батареи не попали ЗНАМЕНАТЕЛИ (sentences/words/lines)",
|
||||||
|
not any(w in f for f in bf for w in ("sentences", "words", "lines", "candidates")),
|
||||||
|
"; ".join(bf[:2]))
|
||||||
|
# ⚠ A2: арм, которого не существовало — вызов падал TypeError, ветка отказа была недостижима.
|
||||||
|
m2 = a2_msgs(u["source"])
|
||||||
|
ck("A2 собирается (однопроходка уравненного мандата)", len(m2) >= 2)
|
||||||
|
ck("банк-закон D39.104 есть и в A2", any("КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in x["content"] for x in m2))
|
||||||
|
man = a2_mandate()
|
||||||
|
ck("мандат A2 несёт дискурс-перевёрстку (иначе мандат НЕ уравнен)",
|
||||||
|
"ДИСКУРС-ПЕРЕВЁРСТКА" in man)
|
||||||
|
ck("мандат A2 несёт правило чэнъюй (смысловые атомы)", "чэнъюй" in man)
|
||||||
|
ck("в мандате A2 не осталось слова «черновик» — редактировать нечего, он переводит",
|
||||||
|
"черновик" not in man.lower(), man[:120].replace("\n", " "))
|
||||||
|
ck("в промте A2 не осталось нераскрытых плейсхолдеров",
|
||||||
|
not any("{{" in x["content"] for x in m2))
|
||||||
|
ck("A2 длиннее голой однопроходки — мандат реально добавлен",
|
||||||
|
len(m2[0]["content"]) > len(PR.translator_msgs(u["source"], None)[0]["content"]))
|
||||||
# теги армов не пересекаются между собой и с чужими паками
|
# теги армов не пересекаются между собой и с чужими паками
|
||||||
tags = {tag(a, u["uid"]) for a in ARMS}
|
tags = {tag(a, u["uid"]) for a in ARMS}
|
||||||
ck("теги армов различны", len(tags) == len(ARMS))
|
ck("теги армов различны", len(tags) == len(ARMS), f"{len(tags)} против {len(ARMS)}")
|
||||||
ck("теги покрыты глобом кассы", all(t.startswith(("dv-a-", "dv-f-")) for t in tags))
|
ck("теги покрыты глобом кассы", all(t.startswith(("dv-a-", "dv-f-")) for t in tags))
|
||||||
|
ck("тег половины пола покрыт глобом кассы", f"dv-a-flo1-{u['uid']}".startswith("dv-a-"))
|
||||||
|
ck("обрезка мест не режет ответ критика ни на одной единице",
|
||||||
|
PLACES_CAP >= max((critic_places("A3", x["uid"])[1] for x in us), default=0),
|
||||||
|
f"кап {PLACES_CAP}, максимум найденного "
|
||||||
|
f"{max((critic_places('A3', x['uid'])[1] for x in us), default=0)}")
|
||||||
# ⚠ Главный ассерт этого харнесса: кэш-чек и ЗАПИСЬ результата смотрят в ОДИН каталог.
|
# ⚠ Главный ассерт этого харнесса: кэш-чек и ЗАПИСЬ результата смотрят в ОДИН каталог.
|
||||||
ck("buy.OUT совпадает с кассой фазы", MONEY.M22.BUY.OUT == MONEY.OUT,
|
ck("buy.OUT совпадает с кассой фазы", MONEY.M22.BUY.OUT == MONEY.OUT,
|
||||||
f"{MONEY.M22.BUY.OUT} против {MONEY.OUT}")
|
f"{MONEY.M22.BUY.OUT} против {MONEY.OUT}")
|
||||||
|
|
@ -233,58 +411,133 @@ def cmd_selftest() -> int:
|
||||||
return bad
|
return bad
|
||||||
|
|
||||||
|
|
||||||
|
def _buy(led, tg: str, msgs: list[dict], **extra) -> dict:
|
||||||
|
"""Покупка, у которой отказ гарда есть СТОП, а не пропуск.
|
||||||
|
|
||||||
|
⚠ Прежняя редакция выбрасывала возврат `MONEY.purchase`. Когда проекционный гард отказывал,
|
||||||
|
`buy.purchase` возвращал `skipped=True` и НЕ писал файла, а цикл спокойно шёл дальше — по
|
||||||
|
остальным единицам и по следующим армам с их собственными потолками. Так арм A4 оказался
|
||||||
|
куплен на 22 единицах из 32, и прогон завершился кодом 0, будто всё в порядке. Объявленная
|
||||||
|
норма фазы (`money_d.py` шапка, пре-рег Д0.8) звучит иначе: **срабатывание гарда = СТОП и
|
||||||
|
вопрос владельцу**. Здесь она наконец механизирована.
|
||||||
|
"""
|
||||||
|
rec = MONEY.purchase(led, tg, FIXER, ED22.client(FIXER),
|
||||||
|
ROSTER.call_kwargs(FIXER, msgs), **extra)
|
||||||
|
if rec.get("skipped"):
|
||||||
|
raise SystemExit(
|
||||||
|
f"⛔ ГАРД ОТКАЗАЛ на клетке {tg} — прогон ОСТАНОВЛЕН.\n"
|
||||||
|
f" Резать ось или поднимать потолок решением сессии ЗАПРЕЩЕНО заказом.\n"
|
||||||
|
f" Это СТОП и вопрос владельцу: назвать недостающую сумму и ждать санкции.")
|
||||||
|
return rec
|
||||||
|
|
||||||
|
|
||||||
|
def _free_cell(tg: str, text: str, **extra) -> None:
|
||||||
|
"""Клетка арма, полученная БЕЗ вызова модели, — записывается с нулевой ценой.
|
||||||
|
|
||||||
|
⚠ Зачем вообще. Если детерминированный контур не нашёл ни одного места, ответ арма на этой
|
||||||
|
единице — исходный текст БЕЗ ПРАВОК, и это законный выход арма, а не отсутствие данных.
|
||||||
|
Прежняя редакция такую единицу молча выбрасывала (`continue`), из-за чего A1 приходил на
|
||||||
|
судейство с n=30 против n=32 у A3: контрасты переставали быть парными, а из выборки выпадали
|
||||||
|
ровно те единицы, где контур не даёт ничего, — то есть смещение шло в пользу арма.
|
||||||
|
"""
|
||||||
|
p = OUT / f"{tg}.json"
|
||||||
|
if p.exists():
|
||||||
|
return
|
||||||
|
rec = dict(tag=tg, model="(без вызова)", finish="stop", content=text, cost_usd=0.0,
|
||||||
|
latency_s=0.0, free=True, **extra)
|
||||||
|
tmp = OUT / f"{tg}.json.tmp"
|
||||||
|
tmp.write_text(json.dumps(rec, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
|
tmp.replace(p)
|
||||||
|
print(f" {tg}: мест нет — выход арма = вход, клетка записана БЕСПЛАТНО (n сохранён)")
|
||||||
|
|
||||||
|
|
||||||
|
def critic_places(arm: str, uid: str) -> tuple[list[str], int]:
|
||||||
|
"""Места из ответа критика: (что уйдёт фиксеру, сколько нашлось всего)."""
|
||||||
|
cf = OUT / f"{tag(arm, uid, 'crit')}.json"
|
||||||
|
if not cf.exists():
|
||||||
|
return [], 0
|
||||||
|
found = json.loads(cf.read_text(encoding="utf-8")).get("content") or ""
|
||||||
|
pl = [ln.strip("- ").strip() for ln in found.splitlines() if ln.strip().startswith("-")]
|
||||||
|
return pl[:PLACES_CAP], len(pl)
|
||||||
|
|
||||||
|
|
||||||
def cmd_run(which: list[str]) -> None:
|
def cmd_run(which: list[str]) -> None:
|
||||||
us = units()
|
us = units_ok()
|
||||||
old = {u["uid"] for u in P22.M.units_zh()}
|
old = {u["uid"] for u in P22.M.units_zh()}
|
||||||
for arm in which:
|
for arm in which:
|
||||||
led = MONEY.Guarded("ФД-F" if arm == "A6" else "ФД-A")
|
led = MONEY.Guarded("ФД-F" if arm.startswith("A6") else "ФД-A")
|
||||||
for u in us:
|
for u in us:
|
||||||
uid, src = u["uid"], u["source"]
|
uid, src = u["uid"], u["source"]
|
||||||
if arm == "A6" and uid not in old:
|
if arm.startswith("A6") and uid not in old:
|
||||||
continue
|
continue
|
||||||
if text_of(arm, uid).strip():
|
if text_of(arm, uid).strip():
|
||||||
continue
|
continue
|
||||||
draft = base_draft2(uid) if arm == "A6" else base_draft(uid)
|
draft = base_draft2(uid) if arm.startswith("A6") else base_draft(uid)
|
||||||
if not draft.strip():
|
if not draft.strip():
|
||||||
print(f" {uid}: базы нет — пропуск")
|
print(f" {uid}: базы нет — пропуск")
|
||||||
continue
|
continue
|
||||||
if arm == "A2": # однопроходка: редактора нет
|
if arm == "A2": # однопроходка уравненного мандата
|
||||||
msgs = PR.translator_msgs(src) if hasattr(PR, "translator_msgs") else None
|
_buy(led, tag(arm, uid), a2_msgs(src), role="onepass", uid=uid, arm=arm)
|
||||||
if msgs is None:
|
|
||||||
print(" A2: сборки однопроходки нет — арм пропущен, объявить в отчёте")
|
|
||||||
break
|
|
||||||
MONEY.purchase(led, tag(arm, uid), FIXER, ED22.client(FIXER),
|
|
||||||
ROSTER.call_kwargs(FIXER, msgs), role="onepass", uid=uid, arm=arm)
|
|
||||||
continue
|
continue
|
||||||
if arm in ("A1", "A4"):
|
if arm in ("A1", "A4", "A6F"): # детерминированные контуры
|
||||||
target = base_a0(uid) if arm == "A4" else draft
|
target = base_a0(uid) if arm == "A4" else draft
|
||||||
places = [f"КАНОН: термин {g} передан не канонной формой либо пропущен"
|
places = ([f"КАНОН: термин {g} передан не канонной формой либо пропущен"
|
||||||
for g in canon_gaps(src, target)] if arm == "A4" else flags_of(u, draft)
|
for g in canon_gaps(src, target)] if arm == "A4"
|
||||||
|
else flags_of(u, target))
|
||||||
if not places:
|
if not places:
|
||||||
print(f" {uid}/{arm}: мест нет — чинить нечего, клетка не покупается")
|
_free_cell(tag(arm, uid), target, role="fixer", uid=uid, arm=arm, places=0)
|
||||||
continue
|
continue
|
||||||
MONEY.purchase(led, tag(arm, uid), FIXER, ED22.client(FIXER),
|
_buy(led, tag(arm, uid), fix_msgs(src, target, places),
|
||||||
ROSTER.call_kwargs(FIXER, fix_msgs(src, target, places)),
|
role="fixer", uid=uid, arm=arm, places=len(places))
|
||||||
role="fixer", uid=uid, arm=arm, places=len(places))
|
|
||||||
continue
|
continue
|
||||||
# A3 и A6: критик, затем фиксер по его списку
|
# A3 и A6: смысловой критик, затем фиксер по его списку
|
||||||
ct = tag(arm, uid, "crit")
|
ct = tag(arm, uid, "crit")
|
||||||
if not (OUT / f"{ct}.json").exists():
|
if not (OUT / f"{ct}.json").exists():
|
||||||
MONEY.purchase(led, ct, FIXER, ED22.client(FIXER),
|
_buy(led, ct, crit_msgs(src, draft), role="critic", uid=uid, arm=arm)
|
||||||
ROSTER.call_kwargs(FIXER, crit_msgs(src, draft)),
|
if not (OUT / f"{ct}.json").exists(): # оплачено, но файла нет
|
||||||
role="critic", uid=uid, arm=arm)
|
|
||||||
cf = OUT / f"{ct}.json"
|
|
||||||
if not cf.exists(): # оплачено, но файла нет — не падать
|
|
||||||
print(f" {uid}/{arm}: критик не отдал файл — арм пропущен, объявить в отчёте")
|
print(f" {uid}/{arm}: критик не отдал файл — арм пропущен, объявить в отчёте")
|
||||||
continue
|
continue
|
||||||
found = json.loads(cf.read_text(encoding="utf-8")).get("content") or ""
|
places, found_n = critic_places(arm, uid)
|
||||||
places = [ln.strip("- ").strip() for ln in found.splitlines() if ln.strip().startswith("-")]
|
|
||||||
if not places:
|
if not places:
|
||||||
print(f" {uid}/{arm}: критик не нашёл мест — фиксер не зовётся")
|
_free_cell(tag(arm, uid), draft, role="fixer", uid=uid, arm=arm,
|
||||||
|
places=0, places_found=0)
|
||||||
continue
|
continue
|
||||||
MONEY.purchase(led, tag(arm, uid), FIXER, ED22.client(FIXER),
|
_buy(led, tag(arm, uid), fix_msgs(src, draft, places),
|
||||||
ROSTER.call_kwargs(FIXER, fix_msgs(src, draft, places[:40])),
|
role="fixer", uid=uid, arm=arm, places=len(places), places_found=found_n)
|
||||||
role="fixer", uid=uid, arm=arm, places=len(places))
|
MONEY.report()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_floor() -> None:
|
||||||
|
"""СВОЙ шумовой пол оси Д4 — ПАРОЙ независимых генераций одного и того же лечения.
|
||||||
|
|
||||||
|
⚠ Режим был объявлен в шапке файла и НЕ СУЩЕСТВОВАЛ: разбор аргументов знал только
|
||||||
|
`--selftest` и `--run`, всё прочее молча уходило в `--plan` с кодом 0. При этом ВСЯ несущесть
|
||||||
|
оси Д4 держится на ЗАИМСТВОВАННОМ прайоре sd 2.12 с запасом около 1%: при sd 2.13 ось уже
|
||||||
|
описательная. Пол — не украшение, а то, что этот прайор проверяет.
|
||||||
|
|
||||||
|
Устройство пола отвечает норме Д0.6, купленной дорого: обе половины — ОТДЕЛЬНЫЕ генерации
|
||||||
|
одного лечения (фиксер по одному и тому же списку мест над одной и той же базой), поэтому
|
||||||
|
судейская разница между ними есть ЧИСТЫЙ шум. Боевая клетка половиной пола не берётся — в
|
||||||
|
эксп-23 `CTRLfloorA` совпал с армом `T1` побайтно в 16/16, и контроль оказался пуст.
|
||||||
|
Половины судят РАЗНЫЕ сессии (это забота судейского рига, здесь — только генерация).
|
||||||
|
"""
|
||||||
|
led = MONEY.Guarded("ФД-A")
|
||||||
|
old = [u for u in P22.M.units_zh() if u["uid"] not in EXCLUDED]
|
||||||
|
for u in old:
|
||||||
|
uid, src = u["uid"], u["source"]
|
||||||
|
draft = base_draft(uid)
|
||||||
|
if not draft.strip():
|
||||||
|
continue
|
||||||
|
places, _ = critic_places("A3", uid)
|
||||||
|
if not places:
|
||||||
|
print(f" {uid}: мест нет — единица в пол не идёт")
|
||||||
|
continue
|
||||||
|
for half in ("flo1", "flo2"):
|
||||||
|
tg = f"dv-a-{half}-{uid}"
|
||||||
|
if (OUT / f"{tg}.json").exists():
|
||||||
|
continue
|
||||||
|
_buy(led, tg, fix_msgs(src, draft, places),
|
||||||
|
role="floor", uid=uid, arm=half, places=len(places))
|
||||||
MONEY.report()
|
MONEY.report()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -292,7 +545,11 @@ if __name__ == "__main__":
|
||||||
a = sys.argv[1:] or ["--plan"]
|
a = sys.argv[1:] or ["--plan"]
|
||||||
if a[0] == "--selftest":
|
if a[0] == "--selftest":
|
||||||
sys.exit(1 if cmd_selftest() else 0)
|
sys.exit(1 if cmd_selftest() else 0)
|
||||||
if a[0] == "--run":
|
elif a[0] == "--run":
|
||||||
cmd_run([x for x in a[1:] if x in ARMS] or list(ARMS))
|
cmd_run([x for x in a[1:] if x in ARMS] or list(ARMS))
|
||||||
else:
|
elif a[0] == "--floor":
|
||||||
|
cmd_floor()
|
||||||
|
elif a[0] == "--plan":
|
||||||
cmd_plan()
|
cmd_plan()
|
||||||
|
else:
|
||||||
|
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --plan --selftest --run --floor")
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Д6 — МАТЕРИАЛ ПАРЫ ja→ru: гардрейл, гейт прав, отбор единиц. $0.
|
"""Д6 — МАТЕРИАЛ ПАРЫ ja→ru: гардрейл, гейт прав, отбор единиц. $0.
|
||||||
|
|
||||||
Источник: `~/books/isekai_majutsushi_jp.txt` (файл владельца, вне git — правило Р8). Площадка
|
Источник: `~/books/enkan_no_hate_ja.txt` — 「円環の果てに ―裏切られた魂、二度目の生―」, `ncode.syosetu.com/n7233mn`,
|
||||||
`novel18.syosetu.com`, то есть срез ЗАВЕДОМО взрослый; аннотация книги сама называет 洗脳 и 暴力.
|
первая публикация **2026-07-30**, 18 глав скачано из 56. Книга подобрана тем же механическим
|
||||||
|
критерием, что китайский срез эксп-22 (§0.2): площадка отдаёт текст главы БЕЗ JS; дата первой
|
||||||
|
публикации читается у самой площадки; жанр из продуктового семейства (異世界転生, преданный герой,
|
||||||
|
вторая жизнь — тот же троп, что у 阴阳天帝诀).
|
||||||
|
|
||||||
|
⚠ **ДВА ФИЛЬТРА, КОТОРЫХ НЕ БЫЛО У ZH-СРЕЗА, и оба обязательны для ja.**
|
||||||
|
(1) **Метка `AI直接使用`.** Syosetu помечает работы, написанные ИИ, и среди свежих ранобэ таких
|
||||||
|
много: из первых трёх кандидатов по длине главы ВСЕ ТРИ оказались AI-сгенерированными. Мерить на
|
||||||
|
них перевод нельзя — это не человеческая проза, и «невиданность» такого текста ничего не значит.
|
||||||
|
Кандидаты с ключами `AI直接使用`/`AI支援`/`生成AI` отбрасываются механически.
|
||||||
|
(2) **Прежний источник `isekai_majutsushi_jp` (novel18) СНЯТ:** гардрейл 18+ уровня 3 дал 3
|
||||||
|
срабатывания из 184 чанков, помеченное не читалось, обработка прекращена. Замена подобрана по
|
||||||
|
слову владельца 10.08.
|
||||||
|
|
||||||
⚠⚠ ГАРДРЕЙЛ ПЕРВЫМ, ДО ЛЮБОГО ДРУГОГО ДЕЙСТВИЯ. Гардрейл владельца (вынесен в `eval/README.md`):
|
⚠⚠ ГАРДРЕЙЛ ПЕРВЫМ, ДО ЛЮБОГО ДРУГОГО ДЕЙСТВИЯ. Гардрейл владельца (вынесен в `eval/README.md`):
|
||||||
**18+ уровень 3 — несовершеннолетние в сексуальном контексте — не обрабатывать и не анализировать
|
**18+ уровень 3 — несовершеннолетние в сексуальном контексте — не обрабатывать и не анализировать
|
||||||
|
|
@ -30,7 +42,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SRC = Path.home() / "books" / "isekai_majutsushi_jp.txt"
|
SRC = Path.home() / "books" / "enkan_no_hate_ja.txt"
|
||||||
OUT = Path.home() / "books" / "dovodka"
|
OUT = Path.home() / "books" / "dovodka"
|
||||||
MANIFEST = OUT / "manifest-ja.json"
|
MANIFEST = OUT / "manifest-ja.json"
|
||||||
TARGET_CHARS, MIN_CHARS, N_UNITS = 1100, 700, 9 # ja плотнее zh на знак: цель короче
|
TARGET_CHARS, MIN_CHARS, N_UNITS = 1100, 700, 9 # ja плотнее zh на знак: цель короче
|
||||||
|
|
@ -53,13 +65,20 @@ def text() -> str:
|
||||||
|
|
||||||
def chapters() -> list[dict]:
|
def chapters() -> list[dict]:
|
||||||
"""Главы по разделителю площадки; заголовок остаётся в теле — он часть материала."""
|
"""Главы по разделителю площадки; заголовок остаётся в теле — он часть материала."""
|
||||||
|
# ⚠ Разделитель — заголовок вида `=== 7 · 身に覚えのない署名 ===`, который ставит качалка.
|
||||||
|
# Первая редакция резала по `\n=+\n` и НЕ находила его: весь файл становился ОДНОЙ главой,
|
||||||
|
# отбор давал девять соседних кусков одной сцены, и прирост мощности был бы мнимым. Ровно
|
||||||
|
# эту ошибку эксп-23 §11 ловил у себя («главы эксп-22 исключены ЦЕЛИКОМ, иначе новые единицы
|
||||||
|
# делили бы с ними сцену и лексику»). Поймано печатью «глав 1».
|
||||||
raw = text()
|
raw = text()
|
||||||
parts = re.split(r"\n=+\n", raw)
|
|
||||||
out = []
|
out = []
|
||||||
for i, p in enumerate(parts):
|
for m in re.finditer(r"^=== (\d+) · (.*?) ===$\n(.*?)(?=^=== \d+ · |\Z)",
|
||||||
p = p.strip()
|
raw, re.M | re.S):
|
||||||
if len(p) >= MIN_CHARS:
|
body = m.group(3).strip()
|
||||||
out.append(dict(idx=i, text=p))
|
if len(body) >= MIN_CHARS:
|
||||||
|
out.append(dict(idx=int(m.group(1)), text=body))
|
||||||
|
if not out:
|
||||||
|
raise SystemExit("⛔ ни одной главы не распознано — разделитель не тот, отбор НЕ делается")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,18 @@
|
||||||
сторож перед КАЖДЫМ вызовом по цене ИМЕННО ЭТОЙ модели, два независимых пути счёта. Копирование
|
сторож перед КАЖДЫМ вызовом по цене ИМЕННО ЭТОЙ модели, два независимых пути счёта. Копирование
|
||||||
размножило бы будущие расхождения вместо того, чтобы их ловить.
|
размножило бы будущие расхождения вместо того, чтобы их ловить.
|
||||||
|
|
||||||
ПОТОЛОК ФАЗЫ — САНКЦИЯ ВЛАДЕЛЬЦА $4.50 (10.08). История цифры: промт называл $10.00, владелец при
|
ПОТОЛОК ФАЗЫ — САНКЦИЯ ВЛАДЕЛЬЦА $5.40 (11.08). История цифры: промт называл $10.00, владелец при
|
||||||
запуске сказал $4.00, затем поднял до $4.50, когда приёмка другого модельного семейства нашла, что
|
запуске сказал $4.00, затем поднял до $4.50, когда приёмка другого модельного семейства нашла, что
|
||||||
дизайн не отвечает на H-1 и H-4, а починка стоит $0.48.
|
дизайн не отвечает на H-1 и H-4, а починка стоит $0.48. 11.08 ревью 82 находок показало, что смета
|
||||||
|
занижала фиксера в 2.41× и фаза не влезает в $4.50 ни при каком порядке покупок; владелец выбрал
|
||||||
|
вариант «всё как заказано» и поднял потолок до $5.40.
|
||||||
|
|
||||||
⚠ РЕЗЕРВ ФАЗЫ $0.03 — 0.7% пакета, и это объявлено риском ДО покупок (Д0.8). Ре-гены эхо-мины и
|
⚠ РЕЗЕРВ ФАЗЫ $0.00 — и это объявлено риском ДО покупок ВТОРОЙ раз. При $4.50 резерв был $0.03,
|
||||||
ретраи в паках 22/23 съедали до +16% сверх сметы фазы. Сессия называла честным потолком $4.80;
|
и первый же промах сметы остановил ось Д4 на 22 клетках из 32. При $5.40 остатка нет вовсе:
|
||||||
владелец выбрал $4.50. Поэтому правило исполнения жёсткое и записано здесь, а не в прозе:
|
честная смета остатка $4.277 плюс потраченные $1.140 дают $5.417, то есть уже $0.017 сверх
|
||||||
|
санкции, и Д1 добирает 15 клеток вместо 16 именно поэтому. Резерв 8% на ре-гены (история паков
|
||||||
|
22/23 — до +16%) потребовал бы $5.85. Сказано ДО покупок, а не после.
|
||||||
|
Поэтому правило исполнения жёсткое и записано здесь, а не в прозе:
|
||||||
**срабатывание проекционного гарда = СТОП и вопрос владельцу.** Резать ось, панель или кандидата
|
**срабатывание проекционного гарда = СТОП и вопрос владельцу.** Резать ось, панель или кандидата
|
||||||
решением сессии ЗАПРЕЩЕНО заказом; поднимать потолок себе — тем более (пак 23 так и сделал, и это
|
решением сессии ЗАПРЕЩЕНО заказом; поднимать потолок себе — тем более (пак 23 так и сделал, и это
|
||||||
стоило ему пункта CONFIRM/DENY).
|
стоило ему пункта CONFIRM/DENY).
|
||||||
|
|
@ -64,9 +69,23 @@ TAGS = {
|
||||||
# пакетный потолок. Первая редакция округляла каждый вверх до цента и дала сумму 4.54 > 4.50 —
|
# пакетный потолок. Первая редакция округляла каждый вверх до цента и дала сумму 4.54 > 4.50 —
|
||||||
# то есть фазовые гарды в совокупности разрешали пробить пакетный. Поймано селфтестом кассы
|
# то есть фазовые гарды в совокупности разрешали пробить пакетный. Поймано селфтестом кассы
|
||||||
# ДО первой покупки; ровно этот класс («касса охраняет не ту границу») стоил эксп-21 денег трижды.
|
# ДО первой покупки; ровно этот класс («касса охраняет не ту границу») стоил эксп-21 денег трижды.
|
||||||
CEILINGS = {"ФД-A": 0.885, "ФД-B": 0.560, "ФД-C": 0.155, "ФД-D": 0.055,
|
#
|
||||||
"ФД-E": 2.355, "ФД-F": 0.283, "ФД-G": 0.202}
|
# ⚠⚠ ПЕРЕ-РАСКЛАДКА 11.08 ПОД САНКЦИЮ $5.40. Прежняя раскладка стояла на смете, где фиксер положен
|
||||||
PACK_CEILING = 4.50
|
# в 0.60× переписа, а замерен в 2.41× (`plan.py` сверяет прогноз с фактом и печатает расхождение).
|
||||||
|
# Из-за этого ФД-A исчерпалась на 22 клетках арма A4 из 32, а арм A2 не куплен ни разу. Новая
|
||||||
|
# раскладка считана по ЗАМЕРЕННЫМ ценам 148 купленных клеток фазы, а не по прогнозу.
|
||||||
|
#
|
||||||
|
# ⚠ ПРИОРИТЕТ ВНУТРИ ФД-A ОБЪЯВЛЯЕТСЯ ЗДЕСЬ: ПЕРВЫМ покупается СВОЙ ШУМОВОЙ ПОЛ. Причина не
|
||||||
|
# бухгалтерская, а решающая: при заимствованном прайоре sd 2.12 и честном квантиле Стьюдента все
|
||||||
|
# три первичных контраста оси Д4 НЕДОМОЩНЫ (MDE 1.67…2.32 против цели 1.50). Несущей ось делает
|
||||||
|
# только СВОЙ пол — при sd ≤ 1.91 контрасты A1/A0 и A3/A0 становятся несущими. Покупать армы,
|
||||||
|
# не зная пола, значит рисковать всей сметой оси, которая может не дать вывода ни при каком исходе.
|
||||||
|
#
|
||||||
|
# ⚠ ЧТО НЕ ВЛЕЗЛО В $5.40 И ОБЪЯВЛЕНО ДО ПОКУПОК: Д1 добирает 15 клеток gemini из 16. Шестнадцатая
|
||||||
|
# стоит $0.147 и требует потолка $5.45. Пре-рег Д0.3 говорит прямо: несущей ось Д1 делают ПОЛНЫЕ 16.
|
||||||
|
CEILINGS = {"ФД-A": 1.640, "ФД-B": 0.620, "ФД-C": 0.160, "ФД-D": 0.055,
|
||||||
|
"ФД-E": 2.305, "ФД-F": 0.410, "ФД-G": 0.210}
|
||||||
|
PACK_CEILING = 5.40
|
||||||
|
|
||||||
M22.configure(ZONE=ZONE, OUT=OUT, TAGS=TAGS, CEILINGS=CEILINGS,
|
M22.configure(ZONE=ZONE, OUT=OUT, TAGS=TAGS, CEILINGS=CEILINGS,
|
||||||
PACK_CEILING=PACK_CEILING, GLOB_ALL="dv-*.json", ROSTER=ROSTER)
|
PACK_CEILING=PACK_CEILING, GLOB_ALL="dv-*.json", ROSTER=ROSTER)
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,21 @@ PRIMARY = {
|
||||||
"Д3 en→ru несущая": ["E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)"],
|
"Д3 en→ru несущая": ["E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)"],
|
||||||
"Д1 gemini добивка": ["R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)"],
|
"Д1 gemini добивка": ["R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)"],
|
||||||
}
|
}
|
||||||
|
# ⚠⚠ n У КАЖДОГО КОНТРАСТА СВОЙ, И ЭТО РЕШАЕТ. Прежняя редакция печатала MDE и статус ПО ОСИ при
|
||||||
|
# плановом n=32 — а члены первичного семейства живут на разных выборках, потому что база у них
|
||||||
|
# разная. `A6/A0` стоит на 16 единицах ПО ПОСТРОЕНИЮ: черновик dspro эксп-22 существует только у
|
||||||
|
# 16 старых единиц. При n=16 его MDE вдвое больше цели, то есть НОСИТЕЛЬ H-1 не мог дать несущего
|
||||||
|
# вывода никогда — и это было известно ДО денег, просто никто не считал мощность поконтрастно.
|
||||||
|
# Ровно этот класс («ось объявлена несущей, контраст недомощен») погубил en-ось эксп-22.
|
||||||
|
# Числа ниже — фактические выборки после исключения единицы с эхо-мином (contour.EXCLUDED).
|
||||||
|
CONTRAST_N = {
|
||||||
|
"A1/A0 — флаговый контур (H-2а)": 31,
|
||||||
|
"A3/A0 — смысловой контур (H-2б, ставка владельца)": 31,
|
||||||
|
"A6/A0 — качественный черновик + контур (H-1)": 16,
|
||||||
|
"E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)": 16,
|
||||||
|
"R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)": 16,
|
||||||
|
}
|
||||||
|
|
||||||
SECONDARY = {
|
SECONDARY = {
|
||||||
"Д4 edit-контур zh": ["A2/A0 — однопроходка уравненного мандата (строка 153 бэклога)",
|
"Д4 edit-контур zh": ["A2/A0 — однопроходка уравненного мандата (строка 153 бэклога)",
|
||||||
"A4/A0 — канон-фиксер ПОСЛЕ переписа: его несущая ось — покрытие "
|
"A4/A0 — канон-фиксер ПОСЛЕ переписа: его несущая ось — покрытие "
|
||||||
|
|
@ -114,18 +129,36 @@ def _z(p: float) -> float:
|
||||||
return statistics.NormalDist().inv_cdf(p)
|
return statistics.NormalDist().inv_cdf(p)
|
||||||
|
|
||||||
|
|
||||||
def mde(sd: float, n: int, k: int = 1, between: bool = True) -> float:
|
def _t(p: float, df: int) -> float:
|
||||||
|
"""Квантиль Стьюдента. ⚠ Первая редакция считала его самописной бисекцией по неполной бете —
|
||||||
|
и та молча возвращала НОЛЬ на половине входов, то есть обнуляла MDE и печатала «НЕСУЩАЯ» там,
|
||||||
|
где мощности нет вовсе. Поймано сверкой с табличными значениями ДО того, как на числе встали.
|
||||||
|
Мораль та же, что у всей фазы: свою арифметику сверяй с эталоном, а не с ожиданием.
|
||||||
|
"""
|
||||||
|
from scipy import stats # noqa: PLC0415
|
||||||
|
return float(stats.t.ppf(p, df))
|
||||||
|
|
||||||
|
|
||||||
|
# ⚠ ЧИСЛО СТЕПЕНЕЙ СВОБОДЫ ПРАЙОРА. sd берётся не из воздуха и не «известна»: она ОЦЕНЕНА на
|
||||||
|
# полу прошлого пака с n=16, то есть df=15. Прежняя редакция брала квантиль НОРМАЛИ, как если бы
|
||||||
|
# дисперсия была известна точно, и это давало оси Д4 MDE 1.49 против цели 1.50 — «несущая» с
|
||||||
|
# запасом 0.7%. При честном t тот же дизайн даёт 1.56, то есть ОПИСАТЕЛЬНУЮ ось. Разница между
|
||||||
|
# «несущая» и «описательная» здесь целиком сидела в подмене t на z; поправка внесена 11.08.
|
||||||
|
PRIOR_DF = 15
|
||||||
|
|
||||||
|
|
||||||
|
def mde(sd: float, n: int, k: int = 1, between: bool = True, df: int = PRIOR_DF) -> float:
|
||||||
"""Минимально различимый эффект при 80% мощности и поправке Холма по k контрастам.
|
"""Минимально различимый эффект при 80% мощности и поправке Холма по k контрастам.
|
||||||
|
|
||||||
⚠ КОНСТАНТА 2.8 ВЕРНА ТОЛЬКО ПРИ k=1, и первая редакция этого файла применяла её всегда.
|
⚠ КОНСТАНТА 2.8 ВЕРНА ТОЛЬКО ПРИ k=1, и первая редакция этого файла применяла её всегда.
|
||||||
Поймано адверсариальным ревью снизу вверх. Решающее правило пака — `p` ХОЛМА < 0.05, то есть
|
Поймано адверсариальным ревью снизу вверх. Решающее правило пака — `p` ХОЛМА < 0.05, то есть
|
||||||
на самый строгий контраст семейства приходится α/k; множитель становится
|
на самый строгий контраст семейства приходится α/k; множитель становится
|
||||||
(z_{1−α/2k} + z_{0.8}), при k=5 это 3.417 вместо 2.8. Пропущенная поправка ×1.22–1.36 БОЛЬШЕ
|
(t_{1−α/2k} + t_{0.8}), при k=5 это ≈3.6 вместо 2.8. Пропущенная поправка ×1.22–1.36 БОЛЬШЕ
|
||||||
той ×1.23, которую файл старательно вносил за межсессионную компоненту, — и она переворачивала
|
той ×1.23, которую файл старательно вносил за межсессионную компоненту, — и она переворачивала
|
||||||
вердикт: Д4 при k=5 давала MDE 1.57 против цели 1.50, то есть ОПИСАТЕЛЬНУЮ ось, объявленную
|
вердикт: Д4 при k=5 давала MDE 1.57 против цели 1.50, то есть ОПИСАТЕЛЬНУЮ ось, объявленную
|
||||||
несущей. Это буквально провал en-оси эксп-22, ради недопущения которого файл и написан.
|
несущей. Это буквально провал en-оси эксп-22, ради недопущения которого файл и написан.
|
||||||
"""
|
"""
|
||||||
mult = _z(1 - 0.05 / (2 * max(1, k))) + _z(0.80)
|
mult = _t(1 - 0.05 / (2 * max(1, k)), df) + _t(0.80, df)
|
||||||
return mult * sd / math.sqrt(n) * (BETWEEN if between else 1.0)
|
return mult * sd / math.sqrt(n) * (BETWEEN if between else 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -155,6 +188,28 @@ def main() -> int:
|
||||||
print(f"{name:24s}{n:4d}{k:3d}{sd:6.2f}{m_true:7.2f}{target:7.2f}{p0:11.4f} {status}")
|
print(f"{name:24s}{n:4d}{k:3d}{sd:6.2f}{m_true:7.2f}{target:7.2f}{p0:11.4f} {status}")
|
||||||
print(f"\nописательных осей: {desc} из {len(AXES)} — объявлено ДО покупок, "
|
print(f"\nописательных осей: {desc} из {len(AXES)} — объявлено ДО покупок, "
|
||||||
"пере-классификации задним числом не будет")
|
"пере-классификации задним числом не будет")
|
||||||
|
# ⚠ ПОКОНТРАСТНАЯ МОЩНОСТЬ — то, по чему решение принимается на самом деле.
|
||||||
|
print("\nПОКОНТРАСТНАЯ МОЩНОСТЬ ПЕРВИЧНЫХ СЕМЕЙСТВ (n у каждого СВОЙ — решает он, не ось):")
|
||||||
|
print(f" {'контраст':56s}{'n':>4s}{'MDE':>7s}{'ЦЕЛЬ':>7s} вердикт")
|
||||||
|
weak = []
|
||||||
|
for name, n_ax, k, sd, target, _w1, _w2 in AXES:
|
||||||
|
for c in PRIMARY.get(name, []):
|
||||||
|
n_c = CONTRAST_N.get(c, n_ax)
|
||||||
|
m = mde(sd, n_c, k)
|
||||||
|
ok = m <= target
|
||||||
|
if not ok:
|
||||||
|
weak.append((c, n_c, m, target))
|
||||||
|
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)
|
||||||
|
print(f" • {c}\n при n={n_c} MDE {m:.2f} > цели {t:.2f}; несущим станет "
|
||||||
|
f"либо при своём поле sd ≤ {need_sd:.2f}, либо при n ≥ {need_n}")
|
||||||
print("\nСЕМЕЙСТВА, объявленные ДО покупок:")
|
print("\nСЕМЕЙСТВА, объявленные ДО покупок:")
|
||||||
for name in [a[0] for a in AXES]:
|
for name in [a[0] for a in AXES]:
|
||||||
if name in PRIMARY:
|
if name in PRIMARY:
|
||||||
|
|
@ -166,7 +221,7 @@ def main() -> int:
|
||||||
for c in SECONDARY[name]:
|
for c in SECONDARY[name]:
|
||||||
print(f" · {c}")
|
print(f" · {c}")
|
||||||
print("\nОБОСНОВАНИЯ (прайор шума · искомый эффект):")
|
print("\nОБОСНОВАНИЯ (прайор шума · искомый эффект):")
|
||||||
for name, _n, _k, _sd, _t, w1, w2 in AXES:
|
for name, _n, _k, _sd, _tg, w1, w2 in AXES:
|
||||||
print(f" {name}\n прайор: {w1}\n цель: {w2}")
|
print(f" {name}\n прайор: {w1}\n цель: {w2}")
|
||||||
print("\nПРОСТРАНСТВО ДИЗАЙНА — честный MDE (с ×1.23) при разных n и разных полах:")
|
print("\nПРОСТРАНСТВО ДИЗАЙНА — честный MDE (с ×1.23) при разных n и разных полах:")
|
||||||
print(" " + "".join(f"{f'sd {s:.2f}':>10s}" for s in GRID_SD))
|
print(" " + "".join(f"{f'sd {s:.2f}':>10s}" for s in GRID_SD))
|
||||||
|
|
|
||||||
247
eval/editor_tier/axiscal.py
Normal file
247
eval/editor_tier/axiscal.py
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""ОСЕВОЙ КАЛИБРАТОР СУДЕЙСКИХ СЕМЕЙСТВ. $0 — только чтение уже купленного сырья.
|
||||||
|
|
||||||
|
Отвечает на вопрос владельца «как синхронизировать судей разных семейств»: шкалу синхронизировать
|
||||||
|
нечем и не надо (риг уже нормирован контрастом внутри единицы, порогом из своего пола и декоем как
|
||||||
|
единицей измерения), а сверять надо ЧУВСТВИТЕЛЬНОСТЬ ПО ОСЯМ — долю пойманного дефекта ИЗВЕСТНОГО
|
||||||
|
типа и правильность отнесения его на ось.
|
||||||
|
|
||||||
|
Прибор состоит из трёх замеров, и все три считаются здесь:
|
||||||
|
|
||||||
|
1. АТРИБУЦИЯ. Посадка декоя восстанавливается побайтно (сверка sha1 с ключом), каждый посаженный
|
||||||
|
дефект локализуется в тексте, и по цитатам судьи проверяется: процитирован ли он и назван ли
|
||||||
|
под ТОЙ осью, к которой правило посадки относится. Числа по осям для этого не годятся —
|
||||||
|
прирост числа не доказывает, что судья увидел именно посаженное.
|
||||||
|
2. ДИНАМИЧЕСКИЙ ДИАПАЗОН. Уровень оси на боевых клетках и шум пола ПО ОСИ. Ось, у которой на
|
||||||
|
реальном тексте уровень ~0 и шум ровно 0, контраст армов нести не может, даже если посаженное
|
||||||
|
на ней ловится 16/16.
|
||||||
|
3. ЗАРАЖЕНИЕ. Ошибки в декой-клетке ВНЕ посаженных мест против той же клетки без порчи: сколько
|
||||||
|
семейство доначисляет тексту просто за то, что он уже пойман испорченным.
|
||||||
|
|
||||||
|
⚠ ЧЕГО ПРИБОР НЕ МЕРЯЕТ — см. вывод `границы`: посадка даёт ГРУБЫЕ дефекты, а спор семейств живёт
|
||||||
|
в маргинальном режиме, и на грубых обе семьи упираются в потолок.
|
||||||
|
|
||||||
|
Запуск: eval/.venv/bin/python eval/editor_tier/axiscal.py [tier]
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import statistics as st
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ZONE = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(ZONE))
|
||||||
|
|
||||||
|
import judge as J # noqa: E402
|
||||||
|
|
||||||
|
OUT = Path.home() / "books" / "editor-tier"
|
||||||
|
AXES = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА")
|
||||||
|
# Ось, к которой ПО ЗАМЫСЛУ относится каждое правило `blind-keys/PLANTS.json`. Порядок правил —
|
||||||
|
# порядок файла; правило, которого в файле нет, сюда не попадает.
|
||||||
|
RULE_AXIS = {0: "ВЕРНОСТЬ", 1: "ВЕРНОСТЬ", 2: "ВЕРНОСТЬ", 3: "ЯЗЫК", 4: "ФОРМА", 5: "ФОРМА"}
|
||||||
|
CTX = 40 # окно контекста вокруг посаженного места, в котором цитата считается попавшей
|
||||||
|
|
||||||
|
|
||||||
|
def plants() -> list:
|
||||||
|
return json.loads((OUT / "blind-keys" / "PLANTS.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def trace(text: str) -> tuple[str, list[dict]]:
|
||||||
|
"""Посадка с отслеживанием ФИНАЛЬНЫХ смещений: каждая следующая замена сдвигает прежние.
|
||||||
|
Без пересчёта смещений метка «где посажено» указывает не туда, и атрибуция врёт."""
|
||||||
|
out, marks = text, []
|
||||||
|
for i, (pat, rep, _c) in enumerate(plants()):
|
||||||
|
m = re.search(pat, out)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
new = re.sub(pat, rep, m.group(0), count=1)
|
||||||
|
delta = len(new) - len(m.group(0))
|
||||||
|
for mk in marks:
|
||||||
|
if mk["beg"] >= m.end():
|
||||||
|
mk["beg"] += delta
|
||||||
|
mk["end"] += delta
|
||||||
|
marks.append(dict(rule=i, beg=m.start(), end=m.start() + len(new)))
|
||||||
|
out = out[:m.start()] + new + out[m.end():]
|
||||||
|
return out, marks
|
||||||
|
|
||||||
|
|
||||||
|
def quotes(why: str) -> list[tuple[str, str]]:
|
||||||
|
"""(ось, цитата). Ось — последняя названная перед цитатой: так пишут обе семьи, и на этом же
|
||||||
|
стоит приёмка ответа (`solscore.votes`), поэтому разбор не вводит нового допущения."""
|
||||||
|
res, cur = [], None
|
||||||
|
for tk in re.finditer(r"(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА)|[«\"“„](.+?)[»\"”]", why):
|
||||||
|
if tk.group(1):
|
||||||
|
cur = tk.group(1)
|
||||||
|
elif cur:
|
||||||
|
res.append((cur, tk.group(2)))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def norm(s: str) -> str:
|
||||||
|
return re.sub(r"\s+", " ", s).strip().lower().replace("ё", "е")
|
||||||
|
|
||||||
|
|
||||||
|
def votes_claude(key: dict) -> dict:
|
||||||
|
out = {}
|
||||||
|
for f in sorted((OUT / "aj-tier-votes").glob("*.json")):
|
||||||
|
v = json.loads(f.read_text(encoding="utf-8"))
|
||||||
|
out[next(iter(v.values()))["uid"]] = {m["arm"]: m for m in v.values()}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def votes_sol(key: dict) -> dict:
|
||||||
|
"""Разбор ответов внешнего судьи. Числа и ПОЧЕМУ читаются теми же выражениями, что в
|
||||||
|
`solscore.votes`; здесь дополнительно СОХРАНЯЕТСЯ разбивка по осям, которую тот схлопывает."""
|
||||||
|
out = {}
|
||||||
|
for f in sorted((OUT / "sol-tier").glob("*.txt")):
|
||||||
|
k = key.get(f.stem)
|
||||||
|
if not k:
|
||||||
|
continue
|
||||||
|
txt, unit = f.read_text(encoding="utf-8"), {}
|
||||||
|
for lab, meta in k.items():
|
||||||
|
ax = {}
|
||||||
|
for a in AXES:
|
||||||
|
m = re.search(rf"^{lab}-{a}[ \t]*:[ \t]*(\d+)[ \t]*$", txt, re.M)
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
ax[a] = int(m.group(1))
|
||||||
|
if len(ax) != len(AXES):
|
||||||
|
break
|
||||||
|
wm = re.search(rf"^{lab}-ПОЧЕМУ[ \t]*:[ \t]*(.*?)(?=^\S+-(?:{'|'.join(AXES)}|ПОЧЕМУ)"
|
||||||
|
rf"[ \t]*:|\Z)", txt, re.M | re.S)
|
||||||
|
unit[meta["arm"]] = dict(meta, why=(wm.group(1) if wm else "").strip(), **ax)
|
||||||
|
if len(unit) == len(k):
|
||||||
|
out[next(iter(k.values()))["uid"]] = unit
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def attribute(data: dict, hook, units: dict) -> tuple[dict, dict]:
|
||||||
|
"""(uid, правило) -> поймано-с-верной-осью · плюс счётчики цитат вне посадки."""
|
||||||
|
hit, tally = {}, dict(quotes=0, on=0, off=0, nomatch=0, base=0, decoy=0)
|
||||||
|
for uid, d in data.items():
|
||||||
|
if "CTRLdecoy" not in d:
|
||||||
|
continue
|
||||||
|
donor = d["CTRLdecoy"].get("decoy_from") or "R0"
|
||||||
|
planted, marks = trace(hook(donor, units[uid]))
|
||||||
|
sig = hashlib.sha1(planted.encode()).hexdigest()[:12] # noqa: S324
|
||||||
|
if sig != d["CTRLdecoy"]["sig"]:
|
||||||
|
raise SystemExit(f"⛔ посадка не восстановлена для {uid}: {sig} ≠ {d['CTRLdecoy']['sig']}")
|
||||||
|
pn = norm(planted)
|
||||||
|
tally["base"] += sum(d[donor][a] for a in AXES)
|
||||||
|
tally["decoy"] += sum(d["CTRLdecoy"][a] for a in AXES)
|
||||||
|
spans = [(norm(planted[max(0, mk["beg"] - CTX):mk["end"] + CTX]), mk) for mk in marks]
|
||||||
|
qs = []
|
||||||
|
for ax, q in quotes(d["CTRLdecoy"]["why"]):
|
||||||
|
tally["quotes"] += 1
|
||||||
|
nq = norm(q)
|
||||||
|
i = pn.find(nq)
|
||||||
|
if i < 0:
|
||||||
|
tally["nomatch"] += 1
|
||||||
|
continue
|
||||||
|
qs.append((ax, i, i + len(nq)))
|
||||||
|
for ctx, mk in spans:
|
||||||
|
c0 = pn.find(ctx)
|
||||||
|
over = [ax for ax, b, e in qs if c0 >= 0 and b < c0 + len(ctx) and e > c0]
|
||||||
|
hit[(uid, mk["rule"])] = int(RULE_AXIS[mk["rule"]] in over)
|
||||||
|
for _ax, b, e in qs:
|
||||||
|
on = any(pn.find(ctx) >= 0 and b < pn.find(ctx) + len(ctx) and e > pn.find(ctx)
|
||||||
|
for ctx, _ in spans)
|
||||||
|
tally["on" if on else "off"] += 1
|
||||||
|
return hit, tally
|
||||||
|
|
||||||
|
|
||||||
|
def mcnemar(b: int, c: int) -> float:
|
||||||
|
from math import comb # noqa: PLC0415
|
||||||
|
n = b + c
|
||||||
|
if n == 0:
|
||||||
|
return 1.0
|
||||||
|
return min(1.0, 2 * sum(comb(n, i) for i in range(min(b, c) + 1)) / 2 ** n)
|
||||||
|
|
||||||
|
|
||||||
|
def need_pairs(p1: float, p2: float, alpha: float = 0.0125, power: float = 0.80) -> int:
|
||||||
|
"""Парная схема (один и тот же посаженный дефект читают оба семейства) — McNemar.
|
||||||
|
Независимость промахов между семьями даёт МАКСИМУМ доли расхождений, то есть оценка сверху."""
|
||||||
|
za = {0.05: 1.96, 0.025: 2.2414, 0.0167: 2.394, 0.0125: 2.4977}[alpha]
|
||||||
|
zb = {0.80: 0.8416, 0.90: 1.2816}[power]
|
||||||
|
p01, p10 = p1 * (1 - p2), (1 - p1) * p2
|
||||||
|
psi, d = p01 + p10, abs(p01 - p10)
|
||||||
|
if d <= 0:
|
||||||
|
return 10 ** 9
|
||||||
|
return math.ceil((za * math.sqrt(psi) + zb * math.sqrt(psi - d * d)) ** 2 / d ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
pass_ = (sys.argv[1:] or ["tier"])[0]
|
||||||
|
J.setup(pass_)
|
||||||
|
key = json.loads((OUT / "blind-keys" / f"{pass_}-KEY.json").read_text(encoding="utf-8"))
|
||||||
|
hook, units = J.text_of(pass_), {u["uid"]: u for u in J.units(pass_)}
|
||||||
|
fam = {"Claude": votes_claude(key), "Sol 5-6": votes_sol(key)}
|
||||||
|
uids = sorted(set.intersection(*(set(v) for v in fam.values())))
|
||||||
|
print(f"проход {pass_} · единиц у обоих семейств {len(uids)} · правил посадки {len(plants())}")
|
||||||
|
print(f"⚠ на ось ТЕРМИН посадки НЕТ: {sorted(set(RULE_AXIS.values()))}")
|
||||||
|
|
||||||
|
H, T = {}, {}
|
||||||
|
for n, d in fam.items():
|
||||||
|
H[n], T[n] = attribute({u: d[u] for u in uids}, hook, units)
|
||||||
|
print("\n1. АТРИБУЦИЯ ПОСАЖЕННОГО (поймано И названо верной осью)")
|
||||||
|
print(f" {'правило':<36} {'ось':<9} {'n':>3} " + " ".join(f"{n:>8}" for n in fam))
|
||||||
|
for i, (_p, _r, cmt) in enumerate(plants()):
|
||||||
|
n_i = sum(1 for (u, r) in H[next(iter(fam))] if r == i)
|
||||||
|
if not n_i:
|
||||||
|
continue
|
||||||
|
cols = [sum(v for (u, r), v in H[n].items() if r == i) / n_i for n in fam]
|
||||||
|
print(f" R{i} {cmt:<32} {RULE_AXIS[i]:<9} {n_i:>3} " + " ".join(f"{c:>8.2f}" for c in cols))
|
||||||
|
print(f"\n {'ось':<9} {'дефектов':>8} " + " ".join(f"{n:>8}" for n in fam) + " McNemar p")
|
||||||
|
names = list(fam)
|
||||||
|
for a in AXES:
|
||||||
|
keys = [k for k in H[names[0]] if RULE_AXIS[k[1]] == a]
|
||||||
|
if not keys:
|
||||||
|
print(f" {a:<9} {'—':>8} посадок нет — ось прибором НЕ ПОКРЫТА")
|
||||||
|
continue
|
||||||
|
b = sum(1 for k in keys if H[names[0]][k] and not H[names[1]][k])
|
||||||
|
c = sum(1 for k in keys if not H[names[0]][k] and H[names[1]][k])
|
||||||
|
cols = [sum(H[n][k] for k in keys) / len(keys) for n in names]
|
||||||
|
print(f" {a:<9} {len(keys):>8} " + " ".join(f"{x:>8.2f}" for x in cols)
|
||||||
|
+ f" {mcnemar(b, c):.3f} (расхождений {b}/{c})")
|
||||||
|
|
||||||
|
print("\n2. ДИНАМИЧЕСКИЙ ДИАПАЗОН ОСИ (уровень боевых клеток · шум пола · порог 2.8·sd/√n)")
|
||||||
|
for n, d in fam.items():
|
||||||
|
print(f" {n}:")
|
||||||
|
arms = [a for a in J.arms(pass_)]
|
||||||
|
for a in AXES:
|
||||||
|
lv = [d[u][x][a] for u in uids for x in arms if x in d[u]]
|
||||||
|
fl = [d[u]["CTRLfloorA"][a] - d[u]["CTRLfloorB"][a] for u in uids]
|
||||||
|
thr = 2.8 * st.pstdev(fl) / math.sqrt(len(fl))
|
||||||
|
dead = " ⛔ ОСЬ МЁРТВАЯ: контраст армов нести не может" if st.pstdev(fl) == 0 else ""
|
||||||
|
print(f" {a:<9} уровень {st.mean(lv):6.2f}/клетку · шум пола sd {st.pstdev(fl):4.2f}"
|
||||||
|
f" · порог {thr:4.2f}{dead}")
|
||||||
|
|
||||||
|
print("\n3. ЗАРАЖЕНИЕ (цитаты в декой-клетке ВНЕ посадки против той же клетки без порчи)")
|
||||||
|
for n in fam:
|
||||||
|
t = T[n]
|
||||||
|
print(f" {n:<8} база {t['base']:>4} · декой {t['decoy']:>4} · цитат {t['quotes']:>4} "
|
||||||
|
f"(на посадке {t['on']:>3} · вне {t['off']:>3} · не найдено в тексте {t['nomatch']:>3})"
|
||||||
|
f" → заражение {t['off'] - t['base']:+d}")
|
||||||
|
print(f" сигнал/фон посадки: {(t['decoy'] - t['base']) / max(t['base'], 1):.2f}")
|
||||||
|
|
||||||
|
print("\n4. ГРАНИЦЫ ПРИБОРА ПРИ ТЕКУЩЕМ ОБЪЁМЕ (парный McNemar, α=0.0125 Холм, мощность 0.80)")
|
||||||
|
for a in AXES:
|
||||||
|
keys = [k for k in H[names[0]] if RULE_AXIS[k[1]] == a]
|
||||||
|
if not keys:
|
||||||
|
continue
|
||||||
|
best = min(((d / 100, p / 100) for d in range(1, 100) for p in range(1, 100 - d)
|
||||||
|
if need_pairs(p / 100 + d / 100, p / 100) <= len(keys)), default=None)
|
||||||
|
got = f"{best[0]:.2f}" if best else ">0.99"
|
||||||
|
print(f" {a:<9} дефектов {len(keys):>3} → различим разрыв долей ≥ {got}")
|
||||||
|
print(" надо дефектов на ось: " + " · ".join(
|
||||||
|
f"Δ{p1 - p2:.2f} ({p1:.2f}/{p2:.2f}) → {need_pairs(p1, p2)}"
|
||||||
|
for p1, p2 in ((0.90, 0.60), (0.90, 0.70), (0.90, 0.80))))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
Add table
Reference in a new issue