Compare commits

...
Author SHA1 Message Date
claude[bot] e7fca7b955 docs(reviews): индекс после сдвига каталога (#699)
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Waiting to run
Проверка (CI) / Классификация изменённых файлов (push) Waiting to run
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Blocked by required conditions
Проверка (CI) / HACS: валидация репозитория (push) Blocked by required conditions
Проверка (CI) / Hassfest: манифест интеграции (push) Blocked by required conditions
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Blocked by required conditions
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Blocked by required conditions
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Blocked by required conditions
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Blocked by required conditions
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Blocked by required conditions
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Blocked by required conditions
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Blocked by required conditions
Проверка (CI) / Бандл головы dev для стенда (push) Blocked by required conditions
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Blocked by required conditions
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Blocked by required conditions
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Blocked by required conditions
Проверка (CI) / Смоки: все шарды зелёные (push) Blocked by required conditions
Проверка (CI) / Golden-кадры против принятых эталонов (push) Blocked by required conditions
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Blocked by required conditions
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Blocked by required conditions
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Blocked by required conditions
Проверка (CI) / Доказательство выполненных проверок (push) Blocked by required conditions
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
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (blend) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (interaction) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (isometric-stage3) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (large-house) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (overlay) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (plan-snap) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-default) (push) Waiting to run
Полные бенчмарки производительности / Бенчмарки рендера и геометрии (space-glow) (push) Waiting to run
Проверка (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: #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
Matysh c047ca2230 Release v1.78.0-beta.8 candidate
Проверка (CI) / Смоки: все шарды зелёные (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) / Смоки в браузере (шард 2 из 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) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Issue: #684
Issue: #685
Issue: #686
Issue: #687
User-Visible: yes
Release: v1.78.0-beta.8
2026-09-28 13:19:11 +03:00
claude[bot] cd59b2eb66 docs(reviews): индекс после сдвига каталога (#685)
Issue: #685
User-Visible: no
2026-09-28 10:07:31 +00:00
claude[bot] 52b1b2e7c9 docs: review document for #685
Issue: #685
User-Visible: no
2026-09-28 10:07:29 +00:00
Matysh 52fe4d6499 fix: вернуть paint-server штриховки в defs (#685)
Полный smoke обнаружил нарушение порядка SVG-слоёв и устаревшее ожидание скрытых устройств после #687. Paint-server снова живёт в defs; общий mode-smoke проверяет видимые ориентиры устройств в Plan.

Issue: #685
User-Visible: no
2026-09-28 12:04:48 +03:00
Matysh 005b7c406d chore(golden): принять ориентиры устройств из dev (#685)
После автоматического ребейза #687 намеренно показывает неинтерактивные устройства-ориентиры в Plan. Из Linux CI приняты ровно две затронутые сцены; ещё 186 кадров совпали.

Release: v1.78.0-beta.8
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36397286486
Issue: #685
User-Visible: no
2026-09-28 11:35:03 +03:00
Matysh 0a5b34a571 chore: освежить производные гейты после ребейза (#685)
Все 11 кадров документации остались пиксельно идентичными; обновлён только fingerprint актуального дерева. Потолок initial View перецентрирован на 100 Б при факте 299713 Б, без изменения общего бюджета.

Release: v1.78.0-beta.8
Issue: #685
User-Visible: no
2026-09-28 11:20:25 +03:00
Matyshandclaude[bot] c37b5b92bc chore(golden): обновить аттестацию после ребейза (#685)
Все 188 golden-сцен и 11 кадров документации совпали пиксель-в-пиксель после переноса на актуальный dev; обновлены только паспорта текущего дерева.

Issue: #685
User-Visible: no
Release: v1.78.0-beta.8
Baseline-Reviewed-Local: sha256:c02067a968b83ee2ec2f110ab06fb3707491a79a98e61448f4b1ee8cc34c1a9a
2026-09-28 07:35:39 +00:00
Matyshandclaude[bot] 139dceaa4f chore(golden): принять резкую штриховку после зума (#685)
WSL-приёмка обновляет 21 существующую масштабную сцену и добавляет четыре проверки 132/140%, DPR 1/2 с проёмами. Восемь кадров документации пересняты в той же доказанной среде; три неизменных кадра подтвердили совместимость.

Issue: #685
User-Visible: no
Release: v1.78.0-beta.8
Baseline-Reviewed-Local: sha256:414e55783bc8272d7b2ff209a94bd69e12de95f6759d5bc95975acb94e937f39
2026-09-28 07:35:39 +00:00
Sergey Matyuninandclaude[bot] 13af1d5e30 fix: стабилизировать штриховку после зума (#685)
Issue: #685
User-Visible: yes
2026-09-28 07:35:39 +00:00
claude[bot] e8e7ecc9f2 docs: review document for #685
Issue: #685
User-Visible: no
2026-09-28 07:35:39 +00:00
claude[bot] 1493145ba5 docs(reviews): индекс после сдвига каталога (#687)
Issue: #687
User-Visible: no
2026-09-28 07:26:29 +00:00
claude[bot] b1bbf4b076 docs: review document for #687
Issue: #687
User-Visible: no
2026-09-28 07:26:25 +00:00
Claude 128af2392a test(plan): Node witness for the landmark cascade; smoke via __hpTest
The three #687 mutants now name a Node suite over the compiled Plan
stylesheet (test/plan-device-landmarks.test.mjs), as the #683 stair
cursor mutant does: the reviewed browser-guard inventory is at its cap
(200/200, #659). The suite pins no hiding in .stage.markup, the
filter: opacity(0.35) fade without an opacity override, the pointer
boundary on the marker, its subtree and ::before (and Background's),
and a fade scoped to the Plan stage. Parity of the 35% with Background
is a computed-style fact and stays with the smoke (#624 forbids text
reads of the monolith). The smoke now enters modes and tools through
window.__hpTest instead of private writes (#629).

Issue: #687
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 09:59:52 +03:00
Claude 560849a1a7 feat(plan): device markers are landmarks in the Plan editor
The Plan editor hid every device marker (display: none), so walls,
openings and stairs were placed against devices from memory. It now
shows them exactly as the Background editor does (#362): the same
markers as View, at the Background editor's effective opacity, fully
pointer-inert. The fade is filter: opacity(0.35) on each marker, not
on .devlayer, because room labels and room settings buttons share that
layer and stay opaque and interactive; filter multiplies the marker's
own opacity (.unavail -> 0.35 x 0.35), as the Background layer does.
Marker handlers already fail closed outside View/Devices.

demo/smoke_plan_device_landmarks.mjs proves AC1/AC2: same markers as
View, per-marker alpha equal to Background, core and capsule fall
through, a real click on a marker reaches the Walls tool and a room
settings button below, cursor and target unchanged, handlers inert, no
tabindex. Three mutants guard hiding, opacity override and the pointer
boundary. UX-MODES, DECOR-EDITOR, USER-GUIDE.ru mode table, changelogs.

Issue: #687
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 09:38:26 +03:00
claude[bot] cab4b4153a docs: review document for #687
Issue: #687
User-Visible: no
2026-09-28 06:36:32 +00:00
claude[bot] a0b471ca1b docs(reviews): индекс после сдвига каталога (#684)
Issue: #684
User-Visible: no
2026-09-28 06:00:49 +00:00
claude[bot] 0ff908690a docs: review document for #684
Issue: #684
User-Visible: no
2026-09-28 06:00:47 +00:00
Claudeandclaude[bot] e79f3cbfbb docs: Esc and the tray's Reset finish an open Walls chain
UX-MODES.md and TOUCH-SUPPORT.md said Reset (and, by omission, Esc) do
not finish an open Walls chain. The code finishes it on both
(houseplan-card.ts Escape handler, the tray's btn.reset), as does
WALL-THICKNESS.md §11 "Finishing a chain". Both documents now list the
same finish actions and point to §11; re-selecting Walls, pan, pinch,
a second pointer and pointer cancellation stay non-finish actions.

Issue: #684
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 05:52:30 +00:00
claude[bot] ffcdb2945f docs(reviews): индекс после сдвига каталога (#686)
Issue: #686
User-Visible: no
2026-09-28 05:52:13 +00:00
claude[bot] 67ac9981cf docs: review document for #686
Issue: #686
User-Visible: no
2026-09-28 05:52:09 +00:00
Claude c455e7c82d fix(stairs): лестница-переход без рамки фокуса (#686)
Решение владельца 2026-09-28: «убрать рамку фокуса полностью». Навигационная
лестница View — фокусируемый `<g role="link" tabindex="0">` (#676), а своих
стилей фокуса у `.hp-stair` не было, поэтому браузер рисовал чёрно-белое
кольцо по прямоугольнику символа. Теперь `.hp-stair:focus` и
`:focus-visible` — `outline: none`: ни кольца браузера, ни замены. Лестница
остаётся в порядке Tab, Enter/Пробел по-прежнему переходят; hover и
выделение в редакторе не тронуты.

Свидетель — `demo/smoke_stairs.mjs`: снимок области лестницы с запасом на
кольцо без фокуса и с клавиатурным фокусом (`:focus-visible`) обязан совпасть
байт в байт; Enter на сфокусированной лестнице переводит на целевой этаж.
Без правила смок красный (`focusedStairPaintsNoFrame`). STAIRS.md и оба
changelog.

Issue: #686
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 08:28:00 +03:00
Sergey Matyunin 03a3dd9916 Release v1.78.0-beta.7 candidate
Issue: #679
Issue: #680
Issue: #681
Issue: #682
Issue: #683
User-Visible: yes
Release: v1.78.0-beta.7
2026-09-28 07:45:07 +03:00
claude[bot] 6d8d077340 docs(reviews): индекс после сдвига каталога (#682)
Проверка (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: #682
User-Visible: no
2026-09-27 22:30:10 +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
236 changed files with 14033 additions and 4245 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
+195 -28
View File
@@ -58,6 +58,8 @@ jobs:
cycle: ${{ steps.decide.outputs.cycle }}
spent: ${{ steps.decide.outputs.spent }}
limit: ${{ steps.decide.outputs.limit }}
# #696: текущие метки — prepare выводит из них трек захода.
labels: ${{ steps.decide.outputs.labels }}
steps:
# Мелкий checkout: guard остаётся лёгким, но ему нужен
# scripts/review-doc-guard.mjs — счёт раундов вынесен туда, потому что
@@ -84,8 +86,15 @@ jobs:
BLOCKED=false; EXHAUSTED=false; SMALL=false; TRIVIAL=false
has blocked && BLOCKED=true
has review-4 && EXHAUSTED=true
# Треки #695 (PROCESS §5): `track:show` и `track:ship` — лимит 2, как у
# прежних `small`/`trivial`; `track:ask` — 4. Цену захода по треку
# (мутанты, ребейз, модель) решает prepare (#696).
has small && SMALL=true
has trivial && TRIVIAL=true
has track:show && SMALL=true
has track:ship && SMALL=true
# Явный `track:ask` главнее прежних меток: метка владельца окончательна.
if has track:ask; then SMALL=false; TRIVIAL=false; fi
# Этап определяется первым: от него зависит, какие вердикты считать.
stage=""; marker=""
@@ -102,7 +111,7 @@ jobs:
stage=""
fi
# Лимит циклов: 4 обычный, 2 на лёгком и коротком треке (PROCESS.md §4).
# Лимит циклов: 4 на `track:ask`, 2 на `track:show`/`track:ship` (PROCESS.md §4, §5).
limit=4
if [ "$SMALL" = "true" ] || [ "$TRIVIAL" = "true" ]; then limit=2; fi
@@ -213,6 +222,26 @@ jobs:
- документы: $new_blocking"
fi
echo "ветка материала: ${branch:-нет, читался dev}"
# §5.1 (r1 #695): инфраструктурная задача (§1) без трековой метки
# идёт как `track:show` — лимит 2. Признак механический, как в §1:
# в диффе ветки против dev ни одного файла класса A. Guard историю
# не клонирует, поэтому пути — из compare API; 300 файлов — потолок
# ответа, и полный список тогда не доказан: остаётся лимит 4.
if [ -n "$branch" ] && ! has track:ship && ! has track:show && ! has track:ask \
&& ! has small && ! has trivial; then
changed=$(mktemp)
if gh api "repos/$REPO/compare/dev...$branch" --jq '.files[].filename' > "$changed" 2>/dev/null \
&& node --input-type=module -e '
import { readFileSync } from "node:fs";
import { classify } from "./scripts/process-gate.mjs";
const files = readFileSync(process.argv[1], "utf8").split("\n").filter(Boolean);
process.exit(files.length > 0 && files.length < 300 && files.every((f) => classify(f) !== "A") ? 0 : 1);
' "$changed"; then
limit=2
echo "инфраструктурная задача без трековой метки — трек show (§5.1), лимит 2"
fi
fi
fi
# Отказ обязан быть виден в issue, а не только в логе прогона.
@@ -274,6 +303,7 @@ jobs:
echo "cycle=$attempt" >> "$GITHUB_OUTPUT"
echo "spent=$spent" >> "$GITHUB_OUTPUT"
echo "limit=$limit" >> "$GITHUB_OUTPUT"
echo "labels=$(printf '%s\n' "$current" | paste -sd, -)" >> "$GITHUB_OUTPUT"
prepare:
name: "Ревью: материал и deterministic gates"
@@ -309,6 +339,10 @@ jobs:
spec_body_doc: ${{ steps.spec_body.outputs.doc }}
spec_body_recorded: ${{ steps.spec_body.outputs.recorded }}
duration_seconds: ${{ steps.duration.outputs.seconds }}
track: ${{ steps.track.outputs.track }}
mutants: ${{ steps.track.outputs.mutants }}
full: ${{ steps.track.outputs.full }}
ship: ${{ steps.track.outputs.ship }}
steps:
- name: Начать измерение стадии
id: clock
@@ -379,6 +413,59 @@ jobs:
echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY"
fi
# Трек решает цену захода (PROCESS.md §5, §10.4, #696): мутанты по диффу —
# только `track:ask` и метка `ci:mutants`; `ship` сливается без модели,
# если правка укладывается в механические рамки. Рамки судят дифф от
# merge-base с dev — работу задачи, а не то, что принёс dev, — поэтому шаг
# стоит до ребейза. Выход за рамки повышает трек до `show` здесь же:
# повысить трек вправе любой, понизить — только владелец (§5). Скрипт
# берётся из dev, как помощник ребейза: show/ship до ревью не ребейзятся,
# и ветка, отставшая от dev, его может ещё не нести.
- name: "Трек задачи и рамки ship (#696)"
id: track
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
NUM: ${{ github.event.issue.number }}
STAGE: ${{ needs.guard.outputs.stage }}
LABELS: ${{ needs.guard.outputs.labels }}
BRANCH: ${{ steps.branch.outputs.name }}
run: |
git fetch -q origin dev
tools="$RUNNER_TEMP/track-tools"
rm -rf "$tools" && mkdir -p "$tools"
git archive origin/dev scripts | tar -x -C "$tools"
head=HEAD
if [ -z "$BRANCH" ]; then head=origin/dev; fi
out=$(node "$tools/scripts/process-track.mjs" resolve --labels="$LABELS" --base=origin/dev --head="$head")
printf '%s\n' "$out"
track=$(printf '%s\n' "$out" | sed -n 's/^track=//p')
mutants=$(printf '%s\n' "$out" | sed -n 's/^mutants=//p')
full=$(printf '%s\n' "$out" | sed -n 's/^full=//p')
ship=false
if [ "$track" = "ship" ] && [ "$STAGE" = "code" ] && [ -n "$BRANCH" ]; then
limits=$(node "$tools/scripts/process-track.mjs" ship-limits --base=origin/dev --head=HEAD)
printf '%s\n' "$limits"
if printf '%s\n' "$limits" | grep -qx 'ship=true'; then
ship=true
else
violations=$(printf '%s\n' "$limits" | sed -n 's/^violations=//p')
track=show
cat > /tmp/ship.md <<EOF
**Трек повышен: \`track:ship\` → \`track:show\`.** Правка выходит за механические рамки ship (PROCESS.md §5): $violations.
Слияние без ревью модели для неё закрыто. Этот заход идёт по треку show: лёгкий Validate и ревью модели «корректность и AC». Понизить трек обратно может только владелец.
[Прогон](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
EOF
gh issue comment "$NUM" --repo "${{ github.repository }}" --body-file /tmp/ship.md
gh issue edit "$NUM" --repo "${{ github.repository }}" \
--add-label track:show --remove-label track:ship
echo "track:ship -> track:show: $violations"
fi
fi
{ echo "track=$track"; echo "mutants=$mutants"; echo "full=${full:-false}"; echo "ship=$ship"; } >> "$GITHUB_OUTPUT"
echo "- трек **$track** · мутанты по диффу: $mutants · полный набор: ${full:-false} · слияние без модели: $ship" >> "$GITHUB_STEP_SUMMARY"
# Ревьюер обязан смотреть тот же код, который уедет в dev (#257). Раньше
# ревью шло по ветке как есть, а слияние делало ребейз — проверенный SHA и
# слитый SHA были разными коммитами. Пока расхождение с dev текстовое,
@@ -400,6 +487,7 @@ jobs:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
BRANCH: ${{ steps.branch.outputs.name }}
NUM: ${{ github.event.issue.number }}
TRACK: ${{ steps.track.outputs.track }}
# rebase, в отличие от commit, не принимает -c user.*: он запускает
# свои процессы и требует личность в окружении, иначе падает с
# «unable to auto-detect email address».
@@ -414,6 +502,21 @@ jobs:
exit 0
fi
behind=$(git rev-list --count "HEAD..origin/dev")
# #696: на show/ship ветка до ревью к dev не приводится, если
# сливается с ним без конфликта. Ребейз — один раз, при слиянии, и
# кандидат проходит Validate до push в dev (merge-candidate.mjs, #492):
# в dev по-прежнему уезжает только проверенный CI SHA. Каждый сдвиг dev
# больше не оплачивается force-push, новым SHA и новым прогоном до
# ревью. На ask ребейз остаётся (#257): ревьюер читает то дерево,
# которое уедет в dev.
if [ "$TRACK" = "show" ] || [ "$TRACK" = "ship" ]; then
if git merge-tree --write-tree origin/dev HEAD >/dev/null 2>&1; then
echo "note=Ветка не приводилась к dev (трек $TRACK, #696): dev впереди на $behind коммит(ов), слияние без конфликта. Материал — ветка как есть; ребейз будет один раз, при слиянии, и кандидат пройдёт Validate до push в dev." >> "$GITHUB_OUTPUT"
echo "трек $TRACK: dev впереди на $behind коммит(ов), слияние чистое — ребейз отложен до слияния"
exit 0
fi
echo "трек $TRACK: слияние с dev не чистое — ветка приводится к dev до ревью"
fi
before=$(git rev-parse "origin/$BRANCH")
echo "dev впереди на $behind коммит(ов) — привожу ветку"
# #643: doc-коммит ветки конфликтует с документами других задач в dev
@@ -600,13 +703,15 @@ jobs:
echo "S7-code-review -> S6-in-progress (ревью не запускалось)"
# Мутанты по диффу бегут только по запросу (#510): до ревью конвейер
# запускает Validate с мутантами на материале и ждёт его. Красный или
# запускает Validate на материале и ждёт его — с мутантами на `ask` и по
# метке `ci:mutants`, лёгкий на `show`/`ship` (#696). Лёгкому годится и
# завершённый push-прогон на том же SHA: dispatch повторил бы его. Красный или
# пропавший прогон возвращает задачу автору без ревью — цикл не
# тратится на код, который CI уже отверг (08.09: #437 дважды ушёл в S6
# после запущенного 15-минутного ревью). Этап spec кода не несёт и
# гейт не проходит; повторное применение вердикта (#499) — тоже: там
# слияние само дожидается Validate на кандидате.
- name: Validate с мутантами на материале
- name: Validate на материале
id: gate
if: steps.rebase.outputs.conflict != 'true'
env:
@@ -615,6 +720,9 @@ jobs:
REUSE: ${{ steps.reuse.outputs.reuse }}
BRANCH: ${{ steps.branch.outputs.name }}
SHA: ${{ steps.material.outputs.sha }}
MUTANTS: ${{ steps.track.outputs.mutants }}
# #697: `ci:full`/`ci:golden` — полный набор на материале.
FULL: ${{ steps.track.outputs.full }}
run: |
if [ "$STAGE" != "code" ] || [ "$REUSE" = "true" ] || [ -z "$BRANCH" ]; then
echo "гейт не применяется: этап $STAGE, reuse=${REUSE:-false}, ветка ${BRANCH:-dev}"
@@ -628,7 +736,8 @@ jobs:
# S7), страховка — process-reconcile. Зелёный или красный завершённый
# прогон гейт и без ожидания возвращает сразу.
set +e
node scripts/validate-gate.mjs --repo="${{ github.repository }}" --ref="$BRANCH" --sha="$SHA" --no-wait
node scripts/validate-gate.mjs --repo="${{ github.repository }}" --ref="$BRANCH" --sha="$SHA" --no-wait \
--mutants="${MUTANTS:-true}" --full="${FULL:-false}"
code=$?
set -e
case "$code" in
@@ -653,6 +762,7 @@ jobs:
SHA: ${{ steps.material.outputs.sha }}
VALIDATE_RUN_ID: ${{ steps.gate.outputs.run_id }}
VALIDATE_URL: ${{ steps.gate.outputs.url }}
MUTANTS: ${{ steps.track.outputs.mutants }}
run: |
dir="$RUNNER_TEMP/review-pending"
mkdir -p "$dir"
@@ -666,7 +776,9 @@ jobs:
(cd "$dir" && sha256sum pending.json > manifest.sha256)
echo "artifact=review-pending-${NUM}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
short=$(git rev-parse --short "$SHA")
echo "Validate с мутантами на \`$short\` идёт — раннер освобождён, раунд продолжится по завершении прогона${VALIDATE_URL:+ ($VALIDATE_URL)}." >> "$GITHUB_STEP_SUMMARY"
kind="Validate с мутантами"
if [ "$MUTANTS" = "false" ]; then kind="Лёгкий Validate"; fi
echo "$kind на \`$short\` идёт — раннер освобождён, раунд продолжится по завершении прогона${VALIDATE_URL:+ ($VALIDATE_URL)}." >> "$GITHUB_STEP_SUMMARY"
- name: Сохранить маркер ожидания
if: steps.gate.outputs.proceed == 'pending'
@@ -687,10 +799,13 @@ jobs:
RESULT: ${{ steps.gate.outputs.result }}
NOTE: ${{ steps.gate.outputs.note }}
URL: ${{ steps.gate.outputs.url }}
MUTANTS: ${{ steps.track.outputs.mutants }}
run: |
short=$(git rev-parse --short "$SHA")
kind="Validate с мутантами"
if [ "$MUTANTS" = "false" ]; then kind="Validate"; fi
cat > /tmp/gate.md <<EOF
**Ревью не запускалось:** Validate с мутантами на материале \`$short\` (ветка \`$BRANCH\`) — **$RESULT**: $NOTE.${URL:+ [Прогон]($URL).} Код никто не читал, вердикта нет, цикл ревью не израсходован.
**Ревью не запускалось:** $kind на материале \`$short\` (ветка \`$BRANCH\`) — **$RESULT**: $NOTE.${URL:+ [Прогон]($URL).} Код никто не читал, вердикта нет, цикл ревью не израсходован.
Гейт стоит до ревью намеренно (#510): красный CI всё равно вернул бы задачу, но уже после потраченного ревью.
@@ -698,14 +813,14 @@ jobs:
1. починить то, что назвал прогон, и запушить ветку **одним** коммитом-заходом;
2. дождаться зелёного дешёвого Validate на пуше;
3. вернуть метку \`S7-code-review\` — конвейер сам запустит Validate с мутантами и ревью.
3. вернуть метку \`S7-code-review\` — конвейер сам запустит $kind и ревью.
[Прогон конвейера](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
EOF
gh issue comment "$NUM" --repo "${{ github.repository }}" --body-file /tmp/gate.md
gh issue edit "$NUM" --repo "${{ github.repository }}" \
--add-label S6-in-progress --remove-label S7-code-review
echo "S7-code-review -> S6-in-progress (Validate с мутантами: $RESULT)"
echo "S7-code-review -> S6-in-progress ($kind: $RESULT)"
# Ревьюер перегонял tsc, юниты и сборку заново в каждом раунде, хотя
# Validate на том же SHA уже зелёный (#343). Это не тщательность: бюджет
@@ -773,7 +888,7 @@ jobs:
- name: Собрать контракт материала между стадиями
id: prepared
if: steps.gate.outputs.proceed == 'true' && steps.reuse.outputs.reuse != 'true'
if: steps.gate.outputs.proceed == 'true' && steps.reuse.outputs.reuse != 'true' && steps.track.outputs.ship != 'true'
env:
NUM: ${{ github.event.issue.number }}
STAGE: ${{ needs.guard.outputs.stage }}
@@ -809,7 +924,7 @@ jobs:
echo "artifact=review-prepared-${NUM}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
- name: Передать подтверждённый материал модели
if: steps.gate.outputs.proceed == 'true' && steps.reuse.outputs.reuse != 'true'
if: steps.gate.outputs.proceed == 'true' && steps.reuse.outputs.reuse != 'true' && steps.track.outputs.ship != 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ steps.prepared.outputs.artifact }}
@@ -852,7 +967,9 @@ jobs:
contents: read
issues: write
needs: [guard, prepare]
if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
# #696: `ship` в рамках сливается без модели — пакетное ревью диапазона
# перед бетой (PROCESS.md §11.7, ship-review.yml).
if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true'
runs-on: ubuntu-24.04
concurrency:
group: process-issue-${{ github.event.issue.number }}
@@ -918,22 +1035,46 @@ jobs:
test "$(git rev-parse HEAD)" = "$MATERIAL_SHA"
test "$(git rev-parse 'HEAD^{tree}')" = "$MATERIAL_TREE"
# #696: окружение ставится по нужде. Ревью ТЗ кода не исполняет — ни
# `npm ci`, ни браузера. Ревью `show` ставит Chromium, только если тело
# issue называет смоук или браузер; `ask` — как прежде, всегда. Тело не
# прочитано — браузер ставится: лишняя минута дешевле непрогнанного смоука.
- name: Что ревьюеру нужно из окружения
id: env_needs
env:
GH_TOKEN: ${{ github.token }}
NUM: ${{ github.event.issue.number }}
STAGE: ${{ needs.guard.outputs.stage }}
TRACK: ${{ needs.prepare.outputs.track }}
run: |
deps=true; browser=true
if [ "$STAGE" = "spec" ]; then deps=false; browser=false; fi
if [ "$STAGE" = "code" ] && [ "$TRACK" = "show" ]; then
if body=$(gh issue view "$NUM" --repo "${{ github.repository }}" --json body --jq .body 2>/dev/null); then
if ! printf '%s' "$body" | grep -qiE 'smoke|смоук|смок|playwright|браузер|demo/'; then browser=false; fi
fi
fi
{ echo "deps=$deps"; echo "browser=$browser"; } >> "$GITHUB_OUTPUT"
echo "- окружение ревьюера: npm ci $deps · Chromium $browser" >> "$GITHUB_STEP_SUMMARY"
# Зависимости ставятся ПОСЛЕ переключения на ветку задачи: lockfile мог
# измениться именно в ней, и установка по копии из dev дала бы не то дерево.
- name: Установить зависимости
if: steps.env_needs.outputs.deps == 'true'
run: npm ci
# Браузер нужен не всякому ревью (см. правило выбора гейтов в промпте),
# но когда нужен — качать его заново дороже, чем держать в кэше.
- name: Кэш браузеров Playwright
id: pw
if: steps.env_needs.outputs.browser == 'true'
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'
if: steps.env_needs.outputs.browser == 'true' && steps.pw.outputs.cache-hit != 'true'
# Без --with-deps: системные библиотеки Chromium предустановлены в
# образе раннера, а apt при промахе кэша съедал минуты из бюджета
# ревью и подолгу перебирал недоступное azure-зеркало (#175). Если
@@ -993,6 +1134,9 @@ jobs:
ничего не вернул на правки и цикла не образует (#227).
Номер захода нужен для имени документа — два документа с
одинаковым номером затёрли бы друг друга.
Трек: ${{ needs.prepare.outputs.track || 'ask' }} (PROCESS.md §5) · мутанты по диффу на материале: ${{ needs.prepare.outputs.mutants == 'false' && 'не запрашивались' || 'прогнаны Validate' }}
${{ needs.prepare.outputs.track == 'show' && needs.guard.outputs.stage == 'code' && 'Трек show: объём и серьёзность находок — docs/process/REVIEWER.md, «Трек show» (#696). Отсутствие мутантов по диффу — не находка.' || '' }}
${{ needs.prepare.outputs.rebase_note }}
@@ -1008,15 +1152,15 @@ jobs:
ограничитель: «features are built, improved and accepted only
if they serve a job listed here». Первый вопрос к задаче —
какую строку Core user jobs она закрывает.
2. docs/process/REVIEWER.md — обязанности ревьюера: позиция,
2. AGENTS.md — карта пакета, правило №1, классы изменений, треки,
трейлеры и ожидание вердикта; сами правила — по его ссылкам.
3. docs/process/REVIEWER.md — обязанности ревьюера: позиция,
ревью ТЗ, код-ревью, объём гейтов, повторный раунд, находки и
вердикт. Раздел PROCESS.md по ссылке открывай, когда пункт
касается твоего решения; при расхождении прав PROCESS.md. Если
файла в материале нет — читай PROCESS.md §2.4, §2.7, §2.10,
§4, §7.2, §8, §12. Задача правит сам конвейер, гейты или
процесс — PROCESS.md целиком, §10 в первую очередь.
3. AGENTS.md — карта пакета, правило №1, классы изменений, треки,
трейлеры и ожидание вердикта; сами правила — по его ссылкам.
4. Тело issue #${{ github.event.issue.number }} и все комментарии.
5. Если меняется видимое поведение — docs/USER-GUIDE.ru.md:
терминология интерфейса берётся оттуда, а не изобретается.
@@ -1078,17 +1222,17 @@ jobs:
Если зелёного прогона на этом SHA нет — прогоняешь сам, они дешёвые,
и в повторном раунде тоже: `npx tsc --noEmit`, `npm test`,
`npm run build` со сверкой трёх копий бандла, плюс
`node scripts/check-docs.mjs`, если diff трогает `src/**`.
Зависимости уже установлены workflow, Chromium тоже — `npm ci`
выполнять не нужно. По диффу и AC: браузерные смоки — названные в AC
`npm run build` со сверкой трёх копий бандла. Свежесть скриншотов
документации — не гейт задачи (#697): её обновляет бот на dev.
${{ needs.guard.outputs.stage == 'spec' && 'Зависимости и Chromium на этапе spec не ставились (#696).' || (needs.prepare.outputs.track == 'show' && 'Зависимости установлены (`npm ci` не нужен); Chromium — только если тело issue называет смоук (#696).' || 'Зависимости уже установлены workflow, Chromium тоже — `npm ci` выполнять не нужно.') }}
По диффу и AC: браузерные смоки — названные в AC
плюс вывод `node scripts/smoke-select.mjs --base <base> --head <head>`,
приложенный к комментарию с решением по каждой строке: прогнал либо
не прогнал и почему. Три вида ответа инструмента разные: «прямое
совпадение», «зарегистрированная связь», «НЕОПРЕДЕЛЁННОСТЬ» — связь
не доказана, и это не разрешение ничего не прогонять; слабые связи —
повод посмотреть, а не обязанность прогонять;
`npm run golden:verify` при видимом изменении;
`npm run golden:verify` при метке `ci:golden`;
`python -m pytest tests_backend -q` при правке
`custom_components/**/*.py`; инварианты модели
`npm run invariants -- --config <экспорт>` при правке геометрии или
@@ -1221,6 +1365,7 @@ jobs:
MODEL_RESULT: ${{ needs.model_review.result }}
PROCEED: ${{ needs.prepare.outputs.proceed }}
REUSE: ${{ needs.prepare.outputs.reuse }}
SHIP: ${{ needs.prepare.outputs.ship }}
run: |
if [ "$PREPARE_RESULT" != "success" ]; then
echo "::error::стадия deterministic prerequisites завершилась: $PREPARE_RESULT"
@@ -1236,7 +1381,8 @@ jobs:
echo "proceed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$REUSE" != "true" ] && [ "$MODEL_RESULT" != "success" ]; then
# #696: `ship` в рамках модель не вызывает — её результата не ждём.
if [ "$REUSE" != "true" ] && [ "$SHIP" != "true" ] && [ "$MODEL_RESULT" != "success" ]; then
echo "::error::стадия model review завершилась: $MODEL_RESULT"
exit 1
fi
@@ -1255,7 +1401,7 @@ jobs:
node-version: 22
- name: Получить результат модели
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: review-result-${{ github.event.issue.number }}-${{ github.run_id }}-${{ github.run_attempt }}
@@ -1263,7 +1409,7 @@ jobs:
- name: Проверить полноту и происхождение результата
id: result
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true'
# #556: единственное, что недоверенная стадия модели может передать
# дальше, — этот artifact, и принимается он как ввод противника: полный
# набор файлов, сходящиеся суммы, совпадение КАЖДОГО поля паспорта с
@@ -1299,7 +1445,7 @@ jobs:
# Ревьюер пишет только в docs/reviews/. Что именно попадёт в коммит,
# решает этот шаг, а не модель: всё остальное откатывается.
- name: Опубликовать документ ревью
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true'
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
BRANCH: ${{ needs.prepare.outputs.branch }}
@@ -1472,7 +1618,7 @@ jobs:
# достижим там из необновлённой локальной ветки. Читателю отчёта от этого
# пользы нет — он достанет только то, что есть на origin.
- name: "Материал раунда воспроизводим (#413)"
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true'
if: steps.ready.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true'
env:
NUM: ${{ github.event.issue.number }}
STAGE: ${{ needs.guard.outputs.stage }}
@@ -1497,10 +1643,28 @@ jobs:
REUSE_DOC: ${{ needs.prepare.outputs.reuse_doc }}
REUSE_ROUND: ${{ needs.prepare.outputs.reuse_round }}
REUSE_TREE: ${{ needs.prepare.outputs.reuse_tree }}
SHIP: ${{ needs.prepare.outputs.ship }}
MATERIAL: ${{ needs.prepare.outputs.material_sha }}
VALIDATE_URL: ${{ needs.prepare.outputs.validate_url }}
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
NUM: ${{ github.event.issue.number }}
run: |
if [ "$REUSE" = "true" ]; then
# Повторно применимый зелёный вердикт (#499) главнее ship: код уже
# прочитан моделью, и пакетному ревью перечитывать его незачем.
if [ "$SHIP" = "true" ] && [ "$REUSE" != "true" ]; then
# #696: `ship` в рамках (PROCESS.md §5) сливается без ревью модели
# после зелёного лёгкого Validate. Комментарий — не вердикт ревью и
# так себя не называет: ревью не было. Машинный маркер находит
# пакетное ревью диапазона перед бетой (ship-review.mjs, §11.7).
verdict=green; high=0
short=$(printf '%s' "$MATERIAL" | cut -c1-8)
cat > /tmp/ship-merge.md <<EOF
**Слияние без ревью модели: трек ship (PROCESS.md §5, #696).** Правка укладывается в механические рамки ship, лёгкий Validate на \`$short\` зелёный${VALIDATE_URL:+ ([прогон]($VALIDATE_URL))}. Код до слияния не читал никто: его прочтёт пакетное ревью диапазона перед бетой (PROCESS.md §11.7), документ \`docs/reviews/SHIP-REVIEW-<тег>.md\`.
<!-- hp:ship-merge material=$MATERIAL -->
EOF
gh issue comment "$NUM" --repo "${{ github.repository }}" --body-file /tmp/ship-merge.md
elif [ "$REUSE" = "true" ]; then
# Модель не вызывалась: вердикт — записанный конвейером зелёный
# прошлого захода, дерево вне docs/reviews с тех пор не менялось (#499).
verdict=green; high=0
@@ -1580,6 +1744,8 @@ jobs:
BRANCH: ${{ needs.prepare.outputs.branch }}
NUM: ${{ github.event.issue.number }}
MATERIAL_SHA: ${{ needs.prepare.outputs.material_sha }}
# #696: show/ship сливаются по лёгкому Validate на кандидате.
MUTANTS: ${{ needs.prepare.outputs.mutants }}
run: |
if [ -z "$BRANCH" ]; then
echo "::error::ветки задачи нет — сливать нечего"
@@ -1587,7 +1753,7 @@ jobs:
exit 0
fi
node scripts/merge-candidate.mjs --branch="$BRANCH" --material="$MATERIAL_SHA" \
--issue="$NUM" --repo="${{ github.repository }}"
--issue="$NUM" --repo="${{ github.repository }}" --mutants="${MUTANTS:-true}"
- name: Переставить метку
if: steps.ready.outputs.proceed == 'true'
@@ -1634,13 +1800,14 @@ jobs:
MODEL_RESULT: ${{ needs.model_review.result }}
PROCEED: ${{ needs.prepare.outputs.proceed }}
REUSE: ${{ needs.prepare.outputs.reuse }}
SHIP: ${{ needs.prepare.outputs.ship }}
run: |
stage="публикация/интеграция"
detail="Модель уже завершила работу; её запечатанный результат сохранён artifact-ом этого run, но не был применён."
if [ "$PREPARE_RESULT" != "success" ]; then
stage="deterministic prerequisites"
detail="Модель не запускалась, цикл ревью не израсходован."
elif [ "$PROCEED" = "true" ] && [ "$REUSE" != "true" ] && [ "$MODEL_RESULT" != "success" ]; then
elif [ "$PROCEED" = "true" ] && [ "$REUSE" != "true" ] && [ "$SHIP" != "true" ] && [ "$MODEL_RESULT" != "success" ]; then
stage="model review"
detail="Полного валидного результата модели нет; метка не менялась."
fi
+214
View File
@@ -0,0 +1,214 @@
name: "Бета: производные артефакты на dev"
run-name: "Derived artifacts ${{ inputs.tag }}"
# #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:
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:
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
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev; then
echo "::error::dev ушёл вперёд за время съёмки — запустить workflow заново: отпечаток судит дерево, а оно уже другое"
exit 1
fi
echo "### Производные артефакты $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Коммит \`$(git rev-parse --short HEAD)\` в dev — проверить перед кандидатом беты." >> "$GITHUB_STEP_SUMMARY"
+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 }}
+334
View File
@@ -0,0 +1,334 @@
name: "Бета: пакетное ревью ship"
run-name: "Ship review ${{ inputs.tag }}"
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
# правка в механических рамках и зелёный лёгкий Validate. Их код читает одна
# сессия модели перед бетой: все ship-задачи диапазона «прошлый тег..кандидат».
# Гейт публикации беты (`ship-review.mjs check`) требует этот документ, если в
# диапазоне есть ship-задачи.
#
# Только `workflow_dispatch`: файл исполняется с ветки прогона (`--ref dev`),
# зеркало в `main` не нужно. Три job, как у release-review.yml (#551, #556):
# детерминированный вход, модель без права записи, детерминированная
# публикация документа в `dev`.
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:
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 }}
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: |
doc=$(node scripts/ship-review.mjs doc --tag="$TAG")
git fetch -q --tags origin
if [ -z "$CANDIDATE" ]; then CANDIDATE=$(git rev-parse origin/dev); fi
git cat-file -e "$CANDIDATE^{commit}"
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
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" \
--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
echo "::notice::ship-задач в диапазоне нет — ревью не нужно"
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
# Недоверенная стадия без права записи (#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. Язык ответа — русский.
Бета: ${{ inputs.tag }} · кандидат ${{ 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"]}'
- 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
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
# индекс и коммит решает этот шаг.
- 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 }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
dir="$RUNNER_TEMP/ship-review-result"
(cd "$dir" && sha256sum -c manifest.sha256)
test "$DOC" = "$(node scripts/ship-review.mjs doc --tag="$TAG")"
high=$(jq -r '.high' "$dir/result.json")
medium=$(jq -r '.medium' "$dir/result.json")
low=$(jq -r '.low' "$dir/result.json")
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,
})));
' "$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
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F - <<MSG
docs: ship review for $TAG
Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7).
Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low.
Issue: #696
User-Visible: no
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
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
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
sleep $((attempt * 10))
done
echo "::error::документ ревью не опубликован в dev за три попытки"
exit 1
+66 -6
View File
@@ -53,12 +53,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 нужны сообщения
# коммитов, трейлеры и ИМЕНА изменённых файлов, а не содержимое старых
@@ -81,17 +82,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). Гейт стоит
@@ -138,6 +146,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), и тогда его коммиты не судит
@@ -218,6 +259,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 +271,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"
@@ -282,7 +340,8 @@ jobs:
# предыдущего пуша штатно, и на #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,6 +349,7 @@ jobs:
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
FULL_INPUT: ${{ inputs.full }}
MUTANTS_INPUT: ${{ inputs.mutants }}
REF_NAME: ${{ github.ref_name }}
run: node scripts/classify-changes.mjs --heavy | tee -a "$GITHUB_OUTPUT"
- id: base
if: github.event_name != 'pull_request'
+18 -9
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` →
@@ -73,13 +75,19 @@ 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.
`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 of the beta range before the tag (`ship-review.yml`, `PROCESS.md`
§11.7). The review pipeline prices each round by track (§10.4): diff mutants
only on `ask` or with `ci:mutants`; 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 +102,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
+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
+297 -136
View File
@@ -3,12 +3,14 @@
> **Статус документа: канон** (редакция 2026-08-13, ролевое уточнение
> 2026-09-13). Решения владельца, на
> которых он стоит: прямые коммиты в `dev` **без PR** · канон статуса — **метки**,
> имена английские · лёгкий трек **включён** · автор и ревьюер — независимые
> имена английские · треки `ship`/`show`/`ask` задаёт метка владельца (#695) · автор и ревьюер — независимые
> агенты/сессии · любой агент может взять любую роль · инфраструктурные задачи
> входят в общий флоу сразу на `S7-code-review`.
>
> **Область действия:** обязателен для владельца и для любого агента. Читается
> сразу после `docs/SCOPE.md` и `AGENTS.md`, до `docs/STATUS.md`. Живёт в
> **Область действия:** обязателен для владельца и для любого агента. Целиком
> его читает тот, кто правит конвейер, гейты или сам процесс, — сразу после
> `docs/SCOPE.md` и `AGENTS.md`, до `docs/STATUS.md`; автор и ревьюер входят через
> ролевые конспекты ниже и открывают раздел канона по ссылке (#634, #701). Живёт в
> репозитории: до августа 2026 канон лежал только в папке владельца, и свежий клон
> его не содержал вовсе.
>
@@ -24,8 +26,9 @@
> метках и больше нигде: Project v2 не используется. При расхождении
> документации с GitHub побеждает GitHub. Этот файл — единственный полный канон
> процесса в репозитории; `AGENTS.md` — его короткое обязательное резюме, а
> внешний `CODEX-RUNBOOK.md` — только маршрутизатор к канону и историческим
> инструкциям. Текущие версии, состояние конкретных issue, runtime pins и списки
> внешние `CODEX-RUNBOOK.md` и `CLAUDE.md` папки владельца — только маршрутизаторы
> к канону и историческим инструкциям: правил в них нет, и в замер цены входа
> (`scripts/entry-cost.mjs`) они не входят — CI их не видит (#701). Текущие версии, состояние конкретных issue, runtime pins и списки
> jobs не копируются в производную прозу: они читаются из своих исполняемых
> источников.
> При расхождении этого документа с `.github/workflows/*.yml` и `scripts/*`
@@ -93,8 +96,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
→ S6-in-progress → S7-code-review ⟲ → S8-merged → закрыт при выпуске беты
служебные: blocked (поверх статуса) rejected (закрыт)
⟲ — возврат на правки, не более 4 циклов (§4), на лёгком и коротком треке 2
короткий трек (`trivial`, §5.1) идёт S2-analysis → S5-ready, минуя S3 и S4
⟲ — возврат на правки, не более 4 циклов (§4), на треке `show` 2
трек `show` (§5) идёт S2-analysis → S5-ready, минуя S3 и S4
трек `ship` (§5) идёт S1-new → S5-ready, минуя S2, S3 и S4
инфраструктурный трек (§1): без S → S7-code-review ⟲ S6-in-progress → S8-merged
```
@@ -125,9 +129,10 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
5. приоритет **P1/P2/P3**;
6. тип: баг / фича / техдолг;
7. затронутые поверхности (модули, диалоги, бэкенд, i18n);
8. трек — **по умолчанию `small`** (§5). Если задача идёт полным треком,
называется критерий §5, который она не проходит: «обычный трек» без
названного критерия обоснованием не является.
8. трек — метка `track:ship`, `track:show` или `track:ask` (§5), **по
умолчанию `track:show`**. Для `track:ask` называется критерий §5, которого
задача не проходит; `track:ship` предлагается, когда правка описывается
одним предложением. Метка владельца главнее предложения аналитика.
- **Оценки и приоритет ставятся метками сразу, согласие не запрашивается.**
Комментарий аналитики — уведомление, а не запрос: **молчание владельца —
согласие**, несогласие он выражает правкой меток или комментарием, и это не
@@ -139,7 +144,8 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
умолчанию и `blocked`. Вопрос, который можно отложить до ТЗ, не задаётся в
аналитике; вопрос, не блокирующий написание ТЗ, не задаётся вовсе — вместо
него в ТЗ пишется блок принятых предположений.
- **Выход:** `S3-spec` — переход выполняет сам аналитик, не дожидаясь ответа.
- **Выход:** на `track:ask` — `S3-spec`, на `track:show` — `S5-ready` после AC в
теле issue (§5); переход выполняет сам аналитик, не дожидаясь ответа.
Либо, при явном конфликте со `SCOPE.md`, — предложение отклонить с причиной:
это единственный случай, когда аналитика останавливается и ждёт владельца.
@@ -159,7 +165,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
- **Ревьюер ≠ автор.** Ревьюер получает issue и ТЗ, без устных пояснений автора.
Его задача — не согласиться, а найти, где ТЗ не выполнимо или не проверяемо.
- **Артефакт:** `docs/reviews/SPEC-REVIEW-<NN>-r<N>.md`, вердикт
зелёный / жёлтый / красный. Лёгкий трек — комментарий в issue.
зелёный / жёлтый / красный. Ревью ТЗ проходит только трек `ask` (§5).
- **High-находки блокируют.** Medium **в скоупе задачи** чинится в текущем
issue: без High это жёлтый вердикт, автор правит ТЗ, фикс проходит повторный
цикл. Medium **вне скоупа** — отдельный issue: чужой скоуп в этой задаче не
@@ -174,7 +180,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
Не работа, а **очередь**: единственный статус, из которого можно трогать код.
Все пункты обязательны:
- ТЗ существует, ревью ТЗ зелёное, ссылки issue ↔ ТЗ на месте;
- ТЗ существует, на `track:ask` ревью ТЗ зелёное, ссылки issue ↔ ТЗ на месте.
На `ship` и `show` ТЗ — в объёме §5, а пункты ниже закрываются одним словом
«нет»: критерии трека их исключают, иначе это `track:ask`;
- **AC1…ACn** — пронумерованные проверяемые критерии приёмки; у каждого указано,
чем он доказывается: `unit` / `backend` / `smoke` / `golden` / «ревью кода»;
- перечислены затронутые файлы и модули;
@@ -196,8 +204,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
«Взял: <роль> · сессия <id> · ветка `issue/<NN>-<slug>`».
- **WIP-лимиты:** не более **1** issue в «В разработке» на исполнителя, не более
**3** одновременно на цикл релиза, не более **2** в «Код-ревью».
- **Трассируемость:** ветка `issue/<NN>-<slug>`; каждый коммит несёт трейлеры
`Issue: #<NN>` и `User-Visible: yes|no`.
- **Трассируемость:** ветка `issue/<NN>-<slug>`; каждый коммит с файлами классов A, B или D несёт трейлеры
`Issue: #<NN>` и `User-Visible: yes|no`; коммит только из документации — без
трейлеров (§3 п.10, #701).
- **Автотесты — часть реализации, а не отдельная фаза.** Каждый AC, помеченный
`unit`/`backend`/`smoke`/`golden`, получает свою проверку здесь же.
«Тестирование вне жизненного цикла» означает отсутствие фазы ручного
@@ -246,7 +255,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
а два теста были записаны в закрытие coverage-ratchet под именами, обещавшими
то, чего они не проверяли (#430).
Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт в
Мутант в реестре `scripts/mutation-registry.mjs` обязателен, когда защита живёт в
продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden): там
ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов
достаточно прогона со снятой защитой, приведённого в документе.
@@ -256,8 +265,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
DOM/CSS paint, измеренной геометрии, trusted pointer/lifecycle или browser
wall-time, а id — попасть в размеченный реестр
`docs/testing-notes/mutation-browser-guards.md`. `mutation-gate --check`
показывает число browser guards против лимита 200, краснеет при росте выше
лимита и предупреждает о любом id без browser-обоснования; ревьюер проверяет
показывает число browser guards против ориентира 200 и предупреждает — не
краснеет (#699) — при росте выше него и о любом id без browser-обоснования;
удалять чужой мутант ради числа не нужно; ревьюер проверяет
не только наличие строки, но и невозможность более дешёвого `node --test`.
Считаются **защитные AC без названного свидетеля**, а не мутанты на
@@ -294,9 +304,10 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
(`scripts/monolith-metrics.mjs`: делегаты, члены порта, `host.`, приватные
члены порта и харнесса, байты `dist/`), база — `scripts/monolith-baseline.json`;
гейт `npm run lint:unused` (в `gate:small` и Validate после сборки) красит
рост любого из них и любой мёртвый код по `noUnusedLocals` вне порта и
харнесса. Снижение фиксируется тем же коммитом
(`node scripts/unused-locals-gate.mjs --update`); рост — только с записью в
рост любого из них сверх полосы (#699: делегаты, члены и приватные порта и
харнесса — 5, `host.` — 25, байты `dist/` — 2 000) и любой мёртвый код по
`noUnusedLocals` вне порта и харнесса. Снижение ветку не красит — базу до
факта опускает бета (§8, «Храповики»); рост сверх полосы — только с записью в
issue задачи и правкой базы в том же коммите.
- **Смок входит в сценарий через публичную поверхность (#629).** DOM с
контрактными хуками, события HA и фикстуры, тестовый фасад `window.__hpTest`
@@ -380,16 +391,26 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
fast-forward (`--commit-if-stale`, коммит класса C) — и публикацией документа ревью ТЗ
прямо в `dev`. В ветке задачи индекс не пересобирается — ни при приведении к
dev, ни при публикации документа код-ревью: иначе две параллельные задачи
конфликтуют на нём по построению. Руками не правится. Конфликт ребейза, в котором **все** пути —
конфликтуют на нём по построению. Сам `INDEX.md` руками не правится никогда. Конфликт ребейза, в котором **все** пути —
`INDEX.md`, отказом не считается (#643): `scripts/rebase-generated.mjs`
пересобирает индекс по каталогу на остановке и продолжает ребейз — так делают
приведение к dev, слияние кандидата и авторский `rebase-on-dev.mjs`; индекс
вместе с любым другим путём — прежний отказ с перечнем файлов. Шаг Validate
вместе с любым другим путём — прежний отказ с перечнем файлов. Ещё два общих
файла не конфликтуют по смыслу (#698): записи ченджлогов в `## Unreleased`
объединяет встроенный драйвер `merge=union` (`.gitattributes`), а на конфликте в
`scripts/monolith-baseline.json` ребейз берёт сторону `dev` — числа
объединённого дерева судит гейт связности монолита (`npm run lint:unused`) на
Validate кандидата. Сегодня пять чисел исходника он судит точно, байты `dist/` —
с полосой 2 000 Б; поэтому сторона `dev` в базе краснеет, если задача сама
меняла эти числа, — это прежний возврат автору, только после Validate, а не до
ревью. Полоса у всех шести чисел — #699. Ни ченджлоги, ни база не входят в
patch-id кандидата слияния: вердикт к работе задачи остаётся в силе. Шаг Validate
«индекс ревью совпадает с каталогом» красит push в `dev`, где `INDEX.md`
расходится с каталогом (на issue-ветках не судится: их переписывает конвейер).
Правка `docs/reviews/`
руками — перенос в `legacy/`, удаление — сопровождается
`node scripts/reviews-index.mjs` в том же коммите. Прежде чем брать
Ручная правка каталога
`docs/reviews/` — перенос документов в `legacy/`, удаление — сопровождается
пересборкой индекса `node scripts/reviews-index.mjs` в том же коммите: индекс
меняет генератор, а не рука. Прежде чем брать
задачу по подсистеме, стоит прочитать её строки в индексе: что находили и чем
закрывали — там, а не в тысяче файлов. Уроки, пережившие свою задачу,
собираются в `docs/LESSONS.md` с датой и ссылкой на источник.
@@ -434,8 +455,10 @@ dev, ни при публикации документа код-ревью: ин
1. **Никаких изменений в код, если нет issue** и он не помечен «Готово к
разработке» или дальше.
2. **Issue не может быть взят в разработку**, пока у него нет ТЗ с зелёным ревью,
пронумерованных AC с указанием доказательства и назначенного исполнителя.
2. **Issue не может быть взят в разработку**, пока у него нет ТЗ в объёме трека
(§5: на `track:ask` — с зелёным ревью ТЗ; на `show` — до трёх AC; на `ship` —
строка «что меняется и чем проверить»), доказательства для каждого AC и
назначенного исполнителя. Инфраструктурная задача ТЗ не пишет (§1).
3. **Issue не может быть взят дважды.** Занятие фиксируется назначением, меткой и
комментарием с именем ветки. У одного исполнителя одновременно не более одного
issue в разработке.
@@ -455,20 +478,24 @@ dev, ни при публикации документа код-ревью: ин
правка. Блокирующая находка отправляет текущий issue в «Заблокировано».
10. **Каждый коммит класса A и B несёт трейлер `Issue: #NN`**, ветка называется
`issue/NN-slug`, а `User-Visible: yes` требует правок в **обоих** changelog в
том же коммите. После `cherry-pick -x` служебная строка `(cherry picked
том же коммите. Документационный коммит — только файлы класса C — трейлеров
не требует (#701, как `skip issue` у CPython); хук `commit-msg`, CI и
`process-gate` судят одинаково. Коммит класса D несёт `Release:` (п.12). После `cherry-pick -x` служебная строка `(cherry picked
from ...)` должна оставаться выше финального блока трейлеров: перед push
проверяем порядок через `git show -s --format=full HEAD`.
11. **Документация — в том же коммите, что поведение.** Отдельным «допишу потом»
коммитом документация не бывает.
12. **Сгенерированное не коммитится само по себе.** Только релизный промоушен или
принятие эталонов с доказательством ревью: URL Linux CI run либо хеш
аттестованного WSL-артефакта.
принятие эталонов с доказательством ревью: трейлер `Release:` и ровно один
источник — URL Linux CI run (`Baseline-Reviewed:`) либо хеш аттестованного
WSL-артефакта (`Baseline-Reviewed-Local:`), §10.1.
13. **Golden-эталоны принимаются только** `npm run golden:accept -- --reviewed` по
полному Linux-артефакту: либо GitHub CI, либо `npm run golden:wsl:capture` в
WSL/ext4 с clean опубликованным SHA и машинно-проверяемым паспортом. Второй
путь убирает только первый ожидаемо красный CI-прогон; полный GitHub Validate
на точном SHA коммита с эталонами остаётся обязательным. Принятие ради
зелёного CI — нарушение процесса.
зелёного CI — нарушение процесса. Где принимаются эталоны — §8: на `dev`
одним коммитом на бету, в задаче — только с меткой `ci:golden` (#697).
14. **Issue закрывается после выпуска беты** с зелёным CI на точном SHA. Не
раньше, не «по факту наличия кода», не исполнителем.
15. **Закрытый issue не переоткрывается.** Новый дефект — новый issue со ссылкой.
@@ -501,8 +528,9 @@ dev, ни при публикации документа код-ревью: ин
- **Заход и цикл — разные величины.** Заход — сколько раз ревью отработало; он
виден в имени документа (`-r1`, `-r2`, …) и нужен, чтобы два документа не
затёрли друг друга. Цикл — единица бюджета §4. Заходов законно бывает больше,
чем циклов, поэтому порог проверки 7 в `scripts/process-gate.mjs` выше лимита
циклов (шесть документов = четыре цикла плюс два ребейза).
чем циклов, поэтому порог проверки №7 в `scripts/process-gate.mjs`
(`REVIEW_DOC_LIMIT` — шесть документов одного вида на issue) выше лимита
циклов: четыре цикла плюс два ребейза.
- Метка `review-4` ставится, когда исчерпан **бюджет циклов**; конвейер снимать
её не вправе — это решение владельца. Если бюджет пересчитан и оказался ниже
лимита, конвейер сообщает пересчёт, но метку не трогает.
@@ -516,90 +544,90 @@ dev, ни при публикации документа код-ревью: ин
- **Граница между «циклом» и «новым багом»:** до закрытия беты находка ревьюера —
возврат на правки; после закрытия — новый issue. Иначе лимит 4 обходится
заведением issue вместо возврата.
- Для лёгкого трека лимит ревью ТЗ — **2** цикла: задача на три часа, которую
переписывают трижды, лёгкой не была.
- На треке `show` лимит код-ревью — **2** цикла, ревью ТЗ на нём нет (§5):
задача на три часа, которую переписывают трижды, лёгкой не была.
---
## 5. Лёгкий трек (метка `small`) — путь по умолчанию
## 5. Треки `ship`, `show`, `ask` — метка владельца
Умолчание изменено решением владельца 2026-08-27, issue #338. Прежде полный трек
был бесплатен, а выбор лёгкого требовал обоснования. Фактическая цена: **2.9
ревью-документа на задачу** в среднем и до шести на одну issue (#329, #316,
#290) — при том что Medium-находки всё равно чинятся в той же задаче, без
отдельного цикла.
Решение владельца 2026-09-28, issue #695. Разбор 85 закрытых задач #600–#691
показал, что прежний лёгкий трек (`small`) стоил почти столько же, сколько
полный: медиана 115 мин и 12 событий против 102 мин и 13. Дешевле был только
короткий (`trivial`) — 41 мин и 5 событий. Трек определялся формальными
критериями, и у владельца не было метки, чтобы задать его самому.
**Порог не изменился.** Критерии ниже те же и по-прежнему обязательны все
одновременно. Изменилась сторона доказательства: теперь обосновывается не выбор
лёгкого трека, а отказ от него — в `S2-analysis` называется критерий, который
задача не проходит. Полный трек остаётся тем, чем был, для геометрии, миграций
конфига и публичных контрактов: там критерии нарушаются сами, и назвать
нарушенный несложно.
**Трек задаёт метка `track:ship`, `track:show` или `track:ask`. Метка владельца
главнее критериев:** критерии ниже — подсказка аналитика, а не приговор.
Инверсия умолчания не отменяет ничего из §5 ниже и ничего из §4: бюджет четырёх
циклов, арбитраж владельца, обязательность ТЗ на полном треке и правило «ревью
до мержа» остаются как были. Меняется только стоимость пути по умолчанию.
- Аналитик предлагает трек в «Оценке» (§7.2) и ставит метку. По умолчанию —
`track:show`.
- Владелец ставит или меняет любую из трёх меток в любой момент; его метка
окончательна.
- Повысить трек (`ship` → `show` → `ask`) вправе любой агент, с причиной в
комментарии. Понизить — только владелец.
- Трек пересматривается, когда владелец снял усложнявший пункт (#688):
аналитик предлагает понижение, решает владелец.
**Критерии — все одновременно; нарушенный называется явно:**
| | `track:ship` | `track:show` | `track:ask` |
|---|---|---|---|
| Для чего | документация, текст, очевидная правка в несколько строк, CSS-мелочь | баг и полировка в рамках описанного поведения; инфраструктура (§1) по умолчанию | геометрия, миграции конфига, публичные контракты, перф и touch, новый UX-контракт |
| Маршрут | `S1-new` → `S5-ready` → `S6` → `S7` → `S8` | `S1-new` → `S2-analysis` → `S5-ready` → `S6` → `S7` → `S8` | полный, §2 |
| ТЗ | строка «что меняется и чем проверить» в теле issue под `## ТЗ` | «Оценка» (§7.2) и до трёх AC в теле issue под `## ТЗ` | полное ТЗ по §7.1 |
| Ревью ТЗ | нет | нет | да, лимит 4 цикла |
| Локальный гейт | `npm run gate:small` | `npm run gate:small` плюс смоуки `smoke-select` и AC | §8 |
| Код-ревью | до слияния нет; пакетное ревью диапазона перед бетой | модель: корректность и AC | документ ревью, как в §2.7 |
| Лимит циклов код-ревью | — | 2 | 4 |
**Рамки `track:ship`** механические: дифф `src/**` не больше 30 строк, без новых
файлов в `src/**`, без ключей i18n, без полей конфига и без Python. Выход за
рамки переводит задачу в `track:show`. Рамки и слияние `ship` без ревью модели
проверяет и исполняет конвейер (§10.4, #696); код `ship` читает пакетное ревью
диапазона перед бетой (§11.7).
**Подсказка аналитику.** `track:show` уместен, когда выполнено всё сразу:
- сложность и риск ≤ 3;
- одна поверхность (один диалог, один модуль, один эндпоинт);
- нет миграции конфига и новых compatibility-полей;
- нет нового UX-контракта — меняется поведение в рамках уже описанного;
- нет влияния на производительность и на touch-контракт.
**Что упрощается:**
- ТЗ короче: проблема · контракт · AC1…ACn с доказательством · откат
(в теле issue, как и на полном треке с 2026-09-10);
- ревью ТЗ — комментарий второго агента, отдельный документ не нужен;
- лимит ревью ТЗ — 2 цикла.
**Что не упрощается:** issue, оценка, статусы, трейлеры коммитов, changelog,
**код-ревью и его документ**, закрытие после беты. Код-ревью не пропускается:
оно проверяет скоуп, риски и качество доказательств, но не заменяет исполнение
тестов. Единственное исключение из повторного ревью — починка упавшего
предрелизного гейта, §11.4.
Если по ходу выясняется, что критерий нарушен (появилась миграция, задело второй
модуль) — метка `small` снимается, issue возвращается в `S3-spec` и получает
полное ТЗ в теле issue по §7.1. Это не провал, это ранняя диагностика.
### 5.1 Короткий трек (метка `trivial`)
Решение владельца 2026-08-13, issue #128. Лёгкий трек делает ТЗ дешёвым; короткий
обходится без него совсем.
**Маршрут:** `S1-new` → `S2-analysis` → `S5-ready` → `S6-in-progress` →
`S7-code-review` → `S8-merged`. Стадии `S3-spec` и `S4-spec-review` пропускаются.
`S2-analysis` остаётся: это комментарий, а не прогон CI, и именно там владелец
решает приоритет и ценность. AC пишет автор в теле issue при переводе в
`S5-ready` — до перехода, иначе ревьюеру нечего будет сверять.
**Критерии, все обязательны:**
- тип `bug`;
- правка ограничена одной поверхностью, нового UX-контракта нет;
- нет миграции конфига, новых ключей i18n, влияния на перф и touch;
- AC выражаются тремя проверяемыми утверждениями или меньше;
- нет влияния на производительность и на touch-контракт;
- **ожидаемое поведение уже зафиксировано** — в `docs/USER-GUIDE.ru.md`, в
каноническом документе подсистемы либо однозначно в самом отчёте. Решать нечего.
Если есть что решать, это `S3-spec`, и никакая экономия этого не отменяет.
Метка ставится в `S2-analysis` вместе с остальными оценками, одним комментарием,
где владелец утверждает и приоритет.
Невыполненный пункт — повод предложить `track:ask` с названным критерием.
`track:ship` — когда правку можно описать одним предложением и она укладывается
в рамки выше.
**Что не упрощается:** issue, оценка, статусы, трейлеры, changelog и **код-ревью**.
Лимит циклов код-ревью — 2, как на лёгком треке.
Если по ходу выясняется, что критерий нарушен, метка снимается и issue уходит в
`S3-spec` за нормальным ТЗ. Как и на лёгком треке, это не провал, а ранняя
**Чем опасны `show` и `ship`.** Они убирают место, где решение проверялось до
написания кода. Признак «решать нечего» держит всю конструкцию, и его нельзя
подтверждать ощущением — только ссылкой на уже зафиксированное поведение. Если по
ходу выясняется, что решать есть что, трек повышается до `ask`: issue уходит в
`S3-spec` и получает полное ТЗ в теле issue по §7.1. Это не провал, а ранняя
диагностика.
**Чем этот трек опасен.** Он убирает единственное место, где решение проверялось
до написания кода. Признак «решать нечего» держит всю конструкцию, и его нельзя
подтверждать ощущением — только ссылкой на уже зафиксированное поведение.
**Что не меняется ни на одном треке:** issue и правило №1 (§1), трейлеры
коммитов, changelog для видимого изменения, зелёный Validate на точном SHA тега
беты (§8), гейты стабильного релиза. Качество держится на `dev` и на кандидате
беты, а не на каждой ветке.
### 5.1 Метки тяжёлых проверок и прежние метки
Тяжёлые проверки заказываются метками на любом треке — ставит владелец или
автор с причиной в комментарии:
| Метка | Что включает |
|---|---|
| `ci:full` | полный Validate на ветке задачи |
| `ci:golden` | golden на ветке и приёмку сдвинутых кадров в самой задаче |
| `ci:mutants` | мутанты по диффу на кандидате ревью и слияния |
Как конвейер читает метки, описывает §10.4: `ci:mutants` (#696), `ci:full` и
`ci:golden` — dispatch Validate с `full=true` на материале ревью (#697).
**Прежние метки.** `trivial` и `small` читаются как `track:show`; продуктовая
задача без трековой метки — как `track:ask`; инфраструктурная задача (§1) без
трековой метки — как `track:show`. Новые задачи получают только `track:*`.
---
@@ -613,7 +641,7 @@ dev, ни при публикации документа код-ревью: ин
| Аналитик | разбор, оценки, поверхности | окончательно ставить приоритет |
| Автор ТЗ | раздел `## ТЗ` в теле issue | ревьюить своё ТЗ |
| Ревьюер ТЗ | `docs/reviews/SPEC-REVIEW-NN-rN.md` | править ТЗ вместо автора |
| Разработчик | код, автотесты, документация, changelog | ревьюить свой код, принимать golden |
| Разработчик | код, автотесты, документация, changelog | ревьюить свой код; принимать golden — кроме сдвига своей задачи с меткой `ci:golden` по §3 п.13 (§8, #697) |
| Ревьюер кода | `docs/reviews/CODE-REVIEW-*-rN.md`, проверка AC | править продуктовый код |
| Релиз-менеджер | пре-релиз, стабильный релиз, закрытие issue | добавлять код в релизный коммит |
| Владелец | приоритет, ценность, скоуп, отклонение, арбитраж, хотфикс | — |
@@ -643,7 +671,7 @@ dev, ни при публикации документа код-ревью: ин
```
issue #NN
↔ ТЗ тело issue, раздел `## ТЗ` (хеш тела — в якорях ревью)
↔ ревью ТЗ docs/reviews/SPEC-REVIEW-NN-rN.md (или комментарий при `small`)
↔ ревью ТЗ docs/reviews/SPEC-REVIEW-NN-rN.md (только `track:ask`)
↔ ветка issue/NN-slug
↔ коммиты трейлеры Issue: #NN · User-Visible: yes|no
↔ ревью кода docs/reviews/CODE-REVIEW-<tag|NN>-rN.md
@@ -695,7 +723,7 @@ issue #NN
Короткие и однообразные, чтобы читались и человеком, и машиной.
- **Аналитика:** `Оценка: ценность N/10 · сложность N/10 · P<1-3> · тип ·
поверхности: … · дубликаты: … · лёгкий трек: да/нет`
поверхности: … · дубликаты: … · трек: ship/show/ask (причина)`
- **Занятие:** `Взял: <роль> · сессия <id> · ветка issue/NN-slug`
- **Хендофф:** `Сделано: … · Файлы: … · Гейты: <команда → результат> ·
НЕ сделано: … · Риски: … · Следующий статус: … · Новые issue: #…`
@@ -718,9 +746,16 @@ issue #NN
## 8. Гейты
**Локальный гейт перед выходом из «В разработке»** — минимальный набор,
покрывающий изменённые поверхности (действующее правило владельца):
покрывающий изменённые поверхности (действующее правило владельца).
Обязательную часть исполняет одна команда, `npm run gate:small`
(`scripts/gate-small.mjs` — единственный источник её состава, #701): сборка с
typecheck, юниты, целостность и бюджет бандла, `no-new-any`, запрет синхронного
чтения layout в render, запрет записи смоков в приватное состояние,
`lint:unused` и вывод `smoke-select`. Команды ниже — то же самое по отдельности
плюс то, что по диффу и AC:
```
npm run gate:small # обязательная часть одной командой
npx tsc --noEmit
npm test
npm run build && node scripts/bundle-policy.mjs --verify HEAD
@@ -729,8 +764,7 @@ npm run build && node scripts/bundle-policy.mjs --verify HEAD
node scripts/smoke-select.mjs --base origin/dev --head HEAD # какие смоки относятся к диффу
node demo/smoke_<целевые>.mjs
node scripts/no-new-any.mjs --base origin/dev --head HEAD # новый код не добавляет any
npm run golden:verify # если менялся визуал
node scripts/check-docs.mjs # если менялся src/**
npm run golden:verify # только с меткой ci:golden (#697)
node scripts/model-invariants.mjs --config <экспорт> # если правилась геометрия или ссылки
python -m pytest tests_backend -q # py3.14 как в CI (npm run toolchain:check), если менялся бэкенд
npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \
@@ -750,24 +784,26 @@ npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \
комментарии, строке или идентификаторе ложных срабатываний не даёт.
**Объём гейтов на код-ревью соразмерен задаче** (issue #127). Всегда:
`typecheck`, `npm test`, `npm run build` с `bundle-policy --verify` (копии сверяются на кандидате, #657), а при
любом diff'е по `src/**` — ещё и `node scripts/check-docs.mjs`. По
`typecheck`, `npm test`, `npm run build` с `bundle-policy --verify` (копии сверяются на кандидате, #657).
Свежесть скриншотов документации — не гейт задачи (#697, ниже). По
необходимости, определяемой diff'ом и AC: браузерные смоки (сколько их —
считает `ls demo/smoke_*.mjs | wc -l`, вшитое число здесь трижды отставало от
дерева; прогон всех уместен только когда задача задевает всё; какие относятся к
диффу, печатает
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`, и его вывод
прикладывается к ревью вместе с решением по каждой строке), `golden:verify` при изменении видимого
результата, `pytest tests_backend` при правках в Python, performance-профили при
прикладывается к ревью вместе с решением по каждой строке), `golden:verify` при метке `ci:golden`, `pytest tests_backend` при правках в Python, performance-профили при
названном в AC влиянии. **Полные наборы — предрелизный гейт, а не гейт ревью.**
Скриншоты снимаются **только** джобой `Docs screenshots` (`workflow_dispatch`) и
принимаются локально: `npm run docs:accept -- --reviewed --from=<распакованный
артефакт>` (#246). Съёмка на своей машине даёт байтово другой PNG при том же
кадре, и набор из «не того» браузера переписывает все десять файлов без единого
Скриншоты снимаются **только** каноническим прогоном в CI — `beta-derived.yml`
(съёмка и приёмка одним коммитом бота на `dev`, #697) или `Docs screenshots`
(`workflow_dispatch`, только артефакт) с приёмкой вручную: `npm run docs:accept --
--reviewed --from=<распакованный артефакт>` (#246). Ручной путь остаётся
релиз-менеджеру, если бот недоступен. Съёмка на своей машине даёт байтово другой PNG при том же
кадре, и набор из «не того» браузера переписывает все кадры (их число — в
`demo/docs/screenshots.mjs`) без единого
содержательного изменения. Приёмка отказывает, если кандидат снят не с этого
дерева, не тем капчуром, не называет свой Chromium или неполон; коммит делает
человек.
дерева, не тем капчуром, не называет свой Chromium или неполон; коммит бота
проверяет релиз-менеджер, коммит ручной приёмки делает человек.
Когда правка `src/**` кадров не меняет — а это большинство правок — CI-цикл не
нужен (#512): `npm run docs:accept -- --identical` снимает кадры локально,
@@ -776,13 +812,48 @@ npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \
закоммиченных PNG, их sha, браузер и упаковщик съёмки остаются прежними. Хотя бы
один отличающийся пиксель — отказ с перечнем кадров и штатный путь через артефакт.
`check-docs` стоит в обязательной части не по важности, а по механике: отпечаток
скриншотов документации считается по всему `src/**`, поэтому **любая** правка
фронтенда делает его устаревшим. Выборка «по diff и AC» здесь не работает — diff
всегда попадает, и решать нечего. Цена пропуска измерена: скриншоты не
пересняли в #230 и #234, и `dev` стоял с красным job `docs`, пока это не нашли
при следующей задаче (#237). Пересъёмка — по двум абзацам выше, коммит
вместе с задачей.
**Производные артефакты — на `dev`, один коммит на бету** (#697, решение
владельца 2026-09-28). Отпечаток скриншотов документации считается по всему
`src/**`, поэтому любая правка фронтенда делает его устаревшим. Пока его
коммитила каждая задача, `docs/images/screenshots.json` правили 70 раз за 14 дней,
и две параллельные задачи конфликтовали на нём гарантированно. Golden
оплачивала следующая задача: сдвиг, влитый одной, всплывал у другой (#687 →
#685, #688 → #689). Теперь:
- **ветки задач не коммитят** `docs/images/**` и `demo/golden/baselines/**`. На
ветке свежесть скриншотов — предупреждение preflight, а golden не идёт;
- **перед кандидатом беты** `beta-derived.yml` одним коммитом бота обновляет на
`dev` отпечаток и кадры (съёмка тем же каноном, приёмка `docs:accept
--reviewed`) и эталоны golden из артефакта полного Validate на `dev`
(`golden:accept --reviewed`, `Release:` и `Baseline-Reviewed:` в коммите).
Изменившийся кадр или сцена принимается, только если назван во входах
workflow; необъявленная разница — отказ с перечнем. Коммит проверяет
релиз-менеджер;
- **задача, которая меняет визуал намеренно**, ставит `ci:golden`: конвейер
прогоняет полный набор на материале ревью, и сдвинутые кадры задача
принимает сама — по §3 п.13;
- **`Release:` на ветке задачи тяжёлый набор не включает** (`classify-changes.mjs`):
тяжёлое запускают `ci:full` и `ci:golden`, трейлер `Release:` на `dev` и ночной
прогон.
Строгая свежесть скриншотов по-прежнему обязательна на кандидате беты
(`publish-prerelease.yml`, `check-docs --screenshots=strict`).
**Храповики — полоса над потолком беты** (#699, решение владельца 2026-09-28).
Строки двух ядер (`test/core-file-budget.test.mjs`), gzip стартового и ленивых
графов (`scripts/bundle-budget.mjs`) и числа связности монолита
(`scripts/monolith-baseline.json`) судятся одним правилом: потолок — факт
последней беты; ветка задачи краснеет, только если вышла выше потолка больше
чем на полосу (ядро — 50 строк, графы и `dist/` — 2 000 Б, числа монолита — по
`METRIC_BANDS`), а снижение её не красит. Прежде храповики были двусторонними с
нулевым запасом: параллельные задачи конфликтовали на общих числах, пересчитывали
их после ребейза и упирались в потолок, потому что перед ними влили чужую (#689
после #691). Вторая сторона храповика живёт на бете: релиз-менеджер на
кандидате выполняет `node scripts/ratchets.mjs tighten` — потолки опускаются
(и поднимаются на принятый линией рост) до факта одним коммитом вместе с
кандидатом; `npm run release:prerelease` печатает рыхлые потолки
предупреждением. Абсолютный бюджет стартового графа
(`INITIAL_VIEW_GZIP_BUDGET`) остаётся стеной.
**Перф-смок в Validate зависит от диффа** (#473). Два glow-профиля
гоняются всегда; при правке `src/iso-*` добавляется `large-house-isometric-v1`,
@@ -850,8 +921,10 @@ Performance зелёные на точном SHA, плюс зелёный E2E н
| `blocked` | Ждём внешнего или владельца, **поверх** статусной метки |
| `rejected` | Отклонено, issue закрыт |
Модификаторы: `small` (лёгкий трек, сложность ≤3), `trivial` (короткий трек,
§5.1), `hotfix`, `process`, `review-4`; приоритет `P1`/`P2`/`P3`; тип `bug`/`feature`/`tech-debt`.
Модификаторы: трек `track:ship`/`track:show`/`track:ask` (§5), тяжёлые проверки
`ci:full`/`ci:golden`/`ci:mutants` (§5.1), `hotfix`, `process`, `review-4`;
приоритет `P1`/`P2`/`P3`; тип `bug`/`feature`/`tech-debt`. Прежние `small` и
`trivial` читаются как `track:show` (§5.1).
Тематические метки (`polish`, `infra`, `tests`, `docs`, `security`, `vacuum`)
ортогональны процессу.
@@ -902,7 +975,8 @@ Performance зелёные на точном SHA, плюс зелёный E2E н
включая свежий контейнер облачного агента, без отдельного шага в инструкции.
- **`commit-msg`** — есть, работает. Отклоняет коммит без терминального
`Issue: #NN`, требует ровно один `User-Visible: yes|no`, а для коммитов,
`Issue: #NN`, требует ровно один `User-Visible: yes|no` (коммит только из
файлов класса C — без трейлеров, §3 п.10, #701), а для коммитов,
трогающих `demo/golden/baselines/**`, — `Release:` плюс ровно один источник:
`Baseline-Reviewed: <URL GitHub run>` либо
`Baseline-Reviewed-Local: sha256:<хеш аттестации>`. Локальный хеш обязан
@@ -955,7 +1029,9 @@ Performance зелёные на точном SHA, плюс зелёный E2E н
`Baseline-Reviewed: <ссылка на прогон CI>` либо
`Baseline-Reviewed-Local: sha256:<хеш аттестации>`;
6. релизный коммит не содержит изменений в `src/` и `custom_components/**/*.py`;
7. документов ревью на один issue не больше четырёх (`-r1`…`-r4`).
7. документов ревью одного вида (`SPEC-REVIEW`, `CODE-REVIEW`) на один issue
не больше шести (`-r1`…`-r6`, `REVIEW_DOC_LIMIT`): четыре цикла плюс два
ребейза (§4).
С токеном GitHub:
@@ -976,6 +1052,11 @@ Validate стартует от этого push и успевает прочит
документ ревью: он ложится в ветку задачи, пока та в `S4-spec-review` или
`S7-code-review`, то есть заведомо вне рабочего множества.
**Инфраструктурный диапазон статуса не требует** (#562, §1): если в диапазоне нет
ни одного файла класса A, задача ещё не вошла в поток — она войдёт в него сразу на
`S7-code-review`, — и отсутствие S-метки не отказ. Признак механический, по
диффу, а не по метке `infra`.
**При продвижении в `main` не перепроверяются коммиты, уже достижимые из
prerelease-тега.** После выпуска беты их issue по §2.8 должны быть закрыты, а
stable fast-forward снова включает эти коммиты в диапазон `old-main..candidate`.
@@ -1062,9 +1143,46 @@ Matysh/houseplan-card/.github/workflows/_<имя>.yml@dev` с `secrets: inherit`
триггеры, входы ручного запуска или потолок прав; тогда он зеркалится в
`main`, и preflight `workflow_sync` в `validate.yml` держит копии равными —
сверяются ровно эти шесть файлов, список держит
`test/default-branch-workflows.test.mjs`. `performance.yml` в список не входит:
`test/default-branch-workflows.test.mjs`. Расхождение красит push в `dev` и
заводит одно issue владельцу (`[workflow-sync]`), а на ветке задачи —
предупреждение в сводке (#700): к её изменению оно отношения не имеет, и чинит
его тот, кто зеркалит в `main`. Так же судятся внешние ссылки документации
(`check-docs --external=warn` на ветках `issue/*`): упавший чужой сайт не
возвращает задачу. `performance.yml` в список не входит:
по расписанию он судит `main` собственным телом из `main`.
**Цена захода зависит от трека** (#696, решение владельца 2026-09-28). Трек
снимает `scripts/process-track.mjs` в стадии подготовки — по текущим меткам и
диффу от merge-base с `dev`, до ребейза. Прежние метки читаются по §5.1:
инфраструктурная задача без трековой метки — `show`, продуктовая — `ask`.
| | `ship` | `show` | `ask` |
|---|---|---|---|
| Validate на материале | лёгкий; годится завершённый push-прогон на этом SHA | лёгкий, как у `ship` | с мутантами по диффу, dispatch |
| Ребейз до ревью | нет, если `git merge-tree` с `dev` чистый | нет, если чистый | да (#257) |
| Ревью модели | нет — пакетное ревью перед бетой (§11.7) | корректность и AC; окружение — по нужде | полное |
| Проверка кандидата слияния | ждёт push-прогон кандидата; dispatch — только если его нет | как у `ship` | dispatch с мутантами |
Метка `ci:mutants` возвращает мутанты по диффу на любом треке. Рамки `ship`
(§5) проверяет тот же шаг; выход за них — комментарий в issue и замена
`track:ship` на `track:show` в этом же заходе. Слияние `ship` оставляет в issue
комментарий с машинным маркером `hp:ship-merge`. Это не вердикт ревью и так себя
не называет; по маркеру пакетное ревью находит задачи диапазона. Повторно
применимый зелёный вердикт (#499) главнее `ship`: код уже прочитан.
В `dev` по-прежнему уезжает только SHA с зелёным Validate. Без ребейза до ревью
кандидат собирается один раз, при слиянии, и лёгкий Validate проходит там.
Ориентир — данные #695: `small` стоил медиану 115 минут и 12 событий, `trivial` —
41 минуту и 5 событий; `show` целится в уровень `trivial`.
**Ревью `show` судит корректность и AC.** Medium — дефект поведения, который
увидит пользователь, или невыполненный AC. Бухгалтерия — нет мутанта или записи
в реестре, нечувствительный тест на побочный вызов, формулировка в документе —
Low и цикла не открывает. Отсутствие мутантов по диффу на `show` не находка:
полный реестр гоняется ночью. Ревью `show` не ставит Chromium, если тело issue
не называет смоук или браузер. Ревью ТЗ не ставит ни `npm ci`, ни браузер: кода
оно не исполняет.
**Ревью не начинается на красном коде** (#510). После фиксации материала конвейер
запускает Validate с мутантами по диффу на этом SHA (`scripts/validate-gate.mjs`:
`workflow_dispatch validate.yml -f mutants=true`). **Ждёт его не раннер, а событие**
@@ -1138,7 +1256,8 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
никто не может выйти и о котором никто не узнает, для конвейера хуже громкой
ошибки.
**Ветка приводится к `dev` до ревью, а не после** (#257). Раньше ревью читало ветку
**Ветка приводится к `dev` до ревью, а не после** (#257) — на треке `ask`. `show`
и `ship` с чистым слиянием ребейзятся один раз, при слиянии (#696, выше). Раньше ревью читало ветку
как есть, а слияние делало ребейз — проверенный SHA и слитый SHA были разными
коммитами. Пока расхождение с `dev` текстовое, ребейз упирается в конфликт и это
видно; смысловое расхождение git склеивает молча, и в `dev` уезжает комбинация,
@@ -1173,10 +1292,16 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
к другому диффу не применим, §2.10), публикация кандидата в ветку задачи, запуск
Validate с мутантами на ней (#510) и ожидание зелёного dispatch-прогона **на этом
SHA** — push-прогон мутантов не несёт — и только затем push в `dev` с lease на ту
вершину, поверх которой кандидат собран. Отклонённый lease — `dev` двинулся снова
вершину, поверх которой кандидат собран. На `show`/`ship` мутантов нет, и
лёгкий Validate кандидата уже запустил сам push в ветку: слияние ждёт этот
push-прогон, а dispatch шлёт, только если его нет за три минуты (#696). Отклонённый lease — `dev` двинулся снова
— новая попытка; после третьей — `S6-in-progress` с комментарием;
- красный Validate на кандидате или прогон, не появившийся за три минуты, —
`S6-in-progress` с ссылкой; `S8-merged` ставится только после push.
`S6-in-progress` с ссылкой; `S8-merged` ставится только после push;
- после push ветка задачи удаляется с lease на влитую вершину (#702): коммит,
прилетевший после слияния, её сохраняет, и комментарий об этом говорит.
Влитая ветка никому не нужна, а агент, ищущий ветку по номеру, иначе может
взять устаревшую.
Проверка кандидата — обычный Validate ветки: лёгкий набор плюс диффозависимые
гейты. Тяжёлые гейты остаются за кандидатом релиза (#479): слияние не превращает
@@ -1230,9 +1355,10 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
## 11. Исключения
### 11.1 Лёгкий трек
### 11.1 Треки
См. §5 — это не исключение из правила №1, а более дешёвый путь по тем же статусам.
См. §5 — треки `ship` и `show` не исключения из правила №1, а более дешёвые пути
по тем же статусам.
### 11.2 Аварийный хотфикс (метка `hotfix`, решение владельца)
@@ -1275,7 +1401,10 @@ Golden, браузерные смоки, performance и полный HA-харн
- эталоны golden принимаются только через `npm run golden:accept -- --reviewed`
на полном Linux-артефакте GitHub CI либо полном аттестованном WSL-артефакте;
после локальной приёмки полный GitHub Validate на точном финальном SHA всё
равно обязателен. «Чтобы гейт позеленел» основанием не является.
равно обязателен. «Чтобы гейт позеленел» основанием не является. Сдвиг, который
влили задачи линии, принимается на `dev` одним коммитом на бету —
`beta-derived.yml` с объявленными сценами (§8, #697), а не задачей, которая
наткнулась на него следующей.
**Границы, за которыми исключение не действует.** Оно про починку названного
гейтом дефекта, а не про продолжение разработки под видом починки. Правка идёт
@@ -1295,8 +1424,10 @@ Golden, браузерные смоки, performance и полный HA-харн
запись в issue публична и релиз-менеджер видит, что именно было сделано перед
выпуском.
Это исключение из правила «код-ревью не пропускается никогда» (§5, §7.1) —
единственное, и относится только к окну между `S8-merged` и выпуском.
Это исключение из правила «каждое изменение проходит код-ревью» (§5, §7.1).
Второе такое место — трек `ship` (§5): его ревью не отменяется, а переносится
на пакетное ревью диапазона перед бетой. Здесь же исключение относится только к
окну между `S8-merged` и выпуском.
### 11.5 Независимое ревью линии перед стабильным релизом
@@ -1360,6 +1491,36 @@ Golden, браузерные смоки, performance и полный HA-харн
выпуск после такого отказа, исправляют причину и получают новый совместимый
полный зелёный прогон на том же финальном SHA либо на новом SHA кандидата.
### 11.7 Пакетное ревью `ship` перед бетой
Решение владельца 2026-09-28, issue #696.
**Зачем.** `track:ship` сливается без ревью модели (§5, §10.4): правка в
механических рамках и зелёный лёгкий Validate. Прочитать её код обязан кто-то
до того, как она уйдёт пользователям беты. Одна сессия на все ship-задачи
диапазона дешевле ревью на каждую, а рамки ship держат объём малым.
**Шаг.** Перед публикацией беты — `ship-review.yml`
(`gh workflow run ship-review.yml --ref dev -f tag=vX.Y.Z-beta.N`):
- **вход** — issue из трейлеров `Issue: #NN` в диапазоне «прошлый тег..кандидат»
(тот же построитель, что `RELEASE-MEMBERSHIP.json`, #547), из них — ship: с
маркером `hp:ship-merge` в комментариях или с меткой `track:ship`. Собирает
`scripts/ship-review.mjs prepare`; ship-задач нет — модель не запускается;
- **суждение** — по строке ТЗ каждой задачи и её коммитам: делает ли код
заявленное и только его, не ломает ли соседнее, не вышла ли правка из ship по
смыслу. Правила ревьюера — `docs/process/REVIEWER.md`, «Пакетное ревью ship»;
- **выход** — `docs/reviews/SHIP-REVIEW-<тег>.md` в `dev` с машинным блоком:
задачи и счёт High/Medium/Low. У модели нет прав записи, документ публикует
детерминированный шаг.
**Гейт беты.** `ship-review.mjs check` стоит в обоих путях публикации —
`publish-prerelease.yml` и `npm run release:prerelease` (включая `--check`).
Если в диапазоне есть ship-задачи, документ обязан лежать в кандидате или в
`dev`, покрывать их все и не нести High. Задача, слитая после ревью, требует
пересъёмки (`-f force=true`). High чинится отдельной задачей, затем ревью
переснимается. Medium и Low решает владелец, как в §11.5.
---
## 12. Запрещено
+1 -1
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.6"
VERSION = "1.78.0"
DECOR_ASSETS_API_VERSION = 1
SUMMARY_PANEL_API_VERSION = 1
@@ -1,436 +1,436 @@
{
"schema": 1,
"fingerprint": "51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce",
"fingerprint": "a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 299153,
"initialViewGzipBytes": 300323,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 300675,
"initialPanelGzipBytes": 301845,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2324,
"initialPanelOnlyGzipBytes": 2325,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/editor-CbeYCXUv.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/fr-CpDKyK4Z.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"houseplan-assets/iso-scene-render-BxXtKC5L.js",
"houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/pdf-export-DL7VmwpB.js",
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js",
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
],
"lazyGzipBytes": 456031,
"lazyGzipBytes": 456389,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/editor-CbeYCXUv.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
],
"lazyEditorGzipBytes": 237970,
"lazyEditorGzipBytes": 238192,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"lazyOnboardingGzipBytes": 27944,
"lazyOnboardingGzipBytes": 27950,
"lazyLocaleFiles": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/fr-BLCP2_-N.js"
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/fr-CpDKyK4Z.js"
],
"lazyLocaleGzipBytes": 56294,
"lazyLocaleGzipBytes": 56330,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-SCzI9z9k.js"
"houseplan-assets/iso-scene-render-BxXtKC5L.js"
],
"lazyIsometricGzipBytes": 17882,
"lazyIsometricGzipBytes": 17885,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js"
],
"lazyFurnitureArtGzipBytes": 16941,
"lazyFurnitureArtGzipBytes": 16940,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/pdf-export-CGU5_sl_.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/pdf-export-DL7VmwpB.js"
],
"lazyPdfGzipBytes": 126914,
"lazyPdfGzipBytes": 126938,
"lazyNamespaceLocaleFiles": [
"houseplan-assets/settings-de-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js",
"houseplan-assets/support-de-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.js"
"houseplan-assets/settings-de-DfJ7EKF9.js",
"houseplan-assets/settings-fr-Bl79E-6s.js",
"houseplan-assets/settings-ru-CkNFmChi.js",
"houseplan-assets/support-de-DZoi-fxk.js",
"houseplan-assets/support-fr-Daosy89Q.js",
"houseplan-assets/support-ru-e8-BrqtH.js",
"houseplan-assets/topology-de-Cs36ouVE.js",
"houseplan-assets/topology-fr-B8yoJGvc.js",
"houseplan-assets/topology-ru-BRKShmrQ.js"
],
"lazyNamespaceLocaleGzipBytes": 21037,
"lazyNamespaceLocaleGzipBytes": 21057,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CmogasSW.js",
"sha256": "aea6305d5506f017f2b0f8239cce47b10f2e02136b53b3002e2f708deab95444",
"path": "houseplan-assets/backdrop-pick-WDIF6iao.js",
"sha256": "5f6dcedf274594075d8acd31295d8c23f6d1408c890c086825db6bcbaa6c9b21",
"rawBytes": 54017,
"gzipBytes": 17574,
"gzipBytes": 17576,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": [
"houseplan-assets/settings-de-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js"
"houseplan-assets/settings-de-DfJ7EKF9.js",
"houseplan-assets/settings-fr-Bl79E-6s.js",
"houseplan-assets/settings-ru-CkNFmChi.js"
]
},
{
"path": "houseplan-assets/de-D7PZcyoc.js",
"sha256": "5e425bd4558bffbf4ad81d3a51e3ecd9fe7f76697482131b00ddeae65c0b37a4",
"rawBytes": 94126,
"gzipBytes": 28375,
"path": "houseplan-assets/de-E17v3gXt.js",
"sha256": "ed10d6c5e7a82b275c9422be67c2213219a75dba9aab0ddb3e98813249cbd1d0",
"rawBytes": 94196,
"gzipBytes": 28393,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CKG7G85d.js",
"sha256": "db6d125e73e1d2c8535645c576f7df912badd35bb93477d2a38537203c0dbba5",
"path": "houseplan-assets/editor-CbeYCXUv.js",
"sha256": "75887638c4d76040651a3cf52c56de10dc19e0cbc7583ee169d9b20cc5987788",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"sha256": "4252673f16e0382d3dd9ec967c8df782b2a315bb0658f05ad246f70e77aad222",
"path": "houseplan-assets/form-kit.styles-v5DlO12L.js",
"sha256": "cdab9e5e612be9b98c49193a2a6287080c13f62aaefa9a28b69bac94dd934012",
"rawBytes": 25600,
"gzipBytes": 5055,
"gzipBytes": 5056,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-BLCP2_-N.js",
"sha256": "b6f1cd483e2f0bb3c30ce2089b5e52fb65578e656496b2b9623b877b32339fa6",
"rawBytes": 96530,
"gzipBytes": 27919,
"path": "houseplan-assets/fr-CpDKyK4Z.js",
"sha256": "ee9fe5934eea69e549ff48d2e3fdabd7c20e0d9906a6f4623314f6eea48bf269",
"rawBytes": 96627,
"gzipBytes": 27937,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"sha256": "1152cf5d82e01ae897ef187661978a5d0c3a7bf49f8655c96ddaad564c191c29",
"path": "houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"sha256": "f668d3c0a47a53e096f26728187b1666fbb6b421c1baa2ff1a649fa1ba5340d5",
"rawBytes": 52035,
"gzipBytes": 16941,
"gzipBytes": 16940,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BVZ7SgFj.js",
"sha256": "de63fea2ef07a50481acafb3236f8f02962895cc630b026910f76b2be4c431fd",
"rawBytes": 1055543,
"gzipBytes": 298351,
"path": "houseplan-assets/houseplan-card-BYwTiVnu.js",
"sha256": "055b6f8ba3a472be163a15ef6e2348727b5b03e3660b9362f3aed21d7be90426",
"rawBytes": 1059790,
"gzipBytes": 299520,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js"
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/editor-CbeYCXUv.js",
"houseplan-assets/fr-CpDKyK4Z.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"houseplan-assets/iso-scene-render-BxXtKC5L.js",
"houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"houseplan-assets/pdf-export-DL7VmwpB.js",
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"sha256": "3fb964d9241252e3b00bedcc0c89f994aec183b3f657b6af38496b1f803db253",
"rawBytes": 687631,
"gzipBytes": 192112,
"path": "houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"sha256": "f25cbcb0453279e3ac64eda67df52af100d96c54bb93cb657bc08c355e0484ed",
"rawBytes": 688848,
"gzipBytes": 192327,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
],
"dynamicImports": [
"houseplan-assets/support-de-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js"
"houseplan-assets/support-de-DZoi-fxk.js",
"houseplan-assets/support-fr-Daosy89Q.js",
"houseplan-assets/support-ru-e8-BrqtH.js",
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"sha256": "2fcbe1523d5e570804a65c9138d7d2b41c951320af19a36844f9536b32218564",
"path": "houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"sha256": "4ea3584c8b2bfc86f96c587549ddce49eaf0b918923df2df8e98c9c0a548f835",
"rawBytes": 15886,
"gzipBytes": 4906,
"gzipBytes": 4908,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"sha256": "bd59aa84c7afc96a1f50abaf8205bd745798264f27114a40cd7b334aabe1fd14",
"path": "houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"sha256": "2a0f9af108b8a39889f90d7119d8e7789716c9960ea8e28af38ada07cdc91a8c",
"rawBytes": 10958,
"gzipBytes": 3767,
"gzipBytes": 3765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js",
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-SCzI9z9k.js",
"sha256": "cf356172a58c90e29476142476c4d625b49d13c7414fbce151327b8b3626d40f",
"path": "houseplan-assets/iso-scene-render-BxXtKC5L.js",
"sha256": "4150eb9120fdaafa745761242ff3285056ceaccd7b016a16364574b90e3e2cfe",
"rawBytes": 52970,
"gzipBytes": 17882,
"gzipBytes": 17885,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"sha256": "a395a4dcb26e33285521d157e16bacff01f26da5f46158b30e6e25c48679644e",
"rawBytes": 8216,
"gzipBytes": 3208,
"path": "houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"sha256": "66e70baa00f4c9470ac9a37a229652cf5c5585caa2364de4d467a1c2ebe24358",
"rawBytes": 8370,
"gzipBytes": 3280,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/namespace-language-CIeeXPH2.js",
"sha256": "4bbd058806d1870d8d6b109f1a7c1a29e86b0ee2188a46441d38ba6719795964",
"path": "houseplan-assets/namespace-language-zIuNvgGV.js",
"sha256": "244bce2d5211106846215be6a9d3dacda4c910d06020144f9ffd5e432c5e8c00",
"rawBytes": 675,
"gzipBytes": 409,
"gzipBytes": 410,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CGU5_sl_.js",
"sha256": "0b1e4968ecf729dc267e03d63062bba53490e737635d9f3f1681a0320768e6d4",
"rawBytes": 236608,
"gzipBytes": 109973,
"path": "houseplan-assets/pdf-export-DL7VmwpB.js",
"sha256": "548dced1a8a54f860dbe57e61019d50f058d3ca13abb738d750a3c107c88de8b",
"rawBytes": 236714,
"gzipBytes": 109998,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-de-Crekk5Hk.js",
"sha256": "a769daa4cf5674115e7ed3172992b247e56f4ce72f883af46921dbb52f8512ad",
"path": "houseplan-assets/settings-de-DfJ7EKF9.js",
"sha256": "0580b9f6191edb664c21dee21e5263489eaf22876bcad67ef12e440abc03e28a",
"rawBytes": 7378,
"gzipBytes": 2879,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-Bl79E-6s.js",
"sha256": "f1bde9db7fe884aabd25130f783a84d8fe27bc9553923c17bcb9b82ee3976475",
"rawBytes": 7620,
"gzipBytes": 2842,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-CkNFmChi.js",
"sha256": "94f96c4a15a3a82ca8be3066629734379dd14eb2ff9e8f6acf369da2378676cb",
"rawBytes": 10304,
"gzipBytes": 3325,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-C_vpFucN.js",
"sha256": "d45dbdfa17a1da9f45bfbca6d6c3cc98849fd5ebcc06c8f72fd6310b04b93aad",
"rawBytes": 14862,
"gzipBytes": 3425,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-D47J-OjO.js",
"sha256": "909d2195fcfca30c2f1279e07e2d7506d470c3daec6fe864c97f7ecc4101e33d",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
"sha256": "e44860a1af9ec7ffc7ac289c8c76ed1d0dcf0177bd91bbfe1981fdd0d4a62c46",
"rawBytes": 72959,
"gzipBytes": 20242,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-C_vpFucN.js",
"houseplan-assets/summary-panel-metrics-D47J-OjO.js"
]
},
{
"path": "houseplan-assets/support-de-DZoi-fxk.js",
"sha256": "f01d2d00a169519f5f402e99e9c85c9f21a23aeee2a9605d4444910f3250d6de",
"rawBytes": 6167,
"gzipBytes": 2396,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-Daosy89Q.js",
"sha256": "e503a1b4e1b1ed31d8fa09bfe0cdc069a82c1d1627d40407472b336fbc98ccf5",
"rawBytes": 6333,
"gzipBytes": 2432,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-e8-BrqtH.js",
"sha256": "76742313c4b510163c380f8fd9c7fcaf1cc7f8bde6a41bcb0da48621a7c3a3b3",
"rawBytes": 8376,
"gzipBytes": 2876,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-BzCuGnet.js",
"sha256": "40a596406b0bafbc3f2d9a54b12b9ac9e1713f939f98a630c0a576b53780d255",
"rawBytes": 7620,
"gzipBytes": 2839,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-BxY9uxyq.js",
"sha256": "929c462c195a7cf42c8cb2d4b558b4ae5ab1508eeafe4fb6a9e890f3650464ff",
"rawBytes": 10304,
"gzipBytes": 3323,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"sha256": "388fb3fcd97490eee5cdb7140953f7b421fcd06bee24c7f19e0e79d24b777bde",
"rawBytes": 14862,
"gzipBytes": 3426,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js",
"sha256": "6c370c9858be82bdc1e77f77d598147be7673ac64656f533a62c0b15f6cfa5a9",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"sha256": "45c6b69dea88a448e81976da8cad5656bc7ee26ee49b3f9685d8bcba62d845a3",
"rawBytes": 72959,
"gzipBytes": 20243,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js"
]
},
{
"path": "houseplan-assets/support-de-9JIBMnMv.js",
"sha256": "83f703aba89eccb9e1b8ebf5e5b78d60521406a6a1ae7e4cc3a852a68f74b1c9",
"rawBytes": 6167,
"gzipBytes": 2395,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-B7V5UpZx.js",
"sha256": "3515c3761d2870728cac7760d871ccd03d9468d5001e43670b14a5b64126f6d3",
"rawBytes": 6333,
"gzipBytes": 2429,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-DVgRGpdZ.js",
"sha256": "794b61c69025bbe3cee78266b8833a5c22094930bfbd419fdea43fc38ca2f8cf",
"rawBytes": 8376,
"gzipBytes": 2875,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-de-Eo3zRC2b.js",
"sha256": "b73933a557179215bdae75cc51b2713541a032ed6c0240b51599f4ab73bf0ebb",
"path": "houseplan-assets/topology-de-Cs36ouVE.js",
"sha256": "7c808f1139bb1c232627bfe884a579177ec653d16147d6b96066851893b312a4",
"rawBytes": 2698,
"gzipBytes": 1333,
"gzipBytes": 1337,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-fr-BZMZ_i-b.js",
"sha256": "341977cab7561948330323c3da99cde1c56f9a225f9f78c154ce37b6af9f6a0c",
"path": "houseplan-assets/topology-fr-B8yoJGvc.js",
"sha256": "6ee1530fd0bd7a8c476c79b83404c7d8a5b98cd0b86ee8a73048be75a70cc2c4",
"rawBytes": 2718,
"gzipBytes": 1353,
"gzipBytes": 1354,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-ru-MwP3wic3.js",
"sha256": "f49389bc3c849476d3b97d38493262f8eb7a10aa6432dd6d555578f486b21f4a",
"path": "houseplan-assets/topology-ru-BRKShmrQ.js",
"sha256": "7fc89542fa860318d459ce1467a5a8931f3214a9d17f57c177ba01f2cfbbd3e8",
"rawBytes": 3669,
"gzipBytes": 1614,
"gzipBytes": 1616,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-CjcVZzSn.js",
"sha256": "b0136228d2845e59ef35b87fff2e19d28609fcef222ad2f46d6c71f186a9ece2",
"path": "houseplan-assets/zigbee-topology-DOAnbvlu.js",
"sha256": "313f71677ba0739d9c978185c2136e73e8037b770eb3f9ef8561f692d654406f",
"rawBytes": 10958,
"gzipBytes": 4296,
"gzipBytes": 4299,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": [
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.js"
"houseplan-assets/topology-de-Cs36ouVE.js",
"houseplan-assets/topology-fr-B8yoJGvc.js",
"houseplan-assets/topology-ru-BRKShmrQ.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js",
"sha256": "25dd84923fd69b9a594b2b74a191f8e6dba398a757f8a05f3ea7c527bcf27399",
"path": "houseplan-assets/zigbee-topology-runtime-CKlry6R8.js",
"sha256": "39e4a250de0e67e80d8afc02c39c1a79e42d4b3e113107bb3817f4fa90cab1e2",
"rawBytes": 3983,
"gzipBytes": 1788,
"gzipBytes": 1789,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "7348cd78a4e1e27962b597d63f8d877291176a296357a03568557dda09df0903",
"sha256": "d4b480db1327152db7b1459c45692ea6b72fb7ac3bccb525bc06adfe1aab3b8d",
"rawBytes": 1205,
"gzipBytes": 802,
"gzipBytes": 803,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "54a23573ba2306bb0734b8b0ab051555d3646f68a2d8ad90798d0e2c69cc0abd",
"sha256": "5aaa677a9c85f4df0f176be31a91ebb5022f5ce129774919269bb0409c310233",
"rawBytes": 6165,
"gzipBytes": 2324,
"gzipBytes": 2325,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.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,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{aD as e,l as o,aG as t,ev as s,A as a,ew as i,ex as l,ey as n,E as r,c}from"./houseplan-card-BVZ7SgFj.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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{aD as e,l as o,aG as t,ez as s,A as a,eA as i,eB as l,eC as n,E as r,c}from"./houseplan-card-BYwTiVnu.js";class f 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,f={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")},h=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}
.computeLabel=${e=>h[e.name]||e.name}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${d(h.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{eA as e,eB as a,ex as n}from"./houseplan-card-BVZ7SgFj.js";function c(n,c){return new e([{code:"en",dictionary:n},{code:"ru",loadDictionary:c.ru},{code:"de",loadDictionary:c.de},{code:"fr",loadDictionary:c.fr}],"51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce",console.warn,a)}function d(e){return function(e,a){return{state:n=>{const c=e.state(n);return"pending"!==c&&a.some(e=>"pending"===e.state(n))?"pending":c},dictionary:a=>e.dictionary(a),ensure:n=>Promise.all([e,...a].map(e=>e.ensure(n))).then(()=>{})}}(n,e)}export{c as n,d as s};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{eE as e,eF as n,eB as a}from"./houseplan-card-BYwTiVnu.js";function c(a,c){return new e([{code:"en",dictionary:a},{code:"ru",loadDictionary:c.ru},{code:"de",loadDictionary:c.de},{code:"fr",loadDictionary:c.fr}],"a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",console.warn,n)}function o(e){return function(e,n){return{state:a=>{const c=e.state(a);return"pending"!==c&&n.some(e=>"pending"===e.state(a))?"pending":c},dictionary:n=>e.dictionary(n),ensure:a=>Promise.all([e,...n].map(e=>e.ensure(a))).then(()=>{})}}(a,e)}export{c 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
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{c as a,A as e}from"./houseplan-card-BVZ7SgFj.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-3QYyyR8a.js";import"./form-kit.styles-Ck5TDLDh.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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{c as a,A as e}from"./houseplan-card-BYwTiVnu.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-B0Dq0vzN.js";import"./form-kit.styles-v5DlO12L.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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{eD as e,N as t,b2 as n,L as i,ae as o,ac as r,c6 as s,eE as a,c5 as c,b1 as l,b0 as d,b3 as u,eF as f,eG as m,c8 as g,eH as v}from"./houseplan-card-BVZ7SgFj.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};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{eH as e,N as t,b2 as n,L as i,ae as o,ac as r,c9 as s,eI as a,c8 as c,b1 as l,b0 as d,b3 as u,eJ as f,eK as m,cb as g,eL as v}from"./houseplan-card-BYwTiVnu.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};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";export{e as dictionary,n as fingerprint};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";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="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";export{e as dictionary,n as fingerprint};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";export{e as dictionary,s as fingerprint};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";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="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";export{e as dictionary,s as fingerprint};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";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 a="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";export{e as dictionary,a as fingerprint};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";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="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";export{e as dictionary,t as fingerprint};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{n as t,c as n}from"./zigbee-topology-CjcVZzSn.js";import{aM as e}from"./houseplan-card-BVZ7SgFj.js";import"./namespace-language-CIeeXPH2.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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{n as t,c as n}from"./zigbee-topology-DOAnbvlu.js";import{aM as e}from"./houseplan-card-BYwTiVnu.js";import"./namespace-language-zIuNvgGV.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 f(t){if(!0!==t?.user?.is_admin)throw a("permission")}function p(n){const e=i(n);return e?c(e,"zha",async()=>{if(f(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(f(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,p=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{p=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 f=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&&p?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof f&&b.push(f),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{p as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";try{await import("./houseplan-assets/houseplan-card-BVZ7SgFj.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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";try{await import("./houseplan-assets/houseplan-card-BYwTiVnu.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
+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.6"
"version": "1.78.0"
}
+21 -40
View File
@@ -1,8 +1,8 @@
{
"schema": 3,
"matrixVersion": 66,
"acceptedAt": "2026-09-27T20:58:44.384Z",
"sourceFingerprint": "8da786e2f4a72b200d450d424e4016fc039ec2611e3b7a28c1450d60bdd2b346",
"matrixVersion": 67,
"acceptedAt": "2026-09-28T13:36:48.059Z",
"sourceFingerprint": "baa0806921c7f72959047c7d2bda343ef5e378c4e8d2a8e612f74b6af2e2c5b1",
"chromium": "151.0.7922.34",
"capturedOn": "linux",
"acceptedOn": "linux",
@@ -10,41 +10,18 @@
"platform": "linux",
"arch": "x64",
"chromium": "151.0.7922.34",
"buildFingerprint": "8da786e2f4a72b200d450d424e4016fc039ec2611e3b7a28c1450d60bdd2b346",
"ci": null
},
"localAttestation": {
"schema": "houseplan-golden-wsl/v1",
"sha256": "a56c54ffef7cd397d89f9eb895937c9af4c3d053d1289ee5c2cf7e9ecb8dd42b",
"artifactSha256": "4b1b850c512f183c1ceb6ed91e4bb61d18a9ec34fd7de676bbfdf7d6214a645c",
"createdAt": "2026-09-27T20:58:14.399Z",
"source": {
"buildFingerprint": "baa0806921c7f72959047c7d2bda343ef5e378c4e8d2a8e612f74b6af2e2c5b1",
"ci": {
"repository": "Matysh/houseplan-card",
"branch": "issue/683-stair-visuals",
"commit": "d5f3e17f2438e99b49c2502fc6856b5e5476cc2e",
"tree": "7a2cbd83cd43722c9690d0e1b6be94bd2a3b0ba2",
"remoteSha": "d5f3e17f2438e99b49c2502fc6856b5e5476cc2e",
"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"
"run": 36427848424,
"attempt": 1,
"sha": "c1289886175e165c5be09e4bd700a572529da234"
}
},
"localAttestation": null,
"foreignCapture": null,
"witnesses": {
"count": 128,
"count": 107,
"floor": 10
},
"scenarios": {
@@ -53,10 +30,10 @@
"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",
"stairs-flat-normal-light": "abd698576bb726bc70226deb72c12c3b7ebf36dd3007186e5d9444d630075bef",
"stairs-flat-hover-dark": "2502c9526ce73017e465bd35a8609afa0dbcac8dfa34ef37cfad8b3f39ebcd52",
"stairs-flat-selected-light": "c395ae3969521f6a157ff6e9460971db26287f9a5ca73f6ecbba6e8d35c3424f",
"stairs-isometric-dark": "038bb2752cb98b652541fdc6307f89e85ddd46212014e1991f7086cb42911ab3",
"panel-wide-view-light-en": "275d09b3730361bb491a832fbb76723d90a7327086f042cbf0fdd79f4a002800",
"panel-wide-plan-editor-dark-en": "093a3fd1022755f5128261796e009383d7e7a23e9e50f193ab1987d30f9313b2",
"panel-narrow-read-only-empty-light-en": "99b8f1af82b1ad13f237125aab8f7e7da0563c138e9da2ddf06e54c2d56138c4",
@@ -92,9 +69,9 @@
"pdf-export-stepped-dimensions-light": "b9732b8b9c38153c564d0c93fbfbf44a8dd348c8667d2f8c2d14492e08e04f3e",
"vacuum-trail-smoothing-dark": "f8eecc726a3e8bdaba414584ad6afd48908ee9d96ea969a1e37758f4e76843e2",
"room-label-parity-view-dark": "4c00ee3119d3886a5906d5880769558083eb06547a2e2c56e91ae84498416bc9",
"room-label-parity-plan-dark": "c0f08f586116c4070723ec89697aa5d966ea57f1c027e7740eeb5147d44d9958",
"room-label-parity-plan-dark": "88374fbca15362da485ff32a0c5dce21bd07ee752087a863e10484537b18a930",
"room-label-parity-view-light": "ec2a1294b0f08019f49a392cdc94970c1545673ebe0d7f22652f602adf00d4e8",
"room-label-parity-plan-light": "2603d0c73b39fc72f8b31919fcee15d8b17ea3c6ba1dea74fc791efdd3cfafc6",
"room-label-parity-plan-light": "1ee88b46a711e9116fefbeb8205618d68ddaa99fa3f290f0881e696afd1d1e57",
"washer-active-cycle-dark": "0d683619c27747b3dd6a1e6c7cdc5e15dd886f4f2db4b85b7ca5a5c529e9ec52",
"washer-idle-cycle-dark": "0138902daa8dabb0588904507ef5a44f5257f7859b4f1198191bfca3dd4130b7",
"day-cycle-dawn-dark": "96bd5ef84345976bb83d35d418e7d247503629eceb732b6e926f2217f3b9faa4",
@@ -124,6 +101,10 @@
"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": "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",
@@ -183,7 +164,7 @@
"junction-x-90-equal15-dark": "f01ecc2b28d457fbff0581681dc8c1a1555558baf4c0ac8e2bd4bdd3637f51b4",
"junction-x-90-mixed-dark": "1218a50ad968eae651e61637eeaed8238d0598d77749d3c9cdb7bdd7c0d6a063",
"junction-x-45-alternating-dark": "08de6b9d3a0e6d0164efc014fb1845cdd70cccb95b05b0d55175f2bca89c378b",
"junction-star5-equal15-dark": "4bc9337b4bd2ac4bcb8d168166330faad20184c8b3e51b153ef91d0ad76124c1",
"junction-star5-equal15-dark": "08a2410949b60cad38885004c01821935fd04dd1aed15880593069fab6db7e06",
"junction-y-60-equal50-dark": "89994b12fd168b3e35ee47520fafa502f904391f98cec909fccac19b7b0a90e5",
"junction-acute30-mixed-dark": "bc4f61f377711df71950ea6a8bea7d185c94b5e03bfc2e8fc4930fa9adc9ca02",
"junction-acute15-equal50-dark": "3883df35644df7cc169cacb96a5ee8391d64878930c69a0d511cd0cca0f2a04e",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+25
View File
@@ -1393,6 +1393,31 @@ export async function prepareGoldenScenario(page, scenario) {
card.requestUpdate();
await card.updateComplete;
}
if (scenario.staticHatchSharpness) {
const root = card.renderRoot;
const scene = root.querySelector('.plan-svg');
const view = card._viewOr(card._baseVb());
const exactViewBox = `${view.x} ${view.y} ${view.w} ${view.h}`;
const scenes = [...root.querySelectorAll('[data-hp-live-viewbox]')];
const layers = [...root.querySelectorAll('[data-hp-live-layer="camera"]')];
const pattern = root.querySelector('defs > pattern#hp-wall-hatch');
const visibleKinds = new Set([...root.querySelectorAll('.opening[data-kind]')]
.map((node) => node.getAttribute('data-kind')));
const modelKinds = new Set((card._openingsR || []).map((opening) => opening.type));
const settled = scene?.getAttribute('viewBox') === exactViewBox
&& scenes.every((node) => ['', 'none'].includes(getComputedStyle(node).transform))
&& scenes.every((node) => !getComputedStyle(node).willChange
|| getComputedStyle(node).willChange === 'auto')
&& layers.every((node) => ['', 'none'].includes(getComputedStyle(node).transform));
// #689: the historical pattern at every scale (#685's gradient reverted).
const hatch = !!pattern && !root.querySelector('linearGradient#hp-wall-hatch');
const openings = ['door', 'window', 'gate'].every((kind) => visibleKinds.has(kind))
&& modelKinds.has('passage');
if (!settled || !hatch || !openings
|| Math.abs(card._zoom - scenario.zoom) > 1e-9) {
throw new Error(`static hatch sharpness contract failed: ${scenario.id}`);
}
}
if (scenario.wallJunctionPreview) {
const { path, pointer, cms, cm } = scenario.wallJunctionPreview;
const validPoint = (point) => Array.isArray(point) && point.length === 2
+16 -1
View File
@@ -1,7 +1,7 @@
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
export const GOLDEN_MATRIX_VERSION = 66;
export const GOLDEN_MATRIX_VERSION = 67;
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
@@ -653,6 +653,21 @@ export const GOLDEN_SCENARIOS = Object.freeze([
{ id: 'opening-symbol-room-wall-light', fixture: 'visual', space: 'golden-opening-symbols',
mode: 'view', openingSymbolContract: openingRoomContract,
theme: 'light', viewport: { width: 1000, height: 900 }, ...stage },
...[
['static-hatch-openings-132-dpr1-light', 1.32, 1, 'light'],
['static-hatch-openings-140-dpr1-dark', 1.40, 1, 'dark'],
['static-hatch-openings-132-dpr2-dark', 1.32, 2, 'dark'],
['static-hatch-openings-140-dpr2-light', 1.40, 2, 'light'],
].map(([id, zoom, deviceScaleFactor, theme]) => ({
id, fixture: 'visual', space: 'golden-opening-symbols', mode: 'view',
openingSymbolContract: openingRoomContract,
extraOpenings: [{
id: 'sharpness-passage', type: 'passage', x: 0.62, y: 0.50,
angle: 0, length: 0.06,
}],
staticHatchSharpness: true, zoom, zoomCenter: [500, 500],
deviceScaleFactor, theme, viewport: { width: 1000, height: 900 }, ...stage,
})),
{ id: 'opening-symbol-diagonal-partition-dark', fixture: 'visual', space: 'golden-opening-symbols',
mode: 'view', openingSymbolContract: openingPartitionContract,
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
+4 -1
View File
@@ -289,7 +289,10 @@ checks.terminalFrameKeepsBudget = settled.layers.every(
checks.settledPlanLayerIsExplicitAndBounded = !!settledPlanLayer
&& settledPlanLayer.width <= active.stage.width * 1.5 + 64
&& settledPlanLayer.height <= active.stage.height * 1.5 + 64
&& settledPlanLayer.reasons.some((reason) => reason.includes('will-change: transform'))
// #689: explicit, but through the opacity hint — the transform hint
// freezes the raster scale and the plan stays blurry after a zoom.
&& settledPlanLayer.reasons.some((reason) => reason.includes('will-change: opacity'))
&& settledPlanLayer.reasons.every((reason) => !reason.includes('will-change: transform'))
&& settledPlanLayer.reasons.every((reason) => !reason.includes('Overlaps other composited content'));
checks.capturedPresentedPinchFrames = frameMetrics.length >= 3;
checks.presentedFramesHaveNoWhiteTile = frameMetrics.every(
+3 -1
View File
@@ -166,7 +166,9 @@ out.dayCycleOutlineFiltered = /drop-shadow/.test(safeDayStyle?.filter || '');
out.dayCycleOutlinePromoted = /filter/.test(safeDayStyle?.willChange || '');
out.dayCyclePaperStaysUnfiltered = (safeDayStyle?.paperFilter || 'none') === 'none'
&& !/filter/.test(safeDayStyle?.paperWillChange || '');
out.safePlanLayerIsExplicit = /transform/.test(safeDayStyle?.planWillChange || '');
// #689: the explicit scene layer is an opacity hint; a transform hint would
// freeze its raster scale after a zoom.
out.safePlanLayerIsExplicit = (safeDayStyle?.planWillChange || '') === 'opacity';
out.rasterTasksObserved = [...staticSamples, ...dayCycleSamples].every((sample) => sample.tasks > 0);
const staticMedianMs = median(staticSamples.map((sample) => sample.ms));
+236
View File
@@ -0,0 +1,236 @@
// #689: with the day/night background the plan layers must stay stage-sized at
// any zoom, and the scene must not keep a will-change: transform hint (its explicit layer is
// an opacity hint). Before
// the fix, CDP LayerTree measured (~460 %, DPR 2) plan-svg at 15.9× and the
// filtered outline at 13.2× the stage during a gesture, the outline at 12.8×
// at rest; at 800 % that is hundreds of MB of GPU memory and navigation
// flashed white. The same will-change: transform hint froze the raster scale:
// zooming from 100 % showed the 100 % raster stretched. Headless Chromium can
// see neither the blur nor the flashes (owner-verified in real Chrome), so this
// witness measures the state that causes them: layer sizes and the hint.
import { launch, checkAll, finish } from './serve.mjs';
import { installHpTestOnPage } from './helpers/hp-test.mjs';
const { page, browser } = await launch({ width: 820, height: 760 }, 2);
const cdp = await page.context().newCDPSession(page);
let latestLayers = [];
cdp.on('LayerTree.layerTreeDidChange', ({ layers }) => { latestLayers = layers; });
await cdp.send('LayerTree.enable');
const frames = [];
cdp.on('Page.screencastFrame', (event) => {
if (frames.length < 120) frames.push(event.data);
void cdp.send('Page.screencastFrameAck', { sessionId: event.sessionId });
});
const settle = async () => {
await page.waitForTimeout(1200);
await page.evaluate(async () => {
await window.__card.updateComplete;
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
});
};
const dayNight = async () => {
// A dark custom room fill keeps the near-white frame sample meaningful at
// 800 %, where the demo's own light paper would otherwise fill the stage.
await page.evaluate(() => window.__hpTest.setServerConfig((cfg) => ({
...cfg,
settings: { ...(cfg.settings || {}), bg_mode: 'daynight', glow_enabled: false },
spaces: cfg.spaces.map((space) => ({
...space, settings: { ...(space.settings || {}), fill_mode: 'custom', custom_fill: '#435468' },
})),
})));
await page.evaluate(async () => {
const card = window.__card;
card.hass = { ...card.hass, states: { ...card.hass.states, 'sun.sun': {
entity_id: 'sun.sun', state: 'above_horizon',
attributes: { azimuth: 180, elevation: 40, rising: false },
} } };
await card.updateComplete;
});
await settle();
};
const stageBox = () => page.evaluate(() => {
const rect = window.__card.renderRoot.querySelector('.stage').getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
const snapshot = async () => {
await page.waitForTimeout(80);
const state = await page.evaluate(() => {
const root = window.__card.renderRoot;
const stage = root.querySelector('.stage');
const plan = root.querySelector('.plan-svg');
const outline = root.querySelector('.hp-paper-outline-svg');
return {
zoom: window.__card._zoom,
daycycle: stage.classList.contains('daycycle'),
safe: stage.classList.contains('hp-safe-daycycle-outline'),
planWillChange: getComputedStyle(plan).willChange,
planTransform: getComputedStyle(plan).transform,
outlineOverflow: outline ? getComputedStyle(outline).overflow : null,
outlineInline: outline ? `${outline.style.overflow}|${outline.style.clipPath}` : null,
sceneClips: [...root.querySelectorAll('[data-hp-live-viewbox]')].map((node) => node.style.clipPath || ''),
};
});
const layers = [];
for (const layer of latestLayers.filter((item) => item.drawsContent)) {
let name = '';
if (layer.backendNodeId) {
try {
const { node } = await cdp.send('DOM.describeNode', { backendNodeId: layer.backendNodeId });
const attrs = node.attributes || [];
const index = attrs.indexOf('class');
name = index >= 0 ? attrs[index + 1] : node.nodeName;
} catch { /* detached between the event and inspection */ }
}
let reasons = [];
try {
({ compositingReasons: reasons } = await cdp.send('LayerTree.compositingReasons', {
layerId: layer.layerId,
}));
} catch { /* retired layers may have no reason record */ }
layers.push({ name, width: layer.width, height: layer.height, reasons });
}
return { state, layers };
};
// Everything the card paints; the page root and the sticky header are not plan.
const planLayers = (snap) => snap.layers.filter((layer) => layer.name
&& layer.name !== '#document' && !/\bhdr\b/.test(layer.name));
const within = (snap, stage, factor) => planLayers(snap).every((layer) =>
layer.width <= stage.width * factor + 240 && layer.height <= stage.height * factor + 240);
const largest = (snap) => planLayers(snap).reduce((max, layer) =>
Math.max(max, layer.width * layer.height), 0);
const wheelAt = async (stage, deltaY, count) => {
// Near a corner: the hover tooltip stays out of the central frame sample.
await page.mouse.move(stage.x + 40, stage.y + 40);
for (let i = 0; i < count; i++) {
await page.mouse.wheel(0, deltaY);
await page.waitForTimeout(24);
}
};
await dayNight();
const stage = await stageBox();
await cdp.send('Page.startScreencast', { format: 'png', everyNthFrame: 2, maxWidth: 820, maxHeight: 760 });
// A: page loaded at 100 %, zoomed to 800 % with the wheel (owner's blur path).
await wheelAt(stage, -240, 30);
const zoomingIn = await snapshot();
await settle();
const zoomedIn = await snapshot();
// The buttons and a pinch settle into the same hint-free scene (AC1).
const clickZoom = (hook) => page.evaluate((hook) =>
window.__card.renderRoot.querySelector(`[data-hp="${hook}"]`).click(), hook);
await clickZoom('zoom-out'); await settle();
await clickZoom('zoom-in'); await settle();
const buttons = await snapshot();
await page.evaluate(() => {
const stage = window.__card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
stage.setPointerCapture = () => {};
stage.releasePointerCapture = () => {};
const emit = (type, id, x) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id, pointerType: 'touch',
isPrimary: id === 6891, button: type === 'pointerdown' ? 0 : -1,
buttons: type === 'pointerup' ? 0 : 1, clientX: x, clientY: rect.top + rect.height / 2,
}));
const cx = rect.left + rect.width / 2;
emit('pointerdown', 6891, cx - 120); emit('pointerdown', 6892, cx + 120);
emit('pointermove', 6891, cx - 90); emit('pointermove', 6892, cx + 90);
emit('pointerup', 6891, cx - 90); emit('pointerup', 6892, cx + 90);
});
await settle();
const pinched = await snapshot();
await wheelAt(stage, -240, 12);
await settle();
// B: reload at 800 % (the camera is restored), day/night again, then navigate
// (owner's white-flash path).
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__card?._model?.length > 0, { timeout: 9000 });
await installHpTestOnPage(page);
await dayNight();
const reloaded = await snapshot();
await wheelAt(stage, 240, 3);
const navigating = await snapshot();
await wheelAt(stage, -240, 6);
await settle();
const navigated = await snapshot();
await cdp.send('Page.stopScreencast');
const nearWhite = await page.evaluate(async ({ encoded, stage }) => {
const out = [];
for (const data of encoded) {
const image = await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = `data:image/png;base64,${data}`;
});
const sx = image.width / innerWidth, sy = image.height / innerHeight;
const w = Math.max(1, Math.floor(stage.width * sx)), h = Math.max(1, Math.floor(stage.height * sy));
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(image, Math.floor(stage.x * sx), Math.floor(stage.y * sy), w, h, 0, 0, w, h);
const px = context.getImageData(0, 0, w, h).data;
let sampled = 0, white = 0;
for (let y = Math.floor(h * 0.25); y < h * 0.75; y += 3) {
for (let x = Math.floor(w * 0.25); x < w * 0.75; x += 3) {
const o = (y * w + x) * 4;
sampled++;
if (px[o] >= 250 && px[o + 1] >= 250 && px[o + 2] >= 250) white++;
}
}
out.push(white / Math.max(1, sampled));
}
return out;
}, { encoded: frames, stage });
const planReasons = (snap) => planLayers(snap).find((layer) => /\bplan-svg\b/.test(layer.name))?.reasons || [];
const out = {
dayNightSafeAt800: zoomedIn.state.daycycle && zoomedIn.state.safe && zoomedIn.state.zoom === 8,
reloadRestores800Untouched: reloaded.state.daycycle && !reloaded.state.safe && reloaded.state.zoom === 8,
// K1: no frozen raster — the explicit layer is a trivial 3D transform.
settledSceneHasNoWillChangeHint: [zoomedIn, buttons, pinched, navigated]
.every((snap) => snap.state.planWillChange === 'opacity'),
buttonAndPinchChangedTheCamera: buttons.state.safe && pinched.state.zoom < 8,
// The scene stays an explicit layer (#582), but no reason is the hint that
// freezes the raster scale, and it is never an implicit overlap layer.
settledSceneLayerIsExplicitWithoutHint: planReasons(zoomedIn).some((reason) => /will-change: opacity/.test(reason))
&& planReasons(zoomedIn).every((reason) => !/will-change: transform/.test(reason)
&& !/Overlaps other composited content/.test(reason)),
// K3: layers stay stage-sized at 800 %.
gestureLayersWithinBudget: within(zoomingIn, stage, 1.5) && within(navigating, stage, 1.5),
settledLayersWithinBudget: [zoomedIn, buttons, pinched, reloaded, navigated]
.every((snap) => within(snap, stage, 1)),
outlineClippedToItsBox: zoomedIn.state.outlineOverflow === 'hidden'
&& navigating.state.outlineInline === '|' && navigated.state.outlineInline === '|',
idleScenesKeepNoClip: [zoomedIn, buttons, pinched, reloaded, navigated].every((snap) => snap.state.sceneClips.every((clip) => clip === '')),
capturedFrames: nearWhite.length >= 3,
// A white/transparent frame covers the stage; white glyphs of 800 % device
// markers stay a few per cent of the central sample.
noWhiteFrames: nearWhite.every((ratio) => ratio < 0.25),
};
console.log(JSON.stringify({
stage,
largest: {
zoomingIn: largest(zoomingIn), zoomedIn: largest(zoomedIn), reloaded: largest(reloaded),
navigating: largest(navigating), navigated: largest(navigated),
},
layers: { zoomingIn: planLayers(zoomingIn), zoomedIn: planLayers(zoomedIn), navigating: planLayers(navigating) },
states: {
zoomingIn: zoomingIn.state, zoomedIn: zoomedIn.state, buttons: buttons.state,
pinched: pinched.state, reloaded: reloaded.state, navigated: navigated.state,
},
whiteMax: Math.max(0, ...nearWhite),
}, null, 1));
checkAll(out);
await cdp.send('LayerTree.disable');
await finish(browser, out);
+2 -1
View File
@@ -14,7 +14,8 @@ const res = await page.evaluate(async () => {
out.gearReadable = cs ? parseFloat(cs.fontSize) > 0 && cs.pointerEvents === 'auto' : null;
out.gearHasLabel = btn ? btn.textContent.trim().length > 0 : null;
const box = btn?.getBoundingClientRect();
// (в редакторе плана .dev скрыты display:none — сравнивать не с чем)
// (маркеры устройств в редакторе плана — неинтерактивные ориентиры #687,
// кнопку с ними не сравниваем)
out.gearTapTarget = box ? box.height > 6 && box.height < 40 : null;
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
await c.updateComplete;
+28 -9
View File
@@ -26,24 +26,43 @@ const res = await page.evaluate(async () => {
// 2) редакторы заблокированы
c._setMode('plan'); await c.updateComplete;
out.editorsBlocked = c._mode === 'view';
// 3) свайп при 1:1 переключает пространство (и точки показываются)
// 3) центральный drag остаётся pan, а свайп из внутренней 48 px зоны
// правого края при 1:1 переключает на соседнее пространство.
const s0 = c._space;
const stage = sr().querySelector('.stage');
stage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, composed: true, pointerId: 1, pointerType: 'touch', isPrimary: true, button: 0, clientX: 600, clientY: 300 }));
stage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, composed: true, pointerId: 1, pointerType: 'touch', isPrimary: true, button: 0, clientX: 450, clientY: 305 }));
const fire = (type, pointerId, clientX, clientY) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId, pointerType: 'touch',
isPrimary: true, button: 0, buttons: type === 'pointerup' ? 0 : 1, clientX, clientY,
}));
const rect = stage.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const centerView = JSON.stringify(c._viewOr(c._baseVb()));
fire('pointerdown', 1, centerX, centerY);
fire('pointermove', 1, centerX - 150, centerY + 5);
fire('pointerup', 1, centerX - 150, centerY + 5);
await c.updateComplete;
out.swipeSwitches = c._space !== s0;
out.centralDragPansWithoutSwitching = c._space === s0
&& JSON.stringify(c._viewOr(c._baseVb())) !== centerView && c._panLock === null;
const rightEdgeX = rect.right - 20;
fire('pointerdown', 2, rightEdgeX, centerY);
fire('pointermove', 2, rightEdgeX - 80, centerY + 2);
fire('pointerup', 2, rightEdgeX - 120, centerY + 3);
await c.updateComplete;
out.edgeSwipeSwitches = c._space !== s0;
out.dotsShown = !!sr().querySelector('.kioskdots');
// 4) при зуме свайп не переключает
c._zoom = 2; const s1 = c._space;
stage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, composed: true, pointerId: 2, pointerType: 'touch', isPrimary: true, button: 0, clientX: 600, clientY: 300 }));
stage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, composed: true, pointerId: 2, pointerType: 'touch', isPrimary: true, button: 0, clientX: 450, clientY: 305 }));
const leftEdgeX = rect.left + 20;
fire('pointerdown', 3, leftEdgeX, centerY);
fire('pointermove', 3, leftEdgeX + 80, centerY + 2);
fire('pointerup', 3, leftEdgeX + 120, centerY + 3);
await c.updateComplete;
out.noSwipeZoomed = c._space === s1;
// 5) двойной тап сбрасывает зум
for (const pointerId of [3, 4]) {
stage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, composed: true, pointerId, pointerType: 'touch', isPrimary: true, button: 0, clientX: 500, clientY: 300 }));
stage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, composed: true, pointerId, pointerType: 'touch', isPrimary: true, button: 0, clientX: 501, clientY: 300 }));
for (const pointerId of [4, 5]) {
fire('pointerdown', pointerId, centerX, centerY);
fire('pointerup', pointerId, centerX + 1, centerY);
}
const resetStarted = performance.now();
while (c._cameraTransition?.active) {
+57 -16
View File
@@ -2,8 +2,9 @@
// audit DEV-1DA1-02 (P2).
//
// `_stagePointerMove` locks the gesture on the first movement past 8 px
// (`_panLock`: 'swipe' if it is horizontal enough inside the swipe zone, 'pan'
// otherwise) and the plan then follows the finger. But `_stagePointerUp` used
// (`_panLock`: 'swipe' only for a sufficiently horizontal inward move that
// started inside the 48 px strip of an edge with a neighbour, 'pan' otherwise).
// A pan then follows the finger. But `_stagePointerUp` used
// to ignore that lock and ask `swipeTarget()` again, from the raw start→end
// vector alone. A CURVED gesture — a small vertical lead-in that locks 'pan',
// then a long horizontal sweep — therefore panned under the finger and still
@@ -12,7 +13,7 @@
//
// The lock is now final: with `_panLock === 'pan'` the floor never changes, no
// matter what the overall vector looks like. The two straight gestures keep
// their old behaviour, and so does the motionless double tap (no movement,
// their intended behaviour, and so does the motionless double tap (no movement,
// no lock — the swipe path is never even reached by it).
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
@@ -29,6 +30,10 @@ const out = await page.evaluate(async () => {
await k.updateComplete;
const sr = k.shadowRoot || k.renderRoot;
const stage = sr.querySelector('.stage');
const rect = stage.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const rightEdgeX = rect.right - 20;
const fire = (type, id, x, y) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id,
pointerType: 'touch', isPrimary: true, button: 0, clientX: x, clientY: y,
@@ -36,8 +41,10 @@ const out = await page.evaluate(async () => {
o.kioskHasSeveralSpaces = k._model.length > 1;
const home = async () => {
k._commitSpace(k._model[0].id, true);
const vb = k._baseVb();
k._applyView(1, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2);
k.requestUpdate();
await k.updateComplete;
};
/**
@@ -67,42 +74,76 @@ const out = await page.evaluate(async () => {
// ---- 1. the auditor's curved pan: vertical lead-in, horizontal ending --
// (500,300) → (502,312) locks 'pan' → curve left to (350,304) → release.
// dx = -150, dy = +4: swipeTarget() would happily call that a swipe.
const curvedPan = await play(51, [500, 300], [[502, 312], [460, 310], [400, 306], [350, 304]]);
const curvedPan = await play(51, [centerX, centerY], [
[centerX + 2, centerY + 12], [centerX - 40, centerY + 10],
[centerX - 100, centerY + 6], [centerX - 150, centerY + 4],
]);
o.curvedPanLocksPan = curvedPan.lockAfterLeadIn === 'pan';
o.curvedPanKeepsTheLock = curvedPan.lockBeforeRelease === 'pan';
o.curvedPanActuallyPans = curvedPan.moved === true;
o.curvedPanKeepsTheFloor = curvedPan.s1 === curvedPan.s0;
// the same trajectory the other way round — a swipe to the right would have
// been the previous floor, so the bug is symmetric and so is the fix
const curvedPanRight = await play(52, [400, 300], [[402, 312], [460, 308], [520, 305], [560, 304]]);
const curvedPanRight = await play(52, [centerX, centerY], [
[centerX + 2, centerY + 12], [centerX + 60, centerY + 8],
[centerX + 120, centerY + 5], [centerX + 160, centerY + 4],
]);
o.curvedPanRightLocksPan = curvedPanRight.lockAfterLeadIn === 'pan';
o.curvedPanRightKeepsTheFloor = curvedPanRight.s1 === curvedPanRight.s0;
// a long diagonal that ends up dominated by x, but started as a pan
const diagonalPan = await play(53, [500, 300], [[496, 316], [420, 340], [330, 350], [260, 352]]);
const diagonalPan = await play(53, [centerX, centerY], [
[centerX - 4, centerY + 16], [centerX - 80, centerY + 40],
[centerX - 170, centerY + 50], [centerX - 240, centerY + 52],
]);
o.diagonalPanLocksPan = diagonalPan.lockAfterLeadIn === 'pan';
o.diagonalPanKeepsTheFloor = diagonalPan.s1 === diagonalPan.s0;
// Even when a drag starts in the active edge strip, a vertical first move
// owns it as pan forever; a later horizontal tail cannot reclassify release.
const edgeCurvedPan = await play(62, [rightEdgeX, centerY], [
[rightEdgeX - 2, centerY + 14], [rightEdgeX - 80, centerY + 10],
[rightEdgeX - 170, centerY + 5],
]);
o.edgeCurvedPanLocksPan = edgeCurvedPan.lockAfterLeadIn === 'pan';
o.edgeCurvedPanActuallyPans = edgeCurvedPan.moved === true;
o.edgeCurvedPanKeepsTheFloor = edgeCurvedPan.s1 === edgeCurvedPan.s0;
// ---- 2. a gesture locked as a SWIPE keeps its own semantics ------------
// horizontal lead-in locks 'swipe'; the plan must not slide under it, even
// when the trajectory then bends vertically and the final vector no longer
// qualifies — the floor simply stays, and nothing pans
const curvedSwipe = await play(54, [600, 300], [[540, 302], [536, 380], [545, 500]]);
const curvedSwipe = await play(54, [rightEdgeX, centerY], [
[rightEdgeX - 60, centerY + 2], [rightEdgeX - 64, centerY + 80],
[rightEdgeX - 55, centerY + 200],
]);
o.curvedSwipeLocksSwipe = curvedSwipe.lockAfterLeadIn === 'swipe';
o.curvedSwipeNeverPans = curvedSwipe.moved === false;
o.curvedSwipeThatDiesChangesNothing = curvedSwipe.s1 === curvedSwipe.s0;
// a swipe that bends but still ends as a swipe does switch the floor
const bentSwipe = await play(55, [600, 300], [[540, 302], [470, 330], [420, 340]]);
const bentSwipe = await play(55, [rightEdgeX, centerY], [
[rightEdgeX - 60, centerY + 2], [rightEdgeX - 130, centerY + 30],
[rightEdgeX - 180, centerY + 40],
]);
o.bentSwipeLocksSwipe = bentSwipe.lockAfterLeadIn === 'swipe';
o.bentSwipeNeverPans = bentSwipe.moved === false;
o.bentSwipeStillSwitches = bentSwipe.s1 !== bentSwipe.s0;
// ---- 3. the straight gestures are exactly as they were -----------------
const straightSwipe = await play(56, [600, 300], [[540, 302], [480, 305], [450, 305]]);
const straightSwipe = await play(56, [rightEdgeX, centerY], [
[rightEdgeX - 60, centerY + 2], [rightEdgeX - 120, centerY + 5],
[rightEdgeX - 150, centerY + 5],
]);
o.straightSwipeSwitches = straightSwipe.s1 !== straightSwipe.s0;
o.straightSwipeDoesNotPan = straightSwipe.moved === false;
const straightPan = await play(57, [450, 200], [[452, 260], [454, 330]]);
const straightPan = await play(57, [centerX, centerY - 100], [
[centerX + 2, centerY - 40], [centerX + 4, centerY + 30],
]);
o.straightPanPans = straightPan.moved === true;
o.straightPanKeepsTheFloor = straightPan.s1 === straightPan.s0;
const centralHorizontalPan = await play(61, [centerX, centerY], [
[centerX - 60, centerY + 2], [centerX - 150, centerY + 5],
]);
o.centralHorizontalDragPans = centralHorizontalPan.moved === true;
o.centralHorizontalDragKeepsTheFloor = centralHorizontalPan.s1 === centralHorizontalPan.s0;
// ---- 4. a motionless double tap still resets the zoom ------------------
// no movement means no lock at all, so nothing above can reach this path
@@ -110,8 +151,8 @@ const out = await page.evaluate(async () => {
k._applyView(2);
await k.updateComplete;
for (const id of [58, 60]) {
fire('pointerdown', id, 500, 300);
fire('pointerup', id, 501, 300);
fire('pointerdown', id, centerX, centerY);
fire('pointerup', id, centerX + 1, centerY);
}
const resetStarted = performance.now();
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
@@ -125,11 +166,11 @@ const out = await page.evaluate(async () => {
await k.updateComplete;
const zoomed = await (async () => {
const s0 = k._space;
fire('pointerdown', 59, 600, 300);
fire('pointermove', 59, 540, 302);
fire('pointerdown', 59, rightEdgeX, centerY);
fire('pointermove', 59, rightEdgeX - 60, centerY + 2);
const lock = k._panLock;
fire('pointermove', 59, 480, 305);
fire('pointerup', 59, 480, 305);
fire('pointermove', 59, rightEdgeX - 120, centerY + 5);
fire('pointerup', 59, rightEdgeX - 120, centerY + 5);
await k.updateComplete;
return { lock, same: k._space === s0 };
})();
+9 -2
View File
@@ -52,12 +52,19 @@ const liveState = () => page.evaluate(() => {
view: structuredClone(card._view),
transformed: scenes.filter((node) => node.style.transform).length,
sceneCount: scenes.length,
// #689: an exposed scene opens only within its bound; the filtered
// day-cycle outline (marked clip) is projected but never opened.
allSceneOverflowOpen: scenes.length > 0
&& scenes.every((node) => node.style.overflow === 'visible'),
&& scenes.every((node) => (node.getAttribute('data-hp-live-overflow') === 'clip'
? !node.style.overflow && !node.style.clipPath
: node.style.overflow === 'visible' && node.style.clipPath === 'inset(-25%)')),
allScenePromoted: scenes.length > 0 && scenes.every((node) => node.style.transform
&& node.style.transformOrigin && node.style.willChange === 'transform'
&& node.style.overflow === 'visible'),
&& (node.getAttribute('data-hp-live-overflow') === 'clip'
? !node.style.overflow && !node.style.clipPath
: node.style.overflow === 'visible' && node.style.clipPath === 'inset(-25%)')),
allTemporaryStylesCleared: scenes.every((node) => !node.style.overflow
&& !node.style.clipPath
&& !node.style.transform && !node.style.transformOrigin && !node.style.willChange),
marker: centre(root.querySelector('[data-hp="device"]')),
room: centre(root.querySelector('[data-hp="room"]')),
+3 -2
View File
@@ -48,10 +48,11 @@ out.viewHoldInfo = await page.evaluate(() => { const r = !!window.__card._infoCa
await page.evaluate(() => window.__card._setMode('plan'));
await page.waitForFunction(() => window.__card._modeTransitionBusy === false);
out.plan = await st();
out.planIconsHidden = await page.evaluate(() => {
// #687: Plan keeps device markers as translucent, inert landmarks.
out.planIconsVisible = await page.evaluate(() => {
const sr = window.__card.shadowRoot || window.__card.renderRoot;
const dev = sr.querySelector('.dev');
return dev ? getComputedStyle(dev).display === 'none' : 'no-dev';
return dev ? getComputedStyle(dev).display !== 'none' : false;
});
// 4) режим Устройства: drag работает, клик открывает редактор
await page.evaluate(() => window.__card._setMode('devices'));
+16 -10
View File
@@ -9,7 +9,8 @@
// at 800 %, in view mode and in every editor; how far you may walk is decided
// by `_clampView` (PAN_SLACK: about a screen past the content) and not by the
// zoom. The tools keep the pointer they already owned — a resize handle, a
// device badge, an opening — and the kiosk keeps its floor swipe.
// device badge, an opening — and the kiosk keeps its floor swipe from the
// active 48 px edge while central horizontal drags remain pan.
import { launch, check, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const out = {};
@@ -192,7 +193,7 @@ Object.assign(out, await page.evaluate(async () => {
return o;
}));
// ---- the kiosk keeps its floor swipe ---------------------------------------
// ---- the kiosk keeps its edge-owned floor swipe ----------------------------
Object.assign(out, await page.evaluate(async () => {
const c0 = window.__card;
const o = {};
@@ -207,19 +208,24 @@ Object.assign(out, await page.evaluate(async () => {
const sr = k.shadowRoot || k.renderRoot;
const stage = sr.querySelector('.stage');
const fire = (type, id, x, y) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id, clientX: x, clientY: y,
bubbles: true, composed: true, cancelable: true, pointerId: id,
pointerType: 'touch', isPrimary: true, button: 0,
buttons: type === 'pointerup' ? 0 : 1, clientX: x, clientY: y,
}));
k._applyView(1); await k.updateComplete;
const rect = stage.getBoundingClientRect();
const rightEdgeX = rect.right - 20;
const centerY = rect.top + rect.height / 2;
const s0 = k._space;
const v0 = { ...k._viewOr(k._baseVb()) };
// a REAL swipe: down, several moves, up — the moves are what used to be
// impossible to add to smoke_kiosk without hijacking the gesture
fire('pointerdown', 41, 600, 300);
fire('pointermove', 41, 540, 302);
fire('pointermove', 41, 480, 305);
fire('pointermove', 41, 450, 305);
// A real floor swipe starts inside the active right-edge strip and moves
// inward. Several moves prove that the plan never pans under the gesture.
fire('pointerdown', 41, rightEdgeX, centerY);
fire('pointermove', 41, rightEdgeX - 60, centerY + 2);
fire('pointermove', 41, rightEdgeX - 120, centerY + 5);
fire('pointermove', 41, rightEdgeX - 150, centerY + 5);
const midPan = Math.abs(k._viewOr(k._baseVb()).x - v0.x);
fire('pointerup', 41, 450, 305);
fire('pointerup', 41, rightEdgeX - 150, centerY + 5);
await k.updateComplete;
o.kioskSwipeStillSwitchesFloors = k._space !== s0;
o.kioskHorizontalDragIsNotAPan = midPan < 1;
+206
View File
@@ -0,0 +1,206 @@
// #687: in the Plan editor device markers are landmarks exactly as in the
// Background editor (#362) — the same markers as View, at the Background
// editor's effective opacity, and fully pointer-inert so every Plan tool
// receives the point below them. Room labels (same .devlayer) stay opaque.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const probe = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const settleMode = async () => {
const started = performance.now();
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
while (c._modeTransitionBusy && performance.now() - started < 1500);
await c.updateComplete;
};
// Entering a mode settles the camera after the stage resizes; wait until
// marker positions hold still so measured points are the clicked points.
const layoutKey = () => [...sr().querySelectorAll('.devlayer [data-hp="device"]')]
.map((node) => { const r = node.getBoundingClientRect(); return `${r.left.toFixed(1)},${r.top.toFixed(1)}`; })
.join('|');
const enter = async (mode) => {
if (c._mode !== mode) await window.__hpTest.setMode(mode);
await settleMode();
const started = performance.now();
let key = layoutKey(), still = 0;
while (still < 10 && performance.now() - started < 3000) {
await new Promise((resolve) => requestAnimationFrame(resolve));
const next = layoutKey();
still = next === key ? still + 1 : 0;
key = next;
}
await c.updateComplete;
};
const stage = () => sr().querySelector('.stage');
const markers = () => [...sr().querySelectorAll('.devlayer [data-hp="device"]')];
const visible = (node) => {
const r = node.getBoundingClientRect();
return getComputedStyle(node).display !== 'none' && r.width > 0 && r.height > 0;
};
const visibleIds = () => markers().filter(visible).map((node) => node.dataset.id).sort();
// Effective alpha of one marker: its own and every ancestor's opacity and
// filter: opacity(), up to and including the stage.
const effectiveOpacity = (node) => {
let alpha = 1;
for (let el = node; el; el = el.parentElement) {
const cs = getComputedStyle(el);
alpha *= Number(cs.opacity);
for (const m of cs.filter.matchAll(/opacity\(([^)]+)\)/g)) alpha *= Number(m[1]);
if (el === stage()) break;
}
return alpha;
};
const opacityById = () => Object.fromEntries(markers().filter(visible)
.map((node) => [node.dataset.id, effectiveOpacity(node)]));
// One unavailable marker exercises contract item 2: the fade multiplies
// the marker's own opacity (.unavail) exactly as the Background layer does.
const target = markers().find((node) => node.dataset.entity && c.hass.states[node.dataset.entity]);
if (target) {
const entity = target.dataset.entity;
c.hass = { ...c.hass, states: { ...c.hass.states,
[entity]: { ...c.hass.states[entity], state: 'unavailable' } } };
await c.updateComplete;
}
await enter('view');
out.unavailableMarkerInView = !!sr().querySelector('.devlayer [data-hp="device"].unavail');
const viewIds = visibleIds();
out.viewHasMarkers = viewIds.length > 0;
await enter('decor');
const decorOpacity = opacityById();
await enter('plan');
const planIds = visibleIds();
const planOpacity = opacityById();
// AC1 — the same markers as View, each at the Background editor's alpha.
out.planShowsViewMarkers = JSON.stringify(planIds) === JSON.stringify(viewIds);
out.planOpacityMatchesBackground = planIds.length > 0 && planIds.every((id) =>
id in decorOpacity && Math.abs(planOpacity[id] - decorOpacity[id]) < 1e-3);
const plain = markers().find((node) => visible(node) && !node.classList.contains('unavail'));
out.planPlainMarkerAt35 = !!plain && Math.abs(planOpacity[plain.dataset.id] - 0.35) < 1e-3;
const unavail = markers().find((node) => visible(node) && node.classList.contains('unavail'));
out.planUnavailableMarkerAt35x35 = !!unavail
&& Math.abs(planOpacity[unavail.dataset.id] - 0.35 * 0.35) < 1e-3;
// Room labels share .devlayer: no layer fade reaches them (their own
// opacity is the label design, not this contract).
const label = sr().querySelector('.roomlabel');
out.roomLabelNotFaded = !!label
&& Math.abs(effectiveOpacity(label) - Number(getComputedStyle(label).opacity)) < 1e-3
&& !/opacity\(/.test(getComputedStyle(label).filter);
out.planMarkersOutOfTabOrder = markers().every((node) => !node.hasAttribute('tabindex')
&& !node.hasAttribute('role'));
// AC2 — pick a marker wholly inside the stage, preferring a value capsule
// that extends outside the marker's nominal box.
const s = stage().getBoundingClientRect();
const inside = markers().filter((node) => {
const r = node.getBoundingClientRect();
return visible(node) && r.left > s.left + 4 && r.right < s.right - 4
&& r.top > s.top + 4 && r.bottom < s.bottom - 4;
});
const at = (p) => p ? sr().elementFromPoint(p.x, p.y) : null;
const centre = (node) => {
const b = node.getBoundingClientRect();
return { x: b.left + b.width / 2, y: b.top + b.height / 2 };
};
// Room chrome lying under a marker keeps the hit (the demo plan puts a
// marker over a room's settings button).
const roomChrome = '.roomlabel, [data-hp="room-settings"]';
const overLabel = inside.find((node) => at(centre(node))?.closest?.(roomChrome));
out.roomChromeUnderMarkerGetsHit = !!overLabel;
const gearPoint = overLabel && at(centre(overLabel))?.closest?.('[data-hp="room-settings"]')
? centre(overLabel) : null;
// The tool probe lies over bare plan, so the Walls tool owns the point;
// prefer a value capsule that extends outside the marker's nominal box.
const overPlan = inside.filter((node) => !at(centre(node))?.closest?.('.devlayer'));
const dev = overPlan.find((node) => node.classList.contains('valonly')
|| node.querySelector('.device-shell.with-values')) || overPlan[0];
const frame = dev?.querySelector('.device-shell-frame');
const r = dev?.getBoundingClientRect();
const fr = frame?.getBoundingClientRect();
const core = dev ? centre(dev) : null;
const capsule = fr && fr.right > r.right + 2
? { x: fr.right - 1, y: fr.top + fr.height / 2 } : core;
out.probeFound = !!dev && !!core;
out.probeHasOutsideCapsule = !!fr && fr.right > r.right + 2;
const cursorAt = (p) => { const t = at(p); return t ? getComputedStyle(t).cursor : null; };
const hit = { core: at(core), capsule: at(capsule) };
const cursor = { core: cursorAt(core), capsule: cursorAt(capsule) };
out.coreFallsThrough = !!hit.core && !hit.core.closest('.dev');
out.capsuleFallsThrough = !!hit.capsule && !hit.capsule.closest('.dev');
out.markerSubtreeIsPointerInert = !!dev && !!frame
&& getComputedStyle(dev).pointerEvents === 'none'
&& getComputedStyle(dev, '::before').pointerEvents === 'none'
&& getComputedStyle(frame).pointerEvents === 'none'
&& [...dev.querySelectorAll('*')].every((el) => getComputedStyle(el).pointerEvents === 'none');
// "The cursor over a marker is the cursor of what lies below": the same
// point with the marker taken out of layout gives the same target/cursor.
if (dev) {
dev.style.display = 'none';
out.targetSameWithoutMarker = at(core) === hit.core && at(capsule) === hit.capsule;
out.cursorSameWithoutMarker = cursorAt(core) === cursor.core && cursorAt(capsule) === cursor.capsule;
dev.style.display = '';
}
await window.__hpTest.setTool('draw');
out.wallsChainEmptyBeforeClick = c._path.length === 0;
return { out, core, id: dev?.dataset.id, gearPoint };
});
// A real mouse click on the marker core reaches the Walls tool. It runs
// before the synthetic probes below, which leave pointer state behind.
if (probe.core) await page.mouse.click(probe.core.x, probe.core.y);
// A real click on a marker lying over a room's settings button opens the
// room dialog: the button below owns the point.
if (probe.gearPoint) await page.mouse.click(probe.gearPoint.x, probe.gearPoint.y);
const gear = await page.evaluate(async () => {
const c = window.__card;
await c.updateComplete;
const opened = c._roomDialog === true && !!c._roomEditId;
if (opened) await window.__hpTest.close(undefined, { via: 'cancel' });
return { roomSettingsOpenThroughMarker: opened, roomDialogClosed: !c._roomDialog };
});
const inert = await page.evaluate(async ({ core, id }) => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
await c.updateComplete;
out.wallsToolReceivesClickThroughMarker = c._path.length === 1;
const dev = [...sr().querySelectorAll('.devlayer [data-hp="device"]')].find((node) => node.dataset.id === id);
const r = dev?.getBoundingClientRect();
// The click above must have landed on the probed marker.
out.probeDidNotMove = !!r && !!core
&& Math.abs(r.left + r.width / 2 - core.x) < 1 && Math.abs(r.top + r.height / 2 - core.y) < 1;
// Handlers fail closed even when events are dispatched on the marker itself.
let serviceCalls = 0, wsCalls = 0;
const oldCallService = c.hass.callService;
const oldCallWS = c.hass.callWS;
c.hass.callService = () => { serviceCalls += 1; };
c.hass.callWS = async () => { wsCalls += 1; return { ok: true }; };
const before = { tip: c._tip, info: c._infoCard, drag: c._deviceDrag, sel: c._selId };
const opts = { bubbles: true, composed: true, cancelable: true, clientX: core?.x || 0, clientY: core?.y || 0 };
dev?.dispatchEvent(new PointerEvent('pointerover', { ...opts, pointerId: 687, pointerType: 'mouse', isPrimary: true }));
dev?.dispatchEvent(new PointerEvent('pointermove', { ...opts, pointerId: 687, pointerType: 'mouse', isPrimary: true }));
dev?.dispatchEvent(new PointerEvent('pointerdown', { ...opts, pointerId: 688, pointerType: 'mouse', button: 0, isPrimary: true }));
dev?.dispatchEvent(new PointerEvent('pointerup', { ...opts, pointerId: 688, pointerType: 'mouse', button: 0, isPrimary: true }));
dev?.dispatchEvent(new MouseEvent('click', { ...opts, button: 0 }));
const ctx = new MouseEvent('contextmenu', { ...opts, button: 2 });
dev?.dispatchEvent(ctx);
await c.updateComplete;
out.handlersFailClosed = serviceCalls === 0 && wsCalls === 0
&& c._tip === before.tip && c._infoCard === before.info
&& c._deviceDrag === before.drag && c._selId === before.sel;
out.noDeviceHoverAttribute = !sr().querySelector('.dev[data-hp-device-hover]');
out.contextMenuNotClaimed = !ctx.defaultPrevented;
c.hass.callService = oldCallService;
c.hass.callWS = oldCallWS;
return out;
}, { core: probe.core, id: probe.id });
const res = { ...probe.out, ...gear, ...inert };
checkAll(res);
await finish(browser, res);
+56 -13
View File
@@ -45,6 +45,8 @@ const out = await page.evaluate(async () => {
}
await c.updateComplete;
};
const waitMs = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const waitRoomDecision = () => waitMs(370);
const pointer = (target, type, id, x = 500, y = 300, extra = {}) =>
target.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true, pointerId: id,
@@ -66,8 +68,17 @@ const out = await page.evaluate(async () => {
};
localStorage.setItem('houseplan_card_zoom_v1', JSON.stringify({ 'fit-floor': 1 }));
const beforeDelayedRoomFit = JSON.stringify({ zoom: c._zoom, view: c._view });
pointer(roomNode(), 'pointerdown', 15201);
pointer(roomNode(), 'pointerup', 15201);
result.singleRoomTapWaitsForSecondTap = !c._cameraTransition?.active
&& c._roomFocus === null
&& JSON.stringify({ zoom: c._zoom, view: c._view }) === beforeDelayedRoomFit;
await waitMs(120);
result.singleRoomTapStillWaitsInsideWindow = !c._cameraTransition?.active
&& c._roomFocus === null
&& JSON.stringify({ zoom: c._zoom, view: c._view }) === beforeDelayedRoomFit;
await waitMs(250);
const tweenStarted = c._cameraTransition?.active === true;
const syncFrames = [];
if (tweenStarted) {
@@ -108,6 +119,7 @@ const out = await page.evaluate(async () => {
const settled = JSON.stringify(c._view);
pointer(roomNode(), 'pointerdown', 15202);
pointer(roomNode(), 'pointerup', 15202);
await waitRoomDecision();
await waitCamera();
result.repeatedFitIsNoOp = !c._cameraTransition.active && JSON.stringify(c._view) === settled;
@@ -158,10 +170,27 @@ const out = await page.evaluate(async () => {
result.areaLinkSuppressesRoomFit = JSON.stringify(c._view) === beforeLink
&& c._roomPointer === null && c._roomFocus === null;
c._clearRoomFocus(true);
pointer(roomNode(), 'pointerdown', 152041);
pointer(roomNode(), 'pointerup', 152041);
pointer(ownerProbe, 'pointerdown', 152042);
pointer(ownerProbe, 'pointerup', 152042);
await waitRoomDecision();
result.interactiveSecondTapCancelsPendingRoomFit = c._roomFocus === null
&& !c._cameraTransition.active;
c._clearRoomFocus(true);
pointer(roomNode(), 'pointerdown', 152043);
pointer(roomNode(), 'pointerup', 152043);
c._clearRoomFocus(true);
await waitRoomDecision();
result.lifecycleCleanupCancelsPendingRoomFit = c._roomFocus === null
&& !c._cameraTransition.active;
const stage = root.querySelector('.stage');
const base = c._baseVb();
const moveAway = async () => {
c._doubleFit.clear();
c._clearRoomFocus(true);
c._applyView(2, base[0] + base[2] * 0.65, base[1] + base[3] * 0.4);
await c.updateComplete;
};
@@ -211,14 +240,14 @@ const out = await page.evaluate(async () => {
result.cancelDisarmsThePreviousTap = c._zoom === 2 && !c._cameraTransition.active;
c._mode = 'decor';
c._doubleFit.clear();
c._clearRoomFocus(true);
pointer(stage, 'pointerdown', 44909, 400, 500);
pointer(stage, 'pointerup', 44909, 400, 500);
pointer(stage, 'pointerdown', 44910, 400, 500);
pointer(stage, 'pointerup', 44910, 400, 500);
result.editorBackgroundDoesNotFit = c._zoom === 2 && !c._cameraTransition.active;
c._mode = 'view';
c._doubleFit.clear();
c._clearRoomFocus(true);
pointer(roomNode(), 'pointerdown', 15205, 500, 300);
pointer(roomNode(), 'pointermove', 15205, 520, 300);
@@ -228,6 +257,7 @@ const out = await page.evaluate(async () => {
pointer(roomNode(), 'pointerdown', 15206, 500, 300);
pointer(roomNode(), 'pointerup', 15206, 500, 300);
await waitRoomDecision();
await waitCamera();
c.style.width = '760px';
c._lastValidStageSize = [1000, c._stageEl.clientHeight];
@@ -246,6 +276,7 @@ const out = await page.evaluate(async () => {
await c.updateComplete;
pointer(roomNode(), 'pointerdown', 15207);
pointer(roomNode(), 'pointerup', 15207);
await waitRoomDecision();
await waitCamera();
const isoMargins = margins();
result.isoUsesProjectedBounds = c._effectiveProjection() === 'iso'
@@ -267,19 +298,31 @@ const out = await page.evaluate(async () => {
pointerType: 'touch', isPrimary: true, button: 0,
buttons: type === 'pointerdown' ? 1 : 0, clientX: 450, clientY: 300,
}));
for (const id of [15208, 15209]) {
kioskPointer(kioskRoom, 'pointerdown', id);
kioskPointer(kioskRoom, 'pointerup', id);
const started = performance.now();
while (kiosk._cameraTransition.active && performance.now() - started < 1200) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
kioskPointer(kioskRoom, 'pointerdown', 15208);
kioskPointer(kioskRoom, 'pointerup', 15208);
const kioskSingleWaits = kiosk._roomFocus === null && !kiosk._cameraTransition.active;
await waitRoomDecision();
while (kiosk._cameraTransition.active) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
const beforeKioskBackground = JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view });
result.kioskSingleRoomTapFitsAfterDelay = kioskSingleWaits
&& kiosk._roomFocus?.roomId === 'room-a';
kiosk._clearRoomFocus(true);
kiosk._applyView(2);
const beforeKioskPair = JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view });
kioskPointer(kioskRoom, 'pointerdown', 15209);
kioskPointer(kioskRoom, 'pointerup', 15209);
const kioskRoomPairHasNoIntermediateMotion = !kiosk._cameraTransition.active
&& JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view }) === beforeKioskPair;
kioskPointer(kiosk._stageEl, 'pointerdown', 15210);
kioskPointer(kiosk._stageEl, 'pointerup', 15210);
result.kioskRoomTapDoesNotEnterDoubleTap = kiosk._roomFocus?.roomId === 'room-a'
&& JSON.stringify({ zoom: kiosk._zoom, view: kiosk._view }) === beforeKioskBackground;
result.kioskRoomThenBackgroundUsesDoubleFit = kioskRoomPairHasNoIntermediateMotion
&& kiosk._cameraTransition.state?.reason === 'double-tap'
&& kiosk._roomFocus === null;
while (kiosk._cameraTransition.active) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
kiosk._clearRoomFocus(true);
kiosk._applyView(2);
for (const id of [15211, 15212]) {
+51
View File
@@ -641,5 +641,56 @@ const out = await page.evaluate(async () => {
return result;
});
// #686: a focused stair link paints no focus frame — neither the browser's
// ring nor a replacement — and it still follows with Enter. The oracle is
// pixels: the stair's area with a margin for the ring is captured unfocused
// and with keyboard focus, and the two frames must be byte-identical.
const focusProbe = await page.evaluate(async () => {
const card = window.__card;
const hp = window.__hpTest;
await hp.setVolumetricView(false);
await hp.switchSpace('f1');
await hp.setMode('view');
await hp.settled();
const node = card.renderRoot.querySelector('[data-hp="stair"][data-target-state="active"]');
const rect = node?.getBoundingClientRect();
return node ? { id: node.getAttribute('data-id'), x: rect.x, y: rect.y, w: rect.width, h: rect.height } : null;
});
out.focusProbeHasActiveStair = !!focusProbe && focusProbe.w > 0 && focusProbe.h > 0;
if (focusProbe) {
const margin = 8;
const clip = {
x: Math.max(0, Math.floor(focusProbe.x - margin)), y: Math.max(0, Math.floor(focusProbe.y - margin)),
width: Math.ceil(focusProbe.w + 2 * margin), height: Math.ceil(focusProbe.h + 2 * margin),
};
await page.mouse.move(1, 1);
const shot = () => page.screenshot({ clip, animations: 'disabled', caret: 'hide' });
const unfocused = await shot();
// A real key press puts the page into keyboard modality, so the following
// programmatic focus matches :focus-visible exactly like Tab would.
await page.keyboard.press('Shift');
const focused = await page.evaluate(async (id) => {
const node = window.__card.renderRoot.querySelector(`[data-hp="stair"][data-id="${id}"]`);
node?.focus();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
return {
active: window.__card.renderRoot.activeElement === node,
visible: !!node?.matches(':focus-visible'),
};
}, focusProbe.id);
const withFocus = await shot();
out.keyboardFocusReachesStair = focused.active && focused.visible;
out.focusedStairPaintsNoFrame = focused.active && unfocused.equals(withFocus);
await page.keyboard.press('Enter');
out.focusedStairEnterNavigates = await page.evaluate(async () => {
const card = window.__card;
for (let guard = 0; card._cameraTransition?.active && guard < 90; guard++)
await new Promise((resolve) => requestAnimationFrame(resolve));
await window.__hpTest.settled();
return card.renderRoot.querySelector('[data-hp="space-tab"][aria-current="page"]')
?.getAttribute('data-id') === 'garden';
});
}
checkAll(out);
await finish(browser, out);
+149
View File
@@ -0,0 +1,149 @@
// Issue #685: a settled zoom must be a fresh vector frame after every route
// (button, wheel, pinch, every mode). #689 reverted #685's analytic gradient —
// the blur came from a frozen compositor raster, not from the hatch — so the
// wall hatch is again the one historical <pattern> at every scale.
import { createHash } from 'node:crypto';
import { launch, checkAll, finish } from './serve.mjs';
import { prepareGoldenScenario } from './golden/harness.mjs';
import { GOLDEN_SCENARIOS } from './golden/matrix.mjs';
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'opening-symbol-room-wall-light');
if (!scenario) throw new Error('opening-symbol-room-wall-light fixture is missing');
const sharpnessScenario = { ...scenario, extraOpenings: [{
id: 'sharpness-passage', type: 'passage', x: 0.62, y: 0.5, angle: 0, length: 0.06,
}] };
const { page, browser } = await launch({ width: 1000, height: 900 }, 1);
await prepareGoldenScenario(page, sharpnessScenario);
const settle = async () => {
await page.waitForFunction(() => !window.__goldenCard?._cameraTransition?.active);
await page.evaluate(async () => {
const card = window.__goldenCard;
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
};
const setZoom = async (zoom) => {
await page.evaluate((value) => {
const card = window.__goldenCard;
const vb = card._baseVb();
card._applyView(value, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2);
card.requestUpdate();
}, zoom);
await settle();
};
const state = () => page.evaluate(() => {
const card = window.__goldenCard;
const root = card.renderRoot;
const scene = root.querySelector('.plan-svg');
const view = card._viewOr(card._baseVb());
const exact = `${view.x} ${view.y} ${view.w} ${view.h}`;
const scenes = [...root.querySelectorAll('[data-hp-live-viewbox]')];
const layers = [...root.querySelectorAll('[data-hp-live-layer="camera"]')];
const pattern = root.querySelector('defs > pattern#hp-wall-hatch');
const kinds = new Set([...root.querySelectorAll('.opening[data-kind]')]
.map((node) => node.getAttribute('data-kind')));
const modelKinds = new Set((card._openingsR || []).map((opening) => opening.type));
return {
zoom: card._zoom,
exactViewBox: scene?.getAttribute('viewBox') === exact,
noSceneTransform: scenes.every((node) => ['', 'none'].includes(getComputedStyle(node).transform)),
noSceneWillChange: scenes.every((node) => !getComputedStyle(node).willChange
|| getComputedStyle(node).willChange === 'auto'),
noLayerTransform: layers.every((node) => ['', 'none'].includes(getComputedStyle(node).transform)),
patternHatch: !!pattern && pattern.getAttribute('patternUnits') === 'userSpaceOnUse'
&& !root.querySelector('linearGradient#hp-wall-hatch'),
openingKinds: ['door', 'window', 'gate'].every((kind) => kinds.has(kind))
&& modelKinds.has('passage'),
};
});
const clip = await page.locator('houseplan-card').evaluate((card) => {
const rect = card.renderRoot.querySelector('.stage').getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
const shotHash = async () => createHash('sha256')
.update(await page.screenshot({ clip, animations: 'disabled', scale: 'css' }))
.digest('hex');
// Button: 100 -> 140%.
await setZoom(1);
await page.locator('houseplan-card').evaluate((card) =>
card.renderRoot.querySelector('[data-hp="zoom-in"]').click());
await settle();
const button = await state();
const buttonHash = await shotHash();
// A different route to the exact same target: 196 -> 140%.
await setZoom(1.96);
await page.locator('houseplan-card').evaluate((card) =>
card.renderRoot.querySelector('[data-hp="zoom-out"]').click());
await settle();
const buttonReverse = await state();
const reverseHash = await shotHash();
// Wheel: one centred notch, 100 -> 115%.
await setZoom(1);
await page.locator('houseplan-card').evaluate((card) => {
const stage = card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
stage.dispatchEvent(new WheelEvent('wheel', {
bubbles: true, cancelable: true, deltaY: -100,
clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2,
}));
});
await settle();
const wheel = await state();
// Pinch: 100 -> 132%, then release both fingers and wait for the terminal Lit
// frame. The intermediate promoted transform is allowed; the returned state is
// deliberately sampled only after the two full settled frames above.
await setZoom(1);
await page.locator('houseplan-card').evaluate((card) => {
const stage = card.renderRoot.querySelector('.stage');
const rect = stage.getBoundingClientRect();
const emit = (type, id, x, y) => stage.dispatchEvent(new PointerEvent(type, {
bubbles: true, composed: true, cancelable: true,
pointerId: id, pointerType: 'touch', isPrimary: id === 71,
button: 0, buttons: type === 'pointerup' ? 0 : 1,
clientX: rect.left + x, clientY: rect.top + y,
}));
const cx = rect.width / 2, cy = rect.height / 2;
emit('pointerdown', 71, cx - 100, cy);
emit('pointerdown', 72, cx + 100, cy);
emit('pointermove', 71, cx - 132, cy);
emit('pointermove', 72, cx + 132, cy);
emit('pointerup', 71, cx - 132, cy);
emit('pointerup', 72, cx + 132, cy);
});
await settle();
const pinch = await state();
// The same architectural SVG is shared by View and all three editors.
const modes = [];
for (const mode of ['view', 'plan', 'devices', 'decor']) {
await page.evaluate((value) => window.__hpTest.setMode(value), mode);
await page.waitForTimeout(350);
await settle();
await setZoom(1.32);
modes.push(await state());
}
const terminal = (sample) => sample.exactViewBox && sample.noSceneTransform
&& sample.noSceneWillChange && sample.noLayerTransform && sample.patternHatch
&& sample.openingKinds;
const out = {
buttonSettlesAt140: Math.abs(button.zoom - 1.4) < 1e-9 && terminal(button),
reverseButtonSettlesAt140: Math.abs(buttonReverse.zoom - 1.4) < 1e-9
&& terminal(buttonReverse),
sameScaleIsDeterministic: buttonHash === reverseHash,
wheelSettlesAt115: Math.abs(wheel.zoom - 1.15) < 1e-9 && terminal(wheel),
pinchSettlesAt132: Math.abs(pinch.zoom - 1.32) < 1e-9 && terminal(pinch),
allModesUseSettledPatternHatch: modes.length === 4 && modes.every(terminal),
};
checkAll(out);
await finish(browser, { ...out, buttonHash, reverseHash, button, buttonReverse, wheel, pinch, modes });
+260 -260
View File
@@ -1,436 +1,436 @@
{
"schema": 1,
"fingerprint": "51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce",
"fingerprint": "a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 299153,
"initialViewGzipBytes": 300323,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 300675,
"initialPanelGzipBytes": 301845,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2324,
"initialPanelOnlyGzipBytes": 2325,
"lazyFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/editor-CbeYCXUv.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/fr-CpDKyK4Z.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"houseplan-assets/iso-scene-render-BxXtKC5L.js",
"houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/pdf-export-DL7VmwpB.js",
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js",
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
],
"lazyGzipBytes": 456031,
"lazyGzipBytes": 456389,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/editor-CbeYCXUv.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
],
"lazyEditorGzipBytes": 237970,
"lazyEditorGzipBytes": 238192,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"lazyOnboardingGzipBytes": 27944,
"lazyOnboardingGzipBytes": 27950,
"lazyLocaleFiles": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/fr-BLCP2_-N.js"
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/fr-CpDKyK4Z.js"
],
"lazyLocaleGzipBytes": 56294,
"lazyLocaleGzipBytes": 56330,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-SCzI9z9k.js"
"houseplan-assets/iso-scene-render-BxXtKC5L.js"
],
"lazyIsometricGzipBytes": 17882,
"lazyIsometricGzipBytes": 17885,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js"
],
"lazyFurnitureArtGzipBytes": 16941,
"lazyFurnitureArtGzipBytes": 16940,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/pdf-export-CGU5_sl_.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/pdf-export-DL7VmwpB.js"
],
"lazyPdfGzipBytes": 126914,
"lazyPdfGzipBytes": 126938,
"lazyNamespaceLocaleFiles": [
"houseplan-assets/settings-de-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js",
"houseplan-assets/support-de-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.js"
"houseplan-assets/settings-de-DfJ7EKF9.js",
"houseplan-assets/settings-fr-Bl79E-6s.js",
"houseplan-assets/settings-ru-CkNFmChi.js",
"houseplan-assets/support-de-DZoi-fxk.js",
"houseplan-assets/support-fr-Daosy89Q.js",
"houseplan-assets/support-ru-e8-BrqtH.js",
"houseplan-assets/topology-de-Cs36ouVE.js",
"houseplan-assets/topology-fr-B8yoJGvc.js",
"houseplan-assets/topology-ru-BRKShmrQ.js"
],
"lazyNamespaceLocaleGzipBytes": 21037,
"lazyNamespaceLocaleGzipBytes": 21057,
"files": [
{
"path": "houseplan-assets/backdrop-pick-CmogasSW.js",
"sha256": "aea6305d5506f017f2b0f8239cce47b10f2e02136b53b3002e2f708deab95444",
"path": "houseplan-assets/backdrop-pick-WDIF6iao.js",
"sha256": "5f6dcedf274594075d8acd31295d8c23f6d1408c890c086825db6bcbaa6c9b21",
"rawBytes": 54017,
"gzipBytes": 17574,
"gzipBytes": 17576,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": [
"houseplan-assets/settings-de-Crekk5Hk.js",
"houseplan-assets/settings-fr-BzCuGnet.js",
"houseplan-assets/settings-ru-BxY9uxyq.js"
"houseplan-assets/settings-de-DfJ7EKF9.js",
"houseplan-assets/settings-fr-Bl79E-6s.js",
"houseplan-assets/settings-ru-CkNFmChi.js"
]
},
{
"path": "houseplan-assets/de-D7PZcyoc.js",
"sha256": "5e425bd4558bffbf4ad81d3a51e3ecd9fe7f76697482131b00ddeae65c0b37a4",
"rawBytes": 94126,
"gzipBytes": 28375,
"path": "houseplan-assets/de-E17v3gXt.js",
"sha256": "ed10d6c5e7a82b275c9422be67c2213219a75dba9aab0ddb3e98813249cbd1d0",
"rawBytes": 94196,
"gzipBytes": 28393,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-CKG7G85d.js",
"sha256": "db6d125e73e1d2c8535645c576f7df912badd35bb93477d2a38537203c0dbba5",
"path": "houseplan-assets/editor-CbeYCXUv.js",
"sha256": "75887638c4d76040651a3cf52c56de10dc19e0cbc7583ee169d9b20cc5987788",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1584,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"sha256": "4252673f16e0382d3dd9ec967c8df782b2a315bb0658f05ad246f70e77aad222",
"path": "houseplan-assets/form-kit.styles-v5DlO12L.js",
"sha256": "cdab9e5e612be9b98c49193a2a6287080c13f62aaefa9a28b69bac94dd934012",
"rawBytes": 25600,
"gzipBytes": 5055,
"gzipBytes": 5056,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-BLCP2_-N.js",
"sha256": "b6f1cd483e2f0bb3c30ce2089b5e52fb65578e656496b2b9623b877b32339fa6",
"rawBytes": 96530,
"gzipBytes": 27919,
"path": "houseplan-assets/fr-CpDKyK4Z.js",
"sha256": "ee9fe5934eea69e549ff48d2e3fdabd7c20e0d9906a6f4623314f6eea48bf269",
"rawBytes": 96627,
"gzipBytes": 27937,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"sha256": "1152cf5d82e01ae897ef187661978a5d0c3a7bf49f8655c96ddaad564c191c29",
"path": "houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"sha256": "f668d3c0a47a53e096f26728187b1666fbb6b421c1baa2ff1a649fa1ba5340d5",
"rawBytes": 52035,
"gzipBytes": 16941,
"gzipBytes": 16940,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BVZ7SgFj.js",
"sha256": "de63fea2ef07a50481acafb3236f8f02962895cc630b026910f76b2be4c431fd",
"rawBytes": 1055543,
"gzipBytes": 298351,
"path": "houseplan-assets/houseplan-card-BYwTiVnu.js",
"sha256": "055b6f8ba3a472be163a15ef6e2348727b5b03e3660b9362f3aed21d7be90426",
"rawBytes": 1059790,
"gzipBytes": 299520,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-D7PZcyoc.js",
"houseplan-assets/editor-CKG7G85d.js",
"houseplan-assets/fr-BLCP2_-N.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"houseplan-assets/iso-scene-render-SCzI9z9k.js",
"houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"houseplan-assets/pdf-export-CGU5_sl_.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js"
"houseplan-assets/de-E17v3gXt.js",
"houseplan-assets/editor-CbeYCXUv.js",
"houseplan-assets/fr-CpDKyK4Z.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"houseplan-assets/iso-scene-render-BxXtKC5L.js",
"houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"houseplan-assets/pdf-export-DL7VmwpB.js",
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-CpIQitHC.js",
"sha256": "3fb964d9241252e3b00bedcc0c89f994aec183b3f657b6af38496b1f803db253",
"rawBytes": 687631,
"gzipBytes": 192112,
"path": "houseplan-assets/houseplan-editor-runtime-DZnn4pgB.js",
"sha256": "f25cbcb0453279e3ac64eda67df52af100d96c54bb93cb657bc08c355e0484ed",
"rawBytes": 688848,
"gzipBytes": 192327,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
],
"dynamicImports": [
"houseplan-assets/support-de-9JIBMnMv.js",
"houseplan-assets/support-fr-B7V5UpZx.js",
"houseplan-assets/support-ru-DVgRGpdZ.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js"
"houseplan-assets/support-de-DZoi-fxk.js",
"houseplan-assets/support-fr-Daosy89Q.js",
"houseplan-assets/support-ru-e8-BrqtH.js",
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-BzvIlGC6.js",
"sha256": "2fcbe1523d5e570804a65c9138d7d2b41c951320af19a36844f9536b32218564",
"path": "houseplan-assets/houseplan-onboarding-runtime-B6AiP0fG.js",
"sha256": "4ea3584c8b2bfc86f96c587549ddce49eaf0b918923df2df8e98c9c0a548f835",
"rawBytes": 15886,
"gzipBytes": 4906,
"gzipBytes": 4908,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-CmogasSW.js",
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/backdrop-pick-WDIF6iao.js",
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-CsVfJvR7.js",
"sha256": "bd59aa84c7afc96a1f50abaf8205bd745798264f27114a40cd7b334aabe1fd14",
"path": "houseplan-assets/hp-zigbee-topology-overlay-8lnIEMWk.js",
"sha256": "2a0f9af108b8a39889f90d7119d8e7789716c9960ea8e28af38ada07cdc91a8c",
"rawBytes": 10958,
"gzipBytes": 3767,
"gzipBytes": 3765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js",
"houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js",
"houseplan-assets/zigbee-topology-runtime-CKlry6R8.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-SCzI9z9k.js",
"sha256": "cf356172a58c90e29476142476c4d625b49d13c7414fbce151327b8b3626d40f",
"path": "houseplan-assets/iso-scene-render-BxXtKC5L.js",
"sha256": "4150eb9120fdaafa745761242ff3285056ceaccd7b016a16364574b90e3e2cfe",
"rawBytes": 52970,
"gzipBytes": 17882,
"gzipBytes": 17885,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-DmDVGMSM.js",
"sha256": "a395a4dcb26e33285521d157e16bacff01f26da5f46158b30e6e25c48679644e",
"rawBytes": 8216,
"gzipBytes": 3208,
"path": "houseplan-assets/live-interaction-runtime-CJ95X8vv.js",
"sha256": "66e70baa00f4c9470ac9a37a229652cf5c5585caa2364de4d467a1c2ebe24358",
"rawBytes": 8370,
"gzipBytes": 3280,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/namespace-language-CIeeXPH2.js",
"sha256": "4bbd058806d1870d8d6b109f1a7c1a29e86b0ee2188a46441d38ba6719795964",
"path": "houseplan-assets/namespace-language-zIuNvgGV.js",
"sha256": "244bce2d5211106846215be6a9d3dacda4c910d06020144f9ffd5e432c5e8c00",
"rawBytes": 675,
"gzipBytes": 409,
"gzipBytes": 410,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-CGU5_sl_.js",
"sha256": "0b1e4968ecf729dc267e03d63062bba53490e737635d9f3f1681a0320768e6d4",
"rawBytes": 236608,
"gzipBytes": 109973,
"path": "houseplan-assets/pdf-export-DL7VmwpB.js",
"sha256": "548dced1a8a54f860dbe57e61019d50f058d3ca13abb738d750a3c107c88de8b",
"rawBytes": 236714,
"gzipBytes": 109998,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-64Gvp7fP.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/furniture-plan-art.generated-B9UMtPsh.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-de-Crekk5Hk.js",
"sha256": "a769daa4cf5674115e7ed3172992b247e56f4ce72f883af46921dbb52f8512ad",
"path": "houseplan-assets/settings-de-DfJ7EKF9.js",
"sha256": "0580b9f6191edb664c21dee21e5263489eaf22876bcad67ef12e440abc03e28a",
"rawBytes": 7378,
"gzipBytes": 2879,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-Bl79E-6s.js",
"sha256": "f1bde9db7fe884aabd25130f783a84d8fe27bc9553923c17bcb9b82ee3976475",
"rawBytes": 7620,
"gzipBytes": 2842,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-CkNFmChi.js",
"sha256": "94f96c4a15a3a82ca8be3066629734379dd14eb2ff9e8f6acf369da2378676cb",
"rawBytes": 10304,
"gzipBytes": 3325,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-C_vpFucN.js",
"sha256": "d45dbdfa17a1da9f45bfbca6d6c3cc98849fd5ebcc06c8f72fd6310b04b93aad",
"rawBytes": 14862,
"gzipBytes": 3425,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-D47J-OjO.js",
"sha256": "909d2195fcfca30c2f1279e07e2d7506d470c3daec6fe864c97f7ecc4101e33d",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-B0Dq0vzN.js",
"sha256": "e44860a1af9ec7ffc7ac289c8c76ed1d0dcf0177bd91bbfe1981fdd0d4a62c46",
"rawBytes": 72959,
"gzipBytes": 20242,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-v5DlO12L.js",
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-C_vpFucN.js",
"houseplan-assets/summary-panel-metrics-D47J-OjO.js"
]
},
{
"path": "houseplan-assets/support-de-DZoi-fxk.js",
"sha256": "f01d2d00a169519f5f402e99e9c85c9f21a23aeee2a9605d4444910f3250d6de",
"rawBytes": 6167,
"gzipBytes": 2396,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-Daosy89Q.js",
"sha256": "e503a1b4e1b1ed31d8fa09bfe0cdc069a82c1d1627d40407472b336fbc98ccf5",
"rawBytes": 6333,
"gzipBytes": 2432,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-e8-BrqtH.js",
"sha256": "76742313c4b510163c380f8fd9c7fcaf1cc7f8bde6a41bcb0da48621a7c3a3b3",
"rawBytes": 8376,
"gzipBytes": 2876,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-BzCuGnet.js",
"sha256": "40a596406b0bafbc3f2d9a54b12b9ac9e1713f939f98a630c0a576b53780d255",
"rawBytes": 7620,
"gzipBytes": 2839,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-BxY9uxyq.js",
"sha256": "929c462c195a7cf42c8cb2d4b558b4ae5ab1508eeafe4fb6a9e890f3650464ff",
"rawBytes": 10304,
"gzipBytes": 3323,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"sha256": "388fb3fcd97490eee5cdb7140953f7b421fcd06bee24c7f19e0e79d24b777bde",
"rawBytes": 14862,
"gzipBytes": 3426,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js",
"sha256": "6c370c9858be82bdc1e77f77d598147be7673ac64656f533a62c0b15f6cfa5a9",
"rawBytes": 2701,
"gzipBytes": 1344,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-3QYyyR8a.js",
"sha256": "45c6b69dea88a448e81976da8cad5656bc7ee26ee49b3f9685d8bcba62d845a3",
"rawBytes": 72959,
"gzipBytes": 20243,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-Ck5TDLDh.js",
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-DmjpqSE0.js",
"houseplan-assets/summary-panel-metrics-Bfjyg5EQ.js"
]
},
{
"path": "houseplan-assets/support-de-9JIBMnMv.js",
"sha256": "83f703aba89eccb9e1b8ebf5e5b78d60521406a6a1ae7e4cc3a852a68f74b1c9",
"rawBytes": 6167,
"gzipBytes": 2395,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-fr-B7V5UpZx.js",
"sha256": "3515c3761d2870728cac7760d871ccd03d9468d5001e43670b14a5b64126f6d3",
"rawBytes": 6333,
"gzipBytes": 2429,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-DVgRGpdZ.js",
"sha256": "794b61c69025bbe3cee78266b8833a5c22094930bfbd419fdea43fc38ca2f8cf",
"rawBytes": 8376,
"gzipBytes": 2875,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-de-Eo3zRC2b.js",
"sha256": "b73933a557179215bdae75cc51b2713541a032ed6c0240b51599f4ab73bf0ebb",
"path": "houseplan-assets/topology-de-Cs36ouVE.js",
"sha256": "7c808f1139bb1c232627bfe884a579177ec653d16147d6b96066851893b312a4",
"rawBytes": 2698,
"gzipBytes": 1333,
"gzipBytes": 1337,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-fr-BZMZ_i-b.js",
"sha256": "341977cab7561948330323c3da99cde1c56f9a225f9f78c154ce37b6af9f6a0c",
"path": "houseplan-assets/topology-fr-B8yoJGvc.js",
"sha256": "6ee1530fd0bd7a8c476c79b83404c7d8a5b98cd0b86ee8a73048be75a70cc2c4",
"rawBytes": 2718,
"gzipBytes": 1353,
"gzipBytes": 1354,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-ru-MwP3wic3.js",
"sha256": "f49389bc3c849476d3b97d38493262f8eb7a10aa6432dd6d555578f486b21f4a",
"path": "houseplan-assets/topology-ru-BRKShmrQ.js",
"sha256": "7fc89542fa860318d459ce1467a5a8931f3214a9d17f57c177ba01f2cfbbd3e8",
"rawBytes": 3669,
"gzipBytes": 1614,
"gzipBytes": 1616,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-CjcVZzSn.js",
"sha256": "b0136228d2845e59ef35b87fff2e19d28609fcef222ad2f46d6c71f186a9ece2",
"path": "houseplan-assets/zigbee-topology-DOAnbvlu.js",
"sha256": "313f71677ba0739d9c978185c2136e73e8037b770eb3f9ef8561f692d654406f",
"rawBytes": 10958,
"gzipBytes": 4296,
"gzipBytes": 4299,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js"
],
"dynamicImports": [
"houseplan-assets/topology-de-Eo3zRC2b.js",
"houseplan-assets/topology-fr-BZMZ_i-b.js",
"houseplan-assets/topology-ru-MwP3wic3.js"
"houseplan-assets/topology-de-Cs36ouVE.js",
"houseplan-assets/topology-fr-B8yoJGvc.js",
"houseplan-assets/topology-ru-BRKShmrQ.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-runtime--7WS-Nzl.js",
"sha256": "25dd84923fd69b9a594b2b74a191f8e6dba398a757f8a05f3ea7c527bcf27399",
"path": "houseplan-assets/zigbee-topology-runtime-CKlry6R8.js",
"sha256": "39e4a250de0e67e80d8afc02c39c1a79e42d4b3e113107bb3817f4fa90cab1e2",
"rawBytes": 3983,
"gzipBytes": 1788,
"gzipBytes": 1789,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js",
"houseplan-assets/namespace-language-CIeeXPH2.js",
"houseplan-assets/zigbee-topology-CjcVZzSn.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js",
"houseplan-assets/namespace-language-zIuNvgGV.js",
"houseplan-assets/zigbee-topology-DOAnbvlu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "7348cd78a4e1e27962b597d63f8d877291176a296357a03568557dda09df0903",
"sha256": "d4b480db1327152db7b1459c45692ea6b72fb7ac3bccb525bc06adfe1aab3b8d",
"rawBytes": 1205,
"gzipBytes": 802,
"gzipBytes": 803,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "54a23573ba2306bb0734b8b0ab051555d3646f68a2d8ad90798d0e2c69cc0abd",
"sha256": "5aaa677a9c85f4df0f176be31a91ebb5022f5ce129774919269bb0409c310233",
"rawBytes": 6165,
"gzipBytes": 2324,
"gzipBytes": 2325,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BVZ7SgFj.js"
"houseplan-assets/houseplan-card-BYwTiVnu.js"
],
"dynamicImports": []
}
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,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{aD as e,l as o,aG as t,ev as s,A as a,ew as i,ex as l,ey as n,E as r,c}from"./houseplan-card-BVZ7SgFj.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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{aD as e,l as o,aG as t,ez as s,A as a,eA as i,eB as l,eC as n,E as r,c}from"./houseplan-card-BYwTiVnu.js";class f 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,f={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")},h=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}
.computeLabel=${e=>h[e.name]||e.name}
.computeLabel=${e=>f[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(f.slice(0,3))}
${d(h.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
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
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
View File
@@ -1 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{eA as e,eB as a,ex as n}from"./houseplan-card-BVZ7SgFj.js";function c(n,c){return new e([{code:"en",dictionary:n},{code:"ru",loadDictionary:c.ru},{code:"de",loadDictionary:c.de},{code:"fr",loadDictionary:c.fr}],"51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce",console.warn,a)}function d(e){return function(e,a){return{state:n=>{const c=e.state(n);return"pending"!==c&&a.some(e=>"pending"===e.state(n))?"pending":c},dictionary:a=>e.dictionary(a),ensure:n=>Promise.all([e,...a].map(e=>e.ensure(n))).then(()=>{})}}(n,e)}export{c as n,d as s};
+1
View File
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{eE as e,eF as n,eB as a}from"./houseplan-card-BYwTiVnu.js";function c(a,c){return new e([{code:"en",dictionary:a},{code:"ru",loadDictionary:c.ru},{code:"de",loadDictionary:c.de},{code:"fr",loadDictionary:c.fr}],"a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e",console.warn,n)}function o(e){return function(e,n){return{state:a=>{const c=e.state(a);return"pending"!==c&&n.some(e=>"pending"===e.state(a))?"pending":c},dictionary:n=>e.dictionary(n),ensure:a=>Promise.all([e,...n].map(e=>e.ensure(a))).then(()=>{})}}(a,e)}export{c 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
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__="51ccdd078fe238a1106ad440cec30c06bbfe3a298117941ce3746a5d2aed90ce";import{c as a,A as e}from"./houseplan-card-BVZ7SgFj.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-3QYyyR8a.js";import"./form-kit.styles-Ck5TDLDh.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__="a7aa4b718d3f5657591f9cf866d227c415e1f5f46fbc0b11ddce80a6845cf34e";import{c as a,A as e}from"./houseplan-card-BYwTiVnu.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-B0Dq0vzN.js";import"./form-kit.styles-v5DlO12L.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"

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