docs: update a lot of plans, readmes, descriptions

This commit is contained in:
2026-09-27 09:10:26 +05:00
parent 9c6f223b13
commit 82207c82f2
47 changed files with 1029 additions and 4615 deletions
+1 -8
View File
@@ -1,13 +1,6 @@
node_modules node_modules
# Reference source (legacy Next.js design in refs/): branding icons are not needed # Отчёты разовых аудитов и артефакты прогонов
refs/public/icon.svg
refs/public/icon-dark-32x32.png
refs/public/icon-light-32x32.png
refs/public/apple-icon.png
refs/public/placeholder-logo.svg
refs/public/placeholder-logo.png
refs/public/placeholder.svg
web/audit/cdp-audit*.txt web/audit/cdp-audit*.txt
css-lint.txt css-lint.txt
web/coverage web/coverage
+81 -108
View File
@@ -1,129 +1,102 @@
# AGENTS.md # AGENTS.md
Инструкции для агентов (и IDE-ассистентов) по работе с этим репозиторием. Краткий обязательный контракт. Правило: **у каждой темы ровно один источник
истины**; здесь только то, что агент обязан знать до чтения остального, и то,
что линтер не проверяет. Подробности — по ссылкам ниже.
## Стек ## Стек и команды
- **SvelteKit** (`web/`) — основное приложение, Svelte 5 (runes). - **SvelteKit** (`web/`) — основное приложение, Svelte 5 (runes).
- Менеджер пакетов: **pnpm**. Запускать команды из `web/`, либо через обёртки в - **pnpm** обязателен, npm не использовать. Команды — из корня (`pnpm build`,
корне: `pnpm build`, `pnpm dev` (это `pnpm --dir web ...`). Не использовать `pnpm dev`, `pnpm verify`) или из `web/` с префиксом `pnpm --dir web`.
npm.
- Сборка: `pnpm --dir web build`, проверка типов: `pnpm --dir web check`, тесты:
`pnpm --dir web test` (Vitest), тесты с coverage-гейтом:
`pnpm --dir web test:coverage`, тесты lint-правил:
`pnpm --dir web test:rules`, e2e: `pnpm --dir web test:e2e` (Playwright,
`web/e2e/`, свой webServer на `pnpm build` + `scripts/serve-static.mjs`, порт
4173).
- Форматирование: `pnpm format` в корне (оба прогона) или
`pnpm --dir web format` (только код web/). Проверка: `pnpm check:docs`
(docs) + `pnpm --dir web format:check` (код).
- Полный быстрый gate: `pnpm verify`; подробная матрица команд и CI —
`docs/quality-gates.md`.
- После пуша результат CI проверяется явно: смотреть статусы отдельных job, а не
общий conclusion прогона. У `e2e` стоит `continue-on-error`, поэтому прогон с
упавшим e2e помечается как `success`. `Quality` workflow запускается на каждом
пуше в `main` и на `pull_request`, поэтому browser matrix проверяется сама;
ручной запуск нужен только для старого коммита.
- Линтинг: `pnpm --dir web lint` (ESLint). Полный прогон дизайн-проверок:
`pnpm --dir web lint:all` (ESLint + stylelint + токен-аудит). Каждый шаг
выполняется, даже если предыдущий упал. Новые изменения не должны добавлять
предупреждения без записи в baseline.
## Правила кода | Что | Команда |
| ----------------------- | ------------------------------------------------- |
| Полный быстрый gate | `pnpm verify` |
| Типы | `pnpm --dir web check` |
| Юнит-тесты | `pnpm --dir web test` |
| Тесты lint-правил | `pnpm --dir web test:rules` |
| Линтинг | `pnpm --dir web lint:all` |
| E2E | `pnpm --dir web test:e2e` |
| Форматирование | `pnpm format` (код + docs) |
| Проверка форматирования | `pnpm check:docs` + `pnpm --dir web format:check` |
Синтаксические ограничения описывать не нужно — они проверяются линтером (весь ## Что читать под задачу
список правил и токены-префиксы в `web/eslint-plugins/README.md`, прогон:
`pnpm --dir web lint` / `pnpm --dir web lint:all`). Здесь — только то, что
линтер не умеет.
### Svelte 5 Читать только нужную строку, а не все документы подряд.
Типизация пропсов через локальный `interface Props` закреплена правилом | Задача | Документ |
`conventions/interface-props`; руками его нигде дублировать не надо. | --------------------------------------------- | ---------------------------------- |
| Слои, поток данных, новый инструмент/поле | `docs/architecture.md` |
| Какие проверки запускать, пороги, CI | `docs/quality-gates.md` |
| Правка самих гейтов: «почему так, а не иначе» | `docs/decisions.md` |
| Порядок работы, нагрузка на машину | `docs/agent-workflow.md` |
| Locator-ы, fixtures, browser matrix | `docs/testing-strategy.md` |
| Детали lint-правил и токенов | `web/eslint-plugins/README.md` |
| Задачи и идеи | `docs/backlog.md` |
| Актуальный список инструментов | `docs/tools-map.md` |
| Ручная приёмка UI | `docs/checklist-manual-testing.md` |
| Индекс остальных документов | `docs/README.md` |
### E2E ## Инварианты, которые линтер не проверяет
- Сначала используй `role`, `aria-*`, label и стабильный `href`; `data-testid` - **Сначала искать.** Новая константа, тип или утилита не заводится, пока не
разрешён только для source/result/verdict/empty-state. найдено существующее определение — по ключу в соседних модулях и по всему
- `test.fixme` допускается только для зарегистрированной задачи в backlog или `web/src/`.
checklist; после исправления тест и ссылку обновляют вместе. - **Комментарий объясняет «почему»**, а не пересказывает код. `TODO`/`FIXME` —
- Полные переводы проверяются в unit-тестах; e2e проверяет поведение. только для зарегистрированной задачи: ссылка на `docs/backlog.md` и критерий
- Playwright-прогоны запускаются только по явной команде пользователя, по одному закрытия.
прогону, без циклов повторов и без фонового запуска: локально matrix идёт с - **Один шаг — одна проблема**, примерно до 500 строк; не смешивать lint, i18n,
`workers: 1`. Отзывчивость компьютера пользователя важнее скорости проверки — UI и рефакторинг.
подробности в `docs/agent-workflow.md`. - **Не коммитить.** Коммит и пуш делает разработчик после ревью.
- **Не запускать тяжёлое без спроса.** Playwright — только по явной команде
пользователя, один прогон, без циклов повторов и фонового запуска.
- **Не писать изменяемые числа** (количество инструментов, тестов, локалей,
проценты покрытия): вместо числа — ссылка на источник или команда. Число
допустимо, только если его проверяет конфиг или тест. Обоснование и границы
автоматизации — `docs/decisions.md`, раздел 3.
- **Результат CI смотреть по отдельным job**: у `e2e` стоит `continue-on-error`,
поэтому общий conclusion может быть зелёным при красном e2e
(`docs/decisions.md`, раздел 10).
### Сначала искать ## Границы изменений
Новая константа или тип не заводится, пока не произведён поиск существующего - Production-UI — `web/src/routes/**` и `web/src/lib/components/**`. Старый UI
определения (по ключу в соседних модулях и по всему `web/src/`). Это правило в (`/v1/**`) удалён и не возвращается.
первую очередь для ИИ-агентов: дублирующее определение ухудшает правки в - `web/src/lib/core/**` и `web/src/lib/theme.svelte.ts` — общие модули: их
нескольких местах и запутывает. (Синтаксис самих определений — под линтером: правка проверяется unit-тестами и smoke-сценариями production-потребителей.
строка `conventions/no-string-union-alias` и т.д.) - Направление зависимостей, точки расширения и рецепт правки (инструмент, вид
поля, маршрут, токен) — `docs/architecture.md`, разделы 1–2, 7, 8.
### Границы изменений ## Что проверяется линтером
- Production — только текущие маршруты и `web/src/lib/components/**`. Старый `pnpm --dir web lint:all` — ESLint с `--max-warnings=0` (warn-only правила тоже
UI (`/v1/**`) удалён и не возвращается: без redirects и совместимого режима; блокируют gate), Stylelint и token audit. Синтаксические ограничения здесь не
исторические упоминания живят только в `docs/archive/`. Будущий API-контракт дублируются: полный список правил и префиксы токенов —
`POST /v1/process` из `docs/plan-platform.md` не связан с удалённым UI-v1. `web/eslint-plugins/README.md`; новое правило сначала появляется в warn-only
- `core/` и `theme/` — общие модули: их изменение проверяется unit-тестами и режиме с fixtures и тестами (`test:rules`).
smoke-сценариями production-потребителей.
### i18n - **E2E линтером не покрыт:** порядок locator-ов, допустимые `data-testid`,
`test.fixme` — `docs/testing-strategy.md`.
- Новый пользовательский текст — только через словари и `t()`. Исключения: ## Дизайн
бренд, форматы и имена файлов, технические значения, registry source strings и
`/kit`. Проверяется warn-правилом `i18n/no-hardcoded-user-text`, детали и
список исключений — в `web/eslint-plugins/README.md`.
- Новые ключи добавляются во **все** локали сразу: паритет ключей и
плейсхолдеров проверяет `i18n/dict-consistency`.
### Комментарии Источник правды по визуальному языку — код: `web/src/app.css` (дизайн-токены и
глобальные стили), `web/src/lib/components/**` и витрина `/kit`. Новый дизайн
- Комментарий объясняет «почему», а не пересказывает код. живёт в `app.css` и применяется ко всем production-маршрутам; повторяющийся
- `TODO`/`FIXME` — только зарегистрированные задачи: ссылка на `docs/backlog.md` визуальный элемент — отдельный компонент, даже «просто div с двумя стилями»;
или issue и критерий закрытия. В коде незарегистрированных пометок не дизайн-токены — только из CSS-переменных, новый токен появляется вместе с
оставляем. потребителем (`pnpm --dir web lint:tokens` сообщает о неиспользуемых). Приёмка —
`docs/checklist-manual-testing.md`.
### Новые lint-правила
Новое правило сначала появляется в report-only/warn-only режиме, получает
fixtures и тесты (`test:rules`), и только после очистки baseline может стать
blocking. Учитывай: `lint:all` гоняет ESLint с `--max-warnings=0`, поэтому
warn-правило обязано приземлиться без срабатываний на текущем коде.
### Дизайн
Источник правды по визуальному языку — сам код: `web/src/app.css` (дизайн-токены
и глобальные стили), `web/src/lib/components/**` (компоненты и локальные стили)
и витрина `/kit`. Приёмка — `docs/checklist-manual-testing.md`, ограничения
линтера — `web/eslint-plugins/README.md`. Общие правила, которые линтер не
проверяет:
- Новый дизайн живёт в `web/src/app.css` и применяется ко всем
production-маршрутам.
- Все повторяющиеся визуальные элементы — отдельные компоненты в
`web/src/lib/components/`, даже «просто div с двумя стилями».
- Дизайн-токены (цвета, размеры, длительности, z-index) — только из
CSS-переменных; детали ограничений — в `web/eslint-plugins/README.md`.
- Новый токен появляется вместе с потребителем: `pnpm --dir web lint:tokens`
сообщает о неиспользуемых токенах, и это не блокирует CI только потому, что
проверка оставлена предупреждением.
## Документация ## Документация
- Здесь держать только обязательные инварианты, команды и ссылки на подробные - Новый документ создаётся, только если темы нет в существующем; иначе
документы; не превращать `AGENTS.md` в полный справочник. обновляется источник истины. У документа есть статус (`draft`, `active`,
- Индекс активных документов: `docs/README.md`; подробный workflow: `completed`, `archived`).
`docs/agent-workflow.md`; quality gates: `docs/quality-gates.md`; тестовая - Не дублировать длинные объяснения в нескольких файлах: короткая формулировка и
архитектура: `docs/testing-strategy.md`. ссылка на канонический документ.
- Новые подробные правила, процедуры и таблицы хранить в `docs/`, а детали - **Backlog живёт открытыми задачами:** выполненные удаляются, а критичные для
конкретных lint-правил — в `web/eslint-plugins/README.md`. проекта факты переносятся в тематический документ. Завершённые планы и
- Завершённые или устаревшие документы переносить в `docs/archive/`, указывать исследования — в `docs/archive/`, удаляются через 30 дней; процедура в
дату архивирования и срок удаления; архив — только справка, не источник `docs/archive/README.md`.
требований.
- Trunk-based: коммиты делает разработчик после ревью, самому не коммитить.
Изменения делать небольшими (< ~500 строк), атомарными.
+37 -47
View File
@@ -1,35 +1,35 @@
# easy-png-tools # easy-png-tools
Browser-based PNG utility toolkit — no server, no data upload, everything runs Browser-based image utility toolkit — no server, no data upload, everything runs
locally. locally in your browser.
Inspired by online services like onlinepngtools.com, but fully offline-capable Inspired by online services like onlinepngtools.com, but fully offline-capable
and open source. and open source.
## Features ## Features
- **Pixel-level image processing** — format conversion, transparency/alpha - **A full set of image tools** across categories: conversion,
channel, color transformations, geometry, filters/convolutions, morphology, transparency/alpha, color, channel decomposition, geometry, filters,
quantization & palettes, image generation, text/watermarks, analysis & checks pixel-property masks, generation, text/watermarks, analysis & verdicts — the
current list lives in [`docs/tools-map.md`](docs/tools-map.md)
- **Pipeline system (workspace)** — load image → chain multiple tool steps → - **Pipeline system (workspace)** — load image → chain multiple tool steps →
sequential application → download result; pipelines saved to localStorage, sequential application → download result; pipelines saved to localStorage,
exportable as JSON exportable as JSON
- **Web Worker execution** — heavy operations run off the main thread with - **Web Worker execution** — heavy operations run off the main thread with
automatic fallback to direct calls automatic fallback to direct calls
- **i18n** — full Russian and English localization with search matching - **i18n** — full Russian and English localization
- **Dark/Light themes** — persisted to localStorage - **Dark/Light themes** — persisted to localStorage
- **Pure TypeScript core** — `lib/core/` operates on - **Pure TypeScript core** — `lib/core/` operates on
`ImageData`/`Uint8ClampedArray` with no DOM dependency `ImageData`/`Uint8ClampedArray` with no DOM dependency
- **~30+ tools** across categories: transparency, color, geometry, filters, - **Fully static** — no backend, no analytics, no tracking
morphology, palettes, generation, text, analysis
## Tech Stack ## Tech Stack
- **SvelteKit** (Svelte 5, runes mode) with static adapter — pure static export, - **SvelteKit** (Svelte 5, runes mode) with static adapter — pure static export,
no server no server
- **Vite** + **TypeScript** (strict) - **Vite** + **TypeScript** (strict)
- **Vitest** for unit tests - **Vitest** for unit tests, **Playwright** for e2e
- **ESLint** + **Prettier** - **ESLint** (incl. local plugins) + Stylelint + Prettier
- **pnpm** (required, ^11.20.0) - **pnpm** (required, ^11.20.0)
- **Lucide** icons, **IBM Plex** fonts (self-hosted) - **Lucide** icons, **IBM Plex** fonts (self-hosted)
@@ -54,54 +54,44 @@ pnpm build
Static output is written to `web/build/`. Static output is written to `web/build/`.
### Other Commands ### Commands
| Command | Description | Run from the repository root; `pnpm --dir web …` targets the app package.
| ------------------------ | ------------------------------ |
| `pnpm verify` | Run the full fast quality gate |
| `pnpm --dir web test` | Run unit tests (Vitest) |
| `pnpm --dir web check` | Type-check (svelte-check) |
| `pnpm --dir web lint` | Lint (ESLint) |
| `pnpm --dir web format` | Format code (Prettier) |
| `pnpm format:docs` | Format docs (Prettier) |
| `pnpm --dir web preview` | Preview production build |
## Project Structure ```bash
pnpm install
```txt pnpm dev # dev server
easy-png-tools/ pnpm verify # full fast quality gate — the pre-review gate
├── web/ # Main SvelteKit application pnpm build # static production export → web/build/
│ ├── src/
│ │ ├── lib/
│ │ │ ├── core/ # Pure TS image processing (ImageData-based)
│ │ │ ├── components/ # UI components
│ │ │ ├── tools/ # Pipeline, executor, overlay store
│ │ │ ├── i18n/ # Localization (ru/en)
│ │ │ └── registry.ts # Tool registry (source of truth)
│ │ └── routes/ # SvelteKit routes
│ └── build/ # Static export output
├── docs/ # Architecture docs, roadmap, plans
└── refs/ # Design reference (Next.js, not part of runtime)
``` ```
Полный список команд и матрица «какие проверки запускать при каком изменении» —
в `AGENTS.md` и `docs/quality-gates.md`.
## Architecture ## Architecture
The project is built around a **tool registry** (`registry.ts`) — each tool is a Everything is built around the **tool registry** (`web/src/lib/registry/`): each
self-contained entry with `{id, title, description, category, params, run}`. tool is a self-contained entry
Pages, forms, and pipelines are generated from this registry. `{id, title, description, category, schema, run}`. Pages, forms, and pipelines
are generated from it, and all execution goes through a single
`executor.execute` entry point.
Core image processing lives in `lib/core/` and operates directly on `ImageData` Core image processing lives in `web/src/lib/core/` and operates directly on
objects with no browser API dependencies, making it portable to other `ImageData`, with no browser API dependencies.
environments (WASM, CLI) in the future.
Layering, data flow, and the recipe for adding a tool or a field kind:
[`docs/architecture.md`](docs/architecture.md).
## Roadmap ## Roadmap
See [docs/roadmap.md](docs/roadmap.md) for the full development plan. Current See [`docs/roadmap.md`](docs/roadmap.md) for phases and
phases: [`docs/plan-platform.md`](docs/plan-platform.md) for the API/PWA direction. The
current focus is SEO/GEO ([`docs/plan-seo.md`](docs/plan-seo.md)) plus the
product tasks in [`docs/backlog.md`](docs/backlog.md).
1. **Phase 1** — TypeScript core + web UI (in progress) ## For AI agents
2. **Phase 2** — Rust/WASM core for performance-critical operations
3. **Phase 3** — CLI tool Read [`AGENTS.md`](AGENTS.md) first — it is the mandatory short contract.
## License ## License
+39 -72
View File
@@ -1,89 +1,56 @@
# Документация проекта # Документация проекта
> Статус: **active**. Это индекс документов и карта источников истины для > Статус: **active**. Индекс документов. Обязательный краткий контракт — в
> разработчиков и агентов. Подробные правила находятся в самих документах, а > `AGENTS.md`, подробные правила — в самих документах.
> обязательный краткий контракт — в `AGENTS.md`.
## Порядок чтения ## Порядок чтения
1. `AGENTS.md` — команды и обязательные инварианты. 1. `AGENTS.md` — команды, инварианты и карта «что читать под задачу».
2. Этот файл — выбрать актуальный документ по теме. 2. Этот файл — выбрать актуальный документ по теме.
3. Соответствующий план или checklist темы. 3. Соответствующий план или checklist темы.
4. Исходники, конфигурация и тесты рядом с изменяемым кодом. 4. Исходники, конфигурация и тесты рядом с изменяемым кодом.
## Активные документы ## Активные документы
| Документ | Роль | Статус | | Документ | Когда читать | Статус |
| ---------------------------------- | ---------------------------------------------------- | ------ | | ---------------------------------- | -------------------------------------------------- | ------ |
| `AGENTS.md` | Краткий обязательный контракт для агента | active | | `AGENTS.md` | Всегда: контракт агента | active |
| `docs/agent-workflow.md` | Подробный порядок исследования, изменения и проверки | active | | `docs/architecture.md` | Слои, поток данных, новый инструмент/поле/маршрут | active |
| `docs/testing-strategy.md` | Locator, fixture, i18n и fixme-policy для e2e | active | | `docs/quality-gates.md` | Какие проверки запускать, пороги, coverage, CI | active |
| `docs/quality-gates.md` | Команды проверки, warnings и CI | active | | `docs/decisions.md` | Правка самих гейтов; чтение CI после пуша | active |
| `docs/agent-workflow.md` | Порядок работы, нагрузка на машину, протокол ревью | active |
| `docs/testing-strategy.md` | Правка e2e: locator-ы, fixtures, browser matrix | active |
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active | | `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
| `docs/roadmap.md` | Общие направления развития | active | | `docs/roadmap.md` | Направления развития, фазы 1–10 | active |
| `docs/tools-map.md` | Актуальный список инструментов против реестра | active |
| `docs/tools-audit.md` | Сырые UX/UI-находки по инструментам (вход аудита) | active |
| `docs/plan-fields-audit-fixes.md` | Разбивка аудита на волны правок | draft |
| `docs/plan-seo.md` | SEO/GEO-план | active | | `docs/plan-seo.md` | SEO/GEO-план | active |
| `docs/plan-platform.md` | PWA/API/WASM-план | active | | `docs/plan-platform.md` | API/PWA/шрифты-план | active |
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active | | `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
| `docs/tools-map.md` | Живая карта инструментов | active | | `web/eslint-plugins/README.md` | Правка lint-правил, токенов и их конфигов | active |
| `web/eslint-plugins/README.md` | Детали custom lint rules и token policy | active | | `docs/archive/README.md` | Архивирование и удаление старых документов | active |
| `docs/archive/README.md` | Правила архивирования и удаления старых документов | active |
`docs/archive/` содержит временную историческую справку и не является текущим `docs/plan-domain.local.md` исключён из git через `.gitignore` (выбранное имя
источником критериев приёмки. Для старых документов действует срок хранения 30 домена не должно попасть в историю до покупки). Ссылки на него в `plan-seo.md` и
дней; после него их удаляют, а при необходимости достают из git history. `backlog.md` при клонировании репозитория не разрешаются — это ожидаемо.
## Где хранить новую информацию ## Правила
- Поведение, которое агент обязан соблюдать сразу, — кратко в `AGENTS.md`. - У каждой темы ровно один владелец: остальные документы ссылаются на него, а не
- Точные команды, параметры скриптов и критерии качества — в пересказывают. Новый документ создаётся, только если темы нет в существующем.
`docs/quality-gates.md` или в README соответствующей подсистемы. - У документа есть статус (`draft`, `active`, `completed`, `archived`) и
- Архитектура, потоки данных и границы модулей — в отдельном architecture-плане понятная область применения. Ссылки на конкретный файл или команду проверяются
или документе, не дублируя существующие планы. при изменении источника истины.
- Реализация lint-правил, fixtures и технические детали ESLint — в - Подробные материалы живут в `docs/`, а не в корне. Не создавать отдельные
`web/eslint-plugins/README.md` и исходниках правил. `docs/commands.md` или `docs/known-issues.md`: команды принадлежат
- Продуктовые решения и ручная приёмка — в backlog, плане и checklist. `quality-gates`, известные проблемы — backlog и checklist. `README.md` в корне
остаётся кратким обзором продукта, а не вторым руководством агента.
У документа должен быть один источник истины: остальные файлы ссылаются на него, - Ссылаясь на инструмент по id, сверять id с реестром (`web/src/lib/registry/`):
а не пересказывают. Подробные материалы хранятся в `docs/`, чтобы не засорять исторические названия в старых планах не действуют.
корень репозитория. Не создавать отдельные `docs/commands.md`, - `docs/backlog.md` содержит только открытые задачи: выполненные удаляются, а
`docs/known-issues.md` или копию backlog: команды принадлежат `quality-gates`, критичные для проекта факты из них переносятся в тематический документ.
известные проблемы — backlog и checklist. `README.md` в корне остаётся кратким - `docs/archive/` — временная историческая справка, не источник критериев
обзором продукта, а не вторым руководством агента. приёмки; срок хранения 30 дней, файлы восстановимы из git history.
- Протокол «остановиться и показать изменения» — `docs/agent-workflow.md`,
Новый документ создаётся только когда темы нет в существующем файле; иначе раздел 6.
обновляется источник истины. Активный документ имеет статус (`draft`, `active`,
`completed`) и понятную область применения.
Подробные материалы хранятся в `docs/`, а не в корне. Новый файл создаётся
только если темы нет в существующем документе; иначе обновляется источник
истины.
## Текущий baseline
- production registry содержит 122 инструмента;
- `pnpm --dir web check` проходит без ошибок и предупреждений;
- `pnpm --dir web test` — 46 test files / 534 tests, включая тесты token audit,
PostCSS HCT-плагина и правил линтинга;
- `pnpm --dir web test:rules` — 5 test files / 104 tests;
- `pnpm --dir web lint:all` — 0 errors, 0 i18n warnings и 0 unused design tokens
от token audit;
- coverage `src/lib` — statements 91.77%, branches 83.3%, functions 74.31%,
lines 92.53% при порогах 90 / 82 / 72 / 91;
- `pnpm verify` проходит;
- `pnpm --dir web test:e2e` — 276 тестов в четырёх проектах: 276 passed, 0
skipped, 0 failed. Отказ декодера битого PNG зафиксирован per-engine в
`web/e2e/png-fixtures.spec.ts`.
Состав проверок и их семантика описаны в `docs/quality-gates.md`, тестовая
архитектура — в `docs/testing-strategy.md`. Baseline обновляется здесь при
изменении любого из гейтов.
## Правила актуальности
- У активного документа есть статус: `draft`, `active`, `completed` или
`archived`.
- Ссылки на конкретный файл или команду проверяются при изменении источника
истины.
- Архивный документ не возвращается в live-критерии без явной сверки.
- После каждого шага плана агент останавливается, показывает изменённые файлы,
проверки и нерешённые решения, и ждёт ревью.
+28 -71
View File
@@ -1,46 +1,31 @@
# Workflow для агентов # Workflow для агентов
> Статус: **active**. Подробная процедура работы с репозиторием; краткие > Статус: **active**. Порядок исследования, изменения и проверки. Обязательные
> обязательные правила остаются в `AGENTS.md`. Этот документ не заменяет source > инварианты — в `AGENTS.md`, этот документ не заменяет source of truth
> of truth конкретной подсистемы. > подсистемы.
## 1. Подготовка к задаче ## 1. Подготовка к задаче
1. Прочитать `AGENTS.md` и `docs/README.md`. 1. Прочитать `AGENTS.md` и выбрать документ по карте «что читать под задачу».
2. Найти релевантный активный план, backlog или checklist. 2. Найти релевантный активный план, backlog или checklist.
3. Проверить `git status`: не перезаписывать чужие незакоммиченные изменения. 3. Проверить `git status`: не перезаписывать чужие незакоммиченные изменения.
4. Найти существующие типы, константы, helpers и тестовые паттерны до создания 4. Определить границу изменения: production UI, shared-модуль или инфраструктура
нового определения. — `AGENTS.md`, раздел «Границы изменений». Изменение shared-кода требует
5. Определить границу изменения: проверки production-потребителей.
- production UI — `web/src/routes/**` и `web/src/lib/components/**`; 5. Не добавлять исключения линтера ради отдельных модулей без записи в baseline.
- shared — `web/src/lib/core/**` и `web/src/lib/theme.svelte.ts`.
Изменение shared-кода требует проверки production-потребителей. Не добавлять
исключения линтера ради отдельных модулей без записи в baseline.
## 2. Порядок изменения ## 2. Порядок изменения
- Сначала исправляются тесты или контракт, который фиксирует неверное поведение, - Сначала исправляются тесты или контракт, который фиксирует неверное поведение,
затем production-код — если это не отдельная продуктовая задача. затем production-код — если это не отдельная продуктовая задача.
- Один PR решает одну проблему и не объединяет lint, i18n, UI и архитектурный - Не начинать следующий этап плана в том же шаге: изменение атомарное, ревью
рефакторинг без необходимости. идёт между шагами (раздел 6).
- Изменения держатся небольшими и атомарными, примерно до 500 строк или меньше.
- Не добавлять комментарии, которые пересказывают код. Комментарий объясняет
причину решения или фиксирует неочевидный инвариант.
- Не коммитить изменения автоматически: коммит делает разработчик после ревью.
## 3. Проверки ## 3. Проверки
Команды и их область применения перечислены в `AGENTS.md`; точная матрица и Матрица «изменение → минимальный набор проверок», состав `pnpm verify`, политика
текущая политика warnings/errors поддерживаются в `docs/quality-gates.md`. warnings и чтение CI — в `docs/quality-gates.md`. Здесь только то, что относится
к работе агента.
| Изменение | Минимальная проверка |
| ------------------------------------------------------- | ------------------------------------------------------ |
| Только Markdown | `pnpm check:docs` |
| TypeScript/Svelte | `pnpm --dir web check`, `pnpm --dir web test` |
| Lint-конфиг или custom rule | `pnpm --dir web test:rules`, `pnpm --dir web lint:all` |
| Маршрут, загрузка, результат, download, тема или локаль | `pnpm --dir web test:e2e` |
| Перед релизом | полный quality gate из плана Q1 |
`lint:all` может завершиться успешно при warnings. Это не означает чистый `lint:all` может завершиться успешно при warnings. Это не означает чистый
baseline: нужно отдельно показать число errors, warnings и token warnings. baseline: нужно отдельно показать число errors, warnings и token warnings.
@@ -56,75 +41,47 @@ E2E запускается отдельно от быстрых проверок
- Playwright-прогоны запускаются только по явной команде пользователя: один - Playwright-прогоны запускаются только по явной команде пользователя: один
прогон за раз, без циклов повторов и без фонового запуска. прогон за раз, без циклов повторов и без фонового запуска.
- Локально `workers: 1` (`web/playwright.config.ts`). Browser matrix из четырёх - Локально `workers: 1` (`web/playwright.config.ts`). Browser matrix проходит
проектов проходит медленно, зато не забирает память и не мешает работе медленно, зато не забирает память и не мешает работе пользователя. Скорость
пользователя. Скорость проверки уступает отзывчивости компьютера. проверки уступает отзывчивости компьютера.
- Нельзя запускать тяжёлый прогон параллельно с другим прогоном или в момент, - Нельзя запускать тяжёлый прогон параллельно с другим прогоном или в момент,
когда пользователь работает с приложением. когда пользователь работает с приложением.
- Диагностика флаков идёт от меньшего к большему: один spec, один проект, - Диагностика флаков идёт от меньшего к большему: один spec, один проект,
`--workers=1`, и только затем полный прогон. `--workers=1`, и только затем полный прогон.
- Таймауты не подбираются под конкретную машину. Если проверка падает из-за - Таймауты не подбираются под конкретную машину. Если проверка падает из-за
нехватки ресурсов, это фиксируется как ограничение окружения, а не лечится нехватки ресурсов, это фиксируется как ограничение окружения, а не лечится
увеличением `expect.timeout`. увеличением `expect.timeout` (обоснование — `docs/decisions.md`, раздел 7).
- При необходимости повторного прогона сначала спрашивается пользователь. - При необходимости повторного прогона сначала спрашивается пользователь.
## 4. Работа с тестами ## 4. Работа с тестами
Политика locator-ов, запрещённые зависимости, fixtures, browser matrix и
`test.fixme` — в `docs/testing-strategy.md`. Здесь только рабочая рутина:
- Поведенческий тест проверяет пользовательский контракт, а не текущую - Поведенческий тест проверяет пользовательский контракт, а не текущую
реализацию. реализацию.
- Сначала предпочитать `role`, `aria-*`, label, stable id или href. - Спецификация вызывает общие операции из `web/e2e/helpers/page.ts`, а не
- `data-testid` ограничен source/result/verdict/empty-state; общие повторяет raw-селекторы.
locator-паттерны собраны в `web/e2e/helpers/page.ts`, policy — в
`docs/testing-strategy.md`.
- `data-testid` использовать только там, где семантического locator нет.
- Не фиксировать абсолютное число инструментов, если проверка не связана с этим - Не фиксировать абсолютное число инструментов, если проверка не связана с этим
инвариантом. инвариантом.
- Точные переводы проверять в словарях и i18n helpers; в e2e проверять поведение
и отдельный locale smoke.
- `test.fixme` допускается только для зарегистрированной проблемы или явного
тестового долга, с ссылкой на backlog или checklist. После исправления тест и
его issue обновляются вместе.
- Новые fixtures должны быть воспроизводимыми и не требовать ручного создания - Новые fixtures должны быть воспроизводимыми и не требовать ручного создания
больших бинарных файлов. больших бинарных файлов.
## 5. Документация ## 5. Документация
Источник истины выбирается по теме: Порядок работы с документами и карта источников истины — `docs/README.md`.
Архивирование — `docs/archive/README.md`.
- обязательное поведение агента — `AGENTS.md`;
- индекс и карта документов — `docs/README.md`;
- качество и точные команды — `docs/quality-gates.md`;
- тестовая архитектура — `docs/testing-strategy.md`;
- lint implementation — `web/eslint-plugins/README.md`;
- продуктовые решения — backlog и активные планы.
Не дублировать длинные объяснения в нескольких файлах. Если правило требует
пояснения, в `AGENTS.md` оставить короткую понятную формулировку и ссылку на
подробный документ.
### Архивирование завершённых документов
- Если план или исследование больше не является рабочим источником, перенести
файл в `docs/archive/`.
- В начало файла добавить `Статус`, `Архивирован`, `Удалить после` и причину.
- Архив хранить 30 дней для быстрой справки; после срока удалить после проверки
ссылок. Если решение понадобится снова, использовать git history.
- Не переносить активный план в архив только потому, что он пока не начат.
## 6. Протокол ревью между шагами ## 6. Протокол ревью между шагами
После каждого шага плана агент: После каждого шага плана агент:
1. останавливается и не начинает следующий Q-этап; 1. останавливается и не начинает следующий этап;
2. перечисляет изменённые файлы и принятые решения; 2. перечисляет изменённые файлы и принятые решения;
3. указывает выполненные проверки и их фактический результат; 3. указывает выполненные проверки и их фактический результат;
4. отдельно перечисляет warnings, fixme, долги и вопросы; 4. отдельно перечисляет warnings, fixme, долги и вопросы;
5. ждёт подтверждения на продолжение. 5. ждёт подтверждения на продолжение.
Коммит и пуш делает разработчик. После пуша агент проверяет результат CI и Коммит и пуш делает разработчик. Следующий шаг начинается только после ревью
сообщает его вместе с локальными проверками, включая случай, когда нужный job не текущего изменения; после пуша результат CI читается по процедуре из
запускался: статусы смотрятся по отдельным job, потому что общий conclusion `docs/decisions.md`, раздел 10.
прогона с `continue-on-error` у `e2e` остаётся зелёным. Процедура описана в
`docs/quality-gates.md`.
Следующий шаг начинается только после ревью текущего изменения.
+172
View File
@@ -0,0 +1,172 @@
# Архитектура
> Статус: **active**. Описание текущего кода: границы модулей, поток данных от
> реестра до скачивания, точки расширения. Канонический источник истины — сам
> код; этот документ объясняет, **куда смотреть и что с чем связано**, и не
> дублирует сигнатуры.
## 1. Слои
```txt
web/src/lib/
registry/ # источник истины: записи ToolEntry
registry-schema.ts # декларативная схема параметров + санитизация дефолтов
catalog.ts # группировка и счётчики для /list-tools
categories.ts # id/названия категорий
tool-icons.ts # id → иконка
core/ # чистый TS: операции над ImageData, без DOM
executor/ # единственная точка исполнения инструментов
i18n/ # словари ru/en, t(), списки для i18n-правил линтера
components/ # UI (fields/, layout/, ui/, schema-компоненты)
zip.ts # сборка мультифайлового результата (1 → many)
theme.svelte.ts # light/dark + localStorage
web/src/routes/
+page.svelte # главная
list-tools/ # каталог
tools/[id]/ # страница инструмента
kit/ # витрина компонентов (production, но исключена из i18n-гейта)
```
Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`,
`executor/**` и `core/**`; `core/**` не знает ни о ком и не зависит от DOM;
`registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`.
Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость.
## 2. Поток данных
```txt
registry/*.ts ──(TOOLS: ToolEntry[])
│
├─► catalog.ts ──► routes/list-tools (каталог, поиск, фильтры)
│
└─► routes/tools/[id]/+page.svelte
│ getTool(id) → запись реестра
▼
SchemaToolView.svelte оркестратор страницы
│ schema = tool.schema
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
├─► SchemaSourceTile/SchemaTextSource.svelte вход
└─► SchemaPreview + schema-preview-model.ts вид превью
│ buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult
▼
executor.execute(ctx) единственная точка исполнения
│ сериализация параметров → worker → worker-handler → tool.run(ctx)
▼
core/* пиксели
│
▼
SchemaResultTile / SchemaTextResult + SchemaActions (download/copy)
│ кодирование по ToolEntry.output (mime/ext/quality)
└─► zip.ts, если result === "files"
```
Ключевые решения:
- **Реестр — единственный источник истины.** Страница, форма и пайплайн строятся
из записи; новый инструмент = новая запись + функция `run`.
- **Исполнение идёт только через `executor.execute`.** Компоненты не зовут
`tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки
вызывающего кода.
- **Форма параметров не знает про инструмент.** Она рендерится из
`registry-schema.ts`, поэтому добавление вида поля не трогает страницы.
- **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая
функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод.
## 3. Запись реестра
`ToolEntry` (`web/src/lib/registry/types.ts`):
| Поле | Смысл |
| ------------- | ------------------------------------------------------------ |
| `id` | уникальный ключ; он же URL-anchor и ключ в пайплайне |
| `title` | EN-строка реестра, переводится через `tools.*.title` |
| `description` | EN-строка реестра, переводится через `tools.*.description` |
| `category` | id из `categories.ts` |
| `schema` | `toolSchema({ fields, layout?, meta? })` |
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `output` | mime/ext/quality для download (не у всех инструментов) |
| `run(ctx)` | чистая функция: `ctx` → `ToolResult` |
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный
способ достать запись по id.
## 4. Схема параметров
`registry-schema.ts` — единственное место, где описаны виды полей.
- **Виды поля** (`fieldSpecs`): `number`, `slider`, `color`, `select`, `text`,
`checkbox`, `dimension`, `color-pair`, `colors`, `offset`, `position9`,
`font-style`, `plate`, `gradient`.
- **Фабрика `field.*`** создаёт `Field<T>`; UI-контрол подбирается в
`SchemaFields.svelte` по `spec.kind` через карту `FIELDS`.
- **`schema.layout`** группирует поля (`resolveLayoutGroups`); раскладка
разделов — единственное место, где живёт порядок и группировка.
- **Значения по умолчанию** даёт `defaultSchemaParams`; `applySourceDefaults`
подставляет размеры текущего source; `sanitizeSchemaParams` приводит мусор к
допустимому виду. Правка поведения полей идёт здесь, а не в компоненте.
Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` →
контрол в `components/fields/schema/` → ключ в `FIELDS` → дефолт и санитизация →
i18n-подписи → тест в `registry-schema.test.ts`.
## 5. Исполнение
`executor/executor.ts` — фабрика `createExecutor(factories?)` плюс готовый
экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются,
исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает
сообщение и зовёт `run`. Если worker недоступен — прямой вызов.
Отсюда следует: подключение wasm позже означает замену реализации **внутри**
исполнителя, а не правку компонентов.
## 6. i18n
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и
читается через `t()`; строки реестра (`title`/`description`) — источник для
EN-ключей `tools.*` и словарь-переводчик. Инварианты паритета ключей,
плейсхолдеров и запрет хардкода проверяются линтером, детали —
`web/eslint-plugins/README.md`.
## 7. Точки расширения
| Задача | Где править |
| ------------------------------ | ----------------------------------------------------------------- |
| Новый инструмент | запись в `registry/<group>.ts` + i18n-ключи + тесты (рецепт ниже) |
| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) |
| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге |
| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` |
| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу |
| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов |
| Мультифайловый результат | `result: "files"` + `zip.ts` |
## 8. Рецепт: добавить инструмент
1. **Найти соседей.** Поиск по ключу в `registry/<group>.ts` и по всему
`web/src/` — не заводить дублирующую константу, тип или утилиту.
2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/
`generate`/`text`/`analyze`) и положить запись в её экспорт; при новой
категории — сначала `categories.ts`.
3. **Описать схему** через `field.*`, задать `layout`, если поля нужно
сгруппировать.
4. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
`core/`, без DOM.
5. **Проверить обязательные контракты** для нового id: entry попадает в `TOOLS`,
страница `/tools/<id>` prerender-ится, `input`/`result` согласованы с тем,
что делает `run`.
6. **Добавить i18n-ключи во все локали** сразу.
7. **Тесты:** unit — в `registry/*.test.ts` или рядом с `core`-функцией;
e2e-smoke — существующий spec по типу `input`/`result` (правила —
`docs/testing-strategy.md`).
8. **Обновить `docs/tools-map.md`:** новый инструмент обязателен в разделе
«Реализовано», формат строки — в шапке раздела. Проверяет
`web/src/lib/registry/tools-map-doc.test.ts`: отсутствующий в карте id и
несуществующий в реестре id роняют тест. Обоснование и границы автоматизации
— `docs/decisions.md`, раздел 4.
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
`docs/quality-gates.md`).
+12 -11
View File
@@ -10,7 +10,10 @@
актуальных правил используются `AGENTS.md`, `docs/README.md` и активные планы. актуальных правил используются `AGENTS.md`, `docs/README.md` и активные планы.
Если решение снова понадобится, его можно восстановить из git history или Если решение снова понадобится, его можно восстановить из git history или
создать новый активный документ. создать новый активный документ: **архивные файлы удаляются, а не хранятся
вечно.** Удалённые планы восстановимы через
`git log --diff-filter=D -- name-only -- docs/archive/` и
`git show <commit>^:<path>`.
## Обязательные метаданные ## Обязательные метаданные
@@ -24,19 +27,18 @@
> Использование: только историческая справка > Использование: только историческая справка
``` ```
Дата `Удалить после` — дата архивирования плюс 30 дней. Старые файлы, созданные Дата `Удалить после` — дата архивирования плюс 30 дней.
до введения политики, могут не иметь этих дат; они также считаются архивными и
не являются активными требованиями. При следующем касании их можно удалить или
привести к шаблону.
## Порядок архивирования ## Порядок архивирования
1. Убедиться, что документ действительно завершён, отменён или заменён более 1. Убедиться, что документ действительно завершён, отменён или заменён более
актуальным источником. актуальным источником.
2. Добавить обязательные метаданные. 2. Убрать из него ссылки на несуществующие пути и артефакты прошлых волн.
3. Переместить файл в `docs/archive/`. 3. Добавить обязательные метаданные.
4. Проверить ссылки из backlog, планов, checklist и `docs/README.md`. 4. Переместить файл в `docs/archive/`.
5. Не менять архивный документ под новые требования. 5. Проверить ссылки из backlog, планов, checklist и `docs/README.md`: удалённые
файлы должны потерять и входящие ссылки.
6. Не менять архивный документ под новые требования.
## Удаление ## Удаление
@@ -44,8 +46,7 @@
- проверить, что документ не нужен для текущей работы; - проверить, что документ не нужен для текущей работы;
- найти и удалить оставшиеся ссылки на него из активных документов; - найти и удалить оставшиеся ссылки на него из активных документов;
- удалить файл из `docs/archive/`; - удалить файл из `docs/archive/`.
- при необходимости отметить удаление в истории изменений.
До истечения срока архивный файл не превращается обратно в live-документ: если До истечения срока архивный файл не превращается обратно в live-документ: если
он снова актуален, создаётся новый документ или возвращается версия из git он снова актуален, создаётся новый документ или возвращается версия из git
-118
View File
@@ -1,118 +0,0 @@
# Demo fidelity: /preview/demo vs refs-html/demo.html
> ⚠️ **УТРАТИЛ АКТУАЛЬНОСТЬ — архивная копия.** Актуальный план фикса верности и
> текущий статус пунктов B/F — в `docs/plan-design-fix.md` (Этап 1). Аудит
> верности теперь автоматизирован (`pnpm audit` → `web/audit/audit-report.md`;
> пиксель-дифф не используется); темы в `refs-html` переключаются (скрипт
> инжектится в `scripts/extract-static.mjs`). Ниже — исходный текст на момент
> написания.
Метод: 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`. Решено C16: `StepCard` рендерит `type` как приглушённый моно-текст
(не синий бейдж); индекс шага (`step-index`) — синий `#1769D2`, как в рефе.
- **F3. Подписи превью-тайлов** (SOURCE / STEP 01 / FINAL OUTPUT). Реф: синий
`#1769D2`, weight 400. Решено C16: `PreviewTile` label — синий `#1769D2`.
- **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` равны).
## Состояние (C16)
Цветовые расхождения против `refs-html/demo.html` устранены: стек шрифтов сверен
с рефом (`"IBM Plex Sans", sans-serif` / `"IBM Plex Mono", monospace`), бренд
топбара — `--foreground`, индекс шага — синий, тип шага и подпись плитки — как в
рефе, `Segmented` — 10px/32px. Аудит: токены light 10 / dark 16, элементы 47.
Остаток — sub-pixel (10px→9px лейблы, letter-spacing) и структурные
(`display`/`rect` от наших flex/grid раскладок против статичного мока);
визуально незначимы, правим точечно при желании. Старые тесты (583) — без
регрессий.
## План действий (предлагаю)
> Актуальный пошаговый план фикса вынесен в `docs/plan-design-fix.md` (Этапы
> 0–1: рефакторинг + верность). Ниже — исходные наброски.
1. Сразу поправить B1 + B2 (чёткие баги, улучшают все страницы).
2. По F1–F4 — решение за тобой (совпадать с рефом дословно ИЛИ оставить как
сигнатуру нового дизайна).
3. F5–F10 — мелочи, подчистить в том же проходе, если решаем «совпадать с
рефом».
4. Переснять скриншоты и перепроверить пиксель-дифф.
-140
View File
@@ -1,140 +0,0 @@
# План: единая дизайн-система
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
## 1. Цели и принципы
1. **Один источник правды** — все визуальные константы живут в токенах
`web/src/app.css`; ни один компонент не пишет «магические» цвета/размеры
напрямую.
2. **Никакого дублирования** — повторяющийся визуальный рецепт оформляется один
раз: либо токен, либо глобальный утилитарный класс, либо компонент в `ui/`.
3. **Простые компоненты важнее умных** — если проще сделать отдельные
`SliderField` и `TextField`, делаем два маленьких компонента, а не один
конфигурируемый монстр.
4. **Scoped-стили — только для раскладки** конкретного компонента (сетка,
отступы между блоками). Внешний вид контролов и текстов — из дизайн-системы.
## 2. Аудит: что дублируется прямо сейчас
- **«Приглушённый текст»** (`--text-muted` + размер 0.85–0.95rem) — повторён в
семи местах: `ToolPage` (.description, .hint), главная (.lead, .card-desc),
лейаут (.nav-link, footer), `Preview` (.dims), `ParamForm` (label),
`InfoPanel` (dt).
- **«Заголовок секции капсом»** (uppercase, letter-spacing, muted) — точная
копипаста в `ToolPage` и на главной.
- **Базовые инпуты** (padding, border, radius) — живут только внутри
`ParamForm`; любые новые типы контролов (слайдеры, пипетка цвета) пришлось бы
копировать вручную.
- **Блок «label + отступы»** — дублировался бы в каждом `*Field`; решается
обёрткой `Field.svelte`.
- **«Панель»** (surface + border + radius) — три ручные реализации одного
паттерна: `DropZone`, карточки главной, строки `InfoPanel`.
- **Кнопки** — глобальные классы `.primary`/`.secondary` без компонента;
состояния «занят/недоступен» каждый описывает заново.
- **Ошибка-баннер** — только в `ToolPage`, а понадобится минимум ещё раз в
workspace.
## 3. Токены (расширение `app.css`)
Существующие остаются; добавляются недостающие группы:
```css
:root {
/* типографика */
--text-xs: 0.8rem;
--text-s: 0.85rem;
--text-m: 0.95rem;
--text-l: 1.1rem;
--text-xl: 1.25rem;
--text-xxl: 1.75rem;
--leading-tight: 1.25;
/* тени */
--shadow-card: 0 4px 14px rgb(37 99 235 / 12%);
/* фокус */
--focus-ring: 2px solid var(--accent);
/* контрольные размеры контролов */
--control-height: 2.4rem;
--control-max-width: 16rem;
}
```
Правило именования: семантика, не значение (`--text-muted`, а не `--gray-500`).
## 4. Глобальные утилиты (`app.css`)
Только то, что не требует JS и не является интерактивным:
- `.text-muted` — приглушённый текст (размер наследуется);
- `.text-caption` — мелкий текст `--text-s`;
- `.heading-section` — заголовок секции капсом (вместо двух копипаст);
- `.panel` — поверхность с бордером/радиусом (DropZone, карточки,
InfoPanel-строки);
- `.error-banner` — баннер ошибки;
- `.visually-hidden` — скрытый, но читаемый скринридерами.
Элементные стили из текущего `app.css` (голые `input, select { ... }`) убрать —
внешний вид контролов переезжает в компоненты `ui/`.
## 5. Состав UI-компонентов (`web/src/lib/components/ui/`)
Все — тонкие обёртки над нативными элементами, без состояний кроме своих:
| Компонент | Пропы | Заменяет |
| ---------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `Field.svelte` | `id`, `label`, `hint?`, слот контрола | label + раскладка поля; единая точка для hint'ов и ошибок валидации |
| `Button.svelte` | `variant: 'primary' \| 'secondary'`, `disabled`, `busy`, слот текста | глобальные `.primary`/`.secondary`, локальный busy в `DownloadButton` |
| `TextField.svelte` | `id`, `label`, `type: 'number' \| 'text'`, `min/max/step`, `value = $bindable()` | number/text ветки `ParamForm` |
| `SliderField.svelte` | `id`, `label`, `min/max/step`, `value = $bindable()`, показ значения рядом | ничего — готовая база для будущих слайдеров (яркость, качество, порог) |
| `SelectField.svelte` | `id`, `label`, `options`, `value = $bindable()` | select-ветка `ParamForm` |
| `CheckboxField.svelte` | `id`, `label`, `checked = $bindable()` | чекбокс `ParamForm` |
| `ColorField.svelte` | `id`, `label`, `value = $bindable()` | color-ветка `ParamForm`; точка будущего расширения пипеткой |
| `EmptyState.svelte` | `title`, `hint`, слот действия | «Обработка…», будущие пустые панели исходника/результата |
`Field` — чисто раскладочная обёртка
(`<div class="field"><label for>…</label><slot /></div>`), ничего не знает о
типе контрола. На ней построены `TextField`, `SliderField`, `SelectField`,
`ColorField`. Исключение — `CheckboxField`: у чекбокса label оборачивает контрол
(кликабельная строка), поэтому у него своя раскладка.
## 6. Миграция существующего
1. `ParamForm` — переписывается на `Field` + `*Field`-компоненты; собственная
стилизация контролов и label удаляется.
2. `DownloadButton` и кнопки `ToolPage` — на `Button`.
3. `DropZone`, карточки главной, строки `InfoPanel` — на `.panel` + свои
раскладочные стили.
4. `ToolPage`, главная, лейаут — приглушённые тексты на `.text-*`, заголовки
секций на `.heading-section`, баннер на `.error-banner`.
5. Из `app.css` удалить глобальные классы кнопок и голые стили инпутов после
перехода.
## 7. Порядок работ
| Этап | Содержимое | Проверка |
| ------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
| A. Фундамент | Токены + утилиты в `app.css` | `pnpm check` |
| B. Примитивы | Восемь компонентов в `components/ui/` | `pnpm check`; каждый используется хотя бы раз после этапа C |
| C. Миграция | Переделка пяти файлов из §6 | Визуально: страницы выглядят как раньше; `test/check/build` зелёные |
| D. Чистка | Удаление устаревших классов и дублей из scoped-стилей | grep: в `components/` нет hex-цветов и `var(--space…)` вне раскладки |
## 8. Критерии готовности
- Ни один компонент не задаёт цвет/радиус/шрифт мимо токенов или классов
системы.
- Все интерактивные контролы на страницах — экземпляры `ui/*`.
- `grep -E '#[0-9a-f]{3,6}' web/src/lib/components` не находит ничего, кроме
токенов в `app.css`.
- Визуальной регрессии нет (ручная сверка всех страниц до/после), тесты и билд
зелёные.
## 9. Что сознательно не делаем
- Никаких CSS-фреймворков и препроцессоров — обычный CSS, как решено на старте.
- Никаких тем/тёмного режима — но токены сразу семантические, чтобы тема
добавилась заменой значений `:root`.
- Никаких сложных составных компонентов (модалки, таблицы) — они появятся, когда
потребуется реальная задача.
-237
View File
@@ -1,237 +0,0 @@
# План начальной реализации: минимальный рабочий MVP
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
>
> **Что сделано по плану:**
>
> - Шаг 0 — каркас: SvelteKit + adapter-static, design-токены на CSS, лейаут с
> навигацией по категориям ✔
> - Шаг 1 — ядро: `PixelImage`, декодирование файлов, кодирование PNG/JPEG/WebP,
> скачивание ✔
> - Шаг 2 — операции ядра: color / geometry / alpha / analyze, чистые функции
> без DOM ✔
> - Шаг 3 — реестр из 11 инструментов + санитизация параметров ✔
> - Шаг 4 — универсальные UI-компоненты (DropZone, Preview, ParamForm,
> DownloadButton, InfoPanel, ToolPage) ✔
> - Шаг 5 — маршруты: `/tools/[id]` с prerender всех страниц, главная-каталог по
> категориям ✔
> - Шаги 6–7 — обработка ошибок, состояния, полировка; верификация ✔
>
> **Checkpoint пройден:** все 11 инструментов вручную проверены в браузере
> (загрузка → применение → скачивание), включая найденный и исправленный баг
> resize с сохранением пропорций. Тесты 75/75, svelte-check 0 ошибок,
> статический экспорт собирается.
>
> **Попутные улучшения сверх плана:** корневой скрипт `pnpm dev`;
> `sanitizeParams` против пустых/невалидных значений полей; семантика «вписать в
> размеры» для resize.
## 0. Зафиксированные решения
- **Объём MVP:** только одиночные инструменты «загрузил → настроил → скачал».
Пайплайн-workspace (`/workspace`) вне скоупа этого плана.
- **Набор инструментов первого среза:** ~11 базовых EASY (см. §3). Достаточно,
чтобы проверить насквозь ядро → реестр → универсальную страницу.
- **Стили:** обычный CSS без фреймворка — design-токены на CSS custom
properties, ноль зависимостей.
- **Стек:** SvelteKit + `@sveltejs/adapter-static` + TypeScript, пакетный
менеджер pnpm (уже зафиксирован в корневом `package.json`). Весь код сайта — в
`web/`.
- **Ядро без DOM:** все пиксельные операции — чистые функции над `PixelImage`;
DOM/canvas допускается только в модуле IO и компонентах превью.
---
## 1. Шаг 0 — Каркас проекта
1. Создать SvelteKit-приложение в `web/`: `npx sv create web` (шаблон minimal,
TypeScript, без лишних аддонов).
2. `pnpm install` внутри `web/`.
3. `svelte.config.js`: заменить адаптер на `@sveltejs/adapter-static`.
4. `src/routes/+layout.ts`: `export const prerender = true;` — весь сайт
prerenderится в статику.
5. `src/app.css`: дизайн-токены (палитра, типографика, отступы, радиусы через
CSS custom properties) + минимальный сброс стилей.
6. `src/routes/+layout.svelte`: шапка с названием сайта и навигацией по
категориям инструментов (категории — константа из реестра), футер.
**Проверка:** `pnpm dev` открывает пустой каркас; `pnpm build && pnpm preview`
отдаёт статический экспорт.
---
## 2. Шаг 1 — Ядро: типы и ввод-вывод
Файлы в `web/src/lib/core/`:
- `types.ts`
```ts
export type PixelImage = {
width: number;
height: number;
data: Uint8ClampedArray;
}; // RGBA, как ImageData
```
- `io.ts` — единственное место ядра, где разрешён canvas/DOM:
- `decodeFile(file: File): Promise<PixelImage>` — `createImageBitmap(file)` →
отрисовка на canvas → `getImageData`;
- `encode(img: PixelImage, mime: 'image/png' | 'image/jpeg' | 'image/webp', quality?: number): Promise<Blob>`
— `putImageData` → `canvas.toBlob`;
- `downloadBlob(blob: Blob, filename: string): void`.
**Проверка:** юнит-смоук — декодировать фикстурный PNG, закодировать обратно,
размеры совпадают.
---
## 3. Шаг 2 — Операции ядра (чистые функции)
Файлы в `web/src/lib/core/`, каждая функция: `(img, params) => new PixelImage`,
без мутации входа:
- `color.ts`: `grayscale(img)` (luma), `invert(img)`,
`brightnessContrast(img, brightness −100..100, contrast −100..100)`.
- `geometry.ts`: `flip(img, axis)`, `rotate90(img, turns)`,
`crop(img, x, y, w, h)` — индексная математика; `resize(img, w, h)` —
билинейная интерполяция на TS (не `drawImage`, чтобы ядро осталось DOM-free и
воспроизводимым бит-в-бит).
- `alpha.ts`: `removeColorToAlpha(img, hex, tolerancePercent)` — цветовое
расстояние → альфа 0.
- `analyze.ts`: `imageInfo(img)` → `{ width, height, hasAlpha, colorCount }`.
**Проверка:** vitest на маленьких матрицах пикселей (2×2, 3×3): flip, rotate,
crop, grayscale — ожидаемые значения руками.
---
## 4. Шаг 3 — Реестр инструментов
`web/src/lib/registry.ts` — источник истины для страниц, форм и навигации:
```ts
export type ParamDef =
| {
id: string;
label: string;
type: "number";
min?: number;
max?: number;
step?: number;
default: number;
}
| {
id: string;
label: string;
type: "select";
options: { value: string; label: string }[];
default: string;
}
| { id: string; label: string; type: "checkbox"; default: boolean }
| { id: string; label: string; type: "color"; default: string };
export type ToolEntry = {
id: string; // slug маршрута
title: string;
description: string;
category: CategoryId;
params: ParamDef[];
run: (
img: PixelImage,
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
};
```
Категории (константа там же): `convert`, `alpha`, `color`, `geometry`,
`analyze`.
### Состав первого среза — 11 инструментов
| # | id | Категория | Параметры |
| --- | -------------------------------- | --------- | ----------------------------------------------------- |
| 1 | `resize-png` | geometry | width, height, checkbox «сохранять пропорции» |
| 2 | `crop-png` | geometry | x, y, width, height |
| 3 | `rotate-png` | geometry | select: 90 / 180 / 270 |
| 4 | `flip-png` | geometry | select: horizontal / vertical |
| 5 | `grayscale-png` | color | — |
| 6 | `invert-colors-png` | color | — |
| 7 | `adjust-brightness-contrast-png` | color | brightness −100..100, contrast −100..100 |
| 8 | `convert-png-to-jpg` | convert | background color (подложка под альфу), quality 0..100 |
| 9 | `convert-png-to-webp` | convert | quality 0..100 |
| 10 | `remove-color-from-png` | alpha | hex color, tolerance % |
| 11 | `png-info` | analyze | — особый случай: не картинка, а текстовая сводка |
Для `png-info` в записи реестра добавляется флаг `resultType: 'info'`; остальные
— `'image'`.
**Проверка:** импорт реестра в тесте — все id уникальны, у каждого `params`
имеют дефолты, `run` определён.
---
## 5. Шаг 4 — Универсальные UI-компоненты
`web/src/lib/components/`:
- `DropZone.svelte` — drag&drop + клик; эмитит `File`; проверка MIME
(PNG/JPEG/WebP/GIF/BMP).
- `Preview.svelte` — рендер `PixelImage` на canvas с fit-масштабированием;
шахматный фон под прозрачность.
- `ParamForm.svelte` — генерирует контролы из `ParamDef[]`, двусторонний
`bind:values`.
- `DownloadButton.svelte` — encode выбранного формата + `downloadBlob`, имя
`<tool-id>.<ext>`.
- `InfoPanel.svelte` — текстовый вывод для `resultType: 'info'`.
- `ToolPage.svelte` — оркестратор страницы инструмента; состояния
`idle → loaded → processing → done | error`; раскладка: слева DropZone, справа
превью + форма параметров + кнопки «Применить» / «Скачать».
**Правило:** новый инструмент = новая запись в реестре (+ функция операции).
Компоненты и маршрут не трогаем.
---
## 6. Шаг 5 — Маршруты
- `/` (`src/routes/+page.svelte`) — сетка карточек инструментов, сгруппированных
по категориям; данные из реестра.
- `/tools/[id]` — универсальная страница:
- `+page.ts`: `entries()` возвращает все `id` из реестра (обязательное условие
prerender при adapter-static);
- `load()` возвращает запись реестра по `id`, неизвестный id → 404;
- `+page.svelte` — тонкая обёртка над `ToolPage`.
**Проверка:** `pnpm build` — в `build/tools/` лежат все 11 страниц; главная
содержит ссылки на каждую.
---
## 7. Шаг 6 — Ошибки и минимальная полировка
- Обработка ошибок: неподдерживаемый формат, ошибка декода, некорректные
параметры crop (выход за границы — клампы или понятная ошибка).
- Состояния: «перетащите файл», спиннер во время обработки, disabled-кнопки до
загрузки.
- Тексты описаний для всех 11 инструментов (title/description уже в реестре).
---
## 8. Шаг 7 — Верификация и checkpoint
Команды:
```bash
cd web && pnpm dev # разработка
cd web && pnpm build # статический экспорт должен проходить без ошибок
cd web && pnpm test # vitest: ядро (геометрия/цвет) + целостность реестра
```
Ручной smoke-тест каждого из 11 инструментов: загрузка PNG → применить → скачать
→ открыть результат.
**Checkpoint MVP:** любой из 11 инструментов доступен по прямому URL с главной
страницы; PNG загружается drag&drop; параметры применяются; результат
скачивается; всё работает офлайн со статического хостинга; ни одной серверной
зависимости.
-213
View File
@@ -1,213 +0,0 @@
# План: аудит верности `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) — не
аудируем вслепую.
-114
View File
@@ -1,114 +0,0 @@
# План C17: перенос preview → корень + (old) → /v1/
> **СТАТУС: ВЫПОЛНЕН (2026-09-11) — устарел.** Перенос завершён:
> preview-страницы на реальных маршрутах, старый дизайн на `/v1/*`,
> `app.css`/`app_v1.css`, isolation-плагин на v1-путях, аудит-скрипты в
> `scripts/archive/`. C18 (правки импортов/редиректов, проверка билда) тоже
> выполнен. Следующий этап — C19–C21 (`plan-redesign.md`, §10).
>
> Оригинальный статус до выполнения: план к выполнению. Предпосылки: tech debt
> закрыт, backlog №14 (старые файлы → lib/old/) выполнен. Домен НЕ блокер (см.
> plan-redesign.md §10, plan-seo.md §6).
## Цель
Новый дизайн становится основным на корневых маршрутах (`/`, `/tools/[id]`,
`/list-tools`). Старый дизайн переезжает на `/v1/*` как архив для справки.
Удаление старого кода (C19) откладывается до полной приёмки нового дизайна.
## Структура маршрутов (до → после)
**До:** `(old)/` даёт URLs `/`, `/demo`, `/list-tools`, `/tools/[id]`.
`preview/` даёт `/preview`, `/preview/list-tools`, `/preview/tools/[id]`,
`/preview/kit`.
**После:** `v1/` даёт `/v1/`, `/v1/demo`, `/v1/list-tools`, `/v1/tools/[id]`.
Корень даёт `/`, `/list-tools`, `/tools/[id]`. Витрина kit — на реальном
маршруте `/kit` (дизайн в стадии тестирования).
## Решения
1. **`v1/` — реальная папка, НЕ route group** — group `(v1)/` не меняет URL, а
нам нужно чтобы старые страницы жили по `/v1/*`.
2. **Корневой layout: shell нового дизайна** — TopBar, Footer, тема, хлебные
крошки, импорт `app.css`, `@layer app, design;`.
3. **`v1/+layout.svelte` — только импорт `app_v1.css`** — без
header/footer/shell.
4. **Тема:** использовать `theme.svelte.ts` (не дублировать локальный `$state`).
5. **`@layer app, design;`** остаётся — design выигрывает у app везде.
6. **`noindex` остаётся** на `tools/[id]/+page.svelte` до покупки домена.
7. **CSS-файлы переименовываются:** `preview.css` → `app.css`, `old.css` →
`app_v1.css`. Все ссылки обновляются.
8. **`kit/` витрина → `/kit`** — переносится на реальный маршрут вместе с
остальными preview-страницами; отдельный `preview/+layout` удаляется (`/kit`
наследует корневой shell).
## Что делать (шаги)
### 1. Rename `(old)/` → `v1/`
Переименовать папку. Обновить `v1/+layout.svelte`: убрать «Preview v2» ссылку,
обновить пути навигации, поправить импорт CSS.
### 2. Новый корневой `+layout.svelte` + rename CSS
Перезаписать текущий нейтральный layout содержимым из `preview/+layout.svelte`
(с правками: тема через `theme.svelte.ts`, путь к CSS). Переименовать
`src/preview.css` → `src/app.css`, `src/old.css` → `src/app_v1.css`. Обновить
все ссылки на эти файлы в проекте ( layout, лайнтеры, eslint-плагины, скрипты,
тесты).
### 3. Move preview pages → корень
Переместить страницы из `preview/` в корень `routes/`: `+page` → `/`,
`list-tools` → `/`, `tools/[id]` → `/`, витрину `kit` → `/kit`. Удалить
`preview/+layout.svelte` (корневой shell заменяет его).
### 4. Update hardcoded `/preview/` URLs
В компонентах с захардкоженными путями `/preview/` заменить на корневые.
### 5. Flatten `kit/` → `components/`
Перенести компоненты из `lib/components/kit/` в `lib/components/`, обновить все
импорты, удалить пустую `kit/`.
### 6. Flatten `preview/` → `lib/`
Перенести код из `lib/previw/` в `lib/`, обновить все импорты, удалить пустую
`preview/`.
### 7. Перенести `old/` → `v1/`
- `$lib/old/` — переименовать в `lib/v1/`, обновить все импорты
### 8. E2E тесты + playwright config
Заменить `/preview/...` на `/...` во всех e2e файлах и конфиге playwright.
### 9. ESLint config + AGENTS.md
Обновить glob-паттерны: `routes/preview/**` → `routes/**`, `components/kit/**` →
`components/**`. Обновить `AGENTS.md`.
### 10. Isolation plugin
Обновить `DEFAULT_OLD`: `routes/(old)/**` → `routes/v1/**`. Убрать
`routes/preview/**` из `DEFAULT_NEW`.
### 11. Archive audit scripts
Перенести скрипты аудита в `scripts/archive/`, убрать вызовы из манифестов.
## Что НЕ меняется
- `routes/+layout.ts` — prerender = true остаётся
- Слаги инструментов (`*-png`) — без изменений
- `/kit` — витрина дизайна остаётся (в стадии тестирования)
## Риски
- **v1/ без shell** — старый дизайн без header/footer.
- **`@layer` взаимодействие** — проверить что design-токены перекрывают
app-токены на обоих маршрутах.
- **Isolation: shared-классификация** — маршруты нового дизайна shared до C19.
-593
View File
@@ -1,593 +0,0 @@
# План: составные типы параметров и полная типизация pipeline
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Типизация pipeline, составные типы
> (dimension/color-pair/offset/position9/font-style/plate/gradient),
> `schema.layout`, изоляция веток и вся миграция инструментов (121/125,
> остальные 4 — отложены решениями) выполнены. Ключевые архитектурные решения
> (параллельная сборка, правило копий, изоляция old↔preview) закреплены в
> `AGENTS.md`. **Незакрытое из этого плана:** шаг 37 — физический переезд старых
> файлов в папки `old/` (перенесён в `backlog.md`), и удаление старого кода — по
> `plan-redesign.md` (C17–C21). Отложенные инструменты — `watermark-image-png`
> (overlay), `png-info`, `reduce-to-size` — в `backlog.md`. Ниже — полное
> состояние на момент закрытия.
> Статус: **в реализации.** Фаза 0 (фундамент) ✔, Фаза 1 (рабочий инструмент в
> preview) ✔, Фаза 2 (простые инструменты без составных типов) — переведены все
> одиночные инструменты (18 шт). Фаза 3 — **составной тип `dimension` полностью
> переведён** (12 инструментов: create-empty, single-color, random-noise,
> linear-gradient, color-spectrum, random-colors, draw-grid, placeholder,
> fit-on-background, change-canvas-size, resize, crop); **составной тип
> `color-pair` полностью переведён** (4 инструмента: blend-two, step-colors,
> linear-gradient, two-colors); **составной тип `offset` полностью переведён**
> (4 инструмента: circle-mask, square-mask, star-mask, wavy-mask); **составной
> тип `position9` — переведены add-text-png и date-stamp-png** (водяной
> знак-картинка — отдельный шаг: требует overlay-механику в новом превью);
> **составной тип `font-style` полностью переведён** (text-to-png, add-text,
> date-stamp); **составной тип `plate` полностью переведён** (add-text,
> date-stamp); **составной тип `gradient` полностью переведён**
> (linear-gradient): вместо горизонтального/ вертикального select — **угол
> 0..360°** (новый виджет `AngleControl`: slider + кнопки-пресеты
> 0°/90°/180°/270°, переиспользуемый компонент); preview научен применять
> **генераторы** (`executeGenerate`, кнопка Generate) и рендерить все kinds
> схемы (slider/number/color/select/checkbox/dimension/
> color-pair/offset/position9/font-style/plate/gradient). **Пер-инструмент
> раскладка `schema.layout` реализована** (шаг 32): тип `ToolSchemaLayout`
> {groups: [{title?, cols?, fields}]}, рендер групп в `SchemaFields.svelte`
> (заголовок группы + сетка колонок, неупомянутые поля — в общей группе), пилот
> — `add-text` (группы Text/Placement/Plate). **UI-макеты расставлены по
> переведённым инструментам** (шаг 33): geometry (fit-on-background,
> change-canvas-size, resize, crop), маски alpha (Shape/Position), generate
> (create-empty, linear-gradient, color-spectrum, random-colors, draw-grid,
> step-colors, placeholder, text-to-png), text (add-text, date-stamp), filters
> (randomize-pixels, add-noise) — везде, где канвас отделён от параметров
> эффекта либо фигура от позиции. Следующее закрыто в
> `archive/plan-migrate-remaining.md` (2026-09-07): перевод оставшихся
> инструментов выполнен (всего 121/125 в preview; 4 хвоста —
> `watermark-image-png`, `png-info`, `reduce-to-size-png`, `compress-png` —
> отложены решениями в `backlog.md`). Незакрытые продолжения: переезд старых
> компонентов в папки `old/` (шаг 37 ниже), закрытие долгов и сбор фидбека — см.
> также `plan-design-fix.md`.
>
> Ключевые файлы нового registry: `web/src/lib/registry-new/{types,index,*}.ts`
> (по файлу на категорию: geometry/alpha/convert/analyze/filters/color/generate)
>
> - `web/src/lib/preview/categories.ts`, `web/src/lib/registry-schema.ts` (kind
> `dimension`), `kit/fields/DimensionField.svelte`,
> `preview/executor/index.ts` (`executeGenerate`), `SchemaFields.svelte`
> (полный рендер kinds). Старый `web/src/lib/registry.ts` разбит по категориям
> в `web/src/lib/registry/` (см. `registry.ts` — тонкий баррель).
## Ключевая стратегия: параллельная сборка, старый UI не трогаем
Старый UI (группа `(old)/`, `ParamForm.svelte` + старый pipeline) **продолжает
работать как сейчас, без рефакторинга**. Новый `ToolSchema<P>` строится рядом и
служит источником для **нового UI**. Инструменты переписываются под новый
registry по мере нужды — для нового UI.
Это та же логика, что в `plan-redesign.md` применена к дизайну: параллельная
сборка, изоляция от старого, затем новый становится основным и старый удаляется
вместе со старым дизайном.
**Правило копий (важно!):** если для нового UI/registry нужно внести изменения в
файл, который **прямо или косвенно** уже используется `(old)` веткой, — этот
файл **НЕ трогаем**. Вместо этого делаем **копию** в новом месте (например, в
`lib/preview/`) и правим копию. Это приводит к дублированию, но **гарантированно
не задевает старую ветку сайта**. Пример: категории — новый
`lib/preview/categories.ts` (object as const) копирует и заменяет собой
`../categories` для нового кода, старый `categories.ts` обслуживает `(old)` и
остаётся без изменений. После перехода (Фаза 5) копия становится основной, а
исходник продолжает обслуживать `(old)` UI (он не удаляется — см. шаг 35).
**Разделение схем: две независимые схемы.**
| Схема | Источник | Использование |
| --------------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
| Старый `ParamDef[]` | В `ToolEntry.params` — **остаётся без изменений** | Старый UI, старый pipeline (`ParamForm.svelte`, executor, chain) |
| Новый `ToolSchema<P>` | Новое поле `ToolEntry.schema` — добавляется рядом | Новый UI (kit/`ParamControl`, новый pipeline) |
- Старая и новая схемы **не выводятся друг из друга** — это два независимых
описания инструмента.
- Пока новый UI не готов — `schema` просто отсутствует у большинства
инструментов, старый UI ничего не замечает.
- Удаление старого UI (после перехода) попутно удаляет и старый `ParamDef[]`
### Разделение registry по UI (новое решение, Фаза 2+)
Помимо двух схем, registry тоже разделён по UI (по факту миграции):
- **Старый registry** (`web/src/lib/registry.ts` + `registry/` +
`registry-helpers.ts`) — работает на старом UI, использует `ParamDef[]`. **Не
трогаем**; остаётся обслуживать `(old)/` маршруты (см. шаг 35).
- **Новый registry** (`web/src/lib/registry-new/`) — строится **с нуля «как
надо»**: `ToolEntry<P>` с обязательным `schema`, типизированный `run`, **без**
`ParamDef[]` и **без** связи со старым. Импортирует core-функции
(`expandCanvas`, `strokeImage`, …) напрямую. Наполняется **по-инструментно**
по мере миграции (не разовым переводом всех 130). Preview показывает только
переведённые инструменты.
Preview (`catalog.ts`, маршруты `preview/**`, `SchemaToolView`, `SchemaFields`)
импортируют из `$lib/registry-new`. После удаления старого UI `registry-new/`
переименуется в `registry`.
**Executor/worker для нового UI (осознанное дублирование):** общий
`executor.worker.ts` резолвит инструменты по `id` в **старом** registry — для
новых инструментов их там нет. Поэтому `executor.ts` + `executor.worker.ts`
**скопированы** в `web/src/lib/preview/` и переписаны под `registry-new` +
`sanitizeSchemaParams`. Старый executor/worker под старый UI остаются
нетронутыми; `SchemaToolView` использует `$lib/preview/executor`. Это
дублирование необходимо до ухода старого UI (после — preview-executor заменяет
общий).
## Проблема (текущая)
- `ParamDef` — плоский union из 6 примитивов
(`number | slider | select | checkbox | color | text`). Параметры инструмента
— отдельные записи в плоском массиве.
- `run()`/`generate()` получают `Record<string, unknown>`; значения достаются
через `num(p, "fromColor")` — runtime string lookup **без type safety**, легко
напутать id.
- Поля инструмента **слабо связаны** (например «ширина» и «цвет» рамки — два
независимых поля без общего контракта) → общий генерализованный рендер, нет
возможности сделать идеальный пер-инструмент UI.
- Preview-система (`tool-views.ts`) **дублирует** registry (FieldDef vs
ParamDef) — одни и те же инструменты описаны дважды.
## Решения (принятые в обсуждении)
1. **Полная типизация.** `interface Params` (runtime-тип) объявляется явно в
каждом инструменте. `run`/`generate` типизируются прямо на него, а не на
`Record<string, unknown>`.
2. **Отдельная типизированная схема для UI/дефолтов/валидации.** Два понятия на
инструмент: runtime-тип + схема. Схема проверяется компилятором на
соответствие `Params` (чтобы не рассинхронизировались).
3. **Общий рендер,** но с пер-инструмент layout: общие компоненты рисуют поля по
схеме; схема инструмента задаёт layout и группировку. Без отдельного
Svelte-компонента на каждый инструмент.
4. **Составные типы** (`gradient`, `color-pair`, `dimension`, `offset`,
`position9`, `font-style`, `plate`) остаются в плане — это способ выразить в
схеме связанную группу полей и переиспользовать её на нескольких инструментах
и в UI.
5. Рано или поздно `tool-views.ts` поглощается registry (preview/lede/layout
переезжают в meta инструмента) — но это отдельный шаг, см. ниже.
6. **Старый UI не рефакторим.** Две независимые схемы: старый `ParamDef[]`
остаётся источником для старого UI без изменений; новый `ToolSchema<P>`
строится рядом для нового UI (см. «Ключевая стратегия» выше).
## Целевая архитектура (на примере «добавить рамку»)
Цель: «добавить рамку: ширина и цвет» — **один интерфейс**, а не два слабо
связанных поля.
```ts
// 1. Runtime-тип — что получает run/generate
interface AddBorderParams {
thickness: number; // рамка: ширина
color: string; // рамка: цвет
}
// 2. Типизированная UI-схема, проверяемая компилятором против Params
// Ключи схемы обязаны совпадать с полями Params; default совместим с типом.
const addBorderSchema = toolSchema<AddBorderParams>({
thickness: field.slider({ min: 1, max: 500, default: 5 }),
color: field.color({ default: "#000000" }),
layout: { group: "border", cols: 2 }, // пер-инструмент раскладка
});
// 3. Инструмент в registry
const addBorder: ToolEntry<AddBorderParams> = {
id: "add-border-png",
title: "Add border to PNG",
category: "geometry",
schema: addBorderSchema,
run: (img, p) => expandCanvas(img, 4x..., p.color), // p.thickness — type safe
};
```
### Что даёт
- `p.thickness`, `p.color` — IDE autocomplete + compile-time ошибка при ошибке.
- `run()` нельзя передать лишний/чужой ключ.
- Схема — единый источник для дефолтов, валидации и рендера.
- Схема одного инструмента может задавать layout, которого нет в общем рендере.
## Типовая система (набросок)
```ts
// ─── Поле схемы ───
interface NumberField { kind: "number"; default: number; min?: number; max?: number; step?: number }
interface SliderField { kind: "slider"; default: number; min: number; max: number; step?: number }
interface ColorField { kind: "color"; default: string }
interface SelectField { kind: "select"; default: string; options: { value: string; label: string }[] }
interface TextField { kind: "text"; default: string; placeholder?: string }
interface CheckboxField { kind: "checkbox"; default: boolean }
type FieldDef =
| NumberField | SliderField | ColorField | SelectField | TextField | CheckboxField;
// ─── «поле» описывает тип значения + схему вместе, чтобы не рассинхронизировать ───
interface Field<T> {
schema: FieldDef;
// T — ожидаемый runtime-тип этого поля (number|string|boolean)
}
// ─── Builder: field.slider({...}) возвращает Field<number> с типизированной схемой ───
const field = {
slider: (s: SliderField): Field<number> => ({ schema: s }),
number: (s: NumberField): Field<number> => ({ schema: s }),
color: (s: ColorField): Field<string> => ({ schema: s }),
select: <V extends string>(s: SelectField & { options: {value: V}[] }): Field<V> => ({ schema: s }),
text: (s: TextField): Field<string> => ({ schema: s }),
checkbox: (s: CheckboxField): Field<boolean> => ({ schema: s }),
};
// ─── toolSchema<P>: проверяет, что ключи схемы === полям P и типы сходятся ───
type FieldsOf<T> = { [K in keyof T]: Field<T[K]> } & {
layout?: { group?: string; cols?: number };
};
function toolSchema<P>(fields: FieldsOf<P>): ToolSchema<P> { ... }
// TS выдаст ошибку, если schema содержит ключ, отсутствующий в Params, или
// если type поля не совпадает с типом Params[K].
```
Ключевая идея связки: **builder `field.x<T>()` связывает runtime-тип поля с его
схемой на этапе компиляции**, а `toolSchema<P>` заставляет ключи схемы совпадать
с ключами `P`. Это и есть защита от рассинхрона.
## Составные типы (для переиспользования и группировки)
| Тип | Под-поля | Инструментов |
| ------------ | -------------------------- | ------------ |
| `dimension` | width + height | 12 |
| `color-pair` | from + to | 4 |
| `offset` | x + y | 4 |
| `gradient` | from + to + dir (+ type?) | 1 |
| `position9` | position (3×3 grid) | 3 |
| `font-style` | font + size + bold + color | 3-4 |
| `plate` | enabled + color + opacity | 2-3 |
Составной тип = группы полей, которые в интерфейсе `Params` могут
разворачиваться во вложенный объект или плоские ключи — решается на этапе
реализации. Пример для `gradient`:
```ts
interface GradientParams {
from: string;
to: string;
dir: string;
}
// схема: field.color(from), field.color(to), field.select(dir)
// плюс annotation в схеме: { group: "gradient" } → UI рисует как цветовой переход
```
## Инвентарь: какие инструменты мигрировать
### dimension (12)
resize-png*, crop-png*, fit-on-background-png, create-empty-png,
single-color-png, random-noise-png, linear-gradient-png, color-spectrum-png,
random-colors-png, draw-grid-png, placeholder-png, change-canvas-size-png
(\* у resize/crop — не просто dimension, есть специфика: keepAspect, x/y и т.д.)
### color-pair (4)
blend-two-png, step-colors-png, linear-gradient-png, two-colors-png
### offset (4)
circle-mask-png, square-mask-png, star-mask-png, wavy-mask-png
### position9 (3)
add-text-png, date-stamp-png, watermark-image-png
### font-style (3-4)
text-to-png, add-text-png, date-stamp-png
### plate (2-3)
add-text-png, date-stamp-png
## Пошаговый план перевода инструментов
> Каждый шаг — **атомарное, маленькое изменение** (в идеале 1 инструмент = 1
> diff, укладывается в правило «коммиты < ~500 строк»). После каждого шага —
> ревью; параллельно можно делать UI нового инструмента.
>
> **Принцип малых шагов:** перевод инструмента не меняет поведение старой схемы
> (`params` остаётся), не ломает старый UI. Инструмент получает ДОПОЛНИТЕЛЬНО
> новое поле `schema` + `interface Params`, старое `run`/`params` не трогаем.
> Это гарантирует, что старый UI продолжает работать без регресса.
>
> **Новый подход (решение пользователя):** сначала делаем **один полноценный
> рабочий инструмент в preview** — не эталон «на бумаге», а реально юзабельный,
> чтобы руками оценить, насколько это рабочее решение и какие возникнут
> сложности. Оцениваем, при необходимости правим инфраструктуру, и только потом
> переводим остальные инструменты тем же проверенным паттерном.
### Фаза 0 — фундамент ✔ (сделано)
Typed field builders + `Field<T>` + `toolSchema<P>()` + `ToolSchema<P>` — в
`web/src/lib/registry-schema.ts`. `ToolEntry<P>` generic + опциональное поле
`schema`. Новые `defaultSchemaParams`/`sanitizeSchemaParams`. Старый
`ParamDef[]`/pipeline не тронуты. Проверено: compile-time-защита
(лишний/неверный/отсутствующий ключ ловит TS), тесты, svelte-check без новых
ошибок.
### Фаза 1 — ОДИН полноценный рабочий инструмент в preview (срез) ✔ (сделано)
Инструмент переехал в `registry-new` и рендерится через `SchemaToolView`
(`SchemaFields`/`SchemaPreview`); применение реально работает end-to-end до
картинки. Паттерн подтверждён. `find-contour-png` — следующий в этой фазе-2.
5. **Инструмент «добавить рамку» (`add-border-png`)** — сделать полностью
рабочим в preview:
- `interface AddBorderParams { thickness: number; color: string }`
- `addBorderSchema = toolSchema<AddBorderParams>(...)` + `schema: ...` в
entry (старый `params`/`run` остаются — старый UI не регрессит).
- **Новый рендер в preview** (`kit/ToolView` → `ParamControl` или новый
компонент схемы): читает `ToolSchema<P>`, рисует поля рамки, применение
реально работает (end-to-end до картинки).
- Дефолты/валидация/значения — из схемы (`defaultSchemaParams`,
`sanitizeSchemaParams`).
- **Ревью-оценка:** удобно ли это, что сломалось/усложнилось, что поправить в
инфраструктуре до масштабирования. Принимается макет/решение.
Результат: один реально рабочий инструмент на новой системе = доказываем
паттерн, прежде чем переводить остальных.
### Фаза 2 — простые инструменты без составных типов (1 инструмент = 1 шаг)
Одиночные/небольшие инструменты, где `interface Params` + `schema` не требуют
составных типов. Каждый — отдельный маленький diff (~15-30 строк), тем самым
проверенным в Фазе 1 паттерном. **Переезжают в `registry-new`.**
Переведено: `add-border-png` (Фаза 1), `add-stroke-png`, `find-contour-png`,
плюс весь блок простых инструментов ниже (пункты 8-22). Следующее — Фаза 3.
6. ~~add-stroke-png (color + slider)~~ → переведён в `registry-new` ✔
7. ~~find-contour-png (color + slider)~~ → переведён в `registry-new` ✔
8. ~~convert-png-to-jpg (color + slider)~~ → переведён в `registry-new` ✔
9. ~~convert-png-to-webp (slider)~~ → переведён в `registry-new` ✔
10. ~~remove-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
11. ~~extract-color-from-png (color + slider)~~ → переведён в `registry-new` ✔
12. ~~add-noise-png (slider + select + number)~~ → переведён в `registry-new` ✔
13. ~~randomize-pixels-png (slider + number)~~ → переведён в `registry-new` ✔
14. ~~pixelate-png (slider)~~ → переведён в `registry-new` ✔
15. ~~vignette-png (slider)~~ → переведён в `registry-new` ✔
16. ~~gamma-png (slider)~~ → переведён в `registry-new` ✔
17. ~~temperature-png (slider)~~ → переведён в `registry-new` ✔
18. ~~tint-png (color + slider)~~ → переведён в `registry-new` ✔
19. ~~quantize-png (slider)~~ → переведён в `registry-new` ✔
20. ~~custom-palette-png (text)~~ → переведён в `registry-new` ✔
21. ~~dithering-png (slider + select)~~ → переведён в `registry-new` ✔
22. ~~jpeg-artifacts-png (slider)~~ → переведён в `registry-new` ✔
> **Замечание (конвертеры):** старые `convert-png-to-jpg`/`convert-png-to-webp`
> несли `output`-метаданные (`mime`/`ext`/`qualityParamId`) для выбора формата
> вывода. В новом `ToolEntry<P>` поля `output` пока нет, поэтому при переводе
> эти метаданные не перенесены — preview пока отдаёт результат как PNG. Механика
> выбора формата/качества в новом UI — отдельный шаг (не блокирует Фазу 2).
### Фаза 3 — инструменты с составными типами (по типу, затем по инструментам)
24. `dimension` — **переведены все 12 инструментов** ✔ (`create-empty-png`,
`single-color-png`, `random-noise-png`, `linear-gradient-png`,
`color-spectrum-png`, `random-colors-png`, `draw-grid-png`,
`placeholder-png` — генераторы в `registry-new/generate.ts`;
`fit-on-background-png`, `change-canvas-size-png`, `resize-png`, `crop-png`
— в `registry-new/geometry.ts`). Составной тип во всех видах: вложенный
объект `size: { width, height }` в Params + `field.dimension`, виджет
`kit/fields/DimensionField.svelte`. Preview: генераторы применяются через
`executeGenerate` (кнопка Generate), `SchemaFields` рендерит все kinds схемы
(slider/number/color/select/ checkbox/dimension). Тесты: 592 passed.
25. `color-pair` — **все 4 инструмента переведены** ✔ (`blend-two-png`,
`step-colors-png`, `linear-gradient-png` — генераторы в
`registry-new/generate.ts`, `two-colors-png` — run в
`registry-new/color.ts`). Составной тип во всех видах: вложенный объект
`pair: { from, to }` + `field.colorPair`, виджет
`kit/fields/schema/ColorPairControl.svelte`, kind `color-pair` в схеме
(default/sanitize). Тесты: +4 (596 passed).
26. `offset` — **все 4 инструмента переведены** ✔ (`circle-mask-png`,
`square-mask-png`, `star-mask-png`, `wavy-mask-png` — run в
`registry-new/alpha.ts`). Составной тип во всех видах: вложенный объект
`offset: { x, y }` + `field.offset`, виджет
`kit/fields/schema/OffsetControl.svelte`, kind `offset` в схеме
(default/sanitize). Тесты: +4 (600 passed).
27. `position9` — **переведены `add-text-png` и `date-stamp-png`** ✔ (run в
`registry-new/text.ts`, оба `domOnly`). Составной тип во всех видах:
значение — строка `Position9`, kind `position9` в схеме (default/sanitize по
`POSITION9_VALUES`), виджет 3×3 `kit/fields/schema/PositionControl.svelte`.
В `ToolEntry` добавлен флаг `domOnly` (превью-executor держит такие
инструменты вне worker). `watermark-image-png` — отдельный под-шаг: ему
нужен overlay-source (`getOverlay`/store), которого в новом превью пока нет.
Тесты: +2 (602 passed). `font-style` и `plate` на этих инструментах сводятся
в шаги 28-29.
28. `font-style` — **все 3 инструмента переведены** ✔ (`text-to-png` — генератор
в `registry-new/generate.ts` (domOnly), `add-text-png`/ `date-stamp-png` —
рефакторинг в `registry-new/text.ts`). Составной тип во всех видах:
вложенный объект `style: { font, size, bold, color }` + `field.fontStyle`,
виджет `kit/fields/schema/FontStyleControl.svelte`, kind `font-style` в
схеме (default/sanitize, clamp размера к min/max). Тесты: +1 (603 passed).
29. `plate` — **все 2 инструмента переведены** ✔ (`add-text-png`,
`date-stamp-png` — рефакторинг в `registry-new/text.ts`). Составной тип во
всех видах: вложенный объект `plate: { enabled, color, opacity }` +
`field.plate`, виджет `kit/fields/schema/PlateControl.svelte` (чекбокс +
цвет + слайдер непрозрачности, деактивируется при выключенной плашке), kind
`plate` в схеме (default/sanitize, clamp opacity к 0..100). Покрытие тестов
расширено (дефолты/sanitize plate в существующих тестах).
30. `gradient` — **переведён** ✔ (linear-gradient-png). Направление градиента —
не горизонталь/вертикаль, а **произвольный угол**: новый переиспользуемый
виджет `kit/fields/schema/AngleControl.svelte` (слайдер 0..360° + кнопки
стандартных углов 0°/90°/180°/270° с активным состоянием). Составной тип
`gradient: { from, to, angle }` во всех видах (`field.gradient`, kind
`gradient` в схеме — default/sanitize, clamp угла к 0..360, валидация
цветов; виджет `kit/fields/schema/GradientControl.svelte` — пара цветов +
AngleControl). Рендер по углу — локальный `angleGradient` в
`registry-new/generate.ts` (core-`gradientImage` не трогали: он умеет только
horizontal/vertical и используется старым UI). Семантика: 0° слева направо,
90° сверху вниз, рост угла по часовой (ось Y вниз); нормализация по полному
диапазону проекции на ось (180°/270° разворачивают градиент). Тесты:
дефолты, направление по углу (0°/90°/180°), sanitize/кламп.
### Фаза 4 — масштаб UI на остальные инструменты
31. По мере перевода инструментов в Фазе 2-3 — рендер схемы (из Фазы 1)
покрывает их автоматически; составные виджеты (dimension, color-pair,
offset, position9, font-style, plate, gradient) — по одному, каждый с ревью.
32. Пер-инструмент layout (`schema.layout`) — **реализован** ✔.
`ToolSchemaLayout.groups: { title?, cols?, fields[] }` в registry-schema;
`SchemaFields.svelte` рендерит группы (подпись + сетка `1..n` колонок,
неупомянутые поля — в общей группе в конце, без заголовка). Пилот —
`add-text-png` (Text: text+style; Placement: position+margin; Plate). Тесты:
schema-layout в registry-schema.test.ts; полные запуски чисты.
33. Для каждого переведённого инструмента — UI-макет **расставлен** ✔.
Layout-группы заданы там, где группировка содержательна (канвас отдельно от
параметров эффекта, фигура отдельно от позиции; составные виджеты — внутри
групп целиком):
- geometry: fit-on-background (Canvas/Background), change-canvas-size
(Canvas/Anchor), resize (Canvas/Scaling), crop (Offset/Crop area);
- alpha-маски: circle/square/star/wavy (Shape — с 2 колонками где плотно,
Position — offset);
- generate: create-empty (Canvas/Fill), linear-gradient (Canvas/Colors),
color-spectrum (Canvas/Spectrum), random-colors (Canvas/Random), draw-grid
(Canvas/Grid), step-colors (Colors/Output), placeholder
(Canvas/Colors/Text), text-to-png (Text/Background/Padding);
- text: add-text, date-stamp (Text/Placement/Plate);
- filters: randomize-pixels (Blocks), add-noise (Noise/Seed). Инструменты с
1–3 простыми полями остались без layout (одна общая группа).
### Фаза 5 — изоляция старого UI
34. Поглощение `tool-views.ts` (preview/lede/layout → meta инструмента) ✔.
Отдельного `tool-views.ts` в репо нет: preview сразу строился на
`registry-new`. Meta инструмента живёт в `ToolEntry` (`title`,
`description`, `category`, `run`/`generate`), layout — в `schema.layout`
(шаг 32), рендер — `SchemaToolView`/`SchemaFields`/`SchemaPreview`.
Дублирующей системы нет.
35. Старый UI **не удаляется**, а выносится в `(old)/`-маршруты и остаётся там
временно (посмотреть, как работает, сравнить с новым; старые тесты
продолжают проходить):
- маршруты `(old)/{+page,demo,list-tools,tools/[id]}` — старый дизайн, тянет
`old.css` (не `design2.css`), старый header/footer;
- старый `registry.ts`/`registry/` + `ParamDef[]` обслуживают только
`(old)/`-инструменты — не удаляются, не рефакторятся;
- удаление происходит позже, отдельным решением (когда новый UI покроет все
инструменты и ревью завершено).
36. **Линтер-изоляция веток** (гарантия, что old и preview не смешиваются) ✔.
Кастомный ESLint-плагин `web/eslint-plugins/isolation/no-mixed-imports`
Резолвит каждый импорт (и `$lib/...`, и относительные `./`/`../`) до
реального файла, классифицирует источник и цель по фактическому пути и
ругается на old→new и new→old. Конфигурация (`old`/`new` glob-паттерны,
`root`, `alias`) вынесена в настройки правила — единая точка правды:
- старое: `routes/(old)/**`, `lib/registry.ts`, `lib/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**` (кроме `kit/`);
- новое: `routes/preview/**`, `lib/registry-new/**`, `lib/preview/**`,
`lib/registry-schema.ts`, `lib/registry-schema.test.ts`,
`lib/components/kit/**`;
- общее (разрешено обоим): всё прочее — `core/`, `i18n/`, `theme`,
`assets/`, корневой `lib` (`index.ts`, тесты). Достигнутая полная изоляция
(одиночные пересечения устранены):
- старые пилоты `registry/geometry.ts` (`add-border`) и `registry/alpha.ts`
(`add-stroke`) получали `schema` из нового `registry-schema` — убрано; оба
инструмента работают в старом UI через `params: ParamDef[]`, в preview —
через свои schema-версии в `registry-new/`;
- `registry.ts` больше не импортирует `ToolSchema` из `registry-schema`;
- preview `list-tools` тянул `TOOL_ICONS` из старого `lib/tools/tool-icons`
→ создана копия `lib/preview/tool-icons.ts` (правило копий).
37. Перенос всех старых компонентов/библиотек в папки `old/` (**следующее**):
переезд не трогает плагин — достаточно дописать один glob-паттерн в
настройку правила (например `lib/old/**`, `lib/components/old/**`), а сама
проверка работает по фактическим путям автоматически.
### Как ревьюить каждый шаг
- Diff **только один инструмент** (или фундамент без инструментов).
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` — **не изменены**
(кроме добаление `schema`, если это пилот).
- Старый UI и старые тесты продолжают работать — запустить `pnpm --dir web test`
и `pnpm --dir web exec svelte-check`.
- Порядок полей в `interface Params` и в `schema` совпадает; дефолты в сeme
равны старым `default` из `params`.
- Пилот из Фазы 1 проверяется **руками в preview**: применяется рамка,
дефолты/валидация/смена значений работают end-to-end.
## Поток данных (runtime)
### Старый pipeline (не трогаем)
Всё как сейчас: старое `defaultParams`/`sanitizeParams`/executor/chain работают
на `ParamDef[]` и `Record<string, unknown>`. Никаких изменений.
### Новый pipeline (строим рядом)
- **Хранилище значений** — типизировано на схему; у нового UI значения полей
собираются в `Params` по `ToolSchema<P>`.
- **defaultParams / sanitizeParams (новые)** — выводятся из `ToolSchema<P>`,
дефолты лежат в схеме. Старые функции не трогаем, добавляем новые рядом.
- **Executor/worker (новые)** — получает `Record<string, unknown>`, собирает
`Params` по схеме, вызывает `run`. Остаётся serializable.
- **Pipeline/chain (новый)** — шаги хранят значения, типизируются по схеме.
## Изменения в UI-компонентах
- **Старый UI** (`ParamForm.svelte`, группа `(old)/`) — **не трогаем.** Работает
на `ParamDef[]` как раньше.
- **Новый UI** (kit/`SchemaToolView` + `SchemaFields` + `SchemaPreview`, читает
`ToolSchema<P>`) — сейчас рендерит поля по схеме (number/slider/color).
- `tool-views.ts` отсутствует — preview/lede/layout уже живут в meta инструмента
(`ToolEntry` + `schema.layout`), дублирования нет.
## Оценка трудозатрат
| Часть | Сложность | Время |
| ----------------------------------------------------- | ------------------------ | ---------------------- |
| Typed field builders + toolSchema\<P\> | Средняя | ~2ч |
| Новые defaultParams/sanitizeParams под схему | Средняя | ~2-3ч |
| ToolEntry\<P\> generic + поле schema (рядом с params) | Низкая-Средняя | ~1-2ч |
| Новый executor/worker/pipeline под схему | Средняя | ~2-3ч |
| Миграция инструментов на interface Params + схемы | Механическая, но крупная | ~8-12ч |
| Новый рендер (kit/ParamControl) + составные виджеты | Средняя | ~4-5ч |
| Поглощение tool-views.ts | Средняя | ~1-2ч |
| Вынос старого UI в (old)/ + адаптация маршрутов | Средняя-Низкая | ~2-3ч |
| **Итого** | | **~22-32ч** (поэтапно) |
> Оценка выросла по сравнению с ранней версией плана, потому что принят путь
> «явный interface Params + схема + общий рендер с пер-инструмент layout» — это
> полный рефакторинг pipeline, а не только добавление составных типов.
>
> Старый UI/`ParamDef[]`/старый pipeline в смету **не входят** — они не
> рефакторятся, а продолжают работать на `(old)/`-маршрутах до перехода (затем
> убираются отдельным решением — см. шаг 35).
## Порядок реализации (кратко)
Детальный пошаговый план с атомарными изменениями и порядком ревью — в разделе
«Пошаговый план перевода инструментов» выше. Здесь краткая сводка фаз:
1. **Фаза 0** — фундамент: builders + `toolSchema<P>` + `ToolEntry<P>.schema` +
новые default/sanitize. Ни одного инструмента не изменено. ✔
2. **Фаза 1** — **один полноценный рабочий инструмент в preview** («добавить
рамку»): schema + рендер + end-to-end применение. Ручная оценка
работоспособности решения/сложностей.
3. **Фаза 2** — простые/одиночные инструменты (проверенный паттерн Фазы 1), 1
инструмент = 1 диф.
4. **Фаза 3** — инструменты с составными типами (dimension → color-pair → offset
→ position9 → font-style → plate → gradient), пилот каждого типа отдельно.
5. **Фаза 4** — масштаб UI на остальные: составные виджеты, пер-инструмент
layout (для каждого инструмента — UI-макет).
6. **Фаза 5** — изоляция: старый UI на `(old)/`-маршруты (не удаляется, остаётся
для ревью); preview на `registry-new` полностью; линтер-изоляция веток
(плагин `isolation`), затем перенос старых компонентов/библиотек в папки
`old/`.
## Зависимости
- **UI сперва доделать** — текущая активная работа; типизация params ожидает
готовности нового дизайна.
- Ветка redesign (`plan-redesign.md`) — параллельная, учесть при миграции
kit/`ParamControl.svelte` и `overlay-store`.
- Старый pipeline остаётся рабочим и шippable на всём протяжении перехода
(аналогично стратегии `plan-redesign.md`).
-139
View File
@@ -1,139 +0,0 @@
# План: доводка дизайна до верности (фикс по refs)
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Актуальный дизайн (`preview/*`) ушёл вперёд
> от референсов `refs-html/` — сверка с ними больше не информативна (новые
> компоненты `SchemaToolView`/`SchemaPreview`/каталог не имеют рефовой копии).
> Аудит против рефа прекращается; качество дизайна дальше гоняется ревью руками
> и специализированными проверками (`lint:all` —
> stylelint/design-tokens/токен-аудит). Порядок удаления старого и перехода — по
> `plan-redesign.md` (C17–C21).
> Статус: к выполнению. Ветка `feat/redesign`. Источники:
> `docs/archive/demo-fidelity.md` (архивная копия старого отчёта по верности,
> неактуальна), `docs/plan-redesign.md` (общая стратегия, фазы 1–4 выполнены до
> C16). Актуальный аудит верности — `pnpm refs-cdp-audit` /
> `pnpm refs-css-audit` / `pnpm refs-dom-audit` → `web/audit/audit-report.md`
> (текущий список скриптов — в `web/package.json`, см. `refs-*audit`).
## Контекст
- Новый дизайн собран параллельно под `preview/*` (см. `plan-redesign.md`),
старый сайт `(old)/` не трогаем до C17.
- `refs/` и `refs-html/` приведены к консистентному виду — текущая «правда по
пикселям». Механизм синхронизации ref→код не создаётся (см. §9 plan-redesign).
- Аудит верности автоматизирован: `pnpm refs-cdp-audit` (1440×900,
`scripts/audit-cdp.mjs`) пишет `web/audit/audit-report.md` — light/dark
токены + поэлементный дифф стилей; пиксель-дифф не используется. Палитры
токенов уже сверены с рефом (light совпадает, dark собран). Темы в `refs-html`
переключаются — скрипт инжектится в `scripts/extract-static.mjs`.
- Исходный отчёт `demo-fidelity.md` перенесён в `docs/archive/` как утративший
актуальность; его данные актуализированы ниже (Этап 1).
- Зафиксированные решения (подтверждены автором):
- **F1 — плоский фон холста**, без blueprint-сетки (как в рефе).
- **F4 — preview-stack в одну колонку** (как в рефе).
## Принципы
- Никаких правок старого дизайна `(old)/` и старого `ui/`.
- Компоненты пишем в `kit/` (переименование в `ui/` — на C19 по plan-redesign).
- Коммиты атомарные (< ~500 строк), по 1–2 компонента; после каждого —
`svelte-check` + `lint` + `build` + `refs-cdp-audit` (diff% не должен расти
без причины).
- Коммиты делает автор (агент не коммитит).
## Этап 0 — Рефакторинг (SRP / дизайн-обёртки), без изменения визуала
Гигантские страницы (`demo` ~513, `tools/[id]` ~451 строк) дробим на компоненты
единой ответственности; убираем дублирующиеся inline-стили.
Новые обёртки в `src/lib/components/kit/`:
- `FieldGrid` — сетка полей (заменяет `.controls` / `.controls.compact`).
- `ToggleRow` — лейбл + `Toggle` (дублировался в `demo` и `tools/[id]`).
- `PreviewStack` — контейнер превью-плиток, **одна колонка** (реализует F4 в
одном месте).
- `SectionLabel` — label + strong + actions (вместо `.pipeline-head` /
`.preview-top`).
- `FileChip` — пилюля файла.
- `PipelineFooter` — reset + auto-note.
- `WorkspaceLayout` — двухпанельная сетка (settings | preview) со sticky +
брейкпоинт 800px.
Декомпозиция страниц:
- `tools/[id]`: вынести `ParamControl` (диспетчер полей по `ParamDef`),
`SettingsPanel` (левая панель), `PreviewPanel` (правая панель) — колоцированно
в `routes/preview/tools/[id]/`. Страница сжимается до состояния + глёв.
- `demo`: переписать на те же обёртки + `WorkspaceHeader` (eyebrow + h1 + lede +
file-chip).
Побочный баг, устраняемый тут же: в `demo` поля сейчас обёрнуты в
`<div class="control-block"><MonoLabel>…</MonoLabel><SliderField label>` —
**двойной лейбл**, т.к. `ColorField`/`SliderField`/`SelectField`/`NumberField`
уже содержат `Field` (MonoLabel) изнутри. При рефакторе убираем внешний
`MonoLabel`, оставляем само поле.
Критерий Этапа 0: визуал идентичен (audit diff% без регрессии), старые тесты
зелёные.
## Этап 1 — Верность (по refs-html; статус на момент плана)
Аудит: `pnpm refs-cdp-audit` (1440×900) пишет `web/audit/audit-report.md` —
light/dark токены + поэлементный дифф стилей. Пиксель-дифф не используется.
Палитры токенов уже сверены с рефом: light совпадает (`--background`, `--panel`,
`--foreground`, `--muted`, `--line`, `--blue`), dark собран (16 токенов). Темы в
`refs-html` переключаются (скрипт инжектится в `extract-static.mjs`).
Открытые / решённые пункты (исходник — `docs/archive/demo-fidelity.md`):
- **B1** (открыт, P1): `button, input, select, textarea` рендерятся Arial —
добавить в reset `design2.css` `font-family: inherit`. Влияет на все страницы.
- **B2** (решён — проверить): двойного топбара нет. `preview/+layout.svelte`
рендерит ровно один `TopBar`; `demo` свой `.topbar` не рендерит. Подтвердить
аудитом/визуально при Этапе 1.
- **F1** (решено — плоский фон): наш `--background` уже `#EEF1F4` (как реф);
видимое отличие было в blueprint-сетке поверх фона. Фикс: убрать
`BlueprintGrid` из `AppShell`.
- **F2** (сделано C16): тип шага (`StepCard`) — приглушённый mono-текст, не
синий бейдж; индекс шага — синий `#1769D2`.
- **F3** (сделано C16): подпись превью-тайла (`PreviewTile`) — синяя `#1769D2`.
- **F4** (решено — одна колонка): реализуется в `PreviewStack` (Этап 0), колонка
= `1fr`.
- **F5** (открыт): вес `MonoLabel` 500 → 400 (как в рефе).
- **F6** (открыт): мета-подписи (MetaRow/MetaList, DIMENSIONS/FORMAT/SIZE) fs
10px → 9px.
- **F7** (низкий): копирайт/стиль — у нас `+ Add tool` (реф `Add tool`); кнопка
`Download result` — сверить с рефом (синяя, ~478×42). Проверить визуально
(замеры NOT FOUND — артефакт вложенного чипа размера).
- **F8** (низкий): `h1` line-height чуть выше рефа (150px vs 115px).
- **F9** (открыт): вертикальный ритм — подровнять паддинги секций под реф
(pipeline-head и пр.).
- **F10** (открыт): внутренние отступы preview-панели (паддинг панели).
После Этапа 1 — повторный `refs-cdp-audit`, зафиксировать остаточный diff
(sub-pixel / структурный) и обновить статус пунктов выше прямо в этом файле.
## Этап 2 — plan-redesign.md (C17–C21), ОТЛОЖЕНО
Выполняется **только после** того, как новый дизайн доведён до приемлемого
уровня верности (Этапы 0–1). Сами пункты — в `plan-redesign.md` §10 Шаг 5–6:
- **C17**: `preview/*` → реальные маршруты, удалить `(old)/`, корневой
`+layout.svelte` ← `design2.css`, поправить `newCode` в `eslint.config.js`
(`src/routes/preview/**` → `src/routes/**`).
- **C18**: правки импортов/редиректов, проверка билда.
- **C19**: удалить старое `ui/` + `app.css`, `kit/` → `ui/` (или оставить),
раскрыть ESLint `recommended` на весь код.
- **C20**: брейкпоинты 1200/1100/800/480; build без предупреждений; `grep` по
старым токенам/классам пуст.
- **C21**: опц. удалить `refs/` и `refs-html/` из репо.
## Порядок коммитов (Этап 0)
1. `kit/`: `FieldGrid`, `ToggleRow`, `PreviewStack`, `SectionLabel`, `FileChip`,
`PipelineFooter`, `WorkspaceLayout` — по 1–2 на коммит.
2. `tools/[id]`: `ParamControl` → `SettingsPanel` + `PreviewPanel` → тонкая
страница.
3. `demo`: переписать на обёртки.
После каждого коммита: `svelte-check` + `lint` + `build` + `refs-cdp-audit`.
-99
View File
@@ -1,99 +0,0 @@
# План: вторая волна EASY-инструментов
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Принципы отбора
- Максимум переиспользования: готовые контракты записей (`run`, `preview`,
формат вывода, info-режим) и существующие операции ядра.
- Новые механизмы ядра — минимальные, чистые, покрытые тестами до подключения
инструментов.
- Canvas/DOM-зависимые утилиты (SVG-вход, надписи и водяные знаки, эффекты через
фильтры контекста) — вне этого пакета, отдельная волна.
- Целевой объём волны: около 37 новых инструментов, каталог вырастает с 11 до
~48.
## 2. Новые механизмы
- **Генераторы без входного файла**: у записи появляется признак «источник не
нужен»; страница такого инструмента не показывает левую панель и зону
перетаскивания — только параметры и результат.
- **Текстовый ввод и вывод**: часть утилит принимает вставленный текст вместо
файла и выдаёт текстовый результат (панель с полем только для чтения, кнопки
«копировать» и «скачать как текст»). Распространяется на base64, data-uri,
hex.
- **BMP-энкодер в ядре**: 24-битный, чистая функция из пикселей в байты, с
тестами на маленьких матрицах (заголовок, порядок каналов BGR, паддинг
строки).
## 3. Состав пакета
**Конвертация — 6** `jpg-to-png`, `webp-to-png`, `gif-to-png` (первый кадр),
`bmp-to-png`, `ico-to-png` — всё через уже умеющий декодировать ввод, сохранение
в PNG; плюс `png-to-bmp` на новом энкодере.
**Текстовые представления — 6** `png-to-base64`, `base64-to-png`,
`png-to-data-uri`, `data-uri-to-png`, `png-to-hex`, `hex-to-png`.
**Цвет — 9** `change-opacity` (множитель прозрачности), `sepia`, `change-hue`
(сдвиг тона), `extract-channel` (выбор R/G/B в оттенках серого), `swap-channels`
(перестановка парой), `threshold-black-white` (порог яркости), `posterize`
(число уровней), `two-colors` (два цвета + порог), `invert-alpha`.
**Прозрачность и холст — 7** `remove-alpha-channel` (подложка белым),
`set-alpha-channel` (константная прозрачность), `extract-alpha-mask` (альфа в
чёрно-белую маску), `add-padding` (отступ + цвет/прозрачность), `add-border`
(толщина + цвет), `round-corners` (радиус), `fit-on-background` (вписать на
полотно цвета с полями).
**Композиция — 2** `tile` (повтор по сетке), `center-by-alpha` (центрирование по
непрозрачному содержимому).
**Генераторы — 4** `create-empty` (размер + цвет или прозрачность),
`single-color`, `random-noise` (с фиксированным зерном для воспроизводимости),
`linear-gradient` (два цвета, направление).
**Анализ — 3** `is-grayscale`, `is-transparent`, `orientation`
(портрет/ландшафт/квадрат) — текстовые сводки в info-режиме.
## 4. Новые операции ядра (все — чистые функции с тестами)
- Холст: расширение полотна (общая база для padding/border/fit), тайлинг, центр
по bounding-box альфы, скругление углов маской радиуса.
- Цвет: сепия, сдвиг тона через HSL, множитель альфы, извлечение/перестановка
каналов, пороги (яркость и двухцветный), постеризация уровней, инверсия альфы.
- Генерация: заливка цветом, шум с зерном, линейный градиент.
- Форматы: BMP-энкодер; кодеки текстовых представлений (base64/data-uri/hex в
обе стороны).
- Анализ: проверка монохромности, полной непрозрачности, ориентации.
## 5. Этапы и проверки
- **A. Механизмы** — генераторный режим страницы, текстовый ввод/вывод,
BMP-энкодер. Проверка: `test/check/build`.
- **B. Конвертация и текстовые представления** — 12 инструментов. Проверка:
ручной прогон каждой конвертации туда-обратно.
- **C. Цвет** — 9 инструментов. Проверка: юнит-тесты операций + визуально.
- **D. Прозрачность, холст, композиция** — 9 инструментов. Проверка:
юнит-тесты + визуально.
- **E. Генераторы и анализ** — 7 инструментов. Проверка: детерминизм шума (одно
зерно — одно изображение), своды анализа.
- **F. Итог** — полный прогон, смоук всех новых инструментов «настроить →
скачать», регресс старых одиннадцати.
## 6. Критерии готовности
- Каталог содержит ~48 рабочих инструментов, каждый доступен страницей с формы
из реестра.
- Все новые операции ядра покрыты тестами; обратимые конвертации проверены
круговым прогоном (например PNG → base64 → PNG даёт те же пиксели).
- Генераторы работают без загрузки файла; текстовые представления копируются и
скачиваются.
- `test/check/build` зелёные.
## 7. Что сознательно не делаем
- SVG-вход, рисование текста и водяных знаков, эффекты через фильтры контекста,
GIF/APNG-энкодинг, пакетная обработка — последующие волны.
- Семейства logo/icon/signature/stamp как отдельные страницы — они будут
конфигурациями тех же операций после этой волны.
-87
View File
@@ -1,87 +0,0 @@
# План: третья волна EASY — трансформации, свет, SVG
> **СТАТУС: ВЫПОЛНЕН 24.08.2026.**
## 1. Что получается
Одиннадцать новых инструментов: свободные трансформации формы (наклон, поворот
на произвольный угол, зум, сдвиг), светокоррекция (гамма, автоконтраст,
температура, тонирование), виньетка, эффект «артефактов сжатия» и вход SVG.
Каталог вырастает с 58 до ~69.
Текстовые инструменты и водяные знаки в волну не входят — им нужен рендер текста
на канвасе, это отдельная четвёртая волна.
## 2. Новое ядро
- **Аффинные трансформации** (`core/affine.ts`): единая функция обратного
отображения — для каждого пикселя результата вычисляется исходная координата
матрицей и семплируется билинейно (общий семплер, вынесенный из `resize`).
Поверх неё: наклон по осям, поворот на произвольный угол (с расширением холста
под новые габариты), зум к центру, сдвиг.
- **Свет и цвет** (`core/color.ts` дополнения): гамма-коррекция через LUT,
автоконтраст (растяжка по гистограмме яркости с отсечкой хвостов), температура
(множители тёплых/холодных каналов), тонирование (умножение RGB на цвет с
сохранением яркостного баланса).
- **Виньетка** (`core/effects.ts`): радиальное затемнение от центра, сила
слайдером; чистая математика без canvas.
- **IO-расширения** (`core/io.ts`): декодирование SVG из текста/файла через
blob-URL и `Image`; раундтрип «закодировать JPEG с качеством Q → декодировать
обратно» для эффекта артефактов сжатия.
## 3. Инструменты — 11
- **Геометрия:** `skew-png` (углы X/Y), `rotate-free-png` (любой угол, холст
расширяется), `zoom-png` (масштаб ≥100 % к центру), `shift-png` (сдвиг со
смещением содержимого).
- **Фильтры:** `vignette-png` (сила затемнения), `jpeg-artifacts-png` (имитация
пережатия).
- **Цвет:** `gamma-png` (0.1–3.0), `auto-contrast-png` (без параметров),
`temperature-png` (−100 тёплый…+100 холодный или наоборот — подпись честная),
`tint-png` (цвет + сила).
- **Конвертация:** `svg-to-png` (текст SVG слева, как base64-режим).
## 4. Этапы и проверки
- **A. Ядро** — семплер, аффинный модуль, свет/цвет, эффекты + точные тесты:
поворот квадрата на 90° совпадает с существующим `rotate90`, зум ×2 удваивает
размеры и центр остаётся центром, гамма 1 — идентичность, автоконтраст
растягивает известный диапазон, виньетка симметрична и не трогает центр при
нулевой силе. Проверка: `pnpm test/check`.
- **B. IO** — SVG-декод и JPEG-раундтрип. Проверка: сборка; живой прогон на
этапе D.
- **C. Реестр** — одиннадцать записей, иконки, тесты целостности. Проверка:
`test/build`, страницы в экспорте.
- **D. Итог** — полный прогон, смоук §7.
## 5. Смоук-чеклист
1. Наклон X/Y по отдельности и вместе — форма искажается предсказуемо, без
чёрных полос при малых углах.
2. Поворот на 30° — холст вырос, углы прозрачны, содержимое повернуто.
3. Зум 200 % — содержимое увеличено к центру, холст прежнего размера, края
обрезаны.
4. Сдвиг уводит содержимое за край, противоположный край заполняется фоном.
5. Гамма 0.5 темнее, 2.0 светлее, 1.0 — без изменений.
6. Автоконтраст на тусклом фото вытягивает диапазон.
7. Температура ±50 заметно теплеет/холодеет.
8. Тонирование красным даёт равномерный оттенок, белые области окрашиваются
сильнее всего.
9. Виньетка затемняет только края, центр не трогает.
10. Артефакты: качество 10 даёт видимые квадраты JPEG.
11. SVG вставленный слева декодируется в результат справа; битый SVG — баннер.
12. Все новые инструменты работают в цепочке и скачиваются из своих секций.
## 6. Критерии готовности
- Одиннадцать инструментов работают и покрыты юнит-тестами ядра (кроме
IO-дом-части).
- Аффинные операции опираются на общий билинейный семплер; дублирования
сэмплинга нет.
- Каталог ~69 инструментов; `test/check/build` зелёные, смоук §5 пройден.
## 7. Что сознательно не делаем
- Рендер текста, водяные знаки, штампы — четвёртая волна (DOM-текст).
- Субпиксельная точность аффинных трансформаций сверх билинейного семплинга.
- Пакетное применение и пресеты.
-97
View File
@@ -1,97 +0,0 @@
# План: закрытие EASY/MEDIUM-пробелов из сравнения с onlinepngtools
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Все волны W1–W9 закрыты. Идеи из
> раздела «Вне очереди» (region-инструменты, мультифайловый вывод, анимационные,
> HARD-серия) перенесены в `backlog.md`.
> Статус: черновик на ревью
## Принципы
- Волны собираются вокруг общего математического ядра: одна волна = один
движок + N инструментов на нём.
- Каждая волна оформляется отдельным планом при старте; здесь — очередь и
состав.
- Порядок: сначала кластеры, дающие много инструментов малой кровью, затем
одиночные эффекты, MEDIUM-пакеты в конце.
- Полный список параметров каждого инструмента живёт в карте инструментов
(tools-map.md); здесь только состав и ядро.
## Очередь волн
### W1. Палитры — ВЫПОЛНЕНА (11 инструментов)
Ядро: RGB↔HSL + гармонии цветового круга. Вывод: свотч-полотно (генератор).
Состав: color-wheel, complementary, monochromatic, analogous, triadic, tetradic,
similar-shades, sort-colors. Плюс утилиты смешения тем же ядром: mix-colors,
average-color, blend-two, step-between (+4, итого 12).
### W2. Каналы и пространства — ВЫПОЛНЕНА (6 инструментов)
Ядро: матрицы преобразования RGB→(HSL/HSV/HSI/CMYK/YCbCr/LAB) + визуализация
выбранного компонента серым или окрашенно. Общий select «компонент» + select
«режим отображения».
### W3. Маски по свойствам пикселей — ВЫПОЛНЕНА (7 инструментов)
Ядро: предикат над пикселем → бинарная маска (с инверсией и подсветкой цветом).
Состав: show-transparent, show-grayscale, show-color, light-mask, dark-mask,
unique-color-mask, extract-by-color.
### W4. Фигурные маски — ВЫПОЛНЕНА (4 инструмента)
Ядро: SDF фигуры (круг/квадрат/звезда/волна) → альфа-маска с fit-режимами.
Состав: circle-mask, square-mask, star-mask, wavy-mask.
### W5. Геометрия-добивки — ВЫПОЛНЕНА (5 инструментов)
Ядро: bbox по альфе (trim) переиспользуется тремя инструментами источника.
Состав: trim-empty-space (закрывает remove border/space), change-canvas-size,
change-aspect-ratio, landscape↔portrait, symmetric-copy.
### W6. Эффекты лёгкие — ВЫПОЛНЕНА (6 инструментов; color-blocks покрыт pixelate)
Состав: pixelate, color-blocks, randomize-pixels (seed), add-noise,
feather-edges, clean-edges, silhouette. Shadow/glow — сюда же, если потянет
этап: оба = размытая альфа + смещение + цвет (ядро blur уже есть).
### W7. Конвертеры и генераторы — ВЫПОЛНЕНА (11 инструментов)
PNG↔bytes, PNG↔rgb-values (текстовые результаты), verify-is-png (анализ
сигнатуры), text-to-png (domText без входной картинки), emoji-to-png,
placeholder-png, color-spectrum, colorful-random (seed), draw-grid.
multi-color-gradient — если успеем новый тип параметра «список цветов», иначе
перенос.
### W8. Цветовые MEDIUM — ВЫПОЛНЕНА (4 инструмента; median-cut + Floyd–Steinberg/Bayer, custom-palette через text-параметр)
Ядро: квантование (median-cut или k-means). Состав: quantize (k),
decrease-color-count (=quantize с пресетами), custom-palette (маппинг на список
цветов — нужен тип параметра «список»), dithering (Floyd–Steinberg/Bayer поверх
квантования).
### W9. Сжатие — ВЫПОЛНЕНА (3 инструмента; optimize/change-quality осознанно не выделены — покрыты пресетами compress и W8)
Без wasm честное управление размером PNG ограничено: реальный рычаг —
квантование (W8) + обрезка метаданных + итеративный подбор под целевой KB.
Состав: compress (пресеты усилий), reduce-to-size (целевой KB, бинарный поиск по
k), optimize (re-encode), change-quality/low-quality — переосмыслить как пресеты
W8; jpeg-artifacts уже покрывает «испортить».
## Вне очереди
- Region-инструменты
(censor/erase/pixelate-area/blur-area/sharpen-area/reverse-area) — ждут UI
выделения области на превью; отдельное решение.
- Мультифайловый вывод (split-parts, gif-frames, separate-colors) — ждут
механизм «результат = набор файлов».
- Анимационные (slow-reveal/fade/scrolling) — выход не PNG; отдельное решение о
формате.
- HARD: glitch, barcode, signature-extract, handwritten-digital.
- Нишевые серии logo/icon/stamp/signature — не копируем.
## Оценка масштаба
W1–W7 дают **~47 новых инструментов**, почти все EASY. W8 добавляет ядро
квантования (открывает custom-palette/dithering и улучшает W9). Суммарно каталог
достигает ~120+ при текущих 71.
-83
View File
@@ -1,83 +0,0 @@
# План: герой-поиск v2 — иконки, популярность, карточки
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Что получается
Выдача поиска на главной превращается из списка строк в сетку карточек: слева
иконка инструмента, справа название и описание. Инструменты получают два новых
поля реестра — `popularity` (приоритет в выдаче) и `icon` (идентификатор
иконки). Иконки переиспользуются в каталоге `/list-tools` и в легендах шагов
цепочки. Поиск работает по-прежнему: нечёткий скоринг, клавиатура ↑↓ Enter Esc.
## 2. Данные реестра
- **`popularity?: number`** (0–100, по умолчанию 50) — приоритет внутри равного
скоринга запроса. Значения проставляются вручную осознанно: базовые операции
(обрезка, размер, удаление фона, конвертации JPG/PNG) — 80–95; цветовые и
геометрия — 40–70; узкие и генераторы — 10–30.
- **`icon?: string`** — имя иконки из набора Lucide. Явный словарь-маппинг для
всех инструментов в одном месте; отсутствие записи даёт нейтральную заглушку
(Wrench).
- Тест целостности расширяется: популярность в диапазоне, иконка — непустая
строка из разрешённого набора.
## 3. Иконки
Зависимость `lucide-svelte` (MIT) — один пакет, иконки рендерятся компонентами,
дерево не раздувается за счёт выборочного импорта.
Маппинг ведётся одним объектом `tool-icons.ts` рядом с реестром: 51 запись вида
`'crop-png': Crop`. Подбор по смыслу: обрезка — Crop, размер — Scaling, поворот
— RotateCw, отражение — FlipHorizontal2, оттенок — Palette, размытие — Droplets,
резкость — Focus, удаление фона — Scissors, анализ — Info и так далее; для
семейств конвертаций — FileInput/FileOutput варианты.
Иконка появляется в трёх местах: карточка выдачи, карточка каталога
`/list-tools`, легенда шага цепочки («Шаг N: [icon] Название»).
## 4. Карточки выдачи
- Сетка: одна колонка на узком экране, две — начиная со среднего героя.
- Композиция карточки: квадрат иконки слева, справа название (полужирное) и
однострочное описание с обрезкой.
- Состояния hover и активной с клавиатуры — тот же акцент подсветки, что сейчас.
- Сортировка выдачи: скоринг запроса ↓, затем популярность ↓, затем название.
## 5. Этапы и проверки
- **A. Реестр** — поля `popularity` и `icon`, словарь иконок на все 51,
расширение тестов целостности. Проверка: `pnpm test/check`.
- **B. Карточки поиска** — сетка выдачи с иконками и учётом популярности,
клавиатура без изменений. Проверка: визуально, `check/build`.
- **C. Распространение** — иконки в каталоге `/list-tools` и легендах звеньев.
Проверка: визуально, `build`.
- **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист
1. Пустой запрос на герое показывает популярные первыми после первого символа.
2. Запрос «обрез» поднимает карточку обрезки выше прочих совпадений.
3. У каждой карточки есть иконка; у большинства — разные, соответствующие
смыслу.
4. Наведение и стрелки подсвечивают карточку; Enter открывает инструмент.
5. Открытый инструмент работает как раньше: параметры в колонках, результат,
скачивание.
6. Цепочка: легенда каждого шага показывает иконку выбранного инструмента.
7. `/list-tools`: карточки категорий с иконками, ссылки работают.
8. Регресс: старые сценарии (загрузка файла, цепочка, скачивание) не изменились.
## 7. Критерии готовности
- Все инструменты имеют иконку и ненулевую популярность; тесты это гарантируют.
- Выдача поиска — сетка карточек с корректной сортировкой и живой клавиатурой.
- Иконки присутствуют в поиске, каталоге и легендах шагов.
- `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем
- Автоматический расчёт популярности из статистики использования — значения
ручные.
- Собственный набор SVG-иконок — берём готовый открытый.
- Переводы названий и поиск на других языках — отдельная i18n-волна.
- Изображения-превью в карточках поиска — только иконка и текст.
-111
View File
@@ -1,111 +0,0 @@
# План: i18n — русский и английский
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.** Базовая локаль — английский (реестр), русский
> — перевод в словаре; строки инструментов в реестре переведены на английский в
> рамках этапа C.
## 1. Что получается
Сайт работает на двух языках: русский (базовый) и английский. Переключатель в
шапке, выбор сохраняется в localStorage, тег `html lang` обновляется на клиенте.
Переведено всё: каркас интерфейса, карточки инструментов, цепочки, поиск, тексты
ошибок. Поиск находит инструменты по запросу на любом из двух языков независимо
от активной локали.
Каталог и поведение не меняются: ни новых инструментов, ни изменений логики
существующих.
## 2. Решения
- Без сегментов языка в URL. Статика пререндерится на русском; переключение на
клиенте, выбор живёт в localStorage. hreflang/canonical и вторая копия страниц
для поисковиков — не в этой волне.
- Ядро не знает о локалях. Ошибки в core выбрасываются стабильными кодами
(например ERR_BAD_HEX); перевод кода в человекочитаемый текст происходит в
слое исполнителя/UI.
- Английский — базовая локаль и фолбэк: строки инструментов в реестре записаны
по-английски. Переводы живут в секции tools словарей по ключу id инструмента
(title, description, подписи параметров, подписи опций select,
тексты-результаты анализаторов); сейчас перевод — русский в ru.ts. Доступ из
компонентов только через хелперы
toolTitle/toolDescription/paramLabel/optionLabel. Так каждая строка имеет
единственный источник: en — реестр, переводы — словари.
## 3. Структура i18n-модуля
- web/src/lib/i18n/locale.svelte.ts — состояние активной локали (runes), персист
в localStorage (ключ locale, guard на SSR), обновление
document.documentElement.lang.
- web/src/lib/i18n/t.ts — функция перевода с фолбэком на базовый язык и
интерполяцией переменных вида {n}.
- web/src/lib/i18n/ru.ts и en.ts — словари одного типа Dict; тип экспортируется
из ru.ts, en.ts обязан ему соответствовать — пропущенные ключи ловятся на
компиляции.
- Секции словаря: header, home, catalog, toolPage, chain, sourceCard,
resultCard, paramsCard, textInput, textResult, download, infoPanel, dropZone,
search, ui (aria кнопок слайдера и пипетки), categories, errors, tools.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- A. Инфраструктура — модуль состояния, функция перевода с фолбэком и
интерполяцией, словари-скелеты со всеми секциями (заполнены header и
categories как пилот), переключатель RU/EN в шапке, реактивный lang. Тесты:
фолбэк на ru при отсутствии ключа в en, интерполяция, персист локали.
- B. Каркас интерфейса — перенос всех хардкод-строк компонентов и маршрутов в
словари: layout (шапка, футер), главная (герой, «вернуть последний», title),
каталог, карточки источника/результата/параметров, текстовый вход и результат,
«Показать маску», DropZone/DropOverlay, кнопка скачивания (busyText),
InfoPanel плюс форматирование чисел через тег локали вместо жёсткого ru-RU,
ToolPage (легенды, «Шаг n», ошибки шага), ChainToolBlock, placeholder и пустой
результат поиска, страницы инструмента (title, 404). Обёрточные сообщения
исполнителя («Ошибка исполнения в воркере») — тоже здесь.
- C. Контент реестра — переводы 69 инструментов
(title/description/labels/options) в tools-секции словаря en; компоненты
получают строки через хелперы toolTitle/toolDescription/paramLabel/optionLabel
с фолбэком на реестр. Тест целостности: у каждого инструмента есть непустые
перевод title/description, полный набор param-ключей и подписи всех опций
select; лишние ключи словарей отсутствуют.
- D. Поиск по любому языку — нормализация запроса и полей: нижний регистр, ё→е,
снятие диакритики через NFD; скоринг против активной и базовой локали сразу,
лучший балл побеждает; сортировка совпадений через Intl.Collator активной
локали. Тесты нормализации: ё/е, диакритика, английский запрос при русской
локали и наоборот.
- E. Ошибки на кодах — около 40 мест throw в core, registry, pipeline заменяются
на коды; исполнитель переводит код в текст активной локали, неизвестные
сообщения показываются как есть. Пять тестов, матчащих русские подстроки
ошибок (executor, pipeline, registry, io, alpha), переходят на коды — это
упрощает ассерты. Русские describe/it в тестах не трогаем.
- F. Итог — полный прогон, смоук чеклист §6, архивация плана.
## 5. Смоук-чеклист
1. Переключение RU/EN в шапке мгновенно меняет язык без перезагрузки; после
перезагрузки выбор сохранён.
2. Тег html lang соответствует активной локали.
3. Главная, каталог, страница инструмента, цепочка из трёх звеньев — везде
переведено, без смеси языков.
4. Форма параметров: подписи полей и опции select переведены у произвольных пяти
инструментов из разных категорий.
5. Битый вход (некорректный HEX в тонировании, нулевой размер) показывает ошибку
на активном языке.
6. Поиск: «повер» находит повороты при русской локали; «rotate» находит их же;
при английской локали наоборот. Диакритика и ё не мешают совпадению.
7. Числа в панели информации отформатированы по локали.
8. Скачивание, пипетка, маски, слайдеры работают одинаково на обеих локалях —
функциональность не задета.
## 6. Критерии готовности
- Оба словаря типобезопасны и полны; тест целостности секции tools зелёный.
- Ни одного пользовательского русского строки вне словарей (кроме тестовых
describe/it).
- test/check/build зелёные, смоук пройден.
## 7. Что сознательно не делаем
- Сегменты языка в URL, prerender обеих локалей, hreflang/canonical.
- Транслитерация поисковых запросов ru-en.
- Третьи и дальнейшие языки (структура позволяет, добавление — отдельная волна).
- Перевод описаний коммитов, README и документации docs/.
-83
View File
@@ -1,83 +0,0 @@
# План: параметры между исходником и результатом
> **СТАТУС: УСТАРЕЛ (2026-09-07).** Раскладка описывалась для старого
> `ToolPage`; перекрыта редизайном (`plan-redesign.md`) — страницы инструментов
> теперь `SchemaToolView`/`SchemaPreview` в новом дизайне. Сама идея «инлайн:
> исходник → параметры → результат» живёт в `backlog.md` (№2).
> Статус: план к выполнению.
## 1. Что получается
Альтернативная раскладка этапа инструмента: три колонки «Исходник | Параметры |
Результат» на широких экранах, вертикальный стек на узких. Цель — отдать вариант
дизайнеру как отправную точку. Откат к текущей раскладке — замена одной строки в
`ToolPage`.
## 2. Решения
- Два взаимозаменяемых компонента этапа с одинаковым набором пропсов; `ToolPage`
рендерит один тег. Попробовать новое — поменять компонент в этом теге; вернуть
старое — поменять обратно. Никаких флагов и конфигов.
- Общая типизация пропсов в одном файле рядом с компонентами; сами карточки
(`SourceCard`, `ResultCard`, `TextInputCard`, `ParamsCard`) переиспользуются
как есть — вся логика исполнения, маски, пипетки и автозапуска остаётся в
`ToolPage` и приходит сверху.
- Chain-звенья используют ту же панельную систему «Вход | Параметры | Результат»
— общие классы `.pane` / `.pane-legend` / `.pane-params` вынесены в app.css,
вертикальные поля в узкой колонке заданы один раз глобально.
- Один порог отзывчивости: три колонки от ~75rem, ниже — стек «исходник →
параметры → результат». Промежуточные перестроения не придумываем до фидбека
дизайнера.
## 3. Компоненты
```txt
src/lib/components/stage/
stage-props.ts # интерфейс StageProps + сборщик дефолтов
ToolStageClassic.svelte # текущая разметка .tool-stage, перенесена 1:1 из ToolPage
ToolStageInline.svelte # новая сетка [source | params | result]
```
- `ToolStageClassic` — вырезка существующего шаблона со стилями; после
извлечения страница должна быть пиксель-в-пиксель прежней.
- `ToolStageInline` — grid-колонки
`minmax(0, 1fr) | фикс-ширина параметров | minmax(0, 1fr)`; легенда
«Параметры» над средней колонкой в том же стиле edge-legend.
- Модификаторы состояний через классы на корне компонента: `no-params`
(инструменты без параметров → две колонки), `no-source` (генераторы →
параметры и результат), текстовый вход до декодирования занимает левую ячейку
как сейчас.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Извлечение классики** — перенос разметки и стилей этапа из `ToolPage` в
`ToolStageClassic` без изменений поведения; пропсы собираются в одном объекте.
Критерий: страница неотличима, diff по логике `ToolPage` нулевой.
- **B. Инлайн-раскладка** — `ToolStageInline`, переключение в `ToolPage` на
новый компонент, прогон смоука §5. Ревью в браузере; решение дизайнера —
потом, код уже позволяет жить в обоих вариантах.
## 5. Смоук
1. Замена компонента этапа — одна строка; обе версии собираются без
предупреждений.
2. Инструмент без параметров (например Автоконтраст): две колонки, пустой
середины нет.
3. Генератор без исходника: параметры и результат в две колонки.
4. Текстовый инструмент до декодирования: вход слева, параметры в центре, пустое
состояние результата справа.
5. Маска, пипетка, слайдеры в средней колонке; изменение параметра перезапускает
инструмент как раньше.
6. Узкий экран: стек без горизонтального скролла, порядок исходник → параметры →
результат.
7. Легенды присутствуют у всех трёх зон в обеих раскладках.
## 6. Что сознательно не делаем
- Редизайн визуального языка (цвета, типографика, формы) — материал для
дизайнера, не для этого плана.
- Новая логика: исполнение, автосохранение, пресеты — раскладка ничего не знает
про исполнение.
-103
View File
@@ -1,103 +0,0 @@
# Plan: `/preview/list-tools` → design2 (catalog)
> **СТАТУС: ВЫПОЛНЕН (2026-09-07) — устарел.** Каталог `/preview/list-tools`
> переписан на `CatalogHeader` / `CatalogToolbar` / `CatalogGroup` / `ToolCard`
> (kit), поиск и фильтры функциональны. Доводка верности против рефа остаётся в
> рамках `plan-design-fix.md`.
Ветка `feat/redesign`. Цель — привести каталог инструментов к рефу
`refs-html/list-tools.html`. Сейчас страница вообще не на design2: по
мульти-аудиту `onlyOurs=257, onlyRef=54` (демо-страница — `18/11`).
## Текущее состояние
- `src/routes/preview/list-tools/+page.svelte` использует собственную вёрстку
(`.catalog` / `.cat-head` / `.cat-grid`) + `Panel` / `PanelHeading` /
`ToolCard` (kit). Нет поиска, фильтров, счётчика, навигация по категориям
сделана через `Panel`-блоки, а не через `.catalog-groups`.
- Реф (`list-tools.html`) строит:
- `.catalog-page` → `.catalog-head` (`.eyebrow`, `h1` «Tool catalog», `p`
lede, `.catalog-total` «NN TOOLS AVAILABLE» с большим числом),
- `.catalog-toolbar` (`.catalog-search` + `.catalog-filters` с кнопками),
- `.catalog-groups` (grid `repeat(2,minmax(0,1fr))`, gap `56px 28px`) →
`.catalog-group` (`.group-title` «CAT · NN TOOLS») → `.tool-cards` →
`.tool-card`.
- `.tool-card` (реф) — `display:grid`, колонки `42px minmax(0,1fr) 24px 18px`,
`min-height:106px`, `gap:14px`, `padding:16px`: `.tool-icon` (42×42 синий
размытый квадрат), `.tool-copy` (`strong` 13px mono + `span` 12px mono muted),
`.tool-index` (10px mono, сверху), `.tool-arrow` (появляется при hover).
- Токены `design2.css` уже совпадают с рефом (`--background #eef1f4`,
`--panel #f8fafb`, `--foreground #17212b`, `--muted #6d7883`,
`--line #cbd3da`, `--blue #1769d2`, `--radius 4px`) — править токены не нужно.
## Шаги
1. **Restyle `kit/ToolCard.svelte`** под реф (меняется «на месте» — используется
ещё в `preview/+page` и `preview/kit`, стиль унифицируется, ок):
- grid-колонки `42px minmax(0,1fr) 24px 18px`, `align-items:center`,
`gap:14px`, `min-height:106px`, `padding:16px`, `border:1px solid line`,
`background:panel`, `border-radius:radius`.
- `.tool-icon`: 42×42 `place-items:center`,
`background:color-mix(in srgb, var(--blue) 12%, transparent)`,
`color:var(--blue)` (реф использует `--panel-strong`; вместо нового токена
берём этот `color-mix`).
- `.tool-copy strong`: `600 13px var(--font-mono)`; `span`: `12px/1.5 mono`
`muted`.
- `.tool-index`: `10px mono muted`, `align-self:start`.
- `.tool-arrow`: `color:var(--blue)`, `opacity:0` → `1` +
`translate(2px,-2px)` на `.tool-card:hover`.
- DOM-порядок как в рефе: `icon, copy, index, arrow`.
- `href` через `resolve` из `$app/paths` (убрать `svelte-ignore`).
2. **`kit/CatalogHeader.svelte`**: `.eyebrow` «EASY-PNG-TOOLS / CATALOG», `h1`
«Tool catalog», lede; `.catalog-total` с динамическим `TOOLS.length` (реф
показывает `32`).
3. **`kit/CatalogToolbar.svelte`**: `.catalog-search` (иконка + `<input>`) и
`.catalog-filters` (кнопки по категориям). Состояние `query` / `category`
поднимается наружу через `bind:` (или callback-пропы), чтобы страница
фильтровала список. Фильтры функциональные (клиентский фильтр по названию +
категории), не декоративные.
4. **`kit/CatalogGroup.svelte`**: `.group-title` (uppercase label категории +
«NN TOOLS») + слот со списком карточек; сетка `.tool-cards` — `gap:10px`
(реф), на мобиле (`@media max-width:800px`) всё схлопывается в 1 колонку.
5. **Переписать `preview/list-tools/+page.svelte`** как тонкую композицию:
- обёртка `.catalog-page` (`padding:60px clamp(24px,4vw,72px) 72px`);
- `<CatalogHeader />`;
- `<CatalogToolbar bind:query bind:category />`;
- `.catalog-groups` из отфильтрованных групп (по `CATEGORIES`, пересчитав
`tools` с учётом `query`/`category`);
- внутри `<CatalogGroup>` — `<ToolCard>` на каждый tool
(`href= "/preview/tools/{id}"`, `icon` из `tool-icons`, `index` по порядку,
`title`/`description` из registry);
- убрать `Panel`/`PanelHeading` из этого маршрута.
6. **Категории/локализация**: labels привести к рефу (реф: ALL / CONVERT /
TRANSPARENCY / COLOR / GEOMETRY / FILTERS / ANALYZE). Наши `CATEGORIES`
(`convert/alpha/color/geometry/filters/text/analyze/generate`) — сопоставить
(`alpha`→TRANSPARENCY и т.п.); точное сопоставление уточнить по рефу и
`i18n/ru.ts`. Фильтры = ALL + те же label'ы. Тексты заголовков/lede — по рефу
(англ. «Tool catalog» / «Focused utilities for working with PNG…»), либо
оставить RU-вариант, если это сознательное отклонение (зафиксировать).
7. **Адаптив**: перенести правила рефа (`@media max-width:800px`) —
`.catalog-groups` → 1fr, `.tool-card` → `38px minmax(0,1fr) 18px`, скрыть
`.tool-index`/показать `.tool-arrow`, `.catalog-head` в колонку и т.д.
## Верификация
- `pnpm lint`, `pnpm exec svelte-check --tsconfig ./tsconfig.json`, `pnpm build`
— без ошибок.
- `pnpm refs-audit` (теперь мульти-маршрутный) → глянуть секцию
`/preview/list-tools`: `onlyOurs`/`onlyRef` должны упасть с `257/54` примерно
до уровня demo (`18/11`). Остаточный diff (если будет) зафиксировать в этом
файле.
## Примечания
- `ToolCard` правится на месте — влияет на `preview/+page` и `preview/kit` (там
тоже карточки каталога, унификация уместна).
- Старый дизайн (`routes/(old)/**`, `components/search/**`) не трогать.
- Коммиты делает автор (агент не коммитит).
-97
View File
@@ -1,97 +0,0 @@
# План: MEDIUM-волна №1 — удаление фона, размытие, резкость + Web Worker
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Зачем
- Первые операции настоящего алгоритмического уровня: свёртки и заливка связной
области.
- Витринная фича каталога — удаление однотонного фона без нейросетей: порог по
цвету + заливка от краёв изображения + сглаживание границы маски.
- Эти операции реально блокируют UI-поток на больших картинках — повод включить
давно подготовленный путь: исполнитель `executeStep` переезжает в Web Worker,
страницы не меняются вовсе.
## 2. Инструменты — 3
- **Удалить фон PNG** (`remove-background-png`, категория «Прозрачность») —
параметры: цвет фона, допуск похожести %, режим «только внешние области»
(заливка от краёв вместо глобального совпадения), сглаживание границы (проходы
мажоритарного фильтра маски). Даёт превью маски через существующий контракт
`preview`.
- **Размытие PNG** (`blur-png`, новая категория «Фильтры») — гауссово
приближение тремя проходами разделяемого бокса; радиус слайдером 1–32 px.
- **Резкость PNG** (`sharpen-png`, «Фильтры») — свёртка ядром резкости с
регулируемой силой (смесь исходного и обработанного), сила слайдером 0–100 %.
## 3. Новое ядро (чистые функции, детерминированные, покрытые тестами)
- `core/convolution.ts`
- универсальная свёртка с клампом краёв;
- разделяемый бокс-блюр и сборка гауссова приближения из трёх проходов;
- ядро резкости и смешивание с оригиналом по силе.
- `core/background.ts`
- построение маски удаления по цветовому расстоянию (та же метрика, что у
удаления цвета);
- заливка связной области от всех граничных пикселей (очередь, без рекурсии);
- мажоритарное сглаживание маски заданным числом проходов;
- сборка результата: альфа 0 у удалённых пикселей; плюс функция маски для
превью.
## 4. Web Worker
- Точка исполнения одна — `tools/executor.ts`; меняется только её внутренность.
- Воркер-файл импортирует реестр и исполняет операцию по `toolId`; картинка
приходит копией (структурный клон), результат возвращается передаваемым
буфером.
- Ленивая инициализация воркера при первом вызове; если воркер недоступен
(старый браузер/тестовая среда) — автоматический откат на прямое исполнение в
главном потоке. Обе ветви обязаны давать побайтово одинаковый результат.
- Генераторы и текстовые представления остаются в главном потоке — они
мгновенные.
- Пипетка и лупа работают с уже готовыми картинками и не зависят от переноса.
## 5. Этапы и проверки
- **A. Ядро** — свёртки и фон с тестами на малых матрицах (точные ожидания
руками). Проверка: `pnpm test/check`.
- **B. Реестр** — категория «Фильтры», три записи, превью маски. Проверка: тесты
целостности, визуально.
- **C. Web Worker** — перенос исполнителя, отккат-ветвь. Проверка: результаты
до/после переключения идентичны байт-в-байт на фиксированной картинке и
параметрах; `test/build` зелёные.
- **D. Итог** — полный прогон и смоук §8.
## 6. Смоук-чеклист
1. Размытие: радиус 1 и 32 — плавно усиливается, прозрачные края не темнеют.
2. Резкость: сила 0 не меняет картинку; сила 100 заметно подчёркивает края без
цветового мусора.
3. Удаление фона на логотипе с однотонным фоном: фон исчез, содержимое цело.
4. Режим «только внешние области»: дырка внутри объекта того же цвета остаётся
непрозрачной.
5. Сглаживание 0/2/4 прохода — граница от жёсткой к мягкой, результат
воспроизводим.
6. Превью маски показывает белым удаляемую область; скачивание всегда отдаёт
результат.
7. Во время размытия большой картинки интерфейс остаётся отзывчивым (чип
«Пересчёт…», скролл живой).
8. Цепочка: размытие → резкость → удаление фона работает; каждый блок скачивает
свой промежуток.
9. Регресс старых инструментов и главной-поиска без изменений поведения.
## 7. Критерии готовности
- Три новых инструмента проходят смоук §6; операции ядра покрыты точными
юнит-тестами.
- Исполнение изображений идёт через воркер там, где он доступен, с
гарантированно одинаковым результатом обеих ветвей.
- `test/check/build` зелёные.
## 8. Что сознательно не делаем
- Многопроходное гауссово ядро «по-настоящему» (O(r²)) — трёхпроходного
боксового приближения достаточно визуально и оно быстрее.
- Хромакей по нескольким цветам, ИИ-сегментация — следующие волны.
- Отмена выполняющегося прогона воркера — устаревшие результаты отбрасываются
токеном, сама работа завершается фоном.
-95
View File
@@ -1,95 +0,0 @@
# План: MEDIUM-волна №2 — морфология прозрачности
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Зачем
Одна пара функций — расширение и сужение непрозрачных областей — открывает целое
семейство пользовательских операций: обводка логотипов и иконок, контуры,
утолщение и утончение подписей, удаление мелкого мусора и закрытие дырок. Всё
это частые запросы в наборах PNG-утилит, а ядро получается компактным и легко
покрывается тестами.
## 2. Инструменты — 7 (все в категории «Прозрачность»)
- **Обвести PNG** (`add-stroke-png`) — кольцо выбранного цвета вокруг
непрозрачного содержимого; параметры: цвет, толщина 1–10 px.
- **Найти контур PNG** (`find-contour-png`) — линия по краю непрозрачных
областей; параметр: толщина линии.
- **Утолщить PNG** (`make-thicker-png`) — расширение содержимого на N пикселей.
- **Утончить PNG** (`make-thinner-png`) — сужение содержимого на N пикселей.
- **Жёсткие края PNG** (`harden-alpha-png`) — бинаризация альфы по порогу:
полупрозрачность уходит, остаётся два состояния.
- **Убрать мусор PNG** (`despeckle-alpha-png`) — открытие: удаляет одиночные
полупрозрачные пиксели и мелкие крапинки.
- **Закрыть дыры PNG** (`close-holes-png`) — закрытие: заполняет одиночные
прозрачные точки внутри объекта.
Все операции применяются к альфа-каналу; RGB пикселей сохраняется.
## 3. Ядро `core/morphology.ts`
Чистые функции над бинарной маской и над альфа-каналом:
- `buildAlphaMask(img)` — 1 там, где альфа > 0.
- `dilateMask(mask, w, h, radius, shape)` — расширение; `shape`: квадрат или
диск (для обводок диск даёт ровную круглую линию).
- `erodeMask(...)` — зеркальная операция.
- `dilateImage / erodeImage(img, radiusPx)` — применяют маскирование к альфе
изображения, RGB сохраняется.
- Сборщики:
- обводка: `dilate(mask) − mask` → новое кольцо заливается цветом;
- контур: `mask − erode(mask)` → линия;
- открытие: `erode → dilate`; закрытие: `dilate → erode`.
- `hardenAlpha(img, thresholdPercent)` — отдельно от морфологии, но той же
волной.
Структурирующий элемент при радиусе r — диск радиуса r (расстояние до центра ≤
r); квадрат не используем, чтобы углы обводок не были угловатыми.
## 4. Web Worker
Ничего не требуется: новые операции попадают в реестр, исполнитель автоматически
исполняет их в воркере. Тесты ядра идут мимо воркера (чистые функции).
## 5. Этапы и проверки
- **A. Ядро** — `morphology.ts` + точные тесты на матрицах 5×5: расширение
диска, сужение, открытие убирает одиночный пиксель, закрытие закрывает
одиночную дыру, кольцо обводки нужного цвета и толщины. Проверка:
`pnpm test/check`.
- **B. Реестр** — семь записей, тест целостности обновляется (51 → 58).
Проверка: `test/build`, визуально.
- **C. Итог** — полный прогон, смоук §7.
## 6. Критерии готовности
- Семь новых инструментов работают на прозрачных PNG: обводка, контур,
толще/тоньше, жёсткие края, убрать мусор, закрыть дыры.
- Все операции детерминированы и покрыты юнит-тестами с ручными ожиданиями.
- Исполнение автоматически идёт в воркере.
- `test/check/build` зелёные, смоук §7 пройден.
## 7. Смоук-чеклист
1. Обводка толщиной 3 вокруг круглого стикера — ровное кольцо выбранного цвета,
без угловатости.
2. Толщина 1 — тонкая аккуратная линия; 10 — толстая, форма содержимого не
искажается сверх роста.
3. Контур показывает только край содержимого линией.
4. Толще/тоньше на тонкой надписи — штрихи растут/сжимаются без разрывов при
умеренных значениях.
5. Жёсткие края: полупрозрачность исчезает, силуэт сохраняется.
6. Убрать мусор убирает одиночные точки-крапинки, не трогая крупные области.
7. Закрыть дыры заполняет одиночные прозрачные точки внутри логотипа.
8. Цепочка: обводка после удаления фона работает; каждое звено скачивает свой
промежуток.
9. Регресс остальных инструментов без изменений.
## 8. Что сознательно не делаем
- Морфология по яркости для непрозрачных сканов — волна фокусируется на альфе
прозрачных PNG.
- Выбор структурирующего элемента пользователем — всегда диск.
- Feather/свечение краёв — отдельная будущая задача поверх размытия.
- Произвольные SVG-маски и градиентные обводки.
-181
View File
@@ -1,181 +0,0 @@
# План: дожать перевод инструментов в preview и переезд ветки `old/`
> **СТАТУС: ЗАКРЫТ (2026-09-07).** Перевод каталога завершён — 121 из 125
> инструментов в `registry-new`; 4 хвоста отложены решениями и перенесены в
> `backlog.md` (`watermark-image-png` → overlay-райз, `png-info` → exif-райз,
> `reduce-to-size-png` → download-райз, `compress-png` → закрыт дубликатом).
> Дальнейшие «хвосты плана» — переезд старых компонентов в папки `old/`,
> закрытие долгов и сбор фидбека — ~уже учитываются в `plan-composite-params.md`
> (шаги 36–37) и `plan-design-fix.md`, поэтому план закрыт, а не продолжен.
> Архивная копия, ниже — состояние на момент закрытия.
> Статус: **переведено 121 из 125; остальные 4 — отложены/закрыты решениями.**
> Текущее состояние: **121 инструмент переведён в `registry-new`**, старый UI
> уже живёт на `(old)/`-маршрутах, линтер-изоляция old↔preview на месте (плагин
> `isolation/no-mixed-imports`). Здесь — что сделать, чтобы:
>
> 1. в preview был **весь** каталог инструментов (плюс закрыты известные долги);
> 2. физически перенести старую ветку в папки `old/` (шаг 37 основного плана);
> 3. после переезда провести детальную проверку в реальных условиях и собрать
> отзывы пользователей (правки поверх — отдельными задачами).
>
> Порядок: перевод остатка → починить известные долги → переезд в `old/` →
> пользовательская проверка/фидбек → (позже, отдельным решением) удаление
> старого кода.
## Текущая картина (факты)
- `registry.ts` + `registry/` — **125 инструментов**; `registry-new/` — **121**.
Не переведены только: `png-info` (отложен, хвост-фича с exif), `compress-png`
и `reduce-to-size-png` (закрыто решениями, см. ниже), `watermark-image-png`
(overlay — единственный реальный остаток).
- Preview (`/preview`) показывает переведённые; старый UI работает на
`/`-маршрутах (группа `(old)/`): `+page`, `demo`, `list-tools`, `tools/[id]`.
Переведено всё кроме четырёх хвостов: color 18, filters 3, geometry 13, alpha
13 (+watermark-image в остатке), generate 10 (mix/sort), analyze 12 (6 масок +
verify + 5 вердиктов), convert 14 (jpg/webp/bmp + 5 png→texт + 6 text→png),
text `watermark-tile-png`.
- Плагин `isolation/no-mixed-imports` (`web/eslint-plugins/isolation/`)
полностью разделяет ветки: старый `lib/registry.ts|/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**`(кроме `kit/`) vs новый `routes/preview/**`,
`lib/registry-new/**`, `lib/preview/**`, `lib/registry-schema.ts`,
`lib/components/kit/**`. Общее: `core/`, `i18n/`, `theme`, `assets/`, корневой
`lib`.
- Правило копий (из основного плана): файл, используемый `(old)`, не трогаем —
делаем копию в `lib/preview/` (пример: `lib/preview/tool-icons.ts`).
- Известные долги (предсуществующие, не создавались нами): ошибка
`ToolCard.svelte:19:36` (svelte-check), фейл `palette.test.ts` (ENOENT
`src/app.css` — из-за него `pnpm test` красный при 607 passed), ~335
design-tokens ошибок `lint:all` в старых kit-компонентах, a11y-warning в
`ColorField.svelte`, пустой ruleset в `Toggle.svelte`, неиспользуемые токены в
`preview.css`.
## Скоуп перевода: переведено всё переводимое (отложено 4 хвоста)
Каталог дожат до предела текущих решений: переведено 121 из 125. Оставшиеся 4
инструмента **отложены осознанно** (не делаем сейчас):
- **`watermark-image-png` (alpha)** — **отложен**. Нужна overlay-механика
(второе изображение-знак через `getOverlay`/store + параметры
scale/opacity/position/margin/aspect); решается отдельно, вместе с вопросом,
как второй источник изображения вписывается в schema-driven preview.
- **`png-info`** — **отложен осознанно**: будет серьёзно дорабатываться
отдельным райзом (exif-теги, редактирование, структурированный вывод), для
него нужен свой отдельный случай в UI, не «ещё один text/verdict».
- **`compress-png`** — **закрыт как дубликат** `decrease-color-count-png`:
пресеты сжатия 192/96/44 добавлены прямо в схему `decrease-color-count`.
- **`reduce-to-size-png`** — **отложен** до достройки кнопки Download (backlog
№7, «экспорт с лимитом размера»).
### Переведено в этом проходе (text-механика)
- **Типы:** `ToolEntry.input: "file" | "text" | "none"`,
`ToolEntry.result: "image" | "text" | "verdict"`, `OutputFormat` (mime/ext/
qualityParamId) для download.
- **Executor:** `executeFromText` / `executeToText` / `executeTextToText`
(прямые, без worker), экспорт в `preview/executor/index.ts`.
- **UI:** `SchemaTextSource` (textarea + Render), `SchemaTextResult` (кол-блок с
Copy/Download .txt или бейдж-вердикт по префиксу Yes/No), интеграция в
`SchemaPreview`/`SchemaToolView` (режимы file/text/generate, рендер
image/text/verdict, скрытие image-download для text-результата).
- **Инструменты:** convert 5×png→text (base64/data-uri/hex/bytes/rgb-values),
6×text→png (base64/data-uri/hex/bytes/rgb-values/svg, +width), analyze
verify-is-png (text→text) и 5 вердиктов (grayscale/file-size/transparent/
orientation). Доработка: `base64ToBytes` сужен до `Uint8Array<ArrayBuffer>`
(совместимость с `decodeBytes`).
- **Гейты:** eslint 0, prettier чист, svelte-check — только предсуществующий
ToolCard, тесты — только предсуществующий фейл `palette.test.ts`.
## Известные долги перед переездом (завести tasks и закрыть)
По AGENTS.md техдолг «чинится только по заведённым tasks, не игнорируется»:
`lint:all` (~335 design-tokens в kit), неиспользуемые токены preview.css, a11y
`ColorField`, пустой ruleset `Toggle`. Плюс два, мешающих зелёной проверке
до/после переезда: svelte-check error `ToolCard.svelte:19:36` и фейл
`palette.test.ts` (ENOENT `src/app.css`). После их закрытия целевое состояние
проверок перед переездом: `pnpm --dir web exec svelte-check` → 0 errors,
`pnpm --dir web test` → зелёный, `pnpm --dir web lint:all` → только задокумент.
ированный остаток (или 0).
## Шаги перевода остатка (порядок)
1. **Простые пачки** — **выполнено**: color (18) → filters (3) → geometry (13) →
alpha (13/14, кроме watermark-image) → generate (10, включая mix/sort через
kind `colors`) → analyze-маски (6) → text `watermark-tile-png`.
2. **Text-механика** — **выполнено**: типы `input`/`result` + output-формат,
executor `executeFromText`/`toText`/`textToText`, UI
`SchemaTextSource`/`SchemaTextResult` + интеграция в preview, перевод
конвертеров convert (5 png→text + 6 text→png) и analyze (verify + 5
вердиктов). `png-info` отложен отдельным райзом (exif).
3. **Шаг про overlay не делается** — `watermark-image-png` **отложен решением**
(см. «Скоуп перевода» выше), отдельным райзом вместе с вопросом второго
источника изображения в schema-driven preview.
4. После категории — `schema.layout` для сгруппированных инструментов (шаг 32-33
паттерн) и ручная проверка в `/preview`.
## Переезд в `old/` (шаг 37 основного плана)
> Скоп-сдвиг: «Scoped-пути двигаются вместе с папками» — обновляется
> `eslint.config.js` (isolation-паттерны и design-tokens glob), не код правила.
1. **Что переезжает:** `lib/registry.ts`, `lib/registry/**`,
`lib/registry-helpers.ts`, `lib/categories.ts`, `lib/tools/**`,
`lib/components/**` (кроме `kit/`) → папки `old/` (например
`lib/old/registry …`, `lib/old/tools`, `lib/old/components`) — то есть всё
старое в одном поддереве. `routes/(old)/` остаётся как есть (уже отделено).
2. **Обновить конфиг плагина** `isolation/no-mixed-imports`: старые glob-ы
`lib/registry/**…` заменить на `lib/old/**` (+ `lib/old/components/**` и
т.п.), «новое» и «общее» не меняются. Проверить, что правило по-прежнему
разделяет (тест фолс-позитивов: временный импорт old↔new ловится).
3. **Импорты внутри старого кода** — относительные пути остаются рабочими (папки
двигаются целиком); поправить только то, что ссылалось на `$lib/...` в другом
стиле. Старые тесты (`registry.test.ts` и др.) переезжают вместе с
исходниками и продолжают проходить.
4. **Гейты переезда:** `svelte-check` 0 errors, `pnpm --dir web test` зелёный,
`prettier --check .` чистый, `lint:all` = задокументированный остаток, обе
ветки открываются (старая `/`, новая `/preview`).
## Проверка и сбор отзывов после переезда
1. **Регресс-чеклист по каталогу** — пройти все инструменты в `/preview`:
дефолты совпадают со старыми, валидация/клампы поведения не меняют, результат
эквивалентен старому UI (сверить на одинаковых входах).
2. **Тяжёлые кейсы руками:** генераторы, маски, font-style/plate/gradient на
всем наборе, text-source, output-формат. (`watermark-image` — позже, после
overlay-райза.)
3. **Сбор фидбека** — отдельные задачи на каждый найденный фикс; чеклист
зафиксировать в этом документе по мере находок (или ссылкой на tasks).
4. Сверка каталога с `tools-map.md` (покрытие/дедупликация
`convert-png-to-jpg`/`webp`), при необходимости — обновить cards/мета.
5. **Решение об удалении старого** — после завершения ревью и фидбека, отдельным
шагом: тогда чистятся `lib/old/**`, `routes/(old)/**`, `old.css`, `exports`,
изоляционный плагин (glob-ы больше не нужны) и открываются «правило
копий»-дубли.
## Как ревьюить каждый шаг (гайды)
- Диф на **один инструмент** (или фундамент без инструментов) < ~500 строк.
- Старый `params`/`run`/`defaultParams`/`sanitizeParams` не изменены; в
`registry-new` копия логики + `schema` (дефолты равны старым).
- Старый UI и старые тесты продолжают работать: `pnpm --dir web test`,
`pnpm --dir web exec svelte-check`.
- Новый инструмент проверяется **руками в `/preview`** end-to-end (применение +
сброс дефолтов + смена значений).
- После переезда каждый move-шаг гоняет 4 гейта (см. выше).
## Оценка трудозатрат
| Часть | Сложность | Оценка |
| ---------------------------------------------------------------------- | -------------- | ------------------------- |
| Простые инструменты (color/filters/geometry/alpha/generate/маски/text) | Низкая-Средняя | **выполнено** (62 шт) |
| Text-механика + конвертеры/вердикты | Средняя | **выполнено** (16 шт) |
| watermark-image / png-info / reduce-to-size (отложены решениями) | Высокая | отложено, отдельные райзы |
| Долги (ToolCard, palette.test.ts, lint:all остаток) | Средняя | ~2-4ч |
| Переезд в `old/` + конфиг плагина + гейты | Средняя-Низкая | ~2-4ч |
| Проверка/фидбек + правки | Зависит | ~3-6ч |
| **Итого** | | **~9-18ч** по фазам |
> Оценки ориентировочные; особые случаи — с ревью решения, объём финальных
> правок по фидбеку не предсказуем.
-131
View File
@@ -1,131 +0,0 @@
# План: единый `run(ctx)` в registry-new (унификация 5 режимов исполнения)
> **СТАТУС: ВЫПОЛНЕН (2026-09-10) — устарел.** Убивал пару багов-классов
> «инструмент с generate показывает загрузку и не выдаёт результат»
> (linear-gradient-png и все генераторы): причина — `input` не задан, диспетч по
> факту наличия методов.
## Исходная проблема
Два бага у `linear-gradient-png` (и у всех генераторов):
1. Показывается загрузка изображения — `SchemaToolView.svelte` считает
`inputMode = tool.input ?? "file"`, а `tool.input` у генераторов не задан.
2. Результата нет — `SchemaActions` рендерит кнопку «Generate» только при
`inputMode === "none"`, а он всегда `"file"`. Ui-контур «догадывается» о
генераторе по эвристике `Boolean(tool.generate && !tool.run)`.
Корневая причина архитектурная: у инструмента 5 режимов исполнения (`run` 79
шт., `generate` 21, `runFromText` 6, `toText` 9, `textToText` 1), и диспетч
«какой метод вызывать» размазан между UI и executor. Способ исполнения
(worker/поток, было `domOnly`) просочился в метод-диспетч. Договорились:
**свести всё к одному методу `run(ctx)`**, а требование входа (`input`) — к
обязательному полю. `domOnly` остаётся только executor-хинтом (уйдёт при
переезде на WASM-ядро).
## Целевой контракт
```ts
export type InputMode = "image" | "text" | "none"; // было "file"
export type ResultKind = "image" | "text" | "verdict";
export interface ToolContext<P> {
params: P;
source?: PixelImage; // присутствует ⇔ input === "image"
text?: string; // присутствует ⇔ input === "text"
}
export type ToolResult = PixelImage | string;
export type ToolEntry<P = Record<string, unknown>> = {
id: string;
title: string;
description: string;
category: CategoryId;
schema: ToolSchema<P>;
input: InputMode; // ОБЯЗАТЕЛЬНОЕ, дефолта нет
result?: ResultKind; // default "image"
run(ctx: ToolContext<P>): Promise<ToolResult> | ToolResult;
output?: OutputFormat;
icon?: string;
domOnly?: boolean; // executor-хинт, уйдёт с WASM
};
```
Удаляем: `generate`, `runFromText`, `toText`, `textToText`. Хелперы контракта
(бросают `ToolError`): `requireSource(ctx)`, `requireText(ctx)`. Обёртки для
миграции тел 1:1: `imgTool((img, p) => …)`, `genTool((p) => …)`,
`textGen((t, p) => …)`.
```ts
const imgTool =
<P>(fn: (img: PixelImage, p: P) => ToolResult) =>
(ctx: ToolContext<P>) =>
fn(requireSource(ctx), ctx.params);
const genTool =
<P>(fn: (p: P) => ToolResult) =>
(ctx: ToolContext<P>) =>
fn(ctx.params);
const textGen =
<P>(fn: (t: string, p: P) => ToolResult) =>
(ctx: ToolContext<P>) =>
fn(requireText(ctx), ctx.params);
```
## Executor (единый)
```ts
export async function execute(
tool: ToolEntry,
ctx: { params: Record<string, unknown>; source?: PixelImage; text?: string },
): Promise<ToolResult> {
const params = sanitizeSchemaParams(tool.schema, ctx.params);
if (tool.input === "image" && !ctx.source)
throw new ToolError("errors.sourceRequired");
if (tool.input === "text" && !ctx.text?.trim())
throw new ToolError("errors.textRequired");
return route(tool, { params, source: ctx.source, text: ctx.text });
}
```
`route()` — единственное место выбора места исполнения: `domOnly` или нет Worker
→ прямой вызов `tool.run(ctx)`, иначе worker с fallback (логика
`ensureWorker/runInWorker/disableWorker` остаётся). Worker-протокол —
`{ id, toolId, params, source?, text? }`.
## Шаги (ревью после каждого)
Все 11 шагов выполнены; гейт пройден: `svelte-check` → 0 errors,
`pnpm --dir web lint` → чисто, `pnpm --dir web test` → 618/618 зелёный. E2E не
делали.
| # | Шаг | Файлы | Что |
| --- | --------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Типы | `registry-new/types.ts` | новый `ToolEntry`/`ToolContext`/`ToolResult`/`InputMode "image"`, хелперы `requireSource`/`requireText` + `imgTool`/`genTool`/`textGen` |
| 2 | Executor+Worker | `preview/executor/executor.ts`, `executor.worker.ts`, `index.ts` | единый `execute`, `route` по `domOnly`, протокол `source?`/`text?` |
| 3 | UI kit | `kit/SchemaToolView.svelte`, `SchemaPreview.svelte`, `SchemaActions.svelte`, `SchemaSourceTile.svelte` | одна `run()`, без `isGenerator`-эвристики, `inputMode` → `"image"` |
| 4 | Генераторы | `registry-new/generate.ts` | 21 `generate(p)` → `run(ctx)` + `input: "none"` (чинит градиент и др.) |
| 5 | Текст | `registry-new/convert.ts`, `analyze.ts` | `runFromText`/`toText`/`textToText` → `run`; `input: "image"` на run-инструментах |
| 6 | Текст | `registry-new/text.ts` | domOnly run-инструменты → `run(ctx)` + `input: "image"` |
| 7 | Геометрия | `registry-new/geometry.ts` | run → `run(ctx)` + `input: "image"` |
| 8 | Альфа/цвет | `registry-new/alpha.ts`, `color.ts` | run → `run(ctx)` + `input: "image"` |
| 9 | Фильтры | `registry-new/filters.ts` | run → `run(ctx)` + `input: "image"` |
| 10 | Тесты | `registry-new/registry-new.test.ts` | `generate!`/`run!` → `run(ctx)`; guard-тест: `input` всегда задан, image-инструменты работают с источником, `none` — без |
| 11 | Гейт | — | `svelte-check` → 0 errors, `pnpm --dir web lint` → чисто, `pnpm --dir web test` → 618/618 |
Примечание шага 8: поля `preview:` (маска у remove-color/remove-background)
удалены — новый executor зовёт `run()` напрямую; идея вернуть отображение маски
— в `docs/backlog.md` №17. Prettier-варнинги `prettier --check` по
SchemaToolView.svelte и executor-файлам — предсуществующий долг.
По миграции: `input: "image"` добавлен всем run-инструментам (~80 шт.),
`input: "text"` стоит у 7 text-инструментов, `input: "none"` — у 21 генератора.
## Известные последствия
- **Красная сборка с шага 1 до конца шага 10** — осознанное решение, ревью идёт
по diff'у шагов.
- Старый контур (`registry/**` + `lib/tools/executor.ts` + `ToolPage.svelte`,
`sourceMode`) НЕ трогаем — он на удаление (C19).
- `input: "file"` переименован в `input: "image"` — семантика «нужно именно
изображение», а не «файл» (между инструментами может передаваться не файлом).
-104
View File
@@ -1,104 +0,0 @@
# План: range-слайдеры и пипетка цвета
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Целевое поведение
1. «Процентные» параметры (яркость, контраст, качество JPEG/WebP, порог
похожести) — слайдеры с числовым значением рядом; точные координаты и размеры
(resize/crop) остаются полями ввода.
2. У цветовых параметров появляется кнопка-переключатель «Пипетка»: активна →
клик по превью исходника снимает цвет пикселя и подставляет его в поле цвета;
после выбора режим гаснет.
3. В режиме пипетки возле курсора видна лупа: увеличенный блок пикселей вокруг
точки с перекрестием и подписью hex текущего пикселя; клик выбирает центр
перекрестия.
4. Активная пипетка видна на превью перекрестием курсора; повторный клик по
кнопке отменяет режим без выбора.
5. Всё работает поверх текущего процесса: авто-применение с задержкой, токен
запусков, маски — без изменений.
## 2. Слайдеры
- В реестре новый тип параметра `'slider'` с обязательными `min`/`max`;
валидируется как число (дефолт в диапазоне, минимум меньше максимума).
- Форма маппит его на готовый `SliderField` из `ui/`; санитизация параметров
обрабатывает slider так же, как number.
- Переводятся: яркость и контраст, качество JPEG, качество WebP, порог
похожести. Остальные инструменты — без изменений.
## 3. Пипетка и лупа
- В ядро добавляется чистая функция `rgbToHex(r, g, b)` → строка вида `#rrggbb`,
с юнит-тестами; чтение пикселя — из уже отрисованного canvas превью исходника
через `getImageData(x, y, 1, 1)`.
- `Preview.svelte` получает опциональный режим пипетки: пропсы «активна» и
колбэк «цвет выбран»; курсор перекрестие, клик вычисляет координату пикселя с
учётом разницы CSS-размера canvas и его внутренней ширины.
- **Лупа** — компонент `PipetteLoupe.svelte`: канвас около 110×110 px у курсора
(позиция fixed, `pointer-events: none`); рисует блок 15×15 пикселей из
исходника с увеличением ×8 без сглаживания, перекрестие в центре, подпись hex
текущего пикселя. Координаты блока клампятся у краёв изображения; лупа скрыта
вне превью и вне режима пипетки. Тач-устройства: прямой тап выбирает пиксель
без лупы.
- Проводка: кнопка у каждого цветового поля в карточке параметров → состояние
«активное цветовое поле» живёт в оркестраторе → выбор цвета записывает hex в
это поле и выключает режим.
- Пипетка работает только по левой панели (исходнику): удаляемый цвет и подложка
берутся с оригинала.
## 4. Этапы и проверки
- **A. Слайдеры** — тип в реестре, перевод четырёх инструментов, тесты реестра и
санитизации, тест `rgbToHex`. Проверка: `pnpm test/check`, визуально слайдеры.
- **B. Пипетка** — режим в Preview, проводка через карточки до полей цвета.
Проверка: смоук §6 пп. 5–7.
- **B2. Лупа** — компонент лупы поверх режима пипетки. Проверка: смоук §6 пп.
8–10.
- **C. Итог** — полный прогон `test/check/build`.
## 5. Изменения по файлам
- `registry.ts` — тип `'slider'` в `ParamDef`, перевод четырёх записей, ветка в
`sanitizeParams`.
- `ParamForm.svelte` — ветка slider → `SliderField`.
- `core/color.ts` — `rgbToHex`.
- `Preview.svelte` — режим пипетки (курсор, клик, колбэк).
- `PipetteLoupe.svelte` — лупа у курсора в режиме пипетки.
- `SourceCard.svelte` — проброс режима и колбэка наверх.
- `ParamsCard.svelte` + `ColorField.svelte` — кнопка «Пипетка» у цветового поля.
- `ToolPage.svelte` — состояние активного поля, обработчик выбора цвета.
## 6. Смоук-чеклист
1. Яркость/контраст — слайдеры со значением справа, результат обновляется сам
при перетаскивании.
2. Качество JPEG/WebP — слайдер от 1 до 100.
3. Порог похожести — слайдер от 0 до 100.
4. Resize и crop остались числовыми полями.
5. Удаление цвета: «Пипетка» → клик по красной области исходника → поле цвета
получил hex этого пикселя, режим погас, результат пересчитался.
6. Повторный клик по кнопке или выбор без подтверждения — режим снят, цвет не
изменился.
7. Подложка JPEG: пипетка проставляет цвет подложки тем же способом.
8. В режиме пипетки лупа следует за курсором, перекрестие показывает пиксель под
ним, подпись hex совпадает с выбранным при клике.
9. У края картинки блок лупи корректно клампится — за границей тянется
чёрный/прозрачный фон, выбор по-прежнему точен.
10. Курсор ушёл с превью или режим выключен — лупа скрыта; клики по кнопкам под
превью не перехватываются лупой.
11. Слайдеры и пипетка дружат с быстрыми правками: одна финальная версия
результата, чип «Пересчёт…» на месте.
## 7. Критерии готовности
- Все процентные параметры — слайдеры; числовые поля остались только там, где
нужен точный ввод.
- Цвет любого color-параметра можно взять кликом с исходника.
- `test/check/build` зелёные, чеклист §6 пройден.
## 8. Что сознательно не делаем
- Пипетка и лупа на превью результата и маски — только исходник.
- Кастомная стилизация слайдеров за пределами дизайн-системы.
- Лупа для тач-устройств — там прямой тап без лупы.
-82
View File
@@ -1,82 +0,0 @@
# План: инструмент «Разрезать PNG на части» (split-into-parts-png)
> Статус: **выполнено и в архиве** (2026-09). Инструмент и механизм «результат =
> набор файлов» (1 → many) реализованы; первый потребитель —
> `split-into-parts-png`. Общий процесс таких инструментов — задача 18 в
> `docs/backlog.md`.
## Цель
Пользователь загружает одну картинку, выбирает число столбцов и строк, картинка
разрезается на равномерные части. Результат — набор PNG-файлов, скачивается
ZIP-архивом.
## Решения
- **Неравномерное деление:** канвас дополняется прозрачным до кратного размера
(`pieceW = ceil(w / cols)`, `newW = pieceW * cols`), все части строго равные,
картинка покрывается целиком.
- **Дефолт схемы:** 2 × 2.
- **Zip-библиотека:** `fflate` (zero deps, малый размер), скачивание из UI-слоя
(`encode()` требует DOM).
- **Результат инструмента:** `result: "files"` — терминальный тип, в пайплайн
(следующий шаг) не передаётся.
## Шаги
1. **Зависимость:** `fflate` в `web/package.json`.
2. **`registry/types.ts`:** `RESULT_KINDS.files`, тип `ToolImageFile`,
`FileResult`, расширение `ToolResult = PixelImage | string | FileResult`.
3. **`core/geometry.ts`:** `splitToParts(img, columns, rows)` — padding + сетка
через `crop`.
4. **`web/src/lib/zip.ts` (новый):** `downloadZip(files, zipName)` через
`zipSync`.
5. **`core/errors.ts`:** ключ `errors.tooManyParts` (страховочный лимит
cols·rows ≤ 1000; при максимуме 6×6 недостижим).
6. **`registry/geometry.ts`:** схема (`columns`, `rows`, дефолт 2×2, min 1, max
6 — максимум 36 частей) + entry `split-into-parts-png` в `geometryEntries`.
7. **Executor:** сериализация/десериализация `FileResult` в `executor.worker.ts`
и тип ветки в `executor.ts`.
8. **UI:** `SchemaToolView.svelte` (`fileResult`, Download → zip),
`SchemaPreview.svelte` (canDownload для files), `SchemaResultTile.svelte`
(сетка-превью частей, мета «N parts / ZIP»).
9. **i18n:** переводы в `ru.ts`/`en.ts` **не добавляем** — словари `tools`
привязаны к v1-реестру (тест «нет лишних ключей»), а preview берёт заголовки
из registry (EN). Перевод придёт вместе с беклог-задачей «i18n в preview».
Поиск по id/заголовку работает из registry.
10. **Тесты:** юнит `splitToParts`, registry (result: "files"), i18n coverage.
11. **Docs:** `docs/backlog.md` (задача «процесс 1 → many и many → 1», отметить
пункт 11 «Мультифайловый вывод»), `docs/tools-map.md` (перенос из идей).
## Файлы
| Файл | Изменение |
| ------------------------------------------------ | ----------------------------------------------------------------- |
| `web/package.json` | `+ fflate` (dependencies) |
| `web/src/lib/registry/types.ts` | `RESULT_KINDS.files`, `ToolImageFile`, `FileResult`, `ToolResult` |
| `web/src/lib/core/geometry.ts` | `splitToParts()` |
| `web/src/lib/zip.ts` | **новый** — `downloadZip()` |
| `web/src/lib/core/errors.ts` | `errors.tooManyParts` |
| `web/src/lib/registry/geometry.ts` | schema + `splitPartsTool` |
| `web/src/lib/executor/executor.worker.ts` | ветка `FileResult` |
| `web/src/lib/executor/executor.ts` | тип ветки `FileResult` |
| `web/src/lib/components/SchemaToolView.svelte` | `fileResult`, download → zip |
| `web/src/lib/components/SchemaPreview.svelte` | props `fileResult`, `canDownload` |
| `web/src/lib/components/SchemaResultTile.svelte` | сетка частей, «N parts» |
| `web/src/lib/i18n/ru.ts`, `en.ts` | записи инструмента |
## Нейминг файлов в ZIP
`part-<row>-<col>.png` (row, col нумеруются с 1, при 2×2 — `part-1-1.png`).
## Верификация
```bash
pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json
pnpm --dir web test
pnpm --dir web lint
```
Ручная проверка: PNG не кратного размера (например 101×77), 3×2 → 6 частей
одинакового размера, скачивается ZIP; на кратном размере (100×80, 3×2) padding
= 0.
-95
View File
@@ -1,95 +0,0 @@
# План: закрытие техдолга перед переездом `old/` (C17)
> **Статус: ЗАКРЫТ.** Все гейты пройдены, C17 разблокирован.
## Состав техдолга
- **svelte-check error** — `ToolCard.svelte:19:36`: `resolve(normalizedHref)`
принимает только типизированный роут, а `normalizedHref: string`. Красный
svelte-check, висит на каждом прогоне.
- **фейл теста** — `src/lib/palette.test.ts`: `ENOENT src/app.css` (файл
разъехался на `old.css` + `design2.css` при редизайне). Тест проверял
контрасты палитры по токенам старого `app.css`
(`text/surface/bg/link/ accent/danger/border`). При гейтах `pnpm test`
красный, хотя 607 тестов проходят.
- **design-tokens в `kit/**`** — ~335 ошибок `lint:all` от локального плагина
`eslint-plugins/design-tokens` (захардкоженные цвета/размеры/длительности/
z-index/`color-mix()` в `<style>`-блоках svelte), плюс stylelint по CSS.
- **a11y-предупреждение** — `ColorField.svelte`.
- **пустой ruleset** — `Toggle.svelte`.
## Этапы
### Этап 1 — «зелёные» гейты (мелкие, по ~10–40 строк каждый)
1. **ToolCard** — убрать ошибку типов: либо валидировать `href` против набора
известных маршрутов, либо явно протащить типизированный роут из каталога.
Проверка: svelte-check 0 errors.
2. **palette.test.ts** — переписать чтение на новый словарь: тест контрастов
гоняется по токенам `preview.css` (`:root` + `[data-theme="dark"]`, цвета
`--color-*`/`--brand-*`). Отдельно: взять из нового дизайна, какие пары
реально проверить (текст/фон, muted, акцент/бренд, бордер). Если часть
проверок потеряла смысл в новом дизайне — сократить, не растягивать.
3. **a11y `ColorField` + пустой ruleset `Toggle`** — пофиксить прямо в этих
компонентах.
Гейт этапа: `svelte-check` 0 errors, `pnpm test` зелёный.
### Этап 2 — миграция `kit/**` на design-токены (main объём)
Цель — `lint:all` без ошибок design-tokens (все примитивы/`color-mix()` уходят в
токены `preview.css`).
**Вариант А (основной): авто-фикс одноразовым скриптом.** Писать скрипт ТОЛЬКО
если он гарантированно ничего не испортит. Гарантии:
- Скрипт — одноразовый (`web/scripts/fix-design-tokens.mjs`), после прогона
удаляется либо помечается неактуальным.
- Строгая карта замен: примитив → токен **1:1, только если значение точно
совпадает с уже существующим токеном словаря** `preview.css` (цвет →
`--color-*`/`--brand-*`, радиус → `--radius-*`, размер →
`--space-*`/`--size-*`, длительность → `--duration-*`, z-index → `--z-*`).
Никаких новых токенов скрипт не генерирует.
- Замены — только примитивы, не содержащие `var()`/`calc()`/`color-mix()`;
исключения по правилам плагина (0, 0px, 1px, проценты, unitless) не трогаются.
`color-mix()` → выносится в `preview.css` как производный `--color-*` токен
(может требовать ручного решения имени).
- Скрипт обрабатывает только `src/lib/components/kit/**` и `src/preview.css`
(scoped-пути плагина) и только `<style>`-блоки/`.css` — логику/разметку не
меняет, только строки стилей.
- После прогона: обязательный дифф-ревью (все изменения — замена значений на
`var(--x)`, ничего удалено/переставлено), затем полный `lint:all` +
`svelte-check` + `pnpm test`. Если хоть что-то вне «замена примитива на токен»
— скрипт считается проваленным, откатываем и переходим на вариант Б.
**Вариант Б (страховка): руками, пакетами по компонентам.** Если скрипт не
удался — чистим/заводим токены вручную, пакетами по компонентам (1–2 на задачу,
дифф < ~500 строк). Отсутствующие токены (например
`--space-2xl`/`--space-panel`, уже дважды обходились в SchemaTextSource)
добавляем в словарь `preview.css` осознанно.
Гейт этапа: `pnpm --dir web lint:all` — только допустимый остаток
(неиспользуемые токены — варнинг), плюс регресс-прогон тестов и svelte-check.
### Этап 3 — чистота словаря `preview.css`
- Разобрать варнинги неиспользуемых токенов (check-tokens): либо использовать,
либо удалить из словаря.
- После этапа 2 — повторный аудит токенов
(`pnpm --dir web exec node scripts/check-tokens.mjs`): parity light/dark,
hct-only, без мёртвых токенов.
### Этап 4 — финальные гейты (гейт переезда `old/`)
1. `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` → 0 errors
2. `pnpm --dir web test` → зелёный
3. `pnpm --dir web exec prettier --check .` → чистый
4. `pnpm --dir web lint:all` → 0 (или только задокументированный остаток)
5. Обе ветки открываются: старый UI `/`, новый `/preview`
## Порядок и коммиты
- Коммиты атомарные, < ~500 строк (AGENTS). По завершении каждой мини-задачи —
ревью (агент не коммитит).
- Техдолг чинится по заведённым задачам, правила линтеров не отключаются и не
игнорируются.
-194
View File
@@ -1,194 +0,0 @@
# План: тестирование preview перед промоушеном в главную
> Статус: **реализация завершена**. Playwright-набор в `web/e2e/` (108 тестов:
> 104 прошли, 4 `test.fixme` — известные баги №1–4 ниже), ручной чек-лист —
> `docs/checklist-manual-testing.md`. План ветки preview
> (`web/src/routes/preview/**`, регистр `lib/registry-new/`, 121 инструмент)
> перед шагом C17 из `docs/plan-redesign.md` (переезд preview → реальные
> маршруты).
>
> Охват: **документация плана** — реализация тестов (Playwright-раннер, сами
> тесты) делается отдельными задачами после ревью этого документа.
>
> Приоритет: **отсутствие падений/ошибок** > пиксельная корректность. Дизайн
> доделываем по ходу, визуальная сверка — ручная и вторичная.
>
> Команды для проверки в процессе:
>
> - `pnpm --dir web test` — существующий Vitest (42 файла, 121 инструмент
> частично покрыт юнит-тестами).
> - `pnpm --dir web test:e2e` — Playwright (`web/e2e/`), сборка +
> `scripts/serve-static.mjs` на 4173.
> - `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — проверка
> типов.
> - `pnpm --dir web lint:all` — ESLint + stylelint + токен-аудит.
> - `pnpm --dir web build` — статическая сборка (adapter-static).
## 0. Что тестируем
Preview-ветка — клиентское SPA на SvelteKit (adapter-static, всё в браузере,
никакого API). Структура:
| Маршрут | Что это |
| ------------------------------------- | -------------------------------------------------------------------------------------------- |
| `/preview` | Workspace: поиск + сетка `ToolCard` (топ-24), «Open last project» (stub), футер |
| `/preview/kit` | Витрина всех kit-компонентов (light/dark) |
| `/preview/list-tools` | Каталог: 8 групп, 121 инструмент, фильтр по категории, поиск |
| `/preview/tools/[id]` | Страница инструмента: `SchemaToolView` (настройки + live preview), prerendered на все 121 id |
| `/preview/tools/xxx` (несуществующий) | 404 → `EmptyState` «Tool not found» |
Инструменты: **121**, категории CONVERT / TRANSPARENCY (alpha) / COLOR /
GEOMETRY / FILTERS / TEXT / ANALYZE / GENERATE. Режимы инструмента:
- **file** (transform): upload → `run` → картинка → download;
- **text** (convert-source): ввод текста → `runFromText` → картинка, либо
`textToText` → текстовый результат;
- **verdict** (`result: "verdict"`, file): `toText` → текстовый вердикт;
- **text-output** (`result: "text"`, file): `toText` → текстовый результат;
- **generate**: без входа, `executeGenerate` → картинка.
Исполнение: `preview/executor` — worker с fallback на main-thread (domOnly / нет
Worker / ошибка worker). Auto-run с debounce 200 мс.
## 1. Функциональные блоки для проверки
### A. Инфраструктура и навигация
1. Все 4 маршрута + 404 открываются без console errors, uncaught exceptions и
failed requests.
2. Поиск на `/preview` — фильтрация по названию/описанию, пустой результат →
аккуратное empty-state.
3. Каталог `/preview/list-tools` — 8 групп, счётчик 121, фильтр категории,
иконки не падают.
4. Тема light/dark и язык RU/EN переключаются на превью-страницах без регресса
рендера.
5. 404 на несуществующий id инструмента → EmptyState, без краша.
6. Prerender: `pnpm --dir web build` проходит; все 121 страницы
`/preview/tools/[id]` генерируются (проверка `entries()`/`getTool`).
### B. Конвейер инструмента (file-инструменты)
7. Upload → авто-run → результатный тайл появился, поле ошибки пустое.
8. Download — событие download, имя `<toolId>.<ext>`, MIME соответствует
`output`.
9. Reset — возвращает дефолты, сбрасывает результат и ошибку.
10. Смена параметра → авто-перезапуск (debounce 200 мс) без «залипания»
running-стейта.
11. Worker-исполнение и fallback-исполнение оба дают результат.
### C. Текстовые инструменты и вердикты
12. Text-source: ввод → текстовый результат или картинка; copy/download .txt.
13. Вердикты (analyze): корректный вердикт на PNG с альфой, на PNG без альфы, на
не-PNG входе → без падений.
14. Пустой text-source → run пропускается без ошибки.
### D. Генераторы
15. Каждый генератор по дефолтам создаёт картинку; смена размеров/цвета
перегенерирует; канва ожидаемого размера.
## 2. Репрезентативная выборка по категориям (для «нет падений»)
Полный прогон всех 121 — опционально (C/all); минимальный чек — по группам:
| Категория | Кол-во | Охват для «нет падений» |
| ------------ | ------ | --------------------------------------------------------------------------------- |
| CONVERT | 14 | convert-png-to-jpg, convert-png-to-webp, png-to-base64, base64-to-png, svg-to-png |
| TRANSPARENCY | 20 | remove-background, round-corners, add-stroke, remove-color-from, circle-mask |
| COLOR | 25 | grayscale, invert, black-and-white, quantize, dithering, png-to-hsl |
| GEOMETRY | 18 | resize, crop, rotate, flip, add-border, tile |
| FILTERS | 8 | blur, sharpen, pixelate, add-noise |
| TEXT | 3 | add-text, date-stamp, watermark-tile |
| ANALYZE | 12 | extract-color-from, show-transparent-pixels, verify-is-png, png-is-transparent |
| GENERATE | 21 | create-empty, linear-gradient, two-colors, placeholder, text-to-png |
## 3. Playwright (авто) против ручного — разбиение
### ✅ Playwright — детерминированные сквозные потоки и «нет падений»
- Все маршруты + 404: открытие, отсутствие console errors / uncaught exceptions
/ failed requests (слушатели `page.on('console'/'pageerror')`).
- Поиск и каталог: фильтрация, счётчики, переходы.
- Upload (`setInputFiles` с фикстурой PNG) → авто-результат → Download (ожидание
`download` события, проверка имени/расширения/suggestedFilename).
- Reset / смена параметра → перегенерация, running не «залипает».
- Тема light/dark и язык RU/EN.
- Текстовые инструменты: ввод → текстовый результат → копия/скачивание.
- Вердикты на подготовленных фикстурах (PNG с альфой, PNG без альфы,
JPG/некартинка).
- Генераторы: дефолты → результат появился, размер канвы ожидаемый.
- 404 и empty-state.
- Worker и fallback: сравнить оба пути рендера (инжект отключения Worker).
### 🔄 Playwright + ручная сверка
- Пиксельная корректность результата (сравнение `<canvas>` через
`toDataURL`/тест-per-pixel) — по приоритету вторично; эталоны задаются
вручную, делать отдельной задачей.
- Реальные пользовательские картинки (не фикстуры) — вручную.
### ✋ Ручное тестирование
- **Краевые PNG:** большие (мегапиксели), 1×1, анимация, палитровые,
чекерборд/полупрозрачность, повреждённые файлы → нет зависаний, понятные
сообщения об ошибке.
- **Края параметров:** экстремальные значения, NaN/пустые поля, инвалидные цвета
в плашках/градиентах → корректная санитизация (silent repair).
- **Производительность/отзывчивость:** задержка авто-реранов на слайдерах, нет
«залипания» UI, скролл при длинных настройках.
- **Визуальный слой дизайна** (доделываем по ходу): сетки, выравнивание, отступы
на брейкпоинтах mobile/tablet/desktop (640/800/1100).
- **Буфер обмена** (copy) в реальном браузере.
- **Drag-n-drop** файла на dropzone (UX-ощущения).
- **Разные браузеры:** Firefox/Safari-нюансы (webkit в Playwright опционален).
- **Субъективная корректность:** dithering, two-colors, watermark, текст
не-ASCII/кириллица в add-text/date-stamp/watermark-tile.
## 4. Критерии приёмки (доступ в главную)
1. Все 4 маршрута + 404 открываются без console errors и исключений.
2. Репрезентативный прогон по 8 категориям: ни один инструмент не падает и не
показывает поле ошибки.
3. Upload → результат → download завершается для file-инструментов;
текст/вердикты/генераторы дают корректный output-тип.
4. `pnpm --dir web test`, `pnpm --dir web lint:all`, `pnpm --dir web build`
зелёные.
5. Тема и язык переключаются без регрессов.
## 5. Оценка покрытия
- **~60–70%** «нет падений/ошибок» закрывается Playwright детерминированно
(маршруты, сквозные потоки, отсутствие ошибок).
- **~30–40%** — руками: краевые PNG/параметры, реальные пользовательские файлы,
буфер обмена, кросс-браузеры, визуальный/производительный слой, субъективная
корректность.
## 6. Шаги реализации (после ревью)
1. ✅ Playwright-раннер: `web/playwright.config.ts` (webServer на
`pnpm build + `scripts/serve-static.mjs` `--port
4173`), папка `web/e2e/`, npm-скрипт `test:e2e`.
2. ✅ Фикстуры PNG (генерируются в рантайме, `web/e2e/helpers/fixtures.ts`): с
альфой, без альфы, 1×1, большой, повреждённый, ландшафт.
3. ✅ E2E по блокам A–D и секции 3 (авто-часть): `navigation.spec.ts`,
`catalog.spec.ts`, `pipeline.spec.ts`, `text-and-verdicts.spec.ts`,
`tools-smoke.spec.ts` (матрица ~71 инструмента), `generators.spec.ts`,
`known-issues.spec.ts` (fixme).
4. ✅ Ручной чек-лист из секции 3 (ручная часть) —
`docs/checklist-manual-testing.md` (разделы A–G, включая известные баги).
5. ✅ Найденные баги (зафиксированы как `test.fixme` + чек-лист §G):
1. генераторы (21/121) не имеют кнопки «Generate» и полей схемы — результат
через UI недостижим;
2. `resize-png` с дефолтом 0×0 → `errors.resizeSize` при любом входе;
3. `crop-png` с дефолтом 0×0 → `errors.cropSize` при любом входе;
4. ключи ошибок показываются сырыми (`errors.*`) вместо переведённого текста
из `en.ts`/`ru.ts`. Исправления — отдельными атомарными задачами по
конвенции AGENTS.md (после правок: `lint:all`, `test:e2e`).
## 7. Итоговое состояние на момент сдачи
- `pnpm --dir web test:e2e` — **104 passed, 4 skipped (fixme), 0 failed**.
- `pnpm --dir web lint:all` и `svelte-check` — зелёные.
- Визуальный слой, краевые PNG/параметры, кросс-браузеры — остаются на ручной
прогон по `docs/checklist-manual-testing.md`.
-81
View File
@@ -1,81 +0,0 @@
# План: волна 4 — текст, штампы, водяные знаки
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
## 1. Что получается
Четыре инструмента новой категории «Текст»: надпись на изображении, дата-штамп,
защитная плитка (повторяющийся полупрозрачный текст под углом), водяной знак
картинкой. Каталог вырастает с 69 до 73. Это первые операции, рисующие через
canvas 2D — класс perceptual по роадмапу: точные пиксельные тесты на них не
пишутся, юнит-тестами покрывается чистая математика раскладки.
## 2. Решения
- Вся геометрия — чистые функции без DOM: якорь из 9-позиционной сетки →
координаты, перенос текста по ширине с инъекцией измерителя, раскладка плитки
с углом и шагом. Canvas-вызовы — тонкий слой рядом (`core/textdraw.ts`), в
юнит-тесты не попадает.
- Один движок рендера текста на все три текстовых инструмента: шрифт из
системного набора (sans/serif/mono), размер, цвет, жирность, прозрачность,
поворот. Различия инструментов — только в пресетах параметров и количестве
слоёв (одна надпись vs плитка).
- Дата-штамп — надпись с автотекстом: строка формата (`YYYY-MM-DD hh:mm`)
разворачивается в текущую дату в момент запуска; свой мини-форматтер без
зависимостей.
- Водяной знак картинкой — первый двухисточниковый инструмент: вторая картинка
грузится отдельной зоной в этапе, живёт только в состоянии страницы и
сознательно не сохраняется ни в workspace-pipeline, ни в экспорте JSON; после
восстановления цепочки знак нужно выбрать заново. Ограничение фиксируем в
описании инструмента.
- Новая категория `text` встаёт после «Фильтров»; названия в обоих словарях,
порядок в CATEGORIES один.
## 3. Инструменты
- `add-text-png` — параметры: текст (строка), размер (слайдер 8–200), цвет,
шрифт (select), жирность (чекбокс), позиция (select 3×3: углы/стороны/центр),
отступ (слайдер). Опциональная подложка: цвет + прозрачность плашки за
текстом.
- `date-stamp-png` — тот же набор минус текст, плюс формат даты; дефолт — нижний
правый угол.
- `watermark-tile-png` — текст, размер, цвет, прозрачность (0–100), угол
(-90…90), шаг по X/Y; плитка заполняет весь холст.
- `watermark-image-png` — второй источник (загрузка PNG с альфой), масштаб знака
(% от ширины холста), прозрачность, позиция 3×3, отступ.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Фундамент** — категория `text` (categories + словари + порядок),
`core/textdraw.ts`: anchor9, wrap, tileLayout с юнит-тестами на математике
(координаты углов/центра, переносы слов, плотность плитки), тонкий
canvas-модуль `domText.ts` (без тестов). Реестр пока пуст в категории.
- **B. Надпись и дата** — `add-text-png`, `date-stamp-png` на общем движке;
иконки, переводы, записи реестра. Смоук: обе локали, тёмная тема (плашка
читаема), длинный текст переносится и не вылезает за край.
- **C. Плитка** — `watermark-tile-png`; смоук: отрицательные и нулевой угол,
прозрачность 0/50/100, мелкий шаг не подвешивает страницу на больших холстах
(ограничение количества плиток капом).
- **D. Знак картинкой** — двухисточниковый UX: вторая drop-зона в этапе
(компонент рядом с SourceCard, виден только у этого инструмента),
масштаб/позиция/отступ; ограничение про сохранение — в описании и подсказке.
Этап режется без ущерба остальным, если решим отложить.
- **E. Итог** — полный прогон, смоук §6, архивация плана.
## 5. Критерии готовности
- 73 инструмента; вся математика раскладки покрыта юнит-тестами; canvas-слой
изолирован в одном модуле.
- test/check/build зелёные; новые инструменты работают в цепочке как обычные
шаги (кроме источника знака — см. ограничение).
## 6. Что сознательно не делаем
- Выбор произвольных пользовательских шрифтов (файлами) и веб-шрифтов — только
системный набор.
- Кернинг/линии/многострочное редактирование с абзацами — один абзац с
автопереносом.
- Сохранение картинки-знака в пайплайне и её передача между сессиями.
- Текст по контуру, градиентные заливки текста.
-61
View File
@@ -1,61 +0,0 @@
# План: светлая и тёмная тема
> **СТАТУС: ВЫПОЛНЕН 25.08.2026.**
## 1. Что получается
Переключатель light/dark в шапке рядом с языковым. Выбор в localStorage
(`theme`), первый визит — по `prefers-color-scheme`. Тёмная палитра —
переопределение токенов селектором `[data-theme='dark']`; компоненты не
правятся.
## 2. Решения
- Два состояния, без «авто»: ключ хранит только явный выбор, отсутствие ключа =
системная тема.
- Анти-вспышка: инлайн-скрипт в `app.html` до гидрации ставит `data-theme` из
localStorage или `matchMedia`; пререндеренная разметка остаётся светлой.
- `color-scheme: dark` на корне — нативные контролы (input[type=color], range,
select, скроллбары) темнеют без правок.
- Модуль `theme.svelte.ts` по образцу locale: состояние, персист, применение
атрибута. Стартовую установку делает скрипт из `app.html`, модуль
синхронизирует runes-состояние после гидрации.
## 3. Палитра
- Переопределяем: `--bg`, `--surface`, `--border`, `--text`, `--text-muted`.
- Акцент `#2563eb` не меняется; `--accent-hover` — светлее для тёмного фона.
- `--danger` осветлить для читаемости.
- `--check-a`/`--check-b` шахматки — приглушённые серые: паттерн прозрачности
остаётся узнаваемым, но не слепит.
- `--shadow-card` — почти чёрная с низкой альфой вместо синей.
- Все `color-mix(… var(--surface))` пересчитаются сами — проверить визуально
легенды, recalc-бейдж, drag-оверлеи.
## 4. Этапы и проверки
Каждый этап заканчивается зелёными test/check/build и ревью.
- **A. Инфраструктура** — `theme.svelte.ts`, скрипт в `app.html`, переключатель
в шапке, черновая тёмная палитра. Тесты: персист выбора, игнор мусора в
localStorage, применение атрибута (стаб document).
- **B. Палитра** — выверка значений: герой, каталог, страница инструмента,
цепочка, баннер ошибки, маска, текстовые инструменты; фокус-ринги и hover; обе
локали.
## 5. Смоук
1. Переключение мгновенное, без перезагрузки; выбор переживает её же.
2. При системной тёмной теме первый визит открывается сразу тёмным.
3. Нативные контролы выглядят тёмными.
4. Шахматка различима, легенды и бейджи читаемы.
5. Баннер ошибки и пипетка заметны на тёмных панелях.
## 6. Критерий готовности
Grep: ни одного цвета вне токенов в компонентах и маршрутах.
## 7. Не делаем
Три позиции в переключателе; пользовательские цветовые схемы; автосмену по
времени суток; отдельную тему для превью изображений.
-98
View File
@@ -1,98 +0,0 @@
# План: декомпозиция страницы инструмента, реактивные параметры, предпросмотр маски
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.**
## 1. Зачем
- Страница инструмента (`ToolPage.svelte`) разрослась: состояние процесса,
загрузка файлов, вставка из буфера и разметка трёх зон живут в одном файле.
- Назревают два требования к процессу: параметры применяются автоматически без
кнопки; у части утилит на экране показывается одно — визуализация
фильтра/маски, а скачивается (и в будущем передаётся следующему инструменту)
другое — результат.
## 2. Контракт «результат и маска» в реестре
- `run(img, params)` — как сейчас: возвращает итоговое изображение. Его
скачивают и будут передавать дальше по цепочке.
- Запись реестра получает опциональное поле `preview?(img, params)` — возвращает
картинку для показа на экране (маску/фильтр).
- Если у записи есть `preview`, в карточке результата появляется переключатель
«Показать маску». Включён — на превью маска, выключен (по умолчанию) —
результат. Скачивание всегда отдаёт результат `run`, независимо от положения
переключателя.
- Первая реализация — удаление цвета: в ядро добавляется чистая функция
`colorMask(img, hex, tolerance)` — чёрно-белая маска, где удаляемые пиксели
белые; с юнит-тестами; `preview` записи вызывает её.
## 3. Декомпозиция страницы
Новая папка `components/tool/`; карточки — чисто отображательные, состояние
живёт только в оркестраторе:
- **`SourceCard.svelte`** — левая половина сцены: пусто → зона перетаскивания;
загружено → превью исходника под оверлеем замены с кнопкой «Заменить
изображение».
- **`ResultCard.svelte`** — правая половина: пустые состояния, превью результата
или сводка, чип «Пересчёт…», кнопка скачивания, при наличии `preview` —
переключатель маски.
- **`ParamsCard.svelte`** — форма параметров и подпись «Изменения применяются
автоматически»; для инструментов без параметров не выводится.
- **`ToolPage.svelte`** остаётся оркестратором: состояние (исходник, результат,
сводка, статус, ошибка, значения), декодирование файлов, вставка из буфера,
сброс, токен запусков и эффект с задержкой. Ожидаемо худеет примерно вдвое.
Карточки переиспользуются будущим многошаговым режимом.
## 4. Реактивные параметры (в этом же проходе)
- Кнопки «Применить» больше нет ни у одного инструмента.
- Изменение параметра запускает вычисление через 300 мс после последнего
изменения; каждое новое изменение перезапускает таймер.
- Токен запусков: если во время вычисления параметры поменялись, устаревший
ответ отбрасывается.
- Загрузка и замена файла считают сразу и подавляют ровно один авто-запуск
эффекта.
- Ошибка пересчёта — баннер сверху, прежний результат не сбрасывается.
## 5. Этапы и проверки
- **A. Декомпозиция** — перенос разметки в три карточки один к одному, поведение
не меняется. Проверка: `check/build`, визуально идентично.
- **B. Реактивность** — токен, задержка, чип пересчёта, минус кнопка. Проверка:
смоук §6 пп. 1–6.
- **C. Маска** — типы реестра, `colorMask` с тестами, переключатель в карточке
результата, пример на удалении цвета. Проверка: тесты + смоук §6 пп. 7–9.
- **D. Итог** — полный прогон `test/check/build`.
## 6. Смоук-чеклист
1. Загрузка файла → результат появляется сразу.
2. Изменить параметр → около 0.3 с и результат обновился сам.
3. Быстро изменить яркость несколько раз → одна финальная версия, промежуточные
не мелькают.
4. Во время пересчёта чип виден, предыдущий результат не исчезает.
5. Инструмент без параметров и информационная страница ведут себя как раньше.
6. Некорректные параметры (обрезка вне картинки) → баннер ошибки, прежний
результат цел.
7. Переключатель «Показать маску» у удаления цвета: включён — белым по чёрному
видно, что удалится; выключен — обычный полупрозрачный результат.
8. С включённой маской скачивание всё равно сохраняет прозрачный результат, а не
маску.
9. Замена исходника возвращает переключатель маски в выключенное состояние.
## 7. Критерии готовности
- `ToolPage` содержит только оркестрацию состояния и эффектов; вся разметка
панелей — в `components/tool/*`.
- Кнопки «Применить» нет ни на одной странице.
- Скачивание всегда соответствует `run`; экран может показывать `preview`.
- `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем
- Вынос вычислений в веб-воркер; настраиваемая задержка — фиксированные 300 мс.
- Редактор масок (кисть, слои, история) — только автогенерация из параметров
инструмента.
- Передача результата между инструментами — придёт вместе с многошаговым режимом
позже.
-118
View File
@@ -1,118 +0,0 @@
# План: раскладка «исходник слева — результат справа» + вставка изображения
> **СТАТУС: ВЫПОЛНЕН 2026-08-22.**
>
> **Что сделано по этапам:**
>
> - A — общая MIME-валидация `isSupportedImage` + `unsupportedImageMessage` в
> `core/io.ts`; `DropZone` переведён на них ✔
> - B — постоянная двухпанельная раскладка: одна карточка-«сцена» с равными
> половинами «Исходник | Результат», кнопки внутри своих половин, параметры —
> отдельной карточкой ниже; контент сайта растянут на всю ширину окна ✔
> - C — Ctrl+V: `<svelte:window onpaste>`, поиск image-item в буфере, общая
> валидация; текстовые вставки не затрагиваются ✔
> - D — drag-замена: компонент `DropOverlay` с вуалью-подсказкой и счётчиком
> `dragenter/dragleave` против мерцания; тот же счётчик в `DropZone` ✔
> - E — верификация: тесты 85/85, svelte-check 0 ошибок, статический билд с 11
> страницами; ручной смоук-чеклист §5 пройден ✔
>
> **Отступления от исходного плана в ходе реализации:** вместо пропа `overlay` у
> `DropZone` сделан отдельный компонент-обёртка `DropOverlay` (drag-события
> ловит контент под курсором); раскладка доработана по ревью — одна карточка
> равных половин вместо двух независимых колонок, сайт без ограничения ширины.
## 1. Целевое поведение
1. **Постоянная двухпанельная раскладка** страницы инструмента — отдельного
состояния «страница загрузки» больше нет:
- **Слева — исходник**, панель есть всегда. Не загружено → пустое дроп-поле
(drag&drop или клик). Загружено → превью исходника.
- **Справа — результат**: до загрузки — `EmptyState` «Результат появится
здесь»; во время обработки — `EmptyState` «Обработка…»; после — превью
результата (для info-инструмента — `InfoPanel`).
2. **Ctrl+V** — картинка из буфера обмена вставляется как исходник с любого
места страницы.
3. **Замена перетаскиванием** — новый файл, брошенный на левую панель с
загруженной картинкой, заменяет исходник (с оверлеем-подсказкой во время
перетаскивания).
4. **Кнопка «Заменить»** под превью исходника (переносится туда текущая
«Загрузить другое изображение»).
На мобильной ширине панели складываются в столбец: исходник сверху, результат
снизу.
## 2. Изменения по файлам
Новые компоненты не вводим — `EmptyState`, `Preview`, `DropZone` покрывают всё.
Меняем три файла:
- **`core/io.ts`** — экспорт `isSupportedImage(file): boolean`: общая
MIME-проверка на базе `ACCEPTED_IMAGE_TYPES`.
- **`DropZone.svelte`** — переходит на `isSupportedImage`; новый опциональный
проп `overlay` — режим «невидимая зона приёма поверх контента» для замены
перетаскиванием.
- **`ToolPage.svelte`** — постоянная сетка из двух панелей;
`<svelte:window onpaste>`; drag-оверлей левой панели; кнопки замены/сброса
переезжают в левую панель; правая панель — три состояния через `EmptyState`.
## 3. Этапы и проверки
- **A. Общая валидация** — `isSupportedImage` в `io.ts`, `DropZone` переведён на
неё. Проверка: `pnpm check`.
- **B. Раскладка** — две постоянные колонки в ToolPage; правая панель через
EmptyState; кнопки уходят в левую панель; обёртка `{#if !source}` на всю
страницу убирается. Проверка: визуальный осмотр всех инструментов,
`check/build`.
- **C. Ctrl+V** — `onpaste` на окне → поиск image-item в `clipboardData` → тот
же `handleFile`; не-изображения игнорируются молча. Проверка: ручной смоук со
скриншотом из буфера.
- **D. Drag-замена** — оверлей DropZone(overlay) при dragover на левой панели;
drop → `handleFile` нового файла. Проверка: ручной смоук «бросить файл на
загруженный исходник».
- **E. Итог** — полный ручной смоук-чеклист (§5). Проверка: `test/check/build`
зелёные.
## 4. Нюансы и решения
- **Мерцание dragleave** — классическая проблема оверлеев: используем счётчик
`dragenter/dragleave`, оверлей скрывается только когда счётчик обнулился и
произошёл `drop`/`dragend`.
- **Paste при фокусе в поле параметров** — обрабатываем всегда: если в буфере
именно изображение, пользователь очевидно хочет заменить исходник; текстовые
вставки обработчик не трогает.
- **Форматы буфера** — скриншоты приходят как PNG, часть приложений отдаёт
JPEG/WebP; все проходят общую `isSupportedImage`. GIF/BMP из буфера редки, но
тоже допустимы.
- **Info-инструменты** — ведут себя так же: слева исходник, справа после
загрузки появляется `InfoPanel`.
- **Состояние ошибки** — баннер остаётся сверху страницы; неудачная замена не
сбрасывает старый исходник.
## 5. Чеклист ручного смоука
1. Пустые обе панели → drag&drop файла слева → появился исходник и результат.
2. Клик по пустой левой панели → выбор файла.
3. Ctrl+V со скриншотом → исходник заменился, результат пересчитался.
4. Drag файла на загруженную левую панель → оверлей виден, после drop — замена.
5. Отмена перетаскивания (Esc / мимо) → оверлей скрылся, исходник не изменился.
6. Бросок неподдерживаемого файла (например .txt) → баннер ошибки, старый
исходник цел.
7. Кнопка «Заменить» открывает выбор файла.
8. png-info: работает та же схема, справа сводка.
9. Мобильная ширина: панели в столбец, порядок исходник → результат.
10. Все 11 инструментов отвечают на замену исходника корректным пересчётом.
## 6. Критерии готовности
- На странице инструмента нет состояния «только дропзона на всю страницу» —
раскладка двухпанельная всегда.
- Загрузка работает тремя способами: клик, drag&drop (включая замену), Ctrl+V.
- Ошибочные файлы и отмена перетаскивания не портят текущий исходник.
- `test/check/build` зелёные; чеклист §5 пройден.
## 7. Что сознательно не делаем (граница скоупа этого плана)
- Множественная загрузка / пакетная обработка — вне скоупа этого плана.
- Камера/скриншот-инструменты захвата — только буфер обмена.
- Прогресс-бары декодирования — файлы читаются быстро, достаточно индикации
«Обработка…».
-130
View File
@@ -1,130 +0,0 @@
# План: главная страница как рабочая область с цепочкой
> **СТАТУС: ВЫПОЛНЕН 2026-08-23.** Заменяет идею отдельной страницы рабочего
> пространства.
## 1. Модель
1. **Главная страница — это рабочая область.** Без выбранного инструмента на ней
большой поиск: начните вводить название — список отфильтровывается; выбор
разворачивает инструмент прямо здесь, без перехода.
2. Развернутый инструмент выглядит как привычная страница: слева исходник,
справа результат со своей кнопкой скачивания, параметры — в отдельной
карточке под сценой.
3. Под результатом рядом с кнопкой скачивания — кнопка **«⛓ Chain with…»**:
добавляет пустую секцию, внутри которой такой же поиск; выбор заполняет
секцию следующим инструментом. Так цепочка растёт сколько угодно.
4. Каждая заполненная секция цепи — полноценная: свои параметры, своё превью,
своя кнопка скачивания промежуточного результата. Крестик убирает секцию;
кнопка «✂ Remove chain» снимает всю цепь разом (без подтверждения).
5. Чекбокс «Показать маску» живёт в карточке параметров базового инструмента и
влияет только на его превью: в цепь всегда передаётся сам результат.
6. Сетка-каталог инструментов переезжает на отдельную страницу `/list-tools` —
для SEO и визуального поиска. Основная работа происходит на главной; в шапке
появляются ссылки на обе.
7. Прямые адреса `/tools/<id>` сохраняются: тот же развернутый инструмент для
глубоких ссылок.
8. Цепочка переживает перезагрузку (автосохранение в localStorage).
Экспорт/импорт файлом отложен до появления CLI/wasm-части.
## 2. Архитектура
- Страница инструмента уже собрана как самодостаточный компонент, принимающий
запись реестра, — он и становится встраиваемым блоком. Маршруты `/tools/[id]`
и главная используют один и тот же компонент: первая — по адресу из URL,
вторая — по выбору из поиска.
- Состояние главной: выбран ли инструмент; если да — рендерится блок
инструмента, целиком владеющий своим исходником, параметрами, результатом и
цепочкой.
- Смена инструмента на главной (через повторный поиск) сбрасывает блок целиком.
- Цепочка хранится массивом шагов; у пустого звена инструмент ещё не выбран —
такие звенья не сохраняются в localStorage и не участвуют в прогоне.
- Исполнение — последовательная свёртка: выход каждого звена — вход следующего;
результаты всех звеньев доступны их секциям. Общий авто-раннер даёт токен
отмены и задержку ~300 мс.
## 3. Переиспользование и доработки существующего
Готовое берётся как есть: `SourceCard`, `Preview`, `ParamForm`,
`DownloadButton`, `EmptyState`, модуль пайплайна, авто-раннер, `PNG_OUTPUT`.
Доработки:
- **Блок страницы инструмента** — расширяется секцией цепочки: список
заполненных звеньев, каждое со своими параметрами, превью и скачиванием, плюс
пустые звенья с поиском. Кнопки «⛓» и «✂ Remove chain» — рядом со скачиванием
базового результата.
- **`ParamsCard`** — опциональный блок «Показать маску» (флаг preview у
инструмента + двусторонний флаг).
- **Карточка результата** — упрощается: получает готовую картинку, маска уходит
в карточку параметров.
- **Существующая главная-каталог** — переезжает на маршрут `/list-tools` почти
без изменений.
Удаляется: черновой маршрут рабочего пространства и его компоненты
списка/селектора — вместо них новые, под вертикальную модель.
## 4. Новые компоненты
- **`ToolSearch.svelte`** — поиск с нечётким совпадением: поле ввода,
отфильтрованный список (регистронезависимая подпоследовательность по названию
и описанию, приоритет совпадению с начала), клавиатура ↑↓ Enter Esc; два
размера — герой на главной и компактный внутри пустого звена. Предлагает
только инструменты-трансформеры.
- **`ChainStep.svelte`** — секция одного звена: пустое звено показывает
компактный поиск; заполненное — заголовок с номером и крестиком, форму
параметров, превью результата, кнопку скачивания.
- **`ChainBar.svelte`** — строка под результатом базового инструмента: «⛓ Chain
with…» и «✂ Remove chain» (видна только при непустой цепи).
## 5. Этапы и проверки
- **A. Поиск и главная** — `ToolSearch`, герой поиска на главной, встраивание
блока инструмента, перенос каталога на `/list-tools`, ссылки в шапке.
Проверка: `check/build`, визуально.
- **B. Цепочка** — состояние цепи в блоке инструмента, пустые звенья с поиском,
прогон, промежуточные превью и скачивания, ошибки звеньев. Проверка: смоук §8
пп. 1–6.
- **C. Маска и упрощение** — блок маски в `ParamsCard`, упрощение карточки
результата. Проверка: смоук §8 п. 7.
- **D. Хранение и итог** — автосохранение и восстановление цепочки, удаление
чернового маршрута рабочего пространства, полный регресс каталога,
`test/check/build`.
## 6. Смоук-чеклист
1. На главной — большое поле поиска; ввод фильтрует список по мере набора; Enter
выбирает подсвеченный.
2. Выбранный инструмент разворачивается на той же странице: исходник, параметры,
результат.
3. «⛓ Chain with…» добавляет пустую секцию с компактным поиском; выбор заполняет
её.
4. У каждой секции своё превью и своё скачивание промежуточного результата.
5. Крестик убирает секцию; «✂ Remove chain» снимает все звенья разом.
6. Ошибка звена — баннер «Шаг N (имя): …»; последние успешные результаты секций
не исчезают.
7. «Показать маску» переключает превью базового инструмента; в цепь идёт
результат, скачивание базового отдаёт результат, не маску.
8. Перезагрузка восстанавливает цепочку заполненных звеньев; пустые звенья при
этом исчезают.
9. Быстрые правки подряд — один финальный пересчёт всей вертикали.
10. `/list-tools` открывает старый каталог по категориям; прямые адреса
`/tools/<id>` работают.
## 7. Критерии готовности
- Работа начинается с главной: поиск → инструмент → цепочка звеньями, каждый со
своим превью и скачиванием.
- Отдельной страницы рабочего пространства нет; каталог доступен на
`/list-tools`; прямые ссылки инструментов работают.
- Пустые звенья не попадают в сохранение и не ломают прогон.
- `test/check/build` зелёные, смоук §6 пройден.
## 8. Что сознательно не делаем
- Ветвления и прицепывание от середины цепи — новое звено всегда добавляется в
конец.
- Пипетка и маски внутри звеньев цепи — они остаются фичей базового инструмента.
- Экспорт и импорт цепочки файлом — отложено до появления CLI/wasm-части.
- Несколько параллельных инструментов на главной одновременно — один активный
блок за раз.
+77 -99
View File
@@ -1,22 +1,30 @@
# Backlog # Backlog
> Статус: **active**. Продуктовые, UX- и инфраструктурные задачи. Здесь только
> **открытые** задачи: выполненные удаляются, а критичные для проекта факты из
> них переносятся в тематический документ (архитектура, quality gates, lint,
> e2e) по мере закрытия.
>
> «Что сознательно не вводим и почему» живёт в `docs/decisions.md`, архитектура
> — в `docs/architecture.md`. Raw-находки UX-аудита — `docs/tools-audit.md`, их
> разбивка на волны — `docs/plan-fields-audit-fixes.md`.
>
> Ids инструментов сверять с `web/src/lib/registry/`; полный список —
> `docs/tools-map.md`.
## lint и прочие правила ## lint и прочие правила
- [x] Кросс-языковой линтер словарей (warn-only: паритет ключей, пустые - [ ] расширить `i18n/no-hardcoded-user-text` на `web/src/routes/**`: сейчас
значения, совпадение `{placeholder}`-переменных между всеми локалями) — правило навешено только на `components/**` (без `/kit`), поэтому
правило `i18n/dict-consistency`, детали в `web/eslint-plugins/README.md`. захардкоженный текст в `routes/**` проходит молча (`web/eslint.config.js`,
Правило реализовано; Q2 перевёл i18n baseline к zero-warning через `web/eslint-plugins/README.md`)
policy-aware fallback.
- [x] Проверка текстов в svelte/ts/html — реализовано правилом
`i18n/no-hardcoded-user-text` (warn, baseline 0): ловит текст в шаблоне и
в пользовательских атрибутах, исключения — бренд, форматы, имена файлов,
registry-строки и `/kit`. Детали в `web/eslint-plugins/README.md`.
- [ ] `tseslint.config(...)` deprecated: сигнатура - [ ] `tseslint.config(...)` deprecated: сигнатура
`(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена `(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена
устаревшей, нужен переход на `defineConfig` из `typescript-eslint` — устаревшей, нужен переход на `defineConfig` из `typescript-eslint` —
`web/eslint.config.js`. `web/eslint.config.js`.
- [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей; - [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей;
до этого Q2 держит pattern-based policy-исключение до этого действует pattern-based policy-исключение
(`web/eslint-plugins/README.md`, раздел `dict-consistency`)
## Мелочи всякие ## Мелочи всякие
@@ -90,14 +98,13 @@
## Переезд ## Переезд
- [x] добавить i18n в preview
- [ ] множественные числа (часть/части/частей; «N параметров», счётчики) — - [ ] множественные числа (часть/части/частей; «N параметров», счётчики) —
`tPlural()` и формат `PluralForms`; детали и критерии — в этом backlog `tPlural()` и формат `PluralForms`; детали и критерии — в этом backlog
## SEO/GEO ## SEO/GEO
Общий план: `docs/plan-seo.md` (S0–S4). C17 выполнен; старт возможен — пока Общий план: `docs/plan-seo.md` (S0–S4). Переезд preview на реальные маршруты
только domain-free часть S1. выполнен; старт возможен — пока только domain-free часть S1.
- [ ] переименование бренда + домен: чек-лист в локальном плане - [ ] переименование бренда + домен: чек-лист в локальном плане
`docs/plan-domain.local.md` (вне git, имя домена в репо не публикуется; `docs/plan-domain.local.md` (вне git, имя домена в репо не публикуется;
@@ -125,38 +132,12 @@
## Качество production UI ## Качество production UI
Q5–Q6 и Q7–Q8 закрывают известные preview-проблемы, special fixtures, smoke, Политика проверок — `docs/quality-gates.md`, e2e — `docs/testing-strategy.md`.
layout/view-model инварианты, browser matrix, coverage-гейт и линтеры; активных
`test.fixme` в e2e больше нет.
- [x] Генераторы auto-run без кнопки Generate — `web/e2e/generators.spec.ts`.
- [x] `resize-png` и `crop-png` используют размеры текущего source через общий
source-aware schema resolver — `web/e2e/pipeline.spec.ts`.
- [x] Reset сохраняет source/text, восстанавливает defaults и запускает deferred
auto-run — `web/e2e/pipeline.spec.ts`.
- [x] Ошибки хранят i18n key/vars и переводятся при выводе; изменение языка
обновляет alert — `web/e2e/i18n.spec.ts`.
- [x] Malformed/special PNG fixtures: CRC, truncated, palette и 16-bit проходят
через upload без краша — `web/e2e/png-fixtures.spec.ts`.
- [x] Registry-wide smoke покрывает все 122 инструмента по input/result-типу —
`web/e2e/tools-smoke.spec.ts`, `generators.spec.ts`,
`text-and-verdicts.spec.ts`.
- [x] Layout-группировка вынесена в `resolveLayoutGroups` и покрыта
unit-тестами; `SchemaFields.svelte` использует общий seam.
- [x] `SchemaPreview` view-model вынесена в `buildSchemaPreviewModel` и покрыта
unit-тестами для всех input/result-типов.
- [x] Browser matrix: полный Chromium, Firefox/WebKit для browser-critical
спецификаций и mobile Chromium 390×844 — `web/playwright.config.ts`.
- [x] Расхождение декодеров зафиксировано как ожидаемое поведение: Firefox и
WebKit декодируют `truncated-no-iend` и `idat-cut`, а WebKit ещё и
`crc-bad-idat`. Таблица движков живёт в `web/e2e/png-fixtures.spec.ts`,
продукт не ужесточается, и любая выгрузка битого файла обязана давать
alert, а не падение или «пустой» результат.
- [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного - [ ] Component-тесты `SchemaToolView`/`SchemaPreview` требуют отдельного
решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение. решения: node Vitest не имеет DOM/canvas, а e2e уже покрывает поведение.
- [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается - [ ] Ошибка декодирования изображения из `createImageBitmap` пока показывается
сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ. Это сырым сообщением браузера; нужен `ToolError`-маппер и i18n-ключ.
продуктовая задача, не Q6d.
## Идеи ## Идеи
@@ -178,7 +159,7 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив 4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
chain по каждому файлу, сборка результата обратно в zip для скачивания. chain по каждому файлу, сборка результата обратно в zip для скачивания.
Зависит от п.4 (выбор цепочки) и от экспортного формата pipeline JSON; Зависит от идеи 3 (выбор цепочки) и от экспортного формата pipeline JSON;
прогресс и ошибки — пофайлово. прогресс и ошибки — пофайлово.
5. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на 5. **Избранные инструменты (fav tools)** — оценка S/M. Тоггл-звёздочка на
@@ -188,59 +169,48 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download** 6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download**
— оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др. — оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др.
форматы) не должны жить как самостоятельные инструменты цепочки. Вместо этого форматы) не должны жить как самостоятельные инструменты цепочки. Механическая
— бесшовное внедрение выбора формата/качества прямо в кнопку/диалог часть уже есть: `ToolEntry.output` (`OutputFormat`: mime/ext/qualityParamId)
**Download**: юзер скачивает результат в нужном формате (mime/ext/quality), а и кодирование по нему в `SchemaToolView.download()` — `docs/architecture.md`,
pipeline при этом не усложняется лишним звеном. **Сделано (2026-09):** раздел 3. Осталось (UX-райз «Download»):
механическая часть реализована в preview — `ToolEntry.output`
(`OutputFormat`: mime/ext/qualityParamId) в `web/src/lib/registry/types.ts`;
`SchemaToolView.download()` кодирует по `output` (jpg/webp/bmp уже настроены;
`png-to-bmp` переведён сюда же). Q6c покрывает этот фиксированный контракт
unit/E2E-проверками; переключатель форматов и quality остаётся отдельным
UX-райзом. **Осталось (UX-райз «Download»):**
- выбор формата/качества прямо в кнопке/диалоге Download для любого - выбор формата/качества прямо в кнопке/диалоге Download для любого
результата (не отдельными конвертерами); результата (не отдельными конвертерами);
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png` - экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
(целевой KB, бинарный поиск по k из квантования). Проработать вместе с (целевой KB, бинарный поиск по k из квантования). Проработать вместе с
достройкой download-флоу нового UI. достройкой download-флоу нового UI.
7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Отложен из 7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна
миграции (`archive/plan-migrate-remaining.md`). Нужна overlay-механика: overlay-механика: второй источник изображения (getOverlay/store), параметры
второй источник изображения (getOverlay/store), параметры scale/opacity/ scale/opacity/position/margin/aspect. Решается вместе с вопросом, как второй
position/margin/aspect. Решается вместе с вопросом, как второй источник источник картинки вписывается в schema-driven preview (`SchemaToolView`).
картинки вписывается в schema-driven preview (`SchemaToolView`).
8. **`png-info` (детальная информация о PNG)** — оценка M/L. Отложен из 8. **`png-info` (детальная информация о PNG)** — оценка M/L. Отдельный райз:
миграции. Отдельный райз: exif-теги, редактирование, структурированный вывод; exif-теги, редактирование, структурированный вывод; нужен свой отдельный
нужен свой отдельный случай в UI, не «ещё один text/verdict». возможно нужна случай в UI, не «ещё один text/verdict». Возможно нужна вообще отдельная
вообще отдельная страница, несовместимая с другими инструментами: 1) она страница, несовместимая с другими инструментами: 1) она работает не с
работает не с изображением, а с мета данными из файла, 2) она не может быть изображением, а с метаданными из файла, 2) она не может быть частью цепочки,
частью цепочки, т.к. результат не является изображением, 3) если надо т.к. результат не является изображением, 3) если надо изменить теги, никакого
изменить теги, никакого форматирования и изменения в изображение вносить не форматирования и изменения в изображение вносить не надо, только в
надо, только в мета информацияю метаинформацию.
9. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/ 9. **Region-инструменты** (censor/erase/pixelate-area/blur-area/sharpen-area/
reverse-area) — ждут UI выделения области на превью. Из reverse-area) — ждут UI выделения области на превью. Вне очереди.
`archive/plan-gap-waves.md` («вне очереди»).
10. **Мультифайловые инструменты-потребители** (gif-to-frames, separate-colors) 10. **Мультифайловые инструменты-потребители** (gif-to-frames, separate-colors)
— механизм «результат = набор файлов» (1 → many) реализован: `FileResult` + — механизм «результат = набор файлов» (1 → many) уже есть
`result: "files"` + скачивание zip-архивом + грид-превью частей. Первый (`docs/architecture.md`, раздел 7), первый потребитель —
потребитель — `split-into-parts-png`. Остальные подключаются по мере нужды. `split-into-parts-png`. Остальные подключаются по мере нужды.
11. **Анимационные** (slow-reveal/fade/scrolling) — выход не PNG; отдельное 11. **Анимационные** (slow-reveal/fade/scrolling) — выход не PNG; отдельное
решение о формате. Из `archive/plan-gap-waves.md`. решение о формате.
12. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital). Из 12. **HARD-серия** (glitch, barcode, signature-extract, handwritten-digital).
`archive/plan-gap-waves.md`.
13. **Контраст текста на акцентных кнопках — частично закрыт**. Для dark-темы 13. **Контраст текста на акцентных кнопках — частично закрыт**. Тона
`--color-main`/`--color-accent` подняты до tone 56, чтобы пройти текущий `--color-main`/`--color-accent` подняты под текущий порог `palette.test.ts`;
порог `palette.test.ts` 4.0; targeted-тест проходит. Light-тема сохраняет targeted-тест проходит. Light-тема всё ещё ниже AA 4.5 для обычного текста,
контраст ~4.1–4.3, что ниже AA 4.5 для обычного текста, поэтому после а `--color-danger` в light-теме на границе: после завершения редизайна
завершения редизайна нужно повторно поднять тона и вернуть порог 4.5. Также вернуть порог 4.5 и перепроверить оба токена.
проверить `--color-danger` (dark, тон 55): текст на нём 4.93 — ок, но
light-danger tone 48 = 4.25 на границе.
14. **Отображение маски для инструментов (runMask/preview-mask)** — придумать и 14. **Отображение маски для инструментов (runMask/preview-mask)** — придумать и
реализовать процесс показа маски результата. **Контекст:** в старом реализовать процесс показа маски результата. **Контекст:** в старом
@@ -252,25 +222,32 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
возможность вернуть (или инструмент — объявить) `runMask` — ч/б изображение возможность вернуть (или инструмент — объявить) `runMask` — ч/б изображение
маски изменений либо иной подход (отдельное поле `mask` + функция маски, маски изменений либо иной подход (отдельное поле `mask` + функция маски,
второй результат, overlay). Оценка — на усмотрение при проработке (S/M). второй результат, overlay). Оценка — на усмотрение при проработке (S/M).
Пока живём без неё. **Идея 2:** каждый `run` возвращает два изображения - Пока живём без неё. **Идея 2:** каждый `run` возвращает два изображения —
для отображения (показывает маску или сам результат), и для передачи в для отображения (показывает маску или сам результат) и для передачи в
следующий инструмент (всегда результат. Оценка — на усмотрение при следующий инструмент (всегда результат). Оценка — на усмотрение при
проработке (S/M). Пока живём без неё. проработке (S/M). Пока живём без неё.
15. **Процесс работы с инструментами 1 → many и many → 1** — абстракция 15. **Набор файлов в пайплайне: 1 → many не должен быть терминальным** — оценка
результата «набор файлов» оформлена и работает: тип `FileResult`, L. **Сейчас** `execute` принимает один `source: PixelImage | undefined` и
`result: "files"` в реестре, сериализация через executor, скачивание возвращает один `ToolResult`, а `SchemaToolView` раскладывает вывод в
zip-архивом, грид-превью частей. Реализовано на первом потребителе `result` / `fileResult` / `textResult` по `resultKind` — поэтому шаг с
`split-into-parts-png` (2026-09, `docs/archive/plan-split-into-parts.md`). `result: "files"` упирается в конец цепочки. **Направление:** следующий
Q6b закрывает контракт ZIP/download: unit-тесты `web/src/lib/zip.test.ts` и инструмент применяется **ко всем картинкам** предыдущего результата
browser-сценарий `web/e2e/pipeline.spec.ts` проверяют имена частей и (fan-out), а many → 1 (вход = архив или несколько файлов) — обратная сторона
содержимое архива. **1 → many:** подключение остальных потребителей того же контракта. Терминальными остаются только результаты, которые не
(gif-to-frames, separate-colors) при необходимости; формат/качество файлов картинка (text, verdict). Что обдумать:
внутри zip — отложено до UX-райза «Download» (п.7). **many → 1** (вход = - где проходит граница: один вызов `run(ctx)` на файл (имя части склеивается
архив/несколько картинок → единый результат): не реализовано. Зависит от в `FileResult.files`) или батч целиком на стороне исполнителя;
batch-обработки архивов (п.5 идей, roadmap фаза 8 «batch-страница»). Нужна `executor/protocol.ts` `files` уже сериализуются, но при батче нужен
отдельная проработка входа (распаковка zip в браузере, порядок файлов, прогресс и отмена;
поведение в пайплайне — такие инструменты терминальны, как и 1 → many). - частичные ошибки: один битый файл не должен ронять весь шаг — нужен
per-file отчёт, а не один `ToolError` на шаг;
- имена частей на каждом шаге и Download для набора (сейчас zip по
`tool.id`, `SchemaToolView.download()`);
- обратная сторона: распаковка zip в браузере, порядок файлов, поведение
many → 1 → many в одной цепочке. Зависит от идеи 10 (остальные потребители
1 → many) и идеи 4 (batch по архиву), `docs/roadmap.md` фаза 8
«batch-страница».
16. **Главная должна отличаться от каталога** — оценка S. Сейчас 16. **Главная должна отличаться от каталога** — оценка S. Сейчас
`src/routes/+page.svelte` и `src/routes/list-tools/+page.svelte` побайтово `src/routes/+page.svelte` и `src/routes/list-tools/+page.svelte` побайтово
@@ -278,5 +255,6 @@ layout/view-model инварианты, browser matrix, coverage-гейт и л
рабочий маршрут: на него ссылается крошка в `+layout.svelte` и он открыт рабочий маршрут: на него ссылается крошка в `+layout.svelte` и он открыт
e2e-сценариями `catalog.spec.ts`, `i18n.spec.ts`, `navigation.spec.ts`. e2e-сценариями `catalog.spec.ts`, `i18n.spec.ts`, `navigation.spec.ts`.
Замысел: главная — крупнее и с более простым поиском, без категорий; каталог Замысел: главная — крупнее и с более простым поиском, без категорий; каталог
остаётся группировкой с фильтрами. Найдено измерением дублирования разметки остаётся группировкой с фильтрами. Детектор дублирования разметки
по 65 `.svelte`-файлам: файлы побайтово идентичны. сознательно не вводим — `docs/decisions.md`, раздел 1, поэтому расхождение
ловится на ревью.
+5 -36
View File
@@ -96,41 +96,10 @@
корректный (не «кракозябры»). корректный (не «кракозябры»).
- [ ] remove-background на сложной полупрозрачности — края не «звенят». - [ ] remove-background на сложной полупрозрачности — края не «звенят».
## G. Закрытые Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2/Q6f.3-проблемы
> Q5/Q6b/Q6c/Q6d/Q6e/Q6f.1/Q6f.2/Q6f.3-проблемы закрыты автоматическими тестами;
> для ручной проверки остаются субъективные сценарии разделов A–F.
- [x] **Генераторы:** auto-run по дефолтам, результат и изменение параметра без
кнопки Generate. `web/e2e/generators.spec.ts`.
- [x] **resize-png/crop-png:** размеры текущего source используются по
умолчанию, alert не появляется. `web/e2e/pipeline.spec.ts`.
- [x] **Reset:** defaults восстанавливаются, source сохраняется, deferred
auto-run завершается результатом. `web/e2e/pipeline.spec.ts`.
- [x] **Локализация ошибок:** alert хранит key/vars, переводится при выводе и
обновляется при смене языка. `web/e2e/i18n.spec.ts`.
- [x] **FileResult → ZIP:** `split-into-parts-png` скачивает
`split-into-parts-png.zip` с именованными PNG-частями; содержимое и
сигнатуры проверяются `web/src/lib/zip.test.ts` и
`web/e2e/pipeline.spec.ts`.
- [x] **Output MIME/quality:** JPG/WebP/BMP имеют ожидаемые расширения и
сигнатуры, quality 10/90 меняет размер файла; `web/e2e/pipeline.spec.ts`.
- [x] **Malformed/special PNG:** CRC, truncated, palette и 16-bit fixtures
проходят через upload без краша; `web/e2e/png-fixtures.spec.ts`.
- [x] **Registry smoke:** все 122 production tools имеют специализированный
browser smoke по input/result-типу; `web/e2e/tools-smoke.spec.ts`,
`generators.spec.ts`, `text-and-verdicts.spec.ts`.
- [x] **Layout resolver:** `resolveLayoutGroups` покрыт unit-тестами; UI
группировка полей использует тот же seam.
- [x] **Preview model:** `buildSchemaPreviewModel` покрывает source/result
values, формат и `hasResult` для image/text/verdict/files.
- [x] **Browser matrix:** pipeline, verdicts и PNG-fixtures проходят в Firefox и
WebKit, mobile viewport 390×844 — в Chromium; отказ декодера битого PNG
зафиксирован per-engine. См. раздел 7 `docs/testing-strategy.md`.
## Как долго ## Как долго
- Полный прогон: ~40 минут (все разделы, включая мегапиксели и 4К) в двух - Полный прогон: все разделы A–F, включая мегапиксели и 4К, в двух браузерах.
браузерах. - Быстрый смоук: A (кроме мегапикселей), B (выборочно), C, F выборочно.
- Быстрый смоук (< 15 мин): A (кроме мегапикселей), B (выборочно), C, F
выборочно, G обязательно. > Сценарии, закрытые e2e-тестами, в этот чек-лист не попадают: их покрытие видно
> по спецификациям в `web/e2e/`, политика e2e — `docs/testing-strategy.md`.
+155
View File
@@ -0,0 +1,155 @@
# Решения по гейтам и тестам
> Статус: **active**. Журнал решений «что сознательно НЕ вводим» и «как читаем
> результат CI». Только про гейты, линтеры, coverage, e2e и CI — **для обычных
> правок production-кода не нужен**. Читать, когда проектируется или меняется
> сам гейт, либо когда спрашивают «почему так, а не иначе». Оперативные команды
> и пороги — в `docs/quality-gates.md`, правила нагрузки на машину — в
> `docs/agent-workflow.md`, раздел 3.
Здесь лежат решения, чтобы не пересматривать их заново. Для каждого: причина и
то, что решение пересмотрело бы. В `docs/backlog.md` такие пункты не
дублируются: backlog живёт задачами, а не историей решений.
Числа ниже — разовые замеры, а не текущие значения: помечены датой. Актуальные
цифры даёт прогон (`docs/quality-gates.md`), по правилу `AGENTS.md` в документах
нет величин, которые меняются вместе с кодом.
## 1. Детектор дублирования разметки — не вводим
Ни свой AST-правило, ни готовая зависимость (jscpd, `eslint-plugin-sonarjs`).
- Замер по корпусу `.svelte` в `components/**` и `routes/**` (сентябрь 2026):
при пороге 5 нормализованных строк — одна осмысленная пара, при пороге 3 — 17
пар, и все они шум (`</label>`, `{/each}`, `{#if children}`). Семейство
schema-контролов делит максимум 4 строки — законное сходство by design.
- Причина: построчное сравнение Svelte-разметки принципиально ненадёжно —
одинаковый контейнер с разным содержимым даёт ложное срабатывание, а намеренно
общий класс — ложную тишину. Чтобы не ловить шум, порог пришлось бы поднять до
12+ строк, и тогда правило срабатывает ровно один раз.
- Контроль остаётся на ревью (расхождение видно в диффе) и на поведенческих
e2e-проверках; извлечение общей разметки закреплено конвенцией в `AGENTS.md`.
- Пересмотреть, если появится реально «уехавшая» копипаста в компонентах или
библиотека разрастётся на уровень композиций страниц.
## 2. Линтер согласованности комментариев — не вводим
Описания инструментов различаются осознанно, и ловить это текстовым правилом
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
`TODO`/`FIXME` перенесены в backlog, незарегистрированных пометок в коде нет;
новые запрещены правилом в `AGENTS.md`.
## 3. Линтер «изменяемых чисел» в документации — не вводим
Правило «не фиксировать числа, которые меняются вместе с кодом» живёт в
`AGENTS.md` и держится на ревью. Автоматизация отклонена по замеру (сентябрь
2026): regex «число + существительное» на корпусе Markdown дал 15 совпадений, из
которых 14 — ложные («30 дней» политики архива, «9 опций» описания поля, «4
проекта» факт из `playwright.config.ts», цитата самого правила).
- Правило всё равно неполно: ловит «122 инструмента», но не «инструментов —
122», не «более 100», не «≈120»; на каждое обходное движение нужна правка
правила, а allowlist легитимных чисел становится вторым источником долга.
Худший исход — не «шумно», а «вроде гейт, а толку нет»: срабатывания на пустом
месте забейзелят, и через месяц правило перестанет ловить настоящие нарушения.
Видимое блокирующее правило, которое на деле ничего не проверяет, хуже
отсутствия правила.
- Соблюдение правила дешевле обхода: «список — в `docs/tools-map.md`» стоит
столько же символов, сколько цифра, но не требует обновления.
- Пересмотреть, если появится источник истины, из которого число можно
**вычислить** и сравнить автоматически, без разбора прозы. Такой случай есть и
решён отдельно — следующий пункт.
## 4. Сверка `docs/tools-map.md` с реестром — автоматизирована
Правило про числа не закрывает более опасный класс расхождений: документ не
устарел числом, а **перечисляет то, чего нет в коде**. В разделе «Реализовано»
карты лежали несуществующие id (`png-info`, `color-wheel-generator`,
`jpg-to-png`, `watermark-image-png`), которые агент принял бы за готовые
инструменты. Счёт при этом был верный — число такой класс не ловит.
Сверка множеств отвечает на вопрос однозначно и без allowlist, поэтому
автоматизирована: `web/src/lib/registry/tools-map-doc.test.ts` парсит раздел
«Реализовано» и сравнивает id с `TOOLS` в обе стороны (id без инструмента и
инструмент без записи), плюс ловит повтор id между буллетами. Формат строки
карты — всё до первого тире, id через `/` или `,` — закреплён в самом документе
и в тесте.
Чтобы сломанный парсер не выглядел как успех, у теста есть отдельная проверка
«разбор не пустой»: если формат карты снова поменяется, тест упадёт с сообщением
про парсер, а не молча перестанет что-либо проверять. Входит в обычный
`pnpm --dir web test`; новых гейтов и конфигов не добавляет.
## 5. Visual snapshots — не вводим
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита, а не
с замыслом дизайна. При активно меняющемся визуальном языке это превращает
каждый дизайн-PR в регенерацию и ревью картинок, а baseline платформенные
(снимок с Windows не совпадёт на `ubuntu-latest`). Потребность закрывают
e2e-поведение, `lint:tokens` с token audit, i18n-гейт и ручной чеклист
`docs/checklist-manual-testing.md`.
Вернуться стоит, когда визуальный язык стабилизируется; тогда первым идёт `/kit`
в двух темах, а baseline рождаются на Linux.
## 6. Fail-режим для unused-token аудита — не вводим
Остаётся предупреждением: `verify` входит в deploy-workflow, поэтому стилевой
долг блокировал бы деплой сайта. Кроме того, аудит видит только ссылки
`var(--x)`, и чтение токена из JS дало бы ложное срабатывание. Правило «новый
токен — вместе с потребителем» живёт в `AGENTS.md`.
## 7. Per-project `expect.timeout` — не задаём
Firefox и WebKit отдают результат так же быстро, как Chromium: расхождения
объясняются памятью, а не скоростью движка. Таймаут под конкретную машину —
магическое число, которое придётся подстраивать на каждом раннере, поэтому
Playwright-конфиг остаётся с дефолтами, а проблема решается лимитом воркеров.
Практические правила — `docs/agent-workflow.md`, раздел 3.
## 8. Дублирование `verify` между workflow — сознательно
`Deploy to GitHub Pages` и `Quality` оба зовут `verify`: deploy проверяет
быстрый gate и собирает сайт, `Quality` прогоняет тот же `verify` и browser
matrix. По замеру (сентябрь 2026) `verify` — единицы минут, matrix — около трёх,
поэтому дублирование дешевле условных job-и, а matrix дёшево держать на каждом
PR. Актуальные длительности — в логе прогона. Ручной
`gh workflow run quality.yml` нужен только для перепрогона старого коммита.
## 9. Политика e2e в CI
Текущее состояние: `e2e` — non-blocking baseline, запускается на каждом пуше в
`main` и на каждом PR целиком (все проекты matrix). Ночного разделения нет: при
стоимости job около трёх минут оно не окупается, поэтому новые browser-critical
спецификации просто добавляются в `browserContractTests`.
`e2e` станет blocking, когда выполнятся три условия:
1. Условия matrix пересмотрены: решено, какие спецификации обязаны идти в
`browserContractTests` и `mobileTests`, нужен ли `png-fixtures` вне Chromium
и что делать с engine-специфичными расхождениями декодеров.
2. Накопились стабильные прогоны на раннере без падений по таймингу.
3. Вместе с `continue-on-error` снимается и сам trick с общим conclusion: иначе
зелёный прогон при красном `e2e` останется возможен.
Пока условия не выполнены, `e2e` остаётся non-blocking, а его статус читается по
отдельному job — см. следующий раздел.
## 10. Чтение результатов CI после пуша
Первый реальный прогон browser matrix происходит на runner, а не на локальной
машине: локально Firefox и WebKit проходят на 1 воркере, но состав, скорость и
память Linux-раннера отличаются. Поэтому после пуша разработчика:
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
2. смотреть статусы **отдельных job**, а не общий conclusion прогона: у `e2e`
стоит `continue-on-error: true`, поэтому прогон с упавшим e2e целиком
помечается как `success` — проверено на run `36225579363` (`E2E: failure` при
`conclusion: success`);
3. если `e2e` запускался и зелёный — сообщить фактические числа passed, failed,
skipped;
4. если `e2e` не запускался, сказать это прямо: matrix на раннере не проверена,
и её можно прогнать через `gh workflow run quality.yml`;
5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
окружения: сначала проверяются один проект и один spec
(`docs/agent-workflow.md`, раздел 3), а не увеличиваются таймауты.
+20 -15
View File
@@ -3,7 +3,13 @@
> Статус: **draft** — на ревью. > Статус: **draft** — на ревью.
> >
> Источник: `docs/tools-audit.md`. Этот документ разбивает аудит на конкретные > Источник: `docs/tools-audit.md`. Этот документ разбивает аудит на конкретные
> шаги, группирует по типу работы и фиксирует решения. > шаги, группирует по типу работы и фиксирует решения. Порядок и правила
> проверки — `docs/quality-gates.md`; точки касания при добавлении вида поля —
> `docs/architecture.md`, раздел 4.
>
> **Ids инструментов в таблицах волны B сверены с реестром.** Названия из
> `tools-audit.md` вида `png-to-hsl` или `color-wheel-generator` в реестре нет
> (`hsl`, `color-wheel-png`).
## 0. Ключевое решение: select → buttons ## 0. Ключевое решение: select → buttons
@@ -116,7 +122,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
Каждый шаг — один коммит, < 500 строк. Меняем `field.select()` → Каждый шаг — один коммит, < 500 строк. Меняем `field.select()` →
`field.buttons()` в перечисленных ниже схемах. `field.buttons()` в перечисленных ниже схемах.
### B1. geometry.ts (8 инструментов) ### B1. geometry.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ----------------------- | -------- | ----------------------------------------- | | ----------------------- | -------- | ----------------------------------------- |
@@ -132,7 +138,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
`field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже `field.select()`, т.к. 9 кнопок в ряд не поместятся. Аналогично `position9` (уже
отдельный компонент `PositionControl`). отдельный компонент `PositionControl`).
### B2. color.ts (4 инструмента) ### B2. color.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------------ | --------- | ------------------------------------ | | ------------------------ | --------- | ------------------------------------ |
@@ -144,7 +150,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
**Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это **Исключение:** channel spaces (hsl/hsv/... component) — 3-6 опций, но это
динамические инструменты, оставить `select`. динамические инструменты, оставить `select`.
### B3. analyze.ts (5 инструментов) ### B3. analyze.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------------- | ---- | ------------------ | | ------------------------- | ---- | ------------------ |
@@ -156,13 +162,13 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
Все используют общий `maskBaseFields` — правка в одном месте. Все используют общий `maskBaseFields` — правка в одном месте.
### B4. filters.ts (1 инструмент) ### B4. filters.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------- | ---- | ------------ | | ------------- | ---- | ------------ |
| add-noise-png | mode | mono / color | | add-noise-png | mode | mono / color |
### B5. generate.ts (12 инструментов) ### B5. generate.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------ | ------------ | --------------------- | | ------------------ | ------------ | --------------------- |
@@ -183,10 +189,10 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
### C1. Source-aware defaults для dimension-полей ### C1. Source-aware defaults для dimension-полей
Q5 добавил общий `defaultFromSource` resolver в `registry-schema`. Для Source-aware defaults реализованы: общий resolver в `registry-schema`
`resize-png` и `crop-png` дефолт после загрузки равен размерам текущего source; (`applySourceDefaults`). Для `resize-png` и `crop-png` дефолт после загрузки
до загрузки используется положительный fallback `1×1`. Один-sided `0` у resize равен размерам текущего source; до загрузки используется положительный fallback
сохраняет режим auto. `1×1`. Один-sided `0` у resize сохраняет режим auto.
### C2. symmetric-copy-png: "keep side" не работает ### C2. symmetric-copy-png: "keep side" не работает
@@ -201,9 +207,9 @@ Q5 добавил общий `defaultFromSource` resolver в `registry-schema`.
### C3. Source-aware bounds (отдельный следующий шаг) ### C3. Source-aware bounds (отдельный следующий шаг)
Q5 реализовал только defaults dimension-полей через `SchemaContext` и Реализованы только defaults dimension-полей через `SchemaContext` и
`defaultFromSource`. Динамические min/max для crop x/y, shift и resize остаются `applySourceDefaults`. Динамические min/max для crop x/y, shift и resize
отдельной задачей: bounds не следует смешивать с default resolver. остаются отдельной задачей: bounds не следует смешивать с default resolver.
**Что нужно спроектировать и завести:** **Что нужно спроектировать и завести:**
@@ -232,8 +238,7 @@ Q5 реализовал только defaults dimension-полей через `S
4. **Первые потребители:** crop (x/y → ±sourceWidth/sourceHeight), shift 4. **Первые потребители:** crop (x/y → ±sourceWidth/sourceHeight), shift
(offsetX/offsetY), resize (max → maxSide). (offsetX/offsetY), resize (max → maxSide).
Это отдельный refactor после Q6/Q7; source-aware defaults из Q5 не заменяют Это отдельный рефакторинг; source-aware defaults не заменяют bounds.
bounds.
### C4. verify-is-png: неверное название ### C4. verify-is-png: неверное название
+63 -45
View File
@@ -1,10 +1,12 @@
# План: SEO + GEO — индексация и конкуренция в image tools # План: SEO + GEO — индексация и конкуренция в image tools
> **Статус (2026-09-11):** план к выполнению. Переезд `preview/*` на реальные > **Статус (2026-09-11; уточнено 2026-09-27):** план к выполнению. Переезд
> маршруты выполнен (в `noindex` сохраняются до запуска S1). Сейчас применять > `preview/*` на реальные маршруты выполнен. **`noindex` на страницах
> можно domain-free часть S1. **Уточнено:** домен не был блокером C17 (см. §6). > инструментов не выставляется** — снимать нечего, каталог открыт для
> Задачи S1 разбиты на domain-free (можно делать параллельно с покупкой домена) > индексации; запрет индексации держится только на служебных путях
> и domain-required (после покупки и настройки домена на Vercel). > (`robots.txt`). Домен не был блокером переезда (см. §6). Задачи S1 разбиты на
> domain-free (можно делать параллельно с покупкой домена) и domain-required
> (после покупки и настройки домена).
> >
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный > **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с > интент («crop png», «make png transparent», «jpg to png»…), конкуренция с
@@ -37,18 +39,21 @@
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
«выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже «выбор формата/качества в Download» вытащен из backlog №7 в S1f: механика уже
есть (`ToolEntry.output`), нужна как продуктовая основа «image есть (`ToolEntry.output`), нужна как продуктовая основа «image
tools»-позиционирования. Отдельные страницы-конвертеры — только для tools»-позиционирования. В направлении «из формата в PNG» отдельных
подтверждённых вордстатом интентов. инструментов нет (загрузка принимает любой формат) — см. §5.
- **Слаги инструментов `-png` сохраняются.** Они держат низкоконкурентные - **Слаги инструментов `-png` сохраняются.** Они держат низкоконкурентные
png-интенты; широкие интенты («crop image online») переносятся теми же png-интенты; широкие интенты («crop image online») переносятся теми же
страницами контентом: H1 «Crop image», текст «works with PNG, JPG, WebP». страницами контентом: H1 «Crop image», текст «works with PNG, JPG, WebP».
URL-алиасы без `-png` не создаются (канонический беспорядок). URL-алиасы без `-png` не создаются (канонический беспорядок).
- **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия — - **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия —
отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang
(сознательно отложено в `archive/plan-i18n.md`). Пока язык переключается на (сознательно отложено). Пока язык переключается на клиенте (localStorage) —
клиенте (localStorage) — для SEO не учитываем. для SEO не учитываем.
- **Прод-домен без `BASE_PATH`.** Сборка с базой (GitHub Pages `/repo/`) ломает - **Прод-домен без `BASE_PATH`.** Сборка с базой (GitHub Pages `/repo/`, см.
абсолютные URL, canonical и «корневость» страниц. Свой домен + корень. `BASE_PATH` в `web/vite.config.ts` и `deploy.yml`) ломает абсолютные URL,
canonical и «корневость» страниц. Свой домен + корень. Текущий деплой — GitHub
Pages (`.github/workflows/deploy.yml`); перенос на свой домен выполняется
сменой origin в CI и `PUBLIC_SITE_ORIGIN`.
- **URL-схема инструментов:** короткие плоские адреса. После C17 страницы - **URL-схема инструментов:** короткие плоские адреса. После C17 страницы
инструментов находятся в корне на `/tools/[id]`, каталог — на `/list-tools`; инструментов находятся в корне на `/tools/[id]`, каталог — на `/list-tools`;
текущую схему не менять без отдельного SEO-плана: после индексации текущую схему не менять без отдельного SEO-плана: после индексации
@@ -66,24 +71,24 @@
- SvelteKit + `adapter-static`, `prerender = true` на весь сайт - SvelteKit + `adapter-static`, `prerender = true` на весь сайт
(`web/src/routes/+layout.ts`) — весь контент в статичном HTML, ботам не нужен (`web/src/routes/+layout.ts`) — весь контент в статичном HTML, ботам не нужен
JS. Это главный технический козырь. JS. Это главный технический козырь.
- 122 инструмента в `web/src/lib/registry` с «SEO-шными» id (`crop-png`, - Инструменты в `web/src/lib/registry` с «SEO-шными» id (`crop-png`,
`add-border-png`, …) — URL уже повторяют паттерн конкурента. `add-border-png`, …) — URL уже повторяют паттерн конкурента. Актуальный
состав: `docs/tools-map.md`.
- На корневых страницах инструментов есть базовые `<title>` и `robots`; полный - На корневых страницах инструментов есть базовые `<title>` и `robots`; полный
SEO-слой (canonical, OG, content и JSON-LD) остаётся S1/S2. SEO-слой (canonical, OG, content и JSON-LD) остаётся S1/S2.
- Каталог со ссылками на все инструменты (внутренняя перелинковка) и карта - Каталог со ссылками на все инструменты (внутренняя перелинковка) и карта
инструментов против конкурента: `docs/tools-map.md` (у них 311, из них 107 — инструментов против конкурента: `docs/tools-map.md`.
нишевые клоны-серии Logo*/Icon*/Stamp* поверх обычных операций).
- EN-тексты в реестре + i18n-словари ru/en. - EN-тексты в реестре + i18n-словари ru/en.
Чего нет (пробелы закрываются этапами ниже): Чего нет (пробелы закрываются этапами ниже):
| Пробел | Где сейчас | Этап | | Пробел | Где сейчас | Этап |
| ----------------------------------------------------- | ---------------------------------------- | ---- | | -------------------------------------------------- | ----------------------------------- | ---- |
| `sitemap.xml` | нет | S1 | | `sitemap.xml` | нет | S1 |
| `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 | | `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 |
| `rel=canonical` | нет | S1 | | `rel=canonical` | нет | S1 |
| OG/Twitter-теги, OG-картинки | нет | S1 | | OG/Twitter-теги, OG-картинки | нет | S1 |
| `noindex, nofollow` на страницах инструментов preview | `web/src/routes/tools/[id]/+page.svelte` | S1 | | Канонический домен в абсолютных URL | нет, деплой на GitHub Pages с базой | S1 |
| Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 | | Контент страницы (H1-текст, how-to, FAQ, related) | только title/description | S2 |
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 | | JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
| `llms.txt`, хабы категорий с текстом | нет | S3 | | `llms.txt`, хабы категорий с текстом | нет | S3 |
@@ -109,8 +114,8 @@
## 3. Стратегия ## 3. Стратегия
1. **Не лобовая, а ядро интентов.** 80% объёма поиска приносят ~30–50 запросов. 1. **Не лобовая, а ядро интентов.** 80% объёма поиска приносят ~30–50 запросов.
Покрытие инструментов у нас почти полное (122); приоритет — контент и Покрытие инструментов у нас почти полное (список — `docs/tools-map.md`);
качество страниц ядра, затем хвосты. приоритет — контент и качество страниц ядра, затем хвосты.
2. **Страница = лендинг, а не «пустая коробка».** Шаблон контента: H1 → 1–2 2. **Страница = лендинг, а не «пустая коробка».** Шаблон контента: H1 → 1–2
абзаца «что делает» → как использовать (шаги) → параметры → FAQ (3–5 вопросов абзаца «что делает» → как использовать (шаги) → параметры → FAQ (3–5 вопросов
уровня People-Also-Ask) → related tools по категории. уровня People-Also-Ask) → related tools по категории.
@@ -132,34 +137,35 @@
## 4. Этапы ## 4. Этапы
### S1. Технический минимум (сразу после C17) ### S1. Технический минимум (закрывает блокер индексации)
Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и
**domain-required** (после покупки и DNS-настройки). **domain-required** (после покупки и DNS-настройки).
#### S1-free: без домена (параллельно с quality-планом) #### S1-free: без домена
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на - [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд, существующем ядре (мы сами умеем рисовать PNG) — имя инструмента + бренд,
файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути файл `static/og/[id].png`, ссылка из `Seo.svelte`. Относительные пути
(`/og/crop-png.png`) работают без домена; при деплое на Vercel с (`/og/crop-png.png`) работают без домена; при деплое на собственном домене
собственным доменом OG-картинки будут корректными. OG-картинки будут абсолютными и корректными.
- [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality - [ ] **[S1f]** Download-кнопка с селектором формата PNG/JPG/WebP/BMP (+quality
для lossy) для всех инструментов — механика `ToolEntry.output` уже есть для lossy) для всех инструментов — механика `ToolEntry.output` уже есть
(backlog №7, райз «выбор формата/качества»). Продуктовая основа «image (backlog №7, райз «выбор формата/качества»). Продуктовая основа «image
tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
параллельно с текущими quality-задачами. параллельно с текущими quality-задачами.
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов - [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
формат исходника (disable) — чтобы «convert png to jpg» не мог сохранить в формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в
png. Существующие страницы не трогать: `jpg-to-png`, `png-to-jpg`, png. Существующие страницы не трогать: `jpg-to-png`, `convert-png-to-jpg`,
`png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами со своим `convert-png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами
контентом (§5). со своим контентом (§5).
#### S1-domain: с доменом (после покупки + DNS на Vercel) #### S1-domain: с доменом (после покупки + DNS)
- [ ] **[S1a]** Снять `noindex, no_follow` со страницы инструмента - [ ] **[S1a]** Перевести прод на собственный домен: убрать `BASE_PATH` из
(`tools/[id]/+page.svelte`) — делается одновременно с S1d (canonical) `deploy.yml`, задать `PUBLIC_SITE_ORIGIN`, перенаправить
чтобы Google сразу проиндексировал с правильным доменом. `https://<юзер>/<repo>/…` на корневые URL (301). Делается одновременно с
S1d (canonical), чтобы Google проиндексировал сразу с правильным доменом.
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в - [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
`build/` из `TOOLS` реестра (`web/src/lib/registry`) + статических `build/` из `TOOLS` реестра (`web/src/lib/registry`) + статических
страниц. Вызов из `pnpm build` после `vite build` (adapter-static пишет в страниц. Вызов из `pnpm build` после `vite build` (adapter-static пишет в
@@ -181,8 +187,8 @@
- [ ] **[S2a]** Схема данных: `seo`-блок per-tool (`longDescription`, - [ ] **[S2a]** Схема данных: `seo`-блок per-tool (`longDescription`,
`howTo: string[]`, `faq: {q, a}[]`) — в i18n-словарях (ru/en), не в коде `howTo: string[]`, `faq: {q, a}[]`) — в i18n-словарях (ru/en), не в коде
реестра. Черновики — генерацией из схемы параметров инструмента, вычитка реестра. Черновики — генерацией из схемы параметров инструмента, вычитка
вручную пачками по категориям (самый трудоёмкий этап: ~122 × 2 локали; RU вручную пачками по категориям (самый трудоёмкий этап: объём растёт вместе
— отложить до решения о `/ru/`). с числом инструментов × 2 локали; RU — отложить до решения о `/ru/`).
- [ ] **[S2b]** Компонент контента страницы инструмента (kit): H1, lead, how-to - [ ] **[S2b]** Компонент контента страницы инструмента (kit): H1, lead, how-to
(`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка (`ol`), описание параметров (из схемы), FAQ, related tools (перелинковка
внутри категории + 2–3 «соседних» интента). внутри категории + 2–3 «соседних» интента).
@@ -237,15 +243,28 @@
## 5. Приоритизация инструментов (первая волна контента) ## 5. Приоритизация инструментов (первая волна контента)
Источник — `docs/tools-map.md` (122 реализовано; у конкурента 311, из них 107 — Источник — `docs/tools-map.md` (наш состав и замер по конкуренту — §2). Ids ниже
клоны-серии). Первая волна — топ-интенты, где конкуренты ранжируются, а у нас — **фактические id реестра**; первая волна — топ-интенты, где конкуренты
уже есть инструмент и он не хуже: ранжируются, а у нас уже есть инструмент и он не хуже:
- прозрачность/фон: create-transparent-png, remove-background, change-opacity - прозрачность/фон: create-empty-png, remove-background-png, change-png-opacity,
- конвертация: jpg-to-png, png-to-jpg, webp-to-png, svg-to-png, png-to-base64 set-alpha-channel-png
- геометрия: crop, resize, rotate, circle-crop, round-corners, flip - конвертация: convert-png-to-jpg, convert-png-to-webp, png-to-bmp, svg-to-png,
- цвет/фильтры: grayscale, invert, pixelate, add-text, watermark png-to-base64
- генерация: gradient, empty-canvas - геометрия: crop-png, resize-png, rotate-png, round-corners-png, flip-png,
change-aspect-ratio-png
- цвет/фильтры: grayscale-png, invert-colors-png, pixelate-png, add-text-png,
watermark-tile-png
- генерация: linear-gradient-png, create-empty-png
> **Открытый вопрос по интентам «из формата в PNG».** У конкурента есть
> `jpg-to-png`, `webp-to-png`, `gif-to-png` как отдельные страницы; у нас таких
> инструментов нет и создавать их не хочется — загрузка файла уже принимает
> любой формат, а конвертер «во что-то» закрывает обратное направление. Это
> high-volume интент без посадочной страницы. Решение (страница-обёртка на
> существующий инструмент / приём в H1 и тексте / ничего) требует вордстата и
> должно быть принято до старта S2, иначе первая волна контента закроет не ядро
> запросов.
Новая волна форматов (после S1f): jpg↔webp и другие конвертации «формат → Новая волна форматов (после S1f): jpg↔webp и другие конвертации «формат →
формат» — только по подтверждённому вордстату; базовые кейсы закрывает селектор формат» — только по подтверждённому вордстату; базовые кейсы закрывает селектор
@@ -325,7 +344,7 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной
мелочью), историю docs/archive. мелочью), историю docs/archive.
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и - [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
настроить деплой на Vercel без `BASE_PATH` (§0). Домен нужен до S1-domain: перевести прод на него без `BASE_PATH` (§0). Домен нужен до S1-domain:
canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`. canonical и sitemap генерятся от `PUBLIC_SITE_ORIGIN`.
- **Порядок:** R1+R2 можно делать параллельно с текущими quality-задачами (не - **Порядок:** R1+R2 можно делать параллельно с текущими quality-задачами (не
блокируют). R2 (домен) — блокер только для S1-domain (canonical + sitemap + блокируют). R2 (домен) — блокер только для S1-domain (canonical + sitemap +
@@ -334,8 +353,8 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
## 7. Что осознанно НЕ делаем ## 7. Что осознанно НЕ делаем
- Клонирование 311 URL конкурента (низкокачественные дубли + нет ресурсов на - Клонирование всего каталога конкурента (низкокачественные дубли + нет ресурсов
контент) — вместо этого ядро + обоснованные хвосты. на контент) — вместо этого ядро + обоснованные хвосты.
- RU-локаль в индексе до решения о структуре `/ru/` + hreflang. - RU-локаль в индексе до решения о структуре `/ru/` + hreflang.
- GA/реклама/трекинг — противоречит позиционированию. - GA/реклама/трекинг — противоречит позиционированию.
- Дорвеи, программные зеркала, покупные ссылки. - Дорвеи, программные зеркала, покупные ссылки.
@@ -352,6 +371,5 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- `docs/backlog.md` — пункты SEO/GEO в общем бэклоге. - `docs/backlog.md` — пункты SEO/GEO в общем бэклоге.
- `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA. - `docs/roadmap.md` — фазы 2–8 (ядро TS→Rust/wasm, CLI); фазы 9–10 — API и PWA.
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим). - `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
- `docs/archive/plan-i18n.md` — отложенная многоязычная схема (будущий `/ru/`).
- `docs/plan-domain.local.md` — план переименования/домена; **в git не входит** - `docs/plan-domain.local.md` — план переименования/домена; **в git не входит**
(`.gitignore`), лежит только локально. (`.gitignore`), лежит только локально.
+51 -165
View File
@@ -1,12 +1,12 @@
# Quality gates # Quality gates
> Статус: **active**. Каноническое описание быстрых проверок репозитория. > Статус: **active**. Каноническое описание быстрых проверок: команды, coverage,
> Краткие обязательные команды остаются в `AGENTS.md`. > warnings, CI. Почему гейт такой, а не иначе, — `docs/decisions.md`.
Команды выполняются из корня репозитория.
## Каноническая команда ## Каноническая команда
Запускать из корня репозитория:
```text ```text
pnpm verify pnpm verify
``` ```
@@ -28,25 +28,40 @@ pnpm verify
## Coverage ## Coverage
`pnpm --dir web test:coverage` считает покрытие только по `src/lib` и падает `pnpm --dir web test:coverage` считает покрытие только по `src/lib` и падает
ниже порога. Scope и список исключений — в `web/vitest.config.ts`, обоснование — ниже порога. Scope и список исключений — в `web/vitest.config.ts`.
в `docs/testing-strategy.md`.
| Метрика | Порог | Baseline | | Метрика | Порог |
| ---------- | ----- | -------- | | ---------- | ----- |
| statements | 90 | 91.77% | | statements | 90 |
| branches | 82 | 83.3% | | branches | 82 |
| functions | 72 | 74.31% | | functions | 72 |
| lines | 91 | 92.53% | | lines | 91 |
Порог — нижняя граница от текущего baseline, а не оценка качества: он блокирует Порог — нижняя граница, а не оценка качества: он блокирует регрессию и не
регрессию и не требует сначала закрывать весь долг. Локальный замер занимает ~30 требует сначала закрывать весь долг. Актуальные проценты даёт
с, поэтому отдельного прогона в `verify` нет — `test:coverage` заменяет `test`. `pnpm --dir web test:coverage`; здесь они не дублируются, чтобы число не
устарело молча (`AGENTS.md`). Локальный замер занимает единицы секунд, поэтому
отдельного прогона в `verify` нет — `test:coverage` заменяет `test`.
Исключения из scope и их причины:
| Исключение | Причина |
| ------------------------------------------- | ------------------------------------------- |
| `**/*.test.ts`, `**/test-helpers.ts` | сами тесты |
| `**/*.svelte.ts` | runes, не работают в node |
| `executor/executor.worker.ts` | entry worker, исполняется только в браузере |
| `components/define.ts` | type-only, нет исполняемого кода |
| `core/domText.ts`, `core/io.ts` | canvas, `createImageBitmap`, нужен браузер |
| `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts` | данные, а не логика |
`registry/*` из scope НЕ исключены: низкое покрытие там означает непокрытую
логику, а не ограничение окружения. Включение браузерных модулей в scope —
отдельное решение, а не молчаливое расширение списка.
## E2E ## E2E
E2E не входит в `verify`, потому что Playwright поднимает build и статический E2E не входит в `verify`, потому что Playwright поднимает build и статический
сервер. Locator, fixture, i18n и `test.fixme`-политика описаны в сервер:
`docs/testing-strategy.md`:
```text ```text
pnpm --dir web test:e2e pnpm --dir web test:e2e
@@ -55,104 +70,41 @@ pnpm --dir web test:e2e
Запускать его обязательно для изменений маршрутов, загрузки файла, результата, Запускать его обязательно для изменений маршрутов, загрузки файла, результата,
download, темы или локализации. Изменения runtime fixtures/загрузки файла download, темы или локализации. Изменения runtime fixtures/загрузки файла
проверяются отдельным `web/e2e/png-fixtures.spec.ts`; ручные бинарники из проверяются отдельным `web/e2e/png-fixtures.spec.ts`; ручные бинарники из
`tests/fixtures/` не входят в `verify`. В текущем CI это отдельный non-blocking `tests/fixtures/` не входят в `verify`.
baseline; после закрытия известных ошибок его можно сделать blocking.
Локально прогон идёт с `workers: 1` и заметно медленнее: это осознанный размен, Локально прогон идёт с `workers: 1` и заметно медленнее. Правила нагрузки на
экономит память и оставляет компьютер отзывчивым. Playwright не умеет машину пользователя и диагностика флаков — `docs/agent-workflow.md`, раздел 3;
per-project лимит воркеров, а browser matrix упирается в память, а не CPU. матрица проектов и ожидаемое поведение декодеров — `docs/testing-strategy.md`,
Тяжёлые прогоны запускаются по команде пользователя — по одному, без фонового раздел 7; статус e2e в CI и порядок чтения прогона — `docs/decisions.md`,
запуска и циклов повторов; детали в `docs/agent-workflow.md`. разделы 9–10.
## Быстрые подмножества ## Быстрые подмножества
Матрица «изменение → минимальный набор проверок»:
| Изменение | Команда | | Изменение | Команда |
| ---------------------------- | ------------------------------------------------------- | | ---------------------------------------------------- | ------------------------------------------------------- |
| Только Markdown | `pnpm check:docs` | | Только Markdown | `pnpm check:docs` |
| TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` | | TypeScript/Svelte | `pnpm --dir web check` и `pnpm --dir web test` |
| Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` | | Lint-правило или lint-конфиг | `pnpm --dir web test:rules` и `pnpm --dir web lint:all` |
| Пользовательский поток | `pnpm --dir web test:e2e` | | Маршрут, загрузка, результат, download, тема, локаль | `pnpm --dir web test:e2e` |
| Fixtures/загрузка файла | `pnpm --dir web test:e2e` | | Перед ревью или merge | `pnpm verify` |
| Полный локальный gate | `pnpm verify` |
## Errors и warnings ## Errors и warnings
- `errors` завершают соответствующий инструмент ненулевым кодом. - `errors` завершают соответствующий инструмент ненулевым кодом.
- После Q2 ESLint запускается с `--max-warnings=0`: новые ESLint warnings - ESLint запускается с `--max-warnings=0`: новые ESLint warnings блокируют gate.
блокируют gate. - Token audit оставляет unused-token сообщение неблокирующим: новый токен
- Token audit оставляет unused-token сообщение неблокирующим: после Q7a пыль появляется вместе с потребителем, но стиль не блокирует деплой (решение —
вычищена, решение о fail-режиме зафиксировано в Q7d — новый токен появляется `docs/decisions.md`, раздел 6).
вместе с потребителем, но стиль не блокирует деплой.
- Резолв `var()` в `app.css` — ошибка: несуществующий токен молча роняет - Резолв `var()` в `app.css` — ошибка: несуществующий токен молча роняет
объявление, поэтому этот check failing, в отличие от unused. объявление, поэтому этот check failing, в отличие от unused.
- `lint:all` сейчас чист: 0 errors, 0 i18n warnings, 0 unused tokens.
- `dict-consistency` использует base-locale fallback для registry metadata и - `dict-consistency` использует base-locale fallback для registry metadata и
pattern-based policy для временно неиспользуемых `tools.*.params`; это не pattern-based policy для временно неиспользуемых `tools.*.params`; это не
молчаливый `allowPaths`-список. молчаливый `allowPaths`-список.
## Решения, принятые без автоматизации `lint:all` может завершиться успешно при warnings — это не означает чистый
baseline. Агент показывает число errors, warnings и token warnings отдельно.
Здесь лежат решения «что сознательно НЕ вводим», чтобы не пересматривать их
заново через полгода. Для каждого — причина и то, что пересмотрело бы решение. В
`docs/backlog.md` такие пункты не дублируются: backlog живёт задачами, а не
историей решений.
### Детектор дублирования разметки — не вводим
Ни свой AST-правило, ни готовая зависимость (jscpd, `eslint-plugin-sonarjs`).
- Измерение по 65 `.svelte` в `components/**` и `routes/**`: при пороге 5
нормализованных строк — одна осмысленная пара, при пороге 3 строки — 17 пар и
все они шум (`</label>`, `{/each}`, `{#if children}`). Семейство
schema-контролов делит максимум 4 строки — это законное сходство by design.
- Причина: построчное сравнение Svelte-разметки принципиально ненадёжно —
одинаковый контейнер с разным содержимым даёт ложное срабатывание, а намеренно
общий класс — ложную тишину. Чтобы не ловить шум, порог пришлось бы поднять до
12+ строк, и тогда правило срабатывает ровно один раз.
- Контроль остаётся на ревью (расхождение видно в диффе) и на поведенческих
e2e-проверках; извлечение общей разметки закреплено конвенцией в `AGENTS.md`.
- Пересмотреть, если появится реально «уехавшая» копипаста в компонентах или
библиотека разрастётся на уровень композиций страниц.
### Линтер согласованности комментариев — не вводим
Описания инструментов различаются осознанно, и ловить это текстовым правилом
дороже, чем смотреть на ревью. Действенная часть выполнена вручную: все
`TODO`/`FIXME` перенесены в backlog, незарегистрированных пометок в коде нет;
новые запрещены правилом в `AGENTS.md`.
### Visual snapshots — не вводим
`toHaveScreenshot` сравнивает страницу с PNG-baseline из прошлого коммита. При
активно меняющемся визуальном языке это превращает каждый дизайн-PR в
перегенерацию и ревью картинок, а baseline платформенные (снимок с Windows не
совпадёт на `ubuntu-latest`). Потребность закрывают e2e-поведение, `lint:tokens`
с token audit, i18n-гейт и ручной чеклист. Вернуться стоит, когда визуальный
язык стабилизируется; тогда первым идёт `/kit` в двух темах, а baseline
рождаются на Linux.
### Fail-режим для unused-token аудита — не вводим
Остаётся предупреждением: `verify` входит в deploy-workflow, поэтому стилевой
долг блокировал бы деплой сайта. Кроме того, аудит видит только ссылки
`var(--x)`, и чтение токена из JS дало бы ложное срабатывание. Правило «новый
токен — вместе с потребителем» живёт в `AGENTS.md`.
### Per-project `expect.timeout` — не задаём
Firefox и WebKit отдают результат так же быстро, как Chromium: расхождения
объясняются памятью, а не скоростью движка. Таймаут под конкретную машину —
магическое число, которое придётся подстраивать на каждом раннере, поэтому
`lint`/Playwright-конфиг остаётся с дефолтами, а проблема решается лимитом
воркеров.
### Coverage: браузерные модули вне scope
`src/lib/core/domText.ts` и `src/lib/core/io.ts` (canvas, `createImageBitmap`)
исключены из coverage-гейта: в node-Vitest они не исполнимы, и их включение
превращало бы метрику в шум. `registry/*` из scope НЕ исключены — низкое
покрытие там означает непокрытую логику, а не ограничение окружения. Список
исключений с причинами — в `docs/testing-strategy.md`.
## CI ## CI
@@ -165,76 +117,10 @@ Firefox и WebKit отдают результат так же быстро, ка
`.github/workflows/deploy.yml` перед production build запускает тот же `.github/workflows/deploy.yml` перед production build запускает тот же
`pnpm verify`, поэтому deploy не должен проходить при ошибке быстрых проверок. `pnpm verify`, поэтому deploy не должен проходить при ошибке быстрых проверок.
### Что и когда запускается
| Workflow | Триггер | Jobs | | Workflow | Триггер | Jobs |
| ------------------------ | --------------------------------------- | --------------------------- | | ------------------------ | --------------------------------------- | --------------------------- |
| `Deploy to GitHub Pages` | push в `main`, dispatch | `verify`, `build`, `deploy` | | `Deploy to GitHub Pages` | push в `main`, dispatch | `verify`, `build`, `deploy` |
| `Quality` | push в `main`, `pull_request`, dispatch | `verify`, `e2e` | | `Quality` | push в `main`, `pull_request`, dispatch | `verify`, `e2e` |
Пуш в `main` запускает оба workflow: deploy проверяет быстрый gate и собирает Результат после пуша читается **по отдельным job** — процедура в
сайт, `Quality` прогоняет тот же `verify` и browser matrix. Дублирование `docs/decisions.md`, раздел 10.
`verify` (~1 мин) сознательное: matrix на каждый пуш стоит ~3 мин, и условные
job-и ради минуты не окупаются. Browser matrix проверяется на каждом пуше, а
ручной `gh workflow run quality.yml` нужен только для перепрогона старого
коммита.
### Проверка результатов CI после пуша
Первый реальный прогон browser matrix происходит на runner, а не на локальной
машине: локально Firefox и WebKit проходят на 1 воркере, но состав, скорость и
память Linux-раннера отличаются. Поэтому после пуша разработчика:
1. дождаться завершения workflow и посмотреть, какие job вообще запустились;
2. смотреть статусы **отдельных job**, а не общий conclusion прогона: у `e2e`
стоит `continue-on-error: true`, поэтому прогон с упавшим e2e целиком
помечается как `success` — проверено на run `36225579363` (`E2E: failure` при
`conclusion: success`);
3. если `e2e` запускался и зелёный — сообщить фактические числа passed, failed,
skipped;
4. если `e2e` не запускался, сказать это прямо: matrix на раннере не проверена,
и её можно прогнать через `gh workflow run quality.yml`;
5. красный `e2e` только из-за ресурсов раннера фиксируется как ограничение
окружения: сначала проверяется один проект и один spec, а не увеличиваются
таймауты под конкретную машину.
### Измеренная стоимость прогона
Замер на ручном запуске `quality.yml` (276 e2e-тестов, 4 проекта,
ubuntu-latest):
| Этап | Время |
| ----------------- | -------- |
| `verify` job | ~1 мин |
| `e2e` job целиком | ~3.1 мин |
| из них сами тесты | ~2.2 мин |
| весь run | ~4.2 мин |
Browser matrix стоит около трёх минут, поэтому её дешёво держать на каждом PR.
Полный прогон локально на 1 воркере — заметно дольше (~7 мин), и именно поэтому
локальные правила нагрузки описаны в `docs/agent-workflow.md`.
## Политика e2e в CI
Текущее состояние: `e2e` — non-blocking baseline, запускается на каждом пуше в
`main` и на каждом PR целиком (все четыре проекта matrix). Ночного разделения
нет: при стоимости job около трёх минут оно не окупается, поэтому новые
browser-critical спецификации просто добавляются в `browserContractTests`.
`e2e` станет blocking, когда выполнятся три условия:
1. Условия matrix пересмотрены: решено, какие спецификации обязаны идти в
`browserContractTests` и `mobileTests`, нужен ли `png-fixtures` вне Chromium
и что делать с engine-специфичными расхождениями декодеров.
2. Накопились стабильные прогоны на раннере без падений по таймингу.
3. Вместе с `continue-on-error` снимается и сам trick с общим conclusion: иначе
зелёный прогон при красном `e2e` останется возможен.
Пока условия не выполнены, `e2e` остаётся non-blocking, а его статус читается по
отдельному job.
## Локальная диагностика
При медленном полном прогоне сначала запускай минимальную команду из таблицы, а
перед ревью или merge — `pnpm verify`. После каждого шага плана результат команд
и текущий baseline фиксируются в отчёте агента.
+52 -59
View File
@@ -1,58 +1,53 @@
# План разработки: easy-png-tools # План развития: easy-png-tools
> **Статус:** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт живёт, > **Статус (2026-09):** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт
> каталог переведён в типизированный `registry` (122 текущих инструмента), новый > живёт, каталог переведён в типизированный `registry`, новый дизайн стал
> дизайн стал единственным production UI на корневых маршрутах; UI-v1 удалён в > единственным production UI на корневых маршрутах, UI-v1 удалён. Tech debt не
> Q3. Tech debt не закрыт полностью; текущий baseline и состав quality-проверок > закрыт полностью; состав quality-проверок — `docs/quality-gates.md`,
> — в `docs/README.md` и `docs/quality-gates.md`. Следующий этап: SEO S1-free > архитектура кода — `docs/architecture.md`, список инструментов —
> (`plan-seo.md`) + баги и продуктовые задачи из `docs/backlog.md`. Домен нужен > `docs/tools-map.md`. Текущий фокус: SEO S1-free (`docs/plan-seo.md`) +
> для SEO (`plan-seo.md`, S1-domain). Фазы 2–8 — будущие. Фазы 9–10 спланированы > продуктовые задачи из `docs/backlog.md`. Домен нужен для SEO (`plan-seo.md`,
> в `docs/plan-platform.md`. > S1-domain). Фазы 2–8 — будущие; фазы 9–10 спланированы в
> `docs/plan-platform.md`.
>
> **Нумерация фаз этого документа не совпадает с волнами разработки** (`Q*`/`C*`
> в других документах) и не используется как идентификатор задач: ссылаться на
> фазу, а не на номер волны.
## 0. Решения ## 0. Решения
- **Набор PNG-утилит, всё в браузере** (SvelteKit + `adapter-static`), данные не - **Набор утилит для изображений, всё в браузере** (SvelteKit +
покидают машину. `adapter-static`), данные не покидают машину.
- **Первый и главный приоритет — полностью рабочий TS-сайт**: максимум простых - **Первый и главный приоритет — полностью рабочий сайт**: максимум простых
инструментов уровня EASY, UX/UI, пайплайны (несколько инструментов подряд), инструментов, UX/UI, пайплайны (несколько инструментов подряд), описания
описания инструментов. Сайтом можно пользоваться уже после этой фазы. инструментов.
- **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` — - **Никакого wasm/Rust на этом этапе.** TS-реализация на canvas + `ImageData` —
эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки эталон алгоритмов, из которого позже генерируются эталонные файлы для проверки
Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт. Rust-порта. Rust-ядро, WASM и CLI — отдельные фазы позже, когда сайт живёт.
- **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас - **Пайплайн = JSON-список шагов** `{tool, params}`. Формат закладывается сейчас
и потом станет общим для браузера, wasm и CLI. и потом станет общим для браузера, wasm и CLI.
- **Реестр инструментов** (id, title, description, схема параметров, `run`) — - **Реестр инструментов** (`web/src/lib/registry/`: id, title, description,
источник истины: из него генерируются страницы, формы, пайплайны. Позже — category, schema, `run`) — источник истины: из него генерируются страницы,
общий формат для Rust-парсера. формы, пайплайны. Позже — общий формат для Rust-парсера.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из - Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций, TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
**perceptual diff** для canvas-зависимых. **perceptual diff** для canvas-зависимых.
- **Единая точка исполнения инструментов.** Страницы вызывают операции только - **Единая точка исполнения инструментов.** Страницы вызывают операции только
через `web/src/lib/tools/executor.ts::executeStep` — нигде напрямую через `web/src/lib/executor/` — нигде напрямую `tool.run`. Контракт уже
`tool.run`. Контракт уже асинхронный (`Promise<PixelImage>`), поэтому: асинхронный, поэтому:
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности - когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
`executeStep` переезжают в Web Worker — воркер импортирует тот же чистый исполнителя переезжают в Web Worker — воркер импортирует тот же чистый core
core как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется; как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId` - в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя; — свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не - до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
блокируют UI. блокируют UI.
## 1. Структура репозитория (этап TS) ## 1. Где что лежит
```txt Актуальная карта модулей, поток данных и рецепт добавления инструмента —
easy-png-tools/ `docs/architecture.md`. Список реализованных инструментов — `docs/tools-map.md`,
web/ # SvelteKit (adapter-static) открытые продуктовые задачи — `docs/backlog.md`.
src/lib/core/ # TS-ядро: Image (ImageData) + операции (без DOM)
color.ts alpha.ts geometry.ts format.ts text.ts analyze.ts
src/lib/registry.ts # реестр инструментов: id, title, description, params, run
src/lib/pipeline.ts # пайплайн: шаги, применение, localStorage, экспорт/импорт JSON
src/lib/components/ # DropZone, Preview, ParamForm (из схемы), Download, PipelineSteps
src/routes/ # маршруты-утилиты (генерируются из реестра) + /workspace
docs/
core/ # (позже) Rust-крейт
tests/ # (позже) эталонные файлы и harness
```
## 2. Фазы ## 2. Фазы
@@ -64,22 +59,21 @@ clamp-семантики (в TS это делает `Uint8ClampedArray` сам).
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета, 1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета,
типографика, компоненты), общий лейаут, шапка с навигацией по категориям. типографика, компоненты), общий лейаут, шапка с навигацией по категориям.
1.2 **Ядро.** Тип `Image` (обёртка над `ImageData`), загрузка/декодирование 1.2 **Ядро.** Операции над `ImageData` (загрузка/декодирование файла,
файла, кодирование и скачивание, библиотека операций кодирование и скачивание, библиотека операций
(color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
`core`. `core`.
1.3 **Реестр инструментов.** Одна запись = 1.3 **Реестр инструментов.** Одна запись =
`{ id, title, description, category, params: ParamDef[], run }`. Универсальный `{ id, title, description, category, schema, run }`. Универсальный рендерер:
рендерер: страница инструмента и форма параметров строятся из записи реестра страница инструмента и форма параметров строятся из записи реестра
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
инструмент = новая запись + функция `run`. инструмент = новая запись + функция `run` (рецепт — `docs/architecture.md` §8).
1.4 **Пайплайн-workspace** (`/workspace`): загрузил изображение → список шагов 1.4 **Пайплайн-workspace**: загрузил изображение → список шагов (инструмент +
(инструмент + его параметры) → последовательное применение с превью каждого шага его параметры) → последовательное применение с превью каждого шага → скачивание
→ скачивание финального результата. Добавление/удаление/перестановка шагов. финального результата. Добавление/удаление шагов. Сохранение пайплайнов в
Сохранение пайплайнов в localStorage, экспорт/импорт JSON. Общие компоненты: localStorage, экспорт/импорт JSON.
DropZone, Preview, ParamForm, Download, PipelineSteps.
1.5 **Массовая реализация EASY-инструментов** по категориям: 1.5 **Массовая реализация EASY-инструментов** по категориям:
@@ -105,17 +99,19 @@ wasm.
- **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` + - **Checkpoint:** в `tests/reference/<op>/` лежат `input-*.png` +
`expected-*.png`. `expected-*.png`.
### Фаза 3 — Playwright-тесты ### Фаза 3 — Эталонные файлы и UI-регресс
- `tools/gen-references.ts`: грузит страницу, гоняет TS-эталон, сохраняет > UI-регресс на Playwright уже существует и закрывает фазу 3 частично
> (`web/e2e/`, browser matrix). Недостающая часть фазы — именно эталоны.
- Скрипт генерации эталонов: грузит страницу, гоняет TS-реализацию, сохраняет
`expected-*.png`. `expected-*.png`.
- Регресс: изменение TS-ядра ловится тестами.
- **Checkpoint:** регресс-тесты падают при изменении выхода эталона. - **Checkpoint:** регресс-тесты падают при изменении выхода эталона.
### Фаза 4 — Rust-ядро ### Фаза 4 — Rust-ядро
- Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с - Порт 1:1 чистых операций (первым делом те, что покрыты эталонными файлами) с
`Image` ↔ TS `ImageData`. `PixelImage` ↔ TS `ImageData`.
- `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) + - `clamp_to_u8` с семантикой `ToUint8Clamp` (round-half-to-even, ≥255 → 255) +
юнит-тесты на краях. юнит-тесты на краях.
- `cargo test` читает эталонные файлы напрямую, **без браузера**. - `cargo test` читает эталонные файлы напрямую, **без браузера**.
@@ -179,8 +175,8 @@ Manifest + service worker, оффлайн-режим («работает без
- **Реестр разрастается, страницы дублируются** — один универсальный рендерер - **Реестр разрастается, страницы дублируются** — один универсальный рендерер
страниц/форм из записей реестра; инструмент = данные + `run`. страниц/форм из записей реестра; инструмент = данные + `run`.
- **Семантика пайплайна** — зафиксировать: каждый шаг применяется к результату - **Семантика пайплайна** — зафиксирована: каждый шаг применяется к результату
предыдущего; единый тип `Image` на всём пути; схема параметров на шаге. предыдущего; единый тип `PixelImage` на всём пути; схема параметров на шаге.
- **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3, - **Canvas-операции не бит-в-бит** — класс perceptual закладывается на Фазе 2–3,
а не в конце. а не в конце.
- **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен - **WASM + Vite** — спайк на Фазе 5 до массовой интеграции (риск отложен
@@ -192,10 +188,7 @@ Manifest + service worker, оффлайн-режим («работает без
## 5. Команды верификации ## 5. Команды верификации
```bash Текущие команды и матрица «изменение → проверки» — `docs/quality-gates.md`
pnpm dev # разработка (Фаза 1+, обёртка `pnpm --dir web dev`) (`pnpm verify` — быстрый gate текущего этапа). Для будущих фаз добавляются
pnpm build # статический экспорт (обёртка `pnpm --dir web build`) `cargo test` (Rust-ядро против эталонных файлов, фаза 4+) и
pnpm --dir web exec playwright test # эталоны + UI-регресс (Фаза 3+) `harness compare tests/reference` (сквозная сверка native + wasm, фаза 7).
cargo test # Rust-ядро против эталонных файлов (Фаза 4+)
harness compare tests/reference # сквозная сверка native + wasm (Фаза 7)
```
+14 -60
View File
@@ -68,24 +68,8 @@ browser-контракт: critical CRC и truncated отклоняются дв
`src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright. `src/**/*.test.ts`; DOM, hydration и пользовательские потоки — в Playwright.
Component-тесты не дублируют e2e-контракт. Component-тесты не дублируют e2e-контракт.
### Что попадает в coverage Состав и причины исключений из coverage-гейта, а также пороги — в
`docs/quality-gates.md`; здесь они не дублируются.
Coverage-гейт считает только `src/lib` и только то, что node-Vitest способен
исполнить. Исключения в `web/vitest.config.ts` и их причины:
| Исключение | Причина |
| ------------------------------------------- | ------------------------------------------- |
| `**/*.test.ts`, `**/test-helpers.ts` | сами тесты |
| `**/*.svelte.ts` | runes, не работают в node |
| `executor/executor.worker.ts` | entry worker, исполняется только в браузере |
| `components/define.ts` | type-only, нет исполняемого кода |
| `core/domText.ts`, `core/io.ts` | canvas, `createImageBitmap`, нужен браузер |
| `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts` | данные, а не логика |
`registry/*` в scope остаётся: низкое покрытие там — это непокрытая логика, а не
следствие окружения. Исключение браузерных модулей означает, что переход
браузерного кода в scope — отдельное решение, связанное с DOM-тестами или visual
snapshots, а не молчаливое расширение списка исключений.
## 5. Helpers и `test.fixme` ## 5. Helpers и `test.fixme`
@@ -99,7 +83,7 @@ text — `renderText` + результат, generator — auto-run без upload
`test.fixme` допускается только для зарегистрированной проблемы в backlog или `test.fixme` допускается только для зарегистрированной проблемы в backlog или
checklist. В комментарии указывается ссылка на неё. После исправления тест и checklist. В комментарии указывается ссылка на неё. После исправления тест и
ссылка обновляются вместе; в текущем Q6e baseline активных fixme нет. ссылка обновляются вместе.
Если тест сравнивает два состояния одного элемента, он ждёт изменения значения Если тест сравнивает два состояния одного элемента, он ждёт изменения значения
(`not.toHaveText`, `not.toHaveAttribute`), а не его видимости. Пересчёт идёт (`not.toHaveText`, `not.toHaveAttribute`), а не его видимости. Пересчёт идёт
@@ -108,21 +92,14 @@ checklist. В комментарии указывается ссылка на н
## 6. Проверки ## 6. Проверки
Для изменений e2e выполняются: Матрица «изменение → минимальный набор проверок» и состав `pnpm verify` — в
`docs/quality-gates.md`. Для изменений e2e дополнительно уместны
```text `pnpm --dir web check` и `pnpm --dir web format:check`.
pnpm --dir web check
pnpm --dir web test:rules
pnpm --dir web lint:all
pnpm --dir web format:check
pnpm check:docs
pnpm --dir web test:e2e
pnpm verify
```
`verify` не включает Playwright: E2E остаётся отдельным тяжёлым gate. В CI `verify` не включает Playwright: E2E остаётся отдельным тяжёлым gate. В CI
non-blocking E2E не считается заменой локальному smoke; перед ревью фиксируются non-blocking E2E не считается заменой локальному smoke; перед ревью фиксируются
число passed, skipped и известные fixme. число passed, skipped и известные fixme. Статус e2e в CI — `docs/decisions.md`,
раздел 9.
## 7. Browser matrix ## 7. Browser matrix
@@ -148,32 +125,9 @@ non-blocking E2E не считается заменой локальному smo
| `idat-cut.png` | Chromium | | `idat-cut.png` | Chromium |
| ancillary CRC, tail, palette, 16-bit | ни один движок | | ancillary CRC, tail, palette, 16-bit | ни один движок |
- Firefox и WebKit работают с теми же таймаутами, что и Chromium: движок не - `workers` — 1 локально и 2 в CI: Playwright не умеет per-project лимит
причина расхождений, поэтому per-project `expect.timeout` не задаётся. воркеров, а browser matrix ограничена памятью, а не CPU. Правила нагрузки на
- `workers` — 1 локально и 2 в CI. Playwright не умеет per-project лимит машину и порядок диагностики флаков — `docs/agent-workflow.md`, раздел 3;
воркеров, а browser matrix ограничена памятью, а не CPU: при высокой обоснование отсутствия per-project таймаутов — `docs/decisions.md`, раздел 7.
параллельности браузеры на машине с 16 GB RAM не успевают поднять worker, и - Visual snapshots и per-project таймауты сознательно не используются —
тесты падают без ошибок. Локальный matrix поэтому медленный, но не нагружает `docs/decisions.md`, разделы 5 и 7.
компьютер.
- Локальный baseline: 276 тестов, 276 passed, 0 skipped, 0 failed.
- Coverage ограничена `src/lib` и не включает браузерные модули — см. раздел 4.
## 8. Visual snapshots
Сознательно не используются. `toHaveScreenshot` сравнивает страницу с
PNG-baseline из прошлого коммита, а не с замыслом дизайна, поэтому:
- визуальный язык в активной работе означает перегенерацию и ревью картинок в
каждом дизайн-изменяющем PR;
- baseline привязан к платформе (ОС, шрифты, рендеринг): снимок с Windows не
совпадёт на `ubuntu-latest`, и baseline должны рождаться на Linux;
- четыре проекта matrix плюс тема и viewport дают нелинейный рост числа файлов.
Потребность закрывают e2e-поведение, `lint:tokens` с token audit, i18n-гейт и
ручной чеклист `docs/checklist-manual-testing.md`; «элемент визуально наехал»
ловится чеклистом и ревью.
Возвращаться к snapshots стоит, когда визуальный язык стабилизируется, изменения
станут редкими, а чеклист перестанет справляться. Тогда первым идёт `/kit` в
двух темах, а baseline генерируются на Linux (docker-образ Playwright или
отдельный CI-job).
+6
View File
@@ -1,5 +1,11 @@
# Аудит использования - UX\UI # Аудит использования - UX\UI
> Статус: **active**. Сырые наблюдения по инструментам и полям — вход для
> `docs/plan-fields-audit-fixes.md`, который разбивает их на волны правок, и для
> `docs/backlog.md`. Здесь фиксируется, **что замечено**, а не порядок работы.
> Ids инструментов сверять с `web/src/lib/registry/` — исторические названия
> (`webp-to-png`, `color-wheel-generator`, `png-info`) в реестре отсутствуют.
## везде ## везде
- возможность переключать пиксели\проценты - возможность переключать пиксели\проценты
+80 -73
View File
@@ -1,40 +1,53 @@
# Карта инструментов: реализовано · добавить · идеи # Карта инструментов: реализовано · добавить · идеи
> Живой документ для сверки с каталогом. Формат записи: > Статус: **active**. Живой документ для сверки с каталогом.
> `id / название — параметры через запятую`. Источник сравнения: >
> onlinepngtools.com (311 уникальных; 107 из них — нишевые клоны серий > **Источник правды по id — реестр** `web/src/lib/registry/`. Этот список —
> Logo*/Icon*/Stamp*/Signature* поверх обычных операций). > навигационный: сверять перед использованием. Формат записи:
> `id — параметры через запятую`. Сравнение с каталогом источника (объём,
> нишевые клоны серий Logo*/Icon*/Stamp*/Signature*) — замер в
> `docs/plan-seo.md`, §2; здесь он не дублируется.
>
> Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из
> `TOOLS` в `registry/index.ts`, и дублировать его цифрой значит заставлять себя
> обновлять документ при каждом добавлении (`AGENTS.md`).
--- ---
## 1. Реализовано ## 1. Реализовано
### Конвертация Ids приведены к фактическим в `web/src/lib/registry/`.
- jpg-to-png — без параметров **Формат строки (проверяется тестом
- webp-to-png — без параметров `web/src/lib/registry/tools-map-doc.test.ts`):** в буллите всё **до первого
- gif-to-png — первый кадр тире** `—` — это id инструментов через `/` или `,`; всё после тире — свободное
- bmp-to-png — без параметров описание параметров и не разбирается. Второй id нельзя оставлять после тире,
- ico-to-png — выбор размера кадра иначе он не попадёт в разбор и инструмент будет считаться неописанным.
### Конвертация (`convert.ts`)
Отдельных «из формата в PNG» инструментов **нет**: входной формат выбирается
загрузкой файла, любой инструмент принимает PNG/JPG/WebP/GIF/BMP. Все конвертеры
исходят из PNG.
- svg-to-png — width результата (текстовый вход: SVG-код)
- png-to-bmp — 24-бит, фон вместо альфы - png-to-bmp — 24-бит, фон вместо альфы
- convert-png-to-jpg — background, quality - convert-png-to-jpg — background, quality
- convert-png-to-webp — quality - convert-png-to-webp — quality
- svg-to-png — width результата
- png-to-base64 / base64-to-png — строка - png-to-base64 / base64-to-png — строка
- png-to-data-uri / data-uri-to-png — строка - png-to-data-uri / data-uri-to-png — строка
- png-to-hex — rrggbbaa по строкам / hex-to-png — tokens + width - png-to-hex / hex-to-png — rrggbbaa по строкам / tokens + width
- png-to-bytes — десятичные RGBA-байты по строкам / bytes-to-png — tokens + - png-to-bytes / bytes-to-png — десятичные RGBA-байты по строкам / tokens +
width width
- png-to-rgb-values — rgba(r,g,b,a) по пикселям / rgb-values-to-png — числа + - png-to-rgb-values / rgb-values-to-png — rgba(r,g,b,a) по пикселям / числа +
width width
- svg-to-png — width результата
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
### Прозрачность ### Прозрачность (`alpha.ts`)
- change-png-opacity — percent - change-png-opacity — percent
- set-alpha-channel-png — percent - set-alpha-channel-png — percent
- remove-alpha-channel-png — без параметров - remove-alpha-channel-png — без параметров (фон `#ffffff` захардкожен, см.
`docs/plan-fields-audit-fixes.md` D1)
- extract-alpha-mask-png — без параметров - extract-alpha-mask-png — без параметров
- invert-alpha-png — без параметров - invert-alpha-png — без параметров
- remove-background-png — color, tolerance, outerOnly, smooth - remove-background-png — color, tolerance, outerOnly, smooth
@@ -45,8 +58,6 @@
- harden-alpha-png — threshold - harden-alpha-png — threshold
- feather-edges-png — radius (размытие только альфы) - feather-edges-png — radius (размытие только альфы)
- clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного) - clean-edges-png — radius (defringe: RGB от ближайшего непрозрачного)
- watermark-image-png — вторая картинка-знак (загружается на странице), scale,
opacity, position, margin
- despeckle-alpha-png / close-holes-png — radius - despeckle-alpha-png / close-holes-png — radius
- center-by-alpha-png — без параметров - center-by-alpha-png — без параметров
- round-corners-png — radius - round-corners-png — radius
@@ -55,7 +66,7 @@
- star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY - star-mask-png — points, innerRadius, size, rotation, offsetX, offsetY
- wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY - wavy-mask-png — size, amplitude, waves, phase, offsetX, offsetY
### Цвет ### Цвет (`color.ts`)
- grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без - grayscale-png / invert-colors-png / sepia-png / auto-contrast-png — без
параметров параметров
@@ -69,23 +80,17 @@
- temperature-png — percent - temperature-png — percent
- gamma-png — value - gamma-png — value
- tint-png — color, strength - tint-png — color, strength
### Квантование и палитры
- quantize-png — colors (k, median-cut) - quantize-png — colors (k, median-cut)
- decrease-color-count-png — maxColors (пресеты 2…256) - decrease-color-count-png — maxColors (пресеты 2…256)
- custom-palette-png — colors (hex через запятую, ближайший цвет) - custom-palette-png — colors (hex через запятую, ближайший цвет)
- dithering-png — colors (k), pattern (Floyd–Steinberg / Bayer 4×4) - dithering-png — colors (k), pattern (Floyd–Steinberg / Bayer 4×4)
### Разложение каналов ### Разложение каналов (`color.ts`)
- png-to-hsl / png-to-hsv / png-to-hsi — component (h/s/l и т.п.), display (gray - png-to-hsl, png-to-hsv, png-to-hsi, png-to-cmyk, png-to-ycbcr, png-to-lab —
| space-as-rgb) component (h/s/l и т.п.), display (`gray` | `space-as-rgb`)
- png-to-cmyk — component (c/m/y/k), display
- png-to-ycbcr — component (y/cb/cr), display
- png-to-lab — component (l/a/b), display
### Геометрия ### Геометрия (`geometry.ts`)
- resize-png — width/height (размеры source по умолчанию; 0=авто для одной - resize-png — width/height (размеры source по умолчанию; 0=авто для одной
стороны), keepAspect стороны), keepAspect
@@ -107,7 +112,7 @@
- swap-orientation-png — target (portrait/landscape) - swap-orientation-png — target (portrait/landscape)
- symmetric-copy-png — axis, keepSide - symmetric-copy-png — axis, keepSide
### Фильтры ### Фильтры (`filters.ts`)
- blur-png — radius - blur-png — radius
- sharpen-png — strength - sharpen-png — strength
@@ -118,12 +123,14 @@
- add-noise-png — amount, mode (mono/color), seed - add-noise-png — amount, mode (mono/color), seed
- silhouette-png — color, threshold - silhouette-png — color, threshold
### Анализ ### Анализ и вердикты (`analyze.ts`)
- png-info — размеры, альфа, число цветов - png-file-size — без параметров (DOM-инструмент: кодирует в PNG и отдаёт
размер)
- png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт - png-is-transparent / png-is-grayscale / png-orientation — текстовый вердикт
- verify-is-png — текстовый источник (base64/data-uri), вердикт по сигнатуре
### Маски по свойствам пикселей ### Маски по свойствам пикселей (`analyze.ts`)
- show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных) - show-transparent-png — color, opacity (подсветка прозрачных/полупрозрачных)
- show-grayscale-pixels-png — tolerance, mode (binary/highlight), - show-grayscale-pixels-png — tolerance, mode (binary/highlight),
@@ -136,21 +143,29 @@
- extract-color-from-png — color, tolerance (оставить близкие, остальное - extract-color-from-png — color, tolerance (оставить близкие, остальное
прозрачным) прозрачным)
### Генерация ### Генерация (`generate.ts`)
- create-empty-png — width, height, transparent, color - create-empty-png — width, height, transparent, color
- single-color-png — width, height, color - single-color-png — width, height, color
- random-noise-png — width, height, seed - random-noise-png — width, height, seed
- linear-gradient-png — width, height, fromColor, toColor, direction - linear-gradient-png — width, height, fromColor, toColor, direction
- text-to-png — text, fontSize, font, bold, color, transparentBg,
backgroundColor, padding
- emoji-to-png — emoji, size
- placeholder-png — width, height, backgroundColor, color, showText
- color-spectrum-png — width, height, direction, saturation, lightness - color-spectrum-png — width, height, direction, saturation, lightness
- random-colors-png — width, height, blockSize, seed - random-colors-png — width, height, blockSize, seed
- draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg - draw-grid-png — width, height, cols, rows, lineWidth, color, transparentBg
- placeholder-png — width, height, backgroundColor, color, showText
- blend-two-png — pair (from/to), width
- step-colors-png — pair (from/to), steps, width
- emoji-to-png — emoji, size
- text-to-png — text, fontSize, font, bold, color, transparentBg,
backgroundColor, padding
- color-wheel-png — size, lightness
- complementary-png / triadic-png / tetradic-png / analogous-png /
monochromatic-png / shades-png — общая база палитры: baseColor, width, layout;
`shades-png` дополнительно count, depth
- mix-colors-png — colors, width
- sort-colors-png — colors, порядок (hue/brightness/saturation), width
### Текст ### Текст (`text.ts`)
- add-text-png — text, fontSize, color, font, bold, position (3×3), margin, - add-text-png — text, fontSize, color, font, bold, position (3×3), margin,
plate, plateColor, plateOpacity plate, plateColor, plateOpacity
@@ -159,28 +174,16 @@
- watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY, - watermark-tile-png — text, fontSize, color, opacity, angle, stepX, stepY,
font, bold font, bold
### Палитры и цветовые утилиты
- color-wheel-generator — size, кольца/сектора, показ hex при клике (у нас —
статичный свотч-полотно)
- complementary-palette — baseColor
- monochromatic-palette — baseColor, count
- analogous-palette — baseColor, spread
- triadic-palette — baseColor
- tetradic-palette — baseColor
- similar-shades-palette — baseColor, count, range
- sort-colors — источник (палитра-картинка или список), порядок (hsl/luma)
- mix-colors — colors[], веса?
- average-color — colors[]; blend-two — a, b, steps; step-between — a, b, steps
(три частных случая одного движка)
--- ---
## 2. Можно добавить — из onlinepngtools ## 2. Можно добавить — из onlinepngtools
Всё из этого списка **в реестре отсутствует**.
### Разложение каналов — остаток ### Разложение каналов — остаток
- separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → пока идея) - separate-colors — minShare слоя (MEDIUM, мультифайловый вывод → см. идеи в
`docs/backlog.md`)
### Края и силуэт — остаток ### Края и силуэт — остаток
@@ -197,8 +200,6 @@
- sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM) - sort-pixels — blockSize, ключ (яркость/канал), направление (MEDIUM)
- slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи) - slow-reveal / fade-in / fade-out / disappear — анимационные (→ идеи)
### Сжатие и качество — реализовано (см. Конвертация: compress, reduce-to-size; jpeg-artifacts; W8 quantize)
### Генераторы — остаток ### Генераторы — остаток
- multi-color-gradient — список стопов (нужен новый тип параметра → MEDIUM-UI) - multi-color-gradient — список стопов (нужен новый тип параметра → MEDIUM-UI)
@@ -206,31 +207,37 @@
### Конвертеры — остаток ### Конвертеры — остаток
- png-to-gif — MEDIUM (однокадровый GIF-энкодер руками) - png-to-gif — MEDIUM (однокадровый GIF-энкодер руками)
- gif-to-frames — MEDIUM (мультифайловый вывод → идея) - gif-to-frames — MEDIUM (мультифайловый вывод → идеи)
- change-bit-depth — MEDIUM (пересборка PNG) - change-bit-depth — MEDIUM (пересборка PNG)
### Прочее единичное ### Сжатие и качество
- pick-a-color — пипетка уже есть в превью; отдельная страница не планируется - compress, reduce-to-size, pick-a-color, extract-barcode — **не реализованы**;
(покрыто) заведены в `docs/backlog.md` (reduce-to-size зависит от UX-райза «Download»).
- preview/test на цветных фонах — фича превью, не инструмент (решить позже) Пипетка для выбора цвета уже есть в превью, отдельная страница не планируется.
- extract-barcode — HARD, вне планов Извлечение штрихкодов — HARD, вне планов.
--- ---
## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность) ## 3. Идеи на рассмотрение (нужна архитектура или спорная ценность)
- **Region-инструменты** — требуется UI выделения прямоугольника/ластика на Развёрнутые описания этих идей живут в `docs/backlog.md`, раздел «Идеи»; здесь
превью: censor-region, erase-region, pixelate-area, blur-area, sharpen-area, только список.
reverse-colors-area. Один раз делаем selection-компонент — получаем сразу
шесть инструментов. - **Region-инструменты** — нужен selection-компонент на превью: censor-region,
- **Мультифайловый вывод** — механизм «результат = набор файлов» (zip) erase-region, pixelate-area, blur-area, sharpen-area, reverse-colors-area.
реализован Остальные 1→many: gif-to-frames, separate-colors, Один раз делаем selection — получаем шесть инструментов.
multiply-grid-as-files — подключаются по мере нужды. - **Мультифайловый выход** — механизм «результат = набор файлов» (zip)
реализован на `split-into-parts-png`. Остальные 1→many: gif-to-frames,
separate-colors, multiply-grid-as-files — подключаются по мере нужды.
- **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling: - **Анимационные эффекты** — slow-reveal, fade-in/out, disappearing, scrolling:
это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата. это видео/GIF на выходе, а не PNG. Отдельное решение о формате результата.
- **HARD-хвост** — glitch-art, extract-signature, handwritten→digital, - **HARD-хвост** — glitch-art, extract-signature, handwritten→digital,
extract-barcode. extract-barcode.
- **Нишевые серии** (logo/icon/stamp/signature — 107 клонов у источника) — - **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки. они доступны через базовые инструменты + цепочки.
- **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) сознательно не
заведены: загрузка файла уже принимает любой поддержанный формат, а
инструмент-конвертер добавил бы лишнее звено цепочки. Отдельные страницы под
эти интенты — предмет решения в `docs/plan-seo.md` §5.
-45
View File
@@ -1,45 +0,0 @@
# sv
Everything you need to build a Svelte project, powered by
[`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
pnpm dlx sv@0.17.0 create --template minimal --types ts --install pnpm web
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or
`pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an
> [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+1 -1
View File
@@ -33,7 +33,7 @@
`src/routes/**`). Scoped-пути двигаются вместе с папками: не оставлять `src/routes/**`). Scoped-пути двигаются вместе с папками: не оставлять
устаревшие пути в `eslint.config.js`. устаревшие пути в `eslint.config.js`.
- Базовый `js.configs.recommended` + `globals.node` навешаны на инфраструктуру - Базовый `js.configs.recommended` + `globals.node` навешаны на инфраструктуру
линтинга: `eslint-plugins/**/*.js` и `scripts/**/*.mjs` (Q7c). линтинга: `eslint-plugins/**/*.js` и `scripts/**/*.mjs`.
TS/svelte-рекомендации туда не подключаются. TS/svelte-рекомендации туда не подключаются.
- Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через - Для `*.svelte` выключен `prefer-const` (пропсы в Svelte 5 пишутся через
`let`). `let`).
+119
View File
@@ -0,0 +1,119 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { TOOLS } from ".";
const MAP_PATH = fileURLToPath(
new URL("../../../../docs/tools-map.md", import.meta.url),
);
const SECTION_START = /^## 1\.\s/;
const SECTION_END = /^## 2\.\s/;
// Все id в реестре содержат дефис, поэтому формат строгий: это отсекает слова
// описания вроде `median-cut` из «colors (k, median-cut)».
const ID_SHAPE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
type Mention = { id: string; line: number };
/**
* Буллиты раздела «1. Реализовано» вместе с номером первой строки.
* Перенесённые строки приклеиваются к буллету: иначе id, оказавшийся на
* следующей строке, молча выпадал бы из разбора и инструмент считался бы
* неописанным.
*/
function implementedBullets(
markdown: string,
): { text: string; line: number }[] {
const lines = markdown.split("\n");
const start = lines.findIndex((l) => SECTION_START.test(l));
const end = lines.findIndex((l) => SECTION_END.test(l));
if (start === -1 || end === -1 || end <= start) {
throw new Error(
"docs/tools-map.md: не найден раздел «1. Реализовано» или «2.» — " +
"тест разбирает только реализованные инструменты",
);
}
const bullets: { text: string; line: number }[] = [];
for (let i = start; i < end; i++) {
const line = lines[i];
const text = line.trim();
const previous = bullets[bullets.length - 1];
const isBullet = text.startsWith("- ");
const isContinuation =
previous !== undefined &&
line.startsWith(" ") &&
!/^#{1,6}\s/.test(text) &&
!/^[-*]\s/.test(text);
if (isBullet) {
bullets.push({ text: text.slice(2), line: start + i + 1 });
} else if (isContinuation) {
previous.text += ` ${text}`;
}
}
return bullets;
}
/**
* Всё до первого тире `—` — id через `/` или `,`; после тире свободное
* описание параметров, оно не разбирается. Формат закреплён в самой карте.
*/
function mentionedIds(markdown: string): Mention[] {
const found: Mention[] = [];
for (const { text, line } of implementedBullets(markdown)) {
const head = text.split("—")[0];
for (const raw of head.split(/[/,]/)) {
const id = raw.trim().replace(/`/g, "");
if (ID_SHAPE.test(id)) found.push({ id, line });
}
}
return found;
}
describe("docs/tools-map.md ↔ реестр", () => {
const markdown = readFileSync(MAP_PATH, "utf8");
const mentioned = mentionedIds(markdown);
const registryIds = new Set(TOOLS.map((t) => t.id));
it("разбирает непустой список id (парсер не сломался молча)", () => {
expect(implementedBullets(markdown).length).toBeGreaterThan(0);
expect(new Set(mentioned.map((m) => m.id)).size).toBeGreaterThan(50);
});
it("в карте нет id, которых нет в реестре", () => {
const bogus = mentioned.filter((m) => !registryIds.has(m.id));
expect(
bogus,
bogus
.map(
(m) =>
`${m.id} (docs/tools-map.md:${m.line}) — такого инструмента нет в web/src/lib/registry/`,
)
.join("\n"),
).toEqual([]);
});
it("каждый инструмент реестра описан в карте", () => {
const mentionedSet = new Set(mentioned.map((m) => m.id));
const missing = [...registryIds].filter((id) => !mentionedSet.has(id));
expect(
missing,
`не описаны в разделе «Реализовано»:\n${missing.join("\n")}`,
).toEqual([]);
});
it("id не продублированы между буллетами", () => {
const seen = new Map<string, number>();
const dupes: string[] = [];
for (const { id, line } of mentioned) {
const first = seen.get(id);
if (first !== undefined) {
dupes.push(`${id} (строки ${first} и ${line})`);
} else {
seen.set(id, line);
}
}
expect(dupes, `повторяющиеся id:\n${dupes.join("\n")}`).toEqual([]);
});
});