Skip to content

고객 개발자가 고칠 파사드 2개를 95·103줄로 줄이고 확장 훅을 연다 - #367

Closed
inoray wants to merge 195 commits into
developfrom
task/363-facade-slimming
Closed

고객 개발자가 고칠 파사드 2개를 95·103줄로 줄이고 확장 훅을 연다#367
inoray wants to merge 195 commits into
developfrom
task/363-facade-slimming

Conversation

@inoray

@inoray inoray commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Resolves #363

고객 개발자가 배포된 파사드 2개만 보고 새 문서를 처리할 수 있게 만든다.

변경 요약

  • 파사드 2개를 고객이 읽을 크기로 줄였다parser_processor.py 1,644 → 95줄,
    chunking_processor.py 1,223 → 103줄. 처리 본체는 facade/core/ 로 옮겼다.
  • 확장 훅 4개(pre_source/post_parse/pre_chunk/post_chunk)와 33개 항목의
    toolbox 를 열었다. custom_fields 내부 코어를 고치지 않고 전·후처리로 새 문서를
    처리한다.
  • 새 확장자는 ROUTES 한 줄로 받는다. route_* 를 새로 만들 필요가 없다.
  • 청크 텍스트 정제를 출고 기본으로 켰다(청커 설정). RAG 검색용 문자 노이즈 제거.
  • 검증 하네스 3종(골든 회귀 56케이스, 설정 최소화 ablation, 신규 문서 드릴)과 고객
    문서 2종을 만들었다.

상세

파사드 슬림화 (08-1 ~ 08-3)

파일 남긴 것
facade/parser_processor.py 1,644 95 ROUTES 표, __call__ 4단계, 훅 2개
facade/chunking_processor.py 1,223 103 GenOSVectorMeta 23필드, GenosSmartChunker 옵션, 훅 2개

신규 facade/core/: parser.py(ParserCore) · chunker.py(ChunkerCore) ·
toolbox.py · cli.py · errors.py. ROUTES·VECTOR_META·CHUNKER 는 core 에
기본값을 두지 않는다 — 사본이 둘이 되면 조용히 어긋난다.

toolbox 는 재수출이 원칙이다. 값 변환기는 yaml 의 transform: 이 부르는 것과
같은 함수라 설정으로 하던 변환과 코드로 하는 변환이 어긋나지 않는다.

확장 훅

pre_source 는 확장자에 따라 형을 달리 받는다. .json 은 dict/list, .md·.html
str, 표 파일은 {시트명: 2차원 행}(pandas/polars/list[dict] 로 돌려줘도 된다),
그 밖은 파일 경로 + work_dir. 훅을 덮어쓰지 않았으면 원천을 읽는 비용조차 치르지
않는다(type(self).pre_source is not ParserCore.pre_source).

post_parse청크에 실릴 메타에 닿지 못하는 구멍이 있었다. 파서와 청커는 별도
API 라 메타는 DoclingDocumentKeyValueItem 으로 경계를 넘는데, post_parse
직렬화 뒤에 돈다. toolbox.set_chunk_metadata 로 두 경로를 갈라 처리한다.

청크 텍스트 정제

상담 HTML 실측(청크 11건/2,582자)에서 특수문자 225개 중 지워서 이득인 것은 장식
마커 13개와 미해독 엔티티 3개뿐이었다. 파이프 66개는 마크다운 표의 칸 경계이고
대괄호 12개는 파이프라인이 붙인 [표 검색 설명] 라벨이라 "특수문자를 다 지운다"는
오답
이다. 규칙을 장식 마커 하나로 좁혔다.

  • chunking.text_cleanupresource/resource_dev 청커 설정에서만 켰다.
    intelligent/convert 는 다른 사이트가 쓰고 재색인 시점도 달라 두지 않았다.
  • 엔티티는 규칙이 아니라 파싱에서 고쳤다(아래 docling 항목).
  • post_chunk 로도 정제할 수 있게 toolbox.refresh_stats 를 열었다. 훅은 청킹이
    끝난 뒤라 n_char 와 청크 순번이 옛 값으로 남는다(실측 11건 중 9건 불일치).
  • 사용 예시 3종을 examples/text_cleanup/ 에 넣었다 — yaml 만 / post_chunk 만 /
    둘 다. 데모가 아니라 단정하는 검증이다(정제 후 마커 0, n_char 불일치 0).

함께 고친 결함

  • 신규 확장자 첫 시도에 원본이 파괴되던 것(facade/common/loaders.py).
    get_pdf_path 가 변환 목록에 없는 확장자에 입력 경로를 그대로 돌려줘 write_pdf
    원본을 덮어썼다(실측: .xml 원천이 %PDF-1.7 로 바뀜). 파생 경로가 입력 경로와
    같으면 요청 임시 디렉터리로 돌린다.
  • 마크다운 엔티티 미해독(docling/backend/md_backend.py). 본문과 표 셀에서
    html.unescape 한다. HTML 블록이 섞인 문서는 export_to_html → HTML 백엔드 왕복에서
    > 가 리터럴로 굳어 청크까지 실렸다. 코드 스팬·펜스는 사양대로 제외된다.
  • 단일 마커 청크 경로의 순번 규약(facade/core/chunker.py). 이 경로만 1 부터
    넣고 n_chunk_of_doc 을 비워 뒀다. 다른 세 경로와 같이 0 기준으로 맞췄다.
  • 파사드 계약 설명 오류. pre_chunkDoclingDocument 가 아니라 그 직렬화
    dict 를 받는다. 문서를 믿고 data.texts 를 돌리면 조용히 아무 일도 하지 않는다.
  • 설정 주석의 거짓 문장(7파일). 마크다운 표 행을 건너뛰는 것은 line 뿐이고
    find 는 표 안에서도 치환한다.

분리해 등록한 것: #364(훅이 청크를 다 걸러내면 원인 없는 오류) · #365(파생 PDF 잔류) ·
#366(md 백엔드 표 셀 중복).

검증 하네스

  • examples/parse_chunk/parse_chunk_golden.py — 56케이스 × 2포맷 골든 회귀.
  • examples/parse_chunk/ablation/ — 설정 최소화. 고급 yaml 키를 걷고 훅으로 메워
    산출을 대조한다. 13케이스를 훅 26줄로 재현했다.
  • examples/parse_chunk/drill/ — 신규 문서 드릴(json 15 + xlsx/md/html 7).
  • examples/code_serving/local_main_test.py — 루트 main.py 를 실제로 띄워 파서→청커
    왕복 확인.

고객 문서

  • facade/gitbook_doc/facade_hooks.md(신규) — 고칠 파일 2개, 훅 호출 시점, 확장자별
    데이터 형, 새 확장자 레시피, 정제 3방식, 릴리스 갱신 때 수정분 지키는 절차.
  • facade/gitbook_doc/code_serving_dev_manual.md — 08 구조 반영 + 확장 훅 레시피 신설.

검증

  • 골든 56케이스 — 실제 사이트 설정(genos_layout + ocr auto)으로 재기록 후
    --check 차이 0.
  • tests/unit 전체 1,769건 — 실패 집합이 기준선과 완전히 동일(40건, 전부 사전
    실패. HEAD~1 로 되돌려 대조).
  • examples/parse_chunk/parse_chunk_verify.sh — PASS 22 / FAIL 3(사전 실패).
  • examples/text_cleanup/run_cleanup_examples.sh — 3종 통과.
  • 신규 단위 테스트: 훅 22건 + refresh_stats 7건.

영향 범위

  • docling/ 을 바꿨다backend/md_backend.py. 배포에 wheel 재빌드가 필수
    (핫픽스 overlay 는 genon 전용).
  • 코드서빙 배포 경로(genon/, main.py)를 바꿨다sync-serving-repo.sh 재실행
    필요. 배포 제외 목록에 sample_files/monimo, docs 를 추가했다.
  • 출고 설정 기본값을 바꿨다 — 청커의 chunking.text_cleanupoffsafe.
    청크 본문이 바뀌므로 재색인이 필요하다.
  • 청킹 파이프라인: 활성 3종은 공용 모듈을 쓰므로 한 곳만 고쳤다. legacy/BOK_적재용_*
    3종은 별도 배포 단위라 건드리지 않았다.
  • GenosServiceException 시그니처는 바꾸지 않았다. 파서·청커용 사본을
    facade/core/errors.py 한 벌로 모았다.
  • 루트 main.py 계층 기능은 바꾸지 않았다.

🤖 Generated with Claude Code

inoray and others added 30 commits August 26, 2026 00:41
- CLAUDE.md: 단일 프로세서 배포 진입점(src/main.py), 대형 facade 파일
  부분 읽기 규칙, .claude 도구 설정 섹션 보강
- settings.json: 빌드 산출물·캐시 Read 차단, reference/code-serving Edit 차단,
  읽기 전용 조회 명령 allowlist, pytest 필터 훅 등록
- hooks/pytest-filter.sh: pytest 출력에서 PASSED/SKIPPED 제거, --color=no 부착.
  실측 11,949바이트 -> 653바이트, pipefail로 종료 코드 보존
- skills: deploy-code-serving, create-patch-bundle 절차를 온디맨드 로드로 분리

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
문서 안의 HTML/Markdown 표를 custom_fields LLM 호출에 함께 실어, 예산 안에서는
문서당 1회 호출로 표별 RAG 검색 설명을 만들고 각 TableItem 에 부착한다. 청커는
그 설명을 표 청크 선두에 싣고, 큰 표가 행 단위로 나뉠 때 모든 조각에 retrieval_context
를 반복하며 그 길이까지 chunk_size 예산에 포함한다.

- 설정은 프로세서 공통 `table_text_description` + 문서유형 override 를 rag 하위까지
  키 단위 병합한다(문서유형 우선).
- 표 설명은 부가 기능이므로 프롬프트 미설정·배치 실패·부착 실패 어느 경우에도
  custom_fields 추출 결과를 버리지 않는다.
- 예산 초과 시 주변 문맥 축소 → markdown 강등 → 배치 순으로 낮추고, 표 하나만으로
  예산을 넘으면 그 표만 건너뛴다. 문자 수는 안전계수를 곱해 토큰으로 환산한다.
- annotation provenance 를 이미지 표 설명과 분리해 재실행 정리와 파서 출력에서
  서로를 덮어쓰지 않게 한다.
- 청킹 로직 사본 3종(chunking/intelligent/convert)을 lockstep 으로 맞추고,
  3종 공통 계약 테스트로 고정한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RAG 검색 매칭을 방해하는 문자 노이즈만 결정적으로 제거하는 opt-in 후처리를
추가한다. 기본값은 off 이므로 기존 산출물은 바뀌지 않는다.

facade/chunking/text_norm.py 공용 모듈 신설(3개 processor 가 호출만 한다).

- sanitize: 청킹 입력에 적용하는 문자 위생. NFC(한글 자모 분리 복원),
  BOM/제로폭/제어문자 제거, 특수 공백 → 일반 공백, CRLF → LF,
  전각 영숫자 → 반각, 따옴표/대시 통일.
  출력에서만 정제하면 청크 경계가 노이즈 문자를 센 채로 잡히므로 입력에서 한다.
- tidy: 가드레일 마스킹 뒤, 벡터 생성 직전에 적용하는 표현 정리.
  줄 끝 공백 제거, 연속 빈 줄 최대 1개. 임베딩 텍스트와 n_char/n_word/n_line
  통계를 일치시킨다.

적용 지점 9곳: chunking(docling/recursive/row/marker),
intelligent(docling/by_page), convert(docling/by_page/langchain).

- 빈 청크 제거는 page_chunk_counts 집계 전에 수행한다. 집계 뒤에 제거하면
  n_chunk_of_doc/n_chunk_of_page 와 i_chunk_* 인덱스가 어긋난다.
- 행 기반 경로는 metadata 가 그대로 청크 property 로 나가므로 함께 정규화한다.
- convert langchain 경로는 fitz.search_for 뒤에 tidy 한다(bbox 매칭 보존).
- 표/코드 블록/HEADER 라인은 보존한다. LLM 재작성, 공백 일괄 병합,
  NFKC 일괄 변환은 하지 않는다.
- _single_marker_vector 의 n_char/n_word/n_line 1 하드코딩을 실제 값으로 고친다.

설정은 chunking.text_cleanup(off|safe) 단일 키로 운영/개발 yaml 7개에 추가한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
여러 facade가 쓸 수 있는 로직은 processor 파일에 직접 쓰거나 복붙하지 말고
facade/chunking, facade/enrichment 같은 공용 패키지에 모듈로 만들고 facade는
호출만 한다. 설정 해석/판정 헬퍼도 함께 공용 모듈로 보낸다. 최상위 processor 가
self-contained 라 각자 두면 즉시 lockstep 부채가 되기 때문이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
facade 4종이 genon.preprocessor.facade.enrichment.* 를 try/except ImportError
로 감싸고 폴백을 두었으나, 같은 파일이 바로 아래에서 enrichment_config /
image_description / table_description 을 무가드로 import 한다. 가드 대상이
추가로 요구하는 의존은 httpx, yaml 뿐이고 둘 다 facade 필수 의존이므로,
가드가 발동할 상황이면 무가드 import 가 먼저 실패한다. 성립하지 않는 방어다.

폴백 정의(normalize_doc_type, normalize_doc_types,
_warn_tabular_llm_fields_unsupported)는 실제 구현과 이름이 겹쳐 심볼 혼동을
만들었고, 이를 전제로 한 사용처의 `is None` 분기도 함께 걷어냈다.
parser 의 _build_* 정적 래퍼 5개에 있던 조기 return 도 같은 이유로 제거한다.

attachment_processor 는 해당 가드가 없어 변경 없음.
semchunk/transformers, genos_utils, chardet, weasyprint, hwp_recovery 가드는
진짜 외부 의존이므로 유지한다.

검증: 5개 facade import 성공, tests/unit 실패 목록이 변경 전후 21건 동일
(전부 기존 실패).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
facade 5종이 각자 복제해 두었던 순수 헬퍼들을 배포본에 함께 담기는 공용
하위 모듈로 옮긴다. 최상위 processor 끼리는 여전히 서로 import 하지 않는다.

  facade/common/config_parse.py  설정 값 해석(as_dict, parse_optional_*,
                                 as_int_flag, load_config, resolve_tokenizer,
                                 clamp_chunk_size, resolve_chunk_mode 등)
  facade/common/file_probe.py    포맷·암호화 사전 감지(#278/#307) 및 매직헤더 상수
  facade/chunking/header_path.py 청크 HEADER 섹션 경로 조립

셋 다 docling 타입에 의존하지 않는다.

facade 에는 기존 이름의 별칭·래퍼만 남겨 호출부를 건드리지 않았다. 단위
테스트가 _load_config, _clamp_chunk_size, _build_header_line 을 모듈 속성으로
직접 참조하기 때문이다. 사이트별 조정 대상 상수(_CHUNK_HEADER_SEP,
_CHUNK_PATH_SEP, _CHUNK_PATH_MAX_LEAVES, _MIN_CHUNK_SIZE,
_DEFAULT_TOKENIZER_*)는 실측 근거 주석과 함께 facade 에 그대로 두고 래퍼가
인자로 넘긴다 — 고객이 읽고 고치는 지점이라 감추지 않는다.

변종 처리:
- _load_config 는 attachment 만 관대(경고 후 기본값)했다 → load_config(strict=)
  로 통합하고 attachment 는 strict=False.
- parser 의 _parse_optional_* 로그 접두가 [ImageDescriptionOptions] 였다(복붙
  잔재) → [DocumentProcessor] 로 통일.
- _warn_unresolved_placeholders 는 docstring 문구만 달랐다 → 통합.

제외:
- convert_to_pdf / _has_any_pdf_converter / _get_pdf_path 는 facade 별 구현이
  실제로 3종이라 기계적 통합 대상이 아니다.
- _handle_stage_error 는 docling(_classify_error, CacheContext)과 facade 로컬
  GenosServiceException 에 묶여 공용 모듈의 docling 무의존 원칙에 어긋난다.

검증: 5개 facade import 성공, tests/unit 실패 목록이 변경 전후 21건 동일
(전부 기존 실패). facade 순 감소 1,191줄, 공용 모듈 494줄 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
facade 4종(intelligent/convert/chunking/parser)이 복제해 두었던 docling 배관을
공용 하위 모듈로 옮긴다.

  facade/common/docling_ops.py  build_ocr_options, create_converters,
                                save_table_images, get_media_files,
                                check_glyph_text, check_glyphs,
                                ocr_all_table_cells
  facade/common/runtime.py      setup_logging

docling_ops 는 docling 타입을 직접 import 한다. 배관 자체가 docling 객체를 만들고
고치는 일이라 duck typing 으로 우회할 수 없다(facade/enrichment 의 기존 모듈들과
같은 취급). 프로세서 인스턴스는 받지 않고 필요한 값만 인자로 받는다.

변종 확인: parser 사본은 주석·docstring·로컬 별칭(_io/_base64/_Image)·로그 접두만
달랐고 정규화 후 실질 로직 차이가 0 이었다. 4사본을 그대로 접었다.

유일한 실동작 차이는 setup_logging 이다. attachment 만 _log.info 를, 나머지는
print 를 쓴다. basicConfig 이전 호출에서 print 가 더 잘 보이므로 announce 파라미터로
빼서 양쪽 기존 동작을 유지했다.

고아가 된 docling import(PaddleOcrOptions, ImageRef, relative_path, 백엔드 2종)도
정리했다. UpstageOcrOptions 는 각 facade 가 isinstance 검사에 여전히 쓰므로 남긴다.

검증: 5개 facade import 성공, tests/unit 실패 목록이 변경 전후 21건 동일
(전부 기존 실패). facade 순 감소 1,239줄, 공용 모듈 395줄 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
facade 4종이 복제해 두었던 GenOSVectorMetaBuilder 에서 어느 facade나 같은 부분
(텍스트 통계, 페이지 정보, 청크 인덱스, bbox, 미디어 파일, 글로벌 메타데이터,
민감정보 라벨)을 facade/common/vector_meta.py 의 VectorMetaBuilderBase 로 옮긴다.

벡터 스키마 GenOSVectorMeta 와 그 facade 고유 필드는 그대로 facade 에 둔다. 실제로
다르고(intelligent·chunking=title/created_date/appendix/file_path,
convert=created_date/authors/title, attachment=코어만), 사이트마다 손대는 지점이다.
파생 클래스가 __init__ 에서 고유 필드를 더하고 build() 에서 core_payload() 와 합친다.

set_global_metadata 판정이 갈려 있었다.

    if hasattr(self, key):                                # intelligent/chunking/attachment
    if key in self.__dict__ and key != "extra_metadata":  # convert

hasattr 판정은 메서드 이름과 같은 키가 들어오면 그 메서드를 덮어쓴다(글로벌
메타데이터에 build 키가 있으면 이후 build() 호출이 깨진다). convert 쪽이 이미
고쳐 둔 형태라 공용 base 는 그쪽을 채택했다. attachment 는 원래 미지 키를 버렸는데,
base 가 extra_metadata 로 모아도 attachment build() 가 쓰지 않으므로 출력은 동일하다.

attachment 는 set_media_files 만 override 로 남긴다 — 표 이미지를 싣지 않고 빈
목록일 때 "[]" 가 아니라 "" 를 넣는 기존 동작이 있다.

빌더 크기: intelligent 120→23, chunking 120→23, convert 118→21, attachment 111→15.

검증: 4개 facade 빌더 동작 실측 일치, test_table_image_unit.py 14 passed,
tests/unit 실패 목록이 변경 전후 21건 동일(전부 기존 실패).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
intelligent / convert / chunking 이 각자 들고 있던 청킹 로직(각 약 1,260줄)을
facade/chunking/smart_chunker.py 의 SmartChunkerBase 한 벌로 합친다. 정본은
검증량이 가장 많은 intelligent 사본이다.

동작은 바꾸지 않았다. 메서드 22개 중 16개가 이미 바이트 단위로 같았고, 실제 차이는
세 개뿐이라 그것만 클래스 플래그로 노출했다.

                                    intelligent  convert  chunking
  PICTURE_ANNOTATION_TEXT                  True    False      True
  TABLE_DESCRIPTION_MODE                   full     full   prefix_only
  AUTO_TABLE_AS_CHUNK_FOR_SHEETS          False    False      True

TABLE_DESCRIPTION_MODE 가 앞서 확인한 chunking 사본의 기능 지연(표 refine HTML,
요약 접미 누락)이다. 흡수하지 않고 플래그로 남겨 기존 동작을 그대로 유지했다.
이제 세 값이 한 파일에서 나란히 보인다 — 예전에는 세 파일을 비교해야 알 수 있었다.
chunking 을 "full" 로 올리는 것은 의도된 동작 변경이므로 별도 커밋으로 다룬다.

헤더 경로 구분자는 파생 클래스가 자기 모듈 상수를 실어 준다. 청크 크기 산정과
compose_vectors 의 실제 부착이 같은 문자열을 봐야 하기 때문이다.

검증:
- 청크 출력 스냅샷 432 조합(fixture 6종 x facade 3종 x chunk_size 3종 x
  chunk_mode 2종 x 헤더 on/off x table_format 2종) 불일치 0. 각 조합의 청크 수,
  총 문자 수, 전체 텍스트 sha256 이 변경 전 코드와 바이트 단위로 같다.
- tests/unit 실패 목록이 변경 전후 21건 동일(전부 기존 실패).

facade 순 감소 3,711줄, 공용 모듈 1,498줄 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1차 리펙토링 이후 남은 중복 중 토큰 단위로 실질 로직이 같은 것들을 모았다.
주석·docstring·줄바꿈만 다르고 동작 차이는 없었다.

  common/runtime_kwargs.py (신규)  normalize_runtime_kwargs (3사본),
                                   configure_runtime_image_mode (3사본)
  chunking/page_split.py   (신규)  split_documents_by_page (2사본)
  common/appendix.py       (신규)  check_appendix_keywords (2사본)
  common/file_probe.py     (추가)  has_any_pdf_converter (3사본)
  common/docling_ops.py    (추가)  check_empty_text (2사본)
  enrichment/page_description.py (추가)  inject_page_descriptions (2사본)

runtime_kwargs 는 재구성 대상이 프로세서에 붙은 enricher/options 라 프로세서
인스턴스를 받아 속성을 되쓴다. 속성 접근은 전부 getattr 기본값을 거치므로
object.__new__ 로 만든 인스턴스에서도 죽지 않는다 — 기존 _normalize_runtime_kwargs
는 self._runtime_cfg 를 직접 읽어 그 경우 AttributeError 였다. 실제 인스턴스에서는
동작 변화가 없다.

split_documents_by_page 는 청커 클래스를 인자로 받는다. facade 마다 다른 파생
클래스이기 때문이다. 최소 청크 크기도 facade 모듈 상수를 넘겨받는다.

check_appendix_keywords 의 탐지 패턴은 APPENDIX_KINDS 상수로 뺐다. 사이트 문서
관례가 다르면 거기만 고친다. 이 항목을 facade 에 되돌릴지는 2차에서 판단한다.

고아가 된 import 17개(collect_page_texts, describe_pages, resolve_runtime_*,
DocMeta, ProvenanceItem, BoundingBox)도 정리했다.

검증:
- 동작 스냅샷 54항목 불일치 0. split_documents_by_page(fixture 4종 x facade 2종
  x chunk_size 2종 x chunk_mode 2종의 청크 수·총 문자수·sha256), check_empty_text,
  check_appendix_keywords(표기 변형 7케이스)를 변경 전 코드와 대조했다.
- tests/unit 실패 목록이 변경 전후 21건 동일(전부 기존 실패).

facade 순 감소 745줄(10,873 → 10,128).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
inoray and others added 25 commits September 5, 2026 22:06
같은 배관(OCR 옵션·PDF 파이프라인·layout·컨버터 4개·enricher 생성)이 parser /
intelligent / convert 세 facade 에 복제돼 있다. 복제의 대가는 이미 치렀다 -
parser 의 사본이 원본을 따라가지 못해 check_empty_text 가 빠진 채로 남았고
크래시로 드러났다.

이 커밋은 모듈만 만든다. 기존 파일은 건드리지 않으므로 아무것도 깨지지 않는다.

담지 않은 것과 이유:
- enrichment(): 세 facade 의 본문이 시그니처·예외·PPT 처리에서 다르다. 특히
  convert 의 로컬 GenosServiceException 은 stage/error_type 인자를 받지 않아
  base 가 intelligent 판을 담으면 LLM 오류가 났을 때만 TypeError 로 터진다.
- safe_join / setup_logging / get_media_files / check_appendix_keywords:
  docling 런타임이 아니라 청킹·벡터 조합 헬퍼다.

서브클래스가 끼어드는 자리는 훅 3개(_pre_pipeline_setup / _force_page_images /
_post_runtime_setup)다. 첫 두 개가 PdfPipelineOptions 생성보다 앞에 있어야
generate_page_images 강제가 OCR 컨버터 옵션까지 반영된다.

갈라져 있어 그대로 보존한 축 2개(통일은 별도 이슈):
- formats.xlsx.processing_mode 기본값 - parser 는 tabular, 나머지는 docling.
  _xlsx_default_mode ClassVar 로 가른다.
- MetadataEnricher 의 thinking 인자 - parser·convert 는 넘기고 intelligent 는
  넘기지 않는다. _metadata_enricher_passes_thinking ClassVar 로 가른다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
parser 는 이 런타임을 상속이 아니라 합성으로 쓴다(self._intel). 그래서 base 에서
이름이 하나 사라지면 import 시점이 아니라 요청 처리 중에 AttributeError 로 죽는다.
check_empty_text 누락 크래시가 정확히 그 사고였다.

값이 맞는지가 아니라 이름이 있는지를 단정한다. 훅 호출 순서와 xlsx 기본값이
서브클래스마다 갈리는 사실도 함께 고정한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
세 facade 에 복제돼 있던 docling 배선을 DoclingRuntimeBase 로 대체한다.

- parser: 임베드 사본이던 IntelligentDocumentProcessor 가 base 서브클래스가 된다.
  남는 것은 enrichment() 뿐이다. 이름은 유지한다 — 유닛 2개가 최상위에서 import 한다.
- intelligent / convert: DocumentProcessor 가 base 를 상속하고, 자기 몫은
  _pre_pipeline_setup / _force_page_images / _post_runtime_setup 으로 옮긴다.

전 단계가 파이프라인 옵션 생성보다 앞이라는 것이 중요하다. table_image /
page_description 이 켜질 때 generate_page_images 를 강제하는데, 뒤로 밀리면
OCR 컨버터 옵션(deep copy)에 반영되지 않는다. 그 순서를 계약 테스트로 고정했다.

convert 의 load_documents / load_documents_with_docling(_ocr) 오버라이드는 남긴다.
전자는 HWP/HWPX 레거시 폴백과 .hml 빈 문서 처리(#323)를 담고 있고, 후자 둘은
base 판과 달리 save_images 변경 시 컨버터를 다시 만들지 않는다 — base 로 덮으면
매 호출 컨버터 재생성이 붙는다.

검증:
- 세 프로세서 각각 변경 전/후 인스턴스를 같은 프로세스에서 만들어 __dict__ 를
  대조했다. 값이 다른 항목은 enricher/mapper 객체 주소뿐이고, 새로 생기는 속성은
  intelligent·convert 의 _ext_aliases/_md_cfg/custom_fields_cfgs 3개(아무도 안 읽는다).
  parser 는 새로 생기거나 사라지는 속성이 0 이다.
- 골든 56케이스 차이 0. 표적 유닛 실패 집합이 베이스라인과 동일(18건).

facade 합계 1,110줄 감소, 신규 공용 모듈 +445. 저장소 순감 약 665줄.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
기존 파일 동작은 그대로다 — docling_ops 에는 클래스 2개가 덧붙고 나머지 둘은 신규다.

- common/docling_ops.py: HwpLoaderBase / DocxLoaderBase. 순수 docling 컨버터 조립이라
  이미 docling 을 import 하는 이 모듈이 제자리다. common/loaders.py 는 docling 무의존을
  유지한다.
- common/parser_config.py: custom_fields 설정 → spec/mapper. 설정 오류를 어느 예외로
  감쌀지는 여기서 정하지 않는다(GenosServiceException 이 22곳에 복제돼 있다) —
  호출부가 guard_config 에 자기 예외 팩토리를 넘긴다.
- serialize/parse_format.py: DoclingDocument → 응답 포맷. 디렉터리 이름을 output 으로
  하지 않은 이유는 `output:` 이 설정 yaml 12곳의 최상위 섹션명이라서다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2,497 → 1,605줄. 남는 것은 "어떤 확장자를 어느 경로로 보내는가"(포맷별 라우팅),
__call__, 응답 조립, GenOSVectorMeta 계열, GenosServiceException 이다.

3a — HwpDocumentLoader / DocxDocumentLoader 가 공용 base 의 빈 서브클래스가 된다.
  **GenericDocumentLoader 는 남긴다.** get_loader 가 곧 "어떤 확장자를 어느 로더로
  보내는가" 이고 계획 스스로 그 라우팅은 facade 몫이라고 적었다. 그 결정 덕에 계획이
  걸림돌로 꼽은 둘(공용 모듈의 docling import, 공용 모듈이 쓸 예외 클래스)이 사라졌다.

3b — _build_* 7개를 없애고 __init__ 이 _guard(label, builder, cfgs) 를 직접 부른다.
  예외 변환은 사라지지 않고 한 곳으로 모였다. 두 가지를 손봤다.
  - 포집 예외를 셋 중 가장 넓은 것으로 통일했다. 좁던 자리(json 은 ValueError 만)에서는
    설정 오류가 raw 로 새어 어느 yaml 이 문제인지 드러나지 않았다. 어느 쪽이든 기동 실패다.
  - 기동 오류(stage="custom_fields")와 요청 중 doc_type 매칭 오류(stage 없음)의 팩토리를
    나눴다. 하나로 합치면 후자의 에러 envelope 에 없던 stage 가 붙는다.
  런타임 doc_type 선택 헬퍼는 facade 에 남는다 — 유닛이 인스턴스 속성을 갈아끼우고
  메서드를 MagicMock 으로 대체한다.

3c — 직렬화 13개가 serialize/parse_format.py 로 간다. _item_to_html 은 지웠다
  (유일한 호출부가 주석 처리된 죽은 코드). _build_docling_response 는 남겼다 —
  직렬화가 아니라 응답 조립이고, 설정 4종을 읽고 문서를 변이하고 guardrail 로 외부
  HTTP 를 부른다. facade 에는 staticmethod 래퍼 12개가 남는다(유닛이 클래스 경유로
  부르고 patch.object 로 갈아끼운다). patch("facade.parser_processor.export_markdown")
  4곳은 새 모듈을 가리키도록 고쳤다.

검증: 골든 56케이스 차이 0(LLM 캐시 miss 0). 표적 유닛 실패 집합이 착수 전
베이스라인과 동일(18건). 변경 전/후 프로세서를 같은 프로세스에서 만들어 doc_type
16종에 대한 라우팅 산출을 대조했고 불일치 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
각 문서 머리에 완료 상태와 계획에서 달라진 점을 붙이고, README 에 진행 상태·실측
규모·결정 5가지를 정리한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
01~03 으로 파일이 줄었지만 "그래서 어디를 고치면 되는가" 가 코드에도 문서에도
드러나 있지 않았다. 이 커밋이 그 자리를 못박는다.

코드
- parser_processor.py 머리에 **고칠 자리 3개**(ROUTES / _route_* /
  _load_json_payload)를 적었다. 그 밖은 배관이고 구현은 공용 모듈에 있다.
- 확장자 if 사슬을 DocumentProcessor.ROUTES 표 + _route_* 핸들러로 바꿨다.
  핸들러가 None 을 돌려주면 다음 후보로 넘어가는 구조라 폴스루 2곳(.json 미매칭,
  .md text 모드)이 그대로 살아 있다. docling 분기 5곳이 복붙하던 마무리 6줄은
  _docling_response 하나로 모았다.
- _load_json_payload 가 doc_type 을 받는다. 05 가 이것을 JSON 정규화 훅으로
  지목하는데 @staticmethod(file_path) 라 게이팅을 할 수 없었다. 게이팅 없는
  정규화는 모든 JSON doc_type 의 산출을 바꾼다.
- chunking_processor.py 머리를 "사이트 조정 지점" 블록으로 묶고 그 끝에 경계를
  그었다. 참조 0 인 docling enum 맵 2개(parser)도 지웠다.

문서
- 개발 매뉴얼 5장의 **줄번호를 전부 걷어냈다.** 좌표 기반 문서는 유지가 안 된다 —
  실측으로 대조하니 파일 줄수·클래스 위치·심지어 파일 지목까지 틀려 있었다.
  이제 파일 경로 + 심볼 이름으로만 적는다. 5.8(복제 범위)은 "한 벌인 것" 표를
  더해 01~03 이 무엇을 통합했는지 보이게 했다.
- gitbook_doc/chunking_processor.md 를 신설했다. 프로세서 5종 중 청킹만
  레퍼런스가 없었다.
- 파싱 매뉴얼에 "새 문서 유형 추가하기" 절을 넣었다(05 조사 결과) — extractor
  4종 지원 매트릭스, transform 9종, 별칭 탐색 범위, 조용히 실패하는 자리,
  설정으로 안 되는 8가지와 우회법, 고칠 자리 3개, 파서→청커 element 계약,
  고객이 자기 골든을 만드는 절차.
- installation.md 의 "AI Search 팀에 문의" 와 "직접 수정" 이 충돌하던 자리를
  정리하고 새 문서로 연결했다.
- facade/README.md 삭제 — 존재하지 않는 디렉터리(genos_di/, docling/facade/)를
  설명하는 낡은 문서였다.
- 간소 프로필 2개에 "무엇을 위한 판이고 어디를 보면 되는지" 머리말을 붙였다.

검증: 골든 56케이스 차이 0. 유닛 전체 실패 집합이 착수 전과 동일(40건).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
01~05 는 "프로세서가 작아졌다" 를 증명한다. 그것이 "고객이 새 문서를 스스로 처리할
수 있다" 를 증명하지는 않는다. 이 커밋이 그것을 실제 문서로 확인한 기록이다.

- sample_files/drill/ 픽스처 15종 + 생성 스크립트(재생성 가능해야 축을 바꿀 수 있다)
- examples/parse_chunk/drill/ 실행기와 드릴 전용 custom_field 설정 14개
- docs/plans/facade-slimming/06-drill-results.md 결과 기록

드릴은 배포 설정을 건드리지 않는다. resource_dev 를 임시 디렉터리로 복사한 뒤 드릴
설정만 덧붙여 등록하고 그 사본을 --config 로 넘긴다. 그래서 돌려도 골든이 흔들리지 않는다.
그 목적으로 parse_chunk_test.py 에 --config / --chunker-config 를 열었고, 확장자 별칭도
그 설정에서 읽도록 고쳤다(러너가 파서보다 먼저 파일을 막는 일을 없앤다).

판정 — 합격 기준 6개 전부 통과:
  고친 파일 facade 1개 / 고친 위치는 전부 _load_json_payload(04 가 정한 앵커 3번) /
  27삽입 3삭제(30줄 이하) / 골든 56케이스 차이 0 / 새 doc_type metadata 가 청크에 실림 /
  파일 머리 주석에서 한 번에 도달. **공용 모듈은 한 줄도 건드리지 않았다.**

설정만으로 된 것 8 / 코드가 필요한 것 4 / 경계 확인 3.

계획과 달랐던 것 4가지(문서에 상세):
- B16(doc_type 충돌)의 폭발 반경이 작다. 기동이 아니라 요청 시점이고 그 doc_type 만
  죽는다 — 남의 doc_type 은 살아 있다.
- B1(동명 키 충돌)은 실패하지 않고 **틀린 값을 조용히 싣는다.** "청크가 나왔다" 는
  검증 기준이 될 수 없다. 이 드릴 러너도 처음엔 그걸로 오판했다.
- B3(2단 중첩)은 records_at 이 최초 매칭 1개라 3건 중 2건만 나온다. 역시 무경고.
- B15(빈 레코드 배열)는 청커가 예외로 죽는다 — 별도 이슈 후보.

드릴에서 만든 facade 수정은 커밋하지 않는다(계획의 범위 주의). 남기는 것은 픽스처와
절차와 결과다. B1·B3 의 "조용히 틀린 값" 은 고객 문서에 반영했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
각 문서 머리에 완료 상태와 계획에서 달라진 점을 붙이고, README 에 06 판정표와
남은 것(07 미착수, 별도 이슈 후보 5건)을 정리한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100줄 표면과 확장 훅 4개를 세우는 08 을 신설한다. 결정 7건(엑셀 데이터 훅,
__call__ 노출, 벡터 필드 유지, 대상 2종, 훅 호출 위치, 병합셀 규칙, 07-A=A3)과
훅 계약·core 변경 목록·검증 설계를 담았다.

08-A 는 출고 custom_fields 를 초기 개발 단계 수준으로 깎아 그 차이를 훅으로 메울
수 있는지 재는 ablation 이다. 리팩터링 전에 돌린다 - 이 결과가 toolbox 설계 입력이다.
기본 기능 집합은 최초 구현 시점(git log -S)과 사용 폭 두 실측으로 3계층으로 갈랐다.

1차 실행에서 하네스 결함 2건을 잡았다. --output-format json 강제가 청커를
parse-format 경로로 보내 문서 단위 custom_fields 를 통째로 빠뜨렸고(cs_hpp 5청크
채움 -> 18청크 0채움), doc_type 하나에 yaml 이 둘인 경우를 잘못 귀속했다.
전자를 못 잡았으면 llm 4종의 기준선이 빈 값이 되어 "빈 값으로 일치" 로 통과할 뻔했다.

메울 차이 8건을 특정했다. on_missing 은 어느 케이스에서도 산출을 안 바꿨고,
records_at 은 원천에 따라 갈린다. toolbox 신설 요구는 unfence_text 하나뿐이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
parser_processor.py 1,644줄을 core/parser.py 로 통째로 옮기고 facade 에는 28줄만
남긴다. 고객이 여는 파일이 그 28줄이다. 훅과 이름 변경은 08-3 이며 이번엔 이동만 한다.

GenosServiceException 을 core/errors.py 한 벌로 모은다. core 가 예외를 던지는 이상
활성 경로에 사본이 둘일 수 없다. facade 는 그 이름을 재수출해 main.py 핸들러가
그대로 잡게 한다.

이동으로 __file__ 상대 경로가 깨졌다. _resolve_default_parser_config_path 가
parent 를 기준으로 ../resource_dev 를 찾는데 core/ 로 한 단계 깊어져 facade/resource
를 보게 됐다. parents[1] 로 고쳤다.

테스트 참조 6곳을 core 로 재타겟했다. 로더·docling 런타임·경로 헬퍼는 처리 본체의
것이고, DocumentProcessor 와 GenosServiceException 만 facade 에서 계속 보인다.

골든 56케이스 차이 0. 표적 유닛 실패 집합은 착수 전 13건과 동일.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chunking_processor.py 1,223줄을 core/chunker.py 로 옮기고 facade 에는 88줄만 남긴다.
고객이 여는 파일이 그 88줄이고, 사이트가 실제로 바꾸는 두 가지만 들어 있다 —
적재 DB 컬럼(GenOSVectorMeta 23필드)과 청킹 옵션·헤더 구분자(GenosSmartChunker).

계획은 전부 core 로 옮긴 뒤 08-3 에서 표면만 되돌리는 2단계였다. (1) 만 끝낸 상태에서
유닛 44건이 깨졌고 대부분 mod.GenosSmartChunker / mod.GenOSVectorMeta 를 facade 모듈에서
꺼내 쓰는 자리였다 - 어차피 되돌릴 것이라 08-3 의 그 부분을 앞당겨 함께 넣었다.
44 -> 8건으로 줄었고 나머지 8건도 정리했다.

core 에는 사본을 두지 않는다. VECTOR_META / CHUNKER 를 None 으로 두어 배포되는 facade 가
반드시 정하게 했다. 기본값을 두면 스키마 사본이 둘이 되어 조용히 어긋난다.

_build_header_line 이 모듈 상수 대신 청커 클래스가 든 구분자를 읽는다. "크기 산정과
실제 부착이 같은 문자열을 봐야 한다" 는 기존 주석의 경고를 구조로 강제한다.

_single_marker_vector 는 staticmethod 라 스키마를 못 읽어 classmethod 로 바꿨다.
test_chunk_size_config 의 스파이는 모듈 전역만 갈아 끼우고 있어 self.CHUNKER 배선을
못 가로챈다 - 실제 배선도 함께 갈아 끼우게 고쳤다.

골든 56케이스 차이 0. 전체 유닛 실패 집합이 베이스라인 40건과 완전 동일.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
새 확장자를 받는 자리는 고객 표면이다. ROUTES 를 facade 로 올리고 core 는
ROUTES: tuple = () 로 비워 배포되는 facade 가 반드시 정하게 했다. 기본 표를 두면
사본이 둘이 되어 조용히 어긋난다.

핸들러 이름을 _route_* 에서 route_* 로 바꿨다. facade 의 표가 그 이름을 적으므로
비공개 표기를 유지할 이유가 없다.

진입점은 core 의 run() 이고 facade 가 __call__ 로 위임한다. 고객이 파일을 열었을 때
"요청이 여기서 시작한다" 가 보여야 한다.

골든 56케이스 차이 0. 표적 유닛(parser·extension_alias·custom_fields_routing)
실패 집합 베이스라인과 동일.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
custom_fields 로 안 풀리는 원천을 코어 수정 없이 처리하는 자리를 만든다.
pre_source / post_parse / pre_chunk / post_chunk 넷이고, 셋은 facade 의 __call__ 에서
직접 부른다 - 고객이 파일을 열었을 때 어느 단계에서 불리는지 보여야 한다.

pre_source 만 core 가 부른다. 확장자마다 로드 시점이 다르고, 특히 .json 은
custom_fields 가 매칭될 때만 읽히므로 __call__ 에서 미리 읽으면 동작이 바뀐다
(오늘 평문으로 처리되는 깨진 JSON 이 예외로 죽는다). 04 가 유일한 위험으로 꼽은
폴스루가 그 자리다.

산출 동일성은 두 층으로 지킨다. 훅을 안 덮어썼으면 원천을 읽는 비용조차 치르지 않고,
덮어썼어도 받은 객체를 그대로 돌려주면 파생 입력을 만들지 않는다.

인코딩 폴백(utf-8-sig -> utf-8 -> cp949)을 공용 모듈에 넣었다. 사이트 특성이 아니라
보편 문제다. 06 드릴에서 BOM/CP949 가 고객 코드 9줄이던 것이 0줄이 된다.

청커 __call__ 을 load_input / chunk 로 쪼개 네 단계가 보이게 했다. 가드레일 컨텍스트가
훅 시그니처를 더럽히지 않도록 ChunkInput dataclass 로 묶었다.

toolbox 는 재수출만 29항목이고 신규 구현이 없다. 08-A 가 유일한 신규 함수로 지목한
unfence_text 는 md_text_fence.transform 이 이미 평범한 함수라 얇은 래퍼로 끝났다.
값 변환기는 yaml 의 transform: 이 부르는 것과 같은 함수라 설정과 코드가 어긋나지 않는다.

훅 배선은 골든이 못 잡는다(아무 것도 안 하는 훅이므로 차이 0 으로 통과한다).
단위 테스트 11건을 신설해 배선을 직접 단정했다. 그 과정에서 doc_type 이 소문자로
정규화되어 훅에 온다는 사실을 확인해 단정문으로 못박았다.

골든 56케이스 차이 0. 전체 유닛 실패 집합 베이스라인 40건과 동일(1,710 통과).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
엑셀도 파일이 아니라 데이터로 훅에 넘긴다. 세 갈래(레코드 매핑 / docling / tabular)가
route_tabular 한 곳을 지나므로 거기서 격자를 준다. xlsx_processor.load_sheets 가
"출력 형태에 독립적인 중립 표현" 으로 이미 그 자리에 있었다 - 새로 발명한 것이 없다.

고객이 어떤 라이브러리를 쓰든 받는다. normalize_sheets 가 2차원 행 / DataFrame /
list[dict] 5가지 형을 표준 격자로 되돌리며, pandas 도 polars 도 import 하지 않고
덕타이핑(.columns + .to_numpy())으로 판정한다.

병합 정보는 결정 D6 대로 처리한다. 행·열 개수가 그대로면 좌표가 유효하므로 유지하고
달라지면 버린다. 무조건 버리면 값만 정규화한 사용자가 멀티헤더 자동판정을 잃는다.

격자 훅이 파싱보다 먼저 워크북을 읽는 바람에 읽을 수 없는 파일의 오류 시점과 메시지가
달라졌다. 존재하지 않는 파일로 라우팅만 확인하는 기존 테스트 2건이 잡았다. 읽기 실패 시
훅을 건너뛰게 고쳤고, 겸사겸사 xlsx 를 두 번 읽던 것도 없앴다.

cli() 로 파일 단독 실행을 연다. facade 는 두 줄이고 본체는 core/cli.py 다. 파서 산출을
청커가 그대로 받으므로 두 배포본이 명령 두 줄로 이어진다.

facade 최종 92 / 100줄. 골든 56케이스 차이 0, 전체 유닛 40건 베이스라인 동일,
훅·격자 단위 테스트 19건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
기존 doc_type 의 custom_fields 를 초기 개발 단계 수준으로 깎고 그 차이를 훅으로 실제로
메웠다. 5 doc_type / 13케이스가 훅 26줄로 차이 0 이 됐다(doc_type 당 평균 5줄).
toolbox 밖 import 는 0 이다.

이 드릴이 설계 구멍을 하나 잡았다. post_parse 가 직렬화 뒤에 있어 docling 경로의 청크
메타를 못 바꿨다 - result["metadata"] 는 호출자용이고 청커는 DoclingDocument 의
KeyValueItem 을 읽는다. 대부분의 문서가 그 경로다. toolbox.set_chunk_metadata 로 막았다.
훅이 아무 것도 안 하면 골든은 차이 0 으로 통과하므로 골든은 이 구멍을 못 잡는다.

하네스도 고쳤다. source.pre 를 통째로 지우면 front_matter 승격 / text_fence /
marker_headings 세 기능이 함께 사라져 원인을 못 가른다. prune 을 임의 깊이로 바꿔
셋을 따로 쟀고, 그러고 나서야 text_fence 오판이 드러났다.

기본 기능 집합이 확정됐다. 08-A 잠정 분류에서 merge_rows / sections / ignore_keys /
markdown.front_matter 넷이 기본으로 되돌아왔다. 전부 설정이 값 파이프라인 안쪽이나
순회 자체를 바꾸는 것들이고, 훅이 다루는 것은 그 바깥(원천 모양·산출 모양)이다.

골든 56케이스 차이 0(드릴 되돌린 뒤). 훅 단위 테스트 22건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
06 이 JSON 으로 물은 것을 나머지 세 포맷에 묻는다 - 설정으로 안 되는 원천을 고객이
facade 훅으로 처리할 수 있는가. 훅이 값을 하는지 재는 것이 목적이라 custom_fields 를
쓰지 않는다.

실무 변형 7개로 1단계 2/7 -> 2단계 7/7. 고객 코드는 24줄이고 doc_type 당 1~3줄이다.
필요한 도구는 전부 toolbox 에 있었다(신규 함수 0).

x2 가 병합 유지 규칙(D6)을 실전에서 확인했다. 값만 바꾸고 행·열 개수를 그대로 두니
컬럼명이 연락처_전화 로 유지됐다. 무조건 버리는 규칙이었으면 전화 가 됐다.

드릴 자체에서 둘을 배웠다. "본문에 글자가 있는가" 는 약한 단정이다 - 마커 승격은 글자를
바꾸지 않아 m1·m2 가 1단계에서 통과해 버렸고, 청크 수와 문장 연결로 바꾸고서야 신호가
잡혔다(m1: 1청크 -> 3청크). 그리고 sheets_to_xlsx 는 병합을 못 만든다는 문서화된 한계가
픽스처 생성기에서 그대로 드러나 openpyxl 로 진짜 병합셀을 만들어야 했다.

JSON 15종 재실행은 06 의 8종에서 13종으로 올랐다. b11(BOM·CP949)이 고객 코드 9줄에서
0줄이 됐다 - core 인코딩 폴백이 흡수했다. b12(JSONL)는 훅 3줄이고 단위 테스트가 고정한다.
b15 는 06 이 이미 분리한 사전 결함이다.

드릴 훅은 되돌렸고 골든 56케이스 차이 0 이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gitbook_doc/facade_hooks.md 를 신설한다. 고칠 파일 둘, 훅이 불리는 시점, 확장자별 데이터
형, toolbox 목록, 단독 실행, 그리고 훅으로 안 되는 것 3가지(08-B 실측)를 담았다.
프로세서 문서 2종 머리에서 링크한다.

A3 배포 절차를 확인하다 07 의 전제 하나가 틀린 것을 찾았다. src/preprocessor.py 는
마운트 지점이 아니라 머리에 DEPRECATED 가 붙은 예시이고 "실제로는 DB의 값을 가져와서
사용한다" 고 적혀 있다. 대신 릴리스 갱신이 .git 을 뺀 tar 통째 갱신이라는 사실을 확인했고,
tar 는 tarball 에 없는 파일을 지우지 않으므로 A3 가 성립할 여지가 있다.

다만 코드서빙이 어느 facade 를 /run 에 마운트할지 정하는 방법을 저장소에서 확인하지
못했다. 그것을 알아야 "벤더가 배포하지 않는 이름의 파일을 고객이 소유한다" 가 성립한다.
그때까지는 A2(패치 보관 후 재적용)를 기본으로 하고 릴리스 노트에 템플릿 변경 여부를
표시한다.

openpyxl / beautifulsoup4 / pandas 를 pyproject 에 명시 선언했다. 저장소 코드가 직접
import 하는데 지금까지 전이 의존이었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
배포본에 나가면 안 되는 것을 뺀다. whitelist 가 genon 전체라 sample_files/monimo(고객사
실 문서 48건)와 docs(리팩터링 계획·판정·미해결 결함 목록)가 공개 배포본으로 그대로
나가고 있었다. 일반 샘플(pdf_sample 등)은 남긴다 - 고객이 parse_chunk_golden.py 로 자기
기준선을 찍을 때 쓴다.

출고 custom_field yaml 24개의 v2 자동변환 배너를 걷었다. 고객이 예시로 삼을 파일의 첫
화면이 벤더 내부 이관 안내였다. 배너 아래 산문(원천 파일·헤더·매핑 근거)은 값이 있어
그대로 둔다 - 이관이 끝난 custom_field_faq.yaml 도 배너만 없고 같은 산문을 갖고 있다.
24개 전부 yaml 파싱 결과가 동일하고 precheck 는 기동실패 0 본문변화 0 이다.

07-A 를 A2 로 확정한다. 실행 형태가 루트 main.py 로 정해졌고, main.py 가
facade/parser_processor.py 와 chunking_processor.py 를 각각 생성해 /parser·/chunker 에
붙인다. 즉 고객이 여는 파일이 벤더가 배포하는 그 파일이라 A3(벤더가 배포하지 않는
이름의 파일을 고객이 소유)가 성립하지 않는다. 릴리스 전에 patch 로 보관하고 뒤에
재적용하며, 성립 조건은 훅 시그니처와 ROUTES 형태를 고정 API 로 유지하는 것이다.

07-C 서술도 정정했다. "한 서빙이 파싱과 청킹을 둘 다 할 수 없다" 는 facade 한 개만
preprocessor.py 로 마운트하는 형태에만 해당한다. main.py 형태는 한 서빙이 둘 다 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
07-A 를 A2 로 확정하면서 실행 형태가 루트 main.py 로 정해졌으니 그 형태로 확인할 수단이
필요하다. 기존 예제는 게이트웨이나 분리 배포된 /run 만 부른다.

examples/code_serving/local_main_test.{py,sh} 를 신설한다. FastAPI TestClient 로 앱을
직접 호출하므로 네트워크가 필요 없고, main.py 가 만드는 프로세서 5종과 라우팅·예외
핸들러를 전부 실제로 태운다. 표 경로(xlsx -> elements 2건 -> 청크 2건, doc_type 스탬프)와
docling 경로(md -> document -> 청크 1건, HEADER: 접두) 둘 다 통과한다.

로컬에서 main.py 를 올릴 때 걸리는 것 셋을 함께 확인해 기록했다. common.settings 가
MQ·MinIO 접속 정보를 필수로 요구하는데 그 값이 오는 genon/env/.env.<profile> 은 저장소에
없고, LOG_PATH 는 list[str] 이라 env 로 줄 때 JSON 배열이어야 하며, PREPROCESSOR_ID 가
있으면 MinIO 에서 /app/resource 로 내려받으려 해서 로컬에서는 주지 말아야 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
code_serving_dev_manual.md 가 08 의 구조 변경을 반영하지 않고 있었다. 5.1 코드 지도에
facade/core/ 를 넣고, 5.4·5.5 의 파일 구획 표를 훅·ROUTES·벡터스키마 기준으로 다시 썼다.
_route_* 를 route_* 로 고치고, 5.8 복제 범위에 parser·chunker 본체가 한 벌로 모인 것을
반영했다. 부록 D 에 chunking_processor.md 와 facade_hooks.md 를 링크했다.

7.2 에 (h) 확장 훅 레시피를 신설하고 레시피 (a)~(h) 목차를 붙였다. 새 doc_type 은 대부분
설정만으로 되므로 (g) 를 먼저 보고 안 될 때 (h) 라는 순서를 명시했다.

예시는 실제 doc_type(monimo_event)의 원천이 조금 바뀐 경우 둘로 만들고 전부 돌려서
확인했다. 관계사별 2단 중첩은 3건 중 2건만 청크가 되고 GROUP_C 가 전부 기본값이 되며,
목록·상세 분리는 DETAIL_TEXT 가 전부 비어 본문이 제목만 남는다. 둘 다 에러가 나지 않아
산출을 열어 보지 않으면 모른다는 점을 예시의 핵심으로 적었다. 훅 적용 후 2건->3건 /
IFP->HPP,HPP,SSF / 빈 값->평문화된 마크다운 을 실측으로 기록했다.

원천 2개를 sample_files/drill 에 넣어 독자가 그대로 재현할 수 있게 했다. 드릴 러너에는
넣지 않았다 - 훅 없이는 실패가 정상 상태라 상시 실패 케이스가 된다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RAG 검색을 방해하는 문자 노이즈를 걷어낸다. 상담 HTML 실측(청크 11건/2,582자)에서
특수문자 225개 중 지워서 이득인 것은 장식 마커 13개와 미해독 엔티티 3개뿐이었다.
파이프 66개는 마크다운 표의 칸 경계이고 대괄호 12개는 파이프라인이 붙인 라벨이라
"특수문자를 다 지운다"는 오답이다. 그래서 규칙은 장식 마커 하나로 좁혔다.

- chunking.text_cleanup 을 resource/resource_dev 청커 설정에서 safe + 마커 규칙으로
  켰다. intelligent/convert 는 다른 사이트가 쓰고 재색인 시점도 달라 두지 않았다.
- 엔티티는 규칙이 아니라 파싱에서 고쳤다. md 백엔드가 본문과 표 셀에서 해독하지
  않아 `&gt;` 가 리터럴로 남았고, HTML 블록이 섞인 문서는 export_to_html 왕복에서
  그대로 굳었다. 코드 스팬/펜스는 사양대로 건드리지 않는다.
- post_chunk 로 정제할 수 있게 toolbox.refresh_stats 를 열었다. 훅은 청킹이 끝난
  뒤라 n_char 와 청크 순번이 옛 값으로 남는다(실측 11건 중 9건 불일치).
- 단일 마커 청크 경로만 순번을 1 부터 넣고 n_chunk_of_doc 을 비워 두었다. 다른 세
  경로와 같이 0 기준으로 맞췄다.
- 신규 확장자를 ROUTES 에 등록하기 전에 한 번 넣어 보는 것만으로 원본이 PDF 로
  덮어써지던 것을 막았다. 파생 PDF 경로가 입력 경로와 같으면 임시 디렉터리로 돌린다.
- 파사드 두 파일의 계약 설명을 실제와 맞췄다. pre_chunk 는 DoclingDocument 가 아니라
  그 직렬화 dict 를 받는다.
- 설정 주석의 거짓 문장을 고쳤다(7파일). 표 행을 건너뛰는 것은 line 뿐이고 find 는
  표 안에서도 치환한다.

검증: 골든 56케이스 재기록 후 차이 0, doc_type 검증 PASS 22/FAIL 3(사전 실패),
examples/text_cleanup 3종 통과, 신규 단위 7건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@inoray inoray linked an issue Sep 6, 2026 that may be closed by this pull request
5 tasks
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 335 files, which is 235 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 4d2932c7-5e43-4fc8-90b9-7aec69bdc44d

📥 Commits

Reviewing files that changed from the base of the PR and between a18c8ec and 53d401b.

⛔ Files ignored due to path filters (4)
  • genon/preprocessor/sample_files/drill/x1_junk_header.xlsx is excluded by !**/*.xlsx
  • genon/preprocessor/sample_files/drill/x2_value_only.xlsx is excluded by !**/*.xlsx
  • genon/preprocessor/sample_files/drill/x3_multi_sheet.xlsx is excluded by !**/*.xlsx
  • genon/preprocessor/sample_files/monimo/monimo_stock_insight_sample.xlsx is excluded by !**/*.xlsx
📒 Files selected for processing (335)
  • .claude/agents/scope-check.md
  • .claude/hooks/large-file-read-guard.sh
  • .claude/hooks/pytest-filter.sh
  • .claude/settings.json
  • .claude/skills/create-patch-bundle/SKILL.md
  • .claude/skills/deploy-code-serving/SKILL.md
  • .claude/skills/issue-start/SKILL.md
  • .claude/skills/open-pr/SKILL.md
  • .claude/skills/wip/SKILL.md
  • .codex/config.toml
  • .dockerignore
  • .github/workflows/develop.yml
  • .gitignore
  • .ignore
  • CLAUDE.md
  • Dockerfile
  • build-script/code-serving-doc-parser/Dockerfile
  • build-script/code-serving-doc-parser/Dockerfile.gpu
  • build-script/code-serving-doc-parser/pyproject.toml
  • build-script/create-patch-bundle.sh
  • build-script/sync-serving-repo.sh
  • docling/backend/html_backend.py
  • docling/backend/md_backend.py
  • genon/.gitignore
  • genon/preprocessor/converters/html_flatten.py
  • genon/preprocessor/converters/md_marker_headings.py
  • genon/preprocessor/converters/md_text_fence.py
  • genon/preprocessor/converters/xlsx_processor.py
  • genon/preprocessor/docker/Dockerfile.standard
  • genon/preprocessor/docker/Dockerfile.synap
  • genon/preprocessor/docs/plans/01-semantic-body-explicit.md
  • genon/preprocessor/docs/plans/02-semantic-exclude-merge.md
  • genon/preprocessor/docs/plans/03-semantic-field-pipeline.md
  • genon/preprocessor/docs/plans/04-document-field-pipeline.md
  • genon/preprocessor/docs/plans/05-transform-html-text.md
  • genon/preprocessor/docs/plans/06-chunk-text-rules.md
  • genon/preprocessor/docs/plans/07-intelligent-parity.md
  • genon/preprocessor/docs/plans/08-intelligent-tabular-llm-fields.md
  • genon/preprocessor/docs/plans/09-intelligent-format-routing.md
  • genon/preprocessor/docs/plans/10-intelligent-record-kinds.md
  • genon/preprocessor/docs/plans/README.md
  • genon/preprocessor/docs/plans/facade-slimming/00-golden-baseline.md
  • genon/preprocessor/docs/plans/facade-slimming/01-chunking-dead-code.md
  • genon/preprocessor/docs/plans/facade-slimming/02-docling-runtime.md
  • genon/preprocessor/docs/plans/facade-slimming/03-parser-decompose.md
  • genon/preprocessor/docs/plans/facade-slimming/04-user-surface.md
  • genon/preprocessor/docs/plans/facade-slimming/05-customer-extension.md
  • genon/preprocessor/docs/plans/facade-slimming/06-drill-results.md
  • genon/preprocessor/docs/plans/facade-slimming/06-new-doctype-drill.md
  • genon/preprocessor/docs/plans/facade-slimming/07-customer-change-lifecycle.md
  • genon/preprocessor/docs/plans/facade-slimming/08-4-format-drill-results.md
  • genon/preprocessor/docs/plans/facade-slimming/08-A-ablation-results.md
  • genon/preprocessor/docs/plans/facade-slimming/08-B-ablation-results.md
  • genon/preprocessor/docs/plans/facade-slimming/08-customer-facade.md
  • genon/preprocessor/docs/plans/facade-slimming/README.md
  • genon/preprocessor/docs/plans/facade-slimming/WORKFLOW.md
  • genon/preprocessor/examples/code_serving/local_main_test.py
  • genon/preprocessor/examples/code_serving/local_main_test.sh
  • genon/preprocessor/examples/code_serving/serving_gateway_preprocess_test.py
  • genon/preprocessor/examples/code_serving/serving_gateway_test.py
  • genon/preprocessor/examples/code_serving/serving_gateway_test.sh
  • genon/preprocessor/examples/config_precheck/migrate_to_v2.py
  • genon/preprocessor/examples/config_precheck/migrate_to_v2.sh
  • genon/preprocessor/examples/config_precheck/precheck_custom_fields.py
  • genon/preprocessor/examples/config_precheck/precheck_custom_fields.sh
  • genon/preprocessor/examples/config_precheck/verify_v2_equivalence.py
  • genon/preprocessor/examples/config_precheck/verify_v2_equivalence.sh
  • genon/preprocessor/examples/parse_chunk/ablation/run_ablation.py
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b1.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b11.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b12.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b13.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b14.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b15.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b17.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b2.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b3.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b4.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b5.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b6.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b7.yaml
  • genon/preprocessor/examples/parse_chunk/drill/configs/custom_field_drill_b8.yaml
  • genon/preprocessor/examples/parse_chunk/drill/make_drill_fixtures.py
  • genon/preprocessor/examples/parse_chunk/drill/make_format_fixtures.py
  • genon/preprocessor/examples/parse_chunk/drill/run_drill.py
  • genon/preprocessor/examples/parse_chunk/drill/run_format_drill.py
  • genon/preprocessor/examples/parse_chunk/golden_normalize.py
  • genon/preprocessor/examples/parse_chunk/make_product_hpp_fields_sample.py
  • genon/preprocessor/examples/parse_chunk/make_product_slf_fields_sample.py
  • genon/preprocessor/examples/parse_chunk/make_stock_insight_sample.py
  • genon/preprocessor/examples/parse_chunk/make_text_cleanup_noise_sample.py
  • genon/preprocessor/examples/parse_chunk/parse_chunk_golden.py
  • genon/preprocessor/examples/parse_chunk/parse_chunk_test.py
  • genon/preprocessor/examples/parse_chunk/parse_chunk_test.sh
  • genon/preprocessor/examples/parse_chunk/parse_chunk_verify.py
  • genon/preprocessor/examples/parse_chunk/parse_chunk_verify.sh
  • genon/preprocessor/examples/text_cleanup/README.md
  • genon/preprocessor/examples/text_cleanup/cleanup_rules.yaml
  • genon/preprocessor/examples/text_cleanup/hooks_both.py
  • genon/preprocessor/examples/text_cleanup/hooks_post_chunk.py
  • genon/preprocessor/examples/text_cleanup/run_cleanup_examples.py
  • genon/preprocessor/examples/text_cleanup/run_cleanup_examples.sh
  • genon/preprocessor/facade/README.md
  • genon/preprocessor/facade/attachment_processor.py
  • genon/preprocessor/facade/chunking/__init__.py
  • genon/preprocessor/facade/chunking/doc_prefix.py
  • genon/preprocessor/facade/chunking/header_path.py
  • genon/preprocessor/facade/chunking/hybrid_chunker.py
  • genon/preprocessor/facade/chunking/page_split.py
  • genon/preprocessor/facade/chunking/rich_cells.py
  • genon/preprocessor/facade/chunking/smart_chunker.py
  • genon/preprocessor/facade/chunking/table_blocks.py
  • genon/preprocessor/facade/chunking/table_html.py
  • genon/preprocessor/facade/chunking/table_shape.py
  • genon/preprocessor/facade/chunking/table_splitter.py
  • genon/preprocessor/facade/chunking/table_variants.py
  • genon/preprocessor/facade/chunking/text_norm.py
  • genon/preprocessor/facade/chunking_processor.py
  • genon/preprocessor/facade/common/__init__.py
  • genon/preprocessor/facade/common/appendix.py
  • genon/preprocessor/facade/common/config_parse.py
  • genon/preprocessor/facade/common/doc_meta.py
  • genon/preprocessor/facade/common/docling_ops.py
  • genon/preprocessor/facade/common/docling_runtime.py
  • genon/preprocessor/facade/common/file_probe.py
  • genon/preprocessor/facade/common/format_alias.py
  • genon/preprocessor/facade/common/loaders.py
  • genon/preprocessor/facade/common/markdown_export.py
  • genon/preprocessor/facade/common/parser_config.py
  • genon/preprocessor/facade/common/pdf_convert.py
  • genon/preprocessor/facade/common/pipeline_setup.py
  • genon/preprocessor/facade/common/runtime.py
  • genon/preprocessor/facade/common/runtime_kwargs.py
  • genon/preprocessor/facade/common/vector_meta.py
  • genon/preprocessor/facade/convert_processor.py
  • genon/preprocessor/facade/core/__init__.py
  • genon/preprocessor/facade/core/chunker.py
  • genon/preprocessor/facade/core/cli.py
  • genon/preprocessor/facade/core/errors.py
  • genon/preprocessor/facade/core/parser.py
  • genon/preprocessor/facade/core/toolbox.py
  • genon/preprocessor/facade/enrichment/__init__.py
  • genon/preprocessor/facade/enrichment/config_schema.py
  • genon/preprocessor/facade/enrichment/config_v2.py
  • genon/preprocessor/facade/enrichment/custom_fields_enricher.py
  • genon/preprocessor/facade/enrichment/enrichment_config.py
  • genon/preprocessor/facade/enrichment/field_transforms.py
  • genon/preprocessor/facade/enrichment/json_records.py
  • genon/preprocessor/facade/enrichment/json_semantic.py
  • genon/preprocessor/facade/enrichment/markdown_front_matter.py
  • genon/preprocessor/facade/enrichment/metadata_enricher.py
  • genon/preprocessor/facade/enrichment/page_description.py
  • genon/preprocessor/facade/enrichment/table_description.py
  • genon/preprocessor/facade/enrichment/table_text_context.py
  • genon/preprocessor/facade/enrichment/table_text_description.py
  • genon/preprocessor/facade/enrichment/tabular_custom_fields.py
  • genon/preprocessor/facade/gitbook_doc/chunking_processor.md
  • genon/preprocessor/facade/gitbook_doc/code_serving_dev_manual.md
  • genon/preprocessor/facade/gitbook_doc/convert_processor.md
  • genon/preprocessor/facade/gitbook_doc/facade_hooks.md
  • genon/preprocessor/facade/gitbook_doc/installation.md
  • genon/preprocessor/facade/gitbook_doc/intelligent_processor.md
  • genon/preprocessor/facade/gitbook_doc/parser_processor.md
  • genon/preprocessor/facade/guardrail/source_text.py
  • genon/preprocessor/facade/intelligent_processor.py
  • genon/preprocessor/facade/legacy/BOK_적재용_규정.py
  • genon/preprocessor/facade/legacy/BOK_적재용_내부.py
  • genon/preprocessor/facade/legacy/BOK_적재용_외부.py
  • genon/preprocessor/facade/parser_processor.py
  • genon/preprocessor/facade/serialize/__init__.py
  • genon/preprocessor/facade/serialize/parse_format.py
  • genon/preprocessor/pyproject.toml
  • genon/preprocessor/resource/chunking_processor_config.yaml
  • genon/preprocessor/resource/chunking_processor_config_simple.yaml
  • genon/preprocessor/resource/convert_processor_config.yaml
  • genon/preprocessor/resource/custom_field_card.yaml
  • genon/preprocessor/resource/custom_field_cs_hpp.yaml
  • genon/preprocessor/resource/custom_field_cs_slf.yaml
  • genon/preprocessor/resource/custom_field_cs_ssf.yaml
  • genon/preprocessor/resource/custom_field_cs_sss.yaml
  • genon/preprocessor/resource/custom_field_faq.yaml
  • genon/preprocessor/resource/custom_field_faq_json.yaml
  • genon/preprocessor/resource/custom_field_link.yaml
  • genon/preprocessor/resource/custom_field_menu.yaml
  • genon/preprocessor/resource/custom_field_monimo_event.yaml
  • genon/preprocessor/resource/custom_field_monimo_news.yaml
  • genon/preprocessor/resource/custom_field_product_hpp.yaml
  • genon/preprocessor/resource/custom_field_product_hpp_json.yaml
  • genon/preprocessor/resource/custom_field_product_hpp_semantic.yaml
  • genon/preprocessor/resource/custom_field_product_slf.yaml
  • genon/preprocessor/resource/custom_field_product_ssf.yaml
  • genon/preprocessor/resource/custom_field_research_report.yaml
  • genon/preprocessor/resource/custom_field_stock_insight.yaml
  • genon/preprocessor/resource/custom_field_term.yaml
  • genon/preprocessor/resource/intelligent_processor_config.yaml
  • genon/preprocessor/resource/parser_processor_config.yaml
  • genon/preprocessor/resource/parser_processor_config_simple.yaml
  • genon/preprocessor/resource/prompt_table_text_description_rag.md
  • genon/preprocessor/resource/templates/custom_field_TEMPLATE_json.yaml
  • genon/preprocessor/resource/templates/custom_field_TEMPLATE_llm.yaml
  • genon/preprocessor/resource/templates/custom_field_TEMPLATE_semantic.yaml
  • genon/preprocessor/resource/templates/custom_field_TEMPLATE_tabular.yaml
  • genon/preprocessor/resource_dev/chunking_processor_config.yaml
  • genon/preprocessor/resource_dev/convert_processor_config.yaml
  • genon/preprocessor/resource_dev/custom_field_authors.yaml
  • genon/preprocessor/resource_dev/custom_field_card.yaml
  • genon/preprocessor/resource_dev/custom_field_cs_hpp.yaml
  • genon/preprocessor/resource_dev/custom_field_cs_slf.yaml
  • genon/preprocessor/resource_dev/custom_field_cs_ssf.yaml
  • genon/preprocessor/resource_dev/custom_field_cs_sss.yaml
  • genon/preprocessor/resource_dev/custom_field_faq.yaml
  • genon/preprocessor/resource_dev/custom_field_faq_json.yaml
  • genon/preprocessor/resource_dev/custom_field_link.yaml
  • genon/preprocessor/resource_dev/custom_field_menu.yaml
  • genon/preprocessor/resource_dev/custom_field_monimo_event.yaml
  • genon/preprocessor/resource_dev/custom_field_monimo_news.yaml
  • genon/preprocessor/resource_dev/custom_field_product_hpp.yaml
  • genon/preprocessor/resource_dev/custom_field_product_hpp_semantic.yaml
  • genon/preprocessor/resource_dev/custom_field_product_slf.yaml
  • genon/preprocessor/resource_dev/custom_field_product_ssf.yaml
  • genon/preprocessor/resource_dev/custom_field_stock_insight.yaml
  • genon/preprocessor/resource_dev/custom_field_term.yaml
  • genon/preprocessor/resource_dev/intelligent_processor_config.yaml
  • genon/preprocessor/resource_dev/parser_processor_config.yaml
  • genon/preprocessor/resource_dev/prompt_table_text_description_rag.md
  • genon/preprocessor/sample_files/drill/b11_bom.json
  • genon/preprocessor/sample_files/drill/b11_cp949.json
  • genon/preprocessor/sample_files/drill/b12_feed.jsonl
  • genon/preprocessor/sample_files/drill/b13_null_missing.json
  • genon/preprocessor/sample_files/drill/b14_escaped_json.json
  • genon/preprocessor/sample_files/drill/b15_empty_records.json
  • genon/preprocessor/sample_files/drill/b1_name_collision.json
  • genon/preprocessor/sample_files/drill/b2_dynamic_keys.json
  • genon/preprocessor/sample_files/drill/b3_nested_records.json
  • genon/preprocessor/sample_files/drill/b4_conditional.json
  • genon/preprocessor/sample_files/drill/b5_type_drift.json
  • genon/preprocessor/sample_files/drill/b6_html_table.json
  • genon/preprocessor/sample_files/drill/b7_mixed.json
  • genon/preprocessor/sample_files/drill/b8_deep_nesting.json
  • genon/preprocessor/sample_files/drill/e1_nested_by_company.json
  • genon/preprocessor/sample_files/drill/e2_detail_joined.json
  • genon/preprocessor/sample_files/drill/h1_srcdoc.html
  • genon/preprocessor/sample_files/drill/h2_hidden_accordion.html
  • genon/preprocessor/sample_files/drill/m1_marker_headings.md
  • genon/preprocessor/sample_files/drill/m2_text_fence.md
  • genon/preprocessor/sample_files/monimo/.INC_235488_02_20260626103138.html.parsed
  • genon/preprocessor/sample_files/monimo/monimo_cs_hpp_chunksize_sample.html
  • genon/preprocessor/sample_files/monimo/monimo_cs_hpp_large_table_sample.html
  • genon/preprocessor/sample_files/monimo/monimo_cs_hpp_marker_nospace_sample.html
  • genon/preprocessor/sample_files/monimo/monimo_cs_hpp_marker_real_dom_sample.html
  • genon/preprocessor/sample_files/monimo/monimo_cs_hpp_marker_sections_sample.html
  • genon/preprocessor/sample_files/monimo/monimo_cs_hpp_rich_table_sample.html
  • genon/preprocessor/sample_files/monimo/monimo_cs_sss_chunksize_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_event_table_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_link_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_news_chunksize_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_product_hpp_fields_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_product_hpp_rich_table_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_product_hpp_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_product_hpp_wcms_sample.json
  • genon/preprocessor/sample_files/monimo/monimo_product_slf_fence_sample.md
  • genon/preprocessor/sample_files/monimo/monimo_product_slf_fields_sample.md
  • genon/preprocessor/sample_files/text_cleanup_noise_sample.md
  • genon/preprocessor/scripts/register_image.sh
  • genon/preprocessor/tests/fixtures/cs_hpp_rich_cell_tables.html
  • genon/preprocessor/tests/fixtures/cs_hpp_styled_tables.md
  • genon/preprocessor/tests/fixtures/html/accordion_wrapped_nested_list.html
  • genon/preprocessor/tests/fixtures/product_hpp_fee_table.json
  • genon/preprocessor/tests/fixtures/table_shapes/merged_header.html
  • genon/preprocessor/tests/fixtures/table_shapes/mixed_doc.html
  • genon/preprocessor/tests/fixtures/table_shapes/rowspan_groups.html
  • genon/preprocessor/tests/fixtures/table_shapes/simple_grid.html
  • genon/preprocessor/tests/fixtures/table_text_description/table_description_rag_sample.html
  • genon/preprocessor/tests/fixtures/table_text_description/table_description_rag_sample.md
  • genon/preprocessor/tests/regression/baselines/md_README.json
  • genon/preprocessor/tests/regression/baselines/md_md_sample.json
  • genon/preprocessor/tests/regression/baselines/md_md_sample2.json
  • genon/preprocessor/tests/regression/baselines/md_text_cleanup_noise_sample.json
  • genon/preprocessor/tests/unit/table_invariants.py
  • genon/preprocessor/tests/unit/test_attachment_compact_tables_unit.py
  • genon/preprocessor/tests/unit/test_body_fields_unit.py
  • genon/preprocessor/tests/unit/test_chunk_prefix_fields_unit.py
  • genon/preprocessor/tests/unit/test_chunk_size_config.py
  • genon/preprocessor/tests/unit/test_chunking_processor_unit.py
  • genon/preprocessor/tests/unit/test_config_precheck_unit.py
  • genon/preprocessor/tests/unit/test_config_v2_unit.py
  • genon/preprocessor/tests/unit/test_cs_hpp_styled_table_chunks.py
  • genon/preprocessor/tests/unit/test_custom_fields_routing.py
  • genon/preprocessor/tests/unit/test_custom_fields_table_descriptions.py
  • genon/preprocessor/tests/unit/test_custom_fields_table_format_unit.py
  • genon/preprocessor/tests/unit/test_doc_type_chunk_size.py
  • genon/preprocessor/tests/unit/test_docling_runtime_contract.py
  • genon/preprocessor/tests/unit/test_enrichment_yaml_unit.py
  • genon/preprocessor/tests/unit/test_extension_alias_unit.py
  • genon/preprocessor/tests/unit/test_facade_hooks_unit.py
  • genon/preprocessor/tests/unit/test_field_labels_unit.py
  • genon/preprocessor/tests/unit/test_field_transforms_unit.py
  • genon/preprocessor/tests/unit/test_filter_transform_derive_unit.py
  • genon/preprocessor/tests/unit/test_heading_only_absorb_lossless.py
  • genon/preprocessor/tests/unit/test_helpers.py
  • genon/preprocessor/tests/unit/test_html_flatten_unit.py
  • genon/preprocessor/tests/unit/test_html_table_caption_unit.py
  • genon/preprocessor/tests/unit/test_intelligent_processor_unit.py
  • genon/preprocessor/tests/unit/test_json_records_unit.py
  • genon/preprocessor/tests/unit/test_json_semantic_unit.py
  • genon/preprocessor/tests/unit/test_markdown_front_matter_unit.py
  • genon/preprocessor/tests/unit/test_md_marker_headings_unit.py
  • genon/preprocessor/tests/unit/test_md_text_fence_unit.py
  • genon/preprocessor/tests/unit/test_multi_mapper_unit.py
  • genon/preprocessor/tests/unit/test_parser_processor_unit.py
  • genon/preprocessor/tests/unit/test_processor_enrichment_unit.py
  • genon/preprocessor/tests/unit/test_product_hpp_fee_table_caption.py
  • genon/preprocessor/tests/unit/test_product_hpp_json_config.py
  • genon/preprocessor/tests/unit/test_refresh_stats_unit.py
  • genon/preprocessor/tests/unit/test_section_header_order.py
  • genon/preprocessor/tests/unit/test_table_as_chunk.py
  • genon/preprocessor/tests/unit/test_table_blocks_unit.py
  • genon/preprocessor/tests/unit/test_table_html_cleanup_chunks.py
  • genon/preprocessor/tests/unit/test_table_shape_unit.py
  • genon/preprocessor/tests/unit/test_table_shapes_fixture.py
  • genon/preprocessor/tests/unit/test_table_splitter_processor_integration.py
  • genon/preprocessor/tests/unit/test_table_splitter_unit.py
  • genon/preprocessor/tests/unit/test_table_text_description_standalone.py
  • genon/preprocessor/tests/unit/test_table_text_variant_chunks.py
  • genon/preprocessor/tests/unit/test_table_text_variants_unit.py
  • genon/preprocessor/tests/unit/test_table_vector_meta_unit.py
  • genon/preprocessor/tests/unit/test_text_cleanup_chunk_paths.py
  • genon/preprocessor/tests/unit/test_text_cleanup_config.py
  • genon/preprocessor/tests/unit/test_text_cleanup_rules_unit.py
  • genon/preprocessor/tests/unit/test_text_norm_unit.py
  • genon/preprocessor/tests/unit/test_xlsx_processor.py
  • genon/serving/paddle/docker/Dockerfile
  • genon/serving/paddle/pyproject.toml
  • genon/tools/genos_tools/viewer/docling_viewer.py
  • tests/multi_format.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands.

CI 가 이 브랜치에서만 실패하던 것 중 두 갈래를 없앤다.

- 청크 헤더 접두는 개발 설정(resource_dev)에서 꺼져 있다(e332b1e, 의도된 값).
  헤더 동작을 검사하는 테스트가 그 값을 물려받아 8건이 깨졌다. 테스트 헬퍼가
  include_chunk_header 를 명시하게 했다. 끄는 쪽을 보는 테스트는 호출부가 0 으로
  덮어쓰므로 그대로 동작한다. 제품 코드와 설정은 바꾸지 않았다.
- text_cleanup_noise_sample.md 는 샘플만 추가되고(3f6aa05) 회귀 베이스라인이 기록되지
  않아 "Baseline not found" 로 계속 실패했다. 현재 산출로 기록했다.

남은 실패는 의도된 동작을 단정이 따라가지 못하는 것들(출고 설정 계약, usage.pages 1
하한)과 실행 환경 의존(libreoffice·hwp sdk)이라 이번 변경에서 손대지 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@inoray

inoray commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

아직 올릴 단계가 아니라 닫습니다. 브랜치와 커밋은 그대로 두고, 준비되면 다시 엽니다.

@inoray inoray closed this Sep 6, 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.

facade 프로세서 코드 정리와 공용 런타임 라이브러리화

1 participant