textmachine/eval/exp14b/exp14b_reseed_promote.py

228 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""exp14b/D38.5 — reseed: промоут НАПРАВЛЕННЫХ владельцем термов draft→approved (корень терм-
дрейфа, exp14b D38 §3 / D-лог:499). Воспроизводимый провенанс-трансформ (паттерн
exp13_seed_signoff), ИДЕМПОТЕНТЕН + байт-стабилен (D30.5). Сид ВНЕ git; промоут авторизован
владельцем (D37/D38, «Да» 12.07). НЕ промоутить лишнего — approved-инвариант.
Направлено (D-лог:499, сверено по сиду):
- грейды 甲等/乙等/丙等/丁等 → dst «разряд X» ⇒ «класс X» (D37; развести с 转→«ранг») + draft→approved
- 四代族长 → draft→approved (dst «Четвёртый глава рода» уже верна, D34/D37)
Остальные draft (南疆, 沈嬷嬷, гу-имена — D-лог:437) НЕ трогаем: «при желании владельца», не направлено.
Verify (ИСПОЛНЕНИЕМ): diff = ровно 5 термов; повторный прогон = байт-в-байт; approved +5;
enforce-зеркало memory.go:485-503(memConfirmed-only) + :615-641(suppressContained) на синт.чанке —
после промоута инъекция отдаёт 四代族长→«Четвёртый глава рода» и грейды, 族长 не съедается→«старейшина».
"""
from __future__ import annotations
import sys
import yaml
from pathlib import Path
BOOK = Path("/home/ubuntu/books/gu-zhenren")
SEED = BOOK / "guzhenren-seed-v2.yaml"
LOG = BOOK / "diag" / "glossary_v2_reseed_promote.md"
BAK = BOOK / "diag" / "guzhenren-seed-v2.pre-reseed.yaml"
DUMP = dict(allow_unicode=True, sort_keys=False, width=200) # ТЕ ЖЕ параметры, что писали сид (байт-стабильность)
MARK = "[reseed:approved D38.5]"
GRADE_LETTER = {"甲等": "А", "乙等": "Б", "丙等": "В", "丁等": "Г"}
# Чистые авторские заметки грейдов (D38.5: стейл-хвост «Head разряд declines / owner may prefer
# «первый/высший разряд»» вычищен по указанию оркестратора — сид пере-снапшотится, стейл не запекаем).
GRADE_NOTE = {
"А": "pinyin: jiǎděng; top aptitude grade (方正). Схема 甲乙丙丁 = класс А>Б>В>Г (все четыре); голова «класс» склоняется, буква инвариант.",
"Б": "pinyin: yǐděng; 2nd aptitude grade (漠北, 赤城).",
"В": "pinyin: bǐngděng; 3rd aptitude grade (方源).",
"Г": "pinyin: dīngděng; lowest aptitude grade.",
}
DIRECTED = list(GRADE_LETTER) + ["四代族长"] # ровно направленное владельцем (D-лог:499)
def load_path(p):
return yaml.safe_load(Path(p).read_text(encoding="utf-8"))
def load():
return load_path(SEED)
def n_approved(d):
return sum(1 for t in d["terms"] if t.get("status") == "approved")
def summarize(pre, post):
"""Полная 5-термовая таблица pre→post для направленных (не «что мутировало в этот прогон»)."""
a = {t["src"]: t for t in pre["terms"]}
b = {t["src"]: t for t in post["terms"]}
return [(s, f"{a[s]['dst']}/{a[s]['status']}", f"{b[s]['dst']}/{b[s]['status']}")
for s in DIRECTED if s in a and s in b]
def grade_forms(letter):
return [f"класса {letter}", f"классу {letter}", f"классом {letter}", f"классе {letter}"]
def apply_promote(data):
"""Мутирует data на месте. Возвращает список (src, старое, новое). Идемпотентно."""
applied = []
for t in data["terms"]:
src = t.get("src")
if src in GRADE_LETTER:
letter = GRADE_LETTER[src]
new_dst = f"класс {letter}"
old = (t.get("dst"), t.get("status"), t.get("note"))
# авторская чистая заметка (не наследуем стейл-хвост «разряд declines / первый/высший»)
new_note = (f"{MARK} грейд {src}→«класс {letter}» (D37: развести с 转→«ранг»; "
f"было «разряд», draft→approved). {GRADE_NOTE[letter]}")
t["dst"] = new_dst
t["type"] = "title"
t["decl"] = {"invariant": False, "forms": grade_forms(letter)}
t["status"] = "approved"
t["note"] = new_note
if old != (new_dst, "approved", new_note):
applied.append((src, f"{old[0]}/{old[1]}", f"{new_dst}/approved"))
elif src == "四代族长":
old = (t.get("dst"), t.get("status"))
note0 = t.get("note") or ""
t["status"] = "approved"
if MARK not in note0:
t["note"] = (f"{MARK} 四代族长 draft→approved (D38 §3: approved-длинный выигрывает "
f"longest-match над вложенным 族长 → 族长 больше не съедается неверно). " + note0)
if old[1] != "approved" or MARK not in note0:
applied.append((src, f"{old[0]}/{old[1]}", f"{old[0]}/approved"))
return applied
# ── enforce-зеркало (ТАРГЕТНЫЙ порт memory.go; при расхождении верить Go) ───────────────
def spoiler_blocked(entry, chapter):
return chapter < int(entry.get("since_ch", 0)) # since_ch=0 у всех наших → никогда не блок
def find_matches(chunk, entries):
"""[(start,end,idx)] по rune-индексам, прямой подстрокой (Han-ключи — без word-boundary,
suppressUnboundedPhonetic на Han не применяется, memory.go:651-654)."""
ms = []
for idx, e in enumerate(entries):
src = e["src"]
start = 0
while True:
i = chunk.find(src, start)
if i < 0:
break
ms.append((i, i + len(src), idx))
start = i + 1
return ms
def suppress_contained(ms, entries, chapter):
"""Порт memory.go:615-641. m отсеян, если есть o: содержит m, строго длиннее, validSuppressor(o)."""
def valid_suppressor(o):
return not spoiler_blocked(entries[o[2]], chapter)
kept = []
for i, m in enumerate(ms):
contained = any(
j != i and o[0] <= m[0] and o[1] >= m[1] and (o[1] - o[0]) > (m[1] - m[0]) and valid_suppressor(o)
for j, o in enumerate(ms))
if not contained:
kept.append(m)
return kept
def render_block(ms, entries, chapter):
"""Порт memory.go:485-503. memConfirmed ≈ status==approved (чистый одиночный матч, без
коллизий/post-check-неоднозначности — упрощение для синт.чанка). Дедуп по dst."""
lines, seen = [], set()
for (s, e, idx) in ms:
ent = entries[idx]
confirmed = (ent.get("status") == "approved") and not spoiler_blocked(ent, chapter)
if not confirmed:
continue
dst = (ent.get("dst") or "").strip()
if not dst or dst in seen:
continue
seen.add(dst)
lines.append(f"- «{dst}»")
return lines
def enforce_mirror(data, chunk, chapter):
entries = data["terms"]
ms = find_matches(chunk, entries)
ms = suppress_contained(ms, entries, chapter)
return render_block(ms, entries, chapter)
# синт. чанк (МОЙ конструкт, НЕ из книги — копирайт): 四代族长, standalone 族长, 家老, грейды
CHUNK = "第四代族长传承五转之力。当代族长古月博坐镇村中,家老古月漠尘辅之。方源乃丙等资质,其兄方正甲等。"
CHAPTER = 15 # 四代族长 recurs 節15; все термы since_ch:0 → не спойлер-блок
def main():
dry = "--dry" in sys.argv
# заморозить пре-reseed состояние ОДИН раз — иммутабельная истина «до» (устойчиво к повторным прогонам)
if not dry and not BAK.exists():
BAK.write_text(SEED.read_text(encoding="utf-8"), encoding="utf-8")
pre = load_path(BAK) if BAK.exists() else load() # пре-reseed истина (не текущий, возможно уже промоутнутый, сид)
before = enforce_mirror(pre, CHUNK, CHAPTER)
n_appr_before = n_approved(pre)
data = load() # текущий сид (мог быть уже промоутнут прошлым прогоном)
applied = apply_promote(data) # идемпотентно; applied = что реально мутировало В ЭТОТ прогон
missing = [s for s in DIRECTED if s not in {t.get("src") for t in data["terms"]}]
new_bytes = yaml.dump(data, **DUMP)
n_appr_after = n_approved(data)
if not dry:
SEED.write_text(new_bytes, encoding="utf-8")
after = enforce_mirror(data, CHUNK, CHAPTER)
table = summarize(pre, data) # ПОЛНАЯ 5-термовая провенанс-таблица pre(BAK)→post
# ── лог/провенанс ──
lg = ["# Глоссарий v2 — RESEED промоут грейдов + 四代族长 (D38.5, 2026-07-12)", "",
f"Направлено (D-лог:499): грейды 甲乙丙丁→«класс А/Б/В/Г»+draft→approved, 四代族长 draft→approved.",
f"Провенанс: `eval/exp14b/exp14b_reseed_promote.py` (идемпотентен, байт-стабилен D30.5). Пре-снапшот: `{BAK.name}`.",
f"approved до/после: {n_appr_before}{n_appr_after} (Δ={n_appr_after - n_appr_before}). "
f"Мутаций в последний прогон: {len(applied)} (0 = уже применено, идемпотентно).", "",
"| src | было (dst/status, пре-reseed) | стало |", "|---|---|---|"]
for s, o, n in table:
lg.append(f"| {s} | {o} | **{n}** |")
if missing:
lg += ["", "НЕ найдены в сиде (проверить): " + ", ".join(missing)]
delta = [x for x in after if x not in before]
lg += ["", "## Enforce-верификация (зеркало memory.go:485-503 + :615-641; при расхождении верить Go)",
f"Синт. чанк (МОЙ конструкт, не из книги): `{CHUNK}`", "",
"**editor-constraint блок ДО промоута** (грейды/四代族长 = draft → memConfirmed-фильтр их режет; "
"в 四代族长-контексте главы рода нет якоря — рядом только 家老→«старейшина»; standalone 族长→«глава клана»):",
"```", *(before or ["(пусто)"]), "```",
"**ПОСЛЕ промоута** (四代族长/грейды approved → инъектятся):",
"```", *(after or ["(пусто)"]), "```",
f"**Δ (появилось ТОЛЬКО после промоута):** {', '.join(delta) or ''}",
"→ 四代族长-контекст теперь заякорен на «Четвёртый глава рода» (а не оставлен «старейшине»); "
"грейды инъектятся «класс А/Б/В/Г»; 转-семья (五转) рендерится «ранг» — развести соблюдено."]
if not dry:
LOG.write_text("\n".join(lg), encoding="utf-8")
# ── консоль (self-review) ──
print(f"{'[DRY] ' if dry else ''}мутаций-в-прогон={len(applied)} missing={missing} approved {n_appr_before}{n_appr_after}")
print("промоут-таблица (пре-reseed → пост):")
for s, o, n in table:
print(f" {s}: {o}{n}")
print("\n-- enforce ДО (пре-reseed BAK) --"); [print(" " + x) for x in (before or ["(пусто)"])]
print("-- enforce ПОСЛЕ --"); [print(" " + x) for x in (after or ["(пусто)"])]
print("-- Δ (только после) --"); [print(" " + x) for x in (delta or ["(пусто)"])]
# ── ассерты само-проверки (падаем громко) ──
assert len(applied) <= 5 and not missing, f"scope-нарушение: applied={applied} missing={missing}"
assert len(table) == 5 and all(n.endswith("/approved") for _, _, n in table), f"таблица не 5-approved: {table}"
# фикс проявляется ТОЛЬКО после промоута (были draft-невидимы):
assert "- «Четвёртый глава рода»" in after and "- «Четвёртый глава рода»" not in before, "四代族长 не заякорен после промоута / был виден до"
assert "- «класс А»" in after and "- «класс В»" in after, "грейды не инъектятся после промоута"
assert "- «класс А»" not in before, "грейд был виден до промоута (ожидался draft-невидим)"
# 族长/家老 — approved, корректны в ОБОИХ (家老=старейшина легитимно; НЕ должно исчезать):
assert "- «глава клана»" in after, "standalone 族长 должен инъектить «глава клана»"
print("\nOK: enforce-инварианты выполнены — 四代族长/грейды видны ТОЛЬКО после промоута; standalone 族长 цел.")
if __name__ == "__main__":
main()