## MODIFIED Requirements ### 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 лежать в `/wiki/<метка>/` (каталоги 0700): у каждой страницы — каталог `NN_<название>` внутри каталога родителя (`NN` — двузначная позиция среди соседей по `orderno`, затем по коду; у корня номера нет; название без символов `<>:"/\|?*` и управляющих, обрезано до 80 символов; совпадение у соседей — суффикс `_2`, `_3`), в нём `index.md`: frontmatter `code`, `title`, `url`, `synced_at`, строка `# ` и текст. В корне копии — `_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`