# Code Tidy

> Tidy up a TypeScript/Bun codebase without changing behaviour — sequential awaits and N+1 queries, duplication and near-identical enums, hardcoded values, types and constants mixed into implementation files, oversized or multi-component files, logic living inside components, components worth moving into a shared package, custom CSS and hardcoded style values in a Tailwind project, inconsistent idioms, import cycles, leftover debug code. Use when asked to clean up, tidy, declutter, deduplicate, split a large file or component, extract shared components, make an area consistent, or prepare it before building on it.

- Skill: `h2oarctic/code-tidy` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add h2oarctic/code-tidy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/h2oarctic/code-tidy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: H2OArctic (https://skillmd.com/u/h2oarctic)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/h2oarctic/code-tidy

---


# Code tidying (TypeScript / Bun)

Runtime and package manager: **Bun only**. Never `npm`, `npx`, `yarn`, `pnpm`, or `node`.

This skill removes what is not code any more and evens out what is written five different ways. It does
not improve behaviour: bug fixes, typing, performance work and API design are different tasks and leave
this one with a line in the report.

Talk to the user in the language they use.

## Invariants

**1. The diff does not change behaviour.** The proof is not "the tests passed" — it is a baseline recorded
*before* the first edit and compared *after*. Without that baseline a green run proves nothing, because
nobody knows what was green to begin with.

**2. The project's own rules outrank this skill.** `CLAUDE.md`, `AGENTS.md`, `.agents/rules/`,
`docs/conventions.md`, `CONTRIBUTING.md` — read them in phase 0 and follow them. Where they say something
different from the defaults below, they win, and the report says so. Where they are silent, the code's own
dominant pattern wins. This skill's opinions come last.

**3. Nothing is written until the plan is approved (phases 0–2 are read-only).** Linters run in check mode
only; `--fix`, `--write` and `--unsafe` wait for phase 4.

**4. Formatting and meaning never share a commit.** A reformatted file hides its one real change from review.

**5. The scope is what the user asked for.** "Tidy this module" is not permission to tidy the repository.

**6. Никаких выводов по обрезанному списку.** Текстовый отчёт печатает верхушку каждого раздела и внизу
перечисляет, что обрезано. Разобрать восемь случаев из тридцати четырёх и сказать «в основном это одно и
то же» — не разбор, а угадывание: остальные двадцать шесть никто не смотрел. Поэтому план строится по
`--json`, где списки полные, а в отчёте у каждого класса стоит **разобрано N из M**. Пока M больше нуля
и не разобрано — работа не закончена, и слово «готово» не произносится.

## Phase 0. Scope, rules, preflight

```bash
git status --porcelain          # рабочее дерево должно быть чистым
git branch --show-current
bun --version
```

- **Read the project's rules first.** The scan lists the rule files it found, with line counts. Read them
  before proposing anything: a "violation" that the project deliberately allows is not a finding, and a
  rule the project states explicitly ("SQL only in repositories", "no string literals", "env only through
  the config module") turns half the report from opinion into fact.
- **Decide the scope, and say it.** Preference order: the current diff → a named directory → the whole
  repository, and the last only when asked for exactly that. `--scope diff|branch|<путь>`.
- Dirty tree → **read the diff before asking anything** (`git diff --stat`). Someone's work in progress
  inside the target area moves those files to group C.
- Read the root `package.json` and use the project's own script names. Note that a project can have more
  than one test command (unit and integration are often split, and the second one is not run by the first).
- **Do not introduce tooling.** No new linter, no new config, no new rule in an existing config, no
  formatter run over files the task never mentioned.

## Phase 1. Baseline (read-only)

```bash
bun run typecheck        # или bunx tsc --noEmit -p tsconfig.json
bun run build            # если есть
bun test                 # и второй тестовый скрипт, если он есть
```

- **A command that exits 0 without doing anything is not a passing check.** `0 tests found`,
  `no tasks were executed`, a task runner that skipped every workspace — that is a missing check.
- **A red baseline is not a blocker, it is a record.** List what already fails, so that afterwards nobody
  blames the cleanup for it.
- **No tests at all** → say so plainly and narrow the plan to group A.

## Phase 2. Inventory (read-only)

```bash
bun ~/.claude/skills/code-tidy/scripts/tidy-scan.ts                 # обзор: верхушка каждого раздела
bun ~/.claude/skills/code-tidy/scripts/tidy-scan.ts --json          # полные списки — план строится по ним
bun ~/.claude/skills/code-tidy/scripts/decisions.ts list            # что уже решили не трогать
```

Текстовый прогон — для человека и для первого взгляда. **Разбирать находки по нему нельзя**: он показывает
6–10 строк на список, а внизу честно перечисляет, чего не показал. Как только раздел попал в этот перечень,
дальше работать только с `--json`.

Then whatever the project already has, **in check mode only**: `bunx tsc --noEmit`, `bunx biome check .`,
`bunx eslint .`, `bunx knip`.

| Раздел отчёта | Что с этим делать |
| --- | --- |
| 1. Ломает прямо сейчас | `it.only`, `debugger`, `.forEach(async)`, пустой `catch`, `sql.unsafe`, секрет в логе — первым делом и отдельно |
| 2. Мусор | удаляется без обсуждения (группа A) |
| 3. Асинхронность | `await` в цикле и цепочки `await` — группа B: правка меняет семантику |
| 4. Повторы | блоки, похожие перечисления, зашитые значения — разговор, а не команда «вынести» |
| 5. Крупные единицы | сортировка по вниманию; размер сам по себе не дефект |
| 6. Раскладка и именование | сверено с правилами **этого** проекта; «против правила проекта» — сильный аргумент |
| 7. Компоненты и стили | один файл — один компонент, логика наружу, общее в пакет, только Tailwind |
| 8. Единообразие | показан раскол и меньшинство; решение — привести к большинству или узаконить оба |
| 9. Связность | циклы и barrel-файлы — структурная правка |
| 10. Зона риска | что не трогать: чужие правки, горячие файлы, генерация |
| 11. Журнал | отложенное; вернувшиеся вопросы — с объяснением, что изменилось |

**Раздел 1 — это не уборка, а поломки.** Часть из них снимается удалением (`.only`, `debugger`), но
компонент внутри компонента, хук под условием, список без `key`, пустой `catch`, `sql.unsafe` и секрет
в логе чинятся настоящей правкой кода. Показать их первыми и спросить, чинить сейчас или отдельной
задачей. Молча чинить нельзя: это меняет поведение — пусть и в лучшую сторону.

Заголовок отчёта отдельно показывает **принятое в проекте**: ролевые суффиксы (`*.const.ts`, `*.port.ts`)
и каталоги (`types/`, `consts/`). Это не рекомендация скилла — это то, что уже господствует в коде.

### Мёртвый код — отдельным заходом

Его нет в обычном прогоне: `--dead`. Вердикт по мёртвому коду живёт недолго и требует ручной проверки
чаще, чем всё остальное, — поэтому он не мешается в отчёте, пока его не попросили.

Когда всё-таки просят: скилл считает **достижимость** от точек входа, а не количество ссылок, поэтому
barrel-файл не оживляет мёртвое имя и не хоронит живое. Но статический анализ не видит:

- **потребителей вне репозитория** — опубликованный пакет, соседний репозиторий, `exports` в `package.json`;
- **связывание по строке** — DI-контейнеры, таблицы маршрутов, метаданные декораторов, реестры моделей;
- **ссылки вне кода** — имя в JSON/YAML, шаблоне, миграции, CI-конфиге, `Dockerfile`, preload;
- **использование только в тестах** — это не «мёртв», это вопрос: у кода нет пользователя или тест проверяет
  деталь реализации.

Зоны, где до кода дотягиваются мимо графа (`import()` с переменной, `import.meta.glob`, `ns[key]`), скилл
показывает отдельным списком «вердикта нет». Это кандидаты на чтение, а не на удаление.

## Phase 3. The plan, by what may be done to it

### Считать, а не оценивать

У каждого класса находок в плане стоит число из `--json`, а не впечатление: «34 цикла с `await`», а не
«циклы с запросами». Дальше каждый пункт получает решение — правим, откладываем в журнал или оставляем
с причиной. Класс закрыт, когда сумма решений равна M. Если на весь класс времени нет, так и написать:
«разобрано 8 из 34, остальные 26 — отдельной задачей», и не выдавать восьмёрку за тридцать четыре.

Одинаковые с виду находки одного класса **не считаются разобранными по образцу**: тридцать четыре цикла —
это тридцать четыре разных запроса, из которых часть схлопывается в один `IN (…)`, часть параллелится,
а часть обязана остаться последовательной. Общий вывод по четырём из них неверен по построению.

### A. Механика — без обсуждения

Behaviour-neutral и откатывается одной командой: мусорные файлы (`*.orig`, `*.rej`, `foo copy 2.ts`),
пустые файлы, `debugger`, отладочный `console.log`, закомментированный код блоками, неиспользуемые
импорты, форматирование **по конфигу, который уже лежит в проекте**, отдельным шагом.

Исключение внутри группы: **`it.only` меняет объём прогона.** Убрать правильно, но это включит тесты,
которые никто не гонял. Убрать → прогнать → отдельно разобрать вскрывшееся, и в отчёте сказать, что
упавшее не сломано уборкой, а перестало быть выключенным.

### B. Требует решения пользователя

**Асинхронность.** `Promise.all` — это не оптимизация, а смена семантики, и решает её человек:

- падает на первой ошибке, остальные промисы продолжают выполняться и их отказы уходят в никуда
  (`allSettled` — другая семантика, не замена);
- порядок побочных эффектов исчезает: если вторая операция рассчитывала на строку, созданную первой,
  параллельный запуск ломает её молча и не всегда воспроизводимо;
- нагрузка идёт разом: пул соединений, лимиты внешнего API, блокировки в БД;
- внутри транзакции параллельные запросы по одному соединению — это гонка, а не ускорение.

`await` внутри цикла — тот же разговор плюс вопрос о запросе: N однотипных обращений почти всегда
складываются в одно (`IN (...)`, батч, join). Ставить вместо цикла неограниченный `Promise.all` по
коллекции неизвестного размера — это замена медленного кода на падающий под нагрузкой.

**Повторы.** Три копии — повод спросить, а не вынести (см. фазу 4). Похожие перечисления — то же самое:
одинаковые наборы вариантов часто оказываются осознанным зеркалом по разные стороны границы, через которую
код не переносится (сервер и браузерная сборка, генерируемые типы и ручные словари).

**Зашитые значения.** Литерал, повторяющийся в нескольких местах, — это незаписанная константа. Куда её
класть, определяет проект, а не скилл: смотри раздел «принято в проекте» и правила.

**Раскладка и именование.** Перенос объявлений в `types/`/`consts/`, разделение файла, переименование под
стиль каталога. Переименование ломает `git blame` и чужие открытые ветки — оправдано, когда имя врёт.

**Единообразие.** Меньшинство приводится к большинству — но по умолчанию только в тех файлах, которые и так
правятся по задаче. Сплошной проход по репозиторию — отдельная просьба.

**Компоненты.**

- **Один файл — один компонент.** Второй компонент в файле переезжает в свой файл: иначе он не
  переиспользуется, не тестируется отдельно и тянет за собой соседа при каждом импорте.
- **Логика уезжает из файла представления.** Функция, которая не рисует, а считает, форматирует или
  ходит в сеть, живёт в отдельном файле — там её видно другим экранам и там её можно проверить тестом.
  Экспортированная логика в файле компонента — тем более: её уже кто-то тянет через компонент.
  Исключение — хуки: они часть представления и остаются рядом.
- **Общее — в пакет, а не копией.** Одноимённые компоненты в разных приложениях и одинаковые блоки,
  разложенные по разным воркспейсам, переносятся в общий пакет. Копия в каждом приложении расходится
  молча: правку вносят в одну, а вторая живёт своей жизнью.
- **Только TSX.** `.jsx` и `.js` в TypeScript-проекте — недоехавшая миграция.
- Тяжёлый компонент (много пропсов, много `useState`, глубокий JSX) — это разговор о разделении,
  а не приговор длине.

**Стили.**

- **Только Tailwind, своего CSS нет.** Единственный оправданный файл стилей — конфигурация темы.
  CSS-модули, `styled`-обёртки и инлайновый `style={{}}` — это второй способ делать то же самое, и
  расходится он с темой молча.
- **Цвет и размер — токенами, а не значениями.** `#ff0000` и `w-[13px]` живут мимо шкалы: тема меняется,
  а они остаются. Произвольное значение оправдано ровно там, где в шкале нужного шага нет, — и тогда
  правильнее добавить шаг в тему.
- **Повторяющийся набор классов — это ненаписанный компонент.** Одна и та же длинная строка в трёх
  местах означает, что общий вид уже существует, просто у него нет имени.

**Мёртвые экспорты и файлы, barrel-файлы, циклы импортов, публичный API** — как раньше, только с флагом
`--dead` для первых.

### C. Не трогать

- сгенерированное и вендоренное (шапка `@generated`, `dist/`, `__generated__/`, `*.gen.ts`, снапшоты);
- миграции — их прошлое неизменяемо;
- файлы с чужими незакоммиченными правками и файлы из открытых веток;
- горячие файлы (раздел 9), если правка не входила в задачу;
- намеренные зеркала словарей через границу, которую код не пересекает;
- всё, что журнал держит в отложенных, пока условие отказа в силе.

### D. Это не уборка — только в отчёт

Настоящие баги, `any` вместо типов, гонки, N+1 как архитектурная проблема, слабый тест. Записать одной
строкой с местом и симптомом — и не чинить: багфикс внутри уборочного диффа не пройдёт ревью, и не должен.

### Уже отвеченные вопросы остаются отвеченными

```bash
bun ~/.claude/skills/code-tidy/scripts/decisions.ts keep dupe:1a2b3c \
  --reason "зеркало словаря для браузерной сборки, объединять нечем" --fingerprint <из --json>
```

Ключи и отпечатки лежат в каждой находке `--json`. Отпечаток обязателен везде, где он есть: запись
действует, пока код тот же, а переписанный блок возвращает вопрос — прежнее оправдание относилось не к
нему. Отложенное не переспрашивается: одна строка «отложено: N» и дальше.

## Phase 4. Правки — по одному классу за раз

Порядок: мусор → асинхронность → повторы → раскладка → декомпозиция → единообразие. Каждый класс —
отдельный шаг с отдельной проверкой.

### Асинхронность

```ts
// было: два независимых запроса стоят в очереди друг за другом
const user = await users.byId(id)
const limits = await limits.forPlan(plan)

// стало: одновременно — но только если порядок и семантика ошибки это допускают
const [user, limits] = await Promise.all([users.byId(id), limits.forPlan(plan)])
```

- Проверить независимость по коду, а не по виду: второй вызов не должен читать результат первого ни
  напрямую, ни через общий кэш, ни через строку в БД, созданную первым.
- Цикл с запросом внутри: сначала попытаться сделать **один** запрос вместо N. `Promise.all` по циклу —
  второй выбор, и тогда с ограничением параллельности.
- Внутри транзакции ничего не распараллеливать.
- `.forEach(async …)` — не оптимизация, а потерянные ошибки: переписывается на `for…of` с `await` или на
  `Promise.all(map(...))`, и это осознанный выбор между последовательностью и параллельностью.

### Повторы

- **Правило трёх**: две копии — ждём, три — разговариваем. Объединять стоит, только если копии меняются
  вместе и означают одно и то же.
- **Случайное сходство**: два куска выглядят одинаково, но живут по разным причинам. Объединение свяжет их
  навсегда, и следующая правка пойдёт через флаг.
- **Признак плохой абстракции**: чтобы покрыть различия, в общую функцию добавляется параметр,
  переключающий поведение. Появился `options.mode` — остановиться, дубль дешевле.
- **Перечисления**: победитель один, остальные становятся импортом. Если словари стоят по разные стороны
  границы, через которую код не переносится, — оставить оба и записать причину в журнал.
- **Зашитые значения**: выносить туда, где проект держит константы, именовать по смыслу домена, а не по
  значению. Если проект связывает константный объект с одноимённым типом — повторить этот приём. Литерал,
  встречающийся один раз и читаемый на месте, не трогать.

### Компоненты и стили

- Разделение файла на компоненты — механический перенос: содержимое не меняется, меняются импорты.
  Переносить по одному и проверять сборку после каждого, а не всё сразу.
- Вынос логики из компонента: сначала перенести функцию как есть, потом (отдельным шагом) убрать из неё
  то, что было завязано на замыкание компонента. Смешать это в один шаг — потерять контроль над диффом.
- Перенос компонента в общий пакет — правка публичного API пакета: сначала согласовать, потом двигать,
  и одним движением обновить обе стороны.
- Свой CSS не переписывать «в классы» пачкой: файл за файлом, со сверкой глазами. Замена CSS на утилиты
  без визуальной проверки — это не уборка, а редизайн вслепую.
- Произвольное значение (`w-[13px]`) не заменять ближайшим шагом шкалы молча: сдвиг на пиксель заметен,
  и решение о нём принимает человек.

### Раскладка

- Переносить объявления по ролям, которые уже приняты в проекте, — и не изобретать новые.
- Перенос и переименование — разные шаги: вместе они превращают дифф в нечитаемый.
- После переноса проверить, что импорты обновлены везде, включая тесты и конфиги.

### Декомпозиция

- Резать по швам, которые уже есть: тело цикла, ветка условия, шаг с собственным именем. Куску, которому
  нельзя дать честное имя, шов не там.
- `handleRequestPart2` — не декомпозиция, а перенос строк.
- Сигнатура и поведение внешней функции не меняются.
- Длина сама по себе не дефект: плоский `switch` на 200 строк читается лучше пяти функций с общим состоянием.

### Комментарии

**Не трогать комментарий, если код рядом не менялся.** Правка комментария даёт дифф с нулевым изменением
логики, а CI/CD видит изменённый файл и запускает пересборку и редеплой зависимых приложений впустую.
Не переписывать, не переводить, не переформатировать заодно; удалять — только вместе с кодом.

### Форматирование

Только конфигом проекта и только отдельным шагом. Крупное переформатирование сносит `git blame`; если оно
нужно, предложить `.git-blame-ignore-revs`, но записывает туда человек вместе с коммитом.

## Phase 5. Проверка

```bash
bun run typecheck
bun run build
bun test                      # полный прогон; --changed годится только пока чинишь
git diff --stat               # уборка — это в основном удаления
git diff -w                   # если дифф исчезает, там были только пробелы
git diff                      # прочитать каждый ханк, который не является чистым удалением
```

- Диф, который **растёт**, — уже не уборка: появившиеся строки должны объясняться согласованным пунктом
  группы B.
- Правки в асинхронности проверяются не только тестами: перечитать порядок побочных эффектов глазами,
  потому что тест на порядок обычно никто не писал.
- Упало после удаления «мёртвого» кода → символ был живым. Вернуть, записать в журнал, идти дальше.
- Упало то, что было красным в фазе 1 → так и сказать, со ссылкой на запись из фазы 1.
- Тесты не запускаются (нет БД, нет сервисов) → прямо сказать, что проверки не было.

## Phase 6. Отчёт

```
## Охват
<что смотрели; какие правила проекта прочитаны>

## Удалено
<мусор, отладка, закомментированный код; мёртвое — только если был --dead, с доказательством>

## Асинхронность
<что собрано в Promise.all и почему это безопасно; какие N+1 схлопнуты в один запрос>
<что оставлено последовательным — с причиной>

## Повторы
<объединённое; оставленные дубли и перечисления — с причиной; вынесенные значения и куда именно>

## Раскладка и единообразие
<перенесённое по ролям проекта; к какому большинству приведено меньшинство>

## Компоненты и стили
<что разделено «один файл — один компонент»; какая логика уехала и куда>
<что перенесено в общий пакет; что оставлено копией и почему>
<убранный свой CSS; значения, заменённые токенами — и что оставлено с обоснованием>

## Не тронуто
<группа C: генерация, миграции, чужие правки, горячие файлы, зеркала словарей>

## Отложено
<записано в журнал: ключ, причина, что вернёт вопрос; не переспрашивалось: N>

## Найдено, но не чинилось (группа D)
<баги, типизация, производительность — место и симптом, одной строкой>

## Разобрано
<по каждому классу: N из M; что осталось и почему>

## Проверки
<typecheck / build / тесты: было в фазе 1 → стало сейчас>

## Дифф
<+X / −Y строк; форматирование отдельным шагом: да/нет>
```

## Never

- Менять поведение под видом уборки — даже «очевидно правильно» исправляя баг по дороге.
- Собирать `await` в `Promise.all` без разбора порядка побочных эффектов и семантики ошибки; ставить
  неограниченный `Promise.all` по коллекции неизвестного размера; распараллеливать внутри транзакции.
- Заменять цикл с запросом на параллельный цикл, не проверив, можно ли сделать один запрос.
- Сливать два перечисления, стоящие по разные стороны границы, через которую код не переносится.
- Выносить в константу литерал, который встречается один раз и на месте читается лучше.
- Навязывать раскладку и именование, которых в проекте нет: сначала правила проекта, потом господствующий
  в коде приём, и только потом мнение скилла.
- Переименовывать файлы пачкой «под стиль» и совмещать перенос с переименованием.
- Расширять охват за пределы просьбы: соседний файл, «заодно весь каталог», весь репозиторий.
- Смешивать форматирование со смысловой правкой в одном диффе.
- Запускать `--fix`/`--write`/`--unsafe` до утверждения плана.
- Добавлять инструмент, конфиг или правило как побочный эффект уборки.
- Удалять экспорт, потому что так сказал сканер: сначала снять `export` и спросить компилятор, а для
  ненадёжных зон — `grep` по строке и `git log -S`.
- Удалять файл, помеченный как «вердикта нет» (динамика, глоб, реестр по строке).
- Трогать сгенерированное, вендоренное, миграции и снапшоты.
- Править файл с чужими незакоммиченными изменениями, не спросив.
- Комментировать код вместо удаления и оставлять `.bak`-копии «на всякий случай».
- Править комментарий там, где код не менялся.
- Молча чинить поломки из раздела 1: компонент внутри компонента, хук под условием, список без `key` —
  это правка поведения, её сначала показывают.
- Переписывать CSS в утилитарные классы без визуальной проверки и менять произвольные значения на
  ближайший шаг шкалы по своему усмотрению.
- Переносить компонент в общий пакет, не согласовав: это правка публичного API пакета.
- Делать вывод о классе находок по верхушке списка и по разбору нескольких случаев: «посмотрел четыре,
  остальные такие же» — это не проверка, а предположение.
- Говорить «готово», пока в отчёте сканера стоит «ПОКАЗАНО НЕ ВСЁ» и раздел не разобран по `--json`.
- Писать в отчёте «нет таких находок», когда на самом деле их не смотрели: «не найдено» и «не разбирали» —
  разные строки.
- Ослаблять или удалять тест, чтобы дифф стал зелёным. Пропущенный тест — находка, а не мусор.
- Переспрашивать то, что уже лежит в журнале с действующей причиной; и наоборот — оставлять отказ
  незаписанным.
- Коммитить и пушить: коммит — отдельная явная просьба.
- Докладывать об успехе, когда тесты не запускались или упали.

