Files
houseplan-card/scripts/bundle-budget.mjs
T

859 lines
64 KiB
JavaScript
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.
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { assertBundleManifest } from './bundle-tree.mjs';
import { NAMESPACE_LOCALE_CHUNKS } from './bundle-manifest.mjs';
// #352: the budget guards the CLASS of regression — tens of kilobytes from
// an accidentally imported dependency or an eagerly bundled dictionary —
// not every byte. v1.69.0-beta.1 shipped at 255 993 B gzip against a
// 256 000 B ceiling: seven bytes of headroom turned the gate into a
// lottery where the unlucky LAST commit goes red, not the one that grew
// the bundle (5740324b was exactly that). The ceiling therefore keeps a
// deliberate ~10% allowance over the measured fact; the fact and headroom
// are printed on every run so the trend is visible long before the wall.
//
// #367, рекалибровка 29.08. Запас съеден с 26 КБ до 8.3 КБ, и это не диффузное
// расползание, а один шаг плюс обычная работа. Факт по документам код-ревью:
//
// #317 256 127 · #318 256 091 · #341 256 046 · #354 257 212 · #159 256 828
// #357 271 143 ← +14 КБ за один заход
// #20 271 455 · #361 272 848 · #359 272 469 · #360 273 697 ← текущий факт
//
// Шаг на #357 — plan-art мебели: 44 top-view символа в eager-графе, которые
// платит КАЖДЫЙ план, включая планы без единого предмета мебели. Остальные
// приросты по килобайту и относятся к обычным фичам.
//
// Поэтому запись честная: рекалибровка НИЧЕГО не ускоряет и ничего не чинит.
// Она фиксирует, что 273 697 Б — новая норма, и возвращает рабочий запас, чтобы
// гейт снова красил того, кто вырастил бандл, а не того, кто пушнул последним.
// Настоящий рычаг остаётся прежним и описан в #367: вынести plan-art мебели в
// ленивый чанк (−13.6 КБ, вариант 1) либо русский словарь (вариант 2).
//
// Measured fact at recalibration (dev @ 360, 29.08.2026): 273 697 B gzip.
// 273 697 × 1.10 = 301 067 — прежний потолок 300 000 держался внутри правила ~10%.
//
// 2026-09-05, #462: общий бюджет поднят 300 000 → 301 066. Контроллер
// согласования frontend/backend версии, безопасный kiosk reload и его banner
// обязаны жить в initial View: механизм нужен до любого lazy import и именно
// лечит вкладку со старым eager bundle. Измеренный шаг 297 503 → 300 090 Б
// gzip (+2 587 Б); presentation/safety при этом вынесены из ядра карточки в
// отдельный adapter. Новый бюджет — целая часть прежней верхней границы 10%
// над фактом #367 и оставляет 976 Б запаса; следующий рост обязан решать #367.
export const INITIAL_VIEW_GZIP_BUDGET = 301_066;
export const INITIAL_PANEL_ONLY_GZIP_BUDGET = 8 * 1024;
/**
* Порог, ниже которого запас перестаёт быть запасом (#367).
*
* Прежняя редакция полагалась на то, что человек заметит тренд в выводе. За
* сутки запас ушёл с 26 КБ до 8.3 КБ, и не заметил никто — потому что каждая
* отдельная строка выглядела нормально. Предупреждение срабатывает за две
* средние фичи до стены, а не после неё.
*/
export const LOW_HEADROOM_WARNING_BYTES = 15_000;
/**
* Потолок графа с полосой — работающий храповик (#438), не разовый (#429).
*
* История в двух шагах. #429 снял `SUPPORT_LAZY_INITIAL_BASELINE_BYTES = 291046`
* — порог, привязанный к критерию приёмки #423, с пятнадцатью байтами запаса и
* сообщением про копирайт формы поддержки. Снять было правильно: он обвинял бы
* не ту задачу. Но снят он оказался ровно на том релизе, где сработал бы:
*
* v1.71.0-beta.1 291 031 снятый порог 291 046 beta.2 291 069
*
* То есть рост произошёл внутри той же беты, уже после снятия, и заметить его
* стало нечем: единственным оставшимся сигналом был `lowHeadroomWarning`, а он
* горит постоянно — третий аудит подряд, и третий раз без реакции.
*
* Почему полоса, а не точное число. Метрика — gzip, и она не монотонна по
* исходнику: на beta.2 initial-чанк стал МЕНЬШЕ на 344 сырых байта и при этом
* на 40 байт больше в сжатом виде (переименования минификатора и контекст
* сжатия). Точный храповик по gzip краснел бы на коммитах, которые код
* сокращают, — та же лотерея, о которой предупреждает комментарий к бюджету
* выше: красным станет последний пушнувший, а не тот, кто вырастил граф.
*
* Поэтому правило двустороннее и с полосой, как у ядер (#425): значение обязано
* лежать в `[ceiling - band, ceiling]`. Рост выше потолка — отказ с числом:
* поднимайте потолок в том же коммите с объяснением либо выносите код в
* ленивый граф (#367). Падение ниже полосы — тоже отказ: незафиксированный
* выигрыш отыгрывается обратно молча, и это ровно то, что случилось с запасом
* бюджета (26 КБ → 8.3 КБ за сутки).
*
* Полоса 2 000 Б — примерно одна средняя фича в initial-графе и заметно больше
* колебаний метрики от переименований (наблюдаемые единицы-десятки байт). До
* стены 300 000 остаётся четыре явных шага вместо двухсот незаметных.
*
* Потолок поставлен так, чтобы измеренный факт лежал ближе к середине полосы:
* 291 069 при потолке 292 000 — это 931 Б до отказа сверху и 1 069 Б снизу.
* Иначе одна из сторон срабатывает на десятках байт, то есть на шуме: правило
* должно требовать решения от РЕАЛЬНОГО изменения, а не от переименования.
*
* 2026-09-03, #162: потолок поднят 292 000 → 294 000. Причина — маршрутизация
* карт робота по пространствам: `src/vacuum-routes.ts` обязан быть в
* initial-графе, потому что решение «какая карта на каком этаже» принимает
* View, а не редактор. Замеренная цена 1 075 Б gzip (291 203 → 292 278).
* Редакторская половина при этом в initial-граф НЕ попала: правки маршрутов
* вынесены в `src/vacuum-route-edit.ts`, редакторский UI — в
* `src/editors/vacuum-maps-section.ts`, а тексты блока — в ленивый словарь
* `src/i18n/support/*.json` (приём #423). Эти три выноса дали −1 326 Б от
* первой редакции правки (293 604 → 292 278); без них рост был бы вдвое
* больше. Вторая часть роста — предупреждение у дока: четыре локализованных
* причины обязаны быть во View-словаре, потому что текст читает пользователь
* карточки, а не редактора (+581 Б). Итог 292 859: 1 141 Б до потолка сверху и
* 859 Б до нижней границы полосы — обе стороны дальше шума метрики.
*
* 2026-09-04, #152: потолок поднят 294 000 → 296 000. Room fit является
* действием основного View: канонический owner жеста, точные Flat/Iso bounds и
* camera target нужны до открытия любого редактора и не могут жить в его
* ленивом графе. Измеренный initial-граф вырос 293 220 → 294 864 Б gzip
* (+1 644 Б); после шага остаётся 1 136 Б сверху и 864 Б до нижней границы
* полосы, то есть обе стороны дальше наблюдаемого шума. Общий бюджет 300 000 Б
* не меняется; его запас после функции — 5 136 Б, а долг ленивого выноса #367
* остаётся видимым через LOW_HEADROOM_WARNING.
*
* 2026-09-04, #449: потолок поднят 296 000 → 297 000. Единый распознаватель
* double-click/double-tap для свободного фона View входит в initial-граф;
* после реализации он занимает 295 743 Б gzip. Запас до потолка — 1 257 Б,
* до нижней границы полосы — 743 Б, то есть обе стороны остаются дальше
* наблюдаемого шума. Общий бюджет 300 000 Б и долг #367 не меняются.
*
* 2026-09-04, #451: потолок поднят 297 000 → 298 000. Терминальная доставка
* последнего HA-снимка и явное управление кадром редактора принадлежат
* основному View: без них пропущенный полный render оставлял бы устаревшее
* состояние после pan/resize. После исправления initial-граф занимает
* 296 717 Б gzip: 1 283 Б сверху и 717 Б до нижней границы полосы. Общий
* бюджет 300 000 Б не меняется; долг ленивого выноса #367 остаётся видимым.
*
* 2026-09-05, #460: полоса перецентрирована 298 000 → 298 500 без изменения
* общего бюджета. Продуктовый код задачи находится в ленивом editor-графе,
* однако новые content hash и имя динамического чанка сдвинули initial gzip
* 297 496 → 297 503 Б. Старый факт уже оставлял всего 504 Б до верхней границы,
* то есть следующий семибайтный шум нарушил обязательный запас >500 Б.
* Новый центр оставляет 997 Б сверху и 1 003 Б снизу; бюджет 300 000 Б и
* предупреждение о малом запасе не меняются.
*
* 2026-09-05, #462: потолок поднят 298 500 → 301 000 вместе с общим бюджетом
* 301 066. Обязательный eager version-recovery занимает initial-граф до
* lazy editor/onboarding и потому не может быть вынесен из него. Факт
* 300 090 Б расположен почти по центру полосы: 910 Б сверху и 1 090 Б снизу.
* Core adapter вынесен в `src/version-recovery-card.ts`; это изменение
* фиксирует цену новой границы восстановления, а не разрешает молчаливый рост.
*
* 2026-09-05, #160: потолок опущен 301 000 → 300 000 без изменения общего
* бюджета. Скрытый Stage 3 вынесен в отдельный alpha-runtime: обычный View
* больше не загружает renderer/overlay/opening graph. Измеренный initial факт
* 299 495 Б оставляет 505 Б сверху и 1 495 Б до нижней границы полосы;
* lazy-isometric graph отдельно измеряется манифестом (12 043 Б gzip).
*
* 2026-09-06, v1.73.0-beta.1: потолок перецентрирован 300 000 → 300 500
* без изменения общего бюджета. Более длинная prerelease-версия подняла
* измеренный initial факт до 299 501 Б и оставила лишь 499 Б сверху — меньше
* обязательного шумового запаса. Новый центр оставляет 999 Б сверху и
* 1 001 Б снизу; продуктовый граф при этом не расширялся.
*
* 2026-09-06, #474: потолок опущен 300 500 → 290 500 без изменения общего
* бюджета. Арт 44 дизайнерских символов мебели (`furniture-plan-art.generated`)
* вынесен в ленивый чанк `lazyFurnitureArtFiles` (10 252 Б gzip): его грузят
* только планы с мебелью, редактор отдаёт его рантайму синхронно. Измеренный
* initial факт 289 690 Б оставляет 810 Б сверху и 1 190 Б до нижней границы
* полосы; запас до стены 301 066 — 11 376 Б.
*
* 2026-09-06, #478: потолок опущен 290 500 → 289 500 без изменения общего
* бюджета. Удаление persisted room-draft веток снизило initial факт до
* 288 571 Б: 929 Б сверху и 1 071 Б до нижней границы полосы. Экономия
* закреплена и не превращается в разрешение для незаметного роста.
*
* 2026-09-07, #53: потолок перецентрирован 289 500 → 290 500 без изменения
* общего бюджета. Кнопка PDF и exact-build loader подняли initial факт до
* 289 556 Б; writer, шрифт, печатная сцена и общий дизайнерский арт мебели
* остались в `lazyPdfFiles` (114 486 Б gzip). Новый центр даёт 944 Б сверху и 1 056 Б
* снизу; PDF-граф не пересекается с initial View.
*
* 2026-09-07, #487: потолок поднят 290 500 → 291 500 без изменения общего
* бюджета. Pure-resolver диапазона комнаты и его вызовы нужны обоим View-
* renderer'ам до загрузки редактора: именно они меняют температурную заливку
* обычного и статичного плана. Измеренный факт 290 937 Б оставляет 563 Б
* сверху и 1 437 Б до нижней границы полосы; UI и writer остались в lazy editor.
*
* 2026-09-08, #437: потолок перецентрирован 291 500 → 292 000. Весь runtime
* сводной панели, её CSS, четыре словаря, форма и расчёт агрегатов вынесены в
* ленивые чанки; в initial View остались только точка динамической загрузки,
* capability и narrow bridge, без которых панель нельзя безопасно запустить.
* Измеренный факт 291 219 Б оставляет 781 Б сверху и 1 219 Б снизу; общий
* бюджет 301 066 Б не меняется.
*
* 2026-09-08, #489: потолок перецентрирован 292 000 → 292 500 без изменения
* общего бюджета. Публичные data-hp hooks обязаны находиться в исходной DOM-
* разметке View: внешний E2E должен видеть readiness, режим и базовые действия
* до загрузки любого editor-runtime. Машиночитаемый инвентарь и его проверки
* остаются вне runtime. Измеренный initial-факт 291 681 Б оставляет 819 Б
* сверху и 1 181 Б до нижней границы полосы — обе стороны дальше
* наблюдаемого gzip-шума; общий бюджет и долг #367 не меняются.
*
* 2026-09-08, #485: the authenticated live-presence controller, normalized
* frame model and pointer-transparent renderer join the initial View graph;
* the large setup UI remains lazy. After rebasing over the public data-hp
* hooks from #489 the measured graph is 298 474 B. After rebasing over #493,
* harmless gzip movement put the same graph at 298 527 B, just 473 B below
* that ceiling. A 299.1 kB ceiling restores the required >500 B noise margin
* on both sides of the existing 2 kB ratchet band. The overall 301 066 B
* budget is unchanged.
*
* 2026-09-09, #506: потолок перецентрирован 299 100 → 299 600. В initial View
* вошёл `summary-runtime-loader.ts` — кеш фабрики кода сводной панели и слот
* подключения на экземпляр (+299 Б gzip); сам runtime панели остаётся
* ленивым чанком. Измеренный факт 298 891 Б оставляет 709 Б сверху и 1 291 Б
* до нижней границы полосы; общий бюджет 301 066 Б и долг #367 не меняются.
*
* 2026-09-10, #500: потолок перецентрирован 299 600 → 300 300. Идентичность
* config/layout и единая гейт-последовательность adoption вынесены в
* `config-adoption.ts` (+~740 Б gzip: владелец идентичности, делегаты хоста
* с harness-сеттерами ревизий, post-write профиль для четырёх путей, которые
* раньше гейт пропускали); дубли в `_loadFromServer`/`_reloadConfigOnly`/
* summary recovery сняты. Измеренный факт 299 774 Б оставляет 526 Б сверху и
* 1 474 Б до нижней границы полосы; общий бюджет 301 066 Б и долг #367 не
* меняются.
*
* 2026-09-10, #520: потолок перецентрирован 300 300 → 300 400. Усыновление
* авторитетного конфига стало атомарным: восстановление вьюпорта, флаг
* готовности и посев устройств ушли в `afterAdopt`, а хвост загрузки
* пересобирает устройства ровно один раз (+40 Б gzip — хук общей
* последовательности, флаг и замыкание в `_loadFromServer`). Измеренный факт
* 299 812 Б оставляет 588 Б сверху и 1 412 Б до нижней границы полосы; общий
* бюджет 301 066 Б и долг #367 не меняются.
*
* 2026-09-11, #525: потолок перецентрирован 300 400 → 300 700. Списки проёмов
* и маркеров переведены на ключи (`repeat` по `id`) — без них Lit
* переиспользовал узлы по позиции и при смене пространства доигрывал переход
* створки соседней двери. Цена — сама директива `repeat`, впервые попавшая в
* начальный граф View: +241 Б gzip (проверено альтернативой `keyed` в
* обёртке-IIFE — та дороже, +373 Б). Измеренный факт 300 059 Б оставляет
* 641 Б сверху и 1 359 Б до нижней границы полосы. Общий бюджет 301 066 Б не
* меняется, но зазор между потолком и бюджетом сжался до 366 Б: долг #367
* из предупреждения стал ограничением, следующая правка начального графа
* упрётся в него всерьёз.
*
* 2026-09-11, #526: потолок опущен 300 700 → 288 300. Минификатор шаблонов
* стилей не работал ни разу: его отсев искал `css` вплотную к бэктику, а
* плагин видит вывод TypeScript, где принтер ставит пробел. Включение снимает
* 58 393 Б сырого текста (комментарии и отступы 23 КБ таблиц стилей) и
* 12 816 Б gzip. Измеренный факт 287 284 Б оставляет 1 016 Б сверху и 984 Б до
* нижней границы полосы; общий бюджет 301 066 Б не меняется, запас до него
* вырос с 955 Б до 13 782 Б.
*
* 2026-09-12, #543: потолок перецентрирован 288 300 → 288 900 без изменения
* общего бюджета. В initial View вошёл session-scoped владелец конкурентных
* config reload: request claims, event high-water и lifecycle/baseline guards,
* без которых поздняя подложка откатывала уже показанный план. Измеренный факт
* 288 226 Б оставляет 674 Б сверху и 1 326 Б до нижней границы полосы; тяжёлых
* UI/runtime зависимостей механизм не добавляет.
*
* 2026-09-13, #563: потолок перецентрирован 288 900 → 289 100 без изменения
* общего бюджета. Событийный барьер multi-touch вошёл в initial View и поднял
* измеренный факт 288 236 → 288 399 Б gzip; упрощённый публичный контракт
* помощника оставляет в графе только реально используемое состояние. Новый
* центр даёт 701 Б сверху и 1 299 Б до нижней границы полосы.
*
* 2026-09-14, #564: потолок перецентрирован 289 100 → 290 900 без изменения
* общего бюджета. В initial View появился screen-space resolver плотных
* маркеров: 44 px spatial index, painted-capsule priority и pointer-owner latch
* для hover/click/long-press/drag. Основная реализация вынесена из карточки в
* отдельный модуль; механизм обязан быть синхронным на первом касании и потому
* не может жить в lazy editor graph. Измеренный факт 290 181 Б оставляет 719 Б
* сверху и 1 281 Б до нижней границы полосы.
*
* 2026-09-15, #577: потолок перецентрирован 290 900 → 291 300 без изменения
* общего бюджета. Выбор внутренней или внешней грани окна относится к
* синхронной геометрии лучей в initial View. Измеренный факт 290 629 Б
* оставляет 671 Б сверху и 1 329 Б до нижней границы полосы.
*
* 2026-09-15, #581: потолок перецентрирован 291 300 → 291 700 без изменения
* общего бюджета. Цвет комнаты участвует в раскраске только вместе с её
* собственным режимом «Свой цвет» — одна проверка режима в `roomCustomFillOf`
* и та же проверка на черновике диалога (+16 Б gzip). Факт на `dev` после #580
* был 290 789 Б — 511 Б сверху, у самого края шумовой полосы; измеренный факт
* 290 805 Б под новым центром оставляет 895 Б сверху и 1 105 Б до нижней
* границы полосы.
*
* 2026-09-15, #582: после #581 потолок остаётся 291 700 без изменения общего
* бюджета. Camera-activated stage-sized SVG дневного контура входит в
* синхронный View до первого движущегося кадра; общий renderer бумажных фигур
* удержал объединённый факт на 291 058 Б — 642 Б сверху и 1 358 Б до нижней
* границы полосы.
*
* 2026-09-18, #588: потолок перецентрирован 291_700 → 292_400 без изменения
* общего бюджета. Пятый режим «Отображение» — это два производных предиката
* вместо одной проверки токена и пятая строка в двух словарях ярлыков; всё это
* синхронный View, потому что режим решает, что нарисовано внутри маркера при
* первом кадре. Факт на `dev` был 291 156 Б, измеренный факт 291 346 Б (+190 Б)
* оставлял под прежним центром 354 Б сверху — внутри шумовой полосы метрики.
* Новый центр даёт 1 054 Б сверху и 946 Б до нижней границы полосы.
*/
// #598: +119 B — три диалога настроек получили заголовки карточек, и это
// СТРОКИ: 12 новых ключей в четырёх локалях плюс `gs.hint`, переехавший из
// ленивого support-словаря в основной, потому что «?» читает его через `_t`.
// Код диалогов остался в ленивом графе; вырос словарь, который там же и был.
// Опущен до 291 400 в #593, поднят обратно осознанно — рост объяснён здесь.
/**
* #600, серия 2: 292 500 → 292 200 (замер 290 711). Стартовый граф похудел:
* компас «Общих настроек» стал индикатором набора, его 120-px CSS ушёл из
* eager-листа `plan.styles.ts`, а мёртвые ключи основного каталога удалены.
* Потолок опущен, чтобы выигрыш не потерялся.
*/
/**
* #600, серия 3: 292 200 → 291 600 (замер 290 575). Из eager-листа
* `dialogs.styles.ts` ушли правила диалога комнаты (`.roomtemprange*`,
* `.roomdialog .srcrow/.dropbtn`), модуль `room-temperature-controls.ts`
* удалён — его заменили поля набора в ленивом графе. Потолок опущен следом.
*/
/**
* #600, финал: 291 600 → 290 900 (замер 289 878). Из eager-листа
* `dialogs.styles.ts` и `chrome.styles.ts` сняты стили прежней разметки диалога
* устройства (`.bindsel/.bindharow`, `.ctrlchip*`, `.markerhelp*`,
* `.markerradios`, `.markerlightdisabled`, `.pdftag`, `.descin`, `.togglehint`
* и др.) — их заменил лист набора в ленивом графе.
*/
/**
* #609: 290 900 → 291 400 (замер 290 445). Публичные CSS-переменные оболочки
* настоящего `ha-dialog` и мобильные media rules входят в eager-граф: форма
* обязана получить правильную геометрию до загрузки ленивого редактора. Новый
* центр оставляет 955 Б сверху и 1 045 Б до нижней границы полосы; общий бюджет
* не меняется.
*
* 2026-09-23, #614: 291 400 → 292 000 (замер 291 041). Перенос baseline
* между warm-диалогами и хостом обязан жить в eager-графе: иначе при
* повторном открытии диалог теряет единую dirty-семантику до загрузки
* редактора. Факт оставляет 959 Б сверху и 1 041 Б до нижней границы;
* общий бюджет не меняется.
*
* 2026-09-23, #624: 292 000 → 290 400 (замер 289 448). Из карточки сняты
* мёртвые делегаты и поля, из обоих ядер — мёртвые копии объявлений и
* неиспользуемые импорты; rollup выбрасывал их и раньше, но не всё: минус
* 1 593 Б gzip в стартовом графе. Факт оставляет 952 Б сверху и 1 048 Б до
* нижней границы; общий бюджет не меняется.
*
* 2026-09-25, #645: 290 400 → 291 000 (замер 290 022). Арбитраж второго
* touch-контакта с перетаскиваемой кнопкой комнаты обязан быть в eager-карточке:
* он передаёт начавшийся на кнопке жест существующему pinch-навигационному
* контуру. Факт оставляет 978 Б сверху и 1 022 Б до нижней границы; общий
* бюджет не меняется.
*
* 2026-09-25, #616: 291 000 → 292 600 (замер 291 635). Компактная шапка
* телефона — строка, меню шестерёнки и его стили — часть View домочадца на
* телефоне и обязана работать до загрузки ленивого редактора: у не-админа
* редактор не грузится вовсе. Факт оставляет 965 Б сверху и 1 035 Б до нижней
* границы; общий бюджет не меняется.
*
* 2026-09-25, #649: 292 600 → 294 900 (замер 294 004 после ребейза на #648). 2.5D стал публичным
* режимом: переключатель «Общих настроек», стили плиток и их сгенерированная
* таблица состояний (`iso-tiles`), материалы стен и определение светлого пола
* нужны карточке до загрузки ленивого `iso-scene-render` — CSS не может ждать
* граф, иначе первый кадр 2.5D рисует круги Flat. Геометрия света и коллизии
* остались в ленивом графе. Факт оставляет 896 Б сверху и 1 104 Б до нижней
* границы; общий бюджет не меняется.
*
* 2026-09-26, #654: 294 900 → 296 000 (замер 295 047). Холодный первый кадр
* обязан знать, что 2.5D запрошен, пока его ленивый граф ещё не загружен, и
* удерживать заставку до вычисления цвета бумаги; эту границу по определению
* нельзя положить внутрь охраняемого ею чанка. Геометрия и сам рендер остаются
* ленивыми. Факт оставляет 953 Б сверху и 1 047 Б до нижней границы; общий
* бюджет не меняется.
*
* 2026-09-26, #663: 296 000 → 299 700 (замер 298 722). Read-only геометрия
* двух типов лестниц, межэтажный link hit-target и вычитание их footprint из
* чистой площади нужны непосредственно в View. Placement, transform и
* properties вынесены в lazy editor; общий бюджет 301 066 Б не меняется.
* Центр оставляет 978 Б сверху и 1 022 Б до нижней границы полосы.
*
* 2026-09-27, #683: 299 700 → 300 200 (замер 299 638). Разрешение сохранённых
* цветов лестницы, CSS-переменные её символа и геометрия трапеции/равных
* ступеней нужны обоим View-renderer'ам до загрузки редактора; сами два
* color-picker и запись свойств остаются в lazy editor. Новый центр оставляет
* 562 Б сверху и 1 438 Б до нижней границы полосы. Общий бюджет 301 066 Б не
* меняется; малый остаток продолжает печататься отдельным предупреждением.
*/
export const INITIAL_VIEW_GZIP_CEILING = 300_200;
export const INITIAL_VIEW_CEILING_BAND = 2_000;
/**
* До какого потолка предупреждение о запасе погашено владельцем.
*
* `null` — не погашено, и это текущее состояние: запас 10 885 Б при пороге
* 15 000, долг живёт в #367. Предупреждение, которое нельзя погасить, читается
* как выключенное, поэтому его можно погасить — одной строкой здесь, со
* ссылкой на решение. Признание привязано к ЗНАЧЕНИЮ потолка: как только
* потолок поднимут, оно перестаёт покрывать, и вопрос возвращается ровно
* тогда, когда граф снова вырос.
*/
export const LOW_HEADROOM_ACKNOWLEDGED_CEILING = null;
/**
* Маркеры формы поддержки и граф, которому каждый принадлежит.
*
* #627: английский — слой отката словаря `support`, он статически в графе
* редактора. Русский с этой задачи — отдельный ленивый чанк `support`-ru: в
* статическом графе редактора его быть НЕ должно, иначе ×4 словарь вернулся.
*/
export const SUPPORT_LAZY_MARKERS = [
{ text: 'Contact details (email/tg/WhatsApp), optional.', graph: 'lazyEditorFiles' },
{ text: 'Контакт для связи (email/tg/WhatsApp), необязательно.', graph: 'lazyNamespaceLocaleFiles' },
];
const GRAPH_LABELS = {
lazyEditorFiles: 'lazy editor graph',
lazyOnboardingFiles: 'lazy onboarding graph',
lazyNamespaceLocaleFiles: 'lazy namespace locale graph',
};
/**
* Форма поддержки живёт только в ленивых графах (#423, #627).
*
* Функция судит ВЛАДЕНИЕ, а не размер: маркер обязан отсутствовать в
* `initialViewFiles` и присутствовать в своём графе; маркер ленивого словаря
* вдобавок обязан отсутствовать в статических графах редактора и онбординга.
* Размер охраняют `assertBundleBudget` и `lowHeadroomWarning` — им для этого не
* нужен чужой номер issue (#429).
*/
export function assertSupportBundleOwnership(
manifest,
root = 'dist',
markers = SUPPORT_LAZY_MARKERS,
) {
const graphText = (paths) => paths
.map((path) => readFileSync(resolve(root, path), 'utf8'))
.join('\n');
const initial = graphText(manifest.initialViewFiles || []);
for (const { text, graph } of markers) {
if (initial.includes(text)) {
throw new Error(`support form copy leaked into initial View graph: ${text}`);
}
if (!graphText(manifest[graph] || []).includes(text)) {
throw new Error(`support form copy missing from ${GRAPH_LABELS[graph] || graph}: ${text}`);
}
if (graph === 'lazyNamespaceLocaleFiles') {
for (const staticGraph of ['lazyEditorFiles', 'lazyOnboardingFiles']) {
if (graphText(manifest[staticGraph] || []).includes(text)) {
throw new Error(`support form copy of a lazy locale leaked into ${GRAPH_LABELS[staticGraph]}: ${text}`);
}
}
}
}
}
/** #627: кто статически несёт английский слой каждого пространства. */
export const NAMESPACE_ENGLISH_CONSUMERS = {
settings: ['lazyEditorFiles', 'lazyOnboardingFiles'],
support: ['lazyEditorFiles'],
topology: ['lazyEditorFiles'],
};
/**
* #627 AC3: владение словарей пространств по СОДЕРЖИМОМУ собранных файлов.
*
* Маркер — самая длинная строка словаря, которой нет ни в одном словаре
* основного каталога (иначе совпадение с первым кадром было бы ложным).
* Английский обязан лежать в статическом графе каждого потребителя и не лежать
* в initial View; ru/de/fr — только в своём ленивом чанке: ни в initial View,
* ни в статических графах редактора и онбординга.
*/
export function namespaceLocaleMarkers(source = fileURLToPath(new URL('../src/i18n/', import.meta.url))) {
const read = (path) => JSON.parse(readFileSync(resolve(source, path), 'utf8'));
const main = ['en', 'ru', 'de', 'fr'].map((code) => JSON.stringify(read(`${code}.json`)));
const printable = (value) => typeof value === 'string' && value.length >= 12
&& !/["'`\\\n$]/.test(value) && !main.some((dictionary) => dictionary.includes(value));
const markers = [];
for (const namespace of Object.keys(NAMESPACE_ENGLISH_CONSUMERS)) {
for (const language of ['en', 'ru', 'de', 'fr']) {
const values = Object.values(read(`${namespace}/${language}.json`)).filter(printable)
.sort((left, right) => right.length - left.length || left.localeCompare(right));
if (!values.length) throw new Error(`${namespace}/${language}: no unique marker string`);
markers.push({ namespace, language, text: values[0] });
}
}
return markers;
}
export function assertNamespaceLocaleOwnership(
manifest,
root = 'dist',
markers = namespaceLocaleMarkers(),
) {
const graphText = (paths) => paths
.map((path) => readFileSync(resolve(root, path), 'utf8'))
.join('\n');
const text = Object.fromEntries(['initialViewFiles', ...Object.keys(GRAPH_LABELS)]
.map((graph) => [graph, graphText(manifest[graph] || [])]));
for (const marker of markers) {
const where = `${marker.namespace}/${marker.language}`;
if (text.initialViewFiles.includes(marker.text)) {
throw new Error(`${where} dictionary leaked into initial View graph: ${marker.text}`);
}
const owners = marker.language === 'en'
? NAMESPACE_ENGLISH_CONSUMERS[marker.namespace]
: ['lazyNamespaceLocaleFiles'];
for (const graph of owners) {
if (!text[graph].includes(marker.text)) {
throw new Error(`${where} dictionary missing from ${GRAPH_LABELS[graph]}: ${marker.text}`);
}
}
if (marker.language !== 'en') {
for (const graph of ['lazyEditorFiles', 'lazyOnboardingFiles']) {
if (text[graph].includes(marker.text)) {
throw new Error(`${where} dictionary is static in ${GRAPH_LABELS[graph]} — it must be its own lazy chunk: ${marker.text}`);
}
}
}
}
}
/**
* Потолок графа: `null`, пока значение внутри полосы.
*
* Возвращает находку, а не бросает: вызывающий решает, что с ней делать, и
* тест может проверить обе стороны, не ловя исключение.
*/
export function initialViewCeilingViolation(bytes, {
ceiling = INITIAL_VIEW_GZIP_CEILING,
band = INITIAL_VIEW_CEILING_BAND,
} = {}) {
if (!Number.isFinite(bytes)) {
return { kind: 'missing', text: 'initial View graph не измерен — потолок проверить нечем' };
}
if (bytes > ceiling) {
return {
kind: 'grew',
over: bytes - ceiling,
text: `initial View graph ${bytes} B gzip выше потолка ${ceiling} B на ${bytes - ceiling} B.`
+ ' Поднимите потолок в этом же коммите, объяснив рост, либо вынесите код в ленивый'
+ ' граф (история: #367 → #474). Молча расти этому графу больше нечем.',
};
}
if (bytes < ceiling - band) {
return {
kind: 'shrank',
under: ceiling - bytes,
text: `initial View graph ${bytes} B gzip ниже потолка ${ceiling} B на ${ceiling - bytes} B`
+ ` — больше полосы ${band} B. Опустите потолок: незафиксированный выигрыш граф`
+ ' отыграет обратно, и это уже происходило (#367, закрыт).',
};
}
return null;
}
/**
* #593: потолки ленивых графов. До этой задачи их размеры только печатались в
* отчёт, и заявление «бюджет ленивого графа защищён» было неправдой: сравнения
* не существовало ни одного. Задача, которая перерисовывает всю библиотеку
* мебели, живёт ровно в этих двух графах — значит она их и закрывает.
*
* Значения сняты с собранного бандла и поставлены с тем же запасом, что у
* стартового графа: полоса 2000 Б и обязательный зазор больше 500 Б с обеих
* сторон, чтобы ни семибайтный шум не красил гейт, ни молчаливый выигрыш не
* оставался незафиксированным.
*/
export const LAZY_FURNITURE_ART_GZIP_CEILING = 17_900;
/**
* #600, промежуточные замеры по сериям (итог ставится в конце):
* - серия 0: 222 900 → 225 900. Набор контролов формы вырос до полного
* референса (`docs/design/600-settings-dialogs/reference/styles.css`, с #681 —
* в истории git): ряд с
* тумблером, компактная строка, плитки, плашка цвета, компас, футер со
* статусом. Рост — CSS-строки листа набора, не код.
* - серия 1: 225 900 → 236 300 (замер 235 560). Форма «Пространство» по §4
* референса: валидация, dirty/Save, четыре карточки с подписями — и ленивый
* словарь `i18n/settings` на четыре локали (51 ключ × 4 ≈ 3,5 КБ gzip), который
* раньше лёг бы в первый кадр. Старая разметка онбординга (300 строк) ушла,
* но она была в другом графе. Строки и словарь — большая часть роста.
* - серия 2: 236 300 → 238 600 (замер 237 929). «Общие настройки» по §5:
* плитки цвета, встроенный режим блока Zigbee, dirty/валидация; девять новых
* строк словаря на четыре локали. Старые `_renderColorRow`, `.sunrow/.suncol`
* и 120-px компас удалены.
* - серия 3: 238 600 → 240 100 (замер 239 391). «Настройки комнаты» по §6:
* кнопка выбора источника с панелью в потоке (`sourcePicker` набора, общая с
* диалогом устройства в серии 4), поля границ комфорта с легендой, dirty и
* валидация комнаты, девять строк словаря на четыре локали.
* `room-temperature-controls.ts` удалён.
* - серия 4: 240 100 → 243 900 (замер 243 173). «Устройство на плане» по §7:
* пять карточек контролами набора — сегменты привязки/роли/свечения/стороны
* бейджа, кнопка выбора с панелью и чекбоксом внутри, чипы, плашки цвета,
* поля с единицами, блок превью; 21 строка словаря на четыре локали
* (четыре «?» с aria); в листе набора — блок с отключением, панель, поле
* иконки, заметки. Старые стили диалога (`.markerlightgroup`, `.srcrow`,
* `.colorrow`, `.ctrlchip*`) пока в eager-листе — снимаются в финале серии.
* - финал: потолок 243 900 остаётся (замер 243 127 после снятия мёртвых
* классов из разметки): итог #600 — 222 900 → 243 900 (+21 000, из них
* ≈ 12 000 — четыре словаря `i18n/settings`, остальное — лист набора и
* разметка четырёх форм с валидацией и dirty).
* - #608: 243 900 → 244 400 (замер 243 597). Атомарный commit числового поля
* добавил общий normalizer и обработчик change в form-kit; новый потолок
* оставляет 803 Б сверху и 1 197 Б до нижней границы полосы.
* - #614: 244 400 → 245 400 (замер 244 356). Строгие raw-draft поля,
* inline-ошибки маркера и единая повторная валидация живут в lazy
* editor-графе. Новый центр оставляет 1 044 Б сверху и 956 Б до
* нижней границы полосы.
* - #642: 245 400 → 246 000 (замер 244 975, база 244 420). Диалог
* «Оптимизировать планы» уехал из рантайма в свой модуль за узкий порт:
* сырых байт стало меньше на 465, а gzip вырос на 555 — обёртка класса и
* адаптер порта из 18 членов уникальны, а сжатие соседних `this.host._t(`
* в рантайме их не покрывает. Старый центр оставлял 425 Б сверху, меньше
* порога шума #593; новый — 1 025 Б сверху и 975 Б до нижней границы.
* - #627: 246 000 → 229 600 (замер 228 621 после ребейза на #642; до
* ребейза — 245 400 → 228 900 при замере 227 869). ru/de/fr словарей
* `settings`, `support`, `topology` ушли из статического графа в девять
* ленивых чанков (по одному на пару «пространство × язык», грузится только
* язык на экране); в графе остался английский слой отката и загрузчик. Новая
* строка любого из трёх словарей теперь стоит здесь ×1, а не ×4. Вклад #642
* поверх — те же ≈ +555 Б gzip модуля диалога и адаптера порта (227 869 →
* 228 621, +752 с шумом сжатия). Центр оставляет 979 Б сверху и 1 021 Б до
* нижней границы полосы.
* - #618: 229 600 → 231 200 (замер 230 208, база dev после #617 — 228 997,
* +1 211 Б). Пакетное «Скрыть/Показать выбранные» каталога устройств:
* модуль `device-inbox-batch.ts` (выбор, панель, одна запись на пакет),
* чистые функции пакета в `device-inbox.ts`, девять строк `device_inbox.*`
* на четыре локали — они в основных словарях, не в ленивых чанках #627.
* Центр оставляет 992 Б сверху и 1 008 Б до нижней границы полосы.
* - #645: 231 200 → 232 900 (замер 231 888, +1 680 Б к базе #618).
* Перетаскивание кнопки комнаты живёт только в ленивом редакторе: отдельный
* контроллер хранит сессионные позиции, ограничивает их геометрией комнаты
* и арбитрирует pointer/pinch/cancel. Центр оставляет 1 012 Б сверху и
* 988 Б до нижней границы полосы.
* - #663: 232 900 → 236 400 (замер 235 443). В ленивый редактор вошёл
* отдельный runtime двух типов лестниц: placement, wall/stair magnet,
* continuous move/resize/rotate, properties dialog и undo/redo. Read-only
* символ и межэтажный переход отделены в eager runtime; первый View не
* загружает эти editor-only ветки. Центр оставляет 957 Б сверху и 1 043 Б
* до нижней границы полосы.
* - #676: 236 400 → 239 000 (замер 237 985, +2 542 Б к замеру #663). Слой
* редактирования лестниц переписан на box-контракт декора: чистые функции
* протяжки, ресайза без зеркала, магнита по сторонам с внешними гранями
* тел, секторов курсора и конвертера диалога живут в `stairs-box.ts`,
* который импортирует только ленивый редактор — eager View получил лишь
* подсказку «Переход на этаж» (`stairs-view.ts`) и три строки словарей.
* Центр оставляет 1 015 Б сверху и 985 Б до нижней границы полосы.
*/
export const LAZY_EDITOR_GZIP_CEILING = 239_000;
export const LAZY_GRAPH_CEILING_BAND = 2_000;
/**
* #627: граф первого запуска (форма «Пространство») впервые под потолком.
* До этой задачи он только считался манифестом и не печатался даже в отчёт —
* и вырос 13 918 → 34 526 Б (+148 %) незаметно: общая форма #600 утащила в
* него form-kit, `hp-color-opacity` и четыре словаря `settings`.
*
* Потолок 28 500 (замер 27 470): ru/de/fr словаря `settings` — ленивые чанки,
* в графе остались английский слой и загрузчик. Центр оставляет 1 030 Б
* сверху и 970 Б до нижней границы полосы. Исходная цель ≤ 20 КБ этим не
* достигается: остаток — form-kit, `space-form`, `hp-help` и стили набора
* (форма #600), не словари; их вынос заметен пользователю (задержка на первом
* «?») и в скоуп #627 не входит.
*/
export const LAZY_ONBOARDING_GZIP_CEILING = 28_500;
/**
* Потолок ленивого графа: `null`, пока значение внутри полосы.
*
* Отдельная функция, а не параметр `initialViewCeilingViolation`: текст отказа
* обязан называть граф, иначе «graph 17 000 B выше потолка» не говорит, какой
* именно граф вырос и куда смотреть.
*/
export function lazyGraphCeilingViolation(bytes, { ceiling, label, band = LAZY_GRAPH_CEILING_BAND } = {}) {
if (!Number.isFinite(bytes)) {
return { kind: 'missing', text: `${label} не измерен — потолок проверить нечем` };
}
if (bytes > ceiling) {
return {
kind: 'grew',
over: bytes - ceiling,
text: `${label} ${bytes} B gzip выше потолка ${ceiling} B на ${bytes - ceiling} B.`
+ ' Ленивый граф грузится не в первом кадре, но он всё равно чей-то трафик:'
+ ' поднимите потолок в этом же коммите, объяснив рост, либо уменьшите содержимое.',
};
}
if (bytes < ceiling - band) {
return {
kind: 'shrank',
under: ceiling - bytes,
text: `${label} ${bytes} B gzip ниже потолка ${ceiling} B на ${ceiling - bytes} B`
+ ` — больше полосы ${band} B. Опустите потолок: незафиксированный выигрыш`
+ ' граф отыграет обратно.',
};
}
return null;
}
/** Тревога о запасе: `null`, пока его хватает или пока долг признан. */
export function lowHeadroomWarning(headroom, {
threshold = LOW_HEADROOM_WARNING_BYTES,
ceiling = INITIAL_VIEW_GZIP_CEILING,
acknowledgedCeiling = LOW_HEADROOM_ACKNOWLEDGED_CEILING,
} = {}) {
if (!Number.isFinite(headroom) || headroom >= threshold) return null;
if (headroom < 0) {
return `бюджет превышен на ${-headroom} Б — гейт уже красный`;
}
// Превышение бюджета не гасится признанием: там уже отказ, а не тревога.
if (Number.isFinite(acknowledgedCeiling) && ceiling <= acknowledgedCeiling) return null;
const stale = Number.isFinite(acknowledgedCeiling)
? ` Признание долга покрывает потолок до ${acknowledgedCeiling} Б, а он уже ${ceiling} Б —`
+ ' граф вырос с тех пор, вопрос вернулся.'
: ' Погасить можно решением владельца: LOW_HEADROOM_ACKNOWLEDGED_CEILING в'
+ ' scripts/bundle-budget.mjs. Живого issue у долга нет: #367 закрыт рекалибровкой,'
+ ' его рычаг сделан в #474 (#499) — следующий рост заводит новый.';
return `запас бюджета ${headroom} Б, меньше порога ${threshold} Б:`
+ ' следующая средняя фича упрётся в стену. Рекалибровка это не лечит —'
+ ' смотрите ленивые графы (история: #367 → #474).' + stale;
}
export function assertBundleBudget(
manifest,
budget = INITIAL_VIEW_GZIP_BUDGET,
panelOnlyBudget = INITIAL_PANEL_ONLY_GZIP_BUDGET,
lazyFurnitureArtCeiling = LAZY_FURNITURE_ART_GZIP_CEILING,
lazyEditorCeiling = LAZY_EDITOR_GZIP_CEILING,
lazyOnboardingCeiling = LAZY_ONBOARDING_GZIP_CEILING,
) {
assertBundleManifest(manifest);
if (!manifest.lazyEditorFiles?.length) {
throw new Error('bundle has no lazy editor graph');
}
if (!manifest.lazyLocaleFiles?.length) {
throw new Error('bundle has no lazy locale graph');
}
if (!manifest.lazyIsometricFiles?.length) {
throw new Error('bundle has no lazy isometric graph');
}
if (!manifest.lazyPdfFiles?.length) {
throw new Error('bundle has no lazy PDF graph');
}
// #474: designer furniture artwork is lazy; a static import anywhere in the
// View graph would pull ~10 KB gzip back into the initial graph silently.
if (!manifest.lazyFurnitureArtFiles?.length) {
throw new Error('bundle has no lazy furniture artwork graph');
}
if (manifest.initialViewFiles.some((path) => manifest.lazyFurnitureArtFiles.includes(path))) {
throw new Error('initial View graph overlaps lazy furniture artwork graph');
}
if (manifest.initialViewFiles.some((path) => manifest.lazyEditorFiles.includes(path))) {
throw new Error('initial View graph overlaps lazy editor graph');
}
if (manifest.initialViewFiles.some((path) => manifest.lazyLocaleFiles.includes(path))) {
throw new Error('initial View graph overlaps lazy locale graph');
}
if (manifest.initialViewFiles.some((path) => manifest.lazyIsometricFiles.includes(path))) {
throw new Error('initial View graph overlaps lazy isometric graph');
}
if (manifest.initialViewFiles.some((path) => manifest.lazyPdfFiles.includes(path))) {
throw new Error('initial View graph overlaps lazy PDF graph');
}
if (manifest.lazyLocaleFiles.some((path) => manifest.lazyEditorFiles.includes(path)
|| manifest.lazyOnboardingFiles?.includes(path))) {
throw new Error('lazy locale graph overlaps an editor graph');
}
// #627 AC3: ru/de/fr трёх словарей пространств — девять отдельных чанков,
// ни один не входит ни в первый кадр, ни в статический граф редакторов.
const namespaceLocales = manifest.lazyNamespaceLocaleFiles;
if (!Array.isArray(namespaceLocales) || namespaceLocales.length !== NAMESPACE_LOCALE_CHUNKS.length) {
throw new Error(`lazy namespace locale graph has ${namespaceLocales?.length ?? 0} files,`
+ ` expected ${NAMESPACE_LOCALE_CHUNKS.length} (settings/support/topology × ru/de/fr)`);
}
for (const [graph, label] of [
['initialViewFiles', 'initial View graph'],
['lazyEditorFiles', 'lazy editor graph'],
['lazyOnboardingFiles', 'lazy onboarding graph'],
]) {
if (namespaceLocales.some((path) => manifest[graph]?.includes(path))) {
throw new Error(`${label} overlaps lazy namespace locale graph`);
}
}
if (manifest.initialViewGzipBytes > budget) {
throw new Error(
`initial View graph ${manifest.initialViewGzipBytes} B gzip exceeds ${budget} B budget`,
);
}
if (manifest.initialPanelOnlyGzipBytes > panelOnlyBudget) {
throw new Error(
`initial panel-only graph ${manifest.initialPanelOnlyGzipBytes} B gzip exceeds`
+ ` ${panelOnlyBudget} B budget`,
);
}
// #593: два ленивых графа впервые становятся гейтом, а не строкой в отчёте.
for (const [bytes, ceiling, label] of [
[manifest.lazyFurnitureArtGzipBytes, lazyFurnitureArtCeiling, 'lazy furniture art graph'],
[manifest.lazyEditorGzipBytes, lazyEditorCeiling, 'lazy editor graph'],
// #627: граф первого запуска — третий гейт, тем же правилом.
[manifest.lazyOnboardingGzipBytes, lazyOnboardingCeiling, 'lazy onboarding graph'],
]) {
const violation = lazyGraphCeilingViolation(bytes, { ceiling, label });
if (violation) throw new Error(violation.text);
}
return {
initialViewGzipBytes: manifest.initialViewGzipBytes,
initialPanelGzipBytes: manifest.initialPanelGzipBytes,
initialPanelOnlyGzipBytes: manifest.initialPanelOnlyGzipBytes,
lazyEditorGzipBytes: manifest.lazyEditorGzipBytes,
lazyOnboardingGzipBytes: manifest.lazyOnboardingGzipBytes,
lazyNamespaceLocaleGzipBytes: manifest.lazyNamespaceLocaleGzipBytes,
lazyLocaleGzipBytes: manifest.lazyLocaleGzipBytes,
lazyIsometricGzipBytes: manifest.lazyIsometricGzipBytes,
lazyFurnitureArtGzipBytes: manifest.lazyFurnitureArtGzipBytes,
lazyPdfGzipBytes: manifest.lazyPdfGzipBytes,
};
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
try {
const manifest = JSON.parse(readFileSync(resolve('dist/houseplan-assets.json'), 'utf8'));
const result = assertBundleBudget(manifest);
assertSupportBundleOwnership(manifest);
assertNamespaceLocaleOwnership(manifest);
const ceiling = initialViewCeilingViolation(result.initialViewGzipBytes);
if (ceiling) throw new Error(ceiling.text);
const headroom = INITIAL_VIEW_GZIP_BUDGET - result.initialViewGzipBytes;
const lines = [
`initial View: ${result.initialViewGzipBytes} B gzip`
+ ` (потолок ${INITIAL_VIEW_GZIP_CEILING} B ±${INITIAL_VIEW_CEILING_BAND},`
+ ` budget ${INITIAL_VIEW_GZIP_BUDGET} B, headroom ${headroom} B)`,
`initial panel: ${result.initialPanelGzipBytes} B gzip`,
`initial panel-only: ${result.initialPanelOnlyGzipBytes} B gzip`
+ ` (budget ${INITIAL_PANEL_ONLY_GZIP_BUDGET} B,`
+ ` headroom ${INITIAL_PANEL_ONLY_GZIP_BUDGET - result.initialPanelOnlyGzipBytes} B)`,
`lazy editor: ${result.lazyEditorGzipBytes} B gzip (потолок ${LAZY_EDITOR_GZIP_CEILING} B ±${LAZY_GRAPH_CEILING_BAND})`,
`lazy onboarding: ${result.lazyOnboardingGzipBytes} B gzip`
+ ` (потолок ${LAZY_ONBOARDING_GZIP_CEILING} B ±${LAZY_GRAPH_CEILING_BAND})`,
`lazy namespace locales: ${result.lazyNamespaceLocaleGzipBytes} B gzip`
+ ` (${NAMESPACE_LOCALE_CHUNKS.length} chunks, по одному грузится на пространство)`,
`lazy furniture art: ${result.lazyFurnitureArtGzipBytes} B gzip`
+ ` (потолок ${LAZY_FURNITURE_ART_GZIP_CEILING} B ±${LAZY_GRAPH_CEILING_BAND})`,
`lazy locale: ${result.lazyLocaleGzipBytes} B gzip`,
`lazy isometric: ${result.lazyIsometricGzipBytes} B gzip`,
`lazy PDF: ${result.lazyPdfGzipBytes} B gzip`,
];
for (const line of lines) console.log(line);
const warning = lowHeadroomWarning(headroom);
if (warning) console.log(`::warning::${warning}`);
// #352: the trend belongs where humans look — the run summary.
if (process.env.GITHUB_STEP_SUMMARY) {
const { appendFileSync } = await import('node:fs');
appendFileSync(process.env.GITHUB_STEP_SUMMARY, [
'### Бюджет бандла',
`| граф | gzip | бюджет | запас |`,
`|---|---|---|---|`,
`| initial View | ${result.initialViewGzipBytes} B | ${INITIAL_VIEW_GZIP_BUDGET} B | ${headroom} B |`,
`| initial panel-only | ${result.initialPanelOnlyGzipBytes} B |`
+ ` ${INITIAL_PANEL_ONLY_GZIP_BUDGET} B |`
+ ` ${INITIAL_PANEL_ONLY_GZIP_BUDGET - result.initialPanelOnlyGzipBytes} B |`,
`| потолок (#438) | ${INITIAL_VIEW_GZIP_CEILING} B | полоса ${INITIAL_VIEW_CEILING_BAND} B |`
+ ` ${INITIAL_VIEW_GZIP_CEILING - result.initialViewGzipBytes} B до потолка |`,
...(warning ? ['', `> ${warning}`] : []),
'',
].join('\n'));
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}