Restore the polygon scripts lost to the 08.08 checkout incident and land the phase 2b arms module: exact-number gate matching, degrees-of-freedom correction, counting drift metric, point-repair arms C and E
This commit is contained in:
parent
a152e18770
commit
caaf36d24f
4 changed files with 489 additions and 25 deletions
|
|
@ -465,7 +465,11 @@ def pool() -> None:
|
|||
m1 = st.mean([r["errors"] for r in a1[u].values()])
|
||||
m2 = st.mean([r["errors"] for r in a2[u].values()])
|
||||
cen += [(E(a1, u, k) - m1) - (E(a2, u, k) - m2) for k in a1[u] if k in a2[u]]
|
||||
vf, vc = st.pvariance(flo), st.pvariance(cen)
|
||||
# ⚠ ПОПРАВКА НА СТЕПЕНИ СВОБОДЫ. Кросс-разности центрированы ВНУТРИ единицы (8–9 клеток),
|
||||
# поэтому `pvariance` занижает σ судьи ~на 11%, и печатный вердикт о доминирующей компоненте
|
||||
# переворачивается. Категорического вердикта нет ни в какую сторону — разрыв меньше смещения.
|
||||
k = st.mean([len(v) for v in a1.values()])
|
||||
vf, vc = st.pvariance(flo), st.pvariance(cen) * k / max(1.0, k - 1)
|
||||
s_j, s_g = (vc / 2) ** 0.5, max(0.0, (vf - vc) / 2) ** 0.5
|
||||
print(f"\nпол var {vf:.2f} (n={len(flo)}) = 2σ²ген + 2σ²судья")
|
||||
print(f"кросс var {vc:.2f} (n={len(cen)}) = 2σ²судья, уровень снят центрированием")
|
||||
|
|
|
|||
|
|
@ -230,42 +230,61 @@ def cmd_score() -> None:
|
|||
# выравнивания и сравнимо МЕЖДУ АРМАМИ на одних главах: держит ли арм канонную форму там,
|
||||
# где zh-термин стоит в исходнике, и сколько раз этот признак ПЕРЕКЛЮЧАЕТСЯ между соседними
|
||||
# главами. Переключение и есть дрейф; удержание — консистентность.
|
||||
print(f"{'арм':5s}{'клеток':>8s}{'канон держится':>16s}{'ПЕРЕКЛЮЧЕНИЙ':>14s}{'на переход':>12s}"
|
||||
f"{'ты/вы смеш.':>13s}")
|
||||
print(f"{'арм':6s}{'клеток':>8s}{'канон/исходник':>14s}{'покрытие':>9s}"
|
||||
f"{'полных':>8s}{'нулей':>8s}{'ты/вы':>12s}")
|
||||
common = [c for c in ch if all(text(a, c["uid"]).strip() for a in DRIFT_ARMS)]
|
||||
print(f" (общих глав у всех армов: {len(common)})")
|
||||
# ⚠ СЧЁТНАЯ МЕТРИКА. Прежняя спрашивала «нашлась ли канонная форма ХОТЯ БЫ РАЗ в главе» и
|
||||
# была ТРИВИАЛЬНО ВЫПОЛНИМА: текст с одним каноном и двадцатью соперничающими формами получал
|
||||
# «держится», эхо самого глоссария без строки перевода — тоже. Здесь считается ПОКРЫТИЕ:
|
||||
# сколько раз zh-термин стоит в исходнике и сколько раз канонная форма стоит в переводе.
|
||||
# (Восстановлено после инцидента 08.08, D39.113.)
|
||||
per_arm = {}
|
||||
for a in DRIFT_ARMS:
|
||||
cells = hold = flips = trans = 0
|
||||
src_n = can_n = 0
|
||||
detail: dict[str, list] = {}
|
||||
for zh, (_v1, _v2, r1, _r2) in BO.IP.TERMS.items():
|
||||
seq = []
|
||||
for c in common:
|
||||
if zh not in c["source"]:
|
||||
s = c["source"].count(zh)
|
||||
if not s:
|
||||
continue
|
||||
cells += 1
|
||||
ok = bool(re.search(r1, text(a, c["uid"]), re.I))
|
||||
hold += ok
|
||||
seq.append((c["no"], ok))
|
||||
for (_n1, o1), (_n2, o2) in zip(seq, seq[1:]):
|
||||
trans += 1
|
||||
flips += o1 != o2
|
||||
k = len(re.findall(r1, text(a, c["uid"]), re.I))
|
||||
src_n += s
|
||||
can_n += min(k, s)
|
||||
seq.append((c["no"], min(1.0, k / s)))
|
||||
if len(seq) > 1:
|
||||
detail[zh] = seq
|
||||
per_arm[a] = detail
|
||||
cov = [x for d in detail.values() for _n, x in d]
|
||||
full = sum(1 for x in cov if x >= 1.0)
|
||||
zero = sum(1 for x in cov if x == 0.0)
|
||||
ty = sum(1 for c in common
|
||||
if re.search(r"(?<![а-яё])ты(?![а-яё])", text(a, c["uid"]), re.I)
|
||||
and re.search(r"(?<![а-яё])вы(?![а-яё])", text(a, c["uid"]), re.I))
|
||||
print(f"{a:5s}{cells:>8d}{hold:>13d}/{cells}{flips:>14d}"
|
||||
f"{(flips / trans if trans else 0):>12.2f}{ty:>10d}/{len(common)}")
|
||||
print(f"{a:6s}{len(cov):>8d}{can_n}/{src_n:<9d}{st.mean(cov):>9.2f}"
|
||||
f"{full:>8d}{zero:>8d}{ty:>10d}/{len(common)}")
|
||||
|
||||
print("\nПОТЕРМИННО (+ канон держится, − арм написал что-то другое):")
|
||||
# ⚠ КЛЕТКИ С ОДНИМ УПОМИНАНИЕМ разрешения не имеют: при s=1 покрытие бинарно, то есть ровно
|
||||
# та метрика, которую приёмка забраковала. Считаются отдельно, а не смешиваются.
|
||||
print("\nТО ЖЕ БЕЗ КЛЕТОК С ОДНИМ УПОМИНАНИЕМ:")
|
||||
for a in DRIFT_ARMS:
|
||||
s2 = c2 = n2 = 0
|
||||
for zh, (_v1, _v2, r1, _r2) in BO.IP.TERMS.items():
|
||||
for c in common:
|
||||
s = c["source"].count(zh)
|
||||
if s < 2:
|
||||
continue
|
||||
s2 += s
|
||||
c2 += min(len(re.findall(r1, text(a, c["uid"]), re.I)), s)
|
||||
n2 += 1
|
||||
print(f" {a:6s} {c2}/{s2} = {c2 / s2 if s2 else 0:.3f} на {n2} клетках")
|
||||
|
||||
print("\nПОТЕРМИННО (покрытие: доля упоминаний исходника, отражённых канонной формой):")
|
||||
for a in DRIFT_ARMS:
|
||||
print(f" {a}:")
|
||||
for zh, seq in sorted(per_arm[a].items()):
|
||||
line = " ".join(f"{n}{'+' if o else '−'}" for n, o in seq)
|
||||
fl = sum(1 for x, y in zip(seq, seq[1:]) if x[1] != y[1])
|
||||
print(f" {zh:6s} {line} переключений {fl}")
|
||||
print(f" {zh:6s} " + " ".join(f"{n}:{o:.2f}" for n, o in seq))
|
||||
|
||||
print("\nРОСТ СЛОВАРЯ (новых слов на главу; растущая кривая = арм не помнит решений):")
|
||||
for a in DRIFT_ARMS:
|
||||
|
|
|
|||
416
eval/role_topology/phase2b.py
Normal file
416
eval/role_topology/phase2b.py
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Ф2б — АРМЫ C И E НА ТЕКУЩЕЙ ПАНЕЛИ. Вторая половина заказа промта (стр.114–115).
|
||||
|
||||
═══════════════════════════════════ ПОСЫЛКА ═══════════════════════════════════
|
||||
|
||||
ЧТО ЗАКАЗАНО. Промт стр.114–115 требует два арма, которых пак не гнал ни разу на боевой панели:
|
||||
C — черновик → детекторы+гейты → ТОЧЕЧНЫЙ РЕМОНТ дешёвым фиксером, в ДВУХ режимах:
|
||||
oracle-флаги (потолок схемы) и реальные детекторы (пол); ре-гейт после КАЖДОГО фикса,
|
||||
кап 2 итерации; печатать recall детекторов на срезе.
|
||||
E — ОБРАТНАЯ связка: сильный черновик (однопроходка `deepseek-v4-pro`) → дешёвый фиксер по флагу.
|
||||
Это и есть главная альтернатива полному переписыванию, ради которой пак затевался после эксп-20.
|
||||
|
||||
ПОЧЕМУ СТАРЫЕ МОДУЛИ НЕ ПЕРЕИСПОЛЬЗУЮТСЯ. `repair_arm.py` и `route_arm.py` считали на СНЯТОЙ
|
||||
восьмиюнитовой панели (до дедупа и хеш-ключей), их записи не несут полей модели и арма, лежат вне
|
||||
всех касс, а `route_arm.py` в текущем дереве падает `TypeError` (сигнатура `text_of` разошлась).
|
||||
Числа оттуда с Ф2а несравнимы. Здесь всё считается на ТЕХ ЖЕ 16 единицах и ТЕХ ЖЕ черновиках.
|
||||
|
||||
ЧТО ИЗМЕНИЛОСЬ С МОМЕНТА ИХ НАПИСАНИЯ — и это ключ к арму C. Старый модуль объявлял: детектора
|
||||
смысловой инверзии (класс k2) НЕТ ВООБЩЕ, поэтому разрыв «oracle против реального» равен доле
|
||||
этого класса, то есть 50% посаженного. **Это устарело.** Мини-бенч §13.7 намерил, что QE-ранкер
|
||||
инверзию ВИДИТ — но на уровне СЕГМЕНТА (66/72, p=3.6e-14), а не окна. Значит реальный режим арма
|
||||
C теперь имеет детектор на оба класса, и разрыв потолка и пола обязан сократиться. Насколько —
|
||||
и есть главный новый вопрос.
|
||||
|
||||
СЕМЕЙСТВО КОНТРАСТОВ, объявленное ДО первой покупки. Поправка Холма по всем пяти.
|
||||
1. C_real против F3 — покупает ли точечный ремонт хоть что-то поверх черновика
|
||||
2. C_real против A_law — точечный ремонт против полного переписывания (главный вопрос)
|
||||
3. E против D_ — покупает ли добивка что-то поверх сильной однопроходки
|
||||
4. E против A_law — обратная связка против прямой
|
||||
5. C_oracle против C_real — разрыв потолка и пола схемы (пре-рег называл это главным по C)
|
||||
|
||||
БАЗА СРАВНЕНИЯ — `A_law`, а не `A`. §16.1 намерил, что редактор без закон-блока единственный из
|
||||
шести армов ломает терминологию черновика (покрытие канона 0.566 против 0.917), поэтому боевой
|
||||
конфигурацией объявлен `A_law`. Сравнивать точечный ремонт надо с тем, что реально пойдёт в прод.
|
||||
|
||||
⚠ ОТКЛОНЕНИЕ ОТ БУКВЫ ПРОМТА, объявленное ЗАРАНЕЕ. Промт требует «черновик С МАНДАТОМ перевёрстки
|
||||
— иначе вёрстка-конфаунд». Здесь арм C правит ТОТ ЖЕ черновик `F3`, что и армы `A`/`A_law`, без
|
||||
мандата. Причина: мандат перевёрстки живёт в арме `D`, и взяв черновик с мандатом, мы потеряли бы
|
||||
ПАРНОСТЬ с `A_law` — а именно она и есть предмет замера. Конфаунд формы при этом мал и измерен:
|
||||
вклад оси ФОРМА в контраст `A/F3` равен +0.06, то есть нулю. Разложение по осям печатается, чтобы
|
||||
это было видно, а не заявлено.
|
||||
|
||||
⚠ ФИКСЕР — `gpt-5.6-luna` с `reasoning_effort="none"`. Ручка документирована для этой модели в
|
||||
`prices.CANDIDATES` (класс `effort_none`) и отработала 30 из 30 непустыми на старой панели.
|
||||
DeepSeek-флеш дешевле по выходу, но у него гашение размышления на переводческой роли запрещено
|
||||
гардрейлом (эхо-мина на плотном CJK), а починка предложения — роль переводческая. Выход починки
|
||||
короткий, поэтому разница в цене в абсолюте копеечная.
|
||||
|
||||
Запуск: --plan что будет куплено и почём, $0
|
||||
--flags посчитать флаги детекторов (механика $0 + QE локально, $0)
|
||||
--arms купить починки (под кассой, тег `p2b-*`)
|
||||
--score свод, $0
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
import statistics as st
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
|
||||
sys.path.insert(0, str(REPO / "eval" / "editor_harness"))
|
||||
|
||||
import bakeoff as BO # noqa: E402
|
||||
import battery as BT # noqa: E402
|
||||
import buy as BUY # noqa: E402
|
||||
import align as AL # noqa: E402
|
||||
import detect_word as DW # noqa: E402
|
||||
import guarded as G # noqa: E402
|
||||
from prices import cost # noqa: E402
|
||||
|
||||
OUT = Path.home() / "books" / "role-topology"
|
||||
FIXER = "gpt-5.6-luna"
|
||||
CEIL_P2B = 1.20 # из незанятых $2.99 фазы Ф2б
|
||||
LED_P2B = BUY.Ledger("Ф2б армы C/E", CEIL_P2B, ("p2b-*.json",), default_expect=0.004)
|
||||
|
||||
FAMILY = [
|
||||
("C_real", "F3", "покупает ли точечный ремонт что-то поверх черновика"),
|
||||
("C_real", "A_law", "точечный ремонт против полного переписывания"),
|
||||
("E", "D_", "покупает ли добивка что-то поверх сильной однопроходки"),
|
||||
("E", "A_law", "обратная связка против прямой"),
|
||||
("C_oracle", "C_real", "разрыв потолка и пола схемы"),
|
||||
]
|
||||
|
||||
_SENT = re.compile(r"[^.!?…]*[.!?…]+[»\"']*\s*|[^.!?…]+$")
|
||||
|
||||
|
||||
def sentences(text: str) -> list[str]:
|
||||
"""Разбиение на предложения. Единица починки — предложение: фиксер правит его целиком."""
|
||||
return [s for s in (m.group(0) for m in _SENT.finditer(text)) if s.strip()]
|
||||
|
||||
|
||||
_BANK = None
|
||||
|
||||
|
||||
def word_flag(sent: str) -> bool:
|
||||
"""Детектор Ф0.4 №1 «выдуманное слово» на предложении. ⚠ Без него пол арма C был бы занижен
|
||||
нечестно: механика класс k1 не видит ПО ПОСТРОЕНИЮ (она смотрит письменность и разметку),
|
||||
и «реальные детекторы» без словарного оказались бы не тем набором, что заказан промтом."""
|
||||
global _BANK # noqa: PLW0603
|
||||
if _BANK is None:
|
||||
_BANK = DW.load_bank()
|
||||
for w in re.findall(r"\b[а-яё]{5,}\b", sent.lower()):
|
||||
bad, _why = DW.verdict(w, _BANK)
|
||||
if bad:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def mech_flags(sent: str) -> list[str]:
|
||||
"""Механические детекторы на ОДНОМ предложении. Каждый — класс брака вывода."""
|
||||
out = []
|
||||
f = G.flags(sent)
|
||||
if f["ханьцзы"]:
|
||||
out.append("ханьцзы")
|
||||
if f["разметка"]:
|
||||
out.append("разметка")
|
||||
if f["латиница"]:
|
||||
out.append("латиница")
|
||||
if f["преамбула"]:
|
||||
out.append("преамбула")
|
||||
if word_flag(sent):
|
||||
out.append("слово")
|
||||
return out
|
||||
|
||||
|
||||
TYPE = {
|
||||
"ханьцзы": "непереведённый фрагмент исходной письменности остался в русском тексте",
|
||||
"разметка": "служебная markdown-разметка протекла в тело книги",
|
||||
"латиница": "непереведённое слово латиницей осталось в русском тексте",
|
||||
"преамбула": "служебная фраза модели вместо текста книги",
|
||||
"слово": "в предложении есть слово, которого нет в русском языке (модель его выдумала)",
|
||||
"qe": "предложение переведено неверно по смыслу (ранкер качества дал наихудший балл в окне)",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════ РЕЖИМ ORACLE — потолок схемы, буква промта стр.114 ═══════════════════
|
||||
# Промт требует ДВА режима, и разрыв между ними пре-рег называл главным результатом арма C:
|
||||
# сколько схема теряет НЕ из-за идеи точечного ремонта, а из-за несовершенства детекторов.
|
||||
# Земля известна по построению: в черновик сажаются дефекты двух классов на известные позиции.
|
||||
# k1 ВЫДУМАННОЕ СЛОВО — известное слово портится вставкой буквосочетания; форма отсутствует
|
||||
# в словаре, поэтому словарный детектор обязан её видеть.
|
||||
# k2 ИНВЕРЗИЯ ПОЛЯРНОСТИ — снятие или вставка «не» при личном глаголе (`align.flip_polarity`,
|
||||
# генератор проверен в `qe_power.py`). Смысл меняется на обратный, грамматичность цела,
|
||||
# механические детекторы слепы по построению — её обязан ловить QE.
|
||||
# ORACLE чинит РОВНО посаженные предложения. РЕАЛЬНЫЙ ищет их детекторами и чинит найденное.
|
||||
|
||||
def corrupt(word: str) -> str:
|
||||
"""Порча слова до несуществующей формы: удвоение внутреннего слога."""
|
||||
return word[:2] + word[1:3] + word[2:] if len(word) >= 5 else word + "ль"
|
||||
|
||||
|
||||
def plant(text: str, seed: int) -> tuple[str, list[dict]]:
|
||||
"""Посадить по одному дефекту каждого класса. Возвращает текст и ЗЕМЛЮ (известный ответ)."""
|
||||
import random as _r # noqa: PLC0415
|
||||
rnd = _r.Random(seed)
|
||||
sents = sentences(text)
|
||||
marks: list[dict] = []
|
||||
idx = [i for i, s in enumerate(sents) if len(s.strip()) >= 60 and not mech_flags(s)]
|
||||
rnd.shuffle(idx)
|
||||
for i in idx:
|
||||
if any(m["cls"] == "k1" for m in marks):
|
||||
break
|
||||
ws = [w for w in re.findall(r"\b[а-яё]{6,}\b", sents[i]) if w.isalpha()]
|
||||
if not ws:
|
||||
continue
|
||||
w = ws[len(ws) // 2]
|
||||
sents[i] = sents[i].replace(w, corrupt(w), 1)
|
||||
marks.append(dict(cls="k1", i=i, orig=w, bad=corrupt(w)))
|
||||
for i in idx:
|
||||
if any(m["cls"] == "k2" for m in marks) or any(m["i"] == i for m in marks):
|
||||
continue
|
||||
flipped = AL.flip_polarity(sents[i])
|
||||
if flipped and flipped != sents[i]:
|
||||
sents[i] = flipped
|
||||
marks.append(dict(cls="k2", i=i))
|
||||
break
|
||||
return "".join(sents), marks
|
||||
|
||||
|
||||
def cmd_oracle() -> None:
|
||||
"""Посадить дефекты, померить RECALL детекторов и купить починку в ОБОИХ режимах."""
|
||||
from qe_bench import QE # noqa: PLC0415
|
||||
qe = QE()
|
||||
rows = []
|
||||
for k, u in enumerate(BO.units()):
|
||||
uid = u["uid"]
|
||||
bad, marks = plant(BO.text3_of("F3", u), seed=1000 + k)
|
||||
if len(marks) < 2:
|
||||
print(f" {uid[:8]}: посадить оба класса не удалось ({len(marks)}), единица не берётся")
|
||||
continue
|
||||
sents = sentences(bad)
|
||||
# РЕАЛЬНЫЕ детекторы на ПОСАЖЕННОМ тексте
|
||||
found_mech = {i for i, s in enumerate(sents) if mech_flags(s)}
|
||||
zh, ru = AL.split_zh(u["source"]), AL.split_ru(bad)
|
||||
pairs = AL.align(zh, ru)
|
||||
scored = []
|
||||
for si, di in pairs:
|
||||
s = " ".join(zh[i] for i in si).strip()
|
||||
d = " ".join(ru[i] for i in di).strip()
|
||||
if not s or not d or len(d) < 40:
|
||||
continue
|
||||
sc = qe.score(s, d)
|
||||
for i, x in enumerate(sents):
|
||||
if x.strip() and x.strip()[:24] in d:
|
||||
scored.append((sc, i))
|
||||
worst = {i for _sc, i in sorted(scored, reverse=True)[:3]}
|
||||
found = found_mech | worst
|
||||
hit = {m["cls"]: (m["i"] in found) for m in marks}
|
||||
rows.append(dict(uid=uid, marks=marks, found=sorted(found), hit=hit))
|
||||
(OUT / f"p2b-oracle-{uid}.json").write_text(json.dumps(
|
||||
dict(uid=uid, planted=bad, marks=marks, found=sorted(found), hit=hit),
|
||||
ensure_ascii=False), encoding="utf-8")
|
||||
print(f" {uid[:8]}: посажено {len(marks)} · детекторы нашли "
|
||||
f"k1={hit.get('k1')} k2={hit.get('k2')}", flush=True)
|
||||
if rows:
|
||||
for cls in ("k1", "k2"):
|
||||
n = sum(1 for r in rows if cls in r["hit"])
|
||||
k = sum(1 for r in rows if r["hit"].get(cls))
|
||||
print(f"\nRECALL детекторов, класс {cls}: {k}/{n}")
|
||||
|
||||
|
||||
def flags_path(arm: str) -> Path:
|
||||
return OUT / f"p2b-flags-{arm}.json"
|
||||
|
||||
|
||||
def cmd_flags() -> None:
|
||||
"""Детекторы, $0. Механика мгновенна; QE — локальная модель на CPU, поэтому кэшируется.
|
||||
|
||||
⚠ Порядок важен: сначала дешёвая механика по ВСЕМ предложениям, затем QE только по тем, что
|
||||
механика не флагнула. Так QE-вызовов меньше, а покрытие то же.
|
||||
"""
|
||||
from qe_bench import QE # noqa: PLC0415
|
||||
qe = None
|
||||
for arm, base in (("C", "F3"), ("E", "D_")):
|
||||
p = flags_path(arm)
|
||||
got = json.loads(p.read_text(encoding="utf-8")) if p.exists() else {}
|
||||
for u in BO.units():
|
||||
uid = u["uid"]
|
||||
if uid in got:
|
||||
continue
|
||||
txt = BO.text3_of(base, u)
|
||||
sents = sentences(txt)
|
||||
rows = [dict(i=i, sent=s, mech=mech_flags(s), qe=None) for i, s in enumerate(sents)]
|
||||
# ⚠ QE СЧИТАЕТСЯ НА ВЫРОВНЕННОЙ ПАРЕ, а не «предложение против всего окна». Поймано
|
||||
# верификацией ДО траты: подача целого окна как исходника стоила 6.8 с на вызов
|
||||
# (128 минут на арм) и мерила не то — предложение сравнивалось с текстом, которому
|
||||
# оно не соответствует. Выравнивание по Гейлу–Чёрчу даёт 0.65 с и правильную пару.
|
||||
zh, ru = AL.split_zh(u["source"]), AL.split_ru(txt)
|
||||
pairs = AL.align(zh, ru)
|
||||
if qe is None:
|
||||
qe = QE()
|
||||
n_qe = 0
|
||||
for si, di in pairs:
|
||||
s = " ".join(zh[i] for i in si).strip()
|
||||
d = " ".join(ru[i] for i in di).strip()
|
||||
if not s or not d or len(d) < 40:
|
||||
continue
|
||||
sc = qe.score(s, d)
|
||||
n_qe += 1
|
||||
# балл пары кладётся на ВСЕ предложения `sentences`, которые она покрывает
|
||||
for r in rows:
|
||||
if r["sent"].strip() and r["sent"].strip()[:24] in d:
|
||||
r["qe"] = sc if r["qe"] is None else max(r["qe"], sc)
|
||||
got[uid] = rows
|
||||
p.write_text(json.dumps(got, ensure_ascii=False), encoding="utf-8")
|
||||
print(f" {arm} {uid[:8]}: предложений {len(rows)} · механикой флагнуто "
|
||||
f"{sum(1 for r in rows if r['mech'])} · QE-пар {n_qe} · "
|
||||
f"с баллом {sum(1 for r in rows if r['qe'] is not None)}", flush=True)
|
||||
print("флаги готовы")
|
||||
|
||||
|
||||
def picked(arm: str, uid: str, k_qe: int = 3) -> list[dict]:
|
||||
"""Что чинить: ВСЁ, что флагнула механика, плюс k худших по QE (реальный режим).
|
||||
|
||||
⚠ Кап на QE объявлен ЗАРАНЕЕ и равен 3 предложениям на единицу. Без капа фиксер правил бы
|
||||
половину текста, и «точечный ремонт» превратился бы в переписывание — то есть в другой арм.
|
||||
"""
|
||||
rows = json.loads(flags_path(arm).read_text(encoding="utf-8"))[uid]
|
||||
out = [dict(r, why=r["mech"][0]) for r in rows if r["mech"]]
|
||||
scored = sorted((r for r in rows if r["qe"] is not None), key=lambda r: -r["qe"])[:k_qe]
|
||||
out += [dict(r, why="qe") for r in scored]
|
||||
return sorted(out, key=lambda r: r["i"])
|
||||
|
||||
|
||||
def fix_one(source: str, sent: str, why: str, tag: str) -> dict:
|
||||
"""Один вызов фиксера. Правит ОДНО предложение, видя исходник окна."""
|
||||
sys_msg = ("Ты — корректор художественного перевода. Тебе дан ИСХОДНИК фрагмента на китайском "
|
||||
"и ОДНО предложение русского перевода, в котором найден дефект. Верни ТОЛЬКО "
|
||||
"исправленное предложение, без пояснений, без кавычек-обёрток. Если дефекта нет — "
|
||||
"верни предложение без изменений. Ничего, кроме этого предложения, не выводи.")
|
||||
usr = f"ДЕФЕКТ: {TYPE[why]}\n\nИСХОДНИК:\n{source}\n\nПРЕДЛОЖЕНИЕ:\n{sent}"
|
||||
return BUY.purchase(LED_P2B, tag, FIXER, BO.client(FIXER),
|
||||
dict(model=FIXER, messages=[{"role": "system", "content": sys_msg},
|
||||
{"role": "user", "content": usr}],
|
||||
max_completion_tokens=2000, reasoning_effort="none"),
|
||||
why=why, tag_kind="fix")
|
||||
|
||||
|
||||
def cmd_arms() -> None:
|
||||
"""Купить починки. Кап 2 итерации; ре-гейт после каждой — МЕХАНИЧЕСКИЙ (см. оговорку ниже)."""
|
||||
for arm, base in (("C", "F3"), ("E", "D_")):
|
||||
for u in BO.units():
|
||||
uid = u["uid"]
|
||||
text = BO.text3_of(base, u)
|
||||
sents = sentences(text)
|
||||
todo = picked(arm, uid)
|
||||
for it in (1, 2):
|
||||
if not todo:
|
||||
break
|
||||
for r in todo:
|
||||
tag = f"p2b-{arm}-{uid}-s{r['i']}-i{it}"
|
||||
rec = fix_one(u["source"], sents[r["i"]], r["why"], tag)
|
||||
if rec.get("skipped"):
|
||||
print("⛔ потолок Ф2б")
|
||||
return
|
||||
new = (rec.get("content") or "").strip()
|
||||
if new:
|
||||
# ⚠ ХВОСТОВОЙ ПРОБЕЛЬНЫЙ СУФФИКС СОХРАНЯЕТСЯ. Первая версия ставила
|
||||
# `new + " "` и затирала перевод строки: 28 из 65 отобранных предложений
|
||||
# несут `\n`/`\n\n` в хвосте, то есть харнесс СВОИМИ РУКАМИ схлопнул бы
|
||||
# абзацы и впрыснул дефект оси ФОРМА ровно в контрасты C↔A_law и C↔F3.
|
||||
# Поймано верификацией ДО покупки.
|
||||
old = sents[r["i"]]
|
||||
suf = old[len(old.rstrip()):] or " "
|
||||
sents[r["i"]] = new + suf
|
||||
time.sleep(0.05)
|
||||
# РЕ-ГЕЙТ после итерации. ⚠ Он МЕХАНИЧЕСКИЙ: QE локален и бесплатен, но его
|
||||
# пере-прогон требует пере-выравнивания всего окна, а фиксы локальны. Ограничение
|
||||
# объявлено: смысловую ошибку, ВНЕСЁННУЮ самим фиксером, этот гейт не увидит.
|
||||
# Клейм «полный ре-гейт» из первой версии докстринга снят как завышенный.
|
||||
todo = [dict(i=i, sent=s, why=mech_flags(s)[0])
|
||||
for i, s in enumerate(sents) if mech_flags(s)]
|
||||
(OUT / f"p2b-{arm}-{uid}.json").write_text(json.dumps(
|
||||
dict(arm=arm, uid=uid, base=base, content="".join(sents)),
|
||||
ensure_ascii=False), encoding="utf-8")
|
||||
print(f" {arm} {uid[:8]} готов", flush=True)
|
||||
print(f"куплено · касса ${LED_P2B.spent():.5f} из ${CEIL_P2B}")
|
||||
|
||||
|
||||
def text_of(arm: str, uid: str) -> str:
|
||||
f = OUT / f"p2b-{arm}-{uid}.json"
|
||||
return json.loads(f.read_text(encoding="utf-8"))["content"] if f.exists() else ""
|
||||
|
||||
|
||||
def cmd_score() -> None:
|
||||
"""Свод детерминированной части, $0. Судейство — отдельным шагом через `absjudge`.
|
||||
|
||||
⚠ Здесь НЕТ судейских чисел намеренно: тексты C и E новые, их никто не судил. Чтобы они
|
||||
были сравнимы с уже отснятыми армами, их надо пропустить через ТОТ ЖЕ риг (`absjudge`),
|
||||
а не мерить чем-то своим — иначе контрасты окажутся несравнимы, и покупка обесценится.
|
||||
Это и есть шаг после `--arms`.
|
||||
"""
|
||||
us = BO.units()
|
||||
have = {a: [u for u in us if text_of(a, u["uid"]).strip()] for a in ("C", "E")}
|
||||
print(f"{'арм':6s}{'единиц':>8s}{'знаков':>9s}{'абзацев':>9s}{'канон':>8s}{'механика':>10s}")
|
||||
for a, base in (("F3", None), ("A_law", None), ("D_", None), ("C", "F3"), ("E", "D_")):
|
||||
txts = ([text_of(a, u["uid"]) for u in have[a]] if a in have
|
||||
else [BO.text3_of(a, u) for u in us])
|
||||
txts = [x for x in txts if x.strip()]
|
||||
if not txts:
|
||||
print(f"{a:6s}{'нет':>8s}")
|
||||
continue
|
||||
s = k = 0
|
||||
for u, x in zip(us, txts):
|
||||
for zh, (_1, _2, r1, _3) in BO.IP.TERMS.items():
|
||||
n = u["source"].count(zh)
|
||||
if n:
|
||||
s += n
|
||||
k += min(len(re.findall(r1, x, re.I)), n)
|
||||
mech = sum(1 for x in txts for sn in sentences(x) if mech_flags(sn))
|
||||
print(f"{a:6s}{len(txts):>8d}{st.median([len(x) for x in txts]):>9.0f}"
|
||||
f"{st.median([len([q for q in x.split(chr(10)) if q.strip()]) for x in txts]):>9.0f}"
|
||||
f"{(k / s if s else 0):>8.3f}{mech:>10d}")
|
||||
print("\nЦЕНА (черновик/база + починка):")
|
||||
for a, base in (("C", "F3"), ("E", "D_")):
|
||||
fix = sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd") or 0.0
|
||||
for f in OUT.glob(f"p2b-{a}-*-s*.json"))
|
||||
n = len(have[a]) or 1
|
||||
if base == "F3":
|
||||
b = st.mean([sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd") or 0.0
|
||||
for f in OUT.glob(f"bo2-DRAFT-{u['uid']}*.json")) for u in us])
|
||||
else:
|
||||
b = st.mean([json.loads((OUT / f"bo2-{base}-{u['uid']}.json").read_text(
|
||||
encoding="utf-8"))["cost_usd"] for u in us])
|
||||
print(f" {a:3s} база ${b:.5f} + починка ${fix / n:.5f} = ${b + fix / n:.5f}/единицу")
|
||||
al = st.mean([sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd") or 0.0
|
||||
for f in OUT.glob(f"bo2-DRAFT-{u['uid']}*.json"))
|
||||
+ json.loads((OUT / f"bo4-A_law-{u['uid']}.json").read_text(
|
||||
encoding="utf-8"))["cost_usd"] for u in us])
|
||||
print(f" A_law полная связка ${al:.5f}/единицу — база сравнения")
|
||||
|
||||
|
||||
def cmd_plan() -> None:
|
||||
us = BO.units()
|
||||
tot_s = tot_f = 0
|
||||
for arm, base in (("C", "F3"), ("E", "D_")):
|
||||
if not flags_path(arm).exists():
|
||||
print(f" {arm}: флаги не посчитаны — сначала --flags")
|
||||
continue
|
||||
n = sum(len(picked(arm, u["uid"])) for u in us)
|
||||
tot_f += n
|
||||
tot_s += sum(len(sentences(BO.text3_of(base, u))) for u in us)
|
||||
print(f" арм {arm} (база {base}): предложений {tot_s} · к починке {n}")
|
||||
print(f"\nвызовов фиксера ≈ {tot_f} (плюс до {tot_f} на второй итерации)")
|
||||
print(f"ожидаемая цена ≈ ${tot_f * 2 * 0.0004:.4f} из потолка ${CEIL_P2B}")
|
||||
print(f"касса `p2b-*` · потрачено ${LED_P2B.spent():.5f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:] or ["--plan"]
|
||||
{"--plan": cmd_plan, "--flags": cmd_flags, "--arms": cmd_arms,
|
||||
"--oracle": cmd_oracle, "--score": cmd_score}.get(
|
||||
a[0], lambda: print(__doc__))()
|
||||
|
|
@ -30,6 +30,31 @@ DOC = REPO / "docs" / "experiments" / "21-role-topology.md"
|
|||
FAILS = 0
|
||||
|
||||
|
||||
def num_in(text: str, s: str) -> bool:
|
||||
"""Точное вхождение ЧИСЛА: не префикс более длинного и не хвост другого.
|
||||
|
||||
⚠ Свип приёмки 07.08 прогнал 18 мутантов отчёта, подменяя число на более длинное с тем же
|
||||
префиксом (`0.469816`→`0.4698169`), и ВСЕ 18 прошли зелёными: предикат `num_in(text, f"{x:.5f}")`
|
||||
слеп к префиксу. Плюс две проверки не могли покраснеть вовсе — иглой было голое короткое
|
||||
число. Точная форма чинит весь класс разом. (Восстановлено после инцидента 08.08, D39.113.)
|
||||
"""
|
||||
return re.search(rf"(?<![\d.]){re.escape(s)}(?![\d.])", text) is not None
|
||||
|
||||
|
||||
def num_near(text: str, label: str, s: str) -> bool:
|
||||
"""Число обязано стоять В СТРОКЕ С МЕТКОЙ, а не где угодно в файле: иначе дубль в прозе
|
||||
даёт клетке таблицы алиби, и порча одной клетки проходит зелёной."""
|
||||
for line in text.splitlines():
|
||||
if label in line and num_in(line, s):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def med(xs: list, default: float = 0.0) -> float:
|
||||
"""Медиана, не роняющая гейт на пустом сырье."""
|
||||
return statistics.median(xs) if xs else default
|
||||
|
||||
|
||||
def ck(claim: str, ok: bool, got: str = "") -> None:
|
||||
global FAILS # noqa: PLW0603
|
||||
if not ok:
|
||||
|
|
@ -122,7 +147,7 @@ def main() -> None: # noqa: PLR0915
|
|||
not any(j.startswith(("deepseek", "glm")) for j in J.JUDGES), str(J.JUDGES))
|
||||
bias_zh = BO.position_bias()
|
||||
ck("позиционная фора zh напечатана в отчёте", f"{bias_zh:+.2f}".replace("+", "+") in text
|
||||
or f"{abs(bias_zh):.2f}" in text, f"{bias_zh:+.2f}")
|
||||
or num_in(text, f"{abs(bias_zh):.2f}"), f"{bias_zh:+.2f}")
|
||||
|
||||
print("\n=== ТАБЛИЦЫ ПЕРЕВЕСОВ (пере-вывод из голосов) ===")
|
||||
for a, b in BO.CONTRASTS:
|
||||
|
|
@ -199,8 +224,8 @@ def main() -> None: # noqa: PLR0915
|
|||
v = money(pat)
|
||||
total += v
|
||||
ck(f"{name}: ${v:.6f} ≤ потолок ${ceil:.2f} и напечатано в отчёте",
|
||||
v <= ceil and f"{v:.6f}" in text, f"${v:.6f}")
|
||||
ck(f"итог ${total:.6f} напечатан в отчёте", f"{total:.6f}" in text, f"${total:.6f}")
|
||||
v <= ceil and num_in(text, f"{v:.6f}"), f"${v:.6f}")
|
||||
ck(f"итог ${total:.6f} напечатан в отчёте", num_in(text, f"{total:.6f}"), f"${total:.6f}")
|
||||
|
||||
print("\n=== ЭКОНОМИКА ===")
|
||||
# ⚠ Цена печатается ПО СОБСТВЕННОМУ ВЫЗОВУ арма, без ноги черновика. Основание: армы правят
|
||||
|
|
@ -209,9 +234,9 @@ def main() -> None: # noqa: PLR0915
|
|||
for arm in BO.ARMS:
|
||||
costs = [json.loads((RAW / f"bo2-{arm}-{u['uid']}.json").read_text(encoding="utf-8"))["cost_usd"]
|
||||
for u in us_zh if (RAW / f"bo2-{arm}-{u['uid']}.json").exists()]
|
||||
med = statistics.median(costs)
|
||||
med = med(costs)
|
||||
ck(f"{arm}: ${med:.5f}/единицу (собственный вызов) напечатано в отчёте",
|
||||
f"{med:.5f}" in text, f"{med:.5f}")
|
||||
num_in(text, f"{med:.5f}"), f"{med:.5f}")
|
||||
same = sum(1 for u in us_zh
|
||||
if BO.text_of("F", u).strip()
|
||||
== json.loads((RAW / f"bo2-DRAFT-{u['uid']}.json").read_text(
|
||||
|
|
@ -222,7 +247,7 @@ def main() -> None: # noqa: PLR0915
|
|||
f"совпало {same}/{len(us_zh)}")
|
||||
med_draft = statistics.median([BO.draft_cost(u["uid"]) for u in us_zh])
|
||||
ck(f"пере-замер цены черновой роли ${med_draft:.5f} напечатан отдельным числом",
|
||||
f"{med_draft:.5f}" in text, f"{med_draft:.5f}")
|
||||
num_in(text, f"{med_draft:.5f}"), f"{med_draft:.5f}")
|
||||
|
||||
print("\n=== ПОПРАВКА НА МНОЖЕСТВЕННОСТЬ И РАЗБОР ПО СУДЬЯМ ===")
|
||||
ps = []
|
||||
|
|
@ -325,7 +350,7 @@ def main() -> None: # noqa: PLR0915
|
|||
# контексте, и число рядом со словами «фаза уложилась».
|
||||
(not over) or re.search(rf"\${spent:.5f}(?!\d)", text) is not None, f"{spent:.5f}")
|
||||
total = money("*.json")
|
||||
ck(f"полная сумма ${total:.5f} напечатана в отчёте", f"{total:.5f}" in text, f"{total:.5f}")
|
||||
ck(f"полная сумма ${total:.5f} напечатана в отчёте", num_in(text, f"{total:.5f}"), f"{total:.5f}")
|
||||
|
||||
print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not FAILS else f'РАСХОЖДЕНИЙ: {FAILS}'}")
|
||||
sys.exit(1 if FAILS else 0)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue