Дельта web-app влита в openspec/specs: изменено «Карточка задачи», добавлено «Разметка Jira в карточке». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 KiB
18 KiB
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
- Модуль
web/src/issues/wiki/.parse.ts— чистая функцияparseWiki(text: string): Block[](дерево блоков и строчных узлов, без React);WikiText.tsx— компонент<WikiText text=… />, превращающий дерево в React-элементы. Разделение даёт тесты разборщика без DOM и один компонент для описания и комментариев. - Дерево. Блоки:
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. - Разбор блоков — построчно. Строка классифицируется по началу:
h[1-6]., маркеры списка^\s*[*#]+или^\s*-(ровно один-;--,*-,-#— не список),|,{code…}/{noformat}/{quote}(открывающий тег в начале строки после необязательных пробелов; содержимое — всё до первого закрывающего тега, который может стоять в той же строке или в любой последующей; остаток строки после закрывающего тега разбирается как новая строка; тег не в начале строки — обычный текст),bq.,----, пустая строка; прочее — абзац. Соседние строки списка образуют один список; пустая строка между пунктами одного вида списка не рвёт список (так Jira показывает шаблон ошибок с пустыми строками между*). Незакрытый{code}/{noformat}/{quote}— открывающая строка разбирается как обычный текст (видна как есть). - Строчный разбор — однопроходный по позиции с проверкой границ из спеки: открывающий маркер — в начале текста
или после пробела / знака препинания, за ним не пробел; за открывающим не тот же символ (
**x**, написанное через MCP Markdown-ом, остаётся текстом как есть — читается лучше полуразобранного); закрывающий — тот же символ после не-пробела, за ним не буква и не цифра; закрывающий ищется до конца строки (выделение не переходит через перенос). Без пары — символ остаётся текстом. Вложенность (жирный курсив*_x_*) — рекурсивным разбором содержимого.{{…}}и содержимое{code}не разбираются.\экранирует следующий символ разметки. - Ссылки и безопасность. Адрес ссылки проходит проверку схемы (
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в модуле нет (проверка — тестом поиска по исходникам модуля). - Отказоустойчивость.
WikiText— собственная маленькая граница ошибок (класс сgetDerivedStateFromError, по образцуshell/ErrorBoundary.tsx, но с фолбэком «исходник», а не «Страница сломалась»), внутри — разбор и рендер: при исключении блок показывается исходником (<pre class="wiki-source">), остальная карточка работает. Разборщик не бросает на нормальном пути; граница — страховка. - Переключатель. Компонент
SourceToggle— кнопка.linkс текстом «исходник» (в режиме рендера) / «текст» (в режиме исходника),aria-pressed. Состояние —useState(false)внутриDescriptionи внутри компонента одного комментария (Comment, ключ —comment_id);IssueModalмонтирует карточку заново при смене задачи (<ErrorBoundary key={issueKey}>), поэтому выбор не переживает переход по связи и закрытие окна. Без localStorage. - Свёртка в режиме рендера.
-webkit-line-clampне работает на контейнере с блочными детьми (списки, таблицы), поэтому свёрнутое описание ограничивается высотой:max-height= 8 строк (calc(8 * 1.5em)приline-height: 1.5) иoverflow: hidden, признак «длинное» в режиме рендера — только измерениеscrollHeight > clientHeight + 1после рендера (без счёта строк исходника: шаблон ошибок с пустыми строками между пунктами иначе показал бы «развернуть» без нужды). В режиме исходника свёртка и признак прежние (больше 8 строк исходника или измерение). Переключение режима не сбрасывает «развёрнуто». - Вид. Отрендеренный текст — в том же «листе», что и сейчас (
.description: фонvar(--panel), рамка, отступ), шрифт интерфейса; заголовки — 15/14/13 px полужирным, отступы абзацев и списков компактные; таблица — рамкиvar(--line), заголовочные ячейки — фоном;{code}/{noformat}/{{…}}—var(--font-mono); цитата — левая полосаvar(--line); широкая таблица илиpre— горизонтальная прокрутка внутри блока, окно не расширяется. - Тесты. Разборщик — таблица случаев «текст → дерево» в
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 |