findings.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """问题记录的构造与规范化(§2.7.4)。
  2. 集中在一处生成,保证所有工具产出的问题字段一致:finding_type、severity、
  3. time_alignment_status、evidence_ids、manual_review_required 缺一不可。
  4. """
  5. from __future__ import annotations
  6. from typing import Any, Iterable
  7. from algorithms.airflow_review.contracts import (
  8. ALIGN_ALIGNED,
  9. FINDING_TYPES,
  10. SEV_MANUAL,
  11. SEVERITIES,
  12. as_list,
  13. unique,
  14. )
  15. class FindingFactory:
  16. """按调用顺序编号 F001、F002……;编号在单次工具调用内稳定可引用。"""
  17. def __init__(self) -> None:
  18. self._seq = 0
  19. def make(
  20. self,
  21. *,
  22. finding_type: str,
  23. conclusion: str,
  24. target_id: str = "",
  25. target_type: str = "",
  26. target_name: str = "",
  27. severity: str = SEV_MANUAL,
  28. mine_id: str = "",
  29. actual_value: float | None = None,
  30. required_value: float | None = None,
  31. difference: float | None = None,
  32. ratio: float | None = None,
  33. plan_id: str = "",
  34. plan_version: str = "",
  35. plan_verification_status: str = "",
  36. actual_window: dict[str, Any] | None = None,
  37. required_effective_window: dict[str, Any] | None = None,
  38. time_alignment_status: str = ALIGN_ALIGNED,
  39. rule_version: str = "",
  40. evidence_ids: Iterable[str] = (),
  41. calculation_trace: list[Any] | None = None,
  42. manual_review_required: bool = False,
  43. extra: dict[str, Any] | None = None,
  44. ) -> dict[str, Any]:
  45. if finding_type not in FINDING_TYPES:
  46. raise ValueError(f"未知 finding_type:{finding_type}")
  47. if severity not in SEVERITIES:
  48. raise ValueError(f"未知 severity:{severity}")
  49. self._seq += 1
  50. payload: dict[str, Any] = {
  51. "finding_id": f"F{self._seq:03d}",
  52. "finding_type": finding_type,
  53. "mine_id": mine_id,
  54. "target_id": target_id,
  55. "target_type": target_type,
  56. "target_name": target_name,
  57. "actual_value": actual_value,
  58. "required_value": required_value,
  59. "difference": difference,
  60. "ratio": ratio,
  61. "plan_id": plan_id,
  62. "plan_version": plan_version,
  63. "plan_verification_status": plan_verification_status,
  64. "actual_window": actual_window or {},
  65. "required_effective_window": required_effective_window or {},
  66. "time_alignment_status": time_alignment_status,
  67. "severity": severity,
  68. "conclusion": conclusion,
  69. "rule_version": rule_version,
  70. "evidence_ids": unique(list(evidence_ids)),
  71. "calculation_trace": calculation_trace or [],
  72. "manual_review_required": manual_review_required,
  73. }
  74. if extra:
  75. payload.update(extra)
  76. return payload
  77. def severity_rank(severity: str) -> int:
  78. """排序用权重:重大 → 一般 → 关注 → 待复核。"""
  79. order = {"major": 0, "general": 1, "attention": 2, "manual_review": 3}
  80. return order.get(severity, 9)
  81. def sort_findings(findings: list[dict[str, Any]]) -> list[dict[str, Any]]:
  82. return sorted(findings, key=lambda item: (severity_rank(item.get("severity", "")), item.get("finding_id", "")))
  83. def summarize(findings: list[dict[str, Any]]) -> dict[str, Any]:
  84. counts = {severity: 0 for severity in SEVERITIES}
  85. for finding in findings:
  86. counts[finding.get("severity", "manual_review")] = counts.get(finding.get("severity", "manual_review"), 0) + 1
  87. by_type: dict[str, int] = {}
  88. for finding in findings:
  89. by_type[finding.get("finding_type", "")] = by_type.get(finding.get("finding_type", ""), 0) + 1
  90. return {
  91. "total_findings": len(findings),
  92. "major_count": counts.get("major", 0),
  93. "general_count": counts.get("general", 0),
  94. "attention_count": counts.get("attention", 0),
  95. "manual_review_count": counts.get("manual_review", 0),
  96. "by_finding_type": by_type,
  97. "evidence_ids": unique([eid for finding in findings for eid in as_list(finding.get("evidence_ids"))]),
  98. }