///|
/// Rule coverage analysis over fact batches.
///
/// The coverage operation evaluates every rule condition across a batch
/// with independent fuel per record, then reports per-rule hit counts
/// and rates, batch-wide match distributions, never-hit and always-hit
/// rules, records that no rule covers, and pairwise condition agreement
/// between rules sharing at least one hit pattern. It never raises on
/// per-record failures: failed evaluations count toward a rule's error
/// tally and shrink its evaluation base.
///|
priv struct RuleTally {
id : String
mut hits : Int32
mut errors : Int32
mut evaluated : Int32
}
///|
fn coverage_request(request : Value) -> Value raise {
let rules = load_rules(get(request, "rules"))
let budget = optional_num(get(request, "budget"), 10000.0).to_int()
if budget <= 0 || budget > 1000000 {
raise input_error("E3007", "budget out of range")
}
let facts = match get(request, "facts") {
Array(items) => items
other => [other]
}
let tallies : Array[RuleTally] = []
for rule in rules {
tallies.push({ id: rule.id, hits: 0, errors: 0, evaluated: 0, })
}
let mut covered_records = 0
let mut multi_records = 0
let mut error_records = 0
let distributions : Map[String, Int32] = Map([])
for fact in facts {
let hits : Array[String] = []
let mut record_failed = false
for i = 0; i < rules.length(); i = i + 1 {
let state : Evaluation = { fuel: budget, trace: [], }
let outcome : Bool? = Some(
boolean(evaluate(rules[i].condition, fact, state)),
) catch {
_ => {
tallies[i].errors += 1
record_failed = true
None
}
}
match outcome {
Some(hit) => {
tallies[i].evaluated += 1
if hit {
tallies[i].hits += 1
hits.push(rules[i].id)
}
}
None => ()
}
}
if record_failed {
error_records += 1
}
if hits.length() > 0 {
covered_records += 1
}
if hits.length() > 1 {
multi_records += 1
}
let pattern = if hits.is_empty() { "none" } else { hits.join("+") }
match distributions.get(pattern) {
Some(count) => distributions[pattern] = count + 1
None => distributions[pattern] = 1
}
}
let total = facts.length().to_double()
let rule_report : Array[Value] = []
let mut never_hit = 0
let mut always_hit = 0
for tally in tallies {
let rate = if tally.evaluated == 0 {
0.0
} else {
tally.hits.to_double() * 100.0 / tally.evaluated.to_double()
}
if tally.evaluated > 0 && tally.hits == 0 {
never_hit += 1
}
if tally.evaluated > 0 && tally.hits == tally.evaluated {
always_hit += 1
}
rule_report.push(
record([
("id", String(tally.id)),
("hits", Number(tally.hits.to_double())),
("evaluated", Number(tally.evaluated.to_double())),
("errors", Number(tally.errors.to_double())),
("hit_rate", Number(rate)),
]),
)
}
let patterns : Array[(String, Int32)] = []
for kv in distributions {
patterns.push((kv.0, kv.1))
}
patterns.sort_by(fn(a, b) { stable_string_compare(a.0, b.0) })
let distribution_report : Array[Value] = []
for pair in patterns {
let pattern = pair.0
let count = pair.1
distribution_report.push(
record([
("matched", String(pattern)),
("records", Number(count.to_double())),
("share", Number(count.to_double() * 100.0 / total)),
]),
)
}
record([
("records", Number(total)),
("covered_records", Number(covered_records.to_double())),
("multi_hit_records", Number(multi_records.to_double())),
("error_records", Number(error_records.to_double())),
("rules", Array(rule_report)),
("never_hit", Number(never_hit.to_double())),
("always_hit", Number(always_hit.to_double())),
("patterns", Array(distribution_report)),
])
}