214 lines
15 KiB
Markdown
214 lines
15 KiB
Markdown
# План: аудит верности `preview/*` против `refs-html` (DOM → CSS)
|
||
|
||
> **СТАТУС: РЕАЛИЗОВАН (2026-09-07) — устарел.** Скрипты
|
||
> `web/scripts/audit-dom.mjs`, `audit-css.mjs`, `audit-cdp.mjs`,
|
||
> `audit-cdp-responsive.mjs` и npm-скрипты `refs-dom-audit` / `refs-css-audit` /
|
||
> `refs-cdp-audit` на месте; старый текстовый `audit.mjs` удалён. Актуальная
|
||
> доводка верности и статус пунктов F — в `plan-design-fix.md`.
|
||
|
||
> Статус: к выполнению. Ветка `feat/redesign`. Источник правды по пикселям:
|
||
> `refs-html/*.html` (статический экспорт рефа). Актуальный рабочий аудит сейчас
|
||
> — `pnpm refs-audit` (`scripts/audit.mjs`), но он **сломан** (см. «Почему
|
||
> текущий аудит не работает») и подлежит замене.
|
||
|
||
## Почему текущий аудит не работает
|
||
|
||
`scripts/audit.mjs` сопоставляет элементы **только по тексту** и берёт в
|
||
сравнение **только листовые узлы** (у которых нет детей). Контейнеры/карточки/
|
||
панели, несущие `border-radius`, отступы и позиционирование, в сравнение
|
||
**вообще не попадают** (`byText` кладёт в мапу только элементы с непустым
|
||
`text`). Следствия:
|
||
|
||
- разница радиусов у карточек невидима (карточка — родитель без своего текста);
|
||
- позиция кнопки (`Download result`) не ловится — это структурная разница;
|
||
- `onlyOurs / onlyRef` — это просто счётчик текстовых строк на одной стороне,
|
||
**не метрика верности**.
|
||
|
||
Плюс он не делает пиксельного сравнения, то есть принципиально не может сказать
|
||
«выглядит по-другому». Это текстовый дифф стилей листовых текстов — и всё.
|
||
|
||
## Решения (подтверждены автором)
|
||
|
||
1. **Копирайт превью — через i18n `t()` с EN-базой.** Сейчас RU-литералы
|
||
хардкодом только в `preview/+page.svelte:26-27` и
|
||
`preview/list-tools/+page.svelte:31` (эти страницы не используют i18n, в
|
||
отличие от `(old)/` и компонентов `DownloadButton`/`ChainToolBlock`/…).
|
||
Заводим их через `t()` — совпадёт с рефом (EN).
|
||
2. **Имена классов НЕ сводим к рефу.** Матчинг делаем по **структурному пути**,
|
||
а не по классу. Компоненты `kit/` остаются идиоматичными, Stage 0 не
|
||
откатываем. Требуется только эквивалентность _структуры_ (теги, вложенность,
|
||
порядок сиблингов, наличие узлов, текст).
|
||
3. **Пиксельный дифф (Playwright + pixelmatch) пока НЕ делаем.** DOM+CSS-аудита
|
||
достаточно; добавим как сеть позже, если стили что-то упустят.
|
||
4. **Порядок прогона маршрутов:** `demo` → `list-tools` → затем 2 инструмента
|
||
(`tools/linear-gradient-png` ↔ `gradient.html`, `tools/remove-background-png`
|
||
↔ `background-remover.html`).
|
||
|
||
## Принципы
|
||
|
||
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
|
||
- Коммиты атомарные (< ~500 строк), по 1–2 компонента/скрипта; после каждого —
|
||
`lint` + `svelte-check` + `build` + соответствующий аудит (diff не должен
|
||
расти без причины).
|
||
- Коммиты делает автор (агент не коммитит).
|
||
- Шрифты уже совпадают: `design2.css:1-6` грузит IBM Plex Sans/Mono через
|
||
`@fontsource`, токены `:34-35` — `"IBM Plex Sans"/"IBM Plex Mono"` (как в рефе
|
||
`demo.html:67-68`). Пиксельный шум от шрифтов не предвидится.
|
||
|
||
## Подход: две фазы
|
||
|
||
Корень проблемы — нет **стабильного ключа сопоставления**. Разбиваем на две
|
||
фазы: сначала доводим DOM до структурного равенства с рефом (фаза A), затем на
|
||
стабильном DOM сравниваем стили по тому же пути (фаза B). Фаза A сама по себе
|
||
ловит именно то, из-за чего возникли претензии (кнопка не в том родителе,
|
||
лишние/недостающие обёртки, перепутанный порядок) — и без эстетических решений.
|
||
|
||
---
|
||
|
||
## Фаза A — DOM/структурный аудит (`scripts/audit-dom.mjs`)
|
||
|
||
**Цель:** Svelte рендерит структуру, идентичную `refs-html` (дерево тегов,
|
||
вложенность, порядок сиблингов, наличие узлов, текст). Имена классов могут
|
||
отличаться.
|
||
|
||
### Нормализация узла (ключ сравнения)
|
||
|
||
Для каждого элемента (пропускаем `SCRIPT`/`STYLE`/`NOSCRIPT` и скрытые
|
||
поддеревья, игнорируем служебное):
|
||
|
||
- **тег** (`tagName` в lower-case);
|
||
- **классы** — как отсортированный набор (для инфо, не для матчинга);
|
||
- **нормализованный текст** — `trim`, `collapse-whitespace`, без SVG-иконок
|
||
внутри (текст иконок игнорируем);
|
||
- **порядок детей** — позиция среди сиблингов (`nth-child`).
|
||
|
||
Матчинг — по **структурному пути** от корня сравнения:
|
||
`tag:nth-child(i) > tag:nth-child(j) …`. Не по классу и не по тексту.
|
||
|
||
### Полное соответствие, без IGNORE
|
||
|
||
Оболочка (наш `TopBar`/`AppShell`/`preview-footer` vs рефовый
|
||
`.topbar`/`.footer`/`.language`) тоже сводим к рефу — **IGNORE-список не
|
||
заводим**. Сравниваем всё дерево от `<body>` (без пропуска служебного, кроме
|
||
Svelte-атрибутов и комментов гидрации), чтобы получить полное структурное
|
||
соответствие, а не только контента. Шапка/футер/переключатель языка — часть
|
||
верности; позже, после полного фикса дизайна, всё равно планируется рефакторинг
|
||
компонентов, где оболочку приведём в порядок и унифицируем. Аудит на этом этапе
|
||
просто показывает все расхождения, включая оболочечные.
|
||
|
||
### Дифф
|
||
|
||
Для каждого маршрута — набор дельт:
|
||
|
||
- `added` — у нас, нет в рефе;
|
||
- `removed` — в рефе, нет у нас;
|
||
- `moved` — тот же текст/тег, другой путь (перепутанный порядок/родитель);
|
||
- `textMismatch` — нормализованный текст различается (в т.ч. RU vs EN).
|
||
|
||
Класс-мисматч — **только информационно**, не блокирует (классы свои по решению
|
||
№2). Счётчики: `added / removed / moved / textMismatch`.
|
||
|
||
### Рендеринг
|
||
|
||
- **наше:** dev-сервер на `5179` (как сейчас в `audit.mjs`).
|
||
- **реф:** `pathToFileURL` к `refs-html/<ref>.html` (`file://`). Реф —
|
||
самодостаточный (инлайн `<style>`), рендерится полностью. Шрифты для фазы A не
|
||
важны (сравниваем структуру/текст, не растеризацию).
|
||
- Переиспользуем из текущего `audit.mjs`: `discoverRoutes()`, `EXCLUDE`
|
||
(`/preview`), `DYNAMIC` (`tools/[id]` → `gradient.html` /
|
||
`background-remover.html`), `refNameFor()`.
|
||
|
||
### Шаги (на каждом маршруте)
|
||
|
||
1. Прогнать `pnpm refs-dom-audit`, получить дельты.
|
||
2. Поправить структуру Svelte под реф (добавить/убрать/переставить узлы, вынести
|
||
тексты в `t()` с EN-базой).
|
||
3. Перезапустить, пока `added/removed/moved/textMismatch` ≃ 0 (кроме `IGNORE`).
|
||
|
||
---
|
||
|
||
## Фаза B — CSS-аудит по стабильному DOM (`scripts/audit-css.mjs`)
|
||
|
||
**Цель:** на совпадающем дереве сравнить вычисленные стили по тому же
|
||
структурному пути и получить «элемент по пути X: свойство → наше/реф».
|
||
|
||
### Сравниваемые свойства (computed)
|
||
|
||
`borderRadius`, позиция/размер (`getBoundingClientRect`: x/y/w/h), `padding`
|
||
(все стороны), `margin`, `color`, `backgroundColor`,
|
||
`borderTop/Right/Bottom/Left Width|Color`, `fontFamily`, `fontSize`,
|
||
`fontWeight`, `letterSpacing`, `lineHeight`, `gap`, `display`, `boxShadow`.
|
||
|
||
### Дифф
|
||
|
||
Для каждого состыкованного по пути элемента — список свойств с дельтой (наше
|
||
значение → реф). Элементы, не совпавшие по пути (остаток из фазы A), идут в
|
||
`unmatched`. Отчёт: элемент → свойство → наше/реф. Правится одной строкой CSS,
|
||
без брутфорса.
|
||
|
||
### Шаги
|
||
|
||
1. Прогнать `pnpm refs-css-audit` (только после того, как фаза A по маршруту
|
||
дала ≃ 0 структурных расхождений).
|
||
2. Поправить стили в `design2.css` / стилях компонентов.
|
||
3. Перезапустить, пока delta% по свойствам ≃ 0.
|
||
|
||
---
|
||
|
||
## Порядок маршрутов и особенности
|
||
|
||
1. **`/preview/demo`** (↔ `demo.html`) — уже ближе всего; идеальная «обкатка»
|
||
тулинга фаз A и B. Здесь же проверим, что `Download result` стоит в шапке
|
||
`.preview-top` (как в рефе), а не внизу панели.
|
||
2. **`/preview/list-tools`** (↔ `list-tools.html`) — объёмнее; **зависит от
|
||
`docs/plan-list-tools.md`**: реальный аудит имеет смысл только после того,
|
||
как страница переписана на `CatalogHeader`/`CatalogToolbar`/`CatalogGroup`
|
||
(шаги 1–5 плана list-tools). То есть фаза A/B для list-tools стартует после
|
||
landing этих шагов.
|
||
3. **`tools/linear-gradient-png`** (↔ `gradient.html`) и
|
||
**`tools/remove-background-png`** (↔ `background-remover.html`) —
|
||
**последние**. Известные расхождения: наши инструменты могут иметь **другие
|
||
наборы параметров / UI**, чем конкретные рефовые тулзы. Ожидаются структурные
|
||
и стилевые дельты, которые не сводятся к «подровнять паддинг». Для них —
|
||
триаж после прогона:
|
||
- реальный разрыв верности → фиксим;
|
||
- принципиальное отличие (параметр, которого нет в реф-тулзе; иной контрол) →
|
||
либо аккуратно приводим наш UI параметра к рефу, либо **документируем как
|
||
допустимое расхождение / заносим в allowlist** конфига аудита (селектор +
|
||
причина). «Скипы/фиксы» для инструментов оформляем явно, чтобы аудит не
|
||
шумел на осознанных отличиях.
|
||
|
||
---
|
||
|
||
## Структура скриптов
|
||
|
||
- `scripts/audit-dom.mjs` — фаза A (нормализация + структурный дифф).
|
||
- `scripts/audit-css.mjs` — фаза B (матчинг по пути + дифф стилей).
|
||
- `package.json`: `refs-dom-audit`, `refs-css-audit` (опционально `refs-audit`,
|
||
прогоняющий обе фазы).
|
||
- Переиспользуем из старого `audit.mjs`: `discoverRoutes`, `EXCLUDE`, `DYNAMIC`,
|
||
`refNameFor`, `waitFor` (fast-fail на 4xx/5xx).
|
||
- Отчёты: `web/audit/dom-report.{md,json}` и `web/audit/css-report.{md,json}`
|
||
(старый `audit-report.*` заменяем).
|
||
|
||
## Верификация (после каждого коммита)
|
||
|
||
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
|
||
— без ошибок.
|
||
- `pnpm refs-dom-audit` (фаза A) / `pnpm refs-css-audit` (фаза B) по затронутому
|
||
маршруту: счётчики расхождений не растут без причины.
|
||
|
||
## Риски / заметки
|
||
|
||
- **Стабильность матчинга:** нужна аккуратная нормализация (сортировка классов,
|
||
игнор svelte-атрибутов/`data-*`, игнор whitespace-текстовых узлов, игнор
|
||
внутренностей SVG-иконок). Подстраивается по первым прогонам demo.
|
||
- **Оболочка** (TopBar/footer/язык) сейчас отличается по дизайну — сводим к рефу
|
||
в рамках полного соответствия (без IGNORE); финальная унификация компонентов —
|
||
отдельным рефакторингом позже.
|
||
- **Динамические маршруты:** `DYNAMIC` уже есть; оставляем.
|
||
- **Hover / адаптив / тёмная тема** — вне объёма статического аудита (initial
|
||
state, 1440×900, light). Отмечаем как будущие прогоны.
|
||
- **list-tools** зависит от landing `plan-list-tools.md` (шаги 1–5) — не
|
||
аудируем вслепую.
|