기계 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>
This commit is contained in:
1 parent
9a72e7de3e
commit
a57b197c37
2 files changed
+286
No files matched your search
@@ -5770,11 +5770,111 @@ def _compute_applied_render_consistency(
|
||||
}
|
||||
|
||||
|
||||
# ── issue #15 — density/readability gate (Gitea #98 추천 1·2) ──────────
|
||||
# 기계 PASS vs 사람 "빽빽함" 판정의 간극(false-positive, MDX02 최악)을 잡는
|
||||
# 결정론적 신호. B5 slot metrics(#20) 부재 상태의 v0 휴리스틱:
|
||||
# px_per_line = (zone 측정 높이 − 오버헤드) / 카드당 최대 추정 라인 수
|
||||
# 임계값은 mdx 01~05 실측 캘리브레이션 (2026-07-06, 사람 판정과 정렬):
|
||||
# mdx03 105.8(ready) / mdx05 50~64(density 문제 아님) / mdx02 40~44(cramped)
|
||||
# / mdx04 top 37(dense) / mdx01 bottom-right 11.5("bottom comparison dense")
|
||||
_DENSITY_PX_PER_LINE_DENSE = 16.0 # 물리적 한 줄 높이 미만 — 확실한 과밀
|
||||
_DENSITY_PX_PER_LINE_TIGHT = 45.0 # 사람 판정 cramped 경계 (mdx02 44.0 포함)
|
||||
_DENSITY_ZONE_OVERHEAD_PX = 56 # zone title + padding 추정 오버헤드
|
||||
_CARD_SLOT_RE = re.compile(r".+?_(\d+)_(label|body)$")
|
||||
|
||||
|
||||
def _estimate_max_card_lines(slot_payload: dict) -> int:
|
||||
"""카드/행 단위 최대 라인 수 추정 (휴리스틱 v0 — payload 결정론).
|
||||
|
||||
인식 형태: ``{prefix}_{n}_{label|body}`` repeat 슬롯, ``pillars`` 등
|
||||
list-of-dict 컬럼, str 값의 ``<br>`` 라인. B5 marker(#20) 성숙 시 실측
|
||||
DOM 라인으로 대체 예정.
|
||||
"""
|
||||
if not isinstance(slot_payload, dict):
|
||||
return 0
|
||||
cards: dict[str, int] = {}
|
||||
for key, value in slot_payload.items():
|
||||
if key in ("title", "_slot_count") or not value:
|
||||
continue
|
||||
m = _CARD_SLOT_RE.match(str(key))
|
||||
if m:
|
||||
n_lines = (
|
||||
len(value) if isinstance(value, list)
|
||||
else str(value).count("<br>") + 1
|
||||
)
|
||||
cards[m.group(1)] = cards.get(m.group(1), 0) + n_lines
|
||||
continue
|
||||
if isinstance(value, list) and value and isinstance(value[0], dict):
|
||||
for i, card in enumerate(value):
|
||||
n = 0
|
||||
for v in card.values():
|
||||
if not v:
|
||||
continue
|
||||
n += len(v) if isinstance(v, list) else str(v).count("<br>") + 1
|
||||
cards[f"{key}[{i}]"] = n
|
||||
return max(cards.values()) if cards else 0
|
||||
|
||||
|
||||
def _compute_density_gate(zones_data: list[dict], overflow: dict) -> dict:
|
||||
"""issue #15 — per-zone 밀도 판정 (dense / tight / comfortable).
|
||||
|
||||
측정 높이(run_overflow_check 의 clientHeight)와 payload 라인 추정만 사용
|
||||
— 렌더/최종 HTML 에 영향 없는 순수 리포팅 계층 (slide_status 전용).
|
||||
라인 추정 불가 zone(카드 패턴 아님)은 판정 제외 (not_measurable).
|
||||
"""
|
||||
heights = {
|
||||
str(z.get("position")): z.get("clientHeight")
|
||||
for z in (overflow.get("zones") or [])
|
||||
if isinstance(z, dict)
|
||||
}
|
||||
zone_records: list[dict] = []
|
||||
worst = "comfortable"
|
||||
for zone in zones_data or []:
|
||||
pos = str(zone.get("position"))
|
||||
payload = zone.get("slot_payload") or {}
|
||||
max_lines = _estimate_max_card_lines(payload)
|
||||
height = heights.get(pos)
|
||||
rec: dict = {
|
||||
"position": pos,
|
||||
"template_id": zone.get("template_id"),
|
||||
"estimated_max_lines": max_lines,
|
||||
}
|
||||
if not max_lines or not isinstance(height, (int, float)) or height <= 0:
|
||||
rec["verdict"] = "not_measurable"
|
||||
zone_records.append(rec)
|
||||
continue
|
||||
px_per_line = (float(height) - _DENSITY_ZONE_OVERHEAD_PX) / max_lines
|
||||
rec["zone_height_px"] = int(height)
|
||||
rec["px_per_line"] = round(px_per_line, 1)
|
||||
if px_per_line < _DENSITY_PX_PER_LINE_DENSE:
|
||||
rec["verdict"] = "dense"
|
||||
worst = "dense"
|
||||
elif px_per_line < _DENSITY_PX_PER_LINE_TIGHT:
|
||||
rec["verdict"] = "tight"
|
||||
if worst != "dense":
|
||||
worst = "tight"
|
||||
else:
|
||||
rec["verdict"] = "comfortable"
|
||||
zone_records.append(rec)
|
||||
return {
|
||||
"passed": worst == "comfortable",
|
||||
"worst_verdict": worst,
|
||||
"zones": zone_records,
|
||||
"thresholds": {
|
||||
"dense_px_per_line": _DENSITY_PX_PER_LINE_DENSE,
|
||||
"tight_px_per_line": _DENSITY_PX_PER_LINE_TIGHT,
|
||||
"zone_overhead_px": _DENSITY_ZONE_OVERHEAD_PX,
|
||||
},
|
||||
"heuristic": "payload_line_estimate_v0",
|
||||
}
|
||||
|
||||
|
||||
def _compute_design_readiness(
|
||||
*,
|
||||
units: list[CompositionUnit],
|
||||
rendered_text_coverage: dict,
|
||||
applied_render_consistency: dict,
|
||||
density_gate: Optional[dict] = None,
|
||||
) -> dict:
|
||||
"""T20f: separate technical PASS from design-readiness.
|
||||
|
||||
@@ -5841,6 +5941,23 @@ def _compute_design_readiness(
|
||||
"detail": "The rendered frame is selected from a reject/provisional path.",
|
||||
})
|
||||
|
||||
# issue #15 — density 신호 통합 (#98 추천 1: readiness 신호 통합).
|
||||
# 사람 판정 캘리브레이션상 dense/tight 모두 needs_review 계층 (not_ready
|
||||
# 아님 — mdx01 bottom 11.5px/line 도 사람 판정은 needs_review 였음).
|
||||
if density_gate and not density_gate.get("passed", True):
|
||||
for z in density_gate.get("zones") or []:
|
||||
if z.get("verdict") in ("dense", "tight"):
|
||||
warnings.append({
|
||||
"position": z.get("position"),
|
||||
"template_id": z.get("template_id"),
|
||||
"code": f"density_{z['verdict']}",
|
||||
"detail": (
|
||||
f"{z.get('px_per_line')}px/line "
|
||||
f"(max {z.get('estimated_max_lines')} lines/card in "
|
||||
f"{z.get('zone_height_px')}px zone) — 카드 과밀 신호"
|
||||
),
|
||||
})
|
||||
|
||||
status = "ready"
|
||||
if warnings:
|
||||
status = "needs_review"
|
||||
@@ -11025,10 +11142,14 @@ def run_phase_z2_mvp1(
|
||||
debug_zones=debug_zones,
|
||||
)
|
||||
slide_status["applied_render_consistency"] = applied_render_consistency
|
||||
# issue #15 — density gate (payload 결정론 + 측정 높이, 리포팅 전용)
|
||||
density_gate = _compute_density_gate(zones_data, overflow)
|
||||
slide_status["density_gate"] = density_gate
|
||||
design_readiness = _compute_design_readiness(
|
||||
units=units,
|
||||
rendered_text_coverage=rendered_text_coverage,
|
||||
applied_render_consistency=applied_render_consistency,
|
||||
density_gate=density_gate,
|
||||
)
|
||||
slide_status["design_readiness"] = design_readiness
|
||||
if not rendered_text_coverage.get("passed"):
|
||||
@@ -11092,10 +11213,13 @@ def run_phase_z2_mvp1(
|
||||
and applied_render_consistency.get("passed")
|
||||
)
|
||||
slide_status["visual_pass"] = bool(overflow.get("passed"))
|
||||
# issue #15 (#98 추천 1) — presentation_ready 에 density 통합: 기계 PASS
|
||||
# 인데 사람 눈에 빽빽한 false-positive(MDX02) 차단.
|
||||
slide_status["presentation_ready"] = bool(
|
||||
slide_status["technical_pass"]
|
||||
and slide_status["visual_pass"]
|
||||
and presentation_fit.get("passed")
|
||||
and density_gate.get("passed", True)
|
||||
)
|
||||
slide_status["presentation_reselection"] = _build_t28_5c_reselection_trace(
|
||||
units=units,
|
||||
@@ -11145,10 +11269,13 @@ def run_phase_z2_mvp1(
|
||||
rendered_html=final_html_for_status,
|
||||
debug_zones=debug_zones,
|
||||
)
|
||||
# issue #15 — popup 승격 후 재계산 경로에도 density gate 동일 배선
|
||||
density_gate = _compute_density_gate(zones_data, overflow)
|
||||
design_readiness = _compute_design_readiness(
|
||||
units=units,
|
||||
rendered_text_coverage=rendered_text_coverage,
|
||||
applied_render_consistency=applied_render_consistency,
|
||||
density_gate=density_gate,
|
||||
)
|
||||
presentation_fit = _build_presentation_fit_report(
|
||||
overflow=overflow,
|
||||
@@ -11158,6 +11285,7 @@ def run_phase_z2_mvp1(
|
||||
slide_status["text_coverage_passed"] = bool(rendered_text_coverage.get("passed"))
|
||||
slide_status["forbidden_rendered_syntax"] = forbidden_rendered_syntax
|
||||
slide_status["applied_render_consistency"] = applied_render_consistency
|
||||
slide_status["density_gate"] = density_gate
|
||||
slide_status["design_readiness"] = design_readiness
|
||||
slide_status["presentation_fit"] = presentation_fit
|
||||
slide_status["technical_pass"] = bool(
|
||||
@@ -11172,6 +11300,7 @@ def run_phase_z2_mvp1(
|
||||
slide_status["technical_pass"]
|
||||
and slide_status["visual_pass"]
|
||||
and presentation_fit.get("passed")
|
||||
and density_gate.get("passed", True)
|
||||
)
|
||||
slide_status["presentation_reselection"] = _build_t28_5c_reselection_trace(
|
||||
units=units,
|
||||
|
||||
Reference in new issue
Block a user