Skip to content

SDD-001 — Platform Architecture ​

Nguyên tắc: One World, One Learner, One Platform. Nemo12 là distributed education platform về business capability, unified learner system về technology.

1. High-Level ​

text
nemo12.com ── Cloudflare Edge (WAF, DDoS, rate limit, bot)
   │
   ├─ apps (Vite/React SPA + assets-on-Worker): web · learn · marlins · mentors(dolphin) · admin · coral · b21 · ielts
   ├─ apps (VitePress): docs · pearl · compass · playbooks
   ├─ workers/foundry (nemo12-foundry) ── Cloudflare Workflows sinh nội dung, chỉ service binding từ api
   ├─ workers/tool-plane (mcp.nemo12.com) ── MCP server cho AI agent, dùng chung D1
   │
   └─ api.nemo12.com ── Hono on Workers (modular monolith)
         ├─ D1  `nemo12-platform` (id f4275a70-66fa-4746-8292-40f96a7ed288) — system of record
         ├─ R2  `nemo12-content` — heavy assets/media
         ├─ KV  `nemo12-config` (id f08c239409404bfe82b5aad303de362d) — config/flags/manifests cache
         ├─ Queues `nemo12-events` (+ `nemo12-events-dlq`) — async fan-out
         ├─ Workflows — durable multi-step processes
         └─ AI Gateway `nemo12` — mọi AI request (bắt buộc, không bypass)

Tài nguyên trên account Cloudflare ff302e77218f9616c3baaf01db59d64e, zone nemo12.com (id 09caf325ee80eca4743957e03b655867). Tạo ngày 2026-08-13.

2. Modular Monolith (REQ-PLT-02) ​

Bounded modules trong một Worker api (tách service chỉ khi có lý do scale/security rõ). Danh sách dưới đây là thư mục thật trong workers/api/src/modules/ (29, 2026-09-04) — bản đầu của SDD này liệt kê 16 tên module dự kiến (identity · family · learner · school …) mà không tên nào thành thư mục; Audit #013 (T-8) bắt được người mới đọc "platform SDD" để tìm module thì lạc:

text
auth · invitations · families · onboarding · privacy · referral          (danh tính, gia đình, đồng thuận)
knowledge · learning · models · retention · goals · progress · exams     (trí tuệ học tập, đo, luyện)
curriculum · courses · content · coral · dictation · b21                 (học liệu, xưởng nội dung, khung năng lực)
parent · digest · portraits · forum · events · mentor · showcase         (phụ huynh, cộng đồng, mentor)
orca · whale · admin                                                    (cuộc thi, học bổng, vận hành)

Mỗi module: thư mục riêng, export router Hono (đăng ký ở src/index.ts) + service; dùng chung src/shared/* (authz, audit, prompts, ratelimit, reliability, runlog, time…); không import chéo internals của module khác. Kiểm bằng ls workers/api/src/modules — đừng sửa danh sách này bằng tay mà không đối chiếu.

3. Cloudflare Services (REQ-PLT-01) ​

Nhu cầuDịch vụQuy tắc
ComputeWorkersHono, TypeScript
RelationalD1Một database nemo12-platform; school phân biệt bằng school_id/program_id, không tách DB theo school (ADR-002)
Heavy contentR2metadata ở D1, D1 chỉ giữ content_ref (SDD-004, SDD-009)
Cache/configKVkhông chứa learner state quan trọng
AsyncQueuesbatching, retry, DLQ (SDD-006 §5)
Durable processWorkflowsdiagnosis, evaluation, recalculation…
AIAI Gateway nemo12logging, caching, rate-limit; cấm direct provider call và cấm "probe-then-fallback" kiểu chuyenchon (RISK-022)
API edgeAPI Shield/Gateway (khi bật) + code-level hardeningmethod/path allowlist, body cap (kế thừa pattern sutucon shared/gateway.ts)

4. Frontend (REQ-PLT-05, REQ-BRD-01, REQ-SCH-01) ​

text
apps/
├── web/       nemo12.com (+ www)   — public, 6 school pages /turtle…/whale, SEO+OG
├── learn/     learn.nemo12.com     — Nemos; school = route context (/turtle, /shark…)
├── marlins/   marlins.nemo12.com   — parents
├── mentors/   mentors.nemo12.com   — Dolphins (build sau, thứ 4)
├── admin/     admin.nemo12.com     — internal, sau Cloudflare Access (build cuối)
└── docs/      docs.nemo12.com      — VitePress, render docs/ canonical (sau Access)
  • Vite + React + TypeScript, Cloudflare Vite plugin; deploy dạng Worker với static assets (không dùng Pages — hợp nhất một mô hình deploy).
  • School không có app riêng — chỉ là route/theme context (REQ-SCH-01).
  • @latest khi cài, lock trong lockfile (Vite 8.x, Tailwind v4, shadcn CLI mới nhất).

5. API (REQ-PLT-04, REQ-PLT-10) ​

  • Base: https://api.nemo12.com/v1/ — resources: /learners /families /invitations /enrollments /evidence /assessments /recommendations /learning /content /media /interactions /notifications.
  • Contract OpenAPI sinh từ code (@hono/zod-openapi): mọi route bắt buộc có zod schema (fix RISK-024 — sutucon hand-maintained openapi). GET /v1/openapi.json public.
  • API-first cho mobile: không logic nào chỉ nằm trong web app; response envelope {data} / {error:{code,message}} theo error taxonomy SDD-006 §11.
  • Versioning: /v1 ổn định; breaking change → /v2 song song (REQ-NFR-10).

6. Identity, Auth & Access (REQ-ACC-*, REQ-SEC-05) ​

Parent-first (Q-006):

text
Parent bấm "Đăng nhập với Google" (Google Identity Services, id_token)
→ api verify id_token (aud = GOOGLE_CLIENT_ID, email_verified)
→ chưa có user? tạo `users` + `families` + membership 'owner' (tự động, không form)
→ session: token opaque 32B, lưu sha256(token), cookie `nemo12_session`
  Domain=.nemo12.com HttpOnly Secure SameSite=Lax, Max-Age 30d, CÓ rotation khi refresh
  • GSI id_token flow → không cần Google client secret (kế thừa sutucon, RISK-021 tránh được). GOOGLE_CLIENT_ID là var công khai.
  • Invite flow: parent tạo invitation (email hoặc link + mã) → learner login Google qua link → account learner tạo + gắn family_id + guardian links. Learner không qua invite = không enroll được (REQ-ACC-03).
  • Mobile (REQ-ACC-06): cùng endpoint đổi id_token → session token, client mobile giữ token trong secure storage, gửi qua Authorization: Bearer — cookie và bearer song song.
  • RBAC: bảng role_assignments (user_id, role, scope) — roles learner|guardian|mentor|staff|admin; mọi authorization ở backend, theo quan hệ family/assignment (REQ-SEC-02).
  • Cloudflare Access bảo vệ admin. + docs. (+ staging). Cấu hình Zero Trust: app per hostname, policy = email domain/list; audience + team domain đặt qua env var, cấm hardcode (REQ-SEC-06, fix RISK-013). Trạng thái 2026-08-13: chưa bật được qua API token hiện có — cần làm trên dashboard; admin/docs không deploy public cho tới khi Access bật.

7. Data Layer (REQ-ACC-04, REQ-PLT-11) ​

  • Canonical identity: users.id (UUIDv7), learners.id riêng (một user có thể là learner; guardian liên kết qua guardians/family_members). Email chỉ là thuộc tính đăng nhập — cấm làm khóa dữ liệu (fix RISK-001 owner_key=email của chuyenchon).
  • Core tables (migration 0001): users, families, family_members, learners, guardians, invitations, sessions, auth_identities, role_assignments, audit_log.
  • Domain tables theo SDD-002/003/004/005/009.
  • Migrations: một sequence duy nhất migrations/ ở root monorepo, áp bằng wrangler d1 migrations apply nemo12-platform; idempotent khi có thể; cấm DML destructive trộn DDL; cấm nhiều thư mục migration trỏ cùng DB (fix RISK-004/005 của cả 2 legacy).
  • Timestamps: TEXT ISO-8601 UTC (SQLite), tên cột *_at.

8. Events & AI plumbing (REQ-PLT-03, REQ-PLT-09) ​

  • Domain events chuẩn: {event_id, idempotency_key, type, occurred_at, producer, version, payload} — publish sau commit, qua nemo12-events; consumer idempotent; quá retry → DLQ.
  • Catalog khởi điểm: UserCreated, FamilyCreated, LearnerInvited, LearnerJoined, EnrollmentCreated, EvidenceRecorded, AssessmentCompleted, SkillMasteryChanged, RecommendationGenerated, LearningExperienceCompleted, MentorInteractionRecorded, ParentObservationRecorded, CommentCreated, MediaUploaded, MediaModerated.
  • AI: một module ai/ duy nhất gọi AI Gateway nemo12; ghi {model, prompt_version, input_ref, output, confidence, evaluation_state} cho mọi output quan trọng (REQ-INT-14). Prompt registry versioned trong repo (packages/ai/prompts/). Provider mặc định: Workers AI (llama) cho tác vụ rẻ, có thể route Claude qua Gateway cho đánh giá chất lượng cao (Q-020 tự quyết — xem open-questions).

9. Monorepo (REQ-PLT-07) ​

text
nemo12/
├── apps/{web,learn,marlins,mentors,admin,coral,b21,ielts}   # React + Vite, mỗi app một Worker assets
├── apps/{docs,pearl,compass,playbooks}                      # VitePress
├── workers/api            # Hono modular monolith (29 module, §2)
├── workers/foundry        # Cloudflare Workflows sinh nội dung (SDD-027)
├── workers/tool-plane     # MCP server mcp.nemo12.com (SDD-030)
├── packages/design-system # token + CSS + Motion preset; CHƯA export TSX (Audit #013 U-2)
├── migrations/            # một sequence duy nhất cho nemo12-platform
├── content/               # ngữ liệu (dictation…) nạp lúc build
├── scripts/               # cổng chất lượng check-*.mjs, seed, sinh trang
└── docs/                  # canonical documentation (REQ-PLT-08)

npm workspaces (Node 22). CI: một job ci.yml cho mọi push vào main (cổng → áp migration → deploy app bị đụng, phạm vi tính bằng .github/scripts/scope.sh); 12 deploy-*.yml chỉ chạy tay. Các gói domain · database · events · ai · auth · shared trong bản đầu chưa bao giờ được tạo — logic tương ứng nằm ở workers/api/src/shared/.

Xếp hàng chạm production: chờ, đừng xếp hàng (SRC-797) ​

ci.yml (trên main), rollback.yml và seed-data.yml đều ghi thẳng production, nên chúng không được chạy chồng lên nhau (Audit #013, I-8 và I-19). Cách làm cũ là cho cả ba cùng concurrency: group: production. Cách đó sai theo một kiểu im lặng, và đã gây tai nạn thật ngày 17.09.2026: GitHub chỉ cho đúng một run được xếp hàng chờ trong mỗi group, nên khi một lượt seed-data xếp vào đúng lúc một run ci đang chờ, run ci ấy bị huỷ — mang theo cả phần deploy của nó.

Vì sao ci huỷ ci thì không sao mà seed-data huỷ ci thì có sao: hai lượt ci trên main nối tiếp nhau, lượt sau chứa commit của lượt trước nên deploy vẫn tới đích. seed-data thì không deploy gì cả — nó hất run kia đi rồi ghi D1, và không có ai deploy bù. Kết quả hôm đó là một commit nằm trên main nhưng không có trên production suốt ba tiếng, và trạng thái để lại là cancelled nên rất dễ đọc nhầm thành "ai đó chủ động huỷ" thay vì "deploy đã mất".

Cách làm hiện tại giữ nguyên tính loại trừ nhưng giành nó bằng cách chờ:

  • seed-data.yml có group riêng production-seed — hai lượt seed vẫn không chạy chồng nhau.

  • Trước khi ghi, nó chạy .github/scripts/wait-production.sh: hỏi API xem còn run ci / rollback / deploy-* nào đang chạy không, và chờ cho tới khi sạch.

  • Chạy hai lần: một lần ngay sau checkout, một lần nữa sát lệnh ghi. Giữa hai lần đó là npm ci — đủ dài để một lượt deploy kịp bắt đầu mà lần chờ đầu không biết.

  • Chờ quá 25 phút thì hỏng có tiếng, và không ghi gì vào D1.

  • deploy-app.yml (deploy tay một app) theo đúng cách ấy từ SRC-1205: group riêng production-deploy-app, chờ wait-production.sh ngay trước bước migration. Trước đó nó còn xếp chung production, nên bấm deploy tay lúc một run ci trên main đang chờ là huỷ run ấy, và mọi app khác đổi trong push đó không được deploy. Lỗi do lượt audit 03.10.2026 tìm ra.

rollback.yml cố ý giữ nguyên group: production. Rollback là công cụ khẩn cấp: lúc dùng nó thì việc giành chỗ ngay là đúng, và luôn có người đang ngồi nhìn.

Xanh không có nghĩa là đã kiểm (SRC-798) ​

ci.yml gác từng cổng theo phạm vi thay đổi, nên một lượt có thể kết thúc xanh sau khi bỏ qua mọi cổng. Với commit chỉ đụng docs/ thì đó đúng và cố ý. Vấn đề là hệ ở hai chỗ khác lại coi "xanh" là bằng chứng, mà không hỏi lượt đó có chạy gì không:

  1. scope.sh lấy lượt ci xanh gần nhất làm mốc so sánh cho lần sau;
  2. wait-for-ci.sh cho phép deploy tay khi thấy một lượt ci xanh trên đúng commit.

Ngày 17.09.2026 chuyện này thành tai nạn thật. scope.sh tính sai phạm vi — echo "$files" | grep -q gặp set -o pipefail: grep -q thoát ngay ở dòng khớp đầu tiên, echo chết vì SIGPIPE (141), pipeline bị coi là thất bại, nên hàm trả false đúng vào lúc regex có khớp. Kết quả: một push đụng workers/api/ và apps/mentors/ cho ra phạm vi rỗng, mọi cổng bị bỏ qua, không app nào được deploy, và CI báo xanh trong 24 giây. Lượt xanh rỗng đó rồi thành mốc so sánh cho lượt kế, nên phạm vi tiếp tục thiếu.

Ba lớp vá, mỗi lớp chặn một tầng khác nhau:

LớpỞ đâuChặn cái gì
Nguyên nhânhit/miss dùng herestring thay vì ốngkhông còn ống thì không còn SIGPIPE
Loại hỏng hócscope.sh tự soi: có file đổi mà code lẫn docs đều false → nổmọi nguyên nhân khác cho ra phạm vi rỗng, kể cả chưa biết
Không biết phạm vidanh sách file rỗng → bật mọi cờ, không tắthướng an toàn là chạy rộng, không phải chạy hẹp

Và ở phía tiêu thụ, wait-for-ci.sh nay hỏi thêm một câu trước khi tin: lượt xanh đó có chạy cổng code không? Nó đọc kết luận của bước Typecheck · Lint · Test trong chính run ấy. Xanh mà bỏ qua thì không tính là bằng chứng — rơi về nhánh tự chạy cổng tại chỗ (chậm hơn ~90 giây). Đọc không được danh sách bước cũng tính là không: khi không biết thì kiểm lại, không cho qua.

Lập luận đằng sau cả bốn: một phạm vi hẹp và một phạm vi sai trông giống hệt nhau trên màn hình, và cái giá của hai bên không cùng hạng. Chạy thừa vài phút là tiền; deploy hụt trong im lặng là một commit nằm trên main mà không có trên production, và không ai biết để đi tìm.

Hỏi thẳng production đang chạy commit nào (SRC-799) ​

Cổng trên bắt được phạm vi rỗng. Phạm vi thiếu một phần thì nó không thấy: code=true, mọi cổng chạy thật, mười bốn app lên, một app bị bỏ quên — mọi dấu hiệu đều bình thường. Loại đó chỉ bắt được bằng cách hỏi chính production.

Dấu. .github/scripts/deploy.sh đóng nemo12-commit:<sha> vào Worker Version qua cờ --message của wrangler; Cloudflare giữ nguyên câu đó và trả lại qua API. Chọn cách này thay vì một endpoint /__version trong từng app vì endpoint phải viết mười lăm lần và không dùng được với hai app nằm sau Cloudflare Access — mà dolphin, đúng app đã lệch hôm 17.09, là một trong hai. Không có GITHUB_SHA (chạy tay dưới máy) thì không đóng dấu: một dấu sai tệ hơn không có dấu, vì cổng đối chiếu sẽ tin nó.

Nhân việc này, 15 workflow deploy-*.yml chuyển từ gọi thẳng npx wrangler deploy sang gọi deploy.sh — nên chúng được đóng dấu, và hưởng luôn phần thử lại khi API Cloudflare nấc (Audit #013, I-6) mà trước giờ chỉ ci.yml có.

"Cũ hơn main" không phải lỗi. Hầu hết app luôn cũ hơn vài chục commit, và đó là đúng — chúng chỉ deploy lại khi có thứ thuộc về chúng đổi. Lệch thật là: bản đang chạy cũ hơn, và giữa hai mốc có commit đụng vào chính app đó.

Một nguồn cho câu "app nào gồm đường dẫn nào". Câu đó trước chỉ nằm trong scope.sh. Cổng đối chiếu cần đúng nó, nên nó được tách ra .github/app-paths.json và cả hai bên cùng đọc. Chép sang lần thứ hai là mở đường cho ngày hai bên lệch — và lúc lệch thì cổng đối chiếu nói sai về chính thứ nó sinh ra để canh, lại nói bằng giọng chắc chắn. scope.sh sau khi đổi được đối chiếu trên bốn mốc so sánh khác nhau: đầu ra giống hệt bản cũ.

Chạy hằng ngày, không chắn push. Cổng này hỏi API Cloudflare, tức là phụ thuộc mạng; một cổng như vậy đứng chắn mọi push thì đến ngày API nấc là mọi phiên đứng — cùng lập luận đã dùng để giữ check:ui-latest ngoài CI (DS-001 §0). Và loại hỏng hóc nó canh là loại dai: lần trước dolphin lệch gần bốn tiếng. Một lượt 06:00 giờ Việt Nam cộng chạy tay bất cứ lúc nào là đủ nhanh mà không cầm chân ai.

Giới hạn, nói thẳng: app deploy trước 17.09.2026 chưa mang dấu, nên lần đầu chạy sẽ hiện "chưa có dấu" — cảnh báo, không chặn, và tự hết sau lần deploy kế tiếp của từng app. Cổng cũng chỉ so commit, không so nội dung bản dựng: một lần deploy đúng commit nhưng dựng hỏng thì nó vẫn nói "đúng HEAD".

10. Documentation plane (REQ-PLT-08, REQ-BRD-12) ​

docs/ = canonical duy nhất; VitePress render tại apps/docs; frontmatter machine-readable (conventions.md); CI check broken links + trace closure (QG-001).

Ba site VitePress dùng chung một khuôn (REQ-DOC-10): apps/docs (canonical docs), apps/pearl (knowledge công khai) và apps/playbooks (playbooks.nemo12.com — action library, REQ-BRD-12). Cùng @nemo12/design-system qua bridge token, cùng cleanUrls + appearance: false + search local, cùng đường deploy Worker-với-static-assets trong ci.yml. Playbooks khác hai site kia ở chỗ cây nội dung là bản dẫn xuất: apps/playbooks/catalog/playbooks.json là nguồn, scripts/gen-playbooks.mjs sinh trang mục lục, khung 11 phần và sidebar; script chỉ đồng bộ frontmatter và không ghi đè phần thân đã soạn, vì phần thân là sản phẩm của xưởng nội dung chứ không phải của repo (SDD-027).

Cấu trúc điều hướng Pearl (SRC-750, 2026-09-16). Pearl có 7 nhóm nội dung phẳng trên thanh nav nhưng chỉ 5 trong số đó xuất hiện trên hub trang chủ, nên hai chỗ điều hướng nói hai chuyện khác nhau. Đã gom nav thành 5 cụm theo câu hỏi người đọc thật sự hỏi (Học · Hiểu con · Hoàn thành · Curriculum · Vì sao), hub trang chủ lên 6 card (3 cột x 2 hàng, không để card mồ côi cuối hàng), và không đổi URL của bất kỳ trang nào đang sống — chỉ đổi cách gom.

Mục mới /completion/ là nơi canonical hoá Completion & Graduation Specification cho người đọc công khai: 4 tiêu chí bắt buộc (≥6/12 sản phẩm đạt ≥80/100 với ≥3 phiên bản; ≥12 reflection; tham gia Final Product Showcase; Final Assessment ≥80/100) và 4 trạng thái In Progress · Completed · Graduated · Not Yet Graduated. Nguyên tắc nền: tốt nghiệp xét từ bằng chứng learner tạo ra, không từ tỉ lệ nội dung đã xem. Bản đặc tả tiếng Anh giữ nguyên tại /completion/spec và là bản có hiệu lực khi lệch với các trang diễn giải tiếng Việt.

Product Assessment Rubric (SRC-752, 2026-09-16) tại /completion/rubric, thứ toàn bộ ngưỡng 80/100 treo vào. Năm tiêu chí cân bằng 20 điểm (vấn đề và mục đích · nội dung và độ chính xác · lập luận và lựa chọn · thực thi và hoàn thiện · đáp ứng phản hồi), bốn mức ăn 5/10/16/20 điểm. Số học ra có chủ ý: 16 × 5 = 80, nên ngưỡng tốt nghiệp đúng bằng "Đạt ở cả năm tiêu chí" thay vì một con số tuỳ tiện, và 20 × 5 = 100. Kèm một luật chặn bù trừ: bản nộp cuối phải ≥80 và không có tiêu chí nào ở mức "Chưa đạt", vì phép cộng đơn thuần cho phép 5 + 20×4 = 85 lọt qua với một tiêu chí hỏng hẳn. Tiêu chí 5 ở bản V1 luôn bằng 0 (chưa có phản hồi nào để đáp ứng), nên trần điểm của V1 là 80: một bản nháp đầu tiên không thể một mình thoả điều kiện tốt nghiệp, khớp với ràng buộc ≥3 phiên bản. Quy trình chấm ba bước: learner tự chấm trước, mentor chấm độc lập, rồi so hai bản (khoảng lệch là đầu vào của Confidence Score). Chỉ tiêu chí 2 do từng khoá viết lại mô tả, bốn tiêu chí còn lại dùng chung nguyên văn.

Ghi nhận sau tốt nghiệp (SRC-763, 2026-09-16) tại /completion/certificate. Ba lớp, và thứ tự là quyết định chính: hồ sơ tốt nghiệp (trang có địa chỉ vĩnh viễn, dẫn thẳng tới sản phẩm thật và chuỗi phiên bản) là lớp gốc, chứng nhận chỉ là bản in dẫn xuất, huy hiệu là con trỏ mang mã hồ sơ. Đảo ngược thông lệ (chứng chỉ là thật, bằng chứng không tra được) vì một tờ giấy ghi "đã hoàn thành" phá đúng nguyên tắc nền của quy định tốt nghiệp. Chứng nhận in con số thật của bốn tiêu chí chứ không một câu tuyên bố, và không xếp loại giỏi/xuất sắc: rubric đã tính 96 và 82 như nhau, thêm tầng xếp loại là mở lại cuộc đua điểm. Xác minh qua URL mang mã mờ (không tên, không ngày sinh, theo luật cấm dữ liệu cá nhân trong URL), không cần tài khoản; hồ sơ bị thu hồi thì trang nói rõ đã thu hồi kèm ngày chứ không 404. Mặc định hồ sơ riêng tư, learner tự bật công khai và tự chọn từng sản phẩm hiện hay ẩn. Luật không ngoại lệ: nội dung reflection không bao giờ vào hồ sơ công khai (chỉ số lượng), vì reflection chỉ thật khi nó không phải màn trình diễn. Trạng thái Completed và Not Yet Graduated cũng được cấp hồ sơ, ghi đúng trạng thái và phần còn thiếu, cấm dùng chữ "tốt nghiệp". Trang nói thẳng đây không phải văn bằng được Bộ GD&ĐT công nhận. Khi learner rời Nemo12: mang được toàn bộ dữ liệu ở định dạng mở, link hồ sơ vẫn sống, tắt công khai và xoá được bất cứ lúc nào.

Danh sách các mảng Pearl còn thiếu nằm ở /roadmap; tính tới 16.09.2026 mục Hoàn thành không còn trang stub nào.

Cấp số SRC khi nhiều nhánh chạy song song (SRC-803, 2026-09-17) ​

docs/intake.md là sổ CHÈN-ONLY và mỗi dòng mang một số SRC duy nhất. scripts/src-new.mjs giữ tính duy nhất ấy bằng ba lớp, và lớp thứ ba mới có từ SRC-803:

  1. Khoá mkdir trong --git-common-dir — hai tiến trình trên cùng một máy không vào vùng ghi cùng lúc. Có từ SRC-457.
  2. Số mới = max(mọi số đang có, con trỏ) + 1 — con trỏ tụt lại cũng không cấp trùng. SRC-457.
  3. Ref giành trên remote: refs/src-claims/SRC-<n>, một commit mồ côi rỗng cho mỗi số.

Lớp 3 tồn tại vì hai lớp đầu cùng mù một chỗ: chúng chỉ nhìn file docs/intake.md của cây đang chạy. Ngày 2026-09-17, một nhánh worktree cấp SRC-798 rồi giữ nó nhiều giờ chưa gộp; phiên kế tiếp đọc bản trên main — nơi dòng ấy chưa tới — và cấp đúng 798. Vài giờ sau lặp lại y hệt với 800. Cả hai lần chỉ lộ ra lúc gộp và phải đánh số lại một nhánh đã xong việc.

Vì sao là commit mồ côi. Đẩy một ref MỚI luôn thành công; đẩy đè lên ref ĐÃ CÓ chỉ qua khi đó là fast-forward. Commit không cha không bao giờ là fast-forward của bất cứ gì, nên lượt đẩy thứ hai vào cùng một số chắc chắn bị từ chối — đúng ngữ nghĩa "tạo mới, cấm ghi đè" mà git push không có cờ riêng. Trỏ ref vào một commit có sẵn (tip của main chẳng hạn) thì hai phiên cùng trỏ vào cùng commit sẽ ra "Everything up-to-date", tức là thành công cả hai và cái khoá im lặng không khoá gì.

Chỉ lời TỪ CHỐI của remote mới nghĩa là số đã bị lấy. Mọi lỗi đẩy khác (mất mạng, thiếu quyền, sai remote) được ném ra, không được coi là tranh chấp: nuốt chúng thì một trục trặc thoáng qua sẽ lặng lẽ đốt một số, và một trục trặc dài sẽ đốt liền hai mươi lăm số rồi mới chịu dừng.

Mất mạng thì vẫn cấp được, nhưng script in cảnh báo rằng số CHƯA được giành — một phiên offline phải làm việc được, nhưng im lặng cấp bừa là dựng lại đúng cái lỗ đang vá.

Sổ có lỗ số là bình thường và vô hại; số trùng thì tốn cả một lần đánh số lại. Xem số đã giành: git ls-remote origin 'refs/src-claims/*'. Cổng: scripts/check-src-claim.mjs trong npm run check:code — nó dựng hai bản sao thật tranh số và bắt buộc chúng không được trùng.

Ref tự dọn khi hết việc (SRC-966, 22.09.2026). Một ref giành chỉ có việc tới lúc số của nó vào được bảng trong docs/intake.md trên main; từ đó trở đi nó thừa, vì bước tính "số đã dùng" đọc chính bảng ấy. Nên sau mỗi lần cấp số thành công, src-new.mjs xoá những ref có số đã nằm trong bảng trên main.

Vì sao đáng làm: lệnh git ls-remote ở trên là lệnh người ta gõ để trả lời "số nào đang được giữ". Ngày 22.09.2026 nó in ra 147 dòng, trong đó 142 đã gộp từ lâu — một câu trả lời đúng nhưng không đọc được là một câu trả lời hỏng. Dọn tay một lần thì vài giờ sau lại đầy (21 ref sau nửa buổi), nên chỗ dọn phải nằm ngay trong đường cấp số. Sau khi bật, cùng lệnh ấy in ra 4 dòng: ba số cũ chưa gộp và số vừa giành.

Ba điều bước dọn KHÔNG làm, cố ý: không đụng số vừa giành (số ấy chưa vào main); không đụng số của nhánh chưa gộp — đó chính là thứ ref này sinh ra để bảo vệ; và không bao giờ làm lệnh đỏ, vì việc chính là cấp số, còn một lượt dọn trượt chỉ để lại vài dòng thừa cho lần sau.

Cấp số MIGRATION: cùng cơ chế, một khác biệt (SRC-929, 20.09.2026) ​

Ngày 20.09.2026 tai nạn ấy lặp lại nguyên xi ở một cuốn sổ khác. Hai phiên cùng lấy 0267 — một phiên cho ielts_micro_progress, một phiên cho warmup_set_exactly_three — vì cả hai đọc migrations/ của cây mình, và thư mục mỗi bên đều không chứa file của bên kia. CI đỏ sau mười ba phút với AS-04.1.1 — migration trùng số, và một nhánh đã xong việc phải đánh số lại.

scripts/migration-new.mjs mang đúng ba lớp của SRC-803 sang, với ref refs/migration-claims/<nnnn>. Ở đây thư mục migrations/ đóng vai cuốn sổ: tên file CHÍNH LÀ số, nên không có bảng nào để chèn.

Khác biệt phải nhớ: dãy migration không được có lỗ. Cổng AS-04.1.1 đòi 0001..N liên tục, nên một số giành rồi bỏ làm đỏ CI của mọi phiên - ngược hẳn với sổ SRC, nơi lỗ số là bình thường. Hai hệ quả trong thiết kế:

  • Giành số xong thì script dựng file ngay, kèm khuôn chú thích, để số ấy có mặt thật trong nhánh chứ không sống như một lời hứa.
  • Bỏ việc giữa chừng thì phải trả số: npm run migration:new -- --release 0269. Script từ chối trả một số đang có file thật trong cây, vì trả nó đi là mời phiên sau lấy trùng.

Cổng: scripts/check-migration-claim.mjs trong npm run check:code, dựng hai bản sao thật tranh số y như check-src-claim.mjs, và kiểm thêm đường trả số.

11. Configuration rules (REQ-SEC-06) ​

Cấm hardcode: domain names, school ids, owner emails, Access audiences, upstream URLs — tất cả qua vars/secrets trong wrangler config (packages/shared/config là dự định, chưa dựng — tính tới 2026-08-20 mọi cấu hình nằm trong wrangler.jsonc của từng worker và được cổng verify-bindings.mjs đối chiếu với reference/config.md). (Fix RISK-013/016/023 — hai legacy hardcode domain ở 45+ và ~30 files.)

12. Hai lỗi im lặng, và cái gác cho lớp lỗi thứ hai (SRC-887, 2026-09-20) ​

Hai lỗi này được sửa một lần ở nhánh session/code-hygiene ngày 10.09 nhưng nhánh ấy kẹt lại vì SRC-698 bị cấp trùng, nên bản vá chưa bao giờ lên production. Mười ngày sau, cả hai vẫn còn.

Lỗi 1 — chuỗi escape in nguyên văn. apps/mentors/src/FamilyWorkspace.tsx viết hint='e.g. "Minh\u2019s family"'. Trong chuỗi nháy đơn của một THUỘC TÍNH JSX, \u2019 không phải escape mà là bốn ký tự thường, nên mentor đọc đúng chữ Minh\u2019s family trên màn hình. Sửa bằng cách bọc thành biểu thức JS: hint={'e.g. "Minh\u2019s family"'}. Không thay bằng ký tự ’ viết thẳng, vì cổng SRC-379 chỉ cho chữ người dùng đọc dùng ký tự có trên bàn phím.

Lỗi 2 — nút tab hiện rỗng. apps/coral/src/App.tsx khai TABS = [{ id: "lab", key: "labs" }] nhưng bảng nhãn T không có khoá labs ở cả hai thứ tiếng. Tra một khoá không tồn tại trả về undefined, và React vẽ undefined thành rỗng — nút trống trơn, không lỗi, không log.

Vì sao TypeScript không cứu được, và cái gác mới. Kiểu của bảng là Record<string, string> nên mọi chuỗi đều hợp lệ về mặt kiểu; không cổng nào có thể đỏ. Nay có scripts/check-ui-label-keys.mjs trong npm run check:code: nó đối chiếu mọi khoá mà TABS tra với các bản đồ ngôn ngữ trong T, và đỏ khi thiếu.

Cổng đặt ở scripts/ chứ không phải unit test vì apps/coral không có bộ test nào, và dựng cả một bộ chạy thử chỉ để so hai danh sách chuỗi thì đắt hơn thứ nó bảo vệ. Phạm vi cố ý hẹp — chỉ những file khai đúng hình dạng ấy, hiện chỉ có Coral; app nào chép lại hình dạng thì thêm một dòng vào TARGETS.

Điều đáng rút ra. Cả hai lỗi đều thuộc loại HỎNG IM LẶNG: màn hình vẫn vẽ, không có ngoại lệ nào, và chỉ người nhìn vào đúng chỗ mới thấy. Chúng sống mười ngày sau khi đã có bản vá. Bài học không phải "viết cẩn thận hơn" mà là: lỗi nào không tự báo thì phải có một phép đối chiếu bằng máy, và phép ấy phải được kiểm là có đỏ thật — bản vá này được xác nhận bằng cách gỡ ra cho cổng đỏ rồi lắp lại cho cổng xanh.

13. Hai lỗi im lặng nữa, từ audit theo khía cạnh (SRC-1222, 03.10.2026) ​

  • Deploy hỏng mà CI xanh. .github/scripts/deploy.sh đọc rc=$? SAU if wrangler deploy ...; then exit 0; fi. Không nhánh nào chạy thì $? của cả câu if là 0, nên sau ba lần hỏng script thoát 0. Nay rc lấy ở nhánh else; kiểm bằng một wrangler giả luôn thoát 7, script thoát 7. Rà 25 lượt ci xanh gần nhất trên main: chưa lượt nào dính.
  • Khuôn schema của test bị chính lớp dọn rác xoá. runlog.testkit.ts quét file nemo12-schema-* cũ hơn 6 giờ, mà khuôn đặt tên theo nội dung nên dùng mãi một file và mtime không đổi: lượt chạy đầu tiên sau 6 giờ xoá khuôn giữa lúc worker khác đang copy (ENOENT, "no such table"), vài chục ca đỏ chập chờn bị đọc nhầm là "máy chậm". Nay mỗi process chạm mtime khuôn trước khi quét, và copy hỏng vì ENOENT thì dựng lại khuôn một lần.

Cùng đợt: 30 lỗi khác ở chi phí AI (mọi lượt gọi của learner, kể cả Whisper/TTS và lượt hỏng, tính vào trần USD ngày; trần foundry đóng khi không kiểm được), timeout gọi dịch vụ ngoài, dialog bàn phím (Escape, giữ focus), ngày DD.MM.YYYY, escape HTML trang huỷ đăng ký, ghi theo lô. Còn mở: trần USD foundry chưa tính chi phí lần thử lại của workflow (cần cột mới).

Trace ​

REQMục
REQ-PLT-01..05,07,08,10,11§1–§5, §7, §9, §10
REQ-ACC-01..06§6
REQ-BRD-01/02§4
REQ-BRD-12§10
REQ-SCH-01§4
REQ-SEC-05/06, REQ-MEN-01§6, §11