Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ jobs:
tests/test_view_markdown.py tests/test_view_html.py \
tests/test_view_baseline.py \
tests/test_cli.py \
tests/test_mcp_server.py \
tests/conftest.py tests/type_check_samples.py
- name: Run pyright (intentional errors — expect 4)
if: matrix.lint
Expand Down Expand Up @@ -235,13 +236,14 @@ jobs:
- run: uv pip install dist/*.whl
- name: Run pytest — extras-gated tests must auto-skip via importorskip
# ^ 파일-레벨 importorskip 은 해당 파일 전체를 skip 1개로 카운트.
# v0.3.0 기준 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py
# (langchain-core), test_ir_schema_export.py (jsonschema), test_cli.py (typer)
# → 총 4 파일. test_async.py 는 v0.3.0 부터 stdlib 만 사용 (aiofiles 의존성 제거)
# v0.5.0 S1 기준 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py
# (langchain-core), test_ir_schema_export.py (jsonschema), test_cli.py (typer),
# test_mcp_server.py (fastmcp) → 총 5 파일. test_async.py 는 v0.3.0 부터
# stdlib 만 사용 (aiofiles 의존성 제거).
run: |
uv run pytest tests/ -m "not slow" -v | tee pytest-output.txt
if ! grep -qE '(^|[^0-9])4 skipped([^0-9]|$)' pytest-output.txt; then
echo "::error::expected 4 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, typer)"
if ! grep -qE '(^|[^0-9])5 skipped([^0-9]|$)' pytest-output.txt; then
echo "::error::expected 5 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, typer, fastmcp)"
exit 1
fi

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ mutants/

# * Examples 산출물
render_output/

.mcp.json
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Project-specific instructions. Inherits all rules from `~/.claude/CLAUDE.md` (gl
- `abi3-py310` feature: **one wheel covers 3.10–3.13+**. Don't bind to Python version-specific C API

### Async direction
- Python-surface APIs for I/O and integrations are **async-first**: when adding LangChain / LlamaIndex / Haystack loaders, implement `aload` / `alazy_load` / async counterparts alongside sync versions
- Python-surface APIs for I/O and integrations are **async-first**: when adding LangChain (or future RAG framework) loaders, implement `aload` / `alazy_load` / async counterparts alongside sync versions
- **Forbidden pattern**: `asyncio.to_thread(rhwp.parse, path)` — `_Document` is unsendable (see Rust+Python hybrid build note above), the returned Document panics on main-thread access. `async fn` in `#[pymethods]` is also incompatible (PyO3 requires `Send + 'static` futures)
- **Supported async pattern**: `aparse(path)` uses stdlib `asyncio.to_thread` to offload the file read to a thread pool, then calls `Document.from_bytes(data)` on the event-loop thread. Document never crosses a thread boundary. No external dependency — Python `asyncio` lacks native async file I/O so all async file libs (aiofiles etc.) wrap thread pools anyway; stdlib achieves the same effect with zero install footprint
- **Document instance-level async methods (`doc.ato_ir()` etc.) are NOT provided** — they would require thread offload which unsendable forbids. For async code, `await rhwp.aparse(path)` once, then call sync methods on the Document directly (these are fast, in-memory, GIL-holding operations)
Expand All @@ -33,7 +33,7 @@ Project-specific instructions. Inherits all rules from `~/.claude/CLAUDE.md` (gl
- Real HWP fixtures live in the submodule: `external/rhwp/samples/aift.hwp` (HWP5), `table-vpos-01.hwpx` (HWPX). `tests/conftest.py` + `benches/bench_gil.py` reference this path
- When changing one path, change both
- Markers: `slow` (PDF render), `langchain` (extras required). Default run: `pytest -m "not slow"`
- Extras-gated test files use module-level `pytest.importorskip` so the whole file counts as **1 skip** when the extra is missing. Current gated files: `test_langchain_loader.py` + `test_langchain_loader_ir.py` (langchain-core), `test_ir_schema_export.py` (jsonschema), `test_cli.py` (typer) → CI's `test-without-extras` job validates **exactly 4 skipped** (see `.github/workflows/ci.yml`). When adding a new extras-gated file, bump the count in both AGENTS.md and ci.yml
- Extras-gated test files use module-level `pytest.importorskip` so the whole file counts as **1 skip** when the extra is missing. Current gated files: `test_langchain_loader.py` + `test_langchain_loader_ir.py` (langchain-core), `test_ir_schema_export.py` (jsonschema), `test_cli.py` (typer), `test_mcp_server.py` (fastmcp) → CI's `test-without-extras` job validates **exactly 5 skipped** (see `.github/workflows/ci.yml`). When adding a new extras-gated file, bump the count in both AGENTS.md and ci.yml
- `tests/type_check_errors.py` holds **exactly 4 intentional pyright errors** — CI validates that too. When editing, preserve count; don't fix them

### Git workflow
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 부수 — `test_submodule_pin_matches_changelog_record` 제거 ([tests/test_ir_marker_char_offset.py](tests/test_ir_marker_char_offset.py)). 본래 v0.3.1 의 *deliberate* pin bump (v0.7.7 → 0fb3e67) 가 release-readiness 시점 기재됐는지 가드한 일회성 AC-13 의 일부였으나, GA shipped 후에도 영구 runtime 가드로 잔존해 일상 sync 마다 CHANGELOG 갱신을 강제하는 anti-pattern (1회성 release-gating AC 의 영구 테스트화 + 문서 텍스트 매칭 runtime test) 화. CHANGELOG 갱신은 릴리즈 시점 의무 (`publish.yml::verify-version` 로 version 일치 가드, 사람 review 가 핀 bump 정합성 점검) 로 이양. AC-13 의 historical record 검증은 동 파일 `test_changelog_records_pin_bump` (Frozen v0.3.1 섹션 텍스트 회귀 가드) 가 그대로 유지.
- 부수 — 동 파일 이름 `test_v0_3_1_marker_char_offset.py` → `test_ir_marker_char_offset.py` (`test_ir_*` 패턴 통일, 본 spec lifecycle 은 `pytest.mark.spec("v0.3.1/...")` marker 가 보유).

## [0.5.0] — 2026-05-06

MINOR release. [Model Context Protocol](https://modelcontextprotocol.io/) (Anthropic, 2024) 서버를 새 entry point `rhwp-mcp` 로 노출한다. LLM 에이전트 (Claude Desktop / Cursor / Cline / Continue.dev / Goose / 자체 에이전트) 가 HWP/HWPX 를 직접 파싱·요약·청크화 가능. standalone [`fastmcp`](https://github.com/jlowin/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` / `--port` CLI. 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` 미설치 시 fastmcp `ToolError` 로 wrap → MCP `CallToolResult(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` — fastmcp `Client(server)` in-process round-trip 데모 (typer 기반, `--skip-chunks` 옵션). 7 도구 차례로 호출하며 출력 형식 학습용.
- README § "MCP server (`rhwp-mcp`)" 섹션 신설 — 도구 7 종 표 / Claude Desktop `claude_desktop_config.json` 등록 예 / 클라이언트 호환성 표 (Claude Desktop / Cline / Cursor / Continue.dev / Goose / 자체 에이전트, transport 별 ✅/❌/⚠️) / streamable-http 사용 예.
- spec / ADR / 구현 로그: [docs/roadmap/v0.5.0/mcp.md](docs/roadmap/v0.5.0/mcp.md) (Frozen, 11 인수조건) / [docs/design/v0.5.0/mcp-research.md](docs/design/v0.5.0/mcp-research.md) (Frozen, 4 결정 매트릭스 — SDK 채택 근거 / transport 우선순위 / handler 동시성 / 도구 분할) / [docs/implementation/v0.5.0/stages/](docs/implementation/v0.5.0/stages/) (S1 ~ S5 Frozen).

### Changed

- ADR § 1 SDK 결정 갱신 (S1 진행 중) — 공식 `mcp` Python SDK (FastMCP v1 흡수) → standalone `fastmcp` v3 (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](docs/CONVENTIONS.md) 에 예외 명시.
- CI `test-without-extras` job — expected skip count 4 → 5 (`tests/test_mcp_server.py` 의 file-level `pytest.importorskip("fastmcp")` 추가). `.github/workflows/ci.yml` + `AGENTS.md` § Tests 동시 갱신 (AC-11).

### Build

- `external/rhwp` submodule pin `0fb3e67` 유지 — 본 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](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](docs/implementation/v0.5.0/stages/stage-5.md) 참조).
- 미확정 이슈는 v0.5.0 GA 후 demand-driven: `get_ir` 응답 크기 / 에러 응답 형식 통일 / Resource·Prompt 추상 / 출력 schema 강타입화 (`ChunkRecord` 등). spec [§ 미확정 이슈](docs/roadmap/v0.5.0/mcp.md) 에 기록.

## [0.4.0] — 2026-05-05

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).
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rhwp-python"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
# ^ rust-version 미명시 — 상위 rhwp crate 정책(stable Rust, MSRV unclaimed) 준수.
# PyO3 0.28 이 Rust 1.83+ 요구하지만, 이는 README 에 문서로 안내
Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,71 @@ rhwp-py chunks report.hwp --size 1000 --format ndjson
메타데이터 덤프는 상류 `rhwp` Rust 바이너리. 자세한 사용은 `rhwp-py --help`
또는 [cli.md](docs/roadmap/v0.3.0/cli.md) 참조.

## MCP server (`rhwp-mcp`)

[Model Context Protocol](https://modelcontextprotocol.io/) 서버 — Claude Desktop /
Cursor / Cline / Continue.dev / Goose 등 LLM 에이전트가 HWP/HWPX 파일을 직접
파싱·요약·청크화할 수 있다. standalone [fastmcp v3](https://github.com/jlowin/fastmcp)
기반 (2026-05 기준 MCP 서버 약 70% 시장 점유의 사실상 표준).

```bash
pip install "rhwp-python[mcp]" # 도구 6 종 (parse / extract / IR / blocks / view×2)
pip install "rhwp-python[mcp-chunks]" # + chunks (RAG 청킹 — langchain-text-splitters)
```

### 노출 도구 (7 종)

| 도구 | 입력 | 출력 |
|---|---|---|
| `parse_hwp_summary` | `path` | sections / paragraphs / pages 카운트 + rhwp-core 버전 |
| `extract_text` | `path` | 단락별 평문 (LF 결합) |
| `get_ir` | `path` | Document IR 전체 (JSON-serializable dict) |
| `iter_blocks` | `path`, `kind?`, `scope`, `limit?` | IR 블록 dict 리스트 (kind / scope 필터링) |
| `to_markdown` | `path` | GFM Markdown — v0.4.0 view API thin wrapper |
| `to_html` | `path`, `include_css` | HTML5 문서 — v0.4.0 view API thin wrapper |
| `chunks` | `path`, `mode`, `size`, `overlap`, `include_furniture` | LangChain `RecursiveCharacterTextSplitter` 적용 청크 — `[mcp-chunks]` extras 필요 |

### Claude Desktop 등록

`claude_desktop_config.json` 에 추가:

```json
{
"mcpServers": {
"rhwp": {
"command": "rhwp-mcp"
}
}
}
```

(macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`. Windows:
`%APPDATA%\Claude\claude_desktop_config.json`.) Claude Desktop 재시작 후 도구
아이콘에 7 개 도구 노출.

### 다른 클라이언트

| 클라이언트 | stdio | streamable-http | 등록 방법 |
|---|---|---|---|
| Claude Desktop | ✅ | ❌ | `claude_desktop_config.json` (위 예시) |
| Cline (VSCode) | ✅ | ✅ | VSCode 설정 → MCP servers |
| Cursor | ✅ | ❌ | Settings → Features → Model Context Protocol |
| Continue.dev | ✅ | ⚠️ (실험) | `~/.continue/config.json` |
| Goose (Block) | ✅ | ✅ | `goose configure` |
| 자체 에이전트 | ✅ | ✅ | Anthropic SDK 의 MCP client / fastmcp Client |

### Streamable HTTP (서버 배포)

서버 컨테이너 / 다중 클라이언트 시나리오는 streamable-http transport:

```bash
rhwp-mcp --transport streamable-http --port 8000
# 외부 노출 (보안: reverse proxy + 인증 운영자 책임)
rhwp-mcp --transport streamable-http --host 0.0.0.0 --port 8000
```

기본 `--host 127.0.0.1` — 외부 노출 회피. `rhwp-mcp` 는 인증 / TLS / sandboxing 미내장 — Caddy / Nginx 등 reverse proxy 가 책임. 자세한 사용은 `rhwp-mcp --help` 또는 [mcp.md](docs/roadmap/v0.5.0/mcp.md) 참조.

## 성능

Apple M2 (8 코어) release 빌드. Parse = 파일 읽기 + 전체 파싱 + Document 생성.
Expand Down
8 changes: 4 additions & 4 deletions docs/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
|---|---|---|---|
| **Living** | 항상 최신 — 다른 문서의 위치 포인터 + 시간선 + 규칙 | 자유 갱신, 매 변경 시 손봐도 무방 | `docs/CONVENTIONS.md` (자체), `docs/roadmap/README.md`, `docs/upstream/README.md`, `docs/traces/coverage.md`, `CHANGELOG.md`, `CLAUDE.md`, `AGENTS.md`, `README.md` |
| **Active** | 외부 시스템으로 흘러가기 전 staging | 큰 변경만, in-place 갱신 OK | `docs/upstream/<topic>.md` |
| **Draft** | 작성 중인 spec — 해당 버전 GA 전까지 활발 갱신 | 버전 GA 전까지 자유 갱신, GA 후 Frozen 으로 전환 | `docs/roadmap/v0.7.0/mcp.md` (현재 v0.7.0 GA ) |
| **Draft** | 작성 중인 spec — 해당 버전 GA 전까지 활발 갱신 | 버전 GA 전까지 자유 갱신, GA 후 Frozen 으로 전환 | `docs/roadmap/v<X.Y.Z>/<topic>.md` (target 미-GA 버전) |
| **Frozen** | GA 완료된 spec / 완료된 stage / 완료된 검증 | **변경 금지** — 오타·링크 수정만 in-place 허용. 큰 변경은 새 spec + supersede | `docs/roadmap/v0.2.0/ir.md` (v0.2.0 GA 완료), `docs/implementation/v0.2.0/stages/*.md` |

`Frozen` 은 [Rust RFC](https://rust-lang.github.io/rfcs/) / [Python PEP](https://peps.python.org/) 의 운영 모델. 결정의 historical record 가 보존되어 "왜 그렇게 설계됐는지" 가 명확해진다.
Expand Down Expand Up @@ -52,8 +52,8 @@ last_updated: 2026-04-28
|---|---|---|
| `status` | enum: `Active` / `Draft` / `Frozen` / `Superseded` | 필수 |
| `description` | non-empty string (50-150 자 권장) | 필수. 한 줄 요약 — 인덱스/검색/툴팁용 (MkDocs / Hugo / Astro 패턴) |
| `ga` | `vX.Y.Z` SemVer | `status: Frozen` 또는 `Superseded` 일 때 필수 (예외: meta-level `docs/implementation/<topic>.md`, RESOLVED `docs/upstream/<topic>.md` — § Implementation log 구조 / § upstream/ 참조). `target` 과 mutex |
| `target` | `vX.Y.Z` SemVer | `status: Draft` 일 때 필수. `ga` 와 mutex |
| `ga` | `vX.Y.Z` SemVer | `status: Frozen` 또는 `Superseded` 일 때 필수 (예외: meta-level `docs/implementation/<topic>.md`, RESOLVED `docs/upstream/<topic>.md`, **pre-GA stage log** — § Implementation log 구조 / § upstream/ 참조). `target` 과 mutex (단, pre-GA stage 예외) |
| `target` | `vX.Y.Z` SemVer | `status: Draft` 일 때 필수. `status: Frozen` + `target` 조합은 pre-GA stage log 에 한해 허용 (§ Implementation log 구조). `ga` 와 mutex |
| `supersedes` | `<vX.Y.Z>/<topic>.md` 또는 생략 | 새 spec 이 무엇을 대체하는지 |
| `superseded_by` | `<vX.Y.Z>/<topic>.md` | `status: Superseded` 일 때 필수 |
| `last_updated` | `YYYY-MM-DD` | 필수. 의미 변경 commit 시 자동 갱신 ([D3 hook](#last_updated-자동-갱신)) |
Expand Down Expand Up @@ -248,7 +248,7 @@ CHANGELOG 한 줄로 충분한 변경 (typo 정리, 단순 dep bump, 작은 docs

spec-system-overhaul (2026-04-29) 이후 신규 작성 spec 의 § 인수조건 섹션은 각 항목에 `AC-N` ID 를 부여한다 (테스트 marker 와 1:1 매핑용). 형식은 자유 — testable 하고 명확하면 plain prose 도 OK. 모호성이 우려되면 [EARS notation](https://alistairmavin.com/ears/) (`THE ... SHALL`, `WHEN ..., THE ... SHALL` 등) 같은 구조화 패턴을 참고 가능 (강제 아님).

**적용 시점**: overhaul 발효일 (2026-04-29) 이후 신규 작성 spec. 첫 적용 사례는 [v0.3.1/ir-marker-char-offset](roadmap/v0.3.1/ir-marker-char-offset.md) (PATCH minor 라도 발효 이후 신규면 적용 — cutoff 는 버전이 아닌 *시점*). 발효일 *이전* 작성된 Draft 도 본 PR 에서 일괄 retrofit ([v0.7.0/mcp.md](roadmap/v0.7.0/mcp.md)). 향후 grandfather 가 발생하면 다음 의미 변경 PR 시점에 함께 retrofit.
**적용 시점**: overhaul 발효일 (2026-04-29) 이후 신규 작성 spec. 첫 적용 사례는 [v0.3.1/ir-marker-char-offset](roadmap/v0.3.1/ir-marker-char-offset.md) (PATCH minor 라도 발효 이후 신규면 적용 — cutoff 는 버전이 아닌 *시점*). 발효일 *이전* 작성된 Draft 도 본 PR 에서 일괄 retrofit. 향후 grandfather 가 발생하면 다음 의미 변경 PR 시점에 함께 retrofit.

```markdown
## 인수조건
Expand Down
Loading
Loading