feat: complete academic symbol lexicon heal, 2-column Cooper extraction, and updated assets

This commit is contained in:
2026-10-09 15:26:58 +05:00
parent 7f91366201
commit 0b12e4583b
244 changed files with 611461 additions and 1617 deletions
+579
View File
@@ -0,0 +1,579 @@
#!/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()