Files
2026Technology-Competition/docs/superpowers/specs/2026-08-09-mixed-paragraph-parsing-design.md
T

7.6 KiB
Raw Blame History

里程碑 2.5:MIXED 完整段落解析 设计

  • 日期:2026-08-09
  • 状态:已批准(用户 2026-08-09 确认)
  • 目的:闭环遗留项 #1「MIXED 分段解析」,落地 design §3.5.2「段落分割 → 各段落最优解析」

1. 背景与触发条件

当前 classify_sheet 判定 MIXED 的条件:存在表头行,且表头行之后有以「・」「■」开头的碎片行sheet_nature.py:36-43)。此前实现把 MIXED 折叠进纯表格路径(extract_table(header_row=0)),会把片段行误当数据行。

现有真实样本(新規開発 / 追加改修 / 自由記述)中无任何 sheet 触发 MIXED(改修ポイント为独立 sheet)。因此新增混合型样本 samples/要件定義_混合型.xlsx 用于端到端验证。

2. 目标

  • 落地 design §3.5.2MIXED sheet → 段落分割 → 每段独立判定并选择最优解析
  • 显式表达段落边界与段落类型(data_models 扩展,用户选定)
  • 不改变既有 TABLE / FREE_TEXT 路径行为(回归保持)

3. 设计

3.1 段落分割(新模块 src/genesis/parsers/paragraph_splitter.py

def split_paragraphs(matrix: list[list[Any]]) -> list[tuple[int, int]]:
    """以空行为界做通用段落分割;返回 (start_row, end_row) 列表(含端)。"""

规则:

  • 全空行(空矩阵行)为段落分界
  • 连续非空段落保持为一个段落
  • 尾部空行截断,不产生空段
  • 空矩阵 → []

3.2 每段最优解析(复用现有能力,不改 sheet_nature / table_extractor

对每个段落矩阵 seg注意:合并单元格在装配时对整 sheet 先 forward_fill 再按段切片,见 3.4):

  • classify_sheet(seg) 判定段落性质
  • TABLEforward_fill(seg, merged) + extract_table(...)ExcelTable
  • FREE_TEXTextract_text_blocks(seg) + build_free_text_table(...)ExcelTable
  • (段内二次出现的 MIXED 视为 FREE_TEXT 处理,防无限递归——段落粒度的 MIXED 罕见)

3.3 data_models 扩展(用户选定「扩展 data_models」)

@dataclass
class MixedParagraph:
    """混合 sheet 的一个段落(表格或自由文本)"""
    kind: Literal["table", "free_text"]
    matrix: list[list[Any]] | None = None   # 该段原始矩阵(便于调试/重现/后续增量)
    table: ExcelTable | None = None          # kind="table" 时填充
    text: str | None = None                  # kind="free_text" 时填充(该段拼接文本)
    source_range: tuple[int, int] | None = None  # (first_row, last_row) 矩阵 0-based

@dataclass
class MixedSheet:
    """混合 sheet 的段落集合"""
    name: str
    paragraphs: list[MixedParagraph]

ExcelParseResult(现有 dataclass,位于 src/genesis/parsers/excel_parser.py)增加字段:

mixed: list[MixedSheet] = field(default_factory=list)
  • tables 仍保留(含 MIXED sheet 内的表格段),下游消费者继续用 tables 取表
  • mixed 显式表达段落边界与类型,不丢信息

说明:MixedParagraph.text 为自由文本段全文(按段拼接),与 build_free_text_table 的逐块行并存,供下游两种消费方式。

3.4 装配流程(excel_parser.parse

for ws in workbook.worksheets:
    matrix = sheet_matrix(ws)
    if empty → skipped
    nature = classify_sheet(matrix)
    if nature == FREE_TEXT:  (现有路径,保持不变)
    elif nature == MIXED:
        segments = split_paragraphs(matrix)
        mixed_sheet = MixedSheet(name=ws.title, paragraphs=[])
        for (s, e) in segments:
            seg = matrix[s:e+1]
            kind = classify_sheet(seg)
            if kind == TABLE:
                merged = to_tuples(ws.merged_cells.ranges)
                filled = forward_fill(seg, merged) if merged else seg
                table = extract_table(ws.title, filled, file_name, detected_type)
                result.tables.append(table)
                mixed_sheet.paragraphs.append(MixedParagraph(kind="table", matrix=seg, table=table, source_range=(s, e)))
            else:
                blocks = extract_text_blocks(seg)
                table = build_free_text_table(ws.title, blocks, file_name, detected_type)
                result.tables.append(table)
                mixed_sheet.paragraphs.append(MixedParagraph(
                    kind="free_text", matrix=seg, text="\n".join(blocks), source_range=(s, e),
                ))
        result.mixed.append(mixed_sheet)
        result.comments.extend(collect_comments(ws, file_name))
    else: 现有 TABLE 路径

合并单元格:段内矩阵切片后 forward_fill 需要该段内的 merged 范围——实现时对整段做前向填充再切片,或对段内范围坐标平移。实现时选择对整 sheet 先 forward_fill 再按段切片(避免坐标换算错误;混合 sheet 中合并集中于表格段)。

  • 段内 extract_table 使用 header_row=0(段的表头在段首行)

3.5 样本与验证

  • 新增 samples/要件定義_混合型.xlsx,Sheet 构成(目标是触发 MIXED 判定):
    • 機能一覧:表格段(表头 + 真实数据,前无标题行,表头后含一条 ・/■ 碎片行即 MIXED 触发)
    • 内容规格:表头 機能ID / 機能名 / 画面ID,数据 3 行(F101-F103),表头之下全部跟随「・」碎片行若干(碎片文本描述)
  • tests/test_real_samples.py 新增用例:混合样本 → result.mixed 非空、段落 kind 集合={table, free_text}、表格段无碎片污染(首数据行機能ID == 期望)

3.6 单元测试

  • tests/test_paragraph_splitter.py
    • 空矩阵 → []
    • 无空行 → 单段(整矩阵)
    • 中间空行 → 分段;连续非空段
    • 前端空行 → 首个非空段从第一个非空行开始;尾端空行 → 无空段
    • 段落区间整数正确
  • tests/test_sheet_nature.py 补 MIXED 正向用例(当前无):
    • 表头行后存在「・」开头行 → MIXED
    • 表头行后存在「■」开头行碎片 → MIXED
    • 表头行后无碎片 → TABLE(已覆盖)
  • tests/test_excel_parser.py 补 MIXED 装配用例:
    • 构造混合矩阵(ftw 单测,不必落盘):表格段 + 碎片段 → result.mixed 长度 1、段落 2 个、tables 含表格段的表与自由文本表

3.7 兼容性

  • data_models.py 扩展为追加(新增 dataclass 与字段),不修改既有字段
  • ExcelParseResult 新增字段带默认值 → 既有消费(tests、下游)不受破坏
  • TABLE / FREE_TEXT 路径代码不动

4. 验收标准

  1. classify_sheet 对混合矩阵判 MIXED(含单测)
  2. split_paragraphs 空行分段正确(含边界单测)
  3. MIXED sheet 装配后 result.mixed[name] 段落集合包含 table 与 free_text,且 tables 无碎片污染
  4. 新样本 samples/要件定義_混合型.xlsx 端到端通过
  5. 全量回归 python -m pytest 全绿

5. 非目标(显式延后)

  • 公式双保留(§3.5.7)、多级表头扁平化、隐藏行列/密码:仍延后至后续里程碑
  • 段内二次 MIXED:段落内不再触发 MIXED 判定(递归防御)
  • Impact 对段落数据的消费:下一里程碑契约

6. 涉及文件

  • 修改:src/genesis/data_models.py(追加 MixedParagraph / MixedSheetExcelParseResult 加字段)
  • 新建:src/genesis/parsers/paragraph_splitter.py
  • 修改:src/genesis/parsers/excel_parser.pyMIXED 装配分支)
  • 新建:tests/test_paragraph_splitter.py
  • 修改:tests/test_sheet_nature.py(补 MIXED 用例)、tests/test_excel_parser.py(补 MIXED 装配用例)、tests/test_real_samples.py(新增样本用例)
  • 新增:samples/要件定義_混合型.xlsx