Files
houseplan-card/scripts/smoke-links.mjs
T
Claudeandclaude[bot] 9324d81b5f perf(iso): keep the theme paper across floor switches in 2.5D (#739)
In 2.5D with a backdrop image every floor switch rendered the card twice
before the first frame. The paper key of the first-frame state (#654)
held the space id, so each switch cleared the ready paper: the first
render inserted the loading veil, updated() probed the computed card
background with a temporary span and asked for a second full update,
which removed the veil again. The colour itself never changed: it is the
theme card background, and no space sets those variables. Locally this
second pass was about 50 ms per warm switch on the large house.

The paper under a backdrop is now resolved once per theme identity
(dark mode, default and dark default theme, theme) and card mode. The
state keeps the resolved paper of the current theme and mode beside the
current paper, so a floor with a backdrop is ready in prepare() when that
paper is known -- also after a drawn floor in between -- and the switch
renders once: no veil, no probe, no second update. A drawn plan keeps its
white paper without the DOM. Any change of the theme identity or the
mode, also one made in Flat or in an editor, drops the kept paper, so the
first backdrop floor after load, a theme change and a trip to an editor
take the #654 path unchanged. isoPaperContext still takes the floor; it
deliberately leaves it out of the identity.

Witnesses: the #739 unit test is red on dev at "a floor switch shows no
veil" and on a key-only variant (space dropped, no theme cache) at
"drawn -> backdrop keeps the known theme paper"; the new
smoke_iso_floor_switch is red on dev (2 updates, 1 colour probe and a
veil insertion in every click task). The iso-paper-resolved-per-floor
mutant puts the floor back into the theme identity.

Issue: #739
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 10:06:38 +00:00

623 lines
36 KiB
JavaScript

/**
* Явные связи «символ продуктового кода → browser-smoke» (#241).
*
* Зачем реестр вообще нужен. Смоки не импортируют исходники: они грузят
* собранный бандл и работают через DOM и приватные поля в `page.evaluate`.
* Поэтому граф импортов их не свяжет, а совпадение идентификаторов находит
* только те смоки, которые называют затронутый символ своим именем. Смок,
* проверяющий тот же контракт через другое поле, не найдётся никаким поиском —
* и ровно на таком смоке #234 потерял регресс.
*
* Что сюда попадает. Связь, которую нельзя доказать текстом: смок проверяет
* следствие контракта, не называя ни одного изменённого символа. Каждая запись
* обязана объяснять, ЧТО именно проверяет смок — иначе реестр превращается в
* список суеверий, который никто не решается почистить.
*
* Чего сюда писать НЕ надо. Связи, которые и так находятся по имени: запись
* рядом с прямым совпадением делает вывод selector'а неотличимым от «нашли
* потому что записали», и тест начинает проходить по неверной причине.
*/
/**
* @typedef {object} SmokeLink
* @property {string[]} symbols Изменённые символы, включающие связь.
* @property {string[]} smokes Файлы в `demo/`, без пути.
* @property {string} because Что смок проверяет и почему поиском не найдётся.
*/
/** @type {SmokeLink[]} */
export const SMOKE_LINKS = [
{
symbols: [
'isoPlaneMatrix', 'unprojectFloorPoint', '_convertProjectionView', '_rezoom',
'_warmAdoptViewport', '_logicalViewCenter', '_floorView', 'resolveIsoOverlayFitEnvelope',
'buildIsoOverlayRenderScene',
],
smokes: ['smoke_iso_flat_parity.mjs', 'smoke_isometric_contract.mjs'],
because: '#713: the Flat-plane floor, the one straight-up tile rise and the kept camera are '
+ 'observed only as CSS pixels of rooms, decor, labels and tile anchors across a setting '
+ 'switch, an editor entry and a warm remount; no smoke names these helpers',
},
{
symbols: [
'moonLayer', 'renderMoon', 'moonTick', 'moonView', 'moonShownAt', 'moonPhasePath',
'moonPosition', 'moonIllumination', 'moonFingerprint', 'renderDayCycleEnvironment',
'withMoon', 'moonSkyState', 'dayCycleClock', 'renderMoonSky', '_moonSkyState', '_dayCycleClock',
'moonStatus', 'moonStatusOf', 'openMoonStatus', 'moonStatusText', '_reviveMoonStatus', '_openMoonStatus',
],
smokes: ['smoke_moon.mjs', 'smoke_moon_static.mjs', 'smoke_moon_status.mjs', 'smoke_daycycle_layer_budget.mjs'],
because: '#661: the moon is observed only as `.hp-moon` in the production bundle — the lazy '
+ 'chunk arriving at night, the 30 s ticker and state updates carrying it through 3° with the '
+ '2 s fade, the plan painting over it pixel for pixel, and no extra composited layer (CDP); '
+ '#718: the same over a static background as `.hp-moon-sky` (the clock ticker at 08:00 and '
+ '18:00, tab and background switches, the #101 View weight, houseplan-space-card) and the '
+ 'General settings line as `[data-moon-status]` (delayed, refused and preloaded chunk); '
+ '#731: that line in a dialog revived after a warm remount (View, plan editor, chunk loading); '
+ 'no smoke names the functions',
},
{
symbols: ['memoIsoLightFloorRooms', 'IsoFirstFrameState', 'isoPaperContext', 'commitPaper', 'themePaper'],
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: ['IsoFirstFrameState', 'isoPaperContext', 'commitPaper', 'themePaper'],
smokes: ['smoke_iso_floor_switch.mjs'],
because: '#739: a warm floor switch between backdrop floors in 2.5D is observed only in the '
+ 'production bundle — one card update, no computed-colour probe and no veil insertion in '
+ 'the click task, the light floors of the first show; no smoke names the paper state',
},
{
symbols: ['baseStyles', '_containerOwnedHeight', 'measuredCardHeaderHeight',
'settleSoftStageLayout'],
smokes: ['smoke_sections_resize.mjs'],
because: '#648: the production-bundle witness measures the HA Sections slot, card, header '
+ 'and stage through repeated row resizes and every editor transition; the CSS height chain '
+ 'and helper boolean do not otherwise share a runtime symbol with those DOM measurements',
},
{
symbols: ['chromeStyles', 'dialogsStyles'],
smokes: ['smoke_toolbar_stable_width.mjs', 'smoke_editor_tabs.mjs', 'smoke_mobile_view_header.mjs'],
because: '#647: the header X slot (.editor-close-slot) and the removed .head .count rule are '
+ 'CSS in these sheets; the smokes measure the rendered header width, tab offsets, the X '
+ 'hit target and its visibility at 1400/1000/768/390 px — no TS symbol names that layout; '
+ '#616: the phone one-row header rules live in chromeStyles too',
},
{
symbols: ['HeaderMenu', 'headerMenuItems', 'renderHeaderActions'],
smokes: ['smoke_mobile_view_header.mjs', 'smoke_support_feedback.mjs', 'smoke_gear_tabs.mjs'],
because: '#616: the one-row phone header (<= 480 px) and its gear menu are a media query plus '
+ 'these symbols; the smokes tap the real gear and items at 320/390 px and prove >480 px is unchanged',
},
{
symbols: ['confirmIcon', 'HpConfirmRequest'],
smokes: ['smoke_discard_copy.mjs', 'smoke_dialog_polish_603.mjs'],
because: '#610: the production-bundle witnesses open all four unsaved-settings '
+ 'confirmations, verify both rendered glyphs and actions, then prove an unrelated warning '
+ 'still uses the fallback lock glyphs; source imports cannot prove the rendered dialog',
},
{
symbols: ['rangeLine', 'committedRangeLineValue'],
smokes: ['smoke_range_line_draft.mjs', 'smoke_dialog_config_parity.mjs'],
because: '#608: the production-bundle witnesses type partial numeric text into real settings '
+ 'dialogs, observe that host drafts stay unchanged until commit, and verify field/slider '
+ 'synchronisation; neither browser smoke imports the form-kit helper',
},
{
symbols: ['rangeLine', 'unitInput'],
smokes: ['smoke_room_settings_form.mjs', 'smoke_device_settings_form.mjs', 'smoke_space_settings_form.mjs'],
because: '#639: the three settings-form smokes drive range-line numbers (room name/label scale, '
+ 'device brightness/size/angle, space card font) through the DOM and assert the host draft; '
+ 'they never name the form-kit helper, so #608 changed the commit event and only the nightly '
+ 'registry saw them turn red',
},
{
symbols: ['canonicalFurnitureId'],
smokes: ['smoke_furniture.mjs'],
because: '#606: the bundled furniture palette and saved cactus object are checked through '
+ 'the browser DOM; the smoke cannot import the read-only alias resolver by name',
},
{
symbols: ['formKitCss', 'renderRoomSettingsDialog'],
smokes: ['smoke_dialog_polish_603.mjs'],
because: '#603: the bundled form-kit track/knob box model, localized discard confirmation '
+ 'and headingless room card are measured in a browser; source imports cannot prove '
+ 'the rendered geometry or that both actions fit the real modal footer',
},
{
symbols: ['displayIsNeutral', 'displayWantsValue'],
smokes: ['smoke_static_icon.mjs', 'smoke_device_preview_parity.mjs'],
because: '#588: два предиката режима наблюдаются как следствия — текст значения внутри '
+ 'маркера, отсутствие классов состояния и пульсации, подавленный живой слой пылесоса, '
+ 'поля и предупреждения диалога устройства. Ни один смок не называет предикаты: они '
+ 'живут внутри бандла, а смок работает через DOM',
},
{
symbols: ['roomCustomFillOf'],
smokes: ['smoke_space_settings.mjs', 'smoke_room_settings.mjs'],
because: '#581: the room-colour projection (own custom mode carries the colour, an orphan paints '
+ 'the space) is observed through `.room` style variables in View and through the room-dialog '
+ 'draft; neither smoke calls the pure function by name',
},
{
symbols: ['renderPaperShapes'],
smokes: [
'smoke_daycycle_layer_budget.mjs', 'smoke_daycycle_raster.mjs',
'smoke_live_pan_coverage.mjs', 'smoke_bg_color.mjs', 'smoke_smooth_zoom.mjs',
],
because: '#582: production-bundle witnesses prove that camera movement activates the '
+ 'stage-bounded day-cycle silhouette for direct and animated camera paths at 1 cm/point, '
+ 'keeps the #532 raster budget and '
+ 'follows the same #579 live viewport as the visible plan; source imports cannot expose '
+ 'CDP layers',
},
{
symbols: [
'commitHouseplanViewport', 'isIdentityLiveLayerProjection', 'paintLiveViewport',
],
smokes: ['smoke_live_pan_coverage.mjs'],
because: '#579: the production-bundle witness observes promotion through budgeted viewBox '
+ 'refreshes and active Lit commits via scene style attributes; it intentionally does not '
+ 'call the viewport helpers whose compositor lifecycle it guards',
},
{
symbols: ['_renderDevice', '_renderOpenings'],
smokes: ['smoke_space_switch_transitions.mjs'],
because: '#525: the smoke never names these renderers — it switches spaces the ordinary way '
+ 'and reads running transitions off the nodes they produced. The contract it guards is '
+ 'list identity: a keyless list reuses a node by position, and the leaf of the next space '
+ 'then animates from the previous door\'s angle. Any edit to either renderer can drop the '
+ 'key without a single identifier of the smoke changing',
},
{
symbols: ['LoadedSummaryPanelRuntime', 'SummaryPanelPresentation', 'renderSummaryPanelEditor',
'summaryPanelCss', 'summaryPanelEditorCss', 'summaryPanelDialogCss', 'summaryIcon'],
smokes: ['smoke_summary_panel_polish.mjs'],
because: 'the #505 browser witness samples retained exit DOM and actual opacity/translation, '
+ 'reversal and lifecycle boundaries, then measures the native modal shell and drives '
+ 'dependent draft toggles without rewriting unrelated per-card scale preferences',
},
{
symbols: [
'confirmedSummaryPanelWriteRecovery', 'summaryPanelEntityIds',
'adoptAuthoritativeGated', 'LoadedSummaryPanelRuntime',
],
smokes: ['smoke_summary_panel.mjs'],
because: 'the #490 production-bundle sequence proves that a lost config/set ACK adopts the '
+ 'whole authoritative document and revision before a second write, and that a summary-only '
+ 'HA state row drives the real panel without rendering unrelated ticks or rebuilding geometry',
},
{
symbols: [
'roomTempRangeOf', 'roomTempRangeFromDraft', 'roomTempThresholdDraft',
'applyRoomTempThresholdDraft', 'renderRoomSettingsDialog',
],
smokes: ['smoke_room_temperature_thresholds.mjs', 'smoke_room_settings_form.mjs'],
because: 'the #487 production-bundle scenario proves partial inheritance, zero/invalid input, '
+ 'hidden draft retention, reset and narrow-dialog overflow through the real room editor',
},
{
symbols: [
'PdfExportRuntime', 'buildPdfPage', 'writePdf', 'stableDimensionEdges',
'pdfSegmentTouchesGeometry', 'SegmentIntersection', 'segmentIntersection',
'COORDINATE_EPSILON', 'PdfCollisionPoint', 'forEachRingSegment',
'pointOnSegment', 'segmentParameter', '_openPdfDialog', '_renderPdfDialog',
],
smokes: ['smoke_pdf_export.mjs'],
because: 'the #53 production-bundle scenario proves the administrator button, cold-View '
+ 'lazy boundary, real modal, remembered options, browser download and parseable A4 output; '
+ 'its #484 stepped-facade pass additionally proves the complete external dimension chain '
+ 'through the shipped collision helper; those browser and network effects are outside '
+ 'the pure PDF scene and writer tests',
},
{
symbols: [
'DEFAULT_LABELS', '_closePicker', '_commitHex', '_confirm',
'_hexInvalid', '_hexNeedsValidInput', '_surface',
],
smokes: [
'smoke_color_picker.mjs', 'smoke_color_picker_consumers.mjs', 'smoke_help_affordance.mjs',
],
because: 'the #476 production-bundle scenarios prove the full-width localized confirmation, '
+ 'live-event count, invalid-HEX latch, focus and click containment in both the native '
+ 'popover and the picker surface inside a general-settings dialog; those browser '
+ 'lifecycle effects are not visible to source or pure color conversion tests',
},
{
symbols: ['HpDialog', '_ensureNativeModal', '_useHaDialog', '_usesHaDialog', 'dialogsStyles'],
smokes: ['smoke_dialog_modal_recovery.mjs', 'smoke_ha_form_shell_parity.mjs'],
because: 'the #463 production-bundle scenario observes native top-layer state, geometric '
+ 'centring, ::backdrop, update/reconnect recovery and late ha-dialog registration; none '
+ 'of those browser lifecycle effects can be proved from the Lit source alone. The #609 '
+ 'scenario separately exercises the public HA sizing tokens, one HA body scroller, form '
+ 'canvas and responsive fullscreen geometry used by all five form-shell consumers',
},
{
symbols: [
'commitDraftSegmentGeometry', 'draftLiveCandidateSpace', 'draftLiveSeed',
'isSingleDraftAppend', 'DraftLiveProjection', 'DraftLiveCommitRuntime',
],
smokes: ['smoke_wall_draw_click.mjs'],
because: 'the #461 production-bundle scenario proves local rejection rollback and exact '
+ 'Undo/Redo identity through the real Walls click handler; those browser effects are '
+ 'outside the pure projector and transaction source contracts',
},
{
symbols: [
'commitWallChainFinishGeometry', 'finalizeWallChainSpace',
'_finalizeWallChainPartitions', '_canonicalWallChainHistoryState',
'rewriteMarkerRoomReferences', 'restoreMarkerRoomReferences',
],
smokes: ['smoke_writer_fixed_point.mjs'],
because: 'the #477 production-bundle scenario proves that a finished chain and its later '
+ 'Undo/Redo states remain Optimize no-ops, and that Delete room updates and restores '
+ 'direct and cross-space vacuum references through the real editor history transaction',
},
{
symbols: [
'VersionRecoveryController', 'VersionReloadSafetySnapshot',
'isVersionReloadSafe', 'compareRuntimeVersions', 'normalizeRuntimeVersion',
'fetchAuthoritativeConfig', 'getAuthoritativeCardConfig', '_getAuthoritativeConfig',
'adoptCardConfigCapabilities',
'cardVersionReloadSafetySnapshot', '_versionReloadSafetySnapshot',
'renderVersionBanner', '_renderVersionBanner',
],
smokes: ['smoke_version_recovery.mjs'],
because: 'the #462 production-bundle scenario proves the card-level overlay, trusted '
+ 'keyboard/touch reload seam, exact session target, concrete card dialog/write/gesture '
+ 'guards, config capability adoption before sibling failures, more-info pause and the '
+ 'full-card/space-card boundary; pure controller tests '
+ 'cannot observe those DOM and Home Assistant lifecycle effects',
},
{
symbols: [
'LiveEditorState', 'finishRevision', 'hoverProperties',
'routeHouseplanEditorUpdate', 'whenHouseplanEditorSettled',
],
smokes: ['smoke_furniture.mjs', 'smoke_decor.mjs', 'smoke_decor_text.mjs'],
because: 'the #460 production-bundle scenarios consume the internal settlement contract '
+ 'through the lazy editor runtime and prove that furniture hover cleanup, preview/commit '
+ 'parity and decor gestures observe the latest coalesced live-editor frame',
},
{
symbols: [
'renderZigbeeTopologyOverlay', 'resolveTopologyHover', 'mapTopologyNodes',
'readZhaTopology', 'refreshZ2mTopology', 'zigbeeTopologySettingsOf',
],
smokes: ['smoke_zigbee_topology_hover.mjs'],
because: 'the #54 production-bundle scenario proves default-off lazy loading, explicit provider '
+ 'refresh, incident-only mouse hover, cross-space counting, pointer transparency and immediate '
+ 'cleanup across leave, touch, editor and non-admin boundaries',
},
{
symbols: [
'beginVacuumRouteDraft', 'chooseVacuumRouteSpace', 'commitVacuumRouteDraft',
'VacuumRouteDraft', 'VacuumMapsHost', 'VacuumMapsCardHost',
],
smokes: ['smoke_vacuum_route_draft.mjs'],
because: 'the #441 production-bundle scenario drives both add-map controls, first-route '
+ 'defaulting, required floor selection, cancel, a rejected config write with rollback and '
+ 'a successful retry; those UI and transport effects are outside the pure draft helpers',
},
{
symbols: ['showRoomTooltipOf'],
smokes: ['smoke_room_tooltip_toggle.mjs'],
because: 'the production-bundle scenario proves the global-settings draft and persistence, '
+ 'room-only suppression before area work, immediate stale-room-tip cleanup, preserved room '
+ 'highlight/device tooltip and default-on restoration (#426)',
},
{
symbols: [
'applyDevicePlacement', 'devicePlacement', 'sameDevicePlacement',
'DevicePlacement', 'DevicePositionState',
],
smokes: ['smoke_device_position_history.mjs'],
because: 'the #74 production-bundle scenario drives real Device editor pointer events and '
+ 'observes transaction boundaries, point-wise update/delete, toolbar history, native field '
+ 'shortcuts, cancellation and revision invalidation; none of those browser effects can be '
+ 'proved by the pure placement helpers alone',
},
{
symbols: [
'resolveValueSource', 'valueSourceWriteFields', 'valueBadgeCandidates',
'valueBadgeSourceKey', 'resolveDevicePresentation',
],
smokes: ['smoke_value_face_source.mjs'],
because: 'the #378 production-bundle scenario observes the shared source resolver through '
+ 'the real Device dialog, draft preview, persistence, both plan renderers, unavailable '
+ 'recovery, action dispatch and binding reset; those DOM effects do not call the pure '
+ 'source helpers by name',
},
{
symbols: ['roomClimateKey', 'roomClimateMap'],
smokes: ['smoke_room_climate_placement.mjs'],
because: 'the #317 production-bundle scenario proves one manually placed sensor through '
+ 'room label, tooltip, temperature fill and hosted Static; those DOM surfaces cannot call '
+ 'the pure room-target helpers directly',
},
{
symbols: [
'LanguageRuntime', 'LanguageRuntimeContract', 'languageRenderGate',
'dictionaryFor', 'ensureLanguage',
],
smokes: ['smoke_german_locale.mjs', 'smoke_french_locale.mjs'],
because: 'the production-bundle scenario observes the lazy German chunk through network '
+ 'requests and four root render gates: neutral cold frame, atomic German commit, page-cache '
+ 'reuse and the bounded retry-to-English path cannot be proven from pure registry tests (#348)',
},
{
symbols: [
'namespaceLanguageRuntime', 'composeLanguageRuntimes', 'surfaceLanguageRuntime',
'SETTINGS_LANGUAGE_RUNTIME', 'SUPPORT_LANGUAGE_RUNTIME', 'TOPOLOGY_LANGUAGE_RUNTIME',
'EDITOR_LANGUAGE_RUNTIME', 'ONBOARDING_LANGUAGE_RUNTIME', 'settingsT', 'supportT', 'topologyT',
],
smokes: ['smoke_lazy_admin_locale.mjs', 'smoke_dialog_segments_i18n.mjs'],
because: 'the ru/de/fr namespace dictionaries are separate lazy chunks: only the production '
+ 'bundle shows the per-frame dialog text on a cold open, the held frame on a live switch, '
+ 'the bounded retry-to-English path and the exact chunk requests of each surface (#627)',
},
{
symbols: [
'probeBackdrop', 'classifyPlanFile', 'downscaleBackdrop', 'renderBackdropGuard',
'WARN_DECODED_BYTES', 'HARD_DIMENSION', 'DOWNSCALE_TARGET_PX',
],
smokes: ['smoke_backdrop_guard.mjs'],
because: 'the production-bundle scenario proves the guard end to end: header-only warning '
+ 'with zero decode calls, byte-identical keep-original path, a real 6200px reduce to 4096 '
+ 'with alpha preserved, the cancel-only hard dialog, the honest phase-2 failure (toast, '
+ 'clean staging, no silent original upload) and the SVG bypass (#39)',
},
{
symbols: [
'stagePlanFile', 'uploadPlanFile', 'renderPlanBackdropGuard',
'MAX_PLAN_BYTES', 'MAX_PLAN_MB', 'PLAN_UPLOAD_PATH',
],
smokes: [
'smoke_plan_upload_limit.mjs', 'smoke_plan_upload_race.mjs',
'smoke_plan_upload_reject.mjs', 'smoke_backdrop_guard.mjs',
],
because: '#617: the smokes drive _pickPlanFile/_saveSpaceDialog of both runtimes on the '
+ 'production bundle and observe the transport (one multipart POST, zero plan/set), the '
+ 'pre-send limit toast, the reduced-copy-only guard and the 413 text; none of them names '
+ 'the shared helpers, so a name search cannot find them',
},
{
symbols: ['vacMapIdWithFallback', 'vacMapIdFromAttrs', 'readVacTelemetry'],
smokes: ['smoke_cold_view_vacuum.mjs'],
because: 'the telemetry-bearing cold scenario proves the vacuum render path (willUpdate '
+ 'snapshot, map-id fallback with selected_map: 0, live position frames) survives without '
+ 'the lazy editor runtime — the demo mower has no position attributes, so every other '
+ 'smoke skips this branch (#358)',
},
{
symbols: ['TAP_ACTIONS', 'DeviceTapAction'],
smokes: ['smoke_tap_ctx.mjs'],
because: 'the Device editor exposes the canonical ordered action list and the same scenario '
+ 'proves explicit none consumes pointer and keyboard activation without UI, feedback or '
+ 'HA side effects (#381)',
},
{
symbols: [
'resolveToggleIntent', 'toggleOperation', 'projectedTapAction',
'toggleOriginOf', 'resolvedLightSources',
],
smokes: ['smoke_cold_view_toggle.mjs'],
because: 'the cold-view scenario proves a plain tap resolves and executes WITHOUT the lazy '
+ 'editor runtime: a wall switch drives three passive virtual lamps, a controlled lamp '
+ 'drives its switch, tap_confirm renders and confirms, and no editor chunk is requested '
+ '(#357 — every warm smoke preloads the runtime and cannot see this class)',
},
{
symbols: ['quantizeKeyCoord', 'INCIDENT_EPS', 'KEY_FACTOR'],
smokes: ['smoke_junction_limits.mjs'],
because: 'the smoke proves the write barrier verdicts (refusal toasts, resize stop, '
+ 'fail-closed candidate) through the rendered card, never naming the key-quantisation '
+ 'internals that decide which nodes are one node (#331 §2.1)',
},
{
symbols: [
'EditorRuntimeLoader', 'EditorRuntimeLoaderState', 'EditorRuntimeModule',
'EditorRuntimeLoaderFailure', 'lazyLoadFailureMessage',
'EDITOR_RUNTIME_FINGERPRINT', 'createHouseplanEditorRuntime',
],
smokes: ['smoke_lazy_editor_chunk.mjs', 'smoke_entry_stale.mjs'],
because: 'the production-bundle scenarios prove that the content-hashed editor chunk stays '
+ 'off the View network path, is shared by all three editors, retries once per cycle, '
+ 're-arms after a network failure so the next press heals (#353), refuses an incompatible '
+ 'build before editor state is installed (#337), and that a stale cached entry shows a '
+ 'localized reload panel instead of dying silently (#353)',
},
{
symbols: [
'ROOM_GEAR_DRAG_THRESHOLD_PX', 'roomGearPointAllowed', 'roomGearAutoCenter',
'resolveRoomGearCenter', 'roomGearDragMoved', 'clampRoomGearPointAlongPath',
'RoomGearDragController', '_cancelRoomGearForMultitouch',
],
smokes: ['smoke_room_gear_drag.mjs', 'smoke_resize_labels.mjs'],
because: 'the #645 production gestures prove temporary room-button movement, click/drag '
+ 'arbitration, room-boundary clamping, cancel/multitouch rollback and session reset; the '
+ 'Resize smoke proves its live area labels avoid the actually rendered room button',
},
{
symbols: [
'placeResizeAreaLabel', 'resizeInwardNormal', 'resizeMeasuredEdges',
'ResizeAreaPlacement', 'ResizeAreaPlacementInput', 'ResizeLabelView',
],
smokes: ['smoke_resize_labels.mjs'],
because: 'the #300 production gesture observes the pure projection only through the '
+ 'rendered side-wall highlights, area/leader DOM and actual gear rectangles at current zoom',
},
{
symbols: [
'classifyNearAxisSegment', 'snapNearAxisEndpoint', 'repairNearAxisRoomWalls',
'NEAR_AXIS_MAX_DEGREES', 'NEAR_AXIS_MAX_SLOPE',
],
smokes: ['smoke_plan_drawing_repairs.mjs', 'smoke_near_axis_optimize.mjs'],
because: 'the production bundle must make the Walls hover/click candidate exact-axis, then '
+ 'preview one deduplicated real shared wall, pass production geometry preflight, apply one '
+ 'atomic Optimize write, reload idempotently and restore the original through one Undo (#290)',
},
{
// #297: обход не проверяет снимок — он расшатывает план продуктовыми жестами
// и спрашивает инварианты после каждого. Поиском по тексту связь между этим
// смоком и кодом ресайза, удаления комнаты и «Оптимизировать» не находится.
symbols: [
'resolveSafeResize', 'applySafeResize', 'clampSafeResize', 'validateSafeResize',
'planRoomDeletion', 'materializeWallIntervals', 'optimizePlans',
'reconcileCoincidentPartitions',
],
smokes: ['smoke_edit_walk.mjs'],
because: 'дефекты геометрии рождаются в редактировании, а не в хранении: #289, #296 и '
+ '#298 прошли все снимковые гейты — обход находит их на первом-втором жесте',
},
{
// #285: смок не произносит ни одного имени продуктового кода — он подставляет
// геометрию реального этажа и спрашивает сам продукт через isPointInFill,
// есть ли кладка там, где модель её обещает. Поиском по тексту такая связь
// не находится никогда.
symbols: [
'wallBodiesGeometry', 'wallEdgeBodies', 'buildMultiWallNodeMap',
'multiWallBevelTriangles', 'virtualJunctionPatches', 'linearWallJoinPatches',
'MITRE_LIMIT', 'MULTI_WALL_JOIN_LIMIT', 'insetContour', 'atomicPolyForRoom',
],
smokes: ['smoke_real_plan_masonry.mjs'],
because: 'на реальном плане кладка рвётся там, где синтетические фикстуры целы: '
+ 'восемь закрытых задач по стыкам вошли в beta.9, а разрыв в 45 шагов остался — '
+ 'пиксельные пороги golden такую долю кадра не видят',
},
{
symbols: [
'WallGeometryStatus', 'WallGeometryComponent', 'WallBodiesGeometryResult',
'checkSpacePhysicalGeometry', 'spacePhysicalGeometryFingerprint',
],
smokes: ['smoke_wall_union_isolation.mjs'],
because: 'the #278 production bundle must paint isolated canonical components in Plan, '
+ 'View, Static, hidden Iso and light barriers, reject a physical write with zero WS/Undo, '
+ 'and still allow a non-geometry edit on the same degraded legacy space',
},
{
symbols: [
'resolveSafeResize', 'applySafeResize', 'validateSafeResize', 'clampSafeResize',
'safeResizePointerDisplacement', 'SafeResizePlan', 'SafeResizeResolution',
'SafeResizeReason', 'SafeResizeObstacle',
],
smokes: ['smoke_room_resize.mjs', 'smoke_resize_pointer_real_plan.mjs'],
because: 'the #277 synthetic scenario covers fixed topology, first-corner clamp and disabled '
+ 'accessibility, while #293 recreates the card from config/get and drives real browser mouse '
+ 'events on the tracked second-floor plan; pure helpers are hidden behind both controllers',
},
{
symbols: ['reconcileCoincidentPartitions', 'CoincidentPartitionResult'],
smokes: ['smoke_optimize_coincident_partition.mjs'],
because: 'the production-bundle scenario observes the Optimize report, atomic Apply, reload, '
+ 'one-shot Undo and the resulting Boundary/Thickness targets, while the pure reconciliation '
+ 'helper is not exposed on the card instance (#276)',
},
{
// #263: пикер называет себя (`_bindingCandidates`) и потому находится
// прямым совпадением. Регистрируются только чистые помощники надгробий:
// смок не произносит ни одного их имени — он видит лишь список, который
// они отфильтровали. Проверено зондом: правка одного `src/devices.ts` без
// этой записи не выбирает ни одного смока.
symbols: ['removedPlanBindings', 'isRemovedPlanEntity', 'deletePlanMarkerRecords'],
smokes: ['smoke_binding_picker.mjs'],
because: 'смок проверяет, что удалённая привязка снова предлагается в списке «Добавить», '
+ 'что отдельные сущности устройств живут за галкой и что надгробие устройства '
+ 'по-прежнему прячет его дочерние сущности (#262) — всё это следствия фильтров '
+ 'надгробий, чьи имена в браузерный сценарий не попадают',
},
{
// #261: the browser observes the canonical wall/paper paths and downstream
// geometries, while the pure bevel helpers remain internal to the bundle.
symbols: [
'multiWallBevelTriangles', 'multiWallBevelTrianglesAt',
'buildMultiWallNodeMap', 'MultiWallNodeRay', 'MultiWallNodeRaySupport',
'multiWallProtectedRayIndexes', 'multiWallProtectedStripGeometry',
'multiWallProtectedMapGeometry', 'multiWallEffectiveCutGeometry',
],
smokes: [
'smoke_junction_patch_resilience.mjs', 'smoke_multiwall_junction.mjs',
'smoke_multiwall_strip_containment.mjs',
],
because: 'the #197 smoke probes the repaired exterior wedge across Plan, View, kiosk, '
+ 'Static, hidden Iso and light barriers, while the #249 smoke proves the old excessive '
+ 'wedge stays empty, #271 also proves finite ray endpoints, and #275 densely samples every '
+ 'protected orthogonal strip across Plan, Static, hidden Iso and light barriers; the browser '
+ 'scenarios call none of the pure helpers by name',
},
{
// #258: the browser sees only the resulting path/caches; it cannot call
// the pure identity helpers by name through the production bundle.
symbols: ['wallKey', 'lookupWall', 'canonicalKeyCoordinate', 'keyEpsilon'],
smokes: ['smoke_wall_key_roundtrip.mjs'],
because: 'the affected and canonical midpoint keys must produce the same T-junction in '
+ 'Plan, View, kiosk, Static, hidden Iso, clean-floor and light-barrier consumers',
},
{
// #253: the pure interval transformer is invoked inside the resize preview;
// the smoke can observe only the resulting wall records and rendered body.
symbols: [
'rekeyWallsAfterMove', 'WallEntry', 'angleClose', 'clampWallCm',
'closePoint', 'distToSeg', 'keyOf', 'pointAt', 'segAngle', 'wallDir', 'wallEntry',
],
smokes: ['smoke_resize_wall_thickness.mjs', 'smoke_resize_pointer_real_plan.mjs'],
because: 'the real pointer-handler smoke proves that a partial overlap is split losslessly '
+ 'through live preview, commit, opening movement and Undo, but the browser bundle does '
+ 'not expose the pure transformer name to the scenario',
},
{
// #244: pure repair/validation helpers are bundled behind editor actions;
// the smoke observes their persisted candidate and rendered explanations.
symbols: [
'repairSpaceReferences', 'collectSpaceMarkerDependencies',
'createSpaceDeletionCandidate', 'invalidDefaultFloor',
'ImportLineageRoot', 'canonicalImportRoot', 'reversibleStem', 'addCandidate',
],
smokes: ['smoke_orphan_space_references.mjs'],
because: 'the production-bundle smoke proves the Optimize candidate, live marker rebuild, '
+ 'nested import-lineage collapse, delete blocker and card-editor warning, while those UI '
+ 'surfaces do not expose the pure helper names at runtime',
},
{
// #242: pure placement is called inside shared SVG/Iso renderers, while
// browser smokes can only inspect the resulting transforms and bases.
symbols: ['openingSymbolOffset'],
smokes: ['smoke_isometric_contract.mjs', 'smoke_wall_thickness.mjs'],
because: 'wall-thickness smoke proves the default and saved-flip SVG translations; '
+ 'isometric smoke inspects door/window/gate bases from the same helper, but neither '
+ 'browser bundle exposes the helper name in its test steps',
},
{
// #234: единый резолвер толщины отрезка цепочки. Смок перехода между
// толщинами не называет ни `chainSegmentCms`, ни `_wallChainSegmentCms` — он
// рисует стены инструментом и проверяет, что кладка на стыке толщин
// остаётся связной. Общих с диффом символов у него ровно два, `_cellCm` и
// `_gridPitch`, и оба слишком широкие, чтобы что-то доказывать.
symbols: ['chainSegmentCms', 'wallChainSegments', '_wallChainSegmentCms', '_closingWallCm'],
smokes: ['smoke_wall_thickness_transition.mjs'],
because: 'переход между двумя толщинами в одной цепочке: резолвер толщины '
+ 'отрезка решает, где кладка меняет глубину, а смок проверяет кладку, '
+ 'а не толщину, и ни одного имени из резолвера не называет',
},
];
/** Смоки, связанные с изменёнными символами через реестр. */
export function registeredSmokes(changedSymbols) {
const changed = new Set(changedSymbols);
const out = new Map();
for (const link of SMOKE_LINKS) {
const hit = link.symbols.filter((symbol) => changed.has(symbol)).sort();
if (!hit.length) continue;
for (const smoke of link.smokes) {
const entry = out.get(smoke) || { smoke, symbols: [], because: [] };
entry.symbols = [...new Set([...entry.symbols, ...hit])].sort();
if (!entry.because.includes(link.because)) entry.because.push(link.because);
out.set(smoke, entry);
}
}
return [...out.values()].sort((a, b) => a.smoke.localeCompare(b.smoke));
}