#!/usr/bin/env python3 """docs/ARCHITECTURE.md → docs/ARCHITECTURE.html (самодостаточная страница). Источник правды — markdown; HTML пересобирается этой командой: ./venv/bin/python scripts/build-architecture-html.py Текстовая схема (```-блок после «Куб 4×4×4 задаёт…») заменяется SVG-схемой компонентов. Стиль — как у architecture.html v0.1 (paper / ink / signal). """ import os import re import sys import markdown ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SRC = os.path.join(ROOT, "docs", "ARCHITECTURE.md") DST = os.path.join(ROOT, "docs", "ARCHITECTURE.html") INK, DIM, LINE, SIGNAL, CARD = "#15150f", "#6b675e", "#b9b3a6", "#FF4F00", "#f3f0e8" def _box(x, y, w, name, sub, main=False): fill = "#ffe9dd" if main else "#ffffff" stroke = SIGNAL if main else LINE sw = 2 if main else 1.25 cx = x + w / 2 return (f'' f'{name}' f'{sub}') def diagram() -> str: bx, bw, ax, aw, cx, cw, h = 264, 232, 24, 184, 552, 184, 52 r1, r2, r3, r4, r5, st = 60, 132, 204, 276, 348, 428 mid_b, mid_a = bx + bw / 2, ax + aw / 2 arrows = [ f"M{ax + aw} {r1 + h / 2}H{bx}", f"M{ax + aw} {r2 + h / 2}H{bx}", f"M{mid_b} {r1 + h}V{r2}", f"M{mid_b} {r2 + h}V{r3}", f"M{mid_b} {r3 + h}V{r4}", f"M{mid_b} {r4 + h}V{r5}", f"M{mid_b} {r5 + h}V{st}", f"M{mid_a} {r5 + h}V{st}", f"M{bx + bw} {r5 + h / 2}H{cx}", ] dashed = [f"M{cx} {r2 + h / 2}H{bx + bw}", f"M{cx} {r4 + h / 2}H{bx + bw}"] paths = "".join(f'' for d in arrows) paths += "".join(f'' for d in dashed) boxes = "".join([ _box(ax, r1, aw, "Telegram", "@juno42bot: личка, группы"), _box(ax, r2, aw, "Портал и агенты", "MCP, канал агент↔агент"), _box(ax, r5, aw, "Человек", "cli.py, TUI"), _box(bx, r1, bw, "TG-поллер", "приём, файлы, голос, кнопки"), _box(bx, r2, bw, "Адресация", "кому сообщение: боту или людям"), _box(bx, r3, bw, "Гейт важности", "есть ли событие важнее порога"), _box(bx, r4, bw, "Круг доверия и память", "права тика, глубина D0–D3"), _box(bx, r5, bw, "LLM-воркер", "инструменты под стражем прав", main=True), _box(cx, r2, cw, "Decision-модель", "clef:27b → nimble"), _box(cx, r4, cw, "Память и ядро", "индекс nomic, ученики"), _box(cx, r5, cw, "9router и портал", "LLM-тики, MCP-вызовы"), ]) store = (f'' f'Стор событий' f'' "подписи ed25519 · проекции · сжатие с архивом · каждый шаг пишет события") return ( '
' f'' f'' "Сообщение проходит фильтры до LLM, всё оседает в сторе" f'{paths}{boxes}{store}' "
Слева — входы, по центру — фильтры перед LLM по порядку, справа — помощники " "(пунктир — консультируют, сплошная — вызывает воркер); стор событий — единственный " "источник правды.
") CSS = f""" :root{{--paper:#faf8f3;--ink:{INK};--signal:{SIGNAL};--dim:{DIM};--line:#d8d3c8;--card:{CARD}}} *{{box-sizing:border-box}} body{{margin:0;background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}} .wrap{{max-width:1000px;margin:0 auto;padding:48px 28px 120px;display:grid;grid-template-columns:220px 1fr;gap:48px}} nav{{position:sticky;top:24px;align-self:start;font-size:13.5px}} nav .kicker{{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase;margin-bottom:12px}} nav a{{display:block;color:var(--dim);text-decoration:none;padding:4px 0;border-left:2px solid transparent;padding-left:10px}} nav a:hover{{color:var(--ink);border-left-color:var(--signal)}} main{{min-width:0}} h1{{font-size:clamp(30px,5vw,46px);line-height:1.05;font-weight:800;letter-spacing:-.02em;margin:0 0 8px}} h1+p{{color:var(--dim);font-size:14px;margin-top:0;border-bottom:3px solid var(--ink);padding-bottom:20px}} h2{{font-size:24px;margin:56px 0 12px;padding-top:12px;border-top:1px solid var(--line);letter-spacing:-.01em}} h2::before{{content:"";display:inline-block;width:10px;height:10px;background:var(--signal);margin-right:12px;vertical-align:middle}} code{{font-family:ui-monospace,"JetBrains Mono",Menlo,monospace;font-size:.88em;background:var(--card);padding:1px 5px;border-radius:4px}} pre{{background:var(--card);padding:14px;border-radius:8px;overflow-x:auto}} table{{border-collapse:collapse;width:100%;margin:16px 0;font-size:14px;display:block;overflow-x:auto}} th,td{{border:1px solid var(--line);padding:7px 10px;text-align:left;vertical-align:top}} th{{background:var(--ink);color:var(--paper);font-weight:600}} tr:nth-child(even) td{{background:#f6f3ec}} ul,ol{{padding-left:22px}} li{{margin:4px 0}} strong{{font-weight:700}} .diagram{{margin:24px 0;background:#fff;border:1px solid var(--line);border-radius:10px;padding:12px}} .diagram svg{{width:100%;height:auto;display:block}} figcaption{{font-size:13px;color:var(--dim);margin-top:8px}} @media(max-width:860px){{.wrap{{grid-template-columns:1fr}} nav{{position:static}}}} """ def build() -> str: md = open(SRC, encoding="utf-8").read() md = re.sub(r"\n```\n(?=Telegram ──).*?\n```\n", "\n\n", md, count=1, flags=re.S) # чекбоксы дорожной карты → символ md = md.replace("- [ ] ", "- ☐ ").replace("- [x] ", "- ☑ ") conv = markdown.Markdown(extensions=["tables", "fenced_code", "toc"], extension_configs={"toc": {"slugify": lambda v, s: re.sub( r"[^\w\-]+", "-", v.lower()).strip("-")}}) body = conv.convert(md).replace("", diagram()) title = re.search(r"^# (.+)$", md, re.M).group(1) nav = "".join(f'{t["name"]}' for t in conv.toc_tokens[0]["children"]) if conv.toc_tokens else "" return (f'' f'' f"{title}
" f'
{body}
' "
\n") if __name__ == "__main__": html = build() with open(DST, "w", encoding="utf-8") as f: f.write(html) print(f"OK: {os.path.relpath(DST, ROOT)} ({len(html) // 1024} KB)", file=sys.stderr)