Commit Graph
3006 Commits
Author SHA1 Message Date
Codex 8f5a08a609 Prepare v1.74.0-beta.2
Seven version sources move together to 1.74.0-beta.2; the changelog
sections in both languages close over what has landed since beta.1, the
release notes carry four bullets on each side with the grouped
small-fixes bullet last, and STATUS says what this beta is.

What the user gets: a plan left over from the era of rooms without walls
can be edited again — the leftover is cleaned up on the first save
instead of refusing every change, pointing at an Optimize that cannot
help and blocking the export too (#529, reported in #527); the plan is
smooth again in Firefox, where a marker shadow sized in container units
restarted a non-composited animation on all markers at once and cost 9.4
frames per second (#524); switching spaces no longer replays a
neighbouring door or marker (#525); and the card downloads 12.8 KB less
on a cold start, because the stylesheet minifier had never actually run
(#526).

Internal in the same line: the space-switch witness judges node identity
instead of a transition that #524 removed (#528), and the review gate
charges a round only for what the round changed (#518).

`npm test` 2520/0/1 skip, `pytest tests_backend` 467 passed, release
contract green on all seven sources, bundle 287 286 B gzip inside the
ceiling, docs strict green.

The `Release:` trailer is what asks CI for the heavy gates — smokes,
golden and the full performance comparison — on this exact SHA (#479).

Issue: #529
User-Visible: yes
Release: v1.74.0-beta.2
v1.74.0-beta.2
2026-09-11 11:49:16 +03:00
claude[bot] db39b620ba docs: review document for #529
Issue: #529
User-Visible: no
2026-09-11 08:40:14 +00:00
Codex 85e5d61fb1 docs: refresh the screenshot source fingerprint after the room-drafts fix
All 11 frames are pixel-identical (docs:accept --identical, #512).

Issue: #529
User-Visible: no
2026-09-11 11:19:59 +03:00
Codex 49912ac989 fix(plan): legacy room drafts are healed, not a locked door
A plan that still carried a `room_drafts` key while already on the
current wall model could not be edited at all. The card mirrors the same
migration, so a structural edit was refused before the request ever left
the browser; the toast sent the user to "Optimize plans", which reports
that everything is already optimal because it looks at something else
entirely; and the export path calls the same migration, so the one way
out — take a backup, fix the file by hand — was shut too. An empty
`room_drafts: []`, carrying no data at all, was enough to do it.

The carrier is now removed the way the first migration removes it: an
empty key silently, drafts converted one for one into partitions. The
#478 protection against a stale client re-adding the carrier moves to
the layer that can actually tell the two apart —
`validate_wall_model_transition` sees both the submission and the stored
plan, and refuses when the drafts appear over a plan that does not have
them. It no longer keys on the submitted model number: a stale card
echoes back the number it was given, which is exactly how the outdated
client slipped past this guard and met "conflicting wall identifiers"
instead of "update the card and reload the page". The schema invariant
keeps refusing a non-empty carrier as the last line.

Both mirrors change together and stay identical; the parity fixture is
untouched.

Issue: #529
User-Visible: yes
2026-09-11 11:19:04 +03:00
claude[bot] b736878c0b docs: review document for #529
Issue: #529
User-Visible: no
2026-09-11 08:13:43 +00:00
claude[bot] 2977177283 docs: review document for #529
Issue: #529
User-Visible: no
2026-09-11 08:06:11 +00:00
claude[bot] e408b6fb70 docs: review document for #526
Issue: #526
User-Visible: no
2026-09-11 07:32:32 +00:00
Codex bde05697d1 docs: refresh the screenshot source fingerprint after the minifier fix
All 11 frames are pixel-identical (docs:accept --identical, #512): the
minified stylesheets render the same, which is the point.

Issue: #526
User-Visible: no
2026-09-11 10:12:13 +03:00
Codex b9e40aebc2 fix(mutants): a multi-line anchor is built from escaped parts
The registry lives in a JavaScript file, so an anchor that contains a
newline has to be written with escapes; pasting the real line break
broke the module and --check reported nothing while exiting non-zero.

Issue: #526
User-Visible: no
2026-09-11 10:12:13 +03:00
Codex 0a0a9f0f4c perf(build): the stylesheet minifier had never run
It looked for the tag written as `css` immediately followed by a
backtick. The plugin is a Rollup transform, so the module has already
been through TypeScript by the time it arrives, and the TS printer puts
a space there: `css `. The guard therefore returned null for every
stylesheet in the project, and minification never ran once — around
23 KB of explanatory comments went to every user in every release.

Matching the tag as a word with optional whitespace turns it on:

  chunk, raw       1 079 508 -> 1 021 115 B   (-58 393)
  initial view     300 111 -> 287 284 B gzip  (-12 816)
  room to the budget   955 -> 13 782 B

The ceiling moves down with the fact, as the tool asks when a graph
shrinks past the band.

The risk is not the two lines; it is that 23 KB of CSS is minified for
the first time. Two witnesses cover it: a browser smoke that puts the
original and the minified text into separate stylesheets and compares
the serialised rules — 1 049 of them, identical up to the whitespace
policy the minifier declares — and a test that takes real comment text
out of src/styles and requires it to be absent from dist, so a plugin
that silently stops working cannot pass again.

Issue: #526
User-Visible: yes
2026-09-11 10:12:13 +03:00
claude[bot] 7e39c3715c docs: review document for #528
Issue: #528
User-Visible: no
2026-09-11 07:09:35 +00:00
Codex f37ccdcebc test: judge marker reuse by node identity, not by a transition
The marker half of the space-switch witness asked whether a box-shadow
transition was running on the shell. That worked only because such a
transition existed; #524 removed it — the shadow is sized in container
units and animating it cost a real user 9.4 frames per second — and the
check became trivially true. The mutant that removes the keys from the
marker list has been surviving ever since, and nobody noticed until the
next gate ran it.

The witness now keeps references to the marker nodes and requires that
none of them stays in the DOM under a different data-id after the
switch. Node identity is what the keys are for, and it does not depend
on any stylesheet.

The door half is untouched: there the transition is part of the product
contract, not a side effect.

Issue: #528
User-Visible: no
2026-09-11 10:01:46 +03:00
claude[bot] 78bc76073e docs: review document for #526
Issue: #526
User-Visible: no
2026-09-11 06:23:19 +00:00
claude[bot] 4451cf50d9 docs: review document for #526
Issue: #526
User-Visible: no
2026-09-11 06:18:52 +00:00
claude[bot] 9609040d31 docs: review document for #524
Issue: #524
User-Visible: no
2026-09-11 05:54:09 +00:00
Codex 9ad8b1f710 docs: refresh the screenshot source fingerprint after the CSS note trim
All 11 frames are pixel-identical (docs:accept --identical, #512).

Issue: #524
User-Visible: no
2026-09-11 08:34:23 +03:00
Codex b59e245e32 perf(markers): keep the CSS note short — comments ship to the browser
The long explanation inside the css`` template cost 518 gzipped bytes in
the initial chunk: the template minifier leaves comments in this file, so
every character of them is downloaded by every user. The reasoning now
lives in docs/DEVELOPMENT.md and in the commit above; the stylesheet
keeps one line and the issue number.

Measured: 300 577 B with the long notes, 300 111 B with the short ones,
300 059 B on dev before the fix.

Issue: #524
User-Visible: no
2026-09-11 08:33:18 +03:00
Codex cd47a59d3f docs: refresh the screenshot source fingerprint after the marker shadow fix
All 11 frames are pixel-identical (docs:accept --identical, #512); only
the source fingerprint moved with src/**.

Issue: #524
User-Visible: no
2026-09-11 08:29:57 +03:00
Codex 6b64801603 perf(markers): a marker's shadow no longer animates
The shadow of a device marker is sized from the marker, and the marker is
sized from the container: --device-shell-shadow is expressed in
--dev-size, which resolves to 2.5cqw. With box-shadow in the transition
list, every container-query re-evaluation — a tooltip, a scrollbar, a
rotation — produced a new computed value and restarted a 150 ms
non-composited transition on every marker at once.

The owner's Firefox profile shows what that costs: 244 box-shadow
transitions, all on span.device-shell-frame, all oncompositor:false, in
bursts of exactly 61 (the markers on screen), four bursts in two
seconds. During them the tab presented 103 frames in 11 seconds — 9.4
per second, CONTENT_FRAME_TIME median 149 ms and up to 320 — while the
refresh driver waited for paint 381 times. Our JavaScript in the worst
three seconds: 16 ms. Chromium starts the same transitions (measured:
one pixel of container width starts two per marker in both engines) and
merely pays less for them, which is why this hid there.

The shadow itself is unchanged; it simply applies at once. The core
keeps the same treatment, so the selection and focus rings appear
without a fade — an instant ring is ordinary feedback, a faded one costs
a full-frame repaint per marker. Hover still animates border-color.

Issue: #524
User-Visible: yes
2026-09-11 08:28:49 +03:00
claude[bot] f4625dd4fb docs: review document for #524
Issue: #524
User-Visible: no
2026-09-11 05:04:43 +00:00
claude[bot] 8ba87b3ff7 docs: review document for #525
Проверка (CI) / Классификация изменённых файлов (push) Successful in 1m47s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 2m51s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 1m23s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 1m44s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 2m14s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 4m45s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m4s
Проверка (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
Issue: #525
User-Visible: no
2026-09-11 02:00:05 +00:00
Claudeandclaude[bot] ab296302d1 test: register the space-switch witness for the two renderers (#525)
`smoke-select` answered НЕОПРЕДЕЛЁННОСТЬ on this diff: the smoke does not
name `_renderDevice` or `_renderOpenings` anywhere — it switches spaces the
ordinary way and reads running transitions off the nodes those renderers
produced. That is exactly the case `scripts/smoke-links.mjs` exists for, and
without the record the next edit to either renderer selects no witness at all.

Issue: #525
User-Visible: no
2026-09-11 01:42:46 +00:00
Claudeandclaude[bot] 8d5bade656 docs: refresh the screenshot source fingerprint (#525)
The fix touches `src/houseplan-card.ts` and `src/styles/plan.styles.ts`, and
the documentation fingerprint covers the whole `src/**` corpus.

Accepted with `npm run docs:accept -- --identical` (#512): all 11 frames were
re-captured locally and matched the committed ones byte for byte, so only the
fingerprint moves, `7a90ca04 → cd8e9598`. No PNG changes. Environment: Linux,
Chromium 152 — the canon platform of #455.

Issue: #525
User-Visible: no
2026-09-11 01:42:46 +00:00
Claudeandclaude[bot] 9edef041a9 fix: openings and markers keep their identity across a space switch (#525)
Lit reuses list nodes by position. The opening list and the device markers had
no keys, so on a space switch the leaf that held a slot kept its DOM node and
only changed values — and `.op-leaf` (transform) and `.op-arc`
(stroke-dashoffset) carry a 0.6 s transition, so the browser animated a door
that never moved: the new floor's leaf drove in from the previous floor's
opening angle. Measured on two spaces with a door in the same place and
opposite contact states: the node is reused, transform goes
`rotate(-90deg) → rotate(0deg)`, dash offset `0 → 125.66`, both transitions
`running`. The marker shell adds two more with its `box-shadow`.

Both lists are now rendered through `repeat(…, (item) => item.id, …)`, the
same lesson `glow-scene.ts` already learned for the Glow spots. The trap is
written where it starts — above the two transitions in `plan.styles.ts` —
because that is the file someone edits when adding the next animated property.
`houseplan-card.ts` is at its line ceiling, and the note would have cost the
budget a dozen lines for nothing: the swap itself is line-for-line.

The witness walks the shadow tree per element. `document.getAnimations()` is
empty here EVEN ON THE BROKEN CODE — the card lives in a shadow root and the
document-level call does not reach into it, and the issue proposed exactly
that call. The smoke also builds its own fixture: the demo home has no
openings at all, so two doors in two spaces are prepared in the smoke, and it
asserts the other half of the contract as well — a real contact change inside
one space still animates the leaf.

On `origin/dev` the smoke fails on five facts, naming the offenders:
`op-arc:stroke-dashoffset`, `op-leaf:transform`, `device-shell-frame:box-shadow`
twice. Mutants `openings-rendered-without-keys` and
`device-markers-rendered-without-keys` put each `map` back.

Perf, 7 samples against `19e421b3`: spaceSwitchMs 524.8 (limit 769.35, base
512.9), switchCycleMs 1293.9 (1696.28, 1256.5), firstStableRenderMs 2533.8
(3000, 2529.2), modelReadyMs 732.7 (944.97, 726.9), longTask.maxSingleMs 663
(910, 660) — `benchmark:compare` green in full.

The initial View graph grows 241 B gzip: `repeat` enters it for the first
time. The #438 ceiling is recentred 300 400 → 300 700 with the usual dated
note; measured 300 059 B keeps 641 B above and 1 359 B below the band. The
301 066 B budget is untouched, but only 366 B now separate the ceiling from
it — the #367 headroom debt has stopped being theoretical.

Issue: #525
User-Visible: yes
2026-09-11 01:42:46 +00:00
claude[bot] 95175a7616 docs: review document for #525
Issue: #525
User-Visible: no
2026-09-11 01:20:43 +00:00
Claude 19e421b3cf Refresh the v1.74.0-beta.1 candidate on the merged line
The candidate was assembled before #520 and #521 landed. Version fields are
already at 1.74.0-beta.1 in all seven sources and the bundles rebuild
byte-identical, so this commit only moves what the beta says about itself:
the #521 changelog entry leaves Unreleased for the beta.1 section in both
languages, the release notes gain its bullet on both sides (four and four,
the grouped small-fixes bullet stays last), and STATUS records the two
issues merged since — the alignment guides that follow the gesture again
(#521, a regression shipped in v1.72.0 and v1.73.0) and the cold-start
adoption fix behind it (#520, internal).

No product change: `npm run build` plus `bundle-sync` leave `dist/**` and
the two mirrored trees untouched, `node scripts/release-contract.mjs
v1.74.0-beta.1` is green on all seven version sources, `npm test` 2515/0.

The `Release:` trailer is what asks CI for the heavy gates — smokes, golden
and the full performance comparison — on this exact SHA (#479).

Issue: #521
Issue: #520
User-Visible: no
Release: v1.74.0-beta.1
v1.74.0-beta.1
2026-09-10 23:59:13 +03:00
claude[bot] 3f3583ac58 docs: review document for #521
Issue: #521
User-Visible: no
2026-09-10 20:48:53 +00:00
Claude 9a3af808d9 fix: hide the settled guides copy for the whole gesture, and prove it (#521)
Code review r1 was right that AC5's evidence was empty: the smoke never
forced a settled render during a gesture, so the settled copy of
`.hp-editor-only-layer` was empty at every point it looked, and
`groups() === 1` held whether the copy was hidden or not.

Measuring the case the reviewer named turned up more than a weak assertion.
Ownership of the layer alternates on its own — every settled render ends in
`updated()` → `_commitLiveEditor()`, which empties the live root — so a
settled render mid-gesture takes the guides back and draws them itself, from
the same live `_alignPoint`. That much needs no suppression. But the copy it
leaves behind stays in the settled scene, and the NEXT live paint adds a
second one: measured two `.alignline` on one alignment, the settled one a
grid step behind the marker. So the suppression stays, and now it stays with
a witness.

The smoke counts what is visible, not what is in the DOM: the hidden copy is
still a node, and counting nodes is how this check could have looked green
while showing the user two lines. Its device scenario now drives the whole
handover — force an unrelated settled render mid-drag (`_hdrH`, the same
header-height observer that masked the defect in the S2 measurements), assert
the render actually happened, that the layer went back to the settled scene
with the live point on it, and that one real move later the live painter owns
it again — exactly one visible guide at every step.

Mutant `live-editor-keeps-the-settled-guides-visible` puts the suppression
back under the plan branch, as it was before this issue, and the smoke goes
red on `nextMoveTakesTheLayerBack`.

Issue: #521
User-Visible: no
2026-09-10 23:25:46 +03:00
claude[bot] deebcfb095 docs: review document for #521
Issue: #521
User-Visible: no
2026-09-10 20:05:44 +00:00
Claude 0715321fdc docs: refresh the screenshot source fingerprint (#521)
The fix touches `src/live-editor.ts` and `src/houseplan-card.ts`, and the
documentation fingerprint covers the whole `src/**` corpus, so `check-docs`
called the committed screenshot index stale — the same step #520 needed.

Accepted with `npm run docs:accept -- --identical` (#512): all 11 frames were
re-captured locally and matched the committed ones byte for byte, so only the
fingerprint moves, `c615d580 → 7a90ca04`. No PNG changes, nothing here needs
the owner's visual acceptance. Environment: Linux, Chromium 152 — the canon
platform of #455.

Issue: #521
User-Visible: no
2026-09-10 22:24:27 +03:00
Claude 53585b451b fix: alignment guides follow the live gesture again (#521)
#451 moved every editor gesture onto the live painter, and the guides stayed
behind in the settled scene. While a gesture runs, the settled scene is not
re-rendered at all, so the guides did not follow the marker in the device
editor, the shape in the backdrop editor, or the cursor while a contour is
drawn in the plan editor. Measured with real pointer events on the demo stand
against `origin/dev`, after waiting for the editor chrome to settle: three
gestures, each exactly on another object's axis, 0 settled render cycles,
`.alignline` 0 and no `.alignguides` group in all three.

The report called it two breaks. It is one — the layer — plus one thing that
would have broken the repair: `_alignPoint` read `_pos`, which during a live
gesture answers from the snapshot of the last settled render. Over one drag:
live 254.17 → 220.83 while `_pos` stayed at 254.17, eight grid steps behind,
so a restored layer would have drawn the guide at the marker's old place.

The live template now paints the guides in all three modes (the device editor
had no template at all — `paintDevice` only moves the marker element), and
`_alignPoint` takes the live position. The settled copy of
`.hp-editor-only-layer` is made transparent for the duration of any editor
gesture, not only in plan mode: two guides, one of them stale, is what the
user would otherwise see when an unrelated settled render lands mid-gesture.
`_renderAlignGuides` on the card becomes soft — a gesture that starts while
the editor runtime is still loading must cost nothing, and an exception inside
a `requestAnimationFrame` paint would take the whole gesture with it.

The witness is rewritten around the defect that hid this for two stable
releases: the old smoke assigned `_deviceDrag`/`_decorDraft` wholesale, and an
assignment with `oldValue == null` does not route to the live path — it
verified a state a real gesture never reaches. Every scenario now drives real
`PointerEvent`s, waits for silence first (the `_hdrH` settling window right
after entering a mode hands out settled frames that make even the broken code
draw a guide), and asserts zero settled cycles during the movements plus
exactly one `.alignguides` group. #400's exclusion is checked without touching
the drag state: the dragged marker must simply be absent from the candidates.
On `origin/dev` the smoke fails on nine of its facts; a witness that stays
green before the fix was the actual bug here.

Mutants: `live-editor-devices-drops-align-guides`,
`live-editor-decor-drops-align-guides`, `live-editor-plan-drops-align-guides`,
`align-point-reads-frozen-snapshot` — one per AC, all guarded by the smoke.
`test/smoke-harness-contract.test.mjs` pins that the smoke cannot go back to
fabricating gesture state.

Issue: #521
User-Visible: yes
2026-09-10 22:20:18 +03:00
claude[bot] fa7ac02c0f docs: review document for #521
Issue: #521
User-Visible: no
2026-09-10 19:05:32 +00:00
claude[bot] 548e7a8563 docs: review document for #521
Issue: #521
User-Visible: no
2026-09-10 18:57:55 +00:00
claude[bot] 9592092b8e docs: review document for #521
Issue: #521
User-Visible: no
2026-09-10 18:49:42 +00:00
claude[bot] c50bc725c8 docs: review document for #522
Issue: #522
User-Visible: no
2026-09-10 18:40:54 +00:00
claude[bot] b2346eaac3 docs: review document for #520
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Successful in 2m16s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 1m55s
Проверка (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) Successful in 52s
Проверка (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) Skipped
Issue: #520
User-Visible: no
2026-09-10 18:26:40 +00:00
Claude a325294d7a docs: refresh the screenshot source fingerprint (#520)
The fix touches `src/houseplan-card.ts` and `src/config-adoption.ts`, and the
documentation fingerprint is computed over the whole `src/**` corpus, so
`check-docs` called the committed screenshot index stale — the Medium of code
review r2.

Accepted through `npm run docs:accept -- --identical` (#512): all 11 frames
were re-captured locally and compared byte-for-byte with the committed ones —
«Все 11 кадров попиксельно совпали с закоммиченными». Only the fingerprint
moves, `ccbbfb4e → c615d580`; not a single PNG changes, so nothing here needs
the owner's visual acceptance. Environment: Linux, Chromium 152 (the canon
platform of #455; capture refuses elsewhere).

`node scripts/check-docs.mjs` — «Documentation checks passed (7 files, 12
external links)».

Issue: #520
User-Visible: no
2026-09-10 21:15:42 +03:00
claude[bot] 3066fec17e docs: review document for #520
Issue: #520
User-Visible: no
2026-09-10 17:47:45 +00:00
Claude 462b56453c fix: the authoritative adoption keeps its own task (#520)
The r1 diagnosis was wrong, and the measurement in the code review proved
it: removing the two declarations from `static properties` left the cold
start at 19 update cycles, 4 model builds and 4 config epochs, exactly the
numbers of the bug. Lit's forced first-update change does mark `_serverCfg`
changed, but at that moment the body and `_cfgEpochPreservedConfig` are both
null, `preserveGeometry` is true and the epoch does not move. The comment
above `static properties` now says that; the declaration still stays out,
because two owners of one reactivity is what #500 removed.

The real cause is the `await`. Before #500 everything from
`_adoptStructuralResponses` to the end of the load ran in one task: the
adopted bodies, `_adoptInitialSpace`, the viewport restore, `_loadOk`, and
the device seeding — whose `_syncNewDevices`/`_seedHiddenDevices` write the
config back — all landed in a single Lit update. #500 made the adoption an
async sequence, so the caller resumes one microtask later, after Lit has
already painted the adopted config; the seeding writes then arrive as a
second config epoch, a second model build and a second paint of a 60-room
house.

`GatedAdoptionInput` gains `afterAdopt`, the mirror of `beforeAdopt`: it
runs synchronously at the end of the sequence, before the promise resolves.
`_loadFromServer` moves the viewport restore, `_loadOk` and the device
rebuild into it — `_syncNewDevices` refuses to write before `_loadOk`, so
the order inside the hook matters — and the load tail now rebuilds devices
only when nothing was adopted. `_reloadConfigOnly` takes the same route.

Measured with the project's own runner, 7 samples per profile, base
`a44fbd37` against this tree (Chromium 152, sandbox):

  interaction  modelReadyMs 761.3 ≤ 950.56 (base 731.2)
               firstStableRenderMs 2567.2 ≤ 3000 (base 2542.7)
               longTask.maxSingleMs 690 ≤ 921 · cache.entries.cleanFloor 100
  isometric    modelReadyMs 1252.9 ≤ 1499.76 (base 1249.8)
               firstStableRenderMs 1378 ≤ 1610.16 (base 1341.8)

Boot diagnostics on both trees: 18 update cycles, 3 model builds, 3 config
epochs, with the same epoch trace — the candidate is no longer
distinguishable from the base.

Witnesses. `config-adoption.test.mjs` queues a microtask at the start of
the adoption and pins that `afterAdopt` runs before it — the probe fails the
moment the hook crosses an await; `config-adoption-ownership.test.mjs` pins
the wiring in the card and the hook's place in the sequence. Mutants
`adoption-tail-defers-caller-hook` (defers the hook by one microtask) and
`authoritative-load-seeds-devices-after-the-await` (drops the rebuild from
the hook) redden them.

The initial View graph grows 40 B gzip, so the #438 ceiling is recentred
300 300 → 300 400 with the usual dated note; measured 299 812 B keeps 588 B
above and 1 412 B below the band. The overall 301 066 B budget and the #367
headroom debt are untouched.

Issue: #520
User-Visible: no
2026-09-10 20:12:13 +03:00
claude[bot] 49a02637ca docs: review document for #520
Issue: #520
User-Visible: no
2026-09-10 16:27:39 +00:00
Claude a625871538 test: a mutant for the only surviving wake-up of a replaced config body (#520)
With the bodies no longer declared as Lit properties, `onBodyReplaced` is the
single path that turns a replaced config reference into an update — AC2 of
this issue rests on it, and until now only a hand-run mutation stood behind
that column. The mutant drops the notification from `setConfig`; the existing
unit in `test/config-adoption.test.mjs` reddens on it.

Verified: `node scripts/mutation-gate.mjs --id=adoption-notifies-no-host-on-config-replacement`
— «поймано 1 из 1».

Issue: #520
User-Visible: no
2026-09-10 19:03:21 +03:00
Claude e181b08f2b fix: the adoption bodies are no longer declared Lit properties (#520)
#500 gave `_serverCfg` and `_layout` prototype accessors but left them in
`static properties`. Lit marks such a property `wrapped` and, on the FIRST
update, force-writes it into `changedProperties` with an `undefined` old
value even though nobody assigned anything (`reactive-element.js:249-252`
and `:880-886`). `willUpdate` reads that as a config replacement, raises
`_cfgEpoch`, the memoized model key changes, and a 60-room house builds and
paints its model a second time: measured 19 update cycles, 4 builds and 4
epochs against 18 / 3 / 3 before #500, worth ~550 ms of `modelReadyMs` and
the same on `firstStableRenderMs` (3355 against a 3000 ceiling).

The declaration goes; the bodies stay reactive through the owner —
`_adoption` → `onBodyReplaced` → `requestUpdate(field, previous)` — which
needs no declaration: `getPropertyOptions` falls back to the default and
`changed.has('_serverCfg')` works as before. `noAccessor: true` would not
help, `wrapped` is set before that flag is read. The trap is written above
`static properties`, where someone would put the declaration back.

`cache.entries.cleanFloor` returns to 100 in both interaction budgets: the
120 entries were the extra epoch re-keying the per-room cache, not a
property of the design — the reasoning in 914e8402 was wrong.

Witness: test/config-adoption-ownership.test.mjs pins that neither body is
declared; the mutant `adoption-bodies-declared-reactive` puts the
declaration back and reddens it.

The boot diagnostics of the previous three commits touch four private
members, so they are declared in the performance contract: `_buildModel` and
`_cfgEpoch` outright (both exist in every supported comparison base), and the
adoption entry point as a current/legacy pair — #500 turned the private
`_adoptStructuralResponses` into the public `_adoptAuthoritative`, and an
undeclared rename would have the counter report zero adoptions instead of
failing.

The same commits carried a `node_modules` symlink: `.gitignore` had the
pattern with a trailing slash, which does not cover a symbolic link, and
`git add -A` in a sandbox worktree committed it. The link is removed and the
pattern loses the slash; a mutant run on this branch failed with `EEXIST` on
it.

Issue: #520
User-Visible: no
2026-09-10 18:58:24 +03:00
claude[bot] 50fa7f9be2 docs: review document for #520
Issue: #520
User-Visible: no
2026-09-10 15:38:40 +00:00
claude[bot] b49721a0bd docs: review document for #520
Issue: #520
User-Visible: no
2026-09-10 15:30:21 +00:00
claude[bot] ad2f9afd2b docs: review document for #520
Issue: #520
User-Visible: no
2026-09-10 15:20:07 +00:00
Claude 27ea23c1ec test(perf): trace every config-epoch bump with the frame that made it (#520)
The comparison already names the mechanism: base does 18 update cycles, 3
model builds and ends at epoch 3, the candidate does 19, 4 and epoch 4, and
the extra build is the whole ~550 ms. What is still missing is the caller.
The diagnostic now installs an instance-level setter over `_cfgEpoch` and
records `from->to` with the top stack frames, so the extra bump names itself.

Issue: #520
User-Visible: no
2026-09-10 17:57:27 +03:00
Claude 3d43980568 test(perf): the boot diagnostics travel out of the page into the run log (#520)
The first attempt printed them with console.log inside page.evaluate, and
nothing forwards the page console to Node — the numbers went nowhere. The
sample now returns `bootDiag`, the runner prints it and strips it before the
row is recorded, so the budgeted record keeps its shape.

Issue: #520
User-Visible: no
2026-09-10 17:47:18 +03:00
Claude 37ef970dfc test(perf): count update cycles, model builds and adoptions during boot (#520)
The full comparison says model readiness grew by ~500 ms inside #500 and
that the growth sits in one long task, but neither contentFingerprint
(2.8 ms on this fixture) nor spaceModels (0.1 ms) can account for it. The
benchmark now prints, per sample, how many Lit update cycles ran before the
first stable frame, how long they took together, how many models were built,
how many adoptions happened and the config epoch. Diagnostics only: printed
to the log, never part of the budgeted record, and the same harness runs
against the comparison bundle, so candidate and base are counted alike.

Issue: #520
User-Visible: no
2026-09-10 17:38:36 +03:00
Claude 914e84028d perf: the clean-floor cache holds one entry per room per config epoch (#509)
Проверка (CI) / Классификация изменённых файлов (push) Successful in 52s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 1m32s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 43s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 41s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m11s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Failing after 4m28s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Failing after 4m36s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Failing after 4m43s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Failing after 4m38s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Failing after 3m58s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Failing after 4m7s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 5m49s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m2s
Проверка (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
The pre-release perf gate of the v1.74.0-beta.1 candidate reported
cache.entries.cleanFloor 120 against a ceiling of 100 (run 34480302982,
large-house-interaction-v1). The ceiling was calibrated when only the visible
space populated `_cleanFloorCache`; since #509 the summary panel computes the
clean-floor total in per-room slices through the same cache, so the sixty
fixture rooms are cached under both config epochs the interaction profile
creates — 60 × 2 = 120, exactly what the run measured.

The ceiling moves to 180 in the smoke and in the full interaction profile:
one entry per room per epoch with room for a third epoch, far below the LRU
cap of 600 and far below anything a per-frame or per-marker regression would
produce. The leak detector is untouched: cacheGrowth.cleanFloor stays 0.

Issue: #509
User-Visible: no
Release: v1.74.0-beta.1
2026-09-10 16:29:31 +03:00
Claude d5575f6393 Prepare v1.74.0-beta.1
Version fields and the generated bundles move to 1.74.0-beta.1 and open the
line after the stable v1.73.0. The changelog entries of #509 and #508 leave
Unreleased for the beta.1 section; release notes and STATUS describe them and
name the internal work of this beta. No product change beyond what is already
reviewed and merged.

Issue: #509
Issue: #508
User-Visible: no
Release: v1.74.0-beta.1
2026-09-10 16:01:59 +03:00