from __future__ import annotations from typing import Any def build_risk_rule_execution_trace( manifest: dict[str, Any], *, result: dict[str, Any] | None, ) -> dict[str, Any]: evidence = result.get("evidence") if isinstance(result, dict) else {} if not isinstance(evidence, dict): evidence = {} matched = isinstance(result, dict) severity = _risk_severity(manifest) if matched else "none" steps = _build_condition_steps(manifest, evidence) if not steps: steps = [_generic_step(manifest, evidence, matched)] path_node_ids = ["start", "evidence", *[step["node_id"] for step in steps]] path_node_ids.append("hit" if matched else "pass") return { "matched": matched, "risk_level": severity, "risk_score": _risk_score(manifest), "path_node_ids": _dedupe(path_node_ids), "steps": steps, } def _build_condition_steps(manifest: dict[str, Any], evidence: dict[str, Any]) -> list[dict[str, Any]]: steps: list[dict[str, Any]] = [] condition_results = evidence.get("condition_results") condition_evidence = evidence.get("conditions") if isinstance(condition_results, dict): evidence_by_id = { str(item.get("id") or ""): item for item in condition_evidence if isinstance(item, dict) } if isinstance(condition_evidence, list) else {} for condition_id, passed in condition_results.items(): item = evidence_by_id.get(str(condition_id), {}) steps.append( { "node_id": str(condition_id), "title": _condition_title(manifest, str(condition_id)), "result": bool(passed), "operator": str(item.get("operator") or ""), "inputs": _compact_inputs(item), } ) return steps city_consistency = evidence.get("city_consistency") if isinstance(city_consistency, dict): steps.append( { "node_id": "city_consistency", "title": "城市一致性判断", "result": bool( city_consistency.get("unexpected_route_cities") or not _has_overlap( city_consistency.get("attachment_values"), city_consistency.get("reference_values"), ) ), "operator": "route_city_consistency", "inputs": { "attachment_values": city_consistency.get("attachment_values") or [], "reference_values": city_consistency.get("reference_values") or [], "home_values": city_consistency.get("home_values") or [], "unexpected_route_cities": city_consistency.get("unexpected_route_cities") or [], "explanation_hits": city_consistency.get("explanation_hits") or [], }, } ) return steps failed_conditions = evidence.get("failed_conditions") if isinstance(failed_conditions, list): for index, item in enumerate(failed_conditions, start=1): if not isinstance(item, dict): continue steps.append( { "node_id": str(item.get("id") or f"condition_{index}"), "title": _condition_title(manifest, str(item.get("id") or f"condition_{index}")), "result": True, "operator": str(item.get("operator") or ""), "inputs": _compact_inputs(item), } ) return steps def _generic_step( manifest: dict[str, Any], evidence: dict[str, Any], matched: bool, ) -> dict[str, Any]: params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {} return { "node_id": "decision", "title": "规则判断", "result": matched, "operator": str(params.get("template_key") or manifest.get("template_key") or ""), "inputs": { "condition_summary": evidence.get("condition_summary") or params.get("condition_summary") or "", "missing_fields": evidence.get("missing_fields") or [], "keyword_hits": evidence.get("keyword_hits") or [], }, } def _condition_title(manifest: dict[str, Any], condition_id: str) -> str: params = manifest.get("params") if isinstance(manifest.get("params"), dict) else {} conditions = params.get("conditions") if isinstance(params.get("conditions"), list) else [] for index, condition in enumerate(conditions, start=1): if not isinstance(condition, dict): continue current_id = str(condition.get("id") or f"condition_{index}") if current_id == condition_id: return str(condition.get("title") or condition.get("operator") or condition_id) return condition_id def _compact_inputs(item: dict[str, Any]) -> dict[str, Any]: keys = ( "fields", "left_fields", "right_fields", "left_values", "right_values", "values", "missing_fields", "keyword_hits", "dates", "range_start", "range_end", "outside_dates", ) return {key: item.get(key) for key in keys if item.get(key) not in (None, "", [])} def _risk_severity(manifest: dict[str, Any]) -> str: outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {} fail = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {} return str(fail.get("severity") or "medium") def _risk_score(manifest: dict[str, Any]) -> int | None: metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {} outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {} fail = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {} for value in (fail.get("risk_score"), metadata.get("risk_score")): try: return int(value) except (TypeError, ValueError): continue return None def _has_overlap(left: Any, right: Any) -> bool: left_set = {str(item).strip().lower() for item in left or [] if str(item).strip()} right_set = {str(item).strip().lower() for item in right or [] if str(item).strip()} return bool(left_set & right_set) def _dedupe(values: list[str]) -> list[str]: rows: list[str] = [] for value in values: if value and value not in rows: rows.append(value) return rows