Releases: DanMeon/rhwp-python
v0.5.1 — MCP tool 출력 강타입화 (PATCH)
PATCH release. v0.5.0 GA 한 rhwp-mcp 의 7 도구 중 약타입 (dict[str, Any] / list[dict[str, Any]]) 으로 반환하던 3 도구 (get_ir / iter_blocks / chunks) 의 출력 시그니처를 Pydantic V2 모델 (HwpDocument / list[Block] / list[ChunkRecord]) 로 강타입화. fastmcp v3 의 자동 outputSchema 가 약타입 (additionalProperties: true 만) → 강타입 (필드별 type / Discriminator + Tag 11 변형 / required 명시) 으로 강화 — LLM 의 응답 해석 / 후속 도구 호출 정확도 향상. wire format byte-equal — 외부 클라이언트 (Claude Desktop / Cline 등) 의 LLM 프롬프트 / 후처리 코드 영향 0. 코어 wheel 의존성 / extras / schema ("1.1") 변경 없음.
Added
rhwp.mcp.tools.ChunkRecordBaseModel 신규 —page_content: str+metadata: dict[str, Any].model_config = ConfigDict(extra="forbid", frozen=True). RAG 청크의 직렬화 표면 — LangChainDocument의page_content/metadata평탄화. mode × block kind 동적 metadata 키 집합은rhwp.integrations.langchain.HwpLoader가 SSOT — 분기 모델 (ChunkRecord_Single등) 거부 결정으로metadata자유 dict 유지 (schema 비대 + forward-compat 깨짐 회피).
Changed
rhwp.mcp.tools.get_ir(path)반환 타입 —dict[str, Any]→HwpDocument. fastmcp 가 자동으로model_dump(mode="json")직렬화 →result.structured_content가 v0.5.0 dict 출력과 byte-equal.result.data는 typed BaseModel 인스턴스 (v0.5.1 신규 표면) — discriminated union block 들의 강타입 access 가능.rhwp.mcp.tools.iter_blocks(path, ...)반환 타입 —list[dict[str, Any]]→list[Block]. callable Discriminator + Tag 11 변형 (paragraph / table / picture / formula / footnote / endnote / list_item / caption / toc / field / unknown) 그대로 노출 — outputSchema 의oneOf11 변형이 LLM 에 정확한kind별 필드 구조 노출.rhwp.mcp.tools.chunks(path, ...)반환 타입 —list[dict[str, Any]]→list[ChunkRecord]. dict 평탄화 코드 제거 — fastmcp 자동 직렬화에 위임.- README MCP 도구 표 출력 컬럼 갱신 + v0.5.1 마이그 노트 한 단락 추가 —
result.data의 dict 인덱싱 → typed attribute access 마이그 (단iter_blockslist element 는 fastmcp v3 의oneOfdeserialization 한계로 dict 폴백 — v0.5.0 access 패턴 그대로 동작).
Fixed
python/rhwp/ir/nodes.py의UnknownBlock.kind의 JSON Schema 에not.enum: sorted(_KNOWN_KINDS)constraint 추가 (Field(json_schema_extra=callable)표준 hook).fastmcpv3 +jsonschema의 strictoneOfvalidation 이 ParagraphBlock 과 UnknownBlock 양쪽 valid 인스턴스 (예: 빈 ParagraphBlock) 에서RuntimeError: Invalid structured content로 fail 하던 client side wire format 호환 깨짐 해결. callable Discriminator (_block_discriminator) 의 런타임 동작은 SSOT 그대로 — schema export 만 strict 화. packagedpython/rhwp/ir/schema/hwp_ir_v1.json자동 재생성. v0.2.0 Frozen IR 의 forward-compat 라우팅 (미지 kind →UnknownBlock) 보존 —tests/test_ir_schema_export.py::test_unknown_kind_routing_pydantic_matches_schema가 회귀 가드.
Build
external/rhwpsubmodule pin 변경 없음 — v0.5.0 동일 (62a458a, v0.7.10). 본 PATCH 는 pure Python schema 강화, 상류 변경 0.- 신규 의존성 / extras 변경 없음. CI
test-without-extrasjob 의 expected skip count 5 그대로 (tests/test_mcp_server.py의 file-levelpytest.importorskip("fastmcp")보존).
Notes
- spec / ADR / 구현 로그: docs/roadmap/v0.5.1/mcp-typed-output.md (Frozen, 10 인수조건, 8 결정) / docs/design/v0.5.1/mcp-typed-output-research.md (Frozen, 5 결정 매트릭스) / docs/implementation/v0.5.1/migration.md (Frozen).
- v0.5.1 작업 중 표면화된 fastmcp v3 의 두 한계는 spec body 에 정확히 반영: (1)
result.structured_content의 wrap 분기 (BaseModel = inline /list[T]={"result": ...}envelope), (2) callable Discriminator + Tag 유니온의oneOfschema 가 dynamic 모델 reconstruct 안 됨 (list element dict 폴백). server side 의 typed 출력은 AC-2 / AC-3 가, wire format byte-equal 은 AC-5 가 cover.
v0.5.0 — MCP server (rhwp-mcp)
MINOR release. Model Context Protocol (Anthropic, 2024) 서버를 새 entry point rhwp-mcp 로 노출한다. LLM 에이전트 (Claude Desktop / Cursor / Cline / Continue.dev / Goose / 자체 에이전트) 가 HWP/HWPX 를 직접 파싱·요약·청크화 가능. standalone fastmcp v3 (jlowin) 기반 — 2026-05 기준 MCP 서버 약 70% 시장 점유의 사실상 표준. 7 도구 / 2 transport (stdio 기본 + streamable-http 옵션) / runtime extras gate / unsendable 안전 패턴 강제. 코어 wheel 의존성 변경 0 (additive extras), schema ("1.1") 유지.
Added
- 새 entry point
rhwp-mcp = "rhwp.mcp:run"— argparse 기반--transport stdio | streamable-http/--host/--portCLI. stdio 가 기본 (Claude Desktop / IDE 통합용), streamable-http 는 옵션 (서버 배포 / 다중 클라이언트, uvicorn ASGI).--host기본127.0.0.1외부 노출 회피 +--port[1, 65535] argparse validator + stdio 와 명시적 host/port 조합 시parser.error강제 (silent ignore 보안 사고 회피). - 7 MCP 도구 (
mcp.server.fastmcp.FastMCP.tool()등록):parse_hwp_summary(path)/extract_text(path)/get_ir(path)/iter_blocks(path, kind?, scope, limit?)/to_markdown(path)/to_html(path, *, include_css=False)/chunks(path, mode, size, overlap, include_furniture). 모두 sync 함수 —_Document가unsendable이라 handler 안에서 parse → consume → primitive return 패턴 강제 (async +asyncio.to_thread(rhwp.parse, ...)는 panic).chunks는 런타임 lazy import —langchain-text-splitters미설치 시 fastmcpToolError로 wrap → MCPCallToolResult(isError=True)응답 (서버 기동 / 다른 6 도구는 정상 — AC-7). - 모듈 위치:
python/rhwp/mcp/{__init__.py, __main__.py, server.py, tools.py}(top-level,integrations/가 아님).__init__.py는 lazy-import 패턴 —rhwp.cli와 동일하게[mcp]extras 미설치 시 친절 에러 + exit 2 (AC-1). - 새 extras:
[project.optional-dependencies] mcp = ["fastmcp>=3,<4"]+mcp-chunks = ["fastmcp>=3,<4", "langchain-core>=0.2", "langchain-text-splitters>=0.2"]. extras 키 이름은 "MCP 서버 기능" 표시 — 의존성 패키지명 (fastmcp) 과 분리.[examples]extras 가 fastmcp 합집합 포함하도록 갱신. examples/06_mcp_server.py— fastmcpClient(server)in-process round-trip 데모 (typer 기반,--skip-chunks옵션). 7 도구 차례로 호출하며 출력 형식 학습용.- README § "MCP server (
rhwp-mcp)" 섹션 신설 — 도구 7 종 표 / Claude Desktopclaude_desktop_config.json등록 예 / 클라이언트 호환성 표 (Claude Desktop / Cline / Cursor / Continue.dev / Goose / 자체 에이전트, transport 별 ✅/❌/⚠️ ) / streamable-http 사용 예. - spec / ADR / 구현 로그: docs/roadmap/v0.5.0/mcp.md (Frozen, 11 인수조건) / docs/design/v0.5.0/mcp-research.md (Frozen, 4 결정 매트릭스 — SDK 채택 근거 / transport 우선순위 / handler 동시성 / 도구 분할) / docs/implementation/v0.5.0/stages/ (S1 ~ S5 Frozen).
Changed
- ADR § 1 SDK 결정 갱신 (S1 진행 중) — 공식
mcpPython SDK (FastMCP v1 흡수) → standalonefastmcpv3 (jlowin). 2026-05 현업 표준 패턴 정합 (시장 점유 약 70%) + v3 의 OAuth / OpenTelemetry / server composition / streamable-http 우선 같은 프로덕션 기능. 공식 SDK 의 FastMCP v1 은 frozen 상태 — 추가 framework 기능은 standalone 으로만 발전. docs-lint정책 갱신 (S1 진행 중) —Frozen + target조합을docs/implementation/vX.Y.Z/pre-GA stage log 에 한해 허용 (scripts/_doc_lint.py의is_pre_ga_stage면제 분기). Rust RFC / PEP / ADR 의 editorial vs release 차원 분리 패턴 정합 — stage 본문은 작성 즉시 immutable, GA 라벨은 미부여 (CONVENTIONS § 131 의 의도). CONVENTIONS.md § 필드 schema 에 예외 명시.- CI
test-without-extrasjob — expected skip count 4 → 5 (tests/test_mcp_server.py의 file-levelpytest.importorskip("fastmcp")추가)..github/workflows/ci.yml+AGENTS.md§ Tests 동시 갱신 (AC-11).
Build
external/rhwpsubmodule pin0fb3e67유지 — 본 MINOR 는 pure Python MCP layer, 상류 변경 0.- 신규 의존성 (extras 만):
fastmcp>=3,<4([mcp],[mcp-chunks],[examples],[dependency-groups] testing). 코어 wheel 의존성 (pydantic>=2.5,<3) 변경 없음.
Notes
- 회귀 가드: tests/test_mcp_server.py (40 테스트 — 36 fast + 1 slow + 3 LOW reviewer-suggested). file-level
importorskip("fastmcp")게이트, 메서드별importorskip("langchain_text_splitters")게이트로 chunks smoke 분리. AC-1 ~ AC-11 모두 11/11 충족 (evidence 매핑은 stage-5.md § AC sweep 참조). - 미확정 이슈는 v0.5.0 GA 후 demand-driven:
get_ir응답 크기 / 에러 응답 형식 통일 / Resource·Prompt 추상 / 출력 schema 강타입화 (ChunkRecord등). spec § 미확정 이슈 에 기록.
v0.4.0 — IR view 렌더러 (Markdown / HTML)
MINOR release. Document IR (HwpDocument) → Markdown / HTML view 변환 표면을 추가한다. v0.7.0 MCP server (to_markdown / to_html 도구) + 후속 RAG 프레임워크 통합 (v0.5 LlamaIndex / v0.6 Haystack) 의 문자열 출력 1차 인터페이스로 사용. Pure-stdlib 구현 — 신규 의존성 0, schema ("1.1") / 파싱 경로 / Document wrapper / extras 모두 변경 없음 (additive only).
Added
HwpDocument.to_markdown() -> str— IR → GFM (GitHub Flavored Markdown). 표는 모든 셀span == 1일 때 GFM|...|표, 병합 셀 (rowspan/colspan > 1) 이 있으면TableBlock.html인라인 폴백 (lossy 회피). 이미지는 placeholder 모드 (picture.image.uripass-through, alt 의[/]는 backslash escape), 수식은script_kind/inline분기 (latexdisplay →$$..$$, inline →$..$,hwp_eq→```hwp-eqfenced,mathml→```mathmlfenced), 각주/미주는 본문 paragraph 끝[^N]reference + 출력 끝[^N]: text정의 (미주는[^enN]별도 number 공간). 머리글/꼬리말은 출력 미포함 (페이지 단위 장식, 결정 8).ListItemBlock.level은" " * level들여쓰기로 보존.HwpDocument.to_html(*, include_css: bool = False) -> str— IR → 완전 HTML5 문서 (<!DOCTYPE html>+<html>+<head>+<body>). 표는 IRTableBlock.html그대로 inline (재합성 없음, rowspan/colspan 보존), 이미지는<img alt="<description>" src="<picture.image.uri>">, 수식 디스플레이는<div class="math">$$..$$</div>(KaTeX/MathJax 호환), 인라인은<span class="math">$..$</span>,hwp_eq/mathml은<pre><code class="language-...">. 각주/미주는 본문 직후<aside id="fn-N|en-N" class="footnote|endnote">정의 + 본문 안<sup><a href="#...">[N]</a></sup>인용 마커.ListItemBlock.level > 0은<li data-level="N">속성으로 보존.include_css=True일 때<head>안 embedded<style>1회 동봉 (외부 stylesheet 도입 없음).- spec / ADR / 구현 로그: docs/roadmap/v0.4.0/view-renderer.md / docs/design/v0.4.0/view-renderer-research.md / docs/implementation/v0.4.0/migration.md. HtmlRAG (arXiv:2411.02959, WWW 2025) 등 최근 연구가 보고하는 구조 보존이 RAG 체감 성능과 직결 동기.
Build
external/rhwpsubmodule pin0fb3e67유지 — 본 MINOR 는 pure Python view layer, 상류 변경 0.
Notes
- 회귀 가드: tests/test_view_baseline.py —
aift.hwp/table-vpos-01.hwpx의Document.to_ir().model_dump_json(indent=2, exclude={"source"})가 v0.3.2 GA baseline (tests/baselines/v0_3_2_*_ir.json) 과 byte-equal. 향후 schema / 파싱 변경 시 baseline 도 함께 갱신.
v0.3.2
[0.3.2] — 2026-05-03
PATCH release. v0.2.0 IR 매핑이 보유해 온 자체 UTF-16 → codepoint 변환 복사본 (src/ir.rs::utf16_to_cp) 을 상류 Paragraph::utf16_pos_to_char_idx (PR #494 / Task #484, v0.7.9 GA) 로 치환해 SSOT 를 단일화한다. 알고리즘 동등 — IR 출력 byte-equal, 공개 API 변경 없음, SchemaVersion "1.1" 유지.
Build
src/ir.rs::utf16_to_cp자체 복사본 +u32::MAXshort-circuit +fallback_end인자 + 짝 단위 테스트 2건 (utf16_to_cp_sentinel_returns_fallback,utf16_to_cp_matches_first_ge) 제거.build_char_runs호출부를para.utf16_pos_to_char_idx(start_utf16)/(end_utf16)로 치환. 본 binding 운영 정책 ("상류 신뢰 + 결함 시 PR") 일관 적용 — v0.3.1 의Paragraph::control_text_positions채택과 같은 결.external/rhwpsubmodule pin0fb3e67유지 — 핀 history 에 PR #494 머지 commit60eaa91(2026-04-30) 포함,cargo build가 시그니처 해소로 직접 검증. v0.7.9 GA 흡수는 직교 영역, 본 PATCH 영구 비목표.- 부수 정리: 본 binding 이 제출한 issue 초안
docs/upstream/issue-utf16-pos-to-char-idx.mdin-place Frozen 전환 +docs/upstream/README.md인덱스 RESOLVED 컬럼 채움.
v0.3.1
[0.3.1] — 2026-05-02
PATCH release. v0.3.0 의 IR 출고에서 inline 컨트롤 마커의 Provenance.char_start / char_end 가 항상 null 이던 문제를 정정. 상류 v0.7.8 의 Paragraph::control_text_positions() (PR #405 / Task #390) 노출을 활용해 7 종 블록 (각주·미주 마커, 그림, 수식, 필드, TOC, 표) 의 zero-width character 위치를 채운다. SchemaVersion 변경 없음 ("1.1" 유지) — 기존에 nullable 슬롯에 정의된 int | None 에 non-null 값을 출고할 뿐, schema 호환 100%.
Fixed
- inline 컨트롤 마커 (각주/미주/그림/수식/필드/TOC/표) 의
Provenance.char_start/char_end가 v0.3.0 까지 항상 null 이던 문제 정정. 부모 paragraph 안 zero-width character 위치 (char_start == char_end == position) 로 채운다. - 상류
Paragraph::control_text_positions()(v0.7.8 GA, PR #405 / Task #390) 의 결과를 paragraph 당 1회 호출로 공유하여controls.len() == positions.len()길이 invariant 를 release/debug 모두에서assert_eq!로 가드 — 상류 contract 위반의 silent regression 차단. - 부모 paragraph 의
char_offsets가 빈 경우 (보통 layout-only 컨트롤만 있는 paragraph)None폴백 — 상류 fallback 분기의 의미 손실 position 을 그대로 흘리지 않음 (fail-fast).
Build
external/rhwpsubmodule pin033617e(v0.7.7) →0fb3e67(post-v0.7.8). 본 v0.3.1 의 enabling change 는 v0.7.8 의 PR #405 (pub fn Paragraph::control_text_positions). 후속 commit 들은 직교 영역 (Task #484utf16_pos_to_char_idx등) 으로 본 PATCH 동작에 영향 없음.- PyO3
extension-modulefeature 를 default features 에서 분리 (PR #13, PyO3 FAQ 권장 패턴) —cargo test가 libpython 링크 시도 없이 정상 작동,src/ir.rs의 Rust unit test 13개 (assert_position_invariant_panics_on_mismatch의#[should_panic]포함) 가 CI 에서 검증됨. wheel 빌드 동작 동일 —[tool.maturin] features = ["extension-module"]가 명시적 활성화. AC-12 invariant 가 source-grep 외에 진짜 panic 검증으로도 보호됨.
Known limitations
- 중첩 표 안 inline 컨트롤의
Provenance.char_start/end와(section_idx, para_idx)가 다른 paragraph 를 가리켜text[char_start:char_end]슬라이싱이 잘못된 결과를 낸다. v0.3.0 부터 있던 Provenance 모델 한계 — v0.3.1 은 새 마커 채움이 동일 모델을 재사용. v0.4.0+ Provenance 정정 spec 에서 다룬다 (spec § 영구 비목표 마지막 항목).
v0.3.0 — IR 확장 + rhwp-py CLI 재도입
Changed — async API 의존성 정리
rhwp.aparse가aiofiles대신 stdlibasyncio.to_thread사용 — 외부 의존성 제거.[async]extras 키는 빈 배열로 보존 (pip install rhwp[async]명령 호환 유지, v0.4.0 에서 키 자체 제거 검토). 의미·성능 동등 — Pythonasyncio가 native async file I/O 를 미지원하는 한 모든 async file lib (aiofiles 포함) 도 결국 thread pool wrapping 이라 둘은 같은 메커니즘. CItest-without-extrasskip count 5 → 4 (test_async.py가 더 이상 gated 아님).
MINOR release — Phase 2 두 축 동시 GA. v0.2.0 의 Document IR v1.0 위에 HWP 고유 의미 요소 8 종을 추가하고 (SchemaVersion 1.1), 동시에 Python 고유 가치 (IR/LangChain 청크/스키마 export) 를 shell 에서 직접 소비할 수 있는 rhwp-py CLI 를 재도입한다. 모든 v0.2.0 공개 API 보존 — 추가만 있고 기존 코드는 그대로 동작.
상류 edwardkim/rhwp 커밋 핀을 bea635b → 033617e 로 bump (upstream v0.6.x → v0.7.7 흡수, 380 commits). IR 확장이 사용하는 first-class struct/enum (Picture, Equation, Footnote/Endnote, Caption, Field/FieldType, Header/Footer, ParaShape) 자체는 핀 변경 전부터 노출돼 있어 IR 매핑 작업은 상류 변경에 의존하지 않는다 — bump 의 효과는 직교 영역에 한정: 렌더 경로 정정 (TypesetEngine pagination drift, TAC 표/그림 좌표 통합 수정), export text/markdown 추가 (Task #237), v0.7.6/v0.7.7 릴리즈 흡수.
Added — Document IR v1.1 (8 신규 블록 + Furniture 채움)
PictureBlock(S1) — 이미지.image: ImageRef | None(URI 스킴bin://<bin_data_id>기본),caption: CaptionBlock | None(S3 부터),description: str | None(HWP alt-text).Document.bytes_for_image(picture)헬퍼로 raw bytes 해석.FormulaBlock(S2) — 수식.script: str(HWP equation script raw) +script_kind: Literal["hwp_eq", "latex", "mathml"]+text_alt: str | None(RAG 폴백 평문 근사). LaTeX/MathML 자동 변환은 v0.3.0 미제공 (공개 변환기 부재).FootnoteBlock/EndnoteBlock(S2) — 각주 / 미주.blocks: list[Block]재귀,marker_prov(본문 인용 위치) 와prov(각주 본문 위치) 분리. 각주/미주 본문은furniture.footnotes/endnotes로 라우팅 — body 검색 오염 회피.ListItemBlock(S3) — 목록 항목.level + marker + enumerated평면 모델 (group container 미도입, HWP 상류가 list group 미지원).marker는 v0.3.0 placeholder ("•"/"1."/"-") — 정확 marker ("가.","(a)") 는 v0.4.0+.CaptionBlock(S3) — 캡션.blocks: list[Block]재귀 +direction: Literal["top", "bottom", "left", "right"]. 부모 컨테인먼트 —PictureBlock.caption/TableBlock.caption_block으로 1:1 부착 (ref-id 미도입). v0.2.0TableBlock.caption: str보존 +caption_block옵셔널 추가.TocBlock+TocEntryBlock(S3) — 목차.entries: list[TocEntryBlock](TocEntryBlock 은 leaf type, Block 유니온 멤버 아님). v0.3.0 entries 는 빈 placeholder — 항목 추출은 v0.4.0+ (heading hierarchy + bookmark resolver 필요).FieldBlock+FieldKind(S3) — 필드. 닫힌Literal14 종 (date/crossref/hyperlink/...) +"unknown"안전판 +field_type_code: int | None(forward-compat).FieldType::Formula는"calc"(Equation 의"formula"와 이름 충돌 회피).Furniture채움 (S1+S2) —page_headers/page_footers(master_pages + Control::Header/Footer 매핑) /footnotes/endnotes모두 실제 본문 출고.iter_blocks(scope="furniture")순서: page_headers → page_footers → footnotes → endnotes (v0.2.0 furniture 순서 계약 확장).
Added — rhwp-py CLI 재도입
v0.2.0 에서 폐기됐던 CLI 를 별도 이름 (rhwp-py) 으로 재도입. 상류 Rust rhwp 바이너리와 PATH 충돌 회피 + Python 고유 가치 (IR / LangChain) 에 집중 — 기능 중복 0.
- 신규 entry point:
rhwp-py = "rhwp.cli:app"(typer 지연 로드, 미설치 시 친절 에러 + exit 2). - 서브커맨드:
parse(요약 정보) /version/schema(in-package JSON Schema) /ir(전체 IR JSON) /blocks(블록 스트림 NDJSON / JSON / text) /chunks(LangChain RecursiveCharacterTextSplitter 결과). blocks의--kindenum 11 종 (paragraph/table+ 8 신규 +all) — IR 확장 GA 와 동기.chunks --include-furniture—HwpLoader(mode="ir-blocks", include_furniture=True)와 동일 정책.- 글로벌 옵션
--quiet/-q— stderr progress 메시지 가드. - 새 extras:
[cli](typer 만),[cli-chunks](typer + langchain-text-splitters). - 업스트림 바이너리와의 역할 분담: 구조 추출은
rhwp-py, 시각 출력 (SVG/PDF) / 메타데이터 덤프 (info/dump) / 라운드트립 진단 (diag/ir-diff) 은 상류rhwp— overlap 0.
Added — LangChain HwpLoader.include_furniture
HwpLoader(mode="ir-blocks", include_furniture=True)— body 다음에 furniture (page_headers / page_footers / footnotes / endnotes) 도 LangChain Document 로 yield. 각 furniture Document 는metadata.scope="furniture"로 표시되어 RAG 가 body / furniture 분리 색인 가능.- 기본
include_furniture=False— v0.2.0 시절 동작 (body 만) 보존. mode="single"/"paragraph"에서는include_furniture무시 — text 추출은 항상 body 만.
Added — Schema GA + content-addressed alias
- SchemaVersion
"1.0"→"1.1"(in-place v1 URL — major 안의 minor 추가). _KNOWN_KINDS11 known kinds (10 known + UnknownBlock) —BlockAnnotated Union 11 멤버. callable Discriminator 는 O(1) lookup.- JSON Schema in-place 갱신 —
python/rhwp/ir/schema/hwp_ir_v1.json(1,261 lines, 20$defs). - Content-addressed alias
hwp_ir_v1-sha256-<hash>.json—publish-schema.yml가 매 deploy 시 hash-tagged immutable copy 를 alongside 발행. 구 hash 는 영구 보존 (SchemaStore / 외부 도구 reproducibility). _harden_unknown_variant가_KNOWN_KINDSSSOT 를 사용 —TocEntryBlock.kind="toc_entry"같은 leaf-only kind 가 not.enum 에 포함되어 라운드트립 깨지는 케이스 회피.
Fixed
- LangChain
HwpLoader(mode="ir-blocks")와 CLIrhwp-py blocks --format text가 각주·미주·캡션 본문을 평문화할 때ParagraphBlock만 처리하여ListItemBlock으로 변환된 list 항목이 RAG 색인에서 통째로 누락되던 문제 정정. 신규rhwp.ir._plain_text모듈에ParagraphBlock+ListItemBlock+FormulaBlock+FieldBlock평문 추출 SSOT 헬퍼 (block_inline_text/join_inline_blocks) 를 도입하고 integration / CLI 양쪽에서 공유한다. caption 평문화도 동일 정책으로 통합 (langchain.py::_caption_plain_text/cli/ir.py::_caption_plain제거).
Documentation
docs/roadmap/v0.3.0/ir-expansion.md— IR 확장 spec (8 결정 사항 + research 인용).docs/roadmap/v0.3.0/cli.md—rhwp-py재도입 spec (이름 선정 + overlap=0 + extras 정책).docs/design/v0.3.0/ir-expansion-research.md/cli-design-research.md— 결정 증거.docs/implementation/v0.3.0/stages/stage-{1..4}.md— 단계별 구현 로그 (S1: Picture+Furniture, S2: Formula+Footnote/Endnote, S3: ListItem+Caption+Toc+Field, S4: Schema GA + CLI + LangChain include_furniture + 문서).README.md— v0.3.0 신규 블록 +rhwp-pyCLI 섹션 추가, content-addressed alias 안내.
Tests
- 5 신규 IR 테스트 파일 (S1 picture+furniture, S2 formula+footnote, S3 list+caption+toc+field) + 1 CLI 테스트 파일 → 405 (S3) → S4 추가.
tests/test_cli.py— typer.testing.CliRunner 기반 smoke + 통합 (parse/version/schema/ir/blocks/chunks 전 서브커맨드 + exit code 1/2 검증 + langchain-text-splitters 미설치 monkeypatch).tests/test_langchain_loader_ir.py확장 —include_furniture옵션 4 테스트.- CI
test-without-extrasskip count 4 → 5 (typer 추가). tests/test_ir_plain_text.py신규 + footnote/caption 회귀 테스트 (LangChain·CLI 양쪽) — ListItemBlock 누락 정정 가드.- 테스트 docstring 의 가변 카운트·스테이지 마커 정리 — 다른 파일·CI 잡에 의존하는 카운트가 박혀 있어 stale 되는 안티패턴 (
5 skipped 카운트 중 1/exactly 29 테스트 유지등) 제거, SSOT 단일화.
Deferred to v0.4.0+
ListItemBlock정확 marker ("가.","(a)") —Numbering.level_formatslookup.TocBlock.entries채움 +target_section_idxresolver +is_stale검출.FieldBlock.cached_value추출 (paragraph text inlinefield_ranges매핑 필요).InlineRun.href자동 채움 (Hyperlink/Bookmark Field 와 cross-link).PictureBlockembedded/external_dir임베딩 모드 (Document.to_ir(image_mode=...)옵션).RevisionMark(변경 이력) — 상류 미지원 (영구 비목표 후보).
v0.2.0 — Document IR v1 + binding 구조 개선
MINOR release — Phase 2 착수. RAG / LLM 파이프라인이 직접 소비하는 구조화 Document IR v1 (Pydantic V2 + JSON Schema Draft 2020-12) 을 도입. 기존 Document / HwpLoader API 는 변경 없음 (backward-compatible). 상류 edwardkim/rhwp 커밋 핀을 bea635b (main HEAD) 로 갱신 — v0.1.0 의 1636213 이후 upstream 변경은 docs (매뉴얼 현행화 / README 동기화 / 자기검열) 만으로 코드 동작 변화 없음. BMP→PNG 재인코딩 fix (#240) 는 여전히 upstream origin/devel 에만 있으며 본 release pin 에 미포함 — BMP 임베딩 HWP 의 SVG/PDF 렌더링 이슈는 upstream main 머지를 대기.
Added — Document IR v1
Document IR v1 — RAG / LLM 파이프라인이 직접 소비 가능한 구조화 문서 모델. Pydantic V2 기반 공개 타입 + JSON Schema (Draft 2020-12).
rhwp.ir.nodes모듈 —HwpDocument/ParagraphBlock/TableBlock/TableCell/InlineRun/Provenance/UnknownBlock/Furniture/DocumentMetadata/Section(10 노드, 전부frozen=True+extra="forbid").- Callable
Discriminator기반Block태그드 유니온 — 미지kind는UnknownBlock으로 라우팅하여 forward-compat 보장 (v0.3.0 의 새 블록 타입이 v0.2.0 소비자를 깨뜨리지 않음). Document.to_ir() -> HwpDocument+Document.to_ir_json(*, indent=None) -> str— RustOnceCell<Py<PyAny>>lazy 캐시 (unsendable 덕에 lock 불필요).HwpDocument.iter_blocks(*, scope, recurse)— body/furniture/all scope + TableCell 재귀 DFS 순회.- Rust 측 HTML/text 직렬화 — attribute 순서 고정 (rowspan→colspan), HtmlRAG 호환.
- JSON Schema export —
rhwp.ir.schema.export_schema()/load_schema()/SCHEMA_ID/SCHEMA_DIALECT+ in-packagehwp_ir_v1.json+python -m rhwp.ir.schemaCLI. - Discriminator 후처리 —
_harden_unknown_variant()가 UnknownBlock.kind 에not.enum: [known kinds]주입하여 oneOf 검증 정확도 보장. HwpLoader에mode="ir-blocks"추가 — Block 을 LangChainDocument로 매핑 (표는 HTML content + 구조화 메타, 단락은 text + Provenance).TableCell.role="layout"자동 태깅 — 병합된 빈 셀 (구조 유지용 비의미 셀) 을 LLM 이 "레이아웃 요소" 로 인식하도록 시맨틱 구분. 보수적 heuristic: 병합 AND 공백만 있는 셀만layout, 그 외 empty 셀은data유지..github/workflows/publish-schema.yml— GitHub Pages 배포 파이프라인, 불변 경로 정책 (v1 URL 영구) 자동화.- Provenance 단위는 Unicode codepoint — Python
str[i]슬라이싱과 직접 호환 (이모지/SMP CJK 혼용에서도 off-by-one 없음). - 신규 런타임 의존성:
pydantic>=2.5,<3. 테스트 의존성:jsonschema>=4. - 문서:
docs/roadmap/v0.2.0/ir.md(사양),docs/design/v0.2.0/ir-design-research.md(7개 결정 증거),docs/implementation/v0.2.0/stages/stage-{1..5}.md. - 테스트: 165 passed — IR schema/roundtrip/tables/iter/export + LangChain ir-blocks + Rust unit tests (
cargo test5 passed).
Added — Binding 구조 개선 (Python wrapper class + async 진입점)
#[pyclass(unsendable)] 제약 안에서 가능한 최선의 binding 구조와 async 진입점을 정착. 사용자-대면 API 는 전부 보존 — 가산만 있음 (breaking 없음).
- Python wrapper class 패턴: Rust
_Document는#[pyclass(name = "_Document", module = "rhwp._rhwp", unsendable)]thin core 로, Pythonrhwp.Document는__slots__ = ("_inner",)+_from_rustfactory 로 thin core 를 감싸는 wrapper. 모든 메서드는 pass-through. Document.from_bytes(data, *, source_uri=None) -> Document— bytes 기반 생성 classmethod (Rust_Document::from_bytes+py.detach로 GIL 해제). 네트워크 fetch / in-memory archive /aparse내부 경로용.rhwp.aparse(path) -> Documentasync 함수 —aiofiles로 파일 I/O 만 async 처리, 파싱은 event-loop 스레드에서 sync (Document.from_bytes).unsendable제약 상asyncio.to_thread(parse, path)가 panic 하므로 이 경로가 유일하게 안전한 async 진입점.[async]optional extras 추가 —aiofiles>=23. 미설치 시aparse호출 시점에 명시적ImportError(silent fallback 없음).HwpLoader.aload/alazy_loadasync override —rhwp.aparse위에 구축. 공통 yield 로직_yield_documents헬퍼로 sync/async 공유.python/rhwp/_rhwp.pyi신규 — Rust extension (_Document,version,rhwp_core_version) 의 Python 측 타입 stub.
Changed — Phase 2 계획 전환
- 원안의 CLI 도구 (
rhwp바이너리) 는 폐기. 업스트림edwardkim/rhwp의 Rust 바이너리가 같은 이름을 점유하므로 충돌 방지 + Python 고유 가치 (RAG / LangChain 통합) 에 집중. 상세:docs/roadmap/v0.2.0/ir.md§방향 전환 배경. python/rhwp/__init__.pyi에Document.to_ir/to_ir_json타입 힌트 추가.pyproject.toml [tool.maturin] include에python/rhwp/ir/schema/*.json포함 (wheel + sdist).
Changed — Python 지원 범위 상향 (3.10+)
- Python 3.9 지원 드랍 —
requires-python = ">=3.10",pyo3feature 를abi3-py39→abi3-py310으로 전환, CI 매트릭스에서3.9제거. Python 3.9 는 2025-10-31 EOL 이후 보안 패치가 중단된 상태 (> 6 개월 경과). 기존 공개 API 는 전부 호환 — 3.9 사용자는 PyPI 의rhwp-python 0.1.x를 계속 사용 가능. rhwp.ir.schema.load_schema()의Traversable.joinpath()호출을 chain 패턴 (joinpath(a).joinpath(b)) 으로 정리 —*descendants가변 인자 시그니처가 표준 라이브러리에 도입된 시점이 버전별로 달라 typeshed 기준 pyright 가 py3.9/3.10/3.11 에서reportCallIssue를 내는 문제 제거.
Changed — IR 매핑 구조 (Rust → Python 이전)
IR 합성 (HTML 직렬화 / cell role 분류 / inline run 폴백) 을 Rust 에서 Python 으로 이전. IR 진화 시 maturin rebuild 회피 + Python-only 기여자 진입장벽 제거. 외부 API (Document.to_ir(), to_ir_json(), iter_blocks 등) 모두 동일, 캐시 identity (doc.to_ir() is doc.to_ir()) 유지.
src/ir.rs527 → 254 줄. raw 평탄화 + UTF-16↔codepoint 변환만 담당.#[derive(IntoPyObject)]struct 5개 (RawDocument/RawParagraph/RawTable/RawCell/RawCharRun) 로 PyDict 자동 생성.python/rhwp/ir/_mapper.py신규 — raw dict →HwpDocument합성. Rust 에 있던escape_html/cell_role/table_to_html/build_inline_runs폴백 로직 전부 이전.python/rhwp/ir/_raw_types.py신규 — Rust struct 미러TypedDict5개. nested 구조에서BaseModel대비 약 2.5× 빠른 internal raw record (공식 벤치마크 기준).tests/test_ir_mapper.py신규 — Rust 에서 사라진#[cfg(test)]단위 테스트 (escape 순서, cell role 3갈래, inline run 폴백 정책) 의 Python 측 보존.tests/test_from_bytes.py신규 — bytes 기반 생성 검증.tests/test_async.py신규 —aparse+aiofiles경로 검증 + 미설치 시ImportError검증.
Documentation
CLAUDE.mdasync direction 섹션 갱신 —asyncio.to_thread(rhwp.parse, path)가unsendable제약 상 panic 함을 실험으로 확인. forbidden vs supported async 패턴,aparse+aiofiles권장 경로, 향후 upstreamRefCell변경 시 재검토 가능성 안내.
Deferred to v0.3.0+
PictureBlock/FormulaBlock/FootnoteBlock/ListItemBlock/CaptionBlock/TocEntryBlock/FieldBlock— 현재는 미지kind→UnknownBlock폴백.- Furniture 본문 파싱 (머리글/꼬리말/각주 내용).
DocumentMetadata.creation_time/modification_time을datetime으로 교체 (현재str | None).- text/table 정확 interleaving (컨트롤 문자 0x0B 위치 기반).
- LLM strict-mode 완전 호환 —
export_schema(strict=True)옵션. - SchemaStore 카탈로그 등록 / content-addressed alias — GA 후 별도 PR.
v0.1.1
[0.1.1] — 2026-04-23
Patch release: fixes the sdist packaging so the source distribution stays within PyPI's 100 MB file size limit.
Fixed
maturin sdistnow excludesexternal/rhwp/samples/(≈60 MB of test fixture HWP/HWPX files). The v0.1.0 sdist exceeded PyPI's 100 MB limit and was rejected by PyPI; wheels were unaffected and therhwp-python 0.1.0wheels on PyPI remain functional.
Changed
[tool.maturin] excludeinpyproject.tomladds**/samples/**for the sdist format.
v0.1.0
[0.1.0] — 2026-04-22
Initial PyO3 Python bindings for the rhwp Rust HWP/HWPX parser and renderer.
Phase 1 milestone (upstream issue edwardkim/rhwp#227).
Distributed as rhwp-python on PyPI; import rhwp for usage.
The rhwp Rust core is consumed via git submodule pinned to upstream commit 1636213 (edwardkim/rhwp main as of 2026-04-22).
Added
- Core bindings:
rhwp.version()— this Python package version.rhwp.rhwp_core_version()— underlying Rust core version.rhwp.parse(path)→Document.rhwp.Document(path)— direct constructor, equivalent toparse().- Attributes:
section_count,paragraph_count,page_count. - Methods:
extract_text(),paragraphs(),render_svg(page),
render_all_svg(),export_svg(dir, prefix=None),
render_pdf() → bytes,export_pdf(path) → int,__repr__().
- GIL release (
py.detach) onparse(),render_pdf(), andexport_pdf()PDF-conversion step — parallel parse throughput up to 4.01× on 8 cores (Apple M2). - Crossplatform
abi3-py39wheels: Linux x86_64 + aarch64 (manylinux auto), macOS x86_64 + aarch64, Windows. - Optional extras
rhwp-python[langchain]:rhwp.integrations.langchain.HwpLoader(BaseLoader)withsingle/paragraphmodes.lazy_load()yieldsDocumentobjects on-the-fly for O(1) peak memory inparagraphmode.- Metadata:
source,section_count,paragraph_count,page_count,rhwp_version, plusparagraph_indexin paragraph mode.
- PEP 561 typed API (
py.typed+.pyistubs), pyright clean on valid usage, four intentional-error samples verified. - pytest suite: 48 core + 29 LangChain = 77 tests.
- Error mapping preserves Python exception hierarchy:
FileNotFoundError(NotFound),PermissionError(PermissionDenied),OSError(other I/O),ValueError(invalid format).
Security
- No known CVEs.
- Built with Rust 1.83+ (PyO3 0.28 MSRV). Bindings layer adds no
unsafecode.
Known limitations
Documentis#[pyclass(unsendable)]— cross-thread use raisesRuntimeError. Runparse + consumeinside worker threads.- No font embedding / debug overlay / page metadata APIs (Phase 2+).
- No HWP/HWPX serialization (save) — read/render only.
- No structured access to tables / images / formulas — text extraction only.
Distribution
- Local
maturin build --releasewheel (3.0 MB) andmaturin sdist(1.3 MB, submodule-based vendoring) both verified end-to-end in a clean venv: install → import →rhwp.parse→HwpLoaderload. - GitHub Actions workflow (
publish.yml) builds Linux (x86_64 + aarch64) / macOS (x86_64 + aarch64) / Windows wheels + sdist on release publish, then uploads via PyPI Trusted Publisher (OIDC).