mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-11 16:29:56 +00:00
fix(#1229): GREEN — source-diversity confidence weighting in tier-1 resolver
Option C from issue #1229: weight neighbor-graph edges by the number of distinct observers that contributed to them, so the disambiguator prefers corroborated edges over single-source ones at the same raw score. Stacks with the geo-rejection filter merged for #1228 to give two independent defenses against cross-region prefix-collision pollution. Formula (NeighborEdge.Confidence): c = min(1.0, max(1, |Observers|) / 3.0) - 1 observer -> 1/3 weight (suspect) - 2 observers -> 2/3 weight - >=3 -> 1.0 (saturated, full historical weight) Saturation at 3 is conservative: high enough that a single chatty observer cannot dominate, low enough that 3-observer corroboration in a normally-staffed region already counts as full confidence. Tier-1 score in resolveWithContext becomes Score(now) * Confidence(). The downstream ratio guard (best >= 3x runner-up) is unchanged — a 6-observer edge with 30 obs now beats a 1-observer edge with 25 obs by 3.6x (vs. 1.2x before), enough to trigger tier 1 and skip the geo fallback that was misresolving in the cross-region case. The Observers map[string]bool field already existed on NeighborEdge and was populated on every upsert; this PR is the first to consume it in the resolver. Backward compatibility (persistence): neighbor_edges schema is unchanged. The Observers set is rebuilt by BuildFromStoreWithOptions from live observations on every graph refresh (5-min TTL), so persisted edges only carry a stale empty set during the warm-up window after a restart. Confidence() defaults n to 1 when |Observers|==0, so legacy rows resolve as single-observer (degraded but non-zero) confidence rather than disappearing — defensive. Fixes #1229
This commit is contained in:
@@ -24,6 +24,10 @@ const (
|
||||
affinityConfidenceRatio = 3.0
|
||||
// Minimum observation count to auto-resolve.
|
||||
affinityMinObservations = 3
|
||||
// Source-diversity saturation: edges contributed by this many distinct
|
||||
// observers (or more) earn full confidence weight (multiplier 1.0).
|
||||
// Fewer observers earn a proportional fraction. Issue #1229 (Option C).
|
||||
affinityObserverSaturation = 3.0
|
||||
)
|
||||
|
||||
// affinityLambda = ln(2) / half-life-hours, precomputed.
|
||||
@@ -78,14 +82,20 @@ func (e *NeighborEdge) Score(now time.Time) float64 {
|
||||
//
|
||||
// Formula: min(1.0, max(1, |Observers|) / affinityObserverSaturation).
|
||||
// With saturation=3, a single observer yields 1/3, two observers 2/3, and
|
||||
// three-or-more observers saturate at 1.0 — full historical weight.
|
||||
//
|
||||
// STUB: real implementation lands in the GREEN commit. Returning 1.0 here
|
||||
// keeps the resolver behavior identical so the RED test fails on the
|
||||
// behavioral assertion (resolver picks the wrong candidate), not on a
|
||||
// missing method.
|
||||
// three-or-more observers saturate at 1.0 — full historical weight. Edges
|
||||
// with an empty observer set (legacy persisted rows lacking the column;
|
||||
// see neighbor_persist.go backward-compat) default to a count of 1 so they
|
||||
// behave like single-observer edges rather than disappearing — defensive.
|
||||
func (e *NeighborEdge) Confidence() float64 {
|
||||
return 1.0
|
||||
n := float64(len(e.Observers))
|
||||
if n < 1 {
|
||||
n = 1
|
||||
}
|
||||
c := n / affinityObserverSaturation
|
||||
if c > 1.0 {
|
||||
c = 1.0
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// AvgSNR returns the average SNR, or 0 if no samples.
|
||||
|
||||
+11
-1
@@ -5606,6 +5606,16 @@ func (pm *prefixMap) resolveWithContext(hop string, contextPubkeys []string, gra
|
||||
// highest-affinity candidate among them. Raw score is appropriate because
|
||||
// it reflects both observation frequency and recency, which are the right
|
||||
// signals for "which candidate is this hop most likely referring to."
|
||||
//
|
||||
// Issue #1229 (Option C): the raw score is further multiplied by
|
||||
// e.Confidence() — a source-diversity factor in (0,1] derived from the
|
||||
// number of distinct observers that contributed to the edge. Edges seen
|
||||
// by a single observer are discounted to 1/3 weight; edges seen by ≥3
|
||||
// observers saturate at full weight. This stacks with the geo-rejection
|
||||
// filter merged for #1228 to give two independent lines of defense
|
||||
// against cross-region prefix-collision pollution. Backward-compatible
|
||||
// with the persistence format: legacy edges with empty Observers sets
|
||||
// fall back to single-observer weight.
|
||||
if graph != nil && len(contextPubkeys) > 0 {
|
||||
type scored struct {
|
||||
idx int
|
||||
@@ -5629,7 +5639,7 @@ func (pm *prefixMap) resolveWithContext(hop string, contextPubkeys []string, gra
|
||||
otherPK = e.NodeB
|
||||
}
|
||||
if strings.EqualFold(otherPK, candPK) {
|
||||
s := e.Score(now)
|
||||
s := e.Score(now) * e.Confidence()
|
||||
if s > bestScore {
|
||||
bestScore = s
|
||||
bestCount = e.Count
|
||||
|
||||
Reference in New Issue
Block a user