feat: SourceParser 门面全量输入聚合 StructuredSource
This commit is contained in:
@@ -65,3 +65,4 @@
|
|||||||
| 2026-08-10 | Agent 实现 | Task1 补记:Word 解析测试基建。新建 tests/docx_helpers.py(new_document/save_document/make_rule_doc,make_rule_doc 支持 H1/H2/H3/List Bullet/普通段落 5 种行),供 Phase3 后续全部任务复用 | tests/docx_helpers.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
| 2026-08-10 | Agent 实现 | Task1 补记:Word 解析测试基建。新建 tests/docx_helpers.py(new_document/save_document/make_rule_doc,make_rule_doc 支持 H1/H2/H3/List Bullet/普通段落 5 种行),供 Phase3 后续全部任务复用 | tests/docx_helpers.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||||
| 2026-08-10 | Agent 实现 | Phase3 Task2 实现:WordTemplateParser 章构成/占位符/样式名提取。新建 src/genesis/parsers/_word_common.py(heading_level 共享 helper,Task2/3 复用,从 Heading N 样式名解析大纲级别,非数字/无后缀兜底 1)与 src/genesis/parsers/word_template_parser.py(PLACEHOLDER_RE 统一占位符正则 {{键名}}/{{键名:章节名}};Heading 段落→heading 章标记、bookmarkStart→bookmark、占位符→placeholder 并写 placeholders{名:段落上下文};styles 提取 defined/used 样式名集合去重);tests/test_word_template_parser.py 按 brief 8 用例(6 解析 + 2 heading_level 兜底分支);覆盖补齐:bookmark 用例增加无 name 的 bookmarkStart 覆盖 if name 假分支(98%→100%);TDD 验证 RED(ModuleNotFoundError: No module named 'genesis.parsers._word_common')→ GREEN(聚焦 8 passed);pytest 全量 140 passed 覆盖 100.00%(841 stmts/200 br),fail_under=99 达标 | src/genesis/parsers/_word_common.py, src/genesis/parsers/word_template_parser.py, tests/test_word_template_parser.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
| 2026-08-10 | Agent 实现 | Phase3 Task2 实现:WordTemplateParser 章构成/占位符/样式名提取。新建 src/genesis/parsers/_word_common.py(heading_level 共享 helper,Task2/3 复用,从 Heading N 样式名解析大纲级别,非数字/无后缀兜底 1)与 src/genesis/parsers/word_template_parser.py(PLACEHOLDER_RE 统一占位符正则 {{键名}}/{{键名:章节名}};Heading 段落→heading 章标记、bookmarkStart→bookmark、占位符→placeholder 并写 placeholders{名:段落上下文};styles 提取 defined/used 样式名集合去重);tests/test_word_template_parser.py 按 brief 8 用例(6 解析 + 2 heading_level 兜底分支);覆盖补齐:bookmark 用例增加无 name 的 bookmarkStart 覆盖 if name 假分支(98%→100%);TDD 验证 RED(ModuleNotFoundError: No module named 'genesis.parsers._word_common')→ GREEN(聚焦 8 passed);pytest 全量 140 passed 覆盖 100.00%(841 stmts/200 br),fail_under=99 达标 | src/genesis/parsers/_word_common.py, src/genesis/parsers/word_template_parser.py, tests/test_word_template_parser.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||||
| 2026-08-10 | Agent 实现 | Phase3 Task3 实现:RuleDocParser 规则文档 Markdown 化与分类。新建 src/genesis/parsers/rule_doc_parser.py(body 级遍历保段落/表格交错顺序;Heading N→#×N;列表双通道检测 List 样式或 ・/-/• 前缀;表格→GFM;空段→空行;file_type 固定 word、hash=sha256 hex;复用 _word_common.heading_level 无本地重复)与 tests/test_rule_doc_parser.py 按 brief 6 用例;覆盖补齐:test_parse_empty_document 增加 doc.add_paragraph("") 使空段分支(原 new_document 无任何 w:p 不进分支)达 100%;TDD 验证 RED(ModuleNotFoundError: No module named 'genesis.parsers.rule_doc_parser')→ GREEN(聚焦 6 passed);pytest 全量 146 passed 覆盖 100.00%(886 stmts/218 br),fail_under=99 达标 | src/genesis/parsers/rule_doc_parser.py, tests/test_rule_doc_parser.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
| 2026-08-10 | Agent 实现 | Phase3 Task3 实现:RuleDocParser 规则文档 Markdown 化与分类。新建 src/genesis/parsers/rule_doc_parser.py(body 级遍历保段落/表格交错顺序;Heading N→#×N;列表双通道检测 List 样式或 ・/-/• 前缀;表格→GFM;空段→空行;file_type 固定 word、hash=sha256 hex;复用 _word_common.heading_level 无本地重复)与 tests/test_rule_doc_parser.py 按 brief 6 用例;覆盖补齐:test_parse_empty_document 增加 doc.add_paragraph("") 使空段分支(原 new_document 无任何 w:p 不进分支)达 100%;TDD 验证 RED(ModuleNotFoundError: No module named 'genesis.parsers.rule_doc_parser')→ GREEN(聚焦 6 passed);pytest 全量 146 passed 覆盖 100.00%(886 stmts/218 br),fail_under=99 达标 | src/genesis/parsers/rule_doc_parser.py, tests/test_rule_doc_parser.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||||
|
| 2026-08-10 | Agent 实现 | Phase3 Task4 实现:SourceParser 门面全量输入聚合。新建 src/genesis/parsers/source_aggregator.py(角色显式传参无隐式猜测;requirements 校验 .xlsx/.xls 后缀聚合 tables/comments;template 校验 .docx 用 WordTemplateParser 解析;write_instruction+rule 校验 .docx 按 category='write' 归入 rule_docs;不存在抛 FileNotFoundError、未知扩展名抛 ValueError('不支持的文件类型: ...');image_analyses=[]/existing_system=None 固定)与 tests/test_source_aggregator.py 按 brief 6 用例 + 补 4 用例(模板未知扩展名、规则路径文件缺失、requirements/规则无扩展名兜底 '无扩展名' 文案,覆盖 51/62 行缺失与 or 表达式分支)达 100%;TDD 验证 RED(ModuleNotFoundError: No module named 'genesis.parsers.source_aggregator')→ GREEN(聚焦 10 passed);pytest 全量 156 passed 覆盖 100.00%(929 stmts/236 br),fail_under=99 达标 | src/genesis/parsers/source_aggregator.py, tests/test_source_aggregator.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_unknown_extension_in_template(tmp_path):
|
||||||
|
bad = tmp_path / "note.txt"
|
||||||
|
bad.write_text("hello", encoding="utf-8")
|
||||||
|
with pytest.raises(ValueError, match="不支持的文件类型"):
|
||||||
|
SourceParser().parse(template_path=bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_missing_rule_file(tmp_path):
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
SourceParser().parse(rule_paths=[tmp_path / "missing.docx"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_extensionless_requirement(tmp_path):
|
||||||
|
bad = tmp_path / "note"
|
||||||
|
bad.write_text("hello", encoding="utf-8")
|
||||||
|
with pytest.raises(ValueError, match="无扩展名"):
|
||||||
|
SourceParser().parse(requirement_paths=[bad])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_extensionless_rule(tmp_path):
|
||||||
|
bad = tmp_path / "note"
|
||||||
|
bad.write_text("hello", encoding="utf-8")
|
||||||
|
with pytest.raises(ValueError, match="无扩展名"):
|
||||||
|
SourceParser().parse(rule_paths=[bad])
|
||||||
Reference in New Issue
Block a user