4e38b5a791
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>
186 lines
6.2 KiB
TypeScript
186 lines
6.2 KiB
TypeScript
/**
|
|
* Seed script: JS Closures corpus + blueprint structure.
|
|
*
|
|
* Idempotent: exits early if source_chunks for this topic already exist.
|
|
* Use --force to re-seed (drops + re-inserts).
|
|
*
|
|
* Usage:
|
|
* pnpm seed # idempotent
|
|
* pnpm seed -- --force # drop and re-seed
|
|
*/
|
|
|
|
import 'dotenv/config';
|
|
import { like, eq } from 'drizzle-orm';
|
|
import { db, pool } from '../src/lib/db/index.js';
|
|
import {
|
|
sourceChunks,
|
|
blueprints,
|
|
concepts,
|
|
misconceptions,
|
|
lessons,
|
|
segments,
|
|
checkpoints,
|
|
} from '../src/lib/db/schema.js';
|
|
import { embedTexts } from '../src/lib/generation/embed.js';
|
|
import {
|
|
JS_CLOSURES_CHUNKS,
|
|
JS_CLOSURES_BLUEPRINT,
|
|
JS_CLOSURES_CONCEPTS,
|
|
JS_CLOSURES_MISCONCEPTIONS,
|
|
JS_CLOSURES_SEGMENTS,
|
|
} from '../src/lib/db/seed/js-closures.js';
|
|
|
|
const FORCE = process.argv.includes('--force');
|
|
|
|
async function seed() {
|
|
console.log('Seeding JS Closures corpus...');
|
|
|
|
// Idempotency check
|
|
if (!FORCE) {
|
|
const existing = await db
|
|
.select({ id: sourceChunks.id })
|
|
.from(sourceChunks)
|
|
.where(like(sourceChunks.docRef, 'js-closures/%'))
|
|
.limit(1);
|
|
|
|
if (existing.length > 0) {
|
|
console.log('Already seeded. Run with --force to re-seed.');
|
|
return;
|
|
}
|
|
} else {
|
|
console.log('--force: removing existing js-closures data...');
|
|
await db.delete(sourceChunks).where(like(sourceChunks.docRef, 'js-closures/%'));
|
|
|
|
const existingBlueprint = await db
|
|
.select({ id: blueprints.id })
|
|
.from(blueprints)
|
|
.where(eq(blueprints.intentKey, JS_CLOSURES_BLUEPRINT.intentKey))
|
|
.limit(1);
|
|
|
|
if (existingBlueprint.length > 0) {
|
|
const bpId = existingBlueprint[0].id;
|
|
|
|
// Delete bottom-up to respect FK constraints:
|
|
// checkpoints → segments → lessons, misconceptions → concepts → blueprint
|
|
const lessonRows = await db.select({ id: lessons.id }).from(lessons).where(eq(lessons.blueprintId, bpId));
|
|
for (const lesson of lessonRows) {
|
|
const segRows = await db.select({ id: segments.id }).from(segments).where(eq(segments.lessonId, lesson.id));
|
|
for (const seg of segRows) {
|
|
await db.delete(checkpoints).where(eq(checkpoints.segmentId, seg.id));
|
|
}
|
|
await db.delete(segments).where(eq(segments.lessonId, lesson.id));
|
|
}
|
|
await db.delete(lessons).where(eq(lessons.blueprintId, bpId));
|
|
|
|
const conceptRows = await db.select({ id: concepts.id }).from(concepts).where(eq(concepts.blueprintId, bpId));
|
|
for (const concept of conceptRows) {
|
|
await db.delete(misconceptions).where(eq(misconceptions.conceptId, concept.id));
|
|
}
|
|
await db.delete(concepts).where(eq(concepts.blueprintId, bpId));
|
|
await db.delete(blueprints).where(eq(blueprints.id, bpId));
|
|
}
|
|
}
|
|
|
|
// 1. Embed all chunks + blueprint intent text in one batch
|
|
// Lowercase matches normalizeIntent's query normalization (input.toLowerCase())
|
|
const intentText = JS_CLOSURES_BLUEPRINT.title.toLowerCase(); // "javascript closures"
|
|
const allTexts = [intentText, ...JS_CLOSURES_CHUNKS.map((c) => c.text)];
|
|
console.log(`Embedding ${allTexts.length} texts (1 blueprint + ${JS_CLOSURES_CHUNKS.length} chunks)...`);
|
|
const [blueprintEmbedding, ...chunkEmbeddings] = await embedTexts(allTexts);
|
|
console.log('Embeddings done.');
|
|
|
|
// 2. Insert source_chunks
|
|
const insertedChunks = await db
|
|
.insert(sourceChunks)
|
|
.values(
|
|
JS_CLOSURES_CHUNKS.map((chunk, i) => ({
|
|
docRef: chunk.docRef,
|
|
text: chunk.text,
|
|
embedding: chunkEmbeddings[i],
|
|
})),
|
|
)
|
|
.returning({ id: sourceChunks.id, docRef: sourceChunks.docRef });
|
|
|
|
console.log(`Inserted ${insertedChunks.length} source chunks.`);
|
|
|
|
// 3. Insert blueprint (with embedding so normalizeIntent similarity search finds it)
|
|
const [blueprint] = await db
|
|
.insert(blueprints)
|
|
.values({ ...JS_CLOSURES_BLUEPRINT, embedding: blueprintEmbedding, status: 'published' })
|
|
.returning({ id: blueprints.id });
|
|
|
|
console.log(`Inserted blueprint: ${blueprint.id}`);
|
|
|
|
// 4. Insert concepts
|
|
const insertedConcepts = await db
|
|
.insert(concepts)
|
|
.values(JS_CLOSURES_CONCEPTS.map((c) => ({ ...c, blueprintId: blueprint.id })))
|
|
.returning({ id: concepts.id, name: concepts.name });
|
|
|
|
console.log(`Inserted ${insertedConcepts.length} concepts.`);
|
|
|
|
// 5. Insert misconceptions (keyed by concept name)
|
|
const conceptByName = Object.fromEntries(insertedConcepts.map((c) => [c.name, c.id]));
|
|
|
|
const misconceptionRows = JS_CLOSURES_MISCONCEPTIONS.flatMap((m) => {
|
|
const conceptId = conceptByName[m.conceptName];
|
|
if (!conceptId) {
|
|
console.warn(`No concept found for misconception "${m.conceptName}" — skipping.`);
|
|
return [];
|
|
}
|
|
return [{ conceptId, tag: m.tag, signature: m.signature, verifyStatus: 'pass' as const }];
|
|
});
|
|
|
|
if (misconceptionRows.length > 0) {
|
|
await db.insert(misconceptions).values(misconceptionRows);
|
|
console.log(`Inserted ${misconceptionRows.length} misconceptions.`);
|
|
}
|
|
|
|
// 6. Insert pre-built lesson (so app works without running generation workers)
|
|
const chunkIdByDocRef = Object.fromEntries(insertedChunks.map((c) => [c.docRef, c.id]));
|
|
|
|
const [lesson] = await db
|
|
.insert(lessons)
|
|
.values({ blueprintId: blueprint.id, ord: 0, estMinutes: 10 })
|
|
.returning({ id: lessons.id });
|
|
|
|
console.log(`Inserted lesson: ${lesson.id}`);
|
|
|
|
for (const seg of JS_CLOSURES_SEGMENTS) {
|
|
const sourceChunkIds = seg.sourceDocRefs
|
|
.map((ref) => chunkIdByDocRef[ref])
|
|
.filter((id): id is string => !!id);
|
|
|
|
const [insertedSegment] = await db
|
|
.insert(segments)
|
|
.values({
|
|
lessonId: lesson.id,
|
|
ord: seg.ord,
|
|
bodyJson: { text: seg.body },
|
|
sourceChunkIds,
|
|
verifyStatus: 'pass',
|
|
})
|
|
.returning({ id: segments.id });
|
|
|
|
await db.insert(checkpoints).values({
|
|
segmentId: insertedSegment.id,
|
|
prompt: seg.checkpoint.prompt,
|
|
kind: seg.checkpoint.kind,
|
|
referenceAnswer: seg.checkpoint.referenceAnswer,
|
|
rubricJson: seg.checkpoint.rubric,
|
|
verifyStatus: 'pass',
|
|
});
|
|
|
|
console.log(` Segment ${seg.ord}: ${seg.sourceDocRefs.join(', ')}`);
|
|
}
|
|
|
|
console.log('Seed complete.');
|
|
}
|
|
|
|
seed()
|
|
.catch((err) => {
|
|
console.error('Seed failed:', err);
|
|
process.exit(1);
|
|
})
|
|
.finally(() => pool.end());
|