docs: update plans, backlog

This commit is contained in:
2026-09-27 11:11:13 +05:00
parent e703451474
commit 2fc7801162
14 changed files with 463 additions and 311 deletions
+2
View File
@@ -17,6 +17,7 @@
| Юнит-тесты | `pnpm --dir web test` | | Юнит-тесты | `pnpm --dir web test` |
| Тесты lint-правил | `pnpm --dir web test:rules` | | Тесты lint-правил | `pnpm --dir web test:rules` |
| Линтинг | `pnpm --dir web lint:all` | | Линтинг | `pnpm --dir web lint:all` |
| Только токены | `pnpm --dir web lint:tokens` |
| E2E | `pnpm --dir web test:e2e` | | E2E | `pnpm --dir web test:e2e` |
| Форматирование | `pnpm format` (код + docs) | | Форматирование | `pnpm format` (код + docs) |
| Проверка форматирования | `pnpm check:docs` + `pnpm --dir web format:check` | | Проверка форматирования | `pnpm check:docs` + `pnpm --dir web format:check` |
@@ -35,6 +36,7 @@
| Детали lint-правил и токенов | `web/eslint-plugins/README.md` | | Детали lint-правил и токенов | `web/eslint-plugins/README.md` |
| Задачи и идеи | `docs/backlog.md` | | Задачи и идеи | `docs/backlog.md` |
| Актуальный список инструментов | `docs/tools-map.md` | | Актуальный список инструментов | `docs/tools-map.md` |
| Правка полей и раскладки по волнам | `docs/plan-fields-audit-fixes.md` |
| Ручная приёмка UI | `docs/checklist-manual-testing.md` | | Ручная приёмка UI | `docs/checklist-manual-testing.md` |
| Индекс остальных документов | `docs/README.md` | | Индекс остальных документов | `docs/README.md` |
+20 -6
View File
@@ -21,19 +21,22 @@
| `docs/agent-workflow.md` | Порядок работы, нагрузка на машину, протокол ревью | active | | `docs/agent-workflow.md` | Порядок работы, нагрузка на машину, протокол ревью | active |
| `docs/testing-strategy.md` | Правка e2e: locator-ы, fixtures, browser matrix | active | | `docs/testing-strategy.md` | Правка e2e: locator-ы, fixtures, browser matrix | active |
| `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active | | `docs/backlog.md` | Продуктовые, UX- и инфраструктурные задачи | active |
| `docs/roadmap.md` | Направления развития, фазы 1–10 | active | | `docs/roadmap.md` | Направления развития, фазы 1–11 | active |
| `docs/tools-map.md` | Актуальный список инструментов против реестра | active | | `docs/tools-map.md` | Актуальный список инструментов против реестра | active |
| `docs/tools-audit.md` | Сырые UX/UI-находки по инструментам (вход аудита) | active | | `docs/tools-audit.md` | Сырые UX/UI-находки по инструментам (вход аудита) | active |
| `docs/plan-fields-audit-fixes.md` | Разбивка аудита на волны правок | draft | | `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` | API/PWA/шрифты-план | active | | `docs/plan-platform.md` | API/PWA/шрифты-план | active |
| `docs/plan-domain.local.md` | Чек-лист переименования бренда и покупки домена | active |
| `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active | | `docs/checklist-manual-testing.md` | Ручная приёмка production UI | active |
| `web/eslint-plugins/README.md` | Правка lint-правил, токенов и их конфигов | active | | `web/eslint-plugins/README.md` | Правка lint-правил, токенов и их конфигов | active |
| `docs/archive/README.md` | Архивирование и удаление старых документов | active | | `docs/archive/README.md` | Архивирование и удаление старых документов | active |
`docs/plan-domain.local.md` исключён из git через `.gitignore` (выбранное имя `docs/plan-domain.local.md` **исключён из git** через `.gitignore`: выбранное
домена не должно попасть в историю до покупки). Ссылки на него в `plan-seo.md` и имя домена не должно попасть в историю до покупки. В клоне файла нет — это
`backlog.md` при клонировании репозитория не разрешаются — это ожидаемо. ожидаемо, и ссылки на него в `plan-seo.md` и `backlog.md` не разрешаются. Строка
в таблице оставлена, чтобы задачу R1/R2 можно было найти по индексу; сам
чек-лист лежит только на машине разработчика.
## Правила ## Правила
@@ -46,11 +49,22 @@
`docs/commands.md` или `docs/known-issues.md`: команды принадлежат `docs/commands.md` или `docs/known-issues.md`: команды принадлежат
`quality-gates`, известные проблемы — backlog и checklist. `README.md` в корне `quality-gates`, известные проблемы — backlog и checklist. `README.md` в корне
остаётся кратким обзором продукта, а не вторым руководством агента. остаётся кратким обзором продукта, а не вторым руководством агента.
- Ссылаясь на инструмент по id, сверять id с реестром (`web/src/lib/registry/`):
исторические названия в старых планах не действуют.
- `docs/backlog.md` содержит только открытые задачи: выполненные удаляются, а - `docs/backlog.md` содержит только открытые задачи: выполненные удаляются, а
критичные для проекта факты из них переносятся в тематический документ. критичные для проекта факты из них переносятся в тематический документ.
- `docs/archive/` — временная историческая справка, не источник критериев - `docs/archive/` — временная историческая справка, не источник критериев
приёмки; срок хранения 30 дней, файлы восстановимы из git history. приёмки; срок хранения 30 дней, файлы восстановимы из git history.
Единственный документ в архиве со статусом `active` —
`docs/archive/README.md`: он описывает процедуру архивирования, а не архивный
материал, поэтому не подлежит 30-дневному удалению.
- Ссылаясь на инструмент по id, сверять id с реестром (`web/src/lib/registry/`):
исторические названия в старых планах не действуют. Сейчас `id` записи — это
одновременно адрес страницы `/tools/<id>`, отдельного поля `slug` нет
(`docs/architecture.md`, раздел 3).
- Поиск по `docs/` задевает `docs/archive/` (объём больше, чем у активных
документов вместе). Для правок production-кода архив не нужен: grep по
`docs/*.md` без `archive/`.
- Проверка ссылок в документации не автоматизирована; ожидаемо неразрешимыми
считаются ссылки на `docs/plan-domain.local.md` (файл вне git). Всё остальное
неразрешимое — ошибка.
- Протокол «остановиться и показать изменения» — `docs/agent-workflow.md`, - Протокол «остановиться и показать изменения» — `docs/agent-workflow.md`,
раздел 6. раздел 6.
+70 -36
View File
@@ -23,7 +23,7 @@ web/src/lib/
web/src/routes/ web/src/routes/
+page.svelte # главная +page.svelte # главная
list-tools/ # каталог list-tools/ # каталог
tools/[id]/ # страница инструмента tools/[id]/ # страница инструмента (адрес = id записи)
kit/ # витрина компонентов (production, но исключена из i18n-гейта) kit/ # витрина компонентов (production, но исключена из i18n-гейта)
``` ```
@@ -32,6 +32,21 @@ web/src/routes/
`registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`. `registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`.
Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость. Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость.
### Чего в коде нет
Страница инструмента работает ровно с одним инструментом: загрузил файл →
применил одну запись реестра → скачал результат. Отсутствуют:
- **Пайплайн / цепочки шагов** — модуля нет, состояния нет, localStorage для
него нет. `docs/roadmap.md` описывает его как отдельную фазу, а не как
поставленное. Под будущую фазу зарезервированы `components/StepCard.svelte`
(сейчас импортируется только витриной `/kit`) и ключи `chain.*`,
`errors.badPipelineShape`, `errors.noSteps`.
- **Разделения `id` и `slug`** — раздел 3.
Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по
пайплайну — `docs/roadmap.md`.
## 2. Поток данных ## 2. Поток данных
```txt ```txt
@@ -62,8 +77,8 @@ registry/*.ts ──(TOOLS: ToolEntry[])
Ключевые решения: Ключевые решения:
- **Реестр — единственный источник истины.** Страница, форма и пайплайн строятся - **Реестр — единственный источник истины.** Страница и форма строятся из
из записи; новый инструмент = новая запись + функция `run`. записи; новый инструмент = новая запись + функция `run`.
- **Исполнение идёт только через `executor.execute`.** Компоненты не зовут - **Исполнение идёт только через `executor.execute`.** Компоненты не зовут
`tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки `tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки
вызывающего кода. вызывающего кода.
@@ -76,22 +91,35 @@ registry/*.ts ──(TOOLS: ToolEntry[])
`ToolEntry` (`web/src/lib/registry/types.ts`): `ToolEntry` (`web/src/lib/registry/types.ts`):
| Поле | Смысл | | Поле | Смысл |
| ------------- | ------------------------------------------------------------ | | ------------- | ------------------------------------------------------------------------------------------------ |
| `id` | уникальный ключ; он же URL-anchor и ключ в пайплайне | | `id` | идентичность инструмента **и** публичный адрес `/tools/<id>` **и** база имени скачиваемого файла |
| `title` | EN-строка реестра, переводится через `tools.*.title` | | `title` | EN-строка реестра, переводится через `tools.*.title` |
| `description` | EN-строка реестра, переводится через `tools.*.description` | | `description` | EN-строка реестра, переводится через `tools.*.description` |
| `category` | id из `categories.ts` | | `category` | id из `categories.ts` |
| `schema` | `toolSchema({ fields, layout?, meta? })` | | `schema` | `toolSchema({ fields, layout?, meta? })` |
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе | | `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт | | `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `output` | mime/ext/quality для download (не у всех инструментов) | | `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую, минуя worker |
| `run(ctx)` | чистая функция: `ctx` → `ToolResult` | | `output` | mime/ext/quality для download (не у всех инструментов) |
| `icon` | id иконки из `tool-icons.ts` |
| `run(ctx)` | единственный метод: `ctx` → `ToolResult` |
Отдельного поля `slug` **нет**: адрес страницы, ключ i18n (`tools.<id>.*`) и имя
файла скачивания — это один и тот же `id`. Инструмент работает с любым
поддержанным форматом, но страница держит png-интент, поэтому `-png` живёт
внутри `id` (`id: "crop-png"` → `/tools/crop-png` → `crop-png.png`). Следствие:
переименование `id` меняет адрес страницы, ключи `tools.*` и имя файла.
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS` случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный
способ достать запись по id. способ достать запись, `registry.test.ts` проверяет уникальность `id`.
Разделение `id` (внутреннее имя) и `slug` (адрес) — решённое, но **не
реализованное** решение: задача и порядок волн в `docs/backlog.md`, раздел
«Мелочи всякие». До её выполнения этот документ описывает текущее состояние, а
id и slug совпадают.
## 4. Схема параметров ## 4. Схема параметров
@@ -133,15 +161,16 @@ EN-ключей `tools.*` и словарь-переводчик. Инвариа
## 7. Точки расширения ## 7. Точки расширения
| Задача | Где править | | Задача | Где править |
| ------------------------------ | ----------------------------------------------------------------- | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Новый инструмент | запись в `registry/<group>.ts` + i18n-ключи + тесты (рецепт ниже) | | Новый инструмент | запись в `registry/<group>.ts` + i18n-ключи + тесты (рецепт ниже) |
| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) | | Смена `id` | запись реестра + i18n-ключи `tools.*` + `tool-icons.ts` + хардкод id в тестах, `/kit` и e2e; меняется адрес страницы — операция с SEO-последствиями, не часть обычной задачи |
| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге | | Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) |
| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` | | Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге |
| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу | | Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` |
| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов | | Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу |
| Мультифайловый результат | `result: "files"` + `zip.ts` | | Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов |
| Мультифайловый результат | `result: "files"` + `zip.ts` |
## 8. Рецепт: добавить инструмент ## 8. Рецепт: добавить инструмент
@@ -150,22 +179,27 @@ EN-ключей `tools.*` и словарь-переводчик. Инвариа
2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/ 2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/
`generate`/`text`/`analyze`) и положить запись в её экспорт; при новой `generate`/`text`/`analyze`) и положить запись в её экспорт; при новой
категории — сначала `categories.ts`. категории — сначала `categories.ts`.
3. **Описать схему** через `field.*`, задать `layout`, если поля нужно 3. **Задать `id`.** Он же адрес страницы, он же ключ i18n, он же база имени
скачиваемого файла. Страница держит png-интент, поэтому суффикс `-png` входит
в `id` (`crop-png`, `resize-png`) — это осознанно, отдельного поля `slug` в
реестре нет (раздел 3).
4. **Описать схему** через `field.*`, задать `layout`, если поля нужно
сгруппировать. сгруппировать.
4. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в 5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
`core/`, без DOM. `core/`, без DOM.
5. **Проверить обязательные контракты** для нового id: entry попадает в `TOOLS`, 6. **Проверить обязательные контракты** для новой записи: она попадает в
страница `/tools/<id>` prerender-ится, `input`/`result` согласованы с тем, `TOOLS`, страница `/tools/<id>` prerender-ится, `input`/`result` согласованы
что делает `run`. с тем, что делает `run`.
6. **Добавить i18n-ключи во все локали** сразу. 7. **Добавить i18n-ключи во все локали** сразу (ключ — по `id`).
7. **Тесты:** unit — в `registry/*.test.ts` или рядом с `core`-функцией; 8. **Тесты:** unit — в `registry/*.test.ts` или рядом с `core`-функцией;
e2e-smoke — существующий spec по типу `input`/`result` (правила — e2e-smoke — существующий spec по типу `input`/`result` (правила —
`docs/testing-strategy.md`). `docs/testing-strategy.md`).
8. **Обновить `docs/tools-map.md`:** новый инструмент обязателен в разделе 9. **Обновить `docs/tools-map.md`:** новая страница обязательна в разделе
«Реализовано», формат строки — в шапке раздела. Проверяет «Реализовано», формат строки — в шапке раздела. Карта перечисляет **`id`**
`web/src/lib/registry/tools-map-doc.test.ts`: отсутствующий в карте id и реестра, которые одновременно являются адресами страниц. Проверяет
несуществующий в реестре id роняют тест. Обоснование и границы автоматизации `web/src/lib/registry/tools-map-doc.test.ts`: id без инструмента и инструмент
— `docs/decisions.md`, раздел 4. без записи в карте роняют тест. Обоснование и границы автоматизации —
`docs/decisions.md`, раздел 4.
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3 инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
+9 -3
View File
@@ -1,7 +1,9 @@
# Архив документов # Архив документов
> Статус: **active**. Здесь хранятся завершённые, отменённые и явно устаревшие > Статус: **active**. Здесь хранятся завершённые, отменённые и явно устаревшие
> документы как временная историческая справка. > документы как временная историческая справка. Этот README — единственный
> документ архива со статусом `active`: он описывает процедуру, а не архивный
> материал, поэтому не подлежит 30-дневному удалению.
## Назначение ## Назначение
@@ -36,8 +38,12 @@
2. Убрать из него ссылки на несуществующие пути и артефакты прошлых волн. 2. Убрать из него ссылки на несуществующие пути и артефакты прошлых волн.
3. Добавить обязательные метаданные. 3. Добавить обязательные метаданные.
4. Переместить файл в `docs/archive/`. 4. Переместить файл в `docs/archive/`.
5. Проверить ссылки из backlog, планов, checklist и `docs/README.md`: удалённые 5. Найти оставшиеся входящие ссылки:
файлы должны потерять и входящие ссылки. `git grep -n "<filename>" -- 'docs/*.md' AGENTS.md README.md web/eslint-plugins/README.md`.
Шаг 4 ломает их, и агент без этого шага напишет в новом документе ссылку на
удалённый файл. Ожидаемо неразрешимыми остаются ссылки на
`docs/plan-domain.local.md` — он вне git, это не ошибка (`docs/README.md`,
раздел «Правила»).
6. Не менять архивный документ под новые требования. 6. Не менять архивный документ под новые требования.
## Удаление ## Удаление
+72 -41
View File
@@ -24,6 +24,10 @@
## Мелочи всякие ## Мелочи всякие
Порядок правок полей и раскладки — `docs/plan-fields-audit-fixes.md`, сами
находки («что плохо», с датой аудита) — `docs/tools-audit.md`. Здесь — как это
исправить; находка в аудите не отменяет задачу здесь.
- [ ] Пересмотреть тесты - исправить всякие хрупкие, которые проверяют - [ ] Пересмотреть тесты - исправить всякие хрупкие, которые проверяют
количество инструментов, точное совпадение текстов, поиск по количество инструментов, точное совпадение текстов, поиск по
`img[alt="Result image"]`, svelte классам и тому подобные `img[alt="Result image"]`, svelte классам и тому подобные
@@ -55,6 +59,9 @@
шрифты с кириллицей? сделать галочку "только с кириллицей"??? (решение о шрифты с кириллицей? сделать галочку "только с кириллицей"??? (решение о
наборе и лицензиях: `docs/plan-platform.md` §4 — только OFL/Apache с наборе и лицензиях: `docs/plan-platform.md` §4 — только OFL/Apache с
паспортом, семейства с кириллицей) паспортом, семейства с кириллицей)
- [ ] поле ввода текста - сделать по умолчанию нормального размера, а не одну
строку для svg - форматирование или подсветку синтаксиса??? для bin\hex -
форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder
- [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone) - [ ] сделать загрузку картинки исходника по ctrl+v и drag-and-drop (dropzone)
- [ ] square-mask-png - проверить работу - параметры неверно считаются. width - - [ ] square-mask-png - проверить работу - параметры неверно считаются. width -
100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку 100, height - 100, x - 100, y - 100, обрезает 50% на 50% картинку
@@ -64,16 +71,32 @@
значения значения
- [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было - [ ] для keep Aspect field - пересчитывать второе поле, учитывать какое было
изменено изменено
- [ ] поле ввода текста - сделать по умолчанию нормального размера, а не одну
строку для svg - форматирование или подсветку синтаксиса??? для bin\hex -
форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder
- [ ] обновить страницу kit или сделать storybook - [ ] обновить страницу kit или сделать storybook
- [ ] Слаги инструментов (`*-png`) — **развести `id` и адрес страницы.**
Инструменты работают с любым форматом, но называются `crop-png`,
`resize-png`; имя в коде, пайплайнах и i18n должно быть `crop`, `resize`,
а адрес и имя скачиваемого файла — прежними (`/tools/crop-png` →
`crop-png.png`). Решение и его SEO-последствия: `docs/plan-seo.md` §0;
текущее состояние (поля `slug` нет, роут — `tools/[id]`) —
`docs/architecture.md`, раздел 3. Порядок работ: 1. добавить `slug` в
`ToolEntry` + `toolSlug()` / `getToolBySlug()` в `registry/index.ts`,
проверку уникальности `slug` в `registry.test.ts`; 2. перевести
`routes/tools/[id]` → `[slug]` (`entries()`, `load()`, `+page.svelte`),
`ToolCard`, крошку в `+layout.svelte` и имя файла download в
`SchemaToolView` на `toolSlug(tool)`; старые адреса продолжают работать,
редиректы не нужны; 3. переименовать `id` волнами по группам реестра
(`convert`, `alpha`, `color`, `geometry`, `filters`, `analyze`,
`generate`, `text`); в каждой волне — `registry/<group>.ts`, ключи
`tools.*` в `en.ts`/`ru.ts`, `tool-icons.ts`, захардкоженные id в тестах и
e2e (`smoke-i18n.test.ts`, `kit/+page.svelte`); 4. `docs/tools-map.md` при
этом **не меняется**: карта перечисляет id, а они остаются прежними как
адреса. Обратной совместимости не делаем: формат пайплайна ещё не
устоялся, старые экспорты и записи в localStorage после переименования
просто перестают открываться — это осознанно, миграция усложнила бы код
ради пары пользователей.
- [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop, - [ ] придумать процесс редактирования и настройки pipeline - drag-n-drop,
удаление, добавление инструментов. переход один инструмент - pipeline без удаление, добавление инструментов. переход один инструмент - pipeline без
перезагрузки перезагрузки
- [ ] Слаги инструментов (`*-png`) — надо переименовть, они работают с любыми
изображениями. Надо сделать отдельный интерфейс маппинга инструмент ->
ссылка
- [ ] `CatalogToolbar`: фильтры используют сырой `<button>`, а не `ui/Button` — - [ ] `CatalogToolbar`: фильтры используют сырой `<button>`, а не `ui/Button` —
единый контрол для всей библиотеки единый контрол для всей библиотеки
(`web/src/lib/components/CatalogToolbar.svelte`). (`web/src/lib/components/CatalogToolbar.svelte`).
@@ -99,17 +122,20 @@
## SEO/GEO ## SEO/GEO
Общий план: `docs/plan-seo.md` (S0–S4). Старт возможен — пока только domain-free Общий план: `docs/plan-seo.md` (S1–S4). Старт возможен — пока только domain-free
часть S1. часть S1. Полное содержание этапов не дублируется здесь, только идентификаторы.
- [ ] переименование бренда + домен: чек-лист в локальном плане - [ ] переименование бренда + домен: чек-лист в локальном файле
`docs/plan-domain.local.md` (вне git, имя домена в репо не публикуется; `plan-domain.local.md` (в git не входит, в клоне отсутствует; подробности
план-seo §6, R1/R2) и порядок — `docs/plan-seo.md` §6, R1/R2)
- [ ] снять `noindex` со страниц инструментов, когда будут sitemap, canonical и
контент (S1h; блокирует индексацию всего сайта) — `docs/plan-seo.md` §4
- [ ] download с селектором формата PNG/JPG/WebP/BMP для всех инструментов (райз - [ ] download с селектором формата PNG/JPG/WebP/BMP для всех инструментов (райз
№7 → S1f) «Download» → S1f)
- [ ] форматные страницы (`crop-jpeg`, …) — только по вордстату с уникальным - [ ] форматные страницы по конкурентным слагам (`crop-jpeg`, …) — только по
контентом; редиректы только для опечаток; клонов с canonical не делать вордстату с уникальным контентом; редиректы только для опечаток и
(план §5) переименований уже опубликованных страниц; клонов с canonical не делать
(`docs/plan-seo.md` §5)
- [ ] sitemap.xml из реестра + `Sitemap:` в robots.txt (S1b/S1c) - [ ] sitemap.xml из реестра + `Sitemap:` в robots.txt (S1b/S1c)
- [ ] Seo.svelte: canonical + OG-теги, per-tool OG-картинки (S1d/S1e) - [ ] Seo.svelte: canonical + OG-теги, per-tool OG-картинки (S1d/S1e)
- [ ] контент страницы инструмента: seo-блок в словарях, H1/how-to/FAQ/related - [ ] контент страницы инструмента: seo-блок в словарях, H1/how-to/FAQ/related
@@ -119,12 +145,14 @@
## Платформа (API/PWA) ## Платформа (API/PWA)
Общий план: `docs/plan-platform.md`; WASM-ядро и CLI уже в `docs/roadmap.md` Общий план: `docs/plan-platform.md`; WASM-ядро, эталоны и CLI —
(фазы 3–8) — здесь только новые треки. `docs/roadmap.md` (фазы 2–8). Здесь только треки, которых нет в roadmap.
- [ ] PWA: manifest + service worker, оффлайн-режим, установка (после S1) - [ ] PWA: manifest + service worker, оффлайн-режим, установка (после S1)
- [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier - [ ] API: спайк серверного ядра (нативный Rust vs edge-wasm), ключи, free tier
(после wasm-фаз 5–7) (после wasm-фаз 5–7)
- [ ] Шрифты: лицензирование (F1–F4) и DOM-free рендер (F5–F6) — до фазы 8
(`docs/plan-platform.md` §4)
## Качество production UI ## Качество production UI
@@ -137,6 +165,10 @@
## Идеи ## Идеи
Идеи 1–5 и 15 относятся к **пайплайну (цепочкам шагов)**, которого в коде пока
нет: это фаза 11 `docs/roadmap.md`. Идеи 6–14 — к существующим инструментам и
UI. Оценки (S/M/L) — оценка труда, не приоритет.
1. **Менять первый инструмент цепочки** — оценка M. Сейчас базовый инструмент 1. **Менять первый инструмент цепочки** — оценка M. Сейчас базовый инструмент
зафиксирован при открытии страницы; хочется заменить его на другой без зафиксирован при открытии страницы; хочется заменить его на другой без
разборки цепочки: шаги и их параметры сохраняются, вход перечитывается. разборки цепочки: шаги и их параметры сохраняются, вход перечитывается.
@@ -149,8 +181,9 @@
3. **Несколько цепочек: сохранение и загрузка** — оценка M. Именованные цепочки 3. **Несколько цепочек: сохранение и загрузка** — оценка M. Именованные цепочки
в localStorage (список, создать/переименовать/удалить), быстрое переключение. в localStorage (список, создать/переименовать/удалить), быстрое переключение.
Формат экспортного JSON расширить опциональным полем имени, старые файлы Формат экспортного JSON расширить опциональным полем имени; обратной
читаются как безымянные. совместимости со старыми выгрузками не делаем (см. задачу про `id`/`slug`
выше — формат ещё не устоялся).
4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив 4. **Batch-обработка архивов выбранным chain** — оценка L. Вход — zip-архив
картинок (распаковка в браузере), прогон текущего или выбранного сохранённого картинок (распаковка в браузере), прогон текущего или выбранного сохранённого
@@ -164,16 +197,15 @@
поднимается в выдаче поиска (бонус к popularity при скоринге). поднимается в выдаче поиска (бонус к popularity при скоринге).
6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download** 6. **Конвертация формата — НЕ отдельный инструмент, а выбор в кнопке Download**
— оценка M/L. Инструменты `convert-png-to-jpg` / `convert-png-to-webp` (и др. — оценка M/L. Решение и продуктовое обоснование: `docs/plan-seo.md` §0 и
форматы) не должны жить как самостоятельные инструменты цепочки. Механическая задача **S1f**; почему селектор не заменяет существующие convert-страницы —
часть уже есть: `ToolEntry.output` (`OutputFormat`: mime/ext/qualityParamId) `docs/plan-seo.md` §5. Механическая часть уже есть: `ToolEntry.output`
и кодирование по нему в `SchemaToolView.download()` — `docs/architecture.md`, (`OutputFormat`: mime/ext/qualityParamId) и кодирование по нему в
раздел 3. Осталось (UX-райз «Download»): `SchemaToolView.download()` — `docs/architecture.md`, раздел 3. Что осталось
- выбор формата/качества прямо в кнопке/диалоге Download для любого сверх S1f:
результата (не отдельными конвертерами);
- экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png` - экспорт с лимитом размера → закрывает отложенный `reduce-to-size-png`
(целевой KB, бинарный поиск по k из квантования). Проработать вместе с (целевой KB, бинарный поиск по k из квантования). Проработать вместе с
достройкой download-флоу нового UI. достройкой download-флоу.
7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна 7. **`watermark-image-png` (водяной знак картинкой)** — оценка M. Нужна
overlay-механика: второй источник изображения (getOverlay/store), параметры overlay-механика: второй источник изображения (getOverlay/store), параметры
@@ -208,20 +240,19 @@
а `--color-danger` в light-теме на границе: после завершения редизайна а `--color-danger` в light-теме на границе: после завершения редизайна
вернуть порог 4.5 и перепроверить оба токена. вернуть порог 4.5 и перепроверить оба токена.
14. **Отображение маски для инструментов (runMask/preview-mask)** — придумать и 14. **Отображение маски для инструментов (`runMask`)** — придумать и реализовать
реализовать процесс показа маски результата. **Контекст:** в старом процесс показа маски результата. **Контекст:** удалённый UI-v1 умел
контракте поле `preview` у `remove-color`/`remove-background` показывало показывать ч/б превью-маску «что будет вырезано» отдельным полем; при
быстрый ч/б превью-маску (`colorMask`/`backgroundMaskPreview`) того, что переходе на единый `run(ctx)` этого поля не осталось — executor и
будет вырезано. При миграции на единый `run(ctx)` поле убрано (шаг 8, schema-превью зовут `run()` напрямую, маска не возвращается. **Идея 1:**
`web/src/lib/registry/alpha.ts`) — новый executor и schema-превью зовут дать каждому `run` возможность вернуть (или инструмент — объявить) `runMask`
`run()` напрямую, маска пока не нужна. **Идея 1:** дать каждому `run` — ч/б изображение маски изменений либо иной подход (отдельное поле `mask` +
возможность вернуть (или инструмент — объявить) `runMask` — ч/б изображение функция маски, второй результат, overlay). Оценка — на усмотрение при
маски изменений либо иной подход (отдельное поле `mask` + функция маски, проработке (S/M). Пока живём без неё. **Идея 2:** каждый `run` возвращает
второй результат, overlay). Оценка — на усмотрение при проработке (S/M). два изображения — для отображения (показывает маску или сам результат) и для
Пока живём без неё. **Идея 2:** каждый `run` возвращает два изображения — передачи в следующий инструмент (всегда результат). Оценка — на усмотрение
для отображения (показывает маску или сам результат) и для передачи в при проработке (S/M). Пока живём без неё. Первые кандидаты на маску:
следующий инструмент (всегда результат). Оценка — на усмотрение при `remove-color-from-png`, `remove-background-png`.
проработке (S/M). Пока живём без неё.
15. **Набор файлов в пайплайне: 1 → many не должен быть терминальным** — оценка 15. **Набор файлов в пайплайне: 1 → many не должен быть терминальным** — оценка
L. **Сейчас** `execute` принимает один `source: PixelImage | undefined` и L. **Сейчас** `execute` принимает один `source: PixelImage | undefined` и
+60 -45
View File
@@ -2,14 +2,24 @@
> Статус: **draft** — на ревью. > Статус: **draft** — на ревью.
> >
> Источник: `docs/tools-audit.md`. Этот документ разбивает аудит на конкретные > Находки — `docs/tools-audit.md` (что плохо, с датой прохода). Этот документ
> шаги, группирует по типу работы и фиксирует решения. Порядок и правила > задаёт **порядок правок** полей и раскладки волнами; сами задачи — в
> проверки — `docs/quality-gates.md`; точки касания при добавлении вида поля — > `docs/backlog.md`. Проверки и гейты — `docs/quality-gates.md`; точки касания
> `docs/architecture.md`, раздел 4. > при добавлении вида поля — `docs/architecture.md`, раздел 4.
> >
> **Ids инструментов в таблицах волны B сверены с реестром.** Названия из > **Идентификаторы шагов — `FA-*` (fields-audit).** Буквы `C*`/`Q*` в других
> `tools-audit.md` вида `png-to-hsl` или `color-wheel-generator` в реестре нет > документах относятся к архивным волнам редизайна и здесь не используются.
> (`hsl`, `color-wheel-png`). >
> **Как читать волны.** Шаг с пометкой «реализовано» — закрытый, его правку
> можно не повторять; шаг без пометки — работа. Пометка стоит в первом абзаце
> шага, а не в заголовке, чтобы не потерять её при чтении оглавления. Задачи,
> которые в бэклоге, здесь не дублируются: этот документ задаёт **порядок**,
> backlog — **учёт** (`docs/backlog.md`).
>
> **Ids инструментов сверены с реестром** (`web/src/lib/registry/`) для шагов
> FA-B и волн C–E. Названия из `docs/tools-audit.md` вида `png-to-hsl`,
> `color-wheel-generator`, `webp-to-png` в реестре отсутствуют (реальные —
> `hsl`, `color-wheel-png`): при переносе пункта аудита в шаг id сверять.
## 0. Ключевое решение: select → buttons ## 0. Ключевое решение: select → buttons
@@ -87,7 +97,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
## 1. Волна A — инфраструктура (1 коммит) ## 1. Волна A — инфраструктура (1 коммит)
### Шаг A1. Новый тип `buttons` в схеме ### FA-A1. Новый тип `buttons` в схеме
Файлы: Файлы:
@@ -96,7 +106,7 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
- `web/src/lib/components/fields/schema/ButtonsControl.svelte` — новый компонент - `web/src/lib/components/fields/schema/ButtonsControl.svelte` — новый компонент
- `web/src/lib/components/SchemaFields.svelte` — импорт + запись в `FIELDS` - `web/src/lib/components/SchemaFields.svelte` — импорт + запись в `FIELDS`
### Шаг A2. Seed → input + randomize ### FA-A2. Seed → input + randomize
Для `field.number()` с `kind: "seed"` (или новый подтип `field.seed()`) — Для `field.number()` с `kind: "seed"` (или новый подтип `field.seed()`) —
рендерить поле ввода + кнопку «Random» вместо range slider. рендерить поле ввода + кнопку «Random» вместо range slider.
@@ -122,7 +132,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 ### FA-B1. geometry.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ----------------------- | -------- | ----------------------------------------- | | ----------------------- | -------- | ----------------------------------------- |
@@ -138,7 +148,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 ### FA-B2. color.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------------ | --------- | ------------------------------------ | | ------------------------ | --------- | ------------------------------------ |
@@ -150,7 +160,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 ### FA-B3. analyze.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------------- | ---- | ------------------ | | ------------------------- | ---- | ------------------ |
@@ -162,13 +172,13 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
Все используют общий `maskBaseFields` — правка в одном месте. Все используют общий `maskBaseFields` — правка в одном месте.
### B4. filters.ts ### FA-B4. filters.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------- | ---- | ------------ | | ------------- | ---- | ------------ |
| add-noise-png | mode | mono / color | | add-noise-png | mode | mono / color |
### B5. generate.ts ### FA-B5. generate.ts
| Инструмент | Поле | Опции | | Инструмент | Поле | Опции |
| ------------------ | ------------ | --------------------- | | ------------------ | ------------ | --------------------- |
@@ -187,14 +197,15 @@ buttons: <V extends string>(s: Omit<ButtonsSpec<V>, "kind">): Field<V> => ({
## 3. Волна C — обязательные фиксы (баги + дефолты) ## 3. Волна C — обязательные фиксы (баги + дефолты)
### C1. Source-aware defaults для dimension-полей ### FA-C1. Source-aware defaults для dimension-полей — реализовано
Source-aware defaults реализованы: общий resolver в `registry-schema` Source-aware defaults реализованы: общий resolver в `registry-schema`
(`applySourceDefaults`). Для `resize-png` и `crop-png` дефолт после загрузки (`applySourceDefaults`). Для `resize-png` и `crop-png` дефолт после загрузки
равен размерам текущего source; до загрузки используется положительный fallback равен размерам текущего source; до загрузки используется положительный fallback
`1×1`. Один-sided `0` у resize сохраняет режим auto. `1×1`. Один-sided `0` у resize сохраняет режим auto. Проверено тестом
`registry.test.ts` («resize и crop получают размеры текущего source»).
### C2. symmetric-copy-png: "keep side" не работает ### FA-C2. symmetric-copy-png: "keep side" не работает
Баг в `run()`: `keepSide` не учитывает `axis`. При `axis: "vertical"` (удвоение Баг в `run()`: `keepSide` не учитывает `axis`. При `axis: "vertical"` (удвоение
ширины) работают только `left`/`right`, `top`/`bottom` не имеют смысла. Нужно: ширины) работают только `left`/`right`, `top`/`bottom` не имеют смысла. Нужно:
@@ -205,11 +216,12 @@ Source-aware defaults реализованы: общий resolver в `registry-s
значение. Пока достаточно в `run()`: если `axis === "vertical"` и `keepSide` ∈ значение. Пока достаточно в `run()`: если `axis === "vertical"` и `keepSide` ∈
{top, bottom} → заменить на `left`. {top, bottom} → заменить на `left`.
### C3. Source-aware bounds (отдельный следующий шаг) ### FA-C3. Source-aware bounds (отдельный следующий шаг) — не начат
Реализованы только defaults dimension-полей через `SchemaContext` и Source-aware **defaults** реализованы через `SchemaContext` и
`applySourceDefaults`. Динамические min/max для crop x/y, shift и resize `applySourceDefaults`. Динамические min/max для crop x/y, shift и resize не
остаются отдельной задачей: bounds не следует смешивать с default resolver. реализованы и остаются отдельной задачей: bounds не следует смешивать с default
resolver.
**Что нужно спроектировать и завести:** **Что нужно спроектировать и завести:**
@@ -240,40 +252,43 @@ Source-aware defaults реализованы: общий resolver в `registry-s
Это отдельный рефакторинг; source-aware defaults не заменяют bounds. Это отдельный рефакторинг; source-aware defaults не заменяют bounds.
### C4. verify-is-png: неверное название ### FA-C4. verify-is-png: неверное название — решение принято, шаг не выполнен
Проверяет текст, не PNG-файл. Переименовать: Проверяет текст, не PNG-файл. Варианты:
- `id` → `verify-is-png-data` (или оставить для обратной совместимости) - `title` → "Verify PNG data", `description` → уточнить; `id` оставить — он же
- `title` → "Verify PNG data" адрес страницы, переименование ломает ссылку
- `description` → уточнить - либо полное переименование `id` → `verify-is-png-data` с редиректом со старого
URL (нужен вордстат, иначе страница выпадает из индекса)
**Решение:** поменять title и description, id не трогать (url-dependent). **Решение:** поменять title и description, id не трогать. Переименование `id`
затрагивает адрес — с ним связана задача разведения `id`/`slug`
(`docs/backlog.md`), поэтому в одиночку не делается.
--- ---
## 4. Волна D — обязательные фиксы (новые параметры) ## 4. Волна D — обязательные фиксы (новые параметры)
### D1. remove-alpha-channel-png: выбор цвета фона ### FA-D1. remove-alpha-channel-png: выбор цвета фона
Сейчас захардкожен `#ffffff`. Добавить `field.color({ default: "#ffffff" })`. Сейчас захардкожен `#ffffff`. Добавить `field.color({ default: "#ffffff" })`.
Файл: `alpha.ts`, схема `removeAlphaChannelSchema`. Файл: `alpha.ts`, схема `removeAlphaChannelSchema`.
### D2. extract-alpha-mask-png: галочка «инвертировать» ### FA-D2. extract-alpha-mask-png: галочка «инвертировать»
Добавить `field.checkbox({ label: "fields.invertMask", default: false })`. В Добавить `field.checkbox({ label: "fields.invertMask", default: false })`. В
`run()`: если `invert`, вызвать `invertAlpha()` после `extractAlphaMask()`. `run()`: если `invert`, вызвать `invertAlpha()` после `extractAlphaMask()`.
Файл: `alpha.ts`. Файл: `alpha.ts`.
### D3. change-canvas-size-png: position9 вместо select ### FA-D3. change-canvas-size-png: position9 вместо select
`anchor` сейчас `field.select()` с 9 опциями → `field.position9()`. `anchor` сейчас `field.select()` с 9 опциями → `field.position9()`.
Файл: `geometry.ts`. Уже есть `PositionControl`. Файл: `geometry.ts`. Уже есть `PositionControl`.
### D4. Generators: добавить height ### FA-D4. Generators: добавить height
Инструменты без `size`/`height`: Инструменты без `size`/`height`:
@@ -287,7 +302,7 @@ Source-aware defaults реализованы: общий resolver в `registry-s
Файл: `generate.ts`. Файл: `generate.ts`.
### D5. add-border-png: прозрачность цвета ### FA-D5. add-border-png: прозрачность цвета
Текущий `field.color()` не поддерживает alpha. Пока что: оставить как есть Текущий `field.color()` не поддерживает alpha. Пока что: оставить как есть
(прозрачность не поддерживается нативным color picker). Отметить в аудите как (прозрачность не поддерживается нативным color picker). Отметить в аудите как
@@ -297,29 +312,29 @@ blocked.
## 5. Волна E — полировка (некритично) ## 5. Волна E — полировка (некритично)
### E1. Плейсхолдер "нет параметров" ### FA-E1. Плейсхолдер "нет параметров"
Инструменты с пустой схемой (`EmptyParams`) выглядят странно. Добавить Инструменты с пустой схемой (`EmptyParams`) выглядят странно. Добавить
`EmptyState` или подсказку "No parameters — click Run". `EmptyState` или подсказку "No parameters — click Run".
Файл: `SchemaToolView.svelte` — показать подсказку если `schema.fields` пуст. Файл: `SchemaToolView.svelte` — показать подсказку если `schema.fields` пуст.
### E2. quantize-png: пресеты ### FA-E2. quantize-png: пресеты
`field.slider` с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под `field.slider` с max 64 → добавить пресеты (8, 16, 32, 64) как кнопки под
слайдером. Либо `field.buttons()` с 4 опциями вместо слайдера. слайдером. Либо `field.buttons()` с 4 опциями вместо слайдера.
### E3. trim-empty-space-png: 0-254 → проценты ### FA-E3. trim-empty-space-png: 0-254 → проценты
Текущий `min: 0, max: 254` — нестандартно. Добавить переключатель px/% (аналог Текущий `min: 0, max: 254` — нестандартно. Добавить переключатель px/% (аналог
шага A1 — `variant: "alpha-threshold"` в NumberSpec). шага FA-A1 — `variant: "alpha-threshold"` в NumberSpec).
### E4. add-text-png: plate offset ### FA-E4. add-text-png: plate offset
Plate снизу имеет большой отступ — добавить галочку "compact plate" или Plate снизу имеет большой отступ — добавить галочку "compact plate" или
уменьшить дефолтный padding. уменьшить дефолтный padding.
### E5. emoji-to-png: выбор эмодзи ### FA-E5. emoji-to-png: выбор эмодзи
Поле `text` → добавить группу часто используемых эмодзи как кнопки- пресеты над Поле `text` → добавить группу часто используемых эмодзи как кнопки- пресеты над
полем ввода. полем ввода.
@@ -328,12 +343,11 @@ Plate снизу имеет большой отступ — добавить г
## 6. Верификация ## 6. Верификация
После каждой волны: Проверки и их минимальный набор — `docs/quality-gates.md`, раздел «Быстрые
подмножества»; правила e2e — `docs/testing-strategy.md`. Для волн с изменением
1. `pnpm --dir web build` — без ошибок пользовательского потока (новый контрол, новая раскладка) обязателен ручной
2. `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json` — без ошибок смоук в браузере: часть находок — про воспринимаемость, которую не ловит ни один
3. `pnpm --dir web lint` — без ошибок (кроме ожидаемых долгов) гейт. `pnpm verify` — перед ревью волны.
4. Ручная проверка в браузере: параметры рендерятся, инструменты работают
--- ---
@@ -346,7 +360,8 @@ Plate снизу имеет большой отступ — добавить г
- **Pixel ↔ Percent** (circle-mask, square-mask, star-mask, wavy-mask — %→px, - **Pixel ↔ Percent** (circle-mask, square-mask, star-mask, wavy-mask — %→px,
round-corners — px→%): нужен компонент переключателя единиц в слайдере. round-corners — px→%): нужен компонент переключателя единиц в слайдере.
Отложен до волны E, т.к. требует правки `RangeControl` + всех схем. Требует правки `RangeControl` + всех схем — в волны A–E не входит, нужен
отдельный этап.
- **Smoothing checkbox** для масок (circle, square, star, wavy, round-corners, - **Smoothing checkbox** для масок (circle, square, star, wavy, round-corners,
invert-alpha): нужен `field.checkbox("smoothing")` + правка core (shapes.ts, invert-alpha): нужен `field.checkbox("smoothing")` + правка core (shapes.ts,
alpha). Отложен — требует визуального тестирования. alpha). Отложен — требует визуального тестирования.
+27 -18
View File
@@ -1,18 +1,19 @@
# План: платформа — общее ядро, API и PWA # План: платформа — общее ядро, API и PWA
> **Статус (2026-09-09):** план-набросок. WASM-ядро и CLI уже описаны в > **Статус: active.** Этот план добавляет к `docs/roadmap.md` то, чего там нет:
> `docs/roadmap.md` (фазы 3–8) и здесь не дублируются — этот план добавляет то, > **API** (фаза 9, возможная монетизация) и **PWA** (фаза 10, оффлайн +
> чего в roadmap нет: **API** (фаза 9, возможная монетизация) и **PWA** (фаза > установка), плюс шрифты (раздел 4 — частично относится к фазе 8, поэтому
> 10, оффлайн + установка). Оба стартуют после wasm-фаз; PWA не ждёт API и может > решения-phase 8 здесь дублируются). WASM-ядро, эталоны и CLI — фазы 2–8
> идти раньше. > `docs/roadmap.md`, они здесь не пересказываются. Оба трека стартуют после
> wasm-фаз; PWA не ждёт API и может идти раньше.
## 0. Решения ## 0. Решения
- **Одно ядро — три потребителя.** Rust-крейт `core/` (roadmap фаза 3) даёт: - **Одно ядро — три потребителя.** Rust-крейт `core/` (roadmap фаза 4) даёт:
wasm-пакет для сайта (фаза 5), нативный CLI (фаза 6), серверный слой API (фаза wasm-пакет для сайта (фаза 5), нативный CLI (фаза 6), серверный слой API (фаза
9). Единый контракт — JSON-пайплайн `{tool, params}` (заложен в roadmap §0); 9). Единый контракт — JSON-пайплайн `{tool, params}` (заложен в roadmap §0,
эталонные файлы и harness (фазы 2, 4, 7) гарантируют одинаковый результат во реализуется в фазе 11); эталонные файлы и harness (фазы 2, 3, 7) гарантируют
всех рантаймах. одинаковый результат во всех рантаймах.
- **API — нативный Rust на сервере, не wasm.** В браузере wasm нужен ради - **API — нативный Rust на сервере, не wasm.** В браузере wasm нужен ради
доставки и песочницы; на сервере крейт линкуется напрямую — быстрее и проще. доставки и песочницы; на сервере крейт линкуется напрямую — быстрее и проще.
Исключение — edge-воркеры (см. спайк ниже): там исполняется тот же Исключение — edge-воркеры (см. спайк ниже): там исполняется тот же
@@ -29,7 +30,7 @@
- [ ] **[A1]** Спайк сервера: (а) axum/actix + нативный `core/` на VPS против - [ ] **[A1]** Спайк сервера: (а) axum/actix + нативный `core/` на VPS против
(б) wasm-артефакт фазы 5 на edge (CF Workers). Сравнить: стоимость, лимиты (б) wasm-артефакт фазы 5 на edge (CF Workers). Сравнить: стоимость, лимиты
памяти/времени на больших картинках, сложность деплоя и rate-limiting. памяти/времени на больших картинках, сложность деплоя и rate-limiting.
Решение зафиксировать здесь. Решение записать в §0 этого файла.
- [ ] **[A2]** Контракт: `POST /v1/process` — multipart (image) + JSON-пайплайн - [ ] **[A2]** Контракт: `POST /v1/process` — multipart (image) + JSON-пайплайн
→ изображение/архив; лимиты размера, коды ошибок; OpenAPI-спека. → изображение/архив; лимиты размера, коды ошибок; OpenAPI-спека.
- [ ] **[A3]** Ключи и лимиты: Bearer-ключи, free tier (N запросов/мес), rate - [ ] **[A3]** Ключи и лимиты: Bearer-ключи, free tier (N запросов/мес), rate
@@ -63,7 +64,8 @@
## 3. Порядок и связи ## 3. Порядок и связи
- WASM-ядро, эталоны, CLI — `docs/roadmap.md` фазы 2–8 (не дублируются). - WASM-ядро, эталоны, CLI — `docs/roadmap.md` фазы 2–8; пайплайн как продуктовая
фича — фаза 11 того же документа.
- PWA — после S1 `plan-seo.md`; API — после фаз 5–7. - PWA — после S1 `plan-seo.md`; API — после фаз 5–7.
- Синергия с дистрибуцией: «works offline, installable» и публичный API — - Синергия с дистрибуцией: «works offline, installable» и публичный API —
аргументы для Product Hunt / alternative.to / GitHub (plan-seo S4c) и для аргументы для Product Hunt / alternative.to / GitHub (plan-seo S4c) и для
@@ -71,11 +73,16 @@
## 4. Шрифты: лицензирование и DOM-free рендер текста ## 4. Шрифты: лицензирование и DOM-free рендер текста
Текст — единственный класс операций, который roadmap (фаза 8) держит в TS из-за Текст — один из canvas-зависимых классов, которые roadmap (фаза 8) держит в TS
canvas. Для CLI/API его всё равно придётся рендерить в Rust, поэтому решение: (там их четыре: текст, градиенты, штампы, `ctx.filter`); для текста выбран курс
**перевести text-инструменты в ядро** (в браузере canvas-рендер остаётся на реимплементацию, остальные классы не переносятся. Для CLI/API текст всё равно
TS-фолбэком для A/B). Два вопроса — легальность шрифтов в дистрибутиве и рендер придётся рендерить в Rust, поэтому решение: **перевести text-инструменты в
без DOM. ядро** (в браузере canvas-рендер остаётся TS-фолбэком для A/B). Два вопроса —
легальность шрифтов в дистрибутиве и рендер без DOM.
**Когда начинать:** блока F1–F4 нет — они не привязаны к фазе roadmap, но F5/F6
без них бессмысленны (нечего рендерить). Реализуется всё это в фазе 8, поэтому
лицензирование стоит закрыть до неё, а не после.
### Лицензирование (решить до выбора шрифтов, backlog «Font Style») ### Лицензирование (решить до выбора шрифтов, backlog «Font Style»)
@@ -90,8 +97,10 @@ TS-фолбэком для A/B). Два вопроса — легальност
право класть TTF в дистрибутив CLI/API — у OFL/Apache таких ограничений право класть TTF в дистрибутив CLI/API — у OFL/Apache таких ограничений
нет, у прочих «free»-лицензий бывают. нет, у прочих «free»-лицензий бывают.
- [ ] **[F3]** Паспорт шрифта в `core/fonts/<name>/` (или эквивалент): файл(ы), - [ ] **[F3]** Паспорт шрифта в `core/fonts/<name>/` (или эквивалент): файл(ы),
LICENSE, источник, версия, хеш; CI-проверка «у каждого файла шрифта есть LICENSE, источник, версия, хеш; **новый гейт** «у каждого файла шрифта
паспорт». Браузер грузит шрифты из бандла, не из ОС и не с есть паспорт» — его ещё нет, проектируется здесь и добавляется в
`pnpm verify` по правилам `docs/quality-gates.md` (решение о fail-режиме —
`docs/decisions.md`). Браузер грузит шрифты из бандла, не из ОС и не с
`fonts.googleapis.com`-CDN (сторонний запрос = утечка IP пользователя, `fonts.googleapis.com`-CDN (сторонний запрос = утечка IP пользователя,
противоречит «no tracking»), — одинаковый результат у всех пользователей и противоречит «no tracking»), — одинаковый результат у всех пользователей и
во всех рантаймах. во всех рантаймах.
+100 -64
View File
@@ -1,38 +1,41 @@
# План: SEO + GEO — индексация и конкуренция в image tools # План: SEO + GEO — индексация и конкуренция в image tools
> **Статус (2026-09-11; уточнено 2026-09-27):** план к выполнению; маршруты > **Статус: active.** Адреса страниц инструментов — `/tools/<id>` (`crop-png` и
> инструментов — корневые (`/tools/[id]`). **`noindex` на страницах инструментов > подобные), каталог — `/list-tools`; отдельного поля `slug` в реестре пока нет,
> не выставляется** — снимать нечего, каталог открыт для индексации; запрет > `id` и есть адрес.
> индексации держится только на служебных путях (`robots.txt`). Задачи S1
> разбиты на domain-free (можно делать параллельно с покупкой домена) и
> domain-required (после покупки и настройки домена).
> >
> **Цель:** каждая страница инструмента — индексируемый лендинг под конкретный > **`noindex, nofollow` выставлен на каждой странице инструмента**
> интент («crop png», «make png transparent», «jpg to png»…), конкуренция с > (`web/src/routes/tools/[id]/+page.svelte`) — осознанно, до появления
> onlinepngtools.com за ядро запросов + попадание в ответы генеративных систем > уникального контента (S2). Пока он стоит, сайт не индексируется вообще, и
> (ChatGPT, Perplexity, AI Overviews) — GEO. > **S1b–S1e (sitemap, `robots.txt` с `Sitemap:`, canonical, OG) не дают
> эффекта**: их имеет смысл делать вместе со снятием `noindex`, а не вместо
> него. Условие снятия и порядок — §4, S1.
> >
> Задача: «для каждого инструмента — своя страница, и можно реально > **Цель:** страница каждого инструмента — индексируемый лендинг под конкретный
> конкурировать с onlinepngtools». > интент («crop png», «make png transparent», «jpg to png»…) плюс попадание в
> ответы генеративных систем (ChatGPT, Perplexity, AI Overviews) — GEO.
> >
> **Позиционирование (уточнено 2026-09-09):** сайт — не «PNG-инструменты», а > **Позиционирование:** не «PNG-инструменты», а **image tools с PNG-ядром** —
> **image tools с PNG-ядром**: любой инструмент работает с PNG/JPG/WebP/GIF/BMP > любой инструмент принимает PNG/JPG/WebP/GIF/BMP и отдаёт результат в
> на входе и отдаёт результат в PNG/JPG/WebP/BMP через селектор в кнопке > PNG/JPG/WebP/BMP через селектор в кнопке Download. Дифференциатор —
> Download. Дифференциатор — прозрачность/альфа/фон и no-upload-приватность. > прозрачность/альфа/фон и no-upload-приватность. Референс-конкурент по ядру —
> Референс-конкурент по ядру — onlinepngtools.com (см. §2). Долгосрочно ядро > onlinepngtools.com (§2). Долгосрочно ядро переезжает на wasm и обрастает API +
> переезжает на wasm и обрастает API + PWA (оффлайн/установка) — > PWA (`docs/plan-platform.md`); для SEO это аргументы дистрибуции и GEO-факты,
> `docs/roadmap.md` фазы 3–10, `docs/plan-platform.md`; для SEO это аргументы > схему сайта они не меняют.
> дистрибуции (S4c) и GEO-факты, схему сайта они не меняют. >
> **Слаги в этом документе:** `jpg-to-png`, `crop-jpeg` и подобное — интенты и
> URL конкурента, а не id наших инструментов. Наши id — в `docs/tools-map.md`; в
> тексте ниже реальные id инструментов приводятся в бэктиках.
## 0. Решения ## 0. Решения
- **Бренд: переименовать до индексации** (новое имя и домен сознательно не - **Бренд: переименовать до индексации** (новое имя и домен сознательно не
записаны в этом репозитории — см. §6 и локальный план записаны в этом репозитории — чек-лист в локальном файле
`docs/plan-domain.local.md`, исключён из git: выбранное имя не должно утекать `plan-domain.local.md`, см. §6, он вне git: выбранное имя не должно утекать в
в историю коммитов до покупки домена). Легенда честная: «started as PNG tools, историю коммитов до покупки домена). Легенда честная: «started as PNG tools,
now all formats» — это контент-плюс, а не баг. Названия инструментов now all formats» — это контент-плюс, а не баг. Адреса страниц
(`crop-png`, …) и localStorage-ключи не переименовывать (см. решение про слаги (`/tools/crop-png`) и localStorage-ключи не переименовывать (см. решение про
ниже). Точка истины по бренду: константа в i18n + README; хардкод в `slug` ниже). Точка истины по бренду: константа в i18n + README; хардкод в
компонентах недопустим. компонентах недопустим.
- **Форматы: формат = выбор в UI, не отдельные страницы.** Download-кнопка - **Форматы: формат = выбор в UI, не отдельные страницы.** Download-кнопка
каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз каждого инструмента — с селектором PNG/JPG/WebP/BMP (+quality для lossy). Райз
@@ -40,10 +43,19 @@
есть (`ToolEntry.output`), нужна как продуктовая основа «image есть (`ToolEntry.output`), нужна как продуктовая основа «image
tools»-позиционирования. В направлении «из формата в PNG» отдельных tools»-позиционирования. В направлении «из формата в PNG» отдельных
инструментов нет (загрузка принимает любой формат) — см. §5. инструментов нет (загрузка принимает любой формат) — см. §5.
- **Слаги инструментов `-png` сохраняются.** Они держат низкоконкурентные - **Адреса страниц `-png` сохраняются, внутренние имена — без.** Инструмент
png-интенты; широкие интенты («crop image online») переносятся теми же работает с любым форматом, поэтому его `id` не должен врать (`crop`, а не
страницами контентом: H1 «Crop image», текст «works with PNG, JPG, WebP». `crop-png`); png-интент держит адрес страницы и имя скачиваемого файла.
URL-алиасы без `-png` не создаются (канонический беспорядок). Требуется поле `slug` в `ToolEntry` + переход `routes/tools/[id]` → `[slug]`.
**Сейчас не реализовано:** в реестре `id: "crop-png"`, поля `slug` нет, роут —
`tools/[id]/`; текущее состояние описано в `docs/architecture.md`, раздел 3.
**Следствие для SEO: редиректы не нужны, canonical и sitemap не меняются,
адреса остаются теми же** — поэтому это безопасная правка, которую можно
делать до индексации, а не после. Побочная выгода: широкие интенты («crop
image online») переносятся теми же страницами контентом — H1 «Crop image»,
текст «works with PNG, JPG, WebP»; URL-алиасы без `-png` по-прежнему не
создаются (канонический беспорядок). Порядок работ — `docs/backlog.md`, задача
«Слаги инструментов (`*-png`) — развести `id` и адрес страницы».
- **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия — - **Индексируемая локаль — EN** (базовая; конкурент англоязычный). RU-версия —
отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang отдельно и позже: префикс `/ru/`, prerender обеих локалей, hreflang
(сознательно отложено). Пока язык переключается на клиенте (localStorage) — (сознательно отложено). Пока язык переключается на клиенте (localStorage) —
@@ -53,10 +65,11 @@
canonical и «корневость» страниц. Свой домен + корень. Текущий деплой — GitHub canonical и «корневость» страниц. Свой домен + корень. Текущий деплой — GitHub
Pages (`.github/workflows/deploy.yml`); перенос на свой домен выполняется Pages (`.github/workflows/deploy.yml`); перенос на свой домен выполняется
сменой origin в CI и `PUBLIC_SITE_ORIGIN`. сменой origin в CI и `PUBLIC_SITE_ORIGIN`.
- **URL-схема инструментов:** короткие плоские адреса. После C17 страницы - **URL-схема инструментов:** короткие плоские адреса — `/tools/<id>` для
инструментов находятся в корне на `/tools/[id]`, каталог — на `/list-tools`; инструмента, `/list-tools` для каталога, где `<id>` берётся из записи реестра.
текущую схему не менять без отдельного SEO-плана: после индексации Схему не менять без отдельного SEO-плана: после индексации переименование URL
переименование URL означает 301 и потерю позиций. означает 301 и потерю позиций. Разделение `id`/`slug` (решение выше) адресов
не меняет.
- **Без GA и рекламных скриптов:** аналитика — self-hosted Plausible/Umami или - **Без GA и рекламных скриптов:** аналитика — self-hosted Plausible/Umami или
Cloudflare Web Analytics. «No tracking» — часть позиционирования, её нельзя Cloudflare Web Analytics. «No tracking» — часть позиционирования, её нельзя
подрывать собственными скриптами. подрывать собственными скриптами.
@@ -83,6 +96,7 @@
| Пробел | Где сейчас | Этап | | Пробел | Где сейчас | Этап |
| -------------------------------------------------- | ----------------------------------- | ---- | | -------------------------------------------------- | ----------------------------------- | ---- |
| `noindex` на страницах инструментов | стоит (`+page.svelte`) | S1 |
| `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 |
@@ -93,6 +107,10 @@
| `llms.txt`, хабы категорий с текстом | нет | S3 | | `llms.txt`, хабы категорий с текстом | нет | S3 |
| GSC + приватная аналитика | нет | S4 | | GSC + приватная аналитика | нет | S4 |
Строка `noindex` — первая не потому, что её сложнее всего, а потому, что она
блокирует всё остальное: пока тег стоит, остальные строки таблицы не дают
трафика. Подробности и условие снятия — §4, S1.
## 2. Конкурент (onlinepngtools.com) ## 2. Конкурент (onlinepngtools.com)
Снято с живого сайта (2026-09-09): Снято с живого сайта (2026-09-09):
@@ -141,6 +159,15 @@
Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и Разбито на две группы: **domain-free** (делаем параллельно с покупкой домена) и
**domain-required** (после покупки и DNS-настройки). **domain-required** (после покупки и DNS-настройки).
- [ ] **[S1h]** Снять `noindex, nofollow` со страниц инструментов
(`web/src/routes/tools/[id]/+page.svelte`) — заменить на
`<meta name="robots" content="index, follow" />`. **Это первым
обязательным шагом S1:** пока тег стоит, остальные задачи S1 готовят
инфраструктуру, но не приводят трафик. Порядок: S1h ставится после
S1b/S1c/S1d, иначе Google проиндексирует сайт без sitemap и canonical.
Снимать `noindex` до готовности S2-контента осознанно не стоит: в индекс
попадут тонкие страницы с одним title/description.
#### S1-free: без домена #### S1-free: без домена
- [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на - [ ] **[S1e]** OG-картинки per-tool: генерация PNG-карточек скриптом на
@@ -150,12 +177,12 @@
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 (`docs/backlog.md`, «Идеи» №6, UX-райз «Download»). Продуктовая основа
tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести «image tools»-позиционирования: без неё сайт остаётся PNG-сайтом. Вести
параллельно с текущими quality-задачами. параллельно с текущими quality-задачами.
- [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов - [ ] **[S1g]** Поведение конвертеров после S1f: не предлагать в меню форматов
формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в формат исходника (disable) — чтобы `convert-png-to-jpg` не мог сохранить в
png. Существующие страницы не трогать: `jpg-to-png`, `convert-png-to-jpg`, png. Существующие страницы не трогать: `convert-png-to-jpg`,
`convert-png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами `convert-png-to-webp`, `png-to-bmp` остаются самостоятельными лендингами
со своим контентом (§5). со своим контентом (§5).
@@ -177,9 +204,14 @@
twitter:card. Подключить в root-layout. **Требует `PUBLIC_SITE_ORIGIN`** twitter:card. Подключить в root-layout. **Требует `PUBLIC_SITE_ORIGIN`**
для абсолютных URL в canonical и OG:url. для абсолютных URL в canonical и OG:url.
> **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d. Гейты: build > **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d → **S1h**
> зелёный; в `build/` лежит корректный `sitemap.xml` со всеми инструментами; > (снятие `noindex`) → S2-контент. Гейты: `pnpm --dir web build` зелёный; в
> canonical/OG присутствуют на всех страницах; e2e зелёные. > `build/` лежит корректный `sitemap.xml` со всеми инструментами; canonical/OG
> присутствуют на всех страницах; `noindex` отсутствует в собранном HTML
> (`pnpm --dir web build` и проверка `build/tools/*/index.html`);
> `pnpm --dir web test:e2e` — по правилам `docs/quality-gates.md`, в CI job
> `e2e` non-blocking, поэтому его статус читается по отдельному job
> (`docs/decisions.md`, раздел 10).
### S2. Контентный слой (параллельно quality-плану, волнами по категориям) ### S2. Контентный слой (параллельно quality-плану, волнами по категориям)
@@ -278,11 +310,13 @@
Три возможных механики — важно не перепутать: Три возможных механики — важно не перепутать:
1. **301-редирект `/crop-jpeg` → `/crop-png`** — законно только для опечаток и 1. **301-редирект на существующую страницу** (`/crop-jpg` → `/crop-jpeg`, или
альтернативных написаний (`crop-jpg` → `crop-jpeg`) и переименований `/crop-png` → новый слаг после переименования) — законно **только** для
существующих страниц. Для самостоятельного запроса редирект — это отказ от опечаток, альтернативных написаний и переименований **уже опубликованной**
страницы: Google перестаёт показывать редирект, ранжировать будет только страницы: URL, которого никогда не было в индексе, редиректом не закрывается.
цель. Как посадочная под «crop jpeg» не работает вообще. Для самостоятельного запроса редирект — это отказ от страницы: Google
перестаёт показывать редирект, ранжировать будет только цель. Как посадочная
под «crop jpeg» не работает вообще.
2. **Зеркало с canonical на `/crop-png`** — технически легально, но бесполезно: 2. **Зеркало с canonical на `/crop-png`** — технически легально, но бесполезно:
canonical-дубль в индекс не попадает, видимости не добавляет, а массовое canonical-дубль в индекс не попадает, видимости не добавляет, а массовое
производство таких зеркал выглядит как накрутка площади и при ошибке в производство таких зеркал выглядит как накрутка площади и при ошибке в
@@ -307,9 +341,9 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- **«convert png to jpg»** — интент «страница-конвертер»: пользователь ищет - **«convert png to jpg»** — интент «страница-конвертер»: пользователь ищет
конкретную пару форматов (лучшее качество конверсии, меньше ошибок), это конкретную пару форматов (лучшее качество конверсии, меньше ошибок), это
готовые лендинги. Существующие (`jpg-to-png`, `png-to-jpg`, `png-to-webp`, готовые лендинги. Существующие (`convert-png-to-jpg`, `convert-png-to-webp`,
`png-to-bmp`, `svg-to-png`, …) **оставить и наполнить контентом как все**: `png-to-bmp`, `svg-to-png`, `base64-to-png`, …) **оставить и наполнить
свой H1 («Convert PNG to JPG — free, no upload»), FAQ про контентом как все**: свой H1 («Convert PNG to JPG — free, no upload»), FAQ про
качество/прозрачность/фон, related. Селектор формата (S1f) их **не заменяет** качество/прозрачность/фон, related. Селектор формата (S1f) их **не заменяет**
— он про «уже открыл инструмент и хочу другой формат на выходе». — он про «уже открыл инструмент и хочу другой формат на выходе».
- **«convert image» / «image converter»** — интент «швейцарский нож». Под него — - **«convert image» / «image converter»** — интент «швейцарский нож». Под него —
@@ -331,24 +365,25 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
## 6. Переименование бренда и домен (сделать до S1-domain, дёшево сейчас) ## 6. Переименование бренда и домен (сделать до S1-domain, дёшево сейчас)
Новое имя и конкретный домен **не записаны в этом репозитории** — детальный Выбранное имя и домен **в этом репозитории не записаны**: детальный чек-лист
чек-лист в `docs/plan-domain.local.md` (исключён из git через `.gitignore`), лежит в локальном файле `plan-domain.local.md` рядом с этим планом, он исключён
чтобы выбранное имя не попало в историю коммитов до покупки домена. из git через `.gitignore`, чтобы имя не попало в историю коммитов до покупки
домена. Агент, работающий из клона, этого файла не увидит — при необходимости
попросить чек-лист у пользователя.
- [ ] **[R1]** Механический переезд бренда на новое имя по чек-листу из - [ ] **[R1]** Механический переезд бренда на новое имя. Список файлов и строк с
локального плана: GitHub-репо + `git remote set-url`, README, хардкодом — в локальном плане (раздел «Где живёт бренд в коде»); здесь
`package.json` (корень + `web/`), `TopBar.svelte` / `Footer.svelte`, только SEO-ограничения. **НЕ трогать:** адреса страниц инструментов
`favicon.svg` (title в SVG), i18n-строки (`en.ts`, `ru.ts`: defaultTitle, (`/tools/crop-png`), ключи localStorage (`easy-png-tools:*` — или
pageTitle, metaDescription). НЕ трогать: слаги инструментов (`*-png`), мигрировать отдельной мелочью), историю `docs/archive`. Бренд должен стать
ключи localStorage (`easy-png-tools:*` — или мигрировать отдельной i18n-константой, а не тремя независимыми хардкодами.
мелочью), историю docs/archive.
- [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и - [ ] **[R2]** Купить выбранный домен (кандидаты и пробы — в локальном плане) и
перевести прод на него без `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), а
снятие `noindex`). После индексации переименование = переезд с 301, потеря S1-domain — блокер для S1h (снятие `noindex`). После индексации переименование
позиций и ссылок — не делать. = переезд с 301, потеря позиций и ссылок — не делать.
## 7. Что осознанно НЕ делаем ## 7. Что осознанно НЕ делаем
@@ -368,7 +403,8 @@ with PNG, JPG, WebP» в тексте + селектор формата в Downl
- `docs/tools-map.md` — карта «наш инструмент ↔ интент конкурента». - `docs/tools-map.md` — карта «наш инструмент ↔ интент конкурента».
- `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), фаза 11 (пайплайн);
фазы 9–10 — API и PWA.
- `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим). - `docs/plan-platform.md` — планы API и PWA (общее ядро, оффлайн-режим).
- `docs/plan-domain.local.md` — план переименования/домена; **в git не входит** - `plan-domain.local.md` — локальный чек-лист переименования и покупки домена;
(`.gitignore`), лежит только локально. **в git не входит** (`.gitignore`), в клоне отсутствует.
+6 -11
View File
@@ -19,7 +19,8 @@ pnpm verify
3. `pnpm --dir web test:rules` — отдельный быстрый прогон тестов ESLint-правил. 3. `pnpm --dir web test:rules` — отдельный быстрый прогон тестов ESLint-правил.
4. `pnpm --dir web lint:all` — ESLint, Stylelint и token audit. 4. `pnpm --dir web lint:all` — ESLint, Stylelint и token audit.
5. `pnpm --dir web format:check` — форматирование кода `web/`. 5. `pnpm --dir web format:check` — форматирование кода `web/`.
6. `pnpm check:docs` — форматирование Markdown. 6. `pnpm check:docs` — форматирование Markdown (`AGENTS.md`, `README.md`,
`docs/`).
7. `pnpm --dir web build` — production build. 7. `pnpm --dir web build` — production build.
Полный `test` уже включает тесты lint-правил; отдельный `test:rules` в `verify` Полный `test` уже включает тесты lint-правил; отдельный `test:rules` в `verify`
@@ -28,19 +29,13 @@ 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`
(`coverage.thresholds`, `coverage.exclude`); здесь они не дублируются, чтобы
| Метрика | Порог | числа не устарели молча (`AGENTS.md`).
| ---------- | ----- |
| statements | 90 |
| branches | 82 |
| functions | 72 |
| lines | 91 |
Порог — нижняя граница, а не оценка качества: он блокирует регрессию и не Порог — нижняя граница, а не оценка качества: он блокирует регрессию и не
требует сначала закрывать весь долг. Актуальные проценты даёт требует сначала закрывать весь долг. Актуальные проценты даёт
`pnpm --dir web test:coverage`; здесь они не дублируются, чтобы число не `pnpm --dir web test:coverage`. Локальный замер занимает единицы секунд, поэтому
устарело молча (`AGENTS.md`). Локальный замер занимает единицы секунд, поэтому
отдельного прогона в `verify` нет — `test:coverage` заменяет `test`. отдельного прогона в `verify` нет — `test:coverage` заменяет `test`.
Исключения из scope и их причины: Исключения из scope и их причины:
+59 -68
View File
@@ -1,20 +1,23 @@
# План развития: easy-png-tools # План развития: easy-png-tools
> **Статус (2026-09):** Фаза 1 (полноценный TS-сайт) в основном выполнена — сайт > **Статус:** фаза 1 выполнена — сайт живёт на типизированном реестре,
> живёт, каталог переведён в типизированный `registry`, production UI — корневые > production UI — корневые маршруты. Текущий фокус: SEO S1-free
> маршруты. Tech debt не закрыт полностью; состав quality-проверок — > (`docs/plan-seo.md`) + открытые задачи (`docs/backlog.md`). Домен нужен для
> `docs/quality-gates.md`, архитектура кода — `docs/architecture.md`, список > SEO (S1-domain). Фазы 2–8 — будущие; 9–10 спланированы в
> инструментов — `docs/tools-map.md`. Текущий фокус: SEO S1-free > `docs/plan-platform.md`; 11 — пайплайн.
> (`docs/plan-seo.md`) + продуктовые задачи из `docs/backlog.md`. Домен нужен
> для SEO (`plan-seo.md`, S1-domain). Фазы 2–8 — будущие; фазы 9–10 спланированы
> в `docs/plan-platform.md`.
> >
> **Нумерация фаз этого документа не совпадает с волнами разработки** (`Q*`/`C*` > **Идентификаторы.** Ссылаться на фазу этого документа. Пространства
> в других документах) и не используется как идентификатор задач: ссылаться на > идентификаторов задач: `FA-*` — правки полей и раскладки
> фазу, а не на номер волны. > (`docs/plan-fields-audit-fixes.md`); `S*` — SEO/GEO (`docs/plan-seo.md`); `A*`
> — API, `P*` — PWA, `F*` — шрифты (`docs/plan-platform.md`); `R*` — бренд и
> домен (`docs/plan-seo.md`, §6). Буквы `Q*`/`C*` в архивных документах
> относятся к волнам редизайна и как идентификаторы задач не используются.
## 0. Решения ## 0. Решения
Продуктовые и стратегические — здесь; устройство кода (реестр, исполнитель,
слои) — `docs/architecture.md`, разделы 2–3.
- **Набор утилит для изображений, всё в браузере** (SvelteKit + - **Набор утилит для изображений, всё в браузере** (SvelteKit +
`adapter-static`), данные не покидают машину. `adapter-static`), данные не покидают машину.
- **Первый и главный приоритет — полностью рабочий сайт**: максимум простых - **Первый и главный приоритет — полностью рабочий сайт**: максимум простых
@@ -23,24 +26,12 @@
- **Никакого 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; реализуется в фазе 11, не в
- **Реестр инструментов** (`web/src/lib/registry/`: id, title, description, фазе 1.
category, schema, `run`) — источник истины: из него генерируются страницы,
формы, пайплайны. Позже — общий формат для Rust-парсера.
- Валидация будущего Rust-порта — эталонные файлы, сгенерированные из - Валидация будущего Rust-порта — эталонные файлы, сгенерированные из
TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций, TS-реализации. Сравнение: **бит-в-бит** для чистых пиксельных операций,
**perceptual diff** для canvas-зависимых. **perceptual diff** для canvas-зависимых (правила сравнения — раздел 3).
- **Единая точка исполнения инструментов.** Страницы вызывают операции только
через `web/src/lib/executor/` — нигде напрямую `tool.run`. Контракт уже
асинхронный, поэтому:
- когда появятся тяжёлые MEDIUM-операции (свёртки, квантование), внутренности
исполнителя переезжают в Web Worker — воркер импортирует тот же чистый core
как TS-фолбэк, алгоритмы не дублируются, вызывающий код не меняется;
- в WASM-фазе тот же воркер принимает wasm-модуль и диспетчеризует по `toolId`
— свитч A/B из фазы 5 сводится к замене реализации внутри исполнителя;
- до появления MEDIUM-операций воркер не вводится: EASY-операции быстры и не
блокируют UI.
## 1. Где что лежит ## 1. Где что лежит
@@ -50,46 +41,22 @@
## 2. Фазы ## 2. Фазы
### Фаза 1 — Полноценный TS-сайт (единственный приоритет) ### Фаза 1 — Полноценный TS-сайт (выполнена)
**Цель: сайт, которым можно пользоваться.** Без wasm, без Rust, без Поставлено: каркас SvelteKit + static, дизайн-система и общий лейаут; ядро над
clamp-семантики (в TS это делает `Uint8ClampedArray` сам). `ImageData` (загрузка/декодирование, кодирование, операции по категориям) без
DOM-зависимостей; типизированный реестр с универсальным рендерером страницы и
формы; массовая реализация EASY-инструментов; i18n, темы, состояния
загрузки/ошибок. Checkpoint достигнут: загрузил PNG → применил инструмент →
скачал результат, без wasm.
1.1 **Каркас и UX/UI.** SvelteKit + static adapter, дизайн-система (цвета, **В фазе 1 пайплайна нет.** Workspace цепочек с сохранением в localStorage и
типографика, компоненты), общий лейаут, шапка с навигацией по категориям. экспортом в JSON — фаза 11, не поставленная часть фазы 1. Страница инструмента
работает ровно с одной записью реестра; что именно зарезервировано под пайплайн
(компонент `StepCard`, ключи `chain.*`), перечислено в `docs/architecture.md`,
раздел 1 «Чего в коде нет».
1.2 **Ядро.** Операции над `ImageData` (загрузка/декодирование файла, Незакрытый долг по фазе — в `docs/backlog.md`; он не входит в фазы 2–8 и 11.
кодирование и скачивание, библиотека операций
color/alpha/geometry/format/text/analyze). Всё без DOM-зависимостей внутри
`core`.
1.3 **Реестр инструментов.** Одна запись =
`{ id, title, description, category, schema, run }`. Универсальный рендерер:
страница инструмента и форма параметров строятся из записи реестра
автоматически; маршруты-утилиты генерируются из реестра на билде. Новый
инструмент = новая запись + функция `run` (рецепт — `docs/architecture.md` §8).
1.4 **Пайплайн-workspace**: загрузил изображение → список шагов (инструмент +
его параметры) → последовательное применение с превью каждого шага → скачивание
финального результата. Добавление/удаление шагов. Сохранение пайплайнов в
localStorage, экспорт/импорт JSON.
1.5 **Массовая реализация EASY-инструментов** по категориям:
- конвертация форматов (png/jpg/webp/bmp/base64/gif) и текстовые представления;
- прозрачность и альфа-канал;
- цвет: замена, тон, каналы, оттенки серого, инверсии;
- геометрия и холст: resize, crop, rotate, flip, border, padding, background;
- текст и простые эффекты: watermark, add-text, рамки, шум, pixelate;
- анализ: размеры, палитра, проверки, просмотр. Приоритет — ширина, не глубина:
как можно больше простых инструментов.
1.6 **Полировка.** Описания и подсказки для всех инструментов, состояния
загрузки/ошибок, доступность, пустые состояния.
**Checkpoint:** загрузил PNG → применил пайплайн из N шагов → сохранил и вернул
пайплайн → скачал результат. Все инструменты 1.5 работают на сайте. Ни одного
wasm.
### Фаза 2 — Тестовые PNG и генератор эталонов ### Фаза 2 — Тестовые PNG и генератор эталонов
@@ -159,6 +126,29 @@ Manifest + service worker, оффлайн-режим («работает без
устройство»), установка как приложение. Wasm-фаз не требует, идёт после S1 из устройство»), установка как приложение. Wasm-фаз не требует, идёт после S1 из
`plan-seo.md`. Чек-лист — `docs/plan-platform.md`, §2. `plan-seo.md`. Чек-лист — `docs/plan-platform.md`, §2.
### Фаза 11 — Пайплайн (цепочки шагов)
Продуктовая фича, **не зависит от wasm-фаз**: работает на существующем
исполнителе и реестре, поэтому может идти параллельно с SEO. Номер 11 выбран
чтобы не сдвигать уже используемые ссылки на фазы 2–10.
- **Формат шага** — `{tool, params}`, список шагов применяется последовательно,
результат предыдущего идёт в следующий. Формат зафиксирован в §0 как общий для
браузера, wasm и CLI, поэтому версионируется с первого дня.
- **Первый шаг фазы:** модуль состояния цепочки + сохранение в localStorage +
экспорт/импорт JSON (ошибки разбора уже описаны ключами
`errors.badPipelineShape` / `errors.pipelineVersion` / `errors.noSteps`).
- **Затем:** редактирование шагов (drag-n-drop, удаление, добавление), переход
«один инструмент → цепочка» без перезагрузки, сохранённые именованные цепочки,
отображение маски результата.
- **Checkpoint:** собрал цепочку из N шагов → сохранил → вернулся позже → скачал
результат последнего шага, всё в браузере.
Ограничения и открытые вопросы (1 → many не терминален, many → 1, частичные
ошибки, имена частей) — `docs/backlog.md`, раздел «Идеи». Пайплайн из архивного
UI-v1 удалён; что осталось в коде под эту фазу — `docs/architecture.md`,
раздел 1.
## 3. Правила сравнения (для фаз 4–7) ## 3. Правила сравнения (для фаз 4–7)
| Класс операции | Критерий | | Класс операции | Критерий |
@@ -187,7 +177,8 @@ Manifest + service worker, оффлайн-режим («работает без
## 5. Команды верификации ## 5. Команды верификации
Текущие команды и матрица «изменение → проверки» — `docs/quality-gates.md` Команды текущего этапа и матрица «изменение → проверки» —
(`pnpm verify` — быстрый gate текущего этапа). Для будущих фаз добавляются `docs/quality-gates.md` (`pnpm verify` — быстрый gate). Команды фаз 2–8
`cargo test` (Rust-ядро против эталонных файлов, фаза 4+) и (`cargo test`, `harness compare`) **в проекте не существуют** и появятся вместе
`harness compare tests/reference` (сквозная сверка native + wasm, фаза 7). с соответствующими фазами: до этапа 2 проверять нечем, кроме `pnpm verify` и
e2e.
+20 -5
View File
@@ -1,10 +1,21 @@
# Аудит использования - UX\UI # Аудит использования - UX\UI
> Статус: **active**. Сырые наблюдения по инструментам и полям — вход для > Статус: **active**. **Дата прохода: 2026-09-13** (инструменты и поля). Здесь
> `docs/plan-fields-audit-fixes.md`, который разбивает их на волны правок, и для > только «что плохо» — наблюдения, без порядка работ и без критериев приёмки.
> `docs/backlog.md`. Здесь фиксируется, **что замечено**, а не порядок работы. > Новый проход — новая дата сверху; закрытая находка не удаляется, а помечается
> Ids инструментов сверять с `web/src/lib/registry/` — исторические названия > ссылкой на задачу в `docs/backlog.md`.
> (`webp-to-png`, `color-wheel-generator`, `png-info`) в реестре отсутствуют. >
> Как исправлять — `docs/backlog.md` (задачи) и
> `docs/plan-fields-audit-fixes.md` (порядок правок полей). Находка в аудите и
> задача в бэклоге — не дубль: пока задачи нет, находка просто висит.
>
> Ids ниже **не сверены с реестром** и часть из них в коде отсутствует
> (`webp-to-png`, `color-wheel-generator`, `png-info`, `png-to-hsl`). Перед
> работой по любому пункту — найти соответствующий инструмент в
> `web/src/lib/registry/` или `docs/tools-map.md`; несуществующий id не
> resurrect'ить, а переименовать пункт. Чтобы не сверять вручную каждый раз, ищи
> по смыслу: категория в заголовке раздела + ключевое слово в пункте →
> `docs/tools-map.md`, где id уже фактические.
## везде ## везде
@@ -154,6 +165,7 @@
## аудит полей ## аудит полей
- у полей plate, position9 и font-style вторая подпись дублирует первую
- position offset: должны быть отдельные range slider на x и y - position offset: должны быть отдельные range slider на x и y
- range slider: поле ввода, кнопки + и -, кнопка сброса на умолчание - range slider: поле ввода, кнопки + и -, кнопка сброса на умолчание
- range slider with presets: слайдер, как выше, добавочные кнопки с пресетами - range slider with presets: слайдер, как выше, добавочные кнопки с пресетами
@@ -161,3 +173,6 @@
- ввод высоты и ширины с keep aspect ratio должен быть единым компонентом - ввод высоты и ширины с keep aspect ratio должен быть единым компонентом
- поле seed (random) - range slider не нужен, поле для ввода и "randomize" - поле seed (random) - range slider не нужен, поле для ввода и "randomize"
кнопка кнопка
- поле текста: нормальная высота по умолчанию (не одна строка), для svg —
форматирование или подсветка синтаксиса, для bin\hex — колонки, для
hex\bytes\rgba — пример в placeholder
+12 -9
View File
@@ -2,11 +2,15 @@
> Статус: **active**. Живой документ для сверки с каталогом. > Статус: **active**. Живой документ для сверки с каталогом.
> >
> **Источник правды по id — реестр** `web/src/lib/registry/`. Этот список — > **Карта перечисляет `id` записи реестра (`web/src/lib/registry/`), которые
> навигационный: сверять перед использованием. Формат записи: > одновременно являются адресами страниц:** `/tools/crop-png` — это
> `id — параметры через запятую`. Сравнение с каталогом источника (объём, > `id: "crop-png"`. Отдельного поля `slug` в `ToolEntry` нет, поэтому id и адрес
> нишевые клоны серий Logo*/Icon*/Stamp*/Signature*) — замер в > сейчас совпадают; их разделение — решённая, но не реализованная задача
> `docs/plan-seo.md`, §2; здесь он не дублируется. > (`docs/backlog.md`, раздел «Мелочи всякие», и `docs/architecture.md`, раздел
> 3). Источник правды — реестр. Этот список навигационный: сверять перед
> использованием. Формат записи: `id — параметры через запятую`. Сравнение с
> каталогом источника (объём, нишевые клоны серий Logo*/Icon*/Stamp*Signature*)
> — замер в `docs/plan-seo.md`, §2; здесь он не дублируется.
> >
> Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из > Количество инструментов здесь не фиксируется — оно выводится из раздела 1 и из
> `TOOLS` в `registry/index.ts`, и дублировать его цифрой значит заставлять себя > `TOOLS` в `registry/index.ts`, и дублировать его цифрой значит заставлять себя
@@ -237,7 +241,6 @@ Ids приведены к фактическим в `web/src/lib/registry/`.
- **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) — - **Нишевые серии** (logo/icon/stamp/signature — замер в шапке документа) —
сознательно не копируем: это обычные операции над конкретным контентом, у нас сознательно не копируем: это обычные операции над конкретным контентом, у нас
они доступны через базовые инструменты + цепочки. они доступны через базовые инструменты + цепочки.
- **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) сознательно не - **Входные «из формата в PNG»** (jpg-to-png, webp-to-png) — решение и его SEO-
заведены: загрузка файла уже принимает любой поддержанный формат, а последствия описаны в разделе 1 выше; отдельные страницы под эти интенты —
инструмент-конвертер добавил бы лишнее звено цепочки. Отдельные страницы под предмет решения в `docs/plan-seo.md` §5.
эти интенты — предмет решения в `docs/plan-seo.md` §5.
+2 -2
View File
@@ -9,8 +9,8 @@
"dev": "pnpm --dir web dev", "dev": "pnpm --dir web dev",
"build": "pnpm --dir web build", "build": "pnpm --dir web build",
"format": "pnpm format:docs && pnpm --dir web format", "format": "pnpm format:docs && pnpm --dir web format",
"format:docs": "prettier --write docs --log-level warn --cache", "format:docs": "prettier --write AGENTS.md README.md docs --log-level warn --cache",
"check:docs": "prettier --check docs --cache", "check:docs": "prettier --check AGENTS.md README.md docs --cache",
"generate:fixtures": "node tests/fixtures/manual-edge-cases/generate.mjs" "generate:fixtures": "node tests/fixtures/manual-edge-cases/generate.mjs"
}, },
"keywords": [], "keywords": [],
+4 -3
View File
@@ -1,8 +1,9 @@
# ESLint-плагины и дизайн-токены: детали # ESLint-плагины и дизайн-токены: детали
Эта часть репозитория подробно описана здесь, а не в корневом `AGENTS.md`: > Статус: **active**. Область применения — `web/eslint-plugins/**`,
рутина-разработка её не требует, детали нужны только при правке самих > `web/scripts/**`, конфиги линтинга. Остальная рутина разработки её не требует;
линт-правил или конфигов. > детали нужны только при правке самих линт-правил или конфигов. Команды и
> пороги — `docs/quality-gates.md`.
## Структура ## Структура