From 2ec8fc5a775c92a8305558abecf20eeb77b469a2 Mon Sep 17 00:00:00 2001 From: kyeongmin Date: Mon, 4 May 2026 08:21:50 +0900 Subject: [PATCH] Add Phase Z Layer A planning scaffold - add Internal Region model to Phase Z architecture docs and specs - add frame contract content type and Frame Slot declarations - add dormant content object extractor and internal region planner --- .../PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md | 285 ++++++ .../PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md | 849 ++++++++++++++++++ .../architecture/PHASE-Z-PIPELINE-OVERVIEW.md | 472 ++++++++++ .../PHASE-Z-PIPELINE-STATUS-BOARD.md | 128 +++ src/phase_z2_content_extractor.py | 323 +++++++ src/phase_z2_internal_region_planner.py | 366 ++++++++ .../phase_z2/catalog/frame_contracts.yaml | 181 ++++ 7 files changed, 2604 insertions(+) create mode 100644 docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md create mode 100644 docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md create mode 100644 docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md create mode 100644 docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md create mode 100644 src/phase_z2_content_extractor.py create mode 100644 src/phase_z2_internal_region_planner.py create mode 100644 templates/phase_z2/catalog/frame_contracts.yaml diff --git a/docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md b/docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md new file mode 100644 index 0000000..947b6fa --- /dev/null +++ b/docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md @@ -0,0 +1,285 @@ +# Phase Z — composition planning schema plan (Layer A + Layer B) + +**Status** : v1 plan (2026-04-30 refactor — SPEC v1 의 Layer A + Layer B 구조 반영). schema 작업 *범위 / 순서 / 완료 기준* 정의. 구현 X. +**Anchor** : [`PHASE-Z-PIPELINE-STATUS-BOARD.md`](PHASE-Z-PIPELINE-STATUS-BOARD.md) 의 핵심 missing — Step 3 / 4 / 11 + Step 10 (부분). + +> **v0 → v1 변경 요약** +> - SPEC v1 의 renumbered section 으로 cross-reference 일관 갱신 (§1 / §2 신규 / §3 / §4 / §5 …) +> - Step 4 의 schema 가 SPEC v1 §2 (Internal Region — Layer A) 로 *이미 정의됨* → 본 plan 에서는 *신규 작성* 이 아니라 *완성도 점검* 으로 전환 +> - Step 11 의 schema 가 SPEC v1 §4 (2-stage placement: Stage A → Stage B) 로 재정의됨에 따라 validation 기준 보강 +> - mechanical rename : `sub_zone` → `Frame Slot` (PLAN 본문). YAML 필드명 `sub_zones` 는 코드 reality 로 유지 +> - 4 step 범위 (Step 3 / 4 / 10-partial / 11) 그대로. Step 10 의 density envelope 제외 그대로 +> - implementation / MDX 실행 검증 / AI 호출 / 파일명 변경 / code marker 이름 결정 — *모두 제외 그대로* + +--- + +## 0. 문서 역할 분리 + +| 문서 | 역할 | +|---|---| +| [`PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md`](PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md) (v1) | schema / contract *명세 자체* (authoritative) | +| **본 문서 (`PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md`)** (v1) | 그 schema 작업을 *어떤 범위 / 순서 / 산출물 / validation 기준* 으로 진행할지 *계획* | + +본 plan 은 SPEC 을 *대체하지 않음*. SPEC 의 어느 section 이 어느 step 의 schema 인지 *cross-reference + 완성도 점검* 까지가 본 plan 의 영역. + +--- + +## 1. 목적 + +일반 MDX 1 파일이 *render 전 단계* 에서 typed content_object → Internal Region → Frame Slot 으로 결정 가능한 상태에 도달하도록, 다음 4 step 의 schema 정의를 *완결* 시킨다. + +``` +Step 3. Content Object 추출 +Step 4. Section Internal Composition Planning (Layer A) +Step 10. Frame Contract 의 accepted_content_types + Frame Slot 선언 (부분, Layer B) +Step 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping (2-stage) +``` + +--- + +## 2. 범위 + +### 2.1 포함 + +| Step | schema 항목 | SPEC v1 의 위치 | +|---|---|---| +| Step 3 | content_object base schema + 6 type 별 schema (text_block / table / transform_table / image / diagram / details) | SPEC v1 §1.1, §1.2 | +| Step 3 | role 의미 (summary / detail / decorative / reference) | SPEC v1 §1.3 | +| Step 4 | Internal Region entity schema (region_id / role / content_type / ratio_estimate / content_unit_ids / frame_match_strategy) | SPEC v1 §2.1 | +| Step 4 | Universal Region Model (every zone = 1+ regions) | SPEC v1 §2.2 | +| Step 4 | 3-way decision tree (whole / group / split) | SPEC v1 §2.3 | +| Step 4 | region 비율 산정 (content type 별 size proxy) | SPEC v1 §2.4 | +| Step 4 | region → frame / display 매칭 interface (frame_match / display_only) | SPEC v1 §2.6 | +| Step 10 | frame contract 의 `accepted_content_types` 필드 schema | SPEC v1 §3.1 | +| Step 10 | Frame Slot 선언 schema (YAML 필드명 `sub_zones` — 의미 = Frame Slot) | SPEC v1 §3.1 | +| Step 10 | F13 / F29 / F16 의 Frame Slots 선언 예시 | SPEC v1 §3.2 | +| Step 11 | placement algorithm 2-stage I/O schema | SPEC v1 §4.1 | +| Step 11 | Stage A — content → Internal Region (Layer A 매핑) | SPEC v1 §4.2 | +| Step 11 | Stage B — Internal Region content → Frame Slot (Layer B 매핑) | SPEC v1 §4.3 | +| Step 11 | tie-break (Frame Slot 단위) | SPEC v1 §4.4 | +| Step 11 | display_only region 처리 (frame 우회) | SPEC v1 §4.5 | +| Step 11 | display_strategy schema (inline_full / inline_preview_with_details / details_only / dropped) | SPEC v1 §5.1, §5.5 | +| Step 11 | display_strategy 의 region-level + slot-level 양쪽 적용 | SPEC v1 §5.3 | + +### 2.2 Step 10 의 *부분 범위* 명시 (불변) + +> 본 plan 에서 Step 10 은 **두 항목** 까지만 다룸 : +> - `accepted_content_types` +> - Frame Slot 선언 (YAML 필드명 `sub_zones`) +> +> 본 plan 에서 *다루지 않는* Step 10 항목 : +> - `density envelope` +> +> `density envelope` 은 `frame_internal_fit_candidate` (Step 17 / Step 19 Gap) 와 연결된 *frame internal fit policy* 영역. 본 plan 에 넣으면 *공통 padding 축소 antipattern* (`feedback_phase_z_spacing_direction`) 과 섞일 risk 있어 *별 plan* 으로 분리. +> +> 따라서 본 plan 이 완료되어도 Step 10 전체가 ✅ 완료되는 것은 아니고 — `accepted_content_types` + Frame Slot 부분만 ✅ / density envelope 부분은 ❌ 잔존. + +### 2.3 명시적 제외 + +- 구현 (extractor / planner / parser 코드 작성) — 본 plan 은 schema 까지 +- frame partial template 변경 (Frame Slot / region container marker 추가) +- mapper / classifier 의 region / Frame Slot-aware 진화 +- details / popup runtime +- backward flow 자동화 (telemetry → composition 재호출) +- MDX 01 / 02 / 03 / 04 적용 검증 +- AI 호출 (plan 단계 자체 + plan 산출 schema 자체 모두) +- code / module / HTML marker / attribute *이름 결정* (SPEC v1 와 동일 — implementation step 에서 결정) +- 파일명 변경 (PLAN / SPEC 둘 다 — 향후 별 결정) +- 다음 단계 우선순위 / A/B/C 선택지 + +--- + +## 3. 산출물 + +### 3.1 schema-side 산출물 + +본 plan 의 schema 작업이 완료되면, SPEC v1 가 다음 상태에 도달 : + +| schema | 현재 SPEC v1 상태 | plan 종료 후 목표 상태 | +|---|---|---| +| content_object base + 6 type 별 schema | draft (SPEC v1 §1.1, §1.2) | completeness reviewed + gaps listed | +| role 의미 4 종 | draft (SPEC v1 §1.3) | completeness reviewed + gaps listed | +| Internal Region entity + Universal Region Model | draft (SPEC v1 §2.1, §2.2) | completeness reviewed + gaps listed | +| 3-way decision tree | draft (SPEC v1 §2.3) | completeness reviewed + gaps listed | +| region 비율 산정 | draft (SPEC v1 §2.4) | completeness reviewed + gaps listed | +| region → frame / display 매칭 interface | draft (SPEC v1 §2.6) | completeness reviewed + gaps listed | +| accepted_content_types + Frame Slot 선언 | draft (SPEC v1 §3.1, 3 frame 예시 §3.2) | completeness reviewed + gaps listed | +| 2-stage placement (Stage A + Stage B I/O) | draft (SPEC v1 §4.1, §4.2, §4.3) | completeness reviewed + gaps listed | +| tie-break + display_only region 처리 | draft (SPEC v1 §4.4, §4.5) | completeness reviewed + gaps listed | +| display_strategy 4 종 + region/slot 양쪽 적용 | draft (SPEC v1 §5.1, §5.3, §5.5) | completeness reviewed + gaps listed | + +### 3.2 plan-side 산출물 + +- 본 PLAN 문서 (v1) — schema 작업 범위 정의 +- 작업 결과로 SPEC v1 의 *추가 / 수정 부분* (edge case 보강 등 — substantive 신규 schema 가 아닌 점검 결과) +- pipeline data flow 위치 표 (§5 참조) +- schema validation 기준 표 (§6 참조) + +--- + +## 4. SPEC ↔ PLAN cross-reference + +본 plan 의 4 step 각각이 SPEC v1 의 어느 section 으로 매핑되는지 : + +| Step | SPEC v1 section | plan 에서의 작업 | +|---|---|---| +| Step 3 | §1 (content_object schema) | *완성도 점검*. nested_list / sub-decomposition edge case (SPEC v1 §9.2 미해결 부분 — text_block 의 nested 구조를 sub_text_block 으로 sub-decompose vs Frame Slot cardinality aggregate 해석) | +| Step 4 | §2 (Internal Region schema, Layer A) | *완성도 점검*. 3-way decision 의 boundary 케이스 / region 비율의 fallback / frame_match_strategy 가 unknown / ambiguous 일 때 거동 | +| Step 10 (partial) | §3.1 (schema) + §3.2 (3 frame 예시) | *완성도 점검*. `density envelope` 미포함 명시 | +| Step 11 | §4 (placement algorithm 2-stage) + §5 (display strategy) | *완성도 점검*. Stage A → Stage B *interface* 정합 (Stage A 의 frame_match_strategy 가 Stage B 의 입력으로 자연 호환) / backward flow 자동화 X 등 v1 한계 재확인 | + +--- + +## 5. Pipeline data flow 위치 + +본 4 step 의 schema 가 *어디서 생성 / 누가 소비* 하는지. + +``` +Step 1 MDX 업로드 + ↓ +Step 2 MDX 정규화 (section / heading / raw_content 분리) + ↓ + ├─→ Step 3. Content Object 추출 + │ INPUT : section.raw_content (markdown 문자열) + │ OUTPUT : section.content_objects = [ContentObject ...] + │ SCHEMA : SPEC v1 §1 + │ 소비자 : Step 4, Step 11 + │ + └─→ Step 4. Section Internal Composition Planning (Layer A) + INPUT : section.content_objects (from Step 3) + section metadata + V4 evidence + OUTPUT : zone.internal_regions = [ + {region_id, role, content_type, ratio_estimate, + content_unit_ids, frame_match_strategy}, ... + ] + SCHEMA : SPEC v1 §2 (entity / Universal Region Model / 3-way decision / 비율 / interface) + 소비자 : Step 6 (composition planning) / Step 8 (region ratio) / + Step 9 (region-level frame match) / Step 11 (Stage A 결과) + +Step 5 Matching Evidence 생성 (V4 top-k) +Step 6 Composition Planning (Step 4 region 분할 결과 입력) +Step 7 Slide-Level Layout Planning +Step 8 Zone + Internal Region Ratio Planning (region 비율 = Step 4 산출) +Step 9 Region-Level Frame / Display Selection (region 별 frame_match_strategy = Step 4 산출) + ↓ + └─→ Step 10 (partial). Frame Contract 확인 + INPUT : selected frame_id (region 별) + OUTPUT : frame_contract.accepted_content_types + + frame_contract.sub_zones (= Frame Slots, Layer B) + SCHEMA : SPEC v1 §3 + 소비자 : Step 11 (Stage B) + + ↓ + └─→ Step 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping (2-stage) + INPUT : section.content_objects (from Step 3) + + zone.internal_regions (from Step 4) + + frame_contract.sub_zones (= Frame Slots) (from Step 10, frame_match region 만) + Stage A : content → Internal Region (Step 4 의 region 분할 결과 소비) + Stage B : Internal Region content → Frame Slot (frame_match region 만 진입) + display_only region → display strategy 처리 (Stage B 우회) + OUTPUT : placement = { + internal_regions: [ + {..., slot_assignments[], overflow_buffer[], rejection[]}, ... + ] + } + SCHEMA : SPEC v1 §4 + §5 + 소비자 : Step 12 (slot payload 생성, region + Frame Slot 단위 grouping) + +Step 12 Slot Payload 생성 +Step 13 Render +... +``` + +--- + +## 6. schema validation 기준 + +> validation = *schema 자체의 구조 검증* 까지만. *실제 MDX 적용 검증 X* (sample budget rule). + +### 6.1 Step 3 — content_object schema + +- [ ] base schema 의 모든 필수 필드 (id, type, role, size_estimate, raw_payload, type_specific) 정의됨 +- [ ] 6 type 모두 type_specific schema 있음 (text_block / table / transform_table / image / diagram / details) +- [ ] role 4 종 (summary / detail / decorative / reference) 정의됨 +- [ ] role 별 fallback 거동 (decorative drop / detail → details escalate / summary·reference rejection) 명시됨 +- [ ] *원문 raw_payload 보존* 룰이 schema 에 포함됨 (자름 / 변형 X) + +### 6.2 Step 4 — Internal Region schema (Layer A) + +- [ ] Internal Region entity 의 모든 필수 필드 (region_id, role, content_type, ratio_estimate, content_unit_ids, frame_match_strategy) 정의됨 +- [ ] Universal Region Model 명시 (every zone has 1+ regions / single-region for text-only / multi-region for mixed-content) +- [ ] 3-way decision tree 의 3 분기 (whole-section frame match / child-section grouping / content-type split) 가 *결정론적 함수* 로 표현됨 (AI 판단 X) +- [ ] 각 분기의 판단 기준 (cardinality / accepted_content_types / heading depth + content 구조) 명시됨 +- [ ] region 비율 산정의 size proxy (text_block: line_count / table: rows × line_height / transform_table: pair_count × pair_height / image: aspect_ratio / details: summary line_count) 정의됨 +- [ ] zone 내 region ratio 합 = 1.0 normalize 룰 명시됨 +- [ ] frame_match_strategy 의 두 kind (frame_match / display_only) 정의됨 +- [ ] display_only path 가 frame contract 없이 동작 가능함이 명시됨 +- [ ] decision 이 unknown / ambiguous 일 때의 fallback 명시됨 + +### 6.3 Step 10 (partial) — frame contract schema (Layer B 선언) + +- [ ] `accepted_content_types` 필드 schema 정의됨 (list of type 이름) +- [ ] `not_accepted` 필드 schema 정의됨 (디버그용) +- [ ] Frame Slot 선언 schema 정의됨 (id / role / accepts / cardinality / partial_target_path) — YAML 필드명 `sub_zones`, 의미 = Frame Slot +- [ ] cardinality 표현 방식 (`strict` 또는 `min`/`max`) 정의됨 +- [ ] F13 / F29 / F16 3 frame 의 Frame Slot declaration 예시 *완비* +- [ ] *density envelope 미포함* 이 schema 위에 명시 주석으로 박혀 있음 + +### 6.4 Step 11 — placement (2-stage) + display_strategy schema + +- [ ] placement algorithm 2-stage I/O schema 정의됨 (input + Stage A 출력 + Stage B 출력) +- [ ] Stage A schema 정의됨 (content_object → Internal Region 매핑 결과 + frame_match_strategy 결정) +- [ ] Stage B schema 정의됨 (Internal Region content → Frame Slot 매핑 결과 — frame_match region 만) +- [ ] sorting / type 매칭 / cardinality 적용 / role 우선순위 / tie-break 의 결정론적 룰 (Stage B) 명시됨 +- [ ] display_only region path 의 display_strategy 매핑 정의됨 (image area / table preview / details button / diagram inline) +- [ ] 6 type × 3 escalation (inline / preview+details / popup-only) 매트릭스 정의됨 +- [ ] display_strategy 4 종 (inline_full / inline_preview_with_details / details_only / dropped) 정의됨 +- [ ] display_strategy 가 *region-level + slot-level 둘 다* 적용됨이 명시됨 +- [ ] *AI 호출 X* + *원문 손실 금지* 룰이 schema 위에 명시됨 +- [ ] backward flow 자동화 X (v1 한계) 가 schema 위에 명시됨 + +### 6.5 통합 validation + +- [ ] Step 3 → Step 4 의 type 호환 — content_object.type 이 Internal Region.content_type 으로 *결정론적* 매핑 +- [ ] Step 3 → Step 11 (Stage B) 의 type 호환 — Step 3 의 모든 type 이 어딘가 frame.accepted_content_types 에 등장 가능 (또는 명시적 reject) 또는 display_only path 로 처리 가능 +- [ ] Step 4 → Step 8 의 ratio 호환 — Internal Region.ratio_estimate 가 Step 8 의 region-level ratio 입력으로 호환 +- [ ] Step 4 → Step 9 의 frame_match_strategy 호환 — Step 9 의 region-level frame 매칭 입력으로 호환 +- [ ] Step 4 → Step 11 (Stage A) 의 region 분할 호환 — Stage A 가 Step 4 의 internal_regions 를 *재계산하지 않고 그대로 소비* +- [ ] Step 10 의 cardinality 가 Step 11 Stage B 의 placement algorithm 이 소비 가능한 형태 + +--- + +## 7. AI 원칙 + +- 본 plan 작성 / schema 정의 단계 — **AI 호출 없음** +- runtime AI = Step 12 의 *light_edit / restructure 의 content_object → Internal Region / Frame Slot proposal* 1 곳만 +- Step 0 (Figma → HTML 변환 등 사전 준비) 의 AI 사용은 *precondition phase* 로, runtime AI 가 아님 + +--- + +## 8. 금지 + +- 구현 금지 (extractor / planner / parser 코드 작성) +- render 변경 금지 +- frame partial 변경 금지 (Frame Slot / region container marker 추가 미포함) +- mapper / classifier 의 region / Frame Slot-aware 진화 금지 +- details / popup runtime 작성 금지 +- MDX 01 / 02 / 03 / 04 실행 금지 +- AI 호출 금지 +- code / module / HTML marker / attribute *이름 결정* 금지 (SPEC v1 와 동일 — implementation step 에서 결정) +- 파일명 변경 금지 +- next step 추천 금지 +- 우선순위 결정 금지 +- A / B / C 선택지 제시 금지 + +--- + +## 9. 본 plan 의 보존 / 변경 정책 + +- 본 plan 은 *schema 작업 범위 + 완료 기준* 의 기준점. schema 작업 진행 중 새로운 edge case 발견 시 본 plan 의 6 절 (validation 기준) 에 *항목 추가* 형태로 갱신 +- *범위 확장* (예: Step 7 추가, density envelope 포함, code marker 이름 결정 포함) 은 사용자 명시 잠금 후에만 +- 본 plan 이 완료되면 STATUS-BOARD 의 : + - Step 3 → ⚠ partial (schema 정의 완료, 구현 미완) + - Step 4 → ⚠ partial (Layer A schema 정의 완료, 구현 미완) + - Step 11 → ⚠ partial (2-stage schema 정의 완료, 구현 미완) + - Step 10 → ⚠ partial (accepted_content_types + Frame Slot 부분 완료, density envelope 미포함 잔존) +- SPEC 의 *추가 갱신* (edge case 보강 등) 은 본 plan 의 §3.1 목표 상태 (completeness reviewed + gaps listed) 의 자연 산출물 diff --git a/docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md b/docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md new file mode 100644 index 0000000..09ca6f3 --- /dev/null +++ b/docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md @@ -0,0 +1,849 @@ +# Phase Z-2 — content composition planning spec (Layer A: Internal Region + Layer B: Frame Slot) + +**Status** : v1 spec (2026-04-30 refactor — Layer A / Internal Region 추가 + Layer B / Frame Slot 명확화). 정의만. 구현은 별도 step (사용자 승인 후). + +> **v0 → v1 변경 요약** +> - Zone Internal Region (Layer A) 를 first-class entity 로 추가 (§2 신규) +> - 기존 §2 ~ §8 → §3 ~ §9 로 renumber (v0 의 ## 9 다음 step → v1 의 ## 10) +> - 기존 `sub_zone` 단어 = *Frame Slot (Layer B)* 의미로 일관 정리. *YAML 필드명 `sub_zones` 는 코드 reality 로 유지 — 의미만 명시* +> - placement algorithm (§4) 을 *2-stage* (Stage A: content → Internal Region / Stage B: Internal Region content → Frame Slot) 로 재작성 +> - content_object schema (§1) / display strategy 어휘 (§5) / telemetry 구조 (§6) 는 *layer-agnostic 공유 개념* 으로 보호 — substantive 미변경 +> - code / module / HTML marker 이름은 *implementation step* 으로 defer + +--- + +## §0. 목적 / 위치 + +본 spec 은 **render *전* composition planning layer** 의 정의. fit_classifier / overflow_router / zone_ratio_retry 같은 *post-render telemetry* 가 아니라, *애초에 content 를 어디에 어떻게 배치할지* 결정하는 *진짜 fit policy 의 중심*. + +``` +1. PLANNING (composition) ← 본 spec 의 영역 + - section raw_content → content_object 정규화 + - Zone Internal Region (Layer A) 분할 — text/table/image/details 에 따라 + - frame contract → accepted_content_types + Frame Slot (Layer B) 선언 + - content_object → Internal Region → Frame Slot 배치 (compatibility 기반) + - inline preview vs details/popup 표시 전략 + ↓ +2. RENDER (Jinja2 + frame partial — Frame Slot aware) + ↓ +3. POST-RENDER TELEMETRY (A1~A4) ← 별 spec, 이미 구축됨 + - Selenium → fit_classifier → router → retry → failure_classifier → next_action + - 1 단계 (본 spec 영역) 가 정밀하면 거의 trigger 안 됨 + - exception 케이스의 *진단 + 다음 capability 안내* +``` + +### Layer 구분 + +``` +Slide → Zone → Internal Region → Frame → Frame Slot → Content + ──────────── ────────── + Layer A Layer B +``` + +- **Layer A — Zone Internal Region** : Zone *내부* 영역, frame *밖*. content type 기반 분할 (text region / table region / image region / details region). region 별 frame 또는 display strategy 선택. +- **Layer B — Frame Slot** : frame *내부* 자리 (= F13 의 pillar_1, F29 의 process_column 등). frame 안에서 content unit 이 들어갈 곳. + +Layer A 와 Layer B 는 *별개 entity* 가 아니라 *한 composition pipeline 의 두 sub-phase*. content_object schema / display strategy 어휘 / telemetry interface 는 *공유*. + +### 본 spec 의 핵심 원칙 + +- *render 전* 결정. *render 후 retry* 가 아님 +- content_object 의 *type* 이 핵심 — text / table / image / diagram / details +- Layer A 가 *region 분할* 결정 / Layer B 가 *region 안 Frame Slot 매핑* 결정 +- 매칭 안 되는 content 는 *details/popup 으로 escalate*. *원문 삭제 / 압축 X* +- frame 의 *Frame Slot* 이 어떤 type 을 받을 수 있는지 *명시적으로 선언* + +본 spec 은 *정의만*. 구현 우선순위는 별도 step (사용자 승인 후). + +--- + +## §1. content_object 정규화 schema + +> **layer-agnostic** — Layer A / Layer B 둘 다 사용. v0 → v1 refactor 시 *substantive 미변경*. + +MDX section 의 `raw_content` (markdown 문자열) 를 *typed content_object list* 로 정규화. + +### 1.1 base schema + +```yaml +section: + section_id: str + title: str + content_objects: + - id: str # section 내 unique + type: str # text_block / table / image / diagram / details / transform_table + role: str # summary / detail / decorative / reference + size_estimate: + line_count: int # text/details 의 경우 + rows: int # table 의 경우 + aspect_ratio: float # image/diagram 의 경우 + bytes: int # raw payload 크기 (heuristic용) + raw_payload: str # 원본 (자름 / 변형 X) + type_specific: {...} # 아래 type 별 schema +``` + +### 1.2 type 별 schema + +#### `text_block` — 자유 텍스트 / 불릿 +```yaml +type: text_block +type_specific: + format: paragraph | bullet_list | nested_list + bullet_count: int # 불릿이면 + max_indent_level: int + has_emphasis: bool # **bold** 등 inline emphasis +``` + +#### `table` — markdown 표 +```yaml +type: table +type_specific: + rows: int # header 제외 데이터 row + cols: int + header_present: bool + is_transform: bool # AS-IS / arrow / TO-BE 구조면 true → transform_table 으로 분류 + raw_md: str # 원본 markdown +``` + +#### `transform_table` — AS-IS / TO-BE pair (`table` 의 specialization) +```yaml +type: transform_table +type_specific: + pair_count: int # 행 수 (각 행 = 1 transform pair) + arrow_glyph: str # ➠ 등 + rows: [{from: str, arrow: str, to: str}] +``` + +#### `image` — markdown / HTML 이미지 +```yaml +type: image +type_specific: + src: str + alt: str + aspect_ratio: float | null # 알면 (asset metadata 에서) + intrinsic_width_px: int | null + intrinsic_height_px: int | null +``` + +#### `diagram` — SVG / 도식 +```yaml +type: diagram +type_specific: + source_type: svg_inline | svg_file | mermaid | other + src: str | null +``` + +#### `details` — `
/` 또는 ":::note[...]" 같은 명시 marker +```yaml +type: details +type_specific: + summary: str # 펼치기 전 보일 헤더 + body_raw: str # 펼친 후 content (자름 X) + display_hint: button | inline_collapse | popup # MDX 가 hint 줄 수 있음 +``` + +### 1.3 role 의미 + +- `summary` — section 의 *핵심 메시지*. inline 으로 반드시 표시 +- `detail` — 보조 / 부연. 공간 부족 시 details 로 escalate 가능 +- `decorative` — 시각 보조 (배경 이미지 등). 공간 부족 시 *생략 가능* +- `reference` — 출처 / footnote / 보충 자료 + +### 1.4 정규화 parser 위치 + +신규 module (이름 *implementation step 에서 결정* — defer) : +- `extract_content_objects(section: MdxSection) -> list[ContentObject]` +- markdown AST parser (예: mistune) 활용 또는 regex 기반 v0 +- 현재 `align_sections_to_v4_granularity` 다음 단계에 삽입 + +--- + +## §2. Zone Internal Region schema (Layer A — 신규) + +본 section 은 **v1 신규**. Zone *내부* 영역 (frame *밖*) 의 entity 정의 + 3-way decision tree + region 비율 + region → frame/display interface. + +### 2.1 Internal Region entity schema + +```yaml +zone: + zone_id: str + layout_position: str # top / bottom / left / right / ... + internal_regions: + - region_id: str # zone 내 unique + role: str # primary / secondary / supporting / reference + content_type: str # text / table / image / diagram / details / mixed + ratio_estimate: float # 0.0 ~ 1.0 (zone 내 비율, 합 = 1.0) + content_unit_ids: [str] # 이 region 에 배치된 content_object id 들 (Layer A → B 의 입력) + frame_match_strategy: # region → frame/display 매칭 결과 + kind: str # frame_match | display_only + frame_id: str | null # frame_match 이면 실제 frame + display_strategy: str # inline_full | inline_preview_with_details | details_only | dropped +``` + +### 2.2 Universal Region Model + +``` +모든 Zone 은 1 개 이상의 Internal Region 을 가짐. + + text-only zone = single-region zone (현 거동의 자연 표현) + mixed-content zone = multi-region zone + +각 Internal Region 은 *자기만의* frame match + display strategy 를 가질 수 있음. +``` + +text-only section 도 *single-region zone* 으로 표현 (= 현 거동 보존). mixed-content (text + table / text + image / 등) 은 *multi-region zone* 으로 확장. region 이 *first-class entity* — special case 가 아님. + +### 2.3 3-way decision tree + +각 section 에 대해 *Internal Region 분할* 여부 결정 : + +``` +section 전체 → 1 frame 매칭 가능? + ├ YES → whole-section frame match + │ → single-region zone (region 1개, content_type=primary) + │ + └ NO → child-section grouping 가능? + ├ YES → group merge → 1 frame 매칭 + │ → single-region zone (region 1개, content_type=primary) + │ + └ NO → content-type split + → text region / table region / image region / details region + → region 비율 산정 (예: text 80% / table 20%) + → multi-region zone (region 2~N개) +``` + +**판단 기준** : +- *whole-section frame match* — section 전체와 frame contract 의 accepted_content_types 가 호환 + cardinality 가 맞는 경우 +- *child-section grouping* — sibling section 들이 같은 frame contract (예: F16 의 4-quadrant) 와 묶이는 경우. heading depth + content 구조 + frame cardinality 로 판단 +- *content-type split* — section 안에 *호환 안 되는 content type 조합* 이 있을 때 (text + table 처럼) + +### 2.4 region 비율 산정 + +content type 별 *expected size* 기반 : + +| content type | size proxy | +|---|---| +| `text_block` | line_count (text_block.size_estimate.line_count) | +| `table` | rows × line_height_factor (rows × 1.2 ~ 1.5) | +| `transform_table` | pair_count × pair_height | +| `image` | aspect_ratio 기반 height (width 고정 시) | +| `diagram` | aspect_ratio 기반 height | +| `details` | summary line_count (펼치기 전) | + +*zone 내 합 = 1.0* 으로 normalize. role 가중치 (primary > supporting) 는 v1 에서 균등 — *별 step refinement*. + +### 2.5 Internal Region Layout / Topology Vocabulary + +region 들의 *공간 배치 패턴* 어휘. multi-region zone 의 *방향 / 배치* 결정. ratio 와 content_type 만으로는 *어떻게 배치되는지* 가 결정 안 되므로 *vocabulary 단계* 가 명시적으로 필요. + +#### 명명 style — slide-level vs region-level 의 *의도된 비대칭* + +- **Slide-level 8 vocabulary** (Step 7) = *count-based* 명명 (`horizontal-2`, `top-1-bottom-2`). zone 의 *layout-driven* 성격 반영 +- **Region-level vocabulary** (본 §) = *descriptor-based* 명명 (`vertical-stack`, `main-support`). region 의 *content-type / role-driven* 성격 반영 +- 이 비대칭은 *의도된 것*. region count 가 작고 (1~4) content type / role 이 핵심 결정 기준이라 descriptor 가 더 의미 전달 + +#### v1 vocabulary (6 entry) + +| region_layout_type | 의미 | 사용 조건 | +|---|---|---| +| `region-single` | 1 region 만 (zone 전체 = region 1개) | region count = 1 (single-region zone) | +| `region-vertical-stack` | 위·아래 수직 stack | region count ≥ 2, content type 이 *순차적 흐름* (예: 본문 + supporting). default fallback | +| `region-horizontal-split` | 좌·우 수평 분할 | region count = 2, content type 이 *대등 비교* 또는 *side-by-side 시각* (text + image, text + diagram 등) | +| `region-main-support` | main region + supporting region (asymmetric ratio) | region count = 2, role = [primary, supporting], ratio asymmetric (예: 0.7 / 0.3) | +| `region-preview-details` | inline preview region + details/popup region | details_presence = true, 또는 큰 content (table N ≥ 5, long text 등) | +| `region-grid-2x2` | 2×2 grid (4 region) | region count = 4, content type 이 *대등 4 항목* | + +#### deterministic decision rule + +`region_layout_type` 은 AI 호출 X. 다음 *결정론적 함수* 로 도출 : + +``` +입력 : + - region_count : int + - content_type_mix : list[str] + - ratio_estimate : list[float] + - role 분포 : list[str] (primary / supporting / ...) + - details_presence : bool + +결정 분기 (순차 적용, 첫 매칭 채택) : + 1. region_count == 1 + → region-single + + 2. details_presence == true 또는 큰 content (table N ≥ 5 / long text 등) + → region-preview-details + + 3. region_count == 4 AND content_type_mix 가 *4 종 대등* + → region-grid-2x2 + + 4. region_count == 2 AND role == [primary, supporting] AND ratio asymmetric (max / min ≥ 2) + → region-main-support + + 5. region_count == 2 AND content_type_mix 내 visual element (image / diagram) 포함 + → region-horizontal-split + + 6. fallback (위 모든 분기 미매칭) + → region-vertical-stack +``` + +#### 출력 schema + +각 zone 의 `internal_regions` 컨테이너에 region_layout 필드 추가 : + +```yaml +zone: + zone_id: str + internal_regions: [...] + region_layout: + region_layout_type: str # 위 6 entry 중 하나 + region_order: [str] # region_id 의 배치 순서 (위→아래 / 좌→우 등) + region_placement: str # vertical | horizontal | grid | main-side | stack +``` + +#### 구현 위치 (예정) + +신규 module (이름 *implementation step 에서 결정* — defer) : +- input : `zone.internal_regions` (from §2.1, ratio + content_type 산정 후) +- output : `zone.region_layout` (region_layout_type + order + placement) +- 위치 : §2.4 region 비율 산정 *직후*, §2.6 region → frame / display interface *직전* +- 결정 함수 deterministic — AI 호출 X + +#### v1 vocabulary 의 한계 / 향후 확장 (참고) + +- 현재 6 entry = v1 starting set. 추후 sample / frame DB 확장 시 vocabulary 추가 가능 (예: `region-vertical-3` / `region-horizontal-3` / `region-main-side-bottom` 등) +- v1 fallback (`region-vertical-stack`) 이 매칭 안 되는 패턴 발견 시 별 step 으로 entry 추가 + +### 2.6 region → frame / display 매칭 interface + +각 region 은 다음 중 하나 : +- **frame_match** — region 의 content_type 에 호환되는 frame 선택. 매칭된 frame 의 contract 가 §3 의 입력. Stage B 진입 +- **display_only** — frame 없이 display strategy 로 처리 (image area 직접 / table preview / details button). frame contract 미사용. Stage B 우회 + +현재 *runtime contract-registered / verified* frame set (F13 / F29 / F16) 은 모두 text region 만 수용. image / table / details region 은 현재 *display_only* path. + +### 2.7 구현 위치 (예정) + +신규 module (이름 *implementation step 에서 결정* — defer) : +- input : `section.content_objects` (from §1) +- output : `zone.internal_regions` (with ratio + frame_match_strategy) + `zone.region_layout` (from §2.5) +- 현재 composition planner 의 frame 매칭 *직전* 에 삽입 +- region 분할 / 비율 산정 / topology vocabulary 선택 / 매칭 기준 — 모두 deterministic rule 기반 (AI 호출 X) + +--- + +## §3. frame contract 확장 — accepted_content_types + Frame Slot (Layer B) + +> **v0 의 §2 → v1 의 §3 (renumber)**. *Layer B / Frame Slot* spec. + +`templates/phase_z2/catalog/frame_contracts.yaml` 에 *2 개 신규 필드* 추가. + +### 3.1 schema + +```yaml +: + ... (기존 필드 그대로 — source_shape / cardinality / payload / visual_hints / ...) + + # NEW : 이 frame 이 받을 수 있는 content type 들 + accepted_content_types: + - text_block + - transform_table + - ... + not_accepted: # 명시적 비호환 (디버그용) + - image + - diagram + + # NEW : frame 내부 Frame Slot 선언 + # YAML field name = 'sub_zones' — 코드 / catalog reality 로 유지. 의미 = Frame Slot (Layer B). + sub_zones: + - id: str # Frame Slot 식별자 + role: main_text | supporting_visual | label | details_button | ... + accepts: # 이 Frame Slot 이 받는 content_object type + - text_block + - transform_table + cardinality: # Frame Slot 내 capacity + strict: int # 정확히 N개 + # or + min: int + max: int + partial_target_path: # frame partial template 에서 이 Frame Slot 의 위치 + # 예 : "f29b__cell--left.row-1" — partial 안 marker (attribute name *implementation step 에서 결정*) +``` + +> **YAML field 이름** : 코드 / catalog reality 로 `sub_zones` 유지. 의미는 *Frame Slot* (= Layer B). + +### 3.2 구체 예시 (현재 3 frame) + +#### F13 — three_parallel_requirements +```yaml +three_parallel_requirements: + ... + accepted_content_types: [text_block] + sub_zones: # = Frame Slots (Layer B) + - id: pillar_1 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + - id: pillar_2 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + - id: pillar_3 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } +``` + +#### F29 — process_product_two_way +```yaml +process_product_two_way: + ... + accepted_content_types: [text_block, transform_table] + sub_zones: # = Frame Slots (Layer B) + - id: process_column + role: main_text + accepts: [text_block, transform_table] + cardinality: { strict: 3 } # 3 sections per column + - id: product_column + role: main_text + accepts: [text_block] # product 쪽은 transform 안 받음 (현재 frame 의 시각적 구분) + cardinality: { strict: 3 } +``` + +#### F16 — bim_issues_quadrant_four +```yaml +bim_issues_quadrant_four: + ... + accepted_content_types: [text_block] + sub_zones: # = Frame Slots (Layer B) + - id: quadrant_1 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + - id: quadrant_2 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + - id: quadrant_3 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + - id: quadrant_4 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } +``` + +### 3.3 partial template 의 Frame Slot 마커 + +frame partial 의 HTML 에 Frame Slot 식별 marker 추가 — render 후 Selenium 이 Frame Slot 단위 측정 가능, A1~A4 의 정밀도 향상. *marker attribute name (예: `data-subzone` / `data-frame-slot` / 기타) 은 implementation step 에서 결정 — defer*. + +--- + +## §4. placement algorithm — 2-stage (Layer A → Layer B) + +> **v0 의 §3 → v1 의 §4 (renumber + 2-stage 재작성)**. *layer 순차 dependency*. + +### 4.1 input / output + +``` +input : + section: { section_id, content_objects: [...] } + zone: { zone_id, layout_position } + available_frames: [...] # V4 top-k from Step 5 + +output : + internal_regions: [ + { + region_id, role, content_type, ratio_estimate, + content_unit_ids: [...], + frame_match_strategy: { kind, frame_id, display_strategy }, + # Stage B 결과 (frame_match region 만) + slot_assignments: [ + { content_object_id, frame_slot_id, display_strategy } + ], + overflow_buffer: [...], + rejection: [...] + }, + ... + ] +``` + +### 4.2 Stage A — content → Internal Region (Layer A) + +> region 분할 결정 + content_object → region 배치. + +``` +1. 3-way decision (§2.3) 적용 + - whole-section frame fit 가능 → single-region (Stage B 로 1 region) + - child-grouping 가능 → group merge → single-region + - content-type split 필요 → multi-region + +2. multi-region 인 경우 : + - content_object.type 별로 region 분류 (text region / table region / image region / ...) + - 같은 region 안의 content_object 끼리 묶음 + - region 별 ratio 산정 (§2.4) + +3. region 별 frame_match_strategy 결정 : + - region.content_type 이 frame.accepted_content_types 에 매칭 가능 → frame_match + - 매칭 frame 없음 → display_only (image / table / details path) + +4. 결과 : zone.internal_regions = [region_1, region_2, ...] + 각 region 은 content_unit_ids + frame_match_strategy 를 가짐 +``` + +### 4.3 Stage B — Internal Region content → Frame Slot (Layer B) + +> region 의 content 를 frame 의 Frame Slot 에 배치. *frame_match_strategy.kind == "frame_match"* 인 region 에만 적용. + +각 frame_match region 에 대해 : + +``` +1. content_object 정렬 + - role 기준 우선순위 : summary > reference > detail > decorative + - 같은 role 내 raw_payload 등장 순서 + +2. content_object.type 이 frame.accepted_content_types 에 없는 것 + → rejection 으로 분리. 본 frame 부적합 신호 (Stage A 의 frame_match_strategy 재검토 신호) + +3. 남은 content_object 를 Frame Slot 들에 배치 + - 각 Frame Slot 을 순회 (frame contract 의 declaration 순서) + - Frame Slot.accepts 에 매칭되는 content_object 들에서 + cardinality.strict 또는 max 수만큼 할당 + - role 우선순위 높은 것부터 + +4. 배치 안 된 content_object + - role = decorative → 무조건 drop (생략) + - role = detail → overflow_buffer 로 (details/popup 후보) + - role = summary / reference → rejection (frame 부적합) + +5. 결과 : + - slot_assignments : 정확히 무엇이 어디로 + - overflow_buffer : details/popup 으로 escalate 할 candidate + - rejection : 본 frame 으로는 표현 불가 — frame_reselect 신호 +``` + +### 4.4 매칭 충돌 / tie-break + +Frame Slot 단위로 : + +- 동일 Frame Slot 에 다수 content_object 후보 시 : + - role 우선순위 (summary > reference > detail) + - 동률 시 size_estimate 작은 것 우선 (fit 가능성 ↑) +- 동일 content_object 가 여러 Frame Slot 에 매칭 가능 시 : + - role 매칭 우선 (Frame Slot.role == content_object.role) + - 그래도 동률이면 contract declaration 순서 (앞쪽 Frame Slot 우선) + +### 4.5 display_only region 의 처리 + +`frame_match_strategy.kind == "display_only"` 인 region 은 Stage B 우회. 대신 : +- image region → image area 직접 배치 (frame 없이, region 안에 직접 inline) +- table region → table preview (rows ≤ N inline) + 자세히보기 (rows > N popup) +- details region → details button + popup +- diagram region → diagram inline + +display strategy 어휘는 §5 와 동일 — region-level 적용. + +### 4.6 구현 위치 (예정) + +신규 module (이름 *implementation step 에서 결정* — defer) : +- `plan_placement(section, zone, available_frames) -> Placement` +- composition planner 의 frame 매칭 *직후*, slot_payload 생성 *직전* +- Stage A → Stage B 순차 실행 +- 결과를 slot_payload 생성 단계에 전달 + +--- + +## §5. 표시 전략 — inline preview vs details / popup escalation + +> **v0 의 §4 → v1 의 §5 (renumber)**. **layer-agnostic** — region-level (Stage A) + slot-level (Stage B) 둘 다 적용. *어휘 미변경*. + +### 5.1 결정 기준 (per content_object type) + +| type | inline 가능 조건 | preview + details 전환 | popup-only 전환 | +|---|---|---|---| +| `text_block` | line_count ≤ Frame Slot capacity | line_count > capacity AND role=detail | role=detail AND line_count >> capacity (예: 20+) | +| `table` (rows N) | N ≤ 4 | 5 ≤ N ≤ 7 (preview 첫 N rows + details) | N ≥ 8 (popup-only) | +| `transform_table` | rows ≤ frame 의 transform Frame Slot capacity (보통 3) | rows > capacity, 일부 inline | rows >> capacity | +| `image` | aspect_ratio fit 가능 | 일부 frame 에서 inline + details 의 thumbnail | 거의 없음 (image 는 보통 inline 또는 drop) | +| `diagram` | Frame Slot 호환 | preview thumbnail + popup full | popup-only | +| `details` (already-marked) | inline 만 안 함 (정의상) | summary inline + body popup | summary + body popup | + +### 5.2 *원문 손실 금지* 룰 + +- 표시 전략 결정은 *어디 보여줄지*. *content 자르지 / 압축 / 요약 X* +- inline preview 도 *raw_payload 의 일부* 만 빌려옴. 나머지는 details 로 +- AI 호출 X — 모든 결정은 deterministic rule 기반 + +### 5.3 적용 layer + +display strategy 어휘 (`inline_full` / `inline_preview_with_details` / `details_only` / `dropped`) 는 *동일* : +- **region-level** (Stage A 의 display_only region) — image area / table preview / details button 등 +- **slot-level** (Stage B 의 frame_match region 안 Frame Slot 별 content) — Frame Slot 안 content 가 fit 안 되면 escalate + +### 5.4 details / popup runtime + +- frame partial 또는 region container 에 `
/` 또는 별 button + popup layer +- 단순 v0 : `
` 내장 — 클릭으로 펼침 +- 향후 v1 : 별도 popup overlay (CLAUDE.md 의 자세히보기 원칙) + +### 5.5 구현 위치 (예정) + +placement planner (§4.6) 의 후속 단계 — 각 assignment / region 에 `display_strategy` 부착 : +- `inline_full` — content 전체 inline +- `inline_preview_with_details` — 일부 inline, 나머지 details +- `details_only` — summary 만 inline, content 는 popup +- `dropped` — decorative 가 공간 부족으로 생략 + +--- + +## §6. A1~A4 telemetry 와의 interface + +> **v0 의 §5 → v1 의 §6 (renumber)**. *layer-agnostic*. *구조 미변경* — `sub_zone` 단어 mechanical rename + region-level metadata 추가. + +본 composition layer 와 기존 telemetry layer (A1~A4) 가 *어떻게 흐르는지*. + +### 6.1 forward flow (composition → render → telemetry) + +``` +section + ↓ extract_content_objects +content_objects + ↓ placement_planner (Stage A → Stage B) +placement { + internal_regions: [ + { + region_id, content_type, ratio_estimate, + slot_assignments: [{content_object_id, frame_slot_id, display_strategy}], + overflow_buffer: [...], + rejection: [...], + } + ] +} + ↓ slot_payload 생성 (region + Frame Slot 단위로 grouping) +slot_payload (with region + Frame Slot metadata) + ↓ render (frame partial — Frame Slot aware + region container aware) +HTML + ↓ Selenium check +overflow signals + ↓ A1 fit_classifier +categories + ↓ A2 router +proposed_actions + ↓ A3 retry / A4 failure_classifier / next_action +final_status +``` + +### 6.2 telemetry 에 전달되는 새 metadata + +각 zone 의 debug entry 에 추가 : +```yaml +zone: + ... (기존) + internal_regions: # Layer A + - region_id + content_type + ratio_estimate + frame_match_strategy + placement: + slot_assignments: [...] # 이 zone 에 어떤 content_object 가 어디 Frame Slot 으로 + overflow_buffer: [...] # details 로 간 것 + rejection: [...] # frame 부적합 후보 + region_metrics: # Selenium 이 region 별로 측정 (Layer A) + - region_id + ch / sh / excess_y # region 단위 overflow + frame_slot_metrics: # Selenium 이 Frame Slot 별로 측정 (Layer B, frame_match region 만) + - frame_slot_id + content_object_id + ch / sh / excess_y # Frame Slot 단위 overflow +``` + +### 6.3 backward flow (telemetry → composition) + +A4 의 `next_proposed_action` 이 `frame_internal_fit_candidate` 또는 `frame_reselect` 일 때 : +- composition layer 가 *재호출* 됨 (단, retry budget 별도) +- 다른 frame 또는 다른 placement 시도 + +본 v1 에서는 *backward flow 자동화 X* (구현 단계). placement 가 정확히 되어 있으면 telemetry 거의 trigger X. + +### 6.4 fit_classifier 의 *content_type aware* 진화 + +현재 fit_classifier 는 *className → semantic_content_type* 매핑. 본 spec 적용 후 : +- Selenium 이 region marker / Frame Slot marker / content_object_id marker 를 읽음 (marker attribute name *implementation step 에서 결정* — defer) +- classifier 는 *content_object 의 type* 을 직접 알 수 있음 +- 분류 정밀도 향상 (예: F29 의 frame_match region 안 Frame Slot 의 transform-block 이 transform_table content_object 임을 *직접* 알 수 있음 — 현재는 inner_content_signals 로 추론) + +--- + +## §7. current code gap — 재사용 / 신규 분리 + +> **v0 의 §6 → v1 의 §7 (renumber)**. 신규 module 이름 *defer*. + +### 7.1 이미 있는 것 (재사용) + +- MDX parser : section 단위 (## / ### drilling) +- align_sections_to_v4_granularity +- composition planner (parent_merged_inferred 포함) +- frame_contracts.yaml + builder/parser registry +- mapper (catalog-driven slot_payload 생성) +- Jinja2 render +- 8-preset layout vocabulary +- A1~A4 telemetry chain + +### 7.2 신규 필요 + +| 항목 | 위치 | 비고 | +|---|---|---| +| **content_object 정규화** | 신규 module (이름 *defer*) | markdown AST 또는 regex 기반 v0 | +| **Internal Region planner (Layer A)** | 신규 module (이름 *defer*) | 3-way decision + region 비율 + frame_match_strategy 결정 | +| **frame_contracts.yaml** 의 `accepted_content_types` + `sub_zones` 필드 (= Frame Slot 선언) | catalog (기존 yaml 확장) | 3 frame (F13/F29/F16) 우선 | +| **placement_planner (Layer A → Layer B)** | 신규 module (이름 *defer*) | Stage A: content → Internal Region / Stage B: region content → Frame Slot | +| **display_strategy** 결정기 | placement_planner 내부 | inline_full / inline_preview_with_details / details_only / dropped | +| **frame partial 에 Frame Slot 마커** | `templates/phase_z2/families/*.html` | marker attribute name *defer* | +| **region container 마커** | `templates/phase_z2/slide_base.html` 또는 partial | region 단위 측정 marker, name *defer* | +| **details/popup runtime** | partial template 또는 base slide | `
` 우선, 추후 popup overlay | +| **fit_classifier 의 region / Frame Slot 인식** | `src/phase_z2_classifier.py` 확장 | inner_content_signals → region / Frame Slot 직접 read | +| **mapper 의 region / Frame Slot-aware slot_payload** | `src/phase_z2_mapper.py` 확장 | builder 들이 region + Frame Slot 그룹핑 인식 | + +### 7.3 정의 vs 구현 분리 + +본 spec 은 *정의만*. 구현 axis 는 별도 step : +- B1. content_extractor (MDX → content_object 정규화) +- B2. internal_region_planner (Layer A — 3-way decision + 비율 + frame_match_strategy) +- B3. frame_contracts 의 accepted_content_types + sub_zones (= Frame Slot) 선언 (3 frame) +- B4. placement_planner (Layer A → Layer B 통합) +- B5. partial / region container marker 추가 + telemetry 연동 (이름 결정 포함) +- B6. details/popup runtime + +각 axis 는 *별도 step*. 한 axis 씩 사용자 승인 후 진행. + +> **module / marker / attribute 이름** : 본 spec 에서 *defer*. implementation step 에서 결정. + +--- + +## §8. 본 spec 의 활용 + +> **v0 의 §7 → v1 의 §8 (renumber)**. + +### 8.1 composition layer 의 룰북 + +향후 frame 추가 / content_object 변경 / Layer A 재분할 / Frame Slot 매핑 변경 시 본 spec 의 schema 를 따름. *임의 매핑 / hack 차단*. + +### 8.2 telemetry 와의 cross-check + +A1~A4 의 분류 결과 (`structural_minor_overflow` 등) 가 본 spec 의 placement 결과와 *일치하는가* 확인 가능. 불일치 = composition planning 의 *예상치 못한 케이스* — 진단 자료. + +### 8.3 미사용 sample (MDX 01 / 02) 진단 + +본 spec 적용 후 MDX 01/02 를 돌리면 : +- 각 section 의 content_object 정규화 결과 visible +- 각 zone 의 Internal Region 분할 결과 visible (single vs multi) +- 어떤 content type 이 frame contract 에 없는지 (frame 추가 필요 신호) +- placement 의 rejection 비율 (frame coverage gap) +- overflow_buffer 의 details 후보 (popup runtime 필요 신호) +- display_only region 비율 (현재 frame DB 의 Layer A 미커버 영역) + +이 정보가 *generalization validation* 의 진짜 신호. + +--- + +## §9. MDX 03 의 case 를 본 spec 으로 검증 (illustrative) + +> **v0 의 §8 → v1 의 §9 (renumber)**. mechanical rename + 2-stage 표현. + +> MDX 03 = sample. *fix 대상 X*. 본 spec 룰의 *예시 적용*. + +### 9.1 03-1 의 content_object 정규화 (예상) + +```yaml +section_id: "03-1" +title: "1. DX 시행을 위한 필수 요건" +content_objects: + - id: "03-1.text-1" + type: text_block + role: summary + type_specific: { format: nested_list, bullet_count: 3 (top), nested_count: 7 } + size_estimate: { line_count: ~12 } +``` + +→ 1 content_object (text_block, role=summary). + +### 9.2 03-1 의 Stage A → Stage B (F13 contract 적용) + +**Stage A** : +- 3-way decision : section 전체가 F13 (3 pillars) 의 child grouping 으로 매칭 → *whole-section frame match* +- single-region zone, content_type=text, ratio=1.0 +- frame_match_strategy = { kind: "frame_match", frame_id: "F13" } + +**Stage B** : +- F13 sub_zones (= Frame Slots) : [pillar_1, pillar_2, pillar_3] (각 cardinality strict 1, accepts text_block) +- text_block 1 개 → 3 Frame Slot 에 어떻게 배치? +- 현재 mapper (`pillar_item` parser) 가 *implicit* 으로 top_bullet 3 개를 3 pillar 에 분배 +- 본 spec 적용 시 : text_block 의 nested 구조를 *3 sub_text_block* 으로 sub-decompose 하거나, Frame Slot cardinality 를 *aggregate (3)* 으로 해석할지 결정 필요 +- v1 단순화 : text_block 의 top-bullet 단위가 *implicit 한 sub-content_object* — 향후 explicit 화 + +### 9.3 03-2 의 case (transform_table 포함) + +```yaml +section_id: "03-2" +content_objects: + - id: "03-2.transform-1" + type: transform_table + role: summary + type_specific: { pair_count: 3 } + - id: "03-2.text-1" + type: text_block + role: detail + type_specific: { bullet_count: 1 } + - id: "03-2.text-2" + type: text_block + role: detail + type_specific: { bullet_count: 1 } + - id: "03-2.text-3" + type: text_block + role: detail + type_specific: { bullet_count: 3 (large) } + - ... (product 쪽도 4 개) +``` + +**Stage A** : +- 3-way decision : section 전체가 F29 (process/product 2-column structure) 와 매칭 → *whole-section frame match* +- single-region zone, content_type=text+transform_table, ratio=1.0 +- frame_match_strategy = { kind: "frame_match", frame_id: "F29" } + +**Stage B** : +- F29 sub_zones (= Frame Slots) : [process_column (accepts: text_block + transform_table, cardinality 3), product_column (accepts: text_block, cardinality 3)] +- process_column → transform_table + 2 text_block (3 개) +- product_column → 3 text_block +- 모두 inline_full 로 표시 + +이건 *현재 mapper (column_with_transform / column_plain) 가 implicit 으로 하는 것* — 본 spec 이 *explicit 하게 표현*. + +### 9.4 03-2 의 cell row 1 (transform_table) 의 10 px overflow 재해석 + +placement 가 explicit 하게 되어도 transform_table 이 row 1 cell 에 *콘텐츠 height 131 vs 가용 121* 인 건 변하지 않음. + +**그러나** : +- placement 가 *transform_table 의 size_estimate* 를 미리 알면 +- frame contract 의 Frame Slot 이 *expected_height* 를 declare 하면 +- planning 단계에서 *"transform_table 이 row 1 Frame Slot 의 expected_height 초과한다"* 를 사전 감지 가능 +- 그 시점에서 display_strategy = `inline_preview_with_details` 로 자동 전환 (3 transforms 중 2 inline + "1 더 보기") +- 또는 placement 가 *frame 부적합* 으로 판정 → frame_reselect 신호 + +즉 *본 spec 의 §4 placement algorithm 에 size_estimate 기반 fit pre-check* 가 들어가면 — A1~A4 telemetry 가 *trigger 안 되는 정상 path* 가 됨. + +이게 본 spec 이 가리키는 *진짜 fit policy 의 자리*. + +--- + +## 10. 다음 step (사용자 결정) + +본 spec v1 정의 후 구현 axis 후보 : +- B1. content_extractor (MDX → content_object 정규화) +- B2. internal_region_planner (Layer A — 3-way decision + 비율 + frame_match_strategy) +- B3. frame_contracts 에 accepted_content_types + sub_zones (= Frame Slot) 선언 (3 frame) +- B4. placement_planner (Layer A → Layer B 통합) +- B5. partial / region container marker 추가 + telemetry 연동 (이름 결정 포함) +- B6. details/popup runtime + +각 axis 는 *별도 step*. 사용자가 우선순위 결정. + +본 spec 자체는 *implementation 0 단계의 정의*. 다음 step 은 사용자가 잠근 후 진행. diff --git a/docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md b/docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md new file mode 100644 index 0000000..1157835 --- /dev/null +++ b/docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md @@ -0,0 +1,472 @@ +# Phase Z — master pipeline overview + +**Status** : 마스터 reference (2026-04-30 잠금). 본 문서 = *워크플로우 전체 도면*. 향후 모든 작업은 *이 22-step 도면의 어느 위치에 속하는지* 먼저 self-locate 해야 함. + +**용도** : +- 새 작업 시작 시 — "지금 하는 게 22-step 중 어느 step 인가" 식별 +- 새 spec / memory rule 추가 시 — "어느 step 의 어느 의사결정에 적용되는가" 매핑 +- 새 sample / 새 frame 추가 시 — "어디서 막힐 가능성이 높은가" 사전 예측 + +**본 문서가 *하지 않는* 것** : +- 새 구현 제안 X +- next step 추천 X +- 우선순위 결정 X +- A/B/C 선택지 X +- specific MDX sample 분석 X + +본 문서는 *기준점*. 의사결정은 별도 step 에서. + +--- + +## 3-block 구조 + +전체 22 step 은 다음 3 block 으로 grouping : + +| Block | Step 범위 | 역할 | +|---|---|---| +| **A. PRE-RENDER PLANNING** | 0 — 12 | render *전* 모든 결정 — *slide-level zone 분배* + *zone-internal region 분배* + frame / slot 매핑. *진짜 fit policy 의 중심* | +| **B. RENDER** | 13 | Jinja2 + frame partial → final.html | +| **C. POST-RENDER TELEMETRY / EXCEPTION HANDLING** | 14 — 22 | render 결과 검증 + 분류 + routing + status. *exception 처리 layer* | + +**중요** : 진짜 fit policy 의 자리는 A block (composition planning). C block (telemetry) 은 *exception 처리 + 진단 안내* layer. 둘 다 필요하지만 *위치가 다름*. + +--- + +## 위계 + 용어 (entity hierarchy) + +본 파이프라인은 다음 entity 위계 위에서 동작 : + +> **Lock phrase (canonical)** : `Slide → Zone → Internal Region → Frame → Frame Slot → Content` + +``` +Slide + └─ Zone (slide-level layout 이 만든 큰 영역) + └─ Internal Region (zone *내부* 영역, frame *밖*) + └─ Frame (Figma design 단위) + └─ Frame Slot (frame *내부* 자리) + └─ Content unit (text / table / image / details / ...) +``` + +### Universal Region Model + +> **Lock phrase (canonical)** : +> `Every Zone has 1+ Internal Regions.` +> `text-only zone = single-region.` +> `mixed-content zone = multi-region.` + +``` +모든 Zone 은 1 개 이상의 Internal Region 을 가짐. + + text-only zone = single-region zone (현 거동의 자연 표현) + mixed-content zone = multi-region zone + +각 Internal Region 은 *자기만의* : + - frame match + - display strategy (inline / preview+details / popup-only / dropped) +을 가질 수 있음. +``` + +text-only section 도 *single-region zone* 으로 표현 (= 현 거동 보존). mixed-content (text + table / text + image / 등) 은 *multi-region zone* 으로 확장. region 이 *first-class entity* — special case 가 아님. + +### 용어 표 + +| 용어 | 의미 | 위치 | +|---|---|---| +| **Slide** | 1280×720 한 장 | 최상위 | +| **Zone** | slide-level layout 이 만든 큰 영역 (top / bottom / left / right 등) | Slide 안 | +| **Internal Region** | Zone *내부* 영역, frame *밖*. content type 기반 분할 (text region / table region / image region / details region) | Zone 안 | +| **Frame** | Figma design 단위 (= F13 / F29 / F16 등) | Internal Region 안 | +| **Frame Slot** | frame *내부* 자리 (= pillar_1 / quadrant_1 / process_column 등) | Frame 안 | +| **Content unit** | MDX section 안의 typed 콘텐츠 조각 (text_block / table / image / details / ...) | Frame Slot 에 배치 | + +> **주의** : `PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` 의 `sub_zones` (YAML 필드명) 은 본 표의 *Frame Slot (Layer B)* 의미로 정의되어 있음. 본 표의 *Internal Region (Layer A)* 는 SPEC v1 §2 에 정의됨. + +--- + +## Operating Principles / Hard Locks + +본 섹션 = *anchor / index*. 각 원칙의 상세 정의 / 적용 룰 / 예외 처리는 *referenced source* 에 있음. drift 방지를 위해 OVERVIEW 는 *짧은 anchor* 로만 둠. + +### 1. MDX mapping convention + +| MDX | 슬라이드 | +|---|---| +| `# 대목차 제목` | `slide-title` | +| `# 대목차 결론` / note | `slide-footer` | +| `##` / `###` 본문 | `slide-body` 안 (layout + zone + region + frame + slot) | +| `
` | 별도 details layer | + +> 참조 : `CLAUDE.md` 의 *MDX → 슬라이드 매핑* 표 + +### 2. 자유 디자인 금지 + +Figma frame DB / catalog / frame contract 기반으로만 디자인 결정. *임의 HTML / CSS 디자인 생성 X*. AI 가 frame 자체 / layout 자체 / 새 디자인 패턴을 *생성하지 않음*. + +> 참조 : `CLAUDE.md` 디자인 원칙 + `feedback_no_hardcoding` + `feedback_blocks_must_be_css` + +### 3. 원문 무손실 + +MDX 원문 *삭제 / 요약 / 압축 금지*. AI 호출이 normal path 에서 콘텐츠를 *재작성하지 않음*. 원문은 본문 preview 또는 details/popup 어딘가에 *반드시* 보존. + +> 참조 : `feedback_ai_isolation_contract` + `PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §5.2 + +### 4. 그릇 변경 원칙 (positive form) + +콘텐츠가 안 맞을 때 *콘텐츠를 줄이지 않음*. 대신 *그릇* (layout / zone / internal region / frame / display strategy) 을 변경하여 수용. 공통 CSS / padding / tolerance 임의 축소는 *그릇 변경* 이 아님 → 금지. + +> 참조 : `feedback_phase_z_spacing_direction` + +### 5. preview / details 원칙 + +inline preview = 원문의 *일부* 만 빌려 보여주는 것. 원문은 details / popup 에 *반드시* 보존. preview 자체가 원문을 대체하지 않음. + +> 참조 : `PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §5.1 / §5.5 + +--- + +## Heritage / Current State (참고) + +본 섹션 = 시간 따라 변할 수 있는 *history / state* 기록. *원칙* 아님. frame DB 확장 / vocabulary 진화 시 mechanical 갱신. + +### 1. Type A / B / B' / B'' → 8-layout vocabulary 진화 + +기존 *Type A / B / B' / B''* 의 4 preset 은 사라진 게 아니라 **8-layout vocabulary** (single / horizontal-2 / vertical-2 / top-1-bottom-2 / top-2-bottom-1 / left-1-right-2 / left-2-right-1 / grid-2x2) 로 *일반화* 된 *전신*. Step 7 의 8 vocabulary 는 이 진화의 결과. + +### 2. 현재 runtime-verified frame set 은 text-frame 중심 + +현재 *runtime contract-registered / verified* frame set = `F13` (three_parallel_requirements) / `F29` (process_product_two_way) / `F16` (bim_issues_quadrant_four) — *모두 text 전용 성격*. `figma_to_html_agent/blocks` 의 전체 frame inventory 가 image / table / mixed frame 을 얼마나 포함하는지는 *전수 audit 전까지 미확정*. 따라서 현재 runtime 기준 : + +- *text region* → frame 매칭 (현 거동) +- *image region* / *table region* / *details region* → 현재 contract-registered frame set 안에서는 frame 매칭 근거가 부족하므로 *display strategy* 로 처리 (image area 직접 배치 / table preview / details button 등) + +**Step 9 의 region 단위 매칭은 현재 *runtime-verified frame set 기준으로 text region 만 frame 매칭 가능*** 함을 인지. 전체 frame inventory audit 또는 contract 등록 상태가 바뀌면 본 항목은 갱신 대상. + +--- + +## 22-step 상세 + +각 step 의 형식 : +> **Step N. 이름** — purpose (1-2 줄) +> **Status** : ✅ implemented / ⚠ partial / ❌ missing +> **Code 위치** : (해당 시) +> **Gap** : (해당 시) + +### Block A — PRE-RENDER PLANNING + +#### Step 0. 사전 준비 +파이프라인 가동 전 준비되어 있어야 하는 정적 자료들. catalog / contract / matching data / template / asset. + +- **포함** : Figma/BEP frame → HTML 변환물 / frame catalog / frame contract / V4 matching data + ontology / slide-base template / render assets +- **frame contract 필수 필드** : frame_id, template_id, accepted_content_types, slots, sub_zones, capacity, visual_hints, asset paths +- **Status** : ⚠ partial +- **Code 위치** : `templates/phase_z2/catalog/frame_contracts.yaml` / `tests/matching/v4_full32_result.yaml` / `templates/phase_z2/slide_base.html` / `templates/phase_z2/families/*.html` / `figma_to_html_agent/blocks/` +- **Gap** : `accepted_content_types` 와 `sub_zones` 필드 contract 에 미선언. *visual_hints* 는 일부만 (min_height_px). *density envelope* 미선언. + +#### Step 1. MDX 업로드 +사용자 MDX 파일 입력. 목표 : *MDX 1 → 자동 슬라이드 1 장*. + +- **Status** : ✅ implemented +- **Code 위치** : `src/phase_z2_pipeline.py` 의 CLI entry (`run_phase_z2_mvp1(mdx_path, run_id)`) + +#### Step 2. MDX 정규화 +업로드된 MDX 를 파이프라인 표준 구조로 변환. frontmatter 분리 / slide title / heading tree / section id / 대중소 목차 관계 / note·footer·details 분리 / **raw content 보존**. + +- **Status** : ⚠ partial +- **Code 위치** : `parse_mdx()` (frontmatter, ## sections, footer 추출) + `align_sections_to_v4_granularity()` (### drilling) +- **Gap** : heading tree 자체는 미생성 (현재 flat list). note / details 분리 미완. 대중소 목차 관계도 implicit. 정규화 결과가 *단순 문자열 + section_id* 수준 — heading tree 가 있는 *정규화 MDX 모델* 이 아직 아님. + +#### Step 3. Content Object 추출 +각 section 의 raw content 를 type 별 객체로 분해. text_block / bullet_list / numbered_list / table / image / diagram / jsx_block / note / details / long_original. *MDX 원문 보존, AI 요약 X*. + +- **Status** : ❌ missing +- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §1 (content_object 정규화 schema) + +#### Step 4. Section Internal Composition Planning +각 section 을 어떻게 다룰지 결정 — *whole-section 단일 frame 매칭* / *child-section grouping* / *content-type split* 의 3-way decision. split 인 경우 *Internal Region* 들로 분해 + region 비율 산정. **이 단계가 frame matching 보다 *먼저* 와야 함**. + +- **3-way decision** : + ``` + section 전체 → 1 frame 매칭 가능? + ├ YES → whole-section frame match (single-region zone) + └ NO → child-section grouping 가능? + ├ YES → group merge → 1 frame 매칭 (single-region zone) + └ NO → content-type split + → text region / table region / image region / details region + → region 비율 산정 (예: text 80% / table 20%) + → multi-region zone + ``` +- **출력** : + - `section_layout_signature` = text_only / text_plus_table / text_plus_image / table_heavy / image_with_caption / mixed_visual_text / details_heavy + - `composition_decision` = whole / group / split + - `internal_regions` (split 인 경우) = [{region_id, role, content_type, ratio_estimate}, ...] +- **Status** : ❌ missing +- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §2 (Internal Region schema, Layer A — entity / Universal Region Model / 3-way decision tree / 비율 산정 / topology vocabulary / region → frame·display interface). §1 의 content_object size_estimate / role 도 입력 자료. + +#### Step 5. Matching Evidence 생성 +정규화된 section + layout need 기반으로 V4 매칭 evidence 수집. *최종 선택이 아니라 후보 evidence*. + +- **대상** : 소목차 section / 중목차 parent / 필요 시 sibling group 후보 +- **V4 출력** : top-k frame candidates (frame_id, template_id, confidence, label, axes score) +- **Label** : use_as_is / light_edit / restructure / reject +- **Status** : ⚠ partial +- **Code 위치** : `lookup_v4_match()` in `phase_z2_pipeline.py` +- **Gap** : 현재 *rank-1 만* 반환. top-k 사용 안 됨. sibling group 후보도 없음. + +#### Step 6. Composition Planning +어떤 MDX 덩어리를 하나의 *slide-level zone unit* 으로 볼지 결정. child 따로 / sibling 묶기 / parent 단위. + +- **판단 기준** : heading 관계 / content_object 구조 / section_layout_signature / V4 top-k evidence / frame compatibility / capacity fit / content density +- **Status** : ⚠ partial +- **Code 위치** : `src/phase_z2_composition.py` (`plan_composition`, `parent_merged_inferred`, `capacity_fit` integration) +- **Gap** : section_layout_signature / content_object 구조 input 부재 (step 3, 4 가 없어서). frame compatibility 도 rank-1 매칭만 활용. + +#### Step 7. Slide-Level Layout Planning +composition unit 개수와 성격을 보고 slide 전체 layout 선택. *기존 Type A/B/B'/B'' 의 후속 — 8-vocabulary 로 명시화*. + +- **8 layout vocabulary** : single, horizontal-2, vertical-2, top-1-bottom-2, top-2-bottom-1, left-1-right-2, left-2-right-1, grid-2x2 +- **Status** : ⚠ partial +- **Code 위치** : `src/phase_z2_composition.py` 의 `select_layout_preset()` + `LAYOUT_PRESETS` +- **Gap** : 현재 *count-based 만* (1→single, 2→horizontal-2, 3→top-1-bottom-2, 4→grid-2x2). "성격" (content_object 분포 / section_layout_signature) 미반영. 8 preset 중 horizontal-2 + single 만 실제 검증됨. + +#### Step 8. Zone + Internal Region Ratio Planning +선택된 layout 안에서 각 zone 의 크기 / 비율 결정 + 각 zone *내부의* Internal Region 비율 결정. *두 단계 ratio* 산정 (zone-level + region-level). *50/50 고정 X*. *slide-base / title / divider / footer / gap 임의 축소 금지*. + +- **두 단계 ratio** : + - zone-level : layout 의 각 zone 크기 (slide-body 안 분배) + - region-level : 각 zone 안 Internal Region 비율 (single-region 이면 100%, multi-region 이면 Step 4 의 ratio_estimate) +- **기준** : composition unit 중요도 / content_object 분량 / text·table·image 비중 / frame aspect / capacity / min·max zone / region 별 content type +- **Status** : ⚠ partial +- **Code 위치** : `compute_zone_layout()` (min_height + content_weight 분배) + `build_layout_css()` in `phase_z2_pipeline.py` +- **Gap** : horizontal-2 만 zone-level dynamic. 나머지 7 preset 은 fr-default. **region-level ratio 미구현** (Internal Region 자체가 Step 4 부재로 입력 X). content_object 분량 기반 정밀화 미반영. + +#### Step 9. Region-Level Frame / Display Selection +각 *Internal Region* 에 들어갈 frame 또는 display strategy 확정. step 5 evidence 위에 composition / layout / region 제약 반영해 *최종* 선택. *unit of analysis = region*. single-region zone 은 자연스럽게 zone 1:1 frame 선택과 같음. + +- **region 별 처리** : + - text region → text frame 매칭 (현 runtime-verified contract set 기준 F13 / F29 / F16 등) + - table region → table preview / details / table frame + - image region → image area / image frame + - details region → details / popup 전용 region +- **Label 처리** (region 단위) : + - use_as_is → deterministic slot mapping + - light_edit → 같은 frame contract 유지, minor adaptation 가능 + - restructure → frame 후보 유지하되 content-to-slot 재배치 proposal 필요 + - reject → 자동 적용 X +- **Status** : ⚠ partial — *step 5 와 분리되지 않음 + region-level 미구현 (zone 단위 만)* +- **Code 위치** : `plan_composition()` 이 V4 rank-1 즉시 선택 (step 5 와 conflate, zone 단위) +- **Gap** : top-k 활용 / composition 제약 반영한 final 단계가 없음. *region-level 매칭 부재* (현재 zone 단위만). restructure label 은 현재 *filter* (선택 X). MVP1_ALLOWED_STATUSES = {matched_zone, adapt_matched_zone} 만 통과. + +#### Step 10. Frame Contract 확인 +선택된 frame 의 contract 읽어서 accepted_content_types / slots / sub_zones / cardinality / capacity / visual_hints / density envelope / asset 확인. + +- **Status** : ⚠ partial +- **Code 위치** : `get_contract()` + `frame_contracts.yaml` (F13/F29/F16) +- **Gap** : `accepted_content_types` 미선언. `sub_zones` 미선언. `density envelope` 미선언. +- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §3 (frame contract + Frame Slot, Layer B) + +#### Step 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping +각 zone 안에서 *Internal Region 별로* content unit 또는 child group 을 배치 → 그 region 의 frame 내부 *Frame Slot* 에 매핑. 표 작으면 inline / 크면 preview + 자세히보기 / image aspect 유지 / 긴 원문 details / text capacity 내. + +- **2 단계 매핑** : + - Layer A : content unit / child group → Internal Region (Step 4 의 region 분할 결과 소비) + - Layer B : Internal Region 안 → Frame Slot (frame contract 의 sub_zone 선언 소비) +- **Status** : ❌ missing +- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §4 (placement algorithm 2-stage: Stage A → Stage B) + §5 (display strategy). *해당 SPEC 의 `sub_zones` (YAML 필드명) = Frame Slot (Layer B). Internal Region (Layer A) 는 §2 에 정의됨.* + +#### Step 12. Slot Payload 생성 +frame partial 에 주입할 데이터 생성. *deterministic mapper* 가 기본. *AI 는 normal path 에 없음*. + +- **AI 가능 위치 (제한적)** : light_edit / restructure 에서 content_object → slot 배치 proposal 필요 시 +- **AI 금지** : MDX 원문 요약·삭제 / HTML·CSS 직접 생성 / 새 디자인 임의 / layout·frame 임의 선택 +- **Status** : ✅ implemented (deterministic 부분) +- **Code 위치** : `src/phase_z2_mapper.py` (`map_with_contract`, PAYLOAD_BUILDERS, ITEM_PARSERS) +- **Gap** : restructure label 의 AI proposal path 미구현 (현재 restructure 는 filter). content_object → sub_zone 매핑이 step 11 부재로 *implicit*. + +### Block B — RENDER + +#### Step 13. Render +Jinja2 로 HTML 생성. **고정** : slide-base / slide size / title / divider / footer / slide-body. **가변** : layout / zone ratio / frame partial / slot payload / assets. + +- **산출** : final.html / assets/ / debug.json / preview.png +- **Status** : ✅ implemented +- **Code 위치** : `render_slide()` in `phase_z2_pipeline.py` + `templates/phase_z2/slide_base.html` + `templates/phase_z2/families/*.html` + +### Block C — POST-RENDER TELEMETRY / EXCEPTION HANDLING + +> 본 block 의 핵심 — *A block (planning) 이 정밀하면 거의 trigger 안 일어남*. 이상적으로 대기 상태. exception 케이스의 *진단 + 다음 capability 안내*. + +#### Step 14. Selenium Visual Runtime Check +브라우저 렌더링 기준 실제 결과 검사. slide size / zone overflow / frame internal clipping / text·table·image clipping / content truncation. + +- **Status** : ⚠ partial +- **Code 위치** : `run_overflow_check()` in `phase_z2_pipeline.py` +- **Gap** : 현재 *text / structural element overflow* 만 검사. image aspect mismatch / table clipping / under-fill 검사 미구현. clipped_inner 의 inner_content_signals 는 추가됨 (A1 step). + +#### Step 15. Fit Classification +visual fail 발생 시 원인 분류. + +- **카테고리** : minor_overflow / structural_minor_overflow / structural_major_overflow / tabular_overflow / image_aspect_mismatch / frame_capacity_mismatch / layout_zone_mismatch / hard_visual_fail +- **Status** : ✅ implemented (text / structural 도메인) +- **Code 위치** : `src/phase_z2_classifier.py` (`classify_visual_runtime_check`, `CONTENT_TYPE_PATTERNS`) +- **Cross-reference** : `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md` §1 / §2 / §3 +- **Gap** : image_aspect_mismatch / tabular_overflow 분류는 정의됐지만 *실제 trigger 가 step 14 의 검사 부재로 일어나지 않음*. + +#### Step 16. Overflow Router +fit classification 결과를 action 후보로 매핑. + +- **매핑 예** : structural_minor_overflow → zone_ratio_retry / tabular_overflow → details_popup_candidate / image_aspect_mismatch → image_fit_candidate / frame_capacity_mismatch → frame_internal_fit_candidate +- **Status** : ✅ implemented +- **Code 위치** : `src/phase_z2_router.py` (`route_fit_classification`, `ACTION_BY_CATEGORY`) +- **Cross-reference** : `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md` §4 + +#### Step 17. Implemented Action 실행 +구현된 action 만 실행. retry budget 제한 / 성공시만 final.html promote / 실패 candidate 는 final.html 아님 / 공통 CSS·padding·tolerance 변경 X / MDX 내용 삭제·요약 X. + +- **Status** : ⚠ partial +- **Implemented** : `zone_ratio_retry` (A3) +- **Code 위치** : `src/phase_z2_retry.py` (`plan_zone_ratio_retry`, `apply_retry_to_layout_css`) + `_attempt_zone_ratio_retry` orchestrator in `phase_z2_pipeline.py` +- **Missing actions** : `layout_adjust` / `frame_reselect` / `details_popup_escalation` / `image_fit_candidate` / `frame_internal_fit_candidate` +- **Note (사용자 잠금)** : `frame_internal_fit_candidate` 가 *허용할 수 있는 내부 sub-mechanism* (density envelope / line rhythm / internal grid row / text block allocation 등) 은 *frame contract 가 declare 한 envelope 안* 에서만 동작하는 *내부 영역*. **별도 action label 로 등재하지 않음** — `density_adjust_candidate` 같은 이름은 *공통 CSS/padding 축소 antipattern* 을 초대할 위험이 있어 *unified label `frame_internal_fit_candidate` 하나* 로 묶음. + +#### Step 18. Failure Classification +action 실패 시 원인 분류. + +- **Failure types** : donor_slack_insufficient / no_donor_candidates / rerender_still_fails / not_attempted +- **Status** : ✅ implemented +- **Code 위치** : `src/phase_z2_failure_router.py` (`classify_retry_failure`, `FAILURE_TYPE_DESCRIPTIONS`) + +#### Step 19. Next Action Proposal +실패 원인 + 원래 overflow severity *함께* 보고 다음 후보 기록. failure_type 단독 X. **overflow_category + line_equivalent + failure_type 의 결합**으로 결정. + +- **예시 (severity-aware)** : + - structural_minor_overflow + donor_slack_insufficient → frame_internal_fit_candidate + - structural_major_overflow + * → details_popup_candidate + - tabular_overflow + * → table_preview_or_details_candidate + - frame mismatch → frame_reselect_candidate +- **Status** : ⚠ partial +- **Code 위치** : `src/phase_z2_failure_router.py` (`route_retry_failure`, `NEXT_ACTION_BY_FAILURE`) +- **Gap** : 현재 *failure_type 단독* mapping (1-차원). severity (overflow_category × line_equivalent) 와의 *2-차원* mapping 미구현. `frame_internal_fit_candidate` 의 *execution contract / internal envelope* 미정의 (label 자체는 router/failure routing 에 등장하지만 *실제로 어떻게 동작하는지 + frame contract 가 declare 할 envelope 의 형식* 은 미정). + +#### Step 20. Slide Status 결정 +final.html 존재 ≠ PASS. 정확한 상태 분류. + +- **Status enum** : PASS / RENDERED_WITH_VISUAL_REGRESSION / PARTIAL_COVERAGE / ABORTED +- **판단** : 모든 section coverage + visual ok → PASS / visual fail 있음 → RENDERED_WITH_VISUAL_REGRESSION / 일부 section 만 렌더 → PARTIAL_COVERAGE / 필수 단계 실패 → ABORTED +- **Status** : ✅ implemented +- **Code 위치** : `compute_slide_status()` in `phase_z2_pipeline.py` + +#### Step 21. Debug / Trace 기록 +전체 의사결정을 debug.json 에 기록. 정규화 MDX / content_objects / section_layout_signature / V4 evidence / composition_units / layout / zone sizes / frames / contracts / sub_zone mapping / slot_payload / render result / visual check / fit classification / router decision / action trace / failure classification / next action proposal / slide_status. + +- **Status** : ⚠ partial +- **Code 위치** : `write_debug_json()` in `phase_z2_pipeline.py` +- **Gap** : content_objects / section_layout_signature / sub_zone mapping 항목은 step 3, 4, 11 부재로 미기록. *region-level telemetry* (region count / region ratios / region-level frame matching / region-level display strategy) 도 Internal Region (Layer A) 부재로 미기록. 그 외 항목은 모두 기록됨. + +#### Step 22. 사용자 확인 / Export +사용자가 결과 확인. 현재 목표 = MDX → 자동 슬라이드 1 장 → status / debug. 향후 = layout 재선택 UI / top3 frame 선택 UI / zone 이동 / HTML 다운 / Gitea push. + +- **Status** : ❌ missing (UI 영역 — 현재 범위 외) +- **Code 위치** : 없음 (CLI 만) + +--- + +## Status matrix 요약 + +| Block | Step | Status | +|---|---|---| +| A | 0. 사전 준비 | ⚠ partial | +| A | 1. MDX 업로드 | ✅ | +| A | 2. MDX 정규화 | ⚠ partial | +| A | 3. Content Object 추출 | ❌ | +| A | 4. Section Internal Composition Planning | ❌ | +| A | 5. Matching Evidence | ⚠ partial (rank-1 only) | +| A | 6. Composition Planning | ⚠ partial | +| A | 7. Slide-Level Layout Planning | ⚠ partial (count-based) | +| A | 8. Zone + Internal Region Ratio Planning | ⚠ partial (zone-level horizontal-2 만 dynamic, region-level 미구현) | +| A | 9. Region-Level Frame / Display Selection | ⚠ merged with step 5 + region-level 미구현 | +| A | 10. Frame Contract 확인 | ⚠ partial (no sub_zones) | +| A | 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping | ❌ | +| A | 12. Slot Payload 생성 | ✅ (deterministic) | +| B | 13. Render | ✅ | +| C | 14. Selenium Visual Runtime Check | ⚠ partial (text/structural only) | +| C | 15. Fit Classification | ✅ | +| C | 16. Overflow Router | ✅ | +| C | 17. Implemented Action 실행 | ⚠ partial (zone_ratio_retry only) | +| C | 18. Failure Classification | ✅ | +| C | 19. Next Action Proposal | ⚠ partial (1-D mapping) | +| C | 20. Slide Status 결정 | ✅ | +| C | 21. Debug / Trace 기록 | ⚠ partial (planning trace 누락) | +| C | 22. 사용자 확인 / Export | ❌ (UI 미구현) | + +**핵심 gap 위치 (❌ 표시)** : +- Step 3 — Content Object 추출 +- Step 4 — Section Internal Composition Planning (3-way decision + Internal Region 분할) +- Step 11 — Content Unit / Child Group → Internal Region → Frame Slot Mapping +- Step 22 — 사용자 UI + +**부분 구현 위치 (⚠) 의 주요 결손** : +- Step 5 — top-k 미사용 +- Step 8 — region-level ratio 미구현 (zone-level horizontal-2 만 dynamic) +- Step 9 — Step 5 와 conflate + region-level 매칭 부재 +- Step 10 — sub_zones 미선언 (frame contract / Layer B) +- Step 14 — image / table 검사 부재 +- Step 17 — `zone_ratio_retry` 외 action 모두 미구현 +- Step 19 — severity-aware 2-차원 매핑 미구현 +- Step 21 — planning trace 누락 (step 3, 4, 11 부재 종속) + region-level telemetry 미기록 (Layer A 부재 종속) + +--- + +## 기존 spec 문서 cross-reference + +| Spec 문서 | 다루는 step | +|---|---| +| `docs/architecture/PHASE-Z-CATALOG-RUNTIME-DESIGN.md` | Step 0 (catalog 룰), Step 10 (frame contract), Step 12 (mapper) | +| `docs/architecture/PHASE-Z-FRAME-STYLE-INVENTORY.md` | Step 0 (frame inventory) | +| `docs/architecture/FRAME-INTEGRATION-MAP.md` | Step 0 (frame inventory) | +| `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md` | Step 14, 15, 16, 17, 18, 19 | +| `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` | Step 3 (§1), Step 4 (§2 Internal Region / Layer A), Step 10 (§3 frame contract + Frame Slot / Layer B), Step 11 (§4 placement 2-stage + §5 display strategy). *해당 SPEC 의 `sub_zones` (YAML 필드명) = Frame Slot (Layer B). Internal Region (Layer A) 는 §2 에 정의됨.* | + +## Memory feedback rules cross-reference + +| Memory rule | 적용 step / 의사결정 | +|---|---| +| `feedback_one_step_per_turn` | 모든 step (작업 분할 discipline) | +| `feedback_no_hardcoding` | 모든 step (특히 9, 11, 12, 17) | +| `feedback_ai_role_separation` | Step 12 (AI 위치 제한) | +| `feedback_ai_isolation_contract` | Step 12 (normal path AI 금지) | +| `feedback_phase_z_spacing_direction` | Step 17 (CSS 공통 spacing 변경 금지) | +| `feedback_artifact_status_naming` | Step 20 (slide_status enum) | +| `feedback_auto_pipeline_first` | Block C 전체 (review/UI 개념 끼우지 말 것) | +| `feedback_sample_budget` | Step 1 (미사용 sample 분리 보존) | +| `feedback_detail_quality` | 모든 step (self-check) | +| `feedback_blocks_must_be_css` | Step 13 (frame partial CSS 원칙) | +| `feedback_recipe_variety` | Step 7, 9 (vocabulary 표현 범주) | +| `feedback_absolute_paths` | 보고 / 문서 작성 시 | +| `feedback_html_preview_whitebg` | Step 13 (slide-base 배경) | +| `feedback_figma_*` | Step 0 (figma frame 변환 / asset 작업) | + +--- + +## How to use this document + +새 작업 시작 시 : +1. *어느 step* 의 작업인지 식별 +2. 그 step 의 *Status* 확인 (✅ / ⚠ / ❌) +3. 해당 step 의 cross-reference 된 spec 문서 / memory rule 확인 +4. 작업 결과가 *다른 step 에 영향* 주는지 확인 (block A 변경 → block C 의 trace 자동 변동) + +새 spec 문서 추가 시 : +- 본 문서의 *cross-reference 표* 에 등록 (어느 step 영역인지) + +새 memory rule 추가 시 : +- 본 문서의 *Memory feedback rules cross-reference* 표에 등록 (어느 step 의 의사결정인지) + +작업 도중 — *어느 step 에 속하는지 모르는 작업이 들어오면* — 본 22-step 도면에 매핑이 안 된다는 것 자체가 *작업이 over-scoped 되었거나 새 step 정의가 필요* 하다는 신호. + +--- + +## 본 문서의 보존 / 변경 정책 + +- 본 문서는 *기준점*. 가벼운 정정 / status 갱신은 진행 가능 (예: ❌ → ⚠ → ✅ 변동) +- *22 step 의 추가 / 제거 / 순서 변경* 은 사용자 명시 잠금 후에만 +- 본 문서의 *3-block 구조* 는 architectural reframe lock 의 직접 반영. 변경 시 reframe 자체를 다시 봄 diff --git a/docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md b/docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md new file mode 100644 index 0000000..46e76a9 --- /dev/null +++ b/docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md @@ -0,0 +1,128 @@ +# Phase Z — pipeline status board + +**Snapshot date** : 2026-04-30 (OVERVIEW reframe sync — Layer A / Universal Region Model / Operating Principles 반영) +**역할** : 현재 위치표 / grading snapshot. *지도 본문* 은 [`PHASE-Z-PIPELINE-OVERVIEW.md`](PHASE-Z-PIPELINE-OVERVIEW.md). + +| 문서 | 역할 | 변동 | +|---|---|---| +| `PHASE-Z-PIPELINE-OVERVIEW.md` | 고정 지도 (22-step 도면) | 거의 안 바뀜 | +| `PHASE-Z-PIPELINE-STATUS-BOARD.md` | 현재 진행 snapshot | 자주 갱신 | + +본 문서가 *하지 않는* 것 : +- 새 구현 제안 X +- next step 추천 X +- 우선순위 결정 X +- A/B/C 선택지 X +- MDX03 / MDX04 추가 분석 X +- 코드 변경 X +- OVERVIEW 구조 수정 X + +--- + +## 1. Counting rule + +``` +Step 0 = precondition (파이프라인 가동 전 사전 준비) +Step 1~22 = runtime pipeline ("22-step pipeline" = 이 범위) +총 항목 수 = 23 (Step 0 + Step 1~22) +명명 = "22-step" (runtime 기준) +``` + +Step 0 은 본체가 아닌 *준비 조건*. Step 1 (MDX 업로드) 부터가 runtime entry. + +--- + +## 2. 22-step status board + +| Block | Step | 이름 | Status | +|---|---|---|---| +| — | 0 | 사전 준비 (catalog / contract / V4 / template / asset) | ⚠ partial | +| A | 1 | MDX 업로드 | ✅ | +| A | 2 | MDX 정규화 | ⚠ partial | +| A | 3 | Content Object 추출 | ❌ | +| A | 4 | Section Internal Composition Planning | ❌ | +| A | 5 | Matching Evidence 생성 | ⚠ partial (rank-1 only) | +| A | 6 | Composition Planning | ⚠ partial | +| A | 7 | Slide-Level Layout Planning | ⚠ partial (count-based) | +| A | 8 | Zone + Internal Region Ratio Planning | ⚠ partial (zone-level horizontal-2 만 dynamic, region-level 미구현) | +| A | 9 | Region-Level Frame / Display Selection | ⚠ Step 5 와 conflate + region-level 미구현 | +| A | 10 | Frame Contract 확인 | ⚠ partial (no sub_zones) | +| A | 11 | Content Unit / Child Group → Internal Region → Frame Slot Mapping | ❌ | +| A | 12 | Slot Payload 생성 | ✅ (deterministic) | +| B | 13 | Render | ✅ | +| C | 14 | Selenium Visual Runtime Check | ⚠ partial (text/structural only) | +| C | 15 | Fit Classification (A1) | ✅ | +| C | 16 | Overflow Router (A2) | ✅ | +| C | 17 | Implemented Action 실행 (A3) | ⚠ partial (zone_ratio_retry only) | +| C | 18 | Failure Classification (A4-1) | ✅ | +| C | 19 | Next Action Proposal (A4-2) | ⚠ partial (1-D mapping) | +| C | 20 | Slide Status 결정 | ✅ | +| C | 21 | Debug / Trace 기록 | ⚠ partial (planning trace 누락 + region-level telemetry 미기록) | +| C | 22 | 사용자 확인 / Export | ⚠ future (UI 영역 — 현재 범위 외) | + +범례 : +- ✅ implemented +- ⚠ partial +- ❌ missing +- ⚠ future (현 범위 외 — 후속) + +--- + +## 3. 핵심 missing + +``` +Step 3. Content Object 추출 +Step 4. Section Internal Composition Planning + (3-way decision tree + Internal Region 분할) +Step 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping +``` + +이 3 step 의 부재가 → Step 5 / 6 / 7 / 8 (region-level) / 9 (region-level) / 10 / 12 / 21 의 ⚠ partial 의 *원인 종속*. + +**Cross-cutting missing — Zone Internal Region (Layer A)** : +- *first-class entity* 로 lock 됨 ([`PHASE-Z-PIPELINE-OVERVIEW.md`](PHASE-Z-PIPELINE-OVERVIEW.md) Universal Region Model 참조) +- Step 4 / 8 / 9 / 11 의 *granularity unit* 이 zone → region 으로 한 단계 내려감 — 그 layer 자체의 schema / 구현 *부재* +- 도면 (OVERVIEW) 에는 반영됐으나 SPEC / PLAN / 코드 어디에도 *Layer A 의 schema / 구현* 없음 + +**Step 22** 는 별도 범주 (UI 영역 — 현재 자동 파이프라인 범위 외). + +--- + +## 4. 구조 적절성 검토 (brief) + +> snapshot — 22-step *재구성 / 합치기 / 쪼개기 제안 X*. OVERVIEW 영역. + +- **3-block 구조 (A 계획 / B 렌더 / C 사후 telemetry) 적절**. 위계 추가 (Zone Internal Region) 후에도 block 경계는 변동 없음 +- **Step 3~4 가 Step 5 보다 앞** 인 순서 적절. content_object 와 internal composition decision (3-way) 이 frame matching 의 입력이어야 함 +- **Step 5 (evidence 생성) 와 Step 9 (final frame / display 선택) 가 분리** 된 구조 적절. 현재 conflate 된 건 구현 결손이지 도면 결손 아님. Step 9 의 unit of analysis = *region* 으로 reframe (OVERVIEW) +- **A1~A4 는 post-render telemetry layer**. 진짜 fit policy 의 자리는 Block A (composition planning, region 분할 포함). C block 은 *exception 처리 + 진단 안내* +- **Universal Region Model 적용 후에도 step numbering 보존** : Layer A 도입은 step 추가가 아니라 Step 4 / 8 / 9 / 11 의 *granularity unit shift* 로 흡수됨. step 0 ~ 22 그대로 + +--- + +## 5. AI 사용 위치 (runtime 기준) + +``` +runtime AI = Step 12 의 light_edit / restructure 1 곳만 + ├ 입력 : content_object + frame contract + Internal Region 배치 + Frame Slot 명세 + ├ 출력 : content → Internal Region / Frame Slot proposal + └ 금지 : MDX 원문 요약·삭제 / HTML·CSS 직접 생성 / layout·zone·region·frame 임의 선택 +``` + +Step 0 (사전 준비) 의 Figma → HTML 변환은 *precondition phase 의 작업* — runtime AI 아님. + +다른 step 에서의 AI 호출은 본 도면 안에 *없음*. + +--- + +## 6. 현재 병목 (한 줄) + +> 현재 Phase Z 는 post-render telemetry / status 는 많이 구축되었으나, *일반 MDX 대응을 위한 pre-render planning*, 특히 *content_object extraction* (Step 3) / *section internal composition planning* (Step 4 — 3-way decision + Zone Internal Region 분할) / *Internal Region → Frame Slot mapping* (Step 11) 이 핵심 gap. 추가로 *runtime contract-registered / verified frame set 이 text-frame 중심* 이라 mixed-content (image / table / details) 처리는 frame 매칭이 아니라 display strategy 로만 가능 — 전체 frame inventory audit 또는 contract 등록 확장이 별 영역. + +--- + +## 사용 방법 + +- 새 작업 들어오면 → 본 board 의 *어느 step* 의 status 를 바꾸는 작업인지 식별 +- 작업이 *Step 매핑이 안 되면* → over-scoped 또는 새 step 정의 필요 (OVERVIEW 영역) +- ✅ → ⚠ → ❌ status 전이 / 갱신 시 → 본 board 만 수정. OVERVIEW 는 step 추가/제거/순서 변경 시에만 diff --git a/src/phase_z2_content_extractor.py b/src/phase_z2_content_extractor.py new file mode 100644 index 0000000..f346a24 --- /dev/null +++ b/src/phase_z2_content_extractor.py @@ -0,0 +1,323 @@ +"""Phase Z-2 Content Object extractor (B1 v0 — dormant module). + +SPEC v1 §1 의 typed content_object schema 만족하는 dedicated extractor. + +v0 minimal : + - 지원 type : text_block, transform_table 2 개 만 (table / image / diagram / details 제외) + - role : 모두 "summary" (v0 default — role 정밀화는 별 axis) + - dormant — runtime path 미연결 (pipeline / composition / mapper 미터치) + - mapper 미수정, 기존 helper move / promote / copy 없음 + - transform_table 은 *arrow column 보존* 위해 B1 *local helper* 로 구현 + (regex / parsing 일부가 mapper helper 와 유사 — 단 mapper helper 는 arrow 폐기. + 향후 helper promote / 통합 refactor 는 별 axis) + +v0 흐름 : + section.raw_content + → 3-column markdown table 감지 (arrow glyph 포함) → transform_table + → 나머지 content → text_block (format / bullet_count / has_emphasis 분석) + → list[ContentObject] + +검증 : + - dormancy : MDX 03 final.html SHA = canonical 유지 (runtime path 미연결) + - correctness : __main__ self-test (text_block 1 case + transform_table 1 case) +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field + +# B1 v0 helper 처리 정직 기록 (기존 보고 정정 — 2026-04-30) : +# - `phase_z2_mapper` 미수정. 기존 mapper helper (`_extract_markdown_table` 등) move / +# promote / copy 없음. +# - 단 SPEC v1 §1.2 transform_table.rows = [{from, arrow, to}] schema 가 +# mapper 의 helper 출력 (from/to 만, arrow 폐기) 와 호환 안 됨. +# - 따라서 *arrow column 보존* 이 필요한 transform_table 추출 부분은 본 module 의 +# *layer-agnostic local helper* (`_capture_3col_transform_table`) 로 *별도 구현*. +# - mapper helper 와 regex / parsing 일부 유사 — 향후 *promote / 통합 refactor* 는 +# 별 axis (B1 안정 후 layer-agnostic helper module 통합 검토 가능). + + +# ─── ContentObject schema (SPEC v1 §1.1) ──────────────────────── + + +@dataclass +class ContentObject: + """SPEC v1 §1.1 base schema. v0 = text_block + transform_table 만 지원. + + Fields : + id : section 내 unique id (예: '03-2.transform-1' / '03-2.text-1') + type : "text_block" | "transform_table" + role : v0 = "summary" 만 (정밀화는 별 axis) + raw_payload : 원본 markdown (자름 / 변형 X — 원문 보존 룰) + size_estimate : type 별 (line_count / rows 등) + type_specific : type 별 detail (SPEC v1 §1.2) + """ + + id: str + type: str + role: str + raw_payload: str + size_estimate: dict = field(default_factory=dict) + type_specific: dict = field(default_factory=dict) + + +# ─── Transform table extraction ───────────────────────────────── + + +_ARROW_GLYPHS = ("➜", "➠", "→", "->", "=>") + +_TABLE_PATTERN = re.compile( + r"(^[ \t]*\|[^\n]+\|\n[ \t]*\|[\s\-:|]+\|\n(?:[ \t]*\|[^\n]+\|\n?)+)", + re.MULTILINE, +) + + +def _capture_3col_transform_table(content: str) -> tuple[dict | None, str]: + """3-column markdown table 에서 (from / arrow / to) 캡처 → transform_table. + + 본 함수 = B1 v0 의 *layer-agnostic extractor helper*. mapper 의 + `_extract_markdown_table` 와 regex / parsing 의 일부가 유사하나, mapper helper 는 + arrow column 을 폐기 (from/to 만 추출) — SPEC v1 §1.2 의 + `transform_table.rows = [{from, arrow, to}]` schema 를 직접 만족 못 함. + 따라서 arrow column 보존 필요해 본 module 안에 *별도 구현*. mapper 미수정 유지. + + *향후 helper promote / 통합 refactor 는 별 axis* — B1 안정 후 mapper 와 + *layer-agnostic helper module* 통합 검토 가능. + + arrow column 에 arrow glyph 가 있어야 transform 으로 인정. + + Returns : + ({"type_specific": ..., "raw_payload": }, content_without_table) + 또는 (None, original_content) — transform 패턴 미감지 시 + """ + m = _TABLE_PATTERN.search(content) + if not m: + return None, content + + raw_lines = [r.strip() for r in m.group(1).strip().splitlines() if r.strip()] + if len(raw_lines) < 3: # header + separator + ≥1 data row + return None, content + + data_rows = raw_lines[2:] # skip header + separator + pairs: list[dict] = [] + arrow_glyph = "" + for r in data_rows: + cells = [c.strip() for c in r.strip("|").split("|")] + if len(cells) < 3: + continue + f = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[0]) + a = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[1]) + t = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[2]) + if not arrow_glyph: + for g in _ARROW_GLYPHS: + if g in a: + arrow_glyph = g + break + pairs.append({"from": f, "arrow": a, "to": t}) + + if not pairs: + return None, content + + # transform 인지 검증 — arrow glyph 가 *어느 row 든* 등장해야 + has_arrow = any(any(g in p["arrow"] for g in _ARROW_GLYPHS) for p in pairs) + if not has_arrow: + return None, content + + type_specific = { + "pair_count": len(pairs), + "arrow_glyph": arrow_glyph, + "rows": pairs, + } + raw_table = m.group(1) + remaining = content[: m.start()] + content[m.end() :] + return ({"type_specific": type_specific, "raw_payload": raw_table}, remaining) + + +# ─── Text block extraction ────────────────────────────────────── + + +def _detect_text_block_specific(content: str) -> tuple[dict, int]: + """text_block 의 type_specific + line_count 추출. + + format 결정 : + - top bullet 0 → paragraph + - top bullet 있음, nested 0 → bullet_list + - top bullet + nested → nested_list + + Returns : + (type_specific dict, line_count) + """ + lines = content.splitlines() + + top_bullets = sum(1 for l in lines if re.match(r"^[\*\-]\s", l)) + nested_bullets = sum(1 for l in lines if re.match(r"^\s+[\*\-]\s", l)) + + # max_indent_level (2-space indent 단위) + max_indent = 0 + for l in lines: + mm = re.match(r"^( *)[\*\-]\s", l) + if mm: + level = len(mm.group(1)) // 2 + max_indent = max(max_indent, level) + + if top_bullets == 0: + fmt = "paragraph" + elif nested_bullets > 0: + fmt = "nested_list" + else: + fmt = "bullet_list" + + has_emphasis = bool( + re.search(r"\*\*[^*\n]+\*\*", content) + or re.search(r"(? list[ContentObject]: + """MDX section.raw_content → typed content_object list (SPEC v1 §1). + + v0 minimal : + - 1 section → 1~2 ContentObject (transform_table + text_block 또는 text_block 만) + - role = "summary" (모두 — v0 default) + - 미지원 type (table / image / diagram / details) = 무시 (별 axis) + - 원문 (raw_payload) = 자름 / 변형 X (원문 보존 룰) + + Args : + section : MdxSection-like 객체 (section_id, raw_content 필드 필요) + + Returns : + list[ContentObject] — 0 ~ 2 개 (content 비어 있으면 0, transform-only 면 1, mixed 면 2) + """ + content = section.raw_content + section_id = section.section_id + + objects: list[ContentObject] = [] + + # 1. transform_table 추출 시도 (3-col with arrow) + transform_result, remaining = _capture_3col_transform_table(content) + if transform_result is not None: + objects.append( + ContentObject( + id=f"{section_id}.transform-1", + type="transform_table", + role="summary", + raw_payload=transform_result["raw_payload"], + size_estimate={"rows": transform_result["type_specific"]["pair_count"]}, + type_specific=transform_result["type_specific"], + ) + ) + + # 2. text_block 추출 (transform 추출 후 남은 content, 또는 transform 없으면 전체) + text_remainder = remaining if transform_result is not None else content + if text_remainder.strip(): + text_specific, line_count = _detect_text_block_specific(text_remainder) + objects.append( + ContentObject( + id=f"{section_id}.text-1", + type="text_block", + role="summary", + raw_payload=text_remainder.strip(), + size_estimate={"line_count": line_count}, + type_specific=text_specific, + ) + ) + + return objects + + +# ─── Self-test (B1 v0 correctness 검증) ───────────────────────── + + +def _run_self_test(): + """v0 unit test : text_block 1 case + transform_table 1 case. + + scope-lock 의 검증 (b) correctness — 추출기 정확성 확인. + fixed input 기반, MDX 01/02/04 미사용. + """ + + class MockSection: + def __init__(self, section_id: str, raw_content: str): + self.section_id = section_id + self.raw_content = raw_content + + # ─── Test 1 : text_block (nested_list 형태, F13 style) ─────── + text_section = MockSection( + "test-1", + "* **기술 부족**\n" + " * 디지털 도구 미숙\n" + " * BIM 활용 제한\n" + "* **인력 부족**\n" + " * 전문가 부재\n" + "* **자연 환경**\n" + " * 지역적 제약\n", + ) + objs1 = extract_content_objects(text_section) + assert len(objs1) == 1, f"text-only section → 1 obj 기대, got {len(objs1)}" + o = objs1[0] + assert o.type == "text_block", f"type=text_block 기대, got {o.type}" + assert o.role == "summary" + assert o.id == "test-1.text-1" + assert o.type_specific["format"] == "nested_list", f"format=nested_list 기대, got {o.type_specific['format']}" + assert o.type_specific["bullet_count"] == 3, f"top bullet=3 기대, got {o.type_specific['bullet_count']}" + assert o.type_specific["max_indent_level"] >= 1, "nested 가 있으니 max_indent ≥ 1" + assert o.type_specific["has_emphasis"] is True, "**bold** 존재 → has_emphasis=True" + assert o.size_estimate["line_count"] >= 6 + assert "기술 부족" in o.raw_payload, "원문 보존 — '기술 부족' 잔존 필요" + print("[OK] Test 1 (text_block) passed.") + + # ─── Test 2 : transform_table (3-col, arrow 포함) + 잔여 text ─ + transform_section = MockSection( + "test-2", + "**프로세스 변환**\n" + "\n" + "| AS-IS | ➜ | TO-BE |\n" + "|---|---|---|\n" + "| 도면 중심 | ➜ | BIM 모델 중심 |\n" + "| 단계별 분리 | ➜ | 통합 협업 |\n" + "| 사후 검토 | ➜ | 실시간 검증 |\n" + "\n" + "추가 설명 : 위 변환이 핵심.\n", + ) + objs2 = extract_content_objects(transform_section) + assert len(objs2) == 2, f"transform+text → 2 obj 기대, got {len(objs2)}" + + # transform_table 검증 + t = objs2[0] + assert t.type == "transform_table", f"첫 obj=transform_table 기대, got {t.type}" + assert t.role == "summary" + assert t.id == "test-2.transform-1" + assert t.type_specific["pair_count"] == 3, f"pair_count=3 기대, got {t.type_specific['pair_count']}" + assert t.type_specific["arrow_glyph"] == "➜", f"arrow_glyph=➜ 기대, got {t.type_specific['arrow_glyph']}" + assert len(t.type_specific["rows"]) == 3 + assert t.type_specific["rows"][0]["from"] == "도면 중심" + assert t.type_specific["rows"][0]["to"] == "BIM 모델 중심" + assert t.size_estimate["rows"] == 3 + assert "도면 중심" in t.raw_payload, "raw_payload 에 원본 table 보존" + + # text_block 검증 (transform 제거 후 남은 content) + tb = objs2[1] + assert tb.type == "text_block", f"두번째 obj=text_block 기대, got {tb.type}" + assert tb.id == "test-2.text-1" + assert "프로세스 변환" in tb.raw_payload, "transform 제거 후 surrounding text 보존 — '프로세스 변환'" + assert "추가 설명" in tb.raw_payload, "transform 뒤 잔여 text 보존 — '추가 설명'" + print("[OK] Test 2 (transform_table + text_block) passed.") + + print("\n=== B1 v0 self-test PASS ===") + + +if __name__ == "__main__": + _run_self_test() diff --git a/src/phase_z2_internal_region_planner.py b/src/phase_z2_internal_region_planner.py new file mode 100644 index 0000000..8a9547b --- /dev/null +++ b/src/phase_z2_internal_region_planner.py @@ -0,0 +1,366 @@ +"""Phase Z-2 Internal Region planner (B2 v0 — dormant module). + +SPEC v1 §2 의 Layer A planner — ContentObject[] → InternalRegion[] + region_layout. + +v0 minimal : + - 지원 case : text_block only / text_block + transform_table 2 가지 + - 3-way decision : whole + split (group merge 미지원 — 별 axis) + - topology vocabulary 출력 : `region-single` + `region-vertical-stack` 2 entry 만 + - SPEC v1 §2.5 algorithm = rule 1 + rule 6 만 구현 (rules 2~5 명시 deferred) + - role 할당 : type 기반 (text_block → primary / transform_table → supporting) + - split 결정 : distinct content type 기준 (같은 type 만 → single region) + - frame_match_strategy : kind="frame_match" / frame_id=None (Step 9 / B4 책임) + - dormant — runtime path 미연결 (pipeline / composition / mapper 미터치) + +책임 boundary : + - B2 = region 생성 (split / role / ratio / topology) + - Step 9 / B4 = frame compatibility / frame selection / display strategy + - accepted_content_types 기반 compatibility 판단은 B2 책임 *아님* + +frame_contracts 인자 : + - signature 에 둠 (future hook). v0 에서는 *output 결정에 사용 X*. + - 향후 display_only 활성화 / B4 통합 시 hook 자리. + +검증 : + - dormancy : MDX 03 final.html SHA = canonical 유지 (runtime path 미연결) + - correctness : __main__ self-test (text-only 1 case + text+transform 1 case) +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Optional + +# B2 v0 input contract = B1 의 ContentObject (phase_z2_content_extractor). +# 두 module 모두 dormant — runtime path 와 무관한 *layer-agnostic 의존*. +from phase_z2_content_extractor import ContentObject + + +# ─── Constants (B2 v0 lock) ────────────────────────────────────── + + +# transform_table 의 size proxy 환산 계수 (SPEC v1 §2.4 size proxy). +# pair 1 개 = 1.5 line 등가 (heuristic, content 기반 ratio 산정용). +# 정밀화는 향후 axis (visual_hints / content density signal). +_PAIR_HEIGHT_FACTOR = 1.5 + + +# ─── Output schema (SPEC v1 §2.1 + §2.5) ───────────────────────── + + +@dataclass +class InternalRegion: + """SPEC v1 §2.1 Internal Region entity schema. + + Fields : + region_id : zone 내 unique id (예: '{section_id}.region-1') + role : 'primary' | 'supporting' | (B2 v0 = 두 개만 사용) + content_type : 'text_block' | 'transform_table' (v0 supported) + ratio_estimate : zone 내 비율 (sum normalize = 1.0) + content_unit_ids : 본 region 에 묶인 content_object id list + frame_match_strategy : {kind, frame_id, display_strategy} + — B2 v0 에서 kind="frame_match" / frame_id=None / + display_strategy="inline_full" 고정. + 실제 frame 결정은 Step 9 / B4 책임. + """ + + region_id: str + role: str + content_type: str + ratio_estimate: float + content_unit_ids: list[str] + frame_match_strategy: dict + + +@dataclass +class RegionLayout: + """SPEC v1 §2.5 region_layout — zone 내 region 들의 *공간 배치 패턴*. + + B2 v0 가 출력하는 vocabulary : + - 'region-single' (rule 1 — region_count == 1) + - 'region-vertical-stack' (rule 6 fallback — 그 외) + + rules 2~5 (region-preview-details / region-grid-2x2 / region-main-support / + region-horizontal-split) 는 SPEC 정의 있으나 *B2 v0 deferred*. + """ + + region_layout_type: str # 'region-single' | 'region-vertical-stack' + region_order: list[str] # region_id 의 배치 순서 + region_placement: str # 'single' | 'vertical' + + +@dataclass +class ZoneRegionPlan: + """B2 v0 의 출력 — 1 zone 의 region 분할 결과 + layout. + + Fields : + internal_regions : list[InternalRegion] + region_layout : RegionLayout + """ + + internal_regions: list[InternalRegion] = field(default_factory=list) + region_layout: Optional[RegionLayout] = None + + +# ─── Helpers ───────────────────────────────────────────────────── + + +# B2 v0 지원 type 별 (role, size proxy 추출 함수). +# - text_block : role=primary, size = size_estimate.line_count +# - transform_table : role=supporting, size = size_estimate.rows × _PAIR_HEIGHT_FACTOR +_TYPE_ROLE: dict[str, str] = { + "text_block": "primary", + "transform_table": "supporting", +} + + +def _size_proxy(obj: ContentObject) -> float: + """Content object 의 *공간 크기 proxy* (SPEC v1 §2.4). + + text_block : line_count + transform_table : rows × _PAIR_HEIGHT_FACTOR + 그 외 : 0 (B2 v0 미지원 type) + """ + if obj.type == "text_block": + return float(obj.size_estimate.get("line_count", 0)) + if obj.type == "transform_table": + return float(obj.size_estimate.get("rows", 0)) * _PAIR_HEIGHT_FACTOR + return 0.0 + + +def _group_by_type_preserving_order( + content_objects: list[ContentObject], +) -> dict[str, list[ContentObject]]: + """content_objects 를 type 별로 grouping. 등장 순서 보존 (dict 의 ordered 특성).""" + groups: dict[str, list[ContentObject]] = {} + for obj in content_objects: + groups.setdefault(obj.type, []).append(obj) + return groups + + +# region_order 결정 시 type 우선순위 — primary 먼저, supporting 다음. +# B2 v0 type 만 등록. 향후 axis 에서 secondary / reference 추가 가능. +_TYPE_ORDER_PRIORITY: dict[str, int] = { + "text_block": 0, # primary + "transform_table": 1, # supporting +} + + +# ─── Public entry ──────────────────────────────────────────────── + + +def plan_internal_regions( + content_objects: list[ContentObject], + frame_contracts: Optional[list[dict[str, Any]]] = None, # v0 unused, future hook + section_id: str = "", +) -> ZoneRegionPlan: + """ContentObject[] → ZoneRegionPlan (region 분할 + topology + ratio + role). + + B2 v0 algorithm : + 1. content_objects 를 type 별로 grouping (등장 순서 보존) + 2. distinct type 수 → region_count 결정 (split 결정) + 3. region 별 : + - role = type 기반 (_TYPE_ROLE) + - ratio_estimate = type 내 size proxy 합 / 전체 합 (normalize=1.0) + - frame_match_strategy = {kind: 'frame_match', frame_id: None, + display_strategy: 'inline_full'} (Step 9 / B4 영역) + 4. topology vocabulary 결정 — SPEC v1 §2.5 : + - rule 1 : region_count == 1 → region-single + - rules 2~5 : *deferred* (SPEC 정의만, B2 v0 미구현) + - rule 6 fallback : 그 외 → region-vertical-stack + 5. region_order = type priority (primary → supporting) 순. + + Args : + content_objects : list[ContentObject] — B1 v0 extractor 출력 + frame_contracts : v0 unused (future hook). signature 에 두되 output 결정에 미사용. + section_id : region_id 생성용 prefix + + Returns : + ZoneRegionPlan (1 zone 의 plan, singular). + + Note : + - frame_contracts 무시 — 본 v0 는 *frame compatibility 판단 안 함*. + compatibility 판단은 Step 9 / B4 책임. + - empty content_objects → empty plan (region_layout=None) — caller 가 사전 차단 권장. + """ + if not content_objects: + return ZoneRegionPlan() + + # 1. type 별 grouping + groups = _group_by_type_preserving_order(content_objects) + + # 2. region 별 size proxy 합 + 전체 합 + type_sizes: dict[str, float] = {} + for ctype, objs in groups.items(): + type_sizes[ctype] = sum(_size_proxy(o) for o in objs) + total_size = sum(type_sizes.values()) + if total_size <= 0: + # 모든 size proxy = 0 인 edge case (예: 빈 content) → equal split fallback + equal_share = 1.0 / max(len(groups), 1) + for ctype in groups: + type_sizes[ctype] = equal_share + total_size = sum(type_sizes.values()) or 1.0 + + # 3. region 생성 (type 우선순위 순으로) + sorted_types = sorted( + groups.keys(), + key=lambda t: _TYPE_ORDER_PRIORITY.get(t, 99), + ) + regions: list[InternalRegion] = [] + for idx, ctype in enumerate(sorted_types, start=1): + objs = groups[ctype] + ratio = type_sizes[ctype] / total_size + regions.append( + InternalRegion( + region_id=f"{section_id}.region-{idx}", + role=_TYPE_ROLE.get(ctype, "primary"), # 미지원 type fallback = primary + content_type=ctype, + ratio_estimate=round(ratio, 4), + content_unit_ids=[o.id for o in objs], + frame_match_strategy={ + "kind": "frame_match", + "frame_id": None, # Step 9 / B4 영역 + "display_strategy": "inline_full", # v0 default + }, + ) + ) + + # 4. topology vocabulary 결정 (SPEC v1 §2.5 algorithm — rule 1 + rule 6 만) + region_count = len(regions) + if region_count == 1: + # rule 1 + layout_type = "region-single" + placement = "single" + else: + # rules 2~5 = B2 v0 deferred (SPEC 정의만, 미구현) : + # - rule 2 region-preview-details : details_presence path 미구현 + # - rule 3 region-grid-2x2 : 4 region 미지원 + # - rule 4 region-main-support : role asymmetric trigger 미구현 + # - rule 5 region-horizontal-split : visual element type 미지원 + # rule 6 fallback + layout_type = "region-vertical-stack" + placement = "vertical" + + # 5. region_order = 위 sorted_types 순 (primary → supporting) + region_order = [r.region_id for r in regions] + + return ZoneRegionPlan( + internal_regions=regions, + region_layout=RegionLayout( + region_layout_type=layout_type, + region_order=region_order, + region_placement=placement, + ), + ) + + +# ─── Self-test (B2 v0 correctness 검증) ───────────────────────── + + +def _run_self_test(): + """v0 unit test : text-only 1 case + text+transform 1 case. + + scope-lock 의 검증 (b) correctness — planner 정확성 확인. + fixed input 기반, MDX 01/02/04 미사용. + """ + + # ─── Test 1 : text-only (1 ContentObject) ──────────────────── + text_obj = ContentObject( + id="test-1.text-1", + type="text_block", + role="summary", + raw_payload="* 본문\n * nested", + size_estimate={"line_count": 6}, + type_specific={"format": "nested_list", "bullet_count": 1, "max_indent_level": 1, "has_emphasis": False}, + ) + plan1 = plan_internal_regions([text_obj], section_id="test-1") + assert plan1.region_layout is not None + assert plan1.region_layout.region_layout_type == "region-single", \ + f"text-only → region-single 기대, got {plan1.region_layout.region_layout_type}" + assert plan1.region_layout.region_placement == "single" + assert len(plan1.internal_regions) == 1, f"1 region 기대, got {len(plan1.internal_regions)}" + r = plan1.internal_regions[0] + assert r.region_id == "test-1.region-1" + assert r.role == "primary", f"text-only role=primary 기대, got {r.role}" + assert r.content_type == "text_block" + assert r.ratio_estimate == 1.0, f"단일 region ratio=1.0 기대, got {r.ratio_estimate}" + assert r.content_unit_ids == ["test-1.text-1"] + assert r.frame_match_strategy["kind"] == "frame_match" + assert r.frame_match_strategy["frame_id"] is None, "B2 v0 frame_id=None lock" + assert r.frame_match_strategy["display_strategy"] == "inline_full" + assert plan1.region_layout.region_order == ["test-1.region-1"] + print("[OK] Test 1 (text-only) passed.") + + # ─── Test 2 : text + transform_table (2 ContentObject) ──────── + text_obj2 = ContentObject( + id="test-2.text-1", + type="text_block", + role="summary", + raw_payload="* 본문", + size_estimate={"line_count": 6}, + type_specific={"format": "bullet_list", "bullet_count": 1, "max_indent_level": 0, "has_emphasis": False}, + ) + transform_obj = ContentObject( + id="test-2.transform-1", + type="transform_table", + role="summary", + raw_payload="| AS-IS | ➜ | TO-BE |\n|---|---|---|\n| a | ➜ | b |\n| c | ➜ | d |", + size_estimate={"rows": 2}, + type_specific={"pair_count": 2, "arrow_glyph": "➜", "rows": [ + {"from": "a", "arrow": "➜", "to": "b"}, + {"from": "c", "arrow": "➜", "to": "d"}, + ]}, + ) + plan2 = plan_internal_regions([text_obj2, transform_obj], section_id="test-2") + assert plan2.region_layout is not None + assert plan2.region_layout.region_layout_type == "region-vertical-stack", \ + f"text+transform → region-vertical-stack (rule 6 fallback) 기대, got {plan2.region_layout.region_layout_type}" + assert plan2.region_layout.region_placement == "vertical" + assert len(plan2.internal_regions) == 2, f"2 region 기대, got {len(plan2.internal_regions)}" + + # region_order = primary first (text), supporting second (transform) + assert plan2.region_layout.region_order == ["test-2.region-1", "test-2.region-2"] + + # text region (region-1, primary) + text_r = plan2.internal_regions[0] + assert text_r.region_id == "test-2.region-1" + assert text_r.role == "primary" + assert text_r.content_type == "text_block" + # ratio : 6 / (6 + 2*1.5) = 6/9 ≈ 0.667 + expected_text_ratio = 6.0 / (6.0 + 2.0 * 1.5) + assert abs(text_r.ratio_estimate - expected_text_ratio) < 0.001, \ + f"text ratio {expected_text_ratio:.4f} 기대, got {text_r.ratio_estimate}" + assert text_r.content_unit_ids == ["test-2.text-1"] + assert text_r.frame_match_strategy["kind"] == "frame_match" + assert text_r.frame_match_strategy["frame_id"] is None + + # transform region (region-2, supporting) + tr_r = plan2.internal_regions[1] + assert tr_r.region_id == "test-2.region-2" + assert tr_r.role == "supporting", f"transform_table role=supporting 기대, got {tr_r.role}" + assert tr_r.content_type == "transform_table" + expected_tr_ratio = (2.0 * 1.5) / (6.0 + 2.0 * 1.5) + assert abs(tr_r.ratio_estimate - expected_tr_ratio) < 0.001, \ + f"transform ratio {expected_tr_ratio:.4f} 기대, got {tr_r.ratio_estimate}" + assert tr_r.content_unit_ids == ["test-2.transform-1"] + assert tr_r.frame_match_strategy["frame_id"] is None + + # ratio sum normalize = 1.0 + ratio_sum = text_r.ratio_estimate + tr_r.ratio_estimate + assert abs(ratio_sum - 1.0) < 0.01, f"ratio sum=1.0 기대, got {ratio_sum}" + + # frame_contracts 인자 unused 검증 — None 으로 호출 / dict 으로 호출 결과 동일해야 함 + plan2_with_contracts = plan_internal_regions( + [text_obj2, transform_obj], + frame_contracts=[{"template_id": "dummy", "accepted_content_types": ["text_block"]}], + section_id="test-2", + ) + assert plan2_with_contracts.region_layout.region_layout_type == plan2.region_layout.region_layout_type + assert len(plan2_with_contracts.internal_regions) == len(plan2.internal_regions) + print("[OK] Test 2 (text+transform, vertical-stack, ratio 6:3) passed.") + + print("\n=== B2 v0 self-test PASS ===") + + +if __name__ == "__main__": + _run_self_test() diff --git a/templates/phase_z2/catalog/frame_contracts.yaml b/templates/phase_z2/catalog/frame_contracts.yaml new file mode 100644 index 0000000..f1929dc --- /dev/null +++ b/templates/phase_z2/catalog/frame_contracts.yaml @@ -0,0 +1,181 @@ +# Phase Z-2 frame contracts catalog (executable v0). +# +# 목적 : +# Python mapper 에 박혀있던 slot 구조 / cardinality / visual role 을 catalog 로 끌어냄. +# generic mapper (src/phase_z2_mapper.py:map_with_contract) 가 본 catalog 를 읽고 +# MdxSection → slot_payload 변환. frame 별 hand-coded mapper 를 제거 방향. +# +# 원칙 : +# - frame 추가 = 본 yaml 에 entry 추가 (Python 수정은 새 builder/parser 필요시에만) +# - role_order = 순서 기반 visual role (label 키워드 기반 X — 다른 MDX 도 깨지지 않게) +# - cardinality strict 위반 → FitError → fallback path (AI restructuring 후보) +# - payload.builder = PAYLOAD_BUILDERS named registry 의 entry +# - builder 가 ITEM_PARSERS / COLUMN_BODY_PARSERS 같은 sub-primitive 호출 +# +# v0 등록 frame : +# F13 three_parallel_requirements → builder=items_with_role +# F29 process_product_two_way → builder=process_product_pair +# F16 bim_issues_quadrant_four → builder=quadrant_flat_slots +# 이 셋이 현재 runtime 에서 실제 쓰는 주요 frame 의 모든 mapper. + +three_parallel_requirements: + template_id: three_parallel_requirements + frame_id: 1171281190 + family: three_parallel + + source_shape: top_bullets + cardinality: + strict: 3 + overflow_policy: abort_or_review + + # 순서 기반 visual role (color_class 대체 — label 키워드 의존 제거) + role_order: + - tech + - people + - nature + + # zone-level layout 힌트 — pipeline 의 compute_zone_layout 이 본 값을 lower bound 로 사용. + # 230 = 본 frame 의 token-based font 기준 최소 가독 높이 (Figma → CSS adapt 검증값). + visual_hints: + min_height_px: 230 + + # NEW (SPEC v1 §3) : 본 frame 이 받을 수 있는 content_object type (Layer A → Layer B 매칭 입력). + # 현재 mapper 는 본 필드를 *읽지 않음* — dormant. Layer A planner (B2) 가 도입 시 소비 예정. + accepted_content_types: + - text_block + + # NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언 — frame 내부 자리. + # YAML field name 'sub_zones' = 코드/catalog reality 유지. 의미 = Frame Slot. + # partial_target_path = path *식별자* 만 (값). 실제 marker attribute (data-frame-slot 등) 은 별 axis (B5). + sub_zones: + - id: pillar_1 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + partial_target_path: ".f13b__cols > .f13b__col:nth-child(1)" + - id: pillar_2 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + partial_target_path: ".f13b__cols > .f13b__col:nth-child(2)" + - id: pillar_3 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + partial_target_path: ".f13b__cols > .f13b__col:nth-child(3)" + + payload: + title: + source: section.title + builder: items_with_role + builder_options: + item_parser: pillar_item # ITEM_PARSERS entry + array_root: pillars # payload[array_root] = list of items + role_field: color_class # role_order[i] → item[role_field] + + +process_product_two_way: + template_id: process_product_two_way + frame_id: 1171281210 + family: two_column_h3 + + source_shape: h3_subsections + cardinality: + strict: 2 # F29 frame = 2 visual columns. ≠2 → fallback. + overflow_policy: abort_or_review + + # transform-block + 3 sections 의 breathing 위해 345 (이전 hardcoded 동일). + visual_hints: + min_height_px: 345 + + # NEW (SPEC v1 §3) : 본 frame 이 받을 수 있는 content_object type. + # process column 은 transform_table (AS-IS/TO-BE) 도 수용. product column 은 text_block 만. + # 현재 mapper 는 본 필드를 *읽지 않음* — dormant. + accepted_content_types: + - text_block + - transform_table + + # NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언. + # 2 column × 3 section = 6 cell, but Frame Slot 단위 = column. + # partial_target_path = path 식별자 만 (값). marker attribute 결정은 별 axis. + sub_zones: + - id: process_column + role: main_text + accepts: [text_block, transform_table] + cardinality: { strict: 3 } # 3 sections per column + partial_target_path: ".f29b__grid .f29b__cell--left" + - id: product_column + role: main_text + accepts: [text_block] # product 쪽은 transform 안 받음 (현재 frame 의 시각 구분) + cardinality: { strict: 3 } + partial_target_path: ".f29b__grid .f29b__cell--right" + + payload: + title: + source: section.title + builder: process_product_pair + builder_options: + pad_sections_to: 3 # 각 column.sections 길이 = 3 (legacy 와 동일) + columns: + - title_to: banner_left + body_to: process + body_parser: column_with_transform # 첫 top-bullet AS-IS/TO-BE 표 인식 + - title_to: banner_right + body_to: product + body_parser: column_plain # 모든 section = 일반 text_lines + + +bim_issues_quadrant_four: + template_id: bim_issues_quadrant_four + frame_id: 1171281193 + family: bim_issues_quadrant + + source_shape: top_bullets + # F16 정책 = pad_to=4 + truncate>4 (legacy 와 동일). cardinality strict 화는 본 transition 범위 외. + # 향후 normal path 안정 후 strict 적용 + 위반 시 fallback path (FitError) 검토. + + # NEW (SPEC v1 §3) : 본 frame 이 받을 수 있는 content_object type. + # 현재 mapper 는 본 필드를 *읽지 않음* — dormant. + accepted_content_types: + - text_block + + # NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언 — 4 quadrant. + # sub_zone 별 cardinality strict: 1 = 각 Frame Slot 의 *capacity* (Layer B schema). + # 기존 payload.builder_options.pad_to=4 / truncate_at=4 = mapper runtime 거동 (input mismatch 시). + # 두 layer 는 *공존* — 본 axis 에서 builder_options 는 미변경. + sub_zones: + - id: quadrant_1 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + partial_target_path: ".f16b__quadrant--tl" + - id: quadrant_2 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + partial_target_path: ".f16b__quadrant--tr" + - id: quadrant_3 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + partial_target_path: ".f16b__quadrant--bl" + - id: quadrant_4 + role: main_text + accepts: [text_block] + cardinality: { strict: 1 } + partial_target_path: ".f16b__quadrant--br" + + payload: + title: + source: section.title + builder: quadrant_flat_slots + builder_options: + item_parser: quadrant_item # ITEM_PARSERS entry + pad_to: 4 # quadrant_1..4 모두 채움 (부족 시 empty pad) + truncate_at: 4 # 5번째 이후 무시 + _truncated_count 기록 + label_key_pattern: "quadrant_{n}_label" + body_key_pattern: "quadrant_{n}_body" + empty_label: "" + empty_body: [] + # implicit slot_order = [quadrant_1=TL, quadrant_2=TR, quadrant_3=BL, quadrant_4=BR] + # 위치(TL/TR/BL/BR) 매핑은 partial template (families/bim_issues_quadrant_four.html) 결정.