SDD-025 — Anchor
Never lose the commitment. Thiết kế kỹ thuật cho PRD-003 phần Anchor.
Nếu Dory là trí nhớ, Anchor là lương tâm: nó giữ những thứ Nemo12 đã hứa mà chưa làm, và không cho chúng biến mất một cách êm ái.
1. Nguyên tắc
- Một lời hứa không có chủ và hạn thì không lưu được.
owner_user_idvàdue_atlàNOT NULLtrêncommitments. Cho phép để trống nghĩa là phần lớn sẽ trống, và một danh sách lời hứa không hạn chính là danh sách những thứ sẽ không xảy ra. - Đóng case cần bằng chứng, không cần một cú bấm.
resolution_notebắt buộc; muốn đóng mà chưa có thì phải nói rõ đang đóng bằng ngoại lệ (§6). - Xong chưa chắc là xong. Mỗi resolution tự đặt một lần follow-up (§7). Một vấn đề đóng hôm nay mà hai tuần sau quay lại thì lần đóng đó đã sai.
- Escalation cứu việc, không bắt lỗi người. Không có bảng xếp hạng Dolphin nào sinh ra từ dữ liệu này (PRD-003 §8).
- Danh sách ngắn mới có tác dụng. Anchor cố ý không nhận mọi ý nghĩ thoáng qua — thứ đó thuộc về ghi chú của Dory. Vào Anchor là thành nghĩa vụ.
2. Dữ liệu
cases
id · family_id → families · learner_id? → learners
title · description?
category(academic|logistics|billing|relationship|technical|other)
priority(low|normal|high|urgent)
status(open|in_progress|waiting_family|resolved|closed|reopened)
opened_by → users · owner_user_id → users
source_interaction_id? → family_interactions -- sinh ra từ một cuộc gọi
due_at? -- SLA tính ra, sửa tay được
resolved_at? · resolution_note? · resolution_evidence_json?
closed_at? · reopened_count
created_at · updated_at
commitments -- "chúng tôi sẽ..."
id · case_id? → cases · family_id → families
statement -- nguyên văn điều đã hứa
owner_user_id NOT NULL → users
due_at NOT NULL
made_at · made_to_contact_id? → family_contacts
source_interaction_id? → family_interactions
status(open|kept|missed|cancelled)
kept_at? · evidence_note?
created_at · updated_at
case_actions -- việc cụ thể để xong một case
id · case_id → cases · title · assignee_user_id? → users
due_at? · status(todo|doing|done|dropped) · done_at? · created_at
case_followups -- kiểm lại sau khi đóng
id · case_id → cases · due_at · question
status(pending|confirmed_ok|reopened) · checked_by? · checked_at? · note?
case_events -- vết xử lý, chỉ nối thêm
id · case_id · actor_user_id · event_type · from_status? · to_status?
detail? · created_atcommitments tách khỏi cases chứ không phải một cột trong đó. Lý do: rất nhiều lời hứa không có case nào cả — "em gửi anh lịch thi thử trước thứ Sáu" không phải một vấn đề, nó là một việc. Bắt phải mở case trước mới ghi được lời hứa là dựng một thủ tục khiến người ta thôi ghi.
statement lưu nguyên văn điều đã hứa, không phải bản tóm tắt. Khi cãi nhau về việc đã hứa gì, bản tóm tắt là thứ vô dụng nhất.
3. Vòng đời Case (REQ-ANC-01)
open → in_progress → waiting_family ⇄ in_progress → resolved → closed
↓
reopened → in_progresswaiting_family là trạng thái riêng chứ không gộp vào in_progress, vì đồng hồ SLA dừng ở đó (§5). Không tách thì mọi case chờ phụ huynh trả lời đều trở thành case trễ, và bảng cảnh báo đầy thứ không ai làm gì được — sau vài tuần thì không ai nhìn bảng đó nữa.
reopened_count để trần: một case mở lại ba lần là dấu hiệu resolution lần đầu chưa bao giờ đúng, và đó là thông tin đáng nhìn hơn con số "đã đóng bao nhiêu case".
4. Commitment (REQ-ANC-02)
Ba đường sinh ra một commitment, xếp theo mức hay dùng:
- Từ Interaction Capture của Dory — vừa gọi xong, bấm "tôi vừa hứa gì đó".
- Từ trong một Case.
- Tạo thẳng ở màn gia đình.
status chỉ có bốn giá trị, và missed là một trạng thái thật, không phải lỗi. Một hệ thống không cho phép ghi nhận "đã lỡ hẹn" sẽ được dùng bằng cách im lặng đổi due_at — và khi đó mọi số liệu về độ tin cậy đều là số giả.
5. SLA (REQ-ANC-04)
Hạn tính theo category × priority, ghi trong config chứ không rải trong mã:
| category | urgent | high | normal | low |
|---|---|---|---|---|
| billing | 4h | 1 ngày | 2 ngày | 5 ngày |
| academic | 1 ngày | 2 ngày | 5 ngày | 10 ngày |
| relationship | 1 ngày | 2 ngày | 3 ngày | 7 ngày |
| logistics · technical · other | 1 ngày | 3 ngày | 5 ngày | 10 ngày |
- Đồng hồ dừng khi
waiting_family, chạy lại khi vềin_progress. - Cảnh báo trước khi trễ, ở mốc 80% thời gian — cảnh báo sau khi trễ chỉ là đưa tin buồn.
due_atsửa tay được, nhưng mỗi lần sửa ghi một dòngcase_events. Gia hạn im lặng là cách một hệ thống theo dõi hạn tự vô hiệu hoá chính nó.
6. Resolution Evidence (REQ-ANC-07)
Chuyển sang resolved đòi:
resolution_notekhông rỗng, và- ít nhất một trong: một
case_actionđãdone, mộtcommitmentđãkept, hoặc mộtfamily_interactiongắn với case này.
Thiếu thì API trả 409 kèm câu nói rõ còn thiếu gì — cùng khuôn publish_blockers đã dùng ở SDD-019 §4 và SDD-020 §4. Khuôn này đã chứng minh được: người dùng biết trước phải làm gì thay vì bấm rồi mới nhận lỗi.
Có một đường đóng ngoại lệ: closed với resolution_note bắt đầu bằng [no-evidence]. Nó tồn tại vì thực tế có case chết già (gia đình ngừng học, vấn đề tự hết) và một cổng không có lối thoát nào là một cổng người ta sẽ tìm cách lách. Nhưng những case đó hiện thành một mục riêng trong Commitment Model (closed_without_evidence) — hệ thống tự thú nhận chỗ nó đang bị dùng sai.
7. Follow-up (REQ-ANC-05)
Mỗi lần resolved, hệ tự tạo một case_followup với due_at = +14 ngày (billing +7). question sinh từ tiêu đề case: "Two weeks on, is [title] genuinely settled?"
Đến hạn, follow-up hiện trong danh sách việc của owner. Ba kết cục: confirmed_ok · reopened · gia hạn một lần. Không có nút bỏ qua — bỏ qua chính là thứ follow-up sinh ra để chặn.
8. Escalation (REQ-ANC-06)
| Điều kiện | Hành động |
|---|---|
Case quá due_at 24h | gắn cờ at_risk, hiện đầu danh sách của owner |
Case quá due_at 72h | thêm vào bảng của trưởng nhóm |
| Commitment quá hạn | đánh dấu missed, sinh signal cho Dory (nhà này ta đã lỡ hẹn) |
Case urgent chưa ai nhận sau 4h | vào bảng trưởng nhóm ngay |
Chạy trong cron chung với Family Signals §5. Mỗi lần escalate ghi case_events — có vết thì mới cãi lại được, kể cả cãi rằng luật escalation đang quá nhạy.
9. Audit (REQ-ANC-08)
case_events là vết nghiệp vụ (đọc được trên màn hình, kể lại câu chuyện của một case). audit_log là vết truy cập (ai xem dữ liệu gia đình nào lúc nào, AS-07.4). Hai thứ khác nhau và cùng phải có; gộp lại thì một trong hai sẽ bị cắt xén cho vừa cái kia.
case_events chỉ nối thêm, không có đường sửa hay xoá trong ứng dụng — cùng luật đã áp cho audit_log.
10. API
| Method | Path | Quyền |
|---|---|---|
| GET · POST | /v1/mentor/cases | 🔐 role mentor |
| GET · PATCH | /v1/mentor/cases/{id} | 🔐 role mentor |
| POST | /v1/mentor/cases/{id}/resolve | 🔐 role mentor |
| POST | /v1/mentor/cases/{id}/reopen | 🔐 role mentor |
| POST | /v1/mentor/cases/{id}/actions · PATCH /v1/mentor/actions/{id} | 🔐 role mentor |
| GET · POST | /v1/mentor/commitments | 🔐 role mentor |
| PATCH | /v1/mentor/commitments/{id} | 🔐 role mentor |
| GET | /v1/mentor/my-work | 🔐 role mentor |
| POST | /v1/mentor/followups/{id}/check | 🔐 role mentor |
| GET | /v1/mentor/escalations | 🔐 role mentor |
/v1/mentor/my-work là màn hình mở đầu ca trực: commitments đến hạn · cases at-risk · follow-ups tới hạn, gộp một truy vấn.
11. Cái chưa làm
| Việc | Vì sao |
|---|---|
| Tự nhắn cho phụ huynh khi commitment tới hạn | Anchor nhắc Dolphin, không thay Dolphin nói chuyện (PRD-003 §8) |
| Bảng xếp hạng Dolphin theo tỉ lệ giữ lời | Escalation để cứu việc, không để bắt lỗi người |
| SLA theo lịch làm việc (trừ cuối tuần) | Đợt đầu tính theo giờ trôi; thêm lịch làm việc là một lớp phức tạp đáng làm sau khi biết dữ liệu thật trông thế nào |
| Case template theo loại vấn đề | Chờ đủ dữ liệu thật để biết loại nào lặp lại |
Trace
REQ-ANC-01→§2-3 · REQ-ANC-02→§4 · REQ-ANC-03→§2 · REQ-ANC-04→§5 · REQ-ANC-05→§7 · REQ-ANC-06→§8 · REQ-ANC-07→§6 · REQ-ANC-08→§9.