26 KiB
26 KiB
Context
Модуль wiki держит Markdown-копию дерева страниц проекта: каждая wiki_sync перекачивает всё дерево, sync.commit
подменяет копию целиком. Истории нет: ни времени правки, ни автора, ни прошлых текстов (ARCHITECTURE.md, «Открытые
вопросы»). Владелец 28.10.2026 хотел из чата узнать, что за день сделали аналитики, и упёрся в это (sdlc-hub#140).
Что известно об API Eva
Проверено 28.10.2026 на живой Eva (https://skap.space.rshb.pro) токеном eva-agent, скрипты eva_probe.py и
eva_probe2.py (только чтение; отчёты — в общей папке probe/). Публичная спека EvaTeam API v1.9.22 (69 методов) полей
не описывает, версий документов в ней нет.
CmfDocumentсfields: ["*"]отдаёт ~120 полей, среди нихcmf_modified_at,cmf_modified_by_id,cmf_author_id,cmf_version,doc_version,text,text_draft,cur_workflow_version_id,cur_published_version_id; полеhistory— список ссылок на версииCmfDocumentHistory. Ссылочные поля по имени (cmf_modified_by,cmf_author) или черезfields: ["**"]раскрываются в объект сnameиlogin.CmfDocumentHistory.listс фильтром[["parent_id", "==", "<id документа>"]]работает; фильтрparent.codeпадает (-32001, вложенный фильтр — как уCmfDocument.parent). Поля версии:code(DOCH-…),cmf_created_at,cmf_modified_at,cmf_author_id,text(полный HTML страницы на момент версии, у проверенных — 43–56 тыс. символов),has_published,cur_workflow,doc_version,approved*,status_id.- Пример DOC-101252: шесть версий; четыре — Дмитрий Соснов 27.10 (две из них опубликованы), две — Илья Молчанов в
августе. Текущий черновик (
cur_workflow = true,has_published = false) Eva дописывает на месте; создание новой версии меняетcmf_modified_atпредыдущей. Отдельного поля «время публикации» в проверенных полях нет. CmfAuditесть (audit_data,html_diff_data,operate…), но все поля токену агента закрыты (deny).CmfAttachment: запись файла заменяется на месте (новаяcmf_version,cmf_modified_at), истории версий файла нет; классовCmfAttachmentHistory,CmfFileVersionи подобных нет (Method not found).
Решения владельца (brainstorming 28.10.2026)
- Нужна лента правок за период и полный дифф каждой правки; сценарий — «что за сегодня сделали аналитики».
- Только зона видимости проекта — страницы дерева корня, не вся вики.
- Только опубликованные правки; черновики не берём.
- Версии файлов не нужны: свежий файл по запросу, как сейчас.
- Обновление отдельно от чтения:
syncотвечает «ок» или ошибкой;changes,diff,pageчитают локальную копию. - Веб: общая «Лента» в боковой панели с переключателями источников (Jira, Вики); нажатие на правку — окно с диффом поверх ленты.
Goals / Non-Goals
Goals:
- Опубликованные версии страниц дерева проекта — в хабе, с автором, временем и Markdown.
- Лента правок за период и дифф правки — в HTTP API, MCP и вебе; без обращения к Eva при чтении.
- Инкрементальная догрузка: Eva читается только по новым версиям.
- Агент v1 и хаб нового кода работают вместе (история просто не пополняется).
Non-Goals: см. proposal.md.
Decisions
- Источник —
CmfDocumentHistory, толькоhas_published = true. Черновик меняется на месте и не нужен по решению владельца 3. Опубликованная версия считается неизменной: агент не перечитывает известные коды. - Постраничная выборка по дереву, а не запрос по проекту Eva. Агент после обхода дерева для каждой страницы дерева
вызывает
CmfDocumentHistory.listс фильтромparent_id(id страницы из обхода) и полями без текста. Так Eva не отдаёт версии чужих страниц вообще (решение владельца 2), а страница, перенесённая в дерево позже, получает всю свою историю. Цена: один вызов Eva на страницу за синхронизацию (у AWPCA 28 страниц). - Инкремент — по известным кодам. Хаб при выдаче
wiki_sync(CommandHooks.deliver_params) дописывает в параметрыknown_versions: {<код страницы>: [<код версии>…]}— коды версий, которые уже лежат в хабе у страниц текущего дерева проекта; вcommand.paramsв базе этого поля нет. Агент запрашивает текст (CmfDocumentHistory.getпо коду) только для опубликованных версий, которых нет вknown_versionsего страницы. Первый прогон —known_versions = {}, то есть вся история. - Отдельный маршрут для версий.
POST /agent/wiki/versions {command_id, versions}под токеном агентаwiki; версия —{page_code, code, created_at, author: {login, name}, html, attachments, links}(attachments/links— как у страниц, поhtmlэтой версии); в пачке не больше 50 версий, срез по объёму (4 МБ) делает агент. Версии не вкладываются в страницы: у страницы с долгой историей тело пачки упёрлось бы в 16 МБ. Хаб переводит HTML в Markdown вне транзакции и кладёт в стадию версий команды (wiki_version_stage), как страницы. - Отчёт.
doneполучаетresult.versions— число присланных версий. Поле есть — хаб применяет стадию версий; поля нет (агент v1) — история проекта не меняется, копия страниц обновляется как прежде. Присланных кодов меньше, чемresult.versions, — командаfailed«нет версий: пришло N из M». - Хранение. Таблица
wiki_version:label,page_code,code(PK —label, code),created_at(UTC),author_login,author_name,markdown,added,removed. Markdown версии — тем жеconvert, но ссылки на вложения дают текст📎 <имя>без ссылки (у прошлых версий нет строкwiki_attachment, а новые заводить ради истории незачем); ссылки на страницы —page:<код>, как в копии. У проекта хранитсяhistory_at— время последнего применения истории (null— история не загружалась, агент v1); где именно — на исполнителя: колонкаwiki_projectчерез шаг миграции схемы или своя таблица модуля без миграции. - Время и автор правки. Время —
cmf_created_atверсии, автор —cmf_authorверсии. Отдельного времени публикации в API не нашлось; создание версии — начало правки. См. раздел «Decisions made without discussion». - «+строк / −строк» — при записи.
sync.commitдля каждой новой версии считает строки Markdown, добавленные и удалённые против предыдущей версии той же страницы (поcreated_at, затемcode); у первой версии — против пустого текста. Число пишется вadded/removed. Если новая версия встала между известными (позднее дошедшая история), пересчитывается и следующая за ней. - Запись атомарно с копией. Версии пишет только
sync.commitв той же транзакции, что и копию: новые из стадии добавляются; версии страниц, которых нет в новом дереве, удаляются. Сброс корня (projects._drop_copy) удаляет все версии проекта. Отброшенный отчёт по сменённому корню версии не трогает. - Сбой чтения версий — сбой команды. Ошибка Eva на шаге версий даёт
failed, копия и история прежние. Шаг версий проверен на живой Eva; отдельный режим «страницы без истории» усложнил бы отчёт ради редкого случая. - Дифф считается по запросу.
difflib.unified_diffпо строкам Markdown версии и предыдущей, контекст 3 строки, заголовки--- <код пред.>/+++ <код>; у первой версии — против пустого. Отдаётся частями по 20 000 символов сoffset, как текст страницы. Готовый дифф не хранится: Markdown версий уже в базе. - Чтение — только копия.
GET /wiki/changes,…/versions,…/diff, MCPchanges/diff/pageне ставят команд и не ходят к агенту; свежесть —synced_atв ответе. Новых команд и частоты синхронизации change не вводит. - MCP
sync— короткий ответ. Безwait— «поставлено: пакет N» (или «уже идёт: пакет N»); сwait— «ок: синхронизировано ДД.ММ ЧЧ:ММ» или «ошибка: <текст>», или «не дождался за 300 с: пакет N». Сводки страниц и данных в ответе нет (решение владельца 5). - Веб: лента — страница ядра с источниками от модулей. Страница «Лента» (
/app/feed) переезжает в оболочку; модули отдают ей источники через новое необязательное поле контрактаHubModule.feed. Ядро ведёт период, переключатели источников и общий порядок групп; модуль — свои запросы, группы, параметры (виды событий Jira) и окно по нажатию. Ядро не знает терминов модулей (требование «Граница модулей в вебе»). - Окно диффа — оверлей модуля wiki. Как карточка задачи: адрес окна поверх фоновой страницы
(
location.state.background), закрытие возвращает ленту с тем же периодом. Внутри — переход к предыдущей и следующей правке страницы и ссылка «Открыть страницу».
Risks / Trade-offs
- Время правки — начало, не публикация. Правку, начатую вчера и опубликованную сегодня, лента покажет вчерашней. Смысл времени записан в спеке маршрута; если на живых данных лента разойдётся с тем, что видят в Eva, — отдельная доработка (поиск времени публикации в Eva).
- Опубликованная версия может меняться в Eva (согласование,
approved_*) — агент её не перечитает. Текст версии при согласовании, по проверенным данным, не меняется. - Вызов Eva на страницу — при сотнях страниц синхронизация удлиняется на сотни вызовов с паузой
wiki_pacing. Сейчас деревья — десятки страниц. - Объём базы. Версия — до ~60 тыс. символов Markdown; 28 страниц × десятки версий — единицы-десятки МБ. Уборки старых версий нет.
- Агент v1 после выкатки хаба — история пустая, лента вики пустая; MCP и веб показывают «история ещё не загружена», пока не придёт отчёт агента v2.
Migration Plan
- Хаб:
git pull→ kickstart (владелец) →make web→/mcpreconnect. Новые таблицы создаются при подключении (CREATE TABLE IF NOT EXISTSсхемы модуля); прежний код с такой базой работает — таблицы он не читает. - eva-agent v2:
uv run sdlc-hub agent publish --module wiki ../eva-agent/src/agent.py; агент обновится сам. - После
statusс v2 — владелец поднимаетwiki_min_agent_versionдо 2; первая синхронизация v2 загружает всю историю. - Откат: прежний код хаба; таблицы версий остаются и не мешают.
Open Questions
- Время публикации версии: есть ли поле, отличное от
cmf_created_at(решение 7). Проверить на живых данных после выкатки, если лента разойдётся с тем, что видят аналитики в Eva.
Decisions made without discussion
- 28.10.2026 — постраничная выборка версий (решение 2) вместо одного запроса по проекту Eva, о котором говорилось в brainstorming. Обоснование: строже держит зону видимости (решение владельца 2) и подхватывает историю перенесённых страниц. Цена ошибки: лишние вызовы Eva на больших деревьях; переход на запрос по проекту меняет только агента.
- 28.10.2026 — время правки =
cmf_created_atверсии (решение 7). Обоснование: другого времени в API не найдено. Цена ошибки: правка, опубликованная позже начала, попадает в ленту не в тот день. - 28.10.2026 — ссылки на вложения в версиях — текстом без ссылки (решение 6). Обоснование: у прошлых версий нет строк вложений, дифф по тексту от этого не теряет. Цена ошибки: из диффа нельзя открыть файл; в копии страницы ссылки прежние.
- 28.10.2026 — сбой шага версий валит всю синхронизацию (решение 10). Цена ошибки: сломанный в Eva метод истории остановит и обновление страниц до исправления агента.
- 28.10.2026 — дифф по запросу, не хранится (решение 11); «+/−» хранится. Цена ошибки: время ответа
diffна больших страницах (difflib на ~1000 строк — миллисекунды). - 28.10.2026 — короткий ответ MCP
sync(решение 13) — формулировки мои; смысл — решение владельца 5. - 28.10.2026 — адрес общей ленты
/app/feed,/app/jira/feedперенаправляет на него; пункт «Лента» — сразу под «Обзором». Цена ошибки: закладки на/app/jira/feedработают через перенаправление. - 28.10.2026 — ссылки «Лента» на «Обзоре» (по вопросу ревьюера спеки): «Лента» уходит из группы «Jira» боковой
панели (требование «Спринты и бэклог»); в сводке Jira на «Обзоре» ссылка «Лента» ведёт на
/app/feed?sources=jira, в сводке вики добавлена ссылка «Правки» на/app/feed?sources=wiki. Цена ошибки: две ссылки на «Обзоре». - 28.10.2026 — адрес окна правки
/app/wiki/pages/{code}/versions/{version}(по вопросу ревьюера спеки); без фоновой страницы вlocation.state— поверх «Вики» с этой страницей; «Предыдущая/Следующая правка» заменяют адрес (replace), чтобы «назад» закрывал окно. Цена ошибки: ссылка на правку открывается не поверх ленты. - 28.10.2026 — фильтр по автору в
GET /wiki/changesи MCPchanges— подстрока имени или логина без учёта регистра. Цена ошибки: «Иван» находит и «Иванову». - 28.10.2026 — П-1:
history_at— своя таблицаwiki_history(label, history_at)без миграции. Обоснование: схемаwiki_projectне меняется, миграция не нужна. Цена ошибки: ещё одна таблица на проект. - 28.10.2026 — П-2: повтор версии. В хабе версия с уже известным кодом не перезаписывается (
INSERT OR IGNORE, решение 1 — опубликованная версия неизменна); в стадии повтор кода заменяет строку (INSERT OR REPLACE), N в «пришло N из M» — число уникальных кодов в стадии. Цена ошибки: исправленная Eva версия не попадёт в историю. - 28.10.2026 — П-3:
result.versionsнекорректен. Не целое ≥ 0 (строка,true, −1) — командаfailed«versions — неотрицательное целое»; присланных большеM— применяются все. Цена ошибки: агент с багом в счётчике уронит команду, а не только историю. - 28.10.2026 — П-4: «+/−» считаются в
sync.prepare(вне транзакции), пишутся вsync._commit; число — строки+/-изdifflib.unified_diff(те же, что в диффе правки). Цена ошибки: при смене алгоритма диффа старые «+/−» расходятся с новым диффом. - 28.10.2026 — П-5: время версии. Дробные секунды отбрасываются; равное время — порядок по коду строкой (решение спеки «затем по коду»). Цена ошибки: две правки в одну секунду идут в ленте по коду, а не по порядку правки.
- 28.10.2026 — П-6: фильтр автора в Python (
casefold) до срезаlimit. Цена ошибки: на больших лентах фильтр медленнее, чем в SQL. - 28.10.2026 — П-7:
GET …/diff.offsetне целое ≥ 0 — 422bad_request«offset: целое ≥ 0»;offsetза концом —diff = "",next_offset = null. Цена ошибки: клиент, ждавший 404, получит пустой дифф. - 28.10.2026 — П-8: MCP. Шапка
diff; строкаstatus«история: <history_at>» / «история не загружена»;syncпри отменённом пакете — «ошибка: пакет N отменён»; время в «ок: синхронизировано …» —synced_atизGET /wiki/status; автор правки — имя, иначе логин, иначе «—» (и в вебе). Цена ошибки: формулировки MCP могут не совпасть с ожиданием владельца; смысл не меняется. - 28.10.2026 — П-9: помощники времени MCP (
local_if_naive,since_of) — вcore/mcp.py, сервер jira берёт их оттуда. Цена ошибки: ядро получает функции форматирования времени, общие для двух модулей. - 28.10.2026 — П-10: веб — в шапке «Ленты» вторичные кнопки обновления модулей выбранных источников («Обновить из Jira», «Обновить вики»). Цена ошибки: лишняя кнопка на одном из источников, если модуль не подключён.
- 28.10.2026 — П-11: веб — тексты сообщений.
truncatedи ошибки источника — с префиксом «<Источник>: »; «выберите хотя бы один вид» и «история вики ещё не загружена…» — без префикса; «за период изменений нет» — когда ни один выбранный источник не грузится и не в ошибке, хотя бы один загружен без своего сообщения и групп нет;sources=none— явный пустой набор (какkinds=none). Цена ошибки: формулировки сообщений, не смысл. - 28.10.2026 — П-12: веб —
eventTimeпереезжает вui/format.ts, период (PERIODS,parsePeriod,periodRange) — вshell/feed/period.ts; прежнее перенаправление/app/feed→/app/jira/feedснимается (адрес теперь страница ядра). Цена ошибки: закладки на прежний адрес ленты перестают перенаправлять. - 28.10.2026 — R-1:
history.line_deltaберётdifflib.unified_diff(...)[2:]вместо отбора строк по префиксам+++/---(решение контроллера при исполнении, Task 2). Обоснование: заголовков всегда ровно две, они идут первыми; отбор по префиксам терял бы строки содержимого вида++xи--x. Цена ошибки: если difflib изменит формат заголовков, «+/−» исказятся на две строки у каждой версии; лечится правкой одной функции и пересчётом. - 28.10.2026 — R-2: повторное ревью исправления задачи 3 (поля
at/author/offsetв diff,code/titleв versions) сделал контроллер по диффу (10 строк, тесты RED→GREEN), а не свежий ревьюер. Цена ошибки: пропущенная регрессия в двух ответах; финальное ревью её не нашло. - 28.10.2026 — I-1 (финальное ревью M-1, поднято до Important): «за период изменений нет» — если все выбранные источники загружены без ошибки, хотя бы у одного нет своего сообщения и групп нет (П-11). Обоснование: в окне выкатки (хаб новый, агент v1) у вики висит «история не загружена», и пустая Jira не сообщала о пустом периоде. Цена ошибки: два сообщения рядом.
- 28.10.2026 — I-2 (финальное ревью T7.1, поднято до Important): строки диффа в окне правки — без зазора между строками. Обоснование: дифф — главное, ради чего фича; зазор 12 px делал его нечитаемым. Цена ошибки: нет.