# План: единый `run(ctx)` в registry-new (унификация 5 режимов исполнения) > Статус: выполняется пошагово (ремонтограф: 11 шагов, ревью после каждого). > Ветка `feat/redesign`. Убивает пару багов-классов «инструмент с generate > показывает загрузку и не выдаёт результат» (linear-gradient-png и все > генераторы): причина — `input` не задан, диспетч по факту наличия методов. > Целевое состояние по завершении: `svelte-check` → 0 errors, `pnpm test` → > зелёный, `lint` → без ошибок. E2E не делаем. ## Исходная проблема Два бага у `linear-gradient-png` (и у всех генераторов): 1. Показывается загрузка изображения — `SchemaToolView.svelte` считает `inputMode = tool.input ?? "file"`, а `tool.input` у генераторов не задан. 2. Результата нет — `SchemaActions` рендерит кнопку «Generate» только при `inputMode === "none"`, а он всегда `"file"`. Ui-контур «догадывается» о генераторе по эвристике `Boolean(tool.generate && !tool.run)`. Корневая причина архитектурная: у инструмента 5 режимов исполнения (`run` 79 шт., `generate` 21, `runFromText` 6, `toText` 9, `textToText` 1), и диспетч «какой метод вызывать» размазан между UI и executor. Способ исполнения (worker/поток, было `domOnly`) просочился в метод-диспетч. Договорились: **свести всё к одному методу `run(ctx)`**, а требование входа (`input`) — к обязательному полю. `domOnly` остаётся только executor-хинтом (уйдёт при переезде на WASM-ядро). ## Целевой контракт ```ts export type InputMode = "image" | "text" | "none"; // было "file" export type ResultKind = "image" | "text" | "verdict"; export interface ToolContext

{ params: P; source?: PixelImage; // присутствует ⇔ input === "image" text?: string; // присутствует ⇔ input === "text" } export type ToolResult = PixelImage | string; export type ToolEntry

> = { id: string; title: string; description: string; category: CategoryId; schema: ToolSchema

; input: InputMode; // ОБЯЗАТЕЛЬНОЕ, дефолта нет result?: ResultKind; // default "image" run(ctx: ToolContext

): Promise | ToolResult; output?: OutputFormat; icon?: string; domOnly?: boolean; // executor-хинт, уйдёт с WASM }; ``` Удаляем: `generate`, `runFromText`, `toText`, `textToText`. Хелперы контракта (бросают `ToolError`): `requireSource(ctx)`, `requireText(ctx)`. Обёртки для миграции тел 1:1: `imgTool((img, p) => …)`, `genTool((p) => …)`, `textGen((t, p) => …)`. ```ts const imgTool =

(fn: (img: PixelImage, p: P) => ToolResult) => (ctx: ToolContext

) => fn(requireSource(ctx), ctx.params); const genTool =

(fn: (p: P) => ToolResult) => (ctx: ToolContext

) => fn(ctx.params); const textGen =

(fn: (t: string, p: P) => ToolResult) => (ctx: ToolContext

) => fn(requireText(ctx), ctx.params); ``` ## Executor (единый) ```ts export async function execute( tool: ToolEntry, ctx: { params: Record; source?: PixelImage; text?: string }, ): Promise { const params = sanitizeSchemaParams(tool.schema, ctx.params); if (tool.input === "image" && !ctx.source) throw new ToolError("errors.sourceRequired"); if (tool.input === "text" && !ctx.text?.trim()) throw new ToolError("errors.textRequired"); return route(tool, { params, source: ctx.source, text: ctx.text }); } ``` `route()` — единственное место выбора места исполнения: `domOnly` или нет Worker → прямой вызов `tool.run(ctx)`, иначе worker с fallback (логика `ensureWorker/runInWorker/disableWorker` остаётся). Worker-протокол — `{ id, toolId, params, source?, text? }`. ## Шаги (ревью после каждого) | # | Шаг | Файлы | Что | | --- | --------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | 1 | Типы | `registry-new/types.ts` | новый `ToolEntry`/`ToolContext`/`ToolResult`/`InputMode "image"`, хелперы `requireSource`/`requireText` + `imgTool`/`genTool`/`textGen` | | 2 | Executor+Worker | `preview/executor/executor.ts`, `executor.worker.ts`, `index.ts` | единый `execute`, `route` по `domOnly`, протокол `source?`/`text?` | | 3 | UI kit | `kit/SchemaToolView.svelte`, `SchemaPreview.svelte`, `SchemaActions.svelte`, `SchemaSourceTile.svelte` | одна `run()`, без `isGenerator`-эвристики, `inputMode` → `"image"` | | 4 | Генераторы | `registry-new/generate.ts` | 21 `generate(p)` → `run(ctx)` + `input: "none"` (чинит градиент и др.) | | 5 | Текст | `registry-new/convert.ts`, `analyze.ts` | `runFromText`/`toText`/`textToText` → `run`; `input: "image"` на run-инструментах | | 6 | Текст | `registry-new/text.ts` | domOnly run-инструменты → `run(ctx)` + `input: "image"` | | 7 | Геометрия | `registry-new/geometry.ts` | run → `run(ctx)` + `input: "image"` | | 8 | Альфа/цвет | `registry-new/alpha.ts`, `color.ts` | run → `run(ctx)` + `input: "image"` | | 9 | Фильтры | `registry-new/filters.ts` | run → `run(ctx)` + `input: "image"` | | 10 | Тесты | `registry-new/registry-new.test.ts` | `generate!`/`run!` → `run(ctx)`; guard-тест: `input` всегда задан, image-инструменты работают с источником, `none` — без | | 11 | Гейт | — | `pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json`, `pnpm --dir web lint`, `pnpm --dir web test` | Примечание по миграции: `input: "image"` добавляется всем run-инструментам (~80 шт.), `input: "text"` уже стоит у 7 text-инструментов, `input: "none"` — у 21 генератора. ## Известные последствия - **Красная сборка с шага 1 до конца шага 10** — осознанное решение, ревью идёт по diff'у шагов. - Старый контур (`registry/**` + `lib/tools/executor.ts` + `ToolPage.svelte`, `sourceMode`) НЕ трогаем — он на удаление (C19). - `input: "file"` переименован в `input: "image"` — семантика «нужно именно изображение», а не «файл» (между инструментами может передаваться не файлом).