Freeze the pre-registration and density gate for re-judging the tier pass before any answer arrives
This commit is contained in:
parent
3aaba0dd3b
commit
a03ac66b0b
2 changed files with 712 additions and 0 deletions
364
eval/dovodka/gain.py
Normal file
364
eval/dovodka/gain.py
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
#!/usr/bin/env python3
|
||||
"""КАЛИБРОВКА УСИЛЕНИЯ СУДЬИ. $0, только чтение сырья.
|
||||
|
||||
⚠ ЗАЧЕМ. Риг сравнивает армы ВНУТРИ пачки, и общий сдвиг строгости судьи в контрасте
|
||||
сокращается. Но абсолютные числа «ошибок на единицу» печатаются в отчётах рядом друг с другом
|
||||
между осями и проходами — а строгость счёта у этого рига между прогонами гуляет вчетверо.
|
||||
Здесь она меряется числом, чтобы (а) межпрогонные сравнения не делались молча и (б) пачка,
|
||||
легшая на пол шкалы, была видна ДО того, как её «не различимо» пойдёт в вывод.
|
||||
|
||||
Что печатает:
|
||||
--density плотность счёта и доля нулей по каждому проходу и семейству
|
||||
--samearm ОДИН арм на ОДНИХ единицах в РАЗНЫХ проходах (подписи текста сверяются)
|
||||
--agree корреляция трудности единицы между семействами — меряют ли они одно и то же
|
||||
--tier пере-счёт контрастов прохода `tier` с числом ничьих на нуле
|
||||
--selftest проверки самого инструмента
|
||||
|
||||
Гейт: пачка, где доля нулей по несущей сумме ≥ ZERO_FLOOR_ALARM, не имеет права нести вывод
|
||||
«ниже порога различимости» — там прибор упёрся в пол, а не «не увидел разницы».
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import statistics as st
|
||||
import sys
|
||||
|
||||
BOOKS = pathlib.Path.home() / "books"
|
||||
AXRE = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE)
|
||||
|
||||
# Несущая сумма судьи — ВЕРНОСТЬ+ЯЗЫК (эррата Д0.13 П-5): ТЕРМИН отдан детерминированному
|
||||
# канон-гейту, ФОРМА наполовину механизируема и однажды переворачивала знак вывода.
|
||||
CARRY = ("ВЕРНОСТЬ", "ЯЗЫК")
|
||||
|
||||
ZERO_FLOOR_ALARM = 0.25 # доля нулевых клеток, выше которой «не различимо» не читается
|
||||
|
||||
PASSES = {
|
||||
"tier": (["editor-tier/aj-tier"], ["editor-tier/sol-tier"]),
|
||||
"border": (["editor-tier/aj-border"], ["editor-tier/sol-border"]),
|
||||
"d4": (["judging/sud-d4/p1-answers", "judging/sud-d4/p2-answers"],
|
||||
["judging/sol-d4-work/p1-answers", "judging/sol-d4-work/p2-answers"]),
|
||||
"d3": (["judging/sud-d3/p1-answers", "judging/sud-d3/p2-answers"],
|
||||
["judging/sol-d3-work/p1-answers", "judging/sol-d3-work/p2-answers"]),
|
||||
"d6": (["judging/sud-d6/p1-answers", "judging/sud-d6/p2-answers"],
|
||||
["judging/sol-d6-work/p1-answers", "judging/sol-d6-work/p2-answers"]),
|
||||
"d1": (["judging/sud-d1/p1-answers", "judging/sud-d1/p2-answers"],
|
||||
["judging/sol-d1-work/p1-answers", "judging/sol-d1-work/p2-answers"]),
|
||||
}
|
||||
|
||||
KEYS = { # проход → (ключ claude, ключ Sol)
|
||||
"tier": ("editor-tier/blind-keys/tier-KEY.json", None),
|
||||
"border": ("editor-tier/blind-keys/border-KEY.json", None),
|
||||
"d3": ("dovodka/blind-keys-d3", "dovodka/blind-keys-d3-sol"),
|
||||
"d6": ("dovodka/blind-keys-d6", "dovodka/blind-keys-d6-sol"),
|
||||
"d1": ("dovodka/blind-keys-d1", "dovodka/blind-keys-d1-sol"),
|
||||
"d4": ("dovodka/blind-keys-d4", None),
|
||||
}
|
||||
|
||||
|
||||
def load_key(rel: str | None) -> dict:
|
||||
"""Ключ лежит либо файлом, либо каталогом с `*-KEY.json` по пачкам."""
|
||||
if not rel:
|
||||
return {}
|
||||
p = BOOKS / rel
|
||||
if p.is_dir():
|
||||
k: dict = {}
|
||||
for f in sorted(p.glob("*-KEY.json")):
|
||||
k.update(json.loads(f.read_text(encoding="utf-8")))
|
||||
return k
|
||||
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else {}
|
||||
|
||||
|
||||
def read_answers(rels: list[str]) -> dict[str, dict[str, int]]:
|
||||
"""токен → {метка: несущая сумма}. `.OLD` и каталоги заданий не читаются."""
|
||||
out: dict[str, dict[str, int]] = {}
|
||||
for rel in rels:
|
||||
d = BOOKS / rel
|
||||
if not d.exists():
|
||||
continue
|
||||
for f in sorted(d.glob("*.txt")):
|
||||
cells: dict[str, dict[str, int]] = collections.defaultdict(dict)
|
||||
for m in AXRE.finditer(f.read_text(encoding="utf-8", errors="replace")):
|
||||
cells["Т" + m.group(1)][m.group(2)] = int(m.group(3))
|
||||
if cells:
|
||||
out.setdefault(f.stem, {}).update(
|
||||
{lab: sum(ax.get(a, 0) for a in CARRY) for lab, ax in cells.items()})
|
||||
return out
|
||||
|
||||
|
||||
def density(rels: list[str]) -> tuple | None:
|
||||
sc = read_answers(rels)
|
||||
vals = [v for labs in sc.values() for v in labs.values()]
|
||||
if not vals:
|
||||
return None
|
||||
zeros = sum(1 for v in vals if v == 0) / len(vals)
|
||||
return len(sc), len(vals), st.mean(vals), st.pstdev(vals), zeros
|
||||
|
||||
|
||||
def by_arm(rels: list[str], keyrel: str | None) -> dict[str, list[tuple[str, int]]]:
|
||||
"""арм → [(uid, счёт)]. Метки де-слепятся ключом того же семейства."""
|
||||
key = load_key(keyrel)
|
||||
out: dict[str, list[tuple[str, int]]] = collections.defaultdict(list)
|
||||
for tok, labs in read_answers(rels).items():
|
||||
km = key.get(tok)
|
||||
if not isinstance(km, dict):
|
||||
continue
|
||||
for lab, v in km.items():
|
||||
if isinstance(v, dict) and lab in labs and v.get("arm"):
|
||||
out[v["arm"]].append((v.get("uid", ""), labs[lab]))
|
||||
return out
|
||||
|
||||
|
||||
def sigs(keyrel: str, arm: str) -> dict[str, str]:
|
||||
"""uid → подпись текста арма: доказывает, что в двух проходах судили одни байты."""
|
||||
out = {}
|
||||
for km in load_key(keyrel).values():
|
||||
if not isinstance(km, dict):
|
||||
continue
|
||||
for v in km.values():
|
||||
if isinstance(v, dict) and v.get("arm") == arm:
|
||||
out[v["uid"]] = v.get("sig")
|
||||
return out
|
||||
|
||||
|
||||
def pearson(a: dict, b: dict) -> tuple[float | None, int]:
|
||||
k = sorted(set(a) & set(b))
|
||||
if len(k) < 4:
|
||||
return None, len(k)
|
||||
x, y = [a[i] for i in k], [b[i] for i in k]
|
||||
mx, my = st.mean(x), st.mean(y)
|
||||
num = sum((i - mx) * (j - my) for i, j in zip(x, y))
|
||||
den = (sum((i - mx) ** 2 for i in x) * sum((j - my) ** 2 for j in y)) ** 0.5
|
||||
return (num / den if den else None), len(k)
|
||||
|
||||
|
||||
def per_unit(rels: list[str], keyrel: str | None) -> dict[str, float]:
|
||||
"""uid → средний счёт по БОЕВЫМ армам: трудность единицы глазами семейства.
|
||||
|
||||
⚠ Отбор идёт по ИМЕНИ арма, а не по полю `kind`: в ключах фазы Д контроли `CTRLfloor`
|
||||
и `CTRLmargin` помечены `kind='боевой'`, и фильтр по kind тянул бы их в трудность единицы.
|
||||
"""
|
||||
key = load_key(keyrel)
|
||||
agg: dict[str, list[int]] = collections.defaultdict(list)
|
||||
for tok, labs in read_answers(rels).items():
|
||||
km = key.get(tok)
|
||||
if not isinstance(km, dict):
|
||||
continue
|
||||
for lab, v in km.items():
|
||||
if not (isinstance(v, dict) and lab in labs and v.get("uid")):
|
||||
continue
|
||||
if str(v.get("arm", "")).startswith("CTRL"):
|
||||
continue
|
||||
agg[v["uid"]].append(labs[lab])
|
||||
return {u: st.mean(v) for u, v in agg.items() if v}
|
||||
|
||||
|
||||
def floor_pairs(keyrel: str | None) -> tuple[int, int, int, int]:
|
||||
"""Пары шумового пола: (пар, половины в РАЗНЫХ заданиях, побайтных близнецов, всего половин).
|
||||
|
||||
Норма Д0.6: половины пары судят РАЗНЫЕ сессии (иначе межсессионная компонента в порог не
|
||||
попадает), и побайтно равные половины дают нулевую разницу, то есть занижают порог.
|
||||
"""
|
||||
key = load_key(keyrel)
|
||||
loc: dict[str, list[tuple[str, str]]] = collections.defaultdict(list)
|
||||
for tok, km in key.items():
|
||||
if not isinstance(km, dict):
|
||||
continue
|
||||
for v in km.values():
|
||||
if isinstance(v, dict) and str(v.get("arm", "")).startswith("CTRLfloor"):
|
||||
loc[v["uid"]].append((tok, v.get("sig", "")))
|
||||
pairs = [(u, it) for u, it in loc.items() if len(it) >= 2]
|
||||
diff_task = sum(1 for _, it in pairs if len({t for t, _ in it}) > 1)
|
||||
twins = sum(1 for _, it in pairs if len({s for _, s in it}) == 1)
|
||||
return len(pairs), diff_task, twins, sum(len(it) for it in loc.values())
|
||||
|
||||
|
||||
def cmd_density() -> int:
|
||||
print(f"{'проход':10s} {'семья':7s} {'единиц':>6s} {'клеток':>7s} "
|
||||
f"{'ошибок/ед.':>11s} {'sd':>6s} {'нулей':>7s}")
|
||||
bad = []
|
||||
for name, (ca, so) in PASSES.items():
|
||||
for fam, rels in (("claude", ca), ("Sol", so)):
|
||||
r = density(rels)
|
||||
if r is None:
|
||||
print(f"{name:10s} {fam:7s} — ответов нет")
|
||||
continue
|
||||
files, n, mean, sd, z = r
|
||||
mark = " ⛔ ПОЛ ШКАЛЫ" if z >= ZERO_FLOOR_ALARM else ""
|
||||
print(f"{name:10s} {fam:7s} {files:6d} {n:7d} {mean:11.2f} {sd:6.2f} {z:6.0%}{mark}")
|
||||
if z >= ZERO_FLOOR_ALARM:
|
||||
bad.append(f"{name}/{fam}")
|
||||
print()
|
||||
print("⚠ Плотность счёта — свойство ПРОГОНА. Числа «ошибок на единицу» из разных проходов")
|
||||
print(" между собой НЕ сравнивать: внутри пачки сдвиг сокращается в контрасте, между")
|
||||
print(" пачками — нет.")
|
||||
if bad:
|
||||
print(f"\n⛔ Пачки на полу шкалы (нулей ≥{ZERO_FLOOR_ALARM:.0%}): {', '.join(bad)}")
|
||||
print(" «Ниже порога различимости» от них не принимается: прибор упёрся в ноль.")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_samearm(arm: str = "R0", a: str = "tier", b: str = "border") -> int:
|
||||
ta, tb = sigs(KEYS[a][0], arm), sigs(KEYS[b][0], arm)
|
||||
common = sorted(set(ta) & set(tb))
|
||||
same = [u for u in common if ta[u] == tb[u] and ta[u]]
|
||||
if not same:
|
||||
print(f"общих единиц с совпадающей подписью у арма {arm} нет — контроль неприменим")
|
||||
return 0
|
||||
A = {u: v for u, v in by_arm(PASSES[a][0], KEYS[a][0]).get(arm, [])}
|
||||
B = {u: v for u, v in by_arm(PASSES[b][0], KEYS[b][0]).get(arm, [])}
|
||||
k = [u for u in same if u in A and u in B]
|
||||
print(f"арм {arm}, семья claude, подписи текста совпадают {len(same)}/{len(common)} — "
|
||||
f"судили ОДНИ БАЙТЫ")
|
||||
print(f" проход {a:8s} среднее {st.mean([A[u] for u in k]):5.2f} "
|
||||
f"нулей {sum(1 for u in k if A[u] == 0)}/{len(k)}")
|
||||
print(f" проход {b:8s} среднее {st.mean([B[u] for u in k]):5.2f} "
|
||||
f"нулей {sum(1 for u in k if B[u] == 0)}/{len(k)}")
|
||||
lower = sum(1 for u in k if A[u] < B[u])
|
||||
print(f" по-единично {a} ниже {b}: {lower}/{len(k)}")
|
||||
print(f"\n uid {a:>6s} {b:>7s}")
|
||||
for u in k:
|
||||
print(f" {u} {A[u]:6d} {B[u]:7d}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_floor() -> int:
|
||||
print("ШУМОВОЙ ПОЛ: половины пары обязаны судиться РАЗНЫМИ сессиями и НЕ быть близнецами")
|
||||
print(f"{'проход':10s} {'пар':>4s} {'в разных заданиях':>18s} {'ПОБАЙТНЫХ близнецов':>20s}")
|
||||
rc = 0
|
||||
for name in ("tier", "border", "d4", "d3", "d6", "d1"):
|
||||
pairs, diff_task, twins, halves = floor_pairs(KEYS[name][0])
|
||||
if not halves:
|
||||
print(f"{name:10s} — пола в ключе нет")
|
||||
continue
|
||||
mark = " ⛔" if twins else ""
|
||||
print(f"{name:10s} {pairs:4d} {diff_task:18d} {twins:20d}{mark}")
|
||||
if twins:
|
||||
rc = 1
|
||||
print("\n⚠ Побайтно равная половина даёт нулевую разницу и ЗАНИЖАЕТ порог различимости.")
|
||||
print(" Половины в одном задании не ловят межсессионную компоненту строгости.")
|
||||
return rc
|
||||
|
||||
|
||||
def cmd_agree() -> int:
|
||||
print("корреляция ТРУДНОСТИ ЕДИНИЦЫ между семействами — меряют ли они одно и то же")
|
||||
print(f"{'проход':10s} {'общих ед.':>9s} {'r':>7s}")
|
||||
for name in ("tier", "d3", "d6", "d1"):
|
||||
kc, ks = KEYS[name]
|
||||
A = per_unit(PASSES[name][0], kc)
|
||||
B = per_unit(PASSES[name][1], ks or kc)
|
||||
r, n = pearson(A, B)
|
||||
print(f"{name:10s} {n:9d} {('—' if r is None else f'{r:+.3f}'):>7s}")
|
||||
print("\n⚠ Отрицательная или нулевая корреляция = семейства меряют РАЗНОЕ, и спор между их")
|
||||
print(" вердиктами не разрешается нормировкой на пороги.")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_tier() -> int:
|
||||
key = KEYS["tier"][0]
|
||||
print(f"{'контраст':12s} {'семья':7s} {'n':>3s} {'перевес':>8s} {'ничьих':>7s} "
|
||||
f"{'на нуле':>8s}")
|
||||
for a, b in (("T1", "R0"), ("T2", "R0"), ("T3", "R0"), ("R0", "F")):
|
||||
for fam, rels in (("claude", PASSES["tier"][0]), ("Sol", PASSES["tier"][1])):
|
||||
arms = by_arm(rels, key)
|
||||
A = dict(arms.get(a, [])), dict(arms.get(b, []))
|
||||
k = sorted(set(A[0]) & set(A[1]))
|
||||
if not k:
|
||||
continue
|
||||
d = [A[0][u] - A[1][u] for u in k]
|
||||
ties = sum(1 for x in d if x == 0)
|
||||
tie0 = sum(1 for u in k if A[0][u] == 0 and A[1][u] == 0)
|
||||
print(f"{a + ' vs ' + b:12s} {fam:7s} {len(k):3d} {-st.mean(d):+8.2f} "
|
||||
f"{ties:7d} {tie0:8d}")
|
||||
print("\n⚠ Перевес положителен, когда ПЕРВЫЙ арм лучше (у него меньше ошибок).")
|
||||
print(" Ничья на нуле = обоим армам поставлен ноль: разница ненаблюдаема по построению,")
|
||||
print(" знаковый тест на таких парах мощности не имеет.")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_selftest() -> int:
|
||||
fails = []
|
||||
ok = lambda c, m: fails.append(m) if not c else None
|
||||
|
||||
d = density(PASSES["tier"][0])
|
||||
ok(d is not None, "проход tier не читается")
|
||||
if d:
|
||||
ok(d[1] == 128, f"tier/claude: клеток {d[1]}, ожидалось 128")
|
||||
ok(d[4] >= ZERO_FLOOR_ALARM, "tier/claude обязан флагаться как пол шкалы")
|
||||
|
||||
ta, tb = sigs(KEYS["tier"][0], "R0"), sigs(KEYS["border"][0], "R0")
|
||||
common = set(ta) & set(tb)
|
||||
ok(len(common) == 16, f"общих единиц R0 между tier и border {len(common)}, ожидалось 16")
|
||||
ok(all(ta[u] == tb[u] for u in common),
|
||||
"подписи арма R0 разошлись между проходами — контроль недействителен")
|
||||
|
||||
r, n = pearson(per_unit(PASSES["d3"][0], KEYS["d3"][0]),
|
||||
per_unit(PASSES["d3"][1], KEYS["d3"][1]))
|
||||
ok(n == 16, f"Д3: общих единиц {n}, ожидалось 16")
|
||||
ok(r is not None and r > 0, "Д3: семейства обязаны коррелировать положительно")
|
||||
|
||||
rt, nt = pearson(per_unit(PASSES["tier"][0], KEYS["tier"][0]),
|
||||
per_unit(PASSES["tier"][1], KEYS["tier"][0]))
|
||||
ok(nt == 16, f"tier: общих единиц {nt}, ожидалось 16")
|
||||
ok(rt is not None and rt < r, "tier обязан коррелировать ХУЖЕ, чем Д3")
|
||||
|
||||
# Контроли не должны попадать в трудность единицы. Ловушка: в ключах фазы Д у
|
||||
# CTRLfloor/CTRLmargin поле kind='боевой', поэтому фильтр по kind их не отсекает.
|
||||
# Проверка прямая: трудность считается ровно по боевым армам прохода.
|
||||
key = load_key(KEYS["d3"][0])
|
||||
war = {v["arm"] for km in key.values() if isinstance(km, dict) for v in km.values()
|
||||
if isinstance(v, dict) and not str(v.get("arm", "")).startswith("CTRL")}
|
||||
ctrl = {v["arm"] for km in key.values() if isinstance(km, dict) for v in km.values()
|
||||
if isinstance(v, dict) and str(v.get("arm", "")).startswith("CTRL")}
|
||||
ok("CTRLfloor" in ctrl and "CTRLmargin" in ctrl, "Д3: контролей в ключе не найдено")
|
||||
got = by_arm(PASSES["d3"][0], KEYS["d3"][0])
|
||||
ok(war <= set(got), "Д3: не все боевые армы разобрались")
|
||||
pu = per_unit(PASSES["d3"][0], KEYS["d3"][0])
|
||||
per_uid_labels = collections.Counter()
|
||||
for tok, labs in read_answers(PASSES["d3"][0]).items():
|
||||
km = key.get(tok)
|
||||
if isinstance(km, dict):
|
||||
for lab, v in km.items():
|
||||
if isinstance(v, dict) and lab in labs and v.get("uid") \
|
||||
and not str(v.get("arm", "")).startswith("CTRL"):
|
||||
per_uid_labels[v["uid"]] += 1
|
||||
ok(set(pu) == set(per_uid_labels), "Д3: набор единиц трудности разошёлся с ключом")
|
||||
ok(max(per_uid_labels.values()) <= 2 * len(war),
|
||||
f"Д3: на единицу приходится больше меток, чем боевых армов ×2 прохода ({len(war)})")
|
||||
|
||||
halves = floor_pairs(KEYS["d6"][0])[3]
|
||||
ok(halves > 0, "Д6: пол в ключе не найден")
|
||||
|
||||
for m in fails:
|
||||
print("ПРОВАЛ:", m)
|
||||
print(f"селфтест: провалов {len(fails)}")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = sys.argv[1:] or ["--density"]
|
||||
if "--selftest" in args:
|
||||
return cmd_selftest()
|
||||
rc = 0
|
||||
if "--density" in args:
|
||||
rc |= cmd_density()
|
||||
if "--samearm" in args:
|
||||
rc |= cmd_samearm()
|
||||
if "--floor" in args:
|
||||
rc |= cmd_floor()
|
||||
if "--agree" in args:
|
||||
rc |= cmd_agree()
|
||||
if "--tier" in args:
|
||||
rc |= cmd_tier()
|
||||
if "--all" in args:
|
||||
for f in (cmd_density, cmd_samearm, cmd_floor, cmd_agree, cmd_tier):
|
||||
print("=" * 78)
|
||||
rc |= f()
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
348
eval/dovodka/tier2.py
Normal file
348
eval/dovodka/tier2.py
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ПЕРЕ-СУДЕЙСТВО ПРОХОДА `tier` ПОЧИНЕННЫМ ПРИБОРОМ. $0 (агент-сессии).
|
||||
|
||||
Заказано владельцем 15.08: «Можно пересудить. Но не подгонять. Нужно понять, кто ошибается из
|
||||
судей». Пере-судятся ТЕ ЖЕ ТЕКСТЫ — ни одна клетка не покупается заново, поэтому разница между
|
||||
старым и новым замером есть разница ПРИБОРА, а не материала.
|
||||
|
||||
⚠ ЧТО ИМЕННО СЛОМАНО В СТАРОМ ЗАМЕРЕ (замерено, не предположено — `gain.py`):
|
||||
· пачка легла на пол шкалы: 38% клеток ровно ноль, из них 24% с ПУСТЫМ обоснованием; у арма `T2`
|
||||
нулей 75%, у боевого `R0` — 56%; в решающем контрасте 9 ничьих из 16, 7 из них на нуле;
|
||||
· тот же арм `R0` на тех же 16 единицах в проходе `border` дал 4.31 ошибки против 0.69 здесь
|
||||
(подписи текста совпадают 16/16) — прибор читал те же байты вшестеро глуше;
|
||||
· семейства не коррелируют по трудности единицы (r=−0.22) при +0.31…+0.70 на осях фазы Д;
|
||||
· разобранная руками единица `38c99e5efb`: боевой редактор превратил адресата угрозы в союзника
|
||||
(«мы с тобой покараем его» против 我二人…除了你), судья поставил 0 с пустым обоснованием.
|
||||
|
||||
⚠ ТРИ ПРАВКИ ПАНЕЛИ — ПРЕ-РЕГИСТРИРУЮТСЯ ЗДЕСЬ, ДО ПЕРВОГО ОТВЕТА:
|
||||
1. `CTRLfloorA` УБРАН как отдельная метка: он побайтно равен боевому арму `T1` в 16/16, то есть
|
||||
один текст лежал в пачке дважды. Пол считается как и раньше — по РАЗНОСТИ двух генераций
|
||||
того же лечения, но вторая метка теперь одна (`CTRLfloor` = прежний `CTRLfloorB`), а первой
|
||||
служит сам `T1`. Дублей в пачке не остаётся.
|
||||
2. `CTRLmargin` ДОБАВЛЕН — маржевый декой (норма П-2), которого у прохода `tier` не было никогда.
|
||||
Без него «не различимо» неотличимо от слепоты прибора; именно этого контроля не хватило паку 23.
|
||||
3. Ключ ВЫПУСКАЕТСЯ НОВЫЙ (`tier2-KEY.json`, своя соль), старый не трогается. Раскладка меток —
|
||||
функция соли, uid и НАБОРА армов; эмиссия поверх старого ключа переписала бы отсуженную
|
||||
раскладку (авария фазы Д, хендофф §5 п.1).
|
||||
|
||||
⚠ ТРИ ПРАВКИ ЗАДАНИЯ — ТОЖЕ ПРЕ-РЕГИСТРИРУЮТСЯ. Лечится ЗАДАНИЕМ, а не сменой судьи: прибор
|
||||
устойчив (побайтно один текст под двумя метками дал sd=0.000 по всем осям 16/16), у него высокий
|
||||
порог счёта, а не разболтанность.
|
||||
A. МОЛЧАЛИВЫЙ НОЛЬ ЗАПРЕЩЁН: клетка с нулём обязана нести непустое «почему».
|
||||
B. В рубрику ВЕРНОСТЬ внесены КЛАССЫ, которые старая пачка пропускала: инверсия адресата или
|
||||
участника реплики · потеря/подмена разряда, ранга, числа · состояние идиомы · снятое или
|
||||
добавленное отрицание. Классы арм-нейтральны и ни на один арм не указывают.
|
||||
C. Сказано прямо, что ноль — утверждение, а не отсутствие ответа.
|
||||
Всё прочее в задании побайтно то же, что судили агенты пака 23.
|
||||
|
||||
⚠ ПРАВИЛО РЕШЕНИЯ — ОБЪЯВЛЯЕТСЯ ДО ОТВЕТОВ:
|
||||
· пачка, не взявшая гейт плотности (`gain.py`: доля нулей ≥25%), НЕ несёт вывода «не различимо»;
|
||||
· семейство, чей перевес на маржевом декое не пересекает свой порог, теряет право говорить
|
||||
«не хуже» (П-2);
|
||||
· контрасты `T1/R0`, `T2/R0`, `T3/R0` — точный знаковый тест с поправкой Холма по трём;
|
||||
`R0/F` — позитивный контроль, в семейство не входит;
|
||||
· порог различимости = 2.8·sd/√n по разностям пар пола, как во всей фазе;
|
||||
· сравнение со старым замером печатается по-армно и является ГЛАВНЫМ результатом прогона:
|
||||
вопрос стоит не «кто лучше», а «что показывает прибор, когда ему запрещено молчать».
|
||||
|
||||
Запуск: tier2.py --emit | --score | --selftest
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import statistics as st
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||
ZONE = Path(__file__).resolve().parent
|
||||
for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"):
|
||||
sys.path.insert(0, str(REPO / "eval" / d))
|
||||
|
||||
ET = Path.home() / "books" / "editor-tier"
|
||||
OLD_KEY = ET / "blind-keys" / "tier-KEY.json"
|
||||
OLD_TASKS, OLD_ANSW = ET / "aj-tier-tasks", ET / "aj-tier"
|
||||
NEW_KEY = ET / "blind-keys" / "tier2-KEY.json"
|
||||
SALT_F = ET / "blind-keys" / "SALT-tier2.txt"
|
||||
TASKS, ANSW = ET / "tier2-tasks", ET / "tier2"
|
||||
|
||||
AX = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА")
|
||||
CARRY = ("ВЕРНОСТЬ", "ЯЗЫК")
|
||||
ARMS = ("R0", "T1", "T2", "T3", "F") # боевые; контраст — T*/R0, контроль — R0/F
|
||||
CTRL = ("CTRLdecoy", "CTRLfloor", "CTRLmargin")
|
||||
PER_SESSION = 4 # заданий на сессию, как гонялся `border`
|
||||
SALT = "tier2-2026-08-15" # своя соль; старая не трогается
|
||||
|
||||
_axre = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE)
|
||||
_whyre = re.compile(r"^[ТT](\d+)-ПОЧЕМУ:(.*)$", re.MULTILINE)
|
||||
|
||||
|
||||
def _load(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def _sud():
|
||||
"""`sud` грузится лениво: он разбирает флаги на уровне модуля и тянет сырьё своей оси."""
|
||||
return sys.modules.get("sud") or _load("sud", ZONE / "sud.py")
|
||||
|
||||
|
||||
def old_texts() -> dict[str, dict[str, str]]:
|
||||
"""(uid → {арм: текст}) из ОТСУЖЕННЫХ заданий пака 23. Тексты не покупаются заново."""
|
||||
key = json.loads(OLD_KEY.read_text(encoding="utf-8"))
|
||||
out: dict[str, dict[str, str]] = {}
|
||||
for tok, km in key.items():
|
||||
if not isinstance(km, dict):
|
||||
continue
|
||||
f = OLD_TASKS / f"{tok}.txt"
|
||||
if not f.exists():
|
||||
continue
|
||||
body = f.read_text(encoding="utf-8")
|
||||
# варианты разделены строками вида «Т3:»; исходник идёт до первой такой строки
|
||||
parts = re.split(r"^(Т\d):\s*$", body, flags=re.MULTILINE)
|
||||
src = re.search(r"^ИСХОДНИК:\s*\n(.*?)(?=\n=+\n|\Z)", parts[0], re.DOTALL | re.MULTILINE)
|
||||
texts = {}
|
||||
for i in range(1, len(parts) - 1, 2):
|
||||
texts[parts[i]] = parts[i + 1].strip()
|
||||
for lab, meta in km.items():
|
||||
if not isinstance(meta, dict) or lab not in texts:
|
||||
continue
|
||||
uid = meta["uid"]
|
||||
out.setdefault(uid, {})["_src"] = (src.group(1).strip() if src else "")
|
||||
out[uid][meta["arm"]] = texts[lab]
|
||||
return out
|
||||
|
||||
|
||||
def layout(uid: str, arms: tuple[str, ...]) -> dict[str, str]:
|
||||
"""Метка → арм. Чистая функция от соли, uid и НАБОРА армов — как во всей дуге."""
|
||||
order = sorted(arms)
|
||||
seed = hashlib.sha256(f"{SALT}|{uid}|{'|'.join(order)}".encode()).hexdigest()
|
||||
rank = sorted(order, key=lambda a: hashlib.sha256(f"{seed}|{a}".encode()).hexdigest())
|
||||
return {f"Т{i}": a for i, a in enumerate(rank, 1)}
|
||||
|
||||
|
||||
HEAD = """Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на китайском и {n} независимых русских перевода этого же фрагмента под слепыми метками.
|
||||
|
||||
Посчитай ОШИБКИ в каждом варианте по типам:
|
||||
ВЕРНОСТЬ — сказано не то, что в исходнике: искажение, инверсия, выдумка, пропуск факта.
|
||||
Сюда же входят, и их пропускают чаще всего:
|
||||
· инверсия адресата или участника реплики (кто кому говорит, кто кого бьёт);
|
||||
· потеря или подмена разряда, ранга, числа, единицы измерения;
|
||||
· состояние идиомы или действия: совершилось против ещё не совершилось;
|
||||
· снятое или, наоборот, добавленное отрицание.
|
||||
ТЕРМИН — имя или термин передан не так, как в других местах того же текста.
|
||||
ЯЗЫК — то, чего носитель не напишет: калька, канцелярит, несуществующее слово, кривой
|
||||
порядок слов, рассогласование.
|
||||
ФОРМА — вёрстка и пунктуация: оформление реплик, кавычки, тире, абзацы.
|
||||
|
||||
Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника — не сравнивая варианты между
|
||||
собой. Порядок вариантов ничего не значит и специально перемешан.
|
||||
|
||||
⚠ НОЛЬ — ЭТО УТВЕРЖДЕНИЕ, А НЕ ОТСУТСТВИЕ ОТВЕТА. Ноль означает «я сверил этот вариант с
|
||||
исходником по этой оси и дефектов не нашёл». Поэтому ПОЧЕМУ обязательно для КАЖДОЙ метки, в том
|
||||
числе при нулях: если по всем осям ноль, напиши «прочитано, дефектов не найдено». Пустое ПОЧЕМУ
|
||||
не принимается и на диск не попадает.
|
||||
|
||||
Ответ ЗАПИШИ ФАЙЛОМ в {path} — ровно в таком виде и ничем больше, блок на каждую метку:
|
||||
|
||||
{skeleton}
|
||||
ПОЧЕМУ для НЕНУЛЕВОЙ оси: короткая ЦИТАТА из текста через « | ».
|
||||
Число без цитаты не принимается и на диск не попадает: именно из-за отсутствия цитат показания
|
||||
прошлых судей оказались непроверяемыми.
|
||||
"""
|
||||
|
||||
|
||||
def emit() -> None:
|
||||
S = _sud()
|
||||
src = old_texts()
|
||||
TASKS.mkdir(parents=True, exist_ok=True)
|
||||
ANSW.mkdir(parents=True, exist_ok=True)
|
||||
NEW_KEY.parent.mkdir(parents=True, exist_ok=True)
|
||||
SALT_F.write_text(SALT + "\n", encoding="utf-8")
|
||||
key: dict[str, dict] = {
|
||||
"_проход": "tier2",
|
||||
"_семейство": ["T1/R0", "T2/R0", "T3/R0"],
|
||||
"_контроль": ["R0/F"],
|
||||
"_армы": list(ARMS) + list(CTRL),
|
||||
}
|
||||
made = 0
|
||||
for uid, arms in sorted(src.items()):
|
||||
if not all(arms.get(a, "").strip() for a in ARMS):
|
||||
print(f" ⚠ {uid}: не хватает армов, единица пропущена")
|
||||
continue
|
||||
texts = {a: arms[a] for a in ARMS}
|
||||
texts["CTRLdecoy"] = arms.get("CTRLdecoy", "")
|
||||
texts["CTRLfloor"] = arms.get("CTRLfloorB", "")
|
||||
planted, n_pl = S.margin_plant(arms["R0"])
|
||||
texts["CTRLmargin"] = planted if n_pl >= 2 else ""
|
||||
panel = tuple(a for a in (*ARMS, *CTRL) if texts.get(a, "").strip())
|
||||
lay = layout(uid, panel)
|
||||
tok = hashlib.sha256(f"{SALT}|{uid}".encode()).hexdigest()[:10]
|
||||
skel = "\n\n".join("\n".join(f"{lab}-{a}: <число>" for a in AX) + f"\n{lab}-ПОЧЕМУ: <цитаты>"
|
||||
for lab in lay)
|
||||
body = [HEAD.format(n=len(lay), path=ANSW / f"{tok}.txt", skeleton=skel),
|
||||
"", "=" * 60, "ИСХОДНИК:", arms.get("_src", "")]
|
||||
for lab, arm in lay.items():
|
||||
body += ["", "=" * 60, f"{lab}:", texts[arm]]
|
||||
(TASKS / f"{tok}.txt").write_text("\n".join(body), encoding="utf-8")
|
||||
key[tok] = {lab: {"arm": a, "uid": uid,
|
||||
"kind": "контроль" if a.startswith("CTRL") else "боевой",
|
||||
"sig": hashlib.sha256(texts[a].encode()).hexdigest()[:12]}
|
||||
for lab, a in lay.items()}
|
||||
made += 1
|
||||
NEW_KEY.write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
toks = [t for t in key if not t.startswith("_")]
|
||||
print(f"заданий {made} → {TASKS}")
|
||||
print(f"ключ → {NEW_KEY} (судье НЕ давать); соль → {SALT_F}")
|
||||
print(f"сессий по {PER_SESSION} задания: {-(-len(toks) // PER_SESSION)}")
|
||||
for i in range(0, len(toks), PER_SESSION):
|
||||
print(f" сессия {i // PER_SESSION + 1}: " + " · ".join(toks[i:i + PER_SESSION]))
|
||||
|
||||
|
||||
def read(dirpath: Path, keypath: Path) -> tuple[dict, list]:
|
||||
"""{(uid, арм): сумма несущих осей} + список замечаний годности."""
|
||||
key = json.loads(keypath.read_text(encoding="utf-8"))
|
||||
out, bad = {}, []
|
||||
for f in sorted(dirpath.glob("*.txt")):
|
||||
km = key.get(f.stem)
|
||||
if not isinstance(km, dict):
|
||||
continue
|
||||
t = f.read_text(encoding="utf-8", errors="replace")
|
||||
cells: dict[str, dict[str, int]] = {}
|
||||
for m in _axre.finditer(t):
|
||||
cells.setdefault("Т" + m.group(1), {})[m.group(2)] = int(m.group(3))
|
||||
why = {"Т" + m.group(1): m.group(2).strip() for m in _whyre.finditer(t)}
|
||||
for lab, ax in cells.items():
|
||||
meta = km.get(lab)
|
||||
if not isinstance(meta, dict):
|
||||
continue
|
||||
if len(ax) < len(AX):
|
||||
bad.append(f"{f.stem}/{lab}: не все четыре оси")
|
||||
continue
|
||||
if not why.get(lab):
|
||||
bad.append(f"{f.stem}/{lab}: ПУСТОЕ обоснование (запрещено заданием)")
|
||||
out[(meta["uid"], meta["arm"])] = sum(ax.get(a, 0) for a in CARRY)
|
||||
return out, bad
|
||||
|
||||
|
||||
def _sign_p(diffs: list[float]) -> float:
|
||||
"""Точный двусторонний знаковый тест; ничьи отбрасываются."""
|
||||
import math
|
||||
nz = [d for d in diffs if d != 0]
|
||||
n = len(nz)
|
||||
if not n:
|
||||
return 1.0
|
||||
k = sum(1 for d in nz if d > 0)
|
||||
c = lambda a, b: math.comb(a, b)
|
||||
tail = sum(c(n, i) for i in range(min(k, n - k) + 1)) / 2 ** n
|
||||
return min(1.0, 2 * tail)
|
||||
|
||||
|
||||
def score() -> int:
|
||||
if not NEW_KEY.exists():
|
||||
print("ключа нет — сначала --emit")
|
||||
return 1
|
||||
new, bad = read(ANSW, NEW_KEY)
|
||||
old, _ = read(OLD_ANSW, OLD_KEY)
|
||||
if not new:
|
||||
print(f"ответов в {ANSW} нет")
|
||||
return 1
|
||||
uids = sorted({u for u, _ in new})
|
||||
print(f"единиц отсужено: {len(uids)} · клеток {len(new)} · замечаний годности {len(bad)}")
|
||||
for b in bad[:8]:
|
||||
print(" ⚠", b)
|
||||
|
||||
vals = [v for (u, a), v in new.items() if not a.startswith("CTRL")]
|
||||
zeros = sum(1 for v in vals if v == 0) / max(len(vals), 1)
|
||||
print(f"\nГЕЙТ ПЛОТНОСТИ: ошибок/клетку {st.mean(vals):.2f} · доля нулей {zeros:.0%} "
|
||||
f"→ {'ГОДНО' if zeros < 0.25 else '⛔ ПОЛ ШКАЛЫ, вывод «не различимо» не принимается'}")
|
||||
|
||||
dec = [new[(u, "CTRLdecoy")] - new[(u, "R0")] for u in uids
|
||||
if (u, "CTRLdecoy") in new and (u, "R0") in new]
|
||||
print(f"ГРУБЫЙ ДЕКОЙ: пойман {sum(1 for d in dec if d > 0)}/{len(dec)} · медиана +{st.median(dec) if dec else 0:.1f}")
|
||||
|
||||
fl = [new[(u, "T1")] - new[(u, "CTRLfloor")] for u in uids
|
||||
if (u, "T1") in new and (u, "CTRLfloor") in new]
|
||||
sd = st.pstdev(fl) if len(fl) > 1 else 0.0
|
||||
thr = 2.8 * sd / max(len(fl), 1) ** 0.5 if fl else 0.0
|
||||
print(f"СВОЙ ПОЛ: пар {len(fl)} · sd {sd:.2f} → ПОРОГ РАЗЛИЧИМОСТИ {thr:.2f}")
|
||||
|
||||
mg = [new[(u, "CTRLmargin")] - new[(u, "R0")] for u in uids
|
||||
if (u, "CTRLmargin") in new and (u, "R0") in new]
|
||||
mgm = st.mean(mg) if mg else 0.0
|
||||
ok_m = mgm > thr
|
||||
print(f"МАРЖЕВЫЙ ДЕКОЙ (гейт чувствительности П-2): n={len(mg)} среднее {mgm:+.2f} "
|
||||
f"против порога {thr:.2f} → {'ПРОЙДЕН' if ok_m else '⛔ НЕ ПРОЙДЕН — права на «не хуже» нет'}")
|
||||
|
||||
print(f"\n{'контраст':12s}{'ед.':>5s}{'перевес':>9s}{'p':>9s}{'p Холма':>10s} вердикт")
|
||||
ps = {}
|
||||
for a in ("T1", "T2", "T3"):
|
||||
d = [new[(u, "R0")] - new[(u, a)] for u in uids if (u, a) in new and (u, "R0") in new]
|
||||
if d:
|
||||
ps[a] = (_sign_p(d), st.mean(d), len(d))
|
||||
for i, (a, (p, m, n)) in enumerate(sorted(ps.items(), key=lambda x: x[1][0])):
|
||||
holm = min(1.0, p * (len(ps) - i))
|
||||
v = "ПЕРЕШЁЛ ПОРОГ" if abs(m) > thr and holm < 0.05 else "ниже порога"
|
||||
print(f"{a + ' vs R0':12s}{n:5d}{m:+9.2f}{p:9.4f}{holm:10.4f} {v}")
|
||||
d = [new[(u, "R0")] - new[(u, "F")] for u in uids if (u, "F") in new and (u, "R0") in new]
|
||||
if d:
|
||||
print(f"{'R0 vs F':12s}{len(d):5d}{st.mean(d):+9.2f}{_sign_p(d):9.4f}{'—':>10s} позитивный контроль")
|
||||
|
||||
print(f"\n{'арм':12s}{'СТАРЫЙ замер':>14s}{'НОВЫЙ замер':>13s}{'сдвиг':>8s} ← главный результат")
|
||||
for a in (*ARMS, *CTRL):
|
||||
o = [old[(u, a)] for u in uids if (u, a) in old]
|
||||
n_ = [new[(u, a)] for u in uids if (u, a) in new]
|
||||
if not n_:
|
||||
continue
|
||||
om = st.mean(o) if o else float("nan")
|
||||
print(f"{a:12s}{om:14.2f}{st.mean(n_):13.2f}{st.mean(n_) - om:+8.2f}")
|
||||
print("\n⚠ Тексты не менялись ни в одном арме. Любой сдвиг здесь — свойство ПРИБОРА.")
|
||||
return 0
|
||||
|
||||
|
||||
def selftest() -> int:
|
||||
fails = []
|
||||
ok = lambda c, m: fails.append(m) if not c else None
|
||||
src = old_texts()
|
||||
ok(len(src) == 16, f"старых единиц разобрано {len(src)}, ожидалось 16")
|
||||
if src:
|
||||
u0 = next(iter(src))
|
||||
ok(all(src[u0].get(a, "").strip() for a in ARMS), f"{u0}: не все боевые армы извлеклись")
|
||||
ok(len(src[u0].get("_src", "")) > 200, f"{u0}: исходник не извлёкся")
|
||||
# дублей в новой панели быть не должно
|
||||
S = _sud()
|
||||
t = {a: src[u0][a] for a in ARMS}
|
||||
t["CTRLfloor"] = src[u0].get("CTRLfloorB", "")
|
||||
t["CTRLmargin"] = S.margin_plant(src[u0]["R0"])[0]
|
||||
sigs = [hashlib.sha256(v.encode()).hexdigest() for v in t.values() if v.strip()]
|
||||
ok(len(sigs) == len(set(sigs)), f"{u0}: в новой панели остались побайтные дубли")
|
||||
# старая панель дубли содержала — проверка, что мы чиним реальную болезнь
|
||||
old_pair = src[u0].get("CTRLfloorA", "") == src[u0].get("T1", "")
|
||||
ok(old_pair, f"{u0}: ожидалось, что старый CTRLfloorA ≡ T1 (дефект пака 23)")
|
||||
lay = layout(u0, tuple(a for a in (*ARMS, *CTRL) if t.get(a, "").strip()))
|
||||
ok(len(lay) == len(set(lay.values())), "раскладка меток не биективна")
|
||||
ok(layout(u0, tuple(sorted(lay.values()))) == lay, "раскладка не воспроизводима")
|
||||
ok(_sign_p([1, 1, 1, 1, 1]) < 0.07, "знаковый тест: пять единиц в одну сторону должны дать p<0.07")
|
||||
ok(_sign_p([0, 0, 0]) == 1.0, "знаковый тест: одни ничьи должны дать p=1")
|
||||
for m in fails:
|
||||
print("ПРОВАЛ:", m)
|
||||
print(f"селфтест tier2: провалов {len(fails)}")
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:] or ["--selftest"]
|
||||
if a[0] == "--emit":
|
||||
emit()
|
||||
elif a[0] == "--score":
|
||||
sys.exit(score())
|
||||
elif a[0] == "--selftest":
|
||||
sys.exit(selftest())
|
||||
else:
|
||||
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")
|
||||
Loading…
Add table
Reference in a new issue