# Архитектура > Статус: **active**. Описание текущего кода: границы модулей, поток данных от > реестра до скачивания, точки расширения. Канонический источник истины — сам > код; этот документ объясняет, **куда смотреть и что с чем связано**, и не > дублирует сигнатуры. ## 1. Слои ```txt web/src/lib/ registry/ # источник истины: инструменты и страницы registry/tools/ # Tool: реализации (id), без адресов и текстов registry/pages/ # Page: адрес, тексты, категория, список шагов registry-schema.ts # декларативная схема параметров + санитизация дефолтов catalog.ts # группировка страниц и счётчики для /list-tools categories.ts # id/названия категорий tool-icons.ts # slug страницы → иконка core/ # операции над ImageData; без DOM, кроме io.ts/domText.ts executor/ # единственная точка исполнения инструментов i18n/ # словари ru/en, t(), состояние локали components/ # UI по слоям: ui/ примитивы, layout/ каркас, # catalog/ каталог, schema/ view инструмента, # fields/ поля (+ fields/schema/ контролы схемы), # display/ типографика и карточки, feedback/ состояния zip.ts # сборка мультифайлового результата (1 → many) theme.svelte.ts # light/dark + localStorage web/src/routes/ +page.svelte # главная list-tools/ # каталог tools/[slug]/ # страница инструмента (адрес = slug страницы) kit/ # витрина компонентов (production, но исключена из i18n-гейта) ``` Правило зависимостей: `components/**` и `routes/**` знают про `registry/**`, `executor/**` и `core/**`; `core/**` не знает ни о ком; `registry/**` описывает данные и знает только `core/**` и `registry-schema.ts`. Нарушение этого порядка — повод не заводить импорт, а инвертировать зависимость. Исключение из «core без DOM» — `core/io.ts` (декодирование файла, кодирование, скачивание) и `core/domText.ts` (canvas-рендер текста и эмодзи). Они трогают `document`, `createImageBitmap` и `HTMLCanvasElement`, поэтому не работают в node-Vitest и исключены из coverage-гейта (`docs/quality-gates.md`, раздел Coverage). Всё остальное в `core/**` — чистые функции над `ImageData`. ### Чего в коде нет Страница исполняет ровно один шаг: загрузил файл → применила инструмент → скачала результат. Отсутствуют: - **Пайплайн / цепочки шагов** — реализованы: модуль шагов `web/src/lib/pipeline.svelte.ts` (createChain/parseChain/serializeSteps), именованные цепочки — `web/src/lib/chains.svelte.ts` (localStorage `chains:named`). Страница инструмента всегда открывается чистой; при первом структурном изменении (добавлен шаг, заменён первый инструмент) цепочка автоматически становится именованной (имя — дата/время) и маршрут бесшовно меняется на `/pipeline?id=` (передача исходника через handoff-стор, replaceState). `/chains` — список цепочек (переименовать/удалить/открыть/ создать). Легаси-автосейв `pipeline:` снят без миграции (ключи просто перестают читаться). Экспортный JSON-формат не устоялся и может меняться до фазы 11 `docs/roadmap.md`. - **Разделения страниц, которые делят один инструмент** — сводит все convert-страницы на один инструмент; задача в `docs/backlog.md`. Пока `registry.test.ts` не требует, чтобы на инструмент ссылались две страницы, а на практике не ссылается ни одна. Задачи по обоим пунктам — в `docs/backlog.md`; архитектурные решения по пайплайну — `docs/roadmap.md`. ## 2. Поток данных ```txt registry/{tools,pages}/*.ts │ ├─► catalog.ts ──► routes/list-tools (каталог страниц, поиск, фильтры) │ └─► routes/tools/[slug]/+page.svelte │ getPageBySlug(slug) → Page, getTool(steps[0].id) → Tool ▼ SchemaToolView.svelte оркестратор страницы │ schema = tool.schema, тексты и имя файла — от page ├─► SchemaFields.svelte поля из schema.fields (FIELDS: kind → контрол) ├─► SchemaSourceTile/SchemaTextSource.svelte вход └─► SchemaPreview + schema-preview-model.ts вид превью │ buildSchemaPreviewModel(input) → { sourceValue, resultValue, formatValue, hasResult } ▼ executor.execute(ctx) единственная точка исполнения │ сериализация параметров → worker → worker-handler → tool.run(ctx) ▼ core/* пиксели │ ▼ SchemaResultTile / SchemaTextResult + SchemaActions (download/copy) │ кодирование по Tool.output (mime/ext/quality) └─► zip.ts, если result === "files" ``` Ключевые решения: - **Реестр — единственный источник истины.** Страница и форма строятся из записей; новый инструмент = новая запись `Tool` + функция `run` и новая страница `Page` на него. - **Исполнение идёт только через `executor.execute`.** Компоненты не зовут `tool.run` напрямую — иначе нельзя перенести тяжёлое в worker/wasm без правки вызывающего кода. - **Форма параметров не знает про инструмент.** Она рендерится из `registry-schema.ts`, поэтому добавление вида поля не трогает страницы. - **Модель превью отделена от вёрстки.** `schema-preview-model.ts` — чистая функция, покрыта unit-тестами; `SchemaPreview.svelte` только рисует её вывод. - **Цепочка работает над набором файлов (fan-out).** `run-chain.ts`: «ток» между шагами — массив именованных изображений (одиночная загрузка = набор из одного); image-шаг применяется к каждому файлу, files-шаг (split) склеивает имена частей (`<вход>-<часть>`), analyze-шаг (text/verdict) при batch-входе видит первый файл. Шаг роняет цепочку, только если упали все файлы (`ChainStepError`), частичные сбои — warnings. Мультивыбор файлов в drop-зоне; набор на выходе скачивается zip через `zip.ts`. ## 3. Инструмент и страница Реестр двухслойный: реализация и то, что видит пользователь, лежат в разных файлах. `Tool` (`web/src/lib/registry/types.ts`, записи в `registry/tools/.ts`): | Поле | Смысл | | ---------- | ------------------------------------------------------------------------------ | | `id` | внутреннее имя инструмента, уникально; ключ `tools..*` и `toolId` в worker | | `schema` | `toolSchema({ fields, layout?, meta? })` | | `input` | `"image" \| "text" \| "none"` — что ждёт инструмент на входе | | `result`? | `"image" \| "text" \| "verdict" \| "files"` — что отдаёт | | `domOnly`? | инструмент требует DOM (canvas); превью-executor запускает его напрямую | | `output`? | mime/ext/quality для скачивания (не у всех) | | `runMask`? | ч/б превью «что изменится»; тогл на тайле результата, в цепочку не идёт | | `run(ctx)` | единственный метод: `ctx` → `ToolResult` | `Page` (записи в `registry/pages/.ts`): | Поле | Смысл | | ------------- | ---------------------------------------------------------------------- | | `slug` | уникальный адрес `/tools/`, имя скачиваемого файла и ключ иконки | | `title` | EN-строка страницы, переводится через `pages..title` | | `description` | EN-строка страницы, переводится через `pages..description` | | `category` | id из `categories.ts`; по нему каталог группирует страницы | | `steps` | упорядоченный список шагов `{ id, params? }` — ссылки на инструменты | Кто по чему адресуется: `slug` — маршрут, имя файла, `TOOL_ICONS`, поиск по каталогу; `id` — ключ `tools..*` (подписи полей, `options`, `results`), `toolId` worker-протокола и запись пайплайна. Модель допускает, что один инструмент обслуживает несколько страниц, а страница состоит из нескольких инструментов; на практике пока каждая страница ссылается на свой инструмент, а исполняется первый (и единственный) шаг. Инструмент работает с любым поддержанным форматом, поэтому png-интент живёт только в `slug`: `id: "crop"` → страница `slug: "crop-png"` → `crop-png.png`. Правило имени `id`: снять суффикс `-png` и в середине имени (`png-to-base64` → `to-base64`, `X-to-png` → `from-X`, `verify-is-png` → `verify-png`). Правило относится только к `id`: у `slug` своя история и SEO-значение, поэтому переименование `id` не трогает адреса, а переименование `slug` меняет адрес, имя файла, ключ `pages.*`, иконку и запись в `docs/tools-map.md`. Слаги, сложившиеся до разделения реестра, правилу не соответствуют (`convert-png-to-jpg`, `change-png-opacity`, `png-is-grayscale`, `verify-is-png` и другие) — это не ошибка, менять их без SEO-решения нельзя. Форму slug проверяет `registry.test.ts` (только форма, не смысл). Хелперы `genTool` / `imgTool` / `textGen` из `types.ts` задают `run` для типовых случаев (приведение типа результата), чтобы запись не повторяла обвязку. `TOOLS` и `PAGES` собираются в `registry/index.ts` из восьми групп; `getTool(id)` и `getPageBySlug(slug)` — способы достать запись, `registry.test.ts` проверяет уникальность обоих имён, существование инструмента под каждым шагом и то, что шаг ровно один. ## 4. Схема параметров `registry-schema.ts` — единственное место, где описаны виды полей. - **Виды поля** (`fieldSpecs`): `number`, `slider`, `color`, `select`, `text`, `checkbox`, `dimension`, `color-pair`, `colors`, `offset`, `position9`, `font-style`, `plate`, `gradient`. - **Фабрика `field.*`** создаёт `Field`; 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({ workerFactory? })` плюс готовый экспорт `execute`. Контракт асинхронный (`Promise`), параметры сериализуются по `executor/protocol.ts`, исполнение уходит в `executor.worker.ts`; `worker-handler.ts` разбирает сообщение и зовёт `run`. Если worker недоступен — прямой вызов. UI импортирует `execute` из `executor/index.ts`. Отсюда следует: подключение wasm позже означает замену реализации **внутри** исполнителя, а не правку компонентов. ## 6. i18n Словари — `web/src/lib/i18n/` (`en.ts`, `ru.ts`, общий `dict.ts` с `LOCALES`/`BASE_LOCALE`, `t.ts` с `t()`, `locale.svelte.ts` с состоянием локали). Пользовательский текст живёт только в словарях и читается через `t()`. Списки ключей и допустимых атрибутов для i18n-правил линтера лежат не здесь, а в `web/eslint-plugins/i18n/lists.js`. Секции разделены по слоям: `pages..title` и `pages..description` — тексты страницы (в базовой локали секция пустая, EN берётся из реестра), `tools..options/params/results` — подписи полей и тексты вердиктов инструмента, `fields.*`/`groups.*` — общие подписи схемы. Инварианты паритета ключей, плейсхолдеров и запрет хардкода проверяются линтером, детали — `web/eslint-plugins/README.md`. ## 7. Точки расширения | Задача | Где править | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Новый инструмент | `registry/tools/.ts` + страница в `registry/pages/.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/tools/.ts` и по всему `web/src/` — не заводить дублирующую константу, тип или утилиту. 2. **Выбрать группу** (`geometry`/`alpha`/`color`/`convert`/`filters`/ `generate`/`text`/`analyze`) и положить инструмент в её экспорт; при новой категории — сначала `categories.ts`. 3. **Задать `id`** по правилу раздела 3: без png-интента (`crop`, `to-jpg`, `from-base64`, `is-grayscale`). 4. **Описать схему** через `field.*`, задать `layout`, если поля нужно сгруппировать. Качество кодирования (`quality`) — не поле страницы, а настройка скачивания (`docs/plan-seo.md` §4, S1f). 5. **Написать `run`** поверх `core/*`; пиксельные операции — чистые функции в `core/`, без DOM. 6. **Добавить страницу** в `registry/pages/.ts`: `slug` с адресным png-интентом (`crop-png`), тексты EN, `category`, `steps: [{ id }]`. 7. **Проверить обязательные контракты**: инструмент попадает в `TOOLS`, страница в `PAGES`, `/tools/` prerender-ится, `input`/`result` согласованы с тем, что делает `run`. 8. **Добавить i18n-ключи во все локали** сразу: `pages..title` и `pages..description`, при наличии опций и вердиктов — `tools.`. 9. **Тесты:** unit — в `registry/registry.test.ts` или рядом с `core`-функцией; e2e-smoke — существующий spec по типу `input`/`result` (правила — `docs/testing-strategy.md`). 10. **Обновить `docs/tools-map.md`:** новая страница обязательна в разделе «Реализовано», формат строки — в шапке раздела. Карта перечисляет **`slug` страниц** из `registry/pages/`, то есть адреса. Проверяет `web/src/lib/registry/tools-map-doc.test.ts`: slug без страницы и страница без записи в карте роняют тест; разделы «Можно добавить» и «Идеи» проверяются в обратную сторону — реализованный slug в плане тоже роняет тест, поэтому при переезде инструмента из плана в реестр его вынимают оттуда. Обоснование и границы автоматизации — `docs/decisions.md`, раздел 4. Ожидаемый минимум проверок: `pnpm --dir web check` и `pnpm --dir web test`; для инструмента, меняющего пользовательский поток, — e2e (см. раздел 3 `docs/quality-gates.md`).