190 lines
25 KiB
Markdown
190 lines
25 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` — подстрока имени или логина без учёта
|
||
регистра. Цена ошибки: «Иван» находит и «Иванову».
|
||
- 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 — 422 `bad_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 изменит формат заголовков, «+/−»
|
||
исказятся на две строки у каждой версии; лечится правкой одной функции и пересчётом.
|