docs: Phase3 计划修订(Task1 前缀 test:、_heading_level 提取共享 helper、兜底分支覆盖)
This commit is contained in:
@@ -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: 提交**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user