Files
houseplan-card/scripts/smoke-links.mjs
T
Claude 9b0855b3c0 test: связать предикаты режима со смоками (#588)
`smoke-select` на диффе задачи назвал `displayIsNeutral` и `displayWantsValue`
символами, которых нет ни в одном смоке: смок работает через DOM и по имени
предикаты не находит. Запись в реестре связывает их со свидетелями, которые
проверяют следствия — значение внутри маркера, отсутствие классов состояния и
пульсации, подавленный живой слой пылесоса, поля диалога устройства.

Issue: #588
User-Visible: no
2026-09-18 13:47:16 +03:00

485 lines
28 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: ['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', 'roomTemperatureControls',
],
smokes: ['smoke_room_temperature_thresholds.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'],
smokes: ['smoke_dialog_modal_recovery.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',
},
{
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: [
'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: ['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: [
'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));
}