docs: update to slug - id changes

This commit is contained in:
2026-09-27 17:04:41 +05:00
parent 7b7e12fafc
commit 8cb3f1a8f0
9 changed files with 204 additions and 164 deletions
+98 -75
View File
@@ -9,11 +9,13 @@
```txt
web/src/lib/
registry/ # источник истины: записи ToolEntry
registry/ # источник истины: инструменты и страницы
registry/tools/ # Tool: реализации (id), без адресов и текстов
registry/pages/ # Page: адрес, тексты, категория, список шагов
registry-schema.ts # декларативная схема параметров + санитизация дефолтов
catalog.ts # группировка и счётчики для /list-tools
catalog.ts # группировка страниц и счётчики для /list-tools
categories.ts # id/названия категорий
tool-icons.ts # id → иконка
tool-icons.ts # slug страницы → иконка
core/ # чистый TS: операции над ImageData, без DOM
executor/ # единственная точка исполнения инструментов
i18n/ # словари ru/en, t(), списки для i18n-правил линтера
@@ -23,7 +25,7 @@ web/src/lib/
web/src/routes/
+page.svelte # главная
list-tools/ # каталог
tools/[id]/ # страница инструмента (адрес = id записи)
tools/[slug]/ # страница инструмента (адрес = slug страницы)
kit/ # витрина компонентов (production, но исключена из i18n-гейта)
```
@@ -34,15 +36,18 @@ web/src/routes/
### Чего в коде нет
Страница инструмента работает ровно с одним инструментом: загрузил файл →
применил одну запись реестра → скачал результат. Отсутствуют:
Страница исполняет ровно один шаг: загрузил файл → применила инструмент →
скачала результат. Отсутствуют:
- **Пайплайн / цепочки шагов** — модуля нет, состояния нет, localStorage для
него нет. `docs/roadmap.md` описывает его как отдельную фазу, а не как
поставленное. Под будущую фазу зарезервированы `components/StepCard.svelte`
него нет. Модель страницы уже хранит список `steps`, но исполняет первый шаг,
а `registry.test.ts` требует, чтобы шаг был ровно один: композиция формы из
полей двух инструментов — отдельная работа, описанная как фаза 11
`docs/roadmap.md`. Под неё зарезервированы `components/StepCard.svelte`
(сейчас импортируется только витриной `/kit`) и ключи `chain.*`,
`errors.badPipelineShape`, `errors.noSteps`.
- **Разделения `id` и `slug`** — раздел 3.
- **Разделения страниц, которые делят один инструмент** — сводит 14
convert-страниц на один инструмент; задача в `docs/backlog.md`.
Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по
пайплайну — `docs/roadmap.md`.
@@ -50,15 +55,15 @@ web/src/routes/
## 2. Поток данных
```txt
registry/*.ts ──(TOOLS: ToolEntry[])
registry/{tools,pages}/*.ts
│
├─► catalog.ts ──► routes/list-tools (каталог, поиск, фильтры)
├─► catalog.ts ──► routes/list-tools (каталог страниц, поиск, фильтры)
│
└─► routes/tools/[id]/+page.svelte
│ getTool(id) → запись реестра
└─► routes/tools/[slug]/+page.svelte
│ getPageBySlug(slug) → Page, getTool(steps[0].id) → Tool
▼
SchemaToolView.svelte оркестратор страницы
│ schema = tool.schema
│ schema = tool.schema, тексты и имя файла — от page
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
├─► SchemaSourceTile/SchemaTextSource.svelte вход
└─► SchemaPreview + schema-preview-model.ts вид превью
@@ -71,14 +76,15 @@ registry/*.ts ──(TOOLS: ToolEntry[])
│
▼
SchemaResultTile / SchemaTextResult + SchemaActions (download/copy)
│ кодирование по ToolEntry.output (mime/ext/quality)
│ кодирование по Tool.output (mime/ext/quality)
└─► zip.ts, если result === "files"
```
Ключевые решения:
- **Реестр — единственный источник истины.** Страница и форма строятся из
записи; новый инструмент = новая запись + функция `run`.
записей; новый инструмент = новая запись `Tool` + функция `run` и новая
страница `Page` на него.
- **Исполнение идёт только через `executor.execute`.** Компоненты не зовут
`tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки
вызывающего кода.
@@ -87,39 +93,50 @@ registry/*.ts ──(TOOLS: ToolEntry[])
- **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая
функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод.
## 3. Запись реестра
## 3. Инструмент и страница
`ToolEntry` (`web/src/lib/registry/types.ts`):
Реестр двухслойный: реализация и то, что видит пользователь, лежат в разных
файлах. `Tool` (`web/src/lib/registry/types.ts`, записи в
`registry/tools/<group>.ts`):
| Поле | Смысл |
| ------------- | ------------------------------------------------------------------------------------------------ |
| `id` | идентичность инструмента **и** публичный адрес `/tools/<id>` **и** база имени скачиваемого файла |
| `title` | EN-строка реестра, переводится через `tools.*.title` |
| `description` | EN-строка реестра, переводится через `tools.*.description` |
| `category` | id из `categories.ts` |
| `schema` | `toolSchema({ fields, layout?, meta? })` |
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую, минуя worker |
| `output` | mime/ext/quality для download (не у всех инструментов) |
| `icon` | id иконки из `tool-icons.ts` |
| `run(ctx)` | единственный метод: `ctx` → `ToolResult` |
| Поле | Смысл |
| ---------- | ------------------------------------------------------------------------------ |
| `id` | внутреннее имя инструмента, уникально; ключ `tools.<id>.*` и `toolId` в worker |
| `schema` | `toolSchema({ fields, layout?, meta? })` |
| `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе |
| `result` | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт |
| `domOnly` | инструмент требует DOM (canvas); превью-executor запускает его напрямую |
| `output` | mime/ext/quality для скачивания (не у всех) |
| `run(ctx)` | единственный метод: `ctx` → `ToolResult` |
Отдельного поля `slug` **нет**: адрес страницы, ключ i18n (`tools.<id>.*`) и имя
файла скачивания — это один и тот же `id`. Инструмент работает с любым
поддержанным форматом, но страница держит png-интент, поэтому `-png` живёт
внутри `id` (`id: "crop-png"` → `/tools/crop-png` → `crop-png.png`). Следствие:
переименование `id` меняет адрес страницы, ключи `tools.*` и имя файла.
`Page` (записи в `registry/pages/<group>.ts`):
| Поле | Смысл |
| ------------- | ---------------------------------------------------------------------- |
| `slug` | уникальный адрес `/tools/<slug>`, имя скачиваемого файла и ключ иконки |
| `title` | EN-строка страницы, переводится через `pages.<slug>.title` |
| `description` | EN-строка страницы, переводится через `pages.<slug>.description` |
| `category` | id из `categories.ts`; по нему каталог группирует страницы |
| `steps` | упорядоченный список шагов `{ id, params? }` — ссылки на инструменты |
Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по
каталогу; `id` — ключ `tools.<id>.*` (подписи полей, `options`, `results`),
`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` не трогает адреса, а переименование `slug` меняет адрес, имя
файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`.
Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный
способ достать запись, `registry.test.ts` проверяет уникальность `id`.
Разделение `id` (внутреннее имя) и `slug` (адрес) — решённое, но **не
реализованное** решение: задача и порядок волн в `docs/backlog.md`, раздел
«Мелочи всякие». До её выполнения этот документ описывает текущее состояние, а
id и slug совпадают.
и `PAGES` собираются в `registry/index.ts` из восьми групп; `getTool(id)` и
`getPageBySlug(slug)` — способы достать запись, `registry.test.ts` проверяет
уникальность обоих имён, существование инструмента под каждым шагом и то, что
шаг ровно один.
## 4. Схема параметров
@@ -154,52 +171,58 @@ i18n-подписи → тест в `registry-schema.test.ts`.
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и
читается через `t()`; строки реестра (`title`/`description`) — источник для
EN-ключей `tools.*` и словарь-переводчик. Инварианты паритета ключей,
плейсхолдеров и запрет хардкода проверяются линтером, детали —
`web/eslint-plugins/README.md`.
читается через `t()`. Секции разделены по слоям: `pages.<slug>.title` и
`pages.<slug>.description` — тексты страницы (в базовой локали секция пустая, EN
берётся из реестра), `tools.<id>.options/params/results` — подписи полей и
тексты вердиктов инструмента, `fields.*`/`groups.*` — общие подписи схемы.
Инварианты паритета ключей, плейсхолдеров и запрет хардкода проверяются
линтером, детали — `web/eslint-plugins/README.md`.
## 7. Точки расширения
| Задача | Где править |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Новый инструмент | запись в `registry/<group>.ts` + i18n-ключи + тесты (рецепт ниже) |
| Смена `id` | запись реестра + i18n-ключи `tools.*` + `tool-icons.ts` + хардкод id в тестах, `/kit` и e2e; меняется адрес страницы — операция с SEO-последствиями, не часть обычной задачи |
| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) |
| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге |
| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` |
| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу |
| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов |
| Мультифайловый результат | `result: "files"` + `zip.ts` |
| Задача | Где править |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Новый инструмент | `registry/tools/<group>.ts` + страница в `registry/pages/<group>.ts` + i18n + тесты (рецепт ниже) |
| Новая страница на готовом инструменте | запись `Page` со своим `slug` и текстами; новый код не нужен |
| Смена `id` | `registry/tools/*` + ключи `tools.*` в словарях + хардкод id в тестах; адреса, имена файлов, иконки и карта не меняются |
| Смена `slug` | `registry/pages/*` + `pages.*` в словарях + `tool-icons.ts` + `docs/tools-map.md` + хардкод slug в тестах и e2e; меняется адрес страницы — операция с SEO-последствиями, не часть обычной задачи |
| Новый вид поля | `registry-schema.ts` + контрол + `FIELDS` (раздел 4) |
| Новый маршрут/страница | `web/src/routes/**` + ссылка в каталоге |
| Новый визуальный элемент | компонент в `components/**` по образцу `ui/` или `layout/` |
| Новый дизайн-токен | `web/src/app.css` (только там) + потребитель сразу |
| Тяжёлая операция → worker/wasm | `executor/**`, без правок компонентов |
| Мультифайловый результат | `result: "files"` + `zip.ts` |
## 8. Рецепт: добавить инструмент
1. **Найти соседей.** Поиск по ключу в `registry/<group>.ts` и по всему
1. **Найти соседей.** Поиск по ключу в `registry/tools/<group>.ts` и по всему
`web/src/` — не заводить дублирующую константу, тип или утилиту.
2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/
`generate`/`text`/`analyze`) и положить запись в её экспорт; при новой
`generate`/`text`/`analyze`) и положить инструмент в её экспорт; при новой
категории — сначала `categories.ts`.
3. **Задать `id`.** Он же адрес страницы, он же ключ i18n, он же база имени
скачиваемого файла. Страница держит png-интент, поэтому суффикс `-png` входит
в `id` (`crop-png`, `resize-png`) — это осознанно, отдельного поля `slug` в
реестре нет (раздел 3).
3. **Задать `id`** по правилу раздела 3: без png-интента (`crop`, `to-jpg`,
`from-base64`, `is-grayscale`).
4. **Описать схему** через `field.*`, задать `layout`, если поля нужно
сгруппировать.
сгруппировать. Качество кодирования (`quality`) — не поле страницы, а
настройка скачивания (`docs/plan-seo.md` §1, S1f).
5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
`core/`, без DOM.
6. **Проверить обязательные контракты** для новой записи: она попадает в
`TOOLS`, страница `/tools/<id>` prerender-ится, `input`/`result` согласованы
с тем, что делает `run`.
7. **Добавить i18n-ключи во все локали** сразу (ключ — по `id`).
8. **Тесты:** unit — в `registry/*.test.ts` или рядом с `core`-функцией;
6. **Добавить страницу** в `registry/pages/<group>.ts`: `slug` с адресным
png-интентом (`crop-png`), тексты EN, `category`, `steps: [{ id }]`.
7. **Проверить обязательные контракты**: инструмент попадает в `TOOLS`, страница
в `PAGES`, `/tools/<slug>` prerender-ится, `input`/`result` согласованы с
тем, что делает `run`.
8. **Добавить i18n-ключи во все локали** сразу: `pages.<slug>.title` и
`pages.<slug>.description`, при наличии опций и вердиктов — `tools.<id>`.
9. **Тесты:** unit — в `registry/registry.test.ts` или рядом с `core`-функцией;
e2e-smoke — существующий spec по типу `input`/`result` (правила —
`docs/testing-strategy.md`).
9. **Обновить `docs/tools-map.md`:** новая страница обязательна в разделе
«Реализовано», формат строки — в шапке раздела. Карта перечисляет **`id`**
реестра, которые одновременно являются адресами страниц. Проверяет
`web/src/lib/registry/tools-map-doc.test.ts`: id без инструмента и инструмент
без записи в карте роняют тест. Обоснование и границы автоматизации —
`docs/decisions.md`, раздел 4.
10. **Обновить `docs/tools-map.md`:** новая страница обязательна в разделе
«Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug`
страниц** из `registry/pages/`, то есть адреса. Проверяет
`web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница
без записи в карте роняют тест. Обоснование и границы автоматизации —
`docs/decisions.md`, раздел 4.
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3