Commit Graph
158 Commits
Author SHA1 Message Date
Matysh 633cb20e59 fix: follow Home Assistant Area marker moves
Issue: #126
User-Visible: yes
2026-08-31 09:45:53 +03:00
Codex 98028a3093 ci: the backend gate now checks exactly what it promises (#399)
Three claims a green backend used to make, each slightly wider than the
truth — and #392 happened in exactly that gap.

The frontend pin said 20260826.1 next to homeassistant==2026.8.3, whose
package_constraints.txt requires 20260729.7: a combination that exists
in no HA release. It was never derived from anything — someone once
picked it. It is now taken from the constraints, the source is named in
the file, and a test holds both numbers together so raising HA cannot
quietly desync them.

ruff's include declared three trees while CI linted one. Narrowed the
declaration rather than widening CI: the debt in scripts/ and
tests_backend/ (56 findings, mostly E402/I001, plus 7 B023 and 5 B017)
has its own cost and its own decisions, and belongs in its own task, not
in a visibility fix. test/lint-scope.test.mjs now compares the two, so
they can only move together.

The pin check skipped a workflow when it found neither the package name
nor the requirements path — and both vanish together the moment someone
returns to Defaulting to user installation because normal site-packages is not writeable, i.e. the gate switched itself off
under precisely the change it exists to catch. It now walks the whole
.github/workflows directory and decides per file by a positive sign: if
a file installs python packages, it must install them from the pins
file. Verified by dropping a rogue workflow into the directory — it
reddens without touching any list.

Three mutants registered and each run by hand.

User-Visible: no
Issue: #399
2026-08-31 04:35:56 +03:00
Codex d9b6766362 test: the sys.modules guard now sees the write, not its spelling (#398)
The guard introduced by #394 matched the literal
sys.modules['custom_components... and therefore never looked at
pure_imports.py, which writes through a variable — the third instance of
the #389 class walked straight past the check created for it.

The guard now inspects the write itself and decides by the key: a whole
literal or the literal head of an f-string is safe unless it starts with
custom_components (that is how tests register homeassistant.*, hp_pure.*
and houseplan.trails); anything else — a variable, a concatenation,
setdefault/update — counts as a violation whenever the file is able to
name the package at all, i.e. contains a custom_components. literal. A
file that never names the package cannot poison it through a variable,
so restoring a snapshot stays legal.

load_pure now removes what it registered. Removing its own name is not
enough: relative imports pull neighbours in, so junction_limits leaves
wall_segment_model and coordinate_canonicalization behind. It removes
the whole custom_components difference accumulated during exec_module,
in a finally, and a repeated call still works.

pure_imports.py is a named exemption of the static guard precisely
because that guard cannot see the cleanup — so the cleanup is proven by
an executable test instead, and the mutant pure-imports-stops-cleaning
reddens it. Both mutants were run by hand.

User-Visible: no
Issue: #398
2026-08-31 03:56:01 +03:00
Codex dddbbe5522 ci: make the strict typing gate actually run (#42)
r6 Medium: AC4 was measurable only on a developer's machine — no
workflow invoked mypy, so a typing regression in any of the six
allowlist modules reached dev unnoticed while the issue claimed
measurable backend quality. Coverage and lint had continuous gates;
typing had a text comparison of a committed list.

The backend job now runs mypy right after ruff, from the same pinned
dependency file (mypy==2.3.1 — an unpinned checker would redden on code
that never changed). The step derives its module list from the
pyproject.toml strict allowlist instead of duplicating it, because a
drifted duplicate is a green step checking the wrong modules, and it
refuses an empty list rather than passing silently.

Guarded twice: a contract test pins all three facts (pinned checker,
a step that really invokes it, list read from pyproject) and the new
typing-gate-stops-running mutant reddens when the invocation is
neutered.

User-Visible: no
Issue: #42
2026-08-30 22:00:52 +03:00
Codex 9f50778df5 test: close the r4 mediums — structural tuple scan and reuse-key inputs (#42)
M1: the AC5 scanner parses the (field, code, message) literal tuple in
validation.py structurally instead of naming the two known codes — a
third tuple entry with an unregistered code now fails the registry test
(verified with an injected invalid_ghost_entity_mutant_probe), and a
tuple whose string count is not a multiple of three refuses instead of
guessing.
M2: the backend reuse key now includes its direct job inputs introduced
by this issue — scripts/backend-coverage-baseline.txt (the threshold the
comparison step reads), requirements_test.txt (the pip source) and
pyproject.toml (ruff/mypy config) — verified: the key changes when the
baseline changes and is restored byte-for-byte with the file.

User-Visible: no
Issue: #42
2026-08-30 21:34:32 +03:00
Codex f7ae00019c test: the jamb-margin harness assert reads the JSON details (#42)
The harness test still parsed the legacy 'space=... opening=...
margin_cm=...' string; #42 replaced that message with structured JSON
details (the client localizes from the code and reads the fields). The
assert now parses the payload and checks the same three facts.

User-Visible: no
Issue: #42
2026-08-30 21:34:32 +03:00
Codex 778da930cb fix: the quality test no longer stubs packages in sys.modules (#42)
Same defect class as #389: _const() planted bare ModuleType stand-ins
for custom_components(.houseplan) and never removed them, so the HA
harness running later in the same pytest process saw a package without
async_setup — 85 test_ha_* failures with 'No setup or config entry
setup function defined'. const.py imports nothing, so the loader needs
no package context at all: load it by file path under a standalone
module name and leave sys.modules untouched (verified: no
custom_components* keys after _const()).

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

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

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

User-Visible: yes
Issue: #42
2026-08-30 21:34:31 +03:00
Matysh 8dfb0a9e36 test: put the package stub in one place, under an honest condition
Чистые тесты подменяли custom_components и custom_components.houseplan
пустышками и не убирали их никогда. В CI это не стреляло только потому, что
настоящий пакет успевал импортироваться из файла, который идёт раньше по
алфавиту: условие «если ещё не импортирован» оказывалось ложным. То есть
корректность HA-харнесса держалась на именах файлов в каталоге, и хватило бы
переименования, чтобы получить #389 заново.

Подмена переехала в conftest и стала условной по единственному честному
признаку: есть Home Assistant — работаем с настоящим пакетом и не подменяем
ничего; нет — HA-тесты и так пропущены, ломать нечего.

Первым заходом я делал подмену обратимой прямо в тестах, контекстным
менеджером. Замер показал, что так теряется работоспособность
test_wall_segment_model в песочнице: он импортирует пакет обычным способом и
жил как раз за счёт чужой пустышки. Развилка в conftest сохраняет оба
окружения и убирает зависимость от порядка файлов.

Проверено в обе стороны: в песочнице 240 passed, а в эмуляции «HA есть»
объект пакета после прогона тот же, что был до.

Issue: #394
User-Visible: no
2026-08-30 20:11:31 +03:00
Matysh a3dcee5241 ci: move the harness to the current Home Assistant
Замер по AC5 #392 изменил решение. Я собирался заморозить набор на том, что
резолвилось (HA 2026.2.3, февральский), и вынести обновление в отдельную
задачу «на потом» — потому что шесть месяцев дрейфа API вслепую в dev не
отправляют.

Проверил на ветке experiment/392-py314: Python 3.14, phcc 0.13.357,
homeassistant 2026.8.3 — `450 passed, 2 skipped`. Ровно то же, что на старом
наборе, ни одного падения. Обновление оказалось бесплатным, и держать гейт на
февральском HA после такого замера нельзя.

Взята 0.13.357, а не последняя: она последняя, которая пинует стабильный
2026.8.3, дальше пинуются беты. Гейт на бете невоспроизводим — бету могут
перевыпустить под тем же номером.

pytest не закреплён намеренно: phcc задаёт совместимый диапазон сам, жёсткий
пин с ним конфликтует (ResolutionImpossible на 9.0.0, проверено).

Issue: #392
User-Visible: no
2026-08-30 20:00:17 +03:00
Matysh eb5aa2a0f8 ci: pin the HA harness and stop tests from editing sys.path
Два независимых хвоста из разбора #389, оба про то, что «зелёный» значит не
то, что читается.

#392. Оба места, где поднимается HA-харнесс, ставили зависимости без единой
версии. Python при этом закреплён на 3.13, а pytest-homeassistant-custom-
component с 0.13.348 требует 3.14 — резолвер молча уезжал на 0.13.316, а та
тянет homeassistant 2026.2.3. Интеграция полгода проверялась против
февральского HA, и состав окружения мог измениться без нашего коммита.
Версии закреплены в tests_backend/requirements.txt ровно те, что резолвились
30.08; шаг печатает установленное в лог. Это остановка дрейфа, а не
обновление: переход на 3.14 и свежий phcc — отдельная задача с отдельным
измерением.

#393. test_trails.py клал каталог пакета в sys.path при коллекции — на всю
сессию, включая HA-харнесс. Модули интеграции становились импортируемыми ещё
и как модули верхнего уровня, то есть один файл мог оказаться в sys.modules
дважды. Вставка при этом не работала ни на что: TrailBook берётся чтением
текста и exec среза, а не импортом. Убрана вместе с мёртвым spec.

Гейт статический, по исходникам: он ловит намерение, а рантайм поймал бы
последствие и только при сегодняшнем порядке тестов.

Issue: #392
User-Visible: no
2026-08-30 19:49:37 +03:00
Matysh 32c70ecd19 fix: stop the schema dump from poisoning the HA harness
Красный backend на dev — это два независимых дефекта, и ни один не был виден
в диффе.

Первый, 85 падений. scripts/dump-config-schema.py подменяет
custom_components и custom_components.houseplan пустышками, чтобы прочитать
схему без Home Assistant, и оставляет их в sys.modules навсегда. Вызывает его
в том числе pytest: tests_backend/test_config_schema_manifest.py идёт первым
по алфавиту. Дальше HA просил у загрузчика custom_components.houseplan,
получал пустышку без async_setup и отказывался поднимать интеграцию — «No
setup or config entry setup function defined». Каждый тест харнесса падал на
_setup с «assert False», и ни один не намекал на причину: подмена работает
для подмодулей, потому что __path__ у пустышки настоящий.

Подмена не убрана — без неё скрипт не выполнит свою задачу. Она стала
обратимой: sys.modules снимается до и возвращается после, включая отсутствие
ключа. Обратимость закреплена тестом.

Второй, 1 падение. test_furniture_flip_flags_survive_coordinate_
canonicalization_unchanged требовал CONFIG_SCHEMA(result) == result, но схема
на минимальном конфиге достраивает markers и settings и приводит целые к
float — тождества там нет и не было. Проверяется теперь неподвижная точка:
повторная валидация не меняет канонический вид, а флаги её переживают.

Диагностика шла через CI: в песочнице HA-харнесс не поднять, поэтому
временная ветка experiment/389-diag печатала, что именно видит загрузчик.
Она показала модуль без __file__ и без единого атрибута — namespace-подобную
пустышку, — и это вывело на подмену.

Issue: #389
User-Visible: no
2026-08-30 18:31:42 +03:00
Codex f43187218f fix: the schema dump must not match the HACS *manifest.json glob (#33)
repo-hygiene caught it: HACS globs *manifest.json over the whole clone
and rejects a repository with two. The dump is scripts/config-schema.json.

User-Visible: no
Issue: #33
2026-08-30 12:45:17 +03:00
Codex 24681ee26f feat: schema manifest, parity allow-list and lifecycle fixtures (#33)
The Voluptuous schema is now dumped into a deterministic committed
manifest (265 leaf paths); a pytest fails on drift. A parity test
compares manifest enums with the exported frontend const lists through
a machine-readable allow-list that also refuses to rot. The field
registry gains passports (allow-extra / lovelace-card), enforcedBy
citations for mechanisms that already shipped, and passports for the
v1.68-v1.69 fields; a completeness test bans dead decisions. Lifecycle
fixtures (oldest / current / future) prove lossless loading, and the
config auditor gains the 0/3/2 exit-code contract.

User-Visible: yes
Issue: #33
2026-08-30 12:42:31 +03:00
Sergey Matyunin 686557c5cf feat: add smooth furniture transforms
Issue: #383
User-Visible: yes
2026-08-30 11:56:35 +03:00
Codexandclaude[bot] 223f499e37 fix: audit-lows batch from the v1.69.0 audit (#385)
(a) a same-binding click in the marker dialog is a no-op: the value
source and badge reset only on an actual change of binding (#378 §1.6) —
both the candidate list and the virtual radio.
(b) rewriteMarkerControlReferences no longer plants value_badge /
value_source keys as undefined on markers that never had them.
(v) the expensive release diff proof (2 git-show per src file) runs only
for commits the SAME shared predicate classifies as release — both
disjuncts, including the Release: trailer.
(g) the paired neutralisation formats in space export are documented in
place and pinned by a combined badge+value_source pytest.

User-Visible: yes
Issue: #385
2026-08-30 08:24:40 +00:00
Sergey Matyuninandclaude[bot] ea7803230a feat: add no-op marker tap action
Issue: #381
User-Visible: yes
2026-08-30 07:48:16 +00:00
Sergey Matyunin 591f8f6ac9 Add selectable value face sources
Issue: #378
User-Visible: yes
2026-08-29 22:20:18 +03:00
Codex 10a1a26612 feat: persist the Background editor's default style with the plan (#377)
settings.decor_default_style (all fields optional, validated) seeds
_decorStyle once from the first config that arrives; every UI change of
the session default flows through one runtime method with a 1s debounce
and the ordinary serialized expected_rev write path. The built-in default
is stored as the absence of the key; a partial or garbage key falls back
per-field. decorStyleFromSettings/decorStyleToSettings are the single
snake_case<->camelCase conversion point.

User-Visible: yes
Issue: #377
2026-08-29 21:39:08 +03:00
Codexandclaude[bot] 78c6020747 fix: lazy delivery survives flaky networks and stale caches (#353)
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
2026-08-28 14:32:08 +00:00
Matysh 24c9a169ac fix: require revision for saved layout replacements
Issue: #356
User-Visible: yes
2026-08-28 17:23:01 +03:00
Matyshandclaude[bot] 3e437ca3ec fix: reject config writes without a revision
Issue: #340
User-Visible: yes
2026-08-28 11:26:19 +00:00
Matyshandclaude[bot] ca16d1fe59 Fix vacuum trail lifecycle persistence
Issue: #335
User-Visible: yes
2026-08-28 11:01:40 +00:00
Codex 4ded9c0b7d test: the #248 roundtrip fixture is seeded, not first-written (#333 r1-H1)
The junction gate reads an empty previous as "a first write may not arrive
already broken", and the #248 storage-roundtrip fixture legitimately carries
a 6 cm wall — so the untouched test went red on this branch. The subject of
#248 is byte-exact storage of an optimize commit, not first-write semantics:
the fixture is now seeded as the stored document and optimize inherits its
violations per rule, exactly like a real repair flow. Every storage
assertion (intent, pending, final pair, canonical serialisation) is
unchanged.

Issue: #333
User-Visible: no
2026-08-28 08:55:39 +03:00
Codex 5a2dd333d2 fix: plan/optimize passes the junction gate; import stays free by design (#333)
The owner's decision (2026-08-28): optimize is one of the two commands a
client can use to write arbitrary geometry, so it validates its candidate
against the stored document exactly as config/set does — inheritance counted
per rule (repairing a legacy plan with violations still passes; #329 AC10
already proves an honest optimization adds none, so the gate is a no-op for
legitimate flows), while a crafted payload is refused with the stable
junction_limit_<rule> code the except list has been ready for since #329.
The call lives inside the existing executor function, and a successful
optimize refreshes rt.junction_baseline with the candidate's counts so the
next config/set inherits from the cache (#330 §4.2 symmetry).

Import and backup restore stay OUTSIDE the gate on purpose — #329 §3
promises a restore is never blocked. The module docstring stops promising
more than the code does, and spec #329 §5 records the perimeter and the
trade-off explicitly: a crafted import can persist violations, but they are
inherited, never legalised as new ones.

HA tests pin AC1 (crafted spike refused, stored config and rev
byte-unchanged), AC2 (echo-optimize of a stored plan that already carries a
violation passes) and AC3 (the follow-up config/set takes its baseline from
the cache — observed through a recording wrapper). The
junction-limit-optimize-unguarded mutant turns AC1 red through the
backend-test-guard convention.

Issue: #333
User-Visible: no
2026-08-28 08:55:39 +03:00
Matyshandclaude[bot] 2c65fcec03 perf: lazy-load editor runtime
Issue: #337
User-Visible: yes
2026-08-28 05:40:28 +00:00
Codex 508945c088 fix: a 0° pair is the shared-wall model, not a duplicate — field revert of #331 §2.2
Red dev caught it ninety minutes after the merge: smoke_plan_drawing_repairs
and smoke_resize_pointer_real_plan went red because the new "a 0° wedge is
always a duplicate" rule refused two ordinary edits — creating a room over
an existing partition ring (#308's legal overlay) and resizing a wall until
it lands on a neighbour's. The premise was wrong at the model level: a
shared wall of two adjacent rooms IS two co-located owner atoms on one line,
so every shared-wall node carries a legitimate 0° pair by construction.
Bisection pinned the exact cut: with only the 0° rule reverted, both smokes
are green again; keys, incidence, the iterative walk and fail-closed stay.

Spec revision 4 records the revert and returns "an exact duplicate wall is
invisible to П1" to the status of a KNOWN LIMITATION — an honest detector
needs owner identity, which is a separate decision for the owner to make.
The zero-wedge mutant is removed with its rule; the .5-tick parity unit now
observes quantisation through valence instead of the retired duplicate
visibility; changelogs drop the over-promise.

Issue: #331
User-Visible: yes
2026-08-28 05:20:45 +03:00
Codex 58f3acbbde fix: junction limits are honest at the boundaries (#331)
Six normative cuts, both mirrors symmetric (spec revision 3):

- §2.1 node keys quantise to 1e-7 with the repository's canonicalisation
  formula (sign·floor(|v|·1e7+0.5)/1e7, -0 normalised) — toFixed(6) keys
  split one node into two on floating debris and produced two false П4
  refusals on a legitimate resize (reproduced: -1e-8 vs 0). Node pairs
  within 2e-7 of each other (raw coordinates) are ONE node, and the
  node-to-wall incidence uses the same quantum.
- §2.2 a ~0° wedge IS a violation: two rays leaving a node the same way are
  a duplicated or overlaid wall (a butt joint yields 180°, never 0°) — the
  worst degenerate case was invisible while 0.5° was refused.
- §2.3/§2.4 the wall run is an iterative edge walk over the collinear
  component: no recursion (10 000 atoms answered, not RangeError), no
  silently dropped fork (the old .find lost every branch but the first),
  O(E) by construction, and collinearity is measured against the BASE
  segment's axis so an arc of 0.9°-per-atom pieces cannot pose as one wall.
- §2.5 an exception while judging the CANDIDATE refuses the write with the
  junction.limit_check_failed toast (fail-closed, as the #278 guard); the
  baseline branch stays fail-open by design and the smoke proves the
  asymmetry by breaking only the second call of the deterministic pair.
- §2.6 the python mirror narrows its except on the candidate side only:
  a genuine migration bug (TypeError) surfaces as an honest WS error, while
  a previous-side bug keeps the wide "no baseline" fallback — the two AC6
  cases pin the asymmetry so swapped sides turn a unit red.

Parity fixtures gain the new boundary classes (debris node, duplicate wall,
collinear fork); four new mutants pin the filter, the key precision, the
dropped branch and the fail-open hole.

Issue: #331
User-Visible: yes
2026-08-28 04:39:46 +03:00
Codex ddfca3a865 fix: close code-review 330-r1 — budgets from the slowest machine, the bench in Validate, AC1 through the execution thread (#330)
H2: the benchmark budgets were calibrated on the author's sandbox with a
1.14x margin — the review runner measured tsFullCandidateMs at 169-171 ms
against a 100 ms ceiling. Budgets now keep the spec's 2-3x allowance over
the SLOWEST observed machine, and the benchmark runs as a step of the
Validate perf job on every push (it needs no browser and no bundle), not
only inside the weekly mutation gate.

M1: the promised AC1 backend test exists now and does what AC1 means: it
patches validate_junction_limits with a thread-recording wrapper inside the
real HA harness — on the event loop that would be MainThread — and proves
the verdicts survived the move (a clean write is accepted, a write adding a
spike is refused with junction_limit_angle). Spec revision 4 rewrites AC1
around this invariant instead of a fragile millisecond assertion.

M2: §4.6 equivalence is now behavioural on both sides (three boundary
fixtures each: as-is counts equal through-migration counts, TS and python),
and the parity suite gained the §7 boundary fixtures (exact 15°, exact
20 cm, the thickness-step filler run, exact 5 cm).

H1 was already closed by 7513f93d (the review ran on the previous HEAD):
check-docs is green on this tree — the screenshots and their manifest come
from one capture run.

Issue: #330
User-Visible: no
2026-08-28 03:07:44 +03:00
Codex c90f5bf052 perf: junction limits scale — executor, rev cache, linear П3/П4, shared masonry pass (#330)
Six cuts, zero verdict changes (spec §3; equivalence pinned by units, the
parity suite and the smokes):

- §4.1 the CPU chain of ws_config_set and ws_plan_optimize runs in the
  executor; write_lock still serialises writes, only the HA event loop is
  freed (2.8 s of blocking per 576-atom write before).
- §4.2 the stored document's violation counts are cached on the runtime by
  rev (store.py junction_baseline); a repeated write never re-judges
  `previous`. validate_junction_limits takes baseline_counts and returns the
  candidate's counts to cache after a successful save.
- §4.3 П3 builds its node index once per check in both mirrors
  (289→11 ms TS, 285→~50 ms py).
- §4.5 П4 uses a bucket grid with the threshold as cell size in both
  mirrors (104→19 ms TS, 372→44 ms py); pair enumeration switches to
  lexicographic order — same verdict set, equivalence pinned against a
  brute-force oracle on cell borders.
- §4.6 a document already carrying the current catalogue is judged as-is:
  a no-op re-migration cost 815 ms py / 69 ms TS. Legacy documents migrate
  exactly as before (the #329 H1 test stays green).
- §4.7 П5 shares one junction-topology pass per check and pays the masonry
  union only when multi-wall nodes exist — and the resize path hands over
  the preflight's own artifact, so a pointermove never builds the union
  twice (4.2 s → 88 ms full candidate on the benchmark grid).

The frontend baseline is cached per (document identity, config epoch): ten
pointermoves make N+1 limit computations, not 2N — pinned by the smoke on a
real pointer gesture.

demo/benchmark_junction_limits.mjs (npm run benchmark:junction-limits) pins
the budgets for both mirrors: TS full candidate ≤100 ms (measured 88), py
warm validate ≤250 ms (measured 45), cold legacy ≤3.5 s — that path is
one-off and lives in the executor.

Issue: #330
User-Visible: yes
2026-08-28 03:07:11 +03:00
Codex 0bb42caeff fix: the backend judges both sides after the same migration (#329 H1)
The limits read `wall_segments`, so a document older than the catalogue
reports no walls at all — and therefore no violations, whatever its geometry.
Comparing that raw baseline against a candidate the card had already migrated
counted every inherited violation as new, and a legacy plan could not take an
unrelated edit at all: renaming a room was refused with junction_limit_angle.
Spec §3 forbids exactly this, and the frontend had already learned the same
lesson in 4758767e; the backend mirror simply never got the second half.

validate_junction_limits now runs both documents through
commit_wall_segment_model before counting. A document that cannot be migrated
is not this validator's verdict — the wall-model barrier owns that error and
reports it with its own code — so it degrades to "no baseline to inherit".

The regression is pinned twice: a test that asserts the legacy baseline reads
clean raw and carries the apex once migrated, and the mutant
junction-limit-backend-raw-baseline. Both fixtures that exercise the barrier
were rebuilt as real documents (rooms plus walls), because the previous ones
put walls in wall_segments with no rooms and did not survive migration.

Issue: #329
User-Visible: no
2026-08-28 00:23:54 +03:00
Codex 8945e04fe4 feat: backend mirror of the junction limits (#329 §5, AC9)
custom_components/houseplan/junction_limits.py repeats П1-П4 for the write
barrier in websocket_api, counting per rule so an inherited violation still
round-trips, and raises JunctionLimitError with the stable code
junction_limit_<rule>.

П5 is deliberately not mirrored — it judges the rendered wall bodies, and a
second mitre/inset pipeline in Python would drift more dangerously than the
rule it guards. Optimize stays outside the check for the same reason migration
and import do: it repairs existing geometry.

test_parity_with_the_frontend_checks feeds identical fixtures to the TS
functions and to this module and demands the same verdict, so the two
implementations cannot silently diverge.

Issue: #329
User-Visible: no
2026-08-27 23:32:07 +03:00
Matysh 69e410fea3 fix: materialize empty wall catalog for new spaces
Issue: #324
User-Visible: yes
2026-08-27 16:33:47 +03:00
Codex 0f36ef55d5 fix: an opening without an in-place carrier migrates unhosted (#316, review r1 H1 + r2 M2)
CODE-REVIEW-316-r1 H1: the §3.3 degraded pool picked an angle-compatible wall
at ANY distance, but the backend geometry-match invariant («wall opening
geometry must match its host») requires the host to agree with the opening's
own x/y — the migrated document was rejected by CONFIG_SCHEMA and the write
wedged again on the schema layer. The pool is removed from both migrations
(TS and the Python mirror): without an in-place eligible carrier the opening
goes straight to the unhosted degraded state, exactly the alternative the
spec's «assumed freely changeable» section reserved; the spec is revision 6.
New tests replay the reviewer's reproduction on both sides, and the frontend
test is proven able to fail by restoring the pool (executed red).

CODE-REVIEW-316-r2 M2: the schema-level host check is shared with #132
partition openings, so its unhosted relaxation is now pinned by a regression
test — a stale writer that keeps a partition-hosted opening but silently
drops its host is still rejected by validate_partition_opening_hosts.

Issue: #316
User-Visible: no
2026-08-26 23:41:35 +03:00
Codex ffb232398a fix: the initial wall-model migration resolves every opening conflict itself (#316)
Implements spec revision 4 (green r4). §3.1 — a legacy open_spans/open_to cut
never zeroes the atom that carries an existing contour opening: the opening's
edges become atom boundaries, the door keeps its real wall and the zero run
continues on both sides. §3.2 — an ambiguous carrier resolves
deterministically: current host, then distance, thicker cm, smaller id.
§3.3 — an opening with no usable carrier persists unhosted: a valid degraded
v9 state, inert in the physics, rendered by its own x/y, kept by later writes
and re-placeable in the editor (backend schema accepts it). §3.4 — the
initial migration never throws over an opening; a post-v9 write that LOST its
carrier keeps the fail-closed opening-host refusal. The Python migration
mirror implements the same rules with byte-identical output (verified on the
span+door fixture including the segment id).

The new smoke replays #316 end to end: a conflicted space no longer blocks
drawing on an empty plan. The golden scene span-over-door-migrated-dark
renders the migrated fixture pinned byte-for-byte to the real writer; two
gate mutants revert §3.1 and §3.4 and are red by execution.

Issue: #316
User-Visible: yes
2026-08-26 22:06:43 +03:00
Codex 3ab6fa9f8a fix: the first write of a newer wall model is not an outdated client (#319)
A stale client can only echo the stored model_version, never raise it. The
'unchanged wall catalogue' refusal now applies only when the submitted model
is not above the stored one; the first v9 write over a v8 document with an
orphan open_span/open_to legitimately drops the legacy projection without
touching the catalogue and passes. The regression pair fixture is produced
by the real writers (stored: v1.68.0-beta.2, sent: current migration) and is
pinned on the frontend byte-for-byte so it cannot drift.

Issue: #319
User-Visible: yes
2026-08-26 18:20:15 +03:00
Matysh a66fb7ef53 feat: unify zero-thickness walls
Issue: #306
User-Visible: yes
2026-08-26 13:39:52 +03:00
Codex 1cdd4ed6de fix: a refused geometry preflight names its reason and hands over diagnostics (#295)
Диалог «Оптимизировать» при отказе перечисляет причину по каждому
пространству (7 значений OptimizeGeometryFailureReason получили RU/EN
строки), даёт «Скопировать диагностику» — JSON-блок с origin: runtime,
версией карточки, отпечатками и классами исключений (граница приватности
checkOptimizeGeometry; privacy-тесты дополнены позитивной проверкой) — и
пишет одну структурированную запись в dev-лог (дедупликация по fingerprint).
При недоступном clipboard блок раскрывается прямо в диалоге.

Совет «обновите House Plan» больше не безусловный: websocket
houseplan/config/get теперь возвращает integration_version (бэкенд-тест),
и подсказка показывается только при реальном расхождении с версией карточки;
старый бэкенд без поля — подсказки нет.

Три новых мутанта (потеря причины в диалоге, блок без reason, отключённый
dev-лог) — краснота каждого проверена исполнением; смок
smoke_preflight_diagnostics на dev падает.

Issue: #295
User-Visible: yes
2026-08-26 10:28:29 +03:00
Matysh 2347e8df88 Fix atomic model-v8 draft writes
Issue: #314
User-Visible: yes
2026-08-26 09:13:50 +03:00
Matysh 71ac4a6639 fix: enforce wall model barrier in backend optimize
Issue: #282
User-Visible: yes
2026-08-26 02:20:57 +03:00
Matysh fc9f513082 test: align import fixtures with wall model v8
Issue: #282
User-Visible: no
2026-08-26 02:20:57 +03:00
Matysh e1059e2e29 fix: preserve wall identity through structural edits
Issue: #282
User-Visible: yes
2026-08-26 02:20:57 +03:00
Matysh b336eee996 feat: stabilize persisted wall segment identity
Issue: #282
User-Visible: yes
2026-08-26 02:20:57 +03:00
Matysh 2143e88824 test: close import seam review gaps
Validate / reuse (push) Successful in 1m13s
Validate / hacs (push) Failing after 24s
Validate / hassfest (push) Failing after 33s
Validate / frontend (push) Successful in 6m16s
Validate / backend (push) Failing after 7m3s
Validate / smoke (3) (push) Failing after 18m38s
Validate / smoke (2) (push) Failing after 19m10s
Validate / golden (push) Failing after 23m1s
Validate / smoke (1) (push) Failing after 45m54s
Validate / smoke_done (push) Skipped
Validate / performance_smoke (push) Failing after 50m53s
Validate / process-workflow-sync (push) Successful in 34s
Validate / docs (push) Successful in 51s
Validate / provenance (push) Successful in 1m16s
Validate / process-gate (push) Failing after 1m16s
Validate / changes (push) Successful in 46s
Issue: #265
User-Visible: no
2026-08-25 15:16:40 +03:00
Matysh 51810164c2 refactor: unify import reference seam
Issue: #265
User-Visible: yes
2026-08-25 14:47:04 +03:00
Sergey Matyunin b9db5d4750 fix: close #296 review gaps
Issue: #296
User-Visible: yes
2026-08-24 22:50:00 +03:00
Sergey Matyuninandclaude[bot] 89ac96a8af fix: reconcile hidden walls during Optimize
Issue: #296
User-Visible: yes
2026-08-24 19:10:27 +00:00
Sergey Matyunin 4ccf4a3ccc fix: enforce exact lattice coordinates
Issue: #291
User-Visible: yes
2026-08-24 17:13:21 +03:00
Sergey Matyunin 07bd1a6781 fix: reconcile outer walls before resize
Issue: #281
User-Visible: yes
2026-08-24 10:43:33 +03:00
Sergey Matyunin 38ea8a9b9e fix: validate optimize opening rehosts
Issue: #280
User-Visible: yes
2026-08-24 10:33:05 +03:00