# Phase 3「Word 解析优先」实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现 Word 模板解析(WordTemplateParser)、Word 规则文档 Markdown 化(RuleDocParser)与全量输入聚合(SourceParser 门面),配套单元测试与真实样本集成测试。 **Architecture:** 在 `src/genesis/parsers/` 下新增 3 个扁平模块,复用 `data_models.py` 既有类型(ParsedTemplate/RuleDocument/StructuredSource),零模型改动、零新增依赖(python-docx 已装 1.2.0)。SourceParser 按 api-design `file_type` 语义显式接收角色参数(requirements/template/write_instruction/rules),按来源映射 category=write,组装 StructuredSource。 **Tech Stack:** Python 3.11+ / python-docx 1.2.0 / pytest 8 / openpyxl(现有 Excel 链路) **Spec:** `docs/superpowers/specs/2026-08-09-phase3-word-parser-design.md` ## Global Constraints - 覆盖率红线:`pyproject.toml` fail_under=99,新增模块需分支全覆盖(当前 132 passed / 100.00%) - 交流语言:所有注释、文档、断言消息、commit 消息**使用中文** - 不修改:`excel_parser.py`、`data_models.py`、`pyproject.toml`、任何现有测试文件 - 零真实网络:不得引入 LLM 调用;规则分类按来源映射(零 LLM) - 样本定位:`Path(__file__).resolve().parents[1] / "samples"`,样本缺失 `pytest.skip` - 导入风格:`from tests.xxx import ...`(tests 为包,先例 test_excel_parser.py:4) - 提交消息前缀:`feat:`(生产实现)/ `test:`(测试)/ `docs:`(文档) - 提交纪律:每任务独立 commit,不混提交 --- ### Task 1: tests/docx_helpers.py 测试基建 **Files:** - Create: `tests/docx_helpers.py` - Test: 由 Task 2 开始消费(本任务无可独立测试的交付物,直接作为后续 3 个任务的公共基建) **Interfaces:** - Produces: - `new_document() -> docx.Document` — 新建内存 Document(标题/表格/列表等由调用方自由添加) - `save_document(tmp_path, doc) -> str` — 落盘到 `tmp_path/source.docx` 并返回路径字符串 - `make_rule_doc(tmp_path, lines: list[tuple[str, str]]) -> str` — 快捷构造规则文档:每项 (样式名或空, 文本);`("H1", "1. 章")` → Heading 1、「"", "・リスト"」→ 普通段落(・ 前缀)。返回路径 **说明:** 与现有 `tests/excel_helpers.py`(new_workbook/save_workbook)对称。python-docx 默认模板含 `List Bullet`/`Heading 1` 样式(已实证),`doc.add_paragraph(text, style=...)` 即可。 - [ ] **Step 1: 新建 docx_helpers.py** ```python from docx import Document def new_document() -> Document: """新建内存 Word 文档(python-docx 默认模板)。""" return Document() def save_document(tmp_path, doc: Document) -> str: """落盘到 tmp_path 并返回路径字符串。""" path = tmp_path / "source.docx" doc.save(path) return str(path) def make_rule_doc(tmp_path, lines: list[tuple[str, str]]) -> str: """快捷构造规则文档。 lines 每项为 (样式标识, 文本): ("H1", "1. 章") / ("H2", "1.1 節") / ("H3", ...) → Heading 层级 ("list", "・項目") → List Bullet 样式段落 ("", "普通段落") → Normal 段落 返回落盘路径字符串。 """ doc = new_document() for kind, text in lines: if kind == "H1": doc.add_heading(text, level=1) elif kind == "H2": doc.add_heading(text, level=2) elif kind == "H3": doc.add_heading(text, level=3) elif kind == "list": doc.add_paragraph(text, style="List Bullet") else: doc.add_paragraph(text) return save_document(tmp_path, doc) ``` - [ ] **Step 2: 验证可导入** Run: `python -c "from tests.docx_helpers import new_document, save_document, make_rule_doc; print('ok')"` Expected: `ok`(工作目录为项目根 `D:\00_project\Genesis`) - [ ] **Step 3: 提交** ```bash git add tests/docx_helpers.py git commit -m "test: docx 测试基建(新建/落盘/规则文档快捷构造)" ``` --- ### Task 2: WordTemplateParser 实现 **Files:** - Create: `src/genesis/parsers/word_template_parser.py` - Test: `tests/test_word_template_parser.py` **Interfaces:** - Consumes: `genesis.data_models.ChapterMarker` / `genesis.data_models.ParsedTemplate`;`genesis.parsers._word_common.heading_level`(**本任务新建**共享 helper,Task 3 复用) - Produces: - `class WordTemplateParser`,方法 `parse(path: str | Path) -> ParsedTemplate` - 内部正则 `PLACEHOLDER_RE`:`r"\{\{([a-z][a-z0-9_]*)(?::([a-z][a-z0-9_]*))?\}\}"`(两组:group1=键名,group2=可选章节名) - 行为:Heading 段落 → `ChapterMarker(type="heading", name=段落文本, level=大纲级别 1/2/3)`;`bookmarkStart` → `type="bookmark"`;匹配占位符 → `type="placeholder"`(name=`键名`(封面型)或 `键名:章节名`(section 型)),同时写入 `placeholders` 字典 `{占位符名: 段落上下文文本}` - `styles`:`{"defined": [文档命名样式名...], "used": [各段落实际样式名去重]}`(样式名级,不提取字号/颜色) - 空文档/无 Heading → 空 `sections`/`placeholders`,不崩溃;无法打开 → 透出 python-docx 异常 - [ ] **Step 1: 写失败测试** Create `tests/test_word_template_parser.py`: ```python from genesis.data_models import ChapterMarker, ParsedTemplate from genesis.parsers._word_common import heading_level from genesis.parsers.word_template_parser import WordTemplateParser from tests.docx_helpers import new_document, save_document def test_heading_level_parses_numeric_suffix(): assert heading_level("Heading 1") == 1 assert heading_level("Heading 2") == 2 assert heading_level("Heading 3") == 3 def test_heading_level_fallback_on_invalid(): # 兜底分支(非数字 / 无后缀)→ 1,分支覆盖必须命中 assert heading_level("Heading X") == 1 assert heading_level("Heading") == 1 def test_parse_extracts_heading_levels(tmp_path): doc = new_document() doc.add_heading("1. はじめに", level=1) doc.add_heading("2.1 画面遷移図", level=2) doc.add_heading("2.1.1 詳細", level=3) path = save_document(tmp_path, doc) result = WordTemplateParser().parse(path) assert isinstance(result, ParsedTemplate) headings = [s for s in result.sections if s.type == "heading"] assert [(s.name, s.level) for s in headings] == [ ("1. はじめに", 1), ("2.1 画面遷移図", 2), ("2.1.1 詳細", 3), ] def test_parse_extracts_bookmark(tmp_path): from docx.oxml.ns import qn doc = new_document() para = doc.add_paragraph("アンカー") bm_start = para._p.makeelement(qn("w:bookmarkStart"), {qn("w:id"): "0", qn("w:name"): "template_start"}) para._p.insert(0, bm_start) path = save_document(tmp_path, doc) result = WordTemplateParser().parse(path) bookmarks = [s for s in result.sections if s.type == "bookmark"] assert [s.name for s in bookmarks] == ["template_start"] def test_parse_extracts_placeholders(tmp_path): doc = new_document() doc.add_paragraph("{{doc_title}}") doc.add_paragraph("{{section:introduction}}") doc.add_paragraph("{{section:function_list}}") path = save_document(tmp_path, doc) result = WordTemplateParser().parse(path) assert result.placeholders == { "doc_title": "{{doc_title}}", "section:introduction": "{{section:introduction}}", "section:function_list": "{{section:function_list}}", } ph = [s for s in result.sections if s.type == "placeholder"] assert [s.name for s in ph] == ["doc_title", "section:introduction", "section:function_list"] def test_parse_invalid_placeholder_kept_as_text(tmp_path): doc = new_document() doc.add_paragraph("{{ invalid }}") doc.add_paragraph("ただの {text}") path = save_document(tmp_path, doc) result = WordTemplateParser().parse(path) assert result.placeholders == {} assert [s for s in result.sections if s.type == "placeholder"] == [] def test_parse_empty_document(tmp_path): doc = new_document() path = save_document(tmp_path, doc) result = WordTemplateParser().parse(path) assert result.sections == [] assert result.placeholders == {} assert "Normal" in result.styles["defined"] def test_parse_styles_collected(tmp_path): doc = new_document() doc.add_heading("章", level=1) doc.add_paragraph("本文") path = save_document(tmp_path, doc) result = WordTemplateParser().parse(path) assert "Heading 1" in result.styles["used"] assert "Normal" in result.styles["used"] assert "Heading 1" in result.styles["defined"] ``` - [ ] **Step 2: 运行测试确认失败** Run: `python -m pytest tests/test_word_template_parser.py -v` Expected: FAIL / ERROR(`ModuleNotFoundError: No module named 'genesis.parsers.word_template_parser'` / `_word_common`) - [ ] **Step 3: 写最小实现(含共享 helper)** Create `src/genesis/parsers/_word_common.py`(Task 2/3 共享,避免重复定义): ```python """Word 解析共享小工具(WordTemplateParser / RuleDocParser 复用)。""" from __future__ import annotations def heading_level(style_name: str) -> int: """从 Heading N 样式名解析大纲级别;非数字/无后缀兜底 1。""" try: return int(style_name.split()[-1]) except (ValueError, IndexError): return 1 ``` Create `src/genesis/parsers/word_template_parser.py`: ```python from __future__ import annotations import re from pathlib import Path from docx import Document from docx.oxml.ns import qn from genesis.data_models import ChapterMarker, ParsedTemplate from genesis.parsers._word_common import heading_level # 统一占位符正则:{{键名}} 或 {{键名:章节名}}(spec §3.2) PLACEHOLDER_RE = re.compile(r"\{\{([a-z][a-z0-9_]*)(?::([a-z][a-z0-9_]*))?\}\}") class WordTemplateParser: """概要设计模板 docx 解析:章构成 / 占位符 / 样式名提取。""" def parse(self, path: str | Path) -> ParsedTemplate: doc = Document(str(path)) sections: list[ChapterMarker] = [] placeholders: dict[str, str] = {} used_styles: set[str] = set() # 文档命名样式(定义集合) defined = {s.name for s in doc.styles if s.name} for para in doc.paragraphs: style_name = para.style.name if para.style else "Normal" used_styles.add(style_name) text = para.text if style_name.startswith("Heading"): sections.append(ChapterMarker( type="heading", name=text, level=heading_level(style_name) )) for m in PLACEHOLDER_RE.finditer(text): if m.group(2): key = f"{m.group(1)}:{m.group(2)}" else: key = m.group(1) placeholders[key] = text sections.append(ChapterMarker(type="placeholder", name=key, level=0)) # 书签:遍历 body 中全部 bookmarkStart for bm in doc.element.body.iter(qn("w:bookmarkStart")): name = bm.get(qn("w:name")) if name: sections.append(ChapterMarker(type="bookmark", name=name, level=0)) return ParsedTemplate( file_name=Path(path).name, sections=sections, placeholders=placeholders, styles={"defined": sorted(defined), "used": sorted(used_styles)}, ) ``` - [ ] **Step 4: 运行测试确认通过** Run: `python -m pytest tests/test_word_template_parser.py -v` Expected: 8 passed(6 解析用例 + 2 共享 helper 兜底分支用例) - [ ] **Step 5: 全量回归** Run: `python -m pytest -q` Expected: 140 passed / 100.00%(132 基线 + 8 新增),fail_under=99 达标(branch 模式) - [ ] **Step 6: 提交** ```bash git add tests/test_word_template_parser.py src/genesis/parsers/_word_common.py src/genesis/parsers/word_template_parser.py git commit -m "feat: WordTemplateParser 章构成/占位符/样式名提取" ``` --- ### Task 3: RuleDocParser 实现 **Files:** - Create: `src/genesis/parsers/rule_doc_parser.py` - Test: `tests/test_rule_doc_parser.py` **Interfaces:** - Consumes: `genesis.data_models.RuleDocument`;`genesis.parsers._word_common.heading_level`(Task 2 共享 helper);`tests.docx_helpers.make_rule_doc` - Produces: - `class RuleDocParser`,方法 `parse(path: str | Path, category: str = "write") -> RuleDocument` - Markdown 化:Heading N → `#`×N;`・`/`-`/`•` 前缀文本或 List Bullet/Number 样式 → `- ` 项;普通段落 → 原文;表格 → GFM(表头+分隔行+数据行);空段 → 空行 - `file_type` 固定 `"word"`;`hash` = 文件内容 sha256 hex - [ ] **Step 1: 写失败测试** Create `tests/test_rule_doc_parser.py`: ```python from genesis.data_models import RuleDocument from genesis.parsers.rule_doc_parser import RuleDocParser from tests.docx_helpers import make_rule_doc, new_document, save_document def test_parse_heading_and_bullet(tmp_path): path = make_rule_doc(tmp_path, [ ("H1", "1. 機能一覧の書き方"), ("list", "・機能ID は F001 から連番で付与する。"), ("list", "・省略記号(~、…)は禁止し、正式名称を記載する。"), ]) result = RuleDocParser().parse(path, category="write") assert isinstance(result, RuleDocument) assert result.category == "write" assert result.file_type == "word" assert result.file_name == "source.docx" assert "# 1. 機能一覧の書き方" in result.markdown_content assert "- 機能ID は F001 から連番で付与する。" in result.markdown_content assert "- 省略記号(~、…)は禁止し、正式名称を記載する。" in result.markdown_content def test_parse_prefix_bullet_without_list_style(tmp_path): # 真实样本的「・」前缀在 Normal 样式段落(非 List Bullet)——双通道检测 doc = new_document() doc.add_paragraph("・表ヘッダーは太字とし、下線を付ける。") path = save_document(tmp_path, doc) result = RuleDocParser().parse(path) assert "- 表ヘッダーは太字とし、下線を付ける。" in result.markdown_content def test_parse_table_to_markdown(tmp_path): doc = new_document() table = doc.add_table(rows=3, cols=2) headers = ["項目", "規則"] for c, h in enumerate(headers): table.cell(0, c).text = h table.cell(1, 0).text = "表ヘッダー" table.cell(1, 1).text = "太字のみ" table.cell(2, 0).text = "枠線" table.cell(2, 1).text = "付ける" path = save_document(tmp_path, doc) result = RuleDocParser().parse(path) lines = result.markdown_content.splitlines() assert "| 項目 | 規則 |" in lines assert "| --- | --- |" in lines assert "| 表ヘッダー | 太字のみ |" in lines assert "| 枠線 | 付ける |" in lines def test_parse_empty_document(tmp_path): doc = new_document() path = save_document(tmp_path, doc) result = RuleDocParser().parse(path) assert result.markdown_content == "" def test_parse_hash_stable_and_sha256(tmp_path): path = make_rule_doc(tmp_path, [("H1", "章"), ("", "本文")]) r1 = RuleDocParser().parse(path) r2 = RuleDocParser().parse(path) assert r1.hash == r2.hash assert len(r1.hash) == 64 # sha256 hex def test_parse_design_category(tmp_path): path = make_rule_doc(tmp_path, [("H1", "アーキテクチャ制約")]) result = RuleDocParser().parse(path, category="design") assert result.category == "design" ``` - [ ] **Step 2: 运行测试确认失败** Run: `python -m pytest tests/test_rule_doc_parser.py -v` Expected: FAIL / ERROR(`ModuleNotFoundError: No module named 'genesis.parsers.rule_doc_parser'`) - [ ] **Step 3: 写最小实现** Create `src/genesis/parsers/rule_doc_parser.py`: ```python from __future__ import annotations import hashlib from pathlib import Path from docx import Document from docx.oxml.ns import qn from genesis.data_models import RuleDocument from genesis.parsers._word_common import heading_level # 列表项前缀(真实样本为 Normal 样式 + ・ 前缀,样式名通道不足以命中) LIST_PREFIXES = ("・", "-", "•") def _is_list_item(text: str, style_name: str) -> bool: """双通道列表检测:List 样式或文本前缀(spec §3.3)。""" if "List" in style_name: return True return text.startswith(LIST_PREFIXES) def _table_to_markdown(table) -> list[str]: """docx 表格 → GFM 表格行。""" lines: list[str] = [] for r_idx, row in enumerate(table.rows): cells = [cell.text.replace("|", "\\|").strip() for cell in row.cells] lines.append("| " + " | ".join(cells) + " |") if r_idx == 0: lines.append("| " + " | ".join(["---"] * len(cells)) + " |") return lines class RuleDocParser: """规则文档 docx 解析:Markdown 化 + 分类。""" def parse(self, path: str | Path, category: str = "write") -> RuleDocument: doc = Document(str(path)) md: list[str] = [] # 按文档顺序遍历段落与表格(body 级子元素) for child in doc.element.body.iterchildren(): tag = child.tag if tag == qn("w:p"): from docx.text.paragraph import Paragraph para = Paragraph(child, doc) text = para.text style_name = para.style.name if para.style else "Normal" stripped = text.strip() if not stripped: md.append("") elif style_name.startswith("Heading"): md.append("#" * heading_level(style_name) + " " + stripped) elif _is_list_item(stripped, style_name): md.append("- " + stripped.lstrip("・-•").strip()) else: md.append(stripped) elif tag == qn("w:tbl"): from docx.table import Table md.extend(_table_to_markdown(Table(child, doc))) md.append("") content = "\n".join(md).strip() return RuleDocument( file_name=Path(path).name, category=category, markdown_content=content, source_path=str(path), file_type="word", hash=hashlib.sha256(Path(path).read_bytes()).hexdigest(), ) ``` > 说明:`Paragraph(child, doc)` / `Table(child, doc)` 的第二个参数(parent)只需提供 `.part` 属性;`Document` 实例满足,实测可用。若实现中直接从 `doc.paragraphs` + `doc.tables` 读取,将丢失**文档内交错顺序**(段落/表格混排),故用 body 级遍历保证顺序正确。 - [ ] **Step 4: 运行测试确认通过** Run: `python -m pytest tests/test_rule_doc_parser.py -v` Expected: 6 passed - [ ] **Step 5: 全量回归** Run: `python -m pytest -q` Expected: 146 passed / 100.00%(140 + 6 新增),fail_under=99 达标 - [ ] **Step 6: 提交** ```bash git add tests/test_rule_doc_parser.py src/genesis/parsers/rule_doc_parser.py git commit -m "feat: RuleDocParser 规则文档 Markdown 化与分类" ``` --- ### Task 4: SourceParser 门面实现 **Files:** - Create: `src/genesis/parsers/source_aggregator.py` - Test: `tests/test_source_aggregator.py` **Interfaces:** - Consumes: `genesis.parsers.excel_parser.ExcelParser`(现有)、`genesis.parsers.word_template_parser.WordTemplateParser`(Task 2)、`genesis.parsers.rule_doc_parser.RuleDocParser`(Task 3);`genesis.data_models.StructuredSource` - Produces: - `class SourceParser`,方法 `parse(requirement_paths: list[str | Path] | None = None, template_path: str | Path | None = None, write_instruction_paths: list[str | Path] | None = None, rule_paths: list[str | Path] | None = None) -> StructuredSource` - 角色→解析器:`.xlsx`(requirements)→ ExcelParser → tables/comments;`template_path` → WordTemplateParser;`write_instruction_paths`/`rule_paths`(仅 .docx)→ RuleDocParser(category="write") - 错误:文件不存在 → `FileNotFoundError`;未知扩展名 → `ValueError("不支持的文件类型: ...")` - `image_analyses=[]`、`existing_system=None` 固定(本轮无来源) - [ ] **Step 1: 写失败测试** Create `tests/test_source_aggregator.py`: ```python import pytest from genesis.data_models import StructuredSource from genesis.parsers.source_aggregator import SourceParser from tests.docx_helpers import make_rule_doc, new_document, save_document from tests.excel_helpers import new_workbook, save_workbook def _xlsx(tmp_path, name: str = "source.xlsx") -> str: wb = new_workbook({"機能一覧": [["機能ID", "機能名"], ["A001", "社員登録"]]}) path = tmp_path / name wb.save(path) return str(path) def test_parse_full_assembly(tmp_path): xlsx = _xlsx(tmp_path) template = save_document(tmp_path, new_document()) rule = make_rule_doc(tmp_path, [("H1", "1. 機能一覧の書き方")]) instr = make_rule_doc(tmp_path, [("H1", "2. 機能一覧の作成手順")]) result = SourceParser().parse( requirement_paths=[xlsx], template_path=template, write_instruction_paths=[instr], rule_paths=[rule], ) assert isinstance(result, StructuredSource) assert len(result.tables) == 1 assert result.template is not None assert result.template.file_name == "source.docx" assert len(result.rule_docs) == 2 assert all(r.category == "write" for r in result.rule_docs) assert result.image_analyses == [] assert result.existing_system is None def test_parse_without_template_and_rules(tmp_path): xlsx = _xlsx(tmp_path) result = SourceParser().parse(requirement_paths=[xlsx]) assert len(result.tables) == 1 assert result.template is None assert result.rule_docs == [] def test_parse_missing_requirement_file(tmp_path): with pytest.raises(FileNotFoundError): SourceParser().parse(requirement_paths=[tmp_path / "missing.xlsx"]) def test_parse_missing_template_file(tmp_path): xlsx = _xlsx(tmp_path) with pytest.raises(FileNotFoundError): SourceParser().parse(requirement_paths=[xlsx], template_path=tmp_path / "missing.docx") def test_parse_unknown_extension_in_requirements(tmp_path): bad = tmp_path / "note.txt" bad.write_text("hello", encoding="utf-8") with pytest.raises(ValueError, match="不支持的文件类型"): SourceParser().parse(requirement_paths=[bad]) def test_parse_unknown_extension_in_rules(tmp_path): bad = tmp_path / "note.txt" bad.write_text("hello", encoding="utf-8") with pytest.raises(ValueError, match="不支持的文件类型"): SourceParser().parse(rule_paths=[bad]) ``` - [ ] **Step 2: 运行测试确认失败** Run: `python -m pytest tests/test_source_aggregator.py -v` Expected: FAIL / ERROR(`ModuleNotFoundError: No module named 'genesis.parsers.source_aggregator'`) - [ ] **Step 3: 写最小实现** Create `src/genesis/parsers/source_aggregator.py`: ```python from __future__ import annotations from pathlib import Path from genesis.data_models import StructuredSource from genesis.parsers.excel_parser import ExcelParser from genesis.parsers.rule_doc_parser import RuleDocParser from genesis.parsers.word_template_parser import WordTemplateParser XLSX_EXTS = (".xlsx", ".xls") DOCX_EXT = ".docx" class SourceParser: """全量输入门面:Excel 要件定义 + Word 模板 + Word 规则 → StructuredSource。 角色由调用方按 api-design file_type 语义显式传入(requirements/template/ write_instruction/rules),不做基于文件名的隐式猜测(spec §3.4)。 """ def __init__(self) -> None: self._excel = ExcelParser() def parse( self, requirement_paths: list[str | Path] | None = None, template_path: str | Path | None = None, write_instruction_paths: list[str | Path] | None = None, rule_paths: list[str | Path] | None = None, ) -> StructuredSource: requirement_paths = requirement_paths or [] write_instruction_paths = write_instruction_paths or [] rule_paths = rule_paths or [] tables = [] comments = [] for p in requirement_paths: path = Path(p) if path.suffix.lower() not in XLSX_EXTS: raise ValueError(f"不支持的文件类型: {path.suffix or '(无扩展名)'}") if not path.exists(): raise FileNotFoundError(str(p)) result = self._excel.parse(path) tables.extend(result.tables) comments.extend(result.comments) template = None if template_path is not None: tpath = Path(template_path) if tpath.suffix.lower() != DOCX_EXT: raise ValueError(f"不支持的文件类型: {tpath.suffix or '(无扩展名)'}") if not tpath.exists(): raise FileNotFoundError(str(template_path)) template = WordTemplateParser().parse(tpath) rule_docs = [] for p in [*write_instruction_paths, *rule_paths]: path = Path(p) if path.suffix.lower() != DOCX_EXT: raise ValueError(f"不支持的文件类型: {path.suffix or '(无扩展名)'}") if not path.exists(): raise FileNotFoundError(str(p)) # 做成说明书与记入规则均为 Type A 写入规则 → write(api-design §2.2) rule_docs.append(RuleDocParser().parse(path, category="write")) return StructuredSource( tables=tables, template=template, rule_docs=rule_docs, image_analyses=[], existing_system=None, comments=comments, ) ``` - [ ] **Step 4: 运行测试确认通过** Run: `python -m pytest tests/test_source_aggregator.py -v` Expected: 6 passed - [ ] **Step 5: 全量回归** Run: `python -m pytest -q` Expected: 152 passed / 100.00%(146 + 6 新增),fail_under=99 达标 - [ ] **Step 6: 提交** ```bash git add tests/test_source_aggregator.py src/genesis/parsers/source_aggregator.py git commit -m "feat: SourceParser 门面全量输入聚合 StructuredSource" ``` --- ### Task 5: 真实样本集成测试(test_real_samples.py 扩展) **Files:** - Modify: `tests/test_real_samples.py`(追加 3 个 Word 样本用例 + 1 个全量组装用例;不删改现有 Excel 用例) **Interfaces:** - Consumes: `WordTemplateParser`(Task 2)、`RuleDocParser`(Task 3)、`SourceParser`(Task 4);`SAMPLES = Path(__file__).resolve().parents[1] / "samples"`(现有) - Produces: 真实样本验证——模板 7 个 H1 + 占位符 + 1 书签;记入规则 category=write markdown;说明书画属 write;全量组装双向字段 - [ ] **Step 1: 追加测试用例** Append to `tests/test_real_samples.py`(保留现有 4 个 Excel 用例): ```python from genesis.parsers.word_template_parser import WordTemplateParser from genesis.parsers.rule_doc_parser import RuleDocParser from genesis.parsers.source_aggregator import SourceParser def _d(name: str) -> Path: return SAMPLES / name def test_word_template_sample_chapters_and_placeholders(): p = _d("概要設計書テンプレート.docx") if not p.exists(): pytest.skip("样本缺失") result = WordTemplateParser().parse(p) headings = [s for s in result.sections if s.type == "heading"] h1 = [h for h in headings if h.level == 1] assert len(h1) == 7 assert h1[0].name == "1. はじめに" assert h1[-1].name == "7. バッチ一覧" assert "section:introduction" in result.placeholders assert "doc_title" in result.placeholders bookmarks = [s for s in result.sections if s.type == "bookmark"] assert len(bookmarks) == 1 assert bookmarks[0].name == "template_start" def test_rule_doc_sample_markdown_and_category(): p = _d("記入規則.docx") if not p.exists(): pytest.skip("样本缺失") result = RuleDocParser().parse(p, category="write") assert result.category == "write" assert result.file_type == "word" assert "# 1. 機能一覧の書き方" in result.markdown_content assert "- 機能ID は F001 から連番で付与する。" in result.markdown_content def test_write_instruction_sample_category_write(): p = _d("概要設計做成説明書.docx") if not p.exists(): pytest.skip("样本缺失") result = RuleDocParser().parse(p, category="write") assert result.category == "write" assert "# 2. 機能一覧" in result.markdown_content def test_source_parser_full_sample_assembly(): xlsx = _x("要件定義_新規開発.xlsx") template = _d("概要設計書テンプレート.docx") rule = _d("記入規則.docx") instr = _d("概要設計做成説明書.docx") if not all(p.exists() for p in [xlsx, template, rule, instr]): pytest.skip("样本缺失") result = SourceParser().parse( requirement_paths=[xlsx], template_path=template, write_instruction_paths=[instr], rule_paths=[rule], ) assert result.tables assert result.template is not None assert len(result.rule_docs) == 2 assert all(r.category == "write" for r in result.rule_docs) ``` - [ ] **Step 2: 运行测试确认通过** Run: `python -m pytest tests/test_real_samples.py -v` Expected: 8 passed(现有 4 Excel + 新增 4 Word) - [ ] **Step 3: 全量回归(红线验证)** Run: `python -m pytest -q` Expected: 156 passed / 100.00%(152 + 4 新增),fail_under=99 达标 - [ ] **Step 4: 提交** ```bash git add tests/test_real_samples.py git commit -m "test: 真实样本 Word 解析集成测试(模板/规则/说明书画属write)" ``` --- ## Self-Review 对照 **Spec 覆盖:** - §3.2 WordTemplateParser(章构成/占位符/书签/样式名)→ Task 2 - §3.3 RuleDocParser(Markdown 化 / 双通道列表 / hash / category)→ Task 3 - §3.4 SourceParser 门面(显式角色参数 / 扩展名路由 / 错误处理)→ Task 4 - §3.5 数据流(Excel→tables、模板→template、规则→rule_docs)→ Task 4 全量组装测试 + Task 5 真实样本组装 - §5 验收标准 1/2/3/4 → Task 2-5;验收标准 5(手工破坏)→ 可选不落自动化 - spec §4 测试基建 docx_helpers → Task 1 **占位符扫描:** 全部步骤含完整代码与精确命令,无 TBD/「后续处理」/「类似上文」等占位。 **类型一致性:** - `WordTemplateParser.parse(path) -> ParsedTemplate` 在 Task 2 定义,Task 4/5 引用同名同型 - `RuleDocParser.parse(path, category="write") -> RuleDocument` 在 Task 3 定义,Task 4 传 `category="write"`、Task 5 传显式 - `SourceParser.parse(requirement_paths, template_path, write_instruction_paths, rule_paths) -> StructuredSource` Task 4 定义,Task 5 引用 - `docx_helpers.make_rule_doc(tmp_path, lines)` 在 Task 1 定义,Task 3/4/5 引用 - `ExcelParser().parse()` 返回 `.tables/.comments`(现有,test_excel_parser.py 先例) **预期提交数:** 5 个(Task 1-5 各 1)