9.7 KiB
ТЗ #425 — Ядра фронтенда растут только осознанно
- Issue: https://github.com/Matysh/houseplan-card/issues/425
- Приоритет: P2, tech-debt; полный трек — не потому, что задача сложная, а потому, что лёгкий трек в этом репозитории не обслуживается конвейером («метка small конвейер не запускает»), и ревью ТЗ на нём пришлось бы просить у человека. Для гейта, который будет стоять в CI годами, дешевле пройти обычную процедуру
- Ревизия: 1 (2026-09-03)
- Заменяет #34 ([HP-ARCH-01]), который закрывается вместе с этой задачей
Сценарий
Продуктового сценария у задачи нет — она инженерная, и это сказано явно, а не подразумевается. Разработчик добавляет функцию, кладёт её в ядро, потому что там уже всё, и ядро прибавляет ещё пятьсот строк. Никто не против: каждый шаг разумен, а счёт никто не ведёт. Через год 42% кода снова в двух файлах, и следующая функция стоит дороже предыдущей.
Что человек увидит до и после
Пользователь — ничего: продуктовый код задача не трогает.
До: ядра растут молча, и узнать об этом можно только из аудита, если кто-то
удосужится посчитать.
После: рост выше потолка роняет npm test и называет цену числом, а
уменьшение требует опустить потолок — то есть выигрыш фиксируется, а не
испаряется.
Почему не проект, а ограничитель
#34 заведён 09.08, за 25 дней не сдвинулся и шестой аудит подряд отмечается как «без движения». При этом декомпозиция идёт — но не как проект, а попутно: файлов в src/ было 18 на v1.58, сейчас больше сотни; houseplan-editor-runtime.ts выделен из ядра между v1.66 и v1.69; за последние недели отдельными модулями вышли support-feedback.ts, command-stack.ts, device-area-relocation.ts, danger-confirm.ts.
Целевая карта каталогов из ТЗ #34 (app/, editors/, render/, dialogs/) не реализована и, судя по темпу, не будет. Комментарий от 30.08 в самом #34 это уже признал: «исходная метрика и целевая карта устарели».
Но долг никуда не делся, и вот он в числах:
| card | runtime | |
|---|---|---|
| v1.66.0 | 20 287 | — |
| v1.69.0 | 12 824 | 13 290 ← разделение ядра |
| v1.70.0 | 13 603 | 14 287 |
| dev | 13658 | 14322 |
Два файла держат 42% всего TypeScript в src/, и оба растут примерно на 500–1000 строк за релиз: новое уезжает в новые модули, а ядра всё равно прибавляют. Закрыть #34 молча — значит перестать это считать.
Контракт
Размер двух ядер зафиксирован потолком, и потолок работает храповиком:
- ядро выросло выше потолка → отказ с числом: «выросло на N строк, вынесите столько же»;
- ядро уменьшилось заметно ниже потолка → тоже отказ: «опустите потолок на N». Без этой половины выигрыш от любого выноса испарится молча, и через год потолок станет фикцией.
Люфт между «уменьшилось» и «пора опускать потолок» — 250 строк, чтобы обычная правка не требовала трогать константу.
Правило не запрещает расти: оно требует называть цену. Хочешь добавить в ядро — вынеси столько же, и это видно в дифе одной строкой.
AC
- AC1. Рост любого из двух ядер выше потолка роняет
npm testс сообщением, называющим файл, потолок и превышение в строках. Доказательство: мутант, добавляющий строки вhouseplan-card.ts, прогнанный штатным раннером. - AC2. Уменьшение ядра больше чем на 250 строк ниже потолка тоже роняет тест — с требованием опустить потолок. Доказательство: юнит на чистой функции.
- AC3. Изменение в пределах люфта тест не трогает. Доказательство: тот же юнит.
- AC4. Потолки записаны числами в самом тесте: любое их изменение видно в дифе и проходит ревью как решение, а не как побочный эффект. Доказательство: ревью кода — в тесте нет чтения потолков из внешнего источника (файла, переменной окружения, вычисления от текущего размера), и юнит на то, что функция принимает потолки аргументом, а не добывает их сама.
- AC5. Гейт судит только два ядра. Остальные файлы
src/не ограничиваются: цель — не заморозить размер кода, а сделать рост ядер осознанным.
Скоуп / не-скоуп
В скоупе: один тест с потолками и храповиком, мутант, строка в
docs/TESTING.md, закрытие #34 с объяснением.
Не в скоупе: сама декомпозиция (она идёт попутно и продолжит идти), другие
файлы src/**, стилевой слой, размер бандла (у него свой бюджет — #367).
UX, модель данных, i18n
Не применимо: продуктового кода задача не касается.
План автотестов
Логика выносится в чистую функцию coreBudgetViolations(sizes, caps, slack) —
тест проверяет её на всех ветках без чтения файлов, и она же вызывается на
реальных ядрах.
- рост выше потолка → нарушение с числом превышения (AC1);
- падение ниже потолка больше чем на люфт → нарушение с требованием опустить потолок (AC2);
- изменение в пределах люфта → пусто (AC3);
- ровно на потолке и ровно на границе люфта → пусто (границы включительно);
- потолки заданы для обоих ядер и ни для чего больше (AC5).
Мутант (scripts/mutation-gate.mjs): core-budget-ignores-growth —
добавить в src/houseplan-card.ts заведомо больше строк, чем люфт, → тест
красный. Мутант правит ЯДРО, а не тест: проверяется, что гейт видит рост
настоящего файла, а не что арифметика сходится сама с собой.
Риски
- Потолок будут поднимать не глядя. Это главный риск любого храповика. Смягчение: число живёт в тесте, его изменение попадает в дифф и проходит ревью как решение; сообщение об отказе прямо называет альтернативу — вынести столько же строк.
- Строки — грубая мера. Файл можно «уменьшить», сжав форматирование. Смягчение: мера и не претендует на точность, она нужна как сигнал; ревью видит дифф и отличает вынос от переупаковки.
- Люфт 250 строк может оказаться мал или велик. Смягчение: он записан константой рядом с потолками, меняется тем же порядком и с тем же ревью.
Откат
Удаление одного тестового файла и одного мутанта.
Release-артефакты
docs/CHANGELOG.md/docs/CHANGELOG.ru.md: не требуется (User-Visible: no).docs/TESTING.md: правило и что делать, когда гейт покраснел.