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:
1 parent
2c7881c5cc
commit
9a72e7de3e
3 files changed
+380
-12
No files matched your search
@@ -91,6 +91,13 @@ def build_ai_fallback_prompt(
|
||||
"compact_spacing",
|
||||
"use_repeatable_frame",
|
||||
],
|
||||
# issue #14 — repeat-count op 의 정확한 스키마 (code 가 적용하는
|
||||
# 유일한 op 계열). count 는 top-level 정수 필드 "count" 사용.
|
||||
"operation_schema_example": {
|
||||
"op": "set_repeat_count",
|
||||
"target": "<repeat_axis_name>",
|
||||
"count": 5,
|
||||
},
|
||||
"forbidden_content_fields": [
|
||||
"text",
|
||||
"title",
|
||||
|
||||
+136
-12
@@ -663,11 +663,20 @@ def _build_verbatim_compare_table_2col(unit) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _emergency_p4b_build_verbatim_slot_payload(unit, template_id: str) -> Optional[dict]:
|
||||
def _emergency_p4b_build_verbatim_slot_payload(
|
||||
unit, template_id: str, override_slot_count: Optional[int] = None,
|
||||
) -> Optional[dict]:
|
||||
"""Per-frame verbatim slot_payload builder. AI 없음, 원문 보존만.
|
||||
|
||||
Returns slot_payload dict OR None (해당 frame 의 builder 없음 OR Jinja partial
|
||||
없음 → 기존 adapter_needed path 로 fallback).
|
||||
|
||||
override_slot_count (issue #14) — design_adaptation_plan 의
|
||||
set_repeat_count / increase_repeat_count op 적용용. repeat 패턴
|
||||
builder(cards_n_grid 계열)의 base slot_count 를 대체한다. 최종 count 는
|
||||
max(override, len(groups)) 이므로 **원문 그룹은 절대 잘리지 않는다**
|
||||
(Task 11c no-truncate) — override 는 빈 padding slot 의 개수만 조정.
|
||||
비-repeat builder 에는 영향 없음.
|
||||
"""
|
||||
# Partial template 존재 check — visual_pending frame (contract 있지만 .html 없음)
|
||||
# 차단. _is_visual_pending 기준 + 직접 file 존재 확인 동시.
|
||||
@@ -685,7 +694,7 @@ def _emergency_p4b_build_verbatim_slot_payload(unit, template_id: str) -> Option
|
||||
return _build_verbatim_cycle_intersect_3(unit)
|
||||
if template_id == "construction_bim_three_usage":
|
||||
return _build_verbatim_cards_n_grid(
|
||||
unit, slot_count=3,
|
||||
unit, slot_count=override_slot_count or 3,
|
||||
label_pattern="category_{n}_label",
|
||||
body_pattern="category_{n}_body",
|
||||
)
|
||||
@@ -693,19 +702,19 @@ def _emergency_p4b_build_verbatim_slot_payload(unit, template_id: str) -> Option
|
||||
return _build_verbatim_compare_table_2col(unit)
|
||||
if template_id == "bim_issues_quadrant_four":
|
||||
return _build_verbatim_cards_n_grid(
|
||||
unit, slot_count=4,
|
||||
unit, slot_count=override_slot_count or 4,
|
||||
label_pattern="quadrant_{n}_label",
|
||||
body_pattern="quadrant_{n}_body",
|
||||
)
|
||||
if template_id == "sw_dependency_four_problems":
|
||||
return _build_verbatim_cards_n_grid(
|
||||
unit, slot_count=4,
|
||||
unit, slot_count=override_slot_count or 4,
|
||||
label_pattern="problem_{n}_label",
|
||||
body_pattern="problem_{n}_body",
|
||||
)
|
||||
if template_id == "pre_construction_model_info_stacked":
|
||||
return _build_verbatim_cards_n_grid(
|
||||
unit, slot_count=5,
|
||||
unit, slot_count=override_slot_count or 5,
|
||||
label_pattern="pill_{n}_label",
|
||||
body_pattern="pill_{n}_body",
|
||||
)
|
||||
@@ -2112,11 +2121,113 @@ def _run_step12_ai_repair(units) -> list[dict]:
|
||||
|
||||
_REJECT_SUPPORTED_PROPOSAL_KINDS: frozenset[str] = frozenset({"partial_overrides"})
|
||||
|
||||
# ── issue #14 — design_adaptation_plan apply (Gitea #98 Task 12 완결) ──
|
||||
# 롤백된 _emergency_p4_ai_redistribute docstring 이 명시한 승인 아키텍처의
|
||||
# 5단계: "AI: frame adaptation plan only (구조 조정, 텍스트 X) → CODE: apply".
|
||||
# op 분류 정책:
|
||||
# set_repeat_count / increase_repeat_count → builder 재실행 (원문 무손실 —
|
||||
# override 는 max(override, len(groups)) floor 로만 작동)
|
||||
# preserve_source_order → already_default (Task 9 가 기본 보장)
|
||||
# compact_spacing → policy_skip (T28.5 no_font_shrink + spacing lock —
|
||||
# font/line-height/padding 축소는 fit lever 로 금지)
|
||||
# rebalance_zone_ratio → policy_skip (CSS/grid 는 code 소유; zone ratio 는
|
||||
# retry engine 소유 — AI CSS 주입 금지)
|
||||
# split_group_across_repeat_blocks(frame_repeat) → unsupported (frame 다중
|
||||
# 인스턴스 = 슬라이드 분할 axis, GitHub #26)
|
||||
# use_repeatable_frame → unsupported (frame_id locked — V4 rank-1 보호)
|
||||
_DESIGN_PLAN_KIND = "design_adaptation_plan"
|
||||
_DESIGN_PLAN_COUNT_OPS = frozenset({"set_repeat_count", "increase_repeat_count"})
|
||||
# 모델별 count 필드 어휘 변동 관측 (count / repeat_count / to) — 관용 추출.
|
||||
_DESIGN_PLAN_COUNT_KEYS = ("count", "repeat_count", "to", "target_count")
|
||||
|
||||
|
||||
def _extract_design_plan_count(op: dict) -> Optional[int]:
|
||||
for key in _DESIGN_PLAN_COUNT_KEYS:
|
||||
val = op.get(key)
|
||||
if isinstance(val, bool):
|
||||
continue
|
||||
if isinstance(val, int) and val > 0:
|
||||
return val
|
||||
if isinstance(val, str) and val.isdigit() and int(val) > 0:
|
||||
return int(val)
|
||||
return None
|
||||
_DESIGN_PLAN_OP_STATUS: dict[str, str] = {
|
||||
"preserve_source_order": "already_default:task9_source_order",
|
||||
"compact_spacing": "policy_skip:no_font_shrink_spacing_lock",
|
||||
"rebalance_zone_ratio": "policy_skip:css_code_owned",
|
||||
"split_group_across_repeat_blocks": "unsupported:multi_instance_requires_deck_split",
|
||||
"use_repeatable_frame": "unsupported:frame_swap_locked",
|
||||
}
|
||||
|
||||
|
||||
def _apply_design_adaptation_plan(record: dict, unit, zone: Optional[dict]) -> None:
|
||||
"""issue #14 — Apply one design_adaptation_plan proposal to its zone.
|
||||
|
||||
구조 op(repeat count)만 verbatim builder 재실행으로 반영하고, 나머지
|
||||
op 는 분류 사유와 함께 기록만 한다 (침묵 drop 금지 — PZ-4 surface).
|
||||
텍스트는 항상 code(verbatim builder)가 배치하므로 AI 텍스트 유입 경로
|
||||
없음 (feedback_ai_isolation_contract).
|
||||
"""
|
||||
payload = (record.get("proposal") or {}).get("payload") or {}
|
||||
operations = payload.get("operations") or []
|
||||
op_status: list[dict] = []
|
||||
target_count: Optional[int] = None
|
||||
for op in operations:
|
||||
if not isinstance(op, dict):
|
||||
op_status.append({"op": str(op), "status": "unsupported_op"})
|
||||
continue
|
||||
name = str(op.get("op") or "")
|
||||
if name in _DESIGN_PLAN_COUNT_OPS:
|
||||
op_target = str(op.get("target") or "")
|
||||
if "frame_contract" in op_target or "add_sub_zone" in (op.get("details") or {}):
|
||||
# frame contract 자체 변이(cardinality/sub_zone 추가)는 catalog
|
||||
# 소유 — AI plan 으로 적용 불가 (사용자/코드 승인 axis).
|
||||
op_status.append({
|
||||
"op": name,
|
||||
"status": "unsupported:contract_mutation_requires_catalog_change",
|
||||
})
|
||||
continue
|
||||
count = _extract_design_plan_count(op)
|
||||
if count is not None:
|
||||
target_count = max(target_count or 0, count)
|
||||
op_status.append({"op": name, "status": "effective"})
|
||||
else:
|
||||
op_status.append({"op": name, "status": "invalid_count"})
|
||||
else:
|
||||
op_status.append({
|
||||
"op": name,
|
||||
"status": _DESIGN_PLAN_OP_STATUS.get(name, "unsupported_op"),
|
||||
})
|
||||
record["design_plan_op_status"] = op_status
|
||||
|
||||
if target_count is None:
|
||||
record["apply_status"] = "design_plan_no_applicable_op"
|
||||
return
|
||||
if zone is None:
|
||||
record["apply_status"] = "no_zone_match"
|
||||
return
|
||||
if unit is None:
|
||||
record["apply_status"] = "design_plan_unit_unavailable"
|
||||
return
|
||||
template_id = zone.get("template_id")
|
||||
rebuilt = _emergency_p4b_build_verbatim_slot_payload(
|
||||
unit, template_id, override_slot_count=target_count,
|
||||
)
|
||||
if rebuilt is None:
|
||||
record["apply_status"] = f"design_plan_builder_unavailable:{template_id}"
|
||||
return
|
||||
if rebuilt == zone.get("slot_payload"):
|
||||
record["apply_status"] = "applied:design_adaptation_plan_no_delta"
|
||||
return
|
||||
zone["slot_payload"] = rebuilt
|
||||
record["apply_status"] = "applied:design_adaptation_plan"
|
||||
|
||||
|
||||
def _apply_ai_repair_proposals_to_zones(
|
||||
ai_repair_records: list[dict],
|
||||
unit_positions: list[str],
|
||||
zones_data: list[dict],
|
||||
units: Optional[list] = None,
|
||||
) -> None:
|
||||
"""IMP-47B u5 — Apply PARTIAL_OVERRIDES into zones_data.slot_payload.
|
||||
|
||||
@@ -2127,6 +2238,10 @@ def _apply_ai_repair_proposals_to_zones(
|
||||
untouched (human_review surfacing → u8). IMP-33 u5 validator
|
||||
guarantees declared-slot completeness, so ``dict.update`` is the
|
||||
structural merge (``feedback_ai_isolation_contract``).
|
||||
|
||||
issue #14 — ``design_adaptation_plan`` kind 는
|
||||
:func:`_apply_design_adaptation_plan` 로 위임 (``units`` 필요 — 미제공
|
||||
시 backward-compat 로 ``design_plan_unit_unavailable`` 기록).
|
||||
"""
|
||||
zone_by_position = {z["position"]: z for z in zones_data}
|
||||
for record in ai_repair_records:
|
||||
@@ -2138,6 +2253,19 @@ def _apply_ai_repair_proposals_to_zones(
|
||||
# model_dump() 는 str-mixin Enum 멤버를 그대로 반환할 수 있음 —
|
||||
# artifact 에 "ProposalKind.X" repr 이 박히지 않도록 value 로 정규화.
|
||||
kind = getattr(kind, "value", kind)
|
||||
unit_index = record["unit_index"]
|
||||
position = (
|
||||
unit_positions[unit_index]
|
||||
if 0 <= unit_index < len(unit_positions) else None
|
||||
)
|
||||
zone = zone_by_position.get(position) if position is not None else None
|
||||
if kind == _DESIGN_PLAN_KIND:
|
||||
unit = (
|
||||
units[unit_index]
|
||||
if units is not None and 0 <= unit_index < len(units) else None
|
||||
)
|
||||
_apply_design_adaptation_plan(record, unit, zone)
|
||||
continue
|
||||
if kind not in _REJECT_SUPPORTED_PROPOSAL_KINDS:
|
||||
record["apply_status"] = f"unsupported_kind_for_reject_route:{kind}"
|
||||
print(
|
||||
@@ -2147,12 +2275,6 @@ def _apply_ai_repair_proposals_to_zones(
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
unit_index = record["unit_index"]
|
||||
position = (
|
||||
unit_positions[unit_index]
|
||||
if 0 <= unit_index < len(unit_positions) else None
|
||||
)
|
||||
zone = zone_by_position.get(position) if position is not None else None
|
||||
if zone is None:
|
||||
record["apply_status"] = "no_zone_match"
|
||||
continue
|
||||
@@ -9731,7 +9853,9 @@ def run_phase_z2_mvp1(
|
||||
if _plan_record is not None and _plan_record.get("position"):
|
||||
_pos = _plan_record["position"]
|
||||
unit_positions.append(_pos)
|
||||
_apply_ai_repair_proposals_to_zones(ai_repair_records, unit_positions, zones_data)
|
||||
_apply_ai_repair_proposals_to_zones(
|
||||
ai_repair_records, unit_positions, zones_data, units=units,
|
||||
)
|
||||
|
||||
# ─── Step 12 IMP-47B u7 — Post-AI source_section_ids coverage invariant ───
|
||||
# Structural defense: AI repair must not silently drop a unit's
|
||||
|
||||
Reference in new issue
Block a user