Andrey Epifancev 4848dff206 OpenSpec: change wiki-package-d — пакет D модуля wiki (#135)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:31:55 +04:00

17 KiB
Raw Blame History

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 <a href="…/f.xlsx"><span><img src="…/f.xlsx.meta/thumbnail.jpg"></span></a> текст текст — ссылки на вложение нет, строка вложения есть
T6.2 <a href="project/Document/DOC-1"><img src="…/x.png"></a> [\[📎 x.png](attachment:@0)](page:DOC-1) — ссылка в ссылке
T6.3 два attachments с одним ref побеждает последний (dict по ref)
T6.4 <a href="http://ex.com/a b(c).html">док</a> [док](http://ex.com/a b(c).html) — не ссылка
T6.5 <a href="project/Document/DOC-1">путь C:\</a> [путь 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 ищет ближайшего предка <a> (el.find_parent("a")), а не только el.parent. Если href этого <a> — вложение и картинка — то же вложение (совпадает url записей) или вообще не вложение, картинка не выводится: ссылку выведет <a>. Картинка — другое вложение (миниатюра файла 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 — <span className="wiki-att-img"><img src alt={name}/></span> и под ней прежняя кнопка-ссылка в режиме 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: картинка-другое-вложение внутри ссылки на вложение выводится своей ссылкой перед ссылкой внешнего вложения. Цена ошибки — порядок двух ссылок в строке.