228 lines
14 KiB
Python
228 lines
14 KiB
Python
#!/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()
|