#!/usr/bin/env python3 """Classical Western zodiac as its own atlas tradition (Додекада).""" 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" TAG_RE = re.compile(r"\[Додекада Созвездий:[^\]]*\]\s*") DART_RESTORE = { "попросить": "овен", "выпить": "телец", "близнец": "близнецы", "сесть": "рак", "смочь": "стрелец", "отдать": "козерог", "ожидать": "водолей", } RESTORE_META = { "овен": ( "существительное", "Космический Знак", "Первородный импульс весны, рог пробивающий скалу, ярость пробуждения. " "Овен начинает цикл без оглядки на риск — это пламя первого шага и воля вырваться из чрева зимы. " "В раскладе требует прямой смелости, лидерства и готовности идти на таран, предостерегая от слепой импульсивности.", ), "телец": ( "существительное", "Космический Знак", "Непоколебимая опора материи, плодородие, терпеливое накопление и сила плоти. " "Телец воплощает стойкость духа в физической форме, любовь к земным благам и упорный созидательный труд. " "Знак советует твердо стоять на земле, беречь ресурсы и строить на века.", ), "близнецы": ( "существительное", "Космический Знак", "Воздушный перекресток двух умов, зеркало полярностей, неразрывный союз братьев света и тени. " "Как знак зодиака — ведает гибкостью мысли, скоростью слова и связью миров. " "Требует диалога и интеграции противоположностей.", ), "рак": ( "существительное", "Космический Знак", "Лоно истока, панцирь родовой памяти, охраняющий нежнейшую уязвимость сердца. " "Рак движется боком, оберегая священный очаг и чувствуя приливы невидимой Луны. " "В раскладе говорит о глубинных родовых корнях, защите близких и верности памяти.", ), "стрелец": ( "существительное", "Космический Знак", "Кентавр духа, натянутый лук и стрела, устремленная за горизонт видимого мира. " "Стрелец объединяет животную силу земли с поиском божественной мудрости. " "Знак сулит дальнее странствие, наставника и веру в высокую цель.", ), "козерог": ( "существительное", "Космический Знак", "Одинокое восхождение на ледяную вершину, аскеза времени и кристаллизация высшего закона. " "Козерог преодолевает преграды через терпение, дисциплину и долг. " "Знак сулит вершину тем, кто не боится холода на пути к мастерству.", ), "водолей": ( "существительное", "Космический Знак", "Излияние кувшина живых вод будущего, сокрушение отживших плотин и братство свободных сердец. " "Водолей черпает знание из грядущих веков. Знак призывает сбросить оковы рутины и ценить дружбу.", ), } # n, id, ru, latin, glyph, element, planet, primary, cluster, lore STATIONS = [ ( 1, "aries", "Овен", "Aries", "♈", "Огонь", "Марс", "овен", ["баран", "начало", "рог"], "Первый знак тропического круга, точка овна, огонь Марса. Рог тарана, весенний прорыв, " "воля начать без карты. В раскладе — лидерский шаг и риск первого удара. " "Тень — слепая ярость и разрушение без плана.", ), ( 2, "taurus", "Телец", "Taurus", "♉", "Земля", "Венера", "телец", ["бык", "земля", "труд"], "Второй знак, земля Венеры, бык, держащий сад. Плоть, накопление, верность форме, " "труд, который строит на века. Знак велит стоять и растить. Тень — косность и жадность застоя.", ), ( 3, "gemini", "Близнецы", "Gemini", "♊", "Воздух", "Меркурий", "близнецы", ["вестник", "слово", "воздух"], "Третий знак, воздух Меркурия, двое в одном дыхании. Диалог, скорость мысли, вестник " "между берегами. Знак просит говорить и слушать. Тень — двуличие и рассеивание сил.", ), ( 4, "cancer", "Рак", "Cancer", "♋", "Вода", "Луна", "рак", ["луна", "дом", "вода"], "Четвёртый знак, вода Луны, панцирь рода. Лоно, очаг, память, ход боком вокруг того, " "что нельзя отдать. Знак бережёт близких. Тень — уход в раковину и цепляние за прошлое.", ), ( 5, "leo", "Лев", "Leo", "♌", "Огонь", "Солнце", "лев", ["солнце", "сердце", "царь"], "Пятый знак, огонь Солнца в сердце круга. Грива, трон, щедрость сияния, право быть видимым. " "Знак зовёт явить своё. Тень — тирания эго и театр вместо сердца.", ), ( 6, "virgo", "Дева", "Virgo", "♍", "Земля", "Меркурий", "дева", ["чистота", "хлеб", "мера"], "Шестой знак, земля Меркурия, колос и серп. Различение зерна от плевел, служение детали, " "порядок как забота. Знак велит навести меру. Тень — педантизм и страх осквернения.", ), ( 7, "libra", "Весы", "Libra", "♎", "Воздух", "Венера", "весы", ["союз", "мир", "мера"], "Седьмой знак, воздух Венеры, чаши договора. Равновесие, красота союза, суд без меча. " "Знак просит взвесить. Тень — вечные качели и ложный компромисс.", ), ( 8, "scorpio", "Скорпион", "Scorpio", "♏", "Вода", "Плутон", "скорпион", ["яд", "тайна", "смерть"], "Восьмой знак, вода глубин, жало и феникс. Кризис, который сжигает личину, тайна недр, " "яд, способный стать лекарством. Знак сулит перерождение. Тень — месть и саморазрушение.", ), ( 9, "sagittarius", "Стрелец", "Sagittarius", "♐", "Огонь", "Юпитер", "стрелец", ["путь", "вера", "лук"], "Девятый знак, огонь Юпитера, кентавр и лук. Стрела за горизонт, странствие, наставник, " "вера в цель, которой ещё нет на карте. Тень — догма и бегство в даль вместо дела.", ), ( 10, "capricorn", "Козерог", "Capricorn", "♑", "Земля", "Сатурн", "козерог", ["гора", "закон", "холод"], "Десятый знак, земля Сатурна, козёл на ледяном гребне. Долг, время, восхождение без зрителей. " "Знак сулит вершину за аскезу. Тень — черствость и страх срыва.", ), ( 11, "aquarius", "Водолей", "Aquarius", "♒", "Воздух", "Уран", "водолей", ["свобода", "друг", "воздух"], "Одиннадцатый знак, воздух Урана, кувшин вод будущего. Братья по духу, слом плотины, " "мысль, которая не помещается в обычай. Знак зовёт к свободе. Тень — холод утопии и бунт ради бунта.", ), ( 12, "pisces", "Рыбы", "Pisces", "♓", "Вода", "Нептун", "рыбы", ["вода", "сон", "вера"], "Двенадцатый знак, вода Нептуна, две рыбы в разные стороны. Растворение границ, " "сострадание, интуиция океана, конец круга, уже содержащий исток. Тень — иллюзия и утрата берега.", ), ] def restore_dart(text: str) -> str: for old, new in DART_RESTORE.items(): pos, cat, desc = RESTORE_META[new] desc_esc = desc.replace("\\", "\\\\").replace('"', '\\"') def repl(m: re.Match, new=new, pos=pos, cat=cat, desc_esc=desc_esc) -> str: return ( f"{m.group(1)}{new}{m.group(2)}" f"{m.group(3)}{pos}{m.group(4)}" f"{m.group(5)}{cat}{m.group(6)}" f"{m.group(7)}{desc_esc}{m.group(8)}" ) pat = re.compile( rf'(word: "){re.escape(old)}(",\s*\n)' rf'(\s*partOfSpeech: ")[^"]+(",\s*\n)' rf'(\s*frequency: [^\n]+\n\s*category: ")[^"]+(",\s*\n)' rf'(\s*description: ")[^"]*(")', ) if f'word: "{new}",' in text and f'word: "{old}",' not in text: continue text2, n = pat.subn(repl, text, count=1) if n != 1: raise SystemExit(f"failed to restore {old} -> {new} (n={n})") text = text2 return text def payload(word: str, dart_by_word: dict, lex_by_word: dict) -> dict: lex = lex_by_word.get(word) or {} return { "id": dart_by_word[word], "word": word, "emoji": lex.get("emoji") or "", "pos": lex.get("pos") or "существительное", "category": lex.get("category") or "", "desc": "", } def main() -> None: dart_text = restore_dart(DART.read_text(encoding="utf-8")) DART.write_text(dart_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")) word_to_tags: dict[str, list[str]] = defaultdict(list) missing = [] stations = [] for rec in STATIONS: n, sid, ru, latin, glyph, element, planet, prim, extra, lore = rec if prim not in dart_by_word: missing.append(f"primary {sid}/{prim}") continue cluster = [] seen = {prim} for w in extra: if w in seen: continue if w not in dart_by_word: missing.append(f"cluster {sid}/{w}") continue seen.add(w) cluster.append(payload(w, dart_by_word, lex_by_word)) st = { "id": sid, "name": f"{n} {ru} ({latin})", "symbol": glyph, "group": f"Додекада · {element} · {planet}", "lore": lore, "primary": payload(prim, dart_by_word, lex_by_word), "cluster": cluster, } stations.append(st) tag = f"[Додекада Созвездий: {n}/12 {ru} ({latin} / {element} / {planet})]" word_to_tags[prim].append(tag) for c in cluster: word_to_tags[c["word"]].append(tag) if missing: print("WARN", missing) west = { "id": "western_zodiac", "name": "Классический гороскоп (Додекада)", "icon": "♈", "count": len(stations), "description": ( "12 знаков тропического зодиака: огонь, земля, воздух, вода; " "управители от Марса до Нептуна; пересечения со словарём 999." ), "stations": stations, } ordered = {} for k, v in canon.items(): if k == "zodiac": ordered["western_zodiac"] = west ordered[k] = v if "western_zodiac" not in ordered: ordered["western_zodiac"] = west canon = ordered 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 = 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"western {len(stations)} stamped {nstamp} tagged {len(word_to_tags)}") for st in stations: cl = [c["word"] for c in st["cluster"]] print(f" {st['symbol']} {st['name']:24} prim={st['primary']['word']:12} {cl}") if __name__ == "__main__": main()