Commit Graph
295 Commits
Author SHA1 Message Date
Codex fec99b907b feat: record vacuum trails under the route that produced them
User-Visible: no
Issue: #162
2026-09-03 19:18:13 +03:00
Codex c38501ef50 feat: route live vacuum overlays to the active map space
User-Visible: yes
Issue: #162
2026-09-03 19:18:13 +03:00
Codex 9126e5b430 feat: pure map-to-space routing contracts for multi-floor vacuums
User-Visible: no
Issue: #162
2026-09-03 19:18:13 +03:00
Sergey Matyunin 5409f0b2e3 refactor: isolate room pointer preflight
Issue: #440
User-Visible: no
2026-09-03 16:59:31 +03:00
Sergey Matyunin 53847e3997 fix: harden beta 2 audit paths
Issue: #440
User-Visible: yes
2026-09-03 16:50:54 +03:00
Claude 5f90eaf4ef fix(gates): потолок initial-графа с полосой вместо разового храповика
#429 снял `SUPPORT_LAZY_INITIAL_BASELINE_BYTES = 291046` — порог,
привязанный к критерию приёмки #423, с пятнадцатью байтами запаса и
сообщением про копирайт формы поддержки. Снять было правильно. Но снят
он оказался ровно на том релизе, где сработал бы:

  beta.1 291 031 · снятый порог 291 046 · beta.2 291 069

Рост случился внутри той же беты, уже после снятия, и заметить его стало
нечем: единственным сигналом остался `lowHeadroomWarning`, который горит
постоянно — третий аудит подряд, и третий раз без реакции.

Механизм по образцу ядер (#425): потолок 292 000 Б с полосой 2 000 Б,
двусторонний. Рост выше потолка — отказ с числом и указанием, что делать
(поднять потолок в том же коммите с объяснением либо вынести код в
ленивый граф, #367). Падение ниже полосы — тоже отказ: незафиксированный
выигрыш отыгрывается молча, именно так запас бюджета ушёл с 26 КБ до
8.3 КБ за сутки.

Полоса, а не точное число, — по измерению, а не из осторожности. На
beta.2 initial-чанк стал МЕНЬШЕ на 344 сырых байта и при этом на 40 байт
больше в сжатом виде: gzip не монотонен по исходнику. Точный храповик по
gzip краснел бы на коммитах, которые код сокращают, — та же лотерея, о
которой предупреждает комментарий к бюджету 300 000. Потолок поставлен
так, чтобы факт лежал ближе к середине полосы: 931 Б до отказа сверху,
1 069 Б снизу.

Предупреждение о запасе стало погашаемым: `LOW_HEADROOM_ACKNOWLEDGED_CEILING`
привязан к ЗНАЧЕНИЮ потолка, поэтому признание долга перестаёт покрывать
ровно тогда, когда потолок поднимут. Сейчас `null` — не погашено, и текст
говорит, чем гасится. Превышение самого бюджета признанием не гасится:
там отказ, а не тревога.

Свидетели. Девять мутаций, каждая краснит свой тест: снятие верхней
стороны, снятие нижней, потолок выше бюджета, полоса 50 Б, признание
поверх превышения бюджета, молчащее устаревшее признание, и две — про
подключение: вызов потолка убран из main и отказ понижен до console.log.
Последние две прошли молча на первой редакции тестов — статическая
проверка «в main есть вызов» была бы циклическим доказательством, за
которое #430 снял циклический тест гарда benchmark, поэтому добавлен
прогон настоящего CLI в подставном дереве.

Захардкоженный `lowHeadroomWarning(9058)` из #429 заменён на число из
поставляемого манифеста: константа в тесте выглядела измерением, не
будучи им.

Мутант `initial-view-ceiling-unplugged` в реестре.

Гейты: npm test 1819 tests, 1818 pass, 0 fail; node scripts/bundle-budget.mjs
зелёный — 291 069 внутри полосы, запас до бюджета 8 931 Б.

Issue: #438
User-Visible: no
2026-09-03 16:18:14 +03:00
Codex d4dd027b0a build: prepare v1.71.0-beta.2 candidate
Issue: #426
Issue: #427
Issue: #428
Issue: #431
Issue: #432
Issue: #434
User-Visible: no
2026-09-03 15:23:40 +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
Codex fa1aa7a877 test: record the real backend coverage baseline (#42)
87.2% line coverage, taken from the first fully green backend CI job of
this branch (run 33321192996, coverage.xml line-rate 0.8716) — replaces
the 80.0 placeholder as promised before the verdict. The gate refuses
any run below baseline minus 0.1.

User-Visible: no
Issue: #42
2026-08-30 21:34:32 +03:00
Codex 323b7803e0 feat: measurable backend engineering quality — stage 1 (#42)
Tooling: requirements_test.txt becomes the single source of backend CI
dependencies; pyproject.toml configures ruff (E/F/B/I, E501 excluded by
decision) and mypy strict for a grow-only allowlist of six pure modules
(junction_limits annotated to pass). The 42 substantive ruff findings
are fixed — the B023 loop-variable closures bind their variables as
parameter defaults instead of hiding behind noqa, and every remaining
noqa carries a reason (guarded by a test).

Errors: const.ERROR_CODES / ERROR_CODE_FAMILIES formalise the stable
contract; the scanner test proves every emitted code across BOTH paths
(send_error literals; class attrs, literal and variable-passed
MarkerControlError codes, f-string families) is registered and has a
localized message — 22 missing backup.error.* keys added in all four
languages. invalid_passage_fields / invalid_partition_opening_jamb_margin
ship structured JSON details (legacy format read-compat for one beta),
and _errText renders code-first: unknown codes localize, raw English
messages go to the console.

CI: the backend job lints with ruff, refuses a silently skipped HA
harness (import + collect threshold), measures branch coverage over
pure+harness, fails below the committed baseline and uploads
coverage.xml. quality_scale: docs-troubleshooting/examples honestly
done, test-coverage/strict-typing carry staged progress.

User-Visible: yes
Issue: #42
2026-08-30 21:34:31 +03:00
Codex 066cf44c2f fix: count spec and code review documents apart (#395)
Проверка (CI) / Классификация изменённых файлов (push) Successful in 20s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 52s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 55s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 20s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 19s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 9m52s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 1h5m50s
The p.7 limit bucketed every review document of an issue together, so a
task that honestly passed both stages was refused for having passed
them: #42 has 4 SPEC-REVIEW plus 3 CODE-REVIEW documents — 4 and 3
rounds per stage, both inside the budget — and its already-published
GREEN r5 verdict could not publish its own artefact for three runs in a
row, blocking the merge each time.

The counter is now keyed by stage and issue, and the refusal names the
stage. The threshold itself is unchanged: seven documents of one kind
still fail, and the comment above the constant already said what the
number means — the round budget of ONE stage.

User-Visible: no
Issue: #395
2026-08-30 21:34:19 +03:00
Matyshandclaude[bot] a577fde799 refactor: type editor i18n keys (#391)
Remove legacy any casts from device inbox, marker, and geometry preflight translation calls. Refresh the moved preflight mutation anchor.

Issue: #391
User-Visible: no
2026-08-30 17:13:01 +00:00