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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 본 PR 의 a/b/c 결정 비교 + 14개 결정 historical record 는 [docs/implementation/spec-system-overhaul.md](docs/implementation/spec-system-overhaul.md) (Frozen) 가 보유.
- spec body 구조 SSOT 정착 — `/new-spec` skill 안 `templates/spec.md` + `templates/adr.md` 신설 (skeleton + 섹션별 룰 보유, body 구조 SSOT). `docs/CONVENTIONS.md` 에 § Spec / ADR 본문 구조 (짧은 pointer) + § 섹션 역할 분리 — 정보 배치 룩업 (spec ↔ ADR ↔ CHANGELOG ↔ implementation log 의 cross-cutting 표) 신설. SKILL.md step 2/4/5 가 두 template 파일을 markdown 링크로 자연 참조 (공식 Claude Code skill 패턴 — 명령형). multi-template `templates/` sub-dir 은 Anthropic 공식 docs 의 `examples/` / `scripts/` 카테고리 sub-dir 패턴 + GitHub ISSUE_TEMPLATE 선례 follow.

## [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](https://github.com/edwardkim/rhwp/pull/494) / [Task #484](https://github.com/edwardkim/rhwp/issues/484), v0.7.9 GA) 로 치환해 SSOT 를 단일화한다. 알고리즘 동등 — IR 출력 byte-equal, 공개 API 변경 없음, SchemaVersion `"1.1"` 유지.

### Build

- `src/ir.rs::utf16_to_cp` 자체 복사본 + `u32::MAX` short-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/rhwp` submodule pin `0fb3e67` 유지 — 핀 history 에 PR #494 머지 commit `60eaa91` (2026-04-30) 포함, `cargo build` 가 시그니처 해소로 직접 검증. v0.7.9 GA 흡수는 직교 영역, 본 PATCH 영구 비목표.
- 부수 정리: 본 binding 이 제출한 issue 초안 `docs/upstream/issue-utf16-pos-to-char-idx.md` in-place Frozen 전환 + `docs/upstream/README.md` 인덱스 RESOLVED 컬럼 채움.

## [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](https://github.com/edwardkim/rhwp/pull/405) / [Task #390](https://github.com/edwardkim/rhwp/issues/390)) 노출을 활용해 7 종 블록 (각주·미주 마커, 그림, 수식, 필드, TOC, 표) 의 zero-width character 위치를 채운다. SchemaVersion 변경 없음 (`"1.1"` 유지) — 기존에 nullable 슬롯에 정의된 `int | None` 에 non-null 값을 출고할 뿐, schema 호환 100%.
Expand Down Expand Up @@ -245,7 +255,8 @@ The `rhwp` Rust core is consumed via git submodule pinned to upstream commit `16
- Local `maturin build --release` wheel (3.0 MB) verified end-to-end in a clean venv: install → import → `rhwp.parse` → `HwpLoader` load. (Note: the v0.1.0 sdist exceeded PyPI's 100 MB limit and did not upload; fixed in [0.1.1](#011--2026-04-23).)
- 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).

[Unreleased]: https://github.com/DanMeon/rhwp-python/compare/v0.3.1...HEAD
[Unreleased]: https://github.com/DanMeon/rhwp-python/compare/v0.3.2...HEAD
[0.3.2]: https://github.com/DanMeon/rhwp-python/releases/tag/v0.3.2
[0.3.1]: https://github.com/DanMeon/rhwp-python/releases/tag/v0.3.1
[0.3.0]: https://github.com/DanMeon/rhwp-python/releases/tag/v0.3.0
[0.2.0]: https://github.com/DanMeon/rhwp-python/releases/tag/v0.2.0
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.3.1"
version = "0.3.2"
edition = "2021"
# ^ rust-version 미명시 — 상위 rhwp crate 정책(stable Rust, MSRV unclaimed) 준수.
# PyO3 0.28 이 Rust 1.83+ 요구하지만, 이는 README 에 문서로 안내
Expand Down
139 changes: 139 additions & 0 deletions docs/design/v0.3.2/ir-upstream-utf16-helper-research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
status: Draft
description: "v0.3.2 ir-upstream-utf16-helper ADR — API source / 'u32::MAX' sentinel / 'fallback_end' 인자 / 단위 테스트 처리 4 결정의 근거"
target: v0.3.2
last_updated: 2026-05-03
---

# v0.3.2 ir-upstream-utf16-helper — 설계 의사결정 리서치 요약

[v0.3.2/ir-upstream-utf16-helper.md](../../roadmap/v0.3.2/ir-upstream-utf16-helper.md) §결정 사항 중 외부 독자가 "왜?" 를 던질 만한 4건의 업계 선례·대안·실패 시나리오를 기록한다. spec 본문이 최종 결정을 기술하고, 본 문서는 그 결정의 근거를 담는다.

## 결정 매트릭스

| # | 항목 | 옵션 비교 | 채택 | 1차 근거 |
|---|---|---|---|---|
| 1 | API source | A: 자체 복사본 (`utf16_to_cp`) 보존 / B: 상류 `Paragraph::utf16_pos_to_char_idx()` (PR #494) / C: `helpers::utf16_pos_to_char_idx` (옵션 B 노출 가정) | **B** | 자체 복사본은 본 binding 운영 정책 ("상류 신뢰 + 결함 시 PR") 위반. 상류는 옵션 A 채택 — 옵션 C 는 머지 안 됨 |
| 2 | `u32::MAX` sentinel 처리 | A: 호출부 short-circuit 보존 / B: 호출부 분기 제거, 자연 처리 위임 | **B** | `iter().position(\|&off\| off >= u32::MAX)` 가 항상 `None` → `unwrap_or(char_offsets.len())` 로 자체 short-circuit 결과와 비트 단위 동일. SSOT 분산 회피 |
| 3 | `fallback_end` 인자 제거 | A: invariant 신뢰하고 인자 제거 / B: 방어적 코딩으로 인자 유지 | **A** | `paragraph.rs:22-24` doc-comment 가 `char_offsets.len() == text.chars().count()` 를 정의로 보장. 상류 contract 의 별도 매개변수 운반은 SSOT 분산 |
| 4 | 자체 단위 테스트 처리 | A: 함수 + 테스트 동시 삭제 / B: `Paragraph::utf16_pos_to_char_idx` 호출 wrapper 테스트 보존 / C: `build_char_runs` 통합 테스트 강화 | **A** | 자체 함수가 사라지면 본체 단위 테스트는 *없는 함수* 검증. 상류 자체 테스트 6건 + fixture 회귀가 이미 끝-단 가드 |

## 1. API source — 자체 복사본 vs 상류 `pub` 메서드

### 팩트

- 자체 복사본 위치: `src/ir.rs:421-436` — `fn utf16_to_cp(char_offsets: &[u32], utf16: u32, fallback_end: usize) -> usize`
- 알고리즘 본체: `iter().position(|&off| off >= utf16).unwrap_or(fallback_end)` (1줄) + `u32::MAX` short-circuit
- 상류 v0.7.7 까지: `pub(crate) fn utf16_pos_to_char_idx(char_offsets: &[u32], utf16_pos: u32) -> usize` (`helpers.rs:189-192`) — 외부 crate 접근 불가
- 본 binding 이 [docs/upstream/issue-utf16-pos-to-char-idx.md](../../upstream/issue-utf16-pos-to-char-idx.md) 로 옵션 A (Paragraph 인스턴스 메서드) / 옵션 B (helpers `pub`) 두 안 제시
- 상류는 옵션 A 채택, [Task #484](https://github.com/edwardkim/rhwp/issues/484) / [PR #494](https://github.com/edwardkim/rhwp/pull/494) 머지 (cherry-pick @DanMeon 3 commits), v0.7.9 GA
- 현재 상류 메서드: `external/rhwp/src/model/paragraph.rs:818-823` — `pub fn utf16_pos_to_char_idx(&self, utf16_pos: u32) -> usize`
- 선례: v0.3.1 가 동일 결로 [PR #405 (Task #390)](https://github.com/edwardkim/rhwp/pull/405) 의 `Paragraph::control_text_positions` 채택

### 검증자 반박

- "알고리즘이 1줄인데 SSOT 단일화의 가치가 있나? 상류 docstring 도 'silent drift 위험은 무시 가능' 으로 평가" → 상류 평가는 *상류 자체 안에서의* 동기화 비용. 본 binding 은 *외부 호출자* 입장 — `char_offsets` 의 의미축이 미묘하게 변하면 상류 자체 호출자는 컴파일 / 테스트로 즉시 드러나지만 외부 binding 은 *런타임 결과 어긋남* 으로만 드러남. silent drift 비용 비대칭이 본질
- "v0.3.1 PR #405 (`control_text_positions`) 는 알고리즘이 복잡해서 상류 활용이 자연이지만, 본 case 는 1줄이라 다르지 않나?" → 알고리즘 복잡도와 무관하게 *SSOT 단일화의 정책적 가치* 가 결정 축. cutoff 가 모호해지면 정책 자체가 약화

### 최종 결정

**옵션 B — 상류 `Paragraph::utf16_pos_to_char_idx` 사용**. v0.3.1 결정 1 과 같은 결 — 본 binding 의 "상류 신뢰 + 결함 시 PR" 정책 일관 적용.

### 1차 소스

- 상류 PR: <https://github.com/edwardkim/rhwp/pull/494>
- 상류 Task: <https://github.com/edwardkim/rhwp/issues/484>
- 상류 메서드: `external/rhwp/src/model/paragraph.rs:818-823`
- 본 binding 이 제출한 issue 초안: [docs/upstream/issue-utf16-pos-to-char-idx.md](../../upstream/issue-utf16-pos-to-char-idx.md)

## 2. `u32::MAX` sentinel 처리 — short-circuit 보존 vs 자연 처리

### 팩트

- 자체 함수의 sentinel 분기 (`src/ir.rs:427-429`):

```rust
if utf16 == u32::MAX {
return fallback_end;
}
```
- 호출 패턴: 마지막 char_shape 의 `end_utf16` 를 `u32::MAX` 로 두는 sentinel (`src/ir.rs:390-394`) — "이 shape 는 paragraph 끝까지 적용" 의미
- 상류 메서드 본체:

```rust
self.char_offsets.iter().position(|&off| off >= utf16_pos).unwrap_or(self.char_offsets.len())
```
- `utf16_pos = u32::MAX` 입력 시 동작: `char_offsets` 의 모든 entry 는 정상 코드 유닛 인덱스 (실제 텍스트 길이 한도 내) → 어떤 entry 도 `u32::MAX` 이상 불가 → `iter().position` 결과 항상 `None` → `unwrap_or(char_offsets.len())` 도달 → `char_offsets.len()` 반환
- `char_offsets.len() == text.chars().count() == total_cp == fallback_end` (호출부 invariant) — 비트 단위 동일

### 검증자 반박

- "iter().position 이 모든 entry traverse 하는데 short-circuit 이 효율 우위 아닌가?" → paragraph 당 char_shapes 마지막 1회. char_offsets.len() 보통 < 1000 → O(n) traverse 1회는 마이크로초 단위. PDF 렌더 / SVG 직렬화 / Rust→Python 타입 변환의 millisecond 비용에 비해 무시 가능
- "방어적 코딩으로 short-circuit 유지하는 게 안전하지 않나?" → 코드 두 군데에 동일 의미 분산 보유. 어느 한 쪽이 변경되면 다른 쪽이 silent stale — 방어적 코딩이 도리어 *동기화 부담*. 단일 경로가 SSOT 부합

### 최종 결정

**옵션 B — 호출부 분기 제거, 자연 처리 위임**. 두 경로 비트 단위 동일 + 효율 차이 무시 가능 + SSOT 원칙. 상류 메서드의 docstring 이 "모든 entry 가 작으면 `char_offsets.len()`" 를 명시 — 자연 처리가 *문서화된 contract*.

### 1차 소스

- 자체 sentinel 호출 패턴: `src/ir.rs:390-397` (`build_char_runs`)
- 상류 메서드 docstring: `external/rhwp/src/model/paragraph.rs:803-823`
- `Vec<u32>::iter().position` semantics: <https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.position>

## 3. `fallback_end` 인자 제거 — invariant 신뢰 vs 방어적 명시

### 팩트

- 자체 함수의 fallback 인자 (`src/ir.rs:426`): `fallback_end: usize`
- 호출부 (`src/ir.rs:396-397`): `utf16_to_cp(&para.char_offsets, start_utf16, total_cp)` — `total_cp = para.text.chars().count()` 명시 전달
- 상류 메서드는 인자 없음 — 항상 `self.char_offsets.len()` 반환
- 상류 contract (`paragraph.rs:22-24` doc-comment): `char_offsets[i] = text[i] 에 해당하는 원본 UTF-16 코드 유닛 인덱스` → `char_offsets.len() == text.chars().count()` (i ↔ text codepoint 1:1)
- v0.3.1 [AC-12] 는 다른 종류의 contract (`controls.len() == positions.len()`) 에 `assert_eq!` 가드 강제

### 검증자 반박

- "v0.3.1 [AC-12] 와 정책 비대칭이다 — 둘 다 상류 contract 인데 왜 다른 정책?" → contract 종류 차이. v0.3.1 contract 는 *두 별개 컬렉션* 길이 일치 (paragraph.rs:734/765/786/796 *4 분기* 각자 보장 — 한 분기에서 push 빠지면 silent drift, 상류 자체 CI 가 4 분기 각각을 검증하는지 불확실). v0.3.2 contract 는 *한 paragraph 의 내부 정의 일관성* (`char_offsets[i] = text[i]` 의 1:1 정의) — 깨지면 cursor_nav / clipboard / 렌더 *전부 동시 깨짐* → 상류 prod 가 매일 호출 (cursor_nav 3회, clipboard 1회, 렌더 다수) 하는 invariant 라 drift 시 상류 CI 즉시 잡음. 검증 비용 비대칭이 정책 비대칭 정당화
- "그래도 invariant 명시 보유가 self-documenting 아닌가?" → 본 결정 항목 3 + 본 §3 자체 + paragraph.rs:22-24 doc-comment 가 self-documenting. 코드 인자로 contract 운반은 인자 의미를 곁가지로 만들어 *모호함* 추가

### 최종 결정

**옵션 A — invariant 신뢰하고 인자 제거**. `paragraph.rs:22-24` 가 SSOT 인 contract 를 본 binding 이 별도 매개변수로 이중 보유는 SSOT 분산. 호출부 단순화 (`para.utf16_pos_to_char_idx(start_utf16)` — 단일 인자) 가 가독성 우위.

### 1차 소스

- 상류 `char_offsets` 의미 정의: `external/rhwp/src/model/paragraph.rs:22-24`
- 자체 `total_cp` 정의: `src/ir.rs:381` (`para.text.chars().count()`)
- 상류 메서드 시그니처: `external/rhwp/src/model/paragraph.rs:818`

## 4. 자체 단위 테스트 처리 — 삭제 vs wrapper 보존 vs 통합 강화

### 팩트

- 자체 단위 테스트 위치 (`src/ir.rs:876-890`):
- `utf16_to_cp_sentinel_returns_fallback` — `u32::MAX` 입력 시 fallback_end 반환
- `utf16_to_cp_matches_first_ge` — SMP (2 코드 유닛) 혼용 paragraph first-greater-or-equal
- 상류 자체 단위 테스트: Task #484 Stage 2 에서 6 건 추가 (PR #494 의 일부)
- 본 binding 의 통합 테스트:
- `tests/test_ir_mapper.py` — `_build_inline_runs` 폴백 정책
- `tests/test_ir_*.py` (전체) — `Document.to_ir()` 출력 회귀 가드 (real HWP fixture 기반)

### 검증자 반박

- "옵션 A 면 회귀 가드 약해지는 것 아닌가?" → fixture 회귀 + 폴백 테스트가 끝-단 가드. 자체 단위 테스트는 *함수 단위* 인데 함수가 사라진 이상 통합 테스트가 직접 결과 검증
- "옵션 B (wrapper 테스트) 가 self-documenting 가치 있나?" → wrapper 테스트는 *상류 메서드 호출했더니 상류 메서드 결과* 의 토톨로지. 상류 자체 6 건이 본질 검증 — 본 binding 반복은 동어반복
- "옵션 C (통합 테스트 강화) 는?" → v0.3.1 baseline 회귀가 이미 byte-equal IR 가드. 별도 강화는 같은 검증 반복

### 최종 결정

**옵션 A — 함수 + 단위 테스트 동시 삭제**. 함수가 사라지면 단위 테스트도 자연스럽게 사라짐. 회귀 가드는 fixture 회귀 + 상류 자체 단위 테스트 6 건 이중 보유.

### 1차 소스

- 자체 단위 테스트: `src/ir.rs:876-890`
- 상류 자체 단위 테스트 추가 commit: <https://github.com/edwardkim/rhwp/commit/36631fd> (`Task #484 Stage 2: 단위 테스트 6건 추가`)

## 참조

- [roadmap/v0.3.2/ir-upstream-utf16-helper.md](../../roadmap/v0.3.2/ir-upstream-utf16-helper.md) — 본 리서치의 결정 요약
- [docs/upstream/issue-utf16-pos-to-char-idx.md](../../upstream/issue-utf16-pos-to-char-idx.md) — 본 binding 이 상류에 제출한 이슈 초안 (v0.3.2 GA 시 in-place Frozen)
1 change: 1 addition & 0 deletions docs/roadmap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ rhwp-python 의 버전별 로드맵 + **활성 spec 인덱스 SSOT**. 모든 spe
| v0.3.0 (IR 확장) | Frozen | [v0.3.0/ir-expansion.md](v0.3.0/ir-expansion.md) | [design/v0.3.0/ir-expansion-research.md](../design/v0.3.0/ir-expansion-research.md) |
| v0.3.0 (CLI) | Frozen | [v0.3.0/cli.md](v0.3.0/cli.md) | [design/v0.3.0/cli-research.md](../design/v0.3.0/cli-research.md) |
| v0.3.1 (IR marker char offset) | Frozen | [v0.3.1/ir-marker-char-offset.md](v0.3.1/ir-marker-char-offset.md) | [design/v0.3.1/ir-marker-char-offset-research.md](../design/v0.3.1/ir-marker-char-offset-research.md) |
| v0.3.2 (IR upstream UTF-16 helper) | Draft | [v0.3.2/ir-upstream-utf16-helper.md](v0.3.2/ir-upstream-utf16-helper.md) | [design/v0.3.2/ir-upstream-utf16-helper-research.md](../design/v0.3.2/ir-upstream-utf16-helper-research.md) |
| v0.7.0 (MCP server) | Draft | [v0.7.0/mcp.md](v0.7.0/mcp.md) | [design/v0.7.0/mcp-research.md](../design/v0.7.0/mcp-research.md) |

## 미착수 작업 계획
Expand Down
Loading