Files
999/scripts/repair_canon_integrity.py

580 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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("</", "<\\/").replace("\u2028", "\\u2028").replace("\u2029", "\\u2029")
def embed_html(canon, words_data):
html = HTML.read_text(encoding="utf-8")
start = html.find("const TRADITIONS_CANON = ")
end = html.find("\n const CANONICAL_SYSTEMS")
if start < 0 or end < 0 or end < start:
raise SystemExit("html canon markers not found")
block = (
"const TRADITIONS_CANON = "
+ js_json(canon, 2)
+ ";\n\n const WORDS_DATA = "
+ js_json(words_data, None)
+ ";"
)
html = html[:start] + block + html[end:]
html = html.replace("10 великих систем", "${trads.length} традиций")
HTML.write_text(html, encoding="utf-8")
def write_alpha(lexicon):
rows = sorted(lexicon, key=lambda w: w["word"].lower().replace("ё", "е9"))
lines = ["# Алфавитный список 1000 слов Оракула", "", "| № | Слово | Эмодзи | ID | Часть речи | Категория |", "|---|---|---|---|---|---|"]
txt = []
for i, w in enumerate(rows, 1):
lines.append(
f"| {i} | **{w['word']}** | {w.get('emoji','')} | #{w['id']:03d} | {w.get('pos','')} | {w.get('category','')} |"
)
txt.append(f"{i}. {w['word']} {w.get('emoji','')} (id: {w['id']})")
ALPHA_MD.write_text("\n".join(lines) + "\n", encoding="utf-8")
ALPHA_TXT.write_text("\n".join(txt) + "\n", encoding="utf-8")
def assert_clean(items, kb, canon):
words = [it["word"] for it in items]
if len(words) != 1000 or len(set(words)) != 1000:
raise SystemExit("lexicon is not 1000 unique words")
if [it["id"] for it in items] != list(range(1000)):
raise SystemExit("ids are not 0..999")
by_word = set(words)
ghosts = []
for tid, trad in canon.items():
for st in trad["stations"]:
nodes = [st["primary"]] if st.get("primary") else []
nodes += st.get("cluster") or []
for n in nodes:
if n["word"] not in by_word or n["id"] != next(it["id"] for it in items if it["word"] == n["word"]):
ghosts.append(f"{tid}:{st['id']}:{n['word']}")
if ghosts:
raise SystemExit("ghosts remain: " + ", ".join(ghosts[:20]))
bad = []
for w, entry in kb.items():
if str(w).isdigit() or not isinstance(entry, dict):
continue
if low_has(entry.get("core", ""), STOCK_CORE):
bad.append("core:" + w)
if low_has(entry.get("shadow", ""), STOCK_SHADOW):
bad.append("shadow:" + w)
if low_has(entry.get("asks", ""), ("готов ли ты принять этот урок",)):
bad.append("ask:" + w)
if low_has(entry.get("step", ""), ("созерцать символ",)):
bad.append("step:" + w)
if bad:
raise SystemExit("stock remains: " + ", ".join(bad[:20]))
# id twins match word cores
for it in items:
twin = kb[str(it["id"])]
if twin.get("core") != kb[it["word"]].get("core"):
raise SystemExit(f"id twin mismatch {it['word']}")
def main():
text, items = load_dart()
lexicon = json.loads(LEX.read_text(encoding="utf-8"))
kb = json.loads(KB_ASSETS.read_text(encoding="utf-8"))
canon = json.loads(CANON_A.read_text(encoding="utf-8"))
deaths = apply_swaps(items, lexicon, kb)
apply_laws(kb, items)
rewrite_descriptions(items, kb)
stamp_from_canon(items, canon)
lex_by_id = {r["id"]: r for r in lexicon}
repair_canon(canon, items, lex_by_id)
kb = mirror_kb(kb, items)
assert_clean(items, kb, canon)
write_dart(text, items)
LEX.write_text(json.dumps(lexicon, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
payload = json.dumps(kb, ensure_ascii=False, indent=2) + "\n"
KB_ASSETS.write_text(payload, encoding="utf-8")
KB_OUT.write_text(payload, encoding="utf-8")
canon_raw = json.dumps(canon, ensure_ascii=False, indent=2) + "\n"
CANON_A.write_text(canon_raw, encoding="utf-8")
CANON_B.write_text(canon_raw, encoding="utf-8")
words_data = []
for it in items:
words_data.append(
{
"id": it["id"],
"word": it["word"],
"emoji": lex_by_id[it["id"]].get("emoji", ""),
"pos": it["pos"],
"category": it["cat"],
"description": it["desc"],
}
)
embed_html(canon, words_data)
write_alpha(lexicon)
print(f"deaths {len(deaths)}")
for line in deaths:
print(" ", line)
print("words", len(items), "kb", len(kb), "stations", sum(len(t['stations']) for t in canon.values()))
if __name__ == "__main__":
main()