## Context 25.10.2026 пять сабагентов сверили открытые follow-up issue хаба и агентов с main (hub 1405ccd). Каждый маркер получил статус с доказательством `file:line`. Итоги записаны комментариями в issue. Владелец выбрал первым пакет A — видимые пользователю ошибки веба (sdlc-hub#124). Brainstorming-вопросы владелец пропустил («делай сам… не хочу с ними сталкиваться в будущем»), поэтому все решения ниже приняты агентом. Они перечислены в «Decisions made without discussion». Состав пакета: #42 M-1, M-2, M-4, M-5; #116; #103 С6 и метки `kind-*`; #56 M-4, M-5, M-6; #35 T4.1/M-15; #60 M-3; #64 M-6, T9.1; #86 T6.2, T6.3, M-4; #68 T3.3. Ссылки на строки ниже — по main 1405ccd. ## Goals / Non-Goals Цель — убрать перечисленные ошибки. Каждую закрепить тестом, который падает до правки: юнит или поведение компонента в vitest, для окна карточки дополнительно e2e. Сервер, API, MCP и агенты не меняются. Не цели: - прочие маркеры исходных issue: тесты, стиль, доступность UI-кита (#68), квадратичный разбор (#56 M-2), фокус окна (#42 M-3), `100vh` окна (#42 M-6); - новые возможности. ## Decisions ### Окно карточки 1. **Поиск списка не закрывает окно (#42 M-1).** Сейчас `IssuesPage.tsx:31-38` через 300 мс после ввода делает `setParams(..., {replace: true})`. Если за это время открыто окно карточки, запись заменяет адрес окна адресом списка, и окно закрывается. Решение — перед открытием карточки со страницы «Задачи» (строка, ключ) несохранённый поиск записывается в адрес сразу, таймер снимается. Окно открывается поверх списка уже с этим поиском. Отложенная запись MUST NOT срабатывать, пока открыто окно. Реализация на усмотрение исполнителя, например: - `IssuesPage` даёт таблице свою обёртку `open`, которая сначала делает flush; - или эффект проверяет, что текущий адрес браузера — сам список. 2. **Повторное закрытие не уводит назад дальше (#42 M-2).** `overlay.tsx:37-41`: `close()` при каждом вызове делает `navigate(-depth)`. Двойной щелчок по ✕ или фону, Esc сразу после щелчка — два шага назад. Решение — закрытие выполняется один раз для одной записи истории окна. Повторные вызовы игнорируются, пока адрес не сменился. Признак — `location.key` записи, с которой начато закрытие. 3. **Подтверждение ухода с несохранённой заметкой (#42 M-4).** `NoteForm.tsx:82`: блок `role="alertdialog"` без имени, фокус в него не переводится. Решение: - блоку — `aria-label` «Уйти без сохранения заметки?»; - при появлении фокус переходит на «Остаться» (безопасное действие по умолчанию); - прокрутка к блоку остаётся. 4. **Связанная задача «нет в зеркале» и окно 404 (#42 M-5).** - В «Связях» (`IssueCard.tsx:123-125`) ключ задачи вне зеркала открывает задачу в Jira в новой вкладке (как «Открыть в Jira» в шапке) со значком внешней ссылки, а не окно хаба. Пометка «нет в зеркале» остаётся. - Окно 404 (прямой адрес, ключ из текста, гонка) показывает «Закрыть» (закрывает окно целиком, как ✕). Если окно открыто переходом из другой карточки (`depth ≥ 1`), добавляется «Назад» — `navigate(-1)` к прежней карточке. - То же «Назад» — у ошибки загрузки с «Повторить». ### Строка состояния 5. **Ответ опроса прежнего проекта отбрасывается (#116).** `ActivityProvider.tsx`: - эффект смены проекта (`:75-90`) сбрасывает `activeRef`/`active` и событие; - эффект опроса (`:126-154`) в том же коммите видит `active` из замыкания старого рендера и снова ставит событие `batch` (`:139`); - после этого `active = null`, событие больше никто не снимает. Решение — эффект опроса обрабатывает ответ, только если отслеживаемый пакет на момент обработки тот же: `activeRef.current` совпадает с `active` из замыкания (по `id` и `project`). Иначе ответ игнорируется. `finish` по устаревшему ответу тоже не вызывается. Тест — детерминированный, без нагрузки: ответ `/batches/{id}` приходит в том же такте, что и смена проекта; после этого событие модуля `null`. Существующий тест `activity.test.tsx:331` при этом стабилен (прогон в цикле ≥ 20 раз зелёный). ### Лента и карточка 6. **Текст комментария в ленте без разметки (#103 С6).** Новая чистая функция `wikiPlain(text, opts)` рядом с разборщиком (`issues/wiki/`) переводит вики-разметку в обычный текст тем же разбором `parseWiki`: - выделение и `{{моно}}` — содержимое; - ссылка — её текст (у `[адрес]` — адрес); - ключ задачи — ключ, `[~логин]` — «@логин», вложение — имя; - блоки — их текст через пробел, заголовок — его текст, `----` — пусто. `eventText` для `comment` берёт `wikiPlain`, затем прежние «одна строка» и обрезку до 300 символов с «…». Модуль ленты импортирует функцию из модуля задач того же модуля jira — граница модулей веба не нарушается. Та же функция применяется к «Истории» карточки: она рисуется тем же `EventList`. 7. **Метки видов работ отдельно (#103).** В «Прочих полях» (`IssueCard.tsx:227`): - строка «Вид работы в Jira» — названия видов из `kind_labels`, чьи метки есть у задачи, через запятую, в порядке меток задачи; - строка «Метки» — остальные метки. Строка с пустым значением, как и сейчас, не показывается. Сравнение меток — точное, как у доски (`dicts.ts`). Служебное поле заметки «Вид работы» (`work_kind`) не трогается. Это заметка хаба, а не метка Jira, и они могут расходиться — отсюда подпись «в Jira». 8. **Описание из пробелов (#56 M-4).** `IssueCard.tsx:79`: раздел «Описание» показывается, только если `description.trim()` не пуст. ### Вики-разметка (#56 M-5, M-6) 9. **`\#`.** `#` добавляется в `ESCAPABLE` (`inline.ts:21`). Строка `\# текст` не список (уже так — `LIST` требует `#` первым символом), а `\#` показывается как `#`. 10. **Макросы с параметрами.** `MACRO` (`parse.ts:17`) принимает параметры у всех трёх тегов: `\{(code|noformat|quote)(?::[^}]*)?\}`. Закрывающий тег — по имени без параметров, как сейчас. 11. **Пробелы после открывающего тега.** В `closeMacro` (`parse.ts:111`) остаток строки открывающего тега из одних пробелов в тело блока не попадает. Ведущие пробелы строк внутри блока сохраняются. 12. **`[~логин|текст]`.** В `bracket` (`inline.ts:149-152`) логин — часть до `|`, обрезанная. Получается «@логин», подпись не показывается. Пустой логин или логин с пробелом — текст как есть, как сейчас. 13. **Непарная `[` в ячейке таблицы.** В `row` (`parse.ts:127-132`) `[` увеличивает глубину, только если дальше в строке есть парная `]`. Иначе `[` — обычный символ, и `|` делит ячейки. ### Список задач, доска, проекты 14. **Список ждёт настройки (#60 M-3).** Запрос `GET /issues` (`useIssues`) включается, когда запрос `/settings` завершён — успехом или ошибкой. Пока нет — «Загрузка…», без «По этим фильтрам задач нет». При ошибке настроек закрытых статусов нет, запрос уходит с `hide_closed=1`, как сейчас. 15. **Два быстрых переключения фильтра доски (#35 T4.1/M-15).** `useBoardPrefs.ts:39-54`: `toggle` и `toggleHidden` считают новое значение из `prefs` замыкания, и второй клик в том же батче теряет первый. Решение — последнее значение держится в `ref`, обновляемом синхронно вместе с `setPrefs`. Новое значение считается из `ref` и тем же значением пишется в `localStorage`. Чистота updater-функции сохраняется: StrictMode не пишет дважды. 16. **Фильтр спринтов не из-за эпика (#64 T9.1).** `fromHub.ts:30`: `hasSprint` — только по своим задачам, которые не эпики (`!isEpic(it) && !it.neighbor && !!it.sprint`), то есть по узлам доски. 17. **«Повторить» при ошибке загрузки проектов (#64 M-6).** `ProjectGate.tsx:11`: у сообщения «Проекты не загрузились» есть кнопка «Повторить», которая перезапрашивает `GET /projects` (`refetch` запроса проектов). Через контекст проекта или прямо — на усмотрение исполнителя. ### Вложения 18. **Прежняя ошибка не мигает после успешного повтора (#86 T6.2).** После успешного скачивания хук помнит «успех в этой карточке» для вложения. Строка (`AttachmentList.tsx:36`) не показывает серверный `state = failed`, пока данные карточки не перечитаны. Отметку снимает приход данных, где `state` вложения уже не `failed`, или новый `start`. 19. **401 при опросе скачивания — экран входа (#86 T6.3).** В `useAttachmentDownload` (`:74`, `:84`) `AuthError` идёт в `dropTokenOn(e)`, как в остальном приложении, а не текстом под вложением. `HubDownError` — текст «хаб не отвечает» под вложением с «Повторить» (требование «Ошибки хаба»). 20. **Порядок вложений — как у хаба (#86 M-4).** Хаб уже отдаёт вложения новыми сверху (`mirror.py:300-301`: по `created_at` UTC, затем `attachment_id`). Веб не пересортирует (`newestFirst` убирается из `AttachmentList`). `findAttachment` (`inline.ts:36-45`) при одинаковом имени берёт первое вложение в порядке хаба. Так выбор совпадает с `attachment_ids` хаба и MCP и не зависит от `Date.parse` строк Jira. ### UI-кит 21. **Фокус после «Очистить» (#68 T3.3).** `SearchInput` (`ui/inputs.tsx:22`): после нажатия «Очистить» фокус переводится в поле ввода (ref). Иначе кнопка исчезает вместе с фокусом. ## Risks / Trade-offs - **#116 — гонка в порядке эффектов.** Решение опирается на то, что эффект смены проекта стоит раньше эффекта опроса и синхронно меняет `activeRef`. Тест должен воспроизводить именно один коммит со сменой проекта и ответом опроса, иначе он проходит и без правки. Исполнитель проверяет RED. - **`wikiPlain` и разбор.** Плоский текст строится разбором карточки. Ошибка разбора в ленте даёт исходник — так же безопасно, как в карточке (рендер блока с ошибкой — исходник). - **Ссылка «нет в зеркале» в Jira.** Адрес Jira — тот же `JIRA_URL`, что у «Открыть в Jira». Задача может быть и вне Jira (опечатка в связи) — тогда Jira покажет свою 404, это приемлемо. - **Окно 404 и «Назад».** `depth` берётся из state окна. При прямом адресе `depth = 0` — только «Закрыть». ## Decisions made without discussion Все — 25.10.2026: владелец пропустил вопросы brainstorming («делай сам»). Рулинги R-n — с R-1 в пределах фичи. - R-1 (design #1): при открытии карточки отложенный поиск записывается сразу, окно открывается поверх списка с ним. Обоснование: пользователь ввёл поиск и видит отфильтрованную строку, на которую щёлкнул, — потерять поиск хуже. Цена ошибки — вместо этого отбрасывать поиск: правка одной функции. - R-2 (design #2): повторное закрытие окна игнорируется до смены адреса. Цена ошибки — нет. - R-3 (design #3): фокус подтверждения ухода — на «Остаться». Обоснование: безопасное действие по умолчанию, данные не теряются от лишнего Enter. Цена ошибки — перенести фокус на другую кнопку. - R-4 (design #4): ключ связи «нет в зеркале» открывает Jira в новой вкладке, а не окно хаба с 404. Обоснование: в хабе про задачу ничего нет, полезное действие — посмотреть её в Jira. Цена ошибки — вернуть окно: одна ветка в `Links`. - R-5 (design #4): у окна 404 после перехода по связи — «Назад» (на шаг) и «Закрыть» (всё окно). Цена ошибки — убрать кнопку. - R-6 (design #5): #116 чинится в провайдере (ответ сверяется с текущим `activeRef`), тест не ослабляется. Цена ошибки — нет: гипотеза подтверждена чтением кода при сверке. - R-7 (design #6): лента показывает текст комментария без разметки по разбору карточки; «История» карточки — тоже. Альтернатива — показывать начало комментария отрендеренным — отвергнута: строка ленты однострочная. Цена ошибки — правка `eventText`. - R-8 (design #7): метки видов работ — отдельной строкой «Вид работы в Jira» с названиями видов, из «Меток» убраны. Альтернатива — просто скрыть. Отвергнута: метка вида — единственный след вида работы в Jira, и он может расходиться с заметкой. Цена ошибки — скрыть строку. - R-9 (design #12): `[~логин|текст]` показывается «@логин» без подписи. Jira такой формы не документирует, логин важнее подписи. Цена ошибки — показывать подпись. - R-10 (design #13): непарная `[` в строке таблицы — обычный символ. Цена ошибки — нет. - R-11 (design #14): при ошибке `/settings` список всё равно запрашивается (с `hide_closed=1`, закрытых статусов нет), а не блокируется. Обоснование: список полезнее пустого экрана, ошибку настроек покажет страница. Цена ошибки — нет. - R-12 (design #19): `HubDownError` при опросе скачивания остаётся текстом под вложением с «Повторить», отдельного экрана нет — данные карточки остаются на экране (требование «Ошибки хаба»). Цена ошибки — нет. - R-13 (design #20): веб берёт порядок вложений у хаба, а при одинаковых именах — первое в этом порядке. Обоснование: совпадение с `attachment_ids` хаба и MCP. Цена ошибки — вернуть сортировку на клиенте. - R-14 (proposal): в пакет не взяты #42 M-3 (возврат фокуса после закрытия окна), M-6 (`100vh`), доступность UI-кита #68 и производительность разбора #56 M-2. Это не ошибки поведения, они остаются в своих issue. Цена ошибки — следующий пакет. - R-15 (design #22, 26.10.2026): `closingKey` в `overlay.tsx` сбрасывается и в `abortClose` («Остаться»), а не только при смене `location.key`. Обоснование: при заблокированной навигации ключ записи не меняется, и без сброса второй Esc/✕ после «Остаться» молча игнорировался бы (дефект плана, пойман e2e Task 4). Цена ошибки — повторный ✕ при блокере станет повторно вызывать navigate; безвредно. - R-16 (design #6): в ленте вложение читается именем без списка вложений карточки — флаг `WikiOptions.anyAttachment`, выставляется только `wikiPlain`; карточка его не передаёт. Обоснование: финальное ревью I-1, `!скрин.png|thumbnail!` и `[^отчёт.pdf]` иначе остаются сырой разметкой. Цена ошибки — правка в одном месте. - R-17: #18 выполнен частично (отметка «успех» не снимается приходом данных) и разбор всего комментария ради 300 символов в ленте — отложены (M-2, M-6 финального ревью). Обоснование: редкий случай и общая тема #56 M-2. Цена ошибки — редкая невидимая ошибка вложения в открытой карточке; задержка ленты на очень длинных комментариях. - R-18 (design #2): ссылка R-15 указывает на решение 2, а не на несуществующее #22; к решению 2 дописан сброс в `abortClose`. Цена ошибки — нет (документация).