#!/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 (
''
"Слева — входы, по центру — фильтры перед 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)