Phase 6: take a test (weighted sampling + question flow)
- internal/sampling: ComputeWeight (Laplace-smoothed error rate + recency
multiplier, floor 0.15) and SelectWeighted (A-Res reservoir algorithm).
10k-run statistical test verifies weak questions appear >3x more often
than mastered, and mastered questions still appear (floor exercised).
- GET/POST /test/new: source filter with live available-count JS update,
n-questions input, weighted vs uniform mode radio.
- GET /test/{id}/q/{n}: deterministic answer shuffle per (test_id,
question_id), progress bar, mobile-friendly large tap targets.
- POST /test/{id}/q/{n}: records answer + upserts stat; advances to next
question or finishes test and redirects to results stub.
- GET /test/{id}/results: stub (Phase 7 will add full review).
- Ownership enforced: all test routes 404 for wrong user.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package sampling
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Candidate is a question ID paired with its sampling weight.
|
||||
type Candidate struct {
|
||||
ID string
|
||||
Weight float64
|
||||
}
|
||||
|
||||
// SelectWeighted picks n distinct candidates using the A-Res weighted
|
||||
// reservoir algorithm (Efraimidis–Spirakis). Each item's selection
|
||||
// probability is proportional to its weight. O(m log m) time.
|
||||
func SelectWeighted(candidates []Candidate, n int, rng *rand.Rand) []Candidate {
|
||||
if n >= len(candidates) {
|
||||
out := make([]Candidate, len(candidates))
|
||||
copy(out, candidates)
|
||||
return out
|
||||
}
|
||||
|
||||
type keyed struct {
|
||||
c Candidate
|
||||
key float64
|
||||
}
|
||||
|
||||
keys := make([]keyed, len(candidates))
|
||||
for i, c := range candidates {
|
||||
u := rng.Float64()
|
||||
if u == 0 {
|
||||
u = 1e-12 // avoid log(0) / pow weirdness
|
||||
}
|
||||
keys[i] = keyed{c, math.Pow(u, 1.0/c.Weight)}
|
||||
}
|
||||
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i].key > keys[j].key })
|
||||
|
||||
out := make([]Candidate, n)
|
||||
for i := range out {
|
||||
out[i] = keys[i].c
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user