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
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)
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)
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 как архитектурная проблема, слабый тест. Записать одной
строкой с местом и симптомом — и не чинить: багфикс внутри уборочного диффа не пройдёт ревью, и не должен.
Уже отвеченные вопросы остаются отвеченными
bun ~/.claude/skills/code-tidy/scripts/decisions.ts keep dupe:1a2b3c \
--reason "зеркало словаря для браузерной сборки, объединять нечем" --fingerprint <из --json>
Ключи и отпечатки лежат в каждой находке --json. Отпечаток обязателен везде, где он есть: запись
действует, пока код тот же, а переписанный блок возвращает вопрос — прежнее оправдание относилось не к
нему. Отложенное не переспрашивается: одна строка «отложено: N» и дальше.
Phase 4. Правки — по одному классу за раз
Порядок: мусор → асинхронность → повторы → раскладка → декомпозиция → единообразие. Каждый класс —
отдельный шаг с отдельной проверкой.
Асинхронность
// было: два независимых запроса стоят в очереди друг за другом
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. Проверка
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.
- Писать в отчёте «нет таких находок», когда на самом деле их не смотрели: «не найдено» и «не разбирали» —
разные строки.
- Ослаблять или удалять тест, чтобы дифф стал зелёным. Пропущенный тест — находка, а не мусор.
- Переспрашивать то, что уже лежит в журнале с действующей причиной; и наоборот — оставлять отказ
незаписанным.
- Коммитить и пушить: коммит — отдельная явная просьба.
- Докладывать об успехе, когда тесты не запускались или упали.
1---2name: code-tidy3description: 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.4---56# Code tidying (TypeScript / Bun)78Runtime and package manager: **Bun only**. Never `npm`, `npx`, `yarn`, `pnpm`, or `node`.910This skill removes what is not code any more and evens out what is written five different ways. It does11not improve behaviour: bug fixes, typing, performance work and API design are different tasks and leave12this one with a line in the report.1314Talk to the user in the language they use.1516## Invariants1718**1. The diff does not change behaviour.** The proof is not "the tests passed" — it is a baseline recorded19*before* the first edit and compared *after*. Without that baseline a green run proves nothing, because20nobody knows what was green to begin with.2122**2. The project's own rules outrank this skill.** `CLAUDE.md`, `AGENTS.md`, `.agents/rules/`,23`docs/conventions.md`, `CONTRIBUTING.md` — read them in phase 0 and follow them. Where they say something24different from the defaults below, they win, and the report says so. Where they are silent, the code's own25dominant pattern wins. This skill's opinions come last.2627**3. Nothing is written until the plan is approved (phases 0–2 are read-only).** Linters run in check mode28only; `--fix`, `--write` and `--unsafe` wait for phase 4.2930**4. Formatting and meaning never share a commit.** A reformatted file hides its one real change from review.3132**5. The scope is what the user asked for.** "Tidy this module" is not permission to tidy the repository.3334**6. Никаких выводов по обрезанному списку.** Текстовый отчёт печатает верхушку каждого раздела и внизу35перечисляет, что обрезано. Разобрать восемь случаев из тридцати четырёх и сказать «в основном это одно и36то же» — не разбор, а угадывание: остальные двадцать шесть никто не смотрел. Поэтому план строится по37`--json`, где списки полные, а в отчёте у каждого класса стоит **разобрано N из M**. Пока M больше нуля38и не разобрано — работа не закончена, и слово «готово» не произносится.3940## Phase 0. Scope, rules, preflight4142```bash43git status --porcelain # рабочее дерево должно быть чистым44git branch --show-current45bun --version46```4748- **Read the project's rules first.** The scan lists the rule files it found, with line counts. Read them49 before proposing anything: a "violation" that the project deliberately allows is not a finding, and a50 rule the project states explicitly ("SQL only in repositories", "no string literals", "env only through51 the config module") turns half the report from opinion into fact.52- **Decide the scope, and say it.** Preference order: the current diff → a named directory → the whole53 repository, and the last only when asked for exactly that. `--scope diff|branch|<путь>`.54- Dirty tree → **read the diff before asking anything** (`git diff --stat`). Someone's work in progress55 inside the target area moves those files to group C.56- Read the root `package.json` and use the project's own script names. Note that a project can have more57 than one test command (unit and integration are often split, and the second one is not run by the first).58- **Do not introduce tooling.** No new linter, no new config, no new rule in an existing config, no59 formatter run over files the task never mentioned.6061## Phase 1. Baseline (read-only)6263```bash64bun run typecheck # или bunx tsc --noEmit -p tsconfig.json65bun run build # если есть66bun test # и второй тестовый скрипт, если он есть67```6869- **A command that exits 0 without doing anything is not a passing check.** `0 tests found`,70 `no tasks were executed`, a task runner that skipped every workspace — that is a missing check.71- **A red baseline is not a blocker, it is a record.** List what already fails, so that afterwards nobody72 blames the cleanup for it.73- **No tests at all** → say so plainly and narrow the plan to group A.7475## Phase 2. Inventory (read-only)7677```bash78bun ~/.claude/skills/code-tidy/scripts/tidy-scan.ts # обзор: верхушка каждого раздела79bun ~/.claude/skills/code-tidy/scripts/tidy-scan.ts --json # полные списки — план строится по ним80bun ~/.claude/skills/code-tidy/scripts/decisions.ts list # что уже решили не трогать81```8283Текстовый прогон — для человека и для первого взгляда. **Разбирать находки по нему нельзя**: он показывает846–10 строк на список, а внизу честно перечисляет, чего не показал. Как только раздел попал в этот перечень,85дальше работать только с `--json`.8687Then whatever the project already has, **in check mode only**: `bunx tsc --noEmit`, `bunx biome check .`,88`bunx eslint .`, `bunx knip`.8990| Раздел отчёта | Что с этим делать |91| --- | --- |92| 1. Ломает прямо сейчас | `it.only`, `debugger`, `.forEach(async)`, пустой `catch`, `sql.unsafe`, секрет в логе — первым делом и отдельно |93| 2. Мусор | удаляется без обсуждения (группа A) |94| 3. Асинхронность | `await` в цикле и цепочки `await` — группа B: правка меняет семантику |95| 4. Повторы | блоки, похожие перечисления, зашитые значения — разговор, а не команда «вынести» |96| 5. Крупные единицы | сортировка по вниманию; размер сам по себе не дефект |97| 6. Раскладка и именование | сверено с правилами **этого** проекта; «против правила проекта» — сильный аргумент |98| 7. Компоненты и стили | один файл — один компонент, логика наружу, общее в пакет, только Tailwind |99| 8. Единообразие | показан раскол и меньшинство; решение — привести к большинству или узаконить оба |100| 9. Связность | циклы и barrel-файлы — структурная правка |101| 10. Зона риска | что не трогать: чужие правки, горячие файлы, генерация |102| 11. Журнал | отложенное; вернувшиеся вопросы — с объяснением, что изменилось |103104**Раздел 1 — это не уборка, а поломки.** Часть из них снимается удалением (`.only`, `debugger`), но105компонент внутри компонента, хук под условием, список без `key`, пустой `catch`, `sql.unsafe` и секрет106в логе чинятся настоящей правкой кода. Показать их первыми и спросить, чинить сейчас или отдельной107задачей. Молча чинить нельзя: это меняет поведение — пусть и в лучшую сторону.108109Заголовок отчёта отдельно показывает **принятое в проекте**: ролевые суффиксы (`*.const.ts`, `*.port.ts`)110и каталоги (`types/`, `consts/`). Это не рекомендация скилла — это то, что уже господствует в коде.111112### Мёртвый код — отдельным заходом113114Его нет в обычном прогоне: `--dead`. Вердикт по мёртвому коду живёт недолго и требует ручной проверки115чаще, чем всё остальное, — поэтому он не мешается в отчёте, пока его не попросили.116117Когда всё-таки просят: скилл считает **достижимость** от точек входа, а не количество ссылок, поэтому118barrel-файл не оживляет мёртвое имя и не хоронит живое. Но статический анализ не видит:119120- **потребителей вне репозитория** — опубликованный пакет, соседний репозиторий, `exports` в `package.json`;121- **связывание по строке** — DI-контейнеры, таблицы маршрутов, метаданные декораторов, реестры моделей;122- **ссылки вне кода** — имя в JSON/YAML, шаблоне, миграции, CI-конфиге, `Dockerfile`, preload;123- **использование только в тестах** — это не «мёртв», это вопрос: у кода нет пользователя или тест проверяет124 деталь реализации.125126Зоны, где до кода дотягиваются мимо графа (`import()` с переменной, `import.meta.glob`, `ns[key]`), скилл127показывает отдельным списком «вердикта нет». Это кандидаты на чтение, а не на удаление.128129## Phase 3. The plan, by what may be done to it130131### Считать, а не оценивать132133У каждого класса находок в плане стоит число из `--json`, а не впечатление: «34 цикла с `await`», а не134«циклы с запросами». Дальше каждый пункт получает решение — правим, откладываем в журнал или оставляем135с причиной. Класс закрыт, когда сумма решений равна M. Если на весь класс времени нет, так и написать:136«разобрано 8 из 34, остальные 26 — отдельной задачей», и не выдавать восьмёрку за тридцать четыре.137138Одинаковые с виду находки одного класса **не считаются разобранными по образцу**: тридцать четыре цикла —139это тридцать четыре разных запроса, из которых часть схлопывается в один `IN (…)`, часть параллелится,140а часть обязана остаться последовательной. Общий вывод по четырём из них неверен по построению.141142### A. Механика — без обсуждения143144Behaviour-neutral и откатывается одной командой: мусорные файлы (`*.orig`, `*.rej`, `foo copy 2.ts`),145пустые файлы, `debugger`, отладочный `console.log`, закомментированный код блоками, неиспользуемые146импорты, форматирование **по конфигу, который уже лежит в проекте**, отдельным шагом.147148Исключение внутри группы: **`it.only` меняет объём прогона.** Убрать правильно, но это включит тесты,149которые никто не гонял. Убрать → прогнать → отдельно разобрать вскрывшееся, и в отчёте сказать, что150упавшее не сломано уборкой, а перестало быть выключенным.151152### B. Требует решения пользователя153154**Асинхронность.** `Promise.all` — это не оптимизация, а смена семантики, и решает её человек:155156- падает на первой ошибке, остальные промисы продолжают выполняться и их отказы уходят в никуда157 (`allSettled` — другая семантика, не замена);158- порядок побочных эффектов исчезает: если вторая операция рассчитывала на строку, созданную первой,159 параллельный запуск ломает её молча и не всегда воспроизводимо;160- нагрузка идёт разом: пул соединений, лимиты внешнего API, блокировки в БД;161- внутри транзакции параллельные запросы по одному соединению — это гонка, а не ускорение.162163`await` внутри цикла — тот же разговор плюс вопрос о запросе: N однотипных обращений почти всегда164складываются в одно (`IN (...)`, батч, join). Ставить вместо цикла неограниченный `Promise.all` по165коллекции неизвестного размера — это замена медленного кода на падающий под нагрузкой.166167**Повторы.** Три копии — повод спросить, а не вынести (см. фазу 4). Похожие перечисления — то же самое:168одинаковые наборы вариантов часто оказываются осознанным зеркалом по разные стороны границы, через которую169код не переносится (сервер и браузерная сборка, генерируемые типы и ручные словари).170171**Зашитые значения.** Литерал, повторяющийся в нескольких местах, — это незаписанная константа. Куда её172класть, определяет проект, а не скилл: смотри раздел «принято в проекте» и правила.173174**Раскладка и именование.** Перенос объявлений в `types/`/`consts/`, разделение файла, переименование под175стиль каталога. Переименование ломает `git blame` и чужие открытые ветки — оправдано, когда имя врёт.176177**Единообразие.** Меньшинство приводится к большинству — но по умолчанию только в тех файлах, которые и так178правятся по задаче. Сплошной проход по репозиторию — отдельная просьба.179180**Компоненты.**181182- **Один файл — один компонент.** Второй компонент в файле переезжает в свой файл: иначе он не183 переиспользуется, не тестируется отдельно и тянет за собой соседа при каждом импорте.184- **Логика уезжает из файла представления.** Функция, которая не рисует, а считает, форматирует или185 ходит в сеть, живёт в отдельном файле — там её видно другим экранам и там её можно проверить тестом.186 Экспортированная логика в файле компонента — тем более: её уже кто-то тянет через компонент.187 Исключение — хуки: они часть представления и остаются рядом.188- **Общее — в пакет, а не копией.** Одноимённые компоненты в разных приложениях и одинаковые блоки,189 разложенные по разным воркспейсам, переносятся в общий пакет. Копия в каждом приложении расходится190 молча: правку вносят в одну, а вторая живёт своей жизнью.191- **Только TSX.** `.jsx` и `.js` в TypeScript-проекте — недоехавшая миграция.192- Тяжёлый компонент (много пропсов, много `useState`, глубокий JSX) — это разговор о разделении,193 а не приговор длине.194195**Стили.**196197- **Только Tailwind, своего CSS нет.** Единственный оправданный файл стилей — конфигурация темы.198 CSS-модули, `styled`-обёртки и инлайновый `style={{}}` — это второй способ делать то же самое, и199 расходится он с темой молча.200- **Цвет и размер — токенами, а не значениями.** `#ff0000` и `w-[13px]` живут мимо шкалы: тема меняется,201 а они остаются. Произвольное значение оправдано ровно там, где в шкале нужного шага нет, — и тогда202 правильнее добавить шаг в тему.203- **Повторяющийся набор классов — это ненаписанный компонент.** Одна и та же длинная строка в трёх204 местах означает, что общий вид уже существует, просто у него нет имени.205206**Мёртвые экспорты и файлы, barrel-файлы, циклы импортов, публичный API** — как раньше, только с флагом207`--dead` для первых.208209### C. Не трогать210211- сгенерированное и вендоренное (шапка `@generated`, `dist/`, `__generated__/`, `*.gen.ts`, снапшоты);212- миграции — их прошлое неизменяемо;213- файлы с чужими незакоммиченными правками и файлы из открытых веток;214- горячие файлы (раздел 9), если правка не входила в задачу;215- намеренные зеркала словарей через границу, которую код не пересекает;216- всё, что журнал держит в отложенных, пока условие отказа в силе.217218### D. Это не уборка — только в отчёт219220Настоящие баги, `any` вместо типов, гонки, N+1 как архитектурная проблема, слабый тест. Записать одной221строкой с местом и симптомом — и не чинить: багфикс внутри уборочного диффа не пройдёт ревью, и не должен.222223### Уже отвеченные вопросы остаются отвеченными224225```bash226bun ~/.claude/skills/code-tidy/scripts/decisions.ts keep dupe:1a2b3c \227 --reason "зеркало словаря для браузерной сборки, объединять нечем" --fingerprint <из --json>228```229230Ключи и отпечатки лежат в каждой находке `--json`. Отпечаток обязателен везде, где он есть: запись231действует, пока код тот же, а переписанный блок возвращает вопрос — прежнее оправдание относилось не к232нему. Отложенное не переспрашивается: одна строка «отложено: N» и дальше.233234## Phase 4. Правки — по одному классу за раз235236Порядок: мусор → асинхронность → повторы → раскладка → декомпозиция → единообразие. Каждый класс —237отдельный шаг с отдельной проверкой.238239### Асинхронность240241```ts242// было: два независимых запроса стоят в очереди друг за другом243const user = await users.byId(id)244const limits = await limits.forPlan(plan)245246// стало: одновременно — но только если порядок и семантика ошибки это допускают247const [user, limits] = await Promise.all([users.byId(id), limits.forPlan(plan)])248```249250- Проверить независимость по коду, а не по виду: второй вызов не должен читать результат первого ни251 напрямую, ни через общий кэш, ни через строку в БД, созданную первым.252- Цикл с запросом внутри: сначала попытаться сделать **один** запрос вместо N. `Promise.all` по циклу —253 второй выбор, и тогда с ограничением параллельности.254- Внутри транзакции ничего не распараллеливать.255- `.forEach(async …)` — не оптимизация, а потерянные ошибки: переписывается на `for…of` с `await` или на256 `Promise.all(map(...))`, и это осознанный выбор между последовательностью и параллельностью.257258### Повторы259260- **Правило трёх**: две копии — ждём, три — разговариваем. Объединять стоит, только если копии меняются261 вместе и означают одно и то же.262- **Случайное сходство**: два куска выглядят одинаково, но живут по разным причинам. Объединение свяжет их263 навсегда, и следующая правка пойдёт через флаг.264- **Признак плохой абстракции**: чтобы покрыть различия, в общую функцию добавляется параметр,265 переключающий поведение. Появился `options.mode` — остановиться, дубль дешевле.266- **Перечисления**: победитель один, остальные становятся импортом. Если словари стоят по разные стороны267 границы, через которую код не переносится, — оставить оба и записать причину в журнал.268- **Зашитые значения**: выносить туда, где проект держит константы, именовать по смыслу домена, а не по269 значению. Если проект связывает константный объект с одноимённым типом — повторить этот приём. Литерал,270 встречающийся один раз и читаемый на месте, не трогать.271272### Компоненты и стили273274- Разделение файла на компоненты — механический перенос: содержимое не меняется, меняются импорты.275 Переносить по одному и проверять сборку после каждого, а не всё сразу.276- Вынос логики из компонента: сначала перенести функцию как есть, потом (отдельным шагом) убрать из неё277 то, что было завязано на замыкание компонента. Смешать это в один шаг — потерять контроль над диффом.278- Перенос компонента в общий пакет — правка публичного API пакета: сначала согласовать, потом двигать,279 и одним движением обновить обе стороны.280- Свой CSS не переписывать «в классы» пачкой: файл за файлом, со сверкой глазами. Замена CSS на утилиты281 без визуальной проверки — это не уборка, а редизайн вслепую.282- Произвольное значение (`w-[13px]`) не заменять ближайшим шагом шкалы молча: сдвиг на пиксель заметен,283 и решение о нём принимает человек.284285### Раскладка286287- Переносить объявления по ролям, которые уже приняты в проекте, — и не изобретать новые.288- Перенос и переименование — разные шаги: вместе они превращают дифф в нечитаемый.289- После переноса проверить, что импорты обновлены везде, включая тесты и конфиги.290291### Декомпозиция292293- Резать по швам, которые уже есть: тело цикла, ветка условия, шаг с собственным именем. Куску, которому294 нельзя дать честное имя, шов не там.295- `handleRequestPart2` — не декомпозиция, а перенос строк.296- Сигнатура и поведение внешней функции не меняются.297- Длина сама по себе не дефект: плоский `switch` на 200 строк читается лучше пяти функций с общим состоянием.298299### Комментарии300301**Не трогать комментарий, если код рядом не менялся.** Правка комментария даёт дифф с нулевым изменением302логики, а CI/CD видит изменённый файл и запускает пересборку и редеплой зависимых приложений впустую.303Не переписывать, не переводить, не переформатировать заодно; удалять — только вместе с кодом.304305### Форматирование306307Только конфигом проекта и только отдельным шагом. Крупное переформатирование сносит `git blame`; если оно308нужно, предложить `.git-blame-ignore-revs`, но записывает туда человек вместе с коммитом.309310## Phase 5. Проверка311312```bash313bun run typecheck314bun run build315bun test # полный прогон; --changed годится только пока чинишь316git diff --stat # уборка — это в основном удаления317git diff -w # если дифф исчезает, там были только пробелы318git diff # прочитать каждый ханк, который не является чистым удалением319```320321- Диф, который **растёт**, — уже не уборка: появившиеся строки должны объясняться согласованным пунктом322 группы B.323- Правки в асинхронности проверяются не только тестами: перечитать порядок побочных эффектов глазами,324 потому что тест на порядок обычно никто не писал.325- Упало после удаления «мёртвого» кода → символ был живым. Вернуть, записать в журнал, идти дальше.326- Упало то, что было красным в фазе 1 → так и сказать, со ссылкой на запись из фазы 1.327- Тесты не запускаются (нет БД, нет сервисов) → прямо сказать, что проверки не было.328329## Phase 6. Отчёт330331```332## Охват333<что смотрели; какие правила проекта прочитаны>334335## Удалено336<мусор, отладка, закомментированный код; мёртвое — только если был --dead, с доказательством>337338## Асинхронность339<что собрано в Promise.all и почему это безопасно; какие N+1 схлопнуты в один запрос>340<что оставлено последовательным — с причиной>341342## Повторы343<объединённое; оставленные дубли и перечисления — с причиной; вынесенные значения и куда именно>344345## Раскладка и единообразие346<перенесённое по ролям проекта; к какому большинству приведено меньшинство>347348## Компоненты и стили349<что разделено «один файл — один компонент»; какая логика уехала и куда>350<что перенесено в общий пакет; что оставлено копией и почему>351<убранный свой CSS; значения, заменённые токенами — и что оставлено с обоснованием>352353## Не тронуто354<группа C: генерация, миграции, чужие правки, горячие файлы, зеркала словарей>355356## Отложено357<записано в журнал: ключ, причина, что вернёт вопрос; не переспрашивалось: N>358359## Найдено, но не чинилось (группа D)360<баги, типизация, производительность — место и симптом, одной строкой>361362## Разобрано363<по каждому классу: N из M; что осталось и почему>364365## Проверки366<typecheck / build / тесты: было в фазе 1 → стало сейчас>367368## Дифф369<+X / −Y строк; форматирование отдельным шагом: да/нет>370```371372## Never373374- Менять поведение под видом уборки — даже «очевидно правильно» исправляя баг по дороге.375- Собирать `await` в `Promise.all` без разбора порядка побочных эффектов и семантики ошибки; ставить376 неограниченный `Promise.all` по коллекции неизвестного размера; распараллеливать внутри транзакции.377- Заменять цикл с запросом на параллельный цикл, не проверив, можно ли сделать один запрос.378- Сливать два перечисления, стоящие по разные стороны границы, через которую код не переносится.379- Выносить в константу литерал, который встречается один раз и на месте читается лучше.380- Навязывать раскладку и именование, которых в проекте нет: сначала правила проекта, потом господствующий381 в коде приём, и только потом мнение скилла.382- Переименовывать файлы пачкой «под стиль» и совмещать перенос с переименованием.383- Расширять охват за пределы просьбы: соседний файл, «заодно весь каталог», весь репозиторий.384- Смешивать форматирование со смысловой правкой в одном диффе.385- Запускать `--fix`/`--write`/`--unsafe` до утверждения плана.386- Добавлять инструмент, конфиг или правило как побочный эффект уборки.387- Удалять экспорт, потому что так сказал сканер: сначала снять `export` и спросить компилятор, а для388 ненадёжных зон — `grep` по строке и `git log -S`.389- Удалять файл, помеченный как «вердикта нет» (динамика, глоб, реестр по строке).390- Трогать сгенерированное, вендоренное, миграции и снапшоты.391- Править файл с чужими незакоммиченными изменениями, не спросив.392- Комментировать код вместо удаления и оставлять `.bak`-копии «на всякий случай».393- Править комментарий там, где код не менялся.394- Молча чинить поломки из раздела 1: компонент внутри компонента, хук под условием, список без `key` —395 это правка поведения, её сначала показывают.396- Переписывать CSS в утилитарные классы без визуальной проверки и менять произвольные значения на397 ближайший шаг шкалы по своему усмотрению.398- Переносить компонент в общий пакет, не согласовав: это правка публичного API пакета.399- Делать вывод о классе находок по верхушке списка и по разбору нескольких случаев: «посмотрел четыре,400 остальные такие же» — это не проверка, а предположение.401- Говорить «готово», пока в отчёте сканера стоит «ПОКАЗАНО НЕ ВСЁ» и раздел не разобран по `--json`.402- Писать в отчёте «нет таких находок», когда на самом деле их не смотрели: «не найдено» и «не разбирали» —403 разные строки.404- Ослаблять или удалять тест, чтобы дифф стал зелёным. Пропущенный тест — находка, а не мусор.405- Переспрашивать то, что уже лежит в журнале с действующей причиной; и наоборот — оставлять отказ406 незаписанным.407- Коммитить и пушить: коммит — отдельная явная просьба.408- Докладывать об успехе, когда тесты не запускались или упали.