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

155 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 里程碑 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`
```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 / MixedSheetExcelParseResult 加字段)
- 新建:`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`