Same pairing rule as before: PNG files and their manifest must come from one
capture run; the rebase over the i18n-registry merge (#62) mixed the sides
again.
Issue: #330
User-Visible: no
H2: the benchmark budgets were calibrated on the author's sandbox with a
1.14x margin — the review runner measured tsFullCandidateMs at 169-171 ms
against a 100 ms ceiling. Budgets now keep the spec's 2-3x allowance over
the SLOWEST observed machine, and the benchmark runs as a step of the
Validate perf job on every push (it needs no browser and no bundle), not
only inside the weekly mutation gate.
M1: the promised AC1 backend test exists now and does what AC1 means: it
patches validate_junction_limits with a thread-recording wrapper inside the
real HA harness — on the event loop that would be MainThread — and proves
the verdicts survived the move (a clean write is accepted, a write adding a
spike is refused with junction_limit_angle). Spec revision 4 rewrites AC1
around this invariant instead of a fragile millisecond assertion.
M2: §4.6 equivalence is now behavioural on both sides (three boundary
fixtures each: as-is counts equal through-migration counts, TS and python),
and the parity suite gained the §7 boundary fixtures (exact 15°, exact
20 cm, the thickness-step filler run, exact 5 cm).
H1 was already closed by 7513f93d (the review ran on the previous HEAD):
check-docs is green on this tree — the screenshots and their manifest come
from one capture run.
Issue: #330
User-Visible: no
The rebase resolved docs/images/screenshots.json to the dev side while the
PNG files stayed from this branch's capture — CI correctly refused the
mismatched pair. One local capture regenerates both halves from the same
run, so hashes and the source fingerprint agree again.
Issue: #330
User-Visible: no
Writing the §5 benchmark honestly exposed a cost the point measurements of
П1-П4 could not see: П5 recomputed the full junction topology and masonry
union PER ROOM — 4.2 s per candidate on the benchmark grid. Revision 3 adds
the shared-pass cut (one topology pass per check, the union only when
multi-wall nodes exist, and the resize path reusing its own preflight
artifact) with the measured numbers. Budgets in §5 already assumed the fix;
they are now achievable and proven by the passing benchmark.
Issue: #330
User-Visible: no
The first AC4 unit exercised a re-implementation of the cache algorithm, so
the stale-cache mutant patched houseplan-card.ts and the unit stayed green —
the exact "looks like protection" failure the mutation gate exists to catch,
and it caught mine. The monolith is not compiled into test-build, so the
contract is pinned by source (the #293 technique): the epoch check, the
document-identity key and the §4.6 as-is branch must be present in
_junctionLimitsIntroduced. The behavioural half of AC4 lives in the smoke's
real pointer gesture (resizeBaselineCachedPerGesture).
Issue: #330
User-Visible: no
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
r1-H1 was right twice: the rev cache never touched the candidate's migration,
and П4 is architecturally quadratic. Profiled instead of guessing: the money
is not in deepcopy (3 ms) but in _atomize (663k distance calls), and it runs
even for a document that already carries the current catalogue — 815 ms
python / 69 ms TS for a no-op migration. Two new cuts follow: §4.5 bucket
index for П4 (prototype: 372→44 ms, identical verdicts) and §4.6 current-
version documents are used as-is (an explicit revision of the "both sides
through one migration" wording, guarded by a new parity case: v9 input gives
the same verdict with and without migration).
r1-H2: AC4 now rests on the new benchmark that actually exercises the
junction code; benchmark_safe_resize is named as a non-proof. r1-M1: §9
adds the mandatory i18n/touch/risks/release sections.
Budgets in §5 are recomputed from measured post-fix prototypes with a 2-3x
allowance, including an honest row for the one-off cold legacy case.
Issue: #330
User-Visible: no
Four cuts, zero verdict changes: the ws_config_set validator chain moves to
the executor, the previous-document violation counts are cached by
config_rev, П3 builds its node index once per check in both mirrors, and the
frontend baseline is cached per config epoch. A new benchmark with budgets
pins the class of regression (O(n²) returning) in CI.
Measured on dev 2c20f2dc: a 576-atom plan costs 2.8 s in the HA event loop
per config write today; the spec's acceptance bar is ≤50 ms of loop time.
Issue: #330
User-Visible: no
Ревьюер гонял tsc, юниты и сборку заново в каждом раунде, хотя Validate на том
же SHA уже зелёный. Промпт прямо это требовал. Теперь шаг `validated` спрашивает
у Validate состояние ровно этого SHA, и доказательство такое же строгое, как у
reuse-маркеров (#208): не «недавно было зелено», а completed success на этом
коммите. После ребейза SHA другой, прогона для него нет — ревьюер честно гоняет
сам, и промпт это говорит.
Что Validate не покрывает, в примечании названо отдельно: смоки по диффу,
golden при правке рендера, инварианты на конкретной конфигурации. Иначе
экономия превратилась бы в «CI зелёный, значит всё проверено».
scripts/pre-push-gate.mjs — локальный набор: tsc, юниты, смоки по диффу
(smoke-select), мутанты по диффу (mutation-gate --changed). Замер на реальном
диапазоне 953f675~1..953f675: 46 секунд на всё вместе с двумя смоками.
Три свойства, без которых набор бесполезен: не останавливается на первом
упавшем; громко перечисляет, чего не проверял; не претендует на полноту. Бандл
не собирает — раскладывает закоммиченный dist, а свежесть проверяет сам продукт
через assertFreshDemoBundle внутри смока.
В хуке выключен по умолчанию: 20-45 секунд на каждый пуш, включая пуш одной
строки документации, — цена осознанная, включается HP_PREPUSH_GATE=1.
Дельта-промпт для spec-ревью (пункт 2) уже существует: блок «объём разбора по
дельте» из #214 покрывает оба этапа и прямо называет «дифф файла ТЗ или тела
issue для spec». Ничего не добавлял.
Issue: #343
User-Visible: no
Прежде полный трек был бесплатен, а выбор лёгкого требовал обоснования. Цена —
2.9 ревью-документа на задачу и до шести на одну issue (#329, #316, #290), при
том что Medium-находки всё равно чинятся в той же задаче без отдельного цикла.
Порог не изменился: критерии §5 те же и обязательны все одновременно. Изменилась
сторона доказательства — в S2-analysis называется критерий, который задача НЕ
проходит, если идёт полным треком. «Обычный трек» без названного критерия
обоснованием не является.
Правка идёт и в AGENTS.md: там трек описан как «shortcut для мелкой работы», а
это ровно та формулировка, из-за которой полный трек остаётся умолчанием на
практике. AGENTS.md стоит вторым в порядке доверия, поэтому без него правка
канона поведение не меняет.
Бюджет четырёх циклов, арбитраж владельца, обязательность ТЗ на полном треке и
правило «ревью до мержа» не тронуты.
Issue: #338
User-Visible: no
Бандл собирался пятью job независимо: три шарда смоков, golden, перф-смок —
каждая гоняла `bundle:sync`, то есть `tsc --noEmit` плюс rollup. Теперь его
собирает `frontend` и выкладывает артефактом, остальные скачивают и раскладывают
`bundle-sync.mjs`. Подмену артефакта отдельной проверкой ловить не нужно:
assertFreshDemoBundle сверяет вшитый в бандл отпечаток с sourceFingerprint
выкачанного дерева, и каждая браузерная job делает это перед первым кадром.
`npm ci` остаётся во всех: браузерным job нужен playwright из node_modules, а не
только бандл. Артефакт node_modules был бы медленнее `npm ci` с тёплым кэшем.
docs, process-workflow-sync, provenance и process-gate стали шагами одной job
`preflight`. Независимость сохранена намеренно: у каждого шага
continue-on-error, вердикт в конце падает и перечисляет всё упавшее сразу.
Прежняя запись «краснеет сам и не роняет остальные» продолжает действовать — на
уровне шагов, с той же гранулярностью в логе.
hacs и hassfest не тронуты: предложение сузить их до dev и тегов уже выполнено
классификатором `changes` — на ветках задач они и так идут только при правке
манифестов, а на dev фильтров нет намеренно (гейт беты требует, чтобы «зелёный
Validate» значил одно и то же).
test/validate-workflow.test.mjs закрепляет то, что в диффе строк не видно:
висячая зависимость `needs` не роняет YAML, а молча пропускает job навсегда.
Три мутанта проверены руками — висячая зависимость, вернувшаяся вторая сборка,
шаг без continue-on-error, — каждый убит.
Issue: #336
User-Visible: no
Прежде эталон принимался только из артефакта CI: растеризация шрифтов на другой
машине может отличаться, а доказать обратное было нечем. Цена — два полных
прогона на каждый визуальный фикс, при версии матрицы 48 она платится часто.
Доказательство теперь эмпирическое: среда равна раннеру, если каждая сцена,
которую менять не собирались, совпала со своим эталоном. Расхождение
растеризации спрятать нельзя — оно задевает все сцены с текстом. Ревьюер
объявляет намерение через --expect-change, всё разошедшееся помимо списка
приёмку запрещает. Поэтому неверно угаданный тег образа не может испортить
эталоны: он может только не сработать.
То же правило независимо от среды запрещает «принять всё, чтобы CI позеленел» —
именно так эталон перестаёт быть эталоном, молча и одной командой.
scripts/golden-container.mjs снимает кандидатов в образе Playwright той же
версии, что залочена в package-lock. Хозяйский node_modules прячется анонимным
томом: он собран под Windows, и npm ci внутри контейнера сломал бы дерево.
Обёртка, а не правка demo/golden/accept.mjs, — намеренно. sourceFingerprint
включает ВСЕ .mjs из demo/golden, включая accept.mjs и policy.mjs, которые
исполняются после съёмки и ни одного пикселя изменить не могут. Их правка
объявляет устаревшими бандл и оба манифеста, то есть требует ровно того двойного
цикла, который эта задача убирает. Сужение корпуса отпечатка — отдельная задача:
сам source-fingerprint.mjs в корпусе, и одна пересборка бандла неизбежна.
Issue: #334
User-Visible: no
Four independent cuts into the 2-4 hour full run, none touching the contract
"a mutant must turn its guard red":
- guardNeedsBundle: rollup runs only for guards that open the built bundle
(demo/ smokes, golden captures, bundle:sync) — 68 of 253 registry entries.
Unit and backend guards never read dist/ as a build artifact (verified
against every test that mentions dist/**: they read the git checkout or
synthetic files), so 185 mutants skip the most expensive step entirely.
- seedTestBuild + incremental tsc: the mutant worktree starts from the main
tree's warm test-build/ and .tsbuildinfo; tsc compares file hashes, not
mtimes, so the fresh checkout stays warm and only the mutated delta is
recompiled. This also speeds up the long guards that run tsc themselves.
- --changed[=range]: run only mutants whose patch files are touched by the
diff (origin/dev..HEAD by default). An empty selection is an honest success
with an explicit message — the full registry remains the pre-release
contract, per the workflow comment.
- --shard=i/n: deterministic interleaved slices; the workflow runs a 4-way
matrix, and a warm test-build step feeds every shard. Interleaving spreads
the expensive browser mutants across shards instead of clumping them.
Measured per mutant on this machine: unit 12-13 s (was ~50-70 s), backend
6 s, browser 32 s (unchanged — the bundle is genuinely needed there). Full
run estimate drops to ~70 sequential minutes, ~20 on four shards.
Unit coverage: guard classification on real registry shapes, a floor on both
classes so the split cannot silently collapse, changed-selection semantics,
and shard completeness/disjointness with an anti-clumping bound.
Issue: #332
User-Visible: no
The reviewer is right twice over. My previous commit fixed the red CI by
relaxing the contract — a guard could name a `.py` file — when the registry
already had a convention for exactly this case: every backend mutant runs
`node scripts/backend-test-guard.mjs <pattern> <file>`, which owns the python
executable choice and the `-k` selection. Bending a rule to fit my one-off is
the worse of the two possible fixes, so the contract goes back to demanding a
`.mjs` guard, and junction-limit-backend-raw-baseline now uses the helper and
targets the one test that proves the migration
(test_legacy_baseline_is_judged_after_the_same_migration).
Re-verified: registry --check clean, the mutant still catches its regression
1/1, npm test 1390 passed / 0 failed.
Issue: #329
User-Visible: no
The registry contract demanded that every guard name a `.mjs` file, which was
true until this task added the first backend mutant —
junction-limit-backend-raw-baseline is guarded by pytest, and the mutation-gate
job already installs it. My mistake: I ran `--check` and the single mutant
after adding it, but not the unit suite that owns the registry contract, so CI
caught what I should have.
The contract keeps its point: a guard must name a file that exists.
Issue: #329
User-Visible: no
AC10 was asserted, never shown. Optimize runs alignAllToGrid and
repairNearAxisRoomWalls, which move nodes by fractions of a centimetre, and
none of П1-П5 carries a margin wider than the grid step in general — so
"obviously true by construction" was not available.
Two units, both counting violations the way the write barrier does (each side
through commitWallSegmentModel first):
- the owner's fixture in legacy storage — the inherited apex is there before
Optimize, and no rule's count grows after;
- the П4 boundary — two rooms exactly 5 cm apart, where snapping could have
pulled a node under the limit, stay clean.
The first test asserts the baseline actually carries a violation, so it cannot
pass by measuring an empty plan; violationsByRule fails loudly if the space or
its catalogue goes missing, for the same reason. Spec revision 7 records the
proof and the other three review answers.
Issue: #329
User-Visible: no
The Russian guide carried the junction-limits section twice, word for word.
And both guides described Resize as silently stopping, in contrast to a toast
from drawing and Thickness — it stops AND names the rule once per gesture
(resize.limit_stopped, pinned by the smoke). Wording follows the code.
Issue: #329
User-Visible: no
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
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
The branch was rebased onto the extracted resize controller (#264), which
changes the source fingerprint the documentation screenshots are pinned to.
The images themselves are byte-identical — only the recorded fingerprint moves.
Issue: #329
User-Visible: no
Reviewed the candidate produced by the Linux CI job of run 33106626544 on
issue/329-junction-limits, where golden failed with exactly one line —
"missing-baseline sharp-apex-legacy-dark" — and accepted only that image. The
nine unrelated baselines whose bytes drifted in the same artifact were
restored to their reviewed versions, so this commit changes one picture.
Baseline-Reviewed: run 33106626544, job 98638432113 (Golden-кадры против принятых эталонов)
Release: v1.68.2
Issue: #329
User-Visible: no
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
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
Measured what Resize itself already forbids: a room cannot be squeezed below
30 cm (two 15 cm walls), so П3 and П5 are unreachable through shrinking and
the gate merely fails closed there. П4 IS reachable on a fine grid, so the
smoke drags a real handle on a 2 cm grid: two rooms 10 cm apart, a 6 cm pull
would leave 4 cm between foreign nodes, the wall stops at 6 cm and exactly one
toast names the 5 cm rule.
Spec revision 6 records both the measurement and the two corrections it forces
on AC7a: a dimmed handle cannot express a per-step limit, and the plan is NOT
byte-unchanged — the allowed part of the gesture is a legitimate edit.
Issue: #329
User-Visible: no
П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