Skip to content

fix: add error resilience to all leaderboard song fetches#6

Merged
EtanHey merged 2 commits intomasterfrom
nightshift/2026-02-01-4105
Feb 6, 2026
Merged

fix: add error resilience to all leaderboard song fetches#6
EtanHey merged 2 commits intomasterfrom
nightshift/2026-02-01-4105

Conversation

@EtanHey
Copy link
Owner

@EtanHey EtanHey commented Feb 1, 2026

Automated improvement by GolemsZikaron Night Shift.

Added error resilience to leaderboard song fetches**


Note

Low Risk
Localized refactor of leaderboard read logic; main risk is subtle scoring/language attribution changes when song fetches fail or return null.

Overview
Improves leaderboard robustness by making all song lookups deduplicated and failure-tolerant, so a single missing/failed ctx.db.get(songId) no longer breaks streak/progress/rank calculations.

In convex/leaderboard.ts, replaces per-line Promise.all song fetches with Promise.allSettled plus a songId -> song|null map, and simplifies the default "mixed" language score initialization used in weighted scoring.

Written by Cursor Bugbot for commit 3bbb2b6. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Leaderboards now compute each user’s most-practiced language and include a top language/breakdown; streak entries show language.
  • Bug Fixes

    • Progress and ranking calculations tolerate individual song lookup failures and still return results when some lookups fail.
  • Chores

    • Removed several TypeScript suppression comments; minor type-checking cleanups (no user-facing behavior changes).

@vercel
Copy link

vercel bot commented Feb 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
songscript Ready Ready Preview, Comment Feb 6, 2026 0:34am

@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

Recomputes per-user language from practice lineProgress, uses Promise.allSettled for song fetches, and applies language-aware scoring in streak and progress leaderboards (and progress-based ranking). Removes several @ts-expect-error suppressions in UI files and adds .claude/settings.json.

Changes

Cohort / File(s) Summary
Leaderboard logic
convex/leaderboard.ts
Derives each user’s top language from lineProgress, deduplicates song IDs, fetches song metadata with Promise.allSettled (mapping failures to null), adds language to streak entries, and updates progress leaderboard & rank calculations to compute per-language scores and select topLanguage.
TypeScript suppression removals
src/components/LyricsDisplay.tsx, src/routes/song.$songId.tsx
Removed several // @ts-expect-error`` comments before convexQuery calls; runtime logic unchanged but type-checking may now surface errors.
Developer config
.claude/settings.json
Added file enabling MCP servers and defining a granular allowlist of permitted commands with one deny rule.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LeaderboardService
  participant ProgressDB as Progress DB
  participant SongsDB as Songs DB

  Client->>LeaderboardService: Request leaderboard
  LeaderboardService->>ProgressDB: Query lineProgress per user
  ProgressDB-->>LeaderboardService: Return lineProgress entries
  LeaderboardService->>SongsDB: Fetch deduped song metadata (allSettled)
  SongsDB-->>LeaderboardService: Return song data / nulls for failures
  LeaderboardService->>LeaderboardService: Aggregate language counts per user\nCompute topLanguage and language-weighted scores
  LeaderboardService-->>Client: Return leaderboard entries with language, scores, ranks
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I counted lines and chased each tune,
Some songs slept late beneath the moon,
I guessed the language, scored with care,
Returned the board for all to share,
A bunny clap — data done soon! 🎶

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main change: adding error resilience to leaderboard song fetches by using Promise.allSettled, which is the primary focus of the core functionality changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nightshift/2026-02-01-4105

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Caution

Docstrings generation - FAILED

No docstrings were generated.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Agent execution returned an error. Exit code: 1

@EtanHey
Copy link
Owner Author

EtanHey commented Feb 4, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EtanHey EtanHey changed the title Night Shift: Added error resilience to leaderboard song fetches fix: add error resilience to all leaderboard song fetches Feb 6, 2026
@EtanHey EtanHey force-pushed the nightshift/2026-02-01-4105 branch from bd56624 to 6e1b403 Compare February 6, 2026 03:27
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
convex/leaderboard.ts (4)

98-145: ⚠️ Potential issue | 🟠 Major

Performance concern: per-user song fetching in a leaderboard query.

For every user with a displayName, this query fetches all their lineProgress records and then individually fetches each unique song. With many users and songs, this becomes O(users × unique_songs_per_user) database reads in a single query invocation. Consider caching the user's primary language on the user document (denormalized) and updating it on practice events, so the leaderboard query becomes a simple sort.


208-244: ⚠️ Potential issue | 🟠 Major

Words learned are always scored with "mixed" multiplier, bypassing language difficulty scaling.

All wordProgress is bucketed under "mixed" (lines 212–216), so getLanguageMultiplier("mixed") is applied to the word count. If "mixed" maps to 1.0×, users studying Hard (1.5×) or Super-Hard (2.0×) languages get no word-learning bonus despite the intended formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5).

Since you already resolve each user's song languages from lineProgress, you could distribute word counts proportionally across those languages, or at minimum use the user's topLanguage multiplier for words.

Based on learnings: "Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)" and "Apply language difficulty multipliers based on FSI rankings."


306-420: 🛠️ Refactor suggestion | 🟠 Major

Significant code duplication: progress score calculation is repeated three times.

The same fetch-words → fetch-lines → fetch-songs → compute-language-scores → sum-weighted-scores pattern appears in:

  1. getProgressLeaderboard (lines 183–244)
  2. getUserRank for the current user (lines 308–359)
  3. getUserRank for each other user (lines 370–415)

Extract a shared helper, e.g. calculateProgressScore(ctx, userId), to eliminate ~90 lines of duplication and ensure scoring logic stays consistent across all paths.

Suggested helper extraction
async function calculateProgressScore(
  ctx: any,
  userId: string
): Promise<{ score: number; topLanguage: string }> {
  const wordProgress = await ctx.db
    .query("wordProgress")
    .withIndex("by_user", (q: any) => q.eq("userId", userId))
    .filter((q: any) => q.eq(q.field("learned"), true))
    .collect();

  const lineProgress = await ctx.db
    .query("lineProgress")
    .withIndex("by_user", (q: any) => q.eq("userId", userId))
    .filter((q: any) => q.eq(q.field("learned"), true))
    .collect();

  const songResults = await Promise.allSettled(
    lineProgress.map((line: any) => ctx.db.get(line.songId))
  );
  const songs = songResults.map((r: any) =>
    r.status === "fulfilled" ? r.value : null
  );

  const languageScores = new Map<string, { wordsLearned: number; linesCompleted: number }>();
  const defaultLanguage = "mixed";
  languageScores.set(defaultLanguage, { wordsLearned: wordProgress.length, linesCompleted: 0 });

  for (let i = 0; i < lineProgress.length; i++) {
    const song = songs[i];
    if (song) {
      const language = song.sourceLanguage;
      if (!languageScores.has(language)) {
        languageScores.set(language, { wordsLearned: 0, linesCompleted: 0 });
      }
      languageScores.get(language)!.linesCompleted++;
    }
  }

  let totalScore = 0;
  let topLanguage = "mixed";
  let maxLanguageScore = 0;
  for (const [language, scores] of languageScores.entries()) {
    const multiplier = getLanguageMultiplier(language);
    const languageScore =
      scores.wordsLearned * multiplier + scores.linesCompleted * multiplier * 0.5;
    totalScore += languageScore;
    if (languageScore > maxLanguageScore) {
      maxLanguageScore = languageScore;
      topLanguage = language;
    }
  }

  return {
    score: Math.round(totalScore * 10) / 10,
    topLanguage,
  };
}

As per coding guidelines: "All new helpers and utilities MUST have corresponding test files."


362-420: ⚠️ Potential issue | 🟠 Major

O(users²) query complexity in getUserRank progress path will not scale.

For each call to getUserRank({ type: "progress" }), the handler iterates every user with a displayName and, for each, executes 3+ database queries (wordProgress, lineProgress, individual song lookups). With n users this is O(n) × O(queries_per_user), and since this is a reactive Convex query, it re-executes whenever any relevant data changes.

Consider denormalizing: store each user's computed progressScore on their user document (updated via mutation when progress changes), then rank determination becomes a single indexed query.

🤖 Fix all issues with AI agents
In @.claude/settings.json:
- Around line 50-51: The settings currently grant broad 1Password access via the
wildcard entries "Bash(op run:*)" and "Bash(op inject:*)" in
.claude/settings.json; replace these wildcards with minimal, specific op
invocations (e.g., only the exact "op run:<allowed-command>" or "op
inject:<allowed-action>") or remove them entirely if not required, so the agent
cannot call arbitrary 1Password CLI commands and risk exposing secrets; update
the "Bash(op run:*)" and "Bash(op inject:*)" lines accordingly to enumerate only
the safe, required op commands.
- Line 60: The setting enableAllProjectMcpServers currently set to true
auto-trusts all project-defined MCP servers; change its default to false in
.claude/settings.json (update the enableAllProjectMcpServers boolean to false)
and add guidance/comments or a README entry explaining how to explicitly opt-in
trusted MCP servers (e.g., a per-project allowlist or CLAUDE_TRUSTED_MCP
setting) so only vetted servers are enabled; ensure any code that reads
enableAllProjectMcpServers (configuration loader logic) treats false as the
secure default and provides a clear warning/log when a repo-defined MCP server
is ignored.
- Around line 40-41: The settings currently grant overly broad executor
permissions like "Bash(source:*)" and any "python3:*", which allow arbitrary
shell/Python execution; update .claude/settings.json to replace wildcards with
explicit, minimal allowed targets (e.g.,
"Bash(source:/safe/scripts/specific-script.sh)" or
"python3:/safe/scripts/allowed_script.py"), or remove these entries if not
needed, and add a short comment explaining the restricted paths and why they are
safe so reviewers/contributors understand the changed permissions; reference the
exact entries "Bash(source:*)" and any "python3:*" occurrences when making the
edits.
- Around line 56-58: The deny list currently blocks "Bash(rm -rf:*)" but not the
less-forcing "rm -r"; update the deny array entry named "Bash(rm -rf:*)" by also
adding a pattern for "Bash(rm -r:*)" to explicitly block recursive deletes
without forcing, and remove the variant with an extra space ("Bash(rm -r :*)")
to avoid redundant/incorrect patterns; ensure you edit the deny array symbol so
both "Bash(rm -rf:*)" and "Bash(rm -r:*)" are present and rely on the config's
pattern-matching rules rather than adding duplicate spaced variants.

In `@convex/leaderboard.ts`:
- Around line 110-118: Replace the Promise.allSettled pattern with Promise.all
and remove the settled-status branching: call Promise.all on
uniqueSongIds.map(id => ctx.db.get(id)) (matching existing usages), assign the
resulting array to a variable (e.g., songs), and then build songMap from
uniqueSongIds and songs (use songs[i] instead of songSettled[i].value); update
references to songSettled to use the new songs array and ensure null results
from ctx.db.get are handled by existing null-checks.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd56624 and 6e1b403.

📒 Files selected for processing (4)
  • .claude/settings.json
  • convex/leaderboard.ts
  • src/components/LyricsDisplay.tsx
  • src/routes/song.$songId.tsx
💤 Files with no reviewable changes (2)
  • src/routes/song.$songId.tsx
  • src/components/LyricsDisplay.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TanStack Start framework with Bun runtime for the application

Tests must pass locally via bun run test before committing code, as Husky pre-commit hooks will block commits with failing tests

Files:

  • convex/leaderboard.ts
convex/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use Convex for database queries, mutations, and authentication configuration

Files:

  • convex/leaderboard.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

All new helpers and utilities MUST have corresponding test files

Files:

  • convex/leaderboard.ts
convex/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

convex/**/*.ts: NEVER create .js files in the convex/ folder - only .ts files belong there
Before starting Convex dev server, always run rm -f convex/*.js to clean compiled JavaScript files
Use Convex schema.ts for database schema definitions and songs.ts for queries and mutations

Files:

  • convex/leaderboard.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*{score,progress,leaderboard}*.{ts,tsx,js} : Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/leaderboard*.{ts,tsx,js} : Users can play without displayName, but must set displayName before appearing on the leaderboard
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*.{ts,tsx,js} : Apply language difficulty multipliers based on FSI rankings: Standard languages (Spanish, French, Italian, Dutch) use 1.0x, Hard languages (Hebrew, Persian, Russian, Hindi) use 1.5x, Super-Hard languages (Arabic, Japanese, Korean, Chinese) use 2.0x
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*{score,progress,leaderboard}*.{ts,tsx,js} : Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)

Applied to files:

  • convex/leaderboard.ts
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/leaderboard*.{ts,tsx,js} : Users can play without displayName, but must set displayName before appearing on the leaderboard

Applied to files:

  • convex/leaderboard.ts
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*.{ts,tsx,js} : Apply language difficulty multipliers based on FSI rankings: Standard languages (Spanish, French, Italian, Dutch) use 1.0x, Hard languages (Hebrew, Persian, Russian, Hindi) use 1.5x, Super-Hard languages (Arabic, Japanese, Korean, Chinese) use 2.0x

Applied to files:

  • convex/leaderboard.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (4)
convex/leaderboard.ts (4)

88-93: Leaderboard correctly filters users by displayName presence.

The filter q.neq(q.field("displayName"), undefined) ensures only users who have set a display name appear on leaderboards, consistent with the requirement that users must set a displayName before appearing.


197-206: getProgressLeaderboard correctly applies Promise.allSettled for resilient song fetching.

The Promise.allSettled pattern here preserves index alignment with lineProgress (critical for the loop at lines 219–228) and gracefully handles any failed lookups by mapping them to null. The existing if (song) check at line 221 correctly skips nulls.


237-237: Progress score formula matches the specified formula.

(scores.wordsLearned * multiplier) + (scores.linesCompleted * multiplier * 0.5) correctly implements the required formula. This is consistent across all three copies of the calculation (lines 237, 356, 412).

Based on learnings: "Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)."


283-305: No action required — getUserRank correctly omits the language field.

The return shape difference is intentional. getUserRank returns only { rank, score, hasDisplayName } because it's meant to answer "what's my rank?" for the current user's rank card. Neither consumer (leaderboard.tsx nor LeaderboardMiniSection.tsx) accesses a language field from getUserRank—both only use rank and score. The language field in getStreakLeaderboard is for the public leaderboard display, which serves a different purpose. The two queries have different responsibilities and return shapes by design.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +40 to +41
"Bash(source:*)",
"Bash(yt-dlp:*)",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Broad permissions for source:* and python3:* could execute arbitrary code.

source:* can execute any shell script on disk (including untrusted ones), and python3:* can run arbitrary Python. Consider narrowing these to specific scripts/paths if possible, or ensure contributors understand these permissions are granted.

🤖 Prompt for AI Agents
In @.claude/settings.json around lines 40 - 41, The settings currently grant
overly broad executor permissions like "Bash(source:*)" and any "python3:*",
which allow arbitrary shell/Python execution; update .claude/settings.json to
replace wildcards with explicit, minimal allowed targets (e.g.,
"Bash(source:/safe/scripts/specific-script.sh)" or
"python3:/safe/scripts/allowed_script.py"), or remove these entries if not
needed, and add a short comment explaining the restricted paths and why they are
safe so reviewers/contributors understand the changed permissions; reference the
exact entries "Bash(source:*)" and any "python3:*" occurrences when making the
edits.

Comment on lines +50 to +51
"Bash(op run:*)",
"Bash(op inject:*)",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

op run:* and op inject:* grant access to 1Password secrets.

These permissions allow the AI agent to invoke 1Password CLI commands, which could inadvertently expose secrets in logs or outputs. If these are needed, consider restricting to specific op invocations rather than wildcards.

🤖 Prompt for AI Agents
In @.claude/settings.json around lines 50 - 51, The settings currently grant
broad 1Password access via the wildcard entries "Bash(op run:*)" and "Bash(op
inject:*)" in .claude/settings.json; replace these wildcards with minimal,
specific op invocations (e.g., only the exact "op run:<allowed-command>" or "op
inject:<allowed-action>") or remove them entirely if not required, so the agent
cannot call arbitrary 1Password CLI commands and risk exposing secrets; update
the "Bash(op run:*)" and "Bash(op inject:*)" lines accordingly to enumerate only
the safe, required op commands.

Comment on lines +56 to +58
"deny": [
"Bash(rm -rf:*)"
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

cat .claude/settings.json

Repository: EtanHey/songscript

Length of output: 1630


🌐 Web query:

Claude Code allow deny precedence bash commands

💡 Result:

In Claude Code permission rules for Bash commands, precedence works like this:

  1. deny wins over everything: if a Bash invocation matches any permissions.deny rule, it is blocked—even if it also matches allow or ask. [1]
  2. ask wins over allow: if it matches an ask rule, Claude Code will prompt you, even if there’s also a matching allow rule. [1]
  3. allow applies only if no deny or ask matched: the command runs without prompting. [1]
  4. Otherwise, Claude Code falls back to the default behavior (controlled by settings like defaultMode). [1][2]

Also note: Bash patterns are prefix matches (not regex), so broad allows can unintentionally include more dangerous variants unless you carve them out with deny/ask. [2]

Example

{
  "permissions": {
    "allow": ["Bash(git push:*)"],
    "ask":   ["Bash(git push --force:*)"],
    "deny":  ["Bash(curl:*)"]
  }
}
  • git push origin main → allowed (no prompt)
  • git push --force ... → prompts (ask overrides allow)
  • curl ... → blocked (deny overrides everything)

Sources: Anthropic Claude Code IAM / permissions precedence [1]; Claude Code settings reference (Bash prefix matching) [2].


🌐 Web query:

difference between rm -r and rm -rf destructiveness bash

💡 Result:

  • rm -r: recursive. It lets rm remove directories and their contents (descend into the directory tree). Without -r, rm typically won’t remove directories at all. POSIX defines -r/-R as “recursive descent through the file hierarchy.” [1]

  • rm -rf: recursive + force. It still removes directories recursively, and -f (“force”) makes rm not prompt for confirmation and suppress some errors, notably “file doesn’t exist” (and typically doesn’t treat that as a failure). [1][7]

“Destructiveness” in practice (Bash context)

  • rm -r is destructive because it can delete whole directory trees, but you’re more likely to notice problems (e.g., non-existent files, permission issues) via diagnostics, and depending on your environment/aliases and TTY/writability conditions, you may still see prompts in some cases. [1]
  • rm -rf is usually more destructive because it combines recursive deletion with no confirmation prompts and fewer visible warnings, making it easier to delete large amounts of data quickly (including via scripts/globs). [7][5]

rm -r is less destructive than rm -rf, not equally destructive.

rm -r recursively deletes but still shows prompts and error messages, whereas rm -rf forces deletion and suppresses errors—making it more dangerous. However, rm -r can still pose a risk and may be worth blocking. Given that the allow list is whitelist-based (only specific commands allowed) with no explicit rm -r permission, rm -r would likely already be blocked by default depending on the configuration's default behavior. If protection against rm -r is desired, the suggested addition of "Bash(rm -r:*)" is reasonable, though the "Bash(rm -r :*)" variant may be redundant depending on how pattern matching works.

🤖 Prompt for AI Agents
In @.claude/settings.json around lines 56 - 58, The deny list currently blocks
"Bash(rm -rf:*)" but not the less-forcing "rm -r"; update the deny array entry
named "Bash(rm -rf:*)" by also adding a pattern for "Bash(rm -r:*)" to
explicitly block recursive deletes without forcing, and remove the variant with
an extra space ("Bash(rm -r :*)") to avoid redundant/incorrect patterns; ensure
you edit the deny array symbol so both "Bash(rm -rf:*)" and "Bash(rm -r:*)" are
present and rely on the config's pattern-matching rules rather than adding
duplicate spaced variants.

"Bash(rm -rf:*)"
]
},
"enableAllProjectMcpServers": true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

enableAllProjectMcpServers: true auto-trusts all project-defined MCP servers.

This means any .mcp.json or similar config in the repo will be automatically trusted. A malicious contribution adding an MCP server config could gain these permissions. Consider setting this to false and explicitly enabling only trusted MCP servers.

🤖 Prompt for AI Agents
In @.claude/settings.json at line 60, The setting enableAllProjectMcpServers
currently set to true auto-trusts all project-defined MCP servers; change its
default to false in .claude/settings.json (update the enableAllProjectMcpServers
boolean to false) and add guidance/comments or a README entry explaining how to
explicitly opt-in trusted MCP servers (e.g., a per-project allowlist or
CLAUDE_TRUSTED_MCP setting) so only vetted servers are enabled; ensure any code
that reads enableAllProjectMcpServers (configuration loader logic) treats false
as the secure default and provides a clear warning/log when a repo-defined MCP
server is ignored.

Comment on lines +110 to +118
const songSettled = await Promise.allSettled(
uniqueSongIds.map((id) => ctx.db.get(id))
);
const songMap = new Map(
uniqueSongIds.map((id, i) => [
id,
songSettled[i].status === 'fulfilled' ? songSettled[i].value : null,
])
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

Convex ctx.db.get behavior missing documents null throw error

💡 Result:

In Convex, ctx.db.get(...) does not throw when a document is missing — it returns null and you’re expected to handle that in your code (the Convex docs’ own examples check if (doc === null) ...). [1]

It can throw in “developer error” situations, e.g. if you pass an invalid argument like null instead of a real document id. [2]

If you want “missing doc => throw”

Do it explicitly:

import { ConvexError } from "convex/values";

const doc = await ctx.db.get("tasks", args.id);
if (doc === null) throw new ConvexError("Not found");
return doc;

Or use a helper that “orThrow”s (community pattern), e.g. getOneFromOrThrow / getAllOrThrow. [3]

Sources: Convex query docs showing db.get returning null for missing docs [1]; Convex error-handling docs noting invalid arguments (like null) are developer errors [2]; Convex community helper patterns for throwing instead of returning null [3].

[1] (docs.convex.dev)
[2] (docs.convex.dev)
[3] (stack.convex.dev)

Citations:


🏁 Script executed:

# Check how ctx.db.get is used in the codebase
rg "ctx\.db\.get" convex/ -A 2 -B 2

Repository: EtanHey/songscript

Length of output: 7863


🏁 Script executed:

# Look at the full context of the code in leaderboard.ts
sed -n '100,125p' convex/leaderboard.ts

Repository: EtanHey/songscript

Length of output: 1102


Use Promise.all() with null-checks instead of Promise.allSettled().

In Convex, ctx.db.get() returns null for missing documents—it doesn't throw. The only exceptions are developer errors (e.g., passing null as the id), which allSettled wouldn't help with. Throughout the codebase, all other usages follow the simpler pattern: Promise.all() followed by null checks.

Current code
const songSettled = await Promise.allSettled(
  uniqueSongIds.map((id) => ctx.db.get(id))
);
const songMap = new Map(
  uniqueSongIds.map((id, i) => [
    id,
    songSettled[i].status === 'fulfilled' ? songSettled[i].value : null,
  ])
);

Simplify to match the established pattern in the codebase:

const songs = await Promise.all(
  uniqueSongIds.map((id) => ctx.db.get(id))
);
const songMap = new Map(
  uniqueSongIds.map((id, i) => [id, songs[i]])
);
🤖 Prompt for AI Agents
In `@convex/leaderboard.ts` around lines 110 - 118, Replace the Promise.allSettled
pattern with Promise.all and remove the settled-status branching: call
Promise.all on uniqueSongIds.map(id => ctx.db.get(id)) (matching existing
usages), assign the resulting array to a variable (e.g., songs), and then build
songMap from uniqueSongIds and songs (use songs[i] instead of
songSettled[i].value); update references to songSettled to use the new songs
array and ensure null results from ctx.db.get are handled by existing
null-checks.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
convex/leaderboard.ts (2)

369-421: 🛠️ Refactor suggestion | 🟠 Major

O(N × queries) rank computation recalculates every user's score from scratch — extract shared helper.

The progress score calculation logic is duplicated three times: getProgressLeaderboard (lines 209–245), getUserRank for the current user (lines 331–360), and getUserRank for every other user (lines 392–416). Each iteration issues multiple DB queries per user, making this an expensive N+1 pattern.

Extract a calculateUserProgressScore(ctx, userId) helper and reuse it. Even better, getUserRank could call the progress leaderboard logic once and find the user's position in the sorted result, rather than recomputing every score independently.

Sketch of extracted helper
async function calculateUserProgressScore(
  ctx: any,
  userId: string
): Promise<number> {
  const wordProgress = await ctx.db
    .query("wordProgress")
    .withIndex("by_user", (q: any) => q.eq("userId", userId))
    .filter((q: any) => q.eq(q.field("learned"), true))
    .collect();

  const lineProgress = await ctx.db
    .query("lineProgress")
    .withIndex("by_user", (q: any) => q.eq("userId", userId))
    .filter((q: any) => q.eq(q.field("learned"), true))
    .collect();

  const uniqueSongIds = [...new Set(lineProgress.map((l) => l.songId))];
  const songResults = await Promise.all(
    uniqueSongIds.map((id) => ctx.db.get(id))
  );
  const songMap = new Map(
    uniqueSongIds.map((id, i) => [id, songResults[i]])
  );

  const languageScores = new Map<string, { wordsLearned: number; linesCompleted: number }>();
  languageScores.set("mixed", { wordsLearned: wordProgress.length, linesCompleted: 0 });

  for (const line of lineProgress) {
    const song = songMap.get(line.songId);
    if (song) {
      const lang = song.sourceLanguage;
      if (!languageScores.has(lang)) {
        languageScores.set(lang, { wordsLearned: 0, linesCompleted: 0 });
      }
      languageScores.get(lang)!.linesCompleted++;
    }
  }

  let total = 0;
  for (const [language, scores] of languageScores.entries()) {
    const multiplier = getLanguageMultiplier(language);
    total += (scores.wordsLearned * multiplier) + (scores.linesCompleted * multiplier * 0.5);
  }
  return Math.round(total * 10) / 10;
}

88-93: 🧹 Nitpick | 🔵 Trivial

Unbounded .collect() on all users will hit Convex query limits as the user base grows.

Both getStreakLeaderboard and getUserRank fetch all users with a display name and then run sequential async operations (streak calculation, song lookups) for each. Convex queries have execution time limits, so this will eventually fail. Consider caching/precomputing leaderboard scores via a scheduled mutation, or at minimum adding a reasonable upper bound.

🤖 Fix all issues with AI agents
In @.claude/settings.json:
- Around line 14-15: The allow list currently includes the risky commands
"Bash(git rebase:*)" and "Bash(git merge:*)"; update .claude/settings.json to
prevent automatic destructive history rewrites by removing these two entries
from the allow list and adding them to the "ask" list (or otherwise gating them
behind confirmation), referencing the exact strings "Bash(git rebase:*)" and
"Bash(git merge:*)" so the agent will prompt a human before executing those
commands.

In `@convex/leaderboard.ts`:
- Around line 198-207: The song fetch loop currently calls ctx.db.get for every
entry in lineProgress causing duplicate DB reads; instead compute a deduplicated
set of songIds (like new Set(lineProgress.map(l => l.songId))), fetch each
unique song once (using ctx.db.get for each id) and build a songMap (songId ->
song or null) from those results; then replace usages that index into songs by
position (e.g., the mapping at Lines where songResults/songs are used) to lookup
via songMap.get(lineProgress[i].songId) (also apply the same dedupe/fetch
approach for the other occurrences noted around the blocks referencing
songResults at the other locations).
- Around line 101-138: The leaderboard file contains inline language-detection
logic that needs to be extracted into a pure, testable helper (e.g.,
detectPrimaryLanguage or getPrimaryLanguageFromLineProgress) and unit-tested
alongside calculateCurrentStreak and the query handlers getStreakLeaderboard and
getProgressLeaderboard; refactor lines that build lineProgress, deduplicate song
IDs, resolve songs into a songMap, count languages and pick the top language
into that helper, then add convex/leaderboard.test.ts to cover empty
lineProgress, missing/null songs in songMap, language ties (ensure deterministic
tie-breaking), and normal cases, plus unit tests for calculateCurrentStreak and
full behavior of getStreakLeaderboard/getProgressLeaderboard using mocked DB
responses for lineProgress and song lookups.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e1b403 and 6da90a0.

📒 Files selected for processing (2)
  • .claude/settings.json
  • convex/leaderboard.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TanStack Start framework with Bun runtime for the application

Tests must pass locally via bun run test before committing code, as Husky pre-commit hooks will block commits with failing tests

Files:

  • convex/leaderboard.ts
convex/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use Convex for database queries, mutations, and authentication configuration

Files:

  • convex/leaderboard.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

All new helpers and utilities MUST have corresponding test files

Files:

  • convex/leaderboard.ts
convex/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

convex/**/*.ts: NEVER create .js files in the convex/ folder - only .ts files belong there
Before starting Convex dev server, always run rm -f convex/*.js to clean compiled JavaScript files
Use Convex schema.ts for database schema definitions and songs.ts for queries and mutations

Files:

  • convex/leaderboard.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*{score,progress,leaderboard}*.{ts,tsx,js} : Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*.{ts,tsx,js} : Apply language difficulty multipliers based on FSI rankings: Standard languages (Spanish, French, Italian, Dutch) use 1.0x, Hard languages (Hebrew, Persian, Russian, Hindi) use 1.5x, Super-Hard languages (Arabic, Japanese, Korean, Chinese) use 2.0x
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*{score,progress,leaderboard}*.{ts,tsx,js} : Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)

Applied to files:

  • convex/leaderboard.ts
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*.{ts,tsx,js} : Apply language difficulty multipliers based on FSI rankings: Standard languages (Spanish, French, Italian, Dutch) use 1.0x, Hard languages (Hebrew, Persian, Russian, Hindi) use 1.5x, Super-Hard languages (Arabic, Japanese, Korean, Chinese) use 2.0x

Applied to files:

  • convex/leaderboard.ts
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/leaderboard*.{ts,tsx,js} : Users can play without displayName, but must set displayName before appearing on the leaderboard

Applied to files:

  • convex/leaderboard.ts
📚 Learning: 2026-01-28T12:17:53.877Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-28T12:17:53.877Z
Learning: Applies to convex/**/*.ts : Use Convex schema.ts for database schema definitions and songs.ts for queries and mutations

Applied to files:

  • convex/leaderboard.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
convex/leaderboard.ts (3)

140-145: LGTM — streak entry now includes the inferred language.

The addition of the computed language field to the streak leaderboard entries is a good improvement over the previous hardcoded "mixed" placeholder.


236-245: Progress score formula correctly implements the specified formula with language multipliers.

(scores.wordsLearned * multiplier) + (scores.linesCompleted * multiplier * 0.5) matches the required formula. The topLanguage derivation by highest weighted score is sensible.

Based on learnings: "Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)".


209-217: No action needed. The getLanguageMultiplier function safely handles unknown languages including "mixed" by returning the default multiplier of 1.0x. The test suite confirms that unrecognized language strings return 1.0 rather than 0 or throwing an error, so the score calculation formula is safe and correct.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +14 to +15
"Bash(git rebase:*)",
"Bash(git merge:*)",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

git rebase:* and git merge:* in the allow list can rewrite history.

These are destructive Git operations that can alter commit history or introduce merge conflicts. If the agent is intended only for read/review tasks, consider moving these to "ask" so a human confirms before execution. If the agent does need these for automated workflows, this is acceptable.

🤖 Prompt for AI Agents
In @.claude/settings.json around lines 14 - 15, The allow list currently
includes the risky commands "Bash(git rebase:*)" and "Bash(git merge:*)"; update
.claude/settings.json to prevent automatic destructive history rewrites by
removing these two entries from the allow list and adding them to the "ask" list
(or otherwise gating them behind confirmation), referencing the exact strings
"Bash(git rebase:*)" and "Bash(git merge:*)" so the agent will prompt a human
before executing those commands.

Comment on lines +101 to +138

// Detect user's primary language from their practice data
const lineProgress = await ctx.db
.query("lineProgress")
.withIndex("by_user", (q) => q.eq("userId", user._id))
.filter((q) => q.eq(q.field("learned"), true))
.collect();

// Batch-fetch songs with deduplication + error resilience
const uniqueSongIds = [...new Set(lineProgress.map((l) => l.songId))];
const songSettled = await Promise.allSettled(
uniqueSongIds.map((id) => ctx.db.get(id))
);
const songMap = new Map(
uniqueSongIds.map((id, i) => [
id,
songSettled[i].status === 'fulfilled' ? songSettled[i].value : null,
])
);

// Count lines per language
const languageCounts = new Map<string, number>();
for (const line of lineProgress) {
const song = songMap.get(line.songId);
if (song?.sourceLanguage) {
languageCounts.set(song.sourceLanguage, (languageCounts.get(song.sourceLanguage) || 0) + 1);
}
}

// Find most practiced language
let language = "mixed";
let maxCount = 0;
for (const [lang, count] of languageCounts.entries()) {
if (count > maxCount) {
maxCount = count;
language = lang;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check for test files related to leaderboard
fd -e test.ts -e spec.ts -e test.js -e spec.js | grep -i leaderboard
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.test.js" -o -name "*.spec.js" \) | head -20

Repository: EtanHey/songscript

Length of output: 593


🏁 Script executed:

# Check for test directory structure
ls -la | grep -i test
find . -maxdepth 3 -type d -name "*test*" -o -name "*spec*" -o -name "*__tests__*"

Repository: EtanHey/songscript

Length of output: 360


🏁 Script executed:

# Search for any existing tests that might reference leaderboard
rg "leaderboard" --type ts --type tsx | grep -i "test\|spec\|describe\|it(" | head -10

Repository: EtanHey/songscript

Length of output: 89


🏁 Script executed:

# Check the actual file to understand the context better
wc -l convex/leaderboard.ts

Repository: EtanHey/songscript

Length of output: 87


🏁 Script executed:

# View the specific lines mentioned in the review
sed -n '101,138p' convex/leaderboard.ts

Repository: EtanHey/songscript

Length of output: 1369


🏁 Script executed:

# Check the git status/diff context to see if these lines are new
git diff HEAD -- convex/leaderboard.ts | head -100

Repository: EtanHey/songscript

Length of output: 44


🏁 Script executed:

# Look for any test files that might test this logic
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) -exec grep -l "leaderboard\|getStreakLeaderboard\|getProgressLeaderboard" {} \;

Repository: EtanHey/songscript

Length of output: 127


🏁 Script executed:

# Get wider context around lines 101-138 to identify which function this is in
sed -n '85,145p' convex/leaderboard.ts

Repository: EtanHey/songscript

Length of output: 2085


🏁 Script executed:

# Check what the test files that mention leaderboard are actually testing
grep -A 5 -B 5 "leaderboard" e2e/auth-routes.spec.ts | head -30

Repository: EtanHey/songscript

Length of output: 817


🏁 Script executed:

# Check if there's a git history or if this is the initial commit
git log --oneline convex/leaderboard.ts | head -5

Repository: EtanHey/songscript

Length of output: 92


🏁 Script executed:

# Check the coding guidelines more carefully - look for test requirements in comments
grep -r "test" CLAUDE.md 2>/dev/null | head -10

Repository: EtanHey/songscript

Length of output: 577


🏁 Script executed:

# Check if there's a diff or if lines have markers showing they're new
git status convex/leaderboard.ts

Repository: EtanHey/songscript

Length of output: 127


🏁 Script executed:

# Look at the function signatures to understand what needs testing
grep -n "export\|const.*=" convex/leaderboard.ts | head -20

Repository: EtanHey/songscript

Length of output: 1024


🏁 Script executed:

# Check if languageDifficulty utility mentioned in learnings is tested
cat src/utils/languageDifficulty.test.ts | head -30

Repository: EtanHey/songscript

Length of output: 1192


🏁 Script executed:

# Look for any existing tests in convex/ directory
ls -la convex/*.test.ts
cat convex/admin.test.ts | head -40

Repository: EtanHey/songscript

Length of output: 1680


Add unit tests for leaderboard query handlers.

convex/leaderboard.ts lacks a test file despite containing complex logic similar to admin.ts and authHelpers.ts, which both have corresponding test files. Per coding guidelines, all new helpers and utilities in convex/ must have tests. Extract the language-detection logic (lines 101–138) into a testable helper function and add convex/leaderboard.test.ts with coverage for:

  • Edge cases: empty lineProgress, missing songs, language ties
  • The calculateCurrentStreak helper
  • Both getStreakLeaderboard and getProgressLeaderboard query handlers
🤖 Prompt for AI Agents
In `@convex/leaderboard.ts` around lines 101 - 138, The leaderboard file contains
inline language-detection logic that needs to be extracted into a pure, testable
helper (e.g., detectPrimaryLanguage or getPrimaryLanguageFromLineProgress) and
unit-tested alongside calculateCurrentStreak and the query handlers
getStreakLeaderboard and getProgressLeaderboard; refactor lines that build
lineProgress, deduplicate song IDs, resolve songs into a songMap, count
languages and pick the top language into that helper, then add
convex/leaderboard.test.ts to cover empty lineProgress, missing/null songs in
songMap, language ties (ensure deterministic tie-breaking), and normal cases,
plus unit tests for calculateCurrentStreak and full behavior of
getStreakLeaderboard/getProgressLeaderboard using mocked DB responses for
lineProgress and song lookups.

EtanHey and others added 2 commits February 6, 2026 14:33
Apply Promise.allSettled consistently across all song lookup patterns
so individual fetch failures don't break the entire leaderboard.
Also add git worktree permission to local settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@EtanHey EtanHey force-pushed the nightshift/2026-02-01-4105 branch from 6da90a0 to 3bbb2b6 Compare February 6, 2026 12:34
@EtanHey EtanHey merged commit 5eb1084 into master Feb 6, 2026
4 checks passed
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

uniqueSongIds.map((id, i) => [
id,
songSettled[i].status === 'fulfilled' ? songSettled[i].value : null,
])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identical song-fetching pattern duplicated four times

Low Severity

The same ~10-line pattern for batch-fetching songs with Promise.allSettled and building a songMap is repeated 4 times in this file (lines 109-119, 198-208, 316-326, and 372-381). This duplication increases maintenance burden—if the error resilience logic needs adjustment (e.g., logging failures, retry logic), the fix must be applied in 4 places, risking inconsistent behavior. Extracting this into a helper function like batchFetchSongsAsMap(ctx, lineProgress) would reduce repetition.

Additional Locations (2)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant