Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
bd0cea4
Task #1584: ColumnDef 드롭 재현 테스트 (RED) + 계획서
planet6897 Jun 27, 2026
f5d50f7
Task #1584: 본문 인라인 ColumnDef 방출 (Option A)
planet6897 Jun 27, 2026
2beba22
Task #1584: 통제 비교 검증 + 회귀 가드 영속화 (Stage 3)
planet6897 Jun 27, 2026
e59b2ee
Merge local/task1584: 본문 인라인 ColumnDef 드롭 수정 (#1584)
planet6897 Jun 27, 2026
9d82b09
Task #1587: Ruby 드롭 재현 테스트 (RED) + 계획서
planet6897 Jun 27, 2026
af26cea
Task #1587: Ruby 모델+파서 확장 (Stage 2)
planet6897 Jun 27, 2026
c3d09b0
Task #1587: Ruby 직렬화 (write_ruby + arm) (Stage 3)
planet6897 Jun 27, 2026
6747209
Task #1587: 통제 비교 검증 + 회귀 가드 영속화 (Stage 4)
planet6897 Jun 27, 2026
d375701
Merge local/task1587: HWPX Ruby(덧말) 드롭 수정 (#1587)
planet6897 Jun 27, 2026
1eee2f6
Task #1588: 선 도형 shapeComment 드롭 재현 (RED) + 계획서
planet6897 Jun 27, 2026
f3f2dc0
Task #1588: 선 도형 shapeComment 방출 (Stage 2)
planet6897 Jun 27, 2026
4c47008
Task #1588: 통제 비교 검증 + 회귀 가드 (Stage 3)
planet6897 Jun 27, 2026
31a9c64
Merge local/task1588: HWPX 선 도형 shapeComment 드롭 수정 (#1588)
planet6897 Jun 27, 2026
6fa90b9
Task #1591: para0 북마크 hoist 조사 + RED (Stage 1) + 계획서
planet6897 Jun 27, 2026
a4937bc
Task #1591: 북마크 슬롯 편입 (hoisting 제거) (Stage 2)
planet6897 Jun 27, 2026
57553e5
Task #1591: 북마크 수정 롤백 (순효과 0, 채택 게이트 미달) + 근본원인 문서화
planet6897 Jun 27, 2026
9200506
Merge local/task1591: para0 char_shape 조사 — 북마크 수정 불채택(순효과 0), mismat…
planet6897 Jun 27, 2026
4994b7e
Task #1592: 빈 문단 spurious (0,0) 수정 (Stage 1+2)
planet6897 Jun 27, 2026
aca4fde
Task #1592: 통제 비교 검증 + 회귀 가드 (Stage 3)
planet6897 Jun 27, 2026
e5b9ae3
Merge local/task1592: HWPX 빈 문단 spurious (0,0) 수정 (#1592)
planet6897 Jun 27, 2026
a4cf317
docs: 잔여 IR_DIFF 분석 갱신 — C2(#1593) 보류, 잔여 3건 mismatch-path 수렴
planet6897 Jun 27, 2026
0c72b21
fix(ci): rustfmt 포맷 수정 (mod.rs Ruby/empty-para 테스트)
planet6897 Jun 27, 2026
6550554
docs: 페이지 붕괴 군집 조사 (#1589)
planet6897 Jun 27, 2026
71a4d6a
feat(oracle): 증분 기록 + --resume (전수 배치 크래시 내성)
planet6897 Jun 27, 2026
96768e9
feat(oracle): 주기적 Hwp 재시작 + ERR 재시도 (전수 배치 COM 사망 대응)
planet6897 Jun 27, 2026
771cbca
fix(oracle): 재시작 시 Hwp.exe taskkill (프로세스 누수 200+ 방지)
planet6897 Jun 27, 2026
914b1b1
fix(oracle): 배치 시작 시 Hwp.exe 정리 (오염 환경 첫 인스턴스 com_error 방지)
planet6897 Jun 27, 2026
c3c1c5c
test(visual): opengov 36392900 라운드트립 변위 드리프트 VISUAL_XFAIL 등록
planet6897 Jun 27, 2026
2963ba7
docs(oracle): 페이지 붕괴율 확정 ~16% + restart 주기 600 환원
planet6897 Jun 27, 2026
19ce551
Merge remote-tracking branch 'origin/devel' into devel
planet6897 Jun 27, 2026
2d02356
docs(#1589): 페이지 붕괴 근본원인 통제 실험 — section0 확정 + 9후보 배제
planet6897 Jun 27, 2026
016e1a0
Merge stream/devel into devel (최신화 — 충돌 없음, PR #1586 base 갱신)
planet6897 Jun 27, 2026
0fd8b96
docs(troubleshooting): 대괄호 파일명 읽기 실패 = MSYS 경로변환 quirk (rhwp 버그 아님)
planet6897 Jun 27, 2026
b01c591
Merge remote-tracking branch 'origin/devel' into devel
planet6897 Jun 27, 2026
e2766ea
docs(#1589): 페이지 브레이크 시각 추적 — 발신명의 footer 블록이 spill 요소
planet6897 Jun 27, 2026
8324980
docs(#1589): 페이지 붕괴 근본원인 확정 — holdAnchorAndSO 직렬화 드롭
planet6897 Jun 27, 2026
770e4a1
Task #1594: holdAnchorAndSO IR 보존 (RED + 수정) (Stage 1+2)
planet6897 Jun 27, 2026
a8f5911
Task #1594: diff_documents 에 holdAnchorAndSO 게이트 추가 (Stage 3)
planet6897 Jun 27, 2026
373a886
Task #1594: 통제 비교 + opengov 가드 (Stage 3) — 채택
planet6897 Jun 27, 2026
829eb8c
Merge local/task1594: HWPX holdAnchorAndSO 드롭 수정 (#1594)
planet6897 Jun 27, 2026
bdb1933
Task #1595: ClickHere 필드 타입 CLICK_HERE 교정 (RED + 수정)
planet6897 Jun 27, 2026
9eec4f9
Task #1595: 통제 비교 검증 (Stage 3) — 채택
planet6897 Jun 27, 2026
410801f
Merge local/task1595: HWPX ClickHere 필드 타입 CLICK_HERE 수정 (#1595, 페이지 …
planet6897 Jun 27, 2026
5c87940
style: rustfmt 적용 (serializer/hwpx roundtrip·table — CI Format check 수정)
planet6897 Jun 27, 2026
e91b8ad
docs(#1589): 잔여 붕괴(~8%) 좁히기 — 불완전 generic-shape 지오메트리 직렬화
planet6897 Jun 27, 2026
905a702
Merge remote-tracking branch 'origin/devel' into devel
planet6897 Jun 27, 2026
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
75 changes: 75 additions & 0 deletions mydocs/plans/task_m100_1584.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 수행계획서 — Task #1584

**제목**: HWPX 저장 시 본문 문단의 인라인 ColumnDef(cold) 드롭 → 컨트롤 인덱스 시프트
**마일스톤**: M100 (v1.0.0)
**브랜치**: `local/task1584`
**이슈**: edwardkim/rhwp#1584

---

## 1. 배경

실문서 무손실 검증 v9(hwpdocs 9350건)에서 잔여 IR_DIFF 59건 중 **최대 단일 클래스(약 49건)**.
게이트(diff_documents)는 이를 "표 셀 char_shape ID 오매핑 `(0,3)→(0,6)`"으로 보고하나,
verbose ir-diff 및 raw XML 비교로 **진짜 근본원인은 본문 첫 문단의 인라인 ColumnDef 드롭**임을
확정함(아래 §2). 셀 char_shape 변위는 컨트롤 인덱스 시프트의 **하위 증상**.

## 2. 근본원인 (확정)

대상 파일 `36382399` 문단 0 raw XML 비교 (devel HEAD):

```
orig 컨트롤 태그: [secPr, ctrl, colPr, ctrl, colPr, ctrl, fieldBegin, tbl]
rt 컨트롤 태그: [secPr, ctrl, colPr, ctrl, fieldBegin, tbl]
colPr 개수 : orig 2 → rt 1 (2번째 인라인 ColumnDef 드롭)
IR : controls 6→5, cc 59→51(-8), ctrl[2] cold→field 시프트
```

코드 메커니즘 — `src/serializer/hwpx/section.rs`:

| 지점 | 동작 |
|------|------|
| line 119–127 | 본문 첫 문단에서 `find(첫 ColumnDef)` 1개만 `TEMPLATE_BODY_COL_PR` 앵커로 치환 |
| line 433–435 | 본문(depth 0) 인라인 슬롯 필터가 `is_hwpx_inline_slot` 사용 → **ColumnDef 전부 제외** (subList depth>0 에서만 인라인 허용) |
| line 719 `is_hwpx_inline_slot` | `Control::ColumnDef` 미포함 |

→ 문단에 ColumnDef 가 **2개 이상**이면, 1번째는 템플릿이 흡수하지만 **2번째+는 앵커도 인라인도
아니어서 드롭**된다. (1개뿐이면 정상 — 그래서 대다수 문서는 PASS.)

## 3. 수정 방향 (제안, 승인 대상)

**Option A (surgical, 권장)**: 본문 인라인 슬롯 필터(line 431–437)에서 ColumnDef 를
**"템플릿이 흡수한 첫 1개를 제외한 나머지"** 인라인 슬롯으로 포함한다.

- 템플릿 앵커(line 119)는 그대로 첫 ColumnDef 1개를 소비(=#1407 2단 정의 보존).
- 첫 ColumnDef 의 식별(인덱스)을 render 경로에 전달하여, 그 1개만 인라인에서 건너뛰고
2번째+ ColumnDef 를 `render_col_pr_ctrl` 로 인라인 방출(subList 경로와 동형).
- subList(depth>0) 경로는 불변(이미 전체 인라인 방출).

**배제한 대안(Option B)**: 템플릿을 section_def 컬럼정의로 채우고 문단 ColumnDef 전체를
인라인화 — #1407/#1388 와 광범위하게 얽혀 회귀 위험이 큼. 채택 안 함.

## 4. 회귀 위험 & 채택 기준

컬럼 모델은 #1379(인라인 제외)·#1388(secPr 여백)·#1407(colPr 2단)와 얽힘.
F3(#1556)가 2회 회귀로 실패한 영역과 동질 → **통제 비교(개선−회귀 > 0)를 채택 게이트**로 한다.

수용 기준:
1. 49건 대표 roundtrip 의 `controls 6→5` 해소(ColumnDef 보존), 해당 파일 IR diff = 0.
2. `cargo test --test hwpx_roundtrip_baseline` (samples/hwpx, #1407/#1388 컬럼 케이스 포함) 회귀 0.
3. fidelity 전수(hwpdocs 9350) 통제 비교 **순효과 > 0** (악화 0 필수).

## 5. 구현 단계 (3단계)

- **Stage 1 — 근본원인 회귀 테스트 고정**: 49건 중 대표 N건을 픽스처로 박제,
현재 `controls 6→5` 드롭을 재현하는 단위/통합 테스트 추가(현 상태 RED).
- **Stage 2 — Option A 구현**: 본문 인라인 ColumnDef(첫 앵커 제외) 방출.
Stage1 테스트 GREEN + baseline 회귀 0 확인.
- **Stage 3 — 통제 비교 검증**: fidelity 전수 재측정, 개선−회귀 집계,
순효과>0·악화0 확인 후 최종 보고서.

## 6. 산출물

- 소스: `src/serializer/hwpx/section.rs` (+ 필요 시 헬퍼)
- 테스트: 신규 픽스처 + roundtrip 단위 테스트
- 문서: `task_m100_1584_impl.md`, `_stage{1..3}.md`, `_report.md`
78 changes: 78 additions & 0 deletions mydocs/plans/task_m100_1584_impl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 구현 계획서 — Task #1584

**제목**: HWPX 본문 인라인 ColumnDef(cold) 드롭 수정 (Option A surgical)
**브랜치**: `local/task1584` · **이슈**: edwardkim/rhwp#1584
**전제**: 수행계획서(`task_m100_1584.md`) 승인됨

---

## 1. 변경 대상 요약

`src/serializer/hwpx/section.rs` 의 본문 인라인 슬롯 경로 + 슬롯 렌더 디스패치 +
`src/serializer/hwpx/context.rs` 의 컨텍스트 플래그 1개.

| # | 파일:지점 | 현재 | 변경 |
|---|-----------|------|------|
| C1 | context.rs:97 (`SerializeContext`) | `sub_list_depth` 만 | `is_section_first_para: bool` 필드 추가 |
| C2 | section.rs:78–82 (assemble) | 첫 문단 무표식 렌더 | 첫 문단 렌더 전후로 `ctx.is_section_first_para` true/false 설정 |
| C3 | section.rs:424–438 (slots 구성) | ColumnDef 를 본문 인라인에서 제외 | ColumnDef 를 인라인 후보로 포함 + **본문 첫 문단의 첫 ColumnDef 1개 제거**(템플릿 흡수분) |
| C4 | section.rs:832 (`render_control_slot`) | `ColumnDef if depth>0` 만 방출 | 가드 완화 — slots 에 들어온 ColumnDef 는 본문도 방출 |

> C1·C2 는 "섹션 템플릿(line 119–127)이 첫 ColumnDef 1개를 흡수한다"는 사실을
> render_runs 가 알도록 신호를 전달하기 위함. 그 1개만 인라인에서 빼고 2번째+는 방출.

## 2. 설계 근거 (드롭/중복 양쪽 방지)

핵심 불변식: **본문 첫 문단의 첫 ColumnDef = 섹션 템플릿이 흡수, 나머지 = 인라인.**

- C3 에서 ColumnDef 를 인라인 후보로 올리면 `slot_count == controls.len()`(line 425, 전 컨트롤
슬롯) 분기와 필터 분기(line 431) **양쪽** 모두 ColumnDef 를 포함하게 된다. 따라서
**두 분기 공통으로** "본문 첫 문단이면 slots 에서 첫 ColumnDef 1개 제거"를 적용한다
(`if ctx.sub_list_depth == 0 && ctx.is_section_first_para { slots.remove(첫 ColumnDef pos) }`).
- 이로써: 템플릿(1번째) + 인라인(2번째+) = **드롭 없음, 중복 없음**.
- 본문 **비첫** 문단(템플릿 미흡수): `is_section_first_para=false` → 제거 없음 → ColumnDef 전부
인라인 방출(현재는 전부 드롭되던 잠재 버그도 동시 해소).
- subList(depth>0): 종전과 동일하게 전부 인라인(제거 없음).
- C4: slots 에 도달한 ColumnDef 는 위 불변식상 "흡수분이 아닌" 것이므로 본문에서도 무조건 방출 안전.

## 3. 슬롯 카운트 정합 메모

`inferred_control_slot_count`(line ~717)는 ColumnDef 의 8유닛 슬롯도 카운트하므로, 수정 후
`slot_count` 와 `slots.len()` 관계는 케이스별로 달라질 수 있다. 어느 경우든:
- 일치 → 위치추정 경로로 ColumnDef 가 제 위치에 방출.
- 불일치 → mismatch 경로(line 454)가 `for slot in &slots` 로 **일괄 방출** → ColumnDef 보존.

즉 **드롭은 어느 경로에서도 발생하지 않는다**(현재는 slots 에서 빠져 양쪽 다 드롭).

## 4. 구현 단계 (3단계)

### Stage 1 — 드롭 재현 회귀 테스트 박제 (RED)
- 49건 중 대표 1–2건(예 `36382399`)을 `tests/fixtures/` 또는 기존 roundtrip 테스트에 편입.
- 단위 테스트: 본문 첫 문단 ColumnDef 2개 → serialize → reparse 후 `controls` 수/ColumnDef 보존
검증. 현재 코드에서 **실패(RED)** 함을 확인.
- `samples/hwpx` 에 동형 미니 케이스 부재 시, hwpdocs 대표 파일 기반 통합 테스트로 대체.
- 커밋: `Task #1584: ColumnDef 드롭 재현 테스트 (RED)` + `_stage1.md`.

### Stage 2 — Option A 구현 (GREEN)
- C1–C4 적용.
- Stage1 테스트 GREEN.
- `cargo test --test hwpx_roundtrip_baseline` 회귀 0 (#1407 2단·#1388 여백 케이스 보존 확인).
- `cargo clippy` 클린, 신규/수정 파일만 정리(무관 fmt diff 금지).
- 커밋: `Task #1584: 본문 인라인 ColumnDef 방출 (Option A)` + `_stage2.md`.

### Stage 3 — 통제 비교 검증 (채택 게이트)
- fidelity 전수(hwpdocs 9350 hwpx + samples 319 hwp) 재측정.
- 수정 전(devel HEAD) 대비 **개선−회귀** 집계: 49건 IR_DIFF 해소 확인, **악화 0 필수**, 순효과>0.
- `tools/verify_hangul_pages.py` 대표 샘플 페이지수 불변 확인(시각 붕괴 0).
- `tests/opengov_corpus_snapshot.rs` 스냅샷 갱신(개선 반영).
- 커밋: `_stage3.md` + `_report.md`.

## 5. 롤백 기준

Stage 3 통제 비교에서 **악화 1건 이상** 또는 baseline 회귀 발생 시: F3(#1556) 선례대로
**전량 되돌리고**(net-negative 불채택) 원인 재분석. 부분 채택하지 않는다.

## 6. 산출물
- 소스: `section.rs`, `context.rs`
- 테스트: 신규 픽스처 + roundtrip 단위/통합 테스트, opengov 스냅샷 갱신
- 문서: `_stage1.md`, `_stage2.md`, `_stage3.md`, `_report.md`
73 changes: 73 additions & 0 deletions mydocs/plans/task_m100_1587.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 수행계획서 — Task #1587

**제목**: HWPX 저장 시 Ruby(덧말) 컨트롤 드롭 수정
**마일스톤**: M100 (v1.0.0) · **이슈**: edwardkim/rhwp#1587 · **브랜치**: `local/task1587`

---

## 1. 배경

fidelity10(hwpdocs 9660) 잔여 IR_DIFF 중 3건(36384160·36399208·36389301)에서 Ruby(덧말)
컨트롤이 저장 시 드롭. 잔여 10건 중 **유일하게 시각 영향 있는 실버그**(루비 주음 소실).
컨트롤 드롭으로 후속 char_shape −8 변위 하위 증상 동반(36389301).

## 2. 그라운딩 — 스코프 발견 (중요)

착수 조사 결과, 단순 "방출 arm 추가"로는 **무손실이 되지 않음**을 확인:

원본 dutmal 구조 (36389301):
```xml
<hp:dutmal posType="TOP" szRatio="0" option="0" styleIDRef="0" align="CENTER">
<hp:mainText>팀단위 훈련</hp:mainText>
<hp:subText>전술훈련 30% + 현지훈련 20%</hp:subText>
</hp:dutmal>
```

`Ruby` 모델(`control.rs:165`)은 `ruby_text`(=subText) + `alignment`(u8) **2개 필드뿐**:

| 원본 속성/요소 | 현 모델 처리 | 손실 |
|----------------|-------------|------|
| `subText` | `ruby_text` | OK |
| `mainText`(기준 텍스트) | 파서가 **skip**, 미보존 | **손실** — 시각 복원 불가 |
| `posType`(TOP/BOTTOM) | `alignment` 에 병합 | 충돌 |
| `align`(LEFT/RIGHT/CENTER) | `alignment` 에 병합(posType 덮어씀) | 충돌 |
| `szRatio` / `option` / `styleIDRef` | 미보존 | 손실(현 샘플 전부 0 — 우연 일치) |

또한 파서(`section.rs:515`)는 dutmal 에 `\u{0002}`(컨트롤 마커 1개)만 push.

→ **진짜 무손실은 모델+파서+직렬화기 3계층 확장이 필요**하다. IR 스켈레톤만 복원(빈 mainText)
하면 IR_DIFF 게이트는 통과하나 루비가 기준 텍스트 없이 떠 **시각 충실도 미달**.

## 3. 스코프 결정

**전체 충실도(권장)**: `Ruby` 모델에 `main_text: String`, `pos_type: u8`, `align: u8`,
`sz_ratio`, `option`, `style_id_ref` 를 추가하고, 파서가 이를 채우며, 직렬화기가 역방출.
- HWP5(OLE) 파서/직렬화기에도 동일 필드가 있으면 정합 확인(없으면 HWPX 한정 처리).

**배제(IR 스켈레톤만)**: 빈 mainText 로 IR_DIFF 만 해소 — 시각 미달이라 프로젝트 무손실 목표
부적합. 채택 안 함.

## 4. 회귀 위험 & 채택 기준

모델 필드 추가는 파서/직렬화기/HWP5 정합에 파급. **통제 비교(개선−회귀>0, 악화0)** 게이트.

수용 기준:
1. 3건(36384160·36399208·36389301) roundtrip IR diff = 0 (ruby 보존, char_shape 시프트 해소).
2. dutmal 속성(posType·align·szRatio·option·styleIDRef) + mainText + subText 무손실 재현.
3. `cargo test --lib` + `hwpx_roundtrip_baseline` 회귀 0.
4. fidelity 전수 통제 비교 순효과 > 0, 악화 0.

## 5. 구현 단계 (4단계)

- **Stage 1 — 재현 테스트(RED)**: Ruby 2개·mainText·속성 포함 문단 roundtrip 단위 테스트
(현 코드에서 controls=[] 로 RED).
- **Stage 2 — 모델+파서 확장**: `Ruby` 필드 추가, `parse_dutmal` 가 mainText/posType/align/
szRatio/option/styleIDRef 채움. HWP5 정합 확인.
- **Stage 3 — 직렬화기**: `write_ruby`(=`<hp:dutmal>` 역매핑) + `render_control_slot` 에
`Control::Ruby` arm. Stage1 GREEN + baseline 회귀 0.
- **Stage 4 — 통제 비교**: fidelity 전수 재측정, 개선−회귀 집계, opengov 가드(36389301) 편입.

## 6. 산출물
- 소스: `src/model/control.rs`, `src/parser/hwpx/section.rs`, `src/serializer/hwpx/section.rs`(+신규 ruby 직렬화)
- 테스트: 신규 단위 + opengov 가드
- 문서: `_impl`, `_stage1~4`, `_report`
81 changes: 81 additions & 0 deletions mydocs/plans/task_m100_1587_impl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 구현 계획서 — Task #1587

**제목**: HWPX Ruby(덧말) 컨트롤 드롭 수정 — 모델+파서+직렬화기 3계층
**브랜치**: `local/task1587` · **이슈**: edwardkim/rhwp#1587
**전제**: 수행계획서(`task_m100_1587.md`) 승인됨

---

## 1. 파급 범위 (그라운딩 확정)

| 계층 | 파일 | 현 상태 | 변경 |
|------|------|---------|------|
| 모델 | `src/model/control.rs` Ruby | ruby_text+alignment(2필드) | 필드 확장(아래) |
| 파서(HWPX) | `parser/hwpx/section.rs` parse_dutmal | mainText skip, posType/align 충돌 | 전 속성/요소 보존 |
| 직렬화(HWPX) | `serializer/hwpx/section.rs` | Ruby arm 부재(드롭) | write_ruby + arm |
| HWP5 | — | ruby 파서 **부재**(미지원) | **무영향**(extra 필드 무시) |

`.alignment` 읽기는 parse_dutmal 한 곳뿐(main.rs 는 ruby_text 만, body_text 는 `_` 매칭) →
모델 필드 교체의 외부 파급 없음.

## 2. 모델 변경 (C1)

```rust
pub struct Ruby {
pub main_text: String, // mainText 기준 텍스트 — 신규(시각 충실도 핵심)
pub ruby_text: String, // subText 덧말
pub pos_type: u8, // posType: 0=TOP, 1=BOTTOM — 신규(alignment 분리)
pub align: u8, // align: 0=LEFT, 1=RIGHT, 2=CENTER — 신규
pub sz_ratio: u8, // szRatio — 신규
pub option: u32, // option — 신규
pub style_id_ref: u16, // styleIDRef — 신규
}
```
- `alignment` 제거(pos_type+align 로 분리). `#[derive(Default)]` 유지 → 기존 호출 호환.

## 3. 파서 변경 (C2) — `parse_dutmal`

- 속성: `posType`→pos_type, `align`→align, `szRatio`→sz_ratio, `option`→option,
`styleIDRef`→style_id_ref (문자열→정수 파싱).
- 자식: `mainText`→`read_dutmal_text`로 `main_text` 채움(현 skip 제거), `subText`→ruby_text.
- 호출부(section.rs:515)의 `\u{0002}` 마커 push 는 유지(슬롯 위치 보존).

## 4. 직렬화 변경 (C3) — `write_ruby` + arm

- 신규 `write_ruby(ruby) -> String`: `<hp:dutmal posType= align= szRatio= option= styleIDRef=>`
`<hp:mainText>{main_text}</hp:mainText><hp:subText>{ruby_text}</hp:subText></hp:dutmal>`.
속성/텍스트 XML escape. parse_dutmal 의 정확한 역매핑.
- `render_control_slot` 에 `Control::Ruby(r) => out.push_str(&write_ruby(r))` arm 추가.
Ruby 는 이미 `is_hwpx_inline_slot` 포함 → 슬롯 위치 자동.

## 5. 구현 단계 (4단계)

### Stage 1 — 재현 테스트 (RED)
- `serialize_hwpx→parse_hwpx` roundtrip 단위 테스트: ruby 포함 문단 → reparse 후 controls 에
Ruby 보존 + ruby_text 일치 검증. 현재 RED(controls=[]).
- 테스트는 `Ruby { ruby_text, ..Default::default() }` 형태로 작성(모델 변경에 견고).
- 커밋: `Task #1587: Ruby 드롭 재현 테스트 (RED)` + `_stage1.md`.

### Stage 2 — 모델+파서 확장 (C1, C2)
- C1 모델 필드 교체, C2 parse_dutmal 전 속성/요소 보존.
- `cargo build` + 기존 테스트 영향 없음 확인(파급 parse_dutmal 한정).
- 커밋: `Task #1587: Ruby 모델+파서 확장` + `_stage2.md`.

### Stage 3 — 직렬화기 (C3)
- write_ruby + arm. Stage1 GREEN + 신규 필드(main_text/pos_type/align/sz_ratio/option/
style_id_ref) 무손실 단언 추가.
- `cargo test --lib` + `hwpx_roundtrip_baseline` 회귀 0.
- 커밋: `Task #1587: Ruby 직렬화 (write_ruby + arm)` + `_stage3.md`.

### Stage 4 — 통제 비교 (채택 게이트)
- fidelity 전수 재측정: 3건(36384160·36399208·36389301) 해소 확인, 악화 0, 순효과>0.
- opengov 가드(36389301) 편입 + snapshot 갱신.
- 커밋: `_stage4.md` + `_report.md`.

## 6. 롤백 기준
Stage 4 통제 비교 악화 ≥1 또는 baseline 회귀 시 전량 되돌리고 재분석(부분 채택 금지).

## 7. 산출물
- 소스: control.rs, parser/hwpx/section.rs, serializer/hwpx/section.rs
- 테스트: 신규 roundtrip 단위 + opengov 가드
- 문서: `_stage1~4`, `_report`
45 changes: 45 additions & 0 deletions mydocs/plans/task_m100_1588.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 수행계획서 — Task #1588

**제목**: HWPX 저장 시 선 도형(`hp:line`) shapeComment 드롭 수정
**마일스톤**: M100 (v1.0.0) · **이슈**: edwardkim/rhwp#1588 · **브랜치**: `local/task1588`

---

## 1. 배경

fidelity11 잔여 IR_DIFF 중 3건(36389418·36392900·36391302)에서 선 도형 설명
(`shapeComment`, "선입니다.")이 저장 시 드롭. 잔여 분석 Class B.

## 2. 근본원인 (`src/serializer/hwpx/shape.rs`)

- `write_shape_comment(w, c)`(line 852)는 `c.description` 비어있지 않으면 `<hp:shapeComment>`
방출.
- `write_rect`(line 110)·`write_container_close`(line 235)는 호출하나, **`write_line`(line 121)은
caption 까지만 방출하고 `write_shape_comment` 미호출** → 선 도형 설명 드롭.
- 파서는 정상(원본 파싱 시 description="선입니다." 캡처 — IR_DIFF 의 expected 값이 증거).
**순수 직렬화기 누락 1줄**.

## 3. 수정 방향

`write_line` 의 caption 방출 직후(end_tag 직전) `write_shape_comment(w, c)?;` 1줄 추가.
OWPML AbstractShapeObjectType 순서(outMargin → caption → shapeComment, write_rect 동형) 준수.

## 4. 회귀 위험 & 채택 기준

극히 낮음(기존 헬퍼 재사용, 선 도형 한정). 그래도 통제 비교로 확인.

수용 기준:
1. 3건(36389418·36392900·36391302) roundtrip IR diff = 0(shapeComment 보존).
2. `cargo test --lib` + `hwpx_roundtrip_baseline` 회귀 0.
3. fidelity 통제 비교 순효과 > 0, 악화 0.

## 5. 구현 단계 (3단계)

- **Stage 1 — RED**: 선 도형 description 포함 도형 roundtrip 단위 테스트(현재 드롭 → RED).
- **Stage 2 — 수정**: `write_line` 에 `write_shape_comment` 1줄 추가. Stage1 GREEN + baseline 회귀 0.
- **Stage 3 — 통제 비교**: fidelity 전수 재측정, 3건 해소 + 악화 0 확인, opengov 가드 편입.

## 6. 산출물
- 소스: `src/serializer/hwpx/shape.rs`
- 테스트: 신규 단위 + opengov 가드
- 문서: `_impl`, `_stage1~3`, `_report`
Loading
Loading