---
url: https://docs.nemo12.com/quality/audit-standard/as-01-03.md
description: >-
  Chỉ báo AS-01 tài liệu và đóng vết, AS-02 yêu cầu và sản phẩm, AS-03 kiến trúc
  và hợp đồng API, kèm ngoại lệ tường minh.
---

# Audit Standard · AS-01 tới AS-03

Một phần của [Audit Standard](./index.md). Chỉ báo AS-01 tài liệu và đóng vết, AS-02 yêu cầu và sản phẩm, AS-03 kiến trúc và hợp đồng API, kèm ngoại lệ tường minh.

## AS-01 — Tài liệu & Đóng vết

| ID | Chỉ báo | Cách kiểm |
| --- | --- | --- |
| **AS-01.1** | **Nguồn duy nhất & sổ tiếp nhận** | |
| AS-01.1.1 | Mọi đặc tả hệ thống nằm trong `docs/`; không tồn tại bản copy song song "cho AI" / "cho human" ở nơi khác | `find . -name "*.md" -not -path "*/node_modules/*"` — mọi kết quả ngoài `docs/` phải là README gốc hoặc nội dung app (`apps/*/content`), không phải đặc tả |
| AS-01.1.2 | Mỗi chỉ đạo/tài liệu đầu vào có đúng một dòng `SRC-xxx` trong `intake.md`, đủ ngày tuyệt đối + tóm tắt + kết quả canonical hóa | đọc các mảnh `docs/intake/src-*.md` |
| AS-01.1.3 | Không có SRC nào đã tiếp nhận mà nội dung chưa xuất hiện trong ít nhất một PRD/SDD/QG | `grep -rn "SRC-<n>"` từng SRC id trong `docs/` — phải có ≥1 hit ngoài `intake.md` |
| AS-01.1.4 | Số SRC không trùng (lỗ số được phép từ SRC-951; con trỏ "Nguồn tiếp theo" bỏ từ SRC-1339) | script: đếm dòng `\| SRC-\d+ \|` trong intake.md, so `uniq` với tổng dòng |
| AS-01.1.5 | Không file `.md` nào ngoài `docs/` (trừ `README.md` gốc và nội dung app) đóng vai trò đặc tả hệ thống | đọc kết quả AS-01.1.1, xác nhận từng file ngoại lệ là nội dung không phải đặc tả |
| **AS-01.2** | **Frontmatter & metadata** | |
| AS-01.2.1 | 100% file trong `docs/` mở đầu bằng YAML frontmatter | `node scripts/check-docs.mjs` |
| AS-01.2.2 | Mọi frontmatter đủ trường bắt buộc: `id, type, title, owner, status, version, last_reviewed, ai_readable` | script |
| AS-01.2.3 | `id` duy nhất toàn docs, lowercase, khớp vai trò file | script |
| AS-01.2.4 | `type` thuộc tập cho phép (prd/sdd/quality/convention/index/adr/reference/workflow) | script |
| AS-01.2.5 | `status` ∈ {draft, active, deprecated}; file `deprecated` ghi rõ file thay thế | script + đọc file `deprecated` (nếu có) |
| **AS-01.3** | **Đóng vết hai chiều** | |
| AS-01.3.1 | Mỗi REQ active nằm trong `satisfies` của ≥1 doc thiết kế (SDD hoặc reference) | `scripts/check-docs.mjs` mục 2 |
| AS-01.3.2 | Mỗi REQ active có ≥1 QG ở cột QG của PRD-001 §5 | đọc PRD-001, mọi dòng `| ✔ |` có `QG-\d+` |
| AS-01.3.3 | Mỗi SDD khai `sources` trỏ tới SRC **tồn tại** trong intake | script |
| AS-01.3.4 | Không có doc mồ côi: mọi file được liên kết từ `index.md` hoặc từ một doc khác | script (mục 4 `check-docs.mjs` bắt link gãy, không bắt file không được trỏ tới — bổ sung: `grep -rL` đối chiếu danh sách file với danh sách link đích) |
| AS-01.3.5 | Mọi US map tới ≥1 REQ, và mọi REQ priority MUST map tới ≥1 US hoặc WF | đọc PRD-001 §6 |
| **AS-01.4** | **Trang sinh tự động khớp source** | |
| AS-01.4.1 | 🔴🚸 `npm run gen:reference` chạy xong **không tạo diff** so với HEAD (bỏ qua dòng `last_reviewed`) | `npm run gen:reference && git diff --exit-code -I '^last_reviewed: ' -- docs/reference` — đã enforce trong `ci.yml` bước 5 |
| AS-01.4.2 | Mọi bảng D1 có trong `migrations/` đều xuất hiện ở `reference/data-dictionary.md` | so bảng — script `gen-reference.mjs` tự sinh, đối chiếu số bảng ở đầu file |
| AS-01.4.3 | Mọi route đăng ký trong router xuất hiện ở `reference/api.md` | so bảng — `npm run contract:diff --print` đối chiếu route count |
| AS-01.4.4 | Mọi event type phát trong code xuất hiện ở `reference/events.md` | so `EVENT_REGISTRY` (`workers/api/src/shared/events.ts`) với bảng trong `events.md` |
| AS-01.4.5 | Trang sinh tự động ghi rõ "sinh tự động — không sửa tay" ở đầu file | đọc — dòng `⚙️ Trang này sinh tự động…` |
| **AS-01.5** | **Tươi mới & vòng đời** | |
| AS-01.5.1 | Mọi doc `active` có `last_reviewed` cách ngày audit ≤ 90 ngày | script |
| AS-01.5.2 | Mỗi lần sửa nội dung đáng kể có tăng `version` trong cùng commit | `git log -p -- <file>` spot-check ≥5 commit sửa nội dung |
| AS-01.5.3 | Không có internal link gãy trong toàn bộ docs | `node scripts/check-docs.mjs` mục 4 |
| AS-01.5.4 | Ngày viết tuyệt đối; không có "tuần trước / gần đây / sắp tới" trong doc active | `grep -rn "tuần trước\|gần đây\|sắp tới" docs/` — mọi hit phải là ngôn ngữ UI hiển thị cho người dùng, không phải mốc thời gian tài liệu |
| AS-01.5.5 | Mọi quyết định AI tự quyết mang ký hiệu ✍️ và nằm trong `open-questions/index.md` | đọc `open-questions/index.md` |

***

## AS-02 — Yêu cầu & Sản phẩm

| ID | Chỉ báo | Cách kiểm |
| --- | --- | --- |
| **AS-02.1** | **Chất lượng REQ catalog** | |
| AS-02.1.1 | Mỗi REQ có ID đúng scheme `REQ-<nhóm>-xx`, nhóm thuộc bảng nhóm PRD-001 §5 | `grep -oE "REQ-[A-Z]+-[0-9]+" docs/product/prd-001/req/*.md \| sort -u` đối chiếu bảng nhóm ở `conventions.md` §2 |
| AS-02.1.2 | Mỗi REQ mô tả **testable**: phán quyết đúng/sai được bằng quan sát, không dùng từ mơ hồ ("tốt", "nhanh", "dễ dùng", "thân thiện", "đẹp", "tối ưu") | `grep -inE "tốt\|nhanh\|dễ dùng\|thân thiện\|đẹp\|tối ưu" docs/product/prd-001/req/*.md` — mỗi hit trong dòng REQ (không phải văn xuôi) là 1 phản ví dụ (L4) |
| AS-02.1.3 | Mỗi REQ có priority đúng một trong MUST/SHOULD/COULD — cấm `M?`, `S?` | `grep -nE "\| M\? \|\| S\? \|\| C\? \|" docs/product/prd-001/req/*.md` phải rỗng |
| AS-02.1.4 | Mỗi REQ ghi nguồn `SRC-xxx` hoặc nguồn người + ngày | đọc bảng REQ |
| AS-02.1.5 | Không có hai REQ mô tả cùng một hành vi (trùng nghĩa) — đo bằng token-overlap thô: hai description trùng ≥80% từ, chưa gắn 🗄️ superseded hoặc chưa có ghi chú phân biệt tường minh | script token-overlap trên cột "yêu cầu" của mọi REQ; đây là proxy gần đúng, không phải semantic dedup hoàn hảo — ghi rõ giới hạn trong báo cáo |
| **AS-02.2** | **User story & acceptance criteria** | |
| AS-02.2.1 | Mọi US đúng format *As a … / I want … / so that …* | `grep -c "As a \*\*" docs/workflows/*.md` khớp tổng số US |
| AS-02.2.2 | Persona trong US thuộc tập persona đã định nghĩa (Nemo/Marlin/Dolphin/Staff…) | đối chiếu PRD §personas |
| AS-02.2.3 | Mỗi US có ≥1 acceptance criteria quan sát được | đọc — bảng US phải có cột AC riêng, không chỉ 3 cột (ID·story·REQ) |
| AS-02.2.4 | Acceptance criteria mô tả **kết quả người dùng thấy**, không mô tả giải pháp kỹ thuật | đọc — AC không chứa tên bảng D1/tên hàm/tên biến |
| AS-02.2.5 | Mỗi US MUST đã implement chỉ ra được màn hình/endpoint thật | đối chiếu app — click qua route thật hoặc `grep` path trong `apps/*/src` |
| **AS-02.3** | **Persona, JTBD & phủ vai trò** | |
| AS-02.3.1 | Mỗi persona active có ≥1 JTBD được ghi | đọc `parent-jtbd.md` + PRD |
| AS-02.3.2 | Mỗi JTBD có ≥1 REQ phục vụ | trace |
| AS-02.3.3 | Ba cộng đồng (Nemos, Marlins, Dolphins) đều có luồng onboarding được đặc tả | đọc `docs/workflows/` — có file riêng cho mỗi cộng đồng, hoặc ghi rõ dùng chung |
| AS-02.3.4 | Mỗi school (Turtle, Shark, Octopus, Squid, Ray, Whale) có mô hình riêng hoặc khai báo dùng chung tường minh | SDD-011 |
| AS-02.3.5 | Không có màn hình đang chạy production mà không thuộc persona nào | rà route từng app, đối chiếu persona trong PRD |
| **AS-02.4** | **Phạm vi & ưu tiên** | |
| AS-02.4.1 | Mọi REQ trạng thái ✔ chỉ ra được artifact thực thi (code/migration/nội dung) | trace |
| AS-02.4.2 | Không có tính năng đang chạy production mà thiếu REQ tương ứng | rà ngược từ app — liệt kê route/feature, đối chiếu PRD |
| AS-02.4.3 | Backlog ứng viên (`CC-*`, `FEAT-*`) tách bạch khỏi REQ active, không lẫn bảng | `grep -c "CC-\|FEAT-" docs/product/prd-001/req/*.md` phải là 0 trong bảng REQ chính |
| AS-02.4.4 | Mỗi FEAT legacy được phân loại: đã port / đã thay / cố ý bỏ (kèm lý do) | `legacy-feature-inventory.md` cột "Phân loại" đóng {đã-port, đã-thay, cố-ý-bỏ, chưa-quyết}; 0 dòng chưa-quyết |
| AS-02.4.5 | Không REQ MUST nào ở trạng thái ⏳ quá 30 ngày mà không có ghi chú lý do + ngày tuyệt đối | đọc PRD §7 |
| **AS-02.5** | **Quyết định mở & phê duyệt** | |
| AS-02.5.1 | Mọi câu hỏi mở có ID `Q-xxx` và trạng thái (chờ / đã trả lời — trả lời không rỗng hoặc dòng ✍️) | `grep -c "\| Q-" docs/open-questions/index.md` đối chiếu số Q có trả lời/✍️ |
| AS-02.5.2 | Mọi dòng ✍️ có lý do ≥15 ký tự **và** không thuộc danh sách cụm rỗng nghĩa ("theo ý chủ dự án", "đã quyết rồi", "tự quyết", "vì cần thiết", "hiển nhiên"); mỗi bảng ✍️ nằm dưới tiêu đề có ngày tuyệt đối | `grep -inE "theo ý chủ dự án\|đã quyết rồi\|tự quyết\|vì cần thiết\|hiển nhiên" docs/open-questions/index.md` phải rỗng trong các dòng ✍️; đo độ dài phần lý do sau `✍️` |
| AS-02.5.3 | Artifact "Rà mâu thuẫn giữa các quyết định ✍️" tồn tại, ≤90 ngày, khai luật ưu tiên, liệt kê mọi chỗ lệch tìm thấy, **và khai rõ phạm vi đã rà** (toàn bộ hoặc tập con có lý do — sampling minh bạch được chấp nhận cho đợt trước pilot) | đọc artifact — không còn bắt buộc "số Q đã rà == tổng Q"; bắt buộc dòng "Phạm vi: …" không rỗng |
| AS-02.5.4 | Bảng "Việc còn chờ chủ dự án" có cột Trạng thái, mọi dòng có giá trị | đọc `open-questions/index.md` §Còn chờ hoặc README |
| AS-02.5.5 | Mọi Q đã trả lời có cột "Đã vào" không rỗng (doc+§, path repo, hoặc `vận-hành`); link resolve | script: với mỗi Q có trả lời, kiểm cột "Đã vào" khác rỗng và (nếu là path) `existsSync` |

***

## AS-03 — Kiến trúc & Hợp đồng API

| ID | Chỉ báo | Cách kiểm |
| --- | --- | --- |
| **AS-03.1** | **Ranh giới module & monorepo** | |
| AS-03.1.1 | Mọi `createRoute(` nằm trong `workers/api/src/modules/`, **trừ** các ngoại lệ liệt kê tường minh trong bảng "Ngoại lệ tường minh" ngay dưới bảng này | `grep -rln "createRoute(" workers/api/src \| grep -v "/modules/"` — mỗi hit phải có mặt trong bảng ngoại lệ, nếu không là KHÔNG PASS (L4) |
| AS-03.1.2 | Đồ thị phụ thuộc module 0 chu trình và 0 import chéo trực tiếp giữa hai module (phải qua `shared/` hoặc event), **trừ** cạnh liệt kê tường minh trong bảng "Ngoại lệ tường minh" | `grep -rn "from \"\.\./\.\./[a-z]" workers/api/src/modules --include="*.ts"` loại trừ import `shared/` |
| AS-03.1.3 | Không `fetch(` nào trong `apps/*/src` ngoài đúng một `src/api.ts`/app | `grep -rln "fetch(" apps/*/src \| grep -v "/api.ts"` phải rỗng |
| AS-03.1.4 | Không app định nghĩa lại CSS custom property đã có trong `packages/design-system/tokens/*.css` (file art/SVG miễn trừ) — xem thêm AS-09.2.1 (góc khác: hardcode hex khi *dùng* token, không phải định nghĩa lại token) | `grep -rn "^\s*--[a-z-]" apps/*/src --include="*.css"` đối chiếu tên biến đã có trong `tokens/*.css` |
| AS-03.1.5 | Mọi thư mục cấp 1 của `apps/`/`workers/` có tên trong README §cấu trúc và ngược lại; thư mục ngừng phải ghi rõ "đã ngừng" | đối chiếu `ls apps workers` với README |
| **AS-03.2** | **Hợp đồng API** | |
| AS-03.2.1 | OpenAPI **sinh từ router**; không tồn tại file OpenAPI viết tay ngoài `openapi.snapshot.json` (chính là snapshot của bản sinh) | `find . -iname "*.yaml" -o -iname "openapi*.json" \| grep -v snapshot` phải rỗng |
| AS-03.2.2 | Mọi operation có ≥1 response 2xx kèm schema JSON; mọi requestBody kèm schema JSON | `npm run contract:diff -- --print \| jq` đếm operations thiếu schema |
| AS-03.2.3 | Mọi path trong spec sinh ra bắt đầu `/v1` | `npm run contract:diff -- --print \| jq '.paths \| keys'` |
| AS-03.2.4 | 🔴🚸 Không breaking change trong `/v1` (xoá field/route, đổi kiểu, thêm required ở request) so với `openapi.snapshot.json`; có mặt ở **ít nhất một** workflow chạy trên mọi push (nay là `ci.yml`) | `npm run contract:diff` exit 0 — hiện chạy trong `ci.yml` bước 6 trên mọi push/PR |
| AS-03.2.5 | Probe 404/401/400 trên production trả `{error:{code,message}}`, không rò ZodError/stack | `curl` thử endpoint sai, đọc body |
| **AS-03.3** | **Hợp đồng event/queue** | |
| AS-03.3.1 | Mọi entry `EVENT_REGISTRY` có `version`(int)+`payload`(zod object) | đọc `workers/api/src/shared/events.ts` |
| AS-03.3.2 | Đối soát hai chiều: mọi tên `publishEvent()` gọi ∈ registry; mọi entry registry có ≥1 điểm gọi thật **HOẶC** ghi rõ "cố ý chưa có consumer — lý do + ngày" trong `reference/events.md` — quá **30 ngày** không có REQ liên kết thì tính HH (mồ côi thật), không còn là CX | `grep -rn "publishEvent(" workers/api/src/modules` đối chiếu tên event với `EVENT_REGISTRY` keys |
| AS-03.3.3 | Tên event khớp `domain.entity.action`, action ở tập đóng thì quá khứ | `grep -oE '"[a-z]+\.[a-z]+\.[a-z]+"' workers/api/src/shared/events.ts` |
| AS-03.3.4 | `assertNoFreeText()` tồn tại+được gọi trong `publishEvent`; không `z.string()` trần/field PII không bị cắt | đọc `workers/api/src/shared/events.ts` — có hàm `assertNoFreeText` (xác nhận: tồn tại thật, dòng cuối file) |
| AS-03.3.5 | Số trong `wrangler.jsonc` (tên queue, max_batch_size, max_retries, DLQ) khớp đúng `queues.md` | đối chiếu `workers/api/wrangler.jsonc` với `reference/queues.md` |
| **AS-03.4** | **Vệ sinh secret/config** | |
| AS-03.4.1 | 🔴 Không pattern key/secret/token hardcode trong source hoặc lịch sử git | `grep -rE "(api[_-]?key\|secret\|token)\s*[:=]\s*['\"][A-Za-z0-9]{16,}" --include="*.ts"` + `git log -p --all \| grep -E` cùng pattern |
| AS-03.4.2 | Base URL API xuất hiện ≤1 lần/app, chỉ trong `src/api.ts`; literal `nemo12.com` còn lại phải là host trong README | `grep -rn "nemo12.com" apps/*/src \| grep -v "src/api.ts"` |
| AS-03.4.3 | Mọi biến môi trường bắt buộc được khai kiểu trong `workers/api/src/env.ts` | `node scripts/verify-bindings.mjs` Lớp A |
| AS-03.4.4 | Mọi binding (D1/KV/R2/Queue/AI) được CI verify là tồn tại thật trên Cloudflare, có trong `ci.yml` | `npm run verify:bindings` Lớp C — cần `CLOUDFLARE_API_TOKEN`; không có token thì lùi về A+B (ghi rõ trong kết quả, không coi là PASS đủ Lớp C) |
| AS-03.4.5 | `reference/config.md` liệt kê đủ mọi biến cấu hình đang dùng (danh sách đóng khớp `wrangler.jsonc`) | `npm run verify:bindings` Lớp B |
| **AS-03.5** | **Cấu trúc & kỷ luật pipeline deploy** | |
| AS-03.5.1 | Mỗi `wrangler.jsonc` có workflow deploy tương ứng với path filter đúng | `.github/workflows/` đối chiếu `apps`/`workers` |
| AS-03.5.2 | Mỗi `wrangler.jsonc` khai `custom_domain`/`routes` (không `*.workers.dev` trần) | đọc wrangler config |
| AS-03.5.3 | 🚸 (nội dung mới, thay "app↔workflow đủ đôi" — xem [§1.14](./history.md#_1-14-root-cause-that-cua-su-co-đa-agent-khong-co-chi-bao-nao-bat-push-thang-main-apply-migration-ngoai-ci)) Không thao tác `wrangler d1 migrations apply --remote` nào chạy trực tiếp từ máy/phiên cục bộ ngoài đường `ci.yml` trên `main` và `workflow_dispatch` của `deploy-app.yml`. PASS nếu: (a) branch protection required-review bật trên `main` — kiểm `gh api repos/<org>/<repo>/branches/main/protection` (200) — **HOẶC** (b) plan hiện tại không bật được (403 "Upgrade to GitHub Pro") **và** có quy ước ghi tường minh trong README/CLAUDE.md **và** AS-04.6.1 không phát sinh dòng mồ côi mới nào trong 7 ngày gần nhất (bằng chứng gián tiếp là không ai apply tay) | `gh api repos/.../branches/main/protection`; nếu 403 thì đọc README + đối chiếu AS-04.6.1 |
| AS-03.5.4 | Bước apply-migration đứng trước bước `wrangler deploy` | đọc `ci.yml` và `deploy-app.yml` — xác nhận: dòng apply ở trước bước Deploy ở cả hai |
| AS-03.5.5 | Mọi URL production trong README trả 2xx/3xx; khớp đúng tập `custom_domain` trong mọi `wrangler.jsonc` | `curl -sI` từng URL trong README, đối chiếu domain list |

### Ngoại lệ tường minh (AS-03.1.1 / AS-03.1.2)

Danh sách đóng — thêm dòng mới khi có ngoại lệ mới, không thêm ngoại lệ ngoài bảng này:

| Chỉ báo | Vị trí | Lý do |
| --- | --- | --- |
| AS-03.1.1 | `workers/api/src/index.ts:81` — `healthRoute` | Health check là route hạ tầng cấp worker, không thuộc nghiệp vụ một module cụ thể nào; xác nhận đây là **hit duy nhất** ngoài `modules/` tính đến 2026-08-15 (`grep -rln "createRoute(" workers/api/src \| grep -v "/modules/"` → 1 dòng) |
| AS-03.1.2 | *(bảng trống tới 2026-08-15; 10 cạnh dưới đây khai ngày 2026-08-20 — Audit #004, SRC-412)* | Tính đến 2026-08-15, `grep` import chéo giữa 19 module trả 0 kết quả (Audit #001 xác nhận). Audit #004 đo lại và thấy 10 cạnh mới, tất cả đều cố ý; khai ra đây thay vì im lặng chấp nhận |
| AS-03.1.2 | `coral/routes.ts` → `content/quality`, `content/lifecycle`, `content/review-queue` | Coral **là** mặt quản trị của chính miền content (SDD-013): tách qua `shared/` sẽ tạo một lớp trung chuyển chỉ có đúng một người gọi. Cạnh một chiều, không có chu trình |
| AS-03.1.2 | `learning/routes.ts` → `knowledge/mastery` | Chấm một câu trả lời và cập nhật mastery là **một** giao dịch nghiệp vụ; đẩy qua event sẽ khiến learner thấy điểm cũ ở màn hình ngay sau khi trả lời |
| AS-03.1.2 | `learning/routes.ts` → `retention/service`, `content/lifecycle`, `models/service` | Ba lời gọi đọc/kích hoạt sau khi có bằng chứng mới (câu nhắc ôn, cờ nội dung AI, dựng lại model nếu cũ). `models/service` được gọi qua `runAllModelsInBackground`/`refreshIfStaleInBackground` nên không chặn request |
| AS-03.1.2 | `progress/routes.ts` → `knowledge/mastery` | Dùng lại đúng một hàm thuần `stateFor(mastery, confidence)`; chép sang `shared/` sẽ thành hai định nghĩa của cùng một ngưỡng |
| AS-03.1.2 | `admin/observability.ts` → `models/service`, `models/contextEvents` | Admin là chỗ **bấm chạy** engine và đọc taxonomy sự việc; nó phải gọi thẳng vào miền model để có bằng chứng thi hành đồng bộ (workflow_run + engine_run trong cùng lượt) |

***
