From 5719faa05bb52a253b1408f6abbeafe4ac4c22ff Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Wed, 23 Sep 2026 14:17:38 +0700 Subject: [PATCH] refactor(policy): extract pure scoring and ranking functions --- policy/scoring.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 policy/scoring.py diff --git a/policy/scoring.py b/policy/scoring.py new file mode 100644 index 0000000..1c3634c --- /dev/null +++ b/policy/scoring.py @@ -0,0 +1,53 @@ +"""Pure score arithmetic. No model types, game rules, I/O, or policy thresholds. + +Invalid or incomplete inputs raise ValueError. Callers own evidence checks and +fallbacks; missing evidence must never become a neutral score. +""" +from __future__ import annotations + +import math +from collections.abc import Mapping + + +def _finite(value: float) -> bool: + return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) + + +def normalize_score(value: float, minimum: float, neutral: float, maximum: float) -> float: + """Map an ordered scale to [-1, 1], with neutral at zero. + + Each side is linear. Equal score intervals are a modeling assumption, not + a calibrated measure of game value. Values outside the scale are rejected. + """ + if not all(_finite(v) for v in (value, minimum, neutral, maximum)): + raise ValueError("score and scale must be finite numbers") + if not minimum < neutral < maximum: + raise ValueError("scale must satisfy minimum < neutral < maximum") + if not minimum <= value <= maximum: + raise ValueError("score is outside the scale") + span = maximum - neutral if value >= neutral else neutral - minimum + return (value - neutral) / span + + +def weighted_utility(components: Mapping[str, float], weights: Mapping[str, float]) -> float: + """Combine complete normalized components using explicit nonnegative weights. + + Weights must sum to one. Do not silently remove missing components or + renormalize weights. Confidence is not a component or a utility multiplier. + """ + if not weights or components.keys() != weights.keys(): + raise ValueError("components must exactly match nonempty weights") + if any(not _finite(w) or w < 0 for w in weights.values()): + raise ValueError("weights must be finite and nonnegative") + if not math.isclose(sum(weights.values()), 1.0, rel_tol=0, abs_tol=1e-9): + raise ValueError("weights must sum to one") + if any(not _finite(v) or not -1 <= v <= 1 for v in components.values()): + raise ValueError("components must be finite normalized scores") + return sum(weights[axis] * components[axis] for axis in weights) + + +def rank_candidates(utilities: Mapping[str, float]) -> list[str]: + """Rank highest utility first; ties retain input order. Empty input is valid.""" + if any(not _finite(value) for value in utilities.values()): + raise ValueError("candidate utilities must be finite numbers") + return sorted(utilities, key=utilities.__getitem__, reverse=True)