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

121 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ #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`: правило и что делать, когда гейт покраснел.