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
93 changes: 93 additions & 0 deletions mydocs/manual/hwp5_roundtrip_baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# HWP5 Roundtrip Baseline 가이드 (Task #1552)

`samples/*.hwp` 전수에 대한 HWP5→IR→HWP5 roundtrip **무손실** 검증 체계의 사용·유지보수 매뉴얼.
`hwpx-roundtrip`(Task #1315)의 HWP5 대응 게이트.

## 1. 개요

`serialize_document`(= `export_hwp_native`, HWP5 "저장하기")의 **무손실성**을 회귀 게이트로 고정한다.
검사 항목(C1~C5):

| # | 검사 | 방법 | 잡는 결함 |
|---|------|------|-----------|
| C1 | IR 뼈대 diff | `parse → serialize → 재parse` 후 `diff_documents` == 0 | 구조 손실 |
| C2 | **BinData 보존** | 원본·저장본 CFB의 BinData **decompressed 내용** 멀티셋 동일 | **그림 스트림 드롭(F1)** |
| C3 | 페이지수 복원 | `DocumentCore::from_bytes` 페이지 수 원본==저장본 | 페이지 변형 |
| C4 | CFB 구조 | 필수 스트림(FileHeader/DocInfo/BodyText/Section0) + 섹션 수 = IR | 구조 회귀 |
| C5 | 2-round 안정성 | 저장본 재직렬화→재parse 후 IR diff == 0 | 비결정성 |

> **중요**: 통과 = 구조+BinData+페이지(rhwp 자기 일관) 보존이며 **시각 충실도 보장이 아니다**.
> C3 는 rhwp 자기 재로드 기준이라 **외부 한글에서만** 나타나는 페이지 붕괴(예: `convert`/
> `convert_to_editable` 경로)는 자동 검출하지 못한다. 시각·외부 divergence 판정은
> 작업지시자(한컴에디터)와 `output/poc/fidelity/` 한글 harness(T3 재열림·T4 PDF)가 보조한다.

## 2. 등급 체계

| 등급 | 의미 | 코드 위치 |
|------|------|----------|
| **A (baseline)** | C1~C5 전부 통과. 신규 HWP5 샘플 자동 포함 | `tests/hwp5_roundtrip_baseline.rs` 기본 대상 |
| **B (xfail)** | 식별된 결함으로 baseline 제외. 사유 필수 | `XFAIL` 상수 |
| **자동 제외** | HWP5 아님(HWP3/HWPML) 또는 배포용 문서 — serializer 결함 아님 | `out_of_scope()` (포맷·distribution 감지) |

현황 (2026-06-26, `samples/*.hwp` 319건):
- **A=297, B(xfail)=9**, 자동 제외=13 (HWP3 10 + 배포용 3).
- B(xfail) 9건은 전부 `serialize_document`의 **BinData 그림 스트림 드롭(F1)**:
`img-start-001`(20/20), `BookReview`(7/10), `Worldcup_FIFA2010_32`(13/47),
`exam_social`(2/8), `NewYear_s_Day`(2/4), `곡선이있는분산형`(2/3), `pic-crop-01`(2/3),
`interview`(1/3), `BlogForm_Recipe`(1/3). 후속 이슈에서 serializer 수정 시 승격.

> **자동 제외 근거**: HWP3(`HWP Document File V3.00`)는 별도 포맷이라 HWP5 직렬화 시
> 교차변환(페이지 폭증)된다. 배포용 문서는 `serialize_document` 직접 적용 시
> `DISTRIBUTE_DOC_DATA` 누락으로 재파싱 실패하나, 정상 경로는 `convert_to_editable`
> 선행이므로 게이트 범위 밖이다(별도 이슈로 분리).

## 3. 통합 테스트 (`tests/hwp5_roundtrip_baseline.rs`)

```bash
cargo test --release --test hwp5_roundtrip_baseline
```

| 테스트 | 역할 |
|--------|------|
| `baseline_all_samples_roundtrip` | 소형(≤3MB) 전수 — **신규 샘플 자동 포함** |
| `baseline_large_samples_roundtrip` | 대형(>3MB) 분리 — 하네스 병렬로 wall time 단축 |
| `xfail_entries_still_fail` | xfail 이 통과하면 실패 → baseline 승격 강제 |

### 신규 샘플 추가 시
`samples/` 에 `.hwp` 추가 시 자동으로 baseline 게이트에 포함된다.
- 통과 → 끝 (A등급)
- 실패 → 결함 수정하거나 **사유와 함께** `XFAIL` 등록(사유 없는 등록 금지)
- HWP3/배포용 → `out_of_scope()` 가 자동 제외(목록 불필요)

### xfail 승격 절차
serializer 결함(F1 등) 해소 시 `xfail_entries_still_fail` 가 실패한다.
해당 항목을 `XFAIL` 에서 제거하면 baseline 으로 자동 승격된다.

## 4. 배치 CLI (`rhwp hwp5-roundtrip`)

```bash
rhwp hwp5-roundtrip sample.hwp # 단일 파일 검사
rhwp hwp5-roundtrip --batch samples # 폴더 전수 (재귀)
rhwp hwp5-roundtrip --batch samples -o output/poc/task1552 # 산출물 지정
```

- 산출물: `{out}/inventory.tsv`(17컬럼) + `{out}/{stem}.rt.hwp`(재조립 파일)
- 상태 우선순위: `PARSE_FAIL → SERIALIZE_FAIL → REPARSE_FAIL → IR_DIFF → BINDATA_LOSS → CFB_STRUCT_FAIL → PAGE_DIFF → ROUND2_FAIL → ROUND2_DIFF → PASS`
- 하드 실패(파싱/직렬화/재파싱/BinData/구조/페이지/2-round) 존재 시 종료 코드 1 (CI 사용 가능)
- `inventory.tsv` 컬럼: sample, status, parse/serialize/reparse_ok, ir_diff_count,
bindata_total, bindata_lost, page_before, page_after, cfb_struct_ok, round2_diff,
elapsed_ms, error, ir_diff_summary, cfb_problems, round2_error.

## 5. Known limitations / 후속

| 한계 | 증상 | 후속 |
|------|------|------|
| BinData 그림 스트림 드롭 | `serialize_document` 가 일부 그림 누락 (xfail 9건) | F1 수정 이슈 |
| convert 경로 추가 손실 | `convert_to_editable`/`convert` 경유 시 이미지·페이지 손실(KTX 27→1) — 본 게이트(serialize_document 직접) 범위 밖 | F2' 별도 이슈 |
| C3 외부 divergence | rhwp 자기일관 페이지는 보존이나 외부 한글에서만 붕괴하는 경우 미검출 | 한글 harness 보조 |

## 6. 관련 문서
- 수행/구현 계획: `mydocs/plans/task_m100_1552{,_impl}.md`
- 단계별 보고서: `mydocs/working/task_m100_1552_stage{1..4}.md`
- 최종 보고서: `mydocs/report/task_m100_1552_report.md`
- 선행 조사(한글 4단계 오라클): `output/poc/fidelity/report.md`
90 changes: 90 additions & 0 deletions mydocs/plans/task_m100_1552.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Task #1552: HWP5 roundtrip 무손실 게이트(hwp5-roundtrip CLI) 신설 — 수행계획서

## 목표

`hwpx-roundtrip`에 대응하는 **HWP5 동일포맷 roundtrip 게이트**를 신설한다.
parse → `export_hwp_native` → 재parse 경로의 무손실성을, **IR 뼈대만이 아니라
BinData 스트림 보존·페이지수 복원·CFB 구조까지** 자동 검사하여 회귀 게이트화한다.

## 배경 (조사 #1552 선행)

HWPX/HWP5 동일포맷 roundtrip 무손실 4단계 검증(IR·텍스트·한글 재열림·한글 PDF)
결과, **IR 뼈대 diff=0이 무손실을 보장하지 않음**이 실증됨
(보고서: `output/poc/fidelity/report.md`, 표본 각 포맷 25건).

| ID | 심각도 | 증상 | 자동검사 현황 |
|----|--------|------|---------------|
| F1 | 심각 | HWP5 저장 시 BinData 그림 스트림 통째 드롭 (25건 중 4건/19스트림) | IR diff=0·텍스트 동일 → **전건 false PASS** |
| F2 | 심각 | KTX(437문단·다중 표) 저장본 한글에서 27→1쪽 붕괴 | rhwp 2-round r2=0 → **자가검출 불가** |
| F3 | 경미 | HWPX 컨트롤 슬롯 8유닛 시프트 | 기존 `hwpx-roundtrip`가 검출(IR_DIFF) |

> 본 타스크는 **게이트(검출 도구)** 신설에 한정한다. F1·F2의 실제 serializer 수정,
> F3 HWPX 잔여는 **별도 후속 이슈**로 분리한다(혼합 금지).

## 현황 분석 (기존 자산)

- `hwpx-roundtrip`(`rhwp::diagnostics::hwpx_roundtrip_batch::run`): HWPX 전용 게이트.
단일/`--batch`/`-o`, `inventory.tsv`, 상태 우선순위, 종료 코드. **미러 대상**.
- `export_hwp_native`(`document_core::commands::document.rs:582`) = `serialize_document`. HWP5 저장 진입점.
(HWP 출처는 `export_hwp_with_adapter`도 동일 — 어댑터 no-op.)
- `serialize_hwp_with_verify`(같은 파일): serialize→재로드 후 **page_count_before/after 비교**.
→ F2 게이트 재료(현재 CLI 미노출).
- `CfbReader::list_bin_data()` / `read_bin_data()`(`parser/cfb_reader.rs`): BinData 스트림 이름·바이트 접근.
→ F1 게이트 재료.
- IR 뼈대 비교 함수(HWPX baseline의 `diff_documents` 계열): HWP5에도 적용 가능(공통 `Document` IR).

## 범위

**포함(In)**
- 신규 진단 모듈 `src/diagnostics/hwp5_roundtrip_batch.rs` + `main.rs` 서브커맨드 1행.
- 단일/배치 모드, `inventory.tsv` 산출, 종료 코드(CI).
- 검사 항목 5종(아래).
- `samples/` 전수 배치 1회 실행 → 현 손실 현황 등급화(`XFAIL`/제외 분류).
- 회귀 통합 테스트 + 매뉴얼.

**제외(Out)**
- F1·F2 serializer 실제 수정(후속 이슈). 본 게이트는 **검출만**.
- F3 HWPX 잔여(별도).
- 한글 OCX 오라클(T3/T4) 자동 연동 — 외부 의존이라 게이트 본체에 미포함.
대신 조사 harness(`output/poc/fidelity/`)를 보조 자산으로 병기.

## 검사 항목 설계

| # | 검사 | 방법 | 잡는 결함 |
|---|------|------|-----------|
| C1 | IR 뼈대 diff | parse→serialize→재parse 후 IR 비교(diff=0) | (기존 수준) |
| C2 | **BinData 보존** | orig/rt CFB의 BinData 스트림 **decompressed 내용 멀티셋** 동일 | **F1** |
| C3 | **페이지수 복원** | `serialize_hwp_with_verify`의 before==after | **F2(부분)** |
| C4 | CFB 구조 | 필수 스트림(FileHeader/DocInfo/BodyText/Section{N}) + 섹션 수 = IR | 구조 회귀 |
| C5 | 2-round 안정성 | rt→재serialize→재parse 후 IR diff=0 | 비결정성 |

> C2 주의: BinData는 raw deflate 압축. **decompressed 바이트**로 비교해야 재압축
> (무손실, 크기만 변동)을 오탐하지 않는다(조사에서 검증 완료). 압축 실패 스트림은
> 저장 바이트로 폴백 비교.
> C3 한계: rhwp 자기 재로드 기준이라 KTX형(외부 한글에서만 붕괴) 일부는 미검출 가능 →
> 보고서에 한계 명시 + harness 보조.

## 산출물

- `src/diagnostics/hwp5_roundtrip_batch.rs` (신규)
- `main.rs`: `Some("hwp5-roundtrip") => ...` 1행 + `--help` 항목
- `inventory.tsv`(상태/diff/bindata/page/구조 컬럼) + `{stem}.rt.hwp`
- `tests/hwp5_roundtrip_baseline.rs`(신규) — `samples/*.hwp` 전수 게이트(XFAIL/제외 등급)
- `mydocs/manual/hwp5_roundtrip_baseline.md`(신규)

## 영향 범위

- 신규 파일 위주. 기존 동작 변경 없음(`main.rs` 분기 1행 추가).
- HWP3 전용 분기 추가 없음(CLAUDE.md 규약 준수). 게이트는 HWP5 파서/직렬화만 사용.

## 검증 기준

- 신규 CLI가 단일/배치에서 C1~C5 판정 + `inventory.tsv` 산출 + 손실 존재 시 종료 코드 1.
- 조사에서 확인된 **KTX·interview·Worldcup이 게이트에서 FAIL/XFAIL로 분류**됨(C2·C3 검출 실증).
- exam_kor 등 정상 대조군은 PASS.
- `cargo test --test hwp5_roundtrip_baseline` 통과(현 손실은 사유와 함께 XFAIL 등록).
- 기존 `cargo test` 회귀 없음.

## 다음 단계

승인 시 **구현 계획서**(`task_m100_1552_impl.md`, 3~6단계) 작성 → 재승인 후 단계별 진행.
86 changes: 86 additions & 0 deletions mydocs/plans/task_m100_1552_impl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Task #1552: HWP5 roundtrip 무손실 게이트 — 구현 계획서

> 수행계획서: `task_m100_1552.md` (승인 완료). 본 문서는 단계별 구현 계획(4단계).
> 미러 대상: `src/diagnostics/hwpx_roundtrip_batch.rs`(577줄) + `tests/hwpx_roundtrip_baseline.rs`.

## 재사용 확정 API (조사 완료)

| API | 위치 | 용도 |
|-----|------|------|
| `parser::parse_document(&[u8]) -> Result<Document>` | 파서 진입 | doc1/doc2/doc3 파싱 |
| `serializer::serialize_document(&Document) -> Result<Vec<u8>>` | `serializer/mod.rs:78` | HWP5 저장(= `export_hwp_native`) |
| `diff_documents(&Document, &Document) -> IrDiff` | `serializer/hwpx/roundtrip.rs:427` | **포맷 무관** IR 뼈대 비교(C1·C5) |
| `CfbReader::{list_bin_data, read_bin_data}` | `parser/cfb_reader.rs` | BinData 스트림 열거·raw 읽기(C2) |
| `decompress_stream(&[u8]) -> Result<Vec<u8>>` | `parser/cfb_reader.rs:640` | BinData raw deflate 해제(C2) |
| `CfbReader::{section_count, list_streams}` | `parser/cfb_reader.rs` | CFB 구조 검사(C4) |
| `DocumentCore::{from_bytes, page_count, serialize_hwp_with_verify}` | `document_core/...` | 페이지수 복원(C3) |

---

## Stage 1 — 모듈 골격 + C1(IR diff) + C5(2-round) + CLI 연결

**목표**: `hwpx-roundtrip`을 미러한 `hwp5-roundtrip` 최소 동작(단일/배치/`-o`/`inventory.tsv`/종료코드).

- 신규 `src/diagnostics/hwp5_roundtrip_batch.rs`:
- `Options`/`parse_args`(단일 파일 | `--batch <dir>` | `-o <out>`), `collect_hwp5_files`(재귀 `*.hwp`, ViewText/배포용 등은 포함하되 파싱 실패는 상태로 기록)
- `roundtrip_one`: `parse_document`(doc1) → `serialize_document` → `parse_document`(doc2) → `diff_documents(doc1,doc2)` → 2-round(serialize→doc3, `diff_documents(doc2,doc3)`)
- `RoundtripRow` + `status()`/`is_hard_fail()` (상태 우선순위: `PARSE_FAIL→SERIALIZE_FAIL→REPARSE_FAIL→IR_DIFF→ROUND2_FAIL→ROUND2_DIFF→PASS`)
- `write_tsv`/`print_summary`/`rt_output_path`(`{stem}.rt.hwp`)
- `src/diagnostics/mod.rs`: `pub mod hwp5_roundtrip_batch;`
- `main.rs`: `Some("hwp5-roundtrip") => rhwp::diagnostics::hwp5_roundtrip_batch::run(&args[2..])` + `--help` 항목
- 단위 테스트: `parse_args`(단일/배치/거부), `rt_output_path`, blank 샘플 PASS (hwpx 테스트 미러)

**검증**: `rhwp hwp5-roundtrip samples/business_overview.hwp` PASS, `--batch`로 `inventory.tsv` 생성. `cargo test` 회귀 없음.
**산출**: `task_m100_1552_stage1.md` + 소스 커밋.

## Stage 2 — C2 BinData 스트림 보존 검사

**목표**: F1(이미지 드롭) 게이트화.

- `bindata_fingerprint(bytes: &[u8]) -> BTreeMap<해시, count>`:
- `CfbReader`로 BinData 열거 → 각 스트림 raw 읽기 → `decompress_stream` 시도, 실패 시 raw 사용(압축 플래그 무관 일관)
- **decompressed 내용**의 멀티셋(내용 해시→개수). 이름(BIN0001 등)은 재명명 가능성 있어 **내용 기준 비교**(hwpx `check_package` 정신과 동일)
- `roundtrip_one`에 orig bytes vs rt bytes fingerprint 비교 → `bindata_lost`(드롭 수)/`bindata_total` 기록
- TSV 컬럼 추가(`bindata_total`, `bindata_lost`), 상태에 `BINDATA_LOSS`(IR_DIFF와 동급 hard-fail) 추가
- 단위 테스트: 이미지 포함 소형 샘플로 보존 PASS, 인위적 드롭 검출

**검증**: KTX(3/3)·interview(1/3)·Worldcup(13/47)이 `BINDATA_LOSS`로 검출, exam_kor(재압축만)은 보존 PASS.
**산출**: `task_m100_1552_stage2.md` + 커밋.

## Stage 3 — C3(페이지수 복원) + C4(CFB 구조)

**목표**: F2(페이지 붕괴) 부분 게이트화 + 구조 회귀 봉인.

- C3: `DocumentCore::from_bytes(orig)`로 `page_before`, rt bytes로 `page_after` 비교(또는 `serialize_hwp_with_verify` 활용). `page_before/page_after`/`page_recovered` 기록. 불일치 시 `PAGE_DIFF`.
- **한계 명시**: rhwp 자기 일관 기준이라 KTX형(외부 한글에서만 27→1) 일부는 자동 미검출 가능 — 보고서·매뉴얼에 기재, 한글 harness 보조.
- C4: rt CFB 필수 스트림(`FileHeader`,`DocInfo`,`BodyText/Section{0..}`) 존재 + `section_count(rt) == doc.sections.len()` 검사. 불일치 시 `CFB_STRUCT_FAIL`.
- TSV 컬럼 추가, 상태 우선순위에 PAGE_DIFF·CFB_STRUCT_FAIL 편입
- 단위 테스트: 정상 샘플 page_recovered=true·구조 OK

**검증**: 정상 대조군 PASS, page 불일치 샘플 검출(있으면).
**산출**: `task_m100_1552_stage3.md` + 커밋.

## Stage 4 — 회귀 테스트 + 전수 등급화 + 매뉴얼 + 최종 보고

**목표**: 게이트를 `samples/*.hwp` 전수 회귀로 고정 + 문서화.

- `tests/hwp5_roundtrip_baseline.rs`(hwpx baseline 미러):
- `baseline_all_samples_roundtrip`(전수 재귀, XFAIL/EXCLUDED 제외, 신규 샘플 자동 포함)
- `xfail_entries_still_fail`(XFAIL 승격 강제), `grade_lists_are_consistent`
- `--batch samples` 1회 실행 결과로 **현 손실을 사유와 함께 XFAIL 등록**(F1: KTX/interview/Worldcup 등, F2: 발견분). EXCLUDED: 비-HWP5/손상.
- 대형 샘플 분리(`LARGE`)로 wall time 관리(hwpx 동형)
- `mydocs/manual/hwp5_roundtrip_baseline.md`: 사용법·등급체계·C1~C5·한계(C3 외부 divergence)
- 최종 결과보고서 `mydocs/report/task_m100_1552_report.md` + 후속 이슈 후보(F1/F2/F3 수정) 정리

**검증**: `cargo test --test hwp5_roundtrip_baseline` 통과(XFAIL 정합). `cargo test` 전체 회귀 없음. `cargo clippy` 클린(신규 파일 범위).
**산출**: `task_m100_1552_stage4.md` + `_report.md` + 커밋.

---

## 공통 주의

- HWP3 전용 분기 추가 금지(CLAUDE.md). 게이트는 HWP5 파서/직렬화·CFB만 사용.
- `inventory.tsv` 컬럼은 단계별로 **추가만**(기존 컬럼 의미 불변).
- 한글 OCX 오라클은 게이트 본체 미포함 — `output/poc/fidelity/` harness가 보조.
- 단계마다 완료보고서(`_stage{N}.md`) + 소스 동반 커밋, 승인 후 다음 단계.
- 기능/포맷 변경 미혼합. 신규 파일 위주라 무관 rustfmt diff 미발생.
Loading
Loading