GET /api/v1/bgp/selection-funnel

GET /api/v1/bgp/selection-funnel?as_id=<uuid>&from_asn=64801&to_asn=65100

Selection funnel: for the prefixes whose live winning path traverses the observed adjacency from_asn→to_asn (path-adjacency scope — an AS_PATH neighbour pair, never a verified session), the RFC 4271 §9.1 step at which the best-path decision resolved: winner vs strongest runner-up, computed with the same comparator the engine's selection uses. Numeric steps carry the median winning margin. Self-checking: a prefix whose computed winner contradicts the stored is_best lands in undetermined — never a guess (a MED-non-transitivity pairwise loss also refuses this way). Live only — it reads bgp_rib_entry; there is no at= parameter, and the UI shows a refusal line on a historical cursor instead.

Auth: any authenticated user. Query: the as-graph scope selector (as_id, or network_id+asn; optional af, default ipv4), from_asn + to_asn (both required numeric; to_asn=0 — the fold column — is refused with 400: an aggregate carries no per-adjacency claim), limit (prefix cap, clamped ≤10000; truncated: true marks a lower bound).

Response 200 OK:

{
  "from_asn": 64801, "to_asn": 65100, "prefixes": 42,
  "steps": [
    { "step": "local_pref", "count": 30, "median_margin": 100 },
    { "step": "as_path_len", "count": 8, "median_margin": 1 },
    { "step": "only_path", "count": 3 }
  ],
  "undetermined": 1, "truncated": false
}

step ∈ eligibility (§9.1.2 loop/next-hop ineligibility left it the only survivor), only_path (nothing to compare), local_pref, as_path_len, origin, med, ebgp_over_ibgp, igp_cost, router_id, tie (indistinguishable through the whole ladder). Steps with zero prefixes are omitted; the UI renders the block inside every adjacency card of the AS-flow lenses.