diff --git a/docs/ARCHITECTURE.html b/docs/ARCHITECTURE.html new file mode 100644 index 0000000..4dd7fac --- /dev/null +++ b/docs/ARCHITECTURE.html @@ -0,0 +1,611 @@ +Архитектура Simbiot Harness (Коди)

Архитектура Simbiot Harness (Коди)

+

17.10.2026 · HTML: docs/ARCHITECTURE.html (собирается scripts/build-architecture-html.py) · онлайн-версия для комментариев: https://claude.ai/code/artifact/195fd476-44e7-47cc-9093-2d41ea53c8c9

+

Обзор

+

Simbiot Harness — ритмичный агент кооператива: он живёт тактами (секунда, минута, час, день), а в Telegram отвечает как Коди (@juno42bot). Главная метрика — долгосрочный AUM кооператива: каждое решение проверяется вопросом «добавит ли это ≥1% к AUM на горизонте год+».

+

Архитектура держится на пяти принципах:

+ +

Куб 4×4×4 задаёт поведение одной таблицей политик: 4 круга доверия × 4 тика × 4 глубины памяти. Это не 64 программы, а один код и параметры.

+
Сообщение проходит фильтры до LLM, всё оседает в стореTelegram@juno42bot: личка, группыПортал и агентыMCP, канал агент↔агентЧеловекcli.py, TUITG-поллерприём, файлы, голос, кнопкиАдресациякому сообщение: боту или людямГейт важностиесть ли событие важнее порогаКруг доверия и памятьправа тика, глубина D0–D3LLM-воркеринструменты под стражем правDecision-модельclef:27b → nimbleПамять и ядроиндекс nomic, ученики9router и порталLLM-тики, MCP-вызовыСтор событийподписи ed25519 · проекции · сжатие с архивом · каждый шаг пишет события
Слева — входы, по центру — фильтры перед LLM по порядку, справа — помощники (пунктир — консультируют, сплошная — вызывает воркер); стор событий — единственный источник правды.
+ +

Процессы и сервисы

+

Харнес — два долгоживущих процесса на одном сторе событий плюс короткоживущие LLM-воркеры; код — /root/simbiot-harness, данные инстанса — /var/lib/simbiot-harness (SIMBIOT_HOME).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПроцессЗапускЧто делает
Лестницаsimbiot-harness.service → python -m harness.schedulerтакты sec/min/hour/day в отдельных потоках, демон доставки и сирот, триаж, ночная тренировка ядра
TG-поллерsimbiot-harness-tg.service → python -m harness.tg_inboxlong polling @juno42bot: входящие, файлы, расшифровка голоса, кнопки
LLM-воркерharness/_llm_worker.py, сабпроцесс на каждый LLM-тикагент Strands с инструментами, страж прав, бюджет шагов
Дашбордsimbiot-dashboard.service, HTTP 8765доска задач и тиков, только чтение стора
CLIcli.py (и обёртка simbiot)команды агентов и людей: ответы, задачи, эскалации, ядро, память, доверие
+

Каталоги инстанса (/var/lib/simbiot-harness):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПутьСодержимое
db/harness.dbстор событий + read model + журнал доставок (SQLite, WAL)
env/.envсекреты и флаги (600)
audit/ключ ed25519 харнеса
archive/сжатые события (jsonl.gz, подписаны)
core/чемпионы учеников ядра + кеш эмбеддингов
memory/индекс памяти Коди
inbox/файлы из чатов
+

Внешние зависимости:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
СистемаЗачем
9router (127.0.0.1:20128)LLM для тиков, модель auto
Ollama Clouddecision-модель clef:27b
Ollama локально (127.0.0.1:11434)запасная decision-модель nimble, эмбеддинги nomic-embed-text
Портал cil.by (MCP)звонки, реестры, канал агент↔агент
Telegram Bot APIчаты Коди
Deepgramрасшифровка голосовых
RAG /root/ragпоиск по базе знаний кооператива
+

Event Sourcing и криптоаудит

+

Всё состояние харнеса выводится из подписанной цепочки событий (harness/es.py, harness/audit.py); после сжатия 17.10 стор — 200 MB, полная проверка — около 30 с.

+

Стор. Таблица events (aggregate, aggregate_id, event_type, data, timestamp, version, sig, key_id, prev_sig). Уникальность (aggregate, aggregate_id, version) делает запись идемпотентной: тик level:seq не произойдёт дважды. Клейм задачи — атомарный (claim_task_if_ready, BEGIN IMMEDIATE), поэтому два уровня не возьмут одну задачу.

+

Проекции. run_projections() инкрементально строит read model (задачи, инбокс, эскалации, дайджесты, круги доверия, политика AUM) и сохраняет снапшот в read_model. Перед применением хвост проверяется подписями: подделка останавливает проекции.

+

Подписи. Каждое событие подписано ключом харнеса (ed25519, key_id faf8d19d9aed71fe), prev_sig ссылается на подпись более раннего. События до внедрения подписей помечены genesis:<id>, неподписанные — legacy-границы. Защита от чужого ключа: стор, уже подписанный одним ключом, другим не подписывается. Полная проверка — cli.py verify.

+

Сжатие с подписью (harness/compact.py, scripts/compact-store.sh). Шум сек-уровня старше часа уходит в архив:

+
    +
  1. Строки целиком, со всеми подписями, пишутся в archive/events-<first>-<last>-<id>.jsonl.gz (600).
  2. +
  3. Подписи удалённых строк, на которые ссылаются остающиеся, и маркеры boundary:<id> для legacy-границ попадают в мост compaction_bridge.
  4. +
  5. Подписанное событие StoreCompacted фиксирует sha256 архива, строк и моста.
  6. +
  7. Удаление идёт пачками по 20 000 строк без остановки сервисов; VACUUM — под остановленными писателями (секунды).
  8. +
+

verify принимает мост, только если его хеш совпадает с подписанным StoreCompacted; архивы проверяются построчно (cli.py compact --verify-archives). Первое сжатие: 2 886 765 строк в архив (246 MB), 1389 → 200 MB.

+

Сек-уровень больше не шумит: CycleTicked/ReflexObserved пишутся heartbeat'ом раз в 60 с и в такты с событием (алерт, команды, каналы).

+

Тактовая лестница

+

Четыре уровня тикают независимо, каждый в своём потоке (harness/scheduler.py, harness/levels.py); дорогой LLM-тик запускается, только если есть событие важнее порога уровня.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
УровеньИнтервалLLMПорог гейтаЗадачиШагов на тикТаймаутПамять по умолчанию
sec1 снет—————
min1 мин + по входящемуeffort low, контекст 256kcriticalS30480 сD1
hour1 чeffort medium, 512kwarnS, M, L3002880 сD2
day1 дeffort high, 1MwarnS, M, L10 000таймаут уровняD3
+

sec — рефлекс без LLM. Телеметрия и алерт нагрузки, мгновенные команды бота (/лестница, /статус, /задачи), каналы портала и агент↔агент.

+

Гейт важности. Сигналы: входящее без ответа и готовая задача — critical; эскалации — по их severity; высокая нагрузка — warn. Ниже порога тик закрывается детерминированно (GateSkipped + заглушка-дайджест). Серая зона (только warn/info) спрашивает decision-модель — сейчас в режиме shadow.

+

Тик по входящему. Демон раз в секунду смотрит хвост стора; через 3 с тишины после MessageReceived или MessageTranscribed запускает внеочередной мин-тик в слот следующей минуты. Ответ в Telegram приходит за 10–40 с вместо ~80 с медианы до нуджа.

+

Батч задач. Уровень берёт все ready-задачи своих классов в бюджете контекста: из 9 старейших первой ставится самая ценная для AUM (decision-модель), остальные — FIFO. В один батч попадают задачи одного круга доверия, и уровень берёт только обслуживаемые круги.

+

Эскалация недоделанных. «Бюджет исчерпан» или смерть воркера возвращают задачу в очередь; после 2 захватов на уровне она растёт до размера, которого этот уровень не берёт (S → M, к часу). До этого S-задача могла браться минутным уровнем 33 раза подряд. Задачи про внешние системы (портал, сервер, деплой, UI, git) сразу уходят часу — правило головы task_fit.

+

Занятый уровень поглощает тик (TickAbsorbed), запущенная задача не прерывается, наверх идут только дайджесты и эскалации (cli.py escalate). Демон доставки возвращает задачи-сироты после рестартов и доставляет результаты в чат идемпотентно (журнал доставок).

+

LLM-воркер

+

Каждый LLM-тик — отдельный процесс (isolated_runner.py → _llm_worker.py) со свежим asyncio-рантаймом Strands: падение или таймаут не роняют лестницу, а вся группа процессов добивается по таймауту уровня.

+

Вход воркера (payload): мандат уровня, собранный контекст тика, модель и effort, флаг shell, круг доверия тика. Результат возвращается JSON-файлом; трассу (InvocationStarted, ToolCalled, ToolResult, BudgetSpent) воркер пишет в стор сам и инкрементально, чтобы таймаут не съел учёт расходов.

+

Инструменты:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ИнструментОграничение
shell, read, write, editтолько уровни с can_shell; в кругах ≥ 2 — страж прав
rag_lookup1 запрос на тик, k ≤ 5, таймаут 20 с
portal_mcp4 вызова на тик, деньги-тулы запрещены по умолчанию
cli.py … через shellосновной способ действовать: tg-reply, task, complete, escalate, tg-deliver-file, decide
+

Хуки на каждом вызове (BeforeToolCallEvent, до исполнения):

+
    +
  1. Страж прав — trust.check_tool: запрещённый вызов отменяется (cancel_tool) и пишется TrustDenied.
  2. +
  3. Бюджет шагов — сверх лимита уровня вызовы отменяются, цикл завершается итоговым текстом; задачи без complete возвращаются или эскалируются.
  4. +
  5. Трасса — ToolCalled/ToolResult с trace_id, уровнем и длительностью.
  6. +
+

TTFT-сторож. Если апстрим 9router принял запрос, но за 45 с не прислал первый токен, стрим закрывается и делается новый вызов (до 3 попыток). Начавший отвечать стрим не трогается.

+

Окружение. SIMBIOT_TICK_CIRCLE наследуют shell и cli.py: задачи из тика штампуются его кругом, а cli.py trust set из тика отказывает.

+

Telegram-канал

+

Коди в Telegram — бот @juno42bot (id 7245454391); поллер пишет входящие в стор, а отвечает минутный (реже часовой) тик через cli.py tg-reply.

+

Приём (harness/tg_inbox.py). Long polling (message, edited_message, callback_query). Отправитель вне SIMBIOT_TG_ALLOWED → MessageIgnored(not_allowed). Документы и фото сразу сохраняются на диск, голосовые расшифровываются. Сохраняется, на чьё сообщение сделан reply и упомянут ли бот. Правка уже принятого сообщения не становится новым входящим.

+

Адресация в группах (harness/addressing.py) — до гейта, без LLM:

+
    +
  1. Личка, портал, агенты — всегда адресовано.
  2. +
  3. В группе «да» без модели: @juno42bot, reply на сообщение бота, звательное обращение («Коди, …», «…, Коди», «Коди!»).
  4. +
  5. «Нет» без модели: reply на сообщение другого человека.
  6. +
  7. Серая зона — decision-модель: «кому адресовано — боту, человеку или группе», P(бот) ≥ 0.5. Разговор О Коди в третьем лице обращением не считается. Модель недоступна — бот молчит.
  8. +
+

Вердикт: MessageTriaged{addressed} + 👀 или MessageIgnored(not_addressed) — тогда ни один уровень его не видит. В тени рядом работает ученик ядра addressing.

+

Ответы. В группе — reply'ем на последнее сообщение батча (allow_sending_without_reply), в личке — без цитаты. Сводный ответ на несколько сообщений — tg-reply m-1,m-2 -c. Правило «сначала ответ, потом работа»: если нужно больше ~30 с, сначала короткий ответ, итог — tg-reply <id> --followup в тот же чат (идемпотентно по тексту).

+

Реакции. 👀 ставится при приёме в личке и триажем в группе — только на адресованное боту. Параметр Bot API — reaction (массив); до 17.10 ошибка в имени параметра делала реакцию невидимой.

+

Журнал исходящих. Каждое сообщение бота → OutboundMessage (chat:message_id → полный текст). Reply на сообщение бота получает reply_to_full (до 4000 символов вместо 300 от Telegram), а тик видит блок «ЭТО ОТВЕТ НА ТВОЁ СООБЩЕНИЕ». Собственные рассылки бота за 48 ч попадают в память тика.

+

Кнопки. core:… — «Это было мне?» для ученика ядра; trust:… — «Разрешить полные права», нажать может только владелец.

+

Decision-модель

+

Решения «выбрать один из нескольких» и «да/нет» принимает не LLM, а decision-модель Ollama /v1/systemone (harness/decision.py): облачная clef:27b отвечает за 0.6–0.9 с, запасная локальная nimble (qwen3.5-9B Q8_0, CPU) — за 10–30 с.

+

Контракт. choice — варианты объектом {ключ: описание}, ответ — ключ и распределение вероятностей; noul — P(«да»). Шортлист строго ≤ 9 вариантов: по замерам systemone-bench точность падает со 100% на 6 до 50% на 10. confidence — концентрация распределения, а не вероятность правильности.

+

Надёжность. Бэкенды опрашиваются по порядку, у каждого свой предохранитель: облако — таймаут 30 с, после сбоя пауза 60 с; локальная — 90 с и 5 минут. Ответы детерминированы, поэтому кешируются на час по хешу вопроса и состояния. Каждый реальный вызов → DecisionMade (бэкенд, модель, латентность, применено ли). Модель недоступна — вызывающий берёт свой детерминированный фолбэк.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПотребительВопросРежимФолбэк
адресация в группахкому адресовано сообщениеonмолчать
порядок батча задачкакая из 9 старейших ценнее для AUMonFIFO
тема инициативычто сейчас полезнее предложитьonпо кругу
серая зона гейтанужен ли дорогой LLM-тикshadowпорог
агент в тикеcli.py decide -q … -o ключ=описаниеon—
+

Режимы: SIMBIOT_DECISION=off|shadow|on, точечно SIMBIOT_DECISION_<ПОТРЕБИТЕЛЬ>; ключ облака — SIMBIOT_DECISION_API_KEY в .env инстанса. Статус и статистика — cli.py decide, cli.py decide --stats.

+

Ядро Коди

+

Ядро (harness/kodi_core.py) — цикл самообучения по образцу systemone-bench (engine42/router42): учитель и правила решают, маленький ученик учится на их решениях и реальных исходах и получает право решать сам только после честной проверки. Сейчас у ядра три головы.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ГоловаРешениеМеткиРежимСостояние на 17.10
addressingотвечать ли в группеучитель (вес 1), реакция чата (3), кнопки владельца (5)shadowчемпион на 262 примерах
task_fitвлезет ли S-задача в мин-тикисходы задач из историиon (правило «внешние системы»)ML-чемпион снят: не бил базу; действует правило (точность 0.74)
depthнужна ли глубокая памятьрешения правил (1), нехватка памяти по трассе (3)shadowкопит данные, нужно 40 примеров
+

Ученик. Признаки — эмбеддинг nomic-embed-text (локально, ~70 мс, с дисковым кешем) плюс флаги головы. Модель — логистическая регрессия на numpy (Adam, L2): на сотнях примеров она устойчивее MLP. kNN-память даёт «новизну»; её пороги калибруются при обучении.

+

Роутер. Знакомая ситуация и |p − 0.5| ≥ 0.4 → решает ученик; незнакомая и учитель сомневается → вопрос владельцу кнопками (≤ 5 в сутки); иначе — учитель. В shadow ученик только пишет CorePrediction.

+

Честный допуск (ночная тренировка в дневной тик, cli.py core train):

+
    +
  1. Чемпион помнит trained_upto; с претендентом сравнивается только на свежих данных (≥ 10), которых он не видел.
  2. +
  3. Претендент обязан бить базовую линию — константу, равную доле класса.
  4. +
  5. Чемпион, который на свежих данных хуже базы, снимается (CoreModelRetired) — ученик молчит.
  6. +
  7. Смена чемпиона — подписанное CoreModelPromoted с sha256 файла; подменённый файл не грузится; откат — cli.py core rollback --head <голова>.
  8. +
+

Рефлексия. Раз в сутки: ≥ 3 расхождений адресации с метками человека/чата или ≥ 3 S-задач, не влезших в мин-тик, → задача часу с автодоставкой: разобрать случаи и предложить владельцу правило. Первая рефлексия (17.10) привела к правилу task_fit «внешние системы → сразу к часу».

+

Недельный отчёт. По понедельникам владельцу в личку, без LLM, идемпотентно по неделе (cli.py core report).

+

Куб 4×4×4: круги доверия

+

Поведение задаёт одна таблица POLICY (harness/trust.py): круг доверия определяет инструменты, уровни времени и потолок глубины памяти. Вторая ось — тики (sec/min/hour/day), третья — глубина D0–D3.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
КругКтоShellФайлыПортал MCPУровниПамять до
1 владелецAlex, локальный терминал, система (тик без внешних входов)полныйполный доступвсё (деньги — по флагу)всеD3
2 правлениеVlad, Yuracli.py: ответы, задачи, эскалации, доставка файлов, обзор доскичтение docs/out/sites/artifacts и присланногозвонки, реестры, фичи; без казны и member_createmin, hour, dayD3
3 своиучастники портала, MCP-агенты, партнёрыcli.py: ответы, задачи, эскалациинетзапись на звонок, проекты, фичиmin, hourD2
4 остальныепрочие Telegram-пользователиcli.py: только ответ и эскалациянетнетminD1
+

Как определяется круг. Только по проверенной личности: Telegram user_id, канал MCP-агента или портала, локальный терминал. Текст сообщения прав не даёт. Меняет круг только человек на сервере (cli.py trust set <субъект> <1-4>) — из LLM-тика это запрещено и стражем, и самим CLI.

+

Круг тика = самый недоверенный вход его контекста (входящие + задачи батча). Задачи наследуют круг тика, в котором поданы: просьба правления не исполняется позже часовым уровнем с правами владельца.

+

Страж проверяет каждый вызов инструмента до исполнения. Shell в кругах 2–4 — одна команда cli.py из белого списка круга, без ; && | > $() и обратных кавычек; файлы с секретами закрыты всегда. Задача правления упёрлась в права → владельцу приходят кнопки «Разрешить / Нет»; «Разрешить» даёт задаче круг 1.

+

Вживую проверено на реальном воркере в круге 2: cat /etc/hostname отменён до исполнения, cli.py decide --stats выполнился.

+

Динамическая память D0–D3

+

Перед каждым LLM-тиком харнес выбирает, сколько собственной истории подгрузить (harness/memory.py, SIMBIOT_MEMORY=on); индекс на 17.10 — 1650 разговоров, 270 итогов задач, 91 дайджест.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ГлубинаЧто в контекстеКогда
D0только текущие входящиевсе реплики односложные: «ок», «спасибо»
D1+ 12 последних реплик чата + свои сообщения за 48 чминутный тик по умолчанию
D2+ до 8 эпизодов из своей истории по смыслу (≤ 5000 символов)час; «помнишь», «вчера», «как договаривались»; задачи в батче; reply на сообщение бота старше 48 ч
D3+ до 15 записей архива (≤ 12 000 символов)день; «за месяц», «что ты делал», «итоги», «план на»
+

Потолок задаёт круг: «свои» — до D2, «остальные» — до D1.

+

Индекс (<home>/memory/index.sqlite) инкрементально догоняет стор перед поиском (~3.5 с, только чтение):

+ +

Приватность. У каждой записи — круг: сказанное в чате видно всем его участникам, поэтому круг записи = самый недоверенный участник чата (личка владельца — 1, «Правление» — 2); задача — её круг; дайджест — 1. Поиск отдаёт только записи с кругом ≥ круга тика: тик с сообщением правления не поднимет личку владельца. Круги читаются прямо из событий TrustSet, чтобы отставшая проекция не дала записи слишком широкий круг.

+

Релевантность. Эмбеддинги nomic-embed-text (search_document / search_query). Калибровка на прод-индексе: релевантное — 0.78–0.88, посторонний запрос — до 0.75. Порог 0.76 и окно 0.07 от лучшего совпадения; свежий разговор того же чата (уже в D1) не дублируется.

+

Обучение выбора. Решение о глубине → DepthChosen с причинами и признаками нехватки памяти по трассе: агент полез в базу знаний или в свой стор, ответил «не помню». Это сырьё для головы ядра depth.

+

Конфигурация, CLI, деплой и тесты

+

Настройки читаются из env процесса, затем из /var/lib/simbiot-harness/env/.env; новые подсистемы по умолчанию выключены или в shadow, а на проде включаются строкой в .env.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПеременнаяПродЧто делает
SIMBIOT_DECISIONondecision-модель (гейт — shadow)
SIMBIOT_DECISION_API_KEYзаданоблако clef:27b, запасная — локальная nimble
SIMBIOT_MEMORYonиндекс памяти и блоки D2/D3
SIMBIOT_CORE_ADDRESSINGshadowученик адресации
SIMBIOT_CORE_TASK_FITonсразу к часу задачи про внешние системы
SIMBIOT_CORE_DEPTHshadowученик глубины памяти
SIMBIOT_TG_ALLOWEDAlex, Yura, Vladкто вообще обрабатывается
SIMBIOT_MIN_NUDGEonтик по входящему
SIMBIOT_TASK_ATTEMPTS_PER_LEVEL2попыток до эскалации размера
SIMBIOT_MAX_TOOL_CALLS_<УРОВЕНЬ>min 30, hour 300бюджет шагов тика
+

CLI (cli.py, для людей и агентов в тике):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ГруппаКоманды
чатtg-reply [--followup], tg-notify -k, tg-deliver-file, tg-list, tg-seen
задачиtask, tasks, complete, block, promote, requeue, close
уровниladder, status, escalate, escalations, escalation-ack
решенияdecide, decide --stats
ядроcore status / train / backfill / feedback / reflect / report / rollback / retire [--head]
памятьmemory sync [--all] / stats / search "…" --circle N
довериеtrust list, trust set <субъект> <1-4> (только человек)
сторevents, verify, compact [--dry-run / --vacuum / --verify-archives]
+

Деплой. Код — ветка master в git.cil.by/symbiot/simbiot-harness. Перезапуск лестницы — только scripts/safe-restart.sh: он ждёт, пока закончится идущий LLM-воркер, чтобы задача не застряла в running. TG-поллер перезапускается напрямую: Telegram копит апдейты. Сжатие стора — scripts/compact-store.sh.

+

Тесты. tests/test_*.py — скрипты (./venv/bin/python tests/<имя>.py), каждый со своим временным стором (SIMBIOT_DB). Внешние системы подменены: фейковый /v1/systemone, эмбеддинги «мешок слов», заглушки Telegram. Перед каждым коммитом гоняется полный набор и сравнивается с предыдущим прогоном. Несколько старых тестов (test_tg_commands, test_reply_routing, test_portal_mcp, test_tool_budget) падают и на чистом коде — они зависят от живого состояния.

+

Известные ограничения и дорожная карта

+

Главное открытое место — косвенные каналы между кругами и автономность часового уровня в круге 1.

+

Ограничения:

+ +

Дальше:

+
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7ee3fd2..f6b9ea1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Архитектура 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 ## Обзор diff --git a/scripts/build-architecture-html.py b/scripts/build-architecture-html.py new file mode 100644 index 0000000..144606f --- /dev/null +++ b/scripts/build-architecture-html.py @@ -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'' + 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)