Files
meshcore-analyzer/cmd/server/coverage_score.go
T
n30nex f66cd320da perf(graph): reuse indexed shortest-path workspaces (#2293)
Red commit: `8b3cffd0` ([regression
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40203043161)).

Fixes #2226. Bridge and coverage scoring now convert keys once and reuse
indexed distance/predecessor workspaces and at most one heap item per
vertex. Existing algorithms, normalization, snapshot publication and
synchronous startup behavior remain intact.

Five paired runs, deterministic 2,000-node/10,000-edge fixture, Go
1.27.2 on Windows/amd64, GOMAXPROCS=2:

| Calculation | Median time | Allocated bytes/run |
|---|---:|---:|
| Bridge | 8.237s → 0.819s | 2,083,295,328 → 2,517,664 |
| Coverage | 4.900s → 0.694s | 1,508,440,736 → 2,209,984 |

The desktop was shared; baseline bridge runs ranged 7.81–17.56s. These
are algorithm/allocation measurements, not startup/API latency or
retained memory. Rebuilding committed `5f34b5bf` produced the exact
measured binary hash.

- Independent path oracle, edge/epsilon/tie/disconnected cases,
allocation regression, race checks and full server/frontend suites pass.
- Browser verified: isolated native servers at http://127.0.0.1:18903
and http://127.0.0.1:18904 return identical nonzero graph scores.
- O(V+E) bounded workspace; O(V(V+E) log V) calculation. No persistent
cache, schema, dependency or configuration change.

Independent reviews and local preflight checks passed. External
home-directory preflight bundle unavailable; applicable checks were
performed locally. [Final
CI](https://github.com/Kpa-clawbot/CoreScope/actions/runs/40204557795)
passed on `5f34b5bf`, including all three browser shards and both
architecture builds.
2026-10-26 15:42:29 +01:00

85 lines
3.5 KiB
Go

// Package main: coverage axis of repeater usefulness score (issue #672,
// axis 3 of 4). The "Coverage" signal is the normalized harmonic reach
// centrality of a node in the (undirected, weighted) neighbor graph: how
// well a repeater can reach the rest of the mesh. A node that sits close
// (in affinity-distance) to many other nodes covers more of the network;
// a peripheral or weakly-connected node covers little.
//
// Why harmonic (Σ 1/d) and not plain closeness (1 / Σ d): harmonic reach
// is well-defined on a DISCONNECTED graph — an unreachable node simply
// contributes 1/∞ = 0 — whereas closeness blows up. Real meshes fragment
// into components, so this matters. (Boldi & Vigna, "Axioms for
// Centrality", 2014.)
//
// It is deliberately distinct from the other axes: Traffic is empirical
// (observed relayed load), Bridge is betweenness (being ON shortest
// paths), Redundancy is removal-impact (criticality). Coverage is REACH
// breadth — a hub that can get a packet close to anyone, regardless of
// whether it currently carries that traffic.
//
// Edge weight and distance follow the bridge convention (#1235): weight =
// affinity Score(now) · observer-diversity Confidence(); Dijkstra needs a
// DISTANCE (lower = better) so cost = 1/weight. The input is the shared
// BridgeEdge weighted-edge primitive, and the same min-heap (bridgePQ)
// drives the per-source Dijkstra.
//
// Algorithm: one Dijkstra single-source shortest-path computation per
// vertex, accumulating Σ 1/d over reachable targets, then normalize by
// the max observed reach so per-node scores live in [0, 1]. Complexity
// O(V · (V + E) log V) with the binary heap, using O(V + E) space.
package main
// ComputeCoverageScores returns a map pubkey → coverage score in [0, 1]
// computed as normalized harmonic reach centrality on the undirected
// weighted graph defined by `edges`. Keys are the lowercase pubkey form
// (matching the byPathHop / persisted-edge convention).
//
// The graph and per-source shortest paths come from the shared
// weightedDistanceAdjacency / dijkstraFrom helpers (graph_weighted.go). When
// Coverage and the Bridge axis are computed from the same edge snapshot within
// one recomputeUsefulnessAxes call they therefore see byte-identical structure;
// the bridge map surfaced independently by the bridge recomputer (different
// cadence/snapshot) is NOT guaranteed to match. Self-loops and edges with
// weight < epsilon are skipped there; nodes unable to reach anyone score 0.
//
// Pure (no global state, no locks); safe to call concurrently.
func ComputeCoverageScores(edges []BridgeEdge) map[string]float64 {
graph := weightedDistanceAdjacency(edges)
if len(graph.nodes) == 0 {
return map[string]float64{}
}
harmonic := make(map[string]float64, len(graph.nodes))
work := newDistanceWorkspace(len(graph.nodes))
for s, key := range graph.nodes {
// dijkstraFrom records only reachable nodes, so every distance is
// finite — unreachable peers simply contribute nothing (harmonic
// reach treats them as 1/∞ = 0).
dijkstraFrom(graph.adj, s, work)
var reach float64
for _, t := range work.reached {
d := work.dist[t]
if t == s || d <= 0 {
continue
}
reach += 1.0 / d
}
harmonic[key] = reach
}
// Normalize by the max so the best-reaching repeater is 1.0. If max is
// 0 (e.g. a single isolated edge with no reachable pair) leave zeros.
maxH := 0.0
for _, v := range harmonic {
if v > maxH {
maxH = v
}
}
if maxH > 0 {
for k, v := range harmonic {
harmonic[k] = v / maxH
}
}
return harmonic
}