Пятый вариант «Отображение»: маркер показывает выбранное значение и при этом
никогда не меняет цвет — ни по состоянию, ни по тревоге, ни по недоступности,
ни под цвет RGB-света, ни по активности.
Четыре прежних режима задавали одним выбором две независимые вещи: что
нарисовано внутри маркера и красится ли он состоянием. Поэтому вместо
сравнения с одним токеном появились два производных предиката рядом с
`normalizeDeviceDisplay` — `displayWantsValue` и `displayIsNeutral`, — и их
спрашивают политика, слой пульсации, внешний бейдж, редактор и три ветки
живого слоя пылесоса.
Две ловушки, из-за которых режим не сводится к одной строке в словаре:
- быстрый путь «статичному маркеру источники не нужны» (`sourceDetails: false`)
— это основной путь рендера плана, карточки пространства и PDF. Он оставлен
только режиму без значения: иначе число пропало бы именно на плане и
осталось в предпросмотре редактора;
- три ветки живого пылесоса сравнивают режим строкой и не читают политику,
поэтому зелёная политика их не гарантирует. Свидетель держит две
конфигурации карты: сопоставленная доказывает puck и след, несопоставленная
— бейдж маршрута (при совпадающей калибровке маршрут `ready`, и бейджа не
было бы ни в одном режиме). Бейдж проверяется при наполненном буфере
позиций, иначе его отсутствие объяснялось бы первой веткой.
Потолок initial View перецентрирован 291_700 → 292_400 без изменения общего
бюджета: измеренный факт 291 346 Б оставлял под прежним центром 354 Б —
внутри шумовой полосы метрики.
Эталон: в `device-icon-state-table-{light,dark}` включённая RGB-лампа
переведена в новый режим. Кадры обязаны разойтись; приёмка — отдельным
коммитом класса D с полного линуксового артефакта Validate.
Issue: #588
User-Visible: yes
Аудит 14.09, §4 и §9. `golden-report.json` не нёс платформу вовсе, поэтому
приёмщик вызывал `captureEnvironment()` у себя и записывал СВОЮ платформу как
платформу кадров. На `ad4000f9` это дало `"platform": "win32"` у кадров,
снятых Linux-прогоном 34853080375, а причина осознанного обхода осталась в
stdout и в индекс не попала — хотя `AGENTS.md` обещает след в обоих местах.
Сами PNG были и остаются целы: 101 свидетель совпал байт-в-байт. Врал
провенанс.
Что сделано:
- отчёт съёмки получил раздел `capture`: платформа, архитектура, сборка
Chromium, отпечаток материала и — в CI — прогон с попыткой и SHA. Схема
отчёта поднята до 2;
- приёмка читает среду съёмки из отчёта. Гейт чужой среды теперь судит обе
стороны: съёмку (из отчёта) и приёмку (свою). Отказ — до единой записи;
- индекс эталонов поднят до схемы 2 и различает `capturedOn` и `acceptedOn`,
несёт раздел `capture` и причину осознанного обхода в `foreignCapture`;
- отчёт схемы 1 платформы съёмки не несёт физически: это отдельная явная
ветка, `capturedOn` уезжает `null`. Выдумывать платформу нельзя — ровно этим
задача и вызвана;
- `--baselines=<dir>` у приёмки: без него проверить «отказ произошёл ДО
записи» можно было бы только порчей рабочего дерева, то есть никак (#556).
Осознанно отменено решение #455 «не трогать run.mjs»: ради ГЕЙТА цена
фингерпринта не окупалась, ради ПРОВЕНАНСА окупилась — платформу кадров знает
только тот, кто их снял. Плата разовая: пересобран бандл, индекс скриншотов
документации переснят отдельным коммитом. Тест #455 переписан под новый
инвариант, а не удалён.
Свидетели: `test/golden-capture-provenance.test.mjs` — семь проверок, все пять
сценариев приёмки из issue, включая «отказ до записи» и «подмена PNG и неполный
артефакт по-прежнему fail-closed». Мутанты `golden-index-invents-capture-platform`
(возвращает платформу приёмщика) и `golden-report-provenance-optional`
(разрешает отчёт схемы 2 без провенанса) прогнаны лично: оба краснеют.
npm test 2762/2761/0 fail, typecheck чистый.
Issue: #571
User-Visible: no
`loadGithubProofContext` спрашивал объявленный `Baseline-Reviewed` run для
любого потребителя, а `evaluateCiProof` судил его при `reviewedRun: null` —
merge и review начинали зависеть от доступности старого run по чужой причине.
Теперь запрос делается только с `withReviewedRun` (release-gate передаёт его
вместе с ожиданиями), а проверка стоит внутри `if (expected)` — рядом со
сверкой evidence, где ей и место. Тест: без ожиданий merge/review green при
reviewedRun undefined/null/пустом; фейковый fetch доказывает, что запроса нет.
Issue: #573
User-Visible: no
Приёмка эталонов на beta.3 (`ad4000f9`) стоила второго полного Validate —
22 минуты, из них 17–22 на шард мутантов. Причина одна: корпус отпечатка
(`source-fingerprint.mjs`) называет `demo/golden` строкой-каталогом, а
замыкание входов раскрывало каталог во все текстовые файлы под ним, включая
`baselines-index.json`. Индекс становился входом smoke, performance_smoke и
каждого гарда через `serve.mjs`: на реальной паре C→B ключи smoke/perf были
DIFFERENT, отпечатки 181 из 183 браузерных свидетелей менялись, журнал их не
пропускал.
- `check-inputs.mjs`: `BASELINE_OVERLAY` — раскрытие каталога не выдаёт
overlay; явный корень golden и явная ссылка на файл — как были. На паре
C→B: ключи smoke/perf/parity/backend same, golden DIFFERENT; отпечатки
743 из 744 равны; план мутантов B с журналом C — 0–1 на шард вместо 38–44
- `ci-proof.mjs`: составное evidence — product tree без overlay, overlay
(tree, sha256 индекса, run из `Baseline-Reviewed`), content-ключи всех
реюзных job (исполненных тоже); `evaluateCiProof({expected, reviewedRun})`
сверяет с локальным расчётом, fail-closed на ключ, tree, индекс, reviewed
run, маркер с чужим ключом; proof без evidence при ожиданиях — stale
- `release-gate.mjs` / `release-prerelease.mjs`: ожидания считаются на
checkout кандидата (`candidateExpectations`), чужой checkout — notice
- мутанты: `baseline-overlay-leaks-into-every-key`,
`proof-trusts-evidence-it-could-verify`,
`reused-marker-key-unchecked-against-candidate`,
`product-tree-identity-counts-baselines`; перенацелен
`ci-proof-ignores-run-attempt`
- docs: TESTING (правило overlay), DEVELOPMENT (evidence в release proof),
STATUS
Issue: #573
User-Visible: no
Указанные ширина и глубина теперь совпадают с видимым габаритом предмета.
Раньше рисунок занимал внутри своего бокса около 88 % стороны, поэтому
«Кухонный напольный модуль» 60 × 60 выглядел меньше «Посудомоечной машины»
60 × 60 примерно на 14 % по стороне и на 30 % по площади: цифры совпадали,
картинка врала.
Пак `fix-584-1` от дизайнера (issue, комментарий от 16.09) проверен исполнением
своей метрикой, а не его отчётом: границы считаются аналитически по той же
математике, что у продакшен-экспорта в PDF. Все 44 плановых SVG заполняют
`viewBox` с худшим отклонением 0.000000 при допуске ТЗ 0,1; `pack.json` и 33
иконки меню побайтно прежние. Для сравнения, прежний набор в `dev` давал 65
нарушений того же контракта.
Что сделано в коде:
- `scripts/svg-path-bounds.mjs` — границы рисунка по настоящим экстремумам
кривых (дуги приводятся к кубикам). Контрольные точки в габарит не входят:
иначе «заполняет бокс» проверялось бы по выпуклой оболочке и поля проходили
бы мимо. Генератор — `.mjs` и обязан работать без сборки TypeScript, поэтому
разбор здесь свой; тест сверяет его с `parseSvgPath` продакшена на всей
библиотеке, чтобы реализации не разъехались молча.
- `scripts/furniture-path-join.mjs` — склейка нескольких `<path>` с
канонизацией только начального `moveto`. Приём подсказан патчем в поставке
дизайнера; реализация и свидетели свои. Прежний `paths.join(' ')` продолжал
координаты предыдущего пути: замер показал, что у иконок меню `stairs` и `tv`
части рисунка уезжали за `viewBox` 110 × 110 — до 119. Теперь склейка
побайтно воспроизводит «каждый путь из своего нуля».
- генератор проверяет плановый символ по контракту: один путь, подконтуры с
абсолютной `M`, рисунок заполняет `viewBox` (допуск 0,1).
- `src/furniture.ts` — legacy `plant` растянут от центра в 1/0.96. Это
единственный из двенадцати retained-примитивов с полями (0.02…0.98 unit box);
форма прежняя, габарит стал заявленным.
Свидетели: `test/furniture-visual-bounds.test.mjs` (AC1–AC3, AC5 и сверка двух
реализаций границ) и `test/furniture-path-join.test.mjs` (AC4, включая реальные
`stairs`/`tv`). У AC1 есть положительный контроль: та же функция обязана видеть
поля на геометрии из шапки issue — утверждение «полей нет» стоит ровно столько,
сколько стоит измерение. Мутанты `furniture-symbol-may-keep-inner-padding` и
`furniture-paths-joined-without-reset` возвращают прежнее поведение и краснеют;
прогнаны лично через `mutation-gate --id=`.
Миграции пользовательских данных нет: координаты, размеры, поворот и
зеркалирование сохраняются.
npm test 2743/2742/0 fail, typecheck чистый, furniture:check зелёный.
Issue: #584
User-Visible: yes
Проверка #479 обязана быть строгой на кандидате беты и на релизном гейте.
Фактически она не была строгой ни разу: preflight сравнивал со строкой
`heavy=true` ВЕСЬ вывод `classify-changes.mjs --heavy`, а вывод двухстрочный —
`heavy=…` и `mutants_requested=…`. В `$(…)` строки схлопываются через пробел,
сравнение не совпадало никогда, режим оставался `warn`.
Видно построчно в логе прогона 35091507839 на кандидате `4c44ef60`:
скриншоты документации: режим warn (heavy=true
WARN screenshot source fingerprint is stale; ...
ok документация
То есть проверка увидела устаревший индекс и пропустила кандидата. Обе беты
после `699ab471` уехали с ним; на чистом checkout того же SHA
`node scripts/check-docs.mjs --strict` падает с ERROR.
Правило, которое из этого следует: формат `$GITHUB_OUTPUT` — построчный
`ключ=значение`, читать его надо по ключу либо не читать вовсе. Где нужен один
ответ, CLI отдаёт один ответ: `--screenshots-mode` печатает `warn` или
`strict`, и в shell не остаётся ни разбора, ни развилки.
Свидетели в `test/classify-changes.test.mjs`: режим по каждому событию, форма
вызова в workflow (сравнение со строкой `heavy=true` не должно вернуться) и
прямая проверка того, что вывод `--heavy` многострочный — то есть целиком
сравнивать его нельзя. Мутант `screenshot-freshness-never-strict` возвращает
прежнее «никогда не strict» и обязан краснеть.
Issue: #586
User-Visible: no
Относительная половина «Полных бенчмарков» сравнивала кандидата с прошлой
вершиной `main`. Для стабильного релиза это давало круг, в котором гейт не
может покраснеть дважды: прогон идёт только на push в `main`, кандидат обязан
там оказаться, и следующий коммит той же линейки берёт базой первый — то есть
линейку саму. На выпуске v1.76.0 это видно построчно: прогон 35097102695 на
`c3d64789` честно показал resizePreview 603 → 981 и panZoom 91 → 205 в скрытой
изометрии, а прогон на `9683a590` был зелёным и был бы зелёным без всякой
правки бюджетов.
Теперь база выбирается по намерению коммита: head несёт трейлер `Release:` без
пре-релизного суффикса — сравниваем с предыдущим стабильным тегом. Бета,
обычный push и ручной `comparison_ref` не меняются.
Решение вынесено из shell в `scripts/performance-baseline.mjs` по тому же
доводу, что и разбор вердикта ревью (#556): отрицательные случаи — тега нет,
тег стоит на самой голове, база перестала быть предком, база старше
HP-PERF-01 — в YAML не прогнать ни одним тестом. Обращения к git инжектируются,
фикстуры описывают дерево. Отказы по-прежнему уводят в сторону БОЛЬШЕГО
сравнения: непригодная база → родитель → последний достижимый релизный тег.
Проверено исполнением на этом репозитории: стабильный кандидат v1.76.0 →
`2c6410bb` (v1.75.0); бета v1.76.0-beta.5 и обычный push → `push before`;
dispatch с `comparison_ref=v1.74.0` → `e63460f0`.
Свидетели: `test/performance-baseline.test.mjs` (10 проверок, включая AC2 —
второй коммит линейки не сравнивается сам с собой) и мутант
`stable-candidate-compares-against-itself`, который возвращает прежнее
поведение и обязан краснеть; проверено подменой руками — AC2 падает, оригинал
проходит.
AC4: других релизных гейтов, судящих о родителя, нет. `validate.yml` берёт
`github.event.before` только для ДИАПАЗОНА файлов, и там база уже заменена
доказанно зелёным предком (#387/#388), а не сырым родителем.
npm test 2731/2730/0 fail, typecheck чистый, check-docs зелёный (кроме
известного отпечатка скриншотов, #586).
Issue: #587
User-Visible: no
Полные бенчмарки кандидата v1.76.0 (прогон 35097102695, SHA c3d64789) против
v1.75.0 покраснели на двух скрытых изометрических профилях:
| метрика | v1.75.0 | v1.76.0 | предел |
|---|---|---|---|
| large-house-isometric resizePreview | 603 | 981.4 | 753 |
| large-house-isometric panZoom | 90.8 | 205.2 | 150.8 |
| stage3-dense stateUpdate | 77.5 | 169.1 | 152.5 |
| stage3-dense resizePreview | 582.5 | 765.6 | 732.5 |
| stage3-dense panZoom | 90.6 | 171.7 | 150.6 |
Шаг настоящий и объяснённый: #583 добавил скрытому 2.5D-виду геометрии, а
поиск свободного места для подписей даже после ускорения решёткой стоит вдвое
дороже, чем до #583. Абсолютные потолки не тронуты и держатся с запасом
(panZoom 205 при 600, resize 981 при 2200); все семь пользовательских профилей
зелёные — регрессия целиком внутри вида за `hp_alpha`.
Решение владельца 2026-09-16: принять. Рычаг — допуск в миллисекундах, а не
коэффициент: он покрывает разовый сдвиг уровня и продолжает ловить рост от
нового уровня, тогда как поднятый коэффициент разрешил бы удвоение навсегда.
Значения одинаковы у обоих профилей — контракт #160 требует, чтобы плотный
двойник Stage 3 делил с историческим профилем каждый общий потолок, и тест
`performance-workflow` это стережёт.
Допуски временные. #585 переписывает поиск на перебор границ препятствий
вместо скана диска 48 px; когда он приедет, значения возвращаются к 150/60/75.
Тест `performance-budget` фиксирует и числа, и то, что наблюдённый шаг проходит,
а следующий такой же — уже нет.
Честно о границе метода: сравнение идёт с ПРЕДЫДУЩЕЙ вершиной main, а она уже
несёт эту же линейку, поэтому следующий прогон был бы зелёным и без правки
бюджетов. Правка сделана не ради зелёного прогона, а чтобы принятый уровень был
записан явно и проверялся тестом. Отдельно завожу, что базой стабильного
кандидата должен быть предыдущий стабильный тег, а не вершина main.
npm test 2721/2720/0 fail.
Issue: #585
User-Visible: no
Release: v1.76.0
После первого движения камеры переносит фильтр контура на ограниченный размером viewport слой, сохраняя исходный статичный рендер без изменений.
Issue: #582
User-Visible: no
Режим заливки комнаты и её цвет — два поля конфига под одним переключателем.
«Как у пространства» снимало только режим; цвет оставался и продолжал
применяться, потому что `roomCustomFillOf` отдавал цвет комнаты независимо от
того, чей режим `custom` действует. Так возникало безымянное состояние «режим
наследую, цвет свой» — Cabinet на даче.
Теперь цвет комнаты участвует в раскраске только вместе с её собственным
`fill_mode: 'custom'` (одна функция — все поверхности: карточка, space-card,
PDF, черновик диалога). Диалог загружает цвет в черновик только при своём
режиме, обнуляет его при уходе с «Свой цвет» и показывает строку цвета только
под этим радио; сохранение пишет `custom_fill` только с `fill_mode: 'custom'`,
иначе удаляет — включая сироту от прежнего редактора. Чтение конфиг не
переписывает: застрявшие комнаты выздоравливают обновлением.
- `test/logic.test.mjs`: AC1 — сирота и любой чужой режим → цвет пространства
- `demo/smoke_room_settings.mjs` шаг 7: свой цвет → «Как у пространства» →
ни режима, ни цвета, во View цвет пространства; сирота открывается как
наследование, сохранение её удаляет (проверено красным на базе: 9 фактов)
- `demo/smoke_space_settings.mjs`: override с собственным режимом + сирота
- `demo/golden/harness.mjs`: `roomCustomFill` ставит комнате её режим —
кадры `lighting-custom-glow-*` не меняются
- мутант `room-orphan-colour-wins-again`
- docs: ARCHITECTURE (#56), CONFIG-COMPATIBILITY, USER-GUIDE ru/en, TESTING;
отпечаток скриншотов принят попиксельно (11 кадров)
Issue: #581
User-Visible: yes
Свидетель, который не готовится к прогону, краснил гейт той задачи, чей дифф его
выбрал, даже когда причина лежала в чужом коммите: на #566 это стоило двух
кругов. Теперь при исходе `setup-failure` в дифф-режиме прогоняется ОПРЕДЕЛЕНИЕ
БАЗЫ на дереве базы, и раннер говорит прямо — отказ предсуществующий или внесён
этим диффом. Сравнение подобного с подобным здесь принципиально: с определением
из головы собственная сломанная правка реестра выглядела бы предсуществующей.
Предсуществующий гейт задачи не красит (решение владельца 14.09) и не теряется:
он назван машиночитаемой строкой и обязан покраснеть в ночном полном прогоне, у
которого есть адресат (#472). Мутанта, которого в базе нет, оправдывать нечем по
построению.
Композиция чтения реестра базы и запуска мутанта живёт отдельным модулем:
границу «запуск не зависит от отбора» держит тест #558, и CLI обязан остаться
тонким.
Issue: #568
User-Visible: no
Ревью r1, Medium: наблюдение о записи на удалённом пространстве говорило
«владелец жив» и про ключ, который ни во что не резолвится. Это заявление о
доказанности там, где её нет: продукт в этом состоянии ничего не удаляет не
потому, что владелец жив, а потому, что не знает — `space-reference-repair`
ведёт `live`, `absent` и `unverified`, три состояния, а не два.
Проверка ведёт те же три. Нерезолвящийся ключ получает наблюдение
`unknown_owner` с той же формулировкой, что и в живом пространстве: «владелец не
найден в конфигурации (возможно устройство HA)». Тест сверяет теперь и ТЕКСТ
причины — вид наблюдения без текста эту асимметрию пропускал, ровно так она и
проехала.
Issue: #566
User-Visible: no
Инварианты объявляли нарушением ЛЮБУЮ запись layout, чьё пространство удалено.
Продукт так не считает: `space-reference-repair` удаляет такую запись только
когда может доказать, что владелец тоже исчез, и сознательно хранит её, когда
владелец жив или доказательств нет — удаление уносит расстановку пользователя.
Конфиг сразу после Optimize законно содержит такие записи, и проверка называла
нарушением штатное состояние.
Теперь правило то же, что у продукта: нарушение — только когда владельца нет по
самой конфигурации (комната, область или снятый маркер). Остальное —
наблюдение, как у ветки `unknown_owner` рядом.
Issue: #566
User-Visible: no
Гард `keepTwoPoint` решал, сохранять ли двухточечную калибровку, сравнением
`JSON.stringify`, то есть текста. Порядок ключей `sources` при этом меняет сам
билдер: `slots`/`ranges`/`zones`/`occupancy_entity`/`count_entity` он удаляет и
дописывает заново, а `availability_entity` остаётся на месте и уезжает в начало.
Конфиг, только что записанный этим же билдером, при следующем открытии
сравнивался неравным — и радар с `availability_entity` терял `refs` и `rms_cm`,
получая `method: manual`, при первом же обычном сохранении.
Сравнение стало каноничным по порядку ключей объектов и осталось чувствительным
к порядку элементов массивов: позиция слота — это его `target_N`. Проекция не
затронута — она считается от `mount`, `cell_cm` и `mirror`.
Issue: #567
User-Visible: no
Полевая проверка основного View по #560: шесть обезличенных планов корпуса и
цепочка import → Optimize → Optimize → Resize → сохранение с численными
оракулами, семь household-путей с независимыми оракулами в двух ширинах
карточки, аудит доступности с измеренными числами. Продуктовых правок нет:
находки заведены отдельно (#564, #565, #566).
Issue: #560
User-Visible: no
Объявленные `permissions:` у `model_review` не были потолком: без переданного
`github_token` claude-code-action меняет OIDC на собственный App-токен, дефолт
которого — contents/issues/pull_requests: write, и `ghs_…` от claude[bot]
оказывался прямо в окружении Bash-инструмента модели. Ревью r1 показало это
живым доказательством в собственной же сессии.
Теперь шагу Review передан ambient `secrets.GITHUB_TOKEN`: обмена не происходит,
`id-token` не нужен, список прав становится настоящим. У модели остаётся ровно
одно право записи — `issues: write` под комментарий вердикта (§7.2) и issue по
§12; записи в репозиторий у неё больше нет.
Issue: #556
User-Visible: no
Три вещи, которые аудит 12.09 назвал в §10.
**Перемещаемые ссылки.** `home-assistant/actions/hassfest@master` и
`hacs/action@main` — это произвольный будущий коммит чужой ветки, а ревьюера с
Read/Write/Bash запускал перемещаемый major `anthropics/claude-code-action@v1`.
Все 116 `uses:` в девяти воркфлоу закреплены полным SHA с комментарием-версией;
`scripts/action-pins.mjs` это проверяет, а предполётный вердикт Validate —
исполняет. Локальная переиспользуемая workflow пина не требует и исключена
явно.
**Права.** Один блок `permissions` на весь конвейер выдавал `issues: write` и
OIDC каждой стадии, включая единственную недоверенную — работу модели. Теперь
права выдаются по job: модели только чтение и OIDC для самой
`claude-code-action`, писать в issue умеют детерминированные стадии.
**Граница.** Разбор запечатанного результата переехал из inline-shell в
`scripts/review-result-gate.mjs` — не ради красоты, а потому что в YAML его
нельзя прогнать ни одним отрицательным случаем. Проверяются те же вещи, что и
раньше, и в том же объёме: точный набор файлов, контрольные суммы, схема
паспорта и совпадение КАЖДОГО из семнадцати полей с тем, что посчитала
детерминированная стадия. Сверху — пятнадцать враждебных фикстур: неполный
набор, лишний файл, подменённое содержимое, чужой run и попытка, устаревший
material_sha и tree, чужие задача, этап, раунд и ветка, вердикт вне словаря,
пустой документ, manifest не о тех файлах, неразбираемый JSON.
Настоящих секретов и привилегированных операций фикстуры не трогают.
Issue: #556
User-Visible: no
Run 34760156615 exposed stale registry witnesses under the honest #550 outcome taxonomy. Keep behavioural checks out of setup chains, retarget the preflight mutation to the editor host, and make the junction cache smoke exercise same-object in-place geometry changes.
Release: v1.76.0-beta.1
Issue: #550
User-Visible: no