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
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
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
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