chore: add audit script for compare with refs

This commit is contained in:
2026-08-27 23:52:32 +05:00
parent adcc255107
commit bcdfc4cf12
6 changed files with 2553 additions and 1 deletions
+84
View File
@@ -0,0 +1,84 @@
# Demo fidelity: /preview/demo vs refs-html/demo.html
Метод: Playwright, viewport 1440×900, скриншоты обеих страниц + замер вычисленных
стилей ключевых элементов + пиксель-дифф (без чтения картинки моделью).
Пиксель-дифф: **24.9%** (наш скролл 1731px, реф 1579px). Зоны различий размазаны
по всей высоте → править надо глобально, не точечно.
Теперь процесс автоматизирован: `pnpm audit` (Playwright) пишет
`web/audit/audit-report.md` — light/dark токены + поэлементный дифф стилей по тексту.
Пиксель-дифф убран (неудобен для работы). Фиксы можно делать в любой момент и
перепроверять аудитом.
## Баги (объективно, правим в любом случае)
- **B1. Кнопки/инпуты рендерятся шрифтом Arial, а не IBM Plex Sans.**
Доказательство: пробы `Reset pipeline``fontFamily: Arial`. Причина: у `button/input`
не задан `font-family: inherit`. Фикс: добавить в reset `design2.css`
`button, input, select, textarea { font-family: inherit; }`. Влияет на все страницы. (P1)
- **B2. Двойной топбар.** Страница демо рендерит свой `.topbar` (brand / AUTO PIPELINE /
help / theme / lang), который перекрывается глобальным `fixed` Header из
`preview/+layout.svelte` (в нём только theme-toggle). В рефе — один топбар.
Контент демо-топбара (brand, статус, язык) визуально спрятан за фикс-хедером.
Фикс: убрать кастомный `.topbar` из демо и сделать хедер лейаута реф-стайл топбаром
(brand + version + status + help + theme + lang) — улучшит и C9/C10. (P1)
## Расхождения дизайна (нужно решение)
- **F1. Фон холста.** Реф: плоский серый `#EEF1F4`. Наш: белый + blueprint-сетка.
Решение: совпасть с рефом (убрать сетку на демо) или оставить сетку как сигнатуру нового дизайна.
- **F2. Бейдж типа шага** (BACKGROUND/TRANSFORM/STYLE). Реф: приглушённый серый `#6D787F`.
Наш (`StepCard` type → `Badge accent`): синий `#1769D2`. Решение: сделать приглушённым
(меняет `StepCard` глобально) или оставить синим.
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий `#1769D2`,
weight 400. Наш (`PreviewTile` label): серый, weight 500.
- **F4. Раскладка preview-stack.** Реф: одна колонка (5 строк, выше). Наш: 2 колонки (grid).
Решение: одна колонка (как реф) или оставить 2 колонки как выбор нового дизайна.
- **F5. Вес лейблов контролов.** Реф: 400. Наш (`MonoLabel`): 500. Мелко.
- **F6. Мета-подписи** (DIMENSIONS/FORMAT/SIZE): реф fs 9px, наш 10px. Мелко.
- **F7. Копирайт:** `Add tool` у нас `+ Add tool`; кнопка `Download result` — сверить стиль
(в рефе синяя с белым текстом 478×42). Пробы `Download result`/`Add tool`/`DIRECTION`
вернули NOT FOUND — скорее артефакт замера (вложенный текст/чип размера), проверить визуально.
- **F8. h1 line-height** чуть выше (наш 150px vs реф 115px) — мелкий зазор.
- **F9. Вертикальный ритм:** контент на ~152px выше, зазоры между секциями отличаются
(pipeline-head y 434 vs 400 и т.д.) — подровнять паддинги под реф.
- **F10. Внутренние отступы preview-панели** (Hide intermediate x 907 vs 1218) — паддинг панели.
## Темы (отложено — F1-F4)
Серый фон `#EEF1F4` — это **светлая** тема. Уточнение по F1: наш токен `--background`
уже равен `#EEF1F4` (совпадает с рефом); видимое отличие — только blueprint-сетка
поверх фона, а не «белый vs серый». То есть F1 = «сетка против плоского», не цвет фона.
Перед тем как сверять цвета бейджей/тайлов/раскладок (F2-F4), нужно сначала
зафиксировать палитры light/dark как единый источник токенов. Сейчас они есть в
`web/src/lib/styles/design2.css` (`:root` + `[data-theme="dark"]`), но не сверены с рефом.
**Как проверить темы в refs-html:**
- Открыть `refs-html/demo.html` (и соседние `gradient.html`, `background-remover.html`)
через Playwright/браузер.
- Нажать theme-toggle в топбаре рефа (иконка луны/солнца) — переключает тему
(скорее всего меняет атрибут/класс на `<html>` или `.page` и переопределяет CSS-переменные).
- Снять скриншоты светлой и тёмной темы, замерить computed-стиль `:root` переменных
рефа (через `getComputedStyle(document.documentElement).getPropertyValue('--...')`)
и сопоставить с нашими токенами в `design2.css`.
- Рефы, похоже, не доделаны по тёмной теме — зафиксировать, какие именно токены
меняются и где расхождения. На основе этого выровнять наши токены, затем вернуться
к F2 (бейдж типа), F3 (тайлы), F4 (раскладка preview-stack).
Решение по F1-F4 принимается только после сверки токенов светлой/тёмной темы с рефом.
**Состояние тем (уточнение):** наша dark-тема реализована в `design2.css`
(`[data-theme="dark"]`), но пока подключена только на preview-маршрутах (старые
страницы грузят `app.css`). Этого достаточно для сверки: токены, отличные от light,
соберём по всем `refs-html/*.html` (не только `demo.html`), остальные токены
не меняются. Light-токены уже совпадают с рефом (`--background`, `--panel`,
`--foreground`, `--muted`, `--line`, `--blue` равны).
## План действий (предлагаю)
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как сигнатуру нового дизайна).
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с рефом».
4. Переснять скриншоты и перепроверить пиксель-дифф.
File diff suppressed because it is too large Load Diff
+377
View File
@@ -0,0 +1,377 @@
# Style audit: http://127.0.0.1:5179/preview/demo
vs C:\+XBOCTuK\+life_projects\easy-png-tools\refs-html\demo.html
_2026-08-27T18:45:14.461Z_
## Сводка
- Токены light: **12** расх.
- Токены dark: **18** расх.
- Элементы (стиль/геометрия): **48** расх.
- Только у нас: 14, только в рефе: 7
## Токены — Light
| token | ours | ref |
|---|---|---|
| `--danger` | #e5484d | — |
| `--success` | #25a96a | — |
| `--font-sans` | "IBM Plex Sans", system-ui, -apple-system, sans-serif | "IBM Plex Sans", sans-serif |
| `--font-mono` | "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace | "IBM Plex Mono", monospace |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
## Токены — Dark
| token | ours | ref |
|---|---|---|
| `--panel` | #182129 | #f8fafb |
| `--blue` | #54a2ff | #1769d2 |
| `--muted` | #91a0ac | #6d7883 |
| `--background` | #11171d | #eef1f4 |
| `--danger` | #ff7479 | — |
| `--foreground` | #e8eef2 | #17212b |
| `--success` | #25a96a | — |
| `--line` | #33414c | #cbd3da |
| `--font-sans` | "IBM Plex Sans", system-ui, -apple-system, sans-serif | "IBM Plex Sans", sans-serif |
| `--font-mono` | "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace | "IBM Plex Mono", monospace |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
## Расхождения элементов (топ 48)
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(1)>span:nth-child(3)` — "BACKGROUND"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(23, 105, 210)``rgb(109, 120, 131)`
- **padding**: `2.4px 7.2px``0px`
- **letterSpacing**: `0.8px``1.2px`
- **borderRadius**: `4px``0px`
- **display**: `flex``inline`
- **gap**: `4.8px``normal`
- **rect**: `116,507 84x20``143,489 67x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(2)>div:nth-child(1)>span:nth-child(3)` — "TRANSFORM"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(23, 105, 210)``rgb(109, 120, 131)`
- **padding**: `2.4px 7.2px``0px`
- **letterSpacing**: `0.8px``1.2px`
- **borderRadius**: `4px``0px`
- **display**: `flex``inline`
- **gap**: `4.8px``normal`
- **rect**: `116,670 78x20``143,651 60x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(3)>div:nth-child(1)>span:nth-child(3)` — "STYLE"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(23, 105, 210)``rgb(109, 120, 131)`
- **padding**: `2.4px 7.2px``0px`
- **letterSpacing**: `0.8px``1.2px`
- **borderRadius**: `4px``0px`
- **display**: `flex``inline`
- **gap**: `4.8px``normal`
- **rect**: `116,785 50x20``143,799 34x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(2)>div:nth-child(4)>button:nth-child(1)` — "RU"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `11px``10px`
- **color**: `rgb(255, 255, 255)``rgb(238, 241, 244)`
- **backgroundColor**: `rgb(23, 105, 210)``rgb(23, 33, 43)`
- **padding**: `4.8px 11.2px``6px 9px`
- **letterSpacing**: `0.66px``normal`
- **rect**: `1307,20 37x24``1323,20 29x24`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>span:nth-child(1)` — "COLOR"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontWeight**: `500``400`
- **margin**: `0px``0px 0px 8px`
- **letterSpacing**: `1px``0.8px`
- **lineHeight**: `12px``normal`
- **display**: `block``flex`
- **rect**: `71,576 203x12``143,546 189x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(1)>span:nth-child(1)` — "SOURCE"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **fontWeight**: `500``400`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **letterSpacing**: `1px``normal`
- **lineHeight**: `12px``normal`
- **rect**: `806,320 262x12``808,663 30x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(2)>span:nth-child(1)` — "STEP 01"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **fontWeight**: `500``400`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **letterSpacing**: `1px``normal`
- **lineHeight**: `12px``normal`
- **rect**: `1094,320 262x12``1096,663 35x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(3)>span:nth-child(1)` — "STEP 02"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **fontWeight**: `500``400`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **letterSpacing**: `1px``normal`
- **lineHeight**: `12px``normal`
- **rect**: `806,752 262x12``808,1021 35x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(4)>span:nth-child(1)` — "STEP 03"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **fontWeight**: `500``400`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **letterSpacing**: `1px``normal`
- **lineHeight**: `12px``normal`
- **rect**: `1094,752 262x12``1096,1021 35x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(5)>span:nth-child(1)` — "FINAL OUTPUT"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **fontWeight**: `500``400`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **letterSpacing**: `1px``normal`
- **lineHeight**: `12px``normal`
- **rect**: `806,1183 262x12``808,1378 59x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>button:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "1.2 MB"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``11px`
- **color**: `rgb(255, 255, 255)``rgb(23, 33, 43)`
- **margin**: `0px``5px 0px 0px`
- **display**: `inline``block`
- **rect**: `1090,240 36x13``1246,291 79x13`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(2)>div:nth-child(4)>button:nth-child(2)` — "EN"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `11px``10px`
- **padding**: `4.8px 11.2px``6px 9px`
- **letterSpacing**: `0.66px``normal`
- **rect**: `1344,20 38x24``1352,20 29x24`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(1)` — "100"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `11px``10px`
- **padding**: `4.8px 11.2px``0px`
- **letterSpacing**: `0.66px``normal`
- **rect**: `491,597 44x24``520,567 46x30`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(2)` — "75"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `11px``10px`
- **padding**: `4.8px 11.2px``0px`
- **letterSpacing**: `0.66px``normal`
- **rect**: `535,597 38x24``566,567 46x30`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(3)` — "50"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `11px``10px`
- **padding**: `4.8px 11.2px``0px`
- **letterSpacing**: `0.66px``normal`
- **rect**: `573,597 38x24``612,567 46x30`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>button:nth-child(4)` — "25"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `11px``10px`
- **padding**: `4.8px 11.2px``0px`
- **letterSpacing**: `0.66px``normal`
- **rect**: `611,597 38x24``658,567 46x30`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(4)>div:nth-child(2)>div:nth-child(2)>label:nth-child(2)>span:nth-child(1)` — "Preserve aspect ratio"
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Sans", sans-serif`
- **fontSize**: `13.6px``16px`
- **lineHeight**: `normal``24px`
- **display**: `block``inline`
- **rect**: `399,1036 129x18``432,1021 156x17`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "01"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **padding**: `0px``17px 0px 0px 15px`
- **rect**: `95,510 13x14``59,466 46x150`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(1)>div:nth-child(1)>span:nth-child(4)` — "Gradient background"
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Mono", monospace`
- **fontSize**: `14.4px``15px`
- **margin**: `0px``5px 0px 18px`
- **rect**: `208,508 467x19``143,510 157x18`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(2)>div:nth-child(1)>span:nth-child(2)` — "02"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **padding**: `0px``17px 0px 0px 15px`
- **rect**: `95,673 13x14``59,628 46x136`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(2)>div:nth-child(1)>span:nth-child(4)` — "Remove background"
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Mono", monospace`
- **fontSize**: `14.4px``15px`
- **margin**: `0px``5px 0px 18px`
- **rect**: `201,671 473x19``143,672 140x18`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(3)>div:nth-child(1)>span:nth-child(2)` — "03"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **padding**: `0px``17px 0px 0px 15px`
- **rect**: `95,788 13x14``59,776 46x150`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(3)>div:nth-child(1)>span:nth-child(4)` — "Add outline"
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Mono", monospace`
- **fontSize**: `14.4px``15px`
- **margin**: `0px``5px 0px 18px`
- **rect**: `174,785 501x19``143,820 91x18`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(4)>div:nth-child(1)>span:nth-child(2)` — "04"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(109, 120, 131)``rgb(23, 105, 210)`
- **padding**: `0px``17px 0px 0px 15px`
- **rect**: `95,951 13x14``59,938 46x142`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(4)>div:nth-child(4)>div:nth-child(1)>span:nth-child(4)` — "Round corners"
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Mono", monospace`
- **fontSize**: `14.4px``15px`
- **margin**: `0px``5px 0px 18px`
- **rect**: `174,948 501x19``143,982 107x18`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(1)>span:nth-child(1)` — "DIMENSIONS"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **letterSpacing**: `1px``normal`
- **rect**: `1150,215 70x13``1073,271 79x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>span:nth-child(1)` — "FORMAT"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **letterSpacing**: `1px``normal`
- **rect**: `1150,239 42x13``1159,271 79x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(3)>span:nth-child(1)` — "SIZE"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **fontSize**: `10px``9px`
- **letterSpacing**: `1px``normal`
- **rect**: `1150,264 28x13``1246,271 79x10`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(0, 0, 0)``rgb(23, 33, 43)`
- **rect**: `98,23 118x18``98,23 108x17`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "/"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **color**: `rgb(23, 105, 210)``rgb(109, 120, 131)`
- **rect**: `173,124 7x13``165,124 13x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(1)>h1:nth-child(1)` — "Build your image pipeline."
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Sans", sans-serif`
- **lineHeight**: `normal``57.6px`
- **rect**: `58,155 474x150``58,154 482x115`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(1)>span:nth-child(2)` — "1200 × 800 px"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **margin**: `0px``5px 0px 0px`
- **rect**: `1236,214 86x14``1073,291 79x13`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>span:nth-child(2)` — "PNG-24"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **margin**: `0px``5px 0px 0px`
- **rect**: `1282,238 40x14``1159,291 79x13`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(1)>p:nth-child(2)` — "Chain simple tools together. Every chang"
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Sans", sans-serif`
- **rect**: `58,321 474x51``58,285 482x51`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "source.png"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `597,347 66x14``606,311 60x13`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(2)>div:nth-child(2)>b:nth-child(3)` — "1.8 MB"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `671,347 40x14``674,311 36x13`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(3)>div:nth-child(1)>span:nth-child(1)` — "PROCESSING PIPELINE"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `58,434 137x13``58,400 127x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(1)>div:nth-child(3)>div:nth-child(1)>strong:nth-child(2)>em:nth-child(1)` — "• LIVE"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `184,457 36x13``173,425 33x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "PIPELINE OUTPUTS"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `799,177 58x33``799,188 54x36`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(1)>div:nth-child(1)>strong:nth-child(2)` — "Visual history"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `799,216 93x36``799,233 87x34`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "PNG"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `923,498 28x19``923,478 27x18`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "original.png · 1200 × 800"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `925,706 135x11``942,663 124x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(2)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "gradient applied"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `1262,706 86x11``1275,663 79x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(3)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "background removed"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `962,1138 97x11``977,1021 89x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(4)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "outline added"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `1278,1138 70x11``1290,1021 64x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>div:nth-child(2)>div:nth-child(5)>div:nth-child(2)>div:nth-child(2)>span:nth-child(2)` — "ready · PNG-24"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
- **rect**: `984,1569 76x11``997,1378 69x10`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>section:nth-child(2)>p:nth-child(3)` — "Output is generated in-browser. Your fil"
- **fontFamily**: `"IBM Plex Sans", system-ui, -apple-system, sans-serif``"IBM Plex Sans", sans-serif`
- **rect**: `799,1624 565x17``799,1425 565x17`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "EP"
- **fontFamily**: `"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace``"IBM Plex Mono", monospace`
## Только у нас (структурно)
- `+ Add tool`
- `AUTO`
- `Gradient color`
- `DIRECTION`
- `135°`
- `OPACITY`
- `Automatic subject detection enabled`
- `WIDTH`
- `Outline width`
- `2px`
- `Outline color`
- `RADIUS`
- `Corner radius`
- `18px`
## Только в рефе (структурно)
- `/ DEMO`
- `135
°`
- `100%`
- `2
px`
- `18
px`
- `v2.4.0`
- `© 2024`
+3 -1
View File
@@ -12,7 +12,8 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest run",
"format": "prettier --write .",
"lint": "eslint ."
"lint": "eslint .",
"audit": "node scripts/audit.mjs"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -23,6 +24,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.23.0",
"globals": "^17.11.0",
"playwright": "^1.62.1",
"prettier": "^3.9.6",
"prettier-plugin-svelte": "^4.1.1",
"svelte": "^5.56.1",
+29
View File
@@ -42,6 +42,9 @@ importers:
globals:
specifier: ^17.11.0
version: 17.11.0
playwright:
specifier: ^1.62.1
version: 1.62.1
prettier:
specifier: ^3.9.6
version: 3.9.6
@@ -618,6 +621,11 @@ packages:
flatted@3.4.4:
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -832,6 +840,16 @@ packages:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
playwright-core@1.62.1:
resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==}
engines: {node: '>=20'}
hasBin: true
playwright@1.62.1:
resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==}
engines: {node: '>=20'}
hasBin: true
postcss-load-config@3.1.4:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
@@ -1632,6 +1650,9 @@ snapshots:
flatted@3.4.4: {}
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@@ -1788,6 +1809,14 @@ snapshots:
picomatch@4.0.5: {}
playwright-core@1.62.1: {}
playwright@1.62.1:
dependencies:
playwright-core: 1.62.1
optionalDependencies:
fsevents: 2.3.2
postcss-load-config@3.1.4(postcss@8.5.26):
dependencies:
lilconfig: 2.1.0
+271
View File
@@ -0,0 +1,271 @@
// Стиль-аудит: снимает design-токены (light/dark) и дерево вычисленных стилей
// нашей preview-страницы и refs-html страницы, сравнивает и пишет отчёт.
// Запуск: pnpm audit (опции: --ours <url> --ref <file> --no-serve --route <path>)
import { chromium } from "playwright";
import { spawn } from "node:child_process";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { resolve } from "node:path";
const args = (() => {
const a = {};
for (let i = 0; i < process.argv.length; i++) {
const v = process.argv[i];
if (v === "--no-serve") a.noServe = true;
else if (v === "--ours") a.ours = process.argv[++i];
else if (v === "--ref") a.ref = process.argv[++i];
else if (v === "--route") a.route = process.argv[++i];
}
return a;
})();
const WEB = process.cwd();
const PORT = 5179;
const ROUTE = args.route || "/preview/demo";
const OURS = args.ours || `http://127.0.0.1:${PORT}${ROUTE}`;
const REF = args.ref || resolve(WEB, "../refs-html/demo.html");
const FIELDS = [
"fontFamily",
"fontSize",
"fontWeight",
"color",
"backgroundColor",
"border",
"padding",
"margin",
"letterSpacing",
"lineHeight",
"borderRadius",
"display",
"gap"
];
const waitFor = async (url, ms = 60000) => {
const t = Date.now();
while (Date.now() - t < ms) {
try {
const r = await fetch(url);
if (r.ok) return;
} catch {}
await new Promise((r) => setTimeout(r, 500));
}
throw new Error("dev server not up: " + url);
};
const snapshot = (page, url) =>
page.evaluate((url) => {
const readTokens = () => {
const s = getComputedStyle(document.documentElement);
const out = {};
for (const k of s) if (k.startsWith("--")) out[k] = s.getPropertyValue(k).trim();
return out;
};
const tokensLight = readTokens();
const prev = document.documentElement.getAttribute("data-theme");
document.documentElement.setAttribute("data-theme", "dark");
const tokensDark = readTokens();
if (prev) document.documentElement.setAttribute("data-theme", prev);
else document.documentElement.removeAttribute("data-theme");
const sig = (el) => {
const s = getComputedStyle(el);
const o = {};
for (const f of [
"fontFamily",
"fontSize",
"fontWeight",
"color",
"backgroundColor",
"borderTopWidth",
"borderTopColor",
"padding",
"margin",
"letterSpacing",
"lineHeight",
"borderRadius",
"display",
"gap"
])
o[f] = s[f];
return o;
};
const rect = (el) => {
const r = el.getBoundingClientRect();
return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
};
const walk = (el, path) => {
if (["SCRIPT", "STYLE", "NOSCRIPT"].includes(el.tagName)) return [];
const out = [];
const kids = [...el.children];
out.push({
path,
tag: el.tagName.toLowerCase(),
text: el.children.length === 0 ? el.textContent.trim().slice(0, 40) : "",
rect: rect(el),
style: sig(el)
});
for (let i = 0; i < kids.length; i++)
out.push(...walk(kids[i], `${path}>${kids[i].tagName.toLowerCase()}:nth-child(${i + 1})`));
return out;
};
return { tokensLight, tokensDark, tree: walk(document.body, "body") };
}, url);
async function main() {
let server;
if (!args.noServe) {
const bin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
server = spawn(bin, ["dev", "--port", String(PORT), "--strictPort"], {
cwd: WEB,
stdio: "ignore",
shell: true,
detached: process.platform !== "win32"
});
await waitFor(OURS);
}
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
try {
await page.goto(OURS, { waitUntil: "load" });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(400);
const ours = await snapshot(page, OURS);
await page.goto(pathToFileURL(REF).href, { waitUntil: "load" });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(400);
const ref = await snapshot(page, pathToFileURL(REF).href);
// token diff (игнорируем служебный мусор Tailwind/lightningcss — он
// ref-only и не относится к нашим смысловым токенам)
const TOK_NOISE = /^(--tw-|--lightningcss-|--default-)/;
const tokDiff = (a, b) => {
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
const rows = [];
for (const k of keys) {
if (TOK_NOISE.test(k)) continue;
const av = a[k] ?? "—";
const bv = b[k] ?? "—";
if (av !== bv) rows.push({ key: k, ours: av, ref: bv });
}
return rows;
};
const tokenDiffs = {
light: tokDiff(ours.tokensLight, ref.tokensLight),
dark: tokDiff(ours.tokensDark, ref.tokensDark)
};
// element diff: совпадающие по тексту элементы (структуры разные,
// поэтому nth-child-пути не совпадают — матчим по видимому тексту)
const byText = (tree) => {
const m = new Map();
for (const e of tree) if (e.text) if (!m.has(e.text)) m.set(e.text, e);
return m;
};
const txtO = byText(ours.tree);
const txtR = byText(ref.tree);
const onlyOurs = [];
const onlyRef = [];
const elementDiffs = [];
for (const [k, o] of txtO) {
const r = txtR.get(k);
if (!r) {
onlyOurs.push(k);
continue;
}
const deltas = {};
for (const f of FIELDS) {
const ov = (o.style[f] ?? "").toString();
const rv = (r.style[f] ?? "").toString();
if (ov !== rv) deltas[f] = { ours: ov, ref: rv };
}
const dr = o.rect;
const rr = r.rect;
if (Math.abs(dr.x - rr.x) > 2 || Math.abs(dr.y - rr.y) > 2 || Math.abs(dr.w - rr.w) > 2 || Math.abs(dr.h - rr.h) > 2)
deltas.rect = { ours: dr, ref: rr };
if (Object.keys(deltas).length) elementDiffs.push({ path: o.path, tag: o.tag, text: k, deltas });
}
for (const [k] of txtR) if (!txtO.has(k)) onlyRef.push(k);
const report = {
generatedAt: new Date().toISOString(),
ours: OURS,
ref: REF,
tokenDiffs,
elementDiffs,
onlyOurs: onlyOurs.slice(0, 60),
onlyRef: onlyRef.slice(0, 60),
counts: {
tokenLight: tokenDiffs.light.length,
tokenDark: tokenDiffs.dark.length,
elements: elementDiffs.length,
onlyOurs: onlyOurs.length,
onlyRef: onlyRef.length
}
};
mkdirSync(resolve(WEB, "audit"), { recursive: true });
writeFileSync(resolve(WEB, "audit/audit-report.json"), JSON.stringify(report, null, 2));
writeFileSync(resolve(WEB, "audit/audit-report.md"), toMarkdown(report, ours, ref));
console.log(
`audit done: tokens light/dark=${tokenDiffs.light.length}/${tokenDiffs.dark.length}, ` +
`elements=${elementDiffs.length}, onlyOurs=${onlyOurs.length}, onlyRef=${onlyRef.length}`
);
console.log(`report: web/audit/audit-report.md`);
} finally {
await browser.close();
if (server) {
try {
if (process.platform === "win32")
spawn("taskkill", ["/pid", String(server.pid), "/f", "/t"], { stdio: "ignore" });
else server.kill("SIGTERM");
} catch {}
}
}
}
function toMarkdown(report, ours, ref) {
const tokTable = (rows, a, b) =>
rows.length
? `| token | ours | ref |\n|---|---|---|\n` +
rows.map((r) => `| \`${r.key}\` | ${r.ours} | ${r.ref} |`).join("\n")
: "_все совпадают_";
const sorted = [...report.elementDiffs].sort(
(x, y) => Object.keys(y.deltas).length - Object.keys(x.deltas).length
);
const elRows = sorted
.slice(0, 100)
.map((e) => {
const d = Object.entries(e.deltas)
.map(([k, v]) => {
if (k === "rect") {
const f = (r) => `${r.x},${r.y} ${r.w}x${r.h}`;
return ` - **rect**: \`${f(v.ours)}\`\`${f(v.ref)}\``;
}
return ` - **${k}**: \`${v.ours}\`\`${v.ref}\``;
})
.join("\n");
return `### \`${e.path}\`${e.text ? ` — "${e.text}"` : ""}\n${d}`;
})
.join("\n\n");
return `# Style audit: ${report.ours}\n\nvs ${report.ref}\n\n_${report.generatedAt}_\n\n` +
`## Сводка\n\n- Токены light: **${report.counts.tokenLight}** расх.\n` +
`- Токены dark: **${report.counts.tokenDark}** расх.\n` +
`- Элементы (стиль/геометрия): **${report.counts.elements}** расх.\n` +
`- Только у нас: ${report.counts.onlyOurs}, только в рефе: ${report.counts.onlyRef}\n\n` +
`## Токены — Light\n\n${tokTable(report.tokenDiffs.light)}\n\n` +
`## Токены — Dark\n\n${tokTable(report.tokenDiffs.dark)}\n\n` +
`## Расхождения элементов (топ ${Math.min(100, sorted.length)})\n\n${elRows}\n\n` +
`## Только у нас (структурно)\n\n` +
(report.onlyOurs.length ? report.onlyOurs.map((p) => `- \`${p}\``).join("\n") : "_—_") +
`\n\n## Только в рефе (структурно)\n\n` +
(report.onlyRef.length ? report.onlyRef.map((p) => `- \`${p}\``).join("\n") : "_—_") +
`\n`;
}
main().catch((e) => {
console.error(e);
process.exit(1);
});