@educa-corp/fw 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
3
3
  > `fw install` in các mục của những version mới hơn bản đang cài trong dự án.
4
4
  > Mỗi lần sửa lệnh, khuôn, bộ câu hỏi hay công cụ đều phải tăng version và thêm một mục ở đây. Test sẽ báo lỗi nếu version chưa có mục.
5
5
 
6
+ ## 0.11.1 — 2026-10-02
7
+
8
+ Sửa vòng lặp *review → change → refine → finding mới*. Lần chạy đầu trên LMS: review gợi ý đổi tên một trạng thái, dẫn tới `change` rồi refine ra 8 finding, tốn khoảng 4,4M token mà không có thay đổi nghiệp vụ nào.
9
+
10
+ - **`/prd review` không đưa ý kiến về nội dung**: không đề xuất đổi tên một khái niệm đang được gọi nhất quán, không gợi ý `/prd change`. Thuật ngữ chỉ đề xuất sửa khi cùng một khái niệm mà gọi bằng hai tên.
11
+ - **`--form` chặn một chỗ sửa thì AI hỏi bạn có đổi nghĩa không.** Không đổi nghĩa thì áp ngay, vẫn giữ version. Sửa chỗ trỏ "xem mục 5" → "mục 6" không còn bị chặn.
12
+ - **Refine sau `/prd change` chỉ báo lỗi do chính thay đổi gây ra**, cộng với critical. Vấn đề đã có ở bản đã duyệt được gom vào *"Có từ trước, để sau"*, không phải quyết, không chặn duyệt.
13
+ - **`spec_edit diff <prd>`**: in phần đã đổi so với bản `approved` gần nhất trong git, kèm số UC bị chạm. Refine và review sau change dùng lệnh này để lấy đúng phạm vi.
14
+
6
15
  ## 0.11.0 — 2026-10-02
7
16
 
8
17
  - **Chế độ mới `/prd <EPIC-ID> review`**: rà **hình thức** PRD (chính tả, câu chữ, thuật ngữ, đúng khuôn), chạy sau refine. **Bắt buộc**: PRD không duyệt được nếu chưa review đúng version (`reviewed` = `version`), và `/bdd` dừng nếu PRD chưa review. Review sửa **trong cùng version**, không tăng version, PRD đã `approved` vẫn giữ `approved`. Lỗi chính tả và định dạng thì AI sửa luôn. Câu chữ, thuật ngữ, dòng sai khuôn thì bạn quyết, được quyết theo nhóm. Chỗ muốn sửa mà làm đổi nghĩa thì AI chỉ ghi chú.
@@ -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.11.0**. Chỉ mô tả những lệnh **đã có**. Lệnh mới có thì hướng dẫn mới được bổ sung.
4
+ > Hướng dẫn này viết cho **bản 0.11.1**. Chỉ mô tả những lệnh **đã có**. Lệnh mới có thì hướng dẫn mới được bổ sung.
5
5
  > Trong dự án đã cài framework, bản hướng dẫn đúng với version đang dùng nằm ở **`.fw/guide/`**. Có thể hỏi Claude: *"Đọc .fw/guide và cho biết cách đổi PRD"*.
6
6
 
7
7
  ---
@@ -71,7 +71,7 @@ Sau mỗi lượt có sửa, lệnh sẽ nhắc *"`/clear` rồi `/prd EP-01 ref
71
71
 
72
72
  ## Sau khi đổi PRD
73
73
 
74
- `/prd EP-01 change …` tăng version, nên bạn phải refine lại trước khi duyệt. Lần đó **chỉ soi các mục vừa đổi** (theo dòng Lịch sử thay đổi), nhưng agent vẫn đọc cả PRD. Vì thế PRD càng lớn thì mỗi lần đổi càng đắt: với PRD 91 KB của LMS, thêm một dòng tốn khoảng 6M token trước bản 0.11.0, và ước tính khoảng 3M từ bản 0.11.0. Lượt nào không có finding nào thì không cần agent kiểm chứng. Cách giảm chi phí hiệu quả nhất vẫn là **tách epic** để PRD nhỏ lại.
74
+ `/prd EP-01 change …` tăng version, nên bạn phải refine lại trước khi duyệt. Lần đó **chỉ soi đúng phần vừa đổi** so với bản đã duyệt gần nhất, và **chỉ báo lỗi do chính thay đổi gây ra** (cộng với critical). Lỗi đã có từ trước trong mục bị chạm chỉ được ghi vào *"Có từ trước, để sau"*, không cần bạn quyết. Agent vẫn đọc cả PRD để thấy ảnh hưởng lan sang chỗ khác. Vì thế PRD càng lớn thì mỗi lần đổi càng đắt: với PRD 91 KB của LMS, thêm một dòng tốn khoảng 6M token trước bản 0.11.0, và ước tính khoảng 3M từ bản 0.11.0. Lượt nào không có finding nào thì không cần agent kiểm chứng. Cách giảm chi phí hiệu quả nhất vẫn là **tách epic** để PRD nhỏ lại.
75
75
 
76
76
  ## Duyệt PRD
77
77
 
@@ -22,7 +22,7 @@
22
22
  |---|---|---|
23
23
  | **A. Tự sửa** | Chính tả, dấu tiếng Việt, dấu câu, bảng vỡ cột, danh sách lệch | AI sửa luôn, báo lại cho bạn số chỗ đã sửa |
24
24
  | **B. Đề xuất** | Câu khó hiểu (quá dài, đại từ không rõ chỉ ai, phủ định kép) · một khái niệm gọi hai tên, hoặc sai glossary · dòng sai khuôn, ví dụ dòng Nền tảng ghi bằng lời | **Bạn quyết** |
25
- | **C. Ghi chú** | Muốn sửa thì phải **đổi nghĩa**: đổi số, điều kiện, phạm vi | Không sửa. Muốn đổi thì dùng `/prd EP-01 change` |
25
+ | **C. Ghi chú** | Lỗi hình thức mà sửa thì chắc chắn đổi nghĩa | Không sửa, không chặn duyệt. Chỉ để bạn biết |
26
26
 
27
27
  Bạn quyết loại B theo mã, hoặc theo nhóm:
28
28
 
@@ -36,7 +36,9 @@ bác R05
36
36
  ## Review không bao giờ làm đổi nghĩa
37
37
 
38
38
  - **Không tăng version.** PRD đang `approved` thì vẫn giữ `approved`.
39
- - Công cụ **tự chặn** mọi chỗ sửa làm thay đổi **số, mã** (UC, BR, AC…), **dấu 🤖 / ✅, gạch ngang hoặc nguồn**. Ví dụ AI định sửa *"tối đa 1.000 dòng"* thành *"tối đa 2.000 dòng"* thì công cụ chặn. Chỗ đó được chuyển thành ghi chú C.
39
+ - Công cụ **tự chặn** mọi chỗ sửa làm thay đổi **số, mã** (UC, BR, AC…), **dấu 🤖 / ✅, gạch ngang hoặc nguồn**. Ví dụ AI định sửa *"tối đa 1.000 dòng"* thành *"tối đa 2.000 dòng"* thì công cụ chặn.
40
+ - Bị chặn mà thật ra không đổi nghĩa, ví dụ thêm mã `EP-03` vào tiêu đề cho khớp với bảng, thì **AI hỏi bạn**. Bạn trả lời *"không đổi nghĩa"* thì AI áp luôn, vẫn trong cùng version.
41
+ - **Review không đề xuất đổi tên khái niệm** đang được gọi nhất quán, và không gợi ý `/prd change`. Đặt tên là việc của nghiệp vụ. Lần đầu chạy ở LMS, review gợi ý đổi tên một trạng thái; lần đổi đó kéo theo refine 8 finding, tốn khoảng 4M token, và tên mới bị refine chỉ ra là sai nghĩa.
40
42
  - Nhờ vậy, review không cần refine lại sau đó.
41
43
 
42
44
  ## Phạm vi
@@ -44,7 +46,7 @@ bác R05
44
46
  | Khi nào | Rà gì |
45
47
  |---|---|
46
48
  | PRD chưa từng review | Toàn bộ PRD |
47
- | Sau `/prd EP-01 change` → refine | Chỉ các mục vừa đổi, theo Lịch sử thay đổi |
49
+ | Sau `/prd EP-01 change` → refine | Chỉ phần vừa đổi so với bản đã duyệt gần nhất |
48
50
 
49
51
  Thứ tự sau mỗi lần đổi PRD: `change` → `refine` → `review` → duyệt.
50
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@educa-corp/fw",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Framework làm việc với Claude Code cho phòng PTPM",
5
5
  "bin": {
6
6
  "fw": "bin/fw.js"
package/ref/prd/lenses.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Bạn là **một** lăng kính. Chỉ soi đúng phần của lăng kính mình, trong đúng **phạm vi** và **ngưỡng báo** được giao. Tìm ít nhưng đúng còn hơn tìm nhiều mà sai.
4
4
 
5
+ **Lượt sau `/prd change`** (phạm vi là kết quả `SE diff`): **chỉ báo lỗi do chính thay đổi gây ra**. Ví dụ: mâu thuẫn mới giữa phần vừa đổi và phần khác, chỗ phải đổi theo mà chưa đổi, nghĩa mới làm hỏng một BR hay AC khác. Ngoài ra chỉ báo thêm critical. **Không báo** vấn đề đã có y nguyên ở bản đã duyệt, kể cả khi nó nằm trong mục vừa bị chạm, vì bản đó đã qua refine. Vẫn đọc toàn bộ PRD để thấy ảnh hưởng lan sang chỗ khác.
6
+
5
7
  ## Luật chung
6
8
 
7
9
  1. **Viết bằng ngôn ngữ nghiệp vụ.** Mắt nhìn có thể là kỹ thuật, nhưng finding và đề xuất phải nói *nghiệp vụ còn thiếu hay chưa rõ điều gì*. Không đề xuất API, bảng dữ liệu, retry, timeout hay thư viện.
@@ -78,12 +80,14 @@ Bạn nhận: PRD `D`, file refine `R` (có thể chưa có), thư mục `W` ch
78
80
  - Finding có nêu lại một điểm **đã có quyết định** trong `R` không?
79
81
  - Mức độ có đúng định nghĩa không?
80
82
 
81
- Kết luận cho mỗi finding: `ĐÚNG` · `SAI` (bỏ) · `HẠ MỨC` (giữ, đổi mức).
83
+ - **Lượt sau `/prd change`:** vấn đề này **đã có ở bản đã duyệt** chưa? Xem phần `-` trong `SE diff D`, và phần PRD không nằm trong diff. Đã có từ trước thì đây **không phải** lỗi do thay đổi.
84
+
85
+ Kết luận cho mỗi finding: `ĐÚNG` · `SAI` (bỏ) · `HẠ MỨC` (giữ, đổi mức) · `CÓ TỪ TRƯỚC` (không thành finding; ghi một dòng vào mục *Có từ trước, để sau* ở cuối lượt, không cần PO quyết, không chặn duyệt).
82
86
 
83
87
  **2. Gộp** các finding ĐÚNG trùng nhau (cùng mục, cùng vấn đề), giữ bản rõ nhất. Hai đề xuất trái ngược nhau thì giữ cả hai trong **một** finding, và ghi rõ là cần PO chọn.
84
88
 
85
89
  **3. Sắp xếp** theo mức độ (critical → major → minor), sau đó theo lăng kính (QA → DEV → SA → Phạm vi). **Đánh mã F theo đúng thứ tự đó**, bắt đầu từ mã F kế tiếp.
86
90
 
87
- **4. Ghi vào `R`.** Chưa có `R` thì `SE create R` theo khuôn `.fw/core/templates/prd-refine.md`. Đã có thì `SE edit` thêm mục `## Lượt {n} — v{version} <!-- sec:round-{n} -->` ở **cuối**. **Không sửa các lượt cũ.** Thêm một dòng vào bảng Tóm tắt, rồi `SE set R prd_version={version} round={n} status=pending`. Mỗi finding theo đúng mẫu trong khuôn, `Quyết định: chờ`.
91
+ **4. Ghi vào `R`.** Chưa có `R` thì `SE create R` theo khuôn `.fw/core/templates/prd-refine.md`. Đã có thì `SE edit` thêm mục `## Lượt {n} — v{version} <!-- sec:round-{n} -->` ở **cuối**. **Không sửa các lượt cũ.** Thêm một dòng vào bảng Tóm tắt, rồi `SE set R prd_version={version} round={n} status=pending`. Mỗi finding theo đúng mẫu trong khuôn, `Quyết định: chờ`. Có finding `CÓ TỪ TRƯỚC` thì thêm `### Có từ trước, để sau` ở cuối lượt, mỗi vấn đề một dòng: *"- {mã mục}: {vấn đề}"*.
88
92
 
89
- **5. Trả về một dòng:** *"thô {a} · bỏ {b} · hạ mức {c} · còn {d} (critical {x} · major {y} · minor {z}) → {R}"*.
93
+ **5. Trả về một dòng:** *"thô {a} · bỏ {b} · hạ mức {c} · có từ trước {e} · còn {d} (critical {x} · major {y} · minor {z}) → {R}"*.
package/ref/prd/refine.md CHANGED
@@ -19,7 +19,7 @@ Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đ
19
19
  | `refined` = `version` | **Dừng.** Báo *"Bản v{version} đã refine xong."* Có `--full` thì chạy lượt 1 trên toàn PRD | — |
20
20
  | Chưa có `R`, hoặc chưa từng refine | **1** | Toàn bộ PRD |
21
21
  | `R` có `round: 1` **đã áp** bản sửa, `prd_version` < `version` | **2** | Chỉ các mục mà lượt 1 đã sửa hoặc thêm (đọc từ quyết định `nhận` / `sửa` trong `R`), cộng các mục tham chiếu tới chúng |
22
- | `refined` có giá trị nhưng < `version` (PRD đã qua `/prd change`) | **1** | Chỉ các mục trong các dòng Lịch sử thay đổi (`SE section D history`) có version lớn hơn `refined` |
22
+ | `refined` có giá trị nhưng < `version` (PRD đã qua `/prd change`) | **1** | **Đúng phần đã đổi**: `SE diff D` (so với bản `approved` gần nhất trong git, có sẵn số UC bị chạm). Không có bản đã duyệt trong git thì dùng các dòng Lịch sử thay đổi có version lớn hơn `refined`. Ngưỡng: **chỉ lỗi do chính thay đổi gây ra, hoặc critical** (xem đầu `lenses.md`) |
23
23
 
24
24
  **Không có lượt 3.** Sau lượt 2 thì luôn đặt `refined` (Bước D).
25
25
 
@@ -27,7 +27,7 @@ Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đ
27
27
 
28
28
  Phiên chính **không đọc** kết quả chi tiết của các agent. Agent ghi ra file, phiên chính chỉ đọc bảng tóm tắt cuối cùng.
29
29
 
30
- 1. **Chọn số agent rà** theo lượt và phạm vi ở Bước A. Đếm UC bằng các mã `UC-…` nêu trong phạm vi. Thay đổi chỉ ở Tổng quan, User flow hoặc Màn hình tính là 0 UC.
30
+ 1. **Chọn số agent rà** theo lượt và phạm vi ở Bước A. Số UC bị chạm lấy ở dòng đầu của `SE diff D`. Thay đổi chỉ ở Tổng quan, User flow hoặc Màn hình tính là 0 UC.
31
31
 
32
32
  | Lượt và phạm vi | Agent rà |
33
33
  |---|---|
@@ -49,7 +49,7 @@ Phiên chính **không đọc** kết quả chi tiết của các agent. Agent g
49
49
  ## Bước C — Trình
50
50
 
51
51
  1. Đọc `SE section R summary` và danh sách tiêu đề finding của lượt này. Danh sách đã được agent kiểm chứng **sắp xếp sẵn** theo mức độ, rồi theo lăng kính (QA → DEV → SA → Phạm vi).
52
- 2. Trình **một bảng tóm tắt** gồm mã · mức · lăng kính · mục · một dòng vấn đề, đúng theo thứ tự đó. Kèm số finding thô, số bị bỏ ở bước kiểm chứng, và số bị hạ mức.
52
+ 2. Trình **một bảng tóm tắt** gồm mã · mức · lăng kính · mục · một dòng vấn đề, đúng theo thứ tự đó. Kèm số finding thô, số bị bỏ ở bước kiểm chứng, số bị hạ mức, và số vấn đề *có từ trước* (chỉ báo số, không cần PO quyết).
53
53
  3. Nhắc PO cách quyết:
54
54
  - *"Chi tiết xem {R}. Quyết theo mã: 'nhận F01, F03 · bác F02 vì … · F04 sửa thành … · hoãn F05'."*
55
55
  - *"Quyết theo nhóm được, áp cho major và minor: 'nhận tất cả minor' · 'nhận hết UC-003 trừ F12' · 'bác F20–F24 vì …'. Critical phải quyết từng mã."*
package/ref/prd/review.md CHANGED
@@ -18,7 +18,7 @@ Xét **từ trên xuống**, gặp dòng nào khớp trước thì theo dòng đ
18
18
  | `reviewed` = `version`, không có `--full` | **Dừng.** Báo *"Bản v{version} đã rà hình thức xong."* |
19
19
  | Đã có `W/review-{version}.md` (rà xong, đang chờ PO quyết) | **Không rà lại.** Sang thẳng Bước C |
20
20
  | `reviewed` là `—` (chưa từng review), hoặc có `--full` | Phạm vi: **toàn bộ PRD** |
21
- | `reviewed` < `version` | Phạm vi: chỉ các mục nêu trong các dòng Lịch sử thay đổi (`SE section D history`) có version **lớn hơn** `reviewed` |
21
+ | `reviewed` < `version` | Phạm vi: **đúng phần đã đổi**, lấy bằng `SE diff D` (so với bản `approved` gần nhất trong git). Không có bản đã duyệt trong git thì dùng các dòng Lịch sử thay đổi có version lớn hơn `reviewed` |
22
22
 
23
23
  ## Bước B — Rà (1 sub-agent)
24
24
 
@@ -30,12 +30,15 @@ Dùng công cụ **Agent**, `model: "sonnet"`, gọi **1** agent. Giao cho agent
30
30
  2. Trình theo thứ tự:
31
31
  - **Đã tự sửa (A):** chỉ báo số chỗ, kèm 2–3 ví dụ. Danh sách đầy đủ nằm trong file.
32
32
  - **Bảng đề xuất (B):** mã · loại · mục · *trước → sau*, rút gọn mỗi bên khoảng 60 ký tự.
33
- - **Ghi chú (C):** những chỗ muốn sửa thì phải đổi nghĩa. Mỗi chỗ một dòng, kèm gợi ý `/prd {id} change …`.
33
+ - **Ghi chú (C):** mỗi chỗ một dòng, để PO biết. **Không gợi ý `/prd … change`**, và nói rõ ghi chú không chặn duyệt. Đổi PRD là quyết định nghiệp vụ của PO, review không tự đề xuất.
34
34
  3. Nhắc PO cách quyết: *"Quyết theo mã: 'nhận hết' · 'nhận hết trừ R03' · 'R04 sửa thành …' · 'bác R05'."* Không có đề xuất nào thì sang thẳng Bước D.
35
35
 
36
36
  ## Bước D — Áp
37
37
 
38
- 1. Áp các đề xuất được nhận bằng **một** lần `SE edit --form D`, với `old` là "Trước" và `new` là "Sau" (hoặc bản PO sửa). `SE` chặn chỗ nào thì chỗ đó làm đổi nghĩa: bỏ chỗ đó ra khỏi lần sửa, báo PO, và chuyển nó thành ghi chú C.
38
+ 1. Áp các đề xuất được nhận bằng **một** lần `SE edit --form D`, với `old` là "Trước" và `new` là "Sau" (hoặc bản PO sửa).
39
+ - `SE` chặn chỗ nào (vì chỗ đó đổi số, mã, dấu hoặc nguồn) thì **hỏi PO ngay**, kèm đúng thứ bị đổi. Ví dụ: *"R07 thêm mã EP-03 vào tiêu đề J7, cho khớp với tên trong bảng. Chỗ này có đổi nghĩa không?"*
40
+ - PO nói **không đổi nghĩa**: áp chỗ đó bằng `SE edit D` (không `--form`), vẫn trong cùng version.
41
+ - PO nói **có**: không sửa, chuyển thành ghi chú C.
39
42
  2. Có chỗ nào được sửa (A hoặc B) thì thêm **một** dòng vào cuối bảng Lịch sử thay đổi bằng `SE edit D`, **giữ nguyên version**: `| {version} | {ngày} | Rà hình thức: sửa {k} chỗ (chính tả, câu chữ, thuật ngữ, khuôn). Không đổi nội dung |`.
40
43
  3. `SE set D reviewed={version} updated={ngày}`.
41
44
  - PRD đang `approved` thì `SE` kiểm lại mọi điều kiện duyệt. Báo lỗi thì sửa theo lỗi, thường là một dòng sai khuôn còn sót, rồi chạy lại.
@@ -53,10 +56,15 @@ Bạn rà **hình thức** của PRD trong đúng phạm vi được giao. Phạ
53
56
  | Loại | Gồm | Làm gì |
54
57
  |---|---|---|
55
58
  | **A. Tự sửa** | Chính tả, dấu tiếng Việt, dấu câu, khoảng trắng thừa, bảng vỡ cột, danh sách lệch | Sửa luôn bằng **một** lần `SE edit --form D`. `SE` chặn chỗ nào thì chuyển chỗ đó sang B hoặc C |
56
- | **B. Đề xuất** | Câu khó hiểu (quá dài, nhiều mệnh đề lồng nhau, đại từ không rõ chỉ ai, phủ định kép) · một khái niệm gọi hai tên, hoặc sai so với glossary và phần Khái niệm · dòng sai khuôn, ví dụ dòng Nền tảng ghi bằng lời thay vì mã `web` · `app` · `system` | **Không sửa.** Ghi vào file để PO quyết |
57
- | **C. Ghi chú** | Chỗ chỉ sửa được nếu đổi nghĩa: số, điều kiện, phạm vi, thêm hoặc bớt ý | **Không sửa.** Ghi một dòng |
59
+ | **B. Đề xuất** | Câu khó hiểu (quá dài, nhiều mệnh đề lồng nhau, đại từ không rõ chỉ ai, phủ định kép) · **cùng một khái niệm mà gọi bằng hai tên** ở hai chỗ, hoặc sai so với glossary và phần Khái niệm (đề xuất dùng tên **đang dùng nhiều hơn**) · tham chiếu trỏ sai ("xem mục 5" trong khi đúng là mục 6) · dòng sai khuôn, ví dụ dòng Nền tảng ghi bằng lời thay vì mã `web` · `app` · `system`. A bị `SE` chặn thì cũng đưa vào đây | **Không sửa.** Ghi vào file để PO quyết |
60
+ | **C. Ghi chú** | Lỗi hình thức mà sửa thì chắc chắn đổi nghĩa: số, điều kiện, phạm vi | **Không sửa.** Ghi một dòng |
58
61
 
59
- **Không làm:** rà nội dung nghiệp vụ (đó là việc của refine), đổi mã, đổi số, đổi dấu `🤖`/`✅`, đổi `(nguồn: …)`, sửa phần ngoài phạm vi. Viết lại câu thì giữ đúng mọi ý và mọi giá trị của câu cũ.
62
+ **Không làm:**
63
+ - Rà nội dung nghiệp vụ (việc của refine). Đổi mã, đổi số, đổi dấu `🤖`/`✅`, đổi `(nguồn: …)`. Sửa phần ngoài phạm vi.
64
+ - **Đề xuất đổi tên một khái niệm đang được gọi nhất quán**, dù bạn thấy tên chưa hay hoặc đã cũ. Ví dụ: "đang dùng mật khẩu mặc định" được gọi đúng một tên ở mọi chỗ thì để nguyên, không đề xuất. Đặt tên khái niệm là quyết định nội dung. Lần đổi tên như vậy ở LMS v1.8 đã tốn khoảng 3M token refine, và tên mới bị refine chỉ ra là sai nghĩa.
65
+ - Ghi ý kiến về nghiệp vụ vào C, hoặc gợi ý `/prd … change`.
66
+
67
+ Viết lại câu thì giữ đúng mọi ý và mọi giá trị của câu cũ.
60
68
 
61
69
  **Ghi file** `W/review-{version}.md` bằng `SE create --replace`, đúng khuôn sau:
62
70
 
@@ -71,7 +79,7 @@ Bạn rà **hình thức** của PRD trong đúng phạm vi được giao. Phạ
71
79
  - Lý do: {…}
72
80
 
73
81
  ## Ghi chú (C)
74
- - {mã mục}: {vấn đề} → cần `/prd … change`
82
+ - {mã mục}: {lỗi hình thức không sửa được mà không đổi nghĩa}
75
83
  ```
76
84
 
77
85
  Mục nào không có gì thì ghi `Không có`. Sau đó **chỉ trả về một dòng**: *"sửa {a} · đề xuất {b} · ghi chú {c} → {file}"*.
@@ -31,6 +31,7 @@ Cách dùng (nội dung truyền qua stdin):
31
31
  spec_edit.py scenario <feature> <SC…>… in đúng các kịch bản đó
32
32
  spec_edit.py sc <feature> stdin = JSON [{"id": mã, "new": khối} | {"after": mã | "end", "new": khối} | {"remove": mã, "version": "1.1"}]
33
33
  spec_edit.py bddcheck <feature> [prd] kiểm khuôn, tag, độ phủ AC / BR của file BDD
34
+ spec_edit.py diff <prd> phần đã đổi so với bản `approved` gần nhất trong git, kèm UC bị chạm
34
35
 
35
36
  Mã thoát: 0 = thành công · 1 = lỗi dữ liệu (không ghi gì) · 2 = sai cách dùng.
36
37
  """
@@ -118,7 +119,9 @@ FORM_KEEP = re.compile("UC-\\d{3}(?:-(?:BR|AC|SC)\\d+)?|(?:CON|ACT)-\\d+|\\b(?:B
118
119
 
119
120
  def form_changes(old, new):
120
121
  """Những thứ mang nghĩa bị thêm / mất khi đổi old → new. Rỗng = chỉ đổi hình thức."""
121
- a, b = sorted(FORM_KEEP.findall(old)), sorted(FORM_KEEP.findall(new))
122
+ # "xem mục 5" → "mục 6" là sửa chỗ trỏ tới tiêu đề, không phải đổi số nghiệp vụ.
123
+ ref = lambda s: re.sub(r"\b([Mm]ục)\s+\d+", r"\1 #", s)
124
+ a, b = sorted(FORM_KEEP.findall(ref(old))), sorted(FORM_KEEP.findall(ref(new)))
122
125
  lost = [x for x in a if a.count(x) > b.count(x)]
123
126
  added = [x for x in b if b.count(x) > a.count(x)]
124
127
  return sorted(set(lost)), sorted(set(added))
@@ -618,6 +621,61 @@ def cmd_decide(path):
618
621
  print("đã ghi %d quyết định vào %s" % (len(ops), path))
619
622
 
620
623
 
624
+ def git_show(rev_path):
625
+ import subprocess
626
+ r = subprocess.run(["git", "show", rev_path], capture_output=True)
627
+ return r.stdout.decode("utf-8", "replace") if r.returncode == 0 else None
628
+
629
+
630
+ def last_approved(path):
631
+ """(commit, nội dung) của bản gần nhất trong git mà file đang `status: approved`. None nếu không có."""
632
+ import subprocess
633
+ r = subprocess.run(["git", "log", "--format=%H", "--", path], capture_output=True)
634
+ if r.returncode != 0:
635
+ return None
636
+ rel = "./" + os.path.relpath(path).replace("\\", "/")
637
+ for h in r.stdout.decode().split():
638
+ text = git_show("%s:%s" % (h, rel))
639
+ if text and re.search(r"^status:\s*approved", text, re.M):
640
+ return h, text
641
+ return None
642
+
643
+
644
+ def cmd_diff(path):
645
+ """Phần PRD đã đổi so với bản đã duyệt gần nhất trong git — phạm vi chính xác cho refine / review sau change."""
646
+ import difflib
647
+ base = last_approved(path)
648
+ if not base:
649
+ fail("không tìm thấy bản `approved` nào của %s trong git (chưa commit bản đã duyệt?). "
650
+ "Dùng các dòng Lịch sử thay đổi làm phạm vi." % path)
651
+ h, old = base
652
+ new = read(path)
653
+ old_l, new_l = old.split("\n"), new.split("\n")
654
+ # Mỗi UC chỉ gồm khối của nó: từ tiêu đề ### UC-… tới tiêu đề ## / ### kế tiếp.
655
+ spans = []
656
+ for i, l in enumerate(new_l):
657
+ m = re.match(r"^###\s+(?:~~)?(UC-\d{3})", l)
658
+ if m:
659
+ end = next((j for j in range(i + 1, len(new_l)) if re.match(r"^#{2,3}\s", new_l[j])), len(new_l))
660
+ spans.append((i, end, m.group(1)))
661
+
662
+ def uc_at(i):
663
+ return next((u for s, e, u in spans if s <= i < e), None)
664
+ touched, body = [], []
665
+ sm = difflib.SequenceMatcher(None, old_l, new_l, autojunk=False)
666
+ for tag, i1, i2, j1, j2 in sm.get_opcodes():
667
+ if tag == "equal":
668
+ continue
669
+ for j in range(j1, max(j2, j1 + 1)):
670
+ u = uc_at(min(j, len(new_l) - 1))
671
+ if u and u not in touched:
672
+ touched.append(u)
673
+ body += ["@@ dòng %d" % (j1 + 1)] + ["- " + l for l in old_l[i1:i2]] + ["+ " + l for l in new_l[j1:j2]]
674
+ print("# So với bản đã duyệt v%s (commit %s) · UC bị chạm: %s (%d UC)"
675
+ % (prd_version(old), h[:7], ", ".join(touched) or "không có (chỉ Tổng quan / User flow / Màn hình / Lịch sử)", len(touched)))
676
+ print("\n".join(body) if body else "(không có gì đổi)")
677
+
678
+
621
679
  PRD_SIZE_WARN = 60 * 1024
622
680
 
623
681
 
@@ -1181,7 +1239,7 @@ def main(argv):
1181
1239
  print("ok — Python %s" % sys.version.split()[0])
1182
1240
  return
1183
1241
  if len(argv) < 2 or argv[0] not in ("create", "edit", "set", "pending", "confirm", "next-uc", "coverage", "section", "upgrade", "flowcheck", "item", "uc", "finding", "decide",
1184
- "where", "context", "next-sc", "scenario", "sc", "bddcheck"):
1242
+ "where", "context", "next-sc", "scenario", "sc", "bddcheck", "diff"):
1185
1243
  fail(__doc__.split("Cách dùng")[1], 2)
1186
1244
  cmd, path, rest = argv[0], argv[1], argv[2:]
1187
1245
  if cmd == "create":
@@ -1222,6 +1280,8 @@ def main(argv):
1222
1280
  cmd_next_uc(path)
1223
1281
  elif cmd == "where":
1224
1282
  cmd_where(path, rest)
1283
+ elif cmd == "diff":
1284
+ cmd_diff(path)
1225
1285
  elif cmd == "context":
1226
1286
  cmd_context(path, rest)
1227
1287
  elif cmd == "next-sc":