fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep - #462

Merged
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8
Jul 30, 2026
Merged

fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep#462
AndresL230 merged 3 commits into
mainfrom
worktree-fix-explore-findings-f1-f8

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes all eight findings from the 2026-07-29 Chapter 2 /explore sweep (.explore/findings.md). Each fix was built by an isolated subagent (disjoint files), following systematic-debugging + TDD.

#FindingFix
F6Study-guide feature 500-bricked for every courseroutes/study_guide.py queries assignments by enrollment_id (was the phantom user_id/course_id columns on the enrollment-keyed table). get_exams, _generate_and_insert, and get_courses (now delegates to graph_service.get_courses).
F5aNotetaker agent actions 500 in function-modeRegistered note_summary / note_concepts / note_chat handlers in agents/function_handlers_e2e.py (request-path tasks were unregistered → UnregisteredHandlerError).
F4Every note shows "Unknown course"routes/notes.py list + single-read + create now return the abstract course_id + course_code/course_name resolved from the note's offering.
F1/F3#449 duplicate courses (dashboard count, tree chips, every picker)graph_service.get_courses collapses the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative, node_count counted once, additive enrollment_ids/terms lists).
F2Onboarding search shows indistinguishable duplicate coursesroutes/onboarding.py dedups results by course code (rich/base seeds define same-code courses under different schools).
F7"First Steps" achievement stuck at 100%, never grantedroutes/auth.py fires an idempotent login-streak achievement check on approved Google sign-in. (test-login untouched — it contractually performs no DB writes.)
F5bNotetaker actions fail silentlynotetaker/page.tsx surfaces toast.error(humanizeError(...)) on failed Summarize/Extract/Generate-quiz/Send-to-tutor.
F8Settings profile form blank → data-loss risk on saveSettings.tsx prefills name/username (and bio/location/website) from the profile fetch.

Verification

  • Backend:1325 passed (baseline 1311 + 14 new regression tests), ruff clean.
  • Frontend:tsc --noEmit clean; new Settings.test.tsx (2 cases) + errorMessage (32) pass.
  • Live backend re-check (function-mode stack + e2e oracles): F6 exams → 200, F4 notes carry MATH210/CS101, F5a note actions → 200, F1/F3 courses → 4 rows/4 unique, F2 onboarding dedup → no code dupes, oracles: 0 findings (down from 6).

Follow-ups deliberately left out of scope (flagged for triage)

  • F2: dedup runs after the limit=20 cap and the endpoint has no school scoping — a genuine multi-school production catalog with two real same-code courses would over-collapse. Correct long-term fix is a distinguishing school label, not dedup.
  • F7: a brand-new user with streak_count = 0 still won't be granted on their very first login — the login_streak threshold (1) vs. the "log in for the first time" wording is a mismatch in the 0007 seed trigger definition (needs a migration).
  • F5b: other notetaker handlers (createNoteIn, deleteActive, link/unlink concept, autosave) still only console.error on failure.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Notetaker actions now reliably support summaries, concepts, and tutor chat.
    • Profile settings prefill missing fields from the public profile.
    • Approved Google sign-in now awards the “First Steps” achievement.
  • Bug Fixes
    • Notes now immediately show resolved course details (no “unknown” until refresh).
    • Onboarding course listings deduplicate by course code (blank codes are preserved).
    • Study guides and exam lists are now correctly scoped to your enrollments.
    • Active-semester filtering is now consistent across dashboards and study surfaces, and Notetaker failures show clear toast errors.

Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md):
- F6 study_guide: query `assignments` by enrollment_id, not the phantom
user_id/course_id columns (enrollment-keyed table) — get_exams,
_generate_and_insert, and get_courses all 500'd, bricking the whole
study-guide feature. get_courses now delegates to graph_service.get_courses.
- F5a agents/function_handlers_e2e: register note_summary / note_concepts /
note_chat handlers — these are request-path agent tasks that 500'd with
UnregisteredHandlerError in function mode.
- F4 notes: /api/notes/user, the single-note read, and create now return the
abstract course_id + course_code/name resolved from the offering (every
note showed "Unknown course").
- F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one
row per course_id (most-recent enrollment as representative; node_count
counted once; additive enrollment_ids/terms lists). Fixes the dashboard
count, /tree chips, and every course picker (#449).
- F2 onboarding.search_courses: dedup catalog results by course code so the
rich/base seed same-code courses don't show as indistinguishable dupes.
- F7 auth: fire an idempotent login-streak achievement check on approved
Google sign-in so "First Steps" is actually granted (test-login left
untouched — it contractually performs no DB writes).
- F5b notetaker: surface toast.error(humanizeError(...)) on failed agent
actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops).
- F8 Settings: prefill the profile form from the profile fetch so name/username
aren't blank (data-loss risk on save).
Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc
clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles
return 0 findings (down from 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f769270b-4f7e-4218-b038-06b1470391fb

📥 Commits

Reviewing files that changed from the base of the PR and between ba6409e and 9a4f63e.

📒 Files selected for processing (5)
  • backend/agents/function_handlers_e2e.py
  • backend/tests/test_e2e_function_handlers.py
  • frontend/src/components/screens/Dashboard.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Tree.tsx
📝 Walkthrough

Walkthrough

The PR updates enrollment-based course and exam resolution, adds first-login achievement checks and deterministic notetaker handlers, enriches note responses, supports multi-term frontend course filtering, improves Settings profile fallback, deduplicates onboarding courses, and adds regression coverage.

Changes

Course and enrollment resolution

Layer / File(s)Summary
Course aggregation and response shaping
backend/services/graph_service.py, backend/routes/notes.py, backend/routes/onboarding.py, backend/tests/test_graph_service.py, backend/tests/test_notes_routes.py, backend/tests/test_onboarding_routes.py
Course results aggregate enrollments by abstract course, notes include resolved course metadata, and onboarding courses deduplicate normalized codes while preserving blank codes.
Enrollment-scoped study-guide queries
backend/routes/study_guide.py, backend/tests/test_study_guide_routes.py
Study-guide course and exam queries derive enrollment IDs and filter assignments by enrollment_id rather than user_id.

First-login achievement grant

Layer / File(s)Summary
Approved sign-in achievement processing
backend/routes/auth.py, backend/tests/test_auth_first_login_achievement.py
Approved Google sign-ins run an idempotent achievement check, while failures do not change redirect behavior; tests cover granting, deduplication, and failure handling.

Notetaker action handling

Layer / File(s)Summary
Deterministic notetaker handlers
backend/agents/function_handlers_e2e.py, backend/tests/test_e2e_function_handlers.py
Function-mode handlers return fixed summary, concept, and chat outputs and are verified during agent dispatch.
Notetaker error notifications
frontend/src/app/(shell)/notetaker/page.tsx
AI action failures produce humanized toast notifications for summarization, extraction, quiz generation, and tutor routing.

Frontend course and semester scoping

Layer / File(s)Summary
Multi-term course contract and predicates
frontend/src/lib/api.ts, frontend/src/lib/useActiveSemester.ts, frontend/src/lib/useActiveSemester.test.ts
Course data carries aggregated terms and enrollment IDs, and shared helpers normalize term membership and distinct-term lists.
Semester-aware course and concept filtering
frontend/src/components/screens/*, frontend/src/components/ManageCoursesModal.tsx
Course and concept views use normalized multi-term membership for active-semester filtering.

Settings profile fallback

Layer / File(s)Summary
Profile-backed settings initialization
frontend/src/components/screens/Settings.tsx, frontend/src/components/screens/Settings.test.tsx
Missing settings identity fields are initialized from the public profile, while explicit settings values take precedence and both cases are tested.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers:darkest-teddy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 36.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is specific and accurately summarizes the main cross-cutting fix set.
Description check✅ PassedThe description is mostly complete, covering summary, changes, testing, and notes, though it doesn't follow the exact template headings.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch worktree-fix-explore-findings-f1-f8
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-explore-findings-f1-f8

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging9a4f63eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:49 AM

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
backend/routes/onboarding.py (1)

30-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Dedup runs after the DB limit=20, so results can be silently truncated.

limit=20 (line 34) caps rows fetched before the Python-side course_code collapse (lines 37-45) runs. If several of those 20 rows share a course_code — exactly the scenario this PR's own tests describe (seed-* and rich-* demo schools both defining CS101/BIO110) — the returned list can end up far shorter than 20 distinct courses, even though more distinct courses exist beyond the fetch window. This defeats the purpose of the dedup fix for the very case it targets.

🐛 Proposed fix: over-fetch, then truncate after dedup
 rows = table("courses").select(
"id,course_code,course_name",
filters=filters,
order="course_name.asc",
- limit=20,+ limit=100, # over-fetch so post-filter dedup still yields up to PAGE_SIZE distinct codes
)
deduped = []
seen_codes = set()
for row in rows:
code = (row.get("course_code") or "").strip().casefold()
if code:
if code in seen_codes:
continue
seen_codes.add(code)
deduped.append(row)
+ if len(deduped) >= 20:+ break
return {"courses": deduped}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/onboarding.py` around lines 30 - 47, Adjust the courses query
and post-processing in the onboarding route so deduplication occurs over an
over-fetched result set, then truncate the deduplicated list to 20 entries
before returning. Preserve the existing case-insensitive, whitespace-normalized
course_code handling in the dedup loop and ensure the final courses list never
exceeds 20 distinct entries.
🧹 Nitpick comments (1)
backend/tests/test_auth_first_login_achievement.py (1)

41-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared mock Supabase fixture.

Replace the local table factory and dual table monkeypatches with the shared fixture from tests/conftest.py, then configure its rows for each scenario. This keeps route and service mocks aligned with the repository contract.

As per coding guidelines, “Backend tests belong under backend/tests/ and run with pytest; use shared mock Supabase and mock Gemini fixtures from tests/conftest.py.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/tests/test_auth_first_login_achievement.py` around lines 41 - 127,
Update drive_callback and _make_factory to use the shared mock Supabase fixture
from tests/conftest.py instead of creating a local table factory and patching
auth_module.table and ach_module.table independently. Configure the fixture’s
users, achievement_triggers, user_achievements, and related table rows for each
scenario while preserving insert capture for First Steps assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/study_guide.py`:
- Around line 51-73: Scope the exam lookup in _generate_and_insert to the
requested offering_id as well as the user’s enrollments, using the existing
enrollment/offering relationship or established offering-scoping helper. Ensure
an exam from another offering is rejected even when it belongs to the same user,
while preserving the current cross-user authorization check.
In `@frontend/src/components/screens/Settings.tsx`:
- Around line 94-98: Update the settings initialization fields in Settings.tsx
to use nullish fallback instead of truthiness fallback, replacing || with ?? for
display_name, username, bio, location, and website so explicitly empty strings
remain preserved while null or undefined values still use the profile fallback.
---
Outside diff comments:
In `@backend/routes/onboarding.py`:
- Around line 30-47: Adjust the courses query and post-processing in the
onboarding route so deduplication occurs over an over-fetched result set, then
truncate the deduplicated list to 20 entries before returning. Preserve the
existing case-insensitive, whitespace-normalized course_code handling in the
dedup loop and ensure the final courses list never exceeds 20 distinct entries.
---
Nitpick comments:
In `@backend/tests/test_auth_first_login_achievement.py`:
- Around line 41-127: Update drive_callback and _make_factory to use the shared
mock Supabase fixture from tests/conftest.py instead of creating a local table
factory and patching auth_module.table and ach_module.table independently.
Configure the fixture’s users, achievement_triggers, user_achievements, and
related table rows for each scenario while preserving insert capture for First
Steps assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 806d99e6-f9e1-47ca-b2af-d8bb9e66c1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a5f99 and 8324873.

📒 Files selected for processing (15)
  • backend/agents/function_handlers_e2e.py
  • backend/routes/auth.py
  • backend/routes/notes.py
  • backend/routes/onboarding.py
  • backend/routes/study_guide.py
  • backend/services/graph_service.py
  • backend/tests/test_auth_first_login_achievement.py
  • backend/tests/test_e2e_function_handlers.py
  • backend/tests/test_graph_service.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_onboarding_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/components/screens/Settings.test.tsx
  • frontend/src/components/screens/Settings.tsx

Comment on lines 51 to 73
def _generate_and_insert(user_id: str, offering_id: str, exam_id: str) -> dict:
"""Generate a study guide, insert it into study_guides, and return
{content, generated_at}.

Study guides + the documents that feed them key on the OFFERING (0025);
the caller resolves the abstract course id to an offering first.
"""
# 1. Fetch exam info
exams = table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]
exams = (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
if enrollment_ids
else []
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Exam lookup isn't scoped to the requested offering_id, allowing cross-course exam/offering mismatch.

_generate_and_insert receives offering_id and correctly scopes the documents query (line 90) and the persisted study_guides row (line 160) to it, but the exam lookup (lines 61-72) filters assignments by enrollment_id in (ALL of user_enrollment_ids(user_id)) — every enrollment across every course the user has, not just the one matching offering_id. A request with course_id=A (→ offering_id=X) plus an exam_id belonging to a different course the same user is enrolled in will pass this check and get persisted as a study_guides row tagged offering_id: X with content generated from the wrong course's exam. The comment above only guards against cross-user access, not cross-course mismatch for the same user.

🐛 Proposed fix: scope the exam lookup to the requested offering
- enrollment_ids = [e["id"] for e in user_enrollment_ids(user_id)]+ enrollment_ids = [+ e["id"] for e in user_enrollment_ids(user_id)+ if e.get("offering_id") == offering_id+ ]

Also worth adding a regression test with multiple enrollments across different offerings to catch this class of bug — none of the current TestGetGuide/TestRegenerateGuide/TestGenerationFailure tests exercise more than one enrollment.

As per coding guidelines, "study/analytics data on offering_id" should govern how study-guide data is resolved and scoped.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info
exams=table("assignments").select(
"id,user_id,title,due_date,assignment_type,course_id",
filters={"id": f"eq.{exam_id}", "user_id": f"eq.{user_id}"},
limit=1,
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [e["id"] foreinuser_enrollment_ids(user_id)]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
def_generate_and_insert(user_id: str, offering_id: str, exam_id: str) ->dict:
"""Generateastudyguide, insertitintostudy_guides, andreturn
{content, generated_at}.
Studyguides+thedocumentsthatfeedthemkeyontheOFFERING (0025);
thecallerresolvestheabstractcourseidtoanofferingfirst.
"""
# 1. Fetch exam info. Assignments key on enrollment_id (no user_id/course_id
# column); scope to the user's own enrollments so one user can't generate a
# guide off another's exam.
enrollment_ids= [
e["id"] foreinuser_enrollment_ids(user_id)
ife.get("offering_id") ==offering_id
]
exams= (
table("assignments").select(
"id,enrollment_id,title,due_date,assignment_type",
filters={
"id": f"eq.{exam_id}",
"enrollment_id": f"in.({','.join(enrollment_ids)})",
},
limit=1,
)
ifenrollment_ids
else []
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/study_guide.py` around lines 51 - 73, Scope the exam lookup in
_generate_and_insert to the requested offering_id as well as the user’s
enrollments, using the existing enrollment/offering relationship or established
offering-scoping helper. Ensure an exam from another offering is rejected even
when it belongs to the same user, while preserving the current cross-user
authorization check.

Source: Coding guidelines

Comment on lines +94 to +98
display_name: s.display_name || profile?.name || null,
username: s.username || profile?.username || null,
bio: s.bio || profile?.bio || null,
location: s.location || profile?.location || null,
website: s.website || profile?.website || null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve explicitly empty settings values.

|| treats "" as missing, so an intentionally cleared settings field is replaced by the public-profile value. Because these values seed the form, a later blur can write the fallback back and undo the user’s explicit value. Use ?? instead.

Proposed fix
- display_name: s.display_name || profile?.name || null,- username: s.username || profile?.username || null,- bio: s.bio || profile?.bio || null,- location: s.location || profile?.location || null,- website: s.website || profile?.website || null,+ display_name: s.display_name ?? profile?.name ?? null,+ username: s.username ?? profile?.username ?? null,+ bio: s.bio ?? profile?.bio ?? null,+ location: s.location ?? profile?.location ?? null,+ website: s.website ?? profile?.website ?? null,
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
display_name: s.display_name||profile?.name||null,
username: s.username||profile?.username||null,
bio: s.bio||profile?.bio||null,
location: s.location||profile?.location||null,
website: s.website||profile?.website||null,
display_name: s.display_name??profile?.name??null,
username: s.username??profile?.username??null,
bio: s.bio??profile?.bio??null,
location: s.location??profile?.location??null,
website: s.website??profile?.website??null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Settings.tsx` around lines 94 - 98, Update
the settings initialization fields in Settings.tsx to use nullish fallback
instead of truthiness fallback, replacing || with ?? for display_name, username,
bio, location, and website so explicitly empty strings remain preserved while
null or undefined values still use the profile fallback.

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. graph_service.get_courses collapses each course to a single row keeping only the most-recent enrollment's term, which breaks per-semester course filtering. Six frontend screens filter the getCourses() result by c.term === activeSemester, and distinctTerms() builds the semester tab bar from the same singular .term. A user enrolled in the same abstract course across two terms — e.g. CS101 in Fall 2025 and Spring 2026, exactly the scenario the new test test_collapses_duplicate_course_across_terms encodes — now gets one row with term: "Spring 2026". Selecting the Fall 2025 tab silently drops CS101 from the list/picker even though its Fall graph nodes still load, and if a re-take is a term's only course that tab can disappear entirely. The PR adds terms/enrollment_ids arrays to preserve the data but no frontend consumer was updated to read them, so this regresses the semester-scoping feature from feat: semester-scoped learning + Courses & Semesters hub #360. (bug due to the collapse keeping a single term)

terms[course_id] = []
reps[course_id] =r# last (most recent enrolled_at) wins as representative
eid=r.get("id")

The single term is then emitted per collapsed row here:

"color": r.get("color"),
"nickname": r.get("nickname"),
"term": r.get("term", ""),
"node_count": len(node_rows),

while consumers still filter by it per active semester, e.g.:

constscopedCourses=React.useMemo(
()=>(activeSemester ? courses.filter((c)=>c.term===activeSemester) : courses),
[courses,activeSemester],

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…ive term (PR #462 review)
The #449 get_courses collapse keeps one row per abstract course_id with only
the most-recent enrollment's singular `term`. But six screens filtered
`c.term === activeSemester` and the semester tab bar was built from that
singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 +
Spring 2026) dropped off its older-term tab — a regression of #360 semester
scoping (caught by the adversarial review on #462, confirmed live: the Fall
2025 dashboard showed "1 course" while rendering two course hubs).
- api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already
returns them since the collapse).
- useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term
MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array.
- Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`;
Quiz/Learn concepts now inherit the course `terms[]` so their (defensive)
semester filter matches too.
- useActiveSemester.test.ts: regression tests for membership + terms[] flatten.
Also closes the F4 follow-up: the POST /api/notes create route now returns the
resolved course_id/labels too (was "Unknown course" on a fresh note until reload).
Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs
resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150).
tsc clean, affected vitest 16 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Review finding addressed (ba6409e)

The term-collapse regression is fixed. get_courses still returns one row per abstract course_id (the #449 fix), but the frontend now scopes by term membership instead of the collapsed representative term:

  • new courseInTerm(course, activeSemester) helper ("" = All semesters) and distinctTerms now flattens the per-course terms[] array
  • Dashboard / Study / Tree / Quiz / Learn / ManageCoursesModal filter via courseInTerm; Quiz/Learn concepts inherit the course terms[] so their defensive semester filter matches too
  • useActiveSemester.test.ts gains regression tests for membership + terms[] flattening

Verified live in the browser (in-app date 2026-03-11, rich-user-active):

Semester tabCourses shownBefore
AllCS101, MATH210, BIO110, ENG150 (4)5 (dup) — fixed by #449
Fall 2025CS101, BIO110 ("2 courses")1 course (CS101 dropped)
Spring 2026CS101, MATH210ok
Summer 2026ENG150ok

CS101 (enrolled Fall 2025 + Spring 2026) now correctly appears on both tabs. tsc clean; affected vitest green.

Also folded in the F4 create-route follow-up (a freshly created note now returns its resolved course_id/labels instead of showing "Unknown course" until reload).

🤖 Generated with Claude Code

…findings-f1-f8
# Conflicts:
#	backend/tests/test_e2e_function_handlers.py
@AndresL230
AndresL230 merged commit 6290d9f into mainJul 30, 2026
7 checks passed
AndresL230 added a commit that referenced this pull request Jul 30, 2026
…view findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 30, 2026
* feat(study): semester-scope the study-tool reads (#141)
The approved reframe: NO Archive toggle. The existing semester selector
(Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters
DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL
reads the same way it already scopes the graph. The study endpoints
used to hardcode current-term resolution (resolve_offering(course_id)),
so under the frozen e2e clock (spring-2026 current) fall-2025 study
material was unreachable regardless of the user's selection.
Backend — optional `semester` (term LABEL via term_id_for_label) on the
course-scoped READ paths, resolved STRICTLY: an unknown label or a term
with no offering of the course degrades to each route's empty/404
behavior, never a silent fall-back to another term. New
`fallback=False` mode on academics.resolve_offering carries that rule
(the default create=False path used to silently resolve ANY offering of
the course on a term miss).
- study_guide: GET /{user}/guide + POST /regenerate (404 on a term
miss, and never generates for an offering that isn't there); GET
/{user}/exams scopes the enrollment set to the selected term.
- flashcards: GET /user/{user} filters cards to the selected term's
offerings (term-LESS cards stay visible under any selection); POST
/generate grounds its docs context in the selected term's offering
(a term miss contributes no docs — not all-docs, not current term).
import/commit stays a CREATE path: current term by design.
- notes: GET /user/{user} course-filtered read takes `semester`
(API completeness — the notetaker UI carries no semester context and
is deliberately NOT wired); the create/re-home paths stay
current-term by design (commented).
- quiz: untouched (no term resolution; scoping is client-side).
Frontend — Study.tsx threads `activeSemester || undefined` into
getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/
generateFlashcards, gated on the useActiveSemester hydrated flag
exactly like Dashboard (call-count pinned: one scoped fetch, never
unscoped-then-scoped).
Tests: backend route + resolver coverage for (a) no semester = existing
current-term behavior, (b) explicit term threads (course, term,
fallback=False), (c) unknown/no-offering term = empty/404 not 500;
vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and
the single-fetch hydration gate. New journey e2e/study-semester.spec.ts
(authored, not run here): All semesters shows the fall AND spring decks
together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics
deck and hides the spring-only one. No new agent tasks introduced; the
journey triggers no generation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(review): recent-guides open as their own term + close the #475 review findings
F1 (major): the recent-guides rail lists guides from ANY term, but opening
one resolved with the ACTIVE semester under #141's strict mode — a
multi-term course + other-term entry cache-missed on (offering, exam) and
silently generated-and-PERSISTED a mismatched row; a course absent from
the active term 404'd for a guide visibly in the sidebar. Invariant now:
a recent entry opens AS ITS OWN TERM.
- backend GET /{user}/cached: each entry carries its own `semester` label
(term_for_offering, lru-cached, offering ids deduped in the existing
enrichment loop); ETag key bumped to guides.v2 so bodies cached under
the old shape revalidate.
- frontend: StudyGuideCacheEntry.semester; openRecent records the entry's
term ("" = term-less entry -> explicitly unscoped) in a ref consumed by
exactly one load; loadGuide takes a per-load term override; the failed
state stores the term so retry replays the exact load. Picker-driven
loads keep following the active selector (unit-tested by driving the
real CustomSelects). The #476 emergent examId-clear behavior is
untouched and its tests stay green.
F3: _generate_and_insert scopes the exam lookup to the RESOLVED
offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no
longer generate a guide keyed on one term's offering from another term's
exam (regression test: 404, nothing persisted).
F2: flashcards _get_course_documents — a course-name miss WITH an
explicit semester now contributes no documents (an explicit term gives
the all-docs fallback nothing to anchor to); without a semester the
pre-existing all-docs fallback is byte-identical (both pinned).
F4: the guide 404 branch no longer hardcodes the exam-deleted copy —
the server's detail renders when it isn't the exam-deleted sentence
(no-offering-in-term case), still guidance, never a toast.
F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not
create; still deliberately current-term); api.ts study-guide block now
documents the pre-existing exams-list asymmetry (omitted semester =
current-term resolution for guide/regenerate but ALL terms for
getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear
to the courseId-keyed effect racing openRecent (#476), not to openRecent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the worktree-fix-explore-findings-f1-f8 branch August 2, 2026 18:30
Sign up for freeto 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

@AndresL230