30 KiB
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 «Статусы» (
orderno1) и DOC-2 «Роли/права» (orderno2) - 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— 409not_ready
Scenario: Вложение переживает синхронизацию
- WHEN вложение 7
ready, синхронизация вернула ту же страницу с той же ссылкой - THEN у вложения тот же
id7 и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=(limit1–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у 9image = true, у 8image = 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 — 422bad_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; версии нет у страницы — 404no_version; у страницы нет версий — 404no_versions.
GET /wiki/pages/{code} SHALL дополнительно отдавать modified_at, modified_by: {login, name} (последняя версия;
нет версий — null) и versions — число версий. GET /wiki/status SHALL дополнительно отдавать history_at.
Scenario: Что сделали за сегодня
- WHEN у
AWPCA27.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