Files
easy-png-tools2/docs/architecture.md
T

207 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура
> Статус: **active**. Описание текущего кода: границы модулей, поток данных от
> реестра до скачивания, точки расширения. Канонический источник истины — сам
> код; этот документ объясняет, **куда смотреть и что с чем связано**, и не
> дублирует сигнатуры.
## 1. Слои
```txt
web/src/lib/
registry/ # источник истины: записи ToolEntry
registry-schema.ts # декларативная схема параметров + санитизация дефолтов
catalog.ts # группировка и счётчики для /list-tools
categories.ts # id/названия категорий
tool-icons.ts # id → иконка
core/ # чистый TS: операции над ImageData, без DOM
executor/ # единственная точка исполнения инструментов
i18n/ # словари ru/en, t(), списки для i18n-правил линтера
components/ # UI (fields/, layout/, ui/, schema-компоненты)
zip.ts # сборка мультифайлового результата (1 → many)
theme.svelte.ts # light/dark + localStorage
web/src/routes/
+page.svelte # главная
list-tools/ # каталог
tools/[id]/ # страница инструмента (адрес = id записи)
kit/ # витрина компонентов (production, но исключена из i18n-гейта)
```
Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`,
`executor/**` и `core/**`; `core/**` не знает ни о ком и не зависит от DOM;
`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. Поток данных
```txt
registry/*.ts ──(TOOLS: ToolEntry[])
│
├─► catalog.ts ──► routes/list-tools (каталог, поиск, фильтры)
│
└─► routes/tools/[id]/+page.svelte
│ getTool(id) → запись реестра
▼
SchemaToolView.svelte оркестратор страницы
│ schema = tool.schema
├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол)
├─► SchemaSourceTile/SchemaTextSource.svelte вход
└─► SchemaPreview + schema-preview-model.ts вид превью
│ buildSchemaPreviewModel(inputMode, resultKind, …) → source/result/hasResult
▼
executor.execute(ctx) единственная точка исполнения
│ сериализация параметров → worker → worker-handler → tool.run(ctx)
▼
core/* пиксели
│
▼
SchemaResultTile / SchemaTextResult + SchemaActions (download/copy)
│ кодирование по ToolEntry.output (mime/ext/quality)
└─► zip.ts, если result === "files"
```
Ключевые решения:
- **Реестр — единственный источник истины.** Страница и форма строятся из
записи; новый инструмент = новая запись + функция `run`.
- **Исполнение идёт только через `executor.execute`.** Компоненты не зовут
`tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки
вызывающего кода.
- **Форма параметров не знает про инструмент.** Она рендерится из
`registry-schema.ts`, поэтому добавление вида поля не трогает страницы.
- **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая
функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод.
## 3. Запись реестра
`ToolEntry` (`web/src/lib/registry/types.ts`):
| Поле | Смысл |
| ------------- | ------------------------------------------------------------------------------------------------ |
| `id` | идентичность инструмента **и** публичный адрес `/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` |
Отдельного поля `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` для типовых
случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS`
собирается в `registry/index.ts` из восьми групп; `getTool(id)` — единственный
способ достать запись, `registry.test.ts` проверяет уникальность `id`.
Разделение `id` (внутреннее имя) и `slug` (адрес) — решённое, но **не
реализованное** решение: задача и порядок волн в `docs/backlog.md`, раздел
«Мелочи всякие». До её выполнения этот документ описывает текущее состояние, а
id и slug совпадают.
## 4. Схема параметров
`registry-schema.ts` — единственное место, где описаны виды полей.
- **Виды поля** (`fieldSpecs`): `number`, `slider`, `color`, `select`, `text`,
`checkbox`, `dimension`, `color-pair`, `colors`, `offset`, `position9`,
`font-style`, `plate`, `gradient`.
- **Фабрика `field.*`** создаёт `Field<T>`; UI-контрол подбирается в
`SchemaFields.svelte` по `spec.kind` через карту `FIELDS`.
- **`schema.layout`** группирует поля (`resolveLayoutGroups`); раскладка
разделов — единственное место, где живёт порядок и группировка.
- **Значения по умолчанию** даёт `defaultSchemaParams`; `applySourceDefaults`
подставляет размеры текущего source; `sanitizeSchemaParams` приводит мусор к
допустимому виду. Правка поведения полей идёт здесь, а не в компоненте.
Новый вид поля требует: спека + фабрика `field.*` → запись в `fieldSpecs` →
контрол в `components/fields/schema/` → ключ в `FIELDS` → дефолт и санитизация →
i18n-подписи → тест в `registry-schema.test.ts`.
## 5. Исполнение
`executor/executor.ts` — фабрика `createExecutor(factories?)` плюс готовый
экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются,
исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает
сообщение и зовёт `run`. Если worker недоступен — прямой вызов.
Отсюда следует: подключение wasm позже означает замену реализации **внутри**
исполнителя, а не правку компонентов.
## 6. i18n
Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с
`LOCALES`/`BASE_LOCALE`). Пользовательский текст живёт только в словарях и
читается через `t()`; строки реестра (`title`/`description`) — источник для
EN-ключей `tools.*` и словарь-переводчик. Инварианты паритета ключей,
плейсхолдеров и запрет хардкода проверяются линтером, детали —
`web/eslint-plugins/README.md`.
## 7. Точки расширения
| Задача | Где править |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Новый инструмент | запись в `registry/<group>.ts` + i18n-ключи + тесты (рецепт ниже) |
| Смена `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` |
## 8. Рецепт: добавить инструмент
1. **Найти соседей.** Поиск по ключу в `registry/<group>.ts` и по всему
`web/src/` — не заводить дублирующую константу, тип или утилиту.
2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/
`generate`/`text`/`analyze`) и положить запись в её экспорт; при новой
категории — сначала `categories.ts`.
3. **Задать `id`.** Он же адрес страницы, он же ключ i18n, он же база имени
скачиваемого файла. Страница держит png-интент, поэтому суффикс `-png` входит
в `id` (`crop-png`, `resize-png`) — это осознанно, отдельного поля `slug` в
реестре нет (раздел 3).
4. **Описать схему** через `field.*`, задать `layout`, если поля нужно
сгруппировать.
5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в
`core/`, без DOM.
6. **Проверить обязательные контракты** для новой записи: она попадает в
`TOOLS`, страница `/tools/<id>` prerender-ится, `input`/`result` согласованы
с тем, что делает `run`.
7. **Добавить i18n-ключи во все локали** сразу (ключ — по `id`).
8. **Тесты:** unit — в `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.
Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для
инструмента, меняющего пользовательский поток, — e2e (см. раздел 3
`docs/quality-gates.md`).