Andrey Epifancev 8e26e3a62e Архив OpenSpec: wiki-package-d (#135)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:02:52 +04:00

20 KiB
Raw Blame History

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