---
url: https://docs.nemo12.com/architecture/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](../product/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`. 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 ở `apps/learn/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](../product/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 ở `grammar/content/<level>/_units/<unit>.json`, 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>`. Trang `/ielts/grammar` và cột n/5
ở trang level chỉ đếm 5 câu Quick (`quickDone`), vì bài dùng chung ghi cả level 6-35 dưới cùng mã.

Hai công cụ:

* `npm run grammar:coverage --workspace @nemo12/learn` (`apps/learn/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](../product/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/learn/src/grammar/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](../product/prd-008-grammar.md). `TaskCard` (nay ở `apps/learn/src/grammar/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](../product/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/learn/src/grammar/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).
