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
+100
View File
@@ -0,0 +1,100 @@
"""URI resolver 与强验证(T12OV3)。
背景:design.md §9.2 定义 Citation URI 格式 `file.xlsx#SheetName!ColumnRow`
但仅 `build_source_uri` 存在,无解析、无存在性验证。OV3 裁定将其机制化:
- parse_source_uri:把 URI 解析为结构化 SourceRef(格式不一致即报错)
- provenance_to_uri:从 Provenance 还原 URI(与 build 互为逆)
- resolve_source_uri:在 StructuredSource 内定位真实单元格(存在性校验)
- validate_source_uris:批量强验证,区分 resolved/unresolved(防 QA#8 作弊——
编造的 URI 无法在源中定位,必落入 unresolved)
"""
from __future__ import annotations
from dataclasses import dataclass
from genesis.data_models import CellValue, Provenance, StructuredSource
from genesis.parsers.provenance import build_source_uri
class URIError(ValueError):
"""URI 格式非法(不符合 file.xlsx#SheetName!CellRef)。"""
@dataclass(frozen=True)
class SourceRef:
"""URI 解析后的结构化定位。"""
file_name: str
sheet_name: str
cell_ref: str
def parse_source_uri(uri: str) -> SourceRef:
"""解析 `file.xlsx#SheetName!C3` → SourceRef。
Raises:
URIError: 缺 `#` / 缺 `!` / 任一分段为空。
"""
if not isinstance(uri, str) or "#" not in uri or "!" not in uri:
raise URIError(f"URI 格式非法(期望 file.xlsx#SheetName!CellRef: {uri!r}")
file_part, rest = uri.split("#", 1)
if not file_part or "!" not in rest:
raise URIError(f"URI 格式非法(期望 file.xlsx#SheetName!CellRef: {uri!r}")
sheet_name, cell_ref = rest.split("!", 1)
if not sheet_name or not cell_ref:
raise URIError(f"URI 格式非法(Sheet/Cell 段不可为空): {uri!r}")
return SourceRef(file_name=file_part, sheet_name=sheet_name, cell_ref=cell_ref)
def provenance_to_uri(prov: Provenance) -> str:
"""从 Provenance 还原 URI(与 build_source_uri 互逆)。"""
cell_ref = f"{prov.column}{prov.row}"
return build_source_uri(prov.file_name, prov.sheet_name, cell_ref)
def resolve_source_uri(uri: str, source: StructuredSource) -> CellValue | None:
"""在 StructuredSource 中定位 URI 指向的真实单元格;不存在返回 None。"""
ref = parse_source_uri(uri)
for table in source.tables:
for row in table.rows:
for cell in row.values():
if _matches(cell, ref):
return cell
return None
def validate_source_uris(uris: list[str], source: StructuredSource) -> "ValidationResult":
"""批量强验证:把 URI 分为可在源中定位(resolved)与不可定位(unresolved)。
格式错误或源中不存在的 URI 一律归入 unresolved —— 供 QA 校验断言
「所有引用的 URI 必须存在于输入中」(design.md §6.8 第五步,T12 落地)。
"""
resolved: list[str] = []
unresolved: list[str] = []
for uri in uris:
try:
if resolve_source_uri(uri, source) is not None:
resolved.append(uri)
else:
unresolved.append(uri)
except URIError:
unresolved.append(uri)
return ValidationResult(resolved=resolved, unresolved=unresolved)
@dataclass
class ValidationResult:
resolved: list[str]
unresolved: list[str]
def _matches(cell: CellValue, ref: SourceRef) -> bool:
prov = cell.provenance
if prov is None:
return False
return (
prov.file_name == ref.file_name
and prov.sheet_name == ref.sheet_name
and f"{prov.column}{prov.row}" == ref.cell_ref
)