Commit Graph
80 Commits
Author SHA1 Message Date
Claude d17bcc05fb feat(devices): пакетное «Скрыть/Показать выбранные» в каталоге устройств (#618)
На вкладках «На плане» и «Скрытые» — флажки строк, «Выбрать все (N)» по
всему отфильтрованному набору (включая строки за «Показать ещё»), панель
«Выбрано: K · Скрыть/Показать выбранные (K) · Снять выбор» в отдельном
контейнере .device-inbox-batch вне .device-inbox-filters.

- src/device-inbox.ts: чистые inboxVisibilityAllowed (один источник для
  canHide/canShow и выбора), selectableInboxRows, effectiveInboxSelection,
  applyInboxVisibility — свёртка одиночных действий; одиночное = пакет из
  одной строки.
- src/device-inbox-batch.ts (новый, lazy editor): выбор, панель, строка-
  флажок и writeInboxVisibility — одна запись houseplan/config/set с
  expected_rev на пакет, busy='__batch__' (inert), откат маркеров при отказе,
  выбор сохраняется и сужается. Одиночный _setInboxHidden рантайма — тонкая
  обёртка над той же записью; рантайм стал короче базы на 14 строк.
- сброс выбора при смене вкладки (клик и стрелка), поиска и «Только новые».
- i18n en/ru/de/fr: 9 ключей по ТЗ §8.
- смок demo/smoke_device_inbox_batch.mjs (AC1–AC8, AC10), юниты
  test/device-inbox.test.mjs (AC2, AC4), 4 мутанта в реестре.
- docs: USER-GUIDE ru/en, FILTERING.md, оба changelog.
- monolith-baseline: hostRefs 4948→4959, bundleBytes 2500387→2505569 —
  новая фича (состояние выбора и запись идут через порт хоста).
- bundle-budget: LAZY_EDITOR_GZIP_CEILING 245 400 → 246 600 (замер 245 610,
  +1 190 Б gzip от базы 244 420); initial View 289 449 → 289 765 (+316 Б,
  строки en-словаря), в пределах потолка.

Golden device-inbox-* изменятся ожидаемо — пересъёмка предрелизным гейтом
на Linux CI (§11.4).

Issue: #618
User-Visible: yes
2026-09-24 22:50:30 +03:00
Claude 857369b18b fix(plans): загрузка плана по HTTP, предел 8 МБ проверяется до отправки (#617)
План уходил base64 в WebSocket-кадре: файл больше ~3 МиБ давал кадр больше
4 МиБ, HA закрывал сокет до обработчика, и обещанные 8 МБ были недостижимы.

- бэкенд: HouseplanPlanUploadView (POST /api/houseplan/plans/upload), потоковый
  предел MAX_PLAN_BYTES (read_bounded), общий writer store_plan_upload для view
  и ws_plan_set (контракт WS без изменений);
- карточка: stagePlanFile/uploadPlanFile/renderPlanBackdropGuard в
  backdrop-pick.ts для обоих рантаймов; PlanFilePayload хранит Blob вместо b64;
  SVG больше предела — тост при выборе, растр — диалог #39 только с уменьшенной
  копией, копия больше предела не попадает в staging, 413 называет предел;
- i18n toast.plan_too_large, backdrop.over_limit_body (en/ru/de/fr);
  USER-GUIDE ru/en, CHANGELOG ru/en, docs/testing-notes (#617);
- тесты: test/plan-upload-limit.test.mjs, tests_backend/test_plan_upload.py,
  test_ha_upload.py (#617), smoke_plan_upload_limit.mjs; три смока переведены
  с b64/plan/set на blob/fetchWithAuth; мутанты plan-upload-*;
- база монолита: hostRefs +3 (общий хелпер плана вместо двух копий в рантаймах,
  новый тост уменьшенной копии).

Issue: #617
User-Visible: yes
2026-09-24 19:57:41 +03:00
Claude 82a48ee0ce perf(i18n): словари settings/support/topology — ru/de/fr ленивыми чанками; потолок онбординга (#627)
- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на
  пару «пространство × язык»), английский статически как слой отката;
  рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback,
  общий тост отказа), src/i18n/namespace-language.ts.
- Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер
  только после того, как словарь текущего языка осел; гейт хоста учитывает
  словари загруженной на нём поверхности (смена языка на лету — ветка warm).
  Оверлей Zigbee ждёт topology до первой отрисовки.
- bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со
  строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500
  (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки
  ленивости и владения по содержимому.
- Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык
  синхронно, ждут гейт вместо ослабления проверок; пять мутантов.
- monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro
  и export на каждый) и записи манифеста; gzip каждой поверхности при этом
  уменьшился.

Issue: #627
User-Visible: no
2026-09-24 14:51:18 +03:00
Claudeandclaude[bot] 66ae952c2a refactor(editor): диалог «Оптимизировать планы» — свой модуль с узким портом (#642)
Первый вынос из монолита по образцу live-*/RadarSetupController.
src/optimize-plans-dialog.ts: OptimizePlansDialogState, чистые
preflightDiagnostics/preflightVersionsDiffer и класс OptimizePlansDialog
(open/preview/toggleLivePositions/run/copyDiagnostics/
reportPreflightFailure/render, clipboardFallback) за портом из 18 членов.
Модуль не знает HouseplanEditorHostPort; импортирует его только рантайм,
карточка берёт тип.

Инлайн-фолбэк буфера обмена — WeakMap по объекту диалога
(CODE-REVIEW-295-r1 M2), дедуп dev-лога — поле класса: из карточки и порта
ушли _preflightClipboardFallback и _reportedPreflightFingerprint, пять
делегатов и две стрелки-заглушки. Харнесс (13 смоков, wall-draw-click,
golden) зовёт card._editorRuntime.optimizePlans.* — меняется только адрес.

11 текстовых утверждений i18n.test.mjs о разметке и тосте диалога и гварды
8 мутантов переехали в юнит test/optimize-plans-dialog.test.mjs
(test-build); новый мутант optimize-dialog-imports-host-port.

monolith-baseline: delegates 159→154, portMembers 350→348,
hostRefs 4948→4869, portPrivates 96→94, harnessPrivates 107→101,
bundleBytes 2499647→2499182. Потолки ядер опущены на выигрыш;
потолок lazy editor gzip пересчитан (+555 Б gzip при −465 Б сырых).

Issue: #642
User-Visible: no
2026-09-24 08:00:58 +00:00
Claude baf283c50f ci: thin default-branch callers invoke reusable bodies at @dev (#623)
Six workflows run from the default branch (issues, schedule, workflow_run):
process, process-resume, process-reconcile, mutation-gate, nightly,
process-metrics. Their bodies move to _<name>.yml (on: workflow_call); the
original files keep only triggers, run-name, permissions, concurrency and one
job `uses: Matysh/houseplan-card/.github/workflows/_<name>.yml@dev` with
`secrets: inherit`. A pipeline change becomes one commit to dev.

- caller job permissions = union of body job permissions (#556 minimum kept
  per job inside the body); caller `if` repeats the body guard for process and
  process-resume so unrelated events stay skipped;
- dispatch inputs forwarded via workflow_call inputs of the same names;
- _mutation-gate.yml keys evidence/marker on job.workflow_sha (the body SHA):
  in a called workflow github.workflow_sha belongs to the caller in main;
- action-pins: narrow exception for this repo's _*.yml at @dev with a reason;
- preflight workflow_sync compares all six thin callers (was 3 of 6);
  performance.yml excluded: its schedule judges main with main's own body;
- tests read bodies from _*.yml; new test/default-branch-workflows.test.mjs;
  six mutants; PROCESS.md §10.4, AGENTS.md, REVIEWER.md updated.

Issue: #623
User-Visible: no
2026-09-24 10:23:28 +03:00
Claude 92b83d9525 fix(process): rebase resolves a conflict only in docs/reviews/INDEX.md by rebuilding the index
A pipeline doc commit carries the review document and the rebuilt
INDEX.md; while the task waits, dev receives other tasks' documents with
their own INDEX.md, and the rebase of the branch conflicts in the index
every time. 24.09 this bounced green #617, #618, #629, #642 to S6.

scripts/rebase-generated.mjs: shared rebase helper. At every stop, if ALL
conflicting paths are docs/reviews/INDEX.md (or paths the caller
resolves itself), the index is rebuilt from the directory in the stop
tree, staged, and the rebase continues; any other path aborts and
returns the full list. CLI exit 3 = refusal with paths on stdout.

Wired into process.yml «Привести ветку к dev» (helper taken from dev via
git archive; conflict/conflicts outputs, lease, ref wait and
--commit-if-stale kept), merge-candidate rebaseOnto (claude[bot]
identity, --commit-if-stale kept) and rebase-on-dev.mjs (index next to
GENERATED_ROOTS; bundle still dev copy + rebuild).

Issue: #643
User-Visible: no
2026-09-24 09:35:58 +03:00
Claudeandclaude[bot] 44ee23ee33 ci(mutants): skip the nightly registry on a proven tree; install only the shard's environment
#620. Mutants were ~70 % of CI machine time.

1. mutation-gate.yml: a green full run (aggregator verified all six shards)
   leaves a cache marker keyed by material tree + workflow SHA. A scheduled
   night with the same tree and workflow, marker not older than 7 days, skips
   the shards and writes "reused from run N" to the run summary. Red runs
   leave no marker, so the next night runs again and still files the issue
   (#472). Manual dispatch always runs the full registry. Decision is a pure
   function in scripts/mutation-nightly-reuse.mjs.
2. changed_mutants: the shard plan (already computed before setup, #518) now
   names the environment of its guards (plan-browser=/plan-python=, from
   scripts/mutation-environment.mjs). Python + backend deps only for shards
   with pytest guards, Chromium only for shards whose guards reach
   Playwright; pip wheels cached. Every shard still runs and reports, so the
   six mutant jobs of the review proof are unchanged.

Item 3 of the issue (smoke guards -> node --test) is out of scope here.

Issue: #620
User-Visible: no
2026-09-24 05:48:42 +00:00
Claudeandclaude[bot] 040ad3dc5d test(editors): unit contracts for dialog baselines and form problems (#631)
dialog-baseline.ts and the four *-form-state.ts modules were proven only by
browser smokes. Unit tests on test-build now pin stableKey (top-level order,
transient filter, nested values as-is), no-baseline => dirty, warm transfer,
the transient set of each dialog, and every *Problems code with its order.

room-form-state.ts joins tsconfig.test.json. Three form-state mutants move
from smoke guards to these units; six new mutants cover the new contracts.

Issue: #631
User-Visible: no
2026-09-24 05:09:23 +00:00
Claudeandclaude[bot] 1fbf7704ca task-packet: trivial short track counts as product flow (#632 r1)
The trivial track (PROCESS §5.1) goes S2 -> S5 without a spec or a spec
review, so in S6/S7 its label is the only product-flow evidence. Without
it a trivial bug whose branch has no class A yet got the same false
"class A forbidden" as #607. productFlowEvidence now takes labels;
new test on the #612 body shape and mutant task-packet-trivial-is-product-flow.

Issue: #632
User-Visible: no
2026-09-24 02:52:09 +00:00
Claudeandclaude[bot] 10d32312b5 task-packet: track follows issue status and history, not a class-A-free diff
A product issue in S5-S7 whose branch holds only its spec review was
classified as infrastructure and told "class A is forbidden". The track is
now decided by product-flow evidence first (S1-S5 status, "## ТЗ" in the
body, docs/specs file, spec review doc or verdict); the "diff without
class A" heuristic applies only outside the product flow. docs/reviews/**
no longer counts as branch material. Three mutants guard both directions.

Issue: #632
User-Visible: no
2026-09-24 02:52:09 +00:00
Claudeandclaude[bot] 7dc7597260 docs(process): ролевые конспекты, замер входа, Snapshot генерируется, TESTING.md разделён
Вход агента до первого файла кода стоил ≈ 26 700 слов (аудит 22.09).

- docs/process/AUTHOR.md и REVIEWER.md — выжимки PROCESS.md: каждый пункт
  ссылается на раздел канона, ключевые формулировки дословные;
  test/process-digests.test.mjs сверяет якоря, ссылки и правила.
- scripts/entry-cost.mjs — маршрут чтения по роли и бюджет (автор ≤ 12 000
  слов, AC1); AGENTS.md «Read this first» называет те же маршруты.
- docs/STATUS.md: блок Snapshot генерирует scripts/status-snapshot.mjs
  (версии — release-contract, счётчики — inventory, теги — git); feature
  surface и ранние milestones перенесены дословно в docs/STATUS-FEATURES.md.
- docs/TESTING.md — действующая инструкция (684 строки, AC3); ручные
  чек-листы и приложения по issue перенесены дословно в docs/testing-notes/
  с индексом и тестом на полноту.
- Промпт ревьюера в process.yml читает конспект вместо пересказа правил;
  машинные требования (строка вердикта, REVIEW_DOC, запрет fetch, таблица
  «чем краснеет», разделы повторного раунда) сохранены и закреплены тестом.
- PROCESS.md: правила не менялись; добавлены ссылка на конспекты в шапке и
  уточнение в §10.4, что ревьюер конвейера читает конспект.
- 7 мутантов в реестре.

Issue: #634
User-Visible: no
2026-09-24 02:33:08 +00:00
Claude af62bd4c64 infra: local contour — pre-push gate:small by default, sandbox bootstrap, test:chunk
- .githooks/pre-push + scripts/pre-push-gate.mjs --hook: gate:small runs by
  default for issue/* branches with an executable diff; C/D-only diffs and
  non-issue refs are skipped, HP_PREPUSH_GATE=0 turns it off, =1 forces it.
  A non-HEAD push with an executable diff is rejected (the gate checks the
  working tree). The gate runs with every GIT_* variable removed: git gives
  hooks GIT_DIR (and GIT_CONFIG_PARAMETERS with the pusher's -c), and unit
  tests that `git init` temporary repositories otherwise write into the real
  one — seen on the first live run. The manual #343 mode is unchanged.
- scripts/sandbox-bootstrap.sh: idempotent worktree / deps / chromium (npm
  @sparticuz/chromium@152.0.0 + Playwright shims) / bundle / check steps,
  no owner paths or credentials.
- scripts/test-chunk.mjs, npm run test:chunk -- N/M: round-robin over
  test/*.test.mjs sorted by code point.
- Tests, seven mutants, docs/DEVELOPMENT.md «Локальный контур за 5 минут»,
  docs/TESTING.md. package.json changed → bundle rebuilt (fingerprint input).

Issue: #633
User-Visible: no
2026-09-24 05:00:45 +03:00
Claudeandclaude[bot] 1514899746 ci: имена job validate.yml — контракт, сверяемый с файлом; шарды из матрицы (#622)
- scripts/workflow-jobs.mjs: разбор job-уровня validate.yml (id, name,
  inline matrix) без зависимостей; незнакомые формы — громкая ошибка.
- ci-proof: JOB_RULES по id job с именем-контрактом; число и точные имена
  экземпляров — из матрицы YAML (константы count: 6 нет); resolveJobRules
  на каждом evaluateCiProof — переименование даёт failed с названной job;
  jobContractProblems — сверка в обе стороны, UNCONSUMED_JOBS для proof.
- validate-gate: MUTANT_JOB_PREFIX — из контракта ci-proof.
- e2e-gate: E2E_JOB_NAME — зеркало name: из houseplan-e2e e2e.yml
  (43899da5), распознавание выводится из шаблона; missing называет
  завершённые прогоны без job по контракту.
- тесты: фикстуры имён выводятся из validate.yml; новый
  test/workflow-jobs.test.mjs; 5 мутантов в реестре.

Issue: #622
User-Visible: no
2026-09-24 01:35:25 +00:00
Claudeandclaude[bot] 51e1643762 tests_backend: без HA pytest называет число несобранных test_ha_*.py
conftest.py без Home Assistant исключает test_ha_*.py через
collect_ignore_glob — это не skip, в итоговой строке их нет вовсе. Теперь
шапка и итог прогона печатают «HA harness NOT collected: N files (M tests)»
со ссылкой на канон (Linux CI / WSL scripts/wsl-setup.sh --verify). N и M
считаются при каждом запуске по тому же glob.

Тест test_conftest_harness_notice.py проверяет обе ветки conftest в
отдельном процессе pytest (homeassistant перекрыт заглушкой), счёт по
временному каталогу и по реальному харнессу против AST-счёта. Мутанты
ha-harness-notice-silent, ha-harness-notice-count-frozen. TESTING.md и
AGENTS.md: «silently skips» заменено точной формулировкой.

Issue: #630
User-Visible: no
2026-09-24 01:22:22 +00:00
Claude 47469bab22 Монолит: мёртвый код снят по noUnusedLocals, связность измеряется шестью числами и гейтом (#624)
Карточка и редакторский рантайм держали ≈380 неиспользуемых импортов, 56
мёртвых объявлений и дублей типов (warm-boot, LS_*, GLOW_*, debounce,
navigate, lruRead — копии карточки в рантайме) и 112 приватных членов
карточки, которых не читал никто — делегаты `_editorRuntimeOrThrow()._x()`,
оставшиеся от выноса #425, и аксессоры glow-состояния. Всё это снято; в 9
других файлах — по одиночной ошибке. Делегаты и поля, которых касаются
браузерные смоки (`card._x(...)`), оставлены и посчитаны отдельно.

Гейт `npm run lint:unused` (scripts/unused-locals-gate.mjs, в gate:small и
Validate после сборки): `tsc --noUnusedLocals` чист, кроме приватных членов
карточки из порта HouseplanEditorHostPort / `host.` (portPrivates) и членов,
которых зовёт харнесс (harnessPrivates); храповик по шести числам
scripts/monolith-metrics.mjs против scripts/monolith-baseline.json —
delegates 260→159, portMembers 350, hostRefs 4948, portPrivates 96,
harnessPrivates 107, bundleBytes 2 510 141→2 500 387. `npm run inventory`
печатает те же числа. Заморозка 54 тестов, читающих монолит как текст
(test/monolith-text-anchors.test.mjs); PROCESS.md §2.7 — правило.

Логический исходник для контрактных тестов (test/houseplan-source.mjs)
дописывает члены рантайма без делегата в карточке — контракт продукта не
зависит от наличия заглушки. Потолки ядер и initial gzip опущены на выигрыш
(292 000 → 290 400). Бандл пересобран, три копии синхронны.

Issue: #624
User-Visible: no
2026-09-23 21:09:44 +03:00
Claudeandclaude[bot] 49bae62e9a reviews-index: свежесть индекса после ребейзов конвейера, первый абзац находки целиком (#635 r3)
r2 H1: INDEX.md — снимок каталога, и ребейз ветки на dev, получивший чужие
документы ревью, устаревал его молча. Теперь `--commit-if-stale` пересобирает
и коммитит индекс коммитом конвейера после приведения к dev (process.yml) и
после ребейза кандидата (merge-candidate.mjs); тест «индекс свеж» сравнивает
закоммиченный файл с пересборкой и красит Validate при расхождении.

r2 M1: находка без заголовка — первый абзац секции, склеенный из перенесённых
строк, без маркера буллета и кода `**M1.**`; «не найдено», служебные скобки
«(унаследовано…)» — не находка. Нумерованные пункты тоже забирают перенесённые
строки. Мутант reviews-index-paragraph-tail. PROCESS.md §2.10 дополнен.

Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
Claudeandclaude[bot] d9bd5a446c reviews-index: счётчики из своего вердикта, находки в живых форматах заголовков, колонка файлов (#635 r2)
r1 H1: parseCounts брал первое «High: N» по тексту (часто цитата чужого
раунда), parseFindings знал только «### H1 — …». Теперь: счётчик — своя
строка «Вердикт» → секция «## Вердикт» → единственное значение → подсчёт по
заголовкам; находки — «### Medium (…) — …», секции с «**M1. …**»,
«## Находка N (High…)», «### [High] …», «### Low L1 — …»; «— нет» не
находка. Новая колонка «Файлы» — пути из находок, чтобы grep по имени файла
отвечал на «что находили по X». Пересказ чужого вердикта строчными в шапке
не перебивает свой. INDEX.md пересобран; мутант reviews-index-counts-first-match.

Перезаход без изменения дерева: dispatch-прогон на 3719a06d покраснел
рассинхроном process.yml main/dev (исправлено зеркалом 0c933449), а
validate-gate считает завершённый красный dispatch на SHA окончательным.

Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
Claudeandclaude[bot] a50cbd8f91 docs(reviews): индекс документов ревью, уроки, пересборка индекса конвейером (#635)
scripts/reviews-index.mjs собирает docs/reviews/INDEX.md: одна строка на
документ — issue, этап, раунд, вердикт (явная строка, раздел «Вердикт»,
свободная форма хвоста; 936 из 986 распознаны), High/Medium по строке вердикта
или заголовкам находок, до шести заголовков находок. Индекс детерминирован,
не индексирует сам себя, перечисляет файлы вне схемы имён; `--check` — гейт
свежести. process.yml публикует INDEX.md тем же коммитом, что документ ревью.

docs/LESSONS.md — датированные уроки со ссылками на источники (12 записей из
аудитов и разборов недели). PROCESS.md §2.10 — где искать решения.

Тесты: разбор имён, вердиктов, счётчиков, находок; фикстурный каталог;
живой каталог (100 % покрытие, >90 % вердиктов); контракт шага конвейера.
Мутанты reviews-index-skips-self-check, reviews-index-verdict-substring.

Issue: #635
User-Visible: no
2026-09-23 13:52:23 +00:00
Claude 1a6b12af86 fix(settings): validate drafts and preserve dialog baselines (#614)
Issue: #614
User-Visible: yes
2026-09-23 15:27:51 +03:00
Claude 53b20e8c45 test(backend): покрыть marker-id write boundaries (#625)
Issue: #625
User-Visible: no
2026-09-23 14:30:39 +03:00
Claude 15bc6bcf5a fix(backend): исправить CI-контракты upload (#625)
Issue: #625
User-Visible: no
2026-09-23 13:54:20 +03:00
Claude 4051899b9b test(backend): обновить якоря мутаций upload (#625)
Issue: #625
User-Visible: no
2026-09-23 13:33:58 +03:00
Claude 46edcb1f37 ci(metrics): еженедельный замер процесса одним скриптом (#637)
scripts/process-metrics.mjs — чистые функции над снимками GitHub: по issue
(таймлайн меток) вход по первой статусной метке, S4→S5, вход→S7, S7→S8,
повторные постановки S7; раунды ревью — по документам docs/reviews, не по
событиям метки (конвейер с #636 ставит S7 сам); прогоны Actions по workflow —
исходы, wall-time, события (прогоны конвейера сведены в одну строку); минуты
S4/S7 конвейера без skipped; при наличии jobs — job-минуты и доля «Мутанты».
Markdown-отчёт со сводкой и таблицей по issue; CLI на gh (только чтение).

process-metrics.yml — понедельник 05:00 UTC и по кнопке; отчёт в step summary
и артефакт на 90 дней; прав на запись нет.

Тесты на фикстурах, в т. ч. воспроизведение формы аудита 22.09 (30 issue:
15 r1/13 r2/2 r3 → 1,57; Validate 226/178/37/11). Мутанты
metrics-count-skipped-pipeline-runs, metrics-rounds-by-s7-events.

Issue: #637
User-Visible: no
2026-09-23 11:42:37 +03:00
Claude 2cfb6d2169 fix: protect View config recovery (#612)
Issue: #612
User-Visible: yes
2026-09-23 10:34:09 +03:00
Claudeandclaude[bot] 1dba818581 fix: clarify unsaved-settings confirmation (#610)
Issue: #610
User-Visible: yes
2026-09-23 06:36:39 +00:00
Claude 351fef43d6 ci(process): раунд ревью ждёт Validate событием, а не сном раннера (#636)
Стадия prepare спала ≈ 28 минут на раунд, пока шёл Validate с мутантами на
материале (модель работает 10–12); за неделю ≈ 420–500 job-минут простоя и
потолок бюджета стадии 55 минут.

- validate-gate.mjs: `--no-wait` — гейт диспатчит прогон, убеждается, что тот
  встал на материал (#539 сохранён), и возвращает `pending` (код 2) вместо
  ожидания; завершённый зелёный/красный отдаёт сразу, как прежде.
- process.yml prepare: третий исход `proceed=pending`: запечатанный маркер
  `review-pending-<issue>-<run>-<attempt>` (issue, stage, branch, material_sha,
  validate run) и выход; модель и интеграция не запускаются; возврат автору —
  только на явном `false`.
- process-resume.yml + scripts/process-resume.mjs: на `workflow_run: completed`
  Validate по ветке issue/* — если метка S7 стоит, активного прогона нет и
  последний прогон оставил маркер на этот SHA, переставить S7 (HP_PROCESS_TOKEN);
  новый прогон находит завершённый dispatch сразу. Без маркера не будит.
- process-reconcile.mjs: читает маркер и состояние Validate на материале;
  идёт — wait, завершился/пропал без продолжения — retry; без маркера — прежний
  escalate. Общий loadSealedArtifact, экспорт processRuns/artifactNames.
- preflight сверяет process-resume.yml между main и dev наравне с process.yml.
- Тесты: validate-gate (4), process-resume (8, включая контракт трёх workflow),
  process-reconcile (2); мутанты gate-no-wait-still-sleeps,
  resume-wakes-round-without-marker, resume-ignores-active-run,
  reconcile-wakes-pending-while-validate-active. PROCESS.md §10.4, AGENTS.md.

Issue: #636
User-Visible: no
2026-09-23 08:51:17 +03:00
Claude 8a4ecdea63 fix: commit slider-number input atomically (#608)
Issue: #608
User-Visible: yes
2026-09-23 07:37:30 +03:00
Claude e95936cdce fix(touch): refresh device hits and defer pinch zoom save (#613)
Issue: #613
User-Visible: yes
2026-09-23 05:35:41 +03:00
Claude 9f82960c2c fix(import): remap vacuum routes to copied space (#611)
Repair both incoming routes and safe target-reference recovery, with backend
and mutation coverage for foreign and same-instance imports.

Issue: #611
User-Visible: yes
2026-09-23 04:40:52 +03:00
Claude e8e5c863e4 test(dialog): cover HA theme and enlarged text (#609)
Extend the authentic pinned-HA diagnostic with explicit light and dark palettes, a 32 px root-font case, one-scroller geometry assertions, and reviewed evidence images. Correct stale AC references in the mutation descriptions.

Issue: #609
User-Visible: no
2026-09-23 03:55:27 +03:00
Claude 4b27268e2f test(dialog): strengthen mobile fullscreen witness (#609)
The mobile mutant now changes both public height bounds, so the browser test observes the intended 48 px regression instead of CSS min-height precedence masking it.

Issue: #609
User-Visible: no
2026-09-23 03:23:09 +03:00
Claude 4c9e013276 fix(dialog): align HA settings form shell (#609)
Use Home Assistant's public dialog tokens for the reviewed 560 px canvas, height cap, single scroller and mobile fullscreen layout. Keep generic dialogs and the native fallback unchanged, with authentic-HA evidence and mutation witnesses.

Issue: #609
User-Visible: yes
2026-09-23 03:18:37 +03:00
Claude fc70a3fcf4 test(dialog): register rejected-close mutants (#607)
Issue: #607
User-Visible: no
2026-09-23 02:25:57 +03:00
Claude 20457660fa fix(furniture): correct exercise and shelf artwork (#606)
Publish corrected 0.4.1 pack, preserve old cactus objects as a read-only alias, and cover the catalog, renderers, editor, PDF and visual scene.

Issue: #606
User-Visible: yes
2026-09-21 15:01:18 +03:00
Claude cc2300bf86 ci(mutation-gate): шесть шардов ночного прогона, прерванный шард — отказ, а не «ok» (#604)
Шард 2/4 прогона 35565222849 снят по timeout-minutes: реестр вырос до 810
мутантов (~203 на шард), длительность за 11 дней 42 → 61 мин при потолке 60.
Отчёт назвал его «ok»: лог без строк FAIL считался зелёным, а обрыв по
таймауту строк FAIL не содержит. Агрегатор проверял identity, но не
завершённость — evidence шага `if: always()` было на месте.

- mutation-gate.yml: matrix из шести шардов, `--shard=i/6`, `--shards=6`;
  шаг прогона получил id, его `outcome` пишется в evidence.
- mutation-gate-report.mjs: шард `ok` только с итоговой строкой
  `поймано N из M`, N = M, без FAIL и с исходом шага `success`; лог без
  итога или исход `cancelled`/`skipped` — `interrupted`, отказ; агрегатор
  отвергает прерванный шард как неполный. `outcome` в evidence необязателен
  ради старых артефактов, но, если назван, обязан быть из известного набора.
- тесты: обрыв → interrupted, исходы шага, evidence с outcome; делитель
  шардов один во всех местах workflow; фикстуры зелёных логов получили итог.
- мутанты: mutation-report-truncated-log-is-ok,
  mutation-evidence-ignores-cancelled-step.
- docs/TESTING.md: шесть шардов, итоговая строка.

Потолок 60 минут остаётся стражем от зависшего Chromium. Ledger в ночном
прогоне не включён: ночь гоняет всё.

Issue: #604
User-Visible: no
2026-09-21 14:12:17 +03:00
Claude a551af9219 ci(validate): мутанты по диффу — только по явному запросу, не на кандидате беты и не в full (#601)
`mutantsRequested` отвечает true лишь на PR и `workflow_dispatch mutants=true`
(конвейер ревью, слияние кандидата). Трейлер `Release:` и `full=true` включают
тяжёлые гейты — смоки, golden, performance_smoke — но не мутантов: к бете каждая
задача прогнана ими на ревью и на слитом после ребейза кандидате, ночь покрыта
полным реестром (mutation-gate.yml, #513), а ручной полный прогон ради
артефакта эталонов и приёмка эталонов с трейлером на ветке задачи платили
шестью job впустую. `schedule` мутантов тоже не запрашивает.

Политика release в ci-proof — `mutants: false`: иначе proof кандидата беты
без запрошенных mutant-jobs объявлялся бы stale. review и merge по-прежнему
требуют шесть исполненных job (#541).

Тесты: #510 AC1 переписан под новый список, ci-proof — release без мутантов
green, лёгкий stale, review/merge без запроса stale. Мутанты протокола:
`mutants-run-on-every-push` перепривязан, новые `mutants-run-on-beta-candidate`,
`mutants-run-on-full-dispatch`, `release-proof-demands-mutant-jobs`.
PROCESS.md §10.4, AGENTS.md, docs/TESTING.md, комментарии workflow.

Issue: #601
User-Visible: no
2026-09-20 19:22:38 +03:00
Claude 8dc60aec6e fix(dialogs): находки код-ревью #600 r1 — сегмент без разрыва слов, радиус без блокировки Save, свидетель дефекта №5
M1. Подпись сегмента больше не рвётся посреди слова: колонка `.hpf-seg label`
не сжимается ниже самого длинного слова (`min-width: fit-content`, для
браузеров без него — прежний `0` строкой выше), слова переносятся только по
пробелам (`overflow-wrap: normal`), не поместившийся сегмент переносится на
вторую строку целыми кнопками (`flex-wrap`). Новый свидетель
demo/smoke_dialog_segments_i18n.mjs — четыре диалога × en/ru/de/fr: у каждой
подписи число строк (по Range текстового узла: span — flex-элемент, у блока
один прямоугольник даже при переносе) не больше числа слов, текст внутри
кнопки. На разметке r1 краснеет на en/ru/fr комнаты («Temperature» в 2
строки, «По температуре» в 3). Мутант form-kit-segment-breaks-words
возвращает прежние две строки CSS. ACCEPTANCE.md — строка про подписи
сегмента переписана.

M2. Glow radius — не условие Save: как на dev, нечисло или неположительное
значение при сохранении молча становится общим радиусом. Удалены
glowRadiusValid, MarkerProblem marker.error_glow_radius, ключ в четырёх
локалях словаря settings, aria-invalid у поля; смок device_settings_form
утверждает обратное прежнему (badRadiusDoesNotBlockSave,
zeroRadiusKeepsSaveEnabled) и проверяет «Review N fields» на привязке в
create-режиме. Changelog EN/RU и USER-GUIDE ru/en больше не называют радиус
среди ошибок; таблица устройства говорит про молчаливый общий радиус.

M3. Свидетель дефекта №5 (поповер «?» обрезается границей диалога комнаты):
demo/smoke_dialog_help_clipping.mjs — короткое окно 900×620, диалог комнаты
скроллится, «?» нижней карточки прижат к краю; подсказка выходит за край
скроллера и остаётся hit-test'ируемой во всех пяти точках (elementsFromPoint
сквозь теневые корни — обрезанная часть не hit-test'ится). Четыре ветки:
нативный <dialog> и ha-dialog × Popover API и портальный fallback; ветка
ha-dialog — заглушка с ловушками настоящей mwc-поверхности (transform +
overflow: hidden + собственный скроллер), portal вне поверхности проверен
явно. Настоящий ha-dialog (#505, загрузка колеса HA-frontend) — отдельная
диагностическая приёмка владельца; так и записано в ACCEPTANCE.md, где
появилась таблица AC8 «девять дефектов — свидетель на каждый».

M4. ACCEPTANCE.md: эталонов 13, а не 11 — одиннадцать из ТЗ (список AC10
унаследован от #598 без комнаты) плюс две room-temperature-dialog-*.

L1. USER-GUIDE.ru.md: испорченная строка таблицы настроек пространства
(хвост удалённой строки про декоративный слой) — убран; пояснение «в
редакторе подложки остаются видимыми» вернулось в строку «Видимые слои»;
строка про солнце говорит «слой Двери, окна и ворота выключен» вместо
«Скрыть проёмы включено».

Фикстуры листа диалогов набора пересняты (панель — байт в байт).

Бюджет: initial 289 852 / 290 900; lazy editor 242 989 / 243 900.

Гейты: typecheck, build, bundle:sync (три копии), npm test 2794 pass по
четырём частям, no-new-any, mutation-gate --check, check-inputs --coverage,
смоки: segments_i18n, help_clipping, четыре формы, config_parity,
help_affordance, room_settings, settings_dialog_cards.

Issue: #600
User-Visible: yes
2026-09-20 16:37:31 +03:00
Claude bf6602bbd8 feat(device): «Устройство на плане» по референсу — серия 4 из четырёх (#600)
§7 SPEC.md референса. Пять карточек #598 остались, менялось содержимое; все
ключи _markerDialog и все записи в них прежние (§7.2):

- Basics — баннеры привязки стали callout'ами; Name с подсказкой «Shown on
  the plan»; привязка — сегмент [Virtual | From the HA list] (те же радио
  bmode) и кнопка выбора #marker-binding с панелью в потоке (sourcePicker
  набора), чекбокс «Show entities» — внутри панели рядом с поиском (решение
  владельца Q8); Room селектом с подсказкой «по зоне / переопределяет»;
  секции радара и пылесоса — своими модулями без изменений;
- Tap action — селект с подсказкой Target/Now, «Ask for confirmation» —
  строка-тумблер только при toggle/run (Q8), Entity to toggle и What to run
  теми же условиями (список целей — панель набора), «Controls other light
  sources» — чипы набора и поиск с панелью в потоке;
- Light and glow — «Include the device temperature…» строкой-тумблером;
  роль — сегмент [Auto | Always | Never] с подсказкой результата Auto;
  Leading light entity по условиям; блок свечения (.hpf-block): подзаголовок
  с «?», сегмент [From source | Set colour | Colour and brightness] с
  отключёнными вариантами и aria-describedby на объяснение, плашка цвета
  вокруг прежнего hp-color-opacity (Q5), яркость и радиус строками с
  числом и единицей, при Never блок неактивен целиком (aria-disabled) с
  callout'ом причины;
- Appearance — поле иконки с превью, очисткой и ссылкой Pin в подсказке;
  Display селектом с подсказкой режима; пульс — плашка цвета и строка
  размера; Value source по условиям; подраздел Value badge — компактный
  тумблер, Value селектом с техническим идентификатором, Position сегментом
  [Above | Right | Below | Left]; предупреждения бейджа — заметки и
  callout'ы; Display preview — прежний hp-device-preview целиком в блоке на
  тинте с тегом Now (решение владельца Q3); «Icon size and rotation» —
  две строки range-line в сетке;
- Details — Model и Link в ряд, Description textarea набора, Manuals чипами
  и строкой действий Attach…, «Additional actions» подзаголовком набора.

Футер: Hide и Delete слева, статус, Cancel и Save справа. К10: снимок
черновика при _openMarkerDialog (marker-form-state.ts поверх
dialog-baseline; транзиентные ключи — раскрытие и фильтры списков, busy,
объявление подсказки, autoIcon, uploadId); в edit Save только при
изменениях, закрытие с изменениями спрашивает через _confirmDanger. Прежние
запреты Save (busy, калибровка радара, привязка не выбрана/не подтверждена)
остались; привязка и радиус свечения названы словами под полем
(marker.error_binding, marker.error_glow_radius), «Review N fields» ведёт к
полю. Тумблеры диалога — строки набора (нативный чекбокс, К7), а не
ha-switch; слайдеры — прежний _rangeInput (ha-slider в HA).

Набор: sourcePicker получил toolbar и disabled; сегмент — value у радио,
disabled и describedBy у варианта; callout — role note и id; unitInput —
describedBy; rangeLine — id; новые классы .hpf-block/.hpf-disabled,
.hpf-toolbar/.hpf-check, .hpf-iconfield/.hpf-iconpreview, .hpf-mono,
.hpf-note, .hpf-hint-line, textarea.hpf-input. settingsCopy вынесен в
editors/settings-copy.ts (общий для пространства, комнаты, устройства).

Смоки переписаны осознанно, с пометкой #600: binding_ui (#marker-binding,
.hpf-panel, #marker-show-entities), tap_run (панель набора, #marker-tap-
confirm), value_face_source (.hpf-cand.sel), static_icon (callout'ы),
climate_temp (#marker-use-climate-temp), help_affordance (#marker-name,
.hpf-block.hpf-disabled вместо fieldset[disabled]; второй Escape в грязном
диалоге спрашивает — К10), size_angle_parity (сетка Appearance),
color_picker_consumers (подписи полей), device_preview_parity (сегмент
стороны), ha_controls (тумблеры набора — нативные чекбоксы по К7, ha-slider
прежний). Golden-harness device — .hpf-field/.hpf-block, скроллер .content.
Новый фокусный свидетель demo/smoke_device_settings_form.mjs: пять карточек
без старой разметки, оболочка, привязка/панель/Show entities, Q8, сегменты и
блок свечения, плашки, иконка, бейдж, превью на тинте, размеры, Details,
цели 44, Save/dirty/вопрос при закрытии, create-режим, семь полос экранов.

Мутанты: marker-save-enabled-without-changes, marker-discard-without-asking,
marker-confirm-row-shown-for-do-nothing, marker-never-keeps-glow-block-live,
marker-badge-position-forgets-touch; якоря same-binding-click-resets-source,
state-callout-hidden-under-help, form-kit-segment-drops-radio-semantics
переставлены на новую разметку.

Контрактные тесты: color-picker (два поля набора вместо flex-строк),
native-select-contract (сторона бейджа — сегмент), i18n dead-keys (34 → 38
подсказок), German=English (marker.name_label). Словарь i18n/settings: 21
ключ marker.* на четыре локали; в основном каталоге marker.name_label
«Name», marker.size_label «Icon size and rotation», удалены мёртвые
marker.room_override, marker.light_role_always/never. Фикстуры листа
диалогов набора пересняты намеренно (панель — байт в байт, AC11).

Бюджет: стартовый граф 290 489 (потолок 291 600 без изменений); ленивый
редакторский 243 173 — потолок 240 100 → 243 900 с разбором в
bundle-budget.mjs; старые стили диалога устройства снимаются в финале.

Гейты: typecheck, build, bundle:sync (три копии байт в байт), npm test
2790+ pass по четырём частям, mutation-gate --check, 39 смоков диалога
устройства + новый + три формы серий 1–3.

Issue: #600
User-Visible: no
2026-09-20 12:18:12 +03:00
Claude e2c0c7ed81 feat(room): «Настройки комнаты» по референсу — серия 3 из четырёх (#600)
§6 SPEC.md референса. Четыре карточки #594 остались, менялось содержимое:

- Basics — Display name и Home Assistant area в одну строку из двух полей
  (#room-name, #room-area — контролы набора); подстановка имени из зоны при
  пустом имени прежняя;
- Fill — «Как у пространства» стала строкой-тумблером (#room-fill-inherit,
  решение владельца Q4): включена — подсказка «Следует за пространством:
  режим»; выключена — сегмент из пяти режимов, который стартует с
  действующего режима пространства, а не с «None». «Свой цвет» — плашка
  вокруг прежнего hp-color-opacity (Q5): свотч, hex, число прозрачности,
  Reset у своего цвета. Границы комфорта при эффективной «Температуре» —
  два поля с единицей °C (#room-temp-min/-max) с плейсхолдерами границ
  пространства, легендой Cold / Comfort / Hot, ошибкой под полями и ссылкой
  «Как у пространства». Модуль room-temperature-controls.ts удалён — его
  разметку заменили поля набора;
- Sensor sources — подзаголовки, прежний сегмент «среднее / конкретный», при
  конкретном — кнопка выбора в стиле селекта и панель с поиском в потоке
  карточки (sourcePicker набора, роль listbox/option; общая с диалогом
  устройства в серии 4). Кандидаты и запись прежние;
- Font sizes — две строки «слайдер + число % + Reset to 100%» с концами шкалы,
  подсказка про размер карточек пространства, образец карточки сохранён
  внутри .hpf-tint (решение владельца Q2).

Футер: Cancel | статус | Save; в create — Keep as walls слева. К10: снимок
черновика при _openRoomEdit (room-form-state.ts поверх dialog-baseline),
в edit Save только при изменениях, закрытие с изменениями спрашивает через
_confirmDanger; в create Save по прежнему условию (имя или зона), отмена без
вопроса — она и раньше значила «не создавать» и возвращала контур на план.
Ошибки черновика: имя (edit) / имя-или-зона (create), нечисло в границах —
подпись под полем, «Review N fields» в футере ведёт к первому полю.

Из eager-листа dialogs.styles.ts удалены .roomtemprange*, .roomdialog .srcrow
и .dropbtn; `overflow-x: hidden` у .body убран — скроллер один (К3).

Смоки переписаны осознанно, с пометкой #600: smoke_room_settings (тумблер
вместо радио, #room-name/#room-area, .hpf-panel .hpf-cand, старт сегмента с
режима пространства), smoke_room_temperature_thresholds (.hpf-temprange,
ссылка сброса). Golden-harness room-temperature — те же селекторы. Новый
фокусный свидетель demo/smoke_room_settings_form.mjs: четыре карточки без
старой разметки, имя и зона в одну строку, оболочка 560 и один скроллер,
тумблер/сегмент/плашка/границы/источники/размеры пишут в свои ключи, Save и
вопрос при закрытии, create-режим, семь полос экранов.

Мутанты: room-save-enabled-without-changes, room-discard-without-asking,
room-fill-segment-starts-at-none, room-temp-min-writes-max. Якорь
form-kit-writes-to-a-neighbour-key прежний. smoke-links: символ
roomTemperatureControls заменён на renderRoomSettingsDialog.

Словарь i18n/settings: девять ключей room.* на четыре локали; в основном
каталоге room.area_label без «(unassigned)». Фикстуры листа диалогов набора
пересняты намеренно (панель — байт в байт, AC11).

Бюджет: стартовый граф 290 575 — потолок ОПУЩЕН 292 200 → 291 600 (ушли CSS
диалога комнаты и модуль границ); ленивый редакторский 239 391 — потолок
238 600 → 240 100 с разбором в bundle-budget.mjs.

Гейты: typecheck, build, bundle:sync (три копии байт в байт), npm test
2790+ pass по четырём частям, mutation-gate --check, смоки: room_settings,
room_settings_form, room_temperature_thresholds, editor_tabs, font_scales,
resize_labels, room_autoclose, binding_ui, color_picker_consumers,
lazy_editor_chunk, space_settings_form, general_settings_form, ux_fixes,
settings_dialog_cards, dialog_footer_width.

Issue: #600
User-Visible: no
2026-09-20 11:39:46 +03:00
Claude de71bd7967 feat(general): «Общие настройки» по референсу — серия 2 из четырёх (#600)
§5 SPEC.md референса. Семь карточек #598 остались, менялось содержимое:

- Display — две полные строки с иконкой, подписью и тумблером справа; пояснение
  радара по §5.2 стало подписью строки (текст прежний, ключ переехал в ленивый
  словарь как gs.radar_show_live_hint, «?» у строки больше нет);
- Zigbee links — заголовок и «?» функции (topology.help) рисует карточка, а
  блок hp-zigbee-topology-settings получил режим embedded: строка-тумблер,
  callout «сохраните настройку», подзаголовки ZHA / Zigbee2MQTT, поле тем и
  строки действий контролами набора. Заголовок больше не удваивается
  (дефект 1). Лист набора вносится в теневой корень компонента; токены
  --hpf-* наследуются с хоста. Прежний render без embedded не тронут — его
  держит контракт #459;
- Room fill colors — три подзаголовка (Lights / Temperature / Zigbee signal,
  Q6) и плитки цвета по три в ряд: название на свотче, hex, число
  прозрачности; свотч — trigger прежнего hp-color-opacity (Q5);
- Light-source glow — две плитки и поле радиуса с единицей внутри рамки;
- Plan — плашка цвета стен, сегмент фона, при Static — плашка фона с Reset и
  подсказкой (тема / свой цвет);
- Sun — gs.sun_missing остаётся callout'ом (К5); север: поле градусов с «°»,
  ссылка Clear, «N» и компас 44 px справа — индикатор набора, но тянуть его
  по-прежнему можно (smoke_sun гоняет настоящий drag; старый 120-px SVG и его
  CSS в plan.styles.ts удалены); лучи — строка-тумблер с подписью; источник
  лучей — сегмент (sun-settings-view.ts переписан с <select> на segmented,
  логика та же);
- Data — подзаголовки с «?» и строки действий набора.

К10 (Q1): Save только при изменениях и без ошибок (радиус > 0, север 0–359),
статус в футере, вопрос при закрытии с изменениями; снимок при открытии —
editors/general-form-state.ts поверх общего editors/dialog-baseline.ts (на него
же переведён space-form-state.ts). _settingsDialog не получил новых ключей.

Удалено мёртвое: _renderColorRow (рантайм и карточка), .sunrow/.suncol,
ключи gs.wall_group, gs.bg_group, gs.bg_theme, gs.radar_show_live.help(.aria)
в четырёх локалях. Девять новых ключей — в i18n/settings.

Смоки переписаны осознанно, с пометкой #600: smoke_general_settings (инвентарь
по классам набора, подзаголовки .hpf-sub h4, пояснения под «?» и подписью,
ни одного .rhint), smoke_sun (сегмент источника лучей вместо select, поле
#gs-north, radius drag 18 px), smoke_backup_transfer (подзаголовок и строка
действий), smoke_settings_dialog_cards (callout вместо .rhint). Новый фокусный
свидетель demo/smoke_general_settings_form.mjs: семь карточек и отсутствие
старой разметки, один заголовок у Zigbee, десять плиток, компас 44, цели 44,
запись плиток/плашки/радиуса/севера/сегментов в свои ключи, Save/dirty/вопрос
при закрытии.

Мутанты: general-save-enabled-without-changes,
general-tile-opacity-writes-a-neighbour-key, general-north-clear-writes-zero,
topology-embedded-draws-its-own-heading; якоря state-callout-hidden-under-help
и dialog-card-loses-its-heading переставлены на новую разметку.

Контрактные тесты: houseplan-source.mjs приводит `host.` к `this.` и в телах
вынесенных диалогов; color-picker — три явных showOpacity=true (плитки, плашка
стены, цвет комнаты); счётчик производных .aria 35 → 34.

Бюджет: стартовый граф 290 711 — потолок ОПУЩЕН 292 500 → 292 200 (ушёл
120-px компас из eager-листа и мёртвые ключи); ленивый редакторский 237 929 —
потолок 236 300 → 238 600 с разбором в bundle-budget.mjs.

Гейты: typecheck, build, bundle:sync, npm test 2792 pass, mutation-gate
--check, семь смоков «Общих», smoke_color_picker_consumers без правок.

Issue: #600
User-Visible: no
2026-09-20 11:04:39 +03:00
Claude 91a008db9c feat(space): диалог «Пространство» по референсу — серия 1 из четырёх (#600)
§4 SPEC.md референса, четыре карточки: Basics (имя и масштаб в сетке 1fr/175,
подложка двумя choice-карточками, callout о пересчёте масштаба), Appearance
(строка-тумблер границ с иконкой и подписью, сегмент стиля стен с образцами
линии, плашка цвета с hex/прозрачностью/Reset, сегмент заливки с деталями
режима, компактный список «Видимые слои» с логикой «включено = видно» — К2),
Room cards (строка-тумблер названий, четыре плитки значений с отключением и
callout-ссылкой, слайдер размера с числом, концами и «Сбросить до 100%», образец
карточки внутри блока на тинте — Q2), Sun & light (сегмент фона, плашка цвета
фона, радио «свет через окна» в строку, север селектом с компасом 44 px и полем
градусов, строка-тумблер свечения).

Одна форма на два рантайма. `renderSpaceForm` в `editors/space-form.ts`
собирает тело и футер; редакторский вход (`space-settings-dialog.ts`) и
онбординг подают порт с тем, что у них различается, и сами рисуют
`<hp-dialog data-kind=…>` — контракт #489 требует литеральный kind. Копия
разметки онбординга на 300 строк удалена; локальные дубли CELL_CM_* и
strictNumber там же — тоже («одно число — один источник»).

К10 (решение владельца Q1): Save активен только при изменениях и без ошибок,
в футере «Есть несохранённые изменения» либо ссылка «Проверьте поля: N» к
первому ошибочному; «✕», Cancel и Escape с изменениями спрашивают через
`_confirmDanger`, без изменений закрывают сразу. Снимок делается при открытии
(`editors/space-form-state.ts`, WeakMap по хосту) — `SpaceDialogState` не
получил ни одного ключа. Валидация: имя, подложка без изображения, диапазон
температур, север 0–359 — с `aria-invalid` и текстом под полем.

Строки. 51 новый ключ живёт в ленивом словаре `i18n/settings` (приём
#423/#459), не в первом кадре: стартовый граф 291 260 при потолке 292 500.
Отдельный словарь, а не `support`, — иначе онбординг тянул бы весь словарь
редактора ради одной формы. 11 существующих ключей переименованы по Q6
(«Space name», «Appearance», «Room cards», «Draw it myself», «Zigbee signal
next to devices»…), 9 мёртвых удалены из четырёх локалей. `hp-color-opacity`
получил `hide-label`: подпись рисует плашка, `label` остаётся для title/aria и
`smoke_color_picker_consumers` проходит без правок (AC5).

Смоки. Переписаны осознанно (К6 снят), каждый — с пометкой #600 у правки:
smoke_hide_layers (компактные строки, инверсия К2 проверяется кликом),
smoke_ux_fixes (сегмент, строка-тумблер, поля °C, ширина 560 — Q7),
smoke_dialog_footer_width (560 для form-shell), smoke_temp_fill (два поля по
id), smoke_settings_dialog_cards (три сегмента), smoke_space_settings и
smoke_space_create_display_defaults и smoke_lazy_editor_chunk (choice-карточка
по смыслу, не по индексу — порядок §4.2 draw → file), smoke_space_scale_defaults
(поле с единицей). Новый фокусный свидетель demo/smoke_space_settings_form.mjs:
состав карточек и отсутствие старой разметки, оболочка 560 и один скроллер,
цели 44, запись каждого нового контрола в свой ключ, Save/dirty/вопрос при
закрытии, семь контрольных ширин (320…1920).

Мутанты: space-fill-segment-writes-a-neighbour-key,
space-save-enabled-without-changes, space-discard-without-asking,
space-layer-toggle-loses-inversion.

Контрактные тесты: `houseplan-source.mjs` дописывает общие модули диалогов к
логическому исходнику; коллектор i18n видит `help(...)` порта и `shelp(...)`
(заодно встал на учёт `radar.help`, проходивший мимо счётчика — 30 → 35).

Потолок ленивого редакторского графа 225 900 → 236 300 (замер 235 560) с
разбором в bundle-budget.mjs: словарь на четыре локали и подписи — большая
часть роста; промежуточно, итог в конце серий.

Гейты: typecheck, build, bundle:sync, npm test 2792 pass (красным был только
потолок — поднят с доводом), 10 смоков пространства зелёные,
smoke_color_picker_consumers без правок, mutation-gate --check.

Issue: #600
User-Visible: no
2026-09-20 10:28:51 +03:00
Claude bd6da53f04 fix(editors): свидетель карточек отдельным смоком, два списка в сегменты
Закрывает находки код-ревью r1 по #598.

M1 — неприкосновенный смок восстановлен. `demo/smoke_tap_run.mjs` вернулся
побайтово к версии `origin/dev`: он один из девяти `.srcrow`-смоков, которые
ТЗ объявило неизменяемыми, и правка в нём обесценивала собственное
доказательство AC2/AC3. Проверки карточек переехали в новый
`demo/smoke_settings_dialog_cards.mjs` — отдельного свидетеля ТЗ не запрещало.

M2 — сегментированный переключатель сделан там, где он обещан и уместен:
`space-bg-mode` и `space-zero-wall-style` переведены из `<select>` в
`segmented`. Остальные выборы в трёх диалогах остаются списками осознанно:
привязка маркера, роль света и режим свечения уже `role="radiogroup"`
(`.markerradios`), позиция значения закреплена смоком
`smoke_device_preview_parity`, списки сущностей и тем длинные. Сокращение
объявлено в ТЗ и в обоих changelog.

M3 — мутант `state-callout-hidden-under-help` патчит теперь оба состояния:
`marker.run_target_gone` и `gs.sun_missing`; гард — новый смок.

Заодно: `ensureFormKitStyles(this.host)` не вызывался ни в одном из трёх
диалогов — стили набора приезжали только из диалога комнаты, и без него
сегменты имели высоту 14 px вместо 44. Нашёл новый смок проверкой
`bgSegmentTargetIsBigEnough`.

Гейты: typecheck, test (2788 pass), build, bundle:sync, три смока,
mutation-gate --check, оба мутанта задачи.

Issue: #598
User-Visible: yes
2026-09-19 19:17:20 +03:00
Claude c22a1f43db feat(editors): три диалога настроек собраны в карточки-группы
«Общие настройки», «Пространство» и «Устройство на плане» были плоскими
столбцами. В «Общих» девять подписей-заголовков шли вперемешку с рядами и
абзацами; в «Устройстве» на 760 строк формы не было ни одного деления, и
«Название» лежало в одном списке с «Дополнительными действиями».

Теперь это карточки общего набора: семь в «Общих», четыре в «Пространстве»,
пять в «Устройстве». Набор взят как есть — он поднят в #594 и доказан на диалоге
комнаты и редакторе панели (#597).

Шесть пояснений переехали под «?» к своему заголовку, подзаголовку или строке;
текст не переписан. Два сообщения о СОСТОЯНИИ остались на виду — решение
владельца: `gs.sun_missing` и `marker.run_target_gone` говорят, что настройка
сломана прямо сейчас, а второе ещё и называет пропавшую цель подстановкой,
которой в статической подсказке взяться неоткуда.

`gs.hint` при этом переехал из ленивого support-словаря в основной: «?» читает
текст через `_t`, а не `supportT`. Тест мёртвых ключей сливает оба словаря, так
что копия в support удалена во всех четырёх локалях.

Классы-опоры сохранены намеренно: `.srcrow`, `.dispsection`, `.gsrow`,
`.colorrow`, `.namein`, `.areasel`, `.tempin` остались на своих элементах, а
карточки добавились вокруг. За эти имена держатся одиннадцать смоков, и потеря
любого превратила бы ревью в чтение переписанных свидетелей.

Изменён ровно один смок — `smoke_general_settings`, и изменён в сторону
строгости: вместо одного списка подписей он снимает теперь два (заголовки
карточек и подзаголовки внутри), требует непустоты каждой карточки, проверяет
что радиус свечения лежит ВНУТРИ карточки свечения (подпись такой границей не
была), что четыре переехавших пояснения доступны кнопкой и что их абзацев в
форме не осталось.

`src/houseplan-onboarding-runtime.ts` затронут двумя строками: он второй
потребитель переименованных ключей `space.hide_*_tip` → `space.hide_*.help`.
Один текст — один ключ; дублировать его ради неприкосновенности файла хуже.

Потолок стартового графа поднят 291 400 → 292 500. Выросли СТРОКИ, а не код:
12 новых ключей в четырёх локалях плюс переехавший `gs.hint`. Разметка диалогов
как была в ленивом редакторском графе, так там и осталась.

Свидетели: `state-callout-hidden-under-help` прячет сообщение о состоянии под
«?» и краснит новый блок `smoke_tap_run`; `dialog-card-loses-its-heading`
обнуляет заголовок карточки и краснит `smoke_general_settings` — карточка без
заголовка это снова плоский список, только с рамкой.

Issue: #598
User-Visible: yes
2026-09-19 17:52:49 +03:00
Claudeandclaude[bot] b28907f63d refactor(styles): редактор панели переезжает на общий набор контролов
Набор контролов был поднят из редактора боковой панели в #594, но сама панель
осталась на своей копии тех же правил. Две копии расходятся молча: правка в
наборе не доедет до панели, и наоборот.

Просто вызвать `formKitCss` из панели нельзя. Общие правила в её листе не идут
подряд: между карточкой-группой и обводкой фокуса стоит сетка `.summary-general-grid`,
между фокусом и выключенным состоянием — `button { font: inherit }`, между ним и
рядом-переключателем — `svg`. Вставить один общий кусок значило бы переставить её
объявления, а перестановка правил равной специфичности — уже не подстановка:
проверять пришлось бы каждое правило по отдельности.

Поэтому генератор разрезан на пять именованных фрагментов — карточка-группа,
обводка фокуса, выключенное состояние, ряд-переключатель, подпись ряда, — и
панель подставляет каждый на своё место. `formKitCss` остаётся их композицией,
чтобы у диалогов не появилось пяти вызовов вместо одного.

Доказательство вместо обещания. У настроек панели нет ни одного golden-кадра: в
матрице нет сцены `dialog: 'summary'`, и «панель не изменилась» показать
пикселями нечем. Замена эталону — побайтовое сравнение собранного листа с
замороженным снимком с `origin/dev`:

    длина было 14585 стало 14585 — ПОБАЙТОВО СОВПАЛО

Две находки ревью ТЗ закрыты тестами, а не формулировками:

- проверка «копии не осталось» ловила один фрагмент из пяти (`min-height: 54px`
  встречается только в ряду-переключателе). Теперь цикл по всем пяти текстам;
  проба — фрагмент карточки-группы, оставленный в комментарии, — краснит именно
  этот цикл, а прежняя проверка была бы зелёной;
- «выход `formKitCss` остаётся байт в байт» опиралось на тесты, сравнивающие
  подстроки и счётчики. Добавлены две фикстуры с полным текстом обеих форм листа
  диалогов — с рядом-переключателем и без. Форма с переключателем сегодня не
  используется никем, и её расхождение не поймал бы никто до первого потребителя.

Три замороженные фикстуры — осознанный долг: они удаляются на том шаге эпика,
который законно меняет вид панели. Записано рядом с фикстурой.

Issue: #597
User-Visible: no
2026-09-19 14:19:54 +00:00
Claude 7df2740f1c feat(furniture): пакет 0.4.0 — 60 дизайнерских символов вместо 44 плюс 12 примитивов
Встроенная библиотека мебели была смешанной: 44 предмета рисовал дизайнер, а
12 — код, прямоугольниками и эллипсами в unit box. Они выбивались из стиля, и
три категории меню (`computer`, `hood`, `oven`) вообще не открывались, потому
что вида сверху для них не было.

Пакет 0.4.0 перерисовывает все 56 существующих публичных ID и добавляет четыре
новых: `computer`, `hood`, `oven`, `cactus`. Каталог становится равен поставке:
60 символов, 32 непустые категории.

Совместимость. Ни один сохранённый план не мигрирует: положение, размер,
поворот, отражение, цвет, прозрачность и толщина линии не трогаются, а
default-размеры всех 56 прежних ID совпадают со старыми до числа — таблица
зашита в `test/furniture.test.mjs`, чтобы сдвиг было видно тестом, а не глазами.

Ловушка, ради которой примитивы удалены целиком. Библиотека собиралась союзом
каталога и `LEGACY_FURNITURE`, и обе половины несли ОДНИ И ТЕ ЖЕ публичные ID.
`BY_ID` — это `Map`, она оставляет последнюю запись, поэтому примитив молча
побеждал дизайнерский рисунок, который его заменял: задача выглядит выполненной,
12 предметов не обновились, ни один существующий тест этого не видит. Поэтому
`LEGACY_FURNITURE`, `RETAINED_IDS`, тип `Prim`, `box()` и `primitivePathD`
удалены, а на дубликаты в каталоге стоит тест и мутант.

Два продуктовых решения владельца (#593):

- `cactus` живёт в категории `plant`, а не `exercise`, как предлагал пакет:
  плитка «Тренажёр» рисует тренажёр, и открывать её на кактус — врать о
  содержимом палитры. `exercise` остаётся скрытой пустой категорией, непустых
  становится 32, а не 33;
- при отказе ленивого чанка с рисунками не рисуется НИЧЕГО. Прежде 12
  примитивов рисовались всегда, и `docs/FURNITURE.md` это обещал; обещание
  снято. Это единственное место, где задача ухудшает наблюдаемое поведение, и
  оно проверяется смоком, а не словом.

Бюджеты. Ревью ТЗ показало, что «бюджет ленивого графа защищён» было
заявлением без гейта: размеры печатались в отчёт и ни с чем не сравнивались.
Добавлены `lazyGraphCeilingViolation` и два настоящих потолка со своим мутантом.
Стартовый граф ушёл ниже полосы, поэтому `INITIAL_VIEW_GZIP_CEILING` опущен
292 400 → 291 400 — вторая половина храповика, без которой выигрыш отыграется.

Провенанс. Документ пакета воссоздан под 0.4.0: грант владельца, имя архива и
его SHA-256. Это единственная исполнимая защита от повторения истории #159, и
она не ослаблена — число проверок в тесте не уменьшилось, а мутант подменяет
хеш и обязан её покраснить.

Свидетели: пять новых мутантов, новая golden-сцена `furniture-new-symbols-light`
для четырёх новых ID, матрица 63 → 64. Шестой мутант правится, а не добавляется:
`furniture-placement-needs-art` ссылался на удалённое поле `g` и переставал
компилироваться — раннер честно отличал это от «пойман» и отчитывался
setup-failure. Ложь мутанта осталась прежней по смыслу, убрана только ветка про
примитив.

i18n: 7 ключей × 4 локали. `furn.cat_exercise` не добавлен — категория скрыта.

Issue: #593
User-Visible: yes
2026-09-19 13:38:49 +03:00
ClaudeandClaude 4344ede13a feat: общий набор контролов формы и диалог комнаты по нему (#594)
Шаг 1 эпика #591. Набор поднят из редактора боковой панели — он сделан по
макетам дизайнера и уже живёт в продукте, — а не собран заново по прототипу.

Стили набора выдаёт генератор, параметризованный именами: контейнер формы,
префикс классов, карточки, футер и токены цвета. Это не украшение архитектуры,
а условие переезда панели на общий источник: тест с замороженной фикстурой
показывает, что с параметрами панели генератор отдаёт её нынешние правила
дословно — карточку, обводку фокуса, выключенное состояние и ряд-переключатель.
После этого переезд станет подстановкой, а не редизайном, и пиксели панели не
дрогнут. Сама панель в этом коммите не тронута.

Диалог комнаты собран в четыре карточки: «Основное», «Заливка», «Источники»,
«Размеры шрифтов». Порядок полей прежний, записи прежние — те же ключи
черновика, те же условия «Сохранить», тот же `hp-color-opacity` с атомарной
записью цвета и прозрачности. Пояснения уехали под «?» у заголовков групп
(решение владельца 18.09), выбор источника стал сегментированным
переключателем — под капотом это по-прежнему радиогруппа, поэтому стрелки и
скринридер работают как раньше.

Два измерения определили форму решения, и оба стоило сделать до кода.

Первое: лист набора в синхронном графе дал +1459 Б gzip и пробил потолок
initial View на 415 Б. Диалоги — инструмент администратора, во View их не
открывают, поэтому лист уехал в ленивый редакторский граф и вносится в теневой
корень при открытии диалога — тем же способом, которым это делает боковая
панель. Потолок не поднимался.

Второе: `css` с интерполяцией запрещён минификатором сборки (он намеренно
падает, чтобы не зависеть от рантайма), поэтому лист собирается `unsafeCSS`.
Цена — несжатые пробелы генератора, замерено 59 Б gzip; копия литералом стоила
бы расхождения с генератором, то есть единого источника.

Оставшийся рост initial View — 291 356 → 291 872 Б (+516) — это четыре текста
подсказок в двух встроенных локалях. Тексты сокращены до одного предложения:
подсказка под «?» и должна быть короткой.

`_renderRoomSource` переехал в модуль комнаты вместе со стабом карточки: у него
остался ровно один потребитель.

Issue: #594
User-Visible: yes
2026-09-19 11:08:03 +03:00
Claude 337d85ea4c fix(process): слияние не обрывает дифф кандидата и называет причину сбоя запуска
Шаг слияния конвейера считает patch-id материала и кандидата через
`git diff --full-index`, когда `dev` сдвинулся за время ревью. Дифф задачи,
пересобирающей бандл, несёт три копии `houseplan-card.js` — у #594 это
7 103 616 байт. `sh()` не задавал `maxBuffer`, умолчание `spawnSync` — 1 МиБ,
и процесс убивался по ENOBUFS на 1 027 868 байтах.

Вторая половина дефекта прятала первую: `r.error` нигде не читался, а
`status: r.status ?? 1` выдавало `null` за «git вернул 1». `must()` брал
`r.stderr || r.stdout`, stderr был пуст — и в issue уезжало
«Слияние не выполнено: diff: <огрызок диффа>». Огрызок выглядит осмысленным
и уводит разбор в сторону.

Путь исполняется только при сдвинувшемся `dev`, поэтому дефект дремал. Но
конвейер сам пушит в `dev` документ ревью на каждом раунде, так что
`devMoved` — норма, а не исключение.

Что изменено:

- `MAX_COMMAND_OUTPUT_BYTES` (256 МиБ) и явный `maxBuffer` в `sh()`;
- `sh()` называет сбой запуска в `stderr` результата вместо молчания;
- `sh()` экспортирован, у обоих пунктов есть исполнимый свидетель;
- мутант `merge-candidate-truncates-the-candidate-diff` возвращает 1 МиБ.

Issue: #596
User-Visible: no
2026-09-19 10:31:36 +03:00
Claudeandclaude[bot] 5b6d5d1d8f test: объявленный Baseline-Reviewed прогон проверяется отдельно от вершины ветки
`test/release-gate.test.mjs` строил ожидания из живого состояния репозитория, а
`localEvidence` читает `baselines.reviewedRun` из сообщения HEAD-коммита. Пока
вершиной был обычный коммит, поле оставалось `null` и всё сходилось. Как только
вершиной становился коммит приёмки эталонов, поле становилось непустым, и
`evaluateCiProof` законно требовал объявленный прогон — которого заглушка
`loadContext` не отдавала. Тест краснел на всей ветке, ради которой #573 и
писалась, и вместе с ним краснел бы `dev` сразу после слияния такой ветки.

Воспроизведено на `issue/594-form-kit-room@68bb7558`: 2776 pass / 1 fail, из-за
чего конвейер ревью не запустился вовсе.

Продуктовый код не менялся: контракт «объявленный прогон обязан существовать,
быть завершённым, не отменённым и быть Validate» — ровно то, что задумано, и он
намеренно не требует `success`, потому что приёмка эталонов по определению
ссылается на прогон, где golden покраснел.

Что изменено:

- зелёный путь теста считается на evidence с `baselines.reviewedRun: null` и
  больше не зависит от того, каким коммитом оказалась вершина;
- контракт объявленного прогона вынесен в отдельный тест и проверяется с трёх
  сторон: завершённый неотменённый Validate с `conclusion: 'failure'` — зелёно;
  прогона нет — отказ; прогон отменён — отказ;
- мутант `declared-baseline-review-run-never-checked` обнуляет объявление и
  краснеет на новом тесте.

Доказательство на фикстуре «вершина — приёмка эталонов»: старый тест 8 pass /
1 fail, новый 10 pass / 0 fail.

Issue: #595
User-Visible: no
2026-09-19 06:12:46 +00:00
Claude 1403fb440b test: давать послабление переноса блоку, а не строке (#592)
Находка M1 код-ревью r1, воспроизведена: первая редакция сопоставляла
одиночные строки по всему диффу, и этого хватало для обхода. Несвязанная
уборка удаляет где-то строку с `any`, новый код добавляет свою — текстуально
такую же, — и гейт молчит. Совпадение здесь не экзотика: в базе 887 явных
`any`, типовые однострочники повторяются буквально, и две такие строки
встретились в самом коммите переноса.

Теперь перенесённым признаётся только непрерывный кусок не короче пяти строк,
встречающийся подряд и целиком среди удалённых строк ОДНОГО файла. Случайно
совпасть пятью строками подряд практически невозможно, а настоящее извлечение
подсистемы из таких кусков и состоит: на этом диффе признано 1296 строк из
1395 — на одну меньше, чем при построчном сопоставлении, и эта одна была ровно
случайным совпадением.

Каждый удалённый кусок оплачивает ровно одно добавление: повторная вставка того
же блока остаётся новым кодом.

Мутант заменён на `no-new-any-forgives-a-single-matching-line` — он опускает
порог до одной строки, то есть открывает ровно найденную дыру; тест обхода на
нём краснеет. Тестов пять: перенос куска, обход одиночной строкой, кусок короче
порога, бюджет на повторную вставку, смена отступа.

Issue: #592
User-Visible: no
2026-09-18 22:17:25 +03:00
Claude 61c74a7012 test: не считать дословный перенос новым any (#592)
Validate вернул задачу с красным `no-new-any`: шесть строк в новых модулях
диалогов. Все шесть перенесены побайтово из редакторского рантайма — `as any`
в них написан годы назад и в этом диапазоне не менялся. Счёт долга это
подтверждает: явных `any` в `src/**` было 887 и осталось 887.

Гейт мерил «добавленные строки», и для извлечения подсистемы такая мера
ложна: она требует типизации ровно там, где ничего не изменилось. Причём
единственный способ её удовлетворить — дописать `any-ok` в перенесённое тело,
то есть сломать доказательство переноса (тела обязаны совпадать побайтово) ради
зелёного гейта. Это починка задом наперёд.

Теперь строка, дословно удалённая из одного файла диапазона и добавленная в
другой, новым кодом не считается. Бюджет ведётся мультимножеством: два
добавления при одном удалении оставляют одну находку, а перенос с изменённым
отступом переносом не считается — это уже правка. Отчёт печатает, сколько строк
признано перенесёнными, чтобы послабление было видно, а не подразумевалось.

Гейт #342 при этом остаётся собой: новый `any` без удаления-близнеца — находка,
и три новых теста проверяют обе стороны. Мутант
`no-new-any-counts-every-added-line-as-moved` признаёт перенесённой любую
добавленную строку — заявленный тест на этом краснеет.

Отклонение от скоупа ТЗ: `scripts/no-new-any.mjs` в нём не назван. Альтернатива
— шесть `any-ok` в перенесённом коде — стоила бы побайтовой сверки, на которой
держится вся приёмка задачи.

Issue: #592
User-Visible: no
2026-09-18 19:59:11 +03:00