Skip to content

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Retsomm
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Dev by Retsomm · Pull Request #25 · Retsomm/EasyLearn · GitHub
Skip to content

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

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

Dev - #25

Merged
Retsomm merged 3 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#25
Retsomm merged 3 commits into
mainfrom
dev

Conversation

@Retsomm

@RetsommRetsomm commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added a new recap flow to browse chapters and review chapter-level key points.
    • Added a “收藏重點” (Saved Key Points) screen with the ability to save/unsave key points from recaps.
    • Added navigation and a new Notes card showing your saved key point count.
    • Added the “Grokking Functional Programming” chapter with 12 levels and new practice questions.
  • Improvements
    • Saved key points now persist across sessions and synchronize for signed-in users.
  • Documentation
    • Added curriculum documentation for the new chapter.

Retsommand others added 2 commits July 27, 2026 09:25
Scala 概念(flatMap、Option/Either、IO monad、Ref、property-based
testing 等)翻譯成 JS 可執行等價寫法;跟既有 fp/sicp/dmmf/fljs 章節
重疊主題(純函式、不可變性、Stream、併行)刻意換不同情境出題避免重複。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
複習分頁讓使用者按章節/關卡瀏覽重點整理(目前只有 Grokking Functional
Programming 完整驗證,內容改寫自原書 12 章,其餘書籍待補)。收藏重點功能
讓使用者在每條重點旁點星號收藏,收藏狀態比照既有收藏題目的模式同步到
後端(新增 Prisma model SavedKeyPoint 並套用 migration),登入使用者可
跨裝置同步;同時修掉一個既有的畫面切換捲動位置未歸零的 bug。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
easy-learnReadyReadyPreview, CommentJul 27, 2026 10:54am

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 844a3a5c-ee5b-41a4-9319-e5a3fa300aff

📥 Commits

Reviewing files that changed from the base of the PR and between ad2728d and 741d4b4.

📒 Files selected for processing (1)
  • packages/core/src/data/chapterSummaries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/data/chapterSummaries.ts

📝 Walkthrough

Walkthrough

Adds the Grokking Functional Programming curriculum and recap data, introduces saved key-point progress persistence through Prisma and API synchronization, and adds web recap, saved-key-point screens, navigation, and styling.

Changes

Curriculum and recap data

Layer / File(s)Summary
GFP curriculum and recap data
docs/curriculum.md, packages/core/src/data/*, packages/core/src/data/questions/*, packages/core/src/index.ts
Adds the GFP chapter, twelve six-question datasets, static chapter summaries, stable key-point IDs, saved-key-point lookup helpers, and public exports.

Saved key-point persistence

Layer / File(s)Summary
Saved key-point persistence
apps/web/prisma/*, apps/web/src/lib/*, apps/web/src/app/api/progress/*, apps/web/src/hooks/useProgress.ts, apps/mobile/hooks/useProgress.ts, packages/core/src/types.ts
Adds the SavedKeyPoint model and migration, extends progress conversion and loading, migrates local saved IDs, and synchronizes toggle operations through the authenticated API.

Recap and saved-key-point screens

Layer / File(s)Summary
Recap and saved-key-point screens
apps/web/src/App.tsx, apps/web/src/screens/*, apps/web/src/components/Navbar.tsx, apps/web/src/index.css
Adds recap navigation, chapter accordions with save buttons, the saved-key-points list, Notes integration, view routing, scroll reset behavior, and corresponding styles.

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

Sequence Diagram(s)

sequenceDiagram
participant User
participant RecapChapter
participant useProgress
participant SaveKeyPointToggle
participant Prisma
User->>RecapChapter: Select key-point star
RecapChapter->>useProgress: toggleSavedKeyPoint(keyPointId)
useProgress->>SaveKeyPointToggle: POST keyPointId
SaveKeyPointToggle->>Prisma: Create or delete SavedKeyPoint
SaveKeyPointToggle->>Prisma: Load full progress
SaveKeyPointToggle-->>useProgress: Return progress
Loading

Possibly related PRs

  • Retsomm/EasyLearn#11: Both changes modify apps/web/src/hooks/useProgress.ts and overlap in progress-state handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check nameStatusExplanationResolution
Title check❓ InconclusiveThe title is too generic and does not describe the pull request’s actual changes.Replace it with a concise title that names the main change, such as adding saved key points and recap screens.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

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

🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts`:
- Around line 23-35: Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.
- Around line 19-20: Validate incoming key-point IDs against the canonical IDs
defined in chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.
In `@apps/web/src/screens/RecapChapter.tsx`:
- Around line 47-49: Update the key-point identity flow in RecapChapter and
keyPointId usage to use an immutable ID stored on each summary key point rather
than the mapped array index. Ensure saved-key-point lookup and persistence use
that stable ID, while retaining a migration/fallback path that recognizes
existing index-based saved IDs.
In `@packages/core/src/data/chapterSummaries.ts`:
- Line 48: Revise the product type explanation in chapterSummaries so the
JavaScript `{ name, year }` analogy is not described as inherently immutable.
Describe it as an object with a fixed product-shaped structure, or explicitly
qualify immutability as a convention/type-level guarantee unless the
implementation freezes the object or uses an immutable structure.
- Around line 142-143: Update keyPointId and the key-point data model so every
key point has an explicit immutable identifier rather than deriving its
persisted ID from the array index. Add migration handling for existing
levelId:index values before using the new IDs for saved-row lookup, preserving
existing saved key points and ensuring inserts, removals, or reordering do not
change their identities.
- Around line 87-90: Update the lazy-IO analogy in the affected chapter summary
to avoid presenting an unawaited Promise as equivalent to a deferred thunk,
since creating a Promise from fetch starts the work immediately. Keep the
existing thunk/IO-wrapper explanation, or explicitly qualify Promise as only an
approximation; preserve the surrounding descriptions of delayed execution and IO
composition.
In `@packages/core/src/data/questions/gfp-1-learning-fp.json`:
- Around line 90-105: Update the question around parseAge and categorizeAge so
the stated string-to-number signature does not imply safe composition when
parseAge can throw. Prefer changing parseAge to return an explicit total result
such as Option or Either and revise the prompt, correct answer, and explanation
to address the failure path; otherwise require callers to handle its exception
before composition.
In `@packages/core/src/data/questions/gfp-10-concurrent-programs.json`:
- Around line 115-127: Update the chunk helper to validate size before entering
its loop, rejecting any non-positive or non-integer value so i += size cannot
cause an infinite loop. Preserve the existing batching behavior for positive
integer sizes and ensure invalid inputs fail through the established error
behavior.
- Around line 70-85: Update the question’s `topic`, `prompt`, code labels,
`options`, `answer` explanation, and `verify` output around `safeBalance` so
`reduce` is described as sequential accumulation rather than concurrency-safe or
atomic updating. Preserve the distinction between the lost-update simulation and
the sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.
In `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json`:
- Around line 67-85: The question currently describes the hard-coded testCases
and every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c7d0b630-6938-436e-8353-ffc74b49e205

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b86e and ad2728d.

📒 Files selected for processing (32)
  • apps/mobile/hooks/useProgress.ts
  • apps/web/prisma/migrations/20260727021508_add_saved_keypoint/migration.sql
  • apps/web/prisma/schema.prisma
  • apps/web/src/App.tsx
  • apps/web/src/app/api/progress/migrate-local/route.ts
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts
  • apps/web/src/components/Navbar.tsx
  • apps/web/src/hooks/useProgress.ts
  • apps/web/src/index.css
  • apps/web/src/lib/progressLogic.ts
  • apps/web/src/lib/progressStore.ts
  • apps/web/src/screens/Notes.tsx
  • apps/web/src/screens/Recap.tsx
  • apps/web/src/screens/RecapChapter.tsx
  • apps/web/src/screens/SavedKeyPoints.tsx
  • docs/curriculum.md
  • packages/core/src/data/chapterSummaries.ts
  • packages/core/src/data/chapters.ts
  • packages/core/src/data/questions/gfp-1-learning-fp.json
  • packages/core/src/data/questions/gfp-10-concurrent-programs.json
  • packages/core/src/data/questions/gfp-11-designing-fp-programs.json
  • packages/core/src/data/questions/gfp-12-testing-fp-programs.json
  • packages/core/src/data/questions/gfp-2-pure-functions.json
  • packages/core/src/data/questions/gfp-3-immutable-values.json
  • packages/core/src/data/questions/gfp-4-functions-as-values.json
  • packages/core/src/data/questions/gfp-5-sequential-programs.json
  • packages/core/src/data/questions/gfp-6-error-handling.json
  • packages/core/src/data/questions/gfp-7-requirements-as-types.json
  • packages/core/src/data/questions/gfp-8-io-as-values.json
  • packages/core/src/data/questions/gfp-9-streams-as-values.json
  • packages/core/src/index.ts
  • packages/core/src/types.ts

Comment on lines +19 to +20
if (typeof keyPointId !== 'string' || keyPointId.length === 0) {
return NextResponse.json({ error: 'invalid keyPointId' }, { status: 400 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate key-point IDs at every server ingestion path. Both endpoints accept arbitrary non-empty strings, although packages/core/src/data/chapterSummaries.ts defines the canonical IDs. Invalid IDs become orphaned records and can incorrectly make a user appear to have migrated progress.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20: validate the ID against canonical chapter-summary data.
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46: apply the same validation before creating Prisma rows.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L19-L20 (this comment)
  • apps/web/src/app/api/progress/migrate-local/route.ts#L44-L46
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 19
- 20, Validate incoming key-point IDs against the canonical IDs defined in
chapterSummaries before accepting them. Apply this validation in
save-keypoint-toggle/route.ts at lines 19-20 and migrate-local/route.ts at lines
44-46, rejecting invalid values before creating or persisting any records while
preserving the existing 400 response behavior.

Comment on lines +23 to +35
const existing = await prisma.savedKeyPoint.findUnique({ where: { userId_keyPointId: { userId, keyPointId } } })
try {
if (existing) {
await prisma.savedKeyPoint.delete({ where: { userId_keyPointId: { userId, keyPointId } } })
} else {
await prisma.savedKeyPoint.create({ data: { userId, keyPointId } })
}
} catch (err) {
// 重複點擊造成兩個請求同時 toggle:一個 create 撞到另一個剛建好的紀錄(P2002),
// 或一個 delete 撞到已經被另一個請求刪掉的紀錄(P2025)——兩者都代表結果已經是預期狀態,忽略即可
const isRaceCondition =
err instanceof Prisma.PrismaClientKnownRequestError && (err.code === 'P2002' || err.code === 'P2025')
if (!isRaceCondition) throw err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use an idempotent saved-state API instead of a blind toggle contract. The server uses a stale read-before-write, while the client sends only an ID and accepts responses in any order; concurrent clicks or retries can persist and display the wrong final state.

  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35: accept the desired state or serialize the read/write mutation; do not silently ignore races as success.
  • apps/web/src/hooks/useProgress.ts#L154-L166: send the desired state, sequence requests per key point, and reconcile failed or stale responses.
📍 Affects 2 files
  • apps/web/src/app/api/progress/save-keypoint-toggle/route.ts#L23-L35 (this comment)
  • apps/web/src/hooks/useProgress.ts#L154-L166
🤖 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 `@apps/web/src/app/api/progress/save-keypoint-toggle/route.ts` around lines 23
- 35, Replace the blind toggle flow in
apps/web/src/app/api/progress/save-keypoint-toggle/route.ts lines 23-35 with an
idempotent mutation that accepts the desired saved state and serializes or
atomically applies the read/write; do not silently treat P2002/P2025 races as
successful. Update apps/web/src/hooks/useProgress.ts lines 154-166 to send that
desired state, sequence requests per key point, and reconcile stale or failed
responses so the persisted and displayed state remains correct.

Comment on lines +47 to +49
{summary.keyPoints.map((point, index) => {
const id = keyPointId(summary.levelId, index)
const saved = !!savedKeyPointIds[id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use stable IDs for persisted key points.

keyPointId(summary.levelId, index) is positional. Inserting or reordering summary bullets will make existing saved records point to different text. Store an immutable ID alongside each key point instead of deriving it from its array index, with a migration/fallback for existing saved IDs.

🤖 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 `@apps/web/src/screens/RecapChapter.tsx` around lines 47 - 49, Update the
key-point identity flow in RecapChapter and keyPointId usage to use an immutable
ID stored on each summary key point rather than the mapped array index. Ensure
saved-key-point lookup and persistence use that stable ID, while retaining a
migration/fallback path that recognizes existing index-based saved IDs.

'map、filter、foldLeft 都是「高階函數」(higher-order function:接受函式當參數,或回傳函式的函式)在 JS 裡的直接對應:map 對陣列每個元素套用同一個轉換函式產生等長新陣列;filter 用回傳布林值的判斷函式挑出符合條件的元素;foldLeft(對應 JS 的 reduce)則是帶著一個累加值,逐一走訪陣列元素把結果收斂成單一值,三者都不修改原陣列。',
'函式可以回傳函式:當一個函式需要固定住某些參數(例如評分演算法、清單)、只留一個參數(例如門檻值)晚點才決定時,可以讓外層函式回傳一個只接收剩餘參數的內層函式,避免每次呼叫都要重複傳同一組固定參數。',
'currying(柯里化)就是把一個多參數函式,轉換成一連串每次只接收一個參數、彼此回傳彼此的單參數函式鏈(在 JS 就是連續回傳箭頭函式,如 a => b => c => 結果);重點不是語法炫技,而是讓呼叫端可以在程式的不同地方分別決定每個參數,減少重複傳遞相同引數的樣板碼。',
'product type(乘積型別,Scala 用 case class 表達,對應到 JS 大致就是一個描述固定欄位形狀的物件,例如 { name, year })用來把兩個以上相關聯的資料綁在一起變成一個不可變值;由於它本身不可變,跟 map/filter 這類高階函數搭配時特別安全,可以直接用箭頭函式取出欄位(例如 lang => lang.name)而不用擔心欄位被意外修改。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe a plain JavaScript object as immutable.

{ name, year } is mutable unless the implementation freezes it or uses an immutable data structure. Reword this as a product-shaped object, or explicitly explain that immutability is only a convention/type-level guarantee in this JavaScript analogy.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` at line 48, Revise the product
type explanation in chapterSummaries so the JavaScript `{ name, year }` analogy
is not described as inherently immutable. Describe it as an object with a fixed
product-shaped structure, or explicitly qualify immutability as a
convention/type-level guarantee unless the implementation freezes the object or
uses an immutable structure.

Comment on lines +87 to +90
'把一個會讀寫外部世界的不純函式(呼叫 API、讀寫終端機輸入輸出)包裝起來(概念上類似 JS 裡用一個 thunk 或尚未 await 的 Promise 描述「之後才要執行的動作」),包裝的當下並不會真的執行它,而只是建立一個值,描述「這是一個之後執行時會產生某個結果的計算」,真正的副作用被延後到程式裡明確呼叫一次「執行」的地方才會發生。',
'這種「延遲計算」的容器本身也有 map/flatMap,所以多個包好的 IO 步驟(例如分別取兩個人的行事曆再合併)可以用前面章節同一套 for 迴圈式管線串接起來——串接的過程完全不會真的執行任何副作用,只有在最後把整條組合值拿去執行時才會發生。',
'只要用到一個回傳 IO 的函式,呼叫端自己的回傳型別也會被迫變成 IO(書裡稱為 IO 有「傳染性」,類似 JS 裡 async 函式不管內部做什麼、回傳的一定是 Promise)——這讓型別簽名一眼就能看出程式裡哪些地方可能有副作用、哪些地方保證是純的。',
'重試一次、重試後改用預設值等復原策略,是透過在 IO 值上串接 .orElse(...) 來表達——跟第六章 Option/Either 的 orElse 是同一套語彙,不需要巢狀的 try/catch,讓重試/備援邏輯可以宣告式地跟主要商業邏輯分開放。',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a thunk, not a Promise, for the lazy-IO analogy.

Creating a Promise from fetch() starts the underlying work; await only observes it and does not defer the side effect. Keep the thunk/IO-wrapper analogy, or clearly state that a Promise is only an approximation.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 87 - 90, Update the
lazy-IO analogy in the affected chapter summary to avoid presenting an unawaited
Promise as equivalent to a deferred thunk, since creating a Promise from fetch
starts the work immediately. Keep the existing thunk/IO-wrapper explanation, or
explicitly qualify Promise as only an approximation; preserve the surrounding
descriptions of delayed execution and IO composition.

Comment on lines +142 to +143
// 重點文字沒有天生的唯一 id,用「關卡 id + 陣列索引」組出穩定識別碼給收藏功能存
export const keyPointId = (levelId: string, index: number): string => `${levelId}:${index}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persisted key-point IDs independent of array position.

levelId:index is stable only while key points are never inserted, removed, or reordered. A future recap edit can make an existing saved row select different text or become orphaned. Give each key point an explicit immutable ID and migrate existing IDs before treating this as a persistence key.

🤖 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 `@packages/core/src/data/chapterSummaries.ts` around lines 142 - 143, Update
keyPointId and the key-point data model so every key point has an explicit
immutable identifier rather than deriving its persisted ID from the array index.
Add migration handling for existing levelId:index values before using the new
IDs for saved-row lookup, preserving existing saved key points and ensuring
inserts, removals, or reordering do not change their identities.

Comment on lines +90 to +105
"id": "gfp-1-q5",
"type": "concept",
"difficulty": 2,
"topic": "關注簽章勝過關注函式本體:組合函式時,你在乎的是型別對不對得起來",
"docs": "",
"story": "",
"prompt": "把 parseAge 的結果傳給 categorizeAge(age: number) 使用時,你需要先讀懂 parseAge 內部每一行程式碼在做什麼,才能正確使用它嗎?",
"code": "",
"options": [
{ "id": "a", "text": "不需要:只要知道 parseAge 的簽章是「輸入字串、輸出數字」,就能判斷它的輸出型別跟 categorizeAge 要求的輸入型別對得起來,可以直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的——FP 鼓勵在組合函式的時候,優先關注簽章,而不是鑽進函式本體的實作細節" },
{ "id": "b", "text": "一定要先把 parseAge 內部的每一行程式碼都讀懂,才能知道能不能跟 categorizeAge 組合使用" },
{ "id": "c", "text": "只要函式名稱聽起來合理,型別對不對得起來完全不重要" },
{ "id": "d", "text": "簽章只是裝飾用的標註,實際組合函式時完全不會參考它" }
],
"answer": "a",
"explanation": "只要 parseAge 的簽章跟 categorizeAge 要求的輸入型別對得起來,就能直接組合使用,不需要深入讀懂 parseAge 內部每一行是怎麼算出這個數字的。FP 鼓勵在組合函式時優先關注簽章,這也是為什麼「函式不說謊」這麼重要——一旦簽章可以信任,組合時就能省下讀懂每個函式內部實作細節的心力。",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat this parseAge signature as sufficient.

Lines 48–64 establish that parseAge throws, but this question says callers can safely compose it based only on string -> number. In JavaScript, that signature does not encode exceptions. Either return Option/Either (or another total result) or update the answer to require handling the failure path.

🤖 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 `@packages/core/src/data/questions/gfp-1-learning-fp.json` around lines 90 -
105, Update the question around parseAge and categorizeAge so the stated
string-to-number signature does not imply safe composition when parseAge can
throw. Prefer changing parseAge to return an explicit total result such as
Option or Either and revise the prompt, correct answer, and explanation to
address the failure path; otherwise require callers to handle its exception
before composition.

Comment on lines +70 to +85
"topic": "驗證:把多次「各自讀取、各自寫回」合併成一次安全的累加更新",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce",
"story": "",
"prompt": "這段程式對照示範「模擬遺失更新」跟「用單一個 reduce 安全地累加更新」,處理同一批扣款請求。這段程式會印出什麼?",
"code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\n\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\n\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\n\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);",
"options": [
{ "id": "a", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 30" },
{ "id": "b", "text": "模擬遺失更新的結果: 30\n安全累加的結果: 30" },
{ "id": "c", "text": "模擬遺失更新的結果: 60\n安全累加的結果: 60" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "模擬遺失更新的版本,每一步都根據同一份「呼叫當下的舊值」計算,後面的寫入會直接覆蓋前面的結果,最終只反映了最後一筆扣款(40),30 這筆扣款完全消失,變成 60;安全累加的版本用 reduce,每一步都基於「上一步已經更新過的結果」繼續往下算,正確反映兩筆扣款,變成 30。",
"verify": {
"checks": [
{ "code": "let unsafeBalance = 100;\nconst withdrawals = [30, 40];\nconst snapshot = unsafeBalance;\nwithdrawals.forEach((amount) => {\n unsafeBalance = snapshot - amount;\n});\nconst safeBalance = withdrawals.reduce((balance, amount) => balance - amount, 100);\nconsole.log('模擬遺失更新的結果:', unsafeBalance);\nconsole.log('安全累加的結果:', safeBalance);", "expected": "模擬遺失更新的結果: 60\n安全累加的結果: 30" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Do not present reduce as a concurrency-safe update.

This example only performs sequential computation over a local array; it does not provide atomicity for concurrent writers. Calling the result “safe” can teach users that reduce fixes shared-state races. Relabel it as sequential accumulation or demonstrate an actual atomic/Ref-based update.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 70 - 85, Update the question’s `topic`, `prompt`, code labels, `options`,
`answer` explanation, and `verify` output around `safeBalance` so `reduce` is
described as sequential accumulation rather than concurrency-safe or atomic
updating. Preserve the distinction between the lost-update simulation and the
sequential result, and ensure all displayed text and expected output use the
revised terminology consistently.

Comment on lines +115 to +127
"prompt": "chunk 把一個陣列切成固定大小的小批次,模擬「分批處理」的第一步。這段程式會印出什麼?",
"code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));",
"options": [
{ "id": "a", "text": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" },
{ "id": "b", "text": "[ [ 1, 2, 3, 4, 5, 6, 7 ] ]" },
{ "id": "c", "text": "[ [ 1 ], [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ] ]" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "chunk 每次用 slice 切出固定大小(3 個元素)的一批,直到整個陣列都切完為止——7 個元素被切成 [1,2,3]、[4,5,6]、[7] 三個批次。分批處理的第一步,通常就是先把要處理的資料切成這種固定大小的批次,再依序(而不是全部同時)對每一批做併行處理。",
"verify": {
"checks": [
{ "code": "function chunk(arr, size) {\n const result = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\nconst items = [1, 2, 3, 4, 5, 6, 7];\nconsole.log(chunk(items, 3));", "expected": "[ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reject non-positive chunk sizes.

With size === 0 or a negative value, i += size never reaches arr.length, so this helper loops forever. Add a positive-integer guard before the loop.

🤖 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 `@packages/core/src/data/questions/gfp-10-concurrent-programs.json` around
lines 115 - 127, Update the chunk helper to validate size before entering its
loop, rejecting any non-positive or non-integer value so i += size cannot cause
an infinite loop. Preserve the existing batching behavior for positive integer
sizes and ensure invalid inputs fail through the established error behavior.

Comment on lines +67 to +85
"id": "gfp-12-q4",
"type": "predict-output",
"difficulty": 2,
"topic": "驗證:對好幾份不同的清單,反轉兩次都回到原本的樣子",
"docs": "https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse",
"story": "",
"prompt": "這段程式對三份不同的清單,各自驗證「反轉兩次是否等於原本」這條屬性。這段程式會印出什麼?",
"code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\n\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\n\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\n\nconsole.log(allPass);",
"options": [
{ "id": "a", "text": "true" },
{ "id": "b", "text": "false" },
{ "id": "c", "text": "undefined" },
{ "id": "d", "text": "報錯" }
],
"answer": "a",
"explanation": "不管測試案例是有三個元素的陣列、空陣列、只有一個元素的陣列,還是有重複值的陣列,reverseTwice 都能正確回到原本的樣子,every 對所有測試案例都驗證通過,所以印出 true——這正是屬性測試的精神:同一條規律要對很多種不同形狀的輸入都成立,才算是真正驗證過這條屬性。",
"verify": {
"checks": [
{ "code": "function reverseTwice(arr) {\n return [...arr].reverse().reverse();\n}\nconst testCases = [[1, 2, 3], [], ['a'], [5, 5, 5, 1]];\nconst allPass = testCases.every(\n (arr) => JSON.stringify(reverseTwice(arr)) === JSON.stringify(arr)\n);\nconsole.log(allPass);", "expected": "true" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label fixed examples as property-based testing.

testCases is a hard-coded list and every checks only those four examples; no input generator or property runner is used. Either relabel this as example-based testing or change the code to generate arbitrary arrays so the explanation matches the implementation.

🤖 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 `@packages/core/src/data/questions/gfp-12-testing-fp-programs.json` around
lines 67 - 85, The question currently describes the hard-coded testCases and
every check as property-based testing. Relabel the topic, prompt, and
explanation as example-based testing, or replace testCases with an
arbitrary-array generator and property runner; ensure the answer and
verification remain consistent with the chosen implementation.

內容改寫自 SICP JS 原書 1.1~5.5 每個小節,涵蓋代換模型、遞迴過程、
資料抽象、環境模型、可變資料、併行、Stream、metacircular 直譯器、
惰性求值、非決定性計算、邏輯式程式設計、暫存器機器、垃圾回收、
編譯器等經典主題。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Retsomm
Retsomm merged commit 6c144f1 into mainJul 27, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Retsomm