Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ jobs:
python-version: "3.12"

- name: Install package
run: pip install -e ".[dev]"
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: 테스트 실행
run: pytest -q
Expand All @@ -31,7 +33,10 @@ jobs:
python-version: "3.12"

- name: Build wheel
run: pip install build && python -m build
run: |
python -m pip install --upgrade pip
pip install build
python -m build

- name: wheel 내용물 확인
run: python -m zipfile -l dist/*.whl
Expand All @@ -52,7 +57,11 @@ jobs:
python-version: "3.12"

- name: Install package + pip-audit
run: pip install -e . pip-audit
run: |
python -m pip install --upgrade pip
pip install -e . pip-audit

- name: pip-audit로 의존성 취약점 스캔
run: pip-audit
# 제외 사유와 해제 조건은 .pip-audit-ignore 주석 참고.
run: |
pip-audit $(grep -v '^#' .pip-audit-ignore | grep -v '^$' | sed 's/^/--ignore-vuln /')
37 changes: 37 additions & 0 deletions .pip-audit-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# pip-audit 제외 목록 — CI의 pip-audit 잡이 이 파일을 읽어 --ignore-vuln 인자로 넘긴다.
#
# pillow 12.3.x 취약점 24건.
# 수정판 13.0.0이 나와 있으나 docling-core(2.103.1)와 docling-slim(2.141.0)이
# pillow<13.0.0으로 상한을 걸고 있고, 둘 다 현재 최신 릴리스다. 따라서 의존성
# 트리 안에서 pillow를 올릴 방법이 없다.
# evidence-chunker는 pillow를 직접 호출하지 않는다 — 이미지 처리는 전적으로
# docling 내부(레이아웃 분석/표 구조 인식) 경로에서만 일어난다.
# 업스트림 상한이 풀리면 이 목록을 지우고 pyproject.toml에 pillow>=13 하한을
# 추가할 것. 추적 이슈: #N
#
# 개별 ID를 나열하는 이유: 패키지 단위로 묶어 제외하면 앞으로 보고되는 새 취약점까지
# 조용히 덮인다. 새 CVE가 나오면 CI가 실패하도록 두는 게 맞다.
CVE-2026-112390
CVE-2026-112391
CVE-2026-112392
CVE-2026-112393
CVE-2026-112395
CVE-2026-112396
CVE-2026-112397
CVE-2026-112399
CVE-2026-112400
CVE-2026-112401
CVE-2026-112402
CVE-2026-112404
CVE-2026-112405
CVE-2026-112406
CVE-2026-112891
CVE-2026-112892
CVE-2026-112893
CVE-2026-112894
CVE-2026-112895
CVE-2026-112896
CVE-2026-112897
CVE-2026-112899
CVE-2026-112900
CVE-2026-112901
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dependencies = [
similarity = ["sentence-transformers>=2.2"]
langchain = ["langchain-core"]
llamaindex = ["llama-index-core"]
dev = ["pytest>=8", "sentence-transformers>=2.2", "llama-index-core"]
dev = ["pytest>=8", "sentence-transformers>=2.2", "llama-index-core", "langchain-core"]
tracking = ["mlflow>=2.14"]

[project.urls]
Expand Down
20 changes: 17 additions & 3 deletions src/evidence_chunker/caption.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,31 @@ def _find_caption_adjacent_page(
)


# 병합 셀 캡션 탐색은 표 "첫 행"만 본다. 이 규칙은 캡션이 표 안으로 빨려
# 들어간 경우를 복구하려는 것이고, 캡션은 표 위에 붙으므로 표 HTML 안에서는
# 첫 행으로만 나타난다. 범위를 풀면 표 중간의 그룹 구분 행, "Table 9:
# (continued)" 같은 반복 머리글, 다른 표를 가리키는 셀 값까지 캡션으로
# 채택돼 EU 전체(text/retrieval_text/retrieval_units/table_abstract)에
# 잘못된 정체성이 복제된다.
_FIRST_ROW_PATTERN = re.compile(r"<tr[^>]*>(.*?)</tr>", re.IGNORECASE | re.DOTALL)


def _find_caption_in_merged_header(table_html: Optional[str]) -> Optional[str]:
"""
표 첫 행이 다수 컬럼을 병합한 th/td 하나로만 이루어져 있고, 그 안 텍스트가
캡션 패턴(_looks_like_table_caption)에 맞으면 채택.
표 첫 행에 여러 컬럼을 병합한 th/td가 있고, 그 안 텍스트가 캡션
패턴(_looks_like_table_caption)에 맞으면 채택.

첫 행에서 못 찾으면 더 아래 행은 보지 않는다 — 윗주석 참고.

Returns:
caption_text — 못 찾으면 None
"""
if not table_html:
return None
m = _MERGED_HEADER_PATTERN.search(table_html)
first_row = _FIRST_ROW_PATTERN.search(table_html)
if not first_row:
return None
m = _MERGED_HEADER_PATTERN.search(first_row.group(1))
if not m:
return None
cell_text = re.sub(r"<[^>]+>", " ", m.group(2)).strip()
Expand Down
2 changes: 0 additions & 2 deletions tests/specification/test_spec_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ def test_qa15_merged_first_row_caption(label,expected):
assert map_table_caption(parsed([t]),t,0).confidence==expected

@pytest.mark.spec("QA-15")
@pytest.mark.spec_gap
def test_qa15_caption_must_be_in_first_row():
t=table()
t.html='<table><tr><th>Name</th><th>Value</th></tr><tr><td colspan="2">Table 9: Quoted data</td></tr></table>'
Expand Down Expand Up @@ -124,7 +123,6 @@ def test_qa23_same_page_section_and_no_header():
assert find_section_header(parsed(),1,400) is None

@pytest.mark.spec("QA-23")
@pytest.mark.spec_gap
def test_qa23_previous_page_last_header_is_selected():
ts=[text(0,"Earlier section",label="section_header",page=1,center=100),
text(1,"Continuing section",label="section_header",page=1,center=800)]
Expand Down
1 change: 0 additions & 1 deletion tests/specification/test_spec_split.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ def test_qa34_data_rows_and_sentences_preserved_once_in_order():
assert [s for e in result.chunks for s in e.flattened_rows]==expected_sentences(24)

@pytest.mark.spec("QA-34")
@pytest.mark.spec_gap
def test_qa34_final_text_respects_512_token_limit():
e=raw_unit()
result=split_eu(e)
Expand Down
60 changes: 60 additions & 0 deletions tests/test_caption_merged_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2026 docling-evidence-chunker contributors
# SPDX-License-Identifier: Apache-2.0
"""표 첫 행 병합 셀 캡션 탐색 회귀 테스트 (QA-15 / GAP-02).

이 fallback은 캡션이 표 안으로 빨려 들어간 경우를 복구하는 규칙이고,
캡션은 표 위에 붙으므로 표 HTML 안에서는 첫 행으로만 나타난다. 따라서
"첫 행"이라는 위치가 이 추론의 유일한 근거다. 이전 구현은 HTML 전체를
훑어서 표 중간의 병합 셀도 캡션으로 채택했다.
"""
import pytest

from evidence_chunker.caption import _find_caption_in_merged_header

CAPTION_ROW = '<tr><td colspan="2">Table 1: Embedded caption</td></tr>'
HEADER_ROW = "<tr><th>Name</th><th>Value</th></tr>"
MID_CAPTION_ROW = '<tr><td colspan="2">Table 9: Quoted data</td></tr>'
DATA_ROW = "<tr><td>A</td><td>10</td></tr>"


def _table(*rows):
return "<table>" + "".join(rows) + "</table>"


def test_first_row_merged_caption_is_adopted():
html = _table(CAPTION_ROW, DATA_ROW)
assert _find_caption_in_merged_header(html) == "Table 1: Embedded caption"


def test_merged_cell_below_first_row_is_ignored():
"""표 중간의 그룹 구분 행이나 다른 표를 인용한 셀을 캡션으로 쓰면 안 된다."""
html = _table(HEADER_ROW, MID_CAPTION_ROW, DATA_ROW)
assert _find_caption_in_merged_header(html) is None


def test_caption_row_inside_thead_is_examined():
html = "<table><thead>" + CAPTION_ROW + "</thead><tbody>" + DATA_ROW + "</tbody></table>"
assert _find_caption_in_merged_header(html) == "Table 1: Embedded caption"


def test_non_caption_text_in_first_row_is_ignored():
html = _table('<tr><td colspan="2">Ordinary group heading</td></tr>', DATA_ROW)
assert _find_caption_in_merged_header(html) is None


def test_first_row_without_colspan_is_ignored():
html = _table(HEADER_ROW, DATA_ROW)
assert _find_caption_in_merged_header(html) is None


def test_markup_inside_merged_cell_is_stripped():
html = _table('<tr><td colspan="3"><b>Table 2:</b> Nested markup</td></tr>', DATA_ROW)
result = _find_caption_in_merged_header(html)
assert result is not None
assert result.startswith("Table 2:") and "Nested markup" in result
assert "<b>" not in result


@pytest.mark.parametrize("html", [None, "", "<p>표가 아닌 마크업</p>"])
def test_missing_or_rowless_html_returns_none(html):
assert _find_caption_in_merged_header(html) is None
Loading