docs: 计划查漏补缺修正(formatting_map 段内坐标/样本合并/枚举测试/防御实现)

This commit is contained in:
lhl
2026-08-09 03:57:02 +08:00
parent 035be2acea
commit 81c6b6c522
@@ -250,6 +250,34 @@ def test_parse_mixed_sheet_segmented(tmp_path):
# 自由文本段捕获碎片
ft = [p for p in ms.paragraphs if p.kind == "free_text"][0]
assert "改修ポイント" in (ft.text or "")
def test_parse_mixed_sheet_formatting_in_mid_segment(tmp_path):
# 表格段不在物理行 0(自由文本段在前),数据行 F101 设取消线 —— 锁住 formatting_map 坐标错位
from copy import copy
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "混合"
data = [
["■はじめに"], ["前提説明"], [],
["機能ID", "機能名"], ["F101", "社員登録"], ["F102", "退職処理"],
[], ["・改修ポイント"],
]
for r, row in enumerate(data, start=1):
for c, v in enumerate(row, start=1):
if v:
ws.cell(row=r, column=c, value=v)
font = copy(ws.cell(row=5, column=1).font) # F101 所在物理行(第 5 行)
font.strike = True
ws.cell(row=5, column=1).font = font
path = save_workbook(tmp_path, wb)
result = ExcelParser().parse(path)
ms = result.mixed[0]
tbl = [p.table for p in ms.paragraphs if p.kind == "table"][0]
cv = tbl.rows[0]["機能ID"] # F101
assert cv.formatting is not None
assert cv.formatting.strikethrough is True
```
- [ ] **Step 2: 运行确认失败**
@@ -283,9 +311,15 @@ Expected: FAIL(当前 MIXED 折叠进表格路径,`result.mixed` 为空)
header_row = find_header_row(seg)
if header_row < 0:
header_row = 0
# extract_table 的 formatting_map 用段内矩阵坐标 (r,c)
# 从整 sheet 物理 map 抽出段内子 map(物理行 pr → 段内 pr-s
seg_fmt_map = {}
for (pr, pc), fmt in fmt_map.items():
if s <= pr <= e:
seg_fmt_map[(pr - s, pc)] = fmt
table = extract_table(
ws.title, seg, file_name, detected_type,
header_row=header_row, formatting_map=fmt_map,
header_row=header_row, formatting_map=seg_fmt_map,
)
result.tables.append(table)
mixed_sheet.paragraphs.append(MixedParagraph(
@@ -309,7 +343,7 @@ Expected: FAIL(当前 MIXED 折叠进表格路径,`result.mixed` 为空)
else:
# 现有 TABLE 路径(含 MIXED 旧折叠)
```
> 注:原 `else` 分支现在是 TABLE 专用;MIXED 已独立。`fmt_map` 对整 sheet 构建后再段内使用(对表格段坐标有效,自由文本段无表格 CellValue 使用)
> 注:原 `else` 分支现在是 TABLE 专用;MIXED 已独立。`fmt_map` 对整 sheet 按物理坐标构建一次;表格段经 `seg_fmt_map` 抽为段内坐标后传给 `extract_table`(避免段内相对索引与物理坐标错位)。自由文本段无表格 CellValue,不使用格式化
- [ ] **Step 4: 运行确认通过**
@@ -319,7 +353,7 @@ Expected: PASS(含新 MIXED 用例)
- [ ] **Step 5: 全量回归**
Run: `python -m pytest -v`
Expected: PASS44 + 1 = 45 passed
Expected: PASS44 + 2 = 46 passed
- [ ] **Step 6: 提交**
@@ -355,7 +389,6 @@ rows = [
["F103", "給与計算", "SC003"],
[],
["・改修ポイント:F103 に年末調整バッチ連携を追加する。"],
[],
["■対象期間:2026年度下半期"],
]
for r, row in enumerate(rows, start=1):
@@ -365,7 +398,7 @@ for r, row in enumerate(rows, start=1):
wb.save(r"samples\要件定義_混合型.xlsx")
print("saved")
```
(实际执行时用 PowerShell 运行;注意行尾没有多余空行——`[]` 行是显式空行分隔,最后一个非空行为「■」行后无额外空行,保证 `split_paragraphs` 尾部截断不产生空段。)
(实际执行时用 PowerShell 运行;「・」与「■」行**连续无空行**`split_paragraphs` 将其合并为**一段**碎片段,返回 `[(0,3),(5,6)]` 两段:表格段 + 碎片段。)
- [ ] **Step 2: 写端到端测试**
@@ -380,11 +413,16 @@ def test_mixed_sample_segments_detected():
assert "機能一覧" in by_name
assert result.mixed, "混合样本应产产出段落"
mixed = result.mixed[0]
kinds = [p.kind for p in mixed.paragraphs]
assert "table" in kinds and "free_text" in kinds
assert len(mixed.paragraphs) == 2 # 表格段 + 碎片段(・/■ 连续)
assert [p.kind for p in mixed.paragraphs] == ["table", "free_text"]
# 表格段无碎片污染
table = [p.table for p in mixed.paragraphs if p.kind == "table"][0]
assert table.rows[0]["機能ID"].value == "F101"
assert len(table.rows) == 3
# 碎片段含 ・ 与 ■ 两行文本
ft = [p for p in mixed.paragraphs if p.kind == "free_text"][0]
assert "改修ポイント" in (ft.text or "")
assert "対象期間" in (ft.text or "")
```
- [ ] **Step 3: 运行端到端**
@@ -395,7 +433,7 @@ Expected: PASS4 passed,无 skip
- [ ] **Step 4: 全量回归**
Run: `python -m pytest -v`
Expected: PASS45 + 1 = 46 passed
Expected: PASS46 + 1 = 47 passed
- [ ] **Step 5: 提交**
@@ -423,7 +461,7 @@ git commit -m "test: MIXED 混合样本 + 端到端段落验证"
- [ ] **Step 1: 写失败测试(MIXED 断言 + formatting 正向)**
`tests/test_sheet_nature.py` 追加:
`tests/test_sheet_nature.py` 追加(补 MIXED 正向用例,`test_table_detection` 已覆盖 TABLE 无需重复)
```python
def test_classify_sheet_mixed_with_bullet_line():
m = [["ID", "名前"], ["1", "田中"], ["・備考行"]]
@@ -433,11 +471,27 @@ def test_classify_sheet_mixed_with_bullet_line():
def test_classify_sheet_mixed_with_square_line():
m = [["ID", "名前"], ["1", "田中"], ["■備考行"]]
assert classify_sheet(m) == SheetNature.MIXED
```
`tests/test_table_extractor.py` 追加(锁枚举同源):
```python
from genesis.data_models import ExtractionMethod
def test_classify_table_no_semicolon():
m = [["ID", "名前"], ["1", "田中"]]
assert classify_sheet(m) == SheetNature.TABLE
def test_extract_table_uses_enum_value():
matrix = [["ID"], ["1"]]
table = extract_table("社員", matrix, "f.xlsx", SheetType.FUNCTION)
assert table.extraction_method == ExtractionMethod.OPENPYXL.value
```
`tests/test_free_text_extractor.py` 追加:
```python
from genesis.data_models import ExtractionMethod
def test_free_text_uses_enum_value():
table = build_free_text_table("メモ", ["A"], "f.xlsx")
assert table.extraction_method == ExtractionMethod.LLM_FROM_FREE_TEXT.value
```
`tests/test_formatting_detector.py` 追加(复用现有 make_wb 复制字体技巧):
@@ -478,10 +532,33 @@ extraction_method=ExtractionMethod.OPENPYXL.value,
if header_row < 0 or header_row >= len(matrix):
header_row = 0
```
`merge_fill.py` 防御非法入参:
`merge_fill.py` 防御非法入参(完整替换 `forward_fill`
```python
def forward_fill(matrix, merged_ranges):
# 前置校验:range 值不合法(<1 或超出矩阵)时直接返回深拷贝
def forward_fill(
matrix: list[list[Any]],
merged_ranges: list[tuple[int, int, int, int]],
) -> list[list[Any]]:
"""合并单元格:用左上角主格值填充范围内全部单元格。"""
out = [list(row) for row in matrix]
for (min_row, min_col, max_row, max_col) in merged_ranges:
# 防御:合并范围 1-based,非法(<1)或越界时跳过该范围
if not out or min_row < 1 or min_col < 1:
continue
if min_row > len(out) or min_col > len(out[min_row - 1]):
continue
main_value = out[min_row - 1][min_col - 1]
for r in range(min_row, min(max_row, len(out)) + 1):
row = out[r - 1]
for c in range(min_col, min(max_col, len(row)) + 1):
row[c - 1] = main_value
return out
```
`tests/test_table_extractor.py` 追加(验证防御):
```python
def test_forward_fill_ignores_invalid_range():
matrix = [["A"], ["B"]]
assert forward_fill(matrix, [(0, 1, 2, 1)]) == matrix # min_row=0 非法 → 不崩溃不改数据
assert forward_fill(matrix, [(9, 9, 9, 9)]) == matrix # 越界 → 不崩溃
```
`data_models.py` 注释(`Provenance.row`):
@@ -498,17 +575,17 @@ class Provenance:
- [ ] **Step 3: 运行全部新增/修改测试**
Run: `python -m pytest tests/test_sheet_nature.py tests/test_formatting_detector.py tests/test_free_text_extractor.py tests/test_table_extractor.py -v`
Expected: PASS
Expected: PASS(新增 2 MIXED + 1 formatting + 1 自由文本枚举 + 2 表提取枚举/防御 = 6 个新用例)
- [ ] **Step 4: 全量回归**
Run: `python -m pytest -v`
Expected: PASS46 + 新增断言数
Expected: PASS47 + 6 = 53 passed
- [ ] **Step 5: 提交**
```bash
git add src/genesis/parsers/free_text_extractor.py src/genesis/parsers/table_extractor.py src/genesis/parsers/merge_fill.py src/genesis/data_models.py tests/test_sheet_nature.py tests/test_formatting_detector.py
git add src/genesis/parsers/free_text_extractor.py src/genesis/parsers/table_extractor.py src/genesis/parsers/merge_fill.py src/genesis/data_models.py tests/test_sheet_nature.py tests/test_formatting_detector.py tests/test_free_text_extractor.py tests/test_table_extractor.py
git commit -m "fix: 遗留清理(枚举同源/边界防御/断言补强)"
```