Skip to content

Latest commit

 

History

History
1101 lines (909 loc) · 95.9 KB

CHANGELOG-4.3.1-4.14.4.md

File metadata and controls

1101 lines (909 loc) · 95.9 KB

Changelog archive

Current releases

[4.14.4] - 2026-09-24

읽기 품질 2차 점검. PDF 표(쪽 넘김·칸 안 표·쪽을 넘는 칸), PDF 글(띄어쓰기·줄 이음·자간 숫자), 글자를 곡선으로 그린 PDF, OCR, 개인정보 마스킹(인명·주소)을 고쳤다. 원본 HWPX 를 정답으로 PDF 글을 재는 벤치를 새로 세워 게이트에 넣었다.

  • PDF 표 (hwpx↔pdf 430쌍 1,784표, 지난 판과 같은 모수): exact 90.1% → 92.9%, 셀 F1 0.945 → 0.957, 중첩표(157표) exact 63.7% → 72.0%. 텍스트층이 없는 13쌍(아래 벡터 글자)을 뺀 텍스트층 트랙(417쌍 1,738표): exact 91.7% → 94.5%, 칸 글 제자리(NED) 0.768 → 0.903, 중첩표(130표) 76.9% → 86.9%. 표 단위로 exact 를 잃은 표 0.
  • PDF 글 (HWPX 정답, 417쌍): 어절 F1 0.925 → 0.971, 읽기 순서 0.970 → 0.977, recall 0.992 → 0.994.
  • PDF 커버리지 0.99569 → 0.99760, 기준 미달 68 → 42건(남은 42건은 칸 배열 차 27·정답지 결함 15, 글 손실 0).
  • OCR CER 0.171 → 0.096(중앙값 0.079 → 0.040), OCR 표 셀 F1 0.440 → 0.618. 정답지인 텍스트층 파싱의 자간 숫자·조각 표 수정분이 포함된 값이다 (OCR 쪽만 고친 값은 CER 0.147).
  • 개인정보 마스킹 외부 정답(스키프트 473문장): 인명 0/411 → 354/411(오탐 0), 주소 0/141 → 141/141.

Changed

  • PDF 쪽 넘김 표 (src/pdf/table-parts.ts): 한컴은 글 없이 쪽을 넘은 칸 조각에 클립을 깔지 않는다. 이것을 쪼개진 행 증거로 써서 칸 안 문단 경계에서 넘어간 행을 합친다(편람 법령 대비표 119행 → 103행). 짝·홀 쪽 대칭 여백과 2단 지면으로 옮겨진 조각은 쪼개진 행이나 글 있는 반복 머리 행 증거가 있을 때만 잇는다(쪽마다 새로 놓인 Q&A 상자는 잇지 않는다). 한 줄로 끝난 이름표 뒤 다른 글은 새 행, 양쪽 맞춤 칸은 내어쓰기 자리로 문단 이어짐을 본다. 품목마다 서식을 되풀이한 문서는 새 서식을 새 표로 자른다(과제 품목 명세서 141×22 한 덩어리 → 5표). 쪽 경계에 걸친 세로 병합 칸 글을 잇고, 2단 지면에서 이은 표는 왼쪽 단 블록 뒤에 둔다.
  • PDF 칸 안 표·쪽 안 표 구조 (src/pdf/clip-cells.ts, src/pdf/cell-continuation.ts 신설): 표 위에 걸친 그림·글상자(워터마크, 결문표를 덮은 빈 1칸 표)가 일부 칸의 부모가 되어 한 표를 가르던 것. 셀 간격 표와 칸 클립을 아래 여백만큼 짧게 깐 표(결재문서 문서번호 표)를 칸 클립 변의 괘선으로 이웃으로 보고 표 단위로 틈을 닫는다. 쪽을 넘는 한 칸의 뒤 쪽 조각(이웃 없는 클립)을 앞 쪽 마지막 칸의 이어짐으로 판정해 그 칸에 글·표를 붙이고, 칸 안에서 쪽 경계로 갈린 표를 다시 잇는다(47쪽짜리 기준서 본문 칸의 중첩표 7개 중 6개 exact).
  • 비한컴 PDF (src/pdf/text-line.ts·line-extract.ts·table-grid.ts·cell-text.ts·cluster-detector.ts·columns.ts): 빈칸 위에 따로 얹은 숫자·괄호 런을 제자리에("제 차 전체회의" → "제4차 전체회의", MS Print To PDF·cairo·ezPDF). 행마다 끊어 그은 짧은 괘선 조각을 한 선으로 이어(칸 클립 격자가 없는 쪽만) 예산서 표가 여러 조각 표로 쪼개지던 것. 무괘선 표 칸 글을 선 표와 같은 공백 규칙으로("2 0 , 7 7 5 , 6 6 1" → "20,775,661"). 음영 칸에만 클립을 깐 표는 음영 조각 격자를 버리고 괘선 표로. 괘선 없는 칸 경계를 걸친 글자 단위 낱말은 한 칸에. 앞 조각에 1em 미만으로 이어진 조각은 열 후보가 아니다(본문 쪽이 행 인터리브 한 문단으로 뭉개지던 것).
  • PDF 띄어쓰기·줄 이음 (src/pdf/line-wrap.ts 신설): 본문을 시각 줄이 아니라 문단으로 복원한다. 한컴 텍스트층은 줄 끝 공백을 싣지 않아 글자 단위 줄나눔에서 어절 중간 꺾임("재⏎일학도의용군인")과 어절 경계("특별공로자의⏎배우자")가 기하로 구별되지 않는다. 조사·어미, 문서 안 어휘 증거, 한 음절 조각, 날짜 줄로 판정한다(HWPX 정답 22,911곳 92.9%). 칸 안의 8자 조각·쉼표 붙임 규칙(정밀도 10%·2%)을 꺾임 판정으로 바꿨고(법령 서식 칸 어절 중간 줄바꿈 7,190 → 1,794), 쪽 넘김으로 꺾인 문단도 잇는다.
  • OCR (src/ocr/ruling-lines.ts·line-split.ts·engine.ts·postprocess.ts): 점선 괘선과 채움 사각형 변(표 괘선에 맞물린 것만)을 래스터에서도 선으로 잡고 흐린 선까지 본다. 목차 리더 점을 떼어 쪽번호를 따로 인식하고 목차 한 줄을 한 아이템으로 잇는다. 숫자 앞 △▲ 복원, 배경 도안 환각("D D D…")·"(cid:)" 제거. 강건성 벤치(이미지 입력, 열화 9종) CER 0.190.23 → 0.0880.102. 쪽당 OCR 시간은 약 15% 늘었다.
  • 칸에 쌓인 온전한 숫자 두 줄(천 단위, 쉼표 없는 세 자리 이하)은 잇지 않는다("810" / "810" → "810810", "2,240" / "0" → "2,2400" 방지, 텍스트층·OCR 공통). 쉼표 없는 네 자리 이상 숫자가 줄바꿈에 세 자리 이하 조각으로 갈리면 잇지 않고 줄바꿈으로 남는다.

Added

  • 글자를 곡선으로 그린 PDF 감지 (src/pdf/vector-glyphs.ts, needsOcr 사유 vector_text): 한글을 채운 곡선 경로로 그린 쪽(rhwp cairo 렌더· 글꼴 윤곽 인쇄)은 텍스트층에 한글이 없는데도 종전엔 경고 없이 한글 빠진 결과를 냈다. 음절 모양 채움 경로의 글줄을 연산자 목록 기하로 찾아 OCR 을 켜면 그 쪽을 OCR 하고, 끄면 NEEDS_OCR 경고를 낸다. 그 쪽의 표는 래스터 괘선 대신 실제 벡터 괘선으로 복원한다. 코퍼스 16,775쪽 전수에서 한컴 PDF 1,480개는 오탐 0, 대상 13쌍 174쪽 중 167쪽 감지. 텍스트층 없는 13쌍 46표를 ocr:true 로 읽으면 exact 15 → 20, 칸 글 NED 0.281 → 0.445.
  • 개인정보 마스킹 인명·주소 룰 (name·address, opt-in: --rules·MCP rules 로 켠다; src/redact-name-address.ts): 인명은 문맥 게이트 (역할어·호칭·라벨·연락처 괄호·나열·표 머리글 "성명")와 성씨 사전으로 찾고 문맥 없는 맨이름은 잡지 않는다. 주소는 도로명·지번 문법과 행정구역 사전으로 찾아 시·도·시·군·구는 남기고 그 아래를 가린다("서울특별시 종로구 ●●●● ●●●"). 행정구역만 있는 글은 주소가 아니다. 실문서 5,000여 건 표본 검수 오탐 0, 파일 단위(HWPX·HWP 3,496파일) 재파싱 잔존 0. 공무원 결재란·담당자 이름은 정보공개 관행상 공개 대상이라 기본값에는 넣지 않았다. 파일 단위 마스킹은 본문에서 찾은 값을 마크다운 출력의 다른 자리에도 전파하고, 균등배분("홍 길 동")·두 칸 공백 꼴로도 찾는다.
  • 벤치: bench/pdf-text-gt.mjs(HWPX 정답 대비 PDF 글 recall·precision·순서·어절 F1, bench:gate 편입), bench/collect-schift.mjs(개인정보 외부 정답, Schift License 데이터라 커밋하지 않고 bench/corpus/schift/ 에 받는다), redact-bench.mjs --schift·--corpus-rules, redact-docs.mjs --rules. pdf-table-gt.mjs 는 텍스트층 한글이 HWPX 의 1% 미만인 쌍을 텍스트층 트랙에서 빼고 [텍스트층 없음·OCR] 보고 트랙으로 채점한다(bench:gate 는 --no-ocr). 게이트 플로어를 새 실측으로 올려 잠갔다(PDF 표 exact 0.94·NED 0.9·중첩 0.86, OCR CER ≤ 0.0985, 글 어절 F1 0.97 등).

Fixed

  • PDF 텍스트층의 리터럴 $ 를 \$ 로 담는다(IR 규약, "US $10 … US $20" 사이가 수식으로 사라지던 것).
  • PDF 마크다운 "# 목 차" 가 "#목차" 로 붙어 헤딩이 깨지던 것.
  • 벤치 mdToPlain·표 채점의 수식 정규식이 닫히지 않은 $ 뒤에서 기하급수로 역추적하던 것.
  • 개인정보 탐지가 호출마다 정규식을 복제하던 것(기본 룰셋 약 1.8배 빨라짐).

[4.14.3] - 2026-09-24

읽기 품질 전면 점검: 코퍼스를 넓히고(rhwp 표본 1,351파일·정책브리핑 hwpx+pdf 짝 200쌍) 포맷마다 정답지 벤치를 다시 세운 뒤, 표·표 안의 표·PDF·OCR·개인정보 마스킹을 고쳤다. 수치는 모두 새 모수 기준. 글자만 필요한 호출자(문서 검색 색인 등)를 위해 이미지 끄기와 상주 파싱 워커를 더했다.

Changed

  • PDF 표 복원 (src/pdf/table-parts.ts·table-meta.ts·table-trim.ts 신설, clip-cells.ts·page-blocks.ts·line-extract.ts): hwpx↔pdf 430쌍 1,784표 exact 64.1% → 90.1%, 셀 F1 0.790 → 0.945, 매칭 97.3%.
    • 쪽 넘김 표를 열 경계 합집합 격자로 잇는다. 한컴 PDF 는 쪽마다 그 쪽에 그려진 칸만 클립으로 깔아 조각마다 열 구성이 다르고, 앞 쪽에서 넘어온 세로 병합 칸은 뒤 쪽에 클립이 없다. 쪼개진 행은 앞 쪽 끝줄이 칸 오른끝까지 찼을 때만 합친다(가운데 정렬 칸 제외). 쪽 가장자리 장 표시 글만 끼어 있으면 인접으로 보고, 첨부 머리표(붙임·별지)와 쪽 끝 띠 밖 표는 잇지 않는다.
    • 후행 빈 열을 HWP 계열 builder 와 같은 규칙으로 자른다(칸 전체를 덮는 배경 그림은 내용 아님).
    • 칸 클립 묶음과 좌표가 같은 바깥 클립(글자처럼 놓인 표의 겉 클립)을 1×1 틀로 보지 않는다. 틀이 글을 다 가져가 안쪽 표가 빈 표로 버려지던 것.
    • 클립 없이 배경 채움만 그려지는 폭 3pt 안팎의 가장자리 빈 칸을 채움 사각형으로 되살린다.
    • 글 없는 표 조각(다음 쪽으로 넘어간 빈 마지막 행)을 쪽 넘김 잇기까지 살려 둔다.
    • 이웃 칸 판정을 변 공유 0.15pt·직각 변 정렬로 좁혀 캡션·글줄 클립이 표에 붙지 않게, 좌표 묶음을 0.3pt 로 좁혀 원본에서 다른 열 경계를 합치지 않게.
    • 1pt 안으로 겹친 글자는 콘텐츠 스트림 순서를 따르고(자간 줄인 숫자), 한컴의 ToUnicode 없는 글리프 자리표시 U+F000 을 지운다.
  • 표 안의 표 (PDF): 틀 칸 안의 감싸개 클립(칸 안쪽 여백·글상자)을 건너뛰어 중첩표를 틀 칸 blocks 에 넣는다. 중첩표 exact 65.9% → 80.2% (칸 안 표 91개 모수, 새 중첩표 트랙 157개 모수로는 63.7%).
  • HWP5 (src/hwp5/ir-assemble.ts 신설): 좌표 셀 표를 builder 로 배치해 HWPX 와 같은 표 계약(후행 빈 열 트림)을 따른다. hwp↔hwpx 쌍 표 exact 0.824 → 1, 셀 F1 0.916 → 1(3,593표). 머리말·꼬리말·각주·캡션 안 표 유실, 캡션 안 표가 바깥 표를 깨던 것, 글자처럼 취급 표 뒤 글 순서, 채움 탭 뒤 목차 쪽번호, 미기입 누름틀 안내문, 셀 각주, WMF·TIFF 그림, 문서 요약 정보(제목·지은이 등)를 고쳤다.
  • HWP3 (src/hwp3/table.ts 신설): 표를 셀 기하로 격자 복원(한컴 변환본 표 120개 앵커 일치), 개요 번호·각주·미주·머리말·꼬리말·숨은 설명· 자동 번호·사적 문장부호 8코드.
  • HWPX (src/hwpx/notes.ts·run-spans.ts 신설): 각주·미주 참조 부호와 주석 머리 번호, 캡션 번호, content.hpf 메타데이터(제목·지은이·설명· 키워드·날짜). 전 코퍼스 phantom 0.0047 → 0.00005, 표 exact·셀 F1·수식·각주 게이트 1.
  • OCR (src/ocr/line-split.ts·postprocess.ts·crop.ts·deskew.ts 신설): 기울기 보정, 세로로 이어진 키 큰 박스를 행 밴드로 갈라 인식, 사전에 없는 공문서 기호(○·△·따옴표) 복원, 박스 좌표를 잉크 경계로. CER 0.201 → 0.171(아래 PDF 경로 보완 포함), 표 셀 F1 0.409 → 0.440, 기울어진 스캔(+3°) 57.6% → 22.0%. PDF 경로 보완 3건: 과소분할 표 열 비교는 본문 행만(undersegmented.ts), 2단 판정 통계는 양쪽에 글 있는 줄만(cluster-detector.ts), 병합 칸에 쌓인 온전한 천 단위 숫자는 잇지 않음(cell-text.ts).
  • 개인정보 마스킹 (src/redact-rules.ts·redact-doc.ts·redact-hwpx.ts·redact-hwp5.ts·redact-scrub.ts 신설): 파일 안 모든 글자 저장소 (문단·표·글상자·머리말·각주·메모·필드 명령·속성·미리보기·요약 정보·압축 스트림·OLE 개체)를 같은 길이 치환으로 가리고, HWP5 미할당 섹터를 비운다. 창 안 라벨 전부 반영, 변경 추적 조각 결합, 목록 표기·UTF-16 항목·이미지 메타데이터. 5,081문서 오탐 387 → 0, 2,000행 명단 71초 → 2.2초.
  • src/roundtrip/hwp5-patch.ts: IR 이 후행 빈 열을 자르므로 열이 더 많은 스캔 표도 짝 후보로(표 셀 수정 매핑 실패 563표).
  • HWP5 쌍 벤치: 칸 각주는 HWPX 가 문단 블록 footnoteText, HWP5 가 칸 text 인라인으로 담으므로(칸 text·마크다운은 같다) 양쪽 다 각주 포함으로 맞댄다. 1,060쌍 표 3,739개 exact 1.

Added

  • 이미지 끄기 (ParseOptions.images: false, CLI --no-images; src/index.ts, src/pdf/parser.ts, src/pdf/image-extract.ts): 결과에 이미지 바이트를 싣지 않는다. images 를 비우고 블록 트리(자식·표 셀·캡션)의 imageData 를 떼며, HWP5 inlineImages 도 무시한다. 그림 자리 표시는 마크다운·블록에 남는다. PDF 는 PNG 인코딩을 건너뛰되 거르기·중복 판정은 같아 자리 표시도 같다(메모리 보호용 128MB 누적 상한만 걸리지 않아 그 뒤 그림도 남는다). 실측: changwon-plan2026.pdf JSON 200MB → 9.5MB·CPU 36.4 → 25.4초(자리 표시 외 본문 줄 9,756개 동일), gwd-info-plan.pdf 65MB → 7.2MB·CPU 19.4 → 14.9초. 검색 색인처럼 글자만 필요한 호출자용 (Anything 문서 검색 앱 연동).
  • 상주 파싱 워커 kordoc parse-worker (src/cli/commands-worker.ts): stdin NDJSON 요청 한 줄마다 한 줄 JSON 응답. result 는 --format json 과 같은 ParseResult 이고 파싱 실패도 success:false 로 담긴다. 응답에 rss 를 실어 호스트가 워커 교체 시점을 정하고, 시작 줄 {"ready":true,"version","protocol":1} 로 호환을 판단한다. pdfjs 등의 console 경고가 프로토콜 줄을 깨지 않게 stderr 로 돌리고, 종료 전에 stdout 을 비운다 (맥·윈도의 비동기 파이프에서 곧바로 exit 하면 마지막 큰 응답이 잘렸다, 테스트로 레드 확인).
  • 벤치: bench/collect-rhwp.mjs·collect-korea-kr-pairs.mjs(코퍼스 수집), bench/lib/geo-grid.mjs(PDF 표 기하 정답지: 폭 0 논리 열 접기), pdf-table-gt.mjs 중첩표 트랙·세트별 집계, bench/ocr-robust.mjs(열화 입력), bench/redact-bench.mjs·redact-docs.mjs(합성·코퍼스 마스킹).
  • 게이트 플로어를 새 모수로 다시 잠갔다(PDF 표 매칭 0.97·exact 0.90·F1 0.94·중첩 exact 0.63 등). bench:gate 에 마스킹·OCR 정확도 게이트 추가.
  • fuzz 멈춤·느림 한도를 문서마다 max(30s, 원본 파싱 × 3, 상한 180s)로. 깨끗한 원본도 35초 걸리는 367쪽 PDF 가 고정 30초에 걸리던 것.

Fixed

  • 본문의 $ 가 수식으로 읽히던 것 (HWPX·HWP5·HWP3; src/table/builder.ts escapeLiteralDollar): IR 글 규약을 정했다. 원문의 리터럴 $ 는 \$ 로 담고 $…$·$$…$$ 는 파서가 넣는 수식 스팬에만 쓴다. escapeGfm 은 이스케이프된 $ 에서 수식 스팬을 열지 않는다. 종전엔 셸 변수 "echo $HOME $PATH" 가 수식 스팬으로 보호돼 마크다운 렌더러에서 수식이 됐다(rhwp HWP3 변환본 4건이 HWPX recall 게이트 미달). \$ 는 CommonMark 백슬래시 이스케이프라 렌더 결과는 $ 그대로다. HWP5 는 필드 범위가 글자 위치라 디코딩 중엔 한 글자 표지로 받고 필드 처리 뒤 \$ 로 바꾼다(dollarMark). 라운드트립 역변환(unescapeGfm·unescapeGfmCell· normForMatch)과 마크다운→HWPX 인라인 파싱은 \$ 를 원문 $ 로 되돌린다.
  • 원문 역슬래시 뒤 구두점에서 역슬래시가 사라지던 것 (escapeGfm): "C:.Pls"·"cd !*"·"{} ;" 처럼 리터럴 역슬래시 뒤에 ASCII 구두점이 오면 CommonMark 가 이스케이프로 읽어 역슬래시를 지웠다(렌더러·채점 모두). 마크다운에서 \\ 로 낸다. IR 규약 이스케이프 \$·\| 는 그대로 두고, 라운드트립 역변환도 \\ 를 되돌린다. $ 결함에 가려 있던 rhwp HWP3 변환본 2건의 잔여 미스.
  • HWPX 미기입 누름틀 안내문이 본문으로 나오던 것 (src/hwpx/section-walker.ts): CLICK_HERE 필드가 수정 안 됨(dirty="0")이고 값 자리 글이 안내문 그대로면 한컴은 화면에만 흐리게 보이고 인쇄하지 않는다(rhwp form-01·form-02·issue1893, 한컴 PDF 에 없음). 블록 글에는 남기고(양식 채우기·패치가 원문 자리와 맞대도록) IRSpan.placeholder 로 표시해 마크다운에서만 뺀다. HWP5 의 미기입 안내문 제거와 같은 판정이다. 누름틀 채우기(fillHwpx)는 값을 넣으면 dirty 를 "1" 로 켠다. 한컴이 직접 채운 필드도 그렇고, 켜지 않으면 안내문과 같은 값을 넣었을 때(#3380) 한컴과 파서가 그 값을 미기입 안내문으로 본다.

[4.14.2] - 2026-09-23

Fixed

  • 공문서 항목의 첫 줄 내용과 둘째 줄 왼쪽이 어긋나던 것 (gen-gongmun.ts, gen-marker.ts, gen-section.ts, gen-header.ts): 부호 뒤 일반 공백을 양쪽 정렬이 늘리고, 압축(장평·자간)이 부호 폭을 줄이고, 내어쓰기를 함초롬 폭표로 재던 세 가지가 겹쳐 ○ 항목은 첫 줄이 오른쪽으로, ※ 참고는 둘째 줄이 3pt 왼쪽으로 어긋났다. 부호를 따로 된 run 으로 내고 뒤에 탭을 두며, 문단은 한글의 "내어쓰기용 자동 탭"(tabPr autoTabLeft="1")을 참조한다. 탭 정지점이 내어쓰기 위치에 서므로 첫 줄 내용과 둘째 줄이 폭 추정과 무관하게 같은 x 에서 시작한다(법제처 서식 "(a)⇥" 실측으로 확인). 기안문·보고서·계획서·통지·회의록·업무보고와 개조식·보도자료 전부.
  • 글자 단위 줄바꿈이 낱말을 쪼개던 것("동대/문", "실/국", "안/전"): v5 본문·요약박스를 어절 단위(BREAK_WORD)로 되돌리고 외톨이줄 보호를 켰다. 긴 어절이 넘어가 앞 줄이 벌어지는 문제는 새 fitParagraph 가 자간 조정으로 완화한다.
  • 날짜가 줄 끝에서 갈라지던 것("2026. 1. / 22."): 날짜·시각 범위·금액 "원" 앞 공백을 묶음 빈칸(<hp:nbSpace/>)으로 낸다 (gongmun-typo.ts). 곧은따옴표는 ‘ ’ “ ” 로 바꾼다(휴먼명조에서 여닫음 없이 ’ 로 그려지던 것).
  • 고아 줄 압축이 과하던 것: 폭 추정 오차(한컴돋움 · 를 1.0em 으로 재던 것 등)를 90% 목표로 메우느라 한 줄을 12~20% 눌러 옆 줄과 밀도가 달라 보였다. 실글꼴 폭표로 정확히 재고, 무압축과 압축 사다리 가운데 조판 비용(벌어진 줄·고아 줄·압축량)이 가장 낮은 조합을 고른다.
  • 파서가 묶음 빈칸을 버리던 것: <hp:nbSpace/> 를 공백으로 읽는다(종전 "2026.9." 로 붙었다). 탭이 있는 문단도 인라인 강조(굵게) 왕복 복원이 된다.
  • PPTX 가 HWPX 파서로 흘러가 "섹션 파일을 찾을 수 없습니다"로 실패하던 것 (#80, PR #81 @LimePencil): detectZipFormat 이 pptx 를 감지하고 parse() 는 UNSUPPORTED_FORMAT 을 낸다. MCP fill_form(hwpx-preserve)·patch_document 도 "감지된 포맷: hwpx" 대신 실제 포맷을 안내한다(XLS 를 HWP 로 받아들이던 것도 거부). 내부 구조로 판별이 안 되는 컨테이너(손상 등)는 종전대로 HWPX/HWP 처리기에 넘긴다.
  • HTML 표가 직전 GFM 표의 폭 클래스를 물려받고, 열폭을 표 글꼴이 아닌 함초롬 폭으로 재던 것 (gen-table.ts).
  • 한 줄에 맞춘 문단이 한컴에서 넘치던 것·짧은 꼬리 줄: 압축으로 한 줄에 맞출 때 여유를 0.5% 에서 2% 로 늘렸다(한컴 저장본 코퍼스 대조: 실폭표 모델이 틀린 문단은 대부분 폭 ±0.2~3% 보정으로 맞는다. 여유 0.6% 로 맞춘 문단 2건이 한컴에서 넘친 실사고). 마지막 줄이 절반 이하인 문단은 압축 15% 이내면 한 줄 줄인다("…안양 / AI전략국(2026. 1.)" 류). 줄인 결과에 벌어진 줄이 생기면 줄이지 않는다.
  • 한 줄보다 긴 어절 앞 줄이 크게 벌어지던 것(공백 없는 목록 "구(강남·강서·…·종로)가"): 한컴은 이런 어절을 다음 줄로 넘긴 뒤 쪼갠다(코퍼스 어절 단위 문단 400개 대조, 줄바꿈 모델에 반영). 그런 문단은 글자 단위로 조판하되 줄 끝이 공백이나 목록 구분자 (· , 、 /) 뒤에만 오고 폭 ±1% 에서도 같은 자리에서 끊기는 압축을 고른다(fitCharBreaks, 없으면 어절 단위 유지).
  • 경계 사례: 인용문 **강조** 내용 의 별표를 참고 부호로 떼던 것, 두 자리 h2 번호(10.)의 탭이 기본 탭 칸(40pt)까지 튀던 것 (공백으로 잇는다), 개조식 표지·목차·장 제목에 묶음 빈칸이 문자 U+00A0 로 남던 것, 문단 압축이 autoFit.minRatio(기본 90)보다 납작한 장평을 쓰고 autoFit: false 에서도 ※ 참고를 압축하던 것, 업무보고 항목 띠(한 줄 요소)가 두 줄로 꺾이던 것, 링크·코드 바로 뒤 따옴표와 태그 뒤 따옴표의 여닫음.

Changed

  • 실글꼴 폭표 (font-metrics.ts): 한컴오피스 번들 TTF 8종의 advance. 참조 PDF(한글 2024) 본문 154줄의 줄바꿈을 전부 재현한다. 생성 fit·표 열폭(짧은 열 여유 12% → 3%)·요약박스 줄 수와 렌더에 쓴다.

  • 렌더: 탭 폭을 탭 정지점으로(내어쓰기용 자동 탭·기본 40pt), 마지막 탭 앞 글은 양쪽 정렬에서 빼고, 줄 끝 공백은 정렬·그리기에서 제외한다. reflow 는 글자마다 제 charPr 폭으로 재고 글자 단위 모드에서도 ASCII 연속열을 한 단어로 둔다. 묶음 빈칸에서는 줄을 끊지 않고, 한컴 저장본의 탭은 저장된 폭을 쓰며, 탭 정지점은 첫 줄 들여쓰기에서 잰다. verify-reflow 평균 일치 97.76% → 99.07%.

  • CI npm ci 가 onnxruntime-node CUDA 바이너리 다운로드(api.nuget.org) 타임아웃으로 실패하던 것: CPU 추론에는 필요 없어 ONNXRUNTIME_NODE_INSTALL=skip.

Refactored

  • 1,200줄 넘던 네 파일 분리: mcp.ts(1,308 → 52, 도구는 mcp/tools-*.ts), cli.ts(1,390 → 202, 명령은 cli/commands-*.ts), hwp5/parser.ts(1,405 → 550, 본문은 hwp5/body.ts), render/svg-render.ts(1,252 → 1,010, 문단 모델은 render/para-model.ts). reflow ↔ svg-render import 순환도 끊었다. 동작 불변(bench:gate 전 체인). 테스트 1,783.

[4.14.1] - 2026-09-21

Fixed

  • 섹션 폴백(pageMode: "section")에서 표 셀 안 블록의 쪽번호가 조판 잠정값으로 남던 것 (src/hwpx/parser.ts, #78 · PR #79 @LimePencil): 조판 캐시가 없어 섹션 근사로 떨어질 때 최상위 블록만 섹션 번호로 덮고 table.cells[][].blocks·captionBlocks·children 은 건드리지 않아, pageCount = 1 인데 셀 안 문단·이미지는 2~6쪽을 주장하는 두 좌표계가 섞였다. 블록 트리 전체를 소유 섹션 번호로 통일. layout 모드 산출물은 불변, 마크다운·표 기하도 그대로.

[4.14.0] - 2026-09-15

Added

  • 중앙부처 업무보고 프리셋 ministry(별칭 업무보고·부처업무보고·중앙부처보고서) (src/hwpx/gen-frame-ministry.ts, gongmun-scheme.ts ministryScheme, outline.ts, gen-gongmun.ts): 재정경제부 「2차 업무보고 서면보고자료」(2026-07-15, 17쪽) PDF 를 글꼴·크기·색·도형 좌표까지 전수 실측해 만든 국회 서면보고형 골격 — # 제목=표지(대외주의 빨강 박스 --cover-label, 파란 바 #0C3DCA·#0A33A9 두 줄, HY헤드라인M 32, 날짜 24), 목차 박스(Ⅰ. 17 / 1. 15, 쪽 82% 높이), ##=Ⅰ. 장 띠(세로 그라데이션 #D6EAFE→흰 + 파랑 이중선, 장마다 새 쪽), ###=파란 숫자칸(#3057B9) + 밑줄 제목 절 띠, ####=남색(#203A7B) 소제목 박스, #####=① 하늘색 항목 띠(#E8F7FC, 상하 #00ACFF, HY중고딕 15), > ▪ …=연노랑(#FFF7CC) 성과 요약박스(위치 무관, * 줄은 맑은 고딕 11 각주), ## 별첨 …=별첨 띠(#0066FF). 본문은 함초롬바탕 15 전 단계 동일·145%·ㅇ 1타·- 3타·문단 뒤 6pt, * 각주 맑은 고딕 12, 표 맑은 고딕 12 헤더 #DFE6F7. □·❶ 뒤 (키워드)·[키워드] 는 파랑(#0000FF) bold, ㅇ 뒤는 검정 bold — 원본이 색으로 위계를 내는 관행. ❶❿·➊➓·⇒·↳·☞ 선두 글리프는 그대로 보존. 여백 위·아래 10 + 머리말·꼬리말 10mm(본문 시작 20mm), 좌우 20, 쪽번호 "- 1 -"(표지·목차 숨김). 한글 2024 COM 실렌더 7쪽으로 원본과 대조 확인. 실측표는 docs/gongmunseo-engine-spec.md (j)장.
  • cover.label / --cover-label / MCP cover_label: 표지 우상단 취급 표시("대외주의"·"비공개") — 빨간 0.12mm 테두리 박스.
  • 아웃라인 옵션 headingFrames(h3~h6 을 항목이 아닌 heading 노드로, 리스트 깊이 불간섭 — 부호 없는 - 리스트는 직전 명시 부호 항목의 한 단계 아래에서 시작), quoteBox(인용문 어디서나 요약박스), keepMarkers(❶⇒↳ 보존). 다른 프리셋은 켜지 않아 산출물 불변.
  • 표 셀 테두리 스펙에 색·굵기 직접 지정 (gen-table-bf.ts EdgeKind | BorderSide) — 파랑 이중선·하늘색 0.4mm 같은 색 띠 표. gradation 은 type: "LINEAR", angle 지정 가능.

Changed

  • gradation 은 2색으로 고정 (gen-ids.ts borderFillEntry): 3색(colorNum="3")은 한글 2024 COM 실렌더에서 셀이 검게 칠해진다 (A/B 4종 실측). 색을 2개로 자르고 step="50" 방출. 종전 RADIAL 2색 제목박스 바는 바이트 동일.
  • 문체 검수(munche-lint.ts): 줄마다 부호(▪□ㅇ-*※)로 시작하는 인용문은 리드문이 아니라 성과 요약 박스 — LEAD_ENDING·LEAD_LONG 밖.
  • inlineMapper(굵게 → 레지스트리 변형)를 style-registry.ts 로 옮기고 emphasisColor(굵게의 색) 인자 추가. gen-frame-seoul.ts 의 ftbl·host 골격 헬퍼를 export.

[4.13.2] - 2026-09-14

Fixed

  • 한 쪽을 넘는 표가 쪽 경계에서 나뉘지 않고 잘리던 것 (src/hwpx/gen-table.ts): 생성 표는 글자처럼 취급 (treatAsChar="1")이었고, 한글은 글자처럼 취급 표를 한 줄의 글자로 보아 pageBreak="CELL"이어도 나누지 않는다 — 표 전체가 다음 쪽으로 넘어가 앞 쪽이 비고, 한 쪽보다 큰 표는 하단이 쪽 밖으로 잘렸다(한글 2024 COM 실렌더. 한컴 저장본 표를 90행으로 부풀려도 동일하고, pageBreak·textWrap·flowWithText·행높이·헤더 반복·호스트 줄간격은 모두 무관, treatAsChar="0"으로만 셀 단위 분할과 헤더 반복이 동작). 2행 이상 최상위 표(GFM·HTML 경로)를 부유 앵커(treatAsChar="0" + textWrap="TOP_AND_BOTTOM" + PARA/COLUMN, 실측 모드는 horzAlign="RIGHT")로 바꿨다. 1행 표(밴드 박스·제목 박스)와 중첩표는 나눌 일이 없어 글자처럼 취급 유지. 확산계획서형 15행·30행 표가 한글에서 10쪽으로 정상 분할·헤더 반복되는 것을 확인. 부유 표는 본문이 표 바닥에 바로 붙고 다음 문단의 '문단 위' 간격도 흡수되므로(띠 제목이 괘선에 밀착) 아래 여백 outMargin bottom을 한 줄 피치(글자높이×1.6)로 둔다.
  • 렌더: 셀 성장 표(부유)가 뒤 문단과 겹침 (src/render/svg-render.ts): 부유 개체의 밀어내기 역산이 선언 hp:sz 높이를 써서, reflow 가 실효높이로 민 만큼 표가 아래로 내려가 뒤 문단 위에 겹쳤다 — 인라인 표와 같은 실효높이 사용.

[4.13.1] - 2026-09-06

#75 후속: HWP5(.hwp) 레이아웃 렌더. v4.13.0 의 렌더 통합 계층(RenderScene)이 HWPX 만 받던 것을 HWP5 어댑터로 넓혔다. 한컴이 저장한 HWP5 의 조판 캐시(PARA_LINE_SEG)·표(TABLE/LIST_HEADER)·개체(CTRL_HEADER·SHAPE_COMPONENT)를 HWPX 와 같은 뜻의 section DOM 으로 합성해 같은 렌더러에 넣는다. 같은 문서의 hwp↔hwpx 10쌍(bench/corpus/pairs)에서 페이지 수·페이지 크기·표 수· 표 bbox·괘선 굵기 분포가 HWPX 렌더와 일치한다(레코드 레이아웃은 pairs 의 header.xml 대조로 확정: BORDER_FILL 변 @2+6k, CTRL_HEADER textWrap bit21-23, PAGE_DEF 가로 bit0).

Added

  • HWP5 렌더 어댑터 (src/render/hwp5-scene.ts, #75 Task 7): renderDocument·renderDocumentToScene·extractRenderedRegions· extractTables 와 CLI render·crop·tables, MCP render_document·crop_regions 가 .hwp 를 받는다. RenderScene.format 은 "hwp". 문단·표·이미지·도형 region 과 결정적 id 는 HWPX 와 같은 규칙. 표 sourceId 는 문서 순번 t{N}(src/hwp5/table-ids.ts) 이고 파서 IRTable.sourceId 도 같은 프리패스로 매겨 extractTables 의 region·crop 조인이 HWP5 에서도 된다(종전 "분류만" 경고 제거). 그리기 개체는 그림·사각형·타원·선·다각형·곡선·글상자·묶음, 수식·OLE 는 경고. 암호 문서는 password 옵션.
  • MCP extract_tables (도구 17개): 표 분류(semantic-table / non-tabular-layout / uncertain) + 페이지·bbox + 정책별 crop (응답 8장·output_dir 저장·tables.json). CLI tables·API extractTables 와 같은 결과.
  • MCP render_document 포맷 확장: format 에 jpeg·html·pdf 추가(svg·html·pdf 는 output_path 필수, 페이지별 산출은 _page_NNN 접미), max_width_px. HWPX png/svg 의 pages 미지정 세로 스택 동작은 그대로.
  • 띠 제목 색 옵션 bandColor/bandTextColor(CLI --band-color/--band-text-color, MCP band_color/band_text_color): 기본 #003366/#FFFFFF, 교육청형 밝은 띠 #DFE6F7+#000000. #RRGGBB 아니면 KordocError.

Changed

  • HWP5 파서 진입부를 컨테이너 계층(openHwp5Container·readHwp5DocInfoRecords·readHwp5SectionStreams·readHwp5SectionRecords· readHwp5BinData)으로 분리하고(동작 동일) svg-render 를 포맷 무관 단계(renderSectionRoots·assemblePageSvgs)로 나눴다.
  • 개조식 문단 위 간격 재검증(서울·gate-fill 428건, hp:case 우선. hp:default 는 2배 함정): □ 0 96%·ㅇ 0 84%·- 0 90%. 엔진 값 (□ 300/그 외 0)은 유지하고, 구 주석의 "3000/2000/1200/600 실측 스펙"과 scripts/style-digest.mjs 의 default 우선 읽기를 고쳤다. docs/gongmunseo-reference.md 2.8 에 □ 앞 간격 10pt 실렌더 근거, 2.9 에 띠 색 옵션을 적었다.

Fixed

  • crop 이 표 영역의 좌상단 1/4 만 잘리던 결함(v4.13.0 HWPX 포함): sharp 0.35·libvips 8.18·rsvg 2.62 는 pt 단위 SVG 에 density 배율을 제곱으로 적용해(144dpi → 4px/pt) 래스터가 보고 배율(2px/pt)의 2배로 나왔고, cropRect 가 보고 배율로 픽셀을 환산해 어긋났다. rasterize.ts 가 프로세스당 1회 프로브로 배율 지수를 재고 산출 픽셀 크기·scale 을 실제 이미지에서 읽는다. render_document PNG 도 이제 max_width_px(기본 1400) 를 실제로 지킨다.

[4.13.0] - 2026-09-06

공문서 생성 엔진 재설계(v5). 서울 정보소통광장 결재문서본문 629건(기존 429 + 신규 7종 필터 280)을 전수 실측해 위계·글꼴·들여쓰기·골격을 gongmun-scheme.ts 하나에 박고, 기안문·보고서·계획서·통지·회의록 생성을 새 엔진 gen-gongmun.ts(아웃라인 → 스킴 → 레지스트리)로 갈아탔다. 계기는 실무 검토보고서 실렌더 — □ 두 줄 꺾임, 1. 검토 배경(h2)·가.(h3)·리스트 □ 가 위계 없이 뒤섞임(□ 가 가. 보다 왼쓸림), □·ㅇ 글꼴이 중앙부처 양식 (HY헤드라인M·휴먼명조)이라 서울 실결재(HY견고딕 17b·한컴돋움 15b·휴먼명조 14)와 다른 것.

Added

  • v5 엔진 (src/hwpx/gen-gongmun.ts·outline.ts·gongmun-scheme.ts·style-registry.ts·fit-line.ts·gen-frame-seoul.ts) — official/report/plan/notice/minutes 전담. #/##/###·리스트 깊이·명시 부호(□ ㅇ - ※ 1. 가.)를 하나의 depth 로 정규화해 입력 형태와 무관하게 같은 위계를 낸다. charPr/paraPr/글꼴은 레지스트리 동적 발급(손계산 id 파티션 없음). 개조식(gaejosik)·보도자료·범용은 gen-section.ts 유지.
  • □·제목·장 제목 한 줄 강제 (fit-line.ts) — 장평 100→85(자간 0/-3/-5) → pt 축소(□ 하한 14·제목 20). 실측 □ 150건 중 한 줄 95%, 장평 96/95·자간 -4/-5 관행. 못 담으면 MarkdownToHwpxOptions.warnings 로 경고 (CLI stderr·MCP 응답). 표 셀도 12→11→10pt 자동 축소(requiredTableWidth, 실측 셀 pt 10:91·11:72·12:55).
  • 서울 실결재 골격표 (gen-frame-seoul.ts) — 기안문 두문표(6행: 슬로건 굴림 10·기관명 굴림 20b 자간띄움· 수신/(경유)/제목 굴림 14, 제목행 하변 0.12mm)·결문표(48열 격자: 발신명의 18b·회색 띠 #CCCCCC·결재선 직위 10/ 성명 11b/일자 9b·협조자·시행/접수·우·전화/전송/이메일/공개구분 굴림 10, 직위칸은 내용 폭), 보고서 제목표 (HY헤드라인M 25b 상 0.4mm·하 0.15mm + 담당자 행 휴먼명조 12)·요약박스(#DFE6F7 0.4mm 한컴돋움 15b)·결재선표· 표지(문서정보표·결재선·파랑 띠 제목·기관명·부서). 텍스트 '─' 룰·문단형 두문·결문은 제거.
  • 옵션 — docHead.slogan, docFoot.approvers[]·recipients·zip, summary(또는 제목 직후 인용문), docInfo, cover.dept/--dept, h2Marker: "roman"(보고서 기본 Ⅰ. Ⅱ.). CLI --summary --doc-info --dept, MCP summary·doc_info·dept. reportInfo 는 보고서에서 제목표 담당자 행.
  • 코퍼스 — bench/corpus-gen/opengov-2609/ 7종 필터(plan·report·policy·notice·meeting·y2024·y2022) 280건. 실측 결과는 docs/gongmunseo-reference.md 2.8, 설계는 engine-spec (i)장.
  • 금액 한글 병기값 계산 (src/shared/numbering.ts hangulAmount) — 규정 시행규칙 제2조(아라비아 숫자 다음 괄호 안에 한글). 4자리 그룹 × 만·억·조·경·해, 그룹 안 천·백·십, 숫자 영일이삼사오육칠팔구, 공문 관행대로 일십·일백·일천·일만의 "일"을 생략하지 않는다(금113,560원(금일십일만삼천오백육십원)). MONEY_NO_HANGUL 경고가 고정 예시 대신 걸린 금액의 실제 병기값을 제안.
  • 하이픈·ISO 날짜 룰 DATE_HYPHEN — 2026-07-18 꼴은 편람 날짜 표기(온점 구분, 온점 뒤 한 칸)가 아니다. warning, 제안은 실제 변환값 2026. 7. 18.(0 패딩 제거). URL 경로·파일명·영숫자 접두(v2026-07-18)·표 셀·펜스 코드는 제외.
  • outline 항목 부호 확장 — ❑·❏·ㅁ(대항목), ◎(중항목), ㅡ·‣·▪·▫(세부) 를 □/ㅇ/- 계열로 인식, 개조식 자동감지에도 반영. 영문 o/O/0 은 영문 문장 선두 오탐이라 넣지 않는다. 테스트 gongmun-lint-amount-date.test.ts(5)·outline-markers.test.ts(3).

Changed

  • 실무자 요청 5건 — 출처:·자료:·근거: 항목은 ※ 참고 13pt(당구장표시, 작은 글씨) · 둘째 줄 ≤20% 고아 줄은 자간 -1%씩(Shift+Alt+N 관행) → 장평 조합으로 한 줄(fitOrphanLine) · 줄바꿈 어절 단위(라운드 2에서 글자 단위로 전환, 아래) · 법령 코드 (282791)·KOSIS DT_…·(법정동코드 …)·"○○ MCP 조회" 도구 언급 제거(stripLawCodes) · 요약박스(제목 직후 > 한 문장 3줄 이내 "…하고자 함", 부호 없이 — 보고서는 필수, 없거나 3줄 초과면 warning, 꼬리 고아 줄 자간 축소).

  • 프리셋 기본값(서울 실측) — 기안문 본문 굴림체 12 160%, 보고서·계획서 여백 13/13/18/18 머리·꼬리 13mm·줄간격 180%·bullet2 ㅇ(개조식만 ○)·h2 = 장(Ⅰ.)·h3 = □·리스트 depth+1. 통지·기안문의 h2 는 법정 1. 항목이 되고 아래 리스트는 가.부터. □ 앞 빈 줄 한 줄(실측 76%), 붙임 앞 한 줄.

  • faceClassForGen(text-metrics.ts) — 생성 전용 고딕 폭 클래스(대문자 0.72·숫자 0.58·괄호 0.35em).

  • 라운드 2 (실렌더 결함 3건, 같은 날 밤) — (1) v5 본문 줄바꿈을 글자 단위(KEEP_WORD)+양쪽정렬로: 라운드 1의 어절유지+양쪽정렬은 실결재 개조식 - 문단 1,023개 중 1.2%뿐(글자 단위 76%, 다줄 88%)이라 긴 어절이 통째로 밀리며 앞 줄 어절 간격이 벌어졌다. 라틴·숫자는 계속 단어 유지, 구 경로 (범용·개조식·보도자료)는 어절 유지 불변. fitOrphanLine·요약박스 줄 수 시뮬레이션도 글자 단위. (2) 요약박스 문단 좌우 여백 1000/1000(실측 133건 55%) — 글자가 테두리에 붙지 않게, 선두 공백 2칸 제거. paraPr·ParaSpec 에 right. (3) 표 열 역할(gen-table.ts colRoles) — 비고·근거·참고·출처·담당·연락처 및 내용 열 있는 표의 역할 없는 마지막 열은 25% 상한 (실측 비고 중앙값 21%·p75 26%, 최장 어절 폭까지는 허용), 내용·사항·실적·계획류 열은 비례 가중 ×2(실측 중앙값 51%) + 본문 셀 LEFT. 공문서 모드에서만 적용, 역할 미지정 호출은 종전과 동일. 라운드 1 잔여 타입 오류 2건(lastTextNode never·DISTRIBUTE 정렬) 정리. 테스트 gen-table-roles.test.ts 8건 + 기존 2건 갱신(1,631).

  • 라운드 3 — 한컴 툴바 글꼴 칸 빈칸·스타일 영문명 수정 + 장 제목 띠 표 — (1) 맥 한컴에서 kordoc 산출물의 글꼴 칸이 비고 스타일이 Normal/Outline 1 로 뜨던 결함. 실렌더 A/B(변형 10종)로 원인 확정: 한컴 툴바("대표" 언어)는 charPr 7개 언어 슬롯이 같은 글꼴로 풀릴 때만 이름을 보여주는데 kordoc 은 한글·라틴만 지정하고 나머지를 굴림/Symbol(id 0)로 뒀다. gen-header 범용 분기의 7개 언어 목록을 동일하게(라틴 슬롯의 Times New Roman/ Consolas/Arial Black 제거 — 한컴 기본 템플릿·실결재와 같은 방식), 레지스트리·levels·프로필 charPr 의 나머지 슬롯을 같은 id 로. 스타일 속성 langIDRef → 정본 langID(실결재 58/58). 글꼴 미설치·캐럿·fontfaces 형식 가설은 전부 반증(변형 AL). (2) h2Marker: "band" — 장 제목을 "로마자 채움 칸(#003366, 흰 HY헤드라인M 17b) | 간격 | 제목 칸(하변 0.4mm)" 1행 3열 표로. 계획서 장르 실측(서울 plan 36.8%·교육청 38.9%, reference 2.9)·실무자 요청으로 보고서·계획서 기본값(종전 roman 텍스트는 --h2-marker roman). 제목 칸 __kordoc_h2 왕복 채널로 파서가 heading 2 로 복원. (3) 연속 □(하위 항목 없이 □ 다음 □) 사이 빈 줄 제거 — 실측은 52:48 반반(하위 항목 뒤 □ 는 75% 유지), 실무자 눈에 과했다. (4) □ 의 keepWithNext 는 다음이 하위 항목·※ 일 때만 — □→□→□→표 사슬이 이어지면 표가 안 들어갈 때 장 전체가 다음 쪽으로 밀려 1쪽 하단이 비던 것(실렌더). (5) □ 한 줄 강제는 크기를 줄이지 않는다(장평 90·자간 -5 까지만, fitOneLine minPt/minRatio) — 17→14pt·장평 85 로 우겨넣어 형제 □ 끼리 크기·굵기가 들쭉날쭉하던 결함. 넘치면 내어쓰기 두 줄 + "문장을 줄이세요" 경고. 띠 제목·제목표 아래 첫 □ 반 줄·요약박스 뒤 띠 반 줄. (6) □ 앞 빈 줄을 빈 문단 대신 문단 위 간격 10pt 로 — 빈 문단(15pt×180%=27pt)은 ㅇ→□ 기준선이 본문 행간(30pt)의 1.8배(55pt)로 벌어져 구멍처럼 보였다(실렌더 캡처). 10pt 면 37pt·잉크 간격 22pt 로 실결재의 그룹 사이 비율(본문 1317pt 대비 23~25pt)과 같다. 띠 제목 앞 20pt·띠 아래 첫 □ 12pt.

  • 렌더 통합 계층 (#75, 1차: HWPX) — src/render/scene.ts RenderScene(1-based 페이지·페이지 로컬 pt bbox·결정적 region id table-000017·다중 페이지 조각·parentId·sourceId=hp:tbl id). svg-render.ts 가 문단·표·이미지·도형 region 을 그리는 순서로 기록하고 SVG 에 <g data-kordoc-id/type/page> 래퍼를 단다(그 외 방출 바이트 불변, 시각 게이트 무회귀). renderHwpxPages() 가 페이지별 standalone SVG(사용 심볼만 defs) 를 내고, document.ts renderDocument(input, {format: svg|html|png|jpeg|pdf, pages}) / renderDocumentToScene() 이 포맷 감지→어댑터→자산으로 통합(HWP5 는 후속 — 명시 오류). html.ts 자급자족 레이아웃 HTML (.kordoc-page[data-page], 인쇄 CSS), pdf.ts 는 그 HTML 을 Chromium 으로 인쇄(puppeteer-core optional), rasterize.ts rasterizePageSvg(png/jpeg 같은 배율), regions.ts extractRenderedRegions()/cropRect(bbox×실배율, 페이지 클램프, 최소 1px). CLI kordoc render --format … --pages … -d(기본 svg -o 는 종전 세로 스택 그대로)·kordoc crop --target table -d, MCP render_document 에 pages(페이지별 이미지)·crop_regions 신설. 테스트 render-scene·render-regions·render-document·render-cli.

  • 표 분류·시각 추출 (#76) — src/table/classifier.ts 휴리스틱 분류(의미: 반복 행 스키마·격자 규칙성·활성 밀도·열 타입 일관성 / 비표: 병합 불규칙·빈 띠·희소·중첩 래퍼, 도표 키워드는 구조 증거가 있을 때만 가산) → semantic-table | non-tabular-layout | uncertain. ParseOptions.classifyTables(opt-in, 기본 산출 불변) 가 최상위·셀·캡션 중첩표를 독립 분류해 IRTable.classification 에 붙인다. IRTable.sourceId(HWPX hp:tbl id)·regions. builder.ts hasStructuredCellContent(중첩표·구분선 → HTML) + GFM 셀이 blocks 를 직접 직렬화해 병합 없는 단순 표의 셀 이미지가 text 평탄화에 없어도 ![image](src) 로 남는다(span 문단·HWPX 산출은 종전 그대로). analyze.ts chooseTableRepresentation(gfm/html/visual). visual.ts extractTables(input, {policy}) 가 분류 + #75 region 조인(sourceId 1:1, 중복·미매칭 경고) + 정책별 crop (기본 non-tabular-and-uncertain). CLI kordoc tables --visual … -d. 테스트 table-classifier(픽스처 매트릭스 10)·table-analysis· table-cell-image·table-visual·table-cli. 렌더(reflow)의 faceClassOf 는 불변. 표 열폭 측정에 셀 글꼴 클래스 반영(GongmunTableStyle.faceClass).

  • gen-ids.charPr 자간 인자(spacingPct), gen-table-bf mid(0.15mm) 변, tc() rowSpan·vAlign.

  • 테스트 33건을 v5 계약으로 갱신(레지스트리 id 는 조회로 확인).

  • 시각 오라클 하네스(bench:visual) — 한컴은 마지막 창 크기·확대율을 기억하므로 다른 캡처·수동 확대가 남긴 상태가 그대로 찍혀 14/14 이탈하던 것을, 캡처 전에 창 750×850·확대 45%(상태 표시줄 slider) 로 고정해 재현 가능하게 했다. 종이 검출은 "순백 행 첫~끝" 대신 종이 밖(순백 <3%) 행으로 분절한 뒤 강한 순백 행이 가장 많은 분절 — 표지 문서처럼 다음 쪽 상단이 함께 보여도 한 쪽만 잡는다. baseline 14건은 v4.9.2 이후 갱신이 없던 것이라 실렌더를 눈으로 확인하고 다시 잡았다.

Removed

  • gen-docframe.ts 의 문단형 두문·결문 빌더(buildDocHead·buildDocFoot) — 표 골격이 대체.

[4.12.3] - 2026-09-05

공문서 작성 라운드. 실결재 기안문 206건 + 보고서·보도자료 337건 HWPX 의 항목부호 단계별 글꼴을 실측해 levels(위계 타이포) 옵션을 넣고, 표기법 검수 COLON_SPACE·DATE_NO_SPACE 오탐을 법정 서식·법제처 표기 근거로 좁혔다. 별표 코퍼스 3차(licbyl-byl2/ 183쌍) 편입, PDF 텍스트·틀 결함 3건, HWP5 기호 2건.

Added

  • levels — 항목부호 단계별 위계 타이포 (src/hwpx/gen-levels.ts, gongmun.ts·gen-section.ts·gen-header.ts· gen-gongmun-fit.ts·gongmun-surface.ts·cli.ts·mcp.ts) — { [depth 0~7]: { font?, pt?, bold? } }. CLI --levels "0=HY견고딕/17/bold,1=한컴돋움/15/bold,2=휴먼명조/14", MCP levels. 지정 단계마다 charPr 쌍(보통·굵게)을 docframe charPr 뒤 id 에, 글꼴은 정적 fontface 뒤에 append(한글·라틴만 참조, 그 외 언어는 본문 글꼴 — 비실측 프리셋 1종 언어 테이블 안전). 내어쓰기 폭은 그 단계 글자 크기(levelIndent markerHeight), 장평 변형은 지정 단계 제외, 인라인 **강조** 는 같은 단계 bold 짝. 실측 프리셋 □(HY헤드라인M)·보도자료 각주보다 우선. 미지정이면 산출물 불변. 실측 근거는 docs/gongmunseo-reference.md 2.7, 구현 매핑은 engine-spec (c-4): 법정 8단계는 본문 동일 90%(기본값 무변경), □/ㅇ/- 계열은 □ HY견고딕 +2~3pt bold · ㅇ 한컴돋움 bold · - 휴먼명조.
  • PDF 제목 아래 틀 (clip-cells.ts titledFrame) — 테두리 없는 1칸 틀(별표 1×1 프레임·선서문 바깥 틀)을 "문서 전 페이지 반복 클립 = 본문 영역" 가설로 가르려 했으나 반증(채용공고 pair05 본문 영역 클립은 쪽마다 y1 73~91 로 달라 같은 사각형이 아니고, 별표는 1쪽). 대신 윗변 ≥ 페이지 20%·폭 ≥ 60%·머리말 띠(8%) 아래 위쪽에 글이 있고 안에도 글이 있는 컨테이너를 틀로 본다(buildClipCellGrids 6번째 인자 text 좌표, page-blocks 가 넘김). 2단 채용공고의 단 상자(폭 38%, pair06)는 폭 조건으로 제외 — pdf-table-gt cellF1 0.933 회귀를 잡은 뒤 0.945 유지.
  • PUA-A U+F00E1 → "(인)" (shared/pua.ts) — 한컴 PDF 200dpi 실렌더로 네모 테두리 안 "인" 글리프 확인 (licbyl2 17754757 수련치과병원장·17975885 청원주/경찰서장 결재 위치).
  • 별표 코퍼스 3차 — licbyl-byl2/ 183쌍(seed 20260907, --knd=1 --exclude=licbyl,licbyl2,licbyl-byl, 풀 소진으로 200 미달) + rhwp v0.8.6 HWPX 183. 격리 1건(known-false-miss README: HWP5 쌍 유사도 1 + "훈격구분" 출력 존재). 하위 30건(Jaccard<0.5) 분해: A 1×1 틀 PDF 미감지 8 · B HWP flattenLayoutTables 해체 vs PDF 1열 유지 13(+다열 2) · C 구조 차 7 — C 는 수식 셀(HWP=HWPX, cmp 지표 한계)·쪽 경계 셀 분할·셀 줄바꿈·U+119E 로 파서 결함 없음.
  • 벤치 정규화 미러 (bench/lib/normalize.mjs) — PUA_SUPP 에 F00E1 "(인)" 추가(없으면 HWPX phantom 4문서), U+119E → ㆍ 접기(pdftotext·pdfjs 컨센서스가 ᆞ 를 내므로 파서 정규화와 대칭 — 없으면 별표 PDF 5문서 coverage 0.97~0.99).

Changed

  • COLON_SPACE (gongmun-lint.ts) — 표 줄(GFM |·HTML tr/td/th) 건너뜀(skipTable): 법정 별지서식 자체가 "성 명 :"·"주 소 :" 라벨 셀을 쓰므로 규칙 대상이 아니다(서식 595건 표 셀 801건 전부 라벨 — 서식 801 → 7). 뒤 글자 <·*·_ 제외("성명:" 578건·"과정:** 붙임"). 기안문 본문 "일 시 : 값"(206건 중 100건, 414건)은 편람(쌍점 앞 붙이고 뒤 1타) 위반이라 유지(492 → 425).
  • DATE_NO_SPACE — 법제처 연혁 표기 <개정 2012.2.14>·<신설 …>·삭제 <…>·[시행일:2017.9.8.] 제외(서식 156 → 10, 남은 10은 "예) 2016.07.01"·"0000.00.00." 기입 예시). 기안문 139건은 무변경 — 문서번호 인용 "○○과-123(2026.7.1.)호" 103건(74%)이 편람 날짜 표기 위반이라 유지.
  • PDF 가운뎃점 정규화 (text-clean.ts normalizeAraea) — 한컴 PDF 가 ㆍ(U+318D)를 조합형 중성 아래아 ᆞ(U+119E)로 내는 것(별지서식 573쌍: PDF 3,238자 중 340자·41문서, HWP 0)을 되돌린다. 앞이 초성(U+1100~115F)인 옛한글 결합은 보존. 셀 blocks 도 순회.
  • PDF 1×1 표 줄 보존 (text-clean.ts splitSingleCellTables) — 중첩표 없는 최상위 1×1 표를 줄마다 문단 블록으로. 결합 지점은 builder 1×1 분기가 아니라 cleanPdfText 의 mergeKoreanLines(한글 줄 병합)였다 — 문단 블록 사이 빈 줄은 병합 대상이 아니다. 선서문 "선 서 나는 헌법을 …" 해소. 파급 897 PDF 중 10문서 12표.
  • HWP5 하이픈 제어문자(0x18) 미방출 (hwp5/record.ts) — 한컴 PDF·rhwp export-hwpx 모두 그리지 않는다 ("60g/㎡", licbyl2 17855137 — 전 코퍼스 유일 출현). 실렌더 정합. 테스트 2건 갱신.

판정 기록 (변경 없음)

  • 1열 다행 표 52건 행 경계 — HWP5 는 줄 렌더(행 경계 소실), PDF 는 클립 셀로 1열 표 유지(B 클래스 15건의 실체). 바꾸려면 HWPX 게이트·formats 베이스라인 파급 측정이 먼저라 이번 라운드 유지.
  • 후행 빈 열(17336891) — HWP5 파서 8×5 = 한컴 PDF 8×5, rhwp HWPX 만 8×4. rhwp 변환 아티팩트, kordoc #47 트림 로직 차이 아님.
  • PDF coverage weak 18409013(0.81) — 체크박스 서식의 "예/아니오" 라벨 줄이 박스 줄과 분리 배치돼 어순만 다름 (텍스트 손실 0). 레이아웃 순서 차, 미수정.
  • cmp 하위 3건 — 16826677 좌표전환계산부: HWP=HWPX 35×6 = PDF 35×6, 셀 안 수식(EQEDIT)이 IR cell.text 에 없어 Jaccard 만 낮음. 18191495 양도소득 통보: 21×52 초세밀 격자를 PDF 가 20×51 로 병합(두문 영역 클립 부재). 6427951 근로감독관증: HWP 단일 24×19 틀을 PDF 가 6×4+1×1+4×3 으로 분할(테두리 없는 외곽 틀 미감지 — A 클래스).

[4.12.2] - 2026-09-05

별지서식 라운드 2. 코퍼스를 2차로 늘리고(서식 279쌍 + 별표 90쌍, 총 1,995파일) 세 파서가 같은 IR 모양을 내도록 맞췄다 — 1칸 틀 안 중첩표를 PDF 는 틀 셀의 IRCell.blocks 로, HWP5 는 서식 틀을 해체하지 않고 표로. HWP5↔PDF 셀 텍스트 Jaccard(bench/cmp-hwp-pdf.mjs, 정식 편입) 는 licbyl 0.927 → 0.965, PDF 표 GT 게이트는 cellF1 0.873 → 0.945, cellExact 0.945 → 0.980, contentNED 0.842 → 0.948. 플로어를 실측치로 상향 잠금(cellExact 0.72 → 0.97).

Added

  • PDF 틀 셀 중첩표 (clip-cells.ts·page-blocks.ts) — 클립 그리드가 다른 클립 안에 들면 TableGrid.clipParent 로 부모 사각형을 넘기고, 소비측이 틀 셀을 만들 때 그 셀의 blocks 에 셀 문단과 안쪽 표를 위→아래 원문 순서로 넣는다(text 는 평탄화). 지정서·영치증의 "발신명의 | 직인" 표가 틀 뒤(꼬리행 뒤) 별도 블록으로 빠지던 결함 제거 — HWPX 파서와 같은 3×1·5×3 HTML 표.
    • 1칸 틀: 다른 클립을 품고 네 변에 획이 있는 홀로 선 클립은 1×1 그리드(선서문·각서류의 바깥 틀), 그런 틀 안에 홀로 든 획 있는 클립은 그 중첩표. 획 없는 바깥 틀 안에 획 있는 상자 하나만 있으면(선서문) 그 상자만 1×1 표로 낸다. 획 조건이 핵심 — 한컴은 본문 영역(여백 안쪽 전체)에도 클립을 깔고 그 안에 페이지의 모든 표·칩이 들어가므로, 획 없는 컨테이너를 틀로 보면 페이지가 통째로 1×1 표가 된다(채용공고 PDF 실측 회귀, pair 게이트 0.985 → 0.87). 테두리 없는 1칸 틀은 본문 영역과 구분할 수 없어 표로 삼지 않는다(문서화된 한계).
    • 같은 사각형이 문단마다 반복 클립되는 것(틀 3회·1칸 표 2회 실측)은 좌표로 중복 제거 — 중복을 남기면 1칸 표가 이웃도 포함도 아닌 단독 클립 여러 개로 흩어져 묶이지 않는다.
  • HWP5 한컴 PUA-A 접힘 해제 (record.ts) — HWP5 문자는 2바이트 WCHAR 라 한컴 Supplementary PUA-A(U+F0000대) 기호를 담을 수 없고, 한컴은 U+A000 + (code − 0xF0000) 으로 접어 저장한다(이 글자 음절 블록). 결재란 "(인)"(U+F012B) 이 U+A12B 로 들어 있어 ꄫ 로 새던 것을 펴서 mapPuaText 표에 넘긴다(→ "(인)", rhwp export-hwpx 와 동일). 접힘 영역은 관찰된 U+A000~U+A48C 만. 매핑 없는 코드(U+A0E1 → F00E1)는 종전 PUA-A 정책대로 제거 — 한컴 PDF 도 그 자리에 텍스트를 찍지 않는다.
  • bench/cmp-hwp-pdf.mjs — 같은 서식의 HWP5 원본과 PDF 를 양쪽 파싱해 표 셀 텍스트 집합의 Jaccard 를 내는 보고용 지표(중첩표는 collectIrGrids 로 재귀, HWP5 flatten 비대칭은 헤더에 명시). --linebreaks 는 PDF 셀 안 줄바꿈 자리가 HWP 원문에서 공백(어절 경계)인지 아닌지 센다.
  • bench/collect-licbyl.mjs --knd=1(별표) 와 --exclude=dir,…(형제 코퍼스 일련번호 중복 제외). 2차 코퍼스 licbyl2/(seed 20260906, 서식 279쌍 — 표본 풀 소진으로 300 미달) 와 licbyl-byl/(별표 90쌍) 수집 → rhwp v0.8.6 export-hwpx 변환 → 게이트 편입.

Changed

  • HWP5 flattenLayoutTables 가 서식 틀은 해체하지 않는다 — 중첩표를 품고 글이 600자 이하인 표(별지서식 3×1 틀: 제목행 + 틀(발신명의|직인 중첩) + 꼬리행)는 레이아웃 표가 아니라 서식 그 자체다. HWPX 파서·PDF 와 같은 모양으로 남긴다. licbyl HWPX IR 295문서 605표 전수: 해체 후보 45(3×1 29·2×1 11·3×2 5), 그중 중첩표 동반 33 — 페이지 사슬 레이아웃 표는 글이 많아 종전대로 해체된다(비-licbyl 코퍼스에서 걸리는 표는 채용공고 채용분야 3×2·소식지 카테고리 2×2 등 서식류만, 600자 초과 동의서 3×1 은 종전대로). 1열 다행 표 52건(다행 셀 51)은 줄 단위 렌더 유지 — 잃는 것은 행 경계뿐이라 렌더 정책은 두었다. HWPX 게이트 1.0·roundtrip 게이트 무변화.
  • 표기법 검수 2룰 좁힘 (실결재 기안문 206건 + 별지서식 595건 실측) — TILDE_SPACE 는 숫자· 날짜·시각 범위(앞 숫자·온점·괄호·년월일시분, 뒤 숫자·연도 생략부호)에만: 기입란 "기간( 부터 ~ 까지)"·"공사기간: ~"·"3인 ~ 최대 4인" 제외(기안문 32 → 27, 서식 47 → 30). DUEUM_ERROR 는 서식 기입란(" 년도", "( 년간)", "2026 년도")과 셀 하나가 "년도" 뿐인 표 머리글 제외(서식 28 → 2, 기안문 0 유지). MONEY_NO_HANGUL·LOANWORD_ERROR·DISCRIMINATORY_TERM 은 기안문 206건에서 0건 — 오탐 근거 없어 그대로. END_MARK_MISSING 4건은 실제 "끝." 없음(진양성).
  • 양식 인식기(form/recognize.ts)가 셀 blocks 의 중첩표를 훑는다 — 1칸 틀 안 신청인 정보 표의 라벨-값이 최상위만 볼 때 통째로 빠졌다. fillHwpx 의 중첩표 DFS 와 같은 경계.
  • bench/pdf-table-gt.mjs 플로어 상향 잠금: exact 0.65 → 0.85, cellF1 0.72 → 0.94, cellExact 0.72 → 0.97, contentNED 0.525 → 0.94 (v4.12.1 실측치가 코드에 반영되지 않은 채 11차 플로어가 남아 있었다).
  • bench/corpus/ +1,095파일: licbyl2/ 837(HWP5 279 + PDF 279 + HWPX 268 + manifest), licbyl-byl/ 271, known-false-miss/ +11(licbyl2 rhwp 변환본 — HWP5 쌍 유사도 1·미스 문자열 전부 파서 출력에 있음 → GT 정렬 아티팩트, README 기록). 별표 90쌍은 격리 0건.

검토만 (변경 없음)

  • PDF 셀 안 원문 줄바꿈 결합 — licbyl 300쌍 4,436 줄바꿈 자리를 HWP 원문과 대조하면 어절 경계(공백) 85%·글자 단위 줄나눔(공백 없음) 13%·미발견 2%. 한글 기본 줄나눔은 글자 단위라 PDF 에는 공백 유무 신호가 없다 — 공백으로 잇든 붙여 잇든 한쪽 13~85% 가 틀리므로 원문 줄바꿈을 그대로 둔다(cmp-hwp-pdf.mjs --linebreaks 로 재현).
  • 테두리 없는 1칸 틀(선서문 바깥 틀) 은 본문 영역 클립과 구분 불가 — 위 Added 항 참조.

[4.12.1] - 2026-09-05

법령 별지서식(법제처 별표·서식) 파싱을 세 포맷에서 맞췄다. 법제처 OpenAPI(licbyl)로 서식 300건의 HWP5 원본과 PDF를 쌍으로 모으고(bench/collect-licbyl.mjs), rhwp v0.8.6 CLI (export-hwpx)로 HWPX 변환본을 붙여 900파일 코퍼스 bench/corpus/licbyl/ 을 게이트에 편입했다. HWP↔PDF 셀 텍스트 일치(Jaccard)는 0.79 → 0.92, PDF 표 GT 게이트는 cellF1 0.727 → 0.896, cellExact 0.728 → 0.962, contentNED 0.531 → 0.831 로 올랐다.

Added

  • PDF 셀 클립 그리드 (src/pdf/clip-cells.ts) — 한컴 PDF 는 표 셀마다 W n(clip + endPath) 사각형을 깔고 그 안에 글을 찍는다. 별지서식 외곽 표는 테두리 "없음" 셀이라 획 괘선이 없어 line 파이프라인이 표를 못 잡고 제목·기입란이 헤딩과 문단으로 흩어졌다. extractLines 가 클립 사각형(clipRects)을 함께 수집하고, 서로 변을 맞대는 클립 묶음을 셀 기하가 확정된 그리드(TableGrid.cells)로 만들어 선→교차점→클러스터 경로를 건너뛴다. 전 페이지를 한 그리드로 합칠 때 행마다 다른 열 경계가 MIN_COL_WIDTH 병합으로 뭉개지던 것(도선사면허 신청서 23열 → 13열)도 사라져 HWP 파서와 같은 23열·같은 병합 구조가 나온다.
    • 포함 관계로 층을 나눈다 — 각 클립의 부모(자기를 품는 가장 작은 클립)가 같은 것끼리만 이웃으로 묶는다. 지정서·영치증의 1칸 테두리 틀은 위 제목행·아래 꼬리행과 한 표의 셀이 되고 (HWP 파서의 1열 표와 같은 모양), 안쪽 "발신명의 | 직인" 표는 별도 그리드가 된다. 틀 안 자유 문단이 바깥 격자로 찢기거나(4열) 클러스터 표 감지에 걸려 가짜 6열 표가 되던 결함 제거.
    • 틀과 면적 절반 이상 겹치는 line 그리드는 버린다 — 틀 괘선이 위 행 외곽선과 이어져 line 경로가 틀보다 크게 잡던 경우(영치증)까지 포함. 겹친 바깥 부분은 클립 그리드가 이미 셀로 담고 있다.
    • 클립 그리드는 셀이 확정된 실제 표라 1행·1열 스킵, 프로즈 박스·의사 표 강등을 적용하지 않는다. 처리 순서는 클립 그리드(면적 오름차순 — 중첩표가 바깥 셀보다 먼저) → line 그리드 (블록 순서는 마지막에 Y 로 재정렬).
  • PDF 심볼 폰트 글리프 복원 (src/pdf/symbol-fonts.ts) — 처리절차 화살표·체크박스를 Wingdings 로 찍은 PDF 에서 pdfjs 가 코드를 Latin-1 문자로 돌려줘 è 가 남았다. operatorList 로드 뒤 page.commonObjs 의 폰트 실명이 Wingdings 이면 0x21~0xFF 코드를 유니코드로 되돌린다(➔ □ ✔ ☑ …). Wingdings 2·3, Webdings 는 코드표가 달라 매핑하지 않는다.
  • 공문서 표기법 검수 6룰 (gongmun-lint.ts, pyhwpxlib Gongmun 검사·hwpx-skill 대조) — MONEY_NO_HANGUL(금액 괄호 한글 병기, 규정 시행규칙 제2조), TILDE_SPACE(물결표 앞뒤 붙여쓰기), DUEUM_ERROR(어두 '년도·년간' → '연도·연간'), LOANWORD_ERROR(외래어 오기 40종, 표준 표기를 suggest 로), DISCRIMINATORY_TERM(차별·비하 표현 25종 → 순화어), END_MARK_MISSING(붙임 있는데 "끝." 없음 — kordoc lint 문서 검사 전용, generate 경고에서는 official 프리셋이 "끝."을 자동 방출하므로 끔). lintGongmunText(text, { document }) 옵션 추가.
  • 내부결재 문서의 발신명의 생략 — docHead.to 가 "내부결재"면 결문에 발신명의를 적지 않는다 (행정업무규정 시행규칙). isInternalApproval() 수출.
  • bench/collect-licbyl.mjs — 법제처 별표서식 목록 API 균등 보폭·결정적 표본 수집기 (같은 seed 면 같은 표본, '삭제'·'…으로 이동' 자리표시 서식 제외, 법령당 2건 상한).

Fixed

  • 기입 빈칸 "년 월 일" 이 "년월일" 로 붙던 것 — 균등배분 정리("현 장 대 응 단 장")가 한 글자 토큰 3개 이상을 무조건 이었다. 한 글자 토큰이 전부 날짜·시각 단위(년월일시분초)면 기입란으로 보고 붙이지 않는다. HWP/HWPX(sanitizeText)·PDF(collapseEvenSpacing) 공통.
  • 1열 다행 표의 셀 안 줄바꿈 — 청구서류의 1열 틀 표는 셀 하나에 기입 항목이 줄마다 들어 있는데 공백으로 이어 "1. 소속 2. 성명 3. …" 한 줄로 뭉개졌다. 줄을 줄로 남긴다.

Changed

  • bench/corpus/licbyl/ 900파일(HWP5 300 + PDF 300 + rhwp HWPX 297) 게이트 편입. rhwp 변환본 가운데 부동 표를 section 첫머리에 쓰는 등 문서 순서가 시각 순서와 어긋나 자기참조 GT 정렬이 깨지는 3건은 known-false-miss/ 로 격리(파서 출력은 HWP5 원본과 동일).
  • rhwp tools/forms/일반기안문_서식.hwpx 최신본은 header.xml 에 제어문자(0x01)가 섞여 있어 들여오지 않았다(번들 서식은 종전 그대로).

[4.12.0] - 2026-08-30

v4.11.0 이 걷어낸 HWP3 유실의 잔여 전량을 처리했다. rhwp samples/ 의 HWP3 원본 9종을 한컴 변환본과 문자 멀티셋으로 대조했을 때, 이제 변환본에만 있는 문자는 그림 대체텍스트를 빼면 0 이다.

Added

  • HWP3 그리기 개체 안 글상자 텍스트 회수 (#73) — ch=11(그림) 제어 문자의 확장 블록은 pic_type(info[74])이 3 이면 그림이 아니라 그리기 개체 트리다. 표지 제목·순서도 라벨 처럼 눈에 잘 띄는 본문이 그 안 글상자에 들어가는데, 종전에는 블록을 통째로 건너뛰어 통째로 사라졌다(hwp3-sample16 618자, sample19 86자, hwp3-curve 221자). 새 src/hwp3/drawing.ts 가 프레임 헤더 → 도형 레코드 트리(connection_info bit0=형제· bit1=자식)를 훑어 글상자 문단 리스트를 모으고, 본문과 같은 문단 파서로 읽는다.
    • 비-글상자 도형도 '글상자로 만들기' 면 공통 헤더 뒤에 같은 블록(스펙 11.3 표 78)을 싣고 전체 길이를 header_length 로 선언한다 — 잉여 길이가 표 78 과 정확히 맞아떨어질 때만 회수하고, 아니면 종전대로 건너뛴다 (rhwp #5141·#5558 규칙).
    • 트리 해석은 확장 블록 슬라이스 안에서만 이뤄진다. 본 스트림은 이미 그 길이만큼 전진했으므로 어떤 실패도 본문 문단 스트림의 동기를 깨지 않는다 — 실패하면 경고 한 줄과 함께 종전 동작(통째로 건너뛰기)으로 되돌아간다.
    • 재귀 상한 256단계·도형 4,096개 상한. has_child 는 파일에서 온 값이라 상한이 없으면 중첩 컨테이너 하나로 스택을 고갈시킬 수 있다 (rhwp #4285).

Fixed

  • 비가시 마커가 대체 문자로 새던 것 — 새 번호(19)·쪽 번호 위치(20)·쪽 감추기(21) 제어 문자 자리에 U+FFFC()를 본문으로 심어 마크다운에 그대로 남았다. 한글도 그 자리에 아무 글자를 내지 않는다(rhwp #4957). 차례 표식(25) 과 같은 계약으로 맞춘다 — samples 33개 중 6문서 11건.

검증

문서 v4.11.0 v4.12.0
hwp3-sample16 618 0
hwp3-sample19 0 0 (+86자 회수)
hwp3-sample10 11 11 (전부 그림 대체텍스트·U+FFFD)
나머지 6종 0 0

samples 33개 전수에서 그리기 개체 워커가 예외를 던진 문서는 없다. 테스트 1,570 pass(신규 10), 코퍼스 게이트 59/59 PASS.

[4.11.0] - 2026-08-29

HWP3 텍스트 추출에서 글자가 조용히 사라지던 경로를 걷어냈다. rhwp 최신 devel (v0.8.3~0.8.4) 대조에서 나온 결함들로, 매핑을 못 찾은 문자를 파서가 경고 없이 건너뛰기 때문에 "성공했는데 본문이 비는" 형태로만 드러난다.

Fixed

  • HWP3 기호·한자가 통째로 사라지던 결함 — kordoc 의 johab 디코더는 rhwp 에서 포팅할 때 하드코딩 표만 가져오고 완성형 좌표 규칙 두 개를 빠뜨렸다. HWP3 은 KS X 1001 기호행(0xA10xAC)을 행 간격 96, 한자행(0xCA0xFD)을 행 간격 94 로 편 코드로 문자를 담는데, 이 규칙이 없으니 표에 없는 코드는 전부 미매핑 → 삭제였다 (채권(債權)조서 → 채권()조서). rhwp decode_hwp3_ksc_symbol· decode_hwp3_ksc_hanja 를 이식하고, 표준 매핑과 한컴 표기가 갈리는 두 자리 (∼→~, ⊙→◉)를 보정한다.
  • HWP3 사적 코드 실측 표 확장 — 라틴 확장(0x00A00x00FF, ü·ö·ä·ß 가 지워져 "für"→"fr" 이 되던 것, rhwp #5555)과 rhwp #5860 의 실측 28코드를 이식하고, rhwp 표에도 없던 코드를 kordoc 이 직접 실측해 더했다 — 괘선 조각 6종 (0x3013·0x3014·0x3015·0x3019·0x301B·0x301D), 겹줄 0x37ED, 원문자·괄호문자 계열 (0x2E00·0x2E010x2E07·0x2E0A0x2E12·0x2C210x2C26·0x2C40~0x2C42), 글머리표 0x2022·0x2F17, 장식 0x2F06. 근거는 rhwp samples/ 의 HWP3 원본 ↔ 한컴 변환본 문맥 정렬이며, 코드별 출현 횟수가 변환본의 해당 문자 개수와 정확히 맞는 것만 실었다.
  • 초성이 '채움'인 아래아 음절 유실 — 초성 없이 아래아만 있는 음절(0x87C1 등)을 무효 초성으로 보고 통째로 버렸다. 한컴 변환본과 같이 자모 하나(U+119E)로 남긴다.
  • 서식 run(span)이 PUA 정리를 건너뛰던 결함 — 굵게·밑줄 등 서식이 걸린 문단은 spansToMarkdown 경로를 타는데 여기서만 PUA 치환·제거를 하지 않아, 한컴 전용 글머리표·괘선 조각이 원시 PUA(두부 문자) 그대로 마크다운에 실렸다. HWP5·HWPX· HWP3 전 포맷 공통 경로다 (hwp3-sample10 변환본 1개 문서에서 116자).
  • 한컴 PUA 검증표 동기화 — rhwp VERIFIED_HANCOM_PUA_DISPLAY(한컴 PDF 대조 확정) 에서 kordoc 에만 빠져 있던 22종 추가: 괘선 조각 ┌┬┐└┘│·┈·═·━, 별도 글리프 원숫자 ⓪①②④~⑨, 글머리표 ✺·◇·▸·□, ⊞·⊟. 매핑이 없으면 sanitizeText 가 지우므로 누락은 곧 글자 증발이다.

검증

rhwp samples/ 의 HWP3 원본 9종을 같은 문서의 한컴 변환본(HWP5/HWPX)과 문자 멀티셋으로 대조했다. 변환본에만 있는 문자(=kordoc 이 HWP3 에서 잃은 문자) 기준:

문서 이전 이후
hwp3-sample 7 0
hwp3-sample5 4 0
hwp3-sample10 172 11 (이미지 대체텍스트·U+FFFD)
hwp3-sample11 189 0
hwp3-sample14 7 0
hwp3-sample16 1,139 618 (그리기 개체 안 글상자 — 아래 참조)

남은 618자는 HWP3 그리기 개체 안 글상자(표지 제목·순서도 라벨)로, kordoc 은 그리기 개체를 통째로 건너뛴다. rhwp 는 #5558 에서 이 경로를 복구했고, kordoc 이식은 별도 작업으로 둔다.

[4.10.0] - 2026-08-28

Added

  • 실패 계약 전 포맷 확장 (#69) — 실패 JSON(success:false + code)을 --format json 전용에서 markdown·chunks 포함 전 포맷의 stdout 계약으로 확장. 헤드리스 호출자가 stderr 한국어 문구 대신 원인 코드로 분기한다. code 집합·종료 코드·안정성 보증을 README 표로 문서화.
  • images/manifest.json (#70) — 이미지 저장 시 name/mimeType/bytes/source 를 방출한다. mimeType 은 매직바이트 실측 우선, ExtractedImage.source(원본 컨테이너 항목명) 추가. DOCX 미지 확장자의 image/png 오폴백을 실측 스니프로 교체.

Fixed

  • patchHwp soft-wrap 무결성 (#71) — soft-wrap(2세그 이상) 문단을 단일 행으로 줄일 때 잔존 LINE_SEG textpos 가 새 nChars 밖을 가리켜 한컴이 "손상/변조"로 열기를 거부했다. 범위 밖 꼬리 세그먼트만 잘라내고 lineSegCount 를 정합화한다. 재작성 압축 스트림에 한컴 8바이트 꼬리(CRC32+비압축 크기)도 복원(코퍼스 7/7).

[4.9.2] - 2026-08-24

Fixed

  • 다줄 <right> 출처행이 본문폭을 넘으면 왼쪽 끝이 들쭉날쭉해지던 것 — 조판 폭을 미리 계산해 한 줄이면 우측 배치, 넘치면 LEFT 로 방출한다.
  • 출처:/자료: 캡션 전용 charPr 신설 — 본문 −3pt(하한 10pt). 발신일자 등은 본문 크기 유지.

[4.9.1] - 2026-08-22

실제 자치구 검토보고서를 HWPX 로 뽑아 한컴으로 열어 확인한 결함 3건. 셋 다 양식 파일 하나의 저장값을 실무 관행보다 앞세운 것이 원인이다.

Fixed

  • 리스트 문단 위 간격(□15/○10/-6pt)을 코퍼스 실측 최빈값(0)으로 되돌림 — 줄간격 160% 에 더해져 항목 사이가 1.4줄로 벌어졌다. □ 만 절 구분용으로 실측 2위 300 유지.
  • h3·h4 소제목이 대항목보다 왼쪽에 놓여 계층이 사라지던 것 — h2 부호폭만큼 들여쓴다.
  • '붙임 1.' 다음 들여쓴 '2.' 가 마크다운 리스트로 잡혀 번호가 항목부호로 치환되던 유실.

Added

  • munche-lint.ts — 개조식 문체 검수 12룰(서술형 종결·당위·수사·대구·항목/결론 길이·리드문). 보고서·계획서·개조식 프리셋의 generate 경고 + kordoc lint --munche. 기안문(경어)·통지·회의록·보도자료는 문체 관행이 달라 제외.

[4.9.0] - 2026-08-17

Added

  • 공문서 AI 슬롭 lint 2룰 — 줄표 사용(AI_EM_DASH), 한 줄 볼드 3회 이상 (AI_BOLD_OVERUSE). 편람 13룰 원전과 구분되는 kordoc 자체 조언 룰.

Fixed

  • reflow 렌더가 <hp:p pageBreak="1">(한컴 '쪽 나누기')을 무시하고 이어 그리던 것 — 해당 문단부터 새 페이지로 조판한다.

[4.8.1] - 2026-08-17

kordoc lint 가 문서 파일을 텍스트로 읽어 검수하던 것을 차단하고, 문서에서 빠져 있던 CLI 기능을 채웠다.

Fixed

  • kordoc lint <문서파일> 이 압축 바이트를 본문으로 검수 — lint 는 입력을 UTF-8 텍스트로 읽는다. hwpx(ZIP)를 그대로 넘기면 압축 바이트가 본문으로 둔갑해 위반이 수백~수천 건 나왔다(README 가 예제로 싣고 있던 kordoc lint 보고서.hwpx 실측 1,193건). "검사 못 함"보다 "검수 결과처럼 보이는 쓰레기"가 나쁘므로, detectFormat 으로 판정해 텍스트가 아닌 입력은 읽기 전에 거절하고 파이프 사용법을 안내한다. stdin(-)은 종전대로 동작한다

Docs

  • README(한/영)에 문서화되지 않았던 CLI 기능 반영 — fill --formats·--require-unique·--mask, redact --mask-char, render --reflow-mode, check-ocr-models·check-formula-models. 두 check-* 커맨드는 이름과 달리 없으면 내려받는다(수식 모델 ~155MB)는 주의 병기
  • 영문 README 에 통째로 빠져 있던 redact·profile·models 커맨드 추가 — 한글판과 동기화
  • 라이선스 절에 Pix2Text(YOLOv8 AGPL-3.0 주의) 추가 — NOTICE·THIRD_PARTY 에는 있으나 README 에만 누락돼 있던 고지
  • 지원 포맷에 이미지(PNG/JPG/WebP) 행 신설, parseImage·고급 함수 8종·타입 18종 표 보강
  • v3.6.0 항목에 잘못 복제돼 있던 "서식 프로필"(실제 도입은 v3.18.0) 삭제
  • 서식 프로필 기여자 표기를 실제 GitHub 핸들로 정정 — @chiclooc-rgb → @ai-localgov-officer (PR #42·이슈 #41 의 author)

[4.8.0] - 2026-08-16

--format json 결과에 페이지별 마크다운을 기본 포함 (#68, @sorbetsharkroundhand 제안).

Added

  • ParseSuccess.pages: [{ pageNumber, markdown }] 배열. parse() 가 이미 만든 IRBlock.pageNumber 로 블록을 갈라 페이지마다 blocksToMarkdown() 을 돌린 사영이라, 문서를 다시 파싱하지 않는다. 실측 비용은 파싱 시간의 1% 미만 (24쪽 PDF 기준 parse 399ms / pages 2.5ms). 페이지 경계의 신뢰도는 v4.7.3 의 metadata.pageMode 를 그대로 따른다 — "layout" 이면 실제 쪽, "section" 이면 섹션 근사.
  • blocksToPages(blocks) 공개 API: 포맷별 파서(parsePdf 등)를 직접 부르는 호출자가 같은 값을 직접 얻을 수 있다.

동작 세부

  • --pages 3-5 로 범위를 줄이면 pages 에도 3~5쪽만 담긴다.
  • 페이지 번호를 매기지 않는 포맷(DOCX 등)에서는 pages 자체가 없다 — 전부 1쪽이라는 없는 사실을 만들지 않는다. XLSX 는 종전 pageCount 와 같은 의미로 시트가 한 쪽이다.
  • 여러 페이지에 걸친 표는 현 IR 구조상 시작 페이지의 한 블록이라, 표가 이어지는 중간 페이지의 markdown 은 빈 문자열이다. 이때도 항목 자체는 남긴다 — 빼 버리면 소비자가 배열 길이로 쪽수를 셀 수 없다. 페이지별 표 분할은 별도 기능으로 둔다.
  • 페이지 번호가 없는 블록은 직전 블록의 페이지에 이어붙인다. 어느 쪽에도 안 실려 조용히 사라지는 블록이 없도록. 실코퍼스(hwpx·hwp·pdf·xlsx) 대조에서 페이지 마크다운 합집합의 문자 보존율 100%.
  • --format json 페이로드의 텍스트 분량은 대략 2배가 된다(전체 마크다운 + 페이지별 사본). 이미지 바이트와 달리 본문 텍스트라 #65 의 직렬화 한계와는 자릿수가 다르지만, JSON 을 그대로 저장·전송하는 파이프라인이면 감안할 것.

[4.7.3] - 2026-08-13

Added

  • HWP/HWPX 실제 페이지 경계 복원 (#66): 한컴 저장본의 조판 캐시(HWPX linesegarray / HWP5 PARA_LINE_SEG)로 실제 페이지를 복원한다. pageCount·블록별 pageNumber 가 섹션 근사가 아닌 실제 쪽 번호가 되고, parse(buffer, { pages: "3-5" }) 가 실제 3~5쪽을 낸다. 신호 4종(vertpos 역행·명시 쪽나눔·분할 표의 셀 흐름 리셋·분할 직후 이중 카운트 억제) 결합.
  • metadata.pageMode ("layout" | "section"): 페이지 경계의 신뢰도. 조판 캐시가 없는 생성 파일은 종전대로 섹션 근사로 동작하며, 근사 상태에서 pages 필터를 쓰면 PAGE_BOUNDARY_APPROXIMATE 경고가 붙는다.

[4.7.2] - 2026-08-07

폐쇄망(내부망) 배포 대응 — 아웃바운드 통신 킬스위치, 파일 접근 루트 제한, 모델 오프라인 사이드로드, 오프라인 설치 번들.

Added

  • KORDOC_OFFLINE=1: 모든 아웃바운드 통신(OCR 모델 다운로드, watch webhook)을 요청 발신 전에 차단한다. 감사 지점을 src/shared/offline.ts 한 곳으로 모아, 새 통신 경로가 생겨도 이 관문을 통과하도록 강제한다.
  • KORDOC_ROOT=<디렉토리>: MCP 서버의 파일 읽기·쓰기를 해당 디렉토리 하위로 제한한다(opt-in — 미설정 시 기존 동작 유지). 판정은 realpath 기준이라 심볼릭 링크로 벗어날 수 없고, 형제 prefix 디렉토리도 통과하지 않는다.
  • kordoc models --export/--import/--status: OCR·수식 모델을 SHA-256 검증과 함께 디렉토리로 내보내고 반입한다. 오염된 반입 매체의 모델은 캐시에 설치되지 않는다.
  • scripts/pack-offline.mjs: 런타임 의존성이 설치된 상태의 오프라인 tarball을 생성한다(--with-ocr, --with-models). 내부망에서 압축만 풀면 실행되며 npm 레지스트리가 필요 없다. 설치 안내 INSTALL.md 동봉.
  • docs/offline-deployment.md: 폐쇄망 배포 절차 + 보안성 검토용 근거(아웃바운드 통신 전량 목록과 재현 가능한 검증 명령, 데이터 흐름, 체크리스트).

Changed

  • 폐쇄망 모드에서 kordoc setup 은 MCP 를 npx -y kordoc mcp 대신 설치된 dist/mcp.js 절대경로로 등록하고, KORDOC_OFFLINE·KORDOC_ROOT 를 설정 파일의 env 에 함께 기록한다.
  • MCP 서버가 기동 시 적용된 제한을 stderr 에 1회 표기한다.
  • SECURITY.md 를 현행 코드 기준으로 재작성(지원 버전, 리소스 한도 실측값, 아웃바운드 통신 목록, 모델 무결성). 번들에 인라인된 cfb 관련 옛 제약 서술은 폐기.

Security

  • 의존성 취약점 정리 — npm audit --omit=dev 0건. sharp ^0.35.0 상향(libvips CVE 4건), adm-zip ^0.6.0 override(onnxruntime-node 경유 4GB 할당 이슈), MCP SDK 체인의 ip-address·fast-uri·hono 권고 해소. 남은 esbuild low 는 빌드 전용 의존성.

[4.7.1] - 2026-08-05

밑줄 보존을 전 포맷으로 확장 + PDF 링크 어노테이션 추출.

Added

  • HWPX·HWP5 밑줄 md 방출: v4.7.0 이 PDF 에서 복원한 밑줄을 HWPX(charPr span 단위)·HWP5(대표 스타일 문단 단위)도 <u>…</u> 로 방출한다 — IRSpan/ InlineStyle 에 underline 추가, 강조 조합은 <u>**…**</u>/<u>~~…~~</u> 로 중첩. 판별자는 밑줄 종류(HWPX type="BOTTOM" / HWP5 attr bit 2-3 == 1) — 한컴은 밑줄 없는 charPr 에도 type="NONE" 요소를 넣으므로(코퍼스 352파일 실측: NONE 15,603 / BOTTOM 156) 존재 여부만 믿으면 오탐, 윗줄(TOP)·미지 값은 fail-closed. 생성 경로(md→hwpx)는 취소선과 동일하게 <u> 마커를 평문화.
  • PDF 링크 어노테이션 추출 (src/pdf/links.ts): /Annots 의 Link(/URI) rect 를 텍스트와 상관해 [text](url) 로 방출. sanitizeHref 스킴 화이트리스트 (https/mailto/tel)·괄호 percent-encoding·겹침 어노테이션 이중 래핑 방지. 여러 줄에 걸친 링크는 줄마다 같은 url 로 래핑.
  • 외부 벤치 스크립트 (bench/odl-bench.mjs): opendataloader-bench(200 PDF, TEDS/NID/MHS) 예측 생성기 — 측정용, 게이트 아님. 실측(2026-08-05): overall 0.669 · NID 0.822 · TEDS 0.553 · MHS 0.300 (국제 영문 위주 코퍼스 — 한국 공문서 특화 튜닝 그대로의 결과, 상세는 스크립트 헤더).

Fixed

  • package-lock.json 버전 동기화 (v4.7.0 릴리스에서 package.json 만 범프되어 release-metadata 테스트가 잡던 드리프트).

[4.7.0] - 2026-08-05

PDF 밑줄 보존 — 개정문·심사보고서·계획서의 밑줄 강조 복원.

Added

  • PDF 밑줄 감지 (src/pdf/underline.ts): baseline 바로 아래 밀착한 얇은 수평선을 텍스트와 상관해 <u>…</u> 인라인 HTML로 보존한다. PDF에는 밑줄 폰트 플래그가 없어 밑줄이 별도 그래픽(선/얇은 사각형)으로 그려지는데, 기존 파서는 이를 전부 버렸다 (취소선 ~~는 v3.0부터 보존 — 그 짝). CJK 밑줄이 전각 박스 하단(~0.67em)까지 내려가는 조판 특성 반영 (firecrawl/pdf-inspector underline 휴리스틱 참조, MIT). 표 괘선·장식 오탐 방어 5겹 — ① 수직선 접촉(표 그리드/폼 박스), ② 동일 스팬 반복(수평 괘선만 쓰는 서식), ③ 매칭 런 union 밀착(containment+coverage — 셀 패딩·빈 열까지 걸치는 행 괘선 배제), ④ 런 사이 컬럼급 구멍(표 행), ⑤ 상하변 마주봄(라운드 배지/칩의 하변 — 곡선 모서리라 수직선 접촉 방어를 비껴가는 케이스). 코퍼스 실측: 48문서 중 32문서에서 부서명·각주 강조·절 제목·본문 부분 강조 밑줄 복원 (원문 렌더 대조 검증), PDF 커버리지 게이트 베이스라인 동등(0.99581)·표 GT 게이트 동등. 벤치 정규화(bench/lib/normalize.mjs)는 <u> 태그를 <img>·표 태그와 동일하게 채점 전 제거(서식 마커는 콘텐츠가 아님 — 양쪽 동일 적용 대칭).

[4.6.1] - 2026-08-04

2단 지면(시험지·잡지형) 읽기 순서 수리 — #64 후속.

Fixed

  • 2단 지면 읽기 순서 (#64, @choa712 블록 덤프 제보로 원인 확정): 좌·우 단이 행 단위로 인터리브되던 문제. 원인은 두 겹 — ① 그리드 경로의 최종 정렬이 XY-Cut 그룹·표 블록을 전역 Y로만 재정렬해 단 순서를 도로 섞었고, ② 기존 2단 검출 (findTwoColumnProseCutX)이 속기록류 프로즈 전용 가드(justify·마커 희박·긴 줄)라 선지 마커(①~⑤)가 많은 시험지에서 불발해 XY-Cut 의 수평 우선 축 선택으로 흘렀다. 텍스트 특성 없이 기하 신호만 보는 거터 검출(two-column.ts — 중앙 빈 띠 + 좌우 요소수·높이합·수직범위·폭 대칭)을 신설하고, 검출 시 전폭 요소(머리글 표· 쪽번호 박스)를 밴드 경계로 삼아 밴드 안에서 좌단 전체 → 우단 전체로 배열한다. 그리드 경로(기본 모드)·폴백 경로(--no-tables 포함)·OCR 경로 모두 적용. 거터 오검출로 일반 문서의 행이 찢기지 않도록 행 구조 가드 3종(크로스 연속 행· 행 짝 비율·최소 행수)을 코퍼스 실측으로 캘리브레이션 — PDF 커버리지 게이트 베이스라인 동등(0.99581 vs 0.99583) 확인. 2026학년도 수능 실측(제보와 동일 파일): --no-tables 물리학Ⅰ·생활과윤리 20/20 문항 역전 0(종전 13/3), 국어 44/45 역전 0(종전 13/3). 기본 모드도 역전 3→1·2→0 — 잔여 누락은 전폭 박스 오탐 표의 본문 흡수(별건, #64 계속 추적)가 지배 원인.

[4.6.0] - 2026-08-04

외부 제보 4건 수리 — 중첩 강조, BOM 선행 XML, 이미지 다량 문서의 JSON 출력, PDF 표 감지 opt-out.

Added

  • PDF 표 감지 opt-out (#64, @choa712 제보): parse(buf, { tables: false }) · CLI --no-tables · MCP parse_document의 tables. 시각적 테두리 박스(2단 시험지의 안내문·보기 상자)가 선 기반 표로 잡히면 주변 본문이 셀 매핑으로 끌려 들어가 문항 순서가 뒤집히는데, 표 감지를 끌 방법 자체가 없었다. 끈 경로는 선 그리드·클러스터 표· 다열 정렬·한국어 특수표를 모두 건너뛰고 자연 읽기순 문단만 낸다 (OCR 경로 포함). 기본값은 종전 그대로 true.
  • --image-refs (#65): --format json 에서 이미지 바이트를 base64 로 인라인하지 않고 저장 경로(images/<파일명>)만 남긴다. -o/-d 로 이미지가 실제로 저장되는 실행에서만 유효하다.
  • OUTPUT_TOO_LARGE 에러 코드: 결과 직렬화가 런타임 문자열 한계를 넘은 경우.

Fixed

  • 중첩 인라인 강조 (#61 · PR #62, @LeeYudok 제보·수정): **굵게 *기울임* 다시** 처럼 강조 안에 강조가 들어간 마크다운이 markdownToHwpx 에서 별 리터럴 노출·굵게 소실· 서식 역전으로 깨지던 문제. 강조 정규식이 안쪽의 더 짧은 별 run 을 허용하도록 확장하고, 매칭 내부를 재귀(상한 3)로 분해해 겉 강조 플래그를 OR 한다. 단독 강조·이스케이프· snake_case 비활성·미닫힘 폴백은 종전 출력 그대로.
  • 선두 BOM 이 붙은 XML 파트 (#63, @soyesenna 제보): 일부 OpenXML 라이터가 [Content_Types].xml·.rels 앞에 EF BB BF 를 붙이는데(XML 스펙상 적법 — 엑셀· 리브레오피스는 정상적으로 연다), xmldom 이 "선언이 문서 시작이 아님" fatalError 로 파일 전체를 PARSE_ERROR 거부했다. XML 프롤로그 정리 지점에서 BOM 을 함께 걷어낸다 — xlsx·docx·hwpx·hwpml 파싱 진입점이 모두 이 경로를 지난다.
  • 이미지 다량 문서의 --format json (#65, @choa712 제보): 이미지가 수백 장인 HWP(27~53MB)에서 base64 총량이 V8 문자열 한계를 넘어 JSON.stringify 가 RangeError 로 터졌고, 예외가 파싱 "OK" 로그 뒤 바깥 catch 로 빠져 stdout 이 비었다 — 파이프라인의 JSON.parse 가 원인 코드 없이 깨졌다. 이제 ① 저장 경로가 있으면 참조 모드로 자동 강등, ② 파싱 이후 단계 예외도 --format json 이면 실패 JSON(success·fileType·code)을 낸다.

Notes

  • #64 의 "박스 오탐 자체를 줄이는 휴리스틱 확장"은 이번 릴리스에 포함하지 않았다. 공개 수능 문제지(국어 20p) 실측으로는 표 감지를 꺼도 문항 순서 지표가 거의 그대로였고 (역전 21→19), 재현에 쓰인 4p 문제지가 없어 원인 분리를 못 했다. 표 감지 오탐과 2단 읽기순 중 무엇이 지배적인지 가려낸 뒤 손대는 편이 안전하다.

[4.5.0] - 2026-08-02

마크다운 → HWPX 생성 미지원 6종 도입 — 용지 크기·가로 방향, 다단, 머리말/꼬리말, 하이퍼링크, 각주 개체, 이미지 실데이터 임베드.

Added

  • 페이지 옵션 (markdownToHwpx page, CLI --paper/--landscape/--columns/--header/--footer, MCP generate_document 동명 파라미터):
    • 용지 크기 — A4·A3·B4·B5·Letter 프리셋 + {widthMm, heightMm} 커스텀. 치수는 한컴 정준값과 같은 내림 환산 (A4 59528×84188, A3 84188×119055).
    • 가로 방향 — landscape="NARROWLY" + 용지 치수 유지 (코퍼스 실측 계약: 세로 문서 전수 WIDELY, 가로 실물 2종만 NARROWLY).
    • 다단 — colPr colCount=1~8, 단 간격 8mm(2268HU).
    • 머리말/꼬리말 — <hp:header>/<hp:footer> ctrl + subList (rhwp 직렬화기와 동일 형상, 인라인 마크다운 허용). 재파싱 시 본문 앞/뒤 문단으로 왕복.
  • 하이퍼링크 필드: [text](url)이 anchor 텍스트로 뭉개지지 않고 실측 6-param HYPERLINK 필드(fieldBegin Command/Path/Category…+fieldEnd)로 방출 — 한글에서 실제 클릭 가능, 재파싱 시 [text](url) 복원. 표 셀 안 링크 포함, javascript: 등 위험 스킴은 살균 후 텍스트만.
  • 각주 개체: [^id] 마커 + [^id]: 본문 정의가 <hp:footNote>(rhwp 계약 — suffixChar 상시 방출)로 방출 — 한글이 페이지 하단에 각주 번호·본문을 실제로 조판. 정의 없는 마커는 리터럴 보존.
  • 이미지 실데이터 임베드: images 옵션(url→바이트)·data:image/...;base64 URI· CLI/MCP --image-dir로 PNG/JPEG/GIF/BMP를 BinData에 실제 임베드 — 픽셀 치수 프로브(96dpi 환산) + 본문폭(170mm) 초과 시 비례 축소. 바이트가 없는 참조는 종전 1×1 placeholder 왕복 보존 그대로.
  • OCR 관측 채널: 내장 OCR이 신뢰도<0.5로 무음 폐기하던 라인 수를 OCR_LOW_CONF 경고로 노출 (PDF ocr:"force" 경로·이미지 직접 입력 공통) — 인식 결손을 이제 warnings에서 볼 수 있다.

Changed

  • OCR 표 채점 모수 교정 (bench/ocr-accuracy.mjs): 비어있지 않은 셀이 3개 미만인 ref 표는 채점 모수에서 제외 — 클립아트 창문 격자(5×6에 2셀)·목차 배경 경계 같은 장식 벡터 그리드와 1×1 텍스트박스는 "표 구조"가 아니고, 그 텍스트는 recall/CER 트랙이 이미 채점한다. 제외 수는 skippedRefTables로 노출(9건). 교정 후 실측: 스캔 표 매칭 63.1%→71.4%, cellF1 0.50→0.595, exact 18.5%→21.4% (recall·CER은 불변 — 채점 로직만 교정, 워스트 문서 픽셀 진단으로 근거 확보. 검토했던 "IR 1개↔ref N개 역방향 병합"은 실데이터에서 발화 조건이 성립하지 않아 기각 — 억지 허용오차는 채점 완화라 도입하지 않음).
  • PDF coverage 게이트 재기준 0.999→0.9955 (bench/ref/policy.mjs): OCR 정확도 벤치용 신규 pdf 41문서(예산서·성과계획서 등 밀집 표 문서) 편입으로 모수가 바뀌어 micro 0.99593 실측 — 파서 무변경 상태에서 확인한 모수 변화이며, 새 실측치 바로 아래로 다시 잠가 래칫을 유지한다.

Notes

  • 페이지 옵션 미지정 시 산출물은 종전과 바이트 동일 (colPr 1단 방출 포함).
  • 가로 방향·A4 외 용지에서 표 열폭은 아직 A4 세로 본문폭 기준으로 계산된다 (문서화된 한계 — 표가 페이지를 넘치지는 않는다).
  • OCR 정확도 개선 계획(4.4.1 실측 후속)은 워스트 문서 진단 결과 "가짜 GT(장식 그리드)· GT 표 파편화 vs 래스터 통합의 채점 비대칭"이 주 손실원으로 확인되어 임계 튜닝 없이 관측 채널만 반영 — 진단 기록은 .claude/plans/next-session-ocr-accuracy-uplift.md.

[4.4.1] - 2026-08-02

문서 현행화 + MCP 이미지 입력 수리 + OCR 정확도 첫 실측.

Fixed

  • MCP 파싱 도구의 이미지 입력 차단 해제: parse_document 설명은 이미지(PNG/JPG/WebP) 지원을 안내하는데 확장자 allowlist가 이미지를 거부해 MCP 경로에서만 막혀 있었다 (CLI·라이브러리는 정상). 파싱 계열 6종(parse_document·parse_pages·parse_table· parse_chunks·parse_metadata·detect_format)에 PARSE_EXTENSIONS(문서 + png/jpg/ jpeg/webp) 적용 — 쓰기·패치 계열은 종전 allowlist 유지. MCP 실호출 재현·수리 확인.

Added

  • bench/ocr-accuracy.mjs — 내장 OCR 정확도 실측 신설: 코퍼스 PDF의 클린 텍스트층을 정답 삼아 같은 페이지의 216dpi 렌더를 강제 OCR(ocr:"force")로 대조. 41문서 82페이지 실측(M-series CPU) —
    지표 값
    문자 recall (micro) 90.0% (래스터에 글꼴이 안 그려진 1건 제외 시 94.0%)
    문자 precision (micro) 95.1%
    한글 음절 recall median 99.9%, 37/41 문서 ≥95%
    CER micro 25.5% · median 17.5% (읽기 순서 차이에 민감)
    표 구조 (텍스트층 표 65개 대비) 매칭 63.1% · cellF1 0.50
    속도 0.96s/페이지
    주의: 정답이 클린 렌더라 실스캔 노이즈·스큐는 미반영(상한치). 측정 함정 2종을 함께
    발견해 채점에 반영 — ① 목차 점선 리더(·····)는 텍스트층에만 있는 장식이라 대조 제외
    ② 비내장 글꼴(HY견명조 등)은 pdfium 래스터에 한글이 아예 안 그려져 OCR 아닌 렌더
    충실도를 재게 됨(한글 recall 0% 1건은 이 경우). det 입력 1920 보존 실험은 recall Δ
    median +0.04pp·속도 -25%로 기각 — 현행 960 유지.

Docs

  • 플러그인 스킬의 "OCR을 내장하지 않는다" 문구 현행화(v4.2.0 내장 이후 미갱신) — --ocr/ocr:true 재시도 안내로 교체. README v2.9.0 절에 내장 OCR 이후 상황 주석, 표 복원 소개 문구를 실측치(HWPX 1,673표 무손실·PDF 대조 완전일치 65.2%) 기반으로 교체, 서식 프로필 스펙 예시를 0.3.0(anchor_row·fontName_hangul)으로 갱신.

[4.4.0] - 2026-08-01

비밀번호 문서 열기(#59) 신설 + "성공했는데 글자가 없다"류 무증상 텍스트 유실 일괄 수리 (HWP3 캡션 desync·CDATA·PUA/johab 미매핑·아래아) + npm 배포본 라이선스 고지 복원.

Added

  • 🔑 비밀번호 문서 열기 (#59): 열기 암호가 걸린 HWPX·HWP3·HWP5를 CLI --password / API·MCP password로 엽니다. 세 포맷의 암호화가 전부 다름 — HWPX는 ODF 표준(AES-256-CBC + PBKDF2, 단 한컴은 PKCS#7 패딩을 안 붙이는 변형), HWP3는 단일 DES-ECB(Node/OpenSSL3에 단일 DES가 없어 des-ede3-ecb 동일키 우회, rhwp known-answer로 키 유도 고정), HWP5는 EncryptVersion 4의 비트 단위 CFB. 틀린 비밀번호는 성공으로 위장하지 않고 ENCRYPTED로 실패. HWPX 복호 결과는 평문 대조본과 바이트 동일 실측. (한컴 DRM 문서보안은 별개, @jangster77 제보)
  • CLI --format json이 실패도 JSON으로: stdout에 원인 코드(ENCRYPTED 등)를 담아 프로그램 소비자가 분기 가능 — 암호 문서에서 비밀번호 입력을 띄우는 류의 연동용. 사람이 읽는 포맷은 종전대로 stderr 안내.

Fixed

  • HWP3 그림 캡션 리스트 미소비 — 본문 무증상 대량 유실: 그림(ch=11) 뒤 캡션 문단 리스트(캡션이 없어도 43B sentinel이 항상 존재, 스펙 §10.7)를 읽지 않아 그림당 스트림이 어긋났고, 어긋난 자리가 char_count=0으로 읽히면 "정상 종료"로 판정돼 경고 0건으로 나머지를 통째 버렸습니다 — 945KB 문서가 0자. rhwp 정답지 대조 6종 복구(최대 1,003,199자, 94.8~99.7%). 표 cell_count 고정 상한 256도 실존 문서 (367셀 표)를 죽여 잔여 스트림 기준 판정으로 교체.
  • CDATA 텍스트 소실: <hp:t><![CDATA[본문]]></hp:t>를 경고 없이 버림 — 텍스트 수집이 TEXT_NODE만 보고 CDATA_SECTION_NODE를 흘렸고, 공용 XML 헬퍼라 HWPX·HWPML·DOCX 세 포맷에 걸침. 본문 walk·스타일 span·SVG 렌더까지 5개소 수리.
  • 매핑 누락 = 글자 증발 — 한컴 검증 PUA 11종 + HWP3 johab 2종: 결재란 (인)(U+F012B), 2025 행정업무운영 편람 글머리표 ►·■, 사각 안 숫자 ①~⑳, 머리말 "한글과컴퓨터" 6자, 표 셀 글머리표 ▸ — rhwp가 한컴 PDF 대조로 확정한 표만 반영(인접 코드 범위 추정 금지: PUA는 대조 없이 채우면 없는 뜻을 지어냄).
  • HWP3 아래아(옛한글) 음절 소실: 완성형에 대응 음절이 없는 hchar('ᄒᆞᆫ글 97'의 첫 음절 등)를 조용히 건너뛰던 것을 한컴 HWP5/HWPX 변환본과 같은 자모열 (초성 + ᆞ U+119E + 종성)로 보존 — 같은 문서의 HWPX 경로 출력과 일치.
  • 빈 셀 채우기가 엔티티 원문을 지우던 것 (#54 잔존분): IR상 빈 셀에 엔티티(&#32;)·태그가 있으면 통째 교체 분기로 떨어져 IR에 안 잡힌 원문이 소실 — 조용한 성공 대신 skip하는 fail-closed 계약으로 통일.
  • 누름틀 동명 다중등장 + require_unique: 머리말·본문에 같은 이름의 누름틀이 있으면 한 곳도 채우지 않고 전부 rejected되던 것.
  • 수식 토큰 프로토타입 체인 오염: 토큰이 constructor·toString이면 출력이 오염되던 결함.
  • fill 계열 ZIP 폭탄 가드: fillHwpx·extractClickHereFields·placeSealHwpx가 precheckZipSize 없이 JSZip에 직행 — parse를 안 거치는 MCP fill_form·라이브러리 직접 호출에서 무제한 팽창이 가능했던 것을 차단.

License

  • npm 배포본 라이선스 고지 포함: 배포 tarball files가 dist·templates뿐이라 NOTICE·THIRD_PARTY가 npm 수령자에게 전달되지 않던 것을 수리 — Apache-2.0· claw-hwp(MIT) 전문 동봉, claw-hwp 저작권자를 upstream 원문과 일치시키고, scripts/check-notices.mjs를 prepublishOnly에 배선해 재발 시 발행이 멈춥니다.

검증

테스트 1,387 pass / 0 fail, HWPX 게이트 PASS. 벤치 코퍼스 확충(hwpx 85→291), 채점 미러(bench/lib/normalize.mjs) PUA 정합 복원(phantom 0.000031→0.000007), 반복 문자 정렬 거짓 미스 3건은 corpus/known-false-miss/로 분리 보존.

[4.3.1] - 2026-07-30

Fixed

  • 일반기안문 서식이 한글 2024에서 "파일이 손상되었습니다"로 열리지 않던 문제: v4.3.0에 번들된 templates/일반기안문_서식.hwpx의 header.xml에 XML 1.0 금지 제어문자(0x01) 2바이트가 섞여 있어(rhwp 서식 자산 유래) 한글이 열기를 거부 — fill --template gian 산출물 전부가 영향. 제어문자 제거로 실기기(한글 2024) 열림 확인. 간이기안문은 영향 없음.
  • 재발 방지: 번들 서식과 fill 산출물의 전체 XML 엔트리에 금지 제어문자 (C0, tab/LF/CR 제외)가 없는지 검사하는 회귀 테스트 추가 — rhwp 자산 재복사 시에도 게이트에서 잡는다.

Verified

  • v5.1 HWPX 스킬의 "linesegarray 전체 삭제 시 음수 spacing 캐시 소실로 양식 파괴" 주장에 대한 실기기 대조 실험(한글 2024, 음수 spacing 셀 포함 보고서 양식): 전체 삭제·targeted 삭제·미삭제 세 변형 모두 경고 없이 열리고 렌더가 픽셀 단위로 동일 — 현행 섹션 단위 전체 삭제(allLinesegRemovalSplices) 유지.