## 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 — `{name}/` и под ней прежняя кнопка-ссылка в режиме `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: картинка-другое-вложение внутри ссылки на вложение выводится своей ссылкой перед ссылкой внешнего вложения. Цена ошибки — порядок двух ссылок в строке.