sdlc-hub/ARCHITECTURE.md
2026-10-23 23:33:17 +04:00

121 lines
21 KiB
Markdown
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.

# Архитектура
Хаб моста Jira: ядро + модули; сейчас один модуль — `jira`: SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает.
## Ядро и модули
Хаб — ядро (`src/jira_hub/core/`) и подключаемые модули (`src/jira_hub/modules/<имя>/`). Ядро держит механику: базу,
очередь команд, HTTP, настройки, реле, выпуск агента, каркас MCP и CLI. Модуль даёт смысл: схему своих таблиц, типы
команд, маршруты, настройки, MCP-инструменты, подкоманды. Сейчас модуль один — `jira`; следующие (wiki, sdlc) подключаются
так же, строкой в `core/registry.py`.
### Ядро (`core/`)
| Модуль | Ответственность | Зависит от |
|---|---|---|
| `paths` | каталог данных (`JIRA_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(тип_команды)`, проверка конфликтов (типы команд, маршруты, ключи настроек) | `modules`; модули — ленивым импортом внутри функции |
| `db` | `connect`, схема ядра (`setting`, `batch`, `command`, `project`, `agent_status`) и схемы модулей из `Module.schema`, миграции по `schema_version` (v6; копия базы перед миграцией; история шагов `MIGRATIONS`) | `backup`, `paths`; модули — только внутри шагов истории миграций (ленивый импорт) |
| `settings` | настройки (`setting`), токены, умолчания модулей (`ensure_defaults`) | `registry` (база — через `conn` вызывающего) |
| `projects` | проекты-метки: таблица `project`, проверка метки, создание, список (долг: `board_id`, `board_type`, `issues_count` — см. «Границы») | `db` |
| `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.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-пакета в заметки (`jira-hub import FOLDER`) | `mirror`, `core.db`, `core.settings` |
### Контракт модуля
`core/modules.py` задаёт две структуры; ядро знает только их.
- `Module(name, schema, settings_defaults, agent_keys, validate_setting, settings_view, on_init, commands, routes, tick, file_refs, on_project_created, mcp_server, cli_register, cli_run)` — всё необязательное, кроме `name`.
- `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`. Очередь вызывает их по типу команды через `registry.module_of`.
`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` (`pages`, `nav`, `Provider`, `SidebarStatus`, `TopbarNotice`, `useOverlay`, `useOnProjectCreated`);
список модулей — константа `MODULES` в `web/src/modules/index.ts` (хуки вызываются в постоянном порядке).
Модуль `jira` (`web/src/modules/jira/index.tsx`): страницы «Доска связей» (`/app/board`), «Задачи» (`/app/issues`), «Лента»
(`/app/feed`), карточка `/app/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`, общий с блоком «История» карточки), `refresh/` (кнопка и плашка
обновления данных из Jira в боковой панели, отслеживание пакета: `RefreshProvider`, `useJiraRefresh`), `api/queries.ts`.
Собирается `make web` в `src/jira_hub/web/app/`; `core/api` отдаёт сборку без токена (`/app/assets/*` — файлы, прочие
`/app…` — `index.html`, 503 без сборки), `/` и `/links` перенаправляет на `/app/board`. Данные — только через 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.*`;
- модуль 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 → 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`.
- Агент за VPN: агент → файлы в общей папке (`relay_dir`) → `relay` → HTTP хаба → ответ файлом.
- Самообновление агента: `jira-hub agent publish` → `release/` → `claim.agent_release` → агент скачивает `GET /agent/release`; состояние обновления агент шлёт в `claim`, хаб держит в памяти (вместе с признаком `agent_reported` — был ли `claim` с запуска хаба) и отдаёт в `GET /agent`.
- Токены: агентский — только `/agent/*`, клиентский — остальное; MCP берёт клиентский токен из базы через `HubClient.from_home`.
## Границы
- Ядро (`core/`) не импортирует модули (`jira_hub.modules.*`): исключения — `core/registry.py` (ленивый импорт внутри функции) и шаги истории миграций в `core/db.py` (ленивый импорт внутри шага). Проверяет `tests/test_registry.py::test_core_imports`.
- Типы команд (`ensure_*`, `refresh`, `fetch_attachment` и др.) и термины jira (`sync_run`, `issue_attachment`, `attachment_file`, `issue_comment`, `jql`) живут только в модулях; ядро знает их лишь через `CommandHooks`. Проверяет `test_core_has_no_jira_terms` (исключения — `core/db.py`, история миграций, и строки с меткой `долг hub-modules`).
- Новые имена в ядре — без «jira». Модуль не вызывается из ядра иначе как через `registry` и контракт `Module`.
- Долг проектов (change wiki): проект-метка пока устроен под jira — `board_id`, `board_type` и проверка `board_id` в `core/projects.py`, `issues_count` в списке проектов и `projects.create(..., board_id)` в HTTP-слое (`core/api.py`: `h_projects_get`, `h_projects_post`), `project_create.board_id` и `fmt_projects` в `core/mcp.py`, поле доски в `web/src/projects/NewProjectDialog.tsx`. Места помечены комментарием `долг hub-modules: проекты → модуль jira (change wiki)`; убирать их можно только вместе с переносом проектов в модуль.
- Зеркало (включая `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/jira/mcp`) не импортирует `queue`/`db`/`projects` и модульные `mirror`/`history` напрямую, ходит только через `client`; из `core.files` берёт только предел `MAX_SIZE`, из `attachments` — только `fmt_size`; `downloads/` пишет только он (хаб эту папку лишь убирает).
- `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`.
## Открытые вопросы
- Правки из UI в Jira (спринт, срок, связи, исполнитель) — change `viewer-edit`; страницы очереди, агента и настроек — позже.
- Автоподстройка темпа по фактам 429 не реализована: хаб только показывает их.
- Переименование хаба в sdlc-hub (модули jira + wiki, eva-agent) — после перестройки кода; имена пакета, команды и MCP-сервера пока `jira-hub`.