Skip to content

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 — lần chấm toàn hệ gần nhất là Audit #013 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ướcCách làmKết quả
Chốt phạm vimain @ 8b6c66ba (2026-09-08), cây làm việc dùng chung12 app · 3 worker · 207 migration · 143 docs kiểm · 461 docs có last_reviewed
Máy chạy trướccheck:docs · check:code · typecheck · lint · testdocs ✓ · code ✓ · typecheck ✓ · lint ⚠ 1 cảnh báo (F-4) · test ✓ 1.539 pass
Cổng ít khi chạyaudit:bounds · contract:diff · verify:bindings · migrate:dryrun · check:ui-latestbindings ✓ · 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 ↔ codeSinh lại mọi tài liệu dẫn xuất (gen:knowledge · gen:reference · gen:playbooks) rồi xem git status1 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 docsScript rời: dòng meta **Unit** của 61 package so với số unit thật trên trang module0 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ứcPhát hiệnChỉ báo ASTrạ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 đượcAS-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 §0AS-06✅ đã sửa
F-4⚪Chỉ thị eslint-disable no-console thừa trong apps/marlins/src/parentLessons.test.tsAS-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á IELTSAS-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ự độngAS-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-7AS-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 đã ghi:

CổngCách thửKết quả
Chốt template trong gen-referenceTắt nhánh nở vòng lặp✗ đỏ, exit 1, chỉ đúng GET /v1/whale/${kind}/{id}
Đối chiếu số unitSử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ốngKỳ vọngKết quả
Nguồn không đổi, sinh lại hai lượtFile 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ệcLý 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 appDS-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ệcVì 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
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ổngTrướcSau
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
test1.539 pass1.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:reference313 endpoint329 endpoint, 0 template sót
npm audit4 lỗ hổng3 (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.