Files
houseplan-card/scripts/bundle-budget.mjs
T
Claude 0935a4d099 ci: рекалибровать бюджет initial view и предупреждать до стены, а не после
Запас ушёл с 26 КБ до 8.3 КБ за сутки. По документам код-ревью видно, что это не
диффузное расползание, а один шаг плюс обычная работа:

  #317 256127 · #318 256091 · #341 256046 · #354 257212 · #159 256828
  #357 271143  <- +14 КБ за один заход
  #20 271455 · #361 272848 · #359 272469 · #360 273697  <- текущий факт

Шаг на #357 — plan-art мебели: 44 top-view символа в eager-графе, которые платит
каждый план, включая планы без единого предмета мебели.

Потолок 282000 -> 300000. Правило #352 сохранено: 273697 x 1.10 = 301067, то
есть 300000 остаётся внутри надбавки ~10% над измеренным фактом. Запас
возвращается к 26.3 КБ — примерно к тому, что было до #357.

Запись честная и в комментарии сказана прямо: рекалибровка ничего не ускоряет и
ничего не чинит. Она фиксирует новую норму и возвращает гейту способность красить
того, кто вырастил бандл, а не того, кто пушнул последним. Настоящий рычаг —
ленивый граф, варианты 1 и 2 из #367.

Добавлено предупреждение: пока запас меньше 15000 Б, гейт печатает ::warning:: и
строку в summary. Прежняя редакция полагалась на то, что человек заметит тренд в
выводе; за сутки его не заметил никто, потому что каждая отдельная строка
выглядела нормально. Текст предупреждения называет лечение — иначе следующий
читатель поднимет потолок ещё раз и назовёт это решением.

Тест закрепляет обе стороны: надбавка не больше 10% и не меньше 5% (меньше —
возврат лотереи «красит последний коммит»), тревога срабатывает строго ниже
порога, превышение описывается как превышение. Три мутанта проверены руками,
один добавлен в реестр.

Issue: #367
User-Visible: no
2026-08-29 12:32:01 +03:00

120 lines
6.4 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 { pathToFileURL } from 'node:url';
// #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%.
export const INITIAL_VIEW_GZIP_BUDGET = 300_000;
/**
* Порог, ниже которого запас перестаёт быть запасом (#367).
*
* Прежняя редакция полагалась на то, что человек заметит тренд в выводе. За
* сутки запас ушёл с 26 КБ до 8.3 КБ, и не заметил никто — потому что каждая
* отдельная строка выглядела нормально. Предупреждение срабатывает за две
* средние фичи до стены, а не после неё.
*/
export const LOW_HEADROOM_WARNING_BYTES = 15_000;
/** Тревога о запасе: `null`, пока его хватает. */
export function lowHeadroomWarning(headroom, threshold = LOW_HEADROOM_WARNING_BYTES) {
if (!Number.isFinite(headroom) || headroom >= threshold) return null;
if (headroom < 0) {
return `бюджет превышен на ${-headroom} Б — гейт уже красный`;
}
return `запас бюджета ${headroom} Б, меньше порога ${threshold} Б:`
+ ' следующая средняя фича упрётся в стену. Рекалибровка это не лечит —'
+ ' смотрите ленивые графы (#367)';
}
export function assertBundleBudget(manifest, budget = INITIAL_VIEW_GZIP_BUDGET) {
if (manifest?.schema !== 1 || !Array.isArray(manifest.files)) {
throw new Error('invalid houseplan-assets.json');
}
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.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.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`,
);
}
return {
initialViewGzipBytes: manifest.initialViewGzipBytes,
lazyEditorGzipBytes: manifest.lazyEditorGzipBytes,
lazyLocaleGzipBytes: manifest.lazyLocaleGzipBytes,
};
}
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);
const headroom = INITIAL_VIEW_GZIP_BUDGET - result.initialViewGzipBytes;
const lines = [
`initial View: ${result.initialViewGzipBytes} B gzip`
+ ` (budget ${INITIAL_VIEW_GZIP_BUDGET} B, headroom ${headroom} B)`,
`lazy editor: ${result.lazyEditorGzipBytes} B gzip`,
`lazy locale: ${result.lazyLocaleGzipBytes} 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 |`,
...(warning ? ['', `> ${warning}`] : []),
'',
].join('\n'));
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}