From 1d337f065b9172093e003242b31f622fbfefe338 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 27 Sep 2026 18:54:43 +0500 Subject: [PATCH] docs: review all --- AGENTS.md | 35 +- README.md | 17 +- docs/README.md | 7 +- docs/agent-workflow.md | 11 +- docs/architecture.md | 87 +++-- docs/backlog.md | 140 ++++---- docs/checklist-manual-testing.md | 20 +- docs/decisions.md | 23 +- docs/plan-fields-audit-fixes.md | 225 ++++++------ docs/plan-platform.md | 42 +-- docs/plan-seo.md | 95 ++++-- docs/quality-gates.md | 6 +- docs/roadmap.md | 11 +- docs/testing-strategy.md | 24 +- docs/tools-audit.md | 45 ++- docs/tools-map.md | 319 +++++++++--------- .../{pipeline.spec.ts => tool-flows.spec.ts} | 0 web/eslint-plugins/README.md | 8 +- web/eslint.config.js | 2 +- web/playwright.config.ts | 4 +- web/src/lib/registry/tools-map-doc.test.ts | 70 +++- 21 files changed, 696 insertions(+), 495 deletions(-) rename web/e2e/{pipeline.spec.ts => tool-flows.spec.ts} (100%) diff --git a/AGENTS.md b/AGENTS.md index c2faa8e..8634e1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,19 +26,28 @@ Читать только нужную строку, а не все документы подряд. -| Задача | Документ | -| --------------------------------------------- | ---------------------------------- | -| Слои, поток данных, новый инструмент/поле | `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` | -| Правка полей и раскладки по волнам | `docs/plan-fields-audit-fixes.md` | -| Ручная приёмка UI | `docs/checklist-manual-testing.md` | -| Индекс остальных документов | `docs/README.md` | +| Задача | Документ | +| ---------------------------------------------- | ---------------------------------- | +| Слои, поток данных, новый инструмент/поле | `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` | +| Правка полей и раскладки по волнам | `docs/plan-fields-audit-fixes.md` | +| Ручная приёмка UI | `docs/checklist-manual-testing.md` | +| Направления развития, фазы 1–11 | `docs/roadmap.md` | +| SEO/GEO-план (адреса, `noindex`, sitemap) | `docs/plan-seo.md` | +| API/PWA/шрифты | `docs/plan-platform.md` | +| Переименование бренда и покупка домена | `docs/plan-domain.local.md` † | +| Сырые UX-находки по инструментам (вход аудита) | `docs/tools-audit.md` | +| Архивирование и удаление старых документов | `docs/archive/README.md` | +| Индекс остальных документов | `docs/README.md` | + +† `docs/plan-domain.local.md` вне git, в клоне отсутствует — в этом случае +задачу ищут по `docs/plan-seo.md` §6. ## Инварианты, которые линтер не проверяет diff --git a/README.md b/README.md index 49ac8db..c3aa3aa 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,16 @@ and open source. transparency/alpha, color, channel decomposition, geometry, filters, 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 → - sequential application → download result; pipelines saved to localStorage, - exportable as JSON - **Web Worker execution** — heavy operations run off the main thread with automatic fallback to direct calls - **i18n** — full Russian and English localization - **Dark/Light themes** — persisted to localStorage - **Pure TypeScript core** — `lib/core/` operates on `ImageData`/`Uint8ClampedArray` with no DOM dependency -- **Fully static** — no backend, no analytics, no tracking +- **Fully static** — no backend; the image never leaves your browser. The only + third-party request is a Google Analytics page-view counter — it never sees + the file or its contents; the boundary and its limits are in + `docs/plan-seo.md` §0 ## Tech Stack @@ -65,8 +65,9 @@ pnpm verify # full fast quality gate — the pre-review gate pnpm build # static production export → web/build/ ``` -Полный список команд и матрица «какие проверки запускать при каком изменении» — -в `AGENTS.md` и `docs/quality-gates.md`. +For the full command list and the "which checks to run for which change" matrix, +see [`AGENTS.md`](AGENTS.md) and +[`docs/quality-gates.md`](docs/quality-gates.md). ## Architecture @@ -74,8 +75,8 @@ Everything is built around the **two-layer registry** (`web/src/lib/registry/`): a tool is a self-contained implementation `{id, schema, input, result, run}` (`registry/tools/`), and a page is `{slug, title, description, category, steps}` (`registry/pages/`) that names the address and the tools to run — one tool can -back several pages. Pages, forms, and pipelines are generated from it, and all -execution goes through a single `executor.execute` entry point. +back several pages. Pages and forms are generated from it, and all execution +goes through a single `executor.execute` entry point. Core image processing lives in `web/src/lib/core/` and operates directly on `ImageData`, with no browser API dependencies. diff --git a/docs/README.md b/docs/README.md index fe6fca7..07e5e93 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ | Документ | Когда читать | Статус | | ---------------------------------- | -------------------------------------------------- | ------ | | `AGENTS.md` | Всегда: контракт агента | active | +| `docs/README.md` | Индекс документов, правила ведения документации | active | | `docs/architecture.md` | Слои, поток данных, новый инструмент/поле/маршрут | active | | `docs/quality-gates.md` | Какие проверки запускать, пороги, coverage, CI | active | | `docs/decisions.md` | Правка самих гейтов; чтение CI после пуша | active | @@ -60,9 +61,9 @@ `web/src/lib/registry/tools/`, адрес и имя страницы (`slug`) — в `web/src/lib/registry/pages/`; исторические названия в старых планах не действуют (`docs/architecture.md`, раздел 3). -- Поиск по `docs/` задевает `docs/archive/` (объём больше, чем у активных - документов вместе). Для правок production-кода архив не нужен: grep по - `docs/*.md` без `archive/`. +- Поиск по `docs/` задевает `docs/archive/`: там вынесены завершённые планы и + исследования, поэтому grep по `docs/` даёт больше шума, чем пользы. Для правок + production-кода архив не нужен: grep по `docs/*.md` без `archive/`. - Проверка ссылок в документации не автоматизирована; ожидаемо неразрешимыми считаются ссылки на `docs/plan-domain.local.md` (файл вне git). Всё остальное неразрешимое — ошибка. diff --git a/docs/agent-workflow.md b/docs/agent-workflow.md index 2a35e6e..4cf3254 100644 --- a/docs/agent-workflow.md +++ b/docs/agent-workflow.md @@ -12,7 +12,10 @@ 4. Определить границу изменения: production UI, shared-модуль или инфраструктура — `AGENTS.md`, раздел «Границы изменений». Изменение shared-кода требует проверки production-потребителей. -5. Не добавлять исключения линтера ради отдельных модулей без записи в baseline. +5. Не добавлять исключения линтера ради отдельных модулей: осознанные + отступления живут в конфиге правила (`allowPaths`, `allowWords`, + `ignoreMissingPatterns`, `allowDefaultProject` — + `web/eslint-plugins/README.md`), а не в самих проверяемых файлах. ## 2. Порядок изменения @@ -27,8 +30,10 @@ warnings и чтение CI — в `docs/quality-gates.md`. Здесь только то, что относится к работе агента. -`lint:all` может завершиться успешно при warnings. Это не означает чистый -baseline: нужно отдельно показать число errors, warnings и token warnings. +`lint:all` роняет gate на любых ESLint warnings (`--max-warnings=0`). +Единственное, что он пропускает, — unused-token сообщения (неблокирующие). +Поэтому «команда прошла» не равно «нечего чинить»: нужно отдельно показать число +errors, warnings и token warnings. E2E запускается отдельно от быстрых проверок, потому что поднимает build и статический сервер. Для UI-изменений сначала выбирается минимальный diff --git a/docs/architecture.md b/docs/architecture.md index 686a9f0..9d92b8a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,9 +16,9 @@ web/src/lib/ catalog.ts # группировка страниц и счётчики для /list-tools categories.ts # id/названия категорий tool-icons.ts # slug страницы → иконка - core/ # чистый TS: операции над ImageData, без DOM + core/ # операции над ImageData; без DOM, кроме io.ts/domText.ts executor/ # единственная точка исполнения инструментов - i18n/ # словари ru/en, t(), списки для i18n-правил линтера + i18n/ # словари ru/en, t(), состояние локали components/ # UI (fields/, layout/, ui/, schema-компоненты) zip.ts # сборка мультифайлового результата (1 → many) theme.svelte.ts # light/dark + localStorage @@ -30,9 +30,15 @@ web/src/routes/ ``` Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`, -`executor/**` и `core/**`; `core/**` не знает ни о ком и не зависит от DOM; -`registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`. -Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость. +`executor/**` и `core/**`; `core/**` не знает ни о ком; `registry/**` описывает +данные и знает только `core/**` и `registry-schema.ts`. Нарушение этого порядка +— повод не заводить импорт, а инвертировать зависимость. + +Исключение из «core без DOM» — `core/io.ts` (декодирование файла, кодирование, +скачивание) и `core/domText.ts` (canvas-рендер текста и эмодзи). Они трогают +`document`, `createImageBitmap` и `HTMLCanvasElement`, поэтому не работают в +node-Vitest и исключены из coverage-гейта (`docs/quality-gates.md`, раздел +Coverage). Всё остальное в `core/**` — чистые функции над `ImageData`. ### Чего в коде нет @@ -45,9 +51,11 @@ web/src/routes/ полей двух инструментов — отдельная работа, описанная как фаза 11 `docs/roadmap.md`. Под неё зарезервированы `components/StepCard.svelte` (сейчас импортируется только витриной `/kit`) и ключи `chain.*`, - `errors.badPipelineShape`, `errors.noSteps`. -- **Разделения страниц, которые делят один инструмент** — сводит 14 - convert-страниц на один инструмент; задача в `docs/backlog.md`. + `errors.badPipelineShape`, `errors.pipelineVersion`, `errors.noSteps`. +- **Разделения страниц, которые делят один инструмент** — сводит все + convert-страницы на один инструмент; задача в `docs/backlog.md`. Пока + `registry.test.ts` не требует, чтобы на инструмент ссылались две страницы, а + на практике не ссылается ни одна. Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по пайплайну — `docs/roadmap.md`. @@ -67,7 +75,7 @@ registry/{tools,pages}/*.ts ├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол) ├─► SchemaSourceTile/SchemaTextSource.svelte вход └─► SchemaPreview + schema-preview-model.ts вид превью - │ buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult + │ buildSchemaPreviewModel(input) → { sourceValue, resultValue, formatValue, hasResult } ▼ executor.execute(ctx) единственная точка исполнения │ сериализация параметров → worker → worker-handler → tool.run(ctx) @@ -104,9 +112,9 @@ registry/{tools,pages}/*.ts | `id` | внутреннее имя инструмента, уникально; ключ `tools..*` и `toolId` в worker | | `schema` | `toolSchema({ fields, layout?, meta? })` | | `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе | -| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт | -| `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую | -| `output` | mime/ext/quality для скачивания (не у всех) | +| `result`? | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт | +| `domOnly`? | инструмент требует DOM (canvas); превью-executor запускает его напрямую | +| `output`? | mime/ext/quality для скачивания (не у всех) | | `run(ctx)` | единственный метод: `ctx` → `ToolResult` | `Page` (записи в `registry/pages/.ts`): @@ -121,15 +129,22 @@ registry/{tools,pages}/*.ts Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по каталогу; `id` — ключ `tools..*` (подписи полей, `options`, `results`), -`toolId` worker-протокола и запись пайплайна. Один инструмент может обслуживать -несколько страниц, одна страница — состоять из нескольких инструментов. +`toolId` worker-протокола и запись пайплайна. Модель допускает, что один +инструмент обслуживает несколько страниц, а страница состоит из нескольких +инструментов; на практике пока каждая страница ссылается на свой инструмент, а +исполняется первый (и единственный) шаг. Инструмент работает с любым поддержанным форматом, поэтому png-интент живёт только в `slug`: `id: "crop"` → страница `slug: "crop-png"` → `crop-png.png`. -Правило имени: снять суффикс `-png` и в середине имени (`png-to-base64` → -`to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`); следствие — +Правило имени `id`: снять суффикс `-png` и в середине имени (`png-to-base64` → +`to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`). Правило +относится только к `id`: у `slug` своя история и SEO-значение, поэтому переименование `id` не трогает адреса, а переименование `slug` меняет адрес, имя -файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. +файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. Слаги, сложившиеся +до разделения реестра, правилу не соответствуют (`convert-png-to-jpg`, +`change-png-opacity`, `png-is-grayscale`, `verify-is-png` и другие) — это не +ошибка, менять их без SEO-решения нельзя. Форму slug проверяет +`registry.test.ts` (только форма, не смысл). Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS` @@ -154,15 +169,18 @@ registry/{tools,pages}/*.ts допустимому виду. Правка поведения полей идёт здесь, а не в компоненте. Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` → -контрол в `components/fields/schema/` → ключ в `FIELDS` → дефолт и санитизация → +контрол (`components/fields/schema/`, а для `dimension` — +`fields/DimensionField.svelte`) → ключ в `FIELDS` → дефолт и санитизация → i18n-подписи → тест в `registry-schema.test.ts`. ## 5. Исполнение -`executor/executor.ts` — фабрика `createExecutor(factories?)` плюс готовый -экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются, -исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает -сообщение и зовёт `run`. Если worker недоступен — прямой вызов. +`executor/executor.ts` — фабрика `createExecutor({ workerFactory? })` плюс +готовый экспорт `execute`. Контракт асинхронный (`Promise`), параметры +сериализуются по `executor/protocol.ts`, исполнение уходит в +`executor.worker.ts`; `worker-handler.ts` разбирает сообщение и зовёт `run`. +Если worker недоступен — прямой вызов. UI импортирует `execute` из +`executor/index.ts`. Отсюда следует: подключение wasm позже означает замену реализации **внутри** исполнителя, а не правку компонентов. @@ -170,13 +188,16 @@ i18n-подписи → тест в `registry-schema.test.ts`. ## 6. i18n Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с -`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и -читается через `t()`. Секции разделены по слоям: `pages..title` и -`pages..description` — тексты страницы (в базовой локали секция пустая, EN -берётся из реестра), `tools..options/params/results` — подписи полей и -тексты вердиктов инструмента, `fields.*`/`groups.*` — общие подписи схемы. -Инварианты паритета ключей, плейсхолдеров и запрет хардкода проверяются -линтером, детали — `web/eslint-plugins/README.md`. +`LOCALES`/`BASE_LOCALE`, `t.ts` с `t()`, `locale.svelte.ts` с состоянием +локали). Пользовательский текст живёт только в словарях и читается через `t()`. +Списки ключей и допустимых атрибутов для i18n-правил линтера лежат не здесь, а в +`web/eslint-plugins/i18n/lists.js`. Секции разделены по слоям: +`pages..title` и `pages..description` — тексты страницы (в базовой +локали секция пустая, EN берётся из реестра), +`tools..options/params/results` — подписи полей и тексты вердиктов +инструмента, `fields.*`/`groups.*` — общие подписи схемы. Инварианты паритета +ключей, плейсхолдеров и запрет хардкода проверяются линтером, детали — +`web/eslint-plugins/README.md`. ## 7. Точки расширения @@ -204,7 +225,7 @@ i18n-подписи → тест в `registry-schema.test.ts`. `from-base64`, `is-grayscale`). 4. **Описать схему** через `field.*`, задать `layout`, если поля нужно сгруппировать. Качество кодирования (`quality`) — не поле страницы, а - настройка скачивания (`docs/plan-seo.md` §1, S1f). + настройка скачивания (`docs/plan-seo.md` §4, S1f). 5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в `core/`, без DOM. 6. **Добавить страницу** в `registry/pages/.ts`: `slug` с адресным @@ -221,8 +242,10 @@ i18n-подписи → тест в `registry-schema.test.ts`. «Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug` страниц** из `registry/pages/`, то есть адреса. Проверяет `web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница - без записи в карте роняют тест. Обоснование и границы автоматизации — - `docs/decisions.md`, раздел 4. + без записи в карте роняют тест; разделы «Можно добавить» и «Идеи» + проверяются в обратную сторону — реализованный slug в плане тоже роняет + тест, поэтому при переезде инструмента из плана в реестр его вынимают + оттуда. Обоснование и границы автоматизации — `docs/decisions.md`, раздел 4. Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для инструмента, меняющего пользовательский поток, — e2e (см. раздел 3 diff --git a/docs/backlog.md b/docs/backlog.md index d4bbdea..4ed6972 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -1,36 +1,38 @@ # 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`. +> Статус: **active**. Открытые задачи. Подробные планы, разбор решений и сырые +> находки — `docs/plan-seo.md`, `docs/plan-platform.md`, +> `docs/plan-fields-audit-fixes.md`, `docs/tools-audit.md`, `docs/decisions.md`. ## lint и прочие правила - [ ] `tseslint.config(...)` deprecated: сигнатура `(...configs: InfiniteDepthConfigWithExtends[]): ConfigArray` помечена - устаревшей, нужен переход на `defineConfig` из `typescript-eslint` — - `web/eslint.config.js`. + устаревшей, нужен переход на `defineConfig` из `eslint/config` (ESLint + core, а не `typescript-eslint`) — `web/eslint.config.js`. - [ ] удалить мёртвые `tools.*.params` из root i18n после проверки потребителей; до этого действует pattern-based policy-исключение (`web/eslint-plugins/README.md`, раздел `dict-consistency`) ## Мелочи всякие -Порядок правок полей и раскладки — `docs/plan-fields-audit-fixes.md`, сами -находки («что плохо», с датой аудита) — `docs/tools-audit.md`. Здесь — как это -исправить; находка в аудите не отменяет задачу здесь. - -- [ ] Пересмотреть тесты - исправить всякие хрупкие, которые проверяют - количество инструментов, точное совпадение текстов, поиск по - `img[alt="Result image"]`, svelte классам и тому подобные +- [ ] split-into-parts-png - увеличить максимальные значения, плохо работает для + нарезки ассетов +- [ ] crop, resize (но скорее всего все инструменты) - при загрузке нового + изображения сбрасывает размеры, настроенные пользователем +- [ ] split-into-parts-png, resize (может еще какие-то) - возможность указывать + размеры из заранее одобренного списка размеров - 16х16, 32х32, 64х64 как + минимум, может еще какие-то размеры добавить, единый список из предыдущей + задачи - (для инди разработчиков, стандартные тайлы часто делаются) +- [ ] e2e: `i18n.spec.ts` и `text-and-verdicts.spec.ts` дублируют локаторы из + `helpers/page.ts` вместо вызова — правило «спека зовёт helpers» + (`docs/testing-strategy.md`, раздел 5) +- [ ] мёртвый i18n-ключ `noParams` («нет параметров — результат готов»): его + ничем не читают. Либо показывать подсказку для инструментов с пустой + схемой (FA-E1), либо удалить ключ из обеих локалей +- [ ] Пересмотреть тесты — убрать точные совпадения текстов и любые селекторы, + привязанные к разметке или svelte-классам (сейчас остались: + `tool-flows.spec.ts`, `schema-tool-strings.test.ts`) - [ ] нужен инструмент для удаления фона по выбранной точке - удалять внутри выделенной области - [ ] сделать конвертацию в смайлики\награды для твитча (6 размеров или выбор @@ -63,8 +65,10 @@ строку для svg - форматирование или подсветку синтаксиса??? для bin\hex - форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder - [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone) -- [ ] square-mask-png - проверить работу - параметры неверно считаются. width - - 100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку +- [ ] square-mask-png — перепроверить баг-репорт: он устарел, в схеме больше нет + параметров `width`/`height`/`x`/`y` (теперь `widthPct`/`heightPct` 10..100 + и один `offset` в диапазоне −50..50, значения 100 недостижимы). Либо + подтвердить, что баг исправлен, либо найти новый repro - [ ] прикрутить кастомный color picker, добавить прозрачность (range?) - [ ] проверить настройки range slider на crop-png - слишком большие значения - [ ] для range slider field - добавить поле для ввода цифр и кнопку сброса @@ -72,48 +76,45 @@ - [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было изменено - [ ] обновить страницу kit или сделать storybook -- [ ] Свести convert-страницы на один инструмент `convert` — реализация уже - умеет «скопировать пиксели, декодировать вход, отдать в заданном формате», - а страницы различаются только форматом вывода. **Зависит от S1f** - (`docs/plan-seo.md` §1): пока `quality` живёт в схеме страницы, у - `convert-png-to-bmp` и `convert-png-to-jpg` разные наборы полей, и один - инструмент на них не сойдётся. Качество — настройка кнопки Download - («Download as JPEG»), а не поле страницы; после его переезда пресеты схемы - не нужны, страницы различаются только `output` и `params` шага. Модель - страницы к этому готова: `Page.steps` — список инструментов, один - инструмент может обслуживать много страниц (`docs/architecture.md`, раздел - 3). +- [ ] Свести convert-страницы (`convert-png-to-jpg`, `convert-png-to-webp`, + `png-to-bmp`) на один инструмент `convert` — зависит от S1f + (`docs/plan-seo.md` §4). Мешает не только `quality` (он живёт в схеме + инструмента), а сама модель: `output` с mime/ext — свойство `Tool`, а + `PageStep` умеет только `{ id, params? }`, поэтому страницы на общий `id` + получат одинаковый формат вывода. Нужно научить шаг переопределять формат + вывода - [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop, удаление, добавление инструментов. переход один инструмент - pipeline без перезагрузки - [ ] `CatalogToolbar`: фильтры используют сырой `