wal: flush 20260812T1824

This commit is contained in:
pqhuy1987
2026-08-12 18:24:29 +07:00
parent a0339babe2
commit 036ad3b603
8 changed files with 718 additions and 27 deletions

View File

@ -18,9 +18,13 @@ chain:
[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). [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 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) 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). [x] B4 4-lane: fe1 FIXES-11 (bỏ sót khuôn ContractsListPage + gate typeInt===3) · fe2
wf: wf_6b7f00c2-c26 · artifact sub-style-review-b4-{fe1,fe2,be1,be2}.md FIXES-12 (eslint baseline ĐỎ 47/42 ⇒ ô delta; 4 false-green fault-injected) · be1 CHẾT
[ ] B5 /fable-real reviewer — tổng hợp cuối → plan wave cụ thể "SOLTUON-12-08-2026" non (be2 phủ anchor 9/9 — khai, không relaunch) · be2 FIXES-15 (style-list tự đánh nhau
spec v2.1 → LẬT; BS-3 số thật 12/13 CanDelete=0; T-matrix 13 ca; ghost-wire
AuditBehavior). → STYLE-GATE-V2 land (style-gate-v2.md, delta-thắng-nền) + spec v2.2.
[!] B5 /fable-real reviewer — tổng hợp cuối → plan wave FINAL. wf: wf_bc763d7d-aca ·
artifact sub-b5-plan-final.md (mâu-thuẫn-chéo 3 tầng + plan per-wave + Đ-dừng)
[ ] B6 hmw fan-out Opus 5 MAX execute toàn plan (per-wave gate + cicd 2 app + prod verify) [ ] 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 · [!] TỒN CŨ — CỬA NGOÀI-GIỜ (anh gật giờ): dọn workspace runner + dispatch verify CI ·
(3B) Gitea cron + compact Vietreport_Master + báo VIETREPORT · đo lại A/059 (3B) Gitea cron + compact Vietreport_Master + báo VIETREPORT · đo lại A/059

View File

@ -97,3 +97,19 @@ HEAD lúc mở run: ae1dcf4a · HMW-mode ON · lead Opus 5 MAX (default)
2 biến thể grep/rg) · `WF be-style b4-anchor s190` (soi cả SB-3 policy thành thừa sau 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). 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. Artifact: sub-style-review-b4-{fe1,fe2,be1,be2}.md.
- **B4 KẾT QUẢ** (101 tool-use · 588K · ~15'): fe1 PASS_WITH_FIXES-11 (2 MAJOR: bỏ sót
khuôn `ContractsListPage.tsx:141,265` [bodyMaxHClass] · SF-21 thiếu vế gate typeInt===3;
0 anchor chết, 0 luật tự-chế) · fe2 PASS_WITH_FIXES-12 (BLOCKER C-1 eslint baseline ĐỎ
47/42 → ô DELTA; C-3/C-7/C-13 false-green CHỨNG bằng fault-inject cây tạm; C-9 0-răng
drift 7 token → -oE sort-u) · **be1 CHẾT NON** (skeleton 3-heading — #53 class; be2 phủ
anchor chồng lấn 9/9 SỐNG ⇒ lead chấp nhận không relaunch, KHAI) · be2 PASS_WITH_FIXES-15
(🔴 R-1 style-list ĐÁNH NHAU spec v2.1 [SB-13/14/18+C10/C13 còn đòi policy] → LẬT ·
R-2 BS-3 xác nhận SỐ THẬT: `Contracts` 12/13 role CanDelete=0, Drafter=0, control
KHKK Drafter=1 · R-7 ca "V1-nháp-2" TRÙNG DangSoanThao(2) → thay phase-3 legacy ·
R-13 ghost-wire comment `AuditBehavior` — file KHÔNG tồn tại · T-matrix 13 ca + phản-ca).
**STYLE-GATE-V2** land (`style-gate-v2.md` — delta-thắng-nền + checklist thay-thế
fault-inject-proof + T-matrix) + spec **v2.2** trỏ sang.
- **B5 phóng** `wf_bc763d7d-aca` — /fable-real reviewer tổng hợp cuối:
label `WF reviewer b5-tonghop s190` — rà mâu-thuẫn CHÉO 3 tầng {spec v2.2 ⟂ style-gate-v2
⟂ T-matrix} + PLAN FINAL per-wave worker-cầm-làm-được + điều kiện dừng/leo-thang Đ-1..
Artifact: sub-b5-plan-final.md.

View File

@ -82,9 +82,10 @@ Tổng: **W4/W1/W2/W3 FE-only · W5 BE nhỏ + FE (0 migration)**. DEPLOY CHỜ
`Guid?` khuôn `PeSoftDeleteFeatures.cs:56-58` (BS-6); `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, (e) xác nhận sẵn: `Remove` = SOFT-delete qua `AuditingInterceptor.cs:54-62` (H-6 wave chỉ assert,
đừng đo lại). đừng đo lại).
**STYLE-GATE cho B6:** worker MỌI wave phải theo 2 spec-list style đã land cùng folder **STYLE-GATE cho B6 (🔄 v2.2 sau B4):** nguồn = **`style-gate-v2.md`** (delta THẮNG nền; nền =
`sub-style-fe-b3.md` (SF-1..25 + checklist C-1..C-13) + `sub-style-be-b3.md` (SB-1..20 + 18 ô C) `sub-style-{fe,be}-b3.md`). Checklist ô đo CHỈ dùng bảng trong style-gate-v2 (bảng B3 gốc bị B4
lệch luật nào phải kèm trích lời anh cho phép, không = FLAG khi gate. chứng false-green/dương-giả bằng fault-inject). Ca test W5 = **T-matrix T-1..T-13 + phản-ca
T-14** trong style-gate-v2 §C (thay danh sách ca W5(i) ca "V1 nháp 2" đã bị chứng TRÙNG).
FE: nút "Xóa phiếu" **thanh nút đáy `ContractDetailContent`** (port khuôn 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 đó `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. CODE CHẾT S175), hiện khi phase allow-list + user Drafter/Admin; mirror ×2 app SHA-pair.

View File

@ -0,0 +1,117 @@
# STYLE-GATE v2 — bản HỢP NHẤT sau B4 (lead synthesize, 2026-08-12)
> **Cách đọc (B1 trỏ-nền + delta):** nền = `sub-style-fe-b3.md` (25 SF + 10 bẫy) + `sub-style-be-b3.md`
> (20 SB + 7 BS) — VẪN LÀ LUẬT, **trừ các delta dưới đây (delta THẮNG nền)**. Checklist ô đo: DÙNG
> BẢNG Ở ĐÂY (bảng trong 2 file B3 đã bị B4 chứng có false-green/dương-giả — giữ làm vết, không chạy).
> Nguồn vá: `sub-style-review-b4-fe1.md` (11 R) · `-fe2.md` (12 R, fault-inject cây tạm) ·
> `-be2.md` (15 R + T-matrix + đo LocalDB). Lane be1-anchor chết non (skeleton) — phần anchor BE được
> be2 spot-check chồng lấn **9/9 khuôn SỐNG**, lead chấp nhận không relaunch (khai).
## A. DELTA luật FE (đè lên SF tương ứng)
1. **[fe1-R1] +Khuôn bị bỏ sót:** `fe-admin/src/pages/contracts/ContractsListPage.tsx:141` grid
`[340px_1fr_360px]` + `:265` `{!selectedId && <PipelineTreePanel currentStage={3}
bodyMaxHClass="max-h-[28rem] lg:max-h-none" />}` — đây là khuôn GĐ3-prod ĐỜI TRƯỚC (KhkkListPage
tự khai copy từ nó). **Luật mới SF-26:** W2 đặt cây vào panel host PHẢI quyết `bodyMaxHClass`
tường minh (mặc định của panel đẻ "2 cuộn lồng nhau" — `PipelineTreePanel.tsx:112-115`).
2. **[fe1-R2] SF-21/SF-8 bổ vế GATE:** khuôn thật `WorkflowMatrixViewPage.tsx:85-86`
`typeInt === 10 && rawGroup !== null && /^[1-8]$/`. Nhánh `ct` mới PHẢI `typeInt === 3 &&
rawCt !== null && /^[1-7]$/` — thiếu vế `typeInt === 3` thì `?type=1&ct=5` lọc HD-* trên bộ
non-HĐ = trang trắng.
3. **[fe1-R3] SF-24 literal sửa:** câu khuôn thật là **"hông truyền ⇒ …"** (6 hit) — gate đo theo
NGHĨA: JSDoc opt-in phải có mệnh đề "(kh|h)ông truyền ⇒ giữ nguyên/behavior cũ", regex
`grep -cE "hông truyền"` ≥1 per prop mới.
4. **[fe1-R4] SF-14 sửa tên:** Select của repo = **wrapper `<select>` NATIVE** (`ui/select.tsx:4-19`,
0 hit radix) — CẤM kéo `@radix-ui/react-select` mới.
5. **[fe1-R5] SF-15 class header đầy đủ:** `flex shrink-0 flex-wrap items-center justify-between
gap-3 border-b border-slate-200 bg-white px-6 py-3` (`KhkkListPage.tsx:157`).
6. **[fe1-R6/R-7] số đính chính:** SF-10 = **11** mutation vùng `:243-367` · Bẫy-1 anchor
`removePlan :230-240`, chứng cứng hơn: `setConfirmDelete(true)` **0 caller** · Bẫy-9 sửa:
`grep 'lũy kế'` KHKK nay = **1 hit** và hit đó là CHÍNH comment khai việc cắt (citation-trap —
đọc hit phải xem enclosure).
7. **[fe1-R9] C-6 thêm ô dương:** sau W1, `grep -cE "7 bộ|per-loại|S188"
fe-user/src/components/Layout.tsx` ≥1 (chống xoá-trắng-comment).
## B. DELTA luật BE (đè lên SB tương ứng)
1. **[be2-R1 BLOCKER] SB-13/SB-14/SB-18 + C10/C13 LẬT theo spec v2.1:** SB-13 → *"GIỮ shape hiện
tại: class `[Authorize]` trần, action DELETE **KHÔNG** policy — `[carry:w8-delete-policy]`"* ·
SB-14 → comment khai VÌ SAO CHƯA gắn (dẫn chốt (82) + BS-3) · **SB-18 BỎ** (reflection-test
policy) · C10 kỳ vọng **=0** · C13 bỏ, KHÔNG viết test âm pin-lỗ (phản-ca T-14).
2. **[be2-R2] BS-3 nâng thành LUẬT CẤM có số:** key `Contracts` đo LocalDB Dev = **12/13 role
`CanDelete=0`, Drafter=0** (control: `KeHoachKyKet` Drafter=1) ⇒ gắn policy đợt này = khoá oan
CHẮC. Điều kiện mở W8: policy + grant data cho Drafter **CÙNG commit**; trước W8 đo lại **prod**
(Dev ≠ prod — admin có thể đã sửa tay).
3. **[be2-R13] +Cảnh báo GHOST-WIRE cạnh K7:** comment `ContractChangelog.cs:10-13` nói "AuditBehavior
tự populate" — **file KHÔNG TỒN TẠI** (0 hit ngoài chính comment). Changelog là GHI TAY (2 site
hiện có: `CreateContractFromSigningPlanFeatures.cs:247` · `CreateContractFromEvaluationFeatures.cs:121`)
⇒ W5 PHẢI `Add()` tay, đừng tin comment.
4. **[be2-R14] SB-15 đổi khuôn HÌNH:** fixture theo **Contract-family** (`ContractSigningPlanCrudTests.cs:77`
`KhkkFixture` / `ContractCatalogTests.cs:83` — 5 fixture private-per-file cùng nếp, dựng cái thứ 6
là ĐÚNG khuôn); K12 PE giữ làm nguồn LÝ-DO. Fixture PHẢI wire `AuditingInterceptor` (thiếu =
assert-rỗng-nghĩa).
5. **[be2-R7] Ca "V1-nháp phase 2" TRÙNG DangSoanThao(2)** — thay bằng **phase 3 `DangGopY`**
(legacy `<DangInKy`, Dev có 2 HĐ sống) chống vá-quá-tay THẬT (worker chép hình pattern
`is not (DangSoanThao or TuChoi or TraLai)` của KHKK sẽ đánh rơi phase 1/3/4).
## C. T-MATRIX W5 — 13 ca + 1 phản-ca (nguồn chân lý duy nhất, thay mọi danh sách ca cũ)
| # | Ca | Actor | Kỳ vọng |
|---|---|---|---|
| T-1 | DangSoanThao(2), HĐ mình | Drafter-chủ | OK + assert 3-trong-1 (khuất query thường · `IgnoreQueryFilters` IsDeleted/DeletedBy · changelog đúng 1 dòng ContextNote/PhaseAtChange/UserId) |
| T-2 | TraLai(98) | Drafter-chủ | OK (Dev 0 row ⇒ fixture seed thẳng Phase) |
| T-3 | TuChoi(99) | Drafter-chủ | OK |
| T-4 | DangGopY(3) legacy | Drafter-chủ | OK — chống vá-quá-tay |
| T-5 | ChoDuyet(10) | Drafter-**chủ** | 409 (khuôn KHKK; approver-delete = Q4) |
| T-6 | DaPhatHanh(9) | **Admin** | 409 — đã gen mã, không bao giờ xoá |
| T-7 | DangSoanThao, HĐ người khác | người lạ | 403 |
| T-8 | DangSoanThao, HĐ người khác | **Admin** | OK — ca DƯƠNG vế `\|\| isAdmin` |
| T-9 | `DrafterUserId=null` | actor `UserId=null` | KHÔNG xoá được (nấc-0 — chặn `null==null`) |
| T-10 | id không tồn tại | người lạ | 404 (khoá thứ tự NotFound→Forbidden) |
| T-11 | ChoDuyet, HĐ người khác | người lạ | 403 (không phải 409 — khoá Forbidden→Conflict) |
| T-12 | Reason 2001 ký tự | Drafter-chủ | ValidationException |
| T-13 | Reason " " | Drafter-chủ | OK + ContextNote==null (trim-to-null; gộp vào T-1 được) |
| T-14 | **phản-ca — KHÔNG VIẾT** test reflection-authz cho Delete (2 chiều đều cấm — chiều phủ-định pin lỗ, ĐỎ oan khi W8) | | |
File test: `tests/SolutionErp.Infrastructure.Tests/Application/ContractDeleteGuardTests.cs`
(tên CỤ THỂ — CẤM glob; `[Fact]` ≥13; 680→680+N pin 2 ô STATUS: dòng session DELTA + row canonical
`:476` sau cicd-verify — be2-R12).
## D. CHECKLIST Ô ĐO — bản THAY THẾ (fault-inject-proof; nối `;` không `&&`, `grep -c || true`)
**FE (thay C-1..C-13 của sub-style-fe-b3):**
- **C-1a/b (eslint DELTA — baseline ĐỎ sẵn 47/42):** `npx eslint .` fe-user ≤47 (41E/6W) ·
fe-admin ≤42 (37E/5W) — KHÔNG TĂNG; file-wave: `Layout.tsx` ≤4E và **0 ruleId mới** (4 lỗi sẵn:
325/416/453/478 — cái :478 nằm ĐÚNG khối accordion W1 sửa, đổi hình thì lỗi phải ≤ cũ). Đo exit
bằng `${PIPESTATUS[0]}`, không tin `$?` sau pipe.
- **C-2 (build):** `npm run build` app đụng — baseline `tsc -b` fe-user SẠCH ⇒ ô CÓ RĂNG.
- **C-3 (mirror + guard MISSING):** vòng 4 cặp, `{ [ -f A ] && [ -f U ]; } || echo MISSING` trước
khi so SHA.
- **C-6:** `grep -c "chung 1 bộ" Layout.tsx || true` = 0 **VÀ** ô dương `grep -cE "7 bộ|per-loại|S188"` ≥1.
- **C-7:** `grep -A6 "TRANSIENT_QUERY_KEYS = " fe-user/src/components/Layout.tsx | grep -cE "'(ct|view|pendingMe)'"` = 0.
- **C-8:** vế-1 `grep -rnE "get\(['\"]ct['\"]\)" fe-user/src/components/Layout.tsx` ≥1 (mới) ·
vế-2 `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` ≥1 = **ô canh-thoái-lui** (đã xanh
baseline — label rõ, không phải bằng chứng tiến độ).
- **C-9:** `grep -oE "['\"]HD-(NT-NCC|NT-DV|TP|GK|NCC|DV|MB)['\"]" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx | sort -u | wc -l` = **7**.
- **C-10:** `grep -rn "onOpenContract" fe-admin/src | grep -vE "hooks/usePipelineStages\.ts|components/pipeline/PipelineTreePanel\.tsx" | wc -l` = 0.
- **C-11:** `grep -F "grid-cols-[400px" MyContractsPage.tsx` ≥1 **VÀ** ô hoán-đổi
`grep -c "{!selectedId && <PipelineTreePanel" MyContractsPage.tsx || true` = **0** (baseline 1).
- **C-12:** `grep -c "window.confirm" ContractDetailContent.tsx` ≥1 **VÀ**
`grep -A2 "window.confirm" | grep -c 'Đã xóa'` ≥1 (message có đích) · `<Dialog` giữ = 1.
- **C-13 (W4 ô RANGE):** `open=$(grep -n "if (awV2Match) {" F)` · `close` = dấu `}` sau đó ·
`hit=$(grep -n "findContractWfByTypeCode(code)" F)` ⇒ `open < hit < close` và `hit > dòng KhkkN`.
(Mốc baseline: open=176 · KhkkN=188 · close=189.)
- **C-MIRROR-KHAI (mới, cho W5-iv):** `grep -c "File MIRROR SHA256 identical" <ContractDetailContent
2 bên>` = 1 mỗi bên (baseline 0/0).
- **C-5 (nguồn danh-sách file):** union `{ git diff --name-only HEAD; git ls-files -o
--exclude-standard; } | sort -u | grep -E '^fe-'` — git diff MÙ untracked.
**BE (thay 18 ô C của sub-style-be-b3 — bảng be2 nguyên trạng):** dùng đúng bảng
`sub-style-review-b4-be2.md §Ô đo phải sửa` (C1-C21: scope `awk '/class DeleteContractCommandHandler/,/^}/'`
trước khi grep [C3/C5 dương-giả 7-hit/4-hit] · C10 lật =0 · C13 bỏ · C14 neo TÊN FILE cấm glob ·
C16 lật 2 ô STATUS · C17 viết `"A|B|C"` không escape pipe + control-dương 292 · C18 union
ls-files · C19 `--include=*.cs` [14 hit binary hôm nay] · C21 sqlcmd ma-trận CanDelete trước mọi
kết luận policy).
**Dữ liệu đo tay (be2 đo Dev):** ChoDuyet=1 ✅ · DaPhatHanh=2 ✅ · **TraLai=0 · TuChoi=0** ⇒ 2 trạng
thái trung tâm W5 phải TỰ DỰNG khi đo tay (xUnit seed thẳng Phase là đủ) hoặc DEFERRED-TO-DEPLOY.

View File

@ -0,0 +1,20 @@
# B5 — PLAN FINAL "SOLTUON-12-08-2026" (reviewer /fable-real, tổng hợp cuối YC-029)
> Nguồn: spec v2.2 (`spec-soltuon-dieu-chinh-hd-12-08-2026.md`) ⟂ `style-gate-v2.md` ⟂ `anh-mo-ta.md` + bối cảnh B1/B2/B3.
> Trạng thái: ĐANG GHI — append từng phần (chống #53).
## Verdict tổng
(đang ghi)
## Mâu thuẫn còn sót (X-1..)
(đang ghi)
## PLAN FINAL SOLTUON-12-08-2026 (per-wave)
(đang ghi)
## Điều kiện dừng/leo thang cho worker (Đ-1..)
(đang ghi)

View File

@ -1,20 +1,269 @@
# B4 — BE-STYLE review lane 2/2 (lens: Ô-ĐO + TEST-MATRIX + BS-3) # 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` > 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 > input: `sub-style-be-b3.md` (20 SB + 18 ô C + 7 ca test) · `spec-soltuon-dieu-chinh-hd-12-08-2026.md` §②-W5 + §③-W5 + §④
> nguyên tắc: mọi số dưới là **ĐO**, không suy; 0-hit luôn kèm **control dương** (control sai ⇒ hỏng THƯỚC, không phải hỏng vật).
## Verdict ## Verdict
_(đang chạy)_ **B4-BE-ODO: PASS_WITH_FIXES — 15 findings + T-matrix 13 ca (+1 phản-ca)**
## Findings (R-1..) Phần LUẬT (SB-1..SB-12, SB-20, BS-1/2/4/5/6/7) **đứng vững**: 9/9 khuôn tao mở ra đối chiếu đều SỐNG và
đúng nội dung được khai (K1 `:1741-1806` · K3 `:885-901` · K5 `:13/:30-32/:78` · K6 `:153-156` · K7 field-set
đủ · K9 · K11 helper+registerable · K12 `SoftDeleteFixture:55-80` · K13 `:469-539`). Nhưng lớp **ĐO** thì thủng:
**5/18 ô C sai hoặc dương-giả**, **1 luật (e) đã bị chính spec v2.1 đảo mà list chưa lật**, và **bộ 7 ca test có
1 ca trùng + thiếu 5 ca**. List CHƯA dùng làm STYLE-GATE được cho tới khi vá R-1..R-6 (đều là sửa chữ, ≤30 phút).
_(đang chạy)_ | Nhóm | Kết |
|---|---|
| Anchor/khuôn (lane-1 chồng lấn, tao chỉ spot-check) | 9/9 SỐNG |
| Luật SB nội dung | ĐẠT — trừ (e) SB-13/14/18 (R-1) |
| 18 ô C chạy được | **13 OK · 5 PHẢI SỬA** (C3, C5, C14, C17, C18) + C1/C10/C13/C16 phải chỉnh kỳ vọng |
| 7 ca test | 6 giữ · 1 trùng-lặp phải thay · **+5 ca thiếu** ⇒ 13 |
| BS-3 (policy khoá oan) | **XÁC NHẬN BẰNG SỐ THẬT — mạnh hơn mức list khai** |
## Ô đo phải sửa ## Findings (R-1..R-15)
_(đang chạy)_ ### R-1 🔴 BLOCKER — list ĐÁNH NHAU với spec v2.1: (e) bắt gắn policy, spec CẤM gắn
## T-matrix hợp nhất - `sub-style-be-b3.md:60` (SB-13 "…đặt NGAY TRÊN action Delete") · `:61` (SB-14) · `:68` (SB-18 reflection-test
`attr!.Policy.Should().Be("Contracts.Delete")`) · ô `:100` (C10 "**=1**") · ô `:103` (C13 "≥1").
- Ngược trực tiếp `spec…12-08-2026.md:76-82`*"🔄 **v2.1 theo BS-3 (be-style B3):** KHÔNG gắn
`[Authorize(Policy="Contracts.Delete")]` đợt này"* — và acceptance `:125-127` ✅(iii) *"KHÔNG thêm
`[Authorize(Policy` mới vào DELETE"*.
- Vì sao đây là BLOCKER chứ không phải lệch chữ: `spec:85-87` phong chính 2 file style-list làm **STYLE-GATE
bắt buộc** cho worker B6. Worker sẽ cầm 2 mệnh lệnh ngược nhau, và mệnh lệnh của style-list là mệnh lệnh
**ship regression** (R-2). Trớ trêu: chính BS-3 của file này đẻ ra v2.1, nhưng phần (e) + checklist **không
được lật theo**.
- **Vá:** SB-13 → "GIỮ nguyên shape hiện tại: class `[Authorize]` trần, action DELETE **KHÔNG** policy —
`[carry:w8-delete-policy]`"; SB-14 → comment khai *vì sao chưa gắn* (dẫn (82) + BS-3) thay vì khai lý-do gắn;
SB-18 → **bỏ**; C10 lật kỳ vọng **=0**; C13 **bỏ**.
_(đang chạy)_ ### R-2 🔴 BLOCKER (dữ liệu) — BS-3 ĐÚNG, và số thật còn xấu hơn con số list mượn của PE
Đo LocalDB `SolutionErp_Dev` (`sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -E`), bảng `Permissions`
JOIN `Roles`, 13 role / 13 dòng cho key `Contracts`:
```
Contracts|Admin |R1 C1 U1 D1 ← DUY NHẤT có CanDelete=1
Contracts|Drafter |R1 C1 U0 D0 ← 🔴 CanDelete = 0
Contracts|Procurement |R1 C1 U0 D0
Contracts|{10 role còn lại}|R1 C0 U0 D0
--- CONTROL DƯƠNG (cùng câu query, key KHKK — nơi policy ĐANG chạy tốt):
KeHoachKyKet|Drafter |R1 C1 U1 D1 ← Drafter CÓ CanDelete
KeHoachKyKet|Admin |R1 C1 U1 D1
KeHoachKyKet|{11 role} |R1 C1 U1 D0
```
**12/13 role `CanDelete=0` trên key `Contracts`** (BS-3 mượn số PE "11/13" làm tiền lệ — số của CHÍNH key này
**12/13**, Drafter nằm trong nhóm 0). Gắn policy = mọi role trừ Admin ăn **403 trước khi owner-check chạy**
⇒ Drafter **mất** đường xoá nháp đang có (hành-vi deployed = CHUẨN, S176). KHKK sống được với policy **chỉ vì
seed đã grant `CanDelete` cho Drafter** — đó là biến số data, không phải style.
- **Hệ quả cho list:** BS-3 phải nâng từ *"FLAG lead ĐO trước khi ship"* thành **luật CẤM của đợt này** (đã có
số, khỏi FLAG nữa), + ghi kèm điều kiện mở W8: *policy chỉ gắn được KÈM grant data cho Drafter, 2 việc CÙNG
commit, nếu không là khoá oan*.
- **Giới hạn phép đo (khai thẳng):** đây là **Dev**, không phải prod. Prod có thể đã bị admin sửa tay qua
`/system/permissions`. Trước W8 phải đo lại prod (`sqlcmd -E` qua ssh — bài S177), KHÔNG suy từ Dev.
### R-3 🔴 MAJOR — ô C17 (vế raw-SQL) là phép đo RỖNG: luôn 0 hit dù có vi phạm
`sub-style-be-b3.md:107` viết `grep -rnE "FromSqlRaw\|ExecuteSqlRaw" …`. Với `-E`, `\|` = **pipe LITERAL**
tìm chuỗi `FromSqlRaw|ExecuteSqlRaw` (không tồn tại) ⇒ **0 vĩnh viễn**.
```
grep -rnE "FromSqlRaw\|ExecuteSqlRaw" …/Application/ | wc -l → 0
grep -rnE "IApplicationDbContext\|ZZZNOTEXIST" …/Application/ | wc -l → 0 ← CONTROL DƯƠNG: chuỗi CÓ THẬT vẫn 0 ⇒ THƯỚC HỎNG
grep -rE --include=*.cs "IApplicationDbContext|ZZZNOTEXIST" … | wc -l → 292 ← bản đúng có răng
grep -rE --include=*.cs "FromSqlRaw|ExecuteSqlRaw|SqlQueryRaw" … | wc -l → 0 ← claim K17 ĐÚNG, nhưng đúng nhờ may
```
⇒ claim của K17 (`:28`) là **THẬT**, nhưng ô canh nó **không có răng**: mai worker thêm `FromSqlRaw`, ô vẫn xanh.
### R-4 🔴 MAJOR — 3 ô DƯƠNG-GIẢ ngay hôm nay (pass trước khi worker viết 1 dòng)
Đo trên `ContractFeatures.cs` (902 dòng) **trước** W5:
| Ô | Lệnh trong list | Baseline HÔM NAY | Vì sao hỏng |
|---|---|---|---|
| C3 `:93` | `grep -n "ICurrentUser currentUser" ContractFeatures.cs` | **7 hit** (`:56, :247, :284, :320, :456`, …) | file có 7 handler khác đã inject; ô không phân biệt "đã sửa delete" ⟂ "chưa làm gì" |
| C5 `:95` | `grep -n "AppRoles.Admin"` + `grep -n "DrafterUserId == currentUser.UserId"` | **4 hit** / **1 hit** (`:647` `var isDrafter = c.DrafterUserId == currentUser.UserId;`) | idiom owner-check ĐÃ có sẵn trong file (khối IDOR) ⇒ ô xanh sẵn |
| C1 `:91` | `grep -n "record DeleteContractCommand"` | **1 hit** (`:887`, bản KHÔNG có `Reason`) | kỳ vọng "1 hit, **có** `string? Reason`" không nằm trong lệnh — mắt người phải đọc, máy không bắt |
Chữa: **scope theo thân handler** bằng `awk '/class DeleteContractCommandHandler/,/^}/'` rồi mới grep (xem §Ô đo).
🔸 Ghi nhận ngược lại: `db.Contracts.Remove(` chỉ **1 hit** (`:899`) và `T26`/`[W5` baseline = **0/0** ⇒ C6/C8/C9
là ô LÀNH, không cần scope (nhưng scope vẫn tốt hơn).
### R-5 🔴 MAJOR — C14 hỏng 2 tầng, và tầng 2 phá đúng cái bẫy nó định canh
`:104``grep -n "AuditingInterceptor" tests/.../Application/Contract*Tests.cs`
1. Path chứa `...` **literal** ⇒ lệnh gãy (không có thư mục nào tên `...`).
2. Kể cả sửa thành path thật: glob `Contract*Tests.cs` khớp **6 file có sẵn**, và `grep -l AuditingInterceptor`
trên glob đó trả **6/6 file** (`ContractCatalogTests.cs:83` · `ContractHardCopyTests.cs` ·
`ContractSigningPlanApprovalTests.cs:82` · `ContractSigningPlanCrudTests.cs:77` ·
`ContractSigningPlanGroupAndCatalogTests.cs:81` · `ContractV2PipelineTests.cs`) ⇒ ô **XANH kể cả khi file
test MỚI không wire interceptor** = đúng cái bẫy BS-4 (`:80` assert-rỗng-nghĩa) mà nó được dựng để canh.
Chữa: neo **tên file cụ thể** (`ContractDeleteGuardTests.cs`), cấm glob.
### R-6 🔴 MAJOR — C18 dùng `git diff --stat` ⇒ MÙ với file .cs mới (mà W5 đẻ ra đúng loại đó)
Fault-inject (tạo → đo 2 thước → dọn):
```
echo "class Z{}" > src/Backend/SolutionErp.Application/Contracts/__probe_untracked.cs
git diff --stat -- src/Backend/SolutionErp.Application/Contracts/ → (0 dòng) ← C18 as-written KHÔNG THẤY
git status --porcelain -- src/Backend/SolutionErp.Application/Contracts/
→ ?? …/__probe_untracked.cs ← thước đúng THẤY
```
W5 tạo `ContractDeleteGuardTests.cs` (file .cs MỚI) ⇒ ô "KHÔNG file .cs mới ngoài tests/" chạy trên `git diff`
là ô mù đúng chỗ cần sáng. Đây là **bài S179 của chính vai này** (gate PRE-commit + `git diff` = chết câm).
Chữa: `{ git diff --name-only HEAD; git ls-files -o --exclude-standard; } | sort -u`.
🔸 Bên lề, KHAI để không ai tưởng tao đo ẩu: 2 file `sub-style-review-b4-*.md` mới trong run-folder **không**
hiện ở `git status --porcelain` lẫn `git ls-files -o` (mà `check-ignore -v --no-index` nói `!.claude/**` =
NOT-ignored). Tao không truy tiếp (ngoài địa phận + hết ngân sách lượt) — **nêu để lead tự kiểm**, vì nếu
đúng thì phép audit "tracked-change ngoài run-folder" của Harness-10 có điểm mù cần biết.
### R-7 🔴 MAJOR (test-matrix) — ca "V1-nháp phase 2" TRÙNG ca "DangSoanThao", và không canh được rủi ro nó tự nhận canh
`SB-17 :67` liệt cả *"DangSoanThao Drafter xoá được (giữ cũ)"* **và** *"+V1-nháp phase 2 vẫn xoá được (chống vá
quá tay)"* — mà `ContractPhase.cs:17``DangSoanThao = 2`. **Hai ca = một ca.** (spec `:124` chép y hệt cặp này.)
Rủi ro THẬT mà "chống vá quá tay" phải canh: worker chép **HÌNH** pattern-match của K1
(`is not (DangSoanThao or TuChoi or TraLai)``ContractSigningPlanFeatures.cs:1778-1780`, đúng thứ SB-4 `:39`
gọi là "khuôn cú pháp") ⇒ allow-list **THU HẸP**, đánh rơi phase legacy `DangChon(1)/DangGopY(3)/DangDamPhan(4)`
vốn đang xoá được (`ContractFeatures.cs:896` `Phase >= DangInKy`). Vật thật: Dev đang có **2 HĐ ở phase 3**
(đo: `SELECT Phase, COUNT(*) … GROUP BY Phase``2|2, 3|2, 5|1, 7|1, 9|2, 10|1`).
⇒ Ca đúng = **phase 3 (hoặc 4)**, không phải phase 2.
### R-8 🔴 MAJOR (test-matrix) — nhánh `isAdmin` KHÔNG có ca DƯƠNG nào
7 ca SB-17 không có "Admin xoá HĐ **người khác** ở Nháp → OK". Nếu worker viết `if (!isOwner) throw` (rơi vế
`|| isAdmin`), chỉ ca DaPhatHanh-Admin bắt được — và **chỉ khi** test đó dựng Admin ≠ Drafter (nhận 403 thay vì
409 kỳ vọng). Bắt-được-nhờ-tình-cờ-dựng-dữ-liệu ≠ bằng chứng. Cần ca dương riêng.
### R-9 🔴 MAJOR (test-matrix) — BS-6 nêu lỗ null nhưng KHÔNG ca nào khoá
`Contract.cs:16` `public Guid? DrafterUserId` · `TestCurrentUser.cs:14` `public Guid? UserId` + `:20` ctor rỗng
⇒ dựng actor null **1 dòng**. Không có nấc-0 thì `entity.DrafterUserId == currentUser.UserId` hoá `null == null`
`isOwner = true` ⇒ xoá lọt. Và test **gọi handler TRỰC TIẾP** (khuôn K12 `NewDeleteHandler(f)` /
`PeDeleteByApproverTests.cs:148-152`) ⇒ rào `[Authorize]` ở controller **không cứu** ở tầng này. BS-6 (`:82`) đòi
thêm nấc-0 nhưng SB-17 không có ca ⇒ luật không răng. (Dev hiện **0/9** HĐ có `DrafterUserId` null — nên ca này
sống ở test, càng phải viết.)
### R-10 🟡 MEDIUM — thứ tự guard SB-3 không có ca nào phân biệt được
SB-3 (`:38`) chốt NotFound → Forbidden → Conflict. Muốn đo phải có ca **CHÉO**: (a) người-lạ + id không tồn tại
**404** (không phải 403); (b) người-lạ + HĐ `ChoDuyet`**403** (không phải 409). Thiếu 2 ca thì hai bản cài
đặt khác thứ tự **đều xanh** ⇒ luật SB-3 chỉ là lời khuyên.
### R-11 🟡 MEDIUM — validator MỚI (SB-1/SB-12) là mảnh code duy nhất 0 ca chạm
`DeleteContractCommandValidator` + `MaximumLength(2000)` là code **mới toanh** (baseline: file không có
validator cho delete). Không ca nào gọi. 1 ca `Reason` 2001 ký tự → `ValidationException` là rẻ nhất trong bộ.
### R-12 🟡 MEDIUM — SB-19/C16 "ĐÚNG 1 row" sẽ để canonical stale
STATUS có **2 ô số**: `docs/STATUS.md:6` (dòng session, dạng DELTA `test **668→680**` — khuôn SB-19 trỏ) và
`docs/STATUS.md:476` (row canonical `| Tests | **680 PASS** | 45 Domain + 635 Infra …`). Chính row `:476` tự khai
*"ô này lệch 2 đời… cùng ô đã 4 lần tái phát ⇒ nguồn cập nhật = cicd-verify lượt kế, KHÔNG chép tay"*. Câu
SB-19 *"ĐÚNG 1 row, không neo số tuyệt đối chỗ khác"* đọc thẳng = **không lật `:476`** ⇒ canonical stale đúng
class STATUS đang cảnh báo. Phải gọi tên: dòng session mới (DELTA) **và** row canonical (số tuyệt đối, sau
cicd-verify) — 2 chỗ, khác vai, không phải "neo số 2 nơi".
### R-13 🟡 MEDIUM — K7 có mồi bẫy nằm ngay trên đầu vùng được trỏ (ghost-wire)
`ContractChangelog.cs:10-13`: *"Populate qua MediatR `AuditBehavior` interceptor — auto từ Insert/Update/Delete
commands trên Contract + Details (xem Application/Common/Behaviors/AuditBehavior.cs)"*. Đo:
`ls Application/Common/Behaviors/`**chỉ `ValidationBehavior.cs`**; `grep -rn --include=*.cs "AuditBehavior"
src/ tests/`**2 hit, cả 2 là chính 2 dòng comment này**. File KHÔNG tồn tại; `CLAUDE.md` Tier-4 vẫn liệt
"MediatR AuditBehavior" là TODO. Ai HIỆN GHI `ContractChangelogs`: đúng **2 chỗ tay**
(`CreateContractFromSigningPlanFeatures.cs:247` · `CreateContractFromEvaluationFeatures.cs:121`).
⇒ Worker mở K7 rất dễ kết luận "changelog tự ghi, khỏi Add" ⇒ bỏ SB-10/SB-11 ⇒ 0 dòng ⇒ acceptance ✅(ii) đỏ mà
không hiểu vì sao. List phải cắm 1 dòng cảnh báo cạnh K7.
### R-14 🔵 NIT — SB-15 trỏ fixture xa hơn mức cần (khuôn CÙNG HỌ đã có)
SB-15 (`:65`) bảo chép `SoftDeleteFixture` từ `PeDeleteByApproverTests.cs:55-104` (module PE). Khuôn **cùng họ
Contract, đã deploy**, gần hơn: `ContractSigningPlanCrudTests.cs:77` (`KhkkFixture`) và `ContractCatalogTests.cs:83`
— đều `.AddInterceptors(new AuditingInterceptor(...))`. Đo: **5 fixture private-per-file** đã deploy dùng đúng
nếp này ⇒ dựng fixture thứ 6 là **ĐÚNG khuôn**, không phải trùng lặp (chặn trước phản-biện "sao không dùng
chung"). Giữ K12 làm nguồn **lý-do** (`:32-41`), đổi khuôn **hình** sang Contract-family.
### R-15 🔵 NIT — checklist nối bằng `&&` sẽ TỰ CẮT, và "không chạy" trông y hệt "pass"
`grep -c` trả **exit 1 khi đếm = 0** ⇒ chuỗi `A && B && C` đứt ngay ô đầu tiên có 0 hit. Tao dính thật khi chạy
thử: chuỗi dừng sau ô `T26 = 0`, 3 ô sau **không chạy mà không báo gì**. Ô CẤM (kỳ vọng 0) là loại dính nhiều
nhất. Chữa: nối bằng `;` (hoặc `| wc -l`, hoặc `|| true`).
## Ô đo phải sửa (18 ô → verdict + lệnh thay thế)
Đặt trước: `F=src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs` ·
`H='/class DeleteContractCommandHandler/,/^}/'` · `C=src/Backend/SolutionErp.Api/Controllers/ContractsController.cs` ·
`T=tests/SolutionErp.Infrastructure.Tests/Application/ContractDeleteGuardTests.cs`
| Ô | Verdict | Lệnh/kỳ vọng ĐÚNG |
|---|---|---|
| C1 | siết | `grep -c 'record DeleteContractCommand(Guid Id, string? Reason = null)' $F`**1** |
| C2 | siết | `awk '/class DeleteContractCommandValidator/,/^}/' $F \| grep -c 'MaximumLength(2000)'`**1** |
| C3 | **SỬA (R-4)** | `awk "$H" $F \| grep -c 'ICurrentUser currentUser'`**1** (as-written = 7 dương-giả) |
| C4 | siết | `awk "$H" $F \| grep -n 'NotFoundException\|ForbiddenException\|ConflictException'` → 3 dòng, thứ tự NF < FB < CF |
| C5 | **SỬA (R-4)** | `awk "$H" $F \| grep -c 'AppRoles.Admin'` 1 **và** `awk "$H" $F \| grep -c 'DrafterUserId == currentUser.UserId'` 1 |
| C6 | OK (siết) | `awk "$H" $F \| grep -n 'db.Contracts.Remove(\|db.ContractChangelogs.Add(\|SaveChangesAsync'` Remove < Add · `SaveChangesAsync` đúng **1** |
| C7 | OK | giữ; VN-literal chạy được trên shell này control dương: `grep -c '(chưa có mã)' …/ContractSigningPlanFeatures.cs` **2** |
| C8 | OK | baseline `T26` trong `$F` = **0** ô nghĩa |
| C9 | OK | baseline `[W5` trong `$F` = **0** ô nghĩa |
| C10 | **LẬT (R-1)** | `awk '/HttpDelete\("\{id:guid\}"\)/,/IActionResult> Delete\(/' $C \| grep -c 'Authorize(Policy'` **0** |
| C11 | siết | `grep -c '^\[Authorize\]$' $C` **1** (đừng neo số dòng `:13`) |
| C12 | OK | `grep -c 'FromQuery\] string? reason' $C` **1** |
| C13 | **BỎ (R-1)** | thay bằng ghi chú `[carry:w8-delete-policy]`; **KHÔNG** viết test âm "Delete không policy" test đó **pin cái lỗ** sẽ ĐỎ oan đúng lúc W8 gắn policy hợp lệ |
| C14 | **SỬA (R-5)** | `grep -c 'AuditingInterceptor' $T` 1 · `grep -c 'IgnoreQueryFilters' $T` 1 (**tên file cụ thể; CẤM glob `Contract*Tests.cs`** 6/6 file đã , ô sẽ xanh oan) |
| C15 | siết | `grep -c '\[Fact\]' $T` **13** + `grep -n 'public async Task ' $T` khớp đủ T-1..T-13 |
| C16 | siết (R-12) | `dotnet test SolutionErp.slnx` **680+N**; lật **2 ô**: dòng session mới `docs/STATUS.md` (DELTA `680→680+N`) **và** row canonical `Tests` `:476` (số tuyệt đối, chỉ chép từ cicd-verify) |
| C17 | **SỬA (R-3)** | `grep -rn --include=*.cs "catch (" src/Backend/SolutionErp.Api/Controllers/ \| wc -l` 0 (control: cùng lệnh trên `src/Backend/SolutionErp.Api/` **30**) · `grep -rn --include=*.cs "Console.WriteLine" src/Backend/ \| wc -l` 0 (control `Console\.` **72**) · `grep -rE --include=*.cs "FromSqlRaw\|ExecuteSqlRaw\|SqlQueryRaw"` **PHẢI viết `grep -rE --include=*.cs "FromSqlRaw|ExecuteSqlRaw|SqlQueryRaw"`** 0, kèm control dương `"IApplicationDbContext|ZZZNOTEXIST"` **292** |
| C18 | **SỬA (R-6)** | `{ git diff --name-only HEAD; git ls-files -o --exclude-standard; } \| sort -u` (as-written `git diff --stat` với .cs mới đã fault-inject chứng minh) |
| **C19 mới** | thêm | mọi grep repo-wide phải `--include=*.cs`: `grep -rn "Contracts\.Delete" src/ tests/` hôm nay trả **14 dòng "Binary file … matches"** (bin/obj DLL); thêm `--include=*.cs` **0** (baseline đúng) |
| **C20 mới** | thêm | nối các ô bằng `;` không `&&` (R-15) |
| **C21 mới** | thêm (R-2) | trước khi kết luận "policy an toàn": `sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -E -Q "SELECT r.Name,p.CanDelete FROM Permissions p JOIN Roles r ON r.Id=p.RoleId WHERE p.MenuKey='Contracts'"` control dương = cùng query với `MenuKey='KeHoachKyKet'` (Drafter phải =1) |
**Ngoài checklist — ô §④ nấc đo local thiếu tiền-đề dữ liệu:** spec `:138` đòi *"1 TraLai + 1 ChoDuyet + 1
DaPhatHanh"*. Đo Dev: `ChoDuyet(10)=1` · `DaPhatHanh(9)=2` · **`TraLai(98)=0` · `TuChoi(99)=0` ❌** tức
**2 trạng thái TRUNG TÂM của W5 không có sẵn** để đo tay. Worker phải tự dựng (đưa 1 qua đường Trả lại/Từ
chối) hoặc đánh **DEFERRED-TO-DEPLOY** công khai; ô test tự-dựng trong xUnit thì seed thẳng Phase đủ.
## T-matrix hợp nhất (T-1..T-13 + 1 phản-ca)
Nguồn: `SB-17` (`sub-style-be-b3.md:67`, 7 ca) spec `✅W5(i)-(iii)` (`:120-127`) ca tao thêm.
Cột "nguồn": **B** = cả 2 danh sách đã · **S** = chỉ spec · **BE** = chỉ style-list · **MỚI** = cả 2 đều thiếu.
| # | Ca | Actor | Kỳ vọng | Nguồn |
|---|---|---|---|---|
| T-1 | `DangSoanThao(2)` của chính mình | Drafter-chủ | **OK** + assert 3-trong-1: khuất query thường · `IgnoreQueryFilters().Single().IsDeleted==true` + `DeletedBy` · changelog **đúng 1 dòng** `Action=Delete` với `ContextNote`/`PhaseAtChange=2`/`UserId` | B |
| T-2 | `TraLai(98)` | Drafter-chủ | **OK** (vế MỚI của W5) Dev 0 row phase 98 seed thẳng trong fixture | B |
| T-3 | `TuChoi(99)` | Drafter-chủ | **OK** (vế MỚI của W5) | B |
| T-4 | **`DangGopY(3)` (legacy `< DangInKy`)** | Drafter-chủ | **OK** chống vá-quá-tay THẬT (**thay** ca "V1-nháp phase 2" trùng T-1; Dev 2 phase 3 sống) | MỚI (thay B) |
| T-5 | `ChoDuyet(10)` | Drafter-**chủ** | **409 Conflict** (khuôn KHKK; approver-delete = Q4 chờ anh) | B |
| T-6 | `DaPhatHanh(9)` | **Admin** | **409 Conflict** đã gen không bao giờ xoá | B |
| T-7 | `DangSoanThao(2)` người khác | người lạ (không chủ, không Admin) | **403 Forbidden** (sửa lỗ ; khuôn stranger `ContractSigningPlanCrudTests.cs:536-539`) | B |
| T-8 | `DangSoanThao(2)` người khác | **Admin** | **OK** ca DƯƠNG cho vế `\|\| isAdmin` (R-8) | MỚI |
| T-9 | `DrafterUserId = null` | actor `UserId = null` (`new TestCurrentUser()`) | **KHÔNG xoá được** (`UnauthorizedException` theo nấc-0 BS-6) chặn `null == null ⇒ isOwner` (R-9) | MỚI |
| T-10 | `id` không tồn tại | người lạ | **404 NotFound** (không phải 403) khoá thứ tự NotFound Forbidden | MỚI |
| T-11 | `ChoDuyet(10)` người khác | người lạ | **403 Forbidden** (không phải 409) khoá thứ tự Forbidden Conflict | MỚI |
| T-12 | `Reason` dài 2001 tự | Drafter-chủ | **ValidationException** (validator mới, SB-12) | MỚI |
| T-13 | `Reason` = `" "` (whitespace) | Drafter-chủ | OK + `ContextNote == null` (trim-to-null, SB-12) gộp assert vào T-1 cũng được | MỚI |
| **T-14** | **phản-ca — KHÔNG viết** | | KHÔNG thêm `[Fact]` reflection-authz cho `ContractsController.Delete` (cả chiều khẳng-định lẫn chiều phủ-định): chiều khẳng-định ngược spec v2.1 (R-1), chiều phủ-định pin lỗ + ĐỎ oan khi W8 tới. Dùng ô grep C10 thay test. | MỚI |
**Ghép 2 danh sách:** trùng 6 ca (T-1,2,3,5,6,7) · style-list spec không nói: yêu cầu
`IgnoreQueryFilters` chống-vacuous (BS-4, `:80`) spec chỉ ghi "đọc-ngược DB" · spec style-list nhạt:
✅(iii) grep-âm policy (đã thành C10) · **cả 2 cùng thiếu: T-8..T-13 (6 ca)** · **cả 2 cùng sai: ca "V1 nháp 2"
(→ T-4)**. Tổng đề nghị **13 ca thật** (SB-17 nói 7, spec nói 6) C15 sửa kỳ vọng `≥13 [Fact]`.
## Điều tao KHÔNG kết luận (ranh giới)
- Không phán **có nên** mở approver-delete (Q4) hay gắn policy W8 quyền anh; tao chỉ đưa **số** (R-2) để anh
quyết không bị dữ liệu.
- Không soi logic nghiệp-vụ allow-list đúng/sai về mặt quy trình (→ `reviewer`); tao chỉ soi **đồng-khuôn với
bản đã deploy** + đo được.
- Không đụng `Persistence/**` (EF config/migration = `database-reviewer-style`) W5 0-mig, không để soi.
- Không sửa file nào ngoài chính báo cáo này.
== HẾT ==

View File

@ -1,16 +1,160 @@
# B4 lane-1/2 — front-end-reviewer-style — lens ANCHOR + ĐÚNG-KHUÔN # 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. > Review `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. > Phương pháp: MỞ khuôn THẬT ra đọc, đối chiếu TỪNG mệnh đề (class string đúng từng chữ? message `window.confirm` đúng? `Pick<>` đúng key? số đếm đúng?). Không tin trích dẫn, chỉ tin dòng code.
## Verdict ## Verdict
_(đang đo — điền cuối)_ **B4-FE-ANCHOR: PASS_WITH_FIXES — 11 findings** (2 MAJOR · 3 MODERATE · 6 MINOR)
## Findings (R-1..) - **Sức khoẻ anchor: ~35/37 chính xác** (xem §Anchor đã kiểm — ĐÚNG). Không có anchor CHẾT (trỏ file/vùng không tồn tại). Lệch duy nhất là ±1..3 dòng ở 1 chỗ (R-7).
- **(b) luật TỰ CHẾ kiểu "bỏ type=3" của B2: KHÔNG CÓ.** Không SF nào đòi thứ trái khuôn prod. 2 lỗi "tự chế nhẹ" là gọi sai TÊN (R-4 "shadcn Select") và trích chuỗi không literal (R-3/R-5).
- **(c) mâu thuẫn spec v2.1: 0.** Đo: `grep -n "Authorize\|policy"` trong `sub-style-fe-b3.md` = 0 (không đụng vế W5(d) đã đổi); SF-23 GIỮ `?type=3&ct=` đúng v2.1 (v1 đòi bỏ — đã gỡ).
- Rủi ro chính KHÔNG phải luật sai mà là **luật THIẾU MỆNH-ĐỀ so với khuôn** (R-1, R-2) — đúng lớp làm worker "đạt gate mà vẫn lệch prod".
_(append từng finding ngay khi đo xong)_ ## Findings (R-1..R-11)
## Luật phải sửa/gỡ ### R-1 — MAJOR · khuôn ĐÃ-CHẠY của chính việc W2 làm bị BỎ SÓT khỏi bảng khuôn
_(điền cuối)_ `sub-style-fe-b3.md` không nhắc `ContractsListPage` lần nào (grep tên file trong spec-list = 0 hit), trong khi **fe-admin đang chạy prod một host 3-panel GĐ3 CÓ NHÚNG SẴN cây toàn trình**:
- `fe-admin/src/pages/contracts/ContractsListPage.tsx:141``grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[340px_1fr_360px]`
- `fe-admin/src/pages/contracts/ContractsListPage.tsx:265``{!selectedId && <PipelineTreePanel currentStage={3} bodyMaxHClass="max-h-[28rem] lg:max-h-none" />}`
Hệ quả đo được, 2 cái:
1. **Không luật nào bắt worker quyết `bodyMaxHClass`.** W2 nhúng cây vào CỘT CUỘN của host `h-[calc(100vh-4rem)]`; chính khuôn khai tại `fe-user/src/components/pipeline/PipelineTreePanel.tsx:112-115`: *"max-h mặc định 100vh16rem là hiệu-chỉnh cho trang giấy-cuộn, vượt chỗ Panel-3 /inbox ~26px ⇒ 2 cuộn lồng nhau"*. SF-24 có liệt `bodyMaxHClass` như MẪU opt-in nhưng KHÔNG có luật "nhúng cây vào cột cuộn ⇒ PHẢI truyền/khai". Prod đã trả giá cho bài này (gate F-1 S168) — bỏ ra khỏi gate là mời nó quay lại.
2. **Trỏ khuôn đời sau, bỏ khuôn đời trước.** `fe-user/src/pages/khkk/KhkkListPage.tsx:7-8` tự khai: *"Khuôn code lấy từ `ContractsListPage.tsx` (255 dòng, gọn) — CỐ Ý KHÔNG chép `PurchaseEvaluationsListPage.tsx`"*. Spec-list lấy KhkkListPage làm khuôn 3-panel (đúng) nhưng không nói nó là bản SAO của ContractsListPage — worker W2 sửa `MyContractsPage` sẽ không biết bản GĐ3-anh-em đang đứng ở `340px` + đặt cây ở nhánh `!selectedId`.
**Sửa:** thêm 1 dòng khuôn `ContractsListPage.tsx:141,265` vào bảng + 1 luật SF mới: *"Nhúng `PipelineTreePanel` vào cột có thanh cuộn riêng ⇒ BẮT BUỘC quyết `bodyMaxHClass` và khai lý do (khuôn `:112-115` + host prod `ContractsListPage.tsx:265`)"*. Ô đo: `grep -n "bodyMaxHClass" fe-user/src/pages/contracts/MyContractsPage.tsx` ≥1 HOẶC comment khai vì sao không cần.
### R-2 — MAJOR · SF-21/SF-8 chép THIẾU một vế của guard, vế thiếu chính là vế chống trang-trắng
Khuôn thật `fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx:84-86`:
```ts
const rawGroup = searchParams.get('group')
const khkkGroup =
typeInt === 10 && rawGroup !== null && /^[1-8]$/.test(rawGroup) ? Number(rawGroup) : null
```
Guard có **2 vế**: `typeInt === 10 &&` (GATE THEO TYPE) rồi mới tới clamp regex. SF-21 chỉ chép vế clamp (*"miền hữu hạn ⇒ clamp regex … W1(b) ct dùng `/^[1-7]$/`"*), SF-8 cũng chỉ nói "W1(b) `ct` đi ĐÚNG khuôn này" mà không nêu vế gate.
Vì sao vế đó load-bearing: **1 file phục vụ 3 route**`fe-user/src/App.tsx:71` (`/purchase-evaluations/workflow-matrix`), `:73` (`/contracts/workflow-matrix`), `:83` (`/khkk/workflow-matrix`). Thiếu `typeInt === 3 &&`, một URL `?type=1&ct=5` (route PE) sẽ đem mã `HD-*` đi lọc bộ workflow Duyệt-NCC ⇒ 0 dòng — đúng ca mà comment prod `:81-83` dựng ra để chặn (*"thà hiện đủ còn hơn lọc theo một con số vô nghĩa rồi ra trang trắng"*). Không ô C nào bắt được: C-9 chỉ **đếm 7 chuỗi `'HD-`**, đếm đủ vẫn có thể thiếu gate.
**Sửa SF-21:** thêm mệnh đề *"gate `typeInt === <type của trang>` đứng TRƯỚC clamp (khuôn `:86`) — trang này gắn 3 route, thiếu gate là lọc chéo module"*. Ô đo thêm: `grep -nE "typeInt === 3 &&" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx` ≥1.
### R-3 — MODERATE · SF-24 trích một câu KHÔNG TỒN TẠI trong khuôn ⇒ gate literal sẽ trượt trên chính file khuôn
SF-24: *"JSDoc có câu **'Không truyền ⇒ giữ nguyên hành vi cũ'**"* (đóng ngoặc kép = đọc như literal).
ĐO trên khuôn `fe-user/src/components/pipeline/PipelineTreePanel.tsx`:
- `grep -c "Không truyền ⇒ giữ nguyên hành vi cũ"` = **0**
- `grep -c "hông truyền"` = **6** — 6 biến thể khác nhau: `:108` *"Không truyền ⇒ cây giữ NGUYÊN hành vi cũ"* · `:110` *"4 host không truyền giữ nguyên"* · `:119` *"6 host còn lại KHÔNG truyền ⇒ giữ nguyên hình cũ"* · `:127` *"…giữ nguyên ô lọc cây như cũ"* · `:132` *"5 host còn lại không truyền ⇒ cây giữ NGUYÊN mọi phase như cũ"* · `:135` *"Không truyền ⇒ vẽ đủ 4 giai-đoạn như cũ"*. Riêng `:115` còn dạng *"Mặc định giữ nguyên"* (không có chữ "truyền").
⇒ Khuôn đòi **khớp NGHĨA**, không phải khớp chuỗi. Để nguyên ngoặc kép: hoặc gate 0-hit oan, hoặc worker dán câu cứng nhắc khác giọng 6 mẫu đang có.
**Sửa:** bỏ ngoặc kép literal, ghi *"câu chốt cùng nghĩa: không truyền ⇒ giữ hành vi cũ (6 biến thể `:108-135`)"*; ô đo `grep -c "hông truyền" <file>` ≥1 per prop mới.
### R-4 — MODERATE · SF-14 gọi sai tên công nghệ ⇒ mời worker kéo Radix vào
SF-14: *"shadcn `Select`"*. ĐO:
- `fe-user/src/components/ui/select.tsx:1-21` = wrapper **`<select>` NATIVE**: `forwardRef<HTMLSelectElement, SelectHTMLAttributes<HTMLSelectElement>>` (`:4,:6`), render `<select className={cn(...)}>{children}</select>` (`:7-19`).
- `grep -rn "radix-ui/react-select" fe-user/package.json fe-user/src` = **0 hit** ⇒ repo KHÔNG có shadcn/Radix Select.
Worker "làm cho đúng shadcn" sẽ dựng `SelectTrigger/SelectContent/SelectItem` + thêm dependency = lệch khuôn, và mất luôn idiom đang đo ở `KhkkListPanel.tsx:352-359` (`value={phase ?? ''}` · `aria-label="Lọc trạng thái"` · `onChange={e => … Number(e.target.value) as KhkkPhaseValue}` · `<option value="">Tất cả trạng thái</option>`).
**Sửa:** *"`Select` của repo (`@/components/ui/select` — wrapper `<select>` native, KHÔNG Radix)"*.
### R-5 — MODERATE · SF-15 trích class KHÔNG literal trong đúng luật nói về class literal
SF-15 ghi header khuôn là `shrink-0 border-b px-6 py-3`. Chuỗi THẬT `fe-user/src/pages/khkk/KhkkListPage.tsx:157`:
```
flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-6 py-3
```
Hai vế còn lại của SF-15 lại literal chính xác (`:156` `flex h-[calc(100vh-4rem)] flex-col` ✓ · `:171` `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]` ✓) ⇒ đọc như thể cả 3 đều literal. Worker chép nguyên vế giữa mất `border-slate-200 bg-white` + toàn bộ căn `flex/items-center/justify-between/gap-3`.
**Sửa:** chép đủ chuỗi, hoặc chèn `…` như SF-12 đã làm đúng (`"border-dashed … text-center text-sm text-slate-500"`).
### R-6 — MINOR · SF-10 sai số đếm + sai khoảng anchor
SF-10: *"10/10 mutation `KhkkDetailContent.tsx:243-320` cùng dạng"*. ĐO `fe-user/src/components/khkk/KhkkDetailContent.tsx`:
- `grep -n "= useMutation"`**11** khai báo: `:243, :253, :266, :286, :296, :307, :316, :322, :340, :350, :364`
- `grep -n "onError"`**11** dòng `onError: (e) => toast.error(getErrorMessage(e))` (`:247,263,271,293,299,313,319,337,343,361,367`)
- Khoảng `:243-320` chỉ chứa **7** mutation.
Số đúng: **11/11, `:243-367`**. Nội dung luật không đổi, nhưng đây là con-số kiểm được sai — worker đối chiếu 1 lần là hạ tín nhiệm cả list.
### R-7 — MINOR · Bẫy-1 lệch anchor ±1..3 dòng (nhưng phần NẶNG thì ĐÚNG, và mạnh hơn mô tả)
- Bẫy-1 ghi mutation `removePlan :231-237`; thật là **`:230-240`** (`const removePlan = useMutation({``:230`, đóng `})``:240`).
- Dialog `:742-756` ✓ (thật `:742-758`, đủ gần).
- **Phần quan trọng ĐÚNG và đo được cứng hơn lời khai:** `grep -n "confirmDelete\|setConfirmDelete" fe-user/src/components/khkk/KhkkWorkflowPanel.tsx``:107` useState · `:234`/`:745`/`:748` đều set **`false`** · `:742` đọc. **0 caller `setConfirmDelete(true)`** ⇒ Dialog bất-khả-đạt = CODE CHẾT THẬT. Bia-mộ `:117-118` ✓ và `:438-442` ✓ đúng nguyên văn.
Đề nghị thay anchor số dòng bằng chính phép đo 0-caller (bền hơn, và chính SF-3 dạy vậy).
### R-8 — MINOR · Bẫy-9 chứa citation-trap: chạy lại đúng lệnh nó ghi sẽ ra kết quả NGƯỢC
Bẫy-9 ghi *"`grep 'lũy kế'` trong KHKK = 0 hit"*. ĐO hôm nay: `grep -rn "lũy kế" fe-user/src/components/khkk fe-user/src/pages/khkk` = **1 hit** — chính comment `KhkkListPanel.tsx:345-346` khai điều đó. Worker chạy lại lệnh, thấy 1 hit, có thể kết luận ngược ("KHKK có lũy kế ⇒ bê banner PE được") — đúng lớp lỗi mà Bẫy-9 sinh ra để chặn.
**Sửa:** phát biểu lại *"0 hit NGOÀI comment tự-khai"*, ô đo loại trừ dòng comment.
### R-9 — MINOR · C-6 xoá-là-đạt (mất WHY vẫn xanh)
C-6 = `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = 0. Control-dương ✓ (hôm nay **1 hit** tại `:142`: *"WfView cả 7 loại đều ?type=3 (HĐ chung 1 bộ workflow V2)"*). Nhưng **xoá trọn khối `:141-142` cũng cho 0** ⇒ đạt gate mà mất luôn phần WHY, trái chính SF-2 (*"LẬT comment cũ"*, không phải xoá).
**Sửa:** thêm ô dương ghép đôi — `grep -cE "7 bộ|per-loại|S188" fe-user/src/components/Layout.tsx` ≥1.
### R-10 — MINOR · 2/13 ô C là guard-giữ-nguyên (không delta), nên dán nhãn để khỏi đọc thành "đã kiểm 13 việc"
Đo trạng thái HÔM NAY:
- **C-4** `export default` = 0 ở 7/7 file wave (`fe-user` Layout · WorkflowMatrixViewPage · MyContractsPage · usePipelineStages · PipelineTreePanel · ContractDetailContent · `fe-admin` Layout) ⇒ luôn xanh.
- **C-8 vế 2** `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` — đã TRUE sẵn tại `fe-user/src/components/Layout.tsx:151` (spec W1-iv cũng ghi "GIỮ NGUYÊN").
Ô CÓ RĂNG (0 → mục tiêu), đã đo control-âm hôm nay: **C-9** (`grep -rn "'HD-" fe-user/src` = **0** ⇒ 0→7 thật) · **C-11** (`grid-cols-[400px` = 0; `MyContractsPage.tsx:153` đang `lg:grid-cols-[320px_1fr_360px]`) · **C-12** (`<Dialog` trong `ContractDetailContent` = **1**, tại `:372` ✓ đúng như C-12 khai) · **C-13** · **C-10** (fe-admin CÓ host thật để control-âm: `fe-admin/src/pages/contracts/ContractsListPage.tsx` + `HardCopiesPage.tsx` + `pages/khkk/KhkkListPage.tsx` đều dựng `PipelineTreePanel` ⇒ ô này không rỗng nghĩa).
### R-11 — MINOR · C-1 tóm tắt "máy phủ tới đâu" thiếu 1 preset
C-1 ghi máy = *"recommended + react-hooks + react-refresh (`fe-user/eslint.config.js:12-17`)"*. Anchor ✓ đúng, nhưng đọc ra thì `:12-17`**4** preset: `js.configs.recommended` (`:13`), **`tseslint.configs.recommended` (`:14`)**, `reactHooks.configs.flat.recommended` (`:15`), `reactRefresh.configs.vite` (`:16`). Bỏ sót `tseslint` = bỏ sót họ `no-explicit-any` / `no-unused-vars` TS ⇒ worker tưởng trục TS chưa có máy gác và đi đẻ luật tay chồng lên máy.
## Anchor đã kiểm — ĐÚNG (không cần sửa; liệt để lead khỏi kiểm lại)
| Luật | Anchor | Kết quả đo |
|---|---|---|
| SF-16 thanh nút đáy | `KhkkDetailContent.tsx:1085,1086,1090,1093,1095,1096` | ✓ ĐÚNG TỪNG CHỮ: `{!readOnly && (` · `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` · `window.confirm(\`Xóa phiếu ${plan.maKeHoach ?? ''}? Phiếu chuyển sang mục "Đã xóa".\`)` · `bg-red-600 … hover:bg-red-700 disabled:opacity-50` · `<Trash2 className="h-3.5 w-3.5" />` · `'Đang xóa…' : 'Xóa phiếu'` |
| SF-16 rào hiển-thị | `:1103-1135` (list ghi 1103-1119 / 1103-1125) | ✓ khối IIFE có thật, `title={why ?? …}` ở `:1125` |
| SF-10 invalidate/deletePlan/host-clear | `:193-197` · `:316-320` · `KhkkListPage.tsx:248` | ✓ 2 queryKey + `onChanged?.()` · `onSuccess: toast + onDeleted?.()` · `onDeleted={() => setId(null)}` |
| SF-10 bộ invalidate của CHÍNH file W5 | `ContractDetailContent.tsx:57-61` | ✓ đúng 3 key: `['contract', c.id]`, `['my-contracts']`, `['inbox']` |
| SF-17 mirror baseline | 4 cặp | ✓ **4/4 khớp và đúng prefix đã ghi**: `1b9b5c611663` · `4ffc96beded5` · `a2d486c5c366` · `cd81508f97cb` |
| SF-17 "ContractDetailContent chưa tự khai MIRROR" | header `:1-4` | ✓ ĐÚNG — 403 dòng, header nói "Reusable detail body…", 0 dòng MIRROR |
| SF-4 | `usePipelineStages.ts:228-230,:244,:377` · `PipelineTreePanel.tsx:109-110,:111` | ✓ ternary `opts.onOpenPe ? … : navigate` · `opts.onOpenKhkk ? …` · `onOpen: () => navigate(\`/contracts/${contractId}\`)` · JSDoc đối-xứng họ · `Pick<BuildStagesOptions,'stage2Content'\|'stage2Count'\|'onOpenKhkk'\|'onOpenPe'>` |
| SF-7 / SF-9 | `MyContractsPage.tsx:47,:67-71` · `KhkkListPanel.tsx:247` · `PurchaseEvaluationsListPage.tsx:75` · `usePipelineStages.ts:102` | ✓ positional ⟂ object-gói · `enabled: !!selectedId` · `enabled: canKhkk` |
| SF-8 | `WorkflowMatrixViewPage.tsx:88-95` | ✓ nguyên văn "đẻ 8 lần gọi mạng cho cùng một payload" + khớp bằng `code` |
| SF-11 | `PurchaseEvaluationsListPage.tsx:80,:91,:98` | ✓ deleted TRƯỚC pendingMe · `/inbox` · tự gói `{ items, total, page, pageSize }` |
| SF-12 | `KhkkListPage.tsx:195-203,:257-269` · `WorkflowMatrixViewPage.tsx:152-166` | ✓ cả 3 khuôn, kể cả vế "đếm phần bị lọc khuất" `:161` |
| SF-13 / SF-20 | `types/khkk.ts:39-55,:57` · `KhkkListPanel.tsx:155-158` | ✓ `as const` + `(typeof X)[keyof typeof X]` + bắt đầu từ 1 · Pattern 14 · thang 13/11/10 |
| SF-21 | `MyContractsPage.tsx:30-36,:60` · `WorkflowMatrixViewPage.tsx:84-86` | ✓ NaN-guard · `type: typeFilter ?? undefined` · clamp |
| SF-22 | `fe-admin/Layout.tsx:175-189` | ✓ `awV2Match` `:175`, nhánh chết `code === 'Contract'` `:178`, `KhkkN[1-8]` `:188`, khối comment `:181-187` |
| SF-23 | `fe-user/Layout.tsx:151,:180-185,:350,:358-369` | ✓ `?type=3&ct=` đã có sẵn · khối WHY `view=approved` · `TRANSIENT_QUERY_KEYS = new Set(['id','q','editHeader','page','phase','awId'])` (không chứa ct/view/pendingMe) · pinned-by-target |
| SF-18 / SF-19 | `fe-admin/Layout.tsx:37-39,:138-142,:185-187` · `KhkkListPage.tsx:253-256` · `contractWorkflowFamily.ts:16-18` | ✓ 5/5 khai-lệch-có-chủ-đích tồn tại đúng chỗ |
| SF-5 | `contractWorkflowFamily.ts:6-18,:69,:77-80` · `fe-user/Layout.tsx:16-29,:48` | ✓ khối "VÌ SAO LÀ FILE RIÊNG" · `findContractWfByTypeCode` · const-map |
| SF-5 "bản-sao-thứ-3" | `grep -rn "'HD-" fe-user/src` | ✓ **0 hit** ⇒ đúng là copy thứ 3 (BE .cs → fe-admin lib → fe-user mới) |
| C-9 đúng FILE | `fe-user/src/App.tsx:71,73,83` | ✓ `/contracts/workflow-matrix` render **chính** `pages/pe/WorkflowMatrixViewPage` ⇒ path trong C-9 KHÔNG sai |
| Bẫy-2 | `KhkkListPage.tsx:5` + sha | ✓ *"File MIRROR SHA256 identical với fe-admin counterpart"* và 2 bản **cùng sha `af8d121a5940`** |
| Bẫy-4/5/6/7 | `Layout.tsx:358-369` · `usePipelineStages.ts:88-89` · `types/khkk.ts:57` · `MyContractsPage.tsx:30-33` | ✓ cả 4 đúng nguyên văn |
| SF-16 câu confirm dùng được cho HĐ? (Bẫy-9 self-test) | `fe-user/Layout.tsx:153` | ✓ HĐ CÓ mục "Đã xóa" (`Ct_*_Deleted` → `/my-contracts?type=N&deleted=1`) ⇒ câu *"Phiếu chuyển sang mục 'Đã xóa'"* là mệnh-đề ĐÚNG trong ngữ cảnh HĐ (chỉ cần đổi "Phiếu" → "Hợp đồng") |
## Luật phải sửa/gỡ (đơn cho lead — thứ tự ưu tiên)
1. **THÊM** (R-1): dòng khuôn `fe-admin/src/pages/contracts/ContractsListPage.tsx:141,265` + luật *"nhúng `PipelineTreePanel` vào cột cuộn ⇒ quyết `bodyMaxHClass`, khai lý do"* + ô đo `grep -n "bodyMaxHClass" MyContractsPage.tsx`.
2. **SỬA SF-21** (R-2): thêm vế **gate theo type** trước clamp (khuôn `WorkflowMatrixViewPage.tsx:86`) + ô đo `grep -nE "typeInt === 3 &&"` ≥1.
3. **SỬA SF-24** (R-3): bỏ ngoặc kép literal → "khớp NGHĨA"; ô đo `grep -c "hông truyền"` ≥1.
4. **SỬA SF-14** (R-4): "shadcn `Select`" → "`Select` repo (`ui/select.tsx` — native `<select>`, KHÔNG Radix)".
5. **SỬA SF-15** (R-5): chép đủ chuỗi header `:157` hoặc chèn ``.
6. **SỬA SF-10** (R-6): "10/10 … `:243-320`" → "**11/11 … `:243-367`**".
7. **SỬA Bẫy-1** (R-7): `removePlan :231-237` → `:230-240`; thay bằng phép đo bền: *"0 caller `setConfirmDelete(true)`"*.
8. **SỬA Bẫy-9** (R-8): "0 hit" → "0 hit NGOÀI comment tự-khai" (hôm nay grep ra 1).
9. **THÊM ô C-6b** (R-9): `grep -cE "7 bộ|per-loại|S188" fe-user/src/components/Layout.tsx` ≥1.
10. **DÁN NHÃN** (R-10): C-4 và C-8-vế-2 = `[regression-guard, no-delta]`.
11. **SỬA C-1** (R-11): thêm `tseslint.configs.recommended` (`eslint.config.js:14`) vào phần "máy đã phủ".
**KHÔNG gỡ luật nào.** 25/25 SF đều có khuôn thật đứng sau; không có luật tự chế trái prod, không có luật mâu thuẫn spec v2.1.
— HẾT (sub-style-review-b4-fe1 · lane-1/2 ANCHOR+KHUÔN · read-only, không sửa code) —

View File

@ -1,16 +1,156 @@
# B4 ensemble — front-end-reviewer-style lane-2/2 (lens Ô-ĐO CHẠY-ĐƯỢC + GOODHART) # 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). > Soi lại `sub-style-fe-b3.md` §Checklist C-1..C-13 bằng cách **CHẠY THẬT** trên repo baseline (HEAD `b2f5d6f9`, 0 file `fe-*` dirty — `git status --porcelain` chỉ có `.claude/*` + `docs/*`).
> 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? > Mỗi ô: (a) baseline đo được (b) sau-wave có RĂNG không — **ca chết nào vẫn xanh?** (c) chạy được trên bash Windows không.
> Phép chứng răng = **fault-inject cây tạm** (scratchpad `c3lab/`, `lab7/9/13.tsx`), không phải happy-path.
## Verdict ## Verdict
_(điền cuối)_ **B4-FE-ODO: PASS_WITH_FIXES — 12 findings** (4 BLOCKER phải sửa TRƯỚC khi checklist đóng vai STYLE-GATE cho B6).
Phần **SPEC-LIST SF-1..25 + anchor**: 5/5 claim nặng em kiểm đều **ĐÚNG** (xem §Kiểm-chứng dưới) — không có ca "khuôn X sẵn" bịa như bài B2.
Phần **checklist C-1..C-13**: 1 ô **bất-khả-thi** (C-1), 3 ô **FALSE-GREEN đã chứng bằng fault-inject** (C-3 / C-7 / C-13), 1 ô **0 răng đúng trục nó sinh ra để canh** (C-9), 3 ô Goodhart-được (C-6 / C-11 / C-12), 1 ô hở phạm-vi (C-10), 1 ô đã-xanh-từ-baseline dễ đọc nhầm thành bằng-chứng (C-8 vế 2), 2 lỗi cơ-học (C-4/C-5 + exit-code).
## Kiểm-chứng claim của B3 (lens B2 "khuôn có sẵn" — MỞ RA ĐỌC)
| Claim trong `sub-style-fe-b3.md` | Đo được | Kết |
|---|---|---|
| SF-17 baseline SHA `1b9b5c61…`/`4ffc96be…`/`a2d486c5…`/`cd81508f…` | `sha256sum` fe-user: `1b9b5c611663` · `4ffc96beded5` · `a2d486c5c366` · `cd81508f97cb` | ✅ ĐÚNG 4/4 |
| SF-17 "`ContractDetailContent` identical nhưng **CHƯA tự khai MIRROR**" | 4 dòng header `:1-4` = mô-tả reusable body, **0 chữ MIRROR**; khuôn thật `KhkkDetailContent.tsx:2` = `// File MIRROR SHA256 identical với fe-admin counterpart.` | ✅ ĐÚNG |
| C-12 path `fe-user/src/components/contracts/ContractDetailContent.tsx` | file TỒN TẠI (21 359 B, ×2 app cùng byte); `window.confirm`=0; `<Dialog`=**1** đúng `:372` | ✅ path ĐÚNG |
| SF-22/C-13 anchor `fe-admin/Layout.tsx:178` nhánh chết · `:188` KhkkN | `:178 if (code === 'DuyetNcc' \|\| … \|\| 'Contract')` · `:188 if (/^KhkkN[1-8]$/…)`; khối `if (awV2Match)` mở `:176` đóng `:189`, `return null` `:191` | ✅ ĐÚNG |
| C-9 nguồn 7 token `ContractApprovalWorkflowFamily.cs:33-42` | `HD-TP · HD-GK · HD-NCC · HD-DV · HD-MB · HD-NT-NCC · HD-NT-DV` — khớp từng ký tự | ✅ ĐÚNG |
| C-1 "máy = recommended + react-hooks + react-refresh (`fe-user/eslint.config.js:12-17`)" | đúng 4 extends `:12-17`; `diff` 2 config = **IDENTICAL** | ✅ ĐÚNG |
## Findings (R-1..) ## Findings (R-1..)
_(điền dần)_ ### R-1 🔴 BLOCKER — C-1 "eslint PASS" **BẤT KHẢ THI**: baseline đã ĐỎ 2 app, và 4/4 lỗi rơi ĐÚNG file W1 sửa
Đo (`npx eslint .`, flat-config, `npm run lint` = `eslint .` — cú pháp ô ĐÚNG, chạy được):
- `fe-user`: **47 problems (41 errors, 6 warnings)**
- `fe-admin`: **42 problems (37 errors, 5 warnings)**
- 6 file wave sẽ đụng (fe-user): **4 errors — TẤT CẢ trong `Layout.tsx`**: `325:10`, `416:8`, `453:8` (*Cannot create components during render*), **`478:17` (*Calling setState synchronously within an effect*) = ĐÚNG khối accordion `:470-485` mà W1(a) viết lại.**
## Ô đo phải sửa Hai đường chết, cả hai đều tệ: worker đọc "PASS" ⇒ đi refactor hook-logic prod trong `Layout.tsx` (ngoài scope, đúng vùng bug A/B); hoặc thấy đỏ ⇒ **bỏ luôn C-1** ⇒ tầng máy chết câm — phá chính tiền-đề "máy trước, vai sau".
**Sửa (ô delta, neo số đã đo):**
```bash
# C-1a (app): npx eslint . ⇒ fe-user ≤ 47 problems (41E/6W) · fe-admin ≤ 42 (37E/5W) — KHÔNG TĂNG
# C-1b (file wave, ô CÓ RĂNG): chạy eslint CHỈ trên file wave đụng ⇒ ≤ 4 errors, và
# tập ruleId KHÔNG có mục mới; neo theo (file, ruleId), KHÔNG neo số dòng (dòng sẽ trượt)
```
🔴 kèm bẫy đo: `npx eslint . | tail -5; echo $?` in ra **0** dù eslint 41 errors (đo thật lượt này) — `$?` là của `tail`. Ô phải dùng `${PIPESTATUS[0]}` hoặc bỏ pipe.
_(điền dần)_ ### R-2 🔴 BLOCKER — C-3 in **"OK" khi CẢ HAI bản mất** (false-green; đã fault-inject)
```
FAULT-INJECT (scratchpad/c3lab): file không tồn tại 2 bên
→ sha256sum: No such file or directory (×2, ra stderr) → in "OK hooks/khong-co-file-nay.ts"
```
`$a` rỗng = `$u` rỗng ⇒ `[ "$a" = "$u" ]` TRUE. Ca chết thật: worker **đổi tên/di chuyển** file mirror, hoặc path trong checklist gõ sai (`components/contract/…`) ⇒ ô báo 4/4 OK trong khi **không đo gì cả**. (Ca mất 1 bên thì ô ĐÚNG — ra `LECH`.) Đây đúng lớp "vắng-mặt trông y hệt sạch".
**Sửa:** thêm guard tồn-tại trước khi so:
```bash
for f in …; do A="fe-admin/src/$f"; U="fe-user/src/$f"
{ [ -f "$A" ] && [ -f "$U" ]; } || { echo "MISSING $f"; continue; }
a=$(sha256sum "$A"|cut -c1-12); u=$(sha256sum "$U"|cut -c1-12)
[ "$a" = "$u" ] && echo "OK $f" || echo "LECH $f $a != $u"; done
```
Baseline sau khi sửa: **4/4 OK** (đã chạy — đúng kỳ vọng, `ContractDetailContent` identical từ trước).
### R-3 🔴 BLOCKER — C-7 đọc **XANH** khi key danh-tính ĐÃ nằm trong `TRANSIENT_QUERY_KEYS` (reformat xuống dòng)
Baseline: `fe-user/src/components/Layout.tsx:350` = `new Set(['id','q','editHeader','page','phase','awId'])` — 1 dòng, sạch.
```
FAULT-INJECT (lab7.tsx): Set tách 4 dòng, có 'ct','view'
grep -n "TRANSIENT_QUERY_KEYS = " → chỉ ra dòng `new Set([` ⇒ đếm 'ct|view|pendingMe' = 0 ⇒ XANH (SAI)
ô đề xuất grep -A6 … ⇒ 1 ⇒ ĐỎ (đúng)
```
Set hiện 6 phần tử; W1 thêm bất kỳ thứ gì là prettier/tay xuống dòng rất dễ. Bug S155 (leaf sáng nhầm) sẽ **lọt gate**.
**Sửa:** `grep -A6 "TRANSIENT_QUERY_KEYS = " fe-user/src/components/Layout.tsx | grep -cE "'(ct|view|pendingMe)'"` = **0**.
### R-4 🔴 BLOCKER — C-13 đọc **XANH** khi nhánh đặt NGOÀI khối `awV2Match` (đúng ca gotcha #50 mà W4 sinh ra để diệt) + lẫn dòng import
```
FAULT-INJECT (lab13.tsx): nhánh findContractWfByTypeCode đặt SAU khi khối awV2Match đã đóng
grep -n "findContractWfByTypeCode" → 2 hit: dòng 1 (import) + dòng 9 (nhánh)
"sau KhkkN?" head-1 ⇒ KHÔNG (false-alarm) · tail-1 ⇒ CÓ (false-green: leaf VẪN DROP)
```
Hai lỗi trong một ô: (a) không loại dòng `import` ⇒ kết quả phụ thuộc `head`/`tail` mà ô không nói; (b) **không có cận trên** ⇒ "sau `KhkkN`" thoả cả khi nhánh rơi ra ngoài `}` `:189` — khi đó `code` không còn là phần sau `AwV2_`, leaf drop im lặng đúng như #50.
**Sửa (ô range, dùng dạng GỌI + 2 mốc động):**
```bash
F=fe-admin/src/components/Layout.tsx
open=$(grep -n "if (awV2Match) {" $F | cut -d: -f1)
close=$(awk -v s=$open 'NR>s && /^ }$/{print NR; exit}' $F)
hit=$(grep -n "findContractWfByTypeCode(code)" $F | cut -d: -f1)
k=$(grep -n "KhkkN\[1-8\]" $F | cut -d: -f1)
[ -n "$hit" ] && [ "$hit" -gt "$k" ] && [ "$hit" -lt "$close" ] && echo OK || echo FAIL
```
Baseline: `hit` rỗng ⇒ FAIL (đúng — chưa làm). Mốc verify: `open=176 · KhkkN=188 · close=189 · return null=191`.
### R-5 🔴 BLOCKER — C-9 **0 RĂNG đúng trục nó sinh ra để canh** (drift 7 token) + 2 đường false-alarm
```
FAULT-INJECT (lab9*.tsx)
ca A map viết 1 DÒNG (đúng) : grep -c "'HD-" = 1 ⇒ ≠7 ⇒ BÁO ĐỎ OAN
ca B worker dùng "HD-TP" (đúng) : grep -c "'HD-" = 0 ⇒ BÁO ĐỎ OAN
ca C typo 'HD-NTNCC' (SAI) : grep -c "'HD-" = 2… và nếu 7 dòng thì =7 ⇒ XANH DÙ SAI
ô đề xuất (quote-agnostic, sort -u): A=7 ✅ · B=7 ✅ · C=6 ⇒ ĐỎ ✅
```
`grep -c` đếm **DÒNG** không đếm **lần**; và giả định nháy-đơn **không có gì cưỡng chế**: repo **không có `.prettierrc`** (`ls` 0 hit) và eslint config **không có rule `quotes`** (chỉ 4 `recommended`) — tỉ lệ nháy đơn chỉ là thói quen (`Layout.tsx` 40:4, `WorkflowMatrixViewPage.tsx` 22:2).
**Sửa:**
```bash
grep -oE "['\"]HD-(NT-NCC|NT-DV|TP|GK|NCC|DV|MB)['\"]" fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx | sort -u | wc -l # = 7
```
(alternation để token DÀI trước — đã chạy, bắt đúng typo.) Baseline: `grep -rn "'HD-" fe-user/src` = **0 hit** ⇒ 0 → 7 vẫn là ô tiến-độ tốt, chỉ cần đổi cách đếm.
### R-6 🟠 C-10 hở phạm-vi — `fe-admin/src/pages` **bỏ sót 2 host thật** đang truyền callback kiểu này
Đo: fe-admin host `PipelineTreePanel`**4 chỗ**, trong đó 2 chỗ NGOÀI `pages/`**đang truyền callback qua `stage2Options`**:
`fe-admin/src/components/khkk/KhkkListPanel.tsx:387` (`stage2Options={{ stage2Content, stage2Count, onOpenKhkk: onSelect, onOpenPe }}`) · `fe-admin/src/components/khkk/KhkkWorkListPanel.tsx:176`. (GĐ3 host thì đúng là ở `pages/`: `ContractsListPage.tsx:265` — nên scope cũ không sai hoàn toàn, chỉ **thủng**.)
**Sửa:** đo cả `fe-admin/src`, trừ đúng 2 file mirror có quyền chứa token:
```bash
grep -rn "onOpenContract" fe-admin/src | grep -vE "hooks/usePipelineStages\.ts|components/pipeline/PipelineTreePanel\.tsx" | wc -l # = 0
```
Baseline: 0 (toàn `fe-admin/src` cũng 0). Ô này còn CHẶT hơn: sau W2 nó chứng "định-nghĩa CÓ ở mirror, site truyền KHÔNG có ở fe-admin" = đúng control-âm W2-v.
### R-7 🟠 C-6 Goodhart — **xoá sạch** câu stale cũng cho 0
Baseline: `grep -c "chung 1 bộ" fe-user/src/components/Layout.tsx` = **1** (`:142` `// đều ?type=3 (HĐ chung 1 bộ workflow V2) — thêm &ct=N để active-state riêng từng leaf.`); toàn `fe-user/src`+`fe-admin/src` chỉ 1 hit ⇒ ô có răng, chạy được, `grep`/`rg` cho cùng kết quả (diacritic UTF-8 OK trên Git Bash).
Nhưng SF-2 đòi **LẬT** comment, mà `=0` thoả luôn khi worker **xoá cả dòng** ⇒ mất WHY của `&ct=N`, đẻ vùng câm mới.
**Sửa:** thêm ô sinh-đôi dương: `grep -cE "7 bộ|per-loại|S188" fe-user/src/components/Layout.tsx` **≥1** (baseline: cần đo lại sau wave; hiện khối `:141-142` không có chữ nào trong 3 chữ đó).
### R-8 🟠 THIẾU ô cho acceptance W5-iv (dòng tự-khai MIRROR) — C-3 **không thể** bắt
SHA-pair vẫn khớp khi **cả 2 bên đều thiếu** header tự-khai ⇒ spec ③ W5-iv + SF-17 yêu-cầu "thêm dòng tự-khai MIRROR cùng bytes" mà checklist **không có ô nào đo**.
**Thêm:** `grep -c "File MIRROR SHA256 identical" fe-admin/src/components/contracts/ContractDetailContent.tsx fe-user/src/components/contracts/ContractDetailContent.tsx`**1 mỗi bên** (baseline **0/0** — đã đo).
### R-9 🟠 C-11 Goodhart — đổi con số 320→400 **không chứng minh HOÁN ĐỔI**
Baseline: `MyContractsPage.tsx:153` = `lg:grid-cols-[320px_1fr_360px]`; `grep -F "grid-cols-[400px"` = 0 hit ⇒ ô có răng cho *bề rộng*, nhưng W2 là **đổi chỗ 2 khung**: sửa mỗi token vẫn xanh.
**Thêm ô đồng-hành (đã đo baseline = 1):** `grep -c "{!selectedId && <PipelineTreePanel" fe-user/src/pages/contracts/MyContractsPage.tsx`**0** sau W2 (cây thành panel-1 thường-trực, không còn treo vào `!selectedId`).
### R-10 🟡 C-8 — vế 2 **ĐÃ XANH TỪ BASELINE**, và vế 1 brittle theo nháy
Đo: `grep -rnF 'workflow-matrix?type=3&ct=' fe-user/src` = **1 hit sẵn** (`Layout.tsx:151`) ⇒ đây là **ô canh-thoái-lui** (spec W1-iv ghi "GIỮ NGUYÊN"), phải LABEL rõ trong checklist, kẻo đọc thành bằng-chứng W1 đã làm.
Vế 1 `grep -nF "get('ct')"` baseline = 0 (grep -F và `rg -F` **cùng 0** — không tái hiện lệch grep/rg của lens-3 B2). Nhưng dạng viết `get("ct")` / `params.get( 'ct' )` sẽ trượt ⇒ dùng `grep -nE "get\(['\"]ct['\"]\)"`. (Khuôn hiện có `Layout.tsx:472` `params.get('type')` — nháy đơn, nhưng không có máy nào cưỡng chế, xem R-5.)
### R-11 🟡 C-4/C-5 lỗi cơ-học + nguồn danh-sách file
- `grep -c` khi 0 hit in `0` nhưng **exit 1** ⇒ chuỗi `&&` / `set -e` của runner **đứt giữa chừng** đúng lúc ô ĐANG PASS (C-4 kỳ vọng 0!). Ô phải viết `grep -c … || true` hoặc so bằng `[ "$(grep -c …)" = 0 ]`.
- C-5 nói "`<danh sách file sửa>`" mà **không nêu lấy từ đâu**. Theo bài S179 của chính vai này: `git diff` **mù file untracked** ⇒ file MỚI (nếu wave đẻ) thoát ô comment-header. Nguồn đúng:
```bash
{ git diff --name-only origin/main...HEAD; git diff --name-only HEAD; git ls-files -o --exclude-standard; } \
| sort -u | grep -E '^fe-(admin|user)/src/'
```
(baseline cửa-sổ này = 0 file `fe-*` ⇒ mọi hit sau wave đều là của wave.)
### R-12 🟡 C-2 build — nửa `tsc` XANH baseline; nửa `vite build` CHƯA đo
`npm run build` = `tsc -b && vite build`. Đo được: `npx tsc -b` (fe-user) **exit 0, 0 dòng lỗi** ⇒ tầng type baseline SẠCH — nên C-2 **không** dính bệnh R-1 (khác hẳn eslint). Chưa đo: `vite build` (bundle) và `tsc -b` của `fe-admin` ⇒ 2 ô này để **CHƯA ĐO**, cấm đọc thành xanh.
Hệ quả cho gate: C-2 là ô **có răng thật** (baseline sạch ⇒ đỏ sau wave = do wave) — ngược với C-1. Ghi rõ cặp này trong checklist để worker không gộp "máy" thành một cục.
## Ô đo phải sửa (bảng gọn cho B6)
| Ô | Baseline ĐO ĐƯỢC | Bệnh | Ô thay thế (đã chạy thử) |
|---|---|---|---|
| C-1 | fe-user 47 (41E/6W) · fe-admin 42 (37E/5W) · 6 file wave = 4E toàn `Layout.tsx` {325,416,453,478} | PASS bất-khả-thi | delta: app không tăng · file-wave ≤4E, 0 ruleId mới; dùng `${PIPESTATUS[0]}` |
| C-3 | 4/4 OK | mất-2-bên ⇒ in OK | thêm `[ -f A ] && [ -f U ] \|\| echo MISSING` |
| C-6 | `chung 1 bộ` = 1 (`Layout.tsx:142`) | xoá-trắng cũng =0 | + ô dương `grep -cE "7 bộ\|per-loại\|S188"` ≥1 |
| C-7 | Set 1 dòng `:350`, 0 key danh-tính | reformat ⇒ xanh giả | `grep -A6 … \| grep -cE "'(ct\|view\|pendingMe)'"` = 0 |
| C-8 | vế1 = 0 · **vế2 = 1 (đã xanh)** | vế2 là canh-thoái-lui; vế1 brittle nháy | label rõ + `grep -nE "get\(['\"]ct['\"]\)"` |
| C-9 | 0 hit `'HD-` trong `fe-user/src` | đếm DÒNG · giả định nháy · typo vẫn xanh | `grep -oE "['\"]HD-(NT-NCC\|NT-DV\|TP\|GK\|NCC\|DV\|MB)['\"]" \| sort -u \| wc -l` = 7 |
| C-10 | 0 (cả `fe-admin/src`) | scope `pages/` bỏ sót 2 host `components/khkk/` | quét `fe-admin/src` trừ 2 file mirror ⇒ 0 |
| C-11 | `grid-cols-[320px_1fr_360px]` `:153`; 400px = 0 | đổi số ≠ hoán đổi | + `{!selectedId && <PipelineTreePanel` : 1 → **0** |
| C-12 | `window.confirm`=0 · `<Dialog`=1 (`:372`) | ≥1 xanh dù message thiếu mã/đích (SF-16) | + `grep -A2 "window.confirm" \| grep -c 'Đã xóa'` ≥1 |
| C-13 | 0 hit (`open=176·KhkkN=188·close=189`) | ngoài-khối vẫn xanh · lẫn import | ô range `open < hit < close` + dạng gọi `(code)` |
| — | `File MIRROR SHA256 identical` = 0/0 | **thiếu ô** cho W5-iv | thêm ô = 1 mỗi bên |
| C-4/C-5 | 0 file `fe-*` trong cửa-sổ | exit-1 khi PASS · thiếu nguồn danh-sách | `|| true` + union `git ls-files -o` |
— HẾT (`sub-style-review-b4-fe2.md` · read-only lens, propose-only) —