Files
arnaudne 4e38b5a791 feat: initial commit — Curio mastery tutor
Full Next.js App Router application with:
- AI-graded lesson checkpoints (BKT/Elo mastery tracking)
- Auth.js v5: credentials, Google, GitHub, generic OIDC
- Anonymous-session-first with migrate-on-signin
- Admin panel: users, blueprints, reports, site settings
- Password reset + email verification (nodemailer/SMTP)
- Site config: require_auth + signups_enabled flags
- server-only guards on all DB/generation/verification modules
- PostgreSQL 16 + pgvector, Redis cache, Drizzle ORM
- 271 unit tests (Vitest), golden-eval harness, Playwright e2e stubs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:25:43 +02:00

177 lines
7.8 KiB
TypeScript

/**
* Frozen golden eval cases for content verification (T1 + T2 cascade).
*
* Each case provides a synthetic segment + checkpoint + source chunks.
* The verifier should catch bad content (unsupported claims, wrong reference answers)
* and pass good content (well-grounded, correct reference answer).
*
* expectedT1Verdict: what T1 (cheap model) should return.
* isBadContent: true → verifier MUST return fail (false-pass = catastrophic).
* isGoodContent: true → verifier should return pass or uncertain (false-fail = trust erosion).
*/
export interface ContentCase {
id: string;
label: string;
segmentText: string;
sourceChunks: Array<{ docRef: string; text: string }>;
checkpointPrompt: string;
/** Only provided to T2, not T1. */
referenceAnswer: string;
rubric: Array<{ id: string; criterion: string; weight: number }>;
expectedT1Verdict: 'pass' | 'fail' | 'uncertain';
isBadContent: boolean;
isGoodContent: boolean;
}
const CLOSURE_CHUNK = {
docRef: 'js-closures/definition',
text: `A closure is a function that retains access to its lexical scope even after the outer function has returned. When a function is created, it captures a reference to its surrounding scope. This means variables defined in the outer function remain accessible to the inner function — they are not garbage collected as long as the inner function exists.
Example:
\`\`\`js
function makeGreeter(name) {
return function() {
return 'Hello, ' + name;
};
}
const greetAlice = makeGreeter('Alice');
greetAlice(); // 'Hello, Alice'
\`\`\`
In this example, the inner function closes over the \`name\` variable. Even after \`makeGreeter\` returns, the inner function can still access \`name\`.`,
};
const LOOP_CHUNK = {
docRef: 'js-closures/loop-bug',
text: `A common pitfall: closures in loops with \`var\`.
\`\`\`js
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs: 3, 3, 3
\`\`\`
Because \`var\` is function-scoped (not block-scoped), all three arrow functions share the same \`i\` variable. When the callbacks run, the loop has finished and \`i\` is 3.
Fix: use \`let\`, which creates a new binding per iteration:
\`\`\`js
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs: 0, 1, 2
\`\`\``,
};
export const CONTENT_CASES: ContentCase[] = [
// ── GOOD CONTENT ────────────────────────────────────────────────────────────
{
id: 'content-good-001',
label: 'well-grounded segment — should pass T1',
segmentText: `A closure is a function that keeps a reference to the variables in its outer scope, even after the outer function has finished running. This is possible because JavaScript functions retain a reference to the scope in which they were created — their "lexical environment."
When you call \`makeGreeter('Alice')\`, the returned function closes over the \`name\` variable. The outer function's stack frame is gone, but \`name\` lives on because the inner function holds a reference to it.
\`\`\`js
function makeGreeter(name) {
return function() {
return 'Hello, ' + name;
};
}
const greetAlice = makeGreeter('Alice');
greetAlice(); // 'Hello, Alice'
\`\`\`
This pattern is the foundation of data privacy in JavaScript: variables declared in the outer function are inaccessible from outside, reachable only through the returned function.`,
sourceChunks: [CLOSURE_CHUNK],
checkpointPrompt: 'In your own words, explain what happens to the `name` variable after `makeGreeter` returns.',
referenceAnswer: 'The name variable is retained in memory because the returned inner function closes over it. Even though makeGreeter has returned, name is not garbage-collected — the inner function holds a reference to the lexical scope where name was defined.',
rubric: [
{ id: 'r1', criterion: 'Explains that name persists because the inner function holds a reference', weight: 0.6 },
{ id: 'r2', criterion: 'Notes that makeGreeter has returned but name survives', weight: 0.4 },
],
expectedT1Verdict: 'pass',
isBadContent: false,
isGoodContent: true,
},
{
id: 'content-good-002',
label: 'loop bug explanation — well-grounded, should pass T1',
segmentText: `The classic closure-in-loop bug catches many developers by surprise. When you use \`var\` in a for loop and create functions inside the loop body, all those functions share a single \`i\` variable — because \`var\` is function-scoped, not block-scoped.
\`\`\`js
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3
\`\`\`
By the time the callbacks fire, the loop has run to completion and \`i\` equals 3. The fix is straightforward: replace \`var\` with \`let\`. Because \`let\` is block-scoped, each iteration gets its own separate \`i\` binding.`,
sourceChunks: [LOOP_CHUNK],
checkpointPrompt: 'Why does the loop log "3" three times, and how do you fix it?',
referenceAnswer: 'var is function-scoped so all callbacks share the same i variable. By the time they run, i is 3. Fix: replace var with let to get a new i binding per iteration.',
rubric: [
{ id: 'r1', criterion: 'Identifies var scoping as the root cause', weight: 0.5 },
{ id: 'r2', criterion: 'Explains that all callbacks see the final value of i', weight: 0.3 },
{ id: 'r3', criterion: 'Proposes let as the fix', weight: 0.2 },
],
expectedT1Verdict: 'pass',
isBadContent: false,
isGoodContent: true,
},
// ── BAD CONTENT ─────────────────────────────────────────────────────────────
{
id: 'content-bad-001',
label: 'unsupported claim — closures copy values (not references)',
segmentText: `A closure captures a copy of the variables in its outer scope at the moment the inner function is created. This means the inner function has a snapshot of the values, not a live reference to them.
\`\`\`js
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
getValue: () => count,
};
}
\`\`\`
When the returned methods run, they use the snapshot copy of \`count\` that was captured at creation time.`,
sourceChunks: [CLOSURE_CHUNK],
checkpointPrompt: 'Does a closure capture a copy of variables or a live reference?',
referenceAnswer: 'A closure captures a live reference to the variable, not a copy. Changes to the variable are reflected when the inner function reads it.',
rubric: [
{ id: 'r1', criterion: 'States it is a live reference, not a copy', weight: 1.0 },
],
expectedT1Verdict: 'fail',
isBadContent: true,
isGoodContent: false,
},
{
id: 'content-bad-002',
label: 'wrong reference answer — claims var is block-scoped',
segmentText: `The loop bug occurs because JavaScript's \`var\` is block-scoped, so each iteration of the loop has its own \`i\` variable. However, the asynchronous nature of \`setTimeout\` means the callbacks run after the loop completes, reading the final value of \`i\`.`,
sourceChunks: [LOOP_CHUNK],
checkpointPrompt: 'What is the scoping behavior of var in a for loop?',
referenceAnswer: 'var is block-scoped in for loops, so each iteration creates its own i binding.',
rubric: [
{ id: 'r1', criterion: 'Correctly identifies var scoping (block vs function)', weight: 1.0 },
],
expectedT1Verdict: 'fail',
isBadContent: true,
isGoodContent: false,
},
];
export const CONTENT_VERIFY_THRESHOLDS = {
/** Max fraction of bad-content cases that T1 passes (false-pass rate). */
maxFalsePassRate: 0.15,
/** Max fraction of good-content cases that T1 fails (false-fail rate). */
maxFalseFailRate: 0.15,
};