11건 전수 분류 결과 프로덕션 결함 0, 전부 '의도된 #98 변경 대비 낡은 테스트': - A. compute_zone_layout_cols (1건): Task 11d min-width floor(≤260px) 계약으로 갱신 - B. build_layout_css fixtures (3건): floor 반영값으로 재생성 (fixture refresh = 설계된 lock surface) - C. imp36 self-fire (1건): auto-fit collapsed 0px track 을 점유 트랙 카운트에서 제외 - D. imp88 salvage (5건): T19d(silent layout mutation 금지) 차단 계약으로 재작성 — 차단 터미널/feasibility 선행 차단/전진 금지/trace 필드 4개 측면 + 정책 차단 체인 - E. step17 case_c (1건): 실행 2단 + T28.5 no_font_shrink 정책 차단 터미널로 갱신 - 89a_pre_baseline_sha.json 재캡처 (capture 스크립트 docstring 이 명시한 선행 drift — #98 스퍼트 기간 누적, 정규 절차) 검증 게이트: pytest tests/phase_z2 + phase_z2_ai_fallback + regression = 990 passed 0 failed 참고: T19d 차단으로 _attempt_salvage_chain 의 layout_adjust 실행 코드는 도달 불가 dead code 상태 — 제거는 #16 (height-fit 재설계) 에서 판단 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,20 +19,20 @@ input:
|
||||
override_zone_geometries: null
|
||||
expected_layout_css:
|
||||
areas: '"left right-top" "left right-bottom"'
|
||||
cols: 777px 389px
|
||||
cols: 691px 475px
|
||||
rows: 286px 285px
|
||||
heights_px:
|
||||
- 286
|
||||
- 285
|
||||
widths_px:
|
||||
- 777
|
||||
- 389
|
||||
- 691
|
||||
- 475
|
||||
ratios:
|
||||
- 0.489
|
||||
- 0.487
|
||||
width_ratios:
|
||||
- 0.658
|
||||
- 0.33
|
||||
- 0.586
|
||||
- 0.403
|
||||
computation: 2d_dynamic_aggregated
|
||||
dynamic_rows: true
|
||||
dynamic_cols: true
|
||||
|
||||
@@ -19,20 +19,20 @@ input:
|
||||
override_zone_geometries: null
|
||||
expected_layout_css:
|
||||
areas: '"left-top right" "left-bottom right"'
|
||||
cols: 389px 777px
|
||||
cols: 475px 691px
|
||||
rows: 286px 285px
|
||||
heights_px:
|
||||
- 286
|
||||
- 285
|
||||
widths_px:
|
||||
- 389
|
||||
- 777
|
||||
- 475
|
||||
- 691
|
||||
ratios:
|
||||
- 0.489
|
||||
- 0.487
|
||||
width_ratios:
|
||||
- 0.33
|
||||
- 0.658
|
||||
- 0.403
|
||||
- 0.586
|
||||
computation: 2d_dynamic_aggregated
|
||||
dynamic_rows: true
|
||||
dynamic_cols: true
|
||||
|
||||
@@ -14,18 +14,18 @@ input:
|
||||
override_zone_geometries: null
|
||||
expected_layout_css:
|
||||
areas: '"left right"'
|
||||
cols: 816px 350px
|
||||
cols: 712px 454px
|
||||
rows: 1fr
|
||||
heights_px:
|
||||
- 585
|
||||
widths_px:
|
||||
- 816
|
||||
- 350
|
||||
- 712
|
||||
- 454
|
||||
ratios:
|
||||
- 1.0
|
||||
width_ratios:
|
||||
- 0.692
|
||||
- 0.297
|
||||
- 0.603
|
||||
- 0.385
|
||||
computation: content_weight_distribution_cols
|
||||
dynamic_rows: false
|
||||
dynamic_cols: true
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Column-axis weight-only solver. Mirrors compute_zone_layout for rows.
|
||||
No min_width_px contract exists in frame_contracts.yaml (verified
|
||||
during Stage 2), so column distribution is purely content_weight.
|
||||
during Stage 2). Task 11d (#98) 이후: per-zone min-width floor(≤260px)
|
||||
예약 후 잔여분을 content_weight 비례 배분 (0px track 방지).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -37,12 +38,21 @@ def test_two_equal_zones_split_evenly():
|
||||
|
||||
|
||||
def test_asymmetric_weights_distribute_by_ratio():
|
||||
# Task 11d (#98) — 순수 비례가 아니라 per-zone min-width floor(≤260px) 를
|
||||
# 먼저 예약한 뒤 잔여분만 weight 비례 배분한다 ("keep every occupied grid
|
||||
# track renderable"). 0.8/0.2 두 zone 기준 기대값을 floor 계약으로 검증.
|
||||
zones = [_zone("left", 0.8), _zone("right", 0.2)]
|
||||
result = compute_zone_layout_cols(zones)
|
||||
available = SLIDE_BODY_WIDTH - GRID_GAP
|
||||
assert sum(result["widths_px"]) == available
|
||||
# left should be ~4x right
|
||||
assert result["widths_px"][0] > result["widths_px"][1] * 3
|
||||
min_width_px = min(260, max(1, available // 2))
|
||||
distributable = available - min_width_px * 2
|
||||
expected_left = min_width_px + int(round(distributable * 0.8))
|
||||
assert result["widths_px"][0] == expected_left
|
||||
# floor 위에서도 좌우 우열은 유지되어야 한다.
|
||||
assert result["widths_px"][0] > result["widths_px"][1]
|
||||
# weight_shares 는 raw 비율 그대로 (floor 미반영) 기록된다.
|
||||
assert result["weight_shares"] == [0.8, 0.2]
|
||||
|
||||
|
||||
def test_zero_weight_guard_equal_split():
|
||||
|
||||
@@ -231,7 +231,11 @@ def _measure(drv, frame: dict, html_path: Path) -> dict:
|
||||
"return el ? getComputedStyle(el).fontSize : null;",
|
||||
frame["text_sel"],
|
||||
)
|
||||
tracks = [t for t in (cols_tpl or "").split() if t]
|
||||
# Task 11 (#98) — 일부 family 가 repeat(3, 1fr) → repeat(auto-fit, minmax())
|
||||
# 로 전환 (dynamic cardinality). auto-fit 의 collapsed track 은 computed
|
||||
# style 에 0px 로 나타나므로, IMP-36 P1 계약("점유 트랙 수")은 non-zero
|
||||
# track 만 센다.
|
||||
tracks = [t for t in (cols_tpl or "").split() if t and t != "0px"]
|
||||
return {"cols": cols_tpl, "tracks": len(tracks), "lh": lh, "fs": fs}
|
||||
|
||||
|
||||
|
||||
@@ -16,25 +16,22 @@ Stage 2 binding contract (u6):
|
||||
planner emits a frame-scoped CSS rule via
|
||||
`apply_frame_internal_fit_candidate_css`.
|
||||
|
||||
T19d (#98) 정정 — layout_adjust 는 silent topology mutation 금지로
|
||||
터미널 차단됨 (_attempt_salvage_chain 의 T19d 분기). 구 u6 의
|
||||
"layout_adjust 실행" 계약(옛 2/3/4/7/8번 테스트)은 차단 계약으로 대체.
|
||||
|
||||
Test surfaces (8 tests):
|
||||
1. `_SALVAGE_FAIL_BY_ACTION` map registers the two new actions with
|
||||
the failure_type names the failure_router (u2) cascade rows expect.
|
||||
2. `layout_adjust` PASS — out_path promoted with the swapped render,
|
||||
step records new_layout_preset, cascade exits.
|
||||
3. `layout_adjust` infeasible (no sibling for `single` preset) — step
|
||||
records failure_reason without invoking render_slide; cascade
|
||||
advances to frame_internal_fit_candidate.
|
||||
4. `layout_adjust` rerender FAIL — cascade advances to
|
||||
frame_internal_fit_candidate (which then PASSes via patched
|
||||
envelope).
|
||||
2. layout_adjust 차단 터미널 — step 없음, render 없음, out_path 보존.
|
||||
3. 차단은 feasibility 판정보다 먼저 (plan_layout_adjust 미호출).
|
||||
4. 차단 시 frame_internal_fit_candidate 로 전진하지 않음.
|
||||
5. `frame_internal_fit_candidate` PASS via patched envelope — out_path
|
||||
promoted with CSS-overlay candidate.
|
||||
promoted with CSS-overlay candidate (direct entry 경로는 존속).
|
||||
6. `frame_internal_fit_candidate` no-envelope — step records
|
||||
envelope_present=False; cascade exits via frame_reselect terminal.
|
||||
7. Full 5-step cascade all fail — loop cap (range(len(map))=5)
|
||||
respected; exactly 5 steps recorded; out_path preserved.
|
||||
8. `layout_adjust` uses no CSS-overlay path (css_override field is
|
||||
absent from the layout_adjust step; new_layout_preset is present).
|
||||
7. CSS-overlay 3단 전부 실패 → layout_adjust 차단 터미널 (steps=3).
|
||||
8. 차단 터미널 trace 필드 observability (terminal_action / rationale).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -146,21 +143,23 @@ def test_salvage_fail_map_registers_imp88_actions():
|
||||
assert "image_fit" not in _SALVAGE_FAIL_BY_ACTION
|
||||
|
||||
|
||||
# ── 2. layout_adjust PASS branch ────────────────────────────────────
|
||||
# ── 2. layout_adjust — T19d(#98) 차단 터미널 ────────────────────────
|
||||
# T19d 이후 layout_adjust 는 실행되지 않는다: "retry/salvage path 가 explicit
|
||||
# layout override 없이 final.html 의 zone topology 를 바꾸면 안 된다" (silent
|
||||
# topology mutation 금지). 아래 테스트들은 구 IMP-88 u6 의 실행 계약 대신
|
||||
# 차단 계약을 잠근다.
|
||||
|
||||
|
||||
def test_layout_adjust_pass_promotes_final_html(project_tmp, monkeypatch):
|
||||
"""initial_failure_type=font_step_insufficient routes to layout_adjust;
|
||||
plan_layout_adjust swaps horizontal-2 → vertical-2 (rows ↔ cols, swap
|
||||
priority 0). render_slide is invoked with the NEW preset; overflow
|
||||
passes → out_path promoted; step records new_layout_preset; cascade
|
||||
exits with salvage_passed=True. No CSS overlay path was used."""
|
||||
def test_layout_adjust_blocked_terminal_preserves_final_html(project_tmp, monkeypatch):
|
||||
"""initial_failure_type=font_step_insufficient 는 layout_adjust 로 라우팅
|
||||
되지만, T19d 차단으로 step 기록 없이 즉시 터미널 종료한다. render 미호출,
|
||||
out_path 원본 보존, salvage_passed=False."""
|
||||
out_path = project_tmp / "final.html"
|
||||
out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8")
|
||||
counter = _patch_render(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
_pz_pipeline, "run_overflow_check",
|
||||
lambda p: {"passed": True, "fail_reasons": []},
|
||||
lambda p: pytest.fail("layout_adjust 차단 시 overflow_check 호출 금지"),
|
||||
)
|
||||
|
||||
trace = _attempt_salvage_chain(
|
||||
@@ -173,100 +172,65 @@ def test_layout_adjust_pass_promotes_final_html(project_tmp, monkeypatch):
|
||||
)
|
||||
|
||||
assert trace["salvage_attempted"] is True
|
||||
assert trace["salvage_passed"] is True
|
||||
assert len(trace["salvage_steps"]) == 1
|
||||
step0 = trace["salvage_steps"][0]
|
||||
assert step0["action"] == "layout_adjust"
|
||||
assert step0["passed"] is True
|
||||
assert step0["new_layout_preset"] == "vertical-2"
|
||||
assert step0["plan"]["feasible"] is True
|
||||
# layout_adjust uses the distinct render path — NOT the CSS-overlay path.
|
||||
assert "css_override" not in step0
|
||||
# render_slide was invoked exactly once with the NEW preset.
|
||||
assert counter["n"] == 1
|
||||
promoted = out_path.read_text(encoding="utf-8")
|
||||
assert "ORIGINAL_BEFORE_SALVAGE" not in promoted
|
||||
assert "data-rendered-preset='vertical-2'" in promoted
|
||||
assert trace["salvage_passed"] is False
|
||||
assert trace["salvage_steps"] == []
|
||||
assert trace["salvage_terminal_action"] == (
|
||||
"layout_adjust_blocked_no_silent_layout_mutation"
|
||||
)
|
||||
assert "kept original layout" in trace["salvage_terminal_rationale"]
|
||||
assert counter["n"] == 0
|
||||
assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE"
|
||||
|
||||
|
||||
# ── 3. layout_adjust infeasible (no sibling) ────────────────────────
|
||||
# ── 3. layout_adjust 차단은 feasibility 판정보다 먼저 ────────────────
|
||||
|
||||
|
||||
def test_layout_adjust_infeasible_no_sibling_cascade_advances(project_tmp, monkeypatch):
|
||||
"""`single` preset has no render-ready unit_count=1 sibling (catalog
|
||||
design — single and grid-2x2 have no swap target). layout_adjust
|
||||
returns feasible=False; render_slide is NOT invoked; cascade
|
||||
advances to frame_internal_fit_candidate. Patched get_contract
|
||||
returns no envelope → that branch also infeasible → cascade exits
|
||||
at the frame_reselect terminal action."""
|
||||
def test_layout_adjust_blocked_before_feasibility_check(project_tmp, monkeypatch):
|
||||
"""T19d 차단은 plan_layout_adjust 호출 이전에 발생한다 — swap sibling 이
|
||||
존재하는 horizontal-2 (feasible 했을 케이스)에서도 step 기록 없이 동일
|
||||
터미널. plan 계산/렌더 어느 쪽도 실행되지 않음을 잠근다."""
|
||||
out_path = project_tmp / "final.html"
|
||||
out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8")
|
||||
counter = _patch_render(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
_pz_pipeline, "run_overflow_check",
|
||||
lambda p: pytest.fail(
|
||||
"run_overflow_check must not run when no candidate is emitted"
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
_pz_mapper, "get_contract", lambda _tid: None,
|
||||
_pz_pipeline, "plan_layout_adjust",
|
||||
lambda **kw: pytest.fail("T19d 차단 시 plan_layout_adjust 호출 금지"),
|
||||
)
|
||||
|
||||
trace = _attempt_salvage_chain(
|
||||
run_dir=project_tmp, out_path=out_path,
|
||||
slide_title="imp88-u6", slide_footer=None,
|
||||
zones_data=[{"position": "primary", "template_id": "t-only",
|
||||
"content_weight": {"score": 1.0}}],
|
||||
layout_preset="single", layout_css=_LAYOUT_CSS_GATE_PASS,
|
||||
cascade_inputs={**_ci_image(), "zone_position": "primary"},
|
||||
zones_data=_horizontal_zones(),
|
||||
layout_preset="horizontal-2", layout_css=_LAYOUT_CSS_GATE_PASS,
|
||||
cascade_inputs=_ci_image(),
|
||||
initial_failure_type="font_step_insufficient", gap_px=14,
|
||||
)
|
||||
|
||||
assert trace["salvage_attempted"] is True
|
||||
assert trace["salvage_passed"] is False
|
||||
actions = [s["action"] for s in trace["salvage_steps"]]
|
||||
assert actions == ["layout_adjust", "frame_internal_fit_candidate"]
|
||||
s0, s1 = trace["salvage_steps"]
|
||||
assert s0["plan"]["feasible"] is False
|
||||
assert "no render-ready" in (s0["plan"]["failure_reason"] or "")
|
||||
assert s0["new_layout_preset"] is None
|
||||
assert s1["plan"]["feasible"] is False
|
||||
assert s1["plan"]["envelope_present"] is False
|
||||
# No candidate ever rendered (layout_adjust infeasible → no render call;
|
||||
# frame_internal_fit_candidate infeasible → no render call).
|
||||
assert trace["salvage_steps"] == []
|
||||
assert trace["salvage_terminal_action"] == (
|
||||
"layout_adjust_blocked_no_silent_layout_mutation"
|
||||
)
|
||||
assert counter["n"] == 0
|
||||
# frame_reselect is the next routing target after
|
||||
# frame_internal_fit_candidate_insufficient — not in salvage map → terminal.
|
||||
assert trace.get("salvage_terminal_action") == "frame_reselect"
|
||||
# Original final.html unchanged.
|
||||
assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE"
|
||||
|
||||
|
||||
# ── 4. layout_adjust rerender FAIL → frame_internal_fit_candidate PASS ──
|
||||
# ── 4. layout_adjust 차단 시 frame_internal_fit 로 전진하지 않음 ─────
|
||||
|
||||
|
||||
def test_layout_adjust_fail_cascade_to_frame_internal_fit_pass(project_tmp, monkeypatch):
|
||||
"""layout_adjust feasible but post-swap overflow persists →
|
||||
failure_type=layout_adjust_insufficient → routes to
|
||||
frame_internal_fit_candidate. Patched contract provides an envelope
|
||||
variant that absorbs the excess; that branch PASSes → out_path
|
||||
promoted with the CSS-overlay candidate."""
|
||||
def test_layout_adjust_blocked_no_cascade_advance(project_tmp, monkeypatch):
|
||||
"""T19d 차단은 cascade 전진(layout_adjust_insufficient → frame_internal_
|
||||
fit_candidate)이 아니라 즉시 터미널이다. feasible envelope contract 가
|
||||
준비돼 있어도 frame_internal_fit_candidate 는 실행되지 않는다 — 구
|
||||
IMP-88 u6 의 '4. rerender FAIL → cascade advance' 계약을 대체."""
|
||||
out_path = project_tmp / "final.html"
|
||||
out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8")
|
||||
counter = _patch_render(monkeypatch)
|
||||
|
||||
# First overflow check (after layout_adjust render) FAILS,
|
||||
# second (after frame_internal_fit CSS overlay) PASSes.
|
||||
overflow_results = iter([
|
||||
{"passed": False, "fail_reasons": ["zone overflow persists"]},
|
||||
{"passed": True, "fail_reasons": []},
|
||||
])
|
||||
monkeypatch.setattr(
|
||||
_pz_pipeline, "run_overflow_check",
|
||||
lambda p: next(overflow_results),
|
||||
lambda p: pytest.fail("차단 터미널에서 overflow_check 호출 금지"),
|
||||
)
|
||||
|
||||
# Stub contract with a feasible internal_envelope variant covering 40px excess.
|
||||
# envelope 이 준비된 contract 라도 도달 자체가 없어야 한다.
|
||||
stub_contract = {
|
||||
"internal_envelope": {
|
||||
"variants": [
|
||||
@@ -289,20 +253,13 @@ def test_layout_adjust_fail_cascade_to_frame_internal_fit_pass(project_tmp, monk
|
||||
initial_failure_type="font_step_insufficient", gap_px=14,
|
||||
)
|
||||
|
||||
assert trace["salvage_passed"] is True
|
||||
assert len(trace["salvage_steps"]) == 2
|
||||
s0, s1 = trace["salvage_steps"]
|
||||
assert s0["action"] == "layout_adjust"
|
||||
assert s0["passed"] is False
|
||||
assert s0["plan"]["feasible"] is True
|
||||
assert s1["action"] == "frame_internal_fit_candidate"
|
||||
assert s1["passed"] is True
|
||||
assert s1["plan"]["selected_variant"] == "internal_grid_row"
|
||||
assert s1["css_override"]
|
||||
assert 'data-template-id="t-top"' in s1["css_override"]
|
||||
# Two render_slide calls (one per dispatched branch).
|
||||
assert counter["n"] == 2
|
||||
assert "ORIGINAL_BEFORE_SALVAGE" not in out_path.read_text(encoding="utf-8")
|
||||
assert trace["salvage_passed"] is False
|
||||
assert trace["salvage_steps"] == []
|
||||
assert trace["salvage_terminal_action"] == (
|
||||
"layout_adjust_blocked_no_silent_layout_mutation"
|
||||
)
|
||||
assert counter["n"] == 0
|
||||
assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE"
|
||||
|
||||
|
||||
# ── 5. frame_internal_fit_candidate PASS (direct entry) ─────────────
|
||||
@@ -393,15 +350,15 @@ def test_frame_internal_fit_candidate_no_envelope_cascade_terminal(project_tmp,
|
||||
assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE"
|
||||
|
||||
|
||||
# ── 7. Loop cap respected — 5 stages all fail ───────────────────────
|
||||
# ── 7. CSS-overlay 3단 실패 후 layout_adjust 차단 터미널 ─────────────
|
||||
|
||||
|
||||
def test_full_5_step_cascade_all_fail_loop_cap_respected(project_tmp, monkeypatch):
|
||||
"""Start at donor_slack_insufficient and force every cascade stage to
|
||||
fail: cross_zone (no fit_analysis), glue (excess > envelope), font_step
|
||||
(no headroom), layout_adjust (single → no sibling), frame_internal_fit
|
||||
(no contract). Loop iterates exactly len(_SALVAGE_FAIL_BY_ACTION)=5
|
||||
times → salvage_steps has 5 entries in the exact cascade order."""
|
||||
def test_cascade_stops_at_font_step_policy_block(project_tmp, monkeypatch):
|
||||
"""donor_slack_insufficient 에서 시작: cross_zone(fail) → glue(fail) →
|
||||
font_step 은 T28.5 no_font_shrink 정책 차단 — blocked step 을 기록하고
|
||||
터미널 종료한다. layout_adjust / frame_internal_fit_candidate 는 도달
|
||||
불가 (구 '5단 loop cap' 계약을 대체 — 실행 가능 cascade 는 2단 + 정책
|
||||
차단 2종)."""
|
||||
out_path = project_tmp / "final.html"
|
||||
out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8")
|
||||
counter = _patch_render(monkeypatch)
|
||||
@@ -416,7 +373,6 @@ def test_full_5_step_cascade_all_fail_loop_cap_respected(project_tmp, monkeypatc
|
||||
trace = _attempt_salvage_chain(
|
||||
run_dir=project_tmp, out_path=out_path,
|
||||
slide_title="imp88-u6", slide_footer=None,
|
||||
# single preset → layout_adjust will be infeasible (no sibling)
|
||||
zones_data=[{"position": "primary", "template_id": "t-only",
|
||||
"content_weight": {"score": 1.0}}],
|
||||
layout_preset="single", layout_css=_LAYOUT_CSS_GATE_PASS,
|
||||
@@ -432,38 +388,37 @@ def test_full_5_step_cascade_all_fail_loop_cap_respected(project_tmp, monkeypatc
|
||||
)
|
||||
|
||||
assert trace["salvage_passed"] is False
|
||||
assert len(trace["salvage_steps"]) == 5
|
||||
assert len(trace["salvage_steps"]) == 3
|
||||
actions = [s["action"] for s in trace["salvage_steps"]]
|
||||
assert actions == [
|
||||
"cross_zone_redistribute",
|
||||
"glue_compression",
|
||||
"font_step_compression",
|
||||
"layout_adjust",
|
||||
"frame_internal_fit_candidate",
|
||||
]
|
||||
# All 5 are infeasible — no candidate rendering anywhere.
|
||||
# 어떤 branch 도 candidate 렌더 없음.
|
||||
assert counter["n"] == 0
|
||||
# Loop exhausted at cap (no mid-cascade terminal_action since each
|
||||
# next_action stayed in _SALVAGE_FAIL_BY_ACTION through 5 stages).
|
||||
assert "salvage_terminal_action" not in trace
|
||||
# font_step 은 T28.5 정책 차단 step (blocked_by_policy) + 터미널.
|
||||
assert trace["salvage_steps"][2]["blocked_by_policy"] == "no_font_shrink"
|
||||
assert trace["salvage_terminal_action"] == (
|
||||
"font_step_compression_blocked_no_font_shrink"
|
||||
)
|
||||
assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE"
|
||||
|
||||
|
||||
# ── 8. layout_adjust uses the distinct render path (no CSS overlay) ──
|
||||
# ── 8. 차단 터미널 trace 필드 observability ──────────────────────────
|
||||
|
||||
|
||||
def test_layout_adjust_step_has_no_css_overlay_field(project_tmp, monkeypatch):
|
||||
"""layout_adjust's render path is qualitatively different from the
|
||||
CSS-overlay planners (glue / font_step / cross_zone / frame_internal_fit):
|
||||
it calls render_slide with the NEW preset + remapped zones_data + new
|
||||
layout_css. The step dict therefore omits `css_override` and surfaces
|
||||
`new_layout_preset` instead — observability for downstream classifiers."""
|
||||
def test_layout_adjust_block_trace_fields(project_tmp, monkeypatch):
|
||||
"""T19d 차단 터미널의 관측 계약: salvage_terminal_action 은 고정 문자열,
|
||||
salvage_terminal_rationale 은 '왜 layout 을 유지했는지'를 설명하며,
|
||||
salvage_steps 에 layout_adjust entry 가 존재하지 않는다 (구 '8. distinct
|
||||
render path' 관측 계약을 대체)."""
|
||||
out_path = project_tmp / "final.html"
|
||||
out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8")
|
||||
_patch_render(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
_pz_pipeline, "run_overflow_check",
|
||||
lambda p: {"passed": True, "fail_reasons": []},
|
||||
lambda p: pytest.fail("차단 터미널에서 overflow_check 호출 금지"),
|
||||
)
|
||||
|
||||
trace = _attempt_salvage_chain(
|
||||
@@ -475,9 +430,9 @@ def test_layout_adjust_step_has_no_css_overlay_field(project_tmp, monkeypatch):
|
||||
initial_failure_type="font_step_insufficient", gap_px=14,
|
||||
)
|
||||
|
||||
step0 = trace["salvage_steps"][0]
|
||||
assert step0["action"] == "layout_adjust"
|
||||
# Distinct render path observability contract:
|
||||
assert "css_override" not in step0
|
||||
assert "new_layout_preset" in step0
|
||||
assert "candidate_path" in step0
|
||||
assert trace["salvage_terminal_action"] == (
|
||||
"layout_adjust_blocked_no_silent_layout_mutation"
|
||||
)
|
||||
rationale = trace["salvage_terminal_rationale"]
|
||||
assert "zone positions" in rationale and "kept original layout" in rationale
|
||||
assert all(s["action"] != "layout_adjust" for s in trace["salvage_steps"])
|
||||
|
||||
@@ -203,13 +203,12 @@ def test_case_c_all_three_fail_revert_preserved(project_tmp, monkeypatch):
|
||||
False, out_path is unchanged (original final.html intact — (b)-revert
|
||||
preserved).
|
||||
|
||||
IMP-88 u6 extends the cascade depth from 3 to 5 stages (layout_adjust +
|
||||
frame_internal_fit_candidate added). When all stages are infeasible the
|
||||
cascade now runs through all five — the empty zones_data carried by
|
||||
_kwargs() makes plan_layout_adjust infeasible (length mismatch) and the
|
||||
empty resulting template_id makes plan_frame_internal_fit_candidate
|
||||
infeasible (no contract). The (b)-revert contract this test locks
|
||||
(out_path untouched + salvage_passed=False) is unchanged.
|
||||
IMP-88 u6 는 cascade 를 5단으로 확장했으나, T19d (#98) 가 layout_adjust
|
||||
를 silent topology mutation 금지로 터미널 차단함 — 실행 가능한 cascade
|
||||
는 CSS-overlay 3단(cross_zone/glue/font_step)이고, 3단 전부 실패 시
|
||||
layout_adjust 진입 시점에 차단 터미널로 종료된다. The (b)-revert
|
||||
contract this test locks (out_path untouched + salvage_passed=False)
|
||||
is unchanged.
|
||||
"""
|
||||
out_path = project_tmp / "final.html"
|
||||
out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8")
|
||||
@@ -240,21 +239,24 @@ def test_case_c_all_three_fail_revert_preserved(project_tmp, monkeypatch):
|
||||
|
||||
assert trace["salvage_attempted"] is True
|
||||
assert trace["salvage_passed"] is False
|
||||
# IMP-88 u6 — cascade depth extended from 3 to 5; see _SALVAGE_FAIL_BY_ACTION.
|
||||
assert len(trace["salvage_steps"]) == 5
|
||||
# T28.5 / T19d (#98) — font_step 은 no_font_shrink 정책 차단(blocked
|
||||
# step 기록 + 터미널), layout_adjust 는 silent topology mutation 금지
|
||||
# (도달 불가). cross_zone/glue 실패 후 font_step 차단에서 종료 —
|
||||
# IMP-88 u6 의 5단 깊이는 실행 2단 + 정책 차단으로 재편됨.
|
||||
assert len(trace["salvage_steps"]) == 3
|
||||
actions = [s["action"] for s in trace["salvage_steps"]]
|
||||
assert actions == [
|
||||
"cross_zone_redistribute",
|
||||
"glue_compression",
|
||||
"font_step_compression",
|
||||
"layout_adjust",
|
||||
"frame_internal_fit_candidate",
|
||||
]
|
||||
assert trace["salvage_steps"][2]["blocked_by_policy"] == "no_font_shrink"
|
||||
assert trace["salvage_terminal_action"] == (
|
||||
"font_step_compression_blocked_no_font_shrink"
|
||||
)
|
||||
for step in trace["salvage_steps"]:
|
||||
assert step["passed"] is False
|
||||
# layout_adjust uses a distinct render path → its step dict has no
|
||||
# css_override key (new_layout_preset is the observability field
|
||||
# instead). All other branches use the shared CSS-overlay path.
|
||||
# 전 step shared CSS-overlay path — css_override 는 미배출(None).
|
||||
assert step.get("css_override") is None
|
||||
assert step["failure_reason"]
|
||||
# No CSS / candidate emitted anywhere → no render_slide calls either.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": 2,
|
||||
"axis": "IMP-89 89-a u4 — final.html SHA baseline captured via FULL run_phase_z2_mvp1 pipeline (flag OFF / default)",
|
||||
"description": "Frozen SHA-256 of `final.html` bytes (the artifact written to disk at src/phase_z2_pipeline.py:5994-5996) captured by running the full Phase Z pipeline end-to-end for each mdx 01-05 under PHASE_Z_B4_MAPPER_SOURCE=OFF. Under flag OFF the 89-a selector `_select_mapper_template_id(plan, T)` returns `T` verbatim, so the mapper input is byte-identical to the pre-89-a legacy call shape `map_mdx_to_slots(section, unit.frame_template_id)` — the rendered HTML and therefore the final.html SHA match the pre-89-a baseline. The u4 regression test runs the same pipeline shape under flag OFF and asserts SHA equality. Regenerate only when an upstream mapper/render/template delta is deliberately reviewed and accepted.",
|
||||
"captured_at_utc": "2026-05-27T04:19:30Z",
|
||||
"captured_at_utc": "2026-07-03T03:39:01Z",
|
||||
"renderer": {
|
||||
"entrypoint": "src.phase_z2_pipeline.run_phase_z2_mvp1",
|
||||
"write_site": "src/phase_z2_pipeline.py:5994-5996",
|
||||
@@ -19,37 +19,37 @@
|
||||
"01.mdx": {
|
||||
"mdx_file": "01.mdx",
|
||||
"run_id": "89a_baseline_01",
|
||||
"final_html_size_bytes": 29959,
|
||||
"sha256": "62d793166ca4cdd8d8d1d134d8659b43dce95bb958e7efd6ce0e72619b3c335d",
|
||||
"final_html_size_bytes": 38731,
|
||||
"sha256": "749ebb626e284a62d37b5be32af97c61ae2a5b8d4226d1c3fa90f04c843d9fef",
|
||||
"pipeline_exit_code": null
|
||||
},
|
||||
"02.mdx": {
|
||||
"mdx_file": "02.mdx",
|
||||
"run_id": "89a_baseline_02",
|
||||
"final_html_size_bytes": 25789,
|
||||
"sha256": "333defb22f5da940b0bb3b3563730d12bd901d7b5d870a2aaef3f0c929d2dad1",
|
||||
"final_html_size_bytes": 42127,
|
||||
"sha256": "835d67eb7003bbcc55f3ea9ea73e6c781bb3b51d021a860200876c1d93cecfd2",
|
||||
"pipeline_exit_code": null
|
||||
},
|
||||
"03.mdx": {
|
||||
"mdx_file": "03.mdx",
|
||||
"run_id": "89a_baseline_03",
|
||||
"final_html_size_bytes": 40550,
|
||||
"sha256": "d84e32b402cfbdcab6025b584b5e2657766f11b3fc110c217be0551e39f3f516",
|
||||
"final_html_size_bytes": 40217,
|
||||
"sha256": "44abaaf8fd0737002188c7af0ae3bd0dd281b313f74b8a50ddfd4aa0ddef3eb7",
|
||||
"pipeline_exit_code": null
|
||||
},
|
||||
"04.mdx": {
|
||||
"mdx_file": "04.mdx",
|
||||
"run_id": "89a_baseline_04",
|
||||
"final_html_size_bytes": 28725,
|
||||
"sha256": "da3668d76fc582a1a26ad930b54a8abb7079969ebf91e79cfe7dd273ec4a0e95",
|
||||
"pipeline_exit_code": null
|
||||
"final_html_size_bytes": 40206,
|
||||
"sha256": "8ba35bc6642b0e651cd46b9216070bd49052b016807611d9771fd630ca7a2613",
|
||||
"pipeline_exit_code": 1
|
||||
},
|
||||
"05.mdx": {
|
||||
"mdx_file": "05.mdx",
|
||||
"run_id": "89a_baseline_05",
|
||||
"final_html_size_bytes": 21739,
|
||||
"sha256": "5baa8478a1316403dd3e7b7bccaaaee20fe6ac7738ea7ddd2af2f19b4b294f2c",
|
||||
"pipeline_exit_code": 1
|
||||
"final_html_size_bytes": 36844,
|
||||
"sha256": "397f238df2d23c7f3371d9032de5fbae4fd37aca116bb8b62e8fd36c4255c26d",
|
||||
"pipeline_exit_code": null
|
||||
}
|
||||
},
|
||||
"total_files": 5
|
||||
|
||||
Reference in New Issue
Block a user