Skip to content

feat: kevin9327 누적 PR 통합 검토 - #3742

Merged
jangster77 merged 55 commits into
develfrom
review/kevin9327-20260801
Aug 1, 2026
Merged

jangster77 merged 55 commits into
develfrom
review/kevin9327-20260801

Conversation

@jangster77

@jangster77 jangster77 commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

목적

열린 기여 PR을 개별로 merge하지 않고, 최신 devel 위에서 누적 적용·충돌 해소·공통 회귀 검증을 거친 통합 후보입니다. 원 기여 commit의 작성자 정보는 보존했고, 원 PR 안의 Merge branch devel commit은 누적 대상에서 제외했습니다.

통합 범위

  • kevin9327의 #3689부터 #3735까지의 선택된 누적 변경
  • #3716이 대체하는 stack: #3698, #3701, #3705, #3710
  • #3724와 patch-equivalent duplicate인 #3725
  • 사용자가 지정한 상한에 따라 포함한 planet6897의 #3736 — Kevin 기여와 작성자·원 SHA·최종 disposition을 분리해 기록합니다.

통합 보정

  • batch 전역 인증 옵션을 명시적으로 거부해 stdin 경로 입력과 인증 입력이 충돌하지 않게 했고, --out-dir의 flag 오해석과 대소문자 출력 충돌을 쓰기 전에 차단했습니다.
  • run plan의 set_cell 좁힘 변환을 사전 검증하고, fill_fields journal·human 출력에 confusable 경고를 동일하게 남기도록 했습니다.
  • CLI capability와 MCP batch 도구의 실제 범위를 정렬하고, bench --batch의 읽기 실패를 성공으로 삼키지 않게 했습니다.
  • HWPX unsigned offset 수정 뒤 IR sweep baseline의 divergence-cap 노출 순서를 A/B로 확인해 재생성했습니다.
  • 최신 devel의 조문 과검출 방지 정책과 충돌한 digest v2 구조 회귀는 실제 조문 HWP3 fixture로 교체했습니다.

현재 로컬 검증

  • 기준: upstream/devel fe9749d54를 조상으로 포함한 head b1e9619
  • CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=target/review-kevin9327-20260801 cargo test --profile release-test --tests — 통과
  • cargo fmt --all -- --check, git diff --check — 통과
  • CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=target/review-kevin9327-20260801 cargo clippy --all-targets -- -D warnings — 통과

원 PR별 review archive·통합 review implementation plan·오늘할일은 이 code candidate CI가 성공한 뒤, 하나의 문서-only trailing commit으로 같은 head에 추가합니다. 그 최신 head의 preflight와 Build & Test aggregate fast-pass를 확인한 뒤 merge를 판단합니다.

Merge 조건

  • code candidate와 문서-only head 각각의 필수 CI 성공
  • 원 PR별 review archive·통합 review implementation plan·오늘할일이 PR diff에 포함
  • 최신 CLEAN·MERGEABLE 상태 재확인

kevin9327 and others added 30 commits August 2, 2026 02:37
일괄 덤프식 요약 공급은 쪽·절 주소를 상실해 요약을 원문으로 되짚을 수 없고 분할에
소비자 상태 관리를 요구한다. 조판 엔진 보유를 계약으로 승격:

- --sections: sections:[{title,page,charCount,excerpt}] — 0 기준 글로벌 쪽 주소
  단조 비감소 보장, 잔여량 판정(charCount vs excerpt), 구조 부재 시 쪽 폴백을
  sectionsMode로 명시. build_structure 하위 트리 수집 — 새 파싱 로직 없음
- --pages a..b: 같은 폭 다음 창을 nextStep이 그대로 받아 적게 안내, 꼬리 조임
- MCP hwp_digest: devel 정식 optionalArgs({when,args})로 배선(자체 메커니즘 폐기)

검증: digest_v2_contract 11건 green, v1 8건 무회귀, cli_json_contract 22건, clippy 0.
실측: 16쪽 실문서 절 15개 주소 청킹 + 연속 창 증적 동봉.
M18~M20 이 공유하는 선행 결정을 고정: irSchemaVersion 진화 규약(추가=minor,
변경/삭제=major·회고 승인制), export-ir-schema 계약 초안, 표면 판단 매트릭스
(1차 권고: CLI 서브프로세스 래퍼 — mcp-serve 가 실증한 '얇은 껍데기' 원리로
언어가 늘어도 계약은 본체 한 곳), 파이썬 1호 골격(봉투→dataclass 기계 매핑·
판정 규약 승계), 마일스톤별 착수 조건·수용 기준. tech 지도 등재.
이름 환각은 경량 에이전트의 1순위 실패 유형인데, 미지 명령/도구가 교정 단서 없이
돌아와 맹목 재시도 루프의 원료가 됐다.

- CLI: 미지 명령 exit 2 stderr 에 힌트 1줄. 후보는 capabilities 명령 목록 단일
  출처(commands vec 을 capabilities_command_entries() 로 승격 — 동작 무변경)
- 레벤슈타인(의존성 0) + 임계 초과 무제안(오제안 0 원칙)
- MCP: 미지 도구 isError 를 {error:<원문>, didYouMean:[…]} 구조화 — 하위호환 유지

검증: did_you_mean_contract 3건 green, cli_json 22·server 6 무회귀, clippy 0.
실측: exprot-svg→'export-svg' 힌트, hwp_serch→didYouMean:[hwp_search] 원문 동봉.
hwp_batch/hwp_batch_search 를 paths 없이(또는 비배열·비문자열 항목으로) 부르면
자식 CLI 가 서버의 stdin — MCP JSON-RPC 스트림 자체 — 를 상속했다. 자식 batch 는
stdin 을 EOF 까지 '경로 목록'으로 읽으므로, 이후 클라이언트 프레임(ping 등)이
파일 경로로 소비되고(실측: os error 123 레코드에 ping 요청 전문이 박힘) 서버는
클라이언트가 stdin 을 닫을 때까지 wait_with_output 에서 행이었다.

- run_cli_tool: stdin 도구는 자식 실행 전에 paths 를 문자열 배열로 선검증 —
  부재/비배열/비문자열 항목/빈 배열 전부 즉시 도구 오류로 반환
- 그 외 자식 stdin 은 Stdio::null() 고정 — 어떤 자식도 프로토콜 stdin 을 상속 불가
- MCP_STDIN_TOOLS 상수 신설: capabilities --mcp 의 invocation.stdinTools 선언과
  서버 배선이 같은 목록을 공유 (종전엔 선언 따로, 'paths 가 배열이면' 간접 조건 따로)
- 계약 테스트 3종: 행-방지 타임아웃 하네스(회귀 시 hang 이 아니라 실패로 보고),
  형태 오류 3종 선거부, 정상 paths 무회귀 대조군

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
동일 표면 3중 감사(프로토콜/세션 상태/드리프트) 경위, 결함 5단계 해부,
BEFORE/AFTER 실측 타임라인·재현 변형 매트릭스, 수정 설계 대안 비교(A/B/C),
회귀 가드 설계, 검증 매트릭스, 한계·후속을 기록한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
정보 없는 오류는 맹목 재시도 루프의 원료다(#3630 실패 유형 4). tool_error 를
{error:<원문>, nextCall:{name,arguments,why}} 로 구조화(원문 보존 = 하위호환):

- 닫힌/모르는 핸들 8개 사이트 전부 → hwp_open 교정 호출 동봉
- 미지 도구 → didYouMean 최근접을 nextCall 로 병기 (#3694 단일 출처 재사용)
- nextCall.name 은 실존 도구만 — capabilities 선언 대조를 계약 테스트로 고정

검증: mcp_next_call_contract 3건 green, did_you_mean 3·server 6·session_edit 5
무회귀, clippy 0. 실측: 닫힌 핸들 fill → hwp_open nextCall 라이브 원문 동봉.
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>
"잘 됐겠지" 저장은 에이전트 실패 유형 2의 원료다. 편집 3종+세션 save 에
저장 바이트 즉시 재파싱→IR 대조 자기검증을 내장:

- edit fill-fields/replace-text/set-cell --verify → verify:{identical,diffCount}
  동봉, identical=false 면 봉투 출력 후 exit 3 (판정은 데이터·exit 와 무모순)
- 미요청 시 verify:null = 기존 소비자 하위호환
- 재파싱 실패는 identical:false+reparseError 로 보고 (저장물은 남긴다)
- 교차 포맷 저장은 strip_cross_format_noise 후 판정
- mcp-serve hwp_doc_save verify:true — 같은 헬퍼 재사용

검증: edit_verify_contract 4건 green, fill 7·replace 4·set-cell 5 무회귀,
clippy 0. 실측: 3계열 라이브 identical:true (evidence.txt).
 1-C)

DocLang v0.6 XML 소비 파이프라인용 기계 계약. 산출 축 패턴(#3596)·
export-hml(#3616)을 그대로 재사용한다 — 변환 동작 무변경, --json 에서만
stdout 순수 JSON 봉투 {schemaVersion,source,output,format:"doclang",
doclangVersion,bytes,assetsDir,assetCount,lossCount}, 실패 경로 stdout 비움.

- assetsDir 는 --assets-dir 를 준 경우에만 문자열, 아니면 null
- lossCount 는 사람용 "손실 보고 N건"의 기계 필드
- MCP 도구 hwp_export_doclang 을 단일 출처 mcp_tool_definitions() 에 등재
  — capabilities --mcp 선언과 mcp-serve 실행이 자동으로 함께 얻는다
- 계약 테스트 3건 (red 선확인: 구현 전 --json 은 exit 2) + quick-xml 소비 실측
- mydocs/manual/cli_commands.md 현행화, 처리 문서·실측 증적 동봉

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
다단 체이닝(호출 사이 상태 유실·중간 실패의 반편집 문서)이 에이전트 실패의
뿌리다. 절차 대신 의도(계획서)를 받는 3층을 신설한다:

- rhwp run <계획.json> [--json] + --plan-json 인라인 (MCP 경로)
- 정적 선검증(실행 0): 필드 존재·순번, 치환 일치 건수, 셀 좌표, □ 계수 —
  위반 전부를 invalid[{step,action,reason}] 로 한 번에 보고 + exit 2,
  출력 파일을 아예 만들지 않는다
- 원자 실행: 전 step 인메모리 IR 적용, 사후 단언(verify=#3702 재사용) 통과
  시에만 단 한 번 저장 — 실패 시 디스크 무변경(자연 트랜잭션), 단언 실패 exit 3
- 저널 봉투: step 별 결과 + verify + assertions 에코 — 판정은 전부 데이터
- 새 편집 로직 0: 판정자·적용자가 기존 edit 3종과 동일 함수
- MCP hwp_run_plan{plan}: capabilities 단일 출처 cmdTemplate 등록(서버 무변경)

검증: run_plan_contract 6건 green, cli_json·edit 4계열·mcp_server 무회귀,
clippy 0, fmt clean. 실측: 정상 저널·선검증 exit 2(출력 부재)·MCP 인라인 3계열.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`cargo fmt --all -- --check` 가 tests/run_plan_contract.rs:65 에서 걸려 Lint 잡이
멈췄고, Build & Test 워커 전체가 그 결과를 물려받아 skipped 로 떨어졌다.
동작 변경 없음 — 체이닝 어서션 한 건의 줄바꿈만 규범형으로 맞춘다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
문서 유래 문자열은 --json 봉투와 MCP 도구 결과를 타고 에이전트 컨텍스트에
'검증된 도구 출력'으로 들어간다. 그런데 에이전트는 누름틀 '이름'으로 채울 칸을
지목하므로, 화면상 같지만 바이트가 다른 이름 쌍이 있으면 엉뚱한 칸을 채우고도
봉투는 완벽한 성공을 보고했다:

  fields --json      → ['Total', 'Тotal']  (키릴 Т U+0422) 구별 정보 없음
  edit fill-fields   → filledCount:1, ambiguous:[], notFound:[]  ← 침묵

기존 ambiguous 판정은 '같은 이름 N회'를 세므로 바이트가 다른 쌍둥이에는 구조적으로
침묵한다. 한글 조합형/완성형('총액' NFC vs NFD)은 낯선 글자가 하나도 없어 더 현실적이다.

- document_core::text_security: 무의존 탐지기. 혼동 골격(동형자 접기 + 한글 음절
  산술 조합 + 보이지 않는 문자 제거), 혼합 스크립트, bidi/제로폭/ANSI 탐지.
  UTS #39 전체 표(728KB) 대신 실제 스푸핑에 쓰이는 글자만 담아 WASM 산출물 불변.
- fields --json: textSecurity 봉투(clean/warning). 소견이 없어도 키는 실린다 —
  '깨끗함'과 '옛 바이너리'를 소비자가 구별해야 한다.
- edit fill-fields / hwp_doc_fill_fields: confusable 판정 추가(무상태·세션 동형),
  사람용 경로에도 stderr 경고.
- capabilities.jsonContract.textSecurity 자기서술.

원칙: 보고만 하고 문자열을 고치지 않는다. 문서 엔진이 사용자 텍스트를 조용히
바꾸는 것은 어떤 보안 이득으로도 정당화되지 않는다(정당한 러시아어 인용문 손상).

오탐 시험: samples/ 351건 전수 스윕에서 348/348 clean (경고 0건).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
신뢰 경계 이동(무상태 CLI→MCP 서버), 이름 쌍둥이 실증(키릴·한글 NFC/NFD),
bidi/제로폭 통과, '공격이 아닌 것' 목록, 정화 대신 보고를 택한 4가지 이유,
무의존 선택 근거(WASM 산출물 728KB), 오탐 시험 출하 기준(348/348 clean),
한계·후속(#3709 P1~P3)을 기록한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HWP3 → HWP5 변환본의 FileHeader 가 버전 3 을 선언하던 규격 위반(#3676
조사에서 확정)을 수정한다. HWP3 파서는 version.major=3 을 메모리 전용
표시로 두고 저장용 HWP5 헤더(5.0.3.0)를 raw_data 에 넣는데
(serialize_file_header 의 raw_data 우선 규칙 전제), 저장 정규화
normalize_file_header_for_hwp 가 raw_data 를 무조건 버려 직렬화가 필드
경로로 떨어지고 major=3 이 그대로 디스크에 기록됐다.

- raw_data 를 버리기 전에 그 안의 5.x 버전(바이트 32..36 =
  revision/build/minor/major)을 필드로 회수, 회수 불가면 파서 기본값
  5.0.3.0 으로 실체화
- 이미 5.x 인 경로(HWPX 파서는 5.1.0.0 필드 직접 기록)는 무변경
- 압축 플래그 실체화는 유지 — raw_data 의 flags=0 은 회수하지 않는다
- AdapterReport.file_header_version_materialized 카운터 신설
- red→green: tests/issue_3706_hwp3_convert_file_header_version.rs 3건
  (red 에서 ①·③이 major=3 으로 실패 재현 → green 3 passed), 회귀
  hwpx_to_hwp_adapter 50 passed·모듈 단위 49 passed

한컴 열기 거부(#3676)의 근인은 아니다 — 버전 4종 실험으로 반증됐고 거부
근인 3종은 PR #3685 담당. 본 건은 그 조사에서 분리된 독립 규격 위반
정리다.

Closes #3706

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#3608 1-C 공백: dump-pages 는 조판 진단의 핵심인데 사람용 텍스트만 있어
에이전트가 pi=NN 을 정규식으로 긁고 있었다. 단건 JSON 봉투(#3237 규약)로
기계 계약을 추가한다.

- 봉투: schemaVersion/source/pageCount/pageFilter/respectVposReset/pages
- 항목 kind 6종(fullParagraph/partialParagraph/table/partialTable/shape/
  endnoteSeparator) + 텍스트 덤프의 진단 필드(vpos reset·rewind, line_seg
  요약, 분할 표 rows·cut, 미주 출처 #1082, #1700 계열 extras)를 구조화 노출
- 구역 전처리(미주 합침·items 밖 문단 귀속)를 page_dump_section_ctx 로
  추출해 텍스트/JSON 두 출력이 공유 — 드리프트 구조적 차단, 텍스트 출력
  바이트 동일 유지
- 실패 경로 stdout 0바이트, exit 계약(#2707) 유지; capabilities 광고 갱신
- 계약 테스트 4건 신설(red→green), MCP 커버리지 테스트는 1-D 원칙에 따라
  dump-pages 를 CLI 전용으로 제외

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
열린 PR 볼륨 상한(약 10건) 초과로 PR 개설을 보류하고 제목·본문 초안을 보존한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
에이전트는 편집 결과를 눈으로 못 본다. 표면이 "몇 쪽이 바뀌었나"를 지정해야
그 쪽만 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>
exit 3 은 convert/export-hwpx --verify 하나였다가 edit 3종 --verify(#3702)·
run 계획 단언(#3703)으로 넓어졌는데, capabilities 자기서술은 옛 문구에
머물러 있었다. 자기서술만 읽는 에이전트는 "편집에는 3이 안 나온다"고
오판한다 — 선언이 계약을 배신하는 지점이다.

- exitCodes.3 을 세 표면(convert·edit·run) 전부로 갱신
- 드리프트 가드 exit_code_dictionary_covers_every_verify_surface 추가:
  0~4 전 코드에 비어 있지 않은 설명 + exit 3 설명이 convert·edit·run 을
  모두 언급해야 통과 — 앞으로 exit 3 표면이 늘면 사전도 함께 늘어야 한다

검증: cli_json_contract 23건 green (신규 1 포함), clippy 0, fmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
classification_drift_is_blocked 가 실패했다. Lint(fmt) 가 먼저 걸려 shard 가
skipped 되는 바람에 직전 실행에서는 가려져 있었다.

이 함수는 조판 커버리지를 읽어 페이지 번호만 돌려주는 **조회**다. `&mut` 인
이유는 dirty 구역을 맞추는 paginate_if_needed 한 줄뿐이고 문서 IR 은 건드리지
않으므로, 같은 계열인 flush_deferred_pagination·repaginate_if_needed 와 같이
SessionState 로 등재한다.

검증: issue_2724_passthrough_invalidation_guard 5/5 green, fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
info --json 봉투(공유 함수 info_json_value)에 title 필드를 추가한다.
렌더된 페이지 텍스트의 첫 의미 줄(trim 후 비어있지 않은 첫 줄)이며,
표지가 이미지·빈 쪽이면 앞 3쪽(TITLE_SCAN_PAGES)까지 내려가고 그래도
없으면 null 이다. batch info 는 같은 함수를 쓰므로 자동 전파되어
대량 아카이브 대장화가 문서당 1-pass 로 끝난다. capabilities 매니페스트와
MCP hwp_info resultFields 광고도 함께 갱신한다.

계약 테스트 info_title_contract 5건: 표지 첫 줄·이미지 표지 fallback·
null·export-text 첫 의미 줄 동형·batch 전파.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR 제목: feat(#3407): info/batch 봉투에 best-effort title — 1-pass 대장화

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
컷이 빈 end_cut 으로 완료를 선언한 종료 조각에서, 렌더가 start_row 를
처음부터 재적층해 행 그리드보다 커지고(75544 pi=527: 766px vs 747px),
초과한 꼬리 줄이 "셀 하단 초과 줄 드롭"(다음 쪽 소속 줄 제외용)에 걸려
어느 쪽에도 렌더되지 않았다 — 이어받을 continuation 이 없어 영구 유실.

수정: NestedTableSplit 에 terminal(마지막 유닛까지 포함한 컷) 플래그를
도입해 종료 조각은
(1) 이전 쪽에 이미 보인 start_row 상단 밴드만큼 offset_within_start 를
    부여해 잔여 콘텐츠가 행 그리드 안에 들어가게 하고,
(2) 셀 하단 초과 줄 드롭을 적용하지 않는다 — 재적층 드리프트로 수 px
    넘치는 마지막 줄은 오버플로를 감수하고 렌더한다.

per-중첩행 컷 경로(table_partial)도 end_cut=[] 이면 종료 조각으로
표시한다. ignored 였던 회귀
nested_table_tail_paragraph_is_rendered 를 활성화(red→green 실증).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
근인(종료 조각 재적층 766px vs 행 그리드 747px → 셀 하단 드롭에 걸려
영구 유실)·해법(terminal 플래그: 기표시 밴드 오프셋 + 드롭 면제)·
red→green 검증·인접 회귀 무회귀·시각 전/후를 기록한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
열린 PR 11건(제한 10건)으로 개설을 보류하고 초안을 파킹한다.
볼륨이 빠지면 초안 그대로 gh pr create 하면 된다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kevin9327 and others added 10 commits August 2, 2026 02:37
…-shape·gen-table

hwp5-* 8종과 같은 뿌리(진입점 반환형이 유닛이라 exit_with 를 못 탐)지만, 이쪽은
본문이 제각각이라 갈래마다 판정이 필요해 따로 낸다.

- core-pages·measure-width: 인자 누락 2, 읽기·파싱 실패 1
- bench: 인자 누락이 0이었다(오타로 대상이 비면 '0개 측정 성공'). 파일 처리 실패는
  이미 1이었지만 process::exit 로 직접 끊던 것을 반환으로 바꿔 계약 하나만 지키게 함
- test-shape: 기본 경로 부재는 읽기 실패(1)다 — 인자를 안 준 게 아니라 기본값이 안
  맞은 것이라 사용법 오류로 안내하면 틀린 방향을 가리킨다
- gen-table: 전 인자에 기본값이 있어 인자 없음은 정상 실행(0 유지). 쓰기 실패가
  .expect() 패닉 101 이던 것만 1로 고침

계약 테스트 2종. gen-table 이 가드에 없는 이유를 주석에 남겨 나중에 잘못 추가되지
않게 했다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
레코드는 단건 convert --json 봉투 그대로다(실측 9필드 일치). 기존 batch 규약
(순서 보존·건별 error 레코드·panic 격리·--threads)은 무변경.

설계 결정 둘:
- 목적지는 --out-dir 필수. #3469 의 '입력 파일 옆'은 사용자가 그 파일을 직접
  지목한 단건 규약이라 폭발 반경이 1이다. batch 는 타이핑하지 않은 1,000건을
  한 번에 처리하고 아카이브는 읽기 전용인 경우가 흔하다 — 목적지가 명령줄에
  보여야 한다. 이름 충돌은 검출이 아니라 **쓰기 전 전건 사전 점검**으로 거부해
  절반만 변환된 산출 폴더를 남기지 않는다.
- 검증 차이는 실패가 아니다(변환·저장 성공, 산출물 있음). error 레코드도 failed
  계수도 아니고 레코드의 판정 필드로만 나간다. 집계는 1 > 4 > 3 > 0 — 3/4 를 1로
  접으면 소비자가 재실행 대상과 검토 대상을 못 가른다. 4가 3보다 앞서는 것은
  단건 convert 가 쪽수 검사를 IR 검사보다 먼저 하는 순서를 옮긴 것이다.
  단건과 갈리는 유일한 지점은 재파싱 실패 — 배치는 error 레코드 채널이 있고
  '열 수 없는 산출물' 은 판정 불가가 아니라 하드 실패다.

계약 테스트 5종. 첫 번째는 단건을 오라클로 두고 대조해, 샘플의 왕복 무손실
여부를 테스트가 미리 알 필요가 없게 했다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`hwp_split_document` 의 `from`/`to` 는 "0 기준" 이라 선언돼 있었지만 자식 CLI
`extract-pages` 는 **1 기준**이다(런타임 오류 문구가 그렇게 말한다). 실패가 아니라
**오답**이 나오는 것이 문제다.

    # 6쪽 재정통계 문서. 0 기준 2쪽은 "2010.2월 누계".
    hwp_split_document { from: 1, to: 1 }   # 스키마대로면 2쪽
    → 잘린 문서 첫 줄: "2010.1월 누계"      # 실제로는 1쪽

한 쪽 밀린 문서가 오류 없이 산출되고 에이전트는 요청대로 됐다고 믿는다. 그리고
스키마가 허용하는 `from: 0`(첫 쪽) 은 CLI 가 거부한다 — 선언을 그대로 따르면
첫 호출부터 실패한다.

`capabilities --mcp` 매니페스트는 외부 MCP 호스트가 **그대로 소비하는 공개 계약**이고
(mcp_integration_guide), `cli.args` 는 평문 자리표시자 치환으로 정의돼 있다. 여기에
산술(`{from+1}`)을 들이면 기존 소비자가 전부 깨진다. 그래서 번역 대신 **선언을
사실에 맞췄다** — `minimum: 1` + 설명에 기준 명시.

CLI 의 1 기준 자체는 이미 배포된 계약이라 건드리지 않는다.

rhwp 의 쪽 축은 거의 전부 0 기준이고 agent_knowledge_map 이 이를 불변식으로 적어
뒀다. `extract-pages` 만 예외인데 어디에도 적혀 있지 않았다 — cli_commands 의 명령
설명에도 기준이 없었다. 세 곳에 함께 명시한다:

- MCP 도구 설명·인자 설명 (에이전트가 읽는 곳)
- cli_commands.md 의 extract-pages 절
- agent_knowledge_map.md 의 "페이지는 0 기준" 불변식에 예외 한 줄

`mcp_split_page_base_contract` 2건:
- `split_document_declares_the_one_based_page_axis` — minimum 이 1 이고 설명이
  기준을 말하는지
- `declared_minimum_is_the_page_the_cli_actually_accepts` — 선언된 minimum 을 CLI 가
  실제로 받아들이고, 그보다 하나 작은 값은 거부하는지 (양방향이라 어느 쪽이
  드리프트해도 걸린다)

수정 전 스키마에 돌리면 2건 모두 실패한다(확인함).

검증: 신규 2/2, cli_json_contract 22/22, mcp_server_contract 6/6,
clippy 0, fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HWP3 문서를 HWP5 로 변환하면 미주 본문이 한 쪽씩 뒤로 밀렸다. 한컴은 원본과 왕복본을
같은 쪽에 싣는다(PDF 실측). SO-SUEOP 44쪽: 미주 128·129 가 rhwp 왕복본에서만 45쪽.

근인은 HWP3 파서가 세우는 pagination_bottom_tolerance = 1600 HU(21.3px)다. 한글97 의
마지막 줄 tolerance 를 흉내 내 페이지네이터에게만 여유를 주는 렌더러 내부 값이고 파일
포맷 필드가 아니라, HWP5 로 저장·재파싱하면 0 이 된다.

그만큼 본문 가용이 짧아지고(877.8 -> 856.4px) 미주 단 가용도 같이 줄어, 단 전환이 21.3px
일찍 걸린다. 이 문서는 미주가 2단인데 왼쪽 단이 3줄에서 조기에 닫히고 오른쪽 단만
아래로 늘어나, 같은 쪽에 미주 2개를 덜 싣고 다음 쪽으로 밀어낸다.

좁힌 경로는 기존 진단이었다. RHWP_ENDNOTE_BOUNDARY_DEBUG 로 미주 223개의 높이 누적을
대조하면 pi=1129 까지 소수점까지 같고 pi=1130 에서만 누적기가 0 으로 리셋된다. 높이
계산값(EN_SSOT)은 양쪽 동일하므로 측정이 아니라 단 경계 판정이 다르다는 뜻이고, 판정
입력을 찍으니 avail 만 877.8 vs 856.4 로 갈렸다(문서 전 구간 상수).

수정은 HWP3 출처 마커(/RhwpHwp3Origin)를 저장에 남기고 재파싱이 그 허용치만 되돌리는
것이다. HWPX 가 쓰는 /RhwpHwpxOrigin 과 같은 방식이다.

파일에 실리는 margin_bottom 은 건드리지 않는다. 처음에는 여백에서 1600 HU 를 빼는 방식으로
구현했는데 convert --verify 코퍼스 래칫이 파티션 0·2 에서 실패했다 - 재파싱 IR 이 원본과
달라져 저장 손실로 잡힌다. 허용치는 그 비교에서 제외되는 렌더러 내부 값이라 왕복 정합을
깨지 않는다. 여백을 줄이면 한컴이 보는 쪽 기하도 원본과 달라진다.

검증: 미주 128·129 가 44쪽으로 복원(한컴 정답지 일치), 본문 가용 877.8 복원, 한컴이
왕복본을 46쪽으로 정상 열기(원본과 동일). 게이트는 92셋 92/92, 10k 모집단 586건 전량
무변화, 표 코호트 22건 넘침 15,805 동일, 코퍼스 래칫 4/4, nextest 4,533 전량 통과.

회귀 테스트 3건은 허용치 복원·여백 불변·미주 쪽 배치를 계약으로 잡고, 마커 판정을 끄면
둘이 실패하는 것을 확인했다.

진단 2종을 함께 넣는다. RHWP_DIAG_ENCOL 은 미주 단 전환 판정의 입력을, RHWP_DIAG_AVAIL
은 가용 높이의 차감 내역을 찍는다. 둘 다 env 게이트, 동작 불변.

Refs #3707

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hp:offset의 u32 wraparound 부호화가 기존 shape_attr offset 발산 23행을 해소하면서 MAX_DIVERGENCES=2000 cap 안에 있던 HWPX 문단 ID raw_header_extra 발산 4행이 추가로 노출됐다.

두 줄 A/B 전체 스윕에서 원복본은 기존 baseline을 통과했고, 적용본 TSV는 offset 23행 제거와 raw_header_extra 4행(+2/+5/+2/+4)만 달랐다. #3184 선례에 따라 실제 직렬화 회귀로 취급하지 않고 관측 baseline을 재생성한다.
clippy::bool_assert_comparison 경고를 해소한다. JSON 필드가 boolean인지 확인하는 기존 계약과 실행 의미는 유지한다.
batch 인증·출력 충돌과 run 계획 좌표·동형문자 경고를 회귀로 봉인한다.

bench --batch 읽기 실패도 런타임 오류로 전파한다.
@jangster77
jangster77 force-pushed the review/kevin9327-20260801 branch from 616c889 to b1e9619 Compare August 1, 2026 18:09
@jangster77
jangster77 marked this pull request as ready for review August 1, 2026 18:41
@jangster77
jangster77 merged commit cc38291 into devel Aug 1, 2026
13 checks passed
@jangster77
jangster77 deleted the review/kevin9327-20260801 branch August 1, 2026 18:47
kevin9327 added a commit to kevin9327/rhwp that referenced this pull request Aug 1, 2026
devel 재기준 정리. `--` 구분자 자체는 edwardkim#3742 통합 머지로 이미 들어가 있어
(main.rs 의 end_of_options) 중복분을 걷어내고, 아직 없는 부분만 남긴다.

남은 결함: 검색어가 '-' 로 시작해 옵션으로 파싱되면 "알 수 없는 옵션: -회계"
로 exit 2 가 나는데, 이 메시지가 `--` 를 알려주지 않는다. 종료 코드 계약상
exit 2 는 "인자를 고쳐 다시 부르라" 는 뜻인데, 고치는 방법이 어디에도
드러나 있지 않아 에이전트가 멈춘다.

옵션 오타는 계속 거부한다 — 삼키면 오타가 검색어가 되어 조용히 0건이 된다.
거부는 유지하되 힌트 한 줄을 덧붙인다.

    알 수 없는 옵션: -회계
    힌트: 검색어가 '-' 로 시작한다면 `--` 뒤에 두세요 —
    rhwp search <파일> --json -- <검색어>

문서(mydocs/manual/cli_commands.md)도 시그니처를
`search <파일> [--json] [--ignore-case] [--limit N] [--] <검색어>` 로 고치고
사용법을 적는다 — devel 은 구현만 들어가 있고 문서화가 안 된 상태였다.

회귀 테스트: tests/search_dash_query_contract.rs
- 구분자 없이 '-회계' 는 여전히 exit 2 이고, stderr 가 `--` 를 안내한다
- `--` 뒤의 '-회계' 와 '-i' 가 검색어로 그대로 엔진에 닿는다
- `--` 가 평범한 검색 결과를 바꾸지 않는다
- MCP hwp_search 배선이 {query} 앞에서 옵션 파싱을 닫는다

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327 added a commit to kevin9327/rhwp that referenced this pull request Aug 1, 2026
devel 재기준 정리. 구현은 edwardkim#3742 통합 머지로 이미 들어가 있어 중복분을
전부 걷어내고 회귀 테스트만 남긴다.

devel 의 mcp_serve.rs 는 opt_u64 / opt_bool / req_u64 를 갖추고 세션 도구에
실제로 배선해 두었다 — page(802행), table·row·col(1034·1038·1042행),
caseSensitive(950·987행), keepStyle(1051행). 내가 올렸던 opt_u32 /
opt_bool(default) 판보다 범위가 넓다(파이썬 계열 호스트가 정수를 3.0 으로
직렬화하는 경우까지 받아 준다). 그래서 구현은 버린다.

다만 이 계약을 고정하는 테스트가 devel 에 없다. 계약이 깨지는 방향이
조용하다는 점이 문제다 — 인자 타입을 다시 and_then(as_u64) 관용구로 되돌리면
"없음" 과 "타입이 틀림" 이 None 하나로 합쳐지고, 잘못된 인자가 오류가 아니라
기본 동작으로 돌아온다. 실패가 아니라 거짓 성공이라 에이전트는 재시도조차
하지 않는다.

- hwp_doc_text { page: -1 } → 한 쪽을 달라고 했는데 문서 전체가 온다.
- hwp_doc_search { caseSensitive: "false" } → 요청과 반대로 실행하고 봉투에는
  true 를 적어 돌려준다.

tests/mcp_session_arg_typing_contract.rs — mcp-serve 를 실제로 띄워
JSON-RPC 로 검증하는 블랙박스 테스트다.

- page 에 -1 / 1.5 / "2" / true / [0] 을 주면 모두 isError. 생략은 전체,
  page:0 은 한 쪽이라는 기준선을 먼저 세워 판정이 공허하지 않게 한다.
- caseSensitive 에 "false" 를 주면 기본값으로 흡수하지 않고 거부한다.
- 좌표 인자는 생략과 타입 오류를 서로 다른 문구로 보고한다.

동작 변화 없음. 테스트만 추가한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327 added a commit to kevin9327/rhwp that referenced this pull request Aug 1, 2026
이 PR 의 변경과 무관하게 CI 가 계속 빨갛다. devel(cc38291) 자체가 edwardkim#3742 머지
이후 tests/batch_axes_contract.rs 의 stdin 헬퍼 때문에 깨져 있고, 그 커밋을
기반으로 하는 모든 PR 이 같은 지점에서 막힌다.

    thread 'batch_global_auth_options_are_rejected_before_consuming_path_stdin'
    panicked at tests/batch_axes_contract.rs:34:10:
    stdin 쓰기 실패: Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

재실행으로는 빠져나가지 못한다 — 이 PR 도 재실행했지만 같은 지점에서 다시
실패했다. 이 브랜치의 CI 를 읽을 수 있게 하려고 edwardkim#3766 의 테스트 하네스 수정을
그대로 싣는다.

근인: 이 헬퍼를 쓰는 테스트들의 계약이 "자식이 stdin 을 읽기 전에 거부하고
종료한다" 이다. 자식이 인자 검증에서 즉시 죽으면 파이프의 읽기 끝이 닫히고
부모의 write_all 이 EPIPE 를 받는다. 기능이 의도대로 일찍 거부할수록 더 잘
깨진다. EPIPE 는 오류가 아니라 검증 대상 동작의 정상적인 부산물이므로
ErrorKind::BrokenPipe 만 넘어가고 그 밖의 오류는 그대로 패닉한다.

제품 코드 변경 없음. 테스트 하네스만 고친다.

edwardkim#3766 이 먼저 머지되면 이 커밋은 빈 diff 가 되므로 리베이스 때 떨어져 나간다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327 added a commit to kevin9327/rhwp that referenced this pull request Aug 1, 2026
이 PR 의 변경과 무관하게 CI 가 계속 빨갛다. devel(cc38291) 자체가 edwardkim#3742 머지
이후 tests/batch_axes_contract.rs 의 stdin 헬퍼 때문에 깨져 있고, 그 커밋을
기반으로 하는 모든 PR 이 같은 지점에서 막힌다.

    thread 'batch_global_auth_options_are_rejected_before_consuming_path_stdin'
    panicked at tests/batch_axes_contract.rs:34:10:
    stdin 쓰기 실패: Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

재실행으로는 빠져나가지 못한다 — 이 PR 도 재실행했지만 같은 지점에서 다시
실패했다. 이 브랜치의 CI 를 읽을 수 있게 하려고 edwardkim#3766 의 테스트 하네스 수정을
그대로 싣는다.

근인: 이 헬퍼를 쓰는 테스트들의 계약이 "자식이 stdin 을 읽기 전에 거부하고
종료한다" 이다. 자식이 인자 검증에서 즉시 죽으면 파이프의 읽기 끝이 닫히고
부모의 write_all 이 EPIPE 를 받는다. 기능이 의도대로 일찍 거부할수록 더 잘
깨진다. EPIPE 는 오류가 아니라 검증 대상 동작의 정상적인 부산물이므로
ErrorKind::BrokenPipe 만 넘어가고 그 밖의 오류는 그대로 패닉한다.

제품 코드 변경 없음. 테스트 하네스만 고친다.

edwardkim#3766 이 먼저 머지되면 이 커밋은 빈 diff 가 되므로 리베이스 때 떨어져 나간다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327 added a commit to kevin9327/rhwp that referenced this pull request Aug 1, 2026
이 PR 의 변경과 무관하게 CI 가 계속 빨갛다. devel(cc38291) 자체가 edwardkim#3742 머지
이후 tests/batch_axes_contract.rs 의 stdin 헬퍼 때문에 깨져 있고, 그 커밋을
기반으로 하는 모든 PR 이 같은 지점에서 막힌다.

    thread 'batch_global_auth_options_are_rejected_before_consuming_path_stdin'
    panicked at tests/batch_axes_contract.rs:34:10:
    stdin 쓰기 실패: Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

재실행으로는 빠져나가지 못한다 — 이 PR 도 재실행했지만 같은 지점에서 다시
실패했다. 이 브랜치의 CI 를 읽을 수 있게 하려고 edwardkim#3766 의 테스트 하네스 수정을
그대로 싣는다.

근인: 이 헬퍼를 쓰는 테스트들의 계약이 "자식이 stdin 을 읽기 전에 거부하고
종료한다" 이다. 자식이 인자 검증에서 즉시 죽으면 파이프의 읽기 끝이 닫히고
부모의 write_all 이 EPIPE 를 받는다. 기능이 의도대로 일찍 거부할수록 더 잘
깨진다. EPIPE 는 오류가 아니라 검증 대상 동작의 정상적인 부산물이므로
ErrorKind::BrokenPipe 만 넘어가고 그 밖의 오류는 그대로 패닉한다.

제품 코드 변경 없음. 테스트 하네스만 고친다.

edwardkim#3766 이 먼저 머지되면 이 커밋은 빈 diff 가 되므로 리베이스 때 떨어져 나간다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
enigma-jerry72 pushed a commit to enigma-jerry72/rhwp that referenced this pull request Aug 2, 2026
devel 재기준 정리. 구현은 edwardkim#3742 통합 머지로 이미 들어가 있어 중복분을
전부 걷어내고 회귀 테스트만 남긴다.

devel 의 mcp_serve.rs 는 opt_u64 / opt_bool / req_u64 를 갖추고 세션 도구에
실제로 배선해 두었다 — page(802행), table·row·col(1034·1038·1042행),
caseSensitive(950·987행), keepStyle(1051행). 내가 올렸던 opt_u32 /
opt_bool(default) 판보다 범위가 넓다(파이썬 계열 호스트가 정수를 3.0 으로
직렬화하는 경우까지 받아 준다). 그래서 구현은 버린다.

다만 이 계약을 고정하는 테스트가 devel 에 없다. 계약이 깨지는 방향이
조용하다는 점이 문제다 — 인자 타입을 다시 and_then(as_u64) 관용구로 되돌리면
"없음" 과 "타입이 틀림" 이 None 하나로 합쳐지고, 잘못된 인자가 오류가 아니라
기본 동작으로 돌아온다. 실패가 아니라 거짓 성공이라 에이전트는 재시도조차
하지 않는다.

- hwp_doc_text { page: -1 } → 한 쪽을 달라고 했는데 문서 전체가 온다.
- hwp_doc_search { caseSensitive: "false" } → 요청과 반대로 실행하고 봉투에는
  true 를 적어 돌려준다.

tests/mcp_session_arg_typing_contract.rs — mcp-serve 를 실제로 띄워
JSON-RPC 로 검증하는 블랙박스 테스트다.

- page 에 -1 / 1.5 / "2" / true / [0] 을 주면 모두 isError. 생략은 전체,
  page:0 은 한 쪽이라는 기준선을 먼저 세워 판정이 공허하지 않게 한다.
- caseSensitive 에 "false" 를 주면 기본값으로 흡수하지 않고 거부한다.
- 좌표 인자는 생략과 타입 오류를 서로 다른 문구로 보고한다.

동작 변화 없음. 테스트만 추가한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b9b9c9c)
enigma-jerry72 pushed a commit to enigma-jerry72/rhwp that referenced this pull request Aug 2, 2026
devel 재기준 정리. `--` 구분자 자체는 edwardkim#3742 통합 머지로 이미 들어가 있어
(main.rs 의 end_of_options) 중복분을 걷어내고, 아직 없는 부분만 남긴다.

남은 결함: 검색어가 '-' 로 시작해 옵션으로 파싱되면 "알 수 없는 옵션: -회계"
로 exit 2 가 나는데, 이 메시지가 `--` 를 알려주지 않는다. 종료 코드 계약상
exit 2 는 "인자를 고쳐 다시 부르라" 는 뜻인데, 고치는 방법이 어디에도
드러나 있지 않아 에이전트가 멈춘다.

옵션 오타는 계속 거부한다 — 삼키면 오타가 검색어가 되어 조용히 0건이 된다.
거부는 유지하되 힌트 한 줄을 덧붙인다.

    알 수 없는 옵션: -회계
    힌트: 검색어가 '-' 로 시작한다면 `--` 뒤에 두세요 —
    rhwp search <파일> --json -- <검색어>

문서(mydocs/manual/cli_commands.md)도 시그니처를
`search <파일> [--json] [--ignore-case] [--limit N] [--] <검색어>` 로 고치고
사용법을 적는다 — devel 은 구현만 들어가 있고 문서화가 안 된 상태였다.

회귀 테스트: tests/search_dash_query_contract.rs
- 구분자 없이 '-회계' 는 여전히 exit 2 이고, stderr 가 `--` 를 안내한다
- `--` 뒤의 '-회계' 와 '-i' 가 검색어로 그대로 엔진에 닿는다
- `--` 가 평범한 검색 결과를 바꾸지 않는다
- MCP hwp_search 배선이 {query} 앞에서 옵션 파싱을 닫는다

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 97d9f09)
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.

3 participants