phase z pipeline: Step 5 / 6-A / 7-conn / 8-conn / Step 9 v0 axis 박힘

사용자 lock 2026-05-08 — Step 5/6/9 boundary reframe.
V4 가 frame 선택, Step 6 은 V4 rank-1 default 전사, Step 9 는 application_plan
번역. compat 매트릭스 안 폐기.

src/phase_z2_pipeline.py 변경 :
- lookup_v4_candidates(v4, section_id, max_n=6) 추가 — V4 non-reject max-6
  후보 list. raw 32 entry 는 v4_full32_result.yaml 영속, step05 = 정제 list.
  lookup_v4_match() (rank-1) 유지 — Step 6 backward compat. (Step 5 보완)
- step05_v4_evidence.json schema 확장 — evidence_per_section[i] =
  {section_id, v4_candidates: [...], candidate_status: "ok" |
  "no_non_reject_v4_candidate"}. (Step 5 보완)
- candidates_lookup_fn 정의 + plan_composition() 에 주입.
  V4 raw dict 는 composition module 안 봄 — fn injection. (Step 6-A)
- Step 6 artifact 의 selected_units[i] 에 v4_candidates 필드 추가. (Step 6-A)
- step07_layout.json data 에 unit_count + layout_candidates 필드 추가
  (Step 7-B 의 select_layout_candidates 결과). step07_selected_layout.html
  에 Layout Candidates 섹션 추가 (default / alternative / selected badge).
  (Step 7-conn)
- step08_zone_region_ratios.json 의 per_zone_plan[i] 에 region_layout_candidates
  + display_strategy_candidates 필드 추가 (Step 8-B-1/2 후보 함수 호출).
  step8_conn_placeholder_signals 명시 — Step 3/4 부재 종속 placeholder
  (region_count=1, content_type="text_block"). step08 HTML 에 candidates pill +
  placeholder caveat. (Step 8-conn)
- APPLICATION_MODE_BY_V4_LABEL 상수 추가 — V4 label → application_mode 변환.
- Step 9 v0 artifact block 신설 (~180 줄) — step09_application_plan.json +
  .html. unit 별 layout_candidates / region_layout_candidates /
  display_strategy_candidates / v4_candidates / candidate_status /
  application_status / current_default_candidate / application_candidates 박힘.
  invariant 5 가지 자연 만족 (status.md §4). (Step 9 v0)

src/phase_z2_composition.py 변경 :
- CompositionUnit 에 v4_candidates: list field 추가 (additive, logic 무변).
  duck typed (V4Match-shape) — circular import 회피. (Step 6-A)
- collect_candidates() 에 v4_candidates_lookup_fn 인자 추가. 3 분기
  (single / parent_merged / parent_merged_inferred) 에서 v4_candidates 채움.
  (Step 6-A)
- plan_composition() 도 v4_candidates_lookup_fn pass-through. (Step 6-A)
- stale "pipeline 호출처 X" 주석 정정 (3 곳: select_layout_candidates /
  select_region_layout_candidates / select_display_strategy_candidates +
  catalog header 2 곳). Step 7-conn / 8-conn 으로 호출처 박혀 *호출처 X* 사실
  위배. (cleanup-1)

axis 닫힘 : Step 5 보완 + 6-A + 7-conn + 8-conn + Step 9 v0 + cleanup-1.
폐기 : 6-B (frame ownership transfer — misframed axis. PHASE-Z-CHANGE-LOG.md
2026-05-08 #2 entry 참조).

regression 0 : MDX03 fresh run 검증 — final.html / step10/12/13/20 byte-동일.
schema 확장만 (step05/06/07/08/09).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 09:47:11 +09:00
parent 8e1f5c67c1
commit ec83405770
2 changed files with 1545 additions and 73 deletions

View File

@@ -22,77 +22,296 @@ Pipeline 의 빠진 layer = MDX 덩어리들을 *최종 zone unit* 으로 묶는
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
import yaml
# ─── 8 Layout Preset Vocabulary ────────────────────────────────
LAYOUT_PRESETS: dict[str, dict] = {
"single": {
"zones": 1,
"topology": "single",
"positions": ["primary"],
"css_areas": '"primary"',
"css_cols": "1fr",
"css_rows": "1fr",
},
"horizontal-2": {
"zones": 2,
"topology": "rows",
"positions": ["top", "bottom"],
"css_areas": '"top" "bottom"',
"css_cols": "1fr",
"css_rows": "1fr 1fr",
},
"vertical-2": {
"zones": 2,
"topology": "cols",
"positions": ["left", "right"],
"css_areas": '"left right"',
"css_cols": "1fr 1fr",
"css_rows": "1fr",
},
"top-1-bottom-2": {
"zones": 3,
"topology": "T",
"positions": ["top", "bottom-left", "bottom-right"],
"css_areas": '"top top" "bottom-left bottom-right"',
"css_cols": "1fr 1fr",
"css_rows": "1fr 1fr",
},
"top-2-bottom-1": {
"zones": 3,
"topology": "inverted-T",
"positions": ["top-left", "top-right", "bottom"],
"css_areas": '"top-left top-right" "bottom bottom"',
"css_cols": "1fr 1fr",
"css_rows": "1fr 1fr",
},
"left-1-right-2": {
"zones": 3,
"topology": "side-T-left",
"positions": ["left", "right-top", "right-bottom"],
"css_areas": '"left right-top" "left right-bottom"',
"css_cols": "1fr 1fr",
"css_rows": "1fr 1fr",
},
"left-2-right-1": {
"zones": 3,
"topology": "side-T-right",
"positions": ["left-top", "right", "left-bottom"],
"css_areas": '"left-top right" "left-bottom right"',
"css_cols": "1fr 1fr",
"css_rows": "1fr 1fr",
},
"grid-2x2": {
"zones": 4,
"topology": "2x2",
"positions": ["top-left", "top-right", "bottom-left", "bottom-right"],
"css_areas": '"top-left top-right" "bottom-left bottom-right"',
"css_cols": "1fr 1fr",
"css_rows": "1fr 1fr",
},
}
# ─── 8 Layout Preset Vocabulary — catalog-loaded (사용자 lock 2026-05-07) ───
#
# Source of truth = templates/phase_z2/layouts/layouts.yaml (사람이 보고 추가/수정 가능).
# 코드 hardcoded dict 폐기 (Step 7-A catalog 화). logic 변경 X — backward compat.
#
# catalog 의 추가 필드 (render_ready / default_selection / candidate_when) 는
# 기존 사용처에서 무시됨 — Step 7-B (multiple 후보) / Step 9 (layout × frame
# fit eval) 진입 시 입력.
_LAYOUTS_CATALOG_PATH = (
Path(__file__).resolve().parent.parent
/ "templates" / "phase_z2" / "layouts" / "layouts.yaml"
)
def load_layout_presets() -> dict[str, dict]:
"""Load 8 layout presets from catalog.
backward compat: returns same dict shape as old hardcoded LAYOUT_PRESETS —
keys = layout id (single / horizontal-2 / ...),
each value contains zones / topology / positions / css_areas / css_cols / css_rows.
Additional fields (render_ready / default_selection / candidate_when)
ignored by existing callers, consumed by Step 7-B / Step 9 (별 axis).
"""
with open(_LAYOUTS_CATALOG_PATH, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
LAYOUT_PRESETS: dict[str, dict] = load_layout_presets()
def select_layout_candidates(unit_count: int) -> list[str]:
"""Return layout id candidates matching given unit_count.
Step 7-B (사용자 lock 2026-05-07) — multiple 후보 generation.
Args:
unit_count: Final layout placement unit count (Step 4 output).
= section_count + promoted lead_orphans 등.
NOT raw MDX section count — Step 2 raw section count 가 아님.
Returns:
List of layout ids matching candidate_when.unit_count.
Sort order:
1. default_selection: true 먼저 (catalog 정의 순서)
2. default_selection: false 그 다음 (catalog 정의 순서)
Layouts with render_ready: false 는 제외.
Raises:
ValueError: if unit_count < 1 or > 4 (current catalog scope).
Note:
호출처 박힘 (Step 7-conn 2026-05-08) — phase_z2_pipeline.py 의
step07 artifact 가 본 함수 결과 기록 (passive). 기존 select_layout_preset()
은 default 결정 그대로. 후보 평가 / auto decision 은 Step 9 v1 (별 axis).
"""
if unit_count < 1 or unit_count > 4:
raise ValueError(
f"unit_count {unit_count} out of catalog scope [1, 4]"
)
defaults: list[str] = []
alternatives: list[str] = []
for layout_id, spec in LAYOUT_PRESETS.items():
if not spec.get("render_ready", False):
continue
cw = spec.get("candidate_when") or {}
if cw.get("unit_count") != unit_count:
continue
if spec.get("default_selection", False):
defaults.append(layout_id)
else:
alternatives.append(layout_id)
return defaults + alternatives
# ─── Region Layout Catalog — Step 8-B-1 (사용자 lock 2026-05-07) ────────
#
# Source = templates/phase_z2/regions/region_layouts.yaml (SPEC §2.5).
# load 함수 + select_region_layout_candidates().
# 호출처 박힘 (Step 8-conn 2026-05-08) — phase_z2_pipeline.py 의 step08 artifact 가
# 본 함수 결과 기록 (placeholder signals: region_count=1, Step 3/4 부재 종속).
_REGION_LAYOUTS_CATALOG_PATH = (
Path(__file__).resolve().parent.parent
/ "templates" / "phase_z2" / "regions" / "region_layouts.yaml"
)
def load_region_layouts() -> dict[str, dict]:
"""Load Internal Region layout catalog (SPEC §2.5, 6 entry).
Returns same dict shape as catalog yaml.
Step 7-A 와 같은 패턴 — source of truth = yaml, code 는 read 만.
"""
with open(_REGION_LAYOUTS_CATALOG_PATH, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
REGION_LAYOUTS: dict[str, dict] = load_region_layouts()
def select_region_layout_candidates(
region_count: int,
content_type_mix: Optional[list[str]] = None,
details_presence: bool = False,
role_pattern: Optional[str] = None,
ratio_asymmetric: bool = False,
flow_type: Optional[str] = None,
has_visual_element: bool = False,
large_table: bool = False,
long_text: bool = False,
) -> list[str]:
"""Return Internal Region layout candidates per SPEC §2.5 decision tree.
Step 8-B-1 (사용자 lock 2026-05-07) — 후보 generation 함수.
Step 7-B 와 다른 점: SPEC §2.5 는 *순차 결정 트리* (첫 매칭 채택).
Step 7-B 는 단순 매칭 (unit_count 같은 모든 entry).
Decision rule (sequential, first match wins) — catalog 와 1:1 일치:
1. region_count == 1 -> region-single
2. details_presence / large_table / long_text -> region-preview-details
3. region_count == 4 AND flow_type == 'parallel_4' -> region-grid-2x2
4. region_count == 2 AND role_pattern ==
'primary_supporting' AND ratio_asymmetric -> region-main-support
5. region_count == 2 AND has_visual_element -> region-horizontal-split
6. fallback (위 미매칭) -> region-vertical-stack
Sort:
region_count == 1 -> [region-single] (fallback X)
region_count >= 2 -> [매칭, region-vertical-stack] 또는 [region-vertical-stack]
Raises:
ValueError: region_count < 1 or > 4 (SPEC §2.5 vocabulary scope).
Note:
호출처 박힘 (Step 8-conn 2026-05-08) — phase_z2_pipeline.py 의 step08 artifact
가 본 함수 결과 기록. 현재 placeholder signals (region_count=1, content_type=
"text_block") 종속 — 실제 신호 활성화는 Step 3/4 별 axis.
Step 9 v0 (application_plan) 가 본 후보 list 를 application_candidates 로 해석.
"""
if region_count < 1 or region_count > 4:
raise ValueError(
f"region_count {region_count} out of catalog scope [1, 4]"
)
fallback = "region-vertical-stack"
# 1. region_count == 1
if region_count == 1:
return ["region-single"]
# 2. details_presence / large_table / long_text
if details_presence or large_table or long_text:
match = "region-preview-details"
# 3. region_count == 4 + parallel_4
elif region_count == 4 and flow_type == "parallel_4":
match = "region-grid-2x2"
# 4. region_count == 2 + role_pattern primary_supporting + ratio_asymmetric
elif (
region_count == 2
and role_pattern == "primary_supporting"
and ratio_asymmetric
):
match = "region-main-support"
# 5. region_count == 2 + visual element
elif region_count == 2 and has_visual_element:
match = "region-horizontal-split"
# 6. fallback
else:
return [fallback]
# 매칭됨 + fallback (단 매칭 == fallback 인 경우 1개만)
if match == fallback:
return [fallback]
return [match, fallback]
# ─── Display Strategy Catalog — Step 8-B-2 (사용자 lock 2026-05-07) ────
#
# Source = templates/phase_z2/regions/display_strategies.yaml (4 entry).
# load 함수 + select_display_strategy_candidates().
# 호출처 박힘 (Step 8-conn 2026-05-08) — phase_z2_pipeline.py 의 step08 artifact 가
# 본 함수 결과 기록 (placeholder signals: content_type="text_block", Step 3/4 부재 종속).
_DISPLAY_STRATEGIES_CATALOG_PATH = (
Path(__file__).resolve().parent.parent
/ "templates" / "phase_z2" / "regions" / "display_strategies.yaml"
)
def load_display_strategies() -> dict[str, dict]:
"""Load display strategy catalog (4 entry).
Returns same dict shape as catalog yaml.
Step 7-A / 8-B-1 와 같은 패턴 — source of truth = yaml, code 는 read 만.
"""
with open(_DISPLAY_STRATEGIES_CATALOG_PATH, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
DISPLAY_STRATEGIES: dict[str, dict] = load_display_strategies()
_KNOWN_CONTENT_TYPES = frozenset({
"text_block", "table", "image", "details", "decorative_element",
})
def select_display_strategy_candidates(
content_type: str,
long_text: bool = False,
large_table: bool = False,
fits_in_region: Optional[bool] = None,
) -> list[str]:
"""Return display strategy candidates per catalog (display_strategies.yaml).
Step 8-B-2 (사용자 lock 2026-05-07) — 후보 generation 함수.
display_strategies.yaml 만 본다 (region_layouts / frame 은 Step 9 axis).
Hard filter (catalog 박힌 절대 제약 — applies_to / forbidden_for):
- content_type 이 strategy.applies_to 에 있어야 후보
- content_type 이 strategy.forbidden_for 에 있으면 자동 제외
- 핵심 user lock: text_block / table / image / details 는 dropped 절대 X
(catalog forbidden_for 에 박혀 있음 — 원문 무손실 보존)
Ranking (content_type + fit signal):
decorative_element -> [inline_full, dropped]
image -> [inline_full]
text_block / table / details
long_text / large_table
/ fits_in_region == False -> [inline_preview_with_details,
details_only, inline_full]
그 외 -> [inline_full,
inline_preview_with_details,
details_only]
Note:
- fits_in_region 은 가벼운 hint 만. 실제 overflow 판단은 Step 9/14/17 axis.
- dropped 는 decorative_element 의 후순위 (공간 부족 신호 전엔 일단 보여주기).
Raises:
ValueError: content_type 이 catalog scope 밖
(text_block / table / image / details / decorative_element 외).
Note:
호출처 박힘 (Step 8-conn 2026-05-08) — phase_z2_pipeline.py 의 step08 artifact
가 본 함수 결과 기록. 현재 placeholder signal (content_type="text_block")
종속 — 실제 신호 활성화는 Step 3/4 별 axis.
Step 9 v0 (application_plan) 가 본 후보 list 를 application_candidates 의
display_strategy axis 로 해석.
"""
if content_type not in _KNOWN_CONTENT_TYPES:
raise ValueError(
f"content_type {content_type!r} out of catalog scope "
f"(known: {sorted(_KNOWN_CONTENT_TYPES)})"
)
# Hard filter — applies_to / forbidden_for (catalog 직독)
eligible = set()
for name, meta in DISPLAY_STRATEGIES.items():
applies_to = meta.get("applies_to") or []
forbidden_for = meta.get("forbidden_for") or []
if content_type in applies_to and content_type not in forbidden_for:
eligible.add(name)
# Ranking — content_type + fit signal
if content_type == "decorative_element":
order = ["inline_full", "dropped"]
else:
escalate = long_text or large_table or fits_in_region is False
if escalate:
order = [
"inline_preview_with_details",
"details_only",
"inline_full",
]
else:
order = [
"inline_full",
"inline_preview_with_details",
"details_only",
]
return [s for s in order if s in eligible]
# ─── CompositionUnit ────────────────────────────────────────────
@@ -137,6 +356,14 @@ class CompositionUnit:
# 예: "children disagree on rank-1 template_id" / "minority of children non-auto-renderable"
notes: list[str] = field(default_factory=list)
# Step 6-A axis 추가 (사용자 lock 2026-05-08).
# V4 후보 list (V4Match-shape duck typed — composition module 은 V4Match dataclass 미import,
# circular dep 회피). 각 entry attrs : template_id / frame_id / frame_number / confidence / label.
# list 순서 = V4 rank (candidates[0] = rank-1 non-reject — 단일 frame_template_id /
# frame_id / label / confidence 와 일치, backward compat lock).
# 0 길이 = "no_non_reject_v4_candidate" 신호 (Step 9 application_plan input).
v4_candidates: list = field(default_factory=list)
# ─── Heading Tree ──────────────────────────────────────────────
@@ -192,7 +419,8 @@ def _apply_capacity_fit(candidate: CompositionUnit, capacity_fit_fn) -> None:
def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
auto_renderable_statuses: Optional[set[str]] = None,
capacity_fit_fn=None):
capacity_fit_fn=None,
v4_candidates_lookup_fn=None):
"""Generate composition candidates.
v0.1 candidate types :
@@ -207,12 +435,17 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
Args:
sections : align 결과
v4_lookup_fn : (section_id) → V4Match | None
v4_lookup_fn : (section_id) → V4Match | None (rank-1 only, 기존 호환)
v4_label_to_status : V4 label → Phase Z status mapping
auto_renderable_statuses : 자동 렌더 허용 status set (W1/W3 판정 입력)
capacity_fit_fn : Optional (template_id, content) → fit dict.
제공되면 모든 candidate 에 적용 — capacity mismatch 시 auto_selectable=False
(silent truncate / mapper FitError 사전 차단).
v4_candidates_lookup_fn : Optional (section_id) → list[V4Match].
Step 6-A axis (사용자 lock 2026-05-08). non-reject max-N 후보 list.
제공되면 모든 candidate 에 v4_candidates 필드 채움.
None 이면 v4_candidates = [] (backward compat).
본 fn 이 V4 raw dict 구조를 흡수 — composition module 은 V4 yaml shape 모름.
Returns:
list[CompositionUnit]
@@ -220,6 +453,10 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
if auto_renderable_statuses is None:
auto_renderable_statuses = set()
def _v4_cands(section_id: str) -> list:
# v4_candidates_lookup_fn 미제공 시 빈 list (backward compat).
return v4_candidates_lookup_fn(section_id) if v4_candidates_lookup_fn else []
candidates = []
# 1. Separate
@@ -238,6 +475,7 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
phase_z_status=v4_label_to_status.get(match.label, "unknown"),
raw_content=s.raw_content,
title=s.title,
v4_candidates=_v4_cands(s.section_id),
)
_apply_capacity_fit(c, capacity_fit_fn)
candidates.append(c)
@@ -268,6 +506,7 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
phase_z_status=v4_label_to_status.get(parent_match.label, "unknown"),
raw_content=merged_raw,
title=pid,
v4_candidates=_v4_cands(pid),
)
_apply_capacity_fit(c_pm, capacity_fit_fn)
candidates.append(c_pm)
@@ -363,6 +602,8 @@ def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
auto_selectable=auto_selectable,
filter_reasons=filter_reasons,
notes=notes,
# rep_child 의 V4 후보 list (rep_match 와 같은 출처, frame_* 와 일관).
v4_candidates=_v4_cands(rep_child.section_id),
)
_apply_capacity_fit(c_inf, capacity_fit_fn)
candidates.append(c_inf)
@@ -478,7 +719,8 @@ def select_layout_preset(units: list[CompositionUnit]) -> Optional[str]:
def plan_composition(sections, v4_lookup_fn, v4_label_to_status: dict,
allowed_statuses: set[str],
capacity_fit_fn=None) -> tuple[list[CompositionUnit], Optional[str], dict]:
capacity_fit_fn=None,
v4_candidates_lookup_fn=None) -> tuple[list[CompositionUnit], Optional[str], dict]:
"""Composition planner v0.2 entry.
v0.2 변경 :
@@ -486,6 +728,11 @@ def plan_composition(sections, v4_lookup_fn, v4_label_to_status: dict,
(silent truncate / mapper FitError 사전 차단). 불일치 시 auto_selectable=False
+ filter_reason 'C1: ...'.
Step 6-A axis (사용자 lock 2026-05-08) :
- v4_candidates_lookup_fn 주입 시 모든 CompositionUnit 에 v4_candidates 채움.
logic 변화 X — 단일 frame_template_id / frame_id / label / confidence 는 그대로.
runtime 결과 무변. Step 9 application_plan input 위한 schema 확장.
v0.1 / v0.1.1 동작 (유지) :
- parent_merged_inferred candidate 생성 (parent V4 없어도)
- review 개념 X. auto_selectable + filter_reasons 만으로 자동 결정
@@ -500,6 +747,7 @@ def plan_composition(sections, v4_lookup_fn, v4_label_to_status: dict,
sections, v4_lookup_fn, v4_label_to_status,
auto_renderable_statuses=allowed_statuses,
capacity_fit_fn=capacity_fit_fn,
v4_candidates_lookup_fn=v4_candidates_lookup_fn,
)
scored_all = [score_candidate(c) for c in candidates]

File diff suppressed because it is too large Load Diff