Skip to content

SDD-038 · Learning Setup ​

Một phần của SDD-038 Cổng IELTS. Phần đầu là đặc tả đang áp dụng, mỗi mục giữ số § cũ; diễn biến và lý do các vòng quyết định nằm ở mục Lịch sử quyết định cuối trang. Các mục §57, §65, §69 nằm ở Learning Setup: trở lại hub và còn hai chặng.

12. Learning Setup: không ai phải đi qua cùng một quy trình (SRC-860, 2026-09-19) ​

Chỉ đạo chủ dự án: "Với Nemo IELTS, tôi sẽ không bắt mọi learner đi qua cùng một quy trình." Và nguyên tắc rút ra, chép nguyên văn vì nó quyết định mọi thứ dưới đây:

Nemo doesn't need to test you just because it can. Nemo needs enough evidence to know where you are.

12c. Evidence Level được SUY RA, không lưu ​

Đã thay bằng §65 (chặng Starting Point và Diagnosis đã bỏ, §65)

MứcKhi nàoNemo làm gì
Knownbaseline.source = 'official'không hỏi lại
Estimatedmock_test hoặc self_estimatedùng được, nhưng một bài mẫu sẽ làm nó sắc hơn
Unknownchưa có baselinechưa biết gì

Cả ba suy thẳng từ learner_ielts_baselines — bảng đã có sẵn từ SRC-717, không cần cột mới. Lưu evidence_level thành một cột sẽ lệch khỏi sự thật ngay lần đầu learner sửa điểm từ tự đánh giá thành điểm thi thật; đây đúng lỗi mà journey.ts đã tránh khi từ chối lưu "đang ở bước mấy".

Thứ duy nhất phải lưu mới (migration 0253_ielts_learning_setup.sql) là start_mode và focus_skills_json. Lý do hẹp: learner chưa có điểm vì họ mới bắt đầu và muốn học luôn, hay vì họ chưa chọn gì cả? Hai trạng thái ấy nhìn từ dữ liệu giống hệt nhau — cùng là "không có dòng baseline" — nhưng màn hình phải đối xử ngược nhau. Thiếu cột này thì learner đã chọn I'm new vẫn bị hỏi lại mỗi lần mở trang, tức là đúng thứ chỉ đạo yêu cầu bỏ đi.

12g. Chấm Writing theo bốn tiêu chí ​

ielts.production-feedback lên v2. v1 trả một band cho cả bài; con số ấy đủ cho bước chẩn đoán nhưng vô dụng với learner đã 6.5 muốn lên 7.5 — họ cần biết mình tụt ở Task Response hay ở Lexical Resource, vì hai chỗ ấy chữa bằng hai loại việc khác hẳn nhau.

v2 bắt chấm riêng đủ bốn tiêu chí Writing (Task Response · Coherence and Cohesion · Lexical Resource · Grammatical Range and Accuracy) và ba của Speaking (Fluency and Coherence · Lexical Resource · Grammatical Range and Accuracy), mỗi tiêu chí một band nửa bậc và một câu chỉ vào đúng chỗ trong bài. key là enum chứ không phải chuỗi tự do: model được viết note_vi thế nào cũng được, nhưng nếu nó tự đặt tên tiêu chí thì hai lượt chấm của cùng một learner sẽ trả về "Lexical Resource" và "Vốn từ", và không có cách nào ghép chúng thành một đường tiến bộ.

Khuôn JSON in ra đủ số phần tử của đúng kỳ năng đang chấm, không in khuôn mẫu một phần tử — bài học đã trả giá bốn lần trong PROMPT_HISTORY: ví dụ lấn át lời dặn.

Bản ghi chấm trước 2026-09-19 không được backfill: một band tiêu chí bịa ra từ band tổng trông y hệt một band có căn cứ, và mentor không có cách nào phân biệt. Thiếu thì để thiếu (criteria? optional trong report.ts).

Luật hé lộ (§4) không đổi: bốn tiêu chí đi vào ai_notes_json, và đường learner gọi cố ý không trả cột ấy.

12i. Mặc định ĐỌC, bấm Edit mới sửa (SRC-861, 2026-09-19) ​

Áp cho cả bốn màn khai báo: /ielts/settings, /ielts/setup (khối Starting Point), /ielts/setup/goal, /ielts/setup/declarations.

Mẫu này đã có sẵn, và đó là lý do làm thành component chung. Trang Hồ sơ IELTS (§5b) từ lâu bày một <dl> rồi có nút "Sửa" mở DeclarationForm. Nó làm đúng việc, nhưng phải viết nội dung hai lần — một bản đọc, một bản sửa — và hai bản ấy trôi khỏi nhau ngay lần đầu ai thêm một ô: bản sửa có ô mới, bản đọc thì không, và không có lỗi nào hiện ra.

EditGate (apps/learn/src/EditGate.tsx) gỡ đúng cái giá ấy. Chỉ có một bản nội dung; chế độ đọc là chính bản ấy nằm trong <fieldset disabled> đã bỏ viền, bỏ nền, ép opacity về 1. Ba điều đáng ghi:

  • <fieldset disabled> khoá mọi ô lồng bên trong, kể cả ô thêm vào sau này. Đây là hành vi sẵn có của HTML chứ không phải mã ta tự giữ, nên nó không thể quên ô nào — khác hẳn một danh sách disabled={...} rải tay.
  • Ép opacity: 1. shadcn làm mờ ô bị khoá (disabled:opacity-50); để nguyên thì cả màn trông như đang hỏng chứ không phải đang ở chế độ đọc.
  • data-edit-only giấu những thứ chỉ có nghĩa khi đang sửa (nút Lưu, câu hướng dẫn điền). Form tự khai thuộc tính này, vì chỉ form mới biết đâu là hành động và đâu là nội dung. Ngoài EditGate thì thuộc tính ấy không làm gì — nên DeclarationForm dùng chung với trang Hồ sơ không đổi hành vi, và có một case Playwright gác đúng điều đó.

Cancel phải thật sự huỷ. Các form giữ trạng thái cục bộ đọc từ API lúc mở, nên chỉ đổi cờ editing thì giá trị vừa gõ vẫn còn — learner tưởng đã bỏ, và lần bấm Edit sau thấy lại thứ mình tưởng đã vứt. Cancel vì thế đổi key của con, tức dựng lại từ đầu, và useEffect đọc lại API. Cách này đúng kể cả với form ta không sửa được ruột.

Một bug lộ ra khi làm, và nó không thuộc EditGate. IeltsLearningSetup và IeltsSettings trước đây làm mới bằng cách đặt state về undefined (khung xám) rồi đọc lại. Đó là unmount cả cây con, nên EditGate mất chế độ đang sửa: bấm chip Writing xong là khung đóng lại và learner không chọn nổi kỹ năng thứ hai. Nay tách hai đường — load (xoá màn, cho lần mở đầu và nút thử lại) và refresh (đọc lại, không xoá màn, cho các form gọi sau khi lưu).

Ô rỗng ở chế độ đọc. Một input[type=date] rỗng được trình duyệt vẽ placeholder dd/mm/yyyy, đọc ra như lời mời điền trong khi bấm vào không làm gì cả. Hai chỗ có ngày (GoalForm, CurrentScore) vì thế đọc useEditing() và in "chưa đặt ngày thi" / "not recorded". Đây là thứ <fieldset> không làm được, nên context tồn tại đúng cho việc chọn chữ nào hiện ra — không phải để bật tắt ô nhập.

Nợ ngôn ngữ ghi ở đây đã trả xong ở §12j. Khi viết §12i, /ielts/setup/goal và /ielts/setup/declarations còn tiếng Việt nên nhãn nút phải truyền vào "Sửa"/"Thôi"; nay cả hai trang nói tiếng Anh và nhãn trở về mặc định.

Cổng kiểm: 5 ca Playwright trong e2e/smoke.spec.ts — vào là không sửa được, bấm Edit thì sửa được, nút Lưu không hiện ở chế độ đọc, Cancel trả giá trị về, và trang Hồ sơ không đổi hành vi.

12j. Trả nợ ngôn ngữ của hai trang khai báo (SRC-865, 2026-09-19) ​

§12i để lại một khoản nợ: /ielts/setup/goal và /ielts/setup/declarations còn tiếng Việt, trái luật sân luyện (§8a). Vòng này trả xong.

Vì sao nó là nợ chứ không phải một lần quên dịch. GoalForm chỉ chạy trong /ielts/** nên dịch thẳng là xong. DeclarationForm thì khác: nó dùng chung với trang Hồ sơ ở /squid/ielts-profile, và trang ấy phải là tiếng Việt theo đúng luật mặc định của repo. Một biểu mẫu, hai nơi, hai thứ tiếng bắt buộc.

Ba cách, và vì sao chọn cách thứ ba.

CáchVì sao không
Dùng t() của apps/learn/src/lang.tsxHàm ấy chạy theo công tắc ngôn ngữ người dùng tự bật. Ở đây ngôn ngữ do chỗ đứng quyết: /ielts/** luôn tiếng Anh dù công tắc để gì, /squid/** luôn tiếng Việt. Một biểu mẫu đổi tiếng theo công tắc phá cả hai luật cùng lúc.
Tách thành hai componentHai bản trôi khỏi nhau ngay lần đầu ai thêm một ô — đúng cái giá EditGate vừa gỡ đi ở §12i.
Tham số lang trên một componentChọn cách này.

DeclarationForm nhận lang?: "vi" \| "en", mặc định "vi" — trang Hồ sơ là chỗ dùng lâu đời hơn, và một mặc định tiếng Anh sẽ lặng lẽ đổi ngôn ngữ của nó nếu ai quên truyền. DeclarationBlock trong mạch setup truyền lang="en" cố định, không đọc công tắc nào.

Hai chi tiết dễ làm hỏng thứ không liên quan:

  • PURPOSES và FREQ nay có ba cột (mã · vi · en) chứ không phải hai. Cột [1] giữ nguyên vị trí và giữ nguyên tiếng Việt: phần đọc của trang Hồ sơ tra theo ?.[1], và đảo thứ tự sẽ làm trang ấy bày ra mã máy thay vì chữ người đọc.
  • aria-label tách khỏi nhãn nhìn thấy và cố ý ngắn hơn. Lý do chính không phải thẩm mỹ: ba chuỗi tiếng Việt ("Kỹ năng khó nhất", "Bài viết có ai chữa", "Số giờ mỗi tuần") đang là chỗ bám của các test sẵn có ở trang Hồ sơ, nên đổi chúng là làm đỏ những test không liên quan gì tới việc dịch.

Nhãn tám việc khai báo (IELTS_TASKS) cũng sang tiếng Anh, và cố ý trùng với nhãn server đặt trong modules/ielts/setup.ts: learner bấm một dòng ở Learning Setup rồi tới trang con, nên hai chỗ gọi cùng một việc bằng hai cái tên là bắt họ tự đoán xem mình có đi đúng chỗ không.

Cổng kiểm: case Playwright của /ielts/setup/declarations nay đòi nhãn tiếng Anh, và case của trang Hồ sơ thêm một dòng đòi nó vẫn tiếng Việt — hai chiều, vì chỉ gác một chiều thì một lần đổi mặc định lang sẽ kéo trang /squid/** sang tiếng Anh mà không ai báo.

12k. Ba thẻ ở /ielts/settings, và sổ ba lần thi (SRC-871, 2026-09-19) ​

Hai thẻ cạnh nhau, không phải một cột dọc. Điểm hiện tại và mục tiêu chỉ có nghĩa khi đọc cùng nhau — khoảng cách giữa chúng là việc phải học. Xếp dọc thì muốn so phải cuộn, và cái phải so là thứ trang này tồn tại để bày ra. Trên điện thoại vẫn một cột (md:grid-cols-2), và items-start để thẻ trái dài ra vì lịch sử lần thi không kéo thẻ phải cao theo.

Bốn ô nhỏ, hai ô một dòng, dùng chung một component. BandGrid (apps/learn/src/IeltsBandGrid.tsx) vẽ bốn kỹ năng cho cả hai thẻ, cộng một dòng Overall chỉ đọc. Viết hai lần thì lần đầu ai đó đổi cỡ chữ ở một bên là hai cột lệch nhau ngay trên cùng một màn, và không test nào bắt được. Ô nhập không tự khoá theo prop: EditGate (§12i) đã dùng <fieldset disabled>, nên thêm một nguồn trạng thái nữa ở đây là dựng lại đúng thứ §12i gỡ đi.

Sổ ba lần thi giữ nguyên hình dữ liệu cũ. Bảng learner_ielts_baselines (0217) sinh ra với đúng một dòng active mỗi learner, và một chỉ mục duy nhất canh điều đó. Bảy chỗ trong worker đọc baseline bằng WHERE status='active' rồi first(): Learner Model, journey.ts, layerInputs.ts, setup.ts, ba truy vấn thư gửi phụ huynh. Nới chỉ mục cho ba dòng active là làm bảy chỗ ấy bốc một dòng bất kỳ trong ba, không chỗ nào báo lỗi. Nên: một dòng active là lần thi mới nhất, tối đa hai dòng archived là lịch sử. Không có migration nào trong vòng này.

baselines.ts là chỗ duy nhất quyết dòng nào active, qua reconcileBaselines: sắp theo assessed_on giảm dần, giữ ba dòng đầu, hạ tất cả xuống archived rồi nâng dòng đầu lên active (đúng thứ tự ấy — làm ngược lại thì có một khoảnh khắc hai dòng active và chỉ mục duy nhất ném). Sửa ngày một lần thi cũ thành hôm nay thì đúng nó trở thành điểm hiện tại, không phải dòng nào ghi sau.

Ba endpoint mới, và một đường cũ giữ nguyên vai trò: GET/POST /v1/learners/{id}/ielts-baselines và DELETE .../{baselineId} quản cả sổ; PUT /ielts-baseline vẫn sửa lần thi đang hiệu lực (giữ nguyên id, nên lịch sử model trỏ tới nó không đứt). Trần 3 canh ở server và trả 409 — client giấu nút thêm, nhưng một tab mở từ hôm qua vẫn gửi được lượt thứ tư. Lần thi cũ chỉ đọc và xoá được, không sửa: sửa lại điểm của một kỳ thi đã qua thì sửa cái gì?

Skills to improve thành thẻ riêng. Nó không phải một con số điểm mà là một lựa chọn, và nó quyết Diagnosis rút gọn còn mấy bài (§12c). Đứng lẫn trong thẻ điểm thì đọc ra như dòng phụ chú của điểm hiện tại. FocusPicker nhận thêm label (đặt null ở đây) để tiêu đề không in hai lần dưới tiêu đề thẻ.

Cổng kiểm: 7 ca vitest ở workers/api/src/modules/ielts/baselines.test.ts (trần 3, đúng một dòng active, active theo ngày chứ không theo thứ tự ghi, xoá dòng active thì dòng kế lên thay, overall của server, 404, và sổ của con nhà khác) + 2 ca Playwright ở e2e/smoke.spec.ts (hai thẻ cạnh nhau với bốn ô mỗi thẻ; thêm tới trần rồi nút biến mất kèm lời giải thích).

21. Ô điểm hiện tại phải đọc điểm ĐÃ LƯU (SRC-874, 2026-09-19) ​

Lỗi này lọt ra production ở §12i và chỉ lộ khi mở trang thật bằng mắt — không cổng nào bắt được.

Triệu chứng nhìn thấy. Trên /ielts/setup, banner nói "How well does Nemo know your level? Unknown" và "Taken on: not recorded", nhưng ngay bên dưới thẻ điểm vẫn bày L 6.5 · R 6.5 · W 6.0 · S 6.0. Learner chưa khai gì mà màn hình vẫn đưa ra bốn con số.

Nguyên nhân, và vì sao nó nặng hơn một lỗi hiển thị. CurrentScore không bao giờ đọc điểm đã lưu; nó khởi tạo ô nhập bằng một bộ mặc định viết cứng. Ở chế độ SỬA thì bộ mặc định là chỗ đặt con trỏ cho người sắp gõ — vô hại. Ở chế độ ĐỌC (thêm vào ở §12i) thì đúng bốn con số ấy đọc ra như sự thật về learner. Và tệ hơn: một learner đã khai 7.5 mở ra sửa sẽ thấy 6.5, bấm Lưu là ghi đè mất điểm thật — mất dữ liệu, im lặng.

Bản vá.

  • evidence trong payload /ielts-setup nay mang điểm bốn kỹ năng (bands), không chỉ overall. Trả kèm ở đây chứ không để client gọi thêm ielts-goal: hai lượt đọc là hai ảnh chụp có thể lệch, và khi lệch thì banner nói một đằng còn ô nhập nói một nẻo — đúng triệu chứng trên, chỉ khác nguồn.
  • CurrentScore nhận current và khởi tạo từ đó; effect đồng bộ lại theo các giá trị nguyên thuỷ, không theo tham chiếu đối tượng (mỗi lượt đọc API trả về một đối tượng mới, so theo tham chiếu sẽ giẫm lên thứ learner đang gõ dở).
  • Chưa khai gì mà đang ở chế độ đọc thì in thẳng "No score entered yet.", tuyệt đối không bày bộ mặc định.

Phạm vi. Chỉ /ielts/setup. Thẻ điểm ở /ielts/settings đã được dựng lại ở §13 (SRC-871) và bản ấy vốn đọc đúng bản ghi đã lưu.

Vì sao không test nào bắt được, và đã sửa gì ở cổng. Fixture e2e luôn kèm sẵn một bộ điểm cho has_score, nên cảnh "đã chọn là mình có điểm nhưng chưa khai" chưa từng được dựng. Nay fixture phân biệt hai trạng thái, và có hai ca: chưa khai thì phải thấy "No score entered yet."; khai 8.0/7.5 rồi tải lại trang thì ô phải mang đúng 8.0/7.5. Thêm hai ca đơn vị cho evidence.bands.

Bài học đáng giữ: cổng tự động của chúng ta kiểm "màn hình có chạy không", không kiểm "con số trên màn hình có thật không". Chuỗi trong bundle đúng, test xanh, CI xanh — mà trang vẫn nói dối về dữ liệu của learner. Với các màn bày số liệu cá nhân, phải có ít nhất một ca dựng cảnh kho rỗng.

41. Câu trả lời hiện sẵn ở Learning Setup, và quiz đi tiếp bằng một cú bấm (SRC-915, 20.09.2026) ​

Một dòng ✓ không nói learner đã trả lời gì. Sau SRC-910, chặng đã xong thì gập lại, nên toàn bộ màn Learning Setup chỉ còn tên việc cộng dấu xong. Muốn nhớ lại target band của chính mình thì phải mở /ielts/setup/goal rồi quay về — một chuyến đi cho một con số.

Nay mỗi SetupPart mang thêm value: câu trả lời đã lưu, rút thành một dòng ngắn.

Việcvalue
Target band and test dateBand 7.0 · 12.06.2026
Study context8h a week · Writing is hardest
Where you are starting fromAlready has a score
Your current IELTS scoreBand 7.0 · 20.08.2026
Skills you want to improveWriting, Speaking

Ba quyết định đi kèm:

  • Tóm tắt dựng ở SERVER (modules/ielts/setup.ts), cùng chỗ với done. Dựng ở client thì màn hình phải gọi thêm ielts-goal và ielts-declarations — hai lượt đọc nữa, hai ảnh chụp có thể lệch với ảnh này, đúng kiểu lệch mà evidence.bands (SRC-874) đã sinh ra để tránh.
  • Study Context chỉ lấy hai mẩu trong tám trường: số giờ mỗi tuần và kỹ năng khó nhất. Đó là hai câu trả lời đổi thật kế hoạch học; purpose/note là chữ tự do, dài ngắn tuỳ người.
  • Đầu chặng ĐANG GẬP mang theo tối đa hai giá trị, thay cho phụ đề. Nếu tóm tắt chỉ hiện khi mở ra thì nó không giải quyết được đúng điều chỉ đạo vừa nói. Chưa trả lời thì không bày gì — một dòng · · trống còn tệ hơn không có.

Quick Check: chọn xong màn đứng yên. Bản SRC-792 tự nhảy câu sau 220ms. Nó tiết kiệm một cú bấm mỗi câu nhưng lấy đi quyền quyết định lúc nào rời câu: bấm nhầm ô bên cạnh là màn đã đổi trước khi kịp đọc lại, và muốn sửa thì phải lần ngược bằng "Câu trước". Nay có nút Tiếp →, khoá tới khi có đáp án — một nút bấm được trong khi chưa chọn gì sẽ lặng lẽ bỏ qua một câu, và learner chỉ biết ở màn kết quả, lúc đọc "Con chưa trả lời câu này".

Gác: modules/ielts/setup.test.ts (bốn test cho value, kể cả trường hợp chưa trả lời thì null), và hai e2e trong apps/learn/e2e/smoke.spec.ts — một cho tóm tắt khi chặng còn gập, một cho nút Tiếp.

45. Nút "change" ở Starting Point, và một ngõ cụt một chiều (SRC-922, 2026-09-20) ​

45a. Ba hậu quả nối nhau ​

change gọi thẳng choose("unsure"):

  1. Ghi ngay, không hỏi lại. Câu trả lời cũ mất trước khi có câu trả lời mới.
  2. unsure khác null. Màn ba lối vào chỉ hiện khi start_mode === null, nên learner không được hỏi lại - họ bị ĐẶT sang "I'm not sure about my level".
  3. Không có đường về. Bấm change lần nữa cũng chỉ ra unsure, và start_mode không được đặt ở chỗ nào khác trong cả app (đã grep toàn apps/learn/src: chỉ đúng hàm choose ấy). Learner kẹt vĩnh viễn, và hasScore ở IeltsSettings.tsx thành false nên thẻ Current score biến mất theo.

Dữ liệu trong sổ thi không hề bị xoá bởi đường này. Nhưng learner không nhìn thấy cơ sở dữ liệu - họ thấy điểm vừa còn đó giờ mất, và kết luận là mình vừa xoá mất điểm của chính mình.

45b. Cách sửa, và vì sao không phải là đổi chữ ​

Nút chỉ mở ba lối vào, với lựa chọn hiện tại đánh dấu "Current". Không ghi gì cho tới khi learner chọn một cái khác; có "Keep what I have" để lùi. picking là trạng thái của MÀN HÌNH, không phải của dữ liệu - mở ra xem rồi đóng lại thì không có request nào đi.

Đổi chữ thành "start over" cũng làm nhãn nói thật, nhưng learner vẫn kẹt ở unsure. Ngõ cụt mới là chỗ hỏng; cái nhãn chỉ là thứ dẫn người ta vào đó.

Đây là họ hàng của SRC-886, khác chiều: ở đó một cái nhãn ("Nguyễn Tất Đắc") là cò đăng xuất; ở đây một cái nhãn ("change") là cò đặt-lại-và-khoá. Cùng một bài học - cử chỉ vô hại về hình thức mà hậu quả lớn nhất màn hình.

Gác: một ca e2e trong apps/learn/e2e/smoke.spec.ts đi trọn vòng has_score → unsure → has_score, đúng vòng mà bản cũ khoá lại ở nấc thứ hai.

48. Bộ khởi động có đúng ba câu (SRC-925, 20.09.2026) ​

Rà toàn bộ 51 bộ: 17 bộ sai. 16 năng lực Viết có 2 câu (migration 0262 nạp "hai câu mỗi chặng"); reading.main-idea có 6 câu (migration 0251 gom mọi câu chặng 1 chưa xếp vào một bộ).

Ba câu là con số có việc thật: bộ khởi động để learner nhận ra thao tác trước khi vào bộ kỹ thuật, và hai câu thì một câu đoán trúng đã thành một nửa; nhiều hơn ba thì nó thôi làm việc khởi động mà thành một bài tập đầy đủ.

Migration 0268 vá hai chiều và không xoá câu nào: thiếu thì soạn thêm (16 câu chặng 1 cho phần Viết), thừa thì chuyển sang một bộ mới cùng bậc (reading.main-idea.l1.4). Cổng máy: workers/api/src/modules/ieltsSkills/warmupShape.test.ts dựng CSDL từ chính migrations/ rồi đếm, nên năng lực mới nào thêm vào với hai câu khởi động sẽ đỏ ngay.

Lịch sử quyết định ​

Từ mục 12. Learning Setup: không ai phải đi qua cùng một quy trình (SRC-860, 2026-09-19) ​

Chỉ đạo: "Khi mới vào page, mọi thông tin luôn là read-only. Có button Edit, click vào thì các element mới trở thành Editable."

Chỉ đạo, từ một ảnh chụp trang Settings: "Cần một card Current Score ở bên trái, mỗi dòng thể hiện 2 mini-cards, thể hiện điểm của 2 skills. Còn Target ở bên phải, UI cũng tương tự. Có thể tạo thêm vài lượt nữa, đó là các điểm và các lần thi (tạo tối đa 3 lần). Cụm Skill to improve, cần tách riêng ra card riêng."

12a. Tám việc phẳng → năm chặng có tên là câu hỏi ​

Đã thay bằng §65 (Learning Setup còn hai chặng, §65a)

/ielts/settings (§5c) là một danh sách tám dòng: Mục tiêu · Hoàn cảnh học · Quiz · Bài đo · Bài viết · Ghi âm · Buổi gặp · Kế hoạch. Ai vào cũng thấy đủ tám, ai cũng phải làm đủ tám. Với learner đã có IELTS 7.0 muốn nâng Writing, năm trong tám dòng ấy là việc họ không có lý do gì phải làm — và cái giá không phải là thời gian, mà là họ đóng tab.

Nay là /ielts/setup, năm chặng, mỗi chặng mang chính câu hỏi nó hỏi:

#ChặngCâu hỏiGồm
01GoalWhere do you want to get to?band mục tiêu + ngày thi, hoàn cảnh học
02Starting PointWhere are you starting from?lối vào, điểm hiện tại, kỹ năng muốn nâng
03DiagnosisWhat do you need to improve?Quick Check, Reading & Listening Check, Writing Sample, Speaking Sample
04CalibrationWhat does your teacher confirm?khung giờ rảnh, buổi gặp
05Learning PlanHow will you get there?các mốc

Tám việc cũ không mất đi — chúng được xếp vào chặng của mình, và bảng IELTS_TASKS vẫn là nguồn duy nhất nối khoá việc với URL.

12b. Starting Point là một chỗ rẽ, không phải một biểu mẫu ​

Đã thay bằng §65 (chặng Starting Point đã bỏ, §65a)

Đây là quyết định quan trọng nhất của cả vòng. Ba lối vào:

Lối vàoNghĩaDiagnosis đòi gì
new — I'm new to IELTSchưa thi, chưa biết trình độ, muốn học luônkhông gì cả
has_score — I already have an IELTS scoređã thi, biết điểmchỉ bài của kỹ năng họ chọn nâng
unsure — I'm not sure about my levelnhờ Nemo đođủ bốn

Với has_score: chọn Writing → Writing Sample; chọn Speaking → Speaking Sample; chọn Reading hoặc Listening → bài đo chung (skill_assessment), vì hai kỹ năng ấy không có bài đo riêng — cả hai nằm trong một phiên assessment_sessions. Chọn cả hai vẫn là một bài, không phải hai. Quick Check bỏ: người đã thi thì đã biết kỳ thi vận hành thế nào.

Không chọn kỹ năng nào trong khi đã khai có điểm thì đòi đủ bốn như unsure — "chưa chọn" không được phép thành một lối lách qua mọi bài đo.

Với new, đây là điểm UX mà chỉ đạo nhấn mạnh: Learn → Practice → Measure → Adapt, thay vì Test → Test → Test rồi mới được học. Trình độ lộ ra qua chính các bài luyện, và sổ bằng chứng ở §5b-bis đã nhận bài luyện từ SRC-849 nên đường ấy có thật, không phải lời hứa.

12d. Phần không phải làm vẫn HIỆN ra ​

Đã thay bằng §65 (không chặng nào bị khoá nữa, §65a)

Quick Check và Reading & Listening Check của learner has_score không bị giấu đi — chúng hiện mờ kèm dòng not needed for you. Giấu thì trang ngắn hơn, nhưng learner mất cả đường quay lại làm nếu sau này họ muốn, và quan trọng hơn: thấy Nemo đã cân nhắc bài này rồi bỏ qua khác hẳn với việc Nemo chưa từng có nó.

Ngoại lệ: khi cả chặng là Optional (learner new), từng dòng không lặp lại nhãn ấy — bốn lần not needed for you liên tiếp vừa thừa vừa đọc ra như một lời từ chối. Nhãn Optional ở đầu chặng nói đủ.

Tiến độ đếm trên chặng bắt buộc, nên learner new thấy "2 of 4" còn learner unsure thấy "2 of 5". Viết cứng "of 5" ở client sẽ nói dối với learner thứ nhất về một việc không tồn tại.

12e. Nhãn mềm, và vì sao nó không phải chuyện chữ nghĩa ​

Đã thay bằng §65 (năm chặng còn hai, §65a)

Quick Check · Reading & Listening Check · Writing Sample · Speaking Sample — không có chữ Test hay Assessment nào. Learner 7.0 lên 7.5 đọc thấy "Assessment" sẽ hiểu là bị bắt thi lại IELTS, và phần đông sẽ đóng tab thay vì viết một bài luận. Nhãn do server đặt (setup.ts) để một nơi duy nhất quyết, nhưng lý do thuộc về màn hình.

Chữ trên hai trang này là tiếng Anh, đúng luật sân luyện learn.nemo12.com/ielts/** (SRC-834/835). Trang cũ còn sót tiếng Việt ("Mục tiêu và hoàn cảnh", "Xong 4/4"); vòng này trả về đúng luật.

12f. /ielts/settings còn lại là một ngăn kéo ​

Đã thay bằng §12k (ba thẻ ở /ielts/settings)

Learning Setup có thứ tự, có chỗ khoá, có điểm kết thúc. /ielts/settings thì không: nó là ngăn kéo các giá trị đã khai, mở ra sửa một con số rồi đóng lại. Thử thách cho mọi thứ định thêm vào đó chỉ có một câu — learner có quay lại sửa nó nhiều lần không? Band mục tiêu thì có (ngày thi lùi, trường đổi yêu cầu). Writing Sample thì không: nộp xong là xong.

Hai địa chỉ, và thứ tự trong pathToRoute là điều duy nhất giữ cả hai cùng sống:

  • /ielts/setup và /ielts/setup/<task> — địa chỉ hiện hành, và là dạng DUY NHẤT routeToPath sinh ra.
  • /ielts/settings/<task> — link cũ từ SRC-805, vẫn tới đúng việc ấy (luật slug, CLAUDE.md).
  • /ielts/settings trơ trọi — trang Settings mới. Bookmark cũ tới đây đổi đích; đó là đổi có chủ ý theo chỉ đạo, và trang mới có sẵn một đường sang Learning Setup ngay trên màn.

12h. Mã và cổng kiểm ​

Đã thay bằng §65b (tám trang con đã bỏ, §65b)

Ở đâu
Luật ai-phải-làm-gìworkers/api/src/modules/ielts/setup.ts (hàm thuần, không đọc DB)
Trạng thái đã-làm-gìpathway.ts — không đổi, setup.ts phủ lên nó một lớp yêu-cầu
Đường APIGET/PUT /v1/learners/{id}/ielts-setup
Lưu trữ0253_ielts_learning_setup.sql
Màn hìnhapps/learn/src/IeltsLearningSetup.tsx, IeltsSettings.tsx
Địa chỉpathToRoute/routeToPath trong App.tsx (đã xuất ra để test được)

Tách setup.ts khỏi pathway.ts vì hai câu hỏi hỏng theo hai kiểu: "đã làm gì" sai là đọc sai CSDL, "cần làm gì" sai là sai luật sản phẩm. Gộp lại thì một lần đổi luật sản phẩm phải sửa vào đúng chỗ đang đọc CSDL.

Cổng kiểm: setup.test.ts (16 ca, hàm thuần), setupRoutes.test.ts (13 ca qua HTTP — quyền, ghi từng phần, đồng bộ Evidence Level), apps/learn/src/ieltsRoutes.test.ts (12 ca địa chỉ, gác link cũ khỏi gãy), và 4 ca Playwright trong e2e/smoke.spec.ts.

Từ mục 41. Câu trả lời hiện sẵn ở Learning Setup, và quiz đi tiếp bằng một cú bấm (SRC-915, 20.09.2026) ​

Chỉ đạo chủ dự án, hai việc rời nhau trong cùng một vòng xem màn hình:

"Cần nhìn luôn được 1 thông tin nào đó, ví dụ như target band, thì cần hiển thị luôn lên đây, mà không cần click vào mới xem được. Và 1-2 thông tin về Study Context cần hiển thị luôn tại đây."

"Chọn đáp án, rồi bấm button gì đó, thì mới đi tiếp."

Từ mục 45. Nút "change" ở Starting Point, và một ngõ cụt một chiều (SRC-922, 2026-09-20) ​

Chủ dự án mở /ielts/profile trên production và thấy Current overall: not set, trong khi ảnh chụp /ielts/settings một tiếng trước đó còn bày 6.5. Lần ngược thì hoá ra trang hồ sơ nói đúng: sổ thi trống, evidence.level là unknown, start_mode là unsure. Câu hỏi thật là vì sao một learner đã khai "I already have an IELTS score" lại đang ở unsure.

Từ mục 48. Bộ khởi động có đúng ba câu (SRC-925, 20.09.2026) ​

Chỉ đạo chủ dự án 2026-09-20: "Warm-up Set, luôn có đúng 3 câu. Cái này có mỗi 2 câu. Rà soát nốt các Warm-up set còn lại."