#!/usr/bin/env python3 """$0-демо ценности рекомендаций §D корзины 1 на ЖИВОМ банке coldrun-a (вопрос владельца 04.08 «насколько алгоритмы лучше текущего»): A4: intra-run конфликты consolidated×consolidated (containment + лексемный subset — реплика CanonConflicts/lexemeSubset из terminology.go) — сколько противоречий сегодня НЕ видит никто; A5: сдвиг ЧАСТОТНОГО топа вариантов при фолде голосов по нормализованной форме. """ import unicodedata from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "bank_autonomy")) from parse_common import parse_bankstop # noqa: E402 STEM_MIN, SLACK = 3, 2 # stemMinPrefix / stemSuffixSlack (terminology.go:379-386) def norm(s): s = unicodedata.normalize("NFKC", s or "").casefold().replace("ё", "е") for ch in "«»\"'": s = s.replace(ch, "") return " ".join(s.split()) def words(s): return [w for w in "".join(c if c.isalnum() else " " for c in norm(s)).split() if len(w) > 1] def same_lexeme(a, b): if a == b: return True n = 0 while n < len(a) and n < len(b) and a[n] == b[n]: n += 1 shorter = min(len(a), len(b)) if (n == len(a) or n == len(b)) and shorter >= STEM_MIN: return True return n >= max(shorter - SLACK, STEM_MIN) def lexeme_subset(need, have): return all(any(same_lexeme(w, h) for h in have) for w in need) if need else True def a4_conflicts(): bs = parse_bankstop() finals = {s: r["dst"] for s, r in bs.items() if r.get("dst") and r["dst"] != "—"} out = [] for a, da in finals.items(): for b, db in finals.items(): if a == b or b not in a: # b строго содержится в a (containment, как CanonConflicts) continue if not lexeme_subset(words(db), words(da)): out.append((a, da, b, db)) print(f"A4-демо: финалов с dst {len(finals)}; НЕВИДИМЫХ сегодня внутри-прогонных противоречий " f"(составной терм теряет перевод вложенного): {len(out)}") for a, da, b, db in sorted(out): print(f" {a}→{da!r} не несёт {b}→{db!r}") def a5_topshift(): bs = parse_bankstop() shifted = [] for s, r in bs.items(): vs = r.get("drafts") or [] if len(vs) < 2: continue raw_top, raw_top_n = vs[0][0], vs[0][1] # порядок §C2-3 (частота — главный фактор при пустом каноне) folded = {} for d, n in vs: folded[norm(d)] = folded.get(norm(d), 0) + n fold_top = max(folded, key=lambda k: (folded[k], k)) # ЧЕСТНЫЙ сдвиг: слитые голоса ОБОГНАЛИ сырой топ (строго больше). Равенство после фолда — # тай-брейк, не эффект нормализации (ловушка первой версии этого скрипта — поймана самопроверкой). if norm(raw_top) != fold_top and folded[fold_top] > raw_top_n: shifted.append((s, raw_top, fold_top, vs)) print(f"\nA5-демо: строк со spread>=2: {sum(1 for r in bs.values() if len(r.get('drafts') or [])>=2)}; " f"частотный топ ЧЕСТНО сдвигается нормализованным фолдом (слитые голоса обогнали сырой топ) у: {len(shifted)}") for s, rt, ft, vs in sorted(shifted): print(f" {s}: сырой топ {rt!r} → фолднутый {ft!r} ({' | '.join(f'{d}×{n}' for d,n in vs[:5])})") if __name__ == "__main__": a4_conflicts() a5_topshift()