---
url: https://docs.nemo12.com/quality/audits/audit-2026-09-08.md
description: >-
  Audit #014 (08.09.2026, SRC-688): đối chiếu tài liệu với mã nguồn, catalog API
  thiếu 16 route và hai cổng có điểm mù.
---

# Audit #014 — Đối chiếu tài liệu với mã nguồn (2026-09-08, commit `8b6c66ba`)

Chỉ đạo SRC-688: **rà toàn bộ tài liệu và mã nguồn xem có khớp nhau không, đồng bộ, rồi tìm tính
năng còn thiếu hoặc đang lỗi, đưa vào Audit Report, sau đó fix và nâng cấp.**

Bản này không chấm lại 255 chỉ báo của [Audit Standard v0.2](../audit-standard/index.md) — lần chấm toàn
hệ gần nhất là [Audit #013](2026-09-03.md) cách đây năm ngày, và chấm lại không đổi được gì khi
phát hiện chưa sửa. Mỗi dòng dưới đây nối về chỉ báo AS tương ứng.

**Kết quả một dòng:** hệ thống lành hơn nhiều so với Audit #013 — 8/9 cổng xanh ngay từ lần chạy
đầu, 1.539 test pass, 0 lỗi typecheck. Nhưng chỗ hỏng tìm được lại nằm đúng ở **chính công cụ đo**:
bộ sinh `docs/reference/api.md` nuốt mất 16 route có thật, và hai cổng tài liệu có điểm mù khiến
loại trôi này không thể bị bắt. **Tám phát hiện, đã sửa và kiểm chứng cả tám trong phiên.**

Phát hiện thứ bảy đến từ chính lần push của audit này: CI đỏ, và nguyên nhân là công cụ cấp số SRC
ghi ngày theo UTC trong khi git ghi theo giờ máy — một điểm mù mà **không cổng nào bắt được trước
lúc commit**, vì phép đo cần chính commit đó mới tồn tại (xem F-7).

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

| Bước | Cách làm | Kết quả |
| --- | --- | --- |
| Chốt phạm vi | `main` @ `8b6c66ba` (2026-09-08), cây làm việc dùng chung | 12 app · 3 worker · 207 migration · 143 docs kiểm · 461 docs có `last_reviewed` |
| Máy chạy trước | `check:docs` · `check:code` · `typecheck` · `lint` · `test` | docs ✓ · code ✓ · typecheck ✓ · lint ⚠ 1 cảnh báo (**F-4**) · test ✓ 1.539 pass |
| Cổng ít khi chạy | `audit:bounds` · `contract:diff` · `verify:bindings` · `migrate:dryrun` · `check:ui-latest` | bindings ✓ · migration ✓ 207/207 · contract ⚠ snapshot lệch 16 route (**F-2**) · ui-latest ✗ 8 chỗ tụt bản (**F-3**) · bounds ✗ (đã biết, REQ-KNW-17 — xem §4) |
| Đối chiếu docs ↔ code | Sinh lại mọi tài liệu dẫn xuất (`gen:knowledge` · `gen:reference` · `gen:playbooks`) rồi xem `git status` | 1 file trôi thật (**F-5**); phát hiện lệch 313 vs 329 endpoint dẫn tới **F-1** |
| Đối chiếu số liệu docs | Script rời: dòng meta `**Unit**` của 61 package so với số unit thật trên trang module | 0 lệch — nhưng **không cổng nào đang giữ** (**F-6**) |

Điểm mấu chốt của phương pháp: **sinh lại rồi so `git status`** là cách rẻ nhất để bắt tài liệu dẫn
xuất trôi khỏi nguồn. Nó tìm ra F-5 trong một lệnh. Còn F-1 thì `git status` KHÔNG bắt được, vì file
sinh ra vẫn khớp với chính nó — chỉ khi đặt hai bộ đếm độc lập cạnh nhau (`gen:reference` báo 313
endpoint, `contract:diff` báo 329 route) mới lộ ra khoảng cách 16.

## 2. Phát hiện

| # | Mức | Phát hiện | Chỉ báo AS | Trạng thái |
| --- | --- | --- | --- | --- |
| F-1 | 🔴 | **`docs/reference/api.md` thiếu 16 route có thật.** Bộ sinh không nở được template literal khi biến đường dẫn là *tham số của hàm tạo route*; 19 endpoint coral bị thu thành 3 dòng vô nghĩa in ra đúng chữ `${kind}` / `${path}` | AS-01.4.2 | ✅ đã sửa |
| F-2 | 🟡 | **Snapshot hợp đồng OpenAPI lệch 16 route** (email admin + coral authoring). Không breaking, nhưng snapshot lệch thì lần sau có breaking change thật cũng không so được | AS-01.4.2 | ✅ đã sửa |
| F-3 | 🟡 | **`motion` tụt bản ở cả 8 app React** (13.1.1 → 13.2.0), vi phạm DS-001 §0 | AS-06 | ✅ đã sửa |
| F-4 | ⚪ | Chỉ thị `eslint-disable no-console` thừa trong `apps/marlins/src/parentLessons.test.ts` | AS-06 | ✅ đã sửa |
| F-5 | 🟡 | **`docs/knowledge/english.md` trôi khỏi nguồn**: file trên `main` khai 1.320 câu trên 64 node, cây kiến thức thật đã có 2.868 câu trên 322 node kể từ lúc nạp 12 khoá IELTS | AS-01.4.1 | ✅ đã sửa |
| F-6 | 🟡 | **Hai cổng tài liệu có điểm mù** khiến F-1 và loại trôi của F-5 không thể bị bắt tự động | AS-01.1 | ✅ đã bịt |
| F-7 | 🟡 | **`src-new.mjs` cấp SRC với `last_reviewed` lùi một ngày** trong khung 00:00–07:00 giờ Việt Nam: dùng ngày UTC, còn git ghi ngày commit theo giờ local. CI đỏ chắc chắn ở lần push kế | AS-01.1 | ✅ đã sửa |
| F-8 | ⚪ | **Ba file `docs/reference/*` bị đóng dấu thời điểm chạy bộ sinh**, nên bước CI "sinh lại và bắt buộc không lệch" kêu ở mọi push sang ngày mới dù không route nào đổi. Cùng gốc UTC với F-7 | AS-01.4.1 | ✅ đã sửa |

### F-1 — Catalog API thiếu 16 route (nặng nhất)

`scripts/gen-reference.mjs` đã có hàm `expandTemplatePath` để nở đường dẫn viết bằng template
literal, nhưng nó **chỉ hiểu một dạng**: biến chạy trong vòng lặp `for (const [kind, …] of […] as const)`.
Module coral dùng dạng thứ hai — hàm tạo route nhận đường dẫn qua tham số:

```ts
const foundryGet = (path: string, …) =>
  coralRouter.openapi(createRoute({ method: "get", path: `/v1/coral/foundry${path}` … }));

foundryGet("/courses", …);   // 10 chỗ gọi
foundryPost("/runs", …);     //  7 chỗ gọi
listRouteFor("pains", …);    //  2 chỗ gọi
```

Giá trị không nằm quanh chỗ khai báo route mà nằm ở **các chỗ gọi**. Không suy được thì bộ sinh giữ
nguyên template, nên catalog công khai in ra ba dòng:

```
| PUT  | `/v1/coral/courses/{courseId}/${kind}` | 🔐 role `staff` |
| GET  | `/v1/coral/foundry${path}`             | 🔐 role `staff` |
| POST | `/v1/coral/foundry${path}`             | 🔐 role `staff` |
```

**Vì sao đây là 🔴 chứ không phải lỗi hình thức.** `api.md` là tài liệu người và máy đọc để biết
platform có endpoint gì. Ba dòng này không chỉ xấu — chúng **đứng thay cho 19 route có thật**, nên
người đọc catalog kết luận sai rằng 16 endpoint kia không tồn tại. Đúng loại sai mà AS-01.4.2 sinh
ra để chặn: *route có thật nhưng vắng mặt trong api.md*.

**Sửa.** Mở rộng `expandTemplatePath`: khi không tìm được vòng lặp bao ngoài, truy ngược lên hàm tạo
gần nhất có biến đó trong danh sách tham số, rồi thu các đối số chuỗi ở đúng vị trí tham số tại mọi
chỗ gọi. Quét cân bằng ngoặc chứ không cắt theo dấu phẩy, để `z.object({ … })` trong danh sách đối
số không cắt nhầm.

Bằng chứng: `gen:reference` từ **313 → 329 endpoint**, khớp chính xác con số 329 route mà
`contract:diff` đọc độc lập từ OpenAPI. Không còn `${` nào trong `api.md`. 19 dòng mới đều là route
thật, không dòng nào bịa.

### F-5 — `docs/knowledge/english.md` trôi

File trên `main` là ảnh chụp của cây kiến thức **trước** khi nạp 12 khoá IELTS. Chỉ mục kho câu hỏi
ghi 1.320 câu trên 64 node; thực tế đã là 2.868 câu trên 322 node — 258 node vắng mặt hoàn toàn
khỏi tài liệu. Nguyên nhân là phiên nạp nội dung không chạy lại `gen:knowledge`, và không cổng nào
so file dẫn xuất với nguồn (xem F-6).

### F-6 — Hai điểm mù của cổng

**Điểm mù thứ nhất: template chưa nở đi lọt.** Bộ sinh gặp đường dẫn không suy được thì giữ nguyên
rồi ghi file và báo thành công. Đây là lựa chọn có lý ("đừng nuốt route") nhưng thiếu vế sau: không
ai được báo. Đã thêm chốt chặn ở cuối `gen-reference.mjs` — còn route nào chứa `${` thì **gãy và
không ghi file**, vì một catalog thiếu route âm thầm nguy hiểm hơn là không sinh được catalog.

**Điểm mù thứ hai: số unit khai trong meta chưa từng được đối chiếu.** `check-curriculum-pearl.mjs`
đối chiếu số **module** khai trong meta với số module liệt kê thật, nhưng dòng `| **Unit** | N |`
thì không cổng nào đọc. Nặng hơn: cổng đếm unit qua bảng index đánh số kiểu `x.y.z`, mà **ba môn lớn
nhất — toán, văn, tiếng Anh — không đánh số kiểu đó**, nên cột UNIT của chúng bằng 0 và mọi đối
chiếu index ↔ nội dung của **346 unit** đều truợt qua không cột nào giữ.

Đã đo tay toàn bộ 61 package: **0 lệch** — số liệu tài liệu hiện đúng. Nhưng nó đúng do may chứ
không do có cổng giữ, nên đã thêm phép đối chiếu `meta **Unit**` ↔ số unit thật trên trang module,
chạy cho **mọi** môn kể cả ba môn không đánh số.

**Cả hai cổng đều đã thử nghiệm âm tính** (cố tình làm hỏng để xem cổng có đỏ không), chứ không chỉ
chạy thấy xanh — đúng bẫy "typecheck xanh mà sai" mà [SRC-678](../../intake.md) đã ghi:

| Cổng | Cách thử | Kết quả |
| --- | --- | --- |
| Chốt template trong `gen-reference` | Tắt nhánh nở vòng lặp | ✗ đỏ, `exit 1`, chỉ đúng `GET /v1/whale/${kind}/{id}` |
| Đối chiếu số unit | Sửa `**Unit**` của `toan/represent` từ 28 → 27 | ✗ đỏ, `[S4] meta ghi 27 unit nhung trang module co 28` |

Cả hai file đều đã khôi phục nguyên trạng sau khi thử.

### F-7 — `src-new.mjs` cấp SRC với ngày lùi một hôm (tìm ra khi CI đỏ)

Phát hiện này không đến từ vòng rà mà từ **chính lần push của audit này**: CI đỏ ở bước
`Docs integrity (QG-001)` với đúng một dòng —
`docs/intake.md — khai last_reviewed: 2026-09-07, nhưng sửa thật ngày 2026-09-08`.

`src-new.mjs` bump `last_reviewed` bằng `new Date().toISOString().slice(0, 10)`, tức **ngày UTC**.
Git thì ghi ngày commit theo **múi giờ máy**. Ở +07, từ 00:00 tới 07:00 sáng hai con số đó lệch nhau
một ngày, nên mọi SRC cấp trong khung bảy tiếng đó đều làm `check-docs-fresh` đỏ ở lần push kế.
Trớ trêu là đoạn bump ấy sinh ra chính để chặn loại đỏ này, và comment ngay trên nó đã ghi một lần
vấp trước (SRC-674).

**Vì sao không cổng nào bắt được trước khi push.** Phép đo của `check-docs-fresh` là *`last_reviewed`
so với ngày commit gần nhất chạm file*. Trước lúc commit, commit đó chưa tồn tại — nên cả
`npm run check:docs` chạy tay lẫn hook `pre-commit` đều thấy xanh một cách trung thực. Đây là một
điểm mù **không vá được bằng cách chạy cổng sớm hơn**; chỉ vá được ở nguồn sinh ra ngày.

Sửa: dùng ngày local (`toLocaleDateString("sv-SE")`, đúng dạng `YYYY-MM-DD`) cho cùng một quy ước
múi giờ với git. Kiểm chứng tại thời điểm sửa (05:28 giờ Việt Nam — đang trong khung hỏng):
`local 2026-09-08 · UTC 2026-09-07 · git 2026-09-08` — local khớp git, UTC lệch.

### F-8 — File sinh ra không ổn định qua các lần sinh lại

Tìm ra ngay sau khi nâng GitHub Actions: cảnh báo Node 20 biến mất đúng như mong đợi, nhưng lượt CI
đó lộ ra một annotation khác vốn bị lấp dưới nó —
`docs/reference trong repo lệch với source — chạy 'npm run gen:reference' rồi commit`.

`gen-reference.mjs` đóng `last_reviewed` bằng ngày chạy bộ sinh (và bằng ngày **UTC**, y hệt F-7).
Hệ quả: một file sinh ra từ nguồn **không đổi** vẫn khác chính nó khi sinh lại sang ngày mới, nên
bước "sinh lại và bắt buộc không lệch" kêu ở gần như mọi push. Các lượt chạy trước không thấy cảnh
báo này chỉ vì chúng tình cờ cùng ngày UTC với commit gần nhất chạm `docs/reference`.

Đây là hỏng ở mức thiết kế của cổng chứ không phải một lần lệch: **một cảnh báo kêu hằng ngày là
cảnh báo không ai còn đọc**, tức cổng mất tác dụng đúng vào lúc nó có chuyện thật để nói — và
chuyện thật ấy chính là loại trôi F-1/F-5 mà audit này vừa tìm ra.

Sửa: `writeStable()` — nếu phần còn lại của file trùng bản cũ từng chữ thì giữ nguyên ngày cũ, chỉ
bump khi nội dung thật sự đổi. Ngày cũng chuyển sang giờ local cho cùng quy ước với git.

Kiểm chứng hai chiều:

| Tình huống | Kỳ vọng | Kết quả |
| --- | --- | --- |
| Nguồn không đổi, sinh lại hai lượt | File y hệt, `git status` trống | ✓ trống cả hai lượt, ngày giữ `2026-09-07` |
| Nội dung đổi thật (xoá một route khỏi bản đã commit) | Ngày bump, route quay lại | ✓ `2026-09-07` → `2026-09-08`, route trở lại đủ |

## 3. Nâng cấp kèm theo

| Việc | Lý do |
| --- | --- |
| `qs` 6.15.3 → bản vá (`npm audit fix`) | Hai advisory mức trung bình (bỏ qua array-limit; DoS qua `isBuffer`). Vào qua CLI `shadcn` → MCP SDK → express, tức **chỉ công cụ dev**, không lên Workers hay trình duyệt. Lỗ hổng: 4 → 3 |
| Nâng `motion` lên 13.2.0 ở 8 app | DS-001 §0; ghim số cụ thể chứ không để dải mở. Chạy `typecheck` ngay sau khi nâng theo luật SRC-591 — không vỡ kiểu |
| Nâng GitHub Actions: `checkout` v4→**v7** · `setup-node` v4→**v7** · `cache` v4→**v6** (37 chỗ, 18 workflow) | Bản @v4 nhắm Node 20 nên runner **ép ngầm sang Node 24** kèm cảnh báo deprecation ở mọi lượt chạy — loại ép ngầm có ngày thành lỗi cứng. Ba breaking change trên đường v4→v7 đều đã kiểm là không chạm repo này: cache tự động của `setup-node` v5/v6 cần trường `packageManager` (repo không khai, và `cache: npm` ở đây là khai tay); `checkout` v7 chỉ chặn fork PR ở `pull_request_target`/`workflow_run` (không workflow nào dùng); yêu cầu runner ≥ v2.327.1 thoả vì 18/18 chạy `ubuntu-latest` |

## 4. Không sửa, có chủ đích

| Việc | Vì sao để lại |
| --- | --- |
| `audit:bounds` đỏ — package `math` có module 12 unit, `ap`/`genai` có package chỉ 1 module | Đây là **nợ nội dung đã đo được**, không phải lỗi kỹ thuật. Sửa nghĩa là chia lại cây kiến thức, mà mọi bằng chứng học tập đã ghi đều trỏ vào node của cây hiện tại. Quyết định của chủ dự án theo từng môn |
| `esbuild ≤0.24.2` → `vite` → `vitepress` (1 cao, 2 trung bình) | **Chưa có bản vá**. Chỉ ảnh hưởng dev server của bốn site VitePress, không có mặt trong bản build production. Nâng `vitepress` là một việc riêng có commit và có test, không gộp vào audit |
| Bốn file đang sửa dở của phiên khác (`curriculum/*`, `sitemap.xml`) | Cây làm việc dùng chung — đổi số SRC-681 → 682 và sitemap tự sinh là việc của phiên khác. Không đụng, theo luật CLAUDE.md §"Chạy nhiều phiên song song" |
| `verify-bindings` không liệt kê được bucket R2 (cảnh báo ở mọi lượt CI) | Token Cloudflare thiếu quyền `r2:read`, tức **lớp C — đối chiếu binding với bucket có thật — đang không chạy cho R2**. Đây là việc TAY trên dashboard, không sửa được bằng code từ phiên này. Đã ghi thành việc chờ ở [backlog](../../backlog.md#việc-nhỏ-còn-treo) |
| `TODO` gom bảng nhãn môn ở `StartWizard.tsx` | Đã có task riêng, nợ được khai báo rõ ngay tại chỗ. Gom bốn bản chép rời là việc đụng ba app, không gộp vào audit |

## 5. Bằng chứng sau khi sửa

| Cổng | Trước | Sau |
| --- | --- | --- |
| `check:docs` (9 cổng con) | ✓ | ✓ (+1 phép đối chiếu số unit) |
| `check:code` (10 cổng con) | ✓ | ✓ |
| `typecheck` | ✓ | ✓ (sau khi nâng `motion`) |
| `lint` | ⚠ 1 cảnh báo | ✓ 0 |
| `test` | 1.539 pass | 1.539 pass |
| `contract:diff` | ⚠ snapshot lệch 16 | ✓ 329 route, snapshot khớp |
| `check:ui-latest` | ✗ 8 chỗ tụt bản | ✓ 12 app đều mới nhất |
| `gen:reference` | 313 endpoint | **329 endpoint**, 0 template sót |
| `npm audit` | 4 lỗ hổng | 3 (còn lại chưa có bản vá) |

## 6. Đề xuất cho lần sau

1. **Đưa `check:ui-latest` vào một lịch chạy đều** (tuần/lần). Nó cố ý không chắn CI vì phụ thuộc
   mạng — đúng, nhưng hệ quả là không ai chạy, và lần này nó đã tụt bản ở cả 8 app trước khi có
   người nhìn tới.
2. **Đặt hai bộ đếm độc lập cạnh nhau thành cổng.** F-1 lộ ra vì `gen:reference` và `contract:diff`
   đếm route theo hai đường khác nhau. So hai con số đó là một cổng rẻ và mạnh: lệch nghĩa là một
   trong hai đường đang mù.
3. **Sinh lại tài liệu dẫn xuất trong CI rồi bắt `git diff` khác rỗng.** F-5 nằm trên `main` nhiều
   ngày dù mọi cổng đều xanh, vì không ai chạy lại bộ sinh.
