Three independent blind spots in the test harness.
1. smoke-select read symbols only from changed lines of a --unified=0
diff. An edit to the arguments of a multi-line call names nothing:
#741 (d5bdfde9) changed only the arguments of
runtime.resolveIsoOverlayFitEnvelope({ on the line above, and the
selection answered "unproven" plus the visual minimum, although the
callee is registered in smoke-links for smoke_iso_flat_parity and
smoke_isometric_contract - the two smokes the #741 author ran by hand.
The selection diff now carries CALL_CONTEXT_LINES = 3 lines of
context; for each changed line parseDiff looks for the nearest
unclosed "(" above it within the hunk, walking through a literal
argument ({ or [ after "(", "," or "["), stopping at ";" on depth zero
or any other unclosed brace. A callee from the symbol table joins
symbols and the new callees field and is marked "(вызов)" in the
report. Context lines never give direct symbols. task-packet takes a
separate context diff for selectSmokes; change-risk keeps --unified=0.
Over the last 80 src commits of dev: 16 commits gain a callee, 2 move
from unproven to a proven link (#741, #7245f8e8ca7), +15 smokes in
total, at most 4 per commit, none lost.
2. The #732 dead-field check judged only scene-builder calls. The four
resolveIsoOverlayFitEnvelope({...}) literals in iso-scene-render tests
went straight into the test-build function, so stageSize: null (the
field #741 removed) stayed green. They now go through overlayFit typed
with OverlayFitFixture (keys of IsoOverlayFitEnvelopeInput); the check
judges overlayFit/resolveIsoOverlayFitEnvelope calls like the scene
builders, and its probe asserts that OverlayFitFixture rejects
stageSize, so the type resolved to the real input and not to any.
3. smoke_backdrop's mode() called the private _setMode and slept 220 ms.
It now enters a mode through __hpTest.setMode and waits for the end of
the transition by the same markers as section 6b (#715): one page
helper used by both. Oracles and the 59 check names are unchanged.
Witnesses: d5bdfde9 selects both iso smokes with no "unproven"; the same
fixture without context lines is unproven again; attribution disabled
reds both AC1 units. stageSize: null in an overlayFit call reds the first
#732 test; a direct resolveIsoOverlayFitEnvelope({...}) reds the third.
smoke_backdrop is green normally and with animation frames slowed to 60
and 150 ms; a stage animation that never ends fails with a named error.
Issue: #754
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Since #713 the overlay fit envelope reserves no nudge budget, and since
#725 _isoScene passes `stageSize: null` while resolveIsoOverlayFitEnvelope
never reads the field. The room focus still built a { width, height }
object from the stage for nothing. The optional field is removed from
IsoOverlayFitEnvelopeInput together with both call-site arguments.
The #725 AC3 unit compared bounds with stageSize null and 1000x500, which
is now meaningless; it checks instead that the fit bounds follow only
scene.frame and the tiles: the same bounds for every stage aspect, a moved
frame moves them, an enclosing frame is returned as is.
Issue: #741
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Profiling #694 found three costs on every View pass, paid even with the
summary panel hidden.
The summary panel read the safe-area probe's computed style in layout(),
which the card reaches up to five times per render (renderControls,
menuItems, renderPanel twice, the clock check), and its updated()
measured the stage, probe and kiosk buttons after every DOM commit. The
insets now live in the measured state: measureLayout is the only method
that reads style or layout, and updated() calls it only when an input of
the measurement changed (probe, kiosk buttons or stage element, title,
language, mode, kiosk, kiosk scale, narrow, HA theme), after connect()
or an identity change, on visibility, once after document.fonts.ready,
and from resized() as before. A floor switch or an HA tick no longer
measures.
The _model getter rebuilt the config fingerprint (a walk over every
space and room with JSON.stringify of room settings) on each of its
dozens of reads per render. ConfigFingerprintPass remembers the whole
cache key (epoch and fingerprint) from the start of willUpdate() to the
end of render() while the epoch, the config object and its spaces array
are unchanged. Remembering only the fingerprint and concatenating the key
on every read was tried first: in 2.5D on the large house the switch cycle
measured slower than without any memo, and CPU profiles showed several
times more garbage collection on load and on the first visit of a floor;
one remembered key per pass has neither. Outside the pass (handlers, updated(),
timers) every read still builds the key, so an in-place edit without an
epoch bump stays visible (HP-1454-04). No write to the fingerprinted
fields is reachable from willUpdate() or render().
_isoScene read the stage box during render only to feed an aspect into
the overlay fit, whose frame has not depended on the aspect since #713.
It now uses the frame's own aspect and passes stageSize: null.
render-layout-read.mjs now also judges _isoScene and the whole summary
runtime except measureLayout, forbids layout property reads
(clientWidth, offsetTop, ...) besides the two calls, and reports every
violation. Two registered mutants restore the old reads.
No visible change: panel caps, side, offsets and kiosk clearance are
computed from the same values; the 2.5D frame is the same.
Issue: #725
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
After #714 and #724 the 2.5D overlays still carried stubs:
- renderIsoOverlayGrounds and renderIsoRaisedOverlays returned an empty SVG
on every frame. They go with IsoFramePresentation.grounds/raised and the two
bindings in the card. The iso-overlays-svg element itself stays, now empty:
it is the inert camera-viewBox layer the contract and live-touch smokes
measure screen-facing HTML against, so the 2.5D DOM keeps its elements.
- IsoOverlayRenderEntry.groundRadius was computed for every device, room label
and lock and read only by the snapshot comparison that compared it.
The overlay test fixtures passed view, referenceView, stageSize and layers
(and one test selectedDeviceId), which IsoOverlaySceneInput does not have, and
asserted that changing them keeps the placement - a claim the signature makes
by itself. Those fields are gone from every fixture. The zoom/resize asserts of
"Stage 4 reuses pure overlay placements" and "#713 AC3" (renamed to what it
still checks) and the "#570 supersedes #473 W1" selection test go; the #724
AC2 test now zooms the way production does, through the live frame of
resolveIsoScene, and checks that the structural geometry and so the overlay
scene are reused. The #713 K8 fixture no longer passes stageSize, which
resolveIsoOverlayFitEnvelope does not read.
test/iso-overlay-fixture-types.test.mjs typechecks the overlay test files with
the TypeScript compiler: their fixture types (OverlaySceneFixture,
OverlayEntryFixture) are the keys of the production types with deliberately
loose values, so a partial fixture is fine and a field the type lacks is an
excess-property error. Three checks: no excess property in the fixture files;
a probe shows the fixture types resolve to the real inputs and reject view,
referenceView, stageSize, layers, selectedDeviceId and groundRadius; every
call of the scene builder gets its argument through a checked type (a literal
in overlayScene or a declaration of the fixture type). Each check is red when
a dead field is put back into a declared fixture, an override literal or an
entry, when a literal goes straight into the builder, when a fixture loses its
annotation, and when groundRadius returns to the entry type.
isometric-contract now asserts that nothing renders into the overlay surface
and that the removed renderers and groundRadius stay gone. No mutant is
anchored on the removed code; mutation-gate --check is unchanged (3 warnings).
The 19 2.5D golden scenes pass in capture on the accepted baselines.
Issue: #732
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
After #714 the 2.5D overlay scene still carried what decides nothing:
- src/iso-overlays.ts: IsoOverlayPlacement loses tether and grounding (always
invisible) and raisedScene (always equal to visualScene); IsoOverlayOwner
loses area; IsoOverlayPlacementInput loses hovered, focused, selected and
filtersSupported, which the resolver ignored. IsoWallSilhouette and
tetherGeometry go with them.
- src/iso-scene-render.ts: the structural scene no longer projects wall
silhouettes (isoWallSilhouettesOf and IsoSceneCacheEntry.wallSilhouettes)
that served only as a cache key. The placement and render-scene caches are
keyed by the wall geometry the scene is drawn with (IsoOverlaySceneInput.
structure = scene.geometry): the structural LRU hands out the same object
across zoom, stage resize and HA state, and a new one after any wall, room
or opening edit. The resolveCollisions flag and its fit/live cache slots
are gone: since #713 both held equal placements, and 2.5D renders only in
View, where the fit probe and the live frame ask with the same devices, so
they now read one snapshot.
- src/houseplan-card.ts: the fit call passes no flag; the overlay scene gets
structural.geometry. data-hp-iso-nudged stays the constant "false" read by
the golden requireOneRise preflight, the live-touch smoke and the benchmark.
Tests: iso-overlays pins the placement fields; iso-scene-render builds the
structure with buildIsoWallGeometry, the #714 zoom/resize and #711 state tests
stay, fit and live are asserted to share one snapshot, and two #724 AC2 tests
run the production path (createIsoStructuralSource -> resolveIsoScene ->
buildIsoOverlayRenderScene): a thicker wall with the same room rebuilds the
scene (red with a key without walls, e.g. keyed by the room rows), and a room
edit that moves the owner gives the new owner (red with a constant key). The
silhouette-construction test goes with the construction.
Mutants: #473 W2 (iso-placement-cache-survives-silhouette-change, id kept for
history) now keys the placement cache by a constant instead of input.structure
and its guard also runs the #724 AC2 tests; W6 patches the new structure line;
the W5 description no longer speaks of a nudge. The isometric-contract regex
checks the new key instead of the silhouette construction. docs/ISOMETRIC.md
names the key.
Live 2.5D output is unchanged: the 21 isometric golden scenes pass on the
accepted baselines.
Issue: #724
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Since #713 every raised device tile and lock badge is its floor anchor lifted
by one shared wall-top rise and room names stay on the floor, so the live
scene no longer called the #651 search. What was left of it only cost code,
build time and review attention:
- src/iso-overlays.ts: resolveIsoOverlayRigidGroups, resolveIsoOverlayCollisions
with their boundary-candidate machinery, the nudge search in
resolveIsoOverlayPlacement (the vector to the room safe point, the near-wall
test, the zoom hint), the safe point itself, the nudge/nearWall/cleared/capped
and status/reason fields, and ISO_OVERLAY_MAX_NUDGE_CSS_PX /
ISO_OVERLAY_SAFETY_GAP_CSS_PX.
- src/iso-scene-render.ts: the zoom reuse fast path and the CSS-pixel scale it
compared; a placement now depends only on anchor, owner, footprint and rise,
so zoom and stage resize reuse it by signature. residualPairs is gone and the
memo key is called layoutSignature.
- src/houseplan-card.ts: the overlay scene no longer receives the view, the
reference view or the stage rect it only fed to that scale;
data-hp-iso-nudged stays as the constant "false" that the golden
requireOneRise preflight, the live-touch smoke and the Stage 4 benchmark read.
The #585/#651 unit tests and the seven mutants that guarded only the removed
code are deleted; kept tests drop their nudge assertions, and a stage resize is
now pinned as a non-layout event. docs/ISOMETRIC.md keeps #651 as history only.
Live 2.5D output is unchanged: the 21 isometric golden scenes pass on the
accepted baselines.
Issue: #714
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
- Vertical oblique projection: the floor matrix is the identity and a height
rises straight up by z·sin 20°, so the on-screen wall height is unchanged
and the cos 20° foreshortening of the plan, decor and anchors is gone.
- Device tiles and lock badges stand on the wall-top plane with one common
shift; the #651 placement search no longer runs in the live scene (its
removal is #714). Room names keep their Flat floor point.
- The 2.5D fit no longer reserves the 48 CSS px nudge budget.
- Switching projection keeps the camera when the previous projection was on
screen: saving the setting, entering an editor from 2.5D and adopting a warm
memo from the other projection re-read only the scalar zoom. A cold 2.5D
start still opens the 2.5D home.
- Opening faces are ordered along the oblique projector (s·y + z).
Witness: demo/smoke_iso_flat_parity.mjs (AC2–AC5, AC11) is red on the old code.
Issue: #713
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Owner's decision in #694: icons must not change position with state.
Since #651 the rigid overlay layout sized a device by its value text and
badges, which change with HA state: a light toggling on changed its width
from 37.2 to 26.7 CSS px and re-laid out all 62 overlays of the dense
scene (~385 of 495 ms of stateUpdate; v1.77.0 had 208 ms).
- iso-scene-render.ts: the layout sees the state-free tile (icon at its
configured size, no value text, badge or supplemental metrics). An
HA-only change keeps the layout and refreshes only the visual extent
that scene bounds read, without a collision search.
- iso-overlays.ts: the rigid-group search skips candidates that already
lose to the fallback on room, then wall violations (lexicographic
bound). The result is unchanged — identical placement hash on the dense
scene — at about 35 % less work.
- docs/ISOMETRIC.md, changelogs; test #711; mutant
iso-device-layout-follows-state-again (written, not run — #709).
Local isometric-stage3-dense-v1, 3 samples: stateUpdate 522 → 89 ms
(v1.77.0: 208), modelReady 3665 → 3129, switchCycle 5544 → 4700.
Issue: #711
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Keep the raised overlay footprint as calculation-only geometry for collision, nudge and fit while removing its painted SVG surface and texture. Update Stage 3 contracts, smokes, golden harness rules and synchronized bundles.
Issue: #471
User-Visible: no