Files
houseplan-card/docs/specs/425-core-file-budget.md
T
2026-09-03 00:19:07 +03:00

9.7 KiB
Raw Blame History

ТЗ #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) — тест проверяет её на всех ветках без чтения файлов, и она же вызывается на реальных ядрах.

  1. рост выше потолка → нарушение с числом превышения (AC1);
  2. падение ниже потолка больше чем на люфт → нарушение с требованием опустить потолок (AC2);
  3. изменение в пределах люфта → пусто (AC3);
  4. ровно на потолке и ровно на границе люфта → пусто (границы включительно);
  5. потолки заданы для обоих ядер и ни для чего больше (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: правило и что делать, когда гейт покраснел.