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