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:
@@ -0,0 +1,100 @@
|
||||
"""URI resolver 与强验证(T12,OV3)。
|
||||
|
||||
背景: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
|
||||
)
|
||||
Reference in New Issue
Block a user