Andrey Epifancev 29cd569157 chore(openspec): архив hub-modules (#87)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 23:42:27 +04:00

30 KiB
Raw Blame History

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).
  • 23.10.2026 — R-11: веб — SidebarStatus принимает {project, compact}; в HubModule добавлено необязательное TopbarNotice (строка события пакета на узком экране); хуки модулей вызываются через .map по константным спискам; Layout держит hasOverlay булевым; рисуется первое окно, вернувшее pageLocation; запросы jira (qk) разворачивают qk ядра, в api/queries.ts ядра остались настройки, проекты, useCreateProject. Обоснование: плашка данных зависит от проекта, строка события жила в Sidebar через useJiraRefresh. Цена ошибки: нулевая — поведение прежнее.
  • 23.10.2026 — R-12: тест границы ядра (Task 8) ищет типы команд по границе слова, а не по подстроке ensure_. Обоснование: ensure_defaults (настройки ядра) и ensure_ascii (json) — не понятия Jira; подстрока дала бы ложные срабатывания. Докстринги ядра с именами типов команд (ensure_attachment в files.py, api.py) переписываются нейтрально. Цена ошибки: тест границы пропустит новое слово вне списка — список в тесте расширяется вручную.
  • 23.10.2026 — R-13 (I-1 финального ревью): расхождения текста design.md и кода, принятые планом. (а) agent publish остаётся подкомандой ядра (core/cli.py), а не модуля jira: команда публикует релиз агента в release/, который в этом change общий (решение 5); в модуль jira уходит только import. Решения 2 и 9 в части «agent publish — подкоманда модуля» читать с этой поправкой. (б) Вместо одного check_result в CommandHooks четыре хука: check_result (статус и текст ошибки до записи команды), on_result (в той же транзакции), after_result (после фиксации — reconcile у jira), on_late_result (отчёт по уже терминальной команде). Решение 12 («вместо одного check_result — on_result») читать так. (в) Функция files.sweep_downloads остаётся в core/files.py (общая работа с каталогом), в модуль jira переехал вызов — downloads_tick; решение 12 («downloads/ и sweep_downloads — фоновая работа модуля») в этой части уточняется. (г) Сигнатуры: tail(cmds) вместо tail_command(keys), on_project_created(conn, project, author) вместо (conn, project) — автор нужен для заголовка пакета «Новый проект». Цена ошибки: нулевая; при следующем модуле контракт брать из core/modules.py, а не из текста решений.
  • 23.10.2026 — R-14: Minor-находки финального ревью M-1 (неточности ARCHITECTURE.md) и M-2 (две метки долга в core/api.py) исправляются в fix wave; M-3 (оценка пакета читает pacing на каждый тип) откладывается. Обоснование: M-1/M-2 — документация и комментарии; M-3 — N лёгких запросов к локальному SQLite, оптимизация (хук паузы на пакет) при подключении wiki. Цена ошибки: нулевая.