Skip to content

fix(mcp): 세션 편집 직후 페이지 어휘 스테일 — 채움·치환 후 pageCount·텍스트·렌더·검색 주소가 편집 전 레이아웃(실측 3쪽 vs 10쪽) - #3704

Closed
kevin9327 wants to merge 3 commits into
edwardkim:develfrom
kevin9327:pr/mcp-session-repaginate

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

요약

mcp-serve 세션 편집(#3598 hwp_doc_fill_fields / #3601 hwp_doc_replace_text) 직후, 같은 핸들의 조회 4종이 편집 전 페이지네이션을 서빙합니다. 코어 편집 경로가 recompose_section(재조판 + dirty 마킹 + 캐시 무효화)까지만 하고 pagination 벡터를 갱신하지 않는데, 세션 조회는 전부 그 벡터를 읽기 때문입니다.

실측(4,620자 채움, samples/field-01.hwp):

세션이 보고 저장본 신규 파싱(지상 진실)
hwp_doc_info.pageCount 3 10
hwp_doc_text page=5 isError: 페이지 범위 초과: 5 (0~2) 정상 690자
hwp_doc_render_page page=7 isError: 페이지 범위 초과: 7 (0~2) 정상 SVG
hwp_doc_search matches[].page 전부 0 여러 쪽 분산

오류가 나지 않는 것이 핵심입니다. 봉투는 isError:false 에 그럴듯한 숫자를 담아 돌려주고 에이전트는 그 숫자로 다음 판단을 합니다. hwp_doc_info 는 도구 설명에서 "편집 후 페이지 수 변화를 추적할 때 쓴다"고 명시하고, hwp_doc_render_page 는 "편집 직후 눈검증(VLM) 루프가 세션 안에서 닫힌다"고 약속하는데 — 정확히 그 용도에서 값이 갱신되지 않습니다.

상세 보고서: mydocs/report/mcp_session_repaginate/README.md

결함 구조 — 편집 3종의 비대칭

비대칭

세션 도구 코어 경로 재페이지네이션
hwp_doc_set_cell text_editing.rs 셀 편집 진입점이 paginate_if_needed() 호출 ✔ 이미 정상
hwp_doc_fill_fields field_query.rs::set_field_value_by_name_atrecompose_section 까지 ✘ 스테일
hwp_doc_replace_text search_query.rs::replace_all_nativerecompose_section 까지 ✘ 스테일

뿌리는 무상태 전제를 상태 유지 표면이 물려받은 것입니다. grep 모듈 헤더는 "from_bytes 가 로드 시 paginate() 를 끝내므로 순수 조회다"라는 전제를 명시하는데, 무상태 CLI 에서는 참이지만 세션에서는 첫 편집 순간 깨집니다. edit fill-fields 는 편집 직후 직렬화하고 프로세스가 끝나므로 pagination 을 다시 읽을 일이 없습니다 — 세션이 도입되며 생긴 새 수명에 맞는 갱신 지점이 set_cell 경로에만 있었습니다.

실측 — BEFORE

BEFORE pageCount

BEFORE 범위초과

방금 자기가 만들어 낸 쪽을 세션이 존재하지 않는다고 거부합니다. 에이전트 입장에서는 편집이 실패한 것처럼 보이지만 IR 에는 정상 반영돼 있어, 재시도하면 같은 값이 한 번 더 채워지는 이중 편집 위험까지 생깁니다.

수정

  1. DocumentCore::repaginate_if_needed() 공개 표면 추가 — 내부 paginate_if_needed() 위임. batch 모드 규약 유지, dirty 구역만 증분 재처리. 새 로직을 만들지 않고 set_cell 이 이미 쓰는 그 경로를 노출만 해서 세 편집이 같은 규약을 공유하게 했습니다.
  2. 세션 편집 2종에서 도구 호출당 1회 호출 — fill 은 적용 목록이 비어 있지 않을 때 2차 적용 루프가 끝난 뒤 한 번(필드마다 부르면 N회 재조판), replace 는 치환 계수 1 이상일 때 한 번(0건 치환은 IR 불변이라 부르지 않음).

실측 — AFTER

AFTER pageCount

AFTER 쪽 접근

세션 pageCount 10 == 저장본 신규 파싱 10. 편집이 만든 5쪽 텍스트를 곧바로 읽고 7쪽을 SVG 로 렌더 — 약속된 VLM 눈검증 루프가 실제로 닫힙니다.

회귀 가드 (신규 계약 테스트 2종)

테스트

  • session_fill_repaginates_page_vocabulary — 세션 pageCount저장본 신규 파싱과 대조(보고를 믿지 않고 지상 진실 확보) + 늘어난 마지막 쪽이 hwp_doc_text 로 읽히는지. 전제 확인(truth > before) 을 별도 assert 로 둬서, 샘플이 바뀌어 채움이 더는 쪽수를 늘리지 않으면 시험이 무의미해진 사실 자체가 실패로 드러납니다.
  • session_replace_repaginates_page_vocabulary — 대량 치환 후 pageCount 증가와 검색 page 주소가 편집 전 마지막 쪽을 넘어서는지.

무회귀

검증

  • cargo test --test mcp_session_edit_contract --test mcp_session_query_contract — 12/12 (신규 2종 포함)
  • cargo test --test mcp_session_view_contract --test mcp_session_setcell_contract --test mcp_server_contract — 15/15 무회귀
  • cargo clippy --profile release-test --bin rhwp — 경고 0 / cargo fmt --check — 통과
  • 실기 BEFORE(upstream/devel 빌드) / AFTER(수정 빌드) 각 2프로브 stdio 실측

범위 밖 (별도 PR 로 잇겠습니다)

  • 저장 결과물은 종전에도 옳았습니다 — edit_serialize 는 IR 을 직렬화하므로 스테일 pagination 이 파일에 각인되지 않습니다. 이 결함은 세션이 보고하는 값의 문제입니다.
  • 더 근본적으로는 조회 쪽에서 dirty 를 보고 지연 재페이지네이션하는 방식이 낫지만, 현재 조회 경로가 전부 &self 라 시그니처 변경이 광범위합니다. 편집 지점에서 미는 이번 방식이 범위 대비 효과가 크다고 판단했습니다.
  • 같은 감사에서 나온 별개 결함들(배선 없는 선언 인자 7종, hwp_doc_text 의 잘못된 page 타입이 전체 덤프로 빠지는 문제, HWP3 세션 save 의 라이브 IR 변형 등)은 범위를 섞지 않기 위해 분리합니다.

🤖 Generated with Claude Code

kevin9327 and others added 2 commits August 1, 2026 19:03
hwp_doc_fill_fields/hwp_doc_replace_text 는 코어 recompose 로 dirty 만 남기고
재페이지네이션하지 않아, 이후 hwp_doc_info(pageCount)·hwp_doc_text·
hwp_doc_render_page·hwp_doc_search(page 주소)가 전부 편집 전 레이아웃을
서빙했다. 실측: 4,620자 채움 후 세션 pageCount 3(스테일) vs 저장본 신규 파싱
10쪽 — 7쪽 격차. hwp_doc_info 는 "편집 후 페이지 수 변화를 추적할 때 쓴다"고
약속하고, 같은 세션의 hwp_doc_set_cell 은 코어 경로가 paginate_if_needed 를
불러 이미 갱신한다 — 편집 3종 중 2종만 스테일인 비대칭이었다.

- document_core: repaginate_if_needed 공개 표면 추가 (batch 모드 규약 유지,
  dirty 구역만 증분 재처리)
- mcp_serve: fill(적용 1건 이상)·replace(계수 1 이상) 직후 도구 호출당 1회 호출
- 계약 테스트 2종: 채움·치환이 쪽수를 늘렸을 때 세션 pageCount 가 저장본
  신규 파싱과 일치하고, 늘어난 쪽이 text/search 로 곧바로 보이는지 고정

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
편집 3종 비대칭 구조, BEFORE 스테일 pageCount(3 vs 10)·범위초과 거부,
AFTER 일치·쪽 접근 성공, 계약 테스트 설계와 전제 확인 assert 의도,
무회귀 15종, 한계·후속을 기록한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
새 공개 표면이 `pub fn (&mut self)` 뮤테이터 후보로 분류돼 CI 의
classification_drift_is_blocked 가 실패했다. 이 함수는 dirty 구역을 다시 쪽으로
나눌 뿐(pagination·측정 캐시) 문서 IR 을 바꾸지 않으므로 SessionState 로 등재한다
— 같은 파일의 flush_deferred_pagination 과 같은 계열이다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kevin9327 added a commit to kevin9327/rhwp that referenced this pull request Aug 1, 2026
…im#3630 P3 완결)

에이전트는 편집 결과를 눈으로 못 본다. 표면이 "몇 쪽이 바뀌었나"를 지정해야
그 쪽만 hwp_doc_render_page 로 렌더하는 검증 루프가 상수 비용으로 닫힌다:

- 코어 pub 질의 pages_covering_paragraphs: grep 페이지 인덱스와 같은 순회를
  재사용하되 문단이 걸친 모든 쪽(분할 표 포함)을 담는다 — 누락은 거짓 통과,
  상위집합은 렌더 한 번 더일 뿐. 하나라도 커버리지 밖이면 None(부분 목록 금지)
- 진입 시 paginate_if_needed — 편집이 남긴 dirty 를 저장 직전 조판으로 소화
- 추적: fill=FieldLocation, replace=치환 전 grep 매치(문단 인덱스 불변),
  set-cell=호스트 문단, run 저널=step 합집합(set_checkbox n번째 □ 포함)
- 봉투: edit 3종 --json + run 저널 changedPages:[n]|null, dry-run·무산출 null
- capabilities outputFields 5곳 동기화
- 세션 도구는 edwardkim#3704(세션 재조판) 머지 후 후속 적층(이슈 명시)

검증: changed_pages_contract 5건 green, cli_json 22·run_plan 6·edit_verify 4·
fill 7·replace 4·set-cell 5 무회귀, clippy 0, fmt clean. 실측 4종(evidence.txt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jangster77
jangster77 self-requested a review August 1, 2026 14:17
jangster77 pushed a commit that referenced this pull request Aug 1, 2026
에이전트는 편집 결과를 눈으로 못 본다. 표면이 "몇 쪽이 바뀌었나"를 지정해야
그 쪽만 hwp_doc_render_page 로 렌더하는 검증 루프가 상수 비용으로 닫힌다:

- 코어 pub 질의 pages_covering_paragraphs: grep 페이지 인덱스와 같은 순회를
  재사용하되 문단이 걸친 모든 쪽(분할 표 포함)을 담는다 — 누락은 거짓 통과,
  상위집합은 렌더 한 번 더일 뿐. 하나라도 커버리지 밖이면 None(부분 목록 금지)
- 진입 시 paginate_if_needed — 편집이 남긴 dirty 를 저장 직전 조판으로 소화
- 추적: fill=FieldLocation, replace=치환 전 grep 매치(문단 인덱스 불변),
  set-cell=호스트 문단, run 저널=step 합집합(set_checkbox n번째 □ 포함)
- 봉투: edit 3종 --json + run 저널 changedPages:[n]|null, dry-run·무산출 null
- capabilities outputFields 5곳 동기화
- 세션 도구는 #3704(세션 재조판) 머지 후 후속 적층(이슈 명시)

검증: changed_pages_contract 5건 green, cli_json 22·run_plan 6·edit_verify 4·
fill 7·replace 4·set-cell 5 무회귀, clippy 0, fmt clean. 실측 4종(evidence.txt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jangster77 jangster77 closed this Aug 1, 2026
postmelee pushed a commit to postmelee/rhwp that referenced this pull request Aug 2, 2026
무상태 편집 3종은 edwardkim#3712 이후 "어느 쪽을 보라"를 봉투로 지정하지만 세션 편집
3종(hwp_doc_fill_fields/replace_text/set_cell)은 못 냈다. 정작 눈검증 루프를
약속한 것은 세션 쪽(hwp_doc_render_page)이라, 에이전트에게 남는 선택지는 전수
렌더(예산 폭발)나 무검증(거짓 통과)뿐이었다.

새 계산은 넣지 않는다. 코어 질의 pages_covering_paragraphs 를 그대로 쓰고,
변경 문단 근거도 무상태 판과 같은 출처를 쓴다 — fill 은 collect_all_fields 의
FieldLocation, replace 는 치환 전 grep 매치 주소(문자열 치환은 문단 인덱스를
밀지 않는다), set_cell 은 resolve_table_cell 의 호스트 문단이다. 두 경로가 같은
근거·같은 질의를 쓰므로 답이 갈라질 수 없고, 계약 테스트도 값을 적어 두는 대신
세션 봉투와 무상태 봉투를 맞대어 본다.

호출 시점이 계약의 절반이다. 세션은 편집 뒤에도 같은 인스턴스가 살아 있어서
재조판 전에 계산하면 편집 전 레이아웃을 보고한다(edwardkim#3704 가 조회 4종에서 고친
스테일). 질의가 진입에서 paginate_if_needed 를 부르므로 재조판 지점 뒤에 두는
것으로 규약이 지켜진다. 실측: 회사명에 5,000자를 채우면 3쪽→11쪽이 되고
changedPages 는 [0..8] — 3~8쪽은 편집 전에 존재하지 않던 쪽인데 그 자리에서
hwp_doc_render_page 로 렌더된다(182KB SVG).

변경이 없으면 빈 목록이다. null 은 "확정 불가, 전체를 보라"라서 치환 0건이나
notFound 뿐인 호출마다 전수 렌더를 유도하게 된다. 대상 문단이 하나라도 조판
커버리지 밖이면 그때만 전체 null 이다(부분 목록 금지).

세션 도구는 capabilities --mcp 의 tools[] 에 실리지 않아 outputFields 축이 없다.
그래서 산출을 자기서술하는 유일한 채널인 도구 description 에 규약을 적고, 신규
계약 테스트가 선언과 봉투의 드리프트를 잡는다.

검증: 신규 계약 6종 + 세션 무회귀 24 + 드리프트 가드 38 + 무상태 축 31 =
99 passed / 0 failed, clippy -D warnings 0, fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mcp mcp tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants