Commit Graph
101 Commits
Author SHA1 Message Date
Matysh dc3c083352 test: one number, one source — as a checkable rule
Issue: #254
User-Visible: no
2026-08-23 09:49:22 +03:00
Matysh 5cf52707c4 test: check model invariants for references and wall records
Issue: #254
User-Visible: no
2026-08-23 09:34:32 +03:00
Sergey Matyunin 2cb7c73cad fix: separate controller and target availability
Issue: #251
User-Visible: yes
2026-08-23 08:08:17 +03:00
Matysh 0a1465d20c test(smoke): cover wall-face overlap, duplicate and nesting
Validate / docs (push) Failing after 22s
Validate / reuse (push) Successful in 37s
Validate / changes (push) Successful in 1m20s
Validate / provenance (push) Successful in 1m26s
Validate / process-gate (push) Failing after 1m25s
Validate / hacs (push) Failing after 14s
Validate / hassfest (push) Failing after 17s
Validate / frontend (push) Successful in 6m28s
Validate / backend (push) Failing after 6m39s
Validate / golden (push) Failing after 15m20s
Validate / performance_smoke (push) Failing after 18m21s
Validate / smoke (push) Failing after 36m34s
Issue: #177
User-Visible: no
2026-08-23 05:11:01 +03:00
Sergey Matyunin ed7ea29dc9 test: restore gate flip mutation coverage
Issue: #250
User-Visible: no
2026-08-23 02:16:32 +03:00
Sergey Matyunin 0eb9ffe480 fix: keep flipped openings on wall centerline
Issue: #250
User-Visible: yes
2026-08-23 01:59:26 +03:00
Sergey Matyunin cbdd5a7e73 fix: keep Optimize idempotent across storage reload
Issue: #248
User-Visible: yes
2026-08-23 01:13:27 +03:00
Sergey Matyunin f6f877e393 fix(spaces): repair orphaned plan references
Issue: #244
User-Visible: yes
2026-08-23 00:34:55 +03:00
Matysh 0c5683d3bf test: read the fixture basename on either separator
Issue: #247
User-Visible: no
2026-08-23 00:19:54 +03:00
Sergey Matyunin a65db3985d fix(openings): preserve gate flip turn direction
Address code review H1 by keeping gate flip direction observable without a second vertical mirror. Add fail-closed golden contracts, smoke coverage, and a mutation guard for the affected opening-symbol geometry.

Issue: #242
User-Visible: yes
2026-08-22 21:14:37 +03:00
Sergey Matyunin 185eef7af2 test(openings): link centered symbol smokes
Issue: #242
User-Visible: no
2026-08-22 20:25:57 +03:00
Sergey Matyunin 6d6db12c79 fix(openings): center symbols across wall depth
Issue: #242
User-Visible: yes
2026-08-22 20:23:20 +03:00
Sergey Matyunin e6ac0337d0 fix(tabs): restore captured mouse reordering
Issue: #243
User-Visible: yes
2026-08-22 19:42:15 +03:00
Matysh e50c01245a ci: capture documentation screenshots in one place
Issue: #246
User-Visible: no
2026-08-22 19:12:27 +03:00
Matysh 4799c6be0c fix: keep the docs fingerprint blind to the product version
Issue: #245
User-Visible: no
2026-08-22 19:02:11 +03:00
Sergey Matyuninandclaude[bot] 09ab9266c0 fix: render decor above room fills
Issue: #231
User-Visible: yes
2026-08-22 15:58:00 +00:00
Matysh c0a4fa29ee ci: derive the browser-smoke shortlist from the diff
Issue: #241
User-Visible: no
2026-08-22 18:32:52 +03:00
Sergey Matyunin 7105e645d9 fix: sync live wall thickness preview
Issue: #234
User-Visible: yes
2026-08-22 16:49:29 +03:00
Sergey Matyunin 482afb73eb feat: block unsafe Optimize geometry
Issue: #199
User-Visible: yes
2026-08-22 16:29:33 +03:00
Sergey Matyunin 4dbdb446f8 fix: address coordinate review findings
Issue: #224
User-Visible: no
2026-08-22 15:32:55 +03:00
Sergey Matyunin 4a798e3e13 fix: canonicalize persisted geometry
Issue: #224
User-Visible: yes
2026-08-22 14:47:38 +03:00
Sergey Matyunin 0d91c1e18e fix: make plan visuals grid-scale invariant
Issue: #239
User-Visible: yes
2026-08-22 13:45:26 +03:00
Sergey Matyunin cb19b7d52e feat: measure opening preview to inner faces
Issue: #238
User-Visible: yes
2026-08-22 12:24:49 +03:00
Matysh 3aba493b25 fix: compile test-build inside the mutant worktree
Issue: #235
User-Visible: no
2026-08-22 11:24:37 +03:00
Matysh 81f38689bb test: prove the atomic profile on a genuinely split edge (#233 r1 M1,M2)
Issue: #233
User-Visible: no
2026-08-22 10:13:50 +03:00
Matysh abfaae3e38 feat: measure resize labels between wall faces
Issue: #233
User-Visible: yes
2026-08-22 09:45:15 +03:00
Matysh 61c874953a fix: verify demo bundle freshness for smokes too
Golden runs, benchmarks and documentation captures each called
assertFreshDemoBundle; the smoke launcher never did, so all ~128 smokes could
silently test a stale demo/srv/assets bundle. On #234 that cost a round of
analysis: three assertions went red and a fourth went green, because the old
code was wrong in two places that agreed with each other, and a mixed result
reads as a logic defect rather than a stale artefact.

launch() now runs the check once for every smoke, against the repository root
rather than the serving root — demo/srv has no src/** to fingerprint.
HP_ALLOW_STALE_BUNDLE=1 skips it for debugging and warns out loud, because a
guard that says nothing when it steps aside is the silent success this project
keeps removing. A mutation entry proves the call cannot quietly disappear.

Issue: #236
User-Visible: no
2026-08-22 02:03:36 +03:00
Matysh 09b5d394a5 fix: verify demo bundle freshness for smokes too
Golden runs, benchmarks and documentation captures each called
assertFreshDemoBundle; the smoke launcher never did, so all ~128 smokes could
silently test a stale demo/srv/assets bundle. On #234 that cost a round of
analysis: three assertions went red and a fourth went green, because the old
code was wrong in two places that agreed with each other, and a mixed result
reads as a logic defect rather than a stale artefact.

launch() now runs the check once for every smoke, against the repository root
rather than the serving root — demo/srv has no src/** to fingerprint.
HP_ALLOW_STALE_BUNDLE=1 skips it for debugging and warns out loud, because a
guard that says nothing when it steps aside is the silent success this project
keeps removing. A mutation entry proves the call cannot quietly disappear.

Issue: #236
User-Visible: no
2026-08-22 01:53:24 +03:00
Sergey Matyunin c8e9597228 fix: resolve chain segment thickness in one place
Issue: #234
User-Visible: yes
2026-08-21 19:42:42 +03:00
Codex 6836561340 fix: the four unit mutants of #230 have to build test-build first (#230 r1 H2)
Гейт мутаций работает в изолированном `git worktree`, где `test-build/` не
существует: юнит-гвард, идущий сразу в `node --test`, падает с
`ERR_MODULE_NOT_FOUND` ещё на чистом прогоне — то есть не проверяет ничего, а
еженедельный прогон реестра останавливается на первом же таком мутанте и не
доходит до остальных. Соседние юнит-мутанты этого не допускают: их гвард
начинается с `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs`.
Четыре мутанта #230 теперь тоже.

Проверено штатным харнессом, а не вручную: `node scripts/mutation-gate.mjs
--id=<каждый>` — «поймано 1 из 1» для всех семи мутантов задачи, включая три
смоковых, которые и раньше работали.

Тем же дефектом страдают юнит-мутанты #220 и #229 — это чужой скоуп и предмет
[#235](https://github.com/Matysh/houseplan-card/issues/235); здесь не трогаю.

Issue: #230
User-Visible: no
2026-08-21 16:49:33 +03:00
CodexandCodex edf1cca8e3 feat: hatch density is a distance, not a count of units (#230)
Одна и та же стена 15 см выглядела на планах с разным `cell_cm` по-разному:
шаг паттерна был константой в юнитах, а толщина стены переводится в юниты через
`cell_cm`, — значит число полос было пропорционально `1/cell_cm`. Разброс между
крайними масштабами достигал 25 раз, а при `cell_cm ≥ 10` в стену не попадало
и одной полосы: штриховка вырождалась в случайные штрихи или исчезала.

Шаг стал физической величиной: `wallHatchStepUnits(cellCm)` возвращает
`8 × (5 / cell_cm)` — это 9.6 см плана при любом масштабе сетки и ровно
исторические 8 юнитов при эталонном `cell_cm: 5`, так что старые планы не
двигаются. Толщина штриха следует за шагом, поэтому соотношение «штрих к
просвету» тоже перестало зависеть от масштаба.

Формула из описания issue (`cell_cm / 5`) не годилась: она увеличивает шаг там,
где стена и без того тонкая в юнитах, и разброс не исчезает, а растёт — 39
полос против 0.06 на краях диапазона. Множитель обратный; на эту ошибку
поставлен отдельный мутант `hatch-step-inverted`.

Зумовая компенсация `1/zoom` убрана по решению владельца (§4.2 ТЗ): стена,
которая меняет штриховку при зуме, — это тот же дефект, только по другой оси.
От каши на дальнем конце зума защищает второй порог `wallHatchNeedsSolid`
рядом с существующим `wallBodyNeedsSolid`; шаг клампится в [0.5, 80] юнитов,
чтобы патологический `cell_cm` не выродил паттерн.

Статический рендерер (`space-render.ts`) нёс собственную константу 8 и вообще
не знал про зум — то есть уже сегодня расходился с картой при любом зуме,
кроме единицы. Теперь оба читают шаг из одной функции; смок проверяет, что они
согласны между собой, а не только каждый сам с собой.

Два golden-эталона переснято осознанно (`golden:accept -- --reviewed`):
`large-house-zoom-040-dark` (шаг был 20 юнитов, стал 8) и
`large-house-zoom-250-dark` (был 3.2, стал 8). Расхождение осмотрено: меняется
только плотность штриховки тел стен, колонн и перегородок, геометрия и цвета
идентичны. Остальные 80 сцен не тронуты — `accept` переснимает весь набор, и
пять сцен, разошедшихся на шуме рендера, возвращены к прежним байтам вместе с
их хэшами в индексе.

Issue: #230
User-Visible: yes
2026-08-21 15:12:08 +03:00
CodexandCodex fab0c38ca9 refactor: one function owns the junction geometry of a space (#229 r2 M1)
Дефект High-1 был одинаковым в двух местах — и в живом рисовании, и в
«Оптимизировать планы», — потому что каждый вызывающий собирал геометрию
примыканий сам. Ревью r2 справедливо заметило, что и защита получилась
однобокой: юнит и мутант сторожили только оптимизатор, а путь карты — тот, где
дефект и был виден пользователю, — не сторожил никто. Заплатка в виде второго
мутанта-близнеца оставила бы причину на месте: два списка координат, которые
обязаны совпадать, но ничем не связаны.

Поэтому геометрия переехала в `spaceMergeGeometry(space, { excludeDraftId })`:
один источник комнат, колонн и концов черновиков, одни координаты, одно место,
где можно ошибиться. Оба вызывающих теперь строчка вызова.

Покрытие идёт за причиной, а не за симптомом: три юнита в
`test/wall-merge.test.mjs` проверяют масштаб полигонов (включая комнаты в форме
x/y/w/h и комнату без геометрии), исключение активного черновика и сам T-стык к
середине стороны комнаты. Мутанты `partition-merge-rescales-rooms` и
`chain-merge-sees-own-draft` перенацелены на общий модуль и теперь краснеют для
обоих путей сразу: 2 и 1 падение, проверено применением патча.

Сценарий с комнатой в смоке пробовал — не взлетел: рисование в комнату
поднимает `_offerWallFaces`, и цепочка не завершается штатно. Ломать смок под
тест не стал, юниты общего модуля покрывают оба пути честнее.

Issue: #229
User-Visible: no
2026-08-21 13:25:16 +03:00
CodexandCodex ed13b4a5ca fix: a room side and a live draft, seen in the right coordinates (#229 r1 H1,H2)
**High-1.** Комнаты хранятся в тех же координатах, что и перегородки:
`roomPoly` отдаёт сырой полигон конфига. Обе обвязки делили его на `NORM_W`
ещё раз, комната уезжала в область ~0.0001, и `junctionAt` не находил ни
одного совпадения. Узел на T-стыке к середине стены комнаты — тот самый
случай, ради которого ТЗ прошло два раунда ревью, — молча исчезал.
Воспроизведено вызовом `optimizePlans`: `partitionsMerged === 1` там, где
ожидается 0.

**High-2.** Завершаемая цепочка к моменту слияния ещё лежит в `room_drafts`:
каждый клик персистит её через `_persistActiveDraftSegment`, а удаляется
черновик строкой ниже вызова слияния. Собственные концы цепочки считались
чужим примыканием, и стык с существующей стеной не срастался. Активный
черновик теперь исключается — ровно так же, как это делает
`plan-snap-overlay` (`activeDraftId`).

Дыры в тестах, которые это пропустили, закрыты по существу, а не заплаткой:

- `demo/smoke_wall_chain_merge.mjs` рисует продолжение реальными кликами через
  `_markupClick`, а не присваиванием `_path`, — то есть исполняет тот путь, на
  котором дефект и жил. Клики задаются в координатах плана и переводятся через
  живой view box, иначе смок целится мимо только что нарисованной стены.
- `test/plan-optimizer.test.mjs` получил комнату с примыканием к середине
  стороны: юниты модуля этого не ловили, потому что передают полигон уже в
  согласованном масштабе, минуя обвязку.
- Мутанты `partition-merge-rescales-rooms` и `chain-merge-sees-own-draft`
  сторожат оба места: проверены применением патча, 1 и 2 падения.

Issue: #229
User-Visible: no
2026-08-21 13:04:20 +03:00
Codex e6be43b90f feat: a straight wall is one record, not a row of seams (#229)
Рисование прямой стены в несколько кликов оставляло по записи на каждый
отрезок. Швы невидимы, пока их не тронешь: выделение хватает кусок,
перетаскивание ломает стену пополам, толщина задаётся пофрагментно. У стен
комнат этого давно нет — `normalizeWallIntervals` схлопывает каждый сплошной
участок одной толщины. Независимые перегородки жили по другому правилу.

Новый чистый модуль `src/wall-merge.ts` даёт им то же правило:

- `mergeCollinearPartitions` сращивает соседей одинаковой толщины и
  направления до неподвижной точки, но только там, где узел никому не нужен.
  Узел остаётся, если в него приходит третья перегородка, стена комнаты
  (стороной, а не только вершиной), колонна или конец сохранённого черновика.
- Направление выжившей записи канонизируется лексикографически: иначе одна и
  та же физическая стена выходила то a→b, то b→a в зависимости от порядка
  входа, и каждый host.t вдоль неё переворачивался вместе с ней.
- `applyOpeningMoves` переносит проёмы на выжившую запись: и авторитетный
  `host`, и legacy-проекцию `x/y/angle`, которую рисует старый читатель
  конфига (docs/CONFIG-COMPATIBILITY.md, #132). Проекция здесь не кэш —
  канонизация направления разворачивает угол на 180°.

Рисование сращивает только свою цепочку и то, чего она коснулась (§8.6 ТЗ):
молча править чужие швы в стороне оно не вправе — для этого есть
«Оптимизировать планы» с предпросмотром, отчётом и отменой. Оптимизация
проходит по всему пространству без seed-ограничения и отдельной строкой
сообщает, сколько записей исчезло.

Issue: #229
User-Visible: yes
2026-08-21 12:40:34 +03:00
Sergey Matyunin fd43a25746 Merge remote-tracking branch 'origin/issue/220-space-tab-reorder' into dev 2026-08-21 11:03:51 +03:00
Codex 82bb03cdf9 fix: end a tab drag when the card is disconnected
Validate / docs (push) Failing after 31s
Validate / provenance (push) Successful in 1m7s
Validate / process-gate (push) Failing after 1m11s
Validate / changes (push) Successful in 57s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / reuse (push) Successful in 1m30s
Validate / backend (push) Skipped
Validate / frontend (push) Successful in 12m25s
Validate / golden (push) Failing after 12m39s
Validate / performance_smoke (push) Failing after 11m36s
Validate / smoke (push) Failing after 27m40s
Review CODE-REVIEW-220-r2/r3, F1.

The M1 fix installs window listeners for the length of the gesture, and
disconnectedCallback — which takes down everything else, down to the other
local gesture — did not take those down. Losing the card mid-drag (Lovelace
rebuilding its tree, the user leaving the view with the button still down)
left them alive: the closure holds the instance and its config, and the next
pointerup anywhere on the page would have an invisible card write its order.

The smoke now holds a tab, removes the card, and checks that the release it
should no longer hear changes nothing. Registered as a mutant too.

Issue: #220
User-Visible: no
2026-08-21 08:02:03 +03:00
Codex a8aeecc32c fix: judge order dependence by the area in force, close the stuck drag
Review CODE-REVIEW-220-r1.

H1: markersNeedingPlacement decided who depends on the order by reading
marker.area alone, while resolveExplicitMarkerPlacement reads
`marker.area || <area of the HA device>`. The ordinary marker — bind an HA
device, store neither field — is anchored by the registry and never depended
on the order, yet it was being written a space it never asked for. Dormant
today, and the day that HA area changes it moves the marker to whatever space
used to be first. The resolver now asks for the area actually in force.

M1: a mouse released past the panel left the gesture stuck, swallowing the
next click. Pointer capture is the usual answer and is now taken, but it is
not a guarantee — the browser grants it only for a live pointer. The window
listener is what actually closes the gesture.

M2: the fifth mutant from the spec is registered, plus a sixth for the stuck
drag above.

Writing the smoke for M1 turned up why the first attempt passed against
broken code: synthetic PointerEvents default to composed:false and never
leave the shadow root, so nothing outside the panel could ever hear them.
Real pointer events are composed; the smoke now says so.

Issue: #220
User-Visible: no
2026-08-21 00:50:15 +03:00
Codex 8369c0e839 feat: reorder space tabs by dragging one to a new position
The order of config.spaces used to be whatever order the spaces were created
in, and there was no way back other than deleting a space and drawing it
again.

The gesture is deliberately narrow — mouse, editors only. The same tabs are
the primary way to switch spaces in View, where touch is first class, so a
drag there would compete with the tap that switches. Recorded in the spec as
"Touch editor: not exposed".

The part that needed care is not the drag. Position in the array feeds three
things: the marker placement fallback, the swipe neighbour and a positional
`floor`. So the write that stores the new order also writes down the
placement that used to depend on it: a marker with neither an explicit space
nor an area that names one gets the space it has right now. Both changes go in
one save; splitting them would leave a window in which markers move on their
own. The positional `floor` cannot be fixed from here, so the card says so
once.

Issue: #220
User-Visible: yes
2026-08-21 00:27:35 +03:00
Matysh 99c6cd09f3 fix: restore the exact process-gate source for #227
Issue: #227
User-Visible: no
2026-08-20 23:49:12 +03:00
Matysh 9848f4a0cb fix: spend the review budget on blocking verdicts only
The pipeline punished what it prescribed: after a failed merge it tells the
author to rebase and restore S7-code-review, and that attempt finished the
budget. On #225 (light track, limit 2) the sequence yellow, green, rebase
produced review-4 on a task whose code review was green and whose CI was
green, with no product change after the verdict — the owner had to
arbitrate work that was already accepted.

A cycle under section 4 is a verdict with blocking findings followed by a
return to the author, so only yellow and red verdicts spend the budget now.
A green verdict returned nothing and consumes nothing, which also removes
any need to mark rebase re-runs specially.

Attempts and cycles are now separate quantities. The attempt number keeps
naming the document, because two runs sharing a number would overwrite each
other's review artefact, while the limit compares blocking cycles only. The
exhaustion comment lists the verdicts it counted, and the guard no longer
strips review-4 — it reports the recount and leaves the decision with the
owner.

Rule 7 of the process gate follows: its document threshold rises above the
cycle limit, because legitimate attempts can exceed cycles and a threshold
equal to the limit would refuse the very rebase the pipeline demands.

Issue: #227
User-Visible: no
2026-08-20 23:33:08 +03:00
Sergey Matyuninandclaude[bot] d486c64576 fix: canonicalize near-grid coordinates exactly
Issue: #223
User-Visible: yes
2026-08-20 19:32:16 +00:00
Codex 2935c293e1 fix: reject absolute urls in the content resolver, register the mutants
Review CODE-REVIEW-225-r1.

M1: urlsplit(url).path was trusted even when the url carried a scheme or an
authority, so "https://evil.example/houseplan_files/files/m1/doc.pdf"
resolved onto a local file while _looks_internal kept calling it external —
the mirror image of the inconsistency this resolver exists to prevent. Only a
same-document reference is resolved by its path now.

M2: the three mutants the spec described are registered in
scripts/mutation-gate.mjs instead of living as a one-off manual run. The
traversal entry drops both structural checks at once on purpose: taken one at
a time the defence is layered (sanitize_marker_id turns ".." into "misc") and
the mutant would be equivalent — established by running it.

Issue: #225
User-Visible: no
2026-08-20 22:00:22 +03:00
Sergey Matyuninandclaude[bot] 442731e8eb fix: deduplicate entity markers from parent devices
Issue: #226
User-Visible: yes
2026-08-20 18:48:01 +00:00
Sergey Matyunin a20dd54ba6 test: retarget the LQI mutation guard
Validate / docs (push) Failing after 47s
Validate / provenance (push) Successful in 1m41s
Validate / process-gate (push) Failing after 1m52s
Validate / changes (push) Successful in 1m7s
Validate / reuse (push) Successful in 37s
Validate / hacs (push) Failing after 13s
Validate / hassfest (push) Failing after 15s
Validate / frontend (push) Successful in 7m29s
Validate / backend (push) Failing after 9m12s
Validate / golden (push) Failing after 13m36s
Validate / performance_smoke (push) Failing after 14m19s
Validate / smoke (push) Failing after 35m6s
Full Performance / performance (push) Failing after 1h57m49s
Issue: #222
User-Visible: no
2026-08-20 16:26:29 +03:00
Sergey Matyunin 3d11758e2b fix: keep Glow visible with noisy floor geometry
Stabilize polygon-boolean inputs at render time and isolate residual
room failures without weakening fail-dark clipping.

Issue: #218
User-Visible: yes
2026-08-20 14:05:22 +03:00
Sergey Matyunin 0e5ee030fe fix: polish device marker geometry and input
Issue: #213
User-Visible: yes
2026-08-20 11:00:22 +03:00
Sergey Matyunin 120d41317c fix: polish device icons and pointer feedback
Issue: #212
User-Visible: yes
2026-08-20 08:42:34 +03:00
Sergey Matyunin 6063eead10 fix: pin card instances to configured spaces
Issue: #210
User-Visible: yes
2026-08-20 01:38:01 +03:00
Sergey Matyunin 270cf634e6 test: strengthen unavailable hover mutant
Issue: #211
User-Visible: no
2026-08-20 00:44:48 +03:00
Sergey Matyunin 4e82976b4a fix: match device icons to designer package
Issue: #211
User-Visible: yes
2026-08-20 00:44:11 +03:00