sdlc-hub/ARCHITECTURE.md
Andrey Epifancev 9aaf3cee50 Устаревшие ключи настроек удаляет ensure_defaults; выкатка схемы v8 в документах (#104)
I-1, I-2 финального ревью: старые MCP-сессии возвращали status_labels/transitions_map
после миграции; окно до kickstart описано для v8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:22:44 +04:00

33 KiB
Raw Blame History

Архитектура

Хаб моста Jira: ядро + два модуля. jira — SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP; wiki — Markdown-копия вики Eva по проектам, поиск и вложения по запросу. У каждого модуля свой агент на виртуалке (jira-agent, eva-agent): он опрашивает хаб, ходит в свою внешнюю систему и возвращает данные; хаб во внешние системы не ходит.

Ядро и модули

Хаб — ядро (src/sdlc_hub/core/) и подключаемые модули (src/sdlc_hub/modules/<имя>/). Ядро держит механику: базу, очередь команд, HTTP, настройки, реле, выпуск агента, каркас MCP и CLI. Модуль даёт смысл: схему своих таблиц, типы команд, маршруты, настройки, MCP-инструменты, подкоманды. Сейчас модулей два — jira и wiki; следующие (sdlc) подключаются так же, строкой в core/registry.py.

Ядро (core/)

Модуль Ответственность Зависит от
paths каталог данных (SDLC_HUB_HOME), пути базы и backups/, права 0700 (secure_dir) stdlib
errors ошибки, общие для ядра и модулей (CommandError, HttpMapped) stdlib
http общее для обработчиков HTTP: ApiError, Raw, разбор параметров, mapped, not_found errors, projects
modules контракт модуля: Module и CommandHooks (только структуры) stdlib
registry реестр модулей: ленивая сборка, modules(), module(name), module_of(тип_команды), agent_modules(), DEFAULT_AGENT, проверка конфликтов (типы команд, маршруты, ключи настроек, поля проекта) modules; модули — ленивым импортом внутри функции
db connect, схема ядра (setting, batch, command, project, agent_status) и схемы модулей из Module.schema, миграции по schema_version (v8; копия базы перед миграцией; история шагов MIGRATIONS) backup, paths; модули — только внутри шагов истории миграций (ленивый импорт)
settings настройки (setting), токены (клиентский + по агенту на модуль: setting.tokens = {client, agents: {<модуль>: <токен>}}), умолчания модулей (ensure_defaults) registry (база — через conn вызывающего)
projects проекты-метки: таблица project (label, name, created_at), проверка метки, создание и правка (PATCH) с полями модулей, список = поля ядра + project_view модулей db, registry
queue механика очереди: пакеты, выдача (claim), отчёты, отмена, повтор, временные ключи new:<n>; смысл типов команд берёт у модуля через CommandHooks (registry.module_of) backup, db, registry, settings, errors
backup копии файла базы, ротация paths
files хранилище файлов <данные>/files/<sha256> (каталог — параметр, Hub.files_dir): put, touch, sweep, sweep_downloads, path/exists/size/read paths, registry
api HTTP-сервер, авторизация по токенам, фоновый tick, Hub; маршруты ядра CORE_ROUTES (/agent/*, /files, /projects, /settings, раздача /app…) и сводный ROUTES = ядро + маршруты модулей; уборка files/ раз в сутки queue, projects, settings, backup, db, files, relay, release, registry, http, errors, paths
relay реле общей папки (relay_dir): файлы req/ → свой HTTP → resp/; агент с hub_url = folder:<путь> ходит через него settings
release опубликованная версия агента модуля release/<модуль>/agent.py + manifest.json: publish (только вверх), current, load, hint paths
client HTTP-клиент хаба (HubClient, from_home, get_bytes) db, paths, settings (только from_home)
mcp каркас MCP: охрана («хаб выключен» — подсказка), привязка сессии к проекту (PROJECT), общие инструменты projects и project_create, register_core_tools, запуск сервера модуля по stdio client, registry
cli init, serve, mcp <модуль> [--project], tokens, agent publish [--module] и подкоманды модулей (Module.cli_register/cli_run) api, mcp, registry, release, settings, db, paths

Модуль jira (modules/jira/)

Точка входа — modules/jira/__init__.py (MODULE: Module).

Модуль Ответственность Зависит от
schema таблицы модуля: issue, issue_link, issue_comment, issue_attachment, attachment_file, issue_change, issue_note, sync_run и др. stdlib
settings умолчания настроек модуля, ключи для агента, проверка значений, темп (pacing), перенос из config.example.json (init_from_config) core.settings, core.projects, projects
mirror зеркало задач, связей, комментариев и вложений; пишет его только apply_state (с проверками check_comments, check_attachments), разрез по проекту, заметки, фильтры, снимок доски board, карточка get_issue core.db, history, attachments
history журнал изменений issue_change: время Jira → UTC (jira_utc), проверка и запись пачек агента, delivery_param для выдачи refresh, лента feed, история задачи core.db, core.settings, projects
attachments ссылки на вложения в вики-разметке, fmt_size, состояние ready/fetching/failed/none core.files
fetch постановка скачивания вложения (request_fetch, TooLarge, AgentTooOld) core.db, core.queue, attachments, commands
commands контракт типов команд jira (ensure_*, refresh, fetch_attachment), валидация, временные ключи, оценка, подтверждение по состоянию mirror
refresh журнал синхронизаций sync_run, полный прогон, автообновление core.db, core.queue, core.settings, projects
projects базовая JQL из меток (base_jql), доски (boards), миграция v3 → v4 (migrate_v3), проект по умолчанию core.projects, core.settings
hooks CommandHooks для очереди ядра: проверки постановки (bug без настройки, файл вложения, проект пакета, board_id у ensure_sprint), паузы, deliver_params (size, boards, history), отчёты (attachment_file, sync_run) core.queue, core.files, core.projects, core.settings, settings, projects, commands, mirror, history, refresh, attachments, fetch
api маршруты модуля MODULE.routes: /issues, /board, /changes, /attachments, /refresh, /sync-runs и др. core.db, core.files, core.queue, core.settings, core.http, core.errors, attachments, projects, mirror, history, fetch, refresh
mcp инструменты MCP: issues, issue, batch_create, batch_*, comment, attachment, changes, refresh, settings_* и др.; общие projects/project_create подключает register_core_tools core.mcp, core.client, commands, attachments
importer, cli разовый импорт CSV-пакета в заметки (sdlc-hub import FOLDER) mirror, core.db, core.settings

Модуль wiki (modules/wiki/)

Точка входа — modules/wiki/__init__.py (MODULE: Module). Копию страниц хаб хранит файлами wiki/<метка>/ в каталоге данных (Markdown + _tree.json), строки и FTS — в базе.

Модуль Ответственность Зависит от
schema таблицы модуля: wiki_project, wiki_page, wiki_attachment, wiki_sync_stage; таблица FTS5 wiki_page_fts создаётся лениво при первой синхронизации (search.ensure), в schema её нет stdlib
settings умолчания (wiki_pacing, wiki_auto_sync_hours, wiki_min_agent_version), ключи для агента, проверка значений core.settings
paths каталог копий <каталог базы>/wiki/<метка>/ core.db
projects поле проекта wiki_root (изменяемое), project_view (wiki_root, pages_count, synced_at…), on_project_created/on_project_updated (постановка wiki_sync, отмена queued, сброс копии при смене корня) core.projects, core.queue, commands, paths
commands типы wiki_sync и wiki_fetch (оба — чтение Eva, без хвоста и копии базы), CommandHooks модуля: проверки, паузы, before_result/on_result/after_result/abort_result, отбрасывание отчёта по сменённому корню core.queue, stage, sync, attachments
convert HTML Eva → Markdown (markdownify): замена ссылок по ref на attachment:<id> и page:<код>; HTML считается недоверенным stdlib, markdownify
stage приём пачек страниц от агента: проверка формы, перевод вне транзакции, стадия wiki_sync_stage core.db, convert
sync сборка копии из стадии в wiki/.<метка>.new/ (prepare), запись строк и подмена каталога (commit), FTS core.db, paths, search, convert
search поиск по FTS5 (запасной LIKE по index.md, если FTS5 в SQLite нет), фрагменты paths
attachments вложения по запросу: состояние none/fetching/ready/failed, постановка wiki_fetch, хуки результата; адрес берётся только из wiki_attachment core.queue, core.files
tick фоновая синхронизация проектов с корнем раз в wiki_auto_sync_hours core.db, core.settings, commands, sync, search
api маршруты /wiki/tree, /wiki/pages/{code}, /wiki/search, /wiki/sync, /wiki/status, /wiki/attachments/{id}[/fetch|/content] (клиент) и POST /agent/wiki/pages (агент wiki) core.http, core.queue, stage, attachments, search
mcp инструменты сервера sdlc-wiki: projects, project_create, root_set, tree, page, search, attachment, sync, status; Markdown и имена вложений недоверенные core.mcp, core.client

Контракт модуля

core/modules.py задаёт две структуры; ядро знает только их.

  • Module(name, schema, settings_defaults, settings_obsolete, agent_keys, agent_min_version_key, validate_setting, settings_view, on_init, commands, routes, tick, file_refs, project_fields, project_view, on_project_created, on_project_updated, mcp_server, cli_register, cli_run) — всё необязательное, кроме name. Модуль с commands — «вид агента»: у него свой агент, токен, agent_status и релиз.
  • Поля проекта: ядро знает только label, name, created_at. Остальное объявляет модуль: ProjectField(name, validate, editable); значения для GET /projects (в т.ч. вычисляемые) отдаёт project_view(conn, labels); on_project_created(conn, project, fields, author) и on_project_updated(...) пишут свои поля и возвращают пакеты. POST /projects принимает поля всех модулей (неизвестное — 422), PATCH /projects/{label} — только изменяемые (editable). registry.check падает при совпадении имени поля у двух модулей или с полем ядра.
  • CommandHooks — что модуль знает о своих типах команд: types, read_types (не пишут во внешнюю систему: без хвоста и копии базы), validate, check_batch, tail, pause, deliver_params, temp_definer, temp_keys_used, substitute, drop_unresolved, retry_params, check_confirmed, on_claim, on_failed, check_result, on_result, after_result, on_late_result и два необязательных — before_result, abort_result. Очередь вызывает их по типу команды через registry.module_of.
  • before_result(conn, cmd, status, result, files_dir) ядро вызывает в report_result до транзакции отчёта, только для выданной (claimed) команды. Нужен, чтобы тяжёлая подготовка (сборка копии вики, запись файлов) шла вне BEGIN IMMEDIATE и не держала блокировку базы, в которой ждут claim другого агента и веб (рулинг R-6). ValueError из него ядро превращает в RuntimeError (500, агент повторит отчёт), чтобы не спутать с неверным статусом отчёта.
  • abort_result(conn, cmd) ядро вызывает, если отчёт не дошёл до after_result (поздний или идемпотентный выход, исключение в транзакции): модуль снимает подготовку before_result и её блокировку. При успехе подготовку снимает after_result.

registry.check при сборке падает на конфликте: один тип команды у двух модулей, пересечение маршрутов или ключей настроек. Уникальность имён модулей проверяется только в вебе (web/src/modules/modules.test.tsx); в Python реестр её не проверяет.

Веб (web/)

Веб-приложение хаба — React + TypeScript + Vite. Оболочка (web/src/shell/: Layout.tsx, Sidebar.tsx, pages.tsx, тема), общие части (web/src/ui/ — компоненты дизайн-системы AURA, web/src/styles/tokens.css — токены и единственное место с литералами цвета, web/src/api/ — клиент HTTP, web/src/auth/, web/src/projects/ — проект в адресе ?project=<метка>, выпадающий список, окно «Новый проект») и модули web/src/modules/<имя>/. Контракт веб-модуля — HubModule в web/src/modules/types.ts (name, title, pages, nav, Provider, status, Summary, useOverlay, projectFields); список модулей — константа MODULES в web/src/modules/index.ts (хуки вызываются в постоянном порядке).

Состояние модуля — данные, а не компонент: status (ModuleStatus) описывает агента (agent), охват (scope: all/project), свежесть (freshness(project)), действие обновления (refresh: подпись, start, isOwn) и перечитывание после пакета (invalidate). Рисует и ведёт его ядро: трекер пакетов и агентов — web/src/shell/activity/ (ActivityProvider: подхват идущего пакета и GET /agents раз в 60 с, опрос пакета каждые 3 с, useModuleActivity(module), тексты events.ts/agentText.ts), блок состояния панели — ModuleStatusBlock, а модуль даёт только Summary — карточку на «Обзоре» (OverviewPage: карточки модулей и «Последние пакеты проекта»). Адреса: «Обзор» — /app/, страницы модуля — /app/<модуль>/… (/app/jira/board, /app/jira/issues[/{key}], /app/jira/feed, /app/wiki); прежние /app/board, /app/issues, /app/issues/{key}, /app/feed перенаправляются с сохранением search и replace.

Модуль wiki (web/src/modules/wiki/index.tsx): страница «Вики» (/app/wiki): дерево (Tree) с поиском (Search), страница (WikiPage, PageView — react-markdown + remark-gfm, без rehype-raw; ссылки page: ведут на страницу вики, attachment: скачивают файл через download.ts, urlTransform.ts пропускает только эти схемы и http(s)), форма корня без привязки (RootForm), сводка для «Обзора» (Summary) и status для ядра; поле «Корень вики» в окне «Новый проект» — через projectFields.

Модуль jira (web/src/modules/jira/index.tsx): страницы «Доска связей» (/app/jira/board), «Задачи» (/app/jira/issues), «Лента» (/app/jira/feed), карточка /app/jira/issues/{key} окном поверх страницы (issues/overlay.tsx, IssueModal.tsx; оболочка рисует страницу по location.state.background). Внутри: board/ (чистые model, analysis, layout, storage.ts — ключи localStorage прежней страницы, у проекта AWPCA прежние, у остальных с суффиксом @<метка>; отрисовка components/; golden-эталоны __fixtures__/), issues/ (список, карточка, wiki/ — разбор вики-разметки, attachments/ — список, плашки и скачивание: POST /attachments/{id}/fetch, опрос GET /attachments/{id}, затем api.blob на /content), feed/ (период, виды, группировка событий, EventList, общий с блоком «История» карточки), RefreshButton.tsx (кнопка «Обновить из Jira» в шапках страниц), Summary/status для ядра, api/queries.ts.

Собирается make web в src/sdlc_hub/web/app/; core/api отдаёт сборку без токена (/app/assets/* — файлы, прочие /app… — index.html, 503 без сборки), / и /links перенаправляет на /app/ («Обзор»). Данные — только через HTTP API с клиентским токеном; mirror.board отдаёт зеркало без преобразований, перевод в модель доски — в board/model.

Цепочки зависимостей (Python):

  • ядро: api → queue → settings/projects/files/registry → db → backup → paths; api → relay → settings; api → release → paths; files → paths; cli → api/mcp/registry; mcp → client → HTTP API; registry → (лениво) modules.*;
  • модуль wiki: commands → stage/sync/attachments → convert/search/paths → core.*; projects → commands/paths; api → stage/attachments/search → core.http/core.queue; tick → commands/sync;
  • модуль jira: hooks → commands/mirror/history/refresh/attachments/fetch → core.*; mirror → history → projects; mirror → attachments → core.files; api → mirror/history/fetch/refresh → core.http/core.queue; importer → mirror;
  • веб: shell → modules/index → modules/jira, modules/wiki → ui/api/projects (модуль не импортирует оболочку, кроме типов контракта).

Потоки данных

  • Запись в Jira: клиент (MCP/интерфейс) → POST /batches → queue → агент claim → Jira → POST /agent/commands/{id}/result.
  • Вложение: файл → POST /files → files/<sha256> → ensure_attachment {key, file, filename} в пакете (modules/jira/hooks.py (check_batch) проверяет, что файл есть) → claim (хук deliver_params дописывает size, в базе params без него) → агент GET /agent/files/{sha256} → Jira; уборка files/ — из tick раз в сутки.
  • Создание проекта: POST /projects (веб или project_create) → core.projects.create и пакет модуля (Module.on_project_created: «Новый проект <метка>» refresh по общей JQL, batch.project = NULL) в одной транзакции → claim (хаб дописывает в refresh доски проектов, boards) → агент определяет тип доски → отчёт refresh пишет board_type → ensure_sprint проекта получает board_id.
  • Чтение Jira: команда refresh → агент → POST /agent/state → mirror.apply_state.
  • Скачивание вложения: вход — POST /attachments/{id}/fetch (из веб-приложения и из MCP attachment); modules/jira/fetch.request_fetch (одна транзакция: состояние attachments.state + пакет fetch_attachment {key, attachment_id}) → claim (без копии базы) → агент → POST /agent/files → результат {file, size} → report_result проверяет файл в files/ и пишет attachment_file; нет файла или чужой размер — команда failed. Выход — GET /attachments/{id}/content: в вебе Blob и сохранение браузером, в MCP копия downloads/<id>/<имя>.
  • Журнал изменений: выдача refresh (CommandHooks.deliver_params модуля дописывает history — since и/или стартовую jql) → агент v13 после состояния читает changelog Jira → POST /agent/history → history.record_changes (повторы отбрасываются по (history_id, seq)) → отчёт refresh с result.history → sync_run.history_at у полного прогона (новая отметка) → GET /changes, GET /issues/{key}.changes, MCP changes.
  • Синхронизация вики: wiki_sync {label, root} ставят MCP sync, кнопка «Обновить» (POST /wiki/sync), привязка/смена корня (on_project_created/on_project_updated) и tick (раз в wiki_auto_sync_hours) → claim агента wiki → eva-agent обходит дерево и шлёт пачки POST /agent/wiki/pages (хаб принимает ≤50 страниц в пачке; срез по 4 МБ делает агент) → stage.accept переводит HTML в Markdown вне транзакции и кладёт в wiki_sync_stage → отчёт done {root_title, codes} → before_result (sync.prepare: сборка wiki/.<метка>.new/, выделение id вложений, под per-label Lock) → транзакция отчёта (on_result = sync.commit: строки wiki_page/wiki_attachment/FTS, атомарная подмена каталога) → after_result снимает подготовку; при сбое — abort_result. Отчёт по команде с устаревшим корнем отбрасывается (result.discarded), копия прежняя.
  • Вложение вики: POST /wiki/attachments/{id}/fetch (веб, MCP attachment) → attachments.request_fetch (одна транзакция: state = fetching + пакет wiki_fetch {attachment_id, url}) → claim → eva-agent качает файл только с хоста Eva → POST /agent/files → результат {file, size} → хаб проверяет файл в files/ и ставит ready (иначе failed) → GET /wiki/attachments/{id}/content; в MCP копия downloads/wiki-<id>/<имя>.
  • Агент за VPN: агент → файлы в общей папке (relay_dir) → relay → HTTP хаба → ответ файлом.
  • Самообновление агента: sdlc-hub agent publish [--module M] PATH → release/<модуль>/ → claim.agent_release → агент скачивает GET /agent/release; состояние обновления агент шлёт в claim, хаб держит в памяти (вместе с признаком agent_reported — был ли claim с запуска хаба) и отдаёт в GET /agent.
  • Токены: клиентский — всё, кроме /agent/*; по токену на каждый модуль-агент (jira, wiki) — только /agent/*, причём маршруты scope=agent модуля пускают лишь его агента (токен чужого — 401); claim выдаёт агенту только команды его модуля, по одной в полёте на агента; agent_status (ключ — имя агента), релиз release/<модуль>/ и самообновление — по агентам; MCP берёт клиентский токен из базы через HubClient.from_home. Токен Eva в хабе не хранится — только в agent.json eva-agent.

Границы

  • Ядро (core/) не импортирует модули (sdlc_hub.modules.*): исключения — core/registry.py (ленивый импорт внутри функции) и шаги истории миграций в core/db.py (ленивый импорт внутри шага). Проверяет tests/test_registry.py::test_core_imports.
  • Типы команд (ensure_*, refresh, fetch_attachment, wiki_sync, wiki_fetch и др.) и термины модулей (jira: sync_run, issue_attachment, attachment_file, issue_comment, jql, поля досок; wiki: eva, wiki_, wiki/, DOC-) живут только в модулях; ядро знает их лишь через CommandHooks и поля проекта. Проверяют test_core_has_no_jira_terms (исключение — core/db.py, история миграций) и test_core_has_no_module_terms (исключение — core/registry.py). Меток-исключений «долг» нет.
  • Новые имена в ядре — без названий модулей. Модуль не вызывается из ядра иначе как через registry и контракт Module.
  • Копию вики (wiki/<метка>/, wiki_page, FTS) пишет только sync.commit в отчёте wiki_sync (и удаляет при сбросе корня projects._drop_copy в транзакции PATCH); MCP и API её читают. Модуль wiki не обращается к Eva и не хранит токен Eva.
  • Зеркало (включая issue_attachment) пишет только mirror.apply_state по запросу агента (и заметки через PUT /issues/{key}/note); из очереди и API задач напрямую не правят.
  • Журнал issue_change пишет только history.record_changes по POST /agent/history; API и MCP его только читают.
  • Хаб не знает пароль Jira и не обращается к Jira; init не переносит auth, base_url, verify_ssl, ca_bundle.
  • queue не создаёт команд сам: повтор после failed — новый пакет. Выданная без отчёта команда довыдаётся при следующем claim (до 3 выдач, затем failed).
  • MCP (core/mcp, modules/*/mcp) не импортирует queue/db/projects и модульные mirror/history напрямую, ходит только через client; из core.files берёт только предел MAX_SIZE, из attachments — только fmt_size; downloads/ пишет только он (jira — <id>/, wiki — wiki-<id>/) (хаб эту папку лишь убирает).
  • commands, mirror, queue не знают про HTTP; HTTP живёт только в core/api, modules/*/api, client и relay (реле — клиент своего же HTTP).
  • Веб-приложение (web/) ходит в хаб только через HTTP API (web/src/api/client.ts); словари kind_labels, link_types и набор закрытых статусов closed_statuses берёт из GET /settings и своих копий не держит. Модуль веба не импортирует другой модуль; общее — в shell, ui, api, projects.

Выкатка и откат версии с новой схемой (v7, v8)

  • Порядок: git pull → сразу launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub (владелец) → make web → /mcp reconnect. Старый фронт с новым API работает (поля board_id и прочие на месте), поэтому сборку можно отложить.
  • До kickstart не запускать MCP и CLI sdlc-hub (в том числе tokens): HubClient.from_home() и CLI открывают базу через db.connect и мигрируют её, а хаб на прежнем коде с такой базой не работает (v7 — agent_status, токены; v8 — нет колонки issue_note.fact_status: 500 на GET /issues, /board, запись заметки).
  • После kickstart MCP-сессии со старым кодом до reconnect открывают базу своими умолчаниями: ключи, удалённые миграцией, они могут вернуть — их убирает ensure_defaults нового кода (Module.settings_obsolete).
  • Откат на копию v6 из backups/ (*schema-v6*): вместе с базой вернуть публикацию агента release/jira/* в release/ (прежняя раскладка release/agent.py, manifest.json); иначе старый код увидит «публикации нет» и самообновление агента молчит.

Открытые вопросы

  • Правки из UI в Jira (спринт, срок, связи, исполнитель) — change viewer-edit; страницы очереди, агента и настроек — позже.
  • Автоподстройка темпа по фактам 429 не реализована: хаб только показывает их.
  • Запись в вики Eva (правка страниц из хаба) не делается: модуль только читает.
  • История страниц вики и частичная перекачка: сейчас каждая синхронизация перекачивает всё дерево, копия без истории.
  • Переименование проекта-метки и перенос корня вики между проектами не предусмотрены (меняется только корень — PATCH /projects).
  • Ручной просмотр вёрстки веб-страницы «Вики» на живых данных (таблицы, вложенные списки) — за владельцем после выкатки.
  • Хаб переименован в sdlc-hub (24.10.2026, change sdlc-rename): пакет sdlc_hub, команда sdlc-hub, каталог данных ~/.local/share/sdlc-hub/ (SDLC_HUB_HOME), MCP-серверы sdlc-jira и sdlc-wiki, launchd ru.aepif.sdlc-hub.