docs(process): ролевые конспекты, замер входа, Snapshot генерируется, TESTING.md разделён

Вход агента до первого файла кода стоил ≈ 26 700 слов (аудит 22.09).

- docs/process/AUTHOR.md и REVIEWER.md — выжимки PROCESS.md: каждый пункт
  ссылается на раздел канона, ключевые формулировки дословные;
  test/process-digests.test.mjs сверяет якоря, ссылки и правила.
- scripts/entry-cost.mjs — маршрут чтения по роли и бюджет (автор ≤ 12 000
  слов, AC1); AGENTS.md «Read this first» называет те же маршруты.
- docs/STATUS.md: блок Snapshot генерирует scripts/status-snapshot.mjs
  (версии — release-contract, счётчики — inventory, теги — git); feature
  surface и ранние milestones перенесены дословно в docs/STATUS-FEATURES.md.
- docs/TESTING.md — действующая инструкция (684 строки, AC3); ручные
  чек-листы и приложения по issue перенесены дословно в docs/testing-notes/
  с индексом и тестом на полноту.
- Промпт ревьюера в process.yml читает конспект вместо пересказа правил;
  машинные требования (строка вердикта, REVIEW_DOC, запрет fetch, таблица
  «чем краснеет», разделы повторного раунда) сохранены и закреплены тестом.
- PROCESS.md: правила не менялись; добавлены ссылка на конспекты в шапке и
  уточнение в §10.4, что ревьюер конвейера читает конспект.
- 7 мутантов в реестре.

Issue: #634
User-Visible: no
This commit is contained in:
Claude
2026-09-24 02:33:08 +00:00
committed by claude[bot]
parent fe92ce067a
commit 7dc7597260
30 changed files with 5590 additions and 4352 deletions
+391
View File
@@ -0,0 +1,391 @@
# Устройства и маркеры
> Приложение к [`docs/TESTING.md`](../TESTING.md): перенесено оттуда дословно (#634).
> Индекс всех приложений — [`README.md`](README.md).
## Device icon design package (#179)
- [ ] Pure presentation tests cover lock/unlock, exact marker-only LQI bands
`0/40/41/179/180`, unchanged room gradient, package pulse defaults,
semantic colors and reduced motion
[unit: `device-presentation.test.mjs`, `device-pulse.test.mjs`].
- [ ] Shared face tests cover shell/core DOM, four Double positions, a third
legacy section, deterministic font fitting, full text and safe CSS color
variables [unit: `device-face.test.mjs`].
- [ ] The browser renders the exact shell ratio and shadow color, Light/Dark
cores without backdrop blur, state/LQI colors, 3.6 s presence pulse,
unavailable no-hover, full Text/Double values, 44×44 target, View and
Device-editor keyboard paths, and no Plan tab stop
[auto: `smoke_device_icon_design.mjs`].
- [ ] Full plan, preview and static card preserve the same face after the DOM
redesign; static mode, state/value and disabled-device contracts stay
green [auto: `smoke_device_preview_parity.mjs`, `smoke_static_icon.mjs`,
`smoke_state_value.mjs`, `smoke_disabled_device.mjs`].
- [ ] Restoring unavailable hover, shifting the LQI boundary, restoring value
ellipsis or bypassing `_clickDevice()` makes its guard red
[mutation: `device-unavailable-hover-restored`,
`device-marker-lqi-low-boundary-shifted`,
`device-long-value-ellipsis-restored`,
`device-keyboard-bypasses-click-path`].
- [ ] Pre-beta golden reviews desktop/mobile Light/Dark states, combo states,
Text, four Double positions, long and legacy values, LQI, reduced motion,
sizes 32/56/96 and colored backgrounds. Full smoke/golden/performance
remains a Linux release gate.
## Dense device-marker hit ownership (#564)
- [ ] Pure geometry covers visible capsule priority, invisible 44 px-floor
overlap, nearest-core selection, stable exact ties, stadium corners and
spatial-index locality [unit: `device-hit-owner.test.mjs`].
- [ ] Source contracts keep painted shells globally above transparent floors,
route hover/click/pointer lifecycle through one semantic owner and forbid
layout reads from the pointer-move path
[unit: `device-hit-owner-contract.test.mjs`].
- [ ] Household J7 checks the five-marker dense column at tablet and phone
widths: every visible centre has the same native and semantic owner, its
click opens that marker, and a pointer sequence remains latched through
its terminal click [auto: `smoke_household_journeys.mjs`].
- [ ] Real browser geometry covers Icon, Text, Double and untouched legacy
faces at all four cardinal sides/directions. In every case a painted
point overlaps only the neighbour's invisible 44 px floor and still owns
the native hit, semantic owner and click
[auto: `smoke_device_hit_capsules.mjs`].
- [ ] Replacing nearest-screen-core selection with first-input/DOM order makes
both the pure geometry guard and the real rendered-face browser guard red
[mutations: `dense-device-hit-falls-back-to-input-order`,
`dense-device-hit-browser-skips-painted-priority`].
## Device marker polish and pointer modality (#212)
- [ ] Shared icon geometry applies one 0.9 visual factor after card/per-marker
sizing, keeps the saved centre and 44×44 hit floor unchanged, and gives
wide Text cores a radius equal to half their height
[unit: `device-marker-polish-contract.test.mjs`; auto:
`smoke_device_icon_design.mjs`].
- [ ] Only a genuinely dispatched toggle/run action produces one
`1 → .95 → 1` feedback cycle lasting 200 ms. Info, editor, confirmation
before acceptance, unavailable/secure/no-target and cancelled gestures do
not; reduced motion has no scale tween
[auto: `smoke_device_icon_design.mjs`].
- [ ] Pointer authority is isolated per card. Touch/pen and compatibility mouse
input clear JS/CSS hover, while a later real mouse restores it only on
fine/hover hardware; mode/space/visibility/disconnect cleanup remains
bounded [unit: `pointer-modality.test.mjs`; auto: `smoke_feedback_v2.mjs`].
- [ ] Pre-beta visual review covers Light/Dark desktop and touch matrices for
ordinary, Text, Double, unavailable and vacuum markers; full golden and
performance gates remain release work.
## Device value badge (#90)
- [ ] An untouched legacy thermometer/humidity marker remains pixel-identical;
saving another field does not materialize `value_badge`. [auto: device-presentation]
- [ ] Explicit on/off overrides the legacy temperature gate; zero, false and
off remain visible, while missing/unknown/unavailable render a stable `—`. [auto: device-presentation]
- [ ] State, every allowlisted attribute, derived LQI and `marker:<id>` light
state resolve identically on the full plan, static space card and preview. [auto: smoke_device_preview_parity]
- [ ] Opening the editor explicitly selects the persisted source and position,
even when they are not the first dynamic options, without touching config. [auto: smoke_device_preview_parity]
- [ ] Right, bottom, left and top update live in the editor; bottom stacks above
system LQI and derived LQI suppresses the duplicate system row. [auto: device-presentation]
- [ ] Browser bounding boxes stay inside `.previewstage` with a safe gap for
all positions, a long value, scale ×3 and the maximum activity ring. [auto: smoke_device_preview_parity]
- [ ] Rebind resets the source, delete leaves a missing diagnostic reference,
and space import remaps internal refs or disables/counts external refs. [auto: test_ha_import_export]
- [ ] `static_icon` suppresses but preserves the setting; live-state and room
label toggles do not suppress an explicit badge. [auto: device-presentation]
> **Policy:** this checklist is updated **in the same commit** as any functional
> change (like CHANGELOG.md). For a pre-release, build the production bundle and
> run the smallest unit/smoke subset that covers its changed surfaces. Run the
> complete local frontend, backend and smoke gates only before a stable release.
> The exact-SHA GitHub Validate remains mandatory for publication. Items marked
> `[manual]` are covered by unit tests or the headless smokes in `demo/` — they still
> deserve an occasional eyeball. File every failure as a GitHub issue before fixing.
> **What `[manual]` means (since 2026-07-27).** A named check exists that FAILS
> when the behaviour breaks — in `npm test` or in the smoke suite, both of which
> run in CI on every push. Where the check lives is written next to the marker
> (`[auto: smoke_modes]`). Before this date the marker described an intention:
> the smoke suite printed values and always exited 0, so 96 markers guarded
> nothing (external audit T1/T3). If you add a checklist line marked `[manual]`,
> add the failing check in the same commit.
> **⚠ Rule: a new scrollable list inside a dialog is tested by GEOMETRY, never
> by the DOM.** Any new scrolling box or `overflow` container added to a dialog
> MUST get a smoke that measures **the container's own height and the visible
> position of its first item** (`getBoundingClientRect`, and the item's rect
> against the box's rect) — counting rendered rows, or asserting that the nodes
> exist, proves nothing. The failure mode is always the same and always
> invisible to a DOM check: a scrolling box is a flex item whose automatic
> minimum size is zero (`min-height: auto` → 0 for an `overflow` child), and a
> dialog body is a flex column with a height cap, so the box is the one child
> that can be squeezed to a sliver while every row inside it renders happily.
> It has bitten us twice already: the **target search results** in the tap
> action dialog (v1.53.1 — 26 matching automations rendered into a 1 px
> stripe; the smoke counted rows and passed) and the **«Already uploaded»**
> plan picker (dev, unreleased — rows present, box 14 px tall, same story).
> Both smokes measure heights now; write the third one that way from the start.
## HA-disabled binding gate
The source-of-truth matrix is
`docs/superpowers/specs/2026-08-08-ha-disabled-devices-design.md` §17.
`test/ha-binding-status.test.mjs` covers full/limited registry decisions and
the active-only state projection. The standalone demo exposes complete
`disabled_by` rows through both registry list WS commands plus
`window.__setRegistryDisabled(kind, id, disabledBy)` and
`window.__setRegistryAccess(mode)` for browser scenarios.
- [ ] A saved device/entity marker disappears from View, room data, Glow,
controls, live text, openings and vacuum overlays after its registry row
becomes disabled; config/layout remain byte-for-byte unchanged.
- [ ] Device editor → Hidden and disabled shows a labelled grey ghost. Show is
refused, metadata/Delete/Open in HA remain available, and the ghost is
not draggable.
- [ ] Reactivating the same ID restores its metadata/layout without a false
new-device event; an explicitly user-hidden marker stays hidden.
- [ ] A never-seen auto device disabled before discovery appears as new only
after its first activation.
- [ ] All disabled child entities make an otherwise active device disabled;
one disabled auxiliary entity never suppresses active functional rows.
- [ ] If full registry WS access is denied, positive live evidence stays
active, an unknown binding is `unverified`, and no false disabled/orphaned
ghost or service call is produced.
- [ ] Two full cards plus a static space card share one registry fetch and one
subscription pair per HA connection; registry events invalidate all of
them without a reload.
- [ ] `houseplanDiagnostics()` reports only redacted registry access/age/error
and binding-status counts; it contains no names, states or marker data.
## HA Area marker relocation (#126)
- [ ] `test/device-area-relocation.test.mjs` covers direct-binding authority,
same/cross-space transitions, conservative legacy backfill, explicit and
composite exclusions, rebind, malformed metadata and delete-first
provenance.
- [ ] `test/space-geometry.test.mjs` proves both marker-position paths can
suppress one stale saved point without changing the stored layout input.
- [ ] `demo/smoke_area_relocation.mjs` changes an authoritative registry Area
against the production bundle, checks one serialized layout delete plus
config/attention persistence, and proves the read-only hosted card moves
immediately without writes.
- [ ] Area-provenance cleanup ignores the filtered display roster, preserves
empty Device/Entity Registry namespaces, accepts exact live entity states
as existence evidence, requests only one confirmation refresh and removes
an orphan only after two distinct non-empty authoritative revisions.
- [ ] A rejected confirmed-cleanup config write remains retryable on the next
rebuild; the same revision, state ticks and repeated empty frames never
become extra confirmation or a registry reload loop.
- [ ] Backend validation and import/export tests cover the 20,000-entry bound,
exact entry schema, same-source preservation and cross-source removal.
## Device display preview and face parity
The behaviour matrix is defined in
`docs/superpowers/specs/2026-08-08-device-display-preview-design.md` §22.
Pure source/value/presentation rules live in `test/device-presentation.test.mjs`.
`demo/smoke_device_preview_parity.mjs` compares the same live fixture across
the interactive plan, `hp-device-preview` and `houseplan-space-card`, including
semantic classes, icon/value/badges, scale variables, provider text and the
public binding-status hook.
- [ ] Every binding/display/icon/size/angle/control/temperature draft change
updates the preview before Save; Cancel writes neither config nor layout.
- [ ] Working, open, cover, presence, short event, transition, alarm, static,
unavailable, media-neutral, composite-Power and `live_states: false`
explanations match the actual face.
- [ ] The local short-activity demo lasts 3.3 seconds and the continuous demo
runs until stopped. Neither sends a service call; reduced motion uses a
compact dot, and both reset immediately on binding change, real activity
or alarm.
- [ ] Provider metadata is cached between dialog openings and refreshed after
registry/config-entry changes; source integrations remain separate from
the binding provider.
- [ ] Long provider/source/state text wraps without horizontal scroll; maximum
marker/ripple size fits the stage and reports its preview scale.
- [ ] Derived temperature/humidity values keep the compact plan form (`22.4°`,
`48%`), while a direct entity value continues to use HA localization and
units.
## Device icon package parity (#211)
The independent reference subset under
`demo/srv/reference/device-icons/` comes directly from designer package 1.1.1;
it is not generated from production CSS. The package archive hash and the
owner's #219 red/green Lock/Unlock paint override are recorded in that
directory's README.
- [ ] `node demo/smoke_device_icon_design.mjs` reads the SVG colors and stroke
widths, then compares them with fresh computed styles. It also measures
circular core/shell geometry, the real `mdi:lightbulb-spot` painted path,
value-pill radius and 44×44 hit area at 32/56/96 px.
- [ ] `node demo/capture_device_icon_reference.mjs` writes a two-column
**Reference SVG / Runtime** matrix for both themes to
`artifacts/device-icon-reference/`. Code review must inspect this artifact
visually; a green historical golden is not proof of package parity.
- [ ] Preview/static parity and unavailable keyboard/tap behavior remain
covered by `smoke_device_preview_parity`, `smoke_static_icon` and
`smoke_disabled_device` after a fresh production build.
## Device marker geometry and input polish (#213)
- [ ] `node demo/smoke_device_icon_pixel_alignment.mjs` covers core bases
24…112 CSS px in quarter-pixel steps at DPR 1/1.25/1.5/2. DOM centres,
isolated painted centroids/support and a deliberate 1 CSS px mutant must
distinguish browser raster parity from a persistent offset.
- [ ] `node demo/smoke_device_icon_design.mjs` keeps the effective 32/56/96
geometry, uses the direct 0.55 MDI/core ratio and proves hover plus the
configured action from the far value-capsule end at right/bottom/left/top.
- [ ] Opening binding/registry-less/lock-action smokes preserve the secure
no-toggle-on-plan invariant while checking compact Light/Dark
locked/unlocked/unknown shell/core presentation.
- [ ] `node demo/smoke_opening_entity_search.mjs` checks the real opening
dialog: contact and lock search by friendly name/entity ID, preserved
contact priority, visible IDs, persistent **none** option and unchanged
`opening.contact`/`opening.lock` storage.
- [ ] Unit presentation coverage compares marker LQI colour with the shared
continuous `lqiColor()` across former 40/41 and 179/180 boundaries; bands
remain semantic metadata only.
## Text marker shell shape (#217)
- [ ] `node demo/smoke_device_icon_design.mjs` checks the external Text frame,
not only its core: a long value keeps a saturating capsule radius at
24/32/56/96/112 px, while Icon-only remains circular and Double remains a
capsule. The runtime mutation to `border-radius: 50%` must be rejected.
- [ ] `device-text-shell-long-light` and `device-text-shell-long-dark` isolate a
large `498 ppm` Text marker. Golden review must visibly confirm straight
upper/lower middle sections rather than an ellipse.
- [ ] `node demo/capture_device_icon_reference.mjs` includes an additional
96 px Text row beside the normative Light/Dark `Text Default.svg`.
## Device lock and orange foreground palette (#219)
- [ ] Closed/`locked` is green `#66D17A`; open/`unlocked` is red `#F0410C`.
The same core/stroke palette is used by ordinary lock markers and compact
door/gate lock badges; glyph shape remains closed/open/question.
- [ ] Every device glyph on an orange core (`on`/working and physical `open`)
is white in Light and `#252525` in Dark. `device-icon-state-table-light`
and `device-icon-state-table-dark` show `on` and `open` together, plus
both lock states [unit: device-marker-polish-contract; auto:
smoke_device_icon_design; golden: device-icon-state-table-*].
- [ ] Alarm, hover, focus, selected, unavailable, virtual, press feedback,
pulse, hit-area and lock actions retain their existing priority and
behaviour [unit: device presentation/polish/pointer; visual source review].
## Unified device status and pulse activity (#98)
- [ ] The Display list contains exactly Icon + state, Icon + state and activity,
Value + state, Always static icon, in that order. Legacy
`display: ripple` reads and saves back as `icon_ripple`
- [ ] Icon + dynamic plate shows state plate/morph but no ordinary activity
effect; Icon + activity adds the semantic effect; Value keeps the
state-coloured plate and hides ordinary activity; Always static icon
keeps one neutral base icon and suppresses all state-driven visuals
- [ ] Motion/vibration/sound/contact rising edges render exactly three waves
for about 3.3 s; initial load and recovery from unknown/unavailable do
not fake an event; a rapid retrigger restarts it
- [ ] Occupancy/presence is one calm continuous pulse for the whole active state
- [ ] Cover/lock/valve movement continuously pulses until the travelling state ends;
direct terminal `closed ↔ open` / `locked ↔ unlocked` without an
intermediate state breathes for about 3.3 s
- [ ] Actual work (light/switch/fan/humidifier on, active climate action,
vacuum cleaning, script running) is yellow and slowly
breathes in Icon + activity; `automation = on` is merely enabled and
remains neutral
- [ ] Every `media_player` is neutral and has no running activity for `on`,
`idle`, `playing`, `paused` and other transport states; explicit `off`
uses the same faded treatment as `unknown`/`unavailable`. Several
resolved media entities fade only when none is available and powered
- [ ] Controls aggregate their targets: any working target drives both the
yellow plate and the running effect
- [ ] Open contact/open valve are orange; unlocked lock is red and locked lock
is green; an open cover stays neutral because its icon morph carries that
state
- [ ] Unavailable suppresses ordinary activity. Alarm outranks all dynamic
presentation, including when ordinary live states are off; `static_icon`
deliberately hides alarm paint without suppressing service-call errors
- [ ] `static_icon` hides temperature/humidity/LQI, RGB, value, icon morph,
activity and live vacuum puck/trails/room highlight on the full and static
cards; preview still names the real HA state/source and explains the static result
- [ ] Switching a static vacuum back to a dynamic display restores applicable
live/server trails; choosing static never deletes stored trail history
- [ ] `value_static_icon` (#588) shows the same value as `value` — including on
the plan itself and the space card, where sources are resolved lazily
— while state, alarm, unavailability, RGB and activity never paint
it; no pulse, no °/%/LQI, value badge suppressed with its setting kept,
and the live vacuum puck, trail and route warning stay hidden
[auto: `smoke_static_icon.mjs`, golden `device-icon-state-table-*`]
- [ ] Activity colour and size (×2..×8) apply per device; alarm ignores them
- [ ] Icon size ×0.5..×3 and rotation 0..355° apply per device; the
temp/humidity badges scale with the icon
- [ ] With OS "reduce motion" enabled, ordinary activity becomes a compact
solid dot; alarm keeps the red plate and accessible alarm description,
without an animated or static ring
## Climate temperature opt-in (dev)
- [ ] «Use the device's temperature sensor» (marker dialog, climate devices
only, default OFF): current_temperature shows as the standard `.tval`
badge and joins the room average like a thermometer; unavailable /
missing attribute = no badge, no vote; hidden devices keep voting
(registry-wide climate, like hidden thermometers); the tick survives
dialog recreation [auto: smoke_climate_temp; units: test/devices.test.mjs;
backend: tests_backend/test_validation.py (use_climate_temp)]
## Styling hooks and HA-formatted values (docs/STYLING-HOOKS.md, dev, unreleased)
- [ ] **The hooks are there and they are the config's ids**: open the plan's
DOM (devtools → the card's shadow root) and check that a device marker
carries `data-hp="device"`, `data-id`, `data-entity` and `data-area`; a
room `data-hp="room"` + `data-id` + `data-area`; a door/window/gate
`data-hp="opening"` + `data-kind`; a decor shape `data-hp="decor"` +
`data-kind`; a visible room card `data-hp="room-label"`; a floor tab
`data-hp="space-tab"`. The ids are the ones in your config, not DOM
positions — they survive a reload [auto: smoke_styling_hooks]
- [ ] **Absent is absent**: a virtual marker has NO `data-entity` at all, and a
sub-area room (no HA area) has NO `data-area` — never the string
«undefined» [auto: smoke_styling_hooks]
- [ ] **A card-mod rule actually applies**: with card-mod installed, add
`ha-card [data-hp="device"] .lqi { display: none; }` to the card — the
signal badges disappear and nothing else moves. Then target one marker by
`[data-entity="…"]` and confirm it is the only one affected [manual]
- [ ] **The static card carries the same hooks**: a `houseplan-space-card`
has `data-hp` on its rooms, visible room labels and markers (it draws no openings
and no decor, so those are simply absent), and it needs its OWN card-mod
block — it is a different card with its own shadow root
[auto: smoke_styling_hooks]
- [ ] **`ha-icon` internals stay out of reach**: a rule may style the icon HOST
(colour, transform) but cannot reach the `<svg>` inside it. That is a
browser rule, and it is why the hooks sit on our wrappers
[auto: smoke_styling_hooks]
- [ ] **A value badge is formatted by HA**: set a numeric sensor's display
precision in HA (Settings → the entity → Display precision) to 1 and put
it on the plan as «value instead of icon». The badge shows the rounded
number with YOUR decimal separator and the entity's unit — once, not
twice — and matches what more-info shows [auto: smoke_value_format]
- [ ] **A live decor label is formatted by HA**: the same sensor in a text
shape reads identically; a switch shows «Включено», not `on`; an
attribute goes through the attribute formatter (a climate's
`current_temperature` is a number, not the climate's state)
[auto: smoke_value_format + unit logic.test]
- [ ] **A literal suffix stays literal**: write an attribute token followed by
` проц.` — the suffix is part of the text, with no hidden unit override
and no duplicate appended by the label renderer [auto: smoke_live_text]
- [ ] **An older Home Assistant is unchanged**: on an HA without
`formatEntityState` the badge and the label print the raw state with the
entity's unit appended, exactly as before — nothing is blank and nothing
throws [auto: smoke_value_format + unit logic.test]
- [ ] **The °/% plates are untouched**: the small temperature/humidity badges
next to an icon (and the same numbers in a room card and the tooltip)
still read «21.5°» / «48%». They are a derived reading, not an entity
state — deliberately ours [auto: smoke_value_format]
- [ ] **The text block's handles are small but still catchable**: select a
label in the Background editor. The four corner circles and the rotate
handle are a quarter of their old size — beads, not buttons — and the
dashed frame no longer hides the text. Now grab one on a TABLET with a
finger, aiming roughly at it rather than exactly: it is caught, because
the invisible hit circle is still the old finger-sized one. Same at any
zoom [auto: smoke_decor_text]