update skill and docs
This commit is contained in:
@@ -122,6 +122,7 @@ chúng chỉ ghi artifact của mình rồi báo cho người dùng chạy `/ba-
|
||||
Mũi tên = "cần cái kia mới viết đúng được". Thiếu input ⇒ vẫn làm, nhưng phải ghi `OQ` và
|
||||
nêu rõ trong Source là đã dùng nguồn thay thế nào.
|
||||
|
||||
<!-- archify: mermaid-only -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
STK["STAKEHOLDER"] --> BRIEF["BRIEF"]
|
||||
|
||||
@@ -1,224 +1,269 @@
|
||||
# Quy tắc vẽ sơ đồ trong tài liệu BA
|
||||
# Chuẩn sơ đồ — theo tiêu chuẩn và phong cách Archify
|
||||
|
||||
Mọi sơ đồ trong bộ skill này viết bằng **mermaid**, không dùng ASCII art.
|
||||
Áp dụng cho **mọi sơ đồ** của cả ba bộ skill (`ba-*`, `sa-*`, `sad-*` và các agent SAD/proposal/bid).
|
||||
Quy tắc `W13` của bộ BA và `D4` của bộ SA đều trỏ về file này.
|
||||
|
||||
Lý do không phải thẩm mỹ:
|
||||
|
||||
| ASCII art | mermaid |
|
||||
|---|---|
|
||||
| Dán vào Confluence/PowerPoint là vỡ, phải vẽ lại tay | GitHub · GitLab · Confluence · Artifact của Claude Code render thẳng |
|
||||
| `git diff` ra một khối rác | Diff theo dòng, review được từng cạnh |
|
||||
| Ký hiệu tự chế, khách quen UML/BPMN đọc lệch | Ký hiệu chuẩn của từng loại sơ đồ |
|
||||
| Sửa một node phải căn lại cả hình | Sửa một dòng |
|
||||
Chuẩn tham chiếu: [Archify](https://github.com/tt-a1i/archify) (MIT) — gói đã nhúng tại
|
||||
`.claude/skills/archify/` (đọc `SKILL.md`, `schemas/`, `references/authoring-contract.md` ở đó khi
|
||||
cần chi tiết trường). Không cài thêm gì, chỉ cần Node ≥ 18.
|
||||
|
||||
---
|
||||
|
||||
## W13 — Mỗi sơ đồ phải có bảng đi kèm
|
||||
## 0. Ba câu chốt
|
||||
|
||||
🔴 **Quy tắc quan trọng nhất của file này.**
|
||||
1. **Một sơ đồ = spec JSON Archify + Mermaid trong `.md` + bảng đi kèm.** Spec là nguồn sự thật về
|
||||
topology (node, cạnh, ranh giới); Mermaid là bản chiếu để GitHub/Confluence render và `git diff`
|
||||
đọc được; bảng để truy vết, đặt con số và viết test. Ba thứ phải khớp nhau.
|
||||
2. **Một đường chính rõ ràng, ≤ 12 node chính, nhãn cạnh là dữ liệu ngữ nghĩa.** Nhánh phụ rời khỏi
|
||||
node gần nhất trên đường chính. Quá 12 node ⇒ tách sơ đồ, không thu nhỏ chữ.
|
||||
3. **Kiểm bằng máy trước khi trình gate.** `archify validate … --quality showcase` phải pass 0 lỗi,
|
||||
và `diagram-check.mjs` phải xanh. Sơ đồ chưa validate là bản nháp, không đưa vào artifact 🔵/✅.
|
||||
|
||||
**Sơ đồ để nhìn, bảng để truy vết và test.** Sơ đồ không diễn đạt được: điều kiện chính xác,
|
||||
ai được làm, mã lỗi nào, quy tắc nào chi phối, ai chịu trách nhiệm. Một sơ đồ đứng một mình
|
||||
là một bức tranh đẹp mà QA không viết được test case từ đó.
|
||||
---
|
||||
|
||||
## 1. Chọn loại — bộ định tuyến
|
||||
|
||||
| Cần thể hiện | Loại Archify | Mermaid đi kèm | Sinh ở |
|
||||
|---|---|---|---|
|
||||
| Ranh giới hệ thống (BRIEF §5.3), phương án mức khối (`OPT` §3) | `architecture` | `flowchart LR` + `subgraph` | ba-1, sa-1 |
|
||||
| C4 Context / Container / Component, deployment view, ranh giới tin cậy | `architecture` | `flowchart LR/TB` + `subgraph` | `SAD` §3–§5, §7 · `INF` §3 · `SEC` §1 |
|
||||
| Quy trình AS-IS / TO-BE, luồng phê duyệt, phân loại Defect/CR | `workflow` | `flowchart TD` | `PROCESS` A1/B1 · `CR` §0 |
|
||||
| Điều hướng màn hình, phụ thuộc job | `workflow` | `flowchart LR` | `srs-part2/screen` §2.2 · `batch-job` §2.2 |
|
||||
| Luồng nhiều bên theo thời gian, **có nhánh lỗi** | `sequence` | `sequenceDiagram` | `srs-part2/*` §2.3 · `SAD` §6 |
|
||||
| Luồng dữ liệu, lineage, ownership dữ liệu | `dataflow` | `flowchart LR` | `srs-part2/data-pipeline` §2.2 · `DAT` §1 |
|
||||
| Vòng đời trạng thái | `lifecycle` | `stateDiagram-v2` | `BR` §3 · SAD-pipeline mục 6 |
|
||||
| Phụ thuộc mục roadmap | `workflow` | `flowchart LR` | `TRM` §4 |
|
||||
|
||||
**Không có loại Archify — chỉ Mermaid** (`mermaid-only`), vẫn áp §3 phong cách và §4 bảng:
|
||||
|
||||
| Sơ đồ | Mermaid | Vì sao Mermaid-only |
|
||||
|---|---|---|
|
||||
| ERD khái niệm (`BR` §4, `DAT` §2) | `erDiagram` | Archify không có kiểu quan hệ thực thể |
|
||||
| Class diagram (`DOM`, SAD-pipeline mục 6) | `classDiagram` | Như trên |
|
||||
| Use case (`BACKLOG` §0) | `flowchart LR` + `subgraph` | Quan hệ actor–use case là liên kết, không phải luồng |
|
||||
| Cây phân rã RQ→US (`BACKLOG` §1), 5-Why (`ELICITATION` §A9) | `flowchart LR/TD` | Cây tư duy, không phải hệ thống |
|
||||
| Ma trận 2×2 (`STAKEHOLDER` §2) | `quadrantChart` | — |
|
||||
| Ma trận ≥ 3×3, bảng số liệu | **bảng markdown** | Không phải sơ đồ |
|
||||
| Bố cục màn hình | `WF_<US>.md` + `.html` | Mermaid không vẽ UI; xem ba-3 |
|
||||
|
||||
Lưỡng lự ⇒ `node .claude/skills/archify/bin/archify.mjs guide "<mô tả>" --json`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Từ vựng Archify áp cho miền của ta
|
||||
|
||||
### 2.1 `type` của node
|
||||
|
||||
| `type` | Dùng cho |
|
||||
|---|---|
|
||||
| `external` | Người dùng/actor (`ROLE-nn`), hệ thống ngoài phạm vi, đối tác (VNPay, GHN…), sự kiện kích hoạt |
|
||||
| `frontend` | Web/mobile client, màn hình `SCR-nn`, bước do người thao tác trên giao diện |
|
||||
| `backend` | Service/module `CMP-nn`, job, bước xử lý của hệ thống, điểm quyết định |
|
||||
| `database` | CSDL, cache, object store, kho dữ liệu, "kết thúc: ghi sổ" |
|
||||
| `messagebus` | Queue, topic, event bus, luồng bất đồng bộ |
|
||||
| `security` | IdP, WAF, KMS, kiểm quyền, vùng cách ly, bước dừng vì lỗi/bảo mật |
|
||||
| `cloud` | Hạ tầng quản lý: ALB, CDN, DNS, vùng/AZ khi là node |
|
||||
|
||||
Với `lifecycle`, `type` là trạng thái: `start` · `active` · `waiting` (chờ người/hệ khác) · `decision` ·
|
||||
`success` (cuối, tốt) · `failure` (cuối xấu **hoặc** phục hồi được — phải có cạnh quay lại) · `neutral` · `external`.
|
||||
|
||||
### 2.2 `variant` của cạnh
|
||||
|
||||
| `variant` | Nghĩa |
|
||||
|---|---|
|
||||
| `emphasis` | **Đường chính** (happy path) — mỗi sơ đồ có đúng một đường chính |
|
||||
| `default` | Nhánh phụ đồng bộ |
|
||||
| `dashed` | Bất đồng bộ, tuỳ chọn, degraded, quay lại/retry |
|
||||
| `security` | Xác thực, phân quyền, dữ liệu nhạy cảm, nhánh lỗi/dừng |
|
||||
| `return` (chỉ `sequence`) | Phản hồi |
|
||||
|
||||
`workflow` thêm `role`: `main` · `branch` · `async` · `return` · `error`.
|
||||
|
||||
### 2.3 ID và nhãn
|
||||
|
||||
- **ID là mã truy vết, bỏ dấu gạch**: `CMP04`, `SCR01`, `US011`, `IF002`, `A3`, `B2`, `ChoDuyet`. Cùng
|
||||
một ID dùng ở spec, Mermaid và bảng — đó là thứ nối ba thứ với nhau. Mẫu `^[A-Za-z][A-Za-z0-9_-]*$`,
|
||||
không dấu tiếng Việt.
|
||||
- **`label` mang mã đầy đủ + tên**: `"CMP-04 Cart & Order"`, `"🆕 B2 Đối soát tự động"`. `sublabel` mang
|
||||
tham chiếu: `"REST/JSON · /v1"`, `"SCR-01 · ROLE-01"`, `"BR-014 · ⏱ ≤ 5 phút"`.
|
||||
- **Nhãn cạnh là điều kiện, giao thức, sync/async, hành động** — không phải mô tả: ✅ `"REST · IF-002"`,
|
||||
`"lệch > ngưỡng"`, `"publish · async"` · ❌ `"chuyển sang trạng thái đã duyệt"`. Không xoá nhãn để
|
||||
sửa bố cục; chỉ bỏ khi hai đầu đã nói hết nghĩa và ghi lý do trong bảng.
|
||||
- Đánh dấu bằng **tiền tố trong nhãn**, không bằng màu: `⚠️ P1 ·` điểm đau · `🆕` mới · `🔄` đổi ·
|
||||
`⬜` giữ · `➖` bỏ (chỉ ở bảng, không vẽ).
|
||||
|
||||
### 2.4 `meta` — mặc định phải giữ
|
||||
|
||||
| Trường | Giá trị | Lý do |
|
||||
|---|---|---|
|
||||
| `quality_profile` | `"showcase"` **bắt buộc** | Đủ 9 phép kiểm bố cục; `standard` không được nhận ở gate |
|
||||
| `visual_preset` | **bỏ** (= `classic`) | Chỉ đặt `signal-flow`/`blueprint`/`editorial` khi người dùng yêu cầu đích danh |
|
||||
| `subtitle` | **bỏ** | Không lặp lại title/node |
|
||||
| `legend` | **bỏ** (= `auto`) | Legend tự liệt kê đúng loại có mặt |
|
||||
| `locale` | **bỏ** | Tài liệu tiếng Việt: giao diện viewer sẽ là tiếng Anh; ghi chú điều này một lần ở artifact |
|
||||
| `animation` | bỏ | Chỉ `"trace"` cho bản trình chiếu |
|
||||
| `views` | ≤ 5, khuyến nghị 2 | "đường chính" và "nhánh lỗi/bất đồng bộ" |
|
||||
| `cards` | ≤ 3, mỗi card ≤ 3 dòng | Chỉ chép sự thật đã có trong bảng đi kèm; không viết nội dung mới |
|
||||
| `engineering_profile` | bỏ | Chỉ `deployment-ownership` khi làm `INF` fail-closed và biết đủ owner/region |
|
||||
|
||||
---
|
||||
|
||||
## 3. Phong cách Mermaid (bản chiếu của spec)
|
||||
|
||||
1. **Không tô màu, không style** — cấm `style`, `classDef`, `linkStyle`, `:::`. Renderer đổi theme
|
||||
sáng/tối; màu cứng làm chữ biến mất. Phân biệt bằng **hình dạng** và **nhãn**.
|
||||
2. **Hình dạng theo `type`**: `["…"]` backend/bước xử lý · `{"…"}` quyết định · `(["…"])` actor/bắt đầu/kết
|
||||
thúc · `[("…")]` database · `[["…"]]` external/ngoài phạm vi · `>"…"]` messagebus.
|
||||
3. **Hướng**: `TD` cho quy trình nhiều rẽ nhánh · `LR` cho luồng tuyến tính, lineage, điều hướng, C4.
|
||||
4. **`subgraph` = `boundaries`** (region / security-group) hoặc lane. ID subgraph không dấu.
|
||||
5. **Cùng tập node và cạnh với spec** — `diagram-check` so hai chiều. Mermaid được thêm chi tiết mà
|
||||
Archify không có (`alt/else` trong sequence, `[*]` trong state) nhưng không thêm node.
|
||||
6. **Sequence bắt buộc có nhánh lỗi** (`alt`/`else`) và `autonumber`; số bước là khoá của bảng đi kèm.
|
||||
Trong spec, nhánh lỗi là các `messages` với `segments` "Nhánh lỗi".
|
||||
7. `stateDiagram-v2`: `state "Nhãn có dấu" as IDKhongDau`. Cạnh bị cấm không vẽ được ⇒ nằm ở bảng.
|
||||
8. Không dùng `gantt` (việc của PM) và `pie` (bảng luôn rõ hơn).
|
||||
|
||||
---
|
||||
|
||||
## 4. Bảng đi kèm (W13) — bắt buộc
|
||||
|
||||
**Sơ đồ để nhìn, bảng để truy vết và test.** Sơ đồ không diễn đạt được điều kiện chính xác, ai được
|
||||
làm, mã lỗi nào, timeout bao nhiêu, ai sở hữu.
|
||||
|
||||
| Sơ đồ | Bảng bắt buộc đi kèm |
|
||||
|---|---|
|
||||
| Quy trình AS-IS/TO-BE | Bảng chi tiết từng bước: ai · input · output · công cụ · thời gian |
|
||||
| Vòng đời trạng thái | Bảng chuyển: nguồn · sự kiện · **điều kiện** · đích · ai được làm · BR · ghi vết |
|
||||
| Quy trình (`workflow`) | Bảng bước: ai · input · output · công cụ · thời gian |
|
||||
| Vòng đời (`lifecycle`) | Bảng chuyển: nguồn · sự kiện · **điều kiện** · đích · ai · BR · ghi vết · **+ bảng chuyển bị cấm** |
|
||||
| Use case | Bảng US: id · vai trò · RQ · MoSCoW |
|
||||
| ERD | Bảng thực thể: trường · kiểu · khoá · ràng buộc |
|
||||
| ERD / class | Bảng thực thể: trường · kiểu · khoá · ràng buộc |
|
||||
| Sequence | Bảng bước: mã lỗi mỗi nhánh · timeout · hành vi khi thất bại |
|
||||
| Lineage / phụ thuộc | Bảng luồng: tần suất · khối lượng · SLA · chủ sở hữu |
|
||||
| C4 / deployment (`architecture`) | Bảng `CMP-nn`: trách nhiệm · **không làm gì** · dữ liệu sở hữu · interface · team · ASR |
|
||||
| Lineage / phụ thuộc (`dataflow`, `workflow`) | Bảng luồng: tần suất · khối lượng · SLA · chủ sở hữu |
|
||||
|
||||
Khi sơ đồ và bảng mâu thuẫn: **bảng thắng**. Ghi câu này vào tài liệu (quy tắc W11).
|
||||
Sơ đồ mâu thuẫn bảng ⇒ **bảng thắng** về ràng buộc và con số; **sơ đồ thắng** về quan hệ và luồng
|
||||
(`W11`, `D12`). Ghi câu này vào tài liệu.
|
||||
|
||||
---
|
||||
|
||||
## Chọn loại sơ đồ
|
||||
## 5. Tệp, marker, lệnh
|
||||
|
||||
| Cần thể hiện | Dùng | Ở đâu trong bộ skill |
|
||||
### 5.1 Vị trí
|
||||
|
||||
```
|
||||
<thư mục artifact>/diagrams/<ARTIFACT>_<slug>.<type>.json ← spec (nguồn sự thật)
|
||||
<thư mục artifact>/diagrams/<ARTIFACT>_<slug>.<type>.html ← đã deliver (tự chứa, dark/light, export PNG/SVG)
|
||||
```
|
||||
|
||||
Ví dụ: `sa-output/<P>/02-architecture/diagrams/SAD_c4-container.architecture.json`,
|
||||
`ba-output/<P>/02-analysis/diagrams/PROCESS_to-be.workflow.json`, `docs/diagrams/03_c4-container.architecture.json`
|
||||
(SAD-pipeline), `bid/diagrams/…`, `docs/proposal/diagrams/…`.
|
||||
|
||||
### 5.2 Marker trong `.md` — ngay trên khối Mermaid
|
||||
|
||||
```markdown
|
||||
<!-- archify: architecture · diagrams/SAD_c4-container.architecture.json -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
…
|
||||
```
|
||||
```
|
||||
|
||||
Sơ đồ không có kiểu Archify:
|
||||
|
||||
```markdown
|
||||
<!-- archify: mermaid-only -->
|
||||
```
|
||||
|
||||
Thiếu marker ⇒ `diagram-check` 🔴.
|
||||
|
||||
### 5.3 Lệnh — người điều phối (có Bash) chạy sau mỗi activity sinh sơ đồ
|
||||
|
||||
```bash
|
||||
A=.claude/skills/archify/bin/archify.mjs
|
||||
node $A validate <type> <spec.json> --quality showcase --json # sửa theo diagnostics tới khi 0 lỗi
|
||||
node $A deliver <type> <spec.json> <spec.html> --quality showcase --json # chốt HTML, có SHA-256
|
||||
node .claude/skills/ba-lifecycle/scripts/diagram-check.mjs --md <artifact.md> # A0–A7, exit 1 nếu 🔴
|
||||
```
|
||||
|
||||
Runner không có Bash ⇒ vẫn viết spec + Mermaid + marker, ghi `humanInputNeeded` "chạy validate/deliver
|
||||
+ diagram-check", và **không** tự ghi "đã validate".
|
||||
|
||||
### 5.4 Sửa lỗi validate — thứ tự (theo `authoring-contract.md`)
|
||||
|
||||
1. `meta.quality_profile` và lỗi schema → 2. node chồng nhau / ngoài lưới → 3. cạnh xuyên node, sai hướng
|
||||
cổng → 4. cạnh cắt nhau, hành lang mơ hồ → 5. nhãn chồng node/nhãn/tuyến: **giãn khoảng cách hoặc dời
|
||||
nhãn (`labelDy`/`labelDx`/`labelAt` theo gợi ý), rồi mới rút ngắn chữ, không xoá nhãn.** Mỗi lần sửa một
|
||||
điều khiển hình học. Hai vòng không giảm số lỗi ⇒ dừng và báo trung thực.
|
||||
|
||||
Khoảng cách là **khe trống**, không phải khoảng tâm: `khe > 6,5px × số ký tự ASCII + 13 + 8` (chữ CJK tính
|
||||
đôi). Node 150px cách tâm 340px ⇒ khe 190px ⇒ nhãn ≤ 26 ký tự.
|
||||
|
||||
---
|
||||
|
||||
## 6. Mẫu chuẩn — sao chép rồi sửa
|
||||
|
||||
Spec mẫu đã validate pass (`showcase`), một mẫu mỗi loại:
|
||||
|
||||
| Loại | Spec mẫu | Mermaid tương ứng ở template |
|
||||
|---|---|---|
|
||||
| Quy trình có rẽ nhánh | `flowchart TD` | `PROCESS` A1/B1 |
|
||||
| Vòng đời trạng thái | `stateDiagram-v2` | `BR` §3 |
|
||||
| Actor × chức năng, toàn cảnh phạm vi | `flowchart LR` + `subgraph` | `BACKLOG` §0 |
|
||||
| Thực thể và quan hệ | `erDiagram` | `BR` §4 |
|
||||
| Luồng nhiều bên theo thời gian | `sequenceDiagram` | `srs-part2/*` |
|
||||
| Luồng dữ liệu, phụ thuộc job | `flowchart LR` | `srs-part2/data-pipeline`, `batch-job` |
|
||||
| Điều hướng màn hình | `flowchart LR` | `srs-part2/screen` |
|
||||
| Ma trận 2×2 | `quadrantChart` | `STAKEHOLDER` §2 |
|
||||
| Ma trận 3×3 trở lên | **bảng markdown** — mermaid không có loại này | `RISK` §2 |
|
||||
| Ranh giới hệ thống | `flowchart LR` + `subgraph` | `BRIEF` §5.3 |
|
||||
| `architecture` | `sa-2-architecture/templates/diagrams/SAD_c4-container.architecture.json` | `sad.md` §4 |
|
||||
| `sequence` | `sa-2-architecture/templates/diagrams/SAD_luong-chinh.sequence.json` | `sad.md` §6, `srs-part2/screen.md` §2.3.4 |
|
||||
| `workflow` | `ba-2-analysis/templates/diagrams/PROCESS_to-be.workflow.json` | `process-model.md` B1 |
|
||||
| `lifecycle` | `ba-2-analysis/templates/diagrams/BR_vong-doi.lifecycle.json` | `business-rules.md` §3 |
|
||||
| `dataflow` | `ba-3-specification/templates/diagrams/SRS_lineage.dataflow.json` | `srs-part2/data-pipeline.md` §2.2 |
|
||||
|
||||
**Không dùng** `gantt` (tiến độ là việc của PM, không phải BA) và `pie` (một bảng luôn rõ hơn).
|
||||
Dùng mẫu để lấy **hình dạng trường**, không lấy sự thật: ID, tên, con số phải là của dự án.
|
||||
|
||||
---
|
||||
|
||||
## Quy ước bắt buộc
|
||||
|
||||
### 1. ID node không dấu, nhãn có dấu
|
||||
|
||||
Ký tự tiếng Việt trong **ID** làm vỡ ở một số renderer. Luôn tách ID và nhãn:
|
||||
|
||||
```
|
||||
✅ A1["Nhận file POS từ cửa hàng"]
|
||||
❌ Nhận file POS
|
||||
```
|
||||
|
||||
Với `stateDiagram-v2` dùng dạng khai báo riêng:
|
||||
|
||||
```
|
||||
state "Chờ duyệt" as ChoDuyet
|
||||
```
|
||||
|
||||
### 2. ID mang mã truy vết
|
||||
|
||||
Node ID chính là ID trong bảng — đó là thứ nối sơ đồ với bảng (W13):
|
||||
|
||||
```
|
||||
A3["A3. Đối chiếu thủ công"] ← khớp cột # của bảng chi tiết bước
|
||||
UC11(["US-011 Tải file POS"]) ← khớp id trong BACKLOG
|
||||
SCR01["SCR-01 Danh sách"] ← khớp id trong bảng màn hình
|
||||
```
|
||||
|
||||
### 3. Không tô màu, không style
|
||||
|
||||
Renderer đổi theme sáng/tối; màu cứng làm chữ biến mất. Phân biệt bằng **hình dạng** và
|
||||
**nhãn**, không bằng màu:
|
||||
|
||||
| Ý nghĩa | Hình dạng |
|
||||
|---|---|
|
||||
| Bước xử lý | `A["..."]` chữ nhật |
|
||||
| Điểm quyết định | `A{"..."}` thoi |
|
||||
| Bắt đầu / kết thúc | `A(["..."])` bo tròn |
|
||||
| Dữ liệu / tài liệu | `A[("...")]` trụ |
|
||||
| Hệ thống ngoài phạm vi | `A[["..."]]` khung đôi |
|
||||
|
||||
Đánh dấu đặc biệt bằng **tiền tố trong nhãn**, không bằng màu:
|
||||
`"⚠️ P1 · Đối chiếu thủ công"` · `"🆕 B2 · ..."` · `"➖ A5 · (bỏ)"`
|
||||
|
||||
### 4. Hướng vẽ
|
||||
|
||||
`TD` (trên xuống) cho quy trình có nhiều rẽ nhánh · `LR` (trái sang phải) cho luồng tuyến
|
||||
tính, lineage, điều hướng, use case. Sơ đồ quá 20 node ⇒ **tách thành nhiều sơ đồ**, đừng
|
||||
thu nhỏ chữ.
|
||||
|
||||
### 5. Nhãn cạnh là điều kiện, không phải mô tả
|
||||
|
||||
```
|
||||
✅ ChoDuyet --> DaDuyet: duyệt (chênh lệch ≤ 10tr)
|
||||
❌ ChoDuyet --> DaDuyet: chuyển sang trạng thái đã duyệt
|
||||
```
|
||||
|
||||
Điều kiện đầy đủ vẫn nằm ở bảng — nhãn cạnh chỉ là gợi nhớ.
|
||||
|
||||
---
|
||||
|
||||
## Mẫu chuẩn — sao chép rồi sửa
|
||||
|
||||
### Quy trình
|
||||
### Quy trình (`workflow`) — Mermaid
|
||||
|
||||
````markdown
|
||||
<!-- archify: workflow · diagrams/PROCESS_to-be.workflow.json -->
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START(["Đơn hàng phát sinh"]) --> A1["A1. Ghi nhận vào POS"]
|
||||
A1 --> A2["A2. Xuất file cuối ca"]
|
||||
A2 --> D1{"Có sai lệch?"}
|
||||
D1 -->|Không| END(["Kết thúc"])
|
||||
D1 -->|Có| A3["⚠️ P1 · A3. Đối chiếu thủ công"]
|
||||
A3 --> EXT[["Gọi điện xác nhận với cửa hàng"]]
|
||||
EXT --> A2
|
||||
START(["Kết ca"]) -->|cuối ca| B1["🆕 B1 Tải file POS"]
|
||||
B1 -->|file hợp lệ| B2["🆕 B2 Đối soát tự động"]
|
||||
B2 --> D1{"D1 Có sai lệch?"}
|
||||
D1 -->|không lệch| END[("Kết thúc")]
|
||||
D1 -->|lệch > ngưỡng| B3["🔄 B3 Tạo phiếu lệch"]
|
||||
B3 -->|gửi duyệt| B4["⬜ B4 Duyệt phiếu"]
|
||||
B4 -->|đã duyệt| END
|
||||
B1 -->|file sai định dạng| X1[["File hỏng · E-POS-0400"]]
|
||||
X1 -.->|tải lại| B1
|
||||
```
|
||||
````
|
||||
|
||||
### Vòng đời trạng thái
|
||||
### Vòng đời (`lifecycle`) — Mermaid
|
||||
|
||||
````markdown
|
||||
<!-- archify: lifecycle · diagrams/BR_vong-doi.lifecycle.json -->
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
state "Nháp" as Nhap
|
||||
state "Chờ duyệt" as ChoDuyet
|
||||
state "Đã duyệt" as DaDuyet
|
||||
state "Đã đóng" as DaDong
|
||||
state "Bị từ chối" as TuChoi
|
||||
|
||||
[*] --> Nhap
|
||||
Nhap --> ChoDuyet: gửi duyệt (đủ trường bắt buộc)
|
||||
ChoDuyet --> DaDuyet: duyệt
|
||||
ChoDuyet --> Nhap: từ chối
|
||||
ChoDuyet --> DaDuyet: duyệt (chênh lệch ≤ 10tr)
|
||||
ChoDuyet --> TuChoi: từ chối
|
||||
TuChoi --> Nhap: sửa lại
|
||||
DaDuyet --> DaDong: đóng (có ghi chú lý do)
|
||||
DaDong --> [*]
|
||||
```
|
||||
````
|
||||
|
||||
Kèm bảng chuyển trạng thái đầy đủ, **và bảng "chuyển trạng thái KHÔNG được phép"** — sơ đồ
|
||||
chỉ vẽ được cạnh có tồn tại, không vẽ được cạnh bị cấm.
|
||||
|
||||
### Use case
|
||||
|
||||
Mermaid không có use case diagram; dùng `flowchart LR` với `subgraph` làm ranh giới hệ thống:
|
||||
|
||||
````markdown
|
||||
```mermaid
|
||||
flowchart LR
|
||||
NV(["👤 Nhân viên đối soát"])
|
||||
TN(["👤 Trưởng nhóm"])
|
||||
KT(["👤 Kế toán"])
|
||||
|
||||
subgraph HT["Hệ thống đối soát"]
|
||||
UC11(["US-011 Tải file POS"])
|
||||
UC13(["US-013 Xem chênh lệch"])
|
||||
UC14(["US-014 Đóng chênh lệch"])
|
||||
end
|
||||
|
||||
NV --- UC11
|
||||
NV --- UC13
|
||||
TN --- UC13
|
||||
TN --- UC14
|
||||
KT --- UC13
|
||||
```
|
||||
````
|
||||
|
||||
Dùng `---` (không mũi tên): quan hệ actor–use case là **liên kết**, không phải luồng.
|
||||
|
||||
### ERD khái niệm
|
||||
|
||||
Tên thực thể **không dấu, viết hoa**; tên tiếng Việt để ở bảng đi kèm.
|
||||
|
||||
````markdown
|
||||
```mermaid
|
||||
erDiagram
|
||||
CUA_HANG ||--o{ GIAO_DICH : "phát sinh"
|
||||
GIAO_DICH ||--o| CHENH_LECH : "sinh ra khi lệch"
|
||||
NGUOI_DUNG ||--o{ CHENH_LECH : "xử lý"
|
||||
|
||||
CUA_HANG {
|
||||
string ma_cua_hang PK "3-20 ký tự"
|
||||
string ten
|
||||
enum trang_thai
|
||||
}
|
||||
CHENH_LECH {
|
||||
bigint id PK
|
||||
bigint giao_dich_id FK
|
||||
decimal so_tien "VND"
|
||||
enum trang_thai
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
Ký hiệu lực lượng: `||--o{` một-nhiều · `||--||` một-một · `}o--o{` nhiều-nhiều ·
|
||||
`||--o|` một-không hoặc một.
|
||||
|
||||
🔴 **ERD ở GĐ2 là mô hình khái niệm**, không phải schema. Nêu thực thể, quan hệ, khoá nghiệp
|
||||
vụ. Không nêu kiểu dữ liệu vật lý, index, bảng trung gian — đó là việc của SA/dev.
|
||||
|
||||
### Sequence
|
||||
### Sequence (`sequence`) — Mermaid, bắt buộc nhánh lỗi
|
||||
|
||||
````markdown
|
||||
<!-- archify: sequence · diagrams/SAD_luong-chinh.sequence.json -->
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as Nhân viên
|
||||
participant FE as Giao diện
|
||||
participant BE as Hệ thống
|
||||
participant BE as Settlement API
|
||||
participant POS as Hệ thống POS
|
||||
|
||||
U->>FE: Bấm Lưu
|
||||
@@ -229,19 +274,90 @@ sequenceDiagram
|
||||
BE-->>FE: 200 + id
|
||||
FE-->>U: Toast "Đã lưu"
|
||||
else POS timeout
|
||||
POS--xBE: timeout
|
||||
POS--xBE: timeout sau 3s
|
||||
BE-->>FE: 503 · E-STR-0503
|
||||
FE-->>U: Báo lỗi, GIỮ NGUYÊN dữ liệu đã nhập
|
||||
end
|
||||
```
|
||||
````
|
||||
|
||||
`autonumber` đánh số bước để bảng đi kèm tham chiếu được. **Bắt buộc vẽ cả nhánh lỗi** —
|
||||
sequence chỉ có luồng thành công là vi phạm quy tắc W4.
|
||||
|
||||
### Ma trận 2×2
|
||||
### C4 Container (`architecture`) — Mermaid
|
||||
|
||||
````markdown
|
||||
<!-- archify: architecture · diagrams/SAD_c4-container.architecture.json -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
NV(["👤 Nhân viên đối soát · ROLE-01"])
|
||||
IDP[["IdP nội bộ · OIDC"]]
|
||||
subgraph REGION["Vùng hệ thống của ta · ap-southeast-1"]
|
||||
WEB["CMP-01 Web App"]
|
||||
API["CMP-02 Settlement API"]
|
||||
DB[("CMP-03 CSDL chính")]
|
||||
QUEUE>"CMP-04 Hàng đợi · SQS FIFO"]
|
||||
WORKER["CMP-05 Worker"]
|
||||
end
|
||||
POS[["Hệ thống POS · đối tác"]]
|
||||
|
||||
NV -->|HTTPS · sync| WEB
|
||||
WEB -->|REST · IF-002| API
|
||||
IDP -->|JWT · IF-001| API
|
||||
API -->|SQL · sync| DB
|
||||
API -.->|publish · async| QUEUE
|
||||
QUEUE -.->|consume · async| WORKER
|
||||
WORKER -->|SQL · sync| DB
|
||||
WORKER -.->|REST · 3s · retry 3| POS
|
||||
```
|
||||
````
|
||||
|
||||
**Legend** (ghi dưới sơ đồ): `["…"]` service của ta · `[["…"]]` ngoài phạm vi · `[("…")]` CSDL ·
|
||||
`>"…"]` hàng đợi · `-->` đồng bộ · `-.->` bất đồng bộ · nhãn = giao thức · `IF-nnn`.
|
||||
|
||||
### Use case (`mermaid-only`)
|
||||
|
||||
````markdown
|
||||
<!-- archify: mermaid-only -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
NV(["👤 Nhân viên đối soát"])
|
||||
TN(["👤 Trưởng nhóm"])
|
||||
subgraph HT["Hệ thống đối soát"]
|
||||
UC11(["US-011 Tải file POS"])
|
||||
UC13(["US-013 Xem chênh lệch"])
|
||||
UC14(["US-014 Đóng chênh lệch"])
|
||||
end
|
||||
NV --- UC11
|
||||
NV --- UC13
|
||||
TN --- UC13
|
||||
TN --- UC14
|
||||
```
|
||||
````
|
||||
|
||||
Dùng `---` (không mũi tên): quan hệ actor–use case là liên kết, không phải luồng.
|
||||
|
||||
### ERD khái niệm (`mermaid-only`)
|
||||
|
||||
````markdown
|
||||
<!-- archify: mermaid-only -->
|
||||
```mermaid
|
||||
erDiagram
|
||||
CUA_HANG ||--o{ GIAO_DICH : "phát sinh"
|
||||
GIAO_DICH ||--o| CHENH_LECH : "sinh ra khi lệch"
|
||||
CHENH_LECH {
|
||||
bigint id PK
|
||||
bigint giao_dich_id FK
|
||||
decimal so_tien "VND"
|
||||
enum trang_thai
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
Lực lượng: `||--o{` một-nhiều · `||--||` một-một · `}o--o{` nhiều-nhiều · `||--o|` một-không hoặc một.
|
||||
🔴 ERD ở GĐ2 (BA) là **khái niệm**; kiểu vật lý, index, bảng trung gian thuộc `PDM` của SA.
|
||||
|
||||
### Ma trận 2×2 (`mermaid-only`)
|
||||
|
||||
````markdown
|
||||
<!-- archify: mermaid-only -->
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Stakeholder — Quan tâm × Ảnh hưởng
|
||||
@@ -252,24 +368,37 @@ quadrantChart
|
||||
quadrant-3 "Theo dõi"
|
||||
quadrant-4 "Giữ thông tin"
|
||||
"STK-01 Trưởng phòng TC": [0.85, 0.90]
|
||||
"STK-04 Pháp chế": [0.20, 0.85]
|
||||
```
|
||||
````
|
||||
|
||||
⚠️ `quadrantChart` cần mermaid ≥ 10. Renderer cũ (một số bản Confluence) không hiểu ⇒ giữ
|
||||
bảng phân nhóm bên dưới làm phương án dự phòng.
|
||||
⚠️ `quadrantChart` cần mermaid ≥ 10; giữ bảng phân nhóm bên dưới làm dự phòng.
|
||||
|
||||
---
|
||||
|
||||
## Khi mermaid không diễn đạt được
|
||||
|
||||
Ba trường hợp, và cách xử lý:
|
||||
## 7. Khi Mermaid hoặc Archify không diễn đạt được
|
||||
|
||||
| Trường hợp | Làm gì |
|
||||
|---|---|
|
||||
| Bố cục màn hình | `WF_<US>.md` (bảng vùng + bảng vị trí thành phần) **và** `WF_<US>.html` low-fi sinh từ `ba-3-specification/templates/wireframe.html` — mermaid không phải công cụ vẽ UI, ASCII art càng không. Có prototype thì WF ghi lại và đối chiếu; không có thì WF là đề xuất của BA |
|
||||
| Bố cục màn hình | `WF_<US>.md` + `.html` (ba-3), `HIFI` (ba-design) — không phải sơ đồ |
|
||||
| Ma trận ≥ 3×3, bảng số liệu | Bảng markdown |
|
||||
| Sơ đồ > 20 node | Tách thành nhiều sơ đồ theo phân vùng, mỗi cái một mục con |
|
||||
| Sơ đồ > 12 node chính | Tách theo phân vùng / bounded context, mỗi cái một mục con, một spec |
|
||||
| Nhánh `alt/else` nhiều tầng | Mỗi nhánh lỗi lớn một `sequence` riêng; spec dùng `segments` để đặt tên nhánh |
|
||||
| Cần chèn vào HTML proposal/bid/deck | Lấy `<svg>` từ file `.html` đã deliver (tự chứa, không CDN); không render lại Mermaid nếu đã có Archify HTML |
|
||||
|
||||
**Không bao giờ** quay lại ASCII art vì "hình này mermaid vẽ xấu". Xấu thì tách nhỏ hoặc
|
||||
đổi loại sơ đồ.
|
||||
**Không bao giờ** quay lại ASCII art. Xấu thì tách nhỏ hoặc đổi loại sơ đồ.
|
||||
|
||||
---
|
||||
|
||||
## 8. Checklist sơ đồ — in ☐/✅ ở cuối mỗi lần chạy skill có sơ đồ
|
||||
|
||||
```
|
||||
[ ] Mỗi sơ đồ có marker <!-- archify: … --> đúng loại (hoặc mermaid-only có lý do ở §1)
|
||||
[ ] Spec JSON: quality_profile = showcase · không subtitle/visual_preset/legend/locale
|
||||
[ ] archify validate pass 0 lỗi · đã deliver .html (hoặc humanInputNeeded nếu không có Bash)
|
||||
[ ] Một đường chính (emphasis) · ≤ 12 node chính · nhánh phụ rời node gần nhất
|
||||
[ ] ID = mã truy vết bỏ gạch, khớp spec ↔ Mermaid ↔ bảng (diagram-check A3/A4 ✅)
|
||||
[ ] Nhãn cạnh: giao thức · sync/async · điều kiện — không nhãn mô tả, không xoá nhãn để sửa bố cục
|
||||
[ ] Không style/classDef/màu · phân biệt bằng hình dạng và tiền tố nhãn
|
||||
[ ] Bảng đi kèm ngay dưới sơ đồ (W13) · câu thẩm quyền sơ đồ vs bảng (W11/D12)
|
||||
[ ] Sequence có nhánh lỗi · lifecycle có bảng chuyển bị cấm · C4 khai báo mức và legend
|
||||
```
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## 1. Sơ đồ
|
||||
|
||||
<!-- archify: mermaid-only -->
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START(["Ý tưởng / yêu cầu thô"])
|
||||
@@ -83,6 +84,7 @@ giải pháp; "mất 2 giờ/ngày tổng hợp số liệu thủ công" mới l
|
||||
- [ ] `IMPACT` nêu module/dữ liệu/tích hợp bị ảnh hưởng và cách xử lý dữ liệu cũ
|
||||
- [ ] Tech Lead xác nhận **khả thi kỹ thuật** và nêu ràng buộc (nếu có)
|
||||
- [ ] `ba-traceability` báo coverage RQ→US ≥ 100%
|
||||
- [ ] Sơ đồ `PROCESS` (workflow), `BR` §3 (lifecycle) có spec Archify validate pass; `diagram-check.mjs` không 🔴 trên `PROCESS`, `BACKLOG`, `BR`
|
||||
|
||||
**Chặn:** backlog có US nhưng không truy được về RQ nào (scope creep), hoặc TO-BE không
|
||||
giải quyết được pain point đã ghi ở G1.
|
||||
@@ -101,7 +103,8 @@ giải quyết được pain point đã ghi ở G1.
|
||||
- [ ] Mỗi `US` có `AC-<US>-nn` viết theo Given/When/Then, **có cả luồng lỗi**
|
||||
- [ ] Bảng mã lỗi `E-<DOMAIN>-nnnn` đầy đủ, mỗi mã có thông điệp hiển thị
|
||||
- [ ] `NFR` đã chốt cho các nhóm áp dụng, mỗi cái có cách verify
|
||||
- [ ] `API` contract có, hoặc ghi rõ là **giả định của BA** chờ BE xác nhận
|
||||
- [ ] `API` contract có, hoặc ghi rõ là **giả định của BA** chờ BE xác nhận; khi SA đã có `CTR`, mỗi endpoint trỏ tới `operationId` trong file OpenAPI của `CTR` (file thắng bảng)
|
||||
- [ ] Mọi sơ đồ trong `SRS` (điều hướng, sequence có nhánh lỗi, lineage, phụ thuộc job) có spec Archify validate pass; `diagram-check.mjs` không 🔴
|
||||
- [ ] *(screen)* `UICONV_<PROJECT>` tồn tại, không còn ô "chọn một" chưa chọn, có chữ ký Designer (hoặc PO + `DEC-nn` khi dự án không có Designer)
|
||||
- [ ] *(screen)* `WF_<US>` (md + html) phủ **mọi** `SCR` của SRS; tập `C-id` khớp **hai chiều** với bảng thành phần
|
||||
- [ ] *(screen)* `WF` §5 (lệch prototype ↔ SRS) đã điền; không còn lệch **Tồn tại/Hành vi** chưa có người quyết
|
||||
|
||||
@@ -96,15 +96,21 @@ Cuối mỗi tài liệu, mục **"Ngoài phạm vi"**: những thứ người
|
||||
không có. Ví dụ: *"Không xử lý đơn hàng đa tiền tệ trong phiên bản này"*. Nó ngăn dev làm
|
||||
thừa và ngăn PO tưởng đã có.
|
||||
|
||||
## W13 — Sơ đồ vẽ bằng mermaid, và luôn có bảng đi kèm
|
||||
## W13 — Sơ đồ theo chuẩn Archify: spec JSON + mermaid + bảng đi kèm
|
||||
|
||||
Hai vế, cả hai bắt buộc.
|
||||
Ba vế, cả ba bắt buộc. Chi tiết: [`diagram-rules.md`](diagram-rules.md).
|
||||
|
||||
**Vế 1 — mermaid, không ASCII art.** ASCII vỡ khi dán sang Confluence/PowerPoint, `git diff`
|
||||
ra một khối rác, và ký hiệu tự chế khiến khách quen UML/BPMN đọc lệch. Loại sơ đồ nào dùng
|
||||
cú pháp nào, quy ước ID/nhãn/hình dạng: [`diagram-rules.md`](diagram-rules.md).
|
||||
**Vế 1 — spec Archify là nguồn sự thật về topology.** Mỗi sơ đồ có kiểu Archify
|
||||
(`architecture` · `workflow` · `sequence` · `dataflow` · `lifecycle`) phải có file
|
||||
`diagrams/<ARTIFACT>_<slug>.<type>.json` đạt `archify validate … --quality showcase` 0 lỗi, và một
|
||||
đường chính rõ ràng với ≤ 12 node chính. Sơ đồ không có kiểu Archify (ERD, class, use case, 2×2)
|
||||
đánh dấu `mermaid-only`.
|
||||
|
||||
**Vế 2 — sơ đồ để nhìn, bảng để truy vết và test.** Sơ đồ không diễn đạt được điều kiện
|
||||
**Vế 2 — mermaid là bản chiếu, không ASCII art.** Ngay trên khối mermaid có marker
|
||||
`<!-- archify: <type> · <spec.json> -->`; ID node và cạnh khớp spec hai chiều; không `style`/
|
||||
`classDef`/màu. ASCII vỡ khi dán sang Confluence, `git diff` ra rác, ký hiệu tự chế đọc lệch.
|
||||
|
||||
**Vế 3 — sơ đồ để nhìn, bảng để truy vết và test.** Sơ đồ không diễn đạt được điều kiện
|
||||
chính xác, ai được làm, mã lỗi nào, quy tắc nào chi phối. Sơ đồ đứng một mình là bức tranh
|
||||
đẹp mà QA không viết được test case từ đó.
|
||||
|
||||
@@ -135,11 +141,12 @@ Sơ đồ mâu thuẫn bảng ⇒ **bảng thắng** (hệ quả của W11).
|
||||
[ ] W10 Có tóm tắt nghiệp vụ cho PO ở đầu
|
||||
[ ] W11 Có câu quy định ảnh vs bảng; (screen) mọi lệch prototype ↔ SRS nằm ở WF §5
|
||||
[ ] W12 Có mục "Ngoài phạm vi"
|
||||
[ ] W13 Sơ đồ dùng mermaid (không ASCII), và mỗi sơ đồ có bảng đi kèm
|
||||
[ ] W13 Sơ đồ theo chuẩn Archify: marker + spec validate pass + mermaid khớp spec + bảng đi kèm
|
||||
```
|
||||
|
||||
Quét W13: `grep -n '```mermaid' <file>` phải khớp số sơ đồ; `grep -nE '^\s*[┌└├│─▼]' <file>`
|
||||
phải rỗng.
|
||||
Quét W13: `node .claude/skills/ba-lifecycle/scripts/diagram-check.mjs --md <file>` phải exit 0
|
||||
(A0–A7 không 🔴). Không có Bash ⇒ tự kiểm: số marker `<!-- archify:` = số khối ` ```mermaid `;
|
||||
`grep -nE '^\s*[┌└├│─▼]' <file>` rỗng; ghi `humanInputNeeded` "chạy diagram-check".
|
||||
|
||||
In checklist này dạng bảng ☐/✅ ở cuối mỗi lần chạy skill. Mục chưa đạt ⇒ nói rõ thiếu gì,
|
||||
**không được đánh ✅ cho có**.
|
||||
|
||||
190
.claude/skills/ba-lifecycle/scripts/diagram-check.mjs
Normal file
190
.claude/skills/ba-lifecycle/scripts/diagram-check.mjs
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env node
|
||||
// diagram-check.mjs — phép kiểm máy cho sơ đồ theo chuẩn Archify (diagram-rules.md).
|
||||
// Chỉ dùng Node ≥ 18, không phụ thuộc gói ngoài. Không sửa file. Exit 1 khi có 🔴.
|
||||
//
|
||||
// node diagram-check.mjs --md <artifact.md> [--md ...] [--spec <x.json> ...]
|
||||
// [--archify <đường dẫn archify.mjs>] [--no-render] [--json]
|
||||
//
|
||||
// Phép kiểm trên mỗi khối ```mermaid trong .md:
|
||||
// A0 có marker <!-- archify: <type> · <spec.json> --> hoặc <!-- archify: mermaid-only -->
|
||||
// A1 spec JSON tồn tại, đúng diagram_type, meta.quality_profile = showcase, không subtitle/visual_preset
|
||||
// A2 `archify validate <type> <spec> --quality showcase` pass (0 lỗi composition)
|
||||
// A3 tập ID node trong Mermaid == tập ID trong spec (hai chiều)
|
||||
// A4 tập cạnh from>to trong Mermaid == spec (🔴 architecture/workflow/dataflow/lifecycle · 🟠 sequence)
|
||||
// A5 phong cách: không style/classDef/linkStyle/:::, ID ASCII, ≤ 12 node chính, không ASCII art
|
||||
// A6 W13: có bảng markdown đi kèm trong 40 dòng sau sơ đồ
|
||||
// A7 đã deliver: file .html cạnh spec tồn tại (bỏ qua với --no-render)
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const args = process.argv.slice(2)
|
||||
const opt = { md: [], spec: [] }
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i]
|
||||
if (a === '--md') opt.md.push(args[++i])
|
||||
else if (a === '--spec') opt.spec.push(args[++i])
|
||||
else if (a.startsWith('--')) { const k = a.slice(2); const nxt = args[i + 1]; if (nxt && !nxt.startsWith('--')) { opt[k] = nxt; i++ } else opt[k] = true }
|
||||
}
|
||||
if (!opt.md.length && !opt.spec.length) { console.error('Thiếu --md <file.md> hoặc --spec <file.json>'); process.exit(2) }
|
||||
const ARCHIFY = opt.archify || path.resolve(here, '../../archify/bin/archify.mjs')
|
||||
if (!fs.existsSync(ARCHIFY)) { console.error(`Không thấy Archify tại ${ARCHIFY} — dùng --archify <đường dẫn>`); process.exit(2) }
|
||||
|
||||
const TYPES = ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']
|
||||
const NODE_KEY = { architecture: 'components', workflow: 'nodes', sequence: 'participants', dataflow: 'nodes', lifecycle: 'states' }
|
||||
const REL_KEY = { architecture: 'connections', workflow: 'edges', sequence: 'messages', dataflow: 'flows', lifecycle: 'transitions' }
|
||||
|
||||
const out = []
|
||||
let red = 0
|
||||
const ok = (id, msg, details) => out.push({ id, status: '✅', msg, details })
|
||||
const warn = (id, msg, details) => out.push({ id, status: '🟠', msg, details })
|
||||
const bad = (id, msg, details) => { red++; out.push({ id, status: '🔴', msg, details }) }
|
||||
const read = (p) => fs.readFileSync(p, 'utf8')
|
||||
const idRe = /^[A-Za-z][A-Za-z0-9_-]*$/
|
||||
|
||||
// ---------- Mermaid parsing (đủ cho các mẫu chuẩn của bộ skill) ----------
|
||||
const stripLabels = (s) => {
|
||||
let prev
|
||||
do { prev = s; s = s.replace(/"[^"]*"/g, '').replace(/\[\[[^\]]*\]\]/g, '').replace(/\(\([^)]*\)\)/g, '').replace(/\(\[[^\]]*\]\)/g, '').replace(/\[\([^)]*\)\]/g, '').replace(/\[[^\[\]]*\]/g, '').replace(/\([^()]*\)/g, '').replace(/\{[^{}]*\}/g, '') } while (s !== prev)
|
||||
return s
|
||||
}
|
||||
const parseMermaid = (code) => {
|
||||
const lines = code.split('\n').map((l) => l.replace(/%%.*$/, '').trim()).filter(Boolean)
|
||||
const kind = (lines[0] || '').split(/\s+/)[0]
|
||||
const nodes = new Set(); const edges = new Set(); const subgraphs = new Set(); const styleHits = []
|
||||
const body = lines.slice(1)
|
||||
if (/^(flowchart|graph)$/.test(kind)) {
|
||||
for (const raw of body) {
|
||||
if (/^(style|classDef|linkStyle|click)\b/.test(raw) || raw.includes(':::')) { styleHits.push(raw); continue }
|
||||
const sg = raw.match(/^subgraph\s+([A-Za-z][A-Za-z0-9_-]*)/)
|
||||
if (sg) { subgraphs.add(sg[1]); continue }
|
||||
if (raw === 'end' || /^direction\b/.test(raw)) continue
|
||||
const clean = stripLabels(raw).replace(/\|[^|]*\|/g, ' ')
|
||||
const parts = clean.split(/\s*(?:<?-{1,3}[.=-]*>|-{2,3}|={2,3}>|-\.->|--[xo]|<-->|[.-]{2,}[>xo]?)\s*/).map((p) => p.trim()).filter(Boolean)
|
||||
const chain = parts.map((p) => p.split(/\s*&\s*/).map((t) => t.trim()).filter((t) => idRe.test(t)))
|
||||
chain.forEach((grp) => grp.forEach((n) => nodes.add(n)))
|
||||
for (let i = 0; i + 1 < chain.length; i++) for (const a of chain[i]) for (const b of chain[i + 1]) edges.add(`${a}>${b}`)
|
||||
}
|
||||
} else if (kind === 'sequenceDiagram') {
|
||||
for (const raw of body) {
|
||||
const p = raw.match(/^(?:participant|actor)\s+([A-Za-z][A-Za-z0-9_-]*)/)
|
||||
if (p) { nodes.add(p[1]); continue }
|
||||
const e = raw.match(/^([A-Za-z][A-Za-z0-9_]*)\s*-{1,2}(?:>>|>|x|\))\s*([A-Za-z][A-Za-z0-9_]*)\s*:/)
|
||||
if (e) { nodes.add(e[1]); nodes.add(e[2]); edges.add(`${e[1]}>${e[2]}`) }
|
||||
}
|
||||
} else if (/^stateDiagram/.test(kind)) {
|
||||
for (const raw of body) {
|
||||
if (/^(classDef|style)\b/.test(raw) || raw.includes(':::')) { styleHits.push(raw); continue }
|
||||
const s = raw.match(/^state\s+"[^"]*"\s+as\s+([A-Za-z][A-Za-z0-9_-]*)/)
|
||||
if (s) { nodes.add(s[1]); continue }
|
||||
const e = raw.match(/^(\[\*\]|[A-Za-z][A-Za-z0-9_-]*)\s*-->\s*(\[\*\]|[A-Za-z][A-Za-z0-9_-]*)/)
|
||||
if (e) { for (const n of [e[1], e[2]]) if (n !== '[*]') nodes.add(n); if (e[1] !== '[*]' && e[2] !== '[*]') edges.add(`${e[1]}>${e[2]}`) }
|
||||
}
|
||||
} else if (kind === 'erDiagram' || kind === 'classDiagram' || kind === 'quadrantChart') {
|
||||
for (const raw of body) if (/^(style|classDef)\b/.test(raw)) styleHits.push(raw)
|
||||
}
|
||||
return { kind, nodes, edges, subgraphs, styleHits, lineCount: lines.length }
|
||||
}
|
||||
|
||||
// ---------- Spec ----------
|
||||
const specIds = (spec) => {
|
||||
const t = spec.diagram_type
|
||||
const nodes = new Set((spec[NODE_KEY[t]] || []).map((n) => n.id))
|
||||
const edges = new Set((spec[REL_KEY[t]] || []).map((r) => `${r.from}>${r.to}`))
|
||||
return { nodes, edges }
|
||||
}
|
||||
const runValidate = (type, specPath) => {
|
||||
const r = spawnSync(process.execPath, [ARCHIFY, 'validate', type, specPath, '--quality', 'showcase', '--json'], { encoding: 'utf8' })
|
||||
let parsed = null
|
||||
try { parsed = JSON.parse(r.stdout) } catch { /* giữ null */ }
|
||||
const okRun = r.status === 0 && parsed && parsed.ok === true && (!parsed.composition || parsed.composition.status === 'pass')
|
||||
const diag = []
|
||||
if (parsed && Array.isArray(parsed.diagnostics)) for (const d of parsed.diagnostics.slice(0, 8)) diag.push(`${d.code || d.level || ''} ${d.subject || ''} ${d.message || ''}`.trim())
|
||||
if (!parsed) diag.push((r.stderr || r.stdout || '').trim().split('\n').slice(0, 6).join(' | '))
|
||||
return { okRun, diag, status: r.status }
|
||||
}
|
||||
const checkSpec = (label, specPath, expectType) => {
|
||||
if (!fs.existsSync(specPath)) { bad('A1', `${label}: không thấy spec ${specPath}`); return null }
|
||||
let spec
|
||||
try { spec = JSON.parse(read(specPath)) } catch (e) { bad('A1', `${label}: spec không phải JSON hợp lệ`, [String(e.message)]); return null }
|
||||
const t = spec.diagram_type
|
||||
if (!TYPES.includes(t)) { bad('A1', `${label}: diagram_type "${t}" không hợp lệ`); return null }
|
||||
if (expectType && t !== expectType) bad('A1', `${label}: marker nói "${expectType}" nhưng spec là "${t}"`)
|
||||
const m = spec.meta || {}
|
||||
if (m.quality_profile !== 'showcase') bad('A1', `${label}: meta.quality_profile phải là "showcase" (đang: ${m.quality_profile ?? 'thiếu'})`)
|
||||
else ok('A1', `${label}: spec ${path.basename(specPath)} · ${t} · showcase`)
|
||||
if (m.subtitle) warn('A1', `${label}: có meta.subtitle — chuẩn Archify mặc định bỏ subtitle`)
|
||||
if (m.visual_preset && m.visual_preset !== 'classic') warn('A1', `${label}: meta.visual_preset="${m.visual_preset}" — mặc định là classic, chỉ đổi khi người dùng yêu cầu`)
|
||||
if (m.legend && m.legend.mode && m.legend.mode !== 'auto') warn('A1', `${label}: meta.legend.mode="${m.legend.mode}" — mặc định auto`)
|
||||
const v = runValidate(t, specPath)
|
||||
if (v.okRun) ok('A2', `${label}: archify validate pass`)
|
||||
else bad('A2', `${label}: archify validate KHÔNG pass (exit ${v.status})`, v.diag)
|
||||
if (!opt['no-render']) {
|
||||
const html = specPath.replace(/\.json$/i, '.html')
|
||||
if (fs.existsSync(html)) ok('A7', `${label}: đã deliver ${path.basename(html)}`)
|
||||
else warn('A7', `${label}: chưa có ${path.basename(html)} — chạy \`archify deliver ${t} ${path.basename(specPath)} ${path.basename(html)} --quality showcase\``)
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
// ---------- Markdown ----------
|
||||
const MARKER = /<!--\s*archify:\s*(architecture|workflow|sequence|dataflow|lifecycle|mermaid-only)(?:\s*[·|,:]\s*([^\s>]+))?\s*-->/
|
||||
for (const md of opt.md) {
|
||||
if (!fs.existsSync(md)) { bad('A0', `Không thấy file ${md}`); continue }
|
||||
const text = read(md); const lines = text.split('\n'); const dir = path.dirname(md)
|
||||
const ascii = lines.map((l, i) => (/^\s*[┌└├│─▼┐┘┬┴┼]/.test(l) ? i + 1 : 0)).filter(Boolean)
|
||||
if (ascii.length) bad('A5', `${path.basename(md)}: ASCII art ở dòng ${ascii.slice(0, 5).join(', ')} — phải chuyển sang Mermaid + spec Archify`)
|
||||
let n = 0
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (!/^\s*```mermaid\s*$/.test(lines[i])) continue
|
||||
n++
|
||||
const start = i; let j = i + 1
|
||||
while (j < lines.length && !/^\s*```\s*$/.test(lines[j])) j++
|
||||
const code = lines.slice(i + 1, j).join('\n'); const label = `${path.basename(md)} sơ đồ #${n} (dòng ${start + 1})`
|
||||
// marker: trong 4 dòng không rỗng phía trước
|
||||
let marker = null
|
||||
for (let k = start - 1, seen = 0; k >= 0 && seen < 4; k--) { if (!lines[k].trim()) continue; seen++; const mm = lines[k].match(MARKER); if (mm) { marker = mm; break } }
|
||||
const pm = parseMermaid(code)
|
||||
if (pm.styleHits.length) bad('A5', `${label}: có style/classDef/linkStyle/::: — không tô màu, phân biệt bằng hình dạng và nhãn`, pm.styleHits.slice(0, 3))
|
||||
const nonAscii = [...pm.nodes].filter((x) => !/^[A-Za-z][A-Za-z0-9_-]*$/.test(x))
|
||||
if (nonAscii.length) bad('A5', `${label}: ID node không ASCII`, nonAscii)
|
||||
if (pm.nodes.size > 12) warn('A5', `${label}: ${pm.nodes.size} node chính > 12 — chuẩn Archify: một đường chính, ≤ 12 node; tách sơ đồ`)
|
||||
// W13: bảng đi kèm
|
||||
const after = lines.slice(j + 1, j + 41).join('\n')
|
||||
if (/\n\s*\|[\s:-]*-{3,}[\s:|-]*\|/.test('\n' + after)) ok('A6', `${label}: có bảng đi kèm (W13)`)
|
||||
else bad('A6', `${label}: không thấy bảng markdown trong 40 dòng sau sơ đồ (W13)`)
|
||||
if (!marker) { bad('A0', `${label}: thiếu marker <!-- archify: <type> · <spec.json> --> hoặc <!-- archify: mermaid-only -->`); i = j; continue }
|
||||
const type = marker[1]
|
||||
if (type === 'mermaid-only') {
|
||||
const allowed = ['erDiagram', 'classDiagram', 'quadrantChart', 'flowchart', 'graph']
|
||||
if (allowed.includes(pm.kind)) ok('A0', `${label}: mermaid-only (${pm.kind})`)
|
||||
else warn('A0', `${label}: mermaid-only nhưng loại ${pm.kind} có kiểu Archify tương ứng — nên có spec`)
|
||||
i = j; continue
|
||||
}
|
||||
if (!marker[2]) { bad('A0', `${label}: marker "${type}" thiếu đường dẫn spec JSON`); i = j; continue }
|
||||
ok('A0', `${label}: marker ${type} · ${marker[2]}`)
|
||||
const specPath = path.resolve(dir, marker[2])
|
||||
const spec = checkSpec(label, specPath, type)
|
||||
if (!spec) { i = j; continue }
|
||||
const si = specIds(spec)
|
||||
const onlyMd = [...pm.nodes].filter((x) => !si.nodes.has(x)); const onlySpec = [...si.nodes].filter((x) => !pm.nodes.has(x))
|
||||
if (!onlyMd.length && !onlySpec.length) ok('A3', `${label}: ${pm.nodes.size} node khớp hai chiều`)
|
||||
else bad('A3', `${label}: ID node lệch giữa Mermaid và spec`, [`chỉ Mermaid: ${onlyMd.join(', ') || '—'}`, `chỉ spec: ${onlySpec.join(', ') || '—'}`])
|
||||
const eMd = [...pm.edges].filter((x) => !si.edges.has(x)); const eSpec = [...si.edges].filter((x) => !pm.edges.has(x))
|
||||
if (!eMd.length && !eSpec.length) ok('A4', `${label}: ${pm.edges.size} cạnh khớp hai chiều`)
|
||||
else (type === 'sequence' ? warn : bad)('A4', `${label}: cạnh lệch giữa Mermaid và spec`, [`chỉ Mermaid: ${eMd.join(', ') || '—'}`, `chỉ spec: ${eSpec.join(', ') || '—'}`])
|
||||
i = j
|
||||
}
|
||||
if (!n) warn('A0', `${path.basename(md)}: không có khối mermaid nào`)
|
||||
}
|
||||
for (const s of opt.spec) checkSpec(path.basename(s), path.resolve(s), null)
|
||||
|
||||
// ---------- In ----------
|
||||
if (opt.json) console.log(JSON.stringify({ red, results: out }, null, 2))
|
||||
else {
|
||||
for (const r of out) { console.log(`${r.status} ${r.id} ${r.msg}`); if (r.details) for (const d of r.details) console.log(` · ${d}`) }
|
||||
console.log(`\n${red ? `🔴 ${red} lỗi` : '✅ Không có lỗi 🔴'} — ${out.filter((r) => r.status === '🟠').length} cảnh báo 🟠`)
|
||||
}
|
||||
process.exit(red ? 1 : 0)
|
||||
Reference in New Issue
Block a user