#!/usr/bin/env python3 """Geomancy: structured dots, flatten fake groups, lexicon intersections.""" from __future__ import annotations import json import re from collections import defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parents[1] CANON = ROOT / "oracle_enricher" / "data" / "traditions_canon.json" ASSET = ROOT / "assets" / "data" / "traditions_canon.json" LEX = ROOT / "oracle_enricher" / "data" / "lexicon.json" DART = ROOT / "lib" / "data" / "words_data.dart" HTML = ROOT / "lexicon_viewer.html" MD = ROOT / "words_alphabetical.md" GEO_TAG_RE = re.compile(r"\[Геомантия:[^\]]*\]\s*") # head → feet (Fire, Air, Water, Earth): 1 = single (odd), 2 = double (even) DOTS = { "via": [1, 1, 1, 1], "populus": [2, 2, 2, 2], "conjunctio": [2, 1, 1, 2], "carcer": [1, 2, 2, 1], "fortuna_major": [2, 2, 1, 1], "fortuna_minor": [1, 1, 2, 2], "acquisitio": [2, 1, 2, 1], "amissio": [1, 2, 1, 2], "laetitia": [1, 2, 2, 2], "tristitia": [2, 2, 2, 1], "puella": [1, 2, 1, 1], "puer": [1, 1, 2, 1], "albus": [2, 2, 1, 2], "rubeus": [2, 1, 2, 2], "caput_draconis": [2, 1, 1, 1], "cauda_draconis": [1, 1, 1, 2], } META = { "via": ("Вода", "Луна", "путь", ["дорога", "тропа"]), "populus": ("Вода", "Луна", "народ", ["встреча", "мир"]), "conjunctio": ("Воздух", "Меркурий", "союз", ["встреча", "связь"]), "carcer": ("Земля", "Сатурн", "верёвка", ["плен", "цепь", "граница"]), "fortuna_major": ("Земля", "Солнце", "победа", ["защита", "солнце"]), "fortuna_minor": ("Огонь", "Солнце", "успех", ["случай", "помощь"]), "acquisitio": ("Воздух", "Юпитер", "дар", ["золото", "изобилие"]), "amissio": ("Огонь", "Венера", "жертва", ["вдова", "конец"]), "laetitia": ("Воздух", "Юпитер", "радость", ["смех", "пир", "свет"]), "tristitia": ("Земля", "Сатурн", "печаль", ["скорбь", "холод"]), "puella": ("Вода", "Венера", "девушка", ["луна", "чистота"]), "puer": ("Огонь", "Марс", "мальчик", ["огонь", "воля", "страсть"]), "albus": ("Воздух", "Меркурий", "белый", ["чистота", "мудрость", "покой"]), "rubeus": ("Огонь", "Марс", "красный", ["огонь", "кровь", "страсть"]), "caput_draconis": ("Земля", "Северный Узел", "начало", ["вход", "порог", "рост"]), "cauda_draconis": ("Огонь", "Южный Узел", "конец", ["выход", "жертва", "цикл"]), } def symbol_from(dots: list[int]) -> str: return "\n".join("·" * n for n in dots) def payload(word: str, dart_by_word: dict, lex_by_word: dict, old: dict | None = None) -> dict: lex = lex_by_word.get(word) or {} old = old or {} return { "id": dart_by_word[word], "word": word, "emoji": lex.get("emoji") or old.get("emoji") or "", "pos": lex.get("pos") or old.get("pos") or "существительное", "category": lex.get("category") or old.get("category") or "", "desc": "", } def main() -> None: dart_text = DART.read_text(encoding="utf-8") dart_by_word = { w: int(i) for i, w in re.findall(r'WordItem\(\s*id:\s*(\d+),\s*word:\s*"([^"]+)"', dart_text) } lex_by_word = {e["word"]: e for e in json.loads(LEX.read_text(encoding="utf-8"))} canon = json.loads(CANON.read_text(encoding="utf-8")) geo = canon["geomancy"] word_to_tags: dict[str, list[str]] = defaultdict(list) missing = [] for st in geo["stations"]: sid = st["id"] dots = DOTS[sid] element, planet, prim_w, extra_names = META[sid] st["dots"] = dots st["symbol"] = symbol_from(dots) st["group"] = f"Геомантия · {element} · {planet}" rec = { "via": ("الطريق", "ат-Тарик", "Via", "Путь"), "populus": ("الجماعة", "аль-Джама'а", "Populus", "Народ"), "conjunctio": ("الاجتماع", "аль-Иджтима'", "Conjunctio", "Соединение"), "carcer": ("الحبس", "аль-Хабс", "Carcer", "Тюрьма"), "fortuna_major": ("النصرة الداخلة", "ан-Насра ад-Дахиля", "Fortuna Major", "Большая Удача"), "fortuna_minor": ("النصرة الخارجة", "ан-Насра аль-Хариджа", "Fortuna Minor", "Малая Удача"), "acquisitio": ("القبض الداخل", "аль-Кабд ад-Дахиль", "Acquisitio", "Приобретение"), "amissio": ("القبض الخارج", "аль-Кабд аль-Харидж", "Amissio", "Потеря"), "laetitia": ("الفرح", "аль-Фарах", "Laetitia", "Радость"), "tristitia": ("الحزن", "аль-Хузн", "Tristitia", "Печаль"), "puella": ("نقي الخد", "наки аль-хадд", "Puella", "Девушка"), "puer": ("الانكيس", "аль-Анкийс", "Puer", "Юноша"), "albus": ("البياض", "аль-Байяд", "Albus", "Белый"), "rubeus": ("الحمرة", "аль-Хумра", "Rubeus", "Красный"), "caput_draconis": ("رأس الجوزهر", "ра'с аль-джузхар", "Caput Draconis", "Голова Дракона"), "cauda_draconis": ("ذنب الجوزهر", "занаб аль-джузхар", "Cauda Draconis", "Хвост Дракона"), }.get(sid) if rec: script, latn, latin, ru = rec st["arabic"] = script n = int("".join(ch for ch in st.get("name", "") if ch.isdigit()) or "0") or ( list(DOTS).index(sid) + 1 ) st["name"] = f"{n} {latin} ({latn} / {ru})" if prim_w not in dart_by_word: missing.append(f"primary {sid}/{prim_w}") continue old_p = st.get("primary") or {} st["primary"] = payload(prim_w, dart_by_word, lex_by_word, old_p) extra = [] seen = {prim_w} for w in extra_names: if w in seen: continue if w not in dart_by_word: missing.append(f"cluster {sid}/{w}") continue seen.add(w) extra.append(w) st["cluster"] = [payload(w, dart_by_word, lex_by_word) for w in extra] tag = f"[Геомантия: {st['name']}]" word_to_tags[prim_w].append(tag) for w in extra: word_to_tags[w].append(tag) if missing: print("WARN", missing) geo["description"] = ( "16 классических фигур 'Илм ар-рамл: четыре ряда точек (голова → стопы), " "стихия и планета. Пересечения со словарём 999." ) geo["count"] = len(geo["stations"]) text = json.dumps(canon, ensure_ascii=False, indent=2) + "\n" CANON.write_text(text, encoding="utf-8") ASSET.write_text(text, encoding="utf-8") dart_lines = dart_text.splitlines(keepends=True) nstamp = 0 i = 0 while i < len(dart_lines): wm = re.match(r'(\s*word: ")([^"]+)(",\s*)$', dart_lines[i]) if wm: word = wm.group(2) for j in range(i + 1, min(i + 12, len(dart_lines))): raw = dart_lines[j].rstrip("\n") dm = re.match(r'^(\s*description: ")(.*)(",)\s*$', raw) if dm: body = GEO_TAG_RE.sub("", dm.group(2)).strip() tags = word_to_tags.get(word) if tags: dart_lines[j] = f"{dm.group(1)}{' '.join(tags)} {body}{dm.group(3)}\n" nstamp += 1 else: dart_lines[j] = f"{dm.group(1)}{body}{dm.group(3)}\n" break if dart_lines[j].strip().startswith("WordItem("): break i += 1 dart2 = "".join(dart_lines) DART.write_text(dart2, encoding="utf-8") html = HTML.read_text(encoding="utf-8") start = html.find(" const TRADITIONS_CANON = ") end = html.find(" const WORDS_DATA = ") if start > 0 and end > start: html = ( html[:start] + " const TRADITIONS_CANON = " + json.dumps(canon, ensure_ascii=False, indent=2) + ";\n\n" + html[end:] ) m = re.search(r"const WORDS_DATA = (\[.*?\]);", html, flags=re.DOTALL) if not m: raise SystemExit("WORDS_DATA not found") words = json.loads(m.group(1)) dart_descs = dict(re.findall(r'word: "([^"]+)"[\s\S]*?description: "([^"]*)"', dart2)) for w in words: wd = w.get("word") if wd in dart_descs: w["description"] = dart_descs[wd] html = html[: m.start(1)] + json.dumps(words, ensure_ascii=False) + html[m.end(1) :] HTML.write_text(html, encoding="utf-8") if MD.exists(): md = MD.read_text(encoding="utf-8") def repl_block(mm: re.Match) -> str: word = mm.group(1) body = mm.group(3) if word not in dart_descs: return mm.group(0) body2 = re.sub( r"\* \*\*Толкование:\*\* .+", f"* **Толкование:** {dart_descs[word]}", body, count=1, ) return f"### {word} `{mm.group(2)}`\n{body2}" md2 = re.sub( r"### ([^\s`]+) `(#[^`]+)`\n([\s\S]*?)(?=\n### |\n## |\Z)", repl_block, md, ) MD.write_text(md2, encoding="utf-8") print(f"stamped {nstamp} tagged {len(word_to_tags)}") keys = defaultdict(list) for st in geo["stations"]: gkey = st["group"].split(" · ", 1)[0] keys[gkey].append(st["id"]) cl = [c["word"] for c in st["cluster"]] print( f" {st['id']:16} {st['dots']} {st['group']:32} " f"prim={st['primary']['word']:12} {cl}" ) print("group keys", {k: len(v) for k, v in keys.items()}) if __name__ == "__main__": main()