Files
houseplan-card/docs/testing-notes/mutation-browser-guards.md
T
Claudeandclaude[bot] b998b0b34a feat(moon): the moon with any background, and its status in General settings (#718)
The owner decided on 30.09 that the moon is not part of the "Follow the Sun"
environment but a switch of its own: with a static background (global or a
space's own) the card showed no moon even with the switch on, and the switch
said nothing about why the moon was missing right now.

With a static background there is no environment, so the moon stands in its
own layer, `.hp-moon-sky`: the first child of `.stage` / `.hp-static-stage`,
the whole scene, no z-index, filter or will-change, under the plan by DOM
order, fading with the #101 View weight. Inside is the very #661 element, so
place, size, art and fades are unchanged, and a background switch moves it to
its new parent in the same render without a flicker. The phase comes from the
same `resolveDayCycle`, computed only while the moon is on and on View; without
`sun.sun` both cards keep their 30 s clock ticker and re-render only when the
phase changes (the environment is still compared by its whole fingerprint).

General settings get a second caption line under the moon switch
(`data-moon-status`): one snapshot per opening, judged by the lazy chunk as if
the switch were on, first reason wins (no home, day, below 3°, under 3 %),
numbers rounded and clamped below the threshold they missed. `moonStatus`
decides "shown" with the same `moonShownAt` as the element. It lives in a
WeakMap beside the draft, so it never makes the dialog dirty; a closed
opening's result is dropped. The dialog loads the chunk through the gate's
loader (`withMoon`), now shared by every caller while a load is in flight, so
there is still one fingerprint check and one retry token.

Bundle (same build, against origin/dev): initial View 300 072 -> 300 248 B gzip
(+176 B, under the 500 B of the spec; budget and ceiling not raised); lazy
editor 238 558 -> 238 991 B (+433 B, the line and English strings); lazy moon
11 385 -> 11 712 B (+327 B, layer CSS and status). `src/moon.ts` stays out of
the initial and the editor graph; bundle-budget now refuses an editor/moon
overlap. Monolith metrics: hostRefs 4 885 -> 4 888 — the three `host.` reads of
`src/editors/moon-status.ts` (hass, `_settingsDialog`, requestUpdate) through
its own three-member interface, not the editor port; the other five metrics
are unchanged. houseplan-editor-runtime.ts grows by two lines (import, call).

Tests: AC9/AC10/AC15 and the sky layer in test/moon.test.mjs (the #661
"static -> nothing" check inverted), AC14 and the opening lifecycle in
test/moon-settings.test.mjs, smokes demo/smoke_moon_static.mjs (AC1-AC6; AC1
and AC3 were red on dev) and demo/smoke_moon_status.mjs (AC11/AC12), AC7 in
smoke_daycycle_layer_budget. Golden: two new scenes
(static-bg-moon-gibbous-white-light, static-bg-moon-crescent-south-dark,
matrix v70), the harness checks the moon's parent by background and waits for
the status line in the General settings frames. Four new mutants; the clock
ticker one is a browser guard (201 at the guideline of 200).

Issue: #718
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:25:21 +00:00

12 KiB

Browser guards mutation registry (#659)

This is the reviewed classification of every mutation witness that still needs a browser. The executable limit is 200; mutation-gate --check, npm run inventory and the unit contract all read this same inventory. A new browser guard must be added deliberately under one reason below, and its mutant because must explain the concrete browser-only invariant.

Converted witnesses are not listed here: their registry guard names an explicit node --test suite plus test/mutation-browser-offload.test.mjs, and every converted mutant is run once to prove that the Node witness actually kills it.

Category Count Why a browser is still required
Performance threshold 4 The witness measures real browser wall-time or frame work; a pure assertion cannot prove the budget.
Browser harness integrity 4 The mutation breaks page-error, round-trip or page-registration observation in the browser harness itself.
Paint, cascade and layer composition 26 The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium.
Pointer geometry and trusted interaction 45 The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry.
Responsive DOM layout 36 The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size.
Custom-element and HA browser lifecycle 86 The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition.
Total 201 / 200 Growth above the cap fails mutation-gate --check.

Measured effect

The measurements below are GitHub-hosted Linux runs from 2026-09-27. They are recorded separately from the policy limit: the limit proves that browser guards cannot grow unnoticed, while timings show the effect actually observed on the shared runners.

Measurement Before After Observed change
Full-registry wall time 54:18 (1,017 mutants) 48:08 (1,028 mutants) -6:10 (-11.4%), despite 11 additional mutants
Sum of the six Each test catches its breakage steps 4:17:19 4:12:32 -4:47 (-1.9%)
Longest full-registry shard step 50:45 45:13 -5:32 (-10.9%)
Same converted-witness slice, aggregate time 33:43.9 12:07.7 -21:36.2 (-64.0%)
Same converted-witness slice, critical shard 8:05.1 2:19.3 -5:45.8 (-71.3%)

The candidate slice compares the same 83 converted mutant ids in the baseline full-registry log and in the S7 candidate Validate 36316464432. It sums the interval between consecutive per-mutant completion timestamps on each of the six shards. One of the 84 converted ids, active-tab-not-revealed, was the first completed mutant in its shard, so the log cannot isolate its time from clean-guard setup and it is excluded from both sides. The complete candidate run, which also selected registry/tooling mutants outside this converted slice, finished in 10:03.

The original estimate of three to four hours saved on a full-registry run is not supported by this A/B evidence. The directly converted slice is materially faster, but registry growth and shared-runner variance consume most of that gain in the end-to-end total; only the measured reductions above are claimed.

Reviewed per-mutant inventory

Performance threshold

The witness measures real browser wall-time or frame work; a pure assertion cannot prove the budget.

  • junction-limit-p3-quadratic-again
  • junction-limit-p4-bruteforce-again
  • wall-draw-full-preflight-again
  • wall-draw-wall-artifact-discarded

Browser harness integrity

The mutation breaks page-error, round-trip or page-registration observation in the browser harness itself.

  • benchmark-page-verdict-unwatched
  • report-page-errors-skips-round-trip
  • smoke-guard-blind-to-tail
  • smoke-guard-forgets-to-register-pages

Paint, cascade and layer composition

The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium.

  • daycycle-outline-not-promoted
  • daycycle-outline-promoted-on-inner-paper
  • daycycle-static-outline-promoted-on-inner-paper
  • decor-restored-below-room-fills
  • device-keyboard-bypasses-click-path
  • device-long-value-ellipsis-restored
  • device-unavailable-hover-restored
  • golden-filled-tunnel-removed
  • golden-lamp-out-of-reach
  • hatch-static-renderer-untouched
  • hatch-stroke-not-scaled
  • hatch-zoom-compensation-back
  • iso-first-frame-reveals-flat-during-lazy-load
  • iso-room-label-44-box-centres-name
  • iso-sun-card-drops-occluders
  • iso-sun-flat-wedges-remain
  • iso-theme-dark-wall-rule-returns
  • stage3-w4-device-target-loses-44px-floor
  • stage3-w5-runtime-nudge-writes-storage
  • stage3-w6-no-borders-keeps-raised-plates
  • stage3-w7-sun-state-enters-structural-key
  • sun-ray-origin-cache-ignored
  • sun-ray-origin-save-forced-inner
  • value-static-icon-keeps-live-vacuum
  • value-static-icon-keeps-route-warning
  • value-static-icon-keeps-vacuum-overlay

Pointer geometry and trusted interaction

The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry.

  • align-guides-exclude-dead-source
  • align-point-reads-frozen-snapshot
  • decor-default-style-debounce-cut
  • decor-default-style-seed-cut
  • decor-keyboard-nudge-drops-focus-dialog-guards
  • decor-keyboard-nudge-reruns-magnet
  • dense-device-hit-browser-skips-painted-priority
  • double-fit-bypasses-canonical-fit-all
  • furniture-art-editor-adopt-skipped
  • furniture-edge-handles-steal-the-corner
  • furniture-exterior-surface-removed
  • furniture-shift-listeners-not-attached
  • furniture-wall-runtime-drops-drag-side
  • furniture-wall-runtime-drops-raw-intent
  • live-pinch-compositor-demoted-on-active-lit-commit
  • opening-dimension-overlay-hidden
  • opening-search-hides-none
  • opening-search-select-not-wired
  • placement-accepts-any-mouse-button
  • reorder-skips-materialization
  • resize-history-boundary-repair-removed
  • resize-label-uses-old-room-gear-centre
  • resize-labels-hide-narrow-area
  • resize-pointer-capture-removed
  • resize-preview-reject-silent
  • room-fit-html-overlay-jumps-ahead
  • room-fit-pan-release-reaccepted
  • room-fit-persists-zoom
  • safe-resize-commit-preflight-bypassed
  • sections-editor-transition-uses-window-height
  • sections-grid-drops-stage-flex-chain
  • sections-grid-falls-back-to-viewport-height
  • sections-resize-drops-stage-refit-observer
  • space-card-decor-capability-change-not-adopted
  • space-card-decor-capability-downgrade-does-not-clear-assets
  • tab-drag-outlives-the-card
  • tab-drag-survives-release-outside
  • tab-drag-target-follows-captured-source
  • tab-drop-indicator-always-before
  • tab-drop-outside-commits-last-target
  • tab-reorder-not-persisted
  • touch-pinch-marker-hold-rearmed
  • touch-pinch-zoom-persists-per-frame
  • unavailable-toggle-stays-silent
  • wall-draw-rejection-rollback-skipped

Responsive DOM layout

The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size.

  • dialog-ha-disconnected-reject-reopen-enabled
  • dialog-ha-rejected-close-reopen-disabled
  • dialog-native-reconnect-recovery-disabled
  • dialog-native-surface-stretches-to-viewport
  • dialog-native-update-recovery-disabled
  • fit-house-hidden-walls-vote
  • grid-scale-imperial-roundtrip-drift
  • grid-scale-opening-hit-unscaled
  • grid-scale-opening-symbol-unscaled
  • grid-scale-plan-chrome-unscaled
  • grid-scale-static-factor-missing
  • header-menu-item-below-44
  • header-menu-outside-tap-reaches-plan
  • hidden-room-names-compact-svg-fallback
  • hidden-room-names-full-svg-fallback
  • hidden-room-names-iso-override
  • hp-dialog-ignores-flex-content
  • panel-readonly-empty-bypasses-write-capability
  • phone-editor-close-hidden-with-mode-buttons
  • phone-header-at-tablet-width
  • phone-header-shows-title
  • phone-header-wraps
  • static-card-descendants-hit-testable
  • summary-dialog-drops-flex-content
  • summary-picker-hot-add-stays-filtered
  • tab-editing-without-write-access
  • toolbar-active-highlight-stops-at-tab
  • toolbar-close-back-inside-tab
  • toolbar-close-hidden-at-medium-width
  • toolbar-close-slot-collapses-outside-editor
  • toolbar-close-slot-idle-focusable
  • toolbar-close-slot-leaves-mode-group
  • toolbar-close-target-below-24
  • toolbar-device-count-returns
  • toolbar-mode-zoom-gap-regresses
  • warm-dialog-drops-transferred-baseline

Custom-element and HA browser lifecycle

The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition.

  • accepted-marker-rolled-back-by-layout-failure
  • area-relocation-clears-whole-history
  • area-relocation-loses-position-on-refusal
  • backdrop-busy-dismiss-races-decision
  • backdrop-downscale-drops-alpha
  • backdrop-phase2-falls-back-to-original
  • backdrop-probe-always-safe
  • barrier-cache-never-invalidated
  • camera-anchor-from-presented
  • camera-cancel-loses-zoom
  • child-readd-clears-parent-tombstone
  • cold-view-toggle-delegated-to-runtime
  • cold-view-vacuum-mapid-delegated
  • color-picker-confirm-click-through
  • color-picker-invalid-confirm-latch-removed
  • config-updated-event-ignored
  • confirm-dialog-loses-alertdialog
  • current-rejected-physical-write-keeps-optimistic-wall
  • danger-confirm-back-into-the-branch
  • danger-confirm-lost-space-request-guard-removed
  • danger-confirm-lost-space-transition-cancel-removed
  • danger-confirm-uses-last-rendered-language-gate
  • danger-confirm-warm-language-guard-removed
  • danger-confirm-warm-transition-cancel-removed
  • daycycle-programmatic-camera-skips-safe-outline
  • device-echo-keeps-local-noncanonical
  • device-focus-tooltip-blur-cleanup-removed
  • device-focus-tooltip-handler-removed
  • device-focus-tooltip-room-hover-overwrites
  • device-inbox-batch-rollback
  • household-enter-stops-acting
  • household-marker-drops-keyboard-reach
  • device-markers-rendered-without-keys
  • device-pointer-leave-clears-focus-fallback
  • device-position-cancel-routed-to-commit
  • discovery-reset-writes-a-copy
  • editor-neutral-escape-does-not-exit
  • empty-space-cleanup-disabled
  • fixed-floor-transition-guard-bypassed
  • french-locale-wrong-dictionary
  • hp-dialog-escape-does-not-close
  • junction-limit-baseline-cache-stale
  • junction-limit-candidate-fail-open
  • junction-limit-write-gate-removed
  • locale-failure-toast-dropped
  • marker-reject-keeps-optimistic-candidate
  • marker-rollback-keeps-enqueue-time-revision
  • moon-static-clock-tick-off
  • namespace-loader-returns-english
  • near-axis-authoring-snap-bypassed
  • onboarding-loader-skips-namespace-ensure
  • openings-rendered-without-keys
  • plan-only-preview-label-hidden
  • plan-room-area-icon-hidden
  • plan-room-area-icon-navigates
  • plan-upload-client-limit
  • plan-upload-guard-original
  • plan-upload-reduced-over-limit-staged
  • post-write-tail-runs-on-refused-gate
  • readonly-view-syncs-new-devices
  • render-invalidation-renders-irrelevant-ha
  • reopened-room-from-registry-space
  • room-accept-leaves-coincident-partitions
  • room-gear-second-touch-keeps-drag
  • room-settings-click-does-not-open
  • room-tooltip-off-skips-pointer-modality
  • same-space-room-change-recenters
  • space-create-hidden-display-override
  • stairs-view-pan-opens-target-floor
  • support-invalid-response-leaks-issued-token
  • support-stale-preview-response-revives-consent
  • support-timeout-claims-success
  • vacuum-overlay-back-to-the-dock-space-filter
  • volumetric-kiosk-ignores-setting
  • view-current-space-aria-removed
  • wall-face-apply-skips-overlap-guard
  • wallthick-hit-narrowed
  • writer-history-skips-finished-chain-normalization
  • zigbee-topology-endpoint-cleanup-skipped
  • zigbee-topology-endpoint-elevation-removed
  • zigbee-topology-hovered-endpoint-elevation-removed
  • zigbee-topology-overlay-double-live-projection
  • zigbee-topology-overlay-layer-lowered
  • zigbee-topology-unknown-casing-gaps-filled
  • zigbee-topology-unknown-casing-removed
  • zigbee-topology-unrelated-markers-raised