#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Repair oracle canon inside the closed thousand. Swaps sacred names onto junk slots, retargets planet chips onto metals, resyncs station descriptions, rewrites stock dossiers, and re-embeds the HTML atlas so geomancy symbols stay valid JavaScript. """ from __future__ import annotations import json import re import shutil from copy import deepcopy from pathlib import Path from dossier_laws import LAWS, NIGHTS ROOT = Path(__file__).resolve().parent.parent DART = ROOT / "lib" / "data" / "words_data.dart" LEX = ROOT / "oracle_enricher" / "data" / "lexicon.json" KB_ASSETS = ROOT / "assets" / "data" / "oracle_kb.json" KB_OUT = ROOT / "oracle_enricher" / "out" / "oracle_kb.json" CANON_A = ROOT / "assets" / "data" / "traditions_canon.json" CANON_B = ROOT / "oracle_enricher" / "data" / "traditions_canon.json" HTML = ROOT / "lexicon_viewer.html" ALPHA_MD = ROOT / "words_alphabetical.md" ALPHA_TXT = ROOT / "words_alphabetical.txt" STOCK_CORE = ( "сакральный символ", "архетипический образ в поле судьбы", "ключевую сферу кристаллизации", "вектор трансформации", "модус судьбы", "активный процесс духовной динамики", ) STOCK_SHADOW = ("искажение первообраза", "суета, поспешность") STOCK_ASK = ("готов ли ты принять этот урок", "созерцать символ") # Sacred word replaces this living junk word. One death per restore. SWAPS = [ ("умеренность", "начаться", "⚖️", "Образ / Архетип"), ("росток", "рассказать", "🌱", "Образ / Архетип"), ("ученик", "спросить", "📖", "Образ / Архетип"), ("терпение", "принести", "🪨", "Образ / Архетип"), ("войско", "помочь", "⚔️", "Образ / Архетип"), ("уныние", "пройти", "🌧️", "Образ / Архетип"), ("созерцание", "назвать", "👁️", "Образ / Архетип"), ("опасность", "связать", "⚠️", "Образ / Архетип"), ("свежесть", "написать", "💧", "Образ / Архетип"), ("осина", "более", "🌳", "Образ / Архетип"), ("дрок", "пол", "🌼", "Образ / Архетип"), ("лоза", "мужик", "🍇", "Образ / Архетип"), ("бузина", "ребята", "🌿", "Образ / Архетип"), ("императрица", "вновь", "👑", "Образ / Архетип"), ("император", "наверное", "👑", "Образ / Архетип"), ("влюбленные", "опять", "💕", "Образ / Архетип"), ("колесница", "постоянно", "🛞", "Священный Предмет"), ("дьявол", "тоже", "😈", "Образ / Архетип"), ("космос", "чуть", "🌌", "Космический Знак"), ("рябина", "зачем", "🌳", "Образ / Архетип"), ("ольха", "тут", "🌳", "Образ / Архетип"), ("ясень", "бывать", "🌳", "Образ / Архетип"), ("боярышник", "возникать", "🌿", "Образ / Архетип"), ("остролист", "значить", "🌿", "Образ / Архетип"), ("орешник", "казаться", "🌰", "Образ / Архетип"), ("яблоня", "находиться", "🍎", "Образ / Архетип"), ("тростник", "прийтись", "🌾", "Образ / Архетип"), ("сосна", "хотеться", "🌲", "Образ / Архетип"), ("вереск", "показаться", "🌸", "Образ / Архетип"), ("тис", "выглядеть", "🌲", "Образ / Архетип"), ] ALIAS = { "марс": "железо", "меркурий": "ртуть", "юпитер": "олово", "венера": "медь", "сатурн": "свинец", "оранжевый": "золотой", } TAG_OF = { "runes": "Руны", "ogham": "Огам", "kabbalah": "Каббала", "alchemy": "Алхимия", "tarot": "Таро", "lenormand": "Ленорман", "iching": "И Цзин", "geomancy": "Геомантия", "western_zodiac": "Додекада Созвездий", "zodiac": "Китайский гороскоп", "cosmology": "Космология", "days_of_week": "Септенер", "months_of_year": "Додекада", } COSMO_LABEL = { "upper_world": "Верхний Мир", "middle_world": "Срединный Мир", "lower_world": "Нижний Мир", "axis_mundi": "Мировая Ось", } DAY_SCRIPT = { "monday": ("יום שני", "月曜", "الإثنين", "йом шени · юэяо · аль-иснайн"), "tuesday": ("יום שלישי", "火曜", "الثلاثاء", "йом шлиши · хояо · ас-суляса"), "wednesday": ("יום רביעי", "水曜", "الأربعاء", "йом ревии · шуйяо · аль-арбиа"), "thursday": ("יום חמישי", "木曜", "الخميس", "йом хамиши · муяо · аль-хамис"), "friday": ("יום שישי", "金曜", "الجمعة", "йом шиши · цзиньяо · аль-джумуа"), "saturday": ("שבת", "土曜", "السبت", "шаббат · туяо · ас-сабт"), "sunday": ("יום ראשון", "日曜", "الأحد", "йом ришон · жияо · аль-ахад"), } MONTH_SCRIPT = { "january": ("ינואר", "一月", "كانون الثاني", "януар · и-юэ · канун ас-сани"), "february": ("פברואר", "二月", "شباط", "февруар · эр-юэ · шубат"), "march": ("מרץ", "三月", "آذار", "мерц · сань-юэ · азар"), "april": ("אפריל", "四月", "نيسان", "апрель · сы-юэ · нисан"), "may": ("מאי", "五月", "أيار", "май · у-юэ · айяр"), "june": ("יוני", "六月", "حزيران", "юни · лю-юэ · хузейран"), "july": ("יולי", "七月", "تموز", "юли · ци-юэ · таммуз"), "august": ("אוגוסט", "八月", "آب", "огуст · ба-юэ · аб"), "september": ("ספטמבר", "九月", "أيلول", "септембер · цзю-юэ · айлюль"), "october": ("אוקטובר", "十月", "تشرين الأول", "октобер · ши-юэ · тишрин аль-авваль"), "november": ("נובמבר", "十一月", "تشرين الثاني", "новембер · шии-юэ · тишрин ас-сани"), "december": ("דצמבר", "十二月", "كانون الأول", "децембер · шиэр-юэ · канун аль-авваль"), } def dart_unescape(s: str) -> str: out = [] i = 0 while i < len(s): if s[i] == "\\" and i + 1 < len(s): n = s[i + 1] out.append({"n": "\n", "t": "\t", '"': '"', "\\": "\\", "$": "$"}.get(n, n)) i += 2 else: out.append(s[i]) i += 1 return "".join(out) def dart_escape(s: str) -> str: return ( s.replace("\\", "\\\\") .replace('"', '\\"') .replace("$", "\\$") .replace("\n", " ") .replace("\r", " ") ) def grab(block: str, key: str) -> str: m = re.search(rf"{key}:\s*\"((?:\\.|[^\"\\])*)\"", block) if not m: raise SystemExit(f"no {key} in block") return dart_unescape(m.group(1)) def low_has(text: str, needles) -> bool: t = (text or "").lower() return any(n in t for n in needles) def split_tags(desc: str): tags = re.findall(r"\[[^\]]+\]", desc) rest = re.sub(r"\[[^\]]+\]", " ", desc) rest = re.sub(r"\s+", " ", rest).strip() return tags, rest def join_desc(tags, rest: str) -> str: body = rest.strip() if tags and body: return " ".join(tags) + " " + body if tags: return " ".join(tags) return body def ensure_tag(desc: str, tag: str) -> str: if tag in desc: return desc return (tag + " " + desc).strip() def clean_tags(desc: str) -> str: desc = desc.replace("[Планетарный Септенер:", "[Септенер:") desc = desc.replace("[Зодиак:", "[Додекада Созвездий:") # Нумерология и Матрица — живые теги традиции numerology. Не разворачивать в прозу. return re.sub(r"\s+", " ", desc).strip() def load_dart(): text = DART.read_text(encoding="utf-8") blocks = re.findall(r" WordItem\(\n(?:.*\n)*? \),", text) if len(blocks) != 1000: raise SystemExit(f"expected 1000 WordItems, got {len(blocks)}") items = [] for b in blocks: freq = re.search(r"frequency:\s*([0-9.]+)", b).group(1) items.append( { "id": int(re.search(r"id:\s*(\d+)", b).group(1)), "word": grab(b, "word"), "pos": grab(b, "partOfSpeech"), "freq": freq, "cat": grab(b, "category"), "desc": grab(b, "description"), } ) items.sort(key=lambda x: x["id"]) return text, items def write_dart(text: str, items): chunks = [] for it in items: chunks.append( " WordItem(\n" f" id: {it['id']},\n" f" word: \"{dart_escape(it['word'])}\",\n" f" partOfSpeech: \"{dart_escape(it['pos'])}\",\n" f" frequency: {it['freq']},\n" f" category: \"{dart_escape(it['cat'])}\",\n" f" description: \"{dart_escape(it['desc'])}\",\n" " )," ) new_body = "\n".join(chunks) new_text, n = re.subn( r" WordItem\(\n(?:.*\n)*? \),", lambda m, repl=iter(chunks): next(repl), text, count=1000, ) if n != 1000: raise SystemExit(f"rewrote {n} blocks") # The lambda above consumes chunks via closure; verify count of WordItem. if new_text.count("WordItem(") != 1000: raise SystemExit("dart word count drifted") DART.write_text(new_text, encoding="utf-8") def apply_swaps(items, lexicon, kb): by_word = {it["word"]: it for it in items} deaths = [] for new, death, emoji, cat in SWAPS: if new in by_word and death not in by_word: continue if new in by_word and death in by_word: raise SystemExit(f"both live: {new} and {death}") if death not in by_word: raise SystemExit(f"no slot for {new}, missing {death}") it = by_word.pop(death) it["word"] = new it["pos"] = "существительное" it["cat"] = cat by_word[new] = it row = next(r for r in lexicon if r["id"] == it["id"]) row["word"] = new row["emoji"] = emoji row["pos"] = "существительное" row["category"] = cat old = kb.pop(death, None) or {} kb.pop(str(it["id"]), None) core, shadow, ask, step = LAWS[new] entry = deepcopy(old) if isinstance(old, dict) else {} entry.update( { "id": it["id"], "word": new, "emoji": emoji, "pos": "существительное", "category": cat, "operator": "substance", "valence": entry.get("valence") or "liminal", "has_canon": True, "user_edited": False, "core": core, "oracle": f"{core} {shadow}", "shadow": shadow, "asks": ask, "step": step, "grammar": { "modifies": "подставляет соседнему знаку тело образа", "operator": "substance", "valence": "liminal", }, } ) kb[new] = entry deaths.append(f"{death} → {new} (#{it['id']:03d})") return deaths def apply_laws(kb, items): by_word = {it["word"]: it for it in items} for word, (core, shadow, ask, step) in LAWS.items(): if word not in by_word: continue entry = kb.get(word) if not isinstance(entry, dict): entry = {"id": by_word[word]["id"], "word": word} kb[word] = entry entry["word"] = word entry["id"] = by_word[word]["id"] entry["core"] = core entry["shadow"] = shadow entry["asks"] = ask entry["step"] = step entry["oracle"] = f"{core} {shadow}" entry["has_canon"] = True for word, (shadow, ask, step) in NIGHTS.items(): if word not in by_word or word in LAWS: continue entry = kb.get(word) if not isinstance(entry, dict): continue if low_has(entry.get("shadow", ""), STOCK_SHADOW): entry["shadow"] = shadow if low_has(entry.get("asks", ""), STOCK_ASK) or low_has(entry.get("step", ""), STOCK_ASK): entry["asks"] = ask entry["step"] = step def rewrite_descriptions(items, kb): cores = {} for word, entry in kb.items(): if isinstance(entry, dict) and not str(word).isdigit(): cores[entry.get("word") or word] = entry.get("core") or "" for it in items: desc = clean_tags(it["desc"]) tags, rest = split_tags(desc) if low_has(rest, STOCK_CORE) or low_has(rest, ("кристаллизации вашего опыта",)): if it["word"] in LAWS: rest = LAWS[it["word"]][0] else: core = cores.get(it["word"], "") if core and not low_has(core, STOCK_CORE): rest = core it["desc"] = join_desc(tags, rest) def stamp_from_canon(items, canon): by_word = {it["word"]: it for it in items} for tid, trad in canon.items(): label = TAG_OF.get(tid) if not label: continue for st in trad.get("stations") or []: nodes = [] if st.get("primary"): nodes.append(st["primary"]) nodes.extend(st.get("cluster") or []) for node in nodes: word = ALIAS.get(node.get("word"), node.get("word")) if word == "начать" or word not in by_word: continue # Only force a fresh station tag onto restored words and cosmology. if tid == "cosmology": short = COSMO_LABEL.get(st.get("id"), st.get("name", "Космология")) tag = f"[Космология: {short}]" by_word[word]["desc"] = ensure_tag(by_word[word]["desc"], tag) elif word in LAWS and word not in {it["word"] for it in items if False}: pass restored = {new for new, *_ in SWAPS} for tid, trad in canon.items(): label = TAG_OF.get(tid) if not label: continue for st in trad.get("stations") or []: words = [] if st.get("primary"): words.append(st["primary"].get("word")) words.extend(c.get("word") for c in st.get("cluster") or []) for raw in words: word = ALIAS.get(raw, raw) if word not in restored or word not in by_word: continue tag = f"[{label}: {st.get('name', st.get('id', ''))}]" by_word[word]["desc"] = ensure_tag(by_word[word]["desc"], tag) def attach_scripts(station, pack): hebrew, hanzi, arabic, say = pack station["hebrew"] = hebrew station["hanzi"] = hanzi station["arabic"] = arabic group = station.get("group") or "" if say not in group: station["group"] = f"{group} · {say}" if group else say def repair_canon(canon, items, lex_by_id): kab = canon["kabbalah"]["stations"] for st in kab: if st["id"] == "kether" and " · " not in st.get("group", ""): st["group"] = "Высшая триада · Ацилут" for st in canon["days_of_week"]["stations"]: if st["id"] in DAY_SCRIPT: attach_scripts(st, DAY_SCRIPT[st["id"]]) for st in canon["months_of_year"]["stations"]: if st["id"] in MONTH_SCRIPT: attach_scripts(st, MONTH_SCRIPT[st["id"]]) by_word = {it["word"]: it for it in items} missing = [] for trad in canon.values(): for st in trad.get("stations") or []: if st.get("primary"): _retarget(st["primary"], by_word, lex_by_id, missing, st["id"]) cluster = [] for node in st.get("cluster") or []: if node.get("word") == "начать": continue _retarget(node, by_word, lex_by_id, missing, st["id"]) cluster.append(node) st["cluster"] = cluster if missing: raise SystemExit("canon words still missing: " + ", ".join(missing[:30])) def _retarget(node, by_word, lex_by_id, missing, station_id): word = node.get("word") word = ALIAS.get(word, word) if word not in by_word: missing.append(f"{station_id}:{word}") return it = by_word[word] lex = lex_by_id[it["id"]] node["word"] = word node["id"] = it["id"] node["emoji"] = lex.get("emoji") or node.get("emoji") or "" node["pos"] = it["pos"] node["category"] = it["cat"] node["desc"] = it["desc"] def mirror_kb(kb, items): by_word = {it["word"]: it for it in items} out = {} for it in items: entry = kb.get(it["word"]) if not isinstance(entry, dict): raise SystemExit(f"kb missing {it['word']}") entry["id"] = it["id"] entry["word"] = it["word"] entry["pos"] = it["pos"] entry["category"] = it["cat"] entry["description"] = it["desc"] out[it["word"]] = entry out[str(it["id"])] = deepcopy(entry) return out def js_json(obj, indent=2): raw = json.dumps(obj, ensure_ascii=False, indent=indent) return raw.replace("