@educa-corp/fw 0.6.2 → 0.8.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,24 @@
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.8.0 — 2026-10-01
7
+
8
+ - PRD có mục mới **User flow**: các hành trình xuyên suốt nhiều UC, mỗi hành trình gồm một dòng trong bảng và một sơ đồ mermaid, có nhánh lỗi. **Mỗi UC phải có mặt trong ít nhất một hành trình**, `spec_edit flowcheck` kiểm và PRD thiếu User flow thì không duyệt được. PRD đã có: chạy `/prd <EPIC-ID> change thêm User flow`.
9
+ - `/prd refine` rẻ hơn: các agent ghi kết quả ra file trong `.fw/tmp/`, không đổ báo cáo về phiên chính. Agent kiểm chứng **kiểm từng finding gốc trước, gộp sau**, rồi tự ghi vào `prd-refine.md`. Lăng kính Phạm vi chỉ đọc đúng phần cần của epic và `product.md`. Mọi lăng kính vẫn đọc **toàn bộ PRD**.
10
+ - Finding được **sắp theo mức độ, rồi theo lăng kính** (QA → DEV → SA → Phạm vi). Mã F đánh theo thứ tự đó.
11
+ - **Quyết theo nhóm** cho major và minor (*"nhận tất cả minor"*, *"nhận hết UC-003 trừ F12"*). Critical vẫn phải quyết từng mã.
12
+ - Bảng Tóm tắt của refine ghi số nhận · sửa · bác · hoãn, để đo lớp kiểm chứng.
13
+ - Lăng kính SA soi thêm User flow: ngõ cụt, thiếu nhánh lỗi, sơ đồ không khớp UC.
14
+ - `fw install` thêm `.fw/tmp/` vào `.gitignore`.
15
+
16
+ ## 0.7.0 — 2026-10-01
17
+
18
+ - Chế độ mới **`/prd <EPIC-ID> refine`**: rà nội dung PRD qua 4 lăng kính (QA · DEV · SA · Phạm vi). Mỗi lăng kính là một sub-agent **Opus**, sau đó một agent **kiểm chứng** loại bỏ finding không đứng vững. Bạn quyết từng finding: nhận, sửa, bác hoặc hoãn.
19
+ - Chống vòng lặp rà không dứt: **tối đa 2 lượt** cho mỗi version. Lượt 2 chỉ soi phần vừa sửa, chỉ báo critical hoặc lỗi do bản sửa. File `review/prd-refine.md` là **sổ quyết định**, điểm đã quyết không bị nêu lại.
20
+ - **Bắt buộc refine trước khi duyệt PRD**: `status=approved` bị chặn khi `refined` khác `version`, hoặc còn critical/major chưa quyết. Sau `/prd change` thì refine lại, và chỉ rà phần vừa đổi.
21
+ - `spec_edit upgrade` bổ sung các trường frontmatter còn thiếu so với khuôn mới (ví dụ `refined`).
22
+ - Lệnh rà hình thức (chính tả, trình bày) sẽ là `/review <file>`, bỏ tên `/review-context`.
23
+
6
24
  ## 0.6.2 — 2026-09-30
7
25
 
8
26
  - **Hướng dẫn sử dụng** được cài vào dự án ở `.fw/guide/`, đúng với version đang dùng và đọc được khi không có mạng. Bắt đầu từ `.fw/guide/README.md`. Có thể hỏi Claude, ví dụ *"Đọc .fw/guide và cho biết cách đổi PRD"*.
package/bin/fw.js CHANGED
@@ -32,7 +32,8 @@ const SOURCES = [
32
32
  const RUNTIME_FILES = ['package.json', 'CHANGELOG.md', 'bin/config.template.yaml', ...SOURCES.map((s) => s.from)];
33
33
  const CONFIG = '.fw/config.yaml';
34
34
  const SETTINGS = '.claude/settings.json';
35
- const GITIGNORE_LINE = '.fw/backup/';
35
+ // Không commit: bản sao file đã sửa (khi nâng cấp) và file tạm của các agent.
36
+ const GITIGNORE_LINES = ['.fw/backup/', '.fw/tmp/'];
36
37
 
37
38
  // Lệnh AI dùng để sửa file spec (xem tools/spec_edit.py). Cho phép sẵn để PO không bị hỏi quyền mỗi lần.
38
39
  const SPEC_EDIT = '.fw/core/tools/spec_edit.py';
@@ -100,16 +101,17 @@ function allowSpecEdit(target) {
100
101
  return { added };
101
102
  }
102
103
 
103
- // .fw/backup/ chứa bản sao file người dùng đã sửa, giữ lại khi nâng cấp — không nên vào git.
104
- function ignoreBackup(target) {
104
+ // Thêm các dòng còn thiếu vào .gitignore, giữ nguyên nội dung cũ. Trả về các dòng đã thêm.
105
+ function ignoreFwFiles(target) {
105
106
  const p = path.join(target, '.gitignore');
106
107
  const text = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
107
108
  const lines = text.split(/\r?\n/).map((l) => l.trim());
108
- if (lines.includes(GITIGNORE_LINE) || lines.includes('/' + GITIGNORE_LINE)) return false;
109
+ const add = GITIGNORE_LINES.filter((g) => !lines.includes(g) && !lines.includes('/' + g));
110
+ if (!add.length) return [];
109
111
  const sep = text && !text.endsWith('\n') ? '\n' : '';
110
- const block = '\n# @educa-corp/fw — bản sao file đã sửa, tạo khi nâng cấp\n' + GITIGNORE_LINE + '\n';
112
+ const block = '\n# @educa-corp/fw — bản sao khi nâng cấp, file tạm của agent\n' + add.join('\n') + '\n';
111
113
  fs.writeFileSync(p, text + sep + block);
112
- return true;
114
+ return add;
113
115
  }
114
116
 
115
117
  function install(target) {
@@ -177,7 +179,7 @@ function install(target) {
177
179
  changes: changesSince(old.version),
178
180
  python: findPython(),
179
181
  permissions: allowSpecEdit(target),
180
- gitignore: ignoreBackup(target),
182
+ gitignore: ignoreFwFiles(target),
181
183
  };
182
184
  }
183
185
 
@@ -208,7 +210,7 @@ function printReport(r) {
208
210
  }
209
211
  console.log(` thêm ${r.added.length} · cập nhật ${r.updated.length} · gỡ ${r.removed.length}`);
210
212
  if (r.configCreated) console.log(` tạo ${CONFIG} — hãy mở file này và khai specs, tracker, units`);
211
- if (r.gitignore) console.log(` thêm ${GITIGNORE_LINE} vào .gitignore`);
213
+ if (r.gitignore.length) console.log(` thêm ${r.gitignore.join(', ')} vào .gitignore`);
212
214
  if (r.permissions.error) console.log(` ⚠ ${r.permissions.error}`);
213
215
  else if (r.permissions.added.length) console.log(` thêm ${r.permissions.added.length} luật cho phép vào ${SETTINGS} (AI sửa spec không phải hỏi quyền)`);
214
216
  if (r.python) {
package/commands/prd.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: Viết PRD chính thức từ epic đã làm rõ (EPIC-ID), hoặc đổi PRD đã có (EPIC-ID change)
3
- argument-hint: "<EPIC-ID> [change <mô tả thay đổi>] [--force]"
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]"
4
4
  ---
5
5
 
6
6
  # /prd
@@ -29,9 +29,10 @@ Nếu **trước lệnh này** phiên đã có hội thoại khác, dòng đầu
29
29
  |---|---|
30
30
  | Không có `change`, chưa có `D`, epic `ready` | Chế độ **tạo**: đọc `.fw/core/ref/prd/new.md` |
31
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 4 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ở |
32
- | Không có `change`, **đã có** `D` | Dừng: *"PRD đã có. Muốn đổi thì chạy `/prd EP-xx change <mô tả>`."* Không ghi đè |
32
+ | 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 đè |
33
+ | Có `refine`, đã có `D` | Chế độ **rà nội dung**: đọc `.fw/core/ref/prd/refine.md` |
33
34
  | Có `change`, đã có `D` | Chế độ **đổi**: đọc `.fw/core/ref/prd/change.md` |
34
- | Có `change`, chưa có `D` | Dừng: *"Chưa có PRD. Chạy `/prd EP-xx` trước."* |
35
+ | Có `change` hoặc `refine`, chưa có `D` | Dừng: *"Chưa có PRD. Chạy `/prd EP-xx` trước."* |
35
36
 
36
37
  ## Bước 3 — Luật chung
37
38
 
@@ -43,10 +44,10 @@ Nếu **trước lệnh này** phiên đã có hội thoại khác, dòng đầu
43
44
  6. **Sửa file: CHỈ dùng `spec_edit.py`** (gọi là `SE`). Không dùng Edit/Write, không tự viết script:
44
45
  - Tạo: `SE create <file> <<'EOF'` … `EOF` · Sửa: `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)
45
46
  - Frontmatter: `SE set <file> version=1.1 status=draft updated=YYYY-MM-DD`
46
- - `SE pending <file>` · `SE confirm <file> UC-003-BR02 UC-003-AC01` · `SE next-uc {specs}` · `SE coverage <epic> <prd>`
47
+ - `SE pending <file>` · `SE confirm <file> UC-003-BR02 UC-003-AC01` · `SE next-uc {specs}` · `SE coverage <epic> <prd>` · `SE flowcheck <prd>`
47
48
  - Đọc **một mục**: `SE section <file> <mã>`. Mã mục là `<!-- sec:… -->` ở tiêu đề (vd `constraints`, `usecases`, `open`). **Luôn tìm mục theo mã**, không theo số hay tên tiêu đề, không tự cắt file bằng `sed`/`grep`. Khi sửa, giữ nguyên `<!-- sec:… -->`.
48
49
  - `SE` báo lỗi thì **không có gì được ghi**. Đọc lại file, sửa lệnh rồi chạy lại. Không bỏ qua lỗi. Lỗi mà **không in dòng nào** là Python chưa kịp chạy: chạy lại đúng lệnh đó một lần.
49
- 7. **Duyệt.** `status=approved` chỉ đặt được khi không còn `🤖`, `open_questions=0`, và có người 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 đủ. Không tìm cách lách.
50
+ 7. **Duyệt.** `status=approved` chỉ đặt được khi không còn `🤖`, `open_questions=0`, **đã 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 đủ. Không tìm cách lách.
50
51
 
51
52
  ## Bước 4 — Kết thúc
52
53
 
@@ -56,7 +57,7 @@ In ra đúng khối sau:
56
57
  ---
57
58
  Trạng thái : {✅ Đã duyệt v{version} | 🟡 Nháp v{version} — còn {n} mục 🤖, {k} câu hỏi mở}
58
59
  Đã ghi : {D} {· epic → handed-off · product.md nếu có}
59
- Kiểm : coverage {đủ | thiếu …} · {số UC} UC · {số BR} BR · {số AC} AC
60
+ Kiểm : coverage {đủ | thiếu …} · user flow {đủ | thiếu …} · {số UC} UC · {số BR} BR · {số AC} AC
60
61
  Luồng : Product → [PRD ◀ bạn ở đây] → BDD → TDD · Design-spec → Code → Test → QC
61
- Bước tiếp : {/bdd UC-xxx | trả lời / xác nhận các mục trên | `/clear` rồi `/prd {id}` để tiếp}
62
+ 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}
62
63
  ```
@@ -25,10 +25,10 @@ npx @educa-corp/fw install
25
25
  Kết quả mẫu:
26
26
 
27
27
  ```
28
- fw 0.6.2 — đã cài
29
- thêm 20 · cập nhật 0 · gỡ 0
28
+ fw 0.8.0 — đã cài
29
+ thêm 24 · cập nhật 0 · gỡ 0
30
30
  tạo .fw/config.yaml — hãy mở file này và khai specs, tracker, units
31
- thêm .fw/backup/ vào .gitignore
31
+ thêm .fw/backup/, .fw/tmp/ vào .gitignore
32
32
  thêm 3 luật cho phép vào .claude/settings.json (AI sửa spec không phải hỏi quyền)
33
33
  Python: python
34
34
  ```
@@ -43,6 +43,7 @@ Lệnh tạo ra:
43
43
  | `.fw/config.yaml` | **Cấu hình của dự án. Bạn được sửa** | ✅ Có |
44
44
  | `.fw/guide/` | Bộ hướng dẫn này, đúng với version đang cài | ✅ Có |
45
45
  | `.fw/backup/` | Bản sao file bạn đã tự sửa, được giữ lại khi nâng cấp | ❌ Không. `fw install` đã tự thêm vào `.gitignore` |
46
+ | `.fw/tmp/` | File tạm của các agent (ví dụ khi refine) | ❌ Không. Đã tự thêm vào `.gitignore` |
46
47
 
47
48
  Commit xong, đồng đội clone về là **có ngay**, không cần tự cài.
48
49
 
@@ -25,6 +25,7 @@ Chỉ những khái niệm bạn **gặp khi dùng** các lệnh hiện có.
25
25
  | **Business Logic** | Cách luật được thực hiện: rẽ nhánh, công thức, thông báo khi lỗi | Email trống hoặc đã có tài khoản → từ chối, báo lý do |
26
26
  | **AC** (Acceptance Criteria, điều kiện nghiệm thu) | Điều kiện kiểm được là đạt hay không đạt, dạng *"Khi … thì …"* | UC-002-AC01: Khi tài khoản mới quá 7 ngày chưa đăng nhập thì bị chuyển inactive, đăng nhập bị từ chối |
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
+ | **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 |
28
29
  | **Checkpoint** | Một điểm dừng để bạn chốt. `/product` có 2 (sản phẩm) hoặc 3 (epic) checkpoint | — |
29
30
 
30
31
  ## 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.6.2**. 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.8.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
  ---
@@ -21,10 +21,10 @@
21
21
  ## Pipeline hiện có
22
22
 
23
23
  ```
24
- cài framework ──► /product ──► /product EP-xx ──► /prd EP-xx ┄┄► (bước tiếp: /bdd — sắp có)
25
- tầng làm rõ một viết PRD
26
- sản phẩm tính năng ▲ │
27
- └─┘ /prd EP-xx change (đổi PRD)
24
+ cài framework ──► /product ──► /product EP-xx ──► /prd EP-xx ──► /prd EP-xx refine ──► duyệt ┄┄► (/bdd — sắp có)
25
+ tầng làm rõ một viết PRD rà nội dung
26
+ sản phẩm tính năng ▲ │ (bắt buộc)
27
+ └── /prd EP-xx change ◄──┘ đổi PRD thì refine lại
28
28
  ```
29
29
 
30
30
  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**:
@@ -48,6 +48,7 @@ Bước tiếp : `/clear` rồi `/product EP-01`
48
48
  | `/product` | PO / BA | Làm rõ **tầng sản phẩm**: tầm nhìn, nhóm người dùng, danh sách epic, ràng buộc | [/product](lenh/product.md) |
49
49
  | `/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) |
50
50
  | `/prd <EPIC-ID>` | PO / BA | Viết **PRD chính thức** từ epic đã làm rõ | [/prd](lenh/prd.md) |
51
+ | `/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) |
51
52
  | `/prd <EPIC-ID> change <mô tả>` | PO / BA | Thêm, sửa hoặc bỏ nội dung PRD | [/prd](lenh/prd.md) |
52
53
 
53
54
  ---
@@ -0,0 +1,95 @@
1
+ [← Hướng dẫn](../README.md) · [Bảng lệnh](../README.md#bảng-lệnh) · [/prd](prd.md)
2
+
3
+ # `/prd EP-01 refine` — rà nội dung PRD
4
+
5
+ > AI rà PRD qua **4 lăng kính** để tìm chỗ nghiệp vụ còn thiếu, mơ hồ hoặc mâu thuẫn, tức những chỗ sẽ làm dev hay QC làm sai hoặc phải đoán. **Bắt buộc**: chưa refine thì không duyệt được PRD.
6
+ > Không rà chính tả hay cách trình bày. Việc đó là của `/review` *(sắp có)*.
7
+
8
+ | | |
9
+ |---|---|
10
+ | **Gõ** | `/prd EP-01 refine` |
11
+ | **Ai** | PO / BA |
12
+ | **Cần có trước** | PRD đã có |
13
+ | **Số lượt rà** | Tối đa **2** cho mỗi version PRD |
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 |
16
+
17
+ ---
18
+
19
+ ## 4 lăng kính
20
+
21
+ | Lăng kính | Soi | Ví dụ finding |
22
+ |---|---|---|
23
+ | **QA** | AC có kiểm được là đạt hay không? AC có lặp lại BR không? | *"UC-002-AC03 nói 'hiển thị thông báo phù hợp' — không kiểm được"* |
24
+ | **DEV** | BR và Business Logic có đủ để làm mà không phải đoán? Nhánh thiếu, điều kiện biên, xử lý khi lỗi, BR mâu thuẫn | *"Gửi email đặt lại mật khẩu thất bại thì sao?"* |
25
+ | **SA** | Các UC có thông suốt, nhất quán? **User flow** có ngõ cụt hay thiếu nhánh lỗi không? Trạng thái, vòng đời của đối tượng; ai được làm gì | *"UC-002 cho Admin khoá tài khoản, nhưng UC-009 tự tạo tài khoản không nói trạng thái ban đầu"* |
26
+ | **Phạm vi** | PRD có vượt ngoài phạm vi epic, đi ngược ràng buộc `CON-xx`, hay giẫm lên epic khác không? | *"UC-009 xử lý đơn hàng CRM — thuộc EP-03"* |
27
+
28
+ *(Các ví dụ trên là ví dụ minh hoạ.)*
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.
31
+
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
+
34
+ ## Mức độ
35
+
36
+ | Mức | Nghĩa | Có chặn duyệt không |
37
+ |---|---|---|
38
+ | `critical` | Không sửa thì sẽ **làm sai** hoặc không làm được | Có, cho tới khi bạn quyết |
39
+ | `major` | Không sửa thì người làm **phải đoán** | Có, cho tới khi bạn quyết |
40
+ | `minor` | Làm rõ thêm cho dễ đọc, không đổi hành vi | Không |
41
+
42
+ ## Bạn quyết thế nào
43
+
44
+ AI trình bảng tóm tắt, **sắp theo mức độ** (critical → major → minor), rồi **theo lăng kính** (QA → DEV → SA → Phạm vi). Mã F đánh theo đúng thứ tự đó, nên F01 luôn là finding nặng nhất. Chi tiết từng finding nằm trong `review/prd-refine.md`.
45
+
46
+ Quyết theo mã:
47
+
48
+ ```
49
+ nhận F01, F03 · bác F02 vì đã có ở UC-002 · F04 sửa thành "khoá 30 phút" · hoãn F05
50
+ ```
51
+
52
+ Hoặc **quyết theo nhóm**, áp cho major và minor:
53
+
54
+ ```
55
+ nhận tất cả minor · nhận hết UC-003 trừ F12 · bác F20–F24 vì đã có ở epic khác
56
+ ```
57
+
58
+ Quyết theo nhóm thì AI **liệt kê lại đúng các mã** sẽ bị áp rồi hỏi bạn xác nhận. **Critical luôn phải quyết từng mã.**
59
+
60
+ | Quyết định | Kết quả |
61
+ |---|---|
62
+ | `nhận` | AI sửa PRD theo đề xuất |
63
+ | `sửa: …` | AI sửa PRD theo nội dung bạn đưa |
64
+ | `bác: <lý do>` | Không sửa. Lần rà sau **không nêu lại** điểm này |
65
+ | `hoãn` | Không sửa lúc này. Được ghi vào "Câu hỏi còn mở" của PRD để bước QC xử lý |
66
+
67
+ Bác hay hoãn đều tính là **đã quyết**. Chỉ cần mọi critical và major đều đã có quyết định là đi tiếp được.
68
+
69
+ ## Vì sao chỉ có 2 lượt
70
+
71
+ Kinh nghiệm cho thấy: rà → sửa → rà lại thì lần nào cũng ra vấn đề mới, nhiều cái là ảo giác, và vòng lặp không dứt. Vì vậy:
72
+
73
+ | Lượt | Soi gì | Được báo gì |
74
+ |---|---|---|
75
+ | **1** | Toàn bộ PRD | Mọi mức độ |
76
+ | **2** (chỉ khi lượt 1 có bản sửa) | **Chỉ phần vừa sửa**, và các mục tham chiếu tới nó | **Chỉ** critical, hoặc lỗi do chính bản sửa gây ra |
77
+ | 3 | **Không có** | — |
78
+
79
+ Thêm vào đó, file refine là **sổ quyết định**: lần rà sau đọc lại sổ và không nêu lại những điểm bạn đã quyết.
80
+
81
+ Sau mỗi lượt có sửa, lệnh sẽ nhắc *"`/clear` rồi `/prd EP-01 refine`"* để chạy lượt 2.
82
+
83
+ ## Sau khi đổi PRD
84
+
85
+ `/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ẻ.
86
+
87
+ ## Duyệt PRD
88
+
89
+ PRD chỉ được đặt `approved` khi đủ cả 4 điều kiện:
90
+ 1. Không còn 🤖.
91
+ 2. Không còn câu hỏi mở.
92
+ 3. **Đã refine đúng version này** (`refined` = `version`), và không còn critical/major chưa quyết.
93
+ 4. Đã ghi người duyệt.
94
+
95
+ Công cụ tự chặn nếu thiếu điều kiện nào.
@@ -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** | Duyệt PRD → `/bdd UC-xxx` *(sắp có)* | Duyệt lại bản mới |
14
+ | **Bước tiếp** | [`/prd EP-01 refine`](prd-refine.md) → duyệt PRD → `/bdd UC-xxx` *(sắp có)* | [`/prd EP-01 refine`](prd-refine.md) (chỉ rà phần vừa đổi) → duyệt lại |
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. Hết 🤖 và hết câu hỏi mở thì AI hỏi *"Ai duyệt PRD này?"*, sau đó đặ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). 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`.
38
38
 
39
39
  ## PRD trông như thế nào
40
40
 
@@ -51,7 +51,23 @@ Acceptance Criteria (điều kiện nghiệm thu)
51
51
  - UC-001-AC01. Khi Admin import file 10 dòng, trong đó 2 dòng trùng email thì 8 tài khoản được tạo, 2 dòng được báo lỗi kèm lý do. (nguồn: AC1)
52
52
  ```
53
53
 
54
- Gồm 5 mục: **Tổng quan** (mục tiêu, actor, phạm vi, ràng buộc áp dụng) · **Use case** · **Màn hình** · **Câu hỏi còn mở** · **Lịch sử thay đổi**. Cột `Nguồn` chỉ ghi mã (xem [Khái niệm](../02-khai-niem.md#nguồn-của-từng-mục-trong-prd)).
54
+ Gồm 6 mục: **Tổng quan** (mục tiêu, actor, phạm vi, ràng buộc áp dụng) · **User flow** · **Use case** · **Màn hình** · **Câu hỏi còn mở** · **Lịch sử thay đổi**.
55
+
56
+ **User flow** là các **hành trình** xuyên suốt nhiều UC: actor đi qua những UC nào, rẽ nhánh ở đâu, gặp lỗi thì đi đâu. Mỗi hành trình gồm một dòng trong bảng và một sơ đồ (GitLab hiển thị sẵn):
57
+
58
+ ````
59
+ | J1 | Nhân sự mới vào hệ thống | ACT-03 | UC-001 → UC-003 → UC-005 | Admin tạo tài khoản | Đăng nhập được, đã đổi mật khẩu |
60
+
61
+ ```mermaid
62
+ flowchart TD
63
+ A["UC-001 Admin tạo tài khoản"] --> B{"Đăng nhập lần đầu bằng?"}
64
+ B -->|Mật khẩu mặc định| C["UC-005 Bắt đổi mật khẩu"]
65
+ B -->|Google / Facebook| D["UC-004 Ghép theo email"]
66
+ C -->|Trùng mật khẩu mặc định| X["Từ chối, nhập lại"]
67
+ ```
68
+ ````
69
+
70
+ *(ví dụ minh hoạ)* **Mỗi UC phải có mặt trong ít nhất một hành trình, và mỗi hành trình phải có ít nhất một nhánh lỗi.** Công cụ kiểm điều kiện đầu, và PRD thiếu User flow thì không duyệt được. PRD tạo bằng bản trước 0.8.0 chưa có mục này: chạy `/prd EP-01 change thêm User flow`. Cột `Nguồn` chỉ ghi mã (xem [Khái niệm](../02-khai-niem.md#nguồn-của-từng-mục-trong-prd)).
55
71
 
56
72
  ---
57
73
 
@@ -71,7 +87,7 @@ Dùng cho **mọi** thay đổi: thêm, sửa, bỏ.
71
87
  | UC-001-AC07 | thêm | — | Khi sai 3 lần thì … |
72
88
  | UC-001-BR06 | bỏ | … | ~~…~~ Đã bỏ (v1.1) |
73
89
 
74
- **Lượt 2: áp dụng.** AI sửa đúng các mục đó, thêm dòng vào Lịch sử thay đổi, tăng version. PRD đang `approved` thì quay về `draft` và **cần duyệt lại**.
90
+ **Lượt 2: áp dụng.** AI sửa đúng các mục đó, thêm dòng vào Lịch sử thay đổi, tăng version. PRD đang `approved` thì quay về `draft`, và phải **refine lại** (chỉ rà phần vừa đổi) rồi **duyệt lại**.
75
91
 
76
92
  ## Lưu ý
77
93
 
@@ -7,17 +7,18 @@
7
7
  ## Đường đi của bạn
8
8
 
9
9
  ```
10
- /product ──► /product EP-xx ──► /prd EP-xx ──► duyệt PRD ──► (giao cho BDD — sắp có)
11
- ▲ │
12
- └─┘ /prd EP-xx change khi yêu cầu thay đổi
10
+ /product ──► /product EP-xx ──► /prd EP-xx ──► /prd EP-xx refine ──► duyệt PRD ──► (giao cho BDD — sắp có)
11
+ ▲ │
12
+ └─ /prd EP-xx change ◄┘ khi yêu cầu thay đổi
13
13
  ```
14
14
 
15
15
  | Bước | Lệnh | Bạn làm gì | Hướng dẫn |
16
16
  |---|---|---|---|
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
- | 3 | `/prd EP-xx` | Duyệt cách chia UC, xác nhận các mục 🤖, ghi người duyệt | [/prd](../lenh/prd.md) |
20
- | 4 | `/prd EP-xx change …` | Khi yêu cầu đổi. Duyệt kế hoạch thay đổi, rồi duyệt lại PRD | [/prd](../lenh/prd.md#đổi-prd-prd-ep-01-change-mô-tả) |
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) |
21
+ | 5 | `/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, rồi duyệt lại PRD | [/prd](../lenh/prd.md#đổi-prd-prd-ep-01-change-mô-tả) |
21
22
 
22
23
  ## Mẹo
23
24
 
@@ -20,11 +20,16 @@
20
20
  | AI chạy lại cùng một lệnh `spec_edit` | Lần trước Python thoát mà không chạy (thường gặp với bản Store) | Bình thường, AI được dặn chạy lại một lần |
21
21
  | `không đặt được status=approved: còn 3 dòng 🤖` | Còn mục chưa được bạn xác nhận | Xác nhận các mục AI liệt kê |
22
22
  | `… chưa có approved_by` | Chưa ghi người duyệt | Trả lời câu *"Ai duyệt?"* |
23
+ | `PRD chưa có mục User flow` | PRD tạo bằng bản trước 0.8.0 | Chạy `/prd EP-xx change thêm User flow` |
24
+ | `UC chưa có mặt trong hành trình nào của User flow: UC-007` | Có UC chưa được vẽ vào sơ đồ nào | AI sẽ bổ sung. Hoặc chạy `/prd EP-xx change thêm UC-007 vào user flow` |
25
+ | `… bản v1.2 chưa refine` | PRD chưa được rà nội dung ở version này | Chạy `/prd EP-xx refine` |
26
+ | `… còn finding critical/major chưa có quyết định: F01, F04` | Bạn chưa quyết các finding đó | Trả lời: nhận, sửa, bác (kèm lý do) hay hoãn |
27
+ | `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` |
23
28
  | `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õ |
24
29
  | `(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` |
25
30
  | `💡 Phiên này đã dài. Nên /clear …` | Phiên đang mang theo hội thoại cũ | Gõ `/clear` rồi gọi lại lệnh |
26
31
  | `EP-xx chưa làm rõ xong. Chạy /product EP-xx trước` | Epic chưa `ready` | Chạy tiếp `/product EP-xx`. Thật sự cần PRD sớm thì thêm `--force`, PRD sẽ được ghi chú là tạo khi epic chưa xong |
27
- | `PRD đã có …` | Gọi `/prd EP-xx` khi PRD đã tồn tại | Dùng `/prd EP-xx change <mô tả>` |
32
+ | `PRD đã có …` | Gọi `/prd EP-xx` khi PRD đã tồn tại | Rà nội dung: `/prd EP-xx refine` · Đổi: `/prd EP-xx change <mô tả>` |
28
33
 
29
34
  ## Vẫn chưa được?
30
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@educa-corp/fw",
3
- "version": "0.6.2",
3
+ "version": "0.8.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"
package/ref/prd/change.md CHANGED
@@ -27,6 +27,7 @@ Khi PO đồng ý kế hoạch:
27
27
  1. **Một** lần `SE edit D` cho mọi chỗ sửa:
28
28
  - Mục sửa và mục thêm mang `✅`, vì PO vừa duyệt kế hoạch. Mục thêm ghi `(nguồn: PRD)`, trừ khi lấy từ một mã epic hay ràng buộc cụ thể.
29
29
  - Mục bỏ: giữ dòng, gạch ngang nội dung, ghi *"Đã bỏ (v{mới})"*.
30
- - Thêm dòng vào **5. 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ỏ)"*.
31
- 2. 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=—`.
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
+ - 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
+ 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=—`.
32
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 7 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`.
@@ -0,0 +1,89 @@
1
+ # Lăng kính rà nội dung PRD — dành cho sub-agent
2
+
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
+
5
+ ## Luật chung
6
+
7
+ 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.
8
+ 2. **Có trích dẫn.** Mỗi finding trích **nguyên văn** đoạn PRD liên quan (≤ 120 ký tự) kèm mã mục (`UC-003-BR02`, `UC-003`, hoặc `Tổng quan`). Finding dạng *"PRD thiếu X"* thì nêu mục lẽ ra phải chứa X.
9
+ 3. **Không nêu lại** những gì đã có quyết định trong file refine (`R`), dù là `nhận`, `bác` hay `hoãn`.
10
+ 4. **Không hỏi những gì bước sau sẽ trả lời**: giao diện chi tiết (thuộc design-spec), kịch bản kiểm thử (thuộc BDD), cách làm kỹ thuật (thuộc TDD).
11
+ 5. **Mức độ:**
12
+
13
+ | Mức | Khi nào | Ví dụ |
14
+ |---|---|---|
15
+ | `critical` | Không sửa thì dev hoặc QC **sẽ làm sai**, hoặc không làm được | Hai BR mâu thuẫn trực tiếp |
16
+ | `major` | Không sửa thì người làm **phải đoán** | Không nói chuyện gì xảy ra khi import có dòng sai định dạng |
17
+ | `minor` | Làm rõ thêm cho dễ đọc, không đổi hành vi | Tên trạng thái dùng hai cách gọi |
18
+
19
+ ## QA — hình thức của AC
20
+
21
+ Cố ý hẹp. Chỉ hỏi:
22
+ - AC có nêu **kết quả nhìn thấy được**, kiểm được là đạt / không đạt không?
23
+ - AC có **lặp lại** nội dung BR không? Nếu lặp thì đề xuất làm mỏng AC và trỏ sang BR.
24
+
25
+ **Không** hỏi "AC đủ chi tiết chưa". Chi tiết cơ chế (số lần, thời hạn, nhánh lỗi) thuộc về BR / Business Logic. Nếu thiếu cơ chế thì đề xuất **thêm vào BR**, không phình AC.
26
+
27
+ ## DEV — BR và Business Logic có đủ để làm không
28
+
29
+ Đọc như một dev sắp làm: đã đủ để làm **mà không phải đoán** chưa? Soi:
30
+ - nhánh nghiệp vụ còn thiếu;
31
+ - điều kiện biên chưa nói (0, rỗng, tối đa, trùng, đồng thời);
32
+ - xử lý khi có lỗi hay ngoại lệ bị bỏ ngỏ, ví dụ *"gửi email đặt lại mật khẩu thất bại thì sao?"*;
33
+ - BR mơ hồ, hoặc mâu thuẫn với BR khác.
34
+
35
+ ## SA — thông suốt và nhất quán giữa các UC
36
+
37
+ Đọc như một architect: toàn bộ luồng có **thông suốt và nhất quán** không? Soi:
38
+ - **User flow** (mục `sec:userflow`): mỗi hành trình có đi tới được điểm ra không, có **ngõ cụt** không, có **nhánh lỗi hoặc ngoại lệ** không. Sơ đồ có khớp với các UC không: bước trong sơ đồ có UC nào thực hiện không, UC có làm điều sơ đồ nói không;
39
+ - tương tác giữa các UC đã được định nghĩa chưa (UC này tạo ra thứ mà UC kia cần);
40
+ - **trạng thái và vòng đời** của đối tượng nghiệp vụ có nhất quán không, ví dụ tài khoản: mới → active → inactive;
41
+ - **ai được làm gì, ai sở hữu gì**;
42
+ - cùng một khái niệm có bị gọi hoặc hiểu khác nhau ở hai UC không.
43
+
44
+ Không phán về kiến trúc hay mô hình dữ liệu kỹ thuật.
45
+
46
+ ## Phạm vi — PRD có đi đúng đường đã chốt không
47
+
48
+ Đọc **toàn bộ PRD** (chuyện vượt phạm vi thường nằm trong thân UC, không nằm ở Tổng quan). Tài liệu phụ thì chỉ đọc đúng phần được giao: checkpoint 1 của epic, mục Ràng buộc và Danh sách epic của `product.md`. Soi:
49
+ - PRD có **vượt ra ngoài** phạm vi epic, hoặc làm điều epic ghi là "ngoài phạm vi" không;
50
+ - PRD có **đi ngược** một ràng buộc `CON-xx` không;
51
+ - PRD có **giẫm lên** phạm vi của epic khác không.
52
+
53
+ Không hỏi lại ưu tiên hay mục tiêu, vì đã chốt ở `/product`.
54
+
55
+ ## Định dạng ghi file (agent lăng kính)
56
+
57
+ Ghi vào file được giao bằng `SE create --replace <file>`, nội dung **chỉ** là danh sách sau:
58
+
59
+ ```
60
+ - lens: DEV
61
+ severity: major
62
+ item: UC-003-BR02
63
+ quote: "…nguyên văn…"
64
+ finding: "…"
65
+ suggestion: "…"
66
+ if_accepted: "…" # chỉ critical / major: chốt đề xuất này thì phát sinh trường hợp gì
67
+ ```
68
+
69
+ Không tìm thấy gì thì ghi `[]`. Sau đó **chỉ trả về một dòng**: *"{lens}: {k} finding → {file}"*. Không chép lại finding vào câu trả lời.
70
+
71
+ ## Kiểm chứng và ghi file — dành cho agent kiểm chứng
72
+
73
+ Bạn nhận: PRD `D`, file refine `R` (có thể chưa có), thư mục `W` chứa 4 file kết quả của lượt `n`, và mã F kế tiếp. Làm **đúng thứ tự** sau. Phải **kiểm chứng trước, gộp sau**, để không tự kiểm phần mình vừa viết lại.
74
+
75
+ **1. Kiểm chứng từng finding gốc.** Đọc cả 4 file. Với **từng** finding, **cố chứng minh nó sai**:
76
+ - Tra PRD: đoạn `quote` có đúng là nguyên văn không? Điều finding nói "thiếu" có thật là thiếu không, hay đã nằm ở mục khác (BR khác, Business Logic, UC khác, Tổng quan, User flow)? Đã được trả lời **một phần** thì thu hẹp finding lại, chỉ giữ phần chưa được trả lời.
77
+ - Finding có hỏi điều thuộc bước sau (giao diện, kịch bản test, kỹ thuật) không? Có đề xuất giải pháp kỹ thuật không?
78
+ - Finding có nêu lại một điểm **đã có quyết định** trong `R` không?
79
+ - Mức độ có đúng định nghĩa không?
80
+
81
+ Kết luận cho mỗi finding: `ĐÚNG` · `SAI` (bỏ) · `HẠ MỨC` (giữ, đổi mức).
82
+
83
+ **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
+
85
+ **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
+
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ờ`.
88
+
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}"*.
package/ref/prd/new.md CHANGED
@@ -23,15 +23,16 @@ Chỉ **2 lượt** hỏi-đáp, vì epic đã được làm rõ kỹ. Không h
23
23
  Khi PO đồng ý cách chia UC:
24
24
 
25
25
  1. Tạo `D` bằng **một** lần `SE create`, theo khuôn `.fw/core/templates/prd.md`:
26
- - **1. Tổng quan:** chuyển từ checkpoint 1 của epic. Ghi các ràng buộc `CON-xx` áp cho epic này.
27
- - **2. Use case:** mỗi UC gồm điều kiện trước, kết quả sau, luồng chính (lấy các bước liên quan ở checkpoint 2 của epic), bảng BR, Acceptance Criteria (điều kiện nghiệm thu).
26
+ - **Tổng quan:** chuyển từ checkpoint 1 của epic. Ghi các ràng buộc `CON-xx` áp cho epic này.
27
+ - **User flow** (`sec:userflow`): vẽ các **hành trình** xuyên suốt nhiều UC, lấy từ luồng chính và edge case ở checkpoint 2 của epic. Mỗi hành trình gồm một dòng trong bảng và một sơ đồ `mermaid` (`flowchart TD`). Nút trong sơ đồ ghi mã UC, không chép lại BR. **Mỗi hành trình có ít nhất một nhánh lỗi hoặc ngoại lệ. Mỗi UC có mặt trong ít nhất một hành trình.** Nút nào AI tự suy ra (không có trong epic) thì ghi kèm `🤖` trong nhãn.
28
+ - **Use case:** mỗi UC gồm điều kiện trước, kết quả sau, luồng chính (lấy các bước liên quan ở checkpoint 2 của epic), bảng BR, Acceptance Criteria (điều kiện nghiệm thu).
28
29
  - **Cột Business Rule:** một BR mỗi dòng, dạng *"Hệ thống PHẢI / KHÔNG ĐƯỢC …"*.
29
30
  - **Cột Business Logic:** rẽ nhánh, công thức, thông báo khi lỗi. Lấy từ edge case và nhật ký làm rõ của epic.
30
31
  - **AC:** dạng *"Khi … thì …"*, mô tả **kết quả nhìn thấy được**, không mô tả cơ chế.
31
- - **3. Màn hình:** chuyển từ "Màn hình chính" của epic, kèm cột UC.
32
- - **4. Câu hỏi còn mở:** chỉ những gì phát sinh khi viết PRD.
33
- - **5. Lịch sử thay đổi:** dòng `1.0`.
34
- 2. Chạy `SE coverage <epic> D`. **Thiếu mục nào thì bổ sung vào UC phù hợp rồi chạy lại cho tới khi đủ.** Mục mà PO muốn bỏ thì hỏi PO. Không được tự bỏ.
32
+ - **Màn hình:** chuyển từ "Màn hình chính" của epic, kèm cột UC.
33
+ - **Câu hỏi còn mở:** chỉ những gì phát sinh khi viết PRD.
34
+ - **Lịch sử thay đổi:** dòng `1.0`.
35
+ 2. Chạy `SE coverage <epic> D` và `SE flowcheck D`. **Thiếu mục nào thì bổ sung vào UC phù hợp rồi chạy lại cho tới khi đủ.** Mục mà PO muốn bỏ thì hỏi PO. Không được tự bỏ.
35
36
  3. Epic giờ chỉ còn là lịch sử:
36
37
  - `SE set <epic> status=handed-off`
37
38
  - Trong `{specs}/product/product.md`, sửa cột Trạng thái của epic thành `đã có PRD`.
@@ -0,0 +1,49 @@
1
+ # /prd — chế độ rà nội dung (refine)
2
+
3
+ Mục đích: tìm chỗ **nghiệp vụ còn thiếu, mơ hồ, mâu thuẫn** đến mức sẽ làm sai hoặc phải đoán ở các bước sau. **Không** rà chính tả hay trình bày, vì đó là việc của `/review`.
4
+
5
+ - File refine: `R = {specs}/{domain}/{slug}/review/prd-refine.md` (khuôn `.fw/core/templates/prd-refine.md`).
6
+ - Thư mục làm việc của các agent: `W = .fw/tmp/refine/{slug}/`. Thư mục này không commit.
7
+
8
+ ## Bước A — Xác định lượt
9
+
10
+ Chạy `SE upgrade D` trước. Lệnh này bổ sung trường và mục còn thiếu cho PRD tạo bằng bản cũ (ví dụ `refined`, mục User flow), và đổi mã nguồn `R…` thành `BR…`. Nếu `SE` báo PRD **thiếu mục User flow**, thì dừng và báo: *"PRD chưa có User flow. Chạy `/prd {id} change thêm User flow` trước, rồi refine."*
11
+
12
+ Đọc frontmatter của `D` (`version`, `refined`) và của `R` (nếu có: `prd_version`, `round`).
13
+
14
+ | Tình huống | Lượt | Phạm vi |
15
+ |---|---|---|
16
+ | `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 | — |
17
+ | Chưa có `R`, hoặc chưa từng refine | **1** | Toàn bộ PRD |
18
+ | `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 |
19
+ | `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 |
20
+
21
+ **Không có lượt 3.** Sau lượt 2 thì luôn đặt `refined` (Bước D).
22
+
23
+ ## Bước B — Rà (sub-agent, bắt buộc model Opus)
24
+
25
+ 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.
26
+
27
+ 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:
28
+ - đườ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);
29
+ - 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`;
30
+ - 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}"*.
31
+ 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.
32
+
33
+ ## Bước C — Trình
34
+
35
+ 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).
36
+ 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.
37
+ 3. Nhắc PO cách quyết:
38
+ - *"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'."*
39
+ - *"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ã."*
40
+ 4. Lượt này không có finding nào thì báo, rồi sang Bước D luôn.
41
+
42
+ ## Bước D — Áp quyết định
43
+
44
+ 1. **Quyết theo nhóm:** trước khi ghi, liệt kê lại **đúng các mã** sẽ nhận quyết định đó, rồi hỏi *"Đúng chưa?"*. Critical không được quyết theo nhóm.
45
+ 2. Ghi quyết định vào `R` (`- Quyết định: …`). **Mọi** finding critical / major phải có quyết định khác `chờ`. Còn thiếu thì hỏi lại, tối đa 4 mã mỗi lượt.
46
+ 3. Cập nhật dòng của lượt này trong bảng **Tóm tắt** của `R`: số `nhận` · `sửa` · `bác` · `hoãn`. Đây là số đo để đánh giá lớp kiểm chứng: **tỉ lệ bác cao nghĩa là kiểm chứng còn lỏng**.
47
+ 4. Finding `nhận` hoặc `sửa`: áp vào `D` trong **một** lần `SE edit`, theo đúng luật của `.fw/core/ref/prd/change.md` (mã mới lấy số kế tiếp, mục bỏ thì gạch ngang, `(nguồn: PRD)`, có đổi luồng thì sửa cả mục User flow). Thêm một dòng Lịch sử thay đổi nêu mã finding, ví dụ *"v1.3 — refine lượt 1: F01, F03, F04"*.
48
+ 5. Có áp bản sửa: tăng version phụ (`SE set D version=… status=draft approved_by=— approved_at=—`), rồi `SE set R status=applied`. Nếu đây là **lượt 1**, báo *"`/clear` rồi `/prd {id} refine` để chạy lượt 2, chỉ soi phần vừa sửa."*
49
+ 6. 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)"*.
@@ -0,0 +1,32 @@
1
+ ---
2
+ type: prd-refine
3
+ prd: {đường dẫn prd.md}
4
+ prd_version: {version PRD lúc rà}
5
+ round: 1 # 1 | 2
6
+ status: pending # pending | applied | done
7
+ updated: {YYYY-MM-DD}
8
+ ---
9
+
10
+ # Refine PRD — {EPIC-ID}
11
+
12
+ > Rà **nội dung** PRD qua 4 lăng kính: QA · DEV · SA · Phạm vi. Finding nào cũng đã qua bước **kiểm chứng**.
13
+ > File này cũng là **sổ quyết định**: lần rà sau đọc lại để không nêu lại những điểm đã quyết. **Không xoá finding cũ.**
14
+ > Quyết định: `chờ` · `nhận` · `sửa: <nội dung PO muốn>` · `bác: <lý do>` · `hoãn` (để bước QC).
15
+
16
+ ## Tóm tắt <!-- sec:summary -->
17
+
18
+ | Lượt | Version | Thô | Bỏ khi kiểm chứng | Hạ mức | Còn | critical | major | minor | Kết luận | Nhận | Sửa | Bác | Hoãn |
19
+ |---|---|---|---|---|---|---|---|---|---|---|---|---|---|
20
+ | 1 | {v} | {n} | {n} | {n} | {n} | {n} | {n} | {n} | {BLOCKED · NEEDS_REVISION · APPROVED_WITH_MINOR_CHANGES} | — | — | — | — |
21
+
22
+ > Finding sắp theo mức độ (critical → major → minor), rồi theo lăng kính (QA → DEV → SA → Phạm vi). Mã F đánh theo đúng thứ tự đó, nối tiếp qua các lượt.
23
+ > Bốn cột cuối điền khi PO quyết xong. **Tỉ lệ bác cao nghĩa là lớp kiểm chứng còn lỏng.**
24
+
25
+ ## Lượt 1 — v{version} <!-- sec:round-1 -->
26
+
27
+ ### F01 · {QA · DEV · SA · Phạm vi} · {critical · major · minor} · {mã mục}
28
+ - Trích: "{nguyên văn trong PRD}"
29
+ - Vấn đề: {…}
30
+ - Đề xuất: {…}
31
+ - Nếu chốt đề xuất này: {… — chỉ critical / major}
32
+ - Quyết định: chờ
package/templates/prd.md CHANGED
@@ -6,6 +6,7 @@ status: draft # draft | approved
6
6
  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
+ refined: — # version đã refine xong (/prd <EPIC-ID> refine); bắt buộc = version khi duyệt
9
10
  updated: {YYYY-MM-DD}
10
11
  ---
11
12
 
@@ -27,7 +28,25 @@ updated: {YYYY-MM-DD}
27
28
  - **Phụ thuộc:** {… hoặc "Không có"}
28
29
  - **Ràng buộc áp dụng** (từ `product.md`): {CON-01, CON-03 … hoặc "Không có"}
29
30
 
30
- ## 2. Use case <!-- sec:usecases -->
31
+ ## 2. User flow <!-- sec:userflow -->
32
+
33
+ > Các **hành trình** xuyên suốt nhiều UC: actor đi qua những UC nào, rẽ nhánh ở đâu, gặp lỗi thì đi đâu. Nút trong sơ đồ **ghi mã UC**, không chép lại BR.
34
+ > Mỗi UC phải có mặt trong ít nhất một hành trình. Mỗi hành trình phải có ít nhất một nhánh lỗi hoặc ngoại lệ.
35
+
36
+ | Mã | Hành trình | Actor | Đi qua UC | Điểm vào | Điểm ra |
37
+ |---|---|---|---|---|---|
38
+ | J1 | {Tên hành trình} | ACT-xx | UC-{NNN} → UC-{NNN} | {…} | {…} |
39
+
40
+ ### J1 — {Tên hành trình}
41
+
42
+ ```mermaid
43
+ flowchart TD
44
+ A["UC-{NNN} {bước}"] --> B{"{điểm rẽ nhánh}"}
45
+ B -->|{nhánh chính}| C["UC-{NNN} {bước}"]
46
+ B -->|{nhánh lỗi}| X["{kết quả khi lỗi}"]
47
+ ```
48
+
49
+ ## 3. Use case <!-- sec:usecases -->
31
50
 
32
51
  ### UC-{NNN} — {Tên use case}
33
52
 
@@ -52,17 +71,17 @@ updated: {YYYY-MM-DD}
52
71
 
53
72
  - ✅ UC-{NNN}-AC01. Khi {…} thì {…}. (nguồn: AC1)
54
73
 
55
- ## 3. Màn hình <!-- sec:screens -->
74
+ ## 4. Màn hình <!-- sec:screens -->
56
75
 
57
76
  | Màn hình | Thành phần chính | Hành động → kết quả | UC |
58
77
  |---|---|---|---|
59
78
  | {…} | {…} | {…} | UC-{NNN} |
60
79
 
61
- ## 4. Câu hỏi còn mở <!-- sec:open -->
80
+ ## 5. Câu hỏi còn mở <!-- sec:open -->
62
81
 
63
82
  - {… hoặc "Không còn"}
64
83
 
65
- ## 5. Lịch sử thay đổi <!-- sec:history -->
84
+ ## 6. Lịch sử thay đổi <!-- sec:history -->
66
85
 
67
86
  | Version | Ngày | Thay đổi |
68
87
  |---|---|---|
@@ -17,6 +17,8 @@ Cách dùng (nội dung truyền qua stdin):
17
17
  spec_edit.py section <file> <mã> in đúng một mục theo mã `<!-- sec:… -->` (vd constraints)
18
18
  spec_edit.py upgrade <file> gắn mã mục cho file theo khuôn cũ (chạy một lần)
19
19
  spec_edit.py next-uc <thư mục specs> in mã UC kế tiếp (quét mọi prd.md)
20
+ spec_edit.py flowcheck <prd> kiểm User flow: có sơ đồ, mọi UC có mặt trong một hành trình
21
+ spec_edit.py create --replace <file> chỉ cho file tạm trong .fw/tmp/ (kết quả của agent)
20
22
  spec_edit.py coverage <epic> <prd> liệt kê BR / AC của epic chưa được PRD dùng tới
21
23
 
22
24
  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,9 +37,10 @@ FRONTMATTER = {
35
37
  "product": {"status": ["in-progress", "ready"], "checkpoint": (0, 2)},
36
38
  "epic": {"status": ["in-progress", "ready", "handed-off"], "checkpoint": (0, 3)},
37
39
  "prd": {"status": ["draft", "approved"]},
40
+ "prd-refine": {"status": ["pending", "applied", "done"], "round": (1, 2)},
38
41
  }
39
42
  # Trạng thái "đã xong": chỉ đặt được khi không còn 🤖 và không còn câu hỏi mở.
40
- DONE_STATUS = {"product": "ready", "epic": "ready", "prd": "approved"}
43
+ DONE_STATUS = {"product": "ready", "epic": "ready", "prd": "approved", "prd-refine": "done"}
41
44
  COMMON_INT = {"open_questions": (0, 999)}
42
45
  DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
43
46
  VERSION = re.compile(r"^\d+\.\d+$")
@@ -75,8 +78,15 @@ def stdin_text():
75
78
  return sys.stdin.read()
76
79
 
77
80
 
78
- def cmd_create(path):
79
- if os.path.exists(path):
81
+ def in_tmp(path):
82
+ parts = os.path.abspath(path).replace("\\", "/").split("/")
83
+ return any(parts[i] == ".fw" and parts[i + 1] == "tmp" for i in range(len(parts) - 1))
84
+
85
+
86
+ def cmd_create(path, replace=False):
87
+ if replace and not in_tmp(path):
88
+ fail("--replace chỉ dùng cho file tạm trong .fw/tmp/. File spec thì không bao giờ ghi đè.")
89
+ if os.path.exists(path) and not replace:
80
90
  fail(path + " đã tồn tại — không ghi đè. Dùng `edit` hoặc `set`.")
81
91
  text = stdin_text()
82
92
  check_keys(file_type(text), [], sec_keys(text), creating=True)
@@ -128,7 +138,7 @@ def validate(ftype, key, value):
128
138
  fail("type `%s` chưa được khai trong spec_edit.py" % ftype)
129
139
  if key == "status" and value not in rules["status"]:
130
140
  fail("status=%s không hợp lệ với type %s. Chỉ nhận: %s" % (value, ftype, ", ".join(rules["status"])))
131
- rng = rules.get(key) if key == "checkpoint" else COMMON_INT.get(key)
141
+ rng = rules.get(key) if isinstance(rules.get(key), tuple) else COMMON_INT.get(key)
132
142
  if rng:
133
143
  if not value.isdigit() or not rng[0] <= int(value) <= rng[1]:
134
144
  fail("%s=%s không hợp lệ. Phải là số nguyên từ %d đến %d" % (key, value, rng[0], rng[1]))
@@ -136,11 +146,85 @@ def validate(ftype, key, value):
136
146
  fail("updated=%s không hợp lệ. Định dạng YYYY-MM-DD" % value)
137
147
  if key == "approved_at" and value != "—" and not DATE.match(value):
138
148
  fail("approved_at=%s không hợp lệ. Định dạng YYYY-MM-DD, hoặc — nếu chưa duyệt" % value)
139
- if key == "version" and not VERSION.match(value):
140
- fail("version=%s không hợp lệ. Định dạng số.số, ví dụ 1.0, 1.1" % value)
149
+ if key in ("version", "prd_version") and not VERSION.match(value):
150
+ fail("%s=%s không hợp lệ. Định dạng số.số, ví dụ 1.0, 1.1" % (key, value))
151
+ if key == "refined" and value != "—" and not VERSION.match(value):
152
+ fail("refined=%s không hợp lệ. Là version PRD đã refine xong (vd 1.1), hoặc —" % value)
153
+
154
+
155
+ # ── Refine: finding critical / major chưa có quyết định thì chưa được đi tiếp ──────────────
156
+ FINDING = re.compile(r"^### (F\d+) · [^·\n]+ · (critical|major|minor) ·", re.M)
157
+ DECISION = re.compile(r"^- Quyết định:\s*(.*)$", re.M)
158
+
159
+
160
+ def undecided_findings(text):
161
+ """Mã các finding critical / major còn `chờ` (hoặc chưa ghi quyết định)."""
162
+ out = []
163
+ heads = list(FINDING.finditer(text))
164
+ for n, h in enumerate(heads):
165
+ end = heads[n + 1].start() if n + 1 < len(heads) else len(text)
166
+ block = text[h.end():end]
167
+ block = re.split(r"^##\s", block, maxsplit=1, flags=re.M)[0]
168
+ d = DECISION.search(block)
169
+ if h.group(2) in ("critical", "major") and (not d or d.group(1).strip().startswith("chờ")):
170
+ out.append(h.group(1))
171
+ return out
172
+
173
+
174
+ def section_text(text, key):
175
+ lines = text.split("\n")
176
+ hs = headings(lines)
177
+ for n, (i, level, _, k) in enumerate(hs):
178
+ if k == key:
179
+ end = next((j for j, lv, _, _ in hs[n + 1:] if lv <= level), len(lines))
180
+ return "\n".join(lines[i:end])
181
+ return None
182
+
183
+
184
+ def flow_problems(text):
185
+ """Danh sách vấn đề của mục User flow (rỗng = ổn)."""
186
+ flow = section_text(text, "userflow")
187
+ if flow is None:
188
+ return ["PRD chưa có mục User flow (sec:userflow). Chạy `/prd <EPIC-ID> change thêm User flow`."]
189
+ problems = []
190
+ if "```mermaid" not in flow:
191
+ problems.append("mục User flow chưa có sơ đồ mermaid nào")
192
+ ucs = sorted(set(UC_HEADING.findall(text)))
193
+ missing = ["UC-" + n for n in ucs if not re.search(r"UC-%s(?![\d-])" % n, flow)]
194
+ if missing:
195
+ problems.append("UC chưa có mặt trong hành trình nào của User flow: " + ", ".join(missing))
196
+ return problems
141
197
 
142
198
 
143
- def guard_done(ftype, block, body):
199
+ def cmd_flowcheck(path):
200
+ problems = flow_problems(read(path))
201
+ if problems:
202
+ fail("\n - ".join([""] + problems))
203
+ print("User flow đủ — mọi UC đều có mặt trong ít nhất một hành trình")
204
+
205
+
206
+ def refine_path(prd_path):
207
+ return os.path.join(os.path.dirname(os.path.abspath(prd_path)), "review", "prd-refine.md")
208
+
209
+
210
+ def guard_refined(path, block):
211
+ """PRD chỉ được duyệt khi đã refine đúng version đang có và không còn critical/major chưa quyết."""
212
+ ver = re.search(r"^version:\s*(\S+)", block, re.M)
213
+ ref = re.search(r"^refined:\s*(\S+)", block, re.M)
214
+ if not ref or not ver or ref.group(1) != ver.group(1):
215
+ fail("không đặt được status=approved: bản v%s chưa refine (refined=%s). Chạy `/prd <EPIC-ID> refine` trước."
216
+ % (ver.group(1) if ver else "?", ref.group(1) if ref else "chưa có"))
217
+ flow = flow_problems(read(path))
218
+ if flow:
219
+ fail("không đặt được status=approved:\n - " + "\n - ".join(flow))
220
+ rp = refine_path(path)
221
+ pending = undecided_findings(read(rp)) if os.path.isfile(rp) else []
222
+ if pending:
223
+ fail("không đặt được status=approved: còn finding critical/major chưa có quyết định: %s (xem %s)"
224
+ % (", ".join(pending), rp))
225
+
226
+
227
+ def guard_done(ftype, block, body, path):
144
228
  """Chặn đặt trạng thái 'đã xong' khi còn mục chờ PO — AI không được tự khẳng định."""
145
229
  done = DONE_STATUS.get(ftype)
146
230
  sm = re.search(r"^status:\s*(\S+)", block, re.M)
@@ -159,6 +243,11 @@ def guard_done(ftype, block, body):
159
243
  if fm and fm.group(1) == "—":
160
244
  fail("không đặt được status=%s: chưa có %s. Đặt cùng lệnh, ví dụ: "
161
245
  "set <file> status=%s approved_by=<tên> approved_at=YYYY-MM-DD" % (done, field, done))
246
+ if ftype == "prd":
247
+ guard_refined(path, block)
248
+ if ftype == "prd-refine" and undecided_findings(body):
249
+ fail("không đặt được status=done: còn finding critical/major chưa có quyết định: %s"
250
+ % ", ".join(undecided_findings(body)))
162
251
 
163
252
 
164
253
  def cmd_set(path, pairs):
@@ -182,7 +271,7 @@ def cmd_set(path, pairs):
182
271
  # Giữ nguyên comment đi kèm ở cuối dòng, chỉ đổi giá trị.
183
272
  block = line.sub(lambda mm: mm.group(1) + value + (mm.group(3) or ""), block, count=1)
184
273
 
185
- guard_done(ftype, block, text[m.end():])
274
+ guard_done(ftype, block, text[m.end():], path)
186
275
  write(path, text[: m.start(1)] + block + text[m.end(1):])
187
276
  print("đã đặt %s trong %s" % (", ".join(pairs), path))
188
277
 
@@ -191,7 +280,8 @@ def cmd_set(path, pairs):
191
280
  # Số và tên tiêu đề là cho người đọc, được đổi tự do. Script và AI tìm mục theo mã `sec:`.
192
281
  SEC_KEY = re.compile(r"<!--\s*sec:([a-z0-9-]+)\s*-->")
193
282
  HEADING = re.compile(r"^(#{2,6})\s+(.*)$")
194
- TEMPLATE_OF = {"product": "product.md", "epic": "product-epic.md", "prd": "prd.md"}
283
+ TEMPLATE_OF = {"product": "product.md", "epic": "product-epic.md", "prd": "prd.md",
284
+ "prd-refine": "prd-refine.md"}
195
285
  TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "templates")
196
286
 
197
287
 
@@ -286,6 +376,26 @@ def cmd_section(path, key):
286
376
  "" if have else ". File theo khuôn cũ: chạy `upgrade <file>` trước."))
287
377
 
288
378
 
379
+ def missing_frontmatter(ftype, text):
380
+ """Các dòng frontmatter có trong khuôn mà file chưa có. Giá trị mẫu dạng {…} đổi thành —."""
381
+ name = TEMPLATE_OF.get(ftype)
382
+ tpath = os.path.join(TEMPLATES_DIR, name) if name else None
383
+ if not tpath or not os.path.isfile(tpath):
384
+ return []
385
+ tm = re.match(r"^---\r?\n(.*?)\r?\n---", read(tpath), re.S)
386
+ fm = re.match(r"^---\r?\n(.*?)\r?\n---", text, re.S)
387
+ if not tm or not fm:
388
+ return []
389
+ have = set(re.findall(r"^([a-z_]+):", fm.group(1), re.M))
390
+ out = []
391
+ for line in tm.group(1).split("\n"):
392
+ k = re.match(r"^([a-z_]+):\s*(\S*)(.*)$", line)
393
+ if k and k.group(1) not in have:
394
+ value = "—" if k.group(2).startswith("{") else k.group(2)
395
+ out.append("%s: %s%s" % (k.group(1), value, k.group(3)))
396
+ return out
397
+
398
+
289
399
  def cmd_upgrade(path):
290
400
  """Gắn mã sec: cho file theo khuôn cũ, bằng cách khớp tiêu đề với khuôn. Chỉ cần chạy một lần."""
291
401
  text = read(path)
@@ -308,6 +418,11 @@ def cmd_upgrade(path):
308
418
  else:
309
419
  unknown.append(title)
310
420
  new_text = "\n".join(lines)
421
+ # Trường frontmatter khuôn mới có mà file cũ chưa có (vd refined, approved_by) → thêm với giá trị —.
422
+ add_fm = missing_frontmatter(ftype, new_text)
423
+ if add_fm:
424
+ fm = re.match(r"^---\r?\n(.*?)(\r?\n)---", new_text, re.S)
425
+ new_text = new_text[:fm.end(1)] + "".join(fm.group(2) + l for l in add_fm) + new_text[fm.end(1):]
311
426
  # Mã Business Rule: R3 → BR3 (khuôn từ 0.6.1). Epic: đổi mọi chỗ. PRD: chỉ trong (nguồn: …).
312
427
  renamed = [0]
313
428
  if ftype == "epic":
@@ -322,6 +437,8 @@ def cmd_upgrade(path):
322
437
  write(path, new_text)
323
438
  missing = [(k, orig) for k, _, orig in tpl if k not in used]
324
439
  print("đã gắn mã: %s" % (", ".join(added) or "không có gì mới"))
440
+ if add_fm:
441
+ print("đã thêm trường frontmatter: " + ", ".join(l.split(":")[0] for l in add_fm))
325
442
  if renamed[0]:
326
443
  print("đã đổi mã Business Rule R… → BR…: %d chỗ" % renamed[0])
327
444
  if unknown:
@@ -411,11 +528,16 @@ def main(argv):
411
528
  if argv[:1] == ["--check"]:
412
529
  print("ok — Python %s" % sys.version.split()[0])
413
530
  return
414
- if len(argv) < 2 or argv[0] not in ("create", "edit", "set", "pending", "confirm", "next-uc", "coverage", "section", "upgrade"):
531
+ if len(argv) < 2 or argv[0] not in ("create", "edit", "set", "pending", "confirm", "next-uc", "coverage", "section", "upgrade", "flowcheck"):
415
532
  fail(__doc__.split("Cách dùng")[1], 2)
416
533
  cmd, path, rest = argv[0], argv[1], argv[2:]
417
534
  if cmd == "create":
418
- cmd_create(path)
535
+ if path == "--replace":
536
+ if not rest:
537
+ fail("cách dùng: create --replace <file trong .fw/tmp/>", 2)
538
+ cmd_create(rest[0], replace=True)
539
+ else:
540
+ cmd_create(path)
419
541
  elif cmd == "edit":
420
542
  cmd_edit(path)
421
543
  elif cmd == "pending":
@@ -428,6 +550,8 @@ def main(argv):
428
550
  cmd_section(path, rest[0])
429
551
  elif cmd == "upgrade":
430
552
  cmd_upgrade(path)
553
+ elif cmd == "flowcheck":
554
+ cmd_flowcheck(path)
431
555
  elif cmd == "next-uc":
432
556
  cmd_next_uc(path)
433
557
  elif cmd == "coverage":