From 98842208f52de3bc1b7ad2ceaab47c1b8f314701 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 11:31:32 +0400 Subject: [PATCH] =?UTF-8?q?OpenSpec:=20change=20wiki-history=20=E2=80=94?= =?UTF-8?q?=20=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BE=D0=BA=20=D0=B2=D0=B8=D0=BA=D0=B8,=20=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B8=20=D0=B4=D0=B8=D1=84=D1=84=20?= =?UTF-8?q?(#140)?= 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-history/.openspec.yaml | 2 + openspec/changes/wiki-history/design.md | 153 ++++++++ openspec/changes/wiki-history/proposal.md | 60 +++ .../changes/wiki-history/specs/mcp/spec.md | 80 ++++ .../wiki-history/specs/web-app/spec.md | 357 ++++++++++++++++++ .../changes/wiki-history/specs/wiki/spec.md | 102 +++++ 6 files changed, 754 insertions(+) create mode 100644 openspec/changes/wiki-history/.openspec.yaml create mode 100644 openspec/changes/wiki-history/design.md create mode 100644 openspec/changes/wiki-history/proposal.md create mode 100644 openspec/changes/wiki-history/specs/mcp/spec.md create mode 100644 openspec/changes/wiki-history/specs/web-app/spec.md create mode 100644 openspec/changes/wiki-history/specs/wiki/spec.md diff --git a/openspec/changes/wiki-history/.openspec.yaml b/openspec/changes/wiki-history/.openspec.yaml new file mode 100644 index 0000000..1bb6456 --- /dev/null +++ b/openspec/changes/wiki-history/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-28 diff --git a/openspec/changes/wiki-history/design.md b/openspec/changes/wiki-history/design.md new file mode 100644 index 0000000..3ee6b8a --- /dev/null +++ b/openspec/changes/wiki-history/design.md @@ -0,0 +1,153 @@ +## Context + +Модуль `wiki` держит Markdown-копию дерева страниц проекта: каждая `wiki_sync` перекачивает всё дерево, `sync.commit` +подменяет копию целиком. Истории нет: ни времени правки, ни автора, ни прошлых текстов (ARCHITECTURE.md, «Открытые +вопросы»). Владелец 28.10.2026 хотел из чата узнать, что за день сделали аналитики, и упёрся в это (sdlc-hub#140). + +### Что известно об API Eva + +Проверено 28.10.2026 на живой Eva (`https://skap.space.rshb.pro`) токеном eva-agent, скрипты `eva_probe.py` и +`eva_probe2.py` (только чтение; отчёты — в общей папке `probe/`). Публичная спека EvaTeam API v1.9.22 (69 методов) полей +не описывает, версий документов в ней нет. + +- `CmfDocument` с `fields: ["*"]` отдаёт ~120 полей, среди них `cmf_modified_at`, `cmf_modified_by_id`, + `cmf_author_id`, `cmf_version`, `doc_version`, `text`, `text_draft`, `cur_workflow_version_id`, + `cur_published_version_id`; поле `history` — список ссылок на версии `CmfDocumentHistory`. Ссылочные поля по имени + (`cmf_modified_by`, `cmf_author`) или через `fields: ["**"]` раскрываются в объект с `name` и `login`. +- `CmfDocumentHistory.list` с фильтром `[["parent_id", "==", ""]]` работает; фильтр `parent.code` падает + (`-32001`, вложенный фильтр — как у `CmfDocument.parent`). Поля версии: `code` (`DOCH-…`), `cmf_created_at`, + `cmf_modified_at`, `cmf_author_id`, `text` (полный HTML страницы на момент версии, у проверенных — 43–56 тыс. + символов), `has_published`, `cur_workflow`, `doc_version`, `approved*`, `status_id`. +- Пример DOC-101252: шесть версий; четыре — Дмитрий Соснов 27.10 (две из них опубликованы), две — Илья Молчанов в + августе. Текущий черновик (`cur_workflow = true`, `has_published = false`) Eva дописывает на месте; создание новой + версии меняет `cmf_modified_at` предыдущей. Отдельного поля «время публикации» в проверенных полях нет. +- `CmfAudit` есть (`audit_data`, `html_diff_data`, `operate`…), но все поля токену агента закрыты (`deny`). +- `CmfAttachment`: запись файла заменяется на месте (новая `cmf_version`, `cmf_modified_at`), истории версий файла + нет; классов `CmfAttachmentHistory`, `CmfFileVersion` и подобных нет (`Method not found`). + +### Решения владельца (brainstorming 28.10.2026) + +1. Нужна лента правок за период и полный дифф каждой правки; сценарий — «что за сегодня сделали аналитики». +2. Только зона видимости проекта — страницы дерева корня, не вся вики. +3. Только опубликованные правки; черновики не берём. +4. Версии файлов не нужны: свежий файл по запросу, как сейчас. +5. Обновление отдельно от чтения: `sync` отвечает «ок» или ошибкой; `changes`, `diff`, `page` читают локальную копию. +6. Веб: общая «Лента» в боковой панели с переключателями источников (Jira, Вики); нажатие на правку — окно с диффом + поверх ленты. + +## Goals / Non-Goals + +**Goals:** +- Опубликованные версии страниц дерева проекта — в хабе, с автором, временем и Markdown. +- Лента правок за период и дифф правки — в HTTP API, MCP и вебе; без обращения к Eva при чтении. +- Инкрементальная догрузка: Eva читается только по новым версиям. +- Агент v1 и хаб нового кода работают вместе (история просто не пополняется). + +**Non-Goals:** см. proposal.md. + +## Decisions + +1. **Источник — `CmfDocumentHistory`, только `has_published = true`.** Черновик меняется на месте и не нужен по решению + владельца 3. Опубликованная версия считается неизменной: агент не перечитывает известные коды. +2. **Постраничная выборка по дереву, а не запрос по проекту Eva.** Агент после обхода дерева для каждой страницы дерева + вызывает `CmfDocumentHistory.list` с фильтром `parent_id` (id страницы из обхода) и полями без текста. Так Eva не + отдаёт версии чужих страниц вообще (решение владельца 2), а страница, перенесённая в дерево позже, получает всю свою + историю. Цена: один вызов Eva на страницу за синхронизацию (у AWPCA 28 страниц). +3. **Инкремент — по известным кодам.** Хаб при выдаче `wiki_sync` (`CommandHooks.deliver_params`) дописывает в + параметры `known_versions: {<код страницы>: [<код версии>…]}` — коды версий, которые уже лежат в хабе у страниц + текущего дерева проекта; в `command.params` в базе этого поля нет. Агент запрашивает текст (`CmfDocumentHistory.get` + по коду) только для опубликованных версий, которых нет в `known_versions` его страницы. Первый прогон — + `known_versions = {}`, то есть вся история. +4. **Отдельный маршрут для версий.** `POST /agent/wiki/versions {command_id, versions}` под токеном агента `wiki`; + версия — `{page_code, code, created_at, author: {login, name}, html, attachments, links}` (`attachments`/`links` — как + у страниц, по `html` этой версии); в пачке не больше 50 версий, срез по объёму (4 МБ) делает агент. Версии не + вкладываются в страницы: у страницы с долгой историей тело пачки упёрлось бы в 16 МБ. Хаб переводит HTML в Markdown + вне транзакции и кладёт в стадию версий команды (`wiki_version_stage`), как страницы. +5. **Отчёт.** `done` получает `result.versions` — число присланных версий. Поле есть — хаб применяет стадию версий; + поля нет (агент v1) — история проекта не меняется, копия страниц обновляется как прежде. Присланных кодов меньше, + чем `result.versions`, — команда `failed` «нет версий: пришло N из M». +6. **Хранение.** Таблица `wiki_version`: `label`, `page_code`, `code` (PK — `label, code`), `created_at` (UTC), + `author_login`, `author_name`, `markdown`, `added`, `removed`. Markdown версии — тем же `convert`, но ссылки на + вложения дают текст `📎 <имя>` без ссылки (у прошлых версий нет строк `wiki_attachment`, а новые заводить ради + истории незачем); ссылки на страницы — `page:<код>`, как в копии. У проекта хранится `history_at` — время последнего + применения истории (`null` — история не загружалась, агент v1); где именно — на исполнителя: колонка + `wiki_project` через шаг миграции схемы или своя таблица модуля без миграции. +7. **Время и автор правки.** Время — `cmf_created_at` версии, автор — `cmf_author` версии. Отдельного времени + публикации в API не нашлось; создание версии — начало правки. См. раздел «Decisions made without discussion». +8. **«+строк / −строк» — при записи.** `sync.commit` для каждой новой версии считает строки Markdown, добавленные и + удалённые против предыдущей версии той же страницы (по `created_at`, затем `code`); у первой версии — против пустого + текста. Число пишется в `added`/`removed`. Если новая версия встала между известными (позднее дошедшая история), + пересчитывается и следующая за ней. +9. **Запись атомарно с копией.** Версии пишет только `sync.commit` в той же транзакции, что и копию: новые из стадии + добавляются; версии страниц, которых нет в новом дереве, удаляются. Сброс корня (`projects._drop_copy`) удаляет все + версии проекта. Отброшенный отчёт по сменённому корню версии не трогает. +10. **Сбой чтения версий — сбой команды.** Ошибка Eva на шаге версий даёт `failed`, копия и история прежние. Шаг + версий проверен на живой Eva; отдельный режим «страницы без истории» усложнил бы отчёт ради редкого случая. +11. **Дифф считается по запросу.** `difflib.unified_diff` по строкам Markdown версии и предыдущей, контекст 3 строки, + заголовки `--- <код пред.>` / `+++ <код>`; у первой версии — против пустого. Отдаётся частями по 20 000 символов с + `offset`, как текст страницы. Готовый дифф не хранится: Markdown версий уже в базе. +12. **Чтение — только копия.** `GET /wiki/changes`, `…/versions`, `…/diff`, MCP `changes`/`diff`/`page` не ставят + команд и не ходят к агенту; свежесть — `synced_at` в ответе. Новых команд и частоты синхронизации change не вводит. +13. **MCP `sync` — короткий ответ.** Без `wait` — «поставлено: пакет N» (или «уже идёт: пакет N»); с `wait` — «ок: + синхронизировано ДД.ММ ЧЧ:ММ» или «ошибка: <текст>», или «не дождался за 300 с: пакет N». Сводки страниц и данных + в ответе нет (решение владельца 5). +14. **Веб: лента — страница ядра с источниками от модулей.** Страница «Лента» (`/app/feed`) переезжает в оболочку; + модули отдают ей источники через новое необязательное поле контракта `HubModule.feed`. Ядро ведёт период, + переключатели источников и общий порядок групп; модуль — свои запросы, группы, параметры (виды событий Jira) и окно + по нажатию. Ядро не знает терминов модулей (требование «Граница модулей в вебе»). +15. **Окно диффа — оверлей модуля wiki.** Как карточка задачи: адрес окна поверх фоновой страницы + (`location.state.background`), закрытие возвращает ленту с тем же периодом. Внутри — переход к предыдущей и + следующей правке страницы и ссылка «Открыть страницу». + +## Risks / Trade-offs + +- **Время правки — начало, не публикация.** Правку, начатую вчера и опубликованную сегодня, лента покажет вчерашней. + Смысл времени записан в спеке маршрута; если на живых данных лента разойдётся с тем, что видят в Eva, — отдельная + доработка (поиск времени публикации в Eva). +- **Опубликованная версия может меняться в Eva** (согласование, `approved_*`) — агент её не перечитает. Текст версии + при согласовании, по проверенным данным, не меняется. +- **Вызов Eva на страницу** — при сотнях страниц синхронизация удлиняется на сотни вызовов с паузой `wiki_pacing`. + Сейчас деревья — десятки страниц. +- **Объём базы.** Версия — до ~60 тыс. символов Markdown; 28 страниц × десятки версий — единицы-десятки МБ. Уборки + старых версий нет. +- **Агент v1 после выкатки хаба** — история пустая, лента вики пустая; MCP и веб показывают «история ещё не + загружена», пока не придёт отчёт агента v2. + +## Migration Plan + +1. Хаб: `git pull` → kickstart (владелец) → `make web` → `/mcp` reconnect. Новые таблицы создаются при подключении + (`CREATE TABLE IF NOT EXISTS` схемы модуля); прежний код с такой базой работает — таблицы он не читает. +2. eva-agent v2: `uv run sdlc-hub agent publish --module wiki ../eva-agent/src/agent.py`; агент обновится сам. +3. После `status` с v2 — владелец поднимает `wiki_min_agent_version` до 2; первая синхронизация v2 загружает всю историю. +4. Откат: прежний код хаба; таблицы версий остаются и не мешают. + +## Open Questions + +- Время публикации версии: есть ли поле, отличное от `cmf_created_at` (решение 7). Проверить на живых данных после + выкатки, если лента разойдётся с тем, что видят аналитики в Eva. + +## Decisions made without discussion + +- 28.10.2026 — **постраничная выборка версий (решение 2)** вместо одного запроса по проекту Eva, о котором говорилось + в brainstorming. Обоснование: строже держит зону видимости (решение владельца 2) и подхватывает историю перенесённых + страниц. Цена ошибки: лишние вызовы Eva на больших деревьях; переход на запрос по проекту меняет только агента. +- 28.10.2026 — **время правки = `cmf_created_at` версии (решение 7).** Обоснование: другого времени в API не найдено. + Цена ошибки: правка, опубликованная позже начала, попадает в ленту не в тот день. +- 28.10.2026 — **ссылки на вложения в версиях — текстом без ссылки (решение 6).** Обоснование: у прошлых версий нет + строк вложений, дифф по тексту от этого не теряет. Цена ошибки: из диффа нельзя открыть файл; в копии страницы ссылки + прежние. +- 28.10.2026 — **сбой шага версий валит всю синхронизацию (решение 10).** Цена ошибки: сломанный в Eva метод истории + остановит и обновление страниц до исправления агента. +- 28.10.2026 — **дифф по запросу, не хранится (решение 11)**; «+/−» хранится. Цена ошибки: время ответа `diff` на + больших страницах (difflib на ~1000 строк — миллисекунды). +- 28.10.2026 — **короткий ответ MCP `sync` (решение 13)** — формулировки мои; смысл — решение владельца 5. +- 28.10.2026 — **адрес общей ленты `/app/feed`**, `/app/jira/feed` перенаправляет на него; пункт «Лента» — сразу под + «Обзором». Цена ошибки: закладки на `/app/jira/feed` работают через перенаправление. +- 28.10.2026 — **ссылки «Лента» на «Обзоре»** (по вопросу ревьюера спеки): «Лента» уходит из группы «Jira» боковой + панели (требование «Спринты и бэклог»); в сводке Jira на «Обзоре» ссылка «Лента» ведёт на `/app/feed?sources=jira`, + в сводке вики добавлена ссылка «Правки» на `/app/feed?sources=wiki`. Цена ошибки: две ссылки на «Обзоре». +- 28.10.2026 — **адрес окна правки** `/app/wiki/pages/{code}/versions/{version}` (по вопросу ревьюера спеки); без + фоновой страницы в `location.state` — поверх «Вики» с этой страницей; «Предыдущая/Следующая правка» заменяют адрес + (`replace`), чтобы «назад» закрывал окно. Цена ошибки: ссылка на правку открывается не поверх ленты. +- 28.10.2026 — **фильтр по автору** в `GET /wiki/changes` и MCP `changes` — подстрока имени или логина без учёта + регистра. Цена ошибки: «Иван» находит и «Иванову». diff --git a/openspec/changes/wiki-history/proposal.md b/openspec/changes/wiki-history/proposal.md new file mode 100644 index 0000000..681808e --- /dev/null +++ b/openspec/changes/wiki-history/proposal.md @@ -0,0 +1,60 @@ +## Why + +Владелец 28.10.2026 хотел посмотреть, что за день сделали аналитики в вики проекта, и не смог: копия вики в хабе без +истории, MCP показывает только дату синхронизации. Ни кто правил, ни когда, ни что поменялось — не видно (sdlc-hub#140). +Открытый вопрос из ARCHITECTURE.md «История страниц вики» закрывается этим change. + +Проверка API Eva 28.10.2026 (скрипты `eva_probe.py`, `eva_probe2.py` на виртуалке, токеном eva-agent) показала, что +история есть: у каждой версии страницы (`CmfDocumentHistory`, коды `DOCH-…`) — полный текст на момент версии, автор, +время и признак публикации; выборка по странице — фильтром `parent_id`. Журнал `CmfAudit` токену агента закрыт, +истории версий вложений в API нет. + +## What Changes + +- **Версии страниц в синхронизации.** Команда `wiki_sync` дополнительно забирает опубликованные версии страниц дерева + проекта: постранично, только новые (хаб передаёт агенту уже известные коды версий), первый прогон — вся история. + Агент шлёт версии пачками новым маршрутом `POST /agent/wiki/versions`; хаб переводит их HTML в Markdown и вместе с + копией одной операцией записывает в новую таблицу версий. Версии страниц вне дерева не запрашиваются и не хранятся. +- **Маршруты истории.** `GET /wiki/changes` — лента опубликованных правок проекта за период (время, автор, страница, + «+строк / −строк»); `GET /wiki/pages/{code}/versions` — версии страницы; `GET /wiki/pages/{code}/diff` — unified diff + одной правки против предыдущей, частями. `GET /wiki/pages/{code}` получает поля последней правки. +- **MCP вики.** Новые инструменты `changes` и `diff`; шапка `page` — строка «изменено: когда, кто»; `sync` отвечает + только «ок»/ошибкой. `changes`, `diff`, `page` читают только копию хаба. +- **Веб: общая «Лента».** Лента изменений переезжает из раздела Jira в общий пункт «Лента» боковой панели (`/app/feed`) + с переключателями источников «Jira» и «Вики». Правки вики сгруппированы по странице; нажатие на правку открывает окно + с диффом поверх ленты. Модули дают ленте свои источники через контракт веб-модуля. +- **eva-agent** (aep/eva-agent#7, свой design-only change): шаг версий в `wiki_sync`, `AGENT_VERSION` 2. + +## Non-Goals + +- Черновики и неопубликованные правки — решение владельца 28.10.2026: только опубликованные. +- Версии файлов-вложений — решение владельца: свежий файл по запросу, как сейчас (`attachment`). +- Журнал аудита Eva (`CmfAudit`) — закрыт токену агента. +- Страницы вики вне дерева корня проекта — решение владельца: только зона видимости проекта. +- Запись в Eva, откат версий, комментарии к правкам. +- Ускорение фоновой синхронизации: частота прежняя (`wiki_auto_sync_hours`). + +## Capabilities + +### New Capabilities + +Нет. + +### Modified Capabilities + +- `wiki` — новые требования «История правок страниц» и «Маршруты истории вики». +- `mcp` — «MCP вики». +- `web-app` — «Лента изменений», «Оболочка и адреса»; новое требование «Правки вики в ленте». + +## Impact + +- Хаб: `modules/wiki/` — `schema` (таблицы версий и стадии версий), `commands` (`deliver_params`, учёт версий в + отчёте), `stage` (приём версий), `sync` (запись версий в `commit`, удаление вне дерева), `convert` (режим без id + вложений), `api` (новые маршруты), `mcp`, `projects` (сброс корня удаляет версии); новый модуль чтения истории + (лента, дифф). Тесты `tests/test_wiki_*`. +- Веб: `web/src/shell/` (страница общей ленты, период), `web/src/modules/types.ts` (источник ленты в `HubModule`), + `web/src/modules/jira/feed/` (источник Jira вместо страницы), `web/src/modules/wiki/` (источник вики, окно диффа). +- Контракт с агентом `wiki`: новый маршрут `POST /agent/wiki/versions`, поле `known_versions` в параметрах выдачи + `wiki_sync`, поле `versions` в отчёте `done`. Агент v1 работает как прежде — история не пополняется. +- Схема базы: две новые таблицы модуля `wiki`. Выкатка: `git pull` → kickstart хаба → `make web` → `/mcp` reconnect → + публикация eva-agent v2 → после `agent_status` с v2 владелец поднимает `wiki_min_agent_version`. diff --git a/openspec/changes/wiki-history/specs/mcp/spec.md b/openspec/changes/wiki-history/specs/mcp/spec.md new file mode 100644 index 0000000..2415ca2 --- /dev/null +++ b/openspec/changes/wiki-history/specs/mcp/spec.md @@ -0,0 +1,80 @@ +## MODIFIED Requirements + +### Requirement: MCP вики +`sdlc-hub mcp wiki [--project <метка>]` SHALL запускать по stdio MCP-сервер модуля `wiki` с именем `sdlc-wiki`, +ходящий в HTTP API хаба клиентским токеном; хаб недоступен — каждый инструмент отвечает «хаб не запущен: sdlc-hub serve». +Привязка к проекту проверяется так же, как у сервера jira («Привязка сессии к проекту»). Инструменты: + +| Инструмент | Параметры | Ответ | +|---|---|---| +| `projects` | — | таблица: метка, название, корень вики, страниц, последняя синхронизация; текущий проект отмечен | +| `project_create` | `label`, `name`, `wiki_root` (необязательно) | `POST /projects` с автором `claude`; проект и фрагмент `.mcp.json` с записью `sdlc-wiki` | +| `root_set` | `root` | `PATCH /projects/<текущий> {wiki_root}`; новый корень и id пакета синхронизации | +| `tree` | `code` (необязательно), `depth` (по умолчанию 2) | дерево от корня или от `code`: отступ, код, название, размер | +| `page` | `code` или `title`, `offset` (по умолчанию 0) | шапка (код, путь в дереве, адрес в Eva, дата синхронизации, строка «изменено: ДД.ММ ЧЧ:ММ <автор> (версий: N)» или «изменено: история не загружена», вложения с `id`) и Markdown с `offset` до 20 000 символов, в конце — «продолжение: offset=», если текст длиннее; `title` ищется точно, иначе — до 10 кандидатов с кодами | +| `search` | `query`, `limit` (по умолчанию 10) | страницы: код, название, путь, фрагмент | +| `changes` | `hours`, `since`, `until`, `author` (все необязательные) | правки страниц проекта за период (`GET /wiki/changes`), см. ниже | +| `diff` | `code`, `version` (необязательно), `offset` (по умолчанию 0) | шапка (код, название, путь, версия «N из M», автор, время, «+A / −R», предыдущая версия) и unified diff с `offset` до 20 000 символов, в конце — «продолжение: offset=», если дифф длиннее; без `version` — последняя правка | +| `attachment` | `id` | ставит скачивание, ждёт до 60 с; готово — путь к копии `downloads/wiki-/<имя>`; иначе состояние и подсказка повторить | +| `sync` | `wait` (по умолчанию `false`) | без `wait` — «поставлено: пакет N» или «уже идёт: пакет N»; с `wait` — ждёт до 300 с: «ок: синхронизировано ДД.ММ ЧЧ:ММ», «ошибка: <текст>» или «не дождался за 300 с: пакет N»; данных копии в ответе нет | +| `status` | — | корень, последняя синхронизация, загрузка истории (`history_at` или «история не загружена»), ошибка, пакет в работе, агент `wiki`: онлайн/офлайн, версия, публикация, обновление, запускатель (как `agent_status` сервера jira) | + +Инструменты, кроме `projects` и `project_create`, SHALL требовать привязки к проекту. В привязанной сессии без корня +(кроме `root_set`, `status`) ответ SHALL быть «у проекта <метка> нет корня вики: root_set DOC-…»; с корнем, но без +синхронизаций — «вики проекта ещё не синхронизирована: sync». Набор инструментов и их описания у сервера `sdlc-jira` +(модуль jira) SHALL остаться прежним. Инструменты `page`, `changes`, `diff` MUST NOT ставить команд: они читают только +копию хаба. + +`changes`: период — как у инструмента `changes` сервера jira («Лента изменений в MCP»): `hours` и `since` вместе — +ответ «либо hours, либо since»; без обоих — с начала текущих суток по местному времени машины MCP-сервера; с `hours` — +`since = сейчас − hours`; `since`/`until` без смещения — местное время машины MCP-сервера. `author` передаётся в хаб +как есть. Вид ответа: первая строка — период по местному времени («правки вики с 28.10 00:00 по 28.10 14:05»), вторая +— «синхронизировано: ДД.ММ ЧЧ:ММ»; затем правки, сгруппированные по странице; страницы — по времени последней правки, +новые сверху; у страницы строка «DOC-… <название> — <путь>», под ней правки по времени, новые сверху, каждая — «ЧЧ:ММ +<автор>: +A / −R (<версия>)», с датой «ДД.ММ» перед временем, если правка не в текущих сутках. Правок нет — «за период +правок нет»; `history_at = null` — «история ещё не загружена: нужен eva-agent v2 и sync»; `truncated` — последняя +строка «показаны последние N правок, сузьте период». Ошибка 422 хаба — её текст. + +Значения из Eva, которые инструменты выводят внутри строки ответа (названия страниц и корня, путь, имя вложения, +фрагмент поиска, кандидаты `page` по названию, имя автора правки), SHALL выводиться одной строкой: переводы строк +(`\r`, `\n`) и пробелы вокруг них заменяются одним пробелом. Тело страницы (Markdown после шапки `page`) и дифф после +шапки `diff` выводятся как есть. + +#### 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** запущен `sdlc-hub mcp jira --project AWPCA` +- **THEN** имя сервера `sdlc-jira`, список инструментов и описания совпадают с эталоном до change + +#### Scenario: Перевод строки в названии страницы +- **WHEN** у DOC-3 название «Статусы\nпуть: поддельный», вызван `page {code: DOC-3}` +- **THEN** первая строка ответа — «DOC-3 — Статусы путь: поддельный», строка «путь:» в шапке одна — настоящая + +#### Scenario: Что сделали аналитики сегодня +- **WHEN** сессия `AWPCA`, `changes {}` в 14:05; сегодня Соснов опубликовал две версии DOC-101252 в 11:02 и 12:40 +- **THEN** запрос `GET /wiki/changes?project=AWPCA&since=<сегодня 00:00 местного>`; в ответе строка DOC-101252 с + названием и путём, под ней «12:40 Дмитрий Соснов: +40 / −12 (DOCH-…)» и «11:02 Дмитрий Соснов: …»; команд не + поставлено + +#### Scenario: Дифф правки +- **WHEN** `diff {code: DOC-101252}`, у страницы 4 версии +- **THEN** шапка «версия 4 из 4», автор, время, «+A / −R», предыдущая версия; затем unified diff + +#### Scenario: Синхронизация с ожиданием +- **WHEN** `sync {wait: true}`, пакет завершился `done` в 14:05 +- **THEN** ответ — одна строка «ок: синхронизировано 28.10 14:05» + +#### Scenario: История ещё не загружена +- **WHEN** у агента `wiki` версия 1, `changes {}` +- **THEN** ответ содержит «история ещё не загружена: нужен eva-agent v2 и sync» diff --git a/openspec/changes/wiki-history/specs/web-app/spec.md b/openspec/changes/wiki-history/specs/web-app/spec.md new file mode 100644 index 0000000..cb42bc4 --- /dev/null +++ b/openspec/changes/wiki-history/specs/web-app/spec.md @@ -0,0 +1,357 @@ +## MODIFIED Requirements + +### Requirement: Лента изменений +Страница «Лента» (`/app/feed?project=<метка>`) SHALL быть страницей ядра и показывать события текущего проекта за +период из источников модулей: «Jira» — журнал изменений своих задач (`GET /changes` с `project`, `since`, `until`, +`kinds`), «Вики» — правки страниц (требование «Правки вики в ленте»). Источник показывается, если он доступен проекту +(«Jira» — всегда, «Вики» — у проекта есть корень вики). Доступных источников больше одного — над лентой +флажки источников «Jira», «Вики» (по умолчанию все доступные), выбор хранится в адресе (`sources=` через запятую; набор +по умолчанию в адрес не пишется; в адресе нет ни одного известного источника — набор по умолчанию); сняты все флажки — +сообщение «выберите хотя бы один источник», запросов нет. Доступный источник один — флажков нет. + +Период SHALL выбираться переключателем «2 часа» / «Сегодня» / «Вчера» / «7 дней» (по умолчанию «Сегодня»; границы — по +местному времени браузера: «Сегодня» — с начала текущих суток до сейчас, «Вчера» — с начала прошлых суток до начала +текущих) и храниться в адресе (`period=2h|today|yesterday|7d`; «Сегодня» в адрес не пишется); период общий для всех +источников. Виды событий Jira SHALL выбираться флажками «Статусы», «Описание», «Сроки», «Комментарии», «Прочие поля» +(по умолчанию все, кроме «Прочие поля»), видными, пока выбран источник «Jira», и храниться в адресе (`kinds=` через +запятую; набор по умолчанию в адрес не пишется; в адресе нет ни одного известного вида — набор по умолчанию); сняты все +флажки видов — сообщение «выберите хотя бы один вид» на месте событий Jira, запроса `GET /changes` нет. + +События SHALL быть сгруппированы: Jira — по задаче, Вики — по странице; группы обоих источников идут одним списком по +времени последнего события, новые сверху. У группы Jira: ключ — ссылка на карточку задачи (окно поверх ленты, +требование «Карточка задачи»), тема и текущий статус; под ней события по времени, новые сверху, каждое — время +(`ЧЧ:ММ`, с датой `ДД.ММ`, если событие не в текущих сутках), автор и текст: статус — «статус «было» → «стало»»; +описание — «описание изменено»; срок, плановое начало и окончание — «срок ДД.ММ → ДД.ММ» (дата по значению +`from_value`/`to_value`, иначе строка журнала; пустое — «—»); комментарий — «комментарий:» и первые 300 символов текста +без вики-разметки Jira (так, как текст читается отрендеренным: без `*`, `{code}`, `h3.`; у `[текст|адрес]` — текст, +`[~логин]` — «@логин») одной строкой с «…»; прочие поля — «<название поля>: «было» → «стало»» (значения до 80 символов +с «…»), где название — подпись из словаря приложения (исполнитель, спринт, метки, приоритет, тема, эпик, связь, +вложение, резолюция), иначе имя поля Jira. Подзаголовок страницы — число событий и границы периода. Событий нет ни в +одном выбранном источнике — сообщение «за период изменений нет»; `truncated` у источника — сообщение «показаны +последние N событий — выберите период короче» с названием источника. Ошибка хаба у источника — сообщение с кнопкой +«Повторить» на месте его событий; события других источников показываются. Ленту SHALL перечитывать завершение +обновления модуля-источника («Обновить из Jira», «Обновить вики») и возврат на вкладку браузера. + +#### Scenario: Что закрылось за два часа +- **WHEN** открыта лента проекта AWPCA, выбран период «2 часа», снят источник «Вики», сняты все виды, кроме «Статусы» +- **THEN** адрес — `/app/feed?project=AWPCA&period=2h&sources=jira&kinds=status`; запрос `GET + /changes?project=AWPCA&since=<сейчас − 2 ч>&kinds=status`; запроса `GET /wiki/changes` нет; задачи сгруппированы, у + каждой — события «ЧЧ:ММ автор: статус «В работе» → «Готово»» + +#### Scenario: Карточка из ленты +- **WHEN** в ленте нажат ключ AURORA-9002 +- **THEN** поверх ленты открыто окно карточки AURORA-9002; закрытие окна возвращает ленту с тем же периодом + +#### Scenario: Пустой период +- **WHEN** за выбранный период событий нет ни в Jira, ни в Вики +- **THEN** показано «за период изменений нет» + +#### Scenario: Вчера +- **WHEN** выбран период «Вчера», сейчас 22.10 14:05 местного времени +- **THEN** запросы источников с `since` = 21.10 00:00 и `until` = 22.10 00:00 местного времени + +#### Scenario: Комментарий с разметкой +- **WHEN** в ленте событие комментария с текстом `h3. Итог` и `*Готово*, см. [протокол|https://x.ru/p] и [~ivanov]` +- **THEN** строка события — «комментарий: Итог Готово, см. протокол и @ivanov» + +#### Scenario: Jira и Вики вперемешку +- **WHEN** сегодня в 10:00 сменился статус AURORA-1, в 11:00 опубликована правка DOC-3, в 12:00 — комментарий к AURORA-2 +- **THEN** группы сверху вниз: AURORA-2, DOC-3, AURORA-1 + +#### Scenario: Проект без вики +- **WHEN** у проекта `sfm_kedr` нет корня вики +- **THEN** флажков источников нет, показаны события Jira; запроса `GET /wiki/changes` нет + +#### Scenario: Ошибка одного источника +- **WHEN** `GET /wiki/changes` ответил 500, `GET /changes` — событиями +- **THEN** события Jira показаны; на месте правок вики — сообщение об ошибке с кнопкой «Повторить» + +### Requirement: Оболочка и адреса +Приложение SHALL открываться под префиксом `/app/`. Главная страница — «Обзор» ядра (`/app/?project=<метка>`, +требование «Обзор проекта»); страница ядра «Лента» — `/app/feed` (требование «Лента изменений»). Страницы модулей SHALL +лежать под префиксом имени модуля: модуль jira — `/app/jira/links` («Связи» — доска связей), `/app/jira/backlog` +(«Бэклог»), `/app/jira/sprint` («Спринт»), `/app/jira/kanban` («Kanban»; три последних — требование «Спринты и +бэклог»), `/app/jira/issues` (список задач) и `/app/jira/issues/{key}` (карточка задачи — окно поверх страницы, +требование «Карточка задачи»); модуль wiki — `/app/wiki` (требование «Вики проекта») и +`/app/wiki/pages/{code}/versions/{version}` (окно правки поверх страницы, требование «Правки вики в ленте»). Прежние адреса `/app/board` и `/app/jira/board` (на `/app/jira/links`), +`/app/issues`, `/app/issues/{key}`, `/app/jira/feed` (на `/app/feed`) SHALL перенаправлять на новые с сохранением +строки запроса, без новой записи истории. Неизвестный путь — страница «не найдено» со ссылками на «Обзор», «Ленту» и +разделы модулей. + +Боковая панель SHALL быть закреплена у левого края окна на всю его высоту и MUST NOT прокручиваться вместе со +страницей; если её содержимое не помещается по высоте, прокручивается сама панель. Сверху вниз панель SHALL содержать: +название «SDLC Hub» и кнопку «Свернуть панель»; выпадающий список проектов («<метка> — <название>», текущий отмечен, в +конце «+ Новый проект…»; требование «Проект в приложении»); пункты «Обзор» и «Лента»; разделы модулей группами — +заголовок группы — название модуля («Jira», «Вики»), под ним ссылки разделов модуля со значками (текущий отмечен +`aria-current="page"`); внизу — блок состояния модулей (требование «Состояние и обновление модулей»), переключатель +темы (требование «Тема») и меню с действием «Сменить токен». Свёрнутая панель SHALL быть узкой полосой с теми же +действиями значками: «Развернуть панель», метка текущего проекта (полностью — в подсказке), «Обзор», «Лента», разделы +(группы разделены линией), по значку состояния на модуль, тема, меню; названия — в подсказках и `aria-label`. Свёртка +SHALL храниться в `localStorage` под ключом `sdlc-hub-sidebar` (`collapsed`; без записи — развёрнута). + +На экранах уже 640 px панели в потоке нет: сверху SHALL быть закреплена строка с кнопкой «Меню», меткой текущего +проекта и сводной точкой состояния модулей (ошибка, если она есть у какого-нибудь модуля; иначе — пакет в работе; +иначе — все агенты на связи или нет), которая открывает панель. «Меню» открывает развёрнутую панель поверх страницы +модальным окном: фокус переходит в панель и остаётся в ней, страница под ней недоступна; панель закрывается выбором +раздела, клавишей Escape (если не открыто вложенное меню — тогда Escape закрывает только его) или щелчком мимо неё; фокус +возвращается на «Меню». Горизонтальной прокрутки страницы на любой ширине нет. + +Заголовок вкладки браузера SHALL быть «<название страницы> · <метка проекта> · SDLC Hub» (без проекта — без средней +части); название страницы даёт оболочка («Обзор», «Лента») или модуль. Неожиданная ошибка отрисовки страницы SHALL +показываться сообщением с кнопкой «Перезагрузить», не пустым экраном. + +#### Scenario: Прежний адрес доски связей +- **WHEN** открыт `/app/jira/board?project=AWPCA` +- **THEN** адрес — `/app/jira/links?project=AWPCA`, показана доска связей, в меню отмечен пункт «Связи», заголовок + вкладки — «Связи · AWPCA · SDLC Hub» + +#### Scenario: Переход между разделами +- **WHEN** на доске в боковой панели выбран раздел «Задачи» группы «Jira» +- **THEN** адрес — `/app/jira/issues` с текущим проектом, показан список задач; «назад» возвращает на доску + +#### Scenario: Раздел «Лента» +- **WHEN** на доске в боковой панели выбран пункт «Лента» +- **THEN** адрес — `/app/feed` с текущим проектом, показана лента изменений за сегодня; пункт отмечен в панели + +#### Scenario: Прежний адрес ленты +- **WHEN** открыт `/app/jira/feed?project=AWPCA&period=7d` +- **THEN** адрес заменён на `/app/feed?project=AWPCA&period=7d`, показана общая лента + +#### Scenario: Сменить токен +- **WHEN** в меню боковой панели выбрано «Сменить токен» +- **THEN** токен очищен, показан экран входа + +#### Scenario: Панель на месте при прокрутке +- **WHEN** доска прокручена до самого низа +- **THEN** боковая панель видна целиком на прежнем месте: разделы и блок состояния модулей доступны без прокрутки вверх + +#### Scenario: Свёрнутая панель +- **WHEN** нажата «Свернуть панель», страница перезагружена +- **THEN** панель свёрнута в полосу значков, доска шире; значок «Задачи» открывает список задач; видны два значка + состояния — Jira и Вики; «Развернуть панель» возвращает полную панель + +#### Scenario: Узкий экран +- **WHEN** ширина окна 390 px, нажата «Меню», в панели выбран раздел «Задачи» +- **THEN** панель открылась поверх страницы, фокус в панели; после выбора панель закрыта, фокус на «Меню», показан список + задач; горизонтальной прокрутки нет + +#### Scenario: Прежний адрес +- **WHEN** открыт `/app/issues/AURORA-1536?project=AWPCA` +- **THEN** адрес заменён на `/app/jira/issues/AURORA-1536?project=AWPCA`, карточка открыта поверх списка задач + +#### Scenario: Заголовок вкладки +- **WHEN** открыта страница «Вики» проекта `AWPCA` +- **THEN** заголовок вкладки — «Вики · AWPCA · SDLC Hub» + +### Requirement: Обзор проекта +Страница ядра «Обзор» (`/app/?project=<метка>`, пункт «Обзор» боковой панели) SHALL показывать шапку «Обзор» с +названием проекта и под ней карточку на каждый модуль, у которого есть данные для проекта: название модуля, агент (с +версией из `GET /agents`), свежесть с охватом, текущее событие пакета, вторичная кнопка обновления и сводка, которую +даёт модуль. Сводка jira: число своих задач проекта, из них открытых и просроченных (по снимку доски проекта), ссылки +«Доска связей», «Задачи», «Лента» (общая лента с источником Jira — `/app/feed?project=<метка>&sources=jira`). Сводка +wiki: число страниц, поле поиска по вики (Enter открывает «Вики» с этим поиском), ссылки «Вики» и «Правки» (общая +лента с источником Вики — `/app/feed?project=<метка>&sources=wiki`). Модуль без данных для проекта SHALL показывать карточку с объяснением и действием модуля (у +вики — «Привязать корень вики» ведёт на страницу «Вики»). Под карточками — «Последние пакеты проекта»: до 10 из +`GET /batches?project=<метка>`: номер, модуль, название, автор, статус, время создания. + +#### Scenario: Главная +- **WHEN** открыт `/app/` без проекта, последний выбранный — `AWPCA` +- **THEN** адрес `/app/?project=AWPCA`, две карточки — «Jira» и «Вики», список последних пакетов + +#### Scenario: Поиск по вики с обзора +- **WHEN** на «Обзоре» `AWPCA` в поле поиска вики введено «согласов» и нажат Enter +- **THEN** открыта «Вики» с результатами поиска «согласов» + +#### Scenario: Проект без вики +- **WHEN** «Обзор» проекта `sfm_kedr` без корня вики +- **THEN** карточка «Вики» с объяснением и ссылкой «Привязать корень вики» + +### Requirement: Спринты и бэклог +Модуль jira SHALL показывать для проекта с доской страницы по образцу Jira — только для просмотра: на них MUST NOT быть +действий, меняющих Jira (перетаскивания, кнопок спринтов, смены статуса или ранга). Пункты меню группы «Jira» — +«Связи», «Задачи» и по типу доски текущего проекта (`board_type` из `GET /projects`): `scrum` — «Бэклог» и +«Спринт»; `kanban` — «Kanban»; доски нет или тип неизвестен — новых пунктов нет, а прямой переход на их адрес +показывает «У проекта нет scrum-доски» («…kanban-доски» для «Kanban») со ссылкой на «Связи». Данные — `GET +/agile?project=<текущий>`; если доска есть, но её данных от агента нет (`board = null` или `board.fetched_at = null`) — +«Нужен агент v15 и обновление из Jira» с кнопкой «Обновить» блока состояния модуля; данные доски есть (`fetched_at` задан), а колонок нет (`columns = null`) — «Не удалось +прочитать настройки доски из Jira» и та же кнопка. Пока проекты не загружены, условные пункты меню скрыты; смена проекта +на странице, которой у нового проекта нет, оставляет адрес и показывает сообщение «У проекта нет …-доски». Даты +спринтов (ISO-8601 Jira) показываются «ДД.ММ» в часовом поясе браузера; «осталось N дн.» — от сегодня до конца спринта. + +Строка задачи и карточка задачи на этих страницах: ключ, значок типа, тема, исполнитель (или «—»), оценка (число или +«—»); выполненная — приглушена. Щелчок открывает карточку задачи (окно поверх страницы, как в списке задач). Оценки и +их суммы SHALL показываться числом не более чем с двумя знаками после запятой без хвостовых нулей (`0.1 + 0.2` — +«0.3», `3` — «3»). + +**Значок типа** SHALL различать по `issue_type` без учёта регистра: эпик («Эпик», «Epic», «Фича»), история +(«История», «Story»), задача («Задача», «Task»), ошибка («Ошибка», «Bug»), подзадача («Подзадача», «Sub-task», +«Subtask»); прочие и пустой тип — нейтральный значок. Значок — цветной квадрат с символом цвета `--color-text-on-primary`, контрастным в обеих темах, у каждого вида свои +цвет и символ (эпик — молния, история — закладка, задача — галочка, ошибка — круг, подзадача — два квадрата), в обеих +темах; название типа — в `aria-label` и подсказке. + +**Панель фильтров** — над содержимым «Бэклога», «Спринта» и «Kanban»: поиск «Ключ или тема» (подстрока без учёта +регистра в ключе или теме), выбор «Исполнитель» («все», «Без исполнителя», исполнители задач страницы по алфавиту), +переключатель «Скрыть выполненные» (только «Бэклог»; на «Спринте» и «Kanban» выполненное скрывают колонкой, сохранённое +значение там не применяется); на «Бэклоге» — кнопка «Эпики» (показать или скрыть панель эпиков); на «Спринте» — выбор +спринтов; на «Спринте» и «Kanban» — меню «Колонки». Кнопка «Сбросить фильтры» SHALL быть в панели всегда и снимать все +фильтры (поиск, исполнитель, эпик, «Скрыть выполненные»); пока ни один не задан, она неактивна — панель не сдвигается, +когда фильтр появляется или снимается. Фильтры действуют вместе +(И). Сохранённое значение исполнителя или эпика, которого нет в данных, SHALL оставаться выбранным и видимым в списке. + +**Запоминание:** свёрнутые секции, выбранный эпик, исполнитель, «Скрыть выполненные», видимость панели эпиков и скрытые +колонки SHALL храниться в `localStorage` под ключом `sdlc-hub-agile` — JSON по метке проекта и странице (`backlog`, +`sprint`, `kanban`); у разных страниц и проектов свои значения. Поиск и выбор спринтов не запоминаются. Нечитаемое или +закрытое хранилище — значения по умолчанию, страница работает. + +**«Бэклог»** (`/app/jira/backlog`), сверху вниз: секции активных спринтов, будущих спринтов, «Бэклог», затем +свёрнутый список «Прошлые спринты (N)». Заголовок секции спринта: имя, состояние, даты «ДД.ММ–ДД.ММ», цель (если есть), +«N задач», «S SP (выполнено D)» и «без оценки: U», если `U > 0` — итоги спринта, фильтры их не меняют; если фильтры +скрыли часть задач секции — ещё «показано K». Задачи секции — по рангу. Секция без задач показывает «Нет задач»; все +задачи скрыты фильтрами — «Нет задач по фильтрам». Любую секцию (спринта и «Бэклог») SHALL можно свернуть и развернуть +щелчком по заголовку (кнопка с `aria-expanded`); по умолчанию развёрнуты все. Прошлый спринт в списке — строка «имя · +закрыт ДД.ММ · выполнено X из N · S SP»; раскрытие показывает его задачи с пометкой «не выполнено» у невыполненных. + +Эпики (задачи вида «эпик») MUST NOT показываться строками в секциях «Бэклога». Панель «Эпики» — слева от секций: +пункт «Все задачи», затем эпики — каждая задача-эпик из данных страницы по рангу, затем эпики, на которые ссылается +`epic_key` задач, но строки которых в данных нет, по ключу; в конце — «Без эпика». Пункт эпика: тема (или ключ, если +темы нет), ключ, число задач эпика в секциях активных и будущих спринтов и «Бэклога». Выбор пункта (кнопка с +`aria-pressed`) фильтрует все секции, включая прошлые спринты; повторный выбор выбранного эпика возвращает «Все +задачи». При «Скрыть выполненные» выполненные эпики в панели не показываются (кроме выбранного). На узком экране +(контейнер страницы уже 900 px) панель SHALL становиться выбором «Эпик» в панели фильтров. + +«Скрыть выполненные» на «Бэклоге» скрывает выполненные задачи в секциях активных и будущих спринтов и «Бэклога»; +прошлые спринты показываются полностью. + +**«Спринт»** (`/app/jira/sprint`): выбранные спринты доски — по умолчанию все активные. Выбор спринтов — список с +отметками: активные, затем будущие (по дате начала, без даты — в конце); выбор хранится в адресе +`?sprint=[,…]` (переход без новой записи истории); идентификаторы, которых нет среди активных и будущих, +отбрасываются, если не осталось ни одного — все активные. Над доской — заголовок каждого выбранного спринта (имя, +даты, «осталось N дн.» или «срок прошёл» — только у активного, цель, суммы оценок), под ними колонки доски Jira с числом +задач; задачи доски — задачи выбранных спринтов, карточки в колонке — по рангу; колонка «Без колонки» — для статусов вне +настроек доски, только если в ней есть задачи. Нет активного спринта и ничего не выбрано — «Нет активного спринта» и +ссылка на «Бэклог»; если есть будущие спринты, выбор спринтов при этом доступен. + +**«Kanban»** (`/app/jira/kanban`): колонки доски из `board_issues`, карточки по рангу, у последней колонки подпись +«за 14 дней»; если `kanban_backlog = true` — над колонками свёрнутый список «Бэклог (N)» по рангу; поиск и исполнитель +действуют и на него. + +**Колонки доски** («Спринт» и «Kanban»): заголовок колонки — `h3`. Меню «Колонки» — отметки по колонкам доски (и «Без +колонки», если она есть); снятая отметка скрывает колонку, последнюю видимую скрыть нельзя (отметка недоступна); если +скрыты колонки — на кнопке их число. Доска SHALL занимать высоту окна под заголовком и панелью фильтров (не меньше +360 px; на окне уже 640 px — не меньше 240 px), все колонки — во всю высоту доски; карточки, не помещающиеся по высоте, прокручиваются внутри своей колонки. +Колонки, не помещающиеся по ширине, SHALL прокручиваться по горизонтали внутри доски, не страницы, и полоса горизонтальной +прокрутки доски SHALL находиться в пределах окна при любом числе карточек. Заголовок вкладки — «Бэклог», «Спринт», +«Kanban» по общему правилу оболочки. + +#### Scenario: Меню scrum-проекта +- **WHEN** текущий проект `AWPCA`, `board_type = scrum` +- **THEN** в группе «Jira» пункты «Связи», «Бэклог», «Спринт», «Задачи»; пункта «Kanban» нет + +#### Scenario: Проект без доски +- **WHEN** текущий проект `STOCKS_DRRK` без доски, открыт `/app/jira/backlog?project=STOCKS_DRRK` +- **THEN** пунктов «Бэклог», «Спринт», «Kanban» в меню нет; страница — «У проекта нет scrum-доски» со ссылкой на «Связи» + +#### Scenario: Бэклог только смотрит +- **WHEN** открыт «Бэклог» `AWPCA` +- **THEN** на странице нет перетаскиваемых элементов; кнопки и поля — только свёртка секций, панель фильтров, пункты + панели эпиков и открытие карточки задачи; ни одно из них не ставит команд + +#### Scenario: Прошлый спринт +- **WHEN** спринт `AWPCA_Sprint_2_VGO` закрыт, в нём 73 задачи, 61 в последней колонке, оценки у 10 задач на 25 SP +- **THEN** в «Прошлые спринты» строка «AWPCA_Sprint_2_VGO · закрыт ДД.ММ · выполнено 61 из 73 · 25 SP»; раскрытие — + 73 задачи, 12 с пометкой «не выполнено» + +#### Scenario: Агент без досок +- **WHEN** `AWPCA` scrum, агент v14 (данных доски нет), открыт «Спринт» +- **THEN** «Нужен агент v15 и обновление из Jira» и кнопка «Обновить» + +#### Scenario: Свёрнутый активный спринт +- **WHEN** на «Бэклоге» свёрнута секция активного спринта, страница перезагружена +- **THEN** секция свёрнута (видны заголовок и итоги, задач нет), остальные развёрнуты + +#### Scenario: Фильтр по эпику +- **WHEN** в бэклоге 3 задачи эпика `AURORA-10` и 5 других, в активном спринте 2 задачи `AURORA-10`; в панели «Эпики» + выбран `AURORA-10` +- **THEN** в секции спринта 2 задачи и «показано 2», в «Бэклоге» 3; строки самого `AURORA-10` в секциях нет; у пункта + `AURORA-10` в панели — 5 + +#### Scenario: Скрыть выполненные +- **WHEN** в активном спринте 10 задач, 4 выполнены; включено «Скрыть выполненные» +- **THEN** в секции 6 задач, заголовок — «10 задач» и «показано 6»; «Прошлые спринты» показывают все свои задачи + +#### Scenario: Исполнитель на доске спринта +- **WHEN** на «Спринте» выбран исполнитель `ivanov` +- **THEN** на доске только его карточки; числа в заголовках колонок — по видимым карточкам; на «Бэклоге» исполнитель не + выбран (свой фильтр у каждой страницы) + +#### Scenario: Будущий спринт на доске +- **WHEN** у доски активный спринт 101 и будущий 102; в выборе спринтов отмечен и 102 +- **THEN** адрес — `?sprint=101,102`; над доской два заголовка, «осталось N дн.» только у 101; на доске задачи обоих + +#### Scenario: Сброс фильтров на месте +- **WHEN** открыт «Спринт» без фильтров, затем выбран исполнитель +- **THEN** «Сбросить фильтры» видна и неактивна до выбора, после — активна на том же месте; флажка «Скрыть выполненные» + на «Спринте» нет + +#### Scenario: Скрытая колонка +- **WHEN** на «Спринте» в меню «Колонки» снята отметка «Выполнено», страница перезагружена +- **THEN** колонки «Выполнено» нет, на кнопке «Колонки» — 1; остальные колонки на месте + +#### Scenario: Широкая доска +- **WHEN** окно 2560 × 1300 px, у доски 12 колонок, в одной из них 40 карточек +- **THEN** все колонки одной высоты и помещаются в окно по высоте; полоса горизонтальной прокрутки доски видна в окне; + прокрутка доски вправо до конца показывает «Без колонки» целиком; страница горизонтально не прокручивается + +## ADDED Requirements + +### Requirement: Правки вики в ленте +Модуль wiki SHALL давать ленте источник «Вики»: правки страниц проекта за период (`GET /wiki/changes` с `project`, +`since`, `until`). Правки сгруппированы по странице; у группы — название страницы ссылкой на неё в разделе «Вики» +(`/app/wiki?project=<метка>&page=<код>`) и путь в дереве; под ней правки по времени, новые сверху, каждая — время +(`ЧЧ:ММ`, с датой `ДД.ММ`, если правка не в текущих сутках), автор (имя) и «+A / −R». `history_at = null` — на месте +правок вики сообщение «история вики ещё не загружена — нужен eva-agent v2». + +Нажатие на правку SHALL открывать окно поверх ленты по адресу `/app/wiki/pages/{code}/versions/{version}?project=<метка>` +(фоновая страница остаётся под ним, закрытие — кнопкой, Escape или «назад» — возвращает ленту с тем же периодом и +источниками). Адрес окна, открытый напрямую (перезагрузка, ссылка), показывает окно поверх страницы «Вики» с этой +страницей (`/app/wiki?project=<метка>&page=<код>`); закрытие оставляет её. Окно показывает: название и путь страницы, «правка N из M», +автора, время, «+A / −R» и дифф правки (`GET /wiki/pages/{code}/diff` с `version`): строки, добавленные и удалённые, — +цветами состояния успеха и ошибки из токенов темы с подписью для экранных читалок, строки `@@` — второстепенным цветом, +моноширинным шрифтом, без горизонтальной прокрутки страницы (длинная строка переносится или прокручивается внутри +окна). Дифф длиннее одной части — кнопка «Показать дальше» догружает следующую (`offset = next_offset`). Кнопки +«Предыдущая правка» и «Следующая правка» переключают версию той же страницы (по `GET /wiki/pages/{code}/versions`), +заменяя адрес окна без новой записи истории, — «назад» по-прежнему закрывает окно; у первой и последней +соответствующая кнопка недоступна. Ссылка «Открыть страницу» ведёт на страницу в разделе «Вики». +Окно только показывает: в Eva и в хабе ничего не меняет. + +Контракт веб-модуля SHALL получить необязательное поле источника ленты: ядро передаёт источнику проект, границы +периода и признак «выбран», а источник отдаёт свои группы со временем последнего события, состояние (загрузка, +ошибка, `truncated`, пустая история) и, при необходимости, свои настройки над лентой (виды событий Jira). Ядро MUST NOT +знать видов событий Jira и полей правок вики (требование «Граница модулей в вебе»). + +#### Scenario: Правка из ленты +- **WHEN** в ленте AWPCA за сегодня группа DOC-101252 «17.2.2.1 ЭФ «Ознакомление перед подписанием»» с правкой «12:40 + Дмитрий Соснов +40 / −12»; нажата правка +- **THEN** поверх ленты окно: «правка 4 из 4», Соснов, 12:40, «+40 / −12», дифф с подсвеченными строками; запрос + `GET /wiki/pages/DOC-101252/diff?project=AWPCA&version=DOCH-…` + +#### Scenario: Предыдущая правка +- **WHEN** в окне правки 4 из 4 нажата «Предыдущая правка» +- **THEN** показана правка 3 из 4 той же страницы; «Следующая правка» доступна; «назад» браузера закрывает окно и + возвращает ленту + +#### Scenario: Окно по ссылке +- **WHEN** открыт `/app/wiki/pages/DOC-101252/versions/DOCH-3159076?project=AWPCA` в новой вкладке +- **THEN** окно правки поверх страницы «Вики» с DOC-101252; закрытие окна — адрес `/app/wiki?project=AWPCA&page=DOC-101252` + +#### Scenario: Длинный дифф +- **WHEN** у правки `next_offset = 20000` +- **THEN** под диффом кнопка «Показать дальше»; нажатие догружает часть с `offset=20000` ниже уже показанной + +#### Scenario: История не загружена +- **WHEN** у проекта `history_at = null` +- **THEN** на месте правок вики — «история вики ещё не загружена — нужен eva-agent v2» + +#### Scenario: Окно на узком экране +- **WHEN** ширина окна 390 px, открыто окно правки с длинными строками Markdown +- **THEN** горизонтальной прокрутки страницы нет; строки диффа переносятся или прокручиваются внутри окна diff --git a/openspec/changes/wiki-history/specs/wiki/spec.md b/openspec/changes/wiki-history/specs/wiki/spec.md new file mode 100644 index 0000000..9c4de80 --- /dev/null +++ b/openspec/changes/wiki-history/specs/wiki/spec.md @@ -0,0 +1,102 @@ +## ADDED Requirements + +### Requirement: История правок страниц +Хаб SHALL хранить опубликованные версии страниц дерева проекта: код версии Eva (`DOCH-…`), код страницы, время правки +(UTC), автора (`login`, `name`), Markdown версии и число строк, добавленных и удалённых против предыдущей версии той же +страницы (порядок версий — по времени, затем по коду; у первой — против пустого текста). Время правки — время создания +версии в Eva (`cmf_created_at`). Версии страниц, которых нет в дереве проекта, хаб MUST NOT хранить; неопубликованные +версии (черновики) агент MUST NOT присылать. + +При выдаче `wiki_sync` хаб SHALL дописывать в параметры команды `known_versions` — объект `{<код страницы>: [<код +версии>…]}` по версиям, уже хранящимся у страниц текущей копии проекта (без копии — `{}`); в `command.params` в базе +поле MUST NOT записываться. Агент `wiki` SHALL после страниц прислать опубликованные версии страниц дерева, которых нет +в `known_versions` их страницы, пачками `POST /agent/wiki/versions {command_id, versions}`: версия — `{page_code, code, +created_at, author: {login, name}, html, attachments, links}` (`created_at` — ISO-8601 со смещением; `login` и `name` +— строки или `null`, если Eva автора не отдала; `attachments` и `links` — как у страниц, по `html` версии), в пачке не +больше 50 версий, — и отчитаться `done` с `result.versions` — +числом присланных версий. Пачка по команде не в `claimed` или не `wiki_sync` — 409; неверная форма версии — 422 +`bad_versions` с кодом версии и полем. Хаб SHALL переводить HTML версии в Markdown при приёме вне транзакции записи +(ссылки на вложения — текстом `📎 <имя>` без ссылки, ссылки на страницы — `page:<код>`) и складывать в стадию команды. + +По отчёту `done` с `result.versions` хаб SHALL в той же операции, что и копию (см. «Синхронизация вики»): добавить +версии из стадии к страницам, чьи коды есть в `codes`, отбросив остальные; удалить версии страниц, которых нет в +`codes`; посчитать «+/−» у новых версий и у версии, следующей за вставленной раньше неё; записать время применения +истории `history_at`. Присланных версий меньше `result.versions` — команда `failed` «нет версий: пришло N из M», копия и +история прежние. Отчёт `done` без `result.versions` (агент без истории) SHALL обновлять копию как прежде и не менять +историю и `history_at`. Отчёт `failed` и отброшенный отчёт по сменённому корню историю не меняют. Сброс корня SHALL +удалять историю проекта вместе с копией. Стадия версий очищается так же, как стадия страниц. + +#### Scenario: Первая загрузка истории +- **WHEN** у `AWPCA` дерево DOC-1, DOC-2; в хабе версий нет; выдан `wiki_sync` +- **THEN** в параметрах выдачи `known_versions = {}`; агент прислал 3 версии DOC-1 и 2 версии DOC-2, отчёт `done + {versions: 5}`; в хабе 5 версий, у первой версии каждой страницы «+N / −0», `history_at` заполнен + +#### Scenario: Догрузка новых версий +- **WHEN** у DOC-1 в хабе версии DOCH-10, DOCH-11; в Eva опубликована DOCH-12 +- **THEN** при выдаче `known_versions = {"DOC-1": ["DOCH-10", "DOCH-11"], …}`; агент прислал только DOCH-12; у неё + «+/−» против DOCH-11 + +#### Scenario: Агент без истории +- **WHEN** агент v1 отчитался `done {root_title, codes, eva_calls}` без `versions` +- **THEN** копия обновлена, версии и `history_at` прежние + +#### Scenario: Страница ушла из дерева +- **WHEN** у DOC-3 в хабе 4 версии; новая синхронизация вернула `codes` без DOC-3 +- **THEN** версий DOC-3 в хабе нет + +#### Scenario: Версия чужой страницы +- **WHEN** в стадии версия с `page_code = DOC-99`, которой нет в `codes` +- **THEN** версия не записана, команда `done` + +#### Scenario: Потерянная пачка версий +- **WHEN** отчёт `done {versions: 5}`, в стадии 3 версии +- **THEN** команда `failed` «нет версий: пришло 3 из 5», копия и история прежние + +### Requirement: Маршруты истории вики +Хаб SHALL предоставлять под клиентским токеном (проект — `project=<метка>`, неизвестный — 404, без корня — 409 +`no_root`), читая только базу хаба: +- `GET /wiki/changes?project=&since=&until=&author=&limit=` — правки страниц текущей копии проекта со временем в + `[since, until]`. `since`, `until` — ISO-8601 со смещением или `Z` (без смещения — 422); без `until` — текущее время; + без `since` — `until` минус 24 часа; `author` — подстрока имени или логина без учёта регистра; `limit` — по умолчанию + 500, 1–2000. Ответ — `{since, until, synced_at, history_at, truncated, changes: [{at, page_code, title, path, + version, author: {login, name}, added, removed}]}` по `at` по убыванию, затем по `version` по убыванию; правок больше + `limit` — первые `limit` и `truncated = true`. Неверное время, `since` позже `until`, `limit` вне 1..2000 — 422 + `bad_request` с именем параметра. `history_at = null` — история проекта ещё не загружалась. +- `GET /wiki/pages/{code}/versions?project=` — `{code, title, versions: [{version, at, author: {login, name}, added, + removed}]}`, новые первыми; страницы нет в копии — 404. +- `GET /wiki/pages/{code}/diff?project=&version=&offset=` — правка `version` (без параметра — последняя) против + предыдущей версии страницы: `{code, title, path, version, prev_version, at, author: {login, name}, added, removed, + index, total, offset, next_offset, diff}`, где `diff` — unified diff Markdown (контекст 3 строки; у первой версии — + против пустого, `prev_version = null`) с символа `offset` до 20 000 символов, `next_offset` — начало следующей части + или `null`, `index` — номер версии от старой (с 1), `total` — число версий. Страницы нет в копии — 404; версии нет у + страницы — 404 `no_version`; у страницы нет версий — 404 `no_versions`. + +`GET /wiki/pages/{code}` SHALL дополнительно отдавать `modified_at`, `modified_by: {login, name}` (последняя версия; +нет версий — `null`) и `versions` — число версий. `GET /wiki/status` SHALL дополнительно отдавать `history_at`. + +#### Scenario: Что сделали за сегодня +- **WHEN** у `AWPCA` 27.10 в 17:59 и 18:02 (MSK) Соснов опубликовал версии DOC-101252, 26.10 Молчанов — версию DOC-2; + `GET /wiki/changes?project=AWPCA&since=2026-10-27T00:00:00+03:00` +- **THEN** две правки DOC-101252, новая первой, у каждой автор `{login: "sosnov-dvi@rshbeco.ru", name: "Дмитрий + Соснов"}`, `added`/`removed`, путь страницы в дереве; правки DOC-2 нет + +#### Scenario: Фильтр по автору +- **WHEN** `GET /wiki/changes?project=AWPCA&since=…&author=соснов` +- **THEN** только правки Соснова + +#### Scenario: Дифф последней правки +- **WHEN** у DOC-101252 4 версии; `GET /wiki/pages/DOC-101252/diff?project=AWPCA` +- **THEN** `index = 4`, `total = 4`, `prev_version` — третья версия, `diff` начинается с `--- ` и `+++ ` + +#### Scenario: Длинный дифф +- **WHEN** дифф правки 45 000 символов, запрос без `offset` +- **THEN** первые 20 000 символов, `next_offset = 20000`; запрос с `offset=40000` — остаток, `next_offset = null` + +#### Scenario: История ещё не загружена +- **WHEN** у `AWPCA` копия есть, агент v1, `GET /wiki/changes?project=AWPCA` +- **THEN** 200, `history_at = null`, `changes = []` + +#### Scenario: Последняя правка в странице +- **WHEN** в хабе 4 версии DOC-101252, последняя — Соснов, `created_at = 2026-10-27T23:48:02+03:00`; + `GET /wiki/pages/DOC-101252?project=AWPCA` +- **THEN** `modified_at = "2026-10-27T20:48:02+00:00"`, `modified_by.name = "Дмитрий Соснов"`, `versions = 4`