wal: flush 20260801T1310

This commit is contained in:
pqhuy1987
2026-08-01 13:10:01 +07:00
parent 628132cf10
commit 5b0fa5d9d7
5 changed files with 533 additions and 8 deletions

View File

@ -44,11 +44,20 @@ chain:
lead-ghi, gate-K7 = lưới review THẬT đầu tiên của diff FE). TEST DONE (engine-exit-0-delta → resume
Write-trước: suite 639/0 = 45D+594I Δ+17 [12 bridge T1-T12 + 5 authz] · 1-FAIL-bàn-giao đóng bằng
ngoại-lệ CreateContract #85 · 0-FLAG-production · 2-khai: vá-6-thắng-invest-T1 + seeder-grant-chỉ-đo-
được-@restart-prod→giao-cicd — sub-test-k7.md §0-§5). Gate-K7 ĐANG nền (sub-reviewer-gate-k7.md — cửa
chết ĐỌC FILE trước, đừng spawn lại). CÒN: gate → commit ([CLAUDE] Contract: K7) → push → cicd
(kèm đo seeder-grant restart-proof) → GĐ3ng-nhất (chốt 55)
[ ] GĐ3 cây-4-folder ĐỒNG-NHẤT — anh chốt (55) @S167 "Nên đồng nhất": áp cây 4-folder lên cụm trang
Duyệt-hợp-đồng (7 loại HĐ) ×2 app — làm SAU K7 ship (tái dùng tập ContractIds/usePipelineStages mới)
được-@restart-prod→giao-cicd — sub-test-k7.md §0-§5). Gate-K7 PASS-WITH-FLAGS 10/0-blocker (garble-
0-artifact → resume Write-trước trả CLEAN; tự đo prod 13-vai CanCreate-1/13 + chốt acceptance seeder
"0 added + 2 upgraded" — sub-reviewer-gate-k7.md 32.5KB §9-5-hànhng). F-8 FIXED trước commit (neo
:2516 chết-lúc-land → neo TÊN-HÀM 3-chỗ + 13/13→12/13-đo-prod + khai-chủ-đích-CanRead + trỏ-đúng-
SeedAdminPermissionsAsync) · filter 17-test PASS · COMMIT aaed699 PUSHED (13 file +2241/46) +
governance 628132c (3-script + STATUS + G-1-RATIFIED-stamps ×2-app SHA-pair-giữ; 4-file-agent-memory
đã vào wal:-flush trước). Nợ mang theo 0-blocker: MAJOR-race-2-request-đồng-thời (chống-trùng
in-memory — phương-án unique/RowVersion nhịp sau) · DraftData-không-carry-chưa-khai-chủ-đích ·
a11y-<a>-trong-label + O(n·m) FE (lớp K5-nợ) · CLARIFY menu-cache-re-login (UAT-NÓI-TRƯỚC).
cicd-K7 ĐANG nền (sub-cicd-verify-k7.md — ĐÃ báo vai phủ-2-run vì 628132c đẩy chồng; restart×2
= phép đo idempotent miễn phí)
[!] GĐ3 cây-4-folder ĐỒNG-NHẤT — anh (55) "Nên đồng nhất": lane FE ĐANG nền @S167 (khuôn bfc7b79 S162
3-trang + usePipelineStages-tập-contractIds K7 — sub-implfe-gd3.md, cửa chết ĐỌC FILE trước).
CÒN SAU LANE: gate → commit → cicd
[ ] K8-prep form-14-người fail-closed + gói SOLUTION-PE-{n} + QĐ9 đo-2-nhánh + ZZTEST + rollback-theo-ID.
🔴 (54) FINAL @S167 (anh, 2 lượt): "quy trình duyệt tử tế rồi ko sao" + "hiện cứ như vậy — Nếu điều
chỉnh, tao sẽ tự tay điều chỉnh" ⇒ GIỮ HIỆN TRẠNG workflow/người-duyệt KHKK (8 WF N1-N8 đã seed @K3,

View File

@ -2,11 +2,12 @@
- **[→ archive/2026-07.md — verbatim ĐÃ VERIFY moved-not-cut 3/3 @S166-K5-curate] S166 gate K4b · K4a · vá F-1 (digest):** mỏ-neo XUYÊN FILE (quét file BỊ DỜI, không chỉ file đang sửa) · **tham-số-trang-trí** (đo AI ĐỌC từng param, đừng đo "route có tồn tại") · mỏ-neo tự-vô-hiệu ngay lúc land (đo SAU khi chèn) · **ô canonical sai đo được bằng 1 curl** · khuôn-nguồn 2 tầng ⇒ đo **GIÁ-TRỊ KHỞI-TẠO** không đọc chú-thích · **cây phình giữa review** ⇒ neo mốc + re-đo `--stat` TRƯỚC verdict · HIGH-nằm-ở-git (`git status --porcelain -- src tests | grep '^??'`) · khoá-cache định-danh REQUEST không định-danh URL · sợi-dây-1-argument 0 test.
- **S166 (08-01) gate K5 cây GĐ2 mở 8 sub-folder nhóm — PASS 0 blocker (1 CLARIFY + 5 MINOR):** lõi hiếm-đúng: bất-biến Σ do **CẤU TRÚC** (1 mảng nguồn `khkkItems` → 1 phép phân hoạch if/else TOÀN PHẦN → `stage.leaves` vẫn là chỗ đếm duy nhất `stageCount():96`) nên không phụ thuộc ai nhớ cộng; route `?group=` nối đủ 3 chặng (FE `:79-80` → param `:124` → BE `Where(:757)`), **khác hẳn tham-số-trang-trí K4a**. **Bài thu:** ① 🔴 **phép-đo-rẻ-nhất nằm ở file RÁC của lane khác** `menus.json` untracked ở gốc repo (sản phẩm curl của cicd-monitor) chính là payload `/api/menus/me` LIVE ⇒ đối chiếu nhãn cây suy-ra ⟂ nhãn sidebar thật **8/8 khớp từng ký tự + `displayLabel` đều null**, thứ mà đọc code không bao giờ chứng được. Cùng file đó là **rào commit** (`git add -A` sẽ nuốt 44KB rác) ⇒ 1 vật vừa là bằng-chứng vừa là rủi-ro, đừng chỉ báo 1 vế. ② **chân-lý-rỗng phải khai là rỗng**: curl prod `total=0` phiếu KHKK ⇒ phân bố `approvalGroup` = `{}`, KHÔNG dùng để kết luận gì; nhưng cùng con số ấy đẻ ra sự-thật dùng được cho UAT (ngày đầu mọi gói hiện **8 dòng "SẮP" rỗng** — đúng spec, dễ bị đọc thành hỏng). ③ **tương-đương-từng-vế > "cùng tập cùng thứ tự"**: vòng lặp GĐ3/4 bị viết lại ⇒ lập bảng 4 vế (tập id · thứ tự = `Set` giữ thứ-tự-chèn · khử trùng · dòng-phụ `find` = phiếu ĐẦU) mới dám phán 0-regression; thân stage4 **0 tham chiếu `p`** là lý do cấu-trúc khiến rewrite an toàn. ④ **tiền-lệ phải đo ở BASE, không đo ở cây đã sửa**: `git show 88e7ecf:<file> | grep -A6 '<summary'` cho thấy 6/6 summary cũ chỉ chứa `span`/`svg``<button>` lồng trong `<summary>`**khuôn MỚI** (nested-interactive a11y), lane tưởng đang bám tiền lệ. ⑤ **"15/15 OK" chết theo phiên**: script `new Function` không commit + FE **0 hạ tầng test** (`package.json` chỉ `dev|build|lint|preview`) + chuông Σ là `import.meta.env.DEV` ⇒ prod bị strip ⇒ 0 lưới an toàn; đừng đọc số-test-ad-hoc thành đã-phủ. ⑥ 2 nghĩa của 1 dòng checklist ("GĐ3/4 by c.type") — lead và lane **cùng chọn 1 nghĩa không chứng minh nghĩa đó đúng ý owner** ⇒ ghim cách đọc vào commit. Tag `[s166, bang-chung-o-file-rac, chan-ly-rong, tuong-duong-tung-ve, tien-le-do-o-base]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k5.md`
- **[→ archive/2026-07.md §@S167-K7-curate — verbatim ĐÃ VERIFY moved-not-cut 2/2] S166 gate K5 cây-GĐ2-8-sub-folder · gate K4c cụm-2 (digest):** bất-biến Σ do CẤU TRÚC (1 nguồn phân hoạch TOÀN PHẦN → 1 chỗ đếm) · **phép-đo-rẻ-nhất nằm ở file RÁC của lane khác** (`menus.json` untracked = payload prod LIVE, vừa là bằng-chứng vừa là rào-commit) · chân-lý-RỖNG phải khai là rỗng · tương-đương-TỪNG-VẾ > 'cùng tập cùng thứ tự' · tiền-lệ phải đo ở BASE không đo ở cây đã sửa · 'N/N OK' chết theo phiên khi FE 0 hạ-tầng-test · **flake chỉ lộ ở LƯỢT ĐẦU và `-v q` NUỐT thông điệp assert** (lượt đầu dùng verbosity thường) · probe RED bằng đổi TEST-INPUT hợp lệ CHỈ KHI tham số xuất hiện ĐÚNG 1 chỗ (grep trước) · `Total` là vân-tay lọc-SERVER chỉ khi `CountAsync` sau `Where` trước `Skip/Take`.
- **S166 (08-01) gate K4c chốt cụm-2 (FE lọc nhóm WfView + 1 Fact test) — PASS 0 blocker (1 CLARIFY + 3 MINOR):** ① 🔴 **flake chỉ lộ ở LƯỢT ĐẦU, và chính tôi giết mất chứng cớ**: chạy `dotnet test -v q` SONG SONG với `tsc` ⇒ 1 test CŨ đỏ (`Failed 1/8`) rồi 10/10 xanh sau đó + suite 622/0 ⇒ `-v q` **nuốt thông điệp assert** ⇒ có tên test mà không có lý do. Bài: lượt chạy ĐẦU dùng verbosity thường, `-v q` để dành cho lượt lặp. Truy nhiễm phải đo 3 vế mới dám nói "không do diff": fixture `:memory:` per-test · codegen **0 static** · xUnit 2.9.3 same-class = tuần tự. Báo "8/8 ở 11/12 lượt", KHÔNG báo 8/8 tuyệt đối. ② **probe RED bằng đổi TEST-INPUT (không chạm prod) là hợp lệ CHỈ KHI tham số xuất hiện ĐÚNG 1 CHỖ trong handler** — grep xong mới được chấp nhận; ≥2 chỗ là lập luận sập. ③ `Total` là dấu-vân-tay lọc-SERVER **chỉ khi** `CountAsync` chạy trên `q` SAU `Where` và TRƯỚC `Skip/Take` — đọc `ProjectPagedAsync:790-792` mới dám ký, đừng tin comment. ④ **queryKey KHÔNG thêm trục là ĐÚNG** khi BE endpoint không nhận trục đó (đọc controller: chỉ `applicableType`+`isUserSelectable`) + giá trị lọc là **dẫn xuất trong render** (không `useState`) ⇒ miễn nhiễm bẫy "đổi leaf không remount". ⑤ Guard `/^[1-8]$/` chặn `Number("03")=3`, và **đồng nhất 3 trang** KHKK — kiểm cả 3, lệch 1 chỗ là 2 luật cho 1 tham số. ⑥ Lane khai bundle `1,643.96 kB` — tôi chạy lại `npm run build`, **trùng từng chữ số** ⇒ claim đo-lường có thật. Tag `[s166, chung-co-bi-nuot-boi-verbosity, probe-1-cho-moi-hop-le, total-la-van-tay]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4c.md`
- **S167 (08-01) gate K6 admin thu-gọn (1 file FE, tập 12 key ẩn) — PASS-WITH-FLAGS 5 (1 MAJOR + 1 CLARIFY + 3 MINOR):** ① 🔴 **cây menu PROD SỐNG là thước, seed chỉ là bản vẽ**: `curl /api/menus/me` (token admin, 44KB) → áp CHÍNH tập key **cắt bằng regex từ file đã ship** ⇒ 200 node · 166 lá → **15 lá**, trùng tên 15/15 + control-âm bỏ `KeHoachKyKet` = 63 (+48) khớp lane. Cùng payload đóng luôn FLAG "row chèn tay": **10 `AwV2*`, 0 `AwV2_Contract`**. Khai giới hạn: endpoint đã lọc-quyền ⇒ row không có Permission thì vô hình (nhưng cũng vô hình trên sidebar ⇒ không lệch C1). ② **`filterForAdmin` mới là bước MỘT**: lá còn "THẤY" phải qua tiếp `resolvePath→null ⇒ MenuLeaf drop im lặng` (#50) và **group rỗng-hoá tụt xuống render như LEAF** (`children.length>0`) — lane bỏ cả 2, tôi đo: 0 drop · 0 rỗng-hoá. ③ **typo-noop**: key ẩn sai chính tả = `Set.has` false, module vẫn hiện, tsc vẫn xanh ⇒ phải assert từng key TỒN TẠI trong cây prod (12/12 OK; control-âm 3 key bịa đều MISS). ④ **MAJOR: comment mới nhồi 3 số-đếm, 1 số SAI ngay lúc land** — "28 leaf `Ct_`" (thật 42 lá/49 key, S155 thêm WfView/Approved/Deleted), số chép từ skill `permission-matrix` stale ⇒ đúng bệnh #84(4) "CẤM số-đếm trong comment". ⑤ **CLARIFY hệ-quả không ai khai**: prod có `Hrm/Off/Personal` `isVisible=false`, fe-user lọc `isVisible!==false` còn fe-admin KHÔNG đọc cờ đó ⇒ admin là lối vào CUỐI, ẩn nốt = 3 module mất menu ở CẢ 2 app. ⑥ display-only chứng 3 tầng: diff (grep `+` chỉ hit COMMENT) < kiến-trúc (`usePermission:15` đọc cây THÔ, chỉ 2 consumer) < curl prod 200×2 endpoint đã ẩn. Tag `[s167, thuoc-la-cay-prod, filter-moi-la-buoc-mot, typo-noop, so-dem-trong-comment]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k6.md`
- **[→ archive/2026-07.md §@S167-K7-curate-2 — verbatim ĐÃ VERIFY moved-not-cut] S167 gate K7 cầu KHKK→HĐ — PASS-WITH-FLAGS 10, 0 blocker (digest):** **1 con số đóng 2 câu hỏi** — baseline 622 (đo ở K4c cùng ngày) + 17 `[Fact]` đếm được = 639, đo đúng **639/0** ⇒ chứng CẢ '17 test chạy thật, không skip câm' LẪN '0 test cũ vỡ'. **Mỏ-neo tự-vô-hiệu tái phát, lan 3 nơi**: chú thích trỏ `:2516``git show HEAD:<f>|grep -n`=2516 vs `grep -n` cây=**2523**, chính diff chèn 7 dòng đẩy xuống ⇒ phải đo **2 THẾ GIỚI (HEAD ⟂ cây)**, đo 1 thế giới là im. **Prod probe sửa con số của chính comment**: `/permissions/by-role/{id}`×13 vai → `Contracts.CanCreate` **1/13** ⇒ tiền-đề lỗ ĐÚNG, nhưng '13/13 role' sai (thật 12/13, Admin đã có row True) + đẻ acceptance sau-deploy `0 added + 2 upgraded`. **Răng phủ NỬA**: T11 double-click assert 4 vế nhưng `await` tuần tự ⇒ ca ĐỒNG THỜI vẫn hở (0 unique-index/RowVersion/transaction) — đng đọc 'có T-idem' thành 'đã đóng'. **Ngoại lệ authz đúng cách**: map khớp TUYỆT ĐỐI + action ngoài map vẫn bị đòi prefix ⇒ tương-lai không tự miễn-trừ; kèm `BeSubsetOf` chống cấu-hình-chết. **SHA-identical KHÔNG bảo chứng route-identical** (3 path cứng `/contracts/${id}` trong file mirror 2 app ⇒ grep route CẢ 2). Tag `[s167, mot-so-dong-2-cau-hoi, moneo-tu-vo-hieu, probe-sua-so-cua-comment, rang-phu-nua, sha-khong-bao-chung-route]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k7.md`
- **[→ archive/2026-07.md §@S167-K7-curate-2 — verbatim ĐÃ VERIFY moved-not-cut] S167 gate K6 admin thu-gọn — PASS-WITH-FLAGS 5 (digest):** **cây menu PROD SỐNG là thước, seed chỉ là bản vẽ** (`curl /api/menus/me` + áp CHÍNH tập key cắt regex từ file đã ship; control-âm bỏ 1 key để chứng phép đo có phân biệt). **`filterForAdmin` mới là bước MỘT** — còn `resolvePath→null` drop im lặng (#50) + group rỗng-hoá tụt xuống render như LEAF. **typo-noop**: key ẩn sai chính tả = `Set.has` false, module vẫn hiện, tsc vẫn xanh ⇒ assert TỪNG key TỒN TẠI trong cây prod. MAJOR: comment nhồi 3 số-đếm, 1 số sai ngay lúc land (số chép từ skill stale) ⇒ CẤM số-đếm trong comment. **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k6.md`
- **[→ archive/2026-07.md — verbatim ĐÃ VERIFY moved-not-cut 4/4 @S166-curate] S164/S165 gate K3 · S164 gate K1 · S162 cây-4-folder · S161-W2 KHKK-CRUD (digest):** tham-số-chết-vì-thiếu-tầng-UI (đo TẦNG-CUỐI ai GỬI) · sweep 2-cờ bất-đối-xứng · flake ⇒ "N/0 tươi" phải khai SỐ LƯỢT · giá-trị lớn nhất ở GATE QUY-TRÌNH không ở mã · **ghost-policy** (grep policy, đừng suy từ "key đã vào All") · default-lấn-ngữ-nghĩa · im-lặng-về-độ-chính-xác-của-con-số ×3/diff · **#82 ngược chiều** (FE OR nhiều key LỎNG HƠN policy khớp-CHÍNH-XÁC-1-key ⇒ tự chuốc 403) · hợp-đồng FE↔BE VỠ 6 điểm mà tsc+build+574-test XANH (2 lỗi CHE NHAU) ⇒ so ROUTE+FIELD trên ĐĨA.
@ -93,6 +94,7 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
---
## 🔄 Curate trigger
- ⚠️ **@S167-K7 curate 2 pass: 22.1KB → 19.0KB** (move verbatim K5·K4c·K7·K6 → `archive/2026-07.md` §`@S167-K7-curate` + §`-curate-2`, mỗi pass probe moved-not-cut + control-âm TRƯỚC khi cắt, 4/4 pass). **CÒN NỢ ~1.9KB** để về 17.1KB — ứng viên kế: gộp khối `📁 Area memory` (nhiều dòng L2 đã stale >2 tháng). 🔴 **no-self-exempt CÓ ÁP**: entry K7 của CHÍNH TÔI là entry to nhất (3081B, phá luật "≤1.5K/entry" của chính file này) ⇒ nén trước, không miễn cho mình. 🔴 **Bài S166-K5 TÁI PHÁT NGAY trong pass này**: digest sinh bằng script đẻ chữ hỏng câm (`'ll3/13'` thay `'13/13'`) — byte-count không bắt được, chỉ ĐỌC LẠI BẰNG MẮT mới thấy. Script sinh chữ = luôn đọc lại output.
-**NỢ CURATE @S166-K4b ĐÃ TRẢ @S166-K5** (23.4KB→~17KB): move VERBATIM 3 entry đã hẹn (K4b·K4a·vá-F-1) → `archive/2026-07.md` §`@S166-K5-curate`. Trình tự bài S102: **append archive TRƯỚC → probe 3/3 kèm control-âm → assert `line in arc` NGAY TRONG script cắt → mới cắt → hậu-kiểm L1=0/archive=1**. Bài mới: digest sinh bằng escape `\uXXXX` **đẻ chữ hỏng câm** (byte-count không bắt được) ⇒ **script sinh chữ phải ĐỌC LẠI bằng mắt**.
- Hook-cap **>17.1KB** (24.4KB read-limit, S109) → archive recent → L2 `archive/<period>.md` (append additive) + `_INDEX.md` pointer. Stale >3mo → remove.
- **Prev curates:** S166-K5 (probe 3/3) · S162 (2/2) · S145 · S140 · S134 · S128 · **S102 recovery: cut-not-moved ×5 `f229b07` ⇒ mọi L1-shrink kèm '→archive' BẮT BUỘC grep moved-not-cut TRƯỚC khi trust** · S116·S113·S109·S80·S71·S70·S40.

View File

@ -211,3 +211,23 @@
- **S166 (08-01) gate K4a 49-row menu KHKK 8-nhóm — PWF 4/0 blocker:** lõi ĐÚNG hiếm thấy (bộ-sinh 1 nguồn feed 3 phía `All`/seed-row/grant ⇒ khớp by-construction; 8/8 nhãn khớp TỪNG KÝ TỰ; suite **620/620 tôi tự chạy**). **Bài thu:** ① 🔴 **tham-số-trang-trí** — URL leaf mang `?group=n``grep "get('group')"` ×2 app = **0 hit** ⇒ 7 nhóm × 6 leaf mở ra CÙNG nội dung, `WfView` hiện toàn bộ workflow type-10; *đo "route có tồn tại" là chưa đủ, phải đo **AI ĐỌC** từng param* (đối xứng ca S164 "tham-số-chết-vì-thiếu-tầng-UI", lần này chết ở tầng ĐỌC). ② **mỏ-neo tự-vô-hiệu ngay lúc land** — 2 comment mới trỏ dòng trong CHÍNH file đang sửa, sai đúng **+34 = kích thước khối vừa chèn** (tác giả tính trên bản trước khi chèn); cùng diff đó vừa DỌN lỗi y hệt ở BE (`1893`) ⇒ **mỏ neo trỏ vào file mình đang sửa phải đo SAU khi chèn**, hoặc bỏ số dùng grep-anchor. ③ **ô canonical sai đo-được bằng 1 curl** — STATUS ghi bundle `mySTlx42`/`CZAYiWWa`, live = `1yiNV4VH`/`DoULfmdT` (2 css KHỚP ⇒ chứng đo đúng site, không phải đo nhầm). ④ **bất-biến từng-vỡ-thật lại là bất-biến 0 test** (revoker không đụng `Khkk_*`; thêm 1 chữ vào filter thì 620/620 vẫn xanh, đúng kịch bản Run #423 mà chính comment dẫn lại) — khuôn có sẵn `Revoke_DoesNotTouch_PeModule`. ⑤ test gọi seeder LẺ qua reflection ⇒ chuỗi `SeedAsync` (grant→revoke→site-3) ngoài vùng test ⇒ **restart-thật KHÔNG được coi là đã phủ**. Tag `[s166, tham-so-trang-tri, moneo-tu-vo-hieu, o-canonical-sai-1-curl]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4a.md`
- **S166 (08-01) vá F-1 ô-tích opt-out `applyLevelFinalize` KHKK — PWF 7 finding (3 MAJOR):** vá ĐÚNG kỹ thuật (mirror 2 app chứng bằng **blob-SHA in ngay trong `git diff` index-line** — rẻ hơn sha256 tay; body không rò field; cấp thường bất-biến từng bit). **Bài thu:** ① 🔴 **khuôn-nguồn có 2 tầng, chú-thích nói dối tầng dưới** — implementer trích ĐÚNG dòng JSX PE `:772-791` + nhãn "khuôn PE S96 default tick", nhưng `useState` PE `:61` = `false` (S97 owner **ĐẢO** sang opt-IN) và chính chú-thích PE `:769-771` cũng stale ⇒ **đo khuôn phải đo GIÁ-TRỊ KHỞI-TẠO, không đọc chú-thích**; tái phát S165 F-1 (đo sai tầng). ② **cây làm việc PHÌNH GIỮA LƯỢT REVIEW** — đo `git status` 2 lần cách ~40 tool-call: 5 file→7 file (+121 dòng K4a, `All` 64→113) ⇒ **verdict phải neo mốc đo + re-đo `--stat` TRƯỚC khi phát verdict**, nếu không lead commit ké code chưa qua cổng. ③ vắng-mặt-đọc-thành-sạch: `isApproved ⇒ mọi Bước 'Done'` ⇒ CEO chưa đụng vẫn "đã duyệt", dấu-vết duy nhất = THIẾU dòng "✓ ký". ④ **sợi-dây-1-argument** (projection `l.AllowApproverFinalize`) 0 test ⇒ xoá là control biến mất mà 614 test + tsc vẫn xanh. Tag `[s166, khuon-2-tang-chu-thich-noi-doi, cay-phinh-giua-review, sợi-day-1-argument]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-f1-checkbox.md`
## @S167-K7-curate — verbatim moved from L1 (reviewer MEMORY.md)
> Moved 2026-08-01 during gate K7 closeout. L1 was 22KB vs 17.1KB cap.
> Digest line kept in L1 points here.
- **S166 (08-01) gate K5 cây GĐ2 mở 8 sub-folder nhóm — PASS 0 blocker (1 CLARIFY + 5 MINOR):** lõi hiếm-đúng: bất-biến Σ do **CẤU TRÚC** (1 mảng nguồn `khkkItems` → 1 phép phân hoạch if/else TOÀN PHẦN → `stage.leaves` vẫn là chỗ đếm duy nhất `stageCount():96`) nên không phụ thuộc ai nhớ cộng; route `?group=` nối đủ 3 chặng (FE `:79-80` → param `:124` → BE `Where(:757)`), **khác hẳn tham-số-trang-trí K4a**. **Bài thu:** ① 🔴 **phép-đo-rẻ-nhất nằm ở file RÁC của lane khác**`menus.json` untracked ở gốc repo (sản phẩm curl của cicd-monitor) chính là payload `/api/menus/me` LIVE ⇒ đối chiếu nhãn cây suy-ra ⟂ nhãn sidebar thật **8/8 khớp từng ký tự + `displayLabel` đều null**, thứ mà đọc code không bao giờ chứng được. Cùng file đó là **rào commit** (`git add -A` sẽ nuốt 44KB rác) ⇒ 1 vật vừa là bằng-chứng vừa là rủi-ro, đừng chỉ báo 1 vế. ② **chân-lý-rỗng phải khai là rỗng**: curl prod `total=0` phiếu KHKK ⇒ phân bố `approvalGroup` = `{}`, KHÔNG dùng để kết luận gì; nhưng cùng con số ấy đẻ ra sự-thật dùng được cho UAT (ngày đầu mọi gói hiện **8 dòng "SẮP" rỗng** — đúng spec, dễ bị đọc thành hỏng). ③ **tương-đương-từng-vế > "cùng tập cùng thứ tự"**: vòng lặp GĐ3/4 bị viết lại ⇒ lập bảng 4 vế (tập id · thứ tự = `Set` giữ thứ-tự-chèn · khử trùng · dòng-phụ `find` = phiếu ĐẦU) mới dám phán 0-regression; thân stage4 **0 tham chiếu `p`** là lý do cấu-trúc khiến rewrite an toàn. ④ **tiền-lệ phải đo ở BASE, không đo ở cây đã sửa**: `git show 88e7ecf:<file> | grep -A6 '<summary'` cho thấy 6/6 summary cũ chỉ chứa `span`/`svg``<button>` lồng trong `<summary>`**khuôn MỚI** (nested-interactive a11y), lane tưởng đang bám tiền lệ. ⑤ **"15/15 OK" chết theo phiên**: script `new Function` không commit + FE **0 hạ tầng test** (`package.json` chỉ `dev|build|lint|preview`) + chuông Σ là `import.meta.env.DEV` ⇒ prod bị strip ⇒ 0 lưới an toàn; đừng đọc số-test-ad-hoc thành đã-phủ. ⑥ 2 nghĩa của 1 dòng checklist ("GĐ3/4 by c.type") — lead và lane **cùng chọn 1 nghĩa không chứng minh nghĩa đó đúng ý owner** ⇒ ghim cách đọc vào commit. Tag `[s166, bang-chung-o-file-rac, chan-ly-rong, tuong-duong-tung-ve, tien-le-do-o-base]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k5.md`
- **S166 (08-01) gate K4c chốt cụm-2 (FE lọc nhóm WfView + 1 Fact test) — PASS 0 blocker (1 CLARIFY + 3 MINOR):** ① 🔴 **flake chỉ lộ ở LƯỢT ĐẦU, và chính tôi giết mất chứng cớ**: chạy `dotnet test -v q` SONG SONG với `tsc` ⇒ 1 test CŨ đỏ (`Failed 1/8`) rồi 10/10 xanh sau đó + suite 622/0 ⇒ `-v q` **nuốt thông điệp assert** ⇒ có tên test mà không có lý do. Bài: lượt chạy ĐẦU dùng verbosity thường, `-v q` để dành cho lượt lặp. Truy nhiễm phải đo 3 vế mới dám nói "không do diff": fixture `:memory:` per-test · codegen **0 static** · xUnit 2.9.3 same-class = tuần tự. Báo "8/8 ở 11/12 lượt", KHÔNG báo 8/8 tuyệt đối. ② **probe RED bằng đổi TEST-INPUT (không chạm prod) là hợp lệ CHỈ KHI tham số xuất hiện ĐÚNG 1 CHỖ trong handler** — grep xong mới được chấp nhận; ≥2 chỗ là lập luận sập. ③ `Total` là dấu-vân-tay lọc-SERVER **chỉ khi** `CountAsync` chạy trên `q` SAU `Where` và TRƯỚC `Skip/Take` — đọc `ProjectPagedAsync:790-792` mới dám ký, đừng tin comment. ④ **queryKey KHÔNG thêm trục là ĐÚNG** khi BE endpoint không nhận trục đó (đọc controller: chỉ `applicableType`+`isUserSelectable`) + giá trị lọc là **dẫn xuất trong render** (không `useState`) ⇒ miễn nhiễm bẫy "đổi leaf không remount". ⑤ Guard `/^[1-8]$/` chặn `Number("03")=3`, và **đồng nhất 3 trang** KHKK — kiểm cả 3, lệch 1 chỗ là 2 luật cho 1 tham số. ⑥ Lane khai bundle `1,643.96 kB` — tôi chạy lại `npm run build`, **trùng từng chữ số** ⇒ claim đo-lường có thật. Tag `[s166, chung-co-bi-nuot-boi-verbosity, probe-1-cho-moi-hop-le, total-la-van-tay]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k4c.md`
## @S167-K7-curate-2 — verbatim moved from L1 (reviewer MEMORY.md)
> Moved 2026-08-01 at gate-K7 closeout. L1 over the 17.1KB cap; both entries also broke
> the file's own "entry <= 1.5K chars" rule. no-self-exempt: the K7 entry is my own.
- **S167 (08-01) gate K7 cầu KHKK→HĐ (13 file, 3 nhóm) — PASS-WITH-FLAGS 10 (2 MAJOR + 7 MINOR + 1 CLARIFY), 0 blocker:** ① 🔴 **1 con số đóng 2 câu hỏi**: baseline 622 (đo ở K4c CÙNG NGÀY) + 17 `[Fact]` mới (đếm `grep -c '^\s*\[Fact\]'`, không chép) = kỳ vọng 639, đo được **đúng 639/0** ⇒ chứng CẢ "17 test có chạy thật (không bị filter/skip câm)" LẪN "0 test cũ vỡ" — rẻ hơn 2 phép đo rời. ② **mỏ-neo tự-vô-hiệu TÁI PHÁT và lan 3 nơi** (K4b-class): 3 chú thích mới trỏ `DbInitializer.cs:2516`; `git show HEAD:<file> | grep -n` = 2516, `grep -n` trên cây = **2523** — CHÍNH diff này chèn 7 dòng ở `:2353-2359` đẩy nó xuống ⇒ đúng lúc viết, **sai ngay khoảnh khắc land**. Bài: phải đo ở CẢ 2 THẾ GIỚI (HEAD ⟂ cây) mới thấy, đo 1 thế giới là im. ③ **prod probe sửa được con số của chính comment**: `GET /api/permissions/by-role/{id}` ×13 vai → `Contracts.CanCreate` **1/13 true (chỉ Admin)** ⇒ tiền-đề seeder ĐÚNG (Drafter/Procurement=False, lỗ THẬT), nhưng comment "403 với 13/13 role… chỉ Admin qua nhờ bypass" sai 2 vế: thật là **12/13**, và Admin **đã có row True** chứ không chỉ bypass. Cùng probe đẻ **acceptance rẻ nhất cho sau deploy: log phải là `0 added + 2 upgraded`** — lệch = có thứ khác lật quyền. Khai giới hạn: prod CHƯA deploy ⇒ chỉ chứng TIỀN-ĐỀ, không chứng seeder chạy đúng. ④ **test có răng nhưng phủ NỬA**: T11 double-click assert 4 vế (Conflict + count=1 + line trỏ HĐ đầu + `LastSeq=1` không đốt mã) — nhưng là `await` lần 1 RỒI lần 2 ⇒ chỉ phủ ca TUẦN TỰ; ca ĐỒNG THỜI vẫn hở (`:128-129` đọc-rồi-ghi trong bộ nhớ, 0 unique-index, 0 RowVersion, 0 transaction bao ngoài — transaction duy nhất là của codegen, đã commit trước). Comment test khai đúng phạm vi ⇒ KHÔNG phải lane nói quá; rủi ro là người sau đọc "T-idem có" thành "idempotency đóng". ⑤ **ngoại lệ authz đúng cách**: map `Dictionary<action,policy>` + `Should().Be()` khớp TUYỆT ĐỐI (không `StartsWith`/skip-list) + action ngoài map vẫn rơi `else` đòi prefix module ⇒ **action tương lai không tự miễn trừ**; kèm `Keys.Should().BeSubsetOf(byName.Keys)` chống **cấu-hình-chết** — vế "vắng-mặt trông giống ổn" mà lane TỰ đặt. ⑥ **mirror byte-identical đẻ rủi ro riêng**: 3 đường dẫn cứng `/contracts/${id}` trong file dùng chung 2 app ⇒ phải grep route ở CẢ `fe-user/App.tsx:65``fe-admin/App.tsx:84` (đều có, không thủng) — SHA-identical KHÔNG bảo chứng route-identical. ⑦ residual: 3 `scripts/*.ps1` dirty ngoài cả 13-file-set lẫn lead-set. Tag `[s167, mot-so-dong-2-cau-hoi, moneo-tu-vo-hieu-lan-3-noi, probe-sua-so-cua-comment, rang-phu-nua, sha-khong-bao-chung-route]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k7.md`
- **S167 (08-01) gate K6 admin thu-gọn (1 file FE, tập 12 key ẩn) — PASS-WITH-FLAGS 5 (1 MAJOR + 1 CLARIFY + 3 MINOR):** ① 🔴 **cây menu PROD SỐNG là thước, seed chỉ là bản vẽ**: `curl /api/menus/me` (token admin, 44KB) → áp CHÍNH tập key **cắt bằng regex từ file đã ship** ⇒ 200 node · 166 lá → **15 lá**, trùng tên 15/15 + control-âm bỏ `KeHoachKyKet` = 63 (+48) khớp lane. Cùng payload đóng luôn FLAG "row chèn tay": **10 `AwV2*`, 0 `AwV2_Contract`**. Khai giới hạn: endpoint đã lọc-quyền ⇒ row không có Permission thì vô hình (nhưng cũng vô hình trên sidebar ⇒ không lệch C1). ② **`filterForAdmin` mới là bước MỘT**: lá còn "THẤY" phải qua tiếp `resolvePath→null ⇒ MenuLeaf drop im lặng` (#50) và **group rỗng-hoá tụt xuống render như LEAF** (`children.length>0`) — lane bỏ cả 2, tôi đo: 0 drop · 0 rỗng-hoá. ③ **typo-noop**: key ẩn sai chính tả = `Set.has` false, module vẫn hiện, tsc vẫn xanh ⇒ phải assert từng key TỒN TẠI trong cây prod (12/12 OK; control-âm 3 key bịa đều MISS). ④ **MAJOR: comment mới nhồi 3 số-đếm, 1 số SAI ngay lúc land** — "28 leaf `Ct_`" (thật 42 lá/49 key, S155 thêm WfView/Approved/Deleted), số chép từ skill `permission-matrix` stale ⇒ đúng bệnh #84(4) "CẤM số-đếm trong comment". ⑤ **CLARIFY hệ-quả không ai khai**: prod có `Hrm/Off/Personal` `isVisible=false`, fe-user lọc `isVisible!==false` còn fe-admin KHÔNG đọc cờ đó ⇒ admin là lối vào CUỐI, ẩn nốt = 3 module mất menu ở CẢ 2 app. ⑥ display-only chứng 3 tầng: diff (grep `+` chỉ hit COMMENT) < kiến-trúc (`usePermission:15` đọc cây THÔ, chỉ 2 consumer) < curl prod 200×2 endpoint đã ẩn. Tag `[s167, thuoc-la-cay-prod, filter-moi-la-buoc-mot, typo-noop, so-dem-trong-comment]` **Evidence:** `runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-gate-k6.md`

View File

@ -0,0 +1,25 @@
CICD-K7: IN-PROGRESS (đang đo — verdict ở dòng cuối)
# sub-cicd-verify-k7 — deploy verify K7 (bridge KHKK→HĐ), S167 2026-08-01
> Artifact chống-#53: ghi TỪNG MỤC ngay khi có bằng chứng. Return chỉ là tóm tắt.
> Baseline PRE đọc từ `sub-cicd-verify-k6.md` (post-#440): admin js `Ig1R5tAh` · eoffice js `DFOCQ2ks` · gate 622=45+577 · Mig 71 · sys.tables 97.
---
## M0 — Push + path-filter (ĐÃ ĐO)
- HEAD = `aaed699228f418fa31ecba07414709de465502fc`
subject: `[CLAUDE] Contract: K7 SP-002 bridge KHKK->HD hoi-2-khoa + Line.ContractId + grant seeder (gate PASS-WITH-FLAGS 10, F-8 fixed)`
committed: 2026-08-01 13:04:49 +0700
- `git log origin/main..HEAD` = **RỖNG** ⇒ đã push thật.
- Range `1c6d77a..aaed699` = 7 commit (6 × `wal: flush` + 1 code).
- **File code trong range (13/13 khớp đề bài):**
- BE (4): `Api/Controllers/ContractSigningPlansController.cs` · `Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` · `Application/ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs` · `Infrastructure/Persistence/DbInitializer.cs`
- FE mirror ×2 app (6): `fe-{admin,user}/src/hooks/usePipelineStages.ts` · `.../pages/khkk/KhkkDetailPage.tsx` · `.../types/khkk.ts`
- Test (3): `Api/AuthorizePolicyRegressionTests.cs` · `Api/KhkkBridgeAuthzTests.cs` · `Application/CreateContractFromSigningPlanTests.cs`
- (còn lại 14 file đều `.claude/**` = paths-ignore, KHÔNG trigger)
- **M3-phần-1 (git):** `git diff --name-only 1c6d77a..aaed699 -- '*Migrations*'` = **0 file** ⇒ 0-mig chứng-minh-được từ git.
- Path-filter: có file `src/**` + `fe-*/**` ⇒ CI PHẢI chạy (không skip).
---

View File

@ -0,0 +1,469 @@
GATE-K7: PASS-WITH-FLAGS 10
# Gate K7 — adversarial review cầu KHKK → HĐ (S167, 2026-08-01)
Vai: reviewer (READ-only). Base đo: HEAD = `f5ee146` (`wal: flush 20260801T1242`), diff UNCOMMITTED.
Nguyên tắc: mọi mục dưới đây là phép đo TỰ CHẠY của reviewer, không chép claim của lane.
---
## §0 — Mốc cây + residual scope
Đo bằng `git status --porcelain` + `git diff --stat` (2026-08-01, đầu lượt review).
13 file thuộc phạm vi K7 — CÓ ĐỦ, đúng như spec giao:
| Nhóm | File | Trạng thái |
|---|---|---|
| BE | `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` | M (+33) |
| BE | `src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` | M (+23/-2) |
| BE | `src/Backend/SolutionErp.Application/ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs` | **?? MỚI** (252 dòng) |
| BE | `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` | M (+86) |
| FE | `fe-{admin,user}/src/hooks/usePipelineStages.ts` | M (+61/-?) ×2 |
| FE | `fe-{admin,user}/src/pages/khkk/KhkkDetailPage.tsx` | M (+308) ×2 |
| FE | `fe-{admin,user}/src/types/khkk.ts` | M (+49) ×2 |
| Test | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs` | M (+48/-?) |
| Test | `tests/SolutionErp.Infrastructure.Tests/Api/KhkkBridgeAuthzTests.cs` | **?? MỚI** |
| Test | `tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromSigningPlanTests.cs` | **?? MỚI** |
**FLAG RESIDUAL (F-R1, MINOR)** — 3 file dirty NGOÀI cả 13-file-set LẪN lead-set đã khai
(`.claude/*` + `docs/STATUS.md`):
- `scripts/distill-shard-probe.ps1` (+1/-1)
- `scripts/measure-agent-memory.ps1` (+1/-1)
- `scripts/memory-archive-gate.ps1` (+11/-2)
Đọc diff: cả 3 là việc **governance memory-index rotation `_INDEX-<period>.md` @S167**, KHÔNG
liên quan K7. Không phải lỗi nội dung; rủi ro là **`git add -A` sẽ nuốt chúng vào commit K7**.
Đề nghị: hoặc khai chúng vào lead-set, hoặc commit tách. Untracked trong `src`/`tests` = đúng
3 file MỚI đã khai, **0 file lạ**.
---
## §1 — TRỌNG TÂM 1: hội-2-khoá 2 tầng (BE ∧ FE)
### 1a. Tầng server — ĐO ĐƯỢC, ĐẠT
- Class-level: `ContractSigningPlansController.cs:29``[Authorize(Policy = "KeHoachKyKet.Read")]`.
- Action mới: `ContractSigningPlansController.cs:227-228`
`[HttpPost("{id:guid}/create-contract")]` + `[Authorize(Policy = "Contracts.Create")]`.
- ASP.NET Core cộng dồn class + action theo AND ⇒ hội 2 khoá là THẬT, không phải claim.
- Policy `Contracts.Create` có tồn tại: `MenuKeys.cs:12` khai const `Contracts`, và
`MenuKeys.cs:228` đưa `Contracts` VÀO mảng `All``Program.cs` sinh đủ 4 policy.
(Nếu key ngoài `All` thì runtime 500 chứ không 403 — đã kiểm, không rơi vào ca đó.)
### 1b. Tầng FE — ĐỌC DIFF TỪNG DÒNG (lead ghi §3-§4 on-behalf; đây là lưới THẬT đầu tiên)
`fe-user/src/pages/khkk/KhkkDetailPage.tsx` (bản admin byte-identical, xem §3c):
- `:138-140``const { can } = usePermission()`; `canReadPlan = can('KeHoachKyKet','Read')`;
`canCreateContract = can('Contracts','Create')`. Đúng 2 key của server, KHÔNG OR `Khkk_*`
(gotcha #85 giữ được).
- `:299``showBridge = phase === KhkkPhase.DaDuyet && canReadPlan`.
- `:303-312``bridgeBlockReason`: nhánh ĐẦU TIÊN là `!canCreateContract` ⇒ nút render nhưng
`disabled`.
- `:352-363``<Button disabled={bridgeBlockReason != null} onClick={() => setBridgeOpen(true)}>`.
**Truy đường lách:** grep toàn file cho `setBridgeOpen(true)` → xuất hiện ĐÚNG 1 lần, tại
`onClick` của nút đã disabled. Dialog `CreateContractFromKhkkDialog` render có điều kiện
`{bridgeOpen && ...}` (`:781`) ⇒ **không có đường nào tới `create.mutate()` khi thiếu
`Contracts.Create`**. React không bắn `onClick` trên `<button disabled>`.
⇒ FE **KHÔNG lỏng hơn server**. ĐẠT.
**Verify hook đọc gì:** `fe-user/src/hooks/usePermission.ts``findNode(menu, key)` đệ quy trên
cây THÔ từ `AuthContext`, trả `false` khi không thấy node; `Create``node.canCreate`. Nghĩa là
gate FE phụ thuộc **row Permission của key `Contracts`**, đúng thứ seeder mới cấp.
**CLARIFY-1 (không phải lỗi mã, là điều kiện nghiệm thu):** cây menu FE nạp lúc login và cache
`localStorage` (`AuthContext.loadMenu`). Comment seeder khai "grant ăn @restart, KHÔNG phải
deploy FE" — đúng cho BE, nhưng **nút vẫn xám cho tới khi user logout/login lại**`can()` đọc
menu cache cũ. UAT phải nói rõ bước đăng-xuất/đăng-nhập, nếu không sẽ bị đọc thành "seeder không
ăn".
---
## §2 — TRỌNG TÂM 2: seeder grant, gotcha #84 nghiêm
`DbInitializer.cs:2738-2814` = `SeedContractCreateAccessAsync`; call-site `:2361`.
### 2c. THỨ TỰ (vế nặng nhất) — ĐO BẰNG SỐ DÒNG THẬT, ĐẠT
`awk` liệt kê mọi `await Seed*/Revoke*` trong khối 2250-2420:
```
2331: await SeedAllRolesReviewReadPermissionsAsync(...)
2341: await RevokeTemporarilyHiddenModulesAsync(...) ← revoker
2347: await SeedProcurementMasterAccessAsync(...)
2354: await SeedKeHoachKyKetAccessAsync(...)
2361: await SeedContractCreateAccessAsync(...) ← MỚI, SAU revoker ✔
```
Và trong cửa sổ 2361→2520 **không còn call nào khác** ⇒ seeder mới là seeder-quyền CHẠY CUỐI,
không ai lật lại nó. ĐẠT.
Đọc thêm thân revoker `:2550-2589` để không chỉ tin thứ tự: filter là
`MenuKey.StartsWith("Hrm") || StartsWith("Off") || == Personal` — **nhánh `Contracts`/`Ct_*` đã
bị GỠ từ S159**. Vậy claim "revoker hiện không còn nhánh Contracts" là ĐÚNG, và việc giữ thứ tự
sau revoker là phòng-thủ dư (đúng chiều, không hại).
### 2b. UPGRADE-ONLY + IDEMPOTENT — ĐẠT
- Nhánh row-đã-có (`:2790-2797`): chỉ `if (!row.CanRead) row.CanRead = true`
`if (!row.CanCreate) row.CanCreate = true`. **Không chạm `CanUpdate`/`CanDelete`, không hạ cờ
nào.** Không có `= false` ở bất kỳ đâu trong hàm.
- Nhánh row-chưa-có (`:2800-2808`): tạo `CanRead=true, CanCreate=true, CanUpdate=false,
CanDelete=false`.
- Idempotent: lượt 2 trở đi `added=0, upgraded=0` ⇒ **không gọi `SaveChangesAsync`** (`:2811`),
không log rác. ĐẠT.
- Guard role-chưa-seed (`:2779-2784`): `roles.Count == 0` → log warning + `return`, không ném.
### 2a/2d. TIỀN ĐỀ "chưa role nào có Contracts.Create" — ĐO ĐƯỢC TỪ MÃ
`DbInitializer.cs:2474` `reviewKeys = MenuKeys.All.Where(InReviewScope)…` ⇒ `Contracts` nằm
trong tập. `IsPeKey("Contracts") == false` ⇒ nhánh insert dùng `CanCreate = isPe` (`:2523`) =
**false**, và nhánh row-đã-có là **skip-existing** cho key non-Pe (`:2515`) ⇒ không ai nâng
`CanCreate` lên. Tiền đề của lane ĐÚNG.
Bộ vai: `roleNames = { Drafter, Procurement, Admin }` (`:2775`) — đúng 3 vai spec nêu, kèm comment
"câu III default, chờ anh ratify" và chỉ-dẫn sửa 1 chỗ. ĐẠT.
**MINOR F-2** — Admin thừa: `MenuPermissionHandler.cs:26-31` có Admin-bypass (`Succeed` ngay khi
role chứa `Admin`) ⇒ cấp row cho Admin **vô hại nhưng vô tác dụng**. Không đề nghị sửa; nêu để
người sau đừng đọc row Admin thành "bằng chứng Admin cần row".
**MINOR F-3** — comment `:2814` viết "Thu hồi khi cần: xoá call ở SeedAsync", nhưng call thật
nằm trong `SeedAdminPermissionsAsync` (thân 2288-2381), không phải `SeedAsync`. Chỉ-dẫn trỏ sai
tên hàm.
**MINOR F-4 (mở rộng ngoài Create)** — hàm nâng cả `CanRead`, trong khi tên hàm + comment chỉ hứa
`Contracts.Create`. Với 3 vai này `CanRead` gần như đã true từ S159 nên thực tế không đổi gì;
nhưng `reviewKeys` có ghi rõ "trên prod row false KHÔNG được nâng ở đây" ⇒ nếu prod có role nào
đang `Contracts.CanRead=false` thì hàm này **âm thầm mở lại quyền XEM**, rộng hơn nhãn.
---
## §3 — TRỌNG TÂM 3: chuỗi dữ liệu cây, khớp từng khớp nối
### 3a. Write-site → DTO (BE)
- Write-site: `CreateContractFromSigningPlanFeatures.cs:238` → `foreach (var l in lines) l.ContractId = contract.Id;`
Đây là gán THẬT trên entity đã tracked (`plan.Lines` nạp qua `Include` `:93`), nằm TRƯỚC
`SaveChangesAsync` `:248`. ĐẠT.
- Fallback: `:246` `if (pe is not null) pe.ContractId ??= contract.Id;` — `??=` đúng chiều
(HĐ đầu thắng, không cướp ô của cầu PE cũ).
- DTO list: `ContractSigningPlanFeatures.cs:75` `List<Guid> ContractIds` (append CUỐI record ⇒
positional-arg của các call-site cũ không lệch).
- Projection: `ContractSigningPlanFeatures.cs:836-842` — subquery
`Where(l => l.ContractSigningPlanId == p.Id && l.ContractId != null).OrderBy(CreatedAt)
.Select(l => l.ContractId!.Value).ToList()`. Filter `!= null` đứng TRƯỚC `.Value` ⇒ không NRE,
và line chưa bridge thì mảng RỖNG (không phải null-element). ĐẠT.
### 3b. DTO → FE (tên + null-safety)
- `types/khkk.ts:200` → `contractIds?: string[] | null`. JSON camelCase khớp `ContractIds`.
Khai optional dù BE non-null: đúng, vì cache React Query cũ / API chưa deploy không có field.
- Helper `types/khkk.ts:304-313` `khkkPlanContractIds(plan)`:
`for (const id of plan.contractIds ?? []) if (id) out.push(id)` +
`for (const line of plan.lines ?? []) if (line.contractId) out.push(line.contractId)`.
**Ca "line không chọn ⇒ ContractId null ⇒ mảng rỗng, không crash" = ĐẠT** — cả 2 vòng đều
`?? []` và đều lọc falsy trước khi push.
- Chữ ký `plan: KhkkListItemDto & Partial<Pick<KhkkDetailDto,'lines'>>` ⇒ truyền
`KhkkListItemDto` thuần vẫn hợp lệ (lines optional). ĐẠT.
- Union + dedupe: `usePipelineStages.ts:295-300` — `contractIdsOfPackage` là `Set<string>`, đổ
nguồn (1) `p.contractId` rồi nguồn (2) `khkkPlanContractIds(k)`. Chồng nhau ở HĐ đầu là CỐ Ý và
`Set` khử. ĐẠT.
- Dòng phụ: `usePipelineStages.ts:321-326` — `fallbackName` nay tra CẢ 2 nguồn
(`peItems.find(...)?.tenGoiThau ?? khkkItems.find(k => khkkPlanContractIds(k).includes(id))?.peTenGoiThau ?? null`).
Đo field tồn tại: `types/khkk.ts:171 peTenGoiThau: string | null`, BE `…Features.cs:39
string? PeTenGoiThau`. Khớp. ĐẠT.
### 3c. Mirror 2 app — TỰ ĐO SHA, KHÔNG TIN CLAIM
```
IDENTICAL src/types/khkk.ts 60ec8fa3a5aea94456e2180b1b80f63c1994257c55a424f4e1f7adf376cbbc07
IDENTICAL src/hooks/usePipelineStages.ts 76e1823345d4e78b87e802bc10d22ea0a42307bbb426b8787eb2d25b02e8c52f
IDENTICAL src/pages/khkk/KhkkDetailPage.tsx cf0a2272061f4bd8cc19c54b1a4992fdac331e9093665ec4ce708e119ef38123
```
3/3 byte-identical — claim của lane TÁI LẬP ĐƯỢC.
**Rủi ro riêng của mirror byte-identical (đã kiểm, KHÔNG thủng):** file dùng đường dẫn cứng
`/contracts/${id}` ở 3 chỗ (`:527` neo "✓ HĐ", `:975` neo "Đã có HĐ", `:855` `navigate`). Vì 2
app có bảng route KHÁC nhau, đây là chỗ dễ 404 một bên. Đo:
`fe-user/src/App.tsx:65` và `fe-admin/src/App.tsx:84` đều có `<Route path="/contracts/:id">`.
⇒ ĐẠT ở cả 2 app.
**MINOR F-5** — 2 neo dùng `<a href>` thay `<Link>` ⇒ full reload (mất SPA state); và neo
"Đã có HĐ" (`:975`) nằm LỒNG trong `<label>` (nested-interactive, cùng lớp a11y với finding K5 về
`<button>` trong `<summary>`). Radio của dòng đó `disabled` nên không sinh double-activate, nhưng
khuôn thì vẫn là interactive-lồng-interactive.
**MINOR F-6 (hiệu năng, không sai)** — `usePipelineStages.ts:324` gọi `khkkPlanContractIds(k)`
BÊN TRONG `find` mà `find` lại nằm trong vòng lặp theo `contractIdsOfPackage` ⇒ O(n·m) và cấp phát
mảng mới mỗi lượt. Quy mô hiện tại (list kẹp 200) vô hại.
### 3d. Chuông DEV — đọc kỹ vì dễ thành chuông-câm hoặc chuông-loạn
`usePipelineStages.ts:303-315`: chỉ chạy khi `import.meta.env.DEV`, có cờ module-level
`warnedMissingLineContractIds` (`:62`) chặn lặp; điều kiện
`approved.length > 0 && approved.every(k => k.contractIds == null)`.
`== null` bắt cả `null` lẫn `undefined` — ĐÚNG chủ đích "vắng-field ⟂ mảng-rỗng", vì sau khi BE
deploy field luôn là `[]` (non-null) ⇒ chuông tự im. Logic ĐẠT.
Lưu ý (không phải lỗi): prod strip `DEV` ⇒ đây **không phải lưới an toàn**, chỉ là đèn báo lúc dev.
---
## §4 — TRỌNG TÂM 4: guard 4-vế + idempotency
Đọc `CreateContractFromSigningPlanFeatures.cs` toàn file (252 dòng).
Rào theo thứ tự thực thi:
| # | Dòng | Nội dung | Nhận xét |
|---|---|---|---|
| 0 | :87-89 | `IsAuthenticated` + `UserId` | ném `UnauthorizedException` |
| 1 | :92-99 | phiếu tồn tại + `Phase == DaDuyet` | ĐẠT |
| 2 | :101-109 | dòng ∈ phiếu (đếm sau `Distinct`) | id lạ/của phiếu khác đều rơi vào đây — ĐẠT |
| 3 | :111-115 | 1 HĐ = 1 NCC | ĐẠT |
| 4 | :125-127 | `ApprovedAmount is null` → 409 | KHÔNG fallback `ProposedAmount` — ĐÚNG |
| 5 | :128-129 | `ContractId is not null` → "đã có Hợp đồng" | chống bridge-lặp |
| 6 | :134-148 | workflow pin **4 vế** | xem dưới |
**vá-3 (4 vế workflow) — ĐẠT, và ĐÚNG "throw tách":**
`:136` NotFound nếu không tồn tại · `:137-140` sai `ApplicableType` · `:143-145` `!IsActive` ném
riêng · `:146-148` `!IsUserSelectable` ném riêng. Bốn thông điệp bốn nguyên nhân — đúng yêu cầu
spec, và **rộng hơn khuôn tiền lệ** `ContractFeatures.cs:79-87` (khuôn cũ CHỈ có 2 vế đầu — tôi đã
đọc lại `ContractFeatures.cs:74-88` để xác nhận, claim của lane đúng).
**Idempotency / double-click — ĐÂY LÀ FINDING NẶNG NHẤT.** Rào (5) là kiểm tra
**đọc-rồi-ghi trong bộ nhớ**, không có khoá nào ở tầng CSDL:
- không `unique index` trên `ContractSigningPlanLines.ContractId`;
- không `RowVersion`/optimistic concurrency trên `Line`;
- không transaction bao ngoài (`SaveChangesAsync` trần `:248`; transaction duy nhất trong luồng
là transaction NỘI BỘ của `ContractCodeGenerator`, đã commit xong trước đó).
⇒ Hai request song song (double-click thật, hoặc 2 tab) đều đọc `l.ContractId == null` ở
`:128`, cùng đi tiếp, **tạo 2 Contract**, và lượt ghi sau đè `Line.ContractId`. Kết quả: 1 HĐ mồ
côi (đã tiêu 1 số sequence, đã có changelog) mà không dòng nào trỏ tới. Đây là **TOCTOU thật**,
không phải lo xa: nút "Tạo hợp đồng" chỉ chặn bằng `disabled={create.isPending}` (`:892`) — chặn
được double-click trong CÙNG tab, **không** chặn 2 tab / retry mạng.
Xếp hạng: **MAJOR F-1**, không phải blocker của gate (dữ liệu không mất, HĐ mồ côi xoá được, và
xác suất thấp vì thao tác có dialog), nhưng phải được KHAI, vì lane trình mục này như "đã đóng".
Acceptance đề nghị (không phải chỉ-dẫn sửa mã): một phép kiểm chứng minh 2 lệnh gọi đồng thời chỉ
sinh 1 HĐ — hoặc bằng ràng buộc CSDL, hoặc bằng cập-nhật-có-điều-kiện, hoặc khai công khai
"chấp nhận rủi ro, chặn ở UI".
---
## §5 — TRỌNG TÂM 5: vá-6
- **Project/Supplier NotFound guard:** `:153-154` (`Project`) và `:156-157` (`Supplier`), cả 2 ném
`NotFoundException` trước khi dùng `.Code`. ĐẠT — loose-Guid không có FK đỡ lưng nên guard này
là bắt buộc.
- **Codegen TRƯỚC `Add`:** `:212` `contract.MaHopDong = await codeGenerator.GenerateAsync(contract,
project.Code, supplier.Code, ct);` rồi `:214` `db.Contracts.Add(contract);`. Đúng thứ tự bài S88
(codegen tự-commit ⇒ không được để HĐ dở trong context). Chữ ký khớp
`IContractWorkflowService.cs:29 Task<string> GenerateAsync(Contract, string, string, CancellationToken)`.
ĐẠT.
- **SlaDeadline:** `:205-206` `DateTime.UtcNow.Add(workflow.GetPhaseSla(DangSoanThao) ?? TimeSpan.FromDays(7))`
— trùng từng chữ với 2 tiền lệ (`ContractFeatures.cs:110`, bridge PE `:109-110`). ĐẠT.
- **`WorkflowDefinitionId` pin:** `:163-166` truy `WorkflowDefinitions` active theo
`request.ContractType` → `:201` gán. Ngang bridge PE cũ (đã đọc lại
`CreateContractFromEvaluationFeatures.cs:67-71` + field-map `:105-113` để đối chiếu). Set CẢ
V1 + V2 (`:202` `ApprovalWorkflowId`) là ĐÚNG — service dispatch V2 trước. ĐẠT.
**KHAI-1 của test-lane (invest T1 ghi null nhưng vá-6 thắng): XÁC NHẬN ĐÚNG — ĐÓNG, không mở lại.**
Lý do đo được: vá-6 trong spec ②:16 yêu cầu pin `WorkflowDefinitionId` theo 2 tiền lệ; mã hiện
tại làm đúng thế; "để null" là hành vi mà chính spec loại bỏ. Không có mâu thuẫn còn lại.
**MINOR F-7 (field-map thiếu so với tiền lệ, có thể CỐ Ý):** bridge PE cũ carry
`DraftData = pe.PaymentTerms` (`CreateContractFromEvaluationFeatures.cs:104`); cầu K7 **không**
carry `DraftData`, dù đã nạp `pe` sẵn ở `:170`. Cũng không có `BypassProcurementAndCCM` (cái này
hợp lý vì command K7 không nhận tham số đó). Nếu bỏ `DraftData` là chủ đích thì nên có một dòng
comment, vì mọi khoản lệch khác so với tiền lệ đều đã được chú thích.
---
## §6 — TRỌNG TÂM 6+7 (test có răng · comment sạch số-đếm)
### 6b. Comment mang số-đếm / mỏ-neo — CÓ LỖI, ĐO ĐƯỢC BẰNG 1 LỆNH
**MAJOR F-8 — mỏ-neo TỰ VÔ HIỆU ngay lúc land (đúng lớp S166-K4b, tái phát):**
2 comment MỚI trỏ `DbInitializer.cs:2516` cho `CanCreate = isPe`:
- `ContractSigningPlansController.cs:224` ("seeder `DbInitializer.cs:2516`")
- `DbInitializer.cs:2743` ("`SeedAllRolesReviewReadPermissionsAsync:2516`")
Phép đo (2 lệnh, cho 2 thế giới):
```
git show HEAD:…/DbInitializer.cs | grep -n "CanCreate = isPe" → 2516 (TRƯỚC diff)
grep -n "CanCreate = isPe" …/DbInitializer.cs → 2523 (SAU diff)
```
Chính diff này chèn 7 dòng ở `:2353-2359`, đẩy dòng đích xuống 2523. Nghĩa là con trỏ ĐÚNG lúc
viết và **SAI ngay khoảnh khắc commit** — không ai làm gì sai sau đó cả. Đây là lỗi loại "đo
trước khi chèn". Acceptance: hoặc cập nhật thành 2523, hoặc bỏ số dòng và trỏ bằng TÊN
(`SeedAllRolesReviewReadPermissionsAsync` + token `CanCreate = isPe` là duy nhất, grep ra ngay).
**MINOR F-9 — số-đếm trong comment tự mâu thuẫn:** `DbInitializer.cs:2743-2744` viết "nút bridge
sẽ 403 với **13/13 role** (kể cả Drafter), chỉ Admin qua được nhờ bypass". Đếm thật:
`AppRoles.cs:23-28` liệt **13** vai và Admin nằm trong đó; `MenuPermissionHandler.cs:26-31` cho
Admin bypass ⇒ con số đúng phải là **12/13**. Một câu chứa cả con số lẫn phản-ví-dụ của chính nó.
Ngoài 2 mục trên, đã quét các con số khác trong comment MỚI và chúng kiểm được:
`ContractConfiguration TenHopDong = 500` khớp `MaximumLength(500)` (`:73`) và clamp `:184`;
`ContractTypeLabel` 1..7 (`fe-user/src/types/forms.ts:13-21`) khớp ĐÚNG
`ContractType.cs:5-11` 1..7 ⇒ `IsInEnum()` không bao giờ đá dropdown.
### 6a. Ba file test — ĐỌC HẾT, CÓ RĂNG
**Đếm:** `KhkkBridgeAuthzTests` 5 `[Fact]` + `CreateContractFromSigningPlanTests` 12 `[Fact]`
= **17** — khớp đúng con số "17 test mới" spec nêu (đếm bằng `grep -c '^\s*\[Fact\]'`, không chép).
**`KhkkBridgeAuthzTests.cs` — răng thật, và đúng loại răng:**
- `:33-48` `BridgePolicies()` **đọc 2 tên policy TỪ CHÍNH CONTROLLER** bằng reflection, không
hardcode ⇒ ai sửa attribute thì test đo theo cái mới mà vẫn hỏi đúng câu. Kèm
`actionPolicy.Should().NotBe(classPolicy)` (`:45`) — chặn ca "2 vế trùng nhau thì hội-2-khoá
còn 1 khoá mà nhìn vẫn như cũ".
- Chạy **máy quyết định THẬT** `MenuPermissionHandler` (`:57-68`), không mô phỏng.
- (2) `:170-171` và (3) `:195-196` mỗi test đều assert **vế còn lại VẪN QUA** trước khi assert vế
thiếu bị chặn ⇒ không thể xanh nhờ "chặn tất cả". Đây đúng là thứ tôi định đòi.
- (5) `:235-243` phép **2 thế giới 1 biến**: cùng bộ quyền, chỉ lật `IsActive`, kết quả phải đảo.
- (4) `:214` assert `Permissions.Count == 0` trước khi kết luận Admin qua bằng bypass.
**`CreateContractFromSigningPlanTests.cs` — 12 ca, assert phân biệt được thế giới hỏng:**
- T1 `:243-245` — 3 con số CỐ Ý khác nhau (750 chốt / 999 đề xuất / 900 NS) ⇒ lấy nhầm nguồn nào
cũng ra số khác. T1 `:266` soi **RUỘT mã HĐ** `{ProjectCode}/HĐTP/SOL&{SupplierCode}/01` —
chính là bằng chứng vá-6 đã nạp 2 bảng Project/Supplier, mạnh hơn `NotBeNull`.
- T1 `:255-257` — assert pin **đúng bản V1 active của đúng loại**, và seed sẵn 1 bản NHIỄU loại
khác (`:197-201`) để `!= null` không đủ để xanh. Đây là cách đúng để đóng KHAI-1.
- T3 `:360` + T7 `:491` + T11 `:661-662` — assert **`ContractCodeSequences` không cháy**, tức đo
được thứ tự "rào đứng TRƯỚC codegen" bằng side-effect, không bằng đọc code.
- T6 `:436-440` và T7 `:496-498` — **đối chứng dương cùng lượt**, đổi đúng 1 biến thì QUA.
- T10 `:625-627` — đo **CHIỀU của `??=`**: viết `=` thì mọi assert khác vẫn xanh, chỉ vế này đỏ.
- T8 `:549-551` + `:555-562` — mối nối cầu → đường ống V2, đo bằng **hộp thư của đúng người**
(Cấp 1 thấy, Cấp 2 chưa thấy), không phải đọc cột.
**T12 vs vá-7 — ĐÚNG, đủ 3 vế spec đòi:** (a) phiếu TỰ đi tới `DaDuyet` qua service THẬT
(`:724-730`), không seed thẳng; (b) `ApprovedAmount` đông cứng `{250, 111}` với tương phản chứng
minh `??=` (dòng chốt tay giữ 111, **không bị 999 đè**) `:735-736`, và `GiaTri = 361` với lập luận
"Σ Proposed = 1249 ⇒ mọi cách sai đều ra số khác" `:744-745`; (c) sau bắc cầu, bấm duyệt lại →
`Conflict`, rồi đọc lại **cả `ContractId` lẫn `ApprovedAmount` đều không đổi** `:753-758`. ĐẠT.
**T11 (double-click) — CÓ RĂNG NHƯNG CHỈ CHO NỬA TUẦN TỰ.** Assert thật, 4 vế: Conflict + đúng
thông điệp + `Contracts.Count == 1` + `LastSeq == 1`. Nhưng `:652-655` là `await` lần 1 **rồi mới**
lần 2 (`ChangeTracker.Clear()` mô phỏng request thứ 2). Test này **không đo được và không thể đo**
ca ĐỒNG THỜI — tức F-1 vẫn hở. Bản thân comment test khai đúng phạm vi ("bấm 2 lần liên tiếp"),
nên đây KHÔNG phải lane nói quá; rủi ro là **người đọc sau tưởng "idempotency đã đóng"**.
**Ngoại lệ `CreateContract` trong `AuthorizePolicyRegressionTests` — SIẾT, không mở lỗ:**
- `:370-373` là `Dictionary<string,string>` map **1 action → 1 chuỗi policy CHÍNH XÁC**, dùng
`authz.Policy.Should().Be(expectedPolicy)` (`:419`) — so khớp tuyệt đối, **không** `StartsWith`
/ `Contains` / danh sách bỏ-qua.
- Action nào không có tên trong map vẫn rơi vào `else` đòi prefix `KeHoachKyKet` (`:428`) ⇒
**action tương lai KHÔNG được miễn trừ tự động**. Đúng yêu cầu "chỉ miễn đúng 1 action".
- `MenuKeys.All.Should().Contain(parts[0])` (`:434`) nằm NGOÀI `if/else` ⇒ vẫn chạy cho cả nhánh
ngoại lệ (chặn ca policy không tồn tại → 500).
- `:477-479` `KhkkCrossModuleGates.Keys.Should().BeSubsetOf(byName.Keys)` — chống **cấu-hình-chết**
(đổi tên/xoá action mà quên dọn map thì mục đó im lặng không ai đo). Đây là vế "vắng-mặt trông
giống ổn" mà tôi hay phải tự đòi; lane tự đặt sẵn.
---
## §7 — Trọng tài tươi (reviewer tự chạy, không nhận số của lane)
| Phép đo | Lệnh | Kết quả |
|---|---|---|
| Build BE | `dotnet build SolutionErp.slnx -v q` | **Build succeeded — 0 Warning, 0 Error** (62s) |
| 3 lớp test K7 | `dotnet test --filter …KhkkBridgeAuthzTests\|…CreateContractFromSigningPlanTests\|…AuthorizePolicyRegressionTests` | **39/39 passed, 0 failed** |
| Toàn suite | `dotnet test SolutionErp.slnx --no-build` | Domain **45/45** + Infrastructure **594/594** = **639 pass / 0 fail** |
| tsc fe-user | `npx tsc --noEmit` | exit 0, 0 dòng lỗi |
| tsc fe-admin | `npx tsc --noEmit` | exit 0, 0 dòng lỗi |
🔴 **Phép cộng khớp:** baseline trước đợt này là **622** (đo ở gate K4c cùng ngày) và diff thêm
**17** `[Fact]` ⇒ kỳ vọng 639. Đo được đúng **639**. Nghĩa là 17 test mới **có chạy thật** (không
bị filter/skip câm) **và** 0 test cũ bị vỡ. Đây là 1 con số đóng cả 2 câu hỏi.
### Prod probe — TIỀN ĐỀ SEEDER, ĐO ĐƯỢC, XÁC NHẬN
`GET /api/permissions/by-role/{id}` cho **cả 13 vai** trên `api.solutions.com.vn` (token admin;
lưu ý field token là `accessToken`, không phải `token` — sai tên là "login failed" giả):
```
roles on prod: 13
Accounting/AuthorizedSigner/CatalogManager/CostControl/DeptManager/Director/
Drafter/Equipment/Finance/HrAdmin/Procurement/ProjectManager → Contracts R=True C=False
Admin → Contracts R=True C=True
--- roles with Contracts.CanCreate = true: 1 / 13
```
Kết luận đo được (không suy từ code):
- Tiền đề của lane **ĐÚNG**: hôm nay **Drafter = False, Procurement = False** ⇒ nút bridge sẽ 403
cho họ nếu không có seeder. Lỗ là THẬT.
- **Sửa lại F-9 bằng số sống:** không phải "13/13 role", mà **12/13**; và Admin qua **không chỉ**
nhờ bypass — Admin **đã có sẵn row `CanCreate=True`** trên prod.
- **Củng cố F-2 bằng số sống:** vì Admin đã True, nhánh Admin của seeder mới là **no-op**.
- **Dự đoán kiểm chứng được sau deploy+restart:** seeder phải log đúng **`0 added + 2 upgraded`**
(Drafter, Procurement). Lệch con số này = có thứ khác đang lật quyền. Đây là acceptance rẻ nhất
cho nghiệm thu #75/#76 "restart THẬT rồi đếm row".
Giới hạn khai thật: prod **chưa deploy** diff này ⇒ phép đo trên chứng minh TIỀN ĐỀ, **không**
chứng minh seeder chạy đúng. Không đo được bằng con mắt người dùng thường vì
`nv.test@solutions.com.vn` đăng nhập thất bại (tài khoản không còn/đổi mật khẩu) — đã dùng đường
admin thay thế.
---
## §8 — Tổng hợp finding (CHỐT)
| # | Sev | Vị trí | Một câu |
|---|---|---|---|
| F-1 | MAJOR | `CreateContractFromSigningPlanFeatures.cs:128-129` + `:248` | Chống-trùng chỉ là kiểm-tra-trong-bộ-nhớ, không ràng buộc CSDL / không transaction bao ngoài ⇒ 2 request ĐỒNG THỜI tạo 2 HĐ, 1 cái mồ côi; T11 chỉ phủ ca tuần tự. |
| F-8 | MAJOR | `ContractSigningPlansController.cs:224` · `DbInitializer.cs:2743` · `KhkkBridgeAuthzTests.cs:153` | Mỏ-neo `DbInitializer.cs:2516` đúng lúc viết, **sai ngay lúc land** (dòng thật 2523 — chính diff này chèn 7 dòng ở `:2353-2359` đẩy nó xuống); **3 nơi**, không phải 2. |
| F-R1 | MINOR | `scripts/{distill-shard-probe,measure-agent-memory,memory-archive-gate}.ps1` | Dirty ngoài cả 13-file-set lẫn lead-set (việc rotate `_INDEX-<period>.md`); `git add -A` sẽ nuốt vào commit K7. |
| F-9 | MINOR | `DbInitializer.cs:2743-2744` | "403 với **13/13** role… chỉ Admin qua nhờ bypass" tự mâu thuẫn; đo prod: **12/13** có `CanCreate=False`, và Admin **đã có sẵn row True** (không chỉ bypass). |
| F-2 | MINOR | `DbInitializer.cs:2775` | Cấp row cho Admin vô hại nhưng **vô tác dụng** — có Admin-bypass (`MenuPermissionHandler.cs:26-31`) và prod đo được Admin đã `CanCreate=True`. |
| F-3 | MINOR | `DbInitializer.cs:2814` | "Thu hồi khi cần: xoá call ở SeedAsync" — call thật nằm trong `SeedAdminPermissionsAsync` (thân 2288-2381). |
| F-4 | MINOR | `DbInitializer.cs:2792` | Hàm nâng cả `CanRead` trong khi tên + comment chỉ hứa `Contracts.Create` (rộng hơn nhãn). |
| F-5 | MINOR | `KhkkDetailPage.tsx:527` · `:975` | `<a href>` thay `<Link>` (full reload, mất state SPA) + neo "Đã có HĐ" lồng trong `<label>` (interactive-lồng-interactive, cùng lớp a11y với finding K5). |
| F-6 | MINOR | `usePipelineStages.ts:324` | `khkkPlanContractIds` gọi trong `find` trong vòng lặp — O(n·m) + cấp phát mảng mỗi lượt; vô hại ở list kẹp 200. |
| F-7 | MINOR | `CreateContractFromSigningPlanFeatures.cs:186-207` | Không carry `DraftData` như bridge PE tiền lệ (`CreateContractFromEvaluationFeatures.cs:104`) dù `pe` đã nạp sẵn, và không có dòng nào nói đó là chủ đích. |
| CLARIFY-1 | — | `usePermission.ts` + `AuthContext.loadMenu` | Sau khi seeder chạy, nút vẫn xám cho tới khi user **đăng-xuất/đăng-nhập lại** (menu cache `localStorage`) — UAT phải nêu, không thì bị đọc thành "seeder không ăn". |
**Không có blocker.** F-1 và F-8 phải xử lý nhưng không chặn commit: F-1 xác suất thấp + không mất
dữ liệu (HĐ mồ côi xoá được), F-8 là con trỏ trong chú thích.
**ĐẠT (đo xong, không có finding):** hội-2-khoá đủ 2 tầng và FE không lỏng hơn server (0 đường
gọi API lách gate) · thứ tự seeder SAU revoker (2341 → 2361) và là seeder-quyền chạy CUỐI ·
upgrade-only + idempotent (0 chỗ hạ cờ, lượt 2 không `SaveChanges`) · chuỗi
`Line.ContractId` → `ContractIds` → `khkkPlanContractIds` → `Set` union null-safe từng khớp ·
mirror 3/3 byte-identical (SHA tự đo) · route `/contracts/:id` tồn tại ở CẢ 2 app · guard 4 vế
workflow ném tách từng nguyên nhân · vá-6 đủ 4 khoản (2 NotFound guard · codegen trước `Add` ·
`SlaDeadline` · pin `WorkflowDefinitionId`) · 17 test mới có răng thật · ngoại lệ regression siết
đúng 1 action + chống cấu-hình-chết · build 0/0 · 639/0 test · tsc sạch 2 app.
**KHAI-1 (test-lane: invest T1 ghi `WorkflowDefinitionId` null nhưng vá-6 thắng): XÁC NHẬN ĐÚNG —
ĐÓNG, không mở lại.** Spec `②:4` chốt "16 vá THẮNG khi xung đột với nền"; mã theo vá-6 (`:163-166`
+ `:201`); và T1 `:255-257` assert đúng bản active + có bản nhiễu để chống xanh-giả.
---
## §9 — Đề nghị hành động (mô tả tiêu chí, KHÔNG chỉ-dẫn sửa mã)
1. **F-8** — bỏ số dòng, trỏ bằng TÊN (`SeedAllRolesReviewReadPermissionsAsync` + token
`CanCreate = isPe` là duy nhất trong file, grep ra ngay), hoặc cập nhật cả **3** nơi thành 2523.
Tiêu chí nghiệm thu: sau khi vá, `grep -n "CanCreate = isPe"` trên cây ĐÃ COMMIT phải khớp con
số ghi trong chú thích.
2. **F-1** — hoặc dựng một phép kiểm chứng minh 2 lệnh gọi đồng thời chỉ sinh 1 HĐ, hoặc khai công
khai "chấp nhận rủi ro, chỉ chặn ở UI" ngay tại `:128-129` để người sau không đọc T11 thành
"đã đóng".
3. **F-9 / F-2** — sửa "13/13" thành "12/13" và bỏ vế "chỉ Admin qua nhờ bypass" (prod đo được
Admin có row thật).
4. **F-R1** — tách 3 file `scripts/*.ps1` khỏi commit K7, hoặc khai chúng vào lead-set trước khi
`git add`.
5. **Nghiệm thu sau deploy** — restart THẬT rồi soi log seeder: kỳ vọng **`0 added + 2 upgraded`**;
và UAT phải có bước đăng-xuất/đăng-nhập lại (CLARIFY-1).
END gate-k7 — VERDICT=PASS-WITH-FLAGS 10