feat(provenance): URI 统一 + resolver + 强验证(T12 架构审查整改)

- T12 (OV3, P1): 新建 src/genesis/parsers/resolver.py
  - parse_source_uri: 解析 file.xlsx#Sheet!CellRef → SourceRef(格式非法 raise URIError)
  - provenance_to_uri: Provenance 还原 URI(与 build_source_uri 互逆)
  - resolve_source_uri: StructuredSource 内定位真实 CellValue
  - validate_source_uris: 批量强验证 → ValidationResult(resolved/unresolved)
    格式错误或源中不存在一律 unresolved(防 QA#8 编造 URI 作弊)
- URI 唯一生成入口 build_source_uri(provenance.py),无散落不一致
- 新增 test_resolver.py(13 用例);同步 design.md §9.2 + §6.8 第五步
- TDD: RED(模块缺失)→ GREEN(聚焦 10 passed)→ 全量 231 passed / 100.00%(1191 stmts/298 br)
This commit is contained in:
lhl
2026-08-12 22:35:56 +08:00
parent 6a54580ca4
commit 69aec7716c
4 changed files with 270 additions and 1 deletions
+158
View File
@@ -0,0 +1,158 @@
"""URI resolver + 强验证测试(T12OV3)。
OV3 裁定:可追溯性标准未操作性定义(URI 格式不一致、无 resolver、QA#8 可作弊)
→ 统一 URI + resolver + 强验证。
本文件测试:URI 解析、provenance 往返、结构化源内定位、批量强验证(防 QA 作弊)。
"""
from __future__ import annotations
import pytest
from genesis.data_models import (
CellComment,
CellValue,
ExcelTable,
ParsedTemplate,
Provenance,
RuleDocument,
SheetType,
StructuredSource,
)
from genesis.parsers.provenance import build_source_uri
from genesis.parsers.resolver import (
SourceRef,
URIError,
ValidationResult,
parse_source_uri,
provenance_to_uri,
resolve_source_uri,
validate_source_uris,
)
# ---------- 构造小源 ----------
def _cell(file_name: str, sheet: str, row: int, col: str, value: str) -> CellValue:
return CellValue(
value=value,
provenance=Provenance(
file_name=file_name, sheet_name=sheet, row=row, column=col, column_header="x"
),
)
def _source_with(cells: list[CellValue]) -> StructuredSource:
table = ExcelTable(
name="機能一覧",
detected_type=SheetType.FUNCTION,
extraction_method="structured",
headers=["v"],
rows=[{"v": c} for c in cells],
)
return StructuredSource(
tables=[table],
template=ParsedTemplate(file_name="t.docx", sections=[], placeholders={}, styles={}),
rule_docs=[],
image_analyses=[],
existing_system=None,
comments=[],
)
# ---------- URI 解析 ----------
def test_parse_source_uri():
ref = parse_source_uri("要求.xlsx#機能一覧!C3")
assert ref.file_name == "要求.xlsx"
assert ref.sheet_name == "機能一覧"
assert ref.cell_ref == "C3"
def test_parse_source_uri_rejects_missing_hash():
with pytest.raises(URIError):
parse_source_uri("要求.xlsx機能一覧!C3")
def test_parse_source_uri_rejects_missing_bang():
with pytest.raises(URIError):
parse_source_uri("要求.xlsx#機能一覧")
def test_parse_source_uri_rejects_empty_file():
with pytest.raises(URIError):
parse_source_uri("#機能一覧!C3")
def test_parse_source_uri_rejects_empty_sheet_or_cell():
with pytest.raises(URIError):
parse_source_uri("f.xlsx#!C3")
with pytest.raises(URIError):
parse_source_uri("f.xlsx#Sheet!")
def test_resolve_skips_cell_without_provenance():
"""CellValue.provenance 为 None 时不匹配任何 URI_matches 防御分支)。"""
orphan = CellValue(value="x", provenance=None)
source = _source_with([orphan])
assert resolve_source_uri("f.xlsx#機能一覧!C3", source) is None
# ---------- provenance 往返 ----------
def test_provenance_to_uri_roundtrip():
prov = Provenance(file_name="f.xlsx", sheet_name="S", row=3, column="C", column_header="h")
uri = provenance_to_uri(prov)
assert uri == "f.xlsx#S!C3"
ref = parse_source_uri(uri)
assert ref == SourceRef(file_name="f.xlsx", sheet_name="S", cell_ref="C3")
def test_build_and_parse_roundtrip():
uri = build_source_uri("要求.xlsx", "機能一覧", "A5")
ref = parse_source_uri(uri)
assert ref == SourceRef(file_name="要求.xlsx", sheet_name="機能一覧", cell_ref="A5")
# ---------- 结构化源内定位 ----------
def test_resolve_finds_existing_cell():
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
source = _source_with([cell])
found = resolve_source_uri("f.xlsx#機能一覧!C3", source)
assert found is not None
assert found.value == "登録"
def test_resolve_returns_none_for_missing():
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
source = _source_with([cell])
assert resolve_source_uri("f.xlsx#機能一覧!D9", source) is None
def test_resolve_distinguishes_sheet():
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
source = _source_with([cell])
# 不同 Sheet 的同 cell_ref 不应命中
assert resolve_source_uri("f.xlsx#画面一覧!C3", source) is None
# ---------- 批量强验证(防 QA 作弊) ----------
def test_validate_source_uris_split():
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
source = _source_with([cell])
uris = ["f.xlsx#機能一覧!C3", "f.xlsx#機能一覧!D9", "invalid-uri"]
result = validate_source_uris(uris, source)
assert isinstance(result, ValidationResult)
assert result.resolved == ["f.xlsx#機能一覧!C3"]
assert result.unresolved == ["f.xlsx#機能一覧!D9", "invalid-uri"]
def test_validate_rejects_malformed_separately():
"""格式错误的 URI 应归入 unresolvedQA#8 不可靠 URI 无效,防作弊)。"""
cell = _cell("f.xlsx", "機能一覧", 3, "C", "登録")
source = _source_with([cell])
result = validate_source_uris(["garbage-without-hash"], source)
assert result.resolved == []
assert result.unresolved == ["garbage-without-hash"]