mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
The #735 guard compared cache sizes around the warmed twelve-switch cycle. Once an LRU is full a cold build evicts one entry and every size stays: a union planted cold in the full pool (8/8) changed no field of the snapshot and the guard stayed green. Its `openingWallIndex` field read `card._openingWallIndexCache ? 1 : 0` of a Map and was always 1. The card now counts real builds of each floor-geometry cache in its miss branch (`_floorCacheBuilds`: wall union, inner contours, clean floors via a `cleanFloorForRoom` hook, opening wall index, light barriers, Glow clips, physical bodies, opening tunnels, light physical bodies) — never on a hit, a recency refresh, a resize seed or alias, or a clear. One pure decision (demo/performance/switch-cycle-guard.mjs) judges the window for the benchmark, the floor-cache smoke and the unit tests: any build of a pooled family fails and names the family and its growth; the single-slot families may rebuild once per switch; 2.5D keeps `_isoStructuralBuildCount`. A base bundle without the counters reads null and is not judged; a candidate must expose them. Each row reports `switchCycleBuilds` with the size snapshot as a diagnostic (the real index size there). The budgeted cacheEntries/cacheGrowth keep the index as the presence flag their `openingWallIndex: 1` ceilings were set on: no budget moves. Counted run, one sample per profile on the dev base with the counters: every large-house* profile and isometric-stage3-dense-v1 build 0 in every judged family, 12/12 physical bodies and opening tunnels, nothing in 2.5D; no family moved to the reported list. The same runner against a dev bundle reports `supported: false` and passes. AC7: demo/smoke_floor_geometry_cache.mjs fills the pool to 8 and drops one floor's union; a four-switch cycle builds exactly that union (+1, an eviction, every size the same) and the decision names "wall union" — the old size comparison sees nothing. AC1 of the smoke also reads the counters. Registry mutant: a counter that skips the evicting miss (caught by the smoke, checked by hand); browser inventory 245/200. Issue: #769 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
110 lines
5.2 KiB
JavaScript
110 lines
5.2 KiB
JavaScript
// #769: the #735 switch-cycle guard judges build counters, not cache sizes.
|
|
// The decision is the one pure function the benchmark serialises into the page
|
|
// and the floor-cache smoke uses (demo/performance/switch-cycle-guard.mjs).
|
|
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
floorCacheBuilds, floorCacheSnapshot, judgeSwitchCycle,
|
|
} from '../demo/performance/switch-cycle-guard.mjs';
|
|
|
|
const counters = (overrides = {}) => ({
|
|
wallUnion: 3, innerContour: 60, cleanFloor: 60, openingWallIndex: 3, lightBarrier: 3,
|
|
glowClip: 3, physicalBodies: 5, openingTunnel: 5, lightPhysicalBodies: 3, ...overrides,
|
|
});
|
|
|
|
/** The guard before #769: a failure is any grown size. */
|
|
const legacySizeGuard = (before, after) => Object.keys(after)
|
|
.filter((key) => after[key] > before[key]).map((key) => `${key} +${after[key] - before[key]}`);
|
|
|
|
test('#769 AC6: a warm cycle passes; single-slot families may rebuild once per switch', () => {
|
|
const verdict = judgeSwitchCycle({
|
|
before: counters(),
|
|
after: counters({ physicalBodies: 17, openingTunnel: 17 }),
|
|
switches: 12,
|
|
});
|
|
assert.deepEqual(verdict, {
|
|
ok: true, supported: true, failures: [],
|
|
builds: {
|
|
wallUnion: 0, innerContour: 0, cleanFloor: 0, openingWallIndex: 0, lightBarrier: 0,
|
|
glowClip: 0, physicalBodies: 12, openingTunnel: 12, lightPhysicalBodies: 0,
|
|
},
|
|
});
|
|
});
|
|
|
|
test('#769 AC6: any build of a pooled family fails and names the family', () => {
|
|
for (const [family, label] of [
|
|
['wallUnion', 'wall union'], ['innerContour', 'inner contour'], ['cleanFloor', 'clean floor'],
|
|
['openingWallIndex', 'opening wall index'], ['lightBarrier', 'light barrier'], ['glowClip', 'glow clip'],
|
|
]) {
|
|
const before = counters();
|
|
const verdict = judgeSwitchCycle({ before, after: counters({ [family]: before[family] + 2 }), switches: 12 });
|
|
assert.equal(verdict.ok, false, family);
|
|
assert.deepEqual(verdict.failures, [`${label} +2`]);
|
|
}
|
|
});
|
|
|
|
test('#769 AC6: a single-slot family is judged by the number of switches only', () => {
|
|
const before = counters();
|
|
assert.equal(judgeSwitchCycle({ before, after: counters({ physicalBodies: 5 + 12 }), switches: 12 }).ok, true);
|
|
const over = judgeSwitchCycle({ before, after: counters({ physicalBodies: 5 + 13 }), switches: 12 });
|
|
assert.equal(over.ok, false);
|
|
assert.deepEqual(over.failures, ['physical bodies +13 (more than 12 floor switches)']);
|
|
assert.deepEqual(
|
|
judgeSwitchCycle({ before, after: counters({ lightPhysicalBodies: 3 + 13 }), switches: 12 }).failures,
|
|
['light physical bodies +13 (more than 12 floor switches)'],
|
|
);
|
|
});
|
|
|
|
test('#769 AC6: a base without counters is not judged; a candidate without one family is', () => {
|
|
for (const [before, after] of [[null, null], [null, counters()], [counters(), null]]) {
|
|
const verdict = judgeSwitchCycle({ before, after, switches: 12 });
|
|
assert.deepEqual(verdict, { ok: true, supported: false, builds: null, failures: [] });
|
|
}
|
|
const { glowClip: _glowClip, ...partial } = counters();
|
|
const verdict = judgeSwitchCycle({ before: partial, after: partial, switches: 12 });
|
|
assert.equal(verdict.ok, false);
|
|
assert.deepEqual(verdict.failures, ['glow clip counter is missing']);
|
|
assert.equal(floorCacheBuilds({}), null, 'an old bundle reads null');
|
|
assert.deepEqual(floorCacheBuilds({ _floorCacheBuilds: counters() }), counters());
|
|
});
|
|
|
|
test('#769 AC6: the 2.5D structural counter is still judged', () => {
|
|
const verdict = judgeSwitchCycle({
|
|
before: counters(), after: counters(), switches: 12, isoBefore: 4, isoAfter: 5,
|
|
});
|
|
assert.deepEqual(verdict.failures, ['isoStructuralBuilds +1']);
|
|
assert.equal(judgeSwitchCycle({
|
|
before: null, after: null, switches: 12, isoBefore: null, isoAfter: 7,
|
|
}).ok, true, 'a base without the 2.5D counter is not judged');
|
|
});
|
|
|
|
test('#769 AC7: a cold build in a full LRU keeps every size and still fails', () => {
|
|
// A full union pool: the cold floor's union is built and one entry is
|
|
// evicted, so the pool stays at 8; the floor's contours and clean floors
|
|
// were warm. Sizes before and after are identical.
|
|
const sizes = {
|
|
cleanFloor: 60, glowClip: 3, wallUnion: 1, wallUnionPool: 8, innerContour: 60,
|
|
openingTunnel: 1, openingWallIndex: 3, isoGeometry: 0, planSnapGeometry: 0, wallFaceGraph: 0,
|
|
};
|
|
assert.deepEqual(legacySizeGuard(sizes, { ...sizes }), [], 'the size guard saw nothing');
|
|
const verdict = judgeSwitchCycle({
|
|
before: counters(), after: counters({ wallUnion: 4, physicalBodies: 9, openingTunnel: 9 }), switches: 4,
|
|
});
|
|
assert.equal(verdict.ok, false);
|
|
assert.deepEqual(verdict.failures, ['wall union +1']);
|
|
});
|
|
|
|
test('#769: the size snapshot reads the real size of the opening wall index', () => {
|
|
const card = {
|
|
_cleanFloorCache: new Map([[1, 1]]), _glowClipCache: new Map(),
|
|
_wallUnionCache: { key: 'k' }, _wallUnionPool: new Map([[1, 1], [2, 2]]),
|
|
_innerContourCache: new Map(), _openingTunnelCache: null,
|
|
_openingWallIndexCache: new Map([[1, 1], [2, 2], [3, 3]]),
|
|
};
|
|
assert.deepEqual(floorCacheSnapshot(card), {
|
|
cleanFloor: 1, glowClip: 0, wallUnion: 1, wallUnionPool: 2, innerContour: 0,
|
|
openingTunnel: 0, openingWallIndex: 3, isoGeometry: 0, planSnapGeometry: 0, wallFaceGraph: 0,
|
|
});
|
|
});
|