Andrey Epifancev 2d1cf84c83 docs(openspec): решения R-9, R-10 (#87)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:53:26 +04:00

219 lines
26 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.

## Context
`main` 46a6fc2, схема v6, агент v14, хаб v6. Пакет `src/jira_hub/` плоский (≈4,2 тыс. строк Python), веб — `web/src/`.
Что уже общее, а что завязано на Jira (разбор 23.10.2026):
- **Уже общее, почти без правок:** `paths`, `backup`, `files`, `relay`, `release`, `client`; в вебе — `shell/` (оболочка,
боковая панель, тема), `ui/`, `styles/`, `auth/`, `projects/`, `api/`.
- **Модуль jira:** `mirror`, `history`, `attachments`, `commands`, `importer`; в вебе — `board/`, `issues/`, `feed/`,
`shell/refresh.ts` с `RefreshProvider` и `refreshStatus` (кнопка «Обновить из Jira»).
- **Переплетено:**
- `queue` (537 строк): механика пакетов общая, но `_check_setup` (настройка `bug`, файл вложения), `_check_project`,
`_assign_boards` (`ensure_sprint.board_id`), `request_fetch`, `deliver_params` (`boards`, `history`, `size`),
`READ_TYPES`, `sync_run` в `claim`/`_redeliver`, проверка `fetch_attachment` в `report_result`,
`maybe_auto_refresh` — про Jira.
- `api` (700 строк): `Hub`, сервер, авторизация, раздача веба и `tick` — общие; 35 маршрутов в одной таблице `ROUTES`,
большая часть — Jira.
- `db`: одна строка `SCHEMA` со всеми таблицами, одна цепочка `MIGRATIONS` v1→v6 (шаг v3→v4 лениво зовёт `projects`).
- `settings`: `DEFAULTS` и `validate` смешивают ключи хаба (`port`, `relay_dir`, `backup_keep`, `min_agent_version`) и
Jira (`project_key`, `issue_types`, `field_ids`, `pacing`, `link_types`, `closed_statuses` …).
- `projects`: метка и название — общие; `board_id`, `board_type`, `boards`, `base_jql` — Jira.
- `mcp_server` (680 строк): `FastMCP("jira-hub")`, привязка к проекту (`scoped`, `guard`), инструменты `projects` и
`project_create` — общие; остальное — Jira.
- `cli`: `serve`, `tokens`, `mcp`, `init` — общие; `import` и `agent publish` — Jira.
## Goals / Non-Goals
**Goals:** код делится на ядро и модуль jira с явным контрактом подключения; ядро не импортирует модуль jira (кроме
списка подключённых модулей и истории миграций); MCP и веб собираются по модулям; поведение снаружи не меняется.
**Non-Goals и Anti-Goals:** см. proposal.md.
## Decisions
1. **Раскладка пакета: `jira_hub/core/` и `jira_hub/modules/jira/`.** Будущий модуль — `jira_hub/modules/wiki/`. Имя
пакета `jira_hub` и точка входа `jira-hub` остаются до переименования. В ядре новые имена не содержат «jira»
(переименовывать потом меньше). Старые пути импорта (`jira_hub.queue` и т. п.) не сохраняются — тесты переходят на
новые; внешних потребителей пакета нет.
2. **Контракт модуля — объект `Module` в `core/modules.py`.** Поля (все необязательные, кроме `name`):
- `name` — `"jira"`;
- `schema` — SQL таблиц модуля (`CREATE TABLE IF NOT EXISTS …`);
- `commands` — типы команд модуля: спецификация параметров (нынешний `commands.SPEC`), признак «только чтение»
(нынешние `READ_TYPES`), паузы по умолчанию;
- обработчики очереди (решение 4);
- `routes` — маршруты HTTP в формате нынешней `ROUTES` (`method, regex, handler, scope`);
- `settings` — значения по умолчанию, проверка значений, ключи для агента;
- `tick` — фоновые работы (у jira — автообновление);
- `mcp_tools` — функция регистрации инструментов на сервере MCP модуля;
- `cli` — свои подкоманды (`import`, `agent publish`).
Список модулей — `core/registry.py` (`MODULES = [jira.MODULE]`). При сборке ядро проверяет, что имена типов команд,
ключи настроек и маршруты (метод + шаблон) не пересекаются между ядром и модулями; пересечение — ошибка старта, а не
молчаливое перекрытие.
3. **База: одна схема и одна версия на весь хаб.** Ядро собирает `SCHEMA` из своих таблиц (`setting`, `project`, `batch`,
`command`, `agent_status`) и `schema` модулей. `schema_version` остаётся одной (6), цепочка `MIGRATIONS` — одной
упорядоченной историей в `core/db.py` (шаги v1…v6 не переписываются; шаг v3→v4 по-прежнему лениво зовёт функцию
модуля). Новый модуль добавит свои таблицы следующей версией (v7). *Отвергнуто:* версия схемы на модуль — порядок
миграций между модулями пришлось бы согласовывать, а копия базы перед миграцией делается на файл целиком.
Существующие таблицы не переименовываются; `sync_run` остаётся таблицей модуля jira.
4. **Очередь: ядро — механика, модуль — смысл типов.** В `core/queue.py` остаются пакеты, порядок, одна команда в полёте,
временные ключи `new:<n>`, довыдача до 3 раз, отчёты, отмена, повтор, резервная копия перед первым пишущим пакетом,
выборка пакетов. Модуль даёт обработчики, которые ядро зовёт по типу команды или на весь пакет:
- `check_batch(conn, cmds, project, files_dir)` — нынешние `_check_setup`, `_check_project`, `_assign_boards`;
- `deliver_params(conn, cmd, files_dir)` — дописывание параметров при выдаче;
- `on_claim(conn, cmd, now, redelivery)` — нынешняя работа с `sync_run` в `claim`;
- `on_failed(conn, cmd, now)` — закрытие `sync_run` после исчерпанных выдач;
- `on_result(conn, cmd, status, result, files_dir)` — всё, что отчёт делает по типу команды (решение 12);
- `check_confirmed(conn, cmd)` — подтверждение по состоянию (`commands.check_confirmed`);
- `tail_command(keys)` — хвостовой `refresh` пишущего пакета;
- временные ключи: какой тип их определяет (`ensure_issue.temp_key`), где они встречаются в параметрах
(`referenced_keys`, `temp_keys_used`), подстановка (`substitute`) и поведение при неразрешённых ключах (у `refresh` —
выбросить временные ключи и выполнить по остальным). Механика «зависимая команда ждёт создающую, отменённое создание
→ `skipped` с текстом» остаётся в ядре.
Особые входы модуля (`request_fetch`, `maybe_auto_refresh`, создание проекта с пакетом `refresh`) переезжают в модуль
jira и пользуются публичными функциями очереди ядра (`create_batch`, `_insert_batch` → публичный `insert_batch`).
Порядок проверок и тексты ошибок сохраняются: на них смотрят тесты и MCP.
5. **Один агент — пока.** `agent_status` (одна строка), агентский токен, `release/`, `claim` без вида агента — как сейчас,
код лежит в ядре. Виды агентов (`jira`, `eva`) появятся в change модуля wiki, вместе со вторым агентом, на котором их
можно проверить. Здесь только шов: каждый тип команды знает свой модуль (решение 2), значит, выдачу по виду агента
потом можно будет добавить фильтром в `claim`. *Отвергнуто:* ввести виды агентов сейчас — миграция схемы и смена
протокола без второго агента, на котором их можно проверить (анти-цель «миграция схемы»).
6. **HTTP: сервер в ядре, маршруты по модулям.** `core/api.py` — `Hub`, `Handler`, авторизация двумя токенами, раздача
веба, `tick`, маршруты ядра (`/agent/claim`, `/agent/settings`, `/agent/commands/…`, `/agent/release`,
`/agent/files…`, `/files`, `/batches…`, `/commands/…`, `/agent`, `/settings`, `/projects`, `/app…`, `/`). Маршруты
задач, доски, ленты, заметок, вложений, `/agent/state`, `/agent/history`, `/agent/known-keys`, `/refresh`,
`/sync-runs` — в `modules/jira/api.py`. Адреса, коды и тела ответов те же. `POST /projects` остаётся в ядре, а пакет
«Новый проект» ставит обработчик модуля `on_project_created` (решение 8).
7. **Настройки: ядро объединяет.** У ядра свои ключи (`port`, `relay_dir`, `backup_keep`, `min_agent_version`,
`settings_version`, `initialized`, `tokens`, `schema_version`), у модуля jira — остальные. `GET /settings`,
`PUT /settings`, `for_agent` и `init --config` работают по объединённому набору, как сейчас. `pacing` — ключ модуля
jira (паузы по типам его команд).
8. **Проекты: метка и название — ядро, доски — модуль.** Таблица `project` не меняется (колонки `board_id`,
`board_type` остаются в ней). `core/projects.py` — создание, список, проверка метки; `boards`, `base_jql`,
`migrate_v3`, `ensure_legacy` — в `modules/jira/projects.py`. После создания проекта ядро зовёт
`on_project_created(conn, project)` модулей; jira ставит пакет «Новый проект <метка>» в той же транзакции, как сейчас.
Корень вики проекта добавит change модуля wiki.
9. **MCP: общий слой в ядре, сервер на модуль.** `core/mcp.py` — `client`, `guard`, `scoped`, привязка к проекту,
инструменты `projects` и `project_create`, запуск `FastMCP(<имя>)`. `modules/jira/mcp.py` — остальные инструменты.
`jira-hub mcp [<модуль>] [--project <метка>]`: модуль по умолчанию — `jira`; имя сервера модуля jira — `jira-hub`
(как сейчас), набор инструментов и их тексты не меняются. Неизвестный модуль — код 2 и список модулей в stderr.
Будет ли сервер wiki нести `projects`/`project_create` — решит change модуля wiki.
10. **Веб: модуль — папка и описание.** `web/src/modules/jira/` вбирает `board/`, `issues/`, `feed/` и обновление из
Jira (`refresh.ts`, `RefreshProvider`, `refreshStatus`). `web/src/modules/jira/index.tsx` экспортирует описание
модуля: страницы (`PAGE_ROUTES` без изменения адресов `/board`, `/issues`, `/feed`, `/` → `/board`), пункты боковой
панели, действие панели (кнопка обновления с её событием), провайдер для `App.tsx`, окно карточки
(`/issues/{key}`). `web/src/modules/index.ts` — список модулей; `shell/pages.tsx`, `Sidebar.tsx`, `Layout.tsx`,
`App.tsx` берут всё оттуда. Адреса, ключи `localStorage` (`jira-hub-sidebar`, ключи доски), golden-эталоны доски и
e2e не меняются; эталоны переезжают вместе с `board/`.
11. **jira-agent не трогается.** `run.py` и общие части `agent.py` (транспорт папки, журнал, цикл, самообновление)
eva-agent возьмёт копией — это решение change модуля wiki, здесь только фиксируется, что протокол агента не меняется.
12. **Граница ядра — компромисс (решение владельца 23.10.2026).** Ядро не импортирует модуль jira и не содержит понятий
Jira в логике, **кроме проектов**. Строго в модуль jira уходит:
- типы команд, вписанные в код ядра: `'refresh','fetch_attachment'` в SQL `claim` (признак «пишущий пакет» — от
`commands` модуля), `ensure_attachment` в `files.sweep` (обработчик модуля «файлы, на которые ссылаются команды»);
- паузы: `pacing` — ключ модуля, ядро берёт паузу команды и оценку пакета через модуль (`claim`, `estimate`);
- отчёты: вместо одного `check_result` — `on_result(conn, cmd, status, result, files_dir)` модуля, куда переезжает
всё, что `report_result` делает по типу команды (`attachment_file`, закрытие `sync_run`, `record_board_types`,
переименование заметки, отметка соседей, `reconcile`);
- исключения модуля → коды HTTP: модуль объявляет свои исключения с кодом и `error` (`TooLarge` → 413,
`AgentTooOld` → 409), ядро переводит их в ответ, не зная о них;
- `/settings`: модуль дописывает вычисляемые поля ответа (`base_jql`);
- `jira_utc` и вспомогательное для зеркала — в модуль; шаги миграций v1…v6 остаются в истории `core/db.py` и зовут
функции модуля лениво, как шаг v3→v4 сейчас;
- `downloads/` и `sweep_downloads` — фоновая работа модуля (`tick`), `Hub` про неё не знает;
- веб: `shell/DataStatus.tsx` и обновление из Jira — в `web/src/modules/jira/`, оболочка рисует то, что даёт модуль;
`refresh.track(batchId)` после создания проекта — обработчик модуля «проект создан» в вебе.
**Долг — остаётся в ядре до change модуля wiki** (убрать там, «не забыть» — владелец): у проекта `board_id`,
`board_type`, проверка `board_id` в `projects.create`/`validate`, `issues_count` из таблицы `issue` в
`list_projects` и `GET /projects`; параметр доски и колонки «доска | тип доски | задач» в MCP `projects`/
`project_create` (`fmt_projects`); поле доски в `web/src/projects/NewProjectDialog.tsx`. В change модуля wiki проекты
получат поля от модулей (корень вики, число страниц) — тогда же доски и счётчик задач переедут в модуль jira.
Каждое место долга помечается в коде комментарием `# долг hub-modules: проекты → модуль jira (change wiki)` (в TS —
`//`), чтобы найти их одним поиском.
13. **Порядок работ — шагами, каждый зелёный.** Сначала Python (ядро, затем модуль jira по слоям: настройки и проекты →
очередь → API → MCP → CLI), потом веб, потом документация. После каждого шага — весь `uv run pytest` и `make check`
зелёные; тесты меняют только импорты и места, где подставляли зависимости, проверки (утверждения) не меняются.
Добавляются тесты на сборку модулей: пересечение типов команд, ключей настроек или маршрутов — ошибка;
`jira-hub mcp` и `jira-hub mcp jira` регистрируют один и тот же набор инструментов.
## Risks / Trade-offs
- **Объём переноса и конфликты.** Почти каждый файл меняет путь. Пока идёт change, других веток в хабе не открываем.
- **Скрытые зависимости от путей импорта** (ленивые импорты в `db`, подстановки в тестах через `monkeypatch` по имени
модуля). Ловятся набором тестов; поиск по строкам `"jira_hub.` — часть каждого шага.
- **Пути от расположения файла** (проверка сборщика): `WEB_APP = Path(__file__).parent / "web" / "app"` в `api.py`
(сборка Vite кладётся в `src/jira_hub/web/app/` — место сборки не меняется, меняется вычисление пути), `HUB_ROOT =
parents[2]` в `mcp_server.py` (от него — фрагмент `.mcp.json` в ответе `project_create`); `web/scripts/seed-hub.py`
импортирует `jira_hub.db/mirror/settings`; `web/scripts/test-hub.sh`, `golden-from-links.ts`, `e2e/board.spec.ts`
ссылаются на `src/board/__fixtures__` и `src/board/constants.ts`.
- **CLI `mcp <модуль>`**: `argparse` с `choices` даёт свой текст ошибки — модуль проверяется вручную, чтобы текст был
как в спеке.
- **Хаб на Mac mini** после merge — обычная выкатка: `git pull`, `make web`, `kickstart` (за владельцем). Каталог
данных, база и `.mcp.json` не меняются.
## Migration Plan
Миграции данных нет. Выкатка — как любая версия хаба. Откат — предыдущий коммит `main`.
## Decisions made without discussion
- 23.10.2026 — **виды агентов не в этом change** (решение 5). Обоснование: нет второго агента для проверки, миграция
схемы противоречит «без изменения поведения». Цена ошибки: change модуля wiki начнётся с правки ядра очереди и
`agent_status` — +полдня к нему.
- 23.10.2026 — **одна версия схемы на весь хаб** (решение 3). Цена ошибки: если модули понадобится ставить и снимать
независимо — переход на версии по модулю, миграция `setting`.
- 23.10.2026 — **раскладка `core/` + `modules/<имя>/`, без шимов старых импортов** (решения 1, 13). Цена ошибки: при
переименовании пакета — ещё один массовый перенос (он будет в любом случае).
- 23.10.2026 — **`sync_run` и `pacing` — модуля jira** (решения 3, 7). Цена ошибки: если вики понадобится свой журнал
загрузок и паузы — перенос в ядро в change модуля wiki.
- 23.10.2026 — **метка в коде на каждом месте долга проектов** (решение 12). Цена ошибки: нулевая.
- 23.10.2026 — **`jira-hub mcp` без модуля = `jira`** (решение 9), чтобы не трогать `.mcp.json` awp-ca и user scope.
Цена ошибки: нулевая до переименования.
- 23.10.2026 — **R-1: тест `test_core_imports` (Task 3) работает со списком временных исключений.** Обоснование: файлы
`queue`, `api`, `mcp_server`, `cli`, `files` до Task 4–6 ещё импортируют модуль jira (так задумано переносом Task 2),
без списка тест красный сразу; каждая из Task 4–6 сокращает список, к Task 6/8 он пуст. Цена ошибки: граница ядра
проверяется слабее на промежуточных шагах; закрывается Task 8.
- 23.10.2026 — **R-2: метки долга проектов ставит задача, создающая или переносящая код** (Task 3 — `core/projects.py`,
Task 6 — `core/mcp.py`, Task 7 — `NewProjectDialog.tsx`); Task 8 только проверяет поиском. Обоснование: иначе метки
теряются при переносе. Цена ошибки: нулевая.
- 23.10.2026 — **R-3: `jira_utc` и `fill_comment_utc` лежат в `modules/jira/history.py`, порядок ключей агента — ключи
модуля, затем ядра.** Обоснование: в `mirror.py` они дали бы цикл (mirror импортирует history); порядок ключей
сохраняет ответ `/agent/settings`. Цена ошибки: нулевая.
- 23.10.2026 — **R-4: `registry.check` получает маршруты ядра только в Task 5.** Обоснование: до разделения `api.py`
отдельных маршрутов ядра нет. Цена ошибки: пересечение маршрутов проверяется со Task 5, не с Task 3.
- 23.10.2026 — **R-5: `retry_params` и `drop_unresolved` получают команду целиком (`cmd`), а не `params`; `link_types`
читается в `check_confirmed` модуля, а не в `reconcile` ядра; логгер позднего отчёта —
`jira_hub.modules.jira.hooks`.** Обоснование: хукам нужен тип команды; ядро не должно знать настройку `link_types`.
Цена ошибки: если кто-то фильтрует журнал по имени логгера `jira_hub.core.queue` — строка пропадёт; лишний запрос к
`setting` на каждую выданную команду (пренебрежимо).
- 23.10.2026 — **R-6: утверждение `commands.estimate(...) == 152` заменено тестом `queue.estimate` на реальном модуле
jira** (`test_estimate_sums_module_pauses_replaces_commands_estimate`). Обоснование: `commands.estimate` уходит по
плану (Task 4), семантика сохранена. Цена ошибки: нулевая.
- 23.10.2026 — **R-7: `Hub.tick` — сначала уборка файлов ядра, затем `tick` модулей по порядку; метка сбоя работы — атрибут
`label` функции; суточная уборка `downloads/` — работа модуля jira с временем прошлой в `hub.module_state["jira"]`.**
Обоснование: ядро не знает про автообновление и `downloads/`; раньше автообновление шло первым — теперь после
`files.sweep`, который короткий. Цена ошибки: если `sweep` станет долгим, автообновление сдвинется; метка в атрибуте
функции — неявный контракт (можно оформить полем `Module`).
- 23.10.2026 — **R-8: `POST /projects` отдаёт `batch_id` первого модуля, вернувшего пакет (`null`, если никто).** Обоснование:
модуль один; формат ответа прежний. Цена ошибки: при нескольких модулях с пакетами «Новый проект» остальные не видны в
ответе — решит change модуля wiki.
- 23.10.2026 — **R-9: приватные помощники `core/mcp.py` (`client`, `unbound_or_unknown`, `cut`, `value`) стали публичными;
`mcp.run` возвращает код (0 после остановки сервера, 2 при неизвестном модуле), в сообщении перечислены модули с
`mcp_server`; `import` открывает своё соединение в `cli_run`.** Обоснование: их используют инструменты модуля jira из
другого пакета. Цена ошибки: нулевая; тесты, подменяющие эти имена, ссылаются на новые.
- 23.10.2026 — **R-10: тесты MCP-привязки к проекту (`test_mcp_project_option`) заменены двумя тестами из плана
(`test_mcp_default_module`, `test_mcp_unknown_module`).** Обоснование: прежний тест подменял удалённый
`mcp_server.run`. Цена ошибки: если прежний тест проверял что-то сверх, это нужно вернуть (проверяет ревью Task 6).