impact.py 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. from __future__ import annotations
  2. from collections import defaultdict, deque
  3. from collections.abc import Iterable, Set
  4. from app.core.knowledge.contracts import ImpactResult, KnowledgeDependencyDraft
  5. def _object_key(point_key: str) -> str:
  6. parts = point_key.split("/", 2)
  7. return "/".join(parts[:2]) if len(parts) >= 2 else point_key
  8. def propagate_impact(
  9. changed_point_keys: Set[str],
  10. dependencies: Iterable[KnowledgeDependencyDraft],
  11. *,
  12. allowed_relations: Set[str],
  13. max_hops: int,
  14. max_points: int,
  15. max_objects: int,
  16. ) -> ImpactResult:
  17. if max_hops < 0 or max_points < 0 or max_objects < 1:
  18. raise ValueError("impact propagation limits are invalid")
  19. reverse: dict[str, list[str]] = defaultdict(list)
  20. for dependency in dependencies:
  21. if dependency.source not in {"governance", "deterministic_rule"}:
  22. continue
  23. if dependency.relation_type not in allowed_relations:
  24. continue
  25. reverse[dependency.to_point_key].append(dependency.from_point_key)
  26. for values in reverse.values():
  27. values.sort()
  28. roots = sorted(set(changed_point_keys))
  29. visited = set(roots)
  30. queue = deque((root, 0, (root,)) for root in roots)
  31. impacted: set[str] = set()
  32. paths: dict[str, tuple[str, ...]] = {}
  33. objects: set[str] = set()
  34. truncated = False
  35. while queue:
  36. current, hop, path = queue.popleft()
  37. if hop >= max_hops:
  38. if reverse.get(current):
  39. truncated = True
  40. continue
  41. for dependent in reverse.get(current, ()):
  42. if dependent in visited:
  43. continue
  44. candidate_object = _object_key(dependent)
  45. if len(impacted) >= max_points or (
  46. candidate_object not in objects and len(objects) >= max_objects
  47. ):
  48. truncated = True
  49. continue
  50. visited.add(dependent)
  51. impacted.add(dependent)
  52. objects.add(candidate_object)
  53. dependent_path = (*path, dependent)
  54. paths[dependent] = dependent_path
  55. queue.append((dependent, hop + 1, dependent_path))
  56. return ImpactResult(
  57. impacted_point_keys=frozenset(impacted),
  58. paths=paths,
  59. truncated=truncated,
  60. )