@educa-corp/fw 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  > `fw install` in các mục của những version mới hơn bản đang cài trong dự án.
4
4
  > Mỗi lần sửa lệnh, khuôn, bộ câu hỏi hay công cụ đều phải tăng version và thêm một mục ở đây. Test sẽ báo lỗi nếu version chưa có mục.
5
5
 
6
+ ## 0.11.1 — 2026-10-02
7
+
8
+ Sửa vòng lặp *review → change → refine → finding mới*. Lần chạy đầu trên LMS: review gợi ý đổi tên một trạng thái, dẫn tới `change` rồi refine ra 8 finding, tốn khoảng 4,4M token mà không có thay đổi nghiệp vụ nào.
9
+
10
+ - **`/prd review` không đưa ý kiến về nội dung**: không đề xuất đổi tên một khái niệm đang được gọi nhất quán, không gợi ý `/prd change`. Thuật ngữ chỉ đề xuất sửa khi cùng một khái niệm mà gọi bằng hai tên.
11
+ - **`--form` chặn một chỗ sửa thì AI hỏi bạn có đổi nghĩa không.** Không đổi nghĩa thì áp ngay, vẫn giữ version. Sửa chỗ trỏ "xem mục 5" → "mục 6" không còn bị chặn.
12
+ - **Refine sau `/prd change` chỉ báo lỗi do chính thay đổi gây ra**, cộng với critical. Vấn đề đã có ở bản đã duyệt được gom vào *"Có từ trước, để sau"*, không phải quyết, không chặn duyệt.
13
+ - **`spec_edit diff <prd>`**: in phần đã đổi so với bản `approved` gần nhất trong git, kèm số UC bị chạm. Refine và review sau change dùng lệnh này để lấy đúng phạm vi.
14
+
15
+ ## 0.11.0 — 2026-10-02
16
+
17
+ - **Chế độ mới `/prd <EPIC-ID> review`**: rà **hình thức** PRD (chính tả, câu chữ, thuật ngữ, đúng khuôn), chạy sau refine. **Bắt buộc**: PRD không duyệt được nếu chưa review đúng version (`reviewed` = `version`), và `/bdd` dừng nếu PRD chưa review. Review sửa **trong cùng version**, không tăng version, PRD đã `approved` vẫn giữ `approved`. Lỗi chính tả và định dạng thì AI sửa luôn. Câu chữ, thuật ngữ, dòng sai khuôn thì bạn quyết, được quyết theo nhóm. Chỗ muốn sửa mà làm đổi nghĩa thì AI chỉ ghi chú.
18
+ - PRD đã duyệt từ trước: chạy `/prd <EPIC-ID> review` một lần, rồi mới chạy `/bdd`.
19
+ - **`spec_edit edit --form`**: chặn mọi chỗ sửa làm đổi số, mã, dấu 🤖/✅, gạch ngang hoặc nguồn.
20
+ - **Refine rẻ hơn cho thay đổi nhỏ**: lượt 2, và lượt 1 sau `/prd change` chạm tối đa 3 UC, dùng **1 agent Opus gộp** cả 4 lăng kính thay vì 4 agent. Agent này vẫn đọc toàn bộ PRD. Lượt không có finding nào thì không gọi agent kiểm chứng. Lần đo trên LMS: thêm một dòng Nền tảng tốn 6,5M, ước tính còn khoảng 2,8M.
21
+ - **Nền tảng chỉ nhận mã `web` · `app` · `system`**. `system` dành cho luồng tự động, không có màn hình. Giải thích ghi sau dấu ` — `. Ghi bằng lời thì `bddcheck` báo lỗi và PRD không duyệt được. Trước bản này, dòng `Nền tảng: không có màn hình …` bị đọc thành tag rác mà vẫn cho duyệt.
22
+ - `spec_edit confirm` xác nhận được dòng không có mã, theo số dòng mà `pending` in ra (`L24`). `spec_edit context` ghi sẵn `reviewed` và nền tảng của UC ở dòng đầu.
23
+ - `/prd change` và `/prd refine` ghi rõ khuôn dòng Nền tảng và mã mục `history`, để AI không phải dò bằng `grep`.
24
+
6
25
  ## 0.10.0 — 2026-10-02
7
26
 
8
27
  - **Lệnh mới `/bdd <UC-ID>`**: viết kịch bản BDD cho một UC từ PRD đã duyệt, mỗi UC một file `{domain}/{slug}/bdd/UC-001.feature`. Có 2 lượt: lượt 1 trình dàn ý (kèm bảng nhánh của từng BR và các điểm giao nhận với UC khác), lượt 2 ghi file rồi kiểm độ phủ. Chỉ đọc đúng phần PRD của UC đó: với UC-001 của LMS là 15 KB thay vì 91 KB.
package/commands/bdd.md CHANGED
@@ -23,13 +23,15 @@ Tham số: `$ARGUMENTS`
23
23
 
24
24
  ## Bước 2 — Chọn chế độ
25
25
 
26
- Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đó.
26
+ Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đó. Dòng đầu của `SE context` đã ghi sẵn `status`, `reviewed` và nền tảng của UC.
27
27
 
28
28
  | Tình huống | Làm gì |
29
29
  |---|---|
30
30
  | UC đã bỏ trong PRD (tiêu đề gạch ngang) | Dừng: *"UC-xxx đã bỏ trong PRD (v…). Không viết BDD cho UC đã bỏ."* |
31
- | PRD chưa khai **Nền tảng**: không có dòng `Nền tảng` ở Tổng quan, cũng không có trong UC | Dừng: *"PRD chưa khai nền tảng. Chạy `/prd EP-xx change khai nền tảng: web …` trước. UC nào chạy khác mặc định thì nêu riêng."* |
31
+ | Nền tảng `chưa khai` | Dừng: *"PRD chưa khai nền tảng. Chạy `/prd EP-xx change khai nền tảng: web …` trước. UC nào chạy khác mặc định thì nêu riêng."* |
32
+ | Nền tảng `sai khuôn` (ghi bằng lời, không phải mã `web` · `app` · `system`) | Dừng: *"Dòng Nền tảng của UC-xxx sai khuôn. Chạy `/prd EP-xx review` để sửa."* |
32
33
  | PRD chưa `approved`, **không** có `--force` | Dừng: *"PRD chưa duyệt. Duyệt PRD trước, hoặc chạy `/bdd UC-xxx --force` để viết sớm (BDD sẽ không duyệt được cho tới khi PRD được duyệt)."* |
34
+ | PRD `approved` nhưng `reviewed` khác version, **không** có `--force` | Dừng: *"PRD v… chưa rà hình thức. Chạy `/prd EP-xx review` trước."* |
33
35
  | Không có `change`, chưa có `F` | Chế độ **tạo**: đọc `.fw/core/ref/bdd/writing.md` và `.fw/core/ref/bdd/new.md` |
34
36
  | Không có `change`, **đã có** `F` | Chế độ **bù**: đọc `.fw/core/ref/bdd/writing.md` và `.fw/core/ref/bdd/change.md` |
35
37
  | Có `change`, đã có `F` | Chế độ **đổi**: đọc `.fw/core/ref/bdd/writing.md` và `.fw/core/ref/bdd/change.md` |
@@ -52,7 +54,7 @@ Ngoài luật chung (`common.md`) và luật viết (`writing.md`):
52
54
  - `SE scenario F <mã SC | mã AC/BR>`: đọc kịch bản theo mã.
53
55
  - `SE sc F`: thay, thêm, bỏ kịch bản theo mã. Chỉ viết khối mới, không chép khối cũ.
54
56
  - `SE bddcheck F`: kiểm khuôn, tag và độ phủ.
55
- 5. **Duyệt.** `status=approved` chỉ đặt được khi `bddcheck` sạch, không còn `🤖`, PRD đang `approved`, `prd_version` của `F` bằng version của PRD, và có người duyệt. Hỏi *"Ai duyệt BDD này?"* (mặc định là người đã duyệt PRD), rồi đặt trong **một** lệnh: `SE set F status=approved approved_by=<tên> approved_at=YYYY-MM-DD`. `SE` tự chặn nếu chưa đủ.
57
+ 5. **Duyệt.** `status=approved` chỉ đặt được khi `bddcheck` sạch, không còn `🤖`, PRD đang `approved` và đã rà hình thức đúng version, `prd_version` của `F` bằng version của PRD, và có người duyệt. Hỏi *"Ai duyệt BDD này?"* (mặc định là người đã duyệt PRD), rồi đặt trong **một** lệnh: `SE set F status=approved approved_by=<tên> approved_at=YYYY-MM-DD`. `SE` tự chặn nếu chưa đủ.
56
58
 
57
59
  ## Bước 4 — Kết thúc
58
60
 
package/commands/prd.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: Viết PRD từ epic đã làm rõ (EPIC-ID), rà nội dung PRD (EPIC-ID refine), hoặc đổi PRD (EPIC-ID change)
3
- argument-hint: "<EPIC-ID> [refine | change <mô tả thay đổi>] [--force]"
2
+ description: Viết PRD từ epic đã làm rõ (EPIC-ID), rà nội dung (EPIC-ID refine), rà hình thức (EPIC-ID review), hoặc đổi PRD (EPIC-ID change)
3
+ argument-hint: "<EPIC-ID> [refine | review | change <mô tả thay đổi>] [--force | --full]"
4
4
  ---
5
5
 
6
6
  # /prd
@@ -11,6 +11,7 @@ Mục đích: biến epic đã làm rõ (`/product`) thành **PRD chính thức*
11
11
 
12
12
  - `/prd <EPIC-ID>`: tạo PRD từ epic `ready`.
13
13
  - `/prd <EPIC-ID> refine`: rà nội dung PRD qua 4 lăng kính. **Bắt buộc** trước khi duyệt.
14
+ - `/prd <EPIC-ID> review`: rà hình thức (chính tả, câu chữ, thuật ngữ, khuôn), sau refine. **Bắt buộc** trước khi duyệt.
14
15
  - `/prd <EPIC-ID> change <mô tả>`: thêm, sửa hoặc bỏ nội dung trong PRD đã có.
15
16
 
16
17
  Tham số: `$ARGUMENTS`
@@ -28,10 +29,11 @@ Tham số: `$ARGUMENTS`
28
29
  |---|---|
29
30
  | Không có `change`, chưa có `D`, epic `ready` | Chế độ **tạo**: đọc `.fw/core/ref/prd/new.md` |
30
31
  | Không có `change`, chưa có `D`, epic **chưa** `ready` | Dừng: *"EP-xx chưa làm rõ xong. Chạy `/product EP-xx` trước."* Có `--force` thì làm tiếp, nhưng in cảnh báo, ghi vào mục Câu hỏi còn mở của PRD dòng *"Tạo khi epic chưa ready (--force)"*, và không cho `approved` khi epic còn câu hỏi mở |
31
- | Không có `change` / `refine`, **đã có** `D` | Dừng: *"PRD đã có. Rà nội dung: `/prd EP-xx refine` · Đổi: `/prd EP-xx change <mô tả>`."* Không ghi đè |
32
+ | Không có `change` / `refine` / `review`, **đã có** `D` | Dừng: *"PRD đã có. Rà nội dung: `/prd EP-xx refine` · Rà hình thức: `/prd EP-xx review` · Đổi: `/prd EP-xx change <mô tả>`."* Không ghi đè |
32
33
  | Có `refine`, đã có `D` | Chế độ **rà nội dung**: đọc `.fw/core/ref/prd/refine.md` |
34
+ | Có `review`, đã có `D` | Chế độ **rà hình thức**: đọc `.fw/core/ref/prd/review.md` |
33
35
  | Có `change`, đã có `D` | Chế độ **đổi**: đọc `.fw/core/ref/prd/change.md` |
34
- | Có `change` hoặc `refine`, chưa có `D` | Dừng: *"Chưa có PRD. Chạy `/prd EP-xx` trước."* |
36
+ | Có `change`, `refine` hoặc `review`, chưa có `D` | Dừng: *"Chưa có PRD. Chạy `/prd EP-xx` trước."* |
35
37
 
36
38
  ## Bước 3 — Luật riêng của PRD
37
39
 
@@ -39,8 +41,8 @@ Ngoài luật chung (`common.md`):
39
41
  1. **Mã ổn định.** `UC-NNN` đánh số trên toàn sản phẩm. `UC-NNN-BRnn`, `UC-NNN-ACnn` đánh số trong từng UC. Mã đã cấp thì **không bao giờ đổi, không dùng lại**. Thêm mới thì lấy số kế tiếp trong UC đó. Mục bỏ đi thì giữ dòng, gạch ngang nội dung, và ghi *"Đã bỏ (v…)"*.
40
42
  2. **Nguồn.** Mọi BR và AC phải ghi `(nguồn: …)`, và **chỉ ghi mã**: mã epic (`BR3`, `AC1`), ràng buộc (`CON-02`), hoặc `PRD` (thêm khi viết PRD, PO đã duyệt). Không ghi lời giải thích. `SE` chặn nếu sai.
41
43
  3. **Dấu.** Nội dung chuyển nguyên ý từ mục `✅` của epic thì giữ `✅`. Chỗ nào AI **tự thêm, tách hoặc suy ra** thì gắn `🤖`.
42
- 4. **Lệnh `SE` riêng của PRD:** `SE next-uc {specs}` (mã UC kế tiếp) · `SE coverage <epic> <prd>` (không rơi BR/AC nào của epic) · `SE flowcheck <prd>` (mọi UC có mặt trong User flow).
43
- 5. **Duyệt.** `status=approved` chỉ đặt được khi không còn `🤖`, `open_questions=0`, User flow đủ, **đã refine đúng version này** (`refined` = `version`, không còn critical/major chưa quyết), và có người duyệt. Chưa refine thì báo *"Chạy `/prd EP-xx refine` trước khi duyệt."* Hỏi PO *"Ai duyệt PRD này?"* (mặc định là PO của epic), rồi đặt trong **một** lệnh: `SE set D status=approved approved_by=<tên> approved_at=YYYY-MM-DD`. `SE` tự chặn nếu chưa đủ.
44
+ 4. **Lệnh `SE` riêng của PRD:** `SE next-uc {specs}` (mã UC kế tiếp) · `SE coverage <epic> <prd>` (không rơi BR/AC nào của epic) · `SE flowcheck <prd>` (mọi UC có mặt trong User flow) · `SE edit --form <prd>` (sửa hình thức, chặn mọi chỗ đổi số, mã, dấu, nguồn).
45
+ 5. **Duyệt.** `status=approved` chỉ đặt được khi không còn `🤖`, `open_questions=0`, User flow đủ, **đã refine đúng version này** (`refined` = `version`, không còn critical/major chưa quyết), **đã rà hình thức đúng version này** (`reviewed` = `version`), mọi UC có **Nền tảng** đúng khuôn, và có người duyệt. Chưa refine thì báo *"Chạy `/prd EP-xx refine` trước."* Chưa review thì báo *"Chạy `/prd EP-xx review` trước."* Hỏi PO *"Ai duyệt PRD này?"* (mặc định là PO của epic), rồi đặt trong **một** lệnh: `SE set D status=approved approved_by=<tên> approved_at=YYYY-MM-DD`. `SE` tự chặn nếu chưa đủ.
44
46
 
45
47
  ## Bước 4 — Kết thúc
46
48
 
@@ -52,5 +54,5 @@ Trạng thái : {✅ Đã duyệt v{version} | 🟡 Nháp v{version} — còn {n
52
54
  Đã ghi : {D} {· epic → handed-off · product.md nếu có}
53
55
  Kiểm : coverage {đủ | thiếu …} · user flow {đủ | thiếu …} · {số UC} UC · {số BR} BR · {số AC} AC
54
56
  Luồng : Product → [PRD ◀ bạn ở đây] → BDD → TDD · Design-spec → Code → Test → QC
55
- Bước tiếp : {`/clear` rồi `/prd {id} refine` | /bdd UC-xxx | trả lời / xác nhận các mục trên}
57
+ Bước tiếp : {`/clear` rồi `/prd {id} refine` | `/clear` rồi `/prd {id} review` | /bdd UC-xxx | trả lời / xác nhận các mục trên}
56
58
  ```
@@ -27,7 +27,7 @@ Chỉ những khái niệm bạn **gặp khi dùng** các lệnh hiện có.
27
27
  | **Ràng buộc** | Điều kiện áp cho **mọi epic**, mã `CON-01`… | CON-01: Audit log không được xoá hay sửa |
28
28
  | **User flow** (hành trình) | Đường đi của một actor qua nhiều UC, có rẽ nhánh và nhánh lỗi, vẽ bằng sơ đồ trong PRD. Mã `J1`, `J2`… | J1 Nhân sự mới vào hệ thống: UC-001 → UC-003 → UC-005 |
29
29
  | **Kịch bản BDD** (scenario) | Một hành vi cụ thể của hệ thống, viết dạng *Given / When / Then*, dev viết code và test theo đó. Mỗi UC có một file `.feature` | UC-001-SC01: Import có dòng trùng email với tài khoản đã có |
30
- | **Nền tảng** | Nơi người dùng dùng tính năng: `web`, `app`… Khai ở Tổng quan của PRD, UC nào khác thì ghi riêng | Nền tảng: web · app |
30
+ | **Nền tảng** | Nơi người dùng dùng tính năng: `web`, `app`, hoặc `system` cho luồng tự động không có màn hình. Khai ở Tổng quan của PRD, UC nào khác thì ghi riêng | Nền tảng: web · app. UC-009: system — nhận đơn từ EP-03 |
31
31
  | **Checkpoint** | Một điểm dừng để bạn chốt. `/product` có 2 (sản phẩm) hoặc 3 (epic) checkpoint | — |
32
32
 
33
33
  ## Mã ổn định
@@ -1,7 +1,7 @@
1
1
  # Hướng dẫn sử dụng `@educa-corp/fw`
2
2
 
3
3
  > Framework làm việc với Claude Code: đưa một tính năng đi từ **ý tưởng → làm rõ yêu cầu → PRD → …**, AI hỏi và viết, con người xác nhận ở mỗi bước.
4
- > Hướng dẫn này viết cho **bản 0.10.0**. Chỉ mô tả những lệnh **đã có**. Lệnh mới có thì hướng dẫn mới được bổ sung.
4
+ > Hướng dẫn này viết cho **bản 0.11.1**. Chỉ mô tả những lệnh **đã có**. Lệnh mới có thì hướng dẫn mới được bổ sung.
5
5
  > Trong dự án đã cài framework, bản hướng dẫn đúng với version đang dùng nằm ở **`.fw/guide/`**. Có thể hỏi Claude: *"Đọc .fw/guide và cho biết cách đổi PRD"*.
6
6
 
7
7
  ---
@@ -22,10 +22,11 @@
22
22
  ## Pipeline hiện có
23
23
 
24
24
  ```
25
- cài framework ──► /product ──► /product EP-xx ──► /prd EP-xx ──► /prd EP-xx refine ──► duyệt ──► /bdd UC-xxx ──► duyệt
26
- tầng làm rõ một viết PRD rà nội dung mỗi UC một
27
- sản phẩm tính năng ▲ │ (bắt buộc) file kịch bản
28
- └── /prd EP-xx change ◄──┘ đổi PRD thì refine lại, rồi /bdd UC-xxx để bù
25
+ cài framework ──► /product ──► /product EP-xx ──► /prd EP-xx ──► refine ──► review ──► duyệt ──► /bdd UC-xxx ──► duyệt
26
+ tầng làm rõ một viết PRD rà nội rà hình mỗi UC một
27
+ sản phẩm tính năng ▲ dung thức file kịch bản
28
+ │ (cả hai bắt buộc)
29
+ └── /prd EP-xx change: refine, review lại, duyệt, rồi /bdd UC-xxx để bù
29
30
  ```
30
31
 
31
32
  Mỗi lệnh kết thúc bằng một khối giống nhau. Bạn chỉ cần đọc dòng **Bước tiếp**:
@@ -50,6 +51,7 @@ Bước tiếp : `/clear` rồi `/product EP-01`
50
51
  | `/product <EPIC-ID>` | PO / BA | **Làm rõ yêu cầu** một tính năng trước khi viết PRD | [/product](lenh/product.md) |
51
52
  | `/prd <EPIC-ID>` | PO / BA | Viết **PRD chính thức** từ epic đã làm rõ | [/prd](lenh/prd.md) |
52
53
  | `/prd <EPIC-ID> refine` | PO / BA | **Rà nội dung** PRD qua 4 lăng kính. **Bắt buộc** trước khi duyệt | [/prd refine](lenh/prd-refine.md) |
54
+ | `/prd <EPIC-ID> review` | PO / BA | **Rà hình thức**: chính tả, câu chữ, thuật ngữ, đúng khuôn. Chạy sau refine. **Bắt buộc** trước khi duyệt | [/prd review](lenh/prd-review.md) |
53
55
  | `/prd <EPIC-ID> change <mô tả>` | PO / BA | Thêm, sửa hoặc bỏ nội dung PRD | [/prd](lenh/prd.md) |
54
56
  | `/bdd <UC-ID>` | PO / BA | Viết **kịch bản BDD** cho một UC. Đã có thì **bù** phần PRD mới đổi | [/bdd](lenh/bdd.md) |
55
57
  | `/bdd <UC-ID> change <mô tả>` | PO / BA · QC · Dev | Thêm, sửa, bỏ kịch bản khi chưa đủ ý | [/bdd](lenh/bdd.md#đổi-bdd-uc-001-change-mô-tả) |
@@ -9,7 +9,7 @@
9
9
  | **Gõ** | `/bdd UC-001` | `/bdd UC-001` | `/bdd UC-001 change <mô tả>` |
10
10
  | **Khi nào** | Chưa có BDD cho UC này | Đã có BDD, PRD vừa đổi | Kịch bản có nhưng **chưa đủ ý** |
11
11
  | **Ai** | PO / BA | PO / BA | PO / BA, hoặc QC / dev báo thiếu |
12
- | **Cần có trước** | PRD `approved`, có dòng **Nền tảng** | như bên trái | BDD đã có |
12
+ | **Cần có trước** | PRD `approved`, đã [review](prd-review.md) đúng version, có dòng **Nền tảng** | như bên trái | BDD đã có |
13
13
  | **Số lượt** | 2 | 2 | 2 |
14
14
  | **Ghi ra** | `specs/{domain}/{slug}/bdd/UC-001.feature` | cùng file, version tăng | cùng file, version tăng |
15
15
 
@@ -54,7 +54,7 @@ Dòng `@…` là **tag**. Mỗi kịch bản có đủ bốn loại tag:
54
54
  | Tag | Nghĩa |
55
55
  |---|---|
56
56
  | `@UC-001-SC01` | Mã kịch bản. Không bao giờ đổi. Code và test trỏ về kịch bản bằng mã này |
57
- | `@web` `@app` | Nền tảng. Hành vi giống nhau thì một kịch bản mang cả hai tag. Hành vi khác nhau thì tách thành hai kịch bản |
57
+ | `@web` `@app` `@system` | Nền tảng. Hành vi giống nhau thì một kịch bản mang cả hai tag, hành vi khác nhau thì tách thành hai kịch bản. `@system` là luồng tự động, không có màn hình, ví dụ UC-009 nhận đơn từ EP-03 |
58
58
  | `@happy` `@alternative` `@edge` `@negative` | Loại: thành công · luồng khác vẫn thành công · biên, giới hạn · bị từ chối, lỗi |
59
59
  | `@UC-001-AC01` `@UC-001-BR04` | AC và BR mà kịch bản này kiểm |
60
60
 
@@ -12,7 +12,7 @@
12
12
  | **Cần có trước** | PRD đã có |
13
13
  | **Số lượt rà** | Tối đa **2** cho mỗi version PRD |
14
14
  | **Ghi ra** | `specs/{domain}/{slug}/review/prd-refine.md` · PRD nhận các bản sửa bạn đã chấp nhận |
15
- | **Bước tiếp** | Duyệt PRD |
15
+ | **Bước tiếp** | [`/prd EP-01 review`](prd-review.md) (rà hình thức) → duyệt PRD |
16
16
 
17
17
  ---
18
18
 
@@ -27,7 +27,7 @@
27
27
 
28
28
  *(Các ví dụ trên là ví dụ minh hoạ.)*
29
29
 
30
- Mỗi lăng kính là một agent riêng, chạy song song, **bắt buộc dùng model Opus**, và đọc **toàn bộ PRD**. Sau đó có thêm **một agent kiểm chứng**: nó **cố chứng minh từng finding là sai** bằng cách tra lại PRD, rồi mới gộp trùng và sắp xếp. Finding nào không đứng vững thì bị bỏ, finding nào được trả lời một phần thì được thu hẹp. Đây là lớp chặn chính đối với các finding do AI ảo giác.
30
+ Lần rà toàn bộ PRD: mỗi lăng kính là một agent riêng, chạy song song, **bắt buộc dùng model Opus**, và đọc **toàn bộ PRD**. Lượt 2, và lượt 1 sau một thay đổi nhỏ (chạm tối đa 3 UC), chỉ dùng **một agent Opus soi cả 4 lăng kính**. Agent này vẫn đọc toàn bộ PRD, nên vẫn thấy được ảnh hưởng lan sang UC khác. Sau đó có thêm **một agent kiểm chứng**: nó **cố chứng minh từng finding là sai** bằng cách tra lại PRD, rồi mới gộp trùng và sắp xếp. Finding nào không đứng vững thì bị bỏ, finding nào được trả lời một phần thì được thu hẹp. Đây là lớp chặn chính đối với các finding do AI ảo giác.
31
31
 
32
32
  Khi bạn quyết xong, bảng Tóm tắt trong `prd-refine.md` ghi số **nhận · sửa · bác · hoãn**. **Tỉ lệ bác cao** nghĩa là lớp kiểm chứng còn lỏng. Hãy báo người phụ trách framework nếu bạn thấy vậy.
33
33
 
@@ -71,14 +71,16 @@ Sau mỗi lượt có sửa, lệnh sẽ nhắc *"`/clear` rồi `/prd EP-01 ref
71
71
 
72
72
  ## Sau khi đổi PRD
73
73
 
74
- `/prd EP-01 change …` tăng version, nên bạn phải refine lại trước khi duyệt. Lần đó **chỉ rà các mục vừa đổi** (theo dòng Lịch sử thay đổi), nên nhanh và rẻ.
74
+ `/prd EP-01 change …` tăng version, nên bạn phải refine lại trước khi duyệt. Lần đó **chỉ soi đúng phần vừa đổi** so với bản đã duyệt gần nhất, và **chỉ báo lỗi do chính thay đổi gây ra** (cộng với critical). Lỗi đã có từ trước trong mục bị chạm chỉ được ghi vào *"Có từ trước, để sau"*, không cần bạn quyết. Agent vẫn đọc cả PRD để thấy ảnh hưởng lan sang chỗ khác. Vì thế PRD càng lớn thì mỗi lần đổi càng đắt: với PRD 91 KB của LMS, thêm một dòng tốn khoảng 6M token trước bản 0.11.0, và ước tính khoảng 3M từ bản 0.11.0. Lượt nào không có finding nào thì không cần agent kiểm chứng. Cách giảm chi phí hiệu quả nhất vẫn là **tách epic** để PRD nhỏ lại.
75
75
 
76
76
  ## Duyệt PRD
77
77
 
78
- PRD chỉ được đặt `approved` khi đủ cả 4 điều kiện:
78
+ PRD chỉ được đặt `approved` khi đủ cả 6 điều kiện:
79
79
  1. Không còn 🤖.
80
80
  2. Không còn câu hỏi mở.
81
81
  3. **Đã refine đúng version này** (`refined` = `version`), và không còn critical/major chưa quyết.
82
- 4. Đã ghi người duyệt.
82
+ 4. **Đã rà hình thức đúng version này** (`reviewed` = `version`), xem [/prd review](prd-review.md).
83
+ 5. Mọi UC có **Nền tảng** đúng khuôn.
84
+ 6. Đã ghi người duyệt.
83
85
 
84
86
  Công cụ tự chặn nếu thiếu điều kiện nào.
@@ -0,0 +1,55 @@
1
+ [← Hướng dẫn](../README.md) · [Bảng lệnh](../README.md#bảng-lệnh)
2
+
3
+ # `/prd EP-01 review` — rà hình thức PRD
4
+
5
+ > AI rà **câu chữ, chính tả, thuật ngữ, trình bày và đúng khuôn**, để người và AI ở các bước sau (BDD, thiết kế, code) đọc đúng ý. **Bắt buộc**: chưa review thì không duyệt được PRD, và `/bdd` không chạy.
6
+ > Không rà nội dung nghiệp vụ, vì đó là việc của [refine](prd-refine.md).
7
+
8
+ | | |
9
+ |---|---|
10
+ | **Gõ** | `/prd EP-01 review` · rà lại toàn bộ: `/prd EP-01 review --full` |
11
+ | **Ai** | PO / BA |
12
+ | **Cần có trước** | Đã [refine](prd-refine.md) xong đúng version này. **Review luôn đi sau refine**, vì refine làm đổi câu chữ |
13
+ | **Số lượt** | 1–2: xem đề xuất → quyết |
14
+ | **Ghi ra** | Sửa trong `prd.md`, **giữ nguyên version** · trường `reviewed` · một dòng Lịch sử thay đổi |
15
+ | **Bước tiếp** | Duyệt PRD → [`/bdd UC-xxx`](bdd.md) |
16
+
17
+ ---
18
+
19
+ ## AI tìm ra ba loại
20
+
21
+ | Loại | Gồm | Ai quyết |
22
+ |---|---|---|
23
+ | **A. Tự sửa** | Chính tả, dấu tiếng Việt, dấu câu, bảng vỡ cột, danh sách lệch | AI sửa luôn, báo lại cho bạn số chỗ đã sửa |
24
+ | **B. Đề xuất** | Câu khó hiểu (quá dài, đại từ không rõ chỉ ai, phủ định kép) · một khái niệm gọi hai tên, hoặc sai glossary · dòng sai khuôn, ví dụ dòng Nền tảng ghi bằng lời | **Bạn quyết** |
25
+ | **C. Ghi chú** | Lỗi hình thức mà sửa thì chắc chắn đổi nghĩa | Không sửa, không chặn duyệt. Chỉ để bạn biết |
26
+
27
+ Bạn quyết loại B theo mã, hoặc theo nhóm:
28
+
29
+ ```
30
+ nhận hết
31
+ nhận hết trừ R03
32
+ R04 sửa thành: "Admin mở lại tài khoản thì đếm lại 7 ngày"
33
+ bác R05
34
+ ```
35
+
36
+ ## Review không bao giờ làm đổi nghĩa
37
+
38
+ - **Không tăng version.** PRD đang `approved` thì vẫn giữ `approved`.
39
+ - Công cụ **tự chặn** mọi chỗ sửa làm thay đổi **số, mã** (UC, BR, AC…), **dấu 🤖 / ✅, gạch ngang hoặc nguồn**. Ví dụ AI định sửa *"tối đa 1.000 dòng"* thành *"tối đa 2.000 dòng"* thì công cụ chặn.
40
+ - Bị chặn mà thật ra không đổi nghĩa, ví dụ thêm mã `EP-03` vào tiêu đề cho khớp với bảng, thì **AI hỏi bạn**. Bạn trả lời *"không đổi nghĩa"* thì AI áp luôn, vẫn trong cùng version.
41
+ - **Review không đề xuất đổi tên khái niệm** đang được gọi nhất quán, và không gợi ý `/prd change`. Đặt tên là việc của nghiệp vụ. Lần đầu chạy ở LMS, review gợi ý đổi tên một trạng thái; lần đổi đó kéo theo refine 8 finding, tốn khoảng 4M token, và tên mới bị refine chỉ ra là sai nghĩa.
42
+ - Nhờ vậy, review không cần refine lại sau đó.
43
+
44
+ ## Phạm vi
45
+
46
+ | Khi nào | Rà gì |
47
+ |---|---|
48
+ | PRD chưa từng review | Toàn bộ PRD |
49
+ | Sau `/prd EP-01 change` → refine | Chỉ phần vừa đổi so với bản đã duyệt gần nhất |
50
+
51
+ Thứ tự sau mỗi lần đổi PRD: `change` → `refine` → `review` → duyệt.
52
+
53
+ ## PRD đã duyệt trước bản 0.11.0
54
+
55
+ PRD vẫn ở trạng thái `approved`, nhưng `/bdd` sẽ dừng và báo *"PRD chưa rà hình thức"*. Bạn chạy `/prd EP-01 review` một lần, rà toàn bộ PRD. Lần đó cũng sửa luôn dòng Nền tảng ghi sai khuôn nếu có, ví dụ dòng của UC-009 trong LMS.
@@ -11,7 +11,7 @@
11
11
  | **Cần có trước** | Epic `ready` (xem [/product](product.md)) | PRD đã có |
12
12
  | **Số lượt** | 2 | 2 |
13
13
  | **Ghi ra** | `specs/{domain}/{slug}/prd.md` · epic chuyển `handed-off` | `prd.md`, version tăng (1.0 → 1.1) |
14
- | **Bước tiếp** | [`/prd EP-01 refine`](prd-refine.md) → duyệt PRD → [`/bdd UC-xxx`](bdd.md) | [`/prd EP-01 refine`](prd-refine.md) (chỉ rà phần vừa đổi) → duyệt lại → [`/bdd UC-xxx`](bdd.md) để bù |
14
+ | **Bước tiếp** | [`refine`](prd-refine.md) → [`review`](prd-review.md) → duyệt PRD → [`/bdd UC-xxx`](bdd.md) | [`refine`](prd-refine.md) → [`review`](prd-review.md) (chỉ phần vừa đổi) → duyệt lại → [`/bdd UC-xxx`](bdd.md) để bù |
15
15
 
16
16
  ---
17
17
 
@@ -34,7 +34,7 @@ Bạn đồng ý, gộp hoặc tách UC.
34
34
  2. Công cụ **kiểm độ phủ**: mọi BR và AC của epic đều phải có trong PRD. Thiếu thì AI bổ sung cho tới khi đủ. AI không được tự bỏ mục nào; muốn bỏ thì phải hỏi bạn.
35
35
  3. AI **chỉ in các mục 🤖**, tức những chỗ AI tự thêm, tách hoặc suy ra khi viết PRD. Không in lại cả PRD.
36
36
  4. Bạn mở file để đọc toàn bộ, rồi trả lời hoặc xác nhận theo mã: *"OK UC-003-BR04, UC-003-AC02"*.
37
- 5. Tiếp theo **bắt buộc rà nội dung**: `/clear` rồi [`/prd EP-01 refine`](prd-refine.md). Rà xong, hết 🤖 và hết câu hỏi mở thì mới duyệt được. AI hỏi *"Ai duyệt PRD này?"* rồi đặt `status: approved`.
37
+ 5. Tiếp theo **bắt buộc rà nội dung**: `/clear` rồi [`/prd EP-01 refine`](prd-refine.md). Sau đó **bắt buộc rà hình thức**: `/clear` rồi [`/prd EP-01 review`](prd-review.md). Xong cả hai, hết 🤖 và hết câu hỏi mở thì mới duyệt được. AI hỏi *"Ai duyệt PRD này?"* rồi đặt `status: approved`.
38
38
 
39
39
  ## PRD trông như thế nào
40
40
 
@@ -91,7 +91,7 @@ Dùng cho **mọi** thay đổi: thêm, sửa, bỏ.
91
91
 
92
92
  ## Lưu ý
93
93
 
94
- - **Nền tảng** (`web`, `app`…) ghi ở Tổng quan và là mặc định cho mọi UC. UC nào chạy khác thì ghi dòng riêng trong UC. Thiếu dòng này thì PRD không duyệt được, vì BDD cần nó để tách kịch bản theo nền tảng. PRD tạo bằng bản trước 0.10.0: `/prd EP-01 change khai nền tảng: web`.
94
+ - **Nền tảng** chỉ ghi mã `web`, `app` hoặc `system` (luồng tự động, không có màn hình), giải thích để sau dấu ` — `. Dòng này ghi ở Tổng quan và là mặc định cho mọi UC. UC nào chạy khác thì ghi dòng riêng trong UC. Thiếu dòng này thì PRD không duyệt được, vì BDD cần nó để tách kịch bản theo nền tảng. PRD tạo bằng bản trước 0.10.0: `/prd EP-01 change khai nền tảng: web`.
95
95
  - **Bạn được tự sửa tay PRD.** Nhưng giữ nguyên mã, dấu ✅ / 🤖 và đoạn `<!-- sec:… -->`. Sửa nhiều thì nên dùng `change`, để AI tìm giúp các mục bị kéo theo.
96
96
  - **Không sửa epic sau khi đã có PRD.** Epic lúc này chỉ còn là lịch sử.
97
97
  - PRD tạo bằng bản framework cũ (trước 0.6.1) có thể còn ghi nguồn bằng lời, ví dụ *"(nguồn: PO chốt khi viết PRD)"*. Lần sửa đầu tiên AI sẽ được yêu cầu đổi các chỗ đó thành mã, thường là `PRD`.
@@ -7,9 +7,9 @@
7
7
  ## Đường đi của bạn
8
8
 
9
9
  ```
10
- /product ──► /product EP-xx ──► /prd EP-xx ──► /prd EP-xx refine ──► duyệt PRD ──► /bdd UC-xxx ──► duyệt BDD
11
- ▲ │ (từng UC)
12
- └─ /prd EP-xx change ◄┘ khi yêu cầu thay đổi, rồi /bdd UC-xxx để bù
10
+ /product ──► /product EP-xx ──► /prd EP-xx ──► refine ──► review ──► duyệt PRD ──► /bdd UC-xxx ──► duyệt BDD
11
+ ▲ (từng UC)
12
+ └─ /prd EP-xx change: refine, review lại, duyệt lại, rồi /bdd UC-xxx để bù
13
13
  ```
14
14
 
15
15
  | Bước | Lệnh | Bạn làm gì | Hướng dẫn |
@@ -17,9 +17,10 @@
17
17
  | 1 | `/product` | Trả lời về sản phẩm, nhóm người dùng, epic, ràng buộc. Chốt từng mục | [/product](../lenh/product.md) |
18
18
  | 2 | `/product EP-xx` | Trả lời AI hỏi về tính năng. Đây là chỗ **quan trọng nhất**: câu trả lời càng rõ thì PRD càng ít phải sửa | [/product](../lenh/product.md#một-epic-product-ep-01) |
19
19
  | 3 | `/prd EP-xx` | Duyệt cách chia UC, xác nhận các mục 🤖 | [/prd](../lenh/prd.md) |
20
- | 4 | `/prd EP-xx refine` | Quyết từng finding: nhận, sửa, bác hay hoãn. Tối đa 2 lượt. Xong thì duyệt PRD | [/prd refine](../lenh/prd-refine.md) |
20
+ | 4 | `/prd EP-xx refine` | Quyết từng finding: nhận, sửa, bác hay hoãn. Tối đa 2 lượt | [/prd refine](../lenh/prd-refine.md) |
21
+ | 4b | `/prd EP-xx review` | Xem các đề xuất sửa câu chữ, quyết theo nhóm (*"nhận hết trừ R03"*). Xong thì duyệt PRD | [/prd review](../lenh/prd-review.md) |
21
22
  | 5 | `/bdd UC-xxx` | Với từng UC: duyệt dàn ý kịch bản (soát xem có sót nhánh nào không), xác nhận các mục 🤖, duyệt BDD | [/bdd](../lenh/bdd.md) |
22
- | 6 | `/prd EP-xx change …` | Khi yêu cầu đổi. Duyệt kế hoạch thay đổi, refine lại phần vừa đổi, duyệt lại PRD, rồi `/bdd UC-xxx` để bù | [/prd](../lenh/prd.md#đổi-prd-prd-ep-01-change-mô-tả) |
23
+ | 6 | `/prd EP-xx change …` | Khi yêu cầu đổi. Duyệt kế hoạch thay đổi, refine và review lại phần vừa đổi, duyệt lại PRD, rồi `/bdd UC-xxx` để bù | [/prd](../lenh/prd.md#đổi-prd-prd-ep-01-change-mô-tả) |
23
24
 
24
25
  ## Cách làm thường gặp
25
26
 
@@ -28,6 +28,9 @@
28
28
  | `Bản v1.1 đã refine xong` | Gọi refine lại khi PRD chưa đổi | Không cần làm gì. Thật sự muốn rà lại từ đầu thì thêm `--full` |
29
29
  | `chưa được PRD dùng tới: BR7, AC12` | Có BR hoặc AC của epic chưa được đưa vào PRD | AI sẽ bổ sung. Nếu bạn muốn bỏ thì nói rõ |
30
30
  | `(nguồn: …) chỉ được ghi mã …` | PRD đang ghi nguồn bằng lời (thường là PRD tạo bằng bản trước 0.6.1) | AI sẽ đổi thành mã, thường là `PRD` |
31
+ | `… chưa rà hình thức (reviewed=…)` | PRD chưa qua `/prd … review` ở version này | Chạy `/prd EP-xx review`. PRD đã duyệt trước bản 0.11.0 vẫn giữ `approved`, nhưng `/bdd` sẽ dừng cho tới khi review xong |
32
+ | `Nền tảng ghi sai khuôn ở UC-009` | Dòng Nền tảng ghi bằng lời thay vì mã | `/prd EP-xx review` sẽ đề xuất sửa thành `system — …` (hoặc `web`, `app`) |
33
+ | `chỗ sửa #2 không phải sửa hình thức: mất 1.000 · thêm 2.000` | Lúc review, có một chỗ sửa làm đổi số, mã, dấu hoặc nguồn | Đó là đổi nghĩa. AI bỏ chỗ đó và ghi thành ghi chú. Muốn đổi thật thì dùng `/prd EP-xx change` |
31
34
  | `chưa khai Nền tảng cho UC-001, UC-002` | PRD chưa có dòng Nền tảng (thường là PRD tạo bằng bản trước 0.10.0) | Chạy `/prd EP-xx change khai nền tảng: web` (hoặc `web · app`) |
32
35
  | `AC/BR của UC-001 chưa có kịch bản nào: …` | BDD còn sót AC hoặc BR | AI sẽ bổ sung. Đã duyệt BDD rồi mà PRD mới thêm mục thì chạy `/bdd UC-001` để bù |
33
36
  | `@UC-001-BR08 đã bỏ trong PRD` | Kịch bản trỏ tới mục PRD vừa bỏ | Chạy `/bdd UC-001`: AI đề xuất sửa hoặc bỏ kịch bản đó |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@educa-corp/fw",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Framework làm việc với Claude Code cho phòng PTPM",
5
5
  "bin": {
6
6
  "fw": "bin/fw.js"
@@ -26,6 +26,7 @@
26
26
  2. Hành vi **khác nhau**, tức kết quả khác nhau theo PRD: tách thành kịch bản riêng, mỗi kịch bản một mã. Ví dụ `UC-003-SC08 @web` và `UC-003-SC09 @app`.
27
27
  3. Khác nhau chỉ ở giao diện hay thao tác (bố cục, cử chỉ) thì **không** tách, vì đó là việc của design-spec.
28
28
  4. Chỉ dùng nền tảng mà UC khai (dòng `Nền tảng` trong UC, không có thì lấy ở Tổng quan).
29
+ 5. **`@system`** dành cho luồng tự động, không có màn hình: job chạy định kỳ, tích hợp với hệ thống khác. `When` là một sự kiện, ví dụ *"EP-03 chuyển sang đơn hàng của học sinh "Lan""* hoặc *"đã đủ 7×24 giờ kể từ lúc tạo"*. `Then` là kết quả nhìn thấy được trong dữ liệu, trong email gửi đi, hoặc trong thông báo trả về hệ thống kia.
29
30
 
30
31
  ## 4. Điểm giao nhận với UC khác
31
32
 
package/ref/common.md CHANGED
@@ -36,7 +36,7 @@ Gọi là `SE` = `python .fw/core/tools/spec_edit.py` (hoặc `python3` / `py -3
36
36
  | Tạo file mới (lỗi nếu đã có) | `SE create <file> <<'EOF'` … nội dung … `EOF` |
37
37
  | Sửa nội dung | `SE edit <file> <<'EOF'` `[{"old": "…", "new": "…"}]` `EOF`. Gom mọi chỗ sửa của một lượt vào **một** lần gọi. Đoạn lặp giống nhau thì thêm `"all": true` |
38
38
  | Frontmatter | `SE set <file> key=value …` (giá trị được kiểm trước khi ghi) |
39
- | Mục còn chờ xác nhận · xác nhận theo mã | `SE pending <file>` · `SE confirm <file> <mã…>` |
39
+ | Mục còn chờ xác nhận · xác nhận theo mã | `SE pending <file>` · `SE confirm <file> <mã…>`. Dòng không có mã thì xác nhận theo số dòng mà `pending` in ra: `SE confirm <file> L24` |
40
40
  | Đọc **một mục** | `SE section <file> <mã>`, mã là `<!-- sec:… -->` ở tiêu đề |
41
41
  | Đọc **một UC** | `SE uc <file> UC-003 [UC-005 …]` |
42
42
  | Sửa / thêm **dòng có mã** (BR, AC) | `SE item <file> <<'EOF'` `[{"id": "UC-003-BR10", "new": "<dòng mới>"}, {"after": "UC-003-BR10", "new": "<dòng thêm>"}]` `EOF`. Chỉ viết dòng mới. Mã không đổi, không dùng lại |
package/ref/prd/change.md CHANGED
@@ -29,5 +29,6 @@ Khi PO đồng ý kế hoạch:
29
29
  - Mục bỏ: giữ dòng, gạch ngang nội dung, ghi *"Đã bỏ (v{mới})"*.
30
30
  - Thay đổi làm đổi luồng (thêm UC, đổi thứ tự, thêm nhánh lỗi) thì sửa luôn mục **User flow**. Yêu cầu là *"thêm User flow"* (PRD tạo bằng bản cũ) thì vẽ mục này từ checkpoint 2 của epic, theo luật trong `new.md`.
31
31
  - Thêm dòng vào mục **Lịch sử thay đổi**, dạng *"v1.1 — UC-003: khoá sau 3 lần sai (BR02 sửa, AC05 thêm, BR04 bỏ)"*.
32
+ - Dòng **Nền tảng** (ở Tổng quan, hoặc trong UC chạy khác mặc định) chỉ ghi mã: `web`, `app` (nơi người dùng dùng tính năng), `system` (luồng tự động, không có màn hình). Giải thích để sau dấu ` — `. Ví dụ: `- ✅ **Nền tảng:** system — luồng tự động nhận đơn từ EP-03`.
32
33
  2. Chạy `SE flowcheck D`. Còn UC chưa có trong hành trình nào thì sửa cho đủ. Sau đó tăng version phụ (1.0 → 1.1): `SE set D version=1.1 updated=…`. PRD đang `approved` thì đặt luôn `status=draft approved_by=— approved_at=—`.
33
- 3. Báo lại cho PO. Không in lại cả PRD, chỉ tóm tắt những gì đã đổi. Nếu không còn `🤖` và không còn câu hỏi mở thì hỏi: *"Duyệt lại bản v1.1 không?"* PO đồng ý thì duyệt theo luật 5 (Duyệt) trong Bước 3 của lệnh. Bản mới cần người duyệt mới, nên đặt lại `approved_by` và `approved_at`.
34
+ 3. Báo lại cho PO. Không in lại cả PRD, chỉ tóm tắt những gì đã đổi. Còn `🤖` thì xác nhận trước. Bản mới chưa duyệt được ngay, vì phải refine rồi review lại phần vừa đổi. Báo: *"Bước tiếp: `/clear` rồi `/prd {id} refine`, sau đó `/prd {id} review`, rồi duyệt."* Bản mới cần người duyệt mới, nên khi duyệt phải đặt lại `approved_by` và `approved_at`.
package/ref/prd/lenses.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Bạn là **một** lăng kính. Chỉ soi đúng phần của lăng kính mình, trong đúng **phạm vi** và **ngưỡng báo** được giao. Tìm ít nhưng đúng còn hơn tìm nhiều mà sai.
4
4
 
5
+ **Lượt sau `/prd change`** (phạm vi là kết quả `SE diff`): **chỉ báo lỗi do chính thay đổi gây ra**. Ví dụ: mâu thuẫn mới giữa phần vừa đổi và phần khác, chỗ phải đổi theo mà chưa đổi, nghĩa mới làm hỏng một BR hay AC khác. Ngoài ra chỉ báo thêm critical. **Không báo** vấn đề đã có y nguyên ở bản đã duyệt, kể cả khi nó nằm trong mục vừa bị chạm, vì bản đó đã qua refine. Vẫn đọc toàn bộ PRD để thấy ảnh hưởng lan sang chỗ khác.
6
+
5
7
  ## Luật chung
6
8
 
7
9
  1. **Viết bằng ngôn ngữ nghiệp vụ.** Mắt nhìn có thể là kỹ thuật, nhưng finding và đề xuất phải nói *nghiệp vụ còn thiếu hay chưa rõ điều gì*. Không đề xuất API, bảng dữ liệu, retry, timeout hay thư viện.
@@ -78,12 +80,14 @@ Bạn nhận: PRD `D`, file refine `R` (có thể chưa có), thư mục `W` ch
78
80
  - Finding có nêu lại một điểm **đã có quyết định** trong `R` không?
79
81
  - Mức độ có đúng định nghĩa không?
80
82
 
81
- Kết luận cho mỗi finding: `ĐÚNG` · `SAI` (bỏ) · `HẠ MỨC` (giữ, đổi mức).
83
+ - **Lượt sau `/prd change`:** vấn đề này **đã có ở bản đã duyệt** chưa? Xem phần `-` trong `SE diff D`, và phần PRD không nằm trong diff. Đã có từ trước thì đây **không phải** lỗi do thay đổi.
84
+
85
+ Kết luận cho mỗi finding: `ĐÚNG` · `SAI` (bỏ) · `HẠ MỨC` (giữ, đổi mức) · `CÓ TỪ TRƯỚC` (không thành finding; ghi một dòng vào mục *Có từ trước, để sau* ở cuối lượt, không cần PO quyết, không chặn duyệt).
82
86
 
83
87
  **2. Gộp** các finding ĐÚNG trùng nhau (cùng mục, cùng vấn đề), giữ bản rõ nhất. Hai đề xuất trái ngược nhau thì giữ cả hai trong **một** finding, và ghi rõ là cần PO chọn.
84
88
 
85
89
  **3. Sắp xếp** theo mức độ (critical → major → minor), sau đó theo lăng kính (QA → DEV → SA → Phạm vi). **Đánh mã F theo đúng thứ tự đó**, bắt đầu từ mã F kế tiếp.
86
90
 
87
- **4. Ghi vào `R`.** Chưa có `R` thì `SE create R` theo khuôn `.fw/core/templates/prd-refine.md`. Đã có thì `SE edit` thêm mục `## Lượt {n} — v{version} <!-- sec:round-{n} -->` ở **cuối**. **Không sửa các lượt cũ.** Thêm một dòng vào bảng Tóm tắt, rồi `SE set R prd_version={version} round={n} status=pending`. Mỗi finding theo đúng mẫu trong khuôn, `Quyết định: chờ`.
91
+ **4. Ghi vào `R`.** Chưa có `R` thì `SE create R` theo khuôn `.fw/core/templates/prd-refine.md`. Đã có thì `SE edit` thêm mục `## Lượt {n} — v{version} <!-- sec:round-{n} -->` ở **cuối**. **Không sửa các lượt cũ.** Thêm một dòng vào bảng Tóm tắt, rồi `SE set R prd_version={version} round={n} status=pending`. Mỗi finding theo đúng mẫu trong khuôn, `Quyết định: chờ`. Có finding `CÓ TỪ TRƯỚC` thì thêm `### Có từ trước, để sau` ở cuối lượt, mỗi vấn đề một dòng: *"- {mã mục}: {vấn đề}"*.
88
92
 
89
- **5. Trả về một dòng:** *"thô {a} · bỏ {b} · hạ mức {c} · còn {d} (critical {x} · major {y} · minor {z}) → {R}"*.
93
+ **5. Trả về một dòng:** *"thô {a} · bỏ {b} · hạ mức {c} · có từ trước {e} · còn {d} (critical {x} · major {y} · minor {z}) → {R}"*.
package/ref/prd/refine.md CHANGED
@@ -19,7 +19,7 @@ Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đ
19
19
  | `refined` = `version` | **Dừng.** Báo *"Bản v{version} đã refine xong."* Có `--full` thì chạy lượt 1 trên toàn PRD | — |
20
20
  | Chưa có `R`, hoặc chưa từng refine | **1** | Toàn bộ PRD |
21
21
  | `R` có `round: 1` **đã áp** bản sửa, `prd_version` < `version` | **2** | Chỉ các mục mà lượt 1 đã sửa hoặc thêm (đọc từ quyết định `nhận` / `sửa` trong `R`), cộng các mục tham chiếu tới chúng |
22
- | `refined` có giá trị nhưng < `version` (PRD đã qua `/prd change`) | **1** | Chỉ các mục trong dòng Lịch sử thay đổi từ sau `refined` tới nay |
22
+ | `refined` có giá trị nhưng < `version` (PRD đã qua `/prd change`) | **1** | **Đúng phần đã đổi**: `SE diff D` (so với bản `approved` gần nhất trong git, có sẵn số UC bị chạm). Không có bản đã duyệt trong git thì dùng các dòng Lịch sử thay đổi có version lớn hơn `refined`. Ngưỡng: **chỉ lỗi do chính thay đổi gây ra, hoặc critical** (xem đầu `lenses.md`) |
23
23
 
24
24
  **Không có lượt 3.** Sau lượt 2 thì luôn đặt `refined` (Bước D).
25
25
 
@@ -27,16 +27,29 @@ Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đ
27
27
 
28
28
  Phiên chính **không đọc** kết quả chi tiết của các agent. Agent ghi ra file, phiên chính chỉ đọc bảng tóm tắt cuối cùng.
29
29
 
30
- 1. Dùng công cụ **Agent**, `model: "opus"`, gọi **4 agent song song trong cùng một lượt**: QA · DEV · SA · Phạm vi. Mỗi agent nhận:
30
+ 1. **Chọn số agent rà** theo lượt và phạm vi ở Bước A. Số UC bị chạm lấy ở dòng đầu của `SE diff D`. Thay đổi chỉ ở Tổng quan, User flow hoặc Màn hình tính là 0 UC.
31
+
32
+ | Lượt và phạm vi | Agent rà |
33
+ |---|---|
34
+ | Lượt 1, toàn bộ PRD | **4 agent**: QA · DEV · SA · Phạm vi |
35
+ | Lượt 1 sau `/prd change`, phạm vi chạm **≤ 3 UC** | **1 agent gộp** |
36
+ | Lượt 1 sau `/prd change`, phạm vi chạm > 3 UC | **4 agent** |
37
+ | Lượt 2 | **1 agent gộp** |
38
+
39
+ **Agent gộp** soi lần lượt cả 4 phần QA, DEV, SA, Phạm vi trong `lenses.md`. Mỗi finding ghi đúng `lens:` của phần đã phát hiện ra nó. Agent vẫn **đọc toàn bộ `D`**, để bắt được lỗi lan sang UC khác. Agent này nhận đủ tài liệu phụ của Phạm vi, và ghi vào `W/round-{n}-all.md`. Lý do: phạm vi hẹp thì một agent soi đủ 4 góc nhìn, nên không cần 4 agent cùng đọc lại cả PRD.
40
+
41
+ Dùng công cụ **Agent**, `model: "opus"`. Có nhiều agent thì gọi **song song trong cùng một lượt**. Mỗi agent nhận:
31
42
  - đường dẫn `D` (**đọc toàn bộ**), `R` (nếu có, để đọc sổ quyết định), `.fw/core/ref/prd/lenses.md`, tên lăng kính, **phạm vi** và **ngưỡng báo** của lượt (lượt 1: mọi mức · lượt 2: chỉ critical hoặc lỗi do chính bản sửa gây ra);
32
43
  - riêng **Phạm vi**: thêm các lệnh để đọc tài liệu phụ, **chỉ đúng phần cần**: `SE section <epic> cp1`, `SE section {specs}/product/product.md constraints`, `SE section {specs}/product/product.md epics`;
33
44
  - file kết quả: `W/round-{n}-{lens}.md`. Agent ghi bằng `SE create --replace` (chỉ dùng được trong `.fw/tmp/`) và **chỉ trả về một dòng**: *"{lens}: {k} finding → {file}"*.
34
- 2. Khi đủ 4 dòng, gọi **1 agent kiểm chứng** (`model: "opus"`), giao: `D`, `R`, `W`, số lượt `n`, mã F kế tiếp (lớn nhất trong `R` cộng một, hoặc F01), và phần **"Kiểm chứng và ghi file"** trong `lenses.md`. Agent này kiểm chứng, gộp, sắp xếp, đánh mã, ghi vào `R`, rồi trả về **một dòng** số liệu.
45
+ 2. **Tổng finding thô = 0** (mọi agent đều trả *"0 finding"*) và đã có `R`: **không gọi agent kiểm chứng**. Phiên chính tự ghi vào `R` bằng **một** lần `SE edit`: thêm mục `## Lượt {n} — v{version} <!-- sec:round-{k} -->` ở cuối (`k` = số `round-…` lớn nhất trong `R` cộng một), với một dòng *"Phạm vi: … · 0 finding thô"*, và thêm một dòng vào bảng Tóm tắt (các cột số đều 0, Kết luận `APPROVED`). Sau đó `SE set R prd_version={version} round={n}`, rồi sang thẳng **Bước D, bước 8**.
46
+
47
+ Còn lại: khi đủ dòng trả về của các agent rà, gọi **1 agent kiểm chứng** (`model: "opus"`), giao: `D`, `R`, `W`, số lượt `n`, mã F kế tiếp (lớn nhất trong `R` cộng một, hoặc F01), và phần **"Kiểm chứng và ghi file"** trong `lenses.md`. Agent này kiểm chứng, gộp, sắp xếp, đánh mã, ghi vào `R`, rồi trả về **một dòng** số liệu. File kết quả trong `W` có thể là 4 file theo lăng kính, hoặc 1 file `round-{n}-all.md`.
35
48
 
36
49
  ## Bước C — Trình
37
50
 
38
51
  1. Đọc `SE section R summary` và danh sách tiêu đề finding của lượt này. Danh sách đã được agent kiểm chứng **sắp xếp sẵn** theo mức độ, rồi theo lăng kính (QA → DEV → SA → Phạm vi).
39
- 2. Trình **một bảng tóm tắt** gồm mã · mức · lăng kính · mục · một dòng vấn đề, đúng theo thứ tự đó. Kèm số finding thô, số bị bỏ ở bước kiểm chứng, và số bị hạ mức.
52
+ 2. Trình **một bảng tóm tắt** gồm mã · mức · lăng kính · mục · một dòng vấn đề, đúng theo thứ tự đó. Kèm số finding thô, số bị bỏ ở bước kiểm chứng, số bị hạ mức, và số vấn đề *có từ trước* (chỉ báo số, không cần PO quyết).
40
53
  3. Nhắc PO cách quyết:
41
54
  - *"Chi tiết xem {R}. Quyết theo mã: 'nhận F01, F03 · bác F02 vì … · F04 sửa thành … · hoãn F05'."*
42
55
  - *"Quyết theo nhóm được, áp cho major và minor: 'nhận tất cả minor' · 'nhận hết UC-003 trừ F12' · 'bác F20–F24 vì …'. Critical phải quyết từng mã."*
@@ -58,4 +71,4 @@ Phiên chính **không đọc** kết quả chi tiết của các agent. Agent g
58
71
  - chạy `SE flowcheck D`, rồi `SE set D version=… status=draft approved_by=— approved_at=—` và `SE set R status=applied`;
59
72
  - **chỉ trả về một dòng**: *"áp {k} finding → v{version}; {số BR/AC sửa · thêm · bỏ}"*.
60
73
  7. Lượt 1 có áp bản sửa: báo *"`/clear` rồi `/prd {id} refine` để chạy lượt 2, chỉ soi phần vừa sửa."*
61
- 8. Không áp gì (mọi finding đều bác / hoãn), **hoặc** vừa xong **lượt 2**: `SE set D refined={version hiện tại}` và `SE set R status=done`. Finding `hoãn` thì ghi thêm vào mục **Câu hỏi còn mở** của `D`, kèm chú thích *"(để bước QC)"*.
74
+ 8. Không áp gì (mọi finding đều bác / hoãn), **hoặc** vừa xong **lượt 2**: `SE set D refined={version hiện tại}` và `SE set R status=done`. Finding `hoãn` thì ghi thêm vào mục **Câu hỏi còn mở** của `D`, kèm chú thích *"(để bước QC)"*. Rồi báo: *"Refine xong v{version}. Bước tiếp: `/clear` rồi `/prd {id} review` để rà hình thức, bắt buộc trước khi duyệt."*
@@ -0,0 +1,85 @@
1
+ # /prd — chế độ rà hình thức (review)
2
+
3
+ Mục đích: rà **câu chữ, chính tả, thuật ngữ, trình bày, đúng khuôn**, để người và AI ở các bước sau đọc đúng ý. **Không** rà nội dung nghiệp vụ, vì đó là việc của `refine` và đã làm xong.
4
+
5
+ - **Review đi sau refine.** Refine làm đổi câu chữ, nên rà hình thức trước thì phải rà lại.
6
+ - **Sửa trong cùng version, không được đổi nghĩa.** Không tăng version. PRD đang `approved` thì vẫn giữ `approved`. `SE edit --form` tự chặn mọi chỗ sửa làm đổi số, mã, dấu `🤖`/`✅`, gạch ngang hoặc nguồn.
7
+ - Thư mục làm việc: `W = .fw/tmp/review/{slug}/`, không commit. File kết quả: `W/review-{version}.md`.
8
+
9
+ ## Bước A — Xác định phạm vi
10
+
11
+ Chạy `SE upgrade D` trước, để PRD tạo bằng bản cũ có trường `reviewed`. Đọc frontmatter của `D` (`version`, `refined`, `reviewed`, `status`).
12
+
13
+ Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đó.
14
+
15
+ | Tình huống | Làm gì |
16
+ |---|---|
17
+ | `refined` ≠ `version` | **Dừng.** Báo *"Bản v{version} chưa refine. Chạy `/prd {id} refine` trước. Review đi sau refine."* |
18
+ | `reviewed` = `version`, không có `--full` | **Dừng.** Báo *"Bản v{version} đã rà hình thức xong."* |
19
+ | Đã có `W/review-{version}.md` (rà xong, đang chờ PO quyết) | **Không rà lại.** Sang thẳng Bước C |
20
+ | `reviewed` là `—` (chưa từng review), hoặc có `--full` | Phạm vi: **toàn bộ PRD** |
21
+ | `reviewed` < `version` | Phạm vi: **đúng phần đã đổi**, lấy bằng `SE diff D` (so với bản `approved` gần nhất trong git). Không có bản đã duyệt trong git thì dùng các dòng Lịch sử thay đổi có version lớn hơn `reviewed` |
22
+
23
+ ## Bước B — Rà (1 sub-agent)
24
+
25
+ Dùng công cụ **Agent**, `model: "sonnet"`, gọi **1** agent. Giao cho agent: đường dẫn `D`, phạm vi, `{specs}/product/glossary.md` (nếu có), file kết quả `W/review-{version}.md`, và phần **"Dành cho agent rà"** ở cuối file này. Agent **chỉ trả về một dòng**. Phiên chính không đọc PRD.
26
+
27
+ ## Bước C — Trình
28
+
29
+ 1. Đọc `W/review-{version}.md` (file nhỏ).
30
+ 2. Trình theo thứ tự:
31
+ - **Đã tự sửa (A):** chỉ báo số chỗ, kèm 2–3 ví dụ. Danh sách đầy đủ nằm trong file.
32
+ - **Bảng đề xuất (B):** mã · loại · mục · *trước → sau*, rút gọn mỗi bên khoảng 60 ký tự.
33
+ - **Ghi chú (C):** mỗi chỗ một dòng, để PO biết. **Không gợi ý `/prd … change`**, và nói rõ ghi chú không chặn duyệt. Đổi PRD là quyết định nghiệp vụ của PO, review không tự đề xuất.
34
+ 3. Nhắc PO cách quyết: *"Quyết theo mã: 'nhận hết' · 'nhận hết trừ R03' · 'R04 sửa thành …' · 'bác R05'."* Không có đề xuất nào thì sang thẳng Bước D.
35
+
36
+ ## Bước D — Áp
37
+
38
+ 1. Áp các đề xuất được nhận bằng **một** lần `SE edit --form D`, với `old` là "Trước" và `new` là "Sau" (hoặc bản PO sửa).
39
+ - `SE` chặn chỗ nào (vì chỗ đó đổi số, mã, dấu hoặc nguồn) thì **hỏi PO ngay**, kèm đúng thứ bị đổi. Ví dụ: *"R07 thêm mã EP-03 vào tiêu đề J7, cho khớp với tên trong bảng. Chỗ này có đổi nghĩa không?"*
40
+ - PO nói **không đổi nghĩa**: áp chỗ đó bằng `SE edit D` (không `--form`), vẫn trong cùng version.
41
+ - PO nói **có**: không sửa, chuyển thành ghi chú C.
42
+ 2. Có chỗ nào được sửa (A hoặc B) thì thêm **một** dòng vào cuối bảng Lịch sử thay đổi bằng `SE edit D`, **giữ nguyên version**: `| {version} | {ngày} | Rà hình thức: sửa {k} chỗ (chính tả, câu chữ, thuật ngữ, khuôn). Không đổi nội dung |`.
43
+ 3. `SE set D reviewed={version} updated={ngày}`.
44
+ - PRD đang `approved` thì `SE` kiểm lại mọi điều kiện duyệt. Báo lỗi thì sửa theo lỗi, thường là một dòng sai khuôn còn sót, rồi chạy lại.
45
+ - PRD đang `draft`: không còn `🤖`, không còn câu hỏi mở thì hỏi *"Duyệt bản v{version} không?"* và duyệt theo luật 5 (Duyệt) trong Bước 3 của lệnh.
46
+ 4. Còn ghi chú C thì nhắc PO một dòng. Ghi chú **không chặn** duyệt.
47
+
48
+ ---
49
+
50
+ ## Dành cho agent rà
51
+
52
+ Bạn rà **hình thức** của PRD trong đúng phạm vi được giao. Phạm vi là toàn bộ PRD thì đọc cả file. Phạm vi hẹp thì đọc `SE uc D UC-…` và `SE section D <mã mục>`, cộng mục Tổng quan để biết thuật ngữ trong phần Khái niệm. `SE` = `python .fw/core/tools/spec_edit.py`.
53
+
54
+ **Ba loại:**
55
+
56
+ | Loại | Gồm | Làm gì |
57
+ |---|---|---|
58
+ | **A. Tự sửa** | Chính tả, dấu tiếng Việt, dấu câu, khoảng trắng thừa, bảng vỡ cột, danh sách lệch | Sửa luôn bằng **một** lần `SE edit --form D`. `SE` chặn chỗ nào thì chuyển chỗ đó sang B hoặc C |
59
+ | **B. Đề xuất** | Câu khó hiểu (quá dài, nhiều mệnh đề lồng nhau, đại từ không rõ chỉ ai, phủ định kép) · **cùng một khái niệm mà gọi bằng hai tên** ở hai chỗ, hoặc sai so với glossary và phần Khái niệm (đề xuất dùng tên **đang dùng nhiều hơn**) · tham chiếu trỏ sai ("xem mục 5" trong khi đúng là mục 6) · dòng sai khuôn, ví dụ dòng Nền tảng ghi bằng lời thay vì mã `web` · `app` · `system`. A bị `SE` chặn thì cũng đưa vào đây | **Không sửa.** Ghi vào file để PO quyết |
60
+ | **C. Ghi chú** | Lỗi hình thức mà sửa thì chắc chắn đổi nghĩa: số, điều kiện, phạm vi | **Không sửa.** Ghi một dòng |
61
+
62
+ **Không làm:**
63
+ - Rà nội dung nghiệp vụ (việc của refine). Đổi mã, đổi số, đổi dấu `🤖`/`✅`, đổi `(nguồn: …)`. Sửa phần ngoài phạm vi.
64
+ - **Đề xuất đổi tên một khái niệm đang được gọi nhất quán**, dù bạn thấy tên chưa hay hoặc đã cũ. Ví dụ: "đang dùng mật khẩu mặc định" được gọi đúng một tên ở mọi chỗ thì để nguyên, không đề xuất. Đặt tên khái niệm là quyết định nội dung. Lần đổi tên như vậy ở LMS v1.8 đã tốn khoảng 3M token refine, và tên mới bị refine chỉ ra là sai nghĩa.
65
+ - Ghi ý kiến về nghiệp vụ vào C, hoặc gợi ý `/prd … change`.
66
+
67
+ Viết lại câu thì giữ đúng mọi ý và mọi giá trị của câu cũ.
68
+
69
+ **Ghi file** `W/review-{version}.md` bằng `SE create --replace`, đúng khuôn sau:
70
+
71
+ ```
72
+ ## Đã sửa (A)
73
+ - {mã mục}: "{trước}" → "{sau}"
74
+
75
+ ## Đề xuất (B)
76
+ ### R01 · {câu chữ | thuật ngữ | khuôn} · {mã mục}
77
+ - Trước: "{nguyên văn trong PRD, đủ dài để chỉ xuất hiện một lần}"
78
+ - Sau: "{…}"
79
+ - Lý do: {…}
80
+
81
+ ## Ghi chú (C)
82
+ - {mã mục}: {lỗi hình thức không sửa được mà không đổi nghĩa}
83
+ ```
84
+
85
+ Mục nào không có gì thì ghi `Không có`. Sau đó **chỉ trả về một dòng**: *"sửa {a} · đề xuất {b} · ghi chú {c} → {file}"*.
package/templates/prd.md CHANGED
@@ -7,6 +7,7 @@ open_questions: 0
7
7
  approved_by: — # người duyệt PRD; bắt buộc có khi status: approved
8
8
  approved_at: — # YYYY-MM-DD
9
9
  refined: — # version đã refine xong (/prd <EPIC-ID> refine); bắt buộc = version khi duyệt
10
+ reviewed: — # version đã rà hình thức xong (/prd <EPIC-ID> review, sau refine); bắt buộc = version khi duyệt
10
11
  updated: {YYYY-MM-DD}
11
12
  ---
12
13
 
@@ -21,7 +22,7 @@ updated: {YYYY-MM-DD}
21
22
 
22
23
  - **Mục tiêu:** {…}
23
24
  - **Actor:** {ACT-xx (chính) · ACT-yy (phụ)}
24
- - **Nền tảng:** {web · app — nơi người dùng dùng tính năng; là mặc định cho mọi UC}
25
+ - **Nền tảng:** {web · app · system — chỉ ghi mã: web, app (nơi người dùng dùng tính năng), system (luồng tự động, không có màn hình); giải thích sau dấu —. Là mặc định cho mọi UC}
25
26
  - **Trong phạm vi:**
26
27
  - {…}
27
28
  - **Ngoài phạm vi:**
@@ -53,7 +54,7 @@ flowchart TD
53
54
 
54
55
  - **Jira:** {key hoặc —}
55
56
  - **Actor:** {ACT-xx}
56
- - **Nền tảng:** {chỉ ghi khi UC này chạy khác Tổng quan, ví dụ chỉ app; giống thì bỏ dòng này}
57
+ - **Nền tảng:** {chỉ ghi khi UC này chạy khác Tổng quan, ví dụ `app` hoặc `system — luồng tự động nhận đơn`; giống thì bỏ dòng này}
57
58
  - **Điều kiện trước:** {…}
58
59
  - **Kết quả sau:** {…}
59
60
 
@@ -11,9 +11,10 @@ Cách dùng (nội dung truyền qua stdin):
11
11
  spec_edit.py --check
12
12
  spec_edit.py create <file> stdin = toàn bộ nội dung; lỗi nếu file đã có
13
13
  spec_edit.py edit <file> stdin = JSON [{"old": "...", "new": "...", "all": false}]
14
+ spec_edit.py edit --form <file> như edit, nhưng chặn mọi chỗ sửa làm đổi số, mã, dấu, nguồn (/prd review)
14
15
  spec_edit.py set <file> key=value… sửa frontmatter
15
16
  spec_edit.py pending <file> liệt kê các dòng còn dấu 🤖 (kèm số dòng)
16
- spec_edit.py confirm <file> <mã>… đổi 🤖 → ✅ trên dòng chứa mã (vd AC2 BR8b ACT-03)
17
+ spec_edit.py confirm <file> <mã>… đổi 🤖 → ✅ trên dòng chứa mã (vd AC2 BR8b ACT-03), hoặc theo số dòng L24
17
18
  spec_edit.py section <file> <mã> in đúng một mục theo mã `<!-- sec:… -->` (vd constraints)
18
19
  spec_edit.py upgrade <file> gắn mã mục cho file theo khuôn cũ (chạy một lần)
19
20
  spec_edit.py next-uc <thư mục specs> in mã UC kế tiếp (quét mọi prd.md)
@@ -30,6 +31,7 @@ Cách dùng (nội dung truyền qua stdin):
30
31
  spec_edit.py scenario <feature> <SC…>… in đúng các kịch bản đó
31
32
  spec_edit.py sc <feature> stdin = JSON [{"id": mã, "new": khối} | {"after": mã | "end", "new": khối} | {"remove": mã, "version": "1.1"}]
32
33
  spec_edit.py bddcheck <feature> [prd] kiểm khuôn, tag, độ phủ AC / BR của file BDD
34
+ spec_edit.py diff <prd> phần đã đổi so với bản `approved` gần nhất trong git, kèm UC bị chạm
33
35
 
34
36
  Mã thoát: 0 = thành công · 1 = lỗi dữ liệu (không ghi gì) · 2 = sai cách dùng.
35
37
  """
@@ -110,7 +112,22 @@ def cmd_create(path, replace=False):
110
112
  print("đã tạo " + path)
111
113
 
112
114
 
113
- def cmd_edit(path):
115
+ # Sửa hình thức (/prd review): không được đổi số, mã, dấu 🤖/✅, gạch ngang, nguồn — những thứ mang nghĩa.
116
+ FORM_KEEP = re.compile("UC-\\d{3}(?:-(?:BR|AC|SC)\\d+)?|(?:CON|ACT)-\\d+|\\b(?:BR|AC|F|J)\\d+[a-z]?\\b|\\d+(?:[.,]\\d+)*"
117
+ "|[\U0001F916✅]|~~|\\(nguồn:[^)]*\\)")
118
+
119
+
120
+ def form_changes(old, new):
121
+ """Những thứ mang nghĩa bị thêm / mất khi đổi old → new. Rỗng = chỉ đổi hình thức."""
122
+ # "xem mục 5" → "mục 6" là sửa chỗ trỏ tới tiêu đề, không phải đổi số nghiệp vụ.
123
+ ref = lambda s: re.sub(r"\b([Mm]ục)\s+\d+", r"\1 #", s)
124
+ a, b = sorted(FORM_KEEP.findall(ref(old))), sorted(FORM_KEEP.findall(ref(new)))
125
+ lost = [x for x in a if a.count(x) > b.count(x)]
126
+ added = [x for x in b if b.count(x) > a.count(x)]
127
+ return sorted(set(lost)), sorted(set(added))
128
+
129
+
130
+ def cmd_edit(path, form=False):
114
131
  try:
115
132
  edits = json.loads(stdin_text())
116
133
  except json.JSONDecodeError as e:
@@ -131,6 +148,12 @@ def cmd_edit(path):
131
148
  if n > 1 and not replace_all:
132
149
  fail("chỗ sửa #%d: đoạn cần sửa xuất hiện %d lần. Thêm ngữ cảnh cho đoạn `old` đủ duy nhất, "
133
150
  "hoặc đặt \"all\": true.\n old = %r" % (i, n, old[:120]))
151
+ if form:
152
+ lost, added = form_changes(old, e["new"])
153
+ if lost or added:
154
+ fail("chỗ sửa #%d không phải sửa hình thức: %s%s. Số, mã, dấu 🤖/✅, gạch ngang và nguồn phải giữ nguyên. "
155
+ "Sửa làm đổi nghĩa thì ghi thành ghi chú, đi qua `/prd … change`.\n old = %r"
156
+ % (i, ("mất " + " ".join(lost)) if lost else "", (" · thêm " + " ".join(added)) if added else "", old[:120]))
134
157
  before = sec_keys(text)
135
158
  for e in edits:
136
159
  text = text.replace(e["old"], e["new"]) if e.get("all") else text.replace(e["old"], e["new"], 1)
@@ -163,8 +186,9 @@ def validate(ftype, key, value):
163
186
  fail("approved_at=%s không hợp lệ. Định dạng YYYY-MM-DD, hoặc — nếu chưa duyệt" % value)
164
187
  if key in ("version", "prd_version") and not VERSION.match(value):
165
188
  fail("%s=%s không hợp lệ. Định dạng số.số, ví dụ 1.0, 1.1" % (key, value))
166
- if key == "refined" and value != "—" and not VERSION.match(value):
167
- fail("refined=%s không hợp lệ. Là version PRD đã refine xong (vd 1.1), hoặc —" % value)
189
+ if key in ("refined", "reviewed") and value != "—" and not VERSION.match(value):
190
+ fail("%s=%s không hợp lệ. Là version PRD đã %s xong (vd 1.1), hoặc —"
191
+ % (key, value, "refine" if key == "refined" else "rà hình thức"))
168
192
 
169
193
 
170
194
  # ── Refine: finding critical / major chưa có quyết định thì chưa được đi tiếp ──────────────
@@ -240,6 +264,15 @@ def guard_refined(path, block):
240
264
  % (", ".join(pending), rp))
241
265
 
242
266
 
267
+ def guard_reviewed(block):
268
+ """PRD chỉ được duyệt khi đã rà hình thức (/prd review) đúng version đang có. Review đi sau refine."""
269
+ ver = re.search(r"^version:\s*(\S+)", block, re.M)
270
+ rev = re.search(r"^reviewed:\s*(\S+)", block, re.M)
271
+ if not rev or not ver or rev.group(1) != ver.group(1):
272
+ fail("không đặt được status=approved: bản v%s chưa rà hình thức (reviewed=%s). Chạy `/prd <EPIC-ID> review` trước."
273
+ % (ver.group(1) if ver else "?", rev.group(1) if rev else "chưa có"))
274
+
275
+
243
276
  def guard_done(ftype, block, body, path):
244
277
  """Chặn đặt trạng thái 'đã xong' khi còn mục chờ PO — AI không được tự khẳng định."""
245
278
  done = DONE_STATUS.get(ftype)
@@ -260,12 +293,11 @@ def guard_done(ftype, block, body, path):
260
293
  fail("không đặt được status=%s: chưa có %s. Đặt cùng lệnh, ví dụ: "
261
294
  "set <file> status=%s approved_by=<tên> approved_at=YYYY-MM-DD" % (done, field, done))
262
295
  if ftype == "prd":
263
- ptext = read(path)
264
- no_platform = [u for u in re.findall(r"^###\s+(UC-\d{3})(?![\d-])", ptext, re.M) if not uc_platforms(ptext, u)]
265
- if no_platform:
266
- fail("không đặt được status=approved: chưa khai Nền tảng cho %s. Thêm dòng `- **Nền tảng:** web · app` "
267
- "ở Tổng quan (mặc định cho cả PRD), hoặc trong UC chạy khác mặc định." % ", ".join(no_platform))
296
+ problems = platform_problems(read(path))
297
+ if problems:
298
+ fail("không đặt được status=approved:\n - " + "\n - ".join(problems))
268
299
  guard_refined(path, block)
300
+ guard_reviewed(block)
269
301
  if ftype == "bdd":
270
302
  guard_bdd(path, block)
271
303
  if ftype == "prd-refine" and undecided_findings(body):
@@ -589,6 +621,61 @@ def cmd_decide(path):
589
621
  print("đã ghi %d quyết định vào %s" % (len(ops), path))
590
622
 
591
623
 
624
+ def git_show(rev_path):
625
+ import subprocess
626
+ r = subprocess.run(["git", "show", rev_path], capture_output=True)
627
+ return r.stdout.decode("utf-8", "replace") if r.returncode == 0 else None
628
+
629
+
630
+ def last_approved(path):
631
+ """(commit, nội dung) của bản gần nhất trong git mà file đang `status: approved`. None nếu không có."""
632
+ import subprocess
633
+ r = subprocess.run(["git", "log", "--format=%H", "--", path], capture_output=True)
634
+ if r.returncode != 0:
635
+ return None
636
+ rel = "./" + os.path.relpath(path).replace("\\", "/")
637
+ for h in r.stdout.decode().split():
638
+ text = git_show("%s:%s" % (h, rel))
639
+ if text and re.search(r"^status:\s*approved", text, re.M):
640
+ return h, text
641
+ return None
642
+
643
+
644
+ def cmd_diff(path):
645
+ """Phần PRD đã đổi so với bản đã duyệt gần nhất trong git — phạm vi chính xác cho refine / review sau change."""
646
+ import difflib
647
+ base = last_approved(path)
648
+ if not base:
649
+ fail("không tìm thấy bản `approved` nào của %s trong git (chưa commit bản đã duyệt?). "
650
+ "Dùng các dòng Lịch sử thay đổi làm phạm vi." % path)
651
+ h, old = base
652
+ new = read(path)
653
+ old_l, new_l = old.split("\n"), new.split("\n")
654
+ # Mỗi UC chỉ gồm khối của nó: từ tiêu đề ### UC-… tới tiêu đề ## / ### kế tiếp.
655
+ spans = []
656
+ for i, l in enumerate(new_l):
657
+ m = re.match(r"^###\s+(?:~~)?(UC-\d{3})", l)
658
+ if m:
659
+ end = next((j for j in range(i + 1, len(new_l)) if re.match(r"^#{2,3}\s", new_l[j])), len(new_l))
660
+ spans.append((i, end, m.group(1)))
661
+
662
+ def uc_at(i):
663
+ return next((u for s, e, u in spans if s <= i < e), None)
664
+ touched, body = [], []
665
+ sm = difflib.SequenceMatcher(None, old_l, new_l, autojunk=False)
666
+ for tag, i1, i2, j1, j2 in sm.get_opcodes():
667
+ if tag == "equal":
668
+ continue
669
+ for j in range(j1, max(j2, j1 + 1)):
670
+ u = uc_at(min(j, len(new_l) - 1))
671
+ if u and u not in touched:
672
+ touched.append(u)
673
+ body += ["@@ dòng %d" % (j1 + 1)] + ["- " + l for l in old_l[i1:i2]] + ["+ " + l for l in new_l[j1:j2]]
674
+ print("# So với bản đã duyệt v%s (commit %s) · UC bị chạm: %s (%d UC)"
675
+ % (prd_version(old), h[:7], ", ".join(touched) or "không có (chỉ Tổng quan / User flow / Màn hình / Lịch sử)", len(touched)))
676
+ print("\n".join(body) if body else "(không có gì đổi)")
677
+
678
+
592
679
  PRD_SIZE_WARN = 60 * 1024
593
680
 
594
681
 
@@ -685,26 +772,62 @@ def prd_items(text):
685
772
  return out
686
773
 
687
774
 
775
+ # web · app: nơi người dùng dùng tính năng · system: luồng tự động, không có màn hình (job, tích hợp).
776
+ PLATFORMS = ("web", "app", "system")
777
+ PLATFORM_RULE = ("dòng Nền tảng chỉ ghi mã `web` · `app` · `system` (luồng tự động, không có màn hình), "
778
+ "giải thích để sau dấu ` — `. Ví dụ: `- **Nền tảng:** system — luồng tự động nhận đơn từ EP-03`")
779
+
780
+
688
781
  def platform_values(value):
689
- value = re.split(r"\(| — ", value)[0]
690
- return [v for v in re.findall(r"[a-z][a-z0-9-]*", value.lower())]
782
+ """(các mã nền tảng, các chữ không phải mã). Phần sau ` — ` hoặc `(` là lời giải thích."""
783
+ head = re.split(r"\(| — | – | - ", value.replace("✅", "").replace("\U0001F916", ""))[0]
784
+ words = [w for w in re.split(r"[·,/\s]+", head.strip()) if w]
785
+ return [w for w in words if w in PLATFORMS], [w for w in words if w not in PLATFORMS]
691
786
 
692
787
 
693
- def uc_platforms(text, uc):
694
- """Nền tảng của UC: dòng `Nền tảng` trong UC, không có thì lấy ở Tổng quan. [] = PRD chưa khai."""
788
+ def uc_platform_line(text, uc):
789
+ """Giá trị dòng `Nền tảng` áp cho UC: trong UC, không có thì ở Tổng quan. None = chưa khai."""
695
790
  lines = text.split("\n")
696
791
  span = uc_span(lines, uc)
697
792
  if span:
698
793
  for l in lines[span[0]:span[1]]:
699
794
  m = PLATFORM_LINE.match(l)
700
795
  if m:
701
- return platform_values(m.group(1))
702
- overview = section_text(text, "overview") or ""
703
- for l in overview.split("\n"):
796
+ return m.group(1)
797
+ for l in (section_text(text, "overview") or "").split("\n"):
704
798
  m = PLATFORM_LINE.match(l)
705
799
  if m:
706
- return platform_values(m.group(1))
707
- return []
800
+ return m.group(1)
801
+ return None
802
+
803
+
804
+ def uc_platforms(text, uc):
805
+ """Mã nền tảng của UC. [] = chưa khai, hoặc khai sai khuôn (xem platform_problems)."""
806
+ value = uc_platform_line(text, uc)
807
+ if value is None:
808
+ return []
809
+ good, bad = platform_values(value)
810
+ return [] if bad else good
811
+
812
+
813
+ def platform_problems(text):
814
+ """Mỗi UC còn dùng phải có nền tảng đúng khuôn. Trả về danh sách lỗi (rỗng = ổn)."""
815
+ missing, wrong = [], []
816
+ for uc in re.findall(r"^###\s+(UC-\d{3})(?![\d-])", text, re.M):
817
+ value = uc_platform_line(text, uc)
818
+ if value is None:
819
+ missing.append(uc)
820
+ continue
821
+ good, bad = platform_values(value)
822
+ if bad or not good:
823
+ wrong.append('%s ("%s")' % (uc, value.strip()[:50]))
824
+ out = []
825
+ if missing:
826
+ out.append("chưa khai Nền tảng cho %s. Thêm dòng `- **Nền tảng:** web · app` ở Tổng quan (mặc định cho cả PRD), "
827
+ "hoặc trong UC chạy khác mặc định." % ", ".join(missing))
828
+ if wrong:
829
+ out.append("Nền tảng ghi sai khuôn ở %s: %s" % (", ".join(wrong), PLATFORM_RULE))
830
+ return out
708
831
 
709
832
 
710
833
  def prd_version(text):
@@ -790,9 +913,11 @@ def bdd_report(feature, prd=None):
790
913
  items = prd_items(ptext)
791
914
  own = [c for c, alive in items.items() if alive and c.startswith(uc + "-")]
792
915
  platforms = uc_platforms(ptext, uc)
793
- if not platforms:
916
+ if uc_platform_line(ptext, uc) is None:
794
917
  errors.append("PRD chưa khai Nền tảng cho %s (dòng `- **Nền tảng:** web · app` ở Tổng quan, hoặc trong UC). "
795
918
  "Chạy `/prd <EPIC-ID> change khai nền tảng …` trước." % uc)
919
+ elif not platforms:
920
+ errors.append("Nền tảng của %s trong PRD ghi sai khuôn: %s. Sửa PRD bằng `/prd <EPIC-ID> review`." % (uc, PLATFORM_RULE))
796
921
  scenarios, removed = parse_feature(text)
797
922
  if not scenarios:
798
923
  errors.append("file chưa có kịch bản nào")
@@ -860,7 +985,7 @@ def cmd_bddcheck(feature, prd=None):
860
985
  if st:
861
986
  print("%s: %d kịch bản (%d đã bỏ) · AC %d/%d · BR %d/%d · %s" % (
862
987
  uc_of(feature), st["scenarios"], st["removed"], st["ac"][0], st["ac"][1], st["br"][0], st["br"][1],
863
- " · ".join("%s %d" % kv for kv in st["platforms"].items()) or "chưa khai nền tảng"), flush=True)
988
+ " · ".join("%s %d" % kv for kv in st["platforms"].items()) or "chưa có nền tảng hợp lệ"), flush=True)
864
989
  ptext = read(prd or prd_of(feature))
865
990
  todo = [u for u in re.findall(r"^###\s+(UC-\d{3})(?![\d-])", ptext, re.M)
866
991
  if not os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(feature)), u + ".feature"))]
@@ -881,6 +1006,10 @@ def guard_bdd(path, block):
881
1006
  ps = re.search(r"^status:\s*(\S+)", ptext, re.M)
882
1007
  if not ps or ps.group(1) != "approved":
883
1008
  fail("không đặt được status=approved: PRD chưa approved. Duyệt PRD trước.")
1009
+ rv = re.search(r"^reviewed:\s*(\S+)", ptext, re.M)
1010
+ if not rv or rv.group(1) != prd_version(ptext):
1011
+ fail("không đặt được status=approved: PRD v%s chưa rà hình thức. Chạy `/prd <EPIC-ID> review` trước."
1012
+ % prd_version(ptext))
884
1013
  bv = re.search(r"^prd_version:\s*(\S+)", block, re.M)
885
1014
  if not bv or bv.group(1) != prd_version(ptext):
886
1015
  fail("không đặt được status=approved: BDD viết theo PRD v%s, PRD đang ở v%s. Chạy `/bdd <UC>` để bù trước."
@@ -908,7 +1037,12 @@ def cmd_context(path, ucs):
908
1037
  if not span:
909
1038
  fail("không có %s trong %s" % (uc, path))
910
1039
  mention = re.compile(r"%s(?![\d-])" % re.escape(uc))
911
- out = ["# PRD %s — v%s · status %s" % (path, prd_version(text), (re.search(r"^status:\s*(\S+)", text, re.M) or [None, "?"])[1])]
1040
+ fm = lambda k: (re.search(r"^%s:\s*(\S+)" % k, text, re.M) or [None, "—"])[1]
1041
+ value = uc_platform_line(text, uc)
1042
+ plat = "chưa khai" if value is None else (" · ".join(uc_platforms(text, uc)) or "sai khuôn")
1043
+ # Dòng đầu đủ để /bdd chọn chế độ (Bước 2) mà không phải đọc frontmatter riêng.
1044
+ out = ["# PRD %s — v%s · status %s · reviewed %s · nền tảng của %s: %s"
1045
+ % (path, prd_version(text), fm("status"), fm("reviewed"), uc, plat)]
912
1046
  out.append(section_text(text, "overview") or "(PRD chưa có mục Tổng quan)")
913
1047
  flow = (section_text(text, "userflow") or "").split("\n")
914
1048
  rows = [l for l in flow if l.startswith("| J") and mention.search(l)]
@@ -1073,6 +1207,14 @@ def cmd_confirm(path, codes):
1073
1207
  # Mã khớp nguyên từ: AC2 không được khớp nhầm AC20.
1074
1208
  targets = {}
1075
1209
  for code in codes:
1210
+ # Dòng không có mã (vd dòng Nền tảng): xác nhận theo số dòng mà `pending` in ra, viết L24.
1211
+ ln = re.match(r"^L(\d+)$", code)
1212
+ if ln:
1213
+ i = int(ln.group(1)) - 1
1214
+ if not 0 <= i < len(lines) or not is_pending(lines[i]):
1215
+ fail("dòng %s không phải dòng 🤖. Chạy `pending` để xem số dòng các mục còn chờ." % ln.group(1))
1216
+ targets[i] = code
1217
+ continue
1076
1218
  pat = re.compile(r"(?<![\w-])%s(?![\w])" % re.escape(code))
1077
1219
  hits = [i for i, line in enumerate(lines) if is_pending(line) and pat.search(line)]
1078
1220
  if not hits:
@@ -1097,7 +1239,7 @@ def main(argv):
1097
1239
  print("ok — Python %s" % sys.version.split()[0])
1098
1240
  return
1099
1241
  if len(argv) < 2 or argv[0] not in ("create", "edit", "set", "pending", "confirm", "next-uc", "coverage", "section", "upgrade", "flowcheck", "item", "uc", "finding", "decide",
1100
- "where", "context", "next-sc", "scenario", "sc", "bddcheck"):
1242
+ "where", "context", "next-sc", "scenario", "sc", "bddcheck", "diff"):
1101
1243
  fail(__doc__.split("Cách dùng")[1], 2)
1102
1244
  cmd, path, rest = argv[0], argv[1], argv[2:]
1103
1245
  if cmd == "create":
@@ -1108,7 +1250,12 @@ def main(argv):
1108
1250
  else:
1109
1251
  cmd_create(path)
1110
1252
  elif cmd == "edit":
1111
- cmd_edit(path)
1253
+ if path == "--form":
1254
+ if not rest:
1255
+ fail("cách dùng: edit --form <file>", 2)
1256
+ cmd_edit(rest[0], form=True)
1257
+ else:
1258
+ cmd_edit(path)
1112
1259
  elif cmd == "pending":
1113
1260
  cmd_pending(path)
1114
1261
  elif cmd == "confirm":
@@ -1133,6 +1280,8 @@ def main(argv):
1133
1280
  cmd_next_uc(path)
1134
1281
  elif cmd == "where":
1135
1282
  cmd_where(path, rest)
1283
+ elif cmd == "diff":
1284
+ cmd_diff(path)
1136
1285
  elif cmd == "context":
1137
1286
  cmd_context(path, rest)
1138
1287
  elif cmd == "next-sc":