Compare commits

...
Author SHA1 Message Date
Matysh 3048ac9195 test(led): wait for the no-Glow fade to finish (#784)
The state and room-setting updates are separate renders. Assert the stable no-Glow contract after the legitimate 500 ms leaving phase instead of racing it on fast CI runners.

Release: v1.79.0-beta.4

Issue: #784
User-Visible: no
2026-10-03 11:15:38 +03:00
Matysh 654cb9d404 docs(screenshots): refresh source fingerprint for #784
All 11 documentation frames remain pixel-identical after the LED strip runtime changes; only the source fingerprint is refreshed.

Issue: #784

User-Visible: no

Release: v1.79.0-beta.4
2026-10-03 11:01:35 +03:00
Matysh 8dbfe5e6b2 test(golden): accept polished LED strip glow (#784)
Reviewed all three changed WSL frames: the 30 cm field is continuous at
straight cuts and corners in 2D light/dark and 2.5D scenes.

Issue: #784
User-Visible: no
Release: v1.79.0-beta.4
Baseline-Reviewed-Local: sha256:cf474aa1ff676d2262f1c9e442114e7df0e91867c69c78bdc82c923a48355650
2026-10-03 10:54:55 +03:00
Matysh 706eb02d70 fix(led): polish glow and interactions (#784)
Issue: #784
User-Visible: yes
2026-10-03 10:47:28 +03:00
Matysh 4962de1d5c fix(ci): parse beta-derived docs CSV lists (#783)
Keep repeated --expect-change flags compatible while accepting the comma-separated workflow input and ignoring whitespace, empty values, and duplicates.

Issue: #783
User-Visible: no
2026-10-03 10:04:12 +03:00
Matysh 31b4ddda55 Release v1.79.0-beta.3 candidate
Publish the three integrated S8 issues since beta.2. Synchronize all seven version sources, rebuild committed frontend assets, tighten ratchets to measured facts, and update release metadata for LED strips. Canonical documentation screenshots were reviewed from Linux CI and accepted in the preceding derived commit.

Local checks: build/typecheck, bundle policy, bundle budgets, monolith checks and release-contract tests passed. Native-Windows unit run passed 3517 tests; two GNU-bash tests and one TypeScript diagnostic-shape test remain Linux-only. Full exact-SHA Validate is mandatory before publication.

Release: v1.79.0-beta.3
Issue: #765
Issue: #780
Issue: #781
User-Visible: yes
2026-10-03 09:40:14 +03:00
Matysh 072fb108cf docs: accept derived artifacts on dev for v1.79.0-beta.3
Производные артефакты беты приняты вручную по резервному пути релиз-менеджера (PROCESS.md §8, §11.4, #697). Каноническая съёмка: https://github.com/Matysh/houseplan-card/actions/runs/37102465043. Все 11 кадров просмотрены; изменения #780 и тотальный байтовый сдвиг Chromium 151 приняты с записанным --no-witnesses. Golden полного Validate 37102870712 зелёный и эталонов не меняет.

Issue: #697
User-Visible: no
2026-10-03 09:31:48 +03:00
Matysh 24671d4358 perf: снизить стоимость маски LED-лент (#780)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #780
User-Visible: no
2026-10-03 00:10:28 +03:00
Matysh 6607a34973 perf: упростить SVG-поле LED-лент (#780)
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #780
User-Visible: no
2026-10-02 23:38:35 +03:00
Matysh e9e6eba5d1 docs(review): yellow code review #780 r2
Exact-SHA performance keeps the result yellow: the 50x50 LED profile
exceeds warm-ready and camera Long Task budgets on all seven samples.

Issue: #780
User-Visible: no
2026-10-02 22:59:42 +03:00
Codex a20ef2b9b0 chore(monolith): raise bundleBytes for the r1 LED fixes (#780)
bundleBytes 2 667 879 → 2 670 566 (+2 687, over the 2 000 band): the value
badge of a strip on the card and on the static card, the visibility gate
before import(), the release of a card's LED caches on disconnect and the
cache statistics of the lazy runtime/field chunks (r1 M1, M4, M5). The
badge method moves below `_renderDevice`, outside the vacuum section the
isometric contract reads. The review index is rebuilt after the rebase.

Issue: #780
User-Visible: no
2026-10-02 20:58:11 +03:00
Codex e447364636 perf(led): the led-strips-v1 profile proves caches, disconnect and a late import (#780)
r1 M5: the runtime and field chunks release a card's frame and field cache
on disconnect (ledRelease from disconnectedCallback), never cache for a
disconnected card, and a chunk that lands after disconnect renders nothing
(the card's LED hook is connected-only). The profile now reports the three
caches of the shown space separately (shapes ≤ 50, visibility ≤ 50, retained
per-emitter fans ≤ 2500) through the runtime's ledStats, asserts 0 retained
entries and 0 live LED timers/frames/observers after every disconnect,
judges the Long Tasks of a 100-step camera series as well as the interaction
profile's camera scenario, runs one extra cold mount whose runtime response
is held while the card is removed, and enforces ≥ 7 samples after ≥ 1
warm-up — also on reports merged from parts (--warmup-only, --merge).

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
Codex b40e8ff58d fix(led): badge at the anchor, room_id wins, chain stays in its space, hidden loads nothing (#780)
r1 of the code review:
- M1: a strip keeps its marker's value badge, passive, at the half-length
  anchor on the card and on the static card (no icon core, pulse or slot).
- M2: one room resolver for the strip's Glow — an explicit valid room_id of
  the marker wins over the anchor room; a stale one falls back (stripRoom).
- M3: the chain remembers the space it is drawn in; a space switch finishes
  it there, never in the space shown next, and opens no picker over it.
- M4: visibility is decided before import(): a hidden marker or an
  HA-disabled device loads no LED chunk (ledVisible, also checks the stored
  marker so a just-hidden one does not slip through a stale device list).
- M6: the static card is a full light_pools × live_states browser matrix.

Unit tests for M2/M3, smokes for M1/M3/M4/M6, five registered mutants.

Issue: #780
User-Visible: yes
2026-10-02 20:54:50 +03:00
Codex ef3e7a5440 fix(config): an ordinary save without led_strips keeps the stored shapes (#780)
r1 H1: a previous frontend sends every space without the unknown field and
the ordinary write path replaced the document with it, erasing every LED
strip. The shared ordinary-writer helper now copies the stored shapes of a
space the payload omits (config/set and Optimize); an explicit list from a
new client still deletes, a removed space takes its strips, and a link to a
marker the same write deleted unbinds by the usual rule. config/set reports
the normalisation counters after the preservation.

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
MatyshandCodex 403bcca680 docs(reviews): independent code review of LED strips (#780)
Manual review requested after the automated reviewer stopped before a verdict.
Material 2c5b59aa: red, one High and six Medium findings.

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
Codex 91b57ae60e test(golden): accept the LED strip scenes and the Devices toolbar shift (#780)
Accepted from the golden-images artifact of the full Validate on 63e751ad,
every frame reviewed side by side:
- new: led-strip-design-reference-off-light, led-strip-design-reference-on-light,
  lighting-led-strip-glow-dark, led-strip-off-light, iso-led-strip-dark (the
  designer's four strips on #868D94; compared with Led-On/Led-Off in
  docs/design/led-strips/ACCEPTANCE.md);
- changed: geometry-devices-editor-dark, device-inbox-narrow-ru-dark,
  device-dialog-desktop-en/de, toggle-entity-dialog-desktop-en/mobile-ru (the
  «LED strip» tool after «Add»), geometry-decor-editor-dark,
  furniture-categories-light, room-discard-dialog-mobile-ru,
  support-phone-success-dark-en, support-phone-validation-light-ru — icons
  the stale demo icon map lacked are drawn now.
demo/srv/assets/icons.js is the output of demo/gen_icons.mjs again (restores
63e751ad; the 4283cb13 trim is dropped so the frames match the accepted run).

Issue: #780
User-Visible: no
Release: v1.79.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36999711281
2026-10-02 20:54:50 +03:00
Codex 3d2c5ab3ee fix(demo): add only the LED tool icons to the demo icon map (#780)
A full regeneration of demo/srv/assets/icons.js also picked up 29 icons other
tasks reference and dropped one, shifting unrelated golden frames. Keep the
committed map and add exactly mdi:led-strip-variant and mdi:link-variant(-off).

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
Codex 5d3f29c249 fix(card): static «Show as LED strip» in the device dialog; demo icons (#780)
The full Validate on a6c4001c found two things:
- smoke_help_affordance: the device dialog shifted when the LED tool chunk
  arrived after it opened (the representation section appeared late). A
  device without a strip now gets the static section at once (label in the
  base dictionary); the press loads the tool, leaves the dialog through its
  own guard and starts drawing for the same marker. A strip's device still
  loads the tool with the dialog. The dialog alone no longer loads anything.
- smoke_lazy_admin_locale counted nine namespace chunks (stale since the
  `tools` namespace); it now reads NAMESPACE_LOCALE_CHUNKS and accepts the
  editor's own `tools-de`.
- demo/srv/assets/icons.js regenerated: the demo/golden ha-icon stub lacked
  mdi:led-strip-variant and mdi:link-variant(-off).

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
Codex ec9d968c8a test(smoke): the Devices toolbar has the LED strip tool after Add (#780)
Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
Codex 8915e49a58 test(smoke): LED strip smokes enter through __hpTest and the DOM (#780)
setServerConfig/setLayout/setMode/openMarkerDialog/close and the tray's own
«Device settings» + Delete + confirm instead of private card writes; config
writes pass through the fixture so revisions stay consistent.

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
Codex d515c42177 feat: LED strips — editor notes, docs, demo, smokes, golden scenes, profile (#780)
Stage 5 of #780.

- Import summary: «Strips left unbound after import: {n}» from the backend
  `unbound_led_strips` count; «Optimize plans» reports strips passing
  through walls per space and edits none (AC16).
- Linear field for long strips (ТЗ §13.2): pieces of at most the radius
  along the polyline, emitters thinned to r/4, each piece clipped to the
  visibility fans of its own emitters as separate clipPath children (no
  boolean pass per piece), one floor clip for the whole layer, no fan at
  all where nothing blocks within the radius; a grid index of body faces
  and boxed inside tests; unchanged fields skip re-diffing. 50×50 on the
  large house: first stable frame ~1.4 s, warm space ~1.1 s locally.
- led-strips-v1 profile: demo/benchmark_led_strips.mjs with the derived
  large-house fixture (10×5, 50×50, none), absolute limits of the ТЗ table
  in demo/performance/budgets-led-strips.json, exact counters (zero
  recomputes on HA ticks/camera/colour, ≤50 cache entries, no growth over
  20 cycles); added to the full performance workflow.
- Bundle: LAZY_LED_GZIP_CEILING 10 KiB, LAZY_LED_EDITOR_GZIP_CEILING 11 KiB
  (measured + 10 %, rounded up); overlaps with the initial and editor
  graphs refused; the lazy editor graph stays inside its ceiling.
- Smokes smoke_led_strip_draw/bind/glow, linked in smoke-links; 13 mutants
  in the registry (7 browser guards in the inventory); config field registry
  entry `spaces[].led_strips`.
- Golden: five new scenes on the `golden-led` space of the visual fixture
  (`ledStrips` option, the designer's four strips on #868D94), matrix v71.
- Docs: LIGHT, DEVICE-PRESENTATION, USER-GUIDE (en/ru), UX-MODES,
  ARCHITECTURE, ISOMETRIC, CONFIG-COMPATIBILITY, TOUCH-SUPPORT, demo/stand
  README, performance README; docs/design/led-strips with the unchanged
  designer archive, two paired frames and ACCEPTANCE.md; both changelogs.

Issue: #780
User-Visible: yes
2026-10-02 20:54:50 +03:00
Codex ade3078d53 chore(monolith): raise hostRefs and bundleBytes for the LED strips (#780)
hostRefs 4924 → 5050 (+126): the new lazy LED editor reads card state through
its own structural port `LedEditorHost` (src/led-strip-editor.ts) — not the
HouseplanEditorHostPort — the same way the stairs editor does. bundleBytes
2 607 854 → 2 664 132: the three new LED chunks (runtime, field, editor),
their ru/de/fr dictionaries and the shared geometry chunk. Re-measured at the
end of the task.

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
Codex d21fba11d6 feat(card): the LED strip tool in the Devices editor (#780)
Stage 4 of #780 (ТЗ §4–§5). «LED strip» next to «Add» draws a chain with
clean clicks only (pan, pinch, a second finger, cancel and the synthetic
click after navigation add nothing), snaps to the grid and to physical wall
faces / zero-wall axes, stops at the first face of masonry, partitions,
columns and windows (doors, gates and passages are cut by geometry), Shift
gives 45°. Ctrl+Z removes the chain's own point first, Esc finishes, a
double click or a click on the first point (≥3 vertices) closes; fewer than
two distinct points write nothing. A finished strip opens the device picker
(lights first, taken markers explained, «New device…» binds in that
dialog's own write, «Later» keeps unbound geometry).

A selected strip shows its vertex handles (a drag re-checks both neighbours
and the whole path) and a new Devices tray branch: device settings,
bind/change, unbind, show as icon, delete. The device dialog gets the
representation section: show as strip (restores a hidden shape at once or
draws one for this marker, behind the dialog's own save/discard guard),
show as icon, unbind/delete for a hidden shape. Every geometry or
representation change is one optimistic write and one LED command of the
device history; Undo/Redo restores only its own strip record and refuses
when a newer change sits on it. A rebinding renames the link in the marker
save, a deleted marker leaves an unbound strip, a bound marker may not move
to another space. Plan/Background show strips as a passive translucent mark.

The tool, its `led` dictionary (en static, ru/de/fr lazy) and placement
geometry are a new lazy `led-strip-editor` chunk (9.5 KB gzip), loaded only
on the tool, an editable strip in the shown space or a strip device's
dialog. The initial graph gets the loader and delegation only
(src/led-strip-card.ts); the lazy editor graph +122 B, inside its ceiling.
Mutant anchors follow the moved code (marker dialog guard, static LED layer).

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
claude[bot]andCodex ed238059e1 refactor(card): the LED linear field is its own lazy chunk (#780)
ТЗ §13.1: the static card with light_pools:false must not load the linear
field. led-strip-field.ts (2.1 KB gzip) is a dynamic import of the LED
runtime (5.0 KB gzip), loaded only when a strip is on in a Glow room with
a light scene, with the same fingerprint handshake and hashed-URL retry
(manifest role led-field, retry token counted).

Issue: #780
User-Visible: no
2026-10-02 20:54:50 +03:00
claude[bot]andCodex 8237bbc1cc feat(card): LED strips in the 2.5D View (#780)
Stage 3c of #780 (ТЗ §7). In the volumetric View the stripe is raised like
a device tile: body lifted by ISO_TILE.lift (0.075 D), the edge swept
ISO_TILE.depth (0.1 D) below it in isoEdgeColor, an inert blurred floor
shadow from isoTileShadow for the current theme and floor; D takes the
shared ISO_ICON_SCALE. The field stays on the floor plane, the hit path
moves with the raised body, everything is in plan units so zoom cannot
detach it. Editors and the static card keep Flat.

Issue: #780
User-Visible: no
2026-10-02 20:54:49 +03:00
claude[bot]andCodex 05d62f053a feat(card): LED strips on the static space card (#780)
Stage 3b of #780 (ТЗ §8). houseplan-space-card draws a represented marker
as a passive strip through the same lazy runtime: no icon, no auto slot, no
round pool, no hit path, focus or actions. The stripe uses the drawn wall
geometry for the face offset, so light_pools:false builds no barriers,
visibility or timers; the linear field appears only with light_pools:true
and Glow; with live_states:false the stripe is neutral. Strip points vote
in the content frame. Probed in the demo: plain card — one passive stripe
with the source-colour core, no field, no hit path; light_pools — the field.

Issue: #780
User-Visible: no
2026-10-02 20:54:49 +03:00
claude[bot]andCodex 3a1cd7b1ad feat(card): LED strips in View — lazy chunk, stripe, linear light (#780)
Stage 3a of #780.

- src/led-strip-gate.ts is the only initial-graph foothold (ТЗ §13.1): which
  markers a space shows as a strip (active and bound), the half-length
  anchor that replaces their icon position, and one page-wide load of the
  lazy led-strip-runtime chunk (fingerprint handshake, a hashed-URL retry on
  the next explicit entry, never in a loop).
- The card: a represented marker takes no auto slot, draws no icon, casts no
  round pool and is placed at the anchor; two call sites render the stripe
  layer and the linear field from the chunk. The space model carries the
  stored strips untouched; scaling, validation and geometry are in the chunk.
- src/led-strip-runtime.ts: the stripe is two strokes of one derived path
  (outline #383838 t, core t/2, round joins and caps, t = 0.08/0.12 D),
  white off, white core + field on with Glow, source-colour core without
  Glow, grey dashed unavailable without a field. The hit path takes the
  card's own device handlers (one action path) with radius max(22 px, t/2)
  and the nearest stripe as owner. The linear field is the exact distance
  field with the shared falloff: opaque grey bands of a luminance mask per
  piece, pieces joined by lighten (the maximum), each piece clipped to what
  its own emitters see, so a hidden part never lights through another part's
  visibility; buried emitters emit nothing; a failed clip is dark. A bounded
  per-space cache (50) counts geometry rebuilds.
- The initial View graph had 501 B of headroom. The furniture library copy
  (furn.*, 104 keys × 4 languages, editor-only) moves into a new lazy `tools`
  namespace (#627 mechanism) that the editor runtime awaits; the initial
  View graph is 298 686 B gzip with the LED gate in it (−1 879 B vs the base).

Tests: test/led-strip-runtime.test.mjs (6: representation, gate anchor =
geometry anchor, falloff, states and radius, per-piece clipping and buried
strips, bounded cache), bundle and i18n fixtures for the LED chunk and the
fourth namespace.

Issue: #780
User-Visible: no
2026-10-02 20:54:49 +03:00
claude[bot]andCodex fb3071d3c1 feat(card): pure LED strip geometry and the space model field (#780)
Stage 2 of #780. src/led-strip-geometry.ts (pure, not imported by the
initial graph): the anchor at half the polyline length; stripPieces and
visibleStripPath — a segment lying on a thick body face within
epsilonGeom is shifted t/2 into free floor, free floor and zero-wall axes
stay at 0, a face→floor transition is a connector without gap; emitter
samples epsilon outward on a face and none inside a body; placement that
stops at the first face and lets a strip touch and slide along it, a
vertex drag clamped on its path and both neighbours; the screen hit owner
with radius max(22 px, t/2) and a stable-id tie.

SpaceModel gains optional led_strips (render units, data only, no geometry
import in space-geometry.ts).

Tests: test/led-strip-geometry.test.mjs (10).

Issue: #780
User-Visible: no
2026-10-02 20:54:49 +03:00
claude[bot]andCodex 870237f3fb feat(backend): LED strips are stored, normalised and transferred (#780)
Stage 1 of #780 — the data model. A space carries an optional
`led_strips: [{id, points, marker, active?}]` (custom_components/houseplan/
led_strips.py, pure, strict mypy).

- Type schema inside SPACE_SCHEMA: 2–50 finite numeric points (no strings,
  booleans, NaN or off-canvas values), ≤50 strips per space including hidden
  shapes, strict boolean `active`, marker = non-empty string or null.
- A config-level step after coordinate canonicalisation judges the shape
  (two distinct points, non-zero length, a closed strip needs three distinct
  vertices, a hidden shape needs a marker, unique ids per space) and the links
  in the order the spec fixes: duplicates are rejected before any
  normalisation (two links to the same missing id still conflict); a link to
  a marker that is not live becomes an unbound strip (marker null, active
  true, id and points kept), so a client that does not know strips can delete
  a bound marker without its save failing; a live marker with an empty space
  adopts the strip's space; a non-empty foreign space rejects the write.
- config/set answers with `led_strips: {unbound, space_adopted}` when the
  write was normalised, so a new client re-reads; old clients ignore it.
- Space import remaps links through the marker id map; a skipped or
  virtualised duplicate leaves the strip unbound; a coinciding old id never
  binds. Plan-only export keeps geometry and nulls every link. Import details
  report `unbound_led_strips`, computed by the server, never read from the file.
- Coordinates get JSON-noise cleanup only, like stairs (face contacts are
  off-lattice), in both canonicalisers with a shared fixture case.
- Support package: counters only (total/unbound/hidden), no coordinates or ids.

Tests: tests_backend/test_led_strips.py (41, pure), test_ha_import_export
(6 cases: full round trip with a hidden shape, orphan count, remap against a
coinciding id, skip and virtual duplicates, plan-only), test_ha_websocket
(old client deletes a bound marker → save stands, counters, foreign space
rejects without a new revision). Full backend with the HA harness: 969 passed.
Mutating the shape check, the duplicate check, the orphan normalisation or the
space adoption each turns the pure suite red.

Issue: #780
User-Visible: no
2026-10-02 20:54:49 +03:00
Codexandclaude[bot] 9fa5efbdac docs(reviews): manual code review of the pending-round witness (#781)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Manual review requested by the owner after the automated reviewer failed.
Material e931a949: green, no High/Medium findings, one non-blocking Low.

Issue: #781
User-Visible: no
2026-10-02 19:57:08 +03:00
Matyshandclaude[bot] 73b0cfff5b test(process): поймать оживление прошлого запроса (#781)
Issue: #781
User-Visible: no
2026-10-02 19:57:08 +03:00
claude[bot] 13d8df5554 docs: review document for #780
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #780
User-Visible: no
2026-10-02 10:47:04 +03:00
claude[bot] 9e1a2f4a07 docs(reviews): rebuild the review index after rebase onto dev 2026-10-02 10:18:38 +03:00
Matyshandclaude[bot] 16069d1dcc docs(reviews): independent code review of pipeline snapshot (#765)
Manual review requested by the owner after the automated reviewer failed.
Material af09d36d: green, no High/Medium findings, one non-blocking Low.

Issue: #765
User-Visible: no
2026-10-02 10:18:29 +03:00
claude[bot] 23f822475b fix(process): prepare and the usage step run pipeline scripts from one pinned dev snapshot (#765)
The body of _process.yml is read from dev (@dev, #623). After "Перейти на
ветку задачи" the working copy of job prepare is the task branch, and a
show/ship branch with a clean merge is not rebased before review: its
scripts/ may lag dev or be replaced. #749 fixed job integrate; prepare
still ran four control scripts from the material — the issue-body digest
for the anchor, --reuse of a green verdict (#499), validate-gate (#510)
and the spec-change check (#517). The material decided its own admission:
a branch whose review-doc-guard.mjs prints reuse=true merges without the
model. model_review took model-usage.mjs from the material too: a lagging
branch has none, and the publication silently wrote reason=missing.

Now prepare extracts one snapshot right after setup-node, before the
branch switch: `git rev-parse origin/dev` once and `git archive <sha>
scripts .github/workflows/validate.yml`, so the git fetch of the track and
rebase steps cannot mix versions. Every repo script of the job runs from
it via TOOLS — the track step and the rebase guard lose their own
extractions. The SHA goes out as job output tools_sha; the usage step of
model_review archives the same commit inside itself, so a snapshot failure
is a failure of the reporting step (continue-on-error), not of the stage.
The model session runs on that runner, so the usage line stays untrusted
input parsed strictly (#556, #737).

withMaterialAnchors is idempotent: a repeated call drops the separator the
previous call wrote instead of piling up `---` lines.

Tests: test/process-prepare-tools.test.mjs — the job contract (no step
calls scripts/ from the working copy, one pinned archive before the branch
switch, tools_sha reaches model_review) and the steps as they are, on real
bash and git: a branch behind dev without model-usage.mjs and with a
substituted review-doc-guard.mjs; the anchor digest, reuse and the spec
check come from dev, the usage line is data even after dev moved. Red on
the old workflow: all four. Harnesses of process-track, rebase-generated,
review-doc-guard and model-usage take the prepare snapshot. Three registry
mutants (reuse from the material, usage from moving dev, separators).

Canon: PROCESS.md §10.4 «Скрипты конвейера — из dev» covers prepare and
the usage step; «Расход модели» names it a pipeline step, not the reviewer's.

Issue: #765
User-Visible: no
2026-10-02 10:18:29 +03:00
claude[bot] ceee249210 docs: review document for #780
Issue: #780
User-Visible: no
2026-10-02 10:09:11 +03:00
Matysh d0c13bc555 Release v1.79.0-beta.2 candidate
Publish the 58 integrated S8 issues since beta.1. Synchronize versions,
rebuild committed frontend assets, refresh ratchets to measured facts,
and update release metadata. All 11 documentation screenshots were
accepted as pixel-identical in WSL, without replacing PNG bytes.

Owner-authorized manual ship review covers all 13 ship issues; High 0.
Local checks: build/typecheck, 3510 unit tests passed (30 skipped),
20 selected browser smokes, Stage 3 dense observable contract.
Full exact-SHA Validate remains mandatory before publication.

Release: v1.79.0-beta.2
Issue: #694
Issue: #703
Issue: #704
Issue: #705
Issue: #707
Issue: #708
Issue: #712
Issue: #714
Issue: #715
Issue: #716
Issue: #717
Issue: #718
Issue: #719
Issue: #720
Issue: #721
Issue: #722
Issue: #723
Issue: #724
Issue: #725
Issue: #726
Issue: #727
Issue: #728
Issue: #729
Issue: #730
Issue: #731
Issue: #732
Issue: #733
Issue: #734
Issue: #735
Issue: #736
Issue: #737
Issue: #738
Issue: #739
Issue: #740
Issue: #741
Issue: #742
Issue: #743
Issue: #744
Issue: #745
Issue: #746
Issue: #747
Issue: #748
Issue: #749
Issue: #750
Issue: #751
Issue: #752
Issue: #753
Issue: #754
Issue: #755
Issue: #756
Issue: #757
Issue: #758
Issue: #759
Issue: #760
Issue: #761
Issue: #762
Issue: #772
Issue: #775
User-Visible: yes
2026-10-02 07:55:09 +03:00
Matysh 11e9eb10df docs: manual ship review for v1.79.0-beta.2
Owner-authorized fallback after the review model stopped before reading code. All 13 ship issues reviewed; High 0, Medium 0, Low 0.
2026-10-02 07:54:50 +03:00
claude[bot] 044f09a6a5 docs(reviews): индекс после сдвига каталога (#744)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #744
User-Visible: no
2026-10-01 23:05:23 +00:00
claude[bot] 92b89d9b80 docs: review document for #744
Issue: #744
User-Visible: no
2026-10-01 23:05:20 +00:00
Claude 2bcde13158 chore(monolith): raise the bundleBytes baseline for #744 (#744)
After the rebase onto dev (#762) the raw dist total is 2 607 854 B against
the 2 605 172 B baseline, +2 682 B, over the 2 000 B band. The growth is the
task's own and deliberate: the floor-geometry key module and its call sites
(src/floor-geometry-key.ts, houseplan-card.ts, clean-floor.ts) plus the
wrapper of the new lazy space-editor chunk that keeps the initial View graph
under its absolute budget (initial View 300 572 B gzip, headroom 494 B).
No other metric moved past its band (portMembers 347 -> 349, band 5).

Issue: #744
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-02 01:01:38 +03:00
Claude 7bbab4b61a docs(reviews): индекс после сдвига каталога (#744)
Issue: #744
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-02 00:56:24 +03:00
claude[bot]andClaude 9882046b86 docs: review document for #744
Issue: #744
User-Visible: no
2026-10-02 00:56:09 +03:00
Claude a86be350b1 perf(bundle): load the space-card GUI editor lazily to keep #744 under the initial View budget (#744)
The floor-geometry key of #744 (+~143 B gzip) put the initial View graph at
301 097 B gzip, 31 B over the absolute 301 066 B wall in
scripts/bundle-budget.mjs. The comment over that wall says the next growth
is paid by moving code into lazy graphs, not by raising the budget.

`src/space-card.ts` imported its Lovelace GUI config editor
(`src/space-editor.ts`, the `houseplan-space-card-editor` element)
statically, so every View paid for a form only the dashboard editor opens.
`getConfigElement()` now imports it on demand, exactly as
`houseplan-card.getConfigElement()` already does for `./editor`; Home
Assistant awaits the returned promise. The editor's dependencies (lit, i18n,
space-geometry) stay in the shared chunk, so the new `space-editor-*.js`
chunk holds only the element itself (~1.1 KB gzip).

Measured: initial View 301 097 -> 300 489 B gzip (-608 B), headroom to the
301 066 B budget 577 B. The beta ceiling (300 142 B, band 2 000 B above it,
#699) and the budget are unchanged; lazy editor/onboarding graphs are not
touched.

Issue: #744
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-02 00:56:09 +03:00
claude[bot]andClaude 3d68a2fe23 docs(reviews): индекс после сдвига каталога (#744)
Issue: #744
User-Visible: no
2026-10-02 00:56:09 +03:00
claude[bot]andClaude 2d7a1c9788 docs: review document for #744
Issue: #744
User-Visible: no
2026-10-02 00:56:04 +03:00
claude[bot]andClaude bb9aaefe2a docs: review document for #744
Issue: #744
User-Visible: no
2026-10-02 00:56:04 +03:00
Claude 6ed9aa1652 test(floor): judge both frames of a held resize drag (#744)
smoke_floor_geometry_cache failed in Validate run 36875756451 on one check,
ac2cPreviewWallStandsWhereAFreshCardDrawsIt, with every area check green.
The check read the moving wall only from the live layer (data-kind
"preview"). A host render during a held drag ends that layer: updated()
commits it, and nothing repaints it until the next accepted move. The
settled scene then draws the preview record itself, and its walls equal
those of a fresh card on that record. In the smoke, the "Room updated"
toast from the AC1 rename expires 3.5 s after the save, about when the drag
starts: locally the rename-to-drag gap is 3.47-3.6 s. On a faster runner
the expiry landed after the last move, so the check found no live path. A
light toggled in Home Assistant mid-drag gives the same red.

The drag now starts once the toast is gone, so the live frame normally
judges the live layer. If a host render still lands, the check judges the
settled union the same way; a stale union has none of the moved faces. A
new check takes the settled frame on purpose: a state change during the
held drag, then the union path and the areas must equal the fresh card's.
The live strips never pass through the floor-geometry caches, so the old
check stayed green with a floor key that ignores the preview. The new
check fails on that key, and a failure prints a diagnostic line naming the
judged layer and whether the settled union is the stored one.

Issue: #744
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-02 00:56:04 +03:00
Claude f5b20e07f6 perf(floor): key the floor-geometry caches by the floor's content (#744)
The physical bodies, the wall union pool, the inner room contours and the
clean floor carried the global config epoch in their keys. Every edit of
any floor bumps it, so after one edit every other floor was cold again:
in large-house the first visit to an untouched floor rebuilt its wall
union and paid ~0.7 s flat / ~0.65 s 2.5D instead of ~40-55 ms.

A floor's geometry reads only its own config record (spaceModels) and
constants, so the key is now a content fingerprint of that record
(src/floor-geometry-key.ts), remembered per epoch and per record object.
The geometry also reads the current floor's config next to the model it
is given; when those records differ the key covers both. The live resize
preview is its own record, so preview frames get their own key; the
editor runtime seeds the pool and re-keys the bodies through the same
reader. The stairs editor no longer clears the clean floors of every
floor: the stairs are part of the floor's record.

The #735 switch-cycle guard now also sees the union pool and the inner
contours (optional members of the large-house card contract, so an older
comparison bundle reads 0). smoke_floor_geometry_cache proves the warm other floor and the
invalidation against an independent card (multi-floor push with shared
walls, a stair, a resize preview and its cancel); two mutants guard it.

Issue: #744
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-02 00:56:04 +03:00
claude[bot] c4cb0649ae docs(reviews): индекс после сдвига каталога (#762)
Issue: #762
User-Visible: no
2026-10-01 21:00:48 +00:00
claude[bot] baf0136d9a docs: review document for #762
Issue: #762
User-Visible: no
2026-10-01 21:00:48 +00:00
Matyshandclaude[bot] 6149bcf866 refactor: keep room warm-draft construction in the lazy editor (#762)
Issue: #762
User-Visible: no
2026-10-01 21:00:48 +00:00
Matyshandclaude[bot] e2d38d4e16 test: await warm navigation request before checking its authority (#762)
Issue: #762
User-Visible: no
2026-10-01 21:00:48 +00:00
Matyshandclaude[bot] f2936813af fix: keep warm editor adoption subordinate to user navigation (#762)
Issue: #762
User-Visible: yes
2026-10-01 21:00:48 +00:00
claude[bot] a05458b6f3 docs: review document for #662
Issue: #662
User-Visible: no
2026-10-01 20:52:49 +00:00
claude[bot] 5021c48446 docs: review document for #662
Issue: #662
User-Visible: no
2026-10-01 20:33:15 +00:00
claude[bot] 8a3bf71ef1 docs(reviews): индекс после сдвига каталога (#740)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #740
User-Visible: no
2026-10-01 18:38:03 +00:00
claude[bot] 97d40ea710 docs: review document for #740
Issue: #740
User-Visible: no
2026-10-01 18:38:00 +00:00
claude[bot] 8e4f417f51 docs: review document for #740
Issue: #740
User-Visible: no
2026-10-01 18:27:53 +00:00
Claudeandclaude[bot] b8f662cdf1 docs(screenshots): source fingerprint for #740 on the current dev — all frames pixel-identical
Validate on the conveyor's rebase eb439e81 failed preflight: the branch
changes visual sources (stairs), so the screenshot check is strict on it,
and the fingerprint was stale against the moved dev (#739, #742, #759).
The branch is rebased onto b84465e5 (inventory counts merged with #742:
lifecycle 89, total 204/200) and `docs:accept --identical` re-captured
all 11 frames: pixel-identical, only the source fingerprint moves.

Issue: #740
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 18:27:53 +00:00
Claudeandclaude[bot] d709ea110d perf(stairs): draw all treads of a stair as one path (#740)
A floor with stairs pays for them on every switch to it: the stair layer
is emptied on other floors, so Lit recreates every symbol on each return
and the browser lays out and paints it again. With 250 stairs (the
large-house fixture, the per-floor limit) that was 2,875 SVG elements and
about 40 ms per entry locally; each stair carried 3-7 separate tread lines
with four bound coordinates each.

The treads of one stair are now a single <path class="hp-stair-tread">
with one `M a L b` subpath per tread, in geometry order and with the
numbers the lines carried. Treads of one stair never overlap (straight:
parallel, >= 20 cm apart; spiral: inner ends >= 6.7 cm apart at the
3.6 cm stroke), so the path paints the same pixels at any opacity. The
outline points and the tread data are built once per cached geometry
object (cachedStairMarkup, weak keys), not on every render. The View and
plan-editor layers share the strings; outline, hit polygon, trapezoid,
arrow, attributes and handlers are unchanged. Floor 1 of the fixture
drops from 4,210 to 2,960 elements.

Witnesses: the unit test for the path data and its cache, and the
smoke_stairs markup checks in View and in the plan editor, are red on
dev. The stairs-view-tread-lines mutant restores the View lines.

Issue: #740
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 18:27:53 +00:00
claude[bot] 9f695d8a5d docs: review document for #762
Issue: #762
User-Visible: no
2026-10-01 17:53:24 +00:00
claude[bot] 0606a3664a docs(reviews): индекс после сдвига каталога (#748)
Issue: #748
User-Visible: no
2026-10-01 16:49:55 +00:00
claude[bot] ae3db2bacd docs: review document for #748
Issue: #748
User-Visible: no
2026-10-01 16:49:55 +00:00
Claudeandclaude[bot] c9bf16dec3 docs(reviews): индекс после сдвига каталога (#748)
Issue: #748
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 16:49:55 +00:00
claude[bot] 94bd932153 docs: review document for #748
Issue: #748
User-Visible: no
2026-10-01 16:49:55 +00:00
claude[bot] 134a7029b4 docs: review document for #748
Issue: #748
User-Visible: no
2026-10-01 16:49:55 +00:00
Claudeandclaude[bot] cf7a9cc4fc fix(process): reconcile canon and hints with the pipeline after #707–#730 (#748)
Five places still described the pipeline as it was before code that is
already in dev:

- the S3 hint of the task packet told the author to push the branch, while
  the spec lives in the issue body (§2.3, #517) and nothing is pushed
  before S5 (§11.8);
- process-gate printed «FAIL п.9 Gates: light» for a trailer nobody writes
  or reads, while §10.2 item 9 is the unimplemented release:prerelease
  verdict check. The check is removed; a contract test ties every RULES key
  to an implemented item of §10.2 and every finding number to a RULES key;
- §10.4 item 4 demanded a heredoc in run:, while #723/#730 and their tests
  demand the opposite: commit messages echo line by line into a file,
  comment and summary texts come from code;
- the ship merge comment, AUTHOR.md, REVIEWER.md and AGENTS.md named only
  the pre-beta document, though since #727 the night reads ship code first;
- the nightly publication committed «docs: ship review for nightly …
  перед бетой» with the beta step's Issue: #696. It now has its own
  subject (the document name), body and Issue: #727; the beta message is
  unchanged.

The browser-guard inventory note still said growth above 200 fails
mutation-gate --check; since #699 it is a guideline and --check warns. Its
counts now match the inventory: 205, lifecycle 90.

Issue: #748
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 16:49:55 +00:00
claude[bot] 146da62b0d docs(reviews): индекс после сдвига каталога (#775)
Issue: #775
User-Visible: no
2026-10-01 16:44:29 +00:00
claude[bot] cdfc6e59dc docs: review document for #775
Issue: #775
User-Visible: no
2026-10-01 16:44:28 +00:00
Matyshandclaude[bot] 09fb02cf15 fix(process): bind Validate resume to the current pending round
Issue: #775
User-Visible: no
2026-10-01 16:44:28 +00:00
claude[bot] dd86bf955a docs(reviews): индекс после сдвига каталога (#772)
Issue: #772
User-Visible: no
2026-10-01 16:20:19 +00:00
claude[bot] 97f38ecaae docs: review document for #772
Issue: #772
User-Visible: no
2026-10-01 16:20:14 +00:00
Matysh 50b163c60c fix(ci): close smoke selection and persisted-type risk blind spots
Issue: #772
User-Visible: no
2026-10-01 19:05:09 +03:00
claude[bot] 1f15ee143e docs(reviews): индекс после сдвига каталога (#743)
Issue: #743
User-Visible: no
2026-10-01 15:41:48 +00:00
claude[bot] 0d989b79e2 docs(reviews): индекс после сдвига каталога (#743)
Issue: #743
User-Visible: no
2026-10-01 15:41:48 +00:00
claude[bot] a6076e585e docs: review document for #743
Issue: #743
User-Visible: no
2026-10-01 15:41:48 +00:00
claude[bot] a2578e6f14 docs: review document for #743
Issue: #743
User-Visible: no
2026-10-01 15:41:48 +00:00
Claudeandclaude[bot] b76e0d38ef test(perf): the backdrop budget follows the 2.5D ceiling of #747 (#743)
#747 landed while #743 was in flight and set the switchCycleMs hardMaxMs
of the 2.5D family to 1550. The backdrop budget is the historical isometric
budget under its own profile id (#743 test), so after the rebase its 8000
became the only difference; it now carries 1550 like the rest of the family.

Issue: #743
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 15:41:48 +00:00
Claudeandclaude[bot] b18d366e8c test(perf): 2.5D backdrop twin profile judges one update pass per floor switch (#743)
No Full Performance profile walked the backdrop (imagePlan) path: every
large-house fixture has plan_url null. So the #739 K1 double render -- a
warm 2.5D floor switch with a backdrop cleared the ready paper, inserted
the veil, probed the card background and rendered a second time -- was
invisible to CI by time and structurally; a temporary probe found it.

large-house-isometric-backdrop-v1 is the twin of large-house-isometric-v1
with the shipped f1.svg under a URL of its own on every floor
(plan_aspect 1, room geometry unchanged). The variant is derived in the
runner as plan-snap's is, so demo/fixtures and the bundle fingerprint do
not change. A first stable frame without the backdrop image fails the
sample. After the switchCycle window and its #735 guard, before forced
GC and outside every timed window, a probe makes six warm switches and
counts performUpdate passes until updateComplete resolves true: the K1
second pass starts after the first updateComplete resolves, so a count
taken right after the first await reads one on both sides.

evaluate.mjs rejects a candidate of this profile unless every switch took
one pass, or when the probe is missing; the base is reported, not judged
(v1.78.0 and dev before #739 take two). The budget is a copy of the
historical isometric budget under the new profile id. performance.yml
gains the isometric-backdrop matrix entry with exact-SHA comparison.

Witness: a tree with #739 reverted reads perSwitch 2 in every sample and
benchmark:compare against the branch report throws on the pass count;
v1.78.0 reads 2, the branch reads 1.

Issue: #743
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 15:41:48 +00:00
claude[bot] 134e3e79da docs(reviews): индекс после сдвига каталога (#749)
Issue: #749
User-Visible: no
2026-10-01 15:35:16 +00:00
claude[bot] 30508e1a5c docs: review document for #749
Issue: #749
User-Visible: no
2026-10-01 15:35:16 +00:00
claude[bot] 3c58ad1e73 docs(reviews): индекс после сдвига каталога (#749)
Issue: #749
User-Visible: no
2026-10-01 15:35:16 +00:00
claude[bot] f4ec9d995b docs: review document for #749
Issue: #749
User-Visible: no
2026-10-01 15:35:16 +00:00
Claudeandclaude[bot] ff4e096858 fix(process): integrate runs every pipeline script from one dev snapshot (#749)
The body of _process.yml is read from dev (@dev, #623), so the flags and
formats it passes to scripts are dev's. After "Опубликовать документ ревью"
the working copy of job integrate is the task branch, and a show/ship branch
with a clean merge is not rebased before review: its scripts/ may lag dev by
days. review-doc-guard.mjs silently ignores unknown flags (the anchor lost
#726 route and #737 usage), and a stale merge-candidate.mjs merges the old
way. Only two calls (#723 push refusal, #726 route) were taken from dev, each
with its own extraction, and on ship/reuse the remaining ones ran dev's
version anyway: the script version depended on the path.

Now one step right after setup-node extracts
`git archive origin/dev scripts .github/workflows/validate.yml` into
$RUNNER_TEMP/dev-tools and every repo script of the job runs from there via
TOOLS (review-result-gate, review-doc-guard, reviews-index, merge-candidate,
process-track route, status-label). validate.yml is part of the snapshot
because workflow-jobs.mjs reads it relative to itself; without it ci-proof
answers `failed (#622)` and every code merge would return to S6. The working
copy stays the material: git, the document and paths are judged there.

PROCESS.md §10.4 gets the paragraph "Скрипты конвейера — из dev": the
model_review exception, merges of pipeline changes judged by dev's version,
and compatible edits of the Validate proof contract.

Tests: test/process-integrate-tools.test.mjs is the job contract (no step
calls scripts/ from the working copy, every call goes through the snapshot,
one archive from origin/dev with validate.yml, and the step as is yields a
directory where ci-proof resolves the job contract); publish-push-refusal
runs the publish step and the #413 step on real bash with a task branch whose
review-doc-guard.mjs exits 7 (red with the old call). Existing harnesses take
the snapshot step before the publish and decide steps; the #706 mutant anchor
follows the status-label call.

Issue: #749
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 15:35:16 +00:00
claude[bot] 5c0fe8e79f docs(reviews): индекс после сдвига каталога (#752)
Issue: #752
User-Visible: no
2026-10-01 15:34:54 +00:00
claude[bot] bf0bc1f4a6 docs: review document for #752
Issue: #752
User-Visible: no
2026-10-01 15:34:50 +00:00
claude[bot]andClaude 04084581c0 docs(reviews): индекс после сдвига каталога (#752)
Issue: #752
User-Visible: no
2026-10-01 18:21:52 +03:00
claude[bot]andClaude 341c9fcfce docs: review document for #752
Issue: #752
User-Visible: no
2026-10-01 18:21:31 +03:00
Claude b7dbc21c42 fix(process-metrics): task volume, merge return reasons and spec drafts (#752)
The weekly report disagreed with its own definitions (#728) and with the
pipeline texts that appeared after it (#705, #723, #729).

Volume. A task's volume counted documentation, so the archive move of #682
weighed 333 060 lines and #680/#681 thousands, all landing in the "> 1000"
bucket and shifting the cohort medians; and every commit with a Release:
trailer was dropped, including the task's own golden acceptance and test
re-pinning commits. Volume is now the +/- of class A and B files only.
Only beta commits are left out: the beta or release candidate (Release:
trailer plus the candidate subject or a changed bundle, bundle-policy.mjs,
drops every Release: commit, so it is not reused. A task whose commits
touch only docs/reviews/** has no volume and is no longer read as
infrastructure: the pipeline writes those documents, not the task.

Return reasons. Every non-merge outcome of merge-candidate.mjs fell to
"unknown". merge-candidate.mjs now exports a sign for the heading of each
outcome comment (OUTCOME_SIGNS; the step-failure text moved into
commentFor as 'error', byte for byte), and a test on the templates
themselves holds every case to its own sign. The report maps merge-stage
outcomes after a green verdict to "merge" and a push refused while
rebasing before review to the new "push-refused" reason.

Spec drafts. A new section after "По трекам" counts the S4-spec-review
epochs on the ask track for tasks whose first S5-ready falls in the
window, the epochs with a "Черновик:" comment (§7.2) and whether the draft
went to S5 or was thrown at S3, Spec-Draft: commits in dev for the window,
and S5 -> S7 per track for tasks with and without a draft, "мало данных"
under three. The snapshot also reads timelines of tasks in S5-S7.

Three #728 assertions pinned the old behaviour (a Release: fixture without
the candidate subject, and the rebase workflow refusal read as unknown);
they now expect the new definitions.

Issue: #752
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 18:21:30 +03:00
claude[bot] 14ea266112 docs(reviews): индекс после сдвига каталога (#746)
Issue: #746
User-Visible: no
2026-10-01 14:55:10 +00:00
claude[bot] 255677556f docs: review document for #746
Issue: #746
User-Visible: no
2026-10-01 14:55:10 +00:00
Claudeandclaude[bot] 953f15b967 fix(plan): write every room state's transparency as colour plus opacity (#746)
`.room { transition: 0.12s }` interpolates a room's colour and its
fill-opacity / stroke-opacity independently, and the visible opacity is their
product. The states without a fill kept their alpha in the colour with the
default opacity 1 (.overlay transparent, .yard rgba 0.14, .outlined rgba
0.06 / 0.55, .picked rgba 0.25), while .styled writes an opaque colour plus
fill-opacity: var(--room-fill-op). On a change between the two on the same
node one half rose while the other fell, and mid-way the room was darker than
at either end. Opening the space settings on a floor with no fill (the dialog
shows "no fill" as its own colour at alpha 0) flashed every room grey for
~0.1 s, 0 -> 0.241 -> 0; cancelling the dialog after a preview, entering and
leaving the plan editor briefly darkened the fill (0.18 -> 0.317 -> 0.06).

Every .room state now writes an opaque colour plus *-opacity, and
transparent only together with a zero opacity. The transition itself,
.styled and the --room-* variables are unchanged; the space card takes the
same styles. The resting paint is the same: the witness records each state's
colour and visible opacity as dev drew them, and screenshots of seven resting
states (View without fill, with fill and borders, plan editor, room picked for
a merge, yard with and without borders, yard in the plan editor) are
pixel-identical to dev outside the plan editor's tool hint, whose text shifts
by a sub-pixel between runs on dev too.

Witness: new demo/smoke_room_fill_transitions.mjs, deterministic. A
MutationObserver pauses the room's transitions at their first frame right
after Lit commits, and the smoke seeks them through 0..120 ms in 15 ms steps.
Red on dev: fill overshoot 0.241 / 0.125 / 0.137 / 0.134 on the four paths,
stroke 0.241 / 0.242 on the first two. A real colour change of the same room
still runs a fill transition (catches `transition: none`).

A card-mod rule that sets only `fill` on an unfilled room now meets
fill-opacity 0; CHANGELOG and STYLING-HOOKS say to set the opacity with the
colour (the values are generated, STYLING-HOOKS §3.3).

Issue: #746
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 14:55:10 +00:00
claude[bot] c86e4bdd31 docs(reviews): индекс после сдвига каталога (#761)
Issue: #761
User-Visible: no
2026-10-01 14:35:55 +00:00
claude[bot] 24ea768609 docs(reviews): индекс после сдвига каталога (#761)
Issue: #761
User-Visible: no
2026-10-01 14:35:55 +00:00
claude[bot] 30dcc5b2cb docs: review document for #761
Issue: #761
User-Visible: no
2026-10-01 14:35:55 +00:00
Claudeandclaude[bot] a22f6f5411 fix(process-metrics): count model tokens only for the report window (#761)
tokenUsage summed the usage line of every review document in HEAD: the
report had no window, and every week repeated the whole history.

A document now enters a week's tokens when the commit that added it to
dev falls in [since, until]. fetchSnapshot reads the committer date from
git log -M --diff-filter=AR over docs/reviews and legacy/reviews:
an add sets the date, a rename (the #682 archive move) carries it to the
new path instead of adding the document again. The "missing" count of
#737 (hp:usage-none) follows the same window. ship findings keep reading
every SHIP-REVIEW document; only the token sum is windowed. Without the
date map (a unit over ready documents) there is no window, as before.

Proof is a temporary git repository with dated commits: a document
outside the window, one inside, an hp:usage-none pair on both sides and
an archive move inside the window; only the inside documents count.

Issue: #761
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 14:35:55 +00:00
claude[bot] aa910c31a3 docs(reviews): индекс после сдвига каталога (#751)
Issue: #751
User-Visible: no
2026-10-01 14:31:06 +00:00
claude[bot] 72975e7514 docs: review document for #751
Issue: #751
User-Visible: no
2026-10-01 14:31:06 +00:00
Claudeandclaude[bot] 25001ef7ab fix(ci): pipefail before every | tee, API base from GITHUB_API_URL (#751)
No workflow sets `shell:`, and GitHub runs such a step as `bash -e {0}`,
without pipefail: the exit code of `… | tee` is tee's, and a failing left
side passed silently. Three steps were unprotected:
- _process-resume.yml: an exception of process-resume.mjs (gh, API) left the
  step green and the resume event was lost until process-reconcile;
- release-review.yml: a failed `prepare` went on with an incomplete
  GITHUB_OUTPUT and proceed=true;
- validate.yml: a failed `classify-changes.mjs --heavy` left `heavy` empty,
  heavy jobs were skipped and job `changes` stayed green.
Each gets `set -o pipefail` as the first line of `run` (validate.yml's step
becomes a block), following #727 and #472. test/workflow-pipefail.test.mjs
walks every .github/workflows/*.yml: a `| tee` line in `run` must follow
`set -[a-z]*o pipefail` or the step must have `shell: bash`; on the old tree
it names exactly the three places, and the _process-resume and validate
steps run on real bash under `bash -e` with a failing node.

ci-proof.mjs exports githubApiBase(env) (GITHUB_API_URL or
https://api.github.com, no trailing slash); githubCandidateTree,
loadGithubProofContext and release-gate's workflowRunsUrl take `apiBase`
with that default instead of the hardcoded host. night-red.mjs passes the
base directly and drops the fetch wrapper that rewrote the prefix. On
github.com the runner's GITHUB_API_URL is the same host, so behaviour there
does not change; archive_download_url stays as the API returned it.

The `mode` input for ship-review is out of scope (thin file in main, #716).
Thin files are not touched: _process-resume.yml is a body, validate.yml and
release-review.yml are not thin.

Issue: #751
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 14:31:06 +00:00
claude[bot] d93bcf2628 docs(reviews): индекс после сдвига каталога (#750)
Issue: #750
User-Visible: no
2026-10-01 14:27:08 +00:00
claude[bot] 09bf6a95ec docs: review document for #750
Issue: #750
User-Visible: no
2026-10-01 14:27:08 +00:00
Claudeandclaude[bot] de3e1f00e1 fix(process): free the reviewer prompt budget, rules stay in REVIEWER.md (#750)
The code-review prompt sat at exactly 1 400 of its 1 400 words (#634), so
any new line turned the budget test red, and #707/#726 already had to route
their notes through job outputs. Part of the text was dead or a retelling
of the reviewer digest, which #634 says the prompt must not repeat:

- the mutants fragment of the track line: since #709 `mutants` is always
  false, so «прогнаны Validate» was unreachable;
- «Отсутствие мутантов по диффу — не находка» in the show line: a rule of
  every track, already in REVIEWER.md «Трек show» and §10.4;
- three retellings — the repeated round, the gate scope and the severity
  paragraph — now one-line references to the REVIEWER.md sections. The ban
  on a separate issue for an in-scope Medium stays in the prompt: the model
  files issues itself, and that mistake is expensive.

What only the prompt said moves into REVIEWER.md with links to the canon:
the spec delta is the diff of the issue body, a doubt about locality means
a full review with a stated reason, the three smoke-select answers
(docs/TESTING.md), and geometry without invariants in the report is an
unrun gate.

The prompt is now 1 130 words; the 1 400 threshold stays, the difference
is headroom. A new test ties every «docs/process/REVIEWER.md, «X»»
reference in the prompt to an existing `## X` section.

Issue: #750
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 14:27:08 +00:00
claude[bot] 653d94ef09 docs(reviews): индекс после сдвига каталога (#753)
Issue: #753
User-Visible: no
2026-10-01 14:03:19 +00:00
claude[bot] 475f391cf9 docs: review document for #753
Issue: #753
User-Visible: no
2026-10-01 14:03:19 +00:00
claude[bot] 3078962b64 docs(reviews): индекс после сдвига каталога (#753)
Issue: #753
User-Visible: no
2026-10-01 14:03:19 +00:00
claude[bot] 6abb589f0c docs: review document for #753
Issue: #753
User-Visible: no
2026-10-01 14:03:19 +00:00
Claudeandclaude[bot] 4aa6c85f98 fix(process): the #562 infra entry no longer covers an issue in S3/S4 (#753)
Rule 8 skipped the status check for any range without class A files, so a
task in S3-spec or S4-spec-review could push a branch of tests, demo or
scripts with only a warning. §11.8 forbids exactly that: before S5 neither
class A commits nor the branch itself is pushed, because the spec-review
step takes the freshest origin/issue/<NN>-* as its material and lays the
SPEC-REVIEW document there, putting code in front of a spec reviewer who
must not read it (§2.4).

The #562 entry was written for a task before its first S status. The
decision is now made per issue in checkIssueStatuses: the status stays
optional only when the range is infrastructural and the issue carries
neither S3-spec nor S4-spec-review. Such an issue gets a rule 8 refusal
naming its status and §11.8; the range-wide #562 warning is still printed.
No status, S1-new/S2-analysis (reviewer-filed infra issues) and S5-S8 keep
their old outcome; closed, blocked and fail-closed checks are untouched;
rule 10 is still called only for ranges with class A.

PROCESS.md §10.2 gets the one-sentence exception, the mutation registry a
mutant that drops the S3/S4 condition.

Issue: #753
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 14:03:19 +00:00
claude[bot] 9deed2a3c3 docs(reviews): индекс после сдвига каталога (#756)
Issue: #756
User-Visible: no
2026-10-01 13:56:37 +00:00
claude[bot] 80b63b7ee8 docs: review document for #756
Issue: #756
User-Visible: no
2026-10-01 13:56:37 +00:00
claude[bot] 024583bd22 docs: review document for #756
Issue: #756
User-Visible: no
2026-10-01 13:56:37 +00:00
Claudeandclaude[bot] f0e1c0b8cc fix(warm): revive the editor draft when write access arrives late (#756)
A same-route remount that cannot edit yet - hass arrives after the
element is inserted (the demo's own order), or a non-admin waits for the
server's can_write - keeps the editor in _pendingNavMode and enters it
later through _resumePendingNavMode -> _setMode. The draft revival was
wired only into the immediate warm adoption (_requestMode(..., adopt)):
on the pending path the draft was lost, _warmRevivePending stayed up for
the instance's life, _warmSnapshot stopped writing dlg, and the next
remount brought back the predecessor's stale draft.

The adoption tail (draft revival once under a held refit, then the
settled stage as the refit baseline) is shared by both paths: the
sequencing lives in src/warm-mode-adoption.ts, the refit bookkeeping in
the card's _holdWarmRefit/_releaseWarmRefit. The pending mode still enters through _setMode, the transition
authority smoke_nav_persist holds it to; resumeWarmMode then settles the
revival. _setMode ends the passive boot grace and refits the camera to
a header measured before the editor chrome rendered, so a camera the
pending window left untouched is put back and held exactly as an
immediate adoption holds it; a camera that has already moved on (View
refit, the user's pan, another space) is left to the ordinary refit. A
mode that did not commit, an explicit mode command in the pending window
and a route departure settle the revival too: no outcome leaves
_warmRevivePending up. The core file gives back 4 lines.

smoke_warm_dialogs gains section H through the UI: the three late-write
orders keep mode, draft, dirty baseline and a frame-by-frame identical
viewport; the chain carries this instance's draft, not the predecessor's;
a space switch in the pending window eats the draft. 14 checks are red
on dev. The mutant warm-pending-mode-leaves-revive-waiting is guarded by
the smoke; docs/WARM-REMOUNT.md §2 describes the pending editor.

Issue: #756
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 13:56:37 +00:00
Claudeandclaude[bot] 86a301f618 fix(ci): the screenshot fingerprint is only a warning on a task branch (#760)
The pipeline dispatches a full Validate for a `ci:golden` task, and
`screenshotsGateMode` read `full=true` as strict on any ref. Between betas
the source fingerprint on dev is legitimately stale (#479: re-captured for
the beta candidate), so every visual task failed preflight on the
conveyor's material until its author re-ran `docs:accept --identical` on
the current dev - #718 and #740 both did, and two visual tasks in a row
could not merge without it. On a task branch the mode is now `warn` even
with `full=true`; dev, main, PRs, the schedule and Release: candidates stay
strict.

Issue: #760
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 13:51:39 +00:00
claude[bot] 1abd65a2be docs(reviews): индекс после сдвига каталога (#747)
Issue: #747
User-Visible: no
2026-10-01 13:42:35 +00:00
claude[bot] 62a2e3b05b docs: review document for #747
Issue: #747
User-Visible: no
2026-10-01 13:42:35 +00:00
Claudeandclaude[bot] fa18ca81c9 test(perf): pull the switchCycleMs ceilings down to the warmed level (#747)
Since #735 switchCycleMs times the warmed twelve-switch cycle, and the
absolute ceilings of 7000 ms (flat) and 8000 ms (2.5D) sat 7.6-10.2
times above the level. performance_smoke judges only these ceilings, so
between full runs the warm floor switch that #694/#725 just sped up was
guarded only against a several-fold collapse.

Series: every Full Performance run after #735, both sides (the base is
measured by the candidate runner, so it is warm too), 7 samples each -
36821241343 (#735, base 76558bf2), 36838891001 (#740), 36838952536
(#742) and 36839009721 (#739), the last three against dev 7ff2b5ae.

  flat  large-house-v1 / plan-snap / interaction   666.9-812.7 ms
  2.5D  large-house-isometric / stage3-dense       766.5-1333.9 ms

The 2.5D maximum is the dense pair of 36838952536, whose base on the
same runner read 1249.7 ms against 849.9-982.4 ms elsewhere: runner
noise the series is meant to contain. No 3-sample performance_smoke
median is in the series yet; those profiles join Validate only on a
src/** diff.

Rule (as #692, #675 falls in the same band): one number per family, the
first multiple of 50 ms at or above 1.15 x M and no higher than 1.2 x M,
M being the family's maximum median: flat 1.15 x 812.7 = 934.6 -> 950
(+16.9 %), 2.5D 1.15 x 1333.9 = 1534.0 -> 1550 (+16.2 %). One number
per family keeps the smoke = full (#473 AC4), plan-snap/interaction
"every original ceiling" and dense = historical (#160) contracts; the
price is wider headroom for the faster profiles. The base-relative
comparison of the full workflow (0.35 / 0.2, 250 ms) is unchanged and
stays the detector for smaller growth.

The new test pins both families: one ceiling in every file of a family,
every point of the series passes the smoke budget with --absolute-only,
the ceiling follows the rule and stays inside [1.15, 1.2] x M, doubling
the level fails, and the full profiles' ratio and noise allowance are
unchanged. 7000 left in any flat file or a ceiling under 1.15 x M reds
it. The README records the series and the reasoning.

Issue: #747
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 13:42:35 +00:00
claude[bot] ffa13c6628 docs(reviews): индекс после сдвига каталога (#745)
Issue: #745
User-Visible: no
2026-10-01 12:30:46 +00:00
claude[bot] 5e1b508215 docs: review document for #745
Issue: #745
User-Visible: no
2026-10-01 12:30:45 +00:00
claude[bot] 88702eedc6 docs(reviews): индекс после сдвига каталога (#745)
Issue: #745
User-Visible: no
2026-10-01 12:30:45 +00:00
claude[bot] d4eea609cf docs: review document for #745
Issue: #745
User-Visible: no
2026-10-01 12:30:45 +00:00
Claudeandclaude[bot] dc77852e0b fix(space-card): key room shapes by space and id so a config change never repaints a stranger (#745)
The space card drew its rooms with a bare map(), so Lit reused room nodes by
position, and `.room { transition: 0.12s }` (planStyles is part of this card's
styles too) drew a node's fill and stroke in from whichever room held it
before. Two paths change the room set in the same DOM: a new `space` in
setConfig of the same element (the card editor's preview), and a config event
from any device that inserts, removes, reorders or re-zones a room of the
shown space. Filled rooms faded out and back in for ~0.12 s, unfilled ones
briefly darkened in a filled room's place.

The list is now keyed(space.id, repeat(rooms, (r, i) => r.id || i, ...)),
the same shape as the full card after #742: the outer key handles the space
change, the inner one keeps a node bound to its room inside a space. An
id-less room keys by its numeric index, which never equals a string id. The
transition itself stays: it smooths a real fill change on the same room. The
#742 note in plan.styles.ts now names the space card as well.

Witness: a new section of smoke_space_card. The config is delivered by a
server push (__hpTest.setServerConfig), the event the card subscribes to.
Red on dev: node r1 reused for g1 with fill/fill-opacity transitions and a
first-frame fill of rgba(0, 0, 0, 0) / 0; a room inserted first shifts all
four nodes and replays fill transitions. A real custom_fill change still runs
a fill transition on the same node (catches `transition: none`). One mutant:
inner key replaced by map(), guarded by AC2 (checked by hand: red).

Issue: #745
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 12:30:45 +00:00
claude[bot] f10c5b4374 docs(reviews): индекс после сдвига каталога (#754)
Issue: #754
User-Visible: no
2026-10-01 12:24:59 +00:00
claude[bot] 9e64fc80ee docs: review document for #754
Issue: #754
User-Visible: no
2026-10-01 12:24:59 +00:00
Claudeandclaude[bot] d8e09cd1a0 test(harness): close three smoke-select and fixture blind spots (#754)
Three independent blind spots in the test harness.

1. smoke-select read symbols only from changed lines of a --unified=0
   diff. An edit to the arguments of a multi-line call names nothing:
   #741 (d5bdfde9) changed only the arguments of
   runtime.resolveIsoOverlayFitEnvelope({ on the line above, and the
   selection answered "unproven" plus the visual minimum, although the
   callee is registered in smoke-links for smoke_iso_flat_parity and
   smoke_isometric_contract - the two smokes the #741 author ran by hand.
   The selection diff now carries CALL_CONTEXT_LINES = 3 lines of
   context; for each changed line parseDiff looks for the nearest
   unclosed "(" above it within the hunk, walking through a literal
   argument ({ or [ after "(", "," or "["), stopping at ";" on depth zero
   or any other unclosed brace. A callee from the symbol table joins
   symbols and the new callees field and is marked "(вызов)" in the
   report. Context lines never give direct symbols. task-packet takes a
   separate context diff for selectSmokes; change-risk keeps --unified=0.
   Over the last 80 src commits of dev: 16 commits gain a callee, 2 move
   from unproven to a proven link (#741, #724 5f8e8ca7), +15 smokes in
   total, at most 4 per commit, none lost.

2. The #732 dead-field check judged only scene-builder calls. The four
   resolveIsoOverlayFitEnvelope({...}) literals in iso-scene-render tests
   went straight into the test-build function, so stageSize: null (the
   field #741 removed) stayed green. They now go through overlayFit typed
   with OverlayFitFixture (keys of IsoOverlayFitEnvelopeInput); the check
   judges overlayFit/resolveIsoOverlayFitEnvelope calls like the scene
   builders, and its probe asserts that OverlayFitFixture rejects
   stageSize, so the type resolved to the real input and not to any.

3. smoke_backdrop's mode() called the private _setMode and slept 220 ms.
   It now enters a mode through __hpTest.setMode and waits for the end of
   the transition by the same markers as section 6b (#715): one page
   helper used by both. Oracles and the 59 check names are unchanged.

Witnesses: d5bdfde9 selects both iso smokes with no "unproven"; the same
fixture without context lines is unproven again; attribution disabled
reds both AC1 units. stageSize: null in an overlayFit call reds the first
#732 test; a direct resolveIsoOverlayFitEnvelope({...}) reds the third.
smoke_backdrop is green normally and with animation frames slowed to 60
and 150 ms; a stage animation that never ends fails with a named error.

Issue: #754
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 12:24:59 +00:00
claude[bot] da8edb1f0e docs(reviews): индекс после сдвига каталога (#755)
Issue: #755
User-Visible: no
2026-10-01 12:21:13 +00:00
claude[bot] 1265036d2a docs: review document for #755
Issue: #755
User-Visible: no
2026-10-01 12:21:13 +00:00
Claudeandclaude[bot] a7b02db609 fix(process): import and type-only lines no longer raise risk (#755)
The risk table of #707 judged every non-comment line of a class A file as
code. On the history since 15.09 that raised #741 from ship to show for a
removed interface member that never reaches JS, and put false classes on
#624 (removed imports), #693/#694 (stairs-view is rendering, not geometry)
and #725 (the config fingerprint memo is not the config schema).

- Lines of module syntax and TypeScript types give no risk, like comments:
  `import …`, `export … from …`, `export type …`, the head of `interface X`
  or `type X =`, and the lines inside such a block (indented, plus the
  closing line). The block state per side of a change block starts from the
  hunk context git writes after `@@ … @@` and follows every unindented line
  of the block, so a member under `@@ … @@ export interface X {` and a whole
  interface added in one hunk are judged alike. Only `.ts`, and not in the
  `migration` area: there the types are the config contract (#588, #649).
- `stairs*` is narrowed to the stairs model (`stairs`, `stairs-box`,
  `stairs-editor-model`); `config-*` to writing and adopting the config
  (`config-adoption`, `config-store`, `config-reload-authority`,
  `config-write-conflict`).
- A replaced line is one piece of evidence: a removed line whose counterpart
  in the same change block hits the same class is folded into it instead of
  printing `path:N (удалена)` next to `path:N`.

classifyRisk stays a pure function over the diff text. On the history the
raising classes change for #741 (none), #693 (visual only), #694 (no
geometry), #725 (perf only) and #624 (no devices/perf); migration on #588,
#612, #649 and #661 stays. PROCESS.md §5 names the new exemption.

Issue: #755
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 12:21:13 +00:00
claude[bot] 8070357266 docs(reviews): индекс после сдвига каталога (#737)
Issue: #737
User-Visible: no
2026-10-01 12:18:13 +00:00
claude[bot] 2095a40f41 docs: review document for #737
Issue: #737
User-Visible: no
2026-10-01 12:18:13 +00:00
Claudeandclaude[bot] 9c4a45a6b2 test(pre-push): the hook fixture carries model-usage.mjs (#737)
Validate on the conveyor's rebase d1954183 was red on one unit: the real
pre-push hook test (#633 AC1) copies every module the hook runs into a
temporary repo, and since #729 process-gate pulls in review-doc-guard,
which now imports scripts/model-usage.mjs. The copy lacked it, so the hook
died on ERR_MODULE_NOT_FOUND instead of reporting a red gate:small. The
module joins HOOK_FILES next to the #729 ones.

Issue: #737
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 12:18:13 +00:00
Claudeandclaude[bot] f5a6b47b39 feat(process): the pipeline records model usage in the review document's machine block (#737)
The weekly process metrics weigh tracks and the nightly ship review in the
order quality, speed, tokens (#707), but the third axis had no source: the
claude-code-action step hides usage from the Actions log on purpose, nothing
read its execution_file, and the #728 reader printed "no data" every week.

scripts/model-usage.mjs is the single module that builds and parses the line:
`<!-- hp:usage input_tokens=N output_tokens=N cache_creation_input_tokens=N
cache_read_input_tokens=N num_turns=N -->` (sums over every model in the last
`result` message, `result.usage` when modelUsage is absent) or
`<!-- hp:usage-none reason=<code> -->`. Only the result message is read; the
rest of the file holds tool results, and no byte of it is printed.

A new step right after Review in both model_review jobs (always(),
continue-on-error) hands the line out as the job output `usage`. Usage is a
reporting figure like the stage duration, so it travels as a job output and
not through the sealed artifact: REQUIRED_FILES and the #556 gate are
unchanged. Publication treats the line as untrusted input and writes the
normalized form as the last line of the anchor block (review-doc-guard
--anchor --usage=) or right after the SHIP-REVIEW block; empty becomes
reason=missing, anything off-format reason=invalid.

The #728 reader now takes the line only from the machine block: a reviewer
quoting the previous round in prose no longer doubles its usage, and
"no data" is counted as missing, never as zero. PROCESS.md §10.4 documents
the source, the format and why it is a job output.

Issue: #737
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 12:18:13 +00:00
claude[bot] 707cb18f7e docs(reviews): индекс после сдвига каталога (#757)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #757
User-Visible: no
2026-10-01 12:12:15 +00:00
claude[bot] 929044b932 docs: review document for #757
Issue: #757
User-Visible: no
2026-10-01 12:12:15 +00:00
Claudeandclaude[bot] 4091d83af3 fix(editor): a render no longer restarts a failed runtime load (#757)
The lazy-runtime contract (#353) says a non-terminal failure waits for
the next explicit intent and that there are no background retries. But
_renderBody calls ensure() on every repaint while a surface waits for
the editor or onboarding runtime, and to the loader that call was
indistinguishable from an intent. Surfaces the core opens without the
runtime - the kiosk size dialog after a 3 s hold, the floor import
wizard on an empty plan, a dialog a warm remount revives - therefore
turned one failure into a loop: the loader's own state change, the
toast and its expiry, every hass tick repainted, started a new cycle
and showed a new toast every ~3.5 s. A wall tablet whose old hashed
chunks answer 404 after an integration update sat in that loop forever.

EditorRuntimeLoader.ensure takes an intent: the render calls it as
'reconcile'. A reconcile starts the first cycle a surface needs, but
after a non-terminal failure it returns false without loading until an
explicit ensure() - a tab, an opener, _requestMode, "Add space" - has
started a new cycle. Explicit calls, the terminal fingerprint failure,
ready and an in-flight cycle behave as before, for every loader
instance. The card's render lines stay line-neutral.

smoke_lazy_editor_chunk gains the three surfaces offline through their
real paths (a 3 s touch hold on a kiosk card, an empty plan pushed by
the server, General settings revived by a remount): one cycle, one
notice and an idle loader over 8 s, then the Plan tab and "Add space"
heal. On dev: 4 requests / 3 notices, 6 / 2 and 3 / 2. The loader unit
test pins reconcile versus intent; the mutant
render-reconcile-restarts-editor-runtime-cycle is guarded by the smoke.

Issue: #757
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 12:12:15 +00:00
Claude b84465e541 test(smoke): footer-width waits for the locale gate and its own dialog (#759)
smoke_dialog_footer_width switched the language by assigning
card._config and then measured the first hp-dialog in the tree. Since
#627 the main catalog for de/fr and the editor's settings/support/topology
dictionaries for ru/de/fr are lazy chunks; while one is in flight the
language gate keeps the previous frame (inert, aria-busy) and the dialog
is not rendered. The old wait only covered de and only the main catalog
(card._t('btn.save') === 'Speichern'), so under load the first dialog
after a switch (opening in ru/de) was read from the held frame and four
checks went red on a zero row.

Both page.evaluate blocks now wait by condition, like
smoke_dialog_polish_603 (#712): first for the gate's own markers (no
aria-busy, lang equals the requested language), then for
hp-dialog[data-kind=<kind>] to have its .dialog-action-footer laid out,
with a 5 s deadline and a named error. The measurement reads the dialog
of the requested kind instead of the first hp-dialog. Checks, names and
thresholds are unchanged (same 36 names under HP_SMOKE_CHECKS=1).

Runs on the branch: 10/10 sequential, 12/12 in 6 rounds of two parallel
copies (dev: 9/12 red under the same load); green with ru/de chunks
delayed 400 ms and 1500 ms and with only the editor dictionaries delayed
150 ms. Sabotage still bites: opening --hp-dialog-wide-width 560px reds
opening_*_medium_shell, physical footer buttons min-width 170px red
physical_*_three_actions_one_row and _positive_localization_headroom,
and an opening dialog that never renders fails with a named error.

Issue: #759
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 14:12:57 +03:00
claude[bot] 7ce3f6629f docs(reviews): индекс после сдвига каталога (#742)
Issue: #742
User-Visible: no
2026-10-01 10:13:50 +00:00
claude[bot] 33a787a151 docs: review document for #742
Issue: #742
User-Visible: no
2026-10-01 10:13:50 +00:00
Claudeandclaude[bot] a0f72280ee fix(plan): key room shapes by space and id so a floor switch never repaints a stranger (#742)
The flat room list was a bare map(), so Lit reused room nodes by position.
On a floor switch the previous floor's room node became the new floor's room
and `.room { transition: 0.12s }` drew its fill and stroke in from the old
computed values: one paper-white frame, then two or three frames darker than
the final fill (alpha rises while fill-opacity falls), then the fill. Between
two filled floors the fill bled in from the other floor's colour.

The list is now keyed(space.id, repeat(rooms, (r, i) => r.id || i, ...)), the
shape #534 settled on for openings and markers. The outer key handles the
floor switch, including room ids repeated on two floors (ids are unique only
within a space); the inner key keeps a node bound to its room inside a space,
where inserts, merges, splits and the editor filter shift positions. An
id-less room keys by its numeric index, which never equals a string id. The
transition itself stays: it smooths hover and a real fill change on the same
floor.

Witness: a new section of smoke_space_switch_transitions, before physicalize
(after it the first room changes template branch and the node is recreated
anyway). Red on dev: five transitions on g1, node r1 reused for g1, computed
fill rgba(0, 0, 0, 0) / 1; room nodes swapped by an insert; same-id case
reuses r1. A real custom_fill change still runs a fill transition (catches
`transition: none`). Two mutants: inner key removed, outer key removed.

Issue: #742
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 10:13:50 +00:00
claude[bot] 88985d2e4c docs(reviews): индекс после сдвига каталога (#739)
Issue: #739
User-Visible: no
2026-10-01 10:06:38 +00:00
claude[bot] c629c9c238 docs: review document for #739
Issue: #739
User-Visible: no
2026-10-01 10:06:38 +00:00
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
claude[bot] 37238f3990 docs: review document for #744
Issue: #744
User-Visible: no
2026-10-01 10:04:12 +00:00
Claudeandclaude[bot] 5a49c8c32d test(moon): the static-sky AC15 test waits for the moon chunk itself (#758)
The scheduled mutation gate runs the #718 guard alone
(`--test-name-pattern="#718"`), and there the AC15 test was red on clean
code, so three shards failed with "guard red without a mutant". The test
was synchronous and relied on `#661 C7`, earlier in the file, having
loaded the lazy moon chunk; until the chunk arrives `moonLayer` returns
`nothing` by design (#661 C7). The test now awaits the chunk through
`withMoon` before its checks. The guard command is green alone (6/6) and
the whole file stays green (20/20).

Issue: #758
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 09:05:19 +00:00
claude[bot] 8b7725acb1 docs(reviews): индекс после сдвига каталога (#729)
Issue: #729
User-Visible: no
2026-10-01 08:57:25 +00:00
claude[bot] c84c430b2e docs: review document for #729
Issue: #729
User-Visible: no
2026-10-01 08:57:19 +00:00
Claude 0e44f8a69f feat(process): a track:ask draft may be written during the spec review (#729)
On track:ask every spec review round is 10-45 minutes of waiting, and
rule #1 kept the author idle for all of it. Rule 10 (#738) judges a
class A commit by its author date, so code written in the
S4-spec-review epoch was always refused: nothing told a draft written
against the reviewed text from a violation. The owner allowed changing
rule #1 for this (decision 2026-10-01).

A draft commit carries `Spec-Draft: sha256:<issueBodyDigest(body)>`,
the hash the pipeline already writes as "Тело issue" into the review
document anchor. Rule 10 accepts a class A commit written in S4 only
when its S4 epoch (a repeated S4 does not restart it) was closed by
S5-ready, the track at the author date was ask, and the trailer equals
the body of the green, High 0 SPEC-REVIEW added inside that epoch, read
from the range head or origin/dev. The first failing check is the one
finding: trailer format, track, how the epoch ended, the missing
document with a `git fetch origin dev` hint, or both hashes and the
document name. A trailer on a commit written in an allowed epoch is a
warn. Without the document reader rule 10 is exactly #738; main always
passes one, and it reads git only when the range holds a draft.

The task packet tells S4 on ask that a local draft is allowed while the
branch stays closed, prints the trailer line, and in S5/S6 names the
green spec review, whether the body changed since, and the --report
check before push. SPEC-REVIEW documents are a separate input
(specDocs) from the branch and origin/dev, so the previous verdict and
the AC witness keep their source.

PROCESS.md gets §11.8 and the points that refer to it (§1, §2.4-2.6,
§3 item 1, §7.2, §9, §10.2 item 10, §12); AUTHOR.md, REVIEWER.md and
AGENTS.md follow, with three new key rules in process-digests. The
pre-push hook fixture copies the modules process-gate now imports.

Issue: #729
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 11:47:01 +03:00
claude[bot] 7ff2b5aeaa docs(reviews): индекс после сдвига каталога (#731)
Issue: #731
User-Visible: no
2026-10-01 06:47:05 +00:00
claude[bot] 4097132374 docs(reviews): индекс после сдвига каталога (#731)
Issue: #731
User-Visible: no
2026-10-01 06:47:05 +00:00
claude[bot] a729b5000d docs: review document for #731
Issue: #731
User-Visible: no
2026-10-01 06:47:05 +00:00
Claudeandclaude[bot] 36ebabaaec fix(moon): the status line after a warm revive of General settings (#731)
#718 K7 takes the moon status once per opening of General settings,
outside the draft. A warm remount revives the open dialog on a new card
instance, but `_warmReviveDialog` restored only the draft: the new
instance had no opening of its own, so the "Now: ..." line never came
back.

A revive is an opening too. The `settings` branch now asks for the
status the way `_openSettingsDialog` does - through the lazy editor
runtime (`_openMoonStatus` -> `openMoonStatus`): at once when the
runtime is there (an editor revives after `_requestMode(..., adopt)`
has installed it), after it loads in View; once per revive and only
while that revived dialog is still open. The snapshot of now,
`hass.config` and `sun.sun` is the revive's own, nothing of the dead
instance's opening is carried over, the draft key and the dirty flag do
not change. The View graph gets no static moon-status import; other
dialog kinds never ask for the moon chunk.

demo/smoke_moon_status.mjs gains the revive scenarios - View, the plan
editor, a revive while the chunk is still loading, a space-dialog
revive that must not load the chunk; the first three are red on dev.
test/moon-settings.test.mjs executes the revive as a new opening; the
wiring itself is proven by the smoke, not by reading the monolith as
text (#624). docs/SUN.md and docs/WARM-REMOUNT.md say a revive is an
opening; scripts/smoke-links.mjs links the two new symbols to the smoke.

Issue: #731
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 06:47:05 +00:00
claude[bot] de117d5042 docs(reviews): индекс после сдвига каталога (#735)
Issue: #735
User-Visible: no
2026-10-01 06:41:26 +00:00
claude[bot] 6372c21eec docs: review document for #735
Issue: #735
User-Visible: no
2026-10-01 06:41:26 +00:00
Claudeandclaude[bot] 5e31816881 test(perf): time switchCycle warm and fail on a floor build inside it (#735)
Every large-house sample mounts a new card that has visited only floors 1
and 2 before the twelve-switch cycle, so the cycle's second step was always
the first visit to floor 3: 20 new clean-floor entries, and in 2.5D one Iso
geometry entry plus one structural build. In large-house-interaction-v1 the
editor series also moves the config epoch that keys the clean-floor cache,
so floor 1 was cold as well. That one cold step was about half of
switchCycleMs, which the README and the cycle comment describe as warmed
navigation, and a 35% warm regression drowned in it.

The runner now visits every fixture floor once in cycle order after the
settings dialog closes, outside every timed and Long Task window, and
returns to floor 2, so the cycle still starts with 2 -> 1. A guard snapshots
the hot caches and the 2.5D structural build counter around the window and
fails the sample when anything grew. Caches an older base lacks read as 0
and its null counter is not judged, so a v1.78.0 base still passes.

Budgets, hardMaxMs, metric names, the report schema, profiles and the
workflow are unchanged. Base and candidate are both measured by the
candidate runner, so the comparison is unaffected; the absolute
switchCycleMs level steps down, which the README now explains. A unit
anchor pins the warm-up position and the guard message.

Issue: #735
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 06:41:26 +00:00
claude[bot] b417ec4c9e docs(reviews): индекс после сдвига каталога (#741)
Issue: #741
User-Visible: no
2026-10-01 05:59:54 +00:00
claude[bot] 15f8da8563 docs(reviews): индекс после сдвига каталога (#741)
Issue: #741
User-Visible: no
2026-10-01 05:59:54 +00:00
claude[bot] d87f834448 docs: review document for #741
Issue: #741
User-Visible: no
2026-10-01 05:59:54 +00:00
Claudeandclaude[bot] d5bdfde919 refactor(iso): drop the unused stageSize from the overlay fit input (#741)
Since #713 the overlay fit envelope reserves no nudge budget, and since
#725 _isoScene passes `stageSize: null` while resolveIsoOverlayFitEnvelope
never reads the field. The room focus still built a { width, height }
object from the stage for nothing. The optional field is removed from
IsoOverlayFitEnvelopeInput together with both call-site arguments.

The #725 AC3 unit compared bounds with stageSize null and 1000x500, which
is now meaningless; it checks instead that the fit bounds follow only
scene.frame and the tiles: the same bounds for every stage aspect, a moved
frame moves them, an enclosing frame is returned as is.

Issue: #741
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:59:54 +00:00
claude[bot] f559b3fe19 docs(reviews): индекс после сдвига каталога (#736)
Issue: #736
User-Visible: no
2026-10-01 05:56:05 +00:00
claude[bot] 90ea1cab84 docs(reviews): индекс после сдвига каталога (#736)
Issue: #736
User-Visible: no
2026-10-01 05:56:05 +00:00
claude[bot] 9debce3394 docs: review document for #736
Issue: #736
User-Visible: no
2026-10-01 05:56:05 +00:00
Claudeandclaude[bot] 17b7b0ad73 feat(process): a red night comments on the tasks merged since the last green one (#736)
A red nightly Validate was a signal "to the author of the latest dev
commits" that nobody received: the red run was visible only in Actions,
and nobody computed who the author was.

- scripts/night-red.mjs: acts only on conclusion=failure of the red run
  (cancelled, timed_out and the rest are a summary line). The last green
  night is the newest of the last 50 Validate workflow_dispatch runs on
  dev that completed successfully, was created before the red run, sits
  on an ancestor of the red SHA and has a green ci-proof under the
  release policy, so a light green run (stale) never counts. Suspects
  are the Issue: trailers of `git rev-list --no-merges G..R` commits that
  touch a class A/B file and carry no Release: trailer: docs-only and
  beta-candidate commits do not count, a branch merged by a merge commit
  brings its second-parent commits, a commit without a trailer is a
  "no task" summary line, an empty range means a likely flake. One
  comment per task names both runs, up to ten of its commits and the
  failed jobs, says "suspect, not guilty" and ends with the marker
  hp:night-red green=<G> red=<R> commits=<all sha12>. No comment goes to
  a closed task or to a task whose marker with the same green already
  lists all its current range commits: one comment per series of red
  nights until the task commits again; a green night starts a new series.
  Failures become a ::warning:: and a summary line, exit code 0.
- _nightly.yml: dispatch also outputs run_id; a new job night_red runs
  after it only when dispatch failed with a known run, continue-on-error,
  permissions actions: read and contents: read (the union with the other
  jobs is unchanged, thin files in main are untouched), checks out dev
  with full history without blobs, reads Actions with github.token and
  writes issues with HP_PROCESS_TOKEN. The header names the addressee.
- PROCESS.md §10.4: the "Красная ночь" paragraph next to the nightly
  ship review.

Tests run the scripted rules on real git in temporary repositories with
real ci-proof fixtures, and the workflow step on real bash with a local
Actions API server and a fake gh.

Issue: #736
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:56:05 +00:00
claude[bot] 7cb90e379b docs(reviews): индекс после сдвига каталога (#738)
Issue: #738
User-Visible: no
2026-10-01 05:52:18 +00:00
claude[bot] df8482c702 docs: review document for #738
Issue: #738
User-Visible: no
2026-10-01 05:52:18 +00:00
Claudeandclaude[bot] 82fbad67d5 fix(process): rule 10 judges the status at the commit's author date (#738)
Rule 10 compared a class A commit's authorDate with the FIRST time the
issue reached S5-ready. After a return S5+ -> S3/S4 (the #726 reclassify
route or a manual return) code written in S3/S4 and pushed after the new
S5 passed both rules: rule 8 saw the current S5/S6, rule 10 saw the old
S5 from before the return.

checkCommitEraStatuses now builds status epochs from the labeled events:
a label from `allowed` opens the "may touch code" epoch, S1-new..
S4-spec-review close it, every other label (blocked, track:*, review-4,
S8-merged under --no-merged) changes nothing. The status at authorDate is
the last status event at or before it; a pre-ready status (or no status
event at all) is a rule 10 fail. Before the first readiness the old text
stays; after a return the finding names the status, the return time and
the next readiness or "not reached yet". Commits written before the
return stay legitimate. The timeline runner, the warn without timeline
or without `allowed` events and the commit selection are unchanged.

PROCESS.md §10.2 gets item 10 describing the epochs.

Issue: #738
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:52:18 +00:00
claude[bot] bc59917ea4 docs(reviews): индекс после сдвига каталога (#718)
Issue: #718
User-Visible: no
2026-10-01 05:38:09 +00:00
claude[bot] c39a8fe88e docs: review document for #718
Issue: #718
User-Visible: no
2026-10-01 05:38:04 +00:00
Claudeandclaude[bot] b0751497d4 test(moon): judge the editor after the mode transition; fresh screenshot fingerprint (#718)
Validate on the conveyor's rebase 98d98e83 was red twice over:

- smoke_moon_static ac5_noMoonInTheEditor sampled the plan editor two
  frames after `setMode('plan')`, while the View -> editor transition (#101)
  was still running and the sky layer was legitimately fading out. Locally
  2 of 3 runs red on 98d98e83. The check now waits for the transition to
  end (no `_modeTransitionBusy`, no `mode-transition` class) and turns red
  if it never does; 5 of 5 runs green.
- the branch changes visual sources, so the screenshot check is strict on
  it, and #725 moved the sources under the fingerprint. `docs:accept
  --identical`: all 11 frames pixel-identical, only the fingerprint moves.

Issue: #718
User-Visible: no
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
Claudeandclaude[bot] c2ca6a55d2 docs(screenshots): source fingerprint for #718 — all frames pixel-identical
`npm run docs:accept -- --identical` (no prior docs:capture): the local
capture of all 11 documentation frames decoded pixel-identical to the
committed PNGs, so only the source fingerprint moves. None of the frames
shows the General settings dialog (the new moon status line) or a static
background at night (the new moon sky layer); the PNGs stay byte-for-byte.

Issue: #718
User-Visible: no
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
Claudeandclaude[bot] 1c49544831 test(golden): accept the 2 reviewed static-background moon frames (#718)
The golden-images artifact of Validate run 36790529482 (16616f09, Linux,
Chromium 151.0.7922.34) reports exactly the two new #718 scenes as
missing-baseline and every other scene as passed:

- static-bg-moon-gibbous-white-light: a waxing gibbous moon on the plain
  white static background, top-left behind the plan.
- static-bg-moon-crescent-south-dark: the southern-hemisphere crescent on
  the dark static background, top-left behind the plan.

Both frames were reviewed: the moon sits behind the plan in the top-left
corner on either background, as the spec asks. The other 190 baselines are
kept byte-for-byte; 145 environment witnesses matched.

Issue: #718
User-Visible: no
Release: v1.79.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36790529482
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
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
claude[bot] 712d41b6d6 docs: review document for #729
Issue: #729
User-Visible: no
2026-10-01 05:23:15 +00:00
claude[bot] 76558bf2a0 docs: review document for #742
Issue: #742
User-Visible: no
2026-10-01 05:14:49 +00:00
claude[bot] 0621d53a49 docs: review document for #740
Issue: #740
User-Visible: no
2026-10-01 05:11:55 +00:00
claude[bot] bb25110dc4 docs: review document for #739
Issue: #739
User-Visible: no
2026-10-01 05:08:55 +00:00
claude[bot] f3cfb93e94 docs: review document for #737
Issue: #737
User-Visible: no
2026-10-01 05:07:59 +00:00
claude[bot] b27ae06ef8 docs(reviews): индекс после сдвига каталога (#730)
Issue: #730
User-Visible: no
2026-10-01 05:03:39 +00:00
claude[bot] 7126fd8a02 docs: review document for #730
Issue: #730
User-Visible: no
2026-10-01 05:03:35 +00:00
claude[bot]andClaude 02dce77c98 docs(reviews): индекс после сдвига каталога (#730)
Issue: #730
User-Visible: no
2026-10-01 07:48:42 +03:00
claude[bot]andClaude 53bb3bf453 docs: review document for #730
Issue: #730
User-Visible: no
2026-10-01 07:48:37 +03:00
claude[bot]andClaude 0ab685e380 docs: review document for #730
Issue: #730
User-Visible: no
2026-10-01 07:48:37 +03:00
Claude 4eb712be0e fix(process): a workflow-permission refusal tells the author to rebase (#730)
Review r1 (Medium): refusalSummary said "повтор и ребейз не помогут" for every
non-stale outcome, and since AC2 the rebase guard's summary carries it too.
For a workflow-permission refusal a rebase and push by the author is exactly
the way out (PROCESS.md §10.4). That outcome now says so; other GitHub
refusals keep the old sentence.

Issue: #730
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 07:48:37 +03:00
claude[bot]andClaude 8e42f4e972 docs: review document for #730
Issue: #730
User-Visible: no
2026-10-01 07:48:37 +03:00
Claude 562313944f fix(process): ship review and beta-derived pushes tell a GitHub refusal from a moved dev (#730)
After #705 and #723 two more workflow bodies still treated every failed
push as a moved dev: the SHIP-REVIEW publication (_ship-review.yml) retried
three times with "dev went ahead", and the derived-artifacts bot commit
(_beta-derived.yml) told the release manager to rerun the workflow. A
refusal by GitHub itself - a token without the workflow right, a branch
rule, a hook - is cured by neither, and neither step said what GitHub
answered.

Both pushes now keep stderr and hand it to the #705 classifier through the
same CLI (merge-candidate.mjs --push-refusal). A stale lease keeps the old
behaviour: another attempt for the ship review, the rerun advice for the
derived artifacts. Any other outcome stops the step at once: the log gets
the git answer and the step summary gets the reason and the git answer
without secrets (--summary, refusalSummary with the new ship-review and
beta-derived labels). The classifier comes from dev, as for the other steps
of these bodies: both jobs check out dev, and the ship review resets to
origin/dev before every attempt. The ship review commit message is built
line by line into a file instead of a heredoc, as in #723. The thin callers
ship-review.yml and beta-derived.yml are untouched.

The rebase guard in _process.yml also writes the refusal reason to its step
summary now (--summary, label "rebase"); a stale lease writes none.

test/publish-push-refusal.test.mjs runs both steps as they are with real
bash and real git in temporary repositories (moved dev = a real neighbour
push, GitHub refusal = recorded stderr with a token, a credential URL and
an Authorization header); on the old bodies 10 of its 12 new tests fail.
The #705 execution tests of the rebase guard in rebase-generated.test.mjs
now also read the step summary. PROCESS.md names the two steps next to the

Issue: #730
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 07:48:37 +03:00
claude[bot] 3b9f25ea65 docs(reviews): индекс после сдвига каталога (#727)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #727
User-Visible: no
2026-10-01 03:25:24 +00:00
claude[bot] cf8b8f0307 docs: review document for #727
Issue: #727
User-Visible: no
2026-10-01 03:25:24 +00:00
claude[bot] d5bca5dee5 docs(reviews): индекс после сдвига каталога (#727)
Issue: #727
User-Visible: no
2026-10-01 03:25:24 +00:00
claude[bot] 61d98158ce docs: review document for #727
Issue: #727
User-Visible: no
2026-10-01 03:25:24 +00:00
Claudeandclaude[bot] e58d7d06f8 feat(process): nightly ship batch review, reused by the beta gate by patch set (#727)
Ship tasks merge without a model review and their code was first read by
the batch review right before a beta: one session over the whole range,
ten to forty-five minutes on the release path, days after the merge. The
gate also knew a single document (SHIP-REVIEW-<tag>.md) and covered tasks
by number only, so a commit that landed after the review under the same
trailer still counted as read.

- scripts/ship-review.mjs: the patch set of a task is the sorted
  `git patch-id --stable` of its range commits, without `Release:`
  commits (the beta candidate carries every Issue: of the line) and
  commits touching only docs/reviews/**; the diff options are explicit
  so a local git config cannot change it. shipCoverage rates every ship
  task from the documents of the same base (candidate and origin/dev,
  latest publication wins): clean, high, stale, none; documents without
  `patches` cover by number. `tag=nightly` is a reserved mode: the
  candidate is required, the document is
  SHIP-REVIEW-<base>-dev-<sha12>.md, only none/stale tasks are read and
  nothing runs when nothing is uncovered. The beta reads the same delta
  (force=true reads everything, as before); the brief names what the
  night already read. The gate refuses none/stale with the command and
  keeps the High refusal with force=true; all clean passes without a tag
  document. The machine block gains `mode` and `patches` at its end.
  comment-high writes one line per task of a nightly document with High,
  once per document (hp:ship-review-high).
- _ship-review.yml: prepare refuses nightly without a candidate before
  defaulting to the dev tip, computes the document from base and SHA and
  no longer reads a prepare failure behind `| tee` as "no ship tasks";
  publish takes mode and patches from prepare, never from the model
  result; a new step comments High at night with HP_PROCESS_TOKEN.
- _nightly.yml: the Validate run SHA is a separate step output before
  the wait; a new job dispatches ship-review.yml -f tag=nightly on it
  whatever Validate's outcome, waits only for the run to appear and
  never colours the night. Thin files in main are unchanged.
- reviews-index/reviews-archive: the nightly name is a ship document
  with nightly: true; a beta base archives with its line, a stable base
  with the nearest archived line newer than the base, or stays.
- PROCESS.md §11.7, §10.4 and REVIEWER.md describe the nightly mode,
  patch set, coverage and beta delta; the digest test pins the key rule.

Tests run the prepare, publish and comment steps and the nightly steps
on real bash with real git in temporary repositories; only push
transport and gh are faked.

Issue: #727
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 03:25:24 +00:00
claude[bot] af339fda3b docs(reviews): индекс после сдвига каталога (#725)
Issue: #725
User-Visible: no
2026-10-01 03:17:40 +00:00
claude[bot] 011e351249 docs: review document for #725
Issue: #725
User-Visible: no
2026-10-01 03:17:40 +00:00
Claudeandclaude[bot] 235f60e693 perf(card): a floor switch stops forcing layout and re-walking the config (#725)
Profiling #694 found three costs on every View pass, paid even with the
summary panel hidden.

The summary panel read the safe-area probe's computed style in layout(),
which the card reaches up to five times per render (renderControls,
menuItems, renderPanel twice, the clock check), and its updated()
measured the stage, probe and kiosk buttons after every DOM commit. The
insets now live in the measured state: measureLayout is the only method
that reads style or layout, and updated() calls it only when an input of
the measurement changed (probe, kiosk buttons or stage element, title,
language, mode, kiosk, kiosk scale, narrow, HA theme), after connect()
or an identity change, on visibility, once after document.fonts.ready,
and from resized() as before. A floor switch or an HA tick no longer
measures.

The _model getter rebuilt the config fingerprint (a walk over every
space and room with JSON.stringify of room settings) on each of its
dozens of reads per render. ConfigFingerprintPass remembers the whole
cache key (epoch and fingerprint) from the start of willUpdate() to the
end of render() while the epoch, the config object and its spaces array
are unchanged. Remembering only the fingerprint and concatenating the key
on every read was tried first: in 2.5D on the large house the switch cycle
measured slower than without any memo, and CPU profiles showed several
times more garbage collection on load and on the first visit of a floor;
one remembered key per pass has neither. Outside the pass (handlers, updated(),
timers) every read still builds the key, so an in-place edit without an
epoch bump stays visible (HP-1454-04). No write to the fingerprinted
fields is reachable from willUpdate() or render().

_isoScene read the stage box during render only to feed an aspect into
the overlay fit, whose frame has not depended on the aspect since #713.
It now uses the frame's own aspect and passes stageSize: null.

render-layout-read.mjs now also judges _isoScene and the whole summary
runtime except measureLayout, forbids layout property reads
(clientWidth, offsetTop, ...) besides the two calls, and reports every
violation. Two registered mutants restore the old reads.

No visible change: panel caps, side, offsets and kiosk clearance are
computed from the same values; the 2.5D frame is the same.

Issue: #725
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 03:17:40 +00:00
claude[bot] 8fac66e1c3 docs(reviews): индекс после сдвига каталога (#726)
Issue: #726
User-Visible: no
2026-10-01 03:06:49 +00:00
claude[bot] 530eb0090d docs: review document for #726
Issue: #726
User-Visible: no
2026-10-01 03:06:49 +00:00
Claudeandclaude[bot] d327ec3d93 feat(process): a failed show verdict re-routes to ask without a fresh budget (#726)
A non-green show verdict that found "something to decide" went down the same
path as "fix the code": S6 with a limit of 2. Promoting the task to track:ask
was left to the agent's memory, with no named criterion and no trace, and the
exhausted budget only surfaced on the next S7 - after a fix nobody would read.

The structured verdict now carries `route` (fix | reclassify) and an optional
`criterion` (one of the six show criteria of PROCESS.md section 5). The trust
boundary reads a missing route as fix, rejects one outside the dictionary and
rejects reclassify on a green verdict. `reviewRoute` in process-track.mjs is
the single decision: on a code review of an unconfirmed show it moves the task
to track:ask and S3-spec; on an owner-confirmed show it adds `blocked` and asks
the owner; anywhere else reclassify degrades to fix with a note. The verdict
that spends the last cycle sets review-4 at once; the stage budget is shared
across tracks, so promotion changes the limit (4), not the count.

The "Решение по вердикту" step makes one `process-track.mjs route` call (from
dev, like the track step) and only executes its output: comment from a file,
labels from add/remove lists, status via status-label.mjs as before. The track
step also emits `confirmed` and a `route_note` for the review prompt; the
review document anchor gains a route tail that the old reader still parses;
wait-verdict reports the two new pipeline comments. The guard's own
spent >= limit check stays as the safety net.

Issue: #726
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 03:06:49 +00:00
claude[bot] 56be779ca7 docs(reviews): индекс после сдвига каталога (#728)
Issue: #728
User-Visible: no
2026-10-01 03:03:28 +00:00
claude[bot] 5e430d9ede docs: review document for #728
Issue: #728
User-Visible: no
2026-10-01 03:03:28 +00:00
Claudeandclaude[bot] 0cc7c60e8b feat(process): process metrics by track — segments, returns, ship findings, job minutes (#728)
The weekly report could not say whether the tracks of #695/#696 paid off:
it read only the first S4/S5/S7/S8 placements of closed issues, no track,
no waiting, no reason for a return, and the CLI never passed jobs, so the
"Job-минуты" line never printed. The owner decides on these numbers, so the
definitions are spelled out in the report headers and anything unknown is
printed as such.

scripts/process-metrics.mjs (pure functions over the snapshot):
- K1 trackAt/trackPath: track at a moment from the labels set before it,
  resolved by process-track.mjs (labelTrack) — one rule with the pipeline;
  infra = no class A file in the issue's commits (Release: commits aside).
  The issue's track is the one at its first S8-merged.
- K2 issueSegments: queue/spec/work/review/rework/blocked from the first
  status label to the first S8, summing to lead; blocked is taken out of
  the segment under it; S7 over S7 is neither a return nor a new segment.
- K3 returnSignal/returnReason: S7 -> S6/S3 and S4 -> S3 returns, reason
  from the last comment with a sign between the review placement and the
  return. merge and the "not run" family come from PIPELINE_EVENTS, the
  verdicts from verdictDeclaration with the issue's own document; the two
  continuations have no pipeline constant, so NOT_RUN_VALIDATE_RE and
  NOT_RUN_CONFLICT_RE are exported copies held by a contract test on the
  _process.yml templates. Anything else is unknown; hp:route (#726)
  gives reclassify/owner-question when present.
- K4 shipFindings: High/Medium/Low of SHIP-REVIEW-*.md (docs/reviews and
  legacy/reviews) by the anchor block, summed per issue; the track table
  counts each document once.
- K5 stageMinutes: jobs of process and Validate runs (skipped runs aside,
  at most 600, "усечено: N из M" beyond), stages by job name, per track at
  run time, Validate per event; unavailable jobs are "нет данных", not 0.
  jobMinutes gets the same data and prints again.
- K6 tokenUsage: "Токены: нет данных (…)" until the pipeline records usage
  (issue F); the hp:usage line format is provisional.
- K7 compareCohorts: issues with the first S8 within 28 days before and
  after 2026-09-28 (--compare, --compare-days), cohort = track x volume
  bucket (<=30/31-200/201-1000/>1000 lines of Issue-trailer commits without
  Release:, class D and docs/reviews/**); n < 3 on a side is "мало данных".
- fetchSnapshot: issues state=all since the earliest window (the old
  selection is still "closed in the window"), timelines up to 10 pages
  (beyond: "таймлайн усечён"), jobs, ship and usage review docs, git log
  --numstat of origin/dev.

_process-metrics.yml: full history (fetch-depth: 0) for K1/K7 and a 30
minute ceiling. The thin process-metrics.yml is unchanged. PROCESS.md §5
points at the report. Old sections and their tests are unchanged.

Issue: #728
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 03:03:28 +00:00
Claude b3dd9444b5 test(daycycle): judge pinch frames, not the first frames of the recording (#734)
presentedFramesHaveNoWhiteTile in smoke_daycycle_layer_budget turned red now
and then on frames that are not pinch frames: the first one or two frames of
the screencast sometimes show the room before its fill (white paper, 0.997
near-white), before any pinch move, and the next frames are light grey. The
check judged every recorded frame, so a stale opening frame failed a gesture
that painted correctly.

The check now judges pinch frames only. Not judged: a frame whose swap
time (screencast metadata) is earlier than the first pinch move, and a frame
before the first one that shows the room filled. The guard keeps its power:
a white tile during the pinch comes after a filled frame and stays red; a
room white from the recording start through the whole gesture leaves no
judged frame inside the gesture, which fails both the frame count and the
white-tile check (an empty set no longer passes `every`). The frame count
counts judged frames inside the gesture, not every recorded frame.

Why the fill appears later is out of scope.

Issue: #734
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:33:49 +03:00
claude[bot] 52dc08a0e5 docs(reviews): индекс после сдвига каталога (#694)
Issue: #694
User-Visible: no
2026-10-01 00:22:26 +00:00
claude[bot] 78acf2c6ba docs(reviews): индекс после сдвига каталога (#694)
Issue: #694
User-Visible: no
2026-10-01 00:22:26 +00:00
claude[bot] dcb8f80de2 docs: review document for #694
Issue: #694
User-Visible: no
2026-10-01 00:22:26 +00:00
Claudeandclaude[bot] 6f17b6cd4c perf(card): a floor switch stops re-querying the same subtrees (#694)
A floor switch replaces the whole stage, so the card's pointer-hover
MutationObserver receives hundreds of records whose targets are the same
few containers. Each record re-ran `matches` and a `.devlayer` subtree
`querySelector` on its target, and kept doing so after the device layer
had already been found. The batch logic moves to `deviceLayerMutated` in
device-hit-owner.ts: a node is checked at most once per batch, the first
hit ends the checks, and every added node still goes through
`_syncPointerHoverSubtree` in record order. The card shrinks by 12 lines.

The View stair layer read the card's `_model` getter once more for every
navigable stair; the getter rebuilds the config fingerprint on each read.
`renderLayer` now reads it once.

`languageRenderGate` wrote `lang` on the host on every render. It now
writes it only when the value differs (language switch, English fallback,
a foreign value); an unchanged value is left alone.

No behaviour changes: DOM, tooltips and pixels are the same. Unit tests
count subtree queries per node, `_model` reads per render and `lang`
writes; one mutant per change restores the old behaviour.

Issue: #694
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 00:22:26 +00:00
claude[bot] 153fa7a680 docs: review document for #728
Issue: #728
User-Visible: no
2026-10-01 00:19:58 +00:00
claude[bot] fc835cb97d docs: review document for #727
Issue: #727
User-Visible: no
2026-10-01 00:18:39 +00:00
claude[bot] 499c0860df docs(reviews): индекс после сдвига каталога (#707)
Issue: #707
User-Visible: no
2026-10-01 00:15:55 +00:00
claude[bot] 8df4ef710c docs: review document for #707
Issue: #707
User-Visible: no
2026-10-01 00:15:51 +00:00
Claudeandclaude[bot] 1d51beade1 feat(process): risk by changed hunks decides ship and informs show (#707)
The ship limits count lines and files but not what was touched: a
12-line pointerdown handler passed them like a typo and merged unread.
The track rule also lived twice - the guard computed the cycle limit in
bash while process-track.mjs computed the track, and the two disagreed
on multiple track labels. The packet still told authors to rebase
show/ship branches that merge cleanly.

- scripts/change-risk.mjs: one pure classifier over `git diff -U0` from
  the merge base. Class A lines only; comments, blank lines and pure
  renames give no risk; deletions do. Area and token rules per class
  (geometry, touch, migration, devices, perf, ux, visual render/ui),
  evidence as path:line, five per class.
- process-track.mjs: owner confirmation is a comment line
  "Трек: <x> — решение владельца" by the repo owner (latest wins, only
  for the current track); several track labels read as the strictest
  with a warning; cycleLimit, guardLimit and rebaseBeforeReview are the
  single source. `stage` makes the whole S7 track decision in one call:
  ship with risk and no confirmation is raised to show with evidence,
  a confirmed ship keeps merging without the model and records the risk
  for the batch review; show/ask get a risk note for the reviewer.
- _process.yml: the guard asks process-track.mjs for the limit and keeps
  no track logic; the track step calls the script once and only
  executes its raise flag and comment file; risk_note reaches the
  Review prompt, ship_risk reaches the hp:ship-merge comment (marker
  line unchanged).
- task-packet.mjs: track basis, limit and rebase policy; next step
  without the stale rebase line; risk with its consequence per track;
  required checks with reasons (ci:golden only on render risk);
  changelog and visual evidence - from the same exports.
- ship-review.mjs: the batch brief prints the risk line of a ship merge.
- Canon: PROCESS.md §5, §5.1, §10.4, §11.7, both digests, AGENTS.md.
- Registry anchors that watched the moved code are moved, not dropped.

Issue: #707
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 00:03:35 +00:00
claude[bot] a49f7095ce docs: review document for #728
Issue: #728
User-Visible: no
2026-09-30 23:34:30 +00:00
claude[bot] 3847baa5a0 docs: review document for #727
Issue: #727
User-Visible: no
2026-09-30 23:33:52 +00:00
claude[bot] 5ce83feffa docs: review document for #726
Issue: #726
User-Visible: no
2026-09-30 23:33:09 +00:00
claude[bot] fd082ad460 docs: review document for #725
Issue: #725
User-Visible: no
2026-09-30 23:31:13 +00:00
claude[bot] 85598638c1 docs(reviews): индекс после сдвига каталога (#732)
Issue: #732
User-Visible: no
2026-09-30 23:26:46 +00:00
claude[bot] 0f7ec02352 docs: review document for #732
Issue: #732
User-Visible: no
2026-09-30 23:26:46 +00:00
Claudeandclaude[bot] 715735d753 refactor(iso): remove the empty overlay renderers and dead fixture fields (#732)
After #714 and #724 the 2.5D overlays still carried stubs:

- renderIsoOverlayGrounds and renderIsoRaisedOverlays returned an empty SVG
  on every frame. They go with IsoFramePresentation.grounds/raised and the two
  bindings in the card. The iso-overlays-svg element itself stays, now empty:
  it is the inert camera-viewBox layer the contract and live-touch smokes
  measure screen-facing HTML against, so the 2.5D DOM keeps its elements.
- IsoOverlayRenderEntry.groundRadius was computed for every device, room label
  and lock and read only by the snapshot comparison that compared it.

The overlay test fixtures passed view, referenceView, stageSize and layers
(and one test selectedDeviceId), which IsoOverlaySceneInput does not have, and
asserted that changing them keeps the placement - a claim the signature makes
by itself. Those fields are gone from every fixture. The zoom/resize asserts of
"Stage 4 reuses pure overlay placements" and "#713 AC3" (renamed to what it
still checks) and the "#570 supersedes #473 W1" selection test go; the #724
AC2 test now zooms the way production does, through the live frame of
resolveIsoScene, and checks that the structural geometry and so the overlay
scene are reused. The #713 K8 fixture no longer passes stageSize, which
resolveIsoOverlayFitEnvelope does not read.

test/iso-overlay-fixture-types.test.mjs typechecks the overlay test files with
the TypeScript compiler: their fixture types (OverlaySceneFixture,
OverlayEntryFixture) are the keys of the production types with deliberately
loose values, so a partial fixture is fine and a field the type lacks is an
excess-property error. Three checks: no excess property in the fixture files;
a probe shows the fixture types resolve to the real inputs and reject view,
referenceView, stageSize, layers, selectedDeviceId and groundRadius; every
call of the scene builder gets its argument through a checked type (a literal
in overlayScene or a declaration of the fixture type). Each check is red when
a dead field is put back into a declared fixture, an override literal or an
entry, when a literal goes straight into the builder, when a fixture loses its
annotation, and when groundRadius returns to the entry type.

isometric-contract now asserts that nothing renders into the overlay surface
and that the removed renderers and groundRadius stay gone. No mutant is
anchored on the removed code; mutation-gate --check is unchanged (3 warnings).
The 19 2.5D golden scenes pass in capture on the accepted baselines.

Issue: #732
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 23:26:46 +00:00
Claudeandclaude[bot] 40607aa37f fix(scripts): isMainModule compares real paths of argv and module (#733)
process.argv[1] keeps the path as typed, so a script started through a
symlink (or from a symlinked directory) still carries the link path there,
while Node builds import.meta.url of the main module from the real path.
The two never matched, and every CLI guarded by isMainModule silently did
nothing and exited 0. Both sides are now resolved with realpathSync before
the pathToFileURL comparison; a path that does not exist is compared as is,
without throwing, exactly as before.

The unit test writes a CLI and a module it imports into a temporary
directory, launches the CLI directly, through a directory link (a junction
on Windows, no admin rights needed) and through a file symlink (skipped on
EPERM), and checks that only the launched script runs its main. It is red
on the previous implementation.

Issue: #733
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 23:19:47 +00:00
claude[bot] 34423a6236 docs(reviews): индекс после сдвига каталога (#724)
Issue: #724
User-Visible: no
2026-09-30 22:39:24 +00:00
claude[bot] 295d61f377 docs: review document for #724
Issue: #724
User-Visible: no
2026-09-30 22:39:24 +00:00
Claudeandclaude[bot] 5f8e8ca7e8 refactor(iso): drop the 2.5D overlay data nothing reads (#724)
After #714 the 2.5D overlay scene still carried what decides nothing:

- src/iso-overlays.ts: IsoOverlayPlacement loses tether and grounding (always
  invisible) and raisedScene (always equal to visualScene); IsoOverlayOwner
  loses area; IsoOverlayPlacementInput loses hovered, focused, selected and
  filtersSupported, which the resolver ignored. IsoWallSilhouette and
  tetherGeometry go with them.
- src/iso-scene-render.ts: the structural scene no longer projects wall
  silhouettes (isoWallSilhouettesOf and IsoSceneCacheEntry.wallSilhouettes)
  that served only as a cache key. The placement and render-scene caches are
  keyed by the wall geometry the scene is drawn with (IsoOverlaySceneInput.
  structure = scene.geometry): the structural LRU hands out the same object
  across zoom, stage resize and HA state, and a new one after any wall, room
  or opening edit. The resolveCollisions flag and its fit/live cache slots
  are gone: since #713 both held equal placements, and 2.5D renders only in
  View, where the fit probe and the live frame ask with the same devices, so
  they now read one snapshot.
- src/houseplan-card.ts: the fit call passes no flag; the overlay scene gets
  structural.geometry. data-hp-iso-nudged stays the constant "false" read by
  the golden requireOneRise preflight, the live-touch smoke and the benchmark.

Tests: iso-overlays pins the placement fields; iso-scene-render builds the
structure with buildIsoWallGeometry, the #714 zoom/resize and #711 state tests
stay, fit and live are asserted to share one snapshot, and two #724 AC2 tests
run the production path (createIsoStructuralSource -> resolveIsoScene ->
buildIsoOverlayRenderScene): a thicker wall with the same room rebuilds the
scene (red with a key without walls, e.g. keyed by the room rows), and a room
edit that moves the owner gives the new owner (red with a constant key). The
silhouette-construction test goes with the construction.

Mutants: #473 W2 (iso-placement-cache-survives-silhouette-change, id kept for
history) now keys the placement cache by a constant instead of input.structure
and its guard also runs the #724 AC2 tests; W6 patches the new structure line;
the W5 description no longer speaks of a nudge. The isometric-contract regex
checks the new key instead of the silhouette construction. docs/ISOMETRIC.md
names the key.

Live 2.5D output is unchanged: the 21 isometric golden scenes pass on the
accepted baselines.

Issue: #724
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 22:39:24 +00:00
claude[bot] 108427dc77 docs(reviews): индекс после сдвига каталога (#723)
Issue: #723
User-Visible: no
2026-09-30 22:26:44 +00:00
claude[bot] 3e7f7d3936 docs: review document for #723
Issue: #723
User-Visible: no
2026-09-30 22:26:44 +00:00
Claudeandclaude[bot] 0d85807157 fix(process): publish steps tell a GitHub push refusal from a moved branch (#723)
Two steps publish a commit and treated every failed push as a moved branch:
the release review job (release-review.yml) retried three times with "dev
went ahead", and the review document step (_process.yml) rebased and pushed
again. A refusal by GitHub itself - a token without the workflow right, a
branch rule, a hook - cannot be cured by a retry or a rebase, and the step
never said what GitHub answered.

Both pushes now keep stderr and hand it to the #705 classifier through the
same CLI the rebase guard uses (merge-candidate.mjs --push-refusal). Only a
stale lease (rejected / fetch first / stale info) keeps the old retry or
rebase. Any other outcome stops the step at once, without retries: the log
gets the git answer and the step summary gets the reason and the git answer,
both passed through redactSecrets (token, credential URL, Authorization).
The review document step takes the classifier from dev, as the rebase guard
does: a task branch behind dev may not carry it.

The summary text is written by the new --summary option (refusalSummary),
not by a multi-line string in run:, and both commit messages are now built
line by line into a file instead of a heredoc (PROCESS.md §10.4 item 4).
release-review.yml is dispatch-only and is not mirrored to main. PROCESS.md
names the rule next to the rebase guard; the #638 trailer witness in
test/release-review.test.mjs follows the line-by-line message.

test/publish-push-refusal.test.mjs runs both steps as they are with real
bash and real git in temporary repositories; only the push transport is
replaced: a moved branch is a real neighbour push, a GitHub refusal is a
recorded stderr carrying a token, a credential URL and an Authorization
header. On the old steps 9 of its 11 tests fail.

Issue: #723
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 22:26:44 +00:00
claude[bot] 5cd9d0419a docs: review document for #707
Issue: #707
User-Visible: no
2026-09-30 22:21:03 +00:00
Claudeandclaude[bot] e00f62dca0 test(summary): #509 AC3 counts masonry passes instead of milliseconds (#721)
The large-house AC3 witness asserted an absolute 2.5 s budget for one
floor's clean-floor total. On a loaded 2-CPU machine the healthy path
took 2.7-4.3 s and the test went red while the code was fine.

The property #509 AC3 protects is structural: the summary panel builds
the space's wall masonry once and hands it to innerContourForRoom
(shared.roomGeom / shared.multiWallNodes); without it the masonry is
rebuilt for every room. Every masonry build walks the contours of all
rooms, so the test now counts reads of room.poly and compares the
floor total against one explicit spaceWallGeometry pass of the same
floor in the same run. Healthy code costs ~1.3 passes; the registered
mutant summary-area-recomputes-walls-per-room costs 21.3 and is red,
and so are the half-regressions that drop only one of the two shared
arguments (4.6 and 18.0 passes).

The count is deterministic, so machine load no longer matters.

Issue: #721
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 22:16:19 +00:00
Claudeandclaude[bot] bbcc88caf1 test(i18n): the second-card locale check counts locale requests only (#722)
secondCardReusesPageLocale compared the count of all page requests before
and after the second card mounted. The only extra request is that card's own
plan image: under page.route the browser HTTP cache is off, so it is fetched
again, and whether it lands before the read is a race. The German locale file
itself is loaded exactly once per page. The check now counts requests for the
locale chunk only and still fails if the second card fetches it again.

Issue: #722
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 21:08:23 +00:00
claude[bot] 66ac7d23ec docs: review document for #707
Issue: #707
User-Visible: no
2026-09-30 21:07:49 +00:00
claude[bot] 2d735c56f9 docs(reviews): индекс после сдвига каталога (#716)
Issue: #716
User-Visible: no
2026-09-30 21:01:10 +00:00
claude[bot] 48b001c646 docs: review document for #716
Issue: #716
User-Visible: no
2026-09-30 21:01:10 +00:00
claude[bot] dccbb80890 docs: review document for #716
Issue: #716
User-Visible: no
2026-09-30 21:01:10 +00:00
Claudeandclaude[bot] d11ad9c1c2 ci: register ship-review and beta-derived as thin callers in main (#716)
`workflow_dispatch` runs the file from the chosen ref, but GitHub lists a
workflow and accepts a dispatch (button, `gh workflow run`, API) only when
its file exists on the default branch. `ship-review.yml` (#696) and
`beta-derived.yml` (#697) lived only in `dev`, so neither could be started
at all, and the comment "the file runs from `--ref dev`, no mirror in
`main` needed" was wrong. Both beta steps are needed before the next
promotion would bring them to `main`.

They now follow the #623 layout instead of a full copy in `main`: a thin
caller (trigger, dispatch inputs, run-name, permission ceiling, concurrency)
calls `_ship-review.yml` / `_beta-derived.yml` at `@dev` with
`secrets: inherit`. A full copy would either need a mirror on every edit or
drift silently, and a dispatch from `main` (the button's default) would run
the stale copy; the thin caller runs the dev body from any ref. The caller
ceiling is the union of the body jobs' permissions (#556): ship-review
`contents: read` + `issues: read`, beta-derived `contents: read` +
`actions: read`; writes to `dev` stay with HP_PROCESS_TOKEN as before.

`workflow_sync` in validate.yml now compares eight files, and
test/default-branch-workflows.test.mjs lists the two dispatch-only files
explicitly with the reason checked (only `workflow_dispatch`). Workflow
tests and the #697 provenance mutant read the bodies. PROCESS.md §10.4,
§8 and §11.7 say how these are run and that a new thin file is mirrored
into `main` before it is merged into `dev`.

Issue: #716
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 21:01:10 +00:00
claude[bot] 7188db8187 docs: review document for #694
Issue: #694
User-Visible: no
2026-09-30 20:59:45 +00:00
claude[bot] a5a73d1511 docs(reviews): индекс после сдвига каталога (#714)
Issue: #714
User-Visible: no
2026-09-30 20:36:03 +00:00
claude[bot] a312897527 docs(reviews): индекс после сдвига каталога (#714)
Issue: #714
User-Visible: no
2026-09-30 20:36:03 +00:00
claude[bot] 7ac24a03a0 docs: review document for #714
Issue: #714
User-Visible: no
2026-09-30 20:36:03 +00:00
Claudeandclaude[bot] 7574518575 refactor(iso): remove the dead #651 overlay placement search (#714)
Since #713 every raised device tile and lock badge is its floor anchor lifted
by one shared wall-top rise and room names stay on the floor, so the live
scene no longer called the #651 search. What was left of it only cost code,
build time and review attention:

- src/iso-overlays.ts: resolveIsoOverlayRigidGroups, resolveIsoOverlayCollisions
  with their boundary-candidate machinery, the nudge search in
  resolveIsoOverlayPlacement (the vector to the room safe point, the near-wall
  test, the zoom hint), the safe point itself, the nudge/nearWall/cleared/capped
  and status/reason fields, and ISO_OVERLAY_MAX_NUDGE_CSS_PX /
  ISO_OVERLAY_SAFETY_GAP_CSS_PX.
- src/iso-scene-render.ts: the zoom reuse fast path and the CSS-pixel scale it
  compared; a placement now depends only on anchor, owner, footprint and rise,
  so zoom and stage resize reuse it by signature. residualPairs is gone and the
  memo key is called layoutSignature.
- src/houseplan-card.ts: the overlay scene no longer receives the view, the
  reference view or the stage rect it only fed to that scale;
  data-hp-iso-nudged stays as the constant "false" that the golden
  requireOneRise preflight, the live-touch smoke and the Stage 4 benchmark read.

The #585/#651 unit tests and the seven mutants that guarded only the removed
code are deleted; kept tests drop their nudge assertions, and a stage resize is
now pinned as a non-layout event. docs/ISOMETRIC.md keeps #651 as history only.
Live 2.5D output is unchanged: the 21 isometric golden scenes pass on the
accepted baselines.

Issue: #714
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 20:36:03 +00:00
claude[bot] 12bdc0be68 docs(reviews): индекс после сдвига каталога (#704)
Issue: #704
User-Visible: no
2026-09-30 20:30:34 +00:00
claude[bot] 8b7628285a docs: review document for #704
Issue: #704
User-Visible: no
2026-09-30 20:30:34 +00:00
Claudeandclaude[bot] 37b1cbf74b fix(release): let the stable-line review run when release.yml queues it (#704)
release.yml dispatches release-review.yml with GITHUB_TOKEN, so the run is
started by github-actions[bot], and claude-code-action refused it: "Workflow
initiated by non-human actor: github-actions (type: Bot). Add bot to
allowed_bots list" (v1.78.0: release run 36468444979, review 36468505112).
The release went out and nobody learned that the review never ran.

The review step now allows exactly github-actions[bot]. At the pinned SHA
(9cdae7f0) the action compares allowed_bots entries and the actor
case-insensitively with the `[bot]` suffix stripped, so this entry matches
GITHUB_ACTOR; any other bot is still refused, and a human dispatch never
consults the list.

independent-review no longer stops at the dispatch: it looks the run up by
workflow, branch dev, event, time and run-name "Release review <tag>" for
up to three minutes and writes the link and status to the step summary.
A run that did not appear or did not start is a warning; the release is
not blocked.

Neither file is executed from main, so no mirror is needed (§10.4).

Issue: #704
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 20:30:34 +00:00
claude[bot] 40b226405d docs: review document for #718
Issue: #718
User-Visible: no
2026-09-30 20:30:04 +00:00
claude[bot] 2f1be1a86a docs: review document for #694
Issue: #694
User-Visible: no
2026-09-30 20:28:41 +00:00
claude[bot] 24cb6d113a docs(reviews): индекс после сдвига каталога (#705)
Issue: #705
User-Visible: no
2026-09-30 20:25:08 +00:00
claude[bot] 6e2c75afc9 docs: review document for #705
Issue: #705
User-Visible: no
2026-09-30 20:25:08 +00:00
Claudeandclaude[bot] e5c217111c fix(process): a GitHub push refusal is not a stale lease (#705)
merge-candidate treated any push stderr containing "rejected" as a stale
lease. A `! [remote rejected]` from GitHub itself - in #700 the rebased
candidate changed .github/workflows/ and the conveyor token has no workflow
permission (runs 36484993494, 36487044060) - became "the branch moved after
the reviewed material (#312)", and the stderr was never printed, so the
author was sent to look for a commit that did not exist.

classifyPushRefusal now tells three outcomes apart: a stale lease
(`[rejected] (stale info)`, `fetch first`, a server-side lock race) keeps
the old behaviour; GitHub's workflow refusal (PAT, OAuth App, GitHub App,
bot and integration wordings) and any other `[remote rejected]` get their
own outcome, S6-in-progress and a comment naming the reason. The workflow
comment says what to do: the author rebases and pushes, or the owner grants
the permission. The git answer goes to the log and the comment with tokens
and credential URLs cut out; the merge-step failure comment is redacted too.

The rebase guard in _process.yml parses its push refusal with the same code
(`merge-candidate.mjs --push-refusal`): a stale lease is the old error, a
workflow refusal returns the task to S6 without review like a conflict, and
material/reuse/gate skip the rebase that never reached the branch.

Mutant push-refusal-kinds-glued restores the old regex; guard: #705 AC1.

Issue: #705
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 20:25:08 +00:00
Claudeandclaude[bot] 84ed38e3d5 test(perf): the Stage 4 dense contract requires no nudged overlay (#719)
The isometric-stage3-dense-v1 runner still demanded at least one bounded
#651 nudge. Since #713 every raised device tile and lock badge is lifted by
the one shared wall-top rise and carries data-hp-iso-nudged="false", so the
Full Performance profile failed its input contract before any timing.

The contract is inverted: a single nudged raised root now fails the sample,
matching the golden requireOneRise preflight. The performance README states
the current contract, and the #570 runner-contract unit pins the new failure
text.

Issue: #719
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 20:17:15 +00:00
claude[bot] 3e38de07b3 docs(reviews): индекс после сдвига каталога (#703)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #703
User-Visible: no
2026-09-30 18:27:54 +00:00
claude[bot] aee0a5825b docs: review document for #703
Issue: #703
User-Visible: no
2026-09-30 18:27:54 +00:00
Claudeandclaude[bot] 3d99f261ff fix(ci): fetch release tags where the range base reads them (#703)
Review r1 (High): actions/checkout passes `git fetch --no-tags` unless
`fetch-tags: true`, even with fetch-depth 0, so releaseTaggedShas() was always
empty in CI and a candidate outside the 100-run API window fell back to
event.before instead of the last release tag. Preflight and changes now fetch
tags; the workflow contract pins the option. The AC2 dev-push case now uses its
own input (dev runs only, an older `before`) instead of repeating the main call
(review r1, Low).

Issue: #703
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:27:54 +00:00
claude[bot] 204e728097 docs: review document for #703
Issue: #703
User-Visible: no
2026-09-30 18:27:54 +00:00
Claudeandclaude[bot] 1557475af3 fix(ci): stable promotion judges nothing already judged on dev (#703)
Validate on a push to main took the range base from main's own runs only,
and skipped HEAD: the nearest judged ancestor was the previous stable, so the
whole beta line was re-judged by today's rules (run 36468413524: 55 smoke
private writes made before #629). Preflight on main used event.before, the
same old-main..candidate.

The range base now reads Validate runs of both integration branches,
counts published release tags as judged material, and accepts HEAD itself
when it already has a successful run (or a tag). A promoted SHA gets an
empty range and the dev verdict; a failed HEAD is re-judged over the same
range; a hotfix on main is judged from the candidate.

Issue: #703
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:27:54 +00:00
Claudeandclaude[bot] 445a9a0cdf test(rebase): temp repositories run no background maintenance (#717)
The fixture repositories are removed with rmSync in each test's finally,
and that cleanup sometimes failed with ENOTEMPTY on work/.git/objects.
commit, fetch, rebase and the receiving side of push all start
`git maintenance run --auto` / `git gc --auto`; recent git (2.47+)
detaches auto maintenance by default, and a detached run creates
objects/maintenance.lock after the command has returned, i.e. while
rmSync is already walking the tree.

The environment the test already uses for core.autocrlf now also sets
maintenance.auto=false and gc.auto=0 for the working clones. The bare
origin gets receive.autogc=false, maintenance.auto=false and gc.auto=0
in its own config, since git drops GIT_CONFIG_* for the local transport's
receive-pack. The cleanup keeps rmSync in every finally (temp-dir hygiene
rule) with maxRetries/retryDelay, so a file that still appears under it
is retried instead of failing the test. No assertion changed.

Issue: #717
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:24:39 +00:00
Claudeandclaude[bot] 07d90be28f test(dialog): measure the room toggle after its knob stops sliding (#712)
The second flake of smoke_dialog_polish_603: the knob slides with
`transition: left .15s` and the probe waited a fixed 220 ms, 70 ms of slack
that load ate (rightGap 3.05 and 5.6 instead of 2 in 2 of 20 loaded runs).
The probe now waits until the toggle and its pseudo-elements have no running
animation. The geometry oracle and its negative probe are unchanged.

Issue: #712
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:21:21 +00:00
Claudeandclaude[bot] f56342479b test(dialog): wait for the room dialog by condition, not by two frames (#712)
smoke_dialog_polish_603 switches the card language on every step and then
opened the room dialog with a private _openRoomEdit call followed by
updateComplete and two frames. de and fr (and the editor's settings
dictionaries) are lazy chunks: until they arrive the card's language gate
keeps the previous frame, inert and aria-busy, so the dialog is not in the
tree yet. When the chunk took longer than two frames (CI, 1 of 2 runs)
the next line read querySelector of null. Delaying the de/fr chunks by
400 ms in the harness reproduces the TypeError every time.

The step now waits until the new language is painted (no aria-busy, lang
equals the requested code), enters Plan with __hpTest.setMode, opens the
dialog with __hpTest.openRoomEdit (the real gear; the facade waits for
[data-kind="room"]) and waits until the dialog's basics card is laid out.
The covered private calls _setMode and _openRoomEdit are gone; every
check and its oracle is unchanged.

Issue: #712
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:21:21 +00:00
Claudeandclaude[bot] bcff8c7157 test(stairs): take the AC5 reference after the previous write is adopted (#708)
untouchedDialogSaveKeepsSizes compared the stair before and after an
untouched dialog Save byte for byte, but took the reference while the
previous frame gesture's debounced save (500 ms) was still pending. That
write adopts the canonical record it sends, so under load it landed
between the two reads: the reference had x: 0.21699999999999997, the
read after Save had x: 0.217, and the check went red although the dialog
wrote nothing.

The smoke now waits for the card's own "config writes idle" condition
(no debounced save pending, no write in flight; read-only) before taking
the reference, so both sides are the same canonical stair. The exact JSON
comparison, the >1 m field and the no-history check are unchanged; a
reference that never goes idle within 5 s fails the check instead of
racing.

Issue: #708
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:17:50 +00:00
Claude a4af5a2008 test(backdrop): probe editor backgrounds after the mode transition (#715)
The 6b probes entered each mode with a private _setMode call and read the
stage and paper colours 220 ms later. The mode transition interpolates
exactly those colours (inline stage background, --hp-mode-paper under
.stage.mode-transition) for its 220 ms plus a measurement frame, and it is
driven by animation frames, so under load the probe caught an
intermediate colour and plan_editor_stage_white_with_backdrop /
plan_editor_paper_white_with_backdrop went red. Slowing frames to 60 ms
reproduces both every time; three parallel copies of the smoke fail 16 of
18 runs.

Each probe now enters its mode through __hpTest.setMode and waits until
the transition has ended by the card's own markers: the stage carries
mode-<mode> and no longer mode-transition, and neither the stage nor the
paper has a running animation. The same wait precedes the View probe.
The colour assertions are unchanged; a plan stage forced to a non-white
background still fails the check.

Issue: #715
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 20:52:28 +03:00
Claude 9e1cc91277 perf(budget): the 2.5D view toggle is a reported figure, not a gate (#720)
Owner decision of 2026-09-30 in #694. Switching Flat <-> 2.5D is a one-off
General settings change, and since #649 the runner measures a full config
reload for the candidate against a per-device projection flip for v1.77.0,
which alone explains most of 73.8 -> 195.7 ms. The scene build stays gated by
modelReady, firstStableRender and spaceSwitch, a UI freeze by the single
long-task ceiling; the runner still reports viewToggleMs.

Issue: #720
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 19:42:52 +03:00
Claude dca0fd2876 Release v1.79.0-beta.1 candidate
Issue: #661
Issue: #692
Issue: #693
Issue: #711
Issue: #713
User-Visible: yes
Release: v1.79.0-beta.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:38:57 +03:00
Claude 838a60b3b0 test(budget): the #438 noise margin is measured to the #699 failure edge
Since #699 the initial-View gate fails only above ceiling + band and a
decrease never fails, while the beta candidate lowers the ceiling exactly
to the fact (ratchets.mjs tighten). The #438 margin check still demanded
500 B under the ceiling and 500 B above ceiling − band, so it went red on
the first candidate with a fresh shipped bundle. It now checks the room
to the real failure edge and that a decrease stays green.

Issue: #699
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 18:38:56 +03:00
Claude 2606fffa97 docs: ship review for v1.79.0-beta.1
Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7).
Задачи: 693. Итог: High 0 · Medium 0 · Low 1.

Опубликовано вручную по решению владельца: ship-review.yml нет в main,
и запустить его нельзя (#716). Ревью — независимый агент без контекста
реализации по промпту workflow, машинный блок — anchorBlock.

Issue: #696
User-Visible: no
2026-09-30 18:29:31 +03:00
claude[bot] 8afaa63caf docs(reviews): индекс после сдвига каталога (#713)
Issue: #713
User-Visible: no
2026-09-30 14:23:07 +00:00
claude[bot] 5c0e8668f1 docs: review document for #713
Issue: #713
User-Visible: no
2026-09-30 14:23:03 +00:00
Claude 80e3dba700 test(golden): accept the 19 reviewed 2.5D frames of #713
The Validate artifact of run 36721827715 (c2c806fa, Linux, Chromium
151.0.7922.34) differs from the baselines only in 2.5D scenes, all expected by
AC8: the floor is no longer foreshortened, walls rise straight up, every device
tile and lock badge stands one wall-top height above its anchor, room names
keep their floor point and the home frame no longer reserves the 48 px nudge
budget. Reviewed frame by frame against the old baselines: no seam or
opening-order artefacts. 171 scenes unchanged, 130 environment witnesses.

Issue: #713
User-Visible: no
Release: v1.79.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36721827715
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 16:51:17 +03:00
Claude c2c806fae3 docs(screenshots): source fingerprint for #713 — all 11 frames pixel-identical
`npm run docs:accept -- --identical`: the documentation set has no 2.5D
frame, so every frame matched the committed one; only the fingerprint moves.
2026-09-30 16:23:56 +03:00
Claude 6da6d9c699 test(golden): Stage 4 overlay scenes check the one wall-top rise (#713)
The Stage 4 overlay goldens required a bounded #651 nudge; since #713 nothing
is nudged. The preflight now requires the #713 contract instead: every device
tile and lock badge is its floor anchor raised straight up by the wall-top
height, room names keep their floor point, and no root is nudged.

Issue: #713
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 16:19:40 +03:00
Claude 0d641ffbfd feat(iso): the 2.5D floor is the Flat plane, one wall-top rise for tiles (#713)
- Vertical oblique projection: the floor matrix is the identity and a height
  rises straight up by z·sin 20°, so the on-screen wall height is unchanged
  and the cos 20° foreshortening of the plan, decor and anchors is gone.
- Device tiles and lock badges stand on the wall-top plane with one common
  shift; the #651 placement search no longer runs in the live scene (its
  removal is #714). Room names keep their Flat floor point.
- The 2.5D fit no longer reserves the 48 CSS px nudge budget.
- Switching projection keeps the camera when the previous projection was on
  screen: saving the setting, entering an editor from 2.5D and adopting a warm
  memo from the other projection re-read only the scalar zoom. A cold 2.5D
  start still opens the 2.5D home.
- Opening faces are ordered along the oblique projector (s·y + z).

Witness: demo/smoke_iso_flat_parity.mjs (AC2–AC5, AC11) is red on the old code.

Issue: #713
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 15:18:00 +03:00
claude[bot] e19f5b0a0e docs: review document for #713
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #713
User-Visible: no
2026-09-30 11:31:11 +00:00
claude[bot] 90d38b1685 docs: review document for #713
Issue: #713
User-Visible: no
2026-09-30 11:22:04 +00:00
claude[bot] 683c9250b9 docs(reviews): индекс после сдвига каталога (#661)
Issue: #661
User-Visible: no
2026-09-30 10:37:24 +00:00
claude[bot] 2bc688e6c3 docs: review document for #661
Issue: #661
User-Visible: no
2026-09-30 10:37:20 +00:00
Sergey Matyunin 349af7da66 docs(screenshots): source fingerprint for #661 — all 11 frames pixel-identical
`npm run docs:accept -- --identical`: the moon lives only on the "Follow the
Sun" background at night and General settings are not in the documentation
set, so every frame matched the committed one; only the fingerprint moves.
2026-09-30 13:10:59 +03:00
Sergey MatyuninandClaude Opus 5.5 59e05482fe chore(golden): accept the #661 frames from Linux CI
12 reviewed frames from the golden-images artifact of Validate run
36696388011 (c8c470ed), 178 kept byte-for-byte:

- 2 new scenes (#661 AC7): day-cycle-night-moon-gibbous-dark (Moscow,
  2026-10-21 18:00Z, k 0.79) and day-cycle-dusk-moon-crescent-south-dark
  (Sydney, 2026-10-14 09:00Z, k 0.14) — the moon top-left behind the plan,
  lit on the left, the plan covering part of the disc.
- settings-help-zoom-200-en-light and -ru-dark: the General settings card
  title «Sun» became «Sun and Moon»; nothing else in the frame moved by
  more than antialiasing.
- 8 isometric frames (stage6 ×5, stage3-overlays ×2, large-warm-remount):
  badged device icons keep their state-free place — #711's intended
  shift, merged into dev without accepting them; named here explicitly.

Release: v1.79.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36696388011
Issue: #661
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 13:10:59 +03:00
Sergey MatyuninandClaude Opus 5.5 c8c470ed57 feat(view): the moon in its phase over the "Follow the Sun" background (#661)
At dawn, dusk and night the environment shows the moon in the top-left
corner of the scene, behind the plan: computed in the card from the home
coordinates and the browser clock (short Meeus series + topocentric
parallax, within 1.4° / 1.8 pp of JPL Horizons), one designer image under
a continuous phase mask with the lit side always on the left (owner
2026-09-29), a feathered terminator, 3°/3 % thresholds and the 2 s fade of
the window rays. Everything but a small gate lives in the lazy
moon-runtime chunk, with its own 30 s ticker. General settings: "Sun"
becomes "Sun and Moon" with one switch, on for new installations
(DEFAULT_CONFIG), off for existing ones.

The initial View graph sat 728 B under its budget: the gate is paid for by
moving fifteen dialog-only strings of General settings into the lazy
settings dictionary (#459) and by one build fingerprint literal instead of
three, so the graph ends 4 B above dev. Golden: two new moon scenes, and
the two General settings help frames show «Sun and Moon»; the WSL artifact
test fixture now models scenes whose first capture awaits acceptance.

Issue: #661
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 12:25:46 +03:00
claude[bot] e515bdaab8 docs(reviews): индекс после сдвига каталога (#711)
Issue: #711
User-Visible: no
2026-09-30 08:11:50 +00:00
claude[bot] a352c7c132 docs: review document for #711
Issue: #711
User-Visible: no
2026-09-30 08:11:47 +00:00
Claude a372b354c6 fix(2.5D): a device icon never moves because its state changed (#711)
Owner's decision in #694: icons must not change position with state.
Since #651 the rigid overlay layout sized a device by its value text and
badges, which change with HA state: a light toggling on changed its width
from 37.2 to 26.7 CSS px and re-laid out all 62 overlays of the dense
scene (~385 of 495 ms of stateUpdate; v1.77.0 had 208 ms).

- iso-scene-render.ts: the layout sees the state-free tile (icon at its
  configured size, no value text, badge or supplemental metrics). An
  HA-only change keeps the layout and refreshes only the visual extent
  that scene bounds read, without a collision search.
- iso-overlays.ts: the rigid-group search skips candidates that already
  lose to the fallback on room, then wall violations (lexicographic
  bound). The result is unchanged — identical placement hash on the dense
  scene — at about 35 % less work.
- docs/ISOMETRIC.md, changelogs; test #711; mutant
  iso-device-layout-follows-state-again (written, not run — #709).

Local isometric-stage3-dense-v1, 3 samples: stateUpdate 522 → 89 ms
(v1.77.0: 208), modelReady 3665 → 3129, switchCycle 5544 → 4700.

Issue: #711
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 10:51:53 +03:00
claude[bot] 9caa3c2316 docs(reviews): индекс после сдвига каталога (#692)
Issue: #692
User-Visible: no
2026-09-30 05:29:14 +00:00
claude[bot] df20da9676 docs: review document for #692
Issue: #692
User-Visible: no
2026-09-30 05:29:11 +00:00
Claude bb33626f38 ci(perf): the interaction first-frame ceiling sits above the 1.78 level (#692)
The full workflow's 7-sample median of firstStableRenderMs for
large-house-interaction-v1 was about 2790 ms across the 1.77 line and
about 2920 ms at v1.78.0 (7d4d75bd: 2925.0; the neighbouring run of the
same SHA read 3144.8). The 3000 ms ceiling sat 2.7 % above the level and
failed on runner noise; #689's 3-sample smoke read 3002.4.

- hardMaxMs 3000 → 3400 in the full profile and its smoke twin (one
  number, #473 AC4): +16 % over the 1.78 level, +8 % over the worst run.
  The base-relative ratio and noise allowance are unchanged.
- README: the series and the reasoning; the test pins the number and the
  series points.

Issue: #692
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-30 08:16:19 +03:00
claude[bot] 4c74e80aa9 docs(reviews): индекс после сдвига каталога (#709)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #709
User-Visible: no
2026-09-29 20:27:27 +00:00
claude[bot] eaeada16ae docs: review document for #709
Issue: #709
User-Visible: no
2026-09-29 20:27:24 +00:00
Claude 09d35c8d2c docs(testing): the manual pre-push run lists no mutants (#709 r1)
CODE-REVIEW-709-r1 M1: the "Локальный набор перед пушем" section still
showed `--no-mutants`/`--max-mutants` and listed diff mutants among what
the manual pre-push-gate runs, right after the paragraph saying it runs
none. The examples and the list now match the code; the flags are named
as accepted no-ops. The `--changed` tool description is marked as a
nightly-failure diagnostic, not a task gate.

Issue: #709
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 23:19:01 +03:00
claude[bot] 920e626948 docs: review document for #709
Issue: #709
User-Visible: no
2026-09-29 20:14:55 +00:00
Claude a8321e32cc process: mutants run only in the nightly full registry; speed rules for ship/show (#709)
Owner's decision 2026-09-29: mutants check the tests, not the product.
During development they are not run at all — not locally, not in CI,
not by the reviewer. The whole registry is the nightly run
(mutation-gate.yml, #513); a survivor files an issue (#472). The #693
post-mortem: 36 of 57 minutes of a one-line fix went to optional work.

- process-track.mjs: `mutants` is always false (no track, no label).
- classify-changes.mjs: Validate requests no diff mutants on any event;
  the `mutants` input stays so old `-f mutants=…` calls do not fail.
- _process.yml: the default for the gate and the merge is false.
- pre-push-gate.mjs: the manual run no longer runs mutants.
- Canon: PROCESS §2.7 (a mutant is written, not run; `--check` keeps the
  anchors), §5.1 (`ci:mutants` retired), §8 (ship/show: nothing beyond
  gate:small and the spec — one proof per item, no `--smokes` on ship,
  a stray flake is an issue, not an investigation), §10.4; AUTHOR,
  REVIEWER, AGENTS, TESTING.
- Registry: four mutants of the old request rules replaced by
  dev-mutants-requested-again and track-pays-for-mutants-again.

Issue: #709
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 23:04:41 +03:00
Claude 18c9f8e77c fix(stairs): View shows the link pointer over a stair, not the move cursor (#693)
The Plan editor rule `.hp-stair.input-enabled .hp-stair-hit { cursor: move }`
also matched the View layer, which sets input-enabled only to receive
clicks. The hit area sits over the outline, so the link's pointer on the
group was never visible and every stair in View showed a drag it cannot do.

- src/stairs-view.ts: View stairs carry `hp-stair-view`.
- plan.styles.ts: `move` applies only without it; in View the hit area
  keeps the group's cursor — pointer on a link, the stage's otherwise.
- demo/smoke_stairs.mjs: computed cursors in View (link, no target) and
  in the Plan editor; the two View checks are red on the old code.
- test/stairs.test.mjs: the cascade without Chromium; mutant
  view-stair-cursor-move-again.
- docs/STAIRS.md, changelogs.

Issue: #693
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 21:52:52 +03:00
claude[bot] 674e589ad8 docs(reviews): индекс после сдвига каталога (#690)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #690
User-Visible: no
2026-09-29 06:27:42 +00:00
claude[bot] 26763c7ca4 docs: review document for #690
Issue: #690
User-Visible: no
2026-09-29 06:27:39 +00:00
Claude 52a56430ab process: an unproven smoke link runs the visual minimum; raster defects need a witness (#690)
The two remaining owner decisions of #690 and the legacy trivial text.

- scripts/smoke-select.mjs: VISUAL_MINIMUM, eight smokes of modes,
  layers and rendering (under a minute locally). An executable diff
  with no proven link now returns and prints it instead of only "the
  reviewer decides"; #687 missed smoke_modes that way (item 1').
- scripts/gate-small.mjs: `--smokes` runs the minimum with the
  selection.
- PROCESS §7.1 and AUTHOR.md: a raster, sharpness or compositing defect
  needs a witness red on the old code for the owner's symptom and the
  owner's confirmation in a real GPU browser (item 4).
- PROCESS §8, TESTING.md: the minimum in the smoke-select rule.
- scripts/task-packet.mjs: legacy `trivial` is product flow read as
  track:show (§5.1), not a short track without a spec.
- Tests; mutants visual-minimum-silent-again,
  visual-minimum-on-proven-link, gate-small-skips-visual-minimum;
  task-packet-trivial-is-product-flow retargeted.

Issue: #690
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 08:45:44 +03:00
claude[bot] 6e07313460 docs(reviews): индекс после сдвига каталога (#706)
Issue: #706
User-Visible: no
2026-09-29 05:04:40 +00:00
claude[bot] f903e93c83 docs: review document for #706
Issue: #706
User-Visible: no
2026-09-29 05:04:38 +00:00
Claude ae0e516af1 process: a rereview sets S7-code-review again instead of stripping it (#706)
The label step after integration ran one gh call
`--add-label "$TO" --remove-label "$FROM"`. For the rereview outcome
TO == FROM == S7-code-review, and gh added and removed the same label:
#699 was left without a status and no new round started (run
36491087708).

- scripts/status-label.mjs: the same label is removed and set again
  through relabel from process-reconcile (#555), so the labeled event
  starts the next round and a failed restore fails the step; a
  different label is still one call.
- _process.yml: the step calls the script.
- PROCESS.md: the exact-candidate rule names the relabel.
- test/status-label.test.mjs; mutants rereview-relabel-in-one-call and
  process-label-step-combined-again.

Issue: #706
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 07:19:05 +03:00
claude[bot] e7fca7b955 docs(reviews): индекс после сдвига каталога (#699)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #699
User-Visible: no
2026-09-28 22:39:36 +00:00
claude[bot] 48f6f2e6a7 docs: review document for #699
Issue: #699
User-Visible: no
2026-09-28 22:39:36 +00:00
claude[bot] c949e0bbc9 docs(reviews): индекс после сдвига каталога (#699)
Issue: #699
User-Visible: no
2026-09-28 22:39:36 +00:00
claude[bot] 5bb7197620 docs: review document for #699
Issue: #699
User-Visible: no
2026-09-28 22:39:36 +00:00
Claudeandclaude[bot] 6a8658cab9 docs(release): the candidate checklist lowers the ratchets (#699 r1)
CODE-REVIEW-699-r1 M1: `node scripts/ratchets.mjs tighten` was named only
by the warning `release:prerelease` prints at publication, when the
candidate commit is already made. The candidate checklist in
docs/DEVELOPMENT.md now runs it after `npm run bundle:release`, so the
caps come from the fresh dist/ and land in the candidate commit.

- test/ratchets.test.mjs pins the step and its order.
- Mutant release-runbook-forgets-tighten.

Issue: #699
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 22:39:36 +00:00
claude[bot] 258d1990c8 docs: review document for #699
Issue: #699
User-Visible: no
2026-09-28 22:39:36 +00:00
Claudeandclaude[bot] 2369c50607 process: the bundle CLI judges the beta ceiling before the budget (#699)
Validate on 3dd032d7 (run 36480911145): the mutant
initial-view-ceiling-unplugged survived. With the band over the ceiling,
ceiling + band (303 000) lies above the absolute INITIAL_VIEW_GZIP_BUDGET
(301 066), so every value the CLI test could feed went red on the budget
first and the ceiling check became unobservable.

- bundle-budget.mjs CLI: the initial View ceiling is judged before
  assertBundleBudget, so a growth over the band names the ratchet that
  caught it; the budget still stops anything the band lets through.
- The CLI test feeds ceiling + band + 1 and expects the band message.
- The mutant's anchor follows the moved lines.

Issue: #699
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 22:39:36 +00:00
Claudeandclaude[bot] c68d92f674 process: ratchets get a band over the beta ceiling (#699)
Two-sided ratchets with zero slack made parallel tasks conflict on shared
numbers, recompute them after every rebase and hit a ceiling because a
neighbour merged first (#689 after #691).

- Core lines (test/core-file-budget.test.mjs): a branch may grow up to
  CORE_BAND = 50 lines over the beta ceiling; shrinking no longer fails it.
- Bundle graphs (bundle-budget.mjs): initial View and lazy graphs fail only
  above ceiling + 2 000 B; below the ceiling is not a branch finding. The
  absolute INITIAL_VIEW_GZIP_BUDGET stays the wall.
- Monolith numbers (monolith-metrics.mjs, unused-locals-gate.mjs):
  METRIC_BANDS — 5 for delegates, port members and privates, 25 for host.
  refs, 2 000 B for dist/; a lower number is reported, not failed.
- Browser mutation guards: 200 is a guideline — mutation-gate --check warns
  above it instead of failing; every guard still needs its reason line.
- scripts/ratchets.mjs: `report [--warn]` and `tighten` — on the beta
  candidate the release manager sets every ceiling to the fact in one
  commit; release:prerelease prints loose ceilings as a warning.

Canon: PROCESS.md §3 (browser guards, monolith numbers) and §8 «Храповики»;
docs/TESTING.md.

Issue: #699
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 22:39:36 +00:00
claude[bot] 8bb43fd379 docs(reviews): индекс после сдвига каталога (#700)
Issue: #700
User-Visible: no
2026-09-28 22:39:11 +00:00
claude[bot] 310bc6e4f8 docs: review document for #700
Issue: #700
User-Visible: no
2026-09-28 22:39:08 +00:00
claude[bot]andClaude 1aa52d2107 docs: review document for #700
Issue: #700
User-Visible: no
2026-09-29 01:22:14 +03:00
Claude f6e317d871 process: a failed read of open issues never files a duplicate (#700 r1)
CODE-REVIEW-700-r1 Medium: `gh issue list … || true` turned a failed read
into an empty answer, and the step went on to gh issue create — a second
[workflow-sync] issue next to the open one on every network or rate-limit
failure. A failed read now warns and exits 0; creating stays reserved for
«read succeeded, nothing open».

Mutant workflow-sync-issue-duplicated-on-read-failure.

Issue: #700
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 01:22:14 +03:00
claude[bot]andClaude a005aae5b6 docs: review document for #700
Issue: #700
User-Visible: no
2026-09-29 01:22:14 +03:00
Claude e45bc87c6d process: preflight does not fail a task branch for foreign causes (#700)
11 of 85 returns in #600–#691 were the thin-workflow mirror check, and any
push could turn red because a foreign site behind a docs link was down.

- validate.yml preflight: on refs/heads/issue/* the workflow_sync mismatch
  is a warning in the summary, not a failed verdict; push to dev, the beta
  candidate and the release keep it red.
- On push to dev a mismatch opens one owner issue titled [workflow-sync]
  (or comments on the open one), like the nightly mutation gate (#472);
  preflight gets issues: write for that.
- check-docs --external=warn: external link failures become warnings; the
  docs step passes it on task branches only.

Canon: PROCESS.md §10.4 («Workflow из ветки по умолчанию»).

Issue: #700
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 01:22:14 +03:00
claude[bot] e1700757de docs(reviews): индекс после сдвига каталога (#701)
Issue: #701
User-Visible: no
2026-09-28 21:51:03 +00:00
claude[bot] 1606407107 docs: review document for #701
Issue: #701
User-Visible: no
2026-09-28 21:51:00 +00:00
Claude 8dcc1cad4e docs(process): канон без противоречий, вход автора короче (#701)
Сверка PROCESS.md, ролевых выжимок, AGENTS.md, TESTING.md, CONTRIBUTING.md
и скриптов по 26 найденным расхождениям (D1–D26): трейлеры по классам
изменений, gate:small как единственный источник состава, пороги ревью,
путь реестра мутантов, golden по ci:golden, порядок чтения промпта ревью.

- scripts/change-classes.mjs: классы A/B/C/D — один модуль для
  process-gate и проверки трейлеров.
- commit-msg: коммит только с файлами класса C (документация) трейлеров
  не требует; указанные трейлеры по-прежнему проверяются.
- Маршрут автора без docs/STATUS.md: 5345 → 4703 слова.
- Промпт ревью читает SCOPE → AGENTS → REVIEWER, как ROUTES.reviewer.

Issue: #701
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-29 00:30:04 +03:00
claude[bot] 224d0106fd docs(reviews): индекс после сдвига каталога (#702)
Issue: #702
User-Visible: no
2026-09-28 21:18:49 +00:00
claude[bot] ee3aa068fc docs: review document for #702
Issue: #702
User-Visible: no
2026-09-28 21:18:49 +00:00
Claudeandclaude[bot] 19dc61db15 process: the merge deletes the task branch it merged (#702)
370 merged issue/* branches sat on origin; the branch list stopped meaning
anything and an agent looking a branch up by number could take a stale one.

- merge-candidate.mjs: after a successful push to dev the task branch is
  deleted with --force-with-lease on the tip the merge saw last — the
  candidate published into the branch, or the material on fast-forward
  (the index commit lives only in dev). A commit that landed after the
  merge keeps the branch, and the merge comment says so; a failed delete
  never undoes the merge. Failed, stale and conflicting merges keep it.
- The one-time cleanup of the already merged branches is not in this
  commit: the list goes to the owner first.

Canon: PROCESS.md §10.4 (exact-candidate merge).

Issue: #702
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 21:18:49 +00:00
claude[bot] c66884b7f5 docs(reviews): индекс после сдвига каталога (#698)
Issue: #698
User-Visible: no
2026-09-28 21:01:38 +00:00
claude[bot] 59bf216b60 docs: review document for #698
Issue: #698
User-Visible: no
2026-09-28 21:01:38 +00:00
Claudeandclaude[bot] 6ab791e348 docs(process): name today's monolith tolerance in the rebase rule (#698 r1)
CODE-REVIEW-698-r1 Medium: the canon said the merged monolith numbers are
judged «by the band test (#699)», but #699 is not merged — today
compareWithBaseline judges five numbers exactly and only dist/ bytes with
a band. The paragraph now says so: dev's side of the baseline turns the
candidate's Validate red when the task itself changed those numbers — the
same return to the author as before, after Validate instead of before the
review; the band for all six numbers is #699. The comment on UPSTREAM_WINS
says the same.

Issue: #698
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 21:01:38 +00:00
claude[bot] e6dfa9a752 docs: review document for #698
Issue: #698
User-Visible: no
2026-09-28 21:01:38 +00:00
Claudeandclaude[bot] 5986332eda process: the rebase merges what two tasks never disagree on (#698)
14 of 48 returns in #600–#691 were rebase or merge conflicts on shared
files where the two edits do not contradict each other.

- .gitattributes: docs/CHANGELOG.md and docs/CHANGELOG.ru.md use the
  built-in merge=union driver — both tasks' lines in ## Unreleased survive
  a rebase, a merge and git merge-tree (#696's clean-merge test) without a
  stop.
- rebase-generated.mjs: UPSTREAM_WINS — on a conflict in
  scripts/monolith-baseline.json the rebase takes dev's side; the band test
  on the candidate's Validate judges the merged tree (#699). Any other
  conflicting path aborts exactly as before, with the full list.
- merge-candidate.mjs: the candidate's patch-id excludes the changelogs and
  the monolith baseline next to docs/reviews, so a neighbour's line next to
  the task entry does not re-send a green task to review.
- screenshots.json needs nothing: after #697 task branches do not commit it.

Canon: PROCESS.md, the rebase paragraph of the review index (#643).

Issue: #698
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 21:01:38 +00:00
claude[bot] 13054e246e docs(reviews): индекс после сдвига каталога (#697)
Issue: #697
User-Visible: no
2026-09-28 20:59:40 +00:00
claude[bot] fa570642df docs: review document for #697
Issue: #697
User-Visible: no
2026-09-28 20:59:36 +00:00
Claude 2a62ad5b95 process: derived artifacts are accepted on dev once per beta (#697)
The screenshot fingerprint and golden baselines stop being a tax on every
task branch:

- Task branches no longer commit docs/images/** or golden baselines. On a
  branch the screenshot freshness stays a preflight warning; the review
  prompt, REVIEWER.md and AUTHOR.md drop check-docs as a per-task gate.
- beta-derived.yml refreshes them on dev in one bot commit before the beta
  candidate: canonical docs capture + docs:accept --reviewed, golden from
  the golden-images artifact of a completed Validate on dev +
  golden:accept --reviewed. A changed frame or scene is accepted only when
  named in the inputs; undeclared differences refuse. Baseline commits carry
  Release: and Baseline-Reviewed:; the subject is not a candidate subject.
- classify-changes: the Release: trailer on an issue/* branch no longer
  switches on the heavy set. ci:full / ci:golden do: process-track emits
  full=true, the review gate dispatches Validate with full=true and does not
  accept a light proof.

Canon: PROCESS.md §3 п.13, §5.1, §8, §11.4; CONTRIBUTING.md.

Issue: #697
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 23:38:50 +03:00
claude[bot] c716bb0f63 docs(reviews): индекс после сдвига каталога (#696)
Issue: #696
User-Visible: no
2026-09-28 20:33:14 +00:00
claude[bot] ac75df7e9a docs: review document for #696
Issue: #696
User-Visible: no
2026-09-28 20:33:11 +00:00
Claude e1ae8f4ac7 process: the review pipeline prices each round by track (#696)
show/ship stop paying for diff mutants and for every move of dev:

- scripts/process-track.mjs resolves the track from the current labels and
  the diff (show for unlabelled infra, ask for unlabelled product work) and
  checks the mechanical ship limits; outside them the pipeline comments and
  relabels track:ship -> track:show in the same round.
- Validate on the review material is light on show/ship: a completed push
  run on the exact SHA is proof, a dispatch asks mutants=false. ask and the
  ci:mutants label keep the mutant dispatch.
- show/ship skip the pre-review rebase when git merge-tree with dev is
  clean; the candidate is rebased once at merge and still passes Validate
  before the push to dev. The light merge waits for the push run of the
  candidate and dispatches only when none appears.
- ship inside the limits merges after the light Validate without a model
  review; the issue gets a machine marker hp:ship-merge.
- ship-review.yml + scripts/ship-review.mjs read the code of all ship
  tasks of a beta range in one model session and publish
  docs/reviews/SHIP-REVIEW-<tag>.md; both beta publication paths refuse a
  range with ship tasks the document does not cover or that carries a High.
- show reviews judge correctness and AC; the spec review installs neither
  npm ci nor Chromium, the show review installs Chromium only when the issue
  names a smoke.

Canon: PROCESS.md §5, §5.1, §10.4, new §11.7; REVIEWER.md, AUTHOR.md and
AGENTS.md digests.

Issue: #696
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 23:09:46 +03:00
claude[bot] 9e4bfb4379 docs(reviews): индекс после сдвига каталога (#695)
Issue: #695
User-Visible: no
2026-09-28 20:07:15 +00:00
claude[bot] d8465b3a09 docs: review document for #695
Issue: #695
User-Visible: no
2026-09-28 20:07:11 +00:00
Claude f6c76b7351 process: infrastructure without a track label runs as show (#695 r1)
CODE-REVIEW-695-r1 Medium: PROCESS §5.1 says an infrastructure task (§1)
without a track label reads as track:show, but neither the pipeline guard
nor the task packet did that.

- _process.yml guard: with no track:* and no small/trivial label, the
  diff of the task branch against dev (compare API) with no class A file
  gives the show cycle limit 2. A truncated compare answer (300 files)
  proves nothing and keeps the limit 4.
- task-packet.mjs: an infrastructure packet names the track it runs on:
  «инфраструктурный · show» without a label, the owner's label otherwise.
- Mutants guard-infra-keeps-ask-limit and
  packet-infra-track-ignores-show-default.

Issue: #695
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 22:50:19 +03:00
claude[bot] 51854aea53 docs: review document for #695
Issue: #695
User-Visible: no
2026-09-28 19:36:14 +00:00
Claude 57ce10721f process: tracks ship/show/ask are set by the owner's label (#695)
The analysis of 85 closed tasks #600-#691 showed that the light track
cost as much as the full one (115 min and 12 events vs 102 and 13) and
that the owner had no label to choose the route. The owner accepted the
proposal on 2026-09-28.

- PROCESS §5 is the track table: track:ship (S1 -> S5, one line under
  "## ТЗ", <= 30 src lines, batch review before the beta), track:show
  (default, S2 -> S5, up to three AC, no spec review, 2 code cycles),
  track:ask (full route). The owner's label beats the criteria, which
  become a hint; any agent may raise a track, only the owner lowers it.
- §5.1: ci:full / ci:golden / ci:mutants order heavy checks on any track;
  small and trivial read as track:show, no label as track:ask, an
  infrastructure task as track:show.
- §2, §2.2, §2.4, §2.5, §4, §7.1, §7.2, §9, §11 follow; AUTHOR/REVIEWER
  digests and AGENTS.md follow with the digest test and its mutants.
- task-packet.mjs reports the track via trackFromLabels(); the pipeline
  reads track:show/track:ship for the cycle limit of 2 and lets an
  explicit track:ask win. Pipeline behaviour by track is #696.

Issue: #695
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 22:13:21 +03:00
Matysh 7d4d75bdfb release: promote v1.78.0 from beta line
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Canceled after 0s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Canceled after 0s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Бандл головы dev для стенда (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Issue: #616
Issue: #649
Issue: #651
Issue: #654
Issue: #660
Issue: #663
Issue: #676
Issue: #683
Issue: #688
Issue: #691
User-Visible: yes
Release: v1.78.0
2026-09-28 21:07:36 +03:00
Matysh 9e5f211b0a chore(release): include main CI history before v1.78.0 promotion 2026-09-28 21:05:10 +03:00
Matysh 1ae0ea1c3b Release v1.78.0-beta.9 candidate
Issue: #688
Issue: #689
Issue: #691
User-Visible: yes
Release: v1.78.0-beta.9
2026-09-28 20:44:43 +03:00
claude[bot] db2224c964 docs(reviews): индекс после сдвига каталога (#689)
Issue: #689
User-Visible: no
2026-09-28 17:26:33 +00:00
claude[bot] 4b23679d3c docs: review document for #689
Issue: #689
User-Visible: no
2026-09-28 17:26:30 +00:00
Claude f2b66ad677 docs(screenshots): accept the #689 source fingerprint, pixels unchanged
The docs-screenshots workflow run 36430098138 (issue/689-daycycle-layers
before #691) decoded to the committed pixels in all 11 frames; its bytes
differ only by the oxipng repack. After the rebase onto #691,
`npm run docs:accept -- --identical` confirmed 11/11 pixel-identical
frames on the combined tree, so only the source fingerprint moves
(4955edd1 -> 62c5c18b); frames, their sha and the capture packer stay.

Release: v1.78.0-beta.9
Issue: #689
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:54:32 +03:00
Claude 836ef7710f chore(golden): accept the #689 frames from Linux CI
29 reviewed frames from the golden-images artifact of Validate run
36427848424 (c1289886), 159 kept byte-for-byte:

- 21 junction-* and large-house-zoom-040/250 frames return to their
  pre-#685 look: each equals its baseline from before 139dceaa (no pixel
  off by more than 8 levels). #685's gradient had also doubled the visible
  stripe width (25 % of the step instead of the historical 12.5 %).
- 4 static-hatch-openings-* close-ups (#685's scenes, kept) now show the
  restored pattern.
- 4 stairs-* frames carry #688's single 3.6 cm stair line weight: #688 was
  merged into dev without accepting them; named here explicitly (#690).

No day/night frame moved.

Release: v1.78.0-beta.9
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36427848424
Issue: #689
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00
Claude 87195c2b5d test(daycycle): #582/#544 witnesses follow the bounded, hint-free layers
smoke_live_pan_coverage now expects an exposed scene to open only within
clip-path: inset(-25%) and the clip-marked day-cycle outline never to open;
idle scenes keep no clip-path. paper-scene-contract pins the opacity hint.

The Release trailer asks the branch's push Validate for the heavy jobs
(smokes, golden, performance): #689 moves golden frames and must be proven
by the full smoke set before S7 (#690).

Release: v1.78.0-beta.9
Issue: #689
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00
Claude 600330e187 fix(daycycle): stage-sized plan layers without a frozen raster; revert #685 hatch
With the day/night background the plan went blurry after zooming from 100 %
(sharp when the page was opened at 800 %) and navigating a strongly zoomed
plan flashed the page white. Both came from #582's composition, not from the
wall hatch that #685 replaced:

- `.stage.daycycle.hp-safe-daycycle-outline .plan-svg` promoted the scene
  with `will-change: transform`; Chromium freezes the raster scale of such a
  layer, so the 100 % raster was shown stretched. The explicit layer #582
  needs is now `will-change: opacity` (re-rasters at the current scale).
- The filtered outline had `overflow: visible` and a gesture exposed every
  scene (#544) without bound, so the promoted layers grew with zoom squared
  (CDP LayerTree, ~460 %: plan-svg 15.9x, outline 13.2x the stage; 39x after
  navigating at 800 %). The full card clips its outline to its box and marks
  it data-hp-live-overflow="clip" (never exposed); the live viewport bounds
  every other exposure with an inline clip-path: inset(-25%) that leaves
  with it, so idle DOM stays byte-identical (#531).

Owner-verified in Chrome 152 (built-in browser, DPR 2): sharp after 100 ->
800 %, no white flashes after reloading at 800 %.

Owner decision: #685's analytic gradient is reverted (13af1d5e), the single
<pattern> is back at every zoom; its close-up golden scenes stay and check
the pattern, its terminal-frame smoke checks the pattern.

Witnesses: demo/smoke_daycycle_zoom_layers.mjs (800 % x DPR 2: layers vs
stage, the hint, reload path, button/wheel/pinch); #582/#532 smokes now pin
the opacity hint; test/live-viewport.test.mjs (bounded exposure, clipped
scene); test/daycycle-layers.test.mjs (cascade). Four Node-guarded mutants.

Issue: #689
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00
claude[bot] c1e05e465b docs(reviews): индекс после сдвига каталога (#691)
Issue: #691
User-Visible: no
2026-09-28 14:32:55 +00:00
claude[bot] 0a09712bc5 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 14:32:51 +00:00
Matysh 33ae14a99d test(touch): align kiosk smoke and Russian guide (#691)
Issue: #691
User-Visible: no
2026-09-28 17:17:08 +03:00
claude[bot] 09f8cf4c11 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 14:14:26 +00:00
Matysh 53503e0373 chore(metrics): accept touch controller bundle growth (#691)
Issue: #691
User-Visible: no
2026-09-28 16:33:33 +03:00
Matysh 37fbb9824d fix(touch): separate pan, edge swipe and double fit (#691)
Issue: #691
User-Visible: yes
2026-09-28 16:27:45 +03:00
claude[bot] 3f17a1497d docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 12:56:39 +00:00
claude[bot] 20f17fee54 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 12:51:01 +00:00
claude[bot] 1751184b12 docs: review document for #689
Issue: #689
User-Visible: no
2026-09-28 12:30:23 +00:00
claude[bot] 63c24784b6 docs(reviews): индекс после сдвига каталога (#688)
Issue: #688
User-Visible: no
2026-09-28 12:21:14 +00:00
claude[bot] 8207a175d6 docs: review document for #688
Issue: #688
User-Visible: no
2026-09-28 12:21:12 +00:00
Matysh c04d0778ae fix(stairs): use one physical line weight (#688)
Issue: #688
User-Visible: yes
2026-09-28 14:58:33 +03:00
claude[bot] e44fbd6c9e docs: review document for #688
Issue: #688
User-Visible: no
2026-09-28 11:48:48 +00:00
Claude c392ad3a9c ci: mirror thin workflow callers in main after #658
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 3m1s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 3m24s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 3m31s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 3m33s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 1m50s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 1m49s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 1m51s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 1m54s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 27s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 47s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 37s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 32s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m13s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 2m54s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 1m15s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 1m14s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 7m1s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 19s
Расписания исполняются из ветки по умолчанию: три тонких вызывающих файла
обязаны совпадать с dev. Зеркало dev@f1a87fba: cron ушли с круглых минут
(ночь 02:17, мутанты 00:43, метрики понедельника 05:23).

Issue: #658
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 14:20:42 +03:00
Claude dc72141af6 ci: mirror thin workflow callers in main after #623
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 1m47s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 1m45s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 2m8s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 2m3s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 1m37s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 1m39s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 37s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 1m54s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 1m55s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 31s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 1m32s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 25s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 16s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 46s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 39s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 37s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 7m13s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 22s
Шесть файлов, которые GitHub исполняет из ветки по умолчанию, стали тонкими
вызывающими: тела живут в dev (`_*.yml`) и вызываются `@dev`. Это последнее
ручное зеркало тел; дальше тонкие файлы меняются только вместе с триггерами
или потолком прав.

Issue: #623
User-Visible: no
2026-09-24 10:59:05 +03:00
Claude 8741f3f6d7 ci: mirror process.yml in main after #643
Конвейер исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev: ребейз перед ревью пересобирает docs/reviews/INDEX.md вместо отказа.

Issue: #643
User-Visible: no
2026-09-24 10:08:20 +03:00
Claude be54edf988 ci: mirror mutation-gate.yml in main after #620
Ночной реестр исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev@ee4bbf8f: пропуск ночи по неизменённому дереву, окружение шарда по плану.

Issue: #620
User-Visible: no
2026-09-24 09:24:21 +03:00
Claude 7752329d71 ci: mirror process.yml in main after #634
Конвейер исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev@7dc75972: промпт ревьюера ссылается на docs/process/REVIEWER.md.

Issue: #634
User-Visible: no
2026-09-24 07:51:40 +03:00
Claude 093ea906ae ci: mirror process.yml in main after #635
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 1m51s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 2m6s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 1m43s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 1m44s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 2m14s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 2m12s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 2m10s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 2m14s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 35s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 1m10s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 1m39s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 18s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 36s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m11s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 42s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 36s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 38s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 21s
Issue: #635
User-Visible: no
2026-09-23 18:26:47 +03:00
Claude 0c93344913 ci: mirror process.yml in main (#621)
Конвейер исполняется из ветки по умолчанию; файл обязан совпадать с dev.
Зеркало dev@50e67c09: листинг docs/reviews через Git Trees API.

Issue: #621
User-Visible: no
2026-09-23 13:21:59 +03:00
Claude eecb5291d0 ci: зеркало еженедельных метрик процесса в main (#637)
Issue: #637
User-Visible: no
2026-09-23 11:58:52 +03:00
Claude 58ba44dbac ci: mirror review-resume workflows in main (#636)
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Failing after 1m55s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Failing after 1m57s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Failing after 2m6s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Failing after 42s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Failing after 1m35s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Failing after 1m51s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Failing after 1m50s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Failing after 1m56s
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Failing after 1m53s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 45s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 24s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Failing after 24s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 50s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 20s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 46s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 35s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 6m4s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 23s
Issue: #636
User-Visible: no
2026-09-23 09:43:01 +03:00
643 changed files with 62205 additions and 10148 deletions
+6
View File
@@ -12,3 +12,9 @@
*.zip binary
*.woff binary
*.woff2 binary
# #698: записи двух задач в `## Unreleased` друг другу не противоречат — при
# слиянии и ребейзе git объединяет обе стороны сам (встроенный драйвер union),
# и параллельные задачи не возвращаются в S6 из-за соседней строки ченджлога.
docs/CHANGELOG.md merge=union
docs/CHANGELOG.ru.md merge=union
+228
View File
@@ -0,0 +1,228 @@
name: "Бета: производные артефакты на dev · тело (#716)"
# #697, PROCESS.md §8 и §11.4: ветки задач не коммитят ни отпечаток и кадры
# скриншотов документации, ни эталоны golden. Их обновляет один коммит бота на
# `dev` перед кандидатом беты; коммит проверяет релиз-менеджер.
#
# - Скриншоты: съёмка на `dev` тем же каноническим способом, что
# `docs-screenshots.yml`, и приёмка `docs:accept --reviewed`. Кадры, которые
# менять не собирались, обязаны совпасть байт-в-байт (свидетели среды);
# изменившийся кадр принимается, только если назван в `docs_expect_change`.
# Ничего не изменилось — обновляется один отпечаток исходников.
# - Golden: артефакт `golden-images` полного Validate на `dev`
# (`golden_run`) и `golden:accept --reviewed` с объявленными сценами. Коммит
# несёт `Release:` и `Baseline-Reviewed:` — провенанс, который требует
# validate-commit-provenance.mjs.
#
# Необъявленная разница — отказ с перечнем кадров, артефакт съёмки остаётся
# для просмотра: решение, какие кадры сдвинулись законно, принимает человек.
on:
# #716: тело вызывается тонким файлом `beta-derived.yml` по ссылке `@dev`;
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
workflow_call:
inputs:
tag:
description: "Beta tag these artifacts are for, for example v1.79.0-beta.1"
required: true
type: string
docs_expect_change:
description: "Doc scenario ids expected to change, comma-separated (empty = none)"
required: false
type: string
default: ""
golden_run:
description: "Validate run id on dev whose golden-images artifact holds the shifted frames (empty = skip golden)"
required: false
type: string
default: ""
golden_expect_change:
description: "Golden scenes expected to change, comma-separated"
required: false
type: string
default: ""
golden_expect_new:
description: "New golden scenes, comma-separated"
required: false
type: string
default: ""
permissions:
contents: read
# Concurrency уровня workflow — у вызывающего `beta-derived.yml` (#716).
jobs:
accept:
name: "Отпечаток, кадры и эталоны — одним коммитом в dev"
runs-on: ubuntu-24.04
timeout-minutes: 40
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- run: npm ci
- name: Кэш браузеров Playwright
id: pw
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install pinned Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install chromium
- name: Build the bundle the screenshots must come from
run: npm run build
# Тот же закреплённый упаковщик, что у docs-screenshots.yml: другой
# переписал бы все кадры без единого изменённого пикселя.
- name: Установить oxipng
env:
OXIPNG_VERSION: 10.2.0
OXIPNG_SHA256: b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e
run: |
set -euo pipefail
asset="oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -o "$asset" \
"https://github.com/oxipng/oxipng/releases/download/v${OXIPNG_VERSION}/${asset}"
echo "${OXIPNG_SHA256} ${asset}" | sha256sum -c -
mkdir -p "$HOME/.local/bin"
tar -xzf "$asset" --strip-components=1 -C "$HOME/.local/bin" \
"oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu/oxipng"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/oxipng" --version
- name: "Съёмка воспроизводима между прогонами (#410, #422)"
run: node scripts/capture-determinism.mjs
- name: Кадры документации — съёмка и приёмка
id: docs
env:
EXPECT: ${{ inputs.docs_expect_change }}
run: |
set -euo pipefail
node demo/docs/capture.mjs --stability=3
cand="$RUNNER_TEMP/docs-candidate"
rm -rf "$cand" && mkdir -p "$cand"
cp docs/images/*.png docs/images/screenshots.json "$cand/"
# Приёмка сравнивает кандидата с ЗАКОММИЧЕННЫМИ кадрами: рабочая
# копия возвращается к dev, и заменить файлы может только она.
git checkout -- docs/images
git clean -fdq -- docs/images
args=(--reviewed "--from=$cand")
if [ -n "$EXPECT" ]; then args+=("--expect-change=$EXPECT"); fi
node scripts/docs-accept.mjs "${args[@]}"
if git diff --quiet -- docs/images; then changed=false; else changed=true; fi
echo "changed=$changed" >> "$GITHUB_OUTPUT"
git diff --stat -- docs/images
- name: Сохранить кандидата скриншотов для просмотра
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docs-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/docs-candidate
if-no-files-found: ignore
retention-days: 7
- name: Эталоны golden из прогона Validate
id: golden
if: inputs.golden_run != ''
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ inputs.golden_run }}
EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
EXPECT_NEW: ${{ inputs.golden_expect_new }}
run: |
set -euo pipefail
case "$RUN" in ''|*[!0-9]*) echo "::error::golden_run — числовой id прогона Validate"; exit 1 ;; esac
meta=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN" \
--jq '[.path, .head_branch, .status, .html_url] | @tsv')
IFS=$'\t' read -r path branch status url <<< "$meta"
# Провенанс эталонов — завершённый Validate на dev: другой workflow
# или ветка задачи доказательством для кандидата беты не являются.
if [ "$path" != ".github/workflows/validate.yml" ] || [ "$branch" != "dev" ] || [ "$status" != "completed" ]; then
echo "::error::прогон $RUN — не завершённый Validate на dev ($path, $branch, $status)"
exit 1
fi
from="$RUNNER_TEMP/golden-candidate"
rm -rf "$from" && mkdir -p "$from"
gh run download "$RUN" --repo "${{ github.repository }}" -n golden-images -D "$from"
# Отчёт обязан быть снят с этого же дерева: accept.mjs сверяет его
# отпечаток с исходниками и откажет, если dev ушёл вперёд.
args=(--reviewed "--from=$from")
if [ -n "$EXPECT_CHANGE" ]; then args+=("--expect-change=$EXPECT_CHANGE"); fi
if [ -n "$EXPECT_NEW" ]; then args+=("--expect-new=$EXPECT_NEW"); fi
node scripts/golden-accept.mjs "${args[@]}"
if git diff --quiet -- demo/golden/baselines; then changed=false; else changed=true; fi
{ echo "changed=$changed"; echo "url=$url"; } >> "$GITHUB_OUTPUT"
git diff --stat -- demo/golden/baselines
- name: Коммит в dev
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
TAG: ${{ inputs.tag }}
DOCS_CHANGED: ${{ steps.docs.outputs.changed }}
DOCS_EXPECT: ${{ inputs.docs_expect_change }}
GOLDEN_CHANGED: ${{ steps.golden.outputs.changed }}
GOLDEN_URL: ${{ steps.golden.outputs.url }}
GOLDEN_EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
GOLDEN_EXPECT_NEW: ${{ inputs.golden_expect_new }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Хук pre-push гоняет process-gate; gate:small здесь не нужен —
# продуктового кода коммит не несёт.
HP_PREPUSH_GATE: "0"
run: |
set -euo pipefail
git add -- docs/images demo/golden/baselines
if git diff --cached --quiet; then
echo "::notice::отпечаток, кадры и эталоны на dev уже свежие — коммитить нечего"
exit 0
fi
msg="$RUNNER_TEMP/message.txt"
{
echo "docs: accept derived artifacts on dev for $TAG"
echo ""
echo "Производные артефакты беты — одним коммитом на dev (PROCESS.md §8, §11.4, #697)."
echo "Прогон: $RUN_URL"
if [ "$DOCS_CHANGED" = "true" ]; then
echo "Скриншоты документации: отпечаток исходников; изменённые кадры: ${DOCS_EXPECT:-нет}."
fi
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Golden: изменённые сцены: ${GOLDEN_EXPECT_CHANGE:-нет}; новые: ${GOLDEN_EXPECT_NEW:-нет}."
fi
echo ""
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Release: $TAG"
echo "Baseline-Reviewed: $GOLDEN_URL"
fi
echo "Issue: #697"
echo "User-Visible: no"
} > "$msg"
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F "$msg"
git show --stat --format='%H %s' HEAD | head -40
# #730: сдвигом dev считается только устаревший lease — отказ
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705).
# Скрипт — из dev: рабочая копия и есть dev. Сдвиг — прежний совет
# перезапустить; отказ GitHub (право на workflow, правило ветки, хук)
# перезапуск не лечит: причина и ответ git без токена — в журнале и в
# сводке шага.
push_err="$RUNNER_TEMP/beta-derived-push.stderr"
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
--stage=beta-derived --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
if [ "$kind" = "stale" ]; then
echo "::error::dev ушёл вперёд за время съёмки — запустить workflow заново: отпечаток судит дерево, а оно уже другое"
else
echo "::error::push производных артефактов в dev отклонён ($kind) — это не сдвиг dev, перезапуск не поможет; причина и ответ git — выше и в сводке шага"
fi
exit 1
fi
echo "### Производные артефакты $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Коммит \`$(git rev-parse --short HEAD)\` в dev — проверить перед кандидатом беты." >> "$GITHUB_STEP_SUMMARY"
+118 -2
View File
@@ -12,12 +12,19 @@
#
# Красный ночной прогон — сигнал автору последних коммитов на dev, не гейт:
# гейт беты по-прежнему требует зелёный Validate на точном SHA кандидата, и
# там полный набор идёт заново.
# там полный набор идёт заново. Адресат сигнала (#736) — задачи, чьи коммиты
# классов A/B вошли в dev после последней зелёной ночи: job `night_red` пишет
# каждой один комментарий (`scripts/night-red.mjs`, PROCESS.md §10.4).
#
# Сигнал обязан быть настоящим (#492 §7): до этой задачи job завершалась
# успехом в момент постановки Validate в очередь, и красный полный прогон не
# делал ночной workflow красным. Теперь job находит запущенный прогон и ждёт
# его: успешный dispatch — не успешная проверка.
#
# #727: после Validate при любом его исходе ночь запускает пакетное ревью ship
# (`ship-review.yml -f tag=nightly`) на SHA прогона Validate: оно читает код, а
# не прогон. Ship-задача узнаёт о дефектах в ночь после слияния, а бета читает
# только то, что ночь не прочла (PROCESS.md §11.7). Цвет ночи — цвет Validate.
name: "Ночной полный прогон dev · тело (#623)"
on:
@@ -34,6 +41,11 @@ jobs:
name: "Запустить Validate на dev с полным набором и дождаться результата"
runs-on: ubuntu-24.04
timeout-minutes: 90
outputs:
# #727: SHA прогона Validate — отдельным шагом до ожидания, красный Validate его не теряет.
head_sha: ${{ steps.validate.outputs.head_sha }}
# #736: прогон Validate для разбора красной ночи — тем же шагом, до ожидания.
run_id: ${{ steps.validate.outputs.run_id }}
steps:
# #658: при плане 02:30 UTC ночь фактически стартовала в 07:42–08:05 и
# кончалась в рабочее время владельца. Сдвиг старта больше часа — видимое
@@ -55,7 +67,9 @@ jobs:
if [ "$lag" -gt 60 ]; then
echo "::warning::ночной прогон стартовал на $lag мин позже расписания ($SCHEDULE UTC) — очередь расписаний GitHub (#658)"
fi
- env:
- name: "Запустить Validate и найти его прогон"
id: validate
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
@@ -80,5 +94,107 @@ jobs:
url="${{ github.server_url }}/$REPO/actions/runs/$run_id"
echo "дочерний прогон: $url"
echo "- Validate(dev, full=true): $url" >> "$GITHUB_STEP_SUMMARY"
head_sha=$(gh run view "$run_id" --repo "$REPO" --json headSha --jq .headSha)
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- name: "Дождаться Validate"
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ steps.validate.outputs.run_id }}
run: |
set -euo pipefail
run_id="$RUN_ID"
# Ждём завершения; красный дочерний прогон — красный ночной.
gh run watch "$run_id" --repo "$REPO" --exit-status --interval 30
# #727 К6: ночное пакетное ревью ship на SHA прогона Validate — при любом его
# исходе. Dispatch тонкого `ship-review.yml` (`github.token` порождает прогон,
# как у Validate; входы и права тонких файлов уже в main). Job ждёт только
# появления прогона, не его конца; не появился — предупреждение, не красная
# ночь: цвет ночи — цвет Validate.
ship_review:
name: "Запустить ночное пакетное ревью ship на SHA прогона Validate"
needs: dispatch
if: always()
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: "Запустить ship-ревью и дождаться появления прогона"
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
CANDIDATE: ${{ needs.dispatch.outputs.head_sha }}
run: |
set -euo pipefail
if [ -z "$CANDIDATE" ]; then
echo "::warning::нет SHA прогона Validate — ночное ship-ревью не запущено"
echo "- Ship-ревью ночью: не запущено — нет SHA прогона Validate" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
since=$(date -u +%FT%TZ)
if ! gh workflow run ship-review.yml --ref dev -f tag=nightly -f candidate="$CANDIDATE" --repo "$REPO"; then
echo "::warning::ship-ревью ночью не запущено: dispatch отклонён — ночь не красится"
echo "- Ship-ревью ночью: dispatch отклонён" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
run_id=""
for _ in $(seq 1 18); do
sleep 10
run_id=$(gh run list --repo "$REPO" --workflow ship-review.yml --branch dev \
--event workflow_dispatch --json databaseId,createdAt --limit 5 \
--jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty")
[ -n "$run_id" ] && break
done
if [ -z "$run_id" ]; then
echo "::warning::прогон ship-ревью не появился за 3 минуты — ночь не красится, её цвет — цвет Validate"
echo "- Ship-ревью ночью (\`$CANDIDATE\`): прогон не появился за 3 минуты" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "- Ship-ревью ночью (\`$CANDIDATE\`): ${{ github.server_url }}/$REPO/actions/runs/$run_id" >> "$GITHUB_STEP_SUMMARY"
# #736: красная ночь — один комментарий в каждую задачу, чьи коммиты классов
# A/B вошли в dev после последней зелёной ночи (`scripts/night-red.mjs`,
# PROCESS.md §10.4). Только при красном dispatch с известным прогоном;
# исход прогона (failure, а не cancelled/timed_out) скрипт читает сам.
# Actions — `github.token`, issue — `HP_PROCESS_TOKEN`: `issues: write` у
# ночи нет, и потолок тонкого `nightly.yml` в main не меняется. Сбой —
# предупреждение, не красная ночь сверх Validate.
night_red:
name: "Красная ночь: комментарий в задачи, слитые после последней зелёной"
needs: dispatch
if: always() && needs.dispatch.result == 'failure' && needs.dispatch.outputs.run_id != ''
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
actions: read
contents: read
steps:
# Нужны коммиты и деревья, не блобы; скрипт и его импорты — без зависимостей, npm ci не нужен.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 0
filter: blob:none
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: "Комментарий в задачи диапазона от последней зелёной ночи до красной"
env:
ACTIONS_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
REPO: ${{ github.repository }}
RED_RUN: ${{ needs.dispatch.outputs.run_id }}
run: |
set -euo pipefail
# Сбои API, токена и git скрипт сам сводит к предупреждению; упавший
# скрипт — тоже предупреждение: цвет ночи — цвет Validate.
if ! node scripts/night-red.mjs --repo="$REPO" --red-run="$RED_RUN"; then
echo "::warning::красная ночь (#736): скрипт упал — комментарии не написаны; цвет ночи — цвет Validate"
echo "- Красная ночь (#736): скрипт упал — комментарии не написаны" >> "$GITHUB_STEP_SUMMARY"
fi
+10 -3
View File
@@ -5,6 +5,12 @@ name: "Метрики процесса · тело (#623)"
# идёт в step summary и артефакт; в issue и репозиторий ничего не пишется.
# Цифры аудита 22.09 (1,57 раунда код-ревью, S7 ≈ 36 мин/раунд, 226 Validate
# за неделю) были собраны руками за час — теперь они стоят один запуск.
#
# #728: эффект процесса по трекам — отрезки, возвраты с причинами, находки
# ship, job-минуты по стадиям и сравнение до/после 28.09. Прогон дороже:
# jobs до 600 прогонов и таймлайны задач окна сравнения (≈ 600–800 запросов),
# поэтому потолок 30 минут; трек инфраструктуры и объём задачи читаются из
# истории git — нужна полная история dev.
on:
# #623: тело вызывается тонким файлом `process-metrics.yml` из ветки по умолчанию
@@ -26,14 +32,15 @@ jobs:
metrics:
name: "Снимок недели: issue, раунды, прогоны"
runs-on: ubuntu-24.04
timeout-minutes: 15
timeout-minutes: 30
steps:
# Код — из dev, как у reconcile: расписание читается из main, а исполняется
# версия, которую проверил CI.
# версия, которую проверил CI. Полная история (#728): коммиты задачи по
# трейлеру `Issue: #NN` дают признак инфраструктуры и объём для сравнения.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 1
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
+6 -1
View File
@@ -54,7 +54,12 @@ jobs:
SHA: ${{ github.event.workflow_run.head_sha }}
EVENT: ${{ github.event.workflow_run.event }}
STATUS: ${{ github.event.workflow_run.status }}
VALIDATE_RUN_ID: ${{ github.event.workflow_run.id }}
# #751: без pipefail код конвейера — код tee, и исключение скрипта (gh,
# API) проходило зелёным шагом: событие возобновления терялось до
# прохода process-reconcile.
run: |
set -o pipefail
node scripts/process-resume.mjs \
--repo="$REPO" --branch="$BRANCH" --sha="$SHA" \
--event="$EVENT" --status="$STATUS" --apply=true | tee -a "$GITHUB_STEP_SUMMARY"
--event="$EVENT" --status="$STATUS" --run-id="$VALIDATE_RUN_ID" --apply=true | tee -a "$GITHUB_STEP_SUMMARY"
File diff suppressed because it is too large Load Diff
+415
View File
@@ -0,0 +1,415 @@
name: "Бета: пакетное ревью ship · тело (#716)"
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
# правка в механических рамках и зелёный лёгкий Validate. Их код читает одна
# сессия модели перед бетой: все ship-задачи диапазона «прошлый тег..кандидат».
# Гейт публикации беты (`ship-review.mjs check`) требует этот документ, если в
# диапазоне есть ship-задачи.
#
# Три job, как у release-review.yml (#551, #556): детерминированный вход,
# модель без права записи, детерминированная публикация документа в `dev`.
#
# #727: `tag=nightly` — ночной режим. Его запускает `_nightly.yml` после
# полного Validate с `candidate` = голова `dev` того прогона. Читаются только
# ship-задачи без покрытия или изменившиеся после ревью (`shipCoverage`), документ —
# `SHIP-REVIEW-<база>-dev-<sha12>.md`. Бета так же читает только дельту.
on:
# #716: тело вызывается тонким файлом `ship-review.yml` по ссылке `@dev`;
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
workflow_call:
inputs:
# #727: `nightly` — зарезервированное значение (ночной режим, candidate обязателен).
tag:
description: "Beta tag the review is for, for example v1.79.0-beta.1"
required: true
type: string
candidate:
description: "Exact candidate SHA; empty = the dev tip"
required: false
type: string
default: ""
force:
description: "Review again even when the document already exists in dev"
required: false
type: boolean
default: false
permissions:
contents: read
# Concurrency уровня workflow — у вызывающего `ship-review.yml` (#716).
jobs:
prepare:
name: "Ship-ревью: задачи диапазона"
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
issues: read
outputs:
proceed: ${{ steps.range.outputs.proceed }}
candidate: ${{ steps.range.outputs.candidate }}
base: ${{ steps.range.outputs.base }}
doc: ${{ steps.range.outputs.doc }}
issues: ${{ steps.range.outputs.issues }}
mode: ${{ steps.range.outputs.mode }}
patches: ${{ steps.range.outputs.patches }}
subject: ${{ steps.range.outputs.subject }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
ref: dev
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Кандидат, база и ship-задачи
id: range
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
CANDIDATE: ${{ inputs.candidate }}
FORCE: ${{ inputs.force }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
# Отказ prepare за `| tee` не должен читаться как «ship-задач нет» (#727).
set -o pipefail
git fetch -q --tags origin
# #727: tag=nightly без candidate — отказ до подстановки вершины dev.
node scripts/ship-review.mjs mode --tag="$TAG" --candidate="$CANDIDATE"
if [ -z "$CANDIDATE" ]; then CANDIDATE=$(git rev-parse origin/dev); fi
git cat-file -e "$CANDIDATE^{commit}"
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
doc=$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")
if [ "$FORCE" != "true" ] && git cat-file -e "origin/dev:$doc" 2>/dev/null; then
echo "::notice::$doc уже есть в dev — повторное ревью не запускается (force=true, чтобы переснять)"
echo "proceed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
out="$RUNNER_TEMP/ship-review-input"
node scripts/ship-review.mjs prepare --tag="$TAG" --candidate="$CANDIDATE" --force="$FORCE" \
--out="$out" --repo="${{ github.repository }}" --run-url="$RUN_URL" | tee "$RUNNER_TEMP/range.txt"
cat "$RUNNER_TEMP/range.txt" >> "$GITHUB_OUTPUT"
echo "candidate=$CANDIDATE" >> "$GITHUB_OUTPUT"
issues=$(sed -n 's/^issues=//p' "$RUNNER_TEMP/range.txt")
if [ -z "$issues" ]; then
# #727 К5: пустая дельта — все ship-задачи покрыты документами базы.
note=$(sed -n 's/^note=//p' "$RUNNER_TEMP/range.txt")
echo "::notice::${note:-ship-задач в диапазоне нет — ревью не нужно}"
if [ -n "$note" ]; then echo "- $note" >> "$GITHUB_STEP_SUMMARY"; fi
echo "proceed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "proceed=true" >> "$GITHUB_OUTPUT"
(cd "$out" && sha256sum brief.md ship-issues.json > manifest.sha256)
cat "$out/brief.md" >> "$GITHUB_STEP_SUMMARY"
- name: Передать вход модели
if: steps.range.outputs.proceed == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-input
if-no-files-found: error
retention-days: 3
model_review:
name: "Ship-ревью: работа модели"
needs: prepare
if: needs.prepare.outputs.proceed == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
# #737: строка расхода сессии модели (scripts/model-usage.mjs).
outputs:
usage: ${{ steps.usage.outputs.line }}
# Недоверенная стадия без права записи (#556): документ публикует
# `publish`. `github_token` у шага Review обязателен — без него action
# меняет OIDC на собственный App-токен с правом записи.
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.candidate }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- name: Получить вход
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-input
- name: Проверить вход и кандидата
env:
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
run: |
(cd "$RUNNER_TEMP/ship-review-input" && sha256sum -c manifest.sha256)
test "$(git rev-parse HEAD)" = "$CANDIDATE"
- name: Установить зависимости
run: npm ci
- name: Кэш браузеров Playwright
id: pw
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Установить Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install chromium
# Тот же обход, что у конвейера (_process.yml, anthropics issue 1817).
- name: Установить Claude Code детерминированно
id: claude_bin
run: |
src=$(ls "$RUNNER_WORKSPACE"/../_actions/anthropics/claude-code-*/v1/src/entrypoints/run.ts 2>/dev/null | head -1)
ver=$(grep -oE 'claudeCodeVersion = "[0-9]+\.[0-9]+\.[0-9]+"' "$src" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
ver="${ver:-2.1.265}"
base=https://downloads.claude.ai/claude-code-releases
bin="$HOME/.local/bin/claude"
mkdir -p "$(dirname "$bin")"
curl -fsSL --retry 3 "$base/$ver/linux-x64/claude" -o "$bin"
sum=$(curl -fsSL --retry 3 "$base/$ver/manifest.json" | jq -r '.platforms["linux-x64"].checksum')
echo "$sum $bin" | sha256sum -c -
chmod +x "$bin"
"$bin" --version
echo "path=$bin" >> "$GITHUB_OUTPUT"
- name: Review
id: review
uses: anthropics/claude-code-action@9cdae7f0d995e3ba7c33f226087fdf82a59cd520 # v1
env:
REVIEW_DOC: ${{ runner.temp }}/ship-review.md
REVIEW_INPUT: ${{ runner.temp }}/ship-review-input
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }}
prompt: |
Ты ревьюер кода проекта House Plan. Язык ответа — русский.
${{ needs.prepare.outputs.subject }}
Кандидат ${{ needs.prepare.outputs.candidate }} (рабочая копия уже на нём) · база
диапазона: ${{ needs.prepare.outputs.base || 'нет' }}.
Задачи: ${{ needs.prepare.outputs.issues }} — вход в $REVIEW_INPUT/brief.md
(строка ТЗ каждой задачи и её коммиты) и $REVIEW_INPUT/ship-issues.json.
Эти задачи шли по треку ship и слиты в dev без ревью модели: правка
в механических рамках и зелёный лёгкий Validate. Ты — первый, кто
читает их код. Правила — docs/process/REVIEWER.md, раздел
«Пакетное ревью ship», канон — PROCESS.md §5 и §11.7. Прочитай их
первыми.
По каждой задаче: `git show <sha>` каждого коммита; делает ли код
то, что сказано в строке ТЗ, и только это; не ломает ли соседнее
поведение; не вышла ли правка из трека ship по смыслу (новый
UX-контракт, геометрия, конфиг, перф) — это находка Medium с
предложением трека. Исполняй, где дёшево: `npm test`, смоук,
который называет строка ТЗ или выбирает
`node scripts/smoke-select.mjs --base <sha>^ --head <sha>`.
Серьёзность: High — дефект, который увидит пользователь беты, или
правка не делает заявленного; High блокирует публикацию беты.
Medium — поведение вне заявленного или вне рамок трека; Low —
остальное. Находка чинится отдельной задачей, ты ничего не правишь.
Ты ничего не правишь и не публикуешь: ни код, ни issue, ни
комментарии. Права на запись у тебя нет. Любые изменения рабочей
копии будут отброшены — после проверок восстанови её сам
(`git checkout -- . && git clean -fd`), если что-то менял.
Напиши документ в файл по пути из переменной REVIEW_DOC (абсолютный,
вне репозитория). Первой строкой после заголовка —
`Итог: High N · Medium N · Low N`. Затем по задаче на раздел
`## #NN — заголовок`: что проверил (команда → результат), находки с
воспроизведением, чего не проверял.
Затем верни JSON по схеме — последнее обязательное действие.
claude_args: |
--max-turns 150
--allowedTools Read,Write,Grep,Glob,Bash
--json-schema '{"type":"object","properties":{"high":{"type":"integer"},"medium":{"type":"integer"},"low":{"type":"integer"},"summary":{"type":"string"}},"required":["high","medium","low","summary"]}'
# #737: как у конвейера (_process.yml) — расход выходом job, не через
# artifact; execution_file не выгружается и не печатается.
- name: Снять расход модели
id: usage
if: always()
continue-on-error: true
env:
EXEC: ${{ steps.review.outputs.execution_file }}
run: |
line=$(node scripts/model-usage.mjs --execution-file="$EXEC")
echo "line=$line" >> "$GITHUB_OUTPUT"
echo "- расход модели: \`$line\`" >> "$GITHUB_STEP_SUMMARY"
- name: Запечатать результат модели
env:
SOURCE: ${{ runner.temp }}/ship-review.md
OUT: ${{ steps.review.outputs.structured_output }}
run: |
test -s "$SOURCE" || { echo "::error::модель не оставила документ ревью"; exit 1; }
dir="$RUNNER_TEMP/ship-review-result"
mkdir -p "$dir"
printf '%s' "$OUT" > "$dir/result.json"
jq -e '(.high|type=="number") and (.medium|type=="number") and (.low|type=="number") and (.summary|type=="string")' \
"$dir/result.json" >/dev/null
cp "$SOURCE" "$dir/ship-review.md"
(cd "$dir" && sha256sum ship-review.md result.json > manifest.sha256)
- name: Передать результат публикации
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-result
if-no-files-found: error
retention-days: 3
publish:
name: "Ship-ревью: документ в dev"
needs: [prepare, model_review]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
ref: dev
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Получить результат модели
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-result
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
# индекс и коммит решает этот шаг. #727: `mode` и `patches` блока — из
# prepare (детерминированный патч-набор), не из результата модели.
- name: Опубликовать документ
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
TAG: ${{ inputs.tag }}
DOC: ${{ needs.prepare.outputs.doc }}
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
BASE: ${{ needs.prepare.outputs.base }}
ISSUES: ${{ needs.prepare.outputs.issues }}
MODE: ${{ needs.prepare.outputs.mode }}
PATCHES: ${{ needs.prepare.outputs.patches }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# #737: расход модели — строкой сразу после блока; недоверенный ввод.
USAGE: ${{ needs.model_review.outputs.usage }}
run: |
dir="$RUNNER_TEMP/ship-review-result"
(cd "$dir" && sha256sum -c manifest.sha256)
test "$DOC" = "$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")"
high=$(jq -r '.high' "$dir/result.json")
medium=$(jq -r '.medium' "$dir/result.json")
low=$(jq -r '.low' "$dir/result.json")
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#730, как #723).
# #748: ночью — своё: имя документа без префикса, задача ночного режима.
if [ "$MODE" = "nightly" ]; then
night=${DOC##*/SHIP-REVIEW-}
subject="docs: nightly ship review ${night%.md}"
lead="Ночное пакетное ревью задач track:ship (PROCESS.md §11.7)."
issue="#727"
else
subject="docs: ship review for $TAG"
lead="Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7)."
issue="#696"
fi
msg="$RUNNER_TEMP/ship-review-commit.txt"
push_err="$RUNNER_TEMP/ship-review-push.stderr"
for attempt in 1 2 3; do
git fetch -q origin dev
git reset -q --hard origin/dev
git clean -fdq
mkdir -p docs/reviews
{
cat "$dir/ship-review.md"
printf '\n\n'
node -e '
import("./scripts/ship-review.mjs").then((m) => process.stdout.write(m.anchorBlock({
tag: process.env.TAG, candidate: process.env.CANDIDATE, base: process.env.BASE || null,
issues: String(process.env.ISSUES || "").split(",").filter(Boolean).map(Number),
high: Number(process.argv[1]), medium: Number(process.argv[2]), low: Number(process.argv[3]),
runUrl: process.env.RUN_URL, mode: process.env.MODE, patches: m.parsePatches(process.env.PATCHES),
usage: process.env.USAGE ?? "",
})));
' "$high" "$medium" "$low"
} > "$DOC"
node scripts/reviews-index.mjs --dir=docs/reviews --strict
git add -- "$DOC" docs/reviews/INDEX.md
git diff --cached --name-only | node scripts/review-doc-guard.mjs
{
echo "$subject"
echo ""
echo "$lead"
echo "Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low."
echo ""
echo "Issue: $issue"
echo "User-Visible: no"
} > "$msg"
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F "$msg"
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
echo "### Пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Задачи $ISSUES · High $high · Medium $medium · Low $low — \`$DOC\` в dev." >> "$GITHUB_STEP_SUMMARY"
echo "::notice::$DOC опубликован: High $high · Medium $medium · Low $low"
exit 0
fi
# #730: сдвигом dev считается только устаревший lease — отказ
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705),
# как у ревью релиза (#723). Скрипт — из dev: рабочая копия только что
# сброшена на origin/dev. Отказ GitHub (право на workflow, правило
# ветки, хук) повтор не лечит: шаг останавливается, причина и ответ
# git без токена — в журнале и в сводке шага.
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
--stage=ship-review --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
if [ "$kind" != "stale" ]; then
echo "::error::push $DOC в dev отклонён ($kind) — это не сдвиг dev, повтор не поможет; причина и ответ git — выше и в сводке шага"
exit 1
fi
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
sleep $((attempt * 10))
done
echo "::error::документ ревью не опубликован в dev за три попытки"
exit 1
# #727 К8: High ночного документа — одна строка в каждую его задачу.
# Повтор на тот же документ не пишется (метка hp:ship-review-high doc=…),
# Medium и Low в issue не идут — их решает владелец (§11.7). Режим и
# High проверяет и скрипт: шаг вне ночи или без High ничего не пишет.
- name: High ночью — строка в задачи документа
if: needs.prepare.outputs.mode == 'nightly'
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
MODE: ${{ needs.prepare.outputs.mode }}
DOC: ${{ needs.prepare.outputs.doc }}
ISSUES: ${{ needs.prepare.outputs.issues }}
run: |
high=$(jq -r '.high' "$RUNNER_TEMP/ship-review-result/result.json")
node scripts/ship-review.mjs comment-high --mode="$MODE" --high="$high" --doc="$DOC" \
--issues="$ISSUES" --repo="${{ github.repository }}"
+65
View File
@@ -0,0 +1,65 @@
name: "Бета: производные артефакты на dev"
run-name: "Derived artifacts ${{ inputs.tag }}"
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
# run-name, права и concurrency. Тело — `_beta-derived.yml` по ссылке `@dev`:
# правка съёмки и приёмки — один коммит в `dev`, а запуск с любой ветки (в
# кнопке по умолчанию выбрана `main`) исполняет тело из `dev`.
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
# равными.
on:
workflow_dispatch:
inputs:
tag:
description: "Beta tag these artifacts are for, for example v1.79.0-beta.1"
required: true
type: string
docs_expect_change:
description: "Doc scenario ids expected to change, comma-separated (empty = none)"
required: false
type: string
default: ""
golden_run:
description: "Validate run id on dev whose golden-images artifact holds the shifted frames (empty = skip golden)"
required: false
type: string
default: ""
golden_expect_change:
description: "Golden scenes expected to change, comma-separated"
required: false
type: string
default: ""
golden_expect_new:
description: "New golden scenes, comma-separated"
required: false
type: string
default: ""
permissions:
contents: read
concurrency:
group: beta-derived
cancel-in-progress: false
jobs:
# Потолок прав тела: объединение job-level прав `_beta-derived.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
contents: read
actions: read
uses: Matysh/houseplan-card/.github/workflows/_beta-derived.yml@dev # #716: тело съёмки и приёмки из dev
with:
tag: ${{ inputs.tag }}
docs_expect_change: ${{ inputs.docs_expect_change }}
golden_run: ${{ inputs.golden_run }}
golden_expect_change: ${{ inputs.golden_expect_change }}
golden_expect_new: ${{ inputs.golden_expect_new }}
secrets: inherit
+20
View File
@@ -40,6 +40,7 @@ jobs:
profile:
- large-house
- isometric
- isometric-backdrop
- isometric-stage3
- plan-snap
- interaction
@@ -47,6 +48,7 @@ jobs:
- overlay
- space-default
- space-glow
- led-strips
steps:
- name: Check out candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -134,6 +136,10 @@ jobs:
npm run benchmark:large-house-isometric -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-baseline.json
npm run benchmark:large-house-isometric -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-candidate.json
;;
isometric-backdrop)
npm run benchmark:large-house-isometric-backdrop -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-baseline.json
npm run benchmark:large-house-isometric-backdrop -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-candidate.json
;;
isometric-stage3)
npm run benchmark:isometric-stage3-dense -- --allow-stage2-base --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-baseline.json
npm run benchmark:isometric-stage3-dense -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-candidate.json
@@ -170,6 +176,14 @@ jobs:
cp ../artifacts/performance/space-glow-candidate.json ../artifacts/performance/space-glow-baseline.json
fi
;;
led-strips)
# #780 led-strips-v1: base predates LED strips, so the profile is
# judged by its absolute limits inside the runner; the zero-LED
# View stays covered by the relative interaction profile above.
npm run benchmark:led-strips -- --size=10x5 --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-10x5.json
npm run benchmark:led-strips -- --size=50x50 --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-50x50.json
npm run benchmark:led-strips -- --size=none --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-none.json
;;
*)
echo "::error::Unknown performance profile: $PROFILE"
exit 1
@@ -188,6 +202,9 @@ jobs:
isometric)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric.json --baseline=../artifacts/performance/isometric-baseline.json --candidate=../artifacts/performance/isometric-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-comparison.json
;;
isometric-backdrop)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric-backdrop.json --baseline=../artifacts/performance/isometric-backdrop-baseline.json --candidate=../artifacts/performance/isometric-backdrop-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-backdrop-comparison.json
;;
isometric-stage3)
npm run benchmark:compare -- --budgets=demo/performance/budgets-isometric-stage3-dense.json --baseline=../artifacts/performance/isometric-stage3-baseline.json --candidate=../artifacts/performance/isometric-stage3-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-stage3-comparison.json
;;
@@ -209,6 +226,9 @@ jobs:
space-glow)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-space-card-glow.json --baseline=../artifacts/performance/space-glow-baseline.json --candidate=../artifacts/performance/space-glow-candidate.json --output=../artifacts/performance/space-glow-comparison.json
;;
led-strips)
echo "led-strips-v1: absolute limits and counters already enforced by the runner (budgets-led-strips.json)"
;;
esac
- name: Upload full performance report
+8
View File
@@ -91,6 +91,14 @@ jobs:
REPO: ${{ github.repository }}
SHA: ${{ steps.candidate.outputs.sha }}
run: node scripts/release-gate.mjs "$SHA"
# #696, PROCESS.md §11.7: задачи track:ship слиты без ревью модели — бета
# публикуется, только когда их код прочитало пакетное ревью диапазона.
- name: Ship tasks of the range are batch-reviewed
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.candidate.outputs.tag }}
SHA: ${{ steps.candidate.outputs.sha }}
run: node scripts/ship-review.mjs check --tag="$TAG" --candidate="$SHA" --repo="$GITHUB_REPOSITORY"
- name: Bind issue membership to the exact candidate
env:
GH_TOKEN: ${{ github.token }}
+36 -10
View File
@@ -75,6 +75,9 @@ jobs:
FORCE: ${{ inputs.force }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
# Отказ prepare за `| tee` не должен идти дальше с неполным
# GITHUB_OUTPUT и proceed=true (#751).
set -o pipefail
doc=$(node scripts/release-review.mjs doc --tag="$TAG")
git fetch -q --tags origin
if [ -z "$CANDIDATE" ]; then
@@ -183,6 +186,14 @@ jobs:
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# #704: `release.yml` ставит ревью в очередь токеном GITHUB_TOKEN, и
# прогон начинает `github-actions[bot]` (тип Bot). Action отказывает
# ботам, которых нет в списке («Workflow initiated by non-human actor:
# github-actions (type: Bot)», v1.78.0). Разрешён ровно этот бот —
# не '*': любой другой бот по-прежнему отклоняется. Action сравнивает
# имена без регистра и без суффикса `[bot]`. Человек, запустивший
# ревью руками, списка не касается.
allowed_bots: "github-actions[bot]"
path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }}
prompt: |
Ты независимый ревьюер релиза House Plan. Язык ответа — русский.
@@ -295,6 +306,9 @@ jobs:
(cd "$dir" && sha256sum -c manifest.sha256)
test "$DOC" = "$(node scripts/release-review.mjs doc --tag="$TAG")"
counts=$(jq -r '"High \(.high) · Medium \(.medium) · Low \(.low)"' "$dir/result.json")
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#723).
msg="$RUNNER_TEMP/release-review-commit.txt"
push_err="$RUNNER_TEMP/release-review-push.stderr"
for attempt in 1 2 3; do
git fetch -q origin dev
git reset -q --hard origin/dev
@@ -310,24 +324,36 @@ jobs:
node scripts/reviews-index.mjs --dir=docs/reviews --strict
git add -- "$DOC" docs/reviews/INDEX.md
git diff --cached --name-only | node scripts/review-doc-guard.mjs
{
echo "docs: release review for $TAG"
echo ""
echo "Независимое ревью линии перед стабильным релизом (PROCESS.md §11.5)."
echo "Итог: $counts. Выпуск не блокирует; решение по находкам — за владельцем."
echo ""
echo "Issue: #638"
echo "User-Visible: no"
} > "$msg"
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F - <<MSG
docs: release review for $TAG
Независимое ревью линии перед стабильным релизом (PROCESS.md §11.5).
Итог: $counts. Выпуск не блокирует; решение по находкам — за владельцем.
Issue: #638
User-Visible: no
MSG
commit -q -F "$msg"
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev; then
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
echo "### Независимое ревью $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Итог: $counts — \`$DOC\` в dev. Выпуск не блокируется." >> "$GITHUB_STEP_SUMMARY"
echo "::notice::$DOC опубликован: $counts"
exit 0
fi
# #723: сдвигом dev считается только устаревший lease — отказ
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705).
# Отказ GitHub (право на workflow, правило ветки, хук) повтор не
# лечит: шаг останавливается, причина и ответ git без токена — в
# журнале и в сводке шага.
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
--stage=release-review --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
if [ "$kind" != "stale" ]; then
echo "::error::push $DOC в dev отклонён ($kind) — это не сдвиг dev, повтор не поможет; причина и ответ git — выше и в сводке шага"
exit 1
fi
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
sleep $((attempt * 10))
done
+49 -3
View File
@@ -153,14 +153,60 @@ jobs:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.candidate.outputs.tag }}
SHA: ${{ needs.candidate.outputs.sha }}
# #704: сколько ждать, что dispatch стал прогоном, — выпуск не ждёт
# дольше нескольких минут; укладывается в timeout-minutes job.
APPEAR_SECONDS: 180
POLL_SECONDS: 15
run: |
if gh workflow run release-review.yml --repo "${{ github.repository }}" --ref dev \
repo="${{ github.repository }}"
# Отсчёт окна — до dispatch, с запасом минута на расхождение часов.
since=$(( $(date -u +%s) - 60 ))
if ! gh workflow run release-review.yml --repo "${{ github.repository }}" --ref dev \
-f tag="$TAG" -f candidate="$SHA"; then
echo "Независимое ревью $TAG поставлено в очередь: release-review.yml на dev" >> "$GITHUB_STEP_SUMMARY"
else
echo "::warning::ревью линии $TAG не запущено — выпуск продолжается; запустить руками: gh workflow run release-review.yml --ref dev -f tag=$TAG -f candidate=$SHA"
echo "Независимое ревью $TAG: **не запущено** — dispatch release-review.yml отклонён." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
# #704: dispatch не возвращает прогона, и v1.78.0 показал цену: ревью
# упало в прогоне, о котором выпуск не знал. Прогон ищется по workflow,
# ветке dev, событию, времени и run-name «Release review <тег>».
# Найден и стартовал — ссылка и статус в сводке; нет — предупреждение,
# выпуск не блокируется.
title="Release review $TAG"
run=""
started=false
tries=$(( APPEAR_SECONDS / POLL_SECONDS ))
for try in $(seq 1 "$tries"); do
found=$(gh run list --repo "$repo" --workflow release-review.yml --branch dev \
--event workflow_dispatch --limit 20 \
--json databaseId,url,status,conclusion,displayTitle,createdAt \
| jq -c --arg title "$title" --argjson since "$since" \
'[.[] | select(.displayTitle == $title and (.createdAt | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) >= $since)] | sort_by(.createdAt) | last // empty') || found=""
if [ -n "$found" ]; then
run="$found"
case "$(jq -r .status <<< "$run")" in
in_progress|completed) started=true; break ;;
esac
fi
if [ "$try" -lt "$tries" ]; then sleep "$POLL_SECONDS"; fi
done
minutes=$(( (APPEAR_SECONDS + 59) / 60 ))
if [ -z "$run" ]; then
echo "::warning::прогон ревью линии $TAG не появился за $minutes мин — выпуск продолжается; проверить Actions → release-review.yml"
echo "Независимое ревью $TAG: **не стартовало за $minutes мин** — прогон release-review.yml на dev не найден." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
url=$(jq -r .url <<< "$run")
status=$(jq -r '.status + (if (.conclusion // "") != "" then " · " + .conclusion else "" end)' <<< "$run")
if [ "$started" != "true" ]; then
echo "::warning::ревью линии $TAG в очереди и не стартовало за $minutes мин: $url"
echo "Независимое ревью $TAG: **не стартовало за $minutes мин** (в очереди) — [прогон]($url), статус $status." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$(jq -r '.conclusion // ""' <<< "$run")" = "failure" ]; then
echo "::warning::ревью линии $TAG уже упало: $url"
fi
echo "Независимое ревью $TAG **запущено**: [прогон]($url), статус $status. Выпуск его не ждёт." >> "$GITHUB_STEP_SUMMARY"
gate:
name: "Гейт: контракт, Validate, Full Performance и E2E на точном SHA"
+53
View File
@@ -0,0 +1,53 @@
name: "Бета: пакетное ревью ship"
run-name: "Ship review ${{ inputs.tag }}"
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
# run-name, права и concurrency. Тело — `_ship-review.yml` по ссылке `@dev`:
# правка ревью — один коммит в `dev`, а запуск с любой ветки (в кнопке по
# умолчанию выбрана `main`) исполняет тело из `dev`.
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
# равными.
on:
workflow_dispatch:
inputs:
tag:
description: "Beta tag the review is for, for example v1.79.0-beta.1"
required: true
type: string
candidate:
description: "Exact candidate SHA; empty = the dev tip"
required: false
type: string
default: ""
force:
description: "Review again even when the document already exists in dev"
required: false
type: boolean
default: false
permissions:
contents: read
concurrency:
group: ship-review-${{ inputs.tag }}
cancel-in-progress: false
jobs:
# Потолок прав тела: объединение job-level прав `_ship-review.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
contents: read
issues: read
uses: Matysh/houseplan-card/.github/workflows/_ship-review.yml@dev # #716: тело ревью из dev
with:
tag: ${{ inputs.tag }}
candidate: ${{ inputs.candidate }}
force: ${{ inputs.force }}
secrets: inherit
+116 -24
View File
@@ -21,12 +21,11 @@ on:
description: 'Полный набор: смоки, golden, performance_smoke (без мутантов — #601)'
type: boolean
default: true
# Мутанты по диффу бегут только по явному запросу (#510, #601):
# ревью-конвейер и слияние кандидата запускают `-f full=false
# -f mutants=true` на материале. `full=true`, трейлер `Release:` и ночь
# их не включают — ночью идёт полный реестр (mutation-gate.yml).
# #709: мутанты в разработке не гоняются — вход оставлен, чтобы старые
# вызовы `-f mutants=…` не падали, но job мутантов по диффу он больше не
# включает (classify-changes.mjs). Весь реестр — ночью (mutation-gate.yml).
mutants:
description: 'Мутанты по диффу на этом SHA (кандидат ревью или слияния)'
description: 'Не действует с #709: мутанты — только ночной полный прогон'
type: boolean
default: false
@@ -53,12 +52,13 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 15
# `actions: read` — чтение списка прогонов Validate (#388), `issues: read` —
# проверка 8 процессного гейта. Права перечислены явно, потому что job
# обращается к API сверх содержимого репозитория.
# проверка 8 процессного гейта, `issues: write` — одно issue о расхождении
# зеркала workflow на push в dev (#700). Права перечислены явно, потому что
# job обращается к API сверх содержимого репозитория.
permissions:
contents: read
actions: read
issues: read
issues: write
steps:
# `blob:none` при полной истории (#345): этой job нужны сообщения
# коммитов, трейлеры и ИМЕНА изменённых файлов, а не содержимое старых
@@ -68,8 +68,10 @@ jobs:
# Коммиты и деревья скачиваются целиком, поэтому диапазоны и `merge-base`
# работают как раньше. Единственная догрузка по требованию здесь —
# `git show origin/main:.github/workflows/<тонкий файл>`: по блобу на файл.
# `fetch-tags: true` (#703): теги релиза — граница проверенного материала
# для базы диапазона, а checkout по умолчанию качает с `--no-tags`.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: { fetch-depth: 0, filter: 'blob:none' }
with: { fetch-depth: 0, filter: 'blob:none', fetch-tags: true }
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with: { node-version: 22 }
@@ -81,17 +83,24 @@ jobs:
id: docs
continue-on-error: true
env:
REF: ${{ github.ref }}
EVENT_NAME: ${{ github.event_name }}
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
FULL_INPUT: ${{ inputs.full }}
# #697: на ветке задачи `Release:` строгий режим не включает.
REF_NAME: ${{ github.ref_name }}
run: |
# #700: на ветке задачи упавший чужой сайт — предупреждение, а не
# красный preflight; push в dev, кандидат и релиз судят ссылки строго.
external=--external
case "$REF" in refs/heads/issue/*) external=--external=warn ;; esac
# #586: CLI отдаёт ОДИН ответ. Прежде здесь сравнивался со строкой
# `heavy=true` весь вывод `--heavy`, а он двухстрочный: в `$(…)`
# строки схлопываются через пробел, сравнение не совпадало никогда,
# и строгий режим не включился ни на одном кандидате.
mode=$(node scripts/classify-changes.mjs --screenshots-mode)
echo "скриншоты документации: режим $mode"
node scripts/check-docs.mjs --external --screenshots=$mode
node scripts/check-docs.mjs "$external" --screenshots=$mode
# #635: `docs/reviews/INDEX.md` — снимок каталога ревью; расхождение с
# каталогом — невидимые через индекс документы (r2 #635 H1). Гейт стоит
@@ -115,9 +124,16 @@ jobs:
# ни одним событием. Тонкий файл сверяется: правка триггера или потолка
# прав, не доехавшая до `main`, действовала бы только в dev-копии.
#
# #716: `ship-review.yml` и `beta-derived.yml` — только `workflow_dispatch`,
# он исполняет файл с выбранной ветки, но запуск GitHub даёт лишь workflow,
# чей файл есть в `main`. Шагам беты он нужен до промоушена, поэтому они
# устроены так же и сверяются здесь же: правка входа или потолка прав,
# не доехавшая до `main`, не действовала бы в запуске с `main`, а именно
# его кнопка выбирает по умолчанию.
#
# `performance.yml` не входит: по расписанию он судит `main` собственным
# телом из `main`, расхождение с `dev` до промоушена законно. Список ниже
# держит равным множеству файлов с такими триггерами
# держит равным множеству тонких файлов
# test/default-branch-workflows.test.mjs.
- name: "Процесс: тонкие вызывающие workflow идентичны в main и dev"
id: workflow_sync
@@ -125,7 +141,7 @@ jobs:
run: |
git fetch --quiet origin main dev
status=0
for file in process.yml mutation-gate.yml process-resume.yml nightly.yml process-reconcile.yml process-metrics.yml; do
for file in process.yml mutation-gate.yml process-resume.yml nightly.yml process-reconcile.yml process-metrics.yml ship-review.yml beta-derived.yml; do
if diff <(git show "origin/main:.github/workflows/$file") \
<(git show "origin/dev:.github/workflows/$file"); then
echo "$file: main и dev идентичны"
@@ -138,6 +154,39 @@ jobs:
done
exit $status
# #700: расхождение зеркала на dev — одно открытое issue владельцу, как у
# ночного мутационного гейта (#472): чинит его тот, кто зеркалит в main, а
# не автор задачи, чья ветка ни при чём. Сбой API — предупреждение: шаг
# сообщает, а не судит.
- name: "Расхождение зеркала на dev — issue владельцу"
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' && steps.workflow_sync.outcome == 'failure'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
marker="[workflow-sync]"
# Список не прочитан — нового issue нет: иначе сбой сети или
# рейт-лимит плодил бы дубликат к уже открытому (r1 #700).
if ! existing=$(gh issue list --repo "$REPO" --state open --search "\"$marker\" in:title" \
--json number,title --jq '[.[] | select(.title | startswith("[workflow-sync]"))][0].number // empty'); then
echo "::warning::открытые issue не прочитаны — новое не заводится, чтобы не завести дубликат"
exit 0
fi
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo "$REPO" --body "Расхождение повторилось: $RUN_URL" \
|| echo "::warning::комментарий в #$existing не оставлен"
exit 0
fi
cat > /tmp/workflow-sync.md <<EOF
Тонкие вызывающие workflow в \`main\` и \`dev\` различаются. Прогон: $RUN_URL
Тонкий файл исполняется из ветки по умолчанию, поэтому правку триггеров, входов или прав нужно зеркалить в \`main\` (PROCESS.md §10.4, #623). На ветках задач это предупреждение (#700), на push в \`dev\` — красный preflight, пока зеркало не выровнено.
EOF
gh issue create --repo "$REPO" --title "$marker тонкие workflow в main и dev различаются" \
--label infra --label process --body-file /tmp/workflow-sync.md \
|| echo "::warning::issue о расхождении зеркала не заведено"
# Оба гейта ниже судят САМ диапазон коммитов, а не объём проверок, и до
# #388 брали его от головы предыдущего пуша. Прогон предыдущего пуша
# штатно отменяется следующим (concurrency), и тогда его коммиты не судит
@@ -147,9 +196,14 @@ jobs:
# прежним `before`: расширять диапазон здесь нельзя, иначе гейт, который
# сам красит прогон, лишает следующий пуш зелёного предка и запирает dev
# в красноте навсегда.
#
# #703: то же на `main`. Stable-промоушен — пуш в `main` SHA, уже
# судимого на `dev`; с `event.before` (прошлый stable) провенанс и
# процессный гейт судили бы всю бета-линию заново. Прогоны читаются с
# обеих интеграционных веток, сам HEAD засчитывает только успешный.
- name: "База диапазона: последний доказанно зелёный предок"
id: range
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.sha }}
@@ -159,11 +213,14 @@ jobs:
# `status=completed`, а не `success`: гейтам диапазона нужен факт
# «коммит судили», а не «вердикт был оправдательный». Упавший прогон
# коммит судил; отменённый — нет, его отсеивает judgedShas (#388).
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch=dev -f status=completed -F per_page=100 \
> /tmp/validate-runs.json || echo '{}' > /tmp/validate-runs.json
for branch in dev main; do
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch="$branch" -f status=completed -F per_page=100 \
> "/tmp/validate-runs-$branch.json" || echo '{}' > "/tmp/validate-runs-$branch.json"
done
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
--fallback="$FALLBACK" --runs=/tmp/validate-runs.json
--fallback="$FALLBACK" \
--runs=/tmp/validate-runs-dev.json --runs=/tmp/validate-runs-main.json
- name: "Провенанс коммитов: трейлеры и эталоны"
id: provenance
continue-on-error: true
@@ -218,6 +275,7 @@ jobs:
PROCESS_GATE: ${{ steps.process_gate.outcome }}
ACTION_PINS: ${{ steps.action_pins.outcome }}
REVIEWS_INDEX: ${{ steps.reviews_index.outcome }}
REF: ${{ github.ref }}
run: |
fail=0
check() {
@@ -229,9 +287,25 @@ jobs:
fail=1
fi
}
# #700: причина, к которой ветка задачи отношения не имеет, — в сводку
# предупреждением; на dev, кандидате и релизе она красит как прежде.
advise() {
if [ "$2" = "success" ]; then
echo "ok $1"
else
echo "::warning::$1 ($2) — на ветке задачи предупреждение (#700)"
echo "- $1: $2 — предупреждение, чинит релиз-менеджер (#700)" >> "$GITHUB_STEP_SUMMARY"
fi
}
task_branch=false
case "$REF" in refs/heads/issue/*) task_branch=true ;; esac
echo "### Предполётные проверки" >> "$GITHUB_STEP_SUMMARY"
check "документация" "$DOCS"
check "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
if [ "$task_branch" = "true" ]; then
advise "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
else
check "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
fi
check "провенанс коммитов" "$PROVENANCE"
check "процессный гейт" "$PROCESS_GATE"
check "пины сторонних Actions" "$ACTION_PINS"
@@ -273,16 +347,19 @@ jobs:
range_base: ${{ steps.base.outputs.range_base }}
steps:
# `git diff --name-only` содержимого файлов не читает вовсе, поэтому
# блобы истории этой job не нужны ни на одном шаге (#345).
# блобы истории этой job не нужны ни на одном шаге (#345). Теги — нужны:
# они граница проверенного материала для `range_base` (#703), а checkout
# по умолчанию передаёт `git fetch --no-tags`.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: { fetch-depth: 0, filter: 'blob:none' }
with: { fetch-depth: 0, filter: 'blob:none', fetch-tags: true }
# База диапазона — самый новый предок с УСПЕШНО завершённым Validate
# (#387). Прежде бралась голова предыдущего пуша (`github.event.before`),
# то есть допущение «до этого всё проверено». Concurrency отменяет прогон
# предыдущего пуша штатно, и на #86 (r5) это дало зелёный статус ветки
# без единого исполненного тяжёлого гейта.
# Смоки, golden, performance_smoke — на кандидате беты (трейлер
# `Release:`), по `workflow_dispatch full=true`, на PR (#479). Логика и
# `Release:` вне веток задач, #697), по `workflow_dispatch full=true`, на
# PR (#479). Логика и
# её тест — scripts/classify-changes.mjs.
- id: heavy
env:
@@ -290,7 +367,12 @@ jobs:
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
FULL_INPUT: ${{ inputs.full }}
MUTANTS_INPUT: ${{ inputs.mutants }}
run: node scripts/classify-changes.mjs --heavy | tee -a "$GITHUB_OUTPUT"
REF_NAME: ${{ github.ref_name }}
# #751: без pipefail упавший classify-changes оставлял heavy пустым —
# тяжёлые job молча пропускались, а job changes зеленела.
run: |
set -o pipefail
node scripts/classify-changes.mjs --heavy | tee -a "$GITHUB_OUTPUT"
- id: base
if: github.event_name != 'pull_request'
env:
@@ -312,9 +394,15 @@ jobs:
if [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; then
# На интеграционных ветках классифицировать нечего (всё true), но
# база диапазона нужна гейту «новый код не добавляет any» в job
# frontend (#388, #619).
# frontend (#388, #619). #703: прогоны обеих интеграционных веток —
# на `main` приходит SHA, уже судимый на `dev`.
other=dev; [ "$BRANCH" = "dev" ] && other=main
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch="$other" -f status=completed -F per_page=100 \
> /tmp/validate-runs-other.json || echo '{}' > /tmp/validate-runs-other.json
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
--name=range_base --fallback="$FALLBACK" --runs=/tmp/validate-runs.json
--name=range_base --fallback="$FALLBACK" \
--runs=/tmp/validate-runs.json --runs=/tmp/validate-runs-other.json
exit 0
fi
merge_base=$(git merge-base origin/dev "$HEAD_SHA" || git rev-parse "$HEAD_SHA~1")
@@ -590,6 +678,10 @@ jobs:
# слияние кандидата запускают Validate по кнопке с `mutants=true` на
# материале; PR берёт её сам. За 08–09.09 на промежуточных пушах она стоила
# 48 из 56 часов job-минут и в основном отменялась следующим пушем. С #601
# С #709 job не запрашивает никто: мутанты в разработке не гоняются, весь
# реестр проверяет ночь (mutation-gate.yml). Job оставлен до отдельной уборки
# вместе с контрактом имён job (#622); `mutants_requested` всегда false.
# Ниже — история до #709.
# её не запрашивают ни `full=true`, ни кандидат беты (`Release:`): к бете
# каждая задача уже прогнана мутантами на ревью и на слитом кандидате, а
# ночь покрыта полным реестром. Когда мутанты запрошены, job бежит даже при
+38 -14
View File
@@ -29,7 +29,9 @@ leak interactions into View. For work that changes visible behaviour, also read
route and `test/entry-cost.test.mjs` keeps this list equal to its routes:
- author (analysis, spec, implementation, infrastructure): `docs/SCOPE.md` →
`AGENTS.md` → `docs/process/AUTHOR.md` → `docs/STATUS.md`;
`AGENTS.md` → `docs/process/AUTHOR.md`, then the task packet
(`node scripts/task-packet.mjs --issue NN`); the status snapshot
(docs/STATUS.md) only when resuming a session or preparing a release;
- reviewer (spec or code): `docs/SCOPE.md` → `AGENTS.md` →
`docs/process/REVIEWER.md`, then the issue body and its comments;
- changing the pipeline, the gates or the process itself: `docs/SCOPE.md` →
@@ -61,7 +63,11 @@ gh issue view <NN> --repo Matysh/houseplan-card --json number,state,labels
The label must be one of `S5-ready`, `S6-in-progress`, `S7-code-review`. Anything
else — refuse and say why. "Issue #83 is in `S2-analysis`, code is off limits.
Start with the spec?" is the correct answer, not a smaller patch.
Start with the spec?" is the correct answer, not a smaller patch. The one
exception is a local, unpushed draft on `track:ask` while the spec is in
`S4-spec-review`, each commit carrying the `Spec-Draft` trailer from the task
packet; it reaches the branch only after a green spec review (`PROCESS.md`
§11.8).
GitHub Issues are the canonical task records and the **labels** are the status
(`PROCESS.md` §9); when repository documentation disagrees with an issue, the
@@ -73,13 +79,27 @@ overlap. The committed bundle changes only in a commit with a `Release:` trailer
(#657); an ordinary task restores it with `npm run bundle:clean` before
committing.
**Tracks** (`PROCESS.md` §5, §5.1): `small` is the default — the spec lives in the
issue body and its review is a comment; taking the full track means naming the
`small` criterion the task fails. `trivial` skips the spec stage for a bug whose
expected behaviour is already on record. An **infrastructure** task — not a single
**Tracks** (`PROCESS.md` §5, §5.1): the label `track:ship`, `track:show` or
`track:ask` sets the route, and the owner's label beats the criteria. `show` is the
default: up to three AC in the issue body, no spec review, `S2` → `S5`. `ship` is a
one-sentence change within fixed limits, `S1` → `S5`. `ask` is the full route with
a spec review. Any agent may raise a track with a reason; only the owner lowers it.
A label is a proposal until the owner confirms it with a comment line
`Трек: <ship|show|ask> — решение владельца`; an agent never writes that line.
Several track labels at once read as the strictest. A risky changed hunk
(geometry, touch, migration, devices, perf, a new UX key) raises an unconfirmed
`ship` to `show` at `S7`; on `show` it is a question to the reviewer (#707).
`small` and `trivial` read as `show`. An **infrastructure** task — not a single
class A file — skips analysis and spec and enters at `S7-code-review`
(`PROCESS.md` §1). Code review is never skipped on any track: it checks scope,
risks and the evidence from executed tests, but does not replace executing them.
(`PROCESS.md` §1). Every change is code-reviewed; on `ship` the review moves to a
batch review (`ship-review.yml`, `PROCESS.md` §11.7): nightly
(`SHIP-REVIEW-<base>-dev-<sha12>.md`), and before the beta tag whatever the night
did not read (`SHIP-REVIEW-<tag>.md`). The review pipeline prices each round by
track (§10.4): no mutants run
during development on any track — the whole registry runs nightly (#709); a
rebase before review only on `ask` or when
the branch does not merge cleanly into `dev`. Review checks scope, risks and the
evidence from executed tests, but does not replace executing them.
## Specs
@@ -94,8 +114,9 @@ batched comment with a proposed default for each question and `blocked` on top o
## Commits and branches
Hooks install themselves on `npm ci` (`prepare` → `scripts/install-hooks.mjs`);
`git config core.hooksPath` must print `.githooks`. Every non-merge commit
carries **terminal** trailers:
`git config core.hooksPath` must print `.githooks`. Every non-merge commit that
touches anything outside class C (docs) carries **terminal** trailers; a
docs-only commit needs none (`PROCESS.md` §3 п.10, #701):
```text
Issue: #123
@@ -137,9 +158,12 @@ fresh checkout. A worktree works only on the machine that created it — its
## Handoff and the verdict
Start a task from its packet: `node scripts/task-packet.mjs --issue NN` (status,
track, what the status permits, the branch against `dev`, the previous verdict
and the unwitnessed AC; it writes nothing). The local gate is
Start a task from its packet: `node scripts/task-packet.mjs --issue NN` (status;
track with its basis, cycle limit and rebase policy; what the status permits;
the branch against `dev` and the next step — no rebase a clean `show`/`ship`
merge does not need; risk by changed hunks and what it means on this track; the
required checks, each with its reason; changelog and visual evidence; the
previous verdict and the unwitnessed AC; it writes nothing). The local gate is
`npm run gate:small` (`docs/TESTING.md` › Локальный набор перед пушем); run the
smokes named in the AC before `S7-code-review`. **"Verified" without a named
command and its result is not evidence.** Comment formats — claim, handoff,
@@ -160,7 +184,7 @@ set.
| Now reads | What happened | What you do |
|---|---|---|
| `S5-ready` | the spec is accepted | write the code |
| `S5-ready` | the spec is accepted | write the code (a draft from `S4`: rebase onto `dev`, check, push — §11.8) |
| `S3-spec` | the spec came back | read the verdict, revise, re-apply `S4-spec-review` |
| `S6-in-progress` | the code came back | revise, re-apply `S7-code-review` — **or**, if the verdict was green and only the merge conflicted, just rebase and re-apply. The comment says which |
| `S8-merged` | accepted and already in `dev` | nothing |
+14 -10
View File
@@ -60,12 +60,15 @@ because the plan canvas and editors cannot be mirrored by translations alone.
## Documentation screenshots
The images under `docs/images/` are produced only from synthetic data by the
`Docs screenshots` workflow (`demo/docs/capture.mjs` on the pinned Chromium)
and accepted locally with `npm run docs:accept -- --reviewed --from=<unpacked
artifact>`; when a change cannot move a pixel, `npm run docs:accept --
--identical` re-captures locally, compares decoded pixels and refreshes only the
source fingerprint. Scenario version, source fingerprint and every image hash
The images under `docs/images/` are produced only from synthetic data on the
pinned Chromium in CI. A task branch does not commit them, nor the golden
baselines: once per beta the `beta-derived.yml` workflow refreshes the
fingerprint, the frames and the golden baselines on `dev` in one bot commit,
accepting only the frames it was told to expect (`PROCESS.md` §8, #697). A task
that changes visuals on purpose sets the `ci:golden` label. The manual path —
the `Docs screenshots` workflow and `npm run docs:accept -- --reviewed
--from=<unpacked artifact>`, or `--identical` when no pixel can move — stays
for the release manager. Scenario version, source fingerprint and every image hash
are recorded in the [screenshot index](docs/images/screenshots.json), and
`node scripts/check-docs.mjs` reports a stale fingerprint: a warning on an
ordinary push, an error on a beta candidate (a commit with a `Release:`
@@ -91,12 +94,11 @@ issue, but they do not replace it or maintain a separate checklist.
```bash
git clone --filter=blob:none https://github.com/Matysh/houseplan-card && cd houseplan-card
npm ci # frontend toolchain
npm ci # frontend toolchain; `prepare` installs .githooks
npm run typecheck # tsc --noEmit (strict)
npm test # node:test — pure logic, i18n parity, tap-action security
npm run build # tsc + rollup → dist/houseplan-card.js
pip install pytest voluptuous && python -m pytest tests_backend -q # pure backend tests
npm install # also installs .githooks through the prepare script
pip install -r tests_backend/requirements.txt && python -m pytest tests_backend -q # CI pins, HA harness included
```
### Why `--filter=blob:none` (#345)
@@ -124,7 +126,9 @@ Linux CI or WSL (`bash scripts/wsl-setup.sh --verify`). Without an importable
## Ground rules
- **Docs in the same commit**: CHANGELOG entry for user-visible changes;
`docs/STATUS.md` for state changes; `docs/DEVELOPMENT.md` for new gotchas.
`docs/ARCHITECTURE.md` for changes to the data model, WS API or coordinate
system; `docs/STATUS.md` for state changes; `docs/DEVELOPMENT.md` for new
gotchas. A docs-only commit needs no trailers (`PROCESS.md` §3 п.10).
- Every UI string goes through `src/i18n/<lang>.json`; follow the
[Translations](#translations) flow for registry and backend parity.
- The committed bundle changes only in a release candidate: `npm run
+733 -177
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 JB (justbusiness)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+47
View File
@@ -0,0 +1,47 @@
# House Plan moon pack 1.0.0
Designer asset for [issue #661](https://github.com/Matysh/houseplan-card/issues/661)
(moon in its current phase on the "Follow the Sun" background), variant A:
one full-moon image, the phase is applied by the card as an SVG mask (C5).
- `moon.svg`: square `viewBox="0 0 512 512"`, disc centred at (256, 256),
diameter 480 (16 px margin for anti-aliasing). Full moon, front-lit: no
terminator, no shadow, no halo or glow outside the disc, no baked phase.
Near side, north up, Mare Crisium on the right; the art is never mirrored.
Owner 2026-09-29: the lit side is always the left one (as in the designer's
phase sequence), waning runs the same states backwards; only the mask flips.
Owner 2026-09-30: a distinct state for every day of the lunar month; the mask
is continuous, so the card simply uses the daily illumination k.
The dark side is not drawn: the card renders the same art at 8 % opacity.
- Edge: one radial gradient ring with no hard outer edge (as in the designer's
Figma export): bright rim 0.855-0.90 R, dark outline `#2b3138` at 50 % from
0.905 R to 0.92 R (owner 2026-09-29, for a forced white background), then a
1.5-unit fade to transparent by 0.935 R (r 239.4 in the 512 box, inside the 480
disc). The soft limb hides any missing anti-aliasing of the outer edge.
- The disc is masked (`<mask>`), not clipped: Chrome with GPU rasterisation draws
`clip-path` edges without anti-aliasing (seen on Windows), masks are anti-aliased.
- Art by JB (Figma, `House-plan` file). The Figma export (`source/figma-export/Full.svg`
in the source archive) was normalised for the pack by `source/convert_jb.py`:
viewBox 200 -> 512 (scale 2.56, disc clipped at r 240), the `mix-blend-mode`
screen ring and multiply groups removed (blend modes depend on the stacking
context they land in, so the file now composites normally and looks the same
everywhere), the three 5-7 % crater layers and the 484-path speckle texture
dropped (invisible at 200 px), and each mare "blend" of 22 nested rings
thinned to 4 rings (steps 0, 8, 16, 21). The export's screen-blended edge ring is rebuilt
as a normal-composited radial gradient (the art is masked at r 230), so the feathered edge is part
of the 480 disc and nothing is drawn outside it. Path coordinates rounded to 0.1, gradient offsets kept at 0.001 (`svgo.config.mjs`).
- Vector only: two `radialGradient`s (disc, edge), one `mask`, paths. No `filter`,
`feGaussianBlur`, `feImage`, embedded raster, fonts or external references.
25.6 KB raw, about 9 KB gzip.
- Colours: disc `#949493` (centre) -> `#D6D5D4` (rim), maria grey rings
`#404040 -> white` under a 0.2-0.6 group opacity. Checked on the night
(`#111a27 -> #1f2f3e`), dusk (`#48536c -> #9a7380`) and dawn
(`#aabdd1 -> #e8c8b7`) palettes at 200, 120 and 94 px.
- `pack.json`: pack id, version, licence, source link and the disc geometry
for `scripts/generate-moon-assets.mjs`.
JB (justbusiness) supplies this asset for House Plan to use, modify and
distribute under the repository MIT License without separate UI attribution
(`LICENSE.md`). The reviewed source archive with its SHA-256 is attached to
issue #661. Generated TypeScript (`src/moon-art.generated.ts`) must not be
edited by hand; regenerate it from this pack.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

+38
View File
@@ -0,0 +1,38 @@
{
"schema_version": 1,
"pack_id": "houseplan",
"pack_version": "1.0.0",
"title_ru": "Луна: полный диск для маски фазы",
"title_en": "Moon: full disc for the phase mask",
"author": "JB (justbusiness)",
"license": "MIT",
"source_url": "https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan",
"issue": 661,
"file": "moon.svg",
"view_box": [
0,
0,
512,
512
],
"disc": {
"cx": 256,
"cy": 256,
"r": 240
},
"lit": "front",
"north_up": true,
"east_right": true,
"notes_en": "Full moon, front-lit, no terminator, no shadow, no glow outside the disc; thin dark outline for light backgrounds. Art by JB (Figma export normalised: viewBox 512, blend modes removed, rings thinned). Phase mask and the 8 % dark side are applied by the card (C5). The art is never mirrored; the lit side is always the left one, waning runs the same states backwards (owner 2026-09-29).",
"notes_ru": "Полная луна, освещение в лоб, без терминатора, тени и ореола; тонкая тёмная обводка для светлого фона. Арт JB (экспорт из Figma нормализован: viewBox 512, без blend-режимов, кольца морей прорежены). Маску фазы и тёмную сторону 8 % накладывает карточка (C5). Арт не зеркалится; освещённая сторона всегда слева, убывание идёт теми же состояниями в обратном порядке (владелец 29.09).",
"mirror": false,
"outline": {
"color": "#2b3138",
"opacity": 0.5,
"from_r": 0.905,
"to_r": 0.92,
"fade_to_r": 0.935,
"note": "part of the edge gradient, no separate stroke"
},
"lit_side": "left"
}
+4 -2
View File
@@ -57,7 +57,7 @@ SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
ASSETS_DIR = "houseplan/assets"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.78.0-beta.8"
VERSION = "1.79.0-beta.3"
DECOR_ASSETS_API_VERSION = 1
SUMMARY_PANEL_API_VERSION = 1
@@ -91,7 +91,9 @@ MAX_IMPORT_PREVIEWS_TOTAL = 3
DEFAULT_CONFIG: dict[str, object] = {
"spaces": [],
"markers": [],
"settings": {"bg_mode": "daynight", "sun_ray_origin": "inner"},
# #661: new installations get the moon; an existing config without the key
# keeps its look (docs/SUN.md: an update never changes a stored plan).
"settings": {"bg_mode": "daynight", "sun_ray_origin": "inner", "moon": True},
}
# #42: THE stable public error-code contract. Every code a user-facing
@@ -86,6 +86,16 @@ def _lattice_points(value: Any) -> None:
_lattice_point(point)
def _scalar_points(value: Any) -> None:
if not isinstance(value, list):
return
for point in value:
if not isinstance(point, list):
continue
for index in range(min(2, len(point))):
point[index] = canonicalize_number(point[index])
def canonicalize_position(position: Any) -> Any:
"""Canonicalise lattice x/y in one layout record, preserving metadata."""
result = copy.deepcopy(position)
@@ -182,6 +192,12 @@ def canonicalize_config_geometry(config: Any) -> Any:
_lattice_point(span.get("a"))
_lattice_point(span.get("b"))
for strip in _records(space.get("led_strips")):
# #780: LED strips snap to physical wall faces, like stairs and
# furniture: remove JSON noise only, never pull a face contact
# onto a lattice node.
_scalar_points(strip.get("points"))
for marker in _records(root.get("markers")):
_scalar_fields(marker, ("angle",))
@@ -1,382 +1,544 @@
{
"schema": 1,
"fingerprint": "d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5",
"fingerprint": "169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 299719,
"initialViewGzipBytes": 300048,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 301242,
"initialPanelGzipBytes": 301572,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2327,
"lazyFiles": [
"houseplan-assets/backdrop-pick-BE-iJ7rt.js",
"houseplan-assets/de-DNNMnY_z.js",
"houseplan-assets/editor-ChSNpLTK.js",
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/fr-B5qLHBHa.js",
"houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js",
"houseplan-assets/houseplan-editor-runtime-WD5ILNqt.js",
"houseplan-assets/houseplan-onboarding-runtime-al0BGZnG.js",
"houseplan-assets/hp-zigbee-topology-overlay-CIHA7FPL.js",
"houseplan-assets/iso-scene-render-DgucMKMQ.js",
"houseplan-assets/live-interaction-runtime-Dp8eZeCH.js",
"houseplan-assets/namespace-language-DePztGEB.js",
"houseplan-assets/pdf-export-x4JU1MRl.js",
"houseplan-assets/summary-panel-runtime-loaded-H6hY7acD.js",
"houseplan-assets/zigbee-topology-Dq_fGFqf.js",
"houseplan-assets/zigbee-topology-runtime-BTxO2PCB.js"
"houseplan-assets/backdrop-pick-B3E3pF2p.js",
"houseplan-assets/de-BN3syD61.js",
"houseplan-assets/editor-D2WaHE8f.js",
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/fr-CCINCoVv.js",
"houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js",
"houseplan-assets/houseplan-editor-runtime-B5Pema3O.js",
"houseplan-assets/houseplan-onboarding-runtime-CNLWiEE-.js",
"houseplan-assets/hp-zigbee-topology-overlay-DfI0j0uZ.js",
"houseplan-assets/iso-scene-render-B382doeD.js",
"houseplan-assets/led-strip-editor-CshMJWp-.js",
"houseplan-assets/led-strip-geometry-b-NVYCNs.js",
"houseplan-assets/led-strip-runtime-c9sWUV6s.js",
"houseplan-assets/live-interaction-runtime-C2q7kzgj.js",
"houseplan-assets/moon-runtime-D_xnc9K2.js",
"houseplan-assets/namespace-language-CRySE1JG.js",
"houseplan-assets/pdf-export-CU7_R9G1.js",
"houseplan-assets/space-editor-apwn9bIB.js",
"houseplan-assets/summary-panel-runtime-loaded-DpE5oJAr.js",
"houseplan-assets/zigbee-topology-DKrDE5nD.js",
"houseplan-assets/zigbee-topology-runtime-Dzr8mpaZ.js"
],
"lazyGzipBytes": 456318,
"lazyGzipBytes": 477883,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-BE-iJ7rt.js",
"houseplan-assets/editor-ChSNpLTK.js",
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js",
"houseplan-assets/houseplan-editor-runtime-WD5ILNqt.js",
"houseplan-assets/namespace-language-DePztGEB.js",
"houseplan-assets/zigbee-topology-Dq_fGFqf.js"
"houseplan-assets/backdrop-pick-B3E3pF2p.js",
"houseplan-assets/editor-D2WaHE8f.js",
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js",
"houseplan-assets/houseplan-editor-runtime-B5Pema3O.js",
"houseplan-assets/namespace-language-CRySE1JG.js",
"houseplan-assets/zigbee-topology-DKrDE5nD.js"
],
"lazyEditorGzipBytes": 238202,
"lazyEditorGzipBytes": 240859,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-BE-iJ7rt.js",
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/houseplan-onboarding-runtime-al0BGZnG.js",
"houseplan-assets/namespace-language-DePztGEB.js"
"houseplan-assets/backdrop-pick-B3E3pF2p.js",
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/houseplan-onboarding-runtime-CNLWiEE-.js",
"houseplan-assets/namespace-language-CRySE1JG.js"
],
"lazyOnboardingGzipBytes": 27952,
"lazyOnboardingGzipBytes": 28405,
"lazyLocaleFiles": [
"houseplan-assets/de-DNNMnY_z.js",
"houseplan-assets/fr-B5qLHBHa.js"
"houseplan-assets/de-BN3syD61.js",
"houseplan-assets/fr-CCINCoVv.js"
],
"lazyLocaleGzipBytes": 56329,
"lazyLocaleGzipBytes": 53871,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-DgucMKMQ.js"
"houseplan-assets/iso-scene-render-B382doeD.js"
],
"lazyIsometricGzipBytes": 17887,
"lazyIsometricGzipBytes": 12347,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js"
"houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js"
],
"lazyFurnitureArtGzipBytes": 16941,
"lazyFurnitureArtGzipBytes": 16938,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js",
"houseplan-assets/pdf-export-x4JU1MRl.js"
"houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js",
"houseplan-assets/pdf-export-CU7_R9G1.js"
],
"lazyPdfGzipBytes": 126926,
"lazyPdfGzipBytes": 126935,
"lazyMoonFiles": [
"houseplan-assets/moon-runtime-D_xnc9K2.js"
],
"lazyMoonGzipBytes": 11713,
"lazyLedFiles": [
"houseplan-assets/led-strip-field-CJ4ziktu.js",
"houseplan-assets/led-strip-geometry-b-NVYCNs.js",
"houseplan-assets/led-strip-runtime-c9sWUV6s.js"
],
"lazyLedGzipBytes": 9093,
"lazyLedEditorFiles": [
"houseplan-assets/led-strip-editor-CshMJWp-.js",
"houseplan-assets/led-strip-geometry-b-NVYCNs.js",
"houseplan-assets/namespace-language-CRySE1JG.js"
],
"lazyLedEditorGzipBytes": 9961,
"lazyNamespaceLocaleFiles": [
"houseplan-assets/settings-de-BSyPpKTY.js",
"houseplan-assets/settings-fr-DSfTuRX4.js",
"houseplan-assets/settings-ru-BbkCpEMg.js",
"houseplan-assets/support-de-BqFx0CHP.js",
"houseplan-assets/support-fr-DVw_3-2z.js",
"houseplan-assets/support-ru-BChLp-Eg.js",
"houseplan-assets/topology-de-Csx831lO.js",
"houseplan-assets/topology-fr-Dw79rb1j.js",
"houseplan-assets/topology-ru-C4aeCsnx.js"
"houseplan-assets/led-de-gOo35eVH.js",
"houseplan-assets/led-fr-ChGtksoI.js",
"houseplan-assets/led-ru-BFlULP8y.js",
"houseplan-assets/settings-de-DxZEXtQV.js",
"houseplan-assets/settings-fr-FcL8rhRf.js",
"houseplan-assets/settings-ru-DH57wGYn.js",
"houseplan-assets/support-de-D4aOS-oC.js",
"houseplan-assets/support-fr-D3Yz05IS.js",
"houseplan-assets/support-ru-78sMvMUp.js",
"houseplan-assets/tools-de-pr1daK8w.js",
"houseplan-assets/tools-fr-BG50m6AG.js",
"houseplan-assets/tools-ru-CHiUK5GX.js",
"houseplan-assets/topology-de-k0TFglWi.js",
"houseplan-assets/topology-fr-DEOQrFrw.js",
"houseplan-assets/topology-ru-DNMInWXF.js"
],
"lazyNamespaceLocaleGzipBytes": 21054,
"lazyNamespaceLocaleGzipBytes": 30076,
"files": [
{
"path": "houseplan-assets/backdrop-pick-BE-iJ7rt.js",
"sha256": "1f1ff9bc15ecaf47edd5f39173d7bec7932faeb5c617722611006b69834b1d82",
"rawBytes": 54017,
"gzipBytes": 17577,
"path": "houseplan-assets/backdrop-pick-B3E3pF2p.js",
"sha256": "5543c7b50101c694619ff27c7b9c9179289d50369c049635e7e36f36a1f52ca5",
"rawBytes": 55329,
"gzipBytes": 18029,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/namespace-language-DePztGEB.js"
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/namespace-language-CRySE1JG.js"
],
"dynamicImports": [
"houseplan-assets/settings-de-BSyPpKTY.js",
"houseplan-assets/settings-fr-DSfTuRX4.js",
"houseplan-assets/settings-ru-BbkCpEMg.js"
"houseplan-assets/settings-de-DxZEXtQV.js",
"houseplan-assets/settings-fr-FcL8rhRf.js",
"houseplan-assets/settings-ru-DH57wGYn.js"
]
},
{
"path": "houseplan-assets/de-DNNMnY_z.js",
"sha256": "4b167450350abdf7107f13e87e35ddc3164242800b56cc42f739ce61ab6cabb3",
"rawBytes": 94196,
"gzipBytes": 28393,
"path": "houseplan-assets/de-BN3syD61.js",
"sha256": "c53edfa99ff2d593e0fd20fe38f4f407f9d032094c0f886b19fb2a6c9d98a70d",
"rawBytes": 89873,
"gzipBytes": 27167,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-ChSNpLTK.js",
"sha256": "6ed9c5474092c375b4d6251ca595d71858c0191e12cd28aee2bf8b3cab4d6fa1",
"path": "houseplan-assets/editor-D2WaHE8f.js",
"sha256": "524fdb5f25b19ba4bae642db34fb9d33e2d1a827202739708435eea7eb3626dd",
"rawBytes": 3833,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"sha256": "36a180f3ac4d09eff332887b12e9d54c4acf382b83785a653a7f20f0cda20605",
"path": "houseplan-assets/form-kit.styles-7MsRKJCO.js",
"sha256": "2ab7027a8c51bde0413f3cc286c2b27128d2380c3ac25f2e1a01172943a9f02d",
"rawBytes": 25600,
"gzipBytes": 5056,
"gzipBytes": 5057,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-B5qLHBHa.js",
"sha256": "eb6206321026762fd68fc2666a2a2a7445105b3613ba9daf9ca3038d98c5aaa6",
"rawBytes": 96627,
"gzipBytes": 27936,
"path": "houseplan-assets/fr-CCINCoVv.js",
"sha256": "8d8dc0f12e61cd790ea7242ad2b86b81477e743f3d4d5c4c38c6a152600807c2",
"rawBytes": 92266,
"gzipBytes": 26704,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js",
"sha256": "3e9ce2c2fd9261b3ef8c95fa70224af19e911f753a2e2cd9e183de62a9ac43eb",
"path": "houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js",
"sha256": "2a248728b61c684043153b987cd473c2419eba5374d5cf7d4e306ded1a077c28",
"rawBytes": 52035,
"gzipBytes": 16941,
"gzipBytes": 16938,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-C4IYYoj_.js",
"sha256": "7d77de11ae0adeaab348136752a44cecaa9893c2e80a88aa90c40f236c19503a",
"rawBytes": 1057747,
"gzipBytes": 298915,
"path": "houseplan-assets/houseplan-card-DE8tFhlR.js",
"sha256": "a0bb0f7ff3b6be7506f0b11c9d0bcd8608e205d5d9cfc462cb9d27637bc97acd",
"rawBytes": 1056181,
"gzipBytes": 299245,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-DNNMnY_z.js",
"houseplan-assets/editor-ChSNpLTK.js",
"houseplan-assets/fr-B5qLHBHa.js",
"houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js",
"houseplan-assets/houseplan-editor-runtime-WD5ILNqt.js",
"houseplan-assets/houseplan-onboarding-runtime-al0BGZnG.js",
"houseplan-assets/hp-zigbee-topology-overlay-CIHA7FPL.js",
"houseplan-assets/iso-scene-render-DgucMKMQ.js",
"houseplan-assets/live-interaction-runtime-Dp8eZeCH.js",
"houseplan-assets/pdf-export-x4JU1MRl.js",
"houseplan-assets/summary-panel-runtime-loaded-H6hY7acD.js"
"houseplan-assets/de-BN3syD61.js",
"houseplan-assets/editor-D2WaHE8f.js",
"houseplan-assets/fr-CCINCoVv.js",
"houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js",
"houseplan-assets/houseplan-editor-runtime-B5Pema3O.js",
"houseplan-assets/houseplan-onboarding-runtime-CNLWiEE-.js",
"houseplan-assets/hp-zigbee-topology-overlay-DfI0j0uZ.js",
"houseplan-assets/iso-scene-render-B382doeD.js",
"houseplan-assets/led-strip-editor-CshMJWp-.js",
"houseplan-assets/led-strip-runtime-c9sWUV6s.js",
"houseplan-assets/live-interaction-runtime-C2q7kzgj.js",
"houseplan-assets/moon-runtime-D_xnc9K2.js",
"houseplan-assets/pdf-export-CU7_R9G1.js",
"houseplan-assets/space-editor-apwn9bIB.js",
"houseplan-assets/summary-panel-runtime-loaded-DpE5oJAr.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-WD5ILNqt.js",
"sha256": "7a95fb422adae090b2572b1865e08f52ee33f96bcedb2d1a78a69407c80c9424",
"rawBytes": 688814,
"gzipBytes": 192334,
"path": "houseplan-assets/houseplan-editor-runtime-B5Pema3O.js",
"sha256": "90b5e013bd63aac3d98535f502eadd73ce3b1a751fcd9c23b510049aa8d67840",
"rawBytes": 695353,
"gzipBytes": 194539,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BE-iJ7rt.js",
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js",
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/namespace-language-DePztGEB.js",
"houseplan-assets/zigbee-topology-Dq_fGFqf.js"
"houseplan-assets/backdrop-pick-B3E3pF2p.js",
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/namespace-language-CRySE1JG.js",
"houseplan-assets/zigbee-topology-DKrDE5nD.js"
],
"dynamicImports": [
"houseplan-assets/support-de-BqFx0CHP.js",
"houseplan-assets/support-fr-DVw_3-2z.js",
"houseplan-assets/support-ru-BChLp-Eg.js",
"houseplan-assets/zigbee-topology-runtime-BTxO2PCB.js"
"houseplan-assets/support-de-D4aOS-oC.js",
"houseplan-assets/support-fr-D3Yz05IS.js",
"houseplan-assets/support-ru-78sMvMUp.js",
"houseplan-assets/tools-de-pr1daK8w.js",
"houseplan-assets/tools-fr-BG50m6AG.js",
"houseplan-assets/tools-ru-CHiUK5GX.js",
"houseplan-assets/zigbee-topology-runtime-Dzr8mpaZ.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-al0BGZnG.js",
"sha256": "ae3692ec96cccc7beb00fb2401ae839d3bfcbcbdcb438788aced1aaed3306104",
"path": "houseplan-assets/houseplan-onboarding-runtime-CNLWiEE-.js",
"sha256": "2d1a906cb3e2942967b867ccd9f1e3af9d0f06b58e430d27a726114c759fc7ef",
"rawBytes": 15886,
"gzipBytes": 4908,
"gzipBytes": 4910,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-BE-iJ7rt.js",
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/namespace-language-DePztGEB.js"
"houseplan-assets/backdrop-pick-B3E3pF2p.js",
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/namespace-language-CRySE1JG.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-CIHA7FPL.js",
"sha256": "e4b86db514fc279e836d12c0a81d58c3760e3a391702f3d87f2b9e70fc31fa2b",
"path": "houseplan-assets/hp-zigbee-topology-overlay-DfI0j0uZ.js",
"sha256": "6fd7f0d1714124ab2bb43d529015d3aa8039d1e1bf4aadf1574e176cdbb8746a",
"rawBytes": 10958,
"gzipBytes": 3764,
"gzipBytes": 3768,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/namespace-language-DePztGEB.js",
"houseplan-assets/zigbee-topology-Dq_fGFqf.js",
"houseplan-assets/zigbee-topology-runtime-BTxO2PCB.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/namespace-language-CRySE1JG.js",
"houseplan-assets/zigbee-topology-DKrDE5nD.js",
"houseplan-assets/zigbee-topology-runtime-Dzr8mpaZ.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-DgucMKMQ.js",
"sha256": "106ff533ae9ec7f1cd11e0ebbe40cf8ef940dc107364699d2a077468414c46d7",
"rawBytes": 52970,
"gzipBytes": 17887,
"path": "houseplan-assets/iso-scene-render-B382doeD.js",
"sha256": "a0cf0627398960ae91c5da0007ad10019186568f699771c4bb12dac0bf68d7e7",
"rawBytes": 36700,
"gzipBytes": 12347,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-Dp8eZeCH.js",
"sha256": "6bef63baebba188c095197ac4ea4a5057f50bc0078b749a6d72c84f92719bdff",
"rawBytes": 8216,
"gzipBytes": 3209,
"path": "houseplan-assets/led-de-gOo35eVH.js",
"sha256": "b11e22bd8835764529e0f67b89d1b9e8dea531f7d7d95c25c86776feedc2e7c2",
"rawBytes": 1907,
"gzipBytes": 921,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/led-fr-ChGtksoI.js",
"sha256": "2a51c0d321620042aa75b41d80f1462b7dca1d098032b288bd0b6df74159a250",
"rawBytes": 1886,
"gzipBytes": 906,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/led-ru-BFlULP8y.js",
"sha256": "b85b63fa3571ed186c7ce07ec904f407749665a84bfdf62986d5bacc5083ff8d",
"rawBytes": 2525,
"gzipBytes": 1107,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/led-strip-editor-CshMJWp-.js",
"sha256": "d198432cde69513ff71a8d3590cc60b288d60e7cf1a4fe7af533a24e01b8dd18",
"rawBytes": 21559,
"gzipBytes": 7307,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/led-strip-geometry-b-NVYCNs.js",
"houseplan-assets/namespace-language-CRySE1JG.js"
],
"dynamicImports": [
"houseplan-assets/led-de-gOo35eVH.js",
"houseplan-assets/led-fr-ChGtksoI.js",
"houseplan-assets/led-ru-BFlULP8y.js"
]
},
{
"path": "houseplan-assets/led-strip-field-CJ4ziktu.js",
"sha256": "d4f44b5c5fee0947c2d438007d333069e7c7151f0b311885dae96c0c0e0b27f1",
"rawBytes": 6115,
"gzipBytes": 2668,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/led-strip-geometry-b-NVYCNs.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/namespace-language-DePztGEB.js",
"sha256": "d599542bbbd3ae236a3c333485ce7f8d9eae39c256b6aa3ea96a6aece86f70f7",
"path": "houseplan-assets/led-strip-geometry-b-NVYCNs.js",
"sha256": "3e3ff430bc74e8f0bd9d30d717bbdef109180438776a0c135d32d7f6e5961de9",
"rawBytes": 5231,
"gzipBytes": 2245,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/led-strip-runtime-c9sWUV6s.js",
"sha256": "df06cb7861a43dc0db08981886aa3f3010e07d1a422154513589d1846cb776fb",
"rawBytes": 10582,
"gzipBytes": 4180,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/led-strip-geometry-b-NVYCNs.js"
],
"dynamicImports": [
"houseplan-assets/led-strip-field-CJ4ziktu.js"
]
},
{
"path": "houseplan-assets/live-interaction-runtime-C2q7kzgj.js",
"sha256": "1e3b44064a0be2d0c3838335602115349ad48964bfe7b9bf12c5417892d65837",
"rawBytes": 8370,
"gzipBytes": 3280,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/moon-runtime-D_xnc9K2.js",
"sha256": "468909aabec8526250b8edfe7171e845705679e8cc2936ce0ae5412207f8119a",
"rawBytes": 30292,
"gzipBytes": 11713,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/namespace-language-CRySE1JG.js",
"sha256": "c17c26275978d717284ed070d9f0c34fe23ab463747cbc573697939375af9c84",
"rawBytes": 675,
"gzipBytes": 411,
"gzipBytes": 409,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-x4JU1MRl.js",
"sha256": "31faa6450db430462a47b5dcf740623606f9a9302979a6dbc59b8c9a9eca7456",
"rawBytes": 236704,
"gzipBytes": 109985,
"path": "houseplan-assets/pdf-export-CU7_R9G1.js",
"sha256": "a982774265f4227cf990ddbe12ae53ce9173e6bb32c42840cfe401d99e2f62e2",
"rawBytes": 236714,
"gzipBytes": 109997,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-Csm0DSdr.js",
"houseplan-assets/houseplan-card-C4IYYoj_.js"
"houseplan-assets/furniture-plan-art.generated-B6HJUO3v.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-de-BSyPpKTY.js",
"sha256": "d0db509be47284189733ec2fb5eeb858c8cb0c0184d25dd4e2ce1b318cf60c69",
"rawBytes": 7378,
"path": "houseplan-assets/settings-de-DxZEXtQV.js",
"sha256": "87aa3a395558251a55ac7f6b07d70a47dddbc0ef04906af813e3a45d3f25a2e4",
"rawBytes": 8886,
"gzipBytes": 3410,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-FcL8rhRf.js",
"sha256": "31f8c722b182b56fd37672617a7dd2f7475c629ad8e31869291a202ed380140d",
"rawBytes": 9297,
"gzipBytes": 3384,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-DH57wGYn.js",
"sha256": "ac0fcf1abb4b8d4bc3cd38fa84db899257aa4ccd3f09bcec784ca27ce8daa120",
"rawBytes": 12284,
"gzipBytes": 3899,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-editor-apwn9bIB.js",
"sha256": "0675068fa2a6777b209f84bf7487dae1c3866ec64798d79454ab12a282037add",
"rawBytes": 2619,
"gzipBytes": 1127,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DjUiuqI4.js",
"sha256": "2b1f0ecd01f60d40372f54f6ce58ac47a56366794ee2e6194067d43431faf2d8",
"rawBytes": 14862,
"gzipBytes": 3426,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/summary-panel-runtime-loaded-DpE5oJAr.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-cz0glBWJ.js",
"sha256": "257ad750b74e89d849056906c1b22eeca3d80ed36a27d547fb5a6e0a6ff1bd9d",
"rawBytes": 2701,
"gzipBytes": 1346,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-DpE5oJAr.js",
"sha256": "9df29e01c90b4afa3ea7e65a422846e465f74d482a550dc5b54a1cde6321262f",
"rawBytes": 73476,
"gzipBytes": 20488,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-7MsRKJCO.js",
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DjUiuqI4.js",
"houseplan-assets/summary-panel-metrics-cz0glBWJ.js"
]
},
{
"path": "houseplan-assets/support-de-D4aOS-oC.js",
"sha256": "1dc9f15a134ace6ab571abc0c1299eb9a23f9f33702f222466546d12db5f00be",
"rawBytes": 6167,
"gzipBytes": 2398,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-D3Yz05IS.js",
"sha256": "1a385c25c12e42df8072072ea92458aaaf5da2934a1a60c0adefb7ddb46306f1",
"rawBytes": 6333,
"gzipBytes": 2433,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-78sMvMUp.js",
"sha256": "172c3d5a9bba3fbf8acfc4740a04b62828816809b9ed7ecc0bec955f6991fbb6",
"rawBytes": 8376,
"gzipBytes": 2878,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-DSfTuRX4.js",
"sha256": "2517de0a7181e99cb3b4e86ccdb2d8f9630a4b26c9eaa4c1e8cfea6cdd4af00f",
"rawBytes": 7620,
"gzipBytes": 2841,
"path": "houseplan-assets/tools-de-pr1daK8w.js",
"sha256": "259117478173567830810ca38252ad20a017cc3818e388bcc74035601bf70ae7",
"rawBytes": 4130,
"gzipBytes": 1377,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-BbkCpEMg.js",
"sha256": "3694a27b95ac5158eb58a9272f6145fa3790a8ebe668267527ded16de1bf45da",
"rawBytes": 10304,
"gzipBytes": 3325,
"path": "houseplan-assets/tools-fr-BG50m6AG.js",
"sha256": "1d3bdecf38fecbbc3184f6c7630f1c777c1239691ad04304f1260aa1e9d7fbb0",
"rawBytes": 4136,
"gzipBytes": 1359,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DCKoIOD4.js",
"sha256": "287136bbc1c31c3c9bc367781d37890a8a69d572c167b25d516f7a1a80470e9d",
"rawBytes": 14862,
"gzipBytes": 3425,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/summary-panel-runtime-loaded-H6hY7acD.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-CUfTVpWh.js",
"sha256": "1a72d79e1b27a405937cf75cdd346eb85637a58094aff92510b7571da7d4fd89",
"rawBytes": 2701,
"gzipBytes": 1345,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-H6hY7acD.js",
"sha256": "3a6235438d4b7d9c5453be34e058c55d9689497f06a57936eeb5fd54e35a2606",
"rawBytes": 72959,
"gzipBytes": 20244,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-D_y3mgKZ.js",
"houseplan-assets/houseplan-card-C4IYYoj_.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DCKoIOD4.js",
"houseplan-assets/summary-panel-metrics-CUfTVpWh.js"
]
},
{
"path": "houseplan-assets/support-de-BqFx0CHP.js",
"sha256": "ed78bc671d0ebb1fdb8361727a8d46f35833724c6d249e6e6a86ee8a08d05b36",
"rawBytes": 6167,
"gzipBytes": 2396,
"path": "houseplan-assets/tools-ru-CHiUK5GX.js",
"sha256": "1094d8aa0558c4c44d85542b039849b9ae2eee02237e62167090adb25458d3f4",
"rawBytes": 5633,
"gzipBytes": 1696,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-DVw_3-2z.js",
"sha256": "3c2fbb65c36ff21f82f0f63c0d0dba50bb893882ffd2d1fa95f4109970b65151",
"rawBytes": 6333,
"gzipBytes": 2432,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-BChLp-Eg.js",
"sha256": "a80ca80232a416088619659c493b4ceef679f32c237af91ed3d6edb9215154dd",
"rawBytes": 8376,
"gzipBytes": 2876,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-de-Csx831lO.js",
"sha256": "5269d69a3c84a888b393bcdbeea1f629022c73e28e70dd77f32a145d9164fc05",
"path": "houseplan-assets/topology-de-k0TFglWi.js",
"sha256": "035fca1ef192f938d26389582f004ab4d7fc56b1d3206ac9403b144c662c6f59",
"rawBytes": 2698,
"gzipBytes": 1335,
"gzipBytes": 1336,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-fr-Dw79rb1j.js",
"sha256": "e5dafb98252ff7f8cb9c688ebefe1e2fbad55f5af977696fc1cf2d70c726e61e",
"path": "houseplan-assets/topology-fr-DEOQrFrw.js",
"sha256": "4458edd752d14cbad5abc2a06583d7c95b6578f4400aef79ebd8dbdeae495b29",
"rawBytes": 2718,
"gzipBytes": 1355,
"gzipBytes": 1356,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-ru-C4aeCsnx.js",
"sha256": "c9a05a55c38f9580d9efd67676f6efb95e53c46ffdeaabad5be8a553d5121db8",
"path": "houseplan-assets/topology-ru-DNMInWXF.js",
"sha256": "23b61e950284f1740f721f45aa0427725cb3a71e9207a9a1a3655c8e78868558",
"rawBytes": 3669,
"gzipBytes": 1616,
"isEntry": false,
@@ -384,53 +546,53 @@
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-Dq_fGFqf.js",
"sha256": "e5cfbcaeb11a6792d3996daddb97787ddcfe3e8f61705b517b81e1169abd6afe",
"path": "houseplan-assets/zigbee-topology-DKrDE5nD.js",
"sha256": "2b290615fba59c711cb404ef21dd8f092449a72de6aaed7be0f2e36a949200ea",
"rawBytes": 10958,
"gzipBytes": 4299,
"gzipBytes": 4303,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/namespace-language-DePztGEB.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/namespace-language-CRySE1JG.js"
],
"dynamicImports": [
"houseplan-assets/topology-de-Csx831lO.js",
"houseplan-assets/topology-fr-Dw79rb1j.js",
"houseplan-assets/topology-ru-C4aeCsnx.js"
"houseplan-assets/topology-de-k0TFglWi.js",
"houseplan-assets/topology-fr-DEOQrFrw.js",
"houseplan-assets/topology-ru-DNMInWXF.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-runtime-BTxO2PCB.js",
"sha256": "dad08695022fac2a019f824c3b33410dfd9335bba05357efb1970d723a13ae0a",
"path": "houseplan-assets/zigbee-topology-runtime-Dzr8mpaZ.js",
"sha256": "9aa1fd88eae914fb7c028d2711622dfecedb6b5ce866589859dd7a044677e2a2",
"rawBytes": 3983,
"gzipBytes": 1790,
"gzipBytes": 1791,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js",
"houseplan-assets/namespace-language-DePztGEB.js",
"houseplan-assets/zigbee-topology-Dq_fGFqf.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js",
"houseplan-assets/namespace-language-CRySE1JG.js",
"houseplan-assets/zigbee-topology-DKrDE5nD.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "782d4e30ab546275186d6e4740a98d5f6d36d69e3795fe364877a79044973e4d",
"sha256": "4747a2101180424ab8ea605cca8c7175471e2c4c4866278ecf3af7f8f7aaa6a0",
"rawBytes": 1205,
"gzipBytes": 804,
"gzipBytes": 803,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "947a4064428d5670d4057dd268ec3e7f32a931792192af0c38cec653d6aafe26",
"sha256": "6936a8b9524be66a9e43625a3b80ab19f428a9138ca916da433eafec833acf4d",
"rawBytes": 6165,
"gzipBytes": 2327,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-C4IYYoj_.js"
"houseplan-assets/houseplan-card-DE8tFhlR.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";import{aD as e,l as o,aG as t,ey as s,A as a,ez as i,eA as l,eB as n,E as r,c}from"./houseplan-card-C4IYYoj_.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{aG as e,l as o,aJ as t,eD as s,A as a,eE as i,eF as l,eG as n,E as r,c}from"./houseplan-card-DE8tFhlR.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";import{l as t,bO as s,A as e,aG as o,c as i,m as a,cY as l,cZ as h,c_ as n,bK as r,c$ as c,d4 as p,d0 as d,d1 as _,d2 as u,d3 as g,d5 as m,d6 as b}from"./houseplan-card-C4IYYoj_.js";import{z as f,A as v,C as y,D,E as w,G as S,H as $,I as C,n as k,S as M}from"./backdrop-pick-BE-iJ7rt.js";import{s as I}from"./namespace-language-DePztGEB.js";import"./form-kit.styles-D_y3mgKZ.js";const x="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5",T=I([M]);class F{constructor(t){this.host=t,this.languageRuntime=T,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(a){const l=`${a}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,a)&&s(h,l)?i`<hp-help data-help-key=${a}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{l as t,bS as s,A as e,aJ as o,c as i,m as a,d1 as l,d2 as h,d3 as n,bO as r,d4 as c,d9 as p,d5 as d,d6 as _,d7 as u,d8 as g,da as m,db as b}from"./houseplan-card-DE8tFhlR.js";import{z as f,A as v,C as y,D,E as w,G as S,H as $,I as C,n as M,S as k}from"./backdrop-pick-B3E3pF2p.js";import{s as I}from"./namespace-language-CRySE1JG.js";import"./form-kit.styles-7MsRKJCO.js";const x="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa",T=I([k]);class F{constructor(t){this.host=t,this.languageRuntime=T,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(a){const l=`${a}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,a)&&s(h,l)?i`<hp-help data-help-key=${a}
.text=${o(h,a)} .ariaLabel=${o(h,l)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=a(e),i=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?l(e.settings.custom_fill):null,p="none"===o.fill?{...i||h,a:0}:i;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:c(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:p,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:r({},e.settings),northDegInput:null===r({},e.settings)?"":String(r({},e.settings)),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,tempMinInput:String(o.tempMin),tempMaxInput:String(o.tempMax),showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:n(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=p(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...m(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,northDegInput:"",sunRays:null,tempMin:_,tempMax:d,tempMinInput:String(_),tempMaxInput:String(d),showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:n(e,this.host._imperial),cellCmTouched:!1,busy:!1},f(this.host,this.host._spaceDialog)}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await v(this.host,e);o&&this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:o})}_renderBackdropGuard(){return y(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){const s=this.host._spaceDialog,e=s?.saved?.find(s=>s.name===t);if(!s||!e||e.used_by.length||e.url===s.planUrl)return;const o=await this.host._confirmDanger({key:"delete-plan",kind:"destructive",title:this.host._t("confirm.delete_plan_title"),message:this.host._t("confirm.delete_plan_body"),objectName:t,confirmLabel:this.host._t("btn.delete"),cancelLabel:this.host._t("btn.cancel")}),i=this.host._spaceDialog,a=i?.saved?.find(s=>s.name===t);if(o&&i&&a&&a.url===e.url&&a.modified===e.modified&&!a.used_by.length&&a.url!==i.planUrl)try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return i`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return i`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return i`<div class="savedplans">
${s.map(s=>{return i`
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
@@ -38,7 +38,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5
</div>
</hp-dialog>`}_rangeInput(t,s,o,a,l,h=!1,n){return i`<input type="range" min=${t} max=${s} step=${o} .value=${String(a)}
?disabled=${h} aria-label=${n??e}
@input=${t=>l(Number(t.target.value))} />`}_renderSpaceDialog(){const t=k({host:this.host,idPrefix:"onboarding-space",help:this._help.bind(this),rangeInput:(t,s,e,o,i,a,l)=>this._rangeInput(t,s,e,o,i,a,l),pickPlanFile:t=>this._pickPlanFile(t),toggleServerPlans:()=>this._toggleServerPlans(),renderServerPlans:t=>this._renderServerPlans(t),save:()=>this._saveSpaceDialog(),skipImport:()=>this._skipImport()});return i`<hp-dialog .hass=${this.host.hass} data-kind="onboarding" form-shell wide
@input=${t=>l(Number(t.target.value))} />`}_renderSpaceDialog(){const t=M({host:this.host,idPrefix:"onboarding-space",help:this._help.bind(this),rangeInput:(t,s,e,o,i,a,l)=>this._rangeInput(t,s,e,o,i,a,l),pickPlanFile:t=>this._pickPlanFile(t),toggleServerPlans:()=>this._toggleServerPlans(),renderServerPlans:t=>this._renderServerPlans(t),save:()=>this._saveSpaceDialog(),skipImport:()=>this._skipImport()});return i`<hp-dialog .hass=${this.host.hass} data-kind="onboarding" form-shell wide
.title=${t.title} .badge=${t.badge} icon="mdi:floor-plan" @hp-close=${t.requestClose}>
${t.body}${t.footer}
</hp-dialog>`}}export{F as HouseplanOnboardingRuntime,T as ONBOARDING_LANGUAGE_RUNTIME,x as ONBOARDING_RUNTIME_FINGERPRINT};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={"led.tool":"LED-Streifen","led.hint":"Zeichnen Sie den Streifen Punkt für Punkt entlang der Wandflächen oder über den Boden; er rastet am Raster und an Wandflächen ein. Danach wählen Sie das Gerät, das er darstellt.","led.start":"Klicken, um den Streifen zu beginnen","led.start_touch":"Tippen, um den Streifen zu beginnen","led.points":"Punkte: {n} · Umschalt — 45°-Schritte · Esc — fertig · Strg+Z — Punkt entfernen","led.points_touch":"Punkte: {n} · Werkzeug wechseln zum Beenden","led.finish":"Fertig","led.stopped":"Der Streifen stößt an eine Wand","led.unbound":"Streifen ist mit keinem Gerät verbunden","led.bind":"Gerät verbinden","led.change":"Gerät wechseln","led.later":"Später","led.unbind":"Trennen","led.delete":"Streifen löschen","led.settings":"Geräteeinstellungen","led.type_note":"LED-Streifen: Form im Geräte-Editor auf dem Plan bearbeiten","led.show_strip":"Als LED-Streifen zeigen","led.show_icon":"Als Symbol zeigen","led.cancel":"Abbrechen","led.taken":"Bereits mit einem anderen LED-Streifen verbunden","led.space":"Zuerst den LED-Streifen lösen: Er gehört zu seinem Bereich","led.limit_points":"Höchstens 50 Punkte pro Streifen","led.limit_strips":"Höchstens 50 Streifen pro Bereich","led.pick_title":"Gerät für den Streifen wählen","led.pick_new":"Neues Gerät…","led.pick_empty":"Auf diesem Plan gibt es noch keine freien Geräte.","led.saved":"LED-Streifen gespeichert","led.save_failed":"Streifen nicht gespeichert: {err}","led.history_draw":"LED-Streifen","led.history_shape":"Form des LED-Streifens","led.history_view":"Darstellung des Geräts","led.optimize_walls":"Streifen durch Wände: {n} ({spaces})"};const n="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,n as fingerprint};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={"led.tool":"Ruban LED","led.hint":"Tracez le ruban point par point le long des faces des murs ou sur le sol ; il s’aimante à la grille et aux faces des murs. Ensuite, choisissez l’appareil qu’il représente.","led.start":"Cliquez pour commencer le ruban","led.start_touch":"Touchez pour commencer le ruban","led.points":"Points : {n} · Maj — pas de 45° · Échap — terminer · Ctrl+Z — retirer le point","led.points_touch":"Points : {n} · changez d’outil pour terminer","led.finish":"Terminer","led.stopped":"Le ruban bute contre un mur","led.unbound":"Ruban non lié à un appareil","led.bind":"Lier un appareil","led.change":"Changer d’appareil","led.later":"Plus tard","led.unbind":"Délier","led.delete":"Supprimer le ruban","led.settings":"Réglages de l’appareil","led.type_note":"Ruban LED : modifiez sa forme sur le plan dans l’éditeur d’appareils","led.show_strip":"Afficher en ruban LED","led.show_icon":"Afficher en icône","led.cancel":"Annuler","led.taken":"Déjà lié à un autre ruban LED","led.space":"Détachez d’abord le ruban LED : il appartient à son espace","led.limit_points":"50 points au maximum par ruban","led.limit_strips":"50 rubans au maximum par espace","led.pick_title":"Choisissez l’appareil du ruban","led.pick_new":"Nouvel appareil…","led.pick_empty":"Aucun appareil libre sur ce plan pour l’instant.","led.saved":"Ruban LED enregistré","led.save_failed":"Ruban non enregistré : {err}","led.history_draw":"Ruban LED","led.history_shape":"Forme du ruban LED","led.history_view":"Représentation de l’appareil","led.optimize_walls":"Rubans traversant des murs : {n} ({spaces})"};const a="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,a as fingerprint};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={"led.tool":"LED-лента","led.hint":"Нарисуйте ленту по точкам вдоль граней стен или по полу — она прилипает к сетке и граням. После завершения выберите устройство, которое она показывает.","led.start":"Нажмите, чтобы начать ленту","led.start_touch":"Коснитесь, чтобы начать ленту","led.points":"Точек: {n} · Shift — шаг 45° · Esc — завершить · Ctrl+Z — убрать точку","led.points_touch":"Точек: {n} · смените инструмент, чтобы завершить","led.finish":"Завершить","led.stopped":"Лента упёрлась в стену","led.unbound":"Лента не привязана к устройству","led.bind":"Привязать устройство","led.change":"Сменить устройство","led.later":"Позже","led.unbind":"Отвязать","led.delete":"Удалить ленту","led.settings":"Настройки устройства","led.type_note":"LED-лента: форма редактируется на плане в редакторе устройств","led.show_strip":"Показывать LED-лентой","led.show_icon":"Показывать значком","led.cancel":"Отмена","led.taken":"Уже привязано к другой LED-ленте","led.space":"Сначала отвяжите LED-ленту: она принадлежит своему пространству","led.limit_points":"Не более 50 точек в ленте","led.limit_strips":"Не более 50 лент в пространстве","led.pick_title":"Выберите устройство для ленты","led.pick_new":"Новое устройство…","led.pick_empty":"На этом плане пока нет свободных устройств.","led.saved":"LED-лента сохранена","led.save_failed":"Лента не сохранена: {err}","led.history_draw":"LED-лента","led.history_shape":"Форма LED-ленты","led.history_view":"Вид устройства","led.optimize_walls":"Лент, проходящих сквозь стены: {n} ({spaces})"};const d="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,d as fingerprint};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,30 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{eT as e,az as t,eO as s,A as i,eU as n,e as a,i as r,E as l}from"./houseplan-card-DE8tFhlR.js";import{i as o,m as c,k as p}from"./led-strip-geometry-b-NVYCNs.js";const h=a(class extends r{constructor(e){super(e),this.deps=null}render(e,t){return t()}update(e,[t,s]){return this.deps&&this.deps.length===t.length&&t.every((e,t)=>e===this.deps[t])?l:(this.deps=t,s())}}),d="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa",u=12,f=e=>e.map(e=>[e[0],e[1]]);class ${constructor(e=50){this.limit=e,this.entries=new Map,this.space="",this.recomputes=0,this.ids=new Map}get size(){return this.entries.size}id(e){let t=this.ids.get(e);return null==t&&(t=this.ids.size+1,this.ids.set(e,t)),t}forSpace(e){e!==this.space&&(this.entries.clear(),this.ids.clear(),this.space=e)}read(e,t){if(this.entries.has(e)){const t=this.entries.get(e)??null;return this.entries.delete(e),this.entries.set(e,t),t}this.recomputes+=1;const s=t();for(this.entries.set(e,s);this.entries.size>this.limit;){const e=this.entries.keys().next().value;if(void 0===e)break;this.entries.delete(e)}return s}clear(){this.entries.clear(),this.ids.clear(),this.space=""}get sources(){let e=0;for(const t of this.entries.values())for(const s of t?.pieces||[])e+=s.clip?.length??0;return e}}const g=e=>{const t=Math.round(1e4*e)/1e4;return Object.is(t,-0)?"0":String(t)},m=e=>`${e.map((e,t)=>`${t?"L":"M"}${g(e[0])} ${g(e[1])}`).join(" ")} Z`;function y(e,t,s){const i=1.01*t;return s.occluders.some(t=>t?.length>=4&&e.some(e=>((e,t)=>{const s=t[2]-t[0],i=t[3]-t[1],n=s*s+i*i,a=n?Math.max(0,Math.min(1,((e[0]-t[0])*s+(e[1]-t[1])*i)/n)):0;return Math.hypot(e[0]-t[0]-a*s,e[1]-t[1]-a*i)})(e,t)<i))?e.map(e=>n([e[0],e[1]],t,s.occluders,12)).filter(e=>e.length>=3).map(m):null}const M=new WeakMap;function x(e){const t=o(f(e.points));if(t.length<2||!(e.radius>0))return null;const s=e.radius,i=[];let n=[t[0]],a=s;for(let e=1;e<t.length;e++){let r=t[e-1];const l=t[e];let o=Math.hypot(l[0]-r[0],l[1]-r[1]);for(;o>a+1e-12;){const e=a/o,t=[r[0]+(l[0]-r[0])*e,r[1]+(l[1]-r[1])*e];n.push(t),i.push(n),n=[t],r=t,o-=a,a=s}n.push(l),a-=o}n.length>1&&i.push(n);const r=[];let l=1/0,p=1/0,h=-1/0,d=-1/0;for(const t of i){const i=[];for(const n of c(t,e.faces,s/4)){const e=i[i.length-1];(!e||Math.hypot(n[0]-e[0],n[1]-e[1])>=s/4)&&i.push(n)}if(!i.length)continue;let n;try{n=y(i,s,e.scene)}catch{continue}if(!n||n.length){r.push({d:t.map((e,t)=>`${t?"L":"M"}${e[0]} ${e[1]}`).join(" "),clip:n});for(const e of t)l=Math.min(l,e[0]),p=Math.min(p,e[1]),h=Math.max(h,e[0]),d=Math.max(d,e[1])}}return r.length?{pieces:r,box:{x:l-s,y:p-s,w:h-l+2*s,h:d-p+2*s}}:null}function w(t){const s=100*Math.max(0,Math.min(1,t));for(let t=1;t<e.length;t++){const[i,n]=e[t-1],[a,r]=e[t];if(s<=a)return n+(r-n)*(s-i)/Math.max(1e-9,a-i)}return 0}const b=new WeakMap;function k(e){b.get(e)?.clear(),b.delete(e)}function v(e){const t=b.get(e);return{visibility:t?.size??0,sources:t?.sources??0,recomputes:t?.recomputes??0}}function I(e){let t=b.get(e);return t||(t=new $,b.set(e,t)),t}function j(e){if(!e.scene)return t``;const n=I(e.owner);n.forSpace(e.spaceId);const a=e.scene,r=e.views.flatMap(t=>{if(!t.glow||"unavailable"===t.state)return[];const s=`${t.strip.id}|${i=t.strip.points,i.map(e=>`${e[0].toFixed(5)},${e[1].toFixed(5)}`).join(";")}|${t.radius.toFixed(5)}|${a.fingerprint}`;var i;const r=n.read(s,()=>x({points:t.strip.points,radius:t.radius,scene:a,polygons:e.polygons,faces:e.faces,spaceId:e.spaceId}));return r?[{view:t,geometry:r}]:[]});if(!r.length)return t``;const l=Array.from({length:12},(e,t)=>{const s=1-t/12;return{half:s,value:w((s+(1-(t+1)/12))/2)}});return t`<g class="led-fields" pointer-events="none" aria-hidden="true"
data-led-cache="${n.size}" data-led-recomputes="${n.recomputes}">
<defs><clipPath id="hp-led-floor">${function(e){let t=M.get(e);return t||(t=e.floor.filter(e=>e.length>=3).map(m),M.set(e,t)),t}(a).map(e=>t`<path d="${e}"></path>`)}</clipPath></defs>
<g clip-path="url(#hp-led-floor)">
${s(r,({view:e})=>e.strip.id,({view:e,geometry:s})=>{const a=`${n.id(e.strip.id)}`,r=e.radius,o="on"===e.state&&!!e.appearance,c=s.box,d=p(f(e.strip.points)),u=s.pieces.flatMap((e,t)=>e.clip?[{...e,clip:e.clip,clipId:t}]:[]),$=s.pieces.filter(e=>!e.clip).map(e=>e.d).join(" "),g=[...u,...$?[{d:$,clip:null,clipId:-1}]:[]];return h([s,o,e.appearance?.c,e.appearance?.alpha,r,a],()=>t`<g class="glow-spot led-field ${o?"":"is-leaving"}" data-led-field="${e.strip.id}"
data-pieces="${s.pieces.length}" data-closed="${d?"true":"false"}">
<defs>
${u.map(e=>t`<clipPath id="hp-led-clip-${a}-${e.clipId}">
${""}
<path d="${e.clip.join(" ")}"></path>
</clipPath>`)}
<mask id="hp-led-mask-${a}" maskUnits="userSpaceOnUse"
x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}"
color-interpolation="sRGB" style="mask-type:luminance">
<rect x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}" fill="black"></rect>
<g style="isolation:isolate">
${g.map(e=>t`<g style="mix-blend-mode:lighten"
clip-path=${e.clip?`url(#hp-led-clip-${a}-${e.clipId})`:i}>
${l.map(s=>t`<path d="${e.d}" fill="none" stroke="${(e=>{const t=Math.round(255*Math.max(0,Math.min(1,e)));return`rgb(${t},${t},${t})`})(s.value)}"
stroke-width="${2*s.half*r}" stroke-linecap="round" stroke-linejoin="round"></path>`)}
</g>`)}
</g>
</mask>
</defs>
<rect class="glow-pool led-pool" x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}"
fill="${e.appearance?.c??"transparent"}" fill-opacity="${(e.appearance?.alpha??0).toFixed(4)}"
mask="url(#hp-led-mask-${a})"></rect>
</g>`)})}
</g>
</g>`}export{u as LED_FIELD_BANDS,d as LED_FIELD_FINGERPRINT,$ as LedFieldCache,x as buildFieldGeometry,w as falloffAt,I as ledFieldCache,v as ledFieldStats,k as releaseLedField,j as renderLedField};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{et as e,eB as t,c6 as o,eI as s,eJ as i,ad as n,ed as a,O as r,eK as l,az as d,ee as c,aL as p,eL as u,eM as h,eN as f,A as m,eO as v}from"./houseplan-card-DE8tFhlR.js";import{L as g,v as w,s as $,a as _,p as y,b as k,c as b,d as P,e as M,f as C}from"./led-strip-geometry-b-NVYCNs.js";const F="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";let L,x=null,U=null;function D(e,t){return x||U||L===e||""===L||(U=(void 0===L?import("./led-strip-field-CJ4ziktu.js"):import(new URL(`./led-strip-field-CJ4ziktu.js?${Date.now()}`,import.meta.url).href)).then(e=>{e.LED_FIELD_FINGERPRINT===s?x=e:L=""},()=>{L=e}).finally(()=>{U=null})),x||U?.then(()=>x&&t()),x}const R=e=>e.some(e=>e.glow&&"on"===e.state),E=e=>e.map(e=>[e[0],e[1]]),A="#383838";function I(s,i){if(!s)return null;const n=Array.isArray(s.masonryGeometry)?s.masonryGeometry:[],a=[...e(s.masonryGeometry),...s.opaqueBodies],r=[];for(const e of a)for(let t=0;t<e.length;t++){const o=e[t],s=e[(t+1)%e.length];o&&s&&r.push({a:[o[0],o[1]],b:[s[0],s[1]]})}const l=25,d=new Map;r.forEach((e,t)=>{const o=Math.floor(Math.min(e.a[0],e.b[0])/l),s=Math.floor(Math.max(e.a[0],e.b[0])/l),i=Math.floor(Math.min(e.a[1],e.b[1])/l),n=Math.floor(Math.max(e.a[1],e.b[1])/l);for(let e=o;e<=s;e++)for(let o=i;o<=n;o++){const s=`${e},${o}`,i=d.get(s);i?i.push(t):d.set(s,[t])}});const c=e=>{let t=1/0,o=1/0,s=-1/0,i=-1/0;for(const n of e)t=Math.min(t,n[0]),o=Math.min(o,n[1]),s=Math.max(s,n[0]),i=Math.max(i,n[1]);return[t,o,s,i]},p=(e,t)=>e[0]>=t[0]&&e[0]<=t[2]&&e[1]>=t[1]&&e[1]<=t[3],u=n.filter(e=>e?.[0]?.length).map(e=>({box:c(e[0]),poly:e})),h=s.opaqueBodies.map(e=>({box:c(e),body:e}));return{faces:r,inside:e=>u.some(({box:o,poly:s})=>p(e,o)&&t([e[0],e[1]],[s]))||h.some(({box:t,body:s})=>p(e,t)&&o([e[0],e[1]],s)),epsilon:i,near:e=>{const t=new Set;for(let o=Math.floor(e[0]/l);o<=Math.floor(e[2]/l);o++)for(let s=Math.floor(e[1]/l);s<=Math.floor(e[3]/l);s++)for(const e of d.get(`${o},${s}`)||[])t.add(e);return[...t].map(e=>r[e])}}}function T(e){const t=e.candidate?.sourceEid||e.device.primary||"",o=t?e.hass?.states?.[t]?.state:void 0,s="unavailable"===o||"unknown"===o||!!t&&!!e.hass&&!e.hass.states?.[t]&&!e.device.virtual,i=Number(e.device.marker?.glow_radius_cm),n=Number.isFinite(i)&&i>0?i/e.cellCm*e.gridPitch:e.defaultRadius,a=s?null:e.candidate?.appearance??null;return{strip:e.strip,device:e.device,state:s?"unavailable":a?"on":"off",glow:e.glow,appearance:a,radius:n}}function j(e,t){const o=function(e,t){return("on"===e.state?M:C)*t}(e,t.d);return{t:o,path:b(E(e.strip.points),t.faces,o/2)}}function B(e){if(!e.views.length)return d``;const t=e.handlers,o=[...e.views].sort((e,t)=>e.strip.id<t.strip.id?1:e.strip.id>t.strip.id?-1:0),s=e.iso;return d`<g class="led-strips" data-hp-led-strips="${o.length}"
data-hp-iso=${s?"raised":m}>
${s?d`<defs><filter id="hp-led-iso-shadow" filterUnits="userSpaceOnUse"
x="-100000" y="-100000" width="200000" height="200000">
<feGaussianBlur stdDeviation="${s.shadow.sigma}"></feGaussianBlur></filter></defs>`:m}
<style>
.led-strip .led-focus { fill: none; stroke: transparent; }
.led-strip:focus-within .led-focus { stroke: var(--primary-color, #03a9f4); }
.led-strip .led-hit { fill: none; stroke: transparent; cursor: pointer; outline: none; }
</style>
${v(o,e=>e.strip.id,o=>{const{t:i,path:n}=j(o,e),a=y(n),r="unavailable"===o.state,l=r?"#9e9e9e":"on"===o.state&&!o.glow&&o.appearance?o.appearance.c:"#FFFFFF",c=2*k(i*e.perUnit)/(e.perUnit||1),p=o.device,u=t=>function(e,t,o){const s=e.currentTarget,i=s?.getScreenCTM?.();if(!i||o.views.length<2)return t;const n=i.inverse(),a=[n.a*e.clientX+n.c*e.clientY+n.e,n.b*e.clientX+n.d*e.clientY+n.f],r=o.views.map(e=>{const{t:t,path:s}=j(e,o);return{id:e.strip.id,points:s.points.map(e=>[e[0]*o.perUnit,e[1]*o.perUnit]),closed:s.closed,thicknessPx:t*o.perUnit}}),l=P([a[0]*o.perUnit,a[1]*o.perUnit],r);return o.views.find(e=>e.strip.id===l)||t}(t,o,e).device,h=s?`translate(0 ${-s.lift})`:m;return d`<g class="led-strip state-${o.state}" data-led-strip="${o.strip.id}"
data-marker="${p.id}" data-state="${o.state}" data-closed="${n.closed?"true":"false"}">
${s?d`<path class="led-iso-shadow" d="${a}" fill="none" stroke="black"
stroke-opacity="${s.shadow.opacity}" stroke-width="${i}" stroke-linecap="round"
stroke-linejoin="round" filter="url(#hp-led-iso-shadow)" pointer-events="none"
transform="translate(${s.shadow.dx} ${s.shadow.dy-s.lift})"></path>
${[1,.5].map(e=>d`<path class="led-iso-edge" d="${a}" fill="none" stroke="${s.edge}"
stroke-width="${i}" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"
transform="translate(0 ${-s.lift+s.depth*e})"></path>`)}`:m}
<g transform=${h}>
<path class="led-outline" d="${a}" fill="none" stroke="${A}" stroke-width="${i}"
stroke-linecap="round" stroke-linejoin="round"
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
<path class="led-core" d="${a}" fill="none" stroke="${l}" stroke-width="${i/2}"
stroke-linecap="round" stroke-linejoin="round"
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
<path class="led-focus" d="${a}" stroke-width="${1.6*i}" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true"></path>
${t?d`<path class="led-hit" d="${a}" stroke-width="${c}"
stroke-linecap="round" stroke-linejoin="round" pointer-events="stroke"
role="button" tabindex="0" aria-label="${t.label(p)}"
data-hp="device" data-id="${p.id}"
@click=${e=>t.click(e,u(e))}
@keydown=${e=>t.keydown(e,p)}
@contextmenu=${e=>t.contextmenu(e,u(e))}
@pointerdown=${e=>t.pointerdown(e,u(e))}
@pointermove=${e=>t.pointermove(e,u(e))}
@pointerup=${e=>t.pointerup(e,u(e))}
@pointercancel=${e=>t.pointercancel(e,p)}
@lostpointercapture=${e=>t.pointercancel(e,p)}
@pointerover=${e=>t.pointerover(e,u(e))}
@pointerleave=${()=>t.pointerleave()}
@focus=${e=>t.focus(e,p)}
@blur=${()=>t.blur(p)}></path>`:m}
</g>
</g>`})}
</g>`}function G(e,t,o,s){return(t?e.find(e=>e.id===t):void 0)??(o?e.find(e=>s(o,e)):void 0)}function H(e){const t=new Map(e.devices.map(e=>[e.id,e])),o=g/e.cellCm*e.gridPitch,s=new Map(i({hass:e.hass,devices:e.devices,virtualLights:e.virtualLights,spaceId:e.space.id,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,defaultRadiusUnits:o,cellCm:e.cellCm,gridPitch:e.gridPitch,position:()=>({x:0,y:0})}).map(e=>[e.key,e])),r=[];for(const i of e.space.led_strips||[]){if(!1===i?.active||!i.marker||!w(i.points))continue;const a={...i,points:i.points.map(e=>[e[0]*n,e[1]*n])},l=t.get(a.marker);if(!l||l.space!==e.space.id)continue;if(l.hidden&&!e.showHidden)continue;const d=G(e.space.rooms,l.marker?.room_id,$(E(a.points)),e.inRoom);r.push(T({strip:a,device:l,candidate:s.get(`${e.space.id}|${l.id}`)??null,hass:e.hass,glow:!!d&&e.glowFor(d),defaultRadius:o,cellCm:e.cellCm,gridPitch:e.gridPitch}))}const l=_/e.cellCm*e.gridPitch;return{views:r,faces:I(e.scene,l),d:e.iconPct/100*a(e.space),scene:e.scene,polygons:e.polygons}}const N=new WeakMap;function S(e){N.delete(e),x?.releaseLedField(e)}function q(e){return{shapes:N.get(e)?.frame.views.length??0,...x?.ledFieldStats(e)??{visibility:0,sources:0,recomputes:0}}}function O(e,t,o){const s=t.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),i=s.length?e._lightBarriers(t,s.map(({room:e,poly:t})=>({r:e,poly:t}))):null,n=[t,e._renderDevices,e._renderPlanHass,o,i,e._mode,e._showAll],a=N.get(e);if(a&&a.key.every((e,t)=>e===n[t]))return a.frame;const d=e._config?.icon_size??2.5,c=H({space:t,devices:e._renderDevices,hass:e._renderPlanHass,virtualLights:e._virtualLights,defaultColor:e._fillColors.glow_light.c,paletteAlpha:e._fillColors.glow_light.a,cellCm:e._cellCm,gridPitch:e._gridPitch,iconPct:d>8?2.5:d,scene:i,polygons:s,glowFor:e=>l(o,e),inRoom:(t,o)=>e._pointInRoom(t,o),showHidden:"devices"===e._mode&&e._showAll});return!1!==e.isConnected&&N.set(e,{key:n,frame:c}),c}function z(e,t,o,s){const i=O(e,t,o),n=e._stageEl?.clientWidth,a="iso"===e._renderProjection&&"view"===e._mode,r=a?i.d*c:i.d,l="theme-dark"===p(e._renderPlanHass)?"dark":"light",m=!!e._isoLightFloors?.size,v=u(l,m),g=B({views:i.views,d:r,iso:a?{lift:f.lift*r,depth:f.depth*r,edge:h(A,l,m),shadow:{dx:v.dx*r,dy:v.dy*r,sigma:v.sigma*r,opacity:v.opacityWhite}}:null,faces:i.faces,perUnit:n&&s.w?n/s.w:1,handlers:"view"===e._mode?{click:(t,o)=>e._clickDevice(t,o),keydown:(t,o)=>e._keyDevice(t,o),contextmenu:(t,o)=>e._ctxDevice(t,o),pointerdown:(t,o)=>e._pointerDown(t,o),pointermove:(t,o)=>{const s=e._pointerMove(t,o);s&&e._showDeviceTip(t,s)},pointerup:(t,o)=>e._pointerUp(t,o),pointercancel:(t,o)=>e._pointerCancel(t,o),pointerover:(t,o)=>e._showDeviceTip(t,o),pointerleave:()=>e._clearPointerHover(),focus:(t,o)=>e._showDeviceFocusTip(t,o),blur:t=>e._hideDeviceFocusTip(t.id),label:e=>e.name}:null});return"plan"===e._mode||"decor"===e._mode?d`<g class="led-passive" opacity="0.45" pointer-events="none">${g}</g>`:g}function W(e,t,o){if(!1===e.isConnected)return d``;const s=O(e,t,o),i=s.scene&&R(s.views)?D(t.id,()=>!1!==e.isConnected&&e.requestUpdate()):null;return i?i.renderLedField({views:s.views,scene:s.scene,polygons:s.polygons,faces:s.faces,spaceId:t.id,owner:e}):d``}function Y(e){const t=e.space.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),o=H({space:e.space,devices:e.devices,hass:e.hass,virtualLights:e.virtualLights,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,cellCm:e.cellCm,gridPitch:e.gridPitch,iconPct:e.iconPct,scene:e.scene,polygons:t,glowFor:e.glowFor,inRoom:e.inRoom,showHidden:!1}),s=e.live?o.views:o.views.map(e=>({...e,state:"off",appearance:null,glow:!1})),i=I({masonryGeometry:e.bodies.masonryGeometry,opaqueBodies:e.bodies.opaqueBodies},_/e.cellCm*e.gridPitch),n=e.scene&&e.live&&R(s)?D(e.space.id,e.ready):null;return d`${n?n.renderLedField({views:s,scene:e.scene,polygons:t,faces:i,spaceId:e.space.id,owner:e.owner}):m}${B({views:s,d:o.d,faces:i,perUnit:e.perUnit,handlers:null})}`}export{F as LED_RUNTIME_FINGERPRINT,I as faceContext,D as ledField,H as ledFrame,O as ledFrameFor,q as ledStats,T as ledStripView,S as releaseLed,W as renderLedFieldFor,z as renderLedLayerFor,B as renderLedStripes,Y as renderStaticLed,G as stripRoom};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";import{eD as e,eE as n,eA as a}from"./houseplan-card-C4IYYoj_.js";function d(a,d){return new e([{code:"en",dictionary:a},{code:"ru",loadDictionary:d.ru},{code:"de",loadDictionary:d.de},{code:"fr",loadDictionary:d.fr}],"d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5",console.warn,n)}function o(e){return function(e,n){return{state:a=>{const d=e.state(a);return"pending"!==d&&n.some(e=>"pending"===e.state(a))?"pending":d},dictionary:n=>e.dictionary(n),ensure:a=>Promise.all([e,...n].map(e=>e.ensure(a))).then(()=>{})}}(a,e)}export{d as n,o as s};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{eQ as e,eR as n,eF as a}from"./houseplan-card-DE8tFhlR.js";function d(a,d){return new e([{code:"en",dictionary:a},{code:"ru",loadDictionary:d.ru},{code:"de",loadDictionary:d.de},{code:"fr",loadDictionary:d.fr}],"169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa",console.warn,n)}function o(e){return function(e,n){return{state:a=>{const d=e.state(a);return"pending"!==d&&n.some(e=>"pending"===e.state(a))?"pending":d},dictionary:n=>e.dictionary(n),ensure:a=>Promise.all([e,...n].map(e=>e.ensure(a))).then(()=>{})}}(a,e)}export{d as n,o as s};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{aG as e,l as t,aJ as s,A as a,eE as o,eF as i,eG as n,E as l,eP as c,c as r}from"./houseplan-card-DE8tFhlR.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return t(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"fit",selector:{select:{mode:"dropdown",options:[{value:"content",label:s(this._lang,"editor.fit_content")},{value:"house",label:s(this._lang,"editor.fit_house")}]}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"light_pools",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}}]}render(){if(!this.hass||!this._config)return a;const e=o(this,i,t(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return l;this._loadSpaces();const h=this._lang,_={space:s(h,"editor.space"),title:s(h,"editor.title"),fit:s(h,"editor.framing"),show_button:s(h,"editor.show_button"),button_label:s(h,"editor.button_label"),button_target:s(h,"editor.button_target"),icon_size:s(h,"editor.icon_size"),show_temperature:s(h,"editor.show_temperature"),live_states:s(h,"editor.live_states"),light_pools:s(h,"editor.light_pools"),show_signal:s(h,"editor.show_signal")};return r`<ha-form
.hass=${this.hass}
.data=${{...this._config,fit:c(this._config.fit)}}
.schema=${this._schema}
.computeLabel=${e=>_[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`}_valueChanged(e){const t={...this._config||{},...e.detail.value};delete t.aspect_ratio;const s=new Event("config-changed",{bubbles:!0,composed:!0});s.detail={config:t},this.dispatchEvent(s)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-space-card-editor")||customElements.define("houseplan-space-card-editor",h);
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";import{c as a,A as e}from"./houseplan-card-C4IYYoj_.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-H6hY7acD.js";import"./form-kit.styles-D_y3mgKZ.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{c as a,A as e}from"./houseplan-card-DE8tFhlR.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-DpE5oJAr.js";import"./form-kit.styles-7MsRKJCO.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
<span class="summary-switch-caption"><strong>${b("summary.show_local")}</strong>
<small id="summary-local-show-hint">${b("summary.show_local_hint")}</small></span>
<input id="summary-local-show" data-summary-local-show type="checkbox"
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";import{eG as e,N as t,b2 as n,L as i,ae as o,ac as r,c9 as s,eH as a,c8 as c,b1 as l,b0 as d,b3 as u,eI as f,eJ as m,cb as g,eK as v}from"./houseplan-card-C4IYYoj_.js";function y(e){if(!e.registry.authoritative)return null;const t=new Set,n=u(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const r=i.binding.slice(0,o),s=i.binding.slice(o+1),a="device"===r?e.registry.devices?.[s]:null,c="entity"===r?e.registry.entities?.[s]:null,l="device"===r?s:c?.device_id,d="device"===r?a?.area_id:c?.area_id||l&&e.registry.devices?.[l]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;l&&e.registry.devices?.[l]&&e.spaceIds.has(f)&&(!n.devices.has(l)||"entity"===r&&n.liveEntities.has(s))&&t.add(l)}return t}function _(e,t){return e?.length?t?.length?v(e,t):e:t}function b(e,t){const n=l(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,r);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:d(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*h(l,d,u=b){try{let f=0;for(const m of d){const d=l.spaces.find(e=>String(e?.id)===m.id);if(!d)continue;const g=e(d,m),v=u(m,g);let y=null;for(const e of m.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const c=n(m.rooms,e.id,g.walls,g.openCuts,i,g.cellCm,o,r,v.roomGeom,v.multiWallNodes)||a;y=_(y,s(c,g.physicalBodies)),yield}const b=a(y||[],m.stairs);let h=b.next();for(;!h.done;)yield,h=b.next();y=h.value;const p=g.cellCm/o;f+=c(y)*p*p/1e4}return f}catch{return null}}function p(e,t,n=b){const i=h(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function S(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:g(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function C(e,t){const n=e?.states?.[t];if(!n)return null;const i=f(e,t);return i?m(i,String(n.attributes?.unit_of_measurement||"")):null}export{h as cleanFloorAreaSteps,y as representedHaDeviceIds,b as spaceWallGeometry,C as summaryEntityValue,S as summarySystemValue,p as totalCleanFloorAreaM2};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{eV as e,O as t,b5 as n,M as i,af as o,ad as r,ce as s,eW as a,cd as c,b4 as l,b3 as d,b6 as u,eX as f,eY as m,cg as g,eZ as v}from"./houseplan-card-DE8tFhlR.js";function y(e){if(!e.registry.authoritative)return null;const t=new Set,n=u(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const r=i.binding.slice(0,o),s=i.binding.slice(o+1),a="device"===r?e.registry.devices?.[s]:null,c="entity"===r?e.registry.entities?.[s]:null,l="device"===r?s:c?.device_id,d="device"===r?a?.area_id:c?.area_id||l&&e.registry.devices?.[l]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;l&&e.registry.devices?.[l]&&e.spaceIds.has(f)&&(!n.devices.has(l)||"entity"===r&&n.liveEntities.has(s))&&t.add(l)}return t}function _(e,t){return e?.length?t?.length?v(e,t):e:t}function h(e,t){const n=l(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,r);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:d(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*p(l,d,u=h){try{let f=0;for(const m of d){const d=l.spaces.find(e=>String(e?.id)===m.id);if(!d)continue;const g=e(d,m),v=u(m,g);let y=null;for(const e of m.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const c=n(m.rooms,e.id,g.walls,g.openCuts,i,g.cellCm,o,r,v.roomGeom,v.multiWallNodes)||a;y=_(y,s(c,g.physicalBodies)),yield}const h=a(y||[],m.stairs);let p=h.next();for(;!p.done;)yield,p=h.next();y=p.value;const b=g.cellCm/o;f+=c(y)*b*b/1e4}return f}catch{return null}}function b(e,t,n=h){const i=p(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function S(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:g(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function C(e,t){const n=e?.states?.[t];if(!n)return null;const i=f(e,t);return i?m(i,String(n.attributes?.unit_of_measurement||"")):null}export{p as cleanFloorAreaSteps,y as representedHaDeviceIds,h as spaceWallGeometry,C as summaryEntityValue,S as summarySystemValue,b as totalCleanFloorAreaM2};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={"furn.title":"Möbelbibliothek","furn.symbol":"Symbol","furn.group_furniture":"Möbel","furn.group_appliance":"Geräte","furn.group_sanitary":"Sanitär","furn.group_other":"Sonstiges","furn.width":"Breite","furn.depth":"Tiefe","furn.back_to_categories":"Alle Kategorien","furn.pick_hint":"Wählen Sie eine Kategorie und dann ein Symbol.","furn.place_hint":"Klicken Sie auf den Plan — das Element wird an der nächsten Wand platziert. Mit Shift wird es frei platziert.","furn.cat_air_conditioner":"Klimaanlagen","furn.cat_armchair":"Sessel","furn.cat_bathtub":"Badewannen","furn.cat_bed":"Betten","furn.cat_bidet":"Bidets","furn.cat_boiler":"Warmwasserbereiter","furn.cat_chair":"Stühle","furn.cat_coffee_table":"Couchtische","furn.cat_computer":"Computer","furn.cat_cooktop":"Kochfelder","furn.cat_dining_table":"Esstische","furn.cat_dishwasher":"Geschirrspüler","furn.cat_dryer":"Wäschetrockner","furn.cat_exercise":"Fitnessgerät","furn.cat_fireplace":"Kamine","furn.cat_fridge":"Kühlschränke","furn.cat_hood":"Dunstabzugshaube","furn.cat_kitchen_cabinet":"Küchenschränke","furn.cat_kitchen_sink":"Küchenspülen","furn.cat_nightstand":"Schränke","furn.cat_oven":"Backofen","furn.cat_plant":"Pflanzen","furn.cat_rug":"Teppiche","furn.cat_shelving":"Regale","furn.cat_shower":"Duschen","furn.cat_sink":"Waschbecken","furn.cat_sofa":"Sofas","furn.cat_stairs":"Treppen","furn.cat_toilet":"Toiletten","furn.cat_tv":"Fernseher","furn.cat_wardrobe":"Kleiderschränke","furn.cat_washer":"Waschmaschinen","furn.cat_work_table":"Schreibtische","furn.sym_exercise":"Fitnessgerät","furn.sym_computer":"Computer","furn.sym_hood":"Dunstabzugshaube","furn.sym_oven":"Backofen","furn.sym_sofa":"Zweisitzer-Sofa","furn.sym_sofa_three_seat":"Dreisitzer-Sofa","furn.sym_sofa_corner_right":"Ecksofa rechts","furn.sym_armchair":"Polstersessel","furn.sym_armchair_office":"Bürostuhl","furn.sym_coffee_table":"Rechteckiger Couchtisch","furn.sym_coffee_table_round":"Runder Couchtisch","furn.sym_coffee_table_oval":"Ovaler Couchtisch","furn.sym_coffee_table_rounded":"Abgerundeter Couchtisch","furn.sym_table_dining":"Rechteckiger Esstisch","furn.sym_table_round":"Rundtisch","furn.sym_table_dining_oval":"Ovaler Esstisch","furn.sym_table_dining_rounded":"Abgerundeter Esstisch","furn.sym_chair":"Stuhl","furn.sym_chair_bar":"Barhocker","furn.sym_desk":"Rechteckiger Schreibtisch","furn.sym_desk_corner":"Eckschreibtisch","furn.sym_bed_double":"Doppelbett","furn.sym_bed_single":"Einzelbett","furn.sym_nightstand":"Nachttisch","furn.sym_cabinet_tv":"TV-Schrank","furn.sym_cabinet_shoe":"Schuhschrank","furn.sym_cabinet_sink":"Waschtischunterschrank","furn.sym_wardrobe":"Kleiderschrank","furn.sym_bookshelf":"Bücherschrank","furn.sym_wall_unit":"Schrankwand","furn.sym_kitchen_floor":"Küchen-Unterschrank","furn.sym_kitchen_floor_corner":"Küchen-Eckunterschrank","furn.sym_kitchen_wall":"Küchen-Hängeschrank","furn.sym_kitchen_wall_corner":"Küchen-Eckhängeschrank","furn.sym_shelf_floor":"Standregal","furn.sym_shelf_wall":"Wandregal","furn.sym_fridge":"Kühlschrank","furn.sym_stove":"Vierflammiges Kochfeld","furn.sym_cooktop_two":"Zweiflammiges Kochfeld","furn.sym_dishwasher":"Geschirrspüler","furn.sym_washer":"Waschmaschine","furn.sym_dryer":"Wäschetrockner","furn.sym_tv":"Fernseher mit Standfuß","furn.sym_tv_wall":"Wandfernseher","furn.sym_ac":"Klimaanlage","furn.sym_water_heater":"Wassererhitzer","furn.sym_toilet":"Stand-WC","furn.sym_toilet_built_in":"Wand-WC","furn.sym_bathtub":"Rechteckige Badewanne","furn.sym_bathtub_corner":"Eckbadewanne","furn.sym_shower":"Dusche","furn.sym_sink":"Waschbecken","furn.sym_kitchen_sink":"Einzel-Küchenspüle","furn.sym_kitchen_sink_double":"Doppel-Küchenspüle","furn.sym_bidet":"Stand-Bidet","furn.sym_bidet_built_in":"Wand-Bidet","furn.sym_stairs":"Treppe","furn.sym_fireplace":"Kamin","furn.sym_plant":"Pflanze","furn.sym_rug":"Teppich"};const r="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,r as fingerprint};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={"furn.title":"Bibliothèque de mobilier","furn.symbol":"Symbole","furn.group_furniture":"Mobilier","furn.group_appliance":"Électroménager","furn.group_sanitary":"Sanitaires","furn.group_other":"Autre","furn.width":"Largeur","furn.depth":"Profondeur","furn.back_to_categories":"Toutes les catégories","furn.pick_hint":"Choisissez un symbole, puis cliquez sur le plan.","furn.place_hint":"Cliquez sur le plan — le meuble se place contre le mur le plus proche. Maj permet un placement libre.","furn.cat_air_conditioner":"Climatiseurs","furn.cat_armchair":"Fauteuils","furn.cat_bathtub":"Baignoires","furn.cat_bed":"Lits","furn.cat_bidet":"Bidets","furn.cat_boiler":"Chauffe-eau","furn.cat_chair":"Chaises","furn.cat_coffee_table":"Tables basses","furn.cat_computer":"Ordinateur","furn.cat_cooktop":"Plaques de cuisson","furn.cat_dining_table":"Tables à manger","furn.cat_dishwasher":"Lave-vaisselle","furn.cat_dryer":"Sèche-linge","furn.cat_exercise":"Équipement de sport","furn.cat_fireplace":"Cheminées","furn.cat_fridge":"Réfrigérateurs","furn.cat_hood":"Hotte","furn.cat_kitchen_cabinet":"Meubles de cuisine","furn.cat_kitchen_sink":"Éviers de cuisine","furn.cat_nightstand":"Meubles","furn.cat_oven":"Four","furn.cat_plant":"Plantes","furn.cat_rug":"Tapis","furn.cat_shelving":"Étagères","furn.cat_shower":"Douches","furn.cat_sink":"Lavabos","furn.cat_sofa":"Canapés","furn.cat_stairs":"Escaliers","furn.cat_toilet":"Toilettes","furn.cat_tv":"Téléviseurs","furn.cat_wardrobe":"Armoires","furn.cat_washer":"Lave-linge","furn.cat_work_table":"Bureaux","furn.sym_exercise":"Équipement de sport","furn.sym_computer":"Ordinateur","furn.sym_hood":"Hotte","furn.sym_oven":"Four","furn.sym_sofa":"Canapé","furn.sym_sofa_three_seat":"Canapé trois places","furn.sym_sofa_corner_right":"Canapé d'angle droit","furn.sym_armchair":"Fauteuil","furn.sym_armchair_office":"Chaise de bureau","furn.sym_coffee_table":"Table basse","furn.sym_coffee_table_round":"Table basse ronde","furn.sym_coffee_table_oval":"Table basse ovale","furn.sym_coffee_table_rounded":"Table basse arrondie","furn.sym_table_dining":"Table à manger","furn.sym_table_round":"Table ronde","furn.sym_table_dining_oval":"Table à manger ovale","furn.sym_table_dining_rounded":"Table à manger arrondie","furn.sym_chair":"Chaise","furn.sym_chair_bar":"Tabouret de bar","furn.sym_desk":"Bureau","furn.sym_desk_corner":"Bureau d'angle","furn.sym_bed_double":"Lit double","furn.sym_bed_single":"Lit simple","furn.sym_nightstand":"Table de chevet","furn.sym_cabinet_tv":"Meuble TV","furn.sym_cabinet_shoe":"Meuble à chaussures","furn.sym_cabinet_sink":"Meuble sous lavabo","furn.sym_wardrobe":"Armoire","furn.sym_bookshelf":"Bibliothèque","furn.sym_wall_unit":"Élément mural","furn.sym_kitchen_floor":"Module de cuisine bas","furn.sym_kitchen_floor_corner":"Module de cuisine bas d'angle","furn.sym_kitchen_wall":"Module de cuisine haut","furn.sym_kitchen_wall_corner":"Module de cuisine haut d'angle","furn.sym_shelf_floor":"Étagère au sol","furn.sym_shelf_wall":"Étagère murale","furn.sym_fridge":"Réfrigérateur","furn.sym_stove":"Cuisinière","furn.sym_cooktop_two":"Plaque à deux feux","furn.sym_dishwasher":"Lave-vaisselle","furn.sym_washer":"Lave-linge","furn.sym_dryer":"Sèche-linge","furn.sym_tv":"Téléviseur","furn.sym_tv_wall":"Téléviseur mural","furn.sym_ac":"Climatiseur","furn.sym_water_heater":"Chauffe-eau","furn.sym_toilet":"Toilettes","furn.sym_toilet_built_in":"Toilettes suspendues","furn.sym_bathtub":"Baignoire","furn.sym_bathtub_corner":"Baignoire d'angle","furn.sym_shower":"Douche","furn.sym_sink":"Lavabo","furn.sym_kitchen_sink":"Évier de cuisine","furn.sym_kitchen_sink_double":"Évier de cuisine double","furn.sym_bidet":"Bidet","furn.sym_bidet_built_in":"Bidet suspendu","furn.sym_stairs":"Escalier","furn.sym_fireplace":"Cheminée","furn.sym_plant":"Plante","furn.sym_rug":"Tapis"};const r="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,r as fingerprint};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";var e={title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Es werden nur beobachtete direkte Nachbarn angezeigt. Beim Darüberfahren werden keine Daten abgerufen.",help:"Zeigt, mit welchen Zigbee-Geräten jedes Gerät direkt verbunden ist. Die Linienfarbe ist die Verbindungsqualität (LQI) auf derselben Skala wie der LQI-Wert des Geräts: rot bei 40 und darunter, grün bei 180 und darüber. Eine gestrichelte Linie bedeutet, dass die Qualität nicht gemeldet wurde. Ein Pfeil zeigt auf das nächste Gerät auf dem Weg zum Koordinator: ein Pfeil führt hinaus, eingehende Pfeile sind die Geräte, die über dieses Gerät routen. Eine Linie ohne Pfeil ist ein Ersatznachbar. Das ist der Routenbaum, den House Plan bildet, nicht der Weg eines Pakets in diesem Moment: eine Beschriftung am Pfeilende bedeutet, dass das Ziel nicht auf diesem Plan liegt, und ein fehlender Pfeil, dass die Route unbekannt ist. Die Daten stammen aus dem letzten Laden und veralten; Verbindungen sind nur mit der Maus sichtbar.",help_aria:"Hilfe: Zigbee-Verbindungen",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan kann 10 Sekunden bis 2 Minuten dauern und Zigbee vorübergehend verlangsamen.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · einige Knoten fehlen",status_no_links:"Empfangen: {time} · keine beobachteten Verbindungen gefunden",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich"};const n="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";export{e as dictionary,n as fingerprint};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Es werden nur beobachtete direkte Nachbarn angezeigt. Beim Darüberfahren werden keine Daten abgerufen.",help:"Zeigt, mit welchen Zigbee-Geräten jedes Gerät direkt verbunden ist. Die Linienfarbe ist die Verbindungsqualität (LQI) auf derselben Skala wie der LQI-Wert des Geräts: rot bei 40 und darunter, grün bei 180 und darüber. Eine gestrichelte Linie bedeutet, dass die Qualität nicht gemeldet wurde. Ein Pfeil zeigt auf das nächste Gerät auf dem Weg zum Koordinator: ein Pfeil führt hinaus, eingehende Pfeile sind die Geräte, die über dieses Gerät routen. Eine Linie ohne Pfeil ist ein Ersatznachbar. Das ist der Routenbaum, den House Plan bildet, nicht der Weg eines Pakets in diesem Moment: eine Beschriftung am Pfeilende bedeutet, dass das Ziel nicht auf diesem Plan liegt, und ein fehlender Pfeil, dass die Route unbekannt ist. Die Daten stammen aus dem letzten Laden und veralten; Verbindungen sind nur mit der Maus sichtbar.",help_aria:"Hilfe: Zigbee-Verbindungen",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan kann 10 Sekunden bis 2 Minuten dauern und Zigbee vorübergehend verlangsamen.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · einige Knoten fehlen",status_no_links:"Empfangen: {time} · keine beobachteten Verbindungen gefunden",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich"};const n="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,n as fingerprint};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";var e={title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Seuls les voisins directs observés sont affichés. Le survol ne lance aucune requête.",help:"Montre avec quels appareils Zigbee chaque appareil communique directement. La couleur de la ligne est la qualité du lien (LQI) sur la même échelle que l'indicateur LQI de l'appareil : rouge à 40 et en dessous, vert à 180 et au-dessus. Une ligne pointillée signifie que la qualité n'a pas été communiquée. Une flèche pointe vers l'appareil suivant sur le chemin du coordinateur : une flèche part, les flèches entrantes sont les appareils qui passent par celui-ci. Une ligne sans flèche est un voisin de secours. C'est l'arbre de routes que construit House Plan, pas le trajet d'un paquet à cet instant : une étiquette au bout d'une flèche signifie que sa cible n'est pas sur ce plan, et l'absence de flèche que la route est inconnue. Les données proviennent du dernier chargement et vieillissent ; les liens ne sont visibles qu'à la souris.",help_aria:"Aide : liens Zigbee",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau peut durer de 10 secondes à 2 minutes et réduire temporairement la réactivité de Zigbee.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · certains nœuds sont omis",status_no_links:"Reçu : {time} · aucun lien observé trouvé",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace"};const s="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";export{e as dictionary,s as fingerprint};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Seuls les voisins directs observés sont affichés. Le survol ne lance aucune requête.",help:"Montre avec quels appareils Zigbee chaque appareil communique directement. La couleur de la ligne est la qualité du lien (LQI) sur la même échelle que l'indicateur LQI de l'appareil : rouge à 40 et en dessous, vert à 180 et au-dessus. Une ligne pointillée signifie que la qualité n'a pas été communiquée. Une flèche pointe vers l'appareil suivant sur le chemin du coordinateur : une flèche part, les flèches entrantes sont les appareils qui passent par celui-ci. Une ligne sans flèche est un voisin de secours. C'est l'arbre de routes que construit House Plan, pas le trajet d'un paquet à cet instant : une étiquette au bout d'une flèche signifie que sa cible n'est pas sur ce plan, et l'absence de flèche que la route est inconnue. Les données proviennent du dernier chargement et vieillissent ; les liens ne sont visibles qu'à la souris.",help_aria:"Aide : liens Zigbee",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau peut durer de 10 secondes à 2 minutes et réduire temporairement la réactivité de Zigbee.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · certains nœuds sont omis",status_no_links:"Reçu : {time} · aucun lien observé trouvé",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace"};const s="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,s as fingerprint};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";var e={title:"Связи Zigbee",toggle:"Показывать связи Zigbee при наведении на устройство",hint:"Показываются только наблюдаемые прямые соседи. Наведение ничего не запрашивает.",help:"Показывает, с какими устройствами Zigbee каждое связано напрямую. Цвет линии — качество связи (LQI) по той же шкале, что у показателя LQI устройства: красный при 40 и ниже, зелёный при 180 и выше. Пунктир — качество не сообщено. Стрелка ведёт к следующему устройству по пути к координатору: исходящая одна, входящие — те, кто ходит через это устройство. Линия без стрелки — запасной сосед. Это дерево маршрутов, которое строит House Plan, а не путь пакета в эту секунду: подпись на конце стрелки значит, что цель не на этом плане, а отсутствие стрелки — что путь неизвестен. Данные из последней загрузки и устаревают, связи видны только при работе мышью.",help_aria:"Справка: связи Zigbee",admin_only:"Данные топологии доступны только администраторам Home Assistant.",save_first:"Сохраните настройку перед загрузкой данных топологии.",zha:"ZHA",zha_read:"Прочитать данные ZHA",zha_hint:"Читает сохранённую топологию ZHA и не запускает радио-сканирование.",z2m:"Zigbee2MQTT",z2m_topics:"Базовые топики (по одному в строке)",z2m_update:"Обновить карту",z2m_warning:"Сканирование карты сети занимает от 10 секунд до 2 минут и временно может снизить отзывчивость Zigbee.",status_idle:"Данные не загружены",status_loading:"Загрузка…",status_ready:"Получено: {time}",status_stale:"Получено: {time} · данные устарели",status_partial:"Получено: {time} · часть узлов не показана",status_no_links:"Получено: {time} · наблюдаемые связи не найдены",error_permission:"Нужны права администратора.",error_unsupported:"Провайдер или API Home Assistant недоступен.",error_timeout:"Провайдер не ответил вовремя.",error_invalid_topic:"Проверьте базовый топик Zigbee2MQTT.",error_invalid_payload:"Провайдер вернул неподдерживаемые данные топологии.",error_provider:"Не удалось загрузить данные топологии.",remote_count:"+{n} в других пространствах",route_device_not_on_plan:"устройства нет на плане",route_coordinator_not_on_plan:"координатора нет на плане",route_other_space:"другое пространство"};const t="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";export{e as dictionary,t as fingerprint};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";var e={title:"Связи Zigbee",toggle:"Показывать связи Zigbee при наведении на устройство",hint:"Показываются только наблюдаемые прямые соседи. Наведение ничего не запрашивает.",help:"Показывает, с какими устройствами Zigbee каждое связано напрямую. Цвет линии — качество связи (LQI) по той же шкале, что у показателя LQI устройства: красный при 40 и ниже, зелёный при 180 и выше. Пунктир — качество не сообщено. Стрелка ведёт к следующему устройству по пути к координатору: исходящая одна, входящие — те, кто ходит через это устройство. Линия без стрелки — запасной сосед. Это дерево маршрутов, которое строит House Plan, а не путь пакета в эту секунду: подпись на конце стрелки значит, что цель не на этом плане, а отсутствие стрелки — что путь неизвестен. Данные из последней загрузки и устаревают, связи видны только при работе мышью.",help_aria:"Справка: связи Zigbee",admin_only:"Данные топологии доступны только администраторам Home Assistant.",save_first:"Сохраните настройку перед загрузкой данных топологии.",zha:"ZHA",zha_read:"Прочитать данные ZHA",zha_hint:"Читает сохранённую топологию ZHA и не запускает радио-сканирование.",z2m:"Zigbee2MQTT",z2m_topics:"Базовые топики (по одному в строке)",z2m_update:"Обновить карту",z2m_warning:"Сканирование карты сети занимает от 10 секунд до 2 минут и временно может снизить отзывчивость Zigbee.",status_idle:"Данные не загружены",status_loading:"Загрузка…",status_ready:"Получено: {time}",status_stale:"Получено: {time} · данные устарели",status_partial:"Получено: {time} · часть узлов не показана",status_no_links:"Получено: {time} · наблюдаемые связи не найдены",error_permission:"Нужны права администратора.",error_unsupported:"Провайдер или API Home Assistant недоступен.",error_timeout:"Провайдер не ответил вовремя.",error_invalid_topic:"Проверьте базовый топик Zigbee2MQTT.",error_invalid_payload:"Провайдер вернул неподдерживаемые данные топологии.",error_provider:"Не удалось загрузить данные топологии.",remote_count:"+{n} в других пространствах",route_device_not_on_plan:"устройства нет на плане",route_coordinator_not_on_plan:"координатора нет на плане",route_other_space:"другое пространство"};const t="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";export{e as dictionary,t as fingerprint};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";import{n as t,c as n}from"./zigbee-topology-Dq_fGFqf.js";import{aM as e}from"./houseplan-card-C4IYYoj_.js";import"./namespace-language-DePztGEB.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";import{n as t,c as n}from"./zigbee-topology-DKrDE5nD.js";import{aP as e}from"./houseplan-card-DE8tFhlR.js";import"./namespace-language-CRySE1JG.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function m(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function g(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function h(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,h=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,h=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&m(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=m(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&h?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await g(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await g(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,h as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d87b98d35a85dd4726b553bb2f2ac2cd9ef5a978be106b231308eabcd760f6f5";try{await import("./houseplan-assets/houseplan-card-C4IYYoj_.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale houseplan-card.js: the implementation chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="169bb796e6918c65604cad46c38192debd54defe56738b7113735d6119689efa";try{await import("./houseplan-assets/houseplan-card-DE8tFhlR.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale houseplan-card.js: the implementation chunk is unavailable",e)}
File diff suppressed because one or more lines are too long
@@ -35,6 +35,7 @@ from .const import (
PLANS_URL,
VERSION,
)
from .led_strips import led_strip_link_report, plan_only_strips, unbind_strips
from .radar_validation import validate_marker_radars
from .store import HouseplanData
from .validation import (
@@ -294,6 +295,10 @@ def _project_plan_only_space(space: dict[str, Any]) -> dict[str, Any]:
projected["decor"] = [
_project_plan_only_decor(shape) for shape in space.get("decor") or []
]
led_strips = plan_only_strips(space)
if led_strips is not None:
# #780: plan-only keeps the strip geometry and never a device link.
projected["led_strips"] = led_strips
if "stairs" in space:
projected["stairs"] = [
_pick_fields(stair, (
@@ -694,6 +699,12 @@ def parse_document(raw: bytes) -> dict[str, Any]:
# before an import token can be issued.
if model > 0:
config_candidate["model_version"] = model
# #780: the schema step unbinds strips whose marker is not in the
# document; count them before it does, for the import summary.
unbound_led_strips = (
led_strip_link_report(config_candidate)["unbound"]
if isinstance(config_candidate, dict) else 0
)
config = CONFIG_SCHEMA(config_candidate)
config.pop("model_version", None)
except (vol.Invalid, TypeError, ValueError) as err:
@@ -730,6 +741,10 @@ def parse_document(raw: bytes) -> dict[str, Any]:
**(document.get("transfer") or {}),
"dropped_marker_links": dropped_marker_links,
}
# Computed here, never trusted from the file.
document["transfer"].pop("unbound_led_strips", None)
if unbound_led_strips:
document["transfer"]["unbound_led_strips"] = unbound_led_strips
if plan_only:
_validate_plan_only_document(document, config, layout, placement)
if len(json.dumps(
@@ -1608,6 +1623,14 @@ def build_space_merge(
marker.pop("value_source", None)
dropped_marker_links += 1
# #780: an LED strip follows its marker through the same id remap; a
# marker that was skipped or virtualised by the duplicate policy is not
# transferred, so the strip arrives unbound (geometry kept, no device).
unbound_led_strips = unbind_strips(space, remap={
old_id: new_id for old_id, new_id in marker_map.items()
if old_id not in virtualized_targets
})
output_markers_by_id = {
str(marker.get("id")): marker for marker in output_markers
if marker.get("id") is not None
@@ -1709,6 +1732,7 @@ def build_space_merge(
"virtualized": virtualized,
"orphan_markers": len(output_markers) - len(marker_map),
"dropped_marker_links": dropped_marker_links,
"unbound_led_strips": unbound_led_strips,
"repaired_target_refs": repaired_target_refs,
"preserved_unresolved_refs": sum(
int(value) for value in reference_report["preservedUnresolved"].values()
@@ -1914,6 +1938,14 @@ def _materialize_import_candidate(
details = {
"dropped_marker_links": _transfer_dropped_marker_links(prepared),
}
# #780: strips unbound by the document itself (counted while parsing)
# plus those the transfer unbound (space merge) or the schema step will.
transfer = prepared.get("transfer") if isinstance(prepared.get("transfer"), dict) else {}
details["unbound_led_strips"] = (
int(details.get("unbound_led_strips", 0))
+ int(transfer.get("unbound_led_strips", 0) or 0)
+ led_strip_link_report(config)["unbound"]
)
try:
config = CONFIG_SCHEMA(config)
except vol.Invalid as err:
+315
View File
@@ -0,0 +1,315 @@
"""LED strips: stored shape, link normalisation and transfer rules (#780).
A strip is optional geometry of a space::
space.led_strips = [{"id": str, "points": [[x, y], ...],
"marker": str | None, "active": bool (optional)}]
``active`` is the representation of the bound marker: absent/``True`` draws
the strip, ``False`` keeps the shape hidden while the same marker is shown as
an ordinary icon. It is neither ``marker.hidden`` nor the power state.
The module is pure (voluptuous only), so the schema, the write-path
normalisation and the import/plan-only projections are exercised by the pure
pytest suite without Home Assistant.
Write-path order (ТЗ #780 §9):
1. structure, types and limits (``LED_STRIPS_SCHEMA`` inside ``SPACE_SCHEMA``);
2. per-space invariants and duplicate links — **before** any normalisation, so
two strips pointing at the same missing id are still a conflict;
3. normalisation: a link to a marker that is not live in the resulting config
becomes an unbound strip (``marker: None, active: True``) with its id and
points intact — a client that does not know about strips may delete a bound
marker and its save must not fail; a live marker with an empty ``space``
adopts the strip's space;
4. referential check: a live marker with a non-empty foreign ``space``
rejects the whole write.
"""
from __future__ import annotations
import copy
import math
from collections.abc import Iterator
from typing import Any
import voluptuous as vol
MAX_LED_STRIPS = 50
MAX_LED_POINTS = 50
LED_ID_MAX = 64
LED_MARKER_MAX = 500
CANVAS_LIMIT = 5000.0
LED_KEY = "led_strips"
def _coordinate(value: Any) -> float:
"""A stored coordinate: a real finite number, never a string or a bool."""
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise vol.Invalid("LED strip coordinate must be a number")
number = float(value)
if not math.isfinite(number):
raise vol.Invalid("LED strip coordinate must be finite")
if abs(number) > CANVAS_LIMIT:
raise vol.Invalid("LED strip coordinate is outside the canvas")
return number
def _strict_bool(value: Any) -> bool:
if not isinstance(value, bool):
raise vol.Invalid("LED strip active must be a boolean")
return value
def _marker_ref(value: Any) -> str | None:
if value is None:
return None
if not isinstance(value, str) or not value or len(value) > LED_MARKER_MAX:
raise vol.Invalid("LED strip marker must be a non-empty string or null")
return value
def _strip_id(value: Any) -> str:
if not isinstance(value, str) or not value or len(value) > LED_ID_MAX:
raise vol.Invalid("LED strip id must be a non-empty string")
return value
_POINT = vol.All([_coordinate], vol.Length(min=2, max=2))
LED_STRIP_SCHEMA = vol.Schema(
{
vol.Required("id"): _strip_id,
vol.Required("points"): vol.All(
list, vol.Length(min=2, max=MAX_LED_POINTS), [_POINT],
),
vol.Optional("marker", default=None): _marker_ref,
vol.Optional("active"): _strict_bool,
},
extra=vol.ALLOW_EXTRA,
)
LED_STRIPS_SCHEMA = vol.All(list, vol.Length(max=MAX_LED_STRIPS), [LED_STRIP_SCHEMA])
def _same(a: list[Any], b: list[Any]) -> bool:
return float(a[0]) == float(b[0]) and float(a[1]) == float(b[1])
def strip_is_closed(points: list[Any]) -> bool:
return len(points) >= 4 and _same(points[0], points[-1])
def polyline_length(points: list[Any]) -> float:
total = 0.0
for index in range(1, len(points)):
ax, ay = points[index - 1]
bx, by = points[index]
total += math.hypot(float(bx) - float(ax), float(by) - float(ay))
return total
def _distinct_vertices(points: list[Any]) -> int:
seen: set[tuple[float, float]] = set()
for point in points:
seen.add((float(point[0]), float(point[1])))
return len(seen)
def validate_strip_shape(strip: dict[str, Any]) -> None:
"""Geometry invariants that the type schema cannot express."""
points = strip["points"]
if _distinct_vertices(points) < 2 or not polyline_length(points) > 0:
raise vol.Invalid("LED strip needs at least two distinct points")
if _same(points[0], points[-1]) and len(points) > 2:
# A repeated first point closes the strip: three distinct vertices.
if _distinct_vertices(points[:-1]) < 3:
raise vol.Invalid("a closed LED strip needs three distinct vertices")
if strip.get("active") is False and not isinstance(strip.get("marker"), str):
raise vol.Invalid("a hidden LED strip shape must belong to a marker")
class LedStripLinkError(ValueError):
"""A write that cannot be normalised: duplicate or foreign link."""
code = "invalid_led_strip"
def __init__(self, reason: str) -> None:
# Marker ids may carry user-controlled HA identifiers: a stable
# message without the value, like DuplicateMarkerIdError.
super().__init__(reason)
self.reason = reason
def _live_markers(config: dict[str, Any]) -> dict[str, dict[str, Any]]:
live: dict[str, dict[str, Any]] = {}
for marker in config.get("markers") or []:
if not isinstance(marker, dict) or marker.get("removed") is True:
continue
marker_id = marker.get("id")
if isinstance(marker_id, str) and marker_id and marker_id not in live:
live[marker_id] = marker
return live
def _strips(config: dict[str, Any]) -> Iterator[tuple[dict[str, Any], dict[str, Any]]]:
for space in config.get("spaces") or []:
if not isinstance(space, dict):
continue
for strip in space.get(LED_KEY) or []:
if isinstance(strip, dict):
yield space, strip
def preserve_led_strips(config: dict[str, Any], previous: dict[str, Any] | None) -> int:
"""An ordinary writer that omits ``led_strips`` keeps the stored shapes (#780 r1 H1).
A previous frontend does not know the field and sends every space without
it; the omission is not a deletion. A new client deletes shapes by sending
an explicit list (``[]`` removes all of them). Applies to spaces that keep
their id; a space the writer removed takes its strips with it. Runs on the
ordinary write path before normalisation, so a preserved link to a marker
the same write deleted becomes an unbound strip by the usual rule.
Returns how many spaces received their stored shapes back.
"""
stored = {
space.get("id"): space[LED_KEY]
for space in (previous or {}).get("spaces") or []
if isinstance(space, dict) and isinstance(space.get(LED_KEY), list) and space[LED_KEY]
}
restored = 0
for space in config.get("spaces") or []:
if not isinstance(space, dict) or LED_KEY in space:
continue
shapes = stored.get(space.get("id"))
if shapes:
space[LED_KEY] = copy.deepcopy(shapes)
restored += 1
return restored
def led_strip_link_report(config: dict[str, Any]) -> dict[str, int]:
"""What the normaliser would change, without changing anything.
The write path answers the client with these counters so a new client
knows to re-read; an old client ignores the extra keys.
"""
live = _live_markers(config)
unbound = adopted = 0
for _space, strip in _strips(config):
ref = strip.get("marker")
if not isinstance(ref, str) or not ref:
continue
marker = live.get(ref)
if marker is None:
unbound += 1
elif marker.get("space") in (None, ""):
adopted += 1
return {"unbound": unbound, "space_adopted": adopted}
def normalize_led_strip_links(config: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalise every strip of a whole configuration in place.
Runs after the type schema and the coordinate canonicalisation, so the
shape invariants judge the stored numbers.
"""
taken: set[str] = set()
for space in config.get("spaces") or []:
if not isinstance(space, dict):
continue
ids: set[str] = set()
for strip in space.get(LED_KEY) or []:
if strip["id"] in ids:
raise vol.Invalid("LED strip ids must be unique within a space")
ids.add(strip["id"])
validate_strip_shape(strip)
ref = strip.get("marker")
if isinstance(ref, str):
if ref in taken:
raise LedStripLinkError("a marker is bound to more than one LED strip")
taken.add(ref)
live = _live_markers(config)
for space, strip in _strips(config):
ref = strip.get("marker")
if not isinstance(ref, str):
strip["marker"] = None
continue
marker = live.get(ref)
if marker is None:
strip["marker"] = None
strip["active"] = True
continue
owner = marker.get("space")
if owner in (None, ""):
marker["space"] = space["id"]
elif owner != space["id"]:
raise LedStripLinkError("an LED strip is bound to a marker of another space")
return config
def config_led_strip_links(config: dict[str, Any]) -> dict[str, Any]:
"""voluptuous step for CONFIG_SCHEMA: ``LedStripLinkError`` → ``vol.Invalid``."""
try:
return normalize_led_strip_links(config)
except LedStripLinkError as err:
raise vol.Invalid(f"{LedStripLinkError.code}: {err.reason}") from err
def unbind_strips(space: dict[str, Any], keep: set[str] | None = None,
remap: dict[str, str] | None = None) -> int:
"""Transfer rule for imports/copies: remap a link or make the strip unbound.
``remap`` maps the source marker id to the id it received in the target;
a marker that was not transferred (skipped duplicate, plan-only, dropped)
leaves an unbound strip with ``active: True``. Returns how many strips
became unbound, for the import summary.
"""
unbound = 0
for strip in space.get(LED_KEY) or []:
if not isinstance(strip, dict):
continue
ref = strip.get("marker")
if not isinstance(ref, str) or not ref:
strip["marker"] = None
continue
target = (remap or {}).get(ref)
if target is None and keep is not None and ref in keep:
target = ref
if target is None:
strip["marker"] = None
strip["active"] = True
unbound += 1
else:
strip["marker"] = target
return unbound
def plan_only_strips(space: dict[str, Any]) -> list[dict[str, Any]] | None:
"""Plan-only export: geometry stays, device links never leak."""
if LED_KEY not in space:
return None
projected = []
for strip in space.get(LED_KEY) or []:
if not isinstance(strip, dict):
continue
projected.append({
"id": strip.get("id"),
"points": [list(point) for point in strip.get("points") or []],
"marker": None,
"active": True,
})
return projected
def led_strip_counts(config: dict[str, Any]) -> dict[str, int]:
"""Support-package counters: no coordinates, names or HA identifiers."""
total = unbound = hidden = 0
for _space, strip in _strips(config):
total += 1
if not isinstance(strip.get("marker"), str):
unbound += 1
elif strip.get("active") is False:
hidden += 1
return {"led_strips": total, "led_strips_unbound": unbound, "led_strips_hidden": hidden}
+1 -1
View File
@@ -20,5 +20,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.78.0-beta.8"
"version": "1.79.0-beta.3"
}
@@ -22,6 +22,7 @@ from .const import (
MAX_SUPPORT_ATTACHMENT_BYTES,
PLAN_MODEL_VERSION,
)
from .led_strips import led_strip_counts
PACKAGE_FORMAT = "houseplan-support-package"
PACKAGE_VERSION = 1
@@ -158,6 +159,9 @@ def _global_settings(value: object) -> dict[str, Any]:
volumetric_view = value.get("volumetric_view")
if isinstance(volumetric_view, bool):
out["volumetric_view"] = volumetric_view
moon = value.get("moon")
if isinstance(moon, bool):
out["moon"] = moon
fill_colors = value.get("fill_colors")
if isinstance(fill_colors, dict):
palette = {
@@ -454,10 +458,21 @@ def _summary(config: object, layout: object) -> dict[str, Any]:
"lifecycle": dict(sorted(lifecycles.items())),
"binding": dict(sorted(bindings.items())),
},
# #780: counters only — no coordinates, room names or HA identifiers.
"led_strips": _led_strip_summary(spaces),
"layout_entries": len(layout) if isinstance(layout, dict) else 0,
}
def _led_strip_summary(spaces: list[Any]) -> dict[str, int]:
counts = led_strip_counts({"spaces": [space for space in spaces if isinstance(space, dict)]})
return {
"total": counts["led_strips"],
"unbound": counts["led_strips_unbound"],
"hidden": counts["led_strips_hidden"],
}
def _binding_kind(value: object) -> str:
text = str(value or "")
if text == "virtual":
+16 -1
View File
@@ -17,6 +17,11 @@ from custom_components.houseplan.coordinate_canonicalization import (
canonicalize_layout_geometry,
canonicalize_position,
)
from custom_components.houseplan.led_strips import (
LED_STRIPS_SCHEMA,
config_led_strip_links,
preserve_led_strips,
)
from custom_components.houseplan.vacuum_routes import validate_marker_routes
# ---------- limits and extension sets ----------
@@ -1877,6 +1882,8 @@ SPACE_SCHEMA = vol.All(vol.Schema(
# (HP-1454-05): relying on a modern client to strip an unbounded legacy
# list is not a limit, it is a hope. `Remove` returns the key stripped.
vol.Remove("segments"): object,
# #780: optional LED strip shapes of the space (led_strips.py).
vol.Optional("led_strips"): LED_STRIPS_SCHEMA,
},
extra=vol.ALLOW_EXTRA,
), _space_geometry_invariants)
@@ -2268,13 +2275,16 @@ def prepare_ordinary_summary_candidate(
readable_entity_ids: set[str],
normalize,
) -> dict:
"""Apply the one summary-panel contract shared by ordinary config writers.
"""Apply the one preservation contract shared by ordinary config writers.
`normalize` owns the writer-specific schema/migration sequence. Preservation
must happen before it; change-aware reference validation must happen after
it. Authoritative full import/restore intentionally does not use this helper.
Preserved: the summary panel namespace (#437) and the LED strip shapes of a
space an older writer sent without the field (#780).
"""
preserve_summary_panel_namespace(candidate, previous)
preserve_led_strips(candidate, previous)
checked = normalize(candidate)
validate_summary_panel_references(checked, previous, readable_entity_ids)
return checked
@@ -2313,6 +2323,8 @@ CONFIG_SCHEMA = vol.All(
vol.Optional("show_room_tooltip"): bool,
# #649: installation-wide 2.5D View; absent/false keeps Flat.
vol.Optional("volumetric_view"): bool,
# #661: the moon on the "Follow the Sun" background; absent/false = off.
vol.Optional("moon"): bool,
vol.Optional("summary_panel"): SUMMARY_PANEL_WIRE_SCHEMA,
vol.Optional("radar"): vol.Schema({}, extra=vol.ALLOW_EXTRA),
# Removed from the UI/runtime in 2026-08-08. Keep accepting the
@@ -2351,4 +2363,7 @@ CONFIG_SCHEMA = vol.All(
),
canonicalize_config_geometry,
_config_wall_segment_invariants,
# #780: after canonicalisation — shape invariants judge stored numbers;
# orphan links become unbound strips, foreign/duplicate links reject.
config_led_strip_links,
)
+13 -1
View File
@@ -70,6 +70,7 @@ from .import_export import (
revalidate_candidate,
)
from .junction_limits import JunctionLimitError, validate_junction_limits
from .led_strips import led_strip_link_report, preserve_led_strips
from .plans import (
QuotaError,
collect_attachments,
@@ -1681,6 +1682,14 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
radar_registry = radar_registry_evidence(hass)
# #780: what the write path will normalise in LED strip links (an
# orphan becomes unbound, an empty marker.space adopts the strip's
# space). Reported back so a new client re-reads; old ones ignore it.
# An older writer omits the field: its stored shapes are kept first
# (r1 H1), so the report also counts their links.
preserve_led_strips(msg["config"], data.get("config"))
led_report = led_strip_link_report(msg["config"])
def _validate_config_cpu():
def _normalize(candidate):
validate_wall_model_transition(candidate, data.get("config"))
@@ -1789,7 +1798,10 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
from .repairs import async_check_plan_files
hass.async_create_task(async_check_plan_files(hass, entry))
connection.send_result(msg["id"], {"ok": True, "rev": new_rev})
result: dict[str, Any] = {"ok": True, "rev": new_rev}
if any(led_report.values()):
result["led_strips"] = led_report
connection.send_result(msg["id"], result)
# ---------------- whole-plan maintenance ----------------
+81 -7
View File
@@ -22,7 +22,7 @@ const targetRoot = resolve(valueArg('target-root') ?? '.');
const profile = valueArg('profile') ?? 'large-house-v1';
if (![
'large-house-v1', 'large-house-isometric-v1', ISOMETRIC_STAGE3_DENSE_PROFILE,
'large-house-plan-snap-v1', 'large-house-interaction-v1',
'large-house-plan-snap-v1', 'large-house-interaction-v1', 'large-house-isometric-backdrop-v1',
].includes(profile))
throw new Error(`unknown large-house profile: ${profile}`);
const stage3Dense = profile === ISOMETRIC_STAGE3_DENSE_PROFILE;
@@ -30,7 +30,8 @@ const allowStage2Base = process.argv.includes('--allow-stage2-base');
if (allowStage2Base && !stage3Dense)
throw new Error('--allow-stage2-base is valid only for the Stage 3 dense profile');
const requireStage3 = stage3Dense && !allowStage2Base;
const isometric = profile === 'large-house-isometric-v1' || stage3Dense;
const backdrop = profile === 'large-house-isometric-backdrop-v1';
const isometric = profile === 'large-house-isometric-v1' || stage3Dense || backdrop;
const planSnap = profile === 'large-house-plan-snap-v1';
const interaction = profile === 'large-house-interaction-v1';
const requiresIsometric = isometric && existsSync(resolve(targetRoot, 'src/iso-projection.ts'));
@@ -42,7 +43,20 @@ const requiresWallFace = planSnap && existsSync(resolve(targetRoot, 'src/wall-fa
const requiresInteraction = interaction && existsSync(resolve(targetRoot, 'src/live-viewport.ts'));
const requiresStairs = existsSync(resolve(targetRoot, 'src/stairs.ts'));
const fixture = stage3Dense ? makeIsometricStage3DenseFixture() : makeLargeHouseFixture();
const fixtureCounts = stage3Dense ? fixture.counts : LARGE_HOUSE_COUNTS;
if (backdrop) {
// #743: the 2.5D twin with a backdrop image on every floor, so the imagePlan
// path (#739 K1) is measured at all. One shipped picture (demo/srv/assets
// since v1.14.0, present in every base) under a URL of its own per floor, so
// each floor decodes its own image; plan_aspect 1 lays it over the whole
// plan square and leaves the room geometry alone. The variant lives here, as
// plan-snap's does: demo/fixtures/** and the bundle fingerprint stay as they are.
for (const space of fixture.config.spaces) {
space.plan_url = `/assets/f1.svg?${space.id}`;
space.plan_aspect = 1;
}
fixture.counts = { ...fixture.counts, backdrops: fixture.config.spaces.length };
}
const fixtureCounts = stage3Dense || backdrop ? fixture.counts : LARGE_HOUSE_COUNTS;
const sourceSha = (() => {
try {
return execFileSync('git', ['-C', targetRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
@@ -101,7 +115,7 @@ try {
const row = await page.evaluate(async ({
fixture, sample, cardContract, isometric, requiresIsometric, planSnap, requiresPlanSnap,
requiresWallFace, interaction, requiresInteraction, stage3Dense, requireStage3,
requiresIsoStructuralBuildCounter, requiresStairs, profile,
requiresIsoStructuralBuildCounter, requiresStairs, profile, backdrop,
}) => {
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
const until = async (predicate, timeout = 10000) => {
@@ -164,6 +178,11 @@ try {
cleanFloor: card._cleanFloorCache?.size ?? 0,
glowClip: card._glowClipCache?.size ?? 0,
wallUnion: card._wallUnionCache ? 1 : 0,
// #744: the union is pooled per floor, and the inner contours are the
// second structural cache of a floor visit; 0/1 above cannot see a
// cold floor whose union is rebuilt while its clean floor stays warm.
wallUnionPool: card._wallUnionPool?.size ?? 0,
innerContour: card._innerContourCache?.size ?? 0,
openingTunnel: card._openingTunnelCache ? 1 : 0,
openingWallIndex: card._openingWallIndexCache ? 1 : 0,
isoGeometry: card._isoGeometryCache?.size ?? 0,
@@ -255,7 +274,8 @@ try {
if (snapshot.renderedOverlayCount < 1) failures.push('no raised overlay roots rendered');
if (snapshot.raisedOverlayCount !== snapshot.renderedOverlayCount)
failures.push('one or more Stage 4 overlay roots remained outside the low visual plane');
if (snapshot.nudgedOverlayCount < 1) failures.push('dense fixture produced no bounded nudge');
if (snapshot.nudgedOverlayCount !== 0)
failures.push('raised overlay nudged although #713 lifts every tile by one rise');
if (snapshot.raisedVacuumCount !== 0) failures.push('floor-bound vacuum was marked as raised');
if (missingKinds.length) failures.push(`missing overlay kinds: ${missingKinds.join(', ')}`);
for (const kind of expectedKinds) {
@@ -405,6 +425,10 @@ try {
&& card.renderRoot.querySelectorAll('[data-hp="stair"]').length !== fixture.counts.stairs) {
throw new Error(`${profile} did not render the bounded maximum stair collection`);
}
// #743 precondition (docs/TESTING.md rule 3): the fixture carries the
// layer the profile guards, so a stable frame without the backdrop fails.
if (backdrop && !card.renderRoot.querySelector('.stage svg image.hp-backdrop'))
throw new Error(`${profile} rendered no backdrop image`);
const firstStableRenderMs = Number((performance.now() - loadStarted).toFixed(2));
const bootDiag = {
updates: card.__diag.updates,
@@ -714,7 +738,7 @@ try {
const currentView = card._viewOr(card._baseVb());
const currentDevice = card._devices.find((item) => item.id === device.dataset.id);
const position = currentDevice ? card._pos(currentDevice) : null;
const scene = position ? card._scenePoint([position.x, position.y]) : null;
const scene = position ? [position.x, position.y] : null;
const deviceRect = device.getBoundingClientRect();
const expectedX = scene ? rect.left + ((scene[0] - currentView.x) / currentView.w) * rect.width : 0;
const expectedY = scene ? rect.top + ((scene[1] - currentView.y) / currentView.h) * rect.height : 0;
@@ -1120,6 +1144,24 @@ try {
card._settingsDialog = null;
await card.updateComplete;
// #735: every sample mounts a new card, and until here it has visited
// only floors 1 and 2. The cycle would otherwise pay a first visit to
// floor 3 (and, after the interaction editor series moves the config
// epoch, to floor 1 again) inside its window, so one cold floor build
// dominated a metric documented as warmed navigation. Visit every
// fixture floor once in cycle order outside every timed and Long Task
// window, then return to floor 2 so the cycle still starts with 2 -> 1.
for (let floor = 1; floor <= fixture.counts.floors; floor++) {
card._pickSpace(`perf-floor-${floor}`);
await card.updateComplete;
await frame();
}
card._pickSpace('perf-floor-2');
await card.updateComplete;
await frame();
const switchCycleCachesBefore = cacheSnapshot(card);
const switchCycleBuildsBefore = isoStructuralBuildCount(card);
const switchCycle = await duration(async () => {
for (let index = 0; index < 12; index++) {
card._pickSpace(`perf-floor-${(index % fixture.counts.floors) + 1}`);
@@ -1130,6 +1172,37 @@ try {
await new Promise((done) => setTimeout(done, 0));
}
});
// #735 guard: a warmed cycle builds nothing. Any grown hot cache or a
// structural 2.5D build inside the window means a cold floor visit
// leaked back into switchCycleMs. Caches an older base lacks read as 0
// and cannot grow; its build counter is null and is not judged.
const switchCycleCachesAfter = cacheSnapshot(card);
const switchCycleBuildsAfter = isoStructuralBuildCount(card);
const switchCycleBuilt = Object.keys(switchCycleCachesAfter)
.filter((key) => switchCycleCachesAfter[key] > switchCycleCachesBefore[key])
.map((key) => `${key} +${switchCycleCachesAfter[key] - switchCycleCachesBefore[key]}`);
if (switchCycleBuildsBefore != null && switchCycleBuildsAfter !== switchCycleBuildsBefore)
switchCycleBuilt.push(`isoStructuralBuilds +${switchCycleBuildsAfter - switchCycleBuildsBefore}`);
if (switchCycleBuilt.length)
throw new Error(`${profile} switchCycle built a floor inside the window: ${switchCycleBuilt.join(', ')}`);
// #743 structural probe, untimed and outside every Long Task window: six
// warm switches round the floors (the cycle ended on the last floor, so
// each one is a real switch). Each counts the update passes from the
// pick until updateComplete reports quiescence (resolves true). Counting
// right after the first await would miss the #739 K1 second pass: it
// starts after that promise resolves, in the same task. The wait is
// bounded so an endless update loop yields a large count instead of a
// hung job. evaluate.mjs judges the candidate; a base is only reported.
const floorSwitchPasses = [];
if (backdrop) {
for (let index = 0; index < 6; index++) {
const passesBefore = card.__diag.updates;
card._pickSpace(`perf-floor-${(index % fixture.counts.floors) + 1}`);
for (let wait = 0; wait < 20 && !(await card.updateComplete); wait++);
floorSwitchPasses.push(card.__diag.updates - passesBefore);
}
}
await forceGc();
const cacheBefore = cacheSnapshot(card);
@@ -1207,6 +1280,7 @@ try {
? null : afterStateIsoStructuralBuilds - steadyIsoStructuralBuilds,
},
} : {}),
...(backdrop ? { floorSwitchPasses: { supported: true, perSwitch: floorSwitchPasses } } : {}),
...(finalStage3 ? {
isoStageRevision: finalStage3.isoStageRevision,
stage3Diagnostics: {
@@ -1224,7 +1298,7 @@ try {
fixture, sample: measuredSample, cardContract: LARGE_HOUSE_CARD_CONTRACT,
isometric, requiresIsometric, planSnap, requiresPlanSnap, requiresWallFace,
interaction, requiresInteraction, stage3Dense, requireStage3,
requiresIsoStructuralBuildCounter, requiresStairs, profile,
requiresIsoStructuralBuildCounter, requiresStairs, profile, backdrop,
});
// #520: диагностика печатается в лог прогона и в запись не попадает.
const { bootDiag, ...measured } = row;
+463
View File
@@ -0,0 +1,463 @@
#!/usr/bin/env node
/**
* #780 ТЗ §13.2: the `led-strips-v1` performance profile.
*
* The `large-house-v1` fixture (200 devices, 3 floors, 60 rooms, 100
* openings) with 10 or 50 of each floor's existing devices shown as LED
* strips that are on: `--size=10x5` (10 strips × 5 points) or `--size=50x50`
* (50 strips × 50 points); `--size=none` is the same build and plan without
* strips. No icon is added: the converted devices keep their ids, only their
* entity becomes a light. Light radius 30 cm (the strip default), Glow on,
* viewport 1440×1000, DPR 1, reduced motion. `--warmups` (≥1) samples are
* discarded, then `--samples` (≥7) are judged. Every sample is cold: a new
* browser with an empty cache mounts a new card; the warm metrics run in the
* same page after the module has loaded.
*
* Metrics (judged on median AND p95 against budgets-led-strips.json):
* firstStableRenderMs (mount → stripes and fields of every visible strip,
* cold import included), warmSpaceReadyMs (space switch with the module
* loaded → stable LED frame), stateUpdateMs (the interaction profile's method:
* hass update → every stripe in its new state), panZoomMs (the interaction
* profile's camera scenario, benchmark_large_house.mjs) and the longest Long
* Task of the camera — the scenario and the 100-step series, retained heap
* after 20 A→B→C→A cycles over a warm cycle (same GC protocol).
*
* Counters (exact, every sample): geometry/visibility recomputes over 100
* unrelated HA ticks, 100 pan/zoom steps and a colour-only change (0); the
* three LED caches of the shown space — shapes ≤ 50, visibility ≤ 50,
* retained per-emitter fans ≤ 2500 — identical after every cycle (growth 0);
* after disconnect 0 retained entries and 0 live LED timers/observers; a
* late import after disconnect restores nothing (one extra cold run with the
* runtime response delayed); LED chunk requests (none without strips, never
* the editor chunk in the View).
*
* `--merge=a.json,b.json` judges the rows of several partial reports of the
* same size as one run — the samples are the same cold samples, only the
* process that collected them differs (a runner with a short step limit);
* `--warmup-only` collects the discarded warm-up of such a run.
*/
import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { launch } from './serve.mjs';
import { makeLedStripsFixture } from './performance/led-strips-fixture.mjs';
const valueArg = (name) => process.argv.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
const warmupOnly = process.argv.includes('--warmup-only');
const samples = warmupOnly ? 0 : Math.max(1, Math.min(20, Number(valueArg('samples')) || 7));
const warmups = warmupOnly ? 1 : Math.max(0, Math.min(5, Number(valueArg('warmups') ?? 1)));
const size = valueArg('size') ?? '10x5';
const output = valueArg('output') ? resolve(valueArg('output')) : null;
const merge = valueArg('merge')?.split(',').filter(Boolean).map((file) => resolve(file)) ?? null;
const skipLateImport = process.argv.includes('--no-late-import');
const SIZES = { '10x5': [10, 5], '50x50': [50, 50], none: [0, 0] };
if (!SIZES[size]) throw new Error(`unknown size: ${size}`);
const [STRIPS, POINTS] = SIZES[size];
const PROFILE = 'led-strips-v1';
const CYCLES = Math.max(1, Math.min(20, Number(valueArg('cycles')) || 20));
const MIN_SAMPLES = 7;
const fixture = makeLedStripsFixture(STRIPS, POINTS);
const sourceSha = (() => {
try { return execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); } catch { return null; }
})();
const budgets = JSON.parse(readFileSync(new URL('./performance/budgets-led-strips.json', import.meta.url), 'utf8'));
/**
* Installed in the page before the card mounts anything: timers, frames and
* observers created from an LED chunk (its file name is in the creation
* stack) are tracked until they fire, are cleared or disconnected.
*/
function installLedLifecycleTracker() {
if (window.__ledLive) return;
const live = { timers: new Set(), frames: new Set(), observers: new Set() };
window.__ledLive = live;
const fromLed = () => /led-strip-/.test(new Error().stack || '');
const nativeTimeout = window.setTimeout, nativeClearTimeout = window.clearTimeout;
window.setTimeout = function setTimeout(fn, ms, ...rest) {
const mine = fromLed();
let id = 0;
id = nativeTimeout.call(window, typeof fn === 'function'
? (...args) => { live.timers.delete(id); return fn(...args); } : fn, ms, ...rest);
if (mine) live.timers.add(id);
return id;
};
window.clearTimeout = function clearTimeout(id) { live.timers.delete(id); return nativeClearTimeout.call(window, id); };
const nativeInterval = window.setInterval, nativeClearInterval = window.clearInterval;
window.setInterval = function setInterval(fn, ms, ...rest) {
const id = nativeInterval.call(window, fn, ms, ...rest);
if (fromLed()) live.timers.add(id);
return id;
};
window.clearInterval = function clearInterval(id) { live.timers.delete(id); return nativeClearInterval.call(window, id); };
const nativeFrame = window.requestAnimationFrame, nativeCancelFrame = window.cancelAnimationFrame;
window.requestAnimationFrame = function requestAnimationFrame(fn) {
const mine = fromLed();
let id = 0;
id = nativeFrame.call(window, (time) => { live.frames.delete(id); return fn(time); });
if (mine) live.frames.add(id);
return id;
};
window.cancelAnimationFrame = function cancelAnimationFrame(id) { live.frames.delete(id); return nativeCancelFrame.call(window, id); };
for (const name of ['ResizeObserver', 'MutationObserver', 'IntersectionObserver']) {
const Native = window[name];
if (!Native) continue;
window[name] = class extends Native {
constructor(callback) {
super(callback);
if (fromLed()) live.observers.add(this);
}
disconnect() { live.observers.delete(this); return super.disconnect(); }
};
}
}
/** The page side of one mount: the same fixture host as before, shared by the sample and the late-import run. */
function pageHost() {
return `
window.__ledHost = (fixture) => {
const card = document.createElement('houseplan-card');
card.setConfig({ type: 'custom:houseplan-card', title: 'LED strips', icon_size: 3.4 });
let states = fixture.states;
const hassBase = {
language: 'en', locale: { language: 'en' },
user: { id: 'perf', name: 'Performance fixture', is_admin: true },
devices: fixture.devices, entities: fixture.entities, areas: fixture.areas,
callWS: async (message) => {
if (message.type === 'houseplan/config/get') return { config: structuredClone(fixture.config), rev: 1, can_write: true };
if (message.type === 'houseplan/layout/get') return { layout: structuredClone(fixture.layout), rev: 1 };
if (message.type === 'config/device_registry/list') return Object.values(fixture.devices);
if (message.type === 'config/entity_registry/list') return Object.values(fixture.entities);
return { ok: true };
},
callService: async () => undefined,
connection: { subscribeEvents: async () => () => undefined, subscribeMessage: async () => () => undefined },
localize: () => null,
formatEntityState: (state) => state.state,
config: { unit_system: { length: 'km' } },
};
return {
card,
get states() { return states; },
setStates(next) { states = next; card.hass = { ...hassBase, states }; },
};
};
window.__ledStats = async (card) => {
const url = performance.getEntriesByType('resource').map((entry) => entry.name)
.find((name) => /led-strip-runtime-[^/]+\\.js/.test(name));
if (!url) return { shapes: 0, visibility: 0, sources: 0, recomputes: 0, loaded: false };
const runtime = await import(url);
return { ...runtime.ledStats(card), loaded: true };
};
window.__ledLiveCounts = () => ({
timers: window.__ledLive.timers.size, frames: window.__ledLive.frames.size,
observers: window.__ledLive.observers.size,
});
`;
}
async function sample() {
const { page, browser } = await launch({ width: 1440, height: 1000 }, 1,
['--enable-precise-memory-info', '--js-flags=--expose-gc']);
const chromium = await browser.version();
await page.emulateMedia({ reducedMotion: 'reduce' });
const requests = [];
page.on('request', (request) => {
const name = request.url().replace(/.*\//, '').replace(/\?.*$/, '');
if (/^led-strip-(runtime|field|editor)-/.test(name)) requests.push(name.replace(/-[^-]+\.js$/, ''));
});
try {
await page.evaluate(installLedLifecycleTracker);
await page.evaluate(pageHost());
const row = await page.evaluate(async ({ fixture, strips, cycles }) => {
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
const until = async (predicate, timeout = 30000) => {
const started = performance.now();
while (!predicate()) {
if (performance.now() - started > timeout) throw new Error('led-strips-v1 timed out');
await sleep(5);
}
};
const longTasks = () => {
const entries = [];
const observer = new PerformanceObserver((list) => entries.push(...list.getEntries()));
observer.observe({ type: 'longtask', buffered: false });
return async () => {
await sleep(0);
entries.push(...observer.takeRecords());
observer.disconnect();
return Number(Math.max(0, ...entries.map((entry) => entry.duration)).toFixed(2));
};
};
const gc = async () => { if (globalThis.gc) { globalThis.gc(); await frame(); globalThis.gc(); await frame(); } };
window.__card?.remove?.();
localStorage.clear();
const host = window.__ledHost(fixture);
const { card } = host;
const root = () => card.renderRoot;
const count = (selector) => root()?.querySelectorAll(selector).length ?? 0;
const ledStable = (state = 'on') => count(`[data-led-strip][data-state="${state}"]`) === strips
&& (state !== 'on' || count('.led-fields [data-led-field]') === strips);
const stats = () => window.__ledStats(card);
const started = performance.now();
document.getElementById('host').replaceChildren(card);
host.setStates(host.states);
await until(() => card._loadOk && card._model?.length === 3);
if (strips) await until(() => ledStable());
else { await until(() => card._booting === false); await frame(); }
await frame();
const firstStableRenderMs = Number((performance.now() - started).toFixed(2));
const warm = performance.now();
card._pickSpace('perf-floor-2');
await card.updateComplete;
if (strips) await until(() => ledStable());
await frame();
const warmSpaceReadyMs = Number((performance.now() - warm).toFixed(2));
const states = host.states;
const floorLights = Object.keys(states).filter((id) => id.startsWith('light.perf_led_1_'));
const stateStarted = performance.now();
host.setStates({ ...states, ...Object.fromEntries(floorLights.map((id) => [id, { ...states[id], state: 'off' }])) });
await card.updateComplete;
if (strips) await until(() => ledStable('off'));
await frame();
const stateUpdateMs = Number((performance.now() - stateStarted).toFixed(2));
host.setStates({ ...states, ...Object.fromEntries(floorLights.map((id) => [id, { ...states[id], state: 'on' }])) });
await card.updateComplete;
if (strips) await until(() => ledStable());
await frame();
const before = await stats();
// 100 unrelated HA ticks.
const sensor = Object.keys(host.states).find((id) => id.startsWith('sensor.'));
for (let tick = 0; tick < 100; tick++) {
host.setStates({ ...host.states, [sensor]: { ...host.states[sensor], state: String(20 + (tick % 10) / 10) } });
await card.updateComplete;
}
await frame();
const afterTicks = await stats();
// The camera: the interaction profile's scenario (one wheel step, its
// render and frame — benchmark_large_house.mjs), then 100 steps whose
// Long Tasks are judged too and whose recomputes are counted.
const stage = root().querySelector('.stage');
const rect = stage.getBoundingClientRect();
const wheel = (deltaY) => stage.dispatchEvent(new WheelEvent('wheel', {
deltaY, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, bubbles: true, cancelable: true,
}));
const stopPan = longTasks();
const panStarted = performance.now();
wheel(-120);
await card.updateComplete;
await frame();
const panZoomMs = Number((performance.now() - panStarted).toFixed(2));
const panZoomLongTaskMaxMs = await stopPan();
const stopSeries = longTasks();
// One wheel step per frame, as a real wheel or trackpad delivers them.
for (let step = 0; step < 100; step++) {
wheel(step % 2 ? 60 : -60);
await card.updateComplete;
await frame();
}
await frame();
const cameraSeriesLongTaskMaxMs = await stopSeries();
const afterCamera = await stats();
// A colour-only change rebuilds no geometry or visibility.
host.setStates({ ...host.states, ...Object.fromEntries(floorLights.map((id) => [id,
{ ...host.states[id], attributes: { ...host.states[id].attributes, rgb_color: [255, 120, 80] } }])) });
await card.updateComplete;
await frame();
const afterColour = await stats();
// 20 cycles A→B→C→A after one warm cycle; the three caches identical after each.
const cycle = async () => {
for (const id of ['perf-floor-1', 'perf-floor-2', 'perf-floor-3', 'perf-floor-1']) {
card._pickSpace(id);
await card.updateComplete;
if (strips) await until(() => ledStable());
}
await frame();
const { shapes, visibility, sources } = await stats();
return { shapes, visibility, sources };
};
await cycle();
await gc();
const heapBefore = performance.memory?.usedJSHeapSize ?? null;
const cycleStats = [];
for (let index = 0; index < cycles; index++) cycleStats.push(await cycle());
await gc();
const heapAfter = performance.memory?.usedJSHeapSize ?? null;
const liveBeforeDisconnect = window.__ledLiveCounts();
card.remove();
await frame();
await sleep(600);
const afterDisconnect = await stats();
const liveAfterDisconnect = window.__ledLiveCounts();
const key = (entry) => `${entry.shapes}/${entry.visibility}/${entry.sources}`;
return {
firstStableRenderMs, warmSpaceReadyMs, stateUpdateMs, panZoomMs, panZoomLongTaskMaxMs, cameraSeriesLongTaskMaxMs,
retainedHeapBytes: heapBefore == null || heapAfter == null ? null : Math.max(0, heapAfter - heapBefore),
counters: strips ? {
recomputesOnHaTicks: afterTicks.recomputes - before.recomputes,
recomputesOnCamera: afterCamera.recomputes - afterTicks.recomputes,
recomputesOnColour: afterColour.recomputes - afterCamera.recomputes,
shapes: Math.max(...cycleStats.map((entry) => entry.shapes)),
visibility: Math.max(...cycleStats.map((entry) => entry.visibility)),
sources: Math.max(...cycleStats.map((entry) => entry.sources)),
cacheGrowthOverCycles: new Set(cycleStats.map(key)).size - 1,
} : null,
disconnect: {
retained: afterDisconnect.shapes + afterDisconnect.visibility + afterDisconnect.sources,
liveBefore: liveBeforeDisconnect,
live: liveAfterDisconnect.timers + liveAfterDisconnect.frames + liveAfterDisconnect.observers,
},
};
}, { fixture, strips: STRIPS, cycles: CYCLES });
row.ledRequests = [...new Set(requests)].sort();
return { row, chromium };
} finally {
await browser.close();
}
}
/**
* The late import: the card is removed while the runtime chunk is still on
* the wire (its response is held back); when it lands nothing is rendered,
* cached or scheduled for the gone card.
*/
async function lateImport() {
const { page, browser } = await launch({ width: 1440, height: 1000 }, 1, []);
try {
let held = false;
let runtimeUrl = null;
await page.route(/led-strip-runtime-[^/]+\.js/, async (route) => {
held = true;
runtimeUrl = route.request().url();
await new Promise((done) => setTimeout(done, 1500));
await route.fallback();
});
await page.evaluate(installLedLifecycleTracker);
await page.evaluate(pageHost());
const result = await page.evaluate(async ({ fixture }) => {
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
window.__card?.remove?.();
const host = window.__ledHost(fixture);
const { card } = host;
document.getElementById('host').replaceChildren(card);
host.setStates(host.states);
const started = performance.now();
while (!(card._loadOk && card._model?.length === 3) && performance.now() - started < 20000) await sleep(10);
await sleep(300);
const before = card.renderRoot.querySelectorAll('[data-led-strip]').length;
card.remove();
window.__lateCard = card;
await sleep(3000);
return { stripesBeforeLoad: before, stripesAfterLoad: card.renderRoot.querySelectorAll('[data-led-strip]').length };
}, { fixture });
// The chunk the card asked for, by its URL: the same module instance.
const late = await page.evaluate(async (url) => {
const runtime = url ? await import(url) : null;
return { stats: runtime ? { ...runtime.ledStats(window.__lateCard), loaded: true } : { shapes: 0, visibility: 0, sources: 0, loaded: false },
live: window.__ledLiveCounts() };
}, runtimeUrl);
return { ...result, ...late, held };
} finally {
await browser.close();
}
}
let rows = [];
let chromium = null;
let late = null;
let discarded = 0;
if (merge) {
for (const file of merge) {
const part = JSON.parse(readFileSync(file, 'utf8'));
if (part.profile !== PROFILE || part.size !== size) throw new Error(`${file} is not ${PROFILE} ${size}`);
rows.push(...part.rows);
discarded += part.warmups;
chromium ??= part.chromium;
late ??= part.lateImport ?? null;
}
} else {
for (let iteration = 0; iteration < warmups + samples; iteration++) {
const result = await sample();
chromium ??= result.chromium;
if (iteration >= warmups) rows.push(result.row);
}
discarded = warmups;
if (STRIPS && !skipLateImport && !warmupOnly) late = await lateImport();
}
if (warmupOnly) {
// A discarded warm-up sample for a later --merge: nothing is judged here.
const text = `${JSON.stringify({ profile: PROFILE, size, sourceSha, chromium, samples: 0, warmups: 1, rows: [], partial: true }, null, 2)}\n`;
if (output) { mkdirSync(dirname(output), { recursive: true }); writeFileSync(output, text); }
console.log(text);
process.exit(0);
}
const metric = (name) => {
const values = rows.map((row) => row[name]).filter((value) => Number.isFinite(value)).sort((a, b) => a - b);
const at = (q) => values[Math.min(values.length - 1, Math.ceil(q * values.length) - 1)];
return { median: values.length ? at(0.5) : null, p95: values.length ? at(0.95) : null, samples: values };
};
const METRICS = ['firstStableRenderMs', 'warmSpaceReadyMs', 'stateUpdateMs', 'panZoomMs', 'panZoomLongTaskMaxMs',
'cameraSeriesLongTaskMaxMs', 'retainedHeapBytes'];
const report = {
profile: PROFILE, size, strips: STRIPS, points: POINTS, sourceSha, chromium,
samples: rows.length, warmups: discarded, cycles: CYCLES, viewport: { width: 1440, height: 1000 }, dpr: 1,
merged: merge ? merge.length : undefined,
metrics: Object.fromEntries(METRICS.map((name) => [name, metric(name)])),
counters: rows[0]?.counters ?? null,
counterSamples: rows.map((row) => row.counters),
disconnectSamples: rows.map((row) => row.disconnect),
lateImport: late,
ledRequests: [...new Set(rows.flatMap((row) => row.ledRequests))].sort(),
rows,
};
const failures = [];
const partial = !merge && (samples < MIN_SAMPLES || warmups < 1);
if (!partial && (report.samples < MIN_SAMPLES || report.warmups < 1)) {
failures.push(`${report.samples} samples after ${report.warmups} warmups: the profile needs ≥ ${MIN_SAMPLES} after ≥ 1`);
}
const limits = budgets.sizes[size] || {};
for (const [name, limit] of Object.entries(limits)) {
for (const stat of ['median', 'p95']) {
const value = report.metrics[name]?.[stat];
if (value == null) { if (name !== 'retainedHeapBytes') failures.push(`${name} ${stat} missing`); continue; }
if (value > limit) failures.push(`${name} ${stat} ${value} > ${limit}`);
}
}
for (const [index, disconnect] of report.disconnectSamples.entries()) {
if (disconnect.retained !== 0) failures.push(`sample ${index}: ${disconnect.retained} LED cache entries retained after disconnect`);
if (disconnect.live !== 0) failures.push(`sample ${index}: ${disconnect.live} LED timers/frames/observers alive after disconnect`);
}
if (STRIPS) {
const caches = budgets.caches;
for (const counters of report.counterSamples) {
for (const key of ['recomputesOnHaTicks', 'recomputesOnCamera', 'recomputesOnColour', 'cacheGrowthOverCycles']) {
if (counters[key] !== 0) failures.push(`${key} = ${counters[key]}, expected 0`);
}
for (const key of ['shapes', 'visibility', 'sources']) {
if (counters[key] > caches[key]) failures.push(`${key} cache ${counters[key]} > ${caches[key]}`);
}
}
if (late) {
if (!late.held || !late.stats.loaded) failures.push('late import: the runtime response was never held or never landed');
if (late.stripesAfterLoad !== late.stripesBeforeLoad) failures.push('late import rendered the gone card');
const retained = late.stats.shapes + late.stats.visibility + late.stats.sources;
if (retained) failures.push(`late import restored ${retained} cache entries`);
if (late.live.timers + late.live.frames + late.live.observers) failures.push('late import left LED timers/observers');
} else if (!skipLateImport) failures.push('late import not measured');
if (!report.ledRequests.includes('led-strip-runtime') || !report.ledRequests.includes('led-strip-field'))
failures.push(`LED chunks not loaded: ${report.ledRequests.join(', ')}`);
} else if (report.ledRequests.length) failures.push(`no strips but LED chunks requested: ${report.ledRequests.join(', ')}`);
if (report.ledRequests.includes('led-strip-editor')) failures.push('the View loaded the LED editor chunk');
report.failures = failures;
report.partial = partial || undefined;
const text = `${JSON.stringify(report, null, 2)}\n`;
if (output) { mkdirSync(dirname(output), { recursive: true }); writeFileSync(output, text); }
console.log(JSON.stringify({ ...report, rows: undefined }, null, 2));
if (failures.length) { console.error(`led-strips-v1 ${size}: ${failures.length} failure(s)`); process.exit(1); }
+66 -4
View File
@@ -136,7 +136,53 @@ const applianceSpace = {
decor: [],
};
const runtime = (includeAppliance = false) => {
// #780: the LED strip reference scene — the designer's four strips on a
// synthetic plan with the reference floor colour #868D94: a free straight
// strip, a strip on the inner face of the bottom wall, a polyline along the
// wall faces around a corner and a closed rectangle. The colours are scene
// values of the four light sources, not a product palette.
const ledRooms = [
{ id: 'led-living', name: 'LED living', area: 'golden_led_living',
poly: [[0.06, 0.08], [0.56, 0.08], [0.56, 0.62], [0.06, 0.62]] },
{ id: 'led-studio', name: 'LED studio', area: 'golden_led_studio',
poly: [[0.56, 0.08], [0.94, 0.08], [0.94, 0.62], [0.56, 0.62]] },
];
// Inner faces of the 20 cm walls (half thickness = 10 cm = 2 cells of 1/240).
const LED_FACE = 2 / 240;
export const LED_REFERENCE_STRIPS = Object.freeze([
{ id: 'led-free', marker: 'demo-led-strip', color: [128, 213, 255],
points: [[0.14, 0.26], [0.44, 0.26]] },
{ id: 'led-wall', marker: 'demo-led-strip-wall', color: [230, 128, 255],
points: [[0.12, round(0.62 - LED_FACE)], [0.50, round(0.62 - LED_FACE)]] },
{ id: 'led-corner', marker: 'demo-led-strip-corner', color: [255, 234, 128],
points: [[0.64, round(0.08 + LED_FACE)], [round(0.94 - LED_FACE), round(0.08 + LED_FACE)],
[round(0.94 - LED_FACE), 0.42]] },
{ id: 'led-loop', marker: 'demo-led-strip-loop', color: [88, 255, 88],
points: [[0.66, 0.30], [0.84, 0.30], [0.84, 0.52], [0.66, 0.52], [0.66, 0.30]] },
]);
const ledSpace = {
id: 'golden-led',
title: 'LED strips',
plan_url: null,
view_box: [0, 0, 1, 0.7],
cell_cm: 5,
settings: {
fill_mode: 'custom', custom_fill: { c: '#868D94', a: 1 }, glow_enabled: true,
show_borders: true, show_names: false, sun_rays: false, bg_mode: 'static',
},
rooms: ledRooms,
walls: wallsFor('led', ledRooms, 20),
openings: [
{ id: 'led-door', type: 'door', x: 0.56, y: 0.53, angle: 90, length: 0.10 },
],
partitions: [],
wall_columns: [],
decor: [],
led_strips: LED_REFERENCE_STRIPS.map(({ id, marker, points }) => ({ id, marker, points })),
};
const runtime = (includeAppliance = false, includeLed = false) => {
const devices = {};
const entities = {};
const states = {
@@ -199,6 +245,15 @@ const runtime = (includeAppliance = false) => {
unit_of_measurement: 'lqi',
});
if (includeLed) {
for (const room of ledRooms) areas[room.area] = { area_id: room.area, name: room.name };
for (const strip of LED_REFERENCE_STRIPS) {
add(strip.marker, 'light', ledRooms[strip.id === 'led-free' || strip.id === 'led-wall' ? 0 : 1].area,
0.5, 0.5, 'on', { rgb_color: strip.color, brightness: 255 });
layout[strip.marker] = { s: 'golden-led', x: 0.5, y: 0.35 };
}
}
if (includeAppliance) {
const washerId = 'golden-washer';
devices[washerId] = {
@@ -247,16 +302,23 @@ export const VISUAL_MATRIX_COUNTS = Object.freeze({
columns: geometrySpace.wall_columns.length + lightingSpace.wall_columns.length,
});
export const makeVisualMatrixFixture = ({ applianceLifecycle = false } = {}) => ({
export const makeVisualMatrixFixture = ({ applianceLifecycle = false, ledStrips = false } = {}) => ({
config: {
spaces: [
structuredClone(geometrySpace), structuredClone(lightingSpace),
...(applianceLifecycle ? [structuredClone(applianceSpace)] : []),
...(ledStrips ? [structuredClone(ledSpace)] : []),
],
// A persisted marker is part of the fixture contract for scenarios that
// override per-source Glow controls. The device/layout alone are not a
// saved marker configuration and must not be silently treated as one.
markers: [{ id: 'golden-light-two', binding: 'device:golden-light-two' }],
markers: [
{ id: 'golden-light-two', binding: 'device:golden-light-two' },
// #780: a strip link writes the marker's explicit space (ТЗ §5).
...(ledStrips ? LED_REFERENCE_STRIPS.map(({ marker }) => ({
id: marker, binding: `device:${marker}`, space: 'golden-led',
})) : []),
],
settings: {
glow_radius_cm: 360,
north_deg: 0,
@@ -269,7 +331,7 @@ export const makeVisualMatrixFixture = ({ applianceLifecycle = false } = {}) =>
},
},
},
...runtime(applianceLifecycle),
...runtime(applianceLifecycle, ledStrips),
counts: applianceLifecycle ? {
...VISUAL_MATRIX_COUNTS,
spaces: VISUAL_MATRIX_COUNTS.spaces + 1,
+103 -71
View File
@@ -1,8 +1,8 @@
{
"schema": 3,
"matrixVersion": 67,
"acceptedAt": "2026-09-28T08:34:22.291Z",
"sourceFingerprint": "cdf2180b22b4a958d7d5f678094df362c098ea8c66aa7ff6c0db96f3ae21cc90",
"matrixVersion": 71,
"acceptedAt": "2026-10-03T07:54:16.450Z",
"sourceFingerprint": "cd698d703d8d3b58af43c32e9d23153a4532650909eadfd43b25ed1969ec4f4a",
"chromium": "151.0.7922.34",
"capturedOn": "linux",
"acceptedOn": "linux",
@@ -10,30 +10,53 @@
"platform": "linux",
"arch": "x64",
"chromium": "151.0.7922.34",
"buildFingerprint": "cdf2180b22b4a958d7d5f678094df362c098ea8c66aa7ff6c0db96f3ae21cc90",
"ci": {
"buildFingerprint": "cd698d703d8d3b58af43c32e9d23153a4532650909eadfd43b25ed1969ec4f4a",
"ci": null
},
"localAttestation": {
"schema": "houseplan-golden-wsl/v1",
"sha256": "cf474aa1ff676d2262f1c9e442114e7df0e91867c69c78bdc82c923a48355650",
"artifactSha256": "7a410b63ae24027ebe7c77d3e5fe63b2e36cad6d83543bf478c8af183784e2dc",
"createdAt": "2026-10-03T07:53:46.175Z",
"source": {
"repository": "Matysh/houseplan-card",
"run": 36397286486,
"attempt": 1,
"sha": "0a5b34a571caba8638a51ecbd6d496c064e7b040"
"branch": "issue/784-led-strip-polish",
"commit": "706eb02d70b6fecaafda0da67e4564e8909606ff",
"tree": "f9331d65ecb02f922720f95f9a6feb6781420f29",
"remoteSha": "706eb02d70b6fecaafda0da67e4564e8909606ff",
"clean": true,
"status": ""
},
"environment": {
"platform": "linux",
"arch": "x64",
"kernel": "6.18.33.2-microsoft-standard-WSL2",
"wsl": true,
"distro": "Ubuntu",
"filesystem": "ext2/ext3"
},
"toolchain": {
"node": "22.23.2",
"npm": "10.9.8",
"playwright": "1.62.0",
"chromiumExecutableSha256": "0b20b130e7edd9dd51873be867761295fe0cfad490c2b9a64f95bd3cfc08fa71"
}
},
"localAttestation": null,
"foreignCapture": null,
"witnesses": {
"count": 138,
"count": 101,
"floor": 10
},
"scenarios": {
"isometric-stage6-light-lightfloor": "80ac5d4ed1535fbaa1420b5da1a35599cc8ef9a35e4fa723c89e25ef235f792e",
"isometric-stage6-light-darkfloor": "46e25d32a4e4f156586a327cdfbc6f59e79178733b6301c89843b8a8535a738a",
"isometric-stage6-dark-darkfloor": "5b76746846c13c2dbca9ade31d4e6cb891a9a486029ebcf4008539a4a92b47fc",
"isometric-stage6-dark-lightfloor": "157e0aaf68396b8ed5acc0d2caed935dc0d17c014c265cb2aaeda66944bba8c2",
"isometric-stage6-hover-light": "3f8dc5052e883a30e3cf89c1a0fdea0af3bae3c763b6ebaa50e41cba9cdb3bfd",
"stairs-flat-normal-light": "ac5035d8cb57f1e08d2c4b60940b4f2659a780e1efefc7e2b05bbf4c3f11da8c",
"stairs-flat-hover-dark": "8ac953c6d5ef600b8ef462b7e5b5e4e5d378205f600b8089932e33cbc193bacb",
"stairs-flat-selected-light": "8affa28f66f80bb3f911cde48ed1121db24d278f4e464a8ce1e53a5ca6efc0df",
"stairs-isometric-dark": "72a6f2490b7f18e9f68f482d9cc960bb4a7beb7e799082ade701c751bbb7bd3f",
"isometric-stage6-light-lightfloor": "edeadb080b4e9f1114ee30189a37c06bfbf6b1a4657201728fe1dbfc5a8cbf55",
"isometric-stage6-light-darkfloor": "2763249d1ad93404f41d4bd725eea2c1a3a57ee34930eb5a9fa8c5826d8aeae5",
"isometric-stage6-dark-darkfloor": "1ff0cb3a79d61c0a8debb36e4d14c8ff8e9d0698e6920c6b3f6250738d26e32c",
"isometric-stage6-dark-lightfloor": "b871df843117371fb30ae195eb2b4bcc2460bb9f205c60884c13341e64d3150e",
"isometric-stage6-hover-light": "67bf3b08548a69a8354d2bb432c6ac5b98670a2732ef63f3e710726c9197f6d7",
"stairs-flat-normal-light": "abd698576bb726bc70226deb72c12c3b7ebf36dd3007186e5d9444d630075bef",
"stairs-flat-hover-dark": "2502c9526ce73017e465bd35a8609afa0dbcac8dfa34ef37cfad8b3f39ebcd52",
"stairs-flat-selected-light": "c395ae3969521f6a157ff6e9460971db26287f9a5ca73f6ecbba6e8d35c3424f",
"stairs-isometric-dark": "7044fa5ab38b1cbc554391360db4e0906969fba8db238751c8ff6ea113fcf54c",
"panel-wide-view-light-en": "275d09b3730361bb491a832fbb76723d90a7327086f042cbf0fdd79f4a002800",
"panel-wide-plan-editor-dark-en": "093a3fd1022755f5128261796e009383d7e7a23e9e50f193ab1987d30f9313b2",
"panel-narrow-read-only-empty-light-en": "99b8f1af82b1ad13f237125aab8f7e7da0563c138e9da2ddf06e54c2d56138c4",
@@ -51,17 +74,17 @@
"split-corner-wall-thin-dark": "0a8da611ecb6061852cc26ee53c00d12433cf8539681bd91dac3adad1d7e9e67",
"split-corner-wall-thick-dark": "2efc65ddaa8777ebffe63205e2a961fa7053c91d3cd8c0af59810bef01ecd498",
"split-zero-divider-taper-dark": "f7f9ee435da00dcae043c693127561a5e050675ca4d6f6f0d916ac5dbe24a594",
"isometric-geometry-view-dark": "877cde0e1037071a23b3356141c1ddd1a74bcd33da5367ada2809ca6c3ee989b",
"isometric-geometry-view-light": "50164a84574bb81b860db94fd07cc5173eb676d4371deb90f3d2e16f5d252398",
"isometric-live-layers-dark": "1c3a54350b3bb07b4aa34bf853d22f12622616e1c8208f9d64c5e478c7d92bb2",
"isometric-no-borders-dark": "406fa7c7597724cba00df6451a1b81d5c306af0259cc9198e1985b6f325d619d",
"isometric-touch-kiosk-dark": "e88e95124300b723e6a3b4563961f7d178a2517f41f7b4a2c9fac3534a57c24f",
"isometric-large-warm-remount-dark": "d7932b8baea6b5689480a123698d562bcea2eb0d8536c3516272f5ebef4a4cc8",
"isometric-stage3-overlays-light": "3824e3c9901a53a19d2a5e069922e982bc2c8bdf21fc5ba6e8e6477bc2bb1f6d",
"isometric-stage3-overlays-dark": "adb042cf55b0c0e75412eea9167aec1df03e24cadb72e8e67d259927d687009b",
"isometric-stage3-openings-dark": "33c7c657229b44a3a52bbefe0197302d909b69dc0f1127352410d13f6d0d93f7",
"isometric-stage3-forced-colors-dark": "06675a2bf71d405c34dc430e1761f5cb78168d5b52207bb2ca7923f6dd8ce50b",
"isometric-stage3-no-filter-dark": "bdec8fce7628934e00a29b2c489226c8b0ca33b3e315fdd34c1f89a804dc125e",
"isometric-geometry-view-dark": "dd2a2b7bf3f80998ff862fb740d9eba6d92239d15c74d52529fe1bcf25974760",
"isometric-geometry-view-light": "72ab80a0d826657266ed1d7f4bcc9ecd482c05bd9d71dbbafad5079d287d5d95",
"isometric-live-layers-dark": "07e0eb592e91eef3bd960d2421bf4b1b77ff977296470e4ffed3aac3acb66b69",
"isometric-no-borders-dark": "65b24c7d1236adfd18636b23b75a1bc60a1823e3d3ea537259f8d8749cbd9439",
"isometric-touch-kiosk-dark": "71192d83e38a8d306f6a24fdd422faf2917f97a9a95484a0b0bdf6932a447601",
"isometric-large-warm-remount-dark": "85b212f1bb65b4500e4c24459d1eed333d8ba8c8864fd06d4844e91c39ea707d",
"isometric-stage3-overlays-light": "51c87e932f59bde985ec51fc670a81abf1b5ae69c42a0ad9486897bb448eac27",
"isometric-stage3-overlays-dark": "7f36dee09f08c4c0373bca28e3cdb19c8f7424885649841149d88fbbc5a7c992",
"isometric-stage3-openings-dark": "0a7a6dc33cd47191397f7677da106ec42bd04872b08b7ead9ab76d133aeb1a7d",
"isometric-stage3-forced-colors-dark": "8b86a9b7d32259b1bd86bb8626523ac36d4112a5411399c174ed730b760b7fc7",
"isometric-stage3-no-filter-dark": "c6617a286c3819fb0e96fcf2e3f5be1c3ff36af762bd88b718631719e31cda86",
"geometry-view-dark-fit": "454da1248f1fe13b8f60c589ae9ebee5e852995e9bc5175d3c5692c445f7cb94",
"geometry-view-light-fit": "de5f6a41552447ca909d20471ed6665f558fb6e111c1f333eeba55866517d5a5",
"pdf-export-geometry-light": "c403a141cefb73e4653f2ffb9c346b4cfaa3de339b6c39c227ef1552ebde0048",
@@ -78,6 +101,10 @@
"day-cycle-day-dark": "605849bbada8d3c3a02a2c88c7b21c81c675a7a89b72c4c560df635d63005e67",
"day-cycle-dusk-dark": "d19cc97b75014d9f0661705df50d5f30a3a574661957c04c5dab96e26c5e3364",
"day-cycle-night-dark": "39a6a959676295eb4ead02703533641f57901ad0ce1359933e1d47f48867196a",
"day-cycle-night-moon-gibbous-dark": "89ba8cdc95316a670486900b2b1aaf7b4c8889b388b54d16e3a645f1e5d41020",
"day-cycle-dusk-moon-crescent-south-dark": "03022fc1a57e05b9ec6ace88224d5bded6ee2335bb6b5715089edaf224ce0a4e",
"static-bg-moon-gibbous-white-light": "f23cd2ca25a0e18a2d3e6df762e2b0b04f1e89b77bee20fdded895bf9a155112",
"static-bg-moon-crescent-south-dark": "bb7d6a2f80741ce77c39a2258c6a28bbd1cc158ff316bb063a2d85ee63d01059",
"geometry-plan-editor-dark": "5e7365ef5820a9c2ea51abd3be0c56900de7932e3da4f78fc8103f307a5fcf65",
"safe-resize-handles-clamp-light": "80b3cf9c755dbb77deceaf6deb4569bd1570093509b37e96beb32dd2a07d40da",
"safe-resize-handles-clamp-dark": "dd32a1034f349b27717a89516b91a85b916e70080cfeffb127b0b51a24683bff",
@@ -96,29 +123,29 @@
"orthogonal-strip-cell-5-view-dark": "267a3eab1f4ad7d8d484eb2a58b3e38031b0516ca8c46c93e272bfaf9ac409b8",
"orthogonal-strip-cell-1-view-dark": "7c9bab0789f9ec9019edf3c49176415af83e8a325e833f98a0e6324e7f06fe4c",
"wall-key-roundtrip-view-dark": "74fd46cffefd32d805296d844c162dbe6f15b13e86c4d6c7f6974293b253e1b4",
"isometric-wall-junctions-dark": "e6bf156d2afb70a631ceade26d6431caf92721a28a661fffd1654f49711fcb3e",
"isometric-wall-junctions-dark": "5159bfe6cd10543e10cf5c98e490ecee877aa6021e0843dd2e2584250540e276",
"opening-placement-door-thick-wall-dark": "cef2c75a6c60197c24a848fab7e47919437c751aae79ec4d88d744eb70abed43",
"opening-placement-passage-thick-wall-dark": "8d089364f6f9aace8a0cccad952731e6d295440b9eb1c8cb75b6e2264b8fa5c5",
"opening-placement-passage-thick-wall-light": "14c06cd119a476f8a4edb63db37d55c6d4000f6fbb3ed412cb88e225574896a6",
"opening-symbol-room-wall-light": "dc3f6513a08b809a77df6822f59fdb30c912f320e55d47d749963678fbb8f417",
"static-hatch-openings-132-dpr1-light": "0870c63cd3555d6d72f54298b32b302a6e83a82fcab537b13980f521a933e70f",
"static-hatch-openings-140-dpr1-dark": "7fed107c1d8040c5ba6bd109aae959df18aeed081f40771aad4497f6c18aa9d9",
"static-hatch-openings-132-dpr2-dark": "8544b5438d09e4c60df91cc85c60a551c09e001ee3fc6a5509a819c94d0efc94",
"static-hatch-openings-140-dpr2-light": "9cde00004d3d62ae32174f7700eb5abeb1117dcf03939616c4a9521c2e6b08e0",
"static-hatch-openings-132-dpr1-light": "4d9f858e7d88113a3d65ed5ce96d7da3a60d0a9bb4a520d4522c59c6b1cadd1c",
"static-hatch-openings-140-dpr1-dark": "6e1571b0e49771d2e657dddc4fdd53ad64f0234ca0bd127c34a72f23e076319a",
"static-hatch-openings-132-dpr2-dark": "ea27355d19b6a4142a67dd37dab8498315047c01d517251b23059521659f6165",
"static-hatch-openings-140-dpr2-light": "482795eb2186c1cf4b914568a0c3349a864fce6b69c3c05d40a780e75f61e148",
"opening-symbol-diagonal-partition-dark": "fe264aa0a5926d904ea0a2d15e5f0567a536c9251a72bf96a287d7bf9c480aa1",
"opening-symbol-flip-pairs-light": "693fe97bae8375c6a16244fbb4ddaa1ed447732382a45d4629e1711e0aa0d45a",
"isometric-opening-symbol-parity-dark": "5dfcd219d9cdc41c382d6e436f15665c0a265ba2b7138b2856e1a50f0e252062",
"geometry-devices-editor-dark": "1cc18ac3250a0cc20981b090ef7f964555b237e085e3ddbe10cc7b6081342c8c",
"isometric-opening-symbol-parity-dark": "639e19dddf6d0a8e4f145b6850b0ad5d20d83100fd769e2aad678eb093a33067",
"geometry-devices-editor-dark": "0744447d86d071d5ad1c66e44ce3fdde40b6f06b36bdab59c5a2b2cd1da72292",
"device-inbox-desktop-en-light": "3a1229f1f5134c329ee457a62e555896ffd664d1d6cbd217e4e8248dc0c07dc7",
"device-inbox-desktop-ru-dark": "2b0981e7cc134a8f2cfe107c58833e33b862a00a89cadc5033f3b633562f9379",
"device-inbox-narrow-ru-dark": "7694119b368baf49fe8c0aeeec46a75bd77c0987266e4a64f5d13a2099e5f098",
"geometry-decor-editor-dark": "0fddb5ba477ecad15827215d04d22e9ba1adfacbbfc853712b1e76832182fa5f",
"device-inbox-narrow-ru-dark": "694b84a02e4122b973229488440e19e177b3d77dd8e9eb423304c709ffed6ad5",
"geometry-decor-editor-dark": "4077eb3f49182b5075f8cfd3c488cd6affa0fa26f7977ebb1e94cf57baabdc4e",
"tray-wide-selection-en": "e544ee6b4cde64aae188bac9be9e52c1232a3de8e98e1f68af42d8c7064f0a38",
"tray-wide-tool-ru": "d3c49be18142f280c21062612cf06dc8df6e5a076c53aeb2d62da8e3975c8003",
"tray-medium-group-en": "6ba416e0ca3cb2fba4a420703318e62d8651053d76ae1936a37537ab486af4a8",
"tray-medium-selection-ru": "d5a82d474f75f438c06a340897a9e009e7df0c95c5ef07edd3952f0fe43a0f40",
"tray-narrow-palette-en": "d2147c10ec130bead031db16090b0638188a548163ca8dfbfa86e68e71865051",
"furniture-categories-light": "8ffc0d18edc76dad428190f7c041ce9fdf9c5cc71a8329116bc1c7ae040a7fd8",
"furniture-categories-light": "4fdc9db78f6199abe86d1f982e9b1d0b71316f4ee0cbb8a1966742b141e0e0be",
"furniture-variants-dark": "368f863d070b8f2f02cf657731aab0e7d80bdf5a817193709bb2838d429ac4e1",
"furniture-variants-light": "a702b38b8bd42919a5ed8753b87489545eb6d6538726e5cc3cb856a586834974",
"furniture-placement-preview-light": "8a78103d7cdd2808496f6d73b8a65859c4e6b57ee53a0c18f4d90580945673ba",
@@ -158,48 +185,48 @@
"lighting-manual-auto-spill-overlap-dark": "6ebc10970e48f208ecda1e7b2bb86a26c90c903dade43e57c46cca720b41075d",
"hover-over-glow-dark": "eb4126af72e86b7a0b3ea0ea11a666fef3438b37ca574882f036ea5d44e6e938",
"hover-nested-room-dark": "b5337cb4c505f3b4983869bd7d4efd0a5be0d01e34dfa9f8619aba46b805dbef",
"junction-t-90-equal15-dark": "a8e367abbe57c79537f59ec81b92056895b4647ace12838c792e25c91e0a4a61",
"junction-t-90-bar50-leg15-dark": "2dd9dc7d92af63efb7d0aee0594964acad0f150aa353fe39453c27333ff5725b",
"junction-t-90-bar70-leg15-dark": "522f80d6c7dce2e9af827ec5edab48f2b53d03b1a15b9375ff5c7dadac21d2e5",
"junction-x-90-equal15-dark": "563e3a18c2e1013efef6242a407a587b4e6b7d924a03741ac93c89409e089a86",
"junction-x-90-mixed-dark": "b66d743938bd0c5f7b6e8610317a371a6445425ffd17f4b22cfa96b5bb307abf",
"junction-x-45-alternating-dark": "f344b5aad08b2b7bc7f8440a23a8a877147efdcc73d641225add831c405f283d",
"junction-star5-equal15-dark": "ee932a56bd3f81991d24f768d6261fdc55fdcb8ad78510dd514deff351ca8f26",
"junction-y-60-equal50-dark": "020e5e88001b50f1c838aaf5ca4d241d0f26575ff24b02a7586ccc20c8e364ff",
"junction-acute30-mixed-dark": "5002574648e5cfb8dd996facafb5f955ce9b15f65a7de318e5dee984299cf529",
"junction-acute15-equal50-dark": "be86394cdac085188307ae1d73090da586adf11f9d16d88ca3e3bebf98e95307",
"junction-splay10-170-dark": "539c59e79995e3fb2a9080eb5d2880cbd95a69e1bf26af8e2afc927dbc22f767",
"junction-t-virtual-arm-dark": "ae2486c7e97331e1f9b856c8ade6565f2e84b6254665cc9cde748ef685932b08",
"junction-x-virtual-through-dark": "9a7d3fcaa52432565f8950a387a94fc87443373a5dc0d38f25bfa4a8b2c39797",
"junction-column-node-dark": "a8e367abbe57c79537f59ec81b92056895b4647ace12838c792e25c91e0a4a61",
"junction-draft-end-node-dark": "69dc2a4e587a82a88ee10a2c820fac51742f3e57e0af7fda85d8eedbd0ff58f6",
"junction-309-step-dark": "b40d4bf33ef31fb94f10816ec554d5fcab03e839c3845c29be3631e6fd231ea4",
"junction-309-spike-dark": "d9ef02af63600e2a705b6d2848b9e8d291368d003a3f38d1b2fabf5dcb441fab",
"junction-309-hump-dark": "c1d77fa39b68ff8e7dab84ff0b7afae1591f23f4fb6ff7bdfe7ccdb7e7fbb26b",
"junction-t-90-equal15-dark": "5930af307f64db4105b0bcfbabb454643f5a21ce7095c3f20c629d09c20cd212",
"junction-t-90-bar50-leg15-dark": "bc87ae653f8d249be5cec9c35d157c4420529d0dfc94e4b2716293c140828d58",
"junction-t-90-bar70-leg15-dark": "f73508baaab86915e4f667477116ac3b1ef68040caa6aaa932ffcd0e007b20e3",
"junction-x-90-equal15-dark": "f01ecc2b28d457fbff0581681dc8c1a1555558baf4c0ac8e2bd4bdd3637f51b4",
"junction-x-90-mixed-dark": "1218a50ad968eae651e61637eeaed8238d0598d77749d3c9cdb7bdd7c0d6a063",
"junction-x-45-alternating-dark": "08de6b9d3a0e6d0164efc014fb1845cdd70cccb95b05b0d55175f2bca89c378b",
"junction-star5-equal15-dark": "08a2410949b60cad38885004c01821935fd04dd1aed15880593069fab6db7e06",
"junction-y-60-equal50-dark": "89994b12fd168b3e35ee47520fafa502f904391f98cec909fccac19b7b0a90e5",
"junction-acute30-mixed-dark": "bc4f61f377711df71950ea6a8bea7d185c94b5e03bfc2e8fc4930fa9adc9ca02",
"junction-acute15-equal50-dark": "3883df35644df7cc169cacb96a5ee8391d64878930c69a0d511cd0cca0f2a04e",
"junction-splay10-170-dark": "4dd7a3947d11b1dcbd1b922058ac18d08bc5b19949925b8e19373d71429d81f3",
"junction-t-virtual-arm-dark": "e0accff2bb3c39354c9aa139921f31fa08d311f57dcc2d6e9bacdbba3b45e671",
"junction-x-virtual-through-dark": "a7ace42d135f24e49a504072fdc2bc360c2a23f7e95e4c47b1d9df0727af32d5",
"junction-column-node-dark": "5930af307f64db4105b0bcfbabb454643f5a21ce7095c3f20c629d09c20cd212",
"junction-draft-end-node-dark": "7652ec1a96a399b4985c256c62369d325894024710b47e0f1271a4a43a82467b",
"junction-309-step-dark": "86c837457d3774c8d16a9ad673156e5d758e8805c321d8d6366a0656febab35e",
"junction-309-spike-dark": "17b80d7b8f976e01d7ce4b366c2ef57f1b6e88fc4c23e915667e444a7fc9b164",
"junction-309-hump-dark": "2402ad1af472be47c2834452047e8f7ca703814990f5c1d59c1462c50905ce91",
"sharp-apex-legacy-dark": "a7d3694789f0c981c33f600b1773fc55c49289b03fdf1d3d4ebcd79c6f4c6f1e",
"junction-owner-repro-dark": "3cb964159ce1d07e76ccef34ed36385f74afb6dd63bc09c28e30483672202463",
"large-house-zoom-040-dark": "201ee34abd9efda441b8b181a3c09b0d9398f3b1afe22904b6d1dccdd17a6be7",
"large-house-zoom-250-dark": "97b1584864216207bf9793ba81b3eb144b126ca3d475865afcd01ab5396ce216",
"junction-owner-repro-dark": "4d8dd271c2ae6ca6dbdd00e1d0917209b693c94f3c48b2d91de752d541c18cec",
"large-house-zoom-040-dark": "84f9e93b5fd25caaf031dc7758553c29459d4dda31e086b7d79e904c9b3d6230",
"large-house-zoom-250-dark": "fa901d097023320a599c1c80be9f9c65a0ce6245bb7182bc0f3d90d679668c17",
"large-house-warm-remount-dark": "6ed51a1c76cd7c62875eadafc169f7c6fc25604e64edabd9594a3664c886987d",
"device-dialog-desktop-en": "f5d660f9244712f016c02c560a4bd301f3729edd4ca1e63b3d2cf0fab9c71802",
"device-dialog-desktop-en": "1a3d6c9c77b263a438eb896f24282af6998ec3e478267423b5c2fbebda75c9b5",
"device-dialog-mobile-ru": "2486302565fb8dadb7758384e592b059df45f6f8b92195738e5a0bf62758c713",
"device-dialog-desktop-de": "972d56b0a0307ad7a061175aa0f6147c3170fc64eadb88bafd038835fd2ce331",
"device-dialog-desktop-de": "082969eec2b7fd472cd65a9ae162430a07bc0e72661a3b5eaae689f490369efa",
"room-temperature-dialog-desktop-en": "cea714dbe38185ea3d936fdb7cb71a0205feb74687d3ade517aed28407abd4a9",
"room-temperature-dialog-mobile-ru": "5ca1c4f18eab3fb4c0667f76b0d0340df94f24e0457baca89234e0ef159f62d6",
"room-discard-dialog-mobile-ru": "9c85af0cdbd2842808ef5ac8fdb306f621a726b0ac77c1449722c85399b4cb3f",
"room-discard-dialog-mobile-ru": "311cc2da1d85191ca26e72928a86f0846b021be85ed5ba83dd02dfe04397c10a",
"german-view-mobile-light": "1c14ef6e97f0f7c616fe61ad72f4dfa7d94c605efc3d8b26931016c8c13a4282",
"toggle-entity-dialog-desktop-en": "075a714a4f013c99614c4941b1202d9ae411e6382f72cb0d0a98e0f669b3388b",
"toggle-entity-dialog-mobile-ru": "c5bf5077a33c1acfa5f744e0ab1c15ce6c2147bc3622a7e8754d972aeedb555b",
"toggle-entity-dialog-desktop-en": "4a8de7f02995b992cb2e8110305280db4899f6fe9bbe967bb43398a5b479a694",
"toggle-entity-dialog-mobile-ru": "e6e11b5a4f0181d0115199d5c3eac93fa2db19c73bf743713a0821ce6d9fc5d9",
"device-help-popover-light-ru": "7645a419f8214dacc6b97be894643d12b2a0486342e568b0b3ee42d74b8cf878",
"decor-color-popover-mobile-ru": "bdf90e0e265d41d032db948a737eeedd6e60cdacc9f85c9356aa809894654960",
"decor-color-popover-desktop-en": "46b38745da10d4467b510c5a257102395a797e0d3229e7ce8c89e4bbf3b937e7",
"general-color-popover-desktop-en": "b54ef50909c2e7b667d321457b17205d5de38dd39b219eef05bee5ef8fb6bef2",
"settings-help-zoom-200-en-light": "9d9cdc0d849afc821efdf4451be40a1d32ecdff06a91b1d517b46c3f45cbcff0",
"settings-help-zoom-200-ru-dark": "cb2a9f210b19057e26d42ca8975c3a888fb95de3f6cbd03515d1eccff39de387",
"settings-help-zoom-200-en-light": "5539d8df2691eee0b8e90fa1fe086380870eeca5d945a34bb71e65f18bb3c22f",
"settings-help-zoom-200-ru-dark": "7be1e0c156ae3a8f71d6b55fc75e5a20e5a4404efee674fa4bebdb82004daa80",
"support-desktop-empty-light-en": "55ffd7a63b8e4d65b8cb695a7b166e0c4bb2e719ce925b8d28b8e8595897750d",
"support-desktop-preview-dark-en": "326c1b1ecbe71e0bdaed6445137ee4145418317bf5d5faf10faa297707fdb924",
"support-phone-validation-light-ru": "acc540f9459c9dc38981ccb70e75a5415f17c849d3840340648448884bd2ab31",
"support-phone-success-dark-en": "b2edd4415043d673d22dfe89ee16b9c190e7f3f9537371f01665e8e41444a00e",
"support-phone-validation-light-ru": "85baca107a2620c3d36c09ca9d4f72e029cf41661970983560efc4066f2e5688",
"support-phone-success-dark-en": "dcf2cb53e904dd3196052f5e0001b8aee712db9c9c0e4f8cd2982645e4bf7dbd",
"support-relay-error-light-en": "adf30f96a926a1730c9ad34d18b4234d9b19140f11d56c8fb7508976f366c11d",
"support-tablet-preview-dark-ru": "a69ba86bf069d9beba94eedf6cc3b385b026caeee66348e4ef42c1a072b6df6a",
"device-ripple-color-popover-mobile-ru": "8fc31025d32d87e5d3d3df896937d182577cd5169d520e7cf5fc267e7d080776",
@@ -212,6 +239,11 @@
"optimize-orphan-references-dark-en": "6a9ab05787bc789af6181e6f2abca19d3cf92ac166aac661ad8deb7c7a3c88bb",
"optimize-orphan-references-light-ru": "332e56edb151a5a820fc307d69ffd4060348978572e48704b0a2c14ba9802720",
"card-editor-invalid-default-floor-light-en": "dbf251ece014cfee252e0dcb2d7c00eda66aa4b5bd125e4c83e248d4319d24b3",
"card-editor-invalid-default-floor-dark-ru": "422b74bb391b87e383350247ef1bc1f10ca48c8f6fe418f75bde94e62e3b8bc0"
"card-editor-invalid-default-floor-dark-ru": "422b74bb391b87e383350247ef1bc1f10ca48c8f6fe418f75bde94e62e3b8bc0",
"led-strip-design-reference-off-light": "907098bee00d95fdc4b2e93d8fb552ceda8ca34f904dccd66f7cbaecd36fd98a",
"led-strip-design-reference-on-light": "16bfb46f2a90c57e95f1307e375e04e5809d41ae677e3b1e1eb56d7e0bc1d8ec",
"lighting-led-strip-glow-dark": "254fe1e39c5bb7bbb7e67527f69c767c8bdb6ed959986a8085b18adb152001d6",
"led-strip-off-light": "588294347c22eecbb9274f97bebdc4826ae97b237f64f291877165b9cbfd464a",
"iso-led-strip-dark": "572728e34f77f911f0ece247c5b7de6653e9c0dfafe04a0ab198b88644081a58"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Some files were not shown because too many files have changed in this diff Show More