wal: flush 20260808T0224

This commit is contained in:
pqhuy1987
2026-08-08 02:24:47 +07:00
parent affaf66c61
commit 1642c89912
3 changed files with 309 additions and 37 deletions

View File

@ -1,6 +1,6 @@
# Reviewer Agent — Persistent Memory
- **S182 (08-08) REVIEW-1 Fable spec KHKK-AWF-mirror — `PASS_WITH_FIXES 2C/5M` (đĩa `runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-review-1-fable.md`):** review SPEC = đo lại claim trên code, đa số HELD nhưng: 🔴 **spec-unbuildable vì assembly-ranh** — "extract helper + gọi 3 site" chết vì site-3 (Submit) ở Infrastructure còn helper `internal` Application, `InternalsVisibleTo` chỉ mở Tests ⇒ kiểm ACCESSIBILITY mọi extract-helper đa-assembly trước khi phán vá-rẻ · 🔴 **usage-count trên bng có `HasQueryFilter` thiếu `IgnoreQueryFilters()`** = đếm sót phiếu xoá-mềm mà FK vẫn chặn ⇒ test-hẹp xanh, ca phổ biến vẫn 500 (claim "hết 500" mạnh hơn fix) · 🔴 **"FK chặn nên data không mất" phải grep FK THẬT**: 8 cột pin ⟂ 4 `HasForeignKey` hit ⇒ 5 pin loose-Guid hard-delete êm + dangling — invest suy từ 3 module đã biết · **2 invest cùng chữ "per-Code" ≠ cùng công-thức** (typeEnum==10 ⟂ regex-họ-mã, KHÁC hành vi ở code ngoài-họ) — spec khai "hội tụ" che lệch ⇒ khoá công thức + test ca phân-kỳ · **cờ ignored-nhưng-hành-vi-luôn-bật** gray-out kiểu "chưa áp dụng" = nói dối chiu ngược ⇒ forced-true-disabled · baseline "645" spec ⟂ STATUS 644 — đo tươi đừng chép. ⚠️ sổ này `wc -c` 21.058B — nghi chạm cap ký-tự, lượt sau đo + curate đúng trình tự.
- **[→ `archive/2026-08.md` §@S182 — verbatim 2 lượt, probe 7/7 + control-âm 0] S182 (08-08) YC-013 KHKK 8-panel — lượt SPEC `2C/5M`, lượt DIFF pre-commit `0C/3M`+6m:** 🟢 **3/3 finding 🔴 lượt-1 được đóng ĐÚNG BẢN CHẤT** (assembly-ranh → file `public static class` riêng · `IgnoreQueryFilters` **2 CHIỀU NGƯỢC NHAU trong cùng 1 diff**: DELETE bỏ filter vì FK còn thật ⟂ changelog GIỮ filter vì ghi vết phiếu-đã-xoá là rác · 8 module pin verified bng grep Domain, loại +1 hit FK nội bộ) — dữ liệu hiếm, ghi để đừng phá. 🔴 **tooltip đặt trên `<input disabled>` ⇒ Chromium KHÔNG hiện** (label bao ngoài không mang `title`) — quyết-định-owner nằm trong tooltip mà tooltip không có ca hiện = **máy dựng không ai gọi**; ô "hành-vi-LUÔN-BẬT" câm ⇒ đọc thành NGƯỢC NGHĨA. 🔴 **3 thành phần bất đồng HOA/thường** (regex CultureInvariant **case-SENSITIVE**`Code ==` theo collation SQL Server **CI** ⟂ FE `.toUpperCase()`) và **test MÙ THEO CẤU TRÚC vì SQLite = BINARY case-sensitive ⇒ tầng DB trong test có ngữ nghĩa NGƯỢC prod** — không assertion nào bịt được. 🔴 **guard mới ở write-path CHÍNH (Create) 0 test** — lỗ của SPEC, implementer đúng spec, **KHÔNG hạ thước theo spec**. **m:** assert `Contain("N5")∧Contain("N3")` **mù THỨ TỰ toán hạng** (3 call-site 2 tham số kề — phải đối chiếu TAY) · **BOM `ef bb bf`+CRLF lẫn vào commit code**`git add` từng file. **FLAG-A đo được:** `npx tsc --noEmit` trần biên dịch **0 file** (`files: []`+references) = xanh THEO CẤU TRÚC; `--listFiles|wc -l` 0 ⟂ 225/214 ⇒ **acceptance "lệnh X xanh" phải kèm phép chứng thước KHÔNG RỖNG**.
- **[→ `archive/2026-08.md` §@S180 — verbatim, probe 10/10 + control-âm 0] S180 (08-07) LANE A/6 GĐ-0+GĐ-1 (đĩa `runs/…-S180-adap-upgrade-pack-phased/sub-n3-lane-a-gd01.md` 57 KB):** 🔴 **"0 hit" ĐÚNG THEO LUẬT vẫn là LỖ THẬT** — `_index` thiếu **24/69** thư (lead giao "4"); `to=se` **23/23 sạch**`to=all-fit` 22 có/20 thiếu; luật `:7` trỏ `COMMS-LEDGER``find "*COMMS*"`=**0 trong SE** (control `*ledger*`=4) ⇒ **sổ mà luật trỏ tới KHÔNG TỒN TẠI**; S153 phán dương-giả = đúng cho câu-hỏi CŨ. ⇒ gặp 0-hit phải hỏi thêm **"sổ đó có tồn tại không"** · **phân-họ regex 160/159, lệch 1 vì `S57bis`** ⇒ luôn cộng lại đối-chiếu tổng · **`CLAUDE.md` quy-ước nhật-ký khớp ≤64/159 đĩa** · **"single source" tự-xưng ≠ chuẩn** (bảng-dịch; 3 phép thử tiên-đoán/phủ-sóng/hướng) · **số-trong-chú-thích trôi** (`hmw.js` "11 class" ⟂ đĩa **12**).
@ -81,20 +81,16 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
- Hook-cap **>17.1KB** (24.4KB read-limit) → move recent → L2 `archive/<period>.md` (additive) + `_INDEX.md` pointer. Stale >3mo → remove.
- **Trình tự BẮT BUỘC (S102 recovery, cut-not-moved ×5 `f229b07`):** append archive TRƯỚC → probe moved-not-cut **bằng token lấy từ THÂN BÀI, không lấy từ digest** (digest là paraphrase ⇒ 0-hit GIẢ; @S168 suýt giữ 938 B vô ích) + control-âm → mới cắt.
- **3 bài đo:** số trong sổ phải là số **ĐO** không ước (`wc -c`, baseline thừa-kế cũng stale) · script sinh chữ **đẻ chữ hỏng câm** (byte-count không bắt, chỉ ĐỌC LẠI mới thấy) · hook đếm **KÝ TỰ** còn `wc -c` đếm **BYTE** — tiếng Việt đa-byte ⇒ 2 số lệch nhau là bình thường, đừng truy "ai sai".
- **Prev:** S180 (probe 10/10 + control-âm) · S168 (22.2→17.1) · S166-K5 (3/3) · S162 (2/2) · S145 · S140 · S134 · S128 · S102 · S116 · S113 · S109 · S80 · S71 · S70 · S40.
- ⚠️ **NỢ CÒN LẠI (đo, không ước):** curate S182 đưa **22.518 → 20.292 B** (2.226, đồng thời THÊM entry S182) — **vẫn trên trần mềm 17.1KB**, còn dưới trần đọc 24.4KB nên chưa truncate. Lượt sau cắt tiếp: 2 bullet dài nhất là **S182 (dòng 3)****digest gộp S167-K6… (dòng ~17)**, cả hai đã có verbatim trong `archive/`. 🔴 **2 bài THƯỚC phiên này:** probe archive bằng token lấy từ **DIGEST L1** cho **3/7 = 0-hit GIẢ** (archive dùng chữ khác) ⇒ luôn lấy token từ **THÂN BÀI** · **`grep -icF` trong MSYS trả CHUỖI RỖNG thay vì `0`** ⇒ đếm bằng `python -c "s.count(...)"`, đừng tin `$(grep -c)`.
- **Prev:** S182 (22.5→20.3, probe python 7/7 + control-âm 0) · S180 (probe 10/10 + control-âm) · S168 (22.2→17.1) · S166-K5 (3/3) · S162 (2/2) · S145 · S140 · S134 · S128 · S102 · S116 · S113 · S109 · S80 · S71 · S70 · S40.
- **[→ `archive/2026-08.md` §@S152-R1 — verbatim] S152 R1 ranh NEVER:** verdict-only; NEVER áp cho ĐÍCH (code-tree·commit·push — chưa từng vỡ); Write/Edit runtime hợp lệ cho sub-file run-folder + diary (AS-10); containment thật = git-diff lead.
- **[→ archive/2026-07.md @S165-curate] S159 đợt-1 (07-29) diff pipeline — 7 FLAG:** rút TOÀN BỘ chuỗi hiển-thị MỚI từ diff rồi soi từng chuỗi = phép đo 100%-tiếng-Việt · 2 queryKey cùng resource = 2 ảnh-chụp khác thời-điểm · a11y nút TRÙNG tên đọc-được · bác-claim cũng phải có chứng.
- **[→ `archive/2026-08.md` §@S176 — verbatim] S176 (08-06) SETUP-REVIEW đội STYLE 3 vai — `SỬA-6`:** chấm **THIẾT-KẾ một đội sub-agent**, không phải diff code (trục mới của vai). 🔴 **thước dispatch MÙ WORKING-TREE** (`git diff origin/main...HEAD` ba-chấm trả RỖNG ⇒ đội gate pre-commit **chết câm** đúng use-case chính) · **đo TREE ≠ đo HISTORY** (thước tree tự lành ca revert ⇒ lo-nghi "revert phá thước" = lo hão) · **miễn-trừ chỉ-con-trỏ = dấu cao-su** ⇒ đòi TRÍCH NGUYÊN VĂN ≤1 dòng, khép vòng bằng owner-visibility · **thách-CLEAN bẻ được nếu không bắt neo `file:line` cả 2 bờ** · **3/5 lo wiring của lead là LO HÃO** (script enumerate ĐỘNG — đừng giả-định hardcode, mở ra đo) · **chép-luật-vào-persona vi phạm chính B1 viết trong CÙNG file** ⇒ viết luật cấm-chép xong phải grep chính file mình. *(🔄 @S180: khoản "3 vai thiếu dir agent-memory" ĐÃ ĐÓNG — 26⟂26 khớp.)*
- **[→ `archive/2026-08.md` §@S176 — verbatim] S176 (08-06) SETUP-REVIEW đội STYLE 3 vai — `SỬA-6`:** chấm **THIẾT-KẾ một đội sub-agent**, không phải diff code. 🔴 **thước dispatch MÙ WORKING-TREE** (`git diff origin/main...HEAD` ba-chấm trả RỖNG ⇒ gate pre-commit chết câm đúng use-case chính) · **đo TREE ≠ đo HISTORY** · **miễn-trừ chỉ-con-trỏ = dấu cao-su** ⇒ đòi TRÍCH NGUYÊN VĂN ≤1 dòng · **thách-CLEAN bẻ được nếu không bắt neo `file:line` cả 2 bờ** · **3/5 lo wiring của lead là LO HÃO** (script enumerate ĐỘNG — mở ra đo) · **chép-luật-vào-persona vi phạm chính B1 viết trong CÙNG file**.
- **[→ `archive/2026-08.md` §@S180-LANE-B — verbatim, probe 3/3 + control-âm 0] S180 (08-07) N3 LANE-B GĐ-2 (digest):** NGHI-THỨC ≠ MÁY đọc CỘT KIỂU error-ledger (AS-17 `procedural`, AS-19 tự khai chưa-cơ-khí-hoá) · guard-byte MÙ skeleton ⇒ vị-từ nhãn-treo-heading (use⟂mention, 0 dương-giả) · sổ RETIRED-có-khai vẫn làm điểm-danh-2-nguồn MẤT VẾ PHẢI (73/88); nguồn-2 = taskList snapshot run.md, KHÔNG phải `run-chua-gom` (đọc đĩa = nguồn-1) · secrets_sweep chỉ rglob sessions/ + runs/ excluded ⇒ vật chứng đắt nhất ngoài 2 lưới cùng lúc · tỉ lệ không mẫu số = vô nghĩa (42%/72%/25% cùng 1 sự thật) · lead-stale-auditor 0-hit runs/ (control FLAG=4).
- **[→ `archive/2026-08.md` §@S180-LANE-B — verbatim, probe 3/3 + control-âm 0] S180 (08-07) N3 LANE-B GĐ-2 (digest):** NGHI-THỨC ≠ MÁY (đọc CỘT KIỂU error-ledger) · guard-byte MÙ skeleton ⇒ vị-từ nhãn-treo-heading (use⟂mention) · sổ RETIRED-có-khai vẫn làm điểm-danh-2-nguồn MẤT VẾ PHẢI · **secrets_sweep bỏ sót đúng thư-mục chứa vật chứng đắt nhất** (2 lưới hụt cùng lúc) · **tỉ lệ không mẫu số = vô nghĩa** (3 số cùng 1 sự thật) · auditor 0-hit vì không quét `runs/` (control FLAG=4).
- **[→ `archive/2026-08.md` §@S180-ba-khối — verbatim, probe 6/6 + control-âm 0] S180 (08-07) N4 wave-plan · W0 lane-a fitmap · W0 lane-b `_index` (digest gộp):** **ô-đòi-MÃ không nhận CÂU-VĂN** (wave không trỏ mã TC = thoát ràng buộc nghiệm thu) · tách trục **CHỮA ⟂ NGHIỆM-THU** (thiếu THƯỚC ≠ thiếu THUỐC) · 2 hệ đánh số trong 1 artifact phải khác TIỀN-TỐ · **ghi-đĩa-4-nhát qua Edit-append anchor-đuôi** (13 vai dính #53, 4 nhát land sạch).
- 🔴 **acceptance ĐO HÌNH-DẠNG-DÒNG, KHÔNG ĐO GIÁ-TRỊ-Ô**`grep -c '^| GĐ-[0-6]'` vẫn ra 7 dù ô điền sai từ-vựng (từ-vựng Phần C nhét vào ô Phần A ⇒ đẻ giá-trị-thứ-tư, đúng lỗi file sinh ra để đóng, nghiệm thu vẫn xanh) ⇒ thêm phép đo GIÁ-TRỊ · **2 tệp anh em cùng bảng 7 dòng = dương-giả thật** ⇒ chạy chéo 2 chiều + control-dương · **`grep -c` exit 1 khi 0-hit CẮT ĐỨT chuỗi `&&`** · **1 phép đo 3 nguồn 3 số** ⇒ chép PHẦN LIỆT KÊ, đừng chép số tổng.
- 🔴 **W0-b, 3 bài đắt nhất:** (a) **lệnh lead sai theo chiều PHÁ chính mục-đích lệnh***"backfill `se` cho 22 dòng cũ"* nhưng sổ có **45 dòng** (23 `se` + 22 `all`) ⇒ gán hàng loạt dán nhãn sai lên đúng 22 dòng fan-out mà P6 sinh ra để hợp-thức-hoá; gốc = **hai số 22 khác nghĩa va nhau trong CÙNG bản giao việc** ⇒ trị bằng + nghĩa ngược = con số thôi làm chứng, dẫn giá-trị **từng dòng từ đĩa**. (b) **`grep -c <substring>` có TRẦN CỨNG** — lead đọc `=4` thành *"mới bù 4"* trong khi **17 dòng đã land****biết tên bẫy KHÔNG đủ, phải khai mẫu-số NGAY CẠNH con số**. (c) **glob `*/` bỏ trọn thư-mục GỐC** ⇒ mù đúng nơi thư *chưa xử lý* hạ cánh; 69/24 khớp chỉ vì gốc chỉ có `README.md` = trùng hợp, không phải tương đương.
- 🟢 **Khuôn tái dùng:** thêm cột vào sổ bằng-chứng thì đặt **CUỐI hàng***"không sửa dòng cũ"* thành **phép kiểm chạy được** (dòng cũ = **tiền-tố nghiêm ngặt** của dòng mới; 45/45, khối kia byte-identical). 🔸 `status` **định-nghĩa-bằng-thư-mục** ⇒ gộp 2 kênh làm cột chỉ đo đúng NỬA số dòng (fan-out `/adap-apply` không đụng thư mục ⇒ `processed` = *đã move*, KHÔNG phải *đã áp*). 🔸 **mốc nước nói về VẮNG-KHỎI-HỘP**, không miễn cho thư ĐÃ NẰM TRONG hộp mà thiếu dòng — **lead nhận lập luận, phán bù nốt 7** ⇒ đợt-2: 24/24 dòng bù, `stamp_verify.py` **24/24 exit 0 · 0 tamper**, vắng-mặt **24→0**, 5/5 bất-biến đo LẠI vẫn đạt. `status: DRAFT` ⟂ đòi-dấu-đã-duyệt = **lệch trạng-thái, KHÔNG tamper**. 🔸 2 số 69 (đĩa ⟂ dòng) **KHÔNG phải phép đo thứ hai** — là hệ quả của (0 vắng ∧ 0 mồ-côi ∧ 0 trùng).
- **2 thước hỏng:** **heredoc `<<'EOF'` gãy** với tiếng Việt + backtick (lead dính y hệt) ⇒ tệp dài dùng **Write/Edit** · **probe `grep -F` sai HOA/thường ⇒ 0-hit GIẢ** (`ô-``Ô-`) ⇒ 0-hit phải thử lại **đúng ca chữ TRƯỚC** khi kết luận mất nội dung.
- Tag `[s180, w0-lane-b, colliding-22, grep-substring-ceiling, glob-star-slash-blind, prefix-preserving-column, status-defined-by-folder, shape-vs-value-acceptance, heredoc-broken, probe-case-trap]`
- **[→ `archive/2026-08.md` §@S180-ba-khối — verbatim, probe 6/6 + control-âm 0] S180 (08-07) N4 wave-plan · W0 lane-a/b (digest gộp):** **ô-đòi-MÃ không nhận CÂU-VĂN** · tách trục **CHỮA ⟂ NGHIỆM-THU** · 2 hệ đánh số trong 1 artifact phải khác TIỀN-TỐ · 🔴 **acceptance ĐO HÌNH-DẠNG-DÒNG chứ không ĐO GIÁ-TRỊ-Ô** (`grep -c '^| GĐ-…'` vẫn xanh dù ô điền sai từ-vựng) · **2 tệp anh em cùng hình-dạng = dương-giả thật** ⇒ chạy chéo 2 chiều + control-dương · **`grep -c` exit 1 khi 0-hit CẮT ĐỨT chuỗi `&&`** · **lệnh lead có thể sai theo chiều PHÁ chính mục-đích lệnh** (2 số cùng trị khác nghĩa va nhau trong CÙNG bản giao việc) ⇒ dẫn giá-trị **từng dòng từ đĩa** · **`grep -c <substring>` có TRẦN CỨNG** ⇒ khai mẫu-số NGAY CẠNH con số · **glob `*/` bỏ trọn thư-mục GỐC** · 🟢 thêm cột thì đặt **CUỐI hàng** ⇒ "không sửa dòng cũ" thành phép kiểm chạy được (tiền-tố nghiêm ngặt) · `status` **định-nghĩa-bằng-thư-mục** đo đúng NỬA số dòng · **2 số bằng nhau từ 2 nguồn KHÔNG phải phép đo thứ hai** nếu số sau là hệ-quả số trước · thước hỏng: **heredoc `<<'EOF'` gãy** với tiếng Việt+backtick ⇒ dùng Write/Edit · **probe `grep -F` sai HOA/thường ⇒ 0-hit GIẢ**.

View File

@ -93,3 +93,40 @@
- `stamp_verify.py` **17/17 OK, exit 0, 0 tamper**; 4 thư gói khai `content_sha256` **8-hex** (script so tiền-tố). 4 thư mang `status: DRAFT`+`reviewer_gate: PENDING``thu-chinh` đòi dấu đã-duyệt ⇒ **lệch trạng-thái, KHÔNG tamper**.
- Thước hỏng phiên này: **heredoc `<<'EOF'` gãy với nội dung tiếng Việt + backtick** (lead dính y hệt) ⇒ ghi tệp dài dùng **Write/Edit**, đừng `cat >>`.
- Tag `[s180, w0-lane-b, broadcasts-index, p6-additive, colliding-22, grep-substring-ceiling, glob-star-slash-blind, prefix-preserving-column, status-defined-by-folder, watermark-scope, heredoc-broken]`
---
## @S182 — YC-013 KHKK 8 panel nhóm mirror DuyetNcc (2 lượt review: SPEC rồi DIFF)
**Đĩa:** `runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-review-1-fable.md` (lượt SPEC) · `.../sub-reviewer-precommit.md` (lượt DIFF).
### Lượt 1 — review SPEC (`PASS_WITH_FIXES 2C/5M`)
Chấm SPEC = đo lại claim của spec trên code. Đa số HELD, nhưng:
- 🔴 **spec-unbuildable vì ASSEMBLY-RANH.** Spec bảo "extract helper rồi gọi 3 site" — chết ở site-3 (`SubmitAsync`) vì site đó nằm ở **Infrastructure** còn helper định để `internal` trong **Application**, và `InternalsVisibleTo` chỉ mở cho Tests. ⇒ **Luật rút ra:** mọi đề-xuất extract-helper đa-assembly phải kiểm ACCESSIBILITY trước khi phán "vá rẻ".
- 🔴 **usage-count trên bảng có `HasQueryFilter` mà thiếu `IgnoreQueryFilters()`** = đếm sót phiếu xoá-mềm trong khi FK vẫn chặn ⇒ test hẹp xanh, ca phổ biến vẫn 500. Claim "hết 500" MẠNH HƠN bản vá.
- 🔴 **"FK chặn nên data không mất" phải grep FK THẬT.** 8 cột pin ⟂ chỉ 4 `HasForeignKey` hit ⇒ 5 pin loose-Guid hard-delete êm + dangling. Suy từ 3 module đã biết là suy sai.
- **2 invest cùng dùng chữ "per-Code" nhưng KHÔNG cùng công-thức** (`typeEnum==10` ⟂ regex-họ-mã) — khác hành vi ở code ngoài-họ. Spec khai "hội tụ" ⇒ che mất chỗ lệch. Phải khoá công thức + test ca phân-kỳ.
- **Cờ ignored-nhưng-hành-vi-luôn-bật**: gray-out kiểu "chưa áp dụng" = nói dối theo CHIỀU NGƯỢC.
- Baseline "645" trong spec ⟂ STATUS 644 — đo tươi, đừng chép.
### Lượt 2 — review DIFF pre-commit (`PASS_WITH_FIXES 0C/3M` + 6 minor)
**Đo độc lập (không nhận số của lane):** `dotnet test` = 45 + 614 = **659 P / 0 F** (baseline 645 = 45+600 ⇒ +14 đúng ở Infrastructure) · `npm run build` ×2 exit 0 · tsc `-p tsconfig.app.json` 0-err (225 / 214 file).
**3/3 finding 🔴 của lượt 1 được đóng ĐÚNG BẢN CHẤT, không đóng bằng chữ** — đây là dữ liệu hiếm: (a) assembly-ranh → file `public static class` riêng ở Application, 3 call-site, đối số đúng thứ tự cả 3; (b) `IgnoreQueryFilters` đúng 3 bảng có filter, và **phân biệt được 2 CHIỀU** (DELETE cần bỏ filter vì FK còn thật; changelog CỐ Ý giữ filter vì ghi vết cho phiếu đã xoá là rác) — cùng một hàm-gọi, hai ngữ nghĩa ngược nhau, có khai tại chỗ; (c) 8 module pin verified bằng `grep -E "public (Guid|Guid\?) ApprovalWorkflowId"` trên Domain (đúng 8, +1 hit FK nội bộ của Step phải loại), 5 module loose-0-FK được khai là "rào DUY NHẤT" và T12 chứng minh dangling THẬT trong DB.
**MAJOR-1 — tooltip gắn trên `<input disabled>` ⇒ Chrome/Edge không hiện.** 7 `title=` đặt trên chính checkbox bị `disabled`; Chromium chặn sự kiện chuột trên form-control disabled nên native tooltip thường không bật (Firefox thì có), và `<label>` bao ngoài KHÔNG mang `title` nên rê vào chữ cũng không ra. Nặng nhất ở ô #4: câu riêng của nó là thông tin DUY NHẤT phân biệt "cờ chưa phân nhánh" với 6 ô "chưa áp dụng" ⇒ tooltip câm = ô #4 đọc thành NGƯỢC NGHĨA, đúng class lỗi mà ERRATA vừa sửa xong ở tầng `checked`. **Luật rút ra: quyết-định-của-owner nằm trong tooltip thì phải kiểm tooltip có CỬA HIỆN không — máy dựng mà không ai gọi.** Tiền lệ cùng bệnh đã tồn tại từ đợt trước ⇒ vá phải vá cùng LỚP.
**MAJOR-2 — 3 thành phần bất đồng HOA/thường, và test MÙ THEO CẤU TRÚC.** Cùng câu hỏi "code này có phải nhóm N{n}", ba luật khác nhau: regex `^KHKK-N([1-8])$` **CultureInvariant, KHÔNG IgnoreCase** (case-sensitive) ⟂ `w.Code == request.Code` chạy theo **collation SQL Server `_CI_AS`** (case-INsensitive) ⟂ FE `.toUpperCase()` (case-insensitive). Hệ quả: mã `khkk-n1` gõ tay ở panel gộp sẽ **hạ IsActive bản thật**, **vẫn được FE auto-pin**, mà **rào nhóm + rào version-cuối TẮT im lặng**. 🔴 **Test không thể thấy vì fixture chạy SQLite (BINARY = case-SENSITIVE) ⇒ tầng DB trong test có ngữ nghĩa NGƯỢC với prod.** Đây không phải test ẩu — mù theo cấu trúc, không assertion nào bịt được. **Luật rút ra: mọi bất-biến dựa trên so-sánh CHUỖI ở tầng DB đều không được test-SQLite bảo chứng; chuẩn hoá ở ĐƯỜNG GHI (`Trim().ToUpperInvariant()`) hoặc khai phụ-thuộc-collation ngay tại chỗ.**
**MAJOR-3 — guard mới ở write-path CHÍNH có 0 test.** Rào D6 cắm 2 site (Create + UpdateDraft) nhưng 4 chân của ca test đều đi qua UpdateDraft ⇒ nhánh Create (đường mọi phiếu prod sinh ra) không có răng nào. Gỡ nó đi thì 659 test vẫn xanh. Đây là **lỗ của SPEC** (spec liệt 14 ca và thiếu ca này), implementer làm đúng spec — nhưng **không hạ thước theo spec**.
**MINOR đáng nhớ:** (1) `Contain("N5").And.Contain("N3")` **mù THỨ TỰ toán hạng** — hàm mới nhận 2 tham số kề nhau ở 3 call-site, đảo là vẫn xanh (tôi phải đối chiếu TAY mới chốt được cả 3 đúng); pin nguyên câu có thứ tự mới giữ được bất-biến. (2) File governance bị thêm **BOM `ef bb bf` + CRLF** lẫn vào commit code (HEAD `7b 0a` → worktree `ef bb bf 7b 0d 0a`) ⇒ `git add` từng file, đừng `-A`. (3) rào version-cuối đếm anh-em **KHÔNG lọc `ApplicableType`**. (4) "state THẬT" của ô disabled chỉ đúng cho đường CLONE/EDIT — đường TẠO MỚI thì 6 ô khoá bị đóng băng ở DEFAULT của FE, admin không sửa được.
**FLAG-A của FE lane — đo được và ĐÚNG:** `npx tsc --noEmit` trần ở repo này **biên dịch 0 file** (`tsconfig.json` = `{"files": [], "references": [...]}`) ⇒ luôn xanh **theo cấu trúc**. Chứng bằng `--listFiles | wc -l`: **0** dòng cho dạng trần ⟂ **225 / 214** dòng cho `-p tsconfig.app.json`. 🔸 Bổ sung: `npm run build` = `tsc -b && vite build`, mà `tsc -b` CÓ đi theo project references ⇒ acceptance `npm run build` là thước THẬT. **Luật rút ra: acceptance kiểu "lệnh X xanh" phải kèm MỘT phép đo chứng thước không rỗng (`--listFiles`, `-c`, số file/ca) — exit 0 của một lệnh không đọc file nào trông y hệt exit 0 của một lệnh đọc 225 file.**
**Khuôn chống-nhiễu đáng tái dùng (từ chính bộ test này):** ca Submit có SẴN 3 cửa 409 khác ⇒ bắt được `ConflictException` là CHƯA nói lên gì; bộ test xử bằng cách **kéo dữ liệu fixture đi theo biến đang đo** (hạng-mục lấy `ApprovalGroup` của phiếu) để 2 cửa nhiễu im, rồi thả **một phiếu chứng-nhân khác đúng một biến** trong CÙNG lượt.
- Tag `[s182, yc-013, khkk, assembly-boundary-extract, ignorequeryfilters-two-directions, tooltip-on-disabled-input, collation-vs-sqlite-blind, guard-untested-write-path, argument-order-blind-assert, vacuous-tsc-noemit, bom-in-code-commit]`

View File

@ -1,53 +1,292 @@
# sub-reviewer-precommit — S182 YC-013 đợt-2 (PRE-COMMIT, diff thật)
> Trạng thái: ĐANG CHẠY (skeleton land 1 nhát, Edit-append liên tục — chống #53)
> Verdict dòng-1 sẽ chốt ở cuối.
**VERDICT: `PASS_WITH_FIXES — 0C / 3M`** (+ 6 minor). Không có blocker. 3 MAJOR đều **propose-only**, không chặn commit nếu lead chấp nhận rủi ro có khai.
## 0. Diff scope (đo tươi)
---
- 13 file M + 1 file `??` (untracked) = `ContractSigningPlanWorkflowGuards.cs`
- `git diff --stat`: +1400 / -106 (KHÔNG gồm untracked)
## 0. Diff scope (đo tươi, KHÔNG chép từ lane)
## 1. W1a — công thức R-6 (type-10 per-Code)
`git status --porcelain` + `git diff --stat`:
(chờ)
| Nhóm | File | Ghi chú |
|---|---|---|
| BE | `ApprovalWorkflowV2AdminFeatures.cs` (+184) · `ContractSigningPlanFeatures.cs` (+55) · `ContractSigningPlanWorkflowService.cs` (+10) · **`ContractSigningPlanWorkflowGuards.cs` (untracked `??`, 72 dòng)** | 4/4 khớp spec |
| FE | `ApprovalWorkflowsV2Page.tsx` (+213) · `menuKeys.ts` ×2 · `KhkkCreatePage.tsx` ×2 | 5/5 khớp spec |
| Test | `UpdateAwDefinitionTests.cs` (+567) · `ContractSigningPlanApprovalTests.cs` (+326) · **`ContractSigningPlanGroupAndCatalogTests.cs` (+14)** | **3 file, spec/lệnh khai 2** → mục 9 |
| Ngoài scope | `.claude/governance/.session-counter.json` (+54/-53) · `docs/governance/so-yeu-cau-chu-du-an.md` (+2) | → mục 9 |
## 2. W1b — Guards 3-site
Tổng tracked: **+1400 / 106**, 13 file M + 1 file `??`.
(chờ)
### Máy — đo LẠI độc lập (không nhận số của lane)
## 3. W1b2/D6 — nhánh đổi workflow
| Phép đo | Lệnh chạy | Kết quả |
|---|---|---|
| Domain tests | `dotnet test SolutionErp.slnx` | **45 P / 0 F** |
| Infrastructure tests | (cùng lệnh) | **614 P / 0 F** |
| **Tổng** | | **659 P / 0 F** — khớp claim; baseline 645 (45+600) ⇒ **+14 đúng ở Infrastructure** |
| `npm run build` fe-admin | exit 0 | ✓ built 15.36s |
| `npm run build` fe-user | exit 0 | ✓ built 1.04s |
| tsc fe-admin | `npx tsc --noEmit -p tsconfig.app.json` | 0 err (225 file) |
| tsc fe-user | `npx tsc --noEmit -p tsconfig.app.json` | 0 err (214 file) |
(chờ)
---
## 4. W1c — changelog shape
## 1. W1a — công thức R-6 · **HELD**
(chờ)
`ApprovalWorkflowV2AdminFeatures.cs:355-361`:
## 5. W1d — usage-count IgnoreQueryFilters + 404 + D5
```csharp
var actives = typeEnum == ApprovalWorkflowApplicableType.ContractSigningPlan
? await db.ApprovalWorkflows
.Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive)
.ToListAsync(ct)
: await db.ApprovalWorkflows
.Where(w => w.ApplicableType == typeEnum && w.IsActive)
.ToListAsync(ct);
```
(chờ)
- ✅ Đúng **3 vế** R-6, đúng thứ tự, **KHÔNG** có biến-thể regex `^KHKK-N[1-8]$` ở site này (grep `KHKK-N` trong `AdminFeatures.cs` chỉ ra ở **comment** + gọi `ParseGroup` cho D5, không ở vị-ngữ W1a).
- ✅ Comment `:339` cũ ("only ONE active per type") **đã bị xoá**, thay bằng khối 20 dòng nêu cả 2 nhánh + cấm regex. Không còn neo thối.
- ✅ Nhánh `else` **giữ nguyên byte** semantics per-type cho mọi loại khác.
- ✅ Ăn khớp với `nextVersion :336-338` vốn đã per-Code (`Where(w => w.Code == request.Code)`), nên 2 vế cùng một trục.
## 6. W2d — danh-sách-chốt 6 ô
---
(chờ)
## 2. W1b — Guards 3-site · **HELD**
## 7. W2g — find(isActive)
- ✅ File MỚI `ContractSigningPlanWorkflowGuards.cs``public static class`**Application** (đúng F1 của review-1: không nhét vào `Scope` internal, không `InternalsVisibleTo`, không inline-duplicate). Đây đúng là finding "spec-unbuildable vì assembly-ranh" đợt-1 và nó **đã được đóng đúng cách**.
- ✅ 3 call-site, **đối số đúng thứ tự ở cả 3** (tôi đối chiếu tay từng site):
- Create — `ContractSigningPlanFeatures.cs:516-517``(db, request.ApprovalWorkflowId, approvalGroup, ct)`
- UpdateDraft — `:643-644``(db, awId, plan.ApprovalGroup, ct)`
- Submit — `ContractSigningPlanWorkflowService.cs:196-197``(db, awId, plan.ApprovalGroup, ct)`
- ✅ Semantics **cho-qua code ngoài họ** GIỮ NGUYÊN: `ParseGroup` trả `null``if (wfGroup is int k && ...)` không vào ⇒ pass. Ghi chú `:49-57` nêu đúng lý do gốc `:477-478`.
- ✅ Điểm chèn Submit: **sau** 2 rào hạng-mục (`:177-187`), **ngay trên** `LoadStepsAsync(awId, ct)` `:199`. Cũng nằm sau guard phase `:160` và sau `plan.ApprovalWorkflowId is not Guid awId` `:163``awId` luôn hợp lệ, không có nhánh nào bỏ qua.
- ✅ Thân rào cũ ở Create **đã xoá hẳn** (không để 2 bản luật song song).
(chờ)
---
## 8. 14 test
## 3. W1b2 / D6 · **HELD**
(chờ)
-`EnsureWorkflowPinnableAsync` (`ContractSigningPlanFeatures.cs:331-353`) chỉ có **2 caller**: Create `:507`, UpdateDraft `:642` (grep toàn repo — 3 hit gồm 1 định-nghĩa).
- ✅ UpdateDraft: cả 3 rào (`Type` / `Pinnable` / `GroupMatch`) nằm **TRONG** `if (request.ApprovalWorkflowId is Guid awId && awId != plan.ApprovalWorkflowId)` `:638`. Đúng D6 — giữ-nguyên-pin không bị hỏi. T7 chân (4) đo đúng vế này.
-**2 message TÁCH** (`:346-352`): "đã ngừng hoạt động" ⟂ "chưa được ghim cho người dùng chọn". Không gộp `!IsActive || !IsUserSelectable`.
- ✅ Không gọi ở đường Submit — đúng "D6 chốt 2 site".
## 9. Residual
---
(chờ)
## 4. W1c — changelog shape · **HELD**
## 10. FLAG-A tsc
`AdminFeatures.cs:916-955`:
(chờ)
-`EntityType = ContractSigningPlanEntityType.Workflow` — enum **=4** (verified `ContractSigningPlanChangelog.cs:12`), slot có sẵn, không đẻ slot mới.
-`Action = ChangelogAction.Update` · `EntityId = def.Id` · `PhaseAtChange = plan.Phase` · `UserId` · `UserName` · `Summary` · `ContextNote` — đủ 8 field mirror khối PE.
- ✅ Filter: `ApprovalWorkflowId == def.Id && Phase != DaDuyet && Phase != TuChoi``TraLai` **KHÔNG** bị loại (đúng spec: phiếu trả lại sẽ trình lại).
-**CỐ Ý không `IgnoreQueryFilters()`** ở đây (ngược với DELETE) — có khai lý do tại chỗ `:930-934`. Đây là điểm mà đợt-1 tôi FAIL, nay đã được **phân biệt đúng 2 chiều** (changelog = ngữ nghĩa "sống"; DELETE = ngữ nghĩa "FK còn thật").
-`note`/`summary`/`actorName` hoist ra ngoài 2 nhánh — không đổi hành vi (chỉ string op), khối PE dùng lại y hệt giá trị cũ.
- ✅ Ghi nằm **trước** `await db.SaveChangesAsync(ct)` `:958` + `tx.CommitAsync(ct)` `:962` ⇒ atomic cùng lượt PUT.
- ✅ Câu cũ xếp KHKK vào rọ "chỉ có UpdatedAt/UpdatedBy" đã gỡ (`:875-882`) — không để lại neo thối.
## VERDICT
---
(chờ)
## 5. W1d — usage-count + D5 · **HELD** (2 minor)
### (i) pin-count 8 module — **đếm ĐÚNG 8, không thiếu không thừa**
Tôi grep Domain thay vì tin comment:
```
grep -E "public (Guid|Guid\?) ApprovalWorkflowId" src/Backend/SolutionErp.Domain
→ Contract · ContractSigningPlan · LeaveRequest · OtRequest · Proposal
· TravelRequest · VehicleBooking · PurchaseEvaluation = 8
(+ ApprovalWorkflow.cs:76 = FK nội bộ của Step, không phải module)
```
-`IgnoreQueryFilters()` áp đúng **PE / Contract / KHKK** (`:1287-1292`). Tôi grep `HasQueryFilter` toàn `src/Backend`: trong 8 module pin, **chỉ 3 entity đó** có global filter (`ContractConfiguration.cs:44` · `ContractSigningPlanConfiguration.cs:55` · `PurchaseEvaluationConfiguration.cs:84`). 5 module Office **không** có ⇒ đếm thẳng là đúng. Claim trong comment `:1281-1285` HELD bằng phép đo độc lập.
- ✅ Đây chính là finding 🔴 đợt-1 của tôi ("usage-count trên bảng có HasQueryFilter thiếu IgnoreQueryFilters") — **đã đóng**, và T11 đo đúng cái bẫy đó.
### (ii) opinions 8 bảng
- ✅ Đủ 8 (`:1300-1319`): PE · Contract · ContractSigningPlan · Proposal · LeaveRequest · OtRequest · TravelRequest · VehicleBooking.
-`levelIds` lấy qua `l.Step!.ApprovalWorkflowId == def.Id` + `IgnoreQueryFilters()`.
-`IgnoreQueryFilters()` trên 8 bảng opinion = **no-op** (không bảng nào có `HasQueryFilter`; `ContractSigningPlanLevelOpinion.cs:18` khai rõ "CỐ Ý KHÔNG"). Vô hại + đồng hình, đúng như comment nói. Và **KHÔNG** lọc `!IsDeleted` là ĐÚNG cho câu hỏi FK (khác `CountLiveOpinionsAsync :969` hỏi câu khác — "chữ ký còn hiệu lực"). Hai hàm hỏi 2 câu khác nhau, không phải mâu thuẫn.
### (iii) 404
-`throw new NotFoundException("ApprovalWorkflow", request.Id)` thay `KeyNotFoundException` (`:1275`). T13 có control dương.
### (iv) D5
- ✅ Chỉ áp cho họ, qua `ContractSigningPlanWorkflowGuards.ParseGroup(def.Code) is int group` `:1337`**dùng lại đúng 1 nguồn regex**, không chép chuỗi lần 3.
- ✅ Thứ tự: usage (i+ii) TRƯỚC, D5 SAU — khai lý do `:1259-1261`.
---
## 6. W2d — danh-sách-chốt 6 ô · **HELD trên trục dữ-liệu**, hở trên trục hiển-thị (→ MAJOR-1)
### 🔴 #8 `allowApproverFinalize` — **0-CHẠM, chứng minh bằng grep trên DIFF**
```
git diff -- fe-admin/.../ApprovalWorkflowsV2Page.tsx | grep "allowApproverFinalize"
→ exit 1 (0 hit)
```
Control dương: grep trên **file** ra 7 hit (`:57 :123 :217 :238 :267 :1456 :1457`) ⇒ lưới grep có răng, và ô #8`:1456` **không**`disabled`, **không**`title`, `checked={entry.allowApproverFinalize}` nguyên vẹn. **Prod 8/88 cờ chịu lực AN TOÀN.**
### #7 `allowApproverSkipToFinal` — giữ nguyên hành vi
Diff chỉ đổi **3 biểu thức trùng lặp** `applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN``isKhkk`. `isKhkk` định nghĩa `:715` = **cùng biểu thức**, cùng scope, cùng prop. Giá trị y hệt. `checked={isKhkk ? false : ...}` giữ ép-false (đúng spec "GIỮ NGUYÊN").
### `levelFlagsPayload` — 0-chạm
`git diff | grep levelFlagsPayload`**1 hit duy nhất, nằm trong COMMENT** (`:486` của diff). Payload không đổi ⇒ ô mờ vẫn echo state thật lên BE.
### 6 ô disabled — GIỮ state thật, **CẤM ép false: tuân thủ**
| # | Cờ | Dòng | `checked` | `disabled` | tooltip |
|---|---|---|---|---|---|
| 1 | `allowReturnOneLevel` | 1358-1362 | `entry.…` ✅ | `isKhkk` | chung |
| 2 | `allowReturnOneStep` | 1369-1373 | `entry.…` ✅ | `isKhkk` | chung |
| 3 | `allowReturnToAssignee` | 1380-1384 | `entry.…` ✅ | `isKhkk` | chung |
| 5 | `allowApproverEditDetails` | 1407-1411 | `entry.…` ✅ | `isKhkk` | chung |
| 6 | `allowApproverEditBudget` | 1418-1422 | `entry.…` ✅ | `isKhkk` | chung |
| 9 | `allowApproverDelete` | 1468-1472 | `entry.…` ✅ | `isKhkk` | chung |
Không ô nào bị `checked={false}`. ✅ ERRATA D3 (state-thật, không forced-true) được thi hành **đúng** ở ô #4 `:1396-1400`: `checked={entry.allowReturnToDrafter}` + `disabled` + **tooltip RIÊNG** + label **KHÔNG** bị `text-slate-400` (đúng ý "không mờ kiểu chưa-có").
### Ô Ngưỡng CEO (W2f)
-`disabled={isEdit || isKhkk}` `:1052` — vô điều kiện cho type-10.
- ✅ POST gửi `ceoApprovalThreshold: isKhkk ? null : …` `:889-891`.
- ✅ helper-text riêng `:1057-1058`.
---
## 7. W2g — `find(w => w.isActive && …)` · **HELD** (hợp-đồng FE↔BE đã đo)
- ✅ 2 app **mirror LOGIC** — 2 diff byte-identical ở phần code (`fe-admin/.../KhkkCreatePage.tsx:125-127``fe-user/.../KhkkCreatePage.tsx:125-127`).
- ✅ 🔴 **`isActive` CÓ THẬT trong DTO BE** — đây là chỗ dễ vỡ nhất (nếu BE không trả, `w.isActive` = `undefined` ⇒ falsy ⇒ `autoWorkflow` LUÔN null ⇒ **chết đường tạo phiếu KHKK**, mà tsc + build + 659 test đều xanh). Đo: `AwDefinitionDto` nhận `d.IsActive`**`ApprovalWorkflowV2AdminFeatures.cs:178`** (đối số thứ 8), serialize camelCase ⇒ `isActive`. **Hợp đồng khớp.**
-`history` = FULL list kể cả active — verified `:205-213`: `versions` = mọi definition của type, `active` = `versions.FirstOrDefault(v => v.IsActive)`. Claim của cả W2b lẫn W2g HELD.
- ✅ Filter `isUserSelectable` client-side ở `:109` **vẫn giữ**.
---
## 8. 14 test — **đúng 14 ca spec, KHÔNG có ca nào nới để xanh**
| Ca | File | Nhận xét đối-kháng |
|---|---|---|
| T1 | `UpdateAwDefinitionTests.cs` | Có **tiền-đề đo** (`Be(8)` trước khi act) — chống "xanh ở thế giới sai" |
| T2 | ″ | Regression type-1 + chứng-nhân xuyên-type |
| T3 | ″ | **2 chân**: (a) code lạ không nuke 8 nhóm; (b) code lạ vẫn ratchet chính nó ⇒ giết được cả bản vá regex lẫn bản vá "type-10 đừng tắt gì" |
| T4 | `ContractSigningPlanApprovalTests.cs` | 3 vế: chặn + **đĩa bất động** (`ReadPinAsync` từ context MỚI + changelog=0) + control dương |
| T5 | ″ | **Seed phiếu pin lệch thẳng DbContext** ✅ + chứng-nhân phiếu đúng nhóm submit LỌT cùng lượt; fixture kéo hạng-mục theo `approvalGroup` ⇒ loại 2 cửa 409 khác |
| T6 | ″ | Vế âm + **chân (2) chứng minh rào đang sống** (không phải xanh-mù) |
| T7 | ″ | 4 chân, đủ cả (4) giữ-nguyên-pin-vẫn-sửa-được |
| T8 | `UpdateAwDefinitionTests.cs` | Đo **SHAPE** 9 field + sanity `changes ≠ rỗng` + assert PE changelog = 0 (không rải chéo module) |
| T9 | ″ | **Control dương TRONG CÙNG LƯỢT** (4 phiếu: 1 sống → có vết; DaDuyet/TuChoi/xoá-mềm → không) |
| T10 | ″ | Control dương + **vi-sai message** (1 vs 2 hồ sơ ⇒ câu phải khác — giết "câu hằng") + chứng-nhân `DbUpdateException` |
| T11 | ″ | Có **tiền-đề 2 chiều**: filter-on = 0, filter-off = 1 ⇒ đo đúng cái bẫy F2 |
| T12 | ″ | **Seed `LeaveRequest` thật** ✅ + chứng-nhân xoá thẳng module 2 ⇒ dangling THẬT trong DB |
| T13 | ″ | Control dương id thật |
| T14 | ″ | **3 chân** (không phải 2): (1) không-phải-cuối ⇒ OK · (2) cuối ⇒ 409 · (3) ngoài họ 1-version ⇒ OK. Tiền-đề `0 usage` tách bạch D5 với rào (i) |
**Đếm:** 10 `[Fact]` mới ở `UpdateAwDefinitionTests` + 4 ở `ContractSigningPlanApprovalTests` = **14** ⟂ suite +14 (600→614). Khớp 2 nguồn.
**Có test nào bị NỚI không?****đúng 1** assertion đổi, ở file thứ 3 (`ContractSigningPlanGroupAndCatalogTests.cs:858-868`):
`Contain("KHKK-N5")``Contain("N5")`. Đây là **spec-change bắt buộc**, không phải nới-để-xanh: message mới do W1b chốt bỏ mã, giữ số nhóm; câu cũ sẽ ĐỎ. Có ghi lý do 8 dòng tại chỗ. **Chấp nhận** — nhưng xem MINOR-1 (assertion mù thứ-tự).
---
## 9. Residual — 3 khoản NGOÀI danh sách
1. **`tests/…/ContractSigningPlanGroupAndCatalogTests.cs`** (+14/6) — file test **thứ 3**, lệnh chỉ khai 2. Chính-đáng (hệ quả tất yếu của đổi message W1b), nhưng **phải khai trong commit-body**, đừng để người đọc log sau này tưởng có ai lén sửa test cũ.
2. **`.claude/governance/.session-counter.json`** (+54/53) — tick S182 của lead. **Không liên quan YC-013.** → xem MINOR-2 (BOM).
3. **`docs/governance/so-yeu-cau-chu-du-an.md`** (+2) — thêm dòng `YC-013` + `YC-014`. Đúng luật sổ (GHI NGAY), không liên quan code.
**`#8` / `levelFlagsPayload` / `SkipToFinal` có bị đụng ngoài ý không?** — KHÔNG. Đã grep trên **diff** (không phải trên file) cho cả 3, kết quả ở mục 6.
---
## 10. FLAG-A của FE lane — **CLAIM ĐÚNG, và tôi đo được**
| Lệnh | fe-admin | fe-user |
|---|---|---|
| `npx tsc --noEmit` (nguyên văn acceptance cũ) | exit 0 — **`--listFiles` = 0 dòng** | exit 0 — **`--listFiles` = 0 dòng** |
| `npx tsc --noEmit -p tsconfig.app.json` | exit 0 — `--listFiles` = **225 dòng** | exit 0 — `--listFiles` = **214 dòng** |
`fe-admin/tsconfig.json` = `{"files": [], "references": [...]}` ⇒ dạng trần **biên dịch 0 file**, luôn xanh **theo cấu trúc**, không phải may mắn. FLAG-A **HELD**.
🔸 Bổ sung có ích cho lane: `npm run build` = `tsc -b && vite build``tsc -b` (build mode) **CÓ** đi theo project references ⇒ acceptance `npm run build` là thước THẬT. Chỉ acceptance `npx tsc --noEmit` trần là rỗng. Đề nghị: mọi acceptance FE về sau ghi `npm run build` hoặc `-p tsconfig.app.json`, và **kèm `--listFiles | wc -l` một lần** để chứng thước không rỗng.
---
# FINDINGS
## 🔴 MAJOR-1 — tooltip gắn trên `<input disabled>`: Chrome/Edge không hiện ⇒ nửa D3=C có thể chết câm
**Vị trí:** `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:1361, 1372, 1383, **1399**, 1410, 1421, 1471` (và tiền lệ cũ `:1443`).
`title` được đặt trên chính `<input type="checkbox" disabled={isKhkk}>`. Trình duyệt Chromium (Chrome/Edge — đúng trình duyệt đích, xem `feedback_hosolink_open_from_web_browser_block`) **chặn sự kiện chuột trên form-control bị disabled**, nên native tooltip thường **không** bật lên; Firefox thì có. `<label>` bao ngoài KHÔNG mang `title`, nên rê chuột vào phần chữ cũng không ra gì.
**Vì sao là MAJOR chứ không phải nit:** D3=C của anh là *"mờ ô chưa nghe **+ tooltip**"*. Nếu tooltip không hiện thì admin chỉ thấy 7 ô xám không lý do — đúng thứ mà rule-box `:1088-1096` cố tránh. Nặng nhất là **ô #4** `:1399`: câu *"KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh"***thông tin DUY NHẤT** phân biệt ô #4 với 6 ô kia (rule-box chỉ nói #4 "có tác dụng thật", không nói *"cờ chưa phân nhánh"*). Tooltip không hiện ⇒ ô #4 trông y hệt 6 ô "chưa áp dụng", tức **ngược nghĩa** — đúng class lỗi mà ERRATA D3 vừa sửa xong ở tầng `checked`.
**Đây là máy dựng mà không có cửa gọi** (`feedback_guard_built_but_never_called`).
**Acceptance đề nghị (chọn 1):**
- (a) chuyển `title=` từ `<input>` lên `<label>` bao ngoài — label không disabled nên tooltip sống; hoặc
- (b) render câu của ô #4 thành **chữ nhìn thấy được** (một dòng `text-[10px]` dưới ô), giữ tooltip cho 6 ô kia.
- Kiểm chứng: mở dialog nhóm bất kỳ trên Chrome, rê chuột **vào chữ** của ô #4 → phải đọc được câu riêng.
- 🔸 Tiền lệ `:1443` (#7, có từ K3) **cùng bệnh** — vá thì vá cùng lớp (bài `citation-trap` gen-2: vá 1 CLASS phải grep MỌI matcher cùng-lớp TRONG diff).
---
## 🔴 MAJOR-2 — 3 thành phần bất đồng về HOA/thường của `Code`; test mù theo CẤU TRÚC (SQLite ≠ SQL Server)
**Vị trí:** `ContractSigningPlanWorkflowGuards.cs:37-38``ApprovalWorkflowV2AdminFeatures.cs:357``KhkkCreatePage.tsx:126` (×2 app).
Ba chỗ hỏi cùng một câu "code này có phải nhóm N{n} không", bằng **ba luật khác nhau**:
| Chỗ | Phép so | Với `khkk-n1` (thường) |
|---|---|---|
| `GroupCodePattern = new(@"^KHKK-N([1-8])$", CultureInvariant)`**KHÔNG** `IgnoreCase` | case-**SENSITIVE** | → `null` = **ngoài họ** ⇒ rào nhóm 3-site TẮT, D5 TẮT |
| `w.Code == request.Code` (EF → SQL Server) | theo **collation DB**, mặc định `*_CI_AS` = case-**INSENSITIVE** | → **khớp** `KHKK-N1` ⇒ per-Code **vẫn hạ IsActive** bản thật |
| `(w.code ?? '').trim().toUpperCase() === 'KHKK-N1'` | case-**INSENSITIVE** | → **khớp** ⇒ FE auto-pin nó cho phiếu nhóm 1 |
Hệ quả cụ thể: admin gõ `khkk-n1` trên panel type-10 gộp (ô Mã ở panel đó **không** khoá — `lockedCode = null`) ⇒ bản mới **tắt** `KHKK-N1` thật, FE **vẫn** auto-pin sang nó, còn rào nhóm + D5 **im lặng tắt** cho toàn bộ phiếu nhóm 1. Không có lỗi nào báo.
**Vì sao test không thấy:** 14 ca chạy trên **SQLite** (`SqliteDbFixture`), nơi `=` trên TEXT mặc định là **BINARY = case-sensitive**. Tức phép so ở tầng DB trong test có ngữ nghĩa **NGƯỢC** với prod. Đây không phải test viết ẩu — nó **mù theo cấu trúc**, không assertion nào bịt được.
**Acceptance đề nghị:** chuẩn hoá tại **đường ghi**`Code = request.Code.Trim().ToUpperInvariant()` trong `CreateAwDefinitionCommandHandler` (và dùng cùng giá trị đó cho `nextVersion` + `actives`), **hoặc** thêm `RegexOptions.IgnoreCase` cho `GroupCodePattern` + khai rõ "W1a phụ thuộc collation" ngay tại `:350`. Kèm 1 ca test `khkk-n1` để bất-biến này có chứng nhân (dù SQLite, nó vẫn bắt được vế regex).
---
## 🔴 MAJOR-3 — rào D6 ở đường TẠO có 0 test; T7 chỉ phủ đường SỬA NHÁP
**Vị trí:** `ContractSigningPlanFeatures.cs:507` (`EnsureWorkflowPinnableAsync` tại Create).
Spec W1b2 nói rõ **2 site** (Create + UpdateDraft). Danh sách T1T14 **chỉ có T7**, và T7 gọi `UpdateDraftAsync` ở cả 4 chân — **không chân nào đi qua Create**. Vậy một guard **hard-409 mới cắm vào write-path chính của prod** (mọi phiếu KHKK đều sinh ra qua đây) đang có **0 răng đo**.
Rủi ro cụ thể: nếu ai đó về sau dời `EnsureWorkflowPinnableAsync` ra khỏi Create, hoặc đảo nó lên **trước** `EnsureWorkflowTypeAsync` (làm 404 đổi thành 409), hoặc gỡ nó khi refactor — **659 test vẫn xanh**. Và chiều ngược cũng không được đo: rào này có thể đang **chặn oan** một luồng tạo hợp lệ nào đó mà chưa ai chạm tới.
Ghi chú công bằng: đây là **lỗ của SPEC** (spec liệt 14 ca và thiếu ca này), implementer làm đúng spec. Nhưng luật §7 nói *"New endpoint / guard mới ở write-path → test"*, và tôi không hạ thước theo spec.
**Acceptance đề nghị:** thêm **1 ca** vào `ContractSigningPlanGroupAndCatalogTests` (nơi đã có sẵn `CreateAsync` helper + seed PE): Create pin workflow `IsActive=false` ⇒ 409 · pin `IsUserSelectable=false` ⇒ 409 **message khác** · control dương pin bản active+ghim ⇒ tạo được **và không đốt mã** (`ReadPlanScopesAsync` rỗng ở 2 ca đầu — khuôn đã có ở `:869`).
---
# MINOR
- **MINOR-1 — assertion mù THỨ TỰ toán hạng.** `Contain("N5").And.Contain("N3")` (`GroupAndCatalogTests:865`, `ApprovalTests:188` + `:246`) vẫn xanh nếu ai đó **đảo 2 đối số** ở call-site (`EnsureWorkflowGroupMatchAsync(db, awId, approvalGroup, ct)` — 2 tham số kề nhau, 3 call-site). Tôi đã đối chiếu tay: **cả 3 site hiện ĐÚNG**. Nhưng lưới không giữ được bất-biến đó. Đề nghị pin nguyên câu có thứ tự: `Contain("Phiếu nhóm N3 đang gắn quy trình N5")` ở ít nhất 1 ca.
- **MINOR-2 — `.session-counter.json` bị thêm BOM + đổi sang CRLF.** HEAD: `7b 0a` (`{`+LF). Worktree: `ef bb bf 7b 0d 0a` (**BOM** + `{` + CRLF). CRLF sẽ được git normalize, **BOM thì được commit**. Hiện chưa có script nào parse file này (`.claude/hooks/` chỉ có `wal-flush.ps1`, không đọc), nên rủi ro thấp — nhưng đây đúng là bẫy `BOM-json` đã ghi trong `feedback_resume_premise_reverify`, và nó vào commit **kèm theo code** mà không ai chủ ý. Đề nghị: `git add` **từng file cụ thể** (không `-A`), hoặc tách thành commit `wal:`/governance riêng.
- **MINOR-3 — D5 đếm anh-em KHÔNG lọc `ApplicableType`.** `AdminFeatures.cs:1339-1340`: `Count(w => w.Code == def.Code && w.Id != def.Id)`. Một dòng `KHKK-N1` ở type khác (UNIQUE là `(Code, Version)` nên chỉ cần Version khác) sẽ được tính là "anh em" và cho phép xoá bản type-10 cuối cùng. Ca hiếm, nhưng vá là **1 mệnh đề**: `&& w.ApplicableType == def.ApplicableType`.
- **MINOR-4 — panel nhóm tạo version MỚI ⇒ 6 cờ khoá bị đóng băng ở DEFAULT của FE, không phải "state thật".** Trên panel nhóm, "Tạo phiên bản mới" (không clone) khởi tạo entry từ `makeEmptyStep`; 6 ô `disabled` ⇒ admin **không thể** sửa ⇒ POST luôn ghi đúng bộ mặc định FE. Với KHKK thì vô hại (BE không đọc 6 cờ đó), nhưng chữ "state THẬT" trong D3=C **chỉ đúng cho đường CLONE/EDIT**. Đề nghị: khai 1 dòng trong commit-body hoặc nợ-khai, đừng để lượt sau đọc "state thật" rồi suy sang ca tạo-mới.
- **MINOR-5 — bỏ ghim bản active = đóng cửa tạo phiếu nhóm (không có rào).** Spec §54 đã khai; sau W2g còn chặt hơn (`isActive && isUserSelectable`). Không đòi vá đợt này, chỉ xác nhận **răng đã sắc hơn spec mô tả** ⇒ khi đóng sổ nên ghi vào accepted-risk cho đủ 2 vế (bỏ-ghim **và** bỏ-active).
- **MINOR-6 — `EnsureWorkflowGroupMatchAsync` cho qua khi `awId` không tồn tại.** Có khai chủ đích `:55-57` và ở Create/UpdateDraft thì `EnsureWorkflowTypeAsync` chạy trước nên vô hại. Ở **Submit** không có rào type đứng trước — nhưng `ContractSigningPlans.ApprovalWorkflowId`**FK Restrict** (`ContractSigningPlanConfiguration.cs:50-53`) nên `awId` luôn resolve. Ghi lại để lượt sau đừng gỡ FK mà quên chỗ này.
---
# Điểm CHỊU ĐƯỢC soi (positive validation — ghi để lượt sau đừng phá)
1. **3/3 finding 🔴 của tôi ở review đợt-1 đều được đóng ĐÚNG BẢN CHẤT**, không phải đóng bằng chữ: (a) assembly-ranh → file `public static class` riêng; (b) `IgnoreQueryFilters` cho bảng có `HasQueryFilter` → đúng 3 bảng, và **phân biệt được 2 chiều** (DELETE cần, changelog cố ý không); (c) FK-thật → grep 8 module, 5 module loose-0-FK được khai là "rào DUY NHẤT" + T12 chứng minh bằng dangling THẬT trong DB.
2. **Mọi ca test đều có control dương CÙNG LƯỢT** — T9/T10/T11/T13/T14 và cả T6 chân (2). Không có ca "vế âm xanh-mù" nào.
3. **T5 dựng lại đúng trạng thái không-thể-tạo-qua-API** (seed thẳng DbContext) và **loại trước 2 cửa 409 nhiễu** bằng cách kéo hạng-mục theo `approvalGroup`. Đây là chống "xanh vì nhầm cửa" ở mức tôi không bẻ được.
4. **`key={typeCode ?? 'all'}` trên `TypePanel`** (`:400`) — remount khi đổi nhóm, chặn state-bleed giữa 8 panel dùng chung component. Chi tiết nhỏ nhưng đúng chỗ.
5. **POST gửi `code: lockedCode ?? code`** (`:882`) chứ không tin ô input đã `disabled` — bất-biến theo CẤU TRÚC, không theo kỷ luật UI.