# RANKING — Bike-Tinder The deck is ordered by predicted taste. Three rungs, in order: the hunt's existing rubric (v1), a cheap explainable boost from swipe clusters (v1.x), then a small learned model (Phase 3). Nothing ever becomes a black box. ## Signals - Like = strong positive (+1). Maybe = weak positive (+0.4) on the *direction* — "right lane, wrong trim" (Glenn's read): the family/silhouette reads positive, the specific build is uncertain; maybes replenish via second looks + variant hunts. Nope = negative (−1). List-view edits count identically; the latest call wins (full history lives in `swipe_events`). - Planned v2 signals: dwell time on card, "open listing" click-outs. ## Features (v1) Per listing: family tier (from the hunt rubric), displacement (cc), price, year, km, km/year, dealer vs private, place/region, keyword flags from title + description (MVI/inspection, whitewalls, spokes, windshield, saddlebags, ABS, bobber/custom), color words (red is known lore), title tokens (make/model). Numeric features normalized; missing values neutral. - Single-source v1 makes source/region features near-constant — kept in the schema for v2. ## Stage 1 — Cold start Order by the hunt's existing rubric score (family tiers + MVI / whitewalls / windshield / spokes / ABS bonuses + price fit). This is already explainable (reason strings exist in the hunt system). Deck = rubric order until ≥ 20 swipes. ## Stage 1.5 — Cheap swipe boost (v1.x, post-launch) Before any model exists, the importer can nudge `score` with plain swarm signals — e.g. +N when recent likes cluster in a family, small time-decay otherwise — recomputed each import, zero ML, fully explainable. Ships as v1.x if Glenn wants it; v1 itself stays pure rubric. ## Stage 2 — P3 taste engine (frozen 2026-11-10) Deterministic, explainable, pure-Python. Learning reads `swipe_events` only. **Event weights.** action: like +1.0 · maybe +0.4 · nope −1.0; round multiplier: round 1 ×1, round ≥2 **×2** (second strikes hit twice as hard — reaffirmations and flips both); recency: ×0.5^(age_days / 21). Event weight = product. **Feature weights.** Feature set per listing: family · price band (`<3k, 3–5k, 5–8k, 8–12k, 12k+`) · displacement band (from title/desc) · year band · km band (when present) · each extras flag · seller type · silhouette keywords (bobber/custom/classic/cruiser) · color words · region. For each (feature, value): `raw = Σ event weights`; `shrunk = raw × n/(n+3)` — no single strike taboos a family. **Score.** `taste_raw` = Σ shrunk weights over the listing's values; `taste_norm` = percentile rank among active listings (0–100). Blend: `w = min(0.85, N/60)` where N = total events; `points = clip( round( (1−w)·rubric + w·taste_norm + agent_adj ), 0, 100 )`. `agent_adj` = Σ profile adjustments (clamped ±10 per listing). Breakdown for the dossier: rubric share · top 3 ± taste features · agent lines — all source-labeled. **Section recipe.** Every batch targets 25 cards = fresh first (up to 23 slots) + ≥2 second looks whenever eligible stock exists + the 2 wildcard slots (wildcards draw from the fresh pool). When fresh < the remainder, second looks fill up to 25 (family cap relaxes to 8 for fill returns only); leftover fresh rolls to the next batch; fresh/return shortfalls are never padded — a drained pool yields a short (or empty) section. Constraints: ≤4 per family per section for fresh + reserved returns; ≥3 price bands when the pool allows. **Wildcards.** Candidates: active, unseen, not in the last two sections. Exclusions: families with ≥3 cumulative nopes and net-negative weight. Prefer: different family AND different price band than the section's top new items. Score = `0.5·points + 0.5·novelty` (novelty = distance from the liked-profile centroid). Picks: 2 per section. **Returns ("second looks").** Pool + caps per DATA-MODEL. Order: maybe > like > surprising nope (points ≥55 or net-positive family); rank within role by points DESC. A re-nope (round≥2 nope) rests the item permanently in v1. **Guardrails.** Never hide (List untouched; ranking only reorders). The ramp keeps cold start on rubric; the engine must run with zero profile (w=0). Agent offline → compose on the last profile (source-labeled). **No LLM inside any request path or computed score** — the pass only writes the profile. ## Explainability Each card can show up to 2 chips: top positive contributions for that bike ("low km for the money", "Vulcan family", "red", "MVI"). Chips derive from the live feature contributions; during cold start they come from the rubric's existing reasons. ## Evaluation - Track: swipes/session, like-rate trend, maybe-rate, undo rate, and "liked-item rank" (how deep liked items sat — should trend shallow). - P3 adds: hit rate (likes/decided · likes+maybes), returns re-strike rate, wildcard hit rate, flip count — surfaced in the Trends tab. - Notifications (decided 2026-11-10): in-app "new since last visit" badge only — no Conduit pushes. A weekly digest remains possible later if asked for. ## Open decisions - Maybe-handling — **resolved 2026-11-10 (Glenn):** weak positive on the direction ("right lane, wrong trim"); maybes replenish via second looks + variant hunts. - Distance feature (map-based proximity) — likely v2.