# План: единый `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"` — семантика «нужно именно
изображение», а не «файл» (между инструментами может передаваться не файлом).