docs/ARCHITECTURE.html: архитектура харнеса страницей (SVG-схема, оглавление), генератор из ARCHITECTURE.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
74c5443244
commit
3682eb4d67
3 changed files with 741 additions and 1 deletions
611
docs/ARCHITECTURE.html
Normal file
611
docs/ARCHITECTURE.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
# Архитектура Simbiot Harness (Коди)
|
# Архитектура Simbiot Harness (Коди)
|
||||||
|
|
||||||
17.10.2026 · копия документа https://claude.ai/code/artifact/195fd476-44e7-47cc-9093-2d41ea53c8c9 (живая версия — там, со схемой)
|
17.10.2026 · HTML: `docs/ARCHITECTURE.html` (собирается `scripts/build-architecture-html.py`) · онлайн-версия для комментариев: https://claude.ai/code/artifact/195fd476-44e7-47cc-9093-2d41ea53c8c9
|
||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
|
|
||||||
|
|
|
||||||
129
scripts/build-architecture-html.py
Normal file
129
scripts/build-architecture-html.py
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/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)
|
||||||
Loading…
Reference in a new issue