feat(#14): design_adaptation_plan apply 계층 — AI 구조 plan 을 code 가 verbatim builder 재실행으로 적용
Gitea #98 Task 12 완결. 롤백된 _emergency_p4_ai_redistribute docstring 이 명시한 승인 아키텍처 5단계('AI: plan only → CODE: apply') 구현. - _apply_design_adaptation_plan: op 분류 + repeat-count op 만 builder 재실행 적용 - set/increase_repeat_count → _emergency_p4b_build_verbatim_slot_payload (override_slot_count, repeat 패턴 4종) — max(override, len(groups)) floor 로 원문 그룹 절대 비손실 (Task 11c) - compact_spacing → policy_skip (T28.5 no_font_shrink) / rebalance_zone_ratio → policy_skip (CSS code 소유) / split_group(frame_repeat) → unsupported (#26) / frame_contract 변이 → unsupported (catalog 소유) — 전 op 사유와 함께 design_plan_op_status 기록 (침묵 drop 금지, PZ-4) - count 필드 어휘 관용 추출 (count/repeat_count/to/target_count — 실측 변동) - prompts.py: task12 계약에 op 스키마 예시 명시 - 시그니처: units 옵션 kwarg (구 호출 backward compat — 미제공 시 명시 status) 검증: - 신규 테스트 13종 + 영향권/전체 게이트 1003 passed 0 failed - mdx04 live: 04-1 apply=applied:design_adaptation_plan_no_delta (AI plan 이 apply 계층 최초 통과 — count 5 = 현행 동일이라 no_delta 가 정답), coverage ok Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
"""issue #14 — design_adaptation_plan apply 계층 테스트 (Gitea #98 Task 12 완결).
|
||||
|
||||
계약 (승인 아키텍처 — _emergency_p4_ai_redistribute docstring 5단계):
|
||||
AI 는 구조 조정 plan 만 내고, CODE 가 verbatim builder 재실행으로 적용한다.
|
||||
- set_repeat_count / increase_repeat_count 만 effective (builder 재실행)
|
||||
- override count 는 max(override, len(groups)) floor — 원문 그룹 절대 비손실
|
||||
- compact_spacing / rebalance_zone_ratio 는 policy_skip (T28.5 no_font_shrink,
|
||||
CSS code 소유), split_group(frame_repeat) 은 unsupported (#26 axis)
|
||||
- 모든 op 는 design_plan_op_status 에 분류 사유와 함께 기록 (침묵 drop 금지)
|
||||
- partial_overrides 기존 경로 및 구 시그니처(units 미전달) backward compat 유지
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.phase_z2_pipeline import (
|
||||
_apply_ai_repair_proposals_to_zones,
|
||||
_apply_design_adaptation_plan,
|
||||
_emergency_p4b_build_verbatim_slot_payload,
|
||||
)
|
||||
|
||||
|
||||
_TEMPLATE = "pre_construction_model_info_stacked" # repeat 패턴 (pill_{n}, base 5)
|
||||
|
||||
|
||||
def _unit(n_groups: int = 2):
|
||||
lines = []
|
||||
for i in range(1, n_groups + 1):
|
||||
lines.append(f"- **그룹{i} 라벨**")
|
||||
lines.append(f"- 항목 {i}-1")
|
||||
lines.append(f"- 항목 {i}-2")
|
||||
lines.append("")
|
||||
return SimpleNamespace(title="테스트 제목", raw_content="\n".join(lines))
|
||||
|
||||
|
||||
def _zone(unit) -> dict:
|
||||
payload = _emergency_p4b_build_verbatim_slot_payload(unit, _TEMPLATE)
|
||||
assert payload is not None, "전제: builder + partial 존재"
|
||||
return {"position": "top", "template_id": _TEMPLATE, "slot_payload": payload}
|
||||
|
||||
|
||||
def _record(operations: list, unit_index: int = 0) -> dict:
|
||||
return {
|
||||
"unit_index": unit_index,
|
||||
"source_section_ids": ["04-1"],
|
||||
"proposal": {
|
||||
"proposal_kind": "design_adaptation_plan",
|
||||
"payload": {"operations": operations},
|
||||
"rationale": "test",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── set_repeat_count 적용 ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_set_repeat_count_expands_padding_slots():
|
||||
unit = _unit(n_groups=2)
|
||||
zone = _zone(unit)
|
||||
assert zone["slot_payload"]["_slot_count"] == 5 # base 5 > groups 2
|
||||
rec = _record([{"op": "set_repeat_count", "target": "pills", "count": 7}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "applied:design_adaptation_plan"
|
||||
assert zone["slot_payload"]["_slot_count"] == 7
|
||||
# 원문 텍스트 verbatim 보존 (AI 텍스트 유입 없음)
|
||||
assert zone["slot_payload"]["pill_1_label"] == "그룹1 라벨"
|
||||
assert zone["slot_payload"]["pill_7_label"] == "" # padding slot
|
||||
|
||||
|
||||
def test_repeat_count_never_truncates_source_groups():
|
||||
"""count 1 < groups 4 → floor 는 len(groups) — 원문 무손실 (Task 11c)."""
|
||||
unit = _unit(n_groups=4)
|
||||
zone = _zone(unit)
|
||||
rec = _record([{"op": "set_repeat_count", "target": "pills", "count": 1}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert zone["slot_payload"]["_slot_count"] == 4
|
||||
assert zone["slot_payload"]["pill_4_label"] == "그룹4 라벨"
|
||||
|
||||
|
||||
def test_no_delta_when_count_matches_current_build():
|
||||
unit = _unit(n_groups=2)
|
||||
zone = _zone(unit)
|
||||
before = dict(zone["slot_payload"])
|
||||
rec = _record([{"op": "set_repeat_count", "target": "pills", "count": 5}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "applied:design_adaptation_plan_no_delta"
|
||||
assert zone["slot_payload"] == before
|
||||
|
||||
|
||||
def test_increase_repeat_count_alias_and_repeat_count_key():
|
||||
unit = _unit(n_groups=2)
|
||||
zone = _zone(unit)
|
||||
rec = _record([{"op": "increase_repeat_count", "repeat_count": 6}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "applied:design_adaptation_plan"
|
||||
assert zone["slot_payload"]["_slot_count"] == 6
|
||||
|
||||
|
||||
# ── 정책 분류 (침묵 drop 금지) ────────────────────────────────────────
|
||||
|
||||
|
||||
def test_policy_ops_are_classified_not_silently_dropped():
|
||||
unit = _unit()
|
||||
zone = _zone(unit)
|
||||
before = dict(zone["slot_payload"])
|
||||
rec = _record([
|
||||
{"op": "preserve_source_order", "target": "pills"},
|
||||
{"op": "compact_spacing", "target": ".f9b__pill", "line_height": 1.05},
|
||||
{"op": "rebalance_zone_ratio", "target": ".f9b__pill-rows"},
|
||||
{"op": "split_group_across_repeat_blocks", "target": "frame_repeat",
|
||||
"repeat_count": 2},
|
||||
{"op": "use_repeatable_frame", "target": "frame"},
|
||||
{"op": "totally_unknown_op"},
|
||||
])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "design_plan_no_applicable_op"
|
||||
assert zone["slot_payload"] == before # zone 불변
|
||||
statuses = {s["op"]: s["status"] for s in rec["design_plan_op_status"]}
|
||||
assert statuses["preserve_source_order"] == "already_default:task9_source_order"
|
||||
assert statuses["compact_spacing"] == "policy_skip:no_font_shrink_spacing_lock"
|
||||
assert statuses["rebalance_zone_ratio"] == "policy_skip:css_code_owned"
|
||||
assert statuses["split_group_across_repeat_blocks"] == (
|
||||
"unsupported:multi_instance_requires_deck_split"
|
||||
)
|
||||
assert statuses["use_repeatable_frame"] == "unsupported:frame_swap_locked"
|
||||
assert statuses["totally_unknown_op"] == "unsupported_op"
|
||||
|
||||
|
||||
def test_invalid_count_recorded():
|
||||
unit = _unit()
|
||||
zone = _zone(unit)
|
||||
rec = _record([{"op": "set_repeat_count", "count": "다섯"}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "design_plan_no_applicable_op"
|
||||
assert rec["design_plan_op_status"][0]["status"] == "invalid_count"
|
||||
|
||||
|
||||
def test_count_field_vocabulary_tolerance():
|
||||
"""실측: 모델이 count 대신 to/repeat_count/digit-string 을 씀 — 관용 추출."""
|
||||
unit = _unit(n_groups=2)
|
||||
zone = _zone(unit)
|
||||
rec = _record([{"op": "increase_repeat_count", "target": "pills",
|
||||
"from": 5, "to": 6}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "applied:design_adaptation_plan"
|
||||
assert zone["slot_payload"]["_slot_count"] == 6
|
||||
|
||||
|
||||
def test_frame_contract_mutation_classified_unsupported():
|
||||
"""실측: target=frame_contract.cardinality + add_sub_zone — catalog 소유
|
||||
변이는 적용 불가로 정직 분류 (invalid_count 로 오분류 금지)."""
|
||||
unit = _unit()
|
||||
zone = _zone(unit)
|
||||
before = dict(zone["slot_payload"])
|
||||
rec = _record([{
|
||||
"op": "increase_repeat_count",
|
||||
"target": "frame_contract.cardinality",
|
||||
"details": {"from": {"strict": 3}, "to": {"strict": 4},
|
||||
"add_sub_zone": {"id": "item_4_body"}},
|
||||
}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "design_plan_no_applicable_op"
|
||||
assert rec["design_plan_op_status"][0]["status"] == (
|
||||
"unsupported:contract_mutation_requires_catalog_change"
|
||||
)
|
||||
assert zone["slot_payload"] == before
|
||||
|
||||
|
||||
# ── 가드 / backward compat ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_units_not_provided_backward_compat():
|
||||
"""구 시그니처 호출 (units 미전달) — 예외 없이 명시적 status 기록."""
|
||||
unit = _unit()
|
||||
zone = _zone(unit)
|
||||
rec = _record([{"op": "set_repeat_count", "count": 6}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone]) # units 없음
|
||||
|
||||
assert rec["apply_status"] == "design_plan_unit_unavailable"
|
||||
|
||||
|
||||
def test_builder_unavailable_template():
|
||||
unit = _unit()
|
||||
zone = {"position": "top", "template_id": "no_such_template",
|
||||
"slot_payload": {"title": "x"}}
|
||||
rec = _record([{"op": "set_repeat_count", "count": 6}])
|
||||
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[unit])
|
||||
|
||||
assert rec["apply_status"] == "design_plan_builder_unavailable:no_such_template"
|
||||
|
||||
|
||||
def test_no_zone_match_status():
|
||||
unit = _unit()
|
||||
rec = _record([{"op": "set_repeat_count", "count": 6}])
|
||||
_apply_ai_repair_proposals_to_zones([rec], [], [], units=[unit])
|
||||
assert rec["apply_status"] == "no_zone_match"
|
||||
|
||||
|
||||
def test_partial_overrides_path_unchanged():
|
||||
"""기존 partial_overrides 경로 회귀 없음 (units kwarg 추가와 무관)."""
|
||||
zone = {"position": "top", "template_id": _TEMPLATE,
|
||||
"slot_payload": {"title": "old"}}
|
||||
rec = {
|
||||
"unit_index": 0,
|
||||
"proposal": {
|
||||
"proposal_kind": "partial_overrides",
|
||||
"payload": {"slots": {"title": "new"}},
|
||||
},
|
||||
}
|
||||
_apply_ai_repair_proposals_to_zones([rec], ["top"], [zone], units=[_unit()])
|
||||
assert rec["apply_status"] == "applied:partial_overrides"
|
||||
assert zone["slot_payload"]["title"] == "new"
|
||||
|
||||
|
||||
def test_direct_handler_unit_none():
|
||||
rec = _record([{"op": "set_repeat_count", "count": 6}])
|
||||
zone = {"position": "top", "template_id": _TEMPLATE, "slot_payload": {}}
|
||||
_apply_design_adaptation_plan(rec, None, zone)
|
||||
assert rec["apply_status"] == "design_plan_unit_unavailable"
|
||||
Reference in New Issue
Block a user