Commit Graph
299 Commits
Author SHA1 Message Date
Sergey Matyuninandclaude[bot] dbda64e89d test: catch stale danger locale gate
Issue: #434
User-Visible: no
2026-09-03 10:55:16 +00:00
Sergey Matyuninandclaude[bot] cba13621c8 fix: harden v1.71 audit boundaries
Issue: #434
User-Visible: yes
2026-09-03 10:55:16 +00:00
Claude d1091b6d47 test(decor): путь загрузки картинки под автотестом
`src/backdrop-pick.ts` и `src/decor-image-editor.ts` не входили в
tsconfig.test.json — юнитов у них не было вовсе. Смок при этом подменял
`hass.callWS` целиком и до `uploadFromInput`, `upload`, `delete` не
доходил; в шапке файла это было честно написано. AC1 #51 обещал
доказательство «component unit + smoke» — не было ни того, ни другого.

Цена известна: #427 — файл тяжелее 2 МиБ нельзя было добавить ничем,
гасли обе кнопки диалога, включая «уменьшить копию», — прожил четыре
круга ревью именно потому, что этот путь не проверяло ничто.

Оба файла теперь в tsconfig.test.json (компиляция +6 с, без правок
продуктового кода) и покрыты 13 юнитами:

- классификация: проходной файл, тяжелее лимита источника (guard с
  probe.kind='safe' — предупреждение о размере, не об отказе), warn,
  hard, unknown, неподдерживаемый формат;
- состав кнопок диалога на трёх случаях. Декор свыше лимита: «Отмена» и
  «уменьшить копию», без «оставить оригинал» — это и есть #427. Подложка
  на том же файле сохраняет обе кнопки, hard остаётся с одной «Отмена»,
  unknown предупреждает, а не проходит тихо;
- uploadFromInput: сброс input.value, молчание на занятом, отказ формата,
  исключение классификации, передача флага замены в оба слота;
- upload: multipart-поле и URL, порядок каталога, палитра против замены
  ссылки в диалоге, восемь ответов бэкенда с разными текстами, отпускание
  busy на отказе, запрет второго запроса поверх незакончившегося;
- delete: ссылки на ассет, отказ в подтверждении, чистка каталога, карты
  и палитры, различение in_use, сохранность каталога на отказе сервера;
- loadCatalog: отбор валидных строк и тост на отказе.

Смок `smoke_decor_images.mjs` получил последний блок, который проходит
настоящие uploadFromInput → upload → delete в собранном бандле, подменяя
только транспорт: fetchWithAuth и одну ветку callWS.

Свидетели (§2.7, #435). Девять мутаций прогнаны на собранном дереве,
каждая краснит свой тест: условие #427 → тест состава кнобок; снятие
лимита источника; input.value; флаг замены; too_large; busy в upload;
подтверждение удаления; used_by; проглоченный отказ каталога. Мутация
busy сначала вешала прогон и уносила пять тестов cancelledByParent —
тест переписан так, чтобы падать на сравнении, а не по таймауту.

В реестр добавлены три мутанта на продуктовый код: условие #427, потеря
флага замены, схлопывание кодов ошибок.

Гейты: npm test 1806 tests, 1805 pass, 0 fail (было 1791);
mutation-gate --check зелёный. Смок локально не прогнать — Chromium в
песочнице нет, проверит CI.

Issue: #433
User-Visible: no
2026-09-03 13:21:08 +03:00
Sergey Matyunin f3c32fb203 fix(assets): bound resolve integrity work
Issue: #432
User-Visible: yes
2026-09-03 12:48:08 +03:00
Sergey Matyunin 58efb6a1b2 fix(geometry): canonicalize decor image coordinates
Issue: #431
User-Visible: yes
2026-09-03 11:26:31 +03:00
Claude 8b9548775f test(backend): HA-зависимый тест объявляет это сам, а не именем файла
`python3 -m pytest tests_backend/` без Home Assistant обрывался НА СБОРКЕ:
`test_coordinate_canonicalization.py` тянет HA через `store`, а
`collect_ignore_glob` в conftest отсекает только `test_ha_*.py`. Ни один
из трёх сотен чистых тестов при этом не выполнялся, хотя CLAUDE.md и
PROCESS.md §8 обещают ровно обратное. В CI дефект невидим: там HA есть и
список игнора пуст.

Признак «нужен ли файлу HA» был подменён признаком «как файл назван» —
та же конструкция, которая в #389 уронила 85 тестов с голым assert False.

Вариант владельца — третий: `pytest.importorskip("homeassistant")` в
самом файле, до импортов, которые тянут HA. Теперь это честный скип
(«1 skipped» вместо «Interrupted»), остальные файлы прогоняются, а в CI
не скипается ничего.

Обещанная проверка остальных файлов сделана пофайловым collect: из
двадцати HA требует ровно один непоименованный — этот. Чтобы второй не
появился молча, добавлен статический гейт: он читает импорты (не
исполняет), строит множество модулей интеграции, тянущих HA, — с
замыканием по относительным импортам, потому что `import_export`
зависит от HA только через `store`, — и требует у такого теста либо имя
`test_ha_*`, либо importorskip.

Свидетели, каждый проверен отрицательным прогоном:
- снять importorskip → красный «файлам нужен HA, но они этого не
  объявляют»;
- новый чистый файл с импортом store → тот же красный;
- убрать замыкание → красный синтетический тест сканера;
- перестать исключать TYPE_CHECKING → красный он же;
- считать импорты внутри функций → красный он же.
Плюс два свидетеля самого сканера в теле гейта: `store` обязан быть
найден, `coordinate_canonicalization` обязан остаться чистым — иначе
«ничего не нашёл» выглядело бы как «всё в порядке».

Мутант `pure-backend-test-pulls-home-assistant` в реестре: добавляет
импорт store в чистый test_projection.py, guard — этот гейт.

Гейты: npm test 1791 tests, 1790 pass, 0 fail; pytest без HA
312 passed, 3 skipped (было: Interrupted, 0 выполнено);
mutation-gate --check зелёный.

Issue: #436
User-Visible: no
2026-09-03 11:06:25 +03:00
Claude 1a8a51f2d9 fix(gates): ключ смоков покрывает оснастку и пробы гарда
Прогон #2371 (ee678352) добавил в demo/guard/verify-guard.mjs пробу
гарда benchmark — и job «Смоки в браузере» была ПРОПУЩЕНА как
переиспользованная. Причина в HARNESS.smoke: ключ держал ровно
demo/smoke_*.mjs, а `demo/serve.mjs`, `demo/guard/**` и benchmark, который
проба запускает, в него не входили. Файл исполняется только в этой job,
его правка её ключ не меняла — проба уехала в dev, ни разу не
запустившись.

Пропущенная проверка выглядит точно как пройденная: это тот же дефект,
против которого заведён #430, только этажом выше — не тест не умеет
падать, а job не запускается. Заодно это значит, что правки #404 в
serve.mjs и любой из четырёх проб гарда всё это время могли быть
реюзнуты.

Критерий ключа теперь один и читаемый: всё, что job исполняет.
Benchmark'и берутся все, а не по имени, — любой может быть подключён к
пробам позже, и лишний прогон дешевле молчания. demo/fixtures/** не
добавлен: он в корпусе sourceFingerprint, который уже подмешан в ключ.

Тест проверяет обе половины: файлы попадают в оснастку И правка каждого
меняет ключ. На прежнем предикате он краснеет (проверено).

Гейты: npm test 1791 tests, 1790 pass, 0 fail. Этот коммит меняет ключ
смоков, поэтому job со смоками на нём прогонится по-настоящему — вместе
с той пробой, ради которой всё и затевалось.

Issue: #430
User-Visible: no
2026-09-03 10:38:47 +03:00
Claude ee678352c7 test(gates): свидетели пяти защитных контрактов #51 и #423
Аудит v1.71.0-beta.1 (§3.2 M1/M2/M3/M6/M9) прогнал по мутанту на каждый
контракт: пять снятий защиты не покраснили ни один тест. Восьмой подряд
случай проверок, не умеющих падать, и первый — в бете, закрывавшей #421,
задачу ровно об этом.

Каждый свидетель ниже проверен отрицательным прогоном: мутант краснит
ровно свой тест и не задевает остальные.

1. Полное декодирование растра. `test_supported_raster_headers_and_full_decode`
   спрашивал w/h/mime — их даёт header-парсер; обрезка `PNG_1X1[:33]`
   теряет IEND и отбивается там же. No-op блока Pillow оставлял 35 pass.
   Новый свидетель: PNG с верными сигнатурой, IHDR, IEND, длинами и CRC,
   но с текстом вместо zlib-потока в IDAT. Оба прежних теста
   переименованы — их имена обещали то, чего они не проверяли.
   Пропуск без Pillow остался (validate_asset глотает ImportError
   осознанно), но в окружении с Home Assistant отсутствие Pillow теперь
   красное само по себе — иначе свидетель молча скипался бы в каноне.

2. Канонизация SVG. `ValidatedAsset(canonical,…)` → `ValidatedAsset(data,…)`
   не краснило ничего: все тесты смотрели w/h/mime, ни один — байты.
   Свидетель сверяет байты целиком: пролог и комментарий не переживают
   канонизацию, пустой элемент сжимается.
   Вторая половина — `_check_size(canonical)`. Экранирование `>` в тексте
   раздувает документ вчетверо: загрузка 1.84 МиБ канонизуется в 7.35 МиБ
   и до сих пор проходила входной контроль. Свидетель — ровно такая.

3. Гард внешних URL. Все три «внешних» кейса корпуса ловились другими
   правилами (тег не из словаря, атрибут не из словаря, ветвь href), и
   `if False:` не краснило ничего. Свидетель: разрешённый тег, разрешённый
   атрибут, пять токенов — сработать может только сам гард, сообщение это
   подтверждает.

4. Проекция декора. Единственный кейс задавал flip_h и opacity: 2 → 1;
   ожидание неотличимо от «opacity игнорируется», а flip_v не проверялся
   вовсе. Свидетели: четыре комбинации отражений, opacity 0.4 и 0 против
   заглушки, форма asset_id с согласованным url (прежняя строка ловилась
   сравнением url, поэтому регулярку id можно было удалить незаметно).

5. Гард benchmark из #423. Доказательство было циклическим: тест вырезал
   из текста подстроку и спрашивал регулярку, находит ли она её, — а
   регулярка искала именно её. Динамический режим `--guard-probe`
   существовал с #423 и не вызывался ни одним прогоном. Теперь он в
   `demo/guard/verify-guard.mjs` (умеет аргументы и файл выше каталога),
   а тест из обнаружения требует у нового page-benchmark режим пробы и
   запись в верификаторе.

Мутантов в реестре стало восемь новых: четыре бэкендных, три юнитных,
один на пробах гарда. У #423 их было ноль — единственная задача с
циклическим тестом и она же единственная без мутантов.

Гейты: typecheck зелёный; npm test 1790 tests, 1789 pass, 0 fail;
pytest без HA 310 passed, 2 skipped; npm run build зелёный, dist не
изменился (продуктовый код не тронут); mutation-gate --check применяет
все восемь якорей. Полный прогон новых мутантов — следующим шагом.

Issue: #430
User-Visible: no
2026-09-03 10:28:00 +03:00
Claude 4cabcbe828 chore(gates): убрать числовой храповик бюджета из #423
Проверка `assertSupportBundleOwnership` бросала при
`initialViewGzipBytes >= 291046` — «граф не стал больше, чем на момент
закрытия #423». Запас до порога: пятнадцать байт на бете 1.71.0, сто
четыре сейчас. Пятнадцать байт gzip меньше одной строки локали, поэтому
первый же посторонний коммит получил бы красный CI с сообщением про
копирайт формы поддержки — к которому не имеет отношения.

«Граф не вырос» — критерий приёмки на момент задачи, а не свойство
продукта. Свойство продукта охраняют `INITIAL_VIEW_GZIP_BUDGET` (300000)
и предупреждение о низком запасе: они судят размер целиком и не
привязаны к чужому номеру issue.

Долговечная половина того же контракта — владение графом (маркеры формы
отсутствуют в initial View, присутствуют в ленивом редакторе) — осталась
и по-прежнему падает в обе стороны. Новый тест закрепляет, что размер в
эту функцию вернуться не может: 0, 291046, 10 МБ и `undefined` проходят
одинаково, пока владение соблюдено.

Гейты: typecheck зелёный, npm test 1787/1786 pass 0 fail, npm run build
зелёный (dist побайтово не изменился — продуктовый код не тронут),
node scripts/bundle-budget.mjs зелёный: initial View 290942 Б, бюджет
300000 Б, запас 9058 Б плюс честное предупреждение о запасе.

Issue: #429
User-Visible: no
2026-09-03 09:56:54 +03:00
Sergey Matyunin 223951c042 chore: refresh config schema manifest
Issue: #426
User-Visible: no
2026-09-03 09:16:22 +03:00
Sergey Matyunin 8013f7d052 feat: add room hover tooltip setting
Issue: #426
User-Visible: yes
2026-09-03 09:06:57 +03:00
Sergey Matyuninandclaude[bot] a265830e3b chore: refresh custom image config schema
Issue: #51
User-Visible: no
2026-09-02 21:56:04 +00:00
Sergey Matyuninandclaude[bot] dc95563d96 feat: add reusable custom decor images
Issue: #51
User-Visible: yes
2026-09-02 21:56:04 +00:00
Codex d4ecace64c test: cap the two frontend cores with a ratchet (#425)
User-Visible: no
Issue: #425
2026-09-03 00:26:47 +03:00
Codex 0f5161164f test: prove the drift gate itself can fail (#422)
User-Visible: no
Issue: #422
2026-09-02 22:38:51 +03:00
Codex 653b9a7632 docs: refresh the capture fingerprint after the clip extraction (#422)
User-Visible: no
Issue: #422
2026-09-02 22:14:41 +03:00
Codex d59ceee905 feat(gates): measure capture drift between runs and anchor reachability (#422)
User-Visible: no
Issue: #422
2026-09-02 22:10:16 +03:00
Codex 09c410000a fix: pin the compositor so a frame depends only on the commit (#424)
User-Visible: no
Issue: #424
2026-09-02 21:59:43 +03:00
Sergey Matyunin 2038ab91b9 fix: harden support feedback pipeline
Issue: #423
User-Visible: yes
2026-09-02 21:25:30 +03:00
Sergey Matyunin f4b425f3c0 test: prove three defensive contracts fail red
Issue: #421
User-Visible: no
2026-09-02 20:13:58 +03:00
Sergey Matyunin 489e289f79 fix: guard Area snapshot cleanup
Issue: #419
User-Visible: yes
2026-09-02 19:18:28 +03:00
Sergey Matyunin 2b978d63ef Fix support preview and retry races
Issue: #418
User-Visible: yes
2026-09-02 17:10:05 +03:00
Sergey Matyunin e46220f3eb Fix confirmation guards for unrenderable branches
Issue: #417
User-Visible: yes
2026-09-02 16:25:24 +03:00
Matysh 6fb7bbb6ab ci: anchor the review material to content, not to history
#413 закрыл класс «SHA мёртв уже в момент публикации». Остаётся более частый:
SHA был жив, а умер потом — по корпусу таких объявлений 98 из 804, потому что
ветку задачи после ревью перебазируют, сквошат или удаляют.

SHA коммита — свойство истории, а история переписывается. Содержимое не
переписывается: git адресует деревья и блобы их хешем. На #403 спец-коммит
переехал из 83005c3c в 94502d3d, а блоб ТЗ у обоих один — 56a92e12; по нему
материал находится одной командой независимо от ребейза.

Конвейер снимает якоря там, где читает материал — в шаге перехода на ветку
задачи, пока рабочая копия равна тому, что прочтёт ревьюер. В шаге публикации
спрашивать поздно: дерево уже сброшено на целевую ветку. При публикации якоря
дописываются машинным блоком: дерево материала и блоб каждого ТЗ, каждый со
своей исполнимой командой поиска.

Блок машинный и помечен как машинный. Ревьюер его не заполняет: дисциплина
ручного переписывания SHA здесь уже подвела, и заменять её другой ручной
дисциплиной смысла нет.

Гейт #413 смягчён ровно там, где обязан: осиротевший SHA при живых якорях —
предупреждение, а не отказ. Ронять раунд, который воспроизводим, было бы той
же ошибкой в другую сторону. Отказ остаётся, когда не работает ни один
объявленный способ найти материал.

Проверено на настоящем осиротевшем случае: блок, собранный для 94502d3d,
находит и дерево, и блоб ТЗ; тот же документ с якорями даёт предупреждение
вместо отказа, без якорей — отказ.

Issue: #416
User-Visible: no
2026-09-02 08:01:15 +03:00
Matysh 206732e9f5 ci: refuse a review round that cites an unreachable SHA
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
2026-09-02 07:45:39 +03:00
Sergey Matyuninandclaude[bot] 1a953718a7 test: cover help and feedback browser flows (#43)
Issue: #43
User-Visible: no
2026-09-02 00:05:37 +00:00
Codexandclaude[bot] c12ecad3e3 docs: require the trusted-proxy switch for the rate-limit source (#43)
User-Visible: no
Issue: #43
2026-09-02 00:05:36 +00:00
Codexandclaude[bot] 3bbe555ad8 docs: pin the rate-limit source and the webhook recipe (#43)
User-Visible: no
Issue: #43
2026-09-02 00:05:36 +00:00
Codexandclaude[bot] ca86e79501 feat(relay): deliver through the maintainer's Home Assistant webhook (#43)
User-Visible: no
Issue: #43
2026-09-02 00:05:36 +00:00
Codexandclaude[bot] 736a6c143e fix(relay): pin the rate-limit source to the proxy-supplied address (#43)
User-Visible: no
Issue: #43
2026-09-02 00:05:36 +00:00
Codexandclaude[bot] f36ded7fc2 feat(relay): receive support reports on the project stand (#43)
User-Visible: no
Issue: #43
2026-09-02 00:05:36 +00:00
Sergey Matyunin b4809a12ff fix: await draft deletion through the card facade (#405)
Issue: #405
User-Visible: no
2026-09-01 22:23:57 +03:00
Sergey Matyuninandclaude[bot] 372d838ec2 fix: close beta 2 polish gaps
Issue: #406
User-Visible: yes
2026-09-01 16:32:25 +00:00
Matysh 2903374b72 fix: the exception guard reads its counter after delivery, not before
Гард «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
2026-09-01 19:08:07 +03:00
Sergey Matyuninandclaude[bot] 355b0516c7 fix: preserve area relocation state
Issue: #403
User-Visible: yes
2026-09-01 15:08:22 +00:00
Matysh 8119c523fa fix: the screenshot witness floor comes from the set, not from survivors
Тот же дефект, что #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
2026-09-01 17:54:48 +03:00
Matysh cd0ecf4db2 fix: the witness floor comes from the matrix, not from surviving baselines
Порог свидетелей считался от числа сцен со статусом не 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
2026-09-01 17:25:54 +03:00
Codex 00b6f41233 fix: the danger confirmation lives outside render()'s branches (#402)
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
2026-08-31 16:47:05 +03:00
Matysh 633cb20e59 fix: follow Home Assistant Area marker moves
Issue: #126
User-Visible: yes
2026-08-31 09:45:53 +03:00
Matysh 5c8cb58e1f ci: prove the screenshot environment instead of trusting the place
Правило приёмки скриншотов было про место: снимать только в CI. Обоснование
измерено — съёмка в другом окружении переписывает файлы без содержательных
изменений, в #231 два из девяти на 7–8 байт, набор с беты все девять. Но
держалось правило на комментарии, а не на механизме: кандидат проверялся на
самосогласованность и принимался целиком, ни разу не сравниваясь с тем, что
лежит в репозитории.

Цена видна на #390: правка типов, которая физически не может сдвинуть
пиксель, потребовала прогона workflow, а затем правки одиннадцати полей
манифеста руками.

Теперь правило про доказательство, и оно то же, что у golden с #334: среда
доказана, если каждый кадр, который менять не собирались, совпал с
закоммиченным байт-в-байт. Расхождение растеризации спрятать нельзя — оно
задевает все кадры с текстом сразу. Снимать можно где угодно, включая WSL;
принять получится только оттуда, где кадры воспроизводятся, и перестанет
получаться в тот день, когда обновятся шрифты.

Остальное следует из того же правила: намерение объявляется
--expect-change, необъявленное расхождение останавливает приёмку,
объявленное без расхождения — тоже (ложная декларация обесценивает список),
заменяются ровно объявленные файлы, а тотальная перерисовка требует
--no-witnesses --reason, и причина уезжает в манифест.

Частый случай закрылся сам: ничего не объявлено, все кадры совпали —
принимается один манифест, руками ничего писать не надо.

Проверено шестью сквозными прогонами на подделанном артефакте, не только
юнитами: идентичный кандидат, необъявленное расхождение, объявленное,
молчаливая декларация, тотальная перерисовка без причины и с ней.

Issue: #401
User-Visible: no
2026-08-31 09:40:52 +03:00
Codex d153b20a1d fix: make the handle paint order a named decision and mutate it (#400)
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
2026-08-31 08:14:55 +03:00
Codex fe3b85c06b fix: the corner handle wins on small furniture, guides exclude the real drag (#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
2026-08-31 07:56:27 +03:00
Codex 5c4d8cba9e test: prove AC5 by running the scanner, not the predicate (#399)
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
2026-08-31 04:48:41 +03:00
Codex 98028a3093 ci: the backend gate now checks exactly what it promises (#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
2026-08-31 04:35:56 +03:00
Codex d9b6766362 test: the sys.modules guard now sees the write, not its spelling (#398)
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
2026-08-31 03:56:01 +03:00
Codex d87cc29804 fix: the card keeps the position it sent, so its echo is not foreign (#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
2026-08-31 02:36:55 +03:00
Codex 8f485c0b00 fix: the camera keeps the zoom you see and the point you hold (#396)
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
2026-08-31 01:36:59 +03:00
Codex dddbbe5522 ci: make the strict typing gate actually run (#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
2026-08-30 22:00:52 +03:00
Codex 569867487c fix: adopt the #392 dependency file as the single source (#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
2026-08-30 21:34:32 +03:00
Codex 9f50778df5 test: close the r4 mediums — structural tuple scan and reuse-key inputs (#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
2026-08-30 21:34:32 +03:00