Догнать main после #537
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 2m46s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 2m55s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 2m53s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 2m57s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 2m41s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 2m45s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 2m49s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 2m47s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 20s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 1m23s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 47s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 26m7s

Инвариант графа панели из #535 предъявлялся чужому манифесту, и кандидат
отказывался грузить любую базу старше себя: девять профилей Full Performance
падали на одном шаге, гейт стабильного релиза не выдал `houseplan-card.js`
для v1.75.0. #537 развёл вопрос загрузчика и топологию собственной сборки.

Issue: #537
User-Visible: no
This commit is contained in:
Codex
2026-09-12 12:40:19 +03:00
4 changed files with 73 additions and 18 deletions
+4 -1
View File
@@ -16,7 +16,7 @@ import {
import { dirname, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
assertBundleManifest, orderedBundlePayload, verifyBundleTree,
assertBundleManifest, assertOwnBundleTopology, orderedBundlePayload, verifyBundleTree,
} from './bundle-tree.mjs';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -38,6 +38,9 @@ const parseManifest = (path) => {
return parsed;
};
const sourceManifest = assertBundleManifest(parseManifest(manifestPath), manifestPath);
// #537: this script materializes OUR dist, so the topology of the current
// build is judged here and nowhere in the shared loader-side validator.
assertOwnBundleTopology(sourceManifest, manifestPath);
const managedFiles = [MANIFEST_NAME, ...sourceManifest.files.map((file) => file.path)];
const sha256 = (path) => createHash('sha256').update(readFileSync(path)).digest('hex');
const contained = (root, name) => {
+30 -7
View File
@@ -35,6 +35,34 @@ const assertUniqueGraph = (manifest, field, listed, label) => {
return graph;
};
/**
* Topology of the CURRENT build, checked only against our own tree (#537).
*
* `assertBundleManifest` above answers «can this manifest be loaded»: paths
* exist, nothing is duplicated, graphs reference listed assets, sizes add up.
* That question is also asked about FOREIGN trees — the performance harness
* runs the candidate's benchmark against a baseline checkout, so the
* candidate's validator reads a manifest built by an older commit.
*
* «Is the panel wired the way we decided today» is a different question, and
* #535 proved the cost of confusing them: the rule «the panel graph does not
* contain the card facade» is true of every build since #535 and false of
* every build before it, so putting it in the shared validator made the
* candidate refuse to LOAD any older baseline. All nine performance profiles
* went red at once, and the release gate withheld an asset from a published
* stable release. Topology belongs here, where only our own dist is judged.
*/
export function assertOwnBundleTopology(manifest, label = BUNDLE_MANIFEST) {
const initialPanel = manifest?.initialPanelFiles;
if (!Array.isArray(initialPanel)) {
throw new Error(`${label}: initialPanelFiles must be an array of bundle paths`);
}
if (initialPanel.includes(CARD_ENTRY)) {
throw new Error(`${label}: initial panel graph must not contain the card facade`);
}
return manifest;
}
/** Validate the additive two-entry manifest contract independently of disk I/O. */
export function assertBundleManifest(manifest, label = BUNDLE_MANIFEST) {
if (manifest?.schema !== 1 || typeof manifest.fingerprint !== 'string'
@@ -63,13 +91,8 @@ export function assertBundleManifest(manifest, label = BUNDLE_MANIFEST) {
if (!initialView.includes(CARD_ENTRY) || initialView.includes(PANEL_ENTRY)) {
throw new Error(`${label}: initial View graph must contain only the card stable entry`);
}
// #535: the panel reaches the implementation by its content-hashed name, so
// the card's stable facade is no longer part of what the panel loads. That is
// the point: the facade is the one address with no version in it, and the
// panel must not depend on a URL a browser may keep for hours. What still has
// to hold is that the panel reuses the exact card IMPLEMENTATION graph.
if (initialPanel.includes(CARD_ENTRY) || !initialPanel.includes(PANEL_ENTRY)) {
throw new Error(`${label}: initial panel graph must contain its own stable entry only`);
if (!initialPanel.includes(PANEL_ENTRY)) {
throw new Error(`${label}: initial panel graph must contain its own stable entry`);
}
if (initialView.some((path) => path !== CARD_ENTRY && !initialPanel.includes(path))) {
throw new Error(`${label}: initial View implementation is not a subset of initial panel graph`);
+19
View File
@@ -3124,6 +3124,25 @@ const MUTANT_DEFINITIONS = [
replace: ' void panelEntry; // mutant: cached panel keeps its static shared-chunk import',
}],
},
{
id: 'manifest-loader-judges-current-topology',
guard: 'node --test --test-name-pattern="#537 loader-side validation" '
+ 'test/bundle-assets.test.mjs',
because: 'the loader-side validator also reads FOREIGN manifests — the performance harness '
+ 'runs the candidate against a baseline checkout — so a rule about how the CURRENT build '
+ 'is wired makes the candidate refuse to load any older baseline. That is what turned all '
+ 'nine performance profiles red at once and withheld an asset from a published stable '
+ 'release (#537)',
patches: [{
file: 'scripts/bundle-tree.mjs',
find: ' if (!initialPanel.includes(PANEL_ENTRY)) {\n'
+ ' throw new Error(`${label}: initial panel graph must contain its own stable entry`);\n'
+ ' }',
replace: ' if (initialPanel.includes(CARD_ENTRY) || !initialPanel.includes(PANEL_ENTRY)) {\n'
+ ' throw new Error(`${label}: initial panel graph must contain its own stable entry`);\n'
+ ' }',
}],
},
{
id: 'panel-imports-unversioned-card-entry',
guard: 'node --test --test-name-pattern="#486 both stable entries" '
+20 -10
View File
@@ -19,8 +19,8 @@ import {
lowHeadroomWarning,
} from '../scripts/bundle-budget.mjs';
import {
assertBundleManifest, compareBundleTrees, orderedBundlePayload, sha256Bytes,
verifyBundleTree,
assertBundleManifest, assertOwnBundleTopology, compareBundleTrees,
orderedBundlePayload, sha256Bytes, verifyBundleTree,
} from '../scripts/bundle-tree.mjs';
import {
cssTemplateMinifier, minifyCssText, minifyStaticCssTemplates,
@@ -296,20 +296,30 @@ test('#486 manifest graph validator rejects missing panel roots and duplicated c
// #535: панель обязана переиспользовать ту же реализацию, а не свою копию.
assert.throws(
() => assertBundleManifest(duplicateGraph),
/initial panel graph must contain its own stable entry only|not a subset/,
/initial panel graph must contain its own stable entry|not a subset/,
);
});
// Обратная сторона того же инварианта: фасад карточки в графе панели —
// теперь ошибка, потому что это единственный адрес без версии (#535).
const facadeInPanel = structuredClone(valid);
facadeInPanel.initialPanelFiles = [
test('#537 loader-side validation accepts a pre-#535 manifest; topology does not', () => {
// Манифест той топологии, что была до #535: фасад карточки входит в граф
// панели. Ровно такой лежит в каждой базе сравнения старше #535, и
// производительный харнесс читает его валидатором КАНДИДАТА — поэтому
// «можно ли это загрузить» обязано отвечать «да».
const legacy = minimalTwoEntryManifest();
legacy.initialPanelFiles = [
'houseplan-assets/card-HASH.js', 'houseplan-card.js', 'houseplan-panel.js',
];
facadeInPanel.initialPanelGzipBytes = 14;
legacy.initialPanelGzipBytes = 14;
assert.equal(assertBundleManifest(legacy), legacy,
'валидатор загрузки обязан принимать манифест базы прежней топологии');
// А «так ли устроена ТЕКУЩАЯ сборка» — отдельный вопрос и отдельная функция.
assert.throws(
() => assertBundleManifest(facadeInPanel),
/initial panel graph must contain its own stable entry only/,
() => assertOwnBundleTopology(legacy),
/initial panel graph must not contain the card facade/,
);
const current = minimalTwoEntryManifest();
assert.equal(assertOwnBundleTopology(current), current);
});
test('#486 sync payload orders dependencies before both stable entries', () => {