diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 6c6f9ae..f7dc9f5 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -26,4 +26,5 @@ | 2026-08-08 | Agent 实现 | 里程碑2 Task2 实现:SheetDetector 类型识别。新建 src/genesis/parsers/sheet_detector.py(名称关键词→表头关键词→GENERIC 三级判定,detect_sheet_type(sheet_name, matrix)->SheetType)与 tests/test_sheet_detector.py(3 用例:名称/表头/未知);TDD 验证 RED(ModuleNotFoundError)→ GREEN(3 passed);修正 brief 缺陷:brief 原始 NAME_KEYWORDS 无法匹配「コード管理」判为 MASTER(不含"マスタ"),最小补入 ("コード", MASTER);pytest 全量 20 passed;提交 66753e4 | src/genesis/parsers/sheet_detector.py, tests/test_sheet_detector.py | deepseek-v4-flash-free | | 2026-08-08 | Agent 实现 | 里程碑2 Task3 实现:Sheet 性质判定。新建 src/genesis/parsers/sheet_nature.py(纯函数:SheetNature 枚举 TABLE/FREE_TEXT/MIXED、find_header_row 表头行定位、classify_sheet 按 design §3.5.2 启发式判定)与 tests/test_sheet_nature.py(4 用例);TDD RED→GREEN;pytest 全量 24 passed | src/genesis/parsers/sheet_nature.py, tests/test_sheet_nature.py | deepseek-v4-flash-free | | 2026-08-08 23:04 | Agent 实现 | 里程碑2 Task4 实现:MergeHandler 与 TableExtractor。新建 src/genesis/parsers/merge_fill.py(forward_fill 合并单元格展开,(min_row,min_col,max_row,max_col) 1-based 范围用左上主格值填充,边界保护)与 src/genesis/parsers/table_extractor.py(column_letter 1→A/27→AA;extract_table 首行表头→其后为数据行,构建 CellValue+Provenance(row=r-header_row 从 1 起、column 字母、column_header),无矩阵时返回空表)及 tests/test_table_extractor.py(4 用例:纵向/横向合并、列字母、基本提取);TDD 验证 RED(ImportError: No module named 'genesis.parsers.merge_fill')→ GREEN(4 passed);pytest 全量 28 passed;data_models.py 未改动;提交 63d0c90 | src/genesis/parsers/merge_fill.py, src/genesis/parsers/table_extractor.py, tests/test_table_extractor.py | deepseek-v4-flash-free | +| 2026-08-09 02:59 | Agent 实现 | 里程碑2 Task5 实现:FormattingDetector(取消线/背景色/批注)。新建 src/genesis/parsers/formatting_detector.py(cell_formatting 检测 strike/字体色/背景色,纯默认单元格返回 None;cell_comment 返回 CellComment;collect_comments 遍历 ws.iter_rows 收集批注)与 tests/test_formatting_detector.py(4 用例);TDD 验证 RED(ImportError)→ GREEN(4 passed);修正 brief 两处 openpyxl 3.1.5 兼容问题(① 字体 Style 对象不可变,测试用 copy 副本设 strike;② 默认 theme 色 .rgb 返回错误文本而非 None,新增 _to_rgb_hex 严格校验仅接受 6/8 位十六进制并排除纯黑);pytest 全量 32 passed;data_models.py 未改动 | src/genesis/parsers/formatting_detector.py, tests/test_formatting_detector.py | deepseek-v4-flash-free | diff --git a/src/genesis/parsers/formatting_detector.py b/src/genesis/parsers/formatting_detector.py new file mode 100644 index 0000000..1fdb76e --- /dev/null +++ b/src/genesis/parsers/formatting_detector.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from genesis.data_models import CellComment, CellFormatting +from genesis.parsers.provenance import build_source_uri + +_BLACK_RGB = ("00000000", "FF000000") + + +def _to_rgb_hex(color) -> str | None: + """将 openpyxl Color 转为 RGB 十六进制;非 RGB 主题色/默认色返回 None""" + if color is None: + return None + try: + value = str(color.rgb) + except Exception: + return None + if not value or value in _BLACK_RGB: + return None + if len(value) not in (6, 8): + return None + if not all(ch in "0123456789ABCDEFabcdef" for ch in value): + return None + return value + + +def cell_formatting(cell) -> CellFormatting | None: + strike = bool(cell.font.strike) + font_color = _to_rgb_hex(getattr(cell.font, "color", None)) + bg_color = None + fill = cell.fill + if fill is not None: + bg_color = _to_rgb_hex(getattr(fill, "fgColor", None)) + if strike or font_color or bg_color: + return CellFormatting( + strikethrough=strike, font_color=font_color, bg_color=bg_color, + ) + return None + + +def cell_comment(cell, file_name: str) -> CellComment | None: + if cell.comment is None: + return None + return CellComment( + author=cell.comment.author or "", + text=cell.comment.text or "", + source_uri=build_source_uri(file_name, cell.parent.title, cell.coordinate), + ) + + +def collect_comments(ws, file_name: str) -> list[CellComment]: + result = [] + for row in ws.iter_rows(): + for cell in row: + cm = cell_comment(cell, file_name) + if cm is not None: + result.append(cm) + return result \ No newline at end of file diff --git a/tests/test_formatting_detector.py b/tests/test_formatting_detector.py new file mode 100644 index 0000000..402d83d --- /dev/null +++ b/tests/test_formatting_detector.py @@ -0,0 +1,49 @@ +from copy import copy + +from openpyxl import Workbook +from openpyxl.comments import Comment + +from genesis.data_models import CellComment, CellFormatting +from genesis.parsers.formatting_detector import ( + cell_comment, cell_formatting, collect_comments, +) + + +def make_wb(): + wb = Workbook() + ws = wb.active + ws.title = "機能一覧" + ws["A1"] = "F001" + font = copy(ws["A1"].font) + font.strike = True + ws["A1"].font = font + ws["A2"] = "F002" + ws["A2"].comment = Comment("要確認", "reviewer") + return wb + + +def test_cell_formatting_detects_strike(): + fmt = cell_formatting(make_wb().active["A1"]) + assert fmt is not None + assert fmt.strikethrough is True + + +def test_cell_formatting_none_when_plain(): + wb = Workbook() + ws = wb.active + ws["A1"] = "x" + assert cell_formatting(ws["A1"]) is None + + +def test_cell_comment_returns_obj(): + wb = make_wb() + cm = cell_comment(wb.active["A2"], "f.xlsx") + assert isinstance(cm, CellComment) + assert cm.author == "reviewer" + assert cm.text == "要確認" + assert cm.source_uri == "f.xlsx#機能一覧!A2" + + +def test_collect_comments(): + comments = collect_comments(make_wb().active, "f.xlsx") + assert len(comments) == 1 \ No newline at end of file