docs(openspec): change wiki-module — модуль вики, виды агентов, поля проектов (#91)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f7add9b583
commit
b66e538952
2
openspec/changes/wiki-module/.openspec.yaml
Normal file
2
openspec/changes/wiki-module/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-24
|
||||
399
openspec/changes/wiki-module/design.md
Normal file
399
openspec/changes/wiki-module/design.md
Normal file
@ -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 по адресу `<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). Есть ли у документа дата изменения и история версий,
|
||||
неизвестно: публичной документации 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` в работе.
|
||||
|
||||
Хаб не ставит пакет, пока предыдущий `wiki_sync` проекта в работе: отвечает 409 с id пакета, MCP показывает его
|
||||
состояние.
|
||||
|
||||
**Агент** (контракт; решения внутри агента — его change):
|
||||
1. Берёт корень `CmfDocument.get` и все страницы поддерева, как `collect_subtree` в `eva_wiki_export.py`.
|
||||
2. По каждой странице берёт `text`.
|
||||
3. Отправляет страницы пачками `POST /agent/wiki/pages {command_id, pages: [{code, eva_id, title, parent_code,
|
||||
orderno, url, html, attachments: [{url, name}]}]}`. В пачке не больше 50 страниц и не больше 4 МБ тела.
|
||||
4. Отчитывается `done` с `result = {root_title, codes: [<все коды дерева в порядке обхода>], eva_calls}`.
|
||||
|
||||
**Хаб.**
|
||||
- Пачки складываются в `wiki_sync_stage` без изменения копии.
|
||||
- Отчёт `done` применяется одной транзакцией (`CommandHooks.on_result`):
|
||||
- HTML переводится в Markdown;
|
||||
- Markdown-копия собирается в `wiki/.<label>.new/`, затем атомарно подменяет `wiki/<label>/` (переименование;
|
||||
старая копия удаляется после подмены);
|
||||
- `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** (тот же хост и путь по правилу из Context) заменяется на `[📎 <имя>](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 работает. Живая проверка: kickstart, claim от v14 проходит.
|
||||
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`.
|
||||
53
openspec/changes/wiki-module/proposal.md
Normal file
53
openspec/changes/wiki-module/proposal.md
Normal file
@ -0,0 +1,53 @@
|
||||
## Why
|
||||
|
||||
Аналитики фиксируют требования и решения в evaTeam Wiki (Eva). Владельцу нужно две вещи: проверять задачи аналитиков и
|
||||
вовремя замечать, что его решения (и решения Claude в awp-ca) расходятся с тем, что аналитики уже записали в вики.
|
||||
Сейчас вики живёт отдельно от хаба, и Claude её не видит.
|
||||
|
||||
Решения владельца 23–24.10.2026 (brainstorming, jira-hub#91):
|
||||
- проект хаба связан не только с меткой Jira, но и с **одной корневой страницей Eva**, дерево под ней обходится
|
||||
рекурсивно; у каждого проекта метка Jira есть всегда, корень вики с меткой не совпадает; на общий случай (проект без
|
||||
метки, несколько корней) не обобщаем;
|
||||
- страницы хранятся **Markdown-файлами** в `~/.local/share/jira-hub/wiki/<проект>/` — удобный формат, читаемый из
|
||||
терминала;
|
||||
- главное — **MCP**, веб — вторым; MCP вики — **отдельный сервер** модуля, «чтобы не перегружать»;
|
||||
- в Eva ходит **отдельный агент `eva-agent`**, не jira-agent;
|
||||
- **картинки и файлы** страниц — только по запросу, как вложения Jira (#83): они бывают большими;
|
||||
- **запись в вики и история изменений** (30 дней) — нужны, но позже; сейчас базовый функционал.
|
||||
|
||||
Это второй шаг к sdlc-hub после модульной перестройки (#87). Там же оставлены два хвоста, которые закрываются здесь:
|
||||
доски проекта в ядре (метки `долг hub-modules`) и единственный агент.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Модуль `wiki` хаба** (`jira_hub/modules/wiki/`): привязка проекта к корню Eva, копия дерева страниц Markdown-файлами
|
||||
и индекс для поиска, список вложений страниц, команды `wiki_sync` и `wiki_fetch`, маршруты HTTP, фоновая синхронизация
|
||||
раз в сутки.
|
||||
- **MCP вики** `jira-hub mcp wiki [--project <метка>]`: дерево проекта, страница, поиск, вложение, синхронизация,
|
||||
состояние.
|
||||
- **Веб**: страница «Вики» проекта — дерево, страница в Markdown, поиск, обновление, скачивание вложений.
|
||||
- **Виды агентов в ядре**: у каждого модуля с командами — свой агент со своим токеном, статусом, опубликованным релизом
|
||||
и самообновлением; `claim` выдаёт агенту только команды его модуля. jira-agent v14 работает без изменений.
|
||||
- **Проекты без Jira в ядре**: доска (`board_id`, `board_type`) и число задач переезжают в модуль jira; модуль добавляет
|
||||
свои поля в создание проекта, в список проектов, в MCP `projects`/`project_create` и в окно «Новый проект». Все метки
|
||||
`долг hub-modules` снимаются.
|
||||
- **eva-agent** (новый репозиторий `aep/eva-agent`, создаёт владелец): Python 3.9 на stdlib, клиент Eva JSON-RPC,
|
||||
транспорт через общую папку, `run.py` и самообновление по образцу jira-agent. Контракт команд — в этой спеке; решения
|
||||
агента — в его собственном design-only change.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Запись в вики (правка и создание страниц) — позже, командами eva-agent через ту же очередь.
|
||||
- История изменений страниц за 30 дней (встроенная в Eva или своими снимками) — позже; заодно частичная перекачка только
|
||||
изменённых страниц.
|
||||
- Проект без метки Jira, несколько корней вики у проекта, обобщённая «привязка внешних источников».
|
||||
- Переименование в sdlc-hub.
|
||||
- Спринты и бэклог.
|
||||
- Obsidian и любые внешние читатели копии: формат файлов не подстраиваем под них.
|
||||
|
||||
## Anti-Goals
|
||||
|
||||
- Сломать живой мост: jira-agent v14 без обновления, MCP `jira-hub` в awp-ca, адреса веба и API модуля jira.
|
||||
- Токен Eva на Mac mini: он живёт только в конфиге eva-agent на виртуалке, как пароль Jira у jira-agent.
|
||||
- Скачивание всех картинок и файлов Eva при синхронизации.
|
||||
- Хаб, который сам ходит в Eva.
|
||||
76
openspec/changes/wiki-module/specs/data-model/spec.md
Normal file
76
openspec/changes/wiki-module/specs/data-model/spec.md
Normal file
@ -0,0 +1,76 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Журнал синхронизаций и статус агента
|
||||
Таблица `sync_run` SHALL хранить каждую загрузку состояния: `started_at`, `finished_at`, `kind` (`keys | jql`), `query`,
|
||||
`issues_count`, `links_count`, `jira_calls`, `rate_limited`, `command_id`. При повторной выдаче команды `refresh` хаб
|
||||
SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started_at` не меняется); при переводе `refresh` в `failed`
|
||||
по пределу выдач — проставить `finished_at`. Таблица `agent_status` SHALL хранить по строке на агента (ключ `agent` —
|
||||
имя модуля, см. `modules`, «Агенты модулей»): `last_seen_at`, `agent_version`, `settings_version`, `current_command_id`,
|
||||
`last_call_at` (время последнего обращения агента к внешней системе; от jira-agent приходит полем `last_jira_call_at`).
|
||||
Через API и MCP статус агента SHALL отдаваться с признаком `online` (истина, если `last_seen_at` моложе 5 минут) и
|
||||
списком `redelivered` — команд его модуля в `claimed`, выданных больше одного раза (`id`, `type`, `claimed_at`,
|
||||
`deliveries`).
|
||||
|
||||
#### Scenario: Агент давно не выходил на связь
|
||||
- **WHEN** `last_seen_at` агента `jira` старше 5 минут
|
||||
- **THEN** `agent_status` через API и MCP отдаётся с `online = false`
|
||||
|
||||
#### Scenario: Довыдача refresh не удваивает счётчики
|
||||
- **WHEN** `refresh` загрузил 150 задач, отчёт потерян, команда выдана снова и загрузила те же 150
|
||||
- **THEN** у её `sync_run` `issues_count = 150`
|
||||
|
||||
#### Scenario: refresh сдался по пределу выдач
|
||||
- **WHEN** `refresh` переведён в `failed` «агент не отчитался после 3 выдач»
|
||||
- **THEN** у его `sync_run` проставлен `finished_at`
|
||||
|
||||
#### Scenario: Статусы агентов раздельно
|
||||
- **WHEN** агент `jira` выходил на связь минуту назад, агент `wiki` — никогда
|
||||
- **THEN** `GET /agents` отдаёт `jira` с `online = true` и `wiki` с `online = false`, `last_seen_at = null`
|
||||
|
||||
### Requirement: Проекты
|
||||
Таблица `project` SHALL хранить проекты моста — подмножества задач Jira-проекта `project_key`, выделенные меткой:
|
||||
`label` (PK, метка Jira), `name`, `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и
|
||||
управляющих символов (категория Unicode `Cc`), запятых, кавычек, скобок и обратной косой черты; две метки, совпадающие
|
||||
без учёта регистра, MUST NOT существовать одновременно. Название SHALL быть непустым после обрезки пробелов, до 100
|
||||
символов. Метку и название после создания хаб не меняет (переименования и удаления нет).
|
||||
|
||||
Доска проекта SHALL храниться в таблице модуля jira `jira_project`: `label` (PK, ссылка на проект), `board_id` (номер
|
||||
доски Jira или `NULL`), `board_type` (`scrum` | `kanban` | `NULL` — тип ещё не известен). `board_id` — целое больше нуля
|
||||
или `NULL`, после создания не меняется; `board_type` SHALL записываться только по результату `refresh` от агента (см.
|
||||
`agent`). Миграция v7 SHALL перенести `board_id` и `board_type` всех проектов из `project` в `jira_project`. Базовая JQL
|
||||
SHALL вычисляться из проектов: `project = <project_key> AND labels in ("<метка>", …)` — каждая метка в двойных
|
||||
кавычках, метки по created_at, затем label, через `, `; без проектов базовой JQL нет (`null`).
|
||||
|
||||
Для проекта P задачи зеркала делятся на *свои* — с меткой P в `labels` (точное совпадение) — и *соседей* — задачи без
|
||||
метки P, связанные со своей связью `issue_link` в любую сторону, и эпики своих (`epic_key` своей задачи), если они есть в
|
||||
зеркале. Признак `neighbor` в выдаче в разрезе проекта SHALL считаться относительно P: `false` у своих, `true` у соседей.
|
||||
|
||||
#### Scenario: Базовая JQL из двух проектов
|
||||
- **WHEN** заведены проекты `AWPCA` (раньше) и `STOCKS_DRRK`, `project_key = AURORA`
|
||||
- **THEN** базовая JQL — `project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")`
|
||||
|
||||
#### Scenario: Свои и соседи
|
||||
- **WHEN** у AURORA-1 метка `STOCKS_DRRK` и эпик AURORA-9 (метка `AWPCA`), связь AURORA-1 → AURORA-2 (метка `AWPCA`),
|
||||
у AURORA-3 метка `AWPCA` без связей с проектом `STOCKS_DRRK`
|
||||
- **THEN** в разрезе `STOCKS_DRRK` свои — AURORA-1, соседи — AURORA-2 и AURORA-9; AURORA-3 в разрез не входит
|
||||
|
||||
#### Scenario: Метка с пробелом
|
||||
- **WHEN** создаётся проект с меткой `STOCKS DRRK`
|
||||
- **THEN** отказ с указанием поля `label`, проекта нет
|
||||
|
||||
#### Scenario: Метка с обратной косой чертой или управляющим символом
|
||||
- **WHEN** создаётся проект с меткой `A\B` или `A<перевод строки>B`
|
||||
- **THEN** отказ с указанием поля `label`, проекта нет
|
||||
|
||||
#### Scenario: Метка — зарезервированное слово JQL
|
||||
- **WHEN** заведён проект с меткой `EMPTY`
|
||||
- **THEN** базовая JQL — `project = AURORA AND labels in ("EMPTY")`, метка не расширяет выборку
|
||||
|
||||
#### Scenario: Та же метка другим регистром
|
||||
- **WHEN** есть проект `AWPCA`, создаётся проект `awpca`
|
||||
- **THEN** отказ «проект с такой меткой уже есть», проект один
|
||||
|
||||
#### Scenario: Доски после миграции v7
|
||||
- **WHEN** база v6 с проектами `AWPCA` (`board_id = 4216`, `scrum`) и `sfm_kedr` (без доски) мигрирует в v7
|
||||
- **THEN** в `project` нет колонок `board_id`, `board_type`; в `jira_project` — `AWPCA` 4216 `scrum` и `sfm_kedr` `NULL`
|
||||
`NULL`; перед миграцией сделана копия базы
|
||||
145
openspec/changes/wiki-module/specs/hub-api/spec.md
Normal file
145
openspec/changes/wiki-module/specs/hub-api/spec.md
Normal file
@ -0,0 +1,145 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Процесс и доступ
|
||||
`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
|
||||
`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать
|
||||
`Authorization: Bearer <token>`: токен агента модуля открывает только `/agent/*` (маршруты ядра — от имени этого агента,
|
||||
маршруты модуля со scope `agent` — только агенту своего модуля, см. `modules`, «Агенты модулей»), клиентский — всё
|
||||
остальное. `jira-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение —
|
||||
раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без
|
||||
данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или
|
||||
`Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки
|
||||
дополнительно отвечает 502. Методы, кроме GET, POST, PUT и PATCH, отвечают 501 стандартной страницей `http.server`, не
|
||||
JSON.
|
||||
|
||||
#### Scenario: Чужой токен
|
||||
- **WHEN** токеном агента `jira` вызван `GET /issues`
|
||||
- **THEN** 401
|
||||
|
||||
#### Scenario: Неподдерживаемый метод
|
||||
- **WHEN** `DELETE /issues/AURORA-1`
|
||||
- **THEN** 501, тело не JSON; база не меняется
|
||||
|
||||
#### Scenario: Страница без токена
|
||||
- **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть
|
||||
- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет
|
||||
|
||||
#### Scenario: Токены после миграции
|
||||
- **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `jira-hub tokens`
|
||||
- **THEN** вывод содержит клиентский токен, `jira: T` и новый токен `wiki`
|
||||
|
||||
### Requirement: Маршруты проектов
|
||||
Хаб SHALL предоставлять под клиентским токеном:
|
||||
- `GET /projects` → список проектов по `created_at`, затем `label`; каждый — плоский объект из полей ядра (`label`,
|
||||
`name`, `created_at`) и полей всех модулей (`modules`, «Поля проекта от модулей»). Модуль jira даёт `board_id`,
|
||||
`board_type`, `issues_count` (задачи зеркала с меткой проекта), модуль wiki — `wiki_root`, `wiki_root_title`,
|
||||
`wiki_pages`, `wiki_synced_at`, `wiki_sync_error`.
|
||||
- `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id}`: в одной транзакции проект
|
||||
записывается, модули записывают свои поля и ставят свои пакеты; `batch_id` — пакет первого модуля, вернувшего пакет
|
||||
(у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов —
|
||||
`author` (по умолчанию `api`). Неизвестное поле, неверные поля ядра (см. `data-model`, «Проекты») или модуля — 422 с
|
||||
указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакетов нет.
|
||||
- `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids}`: модули правят свои поля
|
||||
и ставят пакеты в одной транзакции. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не
|
||||
меняется»; неизвестное поле или неверное значение — 422 с указанием поля; проекта нет — 404.
|
||||
|
||||
#### Scenario: Новый проект
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}`, есть проект `AWPCA`
|
||||
- **THEN** 201; `GET /projects` отдаёт два проекта, у `STOCKS_DRRK` `board_type = null`, `wiki_root = null`; в очереди
|
||||
пакет автора `viewer` с `refresh {jql: 'project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")'}`
|
||||
|
||||
#### Scenario: Проект без доски
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК"}`
|
||||
- **THEN** 201, `board_id = null`
|
||||
|
||||
#### Scenario: Проект с корнем вики
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234"}`
|
||||
- **THEN** 201; в очереди пакет `refresh` и пакет `wiki_sync {label: "STOCKS_DRRK", root: "DOC-001234"}`; `batch_id` —
|
||||
пакет `refresh`
|
||||
|
||||
#### Scenario: Повтор метки
|
||||
- **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть
|
||||
- **THEN** 409, пакет не поставлен
|
||||
|
||||
#### Scenario: Без названия
|
||||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: " "}`
|
||||
- **THEN** 422 с указанием поля `name`
|
||||
|
||||
#### Scenario: Неизвестное поле
|
||||
- **WHEN** `POST /projects {label: "X", name: "X", board: 5}`
|
||||
- **THEN** 422 с указанием поля `board`, проекта нет
|
||||
|
||||
#### Scenario: Корень вики у существующего проекта
|
||||
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "DOC-000777"}`
|
||||
- **THEN** 200, `batch_ids` — один пакет `wiki_sync`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777`
|
||||
|
||||
#### Scenario: Доску не меняют
|
||||
- **WHEN** `PATCH /projects/AWPCA {board_id: 1}`
|
||||
- **THEN** 422 «board_id после создания не меняется», доска прежняя
|
||||
|
||||
### Requirement: Публикация версии агента
|
||||
`jira-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`;
|
||||
неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ,
|
||||
ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной для этого модуля. Файл SHALL записываться в
|
||||
`<JIRA_HUB_HOME>/release/<модуль>/agent.py` (каталоги 0700), затем `manifest.json` `{version, sha256, size,
|
||||
published_at}`; оба — через временный файл с `fsync` и переименование, манифест последним. Прежний `agent.py` на время
|
||||
публикации SHALL переименовываться в `agent.py.prev` и удаляться после записи манифеста; если запись нового файла или
|
||||
манифеста не удалась (`OSError`), хаб SHALL вернуть `agent.py.prev` на место `agent.py`, удалить временные файлы и
|
||||
завершить команду сообщением «не удалось записать публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя
|
||||
версия. Команда SHALL работать без запущенного `serve` и печатать «опубликована v<N>, sha256 <первые 12 символов>…,
|
||||
<size> байт»; при ошибке — сообщение и код 1, опубликованное не меняется. При старте хаб SHALL один раз перенести
|
||||
прежнюю публикацию `release/agent.py` и `release/manifest.json` в `release/jira/`, если `release/jira/` ещё нет; ошибка
|
||||
переноса — предупреждение в лог, хаб стартует. Хаб SHALL читать манифест модуля агента при каждом `claim`,
|
||||
`GET /agent/release` и `GET /agent`; манифест годен, только если это объект, `version` — целое число (не логическое) не
|
||||
меньше 1, `sha256` — строка из 64 символов `[0-9a-f]`; негодный или нечитаемый манифест означает «публикации нет» и
|
||||
предупреждение в лог — одно на каждый новый текст ошибки. Файл `agent.py`, не декодируемый как UTF-8 при совпавшем
|
||||
sha256, означает «публикации нет» (`GET /agent/release` — 404 `no_release`). `GET /agent?module=<модуль>` (без
|
||||
параметра — `jira`) и каждый элемент `GET /agents` SHALL дополняться полями `release` (`{version, sha256,
|
||||
published_at}` или `null`), `update_state`, `launcher_version`, `agent_reported` (был ли `claim` этого агента с момента
|
||||
запуска хаба) и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии
|
||||
агента, агент онлайн, `agent_reported = true`, `launcher_version = null`), «откат с v<to> на v<from>: <error>»
|
||||
(`rolled_back`), «v<to> отвергнута: <error>» (`rejected`), иначе `null`.
|
||||
|
||||
#### Scenario: Публикация новой версии
|
||||
- **WHEN** опубликована v5, `jira-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6`
|
||||
- **THEN** в `release/jira/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6,
|
||||
sha256 …»; код 0
|
||||
|
||||
#### Scenario: Публикация eva-agent
|
||||
- **WHEN** `jira-hub agent publish --module wiki eva-agent/src/agent.py` с `AGENT_VERSION = 1`, опубликована jira v14
|
||||
- **THEN** в `release/wiki/` — v1; `claim` агента `wiki` получает `agent_release {version: 1, …}`, `claim` агента `jira` —
|
||||
`{version: 14, …}`
|
||||
|
||||
#### Scenario: Неизвестный модуль
|
||||
- **WHEN** `jira-hub agent publish --module nope agent.py`
|
||||
- **THEN** код 2, в stderr «неизвестный модуль nope» и список модулей с агентами; `release/` не изменён
|
||||
|
||||
#### Scenario: Версия не выше опубликованной
|
||||
- **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6`
|
||||
- **THEN** «опубликована v6, новая должна быть больше», код 1; `release/` не изменён
|
||||
|
||||
#### Scenario: Нет строки версии
|
||||
- **WHEN** публикуется файл без строки `AGENT_VERSION = <целое>` или с двумя такими строками
|
||||
- **THEN** сообщение об ошибке, код 1; `release/` не изменён
|
||||
|
||||
#### Scenario: Диск полон при записи манифеста
|
||||
- **WHEN** опубликована v8, публикуется v9, запись `manifest.json` завершилась `OSError`
|
||||
- **THEN** «не удалось записать публикацию: …», код 1, без трейсбека; `agent.py` и манифест — прежние v8,
|
||||
`GET /agent/release` отдаёт v8; временных файлов и `agent.py.prev` в `release/jira/` нет
|
||||
|
||||
#### Scenario: Негодный манифест
|
||||
- **WHEN** в `manifest.json` `version = 6.9` (или `true`, или `sha256` короче 64 символов), хаб получает 100 `claim`
|
||||
- **THEN** в `claim` `agent_release = null`, `GET /agent/release` — 404; в логе одно предупреждение о манифесте
|
||||
|
||||
#### Scenario: Агент без запускателя
|
||||
- **WHEN** опубликована v6, агент v5 онлайн, в последнем `claim` `launcher_version = null`
|
||||
- **THEN** `GET /agent` → `agent_reported = true`, `update_hint` = «агент запущен без run.py — обновится только вручную»
|
||||
|
||||
#### Scenario: Хаб перезапущен, агент ещё не выходил на связь
|
||||
- **WHEN** опубликована v10, агент v9 под запускателем, хаб перезапущен, `claim` после запуска ещё не было
|
||||
- **THEN** `GET /agent` → `agent_reported = false`, `launcher_version = null`, `update_hint = null`; после первого
|
||||
`claim` — `agent_reported = true`, `launcher_version` из `claim`
|
||||
|
||||
#### Scenario: Перенос прежней публикации
|
||||
- **WHEN** хаб с `release/agent.py` v14 и манифестом без `release/jira/` запущен на новом коде
|
||||
- **THEN** публикация v14 лежит в `release/jira/`, `claim` агента `jira` получает `agent_release {version: 14, …}`
|
||||
40
openspec/changes/wiki-module/specs/mcp/spec.md
Normal file
40
openspec/changes/wiki-module/specs/mcp/spec.md
Normal file
@ -0,0 +1,40 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MCP вики
|
||||
`jira-hub mcp wiki [--project <метка>]` SHALL запускать по stdio MCP-сервер модуля `wiki` с именем `jira-hub-wiki`,
|
||||
ходящий в HTTP API хаба клиентским токеном; хаб недоступен — каждый инструмент отвечает «хаб не запущен: jira-hub serve».
|
||||
Привязка к проекту проверяется так же, как у сервера jira («Привязка сессии к проекту»). Инструменты:
|
||||
|
||||
| Инструмент | Параметры | Ответ |
|
||||
|---|---|---|
|
||||
| `projects` | — | таблица: метка, название, корень вики, страниц, последняя синхронизация; текущий проект отмечен |
|
||||
| `project_create` | `label`, `name`, `wiki_root` (необязательно) | `POST /projects` с автором `claude`; проект и фрагмент `.mcp.json` с записью `jira-hub-wiki` |
|
||||
| `root_set` | `root` | `PATCH /projects/<текущий> {wiki_root}`; новый корень и id пакета синхронизации |
|
||||
| `tree` | `code` (необязательно), `depth` (по умолчанию 2) | дерево от корня или от `code`: отступ, код, название, размер |
|
||||
| `page` | `code` или `title`, `offset` (по умолчанию 0) | шапка (код, путь в дереве, адрес в Eva, дата синхронизации, вложения с `id`) и Markdown с `offset` до 20 000 символов, в конце — «продолжение: offset=<N>», если текст длиннее; `title` ищется точно, иначе — до 10 кандидатов с кодами |
|
||||
| `search` | `query`, `limit` (по умолчанию 10) | страницы: код, название, путь, фрагмент |
|
||||
| `attachment` | `id` | ставит скачивание, ждёт до 60 с; готово — путь к копии `downloads/wiki-<id>/<имя>`; иначе состояние и подсказка повторить |
|
||||
| `sync` | `wait` (по умолчанию `false`) | ставит синхронизацию (или сообщает о текущей) и id пакета; с `wait` — ждёт до 300 с и отдаёт итог |
|
||||
| `status` | — | корень, последняя синхронизация, ошибка, пакет в работе, агент `wiki`: онлайн/офлайн, версия, публикация, обновление, запускатель (как `agent_status` сервера jira) |
|
||||
|
||||
Инструменты, кроме `projects` и `project_create`, SHALL требовать привязки к проекту. В привязанной сессии без корня
|
||||
(кроме `root_set`, `status`) ответ SHALL быть «у проекта <метка> нет корня вики: root_set DOC-…»; с корнем, но без
|
||||
синхронизаций — «вики проекта ещё не синхронизирована: sync». Набор инструментов и их описания у сервера `jira-hub`
|
||||
(модуль jira) SHALL остаться прежним.
|
||||
|
||||
#### Scenario: Поиск из сессии awp-ca
|
||||
- **WHEN** сервер запущен с `--project AWPCA`, копия синхронизирована, вызван `search {query: "статус согласован"}`
|
||||
- **THEN** ответ — страницы с кодами и фрагментами; запрос в хаб — `GET /wiki/search?project=AWPCA&q=…`
|
||||
|
||||
#### Scenario: Длинная страница
|
||||
- **WHEN** `page {code: DOC-3}`, Markdown 45 000 символов
|
||||
- **THEN** первые 20 000 символов и «продолжение: offset=20000»; `page {code: DOC-3, offset: 40000}` — остаток без строки
|
||||
продолжения
|
||||
|
||||
#### Scenario: Корня нет
|
||||
- **WHEN** сессия `sfm_kedr` без корня, вызван `tree`
|
||||
- **THEN** ответ «у проекта sfm_kedr нет корня вики: root_set DOC-…», запроса `GET /wiki/tree` нет
|
||||
|
||||
#### Scenario: Сервер jira не изменился
|
||||
- **WHEN** запущен `jira-hub mcp --project AWPCA`
|
||||
- **THEN** имя сервера `jira-hub`, список инструментов и описания совпадают с эталоном до change
|
||||
46
openspec/changes/wiki-module/specs/modules/spec.md
Normal file
46
openspec/changes/wiki-module/specs/modules/spec.md
Normal file
@ -0,0 +1,46 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Агенты модулей
|
||||
У каждого подключённого модуля, объявляющего типы команд, SHALL быть свой агент; имя агента — имя модуля (`jira` —
|
||||
jira-agent, `wiki` — eva-agent). Агент SHALL определяться по токену: `setting.tokens = {client, agents: {<модуль>:
|
||||
<токен>}}`, недостающий токен агента хаб создаёт при старте. Хаб SHALL для каждого агента отдельно:
|
||||
- выдавать в `claim` только команды типов его модуля, с одной командой в полёте на агента (агенты работают
|
||||
параллельно);
|
||||
- хранить строку `agent_status` (ключ — имя агента);
|
||||
- держать опубликованный релиз `release/<модуль>/` и состояние самообновления;
|
||||
- отдавать в `GET /agent/settings` ключи ядра и ключи для агента только своего модуля;
|
||||
- пускать на маршруты модуля со scope `agent` только агента этого модуля (токен другого агента — 401).
|
||||
|
||||
Миграция v7 SHALL сохранить прежний агентский токен как токен агента `jira`, перенести строку `agent_status` в агента
|
||||
`jira` и прежнюю публикацию `release/agent.py` в `release/jira/`. jira-agent v14 SHALL работать без обновления.
|
||||
|
||||
#### Scenario: jira-agent после выкатки
|
||||
- **WHEN** хаб v6 с агентским токеном T и публикацией v14 обновлён до v7, jira-agent v14 шлёт `claim` с токеном T
|
||||
- **THEN** 200; ответ содержит `agent_release {version: 14, …}`; `GET /agent` показывает агента `jira` с версией 14
|
||||
|
||||
#### Scenario: Агенты не берут чужие команды
|
||||
- **WHEN** в очереди пакет 1 с `ensure_issue` и пакет 2 с `wiki_sync`, оба `queued`; агент `wiki` шлёт `claim`
|
||||
- **THEN** выдан `wiki_sync`; следующий `claim` агента `jira` получает `ensure_issue`, хотя `wiki_sync` ещё `claimed`
|
||||
|
||||
#### Scenario: Чужой маршрут агента
|
||||
- **WHEN** токеном агента `wiki` вызван `POST /agent/state`
|
||||
- **THEN** 401, зеркало не меняется
|
||||
|
||||
#### Scenario: Настройки только своего модуля
|
||||
- **WHEN** токеном агента `wiki` вызван `GET /agent/settings`
|
||||
- **THEN** в ответе есть `wiki_pacing` и `settings_version`, ключей jira (`project_key`, `transitions_map`) нет
|
||||
|
||||
### Requirement: Поля проекта от модулей
|
||||
Проект ядра SHALL состоять из `label`, `name`, `created_at`; остальные поля проекта SHALL объявлять модули: имя, тип,
|
||||
проверку, изменяемость после создания, подпись и подсказку. Модуль SHALL сам хранить свои поля и отдавать их значения
|
||||
(включая вычисляемые) для списка проектов. При старте хаб SHALL отвергать два модуля с одним именем поля или поле
|
||||
модуля с именем поля ядра — ошибка старта с именем поля и модулей. В ядре SHALL NOT оставаться знания о полях модулей
|
||||
(доска, число задач, корень вики).
|
||||
|
||||
#### Scenario: Конфликт полей
|
||||
- **WHEN** два модуля объявляют поле проекта `root`
|
||||
- **THEN** хаб не стартует, ошибка называет поле и оба модуля
|
||||
|
||||
#### Scenario: Поля jira прежние
|
||||
- **WHEN** после миграции v7 вызван `GET /projects`
|
||||
- **THEN** у `AWPCA` `board_id = 4216`, `board_type = scrum`, `issues_count` — как до миграции
|
||||
82
openspec/changes/wiki-module/specs/web-app/spec.md
Normal file
82
openspec/changes/wiki-module/specs/web-app/spec.md
Normal file
@ -0,0 +1,82 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Вики проекта
|
||||
Приложение SHALL иметь страницу «Вики» (`/app/wiki?project=<метка>`, пункт боковой панели; текущий проект — как у доски
|
||||
и задач, «Проект в приложении»). Страница SHALL показывать:
|
||||
- дерево страниц проекта (`GET /wiki/tree`) слева, свёрнутое до первого уровня; узел раскрывается по клику;
|
||||
- выбранную страницу справа (`GET /wiki/pages/{code}`): заголовок, дата синхронизации, ссылка «открыть в Eva»,
|
||||
Markdown с таблицами, без сырого HTML; ссылки `page:<код>` открывают страницу вики (код в адресе `&page=<код>`),
|
||||
ссылки `attachment:<id>` скачивают вложение (запрос, опрос состояния, сохранение файла браузером), состояние
|
||||
`failed` — текст ошибки рядом со ссылкой;
|
||||
- поиск над деревом (`GET /wiki/search`): список результатов с фрагментами вместо дерева, пока строка поиска не пуста;
|
||||
- кнопку «Обновить вики» (`POST /wiki/sync`) и плашку хода синхронизации в боковой панели; ответ 409 — плашка текущего
|
||||
пакета;
|
||||
- проекта без корня — форму «Корень вики (DOC-… или ссылка)» с кнопкой «Привязать» (`PATCH /projects/{label}`); 422 —
|
||||
текст ошибки у поля;
|
||||
- корень есть, синхронизаций нет — «вики ещё не синхронизирована» и кнопку «Обновить вики»;
|
||||
- последняя синхронизация с ошибкой — плашку с текстом ошибки над страницей.
|
||||
|
||||
#### Scenario: Открыть страницу по ссылке
|
||||
- **WHEN** на странице DOC-3 нажата ссылка `page:DOC-2`
|
||||
- **THEN** адрес `/app/wiki?project=AWPCA&page=DOC-2`, справа DOC-2, в дереве DOC-2 выделен
|
||||
|
||||
#### Scenario: Проект без корня
|
||||
- **WHEN** открыта `/app/wiki?project=sfm_kedr`, корня нет; введено `DOC-001234`, нажато «Привязать»
|
||||
- **THEN** `PATCH /projects/sfm_kedr {wiki_root: "DOC-001234"}`; боковая панель показывает ход пакета синхронизации
|
||||
|
||||
#### Scenario: Поиск
|
||||
- **WHEN** в строке поиска «согласов»
|
||||
- **THEN** вместо дерева — результаты `GET /wiki/search?project=AWPCA&q=согласов`; очистка строки возвращает дерево
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Проект в приложении
|
||||
Доска, список задач и вики SHALL показывать один проект — текущий. Текущий проект SHALL задаваться параметром
|
||||
`project` в адресе `/app/board`, `/app/issues` и `/app/wiki`; без параметра — последний выбранный в этом браузере
|
||||
(`localStorage`), если он есть в `GET /projects`, иначе первый из `GET /projects`; выбранный проект записывается в адрес
|
||||
без новой записи истории. Проекта из адреса нет в `GET /projects` — страница SHALL показывать «проект <метка> не найден»
|
||||
и выпадающий список проектов вместо данных. Проектов нет — доска, список и вики SHALL показывать приглашение создать
|
||||
проект («+ Новый проект…»). Переходы между «Доска связей», «Задачи» и «Вики» SHALL сохранять текущий проект. Карточка
|
||||
задачи (`/app/issues/{key}`) от проекта не зависит.
|
||||
|
||||
Доска SHALL загружать `GET /board?project=<текущий>`; «Обновить из Jira» — `POST /refresh {full: true}`, обновляет все
|
||||
проекты. Раскладка, дорожки и фильтры доски в `localStorage` SHALL храниться отдельно для каждого проекта;
|
||||
у проекта `AWPCA` — на прежних ключах (раскладка, сделанная до проектов, сохраняется). Фильтр спринтов SHALL
|
||||
показываться, только если хотя бы у одной своей задачи снимка (`neighbor = false`) есть спринт; иначе — «колонки
|
||||
«Спринт» нет — показаны все задачи».
|
||||
|
||||
Форма «Новый проект» (пункт «+ Новый проект…» выпадающего списка) SHALL открываться окном поверх страницы: метка,
|
||||
название и поля модулей — номер доски Jira (необязательно; подсказка «scrum или kanban определится сам») и корень вики
|
||||
(необязательно; подсказка «DOC-… или ссылка на страницу Eva»). Пустое необязательное поле в запрос не попадает.
|
||||
«Создать» — `POST /projects` с автором `viewer`; ошибка хаба 409/422 — текст `detail` рядом с кнопкой, окно остаётся
|
||||
открытым; успех — окно закрывается, текущим становится новый проект, боковая панель показывает ход пакета обновления из
|
||||
ответа (`batch_id`).
|
||||
|
||||
#### Scenario: Первый вход после выкатки
|
||||
- **WHEN** открыт `/app/board` без `project`, в `localStorage` выбора нет, проекты `AWPCA` и `STOCKS_DRRK`
|
||||
- **THEN** адрес — `/app/board?project=AWPCA`, доска — `GET /board?project=AWPCA` с прежней раскладкой карточек
|
||||
|
||||
#### Scenario: Переключение проекта
|
||||
- **WHEN** на доске `AWPCA` в выпадающем списке выбран `STOCKS_DRRK`, затем открыт раздел «Задачи»
|
||||
- **THEN** доска — `GET /board?project=STOCKS_DRRK` со своей раскладкой; адрес «Задач» — `/app/issues?project=STOCKS_DRRK`
|
||||
|
||||
#### Scenario: Неизвестный проект в адресе
|
||||
- **WHEN** открыт `/app/issues?project=NOPE`
|
||||
- **THEN** «проект NOPE не найден» и список проектов; запроса `GET /issues` нет
|
||||
|
||||
#### Scenario: Создание проекта из формы
|
||||
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», доска и корень вики пусто; нажато «Создать»
|
||||
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", author: "viewer"}`; текущий проект — `STOCKS_DRRK`;
|
||||
боковая панель показывает «пакет #N выполняется»
|
||||
|
||||
#### Scenario: Проект с корнем вики из формы
|
||||
- **WHEN** в форме введены `STOCKS_DRRK`, «Запасы ДРРК», корень `DOC-001234`; нажато «Создать»
|
||||
- **THEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234", author: "viewer"}`
|
||||
|
||||
#### Scenario: Метка занята
|
||||
- **WHEN** в форме введена метка `awpca`, хаб ответил 409
|
||||
- **THEN** окно открыто, рядом с «Создать» — текст ошибки хаба
|
||||
|
||||
#### Scenario: Канбан-проект
|
||||
- **WHEN** текущий проект — `STOCKS_DRRK`, ни у одной своей задачи снимка нет спринта (у соседа из `AWPCA` спринт есть)
|
||||
- **THEN** вместо фильтра спринтов — «колонки «Спринт» нет — показаны все задачи»
|
||||
129
openspec/changes/wiki-module/specs/wiki/spec.md
Normal file
129
openspec/changes/wiki-module/specs/wiki/spec.md
Normal file
@ -0,0 +1,129 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Корень вики проекта
|
||||
Модуль `wiki` SHALL давать проекту поле `wiki_root` — код корневого документа Eva вида `DOC-<цифры>` (регистр
|
||||
приводится к верхнему; ссылка на документ Eva с таким кодом принимается и сводится к коду) или `null`. Поле
|
||||
необязательно при создании и изменяемо после (`PATCH /projects/{label}`). Задание или смена корня SHALL ставить пакет
|
||||
синхронизации проекта; смена корня SHALL оставлять прежнюю копию до успешной синхронизации нового. Сброс в `null`
|
||||
SHALL удалять копию проекта, его страницы, индекс и строки вложений. Модуль SHALL хранить для проекта `root_title`,
|
||||
`synced_at`, `pages_count`, `sync_error`.
|
||||
|
||||
#### Scenario: Ссылка вместо кода
|
||||
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "https://eva.example.ru/project/Document/doc-001234"}`
|
||||
- **THEN** 200, `wiki_root = DOC-001234`, поставлен `wiki_sync {label: AWPCA, root: DOC-001234}`
|
||||
|
||||
#### Scenario: Неверный код
|
||||
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "страница про статусы"}`
|
||||
- **THEN** 422 с указанием поля `wiki_root`, корень прежний, пакета нет
|
||||
|
||||
#### Scenario: Сброс корня
|
||||
- **WHEN** у `AWPCA` синхронизировано 120 страниц, `PATCH /projects/AWPCA {wiki_root: null}`
|
||||
- **THEN** каталога `wiki/AWPCA/` нет, страниц и вложений проекта в базе нет; `GET /projects` — `wiki_root = null`
|
||||
|
||||
### Requirement: Синхронизация вики
|
||||
Команда `wiki_sync {label, root}` SHALL быть командой чтения модуля `wiki` (без копии базы) в пакете с `project =
|
||||
label`. Её SHALL ставить: `POST /wiki/sync {project}`, задание или смена корня, фоновая работа хаба — по каждому
|
||||
проекту с корнем, если с последней успешной или неудачной синхронизации прошло больше `wiki_auto_sync_hours`
|
||||
(настройка модуля, по умолчанию 24; `0` — выключено). Пока у проекта есть пакет `wiki_sync` не в терминальном статусе,
|
||||
новый не ставится: `POST /wiki/sync` отвечает 409 `sync_in_progress` с `batch_id` текущего.
|
||||
|
||||
Агент `wiki` SHALL прислать все страницы поддерева корня (включая корень) пачками `POST /agent/wiki/pages {command_id,
|
||||
pages}` — каждая страница `{code, eva_id, title, parent_code, orderno, url, html, attachments: [{url, name}]}`, в
|
||||
пачке не больше 50 страниц, — и отчитаться `done` с `result = {root_title, codes, eva_calls}`, где `codes` — коды всех
|
||||
страниц дерева в порядке обхода. Хаб SHALL складывать пачки во временную стадию команды, не трогая копию. Пачка по
|
||||
команде не в `claimed` или не `wiki_sync` — 409; неверная форма страницы — 422 `bad_pages` с кодом страницы и полем.
|
||||
|
||||
По отчёту `done` хаб SHALL одной операцией заменить копию проекта (см. «Markdown-копия вики»), страницы, индекс поиска и
|
||||
вложения и записать `synced_at`, `pages_count`, `root_title`, очистив `sync_error`. Код из `codes` без присланной
|
||||
страницы — команда `failed` «нет страницы <код>». Отчёт `failed` или перевод в `failed` по пределу выдач SHALL записать
|
||||
`sync_error`; копия, страницы и вложения остаются прежними. Повторная выдача команды SHALL очищать её стадию. Стадия
|
||||
очищается при любом терминальном статусе команды.
|
||||
|
||||
#### Scenario: Первая синхронизация
|
||||
- **WHEN** у `AWPCA` корень DOC-1 с детьми DOC-2, DOC-3; агент прислал три страницы одной пачкой и `done {root_title:
|
||||
"АРМ КА", codes: [DOC-1, DOC-2, DOC-3]}`
|
||||
- **THEN** в `wiki/AWPCA/` три `index.md`; `GET /projects` — `wiki_pages = 3`, `wiki_root_title = АРМ КА`,
|
||||
`wiki_synced_at` заполнен
|
||||
|
||||
#### Scenario: Неудачная синхронизация не портит копию
|
||||
- **WHEN** копия `AWPCA` из 3 страниц, следующая синхронизация завершилась `failed` «Eva: HTTP 503»
|
||||
- **THEN** копия и поиск прежние; `wiki_sync_error = "Eva: HTTP 503"`
|
||||
|
||||
#### Scenario: Потерянная пачка
|
||||
- **WHEN** отчёт `done` с `codes: [DOC-1, DOC-2]`, пачки пришли только для DOC-1
|
||||
- **THEN** команда `failed` «нет страницы DOC-2», копия прежняя
|
||||
|
||||
#### Scenario: Страница удалена в Eva
|
||||
- **WHEN** в прошлой копии были DOC-1, DOC-2, DOC-3, новая синхронизация вернула DOC-1, DOC-3
|
||||
- **THEN** каталога DOC-2 нет, поиск его не находит, его вложения удалены
|
||||
|
||||
#### Scenario: Повторный запрос синхронизации
|
||||
- **WHEN** пакет `wiki_sync` проекта `AWPCA` выполняется, вызван `POST /wiki/sync {project: AWPCA}`
|
||||
- **THEN** 409 `sync_in_progress` с `batch_id` текущего пакета, нового пакета нет
|
||||
|
||||
### Requirement: Markdown-копия вики
|
||||
Копия проекта SHALL лежать в `<JIRA_HUB_HOME>/wiki/<метка>/` (каталоги 0700): у каждой страницы — каталог
|
||||
`NN_<название>` внутри каталога родителя (`NN` — двузначная позиция среди соседей по `orderno`, затем по коду; у корня
|
||||
номера нет; название без символов `<>:"/\|?*` и управляющих, обрезано до 80 символов; совпадение у соседей —
|
||||
суффикс `_2`, `_3`), в нём `index.md`: frontmatter `code`, `title`, `url`, `synced_at`, строка `# <title>` и текст. В
|
||||
корне копии — `_tree.json`, список `{code, title, depth, path}` в порядке обхода. Копия SHALL собираться рядом
|
||||
(`wiki/.<метка>.new/`) и подменять прежнюю переименованием; незавершённую подмену хаб SHALL доводить при старте.
|
||||
|
||||
HTML страницы SHALL переводиться в Markdown (заголовки `#`); ссылка или картинка на файл Eva (тот же хост, путь с
|
||||
`files`, `file`, `attachments`, `attachment`, `download`, `media` или `upload`) SHALL заменяться на
|
||||
`[📎 <имя>](attachment:<id>)`, где `id` — строка вложения; ссылка на документ Eva с кодом `DOC-<цифры>` — на
|
||||
`[<текст>](page:<код>)`; прочие ссылки остаются. Сырой HTML в копию не попадает.
|
||||
|
||||
#### Scenario: Раскладка
|
||||
- **WHEN** корень DOC-1 «АРМ КА», дети DOC-3 «Статусы» (`orderno` 1) и DOC-2 «Роли/права» (`orderno` 2)
|
||||
- **THEN** `wiki/AWPCA/АРМ КА/index.md`, `…/01_Статусы/index.md`, `…/02_Роли_права/index.md`; `_tree.json` в порядке
|
||||
DOC-1, DOC-3, DOC-2
|
||||
|
||||
#### Scenario: Картинка на странице
|
||||
- **WHEN** в HTML DOC-3 `<img src="/files/abc/scheme.png">` того же хоста
|
||||
- **THEN** в `index.md` — `[📎 scheme.png](attachment:<id>)`, у вложения `<id>` `state = none`, файл не скачан
|
||||
|
||||
### Requirement: Вложения вики по запросу
|
||||
`POST /wiki/attachments/{id}/fetch` SHALL ставить пакет из одной команды чтения `wiki_fetch {attachment_id, url}` и
|
||||
переводить вложение в `fetching`; для `fetching` — ответ текущим состоянием без нового пакета, для `ready` — сразу
|
||||
`ready`. Агент `wiki` SHALL скачать файл с токеном Eva, отказаться от файла больше предела хранилища ядра (10 МБ) с
|
||||
ошибкой «файл больше 10 МБ — откройте в Eva», загрузить файл `POST /agent/files` и отчитаться `done {file, size}`. Хаб
|
||||
SHALL проверить, что файл есть в `files/` и размер совпадает, и перевести вложение в `ready`; иначе команда и вложение
|
||||
`failed`. Файлы вложений в `ready` SHALL считаться ссылками для уборки `files/`. `GET /wiki/attachments/{id}` отдаёт
|
||||
`{id, name, page_code, url, state, size, error}`, `GET /wiki/attachments/{id}/content` — содержимое (`ready`) или 409
|
||||
`not_ready`.
|
||||
|
||||
#### Scenario: Скачивание картинки
|
||||
- **WHEN** вложение 7 в `none`, `POST /wiki/attachments/7/fetch`, агент прислал файл 300 КБ
|
||||
- **THEN** вложение 7 `ready`, `size = 307200`, `GET /wiki/attachments/7/content` — файл
|
||||
|
||||
#### Scenario: Слишком большой файл
|
||||
- **WHEN** агент отчитался `failed` «файл больше 10 МБ — откройте в Eva»
|
||||
- **THEN** вложение `failed` с этим текстом; `GET …/content` — 409 `not_ready`
|
||||
|
||||
#### Scenario: Вложение переживает синхронизацию
|
||||
- **WHEN** вложение 7 `ready`, синхронизация вернула ту же страницу с той же ссылкой
|
||||
- **THEN** у вложения тот же `id` 7 и `state = ready`, файл в `files/` не убран
|
||||
|
||||
### Requirement: Маршруты вики
|
||||
Хаб SHALL предоставлять под клиентским токеном (проект — `project=<метка>`, неизвестный — 404, без корня — 409
|
||||
`no_root`):
|
||||
- `GET /wiki/status?project=` — `{root, root_title, synced_at, pages, error, batch: {id, status} | null}`;
|
||||
- `GET /wiki/tree?project=` — список `{code, title, depth, parent_code, size}` в порядке обхода;
|
||||
- `GET /wiki/pages/{code}?project=` — `{code, title, path, url, synced_at, markdown, attachments: [{id, name, state,
|
||||
size}]}`; страницы нет в копии — 404;
|
||||
- `GET /wiki/search?project=&q=&limit=` (`limit` 1–50, по умолчанию 10) — `[{code, title, path, snippet}]` по
|
||||
убыванию релевантности; слова запроса ищутся по префиксу, пустой запрос — 422;
|
||||
- `POST /wiki/sync {project, author?}` → 201 `{batch_id}` (409 — см. «Синхронизация вики»);
|
||||
- маршруты вложений — см. «Вложения вики по запросу».
|
||||
|
||||
Под токеном агента `wiki` — `POST /agent/wiki/pages` (см. «Синхронизация вики»).
|
||||
|
||||
#### Scenario: Поиск
|
||||
- **WHEN** в копии `AWPCA` страница DOC-3 «Статусы» содержит «статус «На согласовании» ставит аналитик»;
|
||||
`GET /wiki/search?project=AWPCA&q=согласов`
|
||||
- **THEN** первым — DOC-3 с фрагментом, содержащим «На согласовании»
|
||||
|
||||
#### Scenario: Проект без корня
|
||||
- **WHEN** у `sfm_kedr` нет корня, `GET /wiki/tree?project=sfm_kedr`
|
||||
- **THEN** 409 `no_root`
|
||||
Loading…
x
Reference in New Issue
Block a user