simbiot-harness/scripts/build-architecture-html.py

129 lines
8.3 KiB
Python
Raw 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
"""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'<g><rect x="{x}" y="{y}" width="{w}" height="52" rx="8" fill="{fill}" '
f'stroke="{stroke}" stroke-width="{sw}"/>'
f'<text x="{cx}" y="{y + 22}" text-anchor="middle" font-weight="600" fill="{INK}">{name}</text>'
f'<text x="{cx}" y="{y + 40}" text-anchor="middle" font-size="11.5" fill="{DIM}">{sub}</text></g>')
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'<path d="{d}" marker-end="url(#a)"/>' for d in arrows)
paths += "".join(f'<path d="{d}" stroke-dasharray="4 4" marker-end="url(#a)"/>' 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'<rect x="{ax}" y="{st}" width="712" height="{h}" rx="8" fill="{CARD}" stroke="{LINE}" stroke-width="1.25"/>'
f'<text x="380" y="{st + 22}" text-anchor="middle" font-weight="600" fill="{INK}">Стор событий</text>'
f'<text x="380" y="{st + 40}" text-anchor="middle" font-size="11.5" fill="{DIM}">'
"подписи ed25519 · проекции · сжатие с архивом · каждый шаг пишет события</text>")
return (
'<figure class="diagram"><svg viewBox="0 0 760 500" role="img" '
'aria-label="Сообщение проходит фильтры до LLM, всё оседает в сторе" font-size="13" '
'font-family="-apple-system,Segoe UI,Roboto,Arial,sans-serif">'
f'<defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" '
f'orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="{DIM}"/></marker></defs>'
f'<text x="24" y="32" font-size="15" font-weight="600" fill="{INK}">'
"Сообщение проходит фильтры до LLM, всё оседает в сторе</text>"
f'<g fill="none" stroke="{DIM}" stroke-width="1.25">{paths}</g>{boxes}{store}</svg>'
"<figcaption>Слева — входы, по центру — фильтры перед LLM по порядку, справа — помощники "
"(пунктир — консультируют, сплошная — вызывает воркер); стор событий — единственный "
"источник правды.</figcaption></figure>")
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<!--DIAGRAM-->\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-->", diagram())
title = re.search(r"^# (.+)$", md, re.M).group(1)
nav = "".join(f'<a href="#{t["id"]}">{t["name"]}</a>'
for t in conv.toc_tokens[0]["children"]) if conv.toc_tokens else ""
return (f'<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8">'
f'<meta name="viewport" content="width=device-width, initial-scale=1.0">'
f"<title>{title}</title><style>{CSS}</style></head><body><div class=\"wrap\">"
f'<nav><div class="kicker">Simbiot Harness</div>{nav}</nav><main>{body}</main>'
"</div></body></html>\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)