From 7bc9175ebe76a18105290d77f084cd32b40e20e9 Mon Sep 17 00:00:00 2001 From: lhl Date: Mon, 10 Aug 2026 09:09:40 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20Phase3=20=E8=AE=A1=E5=88=92=E4=BF=AE?= =?UTF-8?q?=E8=AE=A2=EF=BC=88Task1=20=E5=89=8D=E7=BC=80=20test:=E3=80=81?= =?UTF-8?q?=5Fheading=5Flevel=20=E6=8F=90=E5=8F=96=E5=85=B1=E4=BA=AB=20hel?= =?UTF-8?q?per=E3=80=81=E5=85=9C=E5=BA=95=E5=88=86=E6=94=AF=E8=A6=86?= =?UTF-8?q?=E7=9B=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-08-09-phase3-word-parser.md | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/docs/superpowers/plans/2026-08-09-phase3-word-parser.md b/docs/superpowers/plans/2026-08-09-phase3-word-parser.md index 9990128..3b975dd 100644 --- a/docs/superpowers/plans/2026-08-09-phase3-word-parser.md +++ b/docs/superpowers/plans/2026-08-09-phase3-word-parser.md @@ -40,8 +40,6 @@ - [ ] **Step 1: 新建 docx_helpers.py** ```python -from pathlib import Path - from docx import Document @@ -90,7 +88,7 @@ Expected: `ok`(工作目录为项目根 `D:\00_project\Genesis`) ```bash git add tests/docx_helpers.py -git commit -m "chore: docx 测试基建(新建/落盘/规则文档快捷构造)" +git commit -m "test: docx 测试基建(新建/落盘/规则文档快捷构造)" ``` --- @@ -102,7 +100,7 @@ git commit -m "chore: docx 测试基建(新建/落盘/规则文档快捷构造 - Test: `tests/test_word_template_parser.py` **Interfaces:** -- Consumes: `genesis.data_models.ChapterMarker` / `genesis.data_models.ParsedTemplate` +- 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=可选章节名) @@ -116,11 +114,24 @@ 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) @@ -210,9 +221,25 @@ def test_parse_styles_collected(tmp_path): - [ ] **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'`) +Expected: FAIL / ERROR(`ModuleNotFoundError: No module named 'genesis.parsers.word_template_parser'` / `_word_common`) -- [ ] **Step 3: 写最小实现** +- [ ] **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`: @@ -226,19 +253,12 @@ 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_]*))?\}\}") -def _heading_level(style_name: str) -> int: - """从 Heading N 样式名解析大纲级别;非数字兜底 1。""" - try: - return int(style_name.split()[-1]) - except (ValueError, IndexError): - return 1 - - class WordTemplateParser: """概要设计模板 docx 解析:章构成 / 占位符 / 样式名提取。""" @@ -258,7 +278,7 @@ class WordTemplateParser: if style_name.startswith("Heading"): sections.append(ChapterMarker( - type="heading", name=text, level=_heading_level(style_name) + type="heading", name=text, level=heading_level(style_name) )) for m in PLACEHOLDER_RE.finditer(text): @@ -286,17 +306,17 @@ class WordTemplateParser: - [ ] **Step 4: 运行测试确认通过** Run: `python -m pytest tests/test_word_template_parser.py -v` -Expected: 6 passed +Expected: 8 passed(6 解析用例 + 2 共享 helper 兜底分支用例) - [ ] **Step 5: 全量回归** Run: `python -m pytest -q` -Expected: 138 passed / 100.00%(132 基线 + 6 新增),fail_under=99 达标 +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_template_parser.py +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 章构成/占位符/样式名提取" ``` @@ -309,7 +329,7 @@ git commit -m "feat: WordTemplateParser 章构成/占位符/样式名提取" - Test: `tests/test_rule_doc_parser.py` **Interfaces:** -- Consumes: `genesis.data_models.RuleDocument`;`tests/docx_helpers.make_rule_doc` +- 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(表头+分隔行+数据行);空段 → 空行 @@ -422,18 +442,12 @@ 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 _heading_level(style_name: str) -> int: - try: - return int(style_name.split()[-1]) - except (ValueError, IndexError): - return 1 - - def _is_list_item(text: str, style_name: str) -> bool: """双通道列表检测:List 样式或文本前缀(spec §3.3)。""" if "List" in style_name: @@ -471,7 +485,7 @@ class RuleDocParser: if not stripped: md.append("") elif style_name.startswith("Heading"): - md.append("#" * _heading_level(style_name) + " " + stripped) + md.append("#" * heading_level(style_name) + " " + stripped) elif _is_list_item(stripped, style_name): md.append("- " + stripped.lstrip("・-•").strip()) else: @@ -502,7 +516,7 @@ Expected: 6 passed - [ ] **Step 5: 全量回归** Run: `python -m pytest -q` -Expected: 144 passed / 100.00%(138 + 6 新增),fail_under=99 达标 +Expected: 146 passed / 100.00%(140 + 6 新增),fail_under=99 达标 - [ ] **Step 6: 提交** @@ -699,7 +713,7 @@ Expected: 6 passed - [ ] **Step 5: 全量回归** Run: `python -m pytest -q` -Expected: 150 passed / 100.00%(144 + 6 新增),fail_under=99 达标 +Expected: 152 passed / 100.00%(146 + 6 新增),fail_under=99 达标 - [ ] **Step 6: 提交** @@ -797,7 +811,7 @@ Expected: 8 passed(现有 4 Excel + 新增 4 Word) - [ ] **Step 3: 全量回归(红线验证)** Run: `python -m pytest -q` -Expected: 154 passed / 100.00%(150 + 4 新增),fail_under=99 达标 +Expected: 156 passed / 100.00%(152 + 4 新增),fail_under=99 达标 - [ ] **Step 4: 提交**