---
url: https://docs.nemo12.com/architecture/sdd-047-gull.md
description: >-
  Bản nháp Gull (SDD-047): hiểu khách hàng và journey intelligence, ba ràng
  buộc, kiến trúc và mô hình dữ liệu migration 0306.
---

# SDD-047 - Gull, Customer Understanding & Journey Intelligence

> Thiết kế cho [PRD-007](../product/prd-007-gull.md). Nguồn: SRC-1094. Mọi quyết định dưới đây
> dựa trên Đợt 16 (Q-183..Q-192) trong [open-questions](../open-questions/index.md), chủ dự án chốt
> 27.09.2026.

## 1. Phạm vi và ba ràng buộc

| Ràng buộc | Nguồn | Hệ quả thiết kế |
| --- | --- | --- |
| Công khai, không đăng nhập | Q-184 | Route API nằm dưới `/v1/public/gull/*`, khai trong `authCoverage.test.ts`; không cookie, không phiên |
| Không có tính năng sửa; chủ dự án sửa DB bằng script | Q-187 | API chỉ có GET. Đường ghi duy nhất là `scripts/gull-build-seed.mjs` + workflow `seed-data.yml` (§5) |
| UI bê nguyên từ `learn.nemo12.com/ielts` | Q-192 | Nền `.n12-deep` chuyển sang `packages/design-system/deep.css`, learn và Gull cùng import; `NavBar`/`PageBody` có canonical ở `packages/design-system/ui/DeepChrome.tsx` (§6) |

Dữ liệu là archetype và bằng chứng nghiên cứu, **không có dữ liệu cá nhân của learner** (Q-191):
MVP không đọc bảng nào ngoài `gull_*`.

## 2. Kiến trúc

```text
content/gull/knowledge.json ──(scripts/gull-build-seed.mjs: kiểm luật)──▶ scripts/seed-gull.sql
                                                                          │ workflow seed-data
                                                                          ▼
gull.nemo12.com (apps/gull, SPA tĩnh) ──GET──▶ api.nemo12.com/v1/public/gull/* ──▶ D1 nemo12-platform, bảng gull_*
```

* **App** `apps/gull` (worker `nemo12-gull`, custom domain `gull.nemo12.com`): React + Tailwind v4
  * shadcn + Motion, đúng bộ ba SRC-650. Không có worker script, chỉ static assets.
* **API** `workers/api/src/modules/gull/routes.ts`: hai route GET.
* **D1**: dùng chung `nemo12-platform`, bảng tiền tố `gull_` (Q-190), migration `0306`.

## 3. Mô hình dữ liệu (migration 0306)

### 3.1 Bốn bảng

| Bảng | Vai trò |
| --- | --- |
| `gull_entities` | Mọi thực thể của 12 loại (`kind`). Trường chung là cột; trường riêng từng loại là JSON `fields` |
| `gull_links` | Quan hệ nhiều-nhiều `(from_id, to_id, rel, program_id)`, có `position` cho thứ tự và `note` |
| `gull_program_context` | Phần khác biệt của một Global Entity trong một Program (PRD-007 §4.3) |
| `gull_history` | Bản cũ của thực thể, do trigger ghi trước mỗi UPDATE (chỉ khi nội dung đổi) và DELETE |

Vì sao một bảng cho 12 loại: chúng chung toàn bộ trường quản trị (status, owner, version,
confidence, generated_by, human_verified_at…), và giá trị của Gull nằm ở quan hệ giữa các loại.
Mười hai bảng là mười hai bộ cột quản trị phải giữ khớp nhau và mười hai lần JOIN để đi một mạch
Persona → Evidence.

### 3.2 Quan hệ (`rel`)

| rel | Chiều |
| --- | --- |
| `has_stakeholder`, `has_persona`, `in_program` | program ↔ stakeholder ↔ persona |
| `has_job`, `has_need`, `has_pain` | persona/jtbd/stage/step → jtbd/need/pain |
| `for_persona`, `for_stakeholder`, `in_program` | journey → persona/stakeholder/program |
| `contains` (có `position`) | journey → stage → step |
| `at_touchpoint` | step → touchpoint |
| `supports`, `contradicts` | evidence → bất kỳ |
| `leads_to` | pain/need → opportunity |

Client đọc quan hệ **hai chiều**, nên người ghi nối theo chiều nào cũng được; chỉ `contains` và
`supports`/`contradicts` có nghĩa theo chiều.

### 3.3 Trạng thái (Q-189)

Một trục: `hypothesis → draft → evidence_backed → validated → deprecated`. "Reviewed" không phải
trạng thái mà là cột `reviewer` + dòng lịch sử. Không xoá cứng: thực thể biến khỏi JSON thì seed
chuyển nó sang `deprecated`.

## 4. Phép nhìn trên đồ thị (client)

`GET /v1/public/gull/graph` trả **cả đồ thị** (thực thể, quan hệ, context) trong một lượt, cache 60
giây. Kho vài trăm dòng nhỏ hơn một bài đọc IELTS; mọi màn là hàm thuần trong
`apps/gull/src/graph.ts`, test bằng `graph.test.ts` trên chính dữ liệu seed.

| Hàm | Màn | Định nghĩa |
| --- | --- | --- |
| `journeyTree` | Journey Detail (F4) | stage theo `position`, step theo `position`, kèm pain/need/touchpoint/evidence và opportunity qua pain |
| `journeyMatrix` | Journey Matrix (F3) | hàng = journey; cột = TÊN chặng (chuẩn hoá chữ thường) nên chặng cùng tên thẳng cột; thứ tự cột = vị trí tương đối trung bình |
| `programs(id)` | mọi màn | chương trình trực tiếp, qua context, qua `program_id` của quan hệ, và kế thừa NGƯỢC về phía chủ (step → stage → journey → program). Không đi xuôi từ Evidence |
| `northStar` | Dashboard | tri thức quan trọng (cờ `important` hoặc persona/jtbd/pain/journey) đồng thời có cấu trúc, có nối, còn mới (≤ 180 ngày) và có Evidence `supports`, không phải hypothesis |
| `painConcentration` | Dashboard (F12) | step xếp theo tổng `fields.severity` của pain gắn vào |
| `researchGaps` | Dashboard (F12) | xương sống chưa có Evidence; stage rỗng; persona chưa có JTBD |
| `crossProgram` | Dashboard (F12) | pain/jtbd/need thuộc ≥ 2 chương trình |
| `search` | Library (F10) | theo từ, bỏ dấu tiếng Việt, mọi từ phải có; khớp tên nặng gấp ba khớp thân |

## 5. Đường sửa dữ liệu (Q-187)

```bash
# 1. sửa content/gull/knowledge.json
node scripts/gull-build-seed.mjs          # kiểm luật, ghi scripts/seed-gull.sql
# 2. commit cả hai file, push, gộp vào main
gh workflow run seed-data.yml -f file=scripts/seed-gull.sql \
  -f verify="SELECT kind, COUNT(*) FROM gull_entities GROUP BY kind"
```

Script chặn (và `npm run check:code` chạy `--check` nên CI cũng chặn):

1. Pain `important` không phải hypothesis mà không có Evidence `supports` (PRD-007 §4.4).
2. `evidence_backed` mà không có Evidence `supports`.
3. Tri thức `generated_by: "ai:*"` ở `validated` mà chưa có `human_verified_at` (§19 của mô tả gốc).
4. id không phải slug tiếng Anh, quan hệ trỏ vào id không có, em dash / en dash.
5. `scripts/seed-gull.sql` lệch `knowledge.json` (quên chạy lại script).

SQL sinh ra idempotent: `ON CONFLICT DO UPDATE` chỉ tăng `version` khi nội dung đổi thật, nên chạy
lại cùng dữ liệu không đẻ phiên bản hay lịch sử. Quan hệ và context dựng lại trọn mỗi lượt.

## 6. Giao diện

* Nhãn giao diện **tiếng Anh**, nội dung người nhập giữ nguyên thứ tiếng của nó (Q-188; ngoại lệ thứ
  tư trong CLAUDE.md).
* Khung: `NavBar` + `PageBody` (bản sao y hệt `packages/design-system/ui/DeepChrome.tsx`, cũng là
  `apps/learn/src/ielts/Chrome.tsx`), vệt đường dẫn bắt đầu bằng `GULL` viết đậm như `NEMO IELTS`,
  thẻ lớn kiểu `HubTile`, nhãn tròn chữ hoa, `Card` shadcn, preset Motion `Stagger`/`Liftable`.
* Năm dạng URL: `/` (Dashboard), `/library[/<kind>][?q=]`, `/e/<id>`, `/matrix`. Journey, Persona,
  JTBD, Evidence đều là `/e/<id>`; khung chung, thứ tự khối quan hệ theo loại.
* Khối **"Why do we believe this?"** đứng ngay sau mô tả ở mọi thực thể; hypothesis có ghi chú
  viền đứt "Treat it as a question, not a fact".
* Change history mở theo yêu cầu (`GET /v1/public/gull/entities/:id/history`).

### 6.1 Journey theo nhóm người dùng (SRC-1095)

Chủ dự án 27.09.2026: cần một chỗ liệt kê các journey chính của một nhóm người dùng, và trang chi
tiết từng journey có tổng quan rồi các thành phần, **mỗi thành phần một card, mỗi card một dòng**.

* Trang `/journeys` gom journey theo cặp (chương trình, stakeholder): IELTS + Student là **IELTS
  Learners**. Journey khai `fields.tier` = `primary` (thẻ to, "Main") hoặc `secondary` (thẻ nhỏ,
  "Supporting"); không khai thì vào "Other journeys". Hàm `journeySegments`, `journeyStats`.

* IELTS Learners có đúng 5 journey, `graph.test.ts` giữ con số này:

  | Hạng | Journey | id |
  | --- | --- | --- |
  | Chính | IELTS Test Taker Journey | `journey-ielts-preparation` (giữ id cũ để link không gãy) |
  | Chính | Learning Session Journey | `journey-ielts-learning-session` |
  | Phụ | Onboarding Journey | `journey-ielts-onboarding` |
  | Phụ | Diagnosis & Re-assessment Journey | `journey-ielts-diagnosis` |
  | Phụ | Recovery Journey | `journey-ielts-recovery` |

* Trang journey (`/e/<id>`): khối **Overview** (hạng, mục tiêu, bắt đầu khi, kết thúc khi, năm con
  số: chặng, bước, pain, touchpoint, % bằng chứng; người liên quan), rồi **Components**: mỗi chặng
  một card rộng hết dòng, bước của chặng là card con xếp dọc. Bố cục cột ngang kiểu journey map
  cũ bị bỏ: nó bắt cuộn ngang và làm chữ gãy vụn trong cột hẹp.

* **Màn đầu 20%, ẩn 80%** (SRC-1096, chủ dự án 28.09.2026: "nhìn như hiện nay thì siêu rối rắm"). Trang
  journey chỉ hiện: tên, một câu tóm tắt, mục tiêu, bốn con số, và danh sách chặng THU GỌN (một dòng:
  số, tên, bao nhiêu bước, bao nhiêu pain). Bấm Steps/Pains/Touchpoints thì hiện đúng danh sách ấy kèm
  nơi xuất hiện; bấm chặng mở mục tiêu và các bước; bấm bước mới thấy ai làm, làm gì, cảm xúc, kênh.
  Bằng chứng, người liên quan, quản trị, lịch sử nằm sau "More about this journey". Accordion shadcn.
  Một dòng trên danh sách giải thích Stage (một chặng) khác Step (một việc cụ thể trong chặng).

* Tên chặng và tên bước viết **tiếng Anh** (chủ dự án 28.09.2026), đúng nhãn giao diện Q-188;
  mô tả và các trường chi tiết giữ tiếng Việt.

## 7. Để sau (đợt 2)

| Việc | REQ | Vì sao chưa |
| --- | --- | --- |
| Tìm theo nghĩa bằng Vectorize | REQ-GUL-15 | Kho 128 thực thể thì tìm theo từ đã trả lời được; Vectorize cần tạo index trên production qua workflow |
| AI Research Assistant (Claude API) trích Persona/JTBD/Pain từ ghi chép phỏng vấn | REQ-GUL-13 | Không có giao diện sửa, nên AI sẽ chạy như một script sinh JSON có `generated_by: "ai:<model>"`, qua đúng cổng §5 |
| AI Relationship Discovery (hỏi bằng lời) | REQ-GUL-14 | Cần REQ-GUL-15 trước |
| Nối Evidence từ dữ liệu thật (Dory, journey_events) | Q-191 | Chỉ số tổng hợp hoặc trích dẫn đã bỏ tên |

## 8. Kiểm chứng

| Cổng | Kiểm gì |
| --- | --- |
| `workers/api/src/modules/gull/routes.test.ts` | đọc không cần phiên; không có POST/PUT/PATCH/DELETE; trigger lịch sử chỉ ghi khi đổi thật; xoá để lại vết; seed chạy hai lần trên schema thật không đổi version |
| `authCoverage.test.ts` | hai route công khai được khai có lý do |
| `apps/gull/src/graph.test.ts` | ví dụ IELTS PRD-007 §7 đi trọn một mạch; mẫu xuyên chương trình IELTS+SAT; ma trận gom cột; North Star giảm khi tri thức cũ; tìm kiếm bỏ dấu |
| `gull-build-seed.mjs --check` trong `check:code` | năm luật §5 |
| `contrast-check.mjs` | đọc `packages/design-system/deep.css` sau khi khối `.n12-deep` chuyển khỏi learn |
