---
url: https://docs.nemo12.com/architecture/sdd-043-ai-teen-program/step-ticks.md
description: >-
  AI Teen (SDD-043) §15-16: learner tick từng bước của lesson, lưu theo learner,
  quyền xem; vòng duyệt nội dung 08.10.2026.
---

# SDD-043 · Tick từng bước và vòng duyệt nội dung 08.10.2026

Một phần của [SDD-043](./index.md). learner tick từng bước của lesson, lưu theo learner, quyền xem; vòng duyệt nội dung 08.10.2026.

## 15. Tick từng bước: "Mình đã làm xong bước này" (SRC-1283)

Chủ dự án 07.10.2026: lesson gồm các Activity, Activity gồm các Bước (AI Teen 1 chuyển sang năm
Activity mỗi lesson ở SRC-1281; AI Teen 2-3 giữ ba journey Bắt đầu · Tìm hiểu · Đúc kết, cũng là
Activity). Learner tick từng bước là đã làm xong; hệ ghi ở server theo learner (đổi máy vẫn còn);
card Activity (§14) và NavBar phản ánh. Đã dựng 07.10.2026 (REQ-LRN-48); những gì dựng và các quyết định lúc dựng ở §15.8.

### 15.1 Ba loại bước, một luật xong

Bước của lesson không cùng một loại. Có bước máy đã biết là xong hay chưa (qua cổng đọc, trả lời
đúng câu kiểm), có bước chỉ learner biết (đọc câu hỏi mở đầu, viết "Mang theo", làm thử trên máy).
Tick là tín hiệu cho loại thứ hai; nó không được là đường vòng qua loại thứ nhất.

| Loại (`kind`) | Bước | Xong khi | Ô tick trong card |
| --- | --- | --- | --- |
| `gated` | phần đọc có cổng (`part-k`) | cổng của phần ấy QUA (§10), đọc từ dòng `:gates` | không bấm được; tự tick khi qua; trước đó là ổ khoá hoặc ô trống kèm lời "Qua kiểm tra nhanh để xong phần này" |
| `checked` | câu kiểm (`qN`), bước tổng kết câu kiểm (`result` của SRC-1281) | câu ấy đúng (§8), hay mọi câu của Activity đã trả lời | không bấm được; tự tick |
| `self` | mọi bước còn lại: `question`, `read` (bài không có cổng), `output`, `next`, bước ví dụ hay thực hành của năm Activity | learner tick | bấm được; bấm lại là bỏ tick |

* ✍️ **Tick KHÔNG thay cổng, cũng không thay câu kiểm.** Server từ chối tick cho bước `gated` và
  `checked` (422 `STEP_NOT_TICKABLE`), nên một client sửa tay cũng không đi vòng được. Một bước cổng
  "xong" chỉ có một nghĩa: đúng 3/3; cho tick thêm một nghĩa thứ hai là để hai sự thật cùng nói về một
  bước và sớm muộn nói ngược nhau.
* ✍️ **Tick không đổi luật qua lesson.** Lesson vẫn "qua" theo §8 (mọi trắc nghiệm đúng), unit vẫn
  sang Practice theo §9.2. Tick là tự khai công sức, không phải bằng chứng học được (bài học
  SRC-849: sổ bằng chứng không phải sổ công sức). Activity xong khi MỌI bước của nó xong theo bảng
  trên; lesson hiện "k/N bước" trong card và trong Mục lục, tách khỏi nhãn "đã qua".
* Bước `self` ở cuối vùng chính có thêm một nút "Xong bước này" (cùng việc với ô tick, rồi cuộn tới
  bước kế), vì trên điện thoại card thường thu gọn. Nút ấy là chữ hành động nên tiếng Việt, không em dash.

### 15.2 Khoá lưu: slot, Activity, Bước

```text
<course>:u<unit>:s<slot>:<activity>:<step>        ví dụ  ai-teen-1:u1:s7:understand:question
```

* **Slot, không phải vị trí** (§12.4): đổi thứ tự lesson không làm tick gắn sang lesson khác; lesson
  gộp hay soạn lại lấy slot mới nên không thừa hưởng tick cũ.
* **Id Activity, không phải số**: `understand`, `check`, `review` và các id của năm Activity mới
  (SRC-1281 chọn). Đổi tên hiển thị không đổi id (bài học SRC-1279).
* **Id Bước là định danh NỘI DUNG**: bước `self` phải mang id khai trong dữ liệu (`question`,
  `output`, `next`, hay slug của bước ví dụ như `compare-prompts`), dạng `^[a-z][a-z0-9-]{1,31}$`, KHÔNG
  được là id theo vị trí. `part-k` và `qN` là id theo vị trí nhưng chỉ dùng cho bước `gated`/`checked`,
  vốn không lưu tick (sự thật của chúng nằm ở cổng và kết quả bài tập, đã được `slots.test.ts` giữ).
  Soạn lại một bước đến mức đổi nghĩa thì đổi id; id cũ nghỉ, dòng cũ ở D1 bị bỏ qua khi đọc.
* Test thuần giữ luật: mọi bước `self` có id khai, không trùng trong một Activity, không có dạng
  `part-\d+` / `q\d+`.

### 15.3 Lưu ở đâu

Bảng mới `ai_teen_step_ticks` (số migration cấp bằng `npm run migration:new` lúc dựng, skill `d1-migrate`):

| Cột | Kiểu | Luật |
| --- | --- | --- |
| `learner_id` | TEXT | FK `learners`, ON DELETE CASCADE |
| `course_id` | TEXT | id có trong `courseIds.ts` |
| `unit_no` | INTEGER | CHECK 1-20 |
| `lesson_slot` | INTEGER | CHECK 1-20 (cùng trần slot của §12.4) |
| `activity_id` | TEXT | CHECK độ dài 2-32 |
| `step_id` | TEXT | CHECK độ dài 2-32 |
| `ticked_at` | TEXT | giờ server, giữ nguyên khi tick lại |
| `ticked_by_user_id` | TEXT | luôn là tài khoản của chính learner (§15.4) |

Khoá chính là cả sáu cột đầu; một dòng = một bước đang được tick, bỏ tick là xoá dòng (không giữ lịch
sử, cùng luật §8). Chỉ mục `(learner_id, course_id)`.

✍️ **Bảng riêng, không nhét vào `ai_teen_journeys`** như cổng đọc (§10) và chỉ báo (§13.4). Hai lần
trước là trạng thái nhỏ, ghi thưa, một máy; tick thì nhiều (mỗi lesson 10-20 bước), ghi dày và hay từ
hai máy cùng lúc. Một blob JSON mỗi unit nghĩa là đọc, sửa, ghi cả blob: hai lần tick gần nhau từ hai
máy thì lần sau đè mất lần trước. Một dòng mỗi tick là UPSERT/DELETE nguyên tử, và đếm "k/N bước" cho
mentor là một câu `COUNT` thay vì giải JSON.

### 15.4 API

| Route | Ai | Làm gì |
| --- | --- | --- |
| `GET /v1/ai-teen/courses/{courseId}/steps?learner_id=` | learner, phụ huynh, mentor (`canAccessLearner`, QG-008) | mọi tick `self` của learner trong course: `{ticks: [{unit, slot, activity, step, ticked_at}]}` |
| `PUT /v1/ai-teen/courses/{courseId}/steps` | **chỉ chính learner**, đã ghi danh course | body `{learner_id, unit, slot, activity, step, done}`; `done: true` UPSERT (giữ `ticked_at` cũ nếu đã có), `false` xoá; trả `{tick}` hay `{tick: null}` |

* **Chỉ learner ghi.** Ngoài `canAccessLearner`, PUT đòi tài khoản đang đăng nhập là tài khoản CỦA
  learner ấy (403 `NOT_LEARNER` cho phụ huynh, mentor, staff): tick là lời learner tự nói về mình, bố
  mẹ tick hộ thì con số mất nghĩa. Chưa ghi danh: 409, như §8.
* **Server kiểm bước có thật và là `self`.** Bản kê bước tick được của từng course sinh vào phía API
  bằng `node scripts/aiteen/gen-api-catalog.mjs` (cạnh `taskSkills.ts`), `program.test.ts` đối chiếu
  với nội dung; khi nội dung vào D1 (SDD-056) server đọc thẳng bản phát hành. Bước lạ: 400
  `UNKNOWN_STEP`; bước `gated`/`checked`: 422 `STEP_NOT_TICKABLE`.
* **Giới hạn.** zod `.strict()`: `learner_id` 1-64 ký tự, `unit` và `slot` 1-20, `activity`/`step` theo
  regex 15.2, `done` boolean; thân yêu cầu dưới 1 KB. Rate limit bằng `enforceRateLimit` với luật mới
  `aiTeenStepTick` (120 lần / 60 giây / tài khoản, khai trong `RATE_LIMITS`), 429 kèm `Retry-After`.
  Số dòng mỗi learner bị chặn tự nhiên bởi bản kê (không ghi được bước không có).
* Idempotent: tick hai lần cùng một bước không đổi gì; bỏ tick bước chưa tick trả `{tick: null}`.
* Test hành vi (skill `api-test`, `createTestDb()`): tick và đọc lại, tick lại giữ `ticked_at`, bỏ tick
  xoá, phụ huynh và mentor đọc được nhưng ghi bị 403, người lạ 401 (§15.8), chưa ghi danh 409, bước cổng
  và câu kiểm 422, bước lạ 400, slot ngoài 1-20 400, quá rate 429.

### 15.5 Client

* Luật thuần `stepState(step, { gates, results, ticks })` trả `done | open | locked` theo bảng 15.1, ở
  cạnh `journeys.ts`; card, NavBar, Mục lục và bộ đếm "k/N bước" đều gọi nó (một chỗ khai luật xong).
* Tick là lạc quan: card đổi ngay, PUT gửi ngay (không độ trễ, một lần bấm là một ý), lỗi thì trả ô về
  trạng thái cũ và báo một dòng.
* Chưa ghi danh: tick giữ ở `localStorage` (`nemo12.aiTeen.steps.<learner>.<course>`, bọc try/catch),
  card nói "Ghi danh course để lưu tiến độ trên mọi máy"; ghi danh xong client gửi các tick ấy một lượt
  (hợp hai bên, không xoá tick nào ở server).
* Dịch slot ở đúng ranh giới kho như §12.4 (`slots.ts`): giao diện làm việc theo vị trí, kho theo slot.

### 15.6 Mentor, phụ huynh và Evidence Registry

* ✍️ **Không ghi vào Evidence Registry.** Tick không chứng minh hiểu hay làm được gì; đưa nó vào sổ
  bằng chứng là đúng lỗi SRC-849 (đếm công sức như năng lực). Bằng chứng của lesson vẫn là trắc nghiệm
  và thực hành (§11). Không nguồn `EVIDENCE_SOURCES` mới, Learner Model không đổi.
* **Mentor** (Dolphin, tab AI Teen của §12 trang lát cắt): mỗi lesson thêm dòng "k/N bước đã tự đánh
  dấu xong · lần cuối DD.MM.YYYY", tính lúc đọc từ bảng (`modelPage.ts`). Đặt cạnh "đã qua" để mentor
  thấy chỗ đáng hỏi: tick đủ mà chưa qua câu kiểm, hay qua câu kiểm mà bỏ trống bước thực hành.
* **Phụ huynh**: đọc được qua cùng route GET (quan hệ gia đình); Marlins chưa có màn AI Teen nên chưa
  hiện gì. Khi có, chỉ hiện số đếm theo lesson và ngày, không hiện giờ từng tick.

### 15.7 Riêng tư

* Dữ liệu chỉ là id + giờ, không có chữ tự do của learner; không gửi bên thứ ba, không vào analytics.
* Không route công khai nào trả tick; mentor đọc qua `canAccessLearner`, nên lượt đọc của vai nội bộ
  vào `audit_log` như mọi dữ liệu learner.
* Xoá learner xoá tick (CASCADE). Rút course không xoá tick: ghi danh lại thì tiến độ còn nguyên, như
  kết quả bài tập.

### 15.8 Đã dựng (07.10.2026, SRC-1283)

| Phần | Ở đâu |
| --- | --- |
| Bảng | `migrations/0339_ai_teen_step_ticks.sql`, đúng §15.3 |
| Bản kê bước | `workers/api/src/modules/aiTeen/stepCatalog.ts`, sinh bằng `node scripts/aiteen/gen-api-catalog.mjs` từ CHÍNH `lessonJourneys` + `stepKind` của learn (plugin esbuild thay `import.meta.glob`), nên không có bản luật thứ hai; `steps.test.ts` bên learn đối chiếu lại |
| API | `GET` / `PUT /v1/ai-teen/courses/{courseId}/steps` ở `routes.ts`, dịch vụ `steps.ts`; mã lỗi mới `NOT_LEARNER` (403), `UNKNOWN_STEP` (400), `STEP_NOT_TICKABLE` (422) trong `shared/errors.ts` và [errors.md](../../reference/errors.md); luật `aiTeenStepTick` trong `RATE_LIMITS` |
| Luật xong | `stepKind` + khoá ở `apps/learn/src/aiTeen/steps.ts`; luật xong vẫn là MỘT hàm, `lessonStepState` (`journeys.ts`), nay nhận thêm `ticks`; `lessonStepCount` cho "k/N bước" |
| Client | `useStepTicks.ts` (lạc quan, trả về khi lỗi, bản trình duyệt khi chưa ghi danh); ô tick trong `LessonOutline` (card và sheet), nút "Xong bước này" cuối bước `self`; "k/N bước" đầu card và trong Mục lục (`CourseTree.tsx`) |
| Dolphin | `modelPage.ts` trả `steps` (k/N theo lesson), `apps/mentors/src/AiTeenReport.tsx` hiện trong thẻ unit |

Quyết định lúc dựng:

* ✍️ **Người lạ nhận 401, không phải 403.** Ca test ở §15.4 ghi "người lạ 403", nhưng luật chung của
  [errors.md](../../reference/errors.md) là 401 cho mọi ai không có quyền với dữ liệu trẻ em, vì 403 lộ
  rằng learner ấy có thật. `NOT_LEARNER` 403 chỉ trả cho người ĐÃ đọc được learner (phụ huynh, mentor,
  staff), nên không lộ thêm gì.
* ✍️ **Bước `self` xong CHỈ khi được tick.** Trước đây `question` tự "xong" khi qua phần đọc 1, `output` /
  `next` khi lesson đã qua; bảng §15.1 nói `self` xong khi learner tick, nên các luật suy đoán ấy bỏ hẳn,
  để không có hai sự thật cho một bước. Hệ quả: learner cũ mở lại lesson đã học sẽ thấy các bước `self`
  chưa tick (đúng nghĩa: em chưa từng tự khai).
* ✍️ `q0` (lesson không có câu kiểm) là bước `checked`: luôn xong, không tick tay.
* ✍️ **N của Dolphin là số bước `self`** ("k/N bước đã tự đánh dấu xong"): server chỉ biết tick và bản kê,
  còn "k/N bước" của learner trong card đếm MỌI bước theo `lessonStepState`. Hai con số đo hai thứ khác
  nhau và chữ nói rõ cái nào là cái nào.
* ✍️ **Bản trình duyệt có dấu `pending`.** Bản sao trình duyệt còn làm gương cho server (tải lại khi mất
  mạng vẫn thấy tick), nên chỉ tick làm lúc CHƯA ghi danh mới được gửi lên khi ghi danh; không có dấu ấy
  thì một tick đã bỏ ở máy khác sẽ sống lại từ bản sao của máy này.
* Bước có cổng / câu kiểm hiện ô viền đứt (không bấm được, `title` nói cách xong), tick đặc khi xong; ô
  tick của bước `self` là `role="checkbox"` đứng CẠNH nút bước (control lồng trong button là HTML sai và
  không tới được bằng bàn phím), vùng chạm 40px.

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

### 16. Vòng duyệt nội dung 08.10.2026 (SRC-1297)

Chủ dự án 08.10.2026 uỷ quyền duyệt toàn bộ nội dung đã soạn ("Bạn quyết toàn bộ và làm hết đi").
Vòng này chỉ sửa CHỮ: không đổi số lesson, slot, phần, câu, phương án hay khoá cổng.

* **Cổng đọc và câu kiểm (18 unit, `gates/*`, `lessons/*`)**: soát theo CD-8c và skill course-design
  §5b-§5g. Đáp án dài nhất (tính theo chữ, dài hơn cả hai nhiễu): sửa 250 câu cổng và 246 câu kiểm,
  nay không còn ca nào. Mâu thuẫn: 6 (AI Teen 1 U1: 2; AI Teen 3: 4 về số liệu và luồng), cộng 1 câu đáp
  không khớp dạng câu hỏi. Nhiễu nửa đúng: 1 (AI Teen 3 U4). Tham chiếu cũ "bài trước"/"lesson đầu":
  4\. Ký tự cấm: không còn ca nào; một dấu ngoặc thừa ở AI Teen 1 U1 đã gỡ.
* **Phát biểu kỹ thuật được làm cho chính xác và có rào**: phím tắt DevTools (tuỳ trình duyệt và hệ
  điều hành, Safari phải bật menu Develop); vị trí trong ảnh (chỉ khi camera có quyền vị trí, nhiều mạng
  xã hội gỡ khi tải lên, gửi dạng file hay đặt trên web riêng thì thường còn); nslookup (IP mẫu thuộc dải
  tài liệu/riêng, ghi "máy em sẽ khác"); chữ hoa chữ thường trong tên file (nhiều máy chủ web phân biệt);
  cảnh báo chứng chỉ (chữ tuỳ trình duyệt, https chỉ là kết nối được mã hoá, không chứng minh trang đáng
  tin); CNAME ở tên miền gốc; giới hạn tuổi tài khoản dịch vụ; Teachable Machine (huấn luyện trong trình
  duyệt, dữ liệu chỉ rời máy khi lưu hoặc tải mô hình lên).
* **Nhãn khung tiếng Anh**: U2 "Question / Size / Check / Spark" đổi thành **Question / Size / Check /
  Hook** (Hook là từ quen cho "điều kéo người đọc vào"); U4 giữ **Fixed parts / Slots**, đổi "Slot notes"
  thành **Slot hints** (gợi ý cho chỗ trống), áp đồng bộ ở bài đọc, cổng, câu kiểm và outcome.
* **Outcome (CD-6)**: bỏ mọi dấu "drafted / chờ chủ dự án duyệt" ở course, unit, lesson. Sửa 9 Lesson
  Outcome: gọn lại câu ba vế, bỏ khẳng định tuyệt đối ("chỉ ... mới", "ngay lập tức", "mất tác dụng"),
  chữa outcome Feedback loop mâu thuẫn với lesson kế, đồng bộ nhãn Slot hints.
* **Chỉ báo skill (§13.5)**: `source` đổi từ `"nemo-draft"` thành `"nemo"`; chỉ báo Level 1 của "Prompt AI
  effectively" đổi thành bốn phần mục tiêu, bối cảnh, yêu cầu, giới hạn cho khớp Unit 1.
* Seed nội dung sinh lại bằng `scripts/aiteen/export-content.mjs`; nạp qua seed-data (lệnh ở đầu từng
  file `scripts/aiteen/seed-content-ai-teen-*.sql`).
