#!/usr/bin/env python3 """exp16 — canon consolidation §C2 + canon-recovery metric §D3-3. miner-v1. $0. Canon by ALL occurrences (NOT first-wins, anti-LTCR): the winning dst for a term is the rendering scored by frequency-across-all-chunks × Palladius-conformity (type name/place) × case-lemma coverage — the VARIANT wins, not the first occurrence. Majority NEVER yields approved (canon proposal = draft). canon-recovery(ent): would the §C2 procedure, run over the drafts, propose a dst whose content lemmas COVER the owner-signed seed dst (memnorm + lemma level)? On records.json (injected) this is trivialized (spread≈0 — the drafts already carry the signed forms; §D1 confound). The ECOLOGICAL measurement is on the cold-start drafts (no injection): does canon recover the signed dst from an un-anchored draft? """ from __future__ import annotations import regex import exp16_common as X import spread as SP import palladius as PAL _COMMON_HEAD = {"гора", "деревня", "село", "селение", "стан", "клан", "род", "море", "первобытный", "камень", "истинный", "ци", "апертура"} # generic heads still counted, but not name-distinctive def signed_content_lemmas(dst: str) -> list[str]: """Distinctive lemmas of the signed dst (drop 2-char function words; keep name + key term words).""" lem = SP.lemmatize_ru(dst) return [l for l in lem if len(l) >= 3] def canon_propose(sm: SP.SpreadModel, ent: X.GTEntity, top=6): """Reconstruct the canon dst proposal from the drafts by ALL occurrences. For name/place: ordered Palladius name lemmas among the top associates. For term/title: the top content associates. Returns (proposed_lemmas, evidence).""" variants, cidx = sm.dst_variants(X.norm(ent.src), top=top) lemmas = [lm for lm, d, _ in variants] if ent.typ in ("name", "place", "nickname"): # canon = Palladius-conformant capitalized-name lemmas (the transliteration), by Dice rank proposed = [lm for lm in lemmas if PAL.is_palladius_token(lm, 1) and sm.is_name_lemma(lm)] # for places, prepend the generic head if strongly associated (гора/деревня) head = [lm for lm in lemmas if lm in ("гора", "деревня")] proposed = head[:1] + proposed if ent.typ == "place" else proposed else: proposed = [lm for lm in lemmas if len(lm) >= 3][:4] return proposed, variants def canon_recovery(sm: SP.SpreadModel, ent: X.GTEntity, cover_frac=0.5): """Recovered if >= cover_frac of the signed distinctive lemmas appear among the proposed lemmas.""" signed = set(signed_content_lemmas(ent.dst)) if not signed: return None, [], [] # nothing to recover (e.g. empty dst) proposed, variants = canon_propose(sm, ent) proposed_set = set(proposed) # also allow coverage by ANY top associate lemma (the content word may not be a name token) assoc = {lm for lm, d, _ in variants} covered = {s for s in signed if s in proposed_set or s in assoc} rec = len(covered) / len(signed) >= cover_frac return rec, sorted(signed), proposed def run(chunks, gt, label=""): sm = SP.SpreadModel(chunks) rows = [] rec_n = tot = 0 for e in gt: if not e.dst: continue occ = X.gt_occurrences(e, chunks) if occ == 0: continue rec, signed, proposed = canon_recovery(sm, e) if rec is None: continue tot += 1 rec_n += bool(rec) rows.append(dict(src=e.src, dst=e.dst, typ=e.typ, occ=occ, recovered=bool(rec), signed=signed, proposed=proposed)) return rows, (rec_n / tot if tot else None), tot if __name__ == "__main__": chunks = X.load_chunks(); gt = X.load_gt() rows, rate, tot = run(chunks, gt, "records.json (INJECTED — confounded upper bound, §D1)") print(f"canon-recovery on records.json (INJECTED drafts — confound §D1 trivializes this): " f"{sum(r['recovered'] for r in rows)}/{tot} = {rate:.3f}\n") print("misses (signed vs proposed):") for r in rows: if not r["recovered"]: print(f" {r['src']:<6}({r['typ']:<8}) signed={r['signed']} proposed={r['proposed']}") print("\nsample recovered:") for r in [x for x in rows if x["recovered"]][:12]: print(f" {r['src']:<6}({r['typ']:<8}) signed={r['signed']} -> proposed={r['proposed']}")