SDD-045 - Journey Management & Activation Engine
Yêu cầu: PRD-005. Nguồn: SRC-1077 (SDD mẫu của chủ dự án, 27.09.2026). Chỗ nào bản này khác SDD mẫu thì ghi rõ lý do và mã quyết định.
1. Hình dạng tổng thể
registry.ts (Journey Matrix, policies)
|
facts.ts -----> engine.ts (thuần) <----- learner_journey_states (ảnh chụp lần trước)
(Learner State) |
v
portfolio + foreground + next action
|
service.ts: ghi ảnh chụp + journey_events (idempotent)
|
routes.ts: learner view | admin trace
|
learn /ielts (JourneyNextUp) admin #/journeysMã: workers/api/src/modules/journeys/ (rules.ts, registry.ts, facts.ts, engine.ts, service.ts, routes.ts). Giao diện: apps/learn/src/JourneyNextUp.tsx, apps/admin/src/pages/Journeys.tsx. Dữ liệu: migration 0301_journey_engine_state_and_events.sql.
2. Bốn mối quan tâm tách nhau
| Mối quan tâm | Câu hỏi | Chỗ |
|---|---|---|
| Journey Definition | Hành trình này là gì? | registry.ts |
| Journey State | Learner có đang ở trong nó không? | engine.ts#deriveState |
| Priority | Nó quan trọng tới đâu lúc này? | engine.ts#scoreJourney + policy |
| Next Best Action | Learner nên làm gì? | engine.ts#pickAction + catalog |
Engine là hàm THUẦN: không chạm D1, không đọc đồng hồ (nhận now). Mọi kịch bản của §13 chạy không cần cơ sở dữ liệu, và test không đỏ theo lịch.
3. Trạng thái suy ra, không ghi làm nguồn (✍️ Q-159)
SDD mẫu lưu journey_instances làm nguồn sự thật. Bản này suy ra trạng thái từ dữ liệu học mỗi lần đánh giá, cùng lý do ielts/journey.ts cũ từ chối lưu "đang ở bước mấy": một cột trạng thái sẽ lệch khỏi sự thật ngay lần đầu learner xoá mục tiêu. D1 chỉ giữ ba thứ không suy ra được: ảnh chụp lần trước (để biết lần này có ĐỔI không, và hành trình nào đang đứng trước), mốc snoozed_until, và sổ sự kiện.
Luật suy trạng thái (deriveState), theo thứ tự:
- Luật xong đúng:
lifecyclethì COMPLETED;episodicchỉ COMPLETED nếu lần trước đã ở trong đợt (ELIGIBLE/ACTIVE/PAUSED/COMPLETED/EXPIRED), nếu không thì NOT_ELIGIBLE. Learner chưa từng nghỉ thì Recovery là NOT_ELIGIBLE, không phải COMPLETED. - Luật hết hạn đúng: EXPIRED nếu đã ở trong đợt, nếu không thì NOT_ELIGIBLE.
- Không đủ điều kiện: NOT_ELIGIBLE.
snoozed_untilcòn hạn: PAUSED.- Luật kích hoạt đúng: ACTIVE, không thì ELIGIBLE.
recurring (Daily Learning) không có luật xong (registry test chặn).
4. Journey Registry (✍️ Q-160)
Định nghĩa sống trong registry.ts, sửa bằng PR, không bằng màn admin: đổi một luật là đổi hành vi của mọi learner, và thay đổi ấy cần review, test và một commit nói vì sao. Registry là DỮ LIỆU (luật khai báo, không phải hàm), nên ngày chuyển vào D1 chỉ là đổi chỗ đọc.
JourneyDefinition {
id, version, name, purpose, horizon, owner, status, kind, // lifecycle | episodic | recurring
eligibility?, activation?, completion?, expiration?, // Rule
factors: { urgency, goal_relevance, need, impact, readiness }, // Factor
policy, actions: JourneyAction[], success_metrics
}validateRegistry() chặn ở test: id trùng, policy không tồn tại, catalog rỗng, gợi ý cuối có điều kiện (phải có gợi ý mặc định), hành trình recurring có luật xong, trọng số policy không cộng bằng 1, và em dash trong chữ gợi ý. Đổi luật thì tăng version: sổ ghi phiên bản đã dùng.
5. Luật và dữ kiện
Luật (rules.ts): { all: [...] } hoặc { any: [...] }, lồng được; mỗi điều kiện { fact, op, value } với op trong == != < <= > >=. Kết quả luôn kèm vết từng điều kiện (fact, op, expected, actual, pass) - chính thứ trang admin bày. Dữ kiện null (chưa biết) chỉ khớp == null: "chưa có ngày thi" không được lọt qua days_remaining <= 30.
Nhân tố: hằng số, hoặc bậc thang trên một dữ kiện (steps: [[op, ngưỡng, giá trị]], bậc đầu khớp thắng, else nếu không bậc nào). Boolean đọc như 1/0.
Learner State Provider (facts.ts) - một chỗ đọc duy nhất; mỗi truy vấn bọc try/catch trả "chưa có" (code có thể lên trước migration):
| Dữ kiện | Nguồn |
|---|---|
goal.exists, goal.target_date, goal.days_remaining, goal.overall | learner_ielts_goals active. Ngày mục tiêu dùng làm ngày thi (✍️ Q-162) |
profile.answered | learner_ielts_profile_facts + learner_ielts_declarations |
diagnosis.exam_quiz, diagnosis.measured | ielts_exam_quiz_attempts; assessment_sessions diagnostic completed hoặc learner_ielts_baselines |
plan.milestones, progress.milestone_due_days_ago | learner_ielts_milestones của mục tiêu đang dùng |
progress.reviewed, goal.reached_reviewed | journey_events kind action_accepted sau ngày mốc / sau lần sửa mục tiêu |
activity.ever, activity.inactive_days, activity.today | learning_events, chỉ các action practice, review, submit, read (✍️ Q-163) tính theo ngày học (04:00 VN) |
capability.measured_overall, capability.reached | ảnh chụp Learner Model IELTS mới nhất (readIeltsModel), cần đủ bốn kỹ năng |
focus.skill, focus.gap, focus.why | kỹ năng xa mục tiêu nhất; chưa đo thì kỹ năng ít luyện nhất 14 ngày |
6. Ưu tiên
Policy: trọng số năm nhân tố (cộng bằng 1), min_score, eligible_multiplier (0.6).
score = Σ weight_f × factor_f × (ACTIVE ? 1 : eligible_multiplier)Hành trình không ACTIVE/ELIGIBLE có điểm 0. Năm policy MVP: setup_v1, daily_v1, recovery_v1, exam_urgency_v1, goal_progress_v1 (ngưỡng 0.75, để Goal Achievement chưa đạt không bao giờ đứng trước). Trọng số là phỏng đoán có lý do ghi trong mã, đo lại sau bốn tuần (Q-172).
Suppression được diễn bằng nhân tố thay vì một luật riêng: exam_preparation.readiness rơi về 0.3 khi learner nghỉ >= 7 ngày, nên Recovery thắng (PRD-005 §8).
7. Dữ liệu (migration 0301)
| Bảng | Vai trò |
|---|---|
learner_journey_states (PK learner_id, journey_id) | Ảnh chụp lần đánh giá gần nhất: state, definition_version, priority_score, is_foreground, action_id, snoozed_until, evaluated_at |
journey_events | Sổ chỉ chèn: transition (from, to), foreground, action_accepted, snoozed; kèm definition_version, reasons_json |
SDD mẫu liệt kê mười một bảng (definitions, versions, stages, rules, policies...). Chín bảng không có vì registry nằm trong mã (§4) và trạng thái suy ra (§3).
8. Hành trình đứng trước
- Ứng viên = ACTIVE hoặc ELIGIBLE, không PAUSED, điểm >=
min_scorecủa policy. - Xếp: điểm, rồi
urgency,goal_relevance,impact, rồi thứ tự trong registry. - Giữ mạch: hành trình đang đứng trước (ảnh chụp) ở lại nếu kém đứng đầu <= 0.05.
- Không ứng viên nào: không có thẻ (learner vẫn còn bốn lối vào tự chọn trên trang chủ).
Mỗi hành trình không phải ứng viên mang not_candidate_reason (đang hoãn, trạng thái, điểm dưới ngưỡng); hành trình thắng mang foreground_reason.
9. API
| Đường | Ai | Làm gì |
|---|---|---|
GET /v1/learners/{id}/journeys | canAccessLearner | Đánh giá, ghi sổ, trả bản learner: foreground, next_action, journeys[] {id, name, horizon, state}. Không có điểm, nhân tố |
POST /v1/learners/{id}/journeys/re-evaluate | như trên | Đánh giá lại tường minh (FR8) |
POST /v1/learners/{id}/journeys/{journeyId}/snooze | như trên | "Để sau" 24 giờ; 409 nếu hành trình không ACTIVE/ELIGIBLE |
POST /v1/learners/{id}/journeys/{journeyId}/actions/{actionId}/accept | như trên | Ghi learner đã bấm; 404 nếu action không có trong catalog |
GET /v1/admin/journeys | admin | Registry, policy, lỗi cấu hình, số learner theo (hành trình, trạng thái) |
GET /v1/admin/learners/{id}/journeys | admin | Vết đầy đủ: facts, ảnh chụp trước, từng journey (luật, nhân tố, điểm, lý do), sổ sự kiện. Không ghi gì |
Endpoint cũ GET /v1/learners/{id}/ielts-journey (năm bước) đã bỏ cùng ielts/journey.ts.
10. Đánh giá đồng bộ, ghi idempotent (✍️ Q-161)
SDD mẫu vẽ Event Router + Cloudflare Workflow + cache. MVP không cần: trạng thái suy ra từ dữ liệu học, nên mọi sự kiện domain (nộp bài, đổi mục tiêu, ngày tới gần) đã nằm trong dữ liệu facts.ts đọc; đánh giá lúc learner mở trang luôn đúng với dữ liệu mới nhất, không có cảnh "sự kiện chưa tới". Chín truy vấn nhỏ, không cần cache.
Đồng thời: mỗi dòng ảnh chụp đổi bằng UPDATE ... WHERE state = <đã đọc> AND is_foreground = <đã đọc> (optimistic concurrency); dòng transition/foreground chỉ chèn khi chính câu ấy đổi được dòng. Hai tab mở cùng lúc thì một lần chuyển, một dòng sổ.
Hỏng: ghi sổ lỗi thì nuốt, gợi ý vẫn trả về. Phía learn, lỗi đọc thì thẻ im lặng biến mất, không hiện "Journey Engine failed" (SDD mẫu §23).
Khi nào cần Workflows: hành trình phải CHỦ ĐỘNG đi ra ngoài (thư nhắc khi Recovery bật) - việc ấy cần một lịch chạy, không cần learner mở trang.
11. Giao diện
Learn - JourneyNextUp ở đầu cụm "What to do now" trên trang chủ /ielts (✍️ Q-164), và thay JourneyStrip năm bước trên trang "Con và IELTS" (/squid/ielts-profile/ielts, đọc lại sau khi nộp quiz). Thẻ: chấm cam, nhãn "Next up", tên hành trình (tiếng Anh), câu gợi ý và câu vì sao (tiếng Việt, do server viết), nút "Làm ngay" (ghi accept với keepalive rồi chuyển trang) và "Để sau" (hoãn rồi vẽ lại với hành trình kế tiếp). Chữ theo SRC-953 (✍️ Q-165).
Admin - tab Journeys (#/journeys): bảng registry kèm số learner mỗi trạng thái, bảng policy, ô tra learner. #/journeys/<learnerId>: việc tiếp theo và vì sao hành trình ấy đứng trước; từng hành trình theo thứ hạng với luật đủ điều kiện / kích hoạt / xong / hết hạn (mỗi điều kiện ✓ ✗ kèm giá trị thật), nhân tố, điểm, lý do không phải ứng viên; bảng dữ kiện; sổ sự kiện. Nút "Journeys →" trên mỗi dòng của tab Learners.
Trang công khai - năm bước thay bằng tám hành trình ở nemo12.com/ielts/journey, /{students|parents}/university/ielts/journey, khối trên trang môn IELTS của học sinh, và mọi nhãn liên kết (apps/web, dữ liệu chung ở ieltsJourneySteps.ts). URL giữ nguyên. SAT giữ năm bước (journeyLabel mặc định).
Bàn mentor (SDD-037 desk/steps.ts) giữ năm bước theo lô cho tới Q-169.
12. Bảo mật và riêng tư
Đường learner qua canAccessLearner (401 cho nhà khác, không lộ learner có tồn tại). Đường vết chỉ admin. Bản learner không có điểm, nhân tố hay dữ kiện thô. Registry chỉ đọc qua API; đổi luật là commit có review. Phụ huynh chưa có đường (Q-168).
13. Kiểm chứng
| Tầng | File | Gác |
|---|---|---|
| Engine thuần | journeys/engine.test.ts (27 ca) | Registry hợp lệ; luật và null; kịch bản learner mới, học đều, tới mốc, quay lại (3/7 ngày, xong khi học lại, chưa từng nghỉ), sắp thi (60/20 ngày, hết hạn), ba hành trình giành nhau, giữ mạch trong/ngoài biên, "Để sau" và hết hạn hoãn |
| API + D1 thật | journeys/routes.test.ts (19 ca) | Quyền (401, admin); bản learner không lộ điểm; dữ kiện thật đổi đúng hành trình; sổ không ghi hai lần; "Để sau"; 409; accept đóng Learning Progress; 404 action lạ; vết admin không ghi sổ |
| E2E learn | e2e/smoke.spec.ts | Thẻ một việc, "Để sau" nhường chỗ, thẻ đứng giữa "What to do now" và "Practise a skill" |
14. Cố ý không làm (MVP)
- Registry trong D1 và màn sửa luật (Q-160) · Event Router, Workflows, cache (Q-161) · bảng
journey_stages(mỗi hành trình MVP có ít bước, catalog có điều kiện là đủ) · AI chọn hành trình · phễu phân tích theo thời gian (REQ-JRN-16).
15. Trace
| REQ | Mục |
|---|---|
| REQ-JRN-01 | §4 |
| REQ-JRN-02 | §4, §5 |
| REQ-JRN-03 | §5 |
| REQ-JRN-04 | §3, §7 |
| REQ-JRN-05 | §6 |
| REQ-JRN-06 | §8 |
| REQ-JRN-07 | §8, §11 |
| REQ-JRN-08 | §9, §10 |
| REQ-JRN-09 | §7, §9 |
| REQ-JRN-10 | §9, §11 |
| REQ-JRN-11 | §8, §9, §11 |
| REQ-JRN-12 | §4 (PRD-005 §6) |
| REQ-JRN-13 | §11 |
| REQ-JRN-14 | §9, §11 |
| REQ-JRN-15 | §12 |
| REQ-JRN-16 | §14 |