Commit Graph
882 Commits
Author SHA1 Message Date
Matysh 9d2ef12c96 feat: smooth vacuum trails (#209)
Issue: #209
User-Visible: yes
2026-08-28 23:16:58 +03:00
Matysh f62926fbeb feat: make Glow follow door opening state (#20)
Issue: #20
User-Visible: yes
2026-08-28 22:33:07 +03:00
Codex bb3c1fd12f fix: a cold View survives a telemetry-bearing vacuum (#358)
Systematic audit after #357 ("can there be more bugs with this root
cause?"): _vacMapId was the one remaining hard stub reachable from the
eager View path. It runs inside willUpdate for every vacuum whose
integration reports live telemetry (Tasshack, XCME, Valetudo), so on a
cold tab the #337 stub threw there and the exception took the whole Lit
update cycle with it — the card froze on its very first frame. The demo
mower has no position attributes, telemetry resolved to null, and every
existing smoke (warm and cold) sailed past the branch.

The card now owns the implementation (both dependencies — _vacEntity and
vacMapIdWithFallback — were already eager); the editor runtime delegates
back to the host. The HP-1541-01 invariant (selected_map: 0 is a real map
id, nullish not truthy) moves verbatim and is pinned by the new smoke.

Hardened alongside (audit Lows): _decorShapeDown gets the same
cold-tab guard its twin _decorShapeDbl received in #337 — decor shapes
render in View and CSS pointer-events alone must not be what prevents a
throw; the _vacCalConfirm dialog renders behind the same _editorRuntime
gate as every other editor dialog instead of relying on the implicit
"only the runtime ever sets it".

smoke_cold_view_vacuum: cold tab, vacuum with vacuum_position and
selected_map: 0 — the card commits three successive telemetry frames
(willUpdate alive, not merely the first paint), map id resolves to '0',
no editor chunk requested, a decor pointerdown is a quiet no-op. A
registry mutant restores the delegation and is killed by that smoke.

Issue: #358
User-Visible: yes
2026-08-28 21:49:50 +03:00
Codex 49174e81f7 fix: a cold View resolves taps without the lazy editor runtime (#357)
Field report from the dacha: the wall switch "Гостиная основной свет",
whose controls name three virtual light sources, periodically ignored taps
— no toggle, no glow — until its settings dialog was opened once with no
changes. "Periodically" was every fresh tab: the #337 lazy split left
_toggleIntent (and the confirm-line helpers) on the card as stubs
delegating into the editor runtime, so a plain View tap on a cold tab
threw `Houseplan editor runtime is not loaded` synchronously inside the
click handler. Opening any editor surface loaded the runtime and "healed"
the tab for its lifetime.

The View card now owns toggle resolution: _toggleIntent calls
resolveToggleIntent directly (device-toggle.ts was already in the initial
graph; the card owns _planHass/_fullRegistryHass/_virtualLights), and
_toggleStateText/_toggleConfirmationStateText/_toggleConfirmationLines
moved with it. The editor runtime delegates back to the host — one source
of truth, editor consumers (dialog preview, hint lines) unchanged.

Every product smoke preloads the runtime, so none of them could see this
class of regression. The new smoke_cold_view_toggle mirrors the field
config on a genuinely cold tab: a real switch drives three passive
virtual lamps with one tap, a controlled lamp drives its switch back,
tap_confirm renders its state lines and confirms, and the editor chunk is
never requested. A registry mutant restores the old delegation and is
killed by that smoke.

Issue: #357
User-Visible: yes
2026-08-28 21:02:19 +03:00
Matysh 399df907ee build: prepare v1.69.0-beta.2 candidate
Проверка (CI) / Классификация изменённых файлов (push) Successful in 15s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 45s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 25s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 27s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 44s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 4m29s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 5m39s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Package the expanded furniture library, room-aware climate placement, controller-state parity, resilient editor delivery, revision-safe writes and the reviewed supporting fixes since beta.1.

Issue: #159
Issue: #317
Issue: #318
Issue: #335
Issue: #339
Issue: #340
Issue: #341
Issue: #353
Issue: #354
Issue: #356
User-Visible: yes
2026-08-28 20:37:13 +03:00
Matysh 94ecf260b5 fix: type furniture translation keys
Issue: #159
User-Visible: no
2026-08-28 18:52:31 +03:00
Matysh 7909ab6eed feat: expand furniture library
Issue: #159
User-Visible: yes
2026-08-28 18:52:03 +03:00
Codex c1356fae18 test: prove the failure delivery itself, not only the unsubscription (#354 r1-M1)
The r1 reviewer cut the listener loop in the production registry and all
three #354 units stayed green — the subscription unit was the same class of
decoy the issue itself fights. The fan-out now lives in an exported
notifyLanguageLoadFailures(code); the unit drives it directly and asserts
real delivery, partial unsubscription and silence after the last listener
leaves; the contract unit additionally pins the runtime wiring
(`loadFailed` → notifyLanguageLoadFailures) in source. A new registry
mutant `locale-failure-delivery-cut` replays the reviewer's exact cut and
is killed by the unit. The r1 Low is taken too: both USER-GUIDEs now
mention the toast in the German-failure paragraph.

Issue: #354
User-Visible: no
2026-08-28 18:08:27 +03:00
Codex 1397afefd5 fix: the locale runtime the card ships is the code the tests prove (#354)
The production LANGUAGE_RUNTIME was a handwritten twin of the tested
LanguageRuntime class (germanDictionary/Pending/Failed): equivalent on the
day it was written, invisible to every i18n-runtime test afterwards. The
registry now exports one page-scoped `new LanguageRuntime(LANGUAGE_REGISTRY,
…)` instance — the whole existing suite starts proving the object production
actually runs, and a contract unit (instanceof + source free of the old
field names) keeps the duplicate from returning.

The class gains an optional `loadFailed(code)` hook — fired once when a
dictionary load settles into English fallback — and the registry fans it out
through `subscribeLanguageLoadFailures`. Only the View card subscribes (it
alone owns toast infrastructure): a failed language pack now shows the new
`toast.locale_load_failed` message (en/ru/de) instead of a console-only
warning; space card and both GUI editors keep the console warning as before.

Proofs: contract unit, hook unit, subscription unit; smoke_german_locale
extended — the both-attempts-failed scenario now asserts the visible toast;
two new registry mutants (handwritten-twin returns, toast dropped).

Issue: #354
User-Visible: yes
2026-08-28 17:49:59 +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
Matysh 6c9752f5e6 fix: follow marker placement for room climate
Issue: #317
User-Visible: yes
2026-08-28 16:48:10 +03:00
Matyshandclaude[bot] 72913fee84 fix: show entityless active controllers
Issue: #318
User-Visible: yes
2026-08-28 13:07:58 +00:00
Matysh 5f6bddeaf3 fix: deduplicate Labs location listeners
Issue: #341
User-Visible: no
2026-08-28 14:40:06 +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
Codexandclaude[bot] ac077d9b6d fix: a mixed-thickness legacy apex converges honestly (#339)
isDegenerateApexCorner measured the inner-face convergence as
max(h1,h2)/tan(theta/2) — for a 10-degree apex between a 15 cm and a 30 cm
wall that overstates the distance (171.5 cm against the true 128.3/128.9 on
160 cm edges), the corner failed the "inside both edges" test and rendered
as the #329 trident again. Worse, the verdict depended on which neighbouring
edge carried the thicker wall.

The check now intersects the two actual face lines: the meeting point lands
at (hOther + hOwn*cos(theta))/sin(theta) along each edge, degenerate only
when inside both. With equal halves this reduces algebraically to the old
h/tan(theta/2), so equal-thickness verdicts are unchanged by construction —
pinned by the untouched section-4 units and the full golden matrix (136
scenes verified). New units cover both traversal orders of the mixed apex,
the one-point outset tip, the 30-degree ordinary pair and the zero-thickness
guard.

The write path is untouched: P1 forbids new sub-15-degree corners since
issue 329, this is purely how a legacy document renders.

Issue: #339
User-Visible: yes
2026-08-28 10:46:27 +00:00
Matysh 801e49092f fix: keep lazy editor release version synchronized
Issue: #337
User-Visible: yes
2026-08-28 12:47:30 +03:00
Matysh 58ebf8b4cd build: prepare v1.69.0-beta.1 candidate
Проверка (CI) / Классификация изменённых файлов (push) Successful in 18s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 40s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 51s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 15s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 22s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 11m19s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 11m46s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Package the complete German interface, the unified Device lifecycle catalog,
wall-junction correctness and performance work, and the supporting delivery
infrastructure reviewed since v1.68.1.

Issue: #29
Issue: #62
Issue: #264
Issue: #267
Issue: #329
Issue: #330
Issue: #331
Issue: #333
Issue: #337
Issue: #348
User-Visible: yes
2026-08-28 12:24:15 +03:00
Matysh 5740324bed fix: keep locale gate within view budget
Track locale-owned inert and busy state together, preserving the same render contract while keeping the deterministic initial View graph below its hard gzip budget. Refresh generated assets and the documentation fingerprint after the source cleanup.

Issue: #348
User-Visible: no
2026-08-28 11:58:46 +03:00
Matysh 12cd8a1af3 feat: add complete German localization
Add Deutsch across all card surfaces and backend flows, backed by the language registry introduced in #62. German loads as a fingerprint-checked page-shared locale chunk so EN/RU remain synchronous and the initial View budget stays intact. Root render gates prevent mixed-language flashes, retry once, and fail open to English. Extend parity, runtime, bundle, browser and visual coverage, plus contributor and user documentation.

Issue: #348
User-Visible: yes
2026-08-28 11:58:46 +03:00
Sergey Matyunin c1b37f0dc5 build: restore the content-hashed asset chunks
Issue: #345
User-Visible: no
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33151614951
2026-08-28 10:39:20 +03:00
ClaudeandSergey Matyunin 1ea460e467 ci: перепаковка кадров документации без потерь при съёмке
oxipng снимает с набора 19.4%: 2096 КБ становятся 1689 КБ, и все десять кадров
остаются пиксельно идентичными — декодированные RGBA совпадают по sha256. Это
выбор фильтров строки и уровня сжатия, а не квантование: визуального решения нет.

Внутри съёмки, а не отдельным проходом по закоммиченным файлам: манифест хранит
imageSha256 каждого кадра, поэтому жать их в репозитории руками нельзя —
check-docs покраснеет; а если жать после подсчёта хешей, следующая съёмка вернёт
неоптимизированные байты. Хеш считается после перепаковки.

Версия oxipng попадает в манифест рядом с версией браузера и по той же причине:
байты кадра зависят от того, чем жали. Отсюда же правка шага «Вердикт» — иначе он
объявил бы «тот же браузер, а картинки изменились — изменился продукт», хотя
изменился упаковщик.

Пин версии и контрольной суммы вместо apt-get: пакет из образа раннера может
пропасть, а падение шага съёмки стоит целого цикла приёмки (#175, #206).

Проверено исполнением на прежней базе: съёмка прогнана целиком с подставным
oxipng, 2096 -> 1689 КБ, хеши манифеста совпали с файлами, check-docs зелёный.

Issue: #345
User-Visible: no
2026-08-28 10:28:13 +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] 81420a0d94 fix: preserve warm editor camera during lazy adoption
Issue: #337
User-Visible: no
2026-08-28 05:40:28 +00:00
Matyshandclaude[bot] 9b519b0d4c fix: preserve editor behavior across lazy runtime
Issue: #337
User-Visible: no
2026-08-28 05:40:28 +00:00
Matyshandclaude[bot] e3f5c49502 fix: keep lazy editor build portable
Issue: #337
User-Visible: no
2026-08-28 05:40:28 +00: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 04bb54aef3 fix: the baseline cache keys on geometry, and the smoke tells all three worlds apart (#330 r2-M1)
The reviewer proved my behavioural claim false by running the stale-cache
mutant against the smoke: 11 vs 12 total calls — indistinguishable. Two real
defects hid behind that finding:

1. The cache keyed on _cfgEpoch, which ticks on every ACCEPTED PREVIEW —
   the cache missed on every pointermove and the baseline was recomputed
   ~4 times per gesture (measured). The key is now the document identity
   plus spacePhysicalGeometryFingerprint of its space: content, not a
   counter. A preview overlay leaves the fingerprint alone; an in-place
   structural commit changes it and honestly invalidates.

2. The smoke now counts BASELINE computations only (calls whose document is
   _serverCfg) across two gestures with a commit in between, expecting
   exactly 1 then exactly 2. A disabled cache lands near 20, an eternal
   cache stays at 1 — every mutant class turns the smoke red, and the smoke
   is now the mutant's guard alongside the source-contract unit.

Issue: #330
User-Visible: no
2026-08-28 03:35:40 +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
Matysh c755a0efc4 fix: normalize i18n registry lookup keys
Issue: #62
User-Visible: no
2026-08-28 02:55:22 +03:00
Matysh 6540474ff5 refactor: centralize i18n language registry
Issue: #62
User-Visible: no
2026-08-28 02:44:26 +03:00
Matyshandclaude[bot] aa9444491d fix: align device catalog with plan rooms
Issue: #29
User-Visible: yes
2026-08-27 23:17:53 +00:00
Matyshandclaude[bot] cab8d128bf feat: add device lifecycle catalog
Issue: #29
User-Visible: yes
2026-08-27 23:17:52 +00:00
Codex 8f0b6b97e8 refactor: drop the chamfer helpers the honest apex made dead (#329 M1)
002795f7 said "the clip helper and its cap plumbing are gone" while leaving
clipPolygonOutsideCap(), degenerateApexCaps() and the apexCaps ring field in
place — exported, uncalled and untested. They belong to the flat chamfer the
owner rejected; the apex now ends in one point on both faces, so the quads
have no caller and no meaning.

The orphaned JSDoc block that described degenerateApexCaps went with them, and
the wallBodiesGeometry documentation this change had earlier separated from
its function is reattached: the #329 constant and predicate now sit above it.

Golden verify stays green on the whole matrix, which is the evidence the
removed code was indeed dead.

Issue: #329
User-Visible: no
2026-08-28 00:24:34 +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
Codex 7a74a37c6e test: golden scene for the legacy sharp apex (#329 AC6)
The owner's spike room (≈9.9°, 15 cm walls) is extracted into
test/fixtures/329-sharp-apex.json and rendered on its own so a returning
trident, a flat chamfer or a jagged edge fails the pixel gate. Baseline
follows from the CI candidate, as the process requires.

Issue: #329
User-Visible: no
2026-08-27 23:32:07 +03:00
Codex f514fb27fe feat: junction limits refuse the write in every editing surface (#329)
П3 measures the WALL, not the catalogue atom: a short filler segment that
compensates a thickness step (owner's fixture, 5 cm = (30-20)/2) is a legal
continuation of a long same-thickness wall, so the rule walks the maximal
collinear run through the shared nodes before judging the length.

Resize stops at the last allowed position and names the broken rule instead
of the generic "geometry cannot be saved"; the Thickness dialog refuses
through its own toast. Both channels are pinned by demo/smoke_junction_limits
plus three mutants (angle threshold, write barrier, degenerate apex bevel).

Issue: #329
User-Visible: yes
2026-08-27 23:31:42 +03:00
Codex 002795f7c8 fix: no jags on the edges of a degenerate apex (#329 §4)
Owner report: small serrations remained on the outer edges between the inner
and the outer vertex. Measured on the fixture ring: two ~4 cm steps plus four
micro-vertices at the tip. Their source was the inset contour's two-point
bevel folding into a bow-tie, and the earlier half-plane clip of that fold,
which left a 0.2 cm sliver the boolean union turned into steps. The inset now
ends in ITS own mitre point at a degenerate apex — mirroring the sharp outer
tip — so there is no fold to clip and no sliver to smear: the room ring is
exactly three vertices, every side longer than the half depth. The clip
helper and its cap plumbing are gone. The user-visible wording of this work
already stands in both changelogs from the #329 entry.

Issue: #329
User-Visible: no
2026-08-27 23:31:17 +03:00
Codex 4758767e0c fix: junction limits judge both sides after the same migration (#329)
The baseline for inheritance was the raw previous document, which for a
legacy space carries no wall catalogue at all — so every inherited short
segment of a real plan looked new and the resize smoke's legitimate write was
refused (executed: two 5 cm segments against their own 30 cm thickness).
Both sides now cross commitWallSegmentModel first, and inheritance is counted
per rule rather than per subject, because a structural write re-keys the
carriers it re-atomises.

Issue: #329
User-Visible: no
2026-08-27 23:30:52 +03:00
Codex 214355f424 fix: a degenerate sharp corner renders as a normal sharp apex (#329 §4)
Owner correction (chat, 2026-08-27): no flat chamfer at the tip — a plain
sharp apex. Proven by execution on the issue fixture: the outset contour fell
back to a two-point bevel (the 4·h mitre limit against an 87 cm reach) while
the inset contour folded into a bow-tie, and subtracting that fold carved the
V-notches — together they made the trident. Now a degenerate corner (below 15
degrees, inner faces meeting inside both walls) contributes ONE outset point
at the plan's own vertex — no bevel, no metres-long mitre needle — and its
inset is clipped at the convergence line so no fold is subtracted. Plain and
merely sharp pairs keep the full mitre of #310. The write-side limits of
П1-П5 stop new plans from creating such corners at all.

Issue: #329
User-Visible: yes
2026-08-27 23:30:26 +03:00
Matysh 118062bb8c test: close device presentation review findings
Issue: #267
User-Visible: no
2026-08-27 22:41:46 +03:00
Matysh 6efc315ba6 refactor: make device presentation decisions explicit
Issue: #267
User-Visible: no
2026-08-27 22:06:10 +03:00
Matysh ba66698f55 refactor: extract resize controller
Issue: #264
User-Visible: no
2026-08-27 20:30:07 +03:00
Codex 78850a8733 build: rebundle after the package.json script addition
Проверка (CI) / Провенанс коммитов: трейлеры и эталоны (push) Failing after 8m3s
Проверка (CI) / Процессный гейт: диапазон, трейлеры, статусы issue (push) Failing after 8m2s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 52s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m30s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 35s
Проверка (CI) / Процесс: process.yml идентичен в main и dev (push) Successful in 7m32s
Проверка (CI) / Документация: гайды, ченджлоги, скриншот-индекс (push) Successful in 7m53s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 23s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Successful in 13m14s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 13m21s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Failing after 26m7s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Failing after 31m4s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Failing after 34m5s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Failing after 12m59s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Failing after 45m46s
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
The bundle embeds the source fingerprint, which covers package.json; the
release:notes script addition moved it, so the three bundle copies must be
rebuilt in the same change.

Issue: #328
User-Visible: no
2026-08-27 18:59:10 +03:00
Matysh ca383febef chore: release v1.68.1
Owner-approved emergency stable hotfix; the release commit changes only version fields, generated bundles and release metadata.

Issue: #324
User-Visible: yes
2026-08-27 17:17:12 +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