feat: phase 4 of AI pipeline hardening — quiz & content quality
- 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>
This commit is contained in:
@@ -154,6 +154,28 @@ export async function deleteCachedContent(topicId) {
|
||||
return db.deleteContent(topicId);
|
||||
}
|
||||
|
||||
function slugify(label) {
|
||||
const base = String(label || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFKD')
|
||||
.replace(/\p{Diacritic}/gu, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
return base || 'topic';
|
||||
}
|
||||
|
||||
async function pickUniqueTopicId(label) {
|
||||
const existing = await db.getTopics();
|
||||
const used = new Set(existing.map((t) => t.id));
|
||||
const base = slugify(label);
|
||||
if (!used.has(base)) return base;
|
||||
for (let i = 2; i < 1000; i++) {
|
||||
const candidate = `${base}-${i}`;
|
||||
if (!used.has(candidate)) return candidate;
|
||||
}
|
||||
return `${base}-${Date.now().toString(36)}`;
|
||||
}
|
||||
|
||||
export async function generateCustomTopic(label) {
|
||||
const result = await callLLM({
|
||||
task: 'topic.custom',
|
||||
@@ -168,7 +190,12 @@ export async function generateCustomTopic(label) {
|
||||
const emitted = result.toolUses[0]?.input;
|
||||
if (!emitted) throw new Error('Could not process custom topic. Please try again.');
|
||||
|
||||
const newTopic = { ...emitted, id: 'custom_' + Date.now().toString(36) };
|
||||
const id = await pickUniqueTopicId(emitted.label);
|
||||
const newTopic = {
|
||||
...emitted,
|
||||
id,
|
||||
learning_relevance: emitted.learning_relevance || 'standard',
|
||||
};
|
||||
await db.upsertTopic(newTopic);
|
||||
return newTopic;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user