## 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`