///|
fn rules_of(decision : Decision) -> Array[String] {
decision.findings.map(f => f.rule_id)
}
///|
/// Compare both policies against exactly the same observed history.
/// Explanation changes include changed error codes/evidence, even with the same verdict.
pub fn compare(
baseline : Policy,
candidate : Policy,
events : Array[Event],
) -> Comparison raise InputError {
let before = evaluate(baseline, events)
let after = evaluate(candidate, events)
let changes : Array[Change] = []
let mut newly_denied = 0
let mut newly_allowed = 0
let mut explanation_changes = 0
for i, a in before.decisions {
let b = after.decisions[i]
if a.decision != b.decision || a.findings != b.findings {
if a.decision == "allow" && b.decision == "deny" {
newly_denied += 1
} else if a.decision == "deny" && b.decision == "allow" {
newly_allowed += 1
} else {
explanation_changes += 1
}
changes.push({
event_id: a.event_id,
session: a.session,
before: a.decision,
after: b.decision,
before_rules: rules_of(a),
after_rules: rules_of(b),
before_findings: a.findings,
after_findings: b.findings,
})
}
}
{
schema_version: 1,
baseline: baseline.name,
candidate: candidate.name,
newly_denied,
newly_allowed,
explanation_changes,
changes,
}
}
///|
pub fn compare_json(
baseline : String,
candidate : String,
trace : String,
) -> String raise InputError {
@json.to_json(
compare(parse_policy(baseline), parse_policy(candidate), parse_trace(trace)),
).stringify(indent=2)
}