@educa-corp/fw 0.10.0 → 0.11.0

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,16 @@
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.0 — 2026-10-02
7
+
8
+ - **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ú.
9
+ - PRD đã duyệt từ trước: chạy `/prd <EPIC-ID> review` một lần, rồi mới chạy `/bdd`.
10
+ - **`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.
11
+ - **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.
12
+ - **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.
13
+ - `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.
14
+ - `/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`.
15
+
6
16
  ## 0.10.0 — 2026-10-02
7
17
 
8
18
  - **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.0**. 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 các mục vừa đổi** (theo dòng Lịch sử thay đổi), nhưng agent vẫn đọc cả PRD. 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,53 @@
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ú** | Muốn sửa thì phải **đổi nghĩa**: đổi số, điều kiện, phạm vi | Không sửa. Muốn đổi thì dùng `/prd EP-01 change` |
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. Chỗ đó được chuyển thành ghi chú C.
40
+ - Nhờ vậy, review không cần refine lại sau đó.
41
+
42
+ ## Phạm vi
43
+
44
+ | Khi nào | Rà gì |
45
+ |---|---|
46
+ | PRD chưa từng review | Toàn bộ PRD |
47
+ | Sau `/prd EP-01 change` → refine | Chỉ các mục vừa đổi, theo Lịch sử thay đổi |
48
+
49
+ Thứ tự sau mỗi lần đổi PRD: `change` → `refine` → `review` → duyệt.
50
+
51
+ ## PRD đã duyệt trước bản 0.11.0
52
+
53
+ 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.0",
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/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** | Chỉ các mục trong các dòng Lịch sử thay đổi (`SE section D history`) có version lớn hơn `refined` |
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,11 +27,24 @@ 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. Đếm UC bằng các mã `UC-…` nêu trong phạm vi. 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
 
@@ -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,77 @@
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: chỉ các mục nêu trong các dòng Lịch sử thay đổi (`SE section D history`) 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):** những chỗ muốn sửa thì phải đổi nghĩa. Mỗi chỗ một dòng, kèm gợi ý `/prd {id} change …`.
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). `SE` chặn chỗ nào thì chỗ đó làm đổi nghĩa: bỏ chỗ đó ra khỏi lần sửa, báo PO, và chuyển nó thành ghi chú C.
39
+ 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 |`.
40
+ 3. `SE set D reviewed={version} updated={ngày}`.
41
+ - 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.
42
+ - 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.
43
+ 4. Còn ghi chú C thì nhắc PO một dòng. Ghi chú **không chặn** duyệt.
44
+
45
+ ---
46
+
47
+ ## Dành cho agent rà
48
+
49
+ 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`.
50
+
51
+ **Ba loại:**
52
+
53
+ | Loại | Gồm | Làm gì |
54
+ |---|---|---|
55
+ | **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 |
56
+ | **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) · một khái niệm gọi hai tên, hoặc sai so với glossary và phần Khái niệm · dòng sai khuôn, ví dụ dòng Nền tảng ghi bằng lời thay vì mã `web` · `app` · `system` | **Không sửa.** Ghi vào file để PO quyết |
57
+ | **C. Ghi chú** | Chỗ chỉ sửa được nếu đổi nghĩa: số, điều kiện, phạm vi, thêm hoặc bớt ý | **Không sửa.** Ghi một dòng |
58
+
59
+ **Không làm:** rà nội dung nghiệp vụ (đó là việc của refine), đổi mã, đổi số, đổi dấu `🤖`/`✅`, đổi `(nguồn: …)`, sửa phần ngoài phạm vi. Viết lại câu thì giữ đúng mọi ý và mọi giá trị của câu cũ.
60
+
61
+ **Ghi file** `W/review-{version}.md` bằng `SE create --replace`, đúng khuôn sau:
62
+
63
+ ```
64
+ ## Đã sửa (A)
65
+ - {mã mục}: "{trước}" → "{sau}"
66
+
67
+ ## Đề xuất (B)
68
+ ### R01 · {câu chữ | thuật ngữ | khuôn} · {mã mục}
69
+ - Trước: "{nguyên văn trong PRD, đủ dài để chỉ xuất hiện một lần}"
70
+ - Sau: "{…}"
71
+ - Lý do: {…}
72
+
73
+ ## Ghi chú (C)
74
+ - {mã mục}: {vấn đề} → cần `/prd … change`
75
+ ```
76
+
77
+ 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)
@@ -110,7 +111,20 @@ def cmd_create(path, replace=False):
110
111
  print("đã tạo " + path)
111
112
 
112
113
 
113
- def cmd_edit(path):
114
+ # 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.
115
+ 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+)*"
116
+ "|[\U0001F916✅]|~~|\\(nguồn:[^)]*\\)")
117
+
118
+
119
+ def form_changes(old, new):
120
+ """Những thứ mang nghĩa bị thêm / mất khi đổi old → new. Rỗng = chỉ đổi hình thức."""
121
+ a, b = sorted(FORM_KEEP.findall(old)), sorted(FORM_KEEP.findall(new))
122
+ lost = [x for x in a if a.count(x) > b.count(x)]
123
+ added = [x for x in b if b.count(x) > a.count(x)]
124
+ return sorted(set(lost)), sorted(set(added))
125
+
126
+
127
+ def cmd_edit(path, form=False):
114
128
  try:
115
129
  edits = json.loads(stdin_text())
116
130
  except json.JSONDecodeError as e:
@@ -131,6 +145,12 @@ def cmd_edit(path):
131
145
  if n > 1 and not replace_all:
132
146
  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
147
  "hoặc đặt \"all\": true.\n old = %r" % (i, n, old[:120]))
148
+ if form:
149
+ lost, added = form_changes(old, e["new"])
150
+ if lost or added:
151
+ 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. "
152
+ "Sửa làm đổi nghĩa thì ghi thành ghi chú, đi qua `/prd … change`.\n old = %r"
153
+ % (i, ("mất " + " ".join(lost)) if lost else "", (" · thêm " + " ".join(added)) if added else "", old[:120]))
134
154
  before = sec_keys(text)
135
155
  for e in edits:
136
156
  text = text.replace(e["old"], e["new"]) if e.get("all") else text.replace(e["old"], e["new"], 1)
@@ -163,8 +183,9 @@ def validate(ftype, key, value):
163
183
  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
184
  if key in ("version", "prd_version") and not VERSION.match(value):
165
185
  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)
186
+ if key in ("refined", "reviewed") and value != "—" and not VERSION.match(value):
187
+ fail("%s=%s không hợp lệ. Là version PRD đã %s xong (vd 1.1), hoặc —"
188
+ % (key, value, "refine" if key == "refined" else "rà hình thức"))
168
189
 
169
190
 
170
191
  # ── Refine: finding critical / major chưa có quyết định thì chưa được đi tiếp ──────────────
@@ -240,6 +261,15 @@ def guard_refined(path, block):
240
261
  % (", ".join(pending), rp))
241
262
 
242
263
 
264
+ def guard_reviewed(block):
265
+ """PRD chỉ được duyệt khi đã rà hình thức (/prd review) đúng version đang có. Review đi sau refine."""
266
+ ver = re.search(r"^version:\s*(\S+)", block, re.M)
267
+ rev = re.search(r"^reviewed:\s*(\S+)", block, re.M)
268
+ if not rev or not ver or rev.group(1) != ver.group(1):
269
+ 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."
270
+ % (ver.group(1) if ver else "?", rev.group(1) if rev else "chưa có"))
271
+
272
+
243
273
  def guard_done(ftype, block, body, path):
244
274
  """Chặn đặt trạng thái 'đã xong' khi còn mục chờ PO — AI không được tự khẳng định."""
245
275
  done = DONE_STATUS.get(ftype)
@@ -260,12 +290,11 @@ def guard_done(ftype, block, body, path):
260
290
  fail("không đặt được status=%s: chưa có %s. Đặt cùng lệnh, ví dụ: "
261
291
  "set <file> status=%s approved_by=<tên> approved_at=YYYY-MM-DD" % (done, field, done))
262
292
  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))
293
+ problems = platform_problems(read(path))
294
+ if problems:
295
+ fail("không đặt được status=approved:\n - " + "\n - ".join(problems))
268
296
  guard_refined(path, block)
297
+ guard_reviewed(block)
269
298
  if ftype == "bdd":
270
299
  guard_bdd(path, block)
271
300
  if ftype == "prd-refine" and undecided_findings(body):
@@ -685,26 +714,62 @@ def prd_items(text):
685
714
  return out
686
715
 
687
716
 
717
+ # 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).
718
+ PLATFORMS = ("web", "app", "system")
719
+ 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), "
720
+ "giải thích để sau dấu ` — `. Ví dụ: `- **Nền tảng:** system — luồng tự động nhận đơn từ EP-03`")
721
+
722
+
688
723
  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())]
724
+ """(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."""
725
+ head = re.split(r"\(| — | – | - ", value.replace("✅", "").replace("\U0001F916", ""))[0]
726
+ words = [w for w in re.split(r"[·,/\s]+", head.strip()) if w]
727
+ return [w for w in words if w in PLATFORMS], [w for w in words if w not in PLATFORMS]
691
728
 
692
729
 
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."""
730
+ def uc_platform_line(text, uc):
731
+ """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
732
  lines = text.split("\n")
696
733
  span = uc_span(lines, uc)
697
734
  if span:
698
735
  for l in lines[span[0]:span[1]]:
699
736
  m = PLATFORM_LINE.match(l)
700
737
  if m:
701
- return platform_values(m.group(1))
702
- overview = section_text(text, "overview") or ""
703
- for l in overview.split("\n"):
738
+ return m.group(1)
739
+ for l in (section_text(text, "overview") or "").split("\n"):
704
740
  m = PLATFORM_LINE.match(l)
705
741
  if m:
706
- return platform_values(m.group(1))
707
- return []
742
+ return m.group(1)
743
+ return None
744
+
745
+
746
+ def uc_platforms(text, uc):
747
+ """Mã nền tảng của UC. [] = chưa khai, hoặc khai sai khuôn (xem platform_problems)."""
748
+ value = uc_platform_line(text, uc)
749
+ if value is None:
750
+ return []
751
+ good, bad = platform_values(value)
752
+ return [] if bad else good
753
+
754
+
755
+ def platform_problems(text):
756
+ """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)."""
757
+ missing, wrong = [], []
758
+ for uc in re.findall(r"^###\s+(UC-\d{3})(?![\d-])", text, re.M):
759
+ value = uc_platform_line(text, uc)
760
+ if value is None:
761
+ missing.append(uc)
762
+ continue
763
+ good, bad = platform_values(value)
764
+ if bad or not good:
765
+ wrong.append('%s ("%s")' % (uc, value.strip()[:50]))
766
+ out = []
767
+ if missing:
768
+ 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), "
769
+ "hoặc trong UC chạy khác mặc định." % ", ".join(missing))
770
+ if wrong:
771
+ out.append("Nền tảng ghi sai khuôn ở %s: %s" % (", ".join(wrong), PLATFORM_RULE))
772
+ return out
708
773
 
709
774
 
710
775
  def prd_version(text):
@@ -790,9 +855,11 @@ def bdd_report(feature, prd=None):
790
855
  items = prd_items(ptext)
791
856
  own = [c for c, alive in items.items() if alive and c.startswith(uc + "-")]
792
857
  platforms = uc_platforms(ptext, uc)
793
- if not platforms:
858
+ if uc_platform_line(ptext, uc) is None:
794
859
  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
860
  "Chạy `/prd <EPIC-ID> change khai nền tảng …` trước." % uc)
861
+ elif not platforms:
862
+ 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
863
  scenarios, removed = parse_feature(text)
797
864
  if not scenarios:
798
865
  errors.append("file chưa có kịch bản nào")
@@ -860,7 +927,7 @@ def cmd_bddcheck(feature, prd=None):
860
927
  if st:
861
928
  print("%s: %d kịch bản (%d đã bỏ) · AC %d/%d · BR %d/%d · %s" % (
862
929
  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)
930
+ " · ".join("%s %d" % kv for kv in st["platforms"].items()) or "chưa có nền tảng hợp lệ"), flush=True)
864
931
  ptext = read(prd or prd_of(feature))
865
932
  todo = [u for u in re.findall(r"^###\s+(UC-\d{3})(?![\d-])", ptext, re.M)
866
933
  if not os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(feature)), u + ".feature"))]
@@ -881,6 +948,10 @@ def guard_bdd(path, block):
881
948
  ps = re.search(r"^status:\s*(\S+)", ptext, re.M)
882
949
  if not ps or ps.group(1) != "approved":
883
950
  fail("không đặt được status=approved: PRD chưa approved. Duyệt PRD trước.")
951
+ rv = re.search(r"^reviewed:\s*(\S+)", ptext, re.M)
952
+ if not rv or rv.group(1) != prd_version(ptext):
953
+ 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."
954
+ % prd_version(ptext))
884
955
  bv = re.search(r"^prd_version:\s*(\S+)", block, re.M)
885
956
  if not bv or bv.group(1) != prd_version(ptext):
886
957
  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 +979,12 @@ def cmd_context(path, ucs):
908
979
  if not span:
909
980
  fail("không có %s trong %s" % (uc, path))
910
981
  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])]
982
+ fm = lambda k: (re.search(r"^%s:\s*(\S+)" % k, text, re.M) or [None, "—"])[1]
983
+ value = uc_platform_line(text, uc)
984
+ plat = "chưa khai" if value is None else (" · ".join(uc_platforms(text, uc)) or "sai khuôn")
985
+ # Dòng đầu đủ để /bdd chọn chế độ (Bước 2) mà không phải đọc frontmatter riêng.
986
+ out = ["# PRD %s — v%s · status %s · reviewed %s · nền tảng của %s: %s"
987
+ % (path, prd_version(text), fm("status"), fm("reviewed"), uc, plat)]
912
988
  out.append(section_text(text, "overview") or "(PRD chưa có mục Tổng quan)")
913
989
  flow = (section_text(text, "userflow") or "").split("\n")
914
990
  rows = [l for l in flow if l.startswith("| J") and mention.search(l)]
@@ -1073,6 +1149,14 @@ def cmd_confirm(path, codes):
1073
1149
  # Mã khớp nguyên từ: AC2 không được khớp nhầm AC20.
1074
1150
  targets = {}
1075
1151
  for code in codes:
1152
+ # 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.
1153
+ ln = re.match(r"^L(\d+)$", code)
1154
+ if ln:
1155
+ i = int(ln.group(1)) - 1
1156
+ if not 0 <= i < len(lines) or not is_pending(lines[i]):
1157
+ 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))
1158
+ targets[i] = code
1159
+ continue
1076
1160
  pat = re.compile(r"(?<![\w-])%s(?![\w])" % re.escape(code))
1077
1161
  hits = [i for i, line in enumerate(lines) if is_pending(line) and pat.search(line)]
1078
1162
  if not hits:
@@ -1108,7 +1192,12 @@ def main(argv):
1108
1192
  else:
1109
1193
  cmd_create(path)
1110
1194
  elif cmd == "edit":
1111
- cmd_edit(path)
1195
+ if path == "--form":
1196
+ if not rest:
1197
+ fail("cách dùng: edit --form <file>", 2)
1198
+ cmd_edit(rest[0], form=True)
1199
+ else:
1200
+ cmd_edit(path)
1112
1201
  elif cmd == "pending":
1113
1202
  cmd_pending(path)
1114
1203
  elif cmd == "confirm":