Files
meshcore-analyzer/cmd/server/usefulness_composite.go
3efa37c46c feat(server): complete the #672 4-axis repeater usefulness score (#1762)
Adds Coverage (harmonic reach) + Redundancy (Tarjan articulation) axes +
composite & grade. Closes #672.
**TDD note (BLOCKER-1):** Community PR delivered as a single squashed
commit, so there is no separate pre-fix failing-test commit — please
accept as a community-PR exemption. The tests are *gating*, not just
thorough: each axis test pins a specific topology outcome (coverage on
line/star/disconnected/weight-sensitive; redundancy
online/triangle/star/bridged-cliques), and an end-to-end `/api/nodes`
surface test drives the whole pipeline and asserts the composite
diverges from the Traffic axis. Inverting the `1/weight` distance,
dropping the NaN/Inf reject, removing the `redundancyMinWeight` floor,
or aliasing `usefulness_score` back onto `traffic_share_score` each
break a specific assertion. The axis functions are pure (no hidden
state), so the suite fully characterises the behavior without the red
anchor.

Co-authored-by: Waydroid Builder <build@waydroid.local>
2026-06-27 22:03:05 -07:00

157 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package main: composite repeater usefulness score + letter grade
// (issue #672). Combines the four per-axis scores — each already in
// [0, 1] — into a single weighted score, plus an AF grade for at-a-
// glance ranking.
//
// Weights (sum = 1.0) follow the #672 proposal:
//
// Bridge 0.30 structural betweenness (chokepoint)
// Coverage 0.25 harmonic reach (how much of the mesh it reaches)
// Redundancy 0.25 irreplaceability (fragmentation on removal)
// Traffic 0.20 observed relayed load
//
// All four inputs are expected to be max-normalized over the current
// repeater population (top node = 1.0): Bridge/Coverage/Redundancy by their
// Compute* functions, and Traffic by the caller dividing the raw share by the
// population max BEFORE calling here (#1762 review — without this, the raw
// traffic share (~0.020.05) collapsed its 0.20 weight to a negligible
// contribution). The exposed `traffic_share_score` field keeps the RAW share;
// only the composite uses the normalized form.
package main
// #672 composite weights. Exposed as named constants so the maintainer can
// retune without hunting through the arithmetic. Must sum to 1.0.
const (
usefulnessWeightBridge = 0.30
usefulnessWeightCoverage = 0.25
usefulnessWeightRedundancy = 0.25
usefulnessWeightTraffic = 0.20
)
// Grade thresholds on the composite score. These are a first-cut calibration,
// NOT empirically tuned against a labelled dataset: with all four axes
// max-normalized, the single most-important repeater approaches 1.0, so the
// bands are placed to spread the realistic mid-field — a node strong on two of
// the three .25.30 structural axes clears B (~0.45), one dominant axis clears
// C (~0.30), and peripheral/low-traffic nodes fall to D/F. Revisit once score
// distributions from real meshes are available; they are named constants
// precisely so retuning is a one-line change.
//
// FOLLOW-UP: a separate tuning issue should be opened to recalibrate these
// bands against observed real-mesh score histograms (cannot be filed from
// here); until then treat the letter grade as a coarse first impression and
// rank on the numeric usefulness_score for anything precise.
const (
usefulnessGradeA = 0.65
usefulnessGradeB = 0.45
usefulnessGradeC = 0.30
usefulnessGradeD = 0.15
)
// usefulnessAxes bundles the four #672 axis scores (each already in [0,1])
// so callers pass them BY NAME rather than as four adjacent, easily-swapped
// float64 arguments (#1762 review). Traffic here is the max-normalized share
// used in the composite, not the raw traffic_share_score.
type usefulnessAxes struct {
Traffic float64
Bridge float64
Coverage float64
Redundancy float64
}
// compositeUsefulness combines the four axis scores into a weighted
// composite in [0, 1] and its letter grade. Inputs are clamped defensively
// to [0, 1]; out-of-range axis values cannot push the composite outside
// the unit interval.
//
// The all-zero case is intentionally ambiguous and `axesComputed` disambiguates
// it (#1762 MAJOR-4):
//
// - axesComputed == false → the recomputers have not populated any snapshot
// yet (the first ~5 min after boot). All-zero then means "no signal YET",
// so the grade is "" (empty) and callers omit usefulness_grade rather than
// flash a misleading boot-time "F".
// - axesComputed == true → the recomputers HAVE run and genuinely scored this
// node zero on every axis (a fully isolated / unreached repeater). That is a
// real, deserved "F" and is returned as such — not hidden.
//
// A node with any non-zero axis is graded normally regardless of the flag.
func compositeUsefulness(ax usefulnessAxes, axesComputed bool) (float64, string) {
t, b, c, r := clamp01(ax.Traffic), clamp01(ax.Bridge), clamp01(ax.Coverage), clamp01(ax.Redundancy)
score := usefulnessWeightBridge*b +
usefulnessWeightCoverage*c +
usefulnessWeightRedundancy*r +
usefulnessWeightTraffic*t
score = clamp01(score)
if t == 0 && b == 0 && c == 0 && r == 0 && !axesComputed {
// Cold start: no axes computed yet — withhold the grade.
return 0, ""
}
return score, usefulnessGrade(score)
}
// usefulnessGrade maps a composite score in [0, 1] to an AF letter grade.
func usefulnessGrade(score float64) string {
switch {
case score >= usefulnessGradeA:
return "A"
case score >= usefulnessGradeB:
return "B"
case score >= usefulnessGradeC:
return "C"
case score >= usefulnessGradeD:
return "D"
default:
return "F"
}
}
// clamp01 bounds v to [0, 1].
func clamp01(v float64) float64 {
if v < 0 {
return 0
}
if v > 1 {
return 1
}
return v
}
// maxFloat returns the largest value in m, or 0 for an empty map. The local
// is named `largest` (not `max`) to avoid shadowing the Go 1.21 builtin —
// consistent with maxScoreValue in coverage_score_test.go (#1762 nit).
func maxFloat(m map[string]float64) float64 {
largest := 0.0
for _, v := range m {
if v > largest {
largest = v
}
}
return largest
}
// enrichNodeUsefulness writes the four #672 axes + composite + grade onto a
// node map (shared by the node-list and node-detail handlers). trafficRaw is
// the Traffic-axis share exposed verbatim as traffic_share_score; ax holds the
// composite inputs — its Traffic is the max-normalized share (across the
// repeater population) so all four axes contribute on a comparable [0,1]
// scale. axesComputed reports whether the structural-axis recomputers have
// produced a snapshot yet; it only matters for the all-zero node, where it
// distinguishes cold start (grade withheld) from a genuinely isolated repeater
// (real "F") — see compositeUsefulness. The usefulness_grade field is omitted
// only when the grade is empty (cold start).
func enrichNodeUsefulness(node map[string]interface{}, trafficRaw float64, ax usefulnessAxes, axesComputed bool) {
if node == nil {
return
}
node["traffic_share_score"] = trafficRaw
node["bridge_score"] = ax.Bridge
node["coverage_score"] = ax.Coverage
node["redundancy_score"] = ax.Redundancy
composite, grade := compositeUsefulness(ax, axesComputed)
node["usefulness_score"] = composite
if grade != "" {
node["usefulness_grade"] = grade
}
}