perf(i18n): словари settings/support/topology — ru/de/fr ленивыми чанками; потолок онбординга (#627)

- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на
  пару «пространство × язык»), английский статически как слой отката;
  рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback,
  общий тост отказа), src/i18n/namespace-language.ts.
- Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер
  только после того, как словарь текущего языка осел; гейт хоста учитывает
  словари загруженной на нём поверхности (смена языка на лету — ветка warm).
  Оверлей Zigbee ждёт topology до первой отрисовки.
- bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со
  строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500
  (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки
  ленивости и владения по содержимому.
- Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык
  синхронно, ждут гейт вместо ослабления проверок; пять мутантов.
- monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro
  и export на каждый) и записи манифеста; gzip каждой поверхности при этом
  уменьшился.

Issue: #627
User-Visible: no
This commit is contained in:
Claude
2026-09-24 14:51:18 +03:00
parent 0f234f769d
commit 82a48ee0ce
38 changed files with 1340 additions and 65 deletions
+149 -15
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
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 —
@@ -349,18 +350,32 @@ export const INITIAL_VIEW_CEILING_BAND = 2_000;
*/
export const LOW_HEADROOM_ACKNOWLEDGED_CEILING = null;
/**
* Маркеры формы поддержки и граф, которому каждый принадлежит.
*
* #627: английский — слой отката словаря `support`, он статически в графе
* редактора. Русский с этой задачи — отдельный ленивый чанк `support`-ru: в
* статическом графе редактора его быть НЕ должно, иначе ×4 словарь вернулся.
*/
export const SUPPORT_LAZY_MARKERS = [
'Contact details (email/tg/WhatsApp), optional.',
'Контакт для связи (email/tg/WhatsApp), необязательно.',
{ 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).
* Форма поддержки живёт только в ленивых графах (#423, #627).
*
* Функция судит ВЛАДЕНИЕ, а не размер: маркеры формы обязаны отсутствовать в
* `initialViewFiles` и присутствовать в `lazyEditorFiles`. Размер охраняют
* `assertBundleBudget` и `lowHeadroomWarning` — им для этого не нужен чужой
* номер issue (#429).
* Функция судит ВЛАДЕНИЕ, а не размер: маркер обязан отсутствовать в
* `initialViewFiles` и присутствовать в своём графе; маркер ленивого словаря
* вдобавок обязан отсутствовать в статических графах редактора и онбординга.
* Размер охраняют `assertBundleBudget` и `lowHeadroomWarning` — им для этого не
* нужен чужой номер issue (#429).
*/
export function assertSupportBundleOwnership(
manifest,
@@ -371,13 +386,85 @@ export function assertSupportBundleOwnership(
.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}`);
for (const { text, graph } of markers) {
if (initial.includes(text)) {
throw new Error(`support form copy leaked into initial View graph: ${text}`);
}
if (!editor.includes(marker)) {
throw new Error(`support form copy missing from lazy editor graph: ${marker}`);
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}`);
}
}
}
}
}
@@ -472,10 +559,31 @@ export const LAZY_FURNITURE_ART_GZIP_CEILING = 17_900;
* адаптер порта из 18 членов уникальны, а сжатие соседних `this.host._t(`
* в рантайме их не покрывает. Старый центр оставлял 425 Б сверху, меньше
* порога шума #593; новый — 1 025 Б сверху и 975 Б до нижней границы.
* - #627: 246 000 → 229 500 (замер XXX). ru/de/fr словарей `settings`,
* `support`, `topology` ушли из статического графа в девять ленивых чанков
* (по одному на пару «пространство × язык», грузится только язык на экране);
* в графе остался английский слой отката и загрузчик. Новая строка любого из
* трёх словарей теперь стоит здесь ×1, а не ×4. Центр оставляет XXX Б
* сверху и XXX Б до нижней границы полосы.
*/
export const LAZY_EDITOR_GZIP_CEILING = 246_000;
export const LAZY_EDITOR_GZIP_CEILING = 229_500;
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`, пока значение внутри полосы.
*
@@ -537,6 +645,7 @@ export function assertBundleBudget(
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) {
@@ -575,6 +684,22 @@ export function assertBundleBudget(
|| 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`,
@@ -590,6 +715,8 @@ export function assertBundleBudget(
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);
@@ -599,6 +726,8 @@ export function assertBundleBudget(
initialPanelGzipBytes: manifest.initialPanelGzipBytes,
initialPanelOnlyGzipBytes: manifest.initialPanelOnlyGzipBytes,
lazyEditorGzipBytes: manifest.lazyEditorGzipBytes,
lazyOnboardingGzipBytes: manifest.lazyOnboardingGzipBytes,
lazyNamespaceLocaleGzipBytes: manifest.lazyNamespaceLocaleGzipBytes,
lazyLocaleGzipBytes: manifest.lazyLocaleGzipBytes,
lazyIsometricGzipBytes: manifest.lazyIsometricGzipBytes,
lazyFurnitureArtGzipBytes: manifest.lazyFurnitureArtGzipBytes,
@@ -611,6 +740,7 @@ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
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;
@@ -623,6 +753,10 @@ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
+ ` (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`,
+48
View File
@@ -12,6 +12,22 @@ const FR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FR_RETRY_ASSET__';
const FURNITURE_ART_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FURNITURE_ART_RETRY_ASSET__';
const PDF_RETRY_ASSET_TOKEN = '__HOUSEPLAN_PDF_RETRY_ASSET__';
/**
* #627: ru/de/fr of the three lazy dictionary namespaces are one chunk per
* namespace × language. Their loader modules are the recognition marker for
* the manifest graph and the target of each second-attempt retry token.
*/
export const NAMESPACE_LOCALE_CHUNKS = ['settings', 'support', 'topology']
.flatMap((namespace) => ['ru', 'de', 'fr'].map((language) => ({
namespace,
language,
module: `/src/i18n/${namespace}/${namespace}-${language}.ts`,
token: `__HOUSEPLAN_${namespace.toUpperCase()}_${language.toUpperCase()}_RETRY_ASSET__`,
})));
const isNamespaceLocaleModule = (id) => NAMESPACE_LOCALE_CHUNKS
.some((chunk) => id.endsWith(chunk.module));
export const CARD_ENTRY_FILE = 'houseplan-card.js';
export const PANEL_ENTRY_FILE = 'houseplan-panel.js';
@@ -84,6 +100,8 @@ export function buildBundleManifest(bundle, fingerprint) {
const modules = Object.keys(chunk.modules || {}).map((id) => id.replaceAll('\\', '/'));
const role = modules.some((id) => id.endsWith('/src/i18n/de.ts') || id.endsWith('/src/i18n/fr.ts'))
? 'locale'
: modules.some(isNamespaceLocaleModule)
? 'namespace-locale'
: modules.some((id) => id.endsWith('/src/houseplan-onboarding-runtime.ts'))
? 'onboarding'
: modules.some((id) => id.endsWith('/src/houseplan-editor-runtime.ts'))
@@ -135,6 +153,10 @@ export function buildBundleManifest(bundle, fingerprint) {
const furnitureArtRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'furniture-art');
const pdfRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'pdf'
|| path.includes('pdf-export-'));
// #627: namespace dictionaries are dynamic imports of LAZY chunks, never of
// the initial graph, so they are found by their module, not as a root.
const namespaceLocaleRoots = files.filter((file) => file._role === 'namespace-locale')
.map((file) => file.path);
const graphFrom = (roots) => {
const graph = new Set();
for (const root of roots) {
@@ -148,6 +170,7 @@ export function buildBundleManifest(bundle, fingerprint) {
const lazyIsometric = graphFrom(isometricRoots);
const lazyFurnitureArt = graphFrom(furnitureArtRoots);
const lazyPdf = graphFrom(pdfRoots);
const lazyNamespaceLocale = graphFrom(namespaceLocaleRoots);
const sum = (paths) => [...paths]
.reduce((total, path) => total + (byPath.get(path)?.gzipBytes || 0), 0);
return {
@@ -175,6 +198,8 @@ export function buildBundleManifest(bundle, fingerprint) {
lazyFurnitureArtGzipBytes: sum(lazyFurnitureArt),
lazyPdfFiles: [...lazyPdf].sort(),
lazyPdfGzipBytes: sum(lazyPdf),
lazyNamespaceLocaleFiles: [...lazyNamespaceLocale].sort(),
lazyNamespaceLocaleGzipBytes: sum(lazyNamespaceLocale),
files: files.map(({ _role, ...file }) => file),
};
}
@@ -239,6 +264,14 @@ export function editorRuntimeRetryUrlPlugin() {
if (!french) throw new Error('French locale chunk was not emitted');
if (!furnitureArt) throw new Error('furniture artwork chunk was not emitted');
if (!pdf) throw new Error('PDF export runtime chunk was not emitted');
const namespaceLocales = NAMESPACE_LOCALE_CHUNKS.map((entry) => {
const chunk = chunks.find((candidate) => Object.keys(candidate.modules)
.some((id) => id.replaceAll('\\', '/').endsWith(entry.module)));
if (!chunk) {
throw new Error(`${entry.namespace} ${entry.language} locale chunk was not emitted`);
}
return { ...entry, chunk, replacements: 0 };
});
let furnitureArtReplacements = 0;
let editorReplacements = 0;
let onboardingReplacements = 0;
@@ -289,6 +322,13 @@ export function editorRuntimeRetryUrlPlugin() {
pdfReplacements += chunk.code.split(PDF_RETRY_ASSET_TOKEN).length - 1;
chunk.code = chunk.code.replaceAll(PDF_RETRY_ASSET_TOKEN, asset);
}
for (const entry of namespaceLocales) {
if (!chunk.code.includes(entry.token)) continue;
let asset = posix.relative(posix.dirname(chunk.fileName), entry.chunk.fileName);
if (!asset.startsWith('.')) asset = `./${asset}`;
entry.replacements += chunk.code.split(entry.token).length - 1;
chunk.code = chunk.code.replaceAll(entry.token, asset);
}
}
if (editorReplacements !== 1 || onboardingReplacements !== 1 || isometricReplacements !== 1
|| germanReplacements !== 1 || frenchReplacements !== 1 || furnitureArtReplacements !== 1
@@ -296,6 +336,14 @@ export function editorRuntimeRetryUrlPlugin() {
throw new Error('lazy retry URL placeholder counts are '
+ `${editorReplacements}/${onboardingReplacements}/${isometricReplacements}/${germanReplacements}/${frenchReplacements}/${furnitureArtReplacements}/${pdfReplacements}, expected 1/1/1/1/1/1/1`);
}
// #627: the same strict rule for every namespace × language token —
// exactly one second-attempt URL each, never zero and never two.
const namespaceMismatch = namespaceLocales.filter((entry) => entry.replacements !== 1);
if (namespaceMismatch.length) {
throw new Error('namespace locale retry URL placeholder counts are '
+ namespaceMismatch.map((entry) => `${entry.namespace}-${entry.language}=${entry.replacements}`).join(', ')
+ ', expected exactly 1 each');
}
},
};
}
+76 -5
View File
@@ -2478,6 +2478,63 @@ const MUTANT_DEFINITIONS = [
replace: " : await import(/* @vite-ignore */ new URL(`${FRENCH_RETRY_ASSET}?retry`, import.meta.url).href);\n void module.dictionary;\n return { dictionary: en, fingerprint: module.fingerprint };",
}],
},
{
id: 'i18n-namespace-eager-locale',
guard: 'node --test test/i18n-lazy-namespaces.test.mjs',
because: 'a static import of one ru/de/fr dictionary of a namespace puts all of it back into '
+ 'the static editor and onboarding graphs — every new string costs ×4 again, the exact '
+ 'growth #627 removes (13.9 → 34.5 KB gzip of first-run onboarding)',
patches: [{
file: 'src/i18n/settings.ts',
find: "import en from './settings/en.json' with { type: 'json' };\n",
replace: "import en from './settings/en.json' with { type: 'json' };\n"
+ "import ru from './settings/ru.json' with { type: 'json' };\n",
}, {
file: 'src/i18n/settings.ts',
find: ' SETTINGS_LANGUAGE_RUNTIME.dictionary(lang)?.[key] ?? en[key];',
replace: " (lang === 'ru' ? ru[key] : SETTINGS_LANGUAGE_RUNTIME.dictionary(lang)?.[key]) ?? en[key];",
}],
},
{
id: 'locale-gate-ignores-namespaces',
guard: 'node --test --test-name-pattern="#627 AC5" test/i18n-runtime.test.mjs',
because: 'a host gate that asks only the main catalog paints the open settings dialog in '
+ 'English for one frame after a live language switch — the flash #348 forbids, now for '
+ 'the lazy namespace dictionaries (#627 AC5)',
patches: [{
file: 'src/i18n/namespace-language.ts',
find: ' state: (code) => {\n'
+ ' const state = primary.state(code);\n'
+ " return state !== 'pending' && namespaces.some((runtime) => runtime.state(code) === 'pending')\n"
+ " ? 'pending' : state;\n"
+ ' },',
replace: ' state: (code) => primary.state(code),',
}],
},
{
id: 'onboarding-loader-skips-namespace-ensure',
guard: 'node demo/smoke_lazy_admin_locale.mjs',
because: 'an onboarding runtime installed before its settings dictionary settled forces the '
+ 'host into the warm branch: the first-run form waits behind an inert, busy card instead '
+ 'of the existing runtime indicator — a new interface state the contract rules out (#627 AC4)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' if (module.ONBOARDING_RUNTIME_FINGERPRINT === ENTRY_BUILD_FINGERPRINT) await module.ONBOARDING_LANGUAGE_RUNTIME.ensure(langOf(this.hass, this._config?.language)); // #627\n',
replace: '',
}],
},
{
id: 'namespace-loader-returns-english',
guard: 'node demo/smoke_lazy_admin_locale.mjs',
because: 'a German administrator silently reading the English settings dialog is invisible '
+ 'to parity units — only the bundle smoke proves the de loader commits the de chunk (#627 AC6)',
patches: [{
file: 'src/i18n/settings.ts',
find: " : import(/* @vite-ignore */ retryUrl('__HOUSEPLAN_SETTINGS_DE_RETRY_ASSET__'))),\n",
replace: " : import(/* @vite-ignore */ retryUrl('__HOUSEPLAN_SETTINGS_DE_RETRY_ASSET__')))\n"
+ ' .then((module) => ({ dictionary: en, fingerprint: module.fingerprint })),\n',
}],
},
{
id: 'static-glow-light-cache-spread',
guard: 'node --test test/space-render-caches.test.mjs',
@@ -2668,8 +2725,9 @@ const MUTANT_DEFINITIONS = [
find: " /** Synchronize host/runtime language state and return the current branch. */\n"
+ ' private _syncDangerConfirmLocaleGate(): LanguageRenderGate {\n'
+ " if (!this._config || !this.hass) return 'ready';\n"
+ ' return languageRenderGate(\n'
+ ' this, LANGUAGE_RUNTIME, langOf(this.hass, this._config.language),\n'
+ ' return languageRenderGate( // #627: + dictionaries of the surfaces loaded on THIS host\n'
+ ' this, this._editorRuntime?.languageRuntime ?? this._onboardingRuntime?.languageRuntime ?? LANGUAGE_RUNTIME,\n'
+ ' langOf(this.hass, this._config.language),\n'
+ ' );\n'
+ ' }',
replace: " private _dangerConfirmLocaleGate: LanguageRenderGate = 'ready';\n"
@@ -2681,7 +2739,8 @@ const MUTANT_DEFINITIONS = [
find: ' const localeGate = this._syncDangerConfirmLocaleGate();\n'
+ " if (localeGate === 'cold') return languageLoadingTemplate();",
replace: ' const localeGate = languageRenderGate(\n'
+ ' this, LANGUAGE_RUNTIME, langOf(this.hass, this._config.language),\n'
+ ' this, this._editorRuntime?.languageRuntime ?? this._onboardingRuntime?.languageRuntime ?? LANGUAGE_RUNTIME,\n'
+ ' langOf(this.hass, this._config.language),\n'
+ ' );\n'
+ ' this._dangerConfirmLocaleGate = localeGate;\n'
+ " if (localeGate === 'cold') return languageLoadingTemplate();",
@@ -8133,8 +8192,8 @@ const MUTANT_DEFINITIONS = [
+ 'dictionary, not of the resolved string (#459 AC2)',
patches: [{
file: 'src/i18n/topology.ts',
find: ' const value = DICTIONARIES[lang]?.[key] ?? en[key];',
replace: ' const value = DICTIONARIES[lang]?.[key] ?? en[key] ?? key;',
find: ' const value = TOPOLOGY_LANGUAGE_RUNTIME.dictionary(lang)?.[key] ?? en[key];',
replace: ' const value = TOPOLOGY_LANGUAGE_RUNTIME.dictionary(lang)?.[key] ?? en[key] ?? key;',
}],
},
{
@@ -9524,6 +9583,18 @@ const MUTANT_DEFINITIONS = [
+ ' if (Number.isFinite(bytes)) return null; // mutant',
}],
},
{
id: 'bundle-budget-onboarding-ceiling-dropped',
guard: 'node --test --test-name-pattern="#627 AC1" test/bundle-assets.test.mjs',
because: '#627: the first-run graph grew 13.9 → 34.5 KB gzip unnoticed because it was only '
+ 'counted, never compared. Dropping it from the ceiling loop returns exactly that state — '
+ 'a declared ceiling nobody enforces',
patches: [{
file: 'scripts/bundle-budget.mjs',
find: " [manifest.lazyOnboardingGzipBytes, lazyOnboardingCeiling, 'lazy onboarding graph'],\n",
replace: '',
}],
},
{
id: 'furniture-art-eager-import',
guard: 'npm run build && node scripts/bundle-budget.mjs',
+11
View File
@@ -283,6 +283,17 @@ export const SMOKE_LINKS = [
+ 'requests and four root render gates: neutral cold frame, atomic German commit, page-cache '
+ 'reuse and the bounded retry-to-English path cannot be proven from pure registry tests (#348)',
},
{
symbols: [
'namespaceLanguageRuntime', 'composeLanguageRuntimes', 'surfaceLanguageRuntime',
'SETTINGS_LANGUAGE_RUNTIME', 'SUPPORT_LANGUAGE_RUNTIME', 'TOPOLOGY_LANGUAGE_RUNTIME',
'EDITOR_LANGUAGE_RUNTIME', 'ONBOARDING_LANGUAGE_RUNTIME', 'settingsT', 'supportT', 'topologyT',
],
smokes: ['smoke_lazy_admin_locale.mjs', 'smoke_dialog_segments_i18n.mjs'],
because: 'the ru/de/fr namespace dictionaries are separate lazy chunks: only the production '
+ 'bundle shows the per-frame dialog text on a cold open, the held frame on a live switch, '
+ 'the bounded retry-to-English path and the exact chunk requests of each surface (#627)',
},
{
symbols: [
'probeBackdrop', 'classifyPlanFile', 'downscaleBackdrop', 'renderBackdropGuard',