← back to karthikjanagiraman__WritingCoach

Function bodies 269 total

All specs Real LLM only Function bodies
login function · typescript · L6-L17 (12 LOC)
e2e/helpers.ts
export async function login(page: Page) {
  await page.goto("/auth/login");
  await page.fill('input[id="email"]', "[email protected]");
  await page.fill('input[id="password"]', "password123");

  // Use Promise.all to avoid race between click and navigation
  await Promise.all([
    page.waitForURL("**/dashboard", { timeout: 30_000 }),
    page.click('button[type="submit"]'),
  ]);
  await expect(page.getByText("Welcome back")).toBeVisible({ timeout: 15_000 });
}
loginAndSelectMaya function · typescript · L22-L47 (26 LOC)
e2e/helpers.ts
export async function loginAndSelectMaya(page: Page) {
  await login(page);

  // Wait for children data to fully load (async streak/badge fetches cause card instability)
  await page.waitForTimeout(1000);

  // Click Maya's card — sets activeChild in React context + localStorage
  const mayaCard = page.locator("div.cursor-pointer", { hasText: "Maya" }).first();
  await mayaCard.click({ force: true });

  // Wait for student dashboard (child view lives at /home; first load may need compilation)
  await page.waitForURL("**/home", { timeout: 30_000 });
  await page.waitForFunction(
    () => {
      const text = document.body.innerText;
      return (
        text.includes("Up Next") ||
        text.includes("Continue lesson") ||
        text.includes("This Week") ||
        text.includes("Lessons done") ||
        text.includes("My Writing")
      );
    },
    { timeout: 20_000 }
  );
}
navigateToLesson function · typescript · L52-L76 (25 LOC)
e2e/helpers.ts
export async function navigateToLesson(page: Page, lessonId: string) {
  const lessonLink = page.locator(`a[href="/lesson/${lessonId}"]`);

  if (await lessonLink.first().isVisible({ timeout: 3_000 }).catch(() => false)) {
    await lessonLink.first().click();
  } else {
    // Fall back to primary CTA
    const primaryCta = page.locator('a[href^="/lesson/"]').first();
    await primaryCta.click();
  }

  // Wait for lesson page URL first, then lesson-specific content
  await page.waitForURL("**/lesson/**", { timeout: 30_000 });
  await page.waitForFunction(
    () => {
      const text = document.body.innerText;
      return (
        /Step \d of 3/.test(text) ||
        text.includes("Start writing") ||
        text.includes("Time to write")
      );
    },
    { timeout: 60_000 }
  );
}
navigateToReport function · typescript · L81-L87 (7 LOC)
e2e/helpers.ts
export async function navigateToReport(page: Page, childId: string) {
  await page.goto(`/dashboard/children/${childId}/report`);
  await page.waitForFunction(
    () => document.body.innerText.includes("Progress Report"),
    { timeout: 15_000 }
  );
}
waitForAIResponse function · typescript · L92-L105 (14 LOC)
e2e/helpers.ts
export async function waitForAIResponse(page: Page) {
  const typing = page.locator('[data-testid="typing-indicator"]');

  try {
    await expect(typing).toBeVisible({ timeout: 15_000 });
  } catch {
    // Typing indicator may have already disappeared
    await page.waitForTimeout(1000);
    return;
  }

  await expect(typing).not.toBeVisible({ timeout: 90_000 });
  await page.waitForTimeout(500);
}
interactWithStep function · typescript · L111-L174 (64 LOC)
e2e/helpers.ts
export async function interactWithStep(page: Page) {
  await page.waitForTimeout(500);

  const quickAnswer = page.getByPlaceholder("Type your answer...");
  const continueBtn = page.getByRole("button", { name: "Continue" });

  if (await quickAnswer.isVisible().catch(() => false)) {
    // Text input is visible — type an answer and submit
    const answers = [
      "First, the cat climbed the tall tree. Next, it saw a beautiful bird!",
      "I think the answer is the first one because it makes the most sense.",
      "The character felt happy because they solved the problem by being brave.",
      "First, I would introduce the character. Then, I would describe the setting.",
      "I think it helps the reader understand what is happening in the story.",
    ];
    const randomAnswer = answers[Math.floor(Math.random() * answers.length)];
    await quickAnswer.fill(randomAnswer);

    const doneBtn = page.getByRole("button", { name: /done/i });
    if (await doneBtn.isVisible().catch(
getCurrentStep function · typescript · L179-L187 (9 LOC)
e2e/helpers.ts
export async function getCurrentStep(page: Page): Promise<number> {
  for (let step = 3; step >= 1; step--) {
    const stepText = page.getByText(`Step ${step} of 3`);
    if (await stepText.isVisible().catch(() => false)) {
      return step;
    }
  }
  return 1;
}
All rows scored by the Repobility analyzer (https://repobility.com)
main function · typescript · L9-L74 (66 LOC)
prisma/migrate-assessments.ts
async function main() {
  const assessments = await prisma.assessment.findMany();
  console.log(`Found ${assessments.length} assessments to migrate`);

  let migrated = 0;
  let skipped = 0;

  for (const assessment of assessments) {
    // Check if already migrated (matching sessionId + submissionText)
    const existing = await prisma.writingSubmission.findFirst({
      where: {
        sessionId: assessment.sessionId,
        submissionText: assessment.submissionText,
      },
    });

    if (existing) {
      skipped++;
      continue;
    }

    // Parse feedback JSON
    let strength = "";
    let growthArea = "";
    let encouragement = "";
    try {
      const feedback = JSON.parse(assessment.feedback);
      strength = feedback.strength || "";
      growthArea = feedback.growth || feedback.growthArea || "";
      encouragement = feedback.encouragement || "";
    } catch {
      console.warn(`  Could not parse feedback for assessment ${assessment.id}`);
    }

    // Calculat
seedBase function · typescript · L15-L60 (46 LOC)
prisma/seed-e2e.ts
async function seedBase() {
  console.log("── Base seed ──");

  const parent = await prisma.user.upsert({
    where: { email: "[email protected]" },
    update: {},
    create: {
      id: "parent-001",
      email: "[email protected]",
      passwordHash: await bcryptjs.hash("password123", 12),
      name: "Demo Parent",
      role: "PARENT",
    },
  });
  console.log(`  Parent: ${parent.name} (${parent.id})`);

  const maya = await prisma.childProfile.upsert({
    where: { id: "child-maya-001" },
    update: {},
    create: {
      id: "child-maya-001",
      parentId: parent.id,
      name: "Maya",
      age: 8,
      tier: 1,
      avatarEmoji: "\u{1F989}",
    },
  });
  console.log(`  Child: ${maya.name} (${maya.id})`);

  const ethan = await prisma.childProfile.upsert({
    where: { id: "child-ethan-001" },
    update: {},
    create: {
      id: "child-ethan-001",
      parentId: parent.id,
      name: "Ethan",
      age: 11,
      tier: 2,
      avatarEmoji: "\u{1F98A}",
 
seedLessonProgress function · typescript · L62-L108 (47 LOC)
prisma/seed-e2e.ts
async function seedLessonProgress(childId: string) {
  console.log("── Lesson progress ──");

  // N1.1.1 — completed, high score
  await prisma.lessonProgress.upsert({
    where: { childId_lessonId: { childId, lessonId: "N1.1.1" } },
    update: { status: "completed", currentPhase: "feedback" },
    create: {
      childId,
      lessonId: "N1.1.1",
      status: "completed",
      currentPhase: "feedback",
      startedAt: new Date("2026-02-01T10:00:00Z"),
      completedAt: new Date("2026-02-01T10:30:00Z"),
    },
  });
  console.log("  N1.1.1: completed (high score)");

  // N1.1.2 — needs_improvement, low score
  await prisma.lessonProgress.upsert({
    where: { childId_lessonId: { childId, lessonId: "N1.1.2" } },
    update: { status: "needs_improvement", currentPhase: "feedback" },
    create: {
      childId,
      lessonId: "N1.1.2",
      status: "needs_improvement",
      currentPhase: "feedback",
      startedAt: new Date("2026-02-03T10:00:00Z"),
      completedAt: new Date
seedAssessmentData function · typescript · L110-L285 (176 LOC)
prisma/seed-e2e.ts
async function seedAssessmentData(childId: string) {
  console.log("── Assessment data ──");

  // Clean up existing assessment-related data for idempotency
  await prisma.aIFeedback.deleteMany({
    where: { submission: { childId } },
  });
  await prisma.writingSubmission.deleteMany({ where: { childId } });
  await prisma.assessment.deleteMany({ where: { childId } });
  await prisma.session.deleteMany({ where: { childId } });

  // ── N1.1.1: Completed, HIGH score (3.2) ──

  const session1 = await prisma.session.create({
    data: {
      id: "e2e-session-n111",
      childId,
      lessonId: "N1.1.1",
      phase: "feedback",
      phaseState: JSON.stringify({
        instructionCompleted: true,
        comprehensionCheckPassed: true,
        guidedAttempts: 3,
        hintsGiven: 1,
        guidedComplete: true,
      }),
      conversationHistory: JSON.stringify([
        { role: "assistant", content: "Welcome! Today we're learning about story beginnings." },
        { role: "use
seedSkillProgress function · typescript · L287-L317 (31 LOC)
prisma/seed-e2e.ts
async function seedSkillProgress(childId: string) {
  console.log("── Skill progress ──");

  const skillData = [
    { skillCategory: "narrative", skillName: "story_structure", score: 3.0, level: "PROFICIENT" as const },
    { skillCategory: "narrative", skillName: "setting_description", score: 2.5, level: "DEVELOPING" as const },
    { skillCategory: "narrative", skillName: "voice_style", score: 3.2, level: "PROFICIENT" as const },
    { skillCategory: "narrative", skillName: "character_development", score: 2.0, level: "DEVELOPING" as const },
    { skillCategory: "narrative", skillName: "plot_pacing", score: 1.5, level: "EMERGING" as const },
  ];

  for (const skill of skillData) {
    await prisma.skillProgress.upsert({
      where: {
        childId_skillCategory_skillName: {
          childId,
          skillCategory: skill.skillCategory,
          skillName: skill.skillName,
        },
      },
      update: { score: skill.score, level: skill.level, totalAttempts: 2 },
      cr
seedStreak function · typescript · L319-L342 (24 LOC)
prisma/seed-e2e.ts
async function seedStreak(childId: string) {
  console.log("── Streak ──");

  await prisma.streak.upsert({
    where: { childId },
    update: {
      currentStreak: 2,
      longestStreak: 5,
      weeklyGoal: 3,
      weeklyCompleted: 2,
      lastActiveDate: new Date("2026-02-13T10:00:00Z"),
    },
    create: {
      childId,
      currentStreak: 2,
      longestStreak: 5,
      weeklyGoal: 3,
      weeklyCompleted: 2,
      lastActiveDate: new Date("2026-02-13T10:00:00Z"),
      weekStartDate: new Date("2026-02-10T00:00:00Z"),
    },
  });
  console.log("  Streak: 2 days current, 5 longest, 2/3 weekly");
}
seedAchievement function · typescript · L344-L358 (15 LOC)
prisma/seed-e2e.ts
async function seedAchievement(childId: string) {
  console.log("── Achievement ──");

  await prisma.achievement.upsert({
    where: { childId_badgeId: { childId, badgeId: "first_lesson" } },
    update: {},
    create: {
      childId,
      badgeId: "first_lesson",
      unlockedAt: new Date("2026-02-01T10:30:00Z"),
      seen: true,
    },
  });
  console.log("  Badge: first_lesson");
}
seedPlacementAndCurriculum function · typescript · L360-L428 (69 LOC)
prisma/seed-e2e.ts
async function seedPlacementAndCurriculum(childId: string) {
  console.log("── Placement & Curriculum ──");

  // Clean up existing
  await prisma.curriculumWeek.deleteMany({
    where: { curriculum: { childId } },
  });
  await prisma.curriculumRevision.deleteMany({
    where: { curriculum: { childId } },
  });
  await prisma.curriculum.deleteMany({ where: { childId } });
  await prisma.placementResult.deleteMany({ where: { childId } });

  await prisma.placementResult.create({
    data: {
      childId,
      prompts: JSON.stringify([
        "Write a short story about an animal who goes on an adventure.",
        "Describe your favorite place using all five senses.",
        "Convince your teacher to let the class have a pet.",
      ]),
      responses: JSON.stringify([
        "Once upon a time there was a bunny named Flop...",
        "My favorite place is grandma's kitchen...",
        "I think our class should get a hamster...",
      ]),
      aiAnalysis: JSON.stringify({
    
Same scanner, your repo: https://repobility.com — Repobility
main function · typescript · L430-L442 (13 LOC)
prisma/seed-e2e.ts
async function main() {
  console.log("=== E2E Seed Start ===\n");

  const { maya } = await seedBase();
  await seedLessonProgress(maya.id);
  await seedAssessmentData(maya.id);
  await seedSkillProgress(maya.id);
  await seedStreak(maya.id);
  await seedAchievement(maya.id);
  await seedPlacementAndCurriculum(maya.id);

  console.log("\n=== E2E Seed Complete ===");
}
loadFile function · typescript · L45-L47 (3 LOC)
scripts/run-evals.ts
function loadFile(rel: string): string {
  return fs.readFileSync(path.join(CONTENT_DIR, rel), "utf-8");
}
extractSection function · typescript · L55-L64 (10 LOC)
scripts/run-evals.ts
function extractSection(content: string, startMarker: string): string {
  const startIndex = content.indexOf(startMarker);
  if (startIndex === -1) return "";
  const afterMarker = content.slice(startIndex + startMarker.length);
  const nextHeadingMatch = afterMarker.match(/\n## /);
  const endIndex = nextHeadingMatch
    ? startIndex + startMarker.length + (nextHeadingMatch.index ?? 0)
    : content.length;
  return content.slice(startIndex, endIndex).trim();
}
buildEvalPrompt function · typescript · L117-L171 (55 LOC)
scripts/run-evals.ts
function buildEvalPrompt(evalCase: EvalCase): string {
  const ctx = evalCase.context;
  const parts: string[] = [];

  // 1. Core skill instructions
  parts.push(skillContent.core);

  // 2. Tier adaptation
  parts.push(TIER_INSERTS[ctx.student_tier] ?? "");

  // 3. Phase behavior
  parts.push(PHASE_PROMPTS[ctx.current_phase] ?? "");

  // 4. Session context — match the real buildPrompt() format from prompt-builder.ts
  let sessionContext = `## Current Session Context\n\n`;

  // Student line (matches buildPrompt)
  const ageStr = ctx.student_age
    ? ` (age ${ctx.student_age}, Tier ${ctx.student_tier})`
    : ` (Tier ${ctx.student_tier})`;
  sessionContext += `Student: ${ctx.student_name}${ageStr}\n`;

  sessionContext += `Lesson: ${ctx.lesson_id}\n`;
  sessionContext += `Current Phase: ${ctx.current_phase.toUpperCase()}\n`;

  // Learning objectives (real buildPrompt always includes these)
  sessionContext += `\nLEARNING OBJECTIVES:\n1. Learn and apply the writing techniques for t
withRetry function · typescript · L176-L190 (15 LOC)
scripts/run-evals.ts
async function withRetry<T>(fn: () => Promise<T>, retries = 3, baseDelay = 5000): Promise<T> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : String(err);
      const isRateLimit = msg.includes("429") || msg.toLowerCase().includes("rate") || msg.includes("RESOURCE_EXHAUSTED");
      if (!isRateLimit || attempt === retries) throw err;
      const delay = baseDelay * Math.pow(2, attempt);
      console.log(`      Rate limited, retrying in ${(delay / 1000).toFixed(0)}s...`);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  throw new Error("Unreachable");
}
runEval function · typescript · L213-L302 (90 LOC)
scripts/run-evals.ts
async function runEval(evalCase: EvalCase): Promise<MarkerResult> {
  const systemPrompt = buildEvalPrompt(evalCase);

  const messages: LLMMessage[] = [];

  // For step-specific evals (step > 1), inject synthetic prior turns
  // so the LLM sees that earlier steps actually happened (mimics real
  // multi-turn conversation history from buildPrompt)
  const ctx = evalCase.context;
  const currentStep = ctx.current_step ?? (ctx.phase_state?.phase1Step as number | undefined) ?? undefined;
  if (ctx.current_phase === "instruction" && currentStep && currentStep > 1) {
    for (let s = 1; s < currentStep; s++) {
      messages.push({
        role: "assistant",
        content: `[STEP: ${s}]\n(Coach taught step ${s} content for this lesson)`,
      });
      messages.push({
        role: "user",
        content: "(Student responded to step " + s + ")",
      });
    }
  }

  // If there's a prior_context, simulate it as an earlier exchange
  if (ctx.prior_context) {
    messages.push({
    
judgeResponse function · typescript · L313-L383 (71 LOC)
scripts/run-evals.ts
async function judgeResponse(
  evalCase: EvalCase,
  response: string
): Promise<JudgeResult[]> {
  const judgeSystemPrompt = `You are an eval judge for an AI writing coach for children. Given the coach's response to a student, evaluate whether each expectation is met.

For each expectation, respond with a JSON array. Each element must have:
- "index": the 1-based expectation number
- "pass": true or false
- "reason": a brief (1 sentence) explanation

Respond with ONLY valid JSON array, no markdown fences, no other text.`;

  const judgeUserContent = `CONTEXT:
- Student: ${evalCase.context.student_name} (Tier ${evalCase.context.student_tier})
- Phase: ${evalCase.context.current_phase}
- Lesson: ${evalCase.context.lesson_id}
${evalCase.context.prior_context ? `- Prior context: ${evalCase.context.prior_context}` : ""}
${evalCase.context.current_step ? `- Current step: ${evalCase.context.current_step}` : ""}

STUDENT SAID: "${evalCase.prompt}"

COACH RESPONSE:
"""
${response}
"""

WORD C
wordCount function · typescript · L388-L390 (3 LOC)
scripts/run-evals.ts
function wordCount(text: string): number {
  return text.trim().split(/\s+/).length;
}
Repobility · MCP-ready · https://repobility.com
runWithConcurrency function · typescript · L392-L412 (21 LOC)
scripts/run-evals.ts
async function runWithConcurrency<T>(
  tasks: (() => Promise<T>)[],
  concurrency: number
): Promise<T[]> {
  const results: T[] = new Array(tasks.length);
  let nextIndex = 0;

  async function worker() {
    while (nextIndex < tasks.length) {
      const index = nextIndex++;
      results[index] = await tasks[index]();
      // Delay between evals to respect rate limits
      if (nextIndex < tasks.length && concurrency <= 2) {
        await new Promise((r) => setTimeout(r, DELAY_BETWEEN_EVALS_MS));
      }
    }
  }

  await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker()));
  return results;
}
worker function · typescript · L399-L408 (10 LOC)
scripts/run-evals.ts
  async function worker() {
    while (nextIndex < tasks.length) {
      const index = nextIndex++;
      results[index] = await tasks[index]();
      // Delay between evals to respect rate limits
      if (nextIndex < tasks.length && concurrency <= 2) {
        await new Promise((r) => setTimeout(r, DELAY_BETWEEN_EVALS_MS));
      }
    }
  }
main function · typescript · L442-L757 (316 LOC)
scripts/run-evals.ts
async function main() {
  const { provider, model } = getLLMConfig();
  const judgeModel = process.env.LLM_JUDGE_MODEL || model;

  if (provider === "anthropic" && !process.env.ANTHROPIC_API_KEY) {
    console.error("Error: ANTHROPIC_API_KEY environment variable is not set.");
    process.exit(1);
  }
  if (provider === "google" && !process.env.GOOGLE_AI_API_KEY) {
    console.error("Error: GOOGLE_AI_API_KEY environment variable is not set.");
    process.exit(1);
  }
  if (provider === "groq" && !process.env.GROQ_API_KEY) {
    console.error("Error: GROQ_API_KEY environment variable is not set.");
    process.exit(1);
  }

  // Parse args
  const args = process.argv.slice(2);
  const filterIdx = args.indexOf("--id");
  const filterId = filterIdx !== -1 ? args[filterIdx + 1] : null;
  const concurrencyIdx = args.indexOf("--concurrency");
  const concurrency = concurrencyIdx !== -1 ? parseInt(args[concurrencyIdx + 1], 10) : DEFAULT_CONCURRENCY;

  let evals = evalsData.evals;
  if (filt
POST function · typescript · L5-L72 (68 LOC)
src/app/api/auth/signup/route.ts
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { name, email, password } = body;

    // Validate required fields
    if (!name || typeof name !== "string" || name.trim().length === 0) {
      return NextResponse.json(
        { error: "Name is required" },
        { status: 400 }
      );
    }

    if (!email || typeof email !== "string") {
      return NextResponse.json(
        { error: "Valid email is required" },
        { status: 400 }
      );
    }

    // Basic email format validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
      return NextResponse.json(
        { error: "Invalid email format" },
        { status: 400 }
      );
    }

    if (!password || typeof password !== "string" || password.length < 8) {
      return NextResponse.json(
        { error: "Password must be at least 8 characters" },
        { status: 400 }
      );
    }

    // Check email uniquene
GET function · typescript · L6-L60 (55 LOC)
src/app/api/children/[id]/badges/route.ts
export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const achievements = await prisma.achievement.findMany({
      where: { childId },
      orderBy: { unlockedAt: "desc" },
    });

    const badges = achievements
      .map((a) => {
        const def = getBadgeById(a.badgeId);
        if (!def) return null;
        return {
          id: def.id,
          name: def.name,
          emoji: def.emoji,
          description: def.description,
          category: def.category,
          unlockedAt: a.unlockedA
POST function · typescript · L5-L51 (47 LOC)
src/app/api/children/[id]/badges/seen/route.ts
export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const body = await request.json();
    const { badgeIds } = body;

    if (!Array.isArray(badgeIds) || badgeIds.length === 0) {
      return NextResponse.json(
        { error: "badgeIds must be a non-empty array" },
        { status: 400 }
      );
    }

    const result = await prisma.achievement.updateMany({
      where: {
        childId,
        badgeId: { in: badgeIds },
        seen: false,
      },
      data: { seen: true },
    });

    return 
GET function · typescript · L6-L66 (61 LOC)
src/app/api/children/[id]/portfolio/export/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const submissions = await prisma.writingSubmission.findMany({
      where: { childId },
      include: { feedback: true },
      orderBy: { createdAt: "asc" },
    });

    const rows = submissions.map((s) => {
      const lesson = getLessonById(s.lessonId);
      return [
        new Date(s.createdAt).toLocaleDateString(),
        lesson?.title ?? "Unknown Lesson",
        lesson?.type ?? "unknown",
        s.wordCount,
        s.feedback?.overallScore ??
GET function · typescript · L13-L119 (107 LOC)
src/app/api/children/[id]/portfolio/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const { searchParams } = request.nextUrl;
    const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10));
    const limit = Math.min(50, Math.max(1, parseInt(searchParams.get("limit") || "10", 10)));
    const type = searchParams.get("type");
    const sort = searchParams.get("sort") || "newest";
    const includeRevisions = searchParams.get("includeRevisions") === "true";

    // Build where clause
    const where: Record<string, unknown> = {
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
formatSubmission function · typescript · L121-L157 (37 LOC)
src/app/api/children/[id]/portfolio/route.ts
function formatSubmission(s: {
  id: string;
  lessonId: string;
  submissionText: string;
  wordCount: number;
  revisionNumber: number;
  createdAt: Date;
  feedback: {
    scores: string;
    overallScore: number;
    strength: string;
    growthArea: string;
    encouragement: string;
  } | null;
}) {
  const lesson = getLessonById(s.lessonId);
  return {
    id: s.id,
    lessonId: s.lessonId,
    lessonTitle: lesson?.title ?? "Unknown Lesson",
    lessonType: lesson?.type ?? "unknown",
    lessonUnit: lesson?.unit ?? "",
    submissionText: s.submissionText,
    wordCount: s.wordCount,
    revisionNumber: s.revisionNumber,
    createdAt: s.createdAt.toISOString(),
    feedback: s.feedback
      ? {
          scores: JSON.parse(s.feedback.scores) as Record<string, number>,
          overallScore: s.feedback.overallScore,
          strength: s.feedback.strength,
          growthArea: s.feedback.growthArea,
          encouragement: s.feedback.encouragement,
        }
      : null,
 
GET function · typescript · L7-L201 (195 LOC)
src/app/api/children/[id]/progress/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    // Verify child exists and belongs to this parent
    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json(
        { error: "Child not found or access denied" },
        { status: 403 }
      );
    }

    // Get all lesson progress for this child
    const progressRecords = await prisma.lessonProgress.findMany({
      where: { childId },
      orderBy: { startedAt: "desc" },
    });

    // Get recent assessments
    const assessments = await prisma.assessment.findMany({
      where: { childId },
      orderBy: { createdAt: "desc" },
      take: 10,
    });

    // Catego
escapeCsvValue function · typescript · L6-L13 (8 LOC)
src/app/api/children/[id]/report/export/route.ts
function escapeCsvValue(value: string | number | null | undefined): string {
  const str = String(value ?? "");
  // If the value contains commas, quotes, or newlines, wrap in quotes and escape internal quotes
  if (str.includes(",") || str.includes('"') || str.includes("\n")) {
    return `"${str.replace(/"/g, '""')}"`;
  }
  return str;
}
GET function · typescript · L15-L91 (77 LOC)
src/app/api/children/[id]/report/export/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    // Verify child exists and belongs to this parent
    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json(
        { error: "Child not found or access denied" },
        { status: 403 }
      );
    }

    // Query all writing submissions with their AI feedback
    const submissions = await prisma.writingSubmission.findMany({
      where: { childId },
      include: { feedback: true },
      orderBy: { createdAt: "desc" },
    });

    // Build CSV header
    const headers = [
      "Date",
      "Lesson",
      "Type",
      "Score",
      "Word Count",
      "Str
GET function · typescript · L8-L151 (144 LOC)
src/app/api/children/[id]/report/[lessonId]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string; lessonId: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId, lessonId } = await params;

    // Verify child belongs to this parent
    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json(
        { error: "Child not found or access denied" },
        { status: 403 }
      );
    }

    // Get lesson metadata from catalog
    const lesson = getLessonById(lessonId);
    if (!lesson) {
      return NextResponse.json(
        { error: "Lesson not found" },
        { status: 404 }
      );
    }

    // Get lesson progress
    const progress = await prisma.lessonProgress.findUnique({
      where: {
        childId_lessonId: { childI
GET function · typescript · L9-L309 (301 LOC)
src/app/api/children/[id]/report/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;
    const url = new URL(request.url);
    const generateSummary = url.searchParams.get("generateSummary") === "true";

    // Verify child exists and belongs to this parent
    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json(
        { error: "Child not found or access denied" },
        { status: 403 }
      );
    }

    // --- Summary aggregations ---

    const lessonProgressRecords = await prisma.lessonProgress.findMany({
      where: { childId },
    });
    const totalLessons = lessonProgressRecords.length;
    const completedLessons = lessonProgressRecords.f
computeTier function · typescript · L5-L9 (5 LOC)
src/app/api/children/[id]/route.ts
function computeTier(age: number): number {
  if (age <= 9) return 1;
  if (age <= 12) return 2;
  return 3;
}
GET function · typescript · L11-L39 (29 LOC)
src/app/api/children/[id]/route.ts
export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id } = await params;

    const child = await prisma.childProfile.findUnique({
      where: { id },
    });

    if (!child || child.parentId !== session.user.userId) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    return NextResponse.json({ child });
  } catch (error) {
    console.error("GET /api/children/[id] error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}
All rows scored by the Repobility analyzer (https://repobility.com)
PATCH function · typescript · L41-L112 (72 LOC)
src/app/api/children/[id]/route.ts
export async function PATCH(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id } = await params;

    const existing = await prisma.childProfile.findUnique({
      where: { id },
    });

    if (!existing || existing.parentId !== session.user.userId) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const body = await request.json();
    const { name, age, gradeLevel, interests, avatarEmoji } = body;

    const updateData: Record<string, unknown> = {};

    if (name !== undefined) {
      if (typeof name !== "string" || name.trim().length === 0) {
        return NextResponse.json(
          { error: "Name cannot be empty" },
          { status: 400 }
        );
      }
      updateData.name = name.trim();
    }

    if (age !== undefined) {
    
DELETE function · typescript · L114-L146 (33 LOC)
src/app/api/children/[id]/route.ts
export async function DELETE(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id } = await params;

    const existing = await prisma.childProfile.findUnique({
      where: { id },
    });

    if (!existing || existing.parentId !== session.user.userId) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    await prisma.childProfile.delete({
      where: { id },
    });

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("DELETE /api/children/[id] error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}
GET function · typescript · L6-L89 (84 LOC)
src/app/api/children/[id]/skills/route.ts
export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const skillRecords = await prisma.skillProgress.findMany({
      where: { childId },
      orderBy: { skillCategory: "asc" },
    });

    // Group by category with display names from SKILL_DEFINITIONS
    const categoryMap: Record<
      string,
      {
        name: string;
        displayName: string;
        avgScore: number;
        skills: {
          name: string;
          displayName: string;
          score: number;
          level: string;
    
POST function · typescript · L5-L61 (57 LOC)
src/app/api/children/[id]/streak/goal/route.ts
export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const body = await request.json();
    const { weeklyGoal } = body;

    if (
      typeof weeklyGoal !== "number" ||
      !Number.isInteger(weeklyGoal) ||
      weeklyGoal < 1 ||
      weeklyGoal > 7
    ) {
      return NextResponse.json(
        { error: "weeklyGoal must be an integer between 1 and 7" },
        { status: 400 }
      );
    }

    const streak = await prisma.streak.upsert({
      where: { childId },
      update: { weeklyGoal },
     
GET function · typescript · L5-L50 (46 LOC)
src/app/api/children/[id]/streak/route.ts
export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { id: childId } = await params;

    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const streak = await prisma.streak.findUnique({ where: { childId } });

    if (!streak) {
      return NextResponse.json({
        currentStreak: 0,
        longestStreak: 0,
        lastActiveDate: null,
        weeklyGoal: 3,
        weeklyCompleted: 0,
      });
    }

    return NextResponse.json({
      currentStreak: streak.currentStreak,
      longestStreak: streak.longestStreak,
      lastActiveDate: streak.lastActiveDate,
      weeklyGoal: strea
computeTier function · typescript · L5-L9 (5 LOC)
src/app/api/children/route.ts
function computeTier(age: number): number {
  if (age <= 9) return 1;
  if (age <= 12) return 2;
  return 3;
}
GET function · typescript · L11-L31 (21 LOC)
src/app/api/children/route.ts
export async function GET() {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const children = await prisma.childProfile.findMany({
      where: { parentId: session.user.userId },
      orderBy: { createdAt: "asc" },
    });

    return NextResponse.json({ children });
  } catch (error) {
    console.error("GET /api/children error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}
POST function · typescript · L33-L79 (47 LOC)
src/app/api/children/route.ts
export async function POST(request: NextRequest) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const body = await request.json();
    const { name, age, gradeLevel, interests, avatarEmoji } = body;

    if (!name || typeof name !== "string" || name.trim().length === 0) {
      return NextResponse.json(
        { error: "Name is required" },
        { status: 400 }
      );
    }

    if (!age || typeof age !== "number" || age < 7 || age > 15) {
      return NextResponse.json(
        { error: "Age must be between 7 and 15" },
        { status: 400 }
      );
    }

    const tier = computeTier(age);

    const child = await prisma.childProfile.create({
      data: {
        parentId: session.user.userId,
        name: name.trim(),
        age,
        tier,
        gradeLevel: gradeLevel || null,
        interests: interests || null,
        avatarEmoji: avatarEmoji ||
Same scanner, your repo: https://repobility.com — Repobility
POST function · typescript · L9-L248 (240 LOC)
src/app/api/curriculum/[childId]/revise/route.ts
export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ childId: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { childId } = await params;

    // Verify ownership
    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const body = await request.json();
    const { reason, description } = body;

    if (!reason || typeof reason !== "string") {
      return NextResponse.json(
        { error: "reason is required" },
        { status: 400 }
      );
    }
    if (!description || typeof description !== "string") {
      return NextResponse.json(
        { error: "description is required" },
        { status: 400 }
      );
    }

    // Get current
GET function · typescript · L6-L82 (77 LOC)
src/app/api/curriculum/[childId]/route.ts
export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ childId: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { childId } = await params;

    // Verify ownership
    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const curriculum = await prisma.curriculum.findUnique({
      where: { childId },
      include: { weeks: { orderBy: { weekNumber: "asc" } } },
    });

    if (!curriculum) {
      return NextResponse.json(
        { error: "No curriculum found" },
        { status: 404 }
      );
    }

    // Get completed lesson IDs for this child
    const completedProgress = await prisma.lessonProgress.findMany({
      where: { childId, stat
PATCH function · typescript · L84-L172 (89 LOC)
src/app/api/curriculum/[childId]/route.ts
export async function PATCH(
  request: NextRequest,
  { params }: { params: Promise<{ childId: string }> }
) {
  try {
    const session = await auth();
    if (!session?.user?.userId) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { childId } = await params;

    // Verify ownership
    const child = await prisma.childProfile.findFirst({
      where: { id: childId, parentId: session.user.userId },
    });
    if (!child) {
      return NextResponse.json({ error: "Child not found" }, { status: 404 });
    }

    const curriculum = await prisma.curriculum.findUnique({
      where: { childId },
    });
    if (!curriculum) {
      return NextResponse.json(
        { error: "No curriculum found" },
        { status: 404 }
      );
    }

    const body = await request.json();
    const updateData: Record<string, unknown> = {};

    if (body.lessonsPerWeek !== undefined) {
      if (
        typeof body.lessonsPerWeek !== "number" ||
       
page 1 / 6next ›