Audit #005 — 2026-08-26
Rà soát toàn hệ sau một ngày xây liên tục: Family Workspace (SRC-584), danh sách theo gia đình (SRC-585), giao diện dolphin sang tiếng Anh (SRC-586), Dory note+tag (SRC-587/588) và Family Journey (SRC-589).
Đây không phải một lần chấm đầy đủ theo Audit Standard 255 chỉ báo. Đây là một lần rà có mục tiêu vào phần vừa xây, cộng một lượt kiểm sức khoẻ toàn hệ. Nói rõ để không ai đọc nhầm con số.
1. Trạng thái cổng — tất cả xanh
| Cổng | Kết quả |
|---|---|
| Test | 625 / 625 pass, 50 file |
| Typecheck | 0 lỗi trên toàn workspace |
| Lint | 0 lỗi |
| QG-001 docs integrity | PASS, 114 docs |
| Contract snapshot | khớp, 240 route /v1 |
| Migration dry-run | 0001..0081 áp sạch |
2. Bề mặt sống — kiểm bằng request thật
Chín bề mặt, đúng trạng thái mong đợi:
api 200 · www 200 · learn 200 · marlins 200 · docs 200 · pearl 200
dolphin 302 · admin 302 · coral 302 ← Cloudflare Access chặn ở biên, đúng thiết kếNăm route nội bộ đều trả 401 khi gọi không phiên: /v1/mentor/families, /v1/mentor/learners, /v1/showcase/mentors, /v1/admin/overview, /v1/coral/overview.
Không có trường nội bộ nào rò ra API công khai. Kiểm /v1/public/mentors trên production: 5 hồ sơ trả về, không mẩu nào chứa alumni_note, verification_note, consent_note, internal_note, sources_json hay alumni_verified_by.
3. Phát hiện
🔴 F1 — GET /v1/mentor/families sẽ hỏng khi vượt ~100 gia đình
Truy vấn danh sách không có LIMIT, và bốn truy vấn phụ sau đó dựng IN (?1,?2,…) với một placeholder cho mỗi gia đình. D1 có trần số tham số buộc trên một truy vấn; vượt trần thì endpoint trả 500, không phải trả chậm.
Đây không phải suy đoán về giới hạn: chính repo này đã gặp và đã xử lý — modules/content/backfill.ts chia lô đúng 100 cho cùng một hình dạng truy vấn. Phần tôi vừa viết không theo tiền lệ đó.
Hiện có 4 gia đình nên chưa lộ. Nó sẽ lộ đột ngột ở một ngưỡng, không phải chậm dần — đó là loại lỗi tệ nhất để phát hiện trong sản xuất.
Trạng thái: ĐÃ SỬA — xem §4.
🟠 F2 — listPublicAlbums cùng một lỗi, trên trang CÔNG KHAI
modules/showcase/service.ts dựng IN (${marks}) trên toàn bộ id album đã publish, không chia lô. Nghiêm trọng hơn F1 ở một điểm: nó nằm trên nemo12.com, tức là hỏng thì người lạ nhìn thấy, không phải nhân viên nội bộ.
Trạng thái: ĐÃ SỬA — xem §4.
🟠 F3 — Journey hứa "từng người" nhưng với phụ huynh thì chưa làm được
SDD-026 §1 nói đơn vị của journey là một người, và lấy chính nhà Trí Minh làm ví dụ: bố đã hào hứng còn con thì chưa.
Nhưng trong hiện thực, dấu vết dùng để suy ra bước của mọi liên hệ trong cùng một nhà là như nhau: note gắn được với một learner cụ thể (learner_id), nhưng không gắn được với một liên hệ cụ thể. Hệ quả: bố và mẹ luôn được suy ra cùng một bước, cho tới khi có người bấm xác nhận khác đi.
Đây là tài liệu hứa nhiều hơn code làm được — đúng loại trôi mà SRC-416 sinh ra để chặn, chỉ khác là cổng đó không bắt được vì nó kiểm tên bảng chứ không kiểm ngữ nghĩa.
Hai đường sửa, cả hai đều đáng cân nhắc chứ không hiển nhiên:
- Thêm
contact_idvàofamily_notes— thêm một cột, đi ngược nguyên tắc "ít fields". - Chấp nhận giới hạn và nói thẳng trên giao diện: bước của phụ huynh là suy ra ở mức gia đình cho tới khi có người xác nhận.
Trạng thái: ĐÃ SỬA TÀI LIỆU theo hướng 2 — code giữ nguyên, tài liệu thôi hứa quá. Cột contact_id để lại cho đợt sau nếu thấy cần thật.
🟡 F4 — Đề xuất tag treo mãi ở pending
family_note_tag_suggestions chỉ chuyển sang accepted/rejected khi người dùng lưu tag cho note đó. Bấm "Suggest tags" rồi bỏ đi thì các dòng nằm pending vĩnh viễn, và gọi lại lần hai sinh thêm một bộ nữa.
Chỗ hỏng thật không phải rác: gọi hai lần rồi lưu thì cùng một tag bị chốt hai lần, và tỉ lệ chấp nhận — đúng con số bảng này sinh ra để đo — bị thổi lên.
Và một lỗ hổng thứ hai không có trong ghi nhận đầu: bảng đo mà không có đường đọc. Luật ở SDD-024 §6 nói "một tag bị từ chối quá nửa số lần thì định nghĩa tag hoặc prompt sai" — không có endpoint nào đọc được thì luật ấy không bao giờ áp được, và bảng thành bảng chỉ-ghi.
Trạng thái: ĐÃ SỬA — xem §4.
🟡 F5 — Journey đọc toàn bộ note của một nhà, không giới hạn
memberSignals() lấy mọi note đang active rồi đếm trong JS. Với 37 note thì không sao; với một nhà theo bốn năm thì đây là một truy vấn lớn chạy mỗi lần mở màn hình.
Trạng thái: ĐÃ SỬA — xem §4.
✅ Đã kiểm và KHÔNG có vấn đề
- Nhật ký không chứa nội dung. Mọi lời gọi
logAccesstrong module families chỉ truyền id và loại hành động (note_id,tag_count,stage,member_id); không khoá nào chạm danh sách cấm củasanitiseDetail(AS-07.4.3). - Cổng phủ xác thực làm việc.
authCoverage.test.tsbắt được hai lần trong ngày:/v1/mentor/notes/*và/v1/mentor/journey/*chưa quarequireSession. Cả hai đã vá trước khi lên production. - Prompt AI đi đúng một đường.
dory.suggest-tagsđăng ký trongshared/prompts.ts; không có lời gọiAI.runnào nằm ngoài file đó. - Phạm vi gửi cho AI hẹp đúng như thiết kế. Chỉ thân note đang xét, cắt 8000 ký tự; không gửi hồ sơ gia đình, không gửi note nhà khác, không gửi dữ liệu học.
- Tag lạ bị chặn ở server.
normaliseTagslọc cả đầu vào người dùng lẫn đầu ra của mô hình.
4. Đã sửa trong chính lần audit này
| Mã | Sửa gì |
|---|---|
| F1 | GET /v1/mentor/families thêm LIMIT 200; bốn truy vấn phụ chia lô 100, theo đúng tiền lệ backfill.ts |
| F2 | listPublicAlbums chia lô 100 cho truy vấn người trong album |
| F3 | SDD-026 §3 nói rõ giới hạn: bước của phụ huynh suy ra ở mức gia đình, không phân biệt được bố với mẹ cho tới khi có người xác nhận |
| F4 | Sinh bộ đề xuất mới thì xoá bộ treo của lần trước — đề xuất chưa ai quyết là đề xuất bị thay thế, không phải bị từ chối; gán rejected cho chúng sẽ nói sai rằng người dùng đã nhìn và đã chối. Thêm GET /v1/mentor/tag-stats để luật "quá nửa bị từ chối" có đường áp; tag chưa ai quyết trả null chứ không trả 0 |
| F5 | memberSignals() đếm bằng COUNT/MAX/SUM(CASE…) trong SQL, mỗi nhóm một dòng, thay vì kéo hết note về |
5. Món nợ mở
Cả năm phát hiện đã sửa. Còn lại là việc chưa xây, không phải lỗi:
| # | Việc | Vì sao chưa làm |
|---|---|---|
| — | Anchor chưa xây | SDD-025 mới có tài liệu |
| — | 12 trên 13 AI agent chưa xây | Mới có Tag suggester |
| — | Chưa xem được dolphin bằng mắt | Sau Cloudflare Access, cần đăng nhập Google; mọi kiểm chứng ở trên là qua API và mã nguồn |
Ghi chú kỹ thuật cho F5
Việc đếm tag trong SQL dùng tags_json LIKE '%"event"%' và tags_json <> '["event"]'. Trông thô, nhưng đúng vì hai lý do cụ thể chứ không phải vì may:
- Bộ tag là danh sách đóng và không tag nào là chuỗi con của tag khác khi đã bọc dấu nháy.
normaliseTagsluôn xuất theo thứ tự cố định củaTAGS, nên note chỉ gắn mỗieventluôn được ghi đúng chuỗi["event"]— đây là so khớp chính xác, không phải xấp xỉ.
Đã kiểm bằng SQLite thật chứ không suy luận: năm note với năm hình dạng tag khác nhau (["event"], [], ["parent","event"], ["goal"], ["child","fact"]) cho ra đúng số mong đợi ở cả mức gia đình lẫn mức learner.
6. Điều đáng nói nhất
Ba cổng chất lượng bắt lỗi thật của tôi trong ngày, không phải lỗi giả:
authCoverage.test.ts— hai route mới thiếurequireSession. Nếu lọt, chúng sẽ mở dữ liệu gia đình cho bất kỳ ai gọi thẳng API.check-docsSRC-416 — tôi nêu tên một bảng chưa tồn tại trong PRD như thể nó đã có.- Test của chính tôi —
\btrong JavaScript không hiểu chữ tiếng Việt có dấu, nên/\bbố\b/không khớp "Bố lo…".
Và một lỗi cổng không bắt được, người dùng bắt được: tab hiện "Notes (0)" trong khi bên trong có mười note. Không có cổng nào kiểm được rằng một con số trên màn hình đọc đúng nguồn dữ liệu — chỗ đó vẫn cần mắt người.
Trace
Nguồn SRC-590 · chuẩn audit-standard · audits/index.