@educa-corp/fw 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
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.6.1 — 2026-09-30
7
+
8
+ - Thuật ngữ: **Acceptance Criteria (điều kiện nghiệm thu)**, viết tắt AC, thay cho "tiêu chí chấp nhận". **Business Rule (BR)** thay cho "rule". Cột "Logic" của bảng BR đổi thành **Business Logic**.
9
+ - Epic đánh số Business Rule là `BR1.`, `BR2.`… (trước đây là `R1.`). `spec_edit upgrade` tự đổi mã trong epic cũ, và trong phần `(nguồn: …)` của PRD cũ. `coverage` coi `R3` và `BR3` là cùng một mã.
10
+ - `(nguồn: …)` **chỉ được ghi mã**: `BR3` / `AC1` của epic, `CON-02`, hoặc `PRD` (mục thêm khi viết PRD, PO đã duyệt). `spec_edit` chặn khi tạo hoặc sửa PRD còn ghi nguồn bằng lời. `coverage` báo lỗi khi nguồn là mã không có trong epic.
11
+ - PRD đã tạo bằng bản cũ: lần sửa đầu tiên sẽ yêu cầu đổi các chỗ ghi nguồn bằng lời thành mã (thường là `PRD`).
12
+
6
13
  ## 0.6.0 — 2026-09-30
7
14
 
8
15
  - Sẵn sàng publish lên npmjs với tên **`@educa-corp/fw`**. Cài lần đầu: `npx @educa-corp/fw install`.
package/commands/prd.md CHANGED
@@ -5,7 +5,7 @@ argument-hint: "<EPIC-ID> [change <mô tả thay đổi>] [--force]"
5
5
 
6
6
  # /prd
7
7
 
8
- Mục đích: biến epic đã làm rõ (`/product`) thành **PRD chính thức**, gồm các use case, rule và tiêu chí chấp nhận có **mã ổn định**. Sau đó PRD là nguồn cho BDD, TDD và code.
8
+ Mục đích: biến epic đã làm rõ (`/product`) thành **PRD chính thức**, gồm các use case, Business Rule (BR) và **Acceptance Criteria (điều kiện nghiệm thu)**, viết tắt **AC**, có **mã ổn định**. Sau đó PRD là nguồn cho BDD, TDD và code.
9
9
 
10
10
  - `/prd <EPIC-ID>`: tạo PRD từ epic `ready`.
11
11
  - `/prd <EPIC-ID> change <mô tả>`: thêm, sửa hoặc bỏ nội dung trong PRD đã có.
@@ -21,7 +21,7 @@ Nếu **trước lệnh này** phiên đã có hội thoại khác, dòng đầu
21
21
  - Không có `.fw/config.yaml`: dừng và báo *"Chưa cài framework. Chạy `npx @educa-corp/fw install` ở thư mục gốc dự án."*
22
22
  2. Tìm file epic: `{specs}/product/epics/{EPIC-ID}-*.md`. Đọc frontmatter để lấy `slug`, `domain`, `status`.
23
23
  3. File PRD: `D = {specs}/{domain}/{slug}/prd.md`.
24
- 4. Đọc `{specs}/product/glossary.md` nếu có, và `SE section {specs}/product/product.md constraints`. Viết đúng thuật ngữ. PRD không được đi ngược ràng buộc nào. Rule bắt nguồn từ ràng buộc thì ghi `(nguồn: CON-01)`. `SE` báo không có mục đó (khuôn cũ): coi như chưa có ràng buộc, và nhắc một dòng *"Chạy `/product` để bổ sung Ràng buộc và Giai đoạn cho tầng sản phẩm."*
24
+ 4. Đọc `{specs}/product/glossary.md` nếu có, và `SE section {specs}/product/product.md constraints`. Viết đúng thuật ngữ. PRD không được đi ngược ràng buộc nào. BR bắt nguồn từ ràng buộc thì ghi `(nguồn: CON-01)`. `SE` báo không có mục đó (khuôn cũ): coi như chưa có ràng buộc, và nhắc một dòng *"Chạy `/product` để bổ sung Ràng buộc và Giai đoạn cho tầng sản phẩm."*
25
25
 
26
26
  ## Bước 2 — Chọn chế độ
27
27
 
@@ -36,7 +36,7 @@ Nếu **trước lệnh này** phiên đã có hội thoại khác, dòng đầu
36
36
  ## Bước 3 — Luật chung
37
37
 
38
38
  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…)"*.
39
- 2. **Nguồn.** Mọi rule và tiêu chí lấy từ epic phải ghi `(nguồn: R3, AC1)`, đúng mã của epic.
39
+ 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.
40
40
  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 `🤖`. Chỉ PO mới đổi được `🤖` thành `✅`.
41
41
  4. **Ngôn ngữ nghiệp vụ.** Không nói API, bảng dữ liệu hay framework.
42
42
  5. **Tối đa 4 câu hỏi mỗi lượt, tính cả câu phụ.**
@@ -56,7 +56,7 @@ In ra đúng khối sau:
56
56
  ---
57
57
  Trạng thái : {✅ Đã duyệt v{version} | 🟡 Nháp v{version} — còn {n} mục 🤖, {k} câu hỏi mở}
58
58
  Đã ghi : {D} {· epic → handed-off · product.md nếu có}
59
- Kiểm : coverage {đủ | thiếu …} · {số UC} UC · {số BR} rule · {số AC} tiêu chí
59
+ Kiểm : coverage {đủ | thiếu …} · {số UC} UC · {số BR} BR · {số AC} AC
60
60
  Luồng : Product → [PRD ◀ bạn ở đây] → BDD → TDD · Design-spec → Code → Test → QC
61
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
62
  ```
@@ -63,7 +63,7 @@ Trước checkpoint 1 của epic, AI tự điền mục **Bối cảnh hệ th
63
63
  - Tạo file mới: `SE create <file> <<'EOF'` … nội dung … `EOF`
64
64
  - 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`.
65
65
  - Frontmatter: `SE set <file> checkpoint=1 open_questions=3 updated=YYYY-MM-DD`
66
- - Xem các mục còn chờ PO: `SE pending <file>`. PO xác nhận các mục có mã: `SE confirm <file> AC2 AC3 R8b`
66
+ - Xem các mục còn chờ PO: `SE pending <file>`. PO xác nhận các mục có mã: `SE confirm <file> AC2 AC3 BR8b`
67
67
  - Đọc một mục: `SE section <file> <mã>` (mã là `<!-- sec:… -->` ở tiêu đề). **Luôn tìm mục theo mã**, không theo số hay tên. Khi sửa, giữ nguyên `<!-- sec:… -->`.
68
68
  - `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.
69
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@educa-corp/fw",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Framework làm việc với Claude Code cho phòng PTPM",
5
5
  "bin": {
6
6
  "fw": "bin/fw.js"
package/ref/prd/change.md CHANGED
@@ -5,9 +5,9 @@ Dùng cho **mọi** thay đổi: thêm, sửa, bỏ. Mã không bao giờ đánh
5
5
  ## Lượt 1 — Kế hoạch thay đổi
6
6
 
7
7
  1. Đọc `D`. Lấy mô tả thay đổi từ `$ARGUMENTS`. Không có mô tả thì hỏi PO *"Bạn muốn đổi gì?"* rồi dừng.
8
- 2. Xác định các mục bị ảnh hưởng. Mô tả thay đổi có thể kéo theo mục khác (một rule đổi thì tiêu chí liên quan cũng phải đổi), nên phải tìm cả những mục đó.
8
+ 2. Xác định các mục bị ảnh hưởng. Mô tả thay đổi có thể kéo theo mục khác (một Business Rule (BR) đổi thì AC liên quan cũng phải đổi), nên phải tìm cả những mục đó.
9
9
  3. Cấp mã cho mục mới:
10
- - Rule hoặc tiêu chí mới trong UC có sẵn: số lớn nhất **đang có trong UC đó**, kể cả dòng đã bỏ, cộng một.
10
+ - BR hoặc AC mới trong UC có sẵn: số lớn nhất **đang có trong UC đó**, kể cả dòng đã bỏ, cộng một.
11
11
  - UC mới: `SE next-uc {specs}`.
12
12
  4. Trình kế hoạch:
13
13
 
@@ -25,7 +25,7 @@ Dùng cho **mọi** thay đổi: thêm, sửa, bỏ. Mã không bao giờ đánh
25
25
  Khi PO đồng ý kế hoạch:
26
26
 
27
27
  1. **Một** lần `SE edit D` cho mọi chỗ sửa:
28
- - Mục sửa và mục thêm mang `✅`, vì PO vừa duyệt kế hoạch.
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
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
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=—`.
package/ref/prd/new.md CHANGED
@@ -8,15 +8,15 @@ Chỉ **2 lượt** hỏi-đáp, vì epic đã được làm rõ kỹ. Không h
8
8
  2. Chạy `SE next-uc {specs}` để lấy mã UC đầu tiên. Các UC tiếp theo tăng dần từ mã đó.
9
9
  3. Đề xuất cách chia UC.
10
10
  - **Một UC = một mục tiêu của một actor**, xong trong một lần tương tác. Ví dụ "Đổi và quên mật khẩu", không phải "Quản lý bảo mật".
11
- - Mỗi UC nên có khoảng 2–8 rule. Nhiều hơn thì cân nhắc tách, ít hơn một thì cân nhắc gộp.
12
- - **Mọi `R…` và `AC…` của epic phải thuộc ít nhất một UC.** Một rule áp cho nhiều UC thì ghi ở tất cả các UC đó.
11
+ - Mỗi UC nên có khoảng 2–8 Business Rule (BR). Nhiều hơn thì cân nhắc tách, ít hơn một thì cân nhắc gộp.
12
+ - **Mọi `BR…` và `AC…` của epic phải thuộc ít nhất một UC.** Một BR áp cho nhiều UC thì ghi ở tất cả các UC đó.
13
13
  4. Trình cho PO dạng bảng:
14
14
 
15
15
  | UC | Tên | Actor | Lấy từ epic |
16
16
  |---|---|---|---|
17
- | UC-001 | … | ACT-01 | R8, R9, R10, AC4, AC5 |
17
+ | UC-001 | … | ACT-01 | BR8, BR9, BR10, AC4, AC5 |
18
18
 
19
- Kèm tối đa 3 câu hỏi khác, chỉ khi cần. Ví dụ: một rule nên đặt vào UC nào, hoặc hai UC có nên gộp không. Không có gì cần hỏi thì chỉ hỏi *"Cách chia này được chưa?"*.
19
+ Kèm tối đa 3 câu hỏi khác, chỉ khi cần. Ví dụ: một BR nên đặt vào UC nào, hoặc hai UC có nên gộp không. Không có gì cần hỏi thì chỉ hỏi *"Cách chia này được chưa?"*.
20
20
 
21
21
  ## Lượt 2 — Viết PRD
22
22
 
@@ -24,10 +24,10 @@ 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
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 rule, tiêu chí.
28
- - **Cột Rule:** một rule mỗi dòng, dạng *"Hệ thống PHẢI / KHÔNG ĐƯỢC …"*.
29
- - **Cột 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
- - **Tiêu chí:** dạng *"Khi … thì …"*, mô tả **kết quả nhìn thấy được**, không mô tả cơ chế.
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).
28
+ - **Cột Business Rule:** một BR mỗi dòng, dạng *"Hệ thống PHẢI / KHÔNG ĐƯỢC …"*.
29
+ - **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
+ - **AC:** dạng *"Khi … thì …"*, mô tả **kết quả nhìn thấy được**, không mô tả cơ chế.
31
31
  - **3. Màn hình:** chuyển từ "Màn hình chính" của epic, kèm cột UC.
32
32
  - **4. Câu hỏi còn mở:** chỉ những gì phát sinh khi viết PRD.
33
33
  - **5. Lịch sử thay đổi:** dòng `1.0`.
@@ -39,12 +39,12 @@ Mỗi câu hỏi và câu trả lời được ghi vào **Nhật ký làm rõ**.
39
39
 
40
40
  ## Checkpoint 3 — Chốt để sang PRD
41
41
 
42
- 1. Từ checkpoint 1–2 và nhật ký, AI **đề xuất** danh sách rule nghiệp vụ, dạng *"Hệ thống PHẢI / KHÔNG ĐƯỢC …"*. Đánh số `R1.`, `R2.`… ở đầu dòng. Chèn giữa thì dùng `R8a.`, **không đánh số lại**, vì `/prd` dùng các mã này để kiểm không rơi rule nào.
43
- 2. AI **đề xuất** tiêu chí chấp nhận, dạng *"Khi … thì …"*, đánh số `AC1.`, `AC2.`…. Mỗi tiêu chí phải kiểm được là đạt hay không đạt.
42
+ 1. Từ checkpoint 1–2 và nhật ký, AI **đề xuất** danh sách Business Rule (BR), dạng *"Hệ thống PHẢI / KHÔNG ĐƯỢC …"*. Đánh số `BR1.`, `BR2.`… ở đầu dòng. Chèn giữa thì dùng `BR8a.`, **không đánh số lại**, vì `/prd` dùng các mã này để kiểm không rơi BR nào.
43
+ 2. AI **đề xuất** Acceptance Criteria (điều kiện nghiệm thu), dạng *"Khi … thì …"*, đánh số `AC1.`, `AC2.`…. Mỗi AC phải kiểm được là đạt hay không đạt.
44
44
  3. **Tự kiểm độ phủ** trước khi trình cho PO:
45
- - Mỗi bước trong luồng chính có ít nhất một rule hoặc tiêu chí.
45
+ - Mỗi bước trong luồng chính có ít nhất một BR hoặc AC.
46
46
  - Mỗi edge case có kết quả mong muốn.
47
- - Không có hai rule mâu thuẫn nhau.
47
+ - Không có hai BR mâu thuẫn nhau.
48
48
  4. Trình cho PO. PO chốt từng mục. Mục nào PO chưa chốt thì giữ dấu `🤖`.
49
49
 
50
50
  Chỉ đặt `status: ready` khi đủ cả hai điều kiện: **mọi mục đều mang dấu `✅`** và **mục `sec:open` = "Không còn"**. Còn câu hỏi mở thì vẫn ghi file, `status` giữ `in-progress`.
package/templates/prd.md CHANGED
@@ -14,7 +14,7 @@ updated: {YYYY-MM-DD}
14
14
  > PRD chính thức. Nguồn làm rõ yêu cầu: `{đường dẫn file epic}` (chỉ còn là lịch sử, không sửa tiếp ở đó).
15
15
  > Dấu: `✅` = PO đã xác nhận · `🤖` = AI thêm khi viết PRD, chờ PO chốt.
16
16
  > Mã (`UC-…`, `…-BR…`, `…-AC…`) **không bao giờ đổi, không dùng lại**. Mục bỏ đi thì gạch ngang và ghi "Đã bỏ (v…)".
17
- > `(nguồn: R3, AC1)` = mục lấy từ rule / tiêu chí nào của epic.
17
+ > `(nguồn: …)` chỉ ghi mã: `BR3` / `AC1` của epic · `CON-02` (ràng buộc) · `PRD` (thêm khi viết PRD, PO đã duyệt).
18
18
 
19
19
  ## 1. Tổng quan <!-- sec:overview -->
20
20
 
@@ -42,13 +42,13 @@ updated: {YYYY-MM-DD}
42
42
  |---|---|---|
43
43
  | 1 | {…} | {…} |
44
44
 
45
- **Rule**
45
+ **Business Rule (BR)**
46
46
 
47
- | Mã | Rule | Logic | Nguồn |
47
+ | Mã | Business Rule | Business Logic | Nguồn |
48
48
  |---|---|---|---|
49
- | ✅ UC-{NNN}-BR01 | {Hệ thống PHẢI / KHÔNG ĐƯỢC …} | {rẽ nhánh, công thức, điều kiện; thông báo khi lỗi} | (nguồn: R1) |
49
+ | ✅ UC-{NNN}-BR01 | {Hệ thống PHẢI / KHÔNG ĐƯỢC …} | {rẽ nhánh, công thức, điều kiện; thông báo khi lỗi} | (nguồn: BR1) |
50
50
 
51
- **Tiêu chí chấp nhận**
51
+ **Acceptance Criteria (điều kiện nghiệm thu)**
52
52
 
53
53
  - ✅ UC-{NNN}-AC01. Khi {…} thì {…}. (nguồn: AC1)
54
54
 
@@ -49,17 +49,17 @@ updated: {YYYY-MM-DD}
49
49
 
50
50
  ## Checkpoint 3 — Chốt để sang PRD <!-- sec:cp3 -->
51
51
 
52
- **Rule nghiệp vụ** (bản nháp, PRD sẽ viết chính thức). Đánh số `R1.`, `R2.`…; chèn thêm giữa thì dùng `R8a.`, không đánh số lại:
53
- - 🤖 R1. {Hệ thống PHẢI / KHÔNG ĐƯỢC …}
52
+ **Business Rule (BR)** (bản nháp, PRD sẽ viết chính thức). Đánh số `BR1.`, `BR2.`…; chèn thêm giữa thì dùng `BR8a.`, không đánh số lại. Sang PRD sẽ thành `UC-xxx-BRnn`:
53
+ - 🤖 BR1. {Hệ thống PHẢI / KHÔNG ĐƯỢC …}
54
54
 
55
- **Tiêu chí chấp nhận** (bản nháp). Đánh số `AC1.`, `AC2.`…:
55
+ **Acceptance Criteria (điều kiện nghiệm thu)** (bản nháp). Đánh số `AC1.`, `AC2.`…:
56
56
  - 🤖 AC1. {Khi … thì …}
57
57
 
58
58
  ## Nhật ký làm rõ <!-- sec:log -->
59
59
 
60
60
  | Vòng | # | Nhóm | Câu hỏi | PO trả lời |
61
61
  |---|---|---|---|---|
62
- | 1 | 1 | {Phạm vi / Luồng / Rule / Phụ thuộc / Thuật ngữ} | {…} | {…} |
62
+ | 1 | 1 | {Phạm vi / Luồng / Business Rule / Phụ thuộc / Thuật ngữ} | {…} | {…} |
63
63
 
64
64
  ## Câu hỏi còn mở <!-- sec:open -->
65
65
 
@@ -46,7 +46,7 @@ updated: {YYYY-MM-DD}
46
46
 
47
47
  ## 6. Ràng buộc <!-- sec:constraints -->
48
48
 
49
- > Ràng buộc áp cho **mọi epic**. PRD có rule bắt nguồn từ ràng buộc nào thì ghi `(nguồn: CON-01)`.
49
+ > Ràng buộc áp cho **mọi epic**. PRD có Business Rule (BR) bắt nguồn từ ràng buộc nào thì ghi `(nguồn: CON-01)`.
50
50
 
51
51
  | Mã | Loại | Ràng buộc |
52
52
  |---|---|---|
@@ -13,11 +13,11 @@ Cách dùng (nội dung truyền qua stdin):
13
13
  spec_edit.py edit <file> stdin = JSON [{"old": "...", "new": "...", "all": false}]
14
14
  spec_edit.py set <file> key=value… sửa frontmatter
15
15
  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 R8b ACT-03)
16
+ spec_edit.py confirm <file> <mã>… đổi 🤖 → ✅ trên dòng chứa mã (vd AC2 BR8b ACT-03)
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 coverage <epic> <prd> liệt kê rule / tiêu chí của epic chưa được PRD dùng tới
20
+ spec_edit.py coverage <epic> <prd> liệt kê BR / AC của epic chưa được PRD dùng tới
21
21
 
22
22
  Mã thoát: 0 = thành công · 1 = lỗi dữ liệu (không ghi gì) · 2 = sai cách dùng.
23
23
  """
@@ -42,7 +42,13 @@ COMMON_INT = {"open_questions": (0, 999)}
42
42
  DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
43
43
  VERSION = re.compile(r"^\d+\.\d+$")
44
44
  UC_HEADING = re.compile(r"^###\s+UC-(\d{3})\b", re.M)
45
- EPIC_ITEM = re.compile("^\\s*- [\U0001F916\u2705] ((?:R|AC)\\d+[a-z]?)\\.", re.M)
45
+ EPIC_ITEM = re.compile("^\\s*- [\U0001F916\u2705] ((?:BR|R|AC)\\d+[a-z]?)\\.", re.M)
46
+ # Epic theo khu\u00f4n tr\u01b0\u1edbc 0.6.1 \u0111\u00e1nh s\u1ed1 Business Rule l\u00e0 R1\u2026; t\u1eeb 0.6.1 l\u00e0 BR1\u2026. Coi hai ki\u1ec3u l\u00e0 m\u1ed9t m\u00e3.
47
+ OLD_BR = re.compile(r"(?<![\w-])R(\d+[a-z]?)(?![\w])")
48
+
49
+
50
+ def br_code(code):
51
+ return OLD_BR.sub(r"BR\1", code.strip())
46
52
  SOURCE_REF = re.compile(r"\(nguồn:\s*([^)]*)\)")
47
53
 
48
54
 
@@ -74,6 +80,7 @@ def cmd_create(path):
74
80
  fail(path + " đã tồn tại — không ghi đè. Dùng `edit` hoặc `set`.")
75
81
  text = stdin_text()
76
82
  check_keys(file_type(text), [], sec_keys(text), creating=True)
83
+ check_sources(file_type(text), text)
77
84
  write(path, text)
78
85
  print("đã tạo " + path)
79
86
 
@@ -103,6 +110,7 @@ def cmd_edit(path):
103
110
  for e in edits:
104
111
  text = text.replace(e["old"], e["new"]) if e.get("all") else text.replace(e["old"], e["new"], 1)
105
112
  check_keys(file_type(text), before, sec_keys(text), creating=False)
113
+ check_sources(file_type(text), text)
106
114
  write(path, text)
107
115
  print("đã sửa %d chỗ trong %s" % (len(edits), path))
108
116
 
@@ -223,6 +231,34 @@ def template_sections(ftype):
223
231
  return [(k, normalize_title(t), SEC_KEY.sub("", t).strip()) for _, _, t, k in headings(lines) if k]
224
232
 
225
233
 
234
+ # (nguồn: …) chỉ nhận mã, để script truy được BR/AC của PRD về đúng chỗ sinh ra nó:
235
+ # BR3 / AC1 (epic; R3 là kiểu cũ) · CON-02 (ràng buộc tầng sản phẩm) · PRD (thêm khi viết PRD, PO đã duyệt)
236
+ SOURCE_TOKEN = re.compile(r"^(?:BR\d+[a-z]?|R\d+[a-z]?|AC\d+[a-z]?|CON-\d+|PRD)$")
237
+
238
+
239
+ def bad_sources(text):
240
+ out = []
241
+ # Dòng trích dẫn (>) là chú thích của khuôn (vd "(nguồn: …) chỉ ghi mã"), không phải nguồn thật.
242
+ body = "\n".join(l for l in text.split("\n") if not l.lstrip().startswith(">"))
243
+ for refs in SOURCE_REF.findall(body):
244
+ for r in (x.strip() for x in refs.split(",")):
245
+ if r and not SOURCE_TOKEN.match(r) and r not in out:
246
+ out.append(r)
247
+ return out
248
+
249
+
250
+ def source_rule_msg(bad):
251
+ return ("(nguồn: …) chỉ được ghi mã — BR3 / AC1 của epic, CON-02, hoặc PRD (mục thêm khi viết PRD, "
252
+ "PO đã duyệt). Đang ghi: " + " · ".join('"%s"' % b for b in bad))
253
+
254
+
255
+ def check_sources(ftype, text):
256
+ if ftype == "prd":
257
+ bad = bad_sources(text)
258
+ if bad:
259
+ fail(source_rule_msg(bad))
260
+
261
+
226
262
  def check_keys(ftype, before, after, creating):
227
263
  dup = sorted({k for k in after if after.count(k) > 1})
228
264
  if dup:
@@ -271,10 +307,23 @@ def cmd_upgrade(path):
271
307
  added.append(k)
272
308
  else:
273
309
  unknown.append(title)
274
- if added:
275
- write(path, "\n".join(lines))
310
+ new_text = "\n".join(lines)
311
+ # Mã Business Rule: R3 → BR3 (khuôn từ 0.6.1). Epic: đổi mọi chỗ. PRD: chỉ trong (nguồn: …).
312
+ renamed = [0]
313
+ if ftype == "epic":
314
+ new_text, renamed[0] = OLD_BR.subn(r"BR\1", new_text)
315
+ elif ftype == "prd":
316
+ def fix(m):
317
+ out = OLD_BR.sub(r"BR\1", m.group(0))
318
+ renamed[0] += out != m.group(0)
319
+ return out
320
+ new_text = SOURCE_REF.sub(fix, new_text)
321
+ if new_text != text:
322
+ write(path, new_text)
276
323
  missing = [(k, orig) for k, _, orig in tpl if k not in used]
277
324
  print("đã gắn mã: %s" % (", ".join(added) or "không có gì mới"))
325
+ if renamed[0]:
326
+ print("đã đổi mã Business Rule R… → BR…: %d chỗ" % renamed[0])
278
327
  if unknown:
279
328
  print("⚠ tiêu đề không khớp khuôn (giữ nguyên): " + " · ".join(unknown))
280
329
  if missing:
@@ -293,21 +342,28 @@ def cmd_next_uc(specs_dir):
293
342
  def cmd_coverage(epic_path, prd_path):
294
343
  wanted = []
295
344
  for code in EPIC_ITEM.findall(read(epic_path)):
296
- if code not in wanted:
297
- wanted.append(code)
345
+ if br_code(code) not in wanted:
346
+ wanted.append(br_code(code))
298
347
  used = set()
299
348
  for refs in SOURCE_REF.findall(read(prd_path)):
300
- used.update(r.strip() for r in refs.split(","))
349
+ used.update(br_code(r) for r in refs.split(","))
301
350
  missing = [c for c in wanted if c not in used]
302
- # CON-xx là ràng buộc của tầng sản phẩm, không thuộc epic — hợp lệ.
303
- unknown = sorted(r for r in used - set(wanted) - {""} if not re.match(r"^CON-\d+$", r))
304
- print("epic có %d rule/tiêu chí · PRD dùng %d" % (len(wanted), len(wanted) - len(missing)), flush=True)
351
+ bad = bad_sources(read(prd_path))
352
+ # CON-xx (ràng buộc tầng sản phẩm) và PRD (thêm khi viết PRD) hợp lệ dù không có trong epic.
353
+ unknown = sorted(r for r in used - set(wanted) - {""}
354
+ if SOURCE_TOKEN.match(r) and not re.match(r"^(CON-\d+|PRD)$", r))
355
+ print("epic có %d BR/AC · PRD dùng %d" % (len(wanted), len(wanted) - len(missing)), flush=True)
356
+ problems = []
357
+ if bad:
358
+ problems.append(source_rule_msg(bad))
305
359
  if unknown:
306
- print("⚠ PRD ghi nguồn không có trong epic: " + ", ".join(unknown), flush=True)
360
+ problems.append("PRD ghi nguồn không có trong epic: %s (gõ nhầm mã?)" % ", ".join(unknown))
307
361
  if missing:
308
- fail("chưa được PRD dùng tới: " + ", ".join(missing)
309
- + ". Đưa vào một UC với (nguồn: …), hoặc hỏi PO có bỏ không.")
310
- print("đủ — mọi rule/tiêu chí của epic đều có trong PRD")
362
+ problems.append("chưa được PRD dùng tới: %s. Đưa vào một UC với (nguồn: …), hoặc hỏi PO có bỏ không."
363
+ % ", ".join(missing))
364
+ if problems:
365
+ fail("\n - ".join([""] + problems))
366
+ print("đủ — mọi BR/AC của epic đều có trong PRD")
311
367
 
312
368
 
313
369
  PENDING, CONFIRMED = "\U0001F916", "✅"
@@ -327,7 +383,7 @@ def cmd_pending(path):
327
383
 
328
384
  def cmd_confirm(path, codes):
329
385
  if not codes:
330
- fail("cần ít nhất một mã, ví dụ: confirm <file> AC2 AC3 R8b", 2)
386
+ fail("cần ít nhất một mã, ví dụ: confirm <file> AC2 AC3 BR8b", 2)
331
387
  lines = read(path).split("\n")
332
388
  # Mã khớp nguyên từ: AC2 không được khớp nhầm AC20.
333
389
  targets = {}