Compare commits
2 Commits
0a9327c50c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b6d86a73c | |||
| b56fd20ae5 |
344
docs/architecture/PHASE-Q-AUDIT.md
Normal file
344
docs/architecture/PHASE-Q-AUDIT.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Phase Q Audit & Salvage
|
||||
|
||||
**작성일**: 2026-05-08 (frontend 통합 session 직후 첫 번째 audit axis)
|
||||
**문서 역할**: Phase Z 보완 항목을 *기준 (lens)* 으로 기존 Phase Q 코드의 **Keep / Migrate / Reference / Delete** 결정.
|
||||
**진행 상태**: §1 Audit Lens 채움 — §2 모듈별 audit / §3 Salvage Plan / §4 우선순위 재정렬은 후속 turn 에서 채움.
|
||||
|
||||
---
|
||||
|
||||
## 0. 목적
|
||||
|
||||
Phase Z 가 fresh rewrite 처럼 진행되면서 Phase Q 의 기존 자산 (mdx_normalizer / section_parser / fit_verifier / slide_measurer / content_editor 등) 을 충분히 흡수하지 못한 부분을 정리한다.
|
||||
|
||||
이번 frontend 통합 session 에서 *frontend 가 임시로 채운 영역* (§7-B 표) 이 사실은 Phase Q 에 비슷한 코드가 있었을 가능성이 큼. **무엇을 새로 만들고 / 무엇을 가져오고 / 무엇을 참고만 하고 / 무엇을 버릴지** 결정하는 것이 본 audit 의 목적.
|
||||
|
||||
진행 방식:
|
||||
1. **§1 Audit Lens** — Phase Z 보완 항목 (§7-B 의 A/B/D 그룹) 별 *목적 / input / output / Phase Q 후보 파일* 정리. 이게 audit 의 기준.
|
||||
2. **§2 모듈별 Audit** — Phase Q 모듈 10 개를 § 1 의 lens 로 검토. 결정 = Keep / Migrate / Reference / Delete.
|
||||
3. **§3 Salvage Plan** — §1 항목별 *어떤 Phase Q 자산을 어떤 방식으로 가져올지* 매핑.
|
||||
4. **§4 우선순위 재정렬** — audit 결과로 [`PHASE-Z-ROADMAP.md`](PHASE-Z-ROADMAP.md) §5 / §7-B 우선순위 재조정.
|
||||
|
||||
> §7-B 의 **C 그룹 (Frontend 일관성 정리)** 은 Phase Q 와 무관하므로 본 audit 대상 외. 별도 axis 에서 처리.
|
||||
|
||||
---
|
||||
|
||||
## 0-A. Phase Q Salvage 원칙 (lock)
|
||||
|
||||
Phase Z 는 본체이고, Phase Q 는 부품 창고 / 참고 자산이다. Phase Q 의 *전체 흐름* 을 Phase Z 위에 덮어쓰지 않는다. Phase Z 의 22-step 도면 / step artifact / V4 catalog / frame contract / visual check / status board 가 *기준 구조*. Phase Q 의 빈 부분만 부품 단위로 가져온다.
|
||||
|
||||
본 audit / salvage 의 모든 결정 (§1 lens / §2 module / §3 salvage / §4 reorder) 은 아래 10 원칙을 따른다.
|
||||
|
||||
1. **Phase Z 22-step 구조는 유지한다.**
|
||||
2. **Phase Q 코드는 Step 단위 빈칸을 채울 때만 가져온다.**
|
||||
3. **가져올 때는 adapter 를 둔다.**
|
||||
4. **기존 Phase Z artifact schema 를 깨지 않는다.**
|
||||
5. **모든 migration 은 one-axis 단위로 한다.**
|
||||
6. **MDX03 regression 을 매번 확인한다.**
|
||||
7. **HTML-heavy MDX 는 신규 케이스로 별도 확인한다.**
|
||||
8. **Reference 판정된 코드는 직접 연결하지 않는다.**
|
||||
9. **Migrate 시 dual-write / shadow 검증을 먼저 한다.**
|
||||
10. **Reference 항목은 활성화 axis 가 없으면 나중에 Delete 후보로 재분류한다.**
|
||||
|
||||
> 꼬임 방지의 핵심 = **3 (adapter)** + **4 (artifact schema 보존)** + **9 (dual-write 검증)**. 나머지 7 개는 그 위의 안전장치.
|
||||
|
||||
---
|
||||
|
||||
## 1. Audit Lens — Phase Z 보완 항목별 목적 / input / output
|
||||
|
||||
각 항목은 [`PHASE-Z-ROADMAP.md`](PHASE-Z-ROADMAP.md) §7-B 와 1:1 매핑.
|
||||
|
||||
| id | 보완 항목 | 목적 | input | output | Phase Q 후보 파일 | 우선순위 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **A-1** | Stage 0 normalize 통합 | HTML-heavy / 비정형 raw MDX 를 Phase Z canonical input 으로 변환 | raw MDX text | `{clean_text, title, images, popups, tables, sections}` (frontmatter / 코드블록 보호 / list/table HTML 변환 / AST 구조 추출) | `mdx_normalizer.py`, `section_parser.py` | 높음 |
|
||||
| **A-2** | Catalog 확장 (frame_contracts + frame_partials) | V4 32 후보 중 backend 적용 가능한 frame 수 증가 (현재 3 → 32 목표) | `figma_to_html_agent/blocks/{frame_id}/` 의 index.html / assets / analysis.md | `templates/phase_z2/catalog/frame_contracts.yaml` entry + `templates/phase_z2/frames/{template_id}.html` partial | `block_reference.py`, `block_selector.py` | 높음 |
|
||||
| **A-3** | Frame preview png 일관성 | 모든 catalog frame 의 일관된 preview.png 자동 생성 (현재 figma_previews 우회) | frame partial HTML + assets | `figma_to_html_agent/blocks/{frame_id}/preview.png` | `renderer.py`, `html_generator.py` (selenium 캡처 흔적 추정) | 중 |
|
||||
| **A-4** | slide-base.html iframe-friendly mode | iframe embed 시 body padding / centering / min-height 미적용 (frontend CSS injection 제거) | slide-base.html template + query string `?embedded=1` 같은 시그널 | conditional CSS (standalone vs embedded) | `html_generator.py` | 중 |
|
||||
| **A-5** | V4 후보 자동 fallback | rank-1 capacity / cardinality / structure mismatch 시 자동 rank-2/3 시도 | V4 후보 list + 각 frame contract 의 cardinality + 추출된 content items | 통과한 frame template_id (모두 fail 시 filtered_capacity) | `fit_verifier.py` | 높음 |
|
||||
| **A-6** | Zone DOM 좌표 export | backend 가 zone 절대 px 좌표를 step08 / 별도 step 에 export (frontend 측정 우회) | layout_css + slide-base 좌표 | `zone_geometries_px: [{position, x, y, w, h}]` | `slide_measurer.py` | 중 |
|
||||
| **B-1** | Zone-section assignment override | 사용자 drag drop 결과를 backend 가 받아 composition planner 의 자동 결정 강제 변경 | `--override-section-assignment ZONE_ID=section_id,section_id` (CLI multi) | units 배치가 사용자 매핑 따름 | `pipeline.py`, `content_editor.py` | 중 |
|
||||
| **B-2** | Edited HTML → MDX 역변환 | frontend 편집 모드의 텍스트 변경이 새 final.html 에 반영 | edited HTML (iframe contentDocument outerHTML) | 새 MDX text 또는 patched mapper input | 글벗 `fmt_slide.py html_to_slide_mdx`, `content_editor.py` | 중 |
|
||||
| **B-3** | Sub-section (### 단위) drag drop backend 처리 | backend 가 sub-section id 를 인식해서 zone 에 sub-section 단위로 매핑 | sub-section id (e.g., "03-1-sub-2") + zone_id | 그 sub-content 단위로 unit 분할 | `section_parser.py` | 낮 |
|
||||
| **B-4** | 다른 layout 의 zone-geometry override 확장 | top-1-bottom-2 / top-2-bottom-1 / left-1-right-2 / left-2-right-1 / grid-2x2 도 사용자 ratio override 적용 (현재 horizontal-2 / vertical-2 만) | `--override-zone-geometry` 인자 + 새 layout_preset 분기 | build_layout_css 의 grid 표현 (areas / cols / rows) | `space_allocator.py` | 낮 |
|
||||
| **D-1** | filtered_section_reasons 노출 UI | 사용자가 어떤 섹션이 왜 빠졌는지 즉시 인지 (Step 8 coverage UI) | `step20_slide_status.json.data.filtered_section_reasons` | frontend header / 패널 UI | N/A (frontend 만) — Phase Q audit 외 | 중 |
|
||||
| **D-2** | Zone resize 시 frame min_height 한계 표시 | pendingLayout 모드 resize 시 frame 한계 인지 + visual hint | frame_contracts.yaml `min_height_px` → step09 trace | frontend resize limit + 한계 도달 시 붉은 outline | `fit_verifier.py` (간접 참고) | 낮 |
|
||||
| **D-3** | 데모 / 업로드 흐름 시각 차별 | 자동 mdx03 vs 사용자 직접 업로드 구분 표시 | uploadedFile state 의 origin (auto-loaded vs user) | 헤더 / 좌측 패널 표시 | N/A (frontend 만) | 낮 |
|
||||
| **D-4** | Step 20 status badge 의미 안내 | PASS / RENDERED_WITH_VISUAL_REGRESSION / PARTIAL_COVERAGE 의 사용자 친화 설명 | runMeta.status | tooltip / popup | N/A (frontend 만) | 낮 |
|
||||
|
||||
**우선순위 분포**:
|
||||
- **높음** (4): A-1, A-2, A-5 + (보고 후 결정)
|
||||
- **중** (6): A-3, A-4, A-6, B-1, B-2, D-1
|
||||
- **낮** (4): B-3, B-4, D-2, D-3, D-4
|
||||
|
||||
**§2 audit 대상이 되는 Phase Q 후보 파일** (위 표에서 추출, 중복 제거):
|
||||
|
||||
1. `mdx_normalizer.py` (A-1)
|
||||
2. `section_parser.py` (A-1, B-3)
|
||||
3. `slide_measurer.py` (A-6)
|
||||
4. `fit_verifier.py` (A-5, D-2 간접)
|
||||
5. `space_allocator.py` (B-4)
|
||||
6. `content_editor.py` (B-1, B-2)
|
||||
7. `content_verifier.py` (검증 — B-2 후속)
|
||||
8. `renderer.py` (A-3, A-4)
|
||||
9. `html_generator.py` (A-3, A-4)
|
||||
10. `block_reference.py` / `block_selector.py` (A-2)
|
||||
11. (마지막) `pipeline.py` / `pipeline_context.py` (orchestration 큼 — 처음부터 보면 피곤. 1~10 보고 마지막에)
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase Q 모듈별 Audit
|
||||
|
||||
> 후속 turn 에서 1 모듈씩 채움. 형식 :
|
||||
>
|
||||
> ```
|
||||
> ### 2.X {파일명}
|
||||
> - **역할** : ...
|
||||
> - **관련 §7-B 항목** : ...
|
||||
> - **현재 Phase Z 와 겹치는 영역** : ...
|
||||
> - **재사용 가능성** : ...
|
||||
> - **결정** : Keep / Migrate / Reference / Delete
|
||||
> - **후속 작업** : ...
|
||||
> ```
|
||||
|
||||
### 2.1 `mdx_normalizer.py`
|
||||
|
||||
#### 역할
|
||||
|
||||
Stage 0 — raw MDX 를 4 Layer 파서로 정규화하여 `{clean_text, title, images, popups, tables, sections}` 반환. `normalize_mdx_content(raw_mdx) -> dict` 단일 entry. 동반 `validate_stage0(result, raw_mdx) -> list[errors]` 검증 함수.
|
||||
|
||||
| Layer | 역할 | 핵심 처리 |
|
||||
|---|---|---|
|
||||
| L1 | frontmatter 분리 | `python-frontmatter` 로 YAML metadata + title 추출 |
|
||||
| L2 | 코드블록 보호 + MDX 전용 패턴 처리 | (1) backtick 10→3 순서로 fenced code 보호 + inline code 보호 (placeholder 치환) → 후처리 후 복원. (2) `<details><summary>` → popups 추출 + 마커 (`[팝업: 제목]`). (3) `:::note[...]` directive → `[핵심요약: ...]` 또는 `## 승격`. (4) **markdown list (`* / - `) → HTML `<ul><li>` 변환** (`_convert_md_list_to_html`, 들여쓰기 2~4 칸 감지로 중첩 처리). (5) **markdown table (`\| col \|`) → HTML `<table>` 변환** (`_convert_md_table_to_html` + `_render_md_table`, 셀 내 `<br/>` 보존). (6) `import` / `export` 문 제거, `<br/>` 제거, JSX `<div style={{...}}>` 태그 제거, 커스텀 컴포넌트 `<Component />` 제거. (7) 헤딩 번호 정규화 (`## N.` → `## `, `### N.N` → `### `). (8) 도입부 `* **제목**` → `## 승격`. (9) 이탤릭 출처 → `출처: ...`. |
|
||||
| L3 | AST 파싱 (markdown-it-py) | `js-default` preset (table 기본 포함). (1) `image` 토큰 → `images: [{alt, path}]`. (2) `table_open` 토큰 → `tables: [{headers, rows}]`. (3) `heading_open h2/h3` → `sections: [{level, title, content}]` (level 2 + level 3 평행 list, bullet_depth 추적해서 content 에 `D1: / D2: / D3:` prefix). |
|
||||
| L4 | 텍스트 정리 | `` → `[이미지: alt]` 마커. 남은 HTML 태그 제거. 연속 빈 줄 정리. |
|
||||
|
||||
검증 (`validate_stage0`):
|
||||
- `clean_text` 비어있음 = FATAL
|
||||
- 원본 대비 텍스트 보존율 < 30% = FATAL
|
||||
- 이미지 수 / popup 수 대조 = ADJUSTABLE
|
||||
|
||||
#### 관련 §7-B 항목
|
||||
|
||||
| §7-B | 직간접 | 영향 |
|
||||
|---|---|---|
|
||||
| **A-1** Stage 0 normalize 통합 | **직접** | 본 모듈 자체가 A-1 의 *진짜 fix*. frontend 의 `## __ROOT__` prefix 임시 우회 제거 가능 |
|
||||
| A-3 Frame preview 일관성 | 간접 | popups / images 가 normalized 에 있으니 backend visual_check 가 활용 가능 (현재 Phase Z 는 popup 처리 없음) |
|
||||
| B-3 Sub-section drag drop | 간접 | L3 가 level 2 + level 3 sections 둘 다 평행 list 로 추출 — Phase Z 의 sub_sections 와 schema 다르지만 변환 어댑터로 매핑 가능 |
|
||||
| D-1 filtered_section_reasons UI | 간접 | `validate_stage0` 의 검증 패턴 (FATAL / ADJUSTABLE) 이 Phase Z 의 status board 와 일관 정리 가능 |
|
||||
|
||||
#### 현재 Phase Z 와 겹치는 영역
|
||||
|
||||
| Phase Q | Phase Z | 비교 |
|
||||
|---|---|---|
|
||||
| `mdx_normalizer.normalize_mdx_content` | `phase_z2_pipeline.parse_mdx` (line 157) | Phase Z = 단순 `^##\s+\d+\.\s+` 정규식으로 ## 단위 sections 분리 + raw_content 통째 보존. *HTML / list / table 변환 / popup 추출 / image 추출 모두 없음*. mdx_normalizer 가 압도적으로 풍부 |
|
||||
| `mdx_normalizer.normalize_mdx_content` | `phase_z2_content_extractor.extract_content_objects` | Phase Z = section.raw_content → 1~2 ContentObject (text_block 또는 transform_table). v0 minimal 명시. mdx_normalizer 가 이미 list/table HTML 분해 + AST tables 추출 결과를 가짐 → Phase Z 의 extract_content_objects 가 그것을 받으면 v0 한계 (1 ContentObject 로 뭉침) 자연 해소 |
|
||||
| `validate_stage0` | (Phase Z 미존재) | Phase Z 의 step02 / step03 에 검증 단계 없음. 문제 발견이 Step 14 visual_check 까지 가서야 잡힘. mdx_normalizer 의 검증 패턴이 step02 검증으로 통합 가능 |
|
||||
|
||||
#### 재사용 가능성
|
||||
|
||||
**매우 높음**. `normalize_mdx_content()` 는 self-contained (외부 의존 = `frontmatter`, `markdown-it-py` 만 — 둘 다 표준 lib). 호출 site 만 바꾸면 됨. 단 schema 변환 어댑터 필요:
|
||||
- mdx_normalizer 의 `sections = [{level: 2|3, title, content}]` (평행 list)
|
||||
- Phase Z 의 `MdxSection = (section_id, section_num, title, raw_content)` (level 2 만, sub 없음)
|
||||
- 어댑터: level 2 = root MdxSection / level 3 = 그 root 의 sub_sections (parent 가 가장 가까운 level 2). frontend 의 `parseMdxText` 와 동일한 hierarchy 로직.
|
||||
|
||||
추가 lift (Phase Z 에 새로 들어오는 정보):
|
||||
- `popups: [{title, content}]` — 현재 Phase Z 는 popup 처리 없음. Step 17 details_popup_escalation 의 진짜 input
|
||||
- `images: [{alt, path}]` — Phase Z 의 missing image count 검증 강화
|
||||
- `tables: [{headers, rows}]` — Step 3 의 transform_table 추출이 정확해짐
|
||||
- L4 `clean_text` — Step 14 visual_check 의 텍스트 보존율 검증 input
|
||||
|
||||
#### 결정
|
||||
|
||||
**Migrate (적극 통합)**.
|
||||
|
||||
이유:
|
||||
1. Phase Z 의 `parse_mdx` 가 *너무 단순* — A-1 문제의 직접 원인
|
||||
2. mdx_normalizer 가 *self-contained + 검증 통과* (Phase Q 에서 정상 동작 흔적). 새로 만들 이유 0
|
||||
3. schema 어댑터만 작성하면 frontend 의 `## __ROOT__` 임시 우회 즉시 제거
|
||||
4. popup / image / table 추출은 Phase Z 의 *현재 약한 영역* (Step 3/4) 의 진짜 input. 통합 시 Step 3/4 진행률 자연 상승
|
||||
|
||||
#### 후속 작업 (B 가 §3 Salvage Plan 으로 넘어감)
|
||||
|
||||
B1. **import 통합** — `phase_z2_pipeline.py` 가 `from mdx_normalizer import normalize_mdx_content, validate_stage0` 추가
|
||||
B2. **schema 어댑터 작성** — `_adapt_normalized_to_mdx_sections(normalized) -> list[MdxSection]`. level 2 = root, level 3 = sub_sections (현재 frontend `parseMdxText` 와 동일 로직). MdxSection 에 `sub_sections: list[MdxSection]` 필드 추가
|
||||
B3. **`parse_mdx` 호출 site 변경** — 단순 정규식 path 폐기. `normalized = normalize_mdx_content(text)` 호출 후 어댑터로 sections 변환
|
||||
B4. **step02 artifact 확장** — `popups`, `images`, `tables`, `clean_text` trace 추가. `validate_stage0` 결과를 step02 의 errors 로 기록
|
||||
B5. **Frontend `## __ROOT__` 우회 제거** — backend section.raw_content 가 이미 정리된 상태로 들어오니 frontend `parseMdxText` 의 prefix hack 제거. `loadRun` 의 `subParsed` 도 backend 의 sub_sections 를 직접 받음
|
||||
B6. **Step 3 `extract_content_objects` 보강** — normalized 의 `tables` / `images` 도 ContentObject 로 변환 (transform_table / image_block). list 항목은 markdown_it AST 의 bullet_list_open/close + depth 정보 활용해서 *N items 로 분할*. 이로 03-1 같은 case (3 bullet → 3 items) 가 자동 통과
|
||||
B7. **A-1 임시 우회 제거 → memory 의 `project_phase_z_normalize_gap.md` 업데이트** (해소 표시)
|
||||
B8. **검증 — MDX 03 fresh run regression check + samples/uploads 의 HTML-heavy 03 으로 fresh run** (이전에 PARTIAL_COVERAGE 였던 case 가 PASS 로 바뀌는지)
|
||||
|
||||
작업 분량: B1+B2+B3 = 한 axis (작음). B4+B5 = 한 axis (작음). B6 = 별도 axis (Step 3 본격 보강 — 큼). B7+B8 = 검증 axis.
|
||||
|
||||
### 2.2 `section_parser.py`
|
||||
|
||||
#### 역할
|
||||
|
||||
Phase Y — `mdx_normalizer.normalize_mdx_content` 출력을 받아 *대목차 (## level 2) 단위 + sub_titles 그룹 + group_schema 분류 + sub_types 점수 + recipe 매핑* 까지 진행하는 후속 layer. mdx_normalizer 와 **chained 구조** (mdx_normalizer 가 producer, section_parser 가 consumer + classifier).
|
||||
|
||||
| 함수 | 역할 |
|
||||
|---|---|
|
||||
| `extract_major_sections(normalized_sections)` | level=2 (빈 content) = 대목차 헤더, level=2 (content 있음) = 소목차, level=3 = 소목차로 묶어서 `[{title, content, sub_titles}]` 반환. 즉 *Phase Z 의 sub_sections 와 동일한 hierarchy 정보를 이미 만듦* |
|
||||
| `detect_component_popups(raw_content, base_path)` | Y-14 — Astro 컴포넌트 (`import X from 'components/...'` + `<X />`) 자동 감지 → popup 등록 |
|
||||
| `_classify_sub_types(sub_titles, content, ...)` | B-1 — 각 sub_title 의 *콘텐츠 유형* 점수 산정 : `parallel_card_candidate / text_list_candidate / visual_detail_candidate / table_heavy_candidate`. D1/D2 패턴 밀도 + content 길이 + table 존재 + popup 매칭 등으로 점수 |
|
||||
| `classify_group_relations(major_sections)` | Y-13b — 대목차의 sub_titles 간 관계로 `group_schema` 부여 : `parallel_cluster` (3 sub) / `compare_paired` / `compare_asymmetric_paired` / `sequence_list` / `sequence_plus_visual` / `single_block` (1 sub) / `card_cluster_N` (4+) + `_plus_visual` suffix |
|
||||
| `SCHEMA_RECIPE_MAP` / `get_recipe_for_schema` | schema → recipe 매핑. recipe = `{recipe (layout 종류), block_kind, blocks (후보 list), [left_kind/right_kind/ratio]}` |
|
||||
| `KIND_SUBTYPE_COMPAT` / `check_kind_compatibility` | recipe_kind 가 실제 sub_type 과 호환되는지 검증 |
|
||||
| `map_topics_to_sections(topics, sections)` | Kei 꼭지 → 대목차 매핑. score 기반 (title 매칭 / sub_title 매칭 / source_hint 매칭) |
|
||||
| `extract_conclusion_text(raw_content)` | `:::note[핵심 요약]` 추출 |
|
||||
|
||||
#### 관련 §7-B 항목
|
||||
|
||||
| §7-B | 직간접 | 영향 |
|
||||
|---|---|---|
|
||||
| **B-3** Sub-section drag drop backend 처리 | **직접** | `extract_major_sections` 가 이미 sub_titles 그룹화 결과를 만듦. backend 가 sub-section id 를 인식하려면 이 layer 가 첫 후보. Phase Z `MdxSection` 에 sub_titles + sub_sections 로 통합 |
|
||||
| **A-1** Stage 0 normalize 통합 | **직접** (mdx_normalizer 와 chained) | mdx_normalizer 의 출력을 *consume 하는 layer* — A-1 통합 시 둘이 같이 와야 함. 따로 통합하면 schema 가 또 꼬임 (사용자 지적 그대로) |
|
||||
| (별 axis 잠재) **Phase Q schema/recipe path** | 잠재 추가 layer | Phase Z 는 V4 매칭만으로 frame 결정. section_parser 는 *content 구조 기반* schema → recipe → blocks 결정 path. 두 path 가 orthogonal. 결합 시 robust 할 수 있음 (V4 점수 + schema 호환 점수) |
|
||||
| (별 axis 잠재) **A-2 Catalog 확장** | 간접 | `SCHEMA_RECIPE_MAP` 의 blocks 후보 list (`prerequisites-3col / card-icon-desc / compare-detail-gradient / process-product-2col / ...`) 가 Phase Q 의 frame 카탈로그 식별자 — Phase Z catalog 확장 시 reference |
|
||||
| (별 axis 잠재) Kei 꼭지 통합 | 잠재 | `map_topics_to_sections` — Kei 통합 시 사용 가능 |
|
||||
|
||||
#### 현재 Phase Z 와 겹치는 영역
|
||||
|
||||
| Phase Q | Phase Z | 비교 |
|
||||
|---|---|---|
|
||||
| `extract_major_sections` | (없음 — Phase Z 의 `parse_mdx` 가 단순 ## 단위 분리) | Phase Z 가 sub_titles 그룹화를 *통째로 빠뜨림*. frontend 의 `parseMdxText` 가 임시로 채운 부분이 사실 여기 |
|
||||
| `classify_group_relations` (Y-13b) + sub_types | `align_sections_to_v4_granularity` (Phase Z) | 둘 다 section 의 *구조 분류* 시도. Phase Z = V4 evidence 의 granularity hint 만 사용. Phase Q = content 분석 기반 schema 자체 결정. **orthogonal path** |
|
||||
| `SCHEMA_RECIPE_MAP` | `LAYOUT_PRESETS` (Phase Z `phase_z2_composition`) | Phase Z = 8 preset (single / horizontal-2 / vertical-2 / ...) 의 *zone 위상*만. Phase Q = schema → recipe → blocks 까지 결정. Phase Q 가 한 단계 더 깊음 |
|
||||
| `_classify_sub_types` (B-1) | (없음 — Phase Z 의 step03 `extract_content_objects` 가 v0 minimal) | Phase Z step03 이 1~2 ContentObject 로 뭉치는 것의 *진짜 fix* 가 여기. sub_type 점수가 그대로 ContentObject role 로 매핑 가능 |
|
||||
| `detect_component_popups` (Y-14) | mdx_normalizer 의 popups | 중복 — mdx_normalizer 가 `<details>` 처리. Y-14 는 Astro `<X />` 처리 (별도 path). 둘이 union 으로 동작 |
|
||||
| `map_topics_to_sections` | (없음 — Phase Z 는 V4 매칭) | Kei 통합 시 사용 |
|
||||
|
||||
#### 재사용 가능성
|
||||
|
||||
**chained 통합 필수 — `mdx_normalizer + section_parser` 둘이 한 axis**.
|
||||
|
||||
분류:
|
||||
| 함수 | 결정 |
|
||||
|---|---|
|
||||
| `extract_major_sections` | **Migrate** — Stage 0 통합 시 mdx_normalizer 와 같이 통합 |
|
||||
| `classify_group_relations` (Y-13b) | **Reference** — V4 path 와 orthogonal. 별 axis 에서 V4 + schema 점수 결합 시 활용 |
|
||||
| `_classify_sub_types` (B-1) | **Reference (강력 후보)** — Phase Z step03 보강 시 그대로 활용 가능 |
|
||||
| `SCHEMA_RECIPE_MAP` / `KIND_SUBTYPE_COMPAT` | **Reference** — Phase Z catalog 확장 (A-2) 의 reference |
|
||||
| `detect_component_popups` (Y-14) | **Reference** — mdx_normalizer 의 popups 과 union. Astro 컴포넌트 처리 시 |
|
||||
| `map_topics_to_sections` | **Reference** — Kei 통합 별 axis |
|
||||
| `extract_conclusion_text` | **Migrate** — 작은 utility. Phase Z 의 `parse_mdx` 가 footer 추출 흐름과 겹침 (Phase Z line 172). 정리 |
|
||||
|
||||
#### 결정
|
||||
|
||||
**Mixed (Migrate + Reference)**.
|
||||
|
||||
- **Migrate**: `extract_major_sections`, `extract_conclusion_text`
|
||||
- **Reference**: `classify_group_relations`, `_classify_sub_types`, `SCHEMA_RECIPE_MAP`, `KIND_SUBTYPE_COMPAT`, `detect_component_popups`, `map_topics_to_sections`
|
||||
|
||||
**핵심 통찰** (사용자 지적 그대로): mdx_normalizer 와 section_parser 를 *따로 이식하면 schema 가 또 꼬인다*. 둘 다 같이 보고 Stage 0 통합 설계 단계 후 B1~B8 + 본 audit 결과 합쳐 통합.
|
||||
|
||||
#### 후속 작업
|
||||
|
||||
C1. **mdx_normalizer + section_parser chained 흐름** — Phase Z `parse_mdx` 를 다음과 같이 재구성:
|
||||
```python
|
||||
def parse_mdx(mdx_path):
|
||||
text = mdx_path.read_text(encoding="utf-8")
|
||||
normalized = normalize_mdx_content(text) # mdx_normalizer
|
||||
major_sections = extract_major_sections( # section_parser
|
||||
normalized["sections"]
|
||||
)
|
||||
footer = extract_conclusion_text(text) # section_parser
|
||||
return _adapt_to_mdx_sections(normalized, major_sections, footer)
|
||||
```
|
||||
|
||||
C2. **`MdxSection` schema 확장**:
|
||||
```python
|
||||
@dataclass
|
||||
class MdxSection:
|
||||
section_id: str
|
||||
section_num: int
|
||||
title: str
|
||||
raw_content: str
|
||||
sub_sections: list[MdxSection] = field(default_factory=list) # NEW — level 3
|
||||
sub_titles: list[str] = field(default_factory=list) # NEW — section_parser
|
||||
# 아래는 Reference 로 두지만 schema 자리는 채움 (별 axis 활성 시 채워짐)
|
||||
group_schema: Optional[str] = None
|
||||
sub_types: list[dict] = field(default_factory=list)
|
||||
```
|
||||
|
||||
C3. **step02 artifact 확장** — `popups`, `images`, `tables`, `clean_text`, `major_sections`, `group_schema`, `sub_types` trace.
|
||||
|
||||
C4. **Frontend `## __ROOT__` 우회 제거** + **`loadRun` 의 sub_sections 매핑** — backend 가 이미 sub_sections 를 정리한 상태로 들어오니 frontend 의 `parseMdxText` re-parse 도 불필요해짐. 직접 sub_sections / sub_titles 사용.
|
||||
|
||||
C5. **B-3 sub-section drag drop backend 처리** — `extract_major_sections` 의 sub_titles 가 이미 hierarchy 정보를 가지므로, frontend drag drop 이 sub-section id (e.g., `03-1-sub-2`) 를 보내면 backend 가 그 sub-content 를 unit 단위로 promote 가능. 단 V4 매칭이 sub-section 단위 안 함 → V4 도 sub-section 단위 평가 필요 (별 axis, 큼).
|
||||
|
||||
C6. **schema/recipe path Reference 활용** (별 axis) — Phase Z V4 path 와 Phase Q schema/recipe path 의 결과 비교 audit. V4 가 약한 case (catalog 미등록 / capacity mismatch) 에서 schema 점수가 backup 으로 의미 있는지.
|
||||
|
||||
C7. **검증** — MDX 03 fresh run regression check + samples/uploads HTML-heavy 03 으로 fresh run + sub_sections 가 step02 artifact 에 정상 등장하는지.
|
||||
|
||||
작업 분량: C1+C2+C3 = 한 axis (Stage 0 chained 통합, 큼). C4 = 한 axis (frontend cleanup). C5 = 별 axis (V4 sub-section 단위 평가, 큼). C6 = 별 axis (schema/recipe Reference 활성화).
|
||||
|
||||
### 2.3 `slide_measurer.py` — *후속 turn*
|
||||
|
||||
### 2.4 `fit_verifier.py` — *후속 turn*
|
||||
|
||||
### 2.5 `space_allocator.py` — *후속 turn*
|
||||
|
||||
### 2.6 `content_editor.py` — *후속 turn*
|
||||
|
||||
### 2.7 `content_verifier.py` — *후속 turn*
|
||||
|
||||
### 2.8 `renderer.py` — *후속 turn*
|
||||
|
||||
### 2.9 `html_generator.py` — *후속 turn*
|
||||
|
||||
### 2.10 `block_reference.py` / `block_selector.py` — *후속 turn*
|
||||
|
||||
### 2.11 (마지막) `pipeline.py` / `pipeline_context.py` — *후속 turn*
|
||||
|
||||
---
|
||||
|
||||
## 3. Salvage Plan
|
||||
|
||||
> 모든 Salvage Plan 은 **§0-A 원칙** 을 따른다. 특히 **Migrate 항목은 adapter + dual-write 검증 (원칙 3 + 9) 을 통과한 뒤** 기존 path 를 대체한다. 각 Migrate 항목별 *dual-write 통과 기준* (어떤 artifact 필드가 일치해야 하는지) 은 항목별로 §3 안에 명시.
|
||||
>
|
||||
> §2 결과를 합산해 §1 의 lens 항목별 *어떤 Phase Q 자산을 어떤 방식으로 가져올지* 결정. *후속 turn*.
|
||||
|
||||
| Phase Z 항목 | 가져올 Phase Q 자산 | 방식 (Migrate / Reference / 새로 만들기) | 후속 axis |
|
||||
|---|---|---|---|
|
||||
| A-1 Stage 0 normalize | (TBD) | (TBD) | (TBD) |
|
||||
| A-2 Catalog 확장 | (TBD) | (TBD) | (TBD) |
|
||||
| A-3 Frame preview 일관성 | (TBD) | (TBD) | (TBD) |
|
||||
| A-4 slide-base iframe mode | (TBD) | (TBD) | (TBD) |
|
||||
| A-5 V4 fallback | (TBD) | (TBD) | (TBD) |
|
||||
| A-6 Zone 좌표 export | (TBD) | (TBD) | (TBD) |
|
||||
| B-1 Zone-section override | (TBD) | (TBD) | (TBD) |
|
||||
| B-2 Edited HTML → MDX | (TBD) | (TBD) | (TBD) |
|
||||
| B-3 Sub-section drag drop | (TBD) | (TBD) | (TBD) |
|
||||
| B-4 다른 layout zone-geometry | (TBD) | (TBD) | (TBD) |
|
||||
| D-1 filtered_section_reasons UI | (TBD) | (TBD) | (TBD) |
|
||||
| D-2 Frame min_height 표시 | (TBD) | (TBD) | (TBD) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 우선순위 재정렬
|
||||
|
||||
> §1 / §3 결과를 [`PHASE-Z-ROADMAP.md`](PHASE-Z-ROADMAP.md) §5 / §7-B 에 반영. *후속 turn*.
|
||||
|
||||
audit 결과 → 어느 항목이:
|
||||
- **Migrate 로 빠르게 끝남** (작은 작업) → 우선순위 ↑
|
||||
- **새로 만들기 + Reference 만 가능** (큰 작업) → 우선순위 ↓ 또는 분해
|
||||
- **Delete** (Phase Q 에 없음 또는 무관) → §1 표에서 제외
|
||||
|
||||
후속 작업 = ROADMAP §5 / §7-B 갱신.
|
||||
|
||||
---
|
||||
|
||||
## 5. 진행 로그
|
||||
|
||||
| 날짜 | axis | 상태 |
|
||||
|---|---|---|
|
||||
| 2026-05-08 | §0 / §1 작성 | 완료 |
|
||||
| 2026-05-08 | §2.1 mdx_normalizer.py audit | 완료 — **Migrate**. B1~B8 후속 작업 정의 |
|
||||
| 2026-05-08 | §2.2 section_parser.py audit | 완료 — **Mixed (Migrate + Reference)**. C1~C7 후속 작업 정의. mdx_normalizer 와 chained 통합 결정 |
|
||||
| 2026-05-08 | §0-A Salvage 원칙 lock (10 원칙) + §3 머리글 + ROADMAP §5 link | 완료 — 정책 lock |
|
||||
| (next) | §2.3 slide_measurer.py audit | (대기) |
|
||||
| ... | §2.10 까지 1 모듈씩 | (대기) |
|
||||
| (last) | §2.11 pipeline.py / pipeline_context.py | (대기) |
|
||||
| (then) | §3 Salvage Plan | (대기) |
|
||||
| (then) | §4 우선순위 재정렬 → ROADMAP 갱신 | (대기) |
|
||||
@@ -1,7 +1,7 @@
|
||||
# Phase Z Roadmap
|
||||
|
||||
**문서 성격**: 앞으로의 진행 순서 + 큰 axis + 우선순위. *자주 갱신*.
|
||||
**작성일**: 2026-05-08
|
||||
**작성일**: 2026-05-08 (frontend 통합 session 반영)
|
||||
**문서 역할 분담**: 본 문서는 *진행 계획*. 정확한 단계별 상태는 [`PHASE-Z-PIPELINE-STATUS-BOARD.md`](PHASE-Z-PIPELINE-STATUS-BOARD.md), 결정 변경 이력은 [`PHASE-Z-CHANGE-LOG.md`](PHASE-Z-CHANGE-LOG.md), 22 단계 도면은 [`PHASE-Z-PIPELINE-OVERVIEW.md`](PHASE-Z-PIPELINE-OVERVIEW.md).
|
||||
|
||||
> **현재 당장 할 일은 개발 진행이 아니라, 보고와 피드백을 위한 `PROCESS_OVERVIEW.html` 프로토타입을 먼저 만드는 것이다.**
|
||||
@@ -41,6 +41,34 @@
|
||||
- README 통합 갱신 — 한글 / 22-step Mermaid / 로드맵 Mermaid
|
||||
- push 완료 — origin (GitHub) + slide2 (Gitea)
|
||||
|
||||
### 2-B. Frontend 통합 session 닫힌 작업 (보고용 1차 초안)
|
||||
|
||||
frontend (`design_agent_front/design-agent`) 와 backend phase_z2 의 연결 axis :
|
||||
|
||||
- **Vite plugin endpoint 들** — 외부 backend 없이 단일 dev server 안에서 spawn :
|
||||
- `POST /api/run` (overrides body 받아 CLI 인자로 forward)
|
||||
- `GET /data/runs/{run_id}/{path}` (run artifact serving)
|
||||
- `GET /frame-preview/{frame_number}` (`data/figma_previews/{NN}.png` 서빙)
|
||||
- `GET /api/sample-mdx` (데모용 fixed mdx03 자동 로드)
|
||||
- **Backend Step 7-A (override CLI 인자)** :
|
||||
- `--override-layout PRESET` (8 preset 중 하나 강제)
|
||||
- `--override-frame UNIT_ID=TEMPLATE_ID` (multiple)
|
||||
- `--override-zone-geometry ZONE_ID=X,Y,W,H` (multiple, horizontal-2 / vertical-2 만 적용)
|
||||
- catalog 미등록 frame 보호 — `frame_overrides_skipped` trace
|
||||
- **Backend trace 확장** — `step09.units[].v4_all_judgments` (reject 포함 32 entry, `catalog_registered` flag, `frame_overrides_applied/skipped`, `layout_override_applied`, `auto_layout_preset`)
|
||||
- **Frontend SlideCanvas** :
|
||||
- iframe 안 `.zone[data-zone-position]` + `.slide-body` boundingClientRect 측정 → zone overlay 좌표 정확
|
||||
- iframe contentDocument CSS injection (html/body margin/padding/min-height 강제 — standalone 표시용 잘림 방지)
|
||||
- 8 preset layout 별 zone overlay rendering
|
||||
- frame top-6 표시 + V4 label badge + figma_previews thumbnail + catalog 미등록 회색 차별
|
||||
- selected frame preview overlay (반투명 미리보기)
|
||||
- section drag-drop drop target (LeftMdxPanel 의 native drag → SlideCanvas zone drop)
|
||||
- **HTML 편집 모드** (글벗 editor.js 패턴 차용 : designMode + contentEditable + outline CSS) — 텍스트 변경 시 `hasPendingChanges` 트리거
|
||||
- **pendingLayout 모드** — 다른 layout "적용하기" → slide-body 영역만 빈 layout 으로 전환 + zone resize 8 방향 handle (edge × 4 + corner × 4) + section drag drop 자동 carry over
|
||||
- "선택대로 재생성하기" 버튼 — `userSelection.overrides` → `PipelineOverrides` 변환 후 `/api/run` forwarding
|
||||
- **Frontend MDX 트리 분해 (frontend re-parse)** — `parseMdxText` 가 `##` 중목차 + `###` 소목차 분해. `loadRun` 에서 backend section 의 `raw_content` 앞에 `## __ROOT__` prefix 임시 추가해서 sub_sections 추출 (Stage 0 normalize 가 backend 에 통합 안 된 임시 우회)
|
||||
- **데모 fixed input** — 페이지 로드 시 자동 `samples/mdx/03 ...` fetch → 사용자 업로드 단계 skip
|
||||
|
||||
각 axis 상세 의사결정 / 폐기 / lock 은 [`PHASE-Z-CHANGE-LOG.md`](PHASE-Z-CHANGE-LOG.md) 참조.
|
||||
|
||||
---
|
||||
@@ -99,14 +127,37 @@
|
||||
|
||||
---
|
||||
|
||||
## 5. 우선순위
|
||||
## 5. 우선순위 (2026-05-08 갱신 — frontend 통합 session 후)
|
||||
|
||||
```
|
||||
1. 보고용 HTML 프로토타입 작성 ← 지금 당장 할 일
|
||||
2. 보고 / 피드백 수집
|
||||
3. Phase Z 파이프라인 작업 복귀 (22 단계 정리 계속)
|
||||
0. 보고용 link 1차 (현재 — 닫는 중)
|
||||
|
||||
1. §7-B 항목별 목적 / input / output 정리 + Phase Q audit & salvage
|
||||
- 먼저 lens 잡고 (각 보완 항목의 목적·input·output)
|
||||
- 그 lens 로 Phase Q 모듈 10 개 검토 (Keep / Migrate / Reference / Delete)
|
||||
- 통합 정책은 [`PHASE-Q-AUDIT.md`](PHASE-Q-AUDIT.md) §0-A 따름 (10 원칙 lock)
|
||||
- 산출물: docs/architecture/PHASE-Q-AUDIT.md
|
||||
|
||||
2. 22 단계 backend 본체 보완
|
||||
- audit 결과 반영 (Migrate 항목 우선 통합)
|
||||
- frontend session 에서 발견된 보완 지점 (§7-B A 그룹) 반영
|
||||
- Stage 0 normalize / V4 fallback / zone 좌표 export / slide-base iframe / catalog 확장 등
|
||||
|
||||
3. Frontend 추가 보완 (AI 투입 / HTML 수정 backend 적용 / DB 연결)
|
||||
- §7-B B 그룹 (zone-section assignment / edited HTML → MDX 등)
|
||||
- §7-B D 그룹 (filtered reason UI / min_height 한계 / status badge 안내)
|
||||
|
||||
4. 전체 일반화 + 시스템 audit
|
||||
4a. MDX 01 / 02 / 04 fresh run (regression / status / coverage 검증)
|
||||
4b. 코드 audit (dead code / stale comment / type drift)
|
||||
4c. frame catalog audit (등록 수 / contract 일관성 / preview.png 일관성)
|
||||
4d. DB / data 활용 audit (assets / runs / static 폴더 정리 수준)
|
||||
|
||||
5. Clean up (4 의 결과를 input 으로)
|
||||
```
|
||||
|
||||
§7-B 의 항목 구현 순서는 §1 의 audit 결과로 재정렬됨 — 본 §5 가 최상위 axis.
|
||||
|
||||
---
|
||||
|
||||
## 6. 보고용 프로토타입 계획
|
||||
@@ -146,6 +197,53 @@
|
||||
|
||||
---
|
||||
|
||||
## 7-B. Frontend 통합으로 드러난 보완 지점 (2026-05-08)
|
||||
|
||||
이번 frontend session 에서 *기능적인 것을 해결하기 위해 frontend 가 임시로 채운 부분* + *backend 본체가 보완해야 의미 있는 axis* 정리.
|
||||
|
||||
> **본 표는 §5 의 1 단계 audit lens** — 각 항목의 *목적 / input / output* 을 먼저 채우면 Phase Q 모듈 검토 시 *명확한 질문* 이 됨 ("이 schema 와 같은 코드가 Phase Q 에 있나? Migrate / Reference / Delete 어느 쪽?"). audit 결과로 본 §7-B 의 구현 우선순위가 재정렬됨.
|
||||
|
||||
### A. Backend 본체 보완 (큰 axis)
|
||||
|
||||
| # | 항목 | 이유 / 영향 | 임시 대체 |
|
||||
|---|---|---|---|
|
||||
| A-1 | **Stage 0 normalize 통합** — `mdx_normalizer.normalize_mdx_content` 를 phase_z2 가 호출 | 표준 input (정리된 bullet MDX) 외 (HTML-heavy raw) 입력 처리 못 함. step03 추출이 1 ContentObject 로 뭉침 → cardinality strict frame 과 mismatch → filter | frontend 가 raw_content 앞에 `## __ROOT__` prefix 붙여 sub_sections 만 추출. content_objects 는 통째로 (별 axis 보류 — `project_phase_z_normalize_gap.md`) |
|
||||
| A-2 | **Catalog 확장** — frame_contracts.yaml + frame_partials 더 많은 frame 등록 (현재 3 개 → 32 개 목표) | 사용자 frame 변경 의미 거의 불가 (V4 32 후보 중 use_as_is/light_edit 만 있어도 catalog 미등록이면 backend 가 skip). MDX03 외 다른 sample 일반화 어려움 | frontend 가 `catalog_registered: false` 카드 시각 차별 + handleGenerate 에서 forward 미시도. 사용자에게 "catalog 미등록 — render path 미연결" toast |
|
||||
| A-3 | **Frame preview png 일관성** — 모든 catalog frame 의 일관된 preview.png 자동 생성 | `figma_to_html_agent/blocks/{frame_id}/preview.png` 가 inconsistent (일부 frame 만 존재). frontend 가 어떤 source 든 통일해야 | 32 개 모두 있는 `data/figma_previews/{NN}.png` (Figma original export) 사용. frame 별 *실제 렌더 결과* 가 아닌 *Figma 디자인* 표시 |
|
||||
| A-4 | **slide-base.html iframe-friendly mode** — body padding / centering / min-height 가 standalone 모드용. iframe embed 시 잘림 | frontend 가 매 iframe onLoad 마다 contentDocument 에 reset CSS 주입 (모든 frame 영향) | frontend CSS injection 으로 우회. 진짜 fix 는 slide-base 가 query string `?embedded=1` 같은 시그널 받아 conditional padding |
|
||||
| A-5 | **V4 후보 자동 fallback** — rank-1 capacity / cardinality / structure mismatch 시 자동 rank-2/3 시도 | 03-1 처럼 rank-1 (`three_parallel_requirements`, strict cardinality 3) 가 추출 0 items 와 mismatch → filtered_capacity. 사용자 입장에서 "왜 빠졌는지" 알기 어려움 | (없음. frontend 는 reject + filtered 결과를 그대로 받아 표시) |
|
||||
| A-6 | **Zone DOM 좌표 export** — backend 가 `.zone[data-zone-position]` / `.slide-body` 의 절대 px 좌표를 step08 또는 별도 step 에 export | frontend 가 매 iframe onLoad 마다 boundingClientRect 측정 — 비용 + iframe 안 DOM 변경 시 stale | (없음. iframe 측정으로 우회) |
|
||||
|
||||
### B. Backend Override path 확장 (Step D-ext 나머지)
|
||||
|
||||
| # | 항목 | 이유 |
|
||||
|---|---|---|
|
||||
| B-1 | **Zone-section assignment override** — `--override-section-assignment ZONE_ID=section_id,section_id` CLI 인자 + composition planner 의 자동 결정을 user override 로 강제 | pendingLayout 모드의 section drag drop 결과가 backend 에 적용되도록. 현재 frontend visual 만 |
|
||||
| B-2 | **Edited HTML → MDX 역변환** — 글벗의 `html_to_slide_mdx` 패턴 차용. frontend 의 `iframe.contentDocument.querySelector('.slide').outerHTML` → backend → MDX → pipeline 재실행 | 편집 모드에서 사용자가 텍스트 수정한 결과가 새 final.html 에 반영. 현재 frontend visual 만 |
|
||||
| B-3 | **Sub-section (### 단위) drag drop 처리** — backend 가 sub-section id 를 인식해서 zone 에 sub-section 단위로 매핑 | LeftMdxPanel 의 트리 (S1.1, S1.2 ...) drag drop 결과가 의미 있게 적용되도록 |
|
||||
| B-4 | **다른 layout 의 zone-geometry override** — 현재 horizontal-2 / vertical-2 만. top-1-bottom-2 / top-2-bottom-1 / left-1-right-2 / left-2-right-1 / grid-2x2 도 지원 | 사용자가 어떤 layout 에서든 자유 zone resize 가능 |
|
||||
|
||||
### C. Frontend 일관성 정리 (drift 정리)
|
||||
|
||||
| # | 항목 |
|
||||
|---|---|
|
||||
| C-1 | `userSelection.selectedZoneId` schema — `zone.id` (e.g., `zone-03-1`, `pending-zone-N`) vs `zone.zone_id` (e.g., `top`, `primary`) 양쪽 매칭 일관화 |
|
||||
| C-2 | `zone_sections` / `zone_geometries` / `zone_frames` 의 key 통일 (zone_id 또는 region.id) |
|
||||
| C-3 | `effectiveSlidePlan` swap 로직이 누락된 component 정리 (FramePanel / LayoutPanel / 기타) |
|
||||
| C-4 | Toaster 제거 후 inline status bar 도입 (선택) — 현재 toast 호출은 silent (비표시) |
|
||||
| C-5 | resize 좌표 schema 통일 (slide-body 내부 0~1 vs 1280×720 절대) — pendingLayout 만 활성으로 단순화한 상태이지만 향후 다양한 mode 지원 시 명확화 |
|
||||
|
||||
### D. UX 보완
|
||||
|
||||
| # | 항목 |
|
||||
|---|---|
|
||||
| D-1 | **filtered_section_reasons 노출 UI** — backend `step20_slide_status.json` 의 filter 사유를 frontend 가 헤더 / 패널에 표시 (Step 8 coverage UI) |
|
||||
| D-2 | **Zone resize 시 frame min_height 한계 표시** — frame contract 의 min_height_px 를 frontend 가 받아서 resize limit + visual hint |
|
||||
| D-3 | **데모 외 사용자 업로드 흐름 일관화** — 자동 mdx03 로드와 사용자 직접 업로드의 시각 차별. "데모 모드" 표시 |
|
||||
| D-4 | **Step 20 status badge 의미 안내** — PASS / RENDERED_WITH_VISUAL_REGRESSION / PARTIAL_COVERAGE 의 사용자 친화 설명 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 문서 역할 분담
|
||||
|
||||
| 문서 | 역할 | 갱신 빈도 |
|
||||
@@ -155,6 +253,7 @@
|
||||
| [`PHASE-Z-PIPELINE-STATUS-BOARD.md`](PHASE-Z-PIPELINE-STATUS-BOARD.md) | 22 단계별 현재 상태의 single source of truth | 자주 |
|
||||
| [`PHASE-Z-CHANGE-LOG.md`](PHASE-Z-CHANGE-LOG.md) | 의사결정 변경 이력 (axis 단위, 시간 순) | 작업 단위 |
|
||||
| **`PHASE-Z-ROADMAP.md`** (본 문서) | **앞으로의 진행 순서 / 큰 axis / 우선순위** | **자주** |
|
||||
| [`PHASE-Q-AUDIT.md`](PHASE-Q-AUDIT.md) | Phase Q 모듈의 Keep / Migrate / Reference / Delete 결정 + Salvage Plan + ROADMAP §7-B 우선순위 재정렬의 input | audit 진행 중 (모듈별로 1 turn) |
|
||||
| `PROCESS_OVERVIEW.html` | 보고 / 피드백용 시각 프로토타입 | 보고 시점 |
|
||||
|
||||
각 문서가 자기 역할만 하면 drift / 중복 / outdated 방지.
|
||||
|
||||
@@ -270,6 +270,33 @@ def lookup_v4_match(v4: dict, section_id: str) -> Optional[V4Match]:
|
||||
)
|
||||
|
||||
|
||||
def lookup_v4_all_judgments(v4: dict, section_id: str) -> list[V4Match]:
|
||||
"""V4 raw 32 entry 그대로 반환 — reject 포함, max_n filter 없음.
|
||||
|
||||
Step 7-A axis 보강 (사용자 lock 2026-05-08) — 사용자 UI 가 모든 frame 의
|
||||
png 를 보여줄 수 있도록 reject 까지 trace. lookup_v4_candidates 는 변경 없음
|
||||
(backward compat — non-reject + max_n 만 반환).
|
||||
|
||||
Returns :
|
||||
list[V4Match] — 0~32 길이. raw judgments_full32 순서 (= V4 score desc) 보존.
|
||||
"""
|
||||
sec = v4.get("mdx_sections", {}).get(section_id)
|
||||
if not sec:
|
||||
return []
|
||||
judgments = sec.get("judgments_full32", [])
|
||||
out: list[V4Match] = []
|
||||
for j in judgments:
|
||||
out.append(V4Match(
|
||||
section_id=section_id,
|
||||
frame_id=str(j["frame_id"]),
|
||||
frame_number=int(j["frame_number"]),
|
||||
template_id=j["template_id"],
|
||||
confidence=float(j["confidence"]),
|
||||
label=j["label"],
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def lookup_v4_candidates(
|
||||
v4: dict, section_id: str, max_n: int = 6
|
||||
) -> list[V4Match]:
|
||||
@@ -412,15 +439,67 @@ def compute_zone_layout(zones_data: list[dict],
|
||||
|
||||
|
||||
def build_layout_css(layout_preset: str, zones_data: list[dict],
|
||||
gap: int = GRID_GAP) -> dict:
|
||||
gap: int = GRID_GAP,
|
||||
override_zone_geometries: Optional[dict[str, dict]] = None) -> dict:
|
||||
"""Composition v0 layout preset → CSS grid 문자열.
|
||||
|
||||
horizontal-2 (= old type-b, 2-zone vertical stack) 만 dynamic heights 유지
|
||||
(MDX 03 회귀 보존 — content_weight 기반). 다른 preset 은 fr default.
|
||||
향후 cardinality_fit / density_score axis 가 score_candidate 에 들어가면
|
||||
cols/rows 도 dynamic 으로 확장 가능.
|
||||
|
||||
Step D-ext (사용자 lock 2026-05-08) — override_zone_geometries (zone_id → {x,y,w,h}
|
||||
slide-body 내부 0~1) 가 들어오면 그 비율로 layout_css 강제. horizontal-2 / vertical-2
|
||||
만 처리. 다른 preset 은 일단 무시 + warning. 비율 합 != 1 이면 normalize.
|
||||
"""
|
||||
preset = LAYOUT_PRESETS[layout_preset]
|
||||
positions = preset["positions"]
|
||||
|
||||
# ── Step D-ext : user override 처리 ──
|
||||
if override_zone_geometries:
|
||||
if layout_preset == "horizontal-2":
|
||||
# heights_px override — zone 의 h 비율로 SLIDE_BODY_HEIGHT 분배.
|
||||
ratios = []
|
||||
for pos in positions:
|
||||
geom = override_zone_geometries.get(pos)
|
||||
ratios.append(float(geom["h"]) if geom else 0.0)
|
||||
total = sum(ratios)
|
||||
if total > 0:
|
||||
heights_px = [int(round(r / total * SLIDE_BODY_HEIGHT)) for r in ratios]
|
||||
rows = " ".join(f"{h}px" for h in heights_px)
|
||||
return {
|
||||
"areas": preset["css_areas"],
|
||||
"cols": preset["css_cols"],
|
||||
"rows": rows,
|
||||
"heights_px": heights_px,
|
||||
"ratios": [round(r / total, 3) for r in ratios],
|
||||
"computation": "user_override_geometry",
|
||||
"dynamic_rows": True,
|
||||
"raw_zone_layout": {"override_applied": True, "source": override_zone_geometries},
|
||||
}
|
||||
elif layout_preset == "vertical-2":
|
||||
# cols override — zone 의 w 비율로 fr 분배.
|
||||
ratios = []
|
||||
for pos in positions:
|
||||
geom = override_zone_geometries.get(pos)
|
||||
ratios.append(float(geom["w"]) if geom else 0.0)
|
||||
total = sum(ratios)
|
||||
if total > 0:
|
||||
cols = " ".join(f"{round(r / total * 100, 2)}fr" for r in ratios)
|
||||
return {
|
||||
"areas": preset["css_areas"],
|
||||
"cols": cols,
|
||||
"rows": preset["css_rows"],
|
||||
"heights_px": [],
|
||||
"ratios": [round(r / total, 3) for r in ratios],
|
||||
"computation": "user_override_geometry",
|
||||
"dynamic_rows": False,
|
||||
"raw_zone_layout": {"override_applied": True, "source": override_zone_geometries},
|
||||
}
|
||||
else:
|
||||
print(
|
||||
f" [override-warning] zone-geometry override 는 layout '{layout_preset}' 미지원 "
|
||||
f"(현재 horizontal-2 / vertical-2 만). default layout_css 사용.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if layout_preset == "horizontal-2":
|
||||
zl = compute_zone_layout(zones_data, gap=gap)
|
||||
@@ -1157,12 +1236,26 @@ def write_debug_json(run_dir: Path, layout_preset: str,
|
||||
|
||||
# ─── Main entry ────────────────────────────────────────────────
|
||||
|
||||
def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
def run_phase_z2_mvp1(
|
||||
mdx_path: Path,
|
||||
run_id: Optional[str] = None,
|
||||
*,
|
||||
override_layout: Optional[str] = None,
|
||||
override_frames: Optional[dict[str, str]] = None,
|
||||
override_zone_geometries: Optional[dict[str, dict]] = None,
|
||||
) -> Path:
|
||||
"""MVP-1.5b entry — single slide + composition planner v0 + 8 preset vocabulary.
|
||||
|
||||
Pipeline :
|
||||
parse_mdx → align_sections_to_v4_granularity → plan_composition →
|
||||
mapper per unit → render slide_base + frame partial → Selenium check
|
||||
|
||||
User overrides (Step 7-A axis, 2026-05-08) :
|
||||
override_layout : 자동 결정된 layout_preset 을 사용자 선택값으로 강제 (8 preset 중 하나).
|
||||
override_frames : {unit_id: template_id} — 자동 결정된 frame template 을 사용자 선택값
|
||||
으로 강제. unit_id = "+".join(source_section_ids) (e.g., "03-1"
|
||||
또는 "03-1+03-2"). 매칭 unit 의 v4_candidates 에 있는 entry 면
|
||||
그 entry 의 score / label 도 함께 갱신. 없으면 template_id 만 변경.
|
||||
"""
|
||||
mdx_path = Path(mdx_path)
|
||||
if run_id is None:
|
||||
@@ -1321,6 +1414,25 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
v4_candidates_lookup_fn=candidates_lookup_fn,
|
||||
)
|
||||
|
||||
# ── Step 7-A axis : layout override ──
|
||||
# 사용자가 LayoutPanel 에서 다른 preset 을 선택했을 때 자동 결정값을 강제 변경.
|
||||
# 길이 mismatch (positions count vs unit count) 는 zone loop 의 fallback (zone_{i})
|
||||
# 으로 처리됨. 알 수 없는 preset 이면 ValueError.
|
||||
auto_layout_preset = layout_preset
|
||||
layout_override_applied = False
|
||||
if override_layout is not None and override_layout != layout_preset:
|
||||
if override_layout not in LAYOUT_PRESETS:
|
||||
raise ValueError(
|
||||
f"--override-layout '{override_layout}' is not a known preset. "
|
||||
f"Available: {sorted(LAYOUT_PRESETS.keys())}"
|
||||
)
|
||||
print(
|
||||
f" [override] layout_preset: {layout_preset} → {override_layout}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
layout_preset = override_layout
|
||||
layout_override_applied = True
|
||||
|
||||
if not units or layout_preset is None:
|
||||
# composition planner 결과 = 0 units. Sections 가 모두 V4 lookup 실패 또는
|
||||
# status filter 통과 못 함. error.json 기록 후 abort.
|
||||
@@ -1404,6 +1516,66 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
debug_zones = []
|
||||
adapter_needed_units: list[dict] = []
|
||||
|
||||
# ── Step 7-A axis : frame override ──
|
||||
# {unit_id: template_id} 형식. unit_id 매칭 시 unit.frame_template_id 강제 변경.
|
||||
# v4_candidates 안에서 같은 template_id 를 가진 entry 를 찾으면 frame_id /
|
||||
# frame_number / confidence / label 까지 그 entry 에서 가져와 갱신 — 그래야 step09
|
||||
# artifact 의 메타가 일관됨.
|
||||
# frame contract 가 catalog 에 등록 안 된 template_id 면 skip + warning —
|
||||
# crash 방지 (V4 score 는 매겨지지만 catalog partial 은 없는 후보 존재).
|
||||
frame_overrides_applied: list[dict] = []
|
||||
frame_overrides_skipped: list[dict] = []
|
||||
if override_frames:
|
||||
for unit in units:
|
||||
unit_id = "+".join(unit.source_section_ids)
|
||||
if unit_id not in override_frames:
|
||||
continue
|
||||
new_tid = override_frames[unit_id]
|
||||
old_tid = unit.frame_template_id
|
||||
if new_tid == old_tid:
|
||||
continue
|
||||
# catalog contract 존재 확인 — 없으면 override 거부.
|
||||
new_contract = get_contract(new_tid)
|
||||
if new_contract is None:
|
||||
frame_overrides_skipped.append({
|
||||
"unit_id": unit_id,
|
||||
"from": old_tid,
|
||||
"to": new_tid,
|
||||
"reason": "no_frame_contract_in_catalog",
|
||||
})
|
||||
print(
|
||||
f" [override-skip] unit {unit_id}: '{new_tid}' has no entry in "
|
||||
f"frame_contracts catalog — keeping {old_tid}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
match = None
|
||||
for cand in (unit.v4_candidates or []):
|
||||
if getattr(cand, "template_id", None) == new_tid:
|
||||
match = cand
|
||||
break
|
||||
if match is not None:
|
||||
unit.frame_template_id = match.template_id
|
||||
unit.frame_id = match.frame_id
|
||||
unit.frame_number = match.frame_number
|
||||
unit.confidence = match.confidence
|
||||
unit.label = match.label
|
||||
meta_source = "v4_candidates"
|
||||
else:
|
||||
unit.frame_template_id = new_tid
|
||||
meta_source = "raw_template_id_only"
|
||||
frame_overrides_applied.append({
|
||||
"unit_id": unit_id,
|
||||
"from": old_tid,
|
||||
"to": new_tid,
|
||||
"meta_source": meta_source,
|
||||
})
|
||||
print(
|
||||
f" [override] unit {unit_id} frame: {old_tid} → {new_tid} "
|
||||
f"({meta_source})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
for i, unit in enumerate(units):
|
||||
position = positions[i] if i < len(positions) else f"zone_{i}"
|
||||
synth_section = MdxSection(
|
||||
@@ -1749,8 +1921,11 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
note="map_with_contract 결과 — actual slot_payload 값 그대로 (key 만 X).",
|
||||
)
|
||||
|
||||
# 6. Build layout CSS — horizontal-2 = dynamic heights (regression preserve), 그 외 = fr default
|
||||
layout_css = build_layout_css(layout_preset, zones_data)
|
||||
# 6. Build layout CSS — horizontal-2 = dynamic heights (regression preserve), 그 외 = fr default.
|
||||
# Step D-ext : override_zone_geometries 가 들어오면 layout_css 강제.
|
||||
layout_css = build_layout_css(
|
||||
layout_preset, zones_data, override_zone_geometries=override_zone_geometries
|
||||
)
|
||||
if layout_css["dynamic_rows"]:
|
||||
for dz, h, r in zip(debug_zones, layout_css["heights_px"], layout_css["ratios"]):
|
||||
dz["height_px"] = h
|
||||
@@ -1771,6 +1946,9 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
"zones_count": len(zones_data),
|
||||
"unit_count": len(units),
|
||||
"layout_candidates": layout_candidates_list,
|
||||
# Step 7-A axis : user override trace
|
||||
"layout_override_applied": layout_override_applied,
|
||||
"auto_layout_preset": auto_layout_preset,
|
||||
},
|
||||
step_status="partial",
|
||||
pipeline_path_connected=True,
|
||||
@@ -2059,6 +2237,12 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
unit.v4_candidates[0].template_id if has_v4 else None
|
||||
)
|
||||
|
||||
# Step 7-A axis 보강 — reject 포함 모든 V4 judgments (frontend UI 가
|
||||
# 모든 frame 의 png 를 카드로 보여주기 위함).
|
||||
# unit_id = source_section_ids join. parent_merged 는 첫 section 의
|
||||
# judgments 사용 (parent V4 entry 가 그 section 에 있으므로).
|
||||
v4_all_for_unit = lookup_v4_all_judgments(v4, unit.source_section_ids[0])
|
||||
|
||||
# application_candidates : V4 후보 zip 으로 application_mode 변환
|
||||
app_candidates = []
|
||||
for c in unit.v4_candidates:
|
||||
@@ -2094,6 +2278,23 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
}
|
||||
for c in unit.v4_candidates
|
||||
],
|
||||
# Step 7-A axis 보강 (사용자 lock 2026-05-08) — frontend UI 가 reject
|
||||
# 포함 모든 V4 후보를 시각 차별 (회색) 로 보여줄 수 있도록 trace.
|
||||
# length = 0~32. label 별 count : v4_candidates 는 non-reject only,
|
||||
# v4_all_judgments 는 reject 포함.
|
||||
# catalog_registered = frame_contracts.yaml 에 contract 있는지 여부.
|
||||
# false 면 사용자가 override 시도해도 Step 7-A 가 skip (render path 미연결).
|
||||
"v4_all_judgments": [
|
||||
{
|
||||
"template_id": c.template_id,
|
||||
"frame_id": c.frame_id,
|
||||
"frame_number": c.frame_number,
|
||||
"confidence": c.confidence,
|
||||
"label": c.label,
|
||||
"catalog_registered": get_contract(c.template_id) is not None,
|
||||
}
|
||||
for c in v4_all_for_unit
|
||||
],
|
||||
"application_candidates": app_candidates,
|
||||
})
|
||||
|
||||
@@ -2109,6 +2310,9 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
"candidate_status_summary": {
|
||||
"units_with_no_v4_candidate": units_with_no_v4,
|
||||
},
|
||||
# Step 7-A axis : user override trace
|
||||
"frame_overrides_applied": frame_overrides_applied,
|
||||
"frame_overrides_skipped": frame_overrides_skipped,
|
||||
"v0_lock_note": (
|
||||
"Step 9 v0 passive (사용자 lock 2026-05-08). "
|
||||
"Step 6 default 그대로 사용 — runtime 결과 byte-동일. "
|
||||
@@ -2130,7 +2334,9 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
"변환을 side-by-side 로 기록. v0 invariant 5 가지 (status.md §4) 만족. "
|
||||
"Step 6 의 default 결정 그대로 (current_default_candidate). "
|
||||
"auto decision 은 Step 9 v1 (별 axis). region/display 후보는 Step 8-conn "
|
||||
"의 placeholder signal 종속 (Step 3/4 부재)."
|
||||
"의 placeholder signal 종속 (Step 3/4 부재). "
|
||||
"Step 7-A axis (2026-05-08): frame_overrides_applied 가 사용자 LayoutPanel/"
|
||||
"FramePanel 선택값을 강제 적용한 trace."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2548,9 +2754,78 @@ def run_phase_z2_mvp1(mdx_path: Path, run_id: Optional[str] = None) -> Path:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python phase_z2_pipeline.py <mdx_path> [run_id]", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
mdx = Path(sys.argv[1])
|
||||
rid = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
run_phase_z2_mvp1(mdx, rid)
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python -m src.phase_z2_pipeline",
|
||||
description="Phase Z-2 MVP-1.5b pipeline (MDX → 1 slide HTML).",
|
||||
)
|
||||
parser.add_argument("mdx_path", type=Path, help="MDX 파일 경로")
|
||||
parser.add_argument(
|
||||
"run_id", nargs="?", default=None,
|
||||
help="run_id (출력 디렉토리 이름). 없으면 자동 생성 (basename + timestamp).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--override-layout", dest="override_layout", default=None,
|
||||
metavar="PRESET",
|
||||
help=(
|
||||
"layout_preset 강제 (8 preset 중 하나 — single, horizontal-2, vertical-2, "
|
||||
"top-1-bottom-2, top-2-bottom-1, left-1-right-2, left-2-right-1, grid-2x2). "
|
||||
"없으면 자동 결정 (count-based v0)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--override-frame", dest="override_frames", action="append", default=[],
|
||||
metavar="UNIT_ID=TEMPLATE_ID",
|
||||
help=(
|
||||
"unit_id 의 frame template 강제 변경. UNIT_ID 는 \"+\".join(source_section_ids) "
|
||||
"(e.g., 03-1 또는 03-1+03-2). multiple 가능: --override-frame 03-1=foo "
|
||||
"--override-frame 03-2=bar"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--override-zone-geometry", dest="override_zone_geometries", action="append", default=[],
|
||||
metavar="ZONE_ID=X,Y,W,H",
|
||||
help=(
|
||||
"zone position (top/bottom/left/right/...) 의 slide-body 내부 비율 (0~1) "
|
||||
"강제. horizontal-2 / vertical-2 만 지원. multiple 가능: "
|
||||
"--override-zone-geometry top=0,0,1,0.3 --override-zone-geometry bottom=0,0.3,1,0.7"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
overrides_frames: dict[str, str] = {}
|
||||
for ov in args.override_frames:
|
||||
if "=" not in ov:
|
||||
print(
|
||||
f"[error] --override-frame must be UNIT_ID=TEMPLATE_ID, got: '{ov}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
k, v = ov.split("=", 1)
|
||||
overrides_frames[k.strip()] = v.strip()
|
||||
|
||||
overrides_geoms: dict[str, dict] = {}
|
||||
for ov in args.override_zone_geometries:
|
||||
if "=" not in ov:
|
||||
print(f"[error] --override-zone-geometry must be ZONE_ID=X,Y,W,H, got: '{ov}'", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
zid, vals = ov.split("=", 1)
|
||||
parts = vals.split(",")
|
||||
if len(parts) != 4:
|
||||
print(f"[error] --override-zone-geometry expects 4 floats X,Y,W,H, got: '{vals}'", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
try:
|
||||
x, y, w, h = (float(p) for p in parts)
|
||||
except ValueError:
|
||||
print(f"[error] --override-zone-geometry floats parse fail: '{vals}'", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
overrides_geoms[zid.strip()] = {"x": x, "y": y, "w": w, "h": h}
|
||||
|
||||
run_phase_z2_mvp1(
|
||||
args.mdx_path,
|
||||
args.run_id,
|
||||
override_layout=args.override_layout,
|
||||
override_frames=overrides_frames or None,
|
||||
override_zone_geometries=overrides_geoms or None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user