mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -354,8 +354,15 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
|
||||
* граф, иначе первый кадр 2.5D рисует круги Flat. Геометрия света и коллизии
|
||||
* остались в ленивом графе. Факт оставляет 896 Б сверху и 1 104 Б до нижней
|
||||
* границы; общий бюджет не меняется.
|
||||
*
|
||||
* 2026-09-26, #654: 294 900 → 296 000 (замер 295 047). Холодный первый кадр
|
||||
* обязан знать, что 2.5D запрошен, пока его ленивый граф ещё не загружен, и
|
||||
* удерживать заставку до вычисления цвета бумаги; эту границу по определению
|
||||
* нельзя положить внутрь охраняемого ею чанка. Геометрия и сам рендер остаются
|
||||
* ленивыми. Факт оставляет 953 Б сверху и 1 047 Б до нижней границы; общий
|
||||
* бюджет не меняется.
|
||||
*/
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 294_900;
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 296_000;
|
||||
export const INITIAL_VIEW_CEILING_BAND = 2_000;
|
||||
|
||||
/**
|
||||
|
||||
@@ -320,7 +320,7 @@ export const CHECKS = {
|
||||
},
|
||||
frontend: {
|
||||
// npm run typecheck, npm test, npm run build, bundle:budget, lint:unused (#624)
|
||||
entries: ['test/*.test.mjs', 'scripts/no-new-any.mjs', 'scripts/no-new-private-writes.mjs', 'scripts/bundle-budget.mjs',
|
||||
entries: ['test/*.test.mjs', 'scripts/no-new-any.mjs', 'scripts/no-new-private-writes.mjs', 'scripts/render-layout-read.mjs', 'scripts/bundle-budget.mjs',
|
||||
'scripts/fix-test-build.mjs', 'scripts/unused-locals-gate.mjs'],
|
||||
// demo/helpers/** — область no-new-private-writes (#629): гейт читает их текст.
|
||||
roots: [...BUILD_INPUTS, 'test/**', 'tsconfig*.json', 'scripts/monolith-baseline.json', 'demo/smoke_*.mjs', 'demo/benchmark_*.mjs', 'demo/guard/*.mjs', 'demo/helpers/**', ...WORKFLOW],
|
||||
|
||||
@@ -63,6 +63,7 @@ export function parallelSteps(base) {
|
||||
return [
|
||||
{ name: 'сборка + typecheck (npm run build)', cmd: npm, args: ['run', 'build'] },
|
||||
{ name: 'новый код не добавляет any', cmd: process.execPath, args: ['scripts/no-new-any.mjs', '--base', base, '--head', 'HEAD'] },
|
||||
{ name: 'render не читает layout синхронно', cmd: process.execPath, args: ['scripts/render-layout-read.mjs'] },
|
||||
// #629: смоки не добавляют записей в приватное состояние карточки.
|
||||
{ name: 'смоки не пишут в приватное состояние', cmd: process.execPath, args: ['scripts/no-new-private-writes.mjs', '--base', base, '--head', 'HEAD'], hint: 'window.__hpTest (docs/TESTING.md) или // private-ok: <причина>' },
|
||||
{ name: 'смоки по диффу (smoke-select)', cmd: process.execPath, args: ['scripts/smoke-select.mjs', '--base', base, '--head', 'HEAD', '--json'], informational: true },
|
||||
|
||||
@@ -12568,6 +12568,42 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' return this.center(room, polygonOverride, spaceId); // mutant: preview prunes state',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'iso-first-frame-reads-paper-during-render',
|
||||
guard: 'node scripts/render-layout-read.mjs',
|
||||
because: '#654 AC2: reading computed paper colour in _renderBody forces layout and sees the '
|
||||
+ 'host fallback before .hp-paper exists on the cold first render; the AST guard forbids '
|
||||
+ 'that regression independently of formatting or comments',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: 'this._isoFirstFrame.lightFloors(new Map([...roomFills.byId].map(([id, fill]) => [id, fill && fill.opacity > 0 ? fill : glowBase.byId.get(id) ?? null])))',
|
||||
replace: "memoIsoLightFloorRooms(null, new Map([...roomFills.byId].map(([id, fill]) => [id, fill && fill.opacity > 0 ? fill : glowBase.byId.get(id) ?? null])), parseCssColor(getComputedStyle(this.renderRoot.querySelector('.hp-paper') ?? this).fill) ?? [255, 255, 255]).rooms",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'iso-first-frame-reveals-flat-during-lazy-load',
|
||||
guard: 'node demo/smoke_iso_first_frame.mjs',
|
||||
because: '#654 AC1/AC3: removing the pending class exposes the effective Flat projection '
|
||||
+ 'while the 2.5D chunk is still loading, most visibly in kiosk mode after the ordinary '
|
||||
+ 'boot window has elapsed',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: "${isoFirstFramePending ? ' hpiso-pending' : ''}",
|
||||
replace: "${isoFirstFramePending ? '' : ''}",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'iso-light-floor-memo-never-reuses',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#654 light-floor memo" test/iso-stage6.test.mjs',
|
||||
because: '#654 AC4: HA state churn that leaves paper, fills and room membership unchanged '
|
||||
+ 'must reuse the classification Set instead of rebuilding it on every card render',
|
||||
patches: [{
|
||||
file: 'src/iso-materials.ts',
|
||||
find: ' return previous?.key === key',
|
||||
replace: " return previous?.key === '__never__'",
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8');
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
// #654: render and 2.5D pure helpers must not force browser layout. This is an
|
||||
// AST contract, not a regex anchor: formatting and comments cannot satisfy it.
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import ts from 'typescript';
|
||||
|
||||
const forbidden = new Set(['getComputedStyle', 'getBoundingClientRect']);
|
||||
const source = (path) => ts.createSourceFile(
|
||||
path, readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'),
|
||||
ts.ScriptTarget.Latest, true,
|
||||
);
|
||||
const callName = (node) => !ts.isCallExpression(node) ? null
|
||||
: ts.isIdentifier(node.expression) ? node.expression.text
|
||||
: ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : null;
|
||||
const forbiddenCalls = (node) => {
|
||||
const found = [];
|
||||
const visit = (child) => {
|
||||
const name = callName(child);
|
||||
if (name && forbidden.has(name)) found.push(name);
|
||||
ts.forEachChild(child, visit);
|
||||
};
|
||||
visit(node);
|
||||
return found;
|
||||
};
|
||||
|
||||
const card = source('src/houseplan-card.ts');
|
||||
const required = new Set(['render', '_renderBody', 'willUpdate']);
|
||||
const checked = new Set();
|
||||
const visit = (node) => {
|
||||
if (ts.isMethodDeclaration(node) && node.name && ts.isIdentifier(node.name)
|
||||
&& required.has(node.name.text)) {
|
||||
checked.add(node.name.text);
|
||||
const calls = forbiddenCalls(node.body);
|
||||
if (calls.length) throw new Error(`${node.name.text} forces layout: ${calls.join(', ')}`);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(card);
|
||||
if ([...required].some((name) => !checked.has(name))) throw new Error('guarded lifecycle method is missing');
|
||||
|
||||
for (const file of readdirSync(new URL('../src', import.meta.url)).filter((name) => /^iso-.*\.ts$/.test(name))) {
|
||||
const calls = forbiddenCalls(source(`src/${file}`));
|
||||
if (calls.length) throw new Error(`${file} forces layout: ${calls.join(', ')}`);
|
||||
}
|
||||
console.log('render/layout-read AST contract: OK');
|
||||
@@ -27,6 +27,14 @@
|
||||
|
||||
/** @type {SmokeLink[]} */
|
||||
export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: ['memoIsoLightFloorRooms', 'IsoFirstFrameState', 'isoPaperContext'],
|
||||
smokes: ['smoke_iso_first_frame.mjs'],
|
||||
because: '#654: the production-bundle witness delays and rejects the lazy 2.5D chunk, then '
|
||||
+ 'samples every animation frame in normal, kiosk and Flat views; no pure import can prove '
|
||||
+ 'that the loading veil hides the transient Flat render or that the first visible marker '
|
||||
+ 'already uses the settled paper-dependent floor class',
|
||||
},
|
||||
{
|
||||
symbols: ['baseStyles', '_containerOwnedHeight', 'measuredCardHeaderHeight',
|
||||
'settleSoftStageLayout'],
|
||||
|
||||
Reference in New Issue
Block a user