From b66e5389525bc206bd94b8760a81b96e4cc250e8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 00:18:31 +0400 Subject: [PATCH] =?UTF-8?q?docs(openspec):=20change=20wiki-module=20?= =?UTF-8?q?=E2=80=94=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8,=20=D0=B2=D0=B8=D0=B4=D1=8B=20=D0=B0=D0=B3=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2,=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- openspec/changes/wiki-module/.openspec.yaml | 2 + openspec/changes/wiki-module/design.md | 399 ++++++++++++++++++ openspec/changes/wiki-module/proposal.md | 53 +++ .../wiki-module/specs/data-model/spec.md | 76 ++++ .../changes/wiki-module/specs/hub-api/spec.md | 145 +++++++ .../changes/wiki-module/specs/mcp/spec.md | 40 ++ .../changes/wiki-module/specs/modules/spec.md | 46 ++ .../changes/wiki-module/specs/web-app/spec.md | 82 ++++ .../changes/wiki-module/specs/wiki/spec.md | 129 ++++++ 9 files changed, 972 insertions(+) create mode 100644 openspec/changes/wiki-module/.openspec.yaml create mode 100644 openspec/changes/wiki-module/design.md create mode 100644 openspec/changes/wiki-module/proposal.md create mode 100644 openspec/changes/wiki-module/specs/data-model/spec.md create mode 100644 openspec/changes/wiki-module/specs/hub-api/spec.md create mode 100644 openspec/changes/wiki-module/specs/mcp/spec.md create mode 100644 openspec/changes/wiki-module/specs/modules/spec.md create mode 100644 openspec/changes/wiki-module/specs/web-app/spec.md create mode 100644 openspec/changes/wiki-module/specs/wiki/spec.md diff --git a/openspec/changes/wiki-module/.openspec.yaml b/openspec/changes/wiki-module/.openspec.yaml new file mode 100644 index 0000000..1f5d280 --- /dev/null +++ b/openspec/changes/wiki-module/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-24 diff --git a/openspec/changes/wiki-module/design.md b/openspec/changes/wiki-module/design.md new file mode 100644 index 0000000..6bf62c2 --- /dev/null +++ b/openspec/changes/wiki-module/design.md @@ -0,0 +1,399 @@ +## 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 по адресу `/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`. +Картинки и файлы — ссылки ``/`` на тот же хост с путём `/(files?|attachments?|download|media|upload)`. +Всё это обкатано в `eva_wiki_export.py` (корень workspace). Есть ли у документа дата изменения и история версий, +неизвестно: публичной документации 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/