refactor(parsers): 提取 _validate_path helper(T7 架构审查整改,I8 DRY)

- source_aggregator.py 三处重复校验(扩展名白名单 + 存在性)提取为
  _validate_path(path, allowed_exts) -> Path,保留 ValueError/FileNotFoundError
  语义与错误信息,parse 三分支改用 helper
- 外部契约不变:未知扩展名→ValueError、不存在→FileNotFoundError
- 新增 3 用例直接测 helper
- TDD: RED(helper 不存在)→ GREEN(聚焦 13 passed)→ 全量 248 passed / 100.00%(1356 stmts/332 br)
This commit is contained in:
lhl
2026-08-12 23:01:58 +08:00
parent e3c714d5d9
commit 3ae1f5f38f
3 changed files with 48 additions and 15 deletions
+1
View File
@@ -81,4 +81,5 @@
| 2026-08-11 | Agent 实现 | T12(架构审查整改,P1):URI 统一 + resolver + 强验证(OV3)。新建 src/genesis/parsers/resolver.pyparse_source_uri(解析 file.xlsx#Sheet!CellRef → SourceRef,格式非法 raise URIError)、provenance_to_uriProvenance 还原,与 build 互逆)、resolve_source_uriStructuredSource 内定位真实 CellValue)、validate_source_uris(批量强验证 → ValidationResult(resolved/unresolved),格式错误或源中不存在一律 unresolved,防 QA#8 编造 URI 作弊);统一 URI 唯一生成入口 build_source_uriprovenance.py),formatting_detector 经其生成,无散落不一致;新建 tests/test_resolver.py13 用例:解析/往返/定位/批量验证/防御分支);同步 design.md §9.2 机制化说明 + §6.8 第五步存在性校验引用;TDD 验证 RED(模块缺失)→ GREEN(聚焦 10 passed)→ 全量 231 passed 覆盖 100.00%1191 stmts/298 br),fail_under=99 达标 | src/genesis/parsers/resolver.py, tests/test_resolver.py, docs/design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-11 | Agent 实现 | T13(架构审查整改,P1):黄金集 + 评分器(OV4)。新建 src/genesis/eval/ 包:golden_set.pyGoldenCase/GoldenSetYAML 加载,samples/ 真实脱敏样本作 input_ref 基线)、scorer.pyChapterScorer 按 §7.2 指标体系打分);确定性维度 traceabilityresolver 验证 source_uri 可解析率)/placeholder_residue(无 {{...}} 残留)/chapter_completeness(章节覆盖期望集合);LLM 语义维度经 llm_evaluators 钩子注入(默认中性分,待 Phase5);新增 tests/test_eval_scorer.py9 用例:各维度/聚合/钩子/空输入);tests/fixtures/eval/golden_set.yaml 示例黄金集(2 case);同步 design.md §7.5 黄金集与评分器机制化说明(定位为 CI 质量门禁);TDD 验证 RED(模块缺失 + NameError)→ GREEN(聚焦 8 passed)→ 全量 240 passed 覆盖 100.00%1279 stmts/308 br),fail_under=99 达标 | src/genesis/eval/__init__.py, src/genesis/eval/golden_set.py, src/genesis/eval/scorer.py, tests/test_eval_scorer.py, tests/fixtures/eval/golden_set.yaml, docs/design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-11 | Agent 实现 | T17(架构审查整改,P1):docx 注入原型提前(OV8)。新建 src/genesis/writer/ 包:docx_injector.pyDocxInjector 用原生 python-docx 实现 §6.6 占位符注入);Block 模型(paragraph/heading/table);章节级 {{section:id}} → 替换为内容块 docx 元素序列,行内 {{meta}} → 元信息填充;残留检查:未替换 {{...}} 抛 DocxInjectError(与 §6.6 规范一致);格式精度:注入 heading 继承模板 Heading 样式、原有内容样式不被破坏;新增 tests/test_docx_injector.py5 用例:章节/行内替换/残留报错/heading 样式继承/原内容保留);同步 design.md §6.7 渲染链路 T17 原型说明;TDD 验证 RED(模块缺失)→ GREEN(聚焦 5 passed)→ 全量 245 passed 覆盖 100.00%1361 stmts/340 br),fail_under=99 达标 | src/genesis/writer/__init__.py, src/genesis/writer/docx_injector.py, tests/test_docx_injector.py, docs/design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-11 | Agent 实现 | T7(架构审查整改,P2):提取 _validate_path helperI8 DRY)。source_aggregator.py 三处重复校验(扩展名白名单 + 存在性)提取为模块级 _validate_path(path, allowed_exts) → Path(保留 ValueError/FileNotFoundError 语义与错误信息),parse 三分支改用 helper;外部契约(未知扩展名→ValueError、不存在→FileNotFoundError)不变;新增 3 用例直接测 helper(坏扩展名/缺失/返回 Path);TDD 验证 REDhelper 不存在)→ GREEN(聚焦 13 passed)→ 全量 248 passed 覆盖 100.00%1356 stmts/332 br),fail_under=99 达标 | src/genesis/parsers/source_aggregator.py, tests/test_source_aggregator.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
| 2026-08-11 | Agent 实现 | T14+T16(架构审查整改,Lane A):DataGate 机制化 + 任务级持久化(OV5 + OV7)。新建 src/genesis/orchestrator/ 包:datagate.pyDataGate.load(source, selector):子集加载 + 规模保护 max_total_rows=500 无 selector 拒绝全量 + token 预算 max_total_tokens=8000 复用 CJK 保守估算 + 未知表容错)+ task_queue.pyTaskQueue ABC + PersistentTaskQueue SQLite 落盘:enqueue/poll/update_status/get/cancel/recover/close + 幂等去重 §5.3 + recover 将 running→failed、pending 保留);新建 tests/test_datagate.py8 用例:子集/规模保护/1000 行 Excel selector 放行/token 预算/未知表/空 selector+ tests/test_task_queue.py11 用例:CRUD/幂等缓存/重开不丢/recover 语义/防御分支);同步 agent-runtime-design.md(§4.2 原则→机制 + §3.5/3.6 任务级恢复已实现)、api-design.md §5.2/5.3、design.md §8.4.1TDD 验证 RED(模块缺失/错误消息不匹配)→ GREEN(聚焦 8+8 passed)→ 全量 218 passed 覆盖 100.00%1140 stmts/278 br),fail_under=99 达标 | src/genesis/orchestrator/__init__.py, src/genesis/orchestrator/datagate.py, src/genesis/orchestrator/task_queue.py, tests/test_datagate.py, tests/test_task_queue.py, docs/agent-runtime-design.md, docs/api-design.md, docs/design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
+18 -15
View File
@@ -11,6 +11,21 @@ XLSX_EXTS = (".xlsx", ".xls")
DOCX_EXT = ".docx"
def _validate_path(path: str | Path, allowed_exts: tuple[str, ...]) -> Path:
"""校验文件扩展名合法且文件存在(T7 DRY:消除三处重复校验)。
Raises:
ValueError: 扩展名不在 allowed_exts(含无扩展名)
FileNotFoundError: 文件不存在
"""
p = Path(path)
if p.suffix.lower() not in allowed_exts:
raise ValueError(f"不支持的文件类型: {p.suffix or '(无扩展名)'}")
if not p.exists():
raise FileNotFoundError(str(path))
return p
class SourceParser:
"""全量输入门面:Excel 要件定义 + Word 模板 + Word 规则 → StructuredSource。
@@ -37,31 +52,19 @@ class SourceParser:
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))
path = _validate_path(p, XLSX_EXTS)
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))
tpath = _validate_path(template_path, (DOCX_EXT,))
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))
path = _validate_path(p, (DOCX_EXT,))
# 做成说明书与记入规则均为 Type A 写入规则 → writeapi-design §2.2
rule_docs.append(RuleDocParser().parse(path, category="write"))
+29
View File
@@ -1,4 +1,5 @@
import pytest
from pathlib import Path
from genesis.data_models import StructuredSource
from genesis.parsers.source_aggregator import SourceParser
@@ -96,3 +97,31 @@ def test_parse_extensionless_rule(tmp_path):
bad.write_text("hello", encoding="utf-8")
with pytest.raises(ValueError, match="无扩展名"):
SourceParser().parse(rule_paths=[bad])
# ---------- T7: DRY _validate_path helperI8 ----------
def test_validate_path_rejects_bad_extension(tmp_path):
from genesis.parsers.source_aggregator import _validate_path
bad = tmp_path / "note.txt"
bad.write_text("x", encoding="utf-8")
with pytest.raises(ValueError, match="不支持的文件类型"):
_validate_path(bad, (".xlsx",))
def test_validate_path_rejects_missing_file(tmp_path):
from genesis.parsers.source_aggregator import _validate_path
with pytest.raises(FileNotFoundError):
_validate_path(tmp_path / "missing.xlsx", (".xlsx",))
def test_validate_path_returns_resolved_path(tmp_path):
from genesis.parsers.source_aggregator import _validate_path
good = tmp_path / "ok.xlsx"
good.write_bytes(b"x")
result = _validate_path(str(good), (".xlsx",))
assert isinstance(result, Path)
assert result.suffix.lower() == ".xlsx"