feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

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

feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

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

feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

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

feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

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

feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

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

feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

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

feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

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

feat(study): semester-scope the study-tool reads (#141 reframe) - #475

Merged
AndresL230 merged 3 commits into
mainfrom
s141-study-semester
Jul 30, 2026
Merged

feat(study): semester-scope the study-tool reads (#141 reframe)#475
AndresL230 merged 3 commits into
mainfrom
s141-study-semester

Conversation

@AndresL230

@AndresL230AndresL230 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

#141, reframed (approved by Andres on 2026-07-30)

Closes#141.

No Archive toggle. The existing semester selector (All-semesters default untouched — the #360 e2e-vetoed contract) now scopes the study-tool read paths the way it already scopes the graph. Full analysis + reframe rationale: the corrected-state comment on #141.

Backend — optional semester (term label via academics.term_id_for_label), strict resolution

  • services/academics.py: resolve_offering(..., fallback: bool = True) — new strict mode. The existing create=False path silently falls back to any offering of the course; with an explicit semester that would have silently served another term's content. fallback=False returns None on a term miss. Additive, default-preserving; resolver tests pin both modes.
  • routes/study_guide.py: GET /{user}/guide?semester=, POST /regenerate (body), GET /{user}/exams?semester= — term miss → 404 (guide/regenerate) with the agent never invoked, exams filtered via term_for_offering.
  • routes/flashcards.py: GET /user/{user}?semester= — cards filtered to the term's offerings; term-less cards (offering_id NULL) stay visible under any selection; unknown label degrades to term-less-only, never 500. POST /generate grounds its docs context in the selected term's offering. import/commit stays current-term (comment).
  • routes/notes.py: course-filtered read takes semester; term miss → empty list. Create/re-home stay current-term by design (comments).
  • routes/quiz.py: untouched — no term resolution exists; quiz scoping is already client-side via the graph picker.
  • 17 new pytest cases (red-first): per-route current-term default / scoped / miss-degrades-not-500, plus the resolver modes.

Frontend

  • lib/api.ts: optional semester on getStudyGuideExams / getStudyGuide / regenerateStudyGuide / getFlashcards / generateFlashcards.
  • Study.tsx: useActiveSemester() threaded into both modes, fetches gated on the hydrated flag (Dashboard pattern, call-count-pinned — no unscoped-then-scoped double fetch). Notetaker deliberately not wired (no semester context on that screen; the notes param is API-completeness).
  • Journey e2e/study-semester.spec.ts: All-semesters default shows the fall + spring decks together; hub → "Fall 2025" → only the fall deck serves. No generation triggered (function-mode-seam safe); no new interactive elements → no surface registration needed.

Gates

Backend pytest tests/ -q → 1499 passed, 32 skipped; ruff check . clean. Frontend vitest 47 files / 353 tests; tsc --noEmit clean; eslint . 0 errors. Based on main @ 9edfcf5. Pre-merge flock'd e2e cycle to follow.

Note for a follow-up issue

Found pre-existing (not fixed here): opening a guide from the "Recent guides" rail clears the exam selection, leaving Regenerate permanently disabled on that path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added semester-aware filtering for flashcards, notes, exams, and study guides.
    • Flashcard generation and study-guide creation now use the selected semester’s course content.
    • Added support for switching between all semesters and a specific semester in Study.
    • Gracefully handles unknown semesters or courses without offerings.
  • Tests

    • Added coverage for semester filtering, generation, study-guide retrieval, and regeneration flows.
    • Added end-to-end validation for semester-specific flashcard decks.

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>
@supabase

supabaseBot commented Jul 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 30, 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:33 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: 8e286f7f-d8b0-4e86-bca2-2f154452fa56

📥 Commits

Reviewing files that changed from the base of the PR and between e6dfb0c and 07ad94e.

📒 Files selected for processing (9)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts
📝 Walkthrough

Walkthrough

Changes

Semester scoping

Layer / File(s)Summary
Strict offering resolution
backend/services/academics.py, backend/tests/test_academics.py
resolve_offering can disable cross-term fallback and is tested for matching and missing term offerings.
Term-scoped backend routes
backend/routes/flashcards.py, backend/routes/notes.py, backend/routes/study_guide.py
Flashcards, notes, exams, and study guides accept semester values and apply term-specific offering, document, enrollment, cache, and response filtering.
Backend route coverage
backend/tests/test_flashcards_routes.py, backend/tests/test_notes_routes.py, backend/tests/test_study_guide_routes.py
Route tests cover semester filtering, unknown terms, missing offerings, and regeneration targets.
Frontend API and study flow
frontend/src/lib/api.ts, frontend/src/components/screens/Study.tsx
API helpers and Study modes propagate the selected semester, gate loading on hydration, and scope retrieval, generation, and regeneration.
Frontend semester behavior coverage
frontend/src/components/screens/Study.semester.test.tsx, frontend/src/components/screens/Study.test.tsx, frontend/e2e/study-semester.spec.ts
Component and end-to-end tests verify selected-term propagation and filtered study-card visibility.

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

Sequence Diagram(s)

sequenceDiagram
participant SemesterState
participant Study
participant ApiHelpers
participant BackendRoutes
participant OfferingResolver
SemesterState-->>Study: hydrated semester selection
Study->>ApiHelpers: request study data with semester
ApiHelpers->>BackendRoutes: send semester query or payload
BackendRoutes->>OfferingResolver: resolve course offering strictly
OfferingResolver-->>BackendRoutes: offering_id or no match
BackendRoutes-->>ApiHelpers: scoped data or empty/404 response
ApiHelpers-->>Study: render term-scoped study content
Loading

Possibly related PRs

  • SaplingLearn/Sapling#462 — Updates study-guide exam and assignment lookup logic that this change further scopes by semester.

Suggested reviewers:jose-gael-cruz-lopez, darkest-teddy

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed, but it does not follow the required template sections like Description, Changes Made, Testing, and Notes.Restructure the PR text to match the template headings and add the missing Changes Made, Testing, Screenshots, and Notes sections.
Linked Issues check⚠️ WarningThe PR only partially matches #141 and does not implement the explicit Archive toggle or graph scoping described in the linked issue.Either implement the Archive-toggle/graph scope requirements from #141 or update the linked issue to reflect this reframed semester-selector approach.
Docstring Coverage⚠️ WarningDocstring coverage is 16.98% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately describes the main semester-scoping change to study-tool reads.
Out of Scope Changes check✅ PassedThe changes appear focused on semester-scoped study-tool reads and related tests, with no clear unrelated additions.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch s141-study-semester

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 30, 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-staging07ad94eCommit Preview URL

Branch Preview URL
Jul 30 2026, 05:09 PM

@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: 1

Caution

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

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

112-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Semester scope is dropped when the course name doesn't match any course.

The function's own docstring promises a term miss "never falls back... to all of the user's material," but that guarantee only holds inside the if course_rows: branch. When course_rows is empty (no course row matches course_name), the else branch unconditionally returns every one of the user's documents across all terms — even when semester was explicitly supplied. A semester-scoped generation request for a topic that doesn't exactly match a stored course name will silently leak material from every other term.

🐛 Proposed fix
 else:
- docs = table("documents").select(- "file_name,category,summary,concept_notes",- filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},- )+ if semester:+ # No course match at all — nothing to strictly scope to, so a+ # semester filter degrades to "no documents" instead of+ # leaking material from every term (`#141`).+ docs = []+ else:+ docs = table("documents").select(+ "file_name,category,summary,concept_notes",+ filters={"user_id": f"eq.{user_id}", "deleted_at": "is.null"},+ )

Worth adding a regression test alongside test_semester_with_no_offering_yields_no_docs_not_all_docs for this "no course row at all" case in backend/tests/test_flashcards_routes.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/routes/flashcards.py` around lines 112 - 155, Update
_get_course_documents so an explicitly supplied semester never falls back to all
user documents when course_rows is empty; return no documents for that scoped
miss, while preserving the existing all-documents fallback only when semester is
absent. Add a regression test in test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
🧹 Nitpick comments (2)
frontend/e2e/study-semester.spec.ts (1)

31-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a database assertion for the seeded semester contract.

The UI assertions alone cannot distinguish correct term filtering from incorrect seeded data. Assert the Fall/current offering-card setup through support/db.ts as well. As per coding guidelines, “E2E journeys should use the fixtures-based test from support/fixtures.ts, database assertions from support/db.ts.”

🤖 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/e2e/study-semester.spec.ts` around lines 31 - 53, Add database
assertions to the semester-selection test using the fixtures-based test and
helpers from support/db.ts, verifying the seeded Fall 2025/current offering-card
setup before the UI flow runs. Keep the existing UI assertions and ensure the
database checks confirm the expected Fall deck/cards and semester relationship.

Source: Coding guidelines

backend/routes/flashcards.py (1)

286-305: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unscoped course_offerings query for the term filter.

The course_offerings lookup fetches every offering in the target term platform-wide, rather than just the offering ids actually referenced by this user's already-fetched rows. Scoping the filter to the offering ids present in rows (via an in.(...) filter) would keep this query bounded by the user's own data instead of the term's total size.

♻️ Proposed refactor
 if semester:
term_id = term_id_for_label(semester)
allowed: set[str] = set()
if term_id:
+ offering_ids = {r["offering_id"] for r in rows if r.get("offering_id")}+ if offering_ids:+ offs = table("course_offerings").select(+ "id",+ filters={+ "term_id": f"eq.{term_id}",+ "id": f"in.({','.join(offering_ids)})",+ },+ ) or []+ allowed = {o["id"] for o in offs if o.get("id")}- offs = table("course_offerings").select(- "id", filters={"term_id": f"eq.{term_id}"}- ) or []- allowed = {o["id"] for o in offs if o.get("id")}
🤖 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/flashcards.py` around lines 286 - 305, Scope the
`course_offerings` lookup in the semester-filtering block to offering IDs
referenced by the user’s existing `rows`, using an `in.(...)` filter alongside
the term filter. Build the ID set from non-null `rows` offering IDs and preserve
the current term-less-card visibility and unknown-term behavior.
🤖 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 `@frontend/src/components/screens/Study.semester.test.tsx`:
- Around line 132-148: Extend the Study screen tests around the regeneration
flow to cover the changed regenerateStudyGuide call. Set up selectable course
and exam state, click Regenerate, and assert the call includes "Fall 2025" when
the active semester is stored and undefined when it is absent, while preserving
the existing fetch assertions.
---
Outside diff comments:
In `@backend/routes/flashcards.py`:
- Around line 112-155: Update _get_course_documents so an explicitly supplied
semester never falls back to all user documents when course_rows is empty;
return no documents for that scoped miss, while preserving the existing
all-documents fallback only when semester is absent. Add a regression test in
test_flashcards_routes.py alongside
test_semester_with_no_offering_yields_no_docs_not_all_docs for a course name
with no matching row.
---
Nitpick comments:
In `@backend/routes/flashcards.py`:
- Around line 286-305: Scope the `course_offerings` lookup in the
semester-filtering block to offering IDs referenced by the user’s existing
`rows`, using an `in.(...)` filter alongside the term filter. Build the ID set
from non-null `rows` offering IDs and preserve the current term-less-card
visibility and unknown-term behavior.
In `@frontend/e2e/study-semester.spec.ts`:
- Around line 31-53: Add database assertions to the semester-selection test
using the fixtures-based test and helpers from support/db.ts, verifying the
seeded Fall 2025/current offering-card setup before the UI flow runs. Keep the
existing UI assertions and ensure the database checks confirm the expected Fall
deck/cards and semester relationship.
🪄 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: 7d7668eb-80c9-470e-98fb-b67a38b5c543

📥 Commits

Reviewing files that changed from the base of the PR and between 9edfcf5 and e6dfb0c.

📒 Files selected for processing (13)
  • backend/routes/flashcards.py
  • backend/routes/notes.py
  • backend/routes/study_guide.py
  • backend/services/academics.py
  • backend/tests/test_academics.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_notes_routes.py
  • backend/tests/test_study_guide_routes.py
  • frontend/e2e/study-semester.spec.ts
  • frontend/src/components/screens/Study.semester.test.tsx
  • frontend/src/components/screens/Study.test.tsx
  • frontend/src/components/screens/Study.tsx
  • frontend/src/lib/api.ts

Comment threadfrontend/src/components/screens/Study.semester.test.tsx Outdated
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. Recent-guides cross-term flow: getCachedStudyGuides lists guides from ANY term, but openRecentloadGuide resolves the offering with the ACTIVE selector's semester under the PR's new strict (fallback=False) resolution. A recent entry from another term either (a) on a multi-term course, misses the (offering_id, exam_id) cache row and falls into _generate_and_insert, silently generating and PERSISTING a study_guides row pairing the active term's offering with the other term's exam, or (b) 404s ("No offering of this course in that semester.") for a guide visibly listed in the sidebar (bug due to the recent entry's own term never being threaded through Study.tsx::openRecent/loadGuide)

constloadGuide=React.useCallback(async(cid: string,eid: string)=>{
if(!userId)return;
setLoadingGuide(true);
setGuideProblem(null);
try{
constr=awaitgetStudyGuide(userId,cid,eid,semester||undefined);
setGuide(r.guide);
setGeneratedAt(r.generated_at);
setCached(r.cached);
if(!r.cached)loadRecent();
}catch(err){
console.error("study guide load failed",err);
setGuide(null);
if(isNotFound(err)){
setGuideProblem({kind: "missing"});
}else{
constmessage=humanizeError(err,"Couldn't build that study guide.");
setGuideProblem({kind: "failed", message,courseId: cid,examId: eid});
toast.error(message);
}
}finally{
setLoadingGuide(false);
}
},[userId,toast,loadRecent,semester]);
React.useEffect(()=>{
if(courseId&&examId)loadGuide(courseId,examId);
},[courseId,examId,loadGuide]);
constopenRecent=(entry: StudyGuideCacheEntry)=>{
setCourseId(entry.course_id);
setExamId(entry.exam_id);
};

Sub-threshold notes (verified, all being fixed in a follow-up commit, scores <80): _get_course_documents' course-name-miss branch returns ALL user docs ignoring semester — reachable via topic-pill generation on "All courses" and contradicting the new docstring (75); _generate_and_insert's exam lookup validates against every enrollment, not the resolved offering — the unapplied #462 CodeRabbit Critical, which the recent-guides path above makes reachable (75); the new no-offering 404 renders the misleading "exam was deleted" copy on the read path while regenerate surfaces the real detail (75); three new-comment accuracy fixes (PATCH re-home mislabeled CREATE; api.ts exams comment claims current-term where unscoped is all-terms; a test comment misattributes the exam-clear to openRecent) (75/75/50).

🤖 Generated with Claude Code

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

AndresL230and others added 2 commits July 30, 2026 10:03
…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>
…pill share the name under All semesters)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit d1d724f into mainJul 30, 2026
6 of 7 checks passed
AndresL230 added a commit that referenced this pull request Jul 31, 2026
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 31, 2026
…#499)
* fix(study): keep the exam selected when opening a recent guide (#476)
Opening a guide from the "Recent guides" rail left Regenerate permanently
disabled. The cause is not the open path — openRecent sets courseId AND
examId together. It's the courseId-keyed exams effect, which opened with an
unconditional setExamId(""): a scope reset that cannot tell "the user
switched course" (selection now invalid) from "we just opened a specific
guide" (selection deliberate and valid).
Both effects run in the same commit, so the loader still saw the intact pair
and the guide loaded; only the NEXT render lost the exam. Hence the symptom —
a guide on screen above a dead Regenerate button — rather than "nothing
opens". It needs a course CHANGE to reproduce, which is why a rail entry for
the already-selected course always worked (pinned as a control test).
The reset now happens at the two events that mean it: the course picker's
onChange, and a term switch. The term case adjusts state during render (the
StudyModePanel pattern already in this file) rather than in an effect,
because an effect-time reset lands a render late — the loader would commit
one read of the old exam under the new term first. That was the same defect's
second trigger, and it now has a test.
Making Regenerate reachable on the rail path exposed a term hazard: it sent
the ACTIVE selector's term, while a recent entry opens under its OWN term
(#475 F1). Regenerating a Fall guide as Spring would rebuild against an
offering the displayed guide never came from. Regenerate now replays the term
the displayed guide was loaded with.
Also seeds a CACHED study guide in the rich local dataset so the e2e journey
can open the rail without generating (the study_guide agent has no
function-mode handler).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(study): only reset the exam when the course actually changes
Code review caught a regression this PR introduced. Moving the reset out of
the courseId-keyed effect and onto the picker's onChange dropped a guard the
effect had for free: setCourseId(sameValue) bails out, so the effect never
re-ran. CustomSelect.commit() fires onChange for the already-selected option
too, so re-confirming the course you were already on wiped the guide you were
reading — the same class of bug as #476 itself.
Guard selectCourse on an actual change, and pin it with a test.
Also records the known cross-term edge the fix leaves standing: the exam
OPTIONS follow the active selector by #475's design, so a rail entry opened
under a different term is absent from that list and the picker shows its
placeholder while the guide and Regenerate are live and correctly aimed at
loadedTerm. Squaring it means tracking "the term I'm viewing" across list and
loads, which is a #475 change rather than part of this fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the s141-study-semester 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.

[P3] Semesters: archive toggle for knowledge graph & study tools

1 participant

@AndresL230