Добавлены безопасные pinned entrypoints, вывод фактических путей, идемпотентный Windows setup и WSL verification с настоящим HA subset и Linux capture.
Issue: #557
User-Visible: no
`release-zip.yml` выкладывал `houseplan.zip` в ту же секунду, когда релиз
становился публичным — до Validate, Full Performance и E2E; `release.yml`
параллельно пересобирал `houseplan-card.js`, а E2E требовал публичного ZIP,
чтобы вообще начаться. Публикаторов было четыре, порядок — ни одного.
Теперь публикатор стабильных один — `release.yml`: закрепить SHA → релиз в
черновике (опубликованный руками немедленно возвращается в черновик) → гейты
на SHA (трейлер `Release: <tag>`, контракт `--stable`, Validate, Full
Performance, E2E на коммите-кандидате через tarball codeload) → одна сборка,
`git archive` ZIP из того же дерева, `SHA256SUMS` → загрузка в черновик →
публикация → скачать публичное и сверить с паспортом → анонс. Dispatch на
публичный тег — ремонт: догружается только недостающее, расходящийся хеш —
отказ. Беты кладут тот же паспорт; локальный публикатор больше не ждёт
републикаторов — их нет.
- `.github/workflows/release-zip.yml` удалён
- `scripts/release-assets.mjs` — паспорт ассетов (`sums`/`check`), чистые
функции под юнитами
- `scripts/e2e-gate.mjs --ref=<sha>` — под тестом кандидат, `--tag` только
для выбора `upgrade_from`
- `scripts/release-contract.mjs --stable`
- мутанты: независимый публикатор, снятая зависимость от гейта, релиз без
возврата в черновик, `--clobber` в ремонте, E2E на теге, слепой паспорт
Issue: #540
User-Visible: no
Сводная панель теперь использует канонический порядок карточек Home Assistant и не переносит локальные настройки между визуальными колонками после remount.
Issue: #561
User-Visible: yes
`release-contract.test.mjs` требовал в анонсе условие про
`github.event_name == 'release'` — оно и было единственным местом, где путь
события закреплялся. Раз анонс вызывается только после выкладки, требование
перевёрнуто: ветки события в файле быть не должно.
Issue: #538
User-Visible: no
Мутант, снимающий `needs: build` с анонса, выживал: проверка искала подстроку
`needs: build` в блоке задания, а ровно эта строка процитирована двумя
строками выше — в комментарии, объясняющем, зачем зависимость нужна. Проверка
зеленела на собственном объяснении.
Issue: #538
User-Visible: no
Три воркфлоу висели на одном событии `release: published` и бежали
параллельно. Анонс выигрывал эту гонку всегда: проверять ему нечего. 12.09
стабильную v1.75.0 объявили в канале в ту же минуту, когда гейт отказал —
Full Performance был красный (#537), E2E после него не выполнялся вовсе,
ассеты не выкладывались. Подписчики получили сообщение о релизе, страница
которого осталась без `houseplan-card.js`.
Триггер события снят: у анонса остаются кнопка проверки связи и вызов из
воркфлоу. `release.yml` зовёт его после джобы выкладки (`needs: build`), то
есть красный гейт или несостоявшаяся выкладка сообщения не рождают. Путь беты
не тронут — `publish-prerelease.yml` звал анонс сам и раньше.
Мёртвая ветка чтения события из шага убрана вместе с триггером: тело берётся
из заметок ветки тега, как в вызове из беты.
Issue: #538
User-Visible: no
Мутант, выкидывающий сравнение ответа REST с новой вершиной, выживал: проверка
смотрела на вызов `gh api`, токен, порядок шагов и текст отказа — всё это
мутант оставляет на месте, а цикл после него выходит на первой же итерации, и
ожидание становится декорацией.
Issue: #539
User-Visible: no
`workflow_dispatch` в API принимает только ref: SHA туда передать нельзя, имя
ветки резолвится на стороне GitHub в момент запуска. Конвейер перед этим сам
переписывает ветку ребейзом — и 12.09 на #536 диспатч, отправленный через три
секунды после force-push, встал на ДОпушевый SHA. Гейт искал прогон строго на
SHA материала, не нашёл и вернул задачу автору со словами «материал сменился».
Чинить было нечего: дерево задачи не менялось ни на байт, материал сдвинул сам
конвейер.
Две меры, у каждой своя роль.
Шаг ребейза не заканчивается, пока REST не отдаст новую вершину — именно REST,
потому что через него же идёт диспатч. Минута ожидания, после чего отказ, а не
молчание: диспатч на устаревший SHA стоит трёх минут гейта и потерянного
захода.
Гейт, не дождавшись прогона на материале и увидев на ветке диспатч на другом
SHA, сначала пробует запустить ещё раз. Своя гонка этим закрывается, чужой
коммит переживает и вторую попытку, а формулировка отказа больше не называет
сменой материала то, что ею не является.
Issue: #539
User-Visible: no
The performance harness runs the candidate's benchmark against a
baseline checkout, so the candidate's validator reads a manifest built
by an older commit. #535 put a rule about the CURRENT build into that
shared validator — the panel graph must not contain the card facade —
and it is false of every build before #535 by construction. The
candidate then refused to load any older baseline: all nine performance
profiles went red at once on the same step, the stable release gate
withheld `houseplan-card.js` from the published v1.75.0, and none of it
was about speed.
assertBundleManifest now answers only the loader's question: paths
exist, nothing is duplicated, graphs reference listed assets, sizes add
up. The topology of the current build moves to assertOwnBundleTopology,
called from bundle-sync.mjs, which materializes our own dist, and from
the unit test that reads dist/houseplan-assets.json. Neither ever looks
at a foreign tree.
Reproduced end to end, not only in a unit: a v1.74.0 worktree built with
its own code, then `node demo/benchmark_large_house.mjs
--target-root=<baseline>` from this tree. Before the change it stops
with «initial panel graph must contain its own stable entry only»; after
it, the profile is captured.
Issue: #537
User-Visible: no
Promotion-only on top of v1.75.0-beta.1: seven version sources, the
generated bundle snapshots and the release metadata. No product source
code moves in this commit.
The line aggregates from the stable v1.74.0 and carries one user-visible
fix. The House Plan sidebar page could serve a previous version of the
card for hours: the panel entry fetched it through `./houseplan-card.js`,
a relative specifier, and relative resolution does not inherit the `?v=`
a dashboard gets from its Lovelace resource, while the entry files carry
no Cache-Control at all. The panel now imports the implementation by its
content-hashed name, so either the matching card arrives or the panel
says out loud that the page is stale (#535). Internal in the line: #536.
Known contradiction, recorded rather than silenced: the release contract
in scripts/release-contract.mjs requires the grouped "small fixes"
bullet unconditionally, while `npm run release:notes -- v1.75.0
--verify` rejects it because every user-visible issue of the range is
already itemised. The two rules deadlock any stable with a single
user-visible issue. The bullet stays, because the contract is the
automated gate that Validate enforces; the verifier's objection is
written down in STATUS and will get its own issue.
npm test 2532/2531/0 fail, release contract green on all seven sources,
docs strict green.
Issue: #535
User-Visible: yes
Release: v1.75.0
Seven version sources move together to 1.75.0-beta.1, both changelogs
close their section over what has landed since the stable v1.74.0, the
release notes carry the one shipped bullet on each side with the grouped
small-fixes bullet last, and STATUS says what this beta is.
What the user gets: the House Plan sidebar page can no longer serve a
previous version of the card. The panel entry fetched it through
`./houseplan-card.js` — a relative specifier, and relative resolution
does not inherit the `?v=` a dashboard gets from its Lovelace resource,
while the entry files carry no Cache-Control at all. A browser was free
to answer from its own heuristic cache for hours, and a stale loader
named a stale immutable chunk, so the panel ran an old card against the
current backend with nothing but the version banner to show for it —
and reloading could not help, because the address never changed (#535).
Internal in the line: #536 — the version banner now asks the host to
repaint when it drops the notice on disconnect, instead of leaving its
removal to whatever unrelated update happened to run next.
npm test 2532/2530/0 fail (the #349 manifest-tracking check goes green
with this commit), release contract green on all seven sources, bundle
287 323 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: #535
User-Visible: yes
Release: v1.75.0-beta.1
The controller dropped its banner on disconnect without asking the host
to repaint. Lit renders neither on disconnect nor on reconnect, so the
markup produced before the detach outlived it: the notice stayed on
screen while the controller no longer owned one, and it left only when
some unrelated update happened to run. Correctness rested on a
coincidence.
Measured on the built module with a counter: the sequence mismatch ->
disconnect -> versions agree -> connect asked for exactly one repaint,
the one that showed the notice. It now asks for two, and the second is
the one that takes the notice away.
Nothing else about the teardown changes. The field is still cleared
because a detached element cannot deliver animationend, reconnect still
rebuilds the notice from the retained input, and a disconnect with no
notice still asks for nothing.
Issue: #536
User-Visible: no
The sidebar page could serve a previous card for hours. It imported the
card through the stable facade, `./houseplan-card.js` — a relative
specifier, and relative resolution does not inherit a query. A dashboard
reaches the same file as `houseplan-card.js?v=1.74.0`, so an upgrade
changes its URL and the browser must refetch. The panel always asked for
the same address, and entries are served with no Cache-Control at all —
only ETag and Last-Modified — so the browser applies heuristic freshness
and may answer from cache without asking. A stale 1164-byte loader names
a stale chunk, chunks are immutable for a year, and the panel then ran a
previous card against the current backend without a single error. The
version banner was telling the truth; reloading could not help, because
the address never changed.
Rollup already emits the right edge: the panel's side-effect import
points straight at the shared implementation. The rewrite in
entryFallbackPlugin replaced it with the facade for a fallback that the
hashed name gives anyway — and better: a chunk the manifest no longer
serves now raises the panel's own "House Plan was updated" notice
instead of silently working on old code.
Two #486 assertions change meaning and are rewritten, not adjusted: the
panel no longer routes through the facade, and its initial graph no
longer contains it. The invariant they defended — the panel reuses the
exact card graph, never a second copy — is now stated over the
implementation, and a new test pins that no built entry reaches the card
by an address without a version.
Issue: #535
User-Visible: yes
Обе задачи вошли в стабильный тег кодом, но их строки чейнджлога остались под
«Не выпущено»: #532 и #534 закрывались в тот же день, что и сам выпуск, и
секция версии к тому моменту уже была закрыта. Тело релиза при этом не называло
ни ту, ни другую — обе молча попали под «мелкие исправления и улучшения», хотя
#532 это как раз та правка, ради которой владелец и присылал три профиля
Firefox.
Строки перенесены в секцию v1.74.0, тело релиза переписано: первый пункт теперь
называет все четыре правки скорости (#532, #531, #524, #534). Проверка правил
#328 зелёная на диапазоне v1.73.0..v1.74.0.
Тег не двигается: он указывает на протестированное дерево, а тело релиза на
GitHub обновляется отдельно — именно его читает человек.
Issue: #534
User-Visible: no
К2 задачи #532 назвала это заранее: промоушен поверхности контура в свой
композиционный слой делит SVG надвое, оставшееся содержимое ложится на другую
субпиксельную сетку, и диагональная штриховка стен сглаживается иначе. Кадры
полагалось пересмотреть на кандидате — они дожили до кандидата стабильного,
потому что golden-джоба гоняется только на дереве с трейлером `Release:`, а
мерж #532 такого трейлера не нёс.
Проверено по критериям AC3 самой #532: разошлись **ровно четыре** кадра
`day-cycle-{dawn,day,dusk,night}-dark`, средний цвет кадра сдвинулся на
0,001–0,080 из 255 при пороге 0,1, средний знаковый сдвиг на разошедшихся
пикселях от −0,19 до +0,87. Кадры просмотрены глазами: отличается только
сглаживание штриховки стен, палитра, геометрия, подписи и ореол те же.
Приняты из артефакта полного прогона Linux CI на этом же дереве, остальные
165 сцен сохранены без изменений, свидетелей среды 129.
Issue: #532
User-Visible: no
Release: v1.74.0
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34641155082
Догоняем main после #534: смена пространства больше не сверяет двести
маркеров поэлементно, чтобы затем всё выбросить.
Issue: #534
User-Visible: no
Release: v1.74.0
#525 увёл оба списка сцены с `map()` на `repeat(items, (item) => item.id, …)`,
чтобы Lit не переиспользовал узлы по позиции и не проигрывал анимацию двери,
которой не было. Правка верная, но на плане с двумя сотнями маркеров она
оказалась дорогой ровно там, где пользы не приносит: при смене пространства
ключи не пересекаются вовсе, и `repeat` строит две карты ключей и обходит оба
списка, чтобы затем всё равно выбросить всё и создать заново.
Бисект по медиане `switchCycleMs` на `large-house`: 871,2 перед #525 → 953,5
после. На раннере эти 80 мс распадаются на шесть-девять дополнительных длинных
задач, и `longTask.countP95` вышел за порог стабильного гейта.
Теперь оба списка рендерятся как `keyed(space.id, repeat(…))`. Внешний ключ
делает смену пространства: поддерево выбрасывается целиком, дифа нет. Внутренний
остаётся, потому что состав списков едет и внутри пространства — у маркеров от
призраков редактора и живого синка, у проёмов от записи с нерешённым хостом,
которая живёт только в режиме plan, — а переходы на `.device-shell-frame`,
`.op-leaf` и `.op-arc` никуда не делись.
Замер после правки: `switchCycleMs` 889,1 против 936,0 на `main` и 871,2 до
#525. Потолок карточки поднят на одну строку — на `import { keyed }`;
переносить нечего, сам рендер не вырос ни на символ.
Issue: #534
User-Visible: yes
Слияние протестированного dev (7a81d41b) в main. Собственные CI-коммиты main
сохраняются: у main свой validate.yml, зеркалятся только process.yml и
mutation-gate.yml.
Issue: #533
User-Visible: no
Release: v1.74.0
Code review r1 found two documentation claims that the task's own contract
contradicts.
Both changelogs promised the picture was unchanged "to the pixel". It is
not: splitting the layer moves the wall hatch by a sub-pixel, and that is
exactly why four day-cycle baselines are re-taken. The entry now says what
a reader can check — the outline around the plan matches pixel for pixel,
the hatch inside the walls is anti-aliased a shade differently.
docs/SUN.md credited Firefox with the fifteen-fold ratio. That number comes
from headless Chromium through CDP tracing; the Firefox profile that opened
the issue measured the same cause differently — 23 MB of texture uploads per
frame and about nine frames per second. Each number now names its engine.
Issue: #532
User-Visible: no
The "Follows the sun" background made the plan crawl in Firefox: 23 MB of
textures per frame, sixteen of about twenty picture-cache tiles thrown
away every frame, nine frames per second. The card's own JavaScript was
idle for 89 % of that.
The cost is the outer outline. It is a triple drop-shadow over the
grouped paper footprint, and although the group holds paper silhouettes
only and never changes on hover or pan, the filter lived in the plan's
own layer — so every repaint of the plan re-ran three blur passes over
the whole sheet. One hint moves the filtered paper into its own layer
and unhooks it from the plan's repaints.
Measured on a demo-stand pan (Chromium, CDP, summed RasterTask): 1456.6
ms against a static background's 96.7 ms before, 84.7 ms against 103.2
ms after. The new smoke measures that ratio and fails above two.
The picture does not change: the outline outside the plan matches byte
for byte and the frame's mean colour moves from 176.59 to 176.66 of 255.
Splitting the layer does move the remaining content onto a different
sub-pixel grid, so the diagonal wall hatch anti-aliases differently and
the four day-cycle baselines are re-taken on the beta candidate.
Issue: #532
User-Visible: yes
Первый кандидат держал не дефект продукта, а мигающий свидетель ресайза: он
фиксировал экранные координаты заранее, а карточка переводит их обратно в момент
события. Свидетель починен (#533), содержание релиза не изменилось.
Трейлер `Release:` просит CI прогнать тяжёлые гейты — смоки, golden и полное
сравнение производительности — на этом SHA.
Issue: #533
User-Visible: no
Release: v1.74.0
Medium: две обёртки унаследовали имя соседа. Блок `aria-disabled`-ручки звался
`owner_boundary`, а блок с проверками `owner_boundary_*` — `range_role`, имени,
которого нет ни у одной проверки. Логика при этом верна, страдает ровно то,
ради чего правка и делалась: чтение красной строки с раннера.
Теперь `disabled.*` и `owner_boundary.*` стоят на своих блоках. Смок зелёный.
Issue: #533
User-Visible: no
Medium: обёрнуты были три вызова из двадцати двух — главный сценарий. Остальные
девятнадцать по-прежнему молча отбрасывали возвращаемое значение, и жест,
не доехавший до ручки, оставлял смок зелёным.
Теперь `sent()` стоит на каждом вызове, имя проверки называет сценарий и тип
события. Отрицательный прогон: подмена `cx` на несуществующую ручку в
`mixed_role` красит ровно `mixed_role.pointerdown_sent`.
Issue: #533
User-Visible: no
Смок считал экранные точки один раз, заранее, а карточка переводит их обратно в
момент события — от текущего размера стейджа и текущего вида. Стоило раскладке
осесть между замером и жестом, и 34 экранных пикселя превращались уже не в 50
единиц плана: ресайз коммитил не ту величину, а свидетель сообщал об этом
четырьмя немыми `expected true, got false`. Раннер это ловил, локальная машина —
нет, и красный гейт закрыл выпуск v1.74.0.
Теперь координаты передаются в единицах плана, а перевод живёт внутри того же
кадра, что и отправка события. Доставка события проверяется (прежде
возвращаемое значение хелпера для `pointermove` игнорировалось молча), а
устойчивость отображения — отдельной проверкой `safe_resize.mapping_stable`:
масштаб на захвате и на движении обязан совпасть, иначе в имени проверки
печатаются оба масштаба и оба размера стейджа.
Продуктовый код не тронут: ни один из экспериментов не указал на дефект
ресайза. Если `mapping_stable` когда-нибудь покраснеет на раннере — это и будет
доказательством обратного, с числами в первой же строке.
Issue: #533
User-Visible: no
Seven version sources move together to 1.74.0; both changelogs gain the
stable section aggregated from the previous stable v1.73.0, the release
notes carry three itemised bullets on each side plus the grouped
small-fixes bullet, and STATUS says what this release is.
What the user gets since v1.73.0: the plan is noticeably smoother and in
Firefox dramatically so — a pan frame now moves what is already drawn by
a composited transform with the scene re-rasterizing on a budget (#531),
and a marker shadow sized in container units no longer restarts a
non-composited animation on every marker at once (#524); the printed plan
is about a third larger, because the in-plan dimension labels shrank and
the separate column of external dimensions is gone (#530); a plan left
over from the era of rooms without walls can be edited again, the
leftover cleaned up on the first save instead of refusing every change
(#529, reported in #527). Grouped under small fixes: alignment guides
follow the gesture again (#521), the summary panel neither freezes the
first frame nor claims an unavailable source (#509) and its settings
dialog scrolls again (#508), a space switch no longer replays a
neighbouring door's leaf (#525), and the card downloads 12.8 KB less on a
cold start (#526).
The speed verdict for #531 is deliberately not claimed here: headless
Firefox and Chromium hold 60 fps on both the old and the new path, so it
is owed by a profile from the owner's machine on the same pan.
`npm test` 2528/0/1 skip, `pytest tests_backend` 467 passed, release
notes pass the #328 verifier against v1.73.0..HEAD, bundle 287 242 B gzip
inside the ceiling, docs strict green.
Issue: #531
User-Visible: yes
Release: v1.74.0
Golden-джоба запускается только на кандидате с трейлером `Release:`, поэтому
Validate на мерже #530 её не гонял, и первый же релизный прогон
v1.74.0-beta.3 показал расхождение ровно в трёх сценах экспорта PDF:
`pdf-export-geometry-light`, `pdf-export-polish-light`,
`pdf-export-stepped-dimensions-light`.
Расхождение — это и есть предмет #530: план на листе крупнее, подписи
размеров стоят на чертеже, столбца выносов сбоку больше нет
(`sceneCoverage` 0.577). Кадры приняты с явным `--expect-change` из
артефакта того самого прогона (отпечаток исходников совпадает),
остальные 166 сцен сохранены без изменений, свидетелей среды 133.
Issue: #530
User-Visible: no
Release: v1.74.0-beta.3
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34597337319
Seven version sources move together to 1.74.0-beta.3; the changelog
sections in both languages close over what has landed since beta.2, the
release notes carry the two shipped bullets on each side with the grouped
small-fixes bullet last, and STATUS says what this beta is.
What the user gets: dragging the plan is smooth again — a gesture frame
now moves what is already drawn by a composited transform, and the scene
re-rasterizes on a budget (100 ms, or a 15 % shift) instead of on every
frame, which on the owner's 144 Hz Firefox cost 200 ms per frame with the
refresh driver skipping 124-144 ticks a second waiting for paint (#531);
and the printed plan is about a third larger, because the in-plan
dimension labels shrank by a quarter and the separate column of external
dimensions is gone, so the drawing keeps a whole scale step (#530).
The speed verdict for #531 is deliberately not claimed here: headless
Firefox and Chromium hold 60 fps on both the old and the new path, so it
is owed by a profile from the owner's machine on the same pan.
`npm test` 2528/0/1 skip, release contract green on all seven sources,
bundle 287 238 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: #531
User-Visible: yes
Release: v1.74.0-beta.3
Ребейз на `dev` (#530) обнулил обе производные копии: хэши чанков считаются от
содержимого, а отпечаток скриншотов — от `src/**`. Пересобрано, отпечаток
принят с `--identical`: все 11 кадров попиксельно совпали.
Issue: #531
User-Visible: no
Medium: `check-docs` красный — любая правка `src/**` делает отпечаток
скриншотов стухшим (#479), а `docs`-job на обычном push идёт в режиме `warn`.
Все 11 кадров попиксельно совпали с закоммиченными, принят только отпечаток
исходников.
Наблюдение без правки закрыто заодно: во всех прежних кадрах свидетелей
`floor === view`, то есть изометрия, ради которой камера и пол проецируются
раздельно, не была тронута ничем. Новый юнит разводит виды и по сдвигу, и по
размеру.
Issue: #531
User-Visible: no
Перезапись `viewBox` — это не сдвиг, а инвалидация растеризации всей сцены.
Кадр жеста делал её каждый раз: в профиле владельца (Firefox 155, 144 Гц) кадр
доезжал до экрана 200 мс, а драйвер пропускал 124–144 тика в секунду с пометкой
«ждём краску».
Теперь `paintLiveViewport` держит якорь — кадр, чей `viewBox` записан в DOM, и
момент записи. Кадр жеста двигает узлы сцены тем же проективным преобразованием,
которым уже двигались HTML-слои, а `viewBox` переписывается по бюджету: 100 мс
либо 15 % сдвига/масштаба. Ни атрибут, ни стиль не пишутся, если строка не
изменилась.
Issue: #531
User-Visible: yes
The A4 export spent a whole step of the scale series on the "Internal
dimensions" column beside the drawing: a ten-metre house printed 1:100
landscape on a quarter of the sheet while the same plan fits 1:75
upright. The column bought little — "R1" takes as much room beside the
wall as "2.31m" does — and it was read separately from the drawing.
The column is gone. A value with no free lane beside its own wall is
simply not printed; the rectangular rule became the general one. In-plan
type drops to three quarters of its former size (dimensions 5.25 pt,
areas 6 pt, names 6.75/5.25 pt) so the larger drawing is not crowded by
labels that do not scale with it.
On the owner's file the sheet goes from 1:100 landscape to 1:75 upright
and prints 37 values inside the plan against 35 before; the cost is
about seven short values (0.41-1.13 m) that no longer appear anywhere.
Issue: #530
User-Visible: yes
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
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
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
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
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
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
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
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
`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
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
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
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
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
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
#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
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
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
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
#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
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
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
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
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
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
The #500 adoption work changed src/** without touching any of the eleven
documented frames. The owner re-captured them in WSL — the canonical Linux
environment, because DirectWrite on native Windows never reproduces an
accepted frame — and `npm run docs:accept -- --identical` (#512) found zero
differing pixels, so only the source fingerprint moves:
48f770cf → ccbbfb4e. Frame bytes, their hashes and the capture-script guard
stay exactly as accepted.
Issue: #500
User-Visible: no
Review r2 M1. The smoke's reset() left the previous scenario's debounced
config/layout write pending; _deleteSpace flushes whatever is pending
before it writes, the fake socket answers without a rev, and the
documented rev+1 fallback then moved the revision on a body from another
scenario. The refused branch looked as if it had adopted:
onboardingDeleteRefusedAdoptsNothing was red on every run. The scenarios
are supposed to be independent, so reset() now cancels both debounced
writers, as smoke_danger_confirmation already does.
37/37 green, three runs in a row; with the cancel removed the same
single check goes red again.
The refused-tail fix from r1 had no witness in CI at all: no mutant
named this smoke as its guard, so the review gate never ran it and a red
witness survived a whole round. A witness that never runs is not a
witness, so the early return in _undoPlanOptimization now has a mutant
that names the smoke.
Issue: #500
User-Visible: no
`space/delete` (both runtimes), Optimize Undo and Import apply now treat
`asset-wait` like every reload path: nothing was adopted, so no toast, no
space switch, no history/undo reset — the dialog is released and the
scheduled reload owns the rest. Unit and smoke cover the refused branch for
all four paths; the spec's reactivity risk row states the real mechanism.
Issue: #500
User-Visible: no
`willUpdate` keys the geometry epoch and render-lifecycle invalidation on
`changed.has('_serverCfg')`. Before #500 every body replacement went through
Lit's accessor and produced that event; the owner wrote its field directly
and the epoch stopped moving on adoption, staging and rollback — the safe
Resize smoke then measured against a stale model (Validate on c360bcc9).
`MutableConfigAdoption` now reports each replaced reference through
`onBodyReplaced`, which the card wires to `requestUpdate(field, previous)`;
echoes and identity-only changes stay silent, exactly as an unchanged
reference never fired the accessor.
Issue: #500
User-Visible: no
`test/config-adoption-ownership.test.mjs` pins identity writes to the owner
and ratchets body staging (AC1/AC2). `demo/smoke_post_write_adoption.mjs`
drives space/delete (both runtimes), Optimize Undo and Import apply with a
concurrent backdrop change between the write and the re-read (AC4). Smokes
that seed revisions from outside the card keep working through the
`seedIdentity` harness seam behind the card's delegate setters. The initial
View ceiling is re-centred with the measured fact; ARCHITECTURE.md gets the
boundary paragraph.
Issue: #500
User-Visible: no
`src/config-adoption.ts` owns config/layout with revision and fingerprint;
the host keeps `_serverCfg`/`_cfgRev`/`_layout`/`_layoutRev` as delegates.
All seven authoritative adoptions go through `adoptAuthoritativeGated`
(backdrop readiness → continuity → adopt → profile tail); the post-write
paths (space/delete ×2, optimize_undo, import/apply) gain the gate and take
revisions from the re-read bodies. `rollbackOptimistic` moves to the owner;
plan-optimize, space copy and the vacuum writers stop assigning identity.
Issue: #500
User-Visible: no
The review gate re-ran almost every selected witness on every round even
when the executor's fix was twelve lines: the ledger fingerprint and the
diff selection both worked on whole files, and the card hosts are
thirteen thousand lines each. On #500 those twelve lines in
houseplan-editor-runtime.ts pulled 53 of the 75 witnesses the third
round ran, and the gate cost 140 job-minutes and an hour of the
reviewer's wall clock across three rounds.
The patch side is now judged by the anchor's neighbourhood — the anchor
lines plus ANCHOR_RADIUS_LINES on each side — in both the ledger
fingerprint and the diff selection, which now reads hunk ranges from
git diff --unified=0. The guard side keeps whole-file granularity: a
guard has no anchor and changes as a whole. An anchor that is not found
exactly once falls back to the whole file, and so does a file whose
hunks were not read: not knowing is not proof. Same class of
approximation as the existing diff selection, with the nightly full
gate (#513) as the floor.
Two more cuts to the wall clock of a review round. The shard plan is now
computed before the environment is installed — restore the ledger,
select, split, and only then pay for npm ci, Python and Chromium; the
job still runs, so the review gate's proof (#510) is unaffected. And the
matrix goes from three shards to six: the same job-minutes, half the
wall time.
On the #500 round the selection drops 60 → 7. Four witnesses guard the
new logic, including the two unsafe defaults (ambiguous anchor, missing
hunks).
Issue: #518
User-Visible: no
The residual 150–200 ms tasks are one space's wallBodiesGeometry — a
single polyclip union that cannot be split — not the aggregate the issue
is about. The threshold now names them and leaves room for a slower CI
runner; the mutant that computes everything in one task still produces
~1.5 s and fails.
Issue: #509
User-Visible: no
On the CI runner the smoke went red without any mutant: the observer was
started before the 60-room plan had finished drawing, and that render —
a long task of its own, unrelated to this issue — landed in the window.
The smoke now waits for a quiet main thread before it starts watching,
prints what it measured, and allows up to 450 ms per task: the residual
150–200 ms slices are one space's masonry union, which polyclip cannot
split, while the mutant that puts the whole aggregate back into one task
still produces ~1.5 s.
Issue: #509
User-Visible: no
Moving the aggregate out of render fixed the first frame, but the work
itself was still one uninterrupted ~1.5 s task on the large-house
fixture — the interface stayed frozen, just a moment later, which is the
same symptom the issue reports. cleanFloorAreaSteps yields after every
room; the runtime advances it with an 8 ms budget per frame and
reschedules until it finishes, so no slice outlives a frame and the
skeletons stay until the number is ready.
The smoke now watches longtask entries for the whole show, not only the
first frame: a single long task while the values are computed fails it.
Issue: #509
User-Visible: no
The injected-counter test proves "one geometry pass per space" but not
that its result reaches innerContourForRoom — without the shared
arguments that function rebuilds the masonry per room, and the only
observable difference is time (176 ms per room, S2). One large-house
floor: ~0.6 s with the shared pass, ~3.7 s without, so a 2.5 s threshold
is coarse enough not to flake.
Issue: #509
User-Visible: no
Two halves of the same first paint. The panel showed «Source unavailable»
in every row until the lazy metrics chunk arrived, because value() could
not tell "not loaded yet" from "source is dead"; and metrics() ran inside
render, walking the HA registry and unioning the clean floor of every
space synchronously — 11 s on the large-house fixture.
- totalCleanFloorAreaM2 computes the space's masonry and junction
topology once per SPACE and hands them to innerContourForRoom, which
otherwise unions the whole space again for every room: 11 045 → 1 488 ms
on that fixture, same 306.3 m². The card has always done this through
its own _innerContour cache; the panel now does the same.
- Aggregates leave the render path: the first frame paints skeletons and
the work starts right after the frame is shown (timeout → rAF →
timeout, never requestIdleCallback, which under load would leave the
skeleton up for seconds). Stale memo keeps the previous number on
screen instead of flashing back to a skeleton.
- valueState() separates pending from unavailable; a pending row keeps
the same plate, grid and height and carries a pulsing rectangle the
height of the line, replaced by the value with a short fade. Reduced
motion keeps the rectangle and drops the pulse.
- Panel enter/exit animation (#505, 190 ms) is now actually visible —
the main thread is free — and the smoke witnesses it.
Mutants: summary-first-paint-shows-unavailable, summary-metrics-block-first-frame,
summary-area-recomputes-walls-per-room, summary-stale-metric-falls-back-to-skeleton.
Issue: #509
User-Visible: yes
The spec file solved exactly one problem — proving that a review verdict
was passed on a given text — and created two: docs/specs/README.md
conflicted between parallel tasks and served as a second, stale status
dictionary, and every spec edit cost a commit, a push and a label. The
proof moves into the pipeline.
- review-doc-guard: normalizeIssueBody / issueBodyDigest (CRLF, trailing
whitespace, trailing newlines), the anchor line `Тело issue: <sha256>`,
anchorIssueBodyFrom, and issueBodyChanged — the finding "the spec
changed after a green spec review", judged against the pipeline's own
record in the last green SPEC-REVIEW, never against prose.
- reusableGreenVerdict takes the current digest: reuse (#499) skips the
model entirely, so without this a spec edit between rounds would pass
unseen. Documents without the record (the whole backlog) keep judging
by tree.
- process.yml: the material step reads the body with `gh issue view` in
the same run that fixes the material — the event snapshot describes a
text the reviewer may never see; the digest goes into the anchors, into
reuse and, when it differs, into the reviewer's prompt.
- process-gate: rule 3 judges the text (a `## ТЗ` heading or an AC1) with
the archived file still accepted; adding a new file under docs/specs/
warns — the directory is frozen.
- task-packet reads AC from the body first, the archived file second.
- PROCESS.md §2.3/§5/§7.1/§7.3/§10.5, AGENTS.md and docs/specs/README.md
say so; the index table is gone with the long-standing §7.3 debt.
Mutants: review-anchor-drops-issue-body, review-ignores-changed-spec-body,
reuse-ignores-changed-issue-body, process-gate-requires-spec-file.
Issue: #517
User-Visible: no
Mirroring validate.yml into main (e9638977, #510) left the branch with a
workflow its own tests do not describe: main still carries the v1.73.0
test suite, which pins the pre-#510 `changed_mutants` condition, so every
push to main has been red since 09.09 15:56. Only process.yml and
mutation-gate.yml must match across branches — they run from the default
branch, which is why the preflight compares exactly those two;
validate.yml runs from the branch under test and travels with dev on the
next stable promotion.
Issue: #510
User-Visible: no
The candidate is the branch tip, which already carries the round's
CODE-REVIEW-N-rK.md; the material the reviewer read does not. With
docs/reviews in the diff the two patch-ids never matched once dev had
moved, so every green candidate went back to review whenever another
task published its own document in the meantime — #514 looped twice on
09.09 and #508 only merged when dev happened to stand still. The
patch-id now excludes docs/reviews, exactly like `reviewedFresh` next to
it; a real change of the patch under rebase still returns the task.
Mutant: merge-rereviews-own-review-doc.
Issue: #516
User-Visible: no
Code review r3 (M1): realOps.releases() swallowed a failing `gh release
list` into an empty list, so a fine-grained token scoped to houseplan-e2e
alone would have dispatched with upgrade_from=stable — the tag onto
itself — and the red run would look like the bug 4143f998 already fixed.
The call now throws like dispatch() and lands in the same catch: result
`error` with the token hint, which now names both repositories. L4:
PROCESS.md says who dispatches and who waits.
Issue: #514
User-Visible: no
Two findings from the live run on v1.73.0 (houseplan-e2e run
34393136097): the upgrade job carries the previous stable's tag in its
name, so "any job with HP <tag>" let a gate for v1.72.0 adopt the
v1.73.0 run — recognition now keys on `journeys`/`first-run`; and the
first poll after a dispatch sees only the matrix-planning job, which
marked the run as foreign forever — a run without any `· HP … ·` job is
undecided and polled again. Live: v1.73.0 → green with the run link,
v1.72.0 → no run of its own.
Issue: #514
User-Visible: no
Live run on v1.73.0 (houseplan-e2e run 34392391382): at `release:
published` the new tag is already the newest stable, so
`upgrade_from=stable` made the upgrade suite update v1.73.0 onto itself
and fail with `Expected: not "1.73.0"`. The gate now resolves the newest
non-prerelease, non-draft release other than the tag from `gh release
list` and passes it as `upgrade_from`; the first stable ever falls back
to `stable`. Spec §4/§6 record the change and the matrix-planning job in
houseplan-e2e (a job-level `if` cannot read `matrix.*`).
Mutant: release-upgrades-stable-onto-itself.
Issue: #514
User-Visible: no
The stable gate proved Validate and Full Performance on the exact SHA but
never ran the release in Home Assistant itself. houseplan-e2e installs
the release's houseplan.zip — the bytes HACS ships — into HA in docker
and walks the sidebar page, dashboards, roles, PDF, restart and the
stable→tag upgrade. release.yml now dispatches e2e.yml on the tag for
`!prerelease` releases and waits for it (scripts/e2e-gate.mjs, modelled
on validate-gate.mjs): the gate recognises its own run by `HP <tag>` in
the job names, ignores foreign dispatches, and reports red / missing /
cancelled / token error with the run link. Betas are untouched.
Mutants: release-ships-on-red-e2e, release-trusts-foreign-e2e-run.
Issue: #514
User-Visible: no
The material anchors (commit, tree, spec blobs) written into every
review document came from the checkout step, before "Привести ветку к
dev". Whenever dev had moved — since 09.09 every review-document publish
moves it — the pipeline rebased and force-pushed the branch, orphaning
the pre-rebase commit and its tree. A fresh clone in the next run could
not resolve that tree: reuse (#499) always reported false and the
model reviewed the same code again, and the #413 post-step refused the
green round because neither the cited SHA nor the tree anchor was
reachable — #508 took three identical green rounds this way.
The `material` step, which already fixes the reviewed SHA after the
rebase, now also records the tree and spec blobs, and the publish step
reads all three from it. The contract test pins the order and forbids
reading anchors from the checkout step.
Issue: #515
User-Visible: no
In HA hp-dialog renders ha-dialog, whose own `.body` is the scroller and
is not a flex container; `.summary-editor` (overflow:auto,
overscroll-behavior:contain, min-height:0) therefore grew to its content
and became a scroll container that never scrolls — Chromium stops wheel
and touch scroll chaining at such a child, so nothing moved (reproduced
on ha.jbstudio.pro, HA 2026.9.1, and in an isolated Playwright page).
hp-dialog gains an opt-in `flex-content` attribute forwarded as
ha-dialog's `flexcontent`, which lays the body out as a flex column: the
editor is height-bound again and scrolls itself, header and footer stay,
exactly as the native branch already did. Only the summary dialog opts in.
Smoke demo/smoke_summary_dialog_scroll.mjs stubs ha-dialog after the HA
2026.9 contract: wheel on desktop, touch swipe on a phone, dialog within
the viewport, and a witness that the stub reproduces the bug without
flexcontent. Docs screenshots: 11/11 pixel-identical (docs:accept
--identical, #512), fingerprint refreshed.
Mutants: summary-dialog-drops-flex-content, hp-dialog-ignores-flex-content.
Issue: #508
User-Visible: yes
Code review r1 (M1): scripts/pre-push-gate.mjs — in its comment and in
the text every developer sees before a push — still named the full
mutation registry a pre-release gate; the same phrase lived in the
header of test/mutation-gate.test.mjs. Both now point at the nightly
schedule (#513); golden/smokes/HA harness are named as the heavy
Validate set on the candidate.
Issue: #513
User-Visible: no
The full registry run proves that tests can fail, not that the product
works; 4 of its 5 runs since 02.09 were manual dispatches tied to
releases. Owner decision 09.09: a daily schedule (01:00 UTC, before the
02:30 nightly Validate), failures reported as an issue by the existing
#472 job, no place in the development or release flow. Docs and the
workflow comments say so; the test pins the daily cron.
Issue: #513
User-Visible: no
The review pipeline runs from the default branch: the Validate-with-
mutants gate before Review and the `mutants` dispatch input must exist
here as well. Files are byte-identical to dev@33cb131b.
Issue: #510
User-Visible: no
Re-acceptance after the version seam (#512 §7) from the full Validate
dispatch on 605991ef (run 34366858855): seven frames carried the version
text — three version-mismatch banners, three PDF footers, the support
preview — and now read `0.0.0-golden`; six of them were within threshold
and still showed stale betas (beta.4, beta.8). The other 162 frames are
kept as reviewed; 128 environment witnesses matched byte for byte.
Sub-threshold drift in nine unrelated frames (tray, resize handles,
compass, junction) is not accepted.
Issue: #512
User-Visible: no
Release: v1.74.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34366858855
Golden frames carried the card version text (about, version banner,
support/export previews), so every beta bump re-accepted up to 20
baselines that had not visually changed. The version now reaches the DOM
and stand requests through displayVersion() (src/card-version.ts); the
golden harness pins window.__HP_VERSION_OVERRIDE__ = '0.0.0-golden'
before the card is created. CARD_VERSION literals stay where the release
contract reads them; cache-busting and the console banner keep the literal.
Docs screenshots: `npm run docs:accept -- --identical` re-captures
locally, compares decoded RGBA pixels with the committed frames inside
Chromium (scripts/png-identical.mjs) and, only when every frame is
identical, refreshes the manifest fingerprints and captureScriptSha256;
bytes stay, any difference refuses with a per-frame count. First run on
this tree: 11/11 identical, manifest refreshed.
Mutants: version-seam-ignores-override, docs-identical-accepts-any-frame.
Issue: #512
User-Visible: no
Spec review r1 (H1): the `--out` flag would have changed the only file
guarded by `captureScriptSha256` and reddened check-docs on its own. The
identical-accept mode now leaves demo/docs/capture.mjs as is (frames are
backed up and restored around the standard capture) and takes
`captureScriptSha256` from the candidate manifest together with the
source fingerprint; the docs re-acceptance for this issue is the first
local --identical run in the same branch.
Issue: #512
User-Visible: no
Code review r2 (M1): the cancelled-run filter in merge-candidate's
waitValidate had no test or mutant — every test replaced ops.waitValidate
with a fake. realOps now takes an injectable `exec` (default: the same
spawnSync wrapper) so the real implementation runs against scripted
`gh run list` answers: a cancelled dispatch is skipped and its
replacement followed; a lone cancelled run ends in `missing`, never red.
Mutant: merge-trusts-cancelled-dispatch.
Issue: #510
User-Visible: no
Code review r1 (M1): validate-gate.mjs and merge-candidate's waitValidate
read a `cancelled` dispatch run on the material as red, so a dispatch
replaced by the next one in the `validate-dispatch-<ref>` concurrency
group would have returned the task S7→S6 for nothing — the same class
#511 fixed in release-gate.mjs. Cancelled runs are now ignored: the gate
follows the replacement dispatch, or starts its own when there is none.
Mutant: review-returns-task-on-cancelled-dispatch.
Issue: #510
User-Visible: no
The mutant registry pointed at the pre-r1 verdict helper that no longer
exists; the anchor test caught it in CI. The patch now removes the red
branch of the completed-run check.
Issue: #510
User-Visible: no
Validate ran the three "Мутанты по диффу" shards on every push of every
branch: 48 of 56 job-hours on 08–09.09, most of them cancelled by the
next push. Mutants now run when asked — pull requests, the nightly
schedule, a push carrying a `Release:` trailer, or a dispatch with
`mutants=true` (classify-changes.mjs → `mutants_requested`); an ordinary
push runs the light checks only.
The proof moves to where it is consumed. process.yml gets a gate after
the #499 reuse step: on the code stage it looks for a dispatch Validate
run on the exact material SHA whose mutant jobs executed and passed
(scripts/validate-gate.mjs); none → it dispatches one and waits; red or
missing → the task goes back S7→S6 with the run link and the review
cycle is not spent. Spec stage and the reuse fast-path skip the gate
(`proceed=true`); all later steps branch on `proceed` in place of the
old conflict conjunct only. merge-candidate.mjs dispatches Validate on
the pushed candidate and waits for that dispatch run.
PROCESS.md/AGENTS.md: review does not start on red code; one handoff —
one push.
Mutants: mutants-run-on-every-push, review-starts-on-red-validate,
review-trusts-push-run-without-mutants, merge-waits-push-run-without-mutants.
Issue: #510
User-Visible: no
Spec review r3: `proceed` is true on the reuse fast-path too, so it must
replace the `rebase.conflict != 'true'` conjunct alone; the existing
`reuse != 'true'` (#499), stage and decide conjuncts stay on every step
that has them. The "single variable" claim is narrowed accordingly.
Issue: #510
User-Visible: no
Spec review r2: §5.2 named the gate outcome three different ways; the
skip branch (spec stage, reuse fast-path) would have matched a literal
`result != 'green'` and produced a spurious S7→S6 return. All step
conditions now branch on `proceed` only (true = green or skipped, false =
red/missing); `result` feeds the comment text alone.
Issue: #510
User-Visible: no
Spec review r1: place the gate after the #499 reuse step so its output is
defined; a green dispatch counts only when the "Мутанты по диффу" jobs ran
and passed (a foreign dispatch with mutants=false leaves them skipped);
therefore the Validate job runs whenever mutants are requested, even on an
empty selection. Explicit User-Visible/UX/i18n N/A statement added.
Issue: #510
User-Visible: no
The gate used to require every Validate run on the tag SHA to be green:
a cancelled duplicate or a red flake that a later re-run had fixed kept
the stable release blocked (v1.73.0, 09.09 — released by hand). Now the
verdict comes from the newest run that was not cancelled: not completed →
wait, success → pass, anything else → fail, no run → wait. The same rule
is documented for the perf workflow and the release runbook.
Mutants: release-gate-counts-cancelled-runs, release-gate-oldest-run-wins.
Issue: #511
User-Visible: no
The #160 contract test keeps the dense profile's longTasks block equal to
the historical isometric one, and both profiles boot through the same lazy
iso-scene-render chunk; the accepted split applies to both. Validate
34356856702 caught the divergence.
Issue: #507
User-Visible: no
Release: v1.73.0
Full Performance of the v1.73.0 stable candidate against the v1.72.0 product
(run 34354409872) was red on one check of the isometric profile:
longTask.countP95 16 → 20 against max(16×1.2, 16+3) = 19.2, with every
timing, longTask.totalP95Ms and longTask.maxSingleMs green. The trace behind
#506 shows why: since v1.73.0-beta.1 the isometric renderer is the lazy
iso-scene-render chunk (#160 Stage 3), so the single v1.72.0 boot task is
split in two around that import — the same work, +2 tasks.
Owner decision 2026-09-09: accept the split. countNoiseAllowance 3 → 5 for
large-house-isometric-v1 only; the ratio, the hard ceiling, total and
maximum single task keep gating real growth. The downloaded CI artefact
re-evaluated with this budget passes (limit 21, actual 20, no failures).
Documented in demo/performance/README.md; the budget test pins the
allowance and the untouched profiles.
Issue: #507
User-Visible: no
Release: v1.73.0
Promote the nine published v1.73.0 betas without new product behaviour:
stable version fields, synchronized generated bundles, the aggregated
bilingual release notes from v1.72.0 and status metadata only. beta.9
carried the startup-regression fix (#506) that Full Performance caught on
the first promotion attempt; the stable body keeps it out as an in-line
regression per the #328 curation rules.
Issue: #506
User-Visible: no
Release: v1.73.0
The beta.9 candidate failed the same phase twice in CI with "Resulting
promise was garbage collected" (runs 34346813552, 34347910231) while it
passes locally; a timer guard did not help, which points at a destroyed
context rather than a starved animation-frame chain. The wait now runs as
page.waitForFunction with raf polling, and the smoke logs frame navigations
and page crashes as `diagnostic …` lines so the next failure names its
cause. Verdict unchanged (animationName of the boot house, or 'missed').
Pre-release gate repair per PROCESS §11.4; locally OK ×2.
Issue: #506
User-Visible: no
Release: v1.73.0-beta.9
Validate 34346813552 on the beta.9 candidate failed only in browser smoke
shard 1: smoke_preloader phase 3 died with "Resulting promise was garbage
collected" — its rAF-only wait for the boot house had no other reference
while the runner withheld animation frames. A timer now bounds the wait
and keeps the promise reachable; the verdict is unchanged (animationName
of the house, or 'missed'). Pre-release gate repair per PROCESS §11.4:
locally `node demo/smoke_preloader.mjs` → OK ×2; all other heavy gates of
that run (golden, perf-smoke, backend, shards 2–3) were green.
Issue: #506
User-Visible: no
Release: v1.73.0-beta.9
Version fields and generated bundles move to 1.73.0-beta.9; the #506
changelog entry leaves Unreleased for the beta.9 section; release notes and
STATUS describe the startup-regression fix that unblocks the stable
promotion. No product change beyond #506, already reviewed and merged.
Issue: #506
User-Visible: no
Release: v1.73.0-beta.9
Every card instance attached its summary-panel runtime through
import().then(), even when the chunk had loaded long ago. The first render
therefore measured a header without summary controls; the controls arrived
a beat later, the stage shrank, the deferred refit opened a `stage-resize`
continuity candidate and the first HA tick paid three extra render passes
(Full Performance: blend stateUpdate1 50 → 130 ms, overlay 217 → 809 ms;
locally 4 performUpdate per tick instead of 1).
summary-runtime-loader.ts separates the summary code from its state: the
loaded factory is cached per page, every host builds its own runtime from
it (no shared preferences, drafts, subscriptions, timers or DOM). A warm
factory yields the runtime synchronously in connectedCallback, before the
first Lit render; a cold mount still pays one lazy import, concurrent cold
mounts share the pending import, a failed import is forgotten so the next
connection retries, and a disconnect cancels the pending attachment of that
connection. SummaryRuntimeSlot owns the per-host lifecycle so the card core
stays under its line ceiling.
Witnesses: loader unit tests (distinct instances, shared pending import,
cancelled attachment, retry after failure); demo/smoke_summary_warm_attach
(warm replacement and cold-key instance on a warm page own the runtime
before the first render, header/stage stable from the first frame, no
stage-resize, one performUpdate per geometry-neutral tick, a real viewport
resize still opens stage-resize); mutant summary-runtime-attaches-after-
first-render. smoke_summary_panel waited for `_summary` as a readiness
proxy; it now waits for the server config load, which stays asynchronous.
Local paired glow benchmarks (4× CPU throttle): blend 159.7 → 49.9 ms and
overlay 326.7 → 120.4 ms at stateUpdate1 with renders 4 → 1; the isometric
load loses the three summary-owned long tasks.
Docs screenshots: all 11 frames decode pixel-identical to the committed
ones; the manifest carries only the new source fingerprint. Initial View
ceiling recentred 299 100 → 299 600 for the +299 B loader.
Issue: #506
User-Visible: yes
Promote the eight published v1.73.0 betas without new product behavior. Synchronize version fields and generated bundles; aggregate public release notes from v1.72.0. Main-only workflow mirrors were merged with an identical beta.8 product tree.
Issue: #505
User-Visible: no
Release: v1.73.0
Personally reviewed all 20 diffs from complete Linux capture34323743229: #505 header/control placement and beta version text only. 149 baselines remain untouched,112 exact witnesses. Product and timing guard unchanged; candidate reruns the same50ms search threshold after a single CI timing failure (local p95=17ms).
Issue: #505
User-Visible: no
Release: v1.73.0-beta.8
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34323743229
Move the explanatory comment to TypeScript without changing styles; preserve the existing bundle-budget noise margin. Include independently accepted #498 in the release status.
Issue: #505
Issue: #498
User-Visible: no
Restore the shared footer minimum exposed by the #505 Linux screenshot gate; the focused browser assertion fails before the restoration and passes after it.
Issue: #505
Issue: #495
User-Visible: yes
Attachment uploads stage the body as `.upload-*` under files_root and then
asked the quota to count that file as stored usage *and* as the incoming
size, so the last file that still fit was refused at the boundary — by
bytes and by count. check_quota/dir_usage now take `exclude` for the
caller's own staged file; other staged files keep counting, so two
concurrent uploads can never both land past the limit.
The support package copied every string key of settings.fill_colors. The
schema stays open for compatibility, but the projection now keeps only the
eleven slots the card defines (SUPPORT_FILL_COLOR_KEYS, pinned to
src/logic.ts DEFAULT_FILL_COLORS by a test); an empty palette is omitted.
The SVG local-reference walk was a recursive DFS: a flat chain of a few
thousand hrefs passed every #436 bound and died with RecursionError, which
the upload view turned into a 500. The walk is iterative and measures the
longest chain through each node (memoised, order-independent); chains
deeper than MAX_SVG_REF_DEPTH = 64 are refused as too_large, cycles stay
invalid_image.
Tests: quota boundaries on the validator and the HA endpoint, a barrier
test for concurrent uploads, palette allowlist and TS parity, reference
chains (plain, hostile id order, cycle) on the validator and the endpoint;
six mutants caught by the standard runner.
Issue: #498
User-Visible: yes
Spec review r1: add the AC list with proofs and the perf/touch statement;
measure the SVG reference limit as the longest chain through a node
(memoised), not the stack height of the first traversal, so a hostile id
order cannot cut a long chain into short segments; state that two uploads
checked while both are staged are both refused (conservative), never both
stored.
Issue: #498
User-Visible: no
Apply re-validated the preview token after both halves were durable, so a
TTL that lapsed during the write, or an eviction by a newer preview of the
same user, answered "preview expired" for a plan that was already replaced
and withheld both update events. The token is now simply spent after the
commit; validity is decided once, on entry under write_lock.
Route runs dropped by drop_unknown_routes never reached the store unless a
marker happened to be orphaned in the same pass; an idle robot kept them in
memory only and a restart brought them back. The drop now happens under
_refresh_lock, leaves with the orphan transaction or with an immediate
write of its own, and rolls back like #335 when the store refuses.
Tests: HA harness for both apply races and a live config/set route drop,
recorder stubs for the four durability cases; five mutants caught by the
standard runner.
Issue: #495
User-Visible: yes
`execFileSync('git ls-files')` inherited stderr, so every call on a test's
temporary tree printed "fatal: not a git repository" although the failure is
caught and the tree is walked instead — dozens of lines in the owner's Windows
run and in CI logs. stderr is ignored; behaviour unchanged.
Issue: #496
User-Visible: no
Owner's Windows run after #496 (2338 pass, 2 fail): both remaining failures
are tests building the script path as `new URL(...).pathname`, which is
`/C:/Users/...` on Windows and makes Node look for `C:\C:\Users\...`.
check-inputs and classify-changes CLI tests now use fileURLToPath; the
portability test forbids `import.meta.url).pathname` in test/**.
Issue: #496
User-Visible: no
claude-code-action v1.0.218 (Claude Code 2.1.265) runs `claude install`,
which on ubuntu-latest sometimes leaves no launcher at ~/.local/bin/claude
while still reporting success; the action trusts the exit code and the SDK
then fails with ENOENT (anthropics/claude-code-action#1817). Four review
runs in a row died this way after 22:27 UTC 08.09.
Add a step that fetches the exact version the action pins (read from its
run.ts, fallback 2.1.265) from downloads.claude.ai, verifies the sha256
from the release manifest, checks `--version`, and hands the path to the
action via `path_to_claude_code_executable`, which makes the action skip
its own installer entirely.
Issue: #503
User-Visible: no
Run 2795: changed_mutants shard 1/3 hit the 30-minute job limit with zero
failures. package.json sits in the guard-input closure of 195 of 590 mutants
(`npm run …`, `npx …`), so adding one script — toolchain:check — selected
nearly the whole registry. A guard depends only on what it calls: a changed
or removed existing script, dependencies, engines. An added script is not an
input of any earlier guard. packageJsonRelevance() decides from the base and
head package.json; unparsable or anything outside scripts still counts as
relevant. For the same range the selection drops from 209 to 38.
Issue: #496
User-Visible: no
Run 2793 (changed_mutants 2/3): the clean run of
`resource-docs-flatten-current-yaml` was red before any mutation because the
screenshot fingerprint is stale after #490 — strict mode, which #479 reserves
for the beta candidate. The guard now runs `--screenshots=warn`; a test keeps
every check-docs guard in that mode.
Issue: #496
User-Visible: no
Windows portability (the three red tests on the owner's machine at green CI):
- scripts/spawn-portable.mjs: isMainModule via pathToFileURL (the
`file://${argv[1]}` form gives file:///C:/C:/... and the CLI stays silent);
portableCommand — a shell only for npm/npx/.cmd, node and git run directly
(spawn via shell dropped the quotes of `node -e "…"`). Applied to
classify-changes, mutation-gate-report, review-doc-guard, check-inputs,
merge-candidate, gate-small, rebase-on-dev.
- the rebase-on-dev test pins core.autocrlf=false / core.eol=lf through
GIT_CONFIG_* for its temp repository instead of touching the user's git
config; test/windows-portability.test.mjs forbids both anti-patterns.
Pins: scripts/toolchain-pins.mjs reads Node/Python from validate.yml, the HA
stack from tests_backend/requirements.txt, Playwright/Chromium from the
lockfile — no second dictionary; `npm run toolchain:check` compares the
machine; .nvmrc/.python-version are derived and tested equal;
scripts/wsl-setup.sh provisions WSL/Linux with those pins.
scripts/task-packet.mjs: one derived view of an issue (status, rights, owner
decisions, branch vs dev, Validate on the tip, previous verdict with recorded
tree, AC → evidence, unwitnessed). scripts/wait-verdict.mjs: polls labels,
pipeline comments and optionally Validate, prints only on state change, exit
0/3/4, writes nothing.
gate:small --smokes: after build the browser phase runs bundle-sync and then
the directly matched and registered smokes, two at a time; broad matches stay
with the reviewer. package.json changed, so the bundle is rebuilt here.
Issue: #496
User-Visible: no
scripts/merge-candidate.mjs owns the review pipeline's merge: when dev
moved during review, the rebased candidate is pushed to the issue branch,
its diff is compared to the reviewed one by patch-id, Validate on that SHA
is awaited, and only then dev is advanced with --force-with-lease on the
base the candidate was built on — a rejected lease restarts, at most three
times. Every non-merge outcome moves the label with a comment, so the
"label always changes" invariant holds. nightly.yml now finds the Validate
run it dispatched and inherits its conclusion. Three mutants guard this.
Issue: #492
User-Visible: no
guardInputs() replaces guardFiles() in selection and fingerprints: the
files named in the guard, the GUARD_INPUTS a wrapper declares (read
statically — the wrappers run on import), and the closure of imports and
path literals of every guard file, stopping at src/** which stays the
patch side. A diff that touches the registry itself selects every added or
changed definition against the base registry read from git. Five mutants
guard the manifest and this selection.
Issue: #492
User-Visible: no
scripts/check-inputs.mjs declares every Validate check with its roots and
entry points and computes the rest: imports and path literals of the
entries, transitively for code, as leaves for data. classify-changes and
gate-reuse both read it, so "which job runs" and "what its key hashes"
cannot disagree any more. An executable file no check knows widens the run
to the full set and is named in the summary; the coverage list makes such
a file a red unit test rather than a permanent widening. The workflow file
is a toolchain input of every job; backend no longer hashes src/**.
Issue: #492
User-Visible: no
Файл называл GitHub Projects v2 частью канонического backlog и держал
второй словарь статусов ТЗ. Проекты не используются с 2026-08-14 (#139),
статус — метка на issue (PROCESS §9); лёгкий трек держит ТЗ в теле issue.
Issue: #499
User-Visible: no
Review pipeline (process.yml):
- concurrency moves from the workflow to the guard/review jobs and the guard
runs only for S4-spec-review / S7-code-review. Any other label used to enter
the issue's concurrency group and evict the pending review run (sample of
150 runs since 2026-09-01: 92 empty guard-only runs, 30 cancelled).
- the guard reads the issue's current labels instead of the event snapshot; a
label removed before the run starts is a withdrawn request, no comment.
- a green verdict is re-applied without calling the model when the latest
review document carries the pipeline-recorded verdict `green`/High 0 and the
tree differs from its anchor in nothing outside docs/reviews/** (#437 r4
re-reviewed an unchanged tree for 7 minutes). The verdict from
structured_output is now written into the anchor block for that purpose.
- the reviewer is pinned to the captured material SHA in the prompt; the
broken escaping in the "merge cancelled" comment (empty SHAs) is fixed.
Mutation gate: nine browser guards started with `npm run bundle:sync` although
the runner already builds the mutant bundle — a second rollup plus a
`tsc --noEmit` that fails on a non-strict mutant before the smoke even runs.
Prefix removed; `--check` refuses guards that build the bundle themselves.
Docs: SCOPE (Project v2 dropped, three editors), STATUS (#437 merged, HACS zip
automated), USER-GUIDE ru/en (static card shows live states; kiosk double tap
on free background fits all), #34 → #425 references, #367 named as closed in
bundle-budget messages, PROCESS §10.4 and AGENTS.md describe the controller.
Issue: #499
User-Visible: no
Fingerprint-only: the panel change does not alter any documented frame
(the docs harness already gave the panel host the viewport height), so the
committed images stay and only the source provenance moves.
Issue: #488
User-Visible: no
HA assigns panel/hass/narrow/route before the top-level-await entry has
defined the element, so the values landed as own properties that shadowed
the accessors and the card never received hass. And <ha-panel-custom> has
no height, so the percentage host height collapsed the stage to 0 px.
Adopt pre-upgrade properties through the accessors and size the panel from
the viewport minus HA's safe-area padding. The smoke now reproduces HA's
real mount order and container; two mutants guard both contracts. The
bundle is rebuilt from these sources.
Issue: #488
User-Visible: yes
witnessFingerprint (файлы патча и гарда + объявление, без строки версии),
readLedger/recordCaught/splitByLedger, флаг --ledger только с --changed;
журнал пишется после каждого пойманного мутанта, в CI — cache restore по
префиксу шарда и save при любом исходе. Три свидетеля.
Issue: #481
User-Visible: no
Fifteen reviewed scenes from the CI matrix on dev 55d832ec: ten isometric
scenes without the painted overlay plates (#471) and five color-popover
scenes with the OK button (#476). Pixels taken from the Validate artifact,
not from a local capture.
Issue: #471
Release: v1.73.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/34046189201
User-Visible: no
Первый ручной запуск nightly (run 2645) был отменён через 1:31 очередным
push в dev: обе стороны делили группу validate-refs/heads/dev. Ночной
полный набор не должен обнуляться коммитом, пришедшим в то же окно.
Issue: #479
User-Visible: no
Классификатор отдаёт выход mutants по scripts/mutation-gate.mjs, job
changed_mutants получает третий дизъюнкт из ТЗ §2; fallback-ветки
выставляют mutants=true. Тест AC7 отбирает бэкенд-мутанты по файлу патча.
Issue: #475
User-Visible: no
В main остаётся только nightly.yml: он диспатчит Validate на dev,
где workflow_dispatch уже есть. Run 2643 падал на classify-changes.mjs.
Issue: #479
User-Visible: no
Каталог (id, группа, размеры) остаётся eager; SVG-пути 44 дизайнерских
символов — отдельный чанк за FurnitureArtRuntime (ready|pending|fallback,
нонс на повторе, отпечаток сборки, осевший отказ). Запуск при приёме плана,
бут-вуаль ждёт арт в пределах BOOT_MAX_MS, редактор отдаёт арт рантайму
синхронно (adopt при загрузке чанка). Магнит проверяет каталог, не арт.
Потолок initial View 300 500 → 290 500. Семь свидетелей, смок
smoke_furniture_lazy_art, golden-harness требует все предметы после бута.
Ядро −2 строки.
Issue: #474
User-Visible: no
Validate ran three smoke shards, golden and performance_smoke on every push,
check-docs went red on any src/** change until screenshots were re-captured,
and a parallel bundle build made every second task branch fail to rebase.
None of these gates ever failed at review time; they fail before betas.
- `heavy` output in job `changes` (scripts/classify-changes.mjs): smoke,
smoke_done, golden, performance_smoke run only for a head commit with a
`Release:` trailer, `workflow_dispatch full=true` and pull requests.
- nightly.yml dispatches Validate on dev with full=true every night.
- check-docs `--screenshots=warn|strict`: freshness of the screenshot index
warns on a plain push, errors on the candidate; everything else still errors.
- publish-prerelease.yml and release.yml refuse a candidate without the
`Release:` trailer and (prerelease) require fresh screenshots — a green
Validate without the heavy jobs cannot pass for a release.
- scripts/rebase-on-dev.mjs: rebase on origin/dev taking dev's copy of the
committed bundle, rebuild with bundle:sync, amend; any other conflict aborts.
- npm run gate:small: mandatory PROCESS §8 part in one parallel run.
Issue: #479
User-Visible: no
Классификация changes вынесена в scripts/classify-changes.mjs (выходы
perf_iso/perf_interaction, fallback --all). performance_smoke добавляет
large-house-isometric-v1 при правке src/iso-* и large-house-interaction-v1
при правке живого пути, по 3 образца против hardMaxMs полных профилей
(budgets-*-smoke.json). Набор профилей входит в ключ reuse. PROCESS.md §8.
Issue: #473
User-Visible: no
Accept the reproducible Linux CI capture for the issue 471 source tree. All ten documentation screenshots are byte-identical; only their source fingerprint changes.
Issue: #471
User-Visible: no
Keep the raised overlay footprint as calculation-only geometry for collision, nudge and fit while removing its painted SVG surface and texture. Update Stage 3 contracts, smokes, golden harness rules and synchronized bundles.
Issue: #471
User-Visible: no
Promote the published v1.72.0 beta line without new product behaviour: stable version fields, synchronized generated bundles, bilingual changelogs, release notes and status metadata only.
Issue: #54
Release: v1.72.0
User-Visible: no
#451 принёс 1 651 строку в восьми новых модулях, а единственный мутант
того релиза патчил houseplan-card.ts и houseplan-editor-runtime.ts —
места, ОТКУДА код ушёл. Вынос в модули был правильным решением, но
защита осталась смотреть на старые файлы.
Восемь мутантов, семь модулей, все прогнаны штатным раннером (чистый
прогон зелёный, мутант красный):
- live-editor-view-mode-routes-live — режим View обязан оставаться
реактивным: живой путь редактора там означает план, который перестал
отвечать на Home Assistant у двух персон из трёх;
- live-editor-first-gesture-frame-goes-live — первый кадр жеста меняет
выделение и хром и обязан остаться реактивным;
- pointer-move-queue-keeps-first-move — очередь last-wins: сохранение
ПЕРВОГО коллбэка кадра рисует план там, где палец уже не находится;
- live-hass-tick-never-deferred — тик состояния посреди жеста
откладывается намеренно, и флаг отложенности гарантирует его
воспроизведение после;
- live-viewport-identity-projection-not-recognized — см. ниже;
- render-invalidation-unknown-key-ignored — классификатор обязан
ошибаться в сторону перерисовки на незнакомых ключах hass;
- resize-live-preflight-keeps-every-room — живой resize проверяет только
задетые комнаты, иначе каждый кадр становится полной проверкой плана;
- render-lifecycle-diagnostics-cache-never-invalidated — кэш диагностики
обязан сбрасываться, иначе красная точка нового устройства не загорится.
Мутант на live-viewport пришлось заменить, и это стоит записать. Issue
предлагал снять `setLayerProjection(layer, null)` из
`commitHouseplanViewport` — прогон показал, что тест остаётся ЗЕЛЁНЫМ:
следом идёт `paintLiveViewport(root, painted, painted)`, который на
равных аргументах даёт identity и обнуляет проекцию сам. Тот цикл —
подстраховка, а не контракт. Настоящий контракт — распознавание identity
(`isIdentityLiveLayerProjection`), потому что даже единичный transform
переключает путь композитинга и сдвигает установившийся растр на
несколько уровней цвета. Мутируется теперь он.
`src/live-hover.ts` остался без мутанта сознательно: его контракты либо
чисто производительные (мемо по наведённой комнате), либо доменные
(подсветка комнаты, застрявшая после ухода курсора). Первое мутантом не
ловится в принципе, второе — только браузерным смоком, которого в
песочнице нет. Записано в тесте и в issue.
Чтобы требование не жило в памяти, добавлен гейт: у каждого модуля
горячего пути обязан быть свой мутант, и он не имеет права патчить
houseplan-card.ts или houseplan-editor-runtime.ts — то есть исходный
дефект #458 больше не воспроизводим молча. Проверено отрицательным
прогоном: перевод патча любого из модулей на старое ядро краснит гейт.
Гейты: npm test 1960 tests, 1959 pass, 0 fail; typecheck зелёный;
mutation-gate --check зелёный на всех якорях; каждый из восьми мутантов
прогнан отдельно.
Issue: #458
User-Visible: no
Отпечаток исходников включает package.json, а 28a4cb5e (#455) его изменил
без bundle:sync. dev красный с 16:27 UTC: CI пересобирает dist и сравнивает с
коммиченной копией — расходятся ровно строкой __HOUSEPLAN_BUILD_FINGERPRINT__
и производными от неё именами чанков. Код чанков побайтово тот же, проверено
diff-ом. Ветка #454 унаследовала красноту ребейзом.
User-Visible: no
Issue: #454
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33895243347
Вопрос владельца: зачем агенты снимают PNG на Windows, если кадры мы всё
равно не принимаем, тем более что WSL есть на обеих машинах. Ответ по
коду: этому ничто не мешало. Ни один из шести скриптов съёмки и приёмки
не знал, на какой он ОС — ни `process.platform`, ни win32, ни WSL не
упоминались нигде. Съёмка отрабатывала штатно, а стена появлялась на
приёмке, и текст стены говорил «сцен-свидетелей 0 из 10», то есть
подсказывал неверный вывод «надо объявить больше сцен» — от которого до
`--expect-change` на всю матрицу одна команда.
Причина запрета не политика, а физика: Windows растеризует текст через
DirectWrite, с другим субпиксельным сглаживанием и DPI, поэтому
байтового совпадения с принятым эталоном не даёт никогда, свидетелей
среды быть не может, и приёмка откажет всё равно. Флаги детерминизма из
#410 убирают разброс внутри среды, а не между ОС.
Что сделано:
- `golden:capture` отказывается до запуска браузера, в тексте отказа
готовая команда для WSL;
- `golden:verify` остаётся законным в любой среде: он ничего не
принимает, а как грубая проверка полезен;
- обе приёмки (golden и документации) отказываются в чужой среде;
- к отказу свидетелей приписывается фраза про расхождение среды — та
самая, которой не хватало, чтобы отказ не читался как «объяви больше
сцен»;
- платформа уезжает в манифесты рядом с версией Chromium: у кадров
появился провенанс среды;
- осознанный обход есть и требует причину: HP_ALLOW_FOREIGN_CAPTURE.
Главный урок задачи — про цену правки файла, у которого записан хеш.
Первая редакция встроила проверку в `demo/docs/capture.mjs`, и гейт
документации сразу покраснел: его sha записан в индексе скриншотов, и
`scripts/check-docs.mjs` их сверяет. То есть проверка, которая ничего не
рисует, стоила бы пересъёмки всех картинок документации и визуальной
приёмки владельца. Поэтому для документации отказ живёт шагом раньше —
`npm run docs:capture` вызывает `scripts/assert-capture-env.mjs` — и
шагом позже, на приёмке. По той же причине гейт golden стоит в
`demo/golden/policy.mjs`, а не в `run.mjs`: последний входит в корпус
sourceFingerprint. Тест закрепляет обе границы: гейт обязан быть в
policy.mjs и в npm-скрипте и обязан отсутствовать в двух
фингерпринтуемых файлах.
Платформа в юнитах — параметр, а не `process.platform`: иначе тест был
бы зелёным на Linux и красным на машине владельца, то есть тестом про
хост, а не про правило.
AGENTS.md приведён к состоянию после #401 (принимается любая среда,
доказавшая себя байтовым совпадением непринятых кадров) и разводит
проверку и съёмку — прежний текст сливал их в «advisory» и утверждал
«accepted only on a complete Linux CI artefact».
Свидетели, все проверены отрицательным прогоном: снятый гейт съёмки,
гейт, отказывающий и на verify, обход без причины, отказ без команды,
убранная приписка про среду, отцепленные гейты обеих приёмок,
переставшая бросать обёртка, npm-скрипт без проверки и возврат гейта в
каждый из двух фингерпринтуемых файлов. Проверка подключения сначала
смотрела только на импорт модуля и молча проходила, когда отказ
заменяли на `void` — теперь она проверяет вызов бросающей обёртки.
Гейты: npm test 1918 tests, 1917 pass, 0 fail; typecheck зелёный;
check-docs зелёный (индекс скриншотов не задет); pytest без HA 378
passed, 3 skipped.
Issue: #455
User-Visible: no
The immutable beta.2 candidate changed one version-bearing golden frame without the two baseline trailers. Its complete Linux Validate successor proved the exact baseline tree; record that one SHA as the only historical exception while preserving every other provenance check.
Issue: #426
Release: v1.71.0
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33760450815
User-Visible: no
Артефакт один — scripts/sh3d-convert/dist/index.html, самодостаточный
файл, как и лендинг: раскладывается копированием, без сборщиков и CDN.
Собирается склейкой тех же модулей, которые проверяют гейты этапа 1;
относительных импортов на странице нет, поэтому она работает из любого
каталога и без сети.
Три обещания страницы проверяются тестами, а не текстом:
1. Ни одного сетевого вызова и ни одной внешней загрузки. Ищутся формы
ВЫЗОВА, а не имена API: первая редакция теста краснела на собственном
комментарии, где эти API перечислены как запрещённые. Шрифты
системные — <link> к шрифтам это внешний запрос при загрузке, а
обещание «файл не покидает браузер» должно быть буквальным. Заодно
поэтому на странице нет аналитики.
2. Конвертер внутри страницы даёт тот же результат, что модули
репозитория: код извлекается из собранного артефакта, прогоняется на
фикстуре и сравнивается с golden. Склейка, потерявшая модуль,
краснеет.
3. Собранная страница не отстала от исходников: build-page.mjs --check
сверяет закоммиченный артефакт с пересборкой.
Плюс полнота переводов: у каждой подписи есть ru и en, словари
совпадают по ключам, у каждого кода отчёта и каждого кода отказа есть
человеческий текст. Иначе пользователь увидит пустую надпись или
`not_zip`.
Отчёт показывается ДО скачивания и по этажам: сколько комнат, стен и
проёмов, какая клетка сетки, и полный список мест, где конвертер принял
решение, — спрямлённая дуга, обрезанная толщина, непривязанный проём,
пропущенный этаж без комнат. Это требование задачи: искать чужие ошибки
в готовом плане из сотен объектов хуже, чем прочитать их заранее.
Гайды (ru/en) получили раздел про импорт из Sweet Home 3D с прямой
записью, что это необязательный ярлык, а не шаг настройки: SCOPE обещает
«no external editors», и обходить это молчанием нельзя.
Свидетели этапа 2, все проверены отрицательным прогоном: добавленный в
страницу fetch, потерянный русский перевод, правка dist руками, склейка
без модуля sh3d.mjs — каждая краснит свой тест.
Гейты: npm test 1871 tests, 1870 pass, 0 fail; pytest без HA 378 passed,
3 skipped.
Осталось владельцу: раскладка на хост (из песочницы SSH недоступен) и
проверка на настоящем файле Sweet Home 3D.
Issue: #446
User-Visible: no
Решение владельца по итогам исследования: из чужих форматов планировок
работать имеет смысл только с .sh3d, и конвертер живёт на сайте, а не в
карточке. Продуктового кода задача не касается вовсе — документ импорта
не подписан и не привязан к инстансу, поэтому сторонний генератор это
легальный сценарий уже сегодня.
Этап 1 — всё, что должно жить в репозитории и проверяться в CI:
- scripts/sh3d-convert/{xml,zip}.mjs — читатели XML и zip без единой
зависимости, работают и в Node, и в браузере (DecompressionStream либо
node:zlib). Недоверенный ввод отбивается на входе: DOCTYPE
пропускается и не загружается, объявления сущностей отвергаются,
шифрованные записи, zip64 и распаковка сверх предела — отказ с кодом;
- sh3d.mjs — уровни, комнаты, стены, двери и окна в сантиметрах;
мебель, материалы, свет, камеры не читаются вовсе;
- convert.mjs — маппинг в документ kind=space, plan-only, model 7.
Форма v7 выбрана намеренно и это главное техническое решение задачи.
При v8+ схема требует полный каталог сегментов: wall_ids по числу рёбер,
один-два владельца у каждого сегмента, проекция walls, совпадающая с
сегментами. Всё это на стороне сайта означало бы повторить серверный
алгоритм и разойтись с ним на первом изменении модели. В форме v7 ту же
работу делает commit_wall_segment_model — тот же путь, которым едут
старые бэкапы: сегменты собираются сами, общая граница двух комнат
склеивается в один сегмент с двумя владельцами, проёмы получают хозяина.
Проверено прогоном: v7 → валидный v9.
Второе решение — план строится по комнатам. Стена в нашей модели
существует как ребро контура комнаты, поэтому стены Sweet Home 3D дают
рёбрам только толщину, а уровень без комнат конвертировать нечем: это
отказ с объяснением, а не пустой план.
Геометрия: вершины комнат привязываются к осевым линиям стен (Sweet Home
3D обводит комнаты по внутренним граням, «как есть» получились бы две
параллельные стены вместо общей), затем сваривются с точностью до
сантиметра. Проёмы проецируются на ребро, угол берётся у ребра, длина
обрезается до ребра — серверная привязка допускает 8° и 0.02 шага
решётки, поэтому ни угол, ни центр из файла доверия не заслуживают.
Гейт против дрейфа версий (AC5) — две половины:
- test/sh3d-convert.test.mjs: фикстуры → конвертер → сравнение с
закоммиченными golden. Правка конвертера без пересборки golden красная;
- tests_backend/test_sh3d_convert.py: golden проверяются настоящими
CONFIG_SCHEMA и commit_wall_segment_model, плюс кросс-рантаймовый пин
формул _wall_key и канонизации решётки. Правка модели, не отражённая в
конвертере, красная — до того, как это увидит пользователь;
- tests_backend/test_ha_sh3d_convert.py: golden проходят настоящий
create_preview (нужен HA, идёт в Linux CI). Там же отрицательная
проверка: документ с приватным полем обязан получить отказ.
Свидетели, все проверены отрицательным прогоном: снятое выравнивание
вершин, отключённая сварка, угол проёма из файла, непроецированный
центр, необрезанная длина, снятая обрезка толщины, объявленная модель
v9, разошедшийся порт _wall_key, правка golden руками, поднятая
PLAN_MODEL_VERSION, изменённая серверная формула, изменённая канонизация
— каждая краснит свой тест.
Две фикстуры пришлось усилить именно из-за таких прогонов: углы и центры
проёмов в первой редакции совпадали со стенами случайно, и мутации
проходили молча; появилась и фикстура с общей границей без стены и шумом
в доли сантиметра — иначе сварка вершин не исполнялась ни разу.
Фикстуры синтетические, собраны генератором по опубликованному формату:
настоящих .sh3d в сборке нет и взять их автоматически негде. Проверка на
реальном файле — ручная приёмка владельца, записана в issue.
Гейты: npm test 1867 tests, 1866 pass, 0 fail; pytest без HA 378 passed,
3 skipped.
Этап 2 (страница /convert на houseplan.tech, ru/en) — следующим шагом.
Issue: #446
User-Visible: no
Accepted the fingerprint-only result from canonical Docs screenshots run 33802095608; all ten image hashes remain byte-identical.
Issue: #443
User-Visible: no
Treat explicit empty route lists as authoritative, preserve them through single-space export, group deleted-space routes, and render vacuums from the immutable vacuum-only snapshot subset.
Issue: #443
User-Visible: yes
#429 снял `SUPPORT_LAZY_INITIAL_BASELINE_BYTES = 291046` — порог,
привязанный к критерию приёмки #423, с пятнадцатью байтами запаса и
сообщением про копирайт формы поддержки. Снять было правильно. Но снят
он оказался ровно на том релизе, где сработал бы:
beta.1 291 031 · снятый порог 291 046 · beta.2 291 069
Рост случился внутри той же беты, уже после снятия, и заметить его стало
нечем: единственным сигналом остался `lowHeadroomWarning`, который горит
постоянно — третий аудит подряд, и третий раз без реакции.
Механизм по образцу ядер (#425): потолок 292 000 Б с полосой 2 000 Б,
двусторонний. Рост выше потолка — отказ с числом и указанием, что делать
(поднять потолок в том же коммите с объяснением либо вынести код в
ленивый граф, #367). Падение ниже полосы — тоже отказ: незафиксированный
выигрыш отыгрывается молча, именно так запас бюджета ушёл с 26 КБ до
8.3 КБ за сутки.
Полоса, а не точное число, — по измерению, а не из осторожности. На
beta.2 initial-чанк стал МЕНЬШЕ на 344 сырых байта и при этом на 40 байт
больше в сжатом виде: gzip не монотонен по исходнику. Точный храповик по
gzip краснел бы на коммитах, которые код сокращают, — та же лотерея, о
которой предупреждает комментарий к бюджету 300 000. Потолок поставлен
так, чтобы факт лежал ближе к середине полосы: 931 Б до отказа сверху,
1 069 Б снизу.
Предупреждение о запасе стало погашаемым: `LOW_HEADROOM_ACKNOWLEDGED_CEILING`
привязан к ЗНАЧЕНИЮ потолка, поэтому признание долга перестаёт покрывать
ровно тогда, когда потолок поднимут. Сейчас `null` — не погашено, и текст
говорит, чем гасится. Превышение самого бюджета признанием не гасится:
там отказ, а не тревога.
Свидетели. Девять мутаций, каждая краснит свой тест: снятие верхней
стороны, снятие нижней, потолок выше бюджета, полоса 50 Б, признание
поверх превышения бюджета, молчащее устаревшее признание, и две — про
подключение: вызов потолка убран из main и отказ понижен до console.log.
Последние две прошли молча на первой редакции тестов — статическая
проверка «в main есть вызов» была бы циклическим доказательством, за
которое #430 снял циклический тест гарда benchmark, поэтому добавлен
прогон настоящего CLI в подставном дереве.
Захардкоженный `lowHeadroomWarning(9058)` из #429 заменён на число из
поставляемого манифеста: константа в тесте выглядела измерением, не
будучи им.
Мутант `initial-view-ceiling-unplugged` в реестре.
Гейты: npm test 1819 tests, 1818 pass, 0 fail; node scripts/bundle-budget.mjs
зелёный — 291 069 внутри полосы, запас до бюджета 8 931 Б.
Issue: #438
User-Visible: no
The main-only #413/#416 workflow mirrors are already byte-identical to dev; merge ancestry without changing the tested stable tree.
Issue: #416
Release: v1.70.0
User-Visible: no
Promote the published v1.70.0 beta line without new product behaviour: stable version fields, synchronized generated bundles, bilingual changelogs, release notes and status metadata only.
Issue: #415
Release: v1.70.0
User-Visible: yes
Канон требовал назвать SHA предыдущего раунда и считал ненайденный SHA
находкой. Механика теперь даёт больше: конвейер дописывает в документ дерево
материала и блоб каждого ТЗ — их ребейз не меняет, потому что git адресует их
содержимым (issue #416).
Отсюда правка по существу: неразрешимый SHA сам по себе перестаёт быть
находкой. Ветку задачи между раундами перебазируют, сквошат или удаляют — по
корпусу ревью таких объявлений 98 из 804, и объявлять это дефектом значит
объявлять дефектом обычную работу. Материал в таком случае берётся по якорям,
и команды приведены прямо в пункте.
Находкой осталось то, чем #413 и был: SHA, мёртвый уже в момент публикации.
Он означает, что значение сняли до amend или rebase и не сверили перед выводом
отчёта, как требует §7.2. Такую публикацию конвейер теперь останавливает сам.
Issue: #416
User-Visible: no
Конвейер исполняет process.yml из ветки по умолчанию, поэтому файл обязан
совпадать в main и dev — это проверяет предполётный шаг. Здесь ровно тот же
файл, что в dev, байт в байт.
Issue: #416
User-Visible: no
#413 закрыл класс «SHA мёртв уже в момент публикации». Остаётся более частый:
SHA был жив, а умер потом — по корпусу таких объявлений 98 из 804, потому что
ветку задачи после ревью перебазируют, сквошат или удаляют.
SHA коммита — свойство истории, а история переписывается. Содержимое не
переписывается: git адресует деревья и блобы их хешем. На #403 спец-коммит
переехал из 83005c3c в 94502d3d, а блоб ТЗ у обоих один — 56a92e12; по нему
материал находится одной командой независимо от ребейза.
Конвейер снимает якоря там, где читает материал — в шаге перехода на ветку
задачи, пока рабочая копия равна тому, что прочтёт ревьюер. В шаге публикации
спрашивать поздно: дерево уже сброшено на целевую ветку. При публикации якоря
дописываются машинным блоком: дерево материала и блоб каждого ТЗ, каждый со
своей исполнимой командой поиска.
Блок машинный и помечен как машинный. Ревьюер его не заполняет: дисциплина
ручного переписывания SHA здесь уже подвела, и заменять её другой ручной
дисциплиной смысла нет.
Гейт #413 смягчён ровно там, где обязан: осиротевший SHA при живых якорях —
предупреждение, а не отказ. Ронять раунд, который воспроизводим, было бы той
же ошибкой в другую сторону. Отказ остаётся, когда не работает ни один
объявленный способ найти материал.
Проверено на настоящем осиротевшем случае: блок, собранный для 94502d3d,
находит и дерево, и блоб ТЗ; тот же документ с якорями даёт предупреждение
вместо отказа, без якорей — отказ.
Issue: #416
User-Visible: no
Конвейер исполняет process.yml из ветки по умолчанию, поэтому файл обязан
совпадать в main и dev — это проверяет предполётный шаг «Процесс: process.yml
идентичен в main и dev». Здесь ровно тот же файл, что уехал в dev коммитом
206732e9, байт в байт.
Issue: #413
User-Visible: no
SPEC-REVIEW-403-r2 объявил материал раунда на `HEAD = 83005c3c`, и тот же SHA
независимо назвал автор ТЗ в комментарии issue. Разбор подтвердил находку и
уточнил её: коммит существовал, но к моменту публикации был осиротевшим.
Ветку перебазировали за пятнадцать минут ДО публикации документа — спец-коммит
переехал в 94502d3d с тем же сообщением и тем же содержимым (блоб ТЗ у обоих
56a92e12). Через раунд команда `git diff 83005c3c..HEAD` из §2.10 буквально не
работала, и r3 восстанавливал коммит по содержимому диффа руками.
Гейт судит только объявление материала в шапке документа, а не каждое
шестнадцатеричное слово: в прозе SHA упоминаются исторически, и обещания
воспроизводимости на них нет. Границы кандидата подобраны по корпусу — 7–40
знаков, хотя бы одна буква, не после `#`, не внутри длинного хеша; это
отсекает sha256, цвета и номера прогонов.
Достижимость считается от refs/remotes/origin, а не от локальных ссылок.
Разница не теоретическая: осиротевший 83005c3c до сих пор достижим в клоне
автора из необновлённой локальной ветки — локальная проверка сказала бы «всё в
порядке» ровно на той машине, где ошибку и совершили.
Шаг стоит ПОСЛЕ публикации и ДО перестановки метки. Артефакт ревью терялся
здесь трижды (#171, #220), и «вердикт без документа» дороже мёртвой ссылки:
документ сначала спасается, потом судится. Инвариант «метка не сменилась =
прогон упал» при этом сохраняется.
Проверено на настоящих документах: SPEC-REVIEW-403-r2 отказ, CODE-REVIEW-390-r1
проходит, документ без объявления материала не судится.
Issue: #413
User-Visible: no
smoke_grid_scale_invariance сообщает, НАСКОЛЬКО разошлись кадры: строка
pixel-diffs с changed, maxDelta и meanDelta по каждой паре. Введена в #302
ровно затем, чтобы падение не было голым boolean.
В логе прогона её не видно: шаг шарда печатает tail -20, а диагностика идёт до
вердикта и срезается. На #411 это и вышло — в логе осталось только «expected
true, got false», а числа, по которым видно, превышение порога это или
расхождение слоя, пришлось бы искать в артефакте.
Строки достаются адресно, перед хвостом лога. Они и есть разница между
«чинить» и «гадать».
Issue: #411
User-Visible: no
Замер сделал свою работу — теперь он остаётся как проверка. Если кадр снова
начнёт зависеть от времени, шаг упадёт, а не напечатает число в лог. Стоит
перед съёмкой набора: публиковать артефакт, снятый недетерминированной
съёмкой, смысла нет.
Issue: #410
User-Visible: no
Замер снял главное: три снимка подряд в одном состоянии страницы совпадают
побайтово у всех десяти сценариев. Значит рендер детерминирован, а плавает то,
что приходит на вход съёмке.
Обрезка считается из живого DOM через getBoundingClientRect и приходит
дробной. Дробная обрезка заставляет Chromium ресемплить кадр — и тогда сдвиг
раскладки на десятую пикселя переписывает границы всех элементов на единицы
уровней. Ровно эта подпись в #410: 76 пикселей, максимум 2 уровня, alpha не
тронута, всё на сглаженных границах полей.
Рамка расширяется наружу, а не округляется к ближайшему: обрезка обязана
содержать цель целиком.
Issue: #410
User-Visible: no
Флаги растеризации убрали один кадр из трёх, но device-editor и device-info
плавают по-прежнему. Дальше гадать нельзя: нужен ответ, плавает ли кадр внутри
одного состояния страницы или разница копится между подготовками сценария.
Режим --stability=N делает N снимков подряд без единой правки состояния и
сравнивает их попиксельно в самой странице — тем же приёмом, что у golden.
Печатает число различающихся пикселей, максимум по RGB, задета ли alpha и
bbox.
Ветка временная.
Issue: #410
User-Visible: no
Съёмка скриншотов документации запускалась вообще без флагов детерминизма,
тогда как golden имел их с самого начала. Отсюда и плавающие кадры: включённое
субпиксельное сглаживание даёт разный результат от прогона к прогону, а
дельта — единицы уровней в RGB на сглаженных границах при неизменной alpha —
это его подпись, а не изменение продукта.
Измерено до починки: два прогона канонического workflow на одном и том же
dev SHA 184e0098, одном Chromium 151.0.7922.34 и одном oxipng 10.2.0 дали три
разошедшихся кадра из десяти — 06-device-editor, 08-room-card, 09-device-info.
Взяты те же три флага, что у golden: --disable-lcd-text снимает субпиксельное
сглаживание, --font-render-hinting=none — зависимость от хинтинга,
--force-color-profile=srgb фиксирует профиль. Добавлен reducedMotion: 'reduce'
и два кадра ожидания перед съёмкой: animations: 'disabled' гасит анимации, но
не гарантирует, что запланированный ре-рендер успел лечь в композитор.
Байтовый контракт приёмки не ослаблен ни в одном месте — чинится источник
шума, а не проверка.
Правка меняет capture.mjs, поэтому captureScriptSha256 в манифесте протух и
check-docs красный до пересъёмки. Пересъёмка неизбежна и по существу: с
выключенным субпиксельным сглаживанием переписываются все десять кадров сразу,
то есть приёмка идёт через --no-witnesses --reason.
Issue: #410
User-Visible: no
Чтобы измерить недетерминированность съёмки, нужен текст, который видно с
экрана: артефакт для этого не годится — его надо скачать и распаковать. Шаг
печатает sha256 каждого кадра, и два прогона одного SHA сравниваются
построчно.
Сначала измерение, потом починка (#410).
Issue: #410
User-Visible: no
Прогон показал, что дописать binding и bindingMode было мало: у объявленного
типа больше сорока обязательных полей, и рендер упал на следующем
недостающем — теперь на name внутри _markerDraft. Гоняться за типом руками
бессмысленно.
Смок открывает диалог штатным _openMarkerDialog() и переопределяет три поля.
Это заодно и доказательство, что дефекта поведения нет: продукт своим же
путём собирает объект, на котором рендер не падает.
Issue: #404
User-Visible: no
Гард «uncaught exception внутри карточки» жил в demo/serve.mjs с 2026-07-27 и
не срабатывал ни разу в самом частом случае. Счётчик читался синхронно, а
Playwright доставляет pageerror асинхронно по CDP: если исключение возникло
после последнего обращения смока к странице, счётчик к моменту проверки
нулевой, а browser.close() уносит недоставленное событие. В логе это видно
дословно — EXC печатается после результата и до OK.
finish() теперь делает round-trip по открытым страницам перед чтением
счётчика. Страницы регистрируются там, где создаются: ссылок на них у
finish(browser, out) нет, а менять сигнатуру нельзя — так её зовут 205
смоков.
Medium-1 жёлтого ревью ТЗ закрыт расширением, а не оговоркой. Страницы,
созданные смоком после launch(), регистрация в launchInternal не покрывает:
smoke_zoom_flash печатал своё EXC2 мимо счётчика, три страницы
smoke_svg_sandbox не имели слушателя вовсе. Документировать слепую зону в
задаче, которая существует ради устранения слепой зоны, значит закрыть issue,
оставив дефект. Наружу отдана одна функция watchPage(page): подписка и
регистрация неразделимы, иначе появится страница, чьи исключения считаются, а
доставки не ждёт никто.
Разрыв оказался шире, чем в ревью: проверка по всему набору нашла ещё два
файла со своей подпиской — smoke_cold_view_toggle и smoke_cold_view_vacuum.
Они не слепая зона, их страница приходит из launchColdView и уже
зарегистрирована, а свой счётчик они превращают в отдельное утверждение.
Поэтому инвариант сформулирован как «ни одна страница не создаётся мимо
гарда» и закреплён по всему набору, а не по двум названным файлам.
reportPageErrors() из #407 стал асинхронным: второй читатель счётчика обязан
ждать доставку так же, как finish(). Пять смоков получили await.
Фикстура smoke_danger_confirmation приведена к объявленному типу: без binding
и bindingMode _bindingHasHaPage падал на undefined.split(':') — два
исключения, которых гард не видел. Дефекта поведения нет, все 15 мест в src/,
создающих диалог, binding пишут; врала фикстура.
Два отступления от ТЗ, каждое по измеренной причине. Пробы лежат в
demo/guard/, а не demo/fixtures/: последний входит в корпус sourceFingerprint,
и каждый файл там объявил бы устаревшими бандл, скриншот-индекс и
golden-индекс — пробы же не касаются ни одного пикселя. Поведение
доказывается в job со браузером, а не в npm test: job «Фронтенд» браузеры не
ставит, и тест молча скипался бы — тот самый тихий успех, против которого вся
задача.
Issue: #404
User-Visible: no
Docs screenshots run 33521193808; all 10 pairs were visually reviewed. The explicit no-witnesses reason is recorded in screenshots.json because the prior rasterization baseline is no longer reproducible.
Issue: #403
User-Visible: no
Тот же дефект, что #408 у golden, и в моём же коде из #401. Порог свидетелей
считался от числа кадров, уцелевших на диске: rm docs/images/*.png плюс
объявить все десять через --expect-change — уцелевших ноль, порог ноль,
причины никто не спрашивает, а в манифесте остаётся {"witnesses":0,"floor":0}
без единого слова о произошедшем. Щель была описана в комментарии над самой
функцией и оставлена открытой.
Порог теперь от набора сценариев, свидетели — из тех, с кем есть что
сравнить. Разделение принципиальное: удаление кадров лишает доказательств, но
не должно снижать планку. Первичная съёмка идёт через --no-witnesses
--reason, как теперь и в golden.
Отдельного параметра размера, как в golden, здесь не нужно, и это не
небрежность: `ids` и есть набор — docs-accept.mjs передаёт DOC_SCREENSHOTS, а
verifyDocsCandidate до того отказывает, если набор сцен в кандидате не совпал
с ожидаемым. Пустой ids — отказ, а не ноль.
Попутно Low из #405: повторная приёмка неизменённого набора затирала
acceptance.declared пустым списком. След приёмки отвечает на вопрос «когда
эти пиксели приняли и что тогда объявляли», а обновление отпечатка пикселей
не меняет — значит и стирать ответ не должно. Прежний след сохраняется и
помечается lastWriteWasFingerprintOnly.
Заодно отказ по свидетелям теперь возвращает сами числа: вызывающий печатает
свой вердикт, и сочинять их заново ему не из чего.
Issue: #409
User-Visible: no
Порог свидетелей считался от числа сцен со статусом не missing-baseline, то
есть от эталонов, уцелевших на диске. Обход в одну команду: git rm
demo/golden/baselines/*.png — все сцены становятся missing-baseline, порог
обращается в ноль, свидетелей никто не требует, и чужая съёмка всей матрицы
принимается без единого следа причины в манифесте. Отказ
goldenAcceptanceRefusal этого не ловит: он требует объявить каждую новую сцену
в --expect-new, а объявить их все ничто не мешает.
Прежняя редакция объясняла ноль тем, что первичная съёмка свидетелей иметь не
может. Верно по факту и неверно по выводу: невозможность доказать среду не
отменяет требования, она требует сказать это вслух. Теперь и первичная съёмка
идёт через --no-witnesses --reason, а причина уезжает в манифест эталонов.
Размер матрицы стал обязательным параметром, а не выводится из отчёта: у
частичного прогона (run.mjs --only=…) results короче матрицы, и порог просел
бы молча — тот же дефект в другой одежде. Отсутствие параметра — отказ.
Формула не менялась: она общая с docsWitnessFloor и обязана такой остаться.
Менялся источник счётчика. На обычной приёмке ничего не меняется: при 143
эталонах порог был и остался 10.
Issue: #408
User-Visible: no
Счётчик исключений внутри карточки живёт в demo/serve.mjs, и читала его одна
функция — finish(). Шесть смоков её не вызывали вовсе: у трёх своя развязка
(`if (!ok) process.exit(1)`), у двух throw из try/finally, у
smoke_entry_stale ни того ни другого. Необработанное исключение во время этих
шести проходило незамеченным всегда — в лог печаталось EXC, а прогон
оставался зелёным.
smoke_entry_stale был хуже остальных: он складывал неудачи в _failures через
check/checkAll, но их никто не печатал и код возврата не выставлял. То есть
смок не мог провалиться в принципе — ровно паттерн «печатали булевы значения
и всегда выходили нулём», который шапка serve.mjs описывает как исправленный
в 2026-07-27.
Добавлен reportPageErrors(): тот же вердикт, что у finish(), для смоков со
своей логикой выхода. Каждый из шести теперь вердикт запрашивает, а
smoke_entry_stale получил finish() и вместе с ним настоящий код возврата.
Вердикт обязан ОСТАНАВЛИВАТЬ, а не только помечать. Первый заход выставлял
process.exitCode, и отрицательный прогон напечатал «FAILED: 1 uncaught
exception(s)» и следом «OK deep-link: …»: код был верным, вывод
противоречивым, а читают вывод.
Доказано отрицательным прогоном, а не рассуждением: на ветке
experiment/407-negative smoke_deeplink получил намеренное исключение внутри
карточки, шард 2/3 упал с exit code 1, в логе FAIL и FAILED без строки
успеха. Ветка удалена.
Гейт против повторения — test/smoke-harness-contract.test.mjs: он падает,
если смок не запрашивает вердикт или запрашивает, не останавливаясь. На
origin/dev до починки он находил ровно шесть файлов, после — ноль.
Issue: #407
User-Visible: no
hp-confirm sat at the end of a chain of early returns, so in onboarding
(«no spaces yet»), in the fixed-floor states and without a space it did
not exist at all: the trash button next to a saved plan was dead and the
promise hung forever, because the decision event had no source in the
DOM. An already open dialog vanished the moment the card slipped into
one of those branches, leaving the caller waiting for a resolution that
could never come. Before #32 a browser confirm() worked there.
render() is now a wrapper: it takes the body — the old chain, unchanged,
as _renderBody — and renders the confirmation beside it. That fixes the
class rather than the instance: a branch added later cannot lose the
dialog again. noChange and nothing are passed through untouched, since
neither may be wrapped in a template; in those states _confirmDanger
refuses the request outright instead of leaving it pending, which is the
honest answer while the card is not on screen and the user has pressed
nothing.
_tapConfirm and _vacCalConfirm deliberately stay where they are. They
share the same final branch, but they have no promise (a synchronous
exec, a dialog closed by hp-close), so the defect cannot occur there,
and their entry points require a drawn plan.
Proven by a separate smoke rather than an addition to
smoke_danger_confirmation: that file keeps deliberately incomplete
dialog fixtures open, and the extra re-renders this change needs make
them throw. The new smoke runs under touch emulation, because
TOUCH-SUPPORT § Safety floor forbids bypassing a destructive
confirmation and the broken branch pierced that floor on finger as
surely as on mouse. Reverting the wrapper reddens it.
User-Visible: yes
Issue: #402
Правило приёмки скриншотов было про место: снимать только в CI. Обоснование
измерено — съёмка в другом окружении переписывает файлы без содержательных
изменений, в #231 два из девяти на 7–8 байт, набор с беты все девять. Но
держалось правило на комментарии, а не на механизме: кандидат проверялся на
самосогласованность и принимался целиком, ни разу не сравниваясь с тем, что
лежит в репозитории.
Цена видна на #390: правка типов, которая физически не может сдвинуть
пиксель, потребовала прогона workflow, а затем правки одиннадцати полей
манифеста руками.
Теперь правило про доказательство, и оно то же, что у golden с #334: среда
доказана, если каждый кадр, который менять не собирались, совпал с
закоммиченным байт-в-байт. Расхождение растеризации спрятать нельзя — оно
задевает все кадры с текстом сразу. Снимать можно где угодно, включая WSL;
принять получится только оттуда, где кадры воспроизводятся, и перестанет
получаться в тот день, когда обновятся шрифты.
Остальное следует из того же правила: намерение объявляется
--expect-change, необъявленное расхождение останавливает приёмку,
объявленное без расхождения — тоже (ложная декларация обесценивает список),
заменяются ровно объявленные файлы, а тотальная перерисовка требует
--no-witnesses --reason, и причина уезжает в манифест.
Частый случай закрылся сам: ничего не объявлено, все кадры совпали —
принимается один манифест, руками ничего писать не надо.
Проверено шестью сквозными прогонами на подделанном артефакте, не только
юнитами: идентичный кандидат, необъявленное расхождение, объявленное,
молчаливая декларация, тотальная перерисовка без причины и с ней.
Issue: #401
User-Visible: no
CODE-REVIEW-400-r1 Medium: the registered mutant edited a comment, not
the order — it could not reproduce the regression AC1 exists to catch.
That is the same defect class this issue is fixing elsewhere, in my own
guard.
The order is now HANDLE_PAINT_ORDER, a named constant, because it IS the
hit priority rather than an accident of where the blocks sit in the
template. The mutant flips that constant, so it reproduces exactly the
behaviour the audit found.
Also: smoke_furniture picked the SE corner as handles[3], an index that
silently depended on the old paint order — CI shard 3 went red on four
checks. It now selects by role (corner handles, third of four), which is
what the test actually means.
User-Visible: no
Issue: #400
(1) Corner and edge handles carry the same hit radius (1.8 % of the
view), so on furniture narrower than 4·hr — a 40 cm cabinet — the two
circles overlap and whichever is painted last takes the tap. Edges were
painted last. Corners are now, because a side handle scales one axis
while a corner scales both, and the object is small exactly when
proportional resize matters most. The visible beads are unchanged.
The audit called this 'proportional resize becomes unavailable'; the
measurement says otherwise and the spec records the correction: the
corner centre lies outside the edge circle, so the corner was reachable
— its area was halved, not lost. A polish, not a bug, and worth fixing
because it is one line of ordering.
(2) Alignment guides in the devices mode excluded the dragged marker by
_drag, which has been null there since #74 moved device dragging into
_deviceDrag. So the marker being moved was among its own candidates.
Nothing looked wrong because a point always matches itself within
tolerance — the guide was drawn from the marker to itself, visually
identical to an honest one, and the smoke asserted only guides() >= 1.
The smoke now compares the candidate lists with and without the drag and
demands exactly one removed entry.
(3) The 38 settings-help strings stay in the initial chunk, and that is
now a recorded decision rather than an oversight: measured 2 654 B gzip,
0.9 % of the ceiling, against splitting a synchronous dictionary in two,
a second request on first hint, and a second source for the key type
derived from en.json (#391). docs/ARCHITECTURE.md says so, with the
number that would justify revisiting it.
Both mutants run by hand: reverting the paint order reddens the 40 cm
probe while the 160 cm one stays green; restoring _drag reddens the
guides smoke.
User-Visible: yes
Issue: #400
CODE-REVIEW-399-r1 High: the test named after AC5 called
installsPythonDeps on string literals and never executed the directory
walk it was supposed to protect. The reviewer showed what that costs:
restoring the old hardcoded pair of real names and dropping a third
workflow with unpinned installs into .github/workflows left all ten
checks green — the exact scenario AC5 describes went undetected.
The walk is now a function taking the directory, so the test can run it
for real: it builds a temporary directory with three files (a pinned
installer, a workflow that installs nothing, and a rogue one) and
asserts on what the scanner returns. Reverting the walk to a list of two
real names now reddens this test, verified by hand.
The mutant is sharpened accordingly: it substitutes the two-name list
instead of a one-name list. The old form failed on an unrelated
assertion about directory size, so it proved nothing about the scan
itself — while the two-name form is indistinguishable from correct code
on today's tree, which is what makes it the likely regression.
User-Visible: no
Issue: #399
Three claims a green backend used to make, each slightly wider than the
truth — and #392 happened in exactly that gap.
The frontend pin said 20260826.1 next to homeassistant==2026.8.3, whose
package_constraints.txt requires 20260729.7: a combination that exists
in no HA release. It was never derived from anything — someone once
picked it. It is now taken from the constraints, the source is named in
the file, and a test holds both numbers together so raising HA cannot
quietly desync them.
ruff's include declared three trees while CI linted one. Narrowed the
declaration rather than widening CI: the debt in scripts/ and
tests_backend/ (56 findings, mostly E402/I001, plus 7 B023 and 5 B017)
has its own cost and its own decisions, and belongs in its own task, not
in a visibility fix. test/lint-scope.test.mjs now compares the two, so
they can only move together.
The pin check skipped a workflow when it found neither the package name
nor the requirements path — and both vanish together the moment someone
returns to Defaulting to user installation because normal site-packages is not writeable, i.e. the gate switched itself off
under precisely the change it exists to catch. It now walks the whole
.github/workflows directory and decides per file by a positive sign: if
a file installs python packages, it must install them from the pins
file. Verified by dropping a rogue workflow into the directory — it
reddens without touching any list.
Three mutants registered and each run by hand.
User-Visible: no
Issue: #399
The guard introduced by #394 matched the literal
sys.modules['custom_components... and therefore never looked at
pure_imports.py, which writes through a variable — the third instance of
the #389 class walked straight past the check created for it.
The guard now inspects the write itself and decides by the key: a whole
literal or the literal head of an f-string is safe unless it starts with
custom_components (that is how tests register homeassistant.*, hp_pure.*
and houseplan.trails); anything else — a variable, a concatenation,
setdefault/update — counts as a violation whenever the file is able to
name the package at all, i.e. contains a custom_components. literal. A
file that never names the package cannot poison it through a variable,
so restoring a snapshot stays legal.
load_pure now removes what it registered. Removing its own name is not
enough: relative imports pull neighbours in, so junction_limits leaves
wall_segment_model and coordinate_canonicalization behind. It removes
the whole custom_components difference accumulated during exec_module,
in a finally, and a repeated call still works.
pure_imports.py is a named exemption of the static guard precisely
because that guard cannot see the cleanup — so the cleanup is proven by
an executable test instead, and the mutant pure-imports-stops-cleaning
reddens it. Both mutants were run by hand.
User-Visible: no
Issue: #398
CODE-REVIEW-397-r1 Medium: AC3 named the second reader of the same
value — _loadFromServer via _adoptStructuralResponses — and nothing
exercised it. Adding the scenario turned out to be less mechanical than
it looked, and both obstacles are worth recording:
The reconnect path reads BOTH answers, and a differing config clears the
history for its own reason (configChanged). The fake server now echoes
the config the card already holds, so the check answers the layout
question it claims to answer.
The first version of the probe used a round 0.42, which canonicalization
leaves untouched — the check passed with and without the fix, i.e. for
the wrong reason. The probe now starts from a non-canonical position
(0.024999999999999942, which snaps to 0.025), and the scenario runs
immediately after the write, before any reload can align the two sides.
Verified by removing the fix: five checks red, now including
reconnectKeepsHistory and deleteEchoKeepsHistory. Both were green in the
weaker version — which is exactly what the reviewer's Medium was about.
User-Visible: no
Issue: #397
B3: _persistDevicePlacement sent canonicalizePosition(...) to the server
and left the raw value in _layout, then recorded the fingerprint over
that raw snapshot. Canonicalization is not identity — it snaps to the
lattice — so 39 of 115 pixel-derived coordinates differ, and the next
_reloadLayoutOnly or _adoptStructuralResponses saw its own write as a
remote edit: history cleared, _layout replaced. The old _persistLayout
wrote the canonical value back; the per-device path introduced by #74
lost that line.
M1: the smoke that was supposed to prove AC10 assigned
serverLayout = structuredClone(c._layout) right before the reload —
erasing by hand the very divergence it existed to catch, so it could not
fail. The fake WS already stores what went over the wire; the
assignment is gone and the check now reddens on the unfixed code
(verified: three checks red without the fix, including this one).
Also proven, because the fix touches their neighbourhood: the echo of a
DELETE keeps the history (the branch removes a key rather than replacing
a value), and an in-flight write still wins the merge against a server
answer holding the old position.
One existing assertion was loosened deliberately: undo now restores a
position that may differ from the raw one by the lattice snap (<1e-9 of
the plan). That is the point of the fix — local and server agree — so the
equality is stated to that precision, with the snap size pinned
separately so a real drift would still fail.
User-Visible: yes
Issue: #397
Three findings of the v1.70.0-beta.1 audit, all on the transition path
added by #82, all of the same shape — the new path did not inherit a
property the old one had.
B1: persisting the zoom moved into _settleCameraTransition only, and a
cancellation never settles. Touching the plan mid-flight — the literal
scenario of the issue — froze the shown frame and threw it away; before
which kind it is: the user one (_stagePointerDown) persists the frame
that stays on screen, the eleven structural ones keep writing nothing.
The distinction is now also written down in spec #82 §13, which had one
line for both.
B2: the anchor was read from the presented (lagging) frame while the
zoom accumulated from the target, so a six-notch trackpad series walked
the point under the cursor 17 px away — against §10's own promise. Both
now come from the same state. Spec §10 said to use the presented frame
and to keep the anchor within 0.5 px; those two are incompatible, and
the paragraph is corrected rather than left as a trap.
M2: the feather freeze keyed on the two gesture flags, which an
animated transition does not set, so every tween frame rebuilt the blur
region. It keys on 'the camera is still' now.
Guards: unit tests pin the anchor at 1e-9 across 8/16/33 ms series and
prove zoom accumulation is untouched; the smoke checks the shown zoom is
the stored one, that a structural cancellation stores nothing, and that
the anchor holds; three mutants (cancel-loses-zoom, anchor-from-
presented, feather-thaws) were run by hand and each reddens.
User-Visible: yes
Issue: #396
The new typing step failed on its first CI run — not on our code:
mypy parses the sources of the installed homeassistant, and after #392
that is HA 2026.8.3 on python 3.14, which uses 3.14-only syntax
(parenthesis-free `except`). With python_version = 3.13 mypy stopped at
a syntax error in someone else's file before reaching a single module of
ours, which is exactly the silent-nothing the gate exists to prevent —
except loud.
ruff keeps target-version py313 deliberately: it lints OUR sources and a
lower target only withholds newer-syntax suggestions, while mypy has to
read the dependency tree the runner actually installs.
User-Visible: no
Issue: #42
r6 Medium: AC4 was measurable only on a developer's machine — no
workflow invoked mypy, so a typing regression in any of the six
allowlist modules reached dev unnoticed while the issue claimed
measurable backend quality. Coverage and lint had continuous gates;
typing had a text comparison of a committed list.
The backend job now runs mypy right after ruff, from the same pinned
dependency file (mypy==2.3.1 — an unpinned checker would redden on code
that never changed). The step derives its module list from the
pyproject.toml strict allowlist instead of duplicating it, because a
drifted duplicate is a green step checking the wrong modules, and it
refuses an empty list rather than passing silently.
Guarded twice: a contract test pins all three facts (pinned checker,
a step that really invokes it, list read from pyproject) and the new
typing-gate-stops-running mutant reddens when the invocation is
neutered.
User-Visible: no
Issue: #42
Rebase resolution: dev's #392 introduced tests_backend/requirements.txt
(python 3.14, phcc 0.13.357, homeassistant 2026.8.3) — exactly the
single-source-of-pins AC of this issue, so the duplicate
requirements_test.txt is dropped and both workflows keep installing from
the #392 file; ruff is added there for the lint step. The backend
reuse key no longer names the dead file (tests_backend/ as a root
already covers the new one); ARCHITECTURE.md points at the real path.
User-Visible: no
Issue: #42
M1: the AC5 scanner parses the (field, code, message) literal tuple in
validation.py structurally instead of naming the two known codes — a
third tuple entry with an unregistered code now fails the registry test
(verified with an injected invalid_ghost_entity_mutant_probe), and a
tuple whose string count is not a multiple of three refuses instead of
guessing.
M2: the backend reuse key now includes its direct job inputs introduced
by this issue — scripts/backend-coverage-baseline.txt (the threshold the
comparison step reads), requirements_test.txt (the pip source) and
pyproject.toml (ruff/mypy config) — verified: the key changes when the
baseline changes and is restored byte-for-byte with the file.
User-Visible: no
Issue: #42
Rebased onto dev with #82 (camera transitions), #74 (marker undo) and
the re-accepted goldens; bundle trees are rebuilt from the rebased
sources and the screenshot capture is retaken on this HEAD — manifest
AND all PNGs committed together.
User-Visible: no
Issue: #42
87.2% line coverage, taken from the first fully green backend CI job of
this branch (run 33321192996, coverage.xml line-rate 0.8716) — replaces
the 80.0 placeholder as promised before the verdict. The gate refuses
any run below baseline minus 0.1.
User-Visible: no
Issue: #42
The harness test still parsed the legacy 'space=... opening=...
margin_cm=...' string; #42 replaced that message with structured JSON
details (the client localizes from the code and reads the fields). The
assert now parses the payload and checks the same three facts.
User-Visible: no
Issue: #42
Same defect class as #389: _const() planted bare ModuleType stand-ins
for custom_components(.houseplan) and never removed them, so the HA
harness running later in the same pytest process saw a package without
async_setup — 85 test_ha_* failures with 'No setup or config entry
setup function defined'. const.py imports nothing, so the loader needs
no package context at all: load it by file path under a standalone
module name and leave sys.modules untouched (verified: no
custom_components* keys after _const()).
User-Visible: no
Issue: #42
The previous commit refreshed docs/images/screenshots.json but left the
re-rendered PNGs out of the index — CI compared the committed manifest
against the committed (stale) images and failed on every hash.
User-Visible: no
Issue: #42
Bundle trees are rebuilt from the rebased sources (the pre-rebase build
commit was dropped during the rebase and the trees recreated), and the
doc capture is retaken on the rebased HEAD.
User-Visible: no
Issue: #42
pytest-homeassistant-custom-component 0.13.317+ require python >=3.14
while the CI runner is 3.13: 0.13.316 is the newest installable release
and resolves to the same homeassistant==2026.2.3 the previously green
unpinned pip line produced. voluptuous is unpinned again — homeassistant
pins 0.15.2 itself and a 0.16.0 pin deadlocks the resolver.
The screenshot capture is refreshed after the any-gate source fix of
e8243d1e, which changed src/ without recapturing (the standing rule:
every src commit needs capture + check-docs).
User-Visible: no
Issue: #42
pytest-homeassistant-custom-component transitively pins the pytest
family and the homeassistant version — re-pinning them deadlocked the
resolver (the sandbox pip index is py3.10-bound and suggested stale
versions). The structured-details branch no longer copies the legacy
(item: any) annotation the new-code any gate rightly rejects.
User-Visible: no
Issue: #42
Tooling: requirements_test.txt becomes the single source of backend CI
dependencies; pyproject.toml configures ruff (E/F/B/I, E501 excluded by
decision) and mypy strict for a grow-only allowlist of six pure modules
(junction_limits annotated to pass). The 42 substantive ruff findings
are fixed — the B023 loop-variable closures bind their variables as
parameter defaults instead of hiding behind noqa, and every remaining
noqa carries a reason (guarded by a test).
Errors: const.ERROR_CODES / ERROR_CODE_FAMILIES formalise the stable
contract; the scanner test proves every emitted code across BOTH paths
(send_error literals; class attrs, literal and variable-passed
MarkerControlError codes, f-string families) is registered and has a
localized message — 22 missing backup.error.* keys added in all four
languages. invalid_passage_fields / invalid_partition_opening_jamb_margin
ship structured JSON details (legacy format read-compat for one beta),
and _errText renders code-first: unknown codes localize, raw English
messages go to the console.
CI: the backend job lints with ruff, refuses a silently skipped HA
harness (import + collect threshold), measures branch coverage over
pure+harness, fails below the committed baseline and uploads
coverage.xml. quality_scale: docs-troubleshooting/examples honestly
done, test-coverage/strict-typing carry staged progress.
User-Visible: yes
Issue: #42
The p.7 limit bucketed every review document of an issue together, so a
task that honestly passed both stages was refused for having passed
them: #42 has 4 SPEC-REVIEW plus 3 CODE-REVIEW documents — 4 and 3
rounds per stage, both inside the budget — and its already-published
GREEN r5 verdict could not publish its own artefact for three runs in a
row, blocking the merge each time.
The counter is now keyed by stage and issue, and the refusal names the
stage. The threshold itself is unchanged: seven documents of one kind
still fail, and the comment above the constant already said what the
number means — the round budget of ONE stage.
User-Visible: no
Issue: #395
Record the unchanged screenshot set against the typed editor runtime source after verifying the capture output and preserving the reviewed PNG bytes.
Issue: #391
User-Visible: no
Remove legacy any casts from device inbox, marker, and geometry preflight translation calls. Refresh the moved preflight mutation anchor.
Issue: #391
User-Visible: no
Чистые тесты подменяли custom_components и custom_components.houseplan
пустышками и не убирали их никогда. В CI это не стреляло только потому, что
настоящий пакет успевал импортироваться из файла, который идёт раньше по
алфавиту: условие «если ещё не импортирован» оказывалось ложным. То есть
корректность HA-харнесса держалась на именах файлов в каталоге, и хватило бы
переименования, чтобы получить #389 заново.
Подмена переехала в conftest и стала условной по единственному честному
признаку: есть Home Assistant — работаем с настоящим пакетом и не подменяем
ничего; нет — HA-тесты и так пропущены, ломать нечего.
Первым заходом я делал подмену обратимой прямо в тестах, контекстным
менеджером. Замер показал, что так теряется работоспособность
test_wall_segment_model в песочнице: он импортирует пакет обычным способом и
жил как раз за счёт чужой пустышки. Развилка в conftest сохраняет оба
окружения и убирает зависимость от порядка файлов.
Проверено в обе стороны: в песочнице 240 passed, а в эмуляции «HA есть»
объект пакета после прогона тот же, что был до.
Issue: #394
User-Visible: no
Замер по AC5 #392 изменил решение. Я собирался заморозить набор на том, что
резолвилось (HA 2026.2.3, февральский), и вынести обновление в отдельную
задачу «на потом» — потому что шесть месяцев дрейфа API вслепую в dev не
отправляют.
Проверил на ветке experiment/392-py314: Python 3.14, phcc 0.13.357,
homeassistant 2026.8.3 — `450 passed, 2 skipped`. Ровно то же, что на старом
наборе, ни одного падения. Обновление оказалось бесплатным, и держать гейт на
февральском HA после такого замера нельзя.
Взята 0.13.357, а не последняя: она последняя, которая пинует стабильный
2026.8.3, дальше пинуются беты. Гейт на бете невоспроизводим — бету могут
перевыпустить под тем же номером.
pytest не закреплён намеренно: phcc задаёт совместимый диапазон сам, жёсткий
пин с ним конфликтует (ResolutionImpossible на 9.0.0, проверено).
Issue: #392
User-Visible: no
Два независимых хвоста из разбора #389, оба про то, что «зелёный» значит не
то, что читается.
#392. Оба места, где поднимается HA-харнесс, ставили зависимости без единой
версии. Python при этом закреплён на 3.13, а pytest-homeassistant-custom-
component с 0.13.348 требует 3.14 — резолвер молча уезжал на 0.13.316, а та
тянет homeassistant 2026.2.3. Интеграция полгода проверялась против
февральского HA, и состав окружения мог измениться без нашего коммита.
Версии закреплены в tests_backend/requirements.txt ровно те, что резолвились
30.08; шаг печатает установленное в лог. Это остановка дрейфа, а не
обновление: переход на 3.14 и свежий phcc — отдельная задача с отдельным
измерением.
#393. test_trails.py клал каталог пакета в sys.path при коллекции — на всю
сессию, включая HA-харнесс. Модули интеграции становились импортируемыми ещё
и как модули верхнего уровня, то есть один файл мог оказаться в sys.modules
дважды. Вставка при этом не работала ни на что: TrailBook берётся чтением
текста и exec среза, а не импортом. Убрана вместе с мёртвым spec.
Гейт статический, по исходникам: он ловит намерение, а рантайм поймал бы
последствие и только при сегодняшнем порядке тестов.
Issue: #392
User-Visible: no
Саморевью по указанию владельца, с оговоркой о независимости. Одна находка
Low на себя: утверждение «эмит тот же» в сообщении cbf5cc1b неверно —
побайтовая сверка нормализованных сборок показала расхождение в одном месте.
Поведение при этом тождественно, разбор в документе.
Отдельно разобрано, почему восемь разошедшихся golden-сцен не из этого
коммита.
Issue: #390
User-Visible: no
The same eight changes are present on origin/dev and are unrelated to the smooth camera implementation. Review confirmed the catalog actions, toolbar undo/redo controls and dialog help/status icons are the intended current dev UI. Accept only those named Linux candidates; keep 139 existing scenarios unchanged.
Issue: #82
User-Visible: no
Release: v1.70.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33319326145
Wait for camera transitions in legacy smoke and golden scenarios, and render the far-object hint state even when fitting is a camera no-op.
Issue: #82
User-Visible: no
Живой прогон #2157 показал огрех формулировки: база диапазона (#388)
представилась заголовком «База классификации (#387)». Текст верный, ссылка
чужая — читатель уходит не в тот issue разбираться, почему диапазон такой.
Заголовок теперь следует режиму, а не общей ветке кода. Закреплено тестом на
оба режима и обе формулировки.
Issue: #388
User-Visible: no
Правка типов в #390 пересобрала бандл, а в него вшит отпечаток исходников —
скриншот-индекс стал формально протухшим, хотя ни один пиксель измениться не
мог: аннотации типов стираются, эмит тот же.
Это не рассуждение, а измерение. Канонический прогон «Скриншоты
документации» #130 (workflow_dispatch, ref=dev) снял кадры пином и вынес
вердикт:
--- изменившихся PNG: 0
--- Chromium: было «151.0.7922.34», стало «151.0.7922.34»
--- oxipng: было «oxipng 10.2.0», стало «oxipng 10.2.0»
ВЕРДИКТ: ничего не изменилось, принимать нечего.
Единственное, что изменилось в артефакте, — screenshots.json. Здесь ровно
он и обновлён: sourceFingerprint и десять sourceSha256 сцен. Хеши картинок,
версия браузера и версия упаковщика не тронуты — проверено полем за полем.
Issue: #390
User-Visible: no
Красный backend на dev — это два независимых дефекта, и ни один не был виден
в диффе.
Первый, 85 падений. scripts/dump-config-schema.py подменяет
custom_components и custom_components.houseplan пустышками, чтобы прочитать
схему без Home Assistant, и оставляет их в sys.modules навсегда. Вызывает его
в том числе pytest: tests_backend/test_config_schema_manifest.py идёт первым
по алфавиту. Дальше HA просил у загрузчика custom_components.houseplan,
получал пустышку без async_setup и отказывался поднимать интеграцию — «No
setup or config entry setup function defined». Каждый тест харнесса падал на
_setup с «assert False», и ни один не намекал на причину: подмена работает
для подмодулей, потому что __path__ у пустышки настоящий.
Подмена не убрана — без неё скрипт не выполнит свою задачу. Она стала
обратимой: sys.modules снимается до и возвращается после, включая отсутствие
ключа. Обратимость закреплена тестом.
Второй, 1 падение. test_furniture_flip_flags_survive_coordinate_
canonicalization_unchanged требовал CONFIG_SCHEMA(result) == result, но схема
на минимальном конфиге достраивает markers и settings и приводит целые к
float — тождества там нет и не было. Проверяется теперь неподвижная точка:
повторная валидация не меняет канонический вид, а флаги её переживают.
Диагностика шла через CI: в песочнице HA-харнесс не поднять, поэтому
временная ветка experiment/389-diag печатала, что именно видит загрузчик.
Она показала модуль без __file__ и без единого атрибута — namespace-подобную
пустышку, — и это вывело на подмену.
Issue: #389
User-Visible: no
Пять мест, найденных при разборе #388: они проехали мимо гейта #342, пока
диапазоны были узкими, и он их больше не покажет.
Кэш климата (houseplan-card.ts) сравнивается только по ссылке, значение не
читается ни разу — поэтому hass стал `unknown`: он и запрещает случайно
воспользоваться содержимым, и не врёт про форму объекта, которую HA нам не
обещает. Правила и маркеры типизированы по-настоящему, их типы известны.
Реестр HA (houseplan-editor-runtime.ts) описан минимальной структурной
формой: код читает ровно два поля и оба защищённо, так что форма честнее
`any` — она говорит, на что код опирается.
Ключ i18n `device_inbox.reason_excluded_integration` не нуждался в приведении
вовсе: он есть в словаре. Шаблонный ключ приведён к `I18nKey`, как уже
сделано в этом файле строкой 8013 — это утверждение о пространстве ключей, а
не отключение проверок; `as any` заодно снимал контроль и со второго
аргумента.
Поведение не меняется: аннотации типов стираются, эмит тот же. Бандл
пересобран, потому что в него вшит отпечаток исходников.
Issue: #390
User-Visible: no
Правка #388 уронила dev, и виновата подмена предиката. Я взял доказательством
`conclusion=success`, то есть оправдательный вердикт. Гейтам диапазона нужен
другой факт: судили ли этот коммит вообще. Упавший прогон коммит судил —
вердикт вынесен, автор его видел; переоткрывать такой коммит диапазоном не
надо. Не судил только отменённый.
Цена ошибки была наглядной. Backend на dev красный несколько дней по своей
причине (test_ha_import_export), успешных прогонов нет вовсе, поэтому база
уезжала на десятки коммитов назад. На 83d646c — docs-коммите — гейт
«новый код не добавляет any» предъявил 5 чужих находок из e4e1e370 и
8d431d6d и уронил frontend. Ровно тот сценарий обвинения невиновного, ради
которого делался #386, только устроенный мной.
Теперь `judgedShas` считает судимыми завершённые прогоны с любым вердиктом,
кроме cancelled, а `greenShas` остаётся для классификации (#387): там вопрос
другой — доказано ли, что тяжёлые гейты на этом дереве ПРОШЛИ. Два вопроса,
два предиката, и путать их дорого.
Запрос к API стал `status=completed` — надмножество, нужный предикат
применяет скрипт.
Issue: #388
User-Visible: no
#387 закрыл классификацию — какие job запускать. Здесь остаток того же
дефекта: гейты, которые судят сам диапазон коммитов. Провенанс, процессный
гейт и «новый код не добавляет any» брали диапазон от головы предыдущего
пуша, а concurrency отменяет прогон предыдущего пуша штатно. Тогда его
коммиты не судит никто: свой прогон отменён, а следующий пуш сравнивает уже
с ними. Окно не закрывается никогда.
Уязвим был прямой пуш в dev — основной режим конвейера. На ветках дефекта
нет: no-new-any там всегда считает от merge-base, а resolveValidationRange
подменяет осиротевший before на origin/dev (#315).
База стала последним предком с успешно завершённым Validate. Фолбэк, когда
такого нет, сознательно оставлен прежним — before, но с пометкой в summary
«диапазон недоказуем». Расширять диапазон здесь нельзя: гейт, который сам
красит прогон, лишил бы следующий пуш зелёного предка и запер dev в
красноте навсегда. Фолбэк обязан не зависеть от собственного успеха гейта.
Дыра сужается с «всегда, когда прогон предыдущего пуша отменён» до «когда
во всём окне обхода нет ни одного успешного прогона».
Находки no-new-any теперь называют коммит, добавивший строку: диапазон стал
шире, и без имени источника сообщение обвиняло бы того, кто пушнул
следующим, — ровно то, что чинили в #386 для golden.
Issue: #388
User-Visible: no
Диапазон классификации брался от `github.event.before` — головы предыдущего
пуша. Это допущение «до этого уже проверено», и оно неверно ровно тогда,
когда прогон предыдущего пуша не завершился. А не завершается он штатно:
concurrency отменяет его следующим пушем.
На #86 (r5) это дало ложный зелёный: push 04da7eb1 тронул dist/** и
frontend/**, его прогон отменили через три минуты; следующий push fa146fb1
тронул только docs/images/**, классификация сравнила эти два коммита и
выставила frontend=false. Job «Фронтенд», а за ней golden, smoke и backend
оказались skipped — прогон при этом success. Маркеры переиспользования эти
гейты тоже не подтверждали: `Cache not found` по всем четырём.
Теперь база — самый новый предок HEAD, для которого Validate ДЕЙСТВИТЕЛЬНО
завершился успешно; если такого нет, диапазон расширяется до merge-base с
dev, то есть до всего вклада ветки. Работает по индукции: цепочка узких
диффов покрывает всё изменённое с последней настоящей проверки, а одно
незавершённое звено теперь расширяет диапазон, а не сужает.
Недоступность API не роняет job: пустой ответ опускает базу до merge-base,
то есть в сторону большего объёма проверок.
Защита от force-push (#347) сохранена: механизм, из-за которого merge-base
врал на переписанной истории, до конца не разобран, и снимать защиту, не
объяснив её, — способ получить #347 второй раз.
Issue: #387
User-Visible: no
Refresh the canonical documentation frames after rebasing #86 onto the
current dev branch and visually reviewing all ten scenarios.
Issue: #86
User-Visible: no
Capture the general-settings help surface at 390 CSS pixels and DPR 2 in
both themes, assert its viewport contract, and teach the golden runner to
select a renderer scale per scenario.
Issue: #86
User-Visible: no
Exercise the effective Chromium renderer contract for 200% browser zoom and
assert that the trigger and tooltip stay visible, the dialog does not gain
horizontal overflow, and opening help leaves stage geometry unchanged.
Issue: #86
User-Visible: no
Accept the complete Linux Chromium documentation set captured after rebasing
change also replaces the uncompressed dev set with the pinned oxipng output.
Issue: #86
User-Visible: no
Release: v1.70.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33306281294
Exercise the real lazy onboarding runtime, verify all five space help controls,
and prove that opening help neither mutates the draft nor eagerly loads the
editor runtime.
Issue: #86
User-Visible: no
Accept the six intentional settings-help layout changes from the complete Linux candidate and the two pending furniture-transform scenarios already merged for #383. The remaining 137 baselines stay unchanged.
Issue: #86
User-Visible: no
Release: v1.70.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33305056892
Add the agreed Party 1 help controls to general, space, marker and device-catalog settings in all four locales, including cold onboarding parity and regression coverage.
Issue: #86
User-Visible: yes
Refresh Party 1 against the current four-locale UI and replace the retired Boundary affordance with zero-thickness wall semantics.
Issue: #86
User-Visible: no
M1: the room-climate cache keys on the STORED exclusion array reference
(stable across renders; _excluded builds a fresh Set per call), so
saving new Discovery filters recomputes climate immediately instead of
waiting for an unrelated hass tick.
M2: a device without a platform-bearing entity takes its integration
name from the identifier domain — the excluded reason can no longer
render a raw {integration} placeholder (if the exclusion matched,
at least one of the two names exists).
User-Visible: no
Issue: #44
Light grouping and the excluded-integrations list move from hidden keys
to a Discovery-filters section on the catalog's Available tab: a toggle
(unset = on, the legacy behaviour), searchable integration chips with a
Restore-recommended reset (defaults stored as key absence), and
appear/disappear counters computed by diffing the REAL
seedHiddenBindings/buildDevices outputs — no second copy of the filter.
Saving writes settings once over the ordinary expected_rev path. Every
excluded candidate now names its integration in the catalog. Room
climate follows the same user exclusions through the single
effectiveExcludedIntegrations resolver (spec H2); explicit climate
opt-in stays stronger. The field registry passports both keys as
current supported settings.
User-Visible: yes
Issue: #44
M1: the AC7 no-import test scans the whole src tree for the CURRENT dump
name (the old assertion checked the pre-rename string; proven by
execution — a planted fetch now turns it red).
M2: deduplicate the #33 paragraphs in both changelogs and ARCHITECTURE.
M3: the auditor's exit-3 statuses match the spec contract exactly
(migrate-*/deprecated-read); decision-required is live behaviour
awaiting #44, drop-on-validation is the backend's own job.
User-Visible: no
Issue: #33
repo-hygiene caught it: HACS globs *manifest.json over the whole clone
and rejects a repository with two. The dump is scripts/config-schema.json.
User-Visible: no
Issue: #33
The Voluptuous schema is now dumped into a deterministic committed
manifest (265 leaf paths); a pytest fails on drift. A parity test
compares manifest enums with the exported frontend const lists through
a machine-readable allow-list that also refuses to rot. The field
registry gains passports (allow-extra / lovelace-card), enforcedBy
citations for mechanisms that already shipped, and passports for the
v1.68-v1.69 fields; a completeness test bans dead decisions. Lifecycle
fixtures (oldest / current / future) prove lossless loading, and the
config auditor gains the 0/3/2 exit-code contract.
User-Visible: yes
Issue: #33
The Voluptuous schema is now dumped into a deterministic committed
manifest (265 leaf paths); a pytest fails on drift. A parity test
compares manifest enums with the exported frontend const lists through
a machine-readable allow-list that also refuses to rot. The field
registry gains passports (allow-extra / lovelace-card), enforcedBy
citations for mechanisms that already shipped, and passports for the
v1.68-v1.69 fields; a completeness test bans dead decisions. Lifecycle
fixtures (oldest / current / future) prove lossless loading, and the
config auditor gains the 0/3/2 exit-code contract.
User-Visible: yes
Issue: #33
An already-checked radio input fires no change event, so the same-binding
guard on the virtual branch was dead code and its smoke assert vacuous.
Removed both; a comment documents why the reset there is always
legitimate. The candidate-list guard (the reachable path) stays tested.
User-Visible: no
Issue: #385
(a) a same-binding click in the marker dialog is a no-op: the value
source and badge reset only on an actual change of binding (#378 §1.6) —
both the candidate list and the virtual radio.
(b) rewriteMarkerControlReferences no longer plants value_badge /
value_source keys as undefined on markers that never had them.
(v) the expensive release diff proof (2 git-show per src file) runs only
for commits the SAME shared predicate classifies as release — both
disjuncts, including the Release: trailer.
(g) the paired neutralisation formats in space export are documented in
place and pinned by a combined badge+value_source pytest.
User-Visible: yes
Issue: #385
Упавший тяжёлый гейт не пишет маркер переиспользования — и правильно, иначе
починка осталась бы незамеченной. Но следствие в том, что следующий коммит
гонит ту же job на тех же входах, падает так же, и письмо «Run failed»
называет его. 29 августа так был назван 0f7b6f5, документ ревью, который не
может изменить ни одного пикселя: сцену без эталона добавил dbbe94ae.
Теперь падение оставляет второй маркер — с тем же ключом, что у маркера
успеха. Совпадение ключа доказывает равенство входов, поэтому повторное
падение может честно сказать «красная с такого-то SHA, этот коммит её не
ронял», а первое — «причина здесь». Само падение по-прежнему не кэшируется:
job прогоняется всегда, меняется только формулировка в notice и summary.
Первопричина записывается только на первом падении: иначе SHA съехал бы на
свидетеля и сообщение перевернулось бы смыслом.
Issue: #386
User-Visible: no
Wall bodies, extras and zero walls vote in the tight frame only while
show_borders renders them — mirroring the hideOpenings guard for opening
symbols. needsCanonicalWallGeometry returns to its pre-#373 form: the
union is no longer forced for extras-only plans just for the frame.
User-Visible: yes
Issue: #384
Record the main-only workflow mirror ancestry repair that makes the v1.69.0 promotion a true fast-forward; no product or bundle bytes change.
Issue: #379
User-Visible: no
Reconcile main-only workflow mirror commits before the stable fast-forward; conflicts retain the already validated dev versions and the resulting tree is unchanged.
Issue: #379
User-Visible: no
Promote the published v1.69.0 beta line without new product behaviour: stable version fields, synchronized generated bundles, bilingual changelogs, release notes and status metadata only.
Issue: #379
Release: v1.69.0
User-Visible: yes
Allow only mechanically proven version-declaration changes in the three canonical source files while keeping every other release source diff fail-closed.
Issue: #379
User-Visible: no
Prepared the reviewed S8 work for the v1.69.0-beta.5 prerelease, refreshed the bilingual changelogs and release notes, and rebuilt the synchronized frontend bundles.
Issue: #373
Issue: #375
Issue: #376
Issue: #377
Issue: #378
User-Visible: yes
Reviewed by Claude in CODE-REVIEW-378-r2 against the complete Linux candidate. Exactly the new 42 percent value-face frame is accepted; 142 existing baselines retain their reviewed hashes.
Issue: #378
User-Visible: no
Release: v1.69.0-beta.5
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33271690654
(а) title: null gets the same compact frame as title: '' — YAML 'title:'
with no value parses as null, the owner's decision makes them synonyms.
(б) the guides state that room labels are inert in the Background editor.
(г) furniture strokes skip the flat-camera compensation in the labs iso
projection, tracking ordinary decor there.
(д) TESTING.md notes the light_pools opt-in for static room cards.
(е) the space-card dispose gate mirrors the strict === true render gate.
User-Visible: yes
Issue: #376
settings.decor_default_style (all fields optional, validated) seeds
_decorStyle once from the first config that arrives; every UI change of
the session default flows through one runtime method with a 1s debounce
and the ordinary serialized expected_rev write path. The built-in default
is stored as the absence of the key; a partial or garbage key falls back
per-field. decorStyleFromSettings/decorStyleToSettings are the single
snake_case<->camelCase conversion point.
User-Visible: yes
Issue: #377
V6a: pass the stable devices array to resolvedLightSources — the WeakMap
cache is keyed by array identity, a spread guaranteed a miss on every
render in BOTH cards (full-card regression since beta.4).
V6b: the static wall geometry now carries the same non-enumerable
sourceFingerprint tag the full card attaches, so buildLightBarrierScene
takes the fast recutWallBodiesGeometry path on door state changes.
V6c: the static barrier-scene cache is an LRU of 8 per space (parity
with _lightBarrierPool) — a flipping door reuses both of its scenes.
V6d: enabledClip is cached by geometry fingerprint + disabled-room set,
with the full card's bbox prefilter for decor bodies.
User-Visible: yes
Issue: #375
The loadGerman-swap mutant tree-shook src/i18n/fr.ts, so the manifest
emit guard failed the mutant BUILD instead of the guard smoke. The
mutant now keeps import('./fr') alive and returns the English
dictionary, which only demo/smoke_french_locale.mjs can catch.
User-Visible: no
Issue: #371
The entry-removed variant died at build time (tree-shaking drops the fr
chunk and the manifest emit guard refuses the bundle) — an infrastructure
failure, not a red guard. The mutant now swaps the fr entry to the German
loader instead: everything builds, parity units stay green, and only the
French smoke catches the wrong dictionary.
Issue: #371
User-Visible: no
The complete French dictionary contributed in #371 (1026 keys, zero empty
values, zero placeholder mismatches) lands as the second lazy locale on the
fr.ts + one static entry. The contributor's snapshot predated this week's
keys, so the 121 additions (device inbox #29, backdrop guard #39, junction
limits #331, lazy-editor toasts #353/#354, furniture #159) are translated
in this commit and flagged in the issue for the author's review; 21 stale
pre-#62 keys are dropped; key order follows en.json. The HA integration
translations file ships as contributed (full parity).
Wiring: loadFrench + __HOUSEPLAN_FR_RETRY_ASSET__ with the same 1/1
replacement guarantee as German; locale roles and localeRoots generalise to
(de|fr); the stale-entry fallback panel (#353) gains its French branch —
the r1 reviewer caught that this second hardcoded language list would have
silently degraded French to English on a cached entry. French profiles
(fr, fr-FR, fr-CA, fr-BE, fr-CH) select automatically.
Proofs: the registry-driven parity suite covers fr by construction
(1128/1128 keys); French analogues of both German-personal tests (product
glossary + non-translation scan with a reviewed equal-to-English
allow-list of 22 legitimate homographs); smoke_french_locale — fr-CA
profile commits French from the real bundle, one lazy chunk per page,
initial graph free of fr; smoke_entry_stale gains the French run; a
registry mutant drops the fr entry and is killed by the smoke. Initial
view: +0.5 KB (registry entry + fallback branch); the 23 KB dictionary is
lazy.
Issue: #371
User-Visible: yes
Внешний пользователь завёл #370 как баг: в houseplan-space-card нет теней от
стен, хотя в houseplan-card они есть. Разбор показал, что кода это не касается —
ограничение намеренное и записано в src/space-render.ts:353 («the compact card
intentionally has no live radial pools»). visibilityPolygon из
src/light-visibility.ts импортируют ровно два файла, houseplan-card.ts и
houseplan-editor-runtime.ts; space-render.ts не импортирует его вовсе. Пулов нет,
а тень существует только как форма пула — затенять нечего.
Но претензия справедлива, просто адресована не туда. Единственная запись о
намеренности жила в комментарии исходника, которого пользователь видеть не может,
а руководство обещало обратное: «маркеры используют те же состояния, значения,
тревоги и эффекты, что полный план» — про свет ни слова. В LIGHT.md про
компактную карточку тоже не было ничего. Человек полез в код именно потому, что
документация молчала, и сам корректно предположил, что это может быть намеренно.
Теперь сказано в трёх местах: оба руководства и LIGHT.md, с причиной — пулы это
самая дорогая часть отрисовки, у неё свой перф-воркфлоу и бюджеты, и компактная
карточка платит за дешевизну именно ими.
Issue: #370
User-Visible: no
#340/#356 made expected_rev mandatory for config/set and layout/set over a
non-empty store — an honest protection the release notes sold only as a
stale-tab guard. A third-party script writing plans directly cannot infer
from "protects from stale tabs" that it must now read the revision first.
Both changelogs gain an explicit breaking-for-external-writers entry with
the read-then-write recipe; ARCHITECTURE.md's WS contract section extends
the #340 paragraph with the cycle external clients must follow (get rev →
send expected_rev → on conflict re-read and retry); and both conflict
messages now carry the actionable hint for scripts — "include expected_rev
from houseplan/config/get / layout/get" — alongside the tab-oriented
"reload" advice. The backend tests pin only the "revision is required"
substring and stay untouched.
Issue: #368
User-Visible: yes
Запас ушёл с 26 КБ до 8.3 КБ за сутки. По документам код-ревью видно, что это не
диффузное расползание, а один шаг плюс обычная работа:
#317 256127 · #318 256091 · #341 256046 · #354 257212 · #159 256828
#357 271143 <- +14 КБ за один заход
#20 271455 · #361 272848 · #359 272469 · #360 273697 <- текущий факт
Шаг на #357 — plan-art мебели: 44 top-view символа в eager-графе, которые платит
каждый план, включая планы без единого предмета мебели.
Потолок 282000 -> 300000. Правило #352 сохранено: 273697 x 1.10 = 301067, то
есть 300000 остаётся внутри надбавки ~10% над измеренным фактом. Запас
возвращается к 26.3 КБ — примерно к тому, что было до #357.
Запись честная и в комментарии сказана прямо: рекалибровка ничего не ускоряет и
ничего не чинит. Она фиксирует новую норму и возвращает гейту способность красить
того, кто вырастил бандл, а не того, кто пушнул последним. Настоящий рычаг —
ленивый граф, варианты 1 и 2 из #367.
Добавлено предупреждение: пока запас меньше 15000 Б, гейт печатает ::warning:: и
строку в summary. Прежняя редакция полагалась на то, что человек заметит тренд в
выводе; за сутки его не заметил никто, потому что каждая отдельная строка
выглядела нормально. Текст предупреждения называет лечение — иначе следующий
читатель поднимет потолок ещё раз и назовёт это решением.
Тест закрепляет обе стороны: надбавка не больше 10% и не меньше 5% (меньше —
возврат лотереи «красит последний коммит»), тревога срабатывает строго ниже
порога, превышение описывается как превышение. Три мутанта проверены руками,
один добавлен в реестр.
Issue: #367
User-Visible: no
The r2 reviewer proved two real exits leaked the window keydown/keyup
listeners of the furniture preview — Escape (the card closes the palette
by direct assignment, bypassing the runtime paths) and disconnectedCallback
(only _clearFurniturePreview ran). The arrow-field listener pins the whole
editor runtime and, through it, the card: the exact leak class this
disconnectedCallback already names two cases above. Both sites now call
_furnShiftDetach alongside the preview cleanup, the smoke gained the
Escape regression (re-arm attaches again, Escape closes the palette AND
brings removes level with adds), and the idempotent double detach in
_furnPlace (r2 Low) is gone.
Issue: #369
User-Visible: no
(a) documented: deleting a vacuum marker erases its server trail at once
and a re-added marker starts from scratch (VACUUM.md + both USER-GUIDEs).
(b) smoothVacPath reports dropped non-finite segments — one console warn
per call with the count — instead of hiding the whole trail silently on a
broken calibration matrix. (c) room climate (#317) now reaches legacy
markers whose exported config carries an ABSENT area key rather than an
explicit null: `== null` where the placement is decided. (d) the armed
furniture preview follows Shift without mouse movement — window
keydown/keyup listeners live exactly as long as the palette is armed,
detached at every palette teardown. (e) only the primary mouse button
places decor/furniture: a right or middle click with an armed tool is a
no-op, touch/pen untouched. (f) a device whose registry entities were ALL
deliberately disabled by the user no longer glows as an alive controller —
the #318 entityless-active rule now requires a genuinely empty roster.
(g) furniture-pack author corrected to Sergey Matyunin (Сергей Матюнин)
per the owner's decision — LICENSE.md, README.md, pack.json,
docs/FURNITURE.md, the provenance check in generate-furniture-assets and
its unit; the source archive bytes are unchanged and the README notes the
romanisation fix.
Proofs: units for (b)/(c)/(f) including the #318 regression pair; new
smoke_furniture_polish for (d)/(e) with listener add/remove counters and
both mouse buttons; five registry mutants, one per code change.
Issue: #369
User-Visible: yes
A gate or door bound to a position-reporting cover fed current_position
into the light-barrier signature at toFixed(3) precision: every percent of
movement produced a new fingerprint, a full physicalBodyParts recompute
and a recut — up to ~100 heavy passes per gate cycle, plus LRU churn.
The light pipeline now consumes one quantised amount
(OPENING_LIGHT_AMOUNT_QUANTUM = 0.05, exact 0 and 1 nodes) at the single
point that feeds BOTH the signature and the cut geometry, so the cache key
and the drawn aperture agree by construction and a full sweep costs at
most 21 recomputes. The door LEAF animation stays smooth — _openingAmt is
quantised only for the light pipeline, nowhere else. Binary contact doors
are byte-identical to the previous behaviour (pinned by unit).
Assumption recorded in the spec: the 5% visual step of the light cut is
indistinguishable on real plans; if field impressions disagree, the
quantum is a one-constant change (0.02 => <=51 recomputes) or the decision
falls back to a debounce. LIGHT.md §Caching documents the grid.
Issue: #366
User-Visible: yes
Конвейер исполняет версию из ветки по умолчанию, поэтому файл обязан совпадать
в main и dev побайтово. Содержательная правка сделана в dev (#365), здесь копия.
Issue: #365
User-Visible: no
28.08 коммит bb2919f уехал в dev с тридцатью файлами вместо одного markdown:
откатил отревьюженную реализацию #359, вернул старые чанки, оставил в dist/
двойной набор. dev держал откаченное дерево три часа. Сообщение коммита было
невинным, и от рутины инцидент отличался только диффом.
Механизм воспроизведён локально, а не предположен. `git checkout -- .`
восстанавливает рабочее дерево ИЗ ИНДЕКСА, `git clean -fd` убирает
неотслеживаемое — ни то, ни другое индекс не трогает. Ревьюер работает с Bash и,
проверяя «умеет ли тест падать», вполне может сделать git add; всё оставшееся у
него в индексе прежняя уборка сохраняла, и следующий git commit забирал это
вместе с документом.
Отсюда три рубежа, каждый закрывает свой отрезок пути.
База: reset --hard на свежий origin/$target снимает и индекс, и дерево разом.
Терять нечего — документ приезжает из RUNNER_TEMP, а не из рабочей копии.
Индексируется ровно один путь, а не каталог.
Индекс: перед коммитом дифф проверяется allowlist'ом docs/reviews/.
Диапазон: перед КАЖДЫМ push проверяется origin/$target...HEAD — то есть то, что
пуш добавит в ветку. Проверок две, потому что push делается из двух мест, и
второй путь срабатывает ровно тогда, когда dev ушёл вперёд — в тех самых
условиях, при которых случился bb2919f.
Пустой дифф — тоже отказ: публиковать нечего означает, что документа нет, а
прежняя редакция шага выходила тут с нулём и оставляла вердикт без артефакта
(#171). Сравнение по префиксу каталога, а не подстрокой: docs/reviews-old и
docs/reviewsx разрешёнными не считаются. Форс-пуш отсутствует и закреплён тестом.
Четыре мутанта проверены руками, два добавлены в реестр. Пятый — «убрать одну из
двух проверок диапазона» — сначала выжил: тест требовал наличия, а не количества.
Тест усилен до подсчёта, мутант убит.
Issue: #365
User-Visible: no
r1-M1: dismissal (Escape/scrim/Cancel) while the reduce or keep-original
flow is executing no longer races the decision — hp-close is ignored while
busy, and every flow re-checks it still owns the guard before applying, so
a force-cleared dialog can never silently install its stale result. The
smoke now drives both: hp-close during a hanging decode leaves the busy
dialog up, and a force-cleared guard ends with clean staging, no toast, no
planFile. A new registry mutant removes the busy gate and is killed.
r1-M2: the hard-dialog text takes its limit from the imported
HARD_DIMENSION instead of a literal — recalibration stays a one-file
change, as the spec promises.
r1-M3: AC8 is now proven end to end, not plausible: the smoke splices a
real EXIF APP1 (orientation 6) into a canvas-encoded 8200×4100 JPEG,
asserts the header probe reads the unrotated SOF, that the decode call
carries imageOrientation:'from-image' (captured on the hook), and that the
reduced copy comes out portrait 2048×4096. TESTING.md names the scenario.
Issue: #39
User-Visible: no
A picked raster is now classified from its HEADER BYTES ONLY before anything
heavy happens: src/backdrop-probe.ts parses PNG IHDR (+colour type/tRNS for
alpha), JPEG SOF and WebP VP8/VP8L/VP8X at fixed offsets, never using a file
field as an allocation size; hostile or truncated headers collapse to
'unknown', which warns without numbers instead of passing silently. The
thresholds live in that module as the single calibration point
(WARN_DECODED_BYTES 128 MiB ≈ 32 MP, HARD_DIMENSION 16384 — the browser
canvas cap, DOWNSCALE_TARGET_PX 4096), derived from the desktop-Chromium
matrix now committed as demo/benchmark_backdrop_decode.mjs with a
conservative tablet margin documented in the spec.
The shared pick flow (src/backdrop-pick.ts) feeds BOTH lazy runtimes — the
editor space dialog and the onboarding first-space dialog — so the guard
cannot drift between them, and nothing of it enters the eager View graph.
Warn shows the real numbers and three actions; the reduced copy decodes
EXIF-aware, keeps aspect and alpha (PNG stays PNG, opaque becomes JPEG
q0.9) and flows through the ordinary planFile → upload path. Hard has two
phases with one outcome: beyond 16384 px only Cancel; a failed or timed-out
(10 s) reduce closes with a toast, clean staging and NO silent fallback to
the original the user just declined. SVG never reaches the probe. The safe
path swaps the manual byte-loop base64 for FileReader — half the JS-heap
peak on every upload, byte-identical output (parity asserted in the smoke).
Proofs: header-table units incl. a fuzz set of hostile headers and ±1
threshold bounds; smoke_backdrop_guard on the real bundle — zero decode
calls before the choice, byte parity of keep-original, a real 6200 px
reduce to 4096 for both alpha and opaque branches, cancel-only hard
dialog, both phase-2 failures (reject and hang under the test-only timeout
override), re-pick after refusal, SVG bypass; four registry mutants
(probe-always-safe, alpha-dropped, hard-demoted, phase-2 silent fallback).
Spec anchor corrected alongside: the server plan limit is 8 MB
(MAX_PLAN_BYTES), attachments are the 50 MB path — an 8 MB JPEG is easily
80-160 MP decoded, so the client-side guard stays the primary defence.
Issue: #39
User-Visible: yes
Конвейер исполняет версию из ветки по умолчанию, поэтому файл обязан совпадать
в main и dev побайтово — это проверяет шаг предполётных проверок в Validate.
Содержательная правка сделана в dev (#364), здесь только копия.
Issue: #364
User-Visible: no
Конвейер приводит ветку к dev сам (#257) и при конфликте возвращает задачу, не
тратя цикл ревью. Оставались три щели, и все три про то, что человек узнаёт
поздно и без подробностей.
Первое. Отставание теперь видно в scripts/pre-push-gate.mjs до пуша, с числом
коммитов и готовой командой. Это предупреждение, а не гейт: гейтом остаётся
конвейер, который забыть не может. Смысл в цене — после любого ребейза разбор
становится полным, а не по дельте (§7.2), а конфликт всё равно чинится на машине
автора. Отключается --no-rebase-check.
Второе. Конфликт называет файлы. Список снимается ДО `git rebase --abort`: abort
снимает состояние конфликта вместе с ним, и раньше автору доставалось «не
ребейзится» без единого имени. Логика проверена на настоящем конфликте в
одноразовом репозитории — два файла названы.
Третье. Если dev ушёл вперёд, пока шло ревью, это записывается в summary
прогона, а при зелёном вердикте ещё и комментарием: вердикт вынесен по дереву,
которое уже не совпадает с вершиной линии, и слияние приведёт ветку к dev.
Комментарий только при зелёном — шуметь на каждом прогоне ни к чему, а вот
молчать перед слиянием нельзя.
Чего задача не делает: не заставляет dev стоять на месте, пока идёт ревью. Если
возвраты частые именно из-за темпа, лечится очередью слияний, а это решение о
процессе, не о скрипте.
Два мутанта проверены руками — «советовать ребейз всегда» и «никогда не сообщать
про уход dev», — каждый убит.
Issue: #364
User-Visible: no
The code-review worker published its report from a stale local snapshot and unintentionally reverted the already reviewed implementation artifacts. Restore every affected path exactly to the reviewed 84bdc7ef tree while retaining CODE-REVIEW-359-r1.md.
Issue: #359
User-Visible: no
Reviewed the Linux candidate from the full CI run. Only the new furniture placement preview frame is accepted; 141 existing baselines remain byte-identical.
Issue: #359
User-Visible: no
Release: v1.69.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33213146088
Accept the reviewed Linux frame left pending by #20 alongside the already accepted #209 vacuum frame. The closed and partially open doors now form the visual contract for proportional Glow transmission.
Issue: #209
User-Visible: no
Release: v1.69.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33207816479
Reviewed the Linux candidate for the deterministic current and previous vacuum routes. Only the new #209 frame is accepted; the unrelated door-hover drift was explicitly retained at its existing reviewed baseline.
Issue: #209
User-Visible: no
Release: v1.69.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33207816479
Systematic audit after #357 ("can there be more bugs with this root
cause?"): _vacMapId was the one remaining hard stub reachable from the
eager View path. It runs inside willUpdate for every vacuum whose
integration reports live telemetry (Tasshack, XCME, Valetudo), so on a
cold tab the #337 stub threw there and the exception took the whole Lit
update cycle with it — the card froze on its very first frame. The demo
mower has no position attributes, telemetry resolved to null, and every
existing smoke (warm and cold) sailed past the branch.
The card now owns the implementation (both dependencies — _vacEntity and
vacMapIdWithFallback — were already eager); the editor runtime delegates
back to the host. The HP-1541-01 invariant (selected_map: 0 is a real map
id, nullish not truthy) moves verbatim and is pinned by the new smoke.
Hardened alongside (audit Lows): _decorShapeDown gets the same
cold-tab guard its twin _decorShapeDbl received in #337 — decor shapes
render in View and CSS pointer-events alone must not be what prevents a
throw; the _vacCalConfirm dialog renders behind the same _editorRuntime
gate as every other editor dialog instead of relying on the implicit
"only the runtime ever sets it".
smoke_cold_view_vacuum: cold tab, vacuum with vacuum_position and
selected_map: 0 — the card commits three successive telemetry frames
(willUpdate alive, not merely the first paint), map id resolves to '0',
no editor chunk requested, a decor pointerdown is a quiet no-op. A
registry mutant restores the delegation and is killed by that smoke.
Issue: #358
User-Visible: yes
Field report from the dacha: the wall switch "Гостиная основной свет",
whose controls name three virtual light sources, periodically ignored taps
— no toggle, no glow — until its settings dialog was opened once with no
changes. "Periodically" was every fresh tab: the #337 lazy split left
_toggleIntent (and the confirm-line helpers) on the card as stubs
delegating into the editor runtime, so a plain View tap on a cold tab
threw `Houseplan editor runtime is not loaded` synchronously inside the
click handler. Opening any editor surface loaded the runtime and "healed"
the tab for its lifetime.
The View card now owns toggle resolution: _toggleIntent calls
resolveToggleIntent directly (device-toggle.ts was already in the initial
graph; the card owns _planHass/_fullRegistryHass/_virtualLights), and
_toggleStateText/_toggleConfirmationStateText/_toggleConfirmationLines
moved with it. The editor runtime delegates back to the host — one source
of truth, editor consumers (dialog preview, hint lines) unchanged.
Every product smoke preloads the runtime, so none of them could see this
class of regression. The new smoke_cold_view_toggle mirrors the field
config on a genuinely cold tab: a real switch drives three passive
virtual lamps with one tap, a controlled lamp drives its switch back,
tap_confirm renders its state lines and confirms, and the editor chunk is
never requested. A registry mutant restores the old delegation and is
killed by that smoke.
Issue: #357
User-Visible: yes
Declaring the whole matrix in --expect-change could accept a completely
foreign capture (different font stack, different machine): no undeclared
passed scenes would remain, and undeclared passed scenes are exactly what
proves the capture environment equals the accepted baseline's. The
realistic failure is fatigue, not malice — a mass framing change where the
author lists "everything that went red", accidentally sweeping in scenes
that diverged because of the environment.
Acceptance now requires a witness floor: after subtracting
--expect-change/--expect-new, at least min(10, 10% of baseline scenes)
undeclared scenes must match their accepted baselines BYTE-FOR-BYTE (a
sub-threshold 'passed' proves nothing about the environment — #351). A
truly total repaint passes only with an explicit
--no-witnesses --reason="…", and the reason is written into the baseline
manifest — a trace in the artifact and its git history, not just in the
shell history. A first-ever capture with no baselines requires no
witnesses: every frame there is declared in --expect-new anyway.
Issue: #355
User-Visible: no
The r1 reviewer cut the listener loop in the production registry and all
three #354 units stayed green — the subscription unit was the same class of
decoy the issue itself fights. The fan-out now lives in an exported
notifyLanguageLoadFailures(code); the unit drives it directly and asserts
real delivery, partial unsubscription and silence after the last listener
leaves; the contract unit additionally pins the runtime wiring
(`loadFailed` → notifyLanguageLoadFailures) in source. A new registry
mutant `locale-failure-delivery-cut` replays the reviewer's exact cut and
is killed by the unit. The r1 Low is taken too: both USER-GUIDEs now
mention the toast in the German-failure paragraph.
Issue: #354
User-Visible: no
The production LANGUAGE_RUNTIME was a handwritten twin of the tested
LanguageRuntime class (germanDictionary/Pending/Failed): equivalent on the
day it was written, invisible to every i18n-runtime test afterwards. The
registry now exports one page-scoped `new LanguageRuntime(LANGUAGE_REGISTRY,
…)` instance — the whole existing suite starts proving the object production
actually runs, and a contract unit (instanceof + source free of the old
field names) keeps the duplicate from returning.
The class gains an optional `loadFailed(code)` hook — fired once when a
dictionary load settles into English fallback — and the registry fans it out
through `subscribeLanguageLoadFailures`. Only the View card subscribes (it
alone owns toast infrastructure): a failed language pack now shows the new
`toast.locale_load_failed` message (en/ru/de) instead of a console-only
warning; space card and both GUI editors keep the console warning as before.
Proofs: contract unit, hook unit, subscription unit; smoke_german_locale
extended — the both-attempts-failed scenario now asserts the visible toast;
two new registry mutants (handwritten-twin returns, toast dropped).
Issue: #354
User-Visible: yes
Network failure of the editor runtime is no longer terminal: the loader
re-arms to idle and the next explicit press starts a fresh cycle, while a
fingerprint mismatch on either attempt stays terminal. The toast now says
what actually helps — retry advice for the network, refresh advice for a
foreign build — via one shared lazyLoadFailureMessage helper (new i18n key
editor.retry_advice in en/ru/de).
The field smoke caught a second, deeper bug on the way: Chromium records a
FAILED module in the page module map permanently, so retrying the same URL
(even the cache-busted one) never touched the network again. Every retry
now carries a per-cycle nonce and becomes a genuinely new module request.
A proxy-cached stale entry no longer kills the card silently: the entry
facade is rewritten at build time from a static re-export into a top-level
`try{await import(...)}catch{...}` — importers keep the happy-path
guarantee (await import(entry) still resolves only after
customElements.define), and the catch defines a fallback element with a
localized "reload the page" panel. Content-hashed chunks are served with
`public, max-age=31536000, immutable`, and verifyBundleTree now fails on
orphan chunks that the manifest does not name.
Proofs: loader units for re-arm/terminality/toast wording + an AST check
that both loaders forward the terminality flag; smoke_entry_stale (en/ru)
against a tree without the main chunk; smoke_lazy_editor_chunk extended —
second press after network failure now really opens the editor; pytest for
the immutable header; orphan-tree unit; five new registry mutants.
TESTING.md budget line updated to the #352 ceiling alongside.
Issue: #353
User-Visible: yes
В src/** сейчас 1034 вхождения явного any в 49 файлах — больше, чем называл
аудит (330), потому что монолит с тех пор разделился и его обвязка уехала в
houseplan-editor-runtime.ts. Разовая замена такого объёма — месяц риска ради
нуля пользовательской ценности, поэтому долг снимается при плановом извлечении
подсистем (#34). Задача гейта одна: не давать долгу расти.
Судятся только добавленные строки диапазона. Изменённая строка со старым any
выглядит в диффе добавленной, и это намеренно: тронул — либо типизируй, либо
обоснуй на той же строке `// any-ok: <причина>`. Голый маркер, пустая причина и
шаблоны вроде todo, hack, потом не проходят.
Ложных срабатываний нет по построению, а не по старанию: текст разбирается
парсером TypeScript, и нарушением считается узел AnyKeyword. Регулярка по строке
ловила бы слово any в прозе внутри шаблона html и в комментариях; здесь
комментарии, строковые литералы, многострочные шаблоны и идентификаторы
company, anyOf, manyRooms узлами такого вида не являются вовсе.
Проверено исполнением на настоящем дереве, а не только юнитами: пробные коммиты
в src/wall-thickness.ts показали, что добавленный any падает с файлом и строкой,
типизированная строка в файле с 122 старыми any проходит, any-ok с конкретной
причиной проходит, а голый и «todo» — нет, и что any в прозе, строке и
идентификаторах не даёт ни одного срабатывания.
В job frontend checkout получил полную историю без блобов: diff-aware проверке
нужен диапазон, а содержимое старых ревизий — нет.
Заодно закрыта ловушка в test/validate-workflow.test.mjs: имя job искалось через
indexOf(' frontend:'), а эта строка встречается внутри ` frontend: ${{ ...
}}` в outputs job changes, поэтому срез уходил не туда. Теперь имя ищется с
начала строки.
Четыре мутанта проверены руками, два добавлены в реестр: гейт, судящий все
строки, и гейт, принимающий голый маркер.
Issue: #342
User-Visible: no
v1.69.0-beta.1 shipped at 255 993 B gzip against a 256 000 B ceiling —
seven bytes of headroom turned the gate into a lottery where the unlucky
last commit goes red, not the one that grew the bundle (5740324b was
exactly that fix; and today's dev already measures 256 012 B, so the old
ceiling would be red right now on an untouched tree).
The ceiling moves to 282 000 B — a deliberate ~10% allowance over the
calibration fact, recorded next to the constant: the budget guards the
CLASS of regression (tens of kilobytes from an accidental dependency or an
eager dictionary), not every byte. Every run now prints the fact, the
budget and the headroom, and CI adds the same row to the step summary so
the trend is visible long before the wall.
The lazy-ru idea from the issue (biggest single cut, ~25 KB gzip) is left
out deliberately: it changes what Russian users see on first paint and
deserves its own decision, not a ride-along.
Issue: #352
User-Visible: no
isDegenerateApexCorner measured the inner-face convergence as
max(h1,h2)/tan(theta/2) — for a 10-degree apex between a 15 cm and a 30 cm
wall that overstates the distance (171.5 cm against the true 128.3/128.9 on
160 cm edges), the corner failed the "inside both edges" test and rendered
as the #329 trident again. Worse, the verdict depended on which neighbouring
edge carried the thicker wall.
The check now intersects the two actual face lines: the meeting point lands
at (hOther + hOwn*cos(theta))/sin(theta) along each edge, degenerate only
when inside both. With equal halves this reduces algebraically to the old
h/tan(theta/2), so equal-thickness verdicts are unchanged by construction —
pinned by the untouched section-4 units and the full golden matrix (136
scenes verified). New units cover both traversal orders of the mixed apex,
the one-point outset tip, the 30-degree ordinary pair and the zero-thickness
guard.
The write path is untouched: P1 forbids new sub-15-degree corners since
issue 329, this is purely how a legacy document renders.
Issue: #339
User-Visible: yes
Accept the complete canonical Linux capture from run 33159459520 after visual
review of View, touch and Device editor surfaces.
Issue: #345
User-Visible: no
Extend the existing localized dialog footer measurements to German at desktop and 320 px. The smoke now checks opening, physical-wall and space dialogs for containment, responsive wrapping and horizontal overflow, closing #348 review r1-M1.
Issue: #348
User-Visible: no
Track locale-owned inert and busy state together, preserving the same render contract while keeping the deterministic initial View graph below its hard gzip budget. Refresh generated assets and the documentation fingerprint after the source cleanup.
Issue: #348
User-Visible: no
Add Deutsch across all card surfaces and backend flows, backed by the language registry introduced in #62. German loads as a fingerprint-checked page-shared locale chunk so EN/RU remain synchronous and the initial View budget stays intact. Root render gates prevent mixed-language flashes, retry once, and fail open to English. Extend parity, runtime, bundle, browser and visual coverage, plus contributor and user documentation.
Issue: #348
User-Visible: yes
`passed` означает «в пределах порога», а не «байт в байт»: comparePng считает
diffRatio, и статус ставится по нему. А приёмка копировала кандидата поверх
КАЖДОГО эталона матрицы, поэтому подпороговый дрейф уезжал в контракт молча — и
накапливался: каждая приёмка подтягивала эталон к последней среде, порог не
пересекался никогда, а эталон уходил. Так 1e341c60 заменил 22 картинки, объявив
четыре.
Проект уже сталкивался с этим: ad3f9981 восстанавливал девять уехавших эталонов
руками. Такую работу обязан делать инструмент.
Теперь копируются только сцены из --expect-change и --expect-new; остальные
сохраняют и файл, и свой хеш из прежнего индекса. Индекс по-прежнему
перезаписывается на полный набор — сирота или пропавшая запись делают манифест
недействительным целиком.
Решение вынесено в чистую функцию goldenAcceptancePlan: оно одно, и ошибка в нём
дорога. Отсутствие прежнего хеша у необъявленной сцены — ошибка, а не повод
взять кандидата: без эталона бывает только новая сцена, а она обязана быть
названа в --expect-new.
Логика вернулась в demo/golden/accept.mjs, где ей и место: после #344 эти файлы
исключены из корпуса отпечатка, так что правка больше не требует пересборки и
пересъёмки. scripts/golden-accept.mjs остался проходным вызовом ради
документированной команды.
Проверено сквозным прогоном на синтетическом кандидате: у двух сцен байты
другие, объявлена одна — на диске изменились ровно два файла, эталон и индекс, а
хеш второй сцены остался прежним. Два мутанта убиты руками: «брать кандидата
вместо прежнего хеша» и «заменять всё».
Issue: #351
User-Visible: no
Причина установлена бисекцией: cab8d128 (#29, feat: add device lifecycle
catalog, User-Visible: yes). На cab8d128^ сцена device-dialog-mobile-ru
совпадала, на cab8d128 разошлась. Изменение объявленное: каталог «Devices»
заменил кнопки «Add» и «Hidden and disabled» в тулбаре редактора устройств.
Стили каталога проверены на протечку: все 32 добавленных селектора и блок
@media (max-width: 680px) заскоплены на .device-inbox*, незаскопленных нет.
Кадры просмотрены — контент не обрезан, сместился.
Съёмка локальная в WSL: параллельность раннеру доказана по правилу #334 —
113 сцен из 117 совпали с эталонами, разошлись ровно объявленные четыре.
Issue: #346
Release: v1.68.2
Baseline-Reviewed: run 33146828502, job 98769832040 (Golden-кадры против принятых эталонов)
User-Visible: no
Правило из #334 требовало объявлять только сцены со статусом different, а
missing-baseline пропускало без вопросов. При закрытии #346 из-за этого три
эталона каталога устройств стали контрактом без единого взгляда.
Половина прежнего обоснования верна и остаётся: расхождение растеризации новая
сцена выявить не может, параллельность среды доказывают только сцены с
эталонами. Но правило отвечало лишь на вопрос «та ли это среда» и молчало про
второй — «правильный ли это кадр». Пустой, обрезанный или снятый в неверном
состоянии кадр новая сцена закрепляет так же надёжно, как испорченный старый, и
README об этом предупреждает прямо.
Поэтому флагов два и они утверждают разное: --expect-change — «я знаю, почему
старый кадр изменился», --expect-new — «я посмотрел на новый кадр». Имя в чужом
флаге тоже останавливает приёмку: путаница означает, что ревьюер думал об одной
сцене, а утверждал про другую.
Новые эталоны печатаются отдельной строкой «СТАНУТ КОНТРАКТОМ ВПЕРВЫЕ», а не
растворяются в общем списке — раньше они там и растворились.
Прежний тест «новая сцена объявления не требует» заменён: он кодировал снятое
правило. Два мутанта проверены руками — возврат молчаливого пропуска и
разрешённая путаница флагов, — каждый убит.
Issue: #350
User-Visible: no
Логика проверки существовала и была написана правильно: verifyBundleTree и
compareBundleTrees в scripts/bundle-tree.mjs. Но применялась только к фикстуре в
tmpdir(), поэтому манифест, ссылающийся на пять несуществующих файлов, прожил в
dev при 1444 зелёных тестах и зелёном check-docs. Установка через HACS получила
бы 404 на каждом ленивом импорте.
Второй тест спрашивает git, а не файловую систему, и это не перестраховка.
Дефект родился так: пересборка дала чанки с новыми хешами содержимого,
`git commit -a --amend` удалил старые (отслеживались) и не добавил новые (не
отслеживались). На машине автора проверка наличия файлов прошла бы — файлы там
были. Отличить «собрано» от «закоммичено» умеет только индекс.
Пропуск проверки при недоступном git — громкий: тихий пропуск это тот самый
класс, из-за которого задача и появилась.
Доказательство пользы исполнением: оба теста прогнаны на c665c7d3, коммите до
починки, и оба падают — первый с «manifest asset is missing:
houseplan-assets/editor-DMlizeQy.js», второй с перечислением десяти путей вне
индекса.
Мутанта не добавляю намеренно. Это утверждение о состоянии дерева, а не о
логике: на здоровом дереве ослабленная проверка проходит, то есть мутант
выживает, а выживающий мутант хуже отсутствующего. Логику verifyBundleTree
по-прежнему держат синтетические мутанты в bundle-assets.test.mjs.
Issue: #349
User-Visible: no
oxipng снимает с набора 19.4%: 2096 КБ становятся 1689 КБ, и все десять кадров
остаются пиксельно идентичными — декодированные RGBA совпадают по sha256. Это
выбор фильтров строки и уровня сжатия, а не квантование: визуального решения нет.
Внутри съёмки, а не отдельным проходом по закоммиченным файлам: манифест хранит
imageSha256 каждого кадра, поэтому жать их в репозитории руками нельзя —
check-docs покраснеет; а если жать после подсчёта хешей, следующая съёмка вернёт
неоптимизированные байты. Хеш считается после перепаковки.
Версия oxipng попадает в манифест рядом с версией браузера и по той же причине:
байты кадра зависят от того, чем жали. Отсюда же правка шага «Вердикт» — иначе он
объявил бы «тот же браузер, а картинки изменились — изменился продукт», хотя
изменился упаковщик.
Пин версии и контрольной суммы вместо apt-get: пакет из образа раннера может
пропасть, а падение шага съёмки стоит целого цикла приёмки (#175, #206).
Проверено исполнением на прежней базе: съёмка прогнана целиком с подставным
oxipng, 2096 -> 1689 КБ, хеши манифеста совпали с файлами, check-docs зелёный.
Issue: #345
User-Visible: no
accept.mjs копирует уже снятые PNG и пишет манифест, policy.mjs — чистые
предикаты. Ни тот, ни другой в момент рендера не исполняется, но оба входили в
корпус, и правка любого объявляла устаревшими бандл и манифест скриншотов. В
#334 из-за этого правило приёмки пришлось вынести в scripts/ и вызывать
обёрткой вместо того, чтобы положить туда, где ему место.
Возражение «run.mjs импортирует policy.mjs, значит исключение протекает» снято в
комментарии: оттуда берутся проверка аргументов, действительность манифеста и
код возврата — байты кадра определяются аргументами браузера и подготовкой сцены.
Исключение — список, а не фильтр по имени. Тест закрепляет обе стороны, и
обратная важнее прямой: исключение, доехавшее до matrix, harness, run или
фикстур, сделает несвежий бандл неотличимым от свежего.
Коммит меняет значение отпечатков, поэтому в dev идёт вместе с пересборкой
бандла и пересъёмкой скриншотов. Golden при этом не затронут: sourceFingerprint,
записанный в baselines-index.json, не валидирует никто — manifestValid смотрит
matrixVersion, chromium и полноту набора сцен.
Issue: #344
User-Visible: no
github.event.before dies with a force-push, and the merge-base fallback then
guessed a diff range: on issue/333 it reported two review-doc files while the
real diff touched custom_components/** — frontend and backend jobs silently
skipped and the run stayed success, the exact #171/#207 class of silent pass
that nearly hid a genuine backend regression from code review.
The classifier now distinguishes the two fallback cases instead of merging
them: a ZERO before is a genuinely new branch and keeps the merge-base
range; a NON-ZERO before that no longer exists is a rewritten history, and
the range is not provable — frontend/backend/integration all go true, with
a loud note in the step summary. A force-push is rare and almost always
follows a rebase, where the full run is what an honest signal costs.
The three branches of the decision are pinned by a workflow-contract unit
next to the existing performance-workflow contracts.
Issue: #347
User-Visible: no
The junction gate reads an empty previous as "a first write may not arrive
already broken", and the #248 storage-roundtrip fixture legitimately carries
a 6 cm wall — so the untouched test went red on this branch. The subject of
#248 is byte-exact storage of an optimize commit, not first-write semantics:
the fixture is now seeded as the stored document and optimize inherits its
violations per rule, exactly like a real repair flow. Every storage
assertion (intent, pending, final pair, canonical serialisation) is
unchanged.
Issue: #333
User-Visible: no
The owner's decision (2026-08-28): optimize is one of the two commands a
client can use to write arbitrary geometry, so it validates its candidate
against the stored document exactly as config/set does — inheritance counted
per rule (repairing a legacy plan with violations still passes; #329 AC10
already proves an honest optimization adds none, so the gate is a no-op for
legitimate flows), while a crafted payload is refused with the stable
junction_limit_<rule> code the except list has been ready for since #329.
The call lives inside the existing executor function, and a successful
optimize refreshes rt.junction_baseline with the candidate's counts so the
next config/set inherits from the cache (#330 §4.2 symmetry).
Import and backup restore stay OUTSIDE the gate on purpose — #329 §3
promises a restore is never blocked. The module docstring stops promising
more than the code does, and spec #329 §5 records the perimeter and the
trade-off explicitly: a crafted import can persist violations, but they are
inherited, never legalised as new ones.
HA tests pin AC1 (crafted spike refused, stored config and rev
byte-unchanged), AC2 (echo-optimize of a stored plan that already carries a
violation passes) and AC3 (the follow-up config/set takes its baseline from
the cache — observed through a recording wrapper). The
junction-limit-optimize-unguarded mutant turns AC1 red through the
backend-test-guard convention.
Issue: #333
User-Visible: no
Полный клон — 215 МБ .git, blobless — 26 МБ, история коммитов и теги в обоих
полные (замер в #345). Две job Validate качают историю целиком: preflight и
changes. Первой нужны сообщения коммитов, трейлеры и имена изменённых файлов,
второй — только `git diff --name-only`. Содержимое старых ревизий не читает ни
одна из них ни на одном шаге.
Коммиты и деревья по-прежнему скачиваются полностью, поэтому диапазоны и
merge-base работают как раньше. Единственная догрузка блоба по требованию —
`git show origin/main:.github/workflows/process.yml` в шаге сверки, один файл.
Браузерным job фильтр не нужен: у них глубина по умолчанию, истории они не
касаются вовсе.
Тест закрепляет и обратную сторону: --depth=1 сюда подставлять нельзя, он того
же размера, но без merge-base, а на нём стоят процессный гейт, smoke-select и
каждый диапазон origin/dev..HEAD. Два мутанта проверены руками — снятый фильтр и
подмена на depth=1, — каждый убит.
Issue: #345
User-Visible: no
Замерено, не оценено: полный клон — 215 МБ .git, blobless — 26 МБ. История
коммитов и теги в обоих полные, поэтому диапазоны, merge-base и git diff по
истории работают одинаково; diff трёх коммитов в blobless-клоне занимает секунду
и добавляет мегабайт.
Разница в том, что содержимое старых файлов скачивается только если его кто-то
спросит. 32% пака — скриншоты документации, десять PNG, переснятых 196 раз; ещё
заметная доля — закоммиченный бандл, 1.16 МБ на каждую продуктовую правку. Старые
ревизии ни того, ни другого практически никто не читает.
Про --depth=1 сказано отдельно, что он не замена: размер тот же, но merge-base
нет, и процессный гейт вместе со smoke-select перестают работать.
Issue: #345
User-Visible: no
Red dev caught it ninety minutes after the merge: smoke_plan_drawing_repairs
and smoke_resize_pointer_real_plan went red because the new "a 0° wedge is
always a duplicate" rule refused two ordinary edits — creating a room over
an existing partition ring (#308's legal overlay) and resizing a wall until
it lands on a neighbour's. The premise was wrong at the model level: a
shared wall of two adjacent rooms IS two co-located owner atoms on one line,
so every shared-wall node carries a legitimate 0° pair by construction.
Bisection pinned the exact cut: with only the 0° rule reverted, both smokes
are green again; keys, incidence, the iterative walk and fail-closed stay.
Spec revision 4 records the revert and returns "an exact duplicate wall is
invisible to П1" to the status of a KNOWN LIMITATION — an honest detector
needs owner identity, which is a separate decision for the owner to make.
The zero-wedge mutant is removed with its rule; the .5-tick parity unit now
observes quantisation through valence instead of the retired duplicate
visibility; changelogs drop the over-promise.
Issue: #331
User-Visible: yes
smoke-select flagged quantizeKeyCoord/INCIDENT_EPS/KEY_FACTOR as symbols no
smoke names — true by design: the smoke proves the verdicts through the
rendered card, never touching the internals that decide which nodes are one
node. The registry entry records that non-textual link (#241 rule).
Issue: #331
User-Visible: no
Running the mutants exposed two toothless guards before review did:
- reverting the keys to toFixed(6) no longer produced false П4 refusals
because the new 2e-7 incidence absorbed the debris pair — the REAL harm of
coarse keys is the opposite direction: nodes 4e-7 apart merged into one
key and П4 went blind to a genuine near-miss. AC1 now pins that case.
- the `break` patch failed to reproduce the old first-branch-only loss (the
frontier re-visits the node through the pushed endpoints); the patch now
truncates the node's candidate list to one entry, which loses forks the
way `.find` did — both the fork unit and the 10 000-atom run turn red.
Issue: #331
User-Visible: no
Six normative cuts, both mirrors symmetric (spec revision 3):
- §2.1 node keys quantise to 1e-7 with the repository's canonicalisation
formula (sign·floor(|v|·1e7+0.5)/1e7, -0 normalised) — toFixed(6) keys
split one node into two on floating debris and produced two false П4
refusals on a legitimate resize (reproduced: -1e-8 vs 0). Node pairs
within 2e-7 of each other (raw coordinates) are ONE node, and the
node-to-wall incidence uses the same quantum.
- §2.2 a ~0° wedge IS a violation: two rays leaving a node the same way are
a duplicated or overlaid wall (a butt joint yields 180°, never 0°) — the
worst degenerate case was invisible while 0.5° was refused.
- §2.3/§2.4 the wall run is an iterative edge walk over the collinear
component: no recursion (10 000 atoms answered, not RangeError), no
silently dropped fork (the old .find lost every branch but the first),
O(E) by construction, and collinearity is measured against the BASE
segment's axis so an arc of 0.9°-per-atom pieces cannot pose as one wall.
- §2.5 an exception while judging the CANDIDATE refuses the write with the
junction.limit_check_failed toast (fail-closed, as the #278 guard); the
baseline branch stays fail-open by design and the smoke proves the
asymmetry by breaking only the second call of the deterministic pair.
- §2.6 the python mirror narrows its except on the candidate side only:
a genuine migration bug (TypeError) surfaces as an honest WS error, while
a previous-side bug keeps the wide "no baseline" fallback — the two AC6
cases pin the asymmetry so swapped sides turn a unit red.
Parity fixtures gain the new boundary classes (debris node, duplicate wall,
collinear fork); four new mutants pin the filter, the key precision, the
dropped branch and the fail-open hole.
Issue: #331
User-Visible: yes
r2 M-r2-1: AC6 now mirrors AC5's structure with two explicit cases — a
candidate-side TypeError is an honest WS error, a previous-side TypeError
falls back to "no baseline" and the unrelated write passes. An
implementation with swapped or missing asymmetry turns at least one of the
two units red. L2: the risk wording follows §2.3's component-sum phrasing.
Issue: #331
User-Visible: no
r1-H1: node keys quantise with the repository's canonicalisation formula
(sign·floor(|v|·1e7+0.5)/1e7) — native Math.round and Python round() part
ways on .5 ticks, the exact parity lesson coordinate-canonicalization
already encodes. r1-M1: the incidence threshold becomes 2e-7 over raw
coordinates, which the spec's own example (1.02e-7) actually satisfies; the
known valence undercount on neighbouring quanta is stated in §3. r1-M2: the
narrow except applies to the candidate side only — a previous-side migration
bug stays a "no baseline" fallback, symmetric with §2.5. r1-M3: the branch
walk is an O(E) edge traversal of the collinear component, not a
combinatorial DFS; AC3 gains a 100-fork case. r1-M4: the USER-GUIDE limits
section documents the new refusal toast. L1: §1 opens with the user
sentence.
Issue: #331
User-Visible: no
Quantised node keys with -0 normalisation and node incidence at the quantum,
zero-degree wedges become visible, an iterative maximal-branch wall run, arc
collinearity measured against the chain base, fail-closed candidate checks,
and a narrow except in the python mirror. Every reproduction in §1 was
verified by execution on current dev after #330.
Issue: #331
User-Visible: no
Third time this class bites in one task: any src/** edit staleness the
screenshot source fingerprint mechanically, and I keep forgetting the
capture step after code-only commits. The pair (PNGs + manifest) is
regenerated from one run; check-docs is green on this SHA.
Issue: #330
User-Visible: no
The reviewer proved my behavioural claim false by running the stale-cache
mutant against the smoke: 11 vs 12 total calls — indistinguishable. Two real
defects hid behind that finding:
1. The cache keyed on _cfgEpoch, which ticks on every ACCEPTED PREVIEW —
the cache missed on every pointermove and the baseline was recomputed
~4 times per gesture (measured). The key is now the document identity
plus spacePhysicalGeometryFingerprint of its space: content, not a
counter. A preview overlay leaves the fingerprint alone; an in-place
structural commit changes it and honestly invalidates.
2. The smoke now counts BASELINE computations only (calls whose document is
_serverCfg) across two gestures with a commit in between, expecting
exactly 1 then exactly 2. A disabled cache lands near 20, an eternal
cache stays at 1 — every mutant class turns the smoke red, and the smoke
is now the mutant's guard alongside the source-contract unit.
Issue: #330
User-Visible: no
Same pairing rule as before: PNG files and their manifest must come from one
capture run; the rebase over the i18n-registry merge (#62) mixed the sides
again.
Issue: #330
User-Visible: no
H2: the benchmark budgets were calibrated on the author's sandbox with a
1.14x margin — the review runner measured tsFullCandidateMs at 169-171 ms
against a 100 ms ceiling. Budgets now keep the spec's 2-3x allowance over
the SLOWEST observed machine, and the benchmark runs as a step of the
Validate perf job on every push (it needs no browser and no bundle), not
only inside the weekly mutation gate.
M1: the promised AC1 backend test exists now and does what AC1 means: it
patches validate_junction_limits with a thread-recording wrapper inside the
real HA harness — on the event loop that would be MainThread — and proves
the verdicts survived the move (a clean write is accepted, a write adding a
spike is refused with junction_limit_angle). Spec revision 4 rewrites AC1
around this invariant instead of a fragile millisecond assertion.
M2: §4.6 equivalence is now behavioural on both sides (three boundary
fixtures each: as-is counts equal through-migration counts, TS and python),
and the parity suite gained the §7 boundary fixtures (exact 15°, exact
20 cm, the thickness-step filler run, exact 5 cm).
H1 was already closed by 7513f93d (the review ran on the previous HEAD):
check-docs is green on this tree — the screenshots and their manifest come
from one capture run.
Issue: #330
User-Visible: no
The rebase resolved docs/images/screenshots.json to the dev side while the
PNG files stayed from this branch's capture — CI correctly refused the
mismatched pair. One local capture regenerates both halves from the same
run, so hashes and the source fingerprint agree again.
Issue: #330
User-Visible: no
Writing the §5 benchmark honestly exposed a cost the point measurements of
П1-П4 could not see: П5 recomputed the full junction topology and masonry
union PER ROOM — 4.2 s per candidate on the benchmark grid. Revision 3 adds
the shared-pass cut (one topology pass per check, the union only when
multi-wall nodes exist, and the resize path reusing its own preflight
artifact) with the measured numbers. Budgets in §5 already assumed the fix;
they are now achievable and proven by the passing benchmark.
Issue: #330
User-Visible: no
The first AC4 unit exercised a re-implementation of the cache algorithm, so
the stale-cache mutant patched houseplan-card.ts and the unit stayed green —
the exact "looks like protection" failure the mutation gate exists to catch,
and it caught mine. The monolith is not compiled into test-build, so the
contract is pinned by source (the #293 technique): the epoch check, the
document-identity key and the §4.6 as-is branch must be present in
_junctionLimitsIntroduced. The behavioural half of AC4 lives in the smoke's
real pointer gesture (resizeBaselineCachedPerGesture).
Issue: #330
User-Visible: no
Six cuts, zero verdict changes (spec §3; equivalence pinned by units, the
parity suite and the smokes):
- §4.1 the CPU chain of ws_config_set and ws_plan_optimize runs in the
executor; write_lock still serialises writes, only the HA event loop is
freed (2.8 s of blocking per 576-atom write before).
- §4.2 the stored document's violation counts are cached on the runtime by
rev (store.py junction_baseline); a repeated write never re-judges
`previous`. validate_junction_limits takes baseline_counts and returns the
candidate's counts to cache after a successful save.
- §4.3 П3 builds its node index once per check in both mirrors
(289→11 ms TS, 285→~50 ms py).
- §4.5 П4 uses a bucket grid with the threshold as cell size in both
mirrors (104→19 ms TS, 372→44 ms py); pair enumeration switches to
lexicographic order — same verdict set, equivalence pinned against a
brute-force oracle on cell borders.
- §4.6 a document already carrying the current catalogue is judged as-is:
a no-op re-migration cost 815 ms py / 69 ms TS. Legacy documents migrate
exactly as before (the #329 H1 test stays green).
- §4.7 П5 shares one junction-topology pass per check and pays the masonry
union only when multi-wall nodes exist — and the resize path hands over
the preflight's own artifact, so a pointermove never builds the union
twice (4.2 s → 88 ms full candidate on the benchmark grid).
The frontend baseline is cached per (document identity, config epoch): ten
pointermoves make N+1 limit computations, not 2N — pinned by the smoke on a
real pointer gesture.
demo/benchmark_junction_limits.mjs (npm run benchmark:junction-limits) pins
the budgets for both mirrors: TS full candidate ≤100 ms (measured 88), py
warm validate ≤250 ms (measured 45), cold legacy ≤3.5 s — that path is
one-off and lives in the executor.
Issue: #330
User-Visible: yes
r1-H1 was right twice: the rev cache never touched the candidate's migration,
and П4 is architecturally quadratic. Profiled instead of guessing: the money
is not in deepcopy (3 ms) but in _atomize (663k distance calls), and it runs
even for a document that already carries the current catalogue — 815 ms
python / 69 ms TS for a no-op migration. Two new cuts follow: §4.5 bucket
index for П4 (prototype: 372→44 ms, identical verdicts) and §4.6 current-
version documents are used as-is (an explicit revision of the "both sides
through one migration" wording, guarded by a new parity case: v9 input gives
the same verdict with and without migration).
r1-H2: AC4 now rests on the new benchmark that actually exercises the
junction code; benchmark_safe_resize is named as a non-proof. r1-M1: §9
adds the mandatory i18n/touch/risks/release sections.
Budgets in §5 are recomputed from measured post-fix prototypes with a 2-3x
allowance, including an honest row for the one-off cold legacy case.
Issue: #330
User-Visible: no
Four cuts, zero verdict changes: the ws_config_set validator chain moves to
the executor, the previous-document violation counts are cached by
config_rev, П3 builds its node index once per check in both mirrors, and the
frontend baseline is cached per config epoch. A new benchmark with budgets
pins the class of regression (O(n²) returning) in CI.
Measured on dev 2c20f2dc: a 576-atom plan costs 2.8 s in the HA event loop
per config write today; the spec's acceptance bar is ≤50 ms of loop time.
Issue: #330
User-Visible: no
Конвейер исполняет версию из ветки по умолчанию, поэтому файл обязан совпадать
в main и dev побайтово — это проверяет шаг `process.yml идентичен в main и dev`
в Validate. Содержательная правка сделана в dev (#343), здесь только копия.
Issue: #343
User-Visible: no
Ревьюер гонял tsc, юниты и сборку заново в каждом раунде, хотя Validate на том
же SHA уже зелёный. Промпт прямо это требовал. Теперь шаг `validated` спрашивает
у Validate состояние ровно этого SHA, и доказательство такое же строгое, как у
reuse-маркеров (#208): не «недавно было зелено», а completed success на этом
коммите. После ребейза SHA другой, прогона для него нет — ревьюер честно гоняет
сам, и промпт это говорит.
Что Validate не покрывает, в примечании названо отдельно: смоки по диффу,
golden при правке рендера, инварианты на конкретной конфигурации. Иначе
экономия превратилась бы в «CI зелёный, значит всё проверено».
scripts/pre-push-gate.mjs — локальный набор: tsc, юниты, смоки по диффу
(smoke-select), мутанты по диффу (mutation-gate --changed). Замер на реальном
диапазоне 953f675~1..953f675: 46 секунд на всё вместе с двумя смоками.
Три свойства, без которых набор бесполезен: не останавливается на первом
упавшем; громко перечисляет, чего не проверял; не претендует на полноту. Бандл
не собирает — раскладывает закоммиченный dist, а свежесть проверяет сам продукт
через assertFreshDemoBundle внутри смока.
В хуке выключен по умолчанию: 20-45 секунд на каждый пуш, включая пуш одной
строки документации, — цена осознанная, включается HP_PREPUSH_GATE=1.
Дельта-промпт для spec-ревью (пункт 2) уже существует: блок «объём разбора по
дельте» из #214 покрывает оба этапа и прямо называет «дифф файла ТЗ или тела
issue для spec». Ничего не добавлял.
Issue: #343
User-Visible: no
Прежде полный трек был бесплатен, а выбор лёгкого требовал обоснования. Цена —
2.9 ревью-документа на задачу и до шести на одну issue (#329, #316, #290), при
том что Medium-находки всё равно чинятся в той же задаче без отдельного цикла.
Порог не изменился: критерии §5 те же и обязательны все одновременно. Изменилась
сторона доказательства — в S2-analysis называется критерий, который задача НЕ
проходит, если идёт полным треком. «Обычный трек» без названного критерия
обоснованием не является.
Правка идёт и в AGENTS.md: там трек описан как «shortcut для мелкой работы», а
это ровно та формулировка, из-за которой полный трек остаётся умолчанием на
практике. AGENTS.md стоит вторым в порядке доверия, поэтому без него правка
канона поведение не меняет.
Бюджет четырёх циклов, арбитраж владельца, обязательность ТЗ на полном треке и
правило «ревью до мержа» не тронуты.
Issue: #338
User-Visible: no
Бандл собирался пятью job независимо: три шарда смоков, golden, перф-смок —
каждая гоняла `bundle:sync`, то есть `tsc --noEmit` плюс rollup. Теперь его
собирает `frontend` и выкладывает артефактом, остальные скачивают и раскладывают
`bundle-sync.mjs`. Подмену артефакта отдельной проверкой ловить не нужно:
assertFreshDemoBundle сверяет вшитый в бандл отпечаток с sourceFingerprint
выкачанного дерева, и каждая браузерная job делает это перед первым кадром.
`npm ci` остаётся во всех: браузерным job нужен playwright из node_modules, а не
только бандл. Артефакт node_modules был бы медленнее `npm ci` с тёплым кэшем.
docs, process-workflow-sync, provenance и process-gate стали шагами одной job
`preflight`. Независимость сохранена намеренно: у каждого шага
continue-on-error, вердикт в конце падает и перечисляет всё упавшее сразу.
Прежняя запись «краснеет сам и не роняет остальные» продолжает действовать — на
уровне шагов, с той же гранулярностью в логе.
hacs и hassfest не тронуты: предложение сузить их до dev и тегов уже выполнено
классификатором `changes` — на ветках задач они и так идут только при правке
манифестов, а на dev фильтров нет намеренно (гейт беты требует, чтобы «зелёный
Validate» значил одно и то же).
test/validate-workflow.test.mjs закрепляет то, что в диффе строк не видно:
висячая зависимость `needs` не роняет YAML, а молча пропускает job навсегда.
Три мутанта проверены руками — висячая зависимость, вернувшаяся вторая сборка,
шаг без continue-on-error, — каждый убит.
Issue: #336
User-Visible: no
Прежде эталон принимался только из артефакта CI: растеризация шрифтов на другой
машине может отличаться, а доказать обратное было нечем. Цена — два полных
прогона на каждый визуальный фикс, при версии матрицы 48 она платится часто.
Доказательство теперь эмпирическое: среда равна раннеру, если каждая сцена,
которую менять не собирались, совпала со своим эталоном. Расхождение
растеризации спрятать нельзя — оно задевает все сцены с текстом. Ревьюер
объявляет намерение через --expect-change, всё разошедшееся помимо списка
приёмку запрещает. Поэтому неверно угаданный тег образа не может испортить
эталоны: он может только не сработать.
То же правило независимо от среды запрещает «принять всё, чтобы CI позеленел» —
именно так эталон перестаёт быть эталоном, молча и одной командой.
scripts/golden-container.mjs снимает кандидатов в образе Playwright той же
версии, что залочена в package-lock. Хозяйский node_modules прячется анонимным
томом: он собран под Windows, и npm ci внутри контейнера сломал бы дерево.
Обёртка, а не правка demo/golden/accept.mjs, — намеренно. sourceFingerprint
включает ВСЕ .mjs из demo/golden, включая accept.mjs и policy.mjs, которые
исполняются после съёмки и ни одного пикселя изменить не могут. Их правка
объявляет устаревшими бандл и оба манифеста, то есть требует ровно того двойного
цикла, который эта задача убирает. Сужение корпуса отпечатка — отдельная задача:
сам source-fingerprint.mjs в корпусе, и одна пересборка бандла неизбежна.
Issue: #334
User-Visible: no
Mirror of the dev-side change: the scheduled run executes this file from
main while checking out the registry from dev, so the shard matrix must
live here too.
Issue: #332
User-Visible: no
Four independent cuts into the 2-4 hour full run, none touching the contract
"a mutant must turn its guard red":
- guardNeedsBundle: rollup runs only for guards that open the built bundle
(demo/ smokes, golden captures, bundle:sync) — 68 of 253 registry entries.
Unit and backend guards never read dist/ as a build artifact (verified
against every test that mentions dist/**: they read the git checkout or
synthetic files), so 185 mutants skip the most expensive step entirely.
- seedTestBuild + incremental tsc: the mutant worktree starts from the main
tree's warm test-build/ and .tsbuildinfo; tsc compares file hashes, not
mtimes, so the fresh checkout stays warm and only the mutated delta is
recompiled. This also speeds up the long guards that run tsc themselves.
- --changed[=range]: run only mutants whose patch files are touched by the
diff (origin/dev..HEAD by default). An empty selection is an honest success
with an explicit message — the full registry remains the pre-release
contract, per the workflow comment.
- --shard=i/n: deterministic interleaved slices; the workflow runs a 4-way
matrix, and a warm test-build step feeds every shard. Interleaving spreads
the expensive browser mutants across shards instead of clumping them.
Measured per mutant on this machine: unit 12-13 s (was ~50-70 s), backend
6 s, browser 32 s (unchanged — the bundle is genuinely needed there). Full
run estimate drops to ~70 sequential minutes, ~20 on four shards.
Unit coverage: guard classification on real registry shapes, a floor on both
classes so the split cannot silently collapse, changed-selection semantics,
and shard completeness/disjointness with an anti-clumping bound.
Issue: #332
User-Visible: no
The reviewer is right twice over. My previous commit fixed the red CI by
relaxing the contract — a guard could name a `.py` file — when the registry
already had a convention for exactly this case: every backend mutant runs
`node scripts/backend-test-guard.mjs <pattern> <file>`, which owns the python
executable choice and the `-k` selection. Bending a rule to fit my one-off is
the worse of the two possible fixes, so the contract goes back to demanding a
`.mjs` guard, and junction-limit-backend-raw-baseline now uses the helper and
targets the one test that proves the migration
(test_legacy_baseline_is_judged_after_the_same_migration).
Re-verified: registry --check clean, the mutant still catches its regression
1/1, npm test 1390 passed / 0 failed.
Issue: #329
User-Visible: no
The registry contract demanded that every guard name a `.mjs` file, which was
true until this task added the first backend mutant —
junction-limit-backend-raw-baseline is guarded by pytest, and the mutation-gate
job already installs it. My mistake: I ran `--check` and the single mutant
after adding it, but not the unit suite that owns the registry contract, so CI
caught what I should have.
The contract keeps its point: a guard must name a file that exists.
Issue: #329
User-Visible: no
AC10 was asserted, never shown. Optimize runs alignAllToGrid and
repairNearAxisRoomWalls, which move nodes by fractions of a centimetre, and
none of П1-П5 carries a margin wider than the grid step in general — so
"obviously true by construction" was not available.
Two units, both counting violations the way the write barrier does (each side
through commitWallSegmentModel first):
- the owner's fixture in legacy storage — the inherited apex is there before
Optimize, and no rule's count grows after;
- the П4 boundary — two rooms exactly 5 cm apart, where snapping could have
pulled a node under the limit, stay clean.
The first test asserts the baseline actually carries a violation, so it cannot
pass by measuring an empty plan; violationsByRule fails loudly if the space or
its catalogue goes missing, for the same reason. Spec revision 7 records the
proof and the other three review answers.
Issue: #329
User-Visible: no
The Russian guide carried the junction-limits section twice, word for word.
And both guides described Resize as silently stopping, in contrast to a toast
from drawing and Thickness — it stops AND names the rule once per gesture
(resize.limit_stopped, pinned by the smoke). Wording follows the code.
Issue: #329
User-Visible: no
002795f7 said "the clip helper and its cap plumbing are gone" while leaving
clipPolygonOutsideCap(), degenerateApexCaps() and the apexCaps ring field in
place — exported, uncalled and untested. They belong to the flat chamfer the
owner rejected; the apex now ends in one point on both faces, so the quads
have no caller and no meaning.
The orphaned JSDoc block that described degenerateApexCaps went with them, and
the wallBodiesGeometry documentation this change had earlier separated from
its function is reattached: the #329 constant and predicate now sit above it.
Golden verify stays green on the whole matrix, which is the evidence the
removed code was indeed dead.
Issue: #329
User-Visible: no
The limits read `wall_segments`, so a document older than the catalogue
reports no walls at all — and therefore no violations, whatever its geometry.
Comparing that raw baseline against a candidate the card had already migrated
counted every inherited violation as new, and a legacy plan could not take an
unrelated edit at all: renaming a room was refused with junction_limit_angle.
Spec §3 forbids exactly this, and the frontend had already learned the same
lesson in 4758767e; the backend mirror simply never got the second half.
validate_junction_limits now runs both documents through
commit_wall_segment_model before counting. A document that cannot be migrated
is not this validator's verdict — the wall-model barrier owns that error and
reports it with its own code — so it degrades to "no baseline to inherit".
The regression is pinned twice: a test that asserts the legacy baseline reads
clean raw and carries the apex once migrated, and the mutant
junction-limit-backend-raw-baseline. Both fixtures that exercise the barrier
were rebuilt as real documents (rooms plus walls), because the previous ones
put walls in wall_segments with no rooms and did not survive migration.
Issue: #329
User-Visible: no
The branch was rebased onto the extracted resize controller (#264), which
changes the source fingerprint the documentation screenshots are pinned to.
The images themselves are byte-identical — only the recorded fingerprint moves.
Issue: #329
User-Visible: no
Reviewed the candidate produced by the Linux CI job of run 33106626544 on
issue/329-junction-limits, where golden failed with exactly one line —
"missing-baseline sharp-apex-legacy-dark" — and accepted only that image. The
nine unrelated baselines whose bytes drifted in the same artifact were
restored to their reviewed versions, so this commit changes one picture.
Baseline-Reviewed: run 33106626544, job 98638432113 (Golden-кадры против принятых эталонов)
Release: v1.68.2
Issue: #329
User-Visible: no
custom_components/houseplan/junction_limits.py repeats П1-П4 for the write
barrier in websocket_api, counting per rule so an inherited violation still
round-trips, and raises JunctionLimitError with the stable code
junction_limit_<rule>.
П5 is deliberately not mirrored — it judges the rendered wall bodies, and a
second mitre/inset pipeline in Python would drift more dangerously than the
rule it guards. Optimize stays outside the check for the same reason migration
and import do: it repairs existing geometry.
test_parity_with_the_frontend_checks feeds identical fixtures to the TS
functions and to this module and demands the same verdict, so the two
implementations cannot silently diverge.
Issue: #329
User-Visible: no
The owner's spike room (≈9.9°, 15 cm walls) is extracted into
test/fixtures/329-sharp-apex.json and rendered on its own so a returning
trident, a flat chamfer or a jagged edge fails the pixel gate. Baseline
follows from the CI candidate, as the process requires.
Issue: #329
User-Visible: no
Measured what Resize itself already forbids: a room cannot be squeezed below
30 cm (two 15 cm walls), so П3 and П5 are unreachable through shrinking and
the gate merely fails closed there. П4 IS reachable on a fine grid, so the
smoke drags a real handle on a 2 cm grid: two rooms 10 cm apart, a 6 cm pull
would leave 4 cm between foreign nodes, the wall stops at 6 cm and exactly one
toast names the 5 cm rule.
Spec revision 6 records both the measurement and the two corrections it forces
on AC7a: a dimmed handle cannot express a per-step limit, and the plan is NOT
byte-unchanged — the allowed part of the gesture is a legitimate edit.
Issue: #329
User-Visible: no
П3 measures the WALL, not the catalogue atom: a short filler segment that
compensates a thickness step (owner's fixture, 5 cm = (30-20)/2) is a legal
continuation of a long same-thickness wall, so the rule walks the maximal
collinear run through the shared nodes before judging the length.
Resize stops at the last allowed position and names the broken rule instead
of the generic "geometry cannot be saved"; the Thickness dialog refuses
through its own toast. Both channels are pinned by demo/smoke_junction_limits
plus three mutants (angle threshold, write barrier, degenerate apex bevel).
Issue: #329
User-Visible: yes
Owner decision (chat, 2026-08-27): keep П3 and fix the smoke. A 10 cm island
room is a column, and columns have their own tool (wall_columns) — that was
the reasoning behind the limit in the first place. The island of the smoke is
now 25 cm, and a new case pins the contract: a 10 cm island is refused.
Issue: #329
User-Visible: no
Owner report: small serrations remained on the outer edges between the inner
and the outer vertex. Measured on the fixture ring: two ~4 cm steps plus four
micro-vertices at the tip. Their source was the inset contour's two-point
bevel folding into a bow-tie, and the earlier half-plane clip of that fold,
which left a 0.2 cm sliver the boolean union turned into steps. The inset now
ends in ITS own mitre point at a degenerate apex — mirroring the sharp outer
tip — so there is no fold to clip and no sliver to smear: the room ring is
exactly three vertices, every side longer than the half depth. The clip
helper and its cap plumbing are gone. The user-visible wording of this work
already stands in both changelogs from the #329 entry.
Issue: #329
User-Visible: no
The baseline for inheritance was the raw previous document, which for a
legacy space carries no wall catalogue at all — so every inherited short
segment of a real plan looked new and the resize smoke's legitimate write was
refused (executed: two 5 cm segments against their own 30 cm thickness).
Both sides now cross commitWallSegmentModel first, and inheritance is counted
per rule rather than per subject, because a structural write re-keys the
carriers it re-atomises.
Issue: #329
User-Visible: no
Owner correction (chat, 2026-08-27): no flat chamfer at the tip — a plain
sharp apex. Proven by execution on the issue fixture: the outset contour fell
back to a two-point bevel (the 4·h mitre limit against an 87 cm reach) while
the inset contour folded into a bow-tie, and subtracting that fold carved the
V-notches — together they made the trident. Now a degenerate corner (below 15
degrees, inner faces meeting inside both walls) contributes ONE outset point
at the plan's own vertex — no bevel, no metres-long mitre needle — and its
inset is clipped at the convergence line so no fold is subtracted. Plain and
merely sharp pairs keep the full mitre of #310. The write-side limits of
П1-П5 stop new plans from creating such corners at all.
Issue: #329
User-Visible: yes
The owner's five limits as pure functions: minimum 15 degrees between
neighbouring rays of a node (a straight wall through the node is a 180 pair,
not a violation), at most 6 walls per node, a segment at least
max(20 cm, its own thickness), 5 cm clearance between non-incident nodes and
between a node and a foreign wall (a T-joint sitting exactly on that wall is
incidence, not a near miss), and a room interior of at least 25 cm2 after the
masonry is subtracted. Thresholds are absolute and do not scale with cell_cm.
newViolations() implements the spec's inheritance boundary: only violations
introduced by the write are reported.
Issue: #329
User-Visible: no
The bundle embeds the source fingerprint, which covers package.json; the
release:notes script addition moved it, so the three bundle copies must be
rebuilt in the same change.
Issue: #328
User-Visible: no
A stable body aggregates the changelog since the previous STABLE release,
not since the last beta; in-beta-only bugfixes are excluded by the curator
(the draft lists every candidate with its source section); the small-fixes
filler line is legal only when the range carries user-visible work not
itemised in the body — a single-issue hotfix ships without it, and body
bullets must link their issues so the rule stays checkable.
scripts/release-notes.mjs prints the aggregation draft and verifies
docs/RELEASE-NOTES.md (npm run release:notes -- <tag> [--verify]); the
verifier rejects the v1.68.1-style empty filler by execution. STATUS.md
release mechanics updated in the same commit.
Issue: #328
User-Visible: no
The test pins the literal workflow name and the release-gate label; both
moved to the Russian names of #327.
Issue: #327
User-Visible: no
(cherry picked from commit 6a3ac52258)
Owner decision (chat, 2026-08-27): the running check's name must say what it
does, in Russian. Scripts locate workflows by file name (release-gate.mjs ->
validate.yml), so display names are free; job ids and needs are untouched.
The same content is cherry-picked to main because release workflows execute
from the default branch and process.yml must stay identical in main and dev.
Issue: #327
User-Visible: no
(cherry picked from commit 1a8b480355)
Owner decision (chat, 2026-08-27): the running check's name must say what it
does, in Russian. Scripts locate workflows by file name (release-gate.mjs ->
validate.yml), so display names are free; job ids and needs are untouched.
The same content is cherry-picked to main because release workflows execute
from the default branch and process.yml must stay identical in main and dev.
Issue: #327
User-Visible: no
Revert of caf6e6c3. The owner's field report disproved the diagnosis: HACS
2.0.5 downloads zip_release assets fine, because async_download_file strips
the 'tags/' prefix from the composed URL before requesting (hacs/base.py) —
the prefix only survives in the error-log message, which prints the
unnormalized URL. The dacha installed every beta and v1.68.0 through HACS
with zip_release active. The observed 'Failed to download' was a transient
network failure fetching release-assets.githubusercontent.com (five retries
exhausted, one occurrence), not a routing bug.
Issue: #325
User-Visible: no
(cherry picked from commit 5ad0c1cdc3)
Revert of caf6e6c3. The owner's field report disproved the diagnosis: HACS
2.0.5 downloads zip_release assets fine, because async_download_file strips
the 'tags/' prefix from the composed URL before requesting (hacs/base.py) —
the prefix only survives in the error-log message, which prints the
unnormalized URL. The dacha installed every beta and v1.68.0 through HACS
with zip_release active. The observed 'Failed to download' was a transient
network failure fetching release-assets.githubusercontent.com (five retries
exhausted, one occurrence), not a routing bug.
Issue: #325
User-Visible: no
Owner-approved emergency stable hotfix; the release commit changes only version fields, generated bundles and release metadata.
Issue: #324
User-Visible: yes
Owner-approved emergency stable hotfix; the release commit changes only version fields, generated bundles and release metadata.
Issue: #324
User-Visible: yes
Installing a specific version from the HACS catalog fails: HACS composes the
zip_release asset URL from its internal ref 'tags/<version>' without
stripping the prefix (hacs/integration base.py:932 -> download_zip_files ->
github_release_asset), requesting releases/download/tags/v1.68.0/... which
GitHub cannot serve. A mirror tag with a slash is not servable either.
Without zip_release HACS falls back to the regular tag-tree download that
worked before 2026-08-08. Release assets keep being attached
(release-zip.yml untouched) so re-enabling after the upstream fix is a
one-line revert.
Issue: #325
User-Visible: no
(cherry picked from commit caf6e6c355)
Installing a specific version from the HACS catalog fails: HACS composes the
zip_release asset URL from its internal ref 'tags/<version>' without
stripping the prefix (hacs/integration base.py:932 -> download_zip_files ->
github_release_asset), requesting releases/download/tags/v1.68.0/... which
GitHub cannot serve. A mirror tag with a slash is not servable either.
Without zip_release HACS falls back to the regular tag-tree download that
worked before 2026-08-08. Release assets keep being attached
(release-zip.yml untouched) so re-enabling after the upstream fix is a
one-line revert.
Issue: #325
User-Visible: no
CODE-REVIEW-323-r1 M1: the full guides README links to still taught the
custom-repository flow; step 1 now matches the README wording.
Issue: #323
User-Visible: no
The HACS row still described an open queue of 835 PRs; the fresh-install
checklist still started from a custom repository.
Issue: #323
User-Visible: no
Since 2026-08-25 the integration is in the HACS default catalog
(hacs/default#9004); the badge and both install sections no longer route
users through Custom repositories — a plain HACS search finds it.
Issue: #323
User-Visible: no
Package the two beta.3 write-blocker fixes: the model-upgrade stale-client
guard and the self-resolving zero-wall migration.
Issue: #316
Issue: #319
User-Visible: yes
The r3-M1 fix was applied as a mechanical substring replacement and flipped
BOTH model_version conditions in the file; the independent room_wall_ids
invariant (#244/#252) silently stopped checking every v9+ document. Only the
opening_host requirement is scoped to model v8 — room_wall_ids is back to
'v8 and every later version', now pinned by its first regression test
(phantom wall_ids reported on v9 and v8; executed red on the broken
comparison, green after the fix).
Issue: #316
User-Visible: no
Since #316 §3.3 an unhosted contour opening is a valid v9 state — the
migration keeps an opening with no in-place carrier as data. The CLI still
reported it as an opening_host violation for every model_version >= 8; the
requirement now applies to model v8 documents only. The regression test is
proven able to fail on the old comparison (executed red), and the reviewer's
CLI reproduction now finishes clean.
Issue: #316
User-Visible: no
Pixels are untouched — the r1/r2 review fixes change migration data flow,
not rendering; every scenario hash stays byte-identical to the frames of the
reviewed CI run. Only the source fingerprint moves to match the fixed tree.
Issue: #316
User-Visible: no
Release: v1.68.0-beta.4
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33000824647
CODE-REVIEW-316-r1 H1: the §3.3 degraded pool picked an angle-compatible wall
at ANY distance, but the backend geometry-match invariant («wall opening
geometry must match its host») requires the host to agree with the opening's
own x/y — the migrated document was rejected by CONFIG_SCHEMA and the write
wedged again on the schema layer. The pool is removed from both migrations
(TS and the Python mirror): without an in-place eligible carrier the opening
goes straight to the unhosted degraded state, exactly the alternative the
spec's «assumed freely changeable» section reserved; the spec is revision 6.
New tests replay the reviewer's reproduction on both sides, and the frontend
test is proven able to fail by restoring the pool (executed red).
CODE-REVIEW-316-r2 M2: the schema-level host check is shared with #132
partition openings, so its unhosted relaxation is now pinned by a regression
test — a stale writer that keeps a partition-hosted opening but silently
drops its host is still rejected by validate_partition_opening_hosts.
Issue: #316
User-Visible: no
The scene was captured by CI run 33000824647 byte-identical to the locally
reviewed frame: the door keeps its solid jambs inside the former border, the
zero run is dashed on both sides, the leaf swings into the room.
Issue: #316
User-Visible: no
Release: v1.68.0-beta.4
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33000824647
Implements spec revision 4 (green r4). §3.1 — a legacy open_spans/open_to cut
never zeroes the atom that carries an existing contour opening: the opening's
edges become atom boundaries, the door keeps its real wall and the zero run
continues on both sides. §3.2 — an ambiguous carrier resolves
deterministically: current host, then distance, thicker cm, smaller id.
§3.3 — an opening with no usable carrier persists unhosted: a valid degraded
v9 state, inert in the physics, rendered by its own x/y, kept by later writes
and re-placeable in the editor (backend schema accepts it). §3.4 — the
initial migration never throws over an opening; a post-v9 write that LOST its
carrier keeps the fail-closed opening-host refusal. The Python migration
mirror implements the same rules with byte-identical output (verified on the
span+door fixture including the segment id).
The new smoke replays #316 end to end: a conflicted space no longer blocks
drawing on an empty plan. The golden scene span-over-door-migrated-dark
renders the migrated fixture pinned byte-for-byte to the real writer; two
gate mutants revert §3.1 and §3.4 and are red by execution.
Issue: #316
User-Visible: yes
A stale client can only echo the stored model_version, never raise it. The
'unchanged wall catalogue' refusal now applies only when the submitted model
is not above the stored one; the first v9 write over a v8 document with an
orphan open_span/open_to legitimately drops the legacy projection without
touching the catalogue and passes. The regression pair fixture is produced
by the real writers (stored: v1.68.0-beta.2, sent: current migration) and is
pinned on the frontend byte-for-byte so it cannot drift.
Issue: #319
User-Visible: yes
Pixels are untouched — the r1 fixes change diagnostics data and dialog state,
not rendering. Every scenario hash is byte-identical to the frames CI
captured and I reviewed for run 32940625718; only the source fingerprint
moves to match the fixed tree.
Issue: #295
User-Visible: no
Release: v1.68.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32940625718
CODE-REVIEW-295-r1, both Medium findings:
M1 — _preflightDiagnostics hashed this._serverCfg, the saved config a space
export would reproduce anyway. The hash now comes from the candidate the
preflight actually judged: the report path passes r.config / d.config and the
copy button reads the dialog's own candidate. The smoke no longer masks the
difference — its dialog carries a candidate whose geometry differs from the
saved config, and swapping the two must change the reported hash.
M2 — the inline clipboard fallback survived dialog close and reopen, so a
later refusal could hand the previous refusal's JSON to a bug report. The
field now dies with its dialog: reset on open (_previewAlignDialog) and on
every close path (escape, mode reset, successful apply, hp-close, cancel).
Both regressions are pinned by execution: reverting either fix turns the
extended smoke red (fingerprintTracksCandidate / fallbackClearedOnClose),
and two new gate mutants keep it that way.
Issue: #295
User-Visible: no
Both scenes were captured by CI run 32940625718 (byte-identical to run
32939996348), visually reviewed: failure reasons render as rows, the
ghost copy button is borderless per dialogs.styles.
Issue: #295
User-Visible: no
Release: v1.68.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32940625718
Диалог «Оптимизировать» при отказе перечисляет причину по каждому
пространству (7 значений OptimizeGeometryFailureReason получили RU/EN
строки), даёт «Скопировать диагностику» — JSON-блок с origin: runtime,
версией карточки, отпечатками и классами исключений (граница приватности
checkOptimizeGeometry; privacy-тесты дополнены позитивной проверкой) — и
пишет одну структурированную запись в dev-лог (дедупликация по fingerprint).
При недоступном clipboard блок раскрывается прямо в диалоге.
Совет «обновите House Plan» больше не безусловный: websocket
houseplan/config/get теперь возвращает integration_version (бэкенд-тест),
и подсказка показывается только при реальном расхождении с версией карточки;
старый бэкенд без поля — подсказки нет.
Три новых мутанта (потеря причины в диалоге, блок без reason, отключённый
dev-лог) — краснота каждого проверена исполнением; смок
smoke_preflight_diagnostics на dev падает.
Issue: #295
User-Visible: yes
resolveValidationRange already knows how to replace a force-push-orphaned
BEFORE_SHA with origin/dev, but the CLI handed it a runner that killed the
process with exit 2 on the first cat-file instead of throwing into
gitObjectExists' catch. Every push after a mandatory issue-branch rebase
therefore painted process-gate red (runs 32939996348, 32940625718,
32942113142). The regression test drives the real CLI in a throwaway repo
with a BEFORE_SHA that no longer exists.
Issue: #315
User-Visible: no
USER-GUIDE.ru.md — канон формулировок интерфейса (AGENTS.md): флаг диалога
привязки называется «Показывать сущности», как в трёх местах гайда и в
преобладающей форме флагов самого словаря. en.json не меняется.
Issue: #269
User-Visible: yes
По находкам CODE-REVIEW-313-r1:
High — резолвер больше не превращает сохранённый 0 сегмента драфта в 15:
ноль — легитимное значение (docs/WALL-THICKNESS.md §6), к дефолту 15 падает
только ОТСУТСТВУЮЩАЯ запись. Смок дополнен: hit нулевого сегмента несёт 0,
диалог показывает пустое поле, Apply без правки отказывает и не портит
данные.
Medium — гвард #278 усилен: паттерн допускает перенос строки после скобки,
и счётчик требует РОВНО ДВЕ точки коммита wall_thickness — мутация любой из
них (включая новую независимую) красит юнит; краснота второго патча мутанта
проверена изолированным исполнением.
Issue: #313
User-Visible: no
Кандидаты _wallThickHit расширены: интервалы комнат ∪ перегородки ∪ сегменты
сохранённых драфтов (активная цепочка исключена, как в снап-геометрии); при
точном наложении побеждает независимая кладка — она владеет хит-зоной и
рисует видимое тело (решение владельца, согласовано с select и кейсом #308).
Диалог для независимой кладки без кнопки «на всю комнату»; запись — в
partition.cm / draft.segments[i].cm той же физической транзакцией с одним
Undo. Ноль/пусто для независимой кладки отклоняется существующим тостом
диапазона; switch записи — единственный шов, куда #306 повесит ветку
«ноль превращает перегородку в виртуальную стену».
Мутант wall-thickness-writer-bypasses-common-barrier расширен вторым патчем
на новую точку коммита (#278-гвард), краснота обоих проверена исполнением.
Смок smoke_wallthick_standalone: hit/диалог/запись/отказ нуля/приоритет
наложения/hover — на dev падает.
Issue: #313
User-Visible: yes
Синхронизация process.yml с dev: шаг слияния сверяет вершину ветки с SHA
материала ревью (допустим ровно один doc-коммит публикации поверх) и при
расхождении отменяет слияние с возвратом в S6-in-progress. Конвейер
исполняется из ветки по умолчанию — правка обязана жить в обеих ветках
(process-workflow-sync).
Issue: #312
User-Visible: no
Правило 10 гейта (#311): DoR сверяется с моментом НАПИСАНИЯ кода — authorDate
коммита класса A не может предшествовать первому labeled-событию S5-ready+
из timeline issue; продвижение метки больше не прячет нарушение, ребейзы
конвейера его не смывают (authorDate переживает их). Проверка вторичная к
правилу 8: недоступный timeline — warn, правило 8 остаётся fail-closed.
LOG_FORMAT несёт authorDate третьим полем (append-совместимо).
Шаг слияния конвейера (#312): сливается только проверенный SHA — вершина
ветки сверяется с материалом ревью (допустим ровно один doc-коммит публикации
с диффом только docs/reviews/ поверх); расхождение отменяет слияние с
возвратом в S6-in-progress тем же путём, что конфликт (инвариант «метка
меняется всегда» сохранён). PROCESS.md §2.7 фиксирует правило «вердикт
привязан к SHA» и для ревьюера.
Issue: #311
Issue: #312
User-Visible: no
Ревью шло по ветке как есть, слияние делало ребейз: проверенный SHA и
слитый SHA были разными коммитами. Текстовое расхождение ловил конфликт,
смысловое git склеивал молча — так пришёл регресс #234. Заодно конфликт
обнаруживался после сорока минут работы ревьюера, хотя виден до них.
Новый шаг для этапа code, сразу после выбора ветки: потомок dev —
ничего; отстала и ребейзится — ребейз, push с --force-with-lease, ревью
приведённого состояния и запись о ребейзе в промпт (§7.2 требует полного
разбора); конфликт — возврат в S6-in-progress без запуска ревью.
Issue: #257
User-Visible: no
The previous change replaced the document path with REVIEW_DOC and left a
parenthetical hanging: the prompt jumped from "create no files in the
repository" straight into "(SPEC for the spec stage, CODE for code): scope,
how it was checked…", with the sentence that introduced the document
structure gone. The prompt now says plainly that the publish step derives
the name in docs/reviews, and the content requirements start a paragraph of
their own.
Issue: #220
User-Visible: no
Three code-review rounds on #220 published a verdict and then failed the
run: the document never reached the branch, so the #171 guard refused
before the label step and neither the merge nor S8-merged happened. The
cause was structural. The document lived as an untracked file inside the
very checkout the reviewer edits while proving that a test can fail, and
restoring that tree — git checkout, git clean — deletes an untracked file.
Spec rounds survived only because they never mutate anything.
The reviewer now writes to REVIEW_DOC under RUNNER_TEMP, outside the
repository, and the publish step copies it into docs/reviews before
committing. Tree cleanup can no longer destroy the artefact, and the
reviewer no longer needs to touch docs/reviews at all.
Verified against a local git fixture on five paths: document outside the
repo with a mutated tree (published, and the code mutation does not leak),
nothing anywhere (loud failure, exit 1), document only in the working copy
(still published — the clean exclusion stays for exactly this), document
already committed by the reviewer (recognised, no duplicate), and a branch
that moved during the review (rebased, both commits kept).
Issue: #220
User-Visible: no
The pipeline punished what it prescribed: after a failed merge it tells the
author to rebase and restore S7-code-review, and that attempt finished the
budget. On #225 (light track, limit 2) the sequence yellow, green, rebase
produced review-4 on a task whose code review was green and whose CI was
green, with no product change after the verdict — the owner had to
arbitrate work that was already accepted.
A cycle under section 4 is a verdict with blocking findings followed by a
return to the author, so only yellow and red verdicts spend the budget now.
A green verdict returned nothing and consumes nothing, which also removes
any need to mark rebase re-runs specially.
Attempts and cycles are now separate quantities. The attempt number keeps
naming the document, because two runs sharing a number would overwrite each
other's review artefact, while the limit compares blocking cycles only. The
exhaustion comment lists the verdicts it counted, and the guard no longer
strips review-4 — it reports the recount and leaves the decision with the
owner.
Rule 7 of the process gate follows: its document threshold rises above the
cycle limit, because legitimate attempts can exceed cycles and a threshold
equal to the limit would refuse the very rebase the pipeline demands.
Issue: #227
User-Visible: no
2026-08-20 23:17:16 +03:00
1819 changed files with 318995 additions and 34936 deletions
- **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.
| **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/houseplan-card.js` is no longer committed (#255): build it with `npm run bundle:sync` |
| **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` |
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`,
@@ -146,8 +157,21 @@ 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. Resolve it, push the branch, re-apply
`S7-code-review`. The second review run is not a formality — after a rebase onto a
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.
@@ -165,32 +189,36 @@ way. The layout is therefore fixed:
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 and the infrastructure agent own no local tree.** The reviewer
runs in CI on a fresh checkout. The infrastructure agent reads via `git show`
and publishes through the GitHub API; it makes no local commits at all, so it
needs no `HEAD` of its own. Its scratch worktrees live outside the repo and are
pruned after use.
- **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.
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.
## Two-agent workflow
## Agent-neutral workflow
**Codex** writes analysis, specs and all product code. **Claude** reviews specs and
code and owns infrastructure and distribution. The owner rules on disputes, closes
issues and commands releases.
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 different models, which is what "a fresh session without
implementation context" means in practice. The reviewer never edits product code;
the author never grades their own work.
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 runs outside this flow.** CI, scripts, labels, demo
stands, the landing page and distribution are Claude's alone, and running them
through spec-writing and review buys nothing: the spec would restate what is
already unambiguous, and author and reviewer would be the same role. So no spec
file, no spec review, no code review, no walk through `S1`…`S8`.
**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
@@ -200,17 +228,28 @@ 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, `typecheck`, `test` and`build` are green, and any non-obvious decision is
written down in the code or the issue rather than kept in someone's head.
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, which reviews without anyone asking and takes ten to forty-five minutes.
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 30 times. A single long sleep hits
the command timeout. Watch the **label**, not the comment: the label is the state,
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.
@@ -274,14 +313,20 @@ 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.
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
`docs/specs/<NN>-<slug>.md`, linked to its issue in both directions. 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.
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.
@@ -317,14 +362,42 @@ 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 all three bundle snapshots in sync — CI compares them
byte-for-byte:
After building, keep the complete manifest-driven bundle trees in sync — CI
House Plan turns Home Assistant into a live map of your home. Upload a plan or
draw rooms directly on the dashboard, bind them to Home Assistant areas, and
House Plan turns Home Assistant into a live map of your home. Open the dedicated
**House Plan** item in the Home Assistant sidebar, upload a plan or draw rooms,
bind them to Home Assistant areas, and
the area's devices appear automatically. You can immediately see where a light
is on, a door is open, a room is too cold, Zigbee signal is weak, or a leak
sensor has fired.
@@ -54,7 +55,7 @@ sync across screens.
## Your first working room
1. Install the integration and add the card to a dashboard.
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
@@ -88,31 +89,78 @@ The [Background editor contract](docs/DECOR-EDITOR.md) and
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=Matysh&repository=houseplan-card&category=integration)
1. In HACS open **⋮ → Custom repositories**.
2. Add `https://github.com/Matysh/houseplan-card` as an **Integration**.
3. Install House Plan and restart Home Assistant.
4. Open **Settings → Devices & services → Add integration → House Plan**.
House Plan is in the HACS default catalog — no custom repository needed.
The card is registered automatically. If you manage Lovelace resources
manually, use the URL served by the integration:
1. In HACS search for **House Plan** and install it.
2. Restart Home Assistant.
3. Open **Settings → Devices & services → Add integration → House Plan**.
The sidebar page and optional dashboard cards are registered automatically.
After installing or updating House Plan,
restart Home Assistant and fully reload the page: `Ctrl+F5` on Windows/Linux or
`Cmd+Shift+R` on macOS.
#### Storage mode (Home Assistant default)
No YAML is normally needed. If automatic registration did not make the card
available, open **Settings → Dashboards → menu ⋮ → Resources → Add
resource**, enter `/houseplan_files/houseplan-card.js`, and select **JavaScript
module**.
#### YAML resources mode (Home Assistant 2026.2+)
To manage resources in `configuration.yaml` independently of the dashboard
mode, use:
```yaml
resources:
- url:/houseplan_files/houseplan-card.js
type:module
lovelace:
resource_mode:yaml
resources:
- url:/houseplan_files/houseplan-card.js
type:module
```
Do not use the on-disk path inside `custom_components`; Home Assistant does not
serve that path as a JavaScript module.
#### Legacy Home Assistant 2024.6–2026.1
Only for a full-YAML dashboard that is already managed in YAML, use:
```yaml
lovelace:
mode:yaml
resources:
- url:/houseplan_files/houseplan-card.js
type:module
```
`mode: yaml` changes the dashboard itself to YAML mode. Do not switch a storage
dashboard to legacy YAML just for House Plan; use the Storage mode instructions
above instead. Do not use the on-disk path inside `custom_components`; Home
Assistant does not serve that path as a JavaScript module.
### Manual installation
Copy `custom_components/houseplan`to `config/custom_components`, restart Home
Assistant, and add the House Plan integration.
Copy the complete `custom_components/houseplan`release folder to
`config/custom_components`, restart Home Assistant, and add the House Plan
integration. Do not copy only `houseplan-card.js`: the card also uses an
internal manifest and content-hashed modules from the same release.
### Add the card
### Open House Plan
Create a dashboard view (Panel works best) and add the card in the UI or as:
After the integration is added, open **House Plan** in the Home Assistant
sidebar. This full-page view is the primary entry point and needs no dashboard
or YAML setup. It remembers the last space but always returns from another HA
page in View rather than reopening an editor. Users without editing permission
see the same live plan without editor controls.
If the sidebar entry cannot be registered, the integration and existing
dashboard cards continue to work; check **Settings → System → Repairs → System
information → House Plan** after restarting and hard-refreshing HA.
### Optional dashboard card
Add the card only when House Plan must be embedded in a dashboard. In a Sections
view it requests full width by default, while a manual size chosen in HA remains
House Plan превращает Home Assistant в живую карту дома. Загрузите изображение
плана или нарисуйте комнаты прямо на дашборде, свяжите их с зонами Home
House Plan превращает Home Assistant в живую карту дома. Откройте отдельный
пункт **House Plan** в боковом меню Home Assistant, загрузите изображение плана
или нарисуйте комнаты, свяжите их с зонами Home
Assistant — и устройства появятся на плане автоматически. Сразу видно, где
горит свет, открыта дверь, слишком холодно, слабый Zigbee-сигнал или сработал
датчик протечки.
@@ -55,7 +56,7 @@ Assistant — и устройства появятся на плане авто
## Первая рабочая комната
1. Установите интеграцию и добавьте карточку на дашборд.
1. Установите интеграцию и откройте **House Plan** в боковом меню Home Assistant.
2. Создайте первое **пространство**: загрузите SVG/PNG/JPG/WebP либо выберите
вариант без изображения, чтобы нарисовать план вручную.
3. В редакторе «План» выберите **Контур комнаты**, поставьте вершины и замкните
@@ -91,31 +92,79 @@ Assistant — и устройства появятся на плане авто
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=Matysh&repository=houseplan-card&category=integration)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.