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
Ревьюер гонял 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
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
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
Кадры прежние — golden и смоки подтверждают пиксельную идентичность; коммит
догоняет dev после мержа, прошедшего без него (пуш ветки и мерж конвейера
разошлись на один коммит).
Issue: #266
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
601 changed files with 81077 additions and 28197 deletions
# Событийный конвейер процесса (PROCESS.md). Смена статусной метки — это
# сообщение: она порождает событие, событие запускает следующий шаг.
@@ -35,6 +35,7 @@ permissions:
jobs:
guard:
name:"Страж: ребейз на dev и предпосылки ревью"
runs-on:ubuntu-latest
outputs:
stage:${{ steps.decide.outputs.stage }}
@@ -162,6 +163,7 @@ jobs:
echo "limit=$limit" >> "$GITHUB_OUTPUT"
review:
name:"Ревью (Claude): вердикт в issue"
needs:guard
if:needs.guard.outputs.stage != ''
runs-on:ubuntu-latest
@@ -287,6 +289,16 @@ jobs:
echo "note=Ветка приведена к dev конвейером до ревью: поверх легло $behind коммит(ов) dev, $short_before -> $short_after. После ребейза это другой код (§7.2) — разбор полный, а не по дельте." >> "$GITHUB_OUTPUT"
echo "ветка $BRANCH приведена к dev: $short_before -> $short_after"
# Материал ревью — конкретный SHA (#312). Вердикт применим только к
# нему: если во время ревью в ветку прилетит коммит, шаг слияния обязан
# это заметить и отказаться, а не молча увезти в dev непроверенный код.
echo "**Дешёвые гейты на этом SHA уже подтверждены** (#343). Validate на \`$short\` завершился success: $url"
echo ""
echo "Значит \`npx tsc --noEmit\`, \`npm test\` и \`npm run build\` со сверкой копий бандла перегонять не нужно — сошлись на этом прогоне, назвав его ссылкой. Бюджет раунда тратится на чтение кода."
echo ""
echo "Что Validate НЕ покрывает и остаётся за тобой: смоки, выбранные по диффу; golden, если diff трогает рендер; инварианты модели на конкретной конфигурации; и любой гейт, который требуют AC задачи."
else
echo "**Зелёного Validate на этом SHA (\`$short\`) нет** — прогон не найден, не завершён либо не success. Дешёвые гейты прогоняешь сам и называешь результат."
fi
echo 'EOF_NOTE'
} >> "$GITHUB_OUTPUT"
if [ -n "$row" ]; then echo "Validate на $short: зелёный"; else echo "Validate на $short: зелёного нет"; fi
# Зависимости ставятся ПОСЛЕ переключения на ветку задачи: lockfile мог
# измениться именно в ней, и установка по копии из dev дала бы не то дерево.
- name:Установить зависимости
@@ -441,8 +488,10 @@ jobs:
правке — не тщательность, а потеря времени: полные наборы это
предрелизный гейт (PROCESS.md §8), а не гейт ревью.
Всегда, они дешёвые, и в повторном раунде тоже: код изменился,
а стоят они минуты:
${{ steps.validated.outputs.note }}
Если зелёного прогона на этом SHA нет — прогоняешь сам, они дешёвые,
и в повторном раунде тоже: код изменился, а стоят они минуты:
`npx tsc --noEmit`, `npm test`, `npm run build` со сверкой трёх
копий бандла. Плюс `node scripts/check-docs.mjs`, если diff трогает
`src/**`: отпечаток скриншотов документации считается по всему
**Слияние отменено: ветка изменилась после проверенного материала (#312).**
Ревью выполнялось на \\`$(git rev-parse --short "$reviewed")\\`, а вершина ветки сейчас \\`$(git rev-parse --short "$actual")\\` — в ней есть коммиты, которых вердикт не покрывает. Зелёный вердикт остаётся в силе только для проверенного SHA.
Задача переведена в \\`S6-in-progress\\`. Дальше: убедиться, что вершина ветки — именно то, что должно ехать в dev, и вернуть метку \\`S7-code-review\\` — новый заход ревью проверит актуальный код.
- **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`,
@@ -274,7 +281,9 @@ 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
@@ -317,12 +326,12 @@ 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
@@ -88,10 +88,11 @@ 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.
1. In HACS search for **House Plan** and install it.
2. Restart Home Assistant.
3. Open **Settings → Devices & services → Add integration → House Plan**.
The card is registered automatically. If you manage Lovelace resources
manually, use the URL served by the integration:
@@ -107,8 +108,10 @@ 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.
@@ -91,10 +91,12 @@ Assistant — и устройства появятся на плане авто
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=Matysh&repository=houseplan-card&category=integration)
1. В HACS откройте **⋮ → Пользовательские репозитории**.
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.