diff --git a/docs/specs/425-core-file-budget.md b/docs/specs/425-core-file-budget.md new file mode 100644 index 00000000..213aaf2a --- /dev/null +++ b/docs/specs/425-core-file-budget.md @@ -0,0 +1,102 @@ +# ТЗ #425 — Ядра фронтенда растут только осознанно + +- Issue: https://github.com/Matysh/houseplan-card/issues/425 +- Приоритет: P2, tech-debt; полный трек — не потому, что задача сложная, а + потому, что лёгкий трек в этом репозитории не обслуживается конвейером + («метка small конвейер не запускает»), и ревью ТЗ на нём пришлось бы просить + у человека. Для гейта, который будет стоять в CI годами, дешевле пройти + обычную процедуру +- Ревизия: 1 (2026-09-03) +- Заменяет #34 ([HP-ARCH-01]), который закрывается вместе с этой задачей + +## Почему не проект, а ограничитель + +#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`: правило и что делать, когда гейт покраснел.