feat(discovery): add SelectionTrace + TraceEntry types (Phase 1 trace surface)

Adds in-memory trace types consumed by upcoming SelectPriorityWithTrace / SelectMultiHopWithTrace siblings on MOLSRelayPolicy and RelaySet, and by the metrics emitter. ClientHash carried for debug-log correlation only; LocalAddress intentionally absent (PII-leak surface). No callers in this commit; pure type addition; existing public API unchanged. Phase 1 (telemetry-only) per the discovery rationalization plan.

cognitive committed Apr 30, 2026 at 04:06 UTC 3fea6deaea99fbe52f8b7f72c874dc93735b6ac6
1 file changed +81
portal/discovery/trace.go new
+81
@@ -0,0 +1,81 @@
1 +package discovery
2 +
3 +// SelectionTrace records observability data for a single relay-selection
4 +// invocation. The struct is populated by SelectPriorityWithTrace /
5 +// SelectMultiHopWithTrace on MOLSRelayPolicy and by the matching siblings on
6 +// RelaySet, and consumed by:
7 +//
8 +// - portal/discovery/metrics.go — emits low-cardinality fields to Prometheus
9 +// (no per-client labels: ClientHash never becomes a metric label).
10 +// - sampled debug logs (zerolog) — ClientHash carried; LocalAddress
11 +// intentionally NOT carried in the trace (PII-leak surface; ClientHash is
12 +// sufficient for log correlation).
13 +//
14 +// The trace is not part of the public RelaySet API; it is consumed in-process
15 +// only.
16 +//
17 +// See /home/alpha/.claude/plans/sophisticate-and-rationalize-discovery-rosy-parnas.md
18 +// (Phase 1 — Telemetry only) for the rationale.
19 +
20 +import "time"
21 +
22 +// SelectionTrace captures the inputs and outputs of one selection invocation.
23 +// All fields are populated by the *WithTrace methods; downstream consumers
24 +// (metrics emitter, debug logger) read but never mutate the trace.
25 +type SelectionTrace struct {
26 + Timestamp time.Time
27 +
28 + // ClientHash is hashToGF64(LocalAddress) — a single byte derived from the
29 + // client identity. Used only for sampled debug-log correlation. Not a
30 + // Prometheus label (would unbounded cardinality) and not a public field on
31 + // any external API.
32 + ClientHash uint8
33 +
34 + // Mode is "priority" or "multihop", matching the calling method.
35 + Mode string
36 +
37 + // Pool snapshot at selection time.
38 + PoolTotal int
39 + PoolEligible int
40 + PoolFallback int
41 +
42 + // Suppressed lists URLs excluded from selection along with the reason map.
43 + Suppressed []string
44 + Reasons map[string]string
45 +
46 + // Congested is true when the existing congestion-grid switch is active
47 + // (RTT mean > molsCongestionRTTThreshold).
48 + Congested bool
49 +
50 + // NonLinear is true when the variant-grid multiplier flip is active
51 + // (CV > molsCVThreshold).
52 + NonLinear bool
53 +
54 + // M1, M2 are the MOLS multipliers used for this selection.
55 + M1, M2 uint8
56 +
57 + // AvgRTT is the mean discovery RTT across the auto pool sample.
58 + AvgRTT time.Duration
59 +
60 + // CV is the coefficient of variation of per-relay discovery RTTs.
61 + CV float64
62 +
63 + // Ranked carries per-relay scoring detail for every candidate considered
64 + // (excluded URLs appear in Suppressed/Reasons instead).
65 + Ranked []TraceEntry
66 +
67 + // OutputURLs is the final ordered list returned by the selection method.
68 + OutputURLs []string
69 +
70 + // SelectionTook is wall time spent in the selection method.
71 + SelectionTook time.Duration
72 +}
73 +
74 +// TraceEntry captures per-relay scoring detail within a SelectionTrace.
75 +type TraceEntry struct {
76 + URL string
77 + Score int
78 + Confirmed bool
79 + RTT time.Duration
80 + Demoted bool
81 +}