---
url: https://docs.nemo12.com/quality/audits/2026-09-03.md
description: >-
  Audit #013 (03.09.2026): rà toàn hệ theo 6 chiều bảo mật, chi phí, đúng đắn,
  giao diện, vận hành, test/docs, kèm thứ tự sửa.
---

# Audit #013 — Toàn hệ theo 6 chiều (2026-09-03, commit `df9e373`)

Chỉ đạo SRC-674: **rà toàn bộ hệ thống, ra báo cáo, rồi nâng cấp dần theo báo cáo** (thứ tự sửa tự
quyết, commit liên tục, 30 phút push + merge một lần). Bản này là báo cáo; phần "Trạng thái" trong các
bảng dưới được cập nhật ngay trong phiên khi một phát hiện được sửa và merge.

Giống [Audit #005 (2026-08-26)](2026-08-26.md), lần này **không chấm lại 255 chỉ báo** của
[Audit Standard v0.2](../audit-standard/index.md): thang điểm toàn hệ gần nhất là
[Audit #011](2026-08-22-004.md) và một lần chấm điểm nữa không đổi được gì nếu chưa sửa. Mỗi phát hiện
dưới đây nối về chỉ báo AS tương ứng để lần chấm điểm kế thừa được.

## 1. Phương pháp và phạm vi

| Bước | Cách làm | Kết quả |
| --- | --- | --- |
| Chốt phạm vi | `origin/main` @ `df9e373` (2026-09-03), cây riêng `session/audit-2026-09-03` | 12 app · 3 worker · 29 module API · 195 migration · 118 file test · 468 file docs |
| Máy chạy trước | `typecheck` · `lint` · `test` · `check:docs` · `audit:bounds` · `contract:diff` · `verify:bindings` | typecheck ✓ · lint ✓ · contract ✓ (298 route, 0 breaking) · bindings ✓ · **`check:docs` ✗** (em dash, xem U-1) · **`test` ✗ local** (timeout, xem T-5) · `audit:bounds` ✗ (đã biết, REQ-KNW-17) |
| Rà bằng agent | 6 agent đọc code song song, mỗi agent một chiều, bắt buộc bằng chứng `file:dòng`, tự loại phát hiện không xác minh được | 13 + 17 + 18 + 22 + 24 + 23 = **117 phát hiện**, sau khử trùng còn **106**: 🔴 15 · 🟡 55 · ⚪ 36 |
| Đối chiếu chéo | Cùng một lỗi được ≥2 chiều nhìn thấy độc lập thì ghi một dòng, giữ chiều nhìn rõ nhất | 11 dòng trùng (em dash U-1, migration-trước-code I-1/T-2, README số migration C-10/I-15/T-11, cổng em dash chỉ quét 2 app U-10/I-22, bundle P-10/U-7, dictation không rate limit S-9/P-12…) |

Sáu chiều và chỉ báo AS đối chiếu: **S** bảo mật & riêng tư (AS-07) · **P** chi phí & hiệu năng
(AS-10, AS-08) · **C** đúng đắn & tin cậy (AS-03, AS-04, AS-08) · **U** trải nghiệm & design system
(AS-09) · **I** vận hành, CI, công cụ (AS-08, AS-04, AS-03.5) · **T** test & lệch tài liệu (AS-01,
AS-05, AS-06).

Bối cảnh cần biết khi đọc: hoá đơn Cloudflare tháng 8/2026 nhảy bất thường (D1 đọc dòng ~$158,
Workers AI ~$86, hoá đơn IN-77746678). **Một phiên khác đang xử lý** migration `0196_hot_path_indexes`
và công tắc KV `ai_cron_paused`; báo cáo này không lặp hai việc đó.

## 2. Mười lăm phát hiện 🔴 (chặn phát hành hoặc đang mất tiền/dữ liệu)

| # | Chiều | Phát hiện | Bằng chứng | Chỉ báo | Trạng thái |
| --- | --- | --- | --- | --- | --- |
| S-1 | Bảo mật | **IDOR module `curriculum`**: 6 route bài ngẫm / chặng / cụm nhận `learner_id` qua query/body mà không có `requireLearnerAccess`; tài khoản bất kỳ đọc và ghi đè đoạn văn trẻ tự viết về mình của mọi learner | `curriculum/routes.ts:56-186`, `service.ts:251-333` | AS-07.2.1 🔴, AS-07.2.3 | ✅ sửa `5c5883b` + test |
| S-2 | Bảo mật | **IDOR module `dictation`**: 8 route learner (options · start · list · audio · check · leaderboard · delete) không chốt quyền; kẻ có một tài khoản liệt kê/nghe/xoá bài của learner khác và khởi động workflow sinh bài (model + TTS) cho `learner_id` bất kỳ, mỗi id 3 bài | `dictation/routes.ts:198-312`, `practice.ts:67-106` | AS-07.2.1 🔴, AS-10.5.5 | ✅ sửa `5c5883b` + test |
| S-3 | Bảo mật | **Lưới `authCoverage` lớp 2 mù**: chỉ quét `{learnerId}` trong path nên hai module trên lên production mà CI xanh; bảng xếp hạng còn trả `learner_id` của mọi trẻ cùng khối, tức cung cấp sẵn danh sách id để khai thác S-1/S-2 | `authCoverage.test.ts:168`, `practice.ts:242-270` | AS-07.2.1 🔴, AS-05.3.2 | ✅ sửa `5c5883b`: quét cả `learner_id` trong schema, bảng xếp hạng bỏ id |
| P-1 | Chi phí | **Cron `*/5` gọi AI không có trần USD/ngày**; `runPrompt` bỏ `res.usage` nên không ghi token/USD ở đâu. Trần lý thuyết 30 lượt × 288 nhịp = 8.640 lượt/ngày ≈ $26/ngày | `shared/aiBudget.ts:34`, `index.ts:302-335`, `prompts.ts:1424-1440` | AS-10.5.1 🔴, AS-10.5.2 | ✅ đợt 3: `runPrompt` đọc `usage`, log `ai_call`, đồng hồ tiền trong ngân sách nhịp; trần `AI_DAILY_USD_CAP=3` USD/ngày ở KV, chạm là nhịp không gọi model |
| P-2 | Chi phí | **9/12 chặng xưởng curriculum sinh lại mãi target đã bị từ chối** (chỉ story · outcomes · focus có log loại trừ); hai chặng dịch lấy `LIMIT` không `ORDER BY` nên dòng hỏng đứng đầu hàng chờ vĩnh viễn. Mỗi target kẹt ≈ 288 lượt/ngày ≈ $26/tháng | `curriculum/authoring.ts:133-137,185-190,348,472-474,510-513,541-548,615,672,867` | AS-10.5.1 🔴 | ✅ đợt 3: `notRecentlyLogged` ở cả 9 chặng (rejected nghỉ 7 ngày, failed 1 ngày), chặng dịch có `ORDER BY` + khoá theo dòng; test trên SQLite thật |
| P-3 | Chi phí | **Máy dịch b21 chạy ngoài ngân sách chung**, `RETRY_CAP=12` hỏi lại từng mẩu kẹt mỗi nhịp, đẩy nhịp cron sát trần khoá 240 s (khoá KV không nguyên tử nên nhịp sau chạy chồng) | `index.ts:326`, `b21/translate.ts:130-136,190-197` | AS-10.5.1 🔴, AS-08.2.1 | ✅ đợt 3: `runB21Translation(env, budget)` xin lượt cho cả lô lẫn hỏi lại; mẩu kẹt sau hỏi lại ghi `content_authoring_log` stage `b21-translate`, nghỉ 7 ngày |
| C-1 | Đúng đắn | **Mastery bị áp hai lần khi POST answer lặp**: `learner_evidence` là `INSERT OR IGNORE` nhưng `learner_skill_state` là `ON CONFLICT DO UPDATE` vô điều kiện; phiên đã `completed` vẫn nhận answer. Hồ sơ năng lực nói N+1 lần đo trong khi sổ bằng chứng có N dòng | `learning/routes.ts:684-686,733-786,878-890` | AS-05.2.2 🔴, AS-04.2.2 | ✅ đợt 3: soi `learner_evidence.event_id` cùng chuyến với `prev`, lượt lặp trả feedback kèm `duplicate: true` và không ghi gì; phiên không `in_progress` → 409; lab cùng cách; test D1 gọi hai lần |
| U-1 | Giao diện | **Cổng em dash (SRC-379) đỏ trên `main` với 408 chỗ** trong 8 chương ebook + `ebookLibrary`; CI không thấy vì `check:docs` chỉ chạy khi commit đụng `docs/` (xem I-5) | `check-no-emdash.mjs`, `ci.yml:148-150` | AS-09.4.1, AS-08.4.1 | ✅ sửa `ef0e9b0` (8 file); 4 chương mới lên sau đó đang dọn |
| U-2 | Giao diện | **Bản canonical trong `packages/design-system` lệch với app, app lệch nhau**: `palette.ts` của web/coral/b21 còn `aqua101 #F2F2F2` (bug SRC-182 "hình biến mất" tái diễn ở nemo12.com), `Tour`/`ReferralPanel` canonical còn `n12-*`, `SwimmingLoader` mentors lệch, shadcn `card`/`dialog` lệch. Gốc: package chỉ `exports` CSS, không app nào import được TSX | md5 12 bộ file; `packages/design-system/package.json` | AS-09.2.2 | ⏳ |
| U-3 | Giao diện | Menu avatar Marlins hiện `👤 Profile · ⚙️ Settings · ↪ Logout`, nav nemo12.com hiện `Login`: chữ tiếng Anh + emoji làm icon ở nơi bố mẹ bấm mỗi ngày | `marlins/AvatarMenu.tsx:35-42`, `web/SiteNav.tsx:97,110` | AS-09.4.1, AS-09.4.2 | ✅ đợt 3: "Hồ sơ · Cài đặt · Đăng xuất" với icon lucide, "Đăng nhập" ở nav web |
| I-1 | Vận hành | **Code api lên production TRƯỚC migration, không cổng nào chặn**: `ci.yml` deploy api trên mọi push đụng `workers/api` hoặc `migrations/`, migration chỉ áp khi bấm tay `deploy-api.yml`; lý do cũ ("token deploy không cần quyền D1") đã hết hiệu lực vì cùng một token. 20 commit có migration trong 7 ngày, 30 lần bấm tay từ 26/08, commit `23b2368` lệch ~12 phút | `ci.yml:268-274`, `deploy-api.yml:43-51`, `scope.sh:114` | AS-04.6.4 🔴, AS-03.5.3 | ⏳ |
| I-2 | Vận hành | `scope.sh` lấy mốc "lần ci xanh gần nhất" không lọc `--event push`: một lần bấm tay ci xanh trên `main` (không deploy gì) làm mọi thay đổi trước đó không bao giờ được deploy, màn hình vẫn xanh | `scope.sh:34` | AS-08.4.1 | ✅ sửa `5fcae84` |
| I-3 | Vận hành | `rollback.yml` chưa từng chạy, ghi `nemo12-mentors` trong khi worker thật là `nemo12-dolphin`, thiếu 6/15 worker, trỏ `docs/operations` không tồn tại | `rollback.yml:27,80-81,125` | AS-08.4.3 🔴 | ✅ sửa `5fcae84` (chưa diễn tập) |
| I-4 | Vận hành | Diễn tập backup/restore (22/08, mốc 67 migration) và đối soát migration (20/08) đều quá hạn so với chính ngưỡng Pilot Gate (AS-04.5.5, AS-04.6.1 ≤7 ngày); từ đó thêm 129 migration và một vụ đổi số 0162→0176 | `docs/ops/backup-restore-drill.md:19-21`, `migration-reconciliation.md:8` | AS-04.5.5 🔴, AS-04.6.1 🔴 | ⏳ việc tay |
| T-1 | Docs | **SDD-002 mô tả thuật toán mastery/confidence khác code**: doc `α = reliability×recency×difficulty×independence×lr`, confidence có `source_diversity` ("10 câu cùng dạng → 0.48"); code `lr = (0.35+0.35·surprise)/(1+0.6·count)·reliability`, confidence `1−0.55^n` (10 câu cùng dạng → 0.9975); rapid `2500 ms` cố định thay vì `max(2.5s, 20% expected_time)`. `reference/engines.md` ghi đúng code, tức SDD và reference mâu thuẫn | `sdd-002:82-96,214-218` ↔ `knowledge/mastery.ts:42-68` | AS-05.4.5, AS-05.1.4 | ✅ đợt 4 (docs): SDD-002 §7/§8/§18 thêm khối "v1 ĐANG CHẠY" với công thức thật và nói rõ phần chưa có (diversity factor, expected_time); việc hiện thực `source_diversity_factor` ghi là việc kế, chưa có SRC |
| T-2 | Docs | README và `migrations/README.md` nói migration được áp trước code khi push; thực tế push chỉ deploy code (cùng gốc với I-1). `migrations/README.md` tự mâu thuẫn giữa §1 và đoạn cuối, và ghi "số tiếp theo `0070`" khi đã có 195 | `README.md:45-47`, `migrations/README.md:15-22`, đoạn cuối | AS-01.4.1 🔴 | ⏳ |
| T-3 | Docs | `reference/schedules.md` khai 1 cron, api chạy 4 (`0 21`, `0 19`, `10 *`, `*/5`) + foundry `*/15`; luật "không có cron ẩn" (REQ-DOC-02) bị vi phạm 3 lần; cổng freshness không bắt vì chỉ đo ngày commit của chính doc | `schedules.md:18-22,97` ↔ `workers/api/wrangler.jsonc:14` | AS-01.4.1 🔴, AS-08.3.1 | ⏳ |
| T-4 | Docs | PRD-001 đánh ✔ (đã hiện thực) REQ-EXAM-05..08 trong khi Real Exam Bank chưa build: SDD-012 `draft`, không migration nào tạo `exam_problems`, `'authentic'` không có trong code, module `exams` 0 test | `prd-001:641-644`, `sdd-012:6` | AS-01.3.2, AS-02.4.5 | ✅ đợt 4: bốn REQ về ⏳ kèm lý do + ngày ở PRD §7 |
| T-5 | Test | **Testkit nạp lại 195 migration (11,7 MB) ở mỗi `beforeEach`**: `npm test` local 18 file / 96 test đỏ vì `Hook timed out 10000ms`; CI test 52 s (23/08) → 222 s (03/09); mỗi migration nội dung mới cộng vào MỌI test | `runlog.testkit.ts:73-84`, 21 file gọi trong `beforeEach` | AS-05.3.5, AS-08.4.1 | ✅ đợt 7: khuôn schema `VACUUM INTO` một lần mỗi worker + copy file mỗi test; 34 file D1 chạy 13 s (trước ~300 s, 96 test đỏ giả) |

(Bảng trên có 19 dòng vì S-1/S-2/S-3 và T-2/T-3 là hai cụm cùng gốc; đếm theo gốc lỗi là 15.)

## 3. Phát hiện 🟡 theo chiều (55, rút gọn, đủ bằng chứng trong code)

### 3.1 Bảo mật & riêng tư (S-4 … S-10)

* **S-4** Tool Plane: nhánh token "đường cũ" (`tp_agents.token_hash`) tin header `X-Nemo12-User-Id` không kiểm chứng; grant learning-agent scope `user` nên ai cầm token tự nhận là admin và đọc hồ sơ mọi trẻ; token production đang cấp đúng đường cũ; `/mcp` không rate limit. `tool-plane/src/auth/authentication.ts:49-70`, `scripts/tool-plane/seed-*.sql`. 🟡 đợt 5: token đường cũ **không được đóng vai người có vai nội bộ** (admin/staff/mentor) qua header — đúng kịch bản khai thác; vai gia đình vẫn đi qua để ba token đang cấp không gãy, kèm log cảnh báo. **Việc tay còn lại:** phát hành lại 3 token bằng `issue-agent-token.mjs --user` rồi bỏ hẳn nhánh cũ; rate limit `/mcp`.
* **S-5** Thư tổng kết (tên trẻ + điểm + kỹ năng yếu) BCC mặc định về Gmail cá nhân, hardcode trong mã, không khai ở trang minh bạch dữ liệu. `digest/email.ts:38`, `wrangler.jsonc:74`, `privacy/routes.ts:297-302`. ✅ `5fcae84`: địa chỉ chỉ từ cấu hình, trang minh bạch khai bên nhận này khi bật. Quyết định giữ BCC là của chủ dự án (chỉ đạo 27/08), giữ nguyên.
* **S-6** Diễn đàn và sự kiện: `author_name` rơi về **email** khi user chưa có `display_name` (tài khoản Workspace for Education) và hiện cho mọi gia đình. `forum/routes.ts:33`, `events/routes.ts:80`. ✅ `5fcae84`: nhãn ẩn danh khi ghi, che `@` khi đọc.
* **S-7** `POST /v1/learners/{id}/context-events`: mọi dòng `family_members` (kể cả role `learner`, `supporter`) ghi được lời khai với `source_role='parent'`; Planning Engine và mentor đọc như lời bố mẹ. `models/routes.ts:54-62`. ✅ `5fcae84`: chỉ owner/guardian.
* **S-8** Yêu cầu xoá dữ liệu chỉ được INSERT (`data_deletion_requests`), không mã nào thi hành; không endpoint export; `retention_policies.keep_days` không có job xoá; trang "dữ liệu nào đang lưu" kể 6 nhóm trong khi ~44 bảng có `learner_id`. `privacy/routes.ts:354-357`, `policies.ts:7-26`. **Sửa:** cron `runDeletionRequests` đi qua danh sách bảng sinh từ `migrations/` (test khớp số bảng), `GET /v1/privacy/export`.
* **S-9** `RATE_LIMITS.submit`/`crawl` khai nhưng không dùng; answer, lab evidence, dictation start/check không rate limit. `shared/ratelimit.ts:31-40`. ✅ đợt 4: `submit` gắn vào practice answer, lab evidence, dictation check; rule mới `dictationCreate` 20 lượt/ngày/tài khoản cho khởi động workflow sinh bài.
* **S-10** Admin cấp/thu quyền, giao mentor, liệt kê learner không ghi `audit_log` (trong khi replay DLQ, sync retention có). `admin/routes.ts:62-110`, `observability.ts:259,453`. ✅ đợt 4 cho ba route ở `admin/routes.ts` (`role.granted/revoked`, `mentor.assigned/unassigned`, `learner_data.list`); hai chỗ trong `observability.ts` còn chờ.

### 3.2 Chi phí & hiệu năng (P-4 … P-12)

* **P-4** `assessment_responses` không có chỉ mục theo `learner_id`: ba câu nóng (bắt đầu luyện, mở cockpit, Learner Model engine) quét cả bảng. **Đính chính sau đối chiếu với phiên giữ 0196:** bảng hiện chỉ ~829 dòng nên chưa góp đồng nào vào hoá đơn tháng 8; đây là chỉ mục **phòng trước** cho bảng tăng nhanh nhất hệ, không phải chỗ đang chảy máu (chỗ đó là `items` thiếu chỉ mục dẫn đầu `node_id`, mục 1-3 của 0196). ✅ Hai chỉ mục đã được gộp vào `0196_hot_path_indexes` (mục 4, `(learner_id, created_at DESC, item_id)` để câu `SELECT DISTINCT item_id` đọc trọn trên chỉ mục), chờ phiên đó push.
* **P-5** Cron phút 10 mỗi giờ sàng lọc lại 1.548 candidate "giữ lại" y hệt, UPDATE + enqueue mỗi giờ (~37k ghi/ngày cho việc không đổi kết quả). `content/backfill.ts:187-226`. ✅ đợt 5: chỉ chọn candidate chưa có `quality_json` hoặc sàng lần cuối quá 7 ngày.
* **P-6** Cockpit ~30 lượt D1 tuần tự mỗi lần mở: đọc `skill_nodes` của môn 5 lần, `COUNT items` theo `role` không chỉ mục, `FOUNDATION_SQL` chạy 3 lần. `knowledge/routes.ts:597-813`.
* **P-7** WF-04 (8 engine, ~40 SELECT + ~35 ghi tuần tự) được `await` trong request nộp bài chẩn đoán/đề thi/onboarding, learner chờ 1,5-3 s; `learning/routes.ts:343` đã làm đúng (`runAllModelsInBackground`). ✅ đợt 5 cho 4 chỗ (nộp đề thi, 3 route khai mục tiêu/lịch thi); nộp chẩn đoán giữ đồng bộ vì cockpit trả về ngay sau cần model mới.
* **P-8** Endpoint đọc công khai (whale, b21, 9 route showcase public) đập thẳng D1 mỗi request, chỉ 2 chỗ dùng `withCache`; `Cache-Control: public` trên custom domain không được CDN cache nếu không dùng Cache API. 🟡 đợt 7: `withCache` KV cho `/v1/whale/countries` (1 h) và ba endpoint đọc b21 (5 phút); showcase public và Cache API còn chờ.
* **P-9** Không có chỉ số `rows_read`/chi phí D1 nào trong log; hoá đơn bất ngờ kế tiếp sẽ lại lộ ra khi Cloudflare gửi invoice. **Sửa:** proxy `env.DB` cộng dồn `meta.rows_read` theo request/cron tick, `logEvent d1_heavy` khi >50k dòng; script `wrangler d1 insights` hằng ngày; billing notification.
* **P-10 / U-7** Bundle: learn `index` 1.052 kB (explore/configs 249 kB import tĩnh), marlins 867 kB (`parentCourses` 207 kB ở trang chủ), web 606 kB; `React.lazy` cả repo = 1; không `manualChunks`.
* **P-11** Mọi request có phiên = 1 UPDATE `sessions.last_seen_at` tuần tự trước handler (ghi D1 đắt gấp 1000× đọc). `auth/service.ts:171-174`. ✅ đợt 5: chỉ ghi khi dấu cũ hơn 15 phút.
* **P-12 / S-9** Dictation Forge: learner tự kích AI (Llama + TTS), hạn mức bỏ qua bài `failed`, không rate limit.

### 3.3 Đúng đắn & tin cậy (C-2 … C-11)

* **C-2** Xoay phiên đua nhau: revoke rồi INSERT phiên mới, 3-5 request song song cùng cookie cũ đúng ngày thứ 7 → "Phiên đã hết hạn" giả hoặc hai phiên mới, một mồ côi 30 ngày. `auth/service.ts:132-172`. ✅ đợt 6: tạo con trước, giành revoke cha bằng `UPDATE … WHERE revoked_at IS NULL` + `meta.changes`, thua thì xoá con của mình; token vừa xoay còn ân hạn 60 s (chỉ khi có con, logout thật thì không); test 5 request song song → 1 con, 0 lần 401.
* **C-3** Accept lời mời không guard `status='pending'` trong UPDATE; hai tài khoản dùng cùng mã → cả hai vào family, `learners.user_id` bị ghi đè. `invitations/routes.ts:266-319,384-419`. ✅ đợt 6: giành lời mời bằng UPDATE có điều kiện trước, `changes ≠ 1` → 409; `UPDATE learners … AND status='invited'`.
* **C-4** 73 khối `catch {}` "chưa chạy migration" nay nuốt mọi lỗi D1, cả đường ghi: INSERT `assessment_sessions` lỗi tạm → INSERT lại không `adaptive_json` → chẩn đoán tĩnh không ai biết. `knowledge/routes.ts:164-174`, `content/routes.ts:106-110`.
* **C-5** "Ngày" tính UTC ở 8 chỗ, +07 ở 2 chỗ; `daysBetween` định nghĩa hai lần khác ngữ nghĩa. Orca báo "hết hạn đăng ký" từ 19:01 VN ngày hạn chót; `days_to_exam` lệch +1 lúc 00:00-07:00; lab hai lượt sáng/chiều thành hai evidence. ✅ đợt 7: `shared/time.ts` với `vnDate`/`daysUntilVn`, 6 chỗ đổi sang ngày lịch VN.
* **C-6** `dictation_sets` ghi timestamp `datetime('now')` (có dấu cách) trong khi cả hệ dùng ISO `T…Z`; Safari đọc thành Invalid Date. `0190:34-35,58`, `dictation/routes.ts:140`.
* **C-7** Hợp đồng API mù ở 242/298 operation (`z.any()`), 78 `as never`: đổi tên field response không làm `contract:diff` đỏ.
* **C-8** Tạo bản ghi không idempotent: `semester_exam_schedule` không UNIQUE `(learner_id, subject_id, exam_date)` → Constraint Model liệt kê hai kỳ thi cùng ngày. ✅ đợt 6 ở tầng route (tìm dòng cùng môn + ngày trước khi INSERT); UNIQUE ở bảng chờ migration.
* **C-9** Foundry: `content_runs` treo `running` vĩnh viễn nếu D1 lỗi lúc ghi trạng thái cuối, không sweeper; cron và nút tay bị 429 "trần song song" cho tới khi sửa tay. ✅ đợt 6: cron `*/15` quét run `running` quá 2 giờ → `failed`, chạy trước cả cờ `FOUNDRY_AUTO_DRAIN`.
* **C-10 / I-15 / T-11** `migrations/README.md:15` "số tiếp theo `0070`" lệch 126; không script cấp số như `src-new.mjs`; hai phiên từng lấy trùng 0162.
* **C-11** `DELETE lab_favorites … .catch(() => null)` rồi trả 200: bỏ thích thất bại vẫn báo thành công.

### 3.4 Giao diện & design system (U-4 … U-16)

* **U-4** `api.ts` của learn/mentors/coral/web ném lỗi kỹ thuật tiếng Anh thẳng ra màn hình trẻ (`res.json()` không bọc; `e.message` hiện ở `App.tsx:86,208`…); marlins `api.ts:100-128` (`humanError`) là mẫu đúng. ✅ đợt 8: `humanError` + bọc `fetch`/`res.json()` ở learn, coral, web, mentors (mentors giữ tiếng Anh theo quy ước của nó).
* **U-5** Không app nào xử lý 401 giữa chừng: `me` chỉ nạp lúc mount; cookie hết hạn → mọi nút báo "Chưa đăng nhập" trên dashboard, chỉ F5 mới thoát. ✅ đợt 8: `api.ts` phát `nemo12:session-expired` CHỈ khi thật sự mất phiên (401 không body, hoặc AUTHORIZATION_ERROR với đúng hai câu "Chưa đăng nhập"/"Phiên đã hết hạn…"), App của learn/marlins/mentors/coral/admin lắng nghe → `setMe(null)` → màn đăng nhập; không đụng nhánh "thiếu vai" sẵn có của coral/admin.
* **U-6** Không `ErrorBoundary` ở app nào: một exception khi render = màn trắng câm. ✅ đợt 8: `components/ErrorBoundary.tsx` + bọc `<App/>` trong `main.tsx` ở 6 app (nút "Tải lại", log component stack).
* **U-8** Modal tự chế (`div fixed inset-0 onClick`) ở AvatarMenu 5 app, ProgressViews, MasteryMap, DataQuality; shadcn `dialog.tsx` có ở 5 app nhưng 0 import: không focus trap, không Escape, không `aria-modal`.
* **U-9** Chữ dưới sàn 14 px (AS-09.2.5): learn 159 `text-xs`, `text-[10px]`/`[11px]`; button `size="xs"` 24 px.
* **U-10 / I-22** Cổng em dash chỉ quét learn + marlins; web `CorePages.tsx:20-39`, coral ~24 dòng, admin ~15, mentors ~8 đang hiển thị em dash. ✅ đợt 6: `ROOTS` = 8 app, đỏ khi quét 0 file; dọn 203 chỗ ở 25 file (web/coral/admin/mentors); sửa luôn bộ lọc regex quá rộng từng nuốt cả chữ JSX giữa hai dấu `/` (hai vi phạm thật bị giấu).
* **U-11** `useEffect` gọi API không có cờ huỷ ở gần như mọi màn chính (Learning 16, App 13, ProgressViews 10, FamilyWorkspace 10): đổi con nhanh → dữ liệu con A đè màn con B.
* **U-12** 25 chỗ `.catch(() => {})` + `then` không `catch` → skeleton đứng mãi khi API hỏng.
* **U-13** Hex cứng trong JSX ngoài vùng art: `marlins/Metrics.tsx:47-49,118-123`, `Courses.tsx:237,277`, `learn/CoursesPanel.tsx:149`, `Learning.tsx:1623-1715`, `b21/App.tsx:103`.
* **U-14** Admin (SRC-098 đòi tiếng Anh) còn ~77 dòng tiếng Việt + `window.confirm` lẫn hai thứ tiếng.
* **U-15** `scripts/gen-sitemap.mjs` ở gốc là bản cũ sinh 20 URL đã thành alias; bản thật ở `apps/web/scripts/`. ✅ đợt 5: xoá bản gốc.
* **U-16** Learn khởi động bằng chữ trần "Đang tải…"; `Ebooks.tsx:336-344` `loadChapter` không `catch`.

### 3.5 Vận hành, CI, công cụ (I-5 … I-15)

* **I-5** Các cổng kiểm **code** (deps, design-stack, shadcn-tokens/adoption, english-identifiers, no-emdash, dictation, labs-ib) treo dưới `check:docs`, mà CI và hook chỉ chạy `check:docs` khi đụng `docs/`. Commit thêm `framer-motion: latest` + code: mọi cổng xanh, deploy. Đây là gốc của U-1.
* **I-6** `wrangler deploy` không retry; 504 của Cloudflare (run 33777956153) làm job đỏ sau khi mọi cổng xanh, các app đứng sau bị `skipped`.
* **I-7** `scope.sh` bỏ sót 4 phụ thuộc build: `package-lock.json` (đổi lockfile không test, không deploy); `$design` cho `app_docs`; `content/dictation` cho admin; `gen-pearl-from-docs.mjs` cho pearl.
* **I-8** Concurrency group ci / deploy-\* / rollback tách nhau → hai `wrangler deploy` cùng worker chồng nhau, last write wins.
* **I-9** Không alerting/uptime nào; `/v1/health` và `mcp /health` sống nhưng không ai canh; DLQ đầy lúc 02:00 không ai biết.
* **I-10** `wait-for-ci.sh` fallback chỉ typecheck/lint/test, thiếu contract:diff / migrate:dryrun / verify:bindings mà `deploy-api.yml` khai; timeout 25 ≠ 30. ✅ đợt 7: fallback chạy đủ bộ (+ `check:code`), timeout 30 phút.
* **I-11** Hook pre-commit không chạy `gen:reference` khi đụng `migrations/` hay `modules/` → 3/10 run đỏ gần nhất là loại này.
* **I-12** `nemo12-api` không `upload_source_maps`, không `workers_dev: false` (foundry/tool-plane có).
* **I-13** `scripts/d1.mjs` + 5 script `--remote --yes` ghi thẳng production từ máy cá nhân, không cờ `--prod`, trái luật "chỉ qua GitHub Actions". 🟡 đợt 5: `d1.mjs` chặn mọi câu ghi trừ khi `NEMO12_ALLOW_REMOTE_WRITE=1`, chỉ tới `seed-data.yml`; 5 script gọi `wrangler d1 execute --remote --yes` trực tiếp còn chờ.
* **I-14** `docs/ops` thiếu runbook cho đúng các sự cố đã gặp: rollback, migration-trước-code, Access outage, replay DLQ. ✅ đợt 7 ba runbook: `ops/rollback.md`, `ops/migration-order.md`, `ops/dlq-replay.md` (Access outage còn chờ: cần xác nhận trên dashboard, token không có scope Access).

### 3.6 Test & lệch tài liệu (T-6 … T-16)

* **T-6** Sáu module 0 test: `exams` (submit → evidence → mastery), `portraits`, `coral` (21 route ghi), `forum`, `whale`, `orca`; `auth/routes.ts` không có test hành vi cho hai lỗ CWE-613/CWE-287 đã vá.
* **T-7** `retention-model.md` nhãn tiếng Việt cũ (`dang_chac`…) trong khi API trả `solid/refresh/at_risk/relearn`.
* **T-8** SDD-001 (`last_reviewed` 03/09) mô tả 6 app, 15 module ảo, 7 package; thực tế 12 app, 29 module, 1 package; không nhắc foundry/tool-plane. ✅ đợt 7: §1/§2/§9 ghi đúng 12 app, 3 worker, 29 module thật, 1 package, luồng CI một job.
* **T-9** `.claude/memory/nemo12-project-state.md` (mọi phiên đọc lúc khởi động) có 5 khẳng định sai: `data.nemo12.com` còn sống, docs deploy tay, migration tới 0009, test bằng curl, "có cổng grep `AI.run`".
* **T-10** `workflows.md` nói chỉ có một Cloudflare Workflow thật; hiện có ba (`ItemForge`, `DictationForge` không được nhắc).
* **T-12** SDD-017 §4 luật retrieval fail khác code; hệ số `recency = exp(−Δt/4S)` không có trong reference. ✅ đợt 4.
* **T-13** Sau khi gỡ QG-014 (SRC-672), nội dung soạn tay tầng Course không có cổng máy nào trong CI; `audit-course.mjs` chỉ được gọi từ doc; không cột `provenance`.
* **T-14** `intake.md` có 9 dòng sai số cột (SRC-122, 404, 432..436, 548, 552) → `check-src-canonical` mù 5 SRC. ✅ đợt 5: gộp ô bị `|` cắt, `|x|` trị tuyệt đối viết `abs(x)`, 5 dòng thiếu ô trạng thái được đóng ✅ (đều là việc đã xong 2026-08-21).
* **T-15** E2E chỉ ở learn, API mock hoàn toàn, không kích khi `workers/api` đổi.
* **T-16** "`prompts.ts` là đường duy nhất chạm model" không có cổng; `AI.run` gọi trực tiếp ở `http.ts`, `context-builder.ts`, `dictation/tts.ts`, `dictation/forge.ts`. ✅ đợt 5: rule eslint `no-restricted-syntax` cấm `env.AI.run` trong `workers/api/src` ngoài allowlist 3 file (prompts · tts · forge) có lý do; `http.ts`/`context-builder.ts` chỉ nhắc trong chú thích, không gọi.

## 4. Phát hiện ⚪ (36, liệt kê để không thất lạc)

S-11 không security header ở API lẫn app tĩnh, media thiếu `nosniff` · S-12 ranh giới tin cậy = cả `*.nemo12.com` (cookie Domain, CORS, CSRF) · S-13 tool-plane `tools/list` bỏ qua policy, request id hai nguồn · P-13 foundry cron `*/15` quét toàn bộ lesson lọc ở JS · P-14 digest quét toàn bộ `learner_evidence` bằng biểu thức ngày · P-15 retention refresh dựng lại `reviewContext` cho từng learner · P-16 queue consumer no-op nhưng vẫn KV claim/mark mỗi event · P-17 `SELECT *` bảng rộng, admin không LIMIT · C-12 `CREATE TABLE/INDEX` thiếu `IF NOT EXISTS` (0063, 0174) · C-13 cột thêm nhưng không ai đọc (`immutable_after_publish`, `solution_authored_by`, `prerequisites_note`) · C-14 103 export mồ côi (`fetchWithTimeout` 0 caller, TTS không timeout — ✅ đợt 5 TTS có `withTimeout`), `nowIso`/`safeJson` trùng · C-15 vote diễn đàn đọc-rồi-ghi · C-16 `withLock` KV không nguyên tử · C-17 232 `any` tập trung 6 file · C-18 22 event chưa consumer nghiệp vụ · U-17 6 gói `@radix-ui/react-*` thừa, `@types/three` ở dependencies · U-18 `div onClick` không role/tabIndex · U-19 analytics opt-out mặc định, cần quyết định trong docs privacy · U-20 1,9 MB ảnh cover social không ai tham chiếu · U-21 `exhaustive-deps` chỉ warn · U-22 8 `vite.config` + 8 `tsconfig` sao chép · I-16 13 workflow không `timeout-minutes` · I-17 action ghim major tag, rollback dùng `wrangler@4` trôi · I-18 nội suy `${{ steps.scope.outputs.test_ws }}` vào `run:` · I-19 `seed-data.yml` thiếu permissions/concurrency/timeout · I-20 `apps/web` lệch dải version, `motion` 13.1.1 → 13.2.0, không Dependabot · I-21 (✅ đợt 5) eslint không phủ `apps/web/scripts`, `design-system/scripts` · I-22 3 cổng PASS rỗng khi glob trống · I-23 `/v1/health` luôn 200 kể cả `database: error` · I-24 `main` không branch protection (GitHub free) · T-17 ~100 quyết định ✍️ chờ chủ dự án, mục "AI Gateway còn chờ" lỗi thời · T-18 authCoverage lớp 2 là source-regex, chưa ghi ngoại lệ QG-005 · T-19 `migration-reconciliation.md` "57 migration" · T-20 top-10 doc đứng yên khi code đổi (schedules 33 commit, workflows 38, sdd-022 30…) · T-21 `check-coverage`/`bounds.test` PASS rỗng · T-22 SRC-674 ✅ trước khi có canonical (đóng bằng chính file này) · T-23 CI test theo workspace, apps không chạy lại khi api đổi.

## 5. Những gì hệ đang làm TỐT (giữ làm khuôn)

1. **Xác thực**: Google id_token và Access JWT ghim issuer/audience/alg và có test ký khoá thật; phiên 256-bit, DB chỉ giữ SHA-256, cookie `HttpOnly; Secure; SameSite=Lax`, xoay 7 ngày, logout revoke đúng bản (`auth/service.ts`, `verify.test.ts`, `access.test.ts`).
2. **Lưới xác thực lớp 1** hỏi bảng route lúc chạy, danh sách PUBLIC đóng có lý do, có sàn chống xanh giả (`authCoverage.test.ts:31-119`).
3. **SQL bind toàn bộ, không secret trong repo hay lịch sử git**; input người gõ trong workflow đi qua `env`; `npm audit --omit=dev` = 0.
4. **Một đường duy nhất chạm model qua AI Gateway có timeout và metadata** (`shared/prompts.ts`, `foundry/models.ts`); foundry có 5 chốt + trần USD/ngày; chia lô IN-list ≤100 đã thành tiền lệ.
5. **Testkit dựng D1 thật từ chính `migrations/`** (lệch tên cột là đỏ, không phải mock); digest chống gửi trùng ở tầng dữ liệu (PK + `meta.changes`); queue consumer idempotent đúng thứ tự.
6. **CI một job, mọi cổng đỏ thật**, mốc deploy cộng dồn, `migrate:dryrun` từ D1 rỗng, số migration liên tục, `gen:reference` diff, `contract:diff`; `wrangler.jsonc` kỷ luật (observability 15/15, `compatibility_date` đồng nhất, DLQ khai báo).
7. **Frontend**: một nguồn route SEO cho web (`routes.ts` → prerender → sitemap → aliases); ebook/KaTeX/three tách chunk đúng chỗ nặng; nền a11y (`focus-visible`, `min-h-11`, `alt`, `lang`); cổng ratchet shadcn-adoption/tokens/design-stack, 0 import cấm.
8. **Docs reference đúng hằng số đang chạy** (`engines.md`, `retention-model.md`); sổ intake 674 SRC liên tục không trùng; 0 `.skip/.only`, 0 TODO trong 3 worker.

## 6. Thứ tự sửa (tự quyết theo SRC-674) và trạng thái

Nguyên tắc xếp: (1) dữ liệu trẻ em đang hở → (2) tiền đang chảy → (3) cổng đang nói dối (vì cổng dối
thì mọi sửa sau đó không được bảo vệ) → (4) lỗi im lặng với learner → (5) docs dẫn người sau đi sai
→ (6) phần còn lại theo chi phí/lợi ích.

| Đợt | Việc | Đóng | Trạng thái |
| --- | --- | --- | --- |
| 1 | Chốt quyền curriculum + dictation, mở rộng lưới authCoverage, bảng xếp hạng bỏ `learner_id`, 2 file test authz | S-1 S-2 S-3 | ✅ `5c5883b`, merge `7a97af1` |
| 1 | Dọn 408 em dash 8 chương ebook | U-1 | ✅ `ef0e9b0`; phiên viết ebook (SRC-673) sau đó tự dọn toàn bộ 13 quyển từ nguồn JSON (`1e12e91`) và thêm luật vào thước soạn |
| 1 | BCC digest chỉ từ cấu hình + khai minh bạch; forum/events không lộ email; context-events chỉ owner/guardian; `scope.sh --event push`; `rollback.yml` đúng tên worker | S-5 S-6 S-7 I-2 I-3 | ✅ `5fcae84` |
| 2 | Tách `check:code` khỏi `check:docs`, CI + hook chạy khi có code | I-5 | ✅ đợt 2 |
| 2 | Mở cổng em dash ra mọi `apps/*/src` (dọn web/coral/admin/mentors trước) | U-10 I-22 | ✅ đợt 6 |
| 2 | Áp migration trước "Deploy foundry/api/tool-plane" trong `ci.yml` (apply + xác nhận "No migrations to apply"); sửa README, `migrations/README.md`, skill d1-migrate, memory | I-1 T-2 T-11 T-9 | ✅ đợt 2 |
| 2 | `schedules.md` đủ 5 cron + luật đối chiếu `triggers.crons`; `workflows.md` thêm 2 workflow | T-3 T-10 | ✅ đợt 2 |
| 3 | Trần USD/ngày cho cron AI + ghi usage; loại target đã rejected ở 9 chặng authoring; b21 translate vào ngân sách chung + ghi sổ mẩu kẹt | P-1 P-2 P-3 | ✅ đợt 3 |
| 3 | Soi trùng `event_id` trước khi ghi + `status='in_progress'` ở practice answer/lab | C-1 | ✅ đợt 3 |
| 3 | Marlins AvatarMenu + web `Login` sang tiếng Việt, icon lucide | U-3 | ✅ đợt 3 |
| 4 | Testkit dựng schema một lần mỗi worker + `hookTimeout` tường minh | T-5 | ✅ đợt 7: `VACUUM INTO` file khuôn một lần mỗi worker, mỗi test copy file (~10 ms); 34 file test D1 chạy 13 s thay vì ~300 s |
| 4 | `wrangler deploy` retry 3 lần (`.github/scripts/deploy.sh`); `scope.sh` thêm lockfile/`$design` cho docs/`content/dictation` cho admin/`gen-pearl` cho pearl; một group `production` cho ci-main, 12 deploy-\*, rollback, seed-data; `timeout-minutes` cho 13 workflow; seed-data có `permissions` | I-6 I-7 I-8 I-16 I-19 | ✅ đợt 4 |
| 4 | `/v1/health` trả 503 + `status: degraded` khi D1 lỗi; api bật `upload_source_maps`; bỏ thích lab không nuốt lỗi D1; `retention-model.md` nhãn tiếng Anh | I-23 I-12 C-11 T-7 | ✅ đợt 4 |
| 4 | SDD-002 §7/§8/§18 và SDD-017 §4 ghi đúng v1 đang chạy; `retention-model.md` §3.7 recency; PRD-001 REQ-EXAM-05..08 về ⏳ | T-1 T-12 T-4 | ✅ đợt 4 |
| 5 | Palette canonical đồng bộ, export TSX từ design-system, cổng md5 bản sao | U-2 | ⏳ |
| 5 | Guard status ở invitation accept; xoay phiên không đua + ân hạn; lịch thi học kỳ idempotent; foundry quét run treo | C-3 C-2 C-8 C-9 | ✅ đợt 6 |
| 5 | `shared/time.ts` (`vnDate`, `daysUntilVn`) cho 6 chỗ tính "ngày": Orca hạn đăng ký, `days_to_exam`, Goal `daysUntil`, Constraint `days_away`, Context `daysLeft`, khoá ngày của lab evidence; digest dùng chung | C-5 | ✅ đợt 7 (còn `coral/routes.ts` "ngày kể từ lần rà", không ảnh hưởng learner) |
| 5 | Chỉ mục `assessment_responses` | P-4 | ✅ gộp vào 0196 của phiên khác, chờ push |
| Tay | Diễn tập rollback trên `nemo12-compass`; chạy lại drill backup/restore + đối soát migration; bật Cloudflare notification + uptime; phát hành lại token tool-plane trói user | I-3 I-4 I-9 S-4 | 📋 chờ chủ dự án |

## 6b. Kết phiên 2026-09-04 — đã sửa gì, còn gì

Phiên chạy từ 2026-09-03 23:5x tới 2026-09-04 ~02:00 giờ VN trên nhánh `session/audit-2026-09-03`,
merge về `main` 11 lần (7a97af1 · c71de42 · fb6e79d · 6b06659 · 931b82c · 9edd232 · b4fe614 ·
e1655d5 · 93dc698 · 3428b2f · 94e43f1). Ba lần `main` đỏ trong phiên, đều do chính phiên này gây ra
và tự sửa trong vòng một lượt: `intake.md` không bump `last_reviewed` (→ `src-new.mjs` nay tự bump),
`emails.md` mồ côi (của phiên khác, nối vào mục lục), và bước quét run treo của foundry đứng trước cờ
`AUTO_DRAIN` làm một test bất biến đỏ (→ đứng sau cờ).

**Đã đóng bằng code hoặc docs trong phiên:** 🔴 S-1 S-2 S-3 P-1 P-2 P-3 C-1 U-1 U-3 I-1 I-2 I-3 T-1 T-2
T-3 T-4 T-5 (15/19 dòng của §2; còn U-2 và I-4 chờ) · 🟡 S-5 S-6 S-7 S-9 S-10 C-2 C-3 C-5 C-8 C-9 C-11
P-5 P-7 P-11 U-4 U-5 U-6 U-10 I-5 I-6 I-7 I-8 I-10 I-12 I-14 T-7 T-8 T-9 T-10 T-11 T-12 T-14 (một phần:
S-4 S-8 P-4 P-8 I-13) · ⚪ C-14 (TTS timeout) I-16 I-19 I-21 I-22 I-23 T-16 U-15 T-22.

**Còn mở, xếp theo giá trị:**

| Việc | Đóng | Vì sao chưa làm trong phiên |
| --- | --- | --- |
| Diễn tập `rollback.yml` trên `nemo12-compass`; chạy lại backup/restore drill + đối soát `d1_migrations`; bật Cloudflare Notifications + uptime cho `/v1/health` | I-3 I-4 I-9 | Việc tay chạm production/dashboard, cần chủ dự án |
| Phát hành lại 3 token tool-plane trói user, bỏ nhánh token cũ; rate limit `/mcp` | S-4 | Token nằm ở vault ngoài repo |
| Cron thi hành `data_deletion_requests` + export dữ liệu + retention theo `keep_days` | S-8 | Cần quyết phạm vi xoá (44 bảng có `learner_id`) với chủ dự án |
| `packages/design-system` export TSX, gỡ 12 bộ bản sao component, cổng md5 | U-2 | Đụng 8 app cùng lúc; nên làm ở phiên riêng có worktree |
| `React.lazy` theo route + `manualChunks` cho learn/marlins/web | U-7/P-10 | Cần đo bundle trước/sau, không làm vội |
| Cockpit: cache node môn, gộp FOUNDATION_SQL, index `items(subject_id, role)` | P-6 | Cần migration (chờ 0196 lên) và đo độ trễ |
| `rows_read` proxy cho `env.DB` + `wrangler d1 insights` hằng ngày | P-9 | Thiết kế log cần bàn ngưỡng |
| Chỉ mục `learner_evidence(occurred_at)` cho digest; `assessment_responses` đã vào 0196 | P-14 P-4 | Chờ 0196 của phiên khác lên `main` rồi đánh 0197 |
| `z.any()` → schema thật cho route learner/parent đọc; lint cấm thêm `z.any()` | C-7 | Việc dài, từng module một |
| Test cho `exams` submit, `auth` logout, `portraits` familyRole, `privacy` deletion | T-6 | Mỗi cái 3-5 case, có testkit nhanh rồi |
| Mức `AI_DAILY_USD_CAP` (đang 2 USD/ngày) và có giữ BCC digest về Gmail cá nhân không | P-1 S-5 | Quyết định của chủ dự án |
| SDD-002: hiện thực `source_diversity_factor` cho confidence | T-1 (phần code) | Đổi thuật toán learner-facing, cần SRC riêng |

## 7. Trace

* Nguồn: SRC-674 (chỉ đạo 2026-09-03).
* Đáp ứng: REQ-DOC-05. Chỉ báo AS liên quan ghi từng dòng ở §2, §3.
* Nhánh làm việc: `session/audit-2026-09-03`, merge về `main` theo nhịp 30 phút; sổ merge ở cuối phiên được ghi vào [index](index.md).
