Files
C.E.L_Slide_test2/tests/test_phase_z2_issue15_density_gate.py
T
KyeongminandClaude Opus 4.8 a57b197c37 feat(#15): density/readability gate — MDX02 false-positive 차단 (#98 추천 1·2)
기계 PASS vs 사람 '빽빽함' 판정 간극을 잡는 결정론적 밀도 신호:
- _estimate_max_card_lines: repeat 슬롯/list-of-dict 컬럼/<br> 라인 payload 추정 (v0)
- _compute_density_gate: px_per_line = (측정 zone 높이 − 56) / 카드 최대 라인
  임계값은 mdx 01~05 실측 캘리브레이션 — dense<16 / tight<45 px/line
  (mdx03 105.8=ready, mdx02 40~44=cramped, mdx01 bottom 11.5=dense 와 정렬)
- design_readiness 통합: dense/tight → warnings(needs_review) — 사람 판정 정렬
  (mdx01 11.5 도 사람 판정은 needs_review 였음 — not_ready 아님)
- presentation_ready 에 density AND 조건 추가 (#98 추천 1: 신호 통합)
- popup 승격 후 재계산 경로에도 동일 배선. 리포팅 전용 — final.html 영향 0
  (SHA parity 게이트 무접촉 확인: regression 40/40 포함 전체 1016 passed)

실전 acceptance:
- mdx02: presentation_ready=False + density_tight×2 (44.0/40.4 px/line) — 정직 표시
- mdx03: PASS + comfortable(105.8) 유지, density warnings 0
  (mdx03 design_readiness=needs_review 는 기존 reject-label warning — 회귀 아님)

후속: B5 slot metrics(#20) 성숙 시 payload 추정 → DOM 실측 교체

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:34:42 +09:00

158 lines
5.6 KiB
Python

"""issue #15 — density/readability gate 테스트 (Gitea #98 추천 1·2).
계약:
- payload 결정론 + 측정 높이만 사용 (final.html 영향 0 — 리포팅 계층)
- 임계값은 mdx 01~05 실측 캘리브레이션: dense<16 / tight<45 px/line
(사람 판정 정렬: mdx03 105.8=ready, mdx02 40~44=cramped, mdx01 11.5=dense)
- dense/tight 는 design_readiness 의 warnings(needs_review) — not_ready 아님
- presentation_ready 는 density 실패 시 False (#98 추천 1: 신호 통합)
"""
from __future__ import annotations
from src.phase_z2_pipeline import (
_compute_density_gate,
_compute_design_readiness,
_estimate_max_card_lines,
)
def _overflow(heights: dict[str, int]) -> dict:
return {"passed": True, "zones": [
{"position": p, "clientHeight": h} for p, h in heights.items()
]}
# ── _estimate_max_card_lines ────────────────────────────────────────
def test_repeat_slot_pattern_lines():
payload = {
"title": "t", "_slot_count": 3,
"pill_1_label": "라벨", "pill_1_body": ["a", "b", "c"],
"pill_2_label": "라벨", "pill_2_body": ["a"],
"pill_3_label": "", "pill_3_body": [],
}
# pill_1: label 1 + body 3 = 4
assert _estimate_max_card_lines(payload) == 4
def test_list_of_dict_columns_lines():
payload = {"pillars": [
{"label": "A", "lines": ["1", "2", "3"]},
{"label": "B", "lines": ["1"]},
]}
assert _estimate_max_card_lines(payload) == 4 # label 1 + 3 lines
def test_br_joined_string_lines():
payload = {"row_1_body": "가<br>나<br>다"}
assert _estimate_max_card_lines(payload) == 3
def test_non_card_payload_not_measurable():
assert _estimate_max_card_lines({"title": "t", "body_text": "x"}) == 0
# ── _compute_density_gate (실측 캘리브레이션 fixture) ─────────────────
def _zone(pos: str, payload: dict, template="three_persona_benefits") -> dict:
return {"position": pos, "template_id": template, "slot_payload": payload}
def test_mdx03_calibration_comfortable():
"""mdx03 left: 585px, pillar 최대 5라인 → 105.8px/line = comfortable."""
zones = [_zone("left", {"pillars": [
{"label": "L", "lines": ["1", "2", "3", "4"]},
]})]
gate = _compute_density_gate(zones, _overflow({"left": 585}))
assert gate["passed"] is True
assert gate["zones"][0]["verdict"] == "comfortable"
assert gate["zones"][0]["px_per_line"] == 105.8
def test_mdx02_calibration_tight():
"""mdx02 bottom: 339px, intro 7라인 → 40.4px/line = tight (사람: cramped)."""
zones = [_zone("bottom", {"intro_sections": [
{"label": "a", "lines": ["1", "2", "3", "4", "5", "6"]}, # 7 lines
{"label": "b", "lines": ["1", "2"]},
]})]
gate = _compute_density_gate(zones, _overflow({"bottom": 339}))
assert gate["passed"] is False
assert gate["worst_verdict"] == "tight"
assert gate["zones"][0]["px_per_line"] == 40.4
def test_mdx01_calibration_dense():
"""mdx01 bottom-right: 286px 표 20라인 → 11.5px/line = dense
(#98 사람 판정 'bottom comparison dense')."""
zones = [_zone("bottom-right", {"rows": [
{"cell": "<br>".join(str(i) for i in range(20))},
]}, template="bim_dx_comparison_table")]
gate = _compute_density_gate(zones, _overflow({"bottom-right": 286}))
assert gate["worst_verdict"] == "dense"
assert gate["zones"][0]["px_per_line"] == 11.5
def test_unmeasurable_zone_does_not_fail_gate():
zones = [_zone("top", {"title": "only-title"})]
gate = _compute_density_gate(zones, _overflow({"top": 300}))
assert gate["passed"] is True
assert gate["zones"][0]["verdict"] == "not_measurable"
def test_missing_height_not_measurable():
zones = [_zone("top", {"pill_1_label": "L", "pill_1_body": ["a"]})]
gate = _compute_density_gate(zones, _overflow({}))
assert gate["zones"][0]["verdict"] == "not_measurable"
assert gate["passed"] is True
# ── design_readiness 통합 ────────────────────────────────────────────
def _readiness(density_gate):
return _compute_design_readiness(
units=[],
rendered_text_coverage={"passed": True},
applied_render_consistency={"passed": True},
density_gate=density_gate,
)
def test_tight_zone_downgrades_to_needs_review():
gate = {"passed": False, "zones": [
{"position": "bottom", "template_id": "t", "verdict": "tight",
"px_per_line": 40.4, "estimated_max_lines": 7, "zone_height_px": 339},
]}
r = _readiness(gate)
assert r["status"] == "needs_review" # not_ready 아님 (사람 판정 정렬)
assert any(w["code"] == "density_tight" for w in r["warnings"])
def test_dense_zone_also_needs_review_not_not_ready():
gate = {"passed": False, "zones": [
{"position": "br", "template_id": "t", "verdict": "dense",
"px_per_line": 11.5, "estimated_max_lines": 20, "zone_height_px": 286},
]}
r = _readiness(gate)
assert r["status"] == "needs_review"
assert any(w["code"] == "density_dense" for w in r["warnings"])
def test_comfortable_gate_keeps_ready():
gate = {"passed": True, "zones": [
{"position": "left", "verdict": "comfortable"},
]}
r = _readiness(gate)
assert r["status"] == "ready"
def test_density_gate_none_backward_compat():
r = _compute_design_readiness(
units=[],
rendered_text_coverage={"passed": True},
applied_render_consistency={"passed": True},
)
assert r["status"] == "ready"