559 lines
57 KiB
Markdown
559 lines
57 KiB
Markdown
## Context
|
||
|
||
Хаб после #87 (main `f7add9b`) — ядро `jira_hub/core/` и модуль `jira_hub/modules/jira/`, схема v6. Контракт модуля —
|
||
`core/modules.py` (`Module`, `CommandHooks`), реестр — `core/registry.py` (сейчас `[jira.MODULE]`).
|
||
|
||
Что в ядре зашито под один агент и под Jira (карта кода 24.10.2026):
|
||
- **Токены.** Один агентский и один клиентский токен в `setting.tokens = {agent, client}` (`core/settings.py:71-76`).
|
||
`Handler._auth` (`core/api.py:406-415`) пускает на любой маршрут со scope `agent` любой агентский токен.
|
||
- **`agent_status`** — одна строка `id = 1` (`core/db.py:52-59`). Её пишут claim, отчёт и отмена (`core/queue.py`, 7
|
||
мест), читают `agent_version()` и `agent_view()`.
|
||
- **`claim`** (`core/queue.py:227-271`) берёт любую команду очереди: SQL `_next` и `_redeliver` не фильтруют ни по
|
||
типу, ни по агенту. «Одна в полёте» тоже общая.
|
||
- **Состояние самообновления** держится в памяти хаба одним объектом `Hub.agent_update` (`core/api.py:47, 91-104`).
|
||
- **Релиз** один: `release/agent.py` и `manifest.json` (`core/release.py`, `paths.release_dir()`), команда
|
||
`jira-hub agent publish PATH`.
|
||
- **Настройки агента.** `/agent/settings` отдаёт ключи всех модулей и ключи ядра `min_agent_version`,
|
||
`settings_version` (`core/settings.py:19, 36-41`). Ключ `board_id` в `AGENT_KEYS` jira мёртвый.
|
||
- **Реле** (`core/relay.py`) про агентов не знает: оно пересылает метод, путь и `Authorization` в свой HTTP. Имена
|
||
файлов запросов (`uuid4().hex`) задаёт агент. Два агента в одной папке `relay_dir` не мешают друг другу.
|
||
- **Проекты.** Колонки `board_id`, `board_type` лежат в таблице ядра `project`. `issues_count` считается в ядре по
|
||
таблице модуля `issue`. Поле доски жёстко вшито в `POST /projects`, MCP `project_create`/`projects` и окно «Новый
|
||
проект». Модуль может только поставить пакет при создании проекта (`on_project_created`). Все такие места помечены
|
||
`долг hub-modules` (решение 12 hub-modules).
|
||
- **MCP `agent_status`** — инструмент модуля jira. `GET /agent` отдаёт один объект.
|
||
- **Ориентир для модуля wiki** — модуль jira: `modules/jira/fetch.py` (скачивание вложения по запросу командой
|
||
`fetch_attachment`), `refresh.maybe_auto_refresh` (фоновый пакет чтения), `core/files.py` (хранилище по sha256,
|
||
`MAX_SIZE` 10 МБ, уборка по `file_refs`).
|
||
|
||
**Eva.** evaTeam, JSON-RPC 2.2 по адресу `<eva>/api/?m=<метод>`, заголовок `Authorization: Bearer <токен>`, тело
|
||
`{jsonrpc: "2.2", method, callid, kwargs}`. Используются методы:
|
||
- `CmfDocument.get(filter=[["code","==",<код>]], fields=[…])`;
|
||
- `CmfDocument.list(filter, fields, slice=[from,to])`, страницы по 200.
|
||
|
||
Поля списка: `id`, `code`, `name`, `parent`, `tree_parent`, `project`, `orderno`. Текст страницы — HTML в поле `text`.
|
||
Дерево строится локально по `parent`/`tree_parent` из списка всех документов проекта Eva. Фильтр `parent.code` API не
|
||
принимает (-32001). Если у страницы нет `project`, дерево обходится в ширину через фильтр `tree_parent.code`.
|
||
Картинки и файлы — ссылки `<img src>`/`<a href>` на тот же хост с путём `/(files?|attachments?|download|media|upload)`.
|
||
Всё это обкатано в `eva_wiki_export.py` (корень workspace).
|
||
|
||
Реальные форматы ссылок взяты из выгрузки ТЗ АРМ КА (`awp-ca/docs/source/tz/`, корень DOC-019855, 22 страницы, 388 КБ
|
||
Markdown; сырого HTML в выгрузке нет):
|
||
- **Ссылка на страницу** — относительный адрес без хоста и без ведущей `/`: `project/Document/DOC-019834`.
|
||
- **Картинка** — `/files/obj/CmfDocument/CmfDocument%3A<3 символа>/CmfDocument%3A<uuid>/<имя в URL-кодировке>?version=1`.
|
||
- **Файл, прикреплённый к странице** (xlsx, dbml), выглядит как миниатюра `…/<имя файла>.meta/thumbnail.jpg?version=1`,
|
||
значок `static/attach-file.png`, имя файла и дата текстом. Ссылка на сам файл в выгрузке не сохранилась: её,
|
||
вероятно, съел markdownify. Адрес файла — адрес миниатюры без `.meta/thumbnail.jpg`.
|
||
- **`static/…`** — ресурсы интерфейса Eva, не вложения.
|
||
|
||
Объём настоящего дерева невелик (десятки страниц), но решения ниже рассчитаны и на сотни. Есть ли у документа дата изменения и история версий,
|
||
неизвестно: публичной документации API нет, Eva доступна только из сети банка.
|
||
|
||
Владелец (brainstorming 23–24.10.2026): см. proposal, «Why».
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- проект хаба получает корень Eva, дерево его страниц лежит у хаба Markdown-копией;
|
||
- Claude ищет и читает вики проекта через отдельный MCP-сервер;
|
||
- веб показывает дерево и страницы проекта;
|
||
- eva-agent — второй агент; хаб различает агентов;
|
||
- в ядре не остаётся Jira (метки `долг hub-modules` сняты).
|
||
|
||
**Non-Goals:** см. proposal. Сверх них — правка уже скачанной копии руками: копия перезаписывается синхронизацией.
|
||
|
||
## Decisions
|
||
|
||
### 1. Вид агента = модуль с командами
|
||
|
||
У каждого модуля, у которого есть `commands`, свой агент. Имя вида совпадает с именем модуля: агент `jira` —
|
||
jira-agent, агент `wiki` — eva-agent. Отдельного справочника видов нет.
|
||
|
||
Почему: модуль уже владеет своими типами команд, настройками для агента и маршрутами `/agent/<…>` своего модуля.
|
||
Агент без модуля бессмыслен, модуль с командами без агента тоже.
|
||
|
||
### 2. Токен агента определяет его вид
|
||
|
||
`setting.tokens` становится `{client, agents: {<модуль>: <токен>}}`. Миграция v7 переносит прежний `tokens.agent` в
|
||
`tokens.agents.jira`, поэтому jira-agent v14 работает со своим прежним токеном. `ensure_defaults` создаёт недостающий
|
||
токен для каждого модуля с командами. `jira-hub tokens` печатает клиентский токен и токены агентов с именем модуля.
|
||
|
||
`Handler._auth("agent")` находит модуль по токену (`hmac.compare_digest` с каждым) и кладёт его в контекст запроса.
|
||
Дальше:
|
||
- маршруты ядра `/agent/*` работают от имени этого агента;
|
||
- маршруты модуля со scope `agent` (у jira это `/agent/state`, `/agent/history`, `/agent/known-keys`) пускают только
|
||
агента своего модуля, токен другого агента получает 401.
|
||
|
||
Клиентский токен по-прежнему один.
|
||
|
||
### 3. `agent_status` по агентам
|
||
|
||
Миграция v7 пересоздаёт таблицу с ключом `agent TEXT PRIMARY KEY` и переносит строку `id = 1` в `agent = 'jira'`.
|
||
Колонку `last_jira_call_at` переименовываем в `last_call_at`. Протокол `claim` (поле `last_jira_call_at` в теле от
|
||
jira-agent) не меняется: хаб читает и `last_call_at`, и `last_jira_call_at`.
|
||
|
||
Строка агента появляется при старте хаба, по одной на модуль с командами. Все места `WHERE id = 1` становятся
|
||
`WHERE agent = ?`. `queue.agent_version(conn)` превращается в `agent_version(conn, agent)`. Модуль jira в `fetch.py`
|
||
спрашивает версию агента `jira`.
|
||
|
||
### 4. `claim` выдаёт только команды своего модуля
|
||
|
||
`queue.claim(conn, agent, …)`. `_next` и `_redeliver` берут только команды, у которых `type` входит в
|
||
`CommandHooks.types` модуля `agent`. Пакет и так однородный: «команды разных модулей в одном пакете» отвергаются с
|
||
#87.
|
||
|
||
«Одна команда в полёте» считается на агента. Агенты работают параллельно: долгая синхронизация вики не задерживает
|
||
`ensure_*` в Jira. `current_command_id` хранится в строке своего агента. Копия базы перед первой пишущей командой
|
||
пакета остаётся как есть: команды wiki в этом change только читают (`read_types`).
|
||
|
||
### 5. Самообновление по агентам
|
||
|
||
- **Релиз:** `release/<модуль>/agent.py` и `manifest.json`. При старте хаб один раз переносит прежние
|
||
`release/agent.py` и `release/manifest.json` в `release/jira/`. Переносит, только если `release/jira/` ещё нет;
|
||
ошибка переноса пишется в лог, хаб стартует.
|
||
- **Публикация:** `jira-hub agent publish [--module <модуль>] PATH`, по умолчанию `jira`. Неизвестный модуль или
|
||
модуль без команд — код 2, как у `mcp`. Проверки и вывод прежние.
|
||
- **Ответы агенту:** `claim` и `GET /agent/release` отдают релиз вида, к которому относится токен.
|
||
- **Память хаба:** `Hub.agent_update` становится словарём по агентам.
|
||
- **`GET /agent?module=<модуль>`**, без параметра — `jira`, как сейчас. Добавляется `GET /agents` — список всех
|
||
агентов в том же формате с полем `module`.
|
||
- **MCP:** `agent_status` остаётся у jira, у wiki есть свой (решение 12).
|
||
|
||
### 6. Настройки агента по модулю
|
||
|
||
`GET /agent/settings` отдаёт ключи ядра и `agent_keys` только своего модуля: jira-agent не видит ключей вики и
|
||
наоборот. `settings_version` остаётся общим счётчиком. Лишний перезапрос настроек у eva-agent после правки настроек
|
||
jira безвреден.
|
||
|
||
`min_agent_version` переезжает из ядра в модуль jira: имя ключа и значение прежние, для jira-agent ничего не меняется.
|
||
Модуль задаёт ключ своей минимальной версии полем `Module.agent_min_version_key`; у wiki это `wiki_min_agent_version`,
|
||
по умолчанию 1. `agent_view(conn, agent)` берёт минимальную версию по этому ключу. Мёртвый ключ `board_id` из
|
||
`AGENT_KEYS` jira удаляется.
|
||
|
||
Требование для jira-agent v14: ответ `/agent/settings` для токена jira совпадает с нынешним, кроме ключа `board_id`.
|
||
Сейчас он всегда `null`, агент его не читает (проверить по `jira-agent/src/agent.py` при реализации; если читает —
|
||
ключ оставить со значением `null`).
|
||
|
||
### 7. Реле не меняется
|
||
|
||
Одна общая папка `relay_dir` на обоих агентов. Запросы различаются токеном, имена файлов — `uuid4().hex` агента,
|
||
`/agent/claim` у обоих один и тот же путь. Уборка `work/` при старте хаба общая, как сейчас: потерянный запрос агент
|
||
повторит. Подпапки на агента не вводим.
|
||
|
||
### 8. Проекты: ядро — метка, название, дата; остальное — модули
|
||
|
||
- **Ядро.** Таблица `project` остаётся с `label`, `name`, `created_at`. Миграция v7 переносит `board_id` и
|
||
`board_type` в таблицу модуля jira `jira_project(label PK → project, board_id, board_type)` и удаляет колонки из
|
||
`project` (через пересоздание таблицы, как в прежних миграциях).
|
||
- **Число задач `issues_count`** считает модуль jira.
|
||
- **Контракт `Module` получает поля проекта:**
|
||
- `project_fields` — описание полей модуля: имя, тип, проверка, можно ли менять после создания, подпись и подсказка
|
||
для веба и MCP;
|
||
- `project_view(conn, labels) -> {label: {поле: значение}}` — значения для списка проектов, включая вычисляемые
|
||
(`issues_count`, `pages_count`, `synced_at`);
|
||
- `on_project_created(conn, project, fields, author)` — запись своих полей и пакет, как сейчас;
|
||
- `on_project_updated(conn, project, fields, author)` — правка своих изменяемых полей и пакет.
|
||
- **`registry.check`** падает, если имя поля проекта есть у двух модулей или совпадает с полем ядра.
|
||
- **`GET /projects`** отдаёт плоские объекты: поля ядра и поля всех модулей. Набор и значения полей jira прежние:
|
||
`board_id`, `board_type`, `issues_count`.
|
||
- **`POST /projects` `{label, name, author?, <поля модулей>}`.** Неизвестное поле — 422. Каждый модуль проверяет свои
|
||
поля до записи; ошибка — 422 с именем поля, ничего не записано. `batch_id` в ответе прежний: пакет первого модуля,
|
||
вернувшего пакет. Решение R-8 hub-modules остаётся.
|
||
- **Новый `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}`.** Неизменяемое поле — 422 «<поле> после
|
||
создания не меняется». Ответ — `{project, batch_ids: [...]}`. Доска jira остаётся неизменяемой, как сейчас.
|
||
Корень вики изменяемый: у уже заведённых проектов (AWPCA и др.) его надо проставить потом.
|
||
- **MCP.** `core/mcp.register_core_tools(mcp, module)` строит `projects` и `project_create` сервера модуля из его
|
||
`project_fields`. Сервер jira отдаёт прежние названия, параметры и описания `project_create` и прежние колонки
|
||
`projects` (эталоны `tests/fixtures/baseline/mcp_tools.expected.json` не меняются). У сервера wiki — свои колонки и
|
||
параметр `wiki_root`.
|
||
- **Веб.** `HubModule.projectFields` — поля модуля в окне «Новый проект». Поле доски переезжает в модуль jira, текст и
|
||
подсказка прежние.
|
||
|
||
Метки `долг hub-modules` в ядре снимаются все. Тест `test_core_has_no_jira_terms` больше не делает для них исключения.
|
||
|
||
### 9. Модуль wiki: данные
|
||
|
||
Таблицы модуля (схема v7, рядом с миграцией решений 2, 3, 8):
|
||
- **`wiki_project`**: `label` PK → project, `root_code` (`DOC-<цифры>`), `root_title`, `synced_at` (последняя успешная
|
||
синхронизация), `pages_count`, `sync_error` (текст последней неудачи или `NULL`).
|
||
- **`wiki_page`**:
|
||
- ключ: `label`, `code`; PK `(label, code)`;
|
||
- поля: `eva_id`, `title`, `parent_code`, `orderno`, `depth`, `path` (каталог страницы относительно
|
||
`wiki/<label>/`), `url` (адрес страницы в Eva от агента), `size` (символов Markdown), `synced_at`.
|
||
|
||
Страница в двух проектах — две строки. Деревья проектов независимы.
|
||
- **`wiki_page_fts`** — FTS5 `(title, body)` с `content` по Markdown-тексту и токенизатором
|
||
`unicode61 remove_diacritics 2`. Используется только для поиска. Сам текст страниц живёт в файлах.
|
||
- **`wiki_attachment`**: `id` INTEGER PK, `label`, `page_code`, `url` (абсолютный адрес в Eva), `name`, `state`
|
||
(`none` | `fetching` | `ready` | `failed`), `file` (sha256 в `files/`), `size`, `error`, `fetched_at`. Одна строка
|
||
на пару «адрес + страница». Адрес, который встречается на нескольких страницах, даёт несколько строк, но файл в
|
||
`files/` хранится один (sha256).
|
||
- **`wiki_sync_stage`** — временные пачки страниц текущей синхронизации, ключ `(command_id, code)`. Очищается при
|
||
завершении команды в любом статусе.
|
||
|
||
`file_refs` модуля возвращает `file` всех `wiki_attachment` в состоянии `ready`, поэтому уборка `files/` их не
|
||
трогает, пока строка жива.
|
||
|
||
### 10. Синхронизация `wiki_sync`
|
||
|
||
**Постановка.** Команда `wiki_sync {label, root}` — тип чтения (`read_types`), пакет с `project = <label>`. Ставят её:
|
||
- MCP `sync` и кнопка «Обновить» в вебе (`POST /wiki/sync {project}`);
|
||
- привязка или смена корня (`on_project_created`/`on_project_updated`);
|
||
- фоновая работа модуля (`Module.tick`): по каждому проекту с корнем, если с `synced_at` (или с последней
|
||
неудачи) прошло больше `wiki_auto_sync_hours` (настройка модуля, по умолчанию 24, `0` — выключено) и у проекта нет
|
||
пакета `wiki_sync` в работе.
|
||
|
||
`POST /wiki/sync` и фоновая работа не ставят пакет, пока предыдущий `wiki_sync` проекта в работе: ответ 409 с id
|
||
пакета, MCP показывает его состояние.
|
||
|
||
**Смена и сброс корня** не упираются в это правило. `PATCH` отменяет ещё не выданные (`queued`) пакеты `wiki_sync`
|
||
проекта. При новом корне он ставит новый пакет; уже выданный агенту пакет доживает. Отчёт `done` по команде, у которой
|
||
`params.root` не равен текущему корню проекта (сменён или сброшен), отбрасывается: копия и данные не меняются, команда
|
||
`done` с пометкой в `result.discarded = "корень сменился"`. Типичный случай: корень проставили до установки eva-agent,
|
||
потом исправили опечатку.
|
||
|
||
**Агент** (контракт; решения внутри агента — его change):
|
||
1. Берёт корень `CmfDocument.get` и все страницы поддерева, как `collect_subtree` в `eva_wiki_export.py`.
|
||
2. По каждой странице берёт `text`.
|
||
3. Разбирает ссылки страницы. Это делает агент, а не хаб: только он знает `eva.base_url`.
|
||
Относительные адреса относятся к хосту Eva (`urljoin` с `eva.base_url`).
|
||
- **Вложение** — `<img src>`/`<a href>` на хост Eva с путём `/files/…`.
|
||
- Миниатюра `<адрес>.meta/thumbnail.jpg` — это файл по `<адрес>`; одно вложение, а не два.
|
||
- Адрес файла и его миниатюры в одной странице — одно вложение.
|
||
- `static/…` (значки интерфейса) не вложение: ссылка удаляется из текста.
|
||
- **Ссылка на страницу** — ссылка на хост Eva с кодом `DOC-<цифры>` в адресе (`project/Document/DOC-…`).
|
||
- По каждой ссылке агент шлёт:
|
||
- `ref` — значение атрибута ровно как в HTML, по нему хаб находит место замены;
|
||
- `url` — абсолютный адрес (только у вложения);
|
||
- `name` — `alt` или `title` картинки, текст ссылки, иначе имя файла из пути;
|
||
- `code` — у ссылки на страницу.
|
||
4. Отправляет страницы пачками `POST /agent/wiki/pages {command_id, pages: [{code, eva_id, title, parent_code,
|
||
orderno, url, html, attachments: [{ref, url, name}], links: [{ref, code}]}]}`. В пачке не больше 50 страниц и не
|
||
больше 4 МБ тела.
|
||
5. Отчитывается `done` с `result = {root_title, codes: [<все коды дерева в порядке обхода>], eva_calls}`.
|
||
6. В `wiki_fetch` скачивает только адреса на хосте `eva.base_url`, иначе `failed` «чужой хост». Так токен Eva не уходит
|
||
на чужой адрес.
|
||
|
||
**Хаб.**
|
||
- **Приём пачки.** HTML каждой страницы сразу переводится в Markdown, ссылки заменяются по `ref`. Вместо `id`
|
||
вложения ставится заглушка, её заполняет применение. Результат кладётся в `wiki_sync_stage`, копия не меняется.
|
||
Перевод идёт вне транзакции записи (markdownify на десятках МБ занимает секунды) и не держит блокировку базы, в
|
||
которой ждут `claim` jira-agent и веб.
|
||
- **Отчёт `done`.** Сначала, вне транзакции, Markdown-копия собирается из стадии в `wiki/.<label>.new/` с
|
||
предварительно выделенными `id` вложений. Затем одной короткой транзакцией (`CommandHooks.on_result`):
|
||
- каталог атомарно подменяется: переименование, старая копия удаляется после подмены;
|
||
- `wiki_page` и FTS пишутся заново;
|
||
- для `wiki_attachment` при той же паре «адрес + страница» сохраняются `id` и скачанный файл, новые пары
|
||
добавляются, пропавшие удаляются;
|
||
- заполняются `wiki_project.synced_at`, `pages_count` и `root_title`, `sync_error` обнуляется.
|
||
- Код из `codes`, для которого не пришла пачка, — команда `failed` «нет страницы <код>», копия прежняя.
|
||
- Отчёт `failed` пишет `sync_error`, копия прежняя.
|
||
- Повторная выдача чистит `wiki_sync_stage` команды.
|
||
|
||
**Раскладка копии** (по образцу `eva_wiki_export.py`):
|
||
- каталог страницы — `NN_<безопасное название>/` внутри каталога родителя; `NN` — позиция среди соседей, у корня
|
||
номера нет;
|
||
- в каталоге `index.md` с frontmatter `code`, `title`, `url`, `synced_at`, затем `# <title>` и текст;
|
||
- в корне `_tree.json` — плоский список `{code, title, depth, path}`;
|
||
- каталоги 0700, как остальные данные хаба.
|
||
|
||
Безопасное название — как `safe_name` скрипта; при совпадении у соседей хаб добавляет `_2`, `_3`.
|
||
|
||
**HTML → Markdown** делает хаб: зависимость `markdownify`, ATX-заголовки. Ссылки (классифицирует агент, см. выше):
|
||
- **картинка или файл Eva** заменяется на `[📎 <имя>](attachment:<id>)`:
|
||
по `id` инструмент `attachment` и веб скачивают файл;
|
||
- **ссылка на страницу Eva с кодом `DOC-<цифры>`** заменяется на `[<текст>](page:<код>)`: по коду её открывает
|
||
инструмент `page`, а в вебе она ведёт на страницу вики, если та есть в дереве проекта;
|
||
- прочие ссылки остаются как есть.
|
||
|
||
**Объём.** Перекачка всего дерева при каждой синхронизации — осознанно (история и частичная перекачка — позже). Темп —
|
||
настройка модуля для агента `wiki_pacing` (пауза между вызовами Eva, по умолчанию 0.2 с).
|
||
|
||
### 11. Вложения по запросу `wiki_fetch`
|
||
|
||
`POST /wiki/attachments/{id}/fetch` (веб и MCP) ставит пакет из одной команды `wiki_fetch {attachment_id, url}`. Это
|
||
тип чтения, по образцу `modules/jira/fetch.request_fetch`: одна транзакция, `state = fetching`. Пока скачивание идёт,
|
||
повторный запрос отвечает текущим состоянием, а не новым пакетом.
|
||
|
||
**Агент:**
|
||
- скачивает адрес с токеном Eva;
|
||
- больше `MAX_SIZE` ядра (10 МБ, по `Content-Length` или по факту чтения) — `failed` «файл больше 10 МБ — откройте в
|
||
Eva»;
|
||
- иначе `POST /agent/files` и отчёт `done {file, size}`.
|
||
|
||
**Хаб** проверяет файл в `files/` и ставит `state = ready`. Если файла нет или размер чужой — `failed`.
|
||
|
||
**Выход:**
|
||
- `GET /wiki/attachments/{id}` — состояние;
|
||
- `GET /wiki/attachments/{id}/content` — сам файл;
|
||
- MCP `attachment` кладёт копию в `downloads/<id>/<имя>` (общая папка ядра, уборка 30 дней). Имена каталогов jira и
|
||
wiki не пересекаются: каталог wiki — `downloads/wiki-<id>/`.
|
||
|
||
### 12. MCP вики
|
||
|
||
**Запуск.** `jira-hub mcp wiki [--project <метка>]`, имя сервера `jira-hub-wiki` (переименование в sdlc-hub — позже,
|
||
вместе со всем). Регистрация в awp-ca — новая запись `jira-hub-wiki` в `.mcp.json` с `--project AWPCA`; её делает
|
||
владелец. Хаб выключен — та же подсказка, что у jira.
|
||
|
||
**Инструменты** (короткие имена: в Claude они видны как `mcp__jira-hub-wiki__<имя>`):
|
||
|
||
| Инструмент | Что делает |
|
||
|---|---|
|
||
| `projects` | проекты: метка, название, корень, страниц, синхронизация |
|
||
| `project_create` | `label`, `name`, `wiki_root` (необязательно) |
|
||
| `root_set` | `root` — корень текущего проекта (`PATCH /projects/{label}`) |
|
||
| `tree` | `code` (необязательно, по умолчанию корень), `depth` (по умолчанию 2) — дерево с кодами и размерами |
|
||
| `page` | `code` или `title` (точное совпадение, иначе до 10 кандидатов), `offset` — Markdown страницы с шапкой (код, путь в дереве, адрес Eva, дата синхронизации), длинный текст — частями по 20 000 символов |
|
||
| `search` | `query`, `limit` (по умолчанию 10) — страницы с фрагментами, FTS5; слова запроса ищутся по префиксу |
|
||
| `attachment` | `id` — скачать вложение (как `attachment` у jira: ждёт до 60 с, затем путь к копии) |
|
||
| `sync` | синхронизировать сейчас, `wait` (по умолчанию нет) |
|
||
| `status` | корень, последняя синхронизация, ошибка, пакет в работе, агент `wiki` (онлайн, версия, публикация, обновление — как `agent_status` jira) |
|
||
|
||
Инструменты, кроме `projects` и `project_create`, требуют привязки к проекту, как у jira. Без корня они отвечают
|
||
«у проекта <метка> нет корня вики: root_set <DOC-…>». Корень есть, а синхронизаций ещё не было — «вики проекта ещё
|
||
не синхронизирована: sync».
|
||
|
||
### 13. Веб
|
||
|
||
Модуль `web/src/modules/wiki/`:
|
||
- **страница «Вики»** `/app/wiki?project=<метка>`, пункт боковой панели;
|
||
- **дерево** слева: свёрнутое, раскрывается по клику;
|
||
- **страница** справа: Markdown, без сырого HTML;
|
||
- **поиск** над деревом;
|
||
- **кнопка «Обновить»** и плашка хода синхронизации в боковой панели — как у jira;
|
||
- **ссылки:** `page:` ведёт на страницу вики, `attachment:` скачивает файл (запрос, опрос, `api.blob`, как вложения в
|
||
карточке задачи);
|
||
- **без корня** — форма «Корень вики (DOC-…)» (`PATCH /projects`);
|
||
- **окно «Новый проект»** получает поле «Корень вики (необязательно)» через `projectFields`.
|
||
|
||
Маршруты HTTP модуля (клиентский токен):
|
||
- `GET /wiki/tree?project=` — дерево с кодами;
|
||
- `GET /wiki/pages/{code}?project=` — `{code, title, path, url, synced_at, markdown, attachments}`;
|
||
- `GET /wiki/search?project=&q=&limit=`;
|
||
- `POST /wiki/sync`;
|
||
- `GET /wiki/status?project=`;
|
||
- вложения из решения 11.
|
||
|
||
Отрисовка Markdown — `react-markdown` с `remark-gfm` (таблицы), без `rehype-raw`.
|
||
|
||
### 14. eva-agent
|
||
|
||
Репозиторий `aep/eva-agent` (создан владельцем пустым 24.10.2026), каталог `jira-bridge/eva-agent/`. Обвязка
|
||
подключается `adopt-project.sh`. Решения агента — его design-only change. Хаб фиксирует только контракт:
|
||
- **Стек:** Python 3.9 на stdlib, одним файлом `src/agent.py` со строкой `AGENT_VERSION = <целое>`. Запускатель
|
||
`src/run.py` — копия запускателя jira-agent. Имена `agent.py`, `update.json` и протокол самообновления прежние, иначе
|
||
`run.py` не подходит.
|
||
- **Конфиг `agent.json`:** `hub_url` (`folder:<путь>` или http), `hub_token` — токен агента `wiki` из `jira-hub tokens`,
|
||
`eva: {base_url, token}`, `verify_ssl`, `ca_bundle`, `timeout`, `poll_seconds`, `auto_update`. Токен Eva — только
|
||
здесь.
|
||
- **Протокол с хабом** — как у jira-agent: `claim`, `GET /agent/settings`, `GET /agent/commands/{id}`, результат,
|
||
`GET /agent/release`, `POST /agent/files`; плюс `POST /agent/wiki/pages`.
|
||
- **Команды:** `wiki_sync`, `wiki_fetch` (решения 10, 11). Обе только читают Eva: журнал на диске нужен для повторного
|
||
отчёта, но «проверь, потом сделай» не нужно.
|
||
- **Из jira-agent копируются, без общей библиотеки:** HTTP-слой, `FolderTransport`, `HubClient` (без jira-маршрутов),
|
||
`Journal`, `Runner`, `Updater`, `main`. Решение владельца 23.10.2026: однофайловый агент на виртуалке.
|
||
- **Запуск** на той же виртуалке, отдельным каталогом и процессом со своей блокировкой. Первую установку делает
|
||
владелец руками, дальше агент обновляется публикацией `jira-hub agent publish --module wiki`.
|
||
|
||
### 15. Миграция v7 одним шагом
|
||
|
||
Один шаг `MIGRATIONS` 6 → 7 с копией базы перед ним (механизм прежний):
|
||
1. `tokens.agent` → `tokens.agents.jira`, токен `agents.wiki` создаётся.
|
||
2. `agent_status` пересоздаётся по агентам (решение 3).
|
||
3. `project` теряет `board_id`/`board_type`, появляется `jira_project` с перенесёнными значениями.
|
||
4. Создаются таблицы wiki (решение 9).
|
||
|
||
Перенос `release/` в `release/jira/` — не миграция базы, а шаг старта хаба (решение 5).
|
||
|
||
### 16. Порядок работ
|
||
|
||
1. Ядро: виды агентов (решения 1–7) — jira-agent v14 работает. Живой проверки посередине нет: launchd запускает хаб
|
||
из основного checkout, а частичная v7 на живой базе сломала бы последующие шаги миграции. Совместимость с v14
|
||
проверяется тестом: снимок схемы v6 с досками и токеном → миграция → `claim`, `GET /agent/settings`,
|
||
`/agent/state` с прежним токеном, формой и ответами v14. Живая проверка — после merge.
|
||
2. Ядро: поля проектов (решение 8) и снятие долга.
|
||
3. Модуль wiki в хабе: данные, синхронизация с имитацией агента в тестах, вложения, HTTP, MCP.
|
||
4. Веб.
|
||
5. eva-agent в своём репозитории: отдельный PR и отдельный план, после merge хаба.
|
||
|
||
Шаги 1–4 — этот change и один PR в jira-hub. Шаг 5 — PR в eva-agent и его design-only change.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Миграция v7 трогает живые данные:** токены, статус агента, доски. Защищают копия базы перед миграцией и тест
|
||
миграции на снимке v6 (`tests/fixtures/` из настоящей схемы v6 с досками AWPCA = 4216 scrum). Если jira-agent после
|
||
выкатки получает 401, это ошибка переноса токена; откат — копия базы.
|
||
- **jira-agent v14 видит другой `/agent/settings`** (решение 6). Это проверяется тестом на равенство ответа
|
||
эталону v6, кроме `board_id`.
|
||
- **FTS5 в Python от uv.** Если модуль `fts5` в SQLite не собран, поиск сломается. Тест на старте модуля проверяет
|
||
`CREATE VIRTUAL TABLE … USING fts5`; если не вышло — поиск идёт через `LIKE` по файлам и в логе одно
|
||
предупреждение.
|
||
- **Русская морфология.** FTS5 ищет по префиксу слова, «статусы» не найдёт по «статус…» в другой форме корня.
|
||
Принимаем; MCP подсказывает искать по корню слова.
|
||
- **Большое дерево.** Сотни страниц по несколько сотен КБ HTML — пачки по 50 страниц и 4 МБ. Стадия хранится в базе;
|
||
копия базы перед командой не делается (чтение).
|
||
- **Атомарность копии.** Подмена каталога переименованием. Если хаб упал между удалением старой и переименованием
|
||
новой, при старте модуль доводит подмену по наличию `wiki/.<label>.new/`.
|
||
- **Агент `wiki` есть в хабе до установки eva-agent.** `status` и `GET /agents` показывают его офлайн с подсказкой
|
||
«eva-agent не выходил на связь». Пакеты `wiki_sync` стоят в очереди и не мешают jira (решение 4). Фоновая
|
||
синхронизация не ставит новый пакет, пока предыдущий не выдан.
|
||
|
||
## Migration Plan
|
||
|
||
- Хаб: merge → `make web` → kickstart (владелец) → `/mcp` reconnect. jira-agent не обновляется.
|
||
- После kickstart:
|
||
- проверить `agent_status` jira (v14 онлайн);
|
||
- проставить корни проектам (`root_set` или веб);
|
||
- добавить `jira-hub-wiki` в `.mcp.json` awp-ca.
|
||
- eva-agent: отдельный PR и ручная установка на виртуалке владельцем; дальше публикация `--module wiki`.
|
||
- Откат хаба: предыдущий коммит и копия базы v6 из `backups/`.
|
||
|
||
## Decisions made without discussion
|
||
|
||
Принятые агентом в brainstorming 24.10.2026; владелец видит их при ревью документа.
|
||
|
||
1. **Вид агента = имя модуля** (`wiki`, а не `eva`). Обоснование: см. решение 1. Цена ошибки — переименование ключа в
|
||
`tokens.agents` и `agent_status`.
|
||
2. **Агенты работают параллельно, «одна в полёте» — на агента** (решение 4). Обоснование: синхронизация вики не должна
|
||
держать Jira. Цена ошибки — правка одного условия в `claim`.
|
||
3. **Реле общее, без подпапок** (решение 7). Обоснование: реле агентов не различает и не должно. Цена ошибки — подпапки
|
||
и вторая настройка, если папка станет узким местом.
|
||
4. **Корень вики меняется после создания, доска — нет** (решение 8). Обоснование: у живых проектов корня нет. Цена
|
||
ошибки — запрет в описании поля.
|
||
5. **Перекачка всего дерева каждый раз** (решение 10). Обоснование: дата изменения в Eva неизвестна, история отложена.
|
||
Цена ошибки — нагрузка на Eva при большом дереве; лечится `wiki_pacing` и частичной перекачкой позже.
|
||
6. **Текст страниц в файлах, в базе — индекс FTS** (решение 9). Обоснование: копия — главный артефакт, база — для
|
||
поиска. Цена ошибки — двойной источник; синхронизация пишет оба в одной транзакции и подмене.
|
||
7. **Ссылки `attachment:<id>` и `page:<код>` в Markdown** (решение 10). Обоснование: по ним работают инструменты и веб.
|
||
Цена ошибки — формат ссылок в файлах; меняется со следующей синхронизацией.
|
||
8. **Предел вложения 10 МБ — общий `MAX_SIZE` ядра** (решение 11). Обоснование: реле передаёт файл base64 в JSON.
|
||
Цена ошибки — большие файлы Eva только по ссылке в Eva.
|
||
9. **Имя MCP-сервера `jira-hub-wiki`, инструменты без префикса** (решение 12). Цена ошибки — правка `.mcp.json` в
|
||
awp-ca.
|
||
10. **Отрисовка Markdown — `react-markdown` + `remark-gfm`** (решение 13). Цена ошибки — замена библиотеки на странице
|
||
вики.
|
||
11. **Ежедневная синхронизация — фоновой работой хаба** (решение 10). Обоснование: прецедент — автообновление
|
||
зеркала jira. Цена ошибки — отключение настройкой `wiki_auto_sync_hours = 0`.
|
||
12. **Одна живая проверка — после merge, без промежуточной** (решение 16; builder check). Обоснование: частичная v7 на
|
||
живой базе ломает последующие шаги, а launchd смотрит в основной checkout. Цена ошибки — несовместимость с v14
|
||
всплывёт после выкатки; страхуют тест миграции на снимке v6 и откат на копию базы.
|
||
13. **HTML → Markdown при приёме пачки, вне транзакции** (решение 10; builder check). Обоснование: перевод десятков МБ
|
||
под `BEGIN IMMEDIATE` блокирует `claim` jira-agent и веб. Цена ошибки — стадия в базе крупнее (Markdown вместо
|
||
HTML — обычно меньше).
|
||
14. **Ссылки классифицирует агент, хаб заменяет по `ref`** (решение 10; builder check). Обоснование: адрес Eva знает
|
||
только агент, хабу он не нужен. Цена ошибки — контракт `pages` меняется в обоих репозиториях.
|
||
15. **`wiki_fetch` только на хост `eva.base_url`** (решение 10; builder check). Обоснование: токен Eva не уходит на
|
||
чужой адрес. Цена ошибки — вложение на другом хосте открывается только ссылкой.
|
||
16. **Смена корня отменяет невыданные `wiki_sync`, поздний `done` старого корня отбрасывается** (решение 10; builder
|
||
check). Обоснование: корень правят, пока eva-agent ещё не установлен; копия старого дерева не должна встать под
|
||
новый корень. Цена ошибки — лишняя синхронизация.
|
||
17. **Тестовые HTML-страницы — синтетические, по реальным форматам адресов из `awp-ca/docs/source/tz/`** (Context). Сам
|
||
текст ТЗ в фикстуры не копируется. Обоснование: сырого HTML Eva нет, а содержимое ТЗ банка не место в репозитории
|
||
хаба. Цена ошибки — разметку вокруг ссылок (особенно у прикреплённых файлов) уточнять по первой живой
|
||
синхронизации; агент и хаб правятся вместе.
|
||
|
||
Решения контроллера при исполнении плана (2026-10-24, SDD).
|
||
|
||
18. **R-1.** 2026-10-24. Строки `agent_status` для `registry.agent_modules()` вставляются `INSERT OR IGNORE` при каждом
|
||
`db.migrate`, а не только в `MIGRATIONS[6]`. Обоснование: wiki-агент появляется после схемы 7 (Task 5), иначе у него
|
||
нет строки статуса. Цена ошибки: падение `claim` wiki; правка одного места.
|
||
19. **R-2.** 2026-10-24. Ответ `POST /projects` — по спеке `hub-api`: `{project, batch_id}`, `batch_id` — пакет первого
|
||
модуля, вернувшего пакет; `batch_ids` в ответе POST не добавляем (исполнитель Task 3 добавил, ревью отклонило как
|
||
лишнее). Цена ошибки: клиент не видит второй пакет без `GET /batches`.
|
||
20. **R-3.** 2026-10-24. `test_core_has_no_module_terms` ищет `eva` по границе слова (`\beva\b`, `eva-agent`), а `wiki_`,
|
||
`wiki/`, `DOC-` — подстрокой. Обоснование: подстрока `eva` даёт ложные срабатывания (`evaluate`, `level`). Цена
|
||
ошибки: правка регулярки.
|
||
21. **R-4.** 2026-10-24. В Task 5 `on_project_updated(None)` чистит `wiki_page_fts` только если таблица существует
|
||
(создаёт её Task 7). Цена ошибки: FTS-строки сброшенного корня живут до Task 7.
|
||
|
||
Решения исполнителя Task 1 (2026-10-24):
|
||
|
||
22. Фикстура `tests/fixtures/settings_v6.json` — замороженные умолчания настроек v6 для `make_v6_db`, чтобы v6-база не
|
||
зависела от текущего кода. Цена ошибки: лишний файл фикстуры.
|
||
23. `queue.claim/agent_version/agent_view` получили необязательный `agent` (умолчание — первый агент реестра, в ядре нет
|
||
слова jira); Task 2 передаёт агента явно. Цена ошибки: до Task 2 поведение равно jira.
|
||
24. Служебный ключ `_agent` перезаписывается диспетчером для маршрутов scope `agent`, поэтому адрес запроса его не
|
||
подделает. Цена ошибки: нет.
|
||
25. `current_command_id` освобождается `WHERE current_command_id = ?` без имени агента (id команды уникален). Цена
|
||
ошибки: нет.
|
||
26. `h_claim` принимает `last_call_at` как алиас `last_jira_call_at` (решение 3). Цена ошибки: нет.
|
||
27. `_auth` для агента сравнивает токен со всеми агентами без раннего выхода (постоянное время). Цена ошибки: нет.
|
||
28. `_tokens_v7`: нет `tokens` или уже есть `agents` — шаг пропускается (идемпотентность). Цена ошибки: нет.
|
||
|
||
Решения исполнителя Task 2 (2026-10-24):
|
||
|
||
29. `module_dir` создаёт каталог модуля (0700) только если его нет; права существующих каталогов выставляет `Hub` при
|
||
старте (и `agent publish` для корня), а не `claim`. Цена ошибки: чужие права на каталогах агентов перезаписываются
|
||
при старте.
|
||
30. `Hub(release_dir=)` переименован в `release_root=`. Цена ошибки: нет (вызовов с kwarg не было).
|
||
31. `GET /agent?module=` отвечает 404 `not_found` и для модуля без команд. Цена ошибки: клиент не отличает «нет
|
||
модуля» от «нет агента».
|
||
32. `Hub.agent_update` строится по `registry.agent_modules()`; для прочих имён — пустой словарь (`_no_update`).
|
||
33. `queue.claim`: параметр `last_jira_call_at` переименован в `last_call_at`; тело HTTP принимает оба ключа.
|
||
34. `migrate_layout` переносит файлы покадрово (agent.py, затем manifest.json), повторяется, пока нет
|
||
`jira/manifest.json`; существующий приёмник не перезаписывается, корневой файл удаляется. Цена ошибки: если старый
|
||
код успел записать в корень более новый agent.py после частичной миграции, он потеряется (риск низкий).
|
||
35. `registry.DEFAULT_AGENT = "jira"` — единственное место с умолчанием агента в ядре; `release.py` импортирует его
|
||
оттуда. Цена ошибки: нет.
|
||
|
||
Решения исполнителя Task 3 (2026-10-24):
|
||
|
||
36. Миграция v7 убирает колонки досок `ALTER TABLE project DROP COLUMN`, а не пересозданием таблицы: при
|
||
`foreign_keys=ON` `DROP TABLE project` ломается из-за дочерней `jira_project`. Нужен SQLite не ниже 3.35 (в
|
||
окружении 3.51). Цена ошибки: на старом SQLite миграция падает; копия базы сделана до неё.
|
||
37. `jira_projects.create(conn, label, name, board_id)` — точка создания проекта с доской для init, миграции v3 и тестов;
|
||
доска проверяется раньше метки и названия. Цена ошибки: другое сообщение при одновременно плохих метке и доске.
|
||
38. `POST /projects`: проверка ядра (`label`, `name`), затем полей модулей (неизвестное — 422 `<поле>: неизвестное
|
||
поле`), затем 409 на дубль; модулям без своих полей в `on_project_created` передаётся пустой dict.
|
||
39. `PATCH /projects/{label}`: проект проверяется (404) раньше полей; пустое тело — 200 с `batch_ids: []`; `author`
|
||
необязателен (по умолчанию `api`).
|
||
40. `HubClient.patch` добавлен в ядро (нужен тестам и `root_set`). Метка в адресе PATCH раскодируется `unquote`.
|
||
41. `register_core_tools` создаёт инструмент `projects` внутри; функций `projects`/`project_create` в `core.mcp` больше
|
||
нет, тесты берут их из `mcp._tool_manager` и `jmcp.project_create`.
|
||
42. Проверка «изменяемое поле модуля без `on_project_updated`» выполняется при старте в `registry.check`, а не 422 при
|
||
запросе (исполнитель Task 3, fix). Цена ошибки: модуль с таким полем не запустится; правится в модуле.
|
||
43. Ключ `author` в `POST`/`PATCH /projects` — служебный и полем проекта не считается. Цена ошибки: модуль не может
|
||
назвать своё поле `author` (запрещено `registry.check`).
|
||
|
||
Решения исполнителя Task 4 (2026-10-24):
|
||
|
||
44. Правка `web/e2e/hub.ts` и `feed.spec.ts` (`tokens().agents.jira`) сделана в Task 4: после Task 1 формат токенов
|
||
изменился, e2e без правки не зелёные. Цена ошибки: нет (только тесты).
|
||
45. `Project` — `{label, name, created_at} & Record<string, unknown>`; типы jira (`JiraProject`) в
|
||
`modules/jira/projectFields.ts`. `useUpdateProject({label, fields})` сам добавляет `author: "viewer"`; сигнатура
|
||
может уточниться в Task 10. Цена ошибки: мелкая правка.
|
||
46. Поля окна — единый `TextInput` (number/text), вёрстка не менялась; `min=1` только для int.
|
||
|
||
Решения исполнителя Task 5 и рулинг R-5 (2026-10-24):
|
||
|
||
47. **R-5.** Имя каталога копии проекта — `paths.dir_name(label)`: `%`, `/`, `\` экранируются (`%25`, `%2F`, `%5C`), `.` и
|
||
`..` — `%2E`, `%2E%2E`. Обычные метки не меняются. Обоснование: метки ядра допускают `/` и `..`, без экранирования
|
||
сборка и `rmtree` выходили бы за каталог вики. Раскладка копии (Task 7) идёт только через `paths.project_dir`. Цена
|
||
ошибки: экзотические метки получают экранированное имя каталога.
|
||
48. `PATCH wiki_root` с тем же кодом — no-op (`batch_ids: []`): повторная синхронизация идёт через `POST /wiki/sync`.
|
||
Цена ошибки: одна строка в `on_updated`.
|
||
49. `on_updated` отменяет только пакеты `wiki_sync`, все команды которых `queued`; выданный (`claimed`) не трогает — его
|
||
поздний `done` по старому корню отбросит Task 7. Цена ошибки: лишняя синхронизация.
|
||
50. Автосинхронизация проекта, у которого нет ни `synced_at`, ни `failed_at` и нет активного пакета, ставится сразу, не
|
||
ожидая часов. Цена ошибки: лишний пакет; зависит от записи `failed_at`/`synced_at` в Task 7. Прочие решения
|
||
исполнителя (3, 5–9, 11–12 отчёта): `paths.root` на базе без файла бросает `RuntimeError`, `_drop_copy` его ловит;
|
||
очистка FTS по rowid страницы; строгая валидация настроек и команд (bool не целое); `wiki_project.failed_at` пока не
|
||
пишется. Цена ошибки: правка одной строки в соответствующем месте.
|
||
|
||
Решения исполнителя Task 6 (2026-10-24; HTML Eva недоверенный):
|
||
|
||
51. Вложения дедуплицируются по `url`: миниатюра и файл — одно вложение, если агент шлёт одинаковый `url`. Цена ошибки:
|
||
при разных `url` в eva-agent появятся две ссылки; проверяется по первой живой синхронизации.
|
||
52. Картинка внутри `<a>` на вложение выдаёт только ссылку `<a>`. Цена ошибки: при разных `ref` показана одна ссылка.
|
||
53. Небезопасные ссылки (`javascript:`, `data:`, `vbscript:` после удаления управляющих символов и пробелов) дают только
|
||
текст, у картинок пусто; ссылки и картинки автора со схемой `attachment:` — текст/пусто. Цена ошибки: видимый
|
||
текст без ссылки.
|
||
54. Ссылку вложения порождает только конвертер: приватный маркер U+E000/U+E001, авторские U+E000/U+E001 из HTML
|
||
удаляются; любое авторское `attachment:@` нейтрализуется вставкой U+200C между `:` и `@`; `fill_ids` заменяет
|
||
только `](attachment:@N)`, индекс вне списка — `ValueError` (вызывающий Task 7 превращает в `failed`). Цена
|
||
ошибки: невидимый символ в сыром `.md` при авторском «attachment:@»; лишние PUA-символы удаляются.
|
||
55. Текст `<` вне кода выводится `<` (сырых тегов нет); внутри `<code>`/`<pre>` не меняется. Имя вложения
|
||
экранирует `\ [ ] <` и `_`/`*` на границе слова. `@` не экранируется. Цена ошибки: сырой `.md` с `<`.
|
||
56. Известные края (отложены): вложение в `<a>` страницы/внешней ссылки, вложенный `<span>` между `<a>` и `<img>`,
|
||
хвостовой `\` в тексте ссылки, пробелы/скобки во внешнем href, дубликаты `ref` (побеждает последний).
|