П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
Owner decision (chat, 2026-08-27): keep П3 and fix the smoke. A 10 cm island
room is a column, and columns have their own tool (wall_columns) — that was
the reasoning behind the limit in the first place. The island of the smoke is
now 25 cm, and a new case pins the contract: a 10 cm island is refused.
Issue: #329
User-Visible: no
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
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
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
The owner's five limits as pure functions: minimum 15 degrees between
neighbouring rays of a node (a straight wall through the node is a 180 pair,
not a violation), at most 6 walls per node, a segment at least
max(20 cm, its own thickness), 5 cm clearance between non-incident nodes and
between a node and a foreign wall (a T-joint sitting exactly on that wall is
incidence, not a near miss), and a room interior of at least 25 cm2 after the
masonry is subtracted. Thresholds are absolute and do not scale with cell_cm.
newViolations() implements the spec's inheritance boundary: only violations
introduced by the write are reported.
Issue: #329
User-Visible: no
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
A stable body aggregates the changelog since the previous STABLE release,
not since the last beta; in-beta-only bugfixes are excluded by the curator
(the draft lists every candidate with its source section); the small-fixes
filler line is legal only when the range carries user-visible work not
itemised in the body — a single-issue hotfix ships without it, and body
bullets must link their issues so the rule stays checkable.
scripts/release-notes.mjs prints the aggregation draft and verifies
docs/RELEASE-NOTES.md (npm run release:notes -- <tag> [--verify]); the
verifier rejects the v1.68.1-style empty filler by execution. STATUS.md
release mechanics updated in the same commit.
Issue: #328
User-Visible: no
Owner decision (chat, 2026-08-27): the running check's name must say what it
does, in Russian. Scripts locate workflows by file name (release-gate.mjs ->
validate.yml), so display names are free; job ids and needs are untouched.
The same content is cherry-picked to main because release workflows execute
from the default branch and process.yml must stay identical in main and dev.
Issue: #327
User-Visible: no
Revert of caf6e6c3. The owner's field report disproved the diagnosis: HACS
2.0.5 downloads zip_release assets fine, because async_download_file strips
the 'tags/' prefix from the composed URL before requesting (hacs/base.py) —
the prefix only survives in the error-log message, which prints the
unnormalized URL. The dacha installed every beta and v1.68.0 through HACS
with zip_release active. The observed 'Failed to download' was a transient
network failure fetching release-assets.githubusercontent.com (five retries
exhausted, one occurrence), not a routing bug.
Issue: #325
User-Visible: no
(cherry picked from commit 5ad0c1cdc3)
Revert of caf6e6c3. The owner's field report disproved the diagnosis: HACS
2.0.5 downloads zip_release assets fine, because async_download_file strips
the 'tags/' prefix from the composed URL before requesting (hacs/base.py) —
the prefix only survives in the error-log message, which prints the
unnormalized URL. The dacha installed every beta and v1.68.0 through HACS
with zip_release active. The observed 'Failed to download' was a transient
network failure fetching release-assets.githubusercontent.com (five retries
exhausted, one occurrence), not a routing bug.
Issue: #325
User-Visible: no
Installing a specific version from the HACS catalog fails: HACS composes the
zip_release asset URL from its internal ref 'tags/<version>' without
stripping the prefix (hacs/integration base.py:932 -> download_zip_files ->
github_release_asset), requesting releases/download/tags/v1.68.0/... which
GitHub cannot serve. A mirror tag with a slash is not servable either.
Without zip_release HACS falls back to the regular tag-tree download that
worked before 2026-08-08. Release assets keep being attached
(release-zip.yml untouched) so re-enabling after the upstream fix is a
one-line revert.
Issue: #325
User-Visible: no
(cherry picked from commit caf6e6c355)
Installing a specific version from the HACS catalog fails: HACS composes the
zip_release asset URL from its internal ref 'tags/<version>' without
stripping the prefix (hacs/integration base.py:932 -> download_zip_files ->
github_release_asset), requesting releases/download/tags/v1.68.0/... which
GitHub cannot serve. A mirror tag with a slash is not servable either.
Without zip_release HACS falls back to the regular tag-tree download that
worked before 2026-08-08. Release assets keep being attached
(release-zip.yml untouched) so re-enabling after the upstream fix is a
one-line revert.
Issue: #325
User-Visible: no
CODE-REVIEW-323-r1 M1: the full guides README links to still taught the
custom-repository flow; step 1 now matches the README wording.
Issue: #323
User-Visible: no
The HACS row still described an open queue of 835 PRs; the fresh-install
checklist still started from a custom repository.
Issue: #323
User-Visible: no
Since 2026-08-25 the integration is in the HACS default catalog
(hacs/default#9004); the badge and both install sections no longer route
users through Custom repositories — a plain HACS search finds it.
Issue: #323
User-Visible: no
Package the two beta.3 write-blocker fixes: the model-upgrade stale-client
guard and the self-resolving zero-wall migration.
Issue: #316
Issue: #319
User-Visible: yes
The r3-M1 fix was applied as a mechanical substring replacement and flipped
BOTH model_version conditions in the file; the independent room_wall_ids
invariant (#244/#252) silently stopped checking every v9+ document. Only the
opening_host requirement is scoped to model v8 — room_wall_ids is back to
'v8 and every later version', now pinned by its first regression test
(phantom wall_ids reported on v9 and v8; executed red on the broken
comparison, green after the fix).
Issue: #316
User-Visible: no
Since #316 §3.3 an unhosted contour opening is a valid v9 state — the
migration keeps an opening with no in-place carrier as data. The CLI still
reported it as an opening_host violation for every model_version >= 8; the
requirement now applies to model v8 documents only. The regression test is
proven able to fail on the old comparison (executed red), and the reviewer's
CLI reproduction now finishes clean.
Issue: #316
User-Visible: no
Pixels are untouched — the r1/r2 review fixes change migration data flow,
not rendering; every scenario hash stays byte-identical to the frames of the
reviewed CI run. Only the source fingerprint moves to match the fixed tree.
Issue: #316
User-Visible: no
Release: v1.68.0-beta.4
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33000824647
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
The scene was captured by CI run 33000824647 byte-identical to the locally
reviewed frame: the door keeps its solid jambs inside the former border, the
zero run is dashed on both sides, the leaf swings into the room.
Issue: #316
User-Visible: no
Release: v1.68.0-beta.4
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33000824647
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
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
Pixels are untouched — the r1 fixes change diagnostics data and dialog state,
not rendering. Every scenario hash is byte-identical to the frames CI
captured and I reviewed for run 32940625718; only the source fingerprint
moves to match the fixed tree.
Issue: #295
User-Visible: no
Release: v1.68.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32940625718
CODE-REVIEW-295-r1, both Medium findings:
M1 — _preflightDiagnostics hashed this._serverCfg, the saved config a space
export would reproduce anyway. The hash now comes from the candidate the
preflight actually judged: the report path passes r.config / d.config and the
copy button reads the dialog's own candidate. The smoke no longer masks the
difference — its dialog carries a candidate whose geometry differs from the
saved config, and swapping the two must change the reported hash.
M2 — the inline clipboard fallback survived dialog close and reopen, so a
later refusal could hand the previous refusal's JSON to a bug report. The
field now dies with its dialog: reset on open (_previewAlignDialog) and on
every close path (escape, mode reset, successful apply, hp-close, cancel).
Both regressions are pinned by execution: reverting either fix turns the
extended smoke red (fingerprintTracksCandidate / fallbackClearedOnClose),
and two new gate mutants keep it that way.
Issue: #295
User-Visible: no
Both scenes were captured by CI run 32940625718 (byte-identical to run
32939996348), visually reviewed: failure reasons render as rows, the
ghost copy button is borderless per dialogs.styles.
Issue: #295
User-Visible: no
Release: v1.68.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32940625718
Диалог «Оптимизировать» при отказе перечисляет причину по каждому
пространству (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
resolveValidationRange already knows how to replace a force-push-orphaned
BEFORE_SHA with origin/dev, but the CLI handed it a runner that killed the
process with exit 2 on the first cat-file instead of throwing into
gitObjectExists' catch. Every push after a mandatory issue-branch rebase
therefore painted process-gate red (runs 32939996348, 32940625718,
32942113142). The regression test drives the real CLI in a throwaway repo
with a BEFORE_SHA that no longer exists.
Issue: #315
User-Visible: no
USER-GUIDE.ru.md — канон формулировок интерфейса (AGENTS.md): флаг диалога
привязки называется «Показывать сущности», как в трёх местах гайда и в
преобладающей форме флагов самого словаря. en.json не меняется.
Issue: #269
User-Visible: yes
По находкам CODE-REVIEW-313-r1:
High — резолвер больше не превращает сохранённый 0 сегмента драфта в 15:
ноль — легитимное значение (docs/WALL-THICKNESS.md §6), к дефолту 15 падает
только ОТСУТСТВУЮЩАЯ запись. Смок дополнен: hit нулевого сегмента несёт 0,
диалог показывает пустое поле, Apply без правки отказывает и не портит
данные.
Medium — гвард #278 усилен: паттерн допускает перенос строки после скобки,
и счётчик требует РОВНО ДВЕ точки коммита wall_thickness — мутация любой из
них (включая новую независимую) красит юнит; краснота второго патча мутанта
проверена изолированным исполнением.
Issue: #313
User-Visible: no
Кандидаты _wallThickHit расширены: интервалы комнат ∪ перегородки ∪ сегменты
сохранённых драфтов (активная цепочка исключена, как в снап-геометрии); при
точном наложении побеждает независимая кладка — она владеет хит-зоной и
рисует видимое тело (решение владельца, согласовано с select и кейсом #308).
Диалог для независимой кладки без кнопки «на всю комнату»; запись — в
partition.cm / draft.segments[i].cm той же физической транзакцией с одним
Undo. Ноль/пусто для независимой кладки отклоняется существующим тостом
диапазона; switch записи — единственный шов, куда #306 повесит ветку
«ноль превращает перегородку в виртуальную стену».
Мутант wall-thickness-writer-bypasses-common-barrier расширен вторым патчем
на новую точку коммита (#278-гвард), краснота обоих проверена исполнением.
Смок smoke_wallthick_standalone: hit/диалог/запись/отказ нуля/приоритет
наложения/hover — на dev падает.
Issue: #313
User-Visible: yes
Синхронизация process.yml с dev: шаг слияния сверяет вершину ветки с SHA
материала ревью (допустим ровно один doc-коммит публикации поверх) и при
расхождении отменяет слияние с возвратом в S6-in-progress. Конвейер
исполняется из ветки по умолчанию — правка обязана жить в обеих ветках
(process-workflow-sync).
Issue: #312
User-Visible: no
Правило 10 гейта (#311): DoR сверяется с моментом НАПИСАНИЯ кода — authorDate
коммита класса A не может предшествовать первому labeled-событию S5-ready+
из timeline issue; продвижение метки больше не прячет нарушение, ребейзы
конвейера его не смывают (authorDate переживает их). Проверка вторичная к
правилу 8: недоступный timeline — warn, правило 8 остаётся fail-closed.
LOG_FORMAT несёт authorDate третьим полем (append-совместимо).
Шаг слияния конвейера (#312): сливается только проверенный SHA — вершина
ветки сверяется с материалом ревью (допустим ровно один doc-коммит публикации
с диффом только docs/reviews/ поверх); расхождение отменяет слияние с
возвратом в S6-in-progress тем же путём, что конфликт (инвариант «метка
меняется всегда» сохранён). PROCESS.md §2.7 фиксирует правило «вердикт
привязан к SHA» и для ревьюера.
Issue: #311
Issue: #312
User-Visible: no
Кадры прежние — golden и смоки подтверждают пиксельную идентичность; коммит
догоняет dev после мержа, прошедшего без него (пуш ветки и мерж конвейера
разошлись на один коммит).
Issue: #266
User-Visible: no
CI-шард смока поймал сдвиг каскада, невидимый golden-набору:
smoke_device_icon_design — alert-shell стал серым, dark-unavailable core
светлым. Причина: классификатор считал ведущий :host(...) владельцем
селектора и уносил гейтнутые группы устройств в base — ВПЕРЁД их поверхности,
меняя победителя при равной специфичности. Теперь :host-префикс — гейт, а не
владелец: владелец — первый значимый токен после него; смешанные @media
режутся на последовательные по-зонные копии обёртки (reduced-motion обёрток
стало 12 поверх тех же правил 10 исходных — юнит заякорен на факт, сверка
scope-ключом доказывает, что ни одно правило обёртку не потеряло). Два
мутантных якоря вернулись в devices.styles.ts; исключение юнита
непересечения опустело.
Гейты: refactor-proof diff пуст · golden 129/129 без переприёмки · смоки
device_icon_design, plan_snap_overlay, preloader OK · npm test 1318/0.
Issue: #266
User-Visible: no
52 блока host/переменных/кросс-поверхностных групп → src/styles/base.styles.ts;
styles.ts — 19-строчный сборщик [base, plan, devices, chrome, dialogs] с
задокументированным контрактом порядка каскада. Два оставшихся мутантных
якоря (:host-гейт ховера устройств) переадресованы на base.styles.ts.
Юниты инвариантов (test/styles-split.test.mjs): состав и порядок сборщика;
непересечение (scope+селектор) между файлами — единственное именованное
исключение: кросс-поверхностная группа «:host(...) .dev:hover, .dev:focus-
visible» живёт в base по §1.1; выживание медиа-обёрток — 2 forced-colors и
10 prefers-reduced-motion (в ТЗ и ревью фигурировали 8 — фактический счёт по
исходнику 10, юнит держит точное число). fix-test-build научился точке в
имени модуля (styles/base.styles → .js). ARCHITECTURE.md — раздел Styles.
Refactor-proof diff (scope-ключ) пуст; golden 129/129 без переприёмки; смоки
plan_snap_overlay/preloader OK; npm test 1318/0; бандл 1 291 440 → 1 291 458
(+18 байт).
Issue: #266
User-Visible: no
271 блок диалогов/форм/кнопок/пикеров → src/styles/dialogs.styles.ts, склейка
[inline, chrome, dialogs]. Контрактные тесты, которые греппят CSS-исходник,
переведены на хелпер readAllStylesSource (styles.ts + все импортированные
файлы поверхностей) — greps видят весь лист на любом состоянии сплита.
Refactor-proof diff пуст; golden 129/129; npm test 1315/0.
Issue: #266
User-Visible: no
styles-split.mjs — механический генератор слайсов (зоны в порядке финального
сборщика, инлайн-остаток промежуточных состояний сохраняет относительные
позиции финала); styles-diff.mjs — нормализованное множество правил с ключом
«полный путь вложенности + селектор» для доказательства refactor-only.
Issue: #266
User-Visible: no
Правило 2 локального process-gate блокировало пуш issue-ветки после того, как
конвейер «Привести ветку к dev» вносил в неё свежую историю dev с чужим
коммитом «docs: review document for #NNN» — локальный origin/dev автора
отставал и не вычитал его из диапазона, а нарушение в истории не чинится
следующим коммитом (единственный выход был --no-verify по §12/17).
Исключение доказуемое и fail-closed: точный subject документа ревью И дифф
только в docs/reviews/ (files обязателен — ownCommits теперь читаются с
filesOf). Любой код рядом с документом или пустой список файлов возвращают
правило 2 в строй; юниты фиксируют оба контура.
Issue: #305
User-Visible: no
Юнит по ТЗ §8 (риск №3): у пары с коротким толстым саппортом клин ограничен
min(2·halfDepth, длина стены), сеточный контракт чист; интерференция с
латеральным тримом #271 структурно невозможна (карта узлов требует ≥3
канонических лучей — узел-пара в неё не попадает), что зафиксировано в
комментарии теста.
Поправка происхождения эталона по находке CODE-REVIEW-310-r1 (High): трейлер
Baseline-Reviewed коммита 0e6fbfb0 ошибочно указывал на прогон ветки #309;
подтверждающий прогон этого кода и эталона — Validate на 0e6fbfb0 (ссылка
ниже), golden в нём зелёный на Linux CI.
Issue: #310
User-Visible: no
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32894391916
Пересъёмка единственной изменившейся сцены junction-309-spike-dark: полное
остриё без зубца торца. Принято npm run golden:accept -- --reviewed; шумовые
93 сцены откачены к прежним байтам; golden:verify после отката — 129/129.
Issue: #310
User-Visible: no
Release: v1.68.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32886626656
Узел ровно двух лучей снова закрывается полным mitre — стены сходятся в
точку, фаска #309 остаётся только веерам узлов ≥3 лучей. Настоящий зубец
убран: pairButtEndTrimWedges возвращает адресный клин — часть тела стены
снаружи наружной грани соседа и не дальше 2·halfDepth от узла — который
physicalBodyParts и превью вычитают из тела до разрезов проёмов. Это второе
адресное вычитание конвейера узлов рядом с латеральным тримом #271.
Узлы-двойки невидимы детектору #302 (карта требует ≥3 лучей): контракт «без
дыр» для них закрыт парным сеточным юнитом (кладка = полосы ∪ патч − клинья)
на spike-узле фикстуры владельца и синтетике. 3 новых мутанта, краснота
каждого проверена исполнением; парный юнит #309 переписан под полное остриё.
Issue: #310
User-Visible: yes
CI-падение sun-ray guard: тест фиксирует номер матрицы, #309 поднял его
тремя junction-teeth сценами. Полный npm test — 1309/0.
Issue: #309
User-Visible: no
Принято npm run golden:accept -- --reviewed; шумовые 92 сцены откачены к
прежним байтам, индекс несёт matrixVersion 46 и sha256 трёх новых сцен;
golden:verify после отката — 129/129.
Issue: #309
User-Visible: no
Release: v1.68.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32882555609
Вылет mitre ограничен VISUAL_MITRE_LIMIT = 1.5·max(h): длиннее — плоская
фаска перпендикулярно направлению вершины (chamferApex), в парных патчах и
в веерах узлов. Узлы ≥3 канонических лучей закрываются веерами
junctionNodeGeometry прямо в linearWallJoinPatches: парный патч живёт в
секторе, противоположном своей паре, и красил ступень поверх тонких полос
(крест 15/15/30/30 из отчёта владельца). Прямые углы (вылет 1.41h)
байтово прежние. Механизм #249 (MULTI_WALL_JOIN_LIMIT,
multiWallBevelCutsAt, mitre контуров комнат) не тронут.
Юниты формы на фикстуре трёх узлов владельца + контрактный детектор дыр;
4 новых мутанта, краснота каждого проверена исполнением. Матрица golden 46:
три новые сцены junction-309-{step,spike,hump}-dark.
Issue: #309
User-Visible: yes
Каждый клик персистит сегмент цепочки в room_drafts, и его непрозрачная
кладка рисовалась поверх markup-слоя — жёлтая ось и узлы исчезали на уже
поставленных частях. Разметка активной цепочки вынесена в свой слой между
телами стен и снап-оверлеем: ось и узлы видны, снап-геометрия не тронута,
самопривязка по-прежнему запрещена. Смок с пиксельными пробами падает на
прежнем порядке слоёв.
Issue: #307
User-Visible: yes
Ребейз на dev столкнул две легитимные правки одной пары сцен: мой узел
(сомкнутая вершина ромба, decision №5) и plan-axis подсветку из dev. Обе
сцены пересняты на объединённом коде и осмотрены: вершина ромба сомкнута,
осевые линии dev на месте. Остальные 124 сцены не тронуты — шум `accept`
возвращён; хэши двух сцен, чьи PNG пришли из dev при ребейзе, приведены к
фактическим файлам.
Принято `npm run golden:accept -- --reviewed`; `golden:verify` после отката
шумовых — 126/126.
Issue: #302
User-Visible: no
Release: v1.68.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32859268589
Код честно держал адресный латеральный трим с самого решения №5, дока после
M1 описывала его верно — расходился только текст ТЗ, писавший «демонтирован
целиком». §4.5, §8.2 и AC6 приведены к фактическому контракту.
Issue: #302
User-Visible: no
Пустой коммит: локально смок grid_scale_invariance стабильно зелёный (3/3,
darkView changed=69 при пороге 150), и тот же дифф-фон 69 воспроизводится на
чистом dev — падение шарда на прошлом прогоне похоже на средовую
вариативность раннера, а прав на rerun-failed-jobs у токена нет.
Issue: #302
User-Visible: no
С адресным тримом (он не режет полосы обычных узлов) возврат саппорт-квадов в
тело стал мёртвым слоем: полный юнит-набор зелёный без него — проверено
исполнением, а не предположено. По дисциплине мутационного реестра
избыточный слой убран (fans only), его мутант `junction-supports-not-restored`
снят: у #271-узлов трим режет только ЗА пределами саппорта, возвращать
нечего. Саппорт-квады остаются экспортом `junctionNodeGeometry` — детектор и
тесты используют их как источник контрактной истины.
`npm test` 1303/1303; `golden:verify` 126/126; контракт-проба репро — 0;
`smoke_junction_holes` OK.
Issue: #302
User-Visible: no
Контрактные пробы детектора строятся из той же junctionNodeGeometry и слепнут
вместе с мутацией; юнит «T-узел даёт два веера» — внешняя истина. Смоковый
гвард с полной сборкой оставался зелёным на сломанном коде — проверено
штатным харнесом, а не заявлено.
Issue: #302
User-Visible: no
**M1.** `docs/WALL-THICKNESS.md` §3 «Junction nodes» переписан под решение №5:
полный mitre, фаска #249 в отставке, `bevelMultiWallBody` — только адресный
латеральный трим. Прежний абзац описывал отменённое утреннее решение.
**M2.** Guard мутанта `junction-fans-disabled` собирает `test-build` и бандл
перед смоком: `smoke_junction_holes` — единственный смок, импортирующий из
`test-build`, и в чистом worktree он падал `ERR_MODULE_NOT_FOUND` до
применения мутации. Ревью прав: после переякорения guard'а на смок я не
перегнал его штатным харнесом — только ручной test-build-патч, который worktree
не видит.
**Low + следствие.** `bevelMultiWallPaper` удалена как мёртвый код; следом
измерено (фикстура #197 и репро владельца — байт в байт с веерами и без), что
и `paperWithNodeCorners` бумаге ничего не даёт: footprint ∪ shell уже
покрывает каждый узел. Слой удалён целиком, бумага возвращена к rawPaper.
Осиротевший мутант `multi-wall-paper-full-origin-cut` (#261, «белый клин от
вычитающего разреза бумаги») снят с обоснованием: в бумаге не осталось ни
одного вычитания — этот класс регресса невозможен по построению.
`npm test` 1303/1303; `golden:verify` 126/126; контракт-проба репро — 0.
Issue: #302
User-Visible: no
Заодно даёт CI прогон с валидным before-SHA: предыдущий пуш был вынужденно
форсовым после ребейза на #265, и process-gate на CI не смог вычислить
диапазон от затёртой вершины.
Issue: #302
User-Visible: no
Ребейз на свежий dev (#265 import seam + его эталон) слил baselines-index из
двух источников; поштучное слияние потеряло `matrixVersion: 45` и держало
хэши двух сцен, чьи PNG пришли из dev. Индекс поправлен по фактическим
файлам, `golden:verify` — 126/126 с валидным манифестом. Бандл и отпечаток
скриншотов пересобраны из объединённых исходников (`npm test` 1303/1303).
Issue: #302
User-Visible: no
Release: v1.68.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32848447191
Шард 3 упал на CI по darkViewPixelsMatch, локально смок стабильно зелёный
(3/3, changed=69 при пороге 150), и тот же фон 69 воспроизводится на чистом
dev. Смок печатал только булевы вердикты — добавлен диагностический вывод
сырых дифф-метрик, чтобы прогон CI показал фактическую величину дрейфа.
Issue: #302
User-Visible: no
Пустой коммит: локально смок grid_scale_invariance стабильно зелёный (3/3,
darkView changed=69 при пороге 150), и тот же дифф-фон 69 воспроизводится на
чистом dev — падение шарда на прошлом прогоне похоже на средовую
вариативность раннера, а прав на rerun-failed-jobs у токена нет.
Issue: #302
User-Visible: no
Шесть сцен, изменившихся законно при переходе на полный mitre (решение №5):
`junction-y-60-equal50-dark` (вырез исчез), `junction-acute30-mixed-dark`
(рожок фаски ушёл, остался законный торец перехода толщин 15→70),
`junction-splay10-170-dark`, `junction-owner-repro-dark` (репро владельца:
стык сомкнут полностью) и `safe-resize-handles-clamp-{light,dark}`, где
вершина ромбовидной комнаты в узле теперь сомкнута веером вместо прежнего
зазора. Остальные 120 сцен совпали; шумовая пересъёмка `accept` возвращена к
прежним байтам вместе с хэшами (практика #230).
Принято `npm run golden:accept -- --reviewed` по полному локальному
Linux-прогону; после отката шумовых verify чист (126/126). Каждая сцена
осмотрена.
Issue: #302
User-Visible: no
Release: v1.68.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32840836354
Владелец, осмотрев первые эталоны сета, отменил дневное решение о сохранении
фаски: `junction-y-60-equal50` показывал вырез, `junction-acute30-mixed` —
торчащие углы. По визуальному сравнению трёх вариантов принято: узлы
смыкаются полным mitre, как обычное пересечение стен на чертеже.
Итоговое правило веера (одно на все случаи):
- mitre принимается, когда он В СЕКТОРЕ пары (вперёд по лучам для обычной
пары, назад — для рефлексной: наружный угол между крайними лучами, где и
жил вырез Y-60), в пределах классического `MITRE_LIMIT` и не дальше конца
толстого саппорта (#271);
- рефлекс без валидного mitre замыкается плоской хордой между гранями;
- обычная пара без mitre — локальный бевел: ход по граням ограничен толстым
саппортом, лимитом и двойной толщиной пары, чтобы хорда осталась деталью
угла. Гигантские бевел-«бабочки» и mitre вне сектора — две реальные ошибки
промежуточных версий, обе пойманы на сценах сета до пуша.
Слой `bevelMultiWallBody` сохранён только как АДРЕСНЫЙ латеральный трим для
узлов с вырожденно-коротким толстым саппортом (#271); все прочие узлы — чисто
аддитивные, следы трима на них исчезли. `bevelMultiWallPaper` из бумаги
удалён. Обе записи CHANGELOG приведены к финальному контракту.
Тесты: юниты §302 усилены; площадь фикстуры #197 +0.6 юнита²; мутанты
переякорены, краснота каждого проверена исполнением.
Issue: #302
User-Visible: yes
Смок `smoke_multiwall_junction` держал старый контракт «клин за фаской пуст» —
его проба лежит в перекрытии двух полос узла и по strip-safe правилу #302
обязана остаться заполненной. Пропущен в первом прогоне AC9, пойман CI.
Issue: #302
User-Visible: no
Ребейз на свежий dev (поиск в селекторах проёмов #301, честная подсветка
толщины #303) объединил исходники; бандл и отпечаток скриншотов пересобраны
из результата. Обе копии бандла байт в байт; `check-docs` passed; полный
`npm test` 1298/1298 и golden verify 126/126 на объединённом коде.
Issue: #302
User-Visible: no
Шестнадцать новых сцен стыков крупным планом (звёзды лучей: T/X/Y, острые
15°/30°, почти коллинеарные, смешанные толщины, виртуальные участки, колонна,
черновик) плюс сцена-репро владельца. Только новые файлы: все 110 существующих
сцен на этом коде прошли verify побайтно — переработка узлов не изменила ни
одну старую картинку, что и требовал AC3. Пересъёмка шумовых копий,
оставленная `accept` на прочих сценах, возвращена к прежним байтам вместе с
хэшами в индексе (практика #230).
Принято `npm run golden:accept -- --reviewed` по полному локальному
Linux-прогону (126/126), после принятия verify чист. Каждая новая сцена
осмотрена; кладка узлов сплошная, фаски #249 на месте.
Baseline-Reviewed указывает на зелёный прогон Validate этой ветки
(ревьюированное ТЗ, SHA 3b19111) — прогона CI с этими эталонами до этого пуша
не существует; job `golden` на них выполнится в прогоне этого же пуша.
Issue: #302
User-Visible: no
Release: v1.68.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32819360269
Вторая половина переработки узлов поверх ядра из прошлого коммита:
- `junctionContractHoles` — объективный инвариант «тело ⊇ полосы ∪ веера в
фасадной границе» как экспортная чистая функция; самопроверка на заведомо
дырявой фикстуре входит в юниты. Первая формулировка детектора из ТЗ
(«окружено кладкой с ≥5 из 8 сторон») уточнена в §8.4 по факту измерения:
она ложно флагует легитимный пол комнаты в острых внутренних углах.
- `junctionNodeBound` — «гладкая» фасадная граница (конверт с обычными углами)
экспортирована: ею клиппуются куски узла и ею же пользуются тесты.
- юниты #302: веера/рефлекс/короткий толстый саппорт/детектор/репро
end-to-end; хелпер тестов старого контракта переведён на strip-safe
семантику по саппортам и фасадной границе.
- смок `smoke_junction_holes`: контрактные пробы считаются в node из той же
фикстуры и проверяются в браузере по реальному `d`-пути карточки.
- сет из 16 golden-сцен стыков крупным планом (звёзды лучей: T/X/Y/острые/
почти коллинеарные/виртуальные/колонна/черновик) + сцена-репро владельца;
билдер сцен и `zoomCenter` в harness. Контракт сцены
`multiwall-junction-bevel-view-dark` инвертирован по решению владельца:
проба в перекрытии полос обязана быть ЗАПОЛНЕНА (strip-safe), а не пустой.
- семь мутантов §14, включая слепоту детектора и невозврат саппортов.
- фикстура #197: площадь кладки выросла на 0.2 юнита² — слайвер вееров вдоль
хорд фаски; константа обновлена с комментарием.
Эталоны новых сцен идут отдельным коммитом с положенными трейлерами.
Issue: #302
User-Visible: yes
Ядро переработки узлов. Слой фаски #249 (`bevelMultiWallBody`) остаётся как
утверждённый вид, но после него узел аддитивно получает обратно:
- точные саппорт-квады своих лучей (каждый ограничен собственной конечной
длиной — обрезанный латеральный фантом #271 вернуться не может);
- по вееру на каждую пару соседних по азимуту лучей (сектор ≤ 180°; рефлексные
секторы — внешность выпуклого угла — пропускаются), mitre в пределах лимита
узла, иначе bevel-хорда на том же радиусе, что и хорда фаски.
Куски клиппуются «гладкой» фасадной границей (`junctionNodeBound`: конверт с
обычными углами, без узловых засечек) — узел не может отрастить новый фасад
(контракт вогнутого Split), но и не теряет секторные веера, как терял бы при
клипе по засечённому конверту. Вырожденные кольца нулевой площади, которые
polyclip оставляет на совпадающих хордах, вычищаются.
Тесты старого контракта переведены на новый: клин за фаской заполнен, если
лежит в полосах узла (острый стык — сплошная кладка, сама починка #302), и
пуст вне полос (фаска #249 как была). Хелпер и точечные тесты #249/#271/#197 и
corner-Split обновлены; площадь фикстуры #197 выросла на 0.2 юнита² — слайвер
вееров вдоль хорд фаски.
Проверено исполнением: `npm test` 1286/1286; контракт «тело ⊇ полосы ∪ веера»
на репро владельца — 0 пропаж; клинья на скриншоте исчезли.
Issue: #302
User-Visible: no
Гейт мутаций работает в изолированном `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
Две сцены и только они: `large-house-zoom-040-dark` (шаг штриховки был 20
юнитов, стал 8) и `large-house-zoom-250-dark` (был 3.2, стал 8). Расхождение
осмотрено покадрово: меняется только плотность штриховки тел стен, колонн и
перегородок — геометрия, цвета, устройства и свет идентичны. Это прямое
следствие решения владельца §4.2 ТЗ, ради которого зумовая компенсация и
убиралась.
Принято `npm run golden:accept -- --reviewed`. `accept` переснимает весь набор,
поэтому пять сцен, разошедшихся на шуме рендера
(`isometric-large-warm-remount-dark`, `room-label-parity-plan-dark`,
`tray-medium-group-en`, `wall-junctions-plan-preview-light`,
`wall-junctions-plan-t-dark`), возвращены к прежним байтам вместе с их хэшами
в индексе: `verify` считал их совпадающими в пределах допуска, и принимать их
задача #230 права не имеет.
Baseline-Reviewed — прогон, где job `golden` прошёл на Linux ровно на этих
эталонах.
Issue: #230
User-Visible: no
Release: v1.67.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32480753934
`edf1cca` принял два эталона внутри продуктового коммита, а по правилу
процесса коммит, трогающий `demo/golden/baselines/**`, обязан нести `Release:`
и `Baseline-Reviewed:`. Локально это не остановилось: в моём клоне не был
выставлен `core.hooksPath`, и `commit-msg` попросту не запускался — теперь хуки
установлены, проверено повторным прогоном скрипта вручную.
История не переписывается (AGENTS.md: «never rewrite published history to
satisfy trailers»): эталоны снимаются этим коммитом и возвращаются следующим,
уже с положенными трейлерами.
Issue: #230
User-Visible: no
Release: v1.67.0-beta.1
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/32480753934
Одна и та же стена 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
Дефект 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
**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