@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 +7 -0
- package/commands/prd.md +4 -4
- package/commands/product.md +1 -1
- package/package.json +1 -1
- package/ref/prd/change.md +3 -3
- package/ref/prd/new.md +8 -8
- package/ref/product/epic.md +4 -4
- package/templates/prd.md +5 -5
- package/templates/product-epic.md +4 -4
- package/templates/product.md +1 -1
- package/tools/spec_edit.py +72 -16
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,
|
|
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.
|
|
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
|
|
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}
|
|
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
|
```
|
package/commands/product.md
CHANGED
|
@@ -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
|
|
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
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
|
|
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
|
-
-
|
|
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
|
|
12
|
-
- **Mọi `
|
|
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 |
|
|
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
|
|
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
|
|
28
|
-
- **Cột Rule:** một
|
|
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
|
-
- **
|
|
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`.
|
package/ref/product/epic.md
CHANGED
|
@@ -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
|
|
43
|
-
2. AI **đề xuấ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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
**
|
|
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
|
|
53
|
-
- 🤖
|
|
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
|
-
**
|
|
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
|
|
package/templates/product.md
CHANGED
|
@@ -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ó
|
|
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
|
|---|---|---|
|
package/tools/spec_edit.py
CHANGED
|
@@ -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
|
|
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ê
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
360
|
+
problems.append("PRD ghi nguồn không có trong epic: %s (gõ nhầm mã?)" % ", ".join(unknown))
|
|
307
361
|
if missing:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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 = {}
|