fix: 遗留清理(枚举同源/边界防御/断言补强)
This commit is contained in:
@@ -36,3 +36,4 @@
|
||||
| 2026-08-09 04:20 | Agent 实现 | 里程碑2.5 Task10 实现:data_models 扩展 MixedParagraph/MixedSheet 段落容器,并新增 ExcelParseResult.mixed 字段(默认空列表)。TDD:先追加 3 个测试(字段存在性与默认行为)确认 RED(ImportError: cannot import name 'MixedParagraph'),再在 data_models.py 末尾追加两个 dataclass(顶部 from typing 加 Literal、dataclasses 加 field),excel_parser.py 更新导入并为 ExcelParseResult 加 mixed 字段,确认 GREEN(3 passed),全量 pytest 49 passed(46 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_sheet,TABLE 段用 find_header_row(seg)(段内相对,<0 回落 0)并抽出段内子 map seg_fmt_map(物理行 pr→段内 pr-s,修复 formatting_map 坐标错位)传给 extract_table(header_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 时的取消线回填)确认 RED(result.mixed 空 + IndexError),再实现确认 GREEN(9 passed),全量 pytest 51 passed(49 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 passed(3 baseline+1 新增,无 skip);pytest 全量 52 passed(51 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) 回落 0,keep 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 防御用例失败,其余为已通过回归锁)→ 实现 → GREEN;pytest 全量 58 passed(52 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 |
|
||||
|
||||
@@ -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,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,
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 # 越界 → 不崩溃
|
||||
Reference in New Issue
Block a user