trace.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. """证据链追溯(工具 7 的确定性内核)。
  2. 把 evidence_id 解析成"原始文件 + 位置 + 数值 + 计算链 + 规则版本"。证据不完整时
  3. 必须显式报告缺失 id 并把问题状态置为 manual_review_required(需求 §2.3 工具 7 要求 3),
  4. 不允许把缺证据的结论当成完整结论输出。
  5. """
  6. from __future__ import annotations
  7. from typing import Any, Iterable, Mapping, Sequence
  8. from algorithms.airflow_review.contracts import as_mapping, unique
  9. def trace_evidence(
  10. *,
  11. evidence: Mapping[str, Mapping[str, Any]] | None = None,
  12. evidence_ids: Iterable[str] | None = None,
  13. finding_id: str = "",
  14. trace_level: str = "full",
  15. calculation_trace: Sequence[Any] | None = None,
  16. rule_versions: Sequence[str] | None = None,
  17. ) -> dict[str, Any]:
  18. """解析证据链。evidence 是各工具返回的 evidence 映射的合并结果。"""
  19. store = dict(evidence or {})
  20. requested = [eid for eid in (evidence_ids or [])]
  21. chain: list[dict[str, Any]] = []
  22. missing: list[str] = []
  23. for evidence_id in requested:
  24. record = store.get(evidence_id)
  25. if record is None:
  26. missing.append(evidence_id)
  27. continue
  28. chain.append(_to_chain_entry(record, trace_level))
  29. versions = unique([str(item) for item in (rule_versions or []) if item])
  30. versions.extend(unique([str(record.get("rule_version")) for record in store.values() if record.get("rule_version")]))
  31. complete = bool(chain) and not missing
  32. return {
  33. "finding_id": finding_id,
  34. "evidence_chain": chain,
  35. "calculation_chain": list(calculation_trace or []),
  36. "rule_versions": unique(versions),
  37. "requested_count": len(requested),
  38. "resolved_count": len(chain),
  39. "missing_evidence_ids": missing,
  40. "is_complete": complete,
  41. "trace_level": trace_level,
  42. "manual_review_required": not complete,
  43. "conclusion": ("证据链完整" if complete else
  44. ("该结论未关联任何原始证据" if not chain else
  45. f"证据链不完整,缺失 {len(missing)} 条证据:{'、'.join(missing)};"
  46. f"该问题状态必须为 manual_review_required")),
  47. }
  48. def _to_chain_entry(record: Mapping[str, Any], trace_level: str) -> dict[str, Any]:
  49. locator = as_mapping(record.get("locator"))
  50. entry: dict[str, Any] = {
  51. "evidence_id": record.get("evidence_id"),
  52. "source_file": record.get("source_file", ""),
  53. "source_type": record.get("source_type", ""),
  54. "value": record.get("normalized_value") if record.get("normalized_value") is not None
  55. else record.get("raw_value"),
  56. "source_url": f"evidence://{record.get('evidence_id')}",
  57. }
  58. if locator:
  59. entry["locator"] = locator
  60. if "page" in locator:
  61. entry["page"] = locator["page"]
  62. if "sheet" in locator:
  63. entry["sheet"] = locator["sheet"]
  64. if "cell" in locator:
  65. entry["cell"] = locator["cell"]
  66. if trace_level == "full":
  67. entry["raw_value"] = record.get("raw_value")
  68. if record.get("unit"):
  69. entry["unit"] = record["unit"]
  70. if record.get("snippet"):
  71. entry["snippet"] = record["snippet"]
  72. if record.get("confidence") is not None:
  73. entry["confidence"] = record["confidence"]
  74. if record.get("note"):
  75. entry["note"] = record["note"]
  76. return entry