2026-10-24 00:18:31 +04:00

37 KiB
Raw Blame History

Context

Хаб после #87 (main f7add9b) — ядро jira_hub/core/ и модуль jira_hub/modules/jira/, схема v6. Контракт модуля — core/modules.py (Module, CommandHooks), реестр — core/registry.py (сейчас [jira.MODULE]).

Что в ядре зашито под один агент и под Jira (карта кода 24.10.2026):

  • Токены. Один агентский и один клиентский токен в setting.tokens = {agent, client} (core/settings.py:71-76). Handler._auth (core/api.py:406-415) пускает на любой маршрут со scope agent любой агентский токен.
  • agent_status — одна строка id = 1 (core/db.py:52-59). Её пишут claim, отчёт и отмена (core/queue.py, 7 мест), читают agent_version() и agent_view().
  • claim (core/queue.py:227-271) берёт любую команду очереди: SQL _next и _redeliver не фильтруют ни по типу, ни по агенту. «Одна в полёте» тоже общая.
  • Состояние самообновления держится в памяти хаба одним объектом Hub.agent_update (core/api.py:47, 91-104).
  • Релиз один: release/agent.py и manifest.json (core/release.py, paths.release_dir()), команда jira-hub agent publish PATH.
  • Настройки агента. /agent/settings отдаёт ключи всех модулей и ключи ядра min_agent_version, settings_version (core/settings.py:19, 36-41). Ключ board_id в AGENT_KEYS jira мёртвый.
  • Реле (core/relay.py) про агентов не знает: оно пересылает метод, путь и Authorization в свой HTTP. Имена файлов запросов (uuid4().hex) задаёт агент. Два агента в одной папке relay_dir не мешают друг другу.
  • Проекты. Колонки board_id, board_type лежат в таблице ядра project. issues_count считается в ядре по таблице модуля issue. Поле доски жёстко вшито в POST /projects, MCP project_create/projects и окно «Новый проект». Модуль может только поставить пакет при создании проекта (on_project_created). Все такие места помечены долг hub-modules (решение 12 hub-modules).
  • MCP agent_status — инструмент модуля jira. GET /agent отдаёт один объект.
  • Ориентир для модуля wiki — модуль jira: modules/jira/fetch.py (скачивание вложения по запросу командой fetch_attachment), refresh.maybe_auto_refresh (фоновый пакет чтения), core/files.py (хранилище по sha256, MAX_SIZE 10 МБ, уборка по file_refs).

Eva. evaTeam, JSON-RPC 2.2 по адресу <eva>/api/?m=<метод>, заголовок Authorization: Bearer <токен>, тело {jsonrpc: "2.2", method, callid, kwargs}. Используются методы:

  • CmfDocument.get(filter=[["code","==",<код>]], fields=[…]);
  • CmfDocument.list(filter, fields, slice=[from,to]), страницы по 200.

Поля списка: id, code, name, parent, tree_parent, project, orderno. Текст страницы — HTML в поле text. Дерево строится локально по parent/tree_parent из списка всех документов проекта Eva. Фильтр parent.code API не принимает (-32001). Если у страницы нет project, дерево обходится в ширину через фильтр tree_parent.code. Картинки и файлы — ссылки <img src>/<a href> на тот же хост с путём /(files?|attachments?|download|media|upload). Всё это обкатано в eva_wiki_export.py (корень workspace). Есть ли у документа дата изменения и история версий, неизвестно: публичной документации API нет, Eva доступна только из сети банка.

Владелец (brainstorming 23–24.10.2026): см. proposal, «Why».

Goals / Non-Goals

Goals:

  • проект хаба получает корень Eva, дерево его страниц лежит у хаба Markdown-копией;
  • Claude ищет и читает вики проекта через отдельный MCP-сервер;
  • веб показывает дерево и страницы проекта;
  • eva-agent — второй агент; хаб различает агентов;
  • в ядре не остаётся Jira (метки долг hub-modules сняты).

Non-Goals: см. proposal. Сверх них — правка уже скачанной копии руками: копия перезаписывается синхронизацией.

Decisions

1. Вид агента = модуль с командами

У каждого модуля, у которого есть commands, свой агент. Имя вида совпадает с именем модуля: агент jira — jira-agent, агент wiki — eva-agent. Отдельного справочника видов нет.

Почему: модуль уже владеет своими типами команд, настройками для агента и маршрутами /agent/<…> своего модуля. Агент без модуля бессмыслен, модуль с командами без агента тоже.

2. Токен агента определяет его вид

setting.tokens становится {client, agents: {<модуль>: <токен>}}. Миграция v7 переносит прежний tokens.agent в tokens.agents.jira, поэтому jira-agent v14 работает со своим прежним токеном. ensure_defaults создаёт недостающий токен для каждого модуля с командами. jira-hub tokens печатает клиентский токен и токены агентов с именем модуля.

Handler._auth("agent") находит модуль по токену (hmac.compare_digest с каждым) и кладёт его в контекст запроса. Дальше:

  • маршруты ядра /agent/* работают от имени этого агента;
  • маршруты модуля со scope agent (у jira это /agent/state, /agent/history, /agent/known-keys) пускают только агента своего модуля, токен другого агента получает 401.

Клиентский токен по-прежнему один.

3. agent_status по агентам

Миграция v7 пересоздаёт таблицу с ключом agent TEXT PRIMARY KEY и переносит строку id = 1 в agent = 'jira'. Колонку last_jira_call_at переименовываем в last_call_at. Протокол claim (поле last_jira_call_at в теле от jira-agent) не меняется: хаб читает и last_call_at, и last_jira_call_at.

Строка агента появляется при старте хаба, по одной на модуль с командами. Все места WHERE id = 1 становятся WHERE agent = ?. queue.agent_version(conn) превращается в agent_version(conn, agent). Модуль jira в fetch.py спрашивает версию агента jira.

4. claim выдаёт только команды своего модуля

queue.claim(conn, agent, …). _next и _redeliver берут только команды, у которых type входит в CommandHooks.types модуля agent. Пакет и так однородный: «команды разных модулей в одном пакете» отвергаются с #87.

«Одна команда в полёте» считается на агента. Агенты работают параллельно: долгая синхронизация вики не задерживает ensure_* в Jira. current_command_id хранится в строке своего агента. Копия базы перед первой пишущей командой пакета остаётся как есть: команды wiki в этом change только читают (read_types).

5. Самообновление по агентам

  • Релиз: release/<модуль>/agent.py и manifest.json. При старте хаб один раз переносит прежние release/agent.py и release/manifest.json в release/jira/. Переносит, только если release/jira/ ещё нет; ошибка переноса пишется в лог, хаб стартует.
  • Публикация: jira-hub agent publish [--module <модуль>] PATH, по умолчанию jira. Неизвестный модуль или модуль без команд — код 2, как у mcp. Проверки и вывод прежние.
  • Ответы агенту: claim и GET /agent/release отдают релиз вида, к которому относится токен.
  • Память хаба: Hub.agent_update становится словарём по агентам.
  • GET /agent?module=<модуль>, без параметра — jira, как сейчас. Добавляется GET /agents — список всех агентов в том же формате с полем module.
  • MCP: agent_status остаётся у jira, у wiki есть свой (решение 12).

6. Настройки агента по модулю

GET /agent/settings отдаёт ключи ядра и agent_keys только своего модуля: jira-agent не видит ключей вики и наоборот. settings_version остаётся общим счётчиком. Лишний перезапрос настроек у eva-agent после правки настроек jira безвреден.

min_agent_version переезжает из ядра в модуль jira: имя ключа и значение прежние, для jira-agent ничего не меняется. Модуль задаёт ключ своей минимальной версии полем Module.agent_min_version_key; у wiki это wiki_min_agent_version, по умолчанию 1. agent_view(conn, agent) берёт минимальную версию по этому ключу. Мёртвый ключ board_id из AGENT_KEYS jira удаляется.

Требование для jira-agent v14: ответ /agent/settings для токена jira совпадает с нынешним, кроме ключа board_id. Сейчас он всегда null, агент его не читает (проверить по jira-agent/src/agent.py при реализации; если читает — ключ оставить со значением null).

7. Реле не меняется

Одна общая папка relay_dir на обоих агентов. Запросы различаются токеном, имена файлов — uuid4().hex агента, /agent/claim у обоих один и тот же путь. Уборка work/ при старте хаба общая, как сейчас: потерянный запрос агент повторит. Подпапки на агента не вводим.

8. Проекты: ядро — метка, название, дата; остальное — модули

  • Ядро. Таблица project остаётся с label, name, created_at. Миграция v7 переносит board_id и board_type в таблицу модуля jira jira_project(label PK → project, board_id, board_type) и удаляет колонки из project (через пересоздание таблицы, как в прежних миграциях).
  • Число задач issues_count считает модуль jira.
  • Контракт Module получает поля проекта:
    • project_fields — описание полей модуля: имя, тип, проверка, можно ли менять после создания, подпись и подсказка для веба и MCP;
    • project_view(conn, labels) -> {label: {поле: значение}} — значения для списка проектов, включая вычисляемые (issues_count, pages_count, synced_at);
    • on_project_created(conn, project, fields, author) — запись своих полей и пакет, как сейчас;
    • on_project_updated(conn, project, fields, author) — правка своих изменяемых полей и пакет.
  • registry.check падает, если имя поля проекта есть у двух модулей или совпадает с полем ядра.
  • GET /projects отдаёт плоские объекты: поля ядра и поля всех модулей. Набор и значения полей jira прежние: board_id, board_type, issues_count.
  • POST /projects {label, name, author?, <поля модулей>}. Неизвестное поле — 422. Каждый модуль проверяет свои поля до записи; ошибка — 422 с именем поля, ничего не записано. batch_id в ответе прежний: пакет первого модуля, вернувшего пакет. Решение R-8 hub-modules остаётся.
  • Новый PATCH /projects/{label} {<изменяемые поля модулей>, author?}. Неизменяемое поле — 422 «<поле> после создания не меняется». Ответ — {project, batch_ids: [...]}. Доска jira остаётся неизменяемой, как сейчас. Корень вики изменяемый: у уже заведённых проектов (AWPCA и др.) его надо проставить потом.
  • MCP. core/mcp.register_core_tools(mcp, module) строит projects и project_create сервера модуля из его project_fields. Сервер jira отдаёт прежние названия, параметры и описания project_create и прежние колонки projects (эталоны tests/fixtures/baseline/mcp_tools.expected.json не меняются). У сервера wiki — свои колонки и параметр wiki_root.
  • Веб. HubModule.projectFields — поля модуля в окне «Новый проект». Поле доски переезжает в модуль jira, текст и подсказка прежние.

Метки долг hub-modules в ядре снимаются все. Тест test_core_has_no_jira_terms больше не делает для них исключения.

9. Модуль wiki: данные

Таблицы модуля (схема v7, рядом с миграцией решений 2, 3, 8):

  • wiki_project: label PK → project, root_code (DOC-<цифры>), root_title, synced_at (последняя успешная синхронизация), pages_count, sync_error (текст последней неудачи или NULL).

  • wiki_page:

    • ключ: label, code; PK (label, code);
    • поля: eva_id, title, parent_code, orderno, depth, path (каталог страницы относительно wiki/<label>/), url (адрес страницы в Eva от агента), size (символов Markdown), synced_at.

    Страница в двух проектах — две строки. Деревья проектов независимы.

  • wiki_page_fts — FTS5 (title, body) с content по Markdown-тексту и токенизатором unicode61 remove_diacritics 2. Используется только для поиска. Сам текст страниц живёт в файлах.

  • wiki_attachment: id INTEGER PK, label, page_code, url (абсолютный адрес в Eva), name, state (none | fetching | ready | failed), file (sha256 в files/), size, error, fetched_at. Одна строка на пару «адрес + страница». Адрес, который встречается на нескольких страницах, даёт несколько строк, но файл в files/ хранится один (sha256).

  • wiki_sync_stage — временные пачки страниц текущей синхронизации, ключ (command_id, code). Очищается при завершении команды в любом статусе.

file_refs модуля возвращает file всех wiki_attachment в состоянии ready, поэтому уборка files/ их не трогает, пока строка жива.

10. Синхронизация wiki_sync

Постановка. Команда wiki_sync {label, root} — тип чтения (read_types), пакет с project = <label>. Ставят её:

  • MCP sync и кнопка «Обновить» в вебе (POST /wiki/sync {project});
  • привязка или смена корня (on_project_created/on_project_updated);
  • фоновая работа модуля (Module.tick): по каждому проекту с корнем, если с synced_at (или с последней неудачи) прошло больше wiki_auto_sync_hours (настройка модуля, по умолчанию 24, 0 — выключено) и у проекта нет пакета wiki_sync в работе.

Хаб не ставит пакет, пока предыдущий wiki_sync проекта в работе: отвечает 409 с id пакета, MCP показывает его состояние.

Агент (контракт; решения внутри агента — его change):

  1. Берёт корень CmfDocument.get и все страницы поддерева, как collect_subtree в eva_wiki_export.py.
  2. По каждой странице берёт text.
  3. Отправляет страницы пачками POST /agent/wiki/pages {command_id, pages: [{code, eva_id, title, parent_code, orderno, url, html, attachments: [{url, name}]}]}. В пачке не больше 50 страниц и не больше 4 МБ тела.
  4. Отчитывается done с result = {root_title, codes: [<все коды дерева в порядке обхода>], eva_calls}.

Хаб.

  • Пачки складываются в wiki_sync_stage без изменения копии.
  • Отчёт done применяется одной транзакцией (CommandHooks.on_result):
    • HTML переводится в Markdown;
    • Markdown-копия собирается в wiki/.<label>.new/, затем атомарно подменяет wiki/<label>/ (переименование; старая копия удаляется после подмены);
    • wiki_page и FTS пишутся заново;
    • для wiki_attachment при той же паре «адрес + страница» сохраняются id и скачанный файл, новые пары добавляются, пропавшие удаляются;
    • заполняются wiki_project.synced_at, pages_count и root_title, sync_error обнуляется.
  • Код из codes, для которого не пришла пачка, — команда failed «нет страницы <код>», копия прежняя.
  • Отчёт failed пишет sync_error, копия прежняя.
  • Повторная выдача чистит wiki_sync_stage команды.

Раскладка копии (по образцу eva_wiki_export.py):

  • каталог страницы — NN_<безопасное название>/ внутри каталога родителя; NN — позиция среди соседей, у корня номера нет;
  • в каталоге index.md с frontmatter code, title, url, synced_at, затем # <title> и текст;
  • в корне _tree.json — плоский список {code, title, depth, path};
  • каталоги 0700, как остальные данные хаба.

Безопасное название — как safe_name скрипта; при совпадении у соседей хаб добавляет _2, _3.

HTML → Markdown делает хаб: зависимость markdownify, ATX-заголовки. Ссылки:

  • картинка или файл Eva (тот же хост и путь по правилу из Context) заменяется на [📎 <имя>](attachment:<id>): по id инструмент attachment и веб скачивают файл;
  • ссылка на страницу Eva с кодом DOC-<цифры> заменяется на [<текст>](page:<код>): по коду её открывает инструмент page, а в вебе она ведёт на страницу вики, если та есть в дереве проекта;
  • прочие ссылки остаются как есть.

Объём. Перекачка всего дерева при каждой синхронизации — осознанно (история и частичная перекачка — позже). Темп — настройка модуля для агента wiki_pacing (пауза между вызовами Eva, по умолчанию 0.2 с).

11. Вложения по запросу wiki_fetch

POST /wiki/attachments/{id}/fetch (веб и MCP) ставит пакет из одной команды wiki_fetch {attachment_id, url}. Это тип чтения, по образцу modules/jira/fetch.request_fetch: одна транзакция, state = fetching. Пока скачивание идёт, повторный запрос отвечает текущим состоянием, а не новым пакетом.

Агент:

  • скачивает адрес с токеном Eva;
  • больше MAX_SIZE ядра (10 МБ, по Content-Length или по факту чтения) — failed «файл больше 10 МБ — откройте в Eva»;
  • иначе POST /agent/files и отчёт done {file, size}.

Хаб проверяет файл в files/ и ставит state = ready. Если файла нет или размер чужой — failed.

Выход:

  • GET /wiki/attachments/{id} — состояние;
  • GET /wiki/attachments/{id}/content — сам файл;
  • MCP attachment кладёт копию в downloads/<id>/<имя> (общая папка ядра, уборка 30 дней). Имена каталогов jira и wiki не пересекаются: каталог wiki — downloads/wiki-<id>/.

12. MCP вики

Запуск. jira-hub mcp wiki [--project <метка>], имя сервера jira-hub-wiki (переименование в sdlc-hub — позже, вместе со всем). Регистрация в awp-ca — новая запись jira-hub-wiki в .mcp.json с --project AWPCA; её делает владелец. Хаб выключен — та же подсказка, что у jira.

Инструменты (короткие имена: в Claude они видны как mcp__jira-hub-wiki__<имя>):

Инструмент Что делает
projects проекты: метка, название, корень, страниц, синхронизация
project_create label, name, wiki_root (необязательно)
root_set root — корень текущего проекта (PATCH /projects/{label})
tree code (необязательно, по умолчанию корень), depth (по умолчанию 2) — дерево с кодами и размерами
page code или title (точное совпадение, иначе до 10 кандидатов), offset — Markdown страницы с шапкой (код, путь в дереве, адрес Eva, дата синхронизации), длинный текст — частями по 20 000 символов
search query, limit (по умолчанию 10) — страницы с фрагментами, FTS5; слова запроса ищутся по префиксу
attachment id — скачать вложение (как attachment у jira: ждёт до 60 с, затем путь к копии)
sync синхронизировать сейчас, wait (по умолчанию нет)
status корень, последняя синхронизация, ошибка, пакет в работе, агент wiki (онлайн, версия, публикация, обновление — как agent_status jira)

Инструменты, кроме projects и project_create, требуют привязки к проекту, как у jira. Без корня они отвечают «у проекта <метка> нет корня вики: root_set <DOC-…>». Корень есть, а синхронизаций ещё не было — «вики проекта ещё не синхронизирована: sync».

13. Веб

Модуль web/src/modules/wiki/:

  • страница «Вики» /app/wiki?project=<метка>, пункт боковой панели;
  • дерево слева: свёрнутое, раскрывается по клику;
  • страница справа: Markdown, без сырого HTML;
  • поиск над деревом;
  • кнопка «Обновить» и плашка хода синхронизации в боковой панели — как у jira;
  • ссылки: page: ведёт на страницу вики, attachment: скачивает файл (запрос, опрос, api.blob, как вложения в карточке задачи);
  • без корня — форма «Корень вики (DOC-…)» (PATCH /projects);
  • окно «Новый проект» получает поле «Корень вики (необязательно)» через projectFields.

Маршруты HTTP модуля (клиентский токен):

  • GET /wiki/tree?project= — дерево с кодами;
  • GET /wiki/pages/{code}?project= — {code, title, path, url, synced_at, markdown, attachments};
  • GET /wiki/search?project=&q=&limit=;
  • POST /wiki/sync;
  • GET /wiki/status?project=;
  • вложения из решения 11.

Отрисовка Markdown — react-markdown с remark-gfm (таблицы), без rehype-raw.

14. eva-agent

Репозиторий aep/eva-agent (создан владельцем пустым 24.10.2026), каталог jira-bridge/eva-agent/. Обвязка подключается adopt-project.sh. Решения агента — его design-only change. Хаб фиксирует только контракт:

  • Стек: Python 3.9 на stdlib, одним файлом src/agent.py со строкой AGENT_VERSION = <целое>. Запускатель src/run.py — копия запускателя jira-agent. Имена agent.py, update.json и протокол самообновления прежние, иначе run.py не подходит.
  • Конфиг agent.json: hub_url (folder:<путь> или http), hub_token — токен агента wiki из jira-hub tokens, eva: {base_url, token}, verify_ssl, ca_bundle, timeout, poll_seconds, auto_update. Токен Eva — только здесь.
  • Протокол с хабом — как у jira-agent: claim, GET /agent/settings, GET /agent/commands/{id}, результат, GET /agent/release, POST /agent/files; плюс POST /agent/wiki/pages.
  • Команды: wiki_sync, wiki_fetch (решения 10, 11). Обе только читают Eva: журнал на диске нужен для повторного отчёта, но «проверь, потом сделай» не нужно.
  • Из jira-agent копируются, без общей библиотеки: HTTP-слой, FolderTransport, HubClient (без jira-маршрутов), Journal, Runner, Updater, main. Решение владельца 23.10.2026: однофайловый агент на виртуалке.
  • Запуск на той же виртуалке, отдельным каталогом и процессом со своей блокировкой. Первую установку делает владелец руками, дальше агент обновляется публикацией jira-hub agent publish --module wiki.

15. Миграция v7 одним шагом

Один шаг MIGRATIONS 6 → 7 с копией базы перед ним (механизм прежний):

  1. tokens.agent → tokens.agents.jira, токен agents.wiki создаётся.
  2. agent_status пересоздаётся по агентам (решение 3).
  3. project теряет board_id/board_type, появляется jira_project с перенесёнными значениями.
  4. Создаются таблицы wiki (решение 9).

Перенос release/ в release/jira/ — не миграция базы, а шаг старта хаба (решение 5).

16. Порядок работ

  1. Ядро: виды агентов (решения 1–7) — jira-agent v14 работает. Живая проверка: kickstart, claim от v14 проходит.
  2. Ядро: поля проектов (решение 8) и снятие долга.
  3. Модуль wiki в хабе: данные, синхронизация с имитацией агента в тестах, вложения, HTTP, MCP.
  4. Веб.
  5. eva-agent в своём репозитории: отдельный PR и отдельный план, после merge хаба.

Шаги 1–4 — этот change и один PR в jira-hub. Шаг 5 — PR в eva-agent и его design-only change.

Risks / Trade-offs

  • Миграция v7 трогает живые данные: токены, статус агента, доски. Защищают копия базы перед миграцией и тест миграции на снимке v6 (tests/fixtures/ из настоящей схемы v6 с досками AWPCA = 4216 scrum). Если jira-agent после выкатки получает 401, это ошибка переноса токена; откат — копия базы.
  • jira-agent v14 видит другой /agent/settings (решение 6). Это проверяется тестом на равенство ответа эталону v6, кроме board_id.
  • FTS5 в Python от uv. Если модуль fts5 в SQLite не собран, поиск сломается. Тест на старте модуля проверяет CREATE VIRTUAL TABLE … USING fts5; если не вышло — поиск идёт через LIKE по файлам и в логе одно предупреждение.
  • Русская морфология. FTS5 ищет по префиксу слова, «статусы» не найдёт по «статус…» в другой форме корня. Принимаем; MCP подсказывает искать по корню слова.
  • Большое дерево. Сотни страниц по несколько сотен КБ HTML — пачки по 50 страниц и 4 МБ. Стадия хранится в базе; копия базы перед командой не делается (чтение).
  • Атомарность копии. Подмена каталога переименованием. Если хаб упал между удалением старой и переименованием новой, при старте модуль доводит подмену по наличию wiki/.<label>.new/.
  • Агент wiki есть в хабе до установки eva-agent. status и GET /agents показывают его офлайн с подсказкой «eva-agent не выходил на связь». Пакеты wiki_sync стоят в очереди и не мешают jira (решение 4). Фоновая синхронизация не ставит новый пакет, пока предыдущий не выдан.

Migration Plan

  • Хаб: merge → make web → kickstart (владелец) → /mcp reconnect. jira-agent не обновляется.
  • После kickstart:
    • проверить agent_status jira (v14 онлайн);
    • проставить корни проектам (root_set или веб);
    • добавить jira-hub-wiki в .mcp.json awp-ca.
  • eva-agent: отдельный PR и ручная установка на виртуалке владельцем; дальше публикация --module wiki.
  • Откат хаба: предыдущий коммит и копия базы v6 из backups/.

Decisions made without discussion

Принятые агентом в brainstorming 24.10.2026; владелец видит их при ревью документа.

  1. Вид агента = имя модуля (wiki, а не eva). Обоснование: см. решение 1. Цена ошибки — переименование ключа в tokens.agents и agent_status.
  2. Агенты работают параллельно, «одна в полёте» — на агента (решение 4). Обоснование: синхронизация вики не должна держать Jira. Цена ошибки — правка одного условия в claim.
  3. Реле общее, без подпапок (решение 7). Обоснование: реле агентов не различает и не должно. Цена ошибки — подпапки и вторая настройка, если папка станет узким местом.
  4. Корень вики меняется после создания, доска — нет (решение 8). Обоснование: у живых проектов корня нет. Цена ошибки — запрет в описании поля.
  5. Перекачка всего дерева каждый раз (решение 10). Обоснование: дата изменения в Eva неизвестна, история отложена. Цена ошибки — нагрузка на Eva при большом дереве; лечится wiki_pacing и частичной перекачкой позже.
  6. Текст страниц в файлах, в базе — индекс FTS (решение 9). Обоснование: копия — главный артефакт, база — для поиска. Цена ошибки — двойной источник; синхронизация пишет оба в одной транзакции и подмене.
  7. Ссылки attachment:<id> и page:<код> в Markdown (решение 10). Обоснование: по ним работают инструменты и веб. Цена ошибки — формат ссылок в файлах; меняется со следующей синхронизацией.
  8. Предел вложения 10 МБ — общий MAX_SIZE ядра (решение 11). Обоснование: реле передаёт файл base64 в JSON. Цена ошибки — большие файлы Eva только по ссылке в Eva.
  9. Имя MCP-сервера jira-hub-wiki, инструменты без префикса (решение 12). Цена ошибки — правка .mcp.json в awp-ca.
  10. Отрисовка Markdown — react-markdown + remark-gfm (решение 13). Цена ошибки — замена библиотеки на странице вики.
  11. Ежедневная синхронизация — фоновой работой хаба (решение 10). Обоснование: прецедент — автообновление зеркала jira. Цена ошибки — отключение настройкой wiki_auto_sync_hours = 0.