docs: ядро и модули хаба; тест границы ядра (#87)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
93007f471b
commit
2673bac5d8
134
ARCHITECTURE.md
134
ARCHITECTURE.md
@ -1,55 +1,99 @@
|
||||
# Архитектура
|
||||
|
||||
Хаб моста Jira: SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает.
|
||||
Хаб моста 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 |
|
||||
| `db` | схема SQLite (v6: + `issue_attachment`, `attachment_file`), миграции по `schema_version` (копия базы перед миграцией), `connect`, время Jira → UTC (`jira_utc`) | `backup`, `paths`, `projects` (ленивый импорт в миграции v3→v4) |
|
||||
| `settings` | настройки (`setting`), токены, темп, перенос из `config.example.json` | `db` |
|
||||
| `projects` | проекты-метки: таблица `project` (метка, название, `board_id`, `board_type`), проверка метки, базовая JQL из меток проектов (`base_jql`), доски (`boards`), миграция схемы v3→v4 (`migrate_v3`) | `db`, `settings` |
|
||||
| `attachments` | вложения для интерфейсов: ссылки на них в вики-разметке текста (`ref_names`, `resolve`), формат размера (`fmt_size`), состояние `ready`/`fetching`/`failed`/`none` по `attachment_file`, хранилищу и последней `fetch_attachment` (`state`, `get`) | `files` |
|
||||
| `mirror` | зеркало задач, связей, комментариев (время комментария в UTC — `created_at`) и списка вложений `issue_attachment` (схема v6; `check_attachments`, пишет `apply_state`; `attachment_file` ему не принадлежит), разрез по проекту (свои, соседи по связям, эпики своих), `apply_state` (с проверками `check_comments`, `check_attachments`), заметки, фильтры, снимок доски `board`, карточка `get_issue` (с историей задачи из `history`) | `db`, `history`, `attachments` |
|
||||
| `history` | журнал изменений задач `issue_change` (схема v5): проверка и запись пачек агента (`check_changes`, `record_changes`, логическое имя поля `logical_field`), параметр `history` для выдачи `refresh` (`delivery_param`: отметка — последний полный прогон с `sync_run.history_at`, минус 15 минут; стартовая JQL `backfill_jql`), признак принятого журнала `accepted`, лента `feed` (журнал + новые комментарии по `issue_comment.created_at`) и история задачи `issue_changes` | `db`, `settings`, `projects` |
|
||||
| `commands` | контракт типов команд, валидация, временные ключи, оценка, подтверждение по состоянию | `mirror` |
|
||||
| `queue` | пакеты, проверки постановки (`_check_setup`: `bug` без настройки, файл вложения; `_check_project`: проверки пакета проекта — метка, чужие метки, задачи вне проекта; `_assign_boards`: `board_id` проекта у `ensure_sprint`), выдача (`claim`, `size` у `ensure_attachment` и `boards` у `refresh` — `deliver_params`), отчёты (для `fetch_attachment` в `done` — проверка файла в хранилище и запись `attachment_file`), `request_fetch` (постановка скачивания вложения в одной транзакции, `TooLarge`, `AgentTooOld`; пакет без хвостового `refresh` и без резервной копии), отмена, повтор, автообновление | `commands`, `mirror`, `settings`, `files`, `projects`, `attachments`, `db` |
|
||||
| `errors` | ошибки, общие для ядра и модулей (`CommandError`, `HttpMapped`) | stdlib |
|
||||
| `http` | общее для обработчиков HTTP: `ApiError`, `Raw`, разбор параметров, `mapped`, `not_found` | `errors` |
|
||||
| `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`), время Jira → UTC (`jira_utc`) | `backup`, `paths`; модули — только внутри шагов истории миграций (ленивый импорт) |
|
||||
| `settings` | настройки (`setting`), токены, темп, умолчания модулей (`ensure_defaults`), перенос из `config.example.json` | `db`, `registry` |
|
||||
| `projects` | проекты-метки: таблица `project`, проверка метки, создание, список (долг: `board_id`, `board_type`, `issues_count` — см. «Границы») | `db`, `settings` |
|
||||
| `queue` | механика очереди: пакеты, выдача (`claim`), отчёты, отмена, повтор, автообновление, временные ключи `new:<n>`; смысл типов команд берёт у модуля через `CommandHooks` (`registry.module_of`) | `registry`, `settings`, `files`, `projects`, `db` |
|
||||
| `backup` | копии файла базы, ротация | `paths` |
|
||||
| `files` | хранилище файлов вложений `<данные>/files/<sha256>` (каталог — параметр, `Hub.files_dir`): `put` (атомарно, одна копия на содержимое, mtime при повторе, предел 10 МБ, пустой — отказ, кроме `allow_empty` для файла от агента), `touch` (mtime при выдаче содержимого), `sweep_downloads` (каталоги копий MCP `downloads/<id>/`, старше 30 дней), `path`/`exists`/`size`/`read` (только sha из 64 hex нижнего регистра), `sweep` (старше 30 дней, кроме файлов `ensure_attachment` в `queued`/`claimed`) | `paths` |
|
||||
| `api` | HTTP-сервер, маршруты, авторизация по токенам, фоновый `tick` (автообновление и уборка `files/` раз в сутки); маршруты файлов `POST /files`, `GET /agent/files/{sha256}`, `POST /agent/files` (от агента, пустой допустим; каталог `Hub.files_dir`, 0700 при старте); вложения `GET /attachments/{id}`, `POST /attachments/{id}/fetch` (`queue.request_fetch`: `TooLarge` → 413, `AgentTooOld` → 409, нет → 404 `no_attachment`), `GET /attachments/{id}/content` (`Content-Disposition: filename*=UTF-8''…`, `nosniff`, `files.touch`); уборка раз в сутки трогает и `Hub.downloads_dir`; раздача веб-приложения `/app…` (каталог `web/app` пакета, без токена) и перенаправление `/`, `/links`; снимок `GET /board`; маршруты `/projects` | `queue`, `mirror`, `projects`, `settings`, `backup`, `files`, `attachments`, `relay`, `release` |
|
||||
| `relay` | реле общей папки (настройка `relay_dir`): поток в `serve`, файлы `req/` → свой HTTP (адрес `serve`, для `0.0.0.0` — `127.0.0.1`) → `resp/`; агент с `hub_url = folder:<путь>` ходит через него; HTTP-обработчики о нём не знают | `settings` |
|
||||
| `release` | опубликованная версия агента: `release/agent.py` + `manifest.json` в каталоге данных, `publish` (только вверх; запись с fsync, прежний файл на время записи — `agent.py.prev`, при сбое возвращается), `current` (проверка манифеста, предупреждение один раз на ошибку), `load` (с проверкой sha256 и UTF-8), подсказка `hint` для `agent_status` | `paths` |
|
||||
| `client` | HTTP-клиент хаба | `db`, `paths`, `settings` (только для `from_home`) |
|
||||
| `mcp_server` | инструменты MCP по stdio; привязка сессии к проекту (`jira-hub mcp --project <метка>`), инструменты `projects` и `project_create` (фрагмент `.mcp.json`), лента `changes` (`GET /changes`, по умолчанию — с начала суток); `batch_create` читает файлы `ensure_attachment.path` и загружает их `POST /files` до постановки пакета; `issue`/`comment` показывают вложения, инструмент `attachment` ставит скачивание (`POST /attachments/{id}/fetch`), опрашивает `GET /attachments/{id}` и кладёт копию в `<данные>/downloads/<id>/<имя>` (0700, через временный файл) | `client` (`get_bytes` — тело ответа как есть), `commands`, `paths`, `files` (только `MAX_SIZE`), `attachments` (только `fmt_size`) |
|
||||
| `importer` | разовый импорт CSV-пакета в заметки, сверка с зеркалом | `mirror`, `settings` |
|
||||
| `cli` | `init`, `serve`, `import`, `mcp`, `tokens`, `agent publish` | `api`, `mcp_server`, `importer`, `release`, `settings`, `db`, `paths` |
|
||||
| `files` | хранилище файлов `<данные>/files/<sha256>` (каталог — параметр, `Hub.files_dir`): `put`, `touch`, `sweep`, `sweep_downloads`, `path`/`exists`/`size`/`read` | `paths` |
|
||||
| `api` | HTTP-сервер, авторизация по токенам, фоновый `tick`, `Hub`; маршруты ядра `CORE_ROUTES` (`/agent/*`, `/files`, `/projects`, `/settings`, раздача `/app…`) и сводный `ROUTES` = ядро + маршруты модулей; уборка `files/` раз в сутки | `queue`, `projects`, `settings`, `backup`, `files`, `relay`, `release`, `registry`, `http` |
|
||||
| `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`, `paths`, `registry` |
|
||||
| `cli` | `init`, `serve`, `mcp [модуль] [--project]`, `tokens`, `agent publish` и подкоманды модулей (`Module.cli_register`/`cli_run`) | `api`, `mcp`, `registry`, `release`, `settings`, `db`, `paths` |
|
||||
|
||||
`web/` — веб-приложение хаба (React + TypeScript + Vite): вход по клиентскому токену, страницы «Доска» (`/app/board`),
|
||||
«Задачи» (`/app/issues`) и «Лента» (`/app/feed`: журнал изменений `GET /changes` за период; `web/src/feed/` — чистые
|
||||
функции `events.ts` (период, виды, текст события, группировка) и `EventList.tsx`, общий с блоком «История» карточки); текущий проект — `?project=<метка>` в адресе (`web/src/projects/`: контекст проекта, выпадающий список «Проект», окно «Новый проект», заглушка «проект не найден»; страницы пересоздаются по `key={project}`); карточка `/app/issues/{key}` — окно поверх страницы, с которой её открыли: `shell/Layout.tsx` рисует
|
||||
страницу по адресу из `location.state.background` (`useRoutes` с явным адресом) и `issues/IssueModal.tsx` поверх неё. Вложения в карточке — `web/src/issues/attachments/`: `format.ts` (размер, адрес в Jira, значок по типу, предел 10 МБ), `useAttachmentDownload.ts` (хук карточки: `POST /attachments/{id}/fetch`, опрос `GET /attachments/{id}` раз в 2 с до `ready`, затем `api.blob` на `/content` и сохранение через временную ссылку; одно состояние на вложение для всех кнопок; опрос стоит при закрытии окна и смене задачи), `AttachmentList.tsx` (раздел «Вложения»), `AttachmentChips.tsx` (плашки под комментарием); ссылки `!имя!`, `[^имя]` в тексте разбирает `issues/wiki/inline.ts` по списку вложений задачи (узел `attachment`). Собирается `make web` в `src/jira_hub/web/app/`; `api` отдаёт сборку
|
||||
без токена (`/app/assets/*` — файлы, прочие `/app…` — `index.html`, 503 без сборки), `/` и `/links` перенаправляет на
|
||||
`/app/board`. Данные — только через HTTP API с клиентским токеном; `mirror.board` отдаёт зеркало без преобразований,
|
||||
перевод в модель доски — в `web/src/board/model`. Логика доски — чистые модули `web/src/board/{model,analysis,layout}`
|
||||
и `storage.ts` (ключи `localStorage` прежней страницы; у проекта AWPCA — прежние, у остальных с суффиксом `@<метка>`; набор ключей — состояние модуля, выставляется при монтировании страницы), отрисовка — `web/src/board/components`.
|
||||
Вид — дизайн-система AURA: токены цвета, шрифта, отступов и теней (светлая и тёмная темы) — `web/src/styles/tokens.css`,
|
||||
единственное место с литералами цвета; базовые компоненты (`Button`, `Notice`, `Dialog`, `Field`, `Panel`, меню и др.) —
|
||||
`web/src/ui/`, остальные стили подключаются к токенам по семантическим именам. Оболочка — `web/src/shell/Layout.tsx`: grid из боковой
|
||||
панели `Sidebar.tsx` (закреплена у левого края, сворачивается в полосу значков — ключ `jira-hub-sidebar`; на экранах
|
||||
уже 640 px — строка с «Меню» и выезжающая панель) и страницы; прокрутка — у окна. Обновление данных из Jira (кнопка в
|
||||
боковой панели, отслеживание пакета) — `web/src/shell/refresh.ts` (провайдер в `App.tsx`); доска отдаёт наружу только
|
||||
источник и события снимка.
|
||||
### Модуль `jira` (`modules/jira/`)
|
||||
|
||||
Цепочки: `api → queue → commands/mirror/settings/projects/history → db`; `mirror → history → projects/settings`; `mirror → attachments → files`; `db → projects` (только миграция, ленивый импорт); `projects → settings`; `db → backup → paths`, `db → paths`; `files → paths`; `api → relay → settings`; `mcp_server → client → HTTP API`; `importer → mirror/settings`.
|
||||
Точка входа — `modules/jira/__init__.py` (`MODULE: Module`).
|
||||
|
||||
| Модуль | Ответственность | Зависит от |
|
||||
|---|---|---|
|
||||
| `schema` | таблицы модуля: `issue`, `issue_link`, `issue_comment`, `issue_attachment`, `attachment_file`, `issue_change`, `issue_note`, `sync_run` и др. | stdlib |
|
||||
| `settings` | умолчания настроек модуля, ключи для агента, проверка значений, перенос из `config.example.json` | `core.settings` |
|
||||
| `mirror` | зеркало задач, связей, комментариев и вложений; пишет его только `apply_state` (с проверками `check_comments`, `check_attachments`), разрез по проекту, заметки, фильтры, снимок доски `board`, карточка `get_issue` | `core.db`, `history`, `attachments` |
|
||||
| `history` | журнал изменений `issue_change`: проверка и запись пачек агента, `delivery_param` для выдачи `refresh`, лента `feed`, история задачи | `core.db`, `core.settings`, `projects` |
|
||||
| `attachments` | ссылки на вложения в вики-разметке, `fmt_size`, состояние `ready`/`fetching`/`failed`/`none` | `core.files` |
|
||||
| `fetch` | постановка скачивания вложения (`request_fetch`, `TooLarge`, `AgentTooOld`) | `core.queue`, `attachments`, `commands` |
|
||||
| `commands` | контракт типов команд jira (`ensure_*`, `refresh`, `fetch_attachment`), валидация, временные ключи, оценка, подтверждение по состоянию | `mirror` |
|
||||
| `refresh` | журнал синхронизаций `sync_run`, полный прогон, автообновление | `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`, `commands`, `mirror`, `history`, `refresh`, `attachments`, `fetch` |
|
||||
| `api` | маршруты модуля `MODULE.routes`: `/issues`, `/board`, `/changes`, `/attachments`, `/refresh`, `/sync` и др. | `core.http`, `core.queue`, `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.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/`)
|
||||
|
||||
Веб-приложение хаба — 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_server`/интерфейс) → `POST /batches` → `queue` → агент `claim` → Jira → `POST /agent/commands/{id}/result`.
|
||||
- Вложение: файл → `POST /files` → `files/<sha256>` → `ensure_attachment {key, file, filename}` в пакете (`queue._check_setup` проверяет, что файл есть) → `claim` (хаб дописывает `size`, в базе `params` без него) → агент `GET /agent/files/{sha256}` → Jira; уборка `files/` — из `tick` раз в сутки.
|
||||
- Создание проекта: `POST /projects` (веб или `project_create`) → `projects.create` и пакет «Новый проект <метка>» (`refresh` по общей JQL, `batch.project = NULL`) в одной транзакции → `claim` (хаб дописывает в `refresh` доски проектов, `boards`) → агент определяет тип доски → отчёт `refresh` пишет `board_type` → `ensure_sprint` проекта получает `board_id`.
|
||||
- Запись в 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`); `queue.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` (`queue.deliver_params` дописывает `history` — `since` и/или стартовую `jql`) →
|
||||
- Скачивание вложения: вход — `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`.
|
||||
@ -58,15 +102,19 @@
|
||||
- Токены: агентский — только `/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` в списке проектов, `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_server` не импортирует `queue`/`mirror`/`db`/`projects`/`history` напрямую и ходит только через `client`; из `files` берёт только предел
|
||||
`MAX_SIZE`, из `attachments` — только `fmt_size`; `downloads/` пишет только он (хаб эту папку лишь убирает) (файлы в хранилище кладёт хаб по `POST /files`).
|
||||
- `commands`, `mirror`, `queue` не знают про HTTP; HTTP живёт только в `api`, `client` и `relay` (реле — клиент своего же HTTP).
|
||||
- Веб-приложение (`web/`) ходит в хаб только через HTTP API (`web/src/api/client.ts`); словари `kind_labels`, `link_types` и набор закрытых статусов `closed_statuses` берёт из `GET /settings` и своих копий не держит.
|
||||
- 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`.
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@ -13,10 +13,11 @@
|
||||
- Два статических токена (в таблице `setting`): агентский открывает только `/agent/*`, клиентский — остальное.
|
||||
|
||||
## Архитектура
|
||||
- `src/jira_hub/` — весь код пакета, без разделения на ядро и адаптеры; чистая логика (`commands`, `mirror`, `queue`, `settings`) принимает соединение SQLite параметром.
|
||||
- `db` (схема, миграции) ← `settings`, `mirror`, `commands`, `projects` ← `queue` ← `api` (HTTP, маршруты в `ROUTES`, фоновый `tick`, реле общей папки `relay` (поток в `serve`, настройка `relay_dir`)) ← `cli`.
|
||||
- `files` (хранилище вложений `files/<sha256>`, каталог — параметр `Hub.files_dir`) ← `queue`, `api`; `mcp_server` берёт из него только предел `MAX_SIZE`.
|
||||
- `client` (HTTP-клиент хаба) ← `mcp_server` (stdio, инструменты MCP); `importer` → `mirror`/`settings`; `backup` вызывается из `api` и из `db` (копия перед миграцией схемы).
|
||||
- `src/jira_hub/core/` — ядро: `db`, `settings`, `projects`, `queue` (механика очереди), `files`, `api` (HTTP, `CORE_ROUTES` + маршруты модулей в `ROUTES`, фоновый `tick`), `relay`, `release`, `client`, `mcp` (каркас MCP), `cli`; контракт модуля `Module`/`CommandHooks` — `core/modules.py`, реестр — `core/registry.py`.
|
||||
- `src/jira_hub/modules/jira/` — модуль jira: `schema`, `mirror`, `history`, `attachments`, `fetch`, `commands`, `refresh`, `projects`, `hooks` (смысл типов команд для очереди), `api`, `mcp`, `settings`, `importer`/`cli`. Входная точка — `MODULE` в `modules/jira/__init__.py`.
|
||||
- Граница: ядро не импортирует модули (кроме ленивого импорта в `registry.py` и шагах миграций `db.py`), типы команд и термины jira живут в модуле, новые имена в ядре — без «jira»; проверяет `tests/test_registry.py`. Места долга проектов (`board_id`, `board_type`, `issues_count`) помечены `долг hub-modules`.
|
||||
- Цепочка ядра: `db` ← `settings`, `projects` ← `queue` ← `api` ← `cli`; `files` ← `queue`, `api`; реле `relay` — поток в `serve`; `backup` вызывается из `api` и из `db`. Модуль jira: `hooks → commands/mirror/history/refresh/attachments/fetch → core`.
|
||||
- Веб: оболочка и общее — `web/src/{shell,ui,api,projects,auth,styles}`, модули — `web/src/modules/<имя>/` (контракт `HubModule` в `modules/types.ts`, список — `modules/index.ts`); модуль jira — страницы доски, задач и ленты.
|
||||
- Подробная карта модулей и границ — `ARCHITECTURE.md`.
|
||||
|
||||
## Команды
|
||||
@ -26,17 +27,17 @@
|
||||
- `uv run jira-hub serve [--host] [--port]` — локальный запуск хаба.
|
||||
- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`, лента изменений — `/app/feed?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску.
|
||||
- `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур).
|
||||
- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [--project <метка>]` — MCP по stdio (с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены.
|
||||
- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [модуль] [--project <метка>]` — MCP модуля по stdio (по умолчанию `jira`; с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены.
|
||||
- `uv run jira-hub agent publish PATH` — опубликовать версию агента для самообновления (только вверх).
|
||||
|
||||
## Тесты
|
||||
- Golden-тесты: `tests/fixtures/<name>.*` + эталон `<name>.expected.*`. Внешние сервисы в тестах мокаются.
|
||||
- SQLite в памяти или во временном `JIRA_HUB_HOME` (`tmp_path`); API поднимается на порту 0 в отдельном потоке. Jira и агент в тестах не участвуют.
|
||||
- Соединение SQLite в тесте — через фикстуру `open_db` (`tests/conftest.py`) или фикстуру `conn` модуля, иначе с явным `close()`: `pyproject.toml` (`filterwarnings`) делает незакрытое соединение ошибкой набора.
|
||||
- Фронт: golden-эталоны доски — `web/src/board/__fixtures__/*.expected.json` (их пишет `web/scripts/golden-from-links.ts` из старой страницы); тесты рядом с кодом `*.test.ts(x)`; e2e — `web/e2e/` на тестовом хабе.
|
||||
- Фронт: golden-эталоны доски — `web/src/modules/jira/board/__fixtures__/*.expected.json` (их пишет `web/scripts/golden-from-links.ts` из старой страницы); тесты рядом с кодом `*.test.ts(x)`; e2e — `web/e2e/` на тестовом хабе.
|
||||
|
||||
## Первая подсистема
|
||||
Очередь команд (`queue.py`, Task 5): пакеты, порядок `(batch.created_at, seq)`, одна команда в полёте, временные ключи `new:<n>`, подтверждение по состоянию зеркала. Остальное (API, MCP, импорт) надстраивается над ней.
|
||||
Очередь команд (`core/queue.py`, Task 5): пакеты, порядок `(batch.created_at, seq)`, одна команда в полёте, временные ключи `new:<n>`, подтверждение по состоянию зеркала. Остальное (API, MCP, импорт) надстраивается над ней.
|
||||
|
||||
## OpenSpec и superpowers
|
||||
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.
|
||||
|
||||
@ -15,6 +15,8 @@ Python для хаба uv ставит свой (3.13, `.python-version`; сис
|
||||
`pyproject.toml`): в SQLite 3.51.0–3.51.1 есть взаимная блокировка, от которой хаб зависал под параллельными запросами.
|
||||
Первый `uv sync` или `uv run` после смены сам пересоздаёт `.venv`.
|
||||
|
||||
Код: ядро — `src/jira_hub/core/`, модуль jira — `src/jira_hub/modules/jira/`, веб — `web/src/` (модуль jira — `web/src/modules/jira/`); карта — `ARCHITECTURE.md`.
|
||||
|
||||
Данные лежат в `~/.local/share/jira-hub/` (каталог переопределяется переменной `JIRA_HUB_HOME`):
|
||||
`hub.sqlite`, `backups/`, `release/` и `files/` (файлы вложений по sha256; каталоги — 0700). Пакет `mcp` закреплён версией `<2` (рулинг R-10): MCP SDK 2.x ломает API.
|
||||
|
||||
@ -49,7 +51,7 @@ uv run jira-hub import ~/projects/awp-ca/admin/jira-sync
|
||||
|
||||
## MCP в Claude
|
||||
|
||||
`jira-hub mcp` запускает MCP-сервер по stdio; он ходит в API хаба клиентским токеном. Если хаб не запущен, инструменты
|
||||
`jira-hub mcp [модуль]` запускает MCP-сервер модуля по stdio (по умолчанию `jira`); он ходит в API хаба клиентским токеном. Если хаб не запущен, инструменты
|
||||
отвечают «хаб не запущен: jira-hub serve». Регистрация вручную (файл `~/.claude.json` скрипты хаба не правят),
|
||||
в user scope — MCP нужен и в `~/projects/jira-bridge`, и в `~/projects/awp-ca`, где ведут задачи AURORA:
|
||||
|
||||
@ -99,7 +101,7 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj
|
||||
В примере у `ensure_issue` нет `labels`: в привязанной сессии метку проекта дописывает хаб. Новый проект — `project_create`:
|
||||
в ответе «проект <метка> «<название>» заведён, пакет обновления #N», затем указание положить в корень репозитория проекта
|
||||
`.mcp.json` (слить с имеющимся) и переподключить MCP (`/mcp` → reconnect, одобрить сервер проекта) и сам фрагмент `.mcp.json`
|
||||
с командой `uv run --project <корень хаба> jira-hub mcp --project <метка>`.
|
||||
с командой `uv run --project <корень хаба> jira-hub mcp --project <метка>` (модуль по умолчанию `jira`; явно — `jira-hub mcp jira --project <метка>`).
|
||||
|
||||
«Ошибка» с файлом одним пакетом (нужна настройка `issue_types.bug`, см. «Контракт команд»): MCP читает файл по `path`,
|
||||
загружает его `POST /files` и ставит `ensure_attachment` с `file` и `filename` (по умолчанию — имя файла). Нет файла,
|
||||
|
||||
@ -152,7 +152,7 @@ def h_agent_settings(hub, conn, m, q, body):
|
||||
|
||||
|
||||
def h_agent_command(hub, conn, m, q, body):
|
||||
"""Команда по id для восстановления из журнала агента: параметры — как при выдаче (с size у ensure_attachment)."""
|
||||
"""Команда по id для восстановления из журнала агента: параметры — как при выдаче (с size у команд с файлом)."""
|
||||
cmd = not_found(queue.get_command(conn, int_group(m)), "command")
|
||||
return 200, {**cmd, "params": queue.deliver_params(conn, cmd, hub.files_dir)}
|
||||
|
||||
@ -255,7 +255,7 @@ def h_files_post(hub, conn, m, q, body):
|
||||
|
||||
|
||||
def h_agent_files_post(hub, conn, m, q, body):
|
||||
"""Файл от агента (fetch_attachment, решение 5): как POST /files, но пустой файл принимается (решение 13)."""
|
||||
"""Файл от агента (ответ на запрос файла, решение 5): как POST /files, но пустой файл принимается (решение 13)."""
|
||||
return _put_file(hub, body, allow_empty=True)
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""Хранилище файлов вложений: <каталог данных>/files/<sha256>, одна копия на содержимое, без имени вложения — имя задаёт
|
||||
команда ensure_attachment (bug-and-attachments, hub-api «Хранилище файлов вложений»). Модуль не знает HTTP и окружения:
|
||||
команда модуля (bug-and-attachments, hub-api «Хранилище файлов вложений»). Модуль не знает HTTP и окружения:
|
||||
каталог передаётся параметром (Hub.files_dir, решение 10)."""
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
@ -75,6 +75,10 @@ def test_mcp_default_module(monkeypatch):
|
||||
assert cli.main(["mcp", "--project", "AWPCA"]) == 0
|
||||
assert cli.main(["mcp", "jira", "--project", "AWPCA"]) == 0
|
||||
assert seen == [("stdio", "AWPCA"), ("stdio", "AWPCA")]
|
||||
# без --project сессия не привязана к проекту
|
||||
monkeypatch.setattr(core_mcp, "PROJECT", "STALE")
|
||||
assert cli.main(["mcp"]) == 0
|
||||
assert seen[-1] == ("stdio", None)
|
||||
|
||||
|
||||
def test_mcp_unknown_module(monkeypatch, capsys):
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""Реестр модулей: состав, конфликты имён, границы импортов ядра."""
|
||||
import ast
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@ -92,3 +93,26 @@ def test_core_imports():
|
||||
dirty.add(path.name)
|
||||
assert dirty - PENDING == set(), f"ядро импортирует модули: {sorted(dirty - PENDING)}"
|
||||
assert PENDING - dirty == set(), f"PENDING устарел, уберите: {sorted(PENDING - dirty)}"
|
||||
|
||||
|
||||
# типы команд и термины jira; ensure_defaults и json.dumps(ensure_ascii=...) — легитимные имена ядра
|
||||
JIRA_TERMS = re.compile(
|
||||
r"""['"]refresh['"]|\bensure_(?!defaults\b|ascii\b)\w+|\bfetch_attachment\b|\bsync_run\b|\bissue_attachment\b"""
|
||||
r"""|\battachment_file\b|\bissue_comment\b|\bjql\b|\w_jql\b|\bjql_\w+""",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
def test_core_has_no_jira_terms():
|
||||
"""Ядро не знает типов команд и терминов jira; исключения — db.py (история миграций) и места с меткой долга."""
|
||||
hits = []
|
||||
for path in sorted(CORE.glob("*.py")):
|
||||
if path.name == "db.py":
|
||||
continue
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
for i, line in enumerate(lines):
|
||||
if not JIRA_TERMS.search(line):
|
||||
continue
|
||||
if "долг hub-modules" in line or (i > 0 and "долг hub-modules" in lines[i - 1]):
|
||||
continue
|
||||
hits.append(f"{path.name}:{i + 1}: {line.strip()}")
|
||||
assert hits == [], "термины jira в ядре:\n" + "\n".join(hits)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user