mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Пятый вариант «Отображение»: маркер показывает выбранное значение и при этом
никогда не меняет цвет — ни по состоянию, ни по тревоге, ни по недоступности,
ни под цвет RGB-света, ни по активности.
Четыре прежних режима задавали одним выбором две независимые вещи: что
нарисовано внутри маркера и красится ли он состоянием. Поэтому вместо
сравнения с одним токеном появились два производных предиката рядом с
`normalizeDeviceDisplay` — `displayWantsValue` и `displayIsNeutral`, — и их
спрашивают политика, слой пульсации, внешний бейдж, редактор и три ветки
живого слоя пылесоса.
Две ловушки, из-за которых режим не сводится к одной строке в словаре:
- быстрый путь «статичному маркеру источники не нужны» (`sourceDetails: false`)
— это основной путь рендера плана, карточки пространства и PDF. Он оставлен
только режиму без значения: иначе число пропало бы именно на плане и
осталось в предпросмотре редактора;
- три ветки живого пылесоса сравнивают режим строкой и не читают политику,
поэтому зелёная политика их не гарантирует. Свидетель держит две
конфигурации карты: сопоставленная доказывает puck и след, несопоставленная
— бейдж маршрута (при совпадающей калибровке маршрут `ready`, и бейджа не
было бы ни в одном режиме). Бейдж проверяется при наполненном буфере
позиций, иначе его отсутствие объяснялось бы первой веткой.
Потолок initial View перецентрирован 291_700 → 292_400 без изменения общего
бюджета: измеренный факт 291 346 Б оставлял под прежним центром 354 Б —
внутри шумовой полосы метрики.
Эталон: в `device-icon-state-table-{light,dark}` включённая RGB-лампа
переведена в новый режим. Кадры обязаны разойтись; приёмка — отдельным
коммитом класса D с полного линуксового артефакта Validate.
Issue: #588
User-Visible: yes
510 lines
37 KiB
JavaScript
510 lines
37 KiB
JavaScript
#!/usr/bin/env node
|
||
import { readFileSync } from 'node:fs';
|
||
import { resolve } from 'node:path';
|
||
import { pathToFileURL } from 'node:url';
|
||
import { assertBundleManifest } from './bundle-tree.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 Б до нижней границы полосы.
|
||
*/
|
||
export const INITIAL_VIEW_GZIP_CEILING = 292_400;
|
||
export const INITIAL_VIEW_CEILING_BAND = 2_000;
|
||
|
||
/**
|
||
* До какого потолка предупреждение о запасе погашено владельцем.
|
||
*
|
||
* `null` — не погашено, и это текущее состояние: запас 10 885 Б при пороге
|
||
* 15 000, долг живёт в #367. Предупреждение, которое нельзя погасить, читается
|
||
* как выключенное, поэтому его можно погасить — одной строкой здесь, со
|
||
* ссылкой на решение. Признание привязано к ЗНАЧЕНИЮ потолка: как только
|
||
* потолок поднимут, оно перестаёт покрывать, и вопрос возвращается ровно
|
||
* тогда, когда граф снова вырос.
|
||
*/
|
||
export const LOW_HEADROOM_ACKNOWLEDGED_CEILING = null;
|
||
|
||
export const SUPPORT_LAZY_MARKERS = [
|
||
'Contact details (email/tg/WhatsApp), optional.',
|
||
'Контакт для связи (email/tg/WhatsApp), необязательно.',
|
||
];
|
||
|
||
/**
|
||
* Форма поддержки живёт только в ленивом графе редактора (#423).
|
||
*
|
||
* Функция судит ВЛАДЕНИЕ, а не размер: маркеры формы обязаны отсутствовать в
|
||
* `initialViewFiles` и присутствовать в `lazyEditorFiles`. Размер охраняют
|
||
* `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 || []);
|
||
const editor = graphText(manifest.lazyEditorFiles || []);
|
||
for (const marker of markers) {
|
||
if (initial.includes(marker)) {
|
||
throw new Error(`support form copy leaked into initial View graph: ${marker}`);
|
||
}
|
||
if (!editor.includes(marker)) {
|
||
throw new Error(`support form copy missing from lazy editor graph: ${marker}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Потолок графа: `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;
|
||
}
|
||
|
||
/** Тревога о запасе: `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,
|
||
) {
|
||
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');
|
||
}
|
||
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`,
|
||
);
|
||
}
|
||
return {
|
||
initialViewGzipBytes: manifest.initialViewGzipBytes,
|
||
initialPanelGzipBytes: manifest.initialPanelGzipBytes,
|
||
initialPanelOnlyGzipBytes: manifest.initialPanelOnlyGzipBytes,
|
||
lazyEditorGzipBytes: manifest.lazyEditorGzipBytes,
|
||
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);
|
||
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 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);
|
||
}
|
||
}
|