Files
nickol-knx-mcp/tests/test_explainable_aggregates.py
Nikolay1andClaude Opus 4.8 1a60264c78 feat(P7b): explainable aggregate scores — expose denominator + formula
Matter readiness, the completeness grade and command/status coverage each now
carry a `math` block: numerator, denominator, the exact formula that reproduces
the percentage, and (for Matter) the functions excluded from the denominator
because their category has no Matter cluster — previously a silent skip and the
biggest "why is this number what it is?" gap. Completeness also states its band
thresholds. Report-only, additive; scores unchanged, only now auditable.
Raised by external review. tests/test_explainable_aggregates.py (12/12 green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:15:40 +02:00

78 lines
3.5 KiB
Python

"""P7b explainable aggregates: every headline percentage (Matter readiness,
completeness grade, feedback coverage) must ship the numbers behind it — the
denominator, the exact formula, and what was excluded — so a reviewer can
reproduce it instead of trusting a bare number. The Matter denominator in
particular silently dropped functions with no Matter cluster; that exclusion is
now stated.
"""
from nickol_knx_mcp.project import build_loaded_from_raw
from nickol_knx_mcp.advanced import matter_readiness, completeness_grade
from nickol_knx_mcp.handover import _feedback_coverage
from nickol_knx_mcp.analyze import detect_missing_status
def _ga(addr, name, dmain, dsub):
return {"name": name, "identifier": f"GA-{addr}", "raw_address": 0, "address": addr,
"project_uid": None, "dpt": {"main": dmain, "sub": dsub}, "data_secure": False,
"communication_object_ids": [], "description": "", "comment": ""}
def _proj(gas):
raw = {"info": {"group_address_style": "ThreeLevel", "schema_version": "21"},
"group_addresses": gas, "communication_objects": {}, "devices": {},
"functions": {}, "topology": {}, "group_ranges": {}}
return build_loaded_from_raw(raw, "t.knxproj")
def main():
# two lighting commands (one with status), plus a SCENE command that has no
# Matter cluster -> must be excluded from the Matter denominator, not silently.
gas = {
"1/0/1": _ga("1/0/1", "Kitchen light switch", 1, 1),
"1/4/1": _ga("1/4/1", "Kitchen light status", 1, 11),
"1/0/2": _ga("1/0/2", "Hall light switch", 1, 1), # no status
"0/0/1": _ga("0/0/1", "Evening scene", 17, 1), # scene -> no Matter cluster
}
p = _proj(gas)
# --- Matter readiness ---
m = matter_readiness(p)
assert "math" in m, "matter_readiness must expose a math block"
math = m["math"]
# denominator must be the 2 lighting commands, numerator the 1 with status
assert math["denominator"] == 2, math
assert math["numerator"] == 1, math
assert math["pct"] == m["ready_pct"] == 50, (math, m["ready_pct"])
assert "50%" in math["formula"], math["formula"]
# the scene must be visibly excluded, not dropped
exc = math.get("excluded_from_denominator")
assert exc and exc["controllable_functions_without_a_matter_cluster"] >= 1, exc
assert "scene" in {k for k in exc["by_category"]}, exc
# --- Completeness grade ---
g = completeness_grade(p)
gm = g["math"]
assert gm["denominator"] == 8 and gm["pct"] == g["score"], gm
assert "bands" in gm and "as-built grade" in gm["bands"], gm
assert gm["formula"].endswith(f"{g['score']}%"), gm["formula"]
# --- Feedback coverage (handover) ---
missing = detect_missing_status(p)
cov = _feedback_coverage(p, missing)
# 2 lighting commands + the scene command is functional too -> commands counts
# all functional command GAs; formula must reproduce the pct exactly
assert cov["with_status"] + cov["missing"] == cov["commands"], cov
assert f"{cov['pct']}%" in cov["formula"], cov
# --- empty project: ratio undefined, no crash, formula says so ---
empty = matter_readiness(_proj({}))
assert empty["ready_pct"] == 0 and "undefined" in empty["math"]["formula"], empty["math"]
print("test_explainable_aggregates: OK — Matter/completeness/coverage each ship a "
"reproducible formula + denominator; Matter states its excluded (no-cluster) "
"functions; empty project is undefined, not a crash.")
if __name__ == "__main__":
main()