wal: flush 20260812T1805
This commit is contained in:
@ -15,9 +15,11 @@ chain:
|
||||
[x] B2 ensemble 3-lane: FAIL-11 + FIXES-12 + FAIL-11 → 2 BLOCKER tiền-đề v1 (bỏ-type=3 ·
|
||||
pendingMe-không-tồn-tại) + W5 sai khuôn kép (KHKK cấm approver-delete; anchor code chết
|
||||
S175) + Q1-Q5. SPEC v2 land + b2-review-synthesis.md. Root-cause A+B HELD 3/3.
|
||||
[!] B3 /fable-real ×2 style: fe + be → spec-list style đồng nhất. wf: wf_f71dc3e3-3ce ·
|
||||
2 lane tier=fable · artifact sub-style-{fe,be}-b3.md
|
||||
[ ] B4 /fable-clone ×2 style FE/BE — review lại 2 spec style
|
||||
[x] B3 style ×2 fable: FE 25-SF+10-bẫy+13-C (#53, đĩa trọn) · BE 20-SB+7-BS+18-C (sạch).
|
||||
2 FLAG xử vào spec v2.1: BS-3 policy-khoá-oan → W5(d) đổi owner-check-handler (khớp
|
||||
chốt (82) W8-hoãn) · SF-17 ContractDetailContent identical-chưa-khai-MIRROR → W5(iv)
|
||||
[!] B4 /fable-clone ×2 style — 4 lane opus (anchor+khuôn ⟂ ô-đo+Goodhart ×2 vai).
|
||||
wf: wf_6b7f00c2-c26 · artifact sub-style-review-b4-{fe1,fe2,be1,be2}.md
|
||||
[ ] B5 /fable-real reviewer — tổng hợp cuối → plan wave cụ thể "SOLTUON-12-08-2026"
|
||||
[ ] B6 hmw fan-out Opus 5 MAX execute toàn plan (per-wave gate + cicd 2 app + prod verify)
|
||||
[!] TỒN CŨ — CỬA NGOÀI-GIỜ (anh gật giờ): dọn workspace runner + dispatch verify CI ·
|
||||
|
||||
@ -83,3 +83,17 @@ HEAD lúc mở run: ae1dcf4a · HMW-mode ON · lead Opus 5 MAX (default)
|
||||
label `WF be-style b3-speclist s190` (SPEC-LIST STYLE BE — guard-order · exception-map ·
|
||||
comment T26 · changelog idiom · policy per-action · test style 680→680+N).
|
||||
Artifact: sub-style-{fe,be}-b3.md.
|
||||
- **B3 KẾT QUẢ** (73 tool-use · 342K · ~13'): FE #53-return-rỗng nhưng artifact TRỌN
|
||||
(25 SF + 10 bẫy-đã-đo + 13 ô C — đặc sản: SF-16 window.confirm-không-Dialog · bẫy-1
|
||||
KhkkWorkflowPanel xóa = xác-chết-còn-máy · SF-17 ContractDetailContent identical 403/403
|
||||
dòng nhưng CHƯA khai MIRROR header) · BE return sạch (20 SB + 7 BS + 18 C — 2 shape
|
||||
controller-authz song song CÓ CHỦ ĐÍCH · enum-trap 98/99>5 · Reason 3-tầng 2000-char).
|
||||
**Lead xử 2 FLAG vào spec v2.1:** BS-3 (policy-403-trước-owner-check + ngược chốt (82))
|
||||
→ W5(d) đổi thành owner-check-handler, policy dời W8 · SF-17 → W5(iv) thêm dòng tự-khai
|
||||
MIRROR. BS-1 (docs/CLAUDE.md §7 "comment English" stale vs 100% deployed comment Việt)
|
||||
→ NEXT-em bookend, ngoài scope wave.
|
||||
- **B4 phóng** `wf_6b7f00c2-c26` — /fable-clone ×2 vai style, 4 lane opus:
|
||||
`WF fe-style b4-anchor s190` · `WF fe-style b4-odo s190` (chạy thử C-1..13 baseline +
|
||||
2 biến thể grep/rg) · `WF be-style b4-anchor s190` (soi cả SB-3 policy thành thừa sau
|
||||
v2.1?) · `WF be-style b4-odo s190` (T-matrix hợp nhất ca test + đo CanDelete LocalDB).
|
||||
Artifact: sub-style-review-b4-{fe1,fe2,be1,be2}.md.
|
||||
|
||||
@ -73,10 +73,18 @@ Tổng: **W4/W1/W2/W3 FE-only · W5 BE nhỏ + FE (0 migration)**. DEPLOY CHỜ
|
||||
dải còn lại (F-5 lens-3 — HĐ đã gen mã KHÔNG BAO GIỜ xoá được);
|
||||
(c) ghi **`ContractChangelog`** (Action=Delete + PhaseAtChange + Reason) — 🔴 thứ tự **`Remove()`
|
||||
TRƯỚC, `Add(changelog)` SAU** (bẫy T26: đảo là cascade DETACH ⇒ 0 row im lặng);
|
||||
(d) `[Authorize(Policy = "Contracts.Delete")]` cho DELETE endpoint (`ContractsController.cs:78-83`
|
||||
hiện `[Authorize]` trần — #82);
|
||||
(d) 🔄 **v2.1 theo BS-3 (be-style B3):** KHÔNG gắn `[Authorize(Policy="Contracts.Delete")]` đợt
|
||||
này — policy-403 chạy TRƯỚC owner-check, tiền lệ PE S155 đo 11/13 role `CanDelete=0` ⇒ gắn =
|
||||
khoá oan Drafter mất nút xoá nháp đang có, và đi NGƯỢC chốt (82) của anh ("phân quyền tất cả
|
||||
trước, phân quyền sau" — W8 HOÃN). Chốt an toàn = **owner-check trong handler** (mục (a) — đủ
|
||||
diệt lỗ "user thường xoá HĐ người khác" F-4 lens-3, trả 403 qua `ForbiddenException`); policy
|
||||
per-action DELETE ghi vào danh mục W8 chờ anh "ổn rồi". Nấc-0 null-guard `DrafterUserId` là
|
||||
`Guid?` — khuôn `PeSoftDeleteFeatures.cs:56-58` (BS-6);
|
||||
(e) xác nhận sẵn: `Remove` = SOFT-delete qua `AuditingInterceptor.cs:54-62` (H-6 — wave chỉ assert,
|
||||
đừng đo lại).
|
||||
**STYLE-GATE cho B6:** worker MỌI wave phải theo 2 spec-list style đã land cùng folder —
|
||||
`sub-style-fe-b3.md` (SF-1..25 + checklist C-1..C-13) + `sub-style-be-b3.md` (SB-1..20 + 18 ô C)
|
||||
— lệch luật nào phải kèm trích lời anh cho phép, không có = FLAG khi gate.
|
||||
FE: nút "Xóa phiếu" ở **thanh nút đáy `ContractDetailContent`** (port khuôn
|
||||
`KhkkDetailContent.tsx:1077-1096` — KHÔNG lấy `KhkkWorkflowPanel` làm khuôn, phần xoá trong đó là
|
||||
CODE CHẾT S175), hiện khi phase ∈ allow-list + user là Drafter/Admin; mirror ×2 app SHA-pair.
|
||||
@ -114,9 +122,12 @@ nào phụ thuộc layout mới; W5 cuối — BE + test-before).
|
||||
Drafter xoá được (như cũ) · **ChoDuyet(10) CẤM kể cả Drafter** (khuôn KHKK — NGƯỢC v1) ·
|
||||
**DaPhatHanh(9) CẤM mọi đường** · user thường xoá HĐ người khác ⇒ **403** (sửa lỗ cũ — khai rõ) ·
|
||||
V1 nháp (2) vẫn xoá được (chống vá quá tay); ✅(ii) changelog: xoá xong đọc-ngược DB ra **đúng 1
|
||||
dòng** Action=Delete (0 dòng = dẫm T26); ✅(iii) `grep -rn "Authorize(Policy" src/Backend/SolutionErp.Api/Controllers/ContractsController.cs`
|
||||
có dòng cho DELETE; ✅(iv) nút Xóa: render đúng điều kiện (phase+vai), `ContractDetailContent` mirror
|
||||
SHA ×2 app; ✅(v) build 2 app PASS.
|
||||
dòng** Action=Delete (0 dòng = dẫm T26); ✅(iii) 🔄 v2.1: owner-check trong handler — test
|
||||
stranger-403 (khuôn `ContractSigningPlanCrudTests` stranger-case) + KHÔNG thêm
|
||||
`[Authorize(Policy` mới vào DELETE (`grep` DELETE-action giữ nguyên shape — policy = W8);
|
||||
✅(iv) nút Xóa: render đúng điều kiện (phase+vai), `ContractDetailContent` mirror
|
||||
SHA ×2 app **+ thêm dòng tự-khai MIRROR ở header cùng bytes 2 bên (SF-17 — file identical
|
||||
nhưng chưa khai)**; ✅(v) build 2 app PASS.
|
||||
|
||||
## ④ Nấc đo local (BẮT BUỘC — deploy CHỜ, ô hành-vi KHÔNG tick bằng suy luận)
|
||||
|
||||
|
||||
@ -5,16 +5,106 @@
|
||||
|
||||
## Khuôn nguồn BE
|
||||
|
||||
(đang đọc — append dần)
|
||||
Tất cả đều là bản **ĐÃ DEPLOY trên `origin/main`** (luật S176). W5 đụng 3 file production + 1-2 file test; mỗi luật SB-n dưới neo vào ≥1 khuôn ở đây.
|
||||
|
||||
| # | Nguồn (file:line) | Cho luật gì |
|
||||
|---|---|---|
|
||||
| K1 | `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs:1740-1807` — `DeleteContractSigningPlanCommand{,Validator,Handler}` | **Khuôn HÀNH-VI chính của W5** (lời anh "tương tự như KHKK"): guard-order, allow-list phase, ForbiddenException owner-check, changelog Remove-trước-Add, comment T26 nén |
|
||||
| K2 | `src/Backend/SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs:14-166` | Comment đầu-file "Vì sao có file này" + XML-doc `Reason` (:34-36) + T26 bản ĐẦY-ĐỦ (:127-144) + tách-đường policy (:25-29) |
|
||||
| K3 | `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs:885-902` — `DeleteContractCommandHandler` HIỆN TẠI | Nền W5 sửa: 0 owner-check, chỉ `Phase >= DangInKy` chặn, 0 changelog, 0 `ICurrentUser` |
|
||||
| K4 | `ContractFeatures.cs:330-356` (khối IDOR + `[W7 S161]`) | Khuôn tag-đợt trong comment + lý-do-đặt-chỗ |
|
||||
| K5 | `src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:11-14` (class `[Authorize]` TRẦN) + `:29-32` (`ListDeleted` per-action `Contracts.Read`) + `:78-83` (DELETE hiện TRẦN) | Shape authz của CHÍNH controller này: class-trần + per-action policy trên endpoint nhạy cảm |
|
||||
| K6 | `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs:153-156` | Khuôn DELETE endpoint: `[HttpDelete("{id:guid}")]` + `[Authorize(Policy = "KeHoachKyKet.Delete")]` + `[FromQuery] string? reason = null` |
|
||||
| K7 | `src/Backend/SolutionErp.Domain/Contracts/ContractChangelog.cs:13-44` | Field changelog + `ChangelogEntityType.Contract=1` + `ChangelogAction.Delete=3` (sẵn, 0-mig) |
|
||||
| K8 | `src/Backend/SolutionErp.Api/Middleware/GlobalExceptionMiddleware.cs:32-40` | Map exception→HTTP: Validation→400 · NotFound→404 · Unauthorized→401 · Forbidden→403 · Conflict→409; `detail = ex.Message` (:52) ⇒ message = chữ user đọc |
|
||||
| K9 | `src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs:14` (`Contracts` const) + `:251` (∈ `All`) + `:278` (`Actions = ["Read","Create","Update","Delete"]`) + `src/Backend/SolutionErp.Api/Program.cs:86-87` (factory `{menu}.{action}`) | `"Contracts.Delete"` được SINH SẴN by-construction — 0 seed mới, 0 key mới |
|
||||
| K10 | `src/Backend/SolutionErp.Domain/Identity/AppRoles.cs:5-6` (`Admin`/`Drafter`) + `src/Backend/SolutionErp.Application/Common/Interfaces/ICurrentUser.cs:3-10` (`Guid? UserId · Roles · FullName · Email · IsAuthenticated`) | Vật liệu owner-check |
|
||||
| K11 | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs:26-34` (2 helper reflection) + `:121-144` (`PolicyName_IsActuallyRegisterable`) + `:150-153` ("2 shape KHÁC nhau — KHÔNG ép về cùng 1 kiểu") | Khuôn test authz reflection (Pattern 10) |
|
||||
| K12 | `tests/.../Application/PeDeleteByApproverTests.cs:32-41` (vì-sao fixture riêng) + `:55-104` (`SoftDeleteFixture` = IdentityFixture + `AuditingInterceptor`) + `:774` (`T26_Delete_WritesChangelogWithDeleteAction_AndCarriesReasonWhenProvided`) | Fixture wire interceptor — chống "assert rỗng nghĩa" |
|
||||
| K13 | `tests/.../Application/ContractSigningPlanCrudTests.cs:469-523` (`DeleteContractSigningPlan_SoftDeletes_KeepsChangelog_ThenCreateAgainSucceeds` — đo 3-trong-1) + `:525-539` (`DeleteContractSigningPlan_GuardedByOwnerAndPhase`, stranger→Forbidden) | Khuôn test delete GẦN W5 NHẤT (chính họ KHKK) |
|
||||
| K14 | Migration `20260423030733_AddContractDetailsAndChangelog.cs:28` — `ContextNote nvarchar(2000)` | Neo `MaximumLength(2000)` cho validator (validator MATCH EF — khuôn K1 `:1748-1750`) |
|
||||
| K15 | 4/4 csproj `src/Backend/*/*.csproj`: `net10.0` + `<Nullable>enable</Nullable>` + `<ImplicitUsings>enable</ImplicitUsings>` | Nullable strict là cấu-hình THẬT |
|
||||
| K16 | `docs/STATUS.md:6` — khuôn pin test-count khai DELTA: `test **668→680**` | Chỗ ghi 680→680+N |
|
||||
| K17 | Phép đo CẤM (đo 2026-08-12): `grep "catch ("` trong `Api/Controllers` = **0 hit** · `Console.WriteLine` trong `src/Backend` = **0 hit** · `FromSqlRaw\|ExecuteSqlRaw\|SqlQueryRaw` trong `Application` = **0 hit** | Luật CẤM §(g) là hiện-trạng đo được, không phải khẩu hiệu |
|
||||
|
||||
## SPEC-LIST STYLE BE (SB-1..)
|
||||
|
||||
(append sau khi đọc khuôn)
|
||||
> Worker viết W5 phải thoả TỪNG SB — mỗi SB có neo khuôn deployed + cách đo. "Khuôn" = copy HÌNH, không copy LỜI KHAI (câu khai sự-thật phải tự đúng trong ngữ-cảnh Contract).
|
||||
|
||||
### (a) Khuôn handler + guard-order
|
||||
|
||||
- **SB-1 — Bộ 3 mảnh cùng file, cùng khối:** `record DeleteContractCommand(Guid Id, string? Reason = null) : IRequest` → `class DeleteContractCommandValidator : AbstractValidator<>` → `class DeleteContractCommandHandler(...) : IRequestHandler<>` nằm LIỀN NHAU trong `ContractFeatures.cs` dưới banner sẵn có `// ========== DELETE (soft) ==========` (:885). KHÔNG tách file mới, KHÔNG tạo folder Commands/. Khuôn: K1 `:1741-1756` (KHKK delete đủ 3 mảnh liền nhau). Hiện Delete THIẾU validator — anh em cùng nghĩa (K1) CÓ ⇒ phải thêm.
|
||||
- **SB-2 — Handler = primary-constructor `(IApplicationDbContext db, ICurrentUser currentUser)`:** khuôn K1 `:1754-1756` + K2 `:50-52`. KHÔNG inject DbContext cụ thể, KHÔNG service mới. `db.ContractChangelogs` sẵn trên interface (`IApplicationDbContext.cs:40`).
|
||||
- **SB-3 — Guard-order chuẩn = NotFound → Forbidden → Conflict** (đúng thứ tự dòng trong K1): (1) load `?? throw new NotFoundException("Contract", request.Id)` (:1760-1761, giữ nguyên dạng đang có ở K3 `:893-894`) → (2) owner-check `var isOwner = entity.DrafterUserId == currentUser.UserId; var isAdmin = currentUser.Roles.Contains(AppRoles.Admin); if (!isOwner && !isAdmin) throw new ForbiddenException(...)` (:1763-1766; `Contract.DrafterUserId` có thật — `Contract.cs:16`) → (3) phase allow-list `throw new ConflictException(...)` (:1778-1783). Ngoại lệ CÓ TÊN: chỉ được đảo Conflict lên trước Forbidden khi guard-người PHỤ THUỘC dữ-kiện guard-trạng-thái (khuôn K2 `:63-107` — phải biết phase/step mới tính được "đúng lượt"), và phải comment lý-do. W5 KHÔNG rơi ca này ⇒ theo K1.
|
||||
- **SB-4 — Allow-list phase viết TƯỜNG MINH một guard:** ngữ nghĩa W5 = cho `{Phase < DangInKy}` ∪ `{TraLai(98), TuChoi(99)}`, chặn hết phần còn lại (bao DaPhatHanh(9) + ChoDuyet(10)). Khuôn cú pháp = pattern-matching `is not (... or ...)` (K1 `:1778-1780`); range+set trộn được nhưng phải 1 guard DUY NHẤT + comment liệt kê đủ trạng-thái cho-phép. 🔴 KHÔNG giữ nguyên `>= DangInKy` rồi vá thêm if thứ hai — 2 if rời là mầm drift.
|
||||
|
||||
### (b) Exception nào cho ca nào
|
||||
|
||||
- **SB-5 — Phân loại theo K8:** sai NGƯỜI (không phải Drafter-của-phiếu/Admin) = `ForbiddenException` → 403; sai TRẠNG-THÁI (phase ngoài allow-list) = `ConflictException` → 409; không thấy = `NotFoundException("Contract", request.Id)` → 404 (ctor: tên-entity-English + key — K3 `:894`). CẤM `InvalidOperationException`/`UnauthorizedAccessException`/return-null. Cả 3 loại từ `SolutionErp.Application.Common.Exceptions` (using sẵn — K2 `:4`).
|
||||
- **SB-6 — Message = tiếng Việt trọn câu, có đường-thoát:** `detail = ex.Message` đi thẳng ra user (K8 `:52`). Khuôn giọng: Forbidden nói AI được phép ("Chỉ người soạn kế hoạch hoặc Admin được xoá phiếu này." — K1 `:1766`); Conflict nói TRẠNG-THÁI nào được + lối ra ("Chỉ xoá được ... ở trạng thái Nháp, Trả lại hoặc Từ chối. ... đang duyệt phải Trả lại/Từ chối trước." — K1 `:1782-1783`). Viết lại cho danh-từ "hợp đồng"/các phase HĐ (DaPhatHanh cần câu riêng ý "HĐ đã phát hành không bao giờ xoá được" — hình theo K1, lời theo ngữ-cảnh Contract).
|
||||
|
||||
### (c) Comment tiếng Việt tự-khai (khuôn T26 + khuôn khai-lý-do)
|
||||
|
||||
- **SB-7 — Comment allow-list phải tự khai 3 vế** (khuôn K1 `:1768-1777`): (i) VÌ SAO có allow-list này (trích/neo lời anh: *"tương tự như KHKK"* — YC-029 Đ4, spec v2 §②-W5); (ii) vì sao ChoDuyet VẪN CẤM (approver-delete = Q4 chờ anh, KHÔNG mở đợt này); (iii) nhắc 2-tầng hiển-thị⟂API (gotcha #82) vì FE cũng hiện nút theo phase+vai. Tag đợt theo khuôn K4: mở đầu comment bằng `[W5 S190]` (mirror `[W7 S161]` `:353`, `[S159-đợt5]` K5 `:29`).
|
||||
- **SB-8 — Owner-check phải KHAI là "sửa lỗ sẵn có":** spec v2 W5(a) bắt khai tường minh — comment nói rõ bản cũ (K3) KHÔNG kiểm chủ-sở-hữu, đây là siết mới đồng-khuôn KHKK, không phải hành-vi từ trước. Khuôn "khai vá lệch/lỗ cũ" = K1 `:1772-1775`.
|
||||
- **SB-9 — Comment T26 chép HÌNH bản NÉN của K1 `:1787-1790`** (4 dòng: thứ-tự Remove-trước-Add + cơ chế `CascadeDeleteTiming.Immediate` DETACH child `Added` ⇒ SaveChanges ghi 0 row im lặng + vì sao thêm-sau thì sống), kèm chữ "bug T26 S155" để grep được. KHÔNG chép nguyên văn 18 dòng bản ĐẦY-ĐỦ của K2 `:127-144` (bản đó dành cho file có thêm rào interceptor — Contract không cần lặp).
|
||||
|
||||
### (d) Changelog — field nào, thứ tự nào
|
||||
|
||||
- **SB-10 — Remove TRƯỚC, Add changelog SAU = LUẬT:** `db.Contracts.Remove(entity);` đứng TRƯỚC `db.ContractChangelogs.Add(new ContractChangelog {...});`, rồi `await db.SaveChangesAsync(ct);` MỘT lần (K1 `:1791-1805`, K2 `:145-164`). Đo: số-dòng của `.Remove(` < số-dòng của `.Add(`.
|
||||
- **SB-11 — Bộ field ghi delete** (map K1 `:1793-1803` sang K7): `ContractId = entity.Id` · `EntityType = ChangelogEntityType.Contract` (header ⇒ `EntityId` BỎ TRỐNG — K7 `:19` "null nếu là Contract header") · `Action = ChangelogAction.Delete` · `PhaseAtChange = entity.Phase` (phase TẠI LÚC xoá — ghi TRƯỚC khi có gì đổi) · `UserId = currentUser.UserId` · `UserName = currentUser.FullName ?? currentUser.Email` · `Summary = $"Xoá hợp đồng {entity.MaHopDong ?? "(chưa có mã)"}"` (placeholder khuôn K1 `:1801` — đa số HĐ xoá được chưa gen mã nên vế `??` là BẮT BUỘC) · `ContextNote = reason`. KHÔNG dùng `FieldChangesJson` cho delete.
|
||||
- **SB-12 — `Reason` đi trọn đường 3 tầng theo khuôn:** controller `[FromQuery] string? reason = null` (K6 `:155-156`) → command `string? Reason = null` + XML-doc khai "DeletedBy/DeletedAt do AuditingInterceptor set chỉ trả lời ai+lúc nào, LÝ DO nằm ở changelog" (K2 `:34-36`, K1 `:1740`) → validator `RuleFor(x => x.Reason).MaximumLength(2000)` + comment "MaxLength MATCH EF" neo config (K1 `:1748-1750`; số 2000 = K14) → handler `var reason = string.IsNullOrWhiteSpace(request.Reason) ? null : request.Reason.Trim();` (K1 `:1785`).
|
||||
|
||||
### (e) Policy string + chỗ đặt attribute
|
||||
|
||||
- **SB-13 — `[Authorize(Policy = "Contracts.Delete")]` đặt NGAY TRÊN action Delete, class-level GIỮ `[Authorize]` trần:** shape của CHÍNH controller này (K5: class trần `:13` + per-action `ListDeleted` `:31-32` kèm câu "chặt hơn [Authorize] trần của class — không phá gì"). 🔴 CẤM "sửa cho nhất quán" sang class-level policy kiểu KHKK — 2 shape tồn tại CÓ CHỦ ĐÍCH (K11 `:150-153`); class-level policy trên ContractsController sẽ khoá GET list của mọi role thiếu Contracts.Read (gotcha #44 silent-403). Tên policy hợp lệ by-construction: K9 (KHÔNG sinh key mới, KHÔNG sửa `DbInitializer`, KHÔNG sửa `MenuKeys`).
|
||||
- **SB-14 — Comment trên endpoint khai lý-do policy** theo khuôn K5 `:29-30` (2 dòng: đợt + vì-sao): nêu `#82` ([Authorize] trần = lỗ), và policy này là tầng-2 ĐỘC LẬP với owner-check trong handler (defence-in-depth như KHKK: K6 có policy VÀ K1 có owner-check).
|
||||
|
||||
### (f) Test style (test-before)
|
||||
|
||||
- **SB-15 — Chỗ đặt + fixture:** file mới `tests/SolutionErp.Infrastructure.Tests/Application/ContractDeleteGuardTests.cs` (hoặc tên `Contract...Tests` cùng dạng; namespace `SolutionErp.Infrastructure.Tests.Application`). 🔴 Fixture PHẢI wire `AuditingInterceptor` — chép hình `SoftDeleteFixture` K12 `:55-104` (private sealed trong file test, KHÔNG sửa `IdentityFixture` dùng chung — lý-do K12 `:41`: 500+ test đang dựa hành-vi cũ). Thiếu interceptor thì `Remove()` = xoá CỨNG ⇒ assert soft-delete/changelog "đúng ở cả hai thế giới" = đo số không (K12 `:32-41`).
|
||||
- **SB-16 — Đặt tên + cấu trúc:** xUnit `[Fact]` + FluentAssertions; tên method English PascalCase `Hành-động_Kịch-bản_Kỳ-vọng` (khuôn K13 `:482` `DeleteContractSigningPlan_SoftDeletes_KeepsChangelog_ThenCreateAgainSucceeds`, `:531` `..._GuardedByOwnerAndPhase`); banner section `// ==== (n) ... ====` tiếng Việt khai VẬT ĐO + VÌ SAO (K13 `:469-480`); because-message tiếng Việt trong mọi `.Should()`; assert exception khuôn `(await act.Should().ThrowAsync<ConflictException>("...")).WithMessage("*mảnh-tiếng-Việt*")` (K13 `:459-462`). AAA ngầm bằng dòng trống + comment `// ---------- TRƯỚC / ACT / SAU ----------` khi test dài (K12-file `:330-357`).
|
||||
- **SB-17 — Bộ ca BẮT BUỘC** (spec v2 ✅W5-i, đủ 6): TraLai Drafter xoá được · TuChoi Drafter xoá được · DangSoanThao Drafter xoá được (giữ cũ) · ChoDuyet CẤM kể cả Drafter (409) · DaPhatHanh CẤM mọi đường kể cả Admin (409) · người-lạ xoá HĐ người khác → 403 (khuôn stranger `new TestCurrentUser(Guid.NewGuid(), ...)` K13 `:538`) · +V1-nháp phase 2 vẫn xoá được (chống vá quá tay). Mỗi ca xoá-thành-công phải assert 3-trong-1 theo K13 `:490-509`: (a) khuất query thường + `IgnoreQueryFilters()` row còn `IsDeleted=true` + `DeletedBy` đúng; (c) changelog đọc-ngược DB `Single(l => l.Action == ChangelogAction.Delete)` + `ContextNote`/`PhaseAtChange`/`UserId` đúng — 0 dòng = dẫm T26.
|
||||
- **SB-18 — Reflection-authz:** THÊM `[Fact]` vào `AuthorizePolicyRegressionTests.cs` (file sẵn — không tạo file mới): `GetActionAuthorize(typeof(ContractsController), nameof(ContractsController.Delete))` → `attr.Should().NotBeNull(...)` + `attr!.Policy.Should().Be("Contracts.Delete", "...")` (khuôn K11 `:73-80`) + giữ nếp kiểm registerable: policy split 2 phần ∈ `MenuKeys.All`/`MenuKeys.Actions` (K11 `:121-144`). Because-message nêu #82 + ngày/đợt.
|
||||
- **SB-19 — Test-before + pin số:** viết test TRƯỚC (RED trên code hiện tại — ca ChoDuyet-CẤM và 403-người-lạ phải ĐỎ với K3) rồi code W5 (GREEN) — khuôn W3 KHKK "test-before RED→GREEN" (`docs/STATUS.md:11`). Chạy `dotnet test SolutionErp.slnx`; số pin 680→680+N khai DELTA vào row session `docs/STATUS.md` (khuôn `:6` "test **668→680**") — ĐÚNG 1 row, không neo số tuyệt đối chỗ khác (B1).
|
||||
|
||||
### (g) CẤM (đã đo hiện-trạng = 0, giữ 0 — K17)
|
||||
|
||||
- **SB-20:** KHÔNG `try-catch` trong controller (exception bay lên K8) · KHÔNG `Console.WriteLine` (Serilog qua `ILogger<T>`; và theo khuôn K1/K2: handler delete KHÔNG log gì — middleware đã log Warning cho 4xx `K8:42-45`, đừng thêm log thừa) · KHÔNG raw SQL trong Application (`FromSqlRaw`/`ExecuteSqlRaw`) · Nullable strict đang bật 4/4 csproj (K15) — không `!` nén warning, `Guid?` null-check tường minh trước dùng (khuôn K2 `:56-58`) · KHÔNG migration, KHÔNG sửa schema (`ChangelogAction.Delete` sẵn — K7 `:42`), KHÔNG đổi `SolutionErp.slnx` (0 file .cs mới ngoài test — file test nằm trong project test sẵn có).
|
||||
|
||||
## Bẫy style BE repo này
|
||||
|
||||
(append)
|
||||
- **BS-1 — Doc nói "comment English", bản deploy nói ngược:** `docs/CLAUDE.md` §7 ghi *"Code + comment: English"*, nhưng TOÀN BỘ khuôn deploy (K1/K2/K4/K5 + cả 2 file test K11-K13) là comment tiếng Việt tự-khai. Luật S176: deployed = CHUẨN ⇒ **theo tiếng Việt**. FLAG cho lead: dòng doc đó stale — KHÔNG tự sửa doc trong wave này (ngoài scope W5).
|
||||
- **BS-2 — Enum value 98/99 nằm NGOÀI dải tuyến-tính:** `TraLai=98`, `TuChoi=99` LỚN HƠN `DaPhatHanh=9` về số ⇒ guard cũ `Phase >= DangInKy` (K3 `:896`) đang chặn cả 2 phase mà W5 phải MỞ. Mọi so-sánh range mới phải xử lý 98/99 bằng vế TẬP tường minh, không bằng range (đây chính là lý-do SB-4 đòi 1 guard duy nhất).
|
||||
- **BS-3 — Policy gắn vào đường xoá Drafter đang dùng = nguy-cơ khoá oan (tiền-lệ PE S155):** K2 `:25-29` khai đo DB Dev 11/13 role `CanDelete=0` ⇒ PE phải TÁCH ĐƯỜNG để Drafter không mất quyền xoá nháp chính mình. W5 chọn khuôn KHKK 1-đường (policy + owner-check trên CÙNG endpoint) — đúng khuôn, NHƯNG hệ-quả data: role Drafter thiếu `CanDelete` trên key `Contracts` sẽ 403 TRƯỚC KHI owner-check chạy ⇒ Drafter có thể MẤT nút xoá nháp đang có (hành-vi hiện tại: DELETE trần, ai đăng nhập cũng gọi được). Đây là quyết-định data/quyền, KHÔNG phải style — **FLAG lead ĐO ma-trận quyền prod trước khi ship** + khai trong PR; FE W5 gate nút theo phase+vai (spec) nhưng tầng API gate theo policy ⇒ đúng lớp 2-tầng #82, có thể "bấm được mà 403".
|
||||
- **BS-4 — Assert-rỗng-nghĩa trong test:** fixture thiếu `AuditingInterceptor` thì soft-delete thành hard-delete, mọi assert "khuất khỏi list" xanh oan (K12 `:32-41`). Neo chống-vacuous khuôn K12 `:352-355`: sau ACT phải assert `IgnoreQueryFilters().Single(...).IsDeleted == true` TRƯỚC khi assert các hệ-quả.
|
||||
- **BS-5 — Bê khuôn nhưng đừng bê lời khai:** message/comment của K1 nói "kế hoạch", "phiếu PE bị khoá 409" — chép nguyên sang Contract là khai SAI sự-thật trong ngữ-cảnh mới (bài S176). Mọi câu tiếng Việt phải viết lại quanh danh-từ "hợp đồng" + phase HĐ.
|
||||
- **BS-6 — `DrafterUserId` là `Guid?`:** (`Contract.cs:16`) — HĐ cũ/di-sản có thể null ⇒ owner-check `entity.DrafterUserId == currentUser.UserId` với cả 2 vế nullable: null == null-user là ca không được lọt. Khuôn K2 `:56-58` chặn từ cửa: `if (!currentUser.IsAuthenticated || currentUser.UserId is null) throw new UnauthorizedException();` — thêm nấc-0 này là ĐÚNG khuôn khi handler cần định-danh (K1 bỏ qua vì controller đã `[Authorize]`, nhưng so-sánh nullable của K1 `:1763` chỉ an toàn nhờ `[Authorize]`; W5 nên theo K2 cho kín — 1 dòng, đo được).
|
||||
- **BS-7 — `SaveChangesAsync` đúng MỘT lần:** khuôn K1/K2 kết bằng 1 lời gọi duy nhất — 2 lần Save (một cho Remove một cho changelog) đẻ nhu-cầu transaction + vỡ atomicity (K2 `:143-144` khai đã cân nhắc và BÁC).
|
||||
|
||||
## Checklist style ĐO ĐƯỢC cho worker
|
||||
|
||||
(append)
|
||||
Chạy từ repo root; PASS = đúng vế kỳ-vọng. (Số dòng dưới là của bản HIỆN TẠI — sau khi worker sửa, đo bằng pattern, không bằng số dòng.)
|
||||
|
||||
| # | Lệnh đo | Kỳ vọng |
|
||||
|---|---|---|
|
||||
| C1 | `grep -n "record DeleteContractCommand" src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` | 1 hit, có `string? Reason = null` |
|
||||
| C2 | `grep -n "DeleteContractCommandValidator" src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` | ≥1 hit + gần đó `MaximumLength(2000)` |
|
||||
| C3 | `grep -n "ICurrentUser currentUser" src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` | có hit trong `DeleteContractCommandHandler` |
|
||||
| C4 | Trong thân handler delete: dòng `NotFoundException` < dòng `ForbiddenException` < dòng `ConflictException` | thứ tự SB-3 |
|
||||
| C5 | `grep -n "AppRoles.Admin" ...ContractFeatures.cs` (vùng delete) + `grep -n "DrafterUserId == currentUser.UserId" ...` | owner-check 2 vế có mặt |
|
||||
| C6 | Dòng `db.Contracts.Remove(` < dòng `db.ContractChangelogs.Add(` ; `grep -c "SaveChangesAsync"` trong handler delete = 1 | SB-10 · BS-7 |
|
||||
| C7 | `grep -n "ChangelogAction.Delete" ...ContractFeatures.cs` + `grep -n "PhaseAtChange" ...` + `grep -n '(chưa có mã)' ...` | bộ field SB-11 |
|
||||
| C8 | `grep -n "T26" ...ContractFeatures.cs` | ≥1 (comment SB-9) |
|
||||
| C9 | `grep -n '\[W5' ...ContractFeatures.cs` | ≥1 (tag đợt SB-7) |
|
||||
| C10 | `grep -n 'Authorize(Policy = "Contracts.Delete")' src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` | =1, đứng giữa `[HttpDelete("{id:guid}")]` và `public async Task<IActionResult> Delete` |
|
||||
| C11 | Class-level controller: `[Authorize]` TRẦN vẫn nguyên tại `:13` (không Policy) | SB-13 |
|
||||
| C12 | `grep -n "FromQuery] string? reason" ...ContractsController.cs` | 1 hit trên Delete (SB-12) |
|
||||
| C13 | `grep -rn "nameof(ContractsController.Delete)" tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs` | ≥1 (SB-18) |
|
||||
| C14 | File test mới: `grep -n "AuditingInterceptor" tests/.../Application/Contract*Tests.cs` + `grep -n "IgnoreQueryFilters" ...` | ≥1 mỗi cái (SB-15, BS-4) |
|
||||
| C15 | Đếm ca trong file test mới: TraLai · TuChoi · DangSoanThao · ChoDuyet-409 · DaPhatHanh-409 · stranger-403 · V1-phase2 | 7 ca SB-17 đủ mặt |
|
||||
| C16 | `dotnet test SolutionErp.slnx` | PASS 680+N; row session `docs/STATUS.md` khai `680→680+N` |
|
||||
| C17 | `grep -rn "catch (" src/Backend/SolutionErp.Api/Controllers/` · `grep -rn "Console.WriteLine" src/Backend/` · `grep -rnE "FromSqlRaw\|ExecuteSqlRaw" src/Backend/SolutionErp.Application/` | 0 · 0 · 0 (giữ nguyên K17) |
|
||||
| C18 | `git diff --stat` của wave | KHÔNG đụng `Persistence/**`, KHÔNG file `.cs` mới ngoài `tests/`, KHÔNG sửa `MenuKeys.cs`/`DbInitializer.cs`/`SolutionErp.slnx` |
|
||||
|
||||
== HẾT ==
|
||||
|
||||
@ -5,16 +5,106 @@
|
||||
|
||||
## Khuôn nguồn (file nào là chuẩn cho việc gì)
|
||||
|
||||
(đang ghi)
|
||||
Tất cả đều ĐÃ DEPLOY prod (origin/main, ngoài paths-ignore) — luật S176: đây là CHUẨN, lệch không có lời anh = FLAG.
|
||||
|
||||
| Việc trong wave | Khuôn ĐÃ CHẠY | Anchor |
|
||||
|---|---|---|
|
||||
| Host 3-panel + trục danh-tính URL (W2) | `KhkkListPage.tsx` | `fe-user/src/pages/khkk/KhkkListPage.tsx:156,171` — `h-[calc(100vh-4rem)]` + `lg:grid-cols-[400px_1fr_360px]`; trục URL đọc-only :62-68; override 3-trạng-thái :71-77; reset-in-render :81-88 |
|
||||
| Panel danh sách + Select filter + banner thùng rác (W2 panel-3, Q2 Select) | `KhkkListPanel.tsx` | `fe-user/src/components/khkk/KhkkListPanel.tsx:326-360` — `<aside>` filter-block trên + thân cuộn; Select trạng-thái :352-359; banner đỏ deleted :347-350; ô Search icon-trong-Input :330-338 |
|
||||
| Thanh nút đáy + nút Xóa + confirm (W5-FE) | `KhkkDetailContent.tsx` | `fe-user/src/components/khkk/KhkkDetailContent.tsx:1076-1137` — `sticky bottom-0 z-10 … border-t bg-white/95 backdrop-blur`; nút đỏ `bg-red-600` + `Trash2` + `window.confirm` :1087-1097 (KHÔNG Dialog); rào hiển-thị mirror BE-guard :1103-1119 |
|
||||
| Mutation + invalidate + toast (W5-FE, W2) | `KhkkDetailContent.tsx` | :193-197 helper `invalidate()` (2 queryKey + `onChanged?.()`); `deletePlan` :316-320 (`onSuccess: toast + onDeleted?.()`; `onError: toast.error(getErrorMessage(e))`) |
|
||||
| Đang-duyệt = ĐỔI ENDPOINT cùng trang (W3) | `PurchaseEvaluationsListPage.tsx` | `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:74-111` — queryKey gói MỌI trục lọc :75; nhánh `pendingMe` gọi `/inbox` rồi TỰ GÓI về shape Paged :91-99; nhánh deleted đứng TRƯỚC :80 |
|
||||
| Cây toàn trình + opt-in callback (W2) | `PipelineTreePanel.tsx` + `usePipelineStages.ts` | `fe-user/src/components/pipeline/PipelineTreePanel.tsx:93-135` — props opt-in mỗi cái 1 khối comment lý-do + câu "không truyền ⇒ giữ nguyên"; `Pick<BuildStagesOptions,…>` :111. `fe-user/src/hooks/usePipelineStages.ts:228-230,244` — ternary `opts.onOpenX ? host-bắt : navigate-như-cũ`; GĐ3 navigate hiện tại :377 |
|
||||
| Danh-tính menu ≠ bộ lọc (W1) | `fe-user/src/components/Layout.tsx` | :16-29 `TYPE_CODE_TO_INT`/`INT_TO_TYPE_CODE` (khuôn const-map); :141-154 nhánh `Ct_*`; :176-186 khuôn PE `view=approved` = KHOÁ ĐỊNH DANH trang bỏ qua; :350 `TRANSIENT_QUERY_KEYS`; :354-375 `queryMatches` pinned-by-target; accordion auto-expand :470-485 |
|
||||
| Nhánh resolvePath Designer (W4) | `fe-admin/src/components/Layout.tsx` | :175-189 khối `awV2Match` — nhánh `KhkkN[1-8]` :188 là khuôn TRỰC TIẾP cho nhánh `findContractWfByTypeCode(code)`; :178 nhánh `Contract` = nhánh chết KHÔNG đụng |
|
||||
| Empty-state panel giữa + hộp dashed panel phải | `KhkkListPage.tsx` | :195-203 `EmptyState icon/title/description` (import `@/components/EmptyState`); :257-269 `<aside>` hộp `border-dashed` khi chưa chọn |
|
||||
| 404 thật ⟂ lỗi hệ thống (mọi detail-fetch mới) | `KhkkListPage.tsx` | :236-246 — `detail.isError && !isNotFound(detail.error)` ⇒ câu "Hệ thống đang chậm…" + nút thử lại; còn lại "Không tìm thấy phiếu." (YC-028(5) S189, 5 site ×2 app) |
|
||||
|
||||
## SPEC-LIST STYLE FE (đánh số SF-1..)
|
||||
|
||||
(đang ghi)
|
||||
Mỗi luật = ĐO ĐƯỢC (grep/SHA/eslint). "Khuôn" = file ĐÃ DEPLOY prod. Worker lệch luật nào phải kèm trích lời anh cho phép — không có = FLAG khi review.
|
||||
|
||||
## Bẫy style đã thấy trong repo
|
||||
### A. Comment tự-khai (đặc sản repo — máy ESLint MÙ trục này)
|
||||
|
||||
(đang ghi)
|
||||
- **SF-1 — Khối sửa lớn/file mới PHẢI mang comment header tự-khai:** `[W<n> S190 · YC-029]` + vai trò + quyết-định owner (nguyên văn khi có) + trade-off/mitigation ĐÁNH SỐ. Khuôn: `fe-user/src/components/khkk/KhkkListPanel.tsx:1-24` (M-1/M-2/B1/R-11 — mỗi mitigation nêu VÌ SAO + hệ-quả nếu bỏ), `KhkkListPage.tsx:9-26` (QĐ-1/M-4/R-5), `fe-admin/src/lib/contractWorkflowFamily.ts:6-18` (khối "VÌ SAO LÀ FILE RIÊNG"). Đo: `grep -n "W[1-5]\b.*S190\|YC-029" <file-wave>` ≥1 per file sửa.
|
||||
- **SF-2 — Sửa hành-vi ⇒ LẬT comment cũ cùng diff; gỡ UI/code ⇒ để bia-mộ `🧊`:** khuôn bia-mộ `KhkkListPage.tsx:161-163` (chip đã gỡ + lời anh "Bỏ mấy dòng này đi"), `KhkkWorkflowPanel.tsx:117-118` (nút rời đi đâu). W1(d) chính là trả nợ luật này: comment `fe-user/Layout.tsx:141-142` "HĐ chung 1 bộ workflow V2" nay SAI (@S188 7 bộ per-loại) — sửa nhánh WfView mà không lật câu này = đẻ stale-comment mới. Đo: `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = 0 sau W1.
|
||||
- **SF-3 — Comment nói WHY, trỏ anchor chéo dạng `file.tsx:line`; vùng hay trượt số dòng thì ghi "grep chuỗi X — đừng neo số dòng"** (khuôn `fe-user/Layout.tsx:202-207`). KHÔNG comment kể-lại code.
|
||||
|
||||
### B. Đặt tên
|
||||
|
||||
- **SF-4 — Callback opt-in mở leaf GĐ3 tại-chỗ đặt tên `onOpenContract`** — đúng họ `onOpenKhkk`/`onOpenPe`, FLAT trong `BuildStagesOptions` (KHÔNG bịa tầng lồng — F-4 lens-1). Site tiêu thụ = ternary y khuôn: `usePipelineStages.ts:228-230` (`opts.onOpenPe ? () => opts.onOpenPe!(id) : () => navigate(…)`) và `:244`; chỗ cắm GĐ3 = `:377`. `PipelineTreePanel.tsx:111` nới `Pick<BuildStagesOptions, …>` thêm `'onOpenContract'`. JSDoc trên prop theo khuôn `:109-110`: nêu đối-xứng họ + câu "host không truyền ⇒ giữ nguyên".
|
||||
- **SF-5 — Const map module-level = SCREAMING_SNAKE `Record<>` + comment khai NGUỒN mirror**, đặt sau imports/trước component: khuôn `TYPE_CODE_TO_INT`/`INT_TO_TYPE_CODE` (`fe-user/Layout.tsx:16-29`, reverse qua `Object.fromEntries`), `PE_CODE_TO_INT :48` (comment "mirror BE … enum"), `FILTER_TO_PHASE` (`KhkkListPage.tsx:48-55`). Map W1(b) ContractType→mã WF: tên theo họ `INT_TO_*` (vd `CT_INT_TO_WF_CODE: Record<number, string>`), 7 entry chép TỪNG KÝ TỰ từ `ContractApprovalWorkflowFamily.cs:33-42` (cẩn thận token gạch-nối `HD-NT-NCC`/`HD-NT-DV`), comment tự khai "bản-sao-thứ-3 của họ (F-7)" + trỏ 2 bản kia (`.cs` + `fe-admin/src/lib/contractWorkflowFamily.ts`). 🔴 CẤM "vá" `contractWorkflowFamily.ts` thành mirror ×2 app — header `:16-18` cấm tường minh (fe-admin ONLY có chủ đích).
|
||||
- **SF-6 — Named export `export function X()`** cho page/panel/component (repo-wide; CLAUDE.md §FE: không default trừ `App`). Đo: `grep -c "export default" <file-wave>` = 0.
|
||||
|
||||
### C. TanStack Query
|
||||
|
||||
- **SF-7 — Mọi biến queryFn đọc PHẢI vào queryKey — THEO SHAPE KEY ĐANG CÓ của file:** dạng positional `['my-contracts', typeFilter, phaseFilter, showDeleted]` (`MyContractsPage.tsx:47`) ⟂ dạng object-gói `['pe-list', {…}]` (`PurchaseEvaluationsListPage.tsx:75`), `['khkk-list', {…}]` (`KhkkListPanel.tsx:247`). Thêm trục (`pendingMe`, `search` server-side) = nối vào shape sẵn, KHÔNG đổi shape key cũ và KHÔNG tái dùng key của trang khác (bài S159 — `usePipelineStages.ts:88-89`: "đổi shape cache dùng chung sẽ vỡ trang khác").
|
||||
- **SF-8 — Ngoại-lệ CÓ KHUÔN: lọc lát-cắt client trên payload đã tải trọn ⇒ KHÔNG thêm trục vào queryKey + PHẢI comment lý-do** — khuôn `WorkflowMatrixViewPage.tsx:88-95` (`group` không vào key: "thêm vào sẽ đẻ 8 lần gọi mạng cho cùng một payload"); khớp bằng `code`, không bằng tên/thứ tự (:92-95). W1(b) `ct` đi ĐÚNG khuôn này.
|
||||
- **SF-9 — Detail-query gate `enabled: !!selectedId`** (`KhkkListPage.tsx:92-96`, `MyContractsPage.tsx:67-71`); query theo quyền gate `enabled: canX` (`usePipelineStages.ts:102`).
|
||||
- **SF-10 — Mutation khuôn 3-mảnh:** `onSuccess: () => { toast.success('<câu VN>'); <invalidate()/callback host> }` + `onError: (e) => toast.error(getErrorMessage(e))` — 10/10 mutation `KhkkDetailContent.tsx:243-320` cùng dạng. Nút Xóa W5 trong `ContractDetailContent`: bộ invalidate lấy của CHÍNH FILE (khuôn `transition` `:57-61`: `['contract', c.id]` + `['my-contracts']` + `['inbox']`); đường về host qua callback optional `onDeleted?.()` khuôn `deletePlan :316-320` + JSDoc khuôn `KhkkWorkflowPanel.tsx:96-97`; host clear chọn khuôn `KhkkListPage.tsx:248` (`onDeleted={() => setId(null)}`).
|
||||
- **SF-11 — "Đang duyệt" = ĐỔI ENDPOINT trong cùng queryFn (W3):** khuôn `PurchaseEvaluationsListPage.tsx:80-99` — nhánh `deleted` đứng TRƯỚC `pendingMe` (2 view loại trừ, comment :77-79); nhánh `pendingMe` gọi endpoint khác (`/inbox`) rồi TỰ GÓI kết quả về shape chung `{ items, total, page, pageSize }` (:98) để phần render không tách nhánh.
|
||||
|
||||
### D. shadcn/ui idiom
|
||||
|
||||
- **SF-12 — Empty-state 3 khuôn, dùng đúng chỗ:** panel giữa = `<EmptyState icon={} title description>` (`KhkkListPage.tsx:195-203`, `MyContractsPage.tsx:220-226`); panel phải chưa-chọn = hộp `border-dashed … text-center text-sm text-slate-500` (`KhkkListPage.tsx:257-269`); danh-sách rỗng phải TÁCH "rỗng-vì-lọc ⟂ rỗng-vì-chưa-có" thành 2 câu + đếm phần đang bị lọc khuất (`WorkflowMatrixViewPage.tsx:152-166`).
|
||||
- **SF-13 — Badge/chip: full class string literal (Pattern 14 Tailwind JIT — `types/khkk.ts:57`), tra qua `Record<PhaseValue, string>` (`KHKK_PHASE_BADGE`); thang chữ 13/11/10, KHÔNG đẻ nấc mới** (bài `KhkkListPanel.tsx:155-158` gom nấc thứ 4 `text-[9px]` về 3 nấc). Chip "đang lọc theo X" đặt trong `PageHeader` title khuôn `WorkflowMatrixViewPage.tsx:110-115` + action "Xem tất cả" `:127-136`.
|
||||
- **SF-14 — Select filter:** shadcn `Select`, option đầu `value=""` = "Tất cả …", `aria-label`, onChange ép kiểu qua `Number()` cast về `XValue` (`KhkkListPanel.tsx:352-359`). Q2 Select loại HĐ trên đầu panel cây W2: đặt trong khối filter TRÊN CÙNG panel trái (khuôn vị-trí `KhkkListPanel.tsx:326-338`: `<aside>` → khối `border-b p-3` chứa Search rồi Select).
|
||||
- **SF-15 — 3-panel host:** `flex h-[calc(100vh-4rem)] flex-col` + header `shrink-0 border-b px-6 py-3` + `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]` (`KhkkListPage.tsx:156-171`). W2 đưa MyContractsPage 320px→400px cột trái (acceptance W2-i grep `grid-cols-[400px`). Hành-vi theo bề rộng: `window.matchMedia('(min-width: 1024px)')` — desktop chọn-tại-chỗ, `<lg` navigate trang đầy đủ (`KhkkListPage.tsx:127-131`, `MyContractsPage.tsx:97-103`).
|
||||
- **SF-16 — Thanh nút đáy + nút Xóa (W5-FE):** port `KhkkDetailContent.tsx:1076-1137` — `{!<điều-kiện> && …}` bao ngoài; `<div className="sticky bottom-0 z-10 flex flex-wrap items-center gap-3 border-t border-slate-200 bg-white/95 px-5 py-3 backdrop-blur">`; nút Xóa = `<button type="button">` đỏ `bg-red-600 hover:bg-red-700 disabled:opacity-50` + `<Trash2 className="h-3.5 w-3.5" />` + nhãn pending "Đang xóa…"/"Xóa phiếu". **Confirm = `window.confirm(...)` với message chứa mã phiếu + đích đến** (":1090 — `Xóa phiếu ${mã}? Phiếu chuyển sang mục "Đã xóa".`") — 🔴 KHÔNG dựng Dialog confirm mới (Dialog xóa trong `KhkkWorkflowPanel.tsx:742-756` là đường CŨ đã gỡ trigger @S175 — xem Bẫy-1). Điều kiện hiện nút tính trước thành biến tên rõ (phase ∈ allow-list ∧ Drafter/Admin); khi có nút bị chặn-mềm thì nói lý-do qua `title` (khuôn rào hiển-thị `:1103-1125`).
|
||||
|
||||
### E. Kỷ luật mirror-pair
|
||||
|
||||
- **SF-17 — File đụng trong wave thuộc cặp SHA256-identical:** `hooks/usePipelineStages.ts` (baseline `1b9b5c61…`) · `components/pipeline/PipelineTreePanel.tsx` (`4ffc96be…`) · `components/contracts/ContractDetailContent.tsx` (`cd81508f…` — ĐO 2026-08-12: identical 403/403 dòng nhưng **CHƯA tự khai MIRROR ở header**; W5 sửa file này thì THÊM dòng tự-khai theo khuôn `KhkkDetailContent.tsx:2`, cùng bytes 2 bên) · `PipelineStageFolders.tsx` (`a2d486c5…`) nếu bị đụng. Luật thi-công: **một diff viết một lần, apply 2 bản trong CÙNG commit**; đo sau wave bằng `sha256sum` từng cặp (checklist C-3).
|
||||
- **SF-18 — KHÔNG-CẶP CÓ CHỦ ĐÍCH, CẤM mirror bừa:** `Layout.tsx` mỗi app một bản (fe-admin không nới `Khkk_*`/`Pe_*_WfView` — khai tại `fe-admin/Layout.tsx:138-142,185-187`; fe-user không có `AwV2_*`); `MyContractsPage.tsx` + `WorkflowMatrixViewPage.tsx` fe-user-only; `contractWorkflowFamily.ts` fe-admin-only (`:16-18`). Dương-giả nếu bắt 2 Layout giống nhau.
|
||||
- **SF-19 — Lệch-có-chủ-đích phải KHAI tại chỗ bằng khuôn "🔴 CỐ Ý KHÔNG … (lý-do + ngày/người chốt)"** — mẫu `fe-user/Layout.tsx:209-212`, `fe-admin/Layout.tsx:37-39`, `KhkkListPage.tsx:253-256` (QĐ-2). Câu tự-khai trong file mirror phải Y HỆT BYTES 2 bên — chấp nhận đọc "lệch ngữ" ở 1 app (vd cả 2 bản cùng ghi "với fe-admin counterpart"), CẤM sửa câu 1 bên cho xuôi tai (phá SHA).
|
||||
|
||||
### F. TS idiom repo
|
||||
|
||||
- **SF-20 — Enum-cấm (gotcha #3):** const object `as const` + `export type XValue = (typeof X)[keyof typeof X]` + nhãn `Record<XValue, string>` (`types/khkk.ts:40-55`). Không `enum`. Giá trị bắt đầu từ 1 (0 falsy JS — `:39`).
|
||||
- **SF-21 — URL param số phải đỡ rác:** miền hữu hạn ⇒ clamp regex (`/^[1-8]$/` `KhkkListPage.tsx:67`, `WorkflowMatrixViewPage.tsx:84-86`; W1(b) ct dùng `/^[1-7]$/`) hoặc NaN-guard `Number.isFinite` (`MyContractsPage.tsx:30-36`, nguồn `HardCopiesPage.tsx:67-68` — "dùng lại nguyên, không đẻ khuôn thứ 2"). Rác ⇒ `null` GIỮ HÀNH-VI CŨ, không throw/redirect. Param axios optional: `?? undefined` để axios bỏ hẳn key (`MyContractsPage.tsx:59-60`).
|
||||
|
||||
### G. Ghim per-wave
|
||||
|
||||
- **SF-22 (W4):** nhánh mới đặt TRONG `awV2Match`, SAU nhánh `KhkkN[1-8]` (`fe-admin/Layout.tsx:188`), viết y khuôn khối `:181-188`: comment `[tag]` + "chỗ thứ 4 Pattern 16-bis" + gotcha #50 + return template route. KHÔNG đụng nhánh `code === 'Contract'` `:178` (nhánh chết 0 seed — spec H-2/H-3). Import named từ `@/lib/contractWorkflowFamily`.
|
||||
- **SF-23 (W1):** `&view=approved` = KHOÁ ĐỊNH DANH trang bỏ qua — bê nguyên NGHĨA từ khuôn PE + giữ khối comment giải-thích (`fe-user/Layout.tsx:180-185`: vì sao `phase` không làm danh-tính được). 🔴 KHÔNG thêm `ct`/`view`/`pendingMe` vào `TRANSIENT_QUERY_KEYS` (`:350`) — chúng là danh-tính, vào TRANSIENT là leaf sáng nhầm (bug S155). Trang KHÔNG ghi ngược `ct`/`type` vào URL (M-4 `KhkkListPage.tsx:17-21` — chỉ `id` được ghi).
|
||||
- **SF-24 (W2):** mọi prop/opts mới = opt-in default-TẮT + JSDoc có câu "Không truyền ⇒ giữ nguyên hành vi cũ" — 5 mẫu liền nhau `PipelineTreePanel.tsx:106-135` (`stage2Options`/`bodyMaxHClass`/`hideHeader`/`hideSearch`/`approvedPeOnly`). Control-âm fe-admin (không truyền `onOpenContract` ⇒ navigate cũ) là acceptance W2-v.
|
||||
- **SF-25 (W2/W3):** đổi tầng lọc (client→server) phải GỠ vế client cũ + comment lật khai "vế X ĐÃ GỠ — BE lọc rồi; giữ lại = lọc 2 lần + che lỗi server" (khuôn W7 S187 `MyContractsPage.tsx:54-59,74-76` + cập deps useMemo `:87-88`). Chỗ CHỌN client-side (W3 lọc `type` trên inbox) phải comment khai lệch + tag carry (`[carry:w7b-inbox-param]` — spec W3).
|
||||
|
||||
## Bẫy style đã thấy trong repo (đã ĐO, không phải giả-định)
|
||||
|
||||
1. **Nút Xóa trong `KhkkWorkflowPanel` = xác-chết-còn-máy:** mutation `removePlan :231-237` + Dialog `:742-756` còn nguyên nhưng trigger ĐÃ GỠ @S175 (bia-mộ `:117-118`; `:438-440` khai "đã rời → thanh nút đáy"). v1 spec từng trỏ nhầm vào đây làm bằng-chứng — port khuôn xóa từ file này là bê CODE CHẾT + sai cả kiểu confirm (Dialog vs `window.confirm`).
|
||||
2. **Mirror-header "lệch ngữ" là ĐÚNG:** `fe-user/pages/khkk/KhkkListPage.tsx:5` ghi "MIRROR … với fe-admin counterpart" và bản fe-admin cũng ghi Y HỆT (vì phải cùng bytes). Ai "sửa câu cho đúng" một bên là phá SHA-pair ngay lập tức.
|
||||
3. **Nhánh `code === 'Contract'`** (`fe-admin/Layout.tsx:178`) trông như chỗ nối tự nhiên cho W4 — nhưng là nhánh CHẾT (0 seed, spec H-2/H-3 HELD). Nối vào = sửa nhánh không ai gọi, leaf mới vẫn DROP.
|
||||
4. **Nhét key lọc mới vào `TRANSIENT_QUERY_KEYS` cho "đỡ mất highlight"** — làm leaf ghim key đó sáng nhầm; `queryMatches` đã có cơ chế pinned-by-target (`fe-user/Layout.tsx:358-369`, bài S155) — đừng tự chế thêm.
|
||||
5. **Đổi shape/tái dùng queryKey trang khác** — `usePipelineStages.ts:88-89` khai thẳng bài S159: index cây phải đặt key MỚI (`pipeline-khkk-index`) thay vì mượn `khkk-list`.
|
||||
6. **Tailwind JIT nội-suy class** (`bg-${color}-100`) = class không được generate — Pattern 14: full string literal trong `Record` (`types/khkk.ts:57`).
|
||||
7. **`Number(rawParam)` không guard** ⇒ axios gửi `?type=NaN`, BE bind `ContractType?` → 400 (`MyContractsPage.tsx:30-33`). Cả 2 khuôn đỡ đã có sẵn (SF-21) — đừng đẻ khuôn thứ 3.
|
||||
8. **resolvePath thiếu nhánh = leaf DROP IM LẶNG** (gotcha #50, Pattern 16-bis "chỗ thứ 4") — không đỏ build, không console error; chỉ thấy khi đếm leaf sidebar (acceptance W4-i đếm 17).
|
||||
9. **Bê lời khai chưa chứng minh khi bê khuôn:** bài S176 banner PE "không còn tính vào lũy kế ngân sách" — KHKK cố ý CẮT vế đó vì `grep 'lũy kế'` trong KHKK = 0 hit (`KhkkListPanel.tsx:345-346`). W2/W5 bê banner/câu chữ nào thì kiểm từng MỆNH-ĐỀ SỰ-THẬT trong câu có đúng ở module HĐ không.
|
||||
10. **Comment stale sau khi đổi hành-vi** — chính W1(d) sinh ra để trả nợ 1 ca (`Layout.tsx:141-142`); đừng vừa trả xong lại vay ca mới (SF-2).
|
||||
|
||||
## Checklist style ĐO ĐƯỢC cho worker
|
||||
|
||||
(đang ghi)
|
||||
Chạy từ repo-root sau MỖI wave (C-1/C-2 bắt buộc trước khi khai xong wave):
|
||||
|
||||
- **C-1 máy trước:** `cd fe-user && npx eslint . ` PASS · W4/W2 thêm `cd fe-admin && npx eslint .` PASS (máy = recommended + react-hooks + react-refresh — `fe-user/eslint.config.js:12-17`; các trục A/B/D/E máy MÙ ⇒ mới cần list này).
|
||||
- **C-2 build:** `npm run build` ở app bị đụng PASS (acceptance mỗi wave đã pin).
|
||||
- **C-3 mirror (sau W2 + W5):**
|
||||
```bash
|
||||
for f in hooks/usePipelineStages.ts components/pipeline/PipelineTreePanel.tsx \
|
||||
components/pipeline/PipelineStageFolders.tsx components/contracts/ContractDetailContent.tsx; do
|
||||
a=$(sha256sum "fe-admin/src/$f"|cut -c1-12); u=$(sha256sum "fe-user/src/$f"|cut -c1-12)
|
||||
[ "$a" = "$u" ] && echo "OK $f" || echo "LECH $f $a≠$u"; done # 4/4 OK
|
||||
```
|
||||
- **C-4 named-export:** `grep -c "export default" <mỗi file wave>` = 0.
|
||||
- **C-5 comment tự-khai:** `grep -ln "YC-029\|S190" <danh sách file sửa>` = đủ 100% file sửa.
|
||||
- **C-6 stale-comment W1(d):** `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = 0.
|
||||
- **C-7 danh-tính không rơi vào TRANSIENT:** dòng `TRANSIENT_QUERY_KEYS` (`grep -n "TRANSIENT_QUERY_KEYS = " fe-user/src/components/Layout.tsx`) KHÔNG chứa `ct`/`view`/`pendingMe`.
|
||||
- **C-8 nhánh ct W1:** `grep -nF "get('ct')" fe-user/src/components/Layout.tsx` ≥1 · `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` ≥1 (spec W1-iv).
|
||||
- **C-9 vòng 7 mã W1(b):** `grep -c "'HD-" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` = 7 và có đủ `HD-TP|HD-GK|HD-NCC|HD-DV|HD-MB|HD-NT-NCC|HD-NT-DV` khớp từng ký tự `ContractApprovalWorkflowFamily.cs:33-42`.
|
||||
- **C-10 opt-in control-âm W2:** `grep -rn "onOpenContract" fe-admin/src/pages` = 0 (fe-admin không host nào truyền — định nghĩa trong hook/panel mirror thì CÓ, site truyền thì KHÔNG).
|
||||
- **C-11 hoán-đổi W2:** `grep -F "grid-cols-[400px" fe-user/src/pages/contracts/MyContractsPage.tsx` ≥1 (spec W2-i).
|
||||
- **C-12 confirm xóa W5:** `grep -c "window.confirm" fe-user/src/components/contracts/ContractDetailContent.tsx` ≥1 và KHÔNG thêm `<Dialog` mới cho việc xóa (số `<Dialog` giữ 1 — transition dialog `:372`).
|
||||
- **C-13 W4 sau-nhánh-đúng:** `grep -n "findContractWfByTypeCode" fe-admin/src/components/Layout.tsx` ≥1 và nằm sau dòng match `KhkkN[1-8]` (`:188` baseline).
|
||||
|
||||
— HẾT (sub-style-fe-b3 · propose-only, lead verify trước khi dùng làm gate) —
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
# B4 — BE-STYLE review lane-1/2 (ANCHOR + ĐÚNG-KHUÔN)
|
||||
|
||||
> Vai: `back-end-reviewer-style` · ensemble B4 `/fable-clone` · lens 1 = anchor sống? + luật có khớp KHUÔN THẬT trên đĩa?
|
||||
> Đối tượng review: `sub-style-be-b3.md` (20 SB + 18 ô đo C) — trước khi nó thành STYLE-GATE cho worker B6.
|
||||
> Trạng thái: ĐANG CHẠY (append liên tục, chống #53).
|
||||
|
||||
## Verdict
|
||||
|
||||
_(điền cuối)_
|
||||
|
||||
## Findings (R-1..)
|
||||
|
||||
_(append dần)_
|
||||
|
||||
## Luật phải sửa/gỡ
|
||||
|
||||
_(append dần)_
|
||||
@ -0,0 +1,20 @@
|
||||
# B4 — BE-STYLE review lane 2/2 (lens: Ô-ĐO + TEST-MATRIX + BS-3)
|
||||
|
||||
> agent: `back-end-reviewer-style` (ensemble B4, lane 2/2) · run `2026-08-12-S190-soltuon-dieu-chinh-hd`
|
||||
> input: `sub-style-be-b3.md` (20 SB + 18 ô C + 7 ca test) · `spec-soltuon-dieu-chinh-hd-12-08-2026.md` §③ W5
|
||||
|
||||
## Verdict
|
||||
|
||||
_(đang chạy)_
|
||||
|
||||
## Findings (R-1..)
|
||||
|
||||
_(đang chạy)_
|
||||
|
||||
## Ô đo phải sửa
|
||||
|
||||
_(đang chạy)_
|
||||
|
||||
## T-matrix hợp nhất
|
||||
|
||||
_(đang chạy)_
|
||||
@ -0,0 +1,16 @@
|
||||
# B4 lane-1/2 — front-end-reviewer-style — lens ANCHOR + ĐÚNG-KHUÔN
|
||||
|
||||
> Review lại `sub-style-fe-b3.md` (25 SF + 13 C) TRƯỚC khi nó thành STYLE-GATE cho worker B6.
|
||||
> Lens: (a) anchor chết/lệch · (b) luật TỰ CHẾ không có trong khuôn · (c) mâu thuẫn spec v2.1 · (d) mơ hồ 2-kiểu-đều-đạt · (e) 10 bẫy sai sự thật.
|
||||
|
||||
## Verdict
|
||||
|
||||
_(đang đo — điền cuối)_
|
||||
|
||||
## Findings (R-1..)
|
||||
|
||||
_(append từng finding ngay khi đo xong)_
|
||||
|
||||
## Luật phải sửa/gỡ
|
||||
|
||||
_(điền cuối)_
|
||||
@ -0,0 +1,16 @@
|
||||
# B4 ensemble — front-end-reviewer-style lane-2/2 (lens Ô-ĐO CHẠY-ĐƯỢC + GOODHART)
|
||||
|
||||
> Soi lại `sub-style-fe-b3.md` §Checklist C-1..C-13 bằng cách **CHẠY THẬT** từng ô đo trên repo baseline (trước wave sửa).
|
||||
> Câu hỏi mỗi ô: (a) baseline đo được bao nhiêu? (b) kỳ-vọng-sau-wave có RĂNG không — ca chết nào vẫn xanh? (c) lệnh có chạy được trên bash Windows không?
|
||||
|
||||
## Verdict
|
||||
|
||||
_(điền cuối)_
|
||||
|
||||
## Findings (R-1..)
|
||||
|
||||
_(điền dần)_
|
||||
|
||||
## Ô đo phải sửa
|
||||
|
||||
_(điền dần)_
|
||||
Reference in New Issue
Block a user