310 lines
30 KiB
Markdown
310 lines
30 KiB
Markdown
# wiki Specification
|
||
|
||
## Purpose
|
||
Модуль `wiki`: корень EVA Wiki у проекта, Markdown-копия дерева страниц, синхронизация через агента `wiki` (eva-agent), поиск, вложения по запросу и маршруты вики.
|
||
|
||
## 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_sync` (`POST /batches`, повтор команды `POST /commands/{id}/retry`) SHALL отклоняться с 422, если у
|
||
проекта уже есть команда `wiki_sync` с тем же `root` в `queued` или `claimed`; текст ошибки называет пакет идущей
|
||
синхронизации. Команда с другим `root` не мешает: смена корня ставит синхронизацию нового корня, пока старая ещё
|
||
выдана агенту. Смена или сброс корня SHALL отменять невыданные (`queued`) пакеты `wiki_sync` проекта и при новом корне ставить новый; отчёт `done` по
|
||
команде, чей `root` не равен текущему корню проекта, SHALL отбрасываться без изменения копии и данных
|
||
(`result.discarded = "корень сменился"`).
|
||
|
||
Агент `wiki` SHALL прислать все страницы поддерева корня (включая корень) пачками `POST /agent/wiki/pages {command_id,
|
||
pages}` — каждая страница `{code, eva_id, title, parent_code, orderno, url, html, attachments: [{ref, url, name}],
|
||
links: [{ref, code}]}`, где `attachments` — ссылки и картинки на файлы Eva, `links` — ссылки на документы Eva, `ref` —
|
||
значение атрибута `src`/`href` ровно как в `html`, `url` — абсолютный адрес, `name` — `alt`/`title`, текст ссылки или
|
||
имя файла; в пачке не больше 50 страниц, — и отчитаться `done` с `result = {root_title, codes, eva_calls}`, где
|
||
`codes` — коды всех страниц дерева в порядке обхода. Хаб SHALL при приёме пачки переводить HTML в Markdown вне
|
||
транзакции записи и складывать результат во временную стадию команды, не трогая копию. Пачка по
|
||
команде не в `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** у `AWPCA` корень DOC-12 и пакет `wiki_sync` в `queued`; `PATCH /projects/AWPCA {wiki_root: "DOC-21"}`
|
||
- **THEN** прежний пакет `cancelled`, поставлен `wiki_sync {root: DOC-21}`
|
||
|
||
#### Scenario: Поздний отчёт по старому корню
|
||
- **WHEN** `wiki_sync {root: DOC-12}` выдан агенту, корень сменён на DOC-21, агент отчитался `done`
|
||
- **THEN** копия и страницы `AWPCA` не изменились, команда `done` с `result.discarded`
|
||
|
||
#### Scenario: Повторный запрос синхронизации
|
||
- **WHEN** пакет `wiki_sync` проекта `AWPCA` выполняется, вызван `POST /wiki/sync {project: AWPCA}`
|
||
- **THEN** 409 `sync_in_progress` с `batch_id` текущего пакета, нового пакета нет
|
||
|
||
#### Scenario: Повтор упавшей синхронизации во время идущей
|
||
- **WHEN** команда `wiki_sync {label: AWPCA, root: DOC-1}` 40 в `failed`, фоновая работа поставила пакет 41 с
|
||
`wiki_sync {root: DOC-1}` (`queued`); `POST /commands/40/retry`
|
||
- **THEN** 422, в тексте — пакет 41; нового пакета нет
|
||
|
||
#### Scenario: Смена корня при выданной синхронизации
|
||
- **WHEN** `wiki_sync {root: DOC-12}` выдан агенту (`claimed`); `PATCH /projects/AWPCA {wiki_root: "DOC-21"}`
|
||
- **THEN** 200, поставлен `wiki_sync {root: DOC-21}`
|
||
|
||
### Requirement: Markdown-копия вики
|
||
Копия проекта SHALL лежать в `<SDLC_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 (по списку `attachments` агента: адрес на хосте Eva
|
||
с путём `/files/…`, относительный адрес — тоже Eva; миниатюра `<адрес>.meta/thumbnail.jpg` означает файл `<адрес>`, а
|
||
ссылки на `static/…` — значки интерфейса — из текста удаляются) SHALL заменяться на
|
||
`[📎 <имя>](attachment:<id>)`, где `id` — строка вложения; ссылка на документ Eva с кодом `DOC-<цифры>` (в Eva — относительный
|
||
`project/Document/DOC-…`) — на `[<текст>](page:<код>)`; прочие ссылки остаются. Сырой HTML в копию не попадает.
|
||
|
||
Перевод SHALL давать корректный Markdown на любом HTML Eva:
|
||
- картинка внутри ссылки на то же вложение — на любой глубине вложенности (`<a><span><img></span></a>`) — даёт одну
|
||
ссылку на вложение;
|
||
- вложение внутри ссылки на документ Eva выводится отдельной ссылкой на вложение перед ссылкой на страницу; текст
|
||
ссылки на страницу — её текст без вложения, пустой — код страницы; ссылка внутри ссылки не появляется;
|
||
- при нескольких записях `attachments` или `links` с одним `ref` действует первая;
|
||
- пробел и круглые скобки в адресе внешней ссылки или картинки кодируются (`%20`, `%28`, `%29`);
|
||
- текст ссылки не может экранировать её закрывающую скобку: хвостовая `\` экранируется.
|
||
|
||
#### 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/obj/CmfDocument/CmfDocument%3Aa08/CmfDocument%3A<uuid>/scheme.png?version=1">`
|
||
- **THEN** в `index.md` — `[📎 scheme.png](attachment:<id>)`, у вложения `<id>` `state = none`, файл не скачан
|
||
|
||
#### Scenario: Прикреплённый файл с миниатюрой
|
||
- **WHEN** на странице миниатюра `…/<uuid>/model.xlsx.meta/thumbnail.jpg?version=1`, значок `static/attach-file.png` и
|
||
ссылка на `…/<uuid>/model.xlsx`
|
||
- **THEN** одно вложение `model.xlsx` с адресом файла; в тексте одна ссылка `[📎 model.xlsx](attachment:<id>)`, значка нет
|
||
|
||
#### Scenario: Миниатюра в обёртке
|
||
- **WHEN** `<a href="…/model.xlsx"><span><img src="…/model.xlsx.meta/thumbnail.jpg"></span></a> текст`
|
||
- **THEN** в тексте `[📎 model.xlsx](attachment:<id>) текст`
|
||
|
||
#### Scenario: Ссылка на другую страницу
|
||
- **WHEN** в HTML `<a href="project/Document/DOC-019834">Модель данных</a>`
|
||
- **THEN** в `index.md` — `[Модель данных](page:DOC-019834)`
|
||
|
||
#### Scenario: Картинка-ссылка на другую страницу
|
||
- **WHEN** `<a href="project/Document/DOC-1"><img src="…/scheme.png"></a>`, `scheme.png` — вложение
|
||
- **THEN** в тексте `[📎 scheme.png](attachment:<id>) [DOC-1](page:DOC-1)`
|
||
|
||
#### Scenario: Пробелы и скобки во внешнем адресе
|
||
- **WHEN** `<a href="http://ex.com/a b(c).html">док</a>`
|
||
- **THEN** в тексте `[док](http://ex.com/a%20b%28c%29.html)`
|
||
|
||
#### Scenario: Обратная косая черта в конце текста
|
||
- **WHEN** `<a href="project/Document/DOC-1">путь C:\</a>`
|
||
- **THEN** ссылка на `page:DOC-1` с текстом «путь C:\» остаётся ссылкой при разборе Markdown
|
||
|
||
### Requirement: Вложения вики по запросу
|
||
`POST /wiki/attachments/{id}/fetch` SHALL ставить пакет из одной команды чтения `wiki_fetch {attachment_id, url}` и
|
||
переводить вложение в `fetching`; для `fetching` — ответ текущим состоянием без нового пакета, для `ready` — сразу
|
||
`ready`. Вложение в `ready`, чьего файла нет в `files/`, SHALL считаться нескачанным: запрос ставит скачивание заново.
|
||
Агент `wiki` SHALL скачать файл с токеном Eva, отказаться от файла больше предела хранилища ядра (10 МБ) с
|
||
ошибкой «файл больше 10 МБ — откройте в Eva», загрузить файл `POST /agent/files` и отчитаться `done {file, size}`. Хаб
|
||
SHALL проверить, что файл есть в `files/` и размер совпадает, и перевести вложение в `ready`; иначе команда и вложение
|
||
`failed`. Адрес не на хосте Eva из конфига агента агент SHALL отвергать без запроса (`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/` не убран
|
||
|
||
#### Scenario: Файл вложения пропал
|
||
- **WHEN** вложение 7 `ready`, его файла нет в `files/`; `POST /wiki/attachments/7/fetch`
|
||
- **THEN** 202, вложение 7 `fetching`, поставлен пакет `wiki_fetch {attachment_id: 7}`
|
||
|
||
### 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=` — `{"pages": [{code, title, depth, parent_code, size}]}` в порядке обхода;
|
||
- `GET /wiki/pages/{code}?project=` — `{code, title, path, url, synced_at, markdown, attachments: [{id, name, state,
|
||
size, error, image}]}`; `image` — `true`, если имя файла в адресе вложения (последний сегмент пути без запроса,
|
||
раскодированный) оканчивается на `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.bmp` без учёта регистра, иначе `false`
|
||
(имя вложения из Eva для этого не годится: у картинок это часто `alt` без расширения); страницы нет в копии — 404;
|
||
- `GET /wiki/search?project=&q=&limit=` (`limit` 1–50, по умолчанию 10) — `{"results": [{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`
|
||
|
||
#### Scenario: Признак картинки
|
||
- **WHEN** у вложения 9 страницы DOC-3 имя «Схема расчёта суммы элиминации по тройке», адрес
|
||
`…/vgo-matching-1.png?version=1`; у вложения 8 имя `attachment`, адрес `…/model.xlsx`
|
||
- **THEN** в `GET /wiki/pages/DOC-3?project=AWPCA` у 9 `image = true`, у 8 `image = false`
|
||
|
||
### 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`
|