Compare commits

...
714 Commits
Author SHA1 Message Date
Matysh d0c13bc555 Release v1.79.0-beta.2 candidate
Publish the 58 integrated S8 issues since beta.1. Synchronize versions,
rebuild committed frontend assets, refresh ratchets to measured facts,
and update release metadata. All 11 documentation screenshots were
accepted as pixel-identical in WSL, without replacing PNG bytes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Issue: #742
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 10:13:50 +00:00
claude[bot] 88985d2e4c docs(reviews): индекс после сдвига каталога (#739)
Issue: #739
User-Visible: no
2026-10-01 10:06:38 +00:00
claude[bot] c629c9c238 docs: review document for #739
Issue: #739
User-Visible: no
2026-10-01 10:06:38 +00:00
Claudeandclaude[bot] 9324d81b5f perf(iso): keep the theme paper across floor switches in 2.5D (#739)
In 2.5D with a backdrop image every floor switch rendered the card twice
before the first frame. The paper key of the first-frame state (#654)
held the space id, so each switch cleared the ready paper: the first
render inserted the loading veil, updated() probed the computed card
background with a temporary span and asked for a second full update,
which removed the veil again. The colour itself never changed: it is the
theme card background, and no space sets those variables. Locally this
second pass was about 50 ms per warm switch on the large house.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Issue: #718
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:25:21 +00:00
Claudeandclaude[bot] c2ca6a55d2 docs(screenshots): source fingerprint for #718 — all frames pixel-identical
`npm run docs:accept -- --identical` (no prior docs:capture): the local
capture of all 11 documentation frames decoded pixel-identical to the
committed PNGs, so only the source fingerprint moves. None of the frames
shows the General settings dialog (the new moon status line) or a static
background at night (the new moon sky layer); the PNGs stay byte-for-byte.

Issue: #718
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:25:21 +00:00
Claudeandclaude[bot] 1c49544831 test(golden): accept the 2 reviewed static-background moon frames (#718)
The golden-images artifact of Validate run 36790529482 (16616f09, Linux,
Chromium 151.0.7922.34) reports exactly the two new #718 scenes as
missing-baseline and every other scene as passed:

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

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

Issue: #718
User-Visible: no
Release: v1.79.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36790529482
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:25:21 +00:00
Claudeandclaude[bot] b998b0b34a feat(moon): the moon with any background, and its status in General settings (#718)
The owner decided on 30.09 that the moon is not part of the "Follow the Sun"
environment but a switch of its own: with a static background (global or a
space's own) the card showed no moon even with the switch on, and the switch
said nothing about why the moon was missing right now.

With a static background there is no environment, so the moon stands in its
own layer, `.hp-moon-sky`: the first child of `.stage` / `.hp-static-stage`,
the whole scene, no z-index, filter or will-change, under the plan by DOM
order, fading with the #101 View weight. Inside is the very #661 element, so
place, size, art and fades are unchanged, and a background switch moves it to
its new parent in the same render without a flicker. The phase comes from the
same `resolveDayCycle`, computed only while the moon is on and on View; without
`sun.sun` both cards keep their 30 s clock ticker and re-render only when the
phase changes (the environment is still compared by its whole fingerprint).

General settings get a second caption line under the moon switch
(`data-moon-status`): one snapshot per opening, judged by the lazy chunk as if
the switch were on, first reason wins (no home, day, below 3°, under 3 %),
numbers rounded and clamped below the threshold they missed. `moonStatus`
decides "shown" with the same `moonShownAt` as the element. It lives in a
WeakMap beside the draft, so it never makes the dialog dirty; a closed
opening's result is dropped. The dialog loads the chunk through the gate's
loader (`withMoon`), now shared by every caller while a load is in flight, so
there is still one fingerprint check and one retry token.

Bundle (same build, against origin/dev): initial View 300 072 -> 300 248 B gzip
(+176 B, under the 500 B of the spec; budget and ceiling not raised); lazy
editor 238 558 -> 238 991 B (+433 B, the line and English strings); lazy moon
11 385 -> 11 712 B (+327 B, layer CSS and status). `src/moon.ts` stays out of
the initial and the editor graph; bundle-budget now refuses an editor/moon
overlap. Monolith metrics: hostRefs 4 885 -> 4 888 — the three `host.` reads of
`src/editors/moon-status.ts` (hass, `_settingsDialog`, requestUpdate) through
its own three-member interface, not the editor port; the other five metrics
are unchanged. houseplan-editor-runtime.ts grows by two lines (import, call).

Tests: AC9/AC10/AC15 and the sky layer in test/moon.test.mjs (the #661
"static -> nothing" check inverted), AC14 and the opening lifecycle in
test/moon-settings.test.mjs, smokes demo/smoke_moon_static.mjs (AC1-AC6; AC1
and AC3 were red on dev) and demo/smoke_moon_status.mjs (AC11/AC12), AC7 in
smoke_daycycle_layer_budget. Golden: two new scenes
(static-bg-moon-gibbous-white-light, static-bg-moon-crescent-south-dark,
matrix v70), the harness checks the moon's parent by background and waits for
the status line in the General settings frames. Four new mutants; the clock
ticker one is a browser guard (201 at the guideline of 200).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Why the fill appears later is out of scope.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

No day/night frame moved.

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

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

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

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

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

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

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

Issue: #689
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 17:51:06 +03:00
claude[bot] c1e05e465b docs(reviews): индекс после сдвига каталога (#691)
Issue: #691
User-Visible: no
2026-09-28 14:32:55 +00:00
claude[bot] 0a09712bc5 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 14:32:51 +00:00
Matysh 33ae14a99d test(touch): align kiosk smoke and Russian guide (#691)
Issue: #691
User-Visible: no
2026-09-28 17:17:08 +03:00
claude[bot] 09f8cf4c11 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 14:14:26 +00:00
Matysh 53503e0373 chore(metrics): accept touch controller bundle growth (#691)
Issue: #691
User-Visible: no
2026-09-28 16:33:33 +03:00
Matysh 37fbb9824d fix(touch): separate pan, edge swipe and double fit (#691)
Issue: #691
User-Visible: yes
2026-09-28 16:27:45 +03:00
claude[bot] 3f17a1497d docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 12:56:39 +00:00
claude[bot] 20f17fee54 docs: review document for #691
Issue: #691
User-Visible: no
2026-09-28 12:51:01 +00:00
claude[bot] 1751184b12 docs: review document for #689
Issue: #689
User-Visible: no
2026-09-28 12:30:23 +00:00
claude[bot] 63c24784b6 docs(reviews): индекс после сдвига каталога (#688)
Issue: #688
User-Visible: no
2026-09-28 12:21:14 +00:00
claude[bot] 8207a175d6 docs: review document for #688
Issue: #688
User-Visible: no
2026-09-28 12:21:12 +00:00
Matysh c04d0778ae fix(stairs): use one physical line weight (#688)
Issue: #688
User-Visible: yes
2026-09-28 14:58:33 +03:00
claude[bot] e44fbd6c9e docs: review document for #688
Issue: #688
User-Visible: no
2026-09-28 11:48:48 +00:00
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[bot] 0dac3488fb docs: review document for #682
Issue: #682
User-Visible: no
2026-09-27 22:30:07 +00:00
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00
claude[bot] 65b3cb25f7 docs: review document for #682
Issue: #682
User-Visible: no
2026-09-27 22:10:47 +00:00
Claudeandclaude[bot] cca9bc856b docs(reviews): документы ревью линий до v1.77.0 — в legacy/reviews (#682)
Волна 5 эпика #674, перенос документов ревью (класс C), сделан
инструментом: `node scripts/reviews-archive.mjs --through=v1.77.0 --apply`.
965 документов 332 задач разложены по `legacy/reviews/<тег>/` (v1.63.0 …
v1.77.0) — 330 задач доказаны трейлерами своей линии, два документа без
трейлера (#68, #94, работа до правила трейлеров) — по линии, где их
добавили. В `docs/reviews/` остались 154 документа задач с трейлером после
v1.77.0 — текущая линия v1.78.0 — и INDEX.md, пересобранный тем же
построителем. История не переписана: SHA-якоря «Материала раунда» живы.
Счёт раундов (`reviewRounds` по живому каталогу и архиву) совпадает до и
после переноса: 694 пары «этап:задача». Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00
Claudeandclaude[bot] d4a672c715 feat(tools): архив документов ревью выпущенных линий (#682)
Волна 5 эпика #674, инструментальная часть (класс B).
`scripts/reviews-archive.mjs --through=vX.Y.Z` печатает план переноса
документов ревью в `legacy/reviews/<тег>/`, `--apply` делает `git mv` и
пересобирает `docs/reviews/INDEX.md`. Членство — трейлеры `Issue: #NN` в
диапазоне линии, как у манифеста беты (#547) и ревью линии (#638). Правила —
в чистой `archivePlan`: задача уходит в последнюю свою линию; задача с
трейлером после тега остаётся целиком (её раунды ссылаются на прошлые);
закрытая без выпуска уходит с линией, где лёг её документ; документ задачи
без трейлера — с линией, где его добавили; RELEASE-REVIEW — в каталог
своего тега; чужие имена не трогаются. План по v1.77.0: 965 документов
332 задач, 154 остаются в открытой линии.

`legacy/` — класс C в process-gate. Сравнения деревьев с якорем вердикта
(`review-doc-guard.mjs` #499, `task-packet.mjs`) не видят переноса в
`legacy/reviews/`. `process-metrics.mjs` считает раунды по живому каталогу и
архиву. Порог «>900 документов» в тесте индекса снят: в каталоге остаётся
текущая линия. PROCESS.md §2.10 уточнён (правила членства, пустая очередь
S7, ревью линии до переноса), в DEVELOPMENT › Release — шаг чеклиста.
Юнит-тесты и два мутанта (`reviews-archive-moves-open-line-issue`,
`reviews-archive-first-line-wins`).

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00
claude[bot] c64a1ab7d5 docs(reviews): индекс после сдвига каталога (#683)
Issue: #683
User-Visible: no
2026-09-27 22:03:08 +00:00
claude[bot] d460b31120 docs: review document for #683
Issue: #683
User-Visible: no
2026-09-27 22:03:05 +00:00
Matysh f880db0338 docs: обновить fingerprint после golden-фикстуры (#683)
Все 11 документационных кадров попиксельно совпали; изменён только отпечаток исходников.

Issue: #683
User-Visible: no
2026-09-28 00:02:16 +03:00
Matysh 2f86266f60 chore(golden): принять пользовательский стиль лестницы (#683)
Каноническая WSL-съёмка изменила только четыре объявленные сцены лестниц; остальные 180 сцен совпали.

Issue: #683
User-Visible: no
Release: v1.78.0-beta.6
Baseline-Reviewed-Local: sha256:a56c54ffef7cd397d89f9eb895937c9af4c3d053d1289ee5c2cf7e9ecb8dd42b
2026-09-27 23:59:01 +03:00
Sergey Matyunin d5f3e17f24 test(golden): показать пользовательский стиль лестницы (#683)
Issue: #683
User-Visible: no
2026-09-27 23:52:34 +03:00
claude[bot] dd365cff45 docs: review document for #683
Issue: #683
User-Visible: no
2026-09-27 20:51:06 +00:00
Matysh 20d2863eea docs: обновить fingerprint скриншотов для лестниц (#683)
Все 11 документационных кадров попиксельно совпали; изменён только отпечаток исходников.

Issue: #683
User-Visible: no
2026-09-27 23:14:02 +03:00
Matysh 2c9ff32dc6 chore(golden): обновить аттестацию после ребейза (#683)
Все 184 сцены совпали пиксель-в-пиксель; обновлён только паспорт актуального дерева после переноса на dev.

Issue: #683
User-Visible: no
Release: v1.78.0-beta.6
Baseline-Reviewed-Local: sha256:0fa156017baaeb4bc9c28de509d90ae04cf9405750a95f866fdc4d06df3c8585
2026-09-27 23:10:49 +03:00
Sergey Matyunin 1a7b1dfceb fix: синхронизировать манифест схемы лестниц (#683)
Issue: #683
User-Visible: no
2026-09-27 23:04:28 +03:00
MatyshandSergey Matyunin 64b544f168 chore(golden): принять четыре сцены лестниц (#683)
Каноническая WSL-съёмка изменила только четыре объявленные сцены лестниц; остальные 180 сцен совпали.

Issue: #683
User-Visible: no
Release: v1.78.0-beta.6
Baseline-Reviewed-Local: sha256:d6b6c013e9be0dda05a7a1615b48dcfb0d5d0a8312415df2e0e13aef47f281bc
2026-09-27 23:04:28 +03:00
Sergey Matyunin 5b5b808184 feat: настроить визуальный стиль лестниц (#683)
Issue: #683
User-Visible: yes
2026-09-27 23:04:28 +03:00
claude[bot]andSergey Matyunin cf01489fa9 docs: review document for #683
Issue: #683
User-Visible: no
2026-09-27 23:04:09 +03:00
claude[bot] dd3a50426d docs(reviews): индекс после сдвига каталога (#680)
Issue: #680
User-Visible: no
2026-09-27 20:01:55 +00:00
claude[bot] bfcd9f1f15 docs: review document for #680
Issue: #680
User-Visible: no
2026-09-27 20:01:52 +00:00
Claude 72ce15bfa1 docs(hygiene): таблица владельцев файлов — в ARCHITECTURE, не во входе (#680)
Предыдущий коммит перенёс таблицу классификации файлов при сборке
(HP-1465-01) из ARCHITECTURE в SCOPE.md › Standing rule, а SCOPE — первый файл
обоих маршрутов входа: +211 слов маршрута автора и ревьюера — ровно перенос,
которого задача не допускает. Правило остаётся в SCOPE, таблица — в
ARCHITECTURE › Integration WS API › Files. entry-cost: автор 5 196, ревьюер
4 074.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:35:02 +03:00
Claude fdde46541e test(mutation): мутант бюджета входа автора превышает бюджет сам (#680)
После волны 3 маршрут автора — 5,4 тыс. слов из 12 тыс.: мутант
`entry-cost-author-route-over-budget` добавлял 2 000 слов и оставался
зелёным. Теперь патч сам больше бюджета (12 001 слово), и свидетель краснеет
независимо от длины остального маршрута.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:04 +03:00
Claude 4bc3e9baa6 docs(hygiene): ARCHITECTURE.md — карта, а не хроника (#680)
Волна 3 эпика #674. ARCHITECTURE.md 2330 → 653 строки: система координат
прототипа 1489×1053, разделы Hidden Isometric Stage 2/4 (текущее — ISOMETRIC.md,
история — ADR 122/570), хроники «Additions v1.28–v1.41» и «Audit follow-ups»
сняты; подсистемы — короткое описание со ссылкой на канонический документ.
Устаревшее исправлено по коду: дерево Layout (store.py, frontend_registration,
весь список модулей), хранилища (config, layout, virtual_lights, trails),
ленивые локали de и fr, таблица WS API (31 + 3 команды, #256-проекция,
space/delete, files/cleanup без keep, контент через /api/houseplan/content),
DevItem без несуществующих полей, отказ help/feedback по support_api.

Всё ещё верное и не записанное в другом месте перенесено в канонические
документы: DEVICE-PRESENTATION (заметки реализации, Action authority, черновик
диалога), RADAR (карта реализации), VACUUM (владение кодом), CANVAS (icon_size,
--hp-cell-visual-scale, барьер записи координат), FILTERING (#44, каталог),
DECOR-EDITOR §7 (инварианты бэкенда ассетов), WALL-THICKNESS (§1 идентичность
и нулевые стены, §2 кэши, §4 hover/туннели/острова, §6 удаление комнаты,
§9 failed-core, §11 завершение цепочки и комната по грани), ISOMETRIC
(isoPlaneMatrix, iso-overlays, створки, служебные атрибуты), LIGHT (Glow над
заливкой #55, формула и screen-смешение), CONFIG-COMPATIBILITY (манифест схемы
#33, Masonry-слоты #561, квадратный холст v1.48, устаревшие URL контента,
layout/set #356), SCOPE (таблица владельцев при сборке файлов), WARM-REMOUNT
§5 (холодная загрузка и визуальная непрерывность), UX-MODES (порядок
стартового пространства), PDF-EXPORT (граница реализации), FURNITURE (adopt,
BOOT_MAX_MS). Раздел TESTING «Backend quality gates (#42)» и строка
DEVELOPMENT о bundle-freshness — из того же разбора, в предыдущем коммите.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:04 +03:00
Claude 696f5a789f docs(hygiene): сократить вход агента, у правила — один дом (#680)
Волна 3 эпика #674. AGENTS.md 650 → 187 строк: карта пакета, маршрут чтения,
правило №1, классы и треки одной строкой со ссылками, трейлеры, рабочие
деревья, хендофф и ожидание вердикта; пересказы PROCESS.md — ссылками на
разделы. Неверный список «Gate jobs» снят (списки jobs не копируются в прозу,
шапка PROCESS.md). Правила, жившие только в AGENTS, получили дом: жёлтый
вердикт при выполненных AC — PROCESS §2.7; свежесть бандла, съёмка только в
Linux (#455, HP_ALLOW_FOREIGN_CAPTURE) и смоки из AC до S7 (#151) —
TESTING.md; причуда демо-стенда и среда-зависимый smoke_opening_measure —
DEVELOPMENT › Smoke tests; отказ публикации без `Release:` и при несвежем
отпечатке бандла, отмена Validate новым пушем, кандидат беты не
promotion-only, fail-closed реестра Labs — DEVELOPMENT; предупреждение и
ошибка свежести скриншотов — CONTRIBUTING.

PROCESS.md: §13 (внедрение с открытым ⏳), §14 (блок со ссылкой на
несуществующий docs/PROCESS.md) и §7.3 (история) удалены. Ссылки «§7.2» на
правило полного разбора после ребейза ведут в §2.10, на сверку SHA перед
выводом — в §2.7; то же в сообщениях scripts/branch-state.mjs,
merge-candidate.mjs, review-doc-guard.mjs, pre-push-gate.mjs, в промпте
_process.yml и TESTING.md. Число `any` в прозе → `node scripts/no-new-any.mjs
--total` (новый режим, юнит-тест; было «1034 в 49 файлах», сейчас 862 в 52),
дата-число замороженного списка якорей монолита снято. Устаревшая команда
пересъёмки скриншотов в §8 заменена ссылкой на действующий путь.

STATUS.md 113 → 61 строка: сгенерированный снимок, текущий цикл и девять
строк решений; Workflow, CI, Toolchain, Tests, Scope, open items и политика
документации — ссылками (PROCESS §2.6, DEVELOPMENT › Release, TESTING);
локали en/ru/de/fr; закрытые «coverage, mypy strict» сняты.

DEVELOPMENT.md: file-sync и «Reproducible scripts» (прототип) удалены;
раздел Release — единственный дом релизной механики: введение, правила
тела стабильного релиза (#328, release:notes), шаг continuity:screencast,
источники версии по release-contract. CONTRIBUTING: ссылка на Release вместо
пересказа, замеры клона без чисел. TESTING: any-гейт — ссылкой на PROCESS §8.

entry-cost: автор 11 125 → 5 407 слов, ревьюер 8 464 → 4 285.

Issue: #680
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:33:03 +03:00
claude[bot] 972ff701d1 docs(reviews): индекс после сдвига каталога (#681)
Issue: #681
User-Visible: no
2026-09-27 19:11:40 +00:00
claude[bot] a57ba2e264 docs: review document for #681
Issue: #681
User-Visible: no
2026-09-27 19:11:36 +00:00
Claude 708ed850f3 docs(hygiene): снять чек-листы testing-notes и дизайн-материалы выпущенных задач (#681)
Волна 4 эпика #674.

docs/testing-notes/: восемь ручных чек-листов по поверхностям и индекс
удалены — ни одного отмеченного пункта, ручной фазы в PROCESS.md §2 нет.
Правило #650 (пустое совпадение --test-name-pattern) перенесено в
TESTING.md; пункты [manual] без автоматического свидетеля сведены в раздел
«Чего не проверяет автоматика» (реальный HA, сенсорное устройство, ресурсы
сервера, несколько клиентов, визуальная оценка, пользовательское
содержимое). В TESTING.md снят блок чек-листов v1.43–1.44 и приложения по
issue в разделе golden (#197/#249/#272/#275/#288/#261) — сцены объявлены в
demo/golden/matrix.mjs. Остался реестр браузерных гвардов #659
(mutation-browser-guards.md). test/testing-notes-index.test.mjs →
test/testing-doc.test.mjs: лимит 800 строк, правила #85, раздел ручных
проверок и живые ссылки TESTING.md; каталог testing-notes содержит только
реестр. Мутант testing-notes-index-drops-section (удалял строку индекса) →
testing-doc-drops-manual-section. golden-matrix: копия 67 id сцен #242/#250
в чек-листе снята, список и способ его измерения — в demo/golden/matrix.mjs.

docs/design/505-summary-panel удалён вместе с
demo/capture_summary_panel_505.mjs и маршрутом /reference/ фикстуры
диалога. docs/design/600-settings-dialogs: reference/, screenshots/,
pairs/, ARCHIVE-README, ISSUE-FORM, OPEN-POINTS удалены; SPEC,
IMPLEMENTATION-GUIDE, field-maps, ACCEPTANCE остаются; вывод
capture_design_pairs_600.mjs и verify_ha_form_shell_609.mjs --capture —
в artifacts/. docs/design: 68 файлов / 4,08 МБ → 13 / 0,70 МБ.
README-ha-dialog-505.md → README-ha-dialog.md (путь в release-review.yml);
demo/guard/README.md: запись гварда — в verify-guard.mjs, не в README.

docs/design/649-25d-stage6 не тронут — пункт после стабильного v1.78.0.

Issue: #681
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 21:27:57 +03:00
claude[bot] c8fc8838d8 docs(reviews): индекс после сдвига каталога (#679)
Проверка (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: #679
User-Visible: no
2026-09-27 18:23:50 +00:00
claude[bot] f4fc3bd680 docs: review document for #679
Issue: #679
User-Visible: no
2026-09-27 18:23:47 +00:00
Claudeandclaude[bot] 030780386c docs(hygiene): назвать тест сверки схемы в реестре совместимости (#679)
Правка шапки CONFIG-COMPATIBILITY.md в предыдущем коммите сослалась на
«тесты #33» без имени — ровно та форма, которую эпик #674 снимает у
матрицы света. Теперь названы файл и что он держит: согласие enum бэкенда и
фронтенда через allow-list и разрешимость каждой записи реестра в путь
манифеста схемы.

Issue: #679
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 18:07:37 +00:00
Claudeandclaude[bot] 5a258f3128 docs(hygiene): свести дубли документов подсистем и снять устаревшее (#679)
Волна 2 эпика #674 — у каждого правила один дом, остальные места ссылаются.

DECOR-EDITOR.md ← BACKDROP.md + LIVE-TEXT.md: один документ с нумерованными
разделами (§3 подложка, §5 текст с живыми значениями), на которые теперь
указывают комментарии кода вместо несуществовавших «BACKDROP §2/§3»;
исправлено утверждение, что space-card не рисует декор (он рисует подложку
и картинки декора, но не фигуры, мебель и текст). LIGHT.md ← матрица
настроек света (перевод, тест назван явно: test/devices.test.mjs «issues
84/88»). DEVICE-PRESENTATION.md ← правила «что показывает маркер» из
FILTERING.md (порядок cover → light sources → device role, шторы,
медиаплееры); «в одном pull request» → «в одном коммите». CANVAS.md: §9.5
«Оптимизировать планы» → CONFIG-COMPATIBILITY.md, overlay и планарные грани
Walls → WALL-THICKNESS.md §10–11, таблицы «было/стало» сняты. TESTING-DEMO.md
→ demo/stand/README.md: карта демо-дома и «чего на стенде нет», ручной
чек-лист снят (ручной фазы в процессе нет). ISOMETRIC.md — только текущее;
история Stage 2/4 — docs/adr/570-isometric-stage4-visual-handoff.md.
SUN.md: удалённый контракт фона снят, правило бумаги — в текущем разделе.
UX-MODES.md: декор над заливками, а не «под комнатами»; «hidden isometric»;
follow-up из #3 — все выпущены. Шапки VACUUM, WARM-REMOUNT («Выровнять всё
по сетке» → «Оптимизировать планы»), WALL-THICKNESS, STYLING-HOOKS,
CONFIG-COMPATIBILITY (#33), PDF-EXPORT — без устаревших статусов и планов.
README EN/RU: абзац про пересъёмку скриншотов → CONTRIBUTING.md, RADAR и
PDF-EXPORT в списке документации, RU догнал EN (2.5D, повторное
использование загруженного изображения, STAIRS). Один список канонических
документов подсистем в AGENTS.md и промпте ревьюера (_process.yml).
WALL-THICKNESS.md ссылается на ADR 282.

Правки src/** и validation.py — только пути документов в комментариях.

Issue: #679
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 18:07:37 +00:00
Sergey Matyunin f989fbfe3d docs: refresh screenshot fingerprint for v1.78.0-beta.6
Issue: #655
Issue: #658
Issue: #659
Issue: #668
Issue: #676
Issue: #677
Issue: #678
User-Visible: no
2026-09-27 21:04:25 +03:00
Sergey Matyunin 15ef7d9700 Release v1.78.0-beta.6 candidate
Issue: #655
Issue: #658
Issue: #659
Issue: #668
Issue: #676
Issue: #677
Issue: #678
User-Visible: yes
Release: v1.78.0-beta.6
2026-09-27 21:02:09 +03:00
claude[bot] 58211d8f64 docs(reviews): индекс после сдвига каталога (#677)
Issue: #677
User-Visible: no
2026-09-27 16:39:07 +00:00
claude[bot] bd949a810f docs: review document for #677
Issue: #677
User-Visible: no
2026-09-27 16:39:06 +00:00
Claudeandclaude[bot] 174ececb28 docs(security): убрать доступы к стенду и домашнему HA из публичных документов (#677)
Волна 0 эпика #674. В публичном дереве лежали логин и пароль закрытого
dev-стенда (TESTING-DEMO.md), карта доступа к домашнему инстансу и стенду —
хост, порт SSH, имена ключей, IP, пути конфигурации, место хранения PAT
(STATUS.md, DEVELOPMENT.md).

TESTING-DEMO.md: строка про dev-стенд без учётных данных. STATUS.md: строка
«Home instance» удалена целиком, из строк «GitHub» и «Demo stand» убраны
ключ, PAT, хост и заметка памяти — публичные адреса и описание стенда
остались. DEVELOPMENT.md: разделы «Deployment to the dacha» (ещё и
противоречил PROCESS.md §12 — ручное копирование запрещено) и «Production
objects in HA» удалены; вместо первого — три строки «Deployment» о HACS по
тегу; раздел «Environment (cowork sessions)» удалён вместе с ними — три его
пункта из шести называли тот же ключ, PAT и хост (в эпике он значился за
волной 3).

Строки остаются в истории git, поэтому пароль стенда меняет владелец —
отдельным подтверждением в задаче.

Issue: #677
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 16:39:06 +00:00
claude[bot] 614310a1e7 docs(reviews): индекс после сдвига каталога (#678)
Issue: #678
User-Visible: no
2026-09-27 16:34:42 +00:00
claude[bot] 9a1d922b24 docs: review document for #678
Issue: #678
User-Visible: no
2026-09-27 16:34:40 +00:00
Claudeandclaude[bot] c081f59a15 chore(hygiene): убрать мёртвые скрипты и старый архив, разовые документы — в legacy/ (#678)
Волна 1 эпика #674 — мёртвое без риска, каждое имя проверено git grep по
текущему dev.

Удалено: шесть demo/shot_*.mjs без читателей (shot_furniture ещё и не
работает с #159), demo/capture_wall_strip_backup.mjs,
demo/benchmark_coordinate_write_barrier.mjs (нигде не запускался, но входил
в манифест smoke через demo/benchmark_*.mjs — теперь лист покрытия чист),
scripts/dev/styles-split.mjs (падает на текущем src/styles.ts),
docs/README.ru.md (индекс четырёх документов из 38 — роль у README.ru.md),
demo/README.md (одна строка в карте пакета AGENTS.md вместо него) и из
legacy/ — снимок аудита v1.58.0, черновик 089, завершённые планы,
продуктовые снимки и две разовые диагностики; история git хранит.
legacy/docs/SUN-CONTRAST.md остаётся: на него ссылаются src/sun.ts и SUN.md.

Перенесено в legacy/docs/: superpowers/specs (11 дизайн-документов до
процесса), QUALITY-560.md, ROADMAP.md, STATUS-FEATURES.md. Ссылки
поправлены там, где они были: FILTERING.md, ТЗ 006/007/058, testing-notes,
комментарии src/open-spans.ts и validation.py (только путь документа),
SCOPE.md, STATUS.md, smoke_household_journeys.mjs, опись legacy/README.md;
ложный маркер benchmark_coordinate_write_barrier снят с чек-листа.

Issue: #678
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 16:23:39 +00:00
claude[bot] 16cf433164 docs(reviews): индекс после сдвига каталога (#676)
Issue: #676
User-Visible: no
2026-09-27 16:20:23 +00:00
claude[bot] e25702c8d6 docs: review document for #676
Issue: #676
User-Visible: no
2026-09-27 16:20:19 +00:00
Claude dd905888cb test(mutation): свидетели жестов и подсказки лестниц — без браузера (#676)
#659 закрыл реестр браузерных гвардов ровно на 200 и проверяет число
точно: два новых смок-свидетеля #676 подняли его до 202, и юниты на
ребейзнутом кандидате покраснели («browser guard inventory is reviewed,
capped and exact»).

Оба инварианта — проводка Lit-шаблонов, которую без браузера не
импортировать, но можно засвидетельствовать по исходнику, как #659 сделал
для прежних трёх лестничных мутантов: подавление синтезированного click
после жеста (`_suppressClick` на один тик в конце протяжки и жеста узла,
`@click=${stop}` на обоих видах узлов) и условие подсказки Просмотра
(`if (!active) return;` — тот же признак, что делает лестницу ссылкой).
Мутанты переведены в группу `unitGuard('test/stairs-box.test.mjs')`,
свидетель — новый тест в `mutation-browser-offload.test.mjs`; поведение в
Chromium по-прежнему держит `demo/smoke_stairs.mjs` (AC6, AC8).

Issue: #676
User-Visible: no
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 17:54:21 +03:00
Claudeandclaude[bot] ab89187f4f test(bundle): полосу потолка по поставляемому манифесту судить только у свежего бандла (#676)
С #657 закоммиченный бандл меняет только кандидат, поэтому между
кандидатами dist/houseplan-assets.json законно отстаёт от исходников — и от
потолков, которые задача поднимает в том же коммите, что и свой граф.
Четыре теста (#438 ×2, #593, #627 AC1) сравнивали отстающий манифест с
новым потолком: после #663 бандл беты.4 лежал ниже поднятой полосы
редактора, после #676 — бандл беты.5 (235 435 против полосы 237 000…
239 000), и на чистом чекауте до кандидата они краснели, а в мутантном
worktree CI (git worktree без сборки) роняли «чистый прогон» шарда 2/6
(run 36324644336).

Теперь поставляемые графы сравниваются с потолками только когда отпечаток
манифеста равен отпечатку исходников — у кандидата и после сборки в
Validate/gate:small; отстающий бандл даёт диагностику #657 вместо ложного
красного. Синтетические проверки обеих сторон полосы от свежести не
зависят, поэтому мутанты потолков ловятся по-прежнему.

Issue: #676
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:41:19 +00:00
Claudeandclaude[bot] 69e0345122 chore(golden): принять рамку выделенной лестницы (#676)
Linux CI 36320878644 на 193d7b0d подтвердил ровно одну ожидаемую сцену:
stairs-flat-selected-light — рамка узлов лестницы в верхнем оверлее с
бусинами сторон и вынесенным узлом вращения вместо прежних узлов под
стенами. Остальные 183 сцены, включая три лестничные, без изменений.
Отпечаток исходников после ребейза на dev тот же (#655 — только бэкенд),
поэтому артефакт того прогона принят как есть.

Issue: #676
User-Visible: no
Release: v1.78.0-beta.6
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36320878644
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 14:41:19 +00:00
Claudeandclaude[bot] 89e8aecbc1 refactor: вынести box-модель лестниц в ленивый модуль stairs-box.ts (#676)
Просмотр импортирует stairTargetState из stairs-editor-model.ts, и общий
модуль Rollup кладёт в eager-чанк: новые функции протяжки, ресайза, магнита
и диалога утянули туда 2,5 КБ gzip и выбили бюджет первого View. Теперь
eager-модуль снова маленький, а всё редакторское живёт в stairs-box.ts,
который импортирует только ленивый редактор: initial View 299 165 Б при
бюджете 301 066, запас 1 901 Б.

Потолок ленивого редактора 236 400 → 239 000 (замер 237 985) с записью
истории в bundle-budget.mjs; база монолита bundleBytes поднята тем же
коммитом (+10 069 Б сырых — код слоя редактирования). Хук data-hp получил
запись stair-frame; смок не пишет в приватное поле.

Issue: #676
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:41:18 +00:00
Claudeandclaude[bot] 93051edfc2 fix: переписать слой редактирования лестниц на box-контракт декора (#676)
Лестница рисуется протяжкой, как прямоугольник или эллипс декора: длинная
ось протяжки — ось подъёма, стрелка смотрит от нажатия к отпусканию, клик
ставит размер по умолчанию. Выделенная лестница получает рамку декора в
верхнем оверлее карточки — выше тел стен, с узлами постоянного экранного
размера и курсорами по мировому направлению узла; винтовая — четыре узла на
касательных. Ресайз идёт от противоположной стороны, не зеркалит и не
поворачивает; магнит работает по сторонам: в пределах 6 клеток и 5° сторона
ложится на видимую грань стены, при перемещении лестница доворачивается не
больше чем на 5°, при ресайзе и черновике — никогда. Грани перегородок и
колонн получают внешнюю сторону по обходу многоугольника.

Клик, синтезированный после жеста, глушится: под «Лестницей» больше не
появляется копия, под «Выбором» не пропадает выделение. Диалог свойств
считает в сантиметрах лестницы (30…10000), а не в конвертере толщины стены;
нетронутое поле хранит число бит в бит, «Сохранить» без изменений ничего не
пишет. В Просмотре наведение на лестницу с корректной целью показывает
«Переход на этаж …» — ровно при том же условии, что делает её ссылкой.

Чистые функции слоя — в stairs-editor-model.ts с юнитами; смок лестниц
покрывает протяжку, курсоры, рамку над стенами, клик после жеста, диалог и
пять состояний подсказки; реестр мутантов получил шесть свидетелей.

Issue: #676
User-Visible: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 14:41:18 +00:00
claude[bot] 062af8f1d9 docs(reviews): индекс после сдвига каталога (#659)
Issue: #659
User-Visible: no
2026-09-27 14:27:22 +00:00
claude[bot] 08b1c4820b docs: review document for #659
Issue: #659
User-Visible: no
2026-09-27 14:27:19 +00:00
Sergey Matyunin dd08aed99e docs(testing): record mutation guard timing evidence (#659)
Issue: #659
User-Visible: no
2026-09-27 17:08:07 +03:00
claude[bot]andSergey Matyunin cc71918126 docs: review document for #659
Issue: #659
User-Visible: no
2026-09-27 17:08:06 +03:00
Sergey Matyunin 718afca221 test(mutation): сократить browser guards реестра (#659)
Issue: #659
User-Visible: no
2026-09-27 17:08:06 +03:00
claude[bot] b9aa2b54b1 docs(reviews): индекс после сдвига каталога (#655)
Issue: #655
User-Visible: no
2026-09-27 13:05:37 +00:00
claude[bot] 196be82d94 docs: review document for #655
Issue: #655
User-Visible: no
2026-09-27 13:05:34 +00:00
Sergey Matyunin f340157f59 test(backend): prove concurrent shutdown flush is idempotent (#655)
Issue: #655
User-Visible: no
2026-09-27 15:47:13 +03:00
claude[bot] 7acf5c74ad docs: review document for #655
Issue: #655
User-Visible: no
2026-09-27 12:43:54 +00:00
Sergey Matyunin 075a2d3fb1 fix(backend): flush deferred state on HA shutdown (#655)
Issue: #655
User-Visible: yes
2026-09-27 15:17:44 +03:00
claude[bot] 7a4032ba26 docs: review document for #655
Issue: #655
User-Visible: no
2026-09-27 12:07:59 +00:00
claude[bot] 9a7020ce42 docs: review document for #676
Issue: #676
User-Visible: no
2026-09-27 11:52:45 +00:00
claude[bot] 19edda83e3 docs: review document for #676
Issue: #676
User-Visible: no
2026-09-27 11:42:17 +00:00
claude[bot] 9fe7f88cb3 docs: review document for #676
Issue: #676
User-Visible: no
2026-09-27 11:28:14 +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[bot] f1a87fbad9 docs(reviews): индекс после сдвига каталога (#658)
Issue: #658
User-Visible: no
2026-09-27 11:13:21 +00:00
claude[bot] ac8eac6034 docs: review document for #658
Issue: #658
User-Visible: no
2026-09-27 11:13:18 +00:00
Claude bdac4b4fdc ci: закрепить образ раннера, таймауты job и некруглые cron (#658)
`ubuntu-latest` с 19.10.2026 переезжает на Ubuntu 26, а golden, скриншоты
документации и перф-бюджеты сняты на текущем образе: все 43 job на раннере
теперь явно на `ubuntu-24.04`, один образ на все workflow. 26 job получили
`timeout-minutes` по наблюдённой длительности с запасом; гейт релиза — 180,
больше суммы собственных ожиданий (60 + 60 + 45). Расписания ушли с круглых
минут (ночь 02:17, мутанты 00:43, метрики 05:23, полный перф 04:11), ночь
пишет в summary сдвиг старта и предупреждает, если он больше часа.

test/workflow-hygiene.test.mjs держит все три правила по тексту workflow
(разбор `parseJobSettings` в scripts/workflow-jobs.mjs) и исполняет шаг
сдвига старта настоящим bash; порядок осознанного подъёма образа —
docs/DEVELOPMENT.md.

Issue: #658
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 13:57:21 +03:00
claude[bot] 6fc789e4c8 docs(reviews): индекс после сдвига каталога (#668)
Проверка (CI) / Классификация изменённых файлов (push) Successful in 26s
Проверка (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 50s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 18s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 26s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 49s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 42s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Successful in 1m51s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 11m41s
Проверка (CI) / Бандл головы dev для стенда (push) Skipped
Проверка (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 36s
Issue: #668
User-Visible: no
2026-09-27 09:29:52 +00:00
claude[bot] 0a066177cf docs: review document for #668
Issue: #668
User-Visible: no
2026-09-27 09:29:49 +00:00
Sergey Matyuninandclaude[bot] 85cba3d8b4 docs: синхронизировать английское руководство (#668)
Английская версия догнана до структуры и содержания русского руководства. Структурный гейт защищает H2-H4 и маркер актуальной версии от нового рассинхрона.

Issue: #668
User-Visible: no
2026-09-27 09:18:50 +00:00
claude[bot] cbc39cea7b docs: review document for #668
Issue: #668
User-Visible: no
2026-09-27 09:18:50 +00:00
claude[bot] d24b0f0eac docs: review document for #668
Issue: #668
User-Visible: no
2026-09-27 09:18:50 +00:00
Claude 19587a797f Release v1.78.0-beta.5 candidate refresh
Пересборка кандидата на голове dev после слияния #669–#673 и #675:
бандл из текущих исходников, снимок STATUS и строка текущего цикла.

Issue: #663
Issue: #664
Issue: #665
Issue: #666
Issue: #667
Issue: #669
Issue: #670
Issue: #671
Issue: #672
Issue: #673
Issue: #675
User-Visible: no
Release: v1.78.0-beta.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 12:14:08 +03:00
claude[bot] 1ffb1c4eb3 docs(reviews): индекс после сдвига каталога (#675)
Issue: #675
User-Visible: no
2026-09-27 09:02:13 +00:00
claude[bot] b11d60eb4e docs: review document for #675
Issue: #675
User-Visible: no
2026-09-27 09:02:10 +00:00
Claude 6183119f1e ci: перекалибровать общий потолок spaceSwitchMs изометрии по ряду CI (#675)
spaceSwitchMs изометрии — одно холодное переключение этажа со сборкой
2.5D-геометрии; он растёт с каждой стадией 2.5D, и потолок 1800 мс из #89
stage 1 стал самим уровнем. Медиана perf-smoke на hosted-раннере:
1500 мс (09-09…09-12, 12 прогонов) → 1668 (09-19…09-25, 6) → 1798 после
#649 (9, σ 42, максимум 1867,7). Попытки одного SHA расходятся на 0,4 %,
разные прогоны одного SHA — до 5,5 %: больше образцов вердикт не меняют,
рычаг — потолок.

2200 мс: +17,8 % над наблюдённым максимумом и ниже единственной настоящей
регрессии окна (2719,6 мс, #583 до решётки, 09-16); удвоение уровня
краснеет с запасом. Одно число в трёх файлах: смок = полный профиль
(#473 AC4), плотный двойник = исторический (#160). Коэффициент и допуск
полного сравнения не тронуты. Обоснование — demo/performance/README.md,
тест закрепляет число и три точки ряда.

Issue: #675
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 08:50:34 +00:00
claude[bot] 7871efb0d6 docs(reviews): индекс после сдвига каталога (#673)
Issue: #673
User-Visible: no
2026-09-27 08:39:59 +00:00
claude[bot] e62dec74cd docs: review document for #673
Issue: #673
User-Visible: no
2026-09-27 08:39:55 +00:00
Matysh 2475403469 docs: обновить fingerprint после ребейза #673
После автоматического ребейза на dev все 11 кадров повторно проверены через docs:accept --identical: пиксели не изменились. Строгий docs-гейт теперь зелёный.

Issue: #673
User-Visible: no
2026-09-27 11:27:28 +03:00
claude[bot] 2aeda2dfa4 docs: review document for #673
Issue: #673
User-Visible: no
2026-09-27 08:24:17 +00:00
Matyshandclaude[bot] 855bede537 docs: обновить fingerprint скриншотов для #673
Все 11 кадров попиксельно совпали с закоммиченными (docs:accept --identical). PNG не изменялись; обновлён только отпечаток исходников.

Issue: #673
User-Visible: no
2026-09-27 07:51:54 +00:00
Matyshandclaude[bot] fc237f93be chore(golden): принять пять сцен Stage 6 (#673)
Issue: #673
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:adb377c40236510ed767a5a829dbbc88e51156e8d1f6eb206166fd3a7c61f921
2026-09-27 07:51:54 +00:00
Sergey Matyuninandclaude[bot] 15207dbacd test: добавить Stage 6 в golden-матрицу (#673)
Issue: #673
User-Visible: no
2026-09-27 07:51:54 +00:00
claude[bot] 2f857a0f6b docs(reviews): индекс после сдвига каталога (#669)
Issue: #669
User-Visible: no
2026-09-27 07:46:02 +00:00
claude[bot] 7e8605981b docs: review document for #669
Issue: #669
User-Visible: no
2026-09-27 07:45:59 +00:00
Claude 47c245b5ba perf: вычитать лестницы из площади пола только по запросу (#669)
Путь чистого пола строят четыре места рендера, а площадь читают только
подсказка комнаты и PDF. cleanFloorForRoom больше не вычитает лестницы при
построении пути: площадь считается при первом чтении и хранится в том же
закэшированном объекте. geometryMinusStairs передаёт в polyclip только
контуры лестниц, чей габарит пересекает вычитаемую геометрию, — результат
тот же, а комната на этаже с 250 лестницами больше не прогоняет все 250.

Issue: #669
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 09:48:02 +03:00
claude[bot] 2728a0ee54 docs(reviews): индекс после сдвига каталога (#670)
Issue: #670
User-Visible: no
2026-09-27 07:04:26 +00:00
claude[bot] df2455c530 docs: review document for #670
Issue: #670
User-Visible: no
2026-09-27 07:04:23 +00:00
Sergey Matyunin 9bc23f9871 fix: индексировать ревью стабильного релиза (#670)
Issue: #670
User-Visible: no
2026-09-27 09:43:51 +03:00
claude[bot] e45dcad47a docs: review document for #669
Issue: #669
User-Visible: no
2026-09-27 06:41:04 +00:00
claude[bot] 944adc65a9 docs(reviews): индекс после сдвига каталога (#672)
Issue: #672
User-Visible: no
2026-09-27 06:32:50 +00:00
claude[bot] 21555a5991 docs: review document for #672
Issue: #672
User-Visible: no
2026-09-27 06:32:49 +00:00
Sergey Matyuninandclaude[bot] 71c8a87e67 test: не считать README входами харнесса (#672)
Issue: #672
User-Visible: no
2026-09-27 06:32:49 +00:00
claude[bot] 8b775247a7 docs: review document for #669
Issue: #669
User-Visible: no
2026-09-27 06:28:44 +00:00
claude[bot] c9cd6cbf1f docs(reviews): индекс после сдвига каталога (#671)
Issue: #671
User-Visible: no
2026-09-27 06:02:37 +00:00
claude[bot] d36f79f156 docs: review document for #671
Issue: #671
User-Visible: no
2026-09-27 06:02:34 +00:00
Sergey Matyunin 05dfcd1c8a ci: guard asset inputs in Validate manifest
Issue: #671
User-Visible: no
2026-09-27 08:45:07 +03:00
Sergey Matyunin 098732512c Release v1.78.0-beta.5 candidate (rebuilt)
Проверка (CI) / Классификация изменённых файлов (push) Successful in 29s
Проверка (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 1m5s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 35s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 32s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 58s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 49s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 57s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 11m6s
Проверка (CI) / Бандл головы dev для стенда (push) Skipped
Проверка (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 20s
Issue: #663
Issue: #664
Issue: #665
Issue: #666
Issue: #667
User-Visible: yes
Release: v1.78.0-beta.5
2026-09-27 03:05:37 +03:00
claude[bot] 88946f69db docs(reviews): индекс после сдвига каталога (#663)
Issue: #663
User-Visible: no
2026-09-26 23:59:05 +00:00
claude[bot] 7464d3e16c docs: review document for #663
Issue: #663
User-Visible: no
2026-09-26 23:59:02 +00:00
Matysh f2e114d71d docs: обновить fingerprint скриншотов после лестниц (#663)
Все 11 кадров попиксельно совпали с закоммиченными (docs:accept --identical, #512). PNG не изменялись; обновлён только отпечаток исходников.

Issue: #663
User-Visible: no
2026-09-27 02:26:19 +03:00
Matysh 9755b200e7 chore(golden): принять разметку обратной лестницы (#663)
Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:d6725147cfe96217ca01c22b86bc05eb4b69dd9c73402d88b57e071adfb4e181
2026-09-27 02:22:26 +03:00
Sergey Matyunin 8852207b18 test: стабилизировать проверку угла magnet лестницы (#663)
Issue: #663
User-Visible: no
2026-09-27 02:12:56 +03:00
claude[bot] 8176f1190f docs: review document for #663
Issue: #663
User-Visible: no
2026-09-26 23:09:27 +00:00
Sergey Matyunin d64338224d fix: учесть направление ступеней и защитить совместимость (#663)
Issue: #663
User-Visible: yes
2026-09-27 01:14:46 +03:00
claude[bot] 3f8aae838f docs: review document for #663
Issue: #663
User-Visible: no
2026-09-26 22:09:33 +00:00
Sergey Matyunin 59a60d526d test: сделать проверку magnet лестницы однозначной (#663)
Issue: #663
User-Visible: no
2026-09-27 00:17:55 +03:00
Matysh ac6f4bfe2e docs: обновить отпечаток скриншотов для лестниц (#663)
Issue: #663
User-Visible: no
2026-09-26 23:59:26 +03:00
Matysh 93876d7569 chore(golden): закрепить производительный расчёт лестниц (#663)
Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:fee8da264cf23f9cab943ced0d0c4c6f21d2c568daaa6d8890a13d6011626597
2026-09-26 23:53:08 +03:00
Matysh ea1f19c352 perf: разбить вычитание лестниц по кадрам (#663)
Issue: #663
User-Visible: no
2026-09-26 23:48:01 +03:00
Matysh bcc5481557 chore(golden): закрепить финальный resize smoke лестниц (#663)
Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:d5dabad3567794d874ee57e8b233990f1d801511c5e56ff08fdf6706c71d8464
2026-09-26 23:22:38 +03:00
Matysh ce26a051e6 test: стабилизировать pointer capture resize smoke (#663)
Issue: #663
User-Visible: no
2026-09-26 23:17:48 +03:00
Matysh e18a7c3385 chore(golden): закрепить финальный SHA лестниц (#663)
Эталоны 179 сцен совпали пиксель-в-пиксель; заменено 0 PNG.
Паспорт привязан к опубликованному дереву после исправлений Validate.

Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:77197ca53de1a025863f8213f5239f8122b2b844a8b77a8d485e27caa65a3a98
2026-09-26 23:01:38 +03:00
Matysh 16ede0c027 ci: очищать dist перед распаковкой бандла (#663)
Issue: #663
User-Visible: no
2026-09-26 22:51:14 +03:00
Matysh 42e7bc24a8 chore(golden): обновить аттестацию после CI-исправления (#663)
Эталоны 179 сцен совпали пиксель-в-пиксель; обновлён только паспорт
опубликованного SHA после исправления порядка Build и Unit tests.

Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:9c691b27fbe3c232afdf6ee513b4be1832e4f15de39c886a77089b84cd545741
2026-09-26 22:43:46 +03:00
Matysh 060d9ea547 ci: собирать dist до unit-контрактов бюджета (#663)
Issue: #663
User-Visible: no
2026-09-26 22:32:36 +03:00
Matysh efdc9df0c6 chore(golden): обновить аттестацию после синхронизации dev (#663)
Эталоны 179 сцен совпали пиксель-в-пиксель; обновлён только паспорт
финального опубликованного SHA после слияния актуального dev.

Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:86c19da2c36eb003ed5a8507c6924f2524e84b2f9ce41c3416cc0c215d91c5d4
2026-09-26 22:15:37 +03:00
Matysh ce4f820a03 test(mutation): сделать свидетели лестниц неэквивалентными (#663)
Issue: #663
User-Visible: no
2026-09-26 22:08:28 +03:00
Matysh b68aa216a9 chore(golden): обновить аттестацию после синхронизации dev (#663)
Эталоны 179 сцен совпали пиксель-в-пиксель; обновлён только паспорт
финального опубликованного SHA после слияния актуального dev.

Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:d0a9d150899e2d155fa350c05ad8c1d2270573d8e83c47b32957fef4e2ab5171
2026-09-26 21:52:19 +03:00
Matysh 19d2463dcc Merge origin/dev into issue/663-stairs
Issue: #663
User-Visible: no
2026-09-26 21:45:22 +03:00
Matysh 8aa7b9295e test(ci): учитывать локальную аттестацию эталонов (#663)
Issue: #663
User-Visible: no
2026-09-26 21:42:57 +03:00
claude[bot] cf876c712a docs(reviews): индекс после сдвига каталога (#667)
Issue: #667
User-Visible: no
2026-09-26 18:36:47 +00:00
claude[bot] 2d6f1e8be3 docs: review document for #667
Issue: #667
User-Visible: no
2026-09-26 18:36:44 +00:00
Matysh 6903f3320b chore(golden): принять сцены лестниц и актуальный dev (#663)
Проверены четыре новые сцены лестниц, перекомпоновка Plan-панели и уже выпущенные изменения активной вкладки и 2.5D-подписей.

Issue: #663
User-Visible: no
Release: v1.78.0-beta.5
Baseline-Reviewed-Local: sha256:a7995a1b977c613a5cde4a1f89b8efdb82129c36010b79d2a1ff7fb8e81f2428
2026-09-26 21:32:21 +03:00
Sergey Matyunin 847ebc91a9 test(golden): отделить стресс лестниц от кадров (#663)
Performance fixture по умолчанию сохраняет максимум 250 лестниц, а визуальные large-house сцены исключают стрессовую коллекцию, уже покрытую четырьмя отдельными кадрами.

Issue: #663
User-Visible: no
2026-09-26 21:22:14 +03:00
Claude 2fe20a095b docs: свести документацию с кодом и каноном (#667)
Пятнадцать расхождений из #667. Бюджет initial View назван одним
источником (scripts/bundle-budget.mjs). CONTRIBUTING описывает бандл после
#657, HA-харнесс после #630 и релиз через release-contract и release.yml.
Мутанты указывают на реестр scripts/mutation-registry.mjs. STATUS и
ROADMAP больше не держат PR в HACS «в очереди» и инструкции прежней
песочницы. SCOPE называет три редактора, форматы плана и радар #485.
README выводят первую комнату через «Стены». Русское руководство сверено с
v1.78.0-beta.5, таблица «Источник плана» склеена. UX-MODES и STYLING-HOOKS
следуют коду: проёмы в просмотре инертны, space-card рисует проёмы и
декор-изображения. Починены якорь в DEVICE-PRESENTATION и пол зума 1/3.
ADR 089/122/160 и ISOMETRIC помечают активацию через hp_alpha исторической.
Из раскладки ARCHITECTURE убран несуществующий src/data. legacy/README не
называет docs/superpowers действующими спецификациями.

Паритет английского руководства (п. 8) выделен в #668.

Issue: #667
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-26 18:19:52 +00:00
Sergey Matyunin 8eb97bcf46 test(golden): выбрать лестницу перед снимком (#663)
Сценарий включает инструмент выбора до клика по лестнице; существующие документационные кадры остаются побайтно прежними.

Issue: #663
User-Visible: no
2026-09-26 21:11:46 +03:00
Sergey Matyunin d03a68b88a feat: добавить лестницы между этажами (#663)
Прямые и винтовые лестницы получили отдельную модель, инструменты редактора, безопасную межэтажную навигацию, вычитание из чистой площади и плоское отображение в 2.5D.

Issue: #663
User-Visible: yes
2026-09-26 21:00:43 +03:00
Claude 781902b3a8 Release v1.78.0-beta.5 candidate
Проверка (CI) / Классификация изменённых файлов (push) Successful in 24s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 41s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 14s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 23s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 54s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 1m50s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 1m9s
Проверка (CI) / Бандл головы dev для стенда (push) Skipped
Проверка (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 5m7s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 18s
Issue: #664
Issue: #665
Issue: #666
User-Visible: yes
Release: v1.78.0-beta.5
2026-09-26 16:17:48 +03:00
claude[bot] f8a12f26b3 docs(reviews): индекс после сдвига каталога (#665)
Issue: #665
User-Visible: no
2026-09-26 13:14:54 +00:00
claude[bot] 2fdafb5bef docs: review document for #665
Issue: #665
User-Visible: no
2026-09-26 13:14:50 +00:00
claude[bot] d06cca2c19 docs: review document for #665
Issue: #665
User-Visible: no
2026-09-26 13:02:12 +00:00
Claude 8ffbdf8a28 test(2.5d): drive the label-metrics smoke through the public zoom controls (#665)
Validate on c93b3a90 failed no-new-private-writes: the smoke moved the
camera by writing card._view. It now presses the header zoom buttons
(data-hp zoom-fit / zoom-in) until the label font has at least doubled
(14 -> 38 px on the demo stand) and reads the private camera state only to
wait for the transition. Same assertions; the mutant
iso-room-label-44-box-centres-name still turns it red
(gapMatchesFlatAtEveryZoom, gapIsZoomIndependent).

Issue: #665
User-Visible: no
2026-09-26 15:23:52 +03:00
Claude c93b3a90ef fix(2.5d): room metrics keep their distance from the name at every zoom (#665)
The raised room label in 2.5D View sized its own box to the 44 px touch
minimum (min-height + justify-content: center). The name therefore sat
centred in 44 px while the metrics row, placed absolutely from the label
bottom, hung (44 - name height) / 2 below it: 18.3 px at a 9 px name and
6.4 px at a 39 px one on the demo stand, against a constant 0.1 name
height on the flat plan.

The 44 x 44 px floor moves into an invisible ::before (the door-lock
pattern), z-index -1 inside the label stacking context; the label box is
sized by its text again.

demo/smoke_iso_room_label_metrics.mjs measures gap / name height at two
cameras (font x4) in Flat and 2.5D, and the 44 px floor and area-link hit
targets on labels no raised device covers. Mutant
iso-room-label-44-box-centres-name.

Screenshots: 11 frames pixel-identical, fingerprint only.

Issue: #665
User-Visible: yes
2026-09-26 15:16:14 +03:00
claude[bot] 72d49040bf docs(reviews): индекс после сдвига каталога (#666)
Issue: #666
User-Visible: no
2026-09-26 12:11:55 +00:00
claude[bot] 288b9b41ab docs: review document for #666
Issue: #666
User-Visible: no
2026-09-26 12:11:51 +00:00
Claude 3bdd3c5163 fix(header): the active editor tab highlight covers its close X (#666)
The accent fill of the active Plan/Devices/Decor tab is now a pseudo-element
stretched over the 2 px group gap and the fixed 24 px X slot of #660, so
the X sits inside the highlighted zone without any box moving: the tab,
the slot, the X and the header keep their geometry. The X takes the tab
text colour, and the keyboard focus ring moves to the same pseudo-element
so it outlines the whole zone instead of cutting it at the tab edge.
.modes becomes its own stacking context so the z-index:-1 fill paints above
the group background. On a phone the mode tabs, and the fill, stay hidden.

smoke_toolbar_stable_width: painted-pixel probes for the gap, the slot edge
and top, and the first pixel after the slot, in every editor at every width
above 480 px; the X colour equals the tab colour. The existing geometry
and hit checks are unchanged and green. Mutant
toolbar-active-highlight-stops-at-tab.

Screenshots: 11 frames pixel-identical, fingerprint only.

Issue: #666
User-Visible: yes
2026-09-26 14:36:14 +03:00
claude[bot] 42f437cafe docs: review document for #665
Issue: #665
User-Visible: no
2026-09-26 11:20:43 +00:00
claude[bot] 942b9ede67 docs(reviews): индекс после сдвига каталога (#664)
Issue: #664
User-Visible: no
2026-09-26 10:25:16 +00:00
claude[bot] e53f9d4f77 docs: review document for #664
Issue: #664
User-Visible: no
2026-09-26 10:25:12 +00:00
Claude 40b73cc599 fix(space-card): nothing in the static schematic is hit-testable (#664)
The stage is pointer-events:none, but that is not an inherited ban: the
shared marker styles opt the 44 px floor (.dev::before) and the painted
capsule (.device-shell-frame) back in for the interactive plan (#564).
The static card imports them whole, so since v1.76.0-beta.2 it showed a
hand cursor over every marker and swallowed clicks that do nothing.

The card now closes every descendant opt-in (.hp-static-stage *, ::before,
::after). smoke_space_card asserts the browser hit test itself
(elementFromPoint over the marker and a 9x9 grid across the stage) and
the cursor, not the stage computed style that stayed green through the
regression. Mutant static-card-descendants-hit-testable.

Screenshots: 11 frames pixel-identical, fingerprint only
(npm run docs:accept -- --identical).

Issue: #664
User-Visible: yes
2026-09-26 13:02:51 +03:00
Sergey Matyunin 6cdcb1ea47 Release v1.78.0-beta.4 candidate refresh 2
Проверка (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) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 52s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 34s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 34s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 48s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 34s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 36s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m43s
Проверка (CI) / Бандл головы dev для стенда (push) Skipped
Проверка (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 20s
Повторная сборка кандидата после починки предрелизных unit- и mutation-гейтов.

Issue: #651
Issue: #654
Issue: #656
Issue: #657
Issue: #660
User-Visible: no
Release: v1.78.0-beta.4
2026-09-26 12:42:03 +03:00
Sergey Matyunin 85119fd9a2 test: восстановить защиту перехода редактора (#660)
Предрелизный гейт снова держит лимит монолитного файла, а защитный мутант #648 привязан к новой формуле общего бюджета сцены и панели. Мутант предметно краснит smoke_sections_resize.

Issue: #660
User-Visible: no
2026-09-26 12:42:03 +03:00
Sergey Matyunin f11c2a4c5d Release v1.78.0-beta.4 candidate refresh
Повторная сборка кандидата после исправления предрелизного smoke-гейта.

Issue: #651
Issue: #654
Issue: #656
Issue: #657
Issue: #660
User-Visible: no
Release: v1.78.0-beta.4
2026-09-26 12:35:19 +03:00
Sergey Matyunin bae6378006 fix: плавно менять высоту плана при входе в редактор (#660)
Переход снова интерполирует общий бюджет сцены и панели инструментов вместо измерения скрытого исходного кадра. Документационные кадры попиксельно не изменились.

Issue: #660
User-Visible: yes
2026-09-26 12:35:18 +03:00
Sergey Matyunin 467eaf873a chore(golden): эталоны beta.4 после 2.5D и панели
Приняты ровно 58 изменившихся сцен из Linux Validate; 117 остальных эталонов остались без изменений.

Issue: #651
Issue: #654
Issue: #660
User-Visible: no
Release: v1.78.0-beta.4
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36231891190
2026-09-26 12:17:29 +03:00
Sergey Matyunin 698f638969 docs: принять неизменные скриншоты для beta.4
Issue: #660
User-Visible: no
Release: v1.78.0-beta.4
2026-09-26 12:08:51 +03:00
Sergey Matyunin 3e48002c92 Release v1.78.0-beta.4 candidate
Issue: #651
Issue: #654
Issue: #656
Issue: #657
Issue: #660
User-Visible: yes
Release: v1.78.0-beta.4
2026-09-26 12:05:06 +03:00
claude[bot] 4c64189564 docs: review document for #663
Issue: #663
User-Visible: no
2026-09-26 09:04:16 +00:00
claude[bot] 9a19e17858 docs: review document for #662
Issue: #662
User-Visible: no
2026-09-26 08:56:02 +00:00
claude[bot] dc83a875f4 docs(reviews): индекс после сдвига каталога (#660)
Issue: #660
User-Visible: no
2026-09-26 08:51:15 +00:00
claude[bot] 5321c32ddc docs: review document for #660
Issue: #660
User-Visible: no
2026-09-26 08:51:15 +00:00
Sergey Matyuninandclaude[bot] 8906675879 test: вести smoke редакторов через публичный фасад (#660)
Issue: #660
User-Visible: no
2026-09-26 08:51:15 +00:00
Sergey Matyuninandclaude[bot] d3c88f06f9 feat: отполировать главную панель редакторов (#660)
Issue: #660
User-Visible: yes
2026-09-26 08:51:15 +00:00
claude[bot] cff83e43ec docs: review document for #663
Issue: #663
User-Visible: no
2026-09-26 08:47:57 +00:00
claude[bot] 422221fe93 docs: review document for #662
Issue: #662
User-Visible: no
2026-09-26 08:37:45 +00:00
claude[bot] 27517db8a4 docs: review document for #661
Issue: #661
User-Visible: no
2026-09-26 08:12:01 +00:00
claude[bot] 1ecf96d596 docs: review document for #660
Issue: #660
User-Visible: no
2026-09-26 07:43:22 +00:00
claude[bot] d65b9e39d5 docs: review document for #660
Issue: #660
User-Visible: no
2026-09-26 07:35:07 +00:00
claude[bot] 563fbe0759 docs: review document for #657
Issue: #657
User-Visible: no
2026-09-26 07:28:24 +00:00
Claudeandclaude[bot] b856dd33c8 infra(process): fast-forward merge rebuilds the review index too (#657 r1 H1)
The task branch no longer carries docs/reviews/INDEX.md (1b), and
merge-candidate rebuilt it only inside rebaseOnto. When dev did not move
the fast-forward pushed the stale index and reviews_index would turn dev
red. freshIndex(tip) commits the rebuilt index on top of the material
before the push; the merge stays a fast-forward.

Real-git test: fast-forward, then reviews-index --check on the dev head
is green. Mutant merge-ff-skips-review-index.

Issue: #657
User-Visible: no
2026-09-26 07:28:24 +00:00
claude[bot] 78d4da4abe docs: review document for #657
Issue: #657
User-Visible: no
2026-09-26 07:28:24 +00:00
Claudeandclaude[bot] 88c4e4e9ae infra(process): bundle and review index change only on the way to dev (#657)
Решения владельца: 1б — индекс ревью не пересобирается в ветке задачи,
только коммитами, идущими в dev; 2б — бандл меняет только кандидат
беты/релиза, стенд dev берёт его из артефакта Validate.

- scripts/bundle-policy.mjs: коммит, трогающий dist/** или
  custom_components/houseplan/frontend/**, обязан нести Release:
  (хук commit-msg и история в CI через validate-commit-provenance;
  коммиты с датой автора до 2026-09-27 не судятся); --verify судит
  целостность свежей сборки всегда, побайтовую сверку с закоммиченной
  копией — только на коммите, меняющем бандл, или кандидате; --clean.
- release-prerelease: публикация отказывает, если отпечаток исходников
  в закоммиченном манифесте не равен отпечатку дерева (хотфикс поверх
  кандидата без пересборки).
- bundle-sync: по умолчанию только demo/srv/assets; --release
  (npm run bundle:release) — ещё и custom_components.
- rebase-on-dev: конфликт в бандле берёт копию dev, без пересборки
  и amend.
- validate.yml: job dev_build публикует card-bundle головы dev в
  сиротскую ветку dev-build (scripts/dev-build.mjs); стенд накладывает
  её demo/stand/update-dev-bundle.sh.
- _process.yml: индекс ревью больше не пересобирается при приведении
  к dev и при публикации документа в ветку задачи.
- golden-wsl-artifact/golden-container: сборка перед съёмкой не
  считается правкой источника, после — bundle:clean.
- test/bundle-tree-committed: судит закоммиченный снимок, не диск.
- 11 мутантов в реестре; PROCESS/AGENTS/DEVELOPMENT/AUTHOR/REVIEWER.

Issue: #657
User-Visible: no
2026-09-26 07:28:23 +00:00
claude[bot] 4b8a82c08a docs: review document for #660
Issue: #660
User-Visible: no
2026-09-26 07:23:13 +00:00
claude[bot] 54f197209d docs: review document for #656
Issue: #656
User-Visible: no
2026-09-26 07:19:32 +00:00
Sergey Matyunin 43fab645b0 fix(ci): fail closed on newest full release proof (#656)
Issue: #656
User-Visible: no
2026-09-26 10:02:40 +03:00
claude[bot] 9287f798b3 docs: review document for #654
Issue: #654
User-Visible: no
2026-09-26 06:39:45 +00:00
Sergey Matyunin f5c6d70b7c fix: stabilize cold 2.5D first frame
Issue: #654
User-Visible: yes
2026-09-26 09:05:20 +03:00
claude[bot] 6d3738adef docs: review document for #654
Issue: #654
User-Visible: no
2026-09-26 05:29:35 +00:00
claude[bot] 6297af6701 docs: review document for #654
Issue: #654
User-Visible: no
2026-09-26 05:22:56 +00:00
claude[bot] 803c0f0eaa docs: review document for #651
Issue: #651
User-Visible: no
2026-09-26 01:54:48 +00:00
Sergey Matyunin e73e56dd94 test(2.5D): cover rigid group boundaries and fallback
Issue: #651
User-Visible: no
2026-09-26 04:34:49 +03:00
claude[bot] 7cc5423efe docs: review document for #651
Issue: #651
User-Visible: no
2026-09-26 01:30:54 +00:00
Sergey Matyunin c40266431c test(2.5D): retire superseded zoom mutant
Issue: #651
User-Visible: no
2026-09-26 03:54:57 +03:00
Sergey Matyunin 4ab7ecb4a2 fix(2.5D): keep device overlays stable while zooming
Issue: #651
User-Visible: yes
2026-09-26 03:41:32 +03:00
claude[bot] c6937f9fc7 docs: review document for #651
Issue: #651
User-Visible: no
2026-09-26 00:21:46 +00:00
claude[bot] 9d4f68a4ee docs: review document for #651
Issue: #651
User-Visible: no
2026-09-26 00:14:38 +00:00
Sergey Matyunin ec2b8c1522 fix(2.5D): preserve geometry on settings toggle
Проверка (CI) / Классификация изменённых файлов (push) Successful in 3m7s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 6m52s
Проверка (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) / HACS: валидация репозитория (push) Failing after 29s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 57s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 16s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 1m25s
Проверка (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 1m32s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 5m6s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 18s
Issue: #649
User-Visible: no
2026-09-25 23:33:07 +03:00
Sergey Matyunin 8703727951 fix(2.5D): make floor shadow twins geometry-only
Render only the core and value-badge geometry needed by the common floor-shadow layer. Avoid mounting hidden icons, activity indicators and satellites for every 2.5D marker.

Issue: #649
User-Visible: no
2026-09-25 23:05:56 +03:00
Sergey Matyunin cc04d341d4 fix(2.5D): reuse projection snapshot during settings toggle
The beta performance gate exposed a duplicate 2.5D scene resolution in one
Lit update: willUpdate built the projection snapshot and immediately cleared
it before render. Invalidate first so the computed snapshot is reused.

Issue: #649
User-Visible: no
2026-09-25 22:44:37 +03:00
Sergey Matyunin 8501d10988 chore(golden): эталоны визуальных изменений beta.3
Съёмка — Linux CI, Validate 36177319720 на 142340ee; приёмка — Windows
владельца с HP_ALLOW_FOREIGN_CAPTURE (#455).

Приняты ровно 52 заявленных изменения: публичный 2.5D, мобильная шапка и
стабильная панель редактора; необъявленных расхождений нет.

Issue: #616
Issue: #647
Issue: #649
User-Visible: no
Release: v1.78.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36177319720
2026-09-25 22:17:43 +03:00
Sergey Matyunin 142340ee1c Release v1.78.0-beta.3 candidate
Issue: #616
Issue: #626
Issue: #628
Issue: #638
Issue: #647
Issue: #648
Issue: #649
Issue: #650
User-Visible: yes
Release: v1.78.0-beta.3
2026-09-25 22:03:17 +03:00
claude[bot] 0e8e87303e docs: review document for #649
Проверка (CI) / Классификация изменённых файлов (push) Successful in 22s
Проверка (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 48s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 23s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 24s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 46s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 34s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 44s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m29s
Проверка (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
Issue: #649
User-Visible: no
2026-09-25 16:51:38 +00:00
Claude 106fdc096d fix(2.5D): prove the sun beam is cut by bodies; review r1 lows (#649)
CODE-REVIEW-649-r1 M1: the 2.5D beam's cut by physical bodies (AC7) had no
witness. Unit: a column in the beam leaves the floor between window and
column lit and shades it behind the column; smoke: a Solid partition across
the south beam removes floor from the rendered wash. Mutants: occluders
extruded toward the sun, occluders ignored, card drops occluders.

Lows: L1 stale alpha wording (DEVELOPMENT, ARCHITECTURE, card comment,
benchmark); L3 dark-theme state edges computed per theme; L4 Alert beats a
plain Hover and a plain Selected; L5 a virtual marker has no dashed ring;
L6 tiles and shadows stay without walls (show_borders off).

Issue: #649
User-Visible: no
2026-09-25 18:55:28 +03:00
claude[bot]andClaude e1f1c7d908 docs: review document for #649
Issue: #649
User-Visible: no
2026-09-25 18:55:27 +03:00
Claude 2ed050311b test(mutants): stage3-w12 guard follows the renamed #649 contract test
The guard pattern "Labs iso is presentation-only" matched no test after #649
renamed it, so node --test ran the file with zero subtests and the mutant
survived (Validate 36151140394, shard 3/6). Point it at the test that still
asserts params.getAll('hp_alpha').

Issue: #649
User-Visible: no
2026-09-25 18:55:15 +03:00
Claude e5ab4659f6 feat: 2.5D stage 6 — public setting, raised tiles, soft sun, theme-free walls (#649)
- settings.volumetric_view (General settings › Display, third switch) replaces
  the alpha entry, the header projection toggle and the phone-menu item; one
  rule for card, sidebar page and kiosk; editors stay Flat; backend accepts
  only a boolean, support package copies it.
- Raised tiles in 2.5D View: no ring, rounded body min(0.275 D, 0.3 h), edge
  0.1 D with the lab filters, ×1.12 size also in layout/collision, lift
  0.075 D, one floor-shadow layer under all markers (theme × floor table),
  frames hugging tile and edge (Alert > Focus > Selected > Hover), forced
  colours without edge and shadow (src/iso-tiles.ts, styles/iso-tiles).
- Soft sun wash instead of projected Flat wedges (src/iso-sun.ts): same gates
  and windows as sun_rays, length from elevation, parallelogram along the sun,
  tone and streaks by floor lightness, sill line.
- Walls take the user's wall colour (top opaque, sides × .77/.68/.60); theme
  rules for walls/openings/labels removed; furniture uses the Flat stroke rule.
- Smokes smoke_iso_tiles/iso_sun/iso_theme_walls/volumetric_setting, 16 new
  mutants, acceptance scenes STAGE6_ACCEPTANCE_SCENARIOS (golden with their
  Linux CI baselines), ACCEPTANCE.md side by side.

Issue: #649
User-Visible: yes
2026-09-25 18:55:14 +03:00
claude[bot] 11a8b623a2 docs: review document for #650
Issue: #650
User-Visible: no
2026-09-25 15:51:12 +00:00
Claude 43459bcbe0 ci(mutants): an unmatched --test-name-pattern is setup-failure, not survived (#650)
A name-filtered `node --test` that matches no test exits 0 and TAP reports
only the file itself. The clean run took that for a healthy witness and the
mutant run for `survived`, although no assertion ran either time — as in
Validate 36151140394 on #649, where a renamed test left a guard pattern empty.

- mutation-guard-outcome: nodeTestSelection/executedTestNames/
  emptyTestSelection; a green oracle with a name filter and zero executed
  named tests is `setup-failure` (clean run: «FAIL чистая подготовка»,
  diff mode: attributed like any setup failure, #568).
- mutation-gate --check: staticTestSelectionProblems — every pattern must
  match a literal test(/it(/describe( name in the guard's files; files with
  ${…} names only warn. POSIX quoting keeps \( inside "…".
- Registry today: 349 unique name-filtered commands run on dev, all execute
  ≥ 1 test; --check: 0 errors, 3 warnings (dynamic names).
- Tests: 6 new in test/mutation-guard-outcome.test.mjs (incl. the installed
  node's real TAP and a registry-wide static check); 3 mutants.
- Docs: TESTING.md pointer, testing-notes/infrastructure.md section.

Issue: #650
User-Visible: no
2026-09-25 18:31:17 +03:00
claude[bot] a55f564498 docs: review document for #648
Issue: #648
User-Visible: no
2026-09-25 13:33:26 +00:00
claude[bot] 4168b1cfce docs(reviews): индекс после сдвига каталога (#648)
Issue: #648
User-Visible: no
2026-09-25 13:11:44 +00:00
Sergey Matyuninandclaude[bot] 8f13325c52 test: проверять минимум Sections на RU и DE (#648)
Issue: #648
User-Visible: no
2026-09-25 13:11:44 +00:00
claude[bot] 180239f44c docs: review document for #648
Issue: #648
User-Visible: no
2026-09-25 13:11:44 +00:00
Sergey Matyuninandclaude[bot] b9fe916662 test: детерминировать transition-smoke Sections (#648)
Issue: #648
User-Visible: no
2026-09-25 13:11:44 +00:00
Sergey Matyuninandclaude[bot] dc0a5ccc5a test: ловить viewport-регрессии Sections (#648)
Issue: #648
User-Visible: no
2026-09-25 13:11:44 +00:00
Sergey Matyuninandclaude[bot] 2452c51d00 feat: вертикальный resize карточки в Sections (#648)
Issue: #648
User-Visible: yes
2026-09-25 13:11:44 +00:00
claude[bot] 230813b355 docs: review document for #648
Issue: #648
User-Visible: no
2026-09-25 13:11:44 +00:00
claude[bot] 7372e977b8 docs: review document for #649
Issue: #649
User-Visible: no
2026-09-25 12:48:25 +00:00
claude[bot] f68878cc44 docs: review document for #649
Issue: #649
User-Visible: no
2026-09-25 12:34:36 +00:00
claude[bot] b7079ead80 docs: review document for #616
Issue: #616
User-Visible: no
2026-09-25 11:49:20 +00:00
Claude 255330b9bc Шапка на телефоне: одна строка — вкладки, масштаб и шестерёнка с меню остального (#616)
На окне не шире 480 px шапка карточки и страницы бокового меню занимает одну
строку ≤ 56 px: вкладки пространств (без шестерёнок и «+», прокрутка вбок,
текущая вкладка докручивается в видимую область), масштаб, одна шестерёнка
«Действия и настройки» и у админа слот крестика #647. Заголовок скрыт.

Меню шестерёнки (src/header-menu.ts): редакторы, настройки и добавление
пространства, общие настройки, PDF, помощь, сводная панель и объёмный вид
(alpha) — в фиксированном порядке и с теми же условиями и действиями, что у
прежних кнопок. Пункт закрывает меню; Escape возвращает фокус на шестерёнку;
касание вне меню поглощается прозрачной подложкой. На > 480 px шапка прежняя,
в киоске меню нет.

- три кнопки шапки вынесены в renderHeaderActions — шапка карточки не растёт;
- сводная панель отдаёт свои пункты методом menuItems();
- i18n: title.header_menu (en/ru/de/fr);
- потолок initial View 291 000 → 292 600 (замер 291 635), база bundleBytes;
- смок smoke_mobile_view_header, юнит header-menu, 10 мутантов; смоки
  support_feedback, gear_tabs, modes, toolbar_stable_width переведены на
  меню на ≤ 480 px; подпись вкладки в span.tabtitle без pointer-events;
- USER-GUIDE.ru, UX-MODES, TOUCH-SUPPORT, STYLING-HOOKS, data-hp-contract,
  оба changelog.

Issue: #616
User-Visible: yes
2026-09-25 14:22:36 +03:00
claude[bot] dc6018ed5a docs: review document for #626
Issue: #626
User-Visible: no
2026-09-25 11:20:33 +00:00
Sergey Matyunin b80193f726 test: restore default upload ACL coverage
Issue: #626
User-Visible: no
2026-09-25 14:09:07 +03:00
claude[bot] b6bae3dba6 docs: review document for #626
Issue: #626
User-Visible: no
2026-09-25 11:07:49 +00:00
Sergey Matyuninandclaude[bot] 552504b834 fix: preserve administrator write access
Issue: #626
User-Visible: yes
2026-09-25 10:47:27 +00:00
Sergey Matyuninandclaude[bot] e6987459a5 fix: respect Home Assistant read-only ACL
Issue: #626
User-Visible: yes
2026-09-25 10:47:27 +00:00
claude[bot] 2529052749 docs: review document for #626
Issue: #626
User-Visible: no
2026-09-25 10:47:27 +00:00
claude[bot] d5df838976 docs: review document for #616
Issue: #616
User-Visible: no
2026-09-25 10:28:49 +00:00
claude[bot] e095c710f9 docs: review document for #628
Issue: #628
User-Visible: no
2026-09-25 10:21:04 +00:00
claude[bot] a32744255f docs(reviews): индекс после сдвига каталога (#628)
Issue: #628
User-Visible: no
2026-09-25 10:13:17 +00:00
Sergey Matyuninandclaude[bot] 054a713977 docs: preserve workstation section hierarchy
Keep the critical file-sync subsection under Local Windows workstation and place the repository-maintenance section after it, as requested by review r1.

Issue: #628
User-Visible: no
2026-09-25 10:13:17 +00:00
claude[bot] f86bc34dd5 docs: review document for #628
Issue: #628
User-Visible: no
2026-09-25 10:13:17 +00:00
Sergey Matyuninandclaude[bot] 774b7c7e04 docs: record repository GC baseline
Owner-selected option 1 only: document the one-time Windows clone GC, its before/after measurements, and the 2026-10-25 growth checkpoint. No LFS, tracked-bundle removal, or history rewrite.

Issue: #628
User-Visible: no
2026-09-25 10:13:17 +00:00
claude[bot] e52afe6349 docs: review document for #638
Issue: #638
User-Visible: no
2026-09-25 10:10:39 +00:00
Claude 0ba81a994b Процесс: независимое ревью линии перед стабильным релизом, не блокирующее выпуск (#638)
PROCESS.md §11.5: перед стабильным релизом — одно ревью поверхностей всей
линии бет «с нуля», без ТЗ и документов раундов, по SCOPE и USER-GUIDE.

- scripts/release-review.mjs: вход линии — прошлый стабильный тег, issue по
  трейлерам в схеме RELEASE-MEMBERSHIP.json, продуктовые файлы; бриф промпта.
- .github/workflows/release-review.yml (workflow_dispatch, исполняется с dev):
  prepare → model_review (модель без прав на запись, github_token #556) →
  publish (docs/reviews/RELEASE-REVIEW-vX.Y.Z.md в dev токеном процесса,
  индекс тем же коммитом, review-doc-guard). Повтор на тот же тег не тратит
  модель, если документ уже в dev.
- release.yml: job independent-review ставит ревью в очередь сразу после
  candidate, continue-on-error; ни один job выпуска от него не зависит
  (решение владельца 2026-09-25).
- REVIEWER.md, AGENTS.md, DEVELOPMENT.md; тесты и четыре мутанта.

Issue: #638
User-Visible: no
2026-09-25 12:53:14 +03:00
claude[bot] d35a52a3f1 docs: review document for #647
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 13m31s
Проверка (CI) / Классификация изменённых файлов (push) Failing after 14m40s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 1m38s
Issue: #647
User-Visible: no
2026-09-25 08:57:28 +00:00
Claudeandclaude[bot] f0ba7c5da9 Основная панель: без счётчика устройств, × редактора в своём слоте — ширина шапки не меняется (#647)
Счётчик «N устр.» и ключ count.devices удалены вместе с правилом
`.head .count` (скрывало счётчик на ≤1100 px). Крестик закрытия редактора
вынесен из активной кнопки режима в отдельный слот `.editor-close-slot`
сразу после группы кнопок режимов: 24 × 24 px (--hp-editor-close-size),
одинаковый в редакторе и вне его; вне редактора слот пуст, aria-hidden,
без фокуса и pointer-events. Кликабельная зона × — весь слот (≥24×24, #195),
глиф 13 px; × виден на любой ширине, где видны кнопки режимов.

Смок smoke_toolbar_stable_width (1400/1000/768/390 px, en/ru): ширина
шапки и отступы кнопок стабильны во всех переходах, × виден, попадание
в край зоны закрывает редактор, нет нового overflow. smoke_editor_tabs,
smoke_edge_cases, smoke_hidden_flag переведены на слот/список устройств.
6 мутантов. docs: UX-MODES.md, STYLING-HOOKS.md, data-hp-contract.json.

Issue: #647
User-Visible: yes
2026-09-25 08:35:57 +00:00
Sergey Matyunin 632d6a2f8e chore(golden): accept beta.2 visual changes
Accepted exactly the five reviewed Linux CI scenes for device batch selection and transparent color tiles.

Release: v1.78.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/36111677966
Issue: #615
Issue: #618
User-Visible: no
2026-09-25 11:23:47 +03:00
claude[bot] 27d78801d0 docs: review document for #647
Issue: #647
User-Visible: no
2026-09-25 08:10:38 +00:00
Sergey Matyunin 02b374f504 Release v1.78.0-beta.2 candidate
Issue: #615
Issue: #617
Issue: #618
Issue: #620
Issue: #622
Issue: #623
Issue: #627
Issue: #629
Issue: #630
Issue: #631
Issue: #632
Issue: #633
Issue: #634
Issue: #642
Issue: #643
Issue: #645
Issue: #646
User-Visible: yes
2026-09-25 11:07:14 +03:00
claude[bot] 1ba4c23a58 docs: review document for #647
Issue: #647
User-Visible: no
2026-09-25 08:01:04 +00:00
claude[bot] b4d6effbca docs: review document for #645
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 6m52s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 4m54s
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (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) Skipped
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Failing after 15m33s
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 1m56s
Issue: #645
User-Visible: no
2026-09-25 07:47:56 +00:00
Sergey Matyunin f293c6e9dc fix(editor): сохранять позицию кнопки при отмене Resize (#645)
Транзитная геометрия Resize теперь только вычисляет центр для текущего кадра и не удаляет сессионную позицию. Окончательная геометрия по-прежнему очищает точку, если она действительно вышла за комнату.

Issue: #645
User-Visible: yes
2026-09-25 10:15:34 +03:00
claude[bot] 449438fb20 docs: review document for #645
Issue: #645
User-Visible: no
2026-09-25 06:23:57 +00:00
Sergey Matyunin a8437e69e5 fix(ci): атомарно печатать отчёт private-writes (#645)
Linux CI при параллельном npm test мог потерять последнюю отдельную запись
stdout после длинной таблицы --count. Формируем отчёт целиком и пишем его
одним вызовом, чтобы итоговая строка всегда доходила до unit-теста.

Issue: #645
User-Visible: no
2026-09-25 08:58:05 +03:00
Sergey Matyunin 23e55d4929 test(resize): различать актуальный центр кнопки #645
Issue: #645
User-Visible: no
2026-09-25 08:40:41 +03:00
Sergey Matyunin 0a95a1cd65 test(editor): использовать публичный фасад в smoke #645
Issue: #645
User-Visible: no
2026-09-25 08:36:21 +03:00
Sergey Matyunin 62d0f50593 feat(editor): перетаскивание кнопки настроек комнаты (#645)
Кнопка сохраняет положение в пределах комнаты только на время сессии редактора,
корректно отменяется при pinch/cancel и учитывается подписями Resize.

Issue: #645
User-Visible: yes
2026-09-25 08:33:07 +03:00
claude[bot]andSergey Matyunin 27ba3f2ee0 docs: review document for #645
Issue: #645
User-Visible: no
2026-09-25 08:06:14 +03:00
claude[bot]andSergey Matyunin 15e047949e docs: review document for #645
Issue: #645
User-Visible: no
2026-09-25 08:06:13 +03:00
claude[bot] 744b4a8398 docs: review document for #629
Проверка (CI) / Классификация изменённых файлов (push) Failing after 13m41s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 21m16s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 1m23s
Issue: #629
User-Visible: no
2026-09-25 01:43:46 +00:00
Claudeandclaude[bot] d90725815a build(#629): ребейз на dev после #615 — пересборка бандла, индекс ревью
- Ветка переиграна поверх dev с #642 (диалог «Оптимизировать планы» —
  свой модуль), #627 (ленивые словари), #617 (загрузка плана по HTTP),
  #618 (пакетное скрытие в каталоге), #615 (шахматка альфы в плитках цвета).
  Текстовые конфликты — в сгенерированном (бандл) и в
  demo/smoke_grid_snap.mjs (#642 сменил адрес вызова на
  _editorRuntime.optimizePlans.open/run, #629 — ввод через фасад; оставлены
  оба); scripts/mutation-registry.mjs слит без конфликта;
  docs/reviews/INDEX.md пересобран reviews-index.mjs.
- После ребейза поверх #615 в дереве остался осиротевший чанк
  namespace-language-ani9_pFw.js (переименование с обеих сторон, разрешено
  версией dev без удаления нашей) — каталоги ассетов собраны с нуля,
  bundle-tree чист.
- #642 опустил потолок src/houseplan-card.ts до факта; хук mode-tab
  перенесён на строку data-editor-navigation (коммит хука), ядро не растёт.
- Бандл пересобран, три копии совпадают; monolith-baseline — числа равны
  базе dev.

Issue: #629
User-Visible: no
2026-09-25 01:25:21 +00:00
claude[bot] a977f2c23e docs: review document for #629
Issue: #629
User-Visible: no
2026-09-25 01:25:20 +00:00
Claudeandclaude[bot] fa319e37b3 test(harness): тестовый фасад window.__hpTest и гейт no-new-private-writes (#629)
- scripts/no-new-private-writes.mjs: смоки и demo/helpers/** не добавляют
  записей в приватное состояние карточки (присваивание, ++/--, delete по
  цепочке с сегментом _x; от this — нет) и вызовов _setMode/_openRoomEdit/
  _openMarkerDialog/_openSpaceDialog. Зачёт правки по полю, перенос блока —
  movedLinesByFile из no-new-any; исключение // private-ok: <причина>.
  --count — остаток на HEAD. Подключён в gate:small и в шаг frontend рядом с
  no-new-any, с той же базой.
- demo/helpers/hp-test.mjs: 10 операций через контрактные хуки и события
  фикстуры (setMode, setTool, switchSpace, openRoomEdit, openMarkerDialog,
  openSpaceDialog, setServerConfig, setLayout, input, close); ставится
  launch*() из demo/serve.mjs. В бандле фасада нет.
- demo/srv/demo.html: доставка houseplan_config_updated/_layout_updated,
  __pushServerConfig/__pushServerLayout; после доставки запись со старым
  expected_rev — conflict, как у настоящего сервера.
- HP_SMOKE_CHECKS=1 печатает имена проверок в finish().
- smoke_area_relocation, smoke_glow, smoke_grid_snap переведены на фасад без
  потери утверждений; новый smoke_test_facade доказывает каждую операцию.
- 7 мутантов, docs/TESTING.md (раздел + правило №6), PROCESS.md §2.7, AGENTS.md.

Issue: #629
User-Visible: no
2026-09-25 01:25:20 +00:00
Claudeandclaude[bot] 035399e7a6 feat(hooks): data-hp="mode-tab" + data-mode на вкладках режимов редакторов (#629)
Единственная дыра контракта #489 для тестового фасада харнесса: у вкладок
plan/devices/decor был только data-editor-navigation, которого нет в JSON-
контракте. Хук описан в docs/data-hp-contract.json (audience test, since
1.78.0-beta.2) и STYLING-HOOKS §7.4; присутствует только вместе с самими
вкладками (право на редактирование). Стилями не выбирается, пикселей не
меняет. Бандл пересобран.

Issue: #629
User-Visible: no
2026-09-25 01:25:20 +00:00
claude[bot] 59f0e49c6e docs: review document for #646
Проверка (CI) / Классификация изменённых файлов (push) Failing after 20m18s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 21m11s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 17m53s
Issue: #646
User-Visible: no
2026-09-25 01:07:05 +00:00
Claude fa91f3a731 test: temp dirs are removed even when a test fails; lint guards it (#646)
golden-capture-provenance accept() left a ~20 MB baselines sandbox in
TMPDIR on every call (433 copies, 8.5 GB, ENOSPC in the shared sandbox).
It now reads what it needs and removes the sandbox in finally; fixtures
are removed via t.after. Same fix for the three other leaking sites
(bundle-assets hp-tree-, docs-accept hp-identical-, rebase-generated
hp-runner- outside finally).

test/temp-dir-hygiene.test.mjs parses test/**/*.mjs with the TypeScript
AST: every mkdtemp/mkdtempSync must be bound to a name removed by
rm*/rmSync in a finally block or an after/afterEach/t.after hook of the
same function, or returned by a named helper whose every call site does
so; exception `// tmp-ok: <reason>`. Two mutants witness the lint.

Issue: #646
User-Visible: no
2026-09-25 03:50:40 +03:00
claude[bot] 524908b23b docs: review document for #615
Проверка (CI) / Классификация изменённых файлов (push) Failing after 7m4s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 9m4s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 7m29s
Issue: #615
User-Visible: no
2026-09-25 00:38:11 +00:00
Claude 2bf929e34e fix(settings): colour tiles show alpha as a checkerboard (#615)
General settings colour tiles (room fills and glow) paint their colour at
its opacity over the checkerboard again, so "No light sources" at 0 % no
longer looks like solid grey. The label ink is chosen from the visible
colour (hex mixed with the checkerboard by alpha). Colour plates
(flat-swatch without cover-swatch) stay solid — now witnessed by the

- hp-color-opacity: one `_solidSwatch` condition drives background and
  opacity; cover-swatch keeps the checkerboard
- editors/color-tile-ink.ts: pure isLightHex/colorTileInk, unit-tested
- smoke_dialog_polish_605: tileAlpha, defaultsDiffer, noneInkDark,
  liveAlpha, platesFlat, plateSolid; oneSurface drops the opaque check
- mutants M-615-tile, M-615-plate, M-615-label
- USER-GUIDE (ru/en): "Colour tile" row; both changelogs

Issue: #615
User-Visible: yes
2026-09-25 02:13:32 +03:00
claude[bot] a72989c4bb docs: review document for #618
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 14m54s
Проверка (CI) / Классификация изменённых файлов (push) Failing after 13m28s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 7m52s
Issue: #618
User-Visible: no
2026-09-24 22:48:32 +00:00
Claude 93bacc9696 build(#618): ребейз на dev после #642, #627, #617 — пересборка бандла, потолок редактора, база монолита
Код-ревью r1 зелёное; слияние отказало на конфликте с dev. Ветка
переиграна на origin/dev 2b3959e5 (32 коммита).

- Конфликты: бандл — версия dev + пересборка; оба changelog — запись
  #618 под записью #617 в том же разделе; bundle-budget.mjs и
  monolith-baseline.json — версия dev, числа #618 измерены заново;
  docs/reviews/INDEX.md — node scripts/reviews-index.mjs.
- Ключи device_inbox.* остаются в основных словарях: #627 вынес в
  ленивые чанки только settings/support/topology, каталог устройств
  туда не входит.
- bundle-budget: LAZY_EDITOR_GZIP_CEILING 229 600 → 231 200 (замер
  230 208, база dev 228 997, +1 211 Б gzip); initial View 289 558 →
  289 884 (+326 Б, en-строки), в пределах потолка 290 400.
- monolith-baseline: hostRefs 4872 → 4883 (+11, как в r1),
  bundleBytes 2509909 → 2515825.
- src/houseplan-card.ts: снято type-зеркало `selected?` в
  DeviceInboxDialogState карточки (2 строки). После #627 запаса под
  потолком ядра (12889) не осталось; карточка поле не читает, пишет и
  читает его только рантайм через свой тип — так же, как черновики
  фильтров #44. Типы стираются, поведение не меняется.

Issue: #618
User-Visible: no
2026-09-24 23:12:30 +03:00
claude[bot]andClaude 7890005a92 docs: review document for #618
Issue: #618
User-Visible: no
2026-09-24 22:50:39 +03:00
Claude d17bcc05fb feat(devices): пакетное «Скрыть/Показать выбранные» в каталоге устройств (#618)
На вкладках «На плане» и «Скрытые» — флажки строк, «Выбрать все (N)» по
всему отфильтрованному набору (включая строки за «Показать ещё»), панель
«Выбрано: K · Скрыть/Показать выбранные (K) · Снять выбор» в отдельном
контейнере .device-inbox-batch вне .device-inbox-filters.

- src/device-inbox.ts: чистые inboxVisibilityAllowed (один источник для
  canHide/canShow и выбора), selectableInboxRows, effectiveInboxSelection,
  applyInboxVisibility — свёртка одиночных действий; одиночное = пакет из
  одной строки.
- src/device-inbox-batch.ts (новый, lazy editor): выбор, панель, строка-
  флажок и writeInboxVisibility — одна запись houseplan/config/set с
  expected_rev на пакет, busy='__batch__' (inert), откат маркеров при отказе,
  выбор сохраняется и сужается. Одиночный _setInboxHidden рантайма — тонкая
  обёртка над той же записью; рантайм стал короче базы на 14 строк.
- сброс выбора при смене вкладки (клик и стрелка), поиска и «Только новые».
- i18n en/ru/de/fr: 9 ключей по ТЗ §8.
- смок demo/smoke_device_inbox_batch.mjs (AC1–AC8, AC10), юниты
  test/device-inbox.test.mjs (AC2, AC4), 4 мутанта в реестре.
- docs: USER-GUIDE ru/en, FILTERING.md, оба changelog.
- monolith-baseline: hostRefs 4948→4959, bundleBytes 2500387→2505569 —
  новая фича (состояние выбора и запись идут через порт хоста).
- bundle-budget: LAZY_EDITOR_GZIP_CEILING 245 400 → 246 600 (замер 245 610,
  +1 190 Б gzip от базы 244 420); initial View 289 449 → 289 765 (+316 Б,
  строки en-словаря), в пределах потолка.

Golden device-inbox-* изменятся ожидаемо — пересъёмка предрелизным гейтом
на Linux CI (§11.4).

Issue: #618
User-Visible: yes
2026-09-24 22:50:30 +03:00
claude[bot] 2b3959e55b docs: review document for #617
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 13m26s
Проверка (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) / Смоки в браузере (шард 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
Проверка (CI) / Классификация изменённых файлов (push) Failing after 3m35s
Issue: #617
User-Visible: no
2026-09-24 17:36:41 +00:00
Claude 43b52826e4 build(#617): ребейз на dev после #642 и #627 — пересборка бандла, база монолита
- Ветка переиграна поверх dev с #642 (вынос диалога «Оптимизировать
  планы») и #627 (ленивые словари settings/support/topology). Текстовые
  конфликты — только в сгенерированном (бандл, база монолита) и в
  docs/reviews/INDEX.md (пересобран reviews-index.mjs); src/**, i18n,
  реестр мутантов, smoke-links слились без конфликтов.
- Бандл пересобран, три копии совпадают, bundle-tree: 30 ассетов.
- monolith-baseline: hostRefs 4 869 → 4 872 (+3 — те же три обращения к
  хосту, что и до ребейза: 4 948 → 4 951, приняты на ревью r1),
  bundleBytes 2 508 211 → 2 509 909 (+1 698, в полосе ±2 000); прочие
  числа равны базе dev.
- bundle-budget: потолки не менялись, все замеры в полосе.

Issue: #617
User-Visible: no
2026-09-24 19:59:51 +03:00
claude[bot]andClaude 9f3511f2dc docs: review document for #617
Issue: #617
User-Visible: no
2026-09-24 19:57:54 +03:00
Claude 3fafd9f2b6 test(plans): «нет файла» — настоящий multipart без части file (#617)
HA-тест test_issue_617_plan_upload_validates_fields_like_ws_plan_set слал
FormData только с текстовыми полями. aiohttp в этом случае кодирует тело
как application/x-www-form-urlencoded, а не multipart: view отвечал
bad_request (контракт ТЗ — «битый multipart»), и ожидание no_file в тесте
было неверным — дефект фикстуры, а не view (Validate 35952858822).

Теперь случай «multipart без части file» задаётся явно
(FormData(default_to_multipart=True)) и ждёт no_file, а тело без multipart
проверяется отдельно и ждёт bad_request. Ни одно ожидание не ослаблено:
добавлена ещё одна проверка ветки отказа.

Issue: #617
User-Visible: no
2026-09-24 19:57:42 +03:00
Claude 857369b18b fix(plans): загрузка плана по HTTP, предел 8 МБ проверяется до отправки (#617)
План уходил base64 в WebSocket-кадре: файл больше ~3 МиБ давал кадр больше
4 МиБ, HA закрывал сокет до обработчика, и обещанные 8 МБ были недостижимы.

- бэкенд: HouseplanPlanUploadView (POST /api/houseplan/plans/upload), потоковый
  предел MAX_PLAN_BYTES (read_bounded), общий writer store_plan_upload для view
  и ws_plan_set (контракт WS без изменений);
- карточка: stagePlanFile/uploadPlanFile/renderPlanBackdropGuard в
  backdrop-pick.ts для обоих рантаймов; PlanFilePayload хранит Blob вместо b64;
  SVG больше предела — тост при выборе, растр — диалог #39 только с уменьшенной
  копией, копия больше предела не попадает в staging, 413 называет предел;
- i18n toast.plan_too_large, backdrop.over_limit_body (en/ru/de/fr);
  USER-GUIDE ru/en, CHANGELOG ru/en, docs/testing-notes (#617);
- тесты: test/plan-upload-limit.test.mjs, tests_backend/test_plan_upload.py,
  test_ha_upload.py (#617), smoke_plan_upload_limit.mjs; три смока переведены
  с b64/plan/set на blob/fetchWithAuth; мутанты plan-upload-*;
- база монолита: hostRefs +3 (общий хелпер плана вместо двух копий в рантаймах,
  новый тост уменьшенной копии).

Issue: #617
User-Visible: yes
2026-09-24 19:57:41 +03:00
claude[bot] f8e089bdf2 docs: review document for #627
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 26m52s
Проверка (CI) / Классификация изменённых файлов (push) Failing after 38s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Skipped
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 19s
Issue: #627
User-Visible: no
2026-09-24 16:52:11 +00:00
Claude b921ba7184 build(#627): ребейз на dev после #642 — пересборка бандла, потолок редактора, база монолита
- Бандл пересобран поверх dev с #642 (вынос диалога «Оптимизировать
  планы»), три копии совпадают.
- bundle-budget: потолок ленивого графа редактора 246 000 (#642) → 229 600,
  замер 228 621 (до ребейза 228 900 при 227 869; +752 — модуль диалога #642).
  Центр полосы: 979 Б сверху, 1 021 Б до нижней границы. Потолок онбординга
  28 500 без изменений (замер 27 468).
- monolith-baseline bundleBytes 2 499 182 → 2 508 211 (+9 029: десять новых
  чанков словарей и записи манифеста, та же причина, что и до ребейза);
  прочие числа равны базе dev.
- smoke_optimize_geometry_preflight: ожидание языкового гейта #627 перед
  открытием диалога через новый порт #642 (`_editorRuntime.optimizePlans.open()`),
  разрешено в коммите задачи.

Issue: #627
User-Visible: no
2026-09-24 18:54:29 +03:00
claude[bot]andClaude 4fea0d2664 docs: review document for #627
Issue: #627
User-Visible: no
2026-09-24 14:51:30 +03:00
Claude 70fe860bac test(smoke): danger confirmation waits for the composite language gate (#627 r1 M1)
smoke_danger_confirmation asked for a confirmation as soon as btn.cancel was
translated. With #627 the settings namespace of de/fr settles after the main
catalog; while it is pending the host gate is warm and #417 refuses the
request — the dialog never painted and the smoke crashed intermittently.

The switch now waits until the host drops aria-busy (the gate settled), and
the settings-de/fr chunks are delayed by 300 ms so the slower-namespace order
is deterministic instead of a CI-speed coincidence. New checks name the lost
dialog directly instead of a TypeError.

Issue: #627
User-Visible: no
2026-09-24 14:51:19 +03:00
claude[bot]andClaude 2eb9fdbf68 docs: review document for #627
Issue: #627
User-Visible: no
2026-09-24 14:51:19 +03:00
Claude 82a48ee0ce perf(i18n): словари settings/support/topology — ru/de/fr ленивыми чанками; потолок онбординга (#627)
- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на
  пару «пространство × язык»), английский статически как слой отката;
  рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback,
  общий тост отказа), src/i18n/namespace-language.ts.
- Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер
  только после того, как словарь текущего языка осел; гейт хоста учитывает
  словари загруженной на нём поверхности (смена языка на лету — ветка warm).
  Оверлей Zigbee ждёт topology до первой отрисовки.
- bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со
  строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500
  (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки
  ленивости и владения по содержимому.
- Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык
  синхронно, ждут гейт вместо ослабления проверок; пять мутантов.
- monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro
  и export на каждый) и записи манифеста; gzip каждой поверхности при этом
  уменьшился.

Issue: #627
User-Visible: no
2026-09-24 14:51:18 +03:00
claude[bot] 0f234f769d docs: review document for #642
Проверка (CI) / HACS: валидация репозитория (push) Failing after 36s
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 40s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 41s
Проверка (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 9s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Failing after 50s
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 49s
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 40s
Issue: #642
User-Visible: no
2026-09-24 08:34:23 +00:00
claude[bot] c70aab8bbf docs: review document for #642
Issue: #642
User-Visible: no
2026-09-24 08:00:59 +00:00
claude[bot] b7747bc7d8 docs(reviews): индекс после сдвига каталога (#642)
Issue: #642
User-Visible: no
2026-09-24 08:00:59 +00:00
claude[bot] 345e613a4a docs: review document for #642
Issue: #642
User-Visible: no
2026-09-24 08:00:58 +00:00
Claudeandclaude[bot] 66ae952c2a refactor(editor): диалог «Оптимизировать планы» — свой модуль с узким портом (#642)
Первый вынос из монолита по образцу live-*/RadarSetupController.
src/optimize-plans-dialog.ts: OptimizePlansDialogState, чистые
preflightDiagnostics/preflightVersionsDiffer и класс OptimizePlansDialog
(open/preview/toggleLivePositions/run/copyDiagnostics/
reportPreflightFailure/render, clipboardFallback) за портом из 18 членов.
Модуль не знает HouseplanEditorHostPort; импортирует его только рантайм,
карточка берёт тип.

Инлайн-фолбэк буфера обмена — WeakMap по объекту диалога
(CODE-REVIEW-295-r1 M2), дедуп dev-лога — поле класса: из карточки и порта
ушли _preflightClipboardFallback и _reportedPreflightFingerprint, пять
делегатов и две стрелки-заглушки. Харнесс (13 смоков, wall-draw-click,
golden) зовёт card._editorRuntime.optimizePlans.* — меняется только адрес.

11 текстовых утверждений i18n.test.mjs о разметке и тосте диалога и гварды
8 мутантов переехали в юнит test/optimize-plans-dialog.test.mjs
(test-build); новый мутант optimize-dialog-imports-host-port.

monolith-baseline: delegates 159→154, portMembers 350→348,
hostRefs 4948→4869, portPrivates 96→94, harnessPrivates 107→101,
bundleBytes 2499647→2499182. Потолки ядер опущены на выигрыш;
потолок lazy editor gzip пересчитан (+555 Б gzip при −465 Б сырых).

Issue: #642
User-Visible: no
2026-09-24 08:00:58 +00: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[bot] 79ee16546d docs: review document for #623
Issue: #623
User-Visible: no
2026-09-24 07:56:01 +00:00
Claude baf283c50f ci: thin default-branch callers invoke reusable bodies at @dev (#623)
Six workflows run from the default branch (issues, schedule, workflow_run):
process, process-resume, process-reconcile, mutation-gate, nightly,
process-metrics. Their bodies move to _<name>.yml (on: workflow_call); the
original files keep only triggers, run-name, permissions, concurrency and one
job `uses: Matysh/houseplan-card/.github/workflows/_<name>.yml@dev` with
`secrets: inherit`. A pipeline change becomes one commit to dev.

- caller job permissions = union of body job permissions (#556 minimum kept
  per job inside the body); caller `if` repeats the body guard for process and
  process-resume so unrelated events stay skipped;
- dispatch inputs forwarded via workflow_call inputs of the same names;
- _mutation-gate.yml keys evidence/marker on job.workflow_sha (the body SHA):
  in a called workflow github.workflow_sha belongs to the caller in main;
- action-pins: narrow exception for this repo's _*.yml at @dev with a reason;
- preflight workflow_sync compares all six thin callers (was 3 of 6);
  performance.yml excluded: its schedule judges main with main's own body;
- tests read bodies from _*.yml; new test/default-branch-workflows.test.mjs;
  six mutants; PROCESS.md §10.4, AGENTS.md, REVIEWER.md updated.

Issue: #623
User-Visible: no
2026-09-24 10:23:28 +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[bot] 72acafd7eb docs: review document for #643
Issue: #643
User-Visible: no
2026-09-24 07:03:41 +00:00
Claude 92b83d9525 fix(process): rebase resolves a conflict only in docs/reviews/INDEX.md by rebuilding the index
A pipeline doc commit carries the review document and the rebuilt
INDEX.md; while the task waits, dev receives other tasks' documents with
their own INDEX.md, and the rebase of the branch conflicts in the index
every time. 24.09 this bounced green #617, #618, #629, #642 to S6.

scripts/rebase-generated.mjs: shared rebase helper. At every stop, if ALL
conflicting paths are docs/reviews/INDEX.md (or paths the caller
resolves itself), the index is rebuilt from the directory in the stop
tree, staged, and the rebase continues; any other path aborts and
returns the full list. CLI exit 3 = refusal with paths on stdout.

Wired into process.yml «Привести ветку к dev» (helper taken from dev via
git archive; conflict/conflicts outputs, lease, ref wait and
--commit-if-stale kept), merge-candidate rebaseOnto (claude[bot]
identity, --commit-if-stale kept) and rebase-on-dev.mjs (index next to
GENERATED_ROOTS; bundle still dev copy + rebuild).

Issue: #643
User-Visible: no
2026-09-24 09:35:58 +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[bot] ee4bbf8f1f docs: review document for #620
Issue: #620
User-Visible: no
2026-09-24 06:07:09 +00:00
Claudeandclaude[bot] 44ee23ee33 ci(mutants): skip the nightly registry on a proven tree; install only the shard's environment
#620. Mutants were ~70 % of CI machine time.

1. mutation-gate.yml: a green full run (aggregator verified all six shards)
   leaves a cache marker keyed by material tree + workflow SHA. A scheduled
   night with the same tree and workflow, marker not older than 7 days, skips
   the shards and writes "reused from run N" to the run summary. Red runs
   leave no marker, so the next night runs again and still files the issue
   (#472). Manual dispatch always runs the full registry. Decision is a pure
   function in scripts/mutation-nightly-reuse.mjs.
2. changed_mutants: the shard plan (already computed before setup, #518) now
   names the environment of its guards (plan-browser=/plan-python=, from
   scripts/mutation-environment.mjs). Python + backend deps only for shards
   with pytest guards, Chromium only for shards whose guards reach
   Playwright; pip wheels cached. Every shard still runs and reports, so the
   six mutant jobs of the review proof are unchanged.

Item 3 of the issue (smoke guards -> node --test) is out of scope here.

Issue: #620
User-Visible: no
2026-09-24 05:48:42 +00:00
claude[bot] 7bb55c2a41 docs: review document for #631
Issue: #631
User-Visible: no
2026-09-24 05:19:42 +00:00
claude[bot] c1cc94f0e0 docs(reviews): индекс после сдвига каталога (#631)
Issue: #631
User-Visible: no
2026-09-24 05:09:24 +00:00
claude[bot] d071ac2e14 docs: review document for #631
Issue: #631
User-Visible: no
2026-09-24 05:09:23 +00:00
Claudeandclaude[bot] 040ad3dc5d test(editors): unit contracts for dialog baselines and form problems (#631)
dialog-baseline.ts and the four *-form-state.ts modules were proven only by
browser smokes. Unit tests on test-build now pin stableKey (top-level order,
transient filter, nested values as-is), no-baseline => dirty, warm transfer,
the transient set of each dialog, and every *Problems code with its order.

room-form-state.ts joins tsconfig.test.json. Three form-state mutants move
from smoke guards to these units; six new mutants cover the new contracts.

Issue: #631
User-Visible: no
2026-09-24 05:09:23 +00:00
claude[bot] 22c7d6340a docs: review document for #632
Issue: #632
User-Visible: no
2026-09-24 05:07:19 +00:00
claude[bot]andClaude 14396ed0b1 docs(reviews): индекс после сдвига каталога (#632)
Issue: #632
User-Visible: no
2026-09-24 07:52:48 +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[bot] b666ac8985 docs: review document for #632
Issue: #632
User-Visible: no
2026-09-24 02:52:09 +00:00
Claudeandclaude[bot] 1fbf7704ca task-packet: trivial short track counts as product flow (#632 r1)
The trivial track (PROCESS §5.1) goes S2 -> S5 without a spec or a spec
review, so in S6/S7 its label is the only product-flow evidence. Without
it a trivial bug whose branch has no class A yet got the same false
"class A forbidden" as #607. productFlowEvidence now takes labels;
new test on the #612 body shape and mutant task-packet-trivial-is-product-flow.

Issue: #632
User-Visible: no
2026-09-24 02:52:09 +00:00
claude[bot] c80f478df9 docs: review document for #632
Issue: #632
User-Visible: no
2026-09-24 02:52:09 +00:00
Claudeandclaude[bot] 10d32312b5 task-packet: track follows issue status and history, not a class-A-free diff
A product issue in S5-S7 whose branch holds only its spec review was
classified as infrastructure and told "class A is forbidden". The track is
now decided by product-flow evidence first (S1-S5 status, "## ТЗ" in the
body, docs/specs file, spec review doc or verdict); the "diff without
class A" heuristic applies only outside the product flow. docs/reviews/**
no longer counts as branch material. Three mutants guard both directions.

Issue: #632
User-Visible: no
2026-09-24 02:52:09 +00:00
claude[bot] c8f9b5d3b4 docs: review document for #634
Issue: #634
User-Visible: no
2026-09-24 02:47:09 +00:00
Claudeandclaude[bot] 7dc7597260 docs(process): ролевые конспекты, замер входа, Snapshot генерируется, TESTING.md разделён
Вход агента до первого файла кода стоил ≈ 26 700 слов (аудит 22.09).

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

Issue: #634
User-Visible: no
2026-09-24 02:33:08 +00:00
claude[bot] fe92ce067a docs: review document for #615
Issue: #615
User-Visible: no
2026-09-24 02:31:24 +00:00
claude[bot] 1d151b5565 docs: review document for #633
Issue: #633
User-Visible: no
2026-09-24 02:23:43 +00:00
Claude af62bd4c64 infra: local contour — pre-push gate:small by default, sandbox bootstrap, test:chunk
- .githooks/pre-push + scripts/pre-push-gate.mjs --hook: gate:small runs by
  default for issue/* branches with an executable diff; C/D-only diffs and
  non-issue refs are skipped, HP_PREPUSH_GATE=0 turns it off, =1 forces it.
  A non-HEAD push with an executable diff is rejected (the gate checks the
  working tree). The gate runs with every GIT_* variable removed: git gives
  hooks GIT_DIR (and GIT_CONFIG_PARAMETERS with the pusher's -c), and unit
  tests that `git init` temporary repositories otherwise write into the real
  one — seen on the first live run. The manual #343 mode is unchanged.
- scripts/sandbox-bootstrap.sh: idempotent worktree / deps / chromium (npm
  @sparticuz/chromium@152.0.0 + Playwright shims) / bundle / check steps,
  no owner paths or credentials.
- scripts/test-chunk.mjs, npm run test:chunk -- N/M: round-robin over
  test/*.test.mjs sorted by code point.
- Tests, seven mutants, docs/DEVELOPMENT.md «Локальный контур за 5 минут»,
  docs/TESTING.md. package.json changed → bundle rebuilt (fingerprint input).

Issue: #633
User-Visible: no
2026-09-24 05:00:45 +03:00
claude[bot] 4de5c0c0b5 docs(reviews): индекс после сдвига каталога (#622)
Issue: #622
User-Visible: no
2026-09-24 01:35:26 +00:00
claude[bot] 067b944337 docs: review document for #622
Issue: #622
User-Visible: no
2026-09-24 01:35:25 +00:00
Claudeandclaude[bot] 1514899746 ci: имена job validate.yml — контракт, сверяемый с файлом; шарды из матрицы (#622)
- scripts/workflow-jobs.mjs: разбор job-уровня validate.yml (id, name,
  inline matrix) без зависимостей; незнакомые формы — громкая ошибка.
- ci-proof: JOB_RULES по id job с именем-контрактом; число и точные имена
  экземпляров — из матрицы YAML (константы count: 6 нет); resolveJobRules
  на каждом evaluateCiProof — переименование даёт failed с названной job;
  jobContractProblems — сверка в обе стороны, UNCONSUMED_JOBS для proof.
- validate-gate: MUTANT_JOB_PREFIX — из контракта ci-proof.
- e2e-gate: E2E_JOB_NAME — зеркало name: из houseplan-e2e e2e.yml
  (43899da5), распознавание выводится из шаблона; missing называет
  завершённые прогоны без job по контракту.
- тесты: фикстуры имён выводятся из validate.yml; новый
  test/workflow-jobs.test.mjs; 5 мутантов в реестре.

Issue: #622
User-Visible: no
2026-09-24 01:35:25 +00:00
claude[bot] 0ca56b4450 docs: review document for #630
Issue: #630
User-Visible: no
2026-09-24 01:34:17 +00:00
Claudeandclaude[bot] 51e1643762 tests_backend: без HA pytest называет число несобранных test_ha_*.py
conftest.py без Home Assistant исключает test_ha_*.py через
collect_ignore_glob — это не skip, в итоговой строке их нет вовсе. Теперь
шапка и итог прогона печатают «HA harness NOT collected: N files (M tests)»
со ссылкой на канон (Linux CI / WSL scripts/wsl-setup.sh --verify). N и M
считаются при каждом запуске по тому же glob.

Тест test_conftest_harness_notice.py проверяет обе ветки conftest в
отдельном процессе pytest (homeassistant перекрыт заглушкой), счёт по
временному каталогу и по реальному харнессу против AST-счёта. Мутанты
ha-harness-notice-silent, ha-harness-notice-count-frozen. TESTING.md и
AGENTS.md: «silently skips» заменено точной формулировкой.

Issue: #630
User-Visible: no
2026-09-24 01:22:22 +00:00
claude[bot] 913420df68 docs: review document for #642
Issue: #642
User-Visible: no
2026-09-24 01:21:00 +00:00
claude[bot] 1babdfb558 docs: review document for #629
Issue: #629
User-Visible: no
2026-09-24 01:19:32 +00:00
claude[bot] 4a5bf2290a docs: review document for #627
Issue: #627
User-Visible: no
2026-09-24 01:15:34 +00:00
claude[bot] 9d26ffe657 docs: review document for #617
Issue: #617
User-Visible: no
2026-09-24 01:14:48 +00:00
claude[bot] adb1727c50 docs: review document for #618
Issue: #618
User-Visible: no
2026-09-24 01:08:12 +00:00
claude[bot] 0f97e3e639 docs: review document for #615
Issue: #615
User-Visible: no
2026-09-24 00:56:26 +00:00
Claude 9d1d27325d docs: refresh screenshot fingerprint for v1.78.0-beta.1
Проверка (CI) / Классификация изменённых файлов (push) Successful in 44s
Проверка (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 1m4s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 23s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 41s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m12s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 45s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 2m28s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m57s
Проверка (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 46s
Все 11 документационных сцен после сборки кандидата попиксельно совпали с принятыми кадрами. Обновлён только source fingerprint; PNG, их хеши и принятый Chromium не менялись.

Release: v1.78.0-beta.1
Issue: #607
Issue: #608
Issue: #609
Issue: #610
Issue: #611
Issue: #612
Issue: #613
Issue: #614
Issue: #619
Issue: #621
Issue: #624
Issue: #625
Issue: #635
Issue: #636
Issue: #637
Issue: #639
Issue: #640
Issue: #641
User-Visible: no
2026-09-23 21:54:11 +03:00
Claude 32552224ea Prepare v1.78.0-beta.1
Кандидат объединяет исправления диалогов и сенсорного ввода, безопасный импорт и хранение, read-only сценарии и обновление палитры мебели. Синхронизированы версия, changelog, release notes и поставляемые bundle-копии.

Release: v1.78.0-beta.1
Issue: #607
Issue: #608
Issue: #609
Issue: #610
Issue: #611
Issue: #612
Issue: #613
Issue: #614
Issue: #619
Issue: #621
Issue: #624
Issue: #625
Issue: #635
Issue: #636
Issue: #637
Issue: #639
Issue: #640
Issue: #641
User-Visible: yes
2026-09-23 21:50:04 +03:00
claude[bot] b0c4e7c32e docs: review document for #624
Issue: #624
User-Visible: no
2026-09-23 18:38:42 +00:00
Claude 47469bab22 Монолит: мёртвый код снят по noUnusedLocals, связность измеряется шестью числами и гейтом (#624)
Карточка и редакторский рантайм держали ≈380 неиспользуемых импортов, 56
мёртвых объявлений и дублей типов (warm-boot, LS_*, GLOW_*, debounce,
navigate, lruRead — копии карточки в рантайме) и 112 приватных членов
карточки, которых не читал никто — делегаты `_editorRuntimeOrThrow()._x()`,
оставшиеся от выноса #425, и аксессоры glow-состояния. Всё это снято; в 9
других файлах — по одиночной ошибке. Делегаты и поля, которых касаются
браузерные смоки (`card._x(...)`), оставлены и посчитаны отдельно.

Гейт `npm run lint:unused` (scripts/unused-locals-gate.mjs, в gate:small и
Validate после сборки): `tsc --noUnusedLocals` чист, кроме приватных членов
карточки из порта HouseplanEditorHostPort / `host.` (portPrivates) и членов,
которых зовёт харнесс (harnessPrivates); храповик по шести числам
scripts/monolith-metrics.mjs против scripts/monolith-baseline.json —
delegates 260→159, portMembers 350, hostRefs 4948, portPrivates 96,
harnessPrivates 107, bundleBytes 2 510 141→2 500 387. `npm run inventory`
печатает те же числа. Заморозка 54 тестов, читающих монолит как текст
(test/monolith-text-anchors.test.mjs); PROCESS.md §2.7 — правило.

Логический исходник для контрактных тестов (test/houseplan-source.mjs)
дописывает члены рантайма без делегата в карточке — контракт продукта не
зависит от наличия заглушки. Потолки ядер и initial gzip опущены на выигрыш
(292 000 → 290 400). Бандл пересобран, три копии синхронны.

Issue: #624
User-Visible: no
2026-09-23 21:09:44 +03:00
claude[bot] 14e56c0399 docs: review document for #641
Проверка (CI) / Классификация изменённых файлов (push) Successful in 47s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 58s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m10s
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 37s
Issue: #641
User-Visible: no
2026-09-23 17:53:54 +00:00
Claude 2b63dd8548 test(golden): закрыть Linux capture без аттестации (#641)
Интеграционный тест запускает настоящий accept.mjs в канонической Linux-среде и подтверждает fail-closed отказ до записи эталонов.

Issue: #641
User-Visible: no
2026-09-23 20:39:56 +03:00
claude[bot] bf3e8a9b3a docs: review document for #641
Issue: #641
User-Visible: no
2026-09-23 17:34:46 +00:00
Claude ee6ca4f3c9 ci: аттестовать локальную WSL-приёмку golden (#641)
Issue: #641
User-Visible: no
2026-09-23 19:16:09 +03:00
claude[bot] 284644acc8 docs: review document for #640
Проверка (CI) / Классификация изменённых файлов (push) Successful in 39s
Проверка (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 1m9s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 25s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m0s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 41s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 42s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 57s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m11s
Проверка (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
Issue: #640
User-Visible: no
2026-09-23 16:03:39 +00:00
claude[bot] 6a905d9ac8 docs(reviews): индекс после сдвига каталога (#640)
Issue: #640
User-Visible: no
2026-09-23 15:49:46 +00:00
Claudeandclaude[bot] 08ff4fbefd chore(golden): принять иконку dishwasher и RU-копию (#640)
Linux CI 35870750208 подтвердил ровно две ожидаемые сцены: новую плитку посудомоечной машины и унаследованную уже слитую RU-копию диалога #610.

Issue: #640
User-Visible: no
Release: v1.78.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/35870750208
2026-09-23 15:49:45 +00:00
Claudeandclaude[bot] 3c860d8159 test(smoke): учесть сырые поля пространства (#640)
Полный прогон для #640 выявил устаревший parity-свидетель после уже слитого #614: прямой путь теперь обязан задавать и сырой текст числовых полей.

Issue: #640
User-Visible: no
2026-09-23 15:49:45 +00:00
Claudeandclaude[bot] 959dda9e48 test(golden): проверить видимость иконки dishwasher (#640)
Issue: #640
User-Visible: no
2026-09-23 15:49:45 +00:00
Claudeandclaude[bot] de8482fde0 test(golden): показать ряд техники в палитре (#640)
Issue: #640
User-Visible: no
2026-09-23 15:49:45 +00:00
Claudeandclaude[bot] 5315ad297d feat(furniture): обновить иконку посудомоечной машины (#640)
Issue: #640
User-Visible: yes
2026-09-23 15:49:45 +00:00
claude[bot] 77bf22f111 docs: review document for #640
Issue: #640
User-Visible: no
2026-09-23 15:49:45 +00:00
claude[bot] 007efaee28 docs: review document for #624
Issue: #624
User-Visible: no
2026-09-23 15:39:00 +00: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[bot] 37879e0c82 docs: review document for #624
Issue: #624
User-Visible: no
2026-09-23 14:29:13 +00:00
claude[bot] fe790ca01f docs: review document for #635
Issue: #635
User-Visible: no
2026-09-23 14:28:09 +00:00
Claude b38bd0c8b5 test(validate): шестой предполётный сигнал — свежесть индекса ревью (#635)
Прогон 35871441981: свидетель «предполётные проверки не прячут друг друга»
(#336) считает continue-on-error по списку шагов; новый шаг reviews_index в
список добавлен.

Issue: #635
User-Visible: no
2026-09-23 17:09:51 +03:00
Claude c9f8b50cd6 reviews-index: гейт свежести индекса — шаг Validate на push в dev, не юнит-тест (#635 r3, повтор)
Прогон 35870123732 на 49bae62e: тест «индекс свеж» покраснел на материале,
который конвейер сам же ребейзнул на dev (#614) — process.yml исполняется из
main и о `--commit-if-stale` ещё не знает; так красился бы любой раунд, пока
правка не отзеркалена, а на issue-ветках коммиты конвейера индекс ветки знать
не обязан. Свежесть судится там, где её держит конвейер: шаг preflight
`reviews-index --check` только на push в dev, в вердикте предполёта; skipped
не считается отказом. Юнит-тест байтовой свежести снят, вместо него — свидетель
на проводке. PROCESS.md §2.10: правка docs/reviews руками сопровождается
пересборкой в том же коммите. INDEX.md пересобран на текущем дереве.

Issue: #635
User-Visible: no
2026-09-23 17:03:32 +03:00
Claudeandclaude[bot] 49bae62e9a reviews-index: свежесть индекса после ребейзов конвейера, первый абзац находки целиком (#635 r3)
r2 H1: INDEX.md — снимок каталога, и ребейз ветки на dev, получивший чужие
документы ревью, устаревал его молча. Теперь `--commit-if-stale` пересобирает
и коммитит индекс коммитом конвейера после приведения к dev (process.yml) и
после ребейза кандидата (merge-candidate.mjs); тест «индекс свеж» сравнивает
закоммиченный файл с пересборкой и красит Validate при расхождении.

r2 M1: находка без заголовка — первый абзац секции, склеенный из перенесённых
строк, без маркера буллета и кода `**M1.**`; «не найдено», служебные скобки
«(унаследовано…)» — не находка. Нумерованные пункты тоже забирают перенесённые
строки. Мутант reviews-index-paragraph-tail. PROCESS.md §2.10 дополнен.

Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
claude[bot] f855cee891 docs: review document for #635
Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
Claudeandclaude[bot] d9bd5a446c reviews-index: счётчики из своего вердикта, находки в живых форматах заголовков, колонка файлов (#635 r2)
r1 H1: parseCounts брал первое «High: N» по тексту (часто цитата чужого
раунда), parseFindings знал только «### H1 — …». Теперь: счётчик — своя
строка «Вердикт» → секция «## Вердикт» → единственное значение → подсчёт по
заголовкам; находки — «### Medium (…) — …», секции с «**M1. …**»,
«## Находка N (High…)», «### [High] …», «### Low L1 — …»; «— нет» не
находка. Новая колонка «Файлы» — пути из находок, чтобы grep по имени файла
отвечал на «что находили по X». Пересказ чужого вердикта строчными в шапке
не перебивает свой. INDEX.md пересобран; мутант reviews-index-counts-first-match.

Перезаход без изменения дерева: dispatch-прогон на 3719a06d покраснел
рассинхроном process.yml main/dev (исправлено зеркалом 0c933449), а
validate-gate считает завершённый красный dispatch на SHA окончательным.

Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
claude[bot] 8c61fe79dc docs: review document for #635
Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
Claudeandclaude[bot] a50cbd8f91 docs(reviews): индекс документов ревью, уроки, пересборка индекса конвейером (#635)
scripts/reviews-index.mjs собирает docs/reviews/INDEX.md: одна строка на
документ — issue, этап, раунд, вердикт (явная строка, раздел «Вердикт»,
свободная форма хвоста; 936 из 986 распознаны), High/Medium по строке вердикта
или заголовкам находок, до шести заголовков находок. Индекс детерминирован,
не индексирует сам себя, перечисляет файлы вне схемы имён; `--check` — гейт
свежести. process.yml публикует INDEX.md тем же коммитом, что документ ревью.

docs/LESSONS.md — датированные уроки со ссылками на источники (12 записей из
аудитов и разборов недели). PROCESS.md §2.10 — где искать решения.

Тесты: разбор имён, вердиктов, счётчиков, находок; фикстурный каталог;
живой каталог (100 % покрытие, >90 % вердиктов); контракт шага конвейера.
Мутанты reviews-index-skips-self-check, reviews-index-verdict-substring.

Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
claude[bot] f234855e66 docs: review document for #614
Issue: #614
User-Visible: no
2026-09-23 13:24:08 +00:00
Claude a2d6371545 test(settings): cover dialog problem boundaries (#614)
Issue: #614
User-Visible: no
2026-09-23 16:10:09 +03:00
claude[bot] 86c765d99e docs: review document for #614
Issue: #614
User-Visible: no
2026-09-23 13:06:50 +00:00
Claude bc6b243f56 fix(types): type warm dialog payload without new any (#614)
Issue: #614
User-Visible: no
2026-09-23 15:46:55 +03:00
Claude 1a6b12af86 fix(settings): validate drafts and preserve dialog baselines (#614)
Issue: #614
User-Visible: yes
2026-09-23 15:27:51 +03:00
claude[bot] 5ac596f93c docs: review document for #614
Issue: #614
User-Visible: no
2026-09-23 12:03:11 +00:00
claude[bot] 9beb6d45a0 docs: review document for #625
Issue: #625
User-Visible: no
2026-09-23 11:49:34 +00:00
Claude 53b20e8c45 test(backend): покрыть marker-id write boundaries (#625)
Issue: #625
User-Visible: no
2026-09-23 14:30:39 +03:00
claude[bot] f10dd1ebba docs: review document for #625
Issue: #625
User-Visible: no
2026-09-23 11:21:18 +00:00
Claude 15bc6bcf5a fix(backend): исправить CI-контракты upload (#625)
Issue: #625
User-Visible: no
2026-09-23 13:54:20 +03:00
Claude 4051899b9b test(backend): обновить якоря мутаций upload (#625)
Issue: #625
User-Visible: no
2026-09-23 13:33:58 +03:00
Claude 965bbb05a8 fix(backend): укрепить I/O и инварианты хранилища (#625)
Issue: #625
User-Visible: yes
2026-09-23 13:27:35 +03:00
claude[bot]andClaude efa2a50665 docs: review document for #625
Issue: #625
User-Visible: no
2026-09-23 13:27:35 +03:00
claude[bot]andClaude 5a38bea3ff docs: review document for #625
Issue: #625
User-Visible: no
2026-09-23 13:27:35 +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[bot] 0672c2b2ff docs: review document for #621
Issue: #621
User-Visible: no
2026-09-23 09:50:57 +00:00
Claude 50e67c0988 process.yml: счёт раундов ревью перечисляет docs/reviews через Git Trees API (#621)
У `contents` API потолок 1 000 записей с молчаливой обрезкой; каталог подошёл к
нему (986 файлов). Guard теперь спускается по дереву commit → docs → reviews и
трактует `truncated` как отказ листинга (счёт по файлам отключается, страховка
по комментариям остаётся). Предупреждение о потолке снято. Тесты: фикстура на
2 400+ имён со своими документами в хвосте; свидетель на проводке workflow.

Issue: #621
User-Visible: no
2026-09-23 12:38:57 +03:00
claude[bot] b1f1f6b754 docs: review document for #639
Проверка (CI) / Классификация изменённых файлов (push) Successful in 43s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 1m2s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m9s
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 31s
Issue: #639
User-Visible: no
2026-09-23 09:21:53 +00:00
Claude a3397329d5 test(smoke): коммитить размер шрифта пространства через DOM (#639)
Issue: #639
User-Visible: no
2026-09-23 12:06:10 +03:00
claude[bot]andClaude 368ea35f7f docs: review document for #639
Issue: #639
User-Visible: no
2026-09-23 12:04:17 +03:00
Claude 9cf848c143 test(smoke-links): правка range-line/unitInput выбирает три смока форм настроек (#639)
#608 поменял событие коммита числа в form-kit, а смоки форм комнаты и
устройства красными увидел только ночной реестр: smoke-select связывал
form-kit лишь с новым smoke_range_line_draft. Явная связь rangeLine/unitInput →
три смока форм, чтобы правка набора контролов гонялась на кандидате ревью.

Issue: #639
User-Visible: no
2026-09-23 12:04:17 +03:00
Claude eecb5291d0 ci: зеркало еженедельных метрик процесса в main (#637)
Issue: #637
User-Visible: no
2026-09-23 11:58:52 +03:00
claude[bot] 4366a1c796 docs: review document for #637
Проверка (CI) / Классификация изменённых файлов (push) Successful in 38s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (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) Skipped
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 57s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 2m0s
Проверка (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) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
Проверка (CI) / Доказательство выполненных проверок (push) Failing after 28s
Issue: #637
User-Visible: no
2026-09-23 08:57:40 +00:00
Claude 44aa358060 fix(metrics): сохранить нулевые медианы процесса (#637)
Issue: #637
User-Visible: no
2026-09-23 11:46:16 +03:00
claude[bot]andClaude 5df8cfc0c1 docs: review document for #637
Issue: #637
User-Visible: no
2026-09-23 11:42:37 +03:00
Claude 46edcb1f37 ci(metrics): еженедельный замер процесса одним скриптом (#637)
scripts/process-metrics.mjs — чистые функции над снимками GitHub: по issue
(таймлайн меток) вход по первой статусной метке, S4→S5, вход→S7, S7→S8,
повторные постановки S7; раунды ревью — по документам docs/reviews, не по
событиям метки (конвейер с #636 ставит S7 сам); прогоны Actions по workflow —
исходы, wall-time, события (прогоны конвейера сведены в одну строку); минуты
S4/S7 конвейера без skipped; при наличии jobs — job-минуты и доля «Мутанты».
Markdown-отчёт со сводкой и таблицей по issue; CLI на gh (только чтение).

process-metrics.yml — понедельник 05:00 UTC и по кнопке; отчёт в step summary
и артефакт на 90 дней; прав на запись нет.

Тесты на фикстурах, в т. ч. воспроизведение формы аудита 22.09 (30 issue:
15 r1/13 r2/2 r3 → 1,57; Validate 226/178/37/11). Мутанты
metrics-count-skipped-pipeline-runs, metrics-rounds-by-s7-events.

Issue: #637
User-Visible: no
2026-09-23 11:42:37 +03:00
claude[bot] 0b55163dc3 docs: review document for #612
Проверка (CI) / Классификация изменённых файлов (push) Successful in 37s
Проверка (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
Проверка (CI) / HACS: валидация репозитория (push) Failing after 22s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 23s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m1s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 39s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 1m0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m56s
Проверка (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 18s
Issue: #612
User-Visible: no
2026-09-23 07:52:28 +00:00
Claude 2cfb6d2169 fix: protect View config recovery (#612)
Issue: #612
User-Visible: yes
2026-09-23 10:34:09 +03:00
claude[bot] 78698a075a docs: review document for #610
Issue: #610
User-Visible: no
2026-09-23 07:22:36 +00: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
claude[bot] 6647ef5677 docs: review document for #610
Issue: #610
User-Visible: no
2026-09-23 06:36:39 +00:00
Claudeandclaude[bot] e08a7b4886 test: align device form smoke with atomic inputs (#610)
Issue: #610
User-Visible: no
2026-09-23 06:36:39 +00:00
Claudeandclaude[bot] 1dba818581 fix: clarify unsaved-settings confirmation (#610)
Issue: #610
User-Visible: yes
2026-09-23 06:36:39 +00:00
claude[bot] fb615f9d5d docs: review document for #610
Issue: #610
User-Visible: no
2026-09-23 06:36:39 +00:00
claude[bot] ee0f97c85c docs: review document for #636
Issue: #636
User-Visible: no
2026-09-23 06:16:09 +00:00
Claude 351fef43d6 ci(process): раунд ревью ждёт Validate событием, а не сном раннера (#636)
Стадия prepare спала ≈ 28 минут на раунд, пока шёл Validate с мутантами на
материале (модель работает 10–12); за неделю ≈ 420–500 job-минут простоя и
потолок бюджета стадии 55 минут.

- validate-gate.mjs: `--no-wait` — гейт диспатчит прогон, убеждается, что тот
  встал на материал (#539 сохранён), и возвращает `pending` (код 2) вместо
  ожидания; завершённый зелёный/красный отдаёт сразу, как прежде.
- process.yml prepare: третий исход `proceed=pending`: запечатанный маркер
  `review-pending-<issue>-<run>-<attempt>` (issue, stage, branch, material_sha,
  validate run) и выход; модель и интеграция не запускаются; возврат автору —
  только на явном `false`.
- process-resume.yml + scripts/process-resume.mjs: на `workflow_run: completed`
  Validate по ветке issue/* — если метка S7 стоит, активного прогона нет и
  последний прогон оставил маркер на этот SHA, переставить S7 (HP_PROCESS_TOKEN);
  новый прогон находит завершённый dispatch сразу. Без маркера не будит.
- process-reconcile.mjs: читает маркер и состояние Validate на материале;
  идёт — wait, завершился/пропал без продолжения — retry; без маркера — прежний
  escalate. Общий loadSealedArtifact, экспорт processRuns/artifactNames.
- preflight сверяет process-resume.yml между main и dev наравне с process.yml.
- Тесты: validate-gate (4), process-resume (8, включая контракт трёх workflow),
  process-reconcile (2); мутанты gate-no-wait-still-sleeps,
  resume-wakes-round-without-marker, resume-ignores-active-run,
  reconcile-wakes-pending-while-validate-active. PROCESS.md §10.4, AGENTS.md.

Issue: #636
User-Visible: no
2026-09-23 08:51:17 +03:00
claude[bot] e29dfdeb72 docs: review document for #608
Issue: #608
User-Visible: no
2026-09-23 05:04:32 +00:00
Claude edfe8af52a docs: clarify slider input changelog (#608)
Issue: #608
User-Visible: yes
2026-09-23 07:56:48 +03:00
claude[bot] 0d969a5348 docs: review document for #608
Issue: #608
User-Visible: no
2026-09-23 04:56:00 +00:00
Claude 8a4ecdea63 fix: commit slider-number input atomically (#608)
Issue: #608
User-Visible: yes
2026-09-23 07:37:30 +03:00
claude[bot] 551ff2f563 docs: review document for #608
Issue: #608
User-Visible: no
2026-09-23 04:25:51 +00:00
claude[bot] 0b29ccb91d docs: review document for #619
Issue: #619
User-Visible: no
2026-09-23 03:20:15 +00:00
Claude 0d047450ce fix(ci): стабилизировать релизный proof на main (#619)
Issue: #619
User-Visible: no
2026-09-23 06:09:08 +03:00
claude[bot] 0e0a0e54e5 docs: review document for #613
Issue: #613
User-Visible: no
2026-09-23 03:01:31 +00:00
Claude e95936cdce fix(touch): refresh device hits and defer pinch zoom save (#613)
Issue: #613
User-Visible: yes
2026-09-23 05:35:41 +03:00
claude[bot] 59fdae817a docs: review document for #613
Issue: #613
User-Visible: no
2026-09-23 02:25:10 +00:00
claude[bot] d36b7f80c3 docs: review document for #613
Issue: #613
User-Visible: no
2026-09-23 02:14:02 +00:00
claude[bot] 1241b9499c docs: review document for #611
Issue: #611
User-Visible: no
2026-09-23 01:59:49 +00:00
Claude 9f82960c2c fix(import): remap vacuum routes to copied space (#611)
Repair both incoming routes and safe target-reference recovery, with backend
and mutation coverage for foreign and same-instance imports.

Issue: #611
User-Visible: yes
2026-09-23 04:40:52 +03:00
claude[bot] 2a21ff65d4 docs: review document for #611
Issue: #611
User-Visible: no
2026-09-23 01:34:07 +00:00
claude[bot] 37aed2c9b1 docs: review document for #611
Issue: #611
User-Visible: no
2026-09-23 01:22:40 +00:00
claude[bot] 61292c9ea0 docs: review document for #609
Issue: #609
User-Visible: no
2026-09-23 01:08:16 +00:00
Claude e8e5c863e4 test(dialog): cover HA theme and enlarged text (#609)
Extend the authentic pinned-HA diagnostic with explicit light and dark palettes, a 32 px root-font case, one-scroller geometry assertions, and reviewed evidence images. Correct stale AC references in the mutation descriptions.

Issue: #609
User-Visible: no
2026-09-23 03:55:27 +03:00
claude[bot] 52a8f58909 docs: review document for #609
Issue: #609
User-Visible: no
2026-09-23 00:51:34 +00:00
Claude 4b27268e2f test(dialog): strengthen mobile fullscreen witness (#609)
The mobile mutant now changes both public height bounds, so the browser test observes the intended 48 px regression instead of CSS min-height precedence masking it.

Issue: #609
User-Visible: no
2026-09-23 03:23:09 +03:00
Claude 4c9e013276 fix(dialog): align HA settings form shell (#609)
Use Home Assistant's public dialog tokens for the reviewed 560 px canvas, height cap, single scroller and mobile fullscreen layout. Keep generic dialogs and the native fallback unchanged, with authentic-HA evidence and mutation witnesses.

Issue: #609
User-Visible: yes
2026-09-23 03:18:37 +03:00
claude[bot] 6569dced23 docs: review document for #609
Проверка (CI) / Классификация изменённых файлов (push) Successful in 25s
Проверка (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 37s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 29s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 28s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m9s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 41s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Failing after 43s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 1m49s
Проверка (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
Issue: #609
User-Visible: no
2026-09-22 23:58:16 +00:00
claude[bot] 84cffb3f92 docs: review document for #607
Issue: #607
User-Visible: no
2026-09-22 23:42:32 +00:00
Claude fc70a3fcf4 test(dialog): register rejected-close mutants (#607)
Issue: #607
User-Visible: no
2026-09-23 02:25:57 +03:00
claude[bot] 4382817e55 docs: review document for #607
Issue: #607
User-Visible: no
2026-09-22 23:21:59 +00:00
Claude 9bbf6fb248 fix(dialog): restore HA modal after rejected close (#607)
Issue: #607
User-Visible: yes
2026-09-23 02:00:48 +03:00
claude[bot] d70b67abc4 docs: review document for #607
Issue: #607
User-Visible: no
2026-09-22 22:45:18 +00:00
2349 changed files with 129821 additions and 35288 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
+28 -14
View File
@@ -30,20 +30,14 @@ else
fi
status=0
gate_status=0
# Локальный набор гейтов (#343). Выключен по умолчанию намеренно: 20-45 секунд на
# каждый пуш, включая пуши одной строки документации, — цена, которую стоит
# платить осознанно. Документация: docs/TESTING.md.
if [ "${HP_PREPUSH_GATE:-}" = "1" ] && [ -f "$repo_root/scripts/pre-push-gate.mjs" ]; then
echo "pre-push-gate: HP_PREPUSH_GATE=1, прогоняю локальный набор" >&2
if ! node "$repo_root/scripts/pre-push-gate.mjs" >&2; then
status=1
fi
fi
# Строки git читаются один раз: их нужно и локальному набору, и процессному гейту.
refs=$(cat)
while read -r local_ref local_sha remote_ref remote_sha; do
# Deleting a remote branch pushes nothing to examine.
if [ "$local_sha" = "$zero" ]; then
# An empty line (nothing on stdin) and deleting a remote branch push nothing to examine.
if [ -z "$local_sha" ] || [ "$local_sha" = "$zero" ]; then
continue
fi
@@ -71,10 +65,25 @@ while read -r local_ref local_sha remote_ref remote_sha; do
if ! node "$gate" --range "${base}..${local_sha}" --target-ref "$remote_ref" $issues_flag >&2; then
status=1
fi
done
done <<EOF
$refs
EOF
# Локальный набор gate:small (#633, прежде opt-in #343) — после процессного
# гейта: тот отвечает за секунды, а набор идёт минуты; прогоняются оба, чтобы
# автор узнал обо всех провалах одним кругом. По умолчанию включён для
# веток issue/* с исполняемым диффом; дифф только класса C/D (документы ревью,
# changelog, бандл) и ветки вне issue/* его не запускают. Выключение — явное:
# HP_PREPUSH_GATE=0; HP_PREPUSH_GATE=1 — прогнать для любой ветки. Документация:
# docs/TESTING.md, docs/DEVELOPMENT.md «Локальный контур за 5 минут».
if [ -f "$repo_root/scripts/pre-push-gate.mjs" ]; then
if ! printf '%s\n' "$refs" | node "$repo_root/scripts/pre-push-gate.mjs" --hook >&2; then
gate_status=1
fi
fi
if [ "$status" -ne 0 ]; then
cat >&2 <<'EOF'
cat >&2 <<'MSG'
Push остановлен: нарушен процесс (PROCESS.md §10.2).
@@ -84,7 +93,12 @@ Push остановлен: нарушен процесс (PROCESS.md §10.2).
Обойти проверку можно через `git push --no-verify`, и тогда то же самое найдёт
job `process-gate` в Validate — уже после того, как код окажется в dev.
EOF
MSG
fi
if [ "$gate_status" -ne 0 ]; then
echo "Push остановлен: красный локальный набор gate:small (см. вывод выше)." >&2
exit 1
fi
exit "$status"
+228
View File
@@ -0,0 +1,228 @@
name: "Бета: производные артефакты на dev · тело (#716)"
# #697, PROCESS.md §8 и §11.4: ветки задач не коммитят ни отпечаток и кадры
# скриншотов документации, ни эталоны golden. Их обновляет один коммит бота на
# `dev` перед кандидатом беты; коммит проверяет релиз-менеджер.
#
# - Скриншоты: съёмка на `dev` тем же каноническим способом, что
# `docs-screenshots.yml`, и приёмка `docs:accept --reviewed`. Кадры, которые
# менять не собирались, обязаны совпасть байт-в-байт (свидетели среды);
# изменившийся кадр принимается, только если назван в `docs_expect_change`.
# Ничего не изменилось — обновляется один отпечаток исходников.
# - Golden: артефакт `golden-images` полного Validate на `dev`
# (`golden_run`) и `golden:accept --reviewed` с объявленными сценами. Коммит
# несёт `Release:` и `Baseline-Reviewed:` — провенанс, который требует
# validate-commit-provenance.mjs.
#
# Необъявленная разница — отказ с перечнем кадров, артефакт съёмки остаётся
# для просмотра: решение, какие кадры сдвинулись законно, принимает человек.
on:
# #716: тело вызывается тонким файлом `beta-derived.yml` по ссылке `@dev`;
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
workflow_call:
inputs:
tag:
description: "Beta tag these artifacts are for, for example v1.79.0-beta.1"
required: true
type: string
docs_expect_change:
description: "Doc scenario ids expected to change, comma-separated (empty = none)"
required: false
type: string
default: ""
golden_run:
description: "Validate run id on dev whose golden-images artifact holds the shifted frames (empty = skip golden)"
required: false
type: string
default: ""
golden_expect_change:
description: "Golden scenes expected to change, comma-separated"
required: false
type: string
default: ""
golden_expect_new:
description: "New golden scenes, comma-separated"
required: false
type: string
default: ""
permissions:
contents: read
# Concurrency уровня workflow — у вызывающего `beta-derived.yml` (#716).
jobs:
accept:
name: "Отпечаток, кадры и эталоны — одним коммитом в dev"
runs-on: ubuntu-24.04
timeout-minutes: 40
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- run: npm ci
- name: Кэш браузеров Playwright
id: pw
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install pinned Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install chromium
- name: Build the bundle the screenshots must come from
run: npm run build
# Тот же закреплённый упаковщик, что у docs-screenshots.yml: другой
# переписал бы все кадры без единого изменённого пикселя.
- name: Установить oxipng
env:
OXIPNG_VERSION: 10.2.0
OXIPNG_SHA256: b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e
run: |
set -euo pipefail
asset="oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -o "$asset" \
"https://github.com/oxipng/oxipng/releases/download/v${OXIPNG_VERSION}/${asset}"
echo "${OXIPNG_SHA256} ${asset}" | sha256sum -c -
mkdir -p "$HOME/.local/bin"
tar -xzf "$asset" --strip-components=1 -C "$HOME/.local/bin" \
"oxipng-${OXIPNG_VERSION}-x86_64-unknown-linux-gnu/oxipng"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/oxipng" --version
- name: "Съёмка воспроизводима между прогонами (#410, #422)"
run: node scripts/capture-determinism.mjs
- name: Кадры документации — съёмка и приёмка
id: docs
env:
EXPECT: ${{ inputs.docs_expect_change }}
run: |
set -euo pipefail
node demo/docs/capture.mjs --stability=3
cand="$RUNNER_TEMP/docs-candidate"
rm -rf "$cand" && mkdir -p "$cand"
cp docs/images/*.png docs/images/screenshots.json "$cand/"
# Приёмка сравнивает кандидата с ЗАКОММИЧЕННЫМИ кадрами: рабочая
# копия возвращается к dev, и заменить файлы может только она.
git checkout -- docs/images
git clean -fdq -- docs/images
args=(--reviewed "--from=$cand")
if [ -n "$EXPECT" ]; then args+=("--expect-change=$EXPECT"); fi
node scripts/docs-accept.mjs "${args[@]}"
if git diff --quiet -- docs/images; then changed=false; else changed=true; fi
echo "changed=$changed" >> "$GITHUB_OUTPUT"
git diff --stat -- docs/images
- name: Сохранить кандидата скриншотов для просмотра
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docs-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/docs-candidate
if-no-files-found: ignore
retention-days: 7
- name: Эталоны golden из прогона Validate
id: golden
if: inputs.golden_run != ''
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ inputs.golden_run }}
EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
EXPECT_NEW: ${{ inputs.golden_expect_new }}
run: |
set -euo pipefail
case "$RUN" in ''|*[!0-9]*) echo "::error::golden_run — числовой id прогона Validate"; exit 1 ;; esac
meta=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN" \
--jq '[.path, .head_branch, .status, .html_url] | @tsv')
IFS=$'\t' read -r path branch status url <<< "$meta"
# Провенанс эталонов — завершённый Validate на dev: другой workflow
# или ветка задачи доказательством для кандидата беты не являются.
if [ "$path" != ".github/workflows/validate.yml" ] || [ "$branch" != "dev" ] || [ "$status" != "completed" ]; then
echo "::error::прогон $RUN — не завершённый Validate на dev ($path, $branch, $status)"
exit 1
fi
from="$RUNNER_TEMP/golden-candidate"
rm -rf "$from" && mkdir -p "$from"
gh run download "$RUN" --repo "${{ github.repository }}" -n golden-images -D "$from"
# Отчёт обязан быть снят с этого же дерева: accept.mjs сверяет его
# отпечаток с исходниками и откажет, если dev ушёл вперёд.
args=(--reviewed "--from=$from")
if [ -n "$EXPECT_CHANGE" ]; then args+=("--expect-change=$EXPECT_CHANGE"); fi
if [ -n "$EXPECT_NEW" ]; then args+=("--expect-new=$EXPECT_NEW"); fi
node scripts/golden-accept.mjs "${args[@]}"
if git diff --quiet -- demo/golden/baselines; then changed=false; else changed=true; fi
{ echo "changed=$changed"; echo "url=$url"; } >> "$GITHUB_OUTPUT"
git diff --stat -- demo/golden/baselines
- name: Коммит в dev
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
TAG: ${{ inputs.tag }}
DOCS_CHANGED: ${{ steps.docs.outputs.changed }}
DOCS_EXPECT: ${{ inputs.docs_expect_change }}
GOLDEN_CHANGED: ${{ steps.golden.outputs.changed }}
GOLDEN_URL: ${{ steps.golden.outputs.url }}
GOLDEN_EXPECT_CHANGE: ${{ inputs.golden_expect_change }}
GOLDEN_EXPECT_NEW: ${{ inputs.golden_expect_new }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Хук pre-push гоняет process-gate; gate:small здесь не нужен —
# продуктового кода коммит не несёт.
HP_PREPUSH_GATE: "0"
run: |
set -euo pipefail
git add -- docs/images demo/golden/baselines
if git diff --cached --quiet; then
echo "::notice::отпечаток, кадры и эталоны на dev уже свежие — коммитить нечего"
exit 0
fi
msg="$RUNNER_TEMP/message.txt"
{
echo "docs: accept derived artifacts on dev for $TAG"
echo ""
echo "Производные артефакты беты — одним коммитом на dev (PROCESS.md §8, §11.4, #697)."
echo "Прогон: $RUN_URL"
if [ "$DOCS_CHANGED" = "true" ]; then
echo "Скриншоты документации: отпечаток исходников; изменённые кадры: ${DOCS_EXPECT:-нет}."
fi
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Golden: изменённые сцены: ${GOLDEN_EXPECT_CHANGE:-нет}; новые: ${GOLDEN_EXPECT_NEW:-нет}."
fi
echo ""
if [ "$GOLDEN_CHANGED" = "true" ]; then
echo "Release: $TAG"
echo "Baseline-Reviewed: $GOLDEN_URL"
fi
echo "Issue: #697"
echo "User-Visible: no"
} > "$msg"
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F "$msg"
git show --stat --format='%H %s' HEAD | head -40
# #730: сдвигом dev считается только устаревший lease — отказ
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705).
# Скрипт — из dev: рабочая копия и есть dev. Сдвиг — прежний совет
# перезапустить; отказ GitHub (право на workflow, правило ветки, хук)
# перезапуск не лечит: причина и ответ git без токена — в журнале и в
# сводке шага.
push_err="$RUNNER_TEMP/beta-derived-push.stderr"
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
--stage=beta-derived --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
if [ "$kind" = "stale" ]; then
echo "::error::dev ушёл вперёд за время съёмки — запустить workflow заново: отпечаток судит дерево, а оно уже другое"
else
echo "::error::push производных артефактов в dev отклонён ($kind) — это не сдвиг dev, перезапуск не поможет; причина и ответ git — выше и в сводке шага"
fi
exit 1
fi
echo "### Производные артефакты $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Коммит \`$(git rev-parse --short HEAD)\` в dev — проверить перед кандидатом беты." >> "$GITHUB_STEP_SUMMARY"
+357
View File
@@ -0,0 +1,357 @@
name: "Мутационный гейт · тело (#623)"
# Реестр известных поломок (issue #85): каждый мутант ломает продуктовый код
# известным способом, и объявленный тест ОБЯЗАН на этом покраснеть. Тест,
# оставшийся зелёным на сломанном коде, ничего не защищает — он лишь выглядит
# защитой, и это хуже его отсутствия.
#
# Прогон дорогой и проверяет не продукт, а тесты, поэтому он не входит ни в
# Validate, ни в цикл разработки, ни в релизный гейт (#513, решение владельца
# 09.09): каждую ночь по расписанию, отказ — issue с отчётом (#472).
# Мутанты, задетые диффом, конвейер ревью и слияние гоняют отдельно на своём
# кандидате (#510); кандидат беты, `full=true` и ночной Validate их не
# запрашивают (#601) — ночью достаточно этого полного реестра.
# Дешёвая половина — «якоря патчей живы, guard-файлы существуют» — идёт с
# обычными юнитами: test/mutation-gate.test.mjs.
#
# #332: бандл собирается только мутантам с браузерным гвардом (guardNeedsBundle),
# компиляция тестов в worktree стартует с тёплого test-build (инкрементальный
# tsc), а реестр режется на четыре чересполосных шарда — полный прогон
# укладывается в десятки минут вместо часов. Локальный дифф-режим:
# node scripts/mutation-gate.mjs --changed origin/dev..HEAD
#
# #620: ночь по расписанию на дереве, уже доказанном зелёным полным прогоном
# (тот же tree материала, тот же SHA workflow, маркер не старше недели), шарды не
# гоняет — в сводке «reused from run N». Маркер пишет только зелёный агрегатор,
# поэтому красный не переносится: следующая ночь гонит реестр заново и снова
# заводит issue (#472). Ручной dispatch гонит полный реестр всегда. Решение —
# чистая функция scripts/mutation-nightly-reuse.mjs.
#
# #623: «SHA workflow» — `job.workflow_sha`, SHA этого файла-тела. В вызываемом
# workflow `github.workflow_sha` принадлежит вызывающему `mutation-gate.yml` из
# main и не меняется вместе с телом, поэтому здесь он не используется.
on:
# #623: тело вызывается тонким файлом `mutation-gate.yml` из ветки по умолчанию
# по ссылке `@dev`; триггеры, run-name и concurrency живут там.
workflow_call:
inputs:
ref:
description: "Git ref whose mutation guards must be proved"
required: false
type: string
default: dev
permissions:
contents: read
# Concurrency уровня workflow — у вызывающего `mutation-gate.yml` (#623).
jobs:
# #549: moving ref разрешается ровно один раз. Все шарды ниже получают один
# commit/tree, а не самостоятельно читают dev в разное время.
material:
name: "Зафиксировать неизменяемый материал"
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
sha: ${{ steps.identity.outputs.sha }}
tree: ${{ steps.identity.outputs.tree }}
ref: ${{ steps.identity.outputs.ref }}
reuse: ${{ steps.reuse.outputs.reuse }}
reused_run: ${{ steps.reuse.outputs.reused_run }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }}
fetch-depth: 0
- name: Зафиксировать commit и tree
id: identity
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"
echo "ref=${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }}" >> "$GITHUB_OUTPUT"
# #620: самый свежий маркер зелёного прогона этого tree и этого workflow.
# Ключ уникален на прогон (кэш неизменяем), восстанавливается по префиксу —
# иначе перепроверка после недели не смогла бы освежить маркер.
- name: Маркер зелёного прогона этого дерева
if: github.event_name == 'schedule'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: artifacts/mutation-green
key: mutation-green-v1-${{ steps.identity.outputs.tree }}-${{ job.workflow_sha }}-${{ github.run_id }}
restore-keys: |
mutation-green-v1-${{ steps.identity.outputs.tree }}-${{ job.workflow_sha }}-
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
# Любой сбой решения — полный прогон: пропуск должен быть доказан, а не
# выведен из ошибки. Материал без скрипта (dispatch старого ref) — тоже.
- name: Нужен ли прогон
id: reuse
env:
EVENT_NAME: ${{ github.event_name }}
TREE: ${{ steps.identity.outputs.tree }}
WORKFLOW_SHA: ${{ job.workflow_sha }}
RUN_URL_BASE: ${{ github.server_url }}/${{ github.repository }}/actions/runs
run: |
decision="$RUNNER_TEMP/mutation-reuse.txt"
if [ -f scripts/mutation-nightly-reuse.mjs ] && node scripts/mutation-nightly-reuse.mjs --decide \
--event="$EVENT_NAME" --tree="$TREE" --workflow-sha="$WORKFLOW_SHA" \
--marker=artifacts/mutation-green/marker.json \
--summary="$GITHUB_STEP_SUMMARY" --run-url-base="$RUN_URL_BASE" > "$decision"; then
cat "$decision" >> "$GITHUB_OUTPUT"
if grep -qx 'reuse=true' "$decision"; then
echo "::notice::reused from run $(sed -n 's/^reused_run=//p' "$decision")"
fi
else
echo "::warning::решение о повторном использовании не получено — полный прогон"
echo "reuse=false" >> "$GITHUB_OUTPUT"
fi
mutants:
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 6)"
needs: material
if: needs.material.outputs.reuse != 'true'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
# Шесть чересполосных шардов (#604): при четырёх шард нёс ~203 мутанта из
# 810 и рос с реестром — 42 мин 10.09, 57 мин 20.09, 61 мин 21.09, и шард
# 2/4 был снят по потолку без единого FAIL. Делитель тот же, что у
# `changed_mutants` в Validate; число шардов повторяется в `--shard=i/6`,
# `--shards=6` и имени job — тест `mutation-gate.test.mjs` держит их
# равными. Бандл собирают только браузерные гварды. Час — потолок против
# зависшего Chromium, а не бюджет шарда: шард, упёршийся в него, — сигнал
# снова делить, и отчёт (#472) называет такой шард прерванным, не «ok».
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.material.outputs.sha }}
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- run: npm ci
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: '3.14'
- name: Установить backend test dependencies
run: |
# Точные версии — в tests_backend/requirements.txt (#392): без них
# резолвер выбирает набор сам, и «зелёный backend» значит разное
# в разные дни.
pip install -r tests_backend/requirements.txt
pip list --format=columns | grep -Ei 'homeassistant|voluptuous|^pytest '
- 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 --with-deps chromium
- name: Реестр применим к текущему коду
run: node scripts/mutation-gate.mjs --check
- name: Тёплый test-build для инкрементальной компиляции мутантов
run: npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs
# Вывод шарда сохраняется артефактом (#472): строки
# `FAIL <id>: тест остался зелёным…` — единственное место, где названо,
# ЧТО сбежало. Без артефакта отказ безымянный. `PIPESTATUS` — чтобы
# `tee` не съел код выхода раннера.
- name: Каждый тест ловит свою поломку
id: gate
run: |
mkdir -p artifacts/mutation-shard-${{ matrix.shard }}
set -o pipefail
node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/6 2>&1 | tee artifacts/mutation-shard-${{ matrix.shard }}/mutation-shard-${{ matrix.shard }}.log
# Исход шага едет в evidence (#604): снятый по timeout-minutes шаг даёт
# `cancelled`, и агрегатор отвергает такой шард как неполный — лог без
# строк FAIL сам по себе зелёным не считается.
- name: Записать identity шарда
if: always()
run: |
node scripts/mutation-gate-report.mjs \
--write-evidence=artifacts/mutation-shard-${{ matrix.shard }}/evidence.json \
--sha=${{ needs.material.outputs.sha }} \
--tree=${{ needs.material.outputs.tree }} \
--workflow-sha=${{ job.workflow_sha }} \
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
--shard=${{ matrix.shard }} --shards=6 \
--outcome=${{ steps.gate.outcome }}
- name: Сохранить лог и identity шарда
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mutation-shard-${{ matrix.shard }}-attempt-${{ github.run_attempt }}
path: artifacts/mutation-shard-${{ matrix.shard }}
if-no-files-found: warn
retention-days: 30
# Результат нельзя приписывать material, пока не доказаны все шесть шардов —
# каждый с identity и с дошедшим до конца прогоном (#549, #604).
# always() нужен при красном мутанте: лог красного шарда всё равно evidence.
evidence:
name: "Доказать единый material всех шардов"
needs: [material, mutants]
if: always() && needs.material.outputs.reuse != 'true'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# CLI отчётчика принадлежит зафиксированному material. В main лежит
# только workflow и может не быть dev-версии scripts/**.
ref: ${{ needs.material.outputs.sha }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Забрать evidence всех попыток
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: mutation-shard-*
path: artifacts/mutation-logs
- name: Проверить полноту и identity
run: |
node scripts/mutation-gate-report.mjs --verify-only \
--logs=artifacts/mutation-logs --shards=6 \
--sha=${{ needs.material.outputs.sha }} \
--tree=${{ needs.material.outputs.tree }} \
--workflow-sha=${{ job.workflow_sha }} \
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }}
# #620: маркер пишется ТОЛЬКО после зелёного агрегатора — все шесть шардов
# доказаны на одном material. Красный или неполный прогон маркера не оставляет,
# и следующая ночь гонит реестр заново.
green_marker:
name: "Записать маркер зелёного прогона"
needs: [material, mutants, evidence]
if: needs.material.outputs.reuse != 'true' && needs.mutants.result == 'success' && needs.evidence.result == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.material.outputs.sha }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Маркер
run: |
node scripts/mutation-nightly-reuse.mjs \
--write-marker=artifacts/mutation-green/marker.json \
--tree=${{ needs.material.outputs.tree }} \
--sha=${{ needs.material.outputs.sha }} \
--workflow-sha=${{ job.workflow_sha }} \
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
--event=${{ github.event_name }}
- name: Сохранить маркер
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: artifacts/mutation-green
key: mutation-green-v1-${{ needs.material.outputs.tree }}-${{ job.workflow_sha }}-${{ github.run_id }}
# Адресат у отказа (#472). Только по расписанию: ручной dispatch остаётся
# для отладки самого гейта, его результат смотрят в прогоне — issue на
# каждый такой отказ был бы шумом, который снова перестанут читать.
#
# Права job-уровня ЗАМЕНЯЮТ права workflow, а не дополняют (прецедент —
# validate.yml, job с actions: read): перечислены все три.
report:
name: "Отказ расписания: issue и Telegram"
needs: [material, mutants, evidence]
# #620: ночь, принявшая доказательство прошлого зелёного прогона, отказом не
# является — её шарды пропущены намеренно. Любой другой пропуск шардов
# (упал material) по-прежнему заводит issue.
if: always() && github.event_name == 'schedule' && needs.material.outputs.reuse != 'true' && (needs.mutants.result != 'success' || needs.evidence.result != 'success')
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
actions: read
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Не перечитываем moving dev: код отчётчика берётся из уже
# зафиксированного material. В стабильном main dev-скриптов нет.
ref: ${{ needs.material.outputs.sha }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Забрать логи шардов
continue-on-error: true
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: mutation-shard-*
path: artifacts/mutation-logs
- name: Собрать отчёт
id: report
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
mkdir -p artifacts
node scripts/mutation-gate-report.mjs \
--require-evidence --logs=artifacts/mutation-logs --shards=6 \
--sha=${{ needs.material.outputs.sha }} \
--tree=${{ needs.material.outputs.tree }} \
--workflow-sha=${{ job.workflow_sha }} \
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
--run-url="$RUN_URL" --ref=${{ needs.material.outputs.ref }} \
--body-out=artifacts/mutation-report.md \
--telegram-out=artifacts/mutation-telegram.txt >> "$GITHUB_OUTPUT"
# Одно issue, а не одно на неделю: открытое с тем же маркером в заголовке
# получает комментарий, новое заводится только если открытого нет.
- name: Issue — создать или дописать
id: issue
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
MARKER: ${{ steps.report.outputs.marker }}
TITLE: ${{ steps.report.outputs.title }}
run: |
existing=$(gh issue list --repo "$REPO" --state open --search "\"$MARKER\" in:title" \
--json number,title --jq '[.[] | select(.title | startswith(env.MARKER))][0].number // empty')
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo "$REPO" --body-file artifacts/mutation-report.md
url="${{ github.server_url }}/$REPO/issues/$existing"
else
url=$(gh issue create --repo "$REPO" --title "$TITLE" \
--label infra --label process --label tests \
--body-file artifacts/mutation-report.md)
fi
echo "url=$url" >> "$GITHUB_OUTPUT"
echo "issue: $url"
# Тот же канал, что у релизов (announce.yml). Нет секретов — не отказ:
# issue уже заведено, а Telegram — второй адресат, не единственный.
- name: Telegram
if: always() && steps.issue.outcome == 'success'
env:
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
ISSUE_URL: ${{ steps.issue.outputs.url }}
run: |
if [ -z "$TOKEN" ] || [ -z "$CHAT" ]; then
echo "::warning::TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID не заданы — оповещение пропущено, issue заведено"
exit 0
fi
TEXT=$(sed "s|(issue)|$ISSUE_URL|" artifacts/mutation-telegram.txt)
curl -sS --fail-with-body -X POST \
"https://api.telegram.org/bot$TOKEN/sendMessage" \
--data-urlencode "chat_id=$CHAT" \
--data-urlencode "text=$TEXT" \
-d disable_web_page_preview=true
+200
View File
@@ -0,0 +1,200 @@
# Ночной полный прогон (#479).
#
# Тяжёлые job Validate — смоки, golden, performance_smoke — на обычном пуше не
# идут: они ни разу не ловили дефект в момент ревью и стоили ~6 минут
# критического пути на каждую итерацию. Полный набор идёт на кандидате беты
# (трейлер `Release:`), по кнопке и здесь — каждую ночь на голове `dev`.
#
# Почему не `schedule` прямо в validate.yml: расписание исполняется на ветке
# по умолчанию (`main`), а проверять надо `dev`. Один dispatch с `--ref dev`
# делает это без переписывания checkout во всех job. Reuse (#208) сохраняется:
# при неизменённом дереве ночной прогон обойдётся маркерами.
#
# Красный ночной прогон — сигнал автору последних коммитов на dev, не гейт:
# гейт беты по-прежнему требует зелёный Validate на точном SHA кандидата, и
# там полный набор идёт заново. Адресат сигнала (#736) — задачи, чьи коммиты
# классов A/B вошли в dev после последней зелёной ночи: job `night_red` пишет
# каждой один комментарий (`scripts/night-red.mjs`, PROCESS.md §10.4).
#
# Сигнал обязан быть настоящим (#492 §7): до этой задачи job завершалась
# успехом в момент постановки Validate в очередь, и красный полный прогон не
# делал ночной workflow красным. Теперь job находит запущенный прогон и ждёт
# его: успешный dispatch — не успешная проверка.
#
# #727: после Validate при любом его исходе ночь запускает пакетное ревью ship
# (`ship-review.yml -f tag=nightly`) на SHA прогона Validate: оно читает код, а
# не прогон. Ship-задача узнаёт о дефектах в ночь после слияния, а бета читает
# только то, что ночь не прочла (PROCESS.md §11.7). Цвет ночи — цвет Validate.
name: "Ночной полный прогон dev · тело (#623)"
on:
# #623: тело вызывается тонким файлом `nightly.yml` из ветки по умолчанию
# по ссылке `@dev`; триггеры, run-name и concurrency живут там.
workflow_call:
permissions:
actions: write
contents: read
jobs:
dispatch:
name: "Запустить Validate на dev с полным набором и дождаться результата"
runs-on: ubuntu-24.04
timeout-minutes: 90
outputs:
# #727: SHA прогона Validate — отдельным шагом до ожидания, красный Validate его не теряет.
head_sha: ${{ steps.validate.outputs.head_sha }}
# #736: прогон Validate для разбора красной ночи — тем же шагом, до ожидания.
run_id: ${{ steps.validate.outputs.run_id }}
steps:
# #658: при плане 02:30 UTC ночь фактически стартовала в 07:42–08:05 и
# кончалась в рабочее время владельца. Сдвиг старта больше часа — видимое
# предупреждение, а не молча съеденная ночь. `github.event.schedule` —
# строка cron вызывающего `nightly.yml`; NOW_EPOCH подставляет только тест.
- name: "Сдвиг старта ночи против расписания"
if: github.event_name == 'schedule'
env:
SCHEDULE: ${{ github.event.schedule }}
run: |
set -euo pipefail
read -r minute hour _ <<< "$SCHEDULE"
now=${NOW_EPOCH:-$(date -u +%s)}
planned=$(date -u -d "@$now" +%F)
planned=$(date -u -d "$planned $hour:$minute" +%s)
[ "$planned" -le "$now" ] || planned=$((planned - 86400))
lag=$(( (now - planned) / 60 ))
echo "- Старт по расписанию \`$SCHEDULE\` (UTC): сдвиг $lag мин" >> "$GITHUB_STEP_SUMMARY"
if [ "$lag" -gt 60 ]; then
echo "::warning::ночной прогон стартовал на $lag мин позже расписания ($SCHEDULE UTC) — очередь расписаний GitHub (#658)"
fi
- name: "Запустить Validate и найти его прогон"
id: validate
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
since=$(date -u +%FT%TZ)
gh workflow run validate.yml --repo "$REPO" --ref dev -f full=true
echo "Validate(dev, full=true) поставлен в очередь: $since"
# Найти именно этот прогон: workflow_dispatch на dev, созданный не
# раньше момента запуска. До трёх минут на появление в списке.
run_id=""
for _ in $(seq 1 18); do
sleep 10
run_id=$(gh run list --repo "$REPO" --workflow validate.yml --branch dev \
--event workflow_dispatch --json databaseId,createdAt --limit 5 \
--jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty")
[ -n "$run_id" ] && break
done
if [ -z "$run_id" ]; then
echo "::error::прогон Validate не появился за 3 минуты — dispatch не равен проверке"
exit 1
fi
url="${{ github.server_url }}/$REPO/actions/runs/$run_id"
echo "дочерний прогон: $url"
echo "- Validate(dev, full=true): $url" >> "$GITHUB_STEP_SUMMARY"
head_sha=$(gh run view "$run_id" --repo "$REPO" --json headSha --jq .headSha)
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- name: "Дождаться Validate"
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ steps.validate.outputs.run_id }}
run: |
set -euo pipefail
run_id="$RUN_ID"
# Ждём завершения; красный дочерний прогон — красный ночной.
gh run watch "$run_id" --repo "$REPO" --exit-status --interval 30
# #727 К6: ночное пакетное ревью ship на SHA прогона Validate — при любом его
# исходе. Dispatch тонкого `ship-review.yml` (`github.token` порождает прогон,
# как у Validate; входы и права тонких файлов уже в main). Job ждёт только
# появления прогона, не его конца; не появился — предупреждение, не красная
# ночь: цвет ночи — цвет Validate.
ship_review:
name: "Запустить ночное пакетное ревью ship на SHA прогона Validate"
needs: dispatch
if: always()
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: "Запустить ship-ревью и дождаться появления прогона"
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
CANDIDATE: ${{ needs.dispatch.outputs.head_sha }}
run: |
set -euo pipefail
if [ -z "$CANDIDATE" ]; then
echo "::warning::нет SHA прогона Validate — ночное ship-ревью не запущено"
echo "- Ship-ревью ночью: не запущено — нет SHA прогона Validate" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
since=$(date -u +%FT%TZ)
if ! gh workflow run ship-review.yml --ref dev -f tag=nightly -f candidate="$CANDIDATE" --repo "$REPO"; then
echo "::warning::ship-ревью ночью не запущено: dispatch отклонён — ночь не красится"
echo "- Ship-ревью ночью: dispatch отклонён" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
run_id=""
for _ in $(seq 1 18); do
sleep 10
run_id=$(gh run list --repo "$REPO" --workflow ship-review.yml --branch dev \
--event workflow_dispatch --json databaseId,createdAt --limit 5 \
--jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty")
[ -n "$run_id" ] && break
done
if [ -z "$run_id" ]; then
echo "::warning::прогон ship-ревью не появился за 3 минуты — ночь не красится, её цвет — цвет Validate"
echo "- Ship-ревью ночью (\`$CANDIDATE\`): прогон не появился за 3 минуты" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "- Ship-ревью ночью (\`$CANDIDATE\`): ${{ github.server_url }}/$REPO/actions/runs/$run_id" >> "$GITHUB_STEP_SUMMARY"
# #736: красная ночь — один комментарий в каждую задачу, чьи коммиты классов
# A/B вошли в dev после последней зелёной ночи (`scripts/night-red.mjs`,
# PROCESS.md §10.4). Только при красном dispatch с известным прогоном;
# исход прогона (failure, а не cancelled/timed_out) скрипт читает сам.
# Actions — `github.token`, issue — `HP_PROCESS_TOKEN`: `issues: write` у
# ночи нет, и потолок тонкого `nightly.yml` в main не меняется. Сбой —
# предупреждение, не красная ночь сверх Validate.
night_red:
name: "Красная ночь: комментарий в задачи, слитые после последней зелёной"
needs: dispatch
if: always() && needs.dispatch.result == 'failure' && needs.dispatch.outputs.run_id != ''
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
actions: read
contents: read
steps:
# Нужны коммиты и деревья, не блобы; скрипт и его импорты — без зависимостей, npm ci не нужен.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 0
filter: blob:none
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: "Комментарий в задачи диапазона от последней зелёной ночи до красной"
env:
ACTIONS_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
REPO: ${{ github.repository }}
RED_RUN: ${{ needs.dispatch.outputs.run_id }}
run: |
set -euo pipefail
# Сбои API, токена и git скрипт сам сводит к предупреждению; упавший
# скрипт — тоже предупреждение: цвет ночи — цвет Validate.
if ! node scripts/night-red.mjs --repo="$REPO" --red-run="$RED_RUN"; then
echo "::warning::красная ночь (#736): скрипт упал — комментарии не написаны; цвет ночи — цвет Validate"
echo "- Красная ночь (#736): скрипт упал — комментарии не написаны" >> "$GITHUB_STEP_SUMMARY"
fi
+66
View File
@@ -0,0 +1,66 @@
name: "Метрики процесса · тело (#623)"
# #637: еженедельный замер вместо ощущений — lead time S→S7→S8, раунды ревью,
# прогоны Validate по исходам, минуты конвейера в S4/S7. Только чтение: отчёт
# идёт в step summary и артефакт; в issue и репозиторий ничего не пишется.
# Цифры аудита 22.09 (1,57 раунда код-ревью, S7 ≈ 36 мин/раунд, 226 Validate
# за неделю) были собраны руками за час — теперь они стоят один запуск.
#
# #728: эффект процесса по трекам — отрезки, возвраты с причинами, находки
# ship, job-минуты по стадиям и сравнение до/после 28.09. Прогон дороже:
# jobs до 600 прогонов и таймлайны задач окна сравнения (≈ 600–800 запросов),
# поэтому потолок 30 минут; трек инфраструктуры и объём задачи читаются из
# истории git — нужна полная история dev.
on:
# #623: тело вызывается тонким файлом `process-metrics.yml` из ветки по умолчанию
# по ссылке `@dev`; триггеры, run-name и concurrency живут там.
workflow_call:
inputs:
days:
description: "Окно в днях"
required: false
type: string
default: "7"
permissions:
contents: read
actions: read
issues: read
jobs:
metrics:
name: "Снимок недели: issue, раунды, прогоны"
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
# Код — из dev, как у reconcile: расписание читается из main, а исполняется
# версия, которую проверил CI. Полная история (#728): коммиты задачи по
# трейлеру `Issue: #NN` дают признак инфраструктуры и объём для сравнения.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Собрать отчёт
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
REPO: ${{ github.repository }}
DAYS: ${{ inputs.days || '7' }}
run: |
mkdir -p artifacts/process-metrics
node scripts/process-metrics.mjs --repo="$REPO" --days="$DAYS" \
--output=artifacts/process-metrics/report.md \
--json=artifacts/process-metrics/report.json > /dev/null
cat artifacts/process-metrics/report.md >> "$GITHUB_STEP_SUMMARY"
- name: Сохранить отчёт
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: process-metrics-${{ github.run_id }}
path: artifacts/process-metrics
if-no-files-found: error
retention-days: 90
+57
View File
@@ -0,0 +1,57 @@
name: "Сверка очереди ревью · тело (#623)"
on:
# #623: тело вызывается тонким файлом `process-reconcile.yml` из ветки по умолчанию
# по ссылке `@dev`; триггеры, run-name и concurrency живут там.
workflow_call:
inputs:
apply:
description: "Повторно будить потерянные запросы и публиковать диагностику"
required: false
type: boolean
default: true
permissions:
actions: read
contents: read
issues: read
jobs:
reconcile:
name: "Один снимок S4/S7 без polling модели"
runs-on: ubuntu-24.04
timeout-minutes: 10
concurrency:
group: process-reconcile
cancel-in-progress: false
steps:
# Расписание читается из main, а исполняемый reconciler — из dev: так
# после штатного merge действует та же версия кода, которую проверил CI.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Сопоставить labels, requests, runs и sealed evidence
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
REPO: ${{ github.repository }}
APPLY: ${{ github.event_name == 'schedule' || inputs.apply == true }}
run: |
mkdir -p artifacts/process-reconcile
node scripts/process-reconcile.mjs \
--repo "$REPO" \
--apply="$APPLY" \
--max-actions=5 \
--output=artifacts/process-reconcile/summary.json
- name: Опубликовать компактный machine-readable итог
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: process-reconcile-${{ github.run_id }}-${{ github.run_attempt }}
path: artifacts/process-reconcile/summary.json
if-no-files-found: error
retention-days: 14
+65
View File
@@ -0,0 +1,65 @@
name: "Продолжение ревью после Validate · тело (#623)"
# #636. Стадия `prepare` конвейера (process.yml) больше не ждёт Validate с
# мутантами на материале внутри job — раннер спал ≈ 28 минут на раунд при
# 10–12 минутах работы модели. Она диспатчит прогон, кладёт запечатанный
# маркер `review-pending-…` и выходит. Этот workflow просыпается на завершение
# любого Validate и, если раунд ждал именно этот прогон (маркер на материале,
# метка S7 стоит, активного прогона конвейера нет), переставляет метку S7 —
# новый прогон `prepare` находит завершённый dispatch и продолжает раунд.
# Ничего не оценивает: зелёный/красный разбирает сам конвейер. Страховка на
# потерянное событие — process-reconcile.yml с тем же маркером.
#
# Для события `workflow_run` GitHub берёт workflow только из ветки по
# умолчанию (main). Там лежит тонкий `process-resume.yml`, который вызывает
# этот файл по ссылке `@dev` (#623); сверка тонких копий — в preflight
# validate.yml.
on:
# #623: тело вызывается тонким файлом `process-resume.yml` из ветки по умолчанию
# по ссылке `@dev`; триггеры, run-name и concurrency живут там.
workflow_call:
permissions:
contents: read
actions: read
jobs:
resume:
name: "Разбудить раунд, ждавший этот Validate"
if: github.event.workflow_run.event == 'workflow_dispatch' && startsWith(github.event.workflow_run.head_branch, 'issue/')
runs-on: ubuntu-24.04
timeout-minutes: 10
concurrency:
group: process-resume-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
steps:
# Код берётся из dev, как у reconcile: после штатного слияния действует
# версия, которую проверил CI, а не копия из main.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
# Метка переставляется HP_PROCESS_TOKEN: событие от GITHUB_TOKEN не
# запустило бы process.yml (см. шапку process.yml, п. 1).
- name: Решить по маркеру ожидания и переставить S7
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
SHA: ${{ github.event.workflow_run.head_sha }}
EVENT: ${{ github.event.workflow_run.event }}
STATUS: ${{ github.event.workflow_run.status }}
VALIDATE_RUN_ID: ${{ github.event.workflow_run.id }}
# #751: без pipefail код конвейера — код tee, и исключение скрипта (gh,
# API) проходило зелёным шагом: событие возобновления терялось до
# прохода process-reconcile.
run: |
set -o pipefail
node scripts/process-resume.mjs \
--repo="$REPO" --branch="$BRANCH" --sha="$SHA" \
--event="$EVENT" --status="$STATUS" --run-id="$VALIDATE_RUN_ID" --apply=true | tee -a "$GITHUB_STEP_SUMMARY"
File diff suppressed because it is too large Load Diff
+415
View File
@@ -0,0 +1,415 @@
name: "Бета: пакетное ревью ship · тело (#716)"
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
# правка в механических рамках и зелёный лёгкий Validate. Их код читает одна
# сессия модели перед бетой: все ship-задачи диапазона «прошлый тег..кандидат».
# Гейт публикации беты (`ship-review.mjs check`) требует этот документ, если в
# диапазоне есть ship-задачи.
#
# Три job, как у release-review.yml (#551, #556): детерминированный вход,
# модель без права записи, детерминированная публикация документа в `dev`.
#
# #727: `tag=nightly` — ночной режим. Его запускает `_nightly.yml` после
# полного Validate с `candidate` = голова `dev` того прогона. Читаются только
# ship-задачи без покрытия или изменившиеся после ревью (`shipCoverage`), документ —
# `SHIP-REVIEW-<база>-dev-<sha12>.md`. Бета так же читает только дельту.
on:
# #716: тело вызывается тонким файлом `ship-review.yml` по ссылке `@dev`;
# кнопка, входы ручного запуска, run-name и concurrency живут там. Без
# файла в ветке по умолчанию GitHub не даёт запустить workflow_dispatch вовсе.
workflow_call:
inputs:
# #727: `nightly` — зарезервированное значение (ночной режим, candidate обязателен).
tag:
description: "Beta tag the review is for, for example v1.79.0-beta.1"
required: true
type: string
candidate:
description: "Exact candidate SHA; empty = the dev tip"
required: false
type: string
default: ""
force:
description: "Review again even when the document already exists in dev"
required: false
type: boolean
default: false
permissions:
contents: read
# Concurrency уровня workflow — у вызывающего `ship-review.yml` (#716).
jobs:
prepare:
name: "Ship-ревью: задачи диапазона"
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
issues: read
outputs:
proceed: ${{ steps.range.outputs.proceed }}
candidate: ${{ steps.range.outputs.candidate }}
base: ${{ steps.range.outputs.base }}
doc: ${{ steps.range.outputs.doc }}
issues: ${{ steps.range.outputs.issues }}
mode: ${{ steps.range.outputs.mode }}
patches: ${{ steps.range.outputs.patches }}
subject: ${{ steps.range.outputs.subject }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
ref: dev
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Кандидат, база и ship-задачи
id: range
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
CANDIDATE: ${{ inputs.candidate }}
FORCE: ${{ inputs.force }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
# Отказ prepare за `| tee` не должен читаться как «ship-задач нет» (#727).
set -o pipefail
git fetch -q --tags origin
# #727: tag=nightly без candidate — отказ до подстановки вершины dev.
node scripts/ship-review.mjs mode --tag="$TAG" --candidate="$CANDIDATE"
if [ -z "$CANDIDATE" ]; then CANDIDATE=$(git rev-parse origin/dev); fi
git cat-file -e "$CANDIDATE^{commit}"
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
doc=$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")
if [ "$FORCE" != "true" ] && git cat-file -e "origin/dev:$doc" 2>/dev/null; then
echo "::notice::$doc уже есть в dev — повторное ревью не запускается (force=true, чтобы переснять)"
echo "proceed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
out="$RUNNER_TEMP/ship-review-input"
node scripts/ship-review.mjs prepare --tag="$TAG" --candidate="$CANDIDATE" --force="$FORCE" \
--out="$out" --repo="${{ github.repository }}" --run-url="$RUN_URL" | tee "$RUNNER_TEMP/range.txt"
cat "$RUNNER_TEMP/range.txt" >> "$GITHUB_OUTPUT"
echo "candidate=$CANDIDATE" >> "$GITHUB_OUTPUT"
issues=$(sed -n 's/^issues=//p' "$RUNNER_TEMP/range.txt")
if [ -z "$issues" ]; then
# #727 К5: пустая дельта — все ship-задачи покрыты документами базы.
note=$(sed -n 's/^note=//p' "$RUNNER_TEMP/range.txt")
echo "::notice::${note:-ship-задач в диапазоне нет — ревью не нужно}"
if [ -n "$note" ]; then echo "- $note" >> "$GITHUB_STEP_SUMMARY"; fi
echo "proceed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "proceed=true" >> "$GITHUB_OUTPUT"
(cd "$out" && sha256sum brief.md ship-issues.json > manifest.sha256)
cat "$out/brief.md" >> "$GITHUB_STEP_SUMMARY"
- name: Передать вход модели
if: steps.range.outputs.proceed == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-input
if-no-files-found: error
retention-days: 3
model_review:
name: "Ship-ревью: работа модели"
needs: prepare
if: needs.prepare.outputs.proceed == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
# #737: строка расхода сессии модели (scripts/model-usage.mjs).
outputs:
usage: ${{ steps.usage.outputs.line }}
# Недоверенная стадия без права записи (#556): документ публикует
# `publish`. `github_token` у шага Review обязателен — без него action
# меняет OIDC на собственный App-токен с правом записи.
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
ref: ${{ needs.prepare.outputs.candidate }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- name: Получить вход
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ship-review-input-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-input
- name: Проверить вход и кандидата
env:
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
run: |
(cd "$RUNNER_TEMP/ship-review-input" && sha256sum -c manifest.sha256)
test "$(git rev-parse HEAD)" = "$CANDIDATE"
- name: Установить зависимости
run: npm ci
- name: Кэш браузеров Playwright
id: pw
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Установить Chromium
if: steps.pw.outputs.cache-hit != 'true'
run: npx playwright install chromium
# Тот же обход, что у конвейера (_process.yml, anthropics issue 1817).
- name: Установить Claude Code детерминированно
id: claude_bin
run: |
src=$(ls "$RUNNER_WORKSPACE"/../_actions/anthropics/claude-code-*/v1/src/entrypoints/run.ts 2>/dev/null | head -1)
ver=$(grep -oE 'claudeCodeVersion = "[0-9]+\.[0-9]+\.[0-9]+"' "$src" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
ver="${ver:-2.1.265}"
base=https://downloads.claude.ai/claude-code-releases
bin="$HOME/.local/bin/claude"
mkdir -p "$(dirname "$bin")"
curl -fsSL --retry 3 "$base/$ver/linux-x64/claude" -o "$bin"
sum=$(curl -fsSL --retry 3 "$base/$ver/manifest.json" | jq -r '.platforms["linux-x64"].checksum')
echo "$sum $bin" | sha256sum -c -
chmod +x "$bin"
"$bin" --version
echo "path=$bin" >> "$GITHUB_OUTPUT"
- name: Review
id: review
uses: anthropics/claude-code-action@9cdae7f0d995e3ba7c33f226087fdf82a59cd520 # v1
env:
REVIEW_DOC: ${{ runner.temp }}/ship-review.md
REVIEW_INPUT: ${{ runner.temp }}/ship-review-input
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }}
prompt: |
Ты ревьюер кода проекта House Plan. Язык ответа — русский.
${{ needs.prepare.outputs.subject }}
Кандидат ${{ needs.prepare.outputs.candidate }} (рабочая копия уже на нём) · база
диапазона: ${{ needs.prepare.outputs.base || 'нет' }}.
Задачи: ${{ needs.prepare.outputs.issues }} — вход в $REVIEW_INPUT/brief.md
(строка ТЗ каждой задачи и её коммиты) и $REVIEW_INPUT/ship-issues.json.
Эти задачи шли по треку ship и слиты в dev без ревью модели: правка
в механических рамках и зелёный лёгкий Validate. Ты — первый, кто
читает их код. Правила — docs/process/REVIEWER.md, раздел
«Пакетное ревью ship», канон — PROCESS.md §5 и §11.7. Прочитай их
первыми.
По каждой задаче: `git show <sha>` каждого коммита; делает ли код
то, что сказано в строке ТЗ, и только это; не ломает ли соседнее
поведение; не вышла ли правка из трека ship по смыслу (новый
UX-контракт, геометрия, конфиг, перф) — это находка Medium с
предложением трека. Исполняй, где дёшево: `npm test`, смоук,
который называет строка ТЗ или выбирает
`node scripts/smoke-select.mjs --base <sha>^ --head <sha>`.
Серьёзность: High — дефект, который увидит пользователь беты, или
правка не делает заявленного; High блокирует публикацию беты.
Medium — поведение вне заявленного или вне рамок трека; Low —
остальное. Находка чинится отдельной задачей, ты ничего не правишь.
Ты ничего не правишь и не публикуешь: ни код, ни issue, ни
комментарии. Права на запись у тебя нет. Любые изменения рабочей
копии будут отброшены — после проверок восстанови её сам
(`git checkout -- . && git clean -fd`), если что-то менял.
Напиши документ в файл по пути из переменной REVIEW_DOC (абсолютный,
вне репозитория). Первой строкой после заголовка —
`Итог: High N · Medium N · Low N`. Затем по задаче на раздел
`## #NN — заголовок`: что проверил (команда → результат), находки с
воспроизведением, чего не проверял.
Затем верни JSON по схеме — последнее обязательное действие.
claude_args: |
--max-turns 150
--allowedTools Read,Write,Grep,Glob,Bash
--json-schema '{"type":"object","properties":{"high":{"type":"integer"},"medium":{"type":"integer"},"low":{"type":"integer"},"summary":{"type":"string"}},"required":["high","medium","low","summary"]}'
# #737: как у конвейера (_process.yml) — расход выходом job, не через
# artifact; execution_file не выгружается и не печатается.
- name: Снять расход модели
id: usage
if: always()
continue-on-error: true
env:
EXEC: ${{ steps.review.outputs.execution_file }}
run: |
line=$(node scripts/model-usage.mjs --execution-file="$EXEC")
echo "line=$line" >> "$GITHUB_OUTPUT"
echo "- расход модели: \`$line\`" >> "$GITHUB_STEP_SUMMARY"
- name: Запечатать результат модели
env:
SOURCE: ${{ runner.temp }}/ship-review.md
OUT: ${{ steps.review.outputs.structured_output }}
run: |
test -s "$SOURCE" || { echo "::error::модель не оставила документ ревью"; exit 1; }
dir="$RUNNER_TEMP/ship-review-result"
mkdir -p "$dir"
printf '%s' "$OUT" > "$dir/result.json"
jq -e '(.high|type=="number") and (.medium|type=="number") and (.low|type=="number") and (.summary|type=="string")' \
"$dir/result.json" >/dev/null
cp "$SOURCE" "$dir/ship-review.md"
(cd "$dir" && sha256sum ship-review.md result.json > manifest.sha256)
- name: Передать результат публикации
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-result
if-no-files-found: error
retention-days: 3
publish:
name: "Ship-ревью: документ в dev"
needs: [prepare, model_review]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
ref: dev
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Получить результат модели
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ship-review-result-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ship-review-result
# Модель пишет только текст. Путь, машинный блок (его читает гейт беты),
# индекс и коммит решает этот шаг. #727: `mode` и `patches` блока — из
# prepare (детерминированный патч-набор), не из результата модели.
- name: Опубликовать документ
env:
TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
TAG: ${{ inputs.tag }}
DOC: ${{ needs.prepare.outputs.doc }}
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
BASE: ${{ needs.prepare.outputs.base }}
ISSUES: ${{ needs.prepare.outputs.issues }}
MODE: ${{ needs.prepare.outputs.mode }}
PATCHES: ${{ needs.prepare.outputs.patches }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# #737: расход модели — строкой сразу после блока; недоверенный ввод.
USAGE: ${{ needs.model_review.outputs.usage }}
run: |
dir="$RUNNER_TEMP/ship-review-result"
(cd "$dir" && sha256sum -c manifest.sha256)
test "$DOC" = "$(node scripts/ship-review.mjs doc --tag="$TAG" --candidate="$CANDIDATE")"
high=$(jq -r '.high' "$dir/result.json")
medium=$(jq -r '.medium' "$dir/result.json")
low=$(jq -r '.low' "$dir/result.json")
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#730, как #723).
# #748: ночью — своё: имя документа без префикса, задача ночного режима.
if [ "$MODE" = "nightly" ]; then
night=${DOC##*/SHIP-REVIEW-}
subject="docs: nightly ship review ${night%.md}"
lead="Ночное пакетное ревью задач track:ship (PROCESS.md §11.7)."
issue="#727"
else
subject="docs: ship review for $TAG"
lead="Пакетное ревью задач track:ship перед бетой (PROCESS.md §11.7)."
issue="#696"
fi
msg="$RUNNER_TEMP/ship-review-commit.txt"
push_err="$RUNNER_TEMP/ship-review-push.stderr"
for attempt in 1 2 3; do
git fetch -q origin dev
git reset -q --hard origin/dev
git clean -fdq
mkdir -p docs/reviews
{
cat "$dir/ship-review.md"
printf '\n\n'
node -e '
import("./scripts/ship-review.mjs").then((m) => process.stdout.write(m.anchorBlock({
tag: process.env.TAG, candidate: process.env.CANDIDATE, base: process.env.BASE || null,
issues: String(process.env.ISSUES || "").split(",").filter(Boolean).map(Number),
high: Number(process.argv[1]), medium: Number(process.argv[2]), low: Number(process.argv[3]),
runUrl: process.env.RUN_URL, mode: process.env.MODE, patches: m.parsePatches(process.env.PATCHES),
usage: process.env.USAGE ?? "",
})));
' "$high" "$medium" "$low"
} > "$DOC"
node scripts/reviews-index.mjs --dir=docs/reviews --strict
git add -- "$DOC" docs/reviews/INDEX.md
git diff --cached --name-only | node scripts/review-doc-guard.mjs
{
echo "$subject"
echo ""
echo "$lead"
echo "Задачи: $ISSUES. Итог: High $high · Medium $medium · Low $low."
echo ""
echo "Issue: $issue"
echo "User-Visible: no"
} > "$msg"
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F "$msg"
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
echo "### Пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Задачи $ISSUES · High $high · Medium $medium · Low $low — \`$DOC\` в dev." >> "$GITHUB_STEP_SUMMARY"
echo "::notice::$DOC опубликован: High $high · Medium $medium · Low $low"
exit 0
fi
# #730: сдвигом dev считается только устаревший lease — отказ
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705),
# как у ревью релиза (#723). Скрипт — из dev: рабочая копия только что
# сброшена на origin/dev. Отказ GitHub (право на workflow, правило
# ветки, хук) повтор не лечит: шаг останавливается, причина и ответ
# git без токена — в журнале и в сводке шага.
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
--stage=ship-review --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
if [ "$kind" != "stale" ]; then
echo "::error::push $DOC в dev отклонён ($kind) — это не сдвиг dev, повтор не поможет; причина и ответ git — выше и в сводке шага"
exit 1
fi
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
sleep $((attempt * 10))
done
echo "::error::документ ревью не опубликован в dev за три попытки"
exit 1
# #727 К8: High ночного документа — одна строка в каждую его задачу.
# Повтор на тот же документ не пишется (метка hp:ship-review-high doc=…),
# Medium и Low в issue не идут — их решает владелец (§11.7). Режим и
# High проверяет и скрипт: шаг вне ночи или без High ничего не пишет.
- name: High ночью — строка в задачи документа
if: needs.prepare.outputs.mode == 'nightly'
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
MODE: ${{ needs.prepare.outputs.mode }}
DOC: ${{ needs.prepare.outputs.doc }}
ISSUES: ${{ needs.prepare.outputs.issues }}
run: |
high=$(jq -r '.high' "$RUNNER_TEMP/ship-review-result/result.json")
node scripts/ship-review.mjs comment-high --mode="$MODE" --high="$high" --doc="$DOC" \
--issues="$ISSUES" --repo="${{ github.repository }}"
+2 -1
View File
@@ -43,7 +43,8 @@ jobs:
telegram:
name: Оповещение в Telegram (только стабильные)
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_call' && inputs.prerelease == false) }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out release notes for a reusable call
if: ${{ inputs.reusable == true }}
+65
View File
@@ -0,0 +1,65 @@
name: "Бета: производные артефакты на dev"
run-name: "Derived artifacts ${{ inputs.tag }}"
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
# run-name, права и concurrency. Тело — `_beta-derived.yml` по ссылке `@dev`:
# правка съёмки и приёмки — один коммит в `dev`, а запуск с любой ветки (в
# кнопке по умолчанию выбрана `main`) исполняет тело из `dev`.
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
# равными.
on:
workflow_dispatch:
inputs:
tag:
description: "Beta tag these artifacts are for, for example v1.79.0-beta.1"
required: true
type: string
docs_expect_change:
description: "Doc scenario ids expected to change, comma-separated (empty = none)"
required: false
type: string
default: ""
golden_run:
description: "Validate run id on dev whose golden-images artifact holds the shifted frames (empty = skip golden)"
required: false
type: string
default: ""
golden_expect_change:
description: "Golden scenes expected to change, comma-separated"
required: false
type: string
default: ""
golden_expect_new:
description: "New golden scenes, comma-separated"
required: false
type: string
default: ""
permissions:
contents: read
concurrency:
group: beta-derived
cancel-in-progress: false
jobs:
# Потолок прав тела: объединение job-level прав `_beta-derived.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
contents: read
actions: read
uses: Matysh/houseplan-card/.github/workflows/_beta-derived.yml@dev # #716: тело съёмки и приёмки из dev
with:
tag: ${{ inputs.tag }}
docs_expect_change: ${{ inputs.docs_expect_change }}
golden_run: ${{ inputs.golden_run }}
golden_expect_change: ${{ inputs.golden_expect_change }}
golden_expect_new: ${{ inputs.golden_expect_new }}
secrets: inherit
+2 -1
View File
@@ -31,7 +31,8 @@ permissions:
jobs:
capture:
name: Съёмка и сверка скриншот-индекса
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
+19 -248
View File
@@ -1,24 +1,12 @@
name: Мутационный гейт
# Реестр известных поломок (issue #85): каждый мутант ломает продуктовый код
# известным способом, и объявленный тест ОБЯЗАН на этом покраснеть. Тест,
# оставшийся зелёным на сломанном коде, ничего не защищает — он лишь выглядит
# защитой, и это хуже его отсутствия.
#
# Прогон дорогой и проверяет не продукт, а тесты, поэтому он не входит ни в
# Validate, ни в цикл разработки, ни в релизный гейт (#513, решение владельца
# 09.09): каждую ночь по расписанию, отказ — issue с отчётом (#472).
# Мутанты, задетые диффом, конвейер ревью и слияние гоняют отдельно на своём
# кандидате (#510); кандидат беты, `full=true` и ночной Validate их не
# запрашивают (#601) — ночью достаточно этого полного реестра.
# Дешёвая половина — «якоря патчей живы, guard-файлы существуют» — идёт с
# обычными юнитами: test/mutation-gate.test.mjs.
#
# #332: бандл собирается только мутантам с браузерным гвардом (guardNeedsBundle),
# компиляция тестов в worktree стартует с тёплого test-build (инкрементальный
# tsc), а реестр режется на четыре чересполосных шарда — полный прогон
# укладывается в десятки минут вместо часов. Локальный дифф-режим:
# node scripts/mutation-gate.mjs --changed origin/dev..HEAD
# Тонкий вызывающий файл (#623). Для этого события GitHub берёт workflow из
# ветки по умолчанию (`main`), поэтому здесь только то, что обязано жить там:
# триггеры, run-name, права и concurrency. Тело — `_mutation-gate.yml` по ссылке
# `@dev`: правка конвейера — один коммит в `dev`, зеркало в `main` не нужно.
# Этот файл меняется, только когда меняются сами триггеры или потолок прав;
# тогда он зеркалится в `main`, и preflight `workflow_sync` (validate.yml)
# держит копии равными.
on:
workflow_dispatch:
@@ -28,9 +16,10 @@ on:
required: false
default: dev
schedule:
# Каждую ночь, 01:00 UTC (04:00 MSK) — после суток правок и до ночного
# полного Validate (nightly.yml, 02:30 UTC), чтобы не делить раннеры (#513).
- cron: '0 1 * * *'
# Каждую ночь, 00:43 UTC (03:43 MSK) — после суток правок и до ночного
# полного Validate (nightly.yml, 02:17 UTC), чтобы не делить раннеры (#513).
# Минута не круглая: такие старты GitHub сдвигает меньше (#658).
- cron: '43 0 * * *'
permissions:
contents: read
@@ -45,233 +34,15 @@ concurrency:
cancel-in-progress: true
jobs:
# #549: moving ref разрешается ровно один раз. Все шарды ниже получают один
# commit/tree, а не самостоятельно читают dev в разное время.
material:
name: "Зафиксировать неизменяемый материал"
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.identity.outputs.sha }}
tree: ${{ steps.identity.outputs.tree }}
ref: ${{ steps.identity.outputs.ref }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }}
fetch-depth: 0
- name: Зафиксировать commit и tree
id: identity
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "tree=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"
echo "ref=${{ github.event_name == 'workflow_dispatch' && inputs.ref || 'dev' }}" >> "$GITHUB_OUTPUT"
mutants:
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 6)"
needs: material
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
# Шесть чересполосных шардов (#604): при четырёх шард нёс ~203 мутанта из
# 810 и рос с реестром — 42 мин 10.09, 57 мин 20.09, 61 мин 21.09, и шард
# 2/4 был снят по потолку без единого FAIL. Делитель тот же, что у
# `changed_mutants` в Validate; число шардов повторяется в `--shard=i/6`,
# `--shards=6` и имени job — тест `mutation-gate.test.mjs` держит их
# равными. Бандл собирают только браузерные гварды. Час — потолок против
# зависшего Chromium, а не бюджет шарда: шард, упёршийся в него, — сигнал
# снова делить, и отчёт (#472) называет такой шард прерванным, не «ok».
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ needs.material.outputs.sha }}
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- run: npm ci
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: '3.14'
- name: Установить backend test dependencies
run: |
# Точные версии — в tests_backend/requirements.txt (#392): без них
# резолвер выбирает набор сам, и «зелёный backend» значит разное
# в разные дни.
pip install -r tests_backend/requirements.txt
pip list --format=columns | grep -Ei 'homeassistant|voluptuous|^pytest '
- 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 --with-deps chromium
- name: Реестр применим к текущему коду
run: node scripts/mutation-gate.mjs --check
- name: Тёплый test-build для инкрементальной компиляции мутантов
run: npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs
# Вывод шарда сохраняется артефактом (#472): строки
# `FAIL <id>: тест остался зелёным…` — единственное место, где названо,
# ЧТО сбежало. Без артефакта отказ безымянный. `PIPESTATUS` — чтобы
# `tee` не съел код выхода раннера.
- name: Каждый тест ловит свою поломку
id: gate
run: |
mkdir -p artifacts/mutation-shard-${{ matrix.shard }}
set -o pipefail
node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/6 2>&1 | tee artifacts/mutation-shard-${{ matrix.shard }}/mutation-shard-${{ matrix.shard }}.log
# Исход шага едет в evidence (#604): снятый по timeout-minutes шаг даёт
# `cancelled`, и агрегатор отвергает такой шард как неполный — лог без
# строк FAIL сам по себе зелёным не считается.
- name: Записать identity шарда
if: always()
run: |
node scripts/mutation-gate-report.mjs \
--write-evidence=artifacts/mutation-shard-${{ matrix.shard }}/evidence.json \
--sha=${{ needs.material.outputs.sha }} \
--tree=${{ needs.material.outputs.tree }} \
--workflow-sha=${{ github.workflow_sha }} \
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
--shard=${{ matrix.shard }} --shards=6 \
--outcome=${{ steps.gate.outcome }}
- name: Сохранить лог и identity шарда
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mutation-shard-${{ matrix.shard }}-attempt-${{ github.run_attempt }}
path: artifacts/mutation-shard-${{ matrix.shard }}
if-no-files-found: warn
retention-days: 30
# Результат нельзя приписывать material, пока не доказаны все шесть шардов —
# каждый с identity и с дошедшим до конца прогоном (#549, #604).
# always() нужен при красном мутанте: лог красного шарда всё равно evidence.
evidence:
name: "Доказать единый material всех шардов"
needs: [material, mutants]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# CLI отчётчика принадлежит зафиксированному material. В main лежит
# только workflow и может не быть dev-версии scripts/**.
ref: ${{ needs.material.outputs.sha }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Забрать evidence всех попыток
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: mutation-shard-*
path: artifacts/mutation-logs
- name: Проверить полноту и identity
run: |
node scripts/mutation-gate-report.mjs --verify-only \
--logs=artifacts/mutation-logs --shards=6 \
--sha=${{ needs.material.outputs.sha }} \
--tree=${{ needs.material.outputs.tree }} \
--workflow-sha=${{ github.workflow_sha }} \
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }}
# Адресат у отказа (#472). Только по расписанию: ручной dispatch остаётся
# для отладки самого гейта, его результат смотрят в прогоне — issue на
# каждый такой отказ был бы шумом, который снова перестанут читать.
#
# Права job-уровня ЗАМЕНЯЮТ права workflow, а не дополняют (прецедент —
# validate.yml, job с actions: read): перечислены все три.
report:
name: "Отказ расписания: issue и Telegram"
needs: [material, mutants, evidence]
if: always() && github.event_name == 'schedule' && (needs.mutants.result != 'success' || needs.evidence.result != 'success')
runs-on: ubuntu-latest
# Потолок прав тела: объединение job-level прав `_mutation-gate.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
contents: read
actions: read
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
# Не перечитываем moving dev: код отчётчика берётся из уже
# зафиксированного material. В стабильном main dev-скриптов нет.
ref: ${{ needs.material.outputs.sha }}
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Забрать логи шардов
continue-on-error: true
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: mutation-shard-*
path: artifacts/mutation-logs
- name: Собрать отчёт
id: report
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
mkdir -p artifacts
node scripts/mutation-gate-report.mjs \
--require-evidence --logs=artifacts/mutation-logs --shards=6 \
--sha=${{ needs.material.outputs.sha }} \
--tree=${{ needs.material.outputs.tree }} \
--workflow-sha=${{ github.workflow_sha }} \
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
--run-url="$RUN_URL" --ref=${{ needs.material.outputs.ref }} \
--body-out=artifacts/mutation-report.md \
--telegram-out=artifacts/mutation-telegram.txt >> "$GITHUB_OUTPUT"
# Одно issue, а не одно на неделю: открытое с тем же маркером в заголовке
# получает комментарий, новое заводится только если открытого нет.
- name: Issue — создать или дописать
id: issue
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
MARKER: ${{ steps.report.outputs.marker }}
TITLE: ${{ steps.report.outputs.title }}
run: |
existing=$(gh issue list --repo "$REPO" --state open --search "\"$MARKER\" in:title" \
--json number,title --jq '[.[] | select(.title | startswith(env.MARKER))][0].number // empty')
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo "$REPO" --body-file artifacts/mutation-report.md
url="${{ github.server_url }}/$REPO/issues/$existing"
else
url=$(gh issue create --repo "$REPO" --title "$TITLE" \
--label infra --label process --label tests \
--body-file artifacts/mutation-report.md)
fi
echo "url=$url" >> "$GITHUB_OUTPUT"
echo "issue: $url"
# Тот же канал, что у релизов (announce.yml). Нет секретов — не отказ:
# issue уже заведено, а Telegram — второй адресат, не единственный.
- name: Telegram
if: always() && steps.issue.outcome == 'success'
env:
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
ISSUE_URL: ${{ steps.issue.outputs.url }}
run: |
if [ -z "$TOKEN" ] || [ -z "$CHAT" ]; then
echo "::warning::TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID не заданы — оповещение пропущено, issue заведено"
exit 0
fi
TEXT=$(sed "s|(issue)|$ISSUE_URL|" artifacts/mutation-telegram.txt)
curl -sS --fail-with-body -X POST \
"https://api.telegram.org/bot$TOKEN/sendMessage" \
--data-urlencode "chat_id=$CHAT" \
--data-urlencode "text=$TEXT" \
-d disable_web_page_preview=true
uses: Matysh/houseplan-card/.github/workflows/_mutation-gate.yml@dev # #623: тело конвейера из dev
with:
ref: ${{ inputs.ref }}
secrets: inherit
+20 -53
View File
@@ -1,28 +1,18 @@
# Ночной полный прогон (#479).
#
# Тяжёлые job Validate — смоки, golden, performance_smoke — на обычном пуше не
# идут: они ни разу не ловили дефект в момент ревью и стоили ~6 минут
# критического пути на каждую итерацию. Полный набор идёт на кандидате беты
# (трейлер `Release:`), по кнопке и здесь — каждую ночь на голове `dev`.
#
# Почему не `schedule` прямо в validate.yml: расписание исполняется на ветке
# по умолчанию (`main`), а проверять надо `dev`. Один dispatch с `--ref dev`
# делает это без переписывания checkout во всех job. Reuse (#208) сохраняется:
# при неизменённом дереве ночной прогон обойдётся маркерами.
#
# Красный ночной прогон — сигнал автору последних коммитов на dev, не гейт:
# гейт беты по-прежнему требует зелёный Validate на точном SHA кандидата, и
# там полный набор идёт заново.
#
# Сигнал обязан быть настоящим (#492 §7): до этой задачи job завершалась
# успехом в момент постановки Validate в очередь, и красный полный прогон не
# делал ночной workflow красным. Теперь job находит запущенный прогон и ждёт
# его: успешный dispatch — не успешная проверка.
name: Ночной полный прогон dev
# Тонкий вызывающий файл (#623). Для этого события GitHub берёт workflow из
# ветки по умолчанию (`main`), поэтому здесь только то, что обязано жить там:
# триггеры, run-name, права и concurrency. Тело — `_nightly.yml` по ссылке
# `@dev`: правка конвейера — один коммит в `dev`, зеркало в `main` не нужно.
# Этот файл меняется, только когда меняются сами триггеры или потолок прав;
# тогда он зеркалится в `main`, и preflight `workflow_sync` (validate.yml)
# держит копии равными.
on:
# 02:17 UTC, не на круглой минуте: старты «ровно в час/полчаса» GitHub
# откладывает на часы (факт — 07:42–08:05 при плане 02:30, #658).
schedule:
- cron: '30 2 * * *'
- cron: '17 2 * * *'
workflow_dispatch: {}
permissions:
@@ -30,35 +20,12 @@ permissions:
contents: read
jobs:
dispatch:
name: "Запустить Validate на dev с полным набором и дождаться результата"
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
since=$(date -u +%FT%TZ)
gh workflow run validate.yml --repo "$REPO" --ref dev -f full=true
echo "Validate(dev, full=true) поставлен в очередь: $since"
# Найти именно этот прогон: workflow_dispatch на dev, созданный не
# раньше момента запуска. До трёх минут на появление в списке.
run_id=""
for _ in $(seq 1 18); do
sleep 10
run_id=$(gh run list --repo "$REPO" --workflow validate.yml --branch dev \
--event workflow_dispatch --json databaseId,createdAt --limit 5 \
--jq "[.[] | select(.createdAt >= \"$since\")] | sort_by(.createdAt) | last | .databaseId // empty")
[ -n "$run_id" ] && break
done
if [ -z "$run_id" ]; then
echo "::error::прогон Validate не появился за 3 минуты — dispatch не равен проверке"
exit 1
fi
url="${{ github.server_url }}/$REPO/actions/runs/$run_id"
echo "дочерний прогон: $url"
echo "- Validate(dev, full=true): $url" >> "$GITHUB_STEP_SUMMARY"
# Ждём завершения; красный дочерний прогон — красный ночной.
gh run watch "$run_id" --repo "$REPO" --exit-status --interval 30
# Потолок прав тела: объединение job-level прав `_nightly.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
actions: write
contents: read
uses: Matysh/houseplan-card/.github/workflows/_nightly.yml@dev # #623: тело конвейера из dev
secrets: inherit
+14 -2
View File
@@ -6,8 +6,12 @@ on:
push:
branches:
- main
paths-ignore:
- ".github/workflows/**"
- "docs/**"
schedule:
- cron: "0 4 * * 1"
# Минута не круглая (#658): доходит до main вместе со стабильным релизом.
- cron: "11 4 * * 1"
workflow_dispatch:
inputs:
comparison_ref:
@@ -28,7 +32,7 @@ jobs:
# Every profile keeps base and candidate sequential on one hosted runner.
# Independent profile pairs run in parallel: cross-profile timing is never
# compared, while serialising all profile pairs cannot fit the job timeout.
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
@@ -36,6 +40,7 @@ jobs:
profile:
- large-house
- isometric
- isometric-backdrop
- isometric-stage3
- plan-snap
- interaction
@@ -130,6 +135,10 @@ jobs:
npm run benchmark:large-house-isometric -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-baseline.json
npm run benchmark:large-house-isometric -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-candidate.json
;;
isometric-backdrop)
npm run benchmark:large-house-isometric-backdrop -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-baseline.json
npm run benchmark:large-house-isometric-backdrop -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-candidate.json
;;
isometric-stage3)
npm run benchmark:isometric-stage3-dense -- --allow-stage2-base --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-baseline.json
npm run benchmark:isometric-stage3-dense -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-candidate.json
@@ -184,6 +193,9 @@ jobs:
isometric)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric.json --baseline=../artifacts/performance/isometric-baseline.json --candidate=../artifacts/performance/isometric-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-comparison.json
;;
isometric-backdrop)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric-backdrop.json --baseline=../artifacts/performance/isometric-backdrop-baseline.json --candidate=../artifacts/performance/isometric-backdrop-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-backdrop-comparison.json
;;
isometric-stage3)
npm run benchmark:compare -- --budgets=demo/performance/budgets-isometric-stage3-dense.json --baseline=../artifacts/performance/isometric-stage3-baseline.json --candidate=../artifacts/performance/isometric-stage3-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-stage3-comparison.json
;;
+41
View File
@@ -0,0 +1,41 @@
name: Метрики процесса
# Тонкий вызывающий файл (#623). Для этого события GitHub берёт workflow из
# ветки по умолчанию (`main`), поэтому здесь только то, что обязано жить там:
# триггеры, run-name, права и concurrency. Тело — `_process-metrics.yml` по ссылке
# `@dev`: правка конвейера — один коммит в `dev`, зеркало в `main` не нужно.
# Этот файл меняется, только когда меняются сами триггеры или потолок прав;
# тогда он зеркалится в `main`, и preflight `workflow_sync` (validate.yml)
# держит копии равными.
on:
schedule:
# Понедельник 05:23 UTC — после ночных прогонов, до рабочего дня; минута
# не круглая, чтобы не стоять в очереди «ровных» расписаний (#658).
- cron: '23 5 * * 1'
workflow_dispatch:
inputs:
days:
description: "Окно в днях"
required: false
default: "7"
type: string
permissions:
contents: read
actions: read
issues: read
jobs:
# Потолок прав тела: объединение job-level прав `_process-metrics.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
contents: read
actions: read
issues: read
uses: Matysh/houseplan-card/.github/workflows/_process-metrics.yml@dev # #623: тело конвейера из dev
with:
days: ${{ inputs.days }}
secrets: inherit
+20 -38
View File
@@ -1,6 +1,14 @@
name: Сверка очереди ревью
run-name: "reconcile process queue · ${{ github.event_name }}"
# Тонкий вызывающий файл (#623). Для этого события GitHub берёт workflow из
# ветки по умолчанию (`main`), поэтому здесь только то, что обязано жить там:
# триггеры, run-name, права и concurrency. Тело — `_process-reconcile.yml` по ссылке
# `@dev`: правка конвейера — один коммит в `dev`, зеркало в `main` не нужно.
# Этот файл меняется, только когда меняются сами триггеры или потолок прав;
# тогда он зеркалится в `main`, и preflight `workflow_sync` (validate.yml)
# держит копии равными.
on:
schedule:
- cron: '7,37 * * * *'
@@ -18,41 +26,15 @@ permissions:
issues: read
jobs:
reconcile:
name: "Один снимок S4/S7 без polling модели"
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: process-reconcile
cancel-in-progress: false
steps:
# Расписание читается из main, а исполняемый reconciler — из dev: так
# после штатного merge действует та же версия кода, которую проверил CI.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: dev
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Сопоставить labels, requests, runs и sealed evidence
env:
GH_TOKEN: ${{ secrets.HP_PROCESS_TOKEN }}
REPO: ${{ github.repository }}
APPLY: ${{ github.event_name == 'schedule' || inputs.apply == true }}
run: |
mkdir -p artifacts/process-reconcile
node scripts/process-reconcile.mjs \
--repo "$REPO" \
--apply="$APPLY" \
--max-actions=5 \
--output=artifacts/process-reconcile/summary.json
- name: Опубликовать компактный machine-readable итог
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: process-reconcile-${{ github.run_id }}-${{ github.run_attempt }}
path: artifacts/process-reconcile/summary.json
if-no-files-found: error
retention-days: 14
# Потолок прав тела: объединение job-level прав `_process-reconcile.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
actions: read
contents: read
issues: read
uses: Matysh/houseplan-card/.github/workflows/_process-reconcile.yml@dev # #623: тело конвейера из dev
with:
apply: ${{ inputs.apply == true }}
secrets: inherit
+33
View File
@@ -0,0 +1,33 @@
name: Продолжение ревью после Validate
run-name: "resume · ${{ github.event.workflow_run.head_branch }} · ${{ github.event.workflow_run.conclusion }}"
# Тонкий вызывающий файл (#623). Для этого события GitHub берёт workflow из
# ветки по умолчанию (`main`), поэтому здесь только то, что обязано жить там:
# триггеры, run-name, права и concurrency. Тело — `_process-resume.yml` по ссылке
# `@dev`: правка конвейера — один коммит в `dev`, зеркало в `main` не нужно.
# Этот файл меняется, только когда меняются сами триггеры или потолок прав;
# тогда он зеркалится в `main`, и preflight `workflow_sync` (validate.yml)
# держит копии равными.
on:
workflow_run:
workflows: ["Проверка (CI)"]
types: [completed]
permissions:
contents: read
actions: read
jobs:
# Потолок прав тела: объединение job-level прав `_process-resume.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
# Тот же фильтр, что у job `resume` тела: посторонние события не
# поднимают вызов, а прогон остаётся skipped, как до #623.
if: github.event.workflow_run.event == 'workflow_dispatch' && startsWith(github.event.workflow_run.head_branch, 'issue/')
permissions:
contents: read
actions: read
uses: Matysh/houseplan-card/.github/workflows/_process-resume.yml@dev # #623: тело конвейера из dev
secrets: inherit
File diff suppressed because it is too large Load Diff
+15 -3
View File
@@ -21,7 +21,9 @@ concurrency:
jobs:
gate:
name: "Гейт: зелёная Проверка и релизный контракт"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# Больше ожидания зелёного Validate в release-gate.mjs (до 60 мин) (#658).
timeout-minutes: 75
outputs:
sha: ${{ steps.candidate.outputs.sha }}
tag: ${{ steps.candidate.outputs.tag }}
@@ -89,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 }}
@@ -122,7 +132,8 @@ jobs:
publish:
name: Публикация тега и релиза
needs: gate
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
outputs:
url: ${{ steps.verify.outputs.url }}
newly_published: ${{ steps.release.outputs.newly_published }}
@@ -289,7 +300,8 @@ jobs:
close-merged:
name: Закрытие вошедших issue
needs: [gate, publish]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
actions: read
+361
View File
@@ -0,0 +1,361 @@
name: "Релиз: независимое ревью линии"
run-name: "Release review ${{ inputs.tag }}"
# #638, PROCESS.md §11.5: перед стабильным релизом — одно независимое ревью
# поверхностей, изменённых всей линией бет, «с нуля»: без ТЗ и документов
# раундов, по SCOPE и USER-GUIDE. Инкрементальное ревью судит дифф задачи
# против её ТЗ; четыре дефекта линии 1.77 (#607, #608, #611, #619) не входили
# ни в один AC и нашлись только так.
#
# Решение владельца 2026-09-25: ревью НЕ блокирует выпуск. `release.yml`
# запускает этот workflow параллельно гейтам из job, от которого не зависит ни
# один job выпуска; документ — рекомендация, в работу его берёт владелец.
#
# Только `workflow_dispatch`: GitHub исполняет файл с той ветки, на которой
# запущен прогон (`release.yml` зовёт `--ref dev`), поэтому зеркало в `main`
# не нужно, а правка — один коммит в `dev`.
#
# Три job, как у конвейера (#551, #556): детерминированная подготовка,
# недоверенная модель без единого права на запись, детерминированная
# публикация документа в `dev`.
on:
workflow_dispatch:
inputs:
tag:
description: "Stable release tag, for example v1.78.0"
required: true
type: string
candidate:
description: "Exact candidate SHA; empty = the commit of the tag"
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: release-review-${{ inputs.tag }}
cancel-in-progress: false
jobs:
prepare:
name: "Ревью релиза: вход линии"
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
outputs:
proceed: ${{ steps.line.outputs.proceed }}
candidate: ${{ steps.line.outputs.candidate }}
base: ${{ steps.line.outputs.base }}
doc: ${{ steps.line.outputs.doc }}
issues: ${{ steps.line.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: Кандидат, база и issue линии
id: line
env:
TAG: ${{ inputs.tag }}
CANDIDATE: ${{ inputs.candidate }}
FORCE: ${{ inputs.force }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
# Отказ prepare за `| tee` не должен идти дальше с неполным
# GITHUB_OUTPUT и proceed=true (#751).
set -o pipefail
doc=$(node scripts/release-review.mjs doc --tag="$TAG")
git fetch -q --tags origin
if [ -z "$CANDIDATE" ]; then
CANDIDATE=$(git rev-parse --verify -q "refs/tags/$TAG^{commit}") || {
echo "::error::тега $TAG нет, а кандидат не передан"; exit 1; }
fi
git cat-file -e "$CANDIDATE^{commit}"
CANDIDATE=$(git rev-parse "$CANDIDATE^{commit}")
# Повтор на тот же тег не тратит модель: документ уже есть (§11.5).
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/release-review-input"
node scripts/release-review.mjs prepare --tag="$TAG" --candidate="$CANDIDATE" \
--out="$out" --run-url="$RUN_URL" | tee -a "$GITHUB_OUTPUT"
echo "candidate=$CANDIDATE" >> "$GITHUB_OUTPUT"
echo "proceed=true" >> "$GITHUB_OUTPUT"
(cd "$out" && sha256sum brief.md line-membership.json > manifest.sha256)
cat "$out/brief.md" >> "$GITHUB_STEP_SUMMARY"
- name: Передать вход модели
if: steps.line.outputs.proceed == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-review-input-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/release-review-input
if-no-files-found: error
retention-days: 3
model_review:
name: "Ревью релиза: работа модели"
needs: prepare
if: needs.prepare.outputs.proceed == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 60
# Недоверенная стадия. Прав на запись нет никаких: ни в репозиторий, ни в
# issue. Документ публикует `publish`; находки в issue превращает владелец.
# `github_token` у шага Review обязателен (#556): без него 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: release-review-input-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/release-review-input
- name: Проверить вход и кандидата
env:
CANDIDATE: ${{ needs.prepare.outputs.candidate }}
run: |
(cd "$RUNNER_TEMP/release-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 }}/release-review.md
REVIEW_INPUT: ${{ runner.temp }}/release-review-input
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# #704: `release.yml` ставит ревью в очередь токеном GITHUB_TOKEN, и
# прогон начинает `github-actions[bot]` (тип Bot). Action отказывает
# ботам, которых нет в списке («Workflow initiated by non-human actor:
# github-actions (type: Bot)», v1.78.0). Разрешён ровно этот бот —
# не '*': любой другой бот по-прежнему отклоняется. Action сравнивает
# имена без регистра и без суффикса `[bot]`. Человек, запустивший
# ревью руками, списка не касается.
allowed_bots: "github-actions[bot]"
path_to_claude_code_executable: ${{ steps.claude_bin.outputs.path }}
prompt: |
Ты независимый ревьюер релиза House Plan. Язык ответа — русский.
Релиз: ${{ inputs.tag }} · кандидат ${{ needs.prepare.outputs.candidate }}
(рабочая копия уже на нём) · база линии: ${{ needs.prepare.outputs.base || 'нет' }}.
Вход линии — файл $REVIEW_INPUT/brief.md (issue линии, доказанные
трейлерами, и изменённые продуктовые файлы) и
$REVIEW_INPUT/line-membership.json.
Правила этого ревью — docs/process/REVIEWER.md, раздел «Независимое
ревью линии», канон — PROCESS.md §11.5. Прочитай их первыми.
Главное, что нельзя пропустить:
- Ты судишь ПОВЕРХНОСТИ, изменённые линией, против ПОЛЬЗОВАТЕЛЯ, а
не дифф против ТЗ. Не читай ТЗ задач (раздел «## ТЗ» в issue,
docs/specs/**) и документы раундов (docs/reviews/**): независимость
и есть смысл шага. Основа суждения — docs/SCOPE.md (персоны и
работы) и docs/USER-GUIDE.ru.md (что обещано пользователю).
- Проверяй исполнением, а не чтением: собери бандл
(`npm run bundle:sync`), открой затронутые поверхности в браузере
(стенд `demo/`, смоки `demo/smoke_*.mjs`), вводи текст
посимвольно, закрывай диалоги настоящим Escape и крестиком. Где
есть диалоги HA — пиннутая фикстура `ha-dialog` (#505,
demo/helpers/README-ha-dialog.md,
`node demo/verify_ha_dialog_discard_recovery.mjs`).
- Для каждой поверхности: обычный сценарий и самый рискованный
соседний, шесть классов риска (async, данные/права, геометрия,
визуал, объём/perf, host/input) — PROCESS.md §2.6.
- Ты ничего не правишь и не публикуешь: ни код, ни issue, ни
комментарии. Права на запись у тебя нет. Любые изменения рабочей
копии будут отброшены — после проверок восстанови её сам
(`git checkout -- . && git clean -fd`), если что-то менял.
Находки: High / Medium / Low, у каждой — поверхность, воспроизведение
(команда или шаги), что увидит пользователь, какая персона задета.
Выпуск это ревью не останавливает: документ — рекомендация владельцу.
Напиши полный документ в файл по пути из переменной REVIEW_DOC
(абсолютный, вне репозитория). Разделы: что входило в линию,
поверхности и как каждая проверялась (команда → результат),
находки, что проверено и корректно, чего не проверял и почему.
Первой строкой после заголовка — `Итог: High N · Medium N · Low N`.
Затем верни JSON по схеме — последнее обязательное действие.
claude_args: |
--max-turns 200
--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 }}/release-review.md
OUT: ${{ steps.review.outputs.structured_output }}
run: |
test -s "$SOURCE" || { echo "::error::модель не оставила документ ревью"; exit 1; }
dir="$RUNNER_TEMP/release-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/release-review.md"
(cd "$dir" && sha256sum release-review.md result.json > manifest.sha256)
- name: Передать результат публикации
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-review-result-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/release-review-result
if-no-files-found: error
retention-days: 3
publish:
name: "Ревью релиза: документ в 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: release-review-result-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/release-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/release-review-result"
(cd "$dir" && sha256sum -c manifest.sha256)
test "$DOC" = "$(node scripts/release-review.mjs doc --tag="$TAG")"
counts=$(jq -r '"High \(.high) · Medium \(.medium) · Low \(.low)"' "$dir/result.json")
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#723).
msg="$RUNNER_TEMP/release-review-commit.txt"
push_err="$RUNNER_TEMP/release-review-push.stderr"
for attempt in 1 2 3; do
git fetch -q origin dev
git reset -q --hard origin/dev
git clean -fdq
mkdir -p docs/reviews
{
cat "$dir/release-review.md"
printf '\n\n<!-- hp-release-review-anchors -->\n### Материал ревью\n\n```\n'
printf 'tag %s\ncandidate %s\nbase %s\nissues %s\nrun %s\n' \
"$TAG" "$CANDIDATE" "${BASE:-—}" "${ISSUES:-—}" "$RUN_URL"
printf '```\n'
} > "$DOC"
node scripts/reviews-index.mjs --dir=docs/reviews --strict
git add -- "$DOC" docs/reviews/INDEX.md
git diff --cached --name-only | node scripts/review-doc-guard.mjs
{
echo "docs: release review for $TAG"
echo ""
echo "Независимое ревью линии перед стабильным релизом (PROCESS.md §11.5)."
echo "Итог: $counts. Выпуск не блокирует; решение по находкам — за владельцем."
echo ""
echo "Issue: #638"
echo "User-Visible: no"
} > "$msg"
git -c user.name="claude[bot]" \
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
commit -q -F "$msg"
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
echo "### Независимое ревью $TAG" >> "$GITHUB_STEP_SUMMARY"
echo "Итог: $counts — \`$DOC\` в dev. Выпуск не блокируется." >> "$GITHUB_STEP_SUMMARY"
echo "::notice::$DOC опубликован: $counts"
exit 0
fi
# #723: сдвигом dev считается только устаревший lease — отказ
# разбирает код слияния (merge-candidate.mjs --push-refusal, #705).
# Отказ GitHub (право на workflow, правило ветки, хук) повтор не
# лечит: шаг останавливается, причина и ответ git без токена — в
# журнале и в сводке шага.
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
--stage=release-review --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
if [ "$kind" != "stale" ]; then
echo "::error::push $DOC в dev отклонён ($kind) — это не сдвиг dev, повтор не поможет; причина и ответ git — выше и в сводке шага"
exit 1
fi
echo "::warning::dev ушёл вперёд — попытка $attempt из 3, документ собирается заново"
sleep $((attempt * 10))
done
echo "::error::документ ревью не опубликован в dev за три попытки"
exit 1
+88 -5
View File
@@ -48,7 +48,8 @@ jobs:
name: "Кандидат: точный SHA, режим и черновик"
# Публикация беты руками — не наш случай: у бет свой staged-путь.
if: ${{ github.event_name == 'workflow_dispatch' || !github.event.release.prerelease }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
sha: ${{ steps.resolve.outputs.sha }}
tag: ${{ steps.resolve.outputs.tag }}
@@ -131,10 +132,89 @@ jobs:
echo "release $TAG is public: repair mode — only missing assets may be added"
fi
independent-review:
name: "Независимое ревью линии (не блокирует выпуск)"
# #638, PROCESS.md §11.5. Решение владельца 2026-09-25: ревью идёт
# параллельно гейтам и выпуск не ждёт и не останавливает. Поэтому этот job
# только ставит в очередь `release-review.yml` на `dev` и ни один job
# выпуска от него не зависит (`needs` на него запрещён тестом
# release-workflow); его отказ — предупреждение, а не красный релиз.
# Беты пропускаются: ревью линии — перед стабильным.
needs: candidate
if: ${{ needs.candidate.outputs.prerelease != 'true' }}
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: write
steps:
- name: Поставить в очередь ревью линии
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.candidate.outputs.tag }}
SHA: ${{ needs.candidate.outputs.sha }}
# #704: сколько ждать, что dispatch стал прогоном, — выпуск не ждёт
# дольше нескольких минут; укладывается в timeout-minutes job.
APPEAR_SECONDS: 180
POLL_SECONDS: 15
run: |
repo="${{ github.repository }}"
# Отсчёт окна — до dispatch, с запасом минута на расхождение часов.
since=$(( $(date -u +%s) - 60 ))
if ! gh workflow run release-review.yml --repo "${{ github.repository }}" --ref dev \
-f tag="$TAG" -f candidate="$SHA"; then
echo "::warning::ревью линии $TAG не запущено — выпуск продолжается; запустить руками: gh workflow run release-review.yml --ref dev -f tag=$TAG -f candidate=$SHA"
echo "Независимое ревью $TAG: **не запущено** — dispatch release-review.yml отклонён." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
# #704: dispatch не возвращает прогона, и v1.78.0 показал цену: ревью
# упало в прогоне, о котором выпуск не знал. Прогон ищется по workflow,
# ветке dev, событию, времени и run-name «Release review <тег>».
# Найден и стартовал — ссылка и статус в сводке; нет — предупреждение,
# выпуск не блокируется.
title="Release review $TAG"
run=""
started=false
tries=$(( APPEAR_SECONDS / POLL_SECONDS ))
for try in $(seq 1 "$tries"); do
found=$(gh run list --repo "$repo" --workflow release-review.yml --branch dev \
--event workflow_dispatch --limit 20 \
--json databaseId,url,status,conclusion,displayTitle,createdAt \
| jq -c --arg title "$title" --argjson since "$since" \
'[.[] | select(.displayTitle == $title and (.createdAt | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) >= $since)] | sort_by(.createdAt) | last // empty') || found=""
if [ -n "$found" ]; then
run="$found"
case "$(jq -r .status <<< "$run")" in
in_progress|completed) started=true; break ;;
esac
fi
if [ "$try" -lt "$tries" ]; then sleep "$POLL_SECONDS"; fi
done
minutes=$(( (APPEAR_SECONDS + 59) / 60 ))
if [ -z "$run" ]; then
echo "::warning::прогон ревью линии $TAG не появился за $minutes мин — выпуск продолжается; проверить Actions → release-review.yml"
echo "Независимое ревью $TAG: **не стартовало за $minutes мин** — прогон release-review.yml на dev не найден." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
url=$(jq -r .url <<< "$run")
status=$(jq -r '.status + (if (.conclusion // "") != "" then " · " + .conclusion else "" end)' <<< "$run")
if [ "$started" != "true" ]; then
echo "::warning::ревью линии $TAG в очереди и не стартовало за $minutes мин: $url"
echo "Независимое ревью $TAG: **не стартовало за $minutes мин** (в очереди) — [прогон]($url), статус $status." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$(jq -r '.conclusion // ""' <<< "$run")" = "failure" ]; then
echo "::warning::ревью линии $TAG уже упало: $url"
fi
echo "Независимое ревью $TAG **запущено**: [прогон]($url), статус $status. Выпуск его не ждёт." >> "$GITHUB_STEP_SUMMARY"
gate:
name: "Гейт: контракт, Validate, Full Performance и E2E на точном SHA"
needs: candidate
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# Больше суммы собственных ожиданий job: Validate и Full Performance ждутся
# до 60 мин каждый (release-gate.mjs), E2E — до 45 (e2e-gate.mjs): 165 < 180 (#658).
timeout-minutes: 180
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
@@ -204,7 +284,8 @@ jobs:
stage:
name: "Сборка: ассеты, SHA256SUMS и загрузка в черновик"
needs: [candidate, gate]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
@@ -344,7 +425,8 @@ jobs:
publish:
name: "Публикация и сверка публичных байтов"
needs: [candidate, gate, stage]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
url: ${{ steps.verify.outputs.url }}
name: ${{ steps.verify.outputs.name }}
@@ -444,7 +526,8 @@ jobs:
# make that distribution failure impossible to miss in the release run.
if: ${{ needs.candidate.outputs.prerelease == 'true' }}
needs: [candidate, publish]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Verify the published tag is the prerelease HACS will discover
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
+53
View File
@@ -0,0 +1,53 @@
name: "Бета: пакетное ревью ship"
run-name: "Ship review ${{ inputs.tag }}"
# Тонкий вызывающий файл (#716, устройство #623). `workflow_dispatch` GitHub
# исполняет с выбранной ветки, но кнопку и сам запуск (`gh workflow run`)
# даёт только workflow, чей файл лежит в ветке по умолчанию (`main`). Поэтому
# здесь только то, что обязано жить там: триггер, входы ручного запуска,
# run-name, права и concurrency. Тело — `_ship-review.yml` по ссылке `@dev`:
# правка ревью — один коммит в `dev`, а запуск с любой ветки (в кнопке по
# умолчанию выбрана `main`) исполняет тело из `dev`.
# Этот файл меняется, только когда меняются входы или потолок прав; тогда он
# зеркалится в `main`, и preflight `workflow_sync` (validate.yml) держит копии
# равными.
on:
workflow_dispatch:
inputs:
tag:
description: "Beta tag the review is for, for example v1.79.0-beta.1"
required: true
type: string
candidate:
description: "Exact candidate SHA; empty = the dev tip"
required: false
type: string
default: ""
force:
description: "Review again even when the document already exists in dev"
required: false
type: boolean
default: false
permissions:
contents: read
concurrency:
group: ship-review-${{ inputs.tag }}
cancel-in-progress: false
jobs:
# Потолок прав тела: объединение job-level прав `_ship-review.yml`. Вызываемый
# workflow может права только сузить, поэтому каждая его job по-прежнему
# получает свой прежний минимум (#556), а шире этого набора не получит никто.
dev:
permissions:
contents: read
issues: read
uses: Matysh/houseplan-card/.github/workflows/_ship-review.yml@dev # #716: тело ревью из dev
with:
tag: ${{ inputs.tag }}
candidate: ${{ inputs.candidate }}
force: ${{ inputs.force }}
secrets: inherit
+278 -69
View File
@@ -21,12 +21,11 @@ on:
description: 'Полный набор: смоки, golden, performance_smoke (без мутантов — #601)'
type: boolean
default: true
# Мутанты по диффу бегут только по явному запросу (#510, #601):
# ревью-конвейер и слияние кандидата запускают `-f full=false
# -f mutants=true` на материале. `full=true`, трейлер `Release:` и ночь
# их не включают — ночью идёт полный реестр (mutation-gate.yml).
# #709: мутанты в разработке не гоняются — вход оставлен, чтобы старые
# вызовы `-f mutants=…` не падали, но job мутантов по диффу он больше не
# включает (classify-changes.mjs). Весь реестр — ночью (mutation-gate.yml).
mutants:
description: 'Мутанты по диффу на этом SHA (кандидат ревью или слияния)'
description: 'Не действует с #709: мутанты — только ночной полный прогон'
type: boolean
default: false
@@ -50,14 +49,16 @@ jobs:
# действовать — на уровне шагов, с той же гранулярностью в логе.
preflight:
name: "Предполёт: документация, провенанс, процесс"
runs-on: ubuntu-latest
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 нужны сообщения
# коммитов, трейлеры и ИМЕНА изменённых файлов, а не содержимое старых
@@ -66,9 +67,11 @@ jobs:
#
# Коммиты и деревья скачиваются целиком, поэтому диапазоны и `merge-base`
# работают как раньше. Единственная догрузка по требованию здесь —
# `git show origin/main:.github/workflows/process.yml`: один блоб.
# `git show origin/main:.github/workflows/<тонкий файл>`: по блобу на файл.
# `fetch-tags: true` (#703): теги релиза — граница проверенного материала
# для базы диапазона, а checkout по умолчанию качает с `--no-tags`.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: { fetch-depth: 0, filter: 'blob:none' }
with: { fetch-depth: 0, filter: 'blob:none', fetch-tags: true }
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with: { node-version: 22 }
@@ -80,42 +83,110 @@ 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
# Конвейер читает `process.yml` из ветки по умолчанию, поэтому файл обязан
# совпадать в `main` и `dev`. До этой проверки совпадение держалось на
# дисциплине: каждая правка требовала двух пушей и ручной сверки.
- name: "Процесс: process.yml и mutation-gate.yml идентичны в main и dev"
# #635: `docs/reviews/INDEX.md` — снимок каталога ревью; расхождение с
# каталогом — невидимые через индекс документы (r2 #635 H1). Гейт стоит
# только на push в dev: там свежесть держит конвейер — пересборка при
# публикации документа и после каждого своего ребейза (приведение к dev,
# слияние кандидата). На issue-ветках не судится: их ребейзит и
# публикует в них конвейер из ветки по умолчанию, и доехавший туда
# документ раунда индекс ветки знать не обязан — красить автора за
# коммит конвейера нельзя; расхождение излечивает слияние.
- name: "Индекс ревью совпадает с каталогом docs/reviews"
id: reviews_index
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
continue-on-error: true
run: node scripts/reviews-index.mjs --dir=docs/reviews --check
# #623: для событий `issues`, `schedule` и `workflow_run` GitHub берёт
# workflow из ветки по умолчанию (`main`). Там лежат тонкие вызывающие
# файлы — триггеры, run-name, права, concurrency, — а тело каждого
# (`_<имя>.yml`) они вызывают по ссылке `@dev`. Правка конвейера — один
# коммит в `dev`; тела здесь не сверяются: их копия в `main` не исполняется
# ни одним событием. Тонкий файл сверяется: правка триггера или потолка
# прав, не доехавшая до `main`, действовала бы только в dev-копии.
#
# #716: `ship-review.yml` и `beta-derived.yml` — только `workflow_dispatch`,
# он исполняет файл с выбранной ветки, но запуск GitHub даёт лишь workflow,
# чей файл есть в `main`. Шагам беты он нужен до промоушена, поэтому они
# устроены так же и сверяются здесь же: правка входа или потолка прав,
# не доехавшая до `main`, не действовала бы в запуске с `main`, а именно
# его кнопка выбирает по умолчанию.
#
# `performance.yml` не входит: по расписанию он судит `main` собственным
# телом из `main`, расхождение с `dev` до промоушена законно. Список ниже
# держит равным множеству тонких файлов
# test/default-branch-workflows.test.mjs.
- name: "Процесс: тонкие вызывающие workflow идентичны в main и dev"
id: workflow_sync
continue-on-error: true
run: |
git fetch --quiet origin main dev
# #472: расписание mutation-gate.yml тоже исполняется из ветки по
# умолчанию — та же ловушка, что у process.yml. Сверяются оба.
status=0
for file in process.yml mutation-gate.yml; do
for file in process.yml mutation-gate.yml process-resume.yml nightly.yml process-reconcile.yml process-metrics.yml ship-review.yml beta-derived.yml; do
if diff <(git show "origin/main:.github/workflows/$file") \
<(git show "origin/dev:.github/workflows/$file"); then
echo "$file: main и dev идентичны"
else
echo "РАСХОЖДЕНИЕ: $file в main и dev различаются."
echo "Файл исполняется из ветки по умолчанию, поэтому"
echo "правку нужно отправить в обе ветки."
echo "Тонкий вызывающий файл исполняется из ветки по умолчанию,"
echo "поэтому правку триггеров или прав нужно зеркалить в main (#623)."
status=1
fi
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), и тогда его коммиты не судит
@@ -125,9 +196,14 @@ jobs:
# прежним `before`: расширять диапазон здесь нельзя, иначе гейт, который
# сам красит прогон, лишает следующий пуш зелёного предка и запирает dev
# в красноте навсегда.
#
# #703: то же на `main`. Stable-промоушен — пуш в `main` SHA, уже
# судимого на `dev`; с `event.before` (прошлый stable) провенанс и
# процессный гейт судили бы всю бета-линию заново. Прогоны читаются с
# обеих интеграционных веток, сам HEAD засчитывает только успешный.
- name: "База диапазона: последний доказанно зелёный предок"
id: range
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
if: github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.sha }}
@@ -137,11 +213,14 @@ jobs:
# `status=completed`, а не `success`: гейтам диапазона нужен факт
# «коммит судили», а не «вердикт был оправдательный». Упавший прогон
# коммит судил; отменённый — нет, его отсеивает judgedShas (#388).
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch=dev -f status=completed -F per_page=100 \
> /tmp/validate-runs.json || echo '{}' > /tmp/validate-runs.json
for branch in dev main; do
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch="$branch" -f status=completed -F per_page=100 \
> "/tmp/validate-runs-$branch.json" || echo '{}' > "/tmp/validate-runs-$branch.json"
done
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
--fallback="$FALLBACK" --runs=/tmp/validate-runs.json
--fallback="$FALLBACK" \
--runs=/tmp/validate-runs-dev.json --runs=/tmp/validate-runs-main.json
- name: "Провенанс коммитов: трейлеры и эталоны"
id: provenance
continue-on-error: true
@@ -195,6 +274,8 @@ jobs:
PROVENANCE: ${{ steps.provenance.outcome }}
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() {
@@ -206,12 +287,30 @@ 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 "process.yml и mutation-gate.yml в 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"
# skipped — шаг не судил (не push в dev), это не отказ
[ "$REVIEWS_INDEX" = "skipped" ] || check "индекс ревью совпадает с каталогом" "$REVIEWS_INDEX"
exit $fail
# Классификация изменённых путей: тяжёлые job идут только там, где менялось
@@ -223,7 +322,8 @@ jobs:
# там делает код-ревью, которое гоняет гейты само (#127).
changes:
name: Классификация изменённых файлов
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
# Чтение списка прогонов Validate — единственное, что этой job нужно сверх
# содержимого репозитория (#387).
permissions:
@@ -243,20 +343,23 @@ jobs:
mutants_requested: ${{ steps.heavy.outputs.mutants_requested }} # #510
base: ${{ steps.base.outputs.base }}
# `base` — до какого коммита классифицировать файлы ветки (#387),
# `range_base` — от какого судить диапазон на dev (#388).
# `range_base` — от какого судить диапазон на dev/main (#388, #619).
range_base: ${{ steps.base.outputs.range_base }}
steps:
# `git diff --name-only` содержимого файлов не читает вовсе, поэтому
# блобы истории этой job не нужны ни на одном шаге (#345).
# блобы истории этой job не нужны ни на одном шаге (#345). Теги — нужны:
# они граница проверенного материала для `range_base` (#703), а checkout
# по умолчанию передаёт `git fetch --no-tags`.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: { fetch-depth: 0, filter: 'blob:none' }
with: { fetch-depth: 0, filter: 'blob:none', fetch-tags: true }
# База диапазона — самый новый предок с УСПЕШНО завершённым Validate
# (#387). Прежде бралась голова предыдущего пуша (`github.event.before`),
# то есть допущение «до этого всё проверено». Concurrency отменяет прогон
# предыдущего пуша штатно, и на #86 (r5) это дало зелёный статус ветки
# без единого исполненного тяжёлого гейта.
# Смоки, golden, performance_smoke — на кандидате беты (трейлер
# `Release:`), по `workflow_dispatch full=true`, на PR (#479). Логика и
# `Release:` вне веток задач, #697), по `workflow_dispatch full=true`, на
# PR (#479). Логика и
# её тест — scripts/classify-changes.mjs.
- id: heavy
env:
@@ -264,7 +367,12 @@ jobs:
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
FULL_INPUT: ${{ inputs.full }}
MUTANTS_INPUT: ${{ inputs.mutants }}
run: node scripts/classify-changes.mjs --heavy | tee -a "$GITHUB_OUTPUT"
REF_NAME: ${{ github.ref_name }}
# #751: без pipefail упавший classify-changes оставлял heavy пустым —
# тяжёлые job молча пропускались, а job changes зеленела.
run: |
set -o pipefail
node scripts/classify-changes.mjs --heavy | tee -a "$GITHUB_OUTPUT"
- id: base
if: github.event_name != 'pull_request'
env:
@@ -283,11 +391,18 @@ jobs:
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch="$BRANCH" -f status=completed -F per_page=100 \
> /tmp/validate-runs.json || echo '{}' > /tmp/validate-runs.json
if [ "$REF" = "refs/heads/dev" ]; then
# На dev классифицировать нечего (всё true), но база диапазона
# нужна гейту «новый код не добавляет any» в job frontend (#388).
if [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; then
# На интеграционных ветках классифицировать нечего (всё true), но
# база диапазона нужна гейту «новый код не добавляет any» в job
# frontend (#388, #619). #703: прогоны обеих интеграционных веток —
# на `main` приходит SHA, уже судимый на `dev`.
other=dev; [ "$BRANCH" = "dev" ] && other=main
gh api -X GET "repos/$REPO/actions/workflows/validate.yml/runs" \
-f branch="$other" -f status=completed -F per_page=100 \
> /tmp/validate-runs-other.json || echo '{}' > /tmp/validate-runs-other.json
node scripts/classify-base.mjs --head="$HEAD_SHA" --mode=range \
--name=range_base --fallback="$FALLBACK" --runs=/tmp/validate-runs.json
--name=range_base --fallback="$FALLBACK" \
--runs=/tmp/validate-runs.json --runs=/tmp/validate-runs-other.json
exit 0
fi
merge_base=$(git merge-base origin/dev "$HEAD_SHA" || git rev-parse "$HEAD_SHA~1")
@@ -302,8 +417,8 @@ jobs:
HEAD_SHA: ${{ github.sha }}
REF: ${{ github.ref }}
run: |
if [ "$REF" = "refs/heads/dev" ]; then
echo "dev: без фильтров, всё true"
if [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; then
echo "dev/main: без фильтров, всё true"
node scripts/classify-changes.mjs --all >> "$GITHUB_OUTPUT"
exit 0
fi
@@ -381,7 +496,8 @@ jobs:
# performance_smoke включает набор, иначе glow-only результат засчитался бы
# прогону, которому нужен изометрический профиль.
needs: changes
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
smoke: ${{ steps.probe.outputs.smoke }}
golden: ${{ steps.probe.outputs.golden }}
@@ -530,7 +646,8 @@ jobs:
name: "HACS: валидация репозитория"
needs: changes
if: needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: HACS validation
@@ -542,7 +659,8 @@ jobs:
name: "Hassfest: манифест интеграции"
needs: changes
if: needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Hassfest validation
@@ -560,6 +678,10 @@ jobs:
# слияние кандидата запускают Validate по кнопке с `mutants=true` на
# материале; PR берёт её сам. За 08–09.09 на промежуточных пушах она стоила
# 48 из 56 часов job-минут и в основном отменялась следующим пушем. С #601
# С #709 job не запрашивает никто: мутанты в разработке не гоняются, весь
# реестр проверяет ночь (mutation-gate.yml). Job оставлен до отдельной уборки
# вместе с контрактом имён job (#622); `mutants_requested` всегда false.
# Ниже — история до #709.
# её не запрашивают ни `full=true`, ни кандидат беты (`Release:`): к бете
# каждая задача уже прогнана мутантами на ревью и на слитом кандидате, а
# ночь покрыта полным реестром. Когда мутанты запрошены, job бежит даже при
@@ -573,7 +695,7 @@ jobs:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -633,29 +755,47 @@ jobs:
fi
echo "диапазон: $base..$HEAD_SHA"
echo "base=$base" >> "$GITHUB_OUTPUT"
count=$(node scripts/mutation-gate.mjs --changed="$base..$HEAD_SHA" --shard="$SHARD/6" \
--ledger="artifacts/mutation-ledger/shard-$SHARD.json" --plan-only \
| tee /dev/stderr | sed -n 's/^plan=//p' | tail -1)
plan="$RUNNER_TEMP/mutation-plan-$SHARD.txt"
node scripts/mutation-gate.mjs --changed="$base..$HEAD_SHA" --shard="$SHARD/6" \
--ledger="artifacts/mutation-ledger/shard-$SHARD.json" --plan-only > "$plan"
cat "$plan"
count=$(sed -n 's/^plan=//p' "$plan" | tail -1)
echo "к прогону в этом шарде: ${count:-0}"
echo "count=${count:-0}" >> "$GITHUB_OUTPUT"
# #620: план называет и окружение своих гардов. Непрочитанная строка
# при непустом плане значит «ставить»: лишняя установка стоит минуты,
# недоустановка — красный чистый прогон и лишний круг задачи.
if [ "${count:-0}" != "0" ]; then
browser=$(sed -n 's/^plan-browser=//p' "$plan" | tail -1)
python=$(sed -n 's/^plan-python=//p' "$plan" | tail -1)
echo "окружение шарда: браузер ${browser:-true}, Python ${python:-true}"
echo "browser=${browser:-true}" >> "$GITHUB_OUTPUT"
echo "python=${python:-true}" >> "$GITHUB_OUTPUT"
fi
- if: steps.plan.outputs.count != '0'
run: npm ci
- if: steps.plan.outputs.count != '0'
# Python и зависимости бэкенда — только шарду с pytest-гардами (#620);
# колёса pip кэшируются по requirements.txt.
- if: steps.plan.outputs.count != '0' && steps.plan.outputs.python == 'true'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: '3.14'
cache: pip
cache-dependency-path: tests_backend/requirements.txt
- name: Backend test dependencies
if: steps.plan.outputs.count != '0'
if: steps.plan.outputs.count != '0' && steps.plan.outputs.python == 'true'
run: pip install -r tests_backend/requirements.txt
# Chromium — только шарду с браузерными гардами (#620): смоки, golden и
# юниты, чей граф исполнения импортирует Playwright.
- name: Кэш браузеров Playwright
id: pw
if: steps.plan.outputs.count != '0'
if: steps.plan.outputs.count != '0' && steps.plan.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.plan.outputs.count != '0' && steps.pw.outputs.cache-hit != 'true'
if: steps.plan.outputs.count != '0' && steps.plan.outputs.browser == 'true' && steps.pw.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Затронутые мутанты ловятся
if: steps.plan.outputs.count != '0'
@@ -684,7 +824,8 @@ jobs:
name: "Фронтенд: типы, юниты, мутанты, синхрон бандла"
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
# Полная история без блобов (#342): гейт «новый код не добавляет any»
# diff-aware, ему нужен диапазон, а содержимое старых ревизий — нет.
@@ -699,8 +840,10 @@ jobs:
run: npm run typecheck
# Долг типизации снимается при извлечении подсистем (#34), а не разовой
# заменой: в src/** его 1034 вхождения в 49 файлах. Гейт держит приращение
# на нуле — судит только добавленные строки (#342).
- name: "Новый код не добавляет any"
# на нуле — судит только добавленные строки (#342). Тем же диапазоном
# судится и приращение записей смоков в приватное состояние карточки
# (#629): больше двух тысяч старых не трогаем, новых не добавляем.
- name: "Новый код не добавляет any и приватных записей в смоках"
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
@@ -713,11 +856,12 @@ jobs:
git fetch -q origin dev
zero=$(printf '%040d' 0)
base=""
# Пуш прямо в dev: диапазон — то, что добавлено с последнего
# ДОКАЗАННО зелёного предка (#388), а не с головы предыдущего пуша:
# его прогон штатно отменяется следующим, и добавленные им строки не
# судил бы никто. Фолбэк — прежний `before`.
if [ "$EVENT_NAME" != "pull_request" ] && [ "$REF" = "refs/heads/dev" ]; then
# Пуш прямо в dev/main: диапазон — то, что добавлено с последнего
# ДОКАЗАННО зелёного предка (#388, #619), а не с головы предыдущего
# пуша: его прогон штатно отменяется следующим, и добавленные им
# строки не судил бы никто. Фолбэк — прежний `before`.
if [ "$EVENT_NAME" != "pull_request" ] \
&& { [ "$REF" = "refs/heads/dev" ] || [ "$REF" = "refs/heads/main" ]; }; then
base="${PROVEN_BASE:-$BEFORE_SHA}"
fi
# Ветка, PR, новая история: точки отсчёта нет, берём merge-base с dev.
@@ -730,16 +874,31 @@ jobs:
echo "не нашёл базу диапазона — гейт пропущен, это увидит следующий пуш"
exit 0
fi
node scripts/no-new-any.mjs --base "$base" --head "$HEAD_SHA"
- name: Unit tests
run: npm test
status=0
node scripts/no-new-any.mjs --base "$base" --head "$HEAD_SHA" || status=1
node scripts/no-new-private-writes.mjs --base "$base" --head "$HEAD_SHA" || status=1
exit "$status"
- name: Build
run: npm run build
# Часть unit-контрактов проверяет размеры реально поставляемых графов в
# dist/ (#438, #593, #627). В issue-ветке закоммиченный dist законно
# отстаёт от исходников (#657), поэтому юниты обязаны читать свежую
# сборку — тот же порядок закреплён локальным gate:small.
- name: Unit tests
run: npm test
# Entry, manifest and every content-hashed chunk are one artifact (#337).
# #657: the committed bundle changes only in a release candidate, so the
# fresh build must equal it only there; elsewhere the committed copy
# legitimately lags and only the fresh dist is verified.
- name: Card bundle trees in sync
run: |
node scripts/bundle-tree.mjs dist custom_components/houseplan/frontend
node scripts/bundle-policy.mjs --verify HEAD
npm run bundle:budget
# #624: `tsc --noUnusedLocals` чист вне порта/харнесса, шесть чисел
# связности монолита не выше scripts/monolith-baseline.json. После
# сборки — одно из чисел считает байты dist/.
- name: Мёртвый код и связность монолита
run: npm run lint:unused
# Бандл собирается один раз и уезжает артефактом (#336). Прежде каждая из
# пяти браузерных job собирала его заново: `bundle:sync` — это `tsc
# --noEmit` плюс rollup, около минуты на job.
@@ -765,12 +924,48 @@ jobs:
retention-days: 1
if-no-files-found: error
# #657 (2б): бандл в дереве dev меняет только кандидат беты, поэтому стенд
# dev.houseplan.tech берёт его не из дерева, а из этого артефакта: после
# зелёного фронтенда на push в dev собранный `dist/` публикуется в служебную
# ветку `dev-build` одним коммитом без истории (scripts/dev-build.mjs), хост
# забирает его demo/stand/update-dev-bundle.sh. Опубликуется только голова:
# если dev ушёл вперёд, пушит следующий прогон. Не вход доказательства:
# стенд — не гейт, и сбой публикации не красит проверку кода.
dev_build:
name: "Бандл головы dev для стенда"
needs: frontend
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' && needs.frontend.result == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 10
continue-on-error: true
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
# download-artifact распаковывает поверх checkout и не удаляет старые
# content-hashed чанки закоммиченного release-бандла (#657). Свежий
# артефакт должен быть единственным содержимым dist, иначе bundle-tree
# справедливо видит оставшиеся файлы как orphan assets.
- name: Очистить закоммиченный dist перед артефактом
run: |
node -e "require('node:fs').rmSync('dist', { recursive: true, force: true })"
- name: Забрать собранный бандл
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: card-bundle
path: dist
- name: Опубликовать в dev-build
run: node scripts/dev-build.mjs --sha "$GITHUB_SHA" --dist dist
smoke:
name: Смоки в браузере (шард ${{ matrix.shard }} из 3)
# Gated on `frontend` so a typecheck failure does not burn browser minutes.
needs: [changes, frontend, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.smoke != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 20
# Смоки шардируются: последовательный прогон занимал ~7.5 минут и был
# критическим путём всего Validate. Три шарда режут его примерно вдвое;
@@ -803,6 +998,9 @@ jobs:
# Бандл приходит артефактом из job `frontend` (#336): та же сборка, но
# один раз на прогон. Свежесть проверяет сам продукт — `assertFreshDemoBundle`
# сверяет вшитый отпечаток с исходниками этого дерева.
- name: Очистить закоммиченный dist перед артефактом
run: |
node -e "require('node:fs').rmSync('dist', { recursive: true, force: true })"
- name: Забрать собранный бандл
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
@@ -874,7 +1072,8 @@ jobs:
name: "Смоки: все шарды зелёные"
needs: [changes, smoke, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.smoke != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Записать маркер успеха
run: |
@@ -898,7 +1097,8 @@ jobs:
# inexpensive and catches a different class of regressions than timings.
needs: [changes, frontend, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.golden != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
@@ -923,6 +1123,9 @@ jobs:
# Бандл приходит артефактом из job `frontend` (#336): та же сборка, но
# один раз на прогон. Свежесть проверяет сам продукт — `assertFreshDemoBundle`
# сверяет вшитый отпечаток с исходниками этого дерева.
- name: Очистить закоммиченный dist перед артефактом
run: |
node -e "require('node:fs').rmSync('dist', { recursive: true, force: true })"
- name: Забрать собранный бандл
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
@@ -997,7 +1200,7 @@ jobs:
# prereleases. The expensive same-runner comparison lives in performance.yml.
needs: [changes, frontend, reuse]
if: needs.changes.outputs.heavy == 'true' && needs.reuse.outputs.performance_smoke != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
# 15 минут не хватало, когда установка браузера шла через apt: замер
# начинался на исходе окна (#206). Запас на холодный кэш — при попадании
# job укладывается в те же минуты, что и раньше. Диффозависимые профили
@@ -1027,6 +1230,9 @@ jobs:
# Бандл приходит артефактом из job `frontend` (#336): та же сборка, но
# один раз на прогон. Свежесть проверяет сам продукт — `assertFreshDemoBundle`
# сверяет вшитый отпечаток с исходниками этого дерева.
- name: Очистить закоммиченный dist перед артефактом
run: |
node -e "require('node:fs').rmSync('dist', { recursive: true, force: true })"
- name: Забрать собранный бандл
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
@@ -1090,7 +1296,8 @@ jobs:
name: "Геометрия: TS/Python parity исполнена"
needs: [changes, reuse]
if: needs.changes.outputs.geometry_parity == 'true' && needs.reuse.outputs.geometry_parity != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
@@ -1124,7 +1331,8 @@ jobs:
name: "Бэкенд: pytest в Home Assistant"
needs: [changes, reuse]
if: needs.changes.outputs.backend == 'true' && needs.reuse.outputs.backend != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
# Browser fixtures are generated by their real ESM factories and then
@@ -1210,7 +1418,8 @@ jobs:
name: "Доказательство выполненных проверок"
if: always()
needs: [preflight, changes, reuse, hacs, hassfest, changed_mutants, frontend, smoke, smoke_done, golden, performance_smoke, geometry_parity, backend]
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
+147 -510
View File
@@ -4,78 +4,51 @@ House Plan is one HACS package with two parts plus a demo harness:
- **Lovelace card** (`src/`, TypeScript + Lit) — the primary product, bundled to
the entry, manifest and hashed chunks under `dist/`.
- **Storage integration** (`custom_components/houseplan/`, Python) — the Home Assistant backend.
- **Demo harness** (`demo/`) — a self-contained Playwright page (`demo/srv/demo.html`) that renders the card against a fake `hass`, used for screenshots and the `smoke_*.mjs` end-to-end suite.
- **Storage integration** (`custom_components/houseplan/`, Python) — the Home
Assistant backend.
- **Demo harness** (`demo/`) — a Playwright page (`demo/srv/demo.html`) that
renders the card against a fake `hass` for screenshots and the `smoke_*.mjs`
suite. The home is fully synthetic. Launcher `demo/serve.mjs`; golden scenes
`demo/golden/`, performance `demo/performance/`, guard suite `demo/guard/`,
live stand seed `demo/stand/` — each with its own README.
This file is the map and the few rules every session needs before its first
command. `PROCESS.md` is the only complete canon and wins any disagreement;
the sections below link to it instead of retelling it.
## Read this first
**`docs/SCOPE.md` before anything else.** It was fixed with the owner and states
its own authority: features are built, improved and accepted **only** if they
serve a job listed there. It carries the mission, the three personas, the core
user jobs and the out-of-scope list.
serve a job listed there. Its central consequence: **View mode is the product
for two of the three personas.** Editors are admin-only tools and must never
leak interactions into View. For work that changes visible behaviour, also read
`docs/USER-GUIDE.ru.md` — interface wording comes from there and is not invented.
Its central consequence: **View mode is the product for two of the three
personas.** Editors are admin-only tools and must never leak interactions into
View.
**Reading order by role** (#634). `node scripts/entry-cost.mjs` measures each
route and `test/entry-cost.test.mjs` keeps this list equal to its routes:
For work that changes visible behaviour, also read `docs/USER-GUIDE.ru.md` —
interface wording comes from there and is not invented, or the UI starts speaking
developer.
- author (analysis, spec, implementation, infrastructure): `docs/SCOPE.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` →
`AGENTS.md` → `PROCESS.md` → `docs/STATUS.md`.
Then `PROCESS.md` (the full process), `docs/STATUS.md` (where the release line
is), and for non-trivial changes `docs/ARCHITECTURE.md` plus the canonical
document of the subsystem you touch: `SUN.md`, `LIGHT.md`, `CANVAS.md`,
`WALL-THICKNESS.md`, `UX-MODES.md`, `CONFIG-COMPATIBILITY.md`,
`TOUCH-SUPPORT.md`.
The two digests quote and link `PROCESS.md` section by section; open the linked
section whenever a digest line governs your current step. For non-trivial
changes add `docs/ARCHITECTURE.md` plus the canonical document of the subsystem
you touch (one list, the same one the reviewer prompt in `_process.yml` reads):
`SUN.md`, `LIGHT.md`, `CANVAS.md`, `WALL-THICKNESS.md`, `UX-MODES.md`,
`CONFIG-COMPATIBILITY.md`, `TOUCH-SUPPORT.md`, `ISOMETRIC.md`, `VACUUM.md`,
`DECOR-EDITOR.md`, `DEVICE-PRESENTATION.md`, `FILTERING.md`, `STAIRS.md`,
`RADAR.md`, `PDF-EXPORT.md`, `STYLING-HOOKS.md`.
Standard commands live in `package.json` scripts, `CONTRIBUTING.md` and
`docs/DEVELOPMENT.md`.
## Canonical backlog and status
[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical
task records: problem, scope, acceptance criteria and discussion.
**Status lives in labels:** `S1-new`, `S2-analysis`, `S3-spec`, `S4-spec-review`,
`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`, plus `blocked` on top
of a status and `rejected` on a closed issue. A product issue in flight carries
exactly one `S*` label. An infrastructure-only issue is the deliberate exception:
it may carry no `S*` label while being implemented, enters the common flow at
`S7-code-review`, and from then on carries exactly one. Labels are the whole of
it: GitHub Projects is no longer used.
**The light track is the default, not a shortcut** (owner's decision 2026-08-27,
issue #338). `small` — the spec lives in the issue body and its review is a
comment. Analysis names the `small` criterion the task *fails* when it takes the
full track; "ordinary track" without a named criterion is not a justification.
The threshold itself did not move — only which side carries the proof. The full
track stays what it was for geometry, config migrations and public contracts,
where a criterion is broken plainly and saying which one is easy.
`trivial` — the short track: no spec stage at all, `S2-analysis` straight to
`S5-ready`, with the AC written into the issue body first. `trivial` requires a bug confined to one surface with no new UX
contract, no migration, no i18n, no perf or touch impact, at most three checkable
AC, **and expected behaviour already on record** — nothing left to decide. Code
review is never skipped on either track: it checks scope, risks and the evidence
from executed tests, but does not stand in for executing them.
`PROCESS.md` §5 and §5.1 hold the criteria.
An issue filed by an outsider is worked exactly like one of the owner's own, once
the owner has decided to take it. For product work the check sits **at the
entrance**, not on every step: the first status label admits it to the flow. For
infrastructure work an explicit assignment by the owner is the entrance, and the
issue may remain without an `S*` label until its first `S7-code-review`. In either
case, **who filed it stops mattering** once the owner has admitted it.
Applying that first label *is* the owner's explicit decision, and the platform
already guarantees it — only someone with write access can label. The earlier rule
made outside reports be refiled as the owner's own issues, which turned out to be
work for nothing: on #123 the spec was already written by the time the guard
refused.
Specs, audits and ADRs may live under `docs/`, but must link to their issue and
must not become a parallel task list. When repository documentation disagrees with
Issues, the issue wins.
Where the rest lives: commands — `package.json` scripts, `CONTRIBUTING.md`,
`docs/DEVELOPMENT.md` (toolchain, build, release — its Release section is the
only home of release mechanics); tests and gates — `docs/TESTING.md`.
## Rule #1
@@ -90,485 +63,149 @@ gh issue view <NN> --repo Matysh/houseplan-card --json number,state,labels
The label must be one of `S5-ready`, `S6-in-progress`, `S7-code-review`. Anything
else — refuse and say why. "Issue #83 is in `S2-analysis`, code is off limits.
Start with the spec?" is the correct answer, not a smaller patch.
Start with the spec?" is the correct answer, not a smaller patch. The one
exception is a local, unpushed draft on `track:ask` while the spec is in
`S4-spec-review`, each commit carrying the `Spec-Draft` trailer from the task
packet; it reaches the branch only after a green spec review (`PROCESS.md`
§11.8).
## Change classes
GitHub Issues are the canonical task records and the **labels** are the status
(`PROCESS.md` §9); when repository documentation disagrees with an issue, the
issue wins. Change classes (`PROCESS.md` §1): **A** product (`src/**`,
integration Python, manifests, i18n), **B** gates and tooling (tests, `demo/**`,
`scripts/**`, `.github/**`, build and package configuration), **C**
documentation, **D** generated (bundle, golden baselines) — D beats A where paths
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.
| Class | Paths | Issue required |
|---|---|---|
| **A — product** | `src/**`, `custom_components/houseplan/**/*.py`, `manifest.json`, `hacs.json`, i18n, `custom_components/**/translations/**` | yes |
| **B — gates and tooling** | `test/**`, `tests_backend/**`, `demo/**`, `scripts/**`, `.github/workflows/**`, `rollup.config.mjs`, `tsconfig*.json` | yes; may reuse the issue it covers |
| **C — documentation** | `docs/**`, `README*`, `CHANGELOG*`, `AGENTS.md` | not if it is part of its issue's DoD |
| **D — generated** | `dist/**`, `custom_components/houseplan/frontend/**`, `demo/golden/baselines/**` | never changes on its own. The stand copy `demo/srv/assets/**` is no longer committed (#255): build the complete tree with `npm run bundle:sync` |
**Tracks** (`PROCESS.md` §5, §5.1): the label `track:ship`, `track:show` or
`track:ask` sets the route, and the owner's label beats the criteria. `show` is the
default: up to three AC in the issue body, no spec review, `S2` → `S5`. `ship` is a
one-sentence change within fixed limits, `S1` → `S5`. `ask` is the full route with
a spec review. Any agent may raise a track with a reason; only the owner lowers it.
A label is a proposal until the owner confirms it with a comment line
`Трек: <ship|show|ask> — решение владельца`; an agent never writes that line.
Several track labels at once read as the strictest. A risky changed hunk
(geometry, touch, migration, devices, perf, a new UX key) raises an unconfirmed
`ship` to `show` at `S7`; on `show` it is a question to the reviewer (#707).
`small` and `trivial` read as `show`. An **infrastructure** task — not a single
class A file — skips analysis and spec and enters at `S7-code-review`
(`PROCESS.md` §1). Every change is code-reviewed; on `ship` the review moves to a
batch review (`ship-review.yml`, `PROCESS.md` §11.7): nightly
(`SHIP-REVIEW-<base>-dev-<sha12>.md`), and before the beta tag whatever the night
did not read (`SHIP-REVIEW-<tag>.md`). The review pipeline prices each round by
track (§10.4): no mutants run
during development on any track — the whole registry runs nightly (#709); a
rebase before review only on `ask` or when
the branch does not merge cleanly into `dev`. Review checks scope, risks and the
evidence from executed tests, but does not replace executing them.
The table above is a summary; `PROCESS.md` §1 is the authority and now covers the
configuration files this one omits — `package.json`, `package-lock.json`,
`pytest.ini`, `.gitignore`, `.gitattributes`, `.githooks/**` and the rest of
`.github/**` are class B. Where paths overlap, **D beats A**: the built bundle
lives inside `custom_components/houseplan/frontend/` and would otherwise read as
product source.
## Specs
## Commits
The spec lives in the **issue body**, under a `## ТЗ` heading (owner decision
2026-09-10, #517); `docs/specs/` is an archive of specs written before that date
and takes no new files. Required sections and the rule for questions are
`PROCESS.md` §7.1: only **product** ambiguity goes to the owner — what a person
sees or does, and how much user-visible change belongs in this issue — in one
batched comment with a proposed default for each question and `blocked` on top of
`S3-spec`. Everything a user cannot observe is yours to decide and record.
Hooks install themselves: `package.json` runs `"prepare": "node
scripts/install-hooks.mjs"`, so `npm ci` sets `core.hooksPath` in every fresh
clone. Verify with `git config core.hooksPath` — expect `.githooks`.
## Commits and branches
Every non-merge commit carries **terminal** trailers:
Hooks install themselves on `npm ci` (`prepare` → `scripts/install-hooks.mjs`);
`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
User-Visible: yes
```
One `Issue:` line per issue if a commit closes several. `User-Visible: no` for
tests, refactors, tooling and documentation that does not change the product.
`User-Visible: yes` requires edits to **both** changelogs — `docs/CHANGELOG.md`
and `docs/CHANGELOG.ru.md` — in the same commit.
One `Issue:` line per issue. `User-Visible: yes` requires edits to **both**
`docs/CHANGELOG.md` and `docs/CHANGELOG.ru.md` in the same commit. A commit
touching `demo/golden/baselines/**` also needs `Release:` plus exactly one of
`Baseline-Reviewed: <GitHub run URL>` or `Baseline-Reviewed-Local: sha256:<WSL
attestation>` (`PROCESS.md` §10.1). Never invent a review link and never rewrite
published history to satisfy trailers.
A commit touching `demo/golden/baselines/**` additionally requires:
Branch `issue/<NN>-slug`; direct commits to `dev`, no PR (owner's decision); a
violation is fixed with a follow-up commit, never a force-push.
```text
Release: v1.62.0-beta.9
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/<run-id>
```
Never invent a review link and never rewrite published history to satisfy
trailers. `.githooks/commit-msg` and the `provenance` CI job both run
`scripts/validate-commit-provenance.mjs`.
Branch: `issue/<NN>-slug`. Direct commits to `dev`, no PR — the owner's decision;
CI checks after the fact, and a violation is fixed with a follow-up commit, never
a force-push.
**Push after every task, not before a beta.** While work sits unpushed there is
nothing to review, and reviewing twenty tasks at once is not review. `dev` may hold
unreviewed code while a task is in flight; what matters is its state when the
reviewer says it is accepted.
**Standing permission: push `issue/<NN>-slug` without asking.** The reviewer runs
in CI and can only read what is on the remote — an unpushed spec or commit means
the review either stalls or judges the wrong tree. Pushing a task branch publishes
nothing to users and does not touch the integration branch, so it needs no command.
**Do not merge into `dev` by hand.** On a green code review the pipeline rebases
the task branch onto `dev`, pushes it, and only then sets `S8-merged` — the label
asserts the code is in `dev`, so the merge has to happen first or the label lies
in between.
If the rebase conflicts the pipeline says so in the issue and sends the task back
to `S6-in-progress`. The verdict still stands: nothing needs reviewing again, the
remaining work is the rebase. When the conflict is only in the committed bundle
(`dist/**`, `custom_components/houseplan/frontend/**` — the usual case when two
tasks built it in parallel), run `node scripts/rebase-on-dev.mjs` (#479): it takes
`dev`'s copy through the rebase, rebuilds with `npm run bundle:sync` and amends
the result into your last commit; a conflict anywhere else aborts and leaves the
tree as it was. Then push the branch and re-apply `S7-code-review`. When the
only difference from the reviewed material is the pipeline's own review-document
commit, the next run re-applies the green verdict without calling the model
(#499); any other change to the tree — a rebase included — gets a full review.
The pipeline is an idempotent controller (#499): only `S4-spec-review` and
`S7-code-review` start it, it reads the issue's *current* labels rather than the
event snapshot, and a label removed before the run starts is treated as a
withdrawn request. Push the material **before** applying the label — the reviewer
is pinned to the SHA the pipeline captured and must not fetch newer commits. The second review run is not a formality — after a rebase onto a
moved `dev` this is different code, and accepting it unchecked is how regressions
arrive. Cycles are counted per stage, so a code review spends its own budget.
Everything else still requires the owner's explicit command: pushing `main`,
creating tags, publishing betas and releases, closing issues.
- **Push after every task, not before a beta** — while work sits unpushed there
is nothing to review.
- **Standing permission: push `issue/<NN>-slug` without asking.** The reviewer
runs in CI and reads only the remote; a task branch publishes nothing to users.
Push the material **before** applying the review label.
- **Do not merge into `dev` by hand.** On a green review the pipeline rebases,
pushes and only then sets `S8-merged`. A conflicting rebase returns the task to
`S6-in-progress` with the verdict intact; `node scripts/rebase-on-dev.mjs`
resolves conflicts in generated files only (`PROCESS.md` §2.10), then push and
re-apply `S7-code-review`.
- Everything else needs the owner's explicit command: pushing `main`, tags,
publishing betas and releases, closing issues.
## Working trees (#115)
One checkout, one `HEAD`: two agents sharing a directory inherit each other's
branch, and twice in one hour a commit landed on someone else's task branch that
way. The layout is therefore fixed:
branch. `houseplan-card-src/houseplan-card` is the author's tree — task branches
live there, and unfamiliar local changes belong to the author or the owner,
never reset or clean them away. `houseplan-card-src/hp-dev` is the owner's
worktree, permanently on `dev`. The reviewer owns no tree: it runs in CI on a
fresh checkout. A worktree works only on the machine that created it — its
`.git` file records an absolute path in that machine's format.
- **`houseplan-card-src/houseplan-card`** — the author's tree. Task branches live
here; nobody else commits in it. Unfamiliar local changes belong to the author
or the owner — never reset or clean them away.
- **`houseplan-card-src/hp-dev`** — the owner's worktree, permanently on `dev`. For owner-side operations that must not disturb the
author's tree: pushing `dev`, restoring a hook's executable bit, emergencies.
- **The reviewer owns no author tree.** It runs in CI on a fresh checkout. The
agent implementing an infrastructure task is an ordinary task author and uses
the same author-tree rules as product work; task branches must not share a
mutable checkout concurrently.
## Handoff and the verdict
A worktree is only usable on the machine that created it: the `.git` file records
an absolute path in that machine's format. One created from a Linux sandbox is
dead on Windows and vice versa — create worktrees on the machine that will use
them, which for `hp-dev` means the owner's.
Start a task from its packet: `node scripts/task-packet.mjs --issue NN` (status;
track with its basis, cycle limit and rebase policy; what the status permits;
the branch against `dev` and the next step — no rebase a clean `show`/`ship`
merge does not need; risk by changed hunks and what it means on this track; the
required checks, each with its reason; changelog and visual evidence; the
previous verdict and the unwitnessed AC; it writes nothing). The local gate is
`npm run gate:small` (`docs/TESTING.md` › Локальный набор перед пушем); run the
smokes named in the AC before `S7-code-review`. **"Verified" without a named
command and its result is not evidence.** Comment formats — claim, handoff,
verdict — are `PROCESS.md` §7.2.
## Agent-neutral workflow
**One handoff, one push** (`PROCESS.md` §10.4). Run
`node scripts/process-gate.mjs --issues` before pushing; after
`S7-code-review` do not push to the branch until the verdict or the return
arrives — a push on top of a running review cancels it.
No task type is reserved for Codex, Claude or any other named model. **Any agent
may take any task**: analysis, spec, product implementation, infrastructure or a
release explicitly commanded by the owner. Roles describe the current artifact,
not the agent brand. The owner rules on product disputes, closes issues and
commands releases.
Author and reviewer are independent agents/sessions. They need not use different
model families, but the reviewer must start without implementation context and
must not be the author grading their own work. The reviewer does not edit the
material under review.
**Infrastructure-only work uses an accelerated entry into the common flow.** It
is implemented immediately by any agent, without analysis, spec, spec review or
the statuses `S1`…`S6`. Once the branch is ready and pushed, apply
`S7-code-review`. From there the ordinary controller applies: green review rebases
and merges the checked material into `dev` and then sets `S8-merged`; findings or
a failed merge return the issue to `S6-in-progress`, and after correction it is
submitted to `S7-code-review` again.
The test for "infrastructure only" is mechanical: **not a single class A file** —
nothing under `src/**`, no `custom_components/**/*.py`, no manifests, no i18n. A
task that touches class A even once is not infrastructure and takes the full flow;
there is no such thing as "mostly infrastructure". The strictness is deliberate:
a loose reading would turn this into the route by which product changes skip
review.
What stays mandatory either way: an issue exists, both trailers are on every
commit, proportionate local gates are green (normally `typecheck`, `test` and
`build`), and any non-obvious decision is written down in the code or the issue
rather than kept in someone's head. Infrastructure work skips specification, not
code review.
**Review starts by itself.** Applying `S4-spec-review` or `S7-code-review` fires the
pipeline. Deterministic gates, model review and integration have independent
55/45/55-minute budgets (#551); typical runs finish well before those ceilings.
**Having applied one of those labels, wait for the result instead of ending the
session.** Reporting "handed over for review" stops a conveyor that could have kept
moving on its own. An agent has no clock — it exists only during its own turn — so
waiting means polling: every 90 seconds, at most 110 times. A single long sleep hits
the command timeout. Do the polling with `node scripts/wait-verdict.mjs --issue NN
[--sha <tip>]` (#496): it watches the label, the pipeline's own comments (conflict,
cancelled merge, failed run) and optionally Validate on the SHA, prints only when
the state changes and exits 0 on a new label, 3 on an event that needs a hand,
4 on timeout — the same 90 s × 110 without a model turn per tick. It writes
nothing. Pipeline comments older than the latest application of `S4`/`S7` are
the baseline, not an outcome of the new round; an outcome from the current round
which already exists when the waiter starts is still delivered immediately (#546).
Watch the **label**, not the comment: the label is the state,
the comment only explains it. Do not wait at all while `blocked` is set — the task
is waiting on the owner, not on the reviewer. On exhausting the attempts, stop and
tell the owner: a failed run leaves the label where it was, forever.
What the new label means:
**Having applied `S4-spec-review` or `S7-code-review`, wait for the result
instead of ending the session.** The label starts the pipeline by itself. Poll
with `node scripts/wait-verdict.mjs --issue NN [--sha <tip>]` (#496): it watches
the label and the pipeline's comments every 90 s, at most 110 times, prints only
on a change and exits 0 on a new label, 3 on an event that needs a hand, 4 on
timeout. Watch the **label**, not the comment. Do not wait while `blocked` is
set.
| Now reads | What happened | What you do |
|---|---|---|
| `S5-ready` | the spec is accepted | write the code |
| `S5-ready` | the spec is accepted | write the code (a draft from `S4`: rebase onto `dev`, check, push — §11.8) |
| `S3-spec` | the spec came back | read the verdict, revise, re-apply `S4-spec-review` |
| `S6-in-progress` | the code came back | revise, re-apply `S7-code-review` — **or**, if the verdict was green and only the merge conflicted, just rebase and re-apply. The comment says which |
| `S8-merged` | accepted and already in `dev` | nothing |
| `review-4` | the cycle limit is spent | stop, the owner decides |
**After a review run the label always changes.** If it did not, the run itself
failed rather than the work — say so to the owner instead of polling on.
failed rather than the work — say so to the owner instead of polling on. The
bounded queue reconciler (#555) re-wakes a review whose event was lost; it is a
safety net, not permission to stop waiting for the review you started.
The repository also has a bounded queue reconciler (#555). It takes one S4/S7
snapshot every thirty minutes and exits. It may re-apply the same review label
only when the matching event was lost or its run ended with a transient
cancellation/timeout before a sealed result existed. It never applies verdicts or
merges. Running work, `blocked`, `review-4`, a foreign material/stage/attempt, a
guard failure, or an unintegrated sealed model result is left untouched and gets
at most one machine-keyed diagnostic. This is a safety net, not permission for an
agent to stop waiting for the result of the review it started.
**A failed pre-release gate does not send the issue back to review.** The
implementation loop runs only typecheck, unit and build; golden, browser smokes,
performance and the full HA harness run before a beta, which is after the code
review has passed and the issue sits in `S8-merged`. Some defects cannot surface
any earlier.
Fix it, re-run what failed, and a green run is enough for the release to continue.
The issue stays in `S8-merged`. Record the **exact command and its result** in the
issue — "verified" without a command proves nothing. Trailers as usual, and
`User-Visible: yes` still means both changelogs in the same commit.
The exception covers repairing the defect the gate named, not carrying on
development under the name of a repair. It goes through the normal flow — a new
issue, or back to `S6-in-progress` — if the fix changes a behaviour contract, gives
the user something new, reaches a subsystem the task never touched, or is
comparable in size to the task itself. And editing the gate so it stops failing is
concealment, not repair; the exception is a defect proven to be **in the fixture**,
as on #89, where the sun sat at azimuth 180° and the only window faced north, so no
ray was ever built.
Baselines are still accepted only via `npm run golden:accept -- --reviewed` on a
complete Linux CI artefact. "So the gate goes green" is not a reason.
The exchange happens in **issue comments** — there is no local message bus. Verdict
format:
```text
Verdict: green/yellow/red · cycle r<N>/4 · High: N · Medium: N → in-task | #… · Document: …
```
High blocks. A Medium finding INSIDE the task's scope is fixed within the task:
with no High findings the verdict is yellow, the author fixes it and the fix
passes another review cycle — no separate issue (owner's decision 2026-08-19,
#202: filing and servicing an issue costs far more than fixing in place). Only
a Medium finding OUTSIDE the scope becomes its own issue — foreign scope is
never patched from this branch. Low is fixed or waived with a note
in the review document. A yellow verdict is legitimate even when every acceptance
criterion passes, if the change does not solve the stated scenario or degrades a
neighbouring one.
**Four review cycles** (two on the light track). The counter lives in the document
name, `-r1`…`-r4`; the fourth adds the `review-4` label. There is no fifth attempt:
the owner splits the task, rejects it, or arbitrates.
On the light track (`small`: complexity ≤3, one surface, no config migration, no
new UX contract, no perf or touch impact — all at once) the spec lives in the issue
body and the spec review is a comment. Code review is never skipped. This track is
the default: taking the full one means naming the criterion above that the task
does not meet.
## Specs
The spec lives in the **issue body**, under a `## ТЗ` heading (owner decision
2026-09-10, #517); `docs/specs/` is an archive of specs written before that date
and takes no new files. Required sections are in `PROCESS.md` §7.1, plus two
product ones: which persona meets this, on which surface, at what moment; and what
the person sees before and after, in one sentence without implementation terms.
Proof that a verdict was passed on a given text is the pipeline's job: it writes
the `sha256` of the normalised body into the review document's anchor block, and
an edit made after a green spec review reaches the code reviewer as a finding.
**Ambiguity is asked, not guessed — but only product ambiguity.** A guess written as
fact is the worst kind of defect: it passes review because it looks like a decision.
The owner answers exactly two kinds of question: **what a person sees or does**, and
**how much user-visible change belongs in this issue**. Behaviour in a boundary case,
which persona wins when two conflict, what counts as acceptable degradation, whether
a neighbouring behaviour is in scope here or becomes its own issue.
Everything a user cannot observe is yours to settle: where state is stored, which
module carries the guard, naming, file layout, test strategy, migration mechanics,
development policy. Decide it, record it in an explicit "assumed, change freely"
block, and let the reviewer challenge it. A technical disagreement between author and
reviewer is settled by the verdict, not by the owner; it reaches him only when the
cycle limit is exhausted.
Split a mixed question instead of escalating all of it. "Where does this state live"
is technical. "Does it survive a page reload and follow the plan across screens" is
product. Ask the second, decide the first.
Ask in one batched issue comment, each question carrying a proposed default, and put
`blocked` on top of `S3-spec` while waiting. A question with a default costs the
owner seconds; one without costs him minutes.
## Gates
```
npm run typecheck
npm test
npm run build
npm run inventory # the only correct way to get test counts
```
Never copy test counts into documents by hand; they go stale in days.
After building, keep the complete manifest-driven bundle trees in sync — CI
verifies every listed file byte-for-byte:
```
npm run bundle:sync # dist → custom_components + demo/srv/assets (#255)
npm run bundle:budget # initial View graph <= 256000 B gzip (#337)
```
`npm run gate:small` runs the mandatory part of PROCESS §8 in one go (#479,
#576): build with typecheck, `no-new-any` and `smoke-select` start in parallel;
unit tests follow the completed build because their bundle-contract witnesses
read the freshly produced `dist`, then the bundle-tree comparison and the
bundle budget run. It prints the smokes the
diff selects but does not run them by default — `npm run gate:small -- --smokes`
(#496) adds the browser phase after the artefact preparation: `bundle-sync`, then
the directly matched and registered smokes two at a time; "broad" matches stay
the reviewer's call. `golden`, `pytest` and `check-docs --screenshots=strict`
remain the author's call by diff and AC.
**Start a task from its packet** (#496): `node scripts/task-packet.mjs --issue NN`
prints one derived view — status and track, what the status permits, the owner's
recent decisions, the branch against `dev` and Validate on its tip, the previous
verdict with its recorded tree, AC → evidence from the last review document and
what is still unwitnessed. It reads GitHub and git and writes nothing; the labels
remain the only source of status.
**Heavy CI gates run on the beta candidate, nightly and on demand — not on every
push (#479).** `smoke`, `golden` and `performance_smoke` in Validate are gated
on the `heavy` output: true for a head commit carrying a `Release:` trailer, for
`workflow_dispatch full=true` (which `nightly.yml` issues on `dev` every night)
and for pull requests. A plain push to `dev` runs preflight, frontend (types,
units, build, bundle sync, no-new-any), the narrow TS/Python geometry parity
guard when its inputs changed, backend, hacs and hassfest. Screenshot
freshness in `check-docs` is likewise a warning on a plain push and an error on
the candidate; `publish-prerelease.yml` and `release.yml` refuse a candidate
without the `Release:` trailer, so a green Validate without the heavy jobs can
never pass for a release.
During the implementation cycle the fast gates always run. Since 2026-08-14 the
owner's machine also carries Playwright with Chromium (Windows) and a full WSL
environment, which changes one thing (#151): **before moving an issue to
`S7-code-review`, run the smokes named in its AC locally** — `node
demo/smoke_<name>.mjs`. A red smoke that reaches the review costs a cycle; run
locally it costs a minute. Precedent: on #89 a fixture error lived through a
whole review round that a local run would have caught immediately.
**One handoff, one push (#510).** Run `node scripts/process-gate.mjs --issues`
locally with `gh` available before pushing (without `gh` the hook cannot check the
issue status and stays silent). After `S7-code-review` do not push to the branch
until the verdict or the return arrives: a push on top of a running review cancels
it (10–20 runner minutes) and, after the material is fixed, also the merge (#312).
Set `S7` once per round, not after every CI fix: the pipeline now runs Validate
with the diff mutants on the material itself and returns a red one to `S6` without
spending a review cycle. Mutants by diff run only where they are explicitly
requested — the review candidate, the merge candidate (both dispatch Validate
with `mutants=true`) and PRs (#510, #601). Ordinary pushes, the beta candidate
(`Release:` trailer) and `full=true` do not request them: a routine push costs
~3 minutes (08–09.09 mutants cost 48 of 56 Validate job-hours and were mostly
cancelled by the next push), and by the beta every issue has already been
mutated twice — on review and on the rebased merge candidate; the night runs
the full registry (`mutation-gate.yml`), not a diff subset.
The full smoke set, `golden` and `performance_smoke` still belong to the
pre-beta run — which is then mandatory and complete. WSL runs of the full HA
harness (`~/houseplan-card`, venv) are advisory; **the canon does not move**:
the beta gate is CI at the exact SHA.
**Verifying and capturing are different things (#455).** `golden:verify` is
advisory and legal anywhere, Windows included: it reports differences and
accepts nothing. **Capturing** frames is refused outside Linux
before the browser even starts — `golden:capture` through
`demo/golden/policy.mjs`, documentation screenshots through
`npm run docs:capture` (use that script, not a bare `node
demo/docs/capture.mjs`: the gate sits one step earlier because editing the
capture script invalidates the committed screenshot index). The refusal prints
the WSL command. The reason is not policy but physics: Windows
rasterizes text through DirectWrite with different subpixel and DPI behaviour,
so no frame ever matches an accepted baseline byte for byte, no environment
witness can exist, and acceptance would refuse anyway (#401 accepts any
environment that proves itself with byte-identical undeclared frames — Linux is
simply the only one we have). The deliberate override is
`HP_ALLOW_FOREIGN_CAPTURE="reason"`; the reason travels into the output and the
manifest. Baselines are still accepted only via
`npm run golden:accept -- --reviewed` on a complete artefact, and the accepted
index records the platform next to the Chromium build. The one local
shortcut is `npm run docs:accept -- --identical` (#512): it re-captures on this
machine, compares decoded pixels with the committed frames and, only when every
frame is identical, refreshes the manifest fingerprints — frames that differ go
through the artefact as before. The displayed card version reaches the DOM
through `displayVersion()` (`src/card-version.ts`); the global
`__HP_VERSION_OVERRIDE__` behind it is for harnesses only and the product never
sets it.
**Backend.** A full Home Assistant harness cannot run on native Windows at all:
Home Assistant imports the Unix-only `fcntl` module. Its canon is Linux CI or WSL.
Locally only the pure subset runs; `python -m pytest tests_backend/ -q` without
Home Assistant **silently skips** `test_ha_*.py` (`conftest.py` ignores them when
`homeassistant` is not importable), so a green result proves nothing. Say so in the
report instead of claiming the backend was verified. Cloud agents have the harness
at `.venv-backend/bin/python`.
**Running the app / smoke suite**: build a fresh bundle and copy it into the demo
assets first, then run `node demo/smoke_*.mjs`. No real Home Assistant server is
required: `demo/srv/demo.html` stubs `hass`, registries and `callService`.
**Golden images**: `npm run golden:capture` and `npm run golden:verify` refuse a
stale demo bundle. Build and copy first, then review `artifacts/golden/actual/` and
`diff/`. Update baselines only with `npm run golden:accept -- --reviewed`, using the
complete Linux CI artifact; never accept a partial scenario or images merely to make
CI green. See `demo/golden/README.md`.
**Freshness contract**: the embedded fingerprint covers `src/` plus Rollup,
TypeScript and package-lock build inputs. Every browser check must verify it
before trusting a result — benchmarks, golden runs and documentation captures
call `assertFreshDemoBundle` themselves, and smokes get it from `launch()` in
`demo/serve.mjs` (#236). A missing or mismatched fingerprint is a hard failure,
not a warning; `HP_ALLOW_STALE_BUNDLE=1` skips the check for debugging and says
so out loud. A smoke against a stale bundle does not fail cleanly: part of its
assertions go red and part stay green, which reads as a logic defect.
**CI is pinned to an exact SHA.** The release gate accepts only a `completed
success` run for the candidate's SHA, not "the last green one"; a new push cancels
an unfinished Validate for the same branch. Gate jobs, matching the actual
`validate.yml` (#191): `docs`, `provenance`, `process-gate`, `hacs`, `hassfest`,
`frontend`, `smoke`, `golden`, `performance_smoke`, `geometry_parity`, `backend`.
The `changes` job
is a service path-filter, not a gate. `docs` is a real blocker: it checks the
screenshots `sourceFingerprint` against current `src/**`, which is exactly what
went red after the #113 merge. A stable release additionally waits for Full
Performance and for a green E2E run on a real Home Assistant (`houseplan-e2e`,
dispatched on the candidate SHA by `release.yml`, #514/#540); betas and the
development cycle never run E2E. Stable installable assets (`houseplan.zip`,
`houseplan-card.js` and their `SHA256SUMS`) reach the public stable release only
from `release.yml` after those gates; a release published by hand is turned back
into a draft first (#540). The prerelease publisher additionally ships a
candidate-bound `RELEASE-MEMBERSHIP.json` covered by the same passport (#547).
**"Verified" without a named command and its result is not evidence.**
A failed pre-release gate (golden, full smokes, performance, HA harness) does not
send the issue back to review: fix, re-run what failed, record the exact command
and result in the issue (`PROCESS.md` §11.4 — and its limits).
## Environments
**Local Windows checkout** is the day-to-day environment: repository-pinned Node
and Python as in CI (`npm run toolchain:check` compares the machine with the pins
CI actually uses — `.nvmrc` and `.python-version` are derived from the same
sources, #496),
`gh` authenticated. On the owner's machine do not trust the ambient PATH:
`.\scripts\windows-toolchain.ps1 setup|check` owns a verified portable Node and
dedicated `.venv-ci`, and its `npm`/`node`/`python`/`playwright` actions are the
explicit pinned entrypoints (#557). It changes no persistent PATH and never
deletes a mismatched venv. In WSL, work from an ext4 clone and use
`bash scripts/wsl-setup.sh --verify` for the real HA subset plus one Linux visual
capture; exact-SHA Linux CI remains authoritative. `.venv-backend` does **not**
exist there — it is provisioned only by cloud agent startup scripts, which also
run `npm ci` and install Playwright Chromium.
Known environment-sensitive smoke: `demo/smoke_opening_measure.mjs` fails two
sub-checks (`place_dialog_x_magnetised`, `place_committed_x_center`) under the pinned
Chromium — a `1e-6`-tolerance magnet-snap on the opening-*placement* path. It
reproduces against the pristine committed bundle, so treat it as
pre-existing/pixel-precision, not a regression you introduced.
## Alpha experiments
`src/labs.ts` is the single registry and resolver for hidden presentation
experiments. One browser-local switch controls the complete set known to the
installed build: `hp_alpha=1` in query or the shared hash grammar enables and
persists it, while `hp_alpha=0` disables and persists it. Do not add per-feature
URL/storage keys or a YAML/config switch. The legacy `hp-labs` grammar and
`houseplan_card_labs_v1` storage are ignored and never migrated.
A new registry entry needs a unique lowercase id, issue, summary and
unit/browser coverage; it does not get `since` or `expires`. Invalid or duplicate
entries fail closed. Alpha capabilities may alter presentation only and must not
gate data, migrations, stores, HA actions or network calls. Current renderer
details are in `docs/ISOMETRIC.md`.
Demo harness render quirk: the fake `hass` in `demo.html` is set once, so opening the
page directly in a browser renders the floor plan but **device icons only appear
after a re-render** (an F5 refresh, or nudging `card.hass = {...card.hass}`). The
smoke launcher `demo/serve.mjs` already does this nudge; a plain browser session does
not. This is a harness limitation, not a card bug.
## Promotion rule
Every new feature or material behaviour change must be published as a beta/RC
before it can enter a stable release, even when its local audit is clean. The
stable release commit is promotion-only: version fields, generated bundle
snapshots and changelog/release metadata. Do not add feature source code in
that commit. An explicit owner-requested emergency hotfix is the only exception
and must be called out in the release handoff.
A `Release vX.Y.Z-beta.N candidate` commit is **not** promotion-only: it carries
the work itself and follows the ordinary rules, trailers included.
Issues are closed in a batch when a beta ships, not when implementation ends: that
way a bug found in the beta returns to the same task, and the beta announcement can
list what went in. The batch is immutable candidate membership proven by Git
trailers, never the mutable S8 queue at close time; authorship does not change
membership. Status labels are stripped as the issues close, and retries resume the
same manifest without duplicating the release comment (#547).
The owner's Windows machine: `.\scripts\windows-toolchain.ps1 setup|check` owns
the pinned Node and Python; WSL works from an ext4 clone with
`bash scripts/wsl-setup.sh --verify` (`docs/DEVELOPMENT.md` › Local Windows
workstation). `npm run toolchain:check` compares any machine with the pins CI
uses. The full Home Assistant harness cannot run on native Windows; without an
importable `homeassistant` pytest does not collect `test_ha_*.py` at all, so a
green pure run proves nothing about the harness (`docs/TESTING.md`). Cloud
agents have the harness at `.venv-backend/bin/python`.
+51 -22
View File
@@ -38,6 +38,17 @@ an eager import. Extend the runtime, manifest, file/key/placeholder parity and
regional-locale tests together; never bypass the registry by importing a locale
directly in a component.
Administrator-only copy lives in three lazy namespace dictionaries —
`src/i18n/settings/`, `src/i18n/support/` and `src/i18n/topology/` (#627). Their
English file is static (the synchronous fallback); every other language is one
lazy chunk per namespace × language: a two-line loader module
`src/i18n/<namespace>/<namespace>-<code>.ts`, one `import()` with a
content-hashed retry token in `src/i18n/<namespace>.ts`, and one entry in
`NAMESPACE_LOCALE_CHUNKS` in `scripts/bundle-manifest.mjs`. The lazy surfaces
(onboarding, editor runtime, Zigbee overlay) wait for their dictionaries before
painting; never import a non-English namespace JSON statically — the budget
gate and `test/i18n-lazy-namespaces.test.mjs` refuse it.
The current `subst()` helper does not implement plural rules. Phrase strings so
their grammar does not depend on the numeric value (for example, use a neutral
label followed by `{n}` rather than an English singular/plural pair).
@@ -47,6 +58,22 @@ set; maintain the existing English and Russian documentation according to the
project's normal rules. Right-to-left layout is a separate product project,
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 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:`
trailer). The full rule is in `PROCESS.md` §8.
## Where to ask
Not sure whether something is a bug, or just want to discuss an idea before
@@ -67,44 +94,47 @@ 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)
A full clone is **215 MB of `.git`**; a blobless one is **26 MB** — measured, not
estimated. Both carry all 1611 commits and all 182 tags, so ranges, `merge-base`
and `git diff` across history work identically; `git diff origin/dev~3..origin/dev`
in a blobless clone takes about a second and grows `.git` by one megabyte.
The difference is that historical *file contents* are fetched only if something
actually asks for them. That matters here because 32% of the pack is documentation
screenshots — ten PNGs re-captured 196 times — and another sizeable share is the
committed bundle, one 1.16 MB file per product change. Almost nobody ever reads an
old revision of either.
A blobless clone keeps every commit and tag, so ranges, `merge-base` and
`git diff` across history work exactly as in a full clone; only historical *file
contents* are fetched on demand. Most of the pack is exactly such content that
almost nobody reads again — documentation screenshots re-captured with the UI and
the committed bundle rewritten by every release candidate — so a blobless clone
is several times smaller. To see the numbers for your own clone, compare
`git count-objects -vH` in a blobless and in a full one.
Drop the flag if you work offline with history, or need `git log -p` over the whole
tree repeatedly. Do **not** replace it with `--depth=1`: a shallow clone is about
the same size but has no `merge-base`, so the process gate, `smoke-select` and
every `origin/dev..HEAD` range stop working.
The HA-harness backend tests (`tests_backend/test_ha_*.py`) need Python ≥3.13 and
`pytest-homeassistant-custom-component home-assistant-frontend`; CI runs them on
every push — locally they are skipped when `homeassistant` is not importable.
The HA-harness backend tests (`tests_backend/test_ha_*.py`) need the
repository-pinned Python (`.python-version`) and
`pytest-homeassistant-custom-component home-assistant-frontend`; their canon is
Linux CI or WSL (`bash scripts/wsl-setup.sh --verify`). Without an importable
`homeassistant` they are not collected at all — not skipped — and pytest prints
`HA harness NOT collected: …` (#630).
## 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 built card must be committed in sync: `cp dist/houseplan-card.js
custom_components/houseplan/frontend/` (CI compares them byte-for-byte).
- The committed bundle changes only in a release candidate: `npm run
bundle:release` in a commit with a `Release:` trailer (#657). An ordinary task
restores it with `npm run bundle:clean` before committing — the `commit-msg`
hook refuses a bundle change otherwise.
- Tap actions have a security model (locks/alarms never toggle from the plan) —
see `resolveToggleIntent` in `src/device-toggle.ts`; don't weaken it.
- Every commit follows the issue and trailer contract in `PROCESS.md`.
@@ -114,6 +144,5 @@ every push — locally they are skipped when `homeassistant` is not importable.
## Architecture
Start with `docs/ARCHITECTURE.md` (data model, WS API, coordinate system) and
`docs/STATUS.md` (current state). Release: bump the version in `package.json`,
`manifest.json`, `const.py`, `CARD_VERSION`, tag `vX.Y.Z`, publish a GitHub release —
the workflow attaches the card bundle.
`docs/STATUS.md` (current state). Release mechanics live in one place:
`docs/DEVELOPMENT.md` › Release.
+922 -265
View File
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -42,6 +42,9 @@ sync across screens.
show temperature, humidity, light state and average LQI.
- **Light and environment.** Room fills, lamp Glow, wall shadows, a day-cycle
backdrop and sunlight through windows.
- **Flat or 2.5D.** One switch in General settings › Display shows the plan
with depth everywhere: raised device tiles with soft floor shadows and a soft
wash of sunlight; decor and wall colours stay as you set them.
- **Doors, windows, gates and vacuums.** Openings follow real contacts and locks;
a robot can show its position, dock and travelled path.
- **Several floors and screens.** Space tabs, swipe navigation, local viewport,
@@ -58,8 +61,8 @@ sync across screens.
1. Install the integration and open **House Plan** in the Home Assistant sidebar.
2. Create the first **space**: upload SVG/PNG/JPG/WebP, reuse an uploaded image,
or choose no image and draw the plan by hand.
3. In Plan, select **Room outline**, place vertices, and click the first point to
close the outline.
3. In Plan, select **Walls** and draw one continuous chain around the room: when
it closes an area, the room dialog opens.
4. Name the room and bind it to a Home Assistant area. Use “No area” for a room
that has no devices.
5. Open Device: devices from the bound area are already placed; drag their
@@ -183,8 +186,11 @@ concurrent clients, but avoid editing the same object in two browsers at once.
- [Full user guide](docs/USER-GUIDE.md)
- [Mouse/touch/keyboard matrix](docs/USER-GUIDE.md#6-navigation-zoom-and-input)
- [Plan tools](docs/USER-GUIDE.md#plan-tools-at-a-glance)
- [Stairs and floor links](docs/STAIRS.md)
- [Background editor](docs/DECOR-EDITOR.md)
- [Robot vacuums](docs/VACUUM.md)
- [Presence radars](docs/RADAR.md)
- [PDF export](docs/PDF-EXPORT.md)
- [Touch support](docs/TOUCH-SUPPORT.md)
<!-- docs-section: support -->
@@ -197,9 +203,4 @@ concurrent clients, but avoid editing the same object in two browsers at once.
Include the version, browser, logs and reproduction steps; private entity IDs
may be replaced with fictional ones.
Documentation screenshots are produced by the reproducible
`npm run build && node demo/docs/capture.mjs` command using synthetic data only. Scenario version,
source fingerprint and every image hash are recorded in the
[screenshot index](docs/images/screenshots.json).
License: [MIT](LICENSE).
+11 -9
View File
@@ -43,6 +43,9 @@ Assistant — и устройства появятся на плане авто
а карточки комнат показывают температуру, влажность, свет и средний LQI.
- **Свет и окружение.** Заливки комнат, Glow от ламп, тени от стен, дневной фон и
солнечные лучи из окон.
- **Плоский план или 2.5D.** Один переключатель в «Общие настройки ›
Отображение › Объёмный вид плана (2.5D)» показывает план с глубиной везде: приподнятые плитки устройств с мягкими
тенями и мягкая засветка солнцем; декор и цвета стен остаются вашими.
- **Двери, окна, ворота и пылесосы.** Проёмы отражают реальные датчики и замки;
робот показывает позицию, базу и пройденный путь.
- **Несколько этажей и экранов.** Вкладки пространств, жесты переключения,
@@ -57,10 +60,11 @@ Assistant — и устройства появятся на плане авто
## Первая рабочая комната
1. Установите интеграцию и откройте **House Plan** в боковом меню Home Assistant.
2. Создайте первое **пространство**: загрузите SVG/PNG/JPG/WebP либо выберите
вариант без изображения, чтобы нарисовать план вручную.
3. В редакторе «План» выберите **Контур комнаты**, поставьте вершины и замкните
контур нажатием на первую точку.
2. Создайте первое **пространство**: загрузите SVG/PNG/JPG/WebP, возьмите уже
загруженное изображение либо выберите вариант без изображения, чтобы
нарисовать план вручную.
3. В редакторе «План» выберите **Стены** и нарисуйте непрерывную цепочку по
периметру комнаты: когда она замкнёт область, откроется диалог комнаты.
4. Назовите комнату и свяжите её с зоной Home Assistant. Для помещения без
устройств выберите «Без зоны».
5. Откройте «Устройства»: устройства связанной зоны уже размещены автоматически;
@@ -188,8 +192,11 @@ default_floor: ground
- [Полное руководство пользователя](docs/USER-GUIDE.ru.md)
- [Матрица mouse/touch/keyboard](docs/USER-GUIDE.ru.md#6-навигация-масштаб-и-жесты)
- [Инструменты плана](docs/USER-GUIDE.ru.md#инструменты-плана-в-короткой-таблице)
- [Лестницы и переходы между этажами](docs/STAIRS.md)
- [Редактор подложки](docs/DECOR-EDITOR.md)
- [Роботы-пылесосы](docs/VACUUM.md)
- [Радары присутствия](docs/RADAR.md)
- [Экспорт в PDF](docs/PDF-EXPORT.md)
- [Поддержка touch](docs/TOUCH-SUPPORT.md)
<!-- docs-section: support -->
@@ -202,9 +209,4 @@ default_floor: ground
обновление страницы (`Ctrl+F5`). Приложите версию, браузер, логи и шаги
воспроизведения; приватные entity ID можно заменить вымышленными.
Скриншоты в документации получены воспроизводимой командой
`npm run build && node demo/docs/capture.mjs` только на синтетических данных. Версия сценариев,
fingerprint исходников и хеш каждого изображения находятся в
[индексе снимков](docs/images/screenshots.json).
Лицензия: [MIT](LICENSE).
+13 -6
View File
@@ -2,8 +2,8 @@
Corrected derivative of the 0.4.0 built-in furniture artwork (#593, #606).
- `svg/menu/`: the same 33 front-view category illustrations as 0.4.0, used
only by the lazy editor bundle.
- `svg/menu/`: 33 front-view category illustrations, used only by the lazy
editor bundle.
- `svg/plan/`: 60 top-view drawings used on the plan.
- `pack.json`: stable public ids, category links, default dimensions and names.
The filename avoids the `*manifest.json` suffix reserved by HACS.
@@ -16,11 +16,18 @@ Corrections to 0.4.0:
as a read-compatible alias by the application, not duplicated in this pack.
2. The *contents* of `plan/bookshelf.svg` and `plan/shelf_floor.svg` were swapped
so each filename and unchanged public id shows the named furniture item.
3. All 33 menu categories now have a plan variant. No menu artwork was changed.
3. All 33 menu categories now have a plan variant.
4. `menu/dishwasher.svg` uses the replacement front-view illustration supplied
by JustBusiness in [issue #640](https://github.com/Matysh/houseplan-card/issues/640).
Its reviewed source archive is `dishwasher.zip`, SHA-256
`1A9B1BDAF490B35812F51E4DD3CB1C425224EC768219C31B979DD6A84E890203`;
the path geometry is unchanged apart from safe repository normalization.
JustBusiness supplied this asset for House Plan to use, modify and distribute
under the repository MIT License without separate UI attribution.
The 93 SVG drawings originated with Sergey Matyunin (`Matysh`). He granted
House Plan permission to use, modify and distribute them under the repository
MIT License without separate UI attribution in
The original 0.4.0 set of 93 SVG drawings originated with Sergey Matyunin
(`Matysh`). He granted House Plan permission to use, modify and distribute them
under the repository MIT License without separate UI attribution in
[issue #593](https://github.com/Matysh/houseplan-card/issues/593#issuecomment-5739841899).
The reviewed source archive is `houseplan-furniture-0.4.0.zip`, attached to
[issue #593](https://github.com/Matysh/houseplan-card/issues/593), SHA-256
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110 110">
<path d="M81 82.5 L81 27.5 M81 82.5 C81 83.8807 79.8807 85 78.5 85 L31.5 85 C30.1193 85 29 83.8807 29 82.5 L29 55.5 M81 82.5 L81 40 M81 27.5 C81 26.1193 79.8807 25 78.5 25 L31.5 25 C30.1193 25 29 26.1193 29 27.5 M81 27.5 L81 40 M29 27.5 L29 55.5 M29 27.5 L29 40 M81 40 C81 38.6193 79.8807 37.5 78.5 37.5 L31.5 37.5 C30.1193 37.5 29 38.6193 29 40 M29 40 L29 55.5 M35.75 31 L45.25 31 M55.9943 55 C55.9943 55 54.6654 56.8105 54.5153 58.1429 C54.3169 59.9042 56.1139 60.6968 55.9943 62.4643 C55.8942 63.9429 54.5153 66 54.5153 66 M60.4943 55 C60.4943 55 59.1654 56.8105 59.0153 58.1429 C58.8169 59.9042 60.6139 60.6968 60.4943 62.4643 C60.3942 63.9429 59.0153 66 59.0153 66 M50.9943 55 C50.9943 55 49.6654 56.8105 49.5153 58.1429 C49.3169 59.9042 51.1139 60.6968 50.9943 62.4643 C50.8942 63.9429 49.5153 66 49.5153 66 M72 60.5 C72 69.8888 64.3888 77.5 55 77.5 C45.6112 77.5 38 69.8888 38 60.5 C38 51.1112 45.6112 43.5 55 43.5 C64.3888 43.5 72 51.1112 72 60.5 Z M66.5 60.5 C66.5 66.8513 61.3513 72 55 72 C48.6487 72 43.5 66.8513 43.5 60.5 C43.5 54.1487 48.6487 49 55 49 C61.3513 49 66.5 54.1487 66.5 60.5 Z M69.5 31 C69.5 32.1046 68.6046 33 67.5 33 C66.3954 33 65.5 32.1046 65.5 31 C65.5 29.8954 66.3954 29 67.5 29 C68.6046 29 69.5 29.8954 69.5 31 Z M77 31 C77 32.1046 76.1046 33 75 33 C73.8954 33 73 32.1046 73 31 C73 29.8954 73.8954 29 75 29 C76.1046 29 77 29.8954 77 31 Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29 76C29 77.3807 30.1193 78.5 31.5 78.5H78.5C79.8807 78.5 81 77.3807 81 76M29 76V82.5C29 83.8807 30.1193 85 31.5 85H78.5C79.8807 85 81 83.8807 81 82.5V76M29 76V40M81 76V40M81 40C81 38.6193 79.8807 37.5 78.5 37.5H31.5C30.1193 37.5 29 38.6193 29 40M81 40V27.5C81 26.1193 79.8807 25 78.5 25H31.5C30.1193 25 29 26.1193 29 27.5V40M43 42H67M35.75 31H45.25M69.5 31C69.5 32.1046 68.6046 33 67.5 33C66.3954 33 65.5 32.1046 65.5 31C65.5 29.8954 66.3954 29 67.5 29C68.6046 29 69.5 29.8954 69.5 31ZM77 31C77 32.1046 76.1046 33 75 33C73.8954 33 73 32.1046 73 31C73 29.8954 73.8954 29 75 29C76.1046 29 77 29.8954 77 31Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 791 B

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 JB (justbusiness)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+47
View File
@@ -0,0 +1,47 @@
# House Plan moon pack 1.0.0
Designer asset for [issue #661](https://github.com/Matysh/houseplan-card/issues/661)
(moon in its current phase on the "Follow the Sun" background), variant A:
one full-moon image, the phase is applied by the card as an SVG mask (C5).
- `moon.svg`: square `viewBox="0 0 512 512"`, disc centred at (256, 256),
diameter 480 (16 px margin for anti-aliasing). Full moon, front-lit: no
terminator, no shadow, no halo or glow outside the disc, no baked phase.
Near side, north up, Mare Crisium on the right; the art is never mirrored.
Owner 2026-09-29: the lit side is always the left one (as in the designer's
phase sequence), waning runs the same states backwards; only the mask flips.
Owner 2026-09-30: a distinct state for every day of the lunar month; the mask
is continuous, so the card simply uses the daily illumination k.
The dark side is not drawn: the card renders the same art at 8 % opacity.
- Edge: one radial gradient ring with no hard outer edge (as in the designer's
Figma export): bright rim 0.855-0.90 R, dark outline `#2b3138` at 50 % from
0.905 R to 0.92 R (owner 2026-09-29, for a forced white background), then a
1.5-unit fade to transparent by 0.935 R (r 239.4 in the 512 box, inside the 480
disc). The soft limb hides any missing anti-aliasing of the outer edge.
- The disc is masked (`<mask>`), not clipped: Chrome with GPU rasterisation draws
`clip-path` edges without anti-aliasing (seen on Windows), masks are anti-aliased.
- Art by JB (Figma, `House-plan` file). The Figma export (`source/figma-export/Full.svg`
in the source archive) was normalised for the pack by `source/convert_jb.py`:
viewBox 200 -> 512 (scale 2.56, disc clipped at r 240), the `mix-blend-mode`
screen ring and multiply groups removed (blend modes depend on the stacking
context they land in, so the file now composites normally and looks the same
everywhere), the three 5-7 % crater layers and the 484-path speckle texture
dropped (invisible at 200 px), and each mare "blend" of 22 nested rings
thinned to 4 rings (steps 0, 8, 16, 21). The export's screen-blended edge ring is rebuilt
as a normal-composited radial gradient (the art is masked at r 230), so the feathered edge is part
of the 480 disc and nothing is drawn outside it. Path coordinates rounded to 0.1, gradient offsets kept at 0.001 (`svgo.config.mjs`).
- Vector only: two `radialGradient`s (disc, edge), one `mask`, paths. No `filter`,
`feGaussianBlur`, `feImage`, embedded raster, fonts or external references.
25.6 KB raw, about 9 KB gzip.
- Colours: disc `#949493` (centre) -> `#D6D5D4` (rim), maria grey rings
`#404040 -> white` under a 0.2-0.6 group opacity. Checked on the night
(`#111a27 -> #1f2f3e`), dusk (`#48536c -> #9a7380`) and dawn
(`#aabdd1 -> #e8c8b7`) palettes at 200, 120 and 94 px.
- `pack.json`: pack id, version, licence, source link and the disc geometry
for `scripts/generate-moon-assets.mjs`.
JB (justbusiness) supplies this asset for House Plan to use, modify and
distribute under the repository MIT License without separate UI attribution
(`LICENSE.md`). The reviewed source archive with its SHA-256 is attached to
issue #661. Generated TypeScript (`src/moon-art.generated.ts`) must not be
edited by hand; regenerate it from this pack.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

+38
View File
@@ -0,0 +1,38 @@
{
"schema_version": 1,
"pack_id": "houseplan",
"pack_version": "1.0.0",
"title_ru": "Луна: полный диск для маски фазы",
"title_en": "Moon: full disc for the phase mask",
"author": "JB (justbusiness)",
"license": "MIT",
"source_url": "https://www.figma.com/design/cpGN8MhJGydwVOUzZv8dia/House-plan",
"issue": 661,
"file": "moon.svg",
"view_box": [
0,
0,
512,
512
],
"disc": {
"cx": 256,
"cy": 256,
"r": 240
},
"lit": "front",
"north_up": true,
"east_right": true,
"notes_en": "Full moon, front-lit, no terminator, no shadow, no glow outside the disc; thin dark outline for light backgrounds. Art by JB (Figma export normalised: viewBox 512, blend modes removed, rings thinned). Phase mask and the 8 % dark side are applied by the card (C5). The art is never mirrored; the lit side is always the left one, waning runs the same states backwards (owner 2026-09-29).",
"notes_ru": "Полная луна, освещение в лоб, без терминатора, тени и ореола; тонкая тёмная обводка для светлого фона. Арт JB (экспорт из Figma нормализован: viewBox 512, без blend-режимов, кольца морей прорежены). Маску фазы и тёмную сторону 8 % накладывает карточка (C5). Арт не зеркалится; освещённая сторона всегда слева, убывание идёт теми же состояниями в обратном порядке (владелец 29.09).",
"mirror": false,
"outline": {
"color": "#2b3138",
"opacity": 0.5,
"from_r": 0.905,
"to_r": 0.92,
"fade_to_r": 0.935,
"note": "part of the edge gradient, no separate stroke"
},
"lit_side": "left"
}
+42 -3
View File
@@ -6,6 +6,7 @@ import logging
from datetime import timedelta
from pathlib import Path
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.event import async_track_time_interval
@@ -39,6 +40,30 @@ from .store import (
_LOGGER = logging.getLogger(__name__)
async def _async_flush_runtime(
hass: HomeAssistant, entry: HouseplanConfigEntry,
) -> None:
"""Stop deferred writers and persist their latest state once.
Home Assistant stop and config-entry unload share this exact path. Both
writers are independently guarded so one failed store cannot prevent the
other from reaching disk during shutdown.
"""
recorder = hass.data.get(DOMAIN, {}).get("trail_recorder")
if recorder is not None:
try:
await recorder.async_teardown()
except Exception: # noqa: BLE001 - shutdown must continue with other stores
_LOGGER.exception("House Plan: flushing vacuum trails failed")
virtual_lights = getattr(entry.runtime_data, "virtual_lights", None)
if virtual_lights is not None:
try:
await virtual_lights.async_flush()
except Exception: # noqa: BLE001 - shutdown must continue with other stores
_LOGGER.exception("House Plan: flushing virtual-light state failed")
async def async_setup(hass: HomeAssistant, config) -> bool:
"""Register global handlers (survive config-entry reloads): WS commands, HTTP view."""
hass.data.setdefault(DOMAIN, {})
@@ -48,11 +73,13 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
HouseplanContentView,
HouseplanDecorAssetUploadView,
HouseplanImportPreviewView,
HouseplanPlanUploadView,
HouseplanUploadView,
)
hass.http.register_view(HouseplanUploadView())
hass.http.register_view(HouseplanDecorAssetUploadView())
hass.http.register_view(HouseplanPlanUploadView())
hass.http.register_view(HouseplanContentView())
hass.http.register_view(HouseplanImportPreviewView())
hass.http.register_view(HouseplanFrontendAssetView())
@@ -250,6 +277,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) ->
# sidebar entry after a migration, repair or initial housekeeping failure.
panel_path = Path(__file__).parent / "frontend" / "houseplan-panel.js"
await async_setup_panel_registration(hass, entry, panel_path)
async def _flush_on_stop(_event) -> None:
stop_listener[0] = None
await _async_flush_runtime(hass, entry)
stop_listener = [hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _flush_on_stop)]
def _remove_stop_listener() -> None:
if stop_listener[0] is not None:
stop_listener[0]()
stop_listener[0] = None
entry.async_on_unload(_remove_stop_listener)
return True
@@ -261,9 +301,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) -
entry is loaded. Static paths cannot be unregistered by design.
"""
remove_panel_registration(hass)
rec = hass.data.get(DOMAIN, {}).pop("trail_recorder", None)
if rec:
rec.teardown()
await _async_flush_runtime(hass, entry)
hass.data.get(DOMAIN, {}).pop("trail_recorder", None)
if entry.runtime_data.radar_coordinator:
entry.runtime_data.radar_coordinator.teardown()
entry.runtime_data.radar_coordinator = None
+25 -1
View File
@@ -7,6 +7,7 @@ one behaviour.
"""
from __future__ import annotations
from homeassistant.auth.const import GROUP_ID_READ_ONLY
from homeassistant.core import HomeAssistant
from .const import CONF_ADMIN_ONLY
@@ -28,4 +29,27 @@ def may_write(hass: HomeAssistant, user) -> bool:
# UI has always been admin-gated, and an unset option must not open every
# write WS/HTTP path to every authenticated household user.
admin_only = bool(entry.options.get(CONF_ADMIN_ONLY, True))
return is_admin if admin_only else True
if admin_only:
return is_admin
# Administrators retain write access regardless of their group payload.
# HA normally exposes admin groups too, but authorization must be based on
# the explicit admin flag rather than incidental group serialization.
if is_admin:
return True
# Turning off ``admin_only`` grants editing to ordinary household users,
# not to HA's explicitly read-only role (#626). ``groups`` is part of the
# supported HA User model; if a non-admin connection cannot provide a
# complete group list, the safe interpretation is that write permission
# has not been established.
groups = getattr(user, "groups", None)
if not isinstance(groups, list) or not groups:
return False
group_ids: list[str] = []
for group in groups:
group_id = getattr(group, "id", None)
if not isinstance(group_id, str):
return False
group_ids.append(group_id)
return GROUP_ID_READ_ONLY not in group_ids
+4 -2
View File
@@ -57,7 +57,7 @@ SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
ASSETS_DIR = "houseplan/assets"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.77.0"
VERSION = "1.79.0-beta.2"
DECOR_ASSETS_API_VERSION = 1
SUMMARY_PANEL_API_VERSION = 1
@@ -91,7 +91,9 @@ MAX_IMPORT_PREVIEWS_TOTAL = 3
DEFAULT_CONFIG: dict[str, object] = {
"spaces": [],
"markers": [],
"settings": {"bg_mode": "daynight", "sun_ray_origin": "inner"},
# #661: new installations get the moon; an existing config without the key
# keeps its look (docs/SUN.md: an update never changes a stored plan).
"settings": {"bg_mode": "daynight", "sun_ray_origin": "inner", "moon": True},
}
# #42: THE stable public error-code contract. Every code a user-facing
@@ -158,6 +158,12 @@ def canonicalize_config_geometry(config: Any) -> Any:
_lattice_fields(decor, ("x", "y"))
_scalar_fields(decor, ("scale", "angle"))
for stair in _records(space.get("stairs")):
# #663: wall/stair magnet placement is continuous like furniture.
# Scalar cleanup removes JSON tails without moving an authored
# physical-face contact onto the plan grid.
_scalar_fields(stair, ("x", "y", "length", "width", "radius", "angle"))
# Legacy-only read boundary: v9 drafts are canonicalized before the
# wall-model migration turns their edges into current partitions.
for draft in _records(space.get("room_drafts")):
+5 -2
View File
@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant
from .store import HouseplanConfigEntry
# Marker metadata may contain personal notes, external links and manual filenames.
TO_REDACT = {"link", "description", "pdfs", "name"}
TO_REDACT = {"link", "description", "pdfs", "name", "binding", "settings"}
async def async_get_config_entry_diagnostics(
@@ -33,10 +33,13 @@ async def async_get_config_entry_diagnostics(
"rooms_with_area": sum(1 for r in s.get("rooms", []) if r.get("area")),
"partitions": len(s.get("partitions", [])),
"wall_columns": len(s.get("wall_columns", [])),
"stairs": len(s.get("stairs", [])),
}
for s in config.get("spaces", [])
],
"markers": async_redact_data(config.get("markers", []), TO_REDACT),
"settings": config.get("settings", {}),
"settings": async_redact_data(
{"settings": config.get("settings", {})}, TO_REDACT
)["settings"],
"layout_entries": len(layout),
}
@@ -1,312 +1,466 @@
{
"schema": 1,
"fingerprint": "5e6b797362146dcb25e3ad594eca3a115b92cc4df3101d718fe7950b0ebfc362",
"fingerprint": "2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004",
"entry": "houseplan-card.js",
"panelEntry": "houseplan-panel.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BGma7Zjs.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 290095,
"initialViewGzipBytes": 300565,
"initialPanelFiles": [
"houseplan-assets/houseplan-card-BGma7Zjs.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-panel.js"
],
"initialPanelGzipBytes": 291618,
"initialPanelGzipBytes": 302087,
"initialPanelOnlyFiles": [
"houseplan-panel.js"
],
"initialPanelOnlyGzipBytes": 2325,
"initialPanelOnlyGzipBytes": 2323,
"lazyFiles": [
"houseplan-assets/backdrop-pick-Cghq1scc.js",
"houseplan-assets/de-DSAbt10z.js",
"houseplan-assets/editor-DuEZ_blF.js",
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/fr-Dqa7XXi8.js",
"houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js",
"houseplan-assets/houseplan-editor-runtime-dcfYk9Ux.js",
"houseplan-assets/houseplan-onboarding-runtime-DO5YuA8C.js",
"houseplan-assets/hp-zigbee-topology-overlay-Fybk76vu.js",
"houseplan-assets/iso-scene-render-DrqMFlnw.js",
"houseplan-assets/live-interaction-runtime-CY4BdkFz.js",
"houseplan-assets/pdf-export-uvcL70gX.js",
"houseplan-assets/summary-panel-runtime-loaded-CGySQMOD.js",
"houseplan-assets/zigbee-topology-DmwpO4_X.js",
"houseplan-assets/zigbee-topology-runtime-BK3ECo6u.js"
"houseplan-assets/backdrop-pick-erCrvaVF.js",
"houseplan-assets/de-d9TwEFPV.js",
"houseplan-assets/editor-C_sFCxgH.js",
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/fr-CWO2BzcK.js",
"houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js",
"houseplan-assets/houseplan-editor-runtime-6Ti0yXA0.js",
"houseplan-assets/houseplan-onboarding-runtime-D2g8WweX.js",
"houseplan-assets/hp-zigbee-topology-overlay-CpILp2jS.js",
"houseplan-assets/iso-scene-render-R0PpaYmJ.js",
"houseplan-assets/live-interaction-runtime-zvKxzkbX.js",
"houseplan-assets/moon-runtime-DXG94qOm.js",
"houseplan-assets/namespace-language-CPNZBROg.js",
"houseplan-assets/pdf-export-ByjBIvRJ.js",
"houseplan-assets/space-editor-Ch0tH8G9.js",
"houseplan-assets/summary-panel-runtime-loaded-rO8jZiCI.js",
"houseplan-assets/zigbee-topology-DZ_-WbFy.js",
"houseplan-assets/zigbee-topology-runtime-Boh9TM9B.js"
],
"lazyGzipBytes": 459921,
"lazyGzipBytes": 464846,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-Cghq1scc.js",
"houseplan-assets/editor-DuEZ_blF.js",
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js",
"houseplan-assets/houseplan-editor-runtime-dcfYk9Ux.js",
"houseplan-assets/zigbee-topology-DmwpO4_X.js"
"houseplan-assets/backdrop-pick-erCrvaVF.js",
"houseplan-assets/editor-C_sFCxgH.js",
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js",
"houseplan-assets/houseplan-editor-runtime-6Ti0yXA0.js",
"houseplan-assets/namespace-language-CPNZBROg.js",
"houseplan-assets/zigbee-topology-DZ_-WbFy.js"
],
"lazyEditorGzipBytes": 243369,
"lazyEditorGzipBytes": 239369,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-Cghq1scc.js",
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/houseplan-onboarding-runtime-DO5YuA8C.js"
"houseplan-assets/backdrop-pick-erCrvaVF.js",
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/houseplan-onboarding-runtime-D2g8WweX.js",
"houseplan-assets/namespace-language-CPNZBROg.js"
],
"lazyOnboardingGzipBytes": 33816,
"lazyOnboardingGzipBytes": 28396,
"lazyLocaleFiles": [
"houseplan-assets/de-DSAbt10z.js",
"houseplan-assets/fr-Dqa7XXi8.js"
"houseplan-assets/de-d9TwEFPV.js",
"houseplan-assets/fr-CWO2BzcK.js"
],
"lazyLocaleGzipBytes": 55370,
"lazyLocaleGzipBytes": 56069,
"lazyIsometricFiles": [
"houseplan-assets/iso-scene-render-DrqMFlnw.js"
"houseplan-assets/iso-scene-render-R0PpaYmJ.js"
],
"lazyIsometricGzipBytes": 17642,
"lazyIsometricGzipBytes": 12345,
"lazyFurnitureArtFiles": [
"houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js"
"houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js"
],
"lazyFurnitureArtGzipBytes": 16940,
"lazyFurnitureArtGzipBytes": 16942,
"lazyPdfFiles": [
"houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js",
"houseplan-assets/pdf-export-uvcL70gX.js"
"houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js",
"houseplan-assets/pdf-export-ByjBIvRJ.js"
],
"lazyPdfGzipBytes": 126825,
"lazyPdfGzipBytes": 126940,
"lazyMoonFiles": [
"houseplan-assets/moon-runtime-DXG94qOm.js"
],
"lazyMoonGzipBytes": 11716,
"lazyNamespaceLocaleFiles": [
"houseplan-assets/settings-de-Cu0ARW9H.js",
"houseplan-assets/settings-fr-BIO-QwTT.js",
"houseplan-assets/settings-ru-gpeXJk2y.js",
"houseplan-assets/support-de-CH0Mth3e.js",
"houseplan-assets/support-fr-5un_aGUp.js",
"houseplan-assets/support-ru-BUP8Z3Kb.js",
"houseplan-assets/topology-de-DoQ2W7CB.js",
"houseplan-assets/topology-fr-4M_CQtH4.js",
"houseplan-assets/topology-ru-DLnNeJR5.js"
],
"lazyNamespaceLocaleGzipBytes": 22672,
"files": [
{
"path": "houseplan-assets/backdrop-pick-Cghq1scc.js",
"sha256": "a3c6643d8fa86b46c0b14d6464331ac722fb2de5f76090f2415c394c2b45830d",
"rawBytes": 74076,
"gzipBytes": 23932,
"path": "houseplan-assets/backdrop-pick-erCrvaVF.js",
"sha256": "58bdb0ca63168828ebdd1fe564ba18c096ee124ae01aa9795465497e3469c868",
"rawBytes": 55329,
"gzipBytes": 18026,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-assets/namespace-language-CPNZBROg.js"
],
"dynamicImports": []
"dynamicImports": [
"houseplan-assets/settings-de-Cu0ARW9H.js",
"houseplan-assets/settings-fr-BIO-QwTT.js",
"houseplan-assets/settings-ru-gpeXJk2y.js"
]
},
{
"path": "houseplan-assets/de-DSAbt10z.js",
"sha256": "00b04a9346c92a955a778fd765078a17e164f038ae93e8bf8980c047cc5c5eae",
"rawBytes": 92496,
"gzipBytes": 27928,
"path": "houseplan-assets/de-d9TwEFPV.js",
"sha256": "350157130d9bc87112a94a0062e6fdc5c1061efe8816be115a3c7836f54225d7",
"rawBytes": 93620,
"gzipBytes": 28263,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DuEZ_blF.js",
"sha256": "ffbfb66c07157e20053fd287e8e423b967322de5f633ba57ed3bdc9b6ab3f353",
"path": "houseplan-assets/editor-C_sFCxgH.js",
"sha256": "cb88aa19af05a9921272bd04e9023fdd75f0fbe63a8e490f7fd3c22652847c76",
"rawBytes": 3833,
"gzipBytes": 1583,
"gzipBytes": 1582,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/form-kit.styles-CfB4DWMs.js",
"sha256": "551efa002ff77410c8ecd04f0b1e8eebba295e8f04f1de1c0f5973bc60343cba",
"path": "houseplan-assets/form-kit.styles-BBYWe4Py.js",
"sha256": "88f80b20957ade215c2028113fab371dfb8588d93d19774cfebcc03f38ab6e6c",
"rawBytes": 25600,
"gzipBytes": 5056,
"gzipBytes": 5055,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-Dqa7XXi8.js",
"sha256": "6e339f8074d2ad2325b5c93161a251f89ab4041f5bda8a90a491c165dd6aec31",
"rawBytes": 94813,
"gzipBytes": 27442,
"path": "houseplan-assets/fr-CWO2BzcK.js",
"sha256": "ca7a4fc1e696690c6e971c94f46d856816583cd247fe66a64ec4ae371e196631",
"rawBytes": 96021,
"gzipBytes": 27806,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js",
"sha256": "4e2e255541baab68c9a71b8996f7db842cd2fcbd1466984acda21423b2279be7",
"path": "houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js",
"sha256": "1c9a0ca09ec566715248c691386bfd17e23d3ae821a0c21608f9513602c72095",
"rawBytes": 52035,
"gzipBytes": 16940,
"gzipBytes": 16942,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BGma7Zjs.js",
"sha256": "020967c47a73d107ac522a4dc2e458154324779405cdddc60e3548f72939e7b4",
"rawBytes": 1028792,
"gzipBytes": 289293,
"path": "houseplan-assets/houseplan-card-DGdbxgxC.js",
"sha256": "1a9aa573e7c525eaa850feecfa59c215ed926706908872937424881b5799f501",
"rawBytes": 1058576,
"gzipBytes": 299764,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-DSAbt10z.js",
"houseplan-assets/editor-DuEZ_blF.js",
"houseplan-assets/fr-Dqa7XXi8.js",
"houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js",
"houseplan-assets/houseplan-editor-runtime-dcfYk9Ux.js",
"houseplan-assets/houseplan-onboarding-runtime-DO5YuA8C.js",
"houseplan-assets/hp-zigbee-topology-overlay-Fybk76vu.js",
"houseplan-assets/iso-scene-render-DrqMFlnw.js",
"houseplan-assets/live-interaction-runtime-CY4BdkFz.js",
"houseplan-assets/pdf-export-uvcL70gX.js",
"houseplan-assets/summary-panel-runtime-loaded-CGySQMOD.js"
"houseplan-assets/de-d9TwEFPV.js",
"houseplan-assets/editor-C_sFCxgH.js",
"houseplan-assets/fr-CWO2BzcK.js",
"houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js",
"houseplan-assets/houseplan-editor-runtime-6Ti0yXA0.js",
"houseplan-assets/houseplan-onboarding-runtime-D2g8WweX.js",
"houseplan-assets/hp-zigbee-topology-overlay-CpILp2jS.js",
"houseplan-assets/iso-scene-render-R0PpaYmJ.js",
"houseplan-assets/live-interaction-runtime-zvKxzkbX.js",
"houseplan-assets/moon-runtime-DXG94qOm.js",
"houseplan-assets/pdf-export-ByjBIvRJ.js",
"houseplan-assets/space-editor-Ch0tH8G9.js",
"houseplan-assets/summary-panel-runtime-loaded-rO8jZiCI.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-dcfYk9Ux.js",
"sha256": "a594bdfea7e0eaac7be0e513cf316d28503194dd4cc156f832271ac3b4f91f2b",
"rawBytes": 677705,
"gzipBytes": 188432,
"path": "houseplan-assets/houseplan-editor-runtime-6Ti0yXA0.js",
"sha256": "a9aae4b426f09d16563c0fef14db3190c1d71f7ca8eb603fff9824d6fa632fdc",
"rawBytes": 690723,
"gzipBytes": 193056,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-Cghq1scc.js",
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js",
"houseplan-assets/houseplan-card-BGma7Zjs.js",
"houseplan-assets/zigbee-topology-DmwpO4_X.js"
"houseplan-assets/backdrop-pick-erCrvaVF.js",
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-assets/namespace-language-CPNZBROg.js",
"houseplan-assets/zigbee-topology-DZ_-WbFy.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-BK3ECo6u.js"
"houseplan-assets/support-de-CH0Mth3e.js",
"houseplan-assets/support-fr-5un_aGUp.js",
"houseplan-assets/support-ru-BUP8Z3Kb.js",
"houseplan-assets/zigbee-topology-runtime-Boh9TM9B.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-DO5YuA8C.js",
"sha256": "2341b680e95085a18f3a4258ef54d76de360d8eb7f62c22d0bbbeb9e552a3776",
"rawBytes": 15667,
"gzipBytes": 4828,
"path": "houseplan-assets/houseplan-onboarding-runtime-D2g8WweX.js",
"sha256": "ddb0ec7750f3269ba2a7d8d259f69f640ae4ddc7cf3bce378c70daece30d21c6",
"rawBytes": 15886,
"gzipBytes": 4907,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-Cghq1scc.js",
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/backdrop-pick-erCrvaVF.js",
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-assets/namespace-language-CPNZBROg.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-Fybk76vu.js",
"sha256": "9a79675b2b08e08e69f48268134b052111cdbaa804b6b270dfc3c66857c475fa",
"rawBytes": 10759,
"gzipBytes": 3685,
"path": "houseplan-assets/hp-zigbee-topology-overlay-CpILp2jS.js",
"sha256": "123c7685bcfb9a2e4b47fcd4ecbc19b2e7acc463afbfea4a00678dcf94287250",
"rawBytes": 10958,
"gzipBytes": 3765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js",
"houseplan-assets/zigbee-topology-DmwpO4_X.js",
"houseplan-assets/zigbee-topology-runtime-BK3ECo6u.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-assets/namespace-language-CPNZBROg.js",
"houseplan-assets/zigbee-topology-DZ_-WbFy.js",
"houseplan-assets/zigbee-topology-runtime-Boh9TM9B.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-DrqMFlnw.js",
"sha256": "516d5176c89713778220ea37910af126ac49deec11bca4835628acb9234a7c5b",
"rawBytes": 51983,
"gzipBytes": 17642,
"path": "houseplan-assets/iso-scene-render-R0PpaYmJ.js",
"sha256": "729b5d1c02300c80614c8fd14c28d9ca9ea310d0824c9630327e38d4b651bea5",
"rawBytes": 36700,
"gzipBytes": 12345,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-CY4BdkFz.js",
"sha256": "4dcf38616ab83f5a2914558399c3852b7afa6ba5878fee9f75cf406cc746dda1",
"rawBytes": 8216,
"gzipBytes": 3209,
"path": "houseplan-assets/live-interaction-runtime-zvKxzkbX.js",
"sha256": "a02ff870b438bd2070f53a164cbcf69efda578358fb14a32c32221e67fd43657",
"rawBytes": 8370,
"gzipBytes": 3277,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-uvcL70gX.js",
"sha256": "206bebb97b82857af0143431a63ef1cf81cc7e869daa807278ef474cc91a57c6",
"rawBytes": 236264,
"gzipBytes": 109885,
"path": "houseplan-assets/moon-runtime-DXG94qOm.js",
"sha256": "8f3839a242d5e6293134ffa5a10edfe63516d0f89e3097dfe68ff07f2111e246",
"rawBytes": 30292,
"gzipBytes": 11716,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-DvOAO0uu.js",
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-CMasQ4vg.js",
"sha256": "4b34a8e5ddd9ddf67aa84d27b93a345cfca03b693f1f2513e2e046054465d817",
"path": "houseplan-assets/namespace-language-CPNZBROg.js",
"sha256": "068f3469028297daea7cf745fc8be94a99aade466532b25d92b3b33928a6242e",
"rawBytes": 675,
"gzipBytes": 408,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/pdf-export-ByjBIvRJ.js",
"sha256": "e94672f04c50bdba29636ab9b5a206ace0d600486ea1ad5d87be9a9183666fee",
"rawBytes": 236714,
"gzipBytes": 109998,
"isEntry": false,
"imports": [
"houseplan-assets/furniture-plan-art.generated-CCd6mmtp.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-de-Cu0ARW9H.js",
"sha256": "a3755fbf40c148aa9848bf4a5191a27c636977c774dc01f137ccc2470c7a0ee5",
"rawBytes": 8886,
"gzipBytes": 3406,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-fr-BIO-QwTT.js",
"sha256": "abeee9f1cef76177568d2994cca968e07a42040ebd0a6bbc89e81d6eb7eb760e",
"rawBytes": 9297,
"gzipBytes": 3379,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/settings-ru-gpeXJk2y.js",
"sha256": "8e05df136db81011ac4c884665d889dc773e12523937215c5f078f71bf882752",
"rawBytes": 12284,
"gzipBytes": 3897,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-editor-Ch0tH8G9.js",
"sha256": "8136fb4e381881486a4f6aaec92e2cfc3d01dd5f9ea51f2c0dca1ebec9141a3e",
"rawBytes": 2619,
"gzipBytes": 1124,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-editor-4by4BwDV.js",
"sha256": "c0a480f80507425774e9a71d465014d825b7a9b877d75f20438ee43986af1325",
"rawBytes": 14862,
"gzipBytes": 3426,
"gzipBytes": 3423,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/houseplan-card-BGma7Zjs.js",
"houseplan-assets/summary-panel-runtime-loaded-CGySQMOD.js"
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-assets/summary-panel-runtime-loaded-rO8jZiCI.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-metrics-BeGWlYtn.js",
"sha256": "d3b46ab20f1de0a48a16737e7456c0318e9903b79df2c5f9f78082bf378e5c93",
"rawBytes": 2611,
"gzipBytes": 1307,
"path": "houseplan-assets/summary-panel-metrics-DmEWv1wn.js",
"sha256": "7ed391b93d0da3dcc1aaf6fc0c47cd5e0ee7106f6697f16143cb3f2d5dc25e80",
"rawBytes": 2701,
"gzipBytes": 1340,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/summary-panel-runtime-loaded-CGySQMOD.js",
"sha256": "70932d7306193061fed10ac47812d912c3a59a7417f95e240f37b61457bf09a8",
"rawBytes": 72733,
"gzipBytes": 20171,
"path": "houseplan-assets/summary-panel-runtime-loaded-rO8jZiCI.js",
"sha256": "9fbaea7e6dae38df1f12013f9eaef0b02936fec9ac5866a8f214b92ac1214050",
"rawBytes": 73476,
"gzipBytes": 20488,
"isEntry": false,
"imports": [
"houseplan-assets/form-kit.styles-CfB4DWMs.js",
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/form-kit.styles-BBYWe4Py.js",
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": [
"houseplan-assets/summary-panel-editor-CMasQ4vg.js",
"houseplan-assets/summary-panel-metrics-BeGWlYtn.js"
"houseplan-assets/summary-panel-editor-4by4BwDV.js",
"houseplan-assets/summary-panel-metrics-DmEWv1wn.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-DmwpO4_X.js",
"sha256": "a5f74e545476fb0048e5ae34d3e6aba6ca56d4774699b437f8400e40069a988e",
"rawBytes": 18962,
"gzipBytes": 7426,
"path": "houseplan-assets/support-de-CH0Mth3e.js",
"sha256": "f835d35a37d84f9972a2673f24105de7768d34e597553b22971ffbf708b36279",
"rawBytes": 6167,
"gzipBytes": 2393,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js"
],
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-BK3ECo6u.js",
"sha256": "aa8b20e637d9d66b0affee0d0efee1bfd7defa9285d92d1bd496777fe5f3432b",
"rawBytes": 3942,
"gzipBytes": 1762,
"path": "houseplan-assets/support-fr-5un_aGUp.js",
"sha256": "537afde2367d66766509cfd53664f76cb77c1ef689ec07ba59abd285a75c0a9f",
"rawBytes": 6333,
"gzipBytes": 2427,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/support-ru-BUP8Z3Kb.js",
"sha256": "d965e7e334b56a19d1175ba3c4b791e2585f45917582e3210904c1eb8705894f",
"rawBytes": 8376,
"gzipBytes": 2874,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-de-DoQ2W7CB.js",
"sha256": "708ceeb7a1c7c843a438f7cb9f9e5a310f4fad8a172c6e6be1ba375c270bc21a",
"rawBytes": 2698,
"gzipBytes": 1332,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-fr-4M_CQtH4.js",
"sha256": "72f6e5c98a4a1c9fce80de9d99193059c687045aae3c4b3b03a87b610469cd94",
"rawBytes": 2718,
"gzipBytes": 1351,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/topology-ru-DLnNeJR5.js",
"sha256": "1cb2e1f8984a997e20d3ea05b2749379e3c773e41bf7670226ac0141fca13ccf",
"rawBytes": 3669,
"gzipBytes": 1613,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-DZ_-WbFy.js",
"sha256": "b2deb6084e835c4864160259cf445a24f4340d54c0db574cc4e9635d47e4f6a1",
"rawBytes": 10958,
"gzipBytes": 4300,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js",
"houseplan-assets/zigbee-topology-DmwpO4_X.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-assets/namespace-language-CPNZBROg.js"
],
"dynamicImports": [
"houseplan-assets/topology-de-DoQ2W7CB.js",
"houseplan-assets/topology-fr-4M_CQtH4.js",
"houseplan-assets/topology-ru-DLnNeJR5.js"
]
},
{
"path": "houseplan-assets/zigbee-topology-runtime-Boh9TM9B.js",
"sha256": "2f82578f02c24a55de11c7591a421c19694eca62a23cc5f71ca1acf9abe8e5a2",
"rawBytes": 3983,
"gzipBytes": 1788,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-DGdbxgxC.js",
"houseplan-assets/namespace-language-CPNZBROg.js",
"houseplan-assets/zigbee-topology-DZ_-WbFy.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "d0b270ebad646accee58fd8cf7a130875da8b505b596f0e05ada91fffb41bb02",
"sha256": "acb67dca9358ea65c86cc0a5e6f1b6eabe364e2cf3b5ed4b67b1bf5f5b8d1c9a",
"rawBytes": 1205,
"gzipBytes": 802,
"gzipBytes": 801,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/houseplan-card-DGdbxgxC.js"
],
"dynamicImports": []
},
{
"path": "houseplan-panel.js",
"sha256": "4150cfd182db03e56a9e4b4b680ad72ace418f9882528e526de2579e32944606",
"sha256": "4b0022acf6b29c9904be5c5d9c69bc0ba92ee4a26e6daa6e09bc93345d8af354",
"rawBytes": 6165,
"gzipBytes": 2325,
"gzipBytes": 2323,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BGma7Zjs.js"
"houseplan-assets/houseplan-card-DGdbxgxC.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
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5e6b797362146dcb25e3ad594eca3a115b92cc4df3101d718fe7950b0ebfc362";import{az as e,l as o,aC as t,e8 as s,A as a,e9 as i,ea as l,eb as n,E as r,c}from"./houseplan-card-BGma7Zjs.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__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";import{aG as e,l as o,aJ as t,eD as s,A as a,eE as i,eF as l,eG as n,E as r,c}from"./houseplan-card-DGdbxgxC.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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
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
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";import{eJ as e,eK as a,eF as n}from"./houseplan-card-DGdbxgxC.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}],"2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004",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};
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
@@ -0,0 +1,7 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";import{aG as e,l as t,aJ as s,A as a,eE as o,eF as i,eG as n,E as c,eI as l,c as r}from"./houseplan-card-DGdbxgxC.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return t(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"fit",selector:{select:{mode:"dropdown",options:[{value:"content",label:s(this._lang,"editor.fit_content")},{value:"house",label:s(this._lang,"editor.fit_house")}]}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"light_pools",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}}]}render(){if(!this.hass||!this._config)return a;const e=o(this,i,t(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return c;this._loadSpaces();const h=this._lang,_={space:s(h,"editor.space"),title:s(h,"editor.title"),fit:s(h,"editor.framing"),show_button:s(h,"editor.show_button"),button_label:s(h,"editor.button_label"),button_target:s(h,"editor.button_target"),icon_size:s(h,"editor.icon_size"),show_temperature:s(h,"editor.show_temperature"),live_states:s(h,"editor.live_states"),light_pools:s(h,"editor.light_pools"),show_signal:s(h,"editor.show_signal")};return r`<ha-form
.hass=${this.hass}
.data=${{...this._config,fit:l(this._config.fit)}}
.schema=${this._schema}
.computeLabel=${e=>_[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`}_valueChanged(e){const t={...this._config||{},...e.detail.value};delete t.aspect_ratio;const s=new Event("config-changed",{bubbles:!0,composed:!0});s.detail={config:t},this.dispatchEvent(s)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-space-card-editor")||customElements.define("houseplan-space-card-editor",h);
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5e6b797362146dcb25e3ad594eca3a115b92cc4df3101d718fe7950b0ebfc362";import{c as a,A as e}from"./houseplan-card-BGma7Zjs.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-CGySQMOD.js";import"./form-kit.styles-CfB4DWMs.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__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";import{c as a,A as e}from"./houseplan-card-DGdbxgxC.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-rO8jZiCI.js";import"./form-kit.styles-BBYWe4Py.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 +0,0 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5e6b797362146dcb25e3ad594eca3a115b92cc4df3101d718fe7950b0ebfc362";import{ee as e,J as t,aX as n,H as i,aa as o,a8 as r,bP as s,bO as a,aW as c,aV as l,aY as d,ef as u,eg as f,bR as m,eh as g}from"./houseplan-card-BGma7Zjs.js";function v(e){if(!e.registry.authoritative)return null;const t=new Set,n=d(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 y(e,t){return e?.length?t?.length?g(e,t):e:t}function _(e,t){const n=c(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:l(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*h(c,l,d=_){try{let u=0;for(const f of l){const l=c.spaces.find(e=>String(e?.id)===f.id);if(!l)continue;const m=e(l,f),g=d(f,m);let v=null;for(const e of f.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const c=n(f.rooms,e.id,m.walls,m.openCuts,i,m.cellCm,o,r,g.roomGeom,g.multiWallNodes)||a;v=y(v,s(c,m.physicalBodies)),yield}const _=m.cellCm/o;u+=a(v)*_*_/1e4}return u}catch{return null}}function p(e,t,n=_){const i=h(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function b(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:m(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 S(e,t){const n=e?.states?.[t];if(!n)return null;const i=u(e,t);return i?f(i,String(n.attributes?.unit_of_measurement||"")):null}export{h as cleanFloorAreaSteps,v as representedHaDeviceIds,_ as spaceWallGeometry,S as summaryEntityValue,b as summarySystemValue,p as totalCleanFloorAreaM2};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";import{eM as e,O as t,b5 as n,M as i,af as o,ad as r,ce as s,eN as a,cd as c,b4 as l,b3 as d,b6 as u,eO as f,eP as m,cg as g,eQ as v}from"./houseplan-card-DGdbxgxC.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};
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
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";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="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";export{e as dictionary,n as fingerprint};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";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="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";export{e as dictionary,s as fingerprint};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";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="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";export{e as dictionary,a as fingerprint};
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__="5e6b797362146dcb25e3ad594eca3a115b92cc4df3101d718fe7950b0ebfc362";import{n as t,b as n}from"./zigbee-topology-DmwpO4_X.js";import{aI as e}from"./houseplan-card-BGma7Zjs.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};
@@ -0,0 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";import{n as t,c as n}from"./zigbee-topology-DZ_-WbFy.js";import{aP as e}from"./houseplan-card-DGdbxgxC.js";import"./namespace-language-CPNZBROg.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function m(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function g(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function h(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,h=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,h=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&m(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=m(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&h?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await g(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await g(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,h as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="5e6b797362146dcb25e3ad594eca3a115b92cc4df3101d718fe7950b0ebfc362";try{await import("./houseplan-assets/houseplan-card-BGma7Zjs.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__="2c4b7fa5cbc37ca9d9ac95d53f27dcefde7da87e20d87774a9e7ec19a9399004";try{await import("./houseplan-assets/houseplan-card-DGdbxgxC.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
+162 -25
View File
@@ -36,6 +36,8 @@ from .const import (
MAX_EXPORT_BYTES,
MAX_FILES_BYTES,
MAX_FILES_COUNT,
MAX_PLANS_BYTES,
MAX_PLANS_FILES,
MIN_FREE_BYTES,
PLANS_DIR,
)
@@ -50,15 +52,25 @@ from .decor_assets import (
validate_asset,
)
from .import_export import ImportFailure, create_preview
from .plans import TMP_PREFIX, QuotaError, check_quota, reserve_filename
from .plans import (
TMP_PREFIX,
QuotaError,
check_quota,
read_bounded,
reserve_filename,
store_plan_upload,
)
from .registry_snapshot import import_registry_snapshot
from .store import get_data
from .validation import (
FILE_EXTENSIONS,
MAX_FILE_BYTES,
MAX_PLAN_BYTES,
PLAN_EXTENSIONS,
file_ext,
sanitize_filename,
sanitize_marker_id,
valid_space_id,
)
_LOGGER = logging.getLogger(__name__)
@@ -255,13 +267,6 @@ class HouseplanDecorAssetUploadView(HomeAssistantView):
if not filename:
return web.json_response({"error": "invalid_format"}, status=400)
try:
validated = await hass.async_add_executor_job(
validate_asset, b"".join(blocks), filename, declared_mime,
)
except DecorAssetError as err:
return web.json_response({"error": err.code, "message": str(err)}, status=413 if err.code == "too_large" else 400)
root = Path(hass.config.path(ASSETS_DIR))
def _store() -> tuple[dict, bool]:
@@ -339,9 +344,19 @@ class HouseplanDecorAssetUploadView(HomeAssistantView):
try:
async with runtime.upload_lock:
# Pillow may consume far more RSS than the compressed body.
# Serialise decode/validation as well as quota+promotion so N
# parallel uploads cannot become N simultaneous decoders.
validated = await hass.async_add_executor_job(
validate_asset, b"".join(blocks), filename, declared_mime,
)
row, reused = await hass.async_add_executor_job(_store)
except DecorAssetError as err:
status = 507 if err.code == "capacity_exceeded" else 400
status = (
507 if err.code == "capacity_exceeded"
else 413 if err.code == "too_large"
else 400
)
return web.json_response({"error": err.code, "message": str(err)}, status=status)
except OSError as err:
_LOGGER.warning("House Plan decor asset upload: store failed: %s", err)
@@ -349,6 +364,90 @@ class HouseplanDecorAssetUploadView(HomeAssistantView):
return web.json_response({"ok": True, "reused": reused, "asset": public_asset(row)})
# Form fields other than the file are a space id (<= 64 chars) and an
# extension; a kilobyte is generous and keeps a hostile field from buffering.
_PLAN_FIELD_BYTES = 1024
class HouseplanPlanUploadView(HomeAssistantView):
"""POST /api/houseplan/plans/upload — store one space plan, return its URL.
The card used to send plans base64-encoded over WebSocket; above ~3 MiB the
frame passed the 4 MiB message limit and HA closed the socket before
`houseplan/plan/set` ever ran, so the advertised 8 MiB limit was a lie
(#617). Multipart over HTTP has no such frame, and the body is bounded
while it streams. The body is buffered in memory (<= MAX_PLAN_BYTES plus
one chunk), like the decor view, and written by the same
`store_plan_upload` that the WebSocket command uses.
"""
url = "/api/houseplan/plans/upload"
name = "api:houseplan:plan-upload"
requires_auth = True
async def post(self, request: web.Request) -> web.Response:
hass: HomeAssistant = request.app[KEY_HASS]
if not may_write(hass, request.get("hass_user")):
return web.json_response({"error": "unauthorized"}, status=403)
runtime = get_data(hass)
if runtime is None:
return web.json_response({"error": "not_ready"}, status=503)
too_large = web.json_response(
{"error": "too_large", "max_mb": MAX_PLAN_BYTES // 1024 // 1024}, status=413,
)
# Content-Length covers the multipart envelope too: reserve one batch
# for it, like the other upload views, and let the streaming bound
# below decide exactly.
declared = request.content_length
if declared is not None and declared > MAX_PLAN_BYTES + _FLUSH_AT:
return too_large
space_id: str | None = None
ext: str | None = None
raw: bytes | None = None
try:
reader = await request.multipart()
async for part in reader:
if part.name == "file":
if raw is not None:
return web.json_response({"error": "one_file_only"}, status=400)
raw = await read_bounded(part, MAX_PLAN_BYTES, _CHUNK)
if raw is None:
return too_large
elif part.name in ("space_id", "ext"):
field = await read_bounded(part, _PLAN_FIELD_BYTES, _CHUNK)
value = field.decode("utf-8", "replace") if field is not None else ""
if part.name == "space_id":
space_id = value
else:
ext = value
except Exception as err: # noqa: BLE001 - a broken multipart stream must answer 400, not crash the view
_LOGGER.warning("House Plan plan upload: multipart read error: %s", err)
return web.json_response({"error": "bad_request"}, status=400)
if space_id is None or not valid_space_id(space_id):
return web.json_response({"error": "invalid_space_id"}, status=400)
if ext not in PLAN_EXTENSIONS:
return web.json_response({"error": "bad_ext"}, status=400)
if raw is None:
return web.json_response({"error": "no_file"}, status=400)
plans_dir = Path(hass.config.path(PLANS_DIR))
try:
async with runtime.upload_lock:
name = await hass.async_add_executor_job(
partial(
store_plan_upload, plans_dir, space_id, ext, raw,
max_bytes=MAX_PLANS_BYTES, max_files=MAX_PLANS_FILES,
)
)
except QuotaError as err:
_LOGGER.warning("House Plan plan upload refused: %s", err.detail)
return web.json_response({"error": err.reason, "detail": err.detail}, status=507)
except OSError as err:
_LOGGER.warning("House Plan plan upload: could not store the file: %s", err)
return web.json_response({"error": "io_error"}, status=500)
return web.json_response({"ok": True, "url": f"{CONTENT_URL}/plans/_/{name}"})
class HouseplanUploadView(HomeAssistantView):
"""POST /api/houseplan/upload — save a marker file, return its URL."""
@@ -362,6 +461,40 @@ class HouseplanUploadView(HomeAssistantView):
return web.json_response({"error": "unauthorized"}, status=403)
files_root = Path(hass.config.path(FILES_DIR))
runtime = get_data(hass)
if runtime is None:
return web.json_response({"error": "not_ready"}, status=503)
# Content-Length includes multipart boundaries and headers, not just
# file bytes. Reserve one streaming batch for that overhead and only
# preflight the guaranteed payload floor; otherwise a tiny file at the
# exact store boundary would be rejected by its envelope (#498). The
# exact staged size is checked again under the same lock immediately
# before promotion.
declared_size = getattr(request, "content_length", None)
if declared_size is not None and declared_size > 0:
if declared_size > MAX_FILE_BYTES + _FLUSH_AT:
return web.json_response(
{"error": "too_large", "max_mb": MAX_FILE_BYTES // 1024 // 1024},
status=413,
)
payload_floor = max(0, declared_size - _FLUSH_AT)
if payload_floor:
try:
async with runtime.upload_lock:
await hass.async_add_executor_job(
partial(
check_quota,
files_root,
payload_floor,
MAX_FILES_BYTES,
MAX_FILES_COUNT,
)
)
except QuotaError as err:
return web.json_response(
{"error": err.reason, "detail": err.detail}, status=507
)
marker_id = "misc"
filename: str | None = None
# Every temporary file this request creates, promoted or not. The outer
@@ -444,21 +577,6 @@ class HouseplanUploadView(HomeAssistantView):
return web.json_response({"error": "no_file"}, status=400)
tmp_path = temps[0]
try:
# The staged file already sits under files_root: hand it to
# the quota as `incoming` only, not as stored usage too (#498).
await hass.async_add_executor_job(
partial(
check_quota, files_root, tmp_path.stat().st_size,
MAX_FILES_BYTES, MAX_FILES_COUNT, exclude=tmp_path,
additional_disk_bytes=0,
),
)
except QuotaError as err:
_LOGGER.warning("House Plan upload refused: %s", err.detail)
return web.json_response({"error": err.reason, "detail": err.detail}, status=507)
except OSError:
pass
target_dir = files_root / marker_id
safe_name = filename
@@ -480,8 +598,27 @@ class HouseplanUploadView(HomeAssistantView):
raise
return name
def _check_and_promote() -> str:
# The staged file already sits under files_root: hand it to
# the quota as `incoming` only, not as stored usage too (#498).
check_quota(
files_root,
tmp_path.stat().st_size,
MAX_FILES_BYTES,
MAX_FILES_COUNT,
exclude=tmp_path,
additional_disk_bytes=0,
)
return _promote()
try:
name = await hass.async_add_executor_job(_promote)
async with runtime.upload_lock:
name = await hass.async_add_executor_job(_check_and_promote)
except QuotaError as err:
_LOGGER.warning("House Plan upload refused: %s", err.detail)
return web.json_response(
{"error": err.reason, "detail": err.detail}, status=507
)
except OSError as err:
_LOGGER.warning("House Plan upload: could not store the file: %s", err)
return web.json_response({"error": "io_error"}, status=500)
+68 -3
View File
@@ -45,12 +45,14 @@ from .validation import (
MAX_LAYOUT,
MAX_MARKERS,
MAX_SPACES,
DuplicateMarkerIdError,
MarkerControlError,
OpeningPassageError,
PartitionOpeningHostError,
PartitionOpeningJambMarginError,
sanitize_filename,
sanitize_marker_id,
validate_active_marker_ids,
validate_marker_controls,
validate_marker_light_entities,
validate_marker_vacuum_routes,
@@ -71,7 +73,7 @@ _LIVE_TEXT_ENTITY = re.compile(r"^[a-z0-9_]+\.[a-z0-9_]+$")
_LIVE_TEXT_ATTRIBUTE = re.compile(r"^[a-zA-Z0-9_.-]+$")
_PLAN_ONLY_DASH = "—"
_IMPORT_ID_NAMESPACES = {
"space", "room", "marker", "partition", "wall", "opening", "decor", "column",
"space", "room", "marker", "partition", "wall", "opening", "decor", "column", "stair",
}
_MAX_IMPORT_LINEAGE_DEPTH = 16
_REPORT_EXAMPLE_LIMIT = 24
@@ -292,6 +294,15 @@ def _project_plan_only_space(space: dict[str, Any]) -> dict[str, Any]:
projected["decor"] = [
_project_plan_only_decor(shape) for shape in space.get("decor") or []
]
if "stairs" in space:
projected["stairs"] = [
_pick_fields(stair, (
"id", "kind", "x", "y", "angle", "direction",
"target_space_id", "length", "width", "radius",
"color", "opacity", "fill_color", "fill_opacity",
))
for stair in space.get("stairs") or []
]
return projected
@@ -740,6 +751,7 @@ def _counts(config: dict[str, Any], layout: dict[str, Any]) -> dict[str, int]:
"markers": len(config.get("markers") or []),
"openings": sum(len(sp.get("openings") or []) for sp in spaces),
"decor": sum(len(sp.get("decor") or []) for sp in spaces),
"stairs": sum(len(sp.get("stairs") or []) for sp in spaces),
"layout": len(layout),
}
@@ -1130,6 +1142,14 @@ def _repair_target_space_refs(
for space in spaces:
space_id = str(space.get("id", "?"))
for stair in space.get("stairs") or []:
target = stair.get("target_space_id") if isinstance(stair, dict) else None
mapped = replace(
f"{space_id}:{stair.get('id', '?')}", "stair.target_space_id",
target, resolve_space,
)
if mapped is not None:
stair["target_space_id"] = mapped
for room in space.get("rooms") or []:
room_id = str(room.get("id", "?"))
values = room.get("open_to")
@@ -1177,6 +1197,17 @@ def _repair_target_space_refs(
preserve_related(
marker_id, "marker.vacuum.segment_map", room_id, resolve_room,
)
routes = vacuum.get("map_routes") if isinstance(vacuum, dict) else None
if isinstance(routes, list):
for route in routes:
if not isinstance(route, dict):
continue
mapped = replace(
marker_id, "marker.vacuum.map_routes.space",
route.get("space"), resolve_space,
)
if mapped is not None:
route["space"] = mapped
controls = marker.get("controls")
if isinstance(controls, list):
for index, ref in enumerate(controls):
@@ -1309,7 +1340,7 @@ def build_space_merge(
(sp,), sp.get("rooms") or [],
sp.get("partitions") or [], sp.get("wall_segments") or [],
sp.get("wall_columns") or [],
sp.get("openings") or [], sp.get("decor") or [],
sp.get("openings") or [], sp.get("decor") or [], sp.get("stairs") or [],
)
for item in collection
if isinstance(item, dict)
@@ -1325,7 +1356,7 @@ def build_space_merge(
(incoming_space,), incoming_space.get("rooms") or [],
incoming_space.get("partitions") or [], incoming_space.get("wall_segments") or [],
incoming_space.get("wall_columns") or [],
incoming_space.get("openings") or [], incoming_space.get("decor") or [],
incoming_space.get("openings") or [], incoming_space.get("decor") or [], incoming_space.get("stairs") or [],
)
for item in collection
if isinstance(item, dict)
@@ -1349,6 +1380,7 @@ def build_space_merge(
("rooms", "room"), ("partitions", "partition"),
("wall_segments", "wall"),
("wall_columns", "column"), ("openings", "opening"), ("decor", "decor"),
("stairs", "stair"),
):
for item in space.get(collection) or []:
if isinstance(item, dict) and item.get("id") is not None:
@@ -1392,6 +1424,16 @@ def build_space_merge(
reference_report, "incoming", "opening.host",
str(opening.get("id", "?")), old_host_id,
)
# A one-space export cannot carry a trustworthy external floor target.
# Self-links are invalid too because the imported source gets a new id.
for stair in space.get("stairs") or []:
target = stair.get("target_space_id") if isinstance(stair, dict) else None
if target is not None:
stair["target_space_id"] = None
_report_remap(
reference_report, "incoming", "stair.target_space_id",
str(stair.get("id", "?")), str(target),
)
space["id"] = new_space_id
space["title"] = _unique_title(
str(space.get("title") or old_space_id), current_config.get("spaces") or []
@@ -1464,6 +1506,21 @@ def build_space_merge(
new_id, old_room_id,
)
vacuum["segment_map"] = remapped_segments
# A single-space export can retain only routes owned by that space.
# The copy itself gets a fresh id, so the retained route must follow
# it just like marker.space does; otherwise foreign imports fail the
# referential validator and same-instance imports silently target the
# source floor (#611).
if isinstance(vacuum, dict) and isinstance(vacuum.get("map_routes"), list):
for route in vacuum["map_routes"]:
if not isinstance(route, dict) \
or str(route.get("space")) != old_space_id:
continue
route["space"] = new_space_id
_report_remap(
reference_report, "incoming", "marker.vacuum.map_routes.space",
new_id, old_space_id,
)
if binding in duplicate and duplicate_policy == "virtual":
virtualized += 1
virtualized_targets.add(old_id)
@@ -1861,6 +1918,14 @@ def _materialize_import_candidate(
config = CONFIG_SCHEMA(config)
except vol.Invalid as err:
raise ImportFailure("invalid_config", str(err)) from err
try:
validate_active_marker_ids(
config,
current_config if prepared["kind"] == "space" else None,
validate_all=prepared["kind"] == "full",
)
except DuplicateMarkerIdError as err:
raise ImportFailure(err.code, str(err)) from err
try:
layout = LAYOUT_SCHEMA(layout)
except vol.Invalid as err:
+1 -1
View File
@@ -20,5 +20,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.77.0"
"version": "1.79.0-beta.2"
}
+75 -1
View File
@@ -10,9 +10,11 @@ from __future__ import annotations
import logging
import os
import secrets
import tempfile
import time
from pathlib import Path
from typing import Any
from typing import Any, Protocol
from .const import MIN_FREE_BYTES, PLAN_ORPHAN_TTL_S
from .validation import MAX_FILENAME, PLAN_EXTENSIONS, sanitize_filename
@@ -25,6 +27,21 @@ _LOGGER = logging.getLogger(__name__)
TMP_PREFIX = ".upload-"
def atomic_write(path: Path, data: bytes, *, prefix: str = ".upload-") -> None:
"""Durably replace ``path`` without exposing a partial destination file."""
path.parent.mkdir(parents=True, exist_ok=True)
fd, temp_name = tempfile.mkstemp(prefix=prefix, dir=str(path.parent))
temp = Path(temp_name)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
os.replace(temp, path)
finally:
temp.unlink(missing_ok=True)
def reserve_filename(directory: Path, name: str) -> str:
"""Atomically claim a free name inside `directory` and return it.
@@ -261,6 +278,63 @@ def check_quota(
raise QuotaError("low_disk_space", f"only {free // 1024 // 1024} MB free on the disk")
class _ChunkReader(Protocol):
async def read_chunk(self, size: int = ...) -> bytes: ...
async def read_bounded(part: _ChunkReader, limit: int, chunk: int) -> bytes | None:
"""Read one multipart part into memory, or None once it passes ``limit``.
The bound is inclusive: exactly ``limit`` bytes is accepted, one more is
refused. Reading stops at the first block that crosses it, so an oversized
upload never costs more than ``limit + chunk`` bytes of memory and nothing
touches the disk (#617). Pure — the part only has to offer ``read_chunk``.
"""
blocks: list[bytes] = []
size = 0
while block := await part.read_chunk(chunk):
size += len(block)
if size > limit:
return None
blocks.append(block)
return b"".join(blocks)
def store_plan_upload(
plans_dir: Path,
space_id: str,
ext: str,
raw: bytes,
*,
max_bytes: int,
max_files: int,
) -> str:
"""Write one validated plan upload under a new name and return that name.
The single writer behind both transports — the HTTP view and the legacy
WebSocket ``houseplan/plan/set`` (#617) — so naming, quota and the atomic
write cannot drift between them. The caller has already checked
``space_id``, ``ext`` and the size, holds ``runtime.upload_lock`` and runs
this as ONE executor job: the quota measurement is only a bound if nothing
else writes between it and our write (HP-1490-02). A failed write reserves
nothing — the file either exists and is counted by the next scan, or does
not and is not.
Copy-on-write: a plan is written under a NEW unique name and nothing is
deleted here (review R2-1). The old name stays readable, so a config write
that is later rejected leaves the stored plan exactly as it was; the file a
commit REPLACES is collected by ``config/set`` itself (review R3-1).
``.`` separates the id from the token because a space id cannot contain one
(SPACE_ID_RE), so "<space>.<token>.<ext>" can never be confused with the
files of a differently named space.
"""
name = f"{space_id}.{secrets.token_hex(4)}.{ext}"
check_quota(plans_dir, len(raw), max_bytes, max_files)
atomic_write(plans_dir / name, raw, prefix=".plan-upload-")
return name
def plan_basename(url: Any) -> str:
"""File name a stored plan_url points at ('' when there is none)."""
if not isinstance(url, str) or not url:
@@ -123,4 +123,7 @@ rules:
comment: No dependencies.
inject-websession:
status: exempt
comment: No outgoing HTTP.
comment: >-
The optional user-confirmed support relay is the integration's only
outgoing HTTP path and creates its bounded ClientSession explicitly;
ordinary plan, device and asset operation remains local.
+11 -1
View File
@@ -81,6 +81,7 @@ class HouseplanData:
store: HouseplanStore
config_store: HouseplanStore
virtual_light_store: HouseplanStore
virtual_lights: Any | None = None
# One lock for every load→modify→save cycle of both stores: prevents
# lost updates from concurrent WS calls and makes the rev check atomic.
write_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
@@ -118,7 +119,7 @@ HouseplanConfigEntry = ConfigEntry[HouseplanData]
def create_data(hass: HomeAssistant) -> HouseplanData:
"""Create the stores for a config entry."""
return HouseplanData(
data = HouseplanData(
store=HouseplanStore(hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_MINOR_VERSION),
config_store=HouseplanStore(
hass, STORAGE_VERSION, STORAGE_CONFIG_KEY, minor_version=STORAGE_MINOR_VERSION
@@ -130,6 +131,10 @@ def create_data(hass: HomeAssistant) -> HouseplanData:
minor_version=STORAGE_MINOR_VERSION,
),
)
from .virtual_lights import VirtualLightController
data.virtual_lights = VirtualLightController(hass, data.virtual_light_store)
return data
def get_data(hass: HomeAssistant) -> HouseplanData | None:
@@ -229,12 +234,17 @@ async def async_save_config_state(
from .virtual_lights import async_reconcile_virtual_lights
try:
controller = getattr(runtime, "virtual_lights", None)
if controller is not None:
await controller.async_flush()
await async_reconcile_virtual_lights(
runtime.virtual_light_store,
canonical_config,
rev,
previous_config_rev=previous_rev,
)
if controller is not None:
controller.reset()
except Exception: # noqa: BLE001 - config commit already stands
_LOGGER.exception("House Plan: virtual-light state reconciliation failed")
return payload
@@ -155,6 +155,12 @@ def _global_settings(value: object) -> dict[str, Any]:
show_room_tooltip = value.get("show_room_tooltip")
if isinstance(show_room_tooltip, bool):
out["show_room_tooltip"] = show_room_tooltip
volumetric_view = value.get("volumetric_view")
if isinstance(volumetric_view, bool):
out["volumetric_view"] = volumetric_view
moon = value.get("moon")
if isinstance(moon, bool):
out["moon"] = moon
fill_colors = value.get("fill_colors")
if isinstance(fill_colors, dict):
palette = {
@@ -371,6 +377,18 @@ def _project_space(ids: _Pseudonyms, space: dict[str, Any], index: int) -> dict[
out["decor"] = [
_project_decor(ids, item) for item in space.get("decor") or [] if isinstance(item, dict)
]
out["stairs"] = []
for item in space.get("stairs") or []:
if not isinstance(item, dict):
continue
projected = {"id": ids.get("stair", item.get("id"))}
projected.update(_copy_keys(item, (
"kind", "x", "y", "angle", "direction", "length", "width", "radius",
"color", "opacity", "fill_color", "fill_opacity",
)))
if item.get("target_space_id"):
projected["target_space_id"] = ids.get("space", item.get("target_space_id"))
out["stairs"].append(projected)
out["open_spans"] = [
{"a": _point(item.get("a")), "b": _point(item.get("b"))}
for item in space.get("open_spans") or [] if isinstance(item, dict)
@@ -433,6 +451,7 @@ def _summary(config: object, layout: object) -> dict[str, Any]:
"columns": sum(len(space.get("wall_columns") or []) for space in spaces if isinstance(space, dict)),
"openings": dict(sorted(kinds.items())),
"decor": dict(sorted(decor_kinds.items())),
"stairs": sum(len(space.get("stairs") or []) for space in spaces if isinstance(space, dict)),
"markers": {
"total": len(markers),
"lifecycle": dict(sorted(lifecycles.items())),
@@ -31,6 +31,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"rooms": sum(len(s.get("rooms", [])) for s in config.get("spaces", [])),
"partitions": sum(len(s.get("partitions", [])) for s in config.get("spaces", [])),
"wall_columns": sum(len(s.get("wall_columns", [])) for s in config.get("spaces", [])),
"stairs": sum(len(s.get("stairs", [])) for s in config.get("spaces", [])),
"markers": len(config.get("markers", [])),
"layout_entries": len(layout_raw.get("layout", {})),
}
+16 -2
View File
@@ -455,16 +455,30 @@ class TrailRecorder:
self.hass, sorted(ents), self._on_state
)
def teardown(self) -> None:
def _close_subscriptions(self) -> bool:
"""Stop callbacks/timers and report whether a save was pending."""
# HP-1540-05: flag FIRST — a refresh parked on its awaited load must
# not re-subscribe after this cleanup has already run
# not re-subscribe after this cleanup has already run.
self._closed = True
if self._unsub_track:
self._unsub_track()
self._unsub_track = None
pending = self._unsub_save is not None
if self._unsub_save:
self._unsub_save()
self._unsub_save = None
return pending
async def async_teardown(self) -> None:
"""Stop the recorder and durably flush a pending debounced save."""
async with self._refresh_lock:
pending = self._close_subscriptions()
if pending:
await self.store.async_save(self.book.data)
def teardown(self) -> None:
"""Synchronous emergency cleanup for already-closing event loops."""
self._close_subscriptions()
def _vacuum_entity(self, m: dict[str, Any]) -> str | None:
b = str(m.get("binding") or "")
+123 -6
View File
@@ -42,6 +42,62 @@ class MarkerControlError(ValueError):
self.code = code
class DuplicateMarkerIdError(ValueError):
"""A write introduced or retained a changed duplicate active marker id."""
code = "invalid_config"
def __init__(self) -> None:
# Marker ids may contain user-controlled entity/device identifiers. A
# stable message is enough and does not leak the value into logs.
super().__init__("duplicate active marker id")
def _active_marker_groups(config: dict | None) -> dict[str, list[str]]:
"""Return order-independent structural signatures grouped by live id."""
groups: dict[str, list[str]] = {}
for marker in (config or {}).get("markers") or []:
if not isinstance(marker, dict) or marker.get("removed") is True:
continue
marker_id = marker.get("id")
if not isinstance(marker_id, str) or not marker_id:
continue
groups.setdefault(marker_id, []).append(
json.dumps(marker, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
)
for signatures in groups.values():
signatures.sort()
return groups
def validate_active_marker_ids(
config: dict,
previous: dict | None = None,
*,
validate_all: bool = False,
) -> None:
"""Reject new active-id ambiguity without stranding legacy documents.
Existing duplicate groups may round-trip unchanged so an unrelated save
remains possible. Once any member changes, the candidate must repair the
group down to at most one active marker. Authoritative imports pass
``validate_all=True`` because they have no local legacy group to preserve.
"""
candidate = _active_marker_groups(config)
duplicates = {
marker_id: signatures
for marker_id, signatures in candidate.items()
if len(signatures) > 1
}
if not duplicates:
return
if validate_all or previous is None:
raise DuplicateMarkerIdError()
old = _active_marker_groups(previous)
if any(old.get(marker_id) != signatures for marker_id, signatures in duplicates.items()):
raise DuplicateMarkerIdError()
class OpeningPassageError(ValueError):
"""Semantic open-passage error with a stable public code and payload."""
@@ -1142,6 +1198,7 @@ MAX_ROOMS = 400
MAX_MARKERS = 2000
MAX_OPENINGS = 500
MAX_DECOR = 1000
MAX_STAIRS = 250
# v8 atomises room boundaries. The 2 MiB wire cap is the practical bound; this
# structural cap mirrors MAX_ROOMS * MAX_POLY_POINTS without depending on the
# later constant declaration.
@@ -1151,7 +1208,7 @@ _LEGACY_MAX_ROOM_DRAFTS = 200
_LEGACY_MAX_DRAFT_SEGMENTS = 2000
MAX_PARTITIONS = 2000
MAX_WALL_COLUMNS = 500
# Open (virtual) wall stretches, docs/superpowers/specs/2026-08-05-open-spans-delete-design.md.
# Open (virtual) wall stretches, legacy/docs/superpowers/specs/2026-08-05-open-spans-delete-design.md.
# Every span is a piece of a shared boundary, so there can never be more of
# them than there are wall segments — the cap is the walls' one (AUD-159B6-03).
MAX_OPEN_SPANS = 500
@@ -1223,7 +1280,7 @@ _GEOM = vol.All(_finite, vol.Range(min=-CANVAS_LIMIT, max=CANVAS_LIMIT))
# than the old unit square — while staying strictly positive.
_EXTENT = vol.All(_finite, vol.Range(min=0.001, max=CANVAS_LIMIT))
# The backdrop's uniform scale (docs/BACKDROP.md). A MULTIPLIER, not a
# The backdrop's uniform scale (docs/DECOR-EDITOR.md §3). A MULTIPLIER, not a
# coordinate: strictly positive, and bounded by what a person could mean —
# a hundredth of the canvas is already a thumbnail, a hundred canvases is
# already absurd. Mirrored by PLAN_SCALE_MIN/MAX in src/space-geometry.ts.
@@ -1371,7 +1428,7 @@ SPACE_DISPLAY_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
# Live text on a decor label (docs/LIVE-TEXT.md). An entity id is
# Live text on a decor label (docs/DECOR-EDITOR.md §5). An entity id is
# `<domain>.<object_id>`; HA itself allows only lowercase letters, digits and
# underscores in both halves. The bound is a sanity limit, not a policy.
MAX_ENTITY_ID = 255
@@ -1433,7 +1490,7 @@ DECOR_SCHEMA = vol.Any(
vol.Schema({**_DECOR_COMMON, vol.Required("kind"): "text",
vol.Required("x"): _NORM, vol.Required("y"): _NORM,
# the template: newlines are the user's own line breaks and are
# kept verbatim (docs/LIVE-TEXT.md); the label never wraps itself
# kept verbatim (docs/DECOR-EDITOR.md §5); the label never wraps itself
vol.Required("text"): vol.All(str, vol.Length(min=1, max=MAX_DECOR_TEXT)),
# legacy font size ('s'|'m'|'l'). The dialog no longer offers it
# — the block is scaled by its corner handles — but a plan
@@ -1476,6 +1533,61 @@ DECOR_SCHEMA = vol.Any(
)
def _strict_stair(value: dict) -> dict:
"""A discriminated stair record carries dimensions for exactly one kind."""
if value["kind"] == "straight":
if "radius" in value:
raise vol.Invalid("radius is allowed only for spiral stairs")
if value["direction"] not in ("forward", "backward"):
raise vol.Invalid("straight stair direction must be forward or backward")
else:
if "length" in value or "width" in value:
raise vol.Invalid("length and width are allowed only for straight stairs")
if value["direction"] not in ("clockwise", "counterclockwise"):
raise vol.Invalid("spiral stair direction must be clockwise or counterclockwise")
return value
def _stair_dimensions(value: dict) -> dict:
"""Require the dimension fields owned by the selected stair kind."""
complete = (
value["kind"] == "straight" and "length" in value and "width" in value
or value["kind"] == "spiral" and "radius" in value
)
if not complete:
raise vol.Invalid("stair kind requires its dimensions")
return value
STAIR_SCHEMA = vol.All(
vol.Schema(
{
vol.Required("id"): vol.All(str, vol.Length(min=1, max=64)),
vol.Required("kind"): vol.In(["straight", "spiral"]),
vol.Required("x"): _NORM,
vol.Required("y"): _NORM,
vol.Required("angle"): vol.All(_finite, vol.Range(min=-360.0, max=360.0)),
vol.Required("direction"): vol.In([
"forward", "backward", "clockwise", "counterclockwise",
]),
vol.Optional("target_space_id"): vol.Any(
None, vol.All(str, vol.Match(SPACE_ID_RE.pattern))
),
vol.Optional("length"): _FURN_SIZE,
vol.Optional("width"): _FURN_SIZE,
vol.Optional("radius"): _FURN_SIZE,
vol.Optional("color"): _COLOR,
vol.Optional("opacity"): vol.All(_finite, vol.Range(min=0.0, max=1.0)),
vol.Optional("fill_color"): _COLOR,
vol.Optional("fill_opacity"): vol.All(_finite, vol.Range(min=0.0, max=1.0)),
},
extra=vol.ALLOW_EXTRA,
),
_stair_dimensions,
_strict_stair,
)
def _wall_endpoints_pair(entry: dict) -> dict:
"""Exact wall endpoints are useful only as a complete a/b pair."""
if ("a" in entry) != ("b" in entry):
@@ -1617,7 +1729,7 @@ OPENING_HOST_SCHEMA = vol.Any(PARTITION_OPENING_HOST_SCHEMA, WALL_OPENING_HOST_S
def _space_geometry_invariants(value: dict) -> dict:
"""All stored geometry shares ids; draft segments also have a space cap."""
seen: set[str] = set()
for key in ("rooms", "openings", "decor", "room_drafts", "partitions", "wall_columns", "wall_segments"):
for key in ("rooms", "openings", "decor", "stairs", "room_drafts", "partitions", "wall_columns", "wall_segments"):
for item in value.get(key, []):
item_id = item.get("id")
if not item_id:
@@ -1687,7 +1799,7 @@ SPACE_SCHEMA = vol.All(vol.Schema(
vol.Optional("plan_aspect"): vol.Any(
None, vol.All(vol.Coerce(float), vol.Range(min=0.05, max=20))
),
# Backdrop placement (docs/BACKDROP.md): the picture may be moved,
# Backdrop placement (docs/DECOR-EDITOR.md §3): the picture may be moved,
# resized per axis and rotated. Every transform field is optional; its
# complete absence is the pre-v1.58.0 behaviour exactly, and an old
# config validates unchanged. The offset is a normalised
@@ -1712,6 +1824,7 @@ SPACE_SCHEMA = vol.All(vol.Schema(
vol.Required("view_box"): _view_box,
vol.Required("rooms"): vol.All([ROOM_SCHEMA], vol.Length(max=MAX_ROOMS)),
vol.Optional("decor"): vol.All([DECOR_SCHEMA], vol.Length(max=MAX_DECOR)),
vol.Optional("stairs"): vol.All([STAIR_SCHEMA], vol.Length(max=MAX_STAIRS)),
vol.Optional("openings"): vol.All([
vol.Schema(
{
@@ -2198,6 +2311,10 @@ CONFIG_SCHEMA = vol.All(
vol.Optional("sun_rays"): bool,
vol.Optional("sun_ray_origin"): _SUN_RAY_ORIGIN,
vol.Optional("show_room_tooltip"): bool,
# #649: installation-wide 2.5D View; absent/false keeps Flat.
vol.Optional("volumetric_view"): bool,
# #661: the moon on the "Follow the Sun" background; absent/false = off.
vol.Optional("moon"): bool,
vol.Optional("summary_panel"): SUMMARY_PANEL_WIRE_SCHEMA,
vol.Optional("radar"): vol.Schema({}, extra=vol.ALLOW_EXTRA),
# Removed from the UI/runtime in 2026-08-08. Keep accepting the
+124 -12
View File
@@ -1,13 +1,20 @@
"""Persistent operational state for manual virtual lights."""
from __future__ import annotations
import asyncio
import copy
import logging
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from .store import HouseplanStore
EVENT_VIRTUAL_LIGHT_UPDATED = "houseplan_virtual_light_updated"
SAVE_DELAY_S = 0.5
_LOGGER = logging.getLogger(__name__)
def is_manual_virtual_light(marker: Any) -> bool:
@@ -57,6 +64,119 @@ def _wire(rev: int, config_rev: int, off: set[str]) -> dict[str, Any]:
return {"rev": rev, "config_rev": config_rev, "off": sorted(off)}
def _snapshot_payload(
stored: Any,
config: dict[str, Any],
config_rev: int,
*,
previous_config_rev: int | None = None,
) -> dict[str, Any]:
rev, state_config_rev, stored_off = _read_state(stored)
eligible = eligible_virtual_light_ids(config)
expected_rev = config_rev if previous_config_rev is None else previous_config_rev
off = stored_off & eligible if state_config_rev == expected_rev else set()
if off != stored_off:
rev += 1
return _wire(rev, config_rev, off)
class VirtualLightController:
"""Runtime cache with coalesced durable writes for rapid toggles."""
def __init__(self, hass: HomeAssistant, store: HouseplanStore) -> None:
self.hass = hass
self.store = store
self._state: dict[str, Any] | None = None
self._dirty = False
self._save_task: asyncio.Task[None] | None = None
self._flush_event = asyncio.Event()
self._flush_lock = asyncio.Lock()
async def async_snapshot(
self, config: dict[str, Any], config_rev: int,
) -> dict[str, Any]:
source = self._state
if source is None:
source = await self.store.async_load() or {}
payload = _snapshot_payload(source, config, config_rev)
self._state = payload
if payload != source:
self._dirty = True
self._schedule_save()
return copy.deepcopy(payload)
async def async_toggle(
self,
config: dict[str, Any],
config_rev: int,
marker_id: str,
) -> dict[str, Any] | None:
if marker_id not in eligible_virtual_light_ids(config):
return None
snapshot = await self.async_snapshot(config, config_rev)
off = set(snapshot["off"])
if marker_id in off:
off.remove(marker_id)
else:
off.add(marker_id)
payload = _wire(_integer(snapshot["rev"]) + 1, config_rev, off)
self._state = payload
self._dirty = True
self._schedule_save()
return {
"marker_id": marker_id,
"on": marker_id not in off,
"rev": payload["rev"],
}
def _schedule_save(self) -> None:
if self._save_task is None or self._save_task.done():
self._save_task = self.hass.async_create_task(self._delayed_save())
async def _delayed_save(self) -> None:
try:
while self._dirty:
if not self._flush_event.is_set():
try:
await asyncio.wait_for(
self._flush_event.wait(), timeout=SAVE_DELAY_S,
)
except TimeoutError:
pass
if not self._dirty or self._state is None:
continue
payload = copy.deepcopy(self._state)
self._dirty = False
try:
await self.store.async_save(payload)
except Exception: # noqa: BLE001 - next toggle/unload retries
self._dirty = True
_LOGGER.exception("House Plan: virtual-light delayed save failed")
return
finally:
self._save_task = None
if not self._dirty:
self._flush_event.clear()
async def async_flush(self) -> None:
"""Persist the latest state before config transitions or unload."""
async with self._flush_lock:
task = self._save_task
if task is not None:
self._flush_event.set()
await task
if self._dirty and self._state is not None:
payload = copy.deepcopy(self._state)
await self.store.async_save(payload)
self._dirty = False
if not self._dirty:
self._flush_event.clear()
def reset(self) -> None:
"""Forget cache after an external reconciliation wrote the store."""
self._state = None
async def async_virtual_light_snapshot(
store: HouseplanStore,
config: dict[str, Any],
@@ -70,12 +190,7 @@ async def async_virtual_light_snapshot(
old off state for a marker whose role changed in the meantime.
"""
stored = await store.async_load() or {}
rev, state_config_rev, stored_off = _read_state(stored)
eligible = eligible_virtual_light_ids(config)
off = stored_off & eligible if state_config_rev == config_rev else set()
if off != stored_off:
rev += 1
payload = _wire(rev, config_rev, off)
payload = _snapshot_payload(stored, config, config_rev)
if payload != stored:
await store.async_save(payload)
return payload
@@ -90,12 +205,9 @@ async def async_reconcile_virtual_lights(
) -> dict[str, Any]:
"""Carry eligible state across one known configuration transition."""
stored = await store.async_load() or {}
rev, state_config_rev, stored_off = _read_state(stored)
eligible = eligible_virtual_light_ids(config)
off = stored_off & eligible if state_config_rev == previous_config_rev else set()
if off != stored_off:
rev += 1
payload = _wire(rev, config_rev, off)
payload = _snapshot_payload(
stored, config, config_rev, previous_config_rev=previous_config_rev,
)
if payload != stored:
await store.async_save(payload)
return payload
@@ -439,7 +439,7 @@ def _thickness(space: dict[str, Any], atom: dict[str, Any], previous: dict[str,
def _non_catalog_ids(space: dict[str, Any]) -> set[str]:
result: set[str] = set()
for name in ("rooms", "openings", "decor", "room_drafts", "partitions", "wall_columns"):
for name in ("rooms", "openings", "decor", "stairs", "room_drafts", "partitions", "wall_columns"):
for item in space.get(name) or []:
if isinstance(item, dict) and isinstance(item.get("id"), str) and item["id"]:
result.add(item["id"])
@@ -661,7 +661,7 @@ def _migrate_room_drafts_to_partitions(space: dict[str, Any]) -> tuple[int, int]
return 0, 0
used = {
str(item["id"])
for name in ("rooms", "openings", "decor", "partitions",
for name in ("rooms", "openings", "decor", "stairs", "partitions",
"wall_columns", "wall_segments")
for item in space.get(name) or []
if isinstance(item, dict) and isinstance(item.get("id"), str) and item["id"]
+105 -72
View File
@@ -72,13 +72,13 @@ from .import_export import (
from .junction_limits import JunctionLimitError, validate_junction_limits
from .plans import (
QuotaError,
check_quota,
collect_attachments,
collect_plans,
is_plan_file,
plan_basename,
plan_refs,
reserve_filename,
store_plan_upload,
)
from .projection import project_config, project_layout
from .radar_validation import (
@@ -113,6 +113,7 @@ from .validation import (
MAX_PLAN_BYTES,
PLAN_EXTENSIONS,
POS_SCHEMA,
DuplicateMarkerIdError,
MarkerControlError,
OpeningPassageError,
PartitionOpeningHostError,
@@ -121,6 +122,7 @@ from .validation import (
prepare_ordinary_summary_candidate,
sanitize_filename,
valid_space_id,
validate_active_marker_ids,
validate_marker_controls,
validate_marker_light_entities,
validate_marker_vacuum_routes,
@@ -141,6 +143,19 @@ from .wall_segment_model import (
)
_LOGGER = logging.getLogger(__name__)
_MISSING_REV_DEBUGGED: set[str] = set()
def _debug_missing_revision_once(command: str, current_rev: int) -> None:
"""Log one low-noise diagnostic per legacy write command."""
if command in _MISSING_REV_DEBUGGED:
return
_MISSING_REV_DEBUGGED.add(command)
_LOGGER.debug(
"House Plan: %s without expected_rev over rev %s; write rejected",
command,
current_rev,
)
def _optimizer_backup_is_current(config_data: dict[str, Any], layout_data: dict[str, Any]) -> bool:
"""An optimization can be undone before any later ordinary plan edit."""
@@ -440,21 +455,23 @@ async def ws_export_create(hass: HomeAssistant, connection, msg: dict[str, Any])
return
try:
async with rt.write_lock:
config_data = await rt.config_store.async_load() or {}
layout_data = await rt.store.async_load() or {}
document, filename = await hass.async_add_executor_job(
partial(
create_export,
rt,
config_data,
layout_data,
kind=msg["kind"],
space_id=msg.get("space_id"),
plan_only=msg.get("plan_only", False),
card_version=msg.get("card_version", ""),
config_root=Path(hass.config.path("")),
)
# Copy one coherent pair while writers are excluded, then release
# the global lock before hashing assets and building the document.
config_data = copy.deepcopy(await rt.config_store.async_load() or {})
layout_data = copy.deepcopy(await rt.store.async_load() or {})
document, filename = await hass.async_add_executor_job(
partial(
create_export,
rt,
config_data,
layout_data,
kind=msg["kind"],
space_id=msg.get("space_id"),
plan_only=msg.get("plan_only", False),
card_version=msg.get("card_version", ""),
config_root=Path(hass.config.path("")),
)
)
except ImportFailure as err:
_send_import_error(connection, msg["id"], err)
return
@@ -575,8 +592,11 @@ async def ws_import_apply(hass: HomeAssistant, connection, msg: dict[str, Any])
"missing_plan",
"Plan file no longer exists: " + ", ".join(sorted(missing)),
)
missing_attachments = _missing_internal_attachments(
Path(hass.config.path("")), target_config, config_data.get("config")
missing_attachments = await hass.async_add_executor_job(
_missing_internal_attachments,
Path(hass.config.path("")),
target_config,
config_data.get("config"),
)
if missing_attachments:
raise ImportFailure(
@@ -751,11 +771,7 @@ async def ws_layout_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
# indistinguishable from a stale writer. Keep the schema field
# optional only for rev-zero bootstrap and return the stable domain
# conflict here rather than allowing canonical no-op to bypass CAS.
_LOGGER.warning(
"House Plan: layout/set without expected_rev over rev %s — "
"write rejected (outdated client?)",
current_rev,
)
_debug_missing_revision_once("layout/set", current_rev)
connection.send_error(
msg["id"], "conflict",
f"Layout revision is required; reload the layout, or — for "
@@ -808,10 +824,6 @@ async def ws_layout_update(hass: HomeAssistant, connection, msg: dict[str, Any])
config_data = resolved.config_data
config = config_data.get("config") or {}
markers = config.get("markers") or []
deleted = any(
str(m.get("id")) == msg["device_id"] and m.get("removed") is True
for m in markers
)
live_virtual = any(
str(m.get("id")) == msg["device_id"] and m.get("removed") is not True
and m.get("binding") == "virtual"
@@ -821,6 +833,10 @@ async def ws_layout_update(hass: HomeAssistant, connection, msg: dict[str, Any])
str(m.get("id")) == msg["device_id"] and m.get("removed") is not True
for m in markers
)
deleted = not live_explicit and any(
str(m.get("id")) == msg["device_id"] and m.get("removed") is True
for m in markers
)
orphan_virtual = (
msg["device_id"].startswith("v_")
and not live_virtual
@@ -1040,6 +1056,9 @@ async def ws_plans_list(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
works as a policy if the user can see them: detaching a plan keeps the
image, and this is how it gets picked up again — or deleted on purpose.
"""
if not _check_write(hass, connection):
connection.send_error(msg["id"], "unauthorized", "Only writers may list plans")
return
rt = _runtime(hass, connection, msg["id"])
if rt is None:
return
@@ -1429,10 +1448,10 @@ async def ws_config_get(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
config = {**DEFAULT_CONFIG, **data.get("config", {})}
config_rev = int(data.get("rev", 0))
try:
virtual_lights = await async_virtual_light_snapshot(
rt.virtual_light_store,
config,
config_rev,
virtual_lights = await (
rt.virtual_lights.async_snapshot(config, config_rev)
if rt.virtual_lights is not None
else async_virtual_light_snapshot(rt.virtual_light_store, config, config_rev)
)
except Exception: # noqa: BLE001 - config remains independently readable
_LOGGER.exception("House Plan: reading virtual-light state failed")
@@ -1482,11 +1501,17 @@ async def ws_virtual_light_toggle(
async with rt.write_lock:
data = await rt.config_store.async_load() or {}
config = {**DEFAULT_CONFIG, **data.get("config", {})}
result = await async_toggle_virtual_light(
rt.virtual_light_store,
config,
int(data.get("rev", 0)),
msg["marker_id"],
result = await (
rt.virtual_lights.async_toggle(
config, int(data.get("rev", 0)), msg["marker_id"]
)
if rt.virtual_lights is not None
else async_toggle_virtual_light(
rt.virtual_light_store,
config,
int(data.get("rev", 0)),
msg["marker_id"],
)
)
if result is None:
connection.send_error(
@@ -1495,8 +1520,8 @@ async def ws_virtual_light_toggle(
"Marker is not an active virtual light with tap_action=toggle",
)
return
# Both the reply and event follow the durable Store write. There is no
# optimistic client state, so all cards converge on this revision.
# The runtime revision is immediate; durable writes are coalesced and
# flushed before config transitions/unload.
connection.send_result(msg["id"], result)
hass.bus.async_fire(EVENT_VIRTUAL_LIGHT_UPDATED, result)
@@ -1627,11 +1652,7 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
# empty-store bootstrap and so this path can return the same stable
# domain error as an explicit stale revision. Accepting it over a
# saved document would bypass optimistic locking entirely.
_LOGGER.warning(
"House Plan: config/set without expected_rev over rev %s — "
"write rejected (outdated client?)",
current_rev,
)
_debug_missing_revision_once("config/set", current_rev)
connection.send_error(
msg["id"], "conflict",
f"Configuration revision is required; reload the configuration, "
@@ -1670,6 +1691,7 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
)
msg["config"].clear()
msg["config"].update(checked)
validate_active_marker_ids(msg["config"], data.get("config"))
validate_marker_controls(msg["config"], data.get("config"))
validate_marker_light_entities(msg["config"], data.get("config"))
validate_marker_radars(
@@ -1686,7 +1708,7 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
try:
candidate_counts = await hass.async_add_executor_job(_validate_config_cpu)
except (
JunctionLimitError, MarkerControlError, OpeningPassageError,
DuplicateMarkerIdError, JunctionLimitError, MarkerControlError, OpeningPassageError,
PartitionOpeningHostError, PartitionOpeningJambMarginError,
WallModelClientOutdatedError,
) as err:
@@ -1826,6 +1848,10 @@ def _space_delete_candidate(
item for item in candidate_config.get("spaces") or []
if item.get("id") != space_id
]
for remaining_space in candidate_config["spaces"]:
for stair in remaining_space.get("stairs") or []:
if isinstance(stair, dict) and stair.get("target_space_id") == space_id:
stair["target_space_id"] = None
for marker in candidate_config.get("markers") or []:
marker_id = str(marker.get("id")) if marker.get("id") is not None else None
marker_position = candidate_layout.get(marker_id) if marker_id is not None else None
@@ -1916,6 +1942,7 @@ async def ws_space_delete(hass: HomeAssistant, connection, msg: dict[str, Any])
)
return
target_config = CONFIG_SCHEMA(target_config)
validate_active_marker_ids(target_config, current_config)
target_layout = LAYOUT_SCHEMA(target_layout)
new_config_rev = config_rev + 1
new_layout_rev = layout_rev + 1
@@ -1952,6 +1979,9 @@ async def ws_space_delete(hass: HomeAssistant, connection, msg: dict[str, Any])
except ImportFailure as err:
_send_import_error(connection, msg["id"], err)
return
except DuplicateMarkerIdError as err:
connection.send_error(msg["id"], err.code, str(err))
return
except vol.Invalid as err:
connection.send_error(msg["id"], "invalid_config", str(err))
return
@@ -2058,6 +2088,7 @@ async def ws_plan_optimize(hass: HomeAssistant, connection, msg: dict[str, Any])
return None, migrated_size
msg["config"].clear()
msg["config"].update(checked)
validate_active_marker_ids(msg["config"], config_data.get("config"))
validate_marker_controls(msg["config"], config_data.get("config"))
validate_marker_light_entities(msg["config"], config_data.get("config"))
validate_marker_radars(
@@ -2093,7 +2124,7 @@ async def ws_plan_optimize(hass: HomeAssistant, connection, msg: dict[str, Any])
)
return
except (
JunctionLimitError, MarkerControlError, OpeningPassageError,
DuplicateMarkerIdError, JunctionLimitError, MarkerControlError, OpeningPassageError,
PartitionOpeningHostError, PartitionOpeningJambMarginError,
WallModelClientOutdatedError,
WallSegmentMigrationError,
@@ -2324,38 +2355,25 @@ async def ws_plan_set(hass: HomeAssistant, connection, msg: dict[str, Any]) -> N
connection.send_error(msg["id"], "too_large", f"Plan is larger than {MAX_PLAN_BYTES // 1024 // 1024} MB")
return
# Copy-on-write: a plan is written under a NEW unique name and nothing is
# deleted here (review R2-1). The old name stays readable, so a config write
# that is later rejected — revision conflict, validation, lost connection —
# leaves the stored plan exactly as it was. The card calls
# nothing here; the file a commit REPLACES is collected by `config/set`
# itself, inside the write lock (review R3-1). An upload that never gets
# committed is not collected at all — it is offered back in the space
# dialog's "already uploaded" list, where the user can attach or delete it.
# Every attempt to age these out ended in data loss or a race (v1.46.4-6).
#
# `.` separates the id from the token because a space id cannot contain one
# (SPACE_ID_RE), so "<space>.<token>.<ext>" can never be confused with the
# files of a differently named space.
# Kept for cards cached from before #617: they still upload over WS. A
# current card uses POST /api/houseplan/plans/upload instead, because the
# base64 frame of a plan above ~3 MiB exceeds the 4 MiB WebSocket message
# limit and the socket closes before this handler runs — the size check
# above is reachable only for small files on this path. Both transports
# write through the same `store_plan_upload` (copy-on-write naming, quota,
# atomic write), so the contract below cannot drift between them.
plans_dir = Path(hass.config.path(PLANS_DIR))
name = f"{space_id}.{secrets.token_hex(4)}.{msg['ext']}"
path = plans_dir / name
def _check_and_write() -> None:
# one executor job for the pair, under upload_lock: the measurement
# is only a bound if nothing else writes between it and our write
# (HP-1490-02). A failed write reserves nothing — the file either
# exists and is counted by the next scan, or does not and is not.
check_quota(plans_dir, len(raw), MAX_PLANS_BYTES, MAX_PLANS_FILES)
plans_dir.mkdir(parents=True, exist_ok=True)
path.write_bytes(raw)
data = _runtime(hass, connection, msg["id"])
if data is None:
return
async with data.upload_lock:
try:
await hass.async_add_executor_job(_check_and_write)
name = await hass.async_add_executor_job(
partial(
store_plan_upload, plans_dir, space_id, msg["ext"], raw,
max_bytes=MAX_PLANS_BYTES, max_files=MAX_PLANS_FILES,
)
)
except QuotaError as err:
connection.send_error(msg["id"], err.reason, err.detail)
return
@@ -2375,12 +2393,27 @@ async def _purge_trail_recorder(hass: HomeAssistant, config: dict[str, Any]) ->
return await rec.async_purge_orphans(config) if rec else 0
def _public_trails(value: Any) -> Any:
"""Copy the trail book for View without exposing source entity ids (#626)."""
if isinstance(value, dict):
return {
key: _public_trails(item)
for key, item in value.items()
if key != "source"
}
if isinstance(value, list):
return [_public_trails(item) for item in value]
return value
@websocket_api.websocket_command({vol.Required("type"): "houseplan/trail/get"})
@websocket_api.async_response
async def ws_trail_get(hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict) -> None:
"""Current + previous cleanup runs per marker, raw robot coordinates."""
"""Current + previous runs and raw coordinates, without source entity ids."""
rec = hass.data.get(DOMAIN, {}).get("trail_recorder")
connection.send_result(msg["id"], {"trails": rec.book.data if rec else {}})
connection.send_result(
msg["id"], {"trails": _public_trails(rec.book.data) if rec else {}}
)
@websocket_api.websocket_command(

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