Дельта web-app влита в openspec/specs: изменено «Карточка задачи», добавлено «Разметка Jira в карточке». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
18 KiB
Markdown
106 lines
18 KiB
Markdown
## 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 |
|