## Context
Пакет D сверки follow-ups 25.10.2026 (sdlc-hub#135). Маркеры #94 T6.1–T6.5, T8.2, T8.3, T9.3 сверены с кодом
27.10.2026 и воспроизведены на `convert.to_markdown`:
| Маркер | Вход | Сейчас |
|---|---|---|
| T6.1 | `
текст` | `текст` — ссылки на вложение нет, строка вложения есть |
| T6.2 | `
` | `[\[📎 x.png](attachment:@0)](page:DOC-1)` — ссылка в ссылке |
| T6.3 | два `attachments` с одним `ref` | побеждает последний (`dict` по `ref`) |
| T6.4 | `док` | `[док](http://ex.com/a b(c).html)` — не ссылка |
| T6.5 | `путь C:\` | `[путь C:\](page:DOC-1)` — `\]` экранирует скобку |
T8.2: `request_fetch` отвечает `ready` по полю `state`, а `GET …/content` при пропавшем файле — 409 «запросите
скачивание»: тупик. T8.3: `POST /wiki/sync` проверяет `active_sync`, а `POST /batches` и `POST /commands/{id}/retry`
проходят только `commands.check_batch`, где этой проверки нет. T9.3: `mcp.py` склеивает шапку `page`, строки `tree`,
`search` и кандидатов из значений Eva без нормализации переводов строк.
Владелец 27.10.2026 согласился с предложенным поведением по всем восьми маркерам и добавил картинки в тексте страницы
«Вики»: показ только после нажатия, уже скачанные — сразу.
## Decisions
1. **T6.1 — предок, а не родитель.** `convert_img` ищет ближайшего предка `` (`el.find_parent("a")`), а не только
`el.parent`. Если `href` этого `` — вложение и картинка — то же вложение (совпадает `url` записей) или вообще не
вложение, картинка не выводится: ссылку выведет ``. Картинка — другое вложение (миниатюра файла Y внутри ссылки
на файл X) выводит свою ссылку, а `convert_a` для вложения X сохраняет маркеры вложений из `text` перед своей
ссылкой (то же правило, что в решении 2): Y не теряется.
2. **T6.2 — вложение перед ссылкой на страницу.** В `convert_a` для ссылки на документ (`code` найден): если в `text`
(уже сконвертированное содержимое) есть маркеры вложений (`_MARK`), они выносятся перед ссылкой: результат —
`<ссылки вложений> [<остаток текста или код>](page:<код>)`. Остаток — `text` без фрагментов `[📎 …<маркер>`;
пустой после `strip()` — код. Через `_label` как сейчас. Внешняя ссылка (`super().convert_a`) с картинкой-вложением
внутри — то же правило: вложение перед ссылкой, текст ссылки — остаток, пустой — адрес. Ссылка на вложение внутри
ссылки на вложение уже покрыта решением 1.
3. **T6.3 — первая запись.** `_by_ref` и `_links` собираются с `setdefault`: первая запись по `ref` побеждает (порядок
агента — порядок документа).
4. **T6.4 — кодирование адреса.** Для внешних ссылок и картинок (ветка `super().convert_a`/`convert_img`) адрес
проходит `_href(url)`: пробел → `%20`, `(` → `%28`, `)` → `%29`; прочие символы не трогаются (уже закодированное не
кодируется повторно). Ссылки `page:`/`attachment:` не затрагиваются — в них таких символов нет.
5. **T6.5 — хвостовая черта.** `_label` после текущих замен удваивает нечётную серию `\` в конце строки (одна `\`
в конце → `\\`). То же в тексте внешней ссылки, если markdownify не экранировал её сам; проверять тестом обе ветки.
6. **T8.2 — проверка файла в `request_fetch`.** `request_fetch(conn, attachment_id, author, files_dir)`: при `state =
ready` и `files.size(files_dir, row["file"]) is None` вложение идёт по ветке «поставить скачивание» (state
`fetching`, новый пакет). Маршрут `h_attachment_fetch` передаёт `hub.files_dir`; MCP `attachment` ходит через этот
же маршрут и поведение получает без правок. `GET /wiki/pages/{code}` и `GET /wiki/attachments/{id}` по-прежнему
отдают `state` из базы (проверка файла на каждом чтении — лишние обращения к диску).
7. **T8.3 — проверка в `check_batch`.** Для каждой `wiki_sync` в пакете: если у проекта есть команда `wiki_sync` в
`queued`/`claimed` с тем же `params.root`, — `CommandError` «у проекта <метка> уже идёт синхронизация корня
(пакет #N)» → 422 `bad_batch`. Тот же `root`, а не любой: `projects.set_root` ставит синхронизацию нового корня,
пока команда старого может быть `claimed` (её отчёт будет отброшен по «корень сменился»). `POST /wiki/sync`
по-прежнему проверяет `active_sync` раньше и отвечает 409; фоновая работа — тоже раньше (`tick.py`).
Возврат корня (A → B → A, пока `wiki_sync {root: A}` ещё `claimed`): `projects.on_updated` отменяет только
`queued`, и новая синхронизация A упёрлась бы в проверку — PATCH получил бы 422. Поэтому `commands.enqueue_sync`
перед постановкой ищет активную `wiki_sync` проекта с тем же `root` и, если она есть, возвращает её пакет без
нового: её отчёт не будет отброшен (корень снова совпадает). Ответ — в форме `create_batch`: `{id,
estimate_seconds, count}` из строки пакета и числа его команд; для вызывающих (`on_created`, `on_updated` → ответ
PATCH, `POST /wiki/sync`, `tick`) это «пакет синхронизации проекта», новый он или идущий — не различается. Проверка
в `check_batch` остаётся для `POST /batches` и повтора команды; `enqueue_sync` с поиском и `check_batch`
реализуются одной задачей, иначе `enqueue_sync` между шагами упирается в новую проверку.
8. **T9.3 — одна функция строки.** В `modules/wiki/mcp.py` — `_line(v)`: `core.value(v)`, затем `re.sub(r"\s*[\r\n]+\s*",
" ", …)`. Применяется ко всем значениям из Eva внутри строк ответа: `tree` (название), `page` (название, путь, имена
вложений, состояние), кандидаты `_candidates` (название и сам `title` запроса), `search` (название, путь, фрагмент),
`attachment` (имя, код страницы), `_summary`/`status` (название корня, ошибка синхронизации). Тело страницы — как
есть.
9. **Признак картинки — от хаба по адресу.** Имя вложения для этого не годится: проверка живого AWPCA 27.10.2026
показала имена без расширения («Схема расчёта суммы элиминации по тройке» — файл `vgo-matching-1.png`; 14 вложений
с именем `attachment`) — eva-agent берёт `alt`/`title`/текст ссылки раньше имени файла. Хаб добавляет в
`attachments` ответа `GET /wiki/pages/{code}` поле `image`: последний сегмент пути `url` (без запроса,
`unquote`) оканчивается на `.png .jpg .jpeg .gif .webp .bmp` без учёта регистра. Функция — в
`modules/wiki/attachments.py`, схема базы не меняется. SVG исключён: файл из Eva недоверенный, а пользы в вики от
него мало.
10. **Картинки — устройство во фронте.**
- `useWikiDownload.start(id, mode)` получает режим: `"save"` (как сейчас) или `"show"`. В режиме `show` по `ready`
содержимое (`api.blob`) не сохраняется, а передаётся в хранилище картинок страницы.
- Хранилище картинок страницы (новый хук рядом, например `useWikiImages(project, code, attachments)`): `id →
objectURL`. При загрузке страницы для каждого вложения-картинки в `ready` один раз запрашивает `…/content` и
кладёт objectURL; ошибка — молча, остаётся ссылка. При смене страницы/проекта и размонтировании — `revokeObjectURL`
всех адресов и отмена поздних ответов (поколение, как в `useWikiDownload`).
- Blob заворачивается с типом по расширению (`image/png` и т.д.): хаб отдаёт содержимое как
`application/octet-stream`.
- Один хук на страницу: хранилище картинок живёт внутри `useWikiDownload` (или в хуке, который его оборачивает),
режим `show` кладёт objectURL туда же. Ссылка на объект, от которого зависит `components` в `PageView`, остаётся
стабильной (как сейчас — см. комментарий в `download.ts`): карту картинок `AttachmentLink` читает через
`status(id)`-подобный геттер, меняющийся вместе с картой, как сейчас `status`, а не через пересоздание
`components`. Перемонтирование вложений при появлении картинки не должно сбрасывать `busy`/`error` других.
- Источник картинки — либо автозапрос `content` при загрузке страницы для `ready`-картинок, либо результат
`start(id, "show")`. Автозапрос — один раз на вложение за жизнь страницы; его неудача (409, сеть) оставляет
ссылку в режиме `show` без текста ошибки. Нажатие на неё — `start(id, "show")`: после T8.2 хаб при пропавшем файле
скачивает заново, по `ready` картинка кладётся в хранилище из ответа `start`. Вложение в `failed` — та же ссылка
с текстом ошибки, нажатие — повтор `start(id, "show")`.
- `AttachmentLink`: картинка с objectURL — `
` и под ней
прежняя кнопка-ссылка в режиме `save`; картинка без objectURL — кнопка в режиме `show`; не картинка — как сейчас.
- CSS: `.wiki-att-img img { max-width: 100%; height: auto; display: block }`, токены AURA.
- Правило «внешние картинки не загружаются» (`img` в `components`) не меняется: картинки — только вложения хаба.
## Risks / Trade-offs
- **Картинки в памяти.** objectURL держат blob до смены страницы; предел файла — 10 МБ, на странице AWPCA — единицы
картинок. Приемлемо.
- **Состояние вложения после скачивания.** Ответ `GET /wiki/pages/{code}` не перечитывается после скачивания:
картинка показывается из хранилища страницы, при повторном открытии — по `ready` из нового ответа.
- **Корень сменился во время повтора.** Повтор команды старого корня при идущей синхронизации нового проходит (корни
разные); его `done` будет отброшен по «корень сменился». Лишний запрос в Eva, данных не портит.
## Decisions made without discussion
- 2026-10-27 — T6.2: вложение выводится перед ссылкой на страницу, пустой текст ссылки — код страницы. Обоснование:
Markdown не допускает ссылку в ссылке; порядок совпадает с порядком в HTML. Цена ошибки — вид одной строки текста.
- 2026-10-27 — T6.3: при повторе `ref` побеждает первая запись. Обоснование: порядок агента — порядок документа,
первое появление определяет имя. Цена ошибки — другое имя вложения в редком случае.
- 2026-10-27 — T6.4: кодируются только пробел и круглые скобки. Обоснование: только они ломают разбор
`[текст](адрес)`; прочее браузер разбирает сам. Цена ошибки — редкая внешняя ссылка не кликается.
- 2026-10-27 — T8.2: проверка файла — только в `request_fetch`, не при чтении состояния. Обоснование: тупик был в
повторном запросе, не в показе. Цена ошибки — до нажатия вложение выглядит скачанным.
- 2026-10-27 — T8.3: отказ только при активной синхронизации того же корня. Обоснование: смена корня ставит
синхронизацию нового корня при выданной старой (спека «Синхронизация вики»). Цена ошибки — один лишний запрос
дерева в Eva.
- 2026-10-27 — T9.3: все значения Eva в строках ответа MCP — через одну функцию `_line`, тело страницы как есть.
Цена ошибки — длинное значение в одну строку.
- 2026-10-27 — картинки: признак `image` считает хаб по имени файла в адресе Eva (не по имени вложения), SVG не
показывается; под картинкой остаётся ссылка сохранения файла. Обоснование: на живом AWPCA имена картинок бывают
без расширения; SVG — недоверенный активный формат. Цена ошибки — картинка с необычным расширением остаётся ссылкой.
- 2026-10-27 — T8.3: при возврате корня `enqueue_sync` отдаёт идущий пакет того же корня в форме `create_batch`, не
различая «новый/идущий». Цена ошибки — ответ PATCH называет уже идущий пакет.
- 2026-10-27 — T6.1: картинка-другое-вложение внутри ссылки на вложение выводится своей ссылкой перед ссылкой
внешнего вложения. Цена ошибки — порядок двух ссылок в строке.