154 lines
19 KiB
Markdown
154 lines
19 KiB
Markdown
## 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)
|
||
|
||
1. Нужна лента правок за период и полный дифф каждой правки; сценарий — «что за сегодня сделали аналитики».
|
||
2. Только зона видимости проекта — страницы дерева корня, не вся вики.
|
||
3. Только опубликованные правки; черновики не берём.
|
||
4. Версии файлов не нужны: свежий файл по запросу, как сейчас.
|
||
5. Обновление отдельно от чтения: `sync` отвечает «ок» или ошибкой; `changes`, `diff`, `page` читают локальную копию.
|
||
6. Веб: общая «Лента» в боковой панели с переключателями источников (Jira, Вики); нажатие на правку — окно с диффом
|
||
поверх ленты.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- Опубликованные версии страниц дерева проекта — в хабе, с автором, временем и Markdown.
|
||
- Лента правок за период и дифф правки — в HTTP API, MCP и вебе; без обращения к Eva при чтении.
|
||
- Инкрементальная догрузка: Eva читается только по новым версиям.
|
||
- Агент v1 и хаб нового кода работают вместе (история просто не пополняется).
|
||
|
||
**Non-Goals:** см. proposal.md.
|
||
|
||
## Decisions
|
||
|
||
1. **Источник — `CmfDocumentHistory`, только `has_published = true`.** Черновик меняется на месте и не нужен по решению
|
||
владельца 3. Опубликованная версия считается неизменной: агент не перечитывает известные коды.
|
||
2. **Постраничная выборка по дереву, а не запрос по проекту Eva.** Агент после обхода дерева для каждой страницы дерева
|
||
вызывает `CmfDocumentHistory.list` с фильтром `parent_id` (id страницы из обхода) и полями без текста. Так Eva не
|
||
отдаёт версии чужих страниц вообще (решение владельца 2), а страница, перенесённая в дерево позже, получает всю свою
|
||
историю. Цена: один вызов Eva на страницу за синхронизацию (у AWPCA 28 страниц).
|
||
3. **Инкремент — по известным кодам.** Хаб при выдаче `wiki_sync` (`CommandHooks.deliver_params`) дописывает в
|
||
параметры `known_versions: {<код страницы>: [<код версии>…]}` — коды версий, которые уже лежат в хабе у страниц
|
||
текущего дерева проекта; в `command.params` в базе этого поля нет. Агент запрашивает текст (`CmfDocumentHistory.get`
|
||
по коду) только для опубликованных версий, которых нет в `known_versions` его страницы. Первый прогон —
|
||
`known_versions = {}`, то есть вся история.
|
||
4. **Отдельный маршрут для версий.** `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`), как страницы.
|
||
5. **Отчёт.** `done` получает `result.versions` — число присланных версий. Поле есть — хаб применяет стадию версий;
|
||
поля нет (агент v1) — история проекта не меняется, копия страниц обновляется как прежде. Присланных кодов меньше,
|
||
чем `result.versions`, — команда `failed` «нет версий: пришло N из M».
|
||
6. **Хранение.** Таблица `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` через шаг миграции схемы или своя таблица модуля без миграции.
|
||
7. **Время и автор правки.** Время — `cmf_created_at` версии, автор — `cmf_author` версии. Отдельного времени
|
||
публикации в API не нашлось; создание версии — начало правки. См. раздел «Decisions made without discussion».
|
||
8. **«+строк / −строк» — при записи.** `sync.commit` для каждой новой версии считает строки Markdown, добавленные и
|
||
удалённые против предыдущей версии той же страницы (по `created_at`, затем `code`); у первой версии — против пустого
|
||
текста. Число пишется в `added`/`removed`. Если новая версия встала между известными (позднее дошедшая история),
|
||
пересчитывается и следующая за ней.
|
||
9. **Запись атомарно с копией.** Версии пишет только `sync.commit` в той же транзакции, что и копию: новые из стадии
|
||
добавляются; версии страниц, которых нет в новом дереве, удаляются. Сброс корня (`projects._drop_copy`) удаляет все
|
||
версии проекта. Отброшенный отчёт по сменённому корню версии не трогает.
|
||
10. **Сбой чтения версий — сбой команды.** Ошибка Eva на шаге версий даёт `failed`, копия и история прежние. Шаг
|
||
версий проверен на живой Eva; отдельный режим «страницы без истории» усложнил бы отчёт ради редкого случая.
|
||
11. **Дифф считается по запросу.** `difflib.unified_diff` по строкам Markdown версии и предыдущей, контекст 3 строки,
|
||
заголовки `--- <код пред.>` / `+++ <код>`; у первой версии — против пустого. Отдаётся частями по 20 000 символов с
|
||
`offset`, как текст страницы. Готовый дифф не хранится: Markdown версий уже в базе.
|
||
12. **Чтение — только копия.** `GET /wiki/changes`, `…/versions`, `…/diff`, MCP `changes`/`diff`/`page` не ставят
|
||
команд и не ходят к агенту; свежесть — `synced_at` в ответе. Новых команд и частоты синхронизации change не вводит.
|
||
13. **MCP `sync` — короткий ответ.** Без `wait` — «поставлено: пакет N» (или «уже идёт: пакет N»); с `wait` — «ок:
|
||
синхронизировано ДД.ММ ЧЧ:ММ» или «ошибка: <текст>», или «не дождался за 300 с: пакет N». Сводки страниц и данных
|
||
в ответе нет (решение владельца 5).
|
||
14. **Веб: лента — страница ядра с источниками от модулей.** Страница «Лента» (`/app/feed`) переезжает в оболочку;
|
||
модули отдают ей источники через новое необязательное поле контракта `HubModule.feed`. Ядро ведёт период,
|
||
переключатели источников и общий порядок групп; модуль — свои запросы, группы, параметры (виды событий Jira) и окно
|
||
по нажатию. Ядро не знает терминов модулей (требование «Граница модулей в вебе»).
|
||
15. **Окно диффа — оверлей модуля wiki.** Как карточка задачи: адрес окна поверх фоновой страницы
|
||
(`location.state.background`), закрытие возвращает ленту с тем же периодом. Внутри — переход к предыдущей и
|
||
следующей правке страницы и ссылка «Открыть страницу».
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Время правки — начало, не публикация.** Правку, начатую вчера и опубликованную сегодня, лента покажет вчерашней.
|
||
Смысл времени записан в спеке маршрута; если на живых данных лента разойдётся с тем, что видят в Eva, — отдельная
|
||
доработка (поиск времени публикации в Eva).
|
||
- **Опубликованная версия может меняться в Eva** (согласование, `approved_*`) — агент её не перечитает. Текст версии
|
||
при согласовании, по проверенным данным, не меняется.
|
||
- **Вызов Eva на страницу** — при сотнях страниц синхронизация удлиняется на сотни вызовов с паузой `wiki_pacing`.
|
||
Сейчас деревья — десятки страниц.
|
||
- **Объём базы.** Версия — до ~60 тыс. символов Markdown; 28 страниц × десятки версий — единицы-десятки МБ. Уборки
|
||
старых версий нет.
|
||
- **Агент v1 после выкатки хаба** — история пустая, лента вики пустая; MCP и веб показывают «история ещё не
|
||
загружена», пока не придёт отчёт агента v2.
|
||
|
||
## Migration Plan
|
||
|
||
1. Хаб: `git pull` → kickstart (владелец) → `make web` → `/mcp` reconnect. Новые таблицы создаются при подключении
|
||
(`CREATE TABLE IF NOT EXISTS` схемы модуля); прежний код с такой базой работает — таблицы он не читает.
|
||
2. eva-agent v2: `uv run sdlc-hub agent publish --module wiki ../eva-agent/src/agent.py`; агент обновится сам.
|
||
3. После `status` с v2 — владелец поднимает `wiki_min_agent_version` до 2; первая синхронизация v2 загружает всю историю.
|
||
4. Откат: прежний код хаба; таблицы версий остаются и не мешают.
|
||
|
||
## 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` и MCP `changes` — подстрока имени или логина без учёта
|
||
регистра. Цена ошибки: «Иван» находит и «Иванову».
|