From d01c7d4901f13cedbceef039461902f41279cdd7 Mon Sep 17 00:00:00 2001 From: lhl Date: Sun, 9 Aug 2026 03:45:08 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20MIXED=20=E5=AE=8C=E6=95=B4=E6=AE=B5?= =?UTF-8?q?=E8=90=BD=E8=A7=A3=E6=9E=90=E8=AE=BE=E8=AE=A1=EF=BC=88=E9=81=97?= =?UTF-8?q?=E7=95=99=E9=A1=B9#1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...26-08-09-mixed-paragraph-parsing-design.md | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-09-mixed-paragraph-parsing-design.md diff --git a/docs/superpowers/specs/2026-08-09-mixed-paragraph-parsing-design.md b/docs/superpowers/specs/2026-08-09-mixed-paragraph-parsing-design.md new file mode 100644 index 0000000..2034958 --- /dev/null +++ b/docs/superpowers/specs/2026-08-09-mixed-paragraph-parsing-design.md @@ -0,0 +1,155 @@ +# 里程碑 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.2:MIXED sheet → 段落分割 → 每段独立判定并选择最优解析 +- 显式表达段落边界与段落类型(data_models 扩展,用户选定) +- 不改变既有 TABLE / FREE_TEXT 路径行为(回归保持) + +## 3. 设计 + +### 3.1 段落分割(新模块 `src/genesis/parsers/paragraph_splitter.py`) + +```python +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)` 判定段落性质 +- `TABLE` → `forward_fill(seg, merged)` + `extract_table(...)` → `ExcelTable` +- `FREE_TEXT` → `extract_text_blocks(seg)` + `build_free_text_table(...)` → `ExcelTable` +- (段内二次出现的 MIXED 视为 FREE_TEXT 处理,防无限递归——段落粒度的 MIXED 罕见) + +### 3.3 data_models 扩展(用户选定「扩展 data_models」) + +```python +@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`)增加字段: +```python +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 / MixedSheet;ExcelParseResult 加字段) +- 新建:`src/genesis/parsers/paragraph_splitter.py` +- 修改:`src/genesis/parsers/excel_parser.py`(MIXED 装配分支) +- 新建:`tests/test_paragraph_splitter.py` +- 修改:`tests/test_sheet_nature.py`(补 MIXED 用例)、`tests/test_excel_parser.py`(补 MIXED 装配用例)、`tests/test_real_samples.py`(新增样本用例) +- 新增:`samples/要件定義_混合型.xlsx` \ No newline at end of file