From 0935a4d09909e0f2b44729fedd6c463dea4646b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 12:32:01 +0300 Subject: [PATCH] =?UTF-8?q?ci:=20=D1=80=D0=B5=D0=BA=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B1=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B1=D1=8E=D0=B4?= =?UTF-8?q?=D0=B6=D0=B5=D1=82=20initial=20view=20=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D1=83=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=BE=20=D1=81=D1=82=D0=B5=D0=BD=D1=8B,=20?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Запас ушёл с 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 --- docs/TESTING.md | 8 +++++-- scripts/bundle-budget.mjs | 47 +++++++++++++++++++++++++++++++++++-- scripts/mutation-gate.mjs | 12 ++++++++++ test/bundle-assets.test.mjs | 42 ++++++++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index e0d09274..3e979dd3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -3403,8 +3403,12 @@ require hands on real hardware — they remain for the human pass. `getConfigElement()` still returns `houseplan-card-editor` [auto: `smoke_lazy_editor_chunk`]. - [ ] `bundle:budget` follows transitive static imports and keeps initial View - at or below 282000 B gzip (#352: ~10% headroom over the calibrated fact, - printed with the trend on every run). Bundle sync, demo freshness, CI artifacts and + at or below 300000 B gzip (#352: ~10% headroom over the calibrated fact, + printed with the trend on every run; recalibrated in #367 from 282000 after + the fact moved 255 993 → 273 697 B, of which +14 KB came in a single step — + furniture plan-art in the eager graph. Recalibration records the growth, it + does not fix it: the lever is a lazy graph. A warning fires while headroom + is still 15000 B, two average features before the wall). Bundle sync, demo freshness, CI artifacts and release zip validation fail when any manifest-listed asset is missing or its SHA-256 differs [auto: `bundle-assets.test`, `bundle-freshness.test`, release-contract tests]. diff --git a/scripts/bundle-budget.mjs b/scripts/bundle-budget.mjs index e7523f29..524835e8 100644 --- a/scripts/bundle-budget.mjs +++ b/scripts/bundle-budget.mjs @@ -11,8 +11,48 @@ import { pathToFileURL } from 'node:url'; // 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. -// Measured fact at calibration (v1.69.0-beta.1): 255 993 B gzip. -export const INITIAL_VIEW_GZIP_BUDGET = 282_000; +// +// #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)) { @@ -58,6 +98,8 @@ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { `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'); @@ -66,6 +108,7 @@ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { `| граф | gzip | бюджет | запас |`, `|---|---|---|---|`, `| initial View | ${result.initialViewGzipBytes} B | ${INITIAL_VIEW_GZIP_BUDGET} B | ${headroom} B |`, + ...(warning ? ['', `> ${warning}`] : []), '', ].join('\n')); } diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index 10ada033..7ace708c 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -1308,6 +1308,18 @@ const MUTANT_DEFINITIONS = [ replace: ' if (false) {', }], }, + { + id: 'budget-warning-never-fires', + guard: 'node --test --test-name-pattern="срабатывает до стены" test/bundle-assets.test.mjs', + because: 'прежняя редакция полагалась на то, что человек заметит тренд в выводе: за сутки ' + + 'запас ушёл с 26 КБ до 8.3 КБ, и не заметил никто — каждая отдельная строка выглядела ' + + 'нормально (#367)', + patches: [{ + file: 'scripts/bundle-budget.mjs', + find: ' if (!Number.isFinite(headroom) || headroom >= threshold) return null;', + replace: ' if (true) return null;', + }], + }, { id: 'review-doc-guard-matches-by-substring', guard: 'node --test --test-name-pattern="соседний каталог" test/review-doc-guard.test.mjs', diff --git a/test/bundle-assets.test.mjs b/test/bundle-assets.test.mjs index 29d338cf..c4cc91a2 100644 --- a/test/bundle-assets.test.mjs +++ b/test/bundle-assets.test.mjs @@ -7,7 +7,9 @@ import test from 'node:test'; import { buildBundleManifest, buildFingerprintPlugin, editorRuntimeRetryUrlPlugin, } from '../scripts/bundle-manifest.mjs'; -import { assertBundleBudget } from '../scripts/bundle-budget.mjs'; +import { + INITIAL_VIEW_GZIP_BUDGET, LOW_HEADROOM_WARNING_BYTES, assertBundleBudget, lowHeadroomWarning, +} from '../scripts/bundle-budget.mjs'; import { compareBundleTrees, sha256Bytes, verifyBundleTree } from '../scripts/bundle-tree.mjs'; import { minifyCssText, minifyStaticCssTemplates, @@ -196,3 +198,41 @@ test('bundle tree verification rejects orphan chunks (#353 AC4)', async () => { /orphan bundle asset: houseplan-assets\/junk-old\.js/, ); }); + +// #367. Рекалибровка после того, как запас ушёл с 26 КБ до 8.3 КБ за сутки. +// Она ничего не ускоряет — она фиксирует новую норму и возвращает рабочий +// запас, чтобы гейт красил того, кто вырастил бандл, а не того, кто пушнул +// последним. Настоящий рычаг — ленивые графы, и о нём напоминает сам текст +// предупреждения. + +test('потолок держится внутри правила ~10% над измеренным фактом (#352, #367)', () => { + // Факт на момент рекалибровки — 273 697 Б gzip (dev @ 360). + const fact = 273_697; + assert.ok(INITIAL_VIEW_GZIP_BUDGET > fact, 'потолок ниже факта сделал бы гейт вечно красным'); + const allowance = (INITIAL_VIEW_GZIP_BUDGET - fact) / fact; + assert.ok(allowance <= 0.10 + 1e-9, `надбавка ${(allowance * 100).toFixed(1)}% больше правила 10%`); + assert.ok(allowance > 0.05, 'надбавка меньше 5% возвращает лотерею «красит последний коммит»'); +}); + +test('тревога о запасе срабатывает до стены, а не после (#367)', () => { + // Порог выбран так, чтобы предупреждение приходило примерно за две средние + // фичи до потолка: обычный прирост — около килобайта за задачу. + assert.equal(lowHeadroomWarning(LOW_HEADROOM_WARNING_BYTES), null); + assert.equal(lowHeadroomWarning(LOW_HEADROOM_WARNING_BYTES + 1), null); + const warning = lowHeadroomWarning(LOW_HEADROOM_WARNING_BYTES - 1); + assert.match(warning, /меньше порога/); + // Предупреждение обязано называть лечение: без этого следующий читатель + // поднимет потолок ещё раз и назовёт это решением. + assert.match(warning, /Рекалибровка это не лечит/); + assert.match(warning, /#367/); +}); + +test('превышенный бюджет описывается как превышение, а не как малый запас (#367)', () => { + assert.match(lowHeadroomWarning(-42), /превышен на 42 Б/); + assert.equal(lowHeadroomWarning(Number.NaN), null); +}); + +test('запас на момент рекалибровки выше порога тревоги (#367)', () => { + // Иначе рекалибровка была бы бессмысленной: гейт сразу же начал бы кричать. + assert.ok(INITIAL_VIEW_GZIP_BUDGET - 273_697 > LOW_HEADROOM_WARNING_BYTES); +});