Uh oh!
There was an error while loading. Please reload this page.
feat: semester-scoped learning + Courses & Semesters hub - #360
Conversation
Warning Review limit reached
Next review available in:3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (35)
📝 WalkthroughWalkthroughAdds semester-scoped graph and learning data across backend and frontend, persists the active semester, updates course management UI, enforces cross-term no-retakes, adds category-aware prose chunking, and strengthens tutor academic-integrity prompts. ChangesSemester-scoped learning
Category-aware document chunking
Academic-integrity prompt guidance
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Deploying with |
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs | frontend-staging | b3d80a6 | Commit Preview URL Branch Preview URL | Jul 29 2026, 11:55 AM |
| import pytest | ||
| from unittest.mock import MagicMock, patch | ||
| import services.graph_service as gs |
Darkest-Teddy
commented
Jul 21, 2026
Added: per-type document chunking (commits |
Darkest-Teddy
commented
Jul 21, 2026
Added: tutor academic-integrity rule (commit |
main merged a separate semester feature (#140: Dashboard current-vs-archive split + getSemesters/@lib/semesters). Reconciled under "active-semester wins": the persisted active-semester selector is the single source of truth for the Dashboard graph + course panels; #140's current/archive drawer is removed, but its getSemesters()/sort_key data is reused so the default active term resolves to the most-recent enrolled term by sort_key (courseTermLabels), fixing #360's documented enrollment-order heuristic. Conflict resolutions: - Dashboard.tsx: fetch graph/recs scoped to activeSemester AND getSemesters; drop partition/currentProgress/archivedProgress/CoursesArchive; CoursesKey and the legacy panel render the scoped courseProgress; default term via courseTermLabels(cs, sems)[0] with resolveActiveSemester fallback. - ManageCoursesModal.tsx: keep the term-tabs selector (drop groupCoursesByTerm). - Quiz.tsx: pass scopedConcepts + scopedCourses (scope the picker too). - api.ts / data.test.ts / DocumentUploadModal.test.tsx: de-duplicate the EnrolledCourse.term field both branches added. - Dashboard.test.tsx: rewrite the #140 archive tests as active-semester scoping tests (default by sort_key, storage-driven scope, label-rank degradation). Verified locally: frontend typecheck clean, lint 0 errors, 196 tests pass; backend 1107 pass / 23 skipped (1 pre-existing OCR event-loop error, unrelated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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)
frontend/src/components/screens/Dashboard.tsx (1)
1276-1280: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the semester-empty state reachable.
This branch can never render:
CoursesKeyreturnsnullat Line 1133 whenevercourseProgress.length === 0. Remove or adjust that early return so an active semester with no courses shows this message and still exposes course management.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/screens/Dashboard.tsx` around lines 1276 - 1280, Update CoursesKey so it does not return null when courseProgress.length is zero for an active semester; allow rendering to continue to the “Nothing enrolled this semester.” state while preserving course management controls.
🧹 Nitpick comments (1)
backend/tests/test_shared_course_context.py (1)
471-478: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover every tutor mode claimed by the test.
The docstring promises the rule is present “regardless of mode,” but this test exercises only
expository. Parameterize it over the supported modes so a mode-specific prompt change cannot bypass the integrity rule.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/tests/test_shared_course_context.py` around lines 471 - 478, Update test_build_system_prompt_carries_academic_integrity_rule to run across every supported tutor mode instead of only "expository". Parameterize the test using the existing test framework conventions, while preserving the prompt construction and both academic-integrity assertions for each mode.
🤖 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/Dashboard.tsx`:
- Around line 265-282: Update the dashboard loading flow around the Promise.all
requests so courses and semesters are fetched first, then derive an
effectiveSemester from the stored activeSemester or the existing
courseTermLabels/resolveActiveSemester fallback before requesting graph and
recommendation data. Pass effectiveSemester to getGraph and getRecommendations,
preserving unscoped requests only when no semester can be resolved; ensure the
default path passes “Spring 2026” rather than undefined and persists that
selected semester.
---
Outside diff comments:
In `@frontend/src/components/screens/Dashboard.tsx`:
- Around line 1276-1280: Update CoursesKey so it does not return null when
courseProgress.length is zero for an active semester; allow rendering to
continue to the “Nothing enrolled this semester.” state while preserving course
management controls.
---
Nitpick comments:
In `@backend/tests/test_shared_course_context.py`:
- Around line 471-478: Update
test_build_system_prompt_carries_academic_integrity_rule to run across every
supported tutor mode instead of only "expository". Parameterize the test using
the existing test framework conventions, while preserving the prompt
construction and both academic-integrity assertions for each mode.
🪄 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: 6eedc26e-16af-4a02-b10b-c42176be693d
📒 Files selected for processing (9)
backend/prompts/preamble.txtbackend/tests/test_shared_course_context.pyfrontend/eslint-suppressions.jsonfrontend/src/components/screens/Dashboard.test.tsxfrontend/src/components/screens/Dashboard.tsxfrontend/src/components/screens/Learn.tsxfrontend/src/components/screens/Quiz.tsxfrontend/src/components/screens/Study.tsxfrontend/src/lib/api.ts
💤 Files with no reviewable changes (1)
- frontend/eslint-suppressions.json
🚧 Files skipped from review as they are similar to previous changes (4)
- frontend/src/lib/api.ts
- frontend/src/components/screens/Quiz.tsx
- frontend/src/components/screens/Study.tsx
- frontend/src/components/screens/Learn.tsx
| const [graphRes, coursesRes, assignsRes, sessionsRes, recsRes, semestersRes] = await Promise.all([ | ||
| getGraph(userId), | ||
| getGraph(userId, activeSemester || undefined), | ||
| getCourses(userId), | ||
| getUpcomingAssignments(userId), | ||
| getSessions(userId, 10), | ||
| getRecommendations(userId).catch(() => ({ recommendations: [] })), | ||
| // Term calendar is a nicety: without it the course lists stay flat | ||
| // rather than the dashboard failing to load. | ||
| getRecommendations(userId, activeSemester || undefined).catch(() => ({ recommendations: [] })), | ||
| // Term calendar drives the default active-semester resolution below; | ||
| // if it fails we fall back to enrollment order rather than failing the load. | ||
| getSemesters().catch(() => ({ semesters: [] })), | ||
| ]); | ||
| const cs = coursesRes.courses || []; | ||
| const sems = semestersRes.semesters || []; | ||
| setCourses(cs); | ||
| setSemesters(semestersRes.semesters || []); | ||
| // First run with no stored semester: default to the most-recent enrolled | ||
| // term by `sort_key` (courseTermLabels), falling back to enrollment order. | ||
| if (!activeSemester) { | ||
| const def = courseTermLabels(cs, sems)[0] || resolveActiveSemester("", cs); | ||
| if (def) setActiveSemester(def); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Resolve the default semester before requesting graph data.
With no stored semester, Lines 266 and 270 request unscoped data, then Line 281 selects and persists a term. This briefly renders unscoped graph/stats/recommendations and performs a second fetch. Fetch courses/semesters first, derive an effectiveSemester, then use it for the graph and recommendations requests.
Also assert the default-path getGraph call receives "Spring 2026" rather than undefined.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Dashboard.tsx` around lines 265 - 282, Update
the dashboard loading flow around the Promise.all requests so courses and
semesters are fetched first, then derive an effectiveSemester from the stored
activeSemester or the existing courseTermLabels/resolveActiveSemester fallback
before requesting graph and recommendation data. Pass effectiveSemester to
getGraph and getRecommendations, preserving unscoped requests only when no
semester can be resolved; ensure the default path passes “Spring 2026” rather
than undefined and persists that selected semester.
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current main as a single commit, resolving the drift accumulated since the merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355 subject-root dedup, the #352 content-hash chunk ids, and the #140 current/archive dashboard split, which the semester tabs supersede per the branch's own reconciliation). Backend semester scoping: - services/academics.py: term_id_for_label (semester label -> term id, with term-id fallback) + user_course_ids_for_term (enrollments -> offerings -> term-filtered course ids). - services/graph_service.py: get_graph/get_recommendations accept an optional semester label; nodes/edges/stats and the synthesized subject roots are restricted to that term's courses (composes with the #355 per-course subject-root dedup — roots are built from the already- filtered enrollment list). - add_course: no-retake rule across ALL terms (returns already_existed with the existing term label) and an optional term label so the hub enrolls into the tab being viewed, falling back to the current term. - routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and /recommendations; AddCourseBody gains term. - routes/gradebook.py: private _term_id_for_semester duplicate removed; call sites now use the shared academics.term_id_for_label (follow-up noted on the original PR). Frontend semester scoping: - lib/useActiveSemester.ts: localStorage-backed active-semester hook (cross-tab sync, hydration flag so first fetches are scoped once). - Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active semester and scope course/concept pickers to it; Learn's scoping was redone by hand against main's SSE-streaming Learn.tsx (pass the active semester into the bootstrap getGraph, term on concepts, scopedCourses for the course select, TopicPicker term filter — main's own suggest/highlight logic kept, not duplicated). - ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs, enroll-into-tab, no-retake feedback); the Dashboard archive rail from #140 is replaced by the active-semester scoping. - vitest.setup.ts: install an in-memory localStorage when the jsdom environment exposes none (jsdom 29 under Vitest leaves window.localStorage undefined), so useActiveSemester/useLayoutPref are testable in DOM tests. Per-type document chunking + tutor integrity: - services/chunker.py: chunk_for_category routes prose-like categories (essays/assignments) to a sentence-aware prose chunker, others to the existing chunker; routes/documents.py passes the doc category through; backfill script follows. Composes with #352's content-addressed chunk ids (sha256 over chunk text — only boundaries change). - prompts/preamble.txt: academic-integrity rule for the tutor (guide, never hand over graded-work answers). Conflict resolutions: test_document_indexing.py keeps both main's #439 relevance-gate test (repointed at the chunk_for_category seam) and the branch's category-passthrough test; Dashboard.tsx keeps main's IS_TEST_MODE/now + #369 learnHrefForNode imports alongside courseTermLabels; eslint-suppressions.json re-baselined (Dashboard no-restricted-syntax count drops with the removed archive UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3369301 to
a53e809CompareAndresL230
commented
Jul 29, 2026
Code reviewFound 3 issues:
Sapling/backend/services/graph_service.py Lines 372 to 396 in a53e809
Sapling/frontend/src/components/screens/Dashboard.tsx Lines 1129 to 1131 in a53e809
Sapling/frontend/src/lib/useActiveSemester.ts Lines 36 to 72 in a53e809 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current main as a single commit, resolving the drift accumulated since the merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355 subject-root dedup, the #352 content-hash chunk ids, and the #140 current/archive dashboard split, which the semester tabs supersede per the branch's own reconciliation). Backend semester scoping: - services/academics.py: term_id_for_label (semester label -> term id, with term-id fallback) + user_course_ids_for_term (enrollments -> offerings -> term-filtered course ids). - services/graph_service.py: get_graph/get_recommendations accept an optional semester label; nodes/edges/stats and the synthesized subject roots are restricted to that term's courses (composes with the #355 per-course subject-root dedup — roots are built from the already- filtered enrollment list). - add_course: no-retake rule across ALL terms (returns already_existed with the existing term label) and an optional term label so the hub enrolls into the tab being viewed, falling back to the current term. - routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and /recommendations; AddCourseBody gains term. - routes/gradebook.py: private _term_id_for_semester duplicate removed; call sites now use the shared academics.term_id_for_label (follow-up noted on the original PR). Frontend semester scoping: - lib/useActiveSemester.ts: localStorage-backed active-semester hook (cross-tab sync, hydration flag so first fetches are scoped once). - Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active semester and scope course/concept pickers to it; Learn's scoping was redone by hand against main's SSE-streaming Learn.tsx (pass the active semester into the bootstrap getGraph, term on concepts, scopedCourses for the course select, TopicPicker term filter — main's own suggest/highlight logic kept, not duplicated). - ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs, enroll-into-tab, no-retake feedback); the Dashboard archive rail from #140 is replaced by the active-semester scoping. - vitest.setup.ts: install an in-memory localStorage when the jsdom environment exposes none (jsdom 29 under Vitest leaves window.localStorage undefined), so useActiveSemester/useLayoutPref are testable in DOM tests. Per-type document chunking + tutor integrity: - services/chunker.py: chunk_for_category routes prose-like categories (essays/assignments) to a sentence-aware prose chunker, others to the existing chunker; routes/documents.py passes the doc category through; backfill script follows. Composes with #352's content-addressed chunk ids (sha256 over chunk text — only boundaries change). - prompts/preamble.txt: academic-integrity rule for the tutor (guide, never hand over graded-work answers). Conflict resolutions: test_document_indexing.py keeps both main's #439 relevance-gate test (repointed at the chunk_for_category seam) and the branch's category-passthrough test; Dashboard.tsx keeps main's IS_TEST_MODE/now + #369 learnHrefForNode imports alongside courseTermLabels; eslint-suppressions.json re-baselined (Dashboard no-restricted-syntax count drops with the removed archive UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gle scoped dashboard fetch Review follow-ups on the semester-scoped learning rework (#360): - add_course race (A): new partial unique index (migration 0036) closes the NULL-section gap in course_offerings_unique; academics.resolve_offering (create=True) now catches the 409 from a lost create race and re-selects the winner's offering; the ManageCoursesModal Add button disables while an add request is in flight. The now-unreachable per-offering already-enrolled check in add_course is removed and the single return contract documented. - add toast honesty (B): handleAdd reads the response — already_existed shows an informational "Already taken in <term>" toast instead of a false success; success only toasts when a row was created. - CoursesKey empty state (C): "Nothing enrolled this semester." is reachable — the key stays rendered when courses exist but the active semester scopes to none; still null when there are no courses at all. - dashboard first load (D): with no stored semester, resolve + persist the default (courses + semesters fetch) BEFORE the scoped fetch and early-return; the effect re-run performs the single scoped fetch. Zero-courses stays a single unscoped pass. - cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester (termLabels) persists a default when none is stored, called from Learn/Quiz/Tree/Study once course term labels are in hand. - prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm, UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks. Tests: resolve_offering conflict-retry + non-409 propagation units; add_course duplicate-path contract; ManageCoursesModal component tests for the already-existed/success/in-flight paths; Dashboard single-scoped-fetch, zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester units. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230
commented
Jul 29, 2026
All three review findings above are fixed in the latest push: (1) the no-retake race is closed by migration 0036's partial unique index on course_offerings (course_id, term_id) WHERE section IS NULL, a 409-retry re-select in resolve_offering, and an in-flight disable on the hub's Add button; (2) CoursesKey now renders the 'Nothing enrolled this semester.' empty-state when an active semester scopes to zero courses; (3) Learn/Quiz/Tree/Study now resolve a default semester via ensureDefaultActiveSemester as soon as they have term labels. Also: the Add flow honors already_existed with an informational toast, the dead duplicate check was removed, first-load double-fetch eliminated, and the orphaned #140 archive helpers were deleted. Backend 1311 passed; frontend 246 passed. |
a53e809 to
9531039Compare…e veto) The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 / Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the auto-resolved term silently hid cross-term fixtures (dashboard lost MATH210; the graph shrank to 1 of 17 nodes). New semantics: - Default = ALL SEMESTERS (unscoped). An empty stored active-semester value IS the default and means "All semesters": removed ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites, Dashboard's default-resolution early-return pass (deferredToScopedPass) and its getSemesters/courseTermLabels/resolveActiveSemester usage. resolveActiveSemester itself is deleted (orphaned — the hub now reads the stored value raw). Hook, change events, and hydration gating stay. - Hub UI: ManageCoursesModal grows an explicit "All semesters" tab — active when the stored value is empty, clicking it clears the value; term tabs unchanged (both carry aria-pressed). A picked term still persists and scopes every surface. - Kept from the review batch: CoursesKey empty-state reachability, the already_existed info toast, in-flight Add disable, the 0036 partial unique index + resolve_offering conflict retry, semesters.ts pruning. - New journey frontend/e2e/semester-scope.spec.ts: default shows cross-term courses together (MATH210 + BIO110), picking Fall 2025 in the hub hides Spring-only MATH210, "All semesters" restores it. Hub opens via new dashboard-courses-manage testid (documented in docs/frontend-testids.md); tabs selected by role/name. - Tests reworked: Dashboard default test now asserts one unscoped fetch with both terms visible; /api/semesters-failure test dropped (no semesters fetch remains); ensureDefault unit tests replaced by hub tab component tests (All active by default / term persists / All clears). - Design doc amendment records the veto and the opt-in scoping decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
…ive term (PR #462 review) The #449 get_courses collapse keeps one row per abstract course_id with only the most-recent enrollment's singular `term`. But six screens filtered `c.term === activeSemester` and the semester tab bar was built from that singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 + Spring 2026) dropped off its older-term tab — a regression of #360 semester scoping (caught by the adversarial review on #462, confirmed live: the Fall 2025 dashboard showed "1 course" while rendering two course hubs). - api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already returns them since the collapse). - useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array. - Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`; Quiz/Learn concepts now inherit the course `terms[]` so their (defensive) semester filter matches too. - useActiveSemester.test.ts: regression tests for membership + terms[] flatten. Also closes the F4 follow-up: the POST /api/notes create route now returns the resolved course_id/labels too (was "Unknown course" on a fresh note until reload). Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150). tsc clean, affected vitest 16 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(explore): resolve F1–F8 from the Chapter 2 exploration sweep Fixes the eight findings from the 2026-07-29 /explore session (.explore/findings.md): - F6 study_guide: query `assignments` by enrollment_id, not the phantom user_id/course_id columns (enrollment-keyed table) — get_exams, _generate_and_insert, and get_courses all 500'd, bricking the whole study-guide feature. get_courses now delegates to graph_service.get_courses. - F5a agents/function_handlers_e2e: register note_summary / note_concepts / note_chat handlers — these are request-path agent tasks that 500'd with UnregisteredHandlerError in function mode. - F4 notes: /api/notes/user, the single-note read, and create now return the abstract course_id + course_code/name resolved from the offering (every note showed "Unknown course"). - F1/F3 graph_service.get_courses: collapse the per-enrollment fan-out to one row per course_id (most-recent enrollment as representative; node_count counted once; additive enrollment_ids/terms lists). Fixes the dashboard count, /tree chips, and every course picker (#449). - F2 onboarding.search_courses: dedup catalog results by course code so the rich/base seed same-code courses don't show as indistinguishable dupes. - F7 auth: fire an idempotent login-streak achievement check on approved Google sign-in so "First Steps" is actually granted (test-login left untouched — it contractually performs no DB writes). - F5b notetaker: surface toast.error(humanizeError(...)) on failed agent actions (Summarize/Extract/Generate quiz/Send to tutor were silent no-ops). - F8 Settings: prefill the profile form from the profile fetch so name/username aren't blank (data-loss risk on save). Verification: 1325 backend tests pass (+14 new regression tests), ruff + tsc clean; live backend re-check of F1/F2/F4/F5a/F6 all pass and the e2e oracles return 0 findings (down from 6). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(courses): scope by term membership, not the collapsed representative term (PR #462 review) The #449 get_courses collapse keeps one row per abstract course_id with only the most-recent enrollment's singular `term`. But six screens filtered `c.term === activeSemester` and the semester tab bar was built from that singular `term`, so a course enrolled across two terms (CS101 in Fall 2025 + Spring 2026) dropped off its older-term tab — a regression of #360 semester scoping (caught by the adversarial review on #462, confirmed live: the Fall 2025 dashboard showed "1 course" while rendering two course hubs). - api.ts: document `terms`/`enrollment_ids` on EnrolledCourse (backend already returns them since the collapse). - useActiveSemester.ts: add `courseInTerm(course, activeSemester)` (term MEMBERSHIP; "" = all) and flatten `distinctTerms` over the `terms[]` array. - Dashboard/Study/Tree/Quiz/Learn/ManageCoursesModal: filter via `courseInTerm`; Quiz/Learn concepts now inherit the course `terms[]` so their (defensive) semester filter matches too. - useActiveSemester.test.ts: regression tests for membership + terms[] flatten. Also closes the F4 follow-up: the POST /api/notes create route now returns the resolved course_id/labels too (was "Unknown course" on a fresh note until reload). Verified live (Fall 2025 tab): CS101 + BIO110 both show, "2 courses"; all tabs resolve correctly (Fall→CS101,BIO110 / Spring→CS101,MATH210 / Summer→ENG150). tsc clean, affected vitest 16 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(study): semester-scope the study-tool reads (#141) The approved reframe: NO Archive toggle. The existing semester selector (Courses & Semesters hub -> lib/useActiveSemester, "" = All semesters DEFAULT — untouched, e2e-pinned per #360) now scopes the STUDY-TOOL reads the same way it already scopes the graph. The study endpoints used to hardcode current-term resolution (resolve_offering(course_id)), so under the frozen e2e clock (spring-2026 current) fall-2025 study material was unreachable regardless of the user's selection. Backend — optional `semester` (term LABEL via term_id_for_label) on the course-scoped READ paths, resolved STRICTLY: an unknown label or a term with no offering of the course degrades to each route's empty/404 behavior, never a silent fall-back to another term. New `fallback=False` mode on academics.resolve_offering carries that rule (the default create=False path used to silently resolve ANY offering of the course on a term miss). - study_guide: GET /{user}/guide + POST /regenerate (404 on a term miss, and never generates for an offering that isn't there); GET /{user}/exams scopes the enrollment set to the selected term. - flashcards: GET /user/{user} filters cards to the selected term's offerings (term-LESS cards stay visible under any selection); POST /generate grounds its docs context in the selected term's offering (a term miss contributes no docs — not all-docs, not current term). import/commit stays a CREATE path: current term by design. - notes: GET /user/{user} course-filtered read takes `semester` (API completeness — the notetaker UI carries no semester context and is deliberately NOT wired); the create/re-home paths stay current-term by design (commented). - quiz: untouched (no term resolution; scoping is client-side). Frontend — Study.tsx threads `activeSemester || undefined` into getStudyGuideExams/getStudyGuide/regenerateStudyGuide/getFlashcards/ generateFlashcards, gated on the useActiveSemester hydrated flag exactly like Dashboard (call-count pinned: one scoped fetch, never unscoped-then-scoped). Tests: backend route + resolver coverage for (a) no semester = existing current-term behavior, (b) explicit term threads (course, term, fallback=False), (c) unknown/no-offering term = empty/404 not 500; vitest Study.semester.test.tsx pins the scoped/unscoped fetch args and the single-fetch hydration gate. New journey e2e/study-semester.spec.ts (authored, not run here): All semesters shows the fall AND spring decks together; picking Fall 2025 in the hub surfaces the fall-2025 CS Basics deck and hides the spring-only one. No new agent tasks introduced; the journey triggers no generation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): recent-guides open as their own term + close the #475 review findings F1 (major): the recent-guides rail lists guides from ANY term, but opening one resolved with the ACTIVE semester under #141's strict mode — a multi-term course + other-term entry cache-missed on (offering, exam) and silently generated-and-PERSISTED a mismatched row; a course absent from the active term 404'd for a guide visibly in the sidebar. Invariant now: a recent entry opens AS ITS OWN TERM. - backend GET /{user}/cached: each entry carries its own `semester` label (term_for_offering, lru-cached, offering ids deduped in the existing enrichment loop); ETag key bumped to guides.v2 so bodies cached under the old shape revalidate. - frontend: StudyGuideCacheEntry.semester; openRecent records the entry's term ("" = term-less entry -> explicitly unscoped) in a ref consumed by exactly one load; loadGuide takes a per-load term override; the failed state stores the term so retry replays the exact load. Picker-driven loads keep following the active selector (unit-tested by driving the real CustomSelects). The #476 emergent examId-clear behavior is untouched and its tests stay green. F3: _generate_and_insert scopes the exam lookup to the RESOLVED offering's enrollment (the #462 CodeRabbit fix) — a two-term user can no longer generate a guide keyed on one term's offering from another term's exam (regression test: 404, nothing persisted). F2: flashcards _get_course_documents — a course-name miss WITH an explicit semester now contributes no documents (an explicit term gives the all-docs fallback nothing to anchor to); without a semester the pre-existing all-docs fallback is byte-identical (both pinned). F4: the guide 404 branch no longer hardcodes the exam-deleted copy — the server's detail renders when it isn't the exam-deleted sentence (no-offering-in-term case), still guidance, never a toast. F5: comment truth fixes — notes PATCH re-home relabeled (re-home, not create; still deliberately current-term); api.ts study-guide block now documents the pre-existing exams-list asymmetry (omitted semester = current-term resolution for guide/regenerate but ALL terms for getStudyGuideExams); Study.semester.test.tsx attributes the exam-clear to the courseId-keyed effect racing openRecent (#476), not to openRecent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): disambiguate the Linear Algebra pill (course pill + topic pill share the name under All semesters) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Adds a semester switcher (a redesigned Courses & Semesters hub) that scopes the learning surfaces to a chosen term, and enforces a no-retake rule. Implements the approved design/plan under
docs/superpowers/.Approach: Path A — read-time filtering, no schema change / no migration. The knowledge graph stays keyed on the abstract
course_id; a term filter is applied at the read boundary. Retakes are disallowed, so each course lives in exactly one semester.Backend
services/academics.py:term_id_for_label(label → term id, mirrors gradebook) +user_course_ids_for_term(the courses a user is enrolled in for a term).services/graph_service.py:get_graph(semester=)andget_recommendations(semester=)filter nodes/edges/stats (and DB-sidecourse_id in (...)for recs) to the term's courses;add_coursenow rejects re-adding a course enrolled in any term (no-retake), surfacing the existing term.routes/graph.py: optional?semester=<term label>onGET /api/graph/{user}and/recommendations. Absent ⇒ all terms (unchanged).Frontend
lib/api.ts:EnrolledCourse.term; optionalsemesterongetGraph/getRecommendations.lib/useActiveSemester.ts: persisted active-semester hook ([value, setter, hydrated]) +distinctTerms/resolveActiveSemesterhelpers.ManageCoursesModal→ Courses & Semesters hub: term tabs (set the active semester), courses grouped by term, "Already taken · " disabled state, disabled "Personal learning — Coming soon" placeholder.Test Plan
python -m pytest tests/ -q→ 981 passed, 1 skipped (1 pre-existing unrelated OCR event-loop error).npm run typecheckclean,npm run lint0 errors,npm run test92 passed.Known non-blocking follow-ups
sessionsbackend change).resolveActiveSemesterdefaults to the most-recently-enrolled term (documented heuristic, not termsort_key).term_id_for_labelduplicates gradebook's private_term_id_for_semester— a later cleanup could consolidate.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests