- src/lib/random.js: Fisher–Yates shuffle/sample/pickInt; replace every
biased .sort(() => 0.5 - Math.random()) site in testService.
- testService: debias correctIndex via prompt + runtime re-roll (up to 2x
when one position holds >50%); quality gate rejecting <4 distinct
options, banned filler ("all of the above" etc) and explanations
shorter than 20 chars; dedup new questions against the existing bank
via normalised question text.
- Quiz schema/tool/prompt require difficulty ('easy'|'medium'|'hard');
db.getQuizBank defaults legacy records to 'medium' on read.
- learningService.generateCustomTopic: kebab-case slug ID from the
polished label with collision suffixes; default learning_relevance
'standard' when the model omits it.
- Tests for random helpers, dedup/quality-gate behaviour and the
extended quiz schema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30 lines
846 B
JavaScript
30 lines
846 B
JavaScript
/**
|
|
* Shared randomness helpers.
|
|
*
|
|
* `Array.prototype.sort(() => 0.5 - Math.random())` is biased — modern V8
|
|
* sorts use Timsort, which compares each element more than once and skews
|
|
* the resulting permutation. Use `shuffle` for anything user-visible
|
|
* (quiz options, review topic selection, leaderboards).
|
|
*/
|
|
|
|
export function shuffle(arr) {
|
|
const out = [...arr];
|
|
for (let i = out.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[out[i], out[j]] = [out[j], out[i]];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function sample(arr, n) {
|
|
if (n <= 0) return [];
|
|
if (n >= arr.length) return shuffle(arr);
|
|
return shuffle(arr).slice(0, n);
|
|
}
|
|
|
|
export function pickInt(min, maxInclusive) {
|
|
const lo = Math.ceil(min);
|
|
const hi = Math.floor(maxInclusive);
|
|
return lo + Math.floor(Math.random() * (hi - lo + 1));
|
|
}
|