406 lines
26 KiB
Python
406 lines
26 KiB
Python
#!/usr/bin/env python3
|
||
"""Ф0.4 — ПАКЕТЫ ДЛЯ SOL: внешний судейский контур руками владельца. $0.
|
||
|
||
Что это. Sol — флагманская reasoning-модель OpenAI в ЧАТ-подписке владельца. Она вне обеих
|
||
популяций судей пака (платные ключи + агенты сессии), поэтому её голос — третий независимый
|
||
инструмент. Сессия готовит paste-ready пакеты, владелец вставляет их руками и возвращает ответы;
|
||
транскрипты персистятся дословно.
|
||
|
||
**Пакет (а) — АРБИТРАЖ ЭКСП-21.** Там два платных судьи разошлись на величину, которая
|
||
собственным шумом не объясняется: разброс судьи-замены на повторе даёт sd перевеса 0.81 на голос,
|
||
то есть ±0.15 на контраст из 15 единиц. Отобраны ТРИ контраста по ФАКТИЧЕСКОМУ расхождению:
|
||
|
||
контраст luna gemini разрыв в sd собственного шума
|
||
A vs F +0.84 +3.03 2.19 ~14
|
||
B vs F +1.44 +3.33 1.90 ~12
|
||
A_law vs A +0.91 −0.20 1.11 ~7 ← ЗНАК РАСХОДИТСЯ
|
||
|
||
Почему именно эти. `A/F` заказан промтом. `B/F` — тот же заголовочный вопрос («покупает ли второй
|
||
проход качество») и второй по величине разрыв. `A_law/A` взят НЕ по величине: это единственный
|
||
контраст, где судьи расходятся ЗНАКОМ, а расхождение знака хуже расхождения величины — оно меняет
|
||
вердикт, а не точность. `D/A` (разрыв 0.74) и `D/D_` (1.23) не берутся: там знак у обоих судей
|
||
один, и арбитраж уточнил бы величину, которая решения не несёт.
|
||
|
||
Протокол — ПАРНЫЙ, потому что арбитражу подлежит парный замер: слепые метки, ОБА порядка, декой.
|
||
Правило зеркальности для пар-протокола живо (для абсолютного снято, D39.117 п.2в).
|
||
|
||
**Семейный запрет.** Sol не судит клетки OpenAI-моделей. В армах эксп-21 их нет (deepseek, glm),
|
||
поэтому запрет здесь не связывает — но он объявлен, и в пакете (б) по финалистам эксп-22 будет
|
||
применён механически.
|
||
|
||
Запуск: --emit выпустить пакеты и инструкцию, $0
|
||
--ingest принять ответы владельца из sol/answers/, $0
|
||
--score что сказал Sol против платных судей, $0
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import json
|
||
import statistics as st
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||
ZONE = Path(__file__).resolve().parent
|
||
sys.path.insert(0, str(ZONE))
|
||
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
|
||
|
||
import absjudge as AJ # noqa: E402
|
||
import bakeoff as BO # noqa: E402
|
||
import judges as J # noqa: E402
|
||
import money as MONEY # noqa: E402
|
||
|
||
SOL = MONEY.OUT / "sol"
|
||
ANSW = SOL / "answers"
|
||
KEYS = MONEY.OUT / "blind-keys"
|
||
CONTRASTS = [("A", "F"), ("B", "F"), ("A_law", "A")]
|
||
N_UNITS = 3
|
||
SALT_FILE = KEYS / "SALT-sol.txt"
|
||
|
||
|
||
def _salt() -> str:
|
||
if not SALT_FILE.exists():
|
||
import secrets # noqa: PLC0415
|
||
SALT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||
SALT_FILE.write_text(secrets.token_hex(16), encoding="utf-8")
|
||
return SALT_FILE.read_text(encoding="utf-8").strip()
|
||
|
||
|
||
def pick_units(a: str, b: str) -> list[dict]:
|
||
"""Единицы с НАИБОЛЬШИМ расхождением судей на этом контрасте — там арбитраж и нужен."""
|
||
us = BO.units()
|
||
scored = []
|
||
for u in us:
|
||
per = []
|
||
for judge in J.JUDGES:
|
||
bo = BO.margins_by_order(a, b, u["uid"], judge)
|
||
if len(bo) == 2:
|
||
per.append(st.mean([st.mean(bo[0]), st.mean(bo[1])]))
|
||
if len(per) == len(J.JUDGES):
|
||
scored.append((abs(per[0] - per[1]), u))
|
||
scored.sort(key=lambda x: (-x[0], x[1]["uid"]))
|
||
return [u for _g, u in scored[:N_UNITS]]
|
||
|
||
|
||
# ⚠ РАМКА ВНЕШНЕГО СУДЬИ. Sol работает в UI-подписке владельца, а не по API: у него нет ни ключа,
|
||
# ни жёсткого формата ответа, ни запрета на веб. Владелец 09.08: «он пипец как любит посмотреть
|
||
# туда, куда его не просят». Три риска, каждый обесценивает пакет: узнавание книги вместо сверки с
|
||
# исходником (контаминация — тот самый класс, ради которого искали невиданный срез); собственный
|
||
# вариант перевода вместо счёта ошибок; разметка и преамбулы, ломающие построчный разбор.
|
||
FRAME = """⛔ РАМКА ЗАДАНИЯ — прочитай целиком до того, как начнёшь.
|
||
|
||
Это слепой замер, а не консультация. От тебя нужен РОВНО ОДИН артефакт: блок строк с числами.
|
||
Материал оплачен и одноразовый; всё, что сверх формата, ломает машинный разбор и обесценивает его.
|
||
|
||
ЧЕГО ДЕЛАТЬ НЕЛЬЗЯ:
|
||
* НЕ искать ничего вовне этого сообщения — ни в сети, ни в памяти: ни книгу, ни автора, ни главу,
|
||
ни чужие переводы. Если ты узнал произведение — не опирайся на это ни одним словом. Единственный
|
||
эталон — ИСХОДНИК ниже. Узнавание вместо сверки с исходником делает замер недействительным.
|
||
* НЕ предлагать свой перевод, не переписывать, не редактировать и не «улучшать» варианты.
|
||
* НЕ гадать, кто автор вариантов — человек, модель, какая именно, какой вариант «основной».
|
||
Порядок вариантов перемешан жребием и не значит ничего.
|
||
* НЕ оценивать варианты друг через друга. Каждый считается ОТДЕЛЬНО против исходника: вопрос не
|
||
«какой лучше», а «сколько ошибок в этом».
|
||
* НЕ добавлять заголовки, таблицы, markdown-разметку, жирный шрифт, преамбулы и рекомендации.
|
||
* НЕ задавать уточняющих вопросов и не просить контекст: его не будет, отвечай по тому, что дано.
|
||
* НЕ менять имена строк ответа и не переставлять их — разбор ищет их буквально, посимвольно."""
|
||
|
||
FORMAT = """ФОРМАТ ОТВЕТА — РОВНО ЭТИ ДЕВЯТЬ СТРОК, КАЖДАЯ С НОВОЙ, БЕЗ РАЗМЕТКИ И БЕЗ ТЕКСТА МЕЖДУ НИМИ:
|
||
В1-ВЕРНОСТЬ: <число>
|
||
В1-ТЕРМИН: <число>
|
||
В1-ЯЗЫК: <число>
|
||
В1-ФОРМА: <число>
|
||
В2-ВЕРНОСТЬ: <число>
|
||
В2-ТЕРМИН: <число>
|
||
В2-ЯЗЫК: <число>
|
||
В2-ФОРМА: <число>
|
||
СТИЛЬ: <В1 или В2 или НИЧЬЯ — где живее и естественнее русская проза>
|
||
|
||
Пояснения и разбор можно дать, но ТОЛЬКО ПОСЛЕ этих девяти строк, отдельным абзацем. Ничего перед
|
||
ними и ничего между ними. Если по какой-то оси ошибок нет — ставь 0, а не пропуск и не прочерк."""
|
||
|
||
|
||
def _packet(src: str, v1: str, v2: str, tok: str) -> str:
|
||
axes = J.RUBRIC.split("---", 1)[0].strip() if "---" in J.RUBRIC else J.RUBRIC.strip()
|
||
return "\n".join([
|
||
f"ПАКЕТ {tok}", "",
|
||
FRAME, "", "ЧТО СЧИТАТЬ ОШИБКОЙ — по типам:", axes, "", FORMAT, "",
|
||
"=" * 60, "ИСХОДНИК:", src, "",
|
||
"=" * 60, "ВАРИАНТ 1:", v1, "",
|
||
"=" * 60, "ВАРИАНТ 2:", v2, ""])
|
||
|
||
|
||
def emit() -> None:
|
||
SOL.mkdir(parents=True, exist_ok=True)
|
||
ANSW.mkdir(parents=True, exist_ok=True)
|
||
key: dict[str, dict] = {}
|
||
n = 0
|
||
for a, b in CONTRASTS:
|
||
for u in pick_units(a, b):
|
||
ta, tb = BO.text_of(a, u), BO.text_of(b, u)
|
||
if not ta.strip() or not tb.strip():
|
||
continue
|
||
for order in (0, 1):
|
||
tok = hashlib.sha1(f"{_salt()}|{a}|{b}|{u['uid']}|{order}".encode()).hexdigest()[:10] # noqa: S324
|
||
v1, v2 = (ta, tb) if order == 0 else (tb, ta)
|
||
(SOL / f"{tok}.txt").write_text(_packet(u["source"], v1, v2, tok), encoding="utf-8")
|
||
key[tok] = dict(contrast=f"{a}v{b}", first=a if order == 0 else b,
|
||
second=b if order == 0 else a, uid=u["uid"], order=order,
|
||
kind="боевой")
|
||
n += 1
|
||
# ДЕКОЙ: посаженная деградация против чистого текста. Сессия, не поймавшая декой, аннулируется.
|
||
u0 = BO.units()[0]
|
||
clean = BO.text3_of("A", u0)
|
||
planted, k = AJ._plant(clean) # noqa: SLF001
|
||
if k >= 2:
|
||
for order in (0, 1):
|
||
tok = hashlib.sha1(f"{_salt()}|decoy|{u0['uid']}|{order}".encode()).hexdigest()[:10] # noqa: S324
|
||
v1, v2 = (clean, planted) if order == 0 else (planted, clean)
|
||
(SOL / f"{tok}.txt").write_text(_packet(u0["source"], v1, v2, tok), encoding="utf-8")
|
||
key[tok] = dict(contrast="ДЕКОЙ", first="чистый" if order == 0 else "порченый",
|
||
second="порченый" if order == 0 else "чистый", uid=u0["uid"],
|
||
order=order, kind="контроль-декой", plants=k)
|
||
n += 1
|
||
(KEYS / "sol-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
|
||
encoding="utf-8")
|
||
(SOL / "ИНСТРУКЦИЯ.md").write_text(_instr(len(key)), encoding="utf-8")
|
||
print(f"пакетов {n} → {SOL} · ключ → {KEYS}/sol-KEY.json (владельцу и Sol НЕ давать)")
|
||
print(f"инструкция → {SOL}/ИНСТРУКЦИЯ.md")
|
||
|
||
|
||
# ── ПАКЕТ (б): слепые пары ФИНАЛИСТОВ Ф2/Ф3 ────────────────────────────────────────────────
|
||
# Заказан промтом «по готовности Ф2/Ф3». Смысл иной, чем у пакета (а): (а) арбитрирует спор
|
||
# ДВУХ платных судей эксп-21, а (б) выносит наружу решение, которое внутри пака вынесла ОДНА
|
||
# модельная семья. Все 40 судейских сессий — один и тот же судья в разных контекстах, и это
|
||
# объявлено непереносимым (§11 отчёта). Sol — единственный контур другой семьи.
|
||
PAIRS_B = [
|
||
# (проход, арм A, арм B, зачем)
|
||
("p3", "R0", "R2", "редактор: рекомендованный против ПОГРАНИЧНОГО (на нём висит Резерв D39.22)"),
|
||
("p2zh", "E0", "E2", "черновик: финал поверх якоря против финала поверх лучшего соперника"),
|
||
]
|
||
N_UNITS_B = 3
|
||
|
||
|
||
def _no_openai(models: list[str]) -> None:
|
||
"""Семейный запрет симметричен (ЗАКОН промта): Sol не судит клетки OpenAI-моделей."""
|
||
import roster as R # noqa: PLC0415
|
||
bad = [m for m in models if "openai.com" in R.CANDIDATES.get(m, ("",))[0]]
|
||
if bad:
|
||
raise SystemExit(f"семейный запрет: Sol не судит клетки OpenAI — {bad}")
|
||
|
||
|
||
def pick_units_b(pass_: str, a: str, b: str) -> list[dict]:
|
||
"""Единицы, где ВНУТРИСЕМЕЙНЫЙ судья был увереннее всего.
|
||
|
||
Для арбитража это правильнее, чем брать спорные: если внешний судья не воспроизводит перевес
|
||
там, где свой был максимально уверен, — под вопросом сам перевес, а не его величина.
|
||
"""
|
||
import judge as JU # noqa: PLC0415
|
||
JU.setup(pass_)
|
||
import absjudge as AJ2 # noqa: PLC0415
|
||
per = []
|
||
for uid, d in AJ2._by_unit().items(): # noqa: SLF001
|
||
if a in d and b in d:
|
||
per.append((abs(d[b]["errors"] - d[a]["errors"]), uid))
|
||
per.sort(key=lambda x: (-x[0], x[1]))
|
||
byid = {u["uid"]: u for u in _units_b()}
|
||
return [byid[uid] for _g, uid in per[:N_UNITS_B] if uid in byid]
|
||
|
||
|
||
def _units_b() -> list[dict]:
|
||
import material as M # noqa: PLC0415
|
||
return M.units_zh()
|
||
|
||
|
||
def _text_b(pass_: str, arm: str, uid: str) -> str:
|
||
import judge as JU # noqa: PLC0415
|
||
return JU.text_of(pass_)(arm, {"uid": uid})
|
||
|
||
|
||
def emit_b() -> None:
|
||
"""Выпуск пакета (б). $0: ничего не покупается, только сборка уже купленных клеток."""
|
||
import judge as JU # noqa: PLC0415
|
||
out = MONEY.OUT / "sol-b"
|
||
out.mkdir(parents=True, exist_ok=True)
|
||
(out / "answers").mkdir(parents=True, exist_ok=True)
|
||
key: dict[str, dict] = {}
|
||
n = 0
|
||
for pass_, a, b, why in PAIRS_B:
|
||
_no_openai([JU.arm_map(pass_).get(a, ""), JU.arm_map(pass_).get(b, "")])
|
||
for u in pick_units_b(pass_, a, b):
|
||
ta, tb = _text_b(pass_, a, u["uid"]), _text_b(pass_, b, u["uid"])
|
||
if not ta.strip() or not tb.strip():
|
||
continue
|
||
for order in (0, 1):
|
||
tok = hashlib.sha1( # noqa: S324
|
||
f"{_salt()}|b|{pass_}|{a}|{b}|{u['uid']}|{order}".encode()).hexdigest()[:10]
|
||
v1, v2 = (ta, tb) if order == 0 else (tb, ta)
|
||
(out / f"{tok}.txt").write_text(_packet(u["source"], v1, v2, tok), encoding="utf-8")
|
||
key[tok] = dict(contrast=f"{a}v{b}", pass_=pass_, why=why,
|
||
first=a if order == 0 else b, second=b if order == 0 else a,
|
||
uid=u["uid"], order=order, kind="боевой")
|
||
n += 1
|
||
# тот же обязательный декой: сессия, не поймавшая посаженную деградацию, аннулируется
|
||
u0 = _units_b()[0]
|
||
clean = _text_b("p3", "R0", u0["uid"])
|
||
planted, k = AJ._plant(clean) # noqa: SLF001
|
||
if k >= 2:
|
||
for order in (0, 1):
|
||
tok = hashlib.sha1(f"{_salt()}|b-decoy|{u0['uid']}|{order}".encode()).hexdigest()[:10] # noqa: S324
|
||
v1, v2 = (clean, planted) if order == 0 else (planted, clean)
|
||
(out / f"{tok}.txt").write_text(_packet(u0["source"], v1, v2, tok), encoding="utf-8")
|
||
key[tok] = dict(contrast="ДЕКОЙ", first="чистый" if order == 0 else "порченый",
|
||
second="порченый" if order == 0 else "чистый", uid=u0["uid"],
|
||
order=order, kind="контроль-декой", plants=k)
|
||
n += 1
|
||
(KEYS / "sol-b-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
|
||
encoding="utf-8")
|
||
(out / "ИНСТРУКЦИЯ.md").write_text(_instr_b(len(key)), encoding="utf-8")
|
||
print(f"пакет (б): {n} файлов → {out} · ключ → {KEYS}/sol-b-KEY.json (владельцу НЕ давать)")
|
||
|
||
|
||
def _instr_b(n: int) -> str:
|
||
return f"""# Пакеты для Sol — ПАКЕТ (б), финалисты эксп-22
|
||
|
||
Всего пакетов: **{n}**. Правила те же, что в пакете (а): один пакет — один НОВЫЙ чат, ответ
|
||
копируется дословно в `answers/<тот же токен>.txt`, порядок вариантов внутри пакета не менять,
|
||
какая модель какой вариант писала — не сообщать.
|
||
|
||
## Чем этот пакет отличается от (а)
|
||
|
||
Пакет (а) разрешал спор двух платных судей эксп-21. Здесь спора нет — здесь есть **решение,
|
||
которое целиком вынесла ОДНА модельная семья**: все 40 судейских сессий эксп-22 — один и тот же
|
||
судья в разных контекстах. Он мог систематически предпочитать один стиль русской прозы другому,
|
||
и изнутри пака это неотличимо от истины. Sol — единственный судейский контур другой семьи.
|
||
|
||
Поэтому важны именно эти пары:
|
||
|
||
* **редактор рекомендованный против пограничного** — на этом контрасте держится вопрос о
|
||
Резерве D39.22, и внутри пака он прошёл на грани (перевес 2.12 при пороге прохода 2.47);
|
||
* **финал поверх якорного черновика против финала поверх лучшего соперника** — крупнейший
|
||
перевес фазы 2.
|
||
|
||
Единицы отобраны те, где внутренний судья был увереннее всего: если внешний судья не
|
||
воспроизводит перевес там, где свой был максимально уверен, под вопросом сам перевес.
|
||
|
||
## Что будет с ответами
|
||
|
||
Возврат не блокер: не вернётся — так и будет записано. Вернётся — числа лягут рядом с
|
||
внутренними, и расхождение станет находкой, а не поводом переписать вывод.
|
||
"""
|
||
|
||
|
||
def _instr(n: int) -> str:
|
||
return f"""# Пакеты для Sol — инструкция владельцу
|
||
|
||
Всего пакетов: **{n}**. Каждый — отдельный файл `<токен>.txt` в этом каталоге.
|
||
Порядок вставки НЕ важен; частичный возврат тоже годится — что вернётся, то и посчитается.
|
||
|
||
## Что делать
|
||
|
||
1. Открыть новый чат с Sol (high reasoning).
|
||
2. Вставить содержимое ОДНОГО файла целиком, без правок и без пояснений от себя.
|
||
3. Скопировать ответ Sol **дословно** в файл `answers/<тот же токен>.txt`.
|
||
4. Следующий пакет — **новым чатом**: иначе Sol узнаёт зеркальные пары и позиционного контроля
|
||
не остаётся (эта беда измерена на агенте-судье в эксп-21 §10).
|
||
|
||
## Формат ответа, который ждёт разбор
|
||
|
||
Sol просят вернуть ровно такие строки (это часть текста пакета, отдельно требовать не нужно):
|
||
|
||
```
|
||
В1-ВЕРНОСТЬ: <число>
|
||
В1-ТЕРМИН: <число>
|
||
В1-ЯЗЫК: <число>
|
||
В1-ФОРМА: <число>
|
||
В2-ВЕРНОСТЬ: <число>
|
||
В2-ТЕРМИН: <число>
|
||
В2-ЯЗЫК: <число>
|
||
В2-ФОРМА: <число>
|
||
```
|
||
|
||
Если Sol добавит рассуждение — не удалять: транскрипт персистится дословно, лишнее разбор
|
||
игнорирует.
|
||
|
||
## Чего делать НЕ надо
|
||
|
||
* не сообщать Sol, какая модель какой вариант писала (в пакетах этого нет, и это важно);
|
||
* не менять порядок вариантов внутри пакета;
|
||
* не сводить несколько пакетов в один чат.
|
||
|
||
## Что это даёт
|
||
|
||
Два платных судьи эксп-21 разошлись на 2.19 ошибки там, где собственный шум судьи ±0.15.
|
||
Направление вывода «второй проход покупает качество» держится, значимость — нет: её несёт ОДИН
|
||
судья, аварийная лёгкая замена. Голос Sol — третий независимый инструмент, и он решает, к кому
|
||
из двух ближе истина.
|
||
|
||
Среди пакетов есть **контрольные** (в них одна сторона заведомо испорчена). Если Sol их не
|
||
ловит — его голоса по боевым парам не читаются. Какие именно контрольные — не сказано намеренно.
|
||
"""
|
||
|
||
|
||
def ingest() -> None:
|
||
key = json.loads((KEYS / "sol-KEY.json").read_text(encoding="utf-8"))
|
||
got = {}
|
||
for f in sorted(ANSW.glob("*.txt")):
|
||
if f.stem not in key:
|
||
print(f" ⚠ токена {f.stem} нет в ключе — пропуск")
|
||
continue
|
||
p = J.parse(f.read_text(encoding="utf-8"))
|
||
if p["В1-ВЕРНОСТЬ"] is None:
|
||
print(f" ⚠ {f.stem}: ответ не разобран")
|
||
continue
|
||
e1 = sum(p[f"В1-{ax}"] or 0 for ax in J.AXES)
|
||
e2 = sum(p[f"В2-{ax}"] or 0 for ax in J.AXES)
|
||
got[f.stem] = dict(**key[f.stem], e1=e1, e2=e2)
|
||
(MONEY.OUT / "sol-votes.json").write_text(json.dumps(got, ensure_ascii=False, indent=1),
|
||
encoding="utf-8")
|
||
print(f"принято голосов {len(got)} из {len(key)} пакетов")
|
||
|
||
|
||
def score() -> None:
|
||
f = MONEY.OUT / "sol-votes.json"
|
||
if not f.exists() or not json.loads(f.read_text(encoding="utf-8")):
|
||
print("Sol не вернулся — голосов нет. Это НЕ блокер: отчёт сдаётся с явной строкой.")
|
||
return
|
||
votes = json.loads(f.read_text(encoding="utf-8"))
|
||
dec = [v for v in votes.values() if v["kind"] == "контроль-декой"]
|
||
if dec:
|
||
caught = sum(1 for v in dec
|
||
if (v["e1"] < v["e2"]) == (v["first"] == "чистый"))
|
||
print(f"ДЕКОЙ: поймано {caught}/{len(dec)}"
|
||
+ ("" if caught == len(dec) else " ⛔ сессия Sol аннулируется целиком"))
|
||
if caught != len(dec):
|
||
return
|
||
print(f"\n{'контраст':14s}{'ед.':>5s}{'Sol':>9s}{'luna':>9s}{'gemini':>9s}")
|
||
for a, b in CONTRASTS:
|
||
per: dict[str, list[float]] = {}
|
||
for v in votes.values():
|
||
if v["contrast"] != f"{a}v{b}":
|
||
continue
|
||
m = (v["e2"] - v["e1"]) if v["first"] == a else (v["e1"] - v["e2"])
|
||
per.setdefault(v["uid"], []).append(m)
|
||
vals = [st.mean(x) for x in per.values() if len(x) == 2]
|
||
if not vals:
|
||
continue
|
||
paid = {}
|
||
for judge in J.JUDGES:
|
||
pv = []
|
||
for u in BO.units():
|
||
if u["uid"] not in per:
|
||
continue
|
||
bo = BO.margins_by_order(a, b, u["uid"], judge)
|
||
if len(bo) == 2:
|
||
pv.append(st.mean([st.mean(bo[0]), st.mean(bo[1])]))
|
||
paid[judge] = st.mean(pv) if pv else float("nan")
|
||
print(f"{a + ' vs ' + b:14s}{len(vals):5d}{st.mean(vals):+9.2f}"
|
||
+ "".join(f"{paid[j]:+9.2f}" for j in J.JUDGES))
|
||
print("\n⚠ единицы Sol — подмножество (наибольшее расхождение судей), поэтому платные колонки"
|
||
"\n пере-считаны на ТЕХ ЖЕ единицах, а не взяты из свода по всем 15.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
a = sys.argv[1:] or ["--emit"]
|
||
fn = {"--emit": emit, "--emit-b": emit_b, "--ingest": ingest, "--score": score}.get(a[0])
|
||
fn() if fn else print(__doc__)
|