From ede028346bcd6e1a7c52c26200024fa803a4a90d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 03:37:17 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=86=D0=B8=D1=8F:=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D1=8C=20wiki=20=D0=B8=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B5=D0=B9=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тест границы test_core_has_no_module_terms; ARCHITECTURE/CLAUDE/README; дельта-спека о хуках before_result/abort_result. Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 69 ++++++++++++++----- CLAUDE.md | 15 ++-- README.md | 38 +++++++++- .../changes/wiki-module/specs/modules/spec.md | 15 ++++ tests/test_registry.py | 16 +++++ 5 files changed, 127 insertions(+), 26 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0d0aaf6..68f8b74 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,12 +1,12 @@ # Архитектура -Хаб моста Jira: ядро + модули; сейчас один модуль — `jira`: SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP. Агент (`jira-agent`, виртуалка) опрашивает хаб, пишет в Jira и возвращает состояние; хаб Jira не вызывает. +Хаб моста Jira: ядро + два модуля. `jira` — SQLite-зеркало задач проектов-меток Jira-проекта AURORA, служебные заметки, очередь команд, HTTP API и MCP; `wiki` — Markdown-копия вики Eva по проектам, поиск и вложения по запросу. У каждого модуля свой агент на виртуалке (`jira-agent`, `eva-agent`): он опрашивает хаб, ходит в свою внешнюю систему и возвращает данные; хаб во внешние системы не ходит. ## Ядро и модули Хаб — ядро (`src/jira_hub/core/`) и подключаемые модули (`src/jira_hub/modules/<имя>/`). Ядро держит механику: базу, очередь команд, HTTP, настройки, реле, выпуск агента, каркас MCP и CLI. Модуль даёт смысл: схему своих таблиц, типы -команд, маршруты, настройки, MCP-инструменты, подкоманды. Сейчас модуль один — `jira`; следующие (wiki, sdlc) подключаются +команд, маршруты, настройки, MCP-инструменты, подкоманды. Сейчас модулей два — `jira` и `wiki`; следующие (sdlc) подключаются так же, строкой в `core/registry.py`. ### Ядро (`core/`) @@ -17,19 +17,19 @@ | `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` | +| `registry` | реестр модулей: ленивая сборка, `modules()`, `module(name)`, `module_of(тип_команды)`, `agent_modules()`, `DEFAULT_AGENT`, проверка конфликтов (типы команд, маршруты, ключи настроек, поля проекта) | `modules`; модули — ленивым импортом внутри функции | +| `db` | `connect`, схема ядра (`setting`, `batch`, `command`, `project`, `agent_status`) и схемы модулей из `Module.schema`, миграции по `schema_version` (v7; копия базы перед миграцией; история шагов `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:`; смысл типов команд берёт у модуля через `CommandHooks` (`registry.module_of`) | `backup`, `db`, `registry`, `settings`, `errors` | | `backup` | копии файла базы, ротация | `paths` | | `files` | хранилище файлов `<данные>/files/` (каталог — параметр, `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` | +| `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` | +| `cli` | `init`, `serve`, `mcp [модуль] [--project]`, `tokens`, `agent publish [--module]` и подкоманды модулей (`Module.cli_register`/`cli_run`) | `api`, `mcp`, `registry`, `release`, `settings`, `db`, `paths` | ### Модуль `jira` (`modules/jira/`) @@ -51,12 +51,35 @@ | `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` | +### Модуль `wiki` (`modules/wiki/`) + +Точка входа — `modules/wiki/__init__.py` (`MODULE: Module`). Копию страниц хаб хранит файлами `wiki/<метка>/` в каталоге данных (Markdown + `_tree.json`), строки и FTS — в базе. + +| Модуль | Ответственность | Зависит от | +|---|---|---| +| `schema` | таблицы модуля: `wiki_project`, `wiki_page`, `wiki_page_fts` (FTS5), `wiki_attachment`, `wiki_sync_stage` | 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:` и `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` | инструменты сервера `jira-hub-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, 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`. +- `Module(name, schema, settings_defaults, 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 реестр её не проверяет. @@ -69,6 +92,11 @@ `web/src/modules/types.ts` (`pages`, `nav`, `Provider`, `SidebarStatus`, `TopbarNotice`, `useOverlay`, `useOnProjectCreated`); список модулей — константа `MODULES` в `web/src/modules/index.ts` (хуки вызываются в постоянном порядке). +Модуль `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`), +плашка синхронизации в боковой панели (`SyncStatus`); поле «Корень вики» в окне «Новый проект» — через `projectFields`. + Модуль `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` — ключи @@ -84,8 +112,9 @@ golden-эталоны `__fixtures__/`), `issues/` (список, карточк Цепочки зависимостей (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 → ui/api/projects` (модуль не импортирует оболочку, кроме типов контракта). +- веб: `shell → modules/index → modules/jira, modules/wiki → ui/api/projects` (модуль не импортирует оболочку, кроме типов контракта). ## Потоки данных - Запись в Jira: клиент (MCP/интерфейс) → `POST /batches` → `queue` → агент `claim` → Jira → `POST /agent/commands/{id}/result`. @@ -97,24 +126,30 @@ golden-эталоны `__fixtures__/`), `issues/` (список, карточк агент 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-/<имя>`. - Агент за 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`. +- Самообновление агента: `jira-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/`) не импортирует модули (`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)`; убирать их можно только вместе с переносом проектов в модуль. +- Типы команд (`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`; 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/jira/mcp`) не импортирует `queue`/`db`/`projects` и модульные `mirror`/`history` напрямую, ходит только через `client`; из `core.files` берёт только предел `MAX_SIZE`, из `attachments` — только `fmt_size`; `downloads/` пишет только он (хаб эту папку лишь убирает). +- MCP (`core/mcp`, `modules/*/mcp`) не импортирует `queue`/`db`/`projects` и модульные `mirror`/`history` напрямую, ходит только через `client`; из `core.files` берёт только предел `MAX_SIZE`, из `attachments` — только `fmt_size`; `downloads/` пишет только он (jira — `/`, wiki — `wiki-/`) (хаб эту папку лишь убирает). - `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 не реализована: хаб только показывает их. +- Запись в вики Eva (правка страниц из хаба) не делается: модуль только читает. +- История страниц вики и частичная перекачка: сейчас каждая синхронизация перекачивает всё дерево, копия без истории. +- Переименование проекта-метки и перенос корня вики между проектами не предусмотрены (меняется только корень — `PATCH /projects`). +- Ручной просмотр вёрстки веб-страницы «Вики» на живых данных (таблицы, вложенные списки) — за владельцем после выкатки. - Переименование хаба в sdlc-hub (модули jira + wiki, eva-agent) — после перестройки кода; имена пакета, команды и MCP-сервера пока `jira-hub`. diff --git a/CLAUDE.md b/CLAUDE.md index 8f4cae8..fad3bed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # jira-hub ## Что это -Хаб моста Jira для АРМ КА: единственный источник истины по задачам проектов-меток Jira-проекта AURORA на Mac mini (`192.168.0.106`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; активные change — `openspec/changes/` (кроме `archive/`). +Хаб моста Jira для АРМ КА: единственный источник истины по задачам проектов-меток Jira-проекта AURORA на Mac mini (`192.168.0.106`) и Markdown-копии вики Eva по проектам (модуль `wiki`). Хранит зеркало задач, служебные заметки и очередь команд в SQLite, отдаёт HTTP API в локальной сети (агенту на виртуалке `jira-agent` и интерфейсам) и MCP-сервер для Claude. Главный сценарий: из чата поставить пакет команд, агент на виртуалке выполняет их в Jira по правилу «проверь, потом сделай» и возвращает состояние задач в зеркало. Спеки: `openspec/specs/`; активные change — `openspec/changes/` (кроме `archive/`). ## Стек и ограничения - Веб-приложение — `web/` (React 19 + TypeScript + Vite, TanStack Query, Vitest, Playwright; Node 24, npm); сборка в `src/jira_hub/web/app/` не в git. @@ -9,15 +9,16 @@ - Хаб не знает пароль Jira и не ходит в Jira: учётные данные и сеть до Jira только у агента. - Зеркало задач пишет только `mirror.apply_state` по загрузке состояния от агента (`POST /agent/state`); правки идут командами через очередь. - Хаб не создаёт команды сам: повтор после failed — новая команда по запросу человека/интерфейса. Команду, выданную агенту и оставшуюся без отчёта, хаб довыдаёт при следующем claim (до 3 выдач, затем failed). -- Данные: `~/.local/share/jira-hub/` (`JIRA_HUB_HOME`): `hub.sqlite` (зеркало, очередь, журнал изменений `issue_change`), `backups/`, `release/` (опубликованная версия агента), `files/` (файлы вложений по sha256, уборка через 30 дней), `downloads/` (копии вложений для MCP `/<имя>`, уборка 30 дней); каталог данных, `backups/`, `release/`, `files/` и `downloads/` — права 0700. Порт по умолчанию 8790. -- Два статических токена (в таблице `setting`): агентский открывает только `/agent/*`, клиентский — остальное. +- Данные: `~/.local/share/jira-hub/` (`JIRA_HUB_HOME`): `hub.sqlite` (зеркало, очередь, журнал изменений `issue_change`), `backups/`, `release/` (опубликованная версия агента), `files/` (файлы вложений по sha256, уборка через 30 дней), `downloads/` (копии вложений для MCP `/<имя>`, у wiki `wiki-/<имя>`, уборка 30 дней), `wiki/<метка>/` (Markdown-копия вики проекта: `index.md` в каталоге страницы, `_tree.json`); каталог данных, `backups/`, `release/<модуль>/`, `files/`, `downloads/` и `wiki/` — права 0700. Порт по умолчанию 8790. +- Статические токены (в таблице `setting`): клиентский — всё, кроме `/agent/*`; по одному токену агента на модуль (`jira`, `wiki`) — только `/agent/*` и только маршруты своего модуля. Токен Eva в хабе не хранится. ## Архитектура - `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`. +- `src/jira_hub/modules/wiki/` — модуль wiki: `schema`, `settings`, `paths`, `projects` (поле `wiki_root`), `commands` (`wiki_sync`, `wiki_fetch` и хуки очереди), `convert` (HTML → Markdown), `stage`, `sync`, `search` (FTS5), `attachments`, `tick`, `api`, `mcp`. Входная точка — `MODULE` в `modules/wiki/__init__.py`. +- Граница: ядро не импортирует модули (кроме ленивого импорта в `registry.py` и шагах миграций `db.py`), типы команд и термины модулей (jira, wiki, eva) живут в модуле, новые имена в ядре — без названий модулей; проверяют `test_core_imports`, `test_core_has_no_jira_terms`, `test_core_has_no_module_terms` в `tests/test_registry.py`. Меток-исключений нет. - Цепочка ядра: `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 — страницы доски, задач и ленты. +- Веб: оболочка и общее — `web/src/{shell,ui,api,projects,auth,styles}`, модули — `web/src/modules/<имя>/` (контракт `HubModule` в `modules/types.ts`, список — `modules/index.ts`); модуль jira — страницы доски, задач и ленты; модуль wiki (`web/src/modules/wiki/`) — страница «Вики» (`/app/wiki?project=<метка>`). - Подробная карта модулей и границ — `ARCHITECTURE.md`. ## Команды @@ -27,8 +28,8 @@ - `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 (по умолчанию `jira`; с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены. -- `uv run jira-hub agent publish PATH` — опубликовать версию агента для самообновления (только вверх). +- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [модуль] [--project <метка>]` — MCP модуля по stdio (по умолчанию `jira`; `jira-hub mcp wiki` — сервер `jira-hub-wiki`; с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены (клиентский и по агенту на модуль). +- `uv run jira-hub agent publish [--module jira|wiki] PATH` — опубликовать версию агента модуля для самообновления (только вверх; по умолчанию `jira`; публикация лежит в `release/<модуль>/`). ## Тесты - Golden-тесты: `tests/fixtures/.*` + эталон `.expected.*`. Внешние сервисы в тестах мокаются. diff --git a/README.md b/README.md index 6cb9c45..e274697 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ uv run jira-hub serve # 0.0.0.0:8790 ``` `init` переносит в настройки хаба всё, что относится к проекту Jira (поля, типы, карту переходов, типы связей, доску, -приоритеты), и создаёт два токена: агентский и клиентский. Адрес и учётные данные Jira (`base_url`, `auth`, `verify_ssl`, +приоритеты), и создаёт токены: клиентский и по одному на агента каждого модуля (`jira`, `wiki`). Адрес и учётные данные Jira (`base_url`, `auth`, `verify_ssl`, `ca_bundle`) остаются у агента и в хаб не попадают. Повторный `init` отказывает ("уже инициализирован"); с `--force` настройки переписываются. Токены можно показать позже: `uv run jira-hub tokens`. @@ -61,6 +61,22 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj После перезапуска хаба или обновления кода MCP — `/mcp` → reconnect. +MCP вики — отдельный сервер `jira-hub-wiki` (`jira-hub mcp wiki`). Регистрация в `.mcp.json` репозитория проекта (рядом с `jira-hub`, с привязкой к проекту): + +```json +{ + "mcpServers": { + "jira-hub-wiki": { + "command": "uv", + "args": ["run", "--project", "/Users/andrey.epifancev/projects/jira-bridge/jira-hub", "jira-hub", "mcp", "wiki", "--project", "AWPCA"] + } + } +} +``` + +Инструменты: `projects`, `project_create`, `root_set`, `tree`, `page`, `search`, `attachment`, `sync`, `status`. Пока у проекта нет корня +вики (`root_set DOC-…` или `wiki_root` в `project_create`), они отвечают «у проекта <метка> нет корня вики». + Сессия MCP работает в разрезе одного проекта-метки. Привязка — `--project <метка>` в `.mcp.json` репозитория проекта (метка сравнивается точно, с учётом регистра): @@ -113,6 +129,23 @@ claude mcp add -s user jira-hub -- uv run --project /Users/andrey.epifancev/proj {"type": "ensure_attachment", "params": {"key": "new:1", "path": "/Users/…/ОСВ сч. 60 за 2 кв. 2026.xlsx"}}] ``` +## Вики и eva-agent + +Копию вики Eva хаб получает от `eva-agent` — отдельного агента на той же виртуалке (отдельный репозиторий `aep/eva-agent`, +каталог `jira-bridge/eva-agent/`; Python 3.9, один файл `src/agent.py` + `run.py` — как jira-agent). Хаб в Eva не ходит и токен Eva не хранит. + +1. `uv run jira-hub tokens` — взять токен агента `wiki` (у `jira` и `wiki` токены разные; чужой токен на маршрутах другого агента даёт 401). +2. На виртуалке отдельным каталогом положить eva-agent и `agent.json`: `hub_url` (`folder:<путь>` или http), `hub_token` — токен агента `wiki`, + `eva: {base_url, token}`, `verify_ssl`, `ca_bundle`, `timeout`, `poll_seconds`, `auto_update`. Токен Eva — только здесь. Первый запуск + делает владелец руками (свой процесс и блокировка). +3. Публикация версии для самообновления: `uv run jira-hub agent publish --module wiki ../eva-agent/src/agent.py` + (файл ложится в `release/wiki/`; `min_agent_version` модуля — настройка `wiki_min_agent_version`). +4. Корень вики проекта: `project_create` с `wiki_root` или MCP `root_set DOC-…` / форма на странице «Вики» (`PATCH /projects/{метка}`) — + хаб ставит `wiki_sync`; дальше синхронизация раз в `wiki_auto_sync_hours` (по умолчанию 24, `0` — выключена), кнопка «Обновить» или MCP `sync`. + +Копия лежит в `~/.local/share/jira-hub/wiki/<метка>/` (Markdown, по каталогу на страницу). Настройки модуля: `wiki_pacing` (пауза агента между +запросами к Eva, с), `wiki_auto_sync_hours`, `wiki_min_agent_version`. + ## Транспорт через общую папку Если сеть между виртуалкой и Mac mini закрыта (VPN «Континент ZTN» на виртуалке режет локальную сеть), агент ходит @@ -143,7 +176,8 @@ curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/j Опубликовать новую версию: ```bash -uv run jira-hub agent publish ../jira-agent/src/agent.py +uv run jira-hub agent publish ../jira-agent/src/agent.py # модуль jira (по умолчанию) +uv run jira-hub agent publish --module wiki ../eva-agent/src/agent.py ``` Версия берётся из строки `AGENT_VERSION = N` и должна быть больше опубликованной; файл и `manifest.json` ложатся в diff --git a/openspec/changes/wiki-module/specs/modules/spec.md b/openspec/changes/wiki-module/specs/modules/spec.md index adf1254..c366b12 100644 --- a/openspec/changes/wiki-module/specs/modules/spec.md +++ b/openspec/changes/wiki-module/specs/modules/spec.md @@ -44,3 +44,18 @@ jira-agent, `wiki` — eva-agent). Агент SHALL определяться п #### Scenario: Поля jira прежние - **WHEN** после миграции v7 вызван `GET /projects` - **THEN** у `AWPCA` `board_id = 4216`, `board_type = scrum`, `issues_count` — как до миграции + +### Requirement: Подготовка отчёта команды модулем +Контракт `CommandHooks` SHALL содержать два необязательных хука. `before_result(conn, cmd, status, result, files_dir)` +SHALL вызываться ядром при отчёте по выданной команде до транзакции отчёта, чтобы модуль готовил тяжёлую работу (перевод, +запись файлов) вне блокировки базы; `ValueError` из него SHALL отдаваться как ошибка сервера, а не как неверный статус +отчёта. `abort_result(conn, cmd)` SHALL вызываться, когда отчёт не дошёл до `after_result` (ранний выход, исключение), и +снимать подготовку `before_result`. Модуль без этих хуков SHALL работать как прежде. + +#### Scenario: Подготовка до транзакции +- **WHEN** агент шлёт отчёт `done` по команде типа модуля с `before_result` +- **THEN** `before_result` вызван до `BEGIN IMMEDIATE`, после фиксации транзакции — `after_result` + +#### Scenario: Отчёт прерван +- **WHEN** `before_result` выполнен, а транзакция отчёта завершилась исключением +- **THEN** вызван `abort_result`, подготовка снята, команда остаётся выданной diff --git a/tests/test_registry.py b/tests/test_registry.py index efc9200..ee49470 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -115,3 +115,19 @@ def test_core_has_no_jira_terms(): continue hits.append(f"{path.name}:{i + 1}: {line.strip()}") assert hits == [], "термины jira в ядре:\n" + "\n".join(hits) + + +# термины модуля wiki/eva: ядро их не знает (рулинг R-3) +MODULE_TERMS = re.compile(r"\beva\b|\beva-agent\b|wiki_|wiki/|DOC-", re.IGNORECASE) + + +def test_core_has_no_module_terms(): + """Ядро не знает терминов модуля wiki и eva; исключение — registry.py (ленивый импорт модулей).""" + hits = [] + for path in sorted(CORE.glob("*.py")): + if path.name == "registry.py": + continue + for i, line in enumerate(path.read_text(encoding="utf-8").splitlines()): + if MODULE_TERMS.search(line): + hits.append(f"{path.name}:{i + 1}: {line.strip()}") + assert hits == [], "термины модулей в ядре:\n" + "\n".join(hits)