From 022da2f63aba8f5555cca94ef714f1c395808ca2 Mon Sep 17 00:00:00 2001 From: DanMeon Date: Sun, 3 May 2026 22:59:08 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20v0.4.0=20spec/ADR=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=E2=80=94=20IR=20view=20=EB=A0=8C=EB=8D=94=EB=9F=AC?= =?UTF-8?q?=20(Markdown=20/=20HTML)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경사항: - view-renderer spec 본문 추가 (Draft) — `HwpDocument.to_markdown()` / `to_html()` 인스턴스 메서드, 9 결정사항 / 11 인수조건 / 7 영구 비목표 - 짝 페어 ADR 추가 — API placement / Markdown 방언 / 표 셀 병합 / 이미지 처리 범위 / furniture 처리 5 결정 근거 - 활성 spec 인덱스(`docs/roadmap/README.md`)에 v0.4.0 (view 렌더러) Draft 행 추가 Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/design/v0.4.0/view-renderer-research.md | 212 +++++++++++++++++++ docs/roadmap/README.md | 1 + docs/roadmap/v0.4.0/view-renderer.md | 64 ++++++ 3 files changed, 277 insertions(+) create mode 100644 docs/design/v0.4.0/view-renderer-research.md create mode 100644 docs/roadmap/v0.4.0/view-renderer.md diff --git a/docs/design/v0.4.0/view-renderer-research.md b/docs/design/v0.4.0/view-renderer-research.md new file mode 100644 index 0000000..5e6c55a --- /dev/null +++ b/docs/design/v0.4.0/view-renderer-research.md @@ -0,0 +1,212 @@ +--- +status: Draft +description: "v0.4.0 view-renderer ADR — API placement / Markdown 방언 / 표 병합 폴백 / 이미지 처리 범위 / furniture 처리 5 결정의 근거" +target: v0.4.0 +last_updated: 2026-05-03 +--- + +# v0.4.0 view-renderer — 설계 의사결정 리서치 요약 + +[v0.4.0/view-renderer.md](../../roadmap/v0.4.0/view-renderer.md) §결정 사항 중 외부 독자가 "왜?" 를 던질 만한 5건의 업계 선례·대안·실패 시나리오를 기록한다. spec 본문이 최종 결정을 기술하고, 본 문서는 그 결정의 근거를 담는다. + +## 결정 매트릭스 + +| # | 항목 | 옵션 비교 | 채택 | 1차 근거 | +|---|---|---|---|---| +| 1 | API placement | A: free function `rhwp.view.markdown(doc)` / B: `HwpDocument` 인스턴스 메서드 / C: `Document` wrapper 메서드 | **B** | IR 모델 자기-기술 + Pydantic / Docling 동등 패턴 일치 | +| 2 | Markdown 방언 | A: CommonMark / B: GFM / C: Pandoc Markdown / D: MyST | **B** | 표·각주·코드펜스로 IR 핵심 블록 모두 표현 + 가장 넓은 클라이언트 호환 분모 | +| 3 | 표 셀 병합 표현 | A: GFM 표만 (병합 시 lossy) / B: HTML `` 인라인 폴백 / C: Markdown extension (`{rowspan=2}` 등) | **B** | GFM spec 자체가 raw HTML inline 허용. `TableBlock.html` 재사용으로 본 binding 내 단일 source | +| 4 | 이미지 처리 범위 | A: placeholder 만 / B: placeholder + embedded / C: 3 모드 모두 (placeholder + embedded + external) | **A** | embedded / external 은 raw bytes resolution → `Document.bytes_for_image()` 의존, IR 메서드 위치 결정 (§1) 과 충돌 | +| 5 | furniture 처리 | A: 모두 비포함 / B: 각주/미주만 footnote 형식 / C: 헤더/푸터까지 모두 포함 | **B** | 각주/미주는 `marker_prov` 로 본문 1:1 / 헤더/푸터는 페이지 단위 → 페이지 무관 view 에서 1:N 매핑 모호 | + +## 1. API placement — IR 메서드 vs free function vs Document 메서드 + +### 팩트 + +- 본 binding IR 모델: `python/rhwp/ir/nodes.py:652` (`HwpDocument` BaseModel, `frozen=True`) +- 본 binding Document wrapper: `python/rhwp/document.py:43` (`Document`, Rust `_Document` thin wrapper, `unsendable`) +- Pydantic V2 BaseModel 은 메서드 추가 가능 — `frozen=True` 는 field 변경만 차단, method 정의 무관 () +- 동등 라이브러리 패턴: + + | 라이브러리 | 모델 | 변환 메서드 | + |---|---|---| + | Pydantic | `BaseModel` | `model_dump()`, `model_dump_json()` | + | Docling | `DoclingDocument` | `export_to_markdown()`, `export_to_html()`, `export_to_text()` | + | Marshmallow | `Schema` | `dump()`, `load()` | + | Word docx (`python-docx`) | `Document` | (변환 없음 — IR 부재) | +- 본 binding 선례: `Document.to_ir()` / `Document.to_ir_json()` — Document 가 IR 으로 변환, IR 자체는 직렬화 메서드만 (Pydantic 기본). 즉 *Document → IR* 은 Document 책임, *IR → string* 은 IR 책임으로 분리 가능 + +### 검증자 반박 + +- "free function `rhwp.view.markdown(doc)` 가 함수형 스타일로 더 명시적 아닌가?" → 발견성 (discoverability) 낮음 — `from rhwp.view import markdown` import 부담. 인스턴스 메서드는 `doc.to_markdown()` 한 줄로 IDE autocomplete 활용. Pydantic / Docling 등 동등 라이브러리 모두 인스턴스 메서드 채택 — 사용자 학습 비용 절감 +- "`Document.to_markdown()` 으로 두면 raw bytes 도 처리 가능한데?" → Document 메서드는 Rust `_Document` 영역. IR 위 변환은 IR 책임 — 책임 분리. `doc.to_ir().to_markdown()` chain 으로 두 단계 명시. 추가 image bytes 처리는 별도 spec 에서 `Document.to_markdown_with_images()` wrapper 추가 (forward compat) — v0.5.0+ 검토 +- "IR 모델에 view 메서드 추가는 모델 비대화? `HwpDocument` 가 view 라이브러리도 알아야 함" → schema 변경 (Pydantic field 추가) 이 아닌 method 추가 — JSON serialization 영향 없음. 메서드 본체는 별도 모듈 (`python/rhwp/ir/_view.py`) 에 두고 `HwpDocument.to_markdown` 은 위임 — import cycle 회피 가능 + +### 최종 결정 + +**옵션 B — `HwpDocument` 인스턴스 메서드**. 동등 라이브러리 (Pydantic / Docling / Marshmallow) 의 일치 + discoverability + 책임 분리 (IR ↔ string 변환은 IR 책임). + +### 1차 소스 + +- Pydantic `BaseModel.model_dump`: +- Docling `DoclingDocument` 변환 메서드: +- 본 binding `HwpDocument` 정의: `python/rhwp/ir/nodes.py:652` +- 본 binding `Document` 정의: `python/rhwp/document.py:43` + +## 2. Markdown 방언 — GFM 채택, CommonMark / Pandoc / MyST 거부 + +### 팩트 + +- CommonMark (): 표 / 각주 모두 미지원 (인용 / 헤딩 / 리스트 / 코드펜스만) +- GFM (): CommonMark superset + 표 (`|a|b|`) + 각주 (`[^1]`) + 코드펜스 (extended) + strikethrough + tasklist +- Pandoc Markdown (): CommonMark + 4 종 표 + 각주 + 정의 리스트 + 인라인 수학 — 가장 표현력 높음 +- MyST (): CommonMark + Sphinx-style directives + 수학 + cross-ref — 학술 / Jupyter 친화 +- IR 핵심 블록 ↔ 방언 호환 매트릭스: + + | IR 블록 | 출력 패턴 | CommonMark | GFM | Pandoc | MyST | + |---|---|---|---|---|---| + | `ParagraphBlock` | text | ✓ | ✓ | ✓ | ✓ | + | `TableBlock` (단순) | `|a|b|` | ❌ | ✓ | ✓ | ✓ | + | `TableBlock` (병합) | raw HTML inline | ⚠️ | ✓ | ✓ | ⚠️ | + | `FootnoteBlock` | `[^N]` | ❌ | ✓ | ✓ | ✓ | + | `FormulaBlock` | `$$...$$` | ❌ | ✓ (de-facto) | ✓ | ✓ | + | `PictureBlock` | `![alt](uri)` | ✓ | ✓ | ✓ | ✓ | + | `ListItemBlock` | `-` / `1.` | ✓ | ✓ | ✓ | ✓ | +- 클라이언트 호환 (사실상 표준 분모): + - GitHub / Slack / Discord / Linear / Notion: GFM 직접 렌더 + - Anthropic / OpenAI 출력: GFM 호환 syntax 가 LLM 학습 분포에 다수 + - Pandoc / MyST: 별도 toolchain 필요 + +### 검증자 반박 + +- "Pandoc 이 가장 표현력 높은데 왜 안 채택?" → Pandoc 은 `pandoc` CLI 환경 의존 + 그 toolchain 외에서 lossy. 본 spec 의 1차 사용처 (RAG / MCP / GitHub 표시) 는 GFM 호환 환경 — Pandoc-only markup 은 RAG 호환 분모를 좁힘. GFM 출력을 사용자가 `pandoc -f gfm -t myst` 등으로 변환 가능 (downstream) +- "MyST 는 Sphinx / Jupyter 환경에 특화된 가치 있지 않나?" → 본 spec 의 1차 사용처는 RAG (LLM 입력) 이라 Sphinx / Jupyter 비호환 markup 불필요. MyST directive (`{note}`, `{admonition}`) 는 LLM 학습 분포 희박 — 가독성 저하 위험 +- "GFM 의 math 확장이 표준 spec 에 없는데 KaTeX `$$` 사용?" → GFM spec 자체는 math 미포함이지만 GitHub (2022 deploy) / Slack / Discord 등 주류 클라이언트가 `$$` KaTeX 로 렌더 — de-facto 표준. Pandoc / MyST 와도 syntax 호환 (`$$`) — 미래 방언 추가 시 기본 호환 +- "CommonMark + GFM 표/각주 확장만 픽 vs full GFM?" → GFM 자체가 CommonMark superset + 확장 정의 — 픽업 범위 모호하면 spec 표현 일관성 흔들림. full GFM 채택이 표준 호환 + +### 최종 결정 + +**옵션 B — GFM**. 표·각주·코드펜스의 IR 핵심 구조 모두 표현 가능 + 가장 넓은 클라이언트 호환 + KaTeX 사실상 호환. Pandoc / MyST 추가는 영구 비목표 — 사용자가 GFM 출력을 외부 변환기로 처리. + +### 1차 소스 + +- GFM spec: +- CommonMark spec: +- Pandoc Markdown 매뉴얼: +- MyST 문서: +- GitHub LaTeX math 지원 (2022): +- HtmlRAG (구조 보존이 RAG 성능 향상에 기여, WWW 2025): + +## 3. 표 셀 병합 (rowspan/colspan) 표현 — HTML 인라인 폴백 + +### 팩트 + +- GFM Tables extension (): 모든 셀이 단일 행/열 (rowspan/colspan 미지원). pipe 문자 `|` 가 곧 행 구분자 +- CommonMark HTML blocks (): raw HTML 인라인 허용 — block-level 또는 inline-level 모두 +- IR `TableBlock` 3중 표현 (`python/rhwp/ir/nodes.py:550`): + - `cells: list[TableCell]` — 프로그래매틱 접근 (병합 정보 포함) + - `html: str` — `
` 인라인 형식, rowspan/colspan 보존 (RAG 주입용) + - `text: str` — 단순 평문 (검색 / diff 폴백) +- 대안 spec: + - Pandoc grid table: ASCII art (`+----+`) — visual 양호, parser 의존 + - markdown-it-py custom rules (`@mditable` 등): third-party 정의, 호환성 좁음 + - Markdown extension (`{rowspan=2}`): 사실상 표준 없음 +- 동등 라이브러리: + - Docling: 단순 표는 GFM, 병합 셀은 HTML inline (Docling `MarkdownExporter._export_table`) + - Pandoc: `--from html --to gfm` 변환 시 HTML 그대로 보존 + +### 검증자 반박 + +- "병합 셀 표만 HTML 폴백 → 같은 출력에 표 두 형태가 공존, 사용자 혼란?" → 두 형태 모두 GFM 호환 (raw HTML inline 허용). LangChain / RAG 소비자도 두 형태 처리 가능. lossy 회피가 우선 — RAG quality 가 최종 KPI +- "표를 항상 HTML 로 통일하면 일관성 확보 아닌가?" → 단순 표 (모든 span=1) 는 GFM `|a|b|` 가 사람이 읽기 좋음 — 가독성 우위. lossy 위험 없는 단순 표는 native GFM +- "TableBlock.html 재사용 → escape 정책 / class 이름 / id 등이 view 출력 표준과 어긋날 위험?" → IR Rust 코어가 생성한 html 그대로 — 본 binding 내 단일 source. drift 위험 없음. 미래 spec 에서 html 재합성이 필요해지면 그때 분기 (YAGNI) +- "병합 셀 검출 비용?" → `any(cell.row_span > 1 or cell.col_span > 1 for cell in table.cells)` — `O(n_cells)`, 표 1개당 마이크로초 + +### 최종 결정 + +**옵션 B — HTML `
` 인라인 폴백 (병합 셀 있는 표만)**. 단순 표는 native GFM, 병합 셀은 `TableBlock.html` 그대로 재사용. lossy 회피 + 가독성 양립 + 코드 중복 0. + +### 1차 소스 + +- GFM Tables extension: +- CommonMark HTML blocks: +- IR `TableBlock` 3중 표현: `python/rhwp/ir/nodes.py:550` +- Pandoc grid tables: +- Docling table export: + +## 4. 이미지 처리 범위 — placeholder only for v0.4.0 + +### 팩트 + +- IR `PictureBlock.image: ImageRef | None` (`python/rhwp/ir/nodes.py:286`) +- IR `ImageRef.uri` (`python/rhwp/ir/nodes.py:234`): v0.3.0 시점 항상 `bin://` 형식 — `data:` (embedded) / `file://` (external) 은 v0.4.0+ opt-in 으로 docstring 에 예고 +- raw bytes 해석 경로: `Document.bytes_for_image(picture)` (`python/rhwp/document.py:150`) → Rust `_Document` 의 `bytes_for_image_id` 호출 → `bin_data_content` lookup +- IR `HwpDocument` 단독으로는 bin_data 접근 불가 — `_Document` 가 함께 살아있어야 함 (Document 폐기 후 IR 만 남으면 raw bytes 잃음 — by design, IR 직렬화 가능성 보장) +- 동등 라이브러리 패턴: + - Pandoc: `--extract-media=DIR` — 외부 파일 추출, CLI 호출자가 dir 지정 + - Docling: `MarkdownExportOptions(image_mode="placeholder"|"embedded"|"referenced")` — 3 모드, 호출자가 base path 지정 +- 로드맵 README narrative (활성 spec 인덱스 § v0.4.0): "이미지 처리: `Picture.ref_mode` 를 따름 (`placeholder` → ``, `embedded` → base64 `src=`, `external` → 외부 파일 + 경로 반환)" — 3 모드 의도 (작성 시점 narrative) + +### 검증자 반박 + +- "embedded 모드는 RAG 사용처에 따라 가치가 큰데 왜 v0.4.0 미제공?" → IR 메서드 위치 결정 (§1) 과 충돌. embedded 는 raw bytes resolution → `Document.bytes_for_image()` 의존 → IR 메서드가 Document 를 알아야 함 → 의존 방향 역전 + IR 직렬화 후 (Document 폐기) 호출 시 동작 모호. 별도 spec 에서 `Document.to_markdown_with_images()` wrapper 추가 (v0.5.0+) +- "external 모드는 file I/O 라 spec 의 의도와 맞지 않은가?" → external 은 호출자가 출력 디렉토리를 지정하는 IO side-effect. 본 spec 의 view 출력 (string return) 과 의미축 다름 — 영구 비목표 +- "placeholder 만 제공하면 RAG 사용자가 실제 이미지를 못 보지 않나?" → RAG 사용처는 LLM 입력 텍스트가 1차. multimodal RAG (이미지 직접 LLM 입력) 도 alt-text + binary lookup 으로 충분 (사용자가 `Document.bytes_for_image()` 별도 호출). 1차 사용처 (텍스트 RAG) 와 매칭 +- "로드맵 narrative 가 3 모드 의도를 명시했는데 spec 이 1 모드로 좁혀도 되나?" → narrative 는 미정 의도. 본 spec 작성 시점에 IR 메서드 위치와 충돌 발견 → 해결책으로 범위 축소. narrative 는 결정 사항이 아니라 작성 직전 상태 — spec 이 SSOT + +### 최종 결정 + +**옵션 A — placeholder 만 (image_mode 인자 없음)**. PictureBlock 출력은 IR `picture.image.uri` (`bin://`) 그대로 + alt-text (description). embedded / external 은 영구 비목표 — `Document` wrapper 위 별도 spec. + +### 1차 소스 + +- IR `PictureBlock` / `ImageRef`: `python/rhwp/ir/nodes.py:234,286` +- `Document.bytes_for_image`: `python/rhwp/document.py:150` +- Pandoc `--extract-media`: +- Docling export options: + +## 5. furniture 처리 — 각주/미주만 포함, 헤더/푸터 비포함 + +### 팩트 + +- IR `Furniture` 컨테이너 (`python/rhwp/ir/nodes.py:633`): + - `page_headers: list[Block]`, `page_footers: list[Block]` — 페이지 단위 + - `footnotes: list[FootnoteBlock]`, `endnotes: list[EndnoteBlock]` — 본문 인용 단위 +- `FootnoteBlock.marker_prov: Provenance` (`python/rhwp/ir/nodes.py:373`): 본문 인용 마커 위치 (`section_idx`, `para_idx`) — RAG 가 각주가 어디서 인용됐는지 역추적 가능 +- 머리글/꼬리말은 페이지 단위 — 본문 paragraph 와 1:N 매핑 모호 (모든 본문 paragraph 가 헤더/푸터 아래에 있음). view 출력은 페이지 무관 단일 string — 페이지 경계 명시 안 함 +- Markdown footnote 표준: + - GFM: `[^1]` ref + `[^1]: definition` 정의 () + - CommonMark: 비표준 (Pandoc / kramdown 확장) + - Pandoc: 동일 syntax (`[^1]`) +- HTML5 `