Skip to content

fix(mcp): hwp_doc_save 가 열린 핸들의 IR 을 바꾼다 — 저장 한 번에 본문 16줄 소실 - #3729

Closed
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:pr/mcp-save-snapshot
Closed

fix(mcp): hwp_doc_save 가 열린 핸들의 IR 을 바꾼다 — 저장 한 번에 본문 16줄 소실#3729
kevin9327 wants to merge 1 commit into
edwardkim:develfrom
kevin9327:pr/mcp-save-snapshot

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

요약

hwp_doc_save 는 도구 설명에서 "핸들은 저장 후에도 열려 있다 — 이어서 편집·재저장할 수 있다" 고 약속합니다. 그런데 저장이 살아 있는 IR 을 바꿉니다.

실측 (samples/hwp3-sample.hwp, mcp-serve stdio):

hwp_doc_text  (저장 전)  →  본문 21,538자
hwp_doc_save             →  {"bytes":64512,"outputFormat":"hwp5",...}
hwp_doc_text  (저장 후)  →  본문 21,522자      ← 빈 줄 16개 소실
hwp_doc_info             →  저장 전후 동일 (메타가 아니라 본문 IR 이 바뀐 것)

저장이 편집이 됐습니다. 에이전트가 저장 전에 잡아 둔 좌표는 저장 후 다른 것을 가리키고, 이어서 편집하면 그 위에 쌓입니다.

원인

session_saveedit_serialize(&mut sd.doc, Hwp)export_hwp_with_adapter(&mut self)convert_if_hwpx_source(&mut self.document, …).

이 어댑터는 FileFormat::Hwpx | FileFormat::Hwp3 양쪽에서 돕니다. HWP3 파서는 Control::SectionDef 를 아예 만들지 않으므로(src/parser/hwp3/ 전체에 해당 생성이 0건), 어댑터의 insert_section_def_control 이 매 구역 첫 문단의 controls[0]삽입합니다. 그런데 같은 문단의 field_ranges[].control_idx 는 따라 밀리지 않습니다 — 컨트롤 인덱스로 표현된 좌표가 저장 한 번에 한 칸씩 어긋납니다.

무상태 CLI 는 이 경로를 타도 무해합니다. 저장 직후 프로세스가 끝나 아무도 그 IR 을 다시 안 봅니다. 세션이 도입되면서 "저장 뒤에도 같은 인스턴스를 계속 쓴다"는 새 수명이 생겼는데, 어댑터는 그 수명을 모릅니다.

수정

/// 어댑터를 **복제본에 적용해** HWP5 를 낸다 — 호출자의 IR 은 그대로다.
pub fn export_hwp_with_adapter_snapshot(&self) -> Result<Vec<u8>, HwpError>
  • &self 입니다 — 시그니처가 계약을 표현합니다.
  • session_save 는 핸들을 get_mut 이 아니라 get 으로 잡습니다. "save 는 IR 을 바꾸지 않는다"를 컴파일러가 지키게 하는 쪽이 주석보다 강합니다.
  • CLI 경로는 그대로 둡니다. 저장 직후 종료하는 무상태 경로에 Document 1회 clone 비용을 지울 이유가 없습니다. 비용을 지는 쪽은 그 값을 실제로 쓰는 세션뿐입니다.
  • HWPX 산출 경로(export_hwpx_native)는 이미 &self 라 그대로입니다.

AFTER

저장 전 본문 길이: 21523 | 저장 후: 21523 | 동일? True
연속 저장 바이트 동일? True

회귀 가드

session_save_does_not_mutate_live_handle_hwp3_source — 네 축을 함께 봅니다.

  1. 저장 전후 hwp_doc_text 가 같은가 (본문 IR)
  2. 저장 전후 hwp_doc_info 가 같은가 (메타)
  3. 연속 저장이 같은 바이트인가 — 1회차가 IR 을 바꿨다면 2회차 산출물이 갈립니다. 저장이 상태를 남기지 않는다는 것을 산출물로 직접 증명합니다.
  4. 저장 뒤 편집이 이어지는가 — "핸들은 계속 열려 있다"의 실질 검증

검증

  • cargo test --test mcp_session_edit_contract6/6 (신규 1종 포함)
  • hwpx_to_hwp_adapter 7/7 · mcp_session_setcell_contract 50/50 · edit_format_preserve_contract 4/4 — 무회귀
  • cargo clippy --profile release-test --bin rhwp — 경고 0 / cargo fmt --check — 통과

후속과 순서 (중요)

같은 감사에서 나온 hwp_doc_save 가 출력 확장자를 무시하는 건(HWPX 핸들을 .hwp 로 저장하면 ZIP(PK) 바이트가 .hwp 안에 들어감)이 남아 있습니다. 이 PR 이 먼저 들어가야 합니다 — 확장자 수정은 HWPX 세션 핸들을 어댑터 경로로 새로 보내는 변경이라, 지금 상태에서 그것만 먼저 넣으면 이 결함의 영향 범위가 "HWP3 출처만"에서 ".hwp 로 저장하는 모든 HWPX 핸들"로 넓어집니다.

🤖 Generated with Claude Code

도구 설명은 "핸들은 저장 후에도 열려 있다 — 이어서 편집·재저장할 수 있다"고
약속하는데, 저장이 살아 있는 IR 을 바꿨다. 실측(samples/hwp3-sample.hwp):
hwp_doc_text 본문 21,538자 → 저장 1회 후 21,522자(빈 줄 16개 소실),
hwp_doc_info 는 불변 — 메타가 아니라 본문 IR 이 바뀐 것이다.

원인: session_save → edit_serialize(&mut) → export_hwp_with_adapter(&mut self) →
convert_if_hwpx_source 가 Hwpx|Hwp3 양쪽에서 돌며 각 구역 첫 문단 controls[0] 에
Control::SectionDef 를 삽입한다. 같은 문단의 field_ranges[].control_idx 는 따라
밀리지 않아 저장 한 번에 좌표가 한 칸씩 어긋난다. 무상태 CLI 는 저장 직후 종료해
무해하지만, 세션이 도입되며 생긴 "저장 뒤에도 같은 인스턴스를 쓴다"는 수명을
어댑터가 모른다.

- DocumentCore::export_hwp_with_adapter_snapshot(&self) — 복제본에만 어댑터 적용
- session_save 는 핸들을 get_mut 이 아니라 get 으로 잡는다(컴파일러가 계약 강제)
- CLI 경로는 그대로 — 저장 직후 종료하는 쪽에 clone 비용을 지우지 않는다
- 계약 테스트: 저장 전후 본문·메타 동일 + 연속 저장 바이트 동일 + 저장 후 편집 지속

후속 순서 주의: hwp_doc_save 의 출력 확장자 무시 건은 HWPX 핸들을 어댑터 경로로
새로 보내므로 이 PR 이 먼저 들어가야 영향 범위가 넓어지지 않는다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jangster77
jangster77 self-requested a review August 1, 2026 14:17
@jangster77 jangster77 closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants