---
url: https://docs.nemo12.com/apps/grammar/sdd-048-grammar-program.md
description: >-
  Bản nháp Program Grammar (SDD-048) ở learn.nemo12.com/grammar: route, dữ liệu,
  giao diện và quyền theo PRD-008.
---

# SDD-048 - Program Grammar

> Nguồn: SRC-1104. Yêu cầu và mười quyết định: [PRD-008](./prd-008-grammar.md).

## 1. Route

`learn.nemo12.com/grammar/**` là một mạch riêng trong app learn, cùng kiểu với sân IELTS: `App.tsx` nhận
`/grammar` thành view `grammarProgram` rồi giao cả thanh địa chỉ cho `grammar/GrammarApp.tsx` (lazy load).
Mạch tự giữ router của nó bằng `history.pushState` + `popstate`:

| Địa chỉ | Trang |
| --- | --- |
| `/grammar` | Dashboard ba ô + danh sách 5 level |
| `/grammar/<level>` | Giới thiệu level (tiếng Việt) + 6 mảng, mỗi mảng một thẻ danh sách bài |
| `/grammar/<level>/<lesson>` | Năm khung Video, Rule, Examples, Common mistakes, Practice ("Coming soon"); bài có chủ đề IELTS tương ứng thì có link "Luyện ngay bài này" sang `/ielts/grammar/<topic>` |

Id bài chỉ cần duy nhất trong một level (test gác). Slug tiếng Anh theo luật SRC-604.

## 2. Dữ liệu

Vòng này không có bảng D1 nào: danh mục là `packages/grammar-catalog/catalog.json` (level → units → lessons,
trường `ielts_topic` nối sang 36 chủ đề cũ). Test `grammar.test.ts` gác ba điều: đúng 5 level, mỗi level 4-6
mảng; id bài không trùng trong level; tập `ielts_topic` bằng đúng tập chủ đề trong `ielts/grammar/content`,
để một chủ đề mới thêm vào `/ielts/grammar` mà quên xếp vào program sẽ làm test đỏ.

Khi làm nội dung (vòng sau): nội dung bài theo khuôn JSON của Grammar hiện tại; tiến độ và phút học ghi vào
`learning_events` (sổ giờ học duy nhất), không mở sổ thứ hai.

## 3. Giao diện và quyền

Dùng lại nguyên khối của sân IELTS: `AuthProvider` + `SignInGate` (bắt đăng nhập), `NavBar`/`PageBody` của
`ielts/Chrome.tsx`, `CardHint`, `HelpButton`, `AvatarMenu` không ảnh. Không có component giao diện mới nào
ngoài bố cục trang, đúng luật bộ ba Motion + shadcn + Tailwind. Chip trên thanh chỉ có phút học hôm nay
(đọc `ielts-effort`, sổ chung cả bốn kỹ năng). Chân trang ký "NEMO GRAMMAR".

Membership (quyết định 7) chưa chặn gì ở vòng này vì chưa có nội dung để chặn; khi có nội dung thì dùng cùng
cổng gói của IELTS.

## 4. Nội dung bài (SRC-1105)

`lessonContent(level, lesson)` trong `GrammarApp.tsx` chọn nguồn: bài có `ielts_topic` lấy chủ đề trong
`ielts/grammar/content` (mã tiến độ là chính id chủ đề, nên tiến độ chung với `/ielts/grammar`); bài còn lại
lấy `grammar/content/<level>/<lesson>.json`, mã tiến độ `g-<level>-<lesson>` (khớp regex `[a-z0-9-]{1,60}`
của `POST /ielts-grammar`, không cần đổi API). Trang bài dùng lại `TaskCard`, `Fold`, `useGrammarDone` của
`ielts/Grammar.tsx`. Từ SRC-1373 (15.10.2026) nội dung không còn trong bundle: bảng D1 `grammar_content`, learn tải theo level qua `GET /v1/public/grammar/levels/{level}` (`apps/grammar/src/content.ts`), `lessonContent(data, level, lesson)` đọc từ dữ liệu đã tải; file seed ở `content/grammar/<level>/<lesson>.json`, sinh `scripts/seed-grammar.sql` bằng `scripts/gen-grammar-seed.mjs` (PRD-008 §19). Test gác: mọi bài Starter, Elementary (SRC-1107) , Intermediate (SRC-1111) , Upper-intermediate (SRC-1112) và Advanced (SRC-1114) có nội dung đủ khuôn và đáp án nằm trong lựa chọn.

## 5. Link nhóm Zalo theo khu (SRC-1108)

Chủ dự án 28.09.2026: Grammar, IELTS, SAT và Speak together mỗi khu có một nhóm Zalo riêng; link gửi sau.
Bảng link ở `packages/learn-shell/src/zaloGroups.tsx` (`ZALO_GROUPS`), nút "Zalo" nằm trên thanh trên của cả bốn khu.
Khu được chọn theo đường dẫn (`zaloGroupFor`): `/ielts/speaking/pair` là nhóm Speaking dù nằm dưới `/ielts`.
**Link còn trống thì nút không hiện**, để không có nút dẫn tới đâu cả; có link thì chỉ cần điền vào bảng.

## Kỳ miễn phí 14 ngày riêng (SRC-1123, 28.09.2026)

Chủ dự án: "NEMO GRAMMAR, free riêng 14 ngày". Bảng `learner_grammar_membership` (migration 0311), route
`GET /v1/learners/{id}/grammar-membership`, chip "Miễn phí: còn N ngày" trên thanh trên `/grammar`. Kỳ này độc
lập với kỳ IELTS: mở khu này không tạo hay tiêu kỳ bên kia. Chi tiết ở SDD-038 §136. Hiện kỳ chỉ để hiển thị,
chưa khoá nội dung khi hết hạn, giống IELTS.

## 6. Set bài tập và cổng chất lượng (SRC-1174, 01.10.2026)

Tiêu chuẩn ở [PRD-008 §14](./prd-008-grammar.md). Khuôn dữ liệu, giữ tương thích với file cũ:

| Trường trong file lesson | Nghĩa |
| --- | --- |
| `tasks` | Set Quick, 5 câu level 1-5 như hiện nay |
| `sets.practice` | Set Practice, 10 câu, cùng khuôn `Task` |
| `sets.mastery` | Set Mastery, 20 câu, cùng khuôn `Task` |

Unit review nằm ở `content/grammar/<level>/_units/<unit>.json` (dòng `kind=unit_review` trong D1), gồm `{ "unit": "<id>", "tasks": [...] }`; mỗi
task thêm trường `lesson` để biết nó thuộc lesson nào. Tiến độ ghi theo mã `g-<level>-<lesson>` như cũ, set
Practice và Mastery là level 6-15 và 16-35.

API `POST /v1/learners/{id}/ielts-grammar` nay nhận `level` 1-35 (trước là 1-5) và nhận mã `gu-<level>-<unit>`
(danh sách sinh bởi `scripts/gen-grammar-topics.mjs`). Phút học: câu Quick vẫn cộng 120 giây; câu của
Practice, Mastery và Unit review cộng 30 giây (`GRAMMAR_SET_TASK_SECONDS`). Lý do: một câu của set dài là một
câu, không phải một bậc của cả bài; giữ 120 giây thì 35 câu thành 70 phút học cho một bài, con số không ai
tin. ✍️ Quyết định của phiên, chờ chủ dự án duyệt.

Giao diện: bài có `sets` thì thay nút "Start practice" bằng danh sách ba set (tên, số câu, đã đúng bao
nhiêu); bấm một set thì chạy từng câu, làm tiếp từ câu đầu chưa đúng. Trang level thêm dòng "Unit review" ở
cuối thẻ mỗi unit khi có file; trang ôn ở `/grammar/<level>/units/<unit>`. Cột tiến độ ở trang level
nay đếm cả bài (§11, SRC-1440), không còn n/5 chỉ đếm Quick.

Hai công cụ:

* `npm run grammar:coverage --workspace @nemo12/learn` (`apps/grammar/scripts/grammar-coverage.ts`) in số lesson
  và unit đạt chuẩn theo từng level. Chỉ báo cáo, không chặn.
* `grammar.test.ts` gác bốn điều máy đo được của PRD §14c **cho mọi set mới** (Practice, Mastery, Unit review).
  Năm câu Quick cũ được miễn cho tới khi lesson ấy được nâng, để cổng không đỏ ngay từ hôm nay; lesson đã có
  `sets` thì Quick của nó cũng phải đạt.

## 7. Giao diện tiếng Anh, căn giữa (SRC-1173, 01.10.2026)

Nhãn và nút của `GrammarApp.tsx` viết tiếng Anh. `TaskCard` (dùng chung với `/ielts/grammar`) nhận cờ
`english`; không có cờ thì giữ nhãn tiếng Việt cho sân IELTS. `helpFor` trả hướng dẫn tiếng Anh riêng cho
`/grammar/**` (trước đây rơi vào hướng dẫn mặc định của IELTS). Chip kỳ miễn phí ở §"Kỳ miễn phí" nay ghi
"Free trial: N days left". Bố cục: mỗi trang bọc trong `mx-auto max-w-3xl` (trang level `max-w-4xl`); ô luyện
bọc `w-full` để không co lại trong hàng căn giữa. e2e `grammarProgram.spec.ts` đo vị trí và độ rộng.

## 8. Buổi Live thứ Hai (SRC-1307, 09.10.2026)

Yêu cầu: [PRD-008 §15](./prd-008-grammar.md). Cùng khuôn lịch Speak together (SRC-1088, SDD-052):

* **Lịch tính từ mốc, không lưu.** `workers/api/src/modules/grammarLive/schedule.ts`: buổi số 0 là thứ Hai
  12.10.2026 20:00 giờ VN (13:00Z), mỗi tuần một buổi 90 phút, mã buổi `YYYY-MM-DD-2000`. `sessionById` chỉ
  nhận mã đúng lịch, nên mã bịa không vào được bảng đăng ký.
* **Hai bảng** (migration 0340): `grammar_live_signups (session_id, learner_id)` nhớ ai bấm tham gia;
  `grammar_live_recordings (session_id, title, url)` giữ link bản ghi, nạp bằng workflow seed-data sau mỗi
  buổi (chưa có màn admin).
* **API** `workers/api/src/modules/grammarLive/routes.ts`: `GET /v1/learners/{id}/grammar-live` trả 6 buổi
  sắp tới và 12 buổi đã xong; `POST` / `DELETE .../grammar-live/{sessionId}` tham gia và huỷ. `zoom_url` chỉ
  có khi learner đã tham gia và buổi chưa xong; chưa đặt `GRAMMAR_ZOOM_URL` thì `zoom_pending: true`. Không có
  hạn chót tham gia, vì bấm tham gia là đường duy nhất tới link. Bản ghi hiện cho mọi learner.
* **Giao diện** `apps/grammar/src/GrammarApp.tsx`: `LiveCard` trên trang chủ, `LivePage` ở
  `/grammar/live`, `HowItWorks` với nút "Ask a mentor on Zalo" (ẩn khi link nhóm Zalo grammar trong
  `zaloGroups.tsx` còn trống).
* **Trang công khai** `apps/web/src/site/grammarProgram.ts`: `GRAMMAR_LIVE`, `GRAMMAR_WEEK`. Test trong
  `grammarLive/routes.test.ts` gác ngày, giờ, thứ của trang công khai khớp `schedule.ts`.

## 9. Phản hồi luyện tập (SRC-1308, 09.10.2026)

Yêu cầu: [PRD-008 §16](./prd-008-grammar.md). `TaskCard` (nay ở `apps/grammar/src/practice.tsx`, SRC-1337)
giữ ba trạng thái `none | wrong | right` và số lần sai; `MAX_TRIES = 3`. Câu đúng mới gọi `markDone` và
`recordGrammarTask` như trước, nên phút học và tiến độ không đổi nghĩa. Câu lộ đáp án sau 3 lần sai thì không
ghi gì. `label=""` ẩn nhãn trong thẻ khi trang đã có thanh tiến độ: `SetRunner` vẽ `Progress` của shadcn
cùng chữ "n / N". e2e `grammarProgram.spec.ts` đi đủ ba nhánh.

## 10. Gỡ /ielts/grammar (SRC-1337, 11.10.2026)

Yêu cầu: [PRD-008 §18](./prd-008-grammar.md). Trường `ielts_topic` bỏ khỏi danh mục; mọi bài có file riêng
và mã tiến độ `g-<level>-<bài>`. Phần dùng chung (kiểu dữ liệu, kho tiến độ, `TaskCard`, `Fold`) chuyển từ
`ielts/Grammar.tsx` (đã xoá) sang `apps/grammar/src/practice.tsx`. Router IELTS giữ một route `grammarMoved` để
chuyển `/ielts/grammar*` sang `/grammar`. `scripts/gen-grammar-topics.mjs` chỉ còn đọc thư mục của Program Grammar,
nên API không còn nhận mã chủ đề IELTS cũ. Endpoint `/v1/learners/{id}/ielts-grammar` giữ tên (đổi tên là việc riêng).

## 11. Tiến độ bài đếm cả bốn set (SRC-1440, 18.10.2026)

Chủ dự án thấy "Used to" hiện 5/5 có dấu tích và Unit 2 là 1/3, trong khi mới làm 5 câu Quick khởi động. Lý do:
cột n/5, dấu tích, số bài xong của unit, level và trang chủ đều chỉ đếm Quick (level 1-5), dù một bài có bốn set.

* Tiến độ một bài = số câu đã làm **đúng** trên cả bài: Quick 5 (level 1-5), Practice 10 (6-15), Mastery 20
  (16-35) dưới mã `g-<level>-<bài>`, Fix it 3 (1-3) dưới `gf-<level>-<bài>`: 38 câu. Trang level hiện phần trăm
  (aria-label "n of 38 questions right"); dấu tích và "bài xong" chỉ khi đủ 38. Câu sai ba lần rồi lộ đáp án
  không được tính, nên làm lại set (chạy tiếp từ câu đầu chưa đúng) là cách lên 100%.
* `lessonProgress` và `LESSON_SHAPE` ở `GrammarApp.tsx` đếm thẳng từ tiến độ, không cần tải nội dung, nên trang
  chủ (chưa tải level nào) đếm đúng như trang level. Đổi lại, mọi bài phải đúng khuôn 5/10/20/3: cổng "lesson
  shape" trong `src/grammar.test.ts` chặn bài lệch khuôn. Unit review vẫn đếm riêng (mã `gu-`).

Chặn tái phát: `src/grammar.test.ts` (khuôn bài, Quick xong là 5/38), ca e2e SRC-1440 trong
`e2e/grammarProgram.spec.ts`.

## 12. "Fix it" đổi tên thành "Fix common mistakes" (SRC-1441, 18.10.2026)

Chủ dự án hỏi vì sao "Fix it" luôn là 3 dù không sai câu nào. Set ấy không lấy từ câu learner làm sai: nó là
3 câu sai mẫu trong mục Common mistakes của bài, giống nhau cho mọi người. Tên "Fix it" và lời nhắc ở trang
unit review ("Got one wrong? ... do its Fix it set") khiến nó đọc như "sửa lỗi của bạn". Nay set tên
**Fix common mistakes**, dòng phụ "The lesson's 3 typical wrong sentences: write them right", và lời nhắc ở
unit review chỉ sang set Practice. Mã tiến độ `gf-` giữ nguyên nên tiến độ đã có không mất.

## 13. Fill in the blanks ở mỗi unit (SRC-1442, 18.10.2026)

Chủ dự án: *"Trong mỗi Unit, cần thêm dạng bài tập điền từ. 10 câu, cần điền 2 từ, và có 2 từ gợi ý."*

* Mỗi file `content/grammar/<level>/_units/<unit>.json` có thêm mảng `fill`: 10 câu `gap`, mỗi câu một chỗ
  `___`, `hint` là đúng 2 từ (hiện dưới câu thành hai thẻ "Use: not · like"), `answer` đúng 2 từ, `accept` cho
  dạng viết khác (ví dụ "do not like"). Đáp án không được chỉ chép lại hai gợi ý theo thứ tự: learner phải đổi
  thì, dạng, phủ định hay trật tự. Mỗi lesson của unit có ít nhất 3 câu. Lời giải tiếng Việt thật, cùng luật
  chống câu đệm như mọi set.
* Không cần migration: thân unit nằm nguyên khối trong `grammar_content.body_json`, API trả nguyên văn, và schema
  `UnitReview` của PUT admin là `passthrough`. Nạp bằng `scripts/seed-grammar.sql` như mọi nội dung grammar.
* Trang unit (`/grammar/<level>/units/<unit>`) có hai set: Unit review và **Fill in the blanks**; `?set=fill` mở
  thẳng set mới. Trang level có thêm dòng "Fill in the blanks n/10" dưới dòng Unit review. Tiến độ ghi dưới mã
  `gn-<level>-<unit>`, level 1-10.
* Cổng: `fillProblems` trong `src/standard.ts` và ca "fill in the blanks" trong `src/grammar.test.ts` (mọi unit
  phải đạt, bộ đo phải bắt được câu hỏng). 710 câu soạn bằng agent, mỗi agent một level.
