api-contract-validator / server /impact_tracer.py
pushpam14's picture
Deploy Enterprise Contract Guardian — finale build
1607c63 verified
Raw
History Blame Contribute Delete
2.86 kB
"""
Phase 2 — ground-truth impact tracing.
Given an agent's predicted list of affected consumers, compare against
the cascade scenario's ground truth and emit a precision/recall-style
``ImpactTraceResult`` so the reward layer can score each consumer
decision independently (composable rubric).
"""
from dataclasses import dataclass, field
from typing import List
from .service_graph import CascadeScenario
@dataclass
class ImpactTraceResult:
"""Per-consumer outcome of one ``trace_impact`` action."""
correct_hits: List[str] = field(default_factory=list)
missed: List[str] = field(default_factory=list)
false_flags: List[str] = field(default_factory=list)
unknown_services: List[str] = field(default_factory=list)
total_consumers: int = 0
@property
def precision(self) -> float:
flagged = len(self.correct_hits) + len(self.false_flags)
if flagged == 0:
return 0.0
return len(self.correct_hits) / flagged
@property
def recall(self) -> float:
truly_affected = len(self.correct_hits) + len(self.missed)
if truly_affected == 0:
return 1.0 # nothing to find
return len(self.correct_hits) / truly_affected
@property
def f1(self) -> float:
p, r = self.precision, self.recall
if p + r == 0:
return 0.0
return 2 * p * r / (p + r)
def _normalise(name: str) -> str:
return name.strip().lower()
def trace_impact(
scenario: CascadeScenario,
predicted_affected: List[str],
) -> ImpactTraceResult:
"""Compare agent's predicted consumer list against ground truth.
``predicted_affected`` is matched case-insensitively. Names that match
no consumer in the scenario are reported in ``unknown_services`` and
treated as false flags for reward purposes.
"""
truth_lookup = {_normalise(n): n for n in scenario.ground_truth_affected}
known_consumers = {_normalise(c.name): c.name for c in scenario.consumers}
seen: set = set()
correct_hits: List[str] = []
false_flags: List[str] = []
unknown_services: List[str] = []
for raw in predicted_affected:
key = _normalise(raw)
if key in seen:
continue
seen.add(key)
if key in truth_lookup:
correct_hits.append(truth_lookup[key])
elif key in known_consumers:
false_flags.append(known_consumers[key])
else:
unknown_services.append(raw)
missed = [
name
for name in scenario.ground_truth_affected
if _normalise(name) not in {_normalise(c) for c in correct_hits}
]
return ImpactTraceResult(
correct_hits=correct_hits,
missed=missed,
false_flags=false_flags,
unknown_services=unknown_services,
total_consumers=len(scenario.consumers),
)