feat(#63): IMP-34 R1 donor capacity measured bound (u1+u2)

Bound donor capacity in plan_zone_ratio_retry by min(static_slack,
max(0, clientHeight-scrollHeight)) when both Step 14 measured fields
are present; fall back to static contract slack when absent. Prevents
the donor from being over-allocated when full-but-not-overflowing,
avoiding a wasted Selenium rerender before cascade falls to
cross_zone_redistribute.

- src/phase_z2_retry.py: planner block L122-157 only; donor filter
  (L107-112), slack<=0 gate, base_plan, greedy aggregation untouched.
  Adds measured_empty_px + slack_bound_source telemetry to
  donor_candidates_considered (additive only).
- tests/phase_z2/test_phase_z2_retry_measured_bound.py: 5-axis
  regression (static_fallback / measured<static / measured>=static /
  measured==0 excludes / filter+bool guard).

Guardrails honored: V4 rank-1 frame lock preserved, no frame_swap,
no spacing/padding/gap/line-height/font shrink, no content drop,
no MDX 03/04/05 branching, no Step 14 schema mutation. Static
fallback idempotent when measured fields absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:37:41 +09:00
parent a06dd3d4b0
commit dceb10129f
2 changed files with 202 additions and 1 deletions

View File

@@ -120,11 +120,30 @@ def plan_zone_ratio_retry(
continue
# rule 4-(d) 현재 height > min_height
# IMP-34 u1: donor capacity bounded by measured empty space
# (clientHeight - scrollHeight from Step 14) when both fields are present,
# falling back to static contract slack when absent. Prevents the donor
# from being over-allocated when it is already full but not overflowing.
height = zones_before.get(pos)
min_h = zone_min_by_pos.get(pos)
if height is None or min_h is None:
continue
slack = height - min_h
static_slack = height - min_h
client_h = zinfo.get("clientHeight")
scroll_h = zinfo.get("scrollHeight")
if (
isinstance(client_h, (int, float))
and isinstance(scroll_h, (int, float))
and not isinstance(client_h, bool)
and not isinstance(scroll_h, bool)
):
measured_empty_px = max(0, int(client_h) - int(scroll_h))
slack = min(static_slack, measured_empty_px)
slack_bound_source = "measured_bound"
else:
measured_empty_px = None
slack = static_slack
slack_bound_source = "static_fallback"
if slack <= 0:
continue
@@ -134,6 +153,8 @@ def plan_zone_ratio_retry(
"min_height": min_h,
"slack": slack,
"capacity_fit_status": cap_status,
"measured_empty_px": measured_empty_px,
"slack_bound_source": slack_bound_source,
})
# rule 4-(f) 여러 후보면 slack 가장 큰 것부터