Andrey Epifancev 372a195ae0 chore(openspec): архив wiki-render (#53)
Дельта web-app влита в openspec/specs: изменено «Карточка задачи», добавлено «Разметка Jira в карточке».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:23:28 +04:00

106 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Context
`main` 86739cf. Устройство, на которое опираются правки:
- **Данные.** `GET /issues/{key}` отдаёт `description: string | null` и `comments: [{comment_id, author, created,
updated, body}]` — тексты Jira как есть (вики-разметка Jira Server, REST v2). Хаб и MCP не меняются.
- **Карточка** — `web/src/issues/IssueCard.tsx`: `Description` выводит текст в `<pre class="description">` со свёрткой
до 8 строк (`-webkit-line-clamp` на `.description.clamped`, признак «длинное» — больше 8 строк или
`scrollHeight > clientHeight`, кнопка «развернуть» / «свернуть»); `Comments` выводит `<pre class="body">` в каждом
`<li>`, над ним `.who` (автор, время); «Скрыть» / «Показать» раздела — `useCommentsHidden` (localStorage).
Стили — `web/src/issues/issues.css`, токены цветов — `var(--panel)`, `var(--line)`, `var(--muted)` и др.
- **Ссылка на карточку** — `IssueLink` (`web/src/issues/IssueLink.tsx`): `<Link to="/issues/{key}"
state={openCardState(loc)}>`, открывает задачу в том же окне с сохранением страницы под ним.
- **Реальные тексты AURORA** (выборка 17.10.2026): заголовки `h3.`; нумерованные `#` и маркированные `*` списки;
` * пункт` с пробелом перед маркером и пустыми строками между пунктами (шаблон ошибок); `*Жирный заголовок.*` в начале
комментария; ключи задач `AURORA-1445` в тексте; в описаниях, написанных через MCP, — Markdown-конструкции
(`` `код` ``, `- пункт`), которые Jira показывает как есть (`-` — список Jira).
## Goals / Non-Goals
**Goals:** описание и комментарии читаются так, как в Jira; исходник любого блока — одной кнопкой; рендер не ломает
карточку ни на каком тексте и не открывает путь для вставки разметки или скриптов.
**Non-Goals:** см. proposal.md.
## Decisions
1. **Модуль `web/src/issues/wiki/`.** `parse.ts` — чистая функция `parseWiki(text: string): Block[]` (дерево блоков и
строчных узлов, без React); `WikiText.tsx` — компонент `<WikiText text=… />`, превращающий дерево в React-элементы.
Разделение даёт тесты разборщика без DOM и один компонент для описания и комментариев.
2. **Дерево.** Блоки: `heading {level, inlines}`, `paragraph {inlines}` (одиночные переносы внутри — узел `br`),
`list {ordered, items: [{inlines, children: list[]}]}`, `table {rows: [{cells: [{header, inlines}]}]}`,
`pre {text}` (`{code}`, `{noformat}`), `quote {blocks}` (`{quote}`, `bq.`), `hr`. Строчные: `text`, `strong`, `em`,
`del`, `ins`, `sup`, `sub`, `code` (`{{…}}`), `link {href, inlines}`, `issue {key, inlines?}`, `mention {login}`,
`br`. Рендер: `h1`–`h6` (внутри карточки уровни сдвигаются стилем, не тегом — тег по разметке), `p`, `ul`/`ol`/`li`,
`table`/`th`/`td`, `pre`, `blockquote`, `hr`, `strong`, `em`, `del`, `ins`, `sup`, `sub`, `code`,
`<a target="_blank" rel="noopener noreferrer">`, `IssueLink`.
3. **Разбор блоков — построчно.** Строка классифицируется по началу: `h[1-6]. `, маркеры списка `^\s*[*#]+ ` или
`^\s*- ` (ровно один `-`; `--`, `*-`, `-#` — не список), `|`, `{code…}` / `{noformat}` / `{quote}` (открывающий тег в
начале строки после необязательных пробелов; содержимое — всё до первого закрывающего тега, который может стоять
в той же строке или в любой последующей; остаток строки после закрывающего тега разбирается как новая строка;
тег не в начале строки — обычный текст), `bq. `, `----`, пустая строка; прочее — абзац. Соседние строки списка образуют один список; пустая строка между пунктами одного вида списка **не** рвёт
список (так Jira показывает шаблон ошибок с пустыми строками между ` * `). Незакрытый `{code}` / `{noformat}` /
`{quote}` — открывающая строка разбирается как обычный текст (видна как есть).
4. **Строчный разбор — однопроходный по позиции** с проверкой границ из спеки: открывающий маркер — в начале текста
или после пробела / знака препинания, за ним не пробел; за открывающим не тот же символ (`**x**`, написанное через MCP Markdown-ом,
остаётся текстом как есть — читается лучше полуразобранного); закрывающий — тот же символ после не-пробела, за ним не
буква и не цифра; закрывающий ищется до конца строки (выделение не переходит через перенос). Без пары — символ
остаётся текстом. Вложенность (жирный курсив `*_x_*`) — рекурсивным разбором содержимого. `{{…}}` и содержимое
`{code}` не разбираются. `\` экранирует следующий символ разметки.
5. **Ссылки и безопасность.** Адрес ссылки проходит проверку схемы (`http:`, `https:`, `mailto:`, без учёта регистра,
после обрезки пробелов); иначе вся конструкция `[…]` — текст. Голые адреса — `https?://` до пробела, хвостовые
`.,;:!?)` не входят в адрес. Ключ задачи — `\b<project_key>-\d+\b`, где `project_key` — из настроек хаба
(`useSettings()`, сейчас `AURORA`), не внутри адреса, `{{…}}`, `{code}`; `parseWiki(text, {projectKey})`, без
`projectKey` (настройки ещё не загружены) ключи — текст. Так `UTF-8`, `SHA-256`, `DOC-221280` ссылками не становятся. Рендер
только через React-элементы: текст экранирует React, `href` — после проверки схемы; `dangerouslySetInnerHTML` в
модуле нет (проверка — тестом поиска по исходникам модуля).
6. **Отказоустойчивость.** `WikiText` — собственная маленькая граница ошибок (класс с `getDerivedStateFromError`, по
образцу `shell/ErrorBoundary.tsx`, но с фолбэком «исходник», а не «Страница сломалась»), внутри — разбор и рендер:
при исключении блок показывается исходником (`<pre class="wiki-source">`), остальная карточка работает. Разборщик
не бросает на нормальном пути; граница — страховка.
7. **Переключатель.** Компонент `SourceToggle` — кнопка `.link` с текстом «исходник» (в режиме рендера) / «текст» (в
режиме исходника), `aria-pressed`. Состояние — `useState(false)` внутри `Description` и внутри компонента одного
комментария (`Comment`, ключ — `comment_id`); `IssueModal` монтирует карточку заново при смене задачи
(`<ErrorBoundary key={issueKey}>`), поэтому выбор не переживает переход по связи и закрытие окна. Без localStorage.
8. **Свёртка в режиме рендера.** `-webkit-line-clamp` не работает на контейнере с блочными детьми (списки, таблицы),
поэтому свёрнутое описание ограничивается высотой: `max-height` = 8 строк (`calc(8 * 1.5em)` при `line-height:
1.5`) и `overflow: hidden`, признак «длинное» в режиме рендера — только измерение `scrollHeight > clientHeight + 1`
после рендера (без счёта строк исходника: шаблон ошибок с пустыми строками между пунктами иначе показал бы
«развернуть» без нужды). В режиме исходника свёртка и признак прежние (больше 8 строк исходника или измерение). Переключение режима не сбрасывает «развёрнуто».
9. **Вид.** Отрендеренный текст — в том же «листе», что и сейчас (`.description`: фон `var(--panel)`, рамка, отступ),
шрифт интерфейса; заголовки — 15/14/13 px полужирным, отступы абзацев и списков компактные; таблица — рамки
`var(--line)`, заголовочные ячейки — фоном; `{code}` / `{noformat}` / `{{…}}` — `var(--font-mono)`; цитата — левая полоса
`var(--line)`; широкая таблица или `pre` — горизонтальная прокрутка внутри блока, окно не расширяется.
10. **Тесты.** Разборщик — таблица случаев «текст → дерево» в `parse.test.ts` (все конструкции спеки, границы
выделения, ложные срабатывания `data_type`, даты, `a*b*c`, числа с ` - `, незакрытые блоки, опасные схемы, ключи
задач, тексты по образцу реальных AURORA). Компонент — `WikiText.test.tsx` (теги, ссылки, `IssueLink`, неизвестное
текстом, фолбэк при ошибке). Карточка — `IssueCard.test.tsx` (кнопки у описания и у каждого комментария,
независимость, сброс при новом монтировании, свёртка в обоих режимах). e2e — сценарий «Описание со структурой» и
«Переключение на исходник» в `web/e2e/issues.spec.ts`. Фикстуры — синтетические тексты по образцу реальных, без
данных заказчика (R-1).
## Risks / Trade-offs
- Собственный разборщик расходится с Jira в краевых случаях (вложенные таблицы, смешанные списки, макросы). Смягчение:
неразобранное — текстом, исходник — одной кнопкой; расхождение видно и чинится точечно.
- Высота свёртки в режиме рендера — приблизительно 8 строк (заголовок или таблица выше строки текста).
## Decisions made without discussion
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| R-1 | 2026-10-17 | Тестовые тексты — синтетические по образцу реальных описаний AURORA, без копирования текстов задач в репозиторий | В задачах — названия организаций и цифры заказчика; конструкции разметки воспроизводятся без них | Пропущенная редкая конструкция реального текста; видна в карточке, чинится тестом |
| R-2 | 2026-10-17 | Ссылками становятся только ключи проекта `project_key` из настроек хаба (`AURORA-123`), открывают карточку в окне хаба, а не в Jira | Jira делает ссылками ключи существующих проектов; хаб знает только свой проект, а общий шаблон `[A-Z]+-\d+` ловит `UTF-8`, `SHA-256`, `DOC-221280` (builder check); переход в Jira есть в шапке | Ключи других проектов Jira — текстом; задача AURORA вне зеркала — «нет в зеркале» |
| R-3 | 2026-10-17 | `[~логин]` показывается как «@логин»: отображаемых имён пользователей в хабе нет | Jira показывает имя, но словаря логин → имя в зеркале нет | Логин вместо ФИО |
| R-4 | 2026-10-17 | Пустая строка между пунктами списка одного вида не рвёт список | Так выглядит шаблон ошибок AURORA (` * пункт`, пустая строка, ` * пункт`); один список читается лучше трёх | Два задуманных отдельными списка сливаются в один |
| R-5 | 2026-10-17 | Свёрнутое описание в режиме рендера ограничивается высотой ≈ 8 строк, а не числом строк | `-webkit-line-clamp` не работает на блочном содержимом | Свёрнутая часть немного выше или ниже 8 строк |
| R-6 | 2026-10-17 | Подсветка синтаксиса `{code}`, макросы `{panel}`, `{color}`, картинки, эмодзи, типографика `--` — вне change | Утверждённый объём; неразобранное видно текстом | Эти конструкции видны исходником |
| R-7 | 2026-10-17 | Открывающий маркер выделения, за которым тот же символ (`**`, `__`, `--`), не считается маркером | Markdown `**жирный**` из описаний, написанных через MCP, при буквальном применении правил Jira разобрался бы в «жирное `*x` и `*`» — хуже исходника; Jira-разметка двойных маркеров не использует | `**x**` видно со звёздочками (как и в Markdown-исходнике) |
| R-8 | 2026-10-17 | Блочные макросы `{code}` / `{noformat}` / `{quote}` распознаются только с открывающим тегом в начале строки; закрывающий — где угодно дальше; остаток строки после него — обычный текст | Однострочный `{quote}` посреди фразы в задачах AURORA не встречался; построчный разбор проще и предсказуемее | Такой `{quote}` виден исходником |
| R-9 | 2026-10-17 | Селектор `.issue .comments ul` (список комментариев без маркеров, flex) сужен до `.issue .comments>ul` | Иначе списки разметки внутри комментариев теряли маркеры и раскладывались flex-ом | Нет: список комментариев — прямой потомок раздела |
| R-10 | 2026-10-17 | Тесты свёртки по измерению подменяют `scrollHeight`/`clientHeight` (`vi.spyOn(Element.prototype, …)`) | В jsdom высоты нулевые; прежний тест свёртки опирался на счёт строк исходника, а в режиме рендера признак — только измерение (design #8) | Нет: поведение в браузере проверяет e2e |
| R-11 | 2026-10-17 | В e2e узкого экрана проверяется ширина окна карточки, а не `document.scrollWidth`; таблица разметки на телефоне — отдельный e2e на 375 px (после I-1) | Страница под модальным окном шире экрана и до этого change (931 > 600 px), прокрутка под окном заблокирована | Ширина страницы под окном не проверяется — прежнее поведение |
| R-12 | 2026-10-17 | Перемер переполнения описания — `ResizeObserver` на контейнере и его первом ребёнке, пока описание свёрнуто (C-1) | Карточка из кэша монтируется до `showModal()` с нулевыми размерами; ресайз окна и догрузка шрифтов тоже меняют высоту | Без `ResizeObserver` (старый браузер) — прежний однократный замер |
| R-13 | 2026-10-17 | Подпись ссылки `[подпись|адрес]` разбирается без ссылок, голых адресов и ключей задач (находка ревью M-1, повышена до I-2) | `[AURORA-1445|https://…/browse/AURORA-1445]` — обычная запись Jira; иначе `<a>` в `<a>` и щелчок открывал карточку вместо адреса | Ключ в подписи не кликается отдельно — как в Jira |