dedupe.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. """问题去重与合并(需求 §2.3 工具 6 要求 4、§2.4 Skill 6 第 2–4 条)。
  2. 两条规则:
  3. 1. 同一地点、同一问题类型的多条记录只保留最严重的一条,被合并的记入 related_ids;
  4. 2. 单一回风系统与矿井总需风量复用同一基准(required_reuse_key 相同)时,保留
  5. 两类对象的层级关系,但同一风险不重复计数。
  6. """
  7. from __future__ import annotations
  8. from typing import Any, Iterable, Mapping
  9. from algorithms.airflow_review.contracts import unique
  10. from algorithms.airflow_review.engine.findings import severity_rank
  11. # 复用同一需风基准的等价对象:回风系统与矿井总进风/总回风都指向同一份权威需风量,
  12. # 因此按 basis 相等去重,只保留最严重的一条。
  13. _REUSE_EQUIVALENT_TYPES = ("return_air_system", "mine_total",
  14. "mine_total_intake", "mine_total_return")
  15. def dedupe_findings(findings: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
  16. """返回去重后的问题清单与合并说明。"""
  17. ordered = sorted(findings, key=lambda item: (severity_rank(item.get("severity", "")),
  18. str(item.get("finding_id") or "")))
  19. kept: list[dict[str, Any]] = []
  20. index: dict[tuple[str, str], int] = {}
  21. reused: dict[str, list[str]] = {}
  22. merged_count = 0
  23. suppressed: list[dict[str, Any]] = []
  24. for finding in ordered:
  25. item = dict(finding)
  26. key = (str(item.get("target_id") or ""), str(item.get("finding_type") or ""))
  27. reuse_key = str(item.get("required_reuse_key") or "")
  28. if reuse_key and str(item.get("target_type")) in _REUSE_EQUIVALENT_TYPES:
  29. # 同一复用基准只报一次,另一类对象只记录层级关系
  30. if reuse_key in reused:
  31. reused[reuse_key].append(str(item.get("target_id") or ""))
  32. merged_count += 1
  33. suppressed.append({"finding_id": item.get("finding_id"),
  34. "merged_into": reused[reuse_key][0],
  35. "reason": f"与 {reused[reuse_key][0]} 复用同一需风基准({reuse_key}),不重复计数",
  36. "target_id": item.get("target_id")})
  37. continue
  38. reused[reuse_key] = [str(item.get("target_id") or "")]
  39. if key in index and key[0]:
  40. existing = kept[index[key]]
  41. existing.setdefault("related_finding_ids", []).append(item.get("finding_id"))
  42. existing["duplicate_count"] = existing.get("duplicate_count", 1) + 1
  43. merged_count += 1
  44. suppressed.append({"finding_id": item.get("finding_id"),
  45. "merged_into": existing.get("finding_id"),
  46. "reason": f"与 {existing.get('finding_id')} 同一地点同一问题类型",
  47. "target_id": item.get("target_id")})
  48. continue
  49. index[key] = len(kept)
  50. kept.append(item)
  51. return {
  52. "findings": kept,
  53. "merged_count": merged_count,
  54. "merged_details": suppressed,
  55. "reuse_groups": {key: value for key, value in reused.items() if len(value) > 1},
  56. "evidence_ids": unique([eid for finding in kept for eid in finding.get("evidence_ids") or []]),
  57. }
  58. def reuse_summary(reuse_groups: Mapping[str, list[str]]) -> list[dict[str, Any]]:
  59. """把复用分组整理成报告可读的说明:同一基准下有哪些对象、只计一次风险。"""
  60. return [
  61. {"required_reuse_key": key, "target_ids": list(targets),
  62. "reason": "单一回风系统与矿井总需风量等值复用,两种对象的结论分列但风险只计一次"}
  63. for key, targets in sorted(reuse_groups.items())
  64. ]