///|
/// Condition complexity scoring.
///
/// The score operation grades every rule condition for maintainability
/// using compiled-tree evidence: node count and depth against soft
/// limits, distinct fact-path count, boolean-branch density, and
/// builtin-call usage. Each rule receives a 0-100 score (higher is
/// simpler) plus a rank (simple / moderate / complex) and the reasons
/// that lowered it. The scoring is deterministic and documented — no
/// facts are evaluated, no hidden ML.
///|
fn score_condition(
id : String,
condition : Expression,
soft_nodes : Int32,
soft_depth : Int32,
) -> Value {
let (nodes, depth) = condition.metrics()
let paths : Map[String, Bool] = Map([])
collect_paths(condition, paths)
let calls : Map[String, Bool] = Map([])
collect_calls(condition, calls)
let distinct_paths = paths.keys().collect().length()
let mut score = 100
let reasons : Array[Value] = []
let mut branch_count = 0
fn count_bools(expr : Expression) -> Unit {
match expr {
Binary("&&", left, right) => {
branch_count += 1
count_bools(left)
count_bools(right)
}
Binary("||", left, right) => {
branch_count += 1
count_bools(left)
count_bools(right)
}
Binary(_, left, right) => {
count_bools(left)
count_bools(right)
}
Unary(_, operand) => count_bools(operand)
Call(_, args) =>
for arg in args {
count_bools(arg)
}
_ => ()
}
}
count_bools(condition)
if nodes > soft_nodes {
let over = nodes - soft_nodes
let penalty = if over * 2 > 40 { 40 } else { over * 2 }
score -= penalty
reasons.push(
String(
"wide: " +
nodes.to_string() +
" nodes over limit " +
soft_nodes.to_string(),
),
)
}
if depth > soft_depth {
let over = depth - soft_depth
let penalty = if over * 5 > 30 { 30 } else { over * 5 }
score -= penalty
reasons.push(
String(
"deep: " +
depth.to_string() +
" levels over limit " +
soft_depth.to_string(),
),
)
}
if distinct_paths > 4 {
let penalty = if (distinct_paths - 4) * 5 > 20 {
20
} else {
(distinct_paths - 4) * 5
}
score -= penalty
reasons.push(
String(
"wide-facts: " + distinct_paths.to_string() + " distinct fact paths",
),
)
}
if branch_count > 3 {
let penalty = if (branch_count - 3) * 4 > 16 {
16
} else {
(branch_count - 3) * 4
}
score -= penalty
reasons.push(
String("branchy: " + branch_count.to_string() + " boolean operators"),
)
}
if calls.keys().collect().length() > 2 {
score -= 5
reasons.push(String("call-heavy: more than two builtin functions"))
}
if score < 0 {
score = 0
}
let rank = if score >= 80 {
"simple"
} else if score >= 55 {
"moderate"
} else {
"complex"
}
record([
("id", String(id)),
("score", Number(score.to_double())),
("rank", String(rank)),
("nodes", Number(nodes.to_double())),
("depth", Number(depth.to_double())),
("distinct_paths", Number(distinct_paths.to_double())),
("boolean_ops", Number(branch_count.to_double())),
("functions", strings(calls.keys().collect())),
("reasons", Array(reasons)),
])
}
///|
fn score_request(request : Value) -> Value raise {
let rules = arr(get(request, "rules"))
let soft_nodes = optional_num(get(request, "soft_nodes"), 12.0).to_int()
let soft_depth = optional_num(get(request, "soft_depth"), 4.0).to_int()
if soft_nodes <= 0 || soft_depth <= 0 {
raise input_error("E3007", "soft limits must be positive")
}
let scored : Array[Value] = []
let mut simple = 0
let mut moderate = 0
let mut complex = 0
let mut total = 0.0
for item in rules {
let id = str(get(item, "id"))
let condition = compile(str(get(item, "when")))
let entry = score_condition(id, condition, soft_nodes, soft_depth)
match get(entry, "rank") {
String(rank) =>
match rank {
"simple" => simple += 1
"moderate" => moderate += 1
_ => complex += 1
}
_ => ()
}
total += match get(entry, "score") {
Number(value) => value
_ => 0.0
}
scored.push(entry)
}
let average = if rules.length() == 0 {
0.0
} else {
total / rules.length().to_double()
}
record([
("rules", Array(scored)),
("rule_count", Number(rules.length().to_double())),
("average_score", Number(average)),
("simple", Number(simple.to_double())),
("moderate", Number(moderate.to_double())),
("complex", Number(complex.to_double())),
])
}