From 00174eaced5bdce002129e48ea8c54136f0ffec0 Mon Sep 17 00:00:00 2001 From: DanMeon Date: Sun, 3 May 2026 22:11:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20v0.3.2=20spec/ADR=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=E2=80=94=20UTF-16=20=E2=86=92=20codepoint=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20SSOT=20=EB=8B=A8=EC=9D=BC=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경사항: - docs/roadmap/v0.3.2/ir-upstream-utf16-helper.md 신설 (8 결정 사항 / 8 인수조건 / 5 영구 비목표) - docs/design/v0.3.2/ir-upstream-utf16-helper-research.md 신설 (API source / sentinel / fallback_end / 단위 테스트 처리 4 결정 ADR) - docs/roadmap/README.md 활성 spec 인덱스에 v0.3.2 row 추가 (Status: Draft) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ir-upstream-utf16-helper-research.md | 139 ++++++++++++++++++ docs/roadmap/README.md | 1 + .../v0.3.2/ir-upstream-utf16-helper.md | 54 +++++++ 3 files changed, 194 insertions(+) create mode 100644 docs/design/v0.3.2/ir-upstream-utf16-helper-research.md create mode 100644 docs/roadmap/v0.3.2/ir-upstream-utf16-helper.md diff --git a/docs/design/v0.3.2/ir-upstream-utf16-helper-research.md b/docs/design/v0.3.2/ir-upstream-utf16-helper-research.md new file mode 100644 index 0000000..4191e18 --- /dev/null +++ b/docs/design/v0.3.2/ir-upstream-utf16-helper-research.md @@ -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: +- 상류 Task: +- 상류 메서드: `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::iter().position` semantics: + +## 3. `fallback_end` 인자 제거 — invariant 신뢰 vs 방어적 명시 + +### 팩트 + +- 자체 함수의 fallback 인자 (`src/ir.rs:426`): `fallback_end: usize` +- 호출부 (`src/ir.rs:396-397`): `utf16_to_cp(¶.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: (`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) diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md index ac60b25..1514b1f 100644 --- a/docs/roadmap/README.md +++ b/docs/roadmap/README.md @@ -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) | ## 미착수 작업 계획 diff --git a/docs/roadmap/v0.3.2/ir-upstream-utf16-helper.md b/docs/roadmap/v0.3.2/ir-upstream-utf16-helper.md new file mode 100644 index 0000000..cf824de --- /dev/null +++ b/docs/roadmap/v0.3.2/ir-upstream-utf16-helper.md @@ -0,0 +1,54 @@ +--- +status: Draft +description: "v0.3.2 — UTF-16 → codepoint 변환 SSOT 단일화. 상류 'Paragraph::utf16_pos_to_char_idx' (PR #494) 활용 (schema 변경 없음)" +target: v0.3.2 +last_updated: 2026-05-03 +--- + +# v0.3.2 — UTF-16 → codepoint 변환 SSOT 단일화 + +v0.2.0 IR 매핑은 UTF-16 → codepoint 변환을 자체 복사본 (`utf16_to_cp`) 으로 보유해 왔다. 동일 알고리즘이 상류 `document_core::helpers` 에 `pub(crate)` 로 갇혀 있어 외부 호출이 불가했기 때문이다. + +상류가 [PR #494 (Task #484)](https://github.com/edwardkim/rhwp/pull/494) 로 `Paragraph::utf16_pos_to_char_idx(&self, utf16_pos: u32) -> usize` 를 v0.7.9 에 `pub` 노출하면서 SSOT 단일화가 가능. 본 spec 은 자체 복사본을 제거해 상류 메서드로 치환한다 — schema 변경 없음, 공개 API 동일, IR 출력 byte-equal. 부수로 짝이 되는 issue draft archive 도 묶는다 (in-place Frozen 전환). + +주요 결정의 근거·대안·실패 시나리오는 짝 페어: [ir-upstream-utf16-helper-research.md](../../design/v0.3.2/ir-upstream-utf16-helper-research.md). + +## 결정 사항 + +| 항목 | 값 | 근거 | +|---|---|---| +| 1 — API source | 상류 `Paragraph::utf16_pos_to_char_idx` (v0.7.9 GA) | PR #494 가 본 binding 이 제출한 [docs/upstream/issue-utf16-pos-to-char-idx.md](../../upstream/issue-utf16-pos-to-char-idx.md) 옵션 A 채택. v0.3.1 의 `control_text_positions` (PR #405) 와 같은 결. 자체 복사본 보유는 본 binding 운영 정책 ("상류 신뢰 + 결함 시 PR") 위반이라 SSOT 단일화 우선. 자세한 옵션 비교는 ADR §1 | +| 2 — `u32::MAX` sentinel 분기 처리 | 호출부 분기 제거, 자연 처리에 위임 | 상류 메서드의 `iter().position` 이 `u32::MAX` 입력 시 자연 None → `unwrap_or(char_offsets.len())` 로 자체 short-circuit 결과와 비트 단위 동일. 자세한 본체 비교는 ADR §2 | +| 3 — `fallback_end` 인자 제거 | 상류 메서드는 항상 `char_offsets.len()` 반환 | `paragraph.rs:22-24` doc-comment 가 `char_offsets.len() == text.chars().count()` 를 정의 자체로 보장. v0.3.1 [AC-12] `assert_eq!` 정책과의 비대칭은 contract 종류 차이 — 정당화는 ADR §3 | +| 4 — 자체 단위 테스트 처리 | 함수 삭제와 동시에 제거 | 자체 함수가 사라지면 본체 단위 테스트는 *없는 함수* 검증. 회귀 가드는 fixture 회귀 (`tests/test_ir_*.py`) + 상류 자체 단위 테스트 6건 (Task #484 Stage 2) 이중 보유. 자세한 옵션 비교는 ADR §4 | +| 5 — 적용 범위 | `src/ir.rs::build_char_runs` 단일 호출자 | 코드베이스 grep — Python `_build_inline_runs` 는 RawCharRun 의 `start_cp/end_cp` 를 그대로 소비. 추가 변환 호출 없음 | +| 6 — 상류 핀 bump | 현재 핀 `0fb3e67` 유지 | 핀 history 에 PR #494 머지 commit `60eaa91` (2026-04-30) 포함 — `cargo build` 가 시그니처 해소로 직접 검증 (AC-1). v0.7.9 GA 흡수는 직교 영역, 본 spec 영구 비목표 | +| 7 — 외부 영향 | schema / API / IR 출력 모두 변경 없음 | 자체 복사본 = 상류 메서드 본체로 알고리즘 동일, fallback 의미축 동일. 사용자 visible 변화 0 | +| 8 — issue archive | `docs/upstream/issue-utf16-pos-to-char-idx.md` in-place Frozen 전환 | v0.3.1 GA 직전 (4/30) PR #494 머지됐으나 v0.3.1 spec 이 archive 를 명시 작업으로 안 넣어 누락. 본 spec 이 묶음 — CONVENTIONS § upstream/ 의 in-place Frozen 분기 (다른 spec 이 본 파일 참조) | + +## 인수조건 + +- **AC-1** — `cargo build --release` 가 통과한다 (상류 `Paragraph::utf16_pos_to_char_idx` 시그니처 해소로 핀 `0fb3e67` 이 메서드를 포함함을 컴파일러가 직접 검증) +- **AC-2** — fixture (`external/rhwp/samples/aift.hwp`, `external/rhwp/samples/table-vpos-01.hwpx`) 의 `Document.to_ir()` 출력 `InlineRun.start_cp` / `end_cp` 값이 v0.3.1 GA baseline 과 byte-equal +- **AC-3** — 마지막 char_shape 의 `end_utf16 = u32::MAX` 인 paragraph 에서 출고 `InlineRun.end_cp == para.text.chars().count()` (sentinel 의 자연 처리 결과) +- **AC-4** — `SchemaVersion` 은 `"1.1"` 유지, `python/rhwp/ir/schema/hwp_ir_v1.json` 본문 변경 없음 +- **AC-5** — `pytest -m "not slow"` 전체 회귀 통과 (`tests/test_ir_*.py` 포함) +- **AC-6** — `docs/upstream/issue-utf16-pos-to-char-idx.md` frontmatter `status: Active` → `Frozen`, 본문 첫 헤더 위에 `> **RESOLVED 2026-04-30** — 상류 PR #494 …` 한 줄 인용 블록 추가 +- **AC-7** — `docs/upstream/README.md` 의 `issue-utf16-pos-to-char-idx.md` row `Status` → `Frozen`, `RESOLVED` 컬럼에 `2026-04-30 ([PR #494](https://github.com/edwardkim/rhwp/pull/494))` 채움 +- **AC-8** — `CHANGELOG.md` `[0.3.2]` 섹션 신설, `### Build` 영역에 SSOT 단일화 명시 + +## 영구 비목표 + +- **상류 핀 추가 bump** (`0fb3e67` → v0.7.9 GA `2efba58`) — 직교 영역 변경, 별도 minor 에서 다른 enabling change 와 묶음 +- **`document_core::helpers::utf16_pos_to_char_idx` visibility 변경** — 상류 옵션 A 채택으로 옵션 B 검토 가치 소멸 +- **다른 PATCH 항목 묶음 (BMP fix / `[async]` extras 키 정리 등)** — 본 spec 은 단일 SSOT 정정 단위, 다른 후보는 별도 spec +- **자체 단위 테스트의 wrapper 형태 보존** — ADR §4 검증자 반박 참조 +- **`InlineRun` 의 codepoint → UTF-16 환원 옵션** — UTF-16 노출은 v0.2.0 결정 시점부터 영구 안 함 + +## 참조 + +- 짝 페어 (ADR): [ir-upstream-utf16-helper-research.md](../../design/v0.3.2/ir-upstream-utf16-helper-research.md) +- 자체 이슈 초안: [docs/upstream/issue-utf16-pos-to-char-idx.md](../../upstream/issue-utf16-pos-to-char-idx.md) (v0.3.2 GA 시 in-place Frozen) +- 상류 PR: +- 상류 Task: +- 상류 메서드: `external/rhwp/src/model/paragraph.rs:818` — `pub fn utf16_pos_to_char_idx` From 10a538cfc871d7e7eff2b9966b3645eed70f0ff3 Mon Sep 17 00:00:00 2001 From: DanMeon Date: Sun, 3 May 2026 22:16:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20UTF-16=20=E2=86=92=20codepoint?= =?UTF-8?q?=20=EB=B3=80=ED=99=98=EC=9D=84=20=EC=83=81=EB=A5=98=20SSOT=20?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A8=EC=9D=BC=ED=99=94=20(v0.3.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경사항: - src/ir.rs::utf16_to_cp 자체 복사본 + u32::MAX short-circuit + fallback_end 인자 + 짝 단위 테스트 2건 제거 - build_char_runs 호출부를 para.utf16_pos_to_char_idx(start_utf16) / (end_utf16) 로 치환 (상류 PR #494 / Task #484, v0.7.9 GA) - Cargo.toml 0.3.1 → 0.3.2 - CHANGELOG.md [0.3.2] 섹션 신설 (### Build — SSOT 단일화 명시 + 핀 0fb3e67 유지) - docs/upstream/issue-utf16-pos-to-char-idx.md frontmatter Active → Frozen + RESOLVED 인용 블록 추가 - docs/upstream/README.md 인덱스 row Frozen + RESOLVED 컬럼 채움 Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 13 ++++++- Cargo.toml | 2 +- docs/upstream/README.md | 2 +- docs/upstream/issue-utf16-pos-to-char-idx.md | 4 ++- src/ir.rs | 37 ++------------------ 5 files changed, 19 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd64afd..453e243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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%. @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 8b5fdcb..d709bf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 에 문서로 안내 diff --git a/docs/upstream/README.md b/docs/upstream/README.md index 8d423fc..11f39a4 100644 --- a/docs/upstream/README.md +++ b/docs/upstream/README.md @@ -9,7 +9,7 @@ | 이슈 | Status | 상류 등록 | RESOLVED | 비고 | |---|---|---|---|---| | [issue-find-control-text-positions.md](issue-find-control-text-positions.md) | Frozen | [edwardkim/rhwp#390](https://github.com/edwardkim/rhwp/issues/390) | 2026-04-28 ([PR #405](https://github.com/edwardkim/rhwp/pull/405)) | `Paragraph::control_text_positions(&self)` 옵션 A 채택. v0.3.1 spec 이 본 파일 참조 → 삭제 대신 in-place Frozen | -| [issue-utf16-pos-to-char-idx.md](issue-utf16-pos-to-char-idx.md) | Active | [edwardkim/rhwp#484](https://github.com/edwardkim/rhwp/issues/484) | — | #390 후속 같은 결. `helpers::utf16_pos_to_char_idx` 외부 노출 | +| [issue-utf16-pos-to-char-idx.md](issue-utf16-pos-to-char-idx.md) | Frozen | [edwardkim/rhwp#484](https://github.com/edwardkim/rhwp/issues/484) | 2026-04-30 ([PR #494](https://github.com/edwardkim/rhwp/pull/494)) | #390 후속 같은 결. `Paragraph::utf16_pos_to_char_idx(&self)` 옵션 A 채택. v0.3.2 spec 이 본 파일 참조 → 삭제 대신 in-place Frozen | ## Archive 정책 diff --git a/docs/upstream/issue-utf16-pos-to-char-idx.md b/docs/upstream/issue-utf16-pos-to-char-idx.md index b32f1df..01acae9 100644 --- a/docs/upstream/issue-utf16-pos-to-char-idx.md +++ b/docs/upstream/issue-utf16-pos-to-char-idx.md @@ -1,9 +1,11 @@ --- -status: Active +status: Frozen description: "업스트림 제안 — 'utf16_pos_to_char_idx' 외부 노출 (UTF-16 위치 → codepoint 인덱스 변환 helper). #390/PR #405 후속 같은 결" last_updated: 2026-04-30 --- +> **RESOLVED 2026-04-30** — 상류 [PR #494](https://github.com/edwardkim/rhwp/pull/494) 머지로 본 issue 해결. + # 업스트림 제안 — `utf16_pos_to_char_idx` 외부 노출 > 외부 binding (`rhwp-python`) 구현 중 업스트림에서 수정이 필요해 보이는 부분을 발견하여, Claude 로 조사 및 다차례 사실 검증을 거친 결과입니다. diff --git a/src/ir.rs b/src/ir.rs index c7c43b1..64d904b 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -393,8 +393,8 @@ fn build_char_runs(para: &Paragraph, doc_info: &DocInfo) -> Vec { u32::MAX }; - let start_cp = utf16_to_cp(¶.char_offsets, start_utf16, total_cp); - let end_cp = utf16_to_cp(¶.char_offsets, end_utf16, total_cp); + let start_cp = para.utf16_pos_to_char_idx(start_utf16); + let end_cp = para.utf16_pos_to_char_idx(end_utf16); if start_cp >= end_cp { continue; @@ -418,23 +418,6 @@ fn build_char_runs(para: &Paragraph, doc_info: &DocInfo) -> Vec { runs } -/// UTF-16 offset → codepoint index 변환. -/// -/// `char_offsets[i]` 는 `text.chars().nth(i)` 에 해당하는 UTF-16 시작 위치. -/// 입력 `utf16` 이상인 첫 번째 codepoint 인덱스를 반환한다. 해당 offset 이 -/// 텍스트 끝을 넘어가면 `fallback_end` 를 반환 (텍스트 codepoint 총 길이). -fn utf16_to_cp(char_offsets: &[u32], utf16: u32, fallback_end: usize) -> usize { - if utf16 == u32::MAX { - return fallback_end; - } - for (i, &off) in char_offsets.iter().enumerate() { - if off >= utf16 { - return i; - } - } - fallback_end -} - fn build_raw_table( table: &Table, outer_section: usize, @@ -873,22 +856,6 @@ fn field_type_to_str(ft: FieldType) -> &'static str { mod tests { use super::*; - #[test] - fn utf16_to_cp_sentinel_returns_fallback() { - let offsets = vec![0u32, 1, 2]; - assert_eq!(utf16_to_cp(&offsets, u32::MAX, 3), 3); - } - - #[test] - fn utf16_to_cp_matches_first_ge() { - let offsets = vec![0u32, 1, 3, 4]; // ^ 2번째 codepoint 는 SMP 라 2 code units - assert_eq!(utf16_to_cp(&offsets, 0, 4), 0); - assert_eq!(utf16_to_cp(&offsets, 1, 4), 1); - assert_eq!(utf16_to_cp(&offsets, 2, 4), 2); // offset 2 는 char_offsets 에 없음 → 다음 >=2 인 3을 가진 인덱스 2 - assert_eq!(utf16_to_cp(&offsets, 3, 4), 2); - assert_eq!(utf16_to_cp(&offsets, 5, 4), 4); // fallback - } - // * simple_eq_text_alt — 토큰 경계 인식 검증 #[test]