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
This commit is contained in:
285
docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md
Normal file
285
docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md
Normal file
@@ -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) 의 자연 산출물
|
||||
849
docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md
Normal file
849
docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md
Normal file
@@ -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` — `<details>/<summary>` 또는 ":::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
|
||||
<template_id>:
|
||||
... (기존 필드 그대로 — 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 에 `<details>/<summary>` 또는 별 button + popup layer
|
||||
- 단순 v0 : `<details>` 내장 — 클릭으로 펼침
|
||||
- 향후 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 | `<details>` 우선, 추후 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 은 사용자가 잠근 후 진행.
|
||||
472
docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md
Normal file
472
docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md
Normal file
@@ -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>` | 별도 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 자체를 다시 봄
|
||||
128
docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md
Normal file
128
docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md
Normal file
@@ -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 추가/제거/순서 변경 시에만
|
||||
323
src/phase_z2_content_extractor.py
Normal file
323
src/phase_z2_content_extractor.py
Normal file
@@ -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": <table markdown>}, 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"(?<!\*)\*[^*\n]+\*(?!\*)", content)
|
||||
)
|
||||
|
||||
line_count = sum(1 for l in lines if l.strip())
|
||||
|
||||
type_specific = {
|
||||
"format": fmt,
|
||||
"bullet_count": top_bullets,
|
||||
"max_indent_level": max_indent,
|
||||
"has_emphasis": has_emphasis,
|
||||
}
|
||||
return type_specific, line_count
|
||||
|
||||
|
||||
# ─── Public entry ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def extract_content_objects(section) -> 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()
|
||||
366
src/phase_z2_internal_region_planner.py
Normal file
366
src/phase_z2_internal_region_planner.py
Normal file
@@ -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()
|
||||
181
templates/phase_z2/catalog/frame_contracts.yaml
Normal file
181
templates/phase_z2/catalog/frame_contracts.yaml
Normal file
@@ -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) 결정.
|
||||
Reference in New Issue
Block a user