Ask a foreign manifest whether it can be loaded, not how it was wired
Проверка (CI) / Классификация изменённых файлов (push) Successful in 22s
Проверка (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) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Successful in 52s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 27s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 21s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m10s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 13m27s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 15m22s
Проверка (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

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. #535 put a rule about the CURRENT build into that
shared validator — the panel graph must not contain the card facade —
and it is false of every build before #535 by construction. The
candidate then refused to load any older baseline: all nine performance
profiles went red at once on the same step, the stable release gate
withheld `houseplan-card.js` from the published v1.75.0, and none of it
was about speed.

assertBundleManifest now answers only the loader's question: paths
exist, nothing is duplicated, graphs reference listed assets, sizes add
up. The topology of the current build moves to assertOwnBundleTopology,
called from bundle-sync.mjs, which materializes our own dist, and from
the unit test that reads dist/houseplan-assets.json. Neither ever looks
at a foreign tree.

Reproduced end to end, not only in a unit: a v1.74.0 worktree built with
its own code, then `node demo/benchmark_large_house.mjs
--target-root=<baseline>` from this tree. Before the change it stops
with «initial panel graph must contain its own stable entry only»; after
it, the profile is captured.

Issue: #537
User-Visible: no
This commit is contained in:
Claude
2026-09-12 09:39:05 +03:00
parent 2c6410bb87
commit 1ef35883e0
4 changed files with 73 additions and 18 deletions
+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`);