fix: 遗留清理(枚举同源/边界防御/断言补强)

This commit is contained in:
lhl
2026-08-09 04:18:39 +08:00
parent 75925168c7
commit d93cc77bae
9 changed files with 59 additions and 11 deletions
+1
View File
@@ -36,3 +36,4 @@
| 2026-08-09 04:20 | Agent 实现 | 里程碑2.5 Task10 实现:data_models 扩展 MixedParagraph/MixedSheet 段落容器,并新增 ExcelParseResult.mixed 字段(默认空列表)。TDD:先追加 3 个测试(字段存在性与默认行为)确认 REDImportError: cannot import name 'MixedParagraph'),再在 data_models.py 末尾追加两个 dataclass(顶部 from typing 加 Literal、dataclasses 加 field),excel_parser.py 更新导入并为 ExcelParseResult 加 mixed 字段,确认 GREEN3 passed),全量 pytest 49 passed46 baseline + 3)。未修改既有类与字段,仅追加。 | src/genesis/data_models.py, src/genesis/parsers/excel_parser.py, tests/test_excel_parser.py | deepseek-v4-flash-free |
| 2026-08-09 04:06 | Agent 实现 | 里程碑2.5 Task11 实现:MIXED 完整段落装配(分割→逐段最优解析)。excel_parser.py 新增 MIXED 分支:整表先 forward_fill(合并单元格)再按段切片;对每段 classify_sheetTABLE 段用 find_header_row(seg)(段内相对,<0 回落 0)并抽出段内子 map seg_fmt_map(物理行 pr→段内 pr-s,修复 formatting_map 坐标错位)传给 extract_tableheader_row=header_row);非表格段归 FREE_TEXT(防二次 MIXED 递归),走 extract_text_blocks + build_free_text_table 构造 kind="free_text"。MixedParagraph 段落追加到 MixedSheet 并进 result.mixed,表/自由文本表进 result.tables。原 else 分支保留为 TABLE 专用,既有 TABLE/FREE_TEXT 路径不变。TDD:先追加 2 测试(段落分割/碎片捕获、表格段不在物理行 0 时的取消线回填)确认 REDresult.mixed 空 + IndexError),再实现确认 GREEN9 passed),全量 pytest 51 passed49 baseline + 2)。提交见 git log | src/genesis/parsers/excel_parser.py, tests/test_excel_parser.py | deepseek-v4-flash-free |
| 2026-08-09 09:40 | 测试验证 | 里程碑2.5 Task12 实现:混合型样本 + 端到端验证。生成 samples/要件定義_混合型.xlsx(機能一覧 表格:表头+3 数据行,空行后接「・改修ポイント」「■対象期間」两条连续碎片文本,无空行);tests/test_real_samples.py 追加 test_mixed_sample_segments_detected(断言 mixed 段落数==2、kinds==["table","free_text"]、表格段第一批数据行機能ID=="F101"、table.rows==3、碎片段 text 含 改修ポイント/対象期間)。验证:real_samples 全绿 4 passed3 baseline+1 新增,无 skip);pytest 全量 52 passed51 baseline + 1);实际样本 split_paragraphs 产出两段(0,3)+(5,6)(表格段+碎片段)。提交见 git log | samples/要件定義_混合型.xlsx, tests/test_real_samples.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-09 | 整体迭代 | 里程碑2.5 Task13 遗留清理:① extraction_method 枚举同源(free_text_extractor→ExtractionMethod.LLM_FROM_FREE_TEXT.value、table_extractor 空/非空两分支→ExtractionMethod.OPENPYXL.value,值不变既有用例兼容);② table_extractor header_row 越界/负值防御(<0 或 >=len(matrix) 回落 0keep if not matrix 早返回);③ merge_fill.forward_fill 完整替换(min_row<1/min_col<1 非法值跳过 + 越界防御);④ data_models.Provenance.row 语义注释;⑤ 补测试 6 个(sheet_nature MIXED ・/■ 正向 x2、formatting_detector 字体色正向、table_extractor 枚举锁 + forward_fill 防御、free_text_extractor 枚举锁)。TDD:先加测试确认 RED(仅 forward_fill 防御用例失败,其余为已通过回归锁)→ 实现 → GREENpytest 全量 58 passed52 baseline + 6 | 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, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
+1 -1
View File
@@ -54,7 +54,7 @@ class ExtractionMethod(Enum):
class Provenance:
file_name: str
sheet_name: str
row: int
row: int # 数据行号(从 1 起:表格为物理行-表头行;自由文本为块序)
column: str
column_header: str
+2 -2
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any
from genesis.data_models import CellValue, ExcelTable, Provenance, SheetType
from genesis.data_models import CellValue, ExcelTable, ExtractionMethod, Provenance, SheetType
def extract_text_blocks(matrix: list[list[Any]]) -> list[str]:
@@ -45,7 +45,7 @@ def build_free_text_table(
return ExcelTable(
name=sheet_name,
detected_type=detected_type,
extraction_method="llm_from_free_text",
extraction_method=ExtractionMethod.LLM_FROM_FREE_TEXT.value,
headers=["text"],
rows=rows,
)
+4 -1
View File
@@ -10,7 +10,10 @@ def forward_fill(
"""合并单元格:用左上角主格值填充范围内全部单元格。"""
out = [list(row) for row in matrix]
for (min_row, min_col, max_row, max_col) in merged_ranges:
if not out or min_row > len(out) or min_col > len(out[min_row - 1]):
# 防御:合并范围 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):
+7 -3
View File
@@ -2,7 +2,9 @@ from __future__ import annotations
from typing import Any
from genesis.data_models import CellFormatting, CellValue, ExcelTable, Provenance, SheetType
from genesis.data_models import (
CellFormatting, CellValue, ExcelTable, ExtractionMethod, Provenance, SheetType,
)
def column_letter(index: int) -> str:
@@ -29,8 +31,10 @@ def extract_table(
if not matrix:
return ExcelTable(
name=sheet_name, detected_type=detected_type,
extraction_method="openpyxl", headers=[], rows=[],
extraction_method=ExtractionMethod.OPENPYXL.value, headers=[], rows=[],
)
if header_row < 0 or header_row >= len(matrix):
header_row = 0
fmt_map = formatting_map or {}
headers = [str(c) if c is not None else "" for c in matrix[header_row]]
rows = []
@@ -52,5 +56,5 @@ def extract_table(
rows.append(row_dict)
return ExcelTable(
name=sheet_name, detected_type=detected_type,
extraction_method="openpyxl", headers=headers, rows=rows,
extraction_method=ExtractionMethod.OPENPYXL.value, headers=headers, rows=rows,
)
+13
View File
@@ -35,6 +35,19 @@ def test_cell_formatting_none_when_plain():
assert cell_formatting(ws["A1"]) is None
def test_cell_formatting_detects_font_color():
from openpyxl.styles import Font
wb = Workbook()
ws = wb.active
ws["A1"] = "x"
f = copy(ws["A1"].font)
f.color = Font(color="FF0000FF").color
ws["A1"].font = f
fmt = cell_formatting(ws["A1"])
assert fmt is not None
assert fmt.font_color is not None
def test_cell_comment_returns_obj():
wb = make_wb()
cm = cell_comment(wb.active["A2"], "f.xlsx")
+6 -1
View File
@@ -1,4 +1,4 @@
from genesis.data_models import CellValue, ExcelTable, SheetType
from genesis.data_models import CellValue, ExcelTable, ExtractionMethod, SheetType
from genesis.parsers.free_text_extractor import build_free_text_table, extract_text_blocks
@@ -15,3 +15,8 @@ def test_build_free_text_table():
assert len(table.rows) == 2
assert isinstance(table.rows[0]["text"], CellValue)
assert table.rows[0]["text"].value == "A"
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
+11 -1
View File
@@ -19,4 +19,14 @@ def test_free_text_many_empty_rows():
def test_header_row_index():
m = [["機能ID", "名前"], ["1", "田中"]]
assert find_header_row(m) == 0
assert find_header_row([["自由テキスト"]]) == -1
assert find_header_row([["自由テキスト"]]) == -1
def test_classify_sheet_mixed_with_bullet_line():
m = [["ID", "名前"], ["1", "田中"], ["・備考行"]]
assert classify_sheet(m) == SheetNature.MIXED
def test_classify_sheet_mixed_with_square_line():
m = [["ID", "名前"], ["1", "田中"], ["■備考行"]]
assert classify_sheet(m) == SheetNature.MIXED
+14 -2
View File
@@ -1,4 +1,4 @@
from genesis.data_models import CellValue, ExcelTable, SheetType
from genesis.data_models import CellValue, ExcelTable, ExtractionMethod, SheetType
from genesis.parsers.merge_fill import forward_fill
from genesis.parsers.table_extractor import column_letter, extract_table
@@ -37,4 +37,16 @@ def test_extract_table_basic():
assert isinstance(first["ID"], CellValue)
assert first["ID"].value == "1"
assert first["ID"].provenance.sheet_name == "社員一覧"
assert first["ID"].provenance.column == "A"
assert first["ID"].provenance.column == "A"
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
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 # 越界 → 不崩溃