wal: flush 20260812T1749

This commit is contained in:
pqhuy1987
2026-08-12 17:49:23 +07:00
parent db9cf99fdf
commit b2f5d6f9c8
9 changed files with 827 additions and 106 deletions

View File

@ -12,11 +12,11 @@ chain:
A+B · máy per-loại OK (lẫn = 3 lỗ UI) · Thứ-5 đã có ~95% (thiếu 1 nhánh resolvePath
fe-admin #50) · 5 wave. LEAD VERIFY 4/4 claim + SPEC GHI:
runs/2026-08-12-S190-soltuon-dieu-chinh-hd/spec-soltuon-dieu-chinh-hd-12-08-2026.md
[!] B2 /fable-clone reviewer — 3 lane opus (lens correctness · khuôn+scope · acceptance).
wf: wf_d9a14945-190 · artifact sub-review-b2-lens{1,2,3}.md · xong thì lead synthesize
→ sửa spec → B3
[ ] B3 /fable-real ×2 style: front-end-reviewer-style + back-end-reviewer-style → spec-list
style đồng nhất khuôn KHKK/DuyetNCC
[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
[ ] 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)

View File

@ -0,0 +1,42 @@
# B2 synthesis — ensemble reviewer 3-lane (wf_d9a14945-190) → spec v2
> Lead refute+synthesize @S190. Lens-1 FAIL-11 (return sạch) · lens-2 PASS_WITH_FIXES-12 (#53,
> artifact cứu trọn) · lens-3 FAIL-11 (return sạch). 34 finding thô → hội tụ 8 cụm, 0 mâu thuẫn
> giữa lane; 2 BLOCKER được ≥2 lane độc lập cùng bắt bằng 2 phép đo khác nhau = chắc.
## Cụm hội tụ → hành động (đã vào spec v2)
1. **"Bỏ type=3" = tự chế + gây trang trắng** (L1-F1 · L2-F1 · L3-F1): trang CHỈ đọc `?type`
(`WorkflowMatrixViewPage.tsx:57,61` fallback `:1`); "alias-path KHKK" thật ra GIỮ type= trên URL
(`Layout.tsx:216`) — khuôn chứng điều NGƯỢC. → v2 GIỮ `?type=3&ct=N`; delta = trang đọc `ct` +
accordion ưu tiên `ct` + allowlist pathname (diệt luôn `Hdc_*` cross-talk L1-F5).
2. **`pendingMe` không tồn tại BE Contract** (L1-F2 · L2-F2 · L3-F2): 0-hit + control-dương KHKK
4-hit; ASP.NET nuốt param lạ im lặng. Khuôn PE THẬT = đổi-endpoint. → v2 W3: `pendingMe=1` ⇒ FE
gọi `/contracts/inbox` + lọc type client (khai lệch-W7 chấp nhận); acceptance so-số 2 chiều.
3. **W5 sai khuôn KÉP** (L2-F7/F12 · L3-F3/F4/F5): (a) hành-vi phải theo KHKK (lời anh) — KHKK CẤM
approver xoá khi đang duyệt, allow {Nháp, TraLai, TuChoi} Drafter/Admin; (b) bằng-chứng "KHKK có
nút Xóa ở panel duyệt" = CODE CHẾT S175 (nút thật ở thanh đáy detail `KhkkDetailContent:1077-1096`);
(c) thiếu changelog + bẫy T26 Remove→Add; (d) DELETE endpoint `[Authorize]` trần + handler không
kiểm chủ sở hữu = lỗ sẵn (#82) mà v1 sắp NỚI RỘNG; (e) thiếu ca chặn `DaPhatHanh(9)`.
→ v2 W5 viết lại trọn; approver-delete tách thành Q4 CHỜ ANH.
4. **W2 thiếu 3 mitigation khuôn + 2 quyết định của anh** (L2-F3/F4/F5 · L1-F3 · L3-F9/F11): cây
không chứa HĐ tạo-trực-tiếp; `!canPe` ⇒ panel trống (#82 mặt-2); cây không lọc theo loại; đọc-ý
"list thành fallback" = suy diễn đội. → v2: Q1 nhánh-an-toàn HOÁN ĐỔI (giữ list ở panel phải) ·
Q2 Select loại khuôn KHKK · control-âm fe-admin + deep-link + ca HĐ-ngoài-gói.
5. **Acceptance Goodhart** (L1-F4 · L3-F6/F7/F8/F10): ô "0 dòng loại khác" xanh trên trang trắng;
grep thiếu `-F` (rg đọc `?` thành regex — đo thật 2 chiều); 1/7 mã được phủ; 9/20 ô hành-vi không
có chỗ chạy khi deploy CHỜ. → v2: vòng ct 1..7 + control ct-rác + grep -F + mục ④ nấc-đo-local +
DEFERRED-TO-DEPLOY + pin 680→680+N.
6. **Xác nhận DƯƠNG giữ nguyên** (3 lane): root-cause A+B HELD · `view=approved` diệt bug B đúng
khuôn PE · W4 cơ chế khớp trọn (17 leaf, nhánh chèn đúng khối awV2Match) · mirror baseline SHA
sạch 4 cặp · soft-delete xác nhận (AuditingInterceptor) — H-1..H-6 lens-3 ghi lại để wave khỏi đo.
7. **Thứ tự wave đổi**: W4 (lành nhất) → W1 → W2 → W3 (sau W2 vì layout quyết chỗ hiển thị) → W5.
8. **Q1-Q5 đánh số** trình anh (Q1/Q2 nhánh-đã-chọn khai · Q3 cấu-hình-câm 2 cờ anh đang thấy ·
Q4 approver-delete · Q5 QT-HD-V2-001) — thay mục "ngoài wave" không số của v1 (L2-F6: rơi im lặng).
## Bài học lead tự nhận
v1 lead verify 4 claim bằng grep NHƯNG toàn claim "chỗ thiếu/chỗ có" — KHÔNG verify 2 claim
"CƠ-CHẾ khuôn" ("trang tự suy theo path" · "BE param sẵn") vì chúng nghe xuôi theo khuôn. Đúng bài
`feedback_claim_stronger_than_work` + lens-2 gọi tên: "spec viện dẫn khuôn X phải MỞ khuôn X ra đọc".
Ensemble 3-lane trả cổ tức đúng chỗ lead mù.

View File

@ -68,3 +68,18 @@ HEAD lúc mở run: ae1dcf4a · HMW-mode ON · lead Opus 5 MAX (default)
`WF reviewer b2-lens2-khuon s190` (fidelity khuôn + scope + đọc-ý cần hỏi lại anh) ·
`WF reviewer b2-lens3-accept s190` (acceptance chạy-được + Goodhart + W5 khuôn PE).
Artifact: sub-review-b2-lens{1,2,3}.md.
- **B2 KẾT QUẢ** (85 tool-use · 486K tok · ~14'): lens-1 FAIL-11 (sạch) · lens-2
PASS_WITH_FIXES-12 (#53, artifact cứu) · lens-3 FAIL-11 (sạch). Hội tụ 8 cụm — 2 BLOCKER
tiền-đề v1 (bỏ-type=3 gây trang-trắng · pendingMe 0-hit BE) + W5 sai khuôn kép (KHKK cấm
approver-delete + anchor "nút Xóa panel KHKK" = CODE CHẾT S175) + acceptance Goodhart
(grep thiếu -F · ô xanh-trên-ca-chết · 9/20 ô không chỗ chạy) + xác nhận DƯƠNG root-cause
A+B HELD 3/3 + H-1..H-6 khỏi đo lại. → **SPEC v2** land (GIỮ type=3&ct= · W3 đổi-endpoint
inbox · W5 khuôn KHKK thuần + changelog T26 + authz + chặn DaPhatHanh · Q1-Q5 đánh số ·
mục ④ nấc-đo-local + DEFERRED-TO-DEPLOY) + `b2-review-synthesis.md`. Thứ tự wave mới:
**W4 → W1 → W2 → W3 → W5**.
- **B3 phóng** `wf_f71dc3e3-3ce` — /fable-real ×2 vai style (anh chỉ định):
label `WF fe-style b3-speclist s190` (SPEC-LIST STYLE FE — khuôn comment-header tự-khai ·
đặt tên opt-in · TanStack idiom · mirror-pair kỷ luật · thanh-đáy KhkkDetailContent) ·
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.

View File

@ -1,103 +1,148 @@
# SPEC — SOLTUON-12-08-2026 (YC-029: 5 điều chỉnh GĐ3 đồng-khuôn KHKK/Duyệt NCC)
# SPEC v2 — SOLTUON-12-08-2026 (YC-029: 5 điều chỉnh GĐ3 đồng-khuôn KHKK/Duyệt NCC)
> H21 spec-file 3-mục. Engine propose = `sub-invest-b1-fable.md` (Fable-5, 3 chặng B1/B1b/B1c).
> Lead verify @S190: 4/4 claim load-bearing spot-check KHỚP (seed `AwV2_Hd*` by-construction ·
> fe-admin `Layout.tsx:175-189` 0 nhánh `Hd` + control `KhkkN:188` · fe-user `Layout.tsx:151-152`
> đúng nguyên văn · delete-guard so số tại `src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs:896-897`
> — lane ghi thiếu segment path `Contracts/`, đã đính chính). Trạng thái: CHỜ B2 review (fable-clone).
> H21 spec-file 3-mục (+④ nấc đo local). Engine propose = `sub-invest-b1-fable.md` (Fable-5, 3 chặng).
> **v2 @S190 sau B2 ensemble 3-lane reviewer** (lens-1 FAIL-11 · lens-2 PASS_WITH_FIXES-12 · lens-3
> FAIL-11 — artifact `sub-review-b2-lens{1,2,3}.md`): 2 BLOCKER tiền-đề v1 SAI đã gỡ (❶ "bỏ type=3,
> trang tự suy theo path" — trang CHỈ đọc `?type`, bỏ là rơi về Duyệt-NCC/trang-trắng; ❷ "BE param
> pendingMe sẵn" — 0-hit Contract, param lạ bị NUỐT im lặng) + W5 viết lại theo khuôn ĐÚNG (hành-vi =
> KHKK theo lời anh; bằng-chứng "KHKK có nút Xóa ở panel duyệt" v1 trỏ vào CODE CHẾT S175 — nút thật
> ở thanh đáy detail, hướng người-soạn) + Q1-Q4 tách rõ. Root-cause bug A+B: 3/3 lane xác nhận HELD.
## ① Tính-năng / đề-bài
Đồng-khuôn GĐ3 "Duyệt Hợp đồng" với KHKK + Duyệt NCC prod (luật owner S176: đã-deploy = CHUẨN),
theo 5 điều chỉnh anh chốt @S190 sau khi review prod (YC-029, ảnh kết tinh `anh-mo-ta.md`):
- **Đ1** cây toàn trình DỜI sang cột TRÁI trang `my-contracts` làm trục điều hướng (khuôn KHKK:
cây trái 400px, flat-list = fallback trong CÙNG panel, panel-3 dashed/WorkflowHistory).
- **Đ2+Đ3** menu 7 loại HĐ mang đúng danh-tính trục ContractType — hết click-nhảy-lẫn (bug A:
`?type=3` ApplicableType đá accordion; bug B: `phase=9` transient 2-leaf-cùng-sáng), leaf
"Luồng duyệt" slice đúng họ `HD-*` của loại. Máy per-loại ĐÃ XONG @S188 — toàn bộ là UI.
- **Đ4** bộ thao-tác theo hành-vi KHKK: Đang-duyệt ở lại cùng trang (`pendingMe=1`), Đã-duyệt
danh-tính chuẩn (`view=approved`), nút XÓA ở màn duyệt theo cờ per-Cấp `AllowApproverDelete`.
- **Đ5** admin Designer hiện 7 leaf per-loại (menu-row + policy + Designer + route ĐÃ seed @S188
— thiếu đúng 1 nhánh `resolvePath` fe-admin ⇒ drop im lặng #50).
Đồng-khuôn GĐ3 "Duyệt Hợp đồng" với KHKK + Duyệt NCC prod (owner S176: đã-deploy = CHUẨN), theo 5
điều chỉnh YC-029 (ảnh kết tinh `anh-mo-ta.md`):
- **Đ1** cây toàn trình về cột TRÁI trang `my-contracts` (chi tiết = W2, nhánh-đã-chọn Q1: HOÁN ĐỔI).
- **Đ2+Đ3** menu 7 loại HĐ đúng danh-tính ContractType — diệt bug A (accordion nhảy nhóm) + bug B
(2 leaf cùng sáng) + leaf "Luồng duyệt" slice đúng họ `HD-*`. Máy per-loại ĐÃ XONG @S188 — đây là UI.
- **Đ4** bộ thao-tác theo hành-vi KHKK: Đang-duyệt ở lại trang list · Đã-duyệt danh-tính `view=approved`
· đường XOÁ theo đúng khuôn KHKK (Nháp/Trả-lại/Từ-chối, người-soạn/Admin — KHÔNG mở approver-delete
nếu anh chưa duyệt Q4).
- **Đ5** admin Designer hiện 7 leaf per-loại đã seed sẵn (thiếu 1 nhánh resolvePath — ~5 dòng).
Tổng: **FE-only 4 wave + 1 wave BE nhỏ (0 migration)**. Đợt này PROPOSE + IMPLEMENT + gate
per-wave; **DEPLOY CHỜ** (CI runner workspace còn hỏng P0 — vá-gốc = CỬA NGOÀI-GIỜ; push gom).
Tổng: **W4/W1/W2/W3 FE-only · W5 BE nhỏ + FE (0 migration)**. DEPLOY CHỜ (CI runner P0 — cửa ngoài-giờ);
đo hành-vi bằng **④ nấc đo local**, ô không đo được = DEFERRED-TO-DEPLOY, cấm tick.
## ② Cách implement (per điều chỉnh — file:line đã verify)
## ② Cách implement (per wave — đã vá theo B2)
- **Đ1 (W2):** `fe-user/src/pages/contracts/MyContractsPage.tsx:153` grid `[320px_1fr_360px]`
`[400px_1fr_360px]`; panel-1 = `PipelineTreePanel currentStage={3} hideHeader hideSearch` +
flat-list fallback server-side trong CÙNG panel (khuôn `KhkkListPage.tsx:171` +
`KhkkListPanel.tsx:21-24,366-371`); panel-3 = dashed khi chưa chọn / `WorkflowHistoryPanel`
khi chọn (QĐ-2 `KhkkListPage.tsx:252-256` — CẤM cây ở panel-3). Bịt gap:
`usePipelineStages.ts:377` leaf GĐ3 `navigate('/contracts/…')` → thêm opt-in
`stage3Options.onOpenContract` (analog `onOpenKhkk` `:185,:244`) + wire `PipelineTreePanel.tsx:111`
→ chọn vào panel-2 qua `?id=`. Host CHỈ fe-user (fe-admin không có MyContractsPage) nhưng
`usePipelineStages` + `PipelineTreePanel` = cặp MIRROR ×2 app → sửa cả 2 bản SHA-identical.
- **Đ2+Đ3 (W1):** `fe-user/src/components/Layout.tsx:151` WfView → `/contracts/workflow-matrix?ct=N`
(BỎ `type=3` khỏi URL — trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn) ·
`:152` thêm `&view=approved` (khuôn PE `:180-185`, giữ `phase=9` làm lọc thật) · sửa comment
stale `:141-142` · accordion `:470-485` ưu tiên `ct` + loại trừ pathname `/contracts/workflow-matrix`
khỏi nhánh else (phòng thủ 2 lớp) · `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` thêm
nhánh đọc `ct` → lọc `code === 'HD-<token>'` y khuôn nhánh `group` `:84-98` (map ContractType→mã:
const cục bộ 7-entry — fe-user KHÔNG mirror lib fe-admin, `contractWorkflowFamily.ts:16-18`
tự khai fe-admin-ONLY). fe-admin KHÔNG đụng (Ct_* bị ẩn `:248` + cố ý không có trang matrix).
- **Đ4-Đang-duyệt (W3):** `fe-user/Layout.tsx:150` `Ct_*_Pending``/my-contracts?type=N&pendingMe=1`
(khuôn PE `:176`; BE param `pendingMe` sẵn) + `MyContractsPage` đọc `pendingMe` gửi server;
GIỮ `/inbox` làm hộp thư tổng. (`[carry:w7b-inbox-param]` liên quan — KHÔNG gộp vào đợt này.)
- **Đ5 (W4):** `fe-admin/src/components/Layout.tsx` import `findContractWfByTypeCode` từ
`@/lib/contractWorkflowFamily` + thêm sau `:188`:
- **W4 — Admin Designer 7 leaf (fe-admin, ≈5 dòng — wave LÀNH NHẤT, chạy ĐẦU):**
`fe-admin/src/components/Layout.tsx` — import `findContractWfByTypeCode` từ `@/lib/contractWorkflowFamily`
+ thêm **TRONG khối `if (awV2Match)`**, sau nhánh `KhkkN` `:188`:
`if (findContractWfByTypeCode(code)) return '/system/approval-workflows-v2/' + code`.
KHÔNG dùng `?ct=`/`?family=` — khuôn họ này là path-param `:typeCode` y hệt `KhkkN{n}`.
KHÔNG sửa DbInitializer (label đã khớp khuôn).
- **Đ4-nút-Xóa (W5, BE nhỏ 0-mig):** mirror khuôn PE prod `PeSoftDeleteFeatures.cs:110-114`
(3 điều-kiện: ChoDuyet · ĐÚNG-LƯỢT actor · `matchingLevel.AllowApproverDelete`). Việc: nới
delete-guard `Application/Contracts/ContractFeatures.cs:896-897` (đang so SỐ
`Phase >= DangInKy` — chặn nhầm V2 ChoDuyet=10/TraLai=98/TuChoi=99, finding S160) thành
V2-aware + nhánh xóa-bởi-approver đọc cờ (cột `ApprovalWorkflowLevels.AllowApproverDelete`
Mig 68 DÙNG CHUNG V2 — Designer round-trip sẵn cho `HD-*`:
`ApprovalWorkflowV2AdminFeatures.cs:475,883,1288`) + DTO expose cờ (khuôn
`PurchaseEvaluationDtos.cs:138`) + FE `ContractWorkflowPanel.tsx` thêm mục Xóa mirror
`KhkkWorkflowPanel.tsx:12-14,118,233` (panel SHA-mirror ×2 app).
🔴 Lead thêm khi verify: đường xóa hiện tại là `db.Contracts.Remove(entity)` — wave PHẢI
XÁC MINH interceptor soft-delete (AuditableEntity `IsDeleted`) đứng sau Remove hay không
TRƯỚC khi nới guard; màn "Đã xóa" (`/contracts/deleted`) đang đọc soft-delete ⇒ nếu Remove
là hard-delete thật thì nhánh xóa-bởi-approver phải đi đường soft-delete như PE, KHÔNG
tái dùng Remove.
(H-2/H-3 lens-3 HELD: `code` = phần sau `AwV2_` = `HdTP`… khớp `contractWorkflowFamily.ts:69,77-80`;
route `App.tsx:80` + Designer slice sẵn. Nhánh `code === 'Contract'` `:178` = nhánh chết, 0 seed —
KHÔNG đụng.) KHÔNG sửa DbInitializer.
- **W1 — Danh-tính menu + slice per-loại (fe-user):** 🔴 **GIỮ `?type=3&ct=N`** (khuôn KHKK
`Layout.tsx:216` giữ type= trên URL — v1 đòi bỏ là TỰ CHẾ, gây trang trắng).
(a) Accordion `fe-user/Layout.tsx:470-485`: **ưu tiên đọc `ct` khi có** (map `INT_TO_TYPE_CODE[ct]`)
+ **allowlist pathname** (chỉ auto-expand nhóm Ct_ từ `?type=` khi pathname ∈ {`/my-contracts`,
`/contracts/new`, `/inbox`} — diệt luôn cross-talk `Hdc_*` `/hard-copies?type=1..7` mà blacklist v1 sót);
(b) `WorkflowMatrixViewPage.tsx`: thêm nhánh đọc `ct` (clamp `/^[1-7]$/` — khuôn `group` `:82-86`,
`ct` rác ⇒ KHÔNG lọc, giữ hành vi cũ) → lọc `code === 'HD-<token>'` y khuôn nhánh `group` `:84-98`;
map ContractType→mã = **const cục-bộ 7-entry trong fe-user** (⚠️ bản-sao-thứ-3 của họ — F-7 lens-3:
chép ĐÚNG từ `ContractApprovalWorkflowFamily.cs:33-42`, cẩn thận token gạch-nối `HD-NT-NCC`/`HD-NT-DV`;
acceptance vòng 7 mã khoá drift);
(c) `:152` Approved thêm `&view=approved` (khuôn PE `:180-185` — H-4 HELD; trang bỏ qua param lạ);
(d) sửa comment stale `:141-142` ("chung 1 bộ" → 7 bộ per-loại @S188).
- **W2 — Cây toàn trình sang trái = HOÁN ĐỔI 2 khung (fe-user; cặp hook/panel mirror ×2 app):**
Nhánh-đã-chọn **Q1 = HOÁN ĐỔI** (khai bậc-2, anh bác thì đổi): panel-1 TRÁI 400px = CÂY
(`PipelineTreePanel currentStage={3} hideHeader hideSearch`) · panel-2 GIỮA = detail/placeholder
(như cũ) · panel-3 PHẢI 360px = **DANH-SÁCH HĐ** (search + empty-state — GIỮ nguyên chức năng, chỉ
đổi chỗ; KHÔNG xoá khối UI anh không bảo xoá; HĐ tạo-trực-tiếp-ngoài-gói vẫn luôn thấy được ở đây —
vá F-3 lens-2: cây CHỈ chứa HĐ sinh từ gói).
Nhánh-đã-chọn **Q2 = Select loại trên đầu panel cây** (khuôn KHKK Select nhóm `KhkkListPanel.tsx:19-20`):
default = `?type=N` của trang; lọc leaf GĐ3 trên cây theo loại đã chọn.
Kỹ thuật: `usePipelineStages.ts:377` thêm opt-in `opts.onOpenContract` (FLAT trong opts — khuôn
`opts.onOpenKhkk :244`/`opts.onOpenPe :228`; KHÔNG bịa tầng `stage3Options` — F-4 lens-1) + panel
nhận qua `Pick<>` `PipelineTreePanel.tsx:97,111`; fe-admin KHÔNG truyền ⇒ **mặc định TẮT = navigate
như cũ** (control-âm bắt buộc). Search server-side: nối ô tìm → param `search` của `/contracts`
(sẵn `ContractsController.cs:19` — F-10 lens-2, nói thẳng: ĐỔI từ client-side).
- **W3 — Đang-duyệt ở lại trang (fe-user, FE-only — khuôn PE THẬT là đổi-endpoint):**
`Ct_*_Pending``/my-contracts?type=N&pendingMe=1`; `MyContractsPage` khi `pendingMe=1` ⇒ **gọi
`/contracts/inbox`** (0-param, khuôn `PurchaseEvaluationsListPage.tsx:91-97` đổi-endpoint) + lọc
`type` client-side (khai lệch-W7 CHẤP NHẬN: inbox không phân trang, payload nhỏ — đúng hiện trạng
`InboxPage.tsx:103`; nâng BE = `[carry:w7b-inbox-param]`, KHÔNG gộp đợt này). GIỮ `/inbox` hộp thư tổng.
- **W5 — Đường XOÁ đồng-khuôn KHKK (BE nhỏ + FE, 0-mig, TEST-BEFORE):** *(viết lại toàn bộ theo
F-7/F-12 lens-2 + F-3/F-4/F-5 lens-3)*
**Khuôn HÀNH-VI = KHKK** (`ContractSigningPlanFeatures.cs:1763-1783`, lời anh "tương tự như KHKK"):
xoá được ở **{DangSoanThao(+phase nháp <DangInKy như ), TraLai(98), TuChoi(99)}**, bởi **Drafter
hoặc Admin**; **ChoDuyet(10) VẪN CẤM** (approver-delete = Q4 chờ anh KHÔNG làm đợt này).
Việc BE tại `Application/Contracts/ContractFeatures.cs:889-901` (DeleteContractCommand):
(a) thêm check **Drafter/Admin** (hiện KHÔNG kiểm chủ sở hữu lỗ sẵn , sửa luôn KHAI sửa-lỗ-cũ);
(b) guard phase: allow `< DangInKy` `{TraLai, TuChoi}`; **chặn tường minh `DaPhatHanh(9)`**
dải còn lại (F-5 lens-3 đã gen 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 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);
(e) xác nhận sẵn: `Remove` = SOFT-delete qua `AuditingInterceptor.cs:54-62` (H-6 wave chỉ assert,
đừng đo lại).
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 đó
CODE CHẾT S175), hiện khi phase allow-list + user Drafter/Admin; mirror ×2 app SHA-pair.
## ③ Checklist — 5 wave + acceptance ĐO ĐƯỢC
## ③ WAVE + CHECKLIST acceptance (đã siết theo B2)
Thứ tự: **W1 → W2 → W3** (W3 sau W1 vì cùng vùng `Layout.tsx:148-152`); **W4 song song** bất kỳ
lúc; **W5 cuối** (BE + test-before guard). Mỗi wave: implement → build → reviewer-gate → tick.
Thứ tự: **W4 → W1 → W2 → W3 → W5** (W4 lành nhất đi đầu; W3 sau W2 "Đang duyệt" hiển thị panel
nào phụ thuộc layout mới; W5 cuối BE + test-before).
- **W1 — Danh-tính menu + slice per-loại (fe-user):**
✅(i) tái-hiện A: mở nhóm 01 → click "Luồng duyệt" → accordion GIỮ nhóm 01, leaf sáng đúng;
✅(ii) tái-hiện B: `?type=1&phase=9&view=approved` → CHỈ leaf "Đã duyệt" sáng;
✅(iii) WfView `ct=1` chỉ hiện họ `HD-TP` (0 dòng QT-HD-V2-001/loại khác);
✅(iv) `grep -rn "workflow-matrix?type=3" fe-user/src` = 0-hit + control dương
`grep "workflow-matrix?ct=" fe-user/src` ≥1;
✅(v) `npm run build` fe-user PASS.
- **W2 — Cây toàn trình sang trái (fe-user + cặp mirror ×2 app):**
✅(i) grep `grid-cols-\[400px` MyContractsPage ≥1;
✅(ii) click leaf HĐ trên cây → panel-2 mở detail, URL Ở LẠI `/my-contracts`;
✅(iii) gõ search → flatMode;
✅(iv) cặp `usePipelineStages.ts`/`PipelineTreePanel.tsx` 2 app SHA256 khớp từng cặp;
✅(v) build 2 app PASS.
- **W3 — Đang-duyệt về cùng trang (fe-user):**
✅(i) grep `pendingMe=1` nhánh Ct_ Layout ≥1; ✅(ii) leaf "Đang duyệt" ở lại `/my-contracts`,
list khớp inbox cũ cùng user; ✅(iii) build PASS.
- **W4 — Admin Designer 7 leaf (fe-admin ≈5 dòng):**
✅(i) sidebar "Quy trình duyệt (Mới)" đủ 17 leaf (2 NCC + 8 KHKK + 7 HĐ Order 11..17);
✅(ii) click "QT Duyệt HĐ — Thầu phụ" → panel khoá mã `HD-TP`, history CHỈ HD-TP;
✅(iii) build fe-admin PASS.
- **W5 — Nút Xóa màn duyệt V2-aware (BE nhỏ + FE, 0-mig, TEST-BEFORE guard):**
✅(i) `dotnet test SolutionErp.slnx` PASS + test mới: TraLai/TuChoi Drafter xóa được ·
ChoDuyet CHỈ approver-đúng-lượt-có-cờ · cờ tắt → 403/409 · V1 `Phase>=DangInKy` VẪN chặn
(regression S160) · xác minh soft-vs-hard-delete (lead-note ②);
✅(ii) grep `AllowApproverDelete` Contract-delete-path ≥1 (trước: 0-hit);
✅(iii) panel 2 app SHA-identical + nút Xóa chỉ render khi DTO cờ bật; ✅(iv) build 2 app PASS.
- **W4:** ✅(i) sidebar admin "Quy trình duyệt (Mới)" đủ **17 leaf** (2 NCC + 8 KHKK + 7 "QT Duyệt
…" Order 11..17 H-5); ✅(ii) click "QT Duyệt Thầu phụ" panel khoá `HD-TP`, history
CHỈ HD-TP; ✅(iii) control-âm: leaf `KhkkN1` + `DuyetNcc` vẫn mở đúng trang ; ✅(iv) build fe-admin PASS.
- **W1:** ✅(i) tái-hiện A (đo local ④): mở nhóm 01 click "Luồng duyệt" accordion GIỮ nhóm 01;
control-âm `ct=5` mở nhóm 05; ✅(ii) tái-hiện B: `?type=1&phase=9&view=approved` CHỈ leaf "Đã
duyệt" sáng; ✅(iii) **vòng `ct ∈ 1..7`**: trang WfView hiện **1** quy trình đúng họ (Dev seed đủ
7 `HD-*` idempotent) + **0** dòng khác họ + riêng `ct=1` phải VẮNG `QT-HD-V2-001` (tồn tại trên Dev
ô "vắng-vì-lọc" nghĩa, không phải "vắng-vì-không-có"); control `ct=99` KHÔNG lọc (giữ hành
vi ); ✅(iv) `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` **1 GIỮ NGUYÊN** (v2 giữ type=3;
ô 0-hit của v1 đã bỏ) + accordion mới nhánh `ct`: `grep -nF "get('ct')" fe-user/src/components/Layout.tsx`
1; ✅(v) build fe-user PASS.
- **W2:** ✅(i) `grep -F "grid-cols-[400px" fe-user/src/pages/contracts/MyContractsPage.tsx` 1;
✅(ii) click leaf trên cây panel-2 mở detail, URL LẠI `/my-contracts` (đo local ④);
✅(iii) panel-3 = danh-sách sống: search gọi `/contracts?search=` (Network tab, server-side);
✅(iv) cặp `usePipelineStages.ts`/`PipelineTreePanel.tsx` 2 app SHA256 khớp từng cặp (baseline
`1b9b5c61…`/`4ffc96be…` H-1); ✅(v) **control-âm fe-admin**: không truyền `onOpenContract` click
leaf GĐ3 vẫn `navigate('/contracts/{id}')` như ; leaf GĐ1/GĐ2/GĐ4 hành-vi không đổi CẢ 2 app;
✅(vi) **deep-link cũ** `/contracts/{id}` dán thẳng vẫn mở detail (không 404); ✅(vii) ** tạo
trực-tiếp (ngoài gói)** hiện panel-3 danh-sách (ca F-3 lens-2); ✅(viii) build 2 app PASS.
- **W3:** ✅(i) hành-vi so-số 2 chiều (đo local ④, user 1 đến-lượt + 1 không-đến-lượt):
bật `pendingMe=1` list **ÍT HƠN** list thường + không-đến-lượt **VẮNG**; control-dương: user
ngoài luồng **0 dòng** (không phải cả list); ✅(ii) leaf "Đang duyệt" lại `/my-contracts`;
✅(iii) build fe-user PASS.
- **W5:** ✅(i) `dotnet test SolutionErp.slnx` PASS **680 → 680+N** (pin số; cập nhật ĐÚNG 1 row test
`docs/STATUS.md`), test-before liệt đủ ca: TraLai(98)/TuChoi(99) Drafter xoá được · DangSoanThao
Drafter xoá được (như ) · **ChoDuyet(10) CẤM kể cả Drafter** (khuôn KHKK NGƯỢC v1) ·
**DaPhatHanh(9) CẤM mọi đường** · user thường xoá người khác **403** (sửa lỗ khai ) ·
V1 nháp (2) vẫn xoá được (chống 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`
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.
**Ngoài wave — CHỜ ANH (không tự làm):** ① gỡ-ghim/thay `QT-HD-V2-001` bằng 7 bản `HD-*` nếu
muốn "tuyệt-đối mỗi loại 1 quy trình" (van xả `ContractWorkflowGuards.cs:141-143` là CỐ Ý —
việc Designer/DATA 0 code); ② port 3 món KHKK cố-ý-không-port (skipToFinal · duyệt-thay-CEO ·
banner-kết-thúc-sớm) = cần nở `ContractDetail` DTO.
## ④ Nấc đo local (BẮT BUỘC — deploy CHỜ, ô hành-vi KHÔNG tick bằng suy luận)
1. BE: `dotnet run --project src/Backend/SolutionErp.Api` (LocalDB `SolutionErp_Dev`; seeder idempotent
tự 7 `HD-*` + `QT-HD-V2-001` sample tiền-đề ô W1(iii)).
2. FE: `npm run dev` `fe-user` (proxy `/api → :5443`) cho W1/W2/W3 · `fe-admin` cho W4.
3. Dữ liệu chuẩn bị: 1 trên cây GĐ3 (tạo từ KHKK) · 1 tạo trực-tiếp ngoài gói (W2-vii) ·
user đúng-lượt + không-đến-lượt (W3-i) · 1 TraLai + 1 ChoDuyet + 1 DaPhatHanh (W5).
4. Ô nào không dựng được dữ liệu local đánh **DEFERRED-TO-DEPLOY** công khai trong run.md, CẤM tick.
## Câu hỏi ĐÁNH SỐ trình anh (Q1/Q2 = nhánh-đã-chọn, đảo được; Q3/Q4/Q5 = chờ anh)
- **Q1 (Thứ-1):** em chọn **HOÁN ĐỔI** 2 khung (cây trái 400px làm trục; danh-sách phải 360px,
GIỮ nguyên chức năng) thay bỏ hẳn danh-sách kiểu KHKK-thuần cây KHÔNG chứa tạo-trực-tiếp
(khác KHKK: mọi phiếu KHKK đều từ PE) + không xoá khối UI anh chưa bảo xoá. Anh muốn KHKK-thuần
(bỏ list, flat-fallback trong panel cây) thì nói đổi 1 wave.
- **Q2 (Thứ-1×2):** cây bên trái em gắn **Select loại HĐ** (khuôn Select nhóm của KHKK), default theo
menu đang đứng leaf GĐ3 trên cây lọc theo loại. Anh muốn cây hiện đủ mọi loại không lọc thì bỏ Select.
- **Q3 (Thứ-4):** màn duyệt port 3 món KHKK đang không: ô "Cấp này KẾT THÚC quy trình" ·
nhãn "⚑ Duyệt thay CEO" · banner "kết thúc sớm Bước X"? **2 cờ đầu anh ĐANG THẤY trong dialog
Sửa-tại-chỗ (ảnh-3) hiện bật Admin máy duyệt KHÔNG đọc = cấu hình câm.** Port = nở
`ContractDetail` DTO (BE nhỏ). Chưa port thì em ghi chú "chưa áp dụng cho " vào Designer (khuôn
mờ-ô D3/S182) để hết câm.
- **Q4 (Thứ-4):** mở **approver-delete** cho (người duyệt đúng-lượt xoá phiếu ChoDuyet theo cờ
`AllowApproverDelete` per-Cấp tính năng PE đang , KHKK KHÔNG )? Mặc định đợt này **KHÔNG**
đúng lời anh "tương tự KHKK". Anh gật thì thành wave riêng (cột DB sẵn, 0-mig).
- **Q5 (dữ liệu):** quy trình tay `QT-HD-V2-001` (bản type-3 duy nhất trên Dev, van-xả cố-ý cho
ngoài họ) GIỮ làm van xả admin, hay gỡ-ghim/thay bằng 7 bản `HD-*` để "mỗi loại 1 quy trình"
tuyệt-đối cả trên dữ liệu? (0 code việc Designer/DATA.)

View File

@ -1,13 +1,243 @@
# LENS-1 CORRECTNESS — review đối-kháng spec SOLTUON 12-08-2026 (YC-029)
Reviewer lane-1/3 (ensemble /fable-clone) · lăng-kính **ĐÚNG-SAI KỸ THUẬT** của spec W1+W2 (+W3/W5 khi
chạm trục correctness). Mọi claim kèm `file:line` đo tại HEAD `1c677fb7`, cây làm việc 2026-08-12.
## Verdict
_(đang đo — điền cuối lượt)_
**FAIL — 11 findings (2 BLOCKER · 4 MAJOR · 3 MINOR · 2 xác-nhận-dương).**
Không phải FAIL về **hướng**: 5 điều chỉnh đúng ý owner, và chẩn-đoán root-cause A + B của invest
**tôi đo lại thì ĐỨNG** (F-10, F-11). FAIL là về **tính thực-thi-được của chữ trong spec**: hai mệnh
đề nền của W1 và W3 là **SAI SỰ THẬT về code hiện tại**, và nếu implementer làm đúng y chữ thì:
- **W1** đẻ trang "Luồng duyệt" **trắng / hiện nhầm dữ liệu Duyệt NCC** (F-1);
- **W3** đẻ danh sách "Đang duyệt" **hiện TOÀN BỘ HĐ của loại đó** mà vẫn xanh mọi acceptance (F-2).
Cả hai đều là hạng **im lặng** — build xanh, grep xanh, chỉ người dùng mới thấy. Sửa 2 mệnh đề +
nới W2 scope (F-3) + siết 3 ô acceptance (F-4, F-6, F-8) là spec chạy được.
---
## Findings
_(đánh số F-1.. — append ngay khi chốt)_
### 🔴 F-1 (BLOCKER, W1) — "trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn" là **SAI**. Bỏ `type=3` khi URL ⇒ trang rơi về fallback `type=1` (Duyệt NCC).
## Đề nghị sửa spec
Spec `②` dòng 37-38 và `③ W1` dựa trọn trên mệnh đề này. Đo thực tế:
_(điền cuối lượt)_
- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:56-61`:
```tsx
const { pathname } = useLocation()
const rawType = Number(searchParams.get('type'))
const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1
```
`pathname` **KHÔNG** tham gia suy `typeInt` — comment `:54-55` tự khai công dụng của nó: *"link 'xem
tất cả' phải bám đường dẫn ĐANG đứng"*. Đó là thứ duy nhất `pathname` làm.
- "Khuôn alias-path khkk" **không phải path-inference**: alias KHKK vẫn mang type trên QUERY —
`Layout.tsx:86` `Khkk_WfView: '/khkk/workflow-matrix?type=10&group=1'` và `:216`
`/khkk/workflow-matrix?type=10&group=${group}`; route alias `App.tsx:83`. Tức khuôn thật là
**"path alias + type vẫn ở query"**, đúng ngược với thứ spec viện dẫn.
- Hệ quả số học: bỏ `type` ⇒ `searchParams.get('type')` = `null` ⇒ `Number(null)` = **0** ⇒ 0 ∉ {1,2,3,10}
⇒ `typeInt = 1` ⇒ `queryKey ['workflow-matrix', 1]` + `params { applicableType: 1 }` (`:63-71`) ⇒ trang
tải **quy trình Duyệt NCC**. Nhãn header cũng đổi theo: `typeLabel = summary?.applicableTypeLabel`
(`:76`) ⇒ "Luồng duyệt — Duyệt NCC".
- Cộng thêm nhánh lọc `ct` mà chính W1 thêm (`code === 'HD-<token>'`): bộ dữ liệu ApplicableType=1
không có mã nào bắt đầu `HD-` ⇒ **0 dòng ⇒ trang trắng**.
**Không đủ để diệt bug, và còn mở đường mới:** kể cả khi trang đọc `ct` đúng, việc bỏ `type` làm
`useEffect` accordion `Layout.tsx:473` `if (!typeParam) return` **thoát sớm** ⇒ deep-link/refresh thẳng
vào `/contracts/workflow-matrix?ct=5` **không mở nhóm 05** ⇒ leaf sáng nhưng **nằm trong nhóm đang
đóng** — đúng triệu chứng "menu không ăn khớp" mà Đ3 muốn diệt.
**Đề nghị:** chọn MỘT trong hai, viết thẳng vào spec (đừng để implementer đoán):
(a) **Giữ `type=3` trên URL** + chỉ sửa accordion (đọc `ct` trước, xem F-5) — rẻ nhất, 0 đụng trang matrix;
(b) **Thật sự thêm path-inference** vào `WorkflowMatrixViewPage`: `pathname.startsWith('/contracts/') → 3`,
`'/khkk/' → 10`, `'/purchase-evaluations/' → query`, và khai rõ đây là **code MỚI**, kèm ô acceptance
riêng cho KHKK (`/khkk/workflow-matrix?type=10&group=1` **không được** đổi hành vi).
---
### 🔴 F-2 (BLOCKER, W3) — "BE param `pendingMe` sẵn" là **SAI** cho phía Hợp đồng. Param lạ bị ASP.NET nuốt im lặng ⇒ "Đang duyệt" hiện TOÀN BỘ HĐ của loại.
Spec `` dòng 45-46: *"`/my-contracts?type=N&pendingMe=1` (khuôn PE `:176`; **BE param `pendingMe` sẵn**)"*.
Invest cũng khẳng định vậy (`sub-invest-b1-fable.md:84`). Đo:
- `grep -rn "pendingMe\|PendingMe" src/Backend/SolutionErp.Application/Contracts/ src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` → **0 hit**.
- **Control-dương (chứng thước không hỏng):** cùng lệnh trên `src/Backend` trả 4 hit ở họ KHKK —
`ContractSigningPlansController.cs:42` `[FromQuery] bool pendingMe = false`,
`ContractSigningPlanFeatures.cs:1219` `bool PendingMe = false`, `:1255` `if (request.PendingMe)`.
⇒ grep chạy đúng; phía Contract **thật sự không có**.
- Chữ ký thật của endpoint list (`ContractsController.cs:20-27`): `phase · supplierId · projectId ·
type · page · pageSize · search · sortDesc` — hết.
- **Cơ chế hỏng im lặng:** model-binding ASP.NET Core **bỏ qua** query param không khai ⇒
`GET /contracts?type=1&pendingMe=1` = `GET /contracts?type=1` ⇒ trang "Đang duyệt" liệt **mọi** HĐ
loại 1 (kể cả Nháp, Đã phát hành), 200 OK, 0 lỗi, 0 warning.
- **Khuôn PE mà spec viện dẫn cũng không phải BE-param:** PE là **FE đổi ENDPOINT** —
`PurchaseEvaluationsListPage.tsx:39` `const pendingMe = sp.get('pendingMe') === '1'` → `:91` `if (pendingMe) { … }`
(nhánh gọi endpoint inbox riêng). Phía BE PE cũng 0 hit `pendingMe`.
**Đường đi đúng cho HĐ (và ranh của nó):** `GET /contracts/inbox` (`ContractsController.cs:41-43`) trả
`List<ContractListItemDto>` **không phân trang** và **không nhận `type`** ⇒ FE phải lọc `type` client-side.
Ở đây lọc client **chấp nhận được** (khác ca W7-S187: nhánh `/contracts` bị cắt 100 dòng nên lọc client
làm loại hiếm biến mất; inbox trả trọn, không cắt) — **nhưng phải viết ra trong spec**, vì nó là ngoại-lệ
của một luật đã có, không phải mặc định.
**Đề nghị:** đổi W3 thành *"MyContractsPage: `pendingMe=1` ⇒ gọi `/contracts/inbox` rồi lọc `type`
client-side (inbox không phân trang — nêu rõ lý do được lọc client)"*, HOẶC nhận W3 là **wave BE** (thêm
`pendingMe` vào `ListContractsQuery` + tái dụng logic match approver V2/V1 của `GetMyInboxQuery`). Không
được để câu "BE đã sẵn".
---
### 🟠 F-3 (MAJOR, W2) — Dời cây sang trái **làm mất danh-tính per-loại** của 7 leaf "Danh sách" — mâu thuẫn thẳng với Đ2+Đ3. Khuôn KHKK mà spec viện dẫn **có** cơ chế giữ danh-tính, spec **bỏ sót** đúng cơ chế đó.
- Hôm nay panel-1 = danh-sách **lọc theo `type` ở SERVER** (`MyContractsPage.tsx:34,50-57`; W7-S187 cố ý
gỡ lọc client vì >100 HĐ thì loại hiếm biến mất im lặng).
- `PipelineTreePanel` **không có trục loại HĐ**: props ở `:94-128` = `currentStage · stage2Options ·
hideHeader · hideSearch …`; cây dựng theo Năm → Dự án → Hạng mục (`:24-32`). ⇒ thay panel-1 bằng cây
thì **cả 7 leaf "Danh sách" render y hệt nhau** — đúng thứ mà chính repo gọi là *"mục menu nói dối tên nó"*
(`WorkflowMatrixViewPage.tsx:80`).
- **Khuôn KHKK KHÔNG bị bệnh này vì nó có lát cắt:** `KhkkListPanel.tsx:200,216` nhận prop `group`, đẩy
xuống server (`:253` `group: group ?? undefined`) **và** cắt nội dung cây (`:281`
`src.filter(k => k.approvalGroup === group)`, `:295`, `:323`) — bơm vào cây qua `stage2Content`
(`PipelineTreePanel.tsx:111` `Pick<BuildStagesOptions,'stage2Content'|'stage2Count'|'onOpenKhkk'|'onOpenPe'>`).
- Bên GĐ3 **chưa có gì tương ứng**: `grep "opts\." fe-user/src/hooks/usePipelineStages.ts` chỉ ra
`stage1Content/stage1Count` (`:212-213`), `onOpenPe` (`:228-229`), `onOpenKhkk` (`:244`),
`stage2Content/stage2Count` (`:303-306`) — **`stage3Content` 0-hit**.
⇒ W2 thật sự cần **4 món** chứ không phải 2 như spec liệt: `BuildStagesOptions` + `stage3Content` +
`stage3Count` + `onOpenContract`, **và** prop `stage3Options` trên panel (`Pick<…>` phải kể tên từng key —
quên tên nào thì opt-in **chết câm**, đúng lớp "guard dựng mà không ai gọi"), **cộng** quyết định
`?type=N` cắt cây thế nào. Đây là 2 file **MIRROR SHA-identical ×2 app** nên mọi sai shape nhân đôi.
**Đề nghị:** spec phải trả lời dứt khoát: *"panel-1 ở `/my-contracts?type=N` hiển thị cây **đã cắt theo
ContractType N** (leaf GĐ3 lọc `c.type === N`), hay cây toàn-cảnh không cắt?"* — và thêm ô acceptance
đối-chứng: đứng `?type=1` và `?type=5`, hai cây **phải khác nhau** (nếu chọn cắt), hoặc khai thẳng
"cố ý giống nhau" như một quyết-định owner (khuôn QĐ-2 KHKK).
---
### 🟠 F-4 (MAJOR, W1 acceptance) — Ô ✅(iii) **đo hình-dạng, không đo giá-trị**: trang TRẮNG vẫn qua.
Nguyên văn: *"WfView `ct=1` chỉ hiện họ `HD-TP` (0 dòng QT-HD-V2-001/loại khác)"*. Ca hỏng F-1 sinh ra
**0 dòng tất cả** ⇒ mệnh đề "0 dòng của loại khác" **ĐÚNG** ⇒ gate xanh trên đúng ca hỏng nặng nhất.
Ô (iv)/(v) là grep + build, cũng xanh. ⇒ **cả W1 có thể tick trọn trong khi tính năng chết.**
**Đề nghị** thay bằng 3 vế đo được: (1) **≥1 dòng** và mọi dòng có `code` bắt đầu `HD-TP`;
(2) header đọc **"Luồng duyệt — Hợp đồng"** (không phải "Duyệt NCC") — bắt đúng ca fallback `typeInt=1`;
(3) control-âm: `?ct=5` phải ra tập **KHÁC** `?ct=1`.
---
### 🟠 F-5 (MAJOR, W1) — "Loại trừ pathname `/contracts/workflow-matrix`" là **danh-sách-đen**; case anh em `Hdc_*` đang dính đúng bug đó và vẫn sống sau khi vá.
- Máy hiện tại (`Layout.tsx:470-485`): mọi pathname **không** bắt đầu `/purchase-evaluations` đều bị đọc
`?type=` **như ContractType** (`INT_TO_TYPE_CODE`, `:16-29`) rồi set accordion (1 slot ⇒ mutex, `:467`).
- **Case anh em còn sống:** `Hdc_*` GĐ4 — `Layout.tsx:70-76` `'/hard-copies?type=1'`…`'?type=7'`. Bấm
"Hợp đồng cứng → Thầu phụ" ⇒ nhánh else ⇒ **mở nhóm "01. HĐ Thầu phụ"** của cây menu khác. Cùng
lớp bug A, **không** nằm trong tập vá của spec.
- `Khkk_WfView` (`:86,:216`, `type=10`) thoát **do may**: `INT_TO_TYPE_CODE[10]` = `undefined` ⇒ guard
`if (code)` `:483` chặn. Không phải do thiết kế.
**Đề nghị:** đổi hình dạng bản vá từ **đen** sang **trắng**: (1) đọc `ct` TRƯỚC (mọi pathname), có `ct`
hợp lệ ⇒ dùng nó, dừng; (2) chỉ khi vắng `ct` mới đọc `type`, và **chỉ trên allowlist** pathname mà
`type` = ContractType (`/my-contracts`, `/contracts/new`, `/inbox`, `/hard-copies`). Ô acceptance kèm:
bấm leaf `Hdc_ThauPhu` **không** làm đổi trạng thái accordion `Ct_*`.
---
### 🟠 F-6 (MAJOR, W5 test-matrix) — "V1 `Phase>=DangInKy` VẪN chặn" là câu **nhập nhằng**: V1-flat cũng đứng ở `ChoDuyet=10` (≥5). Phân nhánh phải theo **cờ pin**, không theo **số phase**.
- Guard thật: `ContractFeatures.cs:897` `if (entity.Phase >= ContractPhase.DangInKy) throw ConflictException(...)`,
`DangInKy = 5` (`ContractPhase.cs`).
- Bảng số: `DaPhatHanh=9` · `ChoDuyet=10` · `TraLai=98` · `TuChoi=99` — **tất cả ≥ 5** ⇒ đều bị chặn hôm nay
(đúng finding S160).
- 🔴 Điểm nhập nhằng: **V1-flat (Mig 21) cũng dùng `ChoDuyet=10`** làm trạng thái trung gian
(`ContractPhase.cs` comment tự khai: *"ChoDuyet = 10 — generic intermediate, CurrentWorkflowStepIndex
tracking"*). ⇒ nếu bản vá nới theo **tập phase** thì nó nới **cả V1-flat** mà vẫn tick được ô
"V1 Phase>=DangInKy vẫn chặn" (vì ô đó chỉ nói tới 5..9). Đúng lớp gotcha **#42** (dual-schema
branch phải theo field pin: `ApprovalWorkflowId != null` ⇒ V2, ngược lại V1).
- Ô test còn **thiếu 2 ca**: **V2 @ `DaPhatHanh=9`** (terminal thành công + đã gen mã HĐ — phải VẪN cấm
xóa) và **V1-flat @ `ChoDuyet=10`** (giữ cấm hay mở? owner phải chốt).
**Đề nghị:** viết acceptance theo **ma trận (schema × phase × actor)**, mỗi ô một dòng, và nêu
discriminator là `ApprovalWorkflowId`.
---
### 🟢 F-7 (GỠ NỢ — trả lời câu lead treo ở `` dòng 62-66) — `db.Contracts.Remove` **ĐÃ LÀ soft-delete**. W5 không cần đổi đường xóa.
- `AuditingInterceptor.cs:54-62`: mọi `AuditableEntity` ở `EntityState.Deleted` bị **lật** thành
`Modified` + `IsDeleted = true` + `DeletedAt` + `DeletedBy`.
- Handler tự khai `// ========== DELETE (soft) ==========` (`ContractFeatures.cs:885`), và màn "Đã xóa"
đọc được chính là hệ quả (`ContractsController.cs:34-40` `/contracts/deleted`).
⇒ Bỏ hạng mục *"xác minh soft-vs-hard-delete"* khỏi W5 ✅(i), hoặc hạ xuống **1 assert trong test**
(xóa xong: row còn tồn tại, `IsDeleted == true`). Đây là chỗ spec **rộng hơn việc thật** — cắt được.
---
### 🟡 F-8 (MINOR, W1) — Sau W1, `ct` từ **trang trí** thành **danh tính**, nhưng chưa ai định nghĩa `ct` rác.
`grep "get('ct')" fe-user/src fe-admin/src` = **0 hit** ⇒ hôm nay `ct` chỉ được **ghi** (`Layout.tsx:151`)
để phân biệt active-state, **không ai đọc**. Khi trang bắt đầu đọc nó, cần clamp y khuôn `group`
(`WorkflowMatrixViewPage.tsx:84-86`: regex `/^[1-8]$/` + *"group rác/thiếu ⇒ BỎ QUA hoàn toàn… thà hiện
đủ còn hơn lọc theo một con số vô nghĩa rồi ra trang trắng"*). Spec nên nói thẳng: `ct ∉ 1..7` ⇒ **không
lọc** (hiện đủ), không phải lọc-ra-rỗng.
### 🟡 F-9 (MINOR, W1) — Bookmark cũ mất highlight (dữ liệu vẫn đúng).
URL đã lưu `/contracts/workflow-matrix?type=3&ct=1`, leaf mới trỏ `?ct=1`. `queryMatches:368-374`: `type`
**không** thuộc `TRANSIENT_QUERY_KEYS` (`:350`) ⇒ không ignorable ⇒ `aKeys=[ct,type]` (2) ≠ `bKeys=[ct]` (1)
⇒ **không sáng**. Không mất chức năng (miễn trang vẫn chấp nhận `type` — xem F-1 phương án (a)/(b)),
nhưng nên có 1 dòng "backward-compat: trang nhận CẢ 2 dạng URL".
### 🟡 F-10 (MINOR, W2 acceptance — xác nhận DƯƠNG) — Baseline mirror **đang sạch**, nên ô SHA có nghĩa.
Đo tại HEAD: `usePipelineStages.ts` `1b9b5c611663666d3c8853a88b020d57b712c3cfd9bb524790bcd0359152ff48`
(fe-user ≡ fe-admin) · `PipelineTreePanel.tsx`
`4ffc96beded5788a7463efa0e7df405c217a5d60ef7f5ca1ce83f323c4c6c334` (fe-user ≡ fe-admin). ⇒ ô ✅(iv) của
W2 không phải "gate hỏng sẵn"; nếu lệch sau wave thì đúng là lỗi của wave.
### 🟢 F-11 (XÁC NHẬN DƯƠNG) — Root-cause A + B của invest **ĐỨNG**; cơ chế `view=approved` **thật sự** diệt 2-leaf-cùng-sáng; `phase=9` **vẫn là** bộ lọc đúng.
- **A dựng lại được**: `Layout.tsx:151` sinh `?type=3&ct=N` → `:470-485` nhánh else (pathname không phải
`/purchase-evaluations`) → `INT_TO_TYPE_CODE[3] = 'NhaCungCap'` (`:16-29`) → `setExpandedCtCode` mà state
chỉ có **1 slot** (`:467`) ⇒ nhóm đang mở bị đóng. Chuỗi HELD.
- **B — kiểm bằng tay theo đúng luật `queryMatches`**: đứng `?type=1&phase=9&view=approved`:
leaf "Đã duyệt" (đích ghim `type,phase,view`) ⇒ `phase` **được pin nên so nghiêm** ⇒ khớp ✓;
leaf "Danh sách" (đích `type`) ⇒ `phase` bị bỏ qua **nhưng `view` KHÔNG thuộc TRANSIENT** (`:350`) ⇒
`aKeys=[type,view]` (2) ≠ `bKeys=[type]` (1) ⇒ **tắt** ✓. Đúng như PE `:180-185`.
- `phase=9` vẫn đúng nghĩa sau S188: `ContractPhase.cs` comment tự khai *"last step done ──►
DaPhatHanh (terminal + gen mã HĐ)"* ⇒ HĐ duyệt xong V2 rơi vào **9**, không phải một mã khác. Ô lọc
không mục ruỗng.
---
## Đề nghị sửa spec (theo thứ tự chặn)
1. **W1 §② dòng 37-38** — xoá mệnh đề *"trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn"*.
Thay bằng phương án (a) **giữ `type=3`, chỉ sửa accordion** *hoặc* (b) **thêm path-inference như code MỚI**
+ ô acceptance không-hồi-quy cho `/khkk/workflow-matrix?type=10`.
2. **W3 §② dòng 45-46** — xoá *"BE param `pendingMe` sẵn"*. Chốt đường: FE đổi endpoint sang
`/contracts/inbox` + lọc `type` client (kèm 1 câu vì sao lọc client được chấp nhận ở đây: inbox
**không phân trang**), hoặc nâng W3 thành wave BE.
3. **W2 §② dòng 33-34** — nới scope: `stage3Content` + `stage3Count` + `onOpenContract` + prop
`stage3Options` (liệt **từng key** trong `Pick<>`), và **chốt** cây có cắt theo `?type=N` hay không.
Sửa luôn ký hiệu: analog thật ở hook là **flat** `opts.onOpenKhkk` (`:244`) — `stage2Options` là tên
**prop của panel** (`PipelineTreePanel.tsx:97,111`), hai tầng khác nhau, spec đang trộn.
4. **W1 ✅(iii)** — thêm "≥1 dòng" + "header = nhãn Hợp đồng" + control-âm `ct=5` (F-4).
5. **W1 accordion** — đổi từ blacklist-pathname sang **`ct` trước / allowlist `type` sau**, + ô acceptance
`Hdc_*` không đụng accordion `Ct_*` (F-5).
6. **W5 ✅(i)** — ma trận (schema × phase × actor), discriminator = `ApprovalWorkflowId` (gotcha #42);
thêm ca **V2@DaPhatHanh=9 vẫn cấm** và **V1-flat@ChoDuyet=10** (owner chốt); **bỏ** hạng mục xác minh
soft-vs-hard (đã đóng ở F-7, hạ thành 1 assert).
7. **W1** — thêm 1 dòng clamp `ct ∉ 1..7 ⇒ không lọc` (F-8) và 1 dòng backward-compat URL cũ (F-9).
---
### Ghi chú phạm vi
Lane này **không** chấm 3 trục: văn phong/UX (lane khác), phân quyền/authz, và deploy — spec tự khai
DEPLOY CHỜ nên không có claim prod để curl. Không đo `dotnet build`/`npm run build` (spec là **đề xuất**,
chưa có diff). Mọi số liệu trên đo tại cây làm việc `main @ 1c677fb7`.

View File

@ -1,15 +1,130 @@
# sub-review-b2-lens2 — LENS-2 FIDELITY-KHUÔN + SCOPE
> Reviewer lane-2/3 (ensemble /fable-clone). Lăng-kính: độ trung-thành với lời anh (YC-029 + 3 ảnh) + khuôn S176 "đã-deploy = CHUẨN" + scope-risk + mirror 2 app + wave order.
> Mọi claim dưới đây ĐO từ đĩa tại HEAD `1c677fb7` (working tree). Không có claim nào chép lại từ `sub-invest-b1-fable.md` mà không mở file gốc.
## Verdict
_(đang đo — điền cuối lượt)_
**PASS_WITH_FIXES — 12 findings** (4 CRITICAL · 4 MAJOR · 4 minor). Spec phủ 5/5 điều chỉnh về mặt ĐỀ-BÀI, nhưng **4 câu implement trong §2 sẽ đẻ lỗi CÂM nếu implementer làm đúng chữ**, W2 (cây sang trái) thiếu 2 mitigation mà chính khuôn KHKK tự khai là BẮT BUỘC, và **W5 mirror NHẦM module** — khuôn KHKK mà anh chỉ định lại CẤM đúng cái W5 định mở.
---
## Findings (F-1..)
_(append từng finding ngay khi đo xong)_
### F-1 · CRITICAL — §2 Đ2+Đ3: "bỏ `type=3` khỏi URL, trang tự suy applicableType theo path" — cái "khuôn alias-path khkk" ĐÓ KHÔNG TỒN TẠI, và trang rơi về **type 1 (Duyệt NCC)** chứ không phải 3
Spec `spec-…md:37-38` chỉ đạo: `Layout.tsx:151` WfView → `/contracts/workflow-matrix?ct=N` (**BỎ `type=3`**) vì "trang tự suy applicableType=3 theo path, khuôn alias-path khkk sẵn".
Đo:
- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:57,61`:
```
const rawType = Number(searchParams.get('type'))
const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1
```
Bỏ `type` khỏi URL ⇒ `Number(null)` = 0 ⇒ **fallback `: 1`** ⇒ fetch `applicableType=1` = quy trình **Duyệt NCC**. Trang KHÔNG đọc `pathname` để suy type: `pathname` chỉ dùng cho link "xem tất cả" — chính comment `:54-56` khai vậy ("link *xem tất cả* phải bám đường dẫn ĐANG đứng").
- "Khuôn alias-path khkk" là **phản-chứng của chính spec**: leaf KHKK `Layout.tsx:216` = `/khkk/workflow-matrix?type=10&group=${group}` — KHKK **GIỮ `type=` trên URL**, không suy theo path. Route `/khkk/workflow-matrix` (`App.tsx:83`) chỉ là alias pathname, KHÔNG mang ngữ nghĩa type.
⇒ Làm đúng chữ spec = trang "Luồng duyệt — HĐ Thầu phụ" hiển thị **quy trình Duyệt NCC**, không lỗi đỏ, không 404. Đây là lệch khuôn S176 (KHKK đã-deploy giữ `type=`), lại là dạng hỏng CÂM.
**Sửa:** GIỮ `?type=3&ct=N` (đúng khuôn KHKK) và chữa bug A ở **accordion** (`Layout.tsx:470-485`) — vốn đã nằm sẵn trong spec như "phòng thủ 2 lớp". Lớp phòng thủ đó chính là toàn bộ liều thuốc; vế "bỏ type=3" là phần TỰ CHẾ và là phần gây hại. Nếu vẫn muốn bỏ `type=3` thì spec phải kèm 1 câu bắt buộc: thêm nhánh suy type từ `pathname` TRƯỚC, và acceptance phải có ca "URL không có type ⇒ vẫn ra HD-*".
### F-2 · CRITICAL — §2 Đ4-W3: "BE param `pendingMe` sẵn" là SAI cho Contract ⇒ `/contracts?pendingMe=1` bị model-binding NUỐT, list ra TOÀN BỘ HĐ mà không báo lỗi
Spec `:45-46` (và invest `:114`) viết: *"`Ct_*_Pending` → `/my-contracts?type=N&pendingMe=1` (khuôn PE `:176`; **BE param `pendingMe` sẵn**) + `MyContractsPage` đọc `pendingMe` **gửi server**"*.
Đo:
- `grep -rn "PendingMe" src/Backend --include=*.cs` = **2 hit, CẢ HAI thuộc KHKK** (`ContractSigningPlansController.cs:42,50` + `ContractSigningPlanFeatures.cs:1219,1255`). Contract: **0 hit**.
- `ContractsController.cs:17-27` — `List(...)` nhận `page,pageSize,search,sortDesc,phase,supplierId,projectId,type`. **Không có `pendingMe`**. Query gửi thừa param ⇒ ASP.NET bỏ qua im lặng ⇒ trả full list.
- Khuôn PE THẬT (không phải "param BE"): `PurchaseEvaluationsListPage.tsx:91-97` — `if (pendingMe)` thì **ĐỔI ENDPOINT** sang `/purchase-evaluations/inbox` (kèm `type`). Còn `/contracts/inbox` **không nhận tham số nào** (`ContractsController.cs:41-43` `GetMyInboxQuery()` 0-arg) ⇒ mirror cho HĐ phải lọc `type` client-side (đúng như `InboxPage.tsx` đang làm) hoặc nới BE.
⇒ W3 đang được spec dán nhãn "FE nhỏ" trên một tiền-đề sai. Hoặc sửa câu implement thành "pendingMe ⇒ gọi `/contracts/inbox` + lọc type client (khuôn PE)", hoặc khai W3 có delta BE. **Acceptance W3 hiện KHÔNG bắt được lỗi này**: (i) grep `pendingMe=1` vẫn xanh; (ii) "list khớp inbox cũ" — người kiểm dễ chấp nhận vì list *có* dữ liệu (chỉ là nhiều hơn đúng). Đề nghị siết acceptance thành **so số dòng bằng nhau + 1 HĐ KHÔNG-đến-lượt phải VẮNG**.
### F-3 · CRITICAL — W2 bỏ mất 2 mitigation mà chính khuôn KHKK khai là **BẮT BUỘC**, và cây GĐ3 về mặt cấu trúc **không chứa HĐ tạo trực tiếp**
Header `fe-user/src/components/khkk/KhkkListPanel.tsx:9-24` khai nguyên văn: *"Chọn HÌNH ≠ chấp-nhận VỠ ⇒ 4 mitigation dưới đây là BẮT BUỘC, không phải tuỳ chọn"* — M-1 (`!canPe` ⇒ fallback danh-sách phẳng), M-2 (`onSelect` thay navigate), B1 (`stage2Content` phải kèm `stage2Count`), R-11 (cây chỉ 200 dòng ⇒ tìm/lọc đi SERVER).
Spec `:29-35` chỉ port **M-2** (`onOpenContract`) + nửa R-11 ("flat-list fallback server-side"). Thiếu:
- **M-1 analog:** nguồn của cây là index PE, `enabled: canPe` (`usePipelineStages.ts:83,102`). User chỉ có quyền HĐ (drafter HĐ, không có key Duyệt NCC) ⇒ **panel trái TRỐNG** trong khi `/contracts` vẫn trả dữ liệu ⇒ đúng mặt còn lại gotcha #82. Hôm nay `/my-contracts` không có bệnh này vì panel-1 là list thuần.
- **Cấu trúc:** `usePipelineStages.ts:333-359` — stage-3 leaves duyệt trên `contractIdsOfPackage` = hợp của `pe.contractId` + contract-id trên dòng KHKK. **HĐ tạo thẳng từ `/contracts/new?type=N`** (chính là leaf "Thao tác" và nút "+ Tạo HĐ mới" `MyContractsPage.tsx:145`) **không thuộc gói nào ⇒ 0 leaf trên cây**. Cây làm panel mặc định ⇒ nhóm HĐ này **không còn đường mặc định để thấy**; chỉ tìm ra nếu user gõ đúng từ khoá. Ở KHKK không có lỗ này vì mọi phiếu KHKK đều sinh từ PE.
⇒ W2 phải khai điều-kiện fallback ĐẦY ĐỦ: `!canPe` **HOẶC** search/lọc **HOẶC** (đề nghị) mặc định vẫn phẳng khi tập "HĐ ngoài gói" > 0. Và acceptance W2 phải có ca **"tạo 1 HĐ trực tiếp (không qua KHKK) → nó phải xuất hiện được ở panel trái"**.
### F-4 · MAJOR — W2 ⟂ Đ2 mâu thuẫn CHÉO: cây toàn trình **không lọc theo ContractType**, trong khi cả đợt này là để "mỗi loại đúng danh tính"
`usePipelineStages.ts:356-379` dựng stage-3 leaf từ tập id của gói, **0 vế lọc `c.type`** (đối chứng: stage-4 có đọc `c.type` nhưng chỉ để dựng URL `:389`). Cây gốc còn đứng ở trục Năm → Dự án → Gói thầu (ảnh-1), tức trục **gói thầu**, không phải trục **loại HĐ**.
⇒ Sau W2, `/my-contracts?type=1` (leaf "01. HĐ Thầu phụ → Danh sách") sẽ hiện panel trái chứa HĐ **của cả 7 loại**. Điều đó phá đúng thứ mà Đ2/Đ3 (W1) vừa dựng, và header trang vẫn ghi "HĐ của tôi · HĐ Thầu phụ (n)" ⇒ nhãn nói dối nội dung.
Khuôn KHKK **có xử vế này**: panel-1 giữ `Select` nhóm ngay trên cây (`KhkkListPanel.tsx:19-20` khai trade-off "folder GĐ2 hiện CARD PHIẾU thay 8 ngăn N1-N8; **lối vào nhóm giữ ở `Select` nhóm ngay bên trên**"). Spec cho HĐ **không có** vế tương ứng.
⇒ Cần chốt 1 trong 2 và ghi vào spec: (a) lọc stage-3 leaf theo `type` khi host truyền `typeFilter`; hoặc (b) giữ cây full + bắt buộc có ô chọn loại/`Select` như KHKK. **Đây là câu nên hỏi anh** (xem §Đề nghị, Q2).
### F-5 · MAJOR — Đọc-ý Thứ-1 chốt một chiều: mũi tên là "dời cây sang trái", nhưng "cột danh-sách biến thành fallback" là **suy diễn của đội**, không phải lời anh
`anh-mo-ta.md:26-30` ghi trung thực: mũi tên đỏ từ khung CÂY (phải) chỉ SANG khung danh-sách (trái) + lời anh *"Bỏ phần này sang bên này nhé"*; và tự đánh dấu *"Danh-sách HĐ đi đâu … = lane invest đối chiếu khuôn KHKK rồi đề xuất"*. Invest chọn "flat-list = fallback trong CÙNG panel" bằng lập luận khuôn KHKK — hợp lý, **nhưng nó XÓA một khối UI mà anh không nói xóa**, trong khi cách đọc thứ hai (**HOÁN ĐỔI**: cây sang trái, danh-sách sang phải/giữa) cũng khớp y hệt mũi tên và **không mất gì**.
Cộng với F-3 (HĐ ngoài gói vô hình) + F-4 (cây không theo loại), phương án "list thành fallback" là phương án **mất chức năng nhiều nhất** trong 3 cách đọc. ⇒ **Phải hỏi anh trước khi khoá** (Q1), hoặc chọn mặc định an toàn: dời cây sang trái, **giữ danh-sách** ở panel giữa/phải và để panel-3 dashed khi chưa chọn.
### F-6 · MAJOR — Đ4 bị THU HẸP: lời anh *"Phần thao tác, duyệt, xóa **...** → Cấu trúc tương tự như KHKK"* bị spec cắt còn 3 món, 3 món KHKK-có/HĐ-thiếu bị đẩy ra "ngoài wave" **không đánh số**
Invest `:91` đo được 3 món cố-ý-không-port: ① ô-tích "Cấp này KẾT THÚC" (`skipToFinal`) ② badge "⚑ Duyệt thay CEO" ③ banner kết-thúc-sớm + meta `{code} v{NN}` — cả 3 đều thuộc đúng chữ **"duyệt"** trong câu của anh, và đều là **hành-vi màn duyệt KHKK trên prod** (chuẩn S176). Spec đẩy xuống dòng cuối `:100-103` dưới nhãn "Ngoài wave — CHỜ ANH (không tự làm)" — nhưng **không có câu hỏi nào được đánh số để anh trả lời**, nên thực tế nó sẽ rơi im lặng đúng như bài `feedback_absence_looks_like_clean`.
Thêm: ảnh-3 mà anh gửi có đúng ô **"Duyệt là KẾT THÚC quy trình (không trình tiếp CEO)"** (chữ xanh) và ô **"Cho phép duyệt thẳng Cấp cuối"** trong dialog Sửa-tại-chỗ (`anh-mo-ta.md:57-58`) ⇒ anh **đang nhìn thấy** 2 cờ đó trong Admin. Cờ bật được ở Designer mà màn duyệt HĐ không đọc = cấu hình câm (đúng lớp `feedback_guard_built_but_never_called`). ⇒ Nâng thành **Q3 đánh số**.
### F-7 · CRITICAL — W5 mirror NHẦM MODULE: khuôn KHKK (thứ anh chỉ định) **CẤM approver xóa phiếu đang duyệt**; cờ `AllowApproverDelete` là của PE, KHKK chưa từng dùng
Đo BE, không suy:
- `ContractSigningPlanFeatures.cs:1763-1766` — chỉ **Drafter hoặc Admin** được xoá (`ForbiddenException`), không có nhánh approver.
- `:1768-1783` — allow-list **{DangSoanThao, TuChoi, TraLai}**, và comment tự khai nguyên văn: *"**Xoá phiếu ĐANG duyệt vẫn cấm** (cần rào đúng-lượt như PE)"*, kèm dấu tích S177 *"anh: Trạng thái trả lại cho xóa đc luôn nhé"*.
- `grep -rn "AllowApproverDelete" src/Backend` = 27 hit, **0 hit trong `ContractSigningPlans/`** (hit nằm ở PE + `ApprovalWorkflowV2AdminFeatures` + Domain/Infra/migration). Control dương: PE có đủ cả 2 đầu (`PeSoftDeleteFeatures.cs:110-114` + `PeWorkflowPanel.tsx:129-139` + endpoint `/purchase-evaluations/{id}/by-approver` `:297`).
⇒ Spec `:53` viết "mirror khuôn PE prod" cho một điều chỉnh mà **lời anh nói "tương tự như KHKK"**. Theo luật S176 (đã-deploy = CHUẨN) + đúng chữ của anh, đích đồng-khuôn là **KHKK**: xoá được ở **Nháp / Trả lại / Từ chối**, **cấm** xoá khi đang duyệt. Mở thêm đường approver-delete = **nhập tính năng của PE**, vượt cả khuôn được trích lẫn câu của anh, và nó đụng **đường xoá dữ liệu** ⇒ phải có chữ "đồng ý" của anh trước.
🟢 Vế còn lại của W5 thì **đúng và cần**: `ContractFeatures.cs:896` so `Phase >= DangInKy` với `DangInKy = 5` (`ContractPhase.cs:20`) trong khi V2 dùng `ChoDuyet=10 · TraLai=98 · TuChoi=99` (`:25-27`) ⇒ hiện **cả 3 trạng thái V2 đều bị chặn xoá**, kể cả phiếu vừa bị Trả lại — lệch đúng cái mà anh đã chốt cho KHKK ở S177.
### F-12 · CRITICAL — Bằng chứng "KHKK có nút Xóa ở màn duyệt" trỏ vào **CODE CHẾT**; anchor `:118` thật ra là dòng khai ĐÃ GỠ nút đó
Invest `:90` (và spec `:60-61`) căn cứ: *"KHKK panel mục 4 `KhkkWorkflowPanel.tsx:12-14,118,233`"*. Mở ra:
- `:117-118` nguyên văn: *"🧊 [S175] `isDraftLike` / `canSubmit` / `canDeletePlan` **ĐÃ GỠ** cùng 2 nút *Gửi duyệt* + *Xóa phiếu* — panel này nay chỉ lo màn ĐANG DUYỆT (Duyệt / Trả lại)."*
- `:438-440`: *"*Gửi duyệt* và *Xóa phiếu* **ĐÃ RỜI khỏi đây** → nay nằm ở THANH NÚT ĐÁY"*.
- `:12-14` là **mục lục comment đầu file chưa cập nhật** (vẫn liệt "4 Nút … + nút Xóa phiếu") — cite đúng dòng nhưng dòng đó nói dối.
- Dialog `confirmDelete` còn ở `:742-750` nhưng **`setConfirmDelete(true)` = 0 hit toàn file** (chỉ có `useState(false)` `:107` và 3 chỗ set `false`) ⇒ **không có đường bấm tới**. Nút Xoá thật của KHKK nằm ở host: `KhkkDetailContent.tsx:1077-1096` (thanh nút đáy, `window.confirm`, hướng tới người soạn).
⇒ Câu "KHKK CÓ mà GĐ3 THIẾU" là **so sánh với một khối code không chạy**. Điều đúng còn lại: `ContractWorkflowPanel.tsx` **0 hit** mọi biến thể "Xóa/Delete" — HĐ thiếu **đường xoá phía người soạn ở màn 3-panel**, và đó mới là món nên port (từ `KhkkDetailContent`, không phải từ `KhkkWorkflowPanel`).
📌 Bài chung: đây là ca `feedback_stale_anchor_born_dead` + `dry-run bắt lời-hứa-chưa-land` — spec đang mang 1 anchor tự-vô-hiệu vào wave.
### F-7b · minor — Nếu port đường xoá theo KHKK thì phải port cả **vết audit**, spec chưa nhắc
KHKK ghi `ContractSigningPlanChangelog` (Action=Delete, PhaseAtChange, Reason) và **bắt buộc thứ tự `Remove()` TRƯỚC, `Add(changelog)` SAU** (`:1787-1803`, bug T26 S155 — làm ngược thì cascade `Immediate` DETACH row changelog ⇒ ghi 0 dòng, **im lặng**). Contract hiện xoá trần: `ContractFeatures.cs:891-901` — 0 changelog, 0 `Reason`. Wave nào nới đường xoá cũng phải mang theo vế này, kèm cùng cái bẫy thứ-tự.
🟢 **Câu hỏi treo ② của lead (soft-vs-hard delete) TRẢ LỜI ĐƯỢC NGAY**: `Contract : AuditableEntity` (`Contract.cs:8`) + `AuditingInterceptor.cs:54-63` (đổi `EntityState.Deleted` → `Modified` + `IsDeleted/DeletedAt/DeletedBy`) ⇒ `db.Contracts.Remove(entity)` (`:899`) **đang là SOFT-DELETE**, khớp màn `/contracts/deleted`. Chốt vào spec để wave khỏi mở lại — và để không ai "sửa" `Remove` thành đường khác.
### F-8 · minor — Acceptance W1(iv) đo được, control-dương đã kiểm
`grep -rn "workflow-matrix?type=3" fe-user/src` hôm nay = **1 hit** (`Layout.tsx:151`) ⇒ phép đo có răng (không phải 0-hit sẵn). Control dương `workflow-matrix?ct=` cũng sẽ ≥1 sau sửa. ✅ Nhưng lưu ý: nếu theo F-1 mà GIỮ `type=3`, thì **acceptance (iv) phải viết lại** — bằng không nó ép implementer làm đúng cái sai.
### F-9 · minor — Đếm "17 leaf" của W4 ĐÚNG (đã truy `AwV2_Contract`)
Nhánh `code === 'Contract'` trong `fe-admin/src/components/Layout.tsx:178` là **nhánh chết**: `grep -rn "AwV2_Contract" src/Backend` = **0 hit** (không có const trong `MenuKeys.cs`, không seed) ⇒ không có menu-row ⇒ sidebar sau W4 = 2 + 8 + 7 = **17**. Acceptance W4(i) giữ nguyên được.
Cơ chế W4 cũng khớp: menu-key `AwV2_Hd{token}` (`MenuKeys.cs:151`, seed `DbInitializer.cs:2303-2310` Order 11..17) ⟂ `contractWfTypeCode('HD-TP') = 'HdTP'` (`contractWorkflowFamily.ts:69,77-80`) ⇒ `findContractWfByTypeCode(code)` khớp đúng phần sau `AwV2_`. **W4 là wave lành nhất — nên chạy TRƯỚC/song song thật sự.**
### F-10 · minor — "flat-list fallback **server-side**" là delta ngầm so hiện trạng (nhưng khả thi FE-only)
`MyContractsPage.tsx:73-88` đang tìm **client-side** trên 100 dòng đã tải. Muốn đúng khuôn R-11 (KHKK tìm đi server) thì phải nối `q` → param `search` của `/contracts` — param **có sẵn** (`ContractsController.cs:19` + `ContractFeatures.cs:358`), nên vẫn FE-only. Spec nên nói thẳng "đổi ô tìm sang server-side" thay vì để chữ "server-side" trôi trong ngoặc, kẻo implementer giữ nguyên client-side rồi vẫn tick ✅ acceptance (iii) "gõ search → flatMode".
### F-11 · minor — Mirror 2 app: 3 cặp file ĐANG identical ở baseline ⇒ acceptance SHA có nghĩa
Đo `sha256sum` (16 ký tự đầu): `usePipelineStages.ts` `1b9b5c61…` (user ≡ admin) · `PipelineTreePanel.tsx` `4ffc96be…` (≡) · `ContractWorkflowPanel.tsx` `9cdf9ffe…` (≡). `MyContractsPage.tsx` **chỉ có ở fe-user** — đúng như spec khai.
2 chỗ spec nói "KHÔNG mirror" cũng đúng: `contractWorkflowFamily.ts` tự khai fe-admin-ONLY (`:16-18`) và fe-admin ẩn `Ct_*` (`Layout.tsx:246-248` `key.startsWith('Ct_')`). ✅ Không có finding — ghi lại để lượt sau khỏi đo lại.
---
## Đề nghị sửa spec
_(điền cuối lượt)_
**Sửa BẮT BUỘC trước khi mở wave (3):**
1. **§2 Đ2+Đ3** — bỏ chỉ đạo "bỏ `type=3` khỏi URL"; giữ `?type=3&ct=N` (khuôn KHKK `Layout.tsx:216`), chữa bug A bằng nhánh accordion (ưu tiên `ct` + loại trừ pathname `/contracts/workflow-matrix`). Sửa kèm acceptance W1(iv). [F-1]
2. **§2 Đ4-W3** — thay "BE param `pendingMe` sẵn" bằng khuôn PE thật: `pendingMe=1` ⇒ gọi `/contracts/inbox` (0-param) + lọc `type` client-side; hoặc khai W3 CÓ delta BE. Siết acceptance: 1 HĐ không-đến-lượt phải VẮNG. [F-2]
3. **§2 Đ1-W2** — bổ sung M-1 (`!canPe` ⇒ phẳng) + ca "HĐ tạo trực tiếp, ngoài gói" vào cả cách-làm lẫn acceptance; nói rõ ô tìm chuyển server-side. [F-3, F-10]
4. **§2 W5** — viết lại theo khuôn **KHKK** chứ không phải PE: (a) nới guard `ContractFeatures.cs:896` thành allow-list **{DangSoanThao, TraLai, TuChoi}** cho **Drafter/Admin** (đúng `ContractSigningPlanFeatures.cs:1763-1783`), (b) port nút Xoá vào chỗ tương đương `KhkkDetailContent.tsx:1077-1096` — **KHÔNG** lấy `KhkkWorkflowPanel` làm khuôn (code chết), (c) kèm changelog + thứ-tự `Remove` trước `Add`, (d) **tách hẳn** phần approver-delete + `AllowApproverDelete` ra thành mục chờ anh duyệt (Q4). Acceptance phải có ca **"ChoDuyet ⇒ VẪN CẤM xoá"** — đúng chiều khuôn KHKK, ngược hẳn acceptance hiện tại `:94-96`. [F-7, F-12, F-7b]
**3 câu hỏi ĐÁNH SỐ nên trình anh (thay cho mục "ngoài wave" hiện đang không có số):**
- **Q1 (Thứ-1):** Ý anh là **hoán đổi 2 khung** (cây sang trái, danh-sách chuyển sang giữa/phải) hay **bỏ hẳn khung danh-sách**, để danh-sách chỉ hiện khi tìm/lọc như trang KHKK? *(Cây hiện chỉ chứa HĐ sinh từ Duyệt NCC/KHKK — HĐ tạo trực tiếp sẽ không nằm trên cây.)*
- **Q2 (Thứ-1 × Thứ-2):** Khi anh bấm "01. HĐ Thầu phụ → Danh sách", khung trái nên **chỉ hiện HĐ Thầu phụ**, hay hiện cây đầy đủ mọi loại (như cây hiện nay) và thêm ô chọn loại giống trang KHKK?
- **Q3 (Thứ-4):** Màn duyệt HĐ có cần **3 món KHKK đang có** không: ô "Cấp này KẾT THÚC quy trình", nhãn "Duyệt thay CEO", và dòng báo "kết thúc sớm ở Bước X"? *(Hai cờ này anh đã thấy trong dialog Sửa-tại-chỗ ở Ảnh 3; bật ở Admin mà màn duyệt HĐ chưa đọc.)*
- *(Giữ nguyên câu ① của spec — thay/gỡ ghim `QT-HD-V2-001` — nhưng cũng nên đánh số Q4: nó là điều-kiện để lời "mỗi loại 1 quy trình" đúng tuyệt đối trên DỮ LIỆU, không chỉ trên UI.)*
**Thứ tự wave — đề nghị đổi:**
`W4` (5 dòng, rủi ro thấp nhất, đã verify khớp cơ chế) → `W1` (bug anh đau nhất) → **chốt Q1/Q2 rồi mới** `W2` → `W3` (phụ thuộc panel của W2: sau khi panel-1 thành cây thì "Đang duyệt" hiển thị ở đâu phải được định nghĩa) → `W5` (sau Q3, gộp cùng lượt nếu anh duyệt 3 món để khỏi mở `ContractDetail` DTO hai lần).
Lý do đổi: W2 là wave DUY NHẤT có quyết-định-của-anh chưa chốt (Q1/Q2) — để nó ở giữa chuỗi sẽ chặn W3.

View File

@ -4,12 +4,246 @@ Reviewer lane-3/3 (ensemble /fable-clone). Lăng-kính: **acceptance ô-nào-gã
## Verdict
_(điền sau khi đo)_
**FAIL** — 2 BLOCKER (tiền-đề spec SAI, làm đúng theo spec ⇒ đẻ regression nặng hơn bug đang chữa),
4 MAJOR, 4 MINOR. Không phải "sửa chữ" — W1 và W3 phải viết lại phần ② trước khi cho wave chạy.
## Findings (F-1..)
_(append từng finding ngay khi đo xong)_
### F-1 🔴 BLOCKER — W1: "trang tự suy applicableType=3 theo path" LÀ SAI. Bỏ `type=3` ⇒ leaf 7 loại HĐ hiện quy trình **Duyệt NCC**
Spec §② Đ2+Đ3: *"WfView → `/contracts/workflow-matrix?ct=N` (BỎ `type=3` khỏi URL — trang tự suy
applicableType=3 theo path, khuôn alias-path khkk sẵn)"*.
Đo trên đĩa:
- `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:57` `const rawType = Number(searchParams.get('type'))`
- `:61` `const typeInt = rawType === 1 || rawType === 2 || rawType === 3 || rawType === 10 ? rawType : 1`
**KHÔNG có một dòng nào map pathname → applicableType.** `pathname` (`:56`) chỉ dùng để dựng link
"xem tất cả" (`:131` ``${pathname}?type=${typeInt}``). Bỏ `type` khỏi URL ⇒ `searchParams.get('type')`
= `null` ⇒ `Number(null)` = `0` ⇒ rơi vào **fallback `: 1`** = ApplicableType **1 (Duyệt NCC)**.
Hệ quả cụ thể: leaf "Luồng duyệt" của cả 7 loại HĐ gọi `/approval-workflows-v2?applicableType=1`
(`:67`) → hiện **quy trình PE**; rồi nhánh lọc `ct` mới (`code === 'HD-*'`) lọc trên tập PE → **0 dòng,
trang trắng**. Cả 2 nhánh đều tệ hơn bug A/B đang chữa, và bản "hiện quy trình PE" trông *hợp lý* nên
qua mắt được người click thử.
Control-dương cho claim "khuôn alias-path khkk sẵn": leaf KHKK **VẪN truyền type tường minh** —
`fe-user/src/components/Layout.tsx:86` `'/khkk/workflow-matrix?type=10&group=1'` và `:216`
``/khkk/workflow-matrix?type=10&group=${group}``. Tức alias-path KHKK **không** chứng minh khả năng
tự-suy; nó chứng minh điều NGƯỢC LẠI. Đây là lỗi khớp-khuôn-chéo (mượn UI của khuôn A, gán năng-lực
của khuôn B) — cùng lớp lỗi S187.
Thêm: `Layout.tsx:151` **hiện tại đã có `ct`** — nguyên văn
``/contracts/workflow-matrix?type=3&ct=${typeInt}``. Nên delta thật của W1 chỉ là (a) trang ĐỌC `ct`,
(b) accordion ưu tiên `ct`. Việc "bỏ `type=3`" là nửa RỦI RO và **không cần** cho cả bug A lẫn bug B.
**Đề nghị:** xoá mệnh lệnh "BỎ `type=3`" khỏi §②. Giữ URL `?type=3&ct=N`. Nếu vẫn muốn bỏ thì spec
phải giao thêm việc: thêm map `pathname → applicableType` trong `WorkflowMatrixViewPage` **và** sửa
`:131` (link "xem tất cả" đang tự re-add `?type=`) **và** đổi luôn leaf KHKK cho nhất quán — 3 việc,
không phải 0.
### F-2 🔴 BLOCKER — W3: `pendingMe` KHÔNG tồn tại phía BE Contract. Acceptance W3 là ô "gõ đúng chữ là đạt"
Spec §② Đ4-W3: *"(khuôn PE `:176`; BE param `pendingMe` sẵn)"*.
Đo: `grep -rn "pendingMe" src/Backend/SolutionErp.Application/Contracts/ src/Backend/SolutionErp.Api/Controllers/ContractsController.cs`
= **0 hit**. Danh sách tham số `ContractsController.cs:16-27` có `page, pageSize, search, sortDesc,
phase, supplierId, projectId, type` — **không có `pendingMe`**.
Control-dương (chứng thước không hỏng): cùng token có hit ở `ContractSigningPlansController.cs` (KHKK)
và `fe-user/src/components/Layout.tsx:176` (PE) ⇒ 0-hit phía Contract là **0 thật**.
Vì sao đây là BLOCKER chứ không phải "thiếu 1 dòng": ASP.NET Core **im lặng bỏ qua** query-param không
bind được ⇒ `/my-contracts?type=N&pendingMe=1` sẽ trả **TOÀN BỘ** HĐ loại N (không lọc đến-lượt-tôi),
**HTTP 200, không lỗi**. Trong khi acceptance W3(i) là `grep "pendingMe=1" Layout.tsx ≥1` — **gõ chuỗi
vào URL là xanh**. Đúng lớp bẫy S171: ô đo HÌNH-DẠNG-CHUỖI, không đo HÀNH-VI.
**Đề nghị:** W3 đổi nhãn từ "FE nhỏ" → **BE + FE**; §② phải giao rõ: thêm `pendingMe` vào
`ListContractsQuery` + handler (khuôn precompute V2 `ResolveV2InboxIdsAsync` phía PE), hoặc thay bằng
route `/inbox?type=N` nếu inbox đã có lọc loại. Acceptance thay ô grep bằng ô so-số:
`GET /api/contracts?type=N&pendingMe=1` trả **ít hơn** `GET /api/contracts?type=N` với user có ít nhất
1 HĐ không-đến-lượt (control-dương: user Admin/ngoài luồng phải ra **0 dòng**, không phải cả list).
### F-3 🟠 MAJOR — W5 mirror khuôn PE mà bỏ mất 3 trong 4 cột trụ của khuôn: đường-TÁCH, policy, và VẾT changelog
Khuôn PE (`PeSoftDeleteFeatures.cs`) không chỉ là "3 điều kiện" như spec §② trích (`:110-114`):
1. **TÁCH ĐƯỜNG có chủ đích** — `:25-29` nói thẳng: đường xóa nháp cũ **GIỮ NGUYÊN không policy**, vì
gắn policy vào đó làm `Drafter` mất quyền xóa nháp của chính mình (*đo thật trên DB Dev: 11/13 role
`CanDelete=0`*). Đường mới là **endpoint RIÊNG** mang `[Authorize(Policy = "PurchaseEvaluations.Delete")]`.
Spec W5 lại đi hướng **NGƯỢC**: "nới delete-guard `ContractFeatures.cs:896-897`" = nhét cả 2 nghiệp
vụ vào MỘT handler `DeleteContractCommand` — đúng thứ khuôn PE đã cân nhắc rồi loại bỏ.
2. **VẾT changelog** — `:147-158` ghi `ContractChangelog`-tương-đương với `Action=Delete`, `PhaseAtChange`,
`UserId`, `ContextNote=lý do`. Spec W5 **không nhắc chữ changelog nào**. Đối tượng bị xóa ở đây là
**hợp đồng** (chứng từ pháp lý) mà lại xóa không vết, trong khi phiếu PE thì có vết. Hạ tầng đã sẵn:
`src/Backend/SolutionErp.Domain/Contracts/ContractChangelog.cs` tồn tại +
`IApplicationDbContext.cs:40 DbSet<ContractChangelog> ContractChangelogs`.
3. **Bẫy thứ-tự T26** — `:127-144`: phải `Remove()` **TRƯỚC**, `Add(changelog)` **SAU**; đảo lại thì
cascade client-side (`CascadeDeleteTiming.Immediate`) DETACH changelog ⇒ **ghi 0 row, im lặng, không
lỗi**. Nếu W5 có changelog mà spec không chép cảnh báo này sang, wave sẽ dẫm lại đúng bug đã trả giá.
4. Chỉ có "3 rào" là được mirror.
**Đề nghị:** §② W5 viết lại theo khuôn TÁCH ĐƯỜNG: command + endpoint riêng
(`DeleteContractByApproverCommand`, `POST/DELETE /api/contracts/{id}/by-approver`) mang policy, GIỮ
`DeleteContractCommand` cũ nguyên vẹn cho nháp; + ghi `ContractChangelog` với đúng thứ tự Remove→Add
(chép nguyên chú thích T26); + acceptance thêm ô: sau khi xóa, `GET` lịch sử đổi của HĐ đó phải còn
**đúng 1 dòng** action Delete (đọc NGƯỢC từ DB — nếu đọc ra 0 dòng là đã dẫm T26).
### F-4 🟠 MAJOR — W5 chạm đường DELETE đang **không có policy per-action** (#82) mà acceptance không có ô authz nào
`src/Backend/SolutionErp.Api/Controllers/ContractsController.cs:11-14` = `[ApiController] [Route] [Authorize]`
**trần** (không Policy). `:78-83` `[HttpDelete("{id:guid}")]` **không** có `[Authorize(Policy=...)]`.
Đối chứng ngay trong cùng file: màn "Đã xóa" (GET) lại tự khai *"policy root `Contracts.Read` (chặt hơn
`[Authorize]` trần của class)"* ⇒ tác giả cũ **biết** class-level là trần, và đã siết cho GET nhưng
**chưa siết cho DELETE**.
Nghĩa là hôm nay: **bất kỳ user đã đăng nhập nào** cũng xóa được HĐ ở phase < `DangInKy` (kể cả HĐ của
người khác — handler `ContractFeatures.cs:889-901` không đọc `ICurrentUser`, không kiểm chủ sở hữu).
W5 **nới** guard này ra phase `ChoDuyet/TraLai/TuChoi` ⇒ mở rộng đúng cái lỗ đó sang HĐ đang chạy duyệt.
Acceptance W5 (i)-(iv) **không có một ô nào** về authz.
**Đề nghị:** thêm ô acceptance test-before: (a) user thường (không role, không trong Cấp) `DELETE` HĐ
nháp của người khác ⇒ phải 403 (hôm nay 204 — ghi rõ đây là **sửa lỗ có sẵn**, không phải regression do
W5 đẻ ra); (b) đường xóa-bởi-approver có `[Authorize(Policy = "Contracts.Delete")]`; (c) grep chứng
policy đã gắn. Nếu owner muốn giữ nguyên hành vi cũ cho nháp thì phải khai **thành lời** trong spec
(accepted-risk), không để im lặng.
### F-5 🟠 MAJOR — W5 acceptance thiếu ca `DaPhatHanh(9)` — chỗ duy nhất mà "V2-aware" dễ mở nhầm
`ContractPhase`: `DangInKy=5 … DaPhatHanh=9, ChoDuyet=10, TraLai=98, TuChoi=99`
(`src/Backend/SolutionErp.Domain/Contracts/ContractPhase.cs`). Guard cũ `Phase >= DangInKy` chặn
**5→99** (chặn thừa V2, đúng như finding S160). Acceptance W5(i) liệt: TraLai/TuChoi cho xóa · ChoDuyet
có điều kiện · "V1 `Phase>=DangInKy` VẪN chặn".
Lỗ: `DaPhatHanh = 9` vừa nằm trong dải "V1 legacy ≥5" **vừa là terminal THÀNH CÔNG của V2** (chú thích
enum: *"last step done ──► DaPhatHanh (terminal + gen mã HĐ)"*). Một bản viết lại "V2-aware" kiểu
`if (v2) { allow ChoDuyet/TraLai/TuChoi/DangSoanThao } else { block >= DangInKy }` sẽ **không còn** ô
nào chặn V2-`DaPhatHanh` một cách tường minh — HĐ **đã duyệt, đã gen mã** có thể bị xóa. Không ô
acceptance nào bắt được.
**Đề nghị:** thêm ô bắt buộc: "HĐ V2 phase `DaPhatHanh(9)` (đã gen `MaHopDong`) ⇒ mọi đường xóa đều
409/403" + ô đối xứng "HĐ V1 phase `DangSoanThao(2)` vẫn xóa được như cũ" (chống vá quá tay).
### F-6 🟡 MINOR-nhưng-chắc-gãy — W1(iv): lệnh grep viết như trong spec **đo trượt** dưới ripgrep / `grep -E`
Acceptance W1(iv): `grep -rn "workflow-matrix?type=3" fe-user/src` = 0-hit + control `workflow-matrix?ct=` ≥1.
Chạy thật, cùng cây, cùng lúc:
- `grep -rn "workflow-matrix?type=3" fe-user/src` (BRE, `?` là ký tự thường) → **1 hit**
`fe-user/src/components/Layout.tsx:151`.
- `rg -n "workflow-matrix?type=3" fe-user/src` → **0 hit** (rg mặc định regex: `x?` = "x có thể vắng"
⇒ mẫu đòi chuỗi `workflow-matritype=3`/`workflow-matrixtype=3`, không có thật).
- `rg -n "workflow-matrix?ct=" fe-user/src` → **0 hit** — tức control-dương cũng gãy theo.
⇒ Nếu wave dùng `rg` (mặc định của Grep-tool) hoặc `grep -E`, ô W1(iv) **xanh ngay trước khi sửa gì**.
May là control-dương cũng 0 ⇒ người cẩn thận sẽ thấy thước hỏng; nhưng spec đang mời gọi đọc "control
0-hit" thành "chưa sửa xong" thay vì "thước hỏng".
**Đề nghị:** ghi lệnh có `-F`: `grep -rnF 'workflow-matrix?type=3' fe-user/src` (hoặc escape `\?`), và
ghi rõ **kỳ vọng của control** (`≥1 ngay từ trước khi sửa` — vì `ct=` ĐÃ có ở `:151` hôm nay).
### F-7 🟠 MAJOR — W1 đẻ **bản sao thứ BA** của họ `HD-*` trong fe-user, và acceptance chỉ phủ **1/7** mã
Spec §②: *"map ContractType→mã: const cục bộ 7-entry — fe-user KHÔNG mirror lib fe-admin"*.
Sau W1 sẽ có 3 bản danh sách họ: BE `src/Backend/SolutionErp.Domain/Contracts/ContractApprovalWorkflowFamily.cs`
(nguồn thật — seeder `DbInitializer.cs:775 foreach (…Family.Members)` sinh mã từ đây) ·
`fe-admin/src/lib/contractWorkflowFamily.ts` · const mới trong fe-user.
Lệch **CÂM**: sai 1 ký tự trong mã ⇒ `allWorkflows.filter(wf => wf.code === …)` ra **mảng rỗng** ⇒
trang trắng, trông y hệt "loại này chưa cấu hình quy trình". Chính lib fe-admin đã ghi cảnh báo này
(`contractWorkflowFamily.ts:2-8`) và còn ghi rõ bẫy chẻ chuỗi: *"`HdNTNCC` chẻ ra `NT-NCC` hay `N-TNCC`
là hai mã khác nhau"* (`:70-73`).
Acceptance W1(iii) chỉ đo **`ct=1` → `HD-TP`** = 1/7. Hai mã dễ sai nhất (`HD-NT-NCC`, `HD-NT-DV` —
token gạch nối) **không có ô nào**.
**Đề nghị:** đổi ô (iii) thành vòng 7 giá trị: với mọi `ct ∈ 1..7`, trang hiện **≥1** quy trình và
**0** dòng mã khác họ; ghi kèm control-dương `ct` rác (`ct=99`) ⇒ **không lọc** (giữ hành vi cũ, không
trang trắng) — đúng khuôn `group` rác đã chốt ở `WorkflowMatrixViewPage.tsx:82-83`.
### F-8 🟠 MAJOR — 9/20 ô acceptance là **hành-vi**, mà spec không ghi **đo ở đâu** khi CI hỏng P0 + deploy CHỜ
Ô hành-vi: W1(i)(ii)(iii) · W2(ii)(iii) · W3(ii) · W4(i)(ii) · W5(iii-render). Spec §① tự khai
*"DEPLOY CHỜ (CI runner workspace còn hỏng P0)"* ⇒ **không có prod để bấm**, và `npm run build` (ô
duy nhất chạy được) **không chứng minh gì** về hành vi. Đây đúng lớp S171: ô đọc thì hợp lý, tới lúc
chạy mới lộ là không có chỗ chạy ⇒ wave sẽ tick bằng suy luận.
Nấc đo local **có thật, rẻ** — nên ghi thẳng vào spec thành bước bắt buộc trước khi tick:
1. `dotnet run --project src/Backend/SolutionErp.Api` (LocalDB `SolutionErp_Dev`). Seeder chạy lúc
khởi động và **idempotent per-code** (`DbInitializer.cs:762-764,775`) ⇒ 7 `HD-*` tự có,
`IsUserSelectable = true` (`:797`) — thoả tiền-đề lọc `isUserSelectable: true` của trang
(`WorkflowMatrixViewPage.tsx:67`). Sample cũ `QT-HD-V2-001` (`:257`) vẫn còn ⇒ ô "0 dòng
QT-HD-V2-001" mới có ý nghĩa (nếu Dev sạch trơn thì ô đó xanh vì **vắng**, không phải vì **lọc**).
2. `npm run dev` ở `fe-user` (proxy `/api → localhost:5443`) — bấm đủ W1/W2/W3; `fe-admin` cho W4.
3. **Chuẩn bị DỮ LIỆU** (spec đang thiếu hẳn): W2(ii) cần ≥1 HĐ hiện trên cây GĐ3; W3(ii) cần ≥1 HĐ
`ChoDuyet` mà user đăng nhập **đúng lượt** + ≥1 HĐ **không** đến lượt (nếu không có vế thứ hai thì
ô "list khớp inbox" xanh cả khi lọc **không chạy** — xem F-2); W5(iii) cần 1 Cấp bật cờ
`AllowApproverDelete` và 1 Cấp tắt.
**Đề nghị:** thêm mục "④ Nấc đo local" vào spec với 3 bước trên; ô nào không đo được local thì đánh
dấu **DEFERRED-TO-DEPLOY** công khai, KHÔNG tick.
### F-9 🟡 MINOR-cao — W2 sửa cặp hook/panel **dùng chung cho GĐ1/GĐ2/GĐ4** mà không có ô hồi-quy
Đo baseline (bất biến mirror còn NGUYÊN hôm nay — tiền-đề W2(iv) HỢP LỆ):
`fe-user|fe-admin/src/hooks/usePipelineStages.ts` = `1b9b5c61…52ff48` (khớp) ·
`fe-user|fe-admin/src/components/pipeline/PipelineTreePanel.tsx` = `4ffc96be…c6c334` (khớp).
Nhưng cặp này là cây toàn-trình 4 GĐ (S162), fe-admin **cũng** render nó trong khi fe-admin **không có**
`MyContractsPage` ⇒ tuỳ chọn `onOpenContract` phải **mặc định TẮT**. Acceptance W2 chỉ có SHA-pair +
build — cả hai đều xanh y nguyên nếu opt-in lỡ thành opt-out (SHA vẫn khớp vì sửa **cả 2 bản**!).
**Đề nghị:** thêm ô control-âm: "trên fe-admin (không truyền `onOpenContract`), click leaf GĐ3 vẫn
`navigate('/contracts/{id}')` như cũ" + ô "leaf GĐ1/GĐ2/GĐ4 hành vi không đổi ở CẢ 2 app".
### F-10 🟡 MINOR-cao — W5: đường xóa HĐ hiện **0 test**, và acceptance không pin số test / không nhắc STATUS
- `grep -rl "DeleteContract" tests/` → chỉ trúng **file .dll** trong `bin/obj`, **0 file nguồn** ⇒
`DeleteContractCommand` chưa từng có test. File duy nhất lân cận là
`tests/SolutionErp.Domain.Tests/Contracts/WorkflowPolicyTests.cs` (có nhắc `DangInKy`) nhưng nó test
**policy chuyển phase**, không test guard xóa ⇒ **không có lưới nào** đỡ nếu W5 nới sai.
- Số canonical hiện tại: **680** (`docs/STATUS.md:6`, S187→S188 "test 668→680"). ⚠️ Dòng `:20` còn ghi
**644** của khối cũ hơn — người đo vội dễ lấy nhầm; spec nên ghi rõ mốc lấy từ `:6`.
- Acceptance W5(i) chỉ nói "`dotnet test` PASS + test mới …" — không pin **680 → 680+N**, không nhắc
cập nhật **DUY NHẤT** row test trong `docs/STATUS.md` (B1). Theo `docs/rules.md §7` guard mới =
test-before BẮT BUỘC (spec có ghi ✔) nhưng "PASS" là ô 0-bit nếu số không tăng.
**Đề nghị:** ô (i) ghi rõ: "trước: 680 · sau: 680+N (liệt N ca)" + "cập nhật đúng 1 row test ở
`docs/STATUS.md`, không chép số ra chỗ khác".
### F-11 🟡 MINOR — W2 đổi trục điều hướng, thiếu ô **tương-thích-ngược deep-link**
W2 chuyển chọn HĐ sang `?id=` trong `/my-contracts`. Đường cũ `/contracts/{id}` đang là đích của
`usePipelineStages.ts:377` **và** của link/thông báo cũ (bookmark, notification). Acceptance W2 không có
ô nào chứng đường cũ còn sống.
**Đề nghị:** thêm ô: "dán thẳng `/contracts/{id}` vào thanh địa chỉ ⇒ vẫn mở detail như cũ (không 404,
không redirect mất id)".
### Held — 5 tiền-đề tôi cố bẻ mà KHÔNG gãy (ghi để wave khỏi đo lại)
| # | Claim spec | Phép đo | Kết |
|---|---|---|---|
| H-1 | Cặp mirror ×2 app đang khớp (tiền-đề W2-iv/W5-iii) | `sha256sum` 4 file + 4 file panel | **HELD** — `usePipelineStages` `1b9b5c61…` · `PipelineTreePanel` `4ffc96be…` · `ContractWorkflowPanel` `9cdf9ffe…` · `KhkkWorkflowPanel` `e7b294c9…`, từng cặp KHỚP |
| H-2 | W4 một dòng `findContractWfByTypeCode(code)` đúng chỗ | đọc `fe-admin/src/components/Layout.tsx:175-189` | **HELD** — chèn *sau `:188`* nằm TRONG khối `awV2Match`, ở đó `code = awV2Match[1]` = phần sau `AwV2_` (`HdTP`…), đúng thứ `findContractWfByTypeCode` tra (`contractWorkflowFamily.ts:69,77-80`). Ngoài khối là sai — spec nên ghi "trong khối `if (awV2Match)`" cho chắc |
| H-3 | Designer/route đã sẵn cho `:typeCode` họ HĐ | `App.tsx:80` route `:typeCode` + `ApprovalWorkflowsV2Page.tsx:39,314` gọi `findContractWfByTypeCode` | **HELD** — W4(ii) khả thi |
| H-4 | `view=approved` chữa được bug B (2 leaf cùng sáng) | đọc `queryMatches` `fe-user/Layout.tsx:350,368-374` | **HELD** — `view` KHÔNG nằm trong `TRANSIENT_QUERY_KEYS`, `phase` chỉ được bỏ qua khi **đích không ghim** ⇒ đích "Danh sách" `[type]` không còn khớp URL `[type,phase,view]`. Đúng khuôn PE `:180-185` |
| H-5 | W4 "17 leaf" | `MenuKeys.cs:118-119` (2 NCC) + `:129-136` (8 KHKK) + `:151` `AwV2_Hd{token}` × 7 thành viên họ | **HELD** — 2+8+7 = 17 |
| H-6 | Lead-note "soft hay hard delete?" | `AuditingInterceptor.cs:56-62` | **ĐÃ TRẢ LỜI, đừng để wave đi đo lại:** `Contract : AuditableEntity` (`Contract.cs:8`) ⇒ interceptor đổi `Deleted → Modified` + set `IsDeleted/DeletedAt/DeletedBy` ⇒ `db.Contracts.Remove` **là XÓA MỀM**, giống hệt PE. Màn `/contracts/deleted` an toàn. **Nhưng** vẫn dính bẫy T26 nếu W5 thêm changelog — xem F-3 |
## Đề nghị sửa spec
_(điền cuối)_
Theo thứ tự phải-làm-trước:
1. **§② Đ2+Đ3: xoá mệnh lệnh "BỎ `type=3` khỏi URL"** (F-1). Giữ `?type=3&ct=N`; delta của W1 =
trang đọc `ct` + accordion ưu tiên `ct` + comment stale. Nếu owner vẫn muốn URL sạch thì spec phải
liệt thêm 3 việc (map pathname→type · sửa link `:131` · đồng bộ leaf KHKK) và nhận rủi ro.
2. **§② Đ4-W3: bỏ chữ "BE param `pendingMe` sẵn"** (F-2) — đổi W3 thành wave **BE+FE**, hoặc chuyển
sang `/inbox?type=N` nếu inbox đã lọc loại. Ô grep thay bằng ô so-số 2 chiều.
3. **§② W5: viết lại theo khuôn TÁCH ĐƯỜNG của PE** (F-3): command/endpoint riêng + policy
(`Contracts.Delete`) + `ContractChangelog` với thứ tự `Remove()` → `Add()` (chép nguyên cảnh báo
T26) + GIỮ `DeleteContractCommand` cũ cho nháp.
4. **§③ W5 thêm 3 ô:** authz (403 cho người ngoài luồng — F-4) · `DaPhatHanh(9)` vẫn chặn (F-5) ·
changelog đọc-ngược ra đúng 1 dòng (F-3).
5. **§③ W1(iv): `grep -rnF` (hoặc `\?`)** + khai kỳ vọng control-dương là "≥1 **ngay từ trước** khi
sửa" (F-6); **W1(iii) mở thành vòng `ct ∈ 1..7`** + ca `ct` rác (F-7).
6. **§ mới "④ Nấc đo local"** — `dotnet run` + `npm run dev` + 3 dòng chuẩn-bị-dữ-liệu; ô nào không đo
được thì đánh **DEFERRED-TO-DEPLOY**, cấm tick (F-8).
7. **§③ W2 thêm ô control-âm fe-admin + ô deep-link `/contracts/{id}`** (F-9, F-11).
8. **§③ W5(i) pin số test 680 → 680+N + nhắc cập nhật đúng 1 row `docs/STATUS.md`** (F-10).

View File

@ -0,0 +1,20 @@
# SUB — back-end-reviewer-style — B3 SPEC-LIST STYLE BE (YC-029, run SOLTUON-12-08-2026)
> Engine: /fable-real single deep-pass · **propose-only** — lead verify rồi mới dùng.
> Đề: SPEC-LIST STYLE để worker Opus MAX (B6) viết W5 (DeleteContract allow-list + `[Authorize(Policy)]` + test-before) **KHÔNG lệch khuôn bản ĐÃ DEPLOY** (luật owner S176: đã deploy production = CHUẨN).
## Khuôn nguồn BE
(đang đọc — append dần)
## SPEC-LIST STYLE BE (SB-1..)
(append sau khi đọc khuôn)
## Bẫy style BE repo này
(append)
## Checklist style ĐO ĐƯỢC cho worker
(append)

View File

@ -0,0 +1,20 @@
# sub-style-fe-b3 — SPEC-LIST STYLE FE (YC-029 B3, /fable-real deep-pass)
> Vai: front-end-reviewer-style · Run: SOLTUON-12-08-2026 · Trạng thái: ĐANG GHI (append từng phần)
> Luật lõi owner S176: bản ĐÃ DEPLOY PROD = CHUẨN. Spec-list này = luật style ĐO ĐƯỢC cho worker Opus (B6) viết wave W1-W5 KHÔNG lệch khuôn.
## Khuôn nguồn (file nào là chuẩn cho việc gì)
(đang ghi)
## SPEC-LIST STYLE FE (đánh số SF-1..)
(đang ghi)
## Bẫy style đã thấy trong repo
(đang ghi)
## Checklist style ĐO ĐƯỢC cho worker
(đang ghi)