Files
Sauci/docs/question-selection.md
2025-12-29 23:10:12 +00:00

155 lines
6.3 KiB
Markdown

# Question Selection Algorithm
This document explains how questions are selected and ordered for users in the swipe interface.
## Overview
Question selection happens in two stages:
1. **Database Layer** - `get_recommended_questions()` PostgreSQL function filters available questions
2. **Client Layer** - `swipe.tsx` applies additional filtering and sorting with bias
## Stage 1: Database Filtering
The `get_recommended_questions(target_pack_id)` function (`apps/supabase/migrations/20251227172500_role_specific_questions.sql`) returns questions the user hasn't answered yet.
### Pack Selection Logic
```
IF target_pack_id provided:
→ Use only that specific pack
ELSE IF couple has any couple_packs entries:
→ Use only packs where enabled = true
ELSE (new couple, no preferences set):
→ Use all public packs (is_public = true)
```
### Question Filtering
For each question in active packs:
- **Excluded**: Questions the current user has already answered
- **Included**: All unanswered questions from active packs
### Returned Fields
| Field | Description |
|-------|-------------|
| `id` | Question UUID |
| `text` | Display text (see two-part logic below) |
| `partner_text` | Original partner text (for reference) |
| `is_two_part` | `true` if question has different text for second responder |
| `pack_id` | Parent pack UUID |
| `intensity` | 1-5 rating |
| `partner_answered` | `true` if partner has already responded |
### Two-Part Questions
Some questions have role-specific text (`partner_text` column). The function handles this:
```
IF partner_text EXISTS AND partner has answered:
→ Return partner_text as display text
ELSE:
→ Return original text
```
This enables questions like:
- First person sees: "Would you like your partner to wake you up with oral?"
- Second person sees: "Would you wake your partner up with oral?"
## Stage 2: Client Sorting & Filtering
Located in `apps/mobile/app/(app)/swipe.tsx`, lines 57-95.
### Skip Filtering
Before sorting, questions are filtered against locally-skipped questions:
```typescript
const filtered = data.filter(q => !skippedIds.has(q.id));
```
**Skip behavior** (`src/lib/skippedQuestions.ts`):
- Swipe down = skip question
- Skipped questions hidden for **24 hours**
- Stored in SecureStore (native) or localStorage (web)
- No server-side record - purely client-side convenience
### Sorting Algorithm with Bias
Questions are sorted by a computed score:
```typescript
const sorted = filtered.sort((a, b) => {
let scoreA = Math.random(); // Base: 0.0 - 1.0
let scoreB = Math.random();
// Apply bias bonuses
if (a.partner_answered && a.is_two_part) scoreA += 1.5;
else if (a.partner_answered) scoreA += 0.7;
else if (a.is_two_part) scoreA += 0.4;
// Same for scoreB...
return scoreB - scoreA; // Descending order
});
```
### Priority Tiers
| Priority | Condition | Score Range | Rationale |
|----------|-----------|-------------|-----------|
| **1st** | `partner_answered` + `is_two_part` | 1.5 - 2.5 | Completes two-part question, immediate match potential |
| **2nd** | `partner_answered` only | 0.7 - 1.7 | High match potential, partner waiting |
| **3rd** | `is_two_part` only | 0.4 - 1.4 | Initiates two-part sequence |
| **4th** | Neither | 0.0 - 1.0 | Standard questions, fully randomized |
### Why This Bias?
1. **Engagement**: Questions partner already answered can immediately create matches, providing instant gratification
2. **Two-part completion**: When partner answered a two-part question first, completing it quickly maintains context and creates better matches
3. **Two-part initiation**: Slightly prioritizes two-part questions to create more engaging interactions
4. **Randomization within tiers**: The random base prevents predictable ordering while maintaining priority bands
## Visual Flow
```
┌─────────────────────────────────────────────────────────────┐
│ get_recommended_questions() │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Pack Filter │ → │ User Answered │ → │ Two-Part Text │ │
│ │ │ │ Exclusion │ │ Resolution │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Client (swipe.tsx) │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Skip Filter │ → │ Bias Scoring │ → │ Sort │ │
│ │ (24 hour) │ │ │ │ (Descending) │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
Questions shown to user
```
## Edge Cases
| Scenario | Behavior |
|----------|----------|
| No packs enabled | Shows "No packs enabled" prompt |
| All questions answered | Shows "All caught up!" with refresh button |
| No partner paired | Shows pairing prompt |
| Pack disabled mid-session | Client filters out questions from disabled packs |
## Configuration
| Setting | Value | Location |
|---------|-------|----------|
| Skip duration | 24 hours | `src/lib/skippedQuestions.ts:6` |
| Two-part + answered bias | +1.5 | `swipe.tsx:78` |
| Answered-only bias | +0.7 | `swipe.tsx:79` |
| Two-part-only bias | +0.4 | `swipe.tsx:80` |