diff --git a/.claude/governance/.session-counter.json b/.claude/governance/.session-counter.json index 5ada1fed..715bbb7b 100644 --- a/.claude/governance/.session-counter.json +++ b/.claude/governance/.session-counter.json @@ -11,9 +11,9 @@ }, "_tick_invariant_note": "Tick invariant (hub dede7ec5 Delta-1, verbatim): moi LAN-CHOT +1; mot cap dung-noi tang DUNG +1, khong +2, khong +0. SE form = tick-at-entry-gate idempotent-per-label (hub Delta-2 recovery-gate +1 = permitted form) - NET +1/session-label EQUIV hub +1/cap on CLOSED pairs; an OPEN pair is transiently +0 until its entry gate fires (hub blessed, 9a35405b block-1 phep dung-noi). Guard song-con = label-convention (session-start 2.1.8: new conversation = new S\u003cnn\u003e label, NEVER reuse). history[] is append-unbounded BY DESIGN =\u003e absence of a marker = never-happened (safe semantics); IF a FIFO cap is ever added, eviction MUST be handled explicitly (absence-vi-bi-day != absence-vi-chua-xay-ra - log-BOUNDED design-note dede7ec5).", "_seed_honesty": "Seeded UNTICKED on purpose. counter=0 and last_ticked_* = null mean \u0027no tick has ever happened\u0027, which is the truth at S121 - the ritual that performs the tick lands in W3. Seeding a fake first tick here would make the very first cadence reading a lie, and H24 exists to catch exactly that kind of invented number.", - "counter": 70, - "last_ticked_session": "S196", - "last_ticked_head": "e255c778215a90014533a9f33c19d09acc42e4bf", + "counter": 71, + "last_ticked_session": "S197", + "last_ticked_head": "a01147e9dfabda342abee48e34d920b1457ec064", "last_ticked_at": "2026-08-14", "last_audit": { "light_at_counter": 66, @@ -362,6 +362,11 @@ "at": "2026-08-14", "session": "S196", "event": "squash-benign (session-counter-tick.ps1 M2, contract fail_loud_on_regress trigger-2 BENIGN branch): counter 69->70, session S195->S196, head e470bdd->e255c77. last_ticked_head e470bdd object EXISTS (cat-file=commit) but NOT reachable (merge-base --is-ancestor exit!=0), counter not regressed => a closeout squash lifted the ticked wal:/session commit out of history (expected drift, not tamper). Trace appended, continue, no owner alarm. Written atomically (temp + Move-Item -Force)." + }, + { + "at": "2026-08-14", + "session": "S197", + "event": "CLEAN tick (session-counter-tick.ps1 M2): counter 70->71, session S196->S197, head e255c77->a01147e. Classify-before-tick: no regression (n=197 >= stored n=196); last_ticked_head reachable (merge-base --is-ancestor exit 0). 4 fields updated, 1 history entry appended, written atomically (temp + Move-Item -Force)." } ] } diff --git a/.claude/governance/reinject-ledger.md b/.claude/governance/reinject-ledger.md index aacbc0bf..004b1977 100644 --- a/.claude/governance/reinject-ledger.md +++ b/.claude/governance/reinject-ledger.md @@ -31,7 +31,9 @@ | item-id | source | detected | reinjected | attempt | remeasured | status | |---|---|---|---|---|---|---| -| _(chưa có floor-rot nào được reinject — ledger khởi-tạo trống S95, 2026-07-01)_ | | | | | | | +| CG-1 `front-end-reviewer-style` (floor-rot sub-class **clobber-rot** — mất H1 + LUẬT LÕI owner) | `.claude/agent-memory/front-end-reviewer-style/MEMORY.md` | S195 | S195 (UNION `e470bddf^`, commit `24566a50`) | 1 | **S197** (lead đo TƯƠI cùng lượt: `wc -c` = **5.913 B ≥ ngưỡng 5.800** + H1 dòng-1 còn; khớp `harness-eval` I1 bản PIN) | **resolved** | + +> 🧊 *Dòng trên = dòng THẬT đầu tiên của bảng (@S197, thi hành `harness-refine` A1). Placeholder cũ "(chưa có floor-rot nào được reinject — ledger khởi-tạo trống S95)" đứng ở đây S95→S197 và từ @S195 đã TỰ MÂU THUẪN với prose `:100` (attempt=1) — `harness-audit` N2 @S197 bắt; lật trong CÙNG lượt ghi để lượt đóng nợ không đẻ neo thối mới. CHỐT-DỪNG giữ nguyên: tick sau nếu file <5.800 B hoặc mất H1 ⇒ ESCALATE, CẤM attempt-2.* --- diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 8a70aa4c..3a0319f1 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -3,9 +3,11 @@ > **Tiering rule (S40):** giữ **2-3 session gần nhất**. Cũ hơn → `docs/changelog/sessions/`. Full brief history pre-S40 → `docs/_archive/HANDOFF-preS40-fullhistory.md`. **🆕 Last updated:** 2026-08-14 (**S194→S195, phiên-LOGIC L16 window 3-4** — GIỮ MỞ chờ anh test prod): **YC-032 SHIP PROD trọn 4 vế** — tách HĐ Thầu phụ thành `01A.` *(Nhân công & Vật tư)* / `01B.` *(Nhân công & Thiết bị)* + thân **document-view** cuộn từ trên xuống + **pre-fill 17 ô** (Duyệt NCC → KHKK → điền phần còn thiếu) + **khoá bản cứng 1 chiều** + print CSS. 2 commit: `88bdd054` (55 file, +13.223/−80) + `4ba2b16b` (vá gotcha **#93** — dấu PHẨY trong tên `.docx` ⇒ `MSB4186` làm CI #489 đỏ; local KHÔNG tài nào tái hiện vì SDK CI 10.0.202 ⟂ local 10.0.104). Deploy **#490 SUCCESS 9m28s**. **Prod verify 6 chân:** MIGCOUNT 73→**74** · cột `SealedAt`+`SealedByUserId`=2 · `sys.tables` giữ **97** · bundle đổi ×2 · smoke 3/3 200 · test CI **699**. **Acceptance treo ĐÓNG bằng đo DB prod:** `Ct_` nhóm 7→**9** · `Hdc_` leaf 7→**9** · nhãn `01A`/`01B`=6 · workflow V2 `HD-TP-VT`/`HD-TP-TB`=2 · template `IsActive`=2/2. Bundle eoffice = `index-BXWjJfDx.js` — 🔴 **nguồn = lead tự `curl https://eoffice.solutions.com.vn/` lúc 10:19 @S195**, KHÔNG phải artifact. *(Đính chính trong lượt, `lead-stale` FLAG-1 HIGH bắt: bản đầu ghi "nguồn SỐNG = `sub-cicd-monitor-2.md`" — **SAI quy công**: artifact đó `grep BXWjJfDx` = **0 hit**, control-dương cùng file `index-` = 2 hit và 2 hash nó thật sự chứa là `CR538vZx`/`C10ta-wB` = **cặp PRE-#488**. Artifact cũng **dừng ở CHÂN 2/6** — bảng 6-chân đầu file còn `⏳` cho chân 3/4/5/6/A ⇒ câu "prod verify 6 chân" ở trên **đọc theo return, không theo vật đã land**; 3 chân sau chưa có vật trên đĩa.)* Run-folder: `runs/2026-08-13-S194-yc032-hd-thauphu-2form/` (24 sub-artifact + spec 27.723 B). -> **🔴 CHỜ ANH — TEST PROD YC-032.** Việc đầu tiên đáng nhìn: sidebar đủ **9 nhóm HĐ + 9 mục Bảng cứng** (DB đã đo 9/9, mắt anh là nghiệm thu cuối). 🔴 **Cụm "anh nói ổn" nay kích 2 CỔNG KHÁC NHAU** (`ctx-verifier` cờ-1 @S195): `WAL next:` ⇒ quay lại 7 câu **(90)-(96)** + 3 món treo · segment L15 ngay dưới ⇒ mở **W8** + `[carry:w8-delete-policy]`. **Phải hỏi rõ anh nghiệm thu bài NÀO trước khi định tuyến** — `W8` nay **0 hit** trong `WAL` và `_context`, chỉ còn sống ở file này. +> **🔴 CHỜ ANH — TEST PROD YC-032.** Việc đầu tiên đáng nhìn: sidebar đủ **9 nhóm HĐ + 9 mục Bảng cứng** (DB đã đo 9/9, mắt anh là nghiệm thu cuối). 🔴 **Cụm "anh nói ổn" nay kích BA CỔNG KHÁC NHAU** *(cập nhật @S197 — `ctx-verifier` cờ-2: bản-đồ 2-cổng cũ @S195 đã thối trong đúng 1 cửa vì `WAL next:` bị ghi lại; con-trỏ tầng PATH sống mà tầng NỘI-DUNG mục)*: **(a)** "ổn rồi" cho GĐ3/prod-kiểm cũ (`:22`/`:23`/`:77` file này) ⇒ mở **W8** + `[carry:w8-delete-policy]` — `W8` vẫn **0 hit** WAL/`_context`, chỉ sống ở đây · **(b)** "ổn rồi" cho **YC-032** (segment đầu file này) ⇒ đóng nghiệm thu YC-032 rồi quay hàng đợi · **(c)** `WAL next:` hiện chở **hàng đợi 4 chặng** (① YC-032 ② 7 câu (90)-(96) ③ (103)(104)(105) ④ 2 nhãn). **Phải hỏi rõ anh nghiệm thu bài NÀO trước khi định tuyến.** > 🔴 **4 nợ owner CỐ Ý hoãn — KHAI, không phải quên:** VAT *(số đang CHƯA VAT mà thân HĐ ghi "bao gồm VAT")* · con-trỏ *"xem ĐIỀU 8/12"* trỏ chỗ trống *(`002.04` chỉ có 5 điều)* · authz upload bản cứng *(endpoint 0 policy / 0 ownership)* · 12 điều PHẦN 2. > 🔢 **CHỜ ANH MỚI @S195 — ĐÁNH SỐ** *(dựng nhà cho 2 câu sinh ở window-3; trước đó chúng chỉ sống ở `_mind §D` [nén được] + `WAL [!]` [tái sinh mỗi `/pause`] — `ctx-verifier` cờ-7, tái diễn đúng lớp lỗi đã đẻ ra khối (90)-(96))*: **(103) Trục style FE/BE của YC-032 CHƯA CÓ AI SOI** — `front-end-reviewer-style` chết ở khung rỗng 565 B, `back-end-reviewer-style` 0 file; câu bỏ ngỏ: `ContractBodyView.tsx` 42 KB một file · `ThauPhuFamily` ở Application ⟂ `ContractApprovalWorkflowFamily` ở Domain. Anh chọn **(a)** spawn lại ở bookend kế · **(b)** chấp nhận nợ, khai thẳng chưa phủ · **(c)** lead tự đo rồi khai là tự-chấm → đề xuất **(a)**. · **(104) Siết `global.json` `rollForward`** đóng lớp SDK-drift: **(a)** ĐO SDK trên runner TRƯỚC rồi pin đúng dải · **(b)** giữ `latestFeature`, sống chung · **(c)** pin cứng ngay → đề xuất **(a)**, vì pin sai dải thì CI **chết hẳn** thay vì đỏ một run. +> 🔢 **CHỜ ANH MỚI @S197** *(cấp SỐ cho ý owner-gated đang vô-danh — `ctx-verifier` cờ-9: án S195 "cấm đề ③/④ lần nữa, escalate cơ-chế" đã bị phạm ngay cửa S196 [nén `harvest-curator` theo MỨC cho bệnh TỐC-ĐỘ] và ý này là ý duy nhất cùng lớp (103)(104) mà KHÔNG có số nên rơi qua 3 bề mặt)*: **(105) Escalate CƠ-CHẾ trần bộ-nhớ cho vai tái-vượt** — 2 vai (`lead-gap-auditor` · `ring2-audit`) vượt trần ở **CẢ HAI đơn-vị** byte/ký-tự, đã ăn thuốc nén ③ rồi vượt lại ⇒ nén tiếp là vô hạn. Anh chọn: **(a)** đổi số cap (quyền anh — cap = owner) · **(b)** đổi quy-ước ghi của vai (tách tầng / hạn tốc-độ ghi — lead thi hành) · **(c)** dựng auto-drain tooling theo value-gate → đề xuất **(b)**, và nên phán **SAU** khi trả câu (94) byte⟂ký-tự vì chọn đơn-vị đổi cả tập vai bị nén. +> 🔢 **CHỜ ANH MỚI @S197 (2/2)** *(sinh từ `YC-039` — anh nói "tạo hợp đồng thì PHẢI bắt từ KHKK"; đo hiện trạng: đường bridge KHKK→HĐ ĐÃ là đường chuẩn [K7 hội-2-khóa, write-site duy nhất] + pre-fill 3 tầng NCC→KHKK→HĐ đã ship #490, NHƯNG **đường tạo HĐ TỰ DO còn MỞ**: `ContractsController:49 POST /contracts` + 3 lối UI [menu Create per-loại `Layout:178` · nút Tạo mới `MyContractsPage:156` · Dashboard `:1106`] — HĐ đi đường này **0 liên kết** ngược KHKK/NCC, chuỗi đứt tại chỗ tạo)*: **(106) Siết đường tạo HĐ tự do?** — **(a)** CHẶN tạo tự do cho 5 loại thi-công/mua-bán (TP·GK·NCC·DV·MB — bắt buộc từ KHKK), GIỮ tự do cho 2 loại Nguyên tắc (NT-NCC·NT-DV thường ký khung TRƯỚC khi có PE/KHKK) · **(b)** chặn HẾT 7 loại — mọi HĐ phải từ KHKK · **(c)** giữ nguyên 2 đường, gắn nhãn "HĐ ngoài chuỗi" cho bản tạo tự do → đề xuất **(a)** — khớp chữ "phải bắt từ KHKK" mà không khoá nhầm ca nghiệp-vụ HĐ nguyên tắc. > > 🔺 **DI-TRÚ KHỎI WAL @S195 — `lead-gap-auditor` deep 7 FLAG, 4 cờ hội tụ về CÙNG một nguyên nhân:** mọi mục dưới đây trước lượt này **chỉ sống ở `.claude/WAL.md`**, mà `§6.4` **xoá trắng WAL ở bước CUỐI closeout** ⇒ đóng phiên là mất. Ghi TAY ở đây vì nghi-thức RE-STAMP **chép danh sách kỳ trước** nên slug sinh-trong-WAL **vô hình có cấu trúc**. > - 🔴 **3 slug carry mồ côi (FLAG-1 HIGH, im 4 nhãn phiên, 3 lượt audit liên tiếp đã chỉ đích danh mà vẫn 0 hit):** `[carry:mind-dinh-chinh-mot-be-mat]` · `[carry:du-cua-rule-khong-phai-phep-kiem]` · `[carry:session-end-thieu-buoc-ghi-block]`. Control-dương cùng lệnh: `[carry:w5-host-ondeleted]`=1 hit · `[carry:reset-doi-het]`=2 hit ⇒ thước sống, 0 là 0 thật. **Bài học kèm:** 2/3 slug này nằm ĐÚNG trong `runs/2026-08-13-S191-bookend-close/sub-ctx-audit-close.md:16` — nguồn mà khối RE-STAMP @S191 **tuyên là đã quét** ⇒ nghi-thức "dựng-từ-nguồn" **không bắt được slug sinh trong chính run-folder của nó**. diff --git a/docs/STATUS.md b/docs/STATUS.md index 8744d1ec..9b92f892 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -479,7 +479,7 @@ monthly re-tier mega-line + ef-core SKILL Mig→59 + 3 vocab-fork (2026-07-01 du | FE pages | **68** | unchanged S54 (ItTicketsPage reassign = in-place 2 app); +1 S52 AttendanceReportPage | | Policies | **488** | 🔄 **488 @S194 G1** = All 122 × 4 (DELTA: **+8** = 2 `AwV2_Hd*` × 4 actions — 2 biến-thể Thầu phụ `HD-TP-VT`/`HD-TP-TB`, YC-032; by-construction từ `ContractApprovalWorkflowFamily.Members`. Neo test = `KhkkGroupMenuSeedTests` `HaveCount(122)`). 🧊 **480 @S187 W3** = All 120 × 4 (DELTA: **+28** = 7 `AwV2_Hd*` × 4 actions — 7 quy trình duyệt HĐ theo LOẠI, YC-023/YC-024). Đo RUNTIME, không suy từ số cũ: nạp `SolutionErp.Domain.dll` vừa build → `MenuKeys.All.Length × MenuKeys.Actions.Length` = 120 × 4 (`Distinct()` = 120 ⇒ 0 key trùng). 🧊 **452 @S166 K4a** = All 113 × 4 (máy khoá: test T2 `KhkkGroupMenuSeedTests` đỏ nếu drift; DELTA vá-2: +4 ContractCatalog K1 · +32 AwV2_KhkkN* K3 · +196 Khkk_G* K4a). Cũ: **220 @S161** (W1 KHKK: `All` +`KeHoachKyKet` ⇒ 55 × 4 — reviewer F-2 bắt cặp-row-đôi đúng luật dòng dưới) · 🆕 **row MỚI S121** (wave adap-6-broadcast W2 — H24 GAP-3: `governance-detectors.ps1` `Get-StatusValue` resolve canonical TỪ bảng này; **KHÔNG có row ⇒ token `policy` UNRESOLVED ⇒ nửa `policy` của GAP-3 không thể fire** ⇒ detector khai FAIL-LOUD + SKIP, KHÔNG im-lặng-giả-vờ-xanh). 🔴 **DERIVED, KHÔNG độc-lập:** `Policies = |MenuKeys.All| × |MenuKeys.Actions| = 55 × 4 = 220 (@S161; trước W1 = 54 × 4 = 216)` — sinh runtime ở `src/Backend/SolutionErp.Api/Program.cs:82-89` (`foreach menu → foreach action → opts.AddPolicy($"{menu}.{action}")`), `Actions = ["Read","Create","Update","Delete"]` (`MenuKeys.cs:167`). ⇒ **row này BUỘC đổi CÙNG row `Menu keys`**: thêm 1 menu key ⇒ 55 × 4 = 220; sửa `Menu keys` mà quên `Policies` ⇒ detector flag derived-doc thay vì flag chính chỗ sai. Ground S121 bằng đếm mã-nguồn (`MenuKeys.All` = 54 entry, `public const string` = 54, khớp) — KHÔNG chép số từ spec. ⚠️ `.claude/skills/permission-matrix/SKILL.md` ghi **~240 = STALE** ⇒ W4 vá (🔴 chuỗi H24 §2(4) BẮT BUỘC: detector fire → cổng chứng-kiến → RỒI MỚI vá; vá trước = giết chứng-nhân sống) | | Menu keys | **122** | 🔄 **122 @S194 G1** (+2 `AwV2_HdTPVT`/`AwV2_HdTPTB`; menu-row `Ct_*`/`Hdc_*` của 2 loại mới sinh động qua factory nên CỐ Ý ngoài `All` — xem `MenuKeys.cs:69`). 🧊 **120 @S187 W3** (đo RUNTIME `MenuKeys.All` bằng cách nạp DLL vừa build, KHÔNG đếm mắt: `ALL=120 · DISTINCT=120 · HD_KEYS=7`). DELTA **+7** = `AwV2_Hd{TP,GK,NCC,DV,MB,NTNCC,NTDV}` — 7 leaf Designer cho 7 quy trình duyệt HĐ theo LOẠI (YC-023/YC-024), sinh by-construction từ `ContractApprovalWorkflowFamily.Members` qua `MenuKeys.ContractWorkflowMenuKeys` ⇒ CÙNG bộ sinh với vòng seed menu-row trong `SeedMenuTreeAsync` (invariant T6: key vào `All` mà thiếu menu-row = Permission FK nổ lần boot đầu). 🧊 **113 @S166 K4a** (đo RUNTIME `MenuKeys.All` — test T2; +49 `Khkk_G{2-8}`+42-leaf bộ-sinh by-construction K4a · +8 `AwV2_KhkkN1..N8` K3 · +1 `ContractCatalog` K1 trên nền 55). Cũ: **+1 S161 `KeHoachKyKet` vào `All` (W1 KHKK — policy `KeHoachKyKet.*` sinh runtime; menu-row prod KHÔNG đổi, 142 giữ)** · +1 S69 `Off_Dashboard` (landing Văn phòng số). Prev re-ground S61 (`MenuKeys.cs` const — Mig 50 gỡ 4 `Bg_*` Budget menu cũ). Prev 57 (S58) | -| Tests | **699 PASS** | **45 Domain + 654 Infra** · 🔄 **699 @S194 — cicd-verify CI run #490** (deploy YC-032; Δ+2 trọn ở Infra; 0 Failed / 0 Skipped — nguồn `runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-cicd-monitor-2.md` — **`:124` = `Passed: 45` Domain · `:127` = `Passed: 654` Infra**; 🔴 neo bản đầu cho `:127` gánh CẢ CẶP là LỆCH 3 dòng cho nửa vế, `ring1` L-5 bắt). 🧊 **697 @S190 — cicd-verify CI run #600 `091073f9`** (deploy YC-029+YC-030 GĐ3; Δ+17 trọn ở Infra = YC-029 +13 [W1 menu-drift + W5 delete-guard 13-ca] + YC-030 +4 [`ContractApproveFinalizeTests` nhánh finalize]; 0 Failed/0 Skipped, đối chứng local `dotnet test SolutionErp.slnx` @gate-cụm). 🧊 **680 @S188 — ĐO ×2 nguồn độc-lập** (`dotnet test SolutionErp.slnx` local @gate-cụm + gate CI **run #482** `b275893c` tách-phần 45/635; đối chứng thứ 3 `sub-reviewer-cum.md`). **+12 vs S185** = 4 test-before W1 (`UpdateAwDefinitionTests` W1a/W1b bom per-type) + T2/T6 menu-key drift W3 + 3 security W6-BE (`ContractWorkflowServiceApproveV2Tests` đúng-trạm) + collateral. 🔴 **Ô này lệch 2 đời trong phiên S188** (668 → 672 đợt-1 → 680 cụm) — `lead-stale-auditor` DEEP FLAG-2 bắt, class `view-stale-count`, cùng ô đã 4 lần tái phát ⇒ **nguồn cập nhật = cicd-verify lượt kế, KHÔNG chép tay**. 🧊 **668 @S185 — ĐO BẰNG CÁCH CHẠY THẬT ×2 nguồn độc-lập** (`dotnet test SolutionErp.slnx` @/tiep-verify + `ring2-audit` tự chạy đối-chứng cùng ngày): Domain 45 · Infra 623 · 0 fail 0 skip. Lineage: +15 KHKK-mirror @S182 (14 spec + T15) + +8 `UploadDuyet_*/UploadCanCu_*` YC-016 @S183. 🧊 **645 @S179** (45D+600I — ô này từng kẹt 645 tới S185 trong khi suite đã 668: stale-F3 canonical-poison, máy lấy ô này làm thước in 6 flag khuyên vá về số SAI — vá @S185). 🔴 Không lấy số từ doc: `lead-stale-auditor` FLAG-4 bắt ô này còn **644**, mà 644 đang là **THƯỚC** cho 6 flag derived-stale của detector ⇒ canonical sai thì máy khuyên vá về số sai (class canonical-poison S148/S166). 🔸 Breakdown cũ ghi "599 Infra" cũng lệch — **600**. 🧊 **644 @S168 #444** (+5 Fact Lines-editor) · 🧊 **639 @S168-sáng** (đo tươi local 45D+594I sau K7 Δ+17; chuỗi K lineage: 597 K1 → 614 K3 → 622 K4c @S167 KHỚP CI #439 tách-phần → **639** K7 — gate `639=45+594` KHỚP cicd-K7 dòng-1) · 🧊 **+28 @S161** (5 wave KHKK: W1 566 → W2 574 → W3 580 → W6 587 → W7 590; đo tươi @S162 `dotnet test` 45+545, gate CI Run #432 khớp). ~~45 Domain + 517 Infra~~ · 0 fail / 0 skip · **S155 +30** (PE xóa-by-approver: `PeDeleteByApproverTests.cs` + T26/T27 — 🔴 **T27 khoá CẢ LỚP lỗi** *entity-có-cờ-mà-DTO-không-có* bằng reflection [mọi cờ `Allow*` trên entity phải có mặt trên DTO], **fault-inject xác nhận có răng**: tiêm bug ⇒ ĐỎ, khôi phục ⇒ xanh — không phải guard tuyên-bố suông; T26 giữ ĐỎ có chủ ý vì bug production thật, lead vá bằng đảo thứ tự chứ KHÔNG nới assert. Số 562 cross-check 2 nguồn độc-lập: log CI run #417 + `dotnet test` local @S156) · **S147 +4** (`ProjectCodeNormalizationTests` — Trim khi lưu + kiểm-trùng bỏ-dấu-cách; 🔴 **ô canonical này STALE 528/483I suốt S147→S148**, header `:6` và session-log S147 đều ghi 532/487I từ đầu — closeout S147 vá header mà quên ô canonical; H24 `lead-view-auditor` bắt @S148, lead verify 3-nguồn rồi vá) · **S134 +8** (`PePendingAccumulationTests` C1-C8 lũy-kế-TẠM-TÍNH: C1 ChoDuyet+winner vào delta-selected + PriorPes · C2 TuChoi/DaDuyet loại sạch · C3 Nháp list-only KHÔNG cộng tiền [D1] + TraLai vào delta-submitted [D2] · C4 peer-window CreatedAt · C5 delta âm signed · **C6 complementary-no-double-count exact-numbers [ComputeAsync bất động khi thêm peer non-terminal]** · C7 frozen→pending 0/null · C8 D4 changelog `[S134 pending-prior]` positive+contrast) · **S133 +11** (`PeBudgetFreezeTests` freeze-NS-at-DaDuyet: **T0 repro test-before RED→GREEN** [phiếu DaDuyet phải giữ số cũ sau `UpdatePeBudgetPro` — RED bắt 999tr @:184 pre-fix, phiếu đưa tới DaDuyet QUA service finalize mirror `PeApproverFinalizeTests` KHÔNG seed thẳng Phase] + **T1-T9** spec-coverage [snapshot các nhánh finalize + display-gate + 2 terminal-guard chặn sửa NS phiếu đã kết thúc (cả Admin) + backfill] + **site-5 AdminOverride** [reviewer catch — override thẳng → DaDuyet cũng chốt snapshot]) · **S118 +8** (PRO-perm scope-isolation **+4** `ProcurementMasterAccessSeedTests` [reflection no-prod-touch: upgrade-path Suppliers R+C+U + Master-Read · **scope-isolation** DeptManager-riêng vẫn revoke=false · insert-path no-Delete · idempotent] · ReportsController authz **+4** `AuthorizePolicyRegressionTests` [dashboard/export require Reports.Read · my-dashboard KHÔNG action-policy · class-level [Authorize] intact]) · **S117 +6** (PE endsBeforeCeo pointer-aware **+1** `PeFinalizeProjectionTests.ListPe_EndsBeforeCeo_ChoDuyet_PointerAware_PastFinalizeIsFalse` [ChoDuyet con-trỏ QUA cấp finalize → endsBeforeCeo=false, test-before RED→GREEN + EF-subquery translate smoke] · duyệt-theo-khoảng ProMinMax **+5** `PeApprovedPriceFinalizeTests` [happy set Min+Max · min==max boundary lock strict-`>` · thiếu-Max→Conflict not-finalized · min>max→Conflict · single-source+Max→Conflict wire-bug + regression Max=null] · harness `InvokeApply` 4→5 args fix TargetParameterCountException) · **S116 +9** (PE nhập giá âm: T1 ngân-sách-bất-biến khi quote-âm · T2 winnerQuoteTotal signed-sum · T3 submit-guard all-zero-blocked/pure-neg+net-zero-submittable · T4 CCM-net-threshold) · **S114 +9** (PE multi-NCC per hạng mục: `PeSelectWinnerClearTests` delete-invariant +4 [DeleteQuote/DeleteDetail re-derive IsWinner **test-before RED→GREEN** + guard-no-over-clear + UpsertQuote-update-branch coverage] + impl-lane +5 [per-hạng-mục-total · giaTri-single-partial · threshold-crossing · derive-invariant]) · **S113 +19** (Supplier import v2: `SupplierImportV2DedupTests` 13 [dedup MST-primary+Code-backstop T1-T7 · draft-on-import IsPublic · all-or-nothing · LayoutValid re-bake real-token · **null-Code→nháp FIXED** flipped-from-characterization] + `SupplierPublishAndListTests` 6 [publish-guard Code-required→409 · unpublish · list-filter published · DTO IsPublic]) · **S112 +18** (Supplier `SupplierExcelImportServiceTests` +10 [case-collation dedup có-RĂNG SQLite-BINARY-collation-nên-chứng-dedup-ở-SERVICE · fill-nulls-safe · all-or-nothing-abort · parser abs-cell-index/#REF!-null/header-fingerprint · Type-lạ→NCC · Status-emoji-strip] · PE `PeSubmitGuardAndBypassTests` 4-update→A1 [minOwn subordinate-only, pointer-stop] + 8-new [A1-happy · non-contiguous · EDGE-5 non-admin-throw/admin-pass/system-pass · opinion-history log/none]) · Prev 440: **S96 +6** (PE `PeApproverFinalizeTests` +2 [#6 finalize-opt-out→advance-CEO · #7 stale-noti-clear ExecuteUpdate] · `PeSuggestedPriceSetterAuthzTests` +4 [price-lock phase-guard after-submit — PRO/CCM ChoDuyet/DaDuyet→Conflict, Admin-override; +sửa SeedPeAsync default ChoDuyet→DangSoanThao spec-change]) · **S92 +3** (`AdminOnlyModulesRevokeTests` ẩn 5 nhóm menu chỉ Admin — non-Admin revoked-4-cờ / Admin untouched / Pe_* preserved) · **S91 +9** (ChangePassword self-service BE+test) · **S89 +3** (`PeDraftVisibilityTests` IDOR nháp riêng-tư) · **S88 +17** (D1 financials/CEO-notify/HoSoLink null-safe +11 · D2 create-contract multi-winner +6) · **S87 +25** (Mig 58 net +18: `PeMultiWinnerTests` sum + `PeSelectWinnerClearTests` multi-update + `PeSubmitGuardAndBypass` budget=0 + `PeApproverFinalizeTests` 5 auto-finalize [từng 7, −2 khi opt-in→auto rework] + `PeCcmThresholdFinalize`/`PeWorkItemBudget` collateral; Mig 59 +7 `PeCcmBudgetPeriodSetterAuthzTests` role-gate Admin|CCM/budget=0/null-clear) · **S86 +3** (`PeApproverNotifyExcludesDrafterTests` — notify "cần duyệt" loại NGƯỜI TẠO phiếu: drafter-excluded · co-approver-vẫn-nhận · drafter-vẫn-nhận-chuông-kết-quả) · **S85 +18** (PE CV PRO C5 hủy/đổi NCC winner +10 · urgent drafter-self-unset +5 · workflow-preserve bug-class S42 +3 `PeUpdateDraftWorkflowPreserveTests`) · **S83 +2** (PE require-workflow: `PeWorkItemGuardTests` ApprovalWorkflowId `NotEmpty` ×2 validator + submit-guard rewrite test 7/13 `PeSubmitGuardAndBypass` V1-submit-deprecated, no-count-change) · **S77 +10** (+7 `PeSuggestedPriceSetterAuthzTests` ghi chú PRO/CCM Mig 57 absolute-set/role-gate/null-clear/max-1000 · +3 `PeUrgentToggleAuthzTests` rewrite cờ-gấp **BẤT ĐỐI XỨNG** SET=function role / UNSET=function+DeptManager, plain-PRO-unset Forbidden) · **S76 +5** (`PeWorkItemBudgetTests` PRO split: set ProInitial+ProAdjust gồm âm · validator ProInitial≥0/ProAdjust-cho-âm · full=proFull khi CCM empty · full=CCM khi CCM present) · **S74 +5** (`PeWorkItemBudgetTests` §4b CcmNote: set CCM/Admin · null-clear absolute-set · non-priv Forbidden+no-mutate · all-3-persist) · **S73 +28** (PE opt-in finalize spec 6→11 + 10 giá-chốt `PeApprovedPriceFinalizeTests` + 13 setter-authz `PeSuggestedPriceSetterAuthzTests`) · **S69 +20** (Office-golive OfficeModulePermissionSeed 6 + PE PeCcmThresholdFinalize 5 + PeUrgentToggleAuthz 9) · **S67 +23 HRM test-after** (DepartmentTreeTests 8 cycle-guard/rollup + PeHoSoLinkTests 9 absolute-set + HrmProfilePermissionSeedTests 6 reflection-seed) → 286 · S61 +22 `PeWorkItemBudgetTests` −14 `BudgetPolicyTests` −1 → 263 · S60 +14 `PeSubmitGuardAndBypassTests` +2 spec → 256 (Domain 58→45 do drop Budget module tests) | +| Tests | **697 PASS + 6 SKIP** | **45 Domain + 652 Infra pass · 6 Skip có-khai (total 703)** · 🔄 **697+6skip @S197 — YC-040 (106)(b) gate KHKK-only** (đo tươi local `dotnet test SolutionErp.slnx`: 45D + 652I pass / **6 Skip** / 0 Fail; Δ so 699: **+4 test gate MỚI** [`ContractCreationGateTests` 3 (quét-trọn-enum fail-closed + 2 InlineData) + `Create_GateDong_ChanTruocMoiLoad` 1] **− 6 chuyển Skip** [`CreateContractFromEvaluationMultiWinnerTests` — spec di sản đường PE→HĐ Mig 58/S114, Skip-reason trỏ YC-040, mở lại khi owner nới `AllowedDirectTypes`] ⇒ 🔴 **6 Skip là CHỦ ĐÍCH có khai, KHÔNG phải test rụng**; CI kế tiếp phải ra CÙNG cặp 697/6 — lệch là ĐỎ). 🧊 **699 @S194 — cicd-verify CI run #490** (deploy YC-032; Δ+2 trọn ở Infra; 0 Failed / 0 Skipped — nguồn `runs/2026-08-13-S194-yc032-hd-thauphu-2form/sub-cicd-monitor-2.md` — **`:124` = `Passed: 45` Domain · `:127` = `Passed: 654` Infra**; 🔴 neo bản đầu cho `:127` gánh CẢ CẶP là LỆCH 3 dòng cho nửa vế, `ring1` L-5 bắt). 🧊 **697 @S190 — cicd-verify CI run #600 `091073f9`** (deploy YC-029+YC-030 GĐ3; Δ+17 trọn ở Infra = YC-029 +13 [W1 menu-drift + W5 delete-guard 13-ca] + YC-030 +4 [`ContractApproveFinalizeTests` nhánh finalize]; 0 Failed/0 Skipped, đối chứng local `dotnet test SolutionErp.slnx` @gate-cụm). 🧊 **680 @S188 — ĐO ×2 nguồn độc-lập** (`dotnet test SolutionErp.slnx` local @gate-cụm + gate CI **run #482** `b275893c` tách-phần 45/635; đối chứng thứ 3 `sub-reviewer-cum.md`). **+12 vs S185** = 4 test-before W1 (`UpdateAwDefinitionTests` W1a/W1b bom per-type) + T2/T6 menu-key drift W3 + 3 security W6-BE (`ContractWorkflowServiceApproveV2Tests` đúng-trạm) + collateral. 🔴 **Ô này lệch 2 đời trong phiên S188** (668 → 672 đợt-1 → 680 cụm) — `lead-stale-auditor` DEEP FLAG-2 bắt, class `view-stale-count`, cùng ô đã 4 lần tái phát ⇒ **nguồn cập nhật = cicd-verify lượt kế, KHÔNG chép tay**. 🧊 **668 @S185 — ĐO BẰNG CÁCH CHẠY THẬT ×2 nguồn độc-lập** (`dotnet test SolutionErp.slnx` @/tiep-verify + `ring2-audit` tự chạy đối-chứng cùng ngày): Domain 45 · Infra 623 · 0 fail 0 skip. Lineage: +15 KHKK-mirror @S182 (14 spec + T15) + +8 `UploadDuyet_*/UploadCanCu_*` YC-016 @S183. 🧊 **645 @S179** (45D+600I — ô này từng kẹt 645 tới S185 trong khi suite đã 668: stale-F3 canonical-poison, máy lấy ô này làm thước in 6 flag khuyên vá về số SAI — vá @S185). 🔴 Không lấy số từ doc: `lead-stale-auditor` FLAG-4 bắt ô này còn **644**, mà 644 đang là **THƯỚC** cho 6 flag derived-stale của detector ⇒ canonical sai thì máy khuyên vá về số sai (class canonical-poison S148/S166). 🔸 Breakdown cũ ghi "599 Infra" cũng lệch — **600**. 🧊 **644 @S168 #444** (+5 Fact Lines-editor) · 🧊 **639 @S168-sáng** (đo tươi local 45D+594I sau K7 Δ+17; chuỗi K lineage: 597 K1 → 614 K3 → 622 K4c @S167 KHỚP CI #439 tách-phần → **639** K7 — gate `639=45+594` KHỚP cicd-K7 dòng-1) · 🧊 **+28 @S161** (5 wave KHKK: W1 566 → W2 574 → W3 580 → W6 587 → W7 590; đo tươi @S162 `dotnet test` 45+545, gate CI Run #432 khớp). ~~45 Domain + 517 Infra~~ · 0 fail / 0 skip · **S155 +30** (PE xóa-by-approver: `PeDeleteByApproverTests.cs` + T26/T27 — 🔴 **T27 khoá CẢ LỚP lỗi** *entity-có-cờ-mà-DTO-không-có* bằng reflection [mọi cờ `Allow*` trên entity phải có mặt trên DTO], **fault-inject xác nhận có răng**: tiêm bug ⇒ ĐỎ, khôi phục ⇒ xanh — không phải guard tuyên-bố suông; T26 giữ ĐỎ có chủ ý vì bug production thật, lead vá bằng đảo thứ tự chứ KHÔNG nới assert. Số 562 cross-check 2 nguồn độc-lập: log CI run #417 + `dotnet test` local @S156) · **S147 +4** (`ProjectCodeNormalizationTests` — Trim khi lưu + kiểm-trùng bỏ-dấu-cách; 🔴 **ô canonical này STALE 528/483I suốt S147→S148**, header `:6` và session-log S147 đều ghi 532/487I từ đầu — closeout S147 vá header mà quên ô canonical; H24 `lead-view-auditor` bắt @S148, lead verify 3-nguồn rồi vá) · **S134 +8** (`PePendingAccumulationTests` C1-C8 lũy-kế-TẠM-TÍNH: C1 ChoDuyet+winner vào delta-selected + PriorPes · C2 TuChoi/DaDuyet loại sạch · C3 Nháp list-only KHÔNG cộng tiền [D1] + TraLai vào delta-submitted [D2] · C4 peer-window CreatedAt · C5 delta âm signed · **C6 complementary-no-double-count exact-numbers [ComputeAsync bất động khi thêm peer non-terminal]** · C7 frozen→pending 0/null · C8 D4 changelog `[S134 pending-prior]` positive+contrast) · **S133 +11** (`PeBudgetFreezeTests` freeze-NS-at-DaDuyet: **T0 repro test-before RED→GREEN** [phiếu DaDuyet phải giữ số cũ sau `UpdatePeBudgetPro` — RED bắt 999tr @:184 pre-fix, phiếu đưa tới DaDuyet QUA service finalize mirror `PeApproverFinalizeTests` KHÔNG seed thẳng Phase] + **T1-T9** spec-coverage [snapshot các nhánh finalize + display-gate + 2 terminal-guard chặn sửa NS phiếu đã kết thúc (cả Admin) + backfill] + **site-5 AdminOverride** [reviewer catch — override thẳng → DaDuyet cũng chốt snapshot]) · **S118 +8** (PRO-perm scope-isolation **+4** `ProcurementMasterAccessSeedTests` [reflection no-prod-touch: upgrade-path Suppliers R+C+U + Master-Read · **scope-isolation** DeptManager-riêng vẫn revoke=false · insert-path no-Delete · idempotent] · ReportsController authz **+4** `AuthorizePolicyRegressionTests` [dashboard/export require Reports.Read · my-dashboard KHÔNG action-policy · class-level [Authorize] intact]) · **S117 +6** (PE endsBeforeCeo pointer-aware **+1** `PeFinalizeProjectionTests.ListPe_EndsBeforeCeo_ChoDuyet_PointerAware_PastFinalizeIsFalse` [ChoDuyet con-trỏ QUA cấp finalize → endsBeforeCeo=false, test-before RED→GREEN + EF-subquery translate smoke] · duyệt-theo-khoảng ProMinMax **+5** `PeApprovedPriceFinalizeTests` [happy set Min+Max · min==max boundary lock strict-`>` · thiếu-Max→Conflict not-finalized · min>max→Conflict · single-source+Max→Conflict wire-bug + regression Max=null] · harness `InvokeApply` 4→5 args fix TargetParameterCountException) · **S116 +9** (PE nhập giá âm: T1 ngân-sách-bất-biến khi quote-âm · T2 winnerQuoteTotal signed-sum · T3 submit-guard all-zero-blocked/pure-neg+net-zero-submittable · T4 CCM-net-threshold) · **S114 +9** (PE multi-NCC per hạng mục: `PeSelectWinnerClearTests` delete-invariant +4 [DeleteQuote/DeleteDetail re-derive IsWinner **test-before RED→GREEN** + guard-no-over-clear + UpsertQuote-update-branch coverage] + impl-lane +5 [per-hạng-mục-total · giaTri-single-partial · threshold-crossing · derive-invariant]) · **S113 +19** (Supplier import v2: `SupplierImportV2DedupTests` 13 [dedup MST-primary+Code-backstop T1-T7 · draft-on-import IsPublic · all-or-nothing · LayoutValid re-bake real-token · **null-Code→nháp FIXED** flipped-from-characterization] + `SupplierPublishAndListTests` 6 [publish-guard Code-required→409 · unpublish · list-filter published · DTO IsPublic]) · **S112 +18** (Supplier `SupplierExcelImportServiceTests` +10 [case-collation dedup có-RĂNG SQLite-BINARY-collation-nên-chứng-dedup-ở-SERVICE · fill-nulls-safe · all-or-nothing-abort · parser abs-cell-index/#REF!-null/header-fingerprint · Type-lạ→NCC · Status-emoji-strip] · PE `PeSubmitGuardAndBypassTests` 4-update→A1 [minOwn subordinate-only, pointer-stop] + 8-new [A1-happy · non-contiguous · EDGE-5 non-admin-throw/admin-pass/system-pass · opinion-history log/none]) · Prev 440: **S96 +6** (PE `PeApproverFinalizeTests` +2 [#6 finalize-opt-out→advance-CEO · #7 stale-noti-clear ExecuteUpdate] · `PeSuggestedPriceSetterAuthzTests` +4 [price-lock phase-guard after-submit — PRO/CCM ChoDuyet/DaDuyet→Conflict, Admin-override; +sửa SeedPeAsync default ChoDuyet→DangSoanThao spec-change]) · **S92 +3** (`AdminOnlyModulesRevokeTests` ẩn 5 nhóm menu chỉ Admin — non-Admin revoked-4-cờ / Admin untouched / Pe_* preserved) · **S91 +9** (ChangePassword self-service BE+test) · **S89 +3** (`PeDraftVisibilityTests` IDOR nháp riêng-tư) · **S88 +17** (D1 financials/CEO-notify/HoSoLink null-safe +11 · D2 create-contract multi-winner +6) · **S87 +25** (Mig 58 net +18: `PeMultiWinnerTests` sum + `PeSelectWinnerClearTests` multi-update + `PeSubmitGuardAndBypass` budget=0 + `PeApproverFinalizeTests` 5 auto-finalize [từng 7, −2 khi opt-in→auto rework] + `PeCcmThresholdFinalize`/`PeWorkItemBudget` collateral; Mig 59 +7 `PeCcmBudgetPeriodSetterAuthzTests` role-gate Admin|CCM/budget=0/null-clear) · **S86 +3** (`PeApproverNotifyExcludesDrafterTests` — notify "cần duyệt" loại NGƯỜI TẠO phiếu: drafter-excluded · co-approver-vẫn-nhận · drafter-vẫn-nhận-chuông-kết-quả) · **S85 +18** (PE CV PRO C5 hủy/đổi NCC winner +10 · urgent drafter-self-unset +5 · workflow-preserve bug-class S42 +3 `PeUpdateDraftWorkflowPreserveTests`) · **S83 +2** (PE require-workflow: `PeWorkItemGuardTests` ApprovalWorkflowId `NotEmpty` ×2 validator + submit-guard rewrite test 7/13 `PeSubmitGuardAndBypass` V1-submit-deprecated, no-count-change) · **S77 +10** (+7 `PeSuggestedPriceSetterAuthzTests` ghi chú PRO/CCM Mig 57 absolute-set/role-gate/null-clear/max-1000 · +3 `PeUrgentToggleAuthzTests` rewrite cờ-gấp **BẤT ĐỐI XỨNG** SET=function role / UNSET=function+DeptManager, plain-PRO-unset Forbidden) · **S76 +5** (`PeWorkItemBudgetTests` PRO split: set ProInitial+ProAdjust gồm âm · validator ProInitial≥0/ProAdjust-cho-âm · full=proFull khi CCM empty · full=CCM khi CCM present) · **S74 +5** (`PeWorkItemBudgetTests` §4b CcmNote: set CCM/Admin · null-clear absolute-set · non-priv Forbidden+no-mutate · all-3-persist) · **S73 +28** (PE opt-in finalize spec 6→11 + 10 giá-chốt `PeApprovedPriceFinalizeTests` + 13 setter-authz `PeSuggestedPriceSetterAuthzTests`) · **S69 +20** (Office-golive OfficeModulePermissionSeed 6 + PE PeCcmThresholdFinalize 5 + PeUrgentToggleAuthz 9) · **S67 +23 HRM test-after** (DepartmentTreeTests 8 cycle-guard/rollup + PeHoSoLinkTests 9 absolute-set + HrmProfilePermissionSeedTests 6 reflection-seed) → 286 · S61 +22 `PeWorkItemBudgetTests` −14 `BudgetPolicyTests` −1 → 263 · S60 +14 `PeSubmitGuardAndBypassTests` +2 spec → 256 (Domain 58→45 do drop Budget module tests) | | Gotchas | **93** | 🔄 **93 @S194** — **+S194 #93** (dấu PHẨY trong tên file `.docx` ⇒ `MSB4186` làm CI #489 đỏ; local KHÔNG tái hiện vì SDK CI 10.0.202 ⟂ local 10.0.104, `grep MakeRelative` ở SDK local = 0). Đo: `grep -c '^### ' docs/gotchas.md` = 93, entry cuối `:1519` = `### 93.`, continuity 1→93 **0 gap 0 dup**. 🧊 **92 @S191** — **+S191 #92** (runner build-env NHIỄM: CI exit-0 + **byte-match CI↔prod** + smoke 200 nhưng bundle **CRASH runtime** `useState null` → trắng màn; 2 ca S189+S191 [#600: fe-user CI 1.803.881B vs local 1.700.709B **+103KB** CÙNG commit, local deterministic 2-build-1-hash; fe-admin lệch +39KB nhưng sống — nhiễm KHÔNG đồng đều]; 🔴 **ROOT-CAUSE THẬT = `deploy.yml` XOÁ `package-lock.json` rồi `npm install`** ⇒ resolve lại cây theo semver mỗi run ⇒ cùng commit ra bundle khác nhau. **2 giả thuyết ĐỀU BỊ BÁC bằng đo**: ① "root-cause=disk" (bác bởi ca-2) · ② "npm-cache SYSTEM là nguồn" (bác bởi run #488 — ra 4/4 hash trùng local mà **KHÔNG ai dọn cache lần nào**). Thuốc = `npm ci` + lockfile-committed + GUARD-92 (`305b573f`) — `npm ci` là **THUỐC**, không phải đường nhiễm; fix chuẩn ×2 = build local HEAD → scp đè app chết GIỮ bundle hỏng làm rollback-1s → verify RUNTIME console+read_page; luật: verify deploy FE PHẢI có 1 phép đo runtime — file-integrity+HTTP-200 chưa đủ · bundle CI lệch local cùng-commit = ĐỎ · sau sự cố hạ tầng coi MỌI cache toolchain là nghi phạm · ràng buộc CẤM-push GỠ @run #488 — mốc điều-kiện giữ cho lần sau) · 🧊 **91 @S189** — **+S189 #91** (push land trên ĐĨA nhưng tầng-app Gitea CÂM — 0 run/0 status dù MỌI tín hiệu quen XANH [runner Running · ENABLED · filter đúng · không push-đè]; tố cáo bằng **2 nguồn LỆCH cùng API**: `branches/main→commit.id` [git-đĩa] tiến ⟂ `repo→updated_at` [DB] đứng ⇒ CI **chưa từng được gọi**, không phải đỏ; kèm `gitea.log` 0-byte 11,5h [xoay không mở lại] ⇒ restart mở logger + `workflow_dispatch` API → run #486 ship trọn; cost-của-thuốc: restart kích re-index 25' tranh RAM → latency bimodal = số bẩn, khai chứ không giấu; vá RAM cùng lượt `max server memory` **unbounded→1200MB** [PLE 522→1123, A/059 0,58s]; luật verifier: `updated_at ≥ push-time` TRƯỚC khi tuyên "đang chờ CI") · **+S189 #89** (cache automation phình vô hình ăn sạch ổ C — `gitea repo-archive` 16,49GB + `npm-cache` SYSTEM 12,76GB → free **−20MB** → SQL `Error -2` không mở nổi connection → "ko duyệt được"/GET-500, FE gom mọi lỗi vào "Không tìm thấy phiếu" [#44] ⇒ nghi oan phiếu trong khi phiếu NGUYÊN; fix dọn 30GB + `AUTO_CLOSE OFF` ×3 DB [ON = mỗi connection mở lại ~5s] + FE tách 404⟂500 5-site ×2-app; guard runbook §Disk-free + cicd-monitor 1b; luật: prod chậm/mất/không-lưu ⇒ `fsutil diskfree` TRƯỚC khi nghi data) · **+S189 #90** (`ssh … \| head` cắt pipe → sshd Windows-OpenSSH MỒ CÔI spin full core — 3 orphan bão hòa máy 3-core, SQL `ASYNC_NETWORK_IO` CPU-66ms-elapsed-29s, client đói CPU không nhận data ⇒ A/059 47s/500 "task was canceled"; kill orphan [CHỪA ancestor-chain] → **0,38s** tức thì; luật: cắt output = `Select-Object -First N` phía REMOTE + đợi cold-start ~48s sau recycle mới đo + op đo nặng tự hỏi "ngắt rồi để lại gì") — sự cố YC-028 trọn: `runs/2026-08-10-S189-bookstart-open/run.md` + RCA `YC028-S189` error-ledger. 🧊 **88 @S185** — **+S185 #88** (sự cố prod 2 tầng: disk C: đầy → 2 DB RECOVERY_PENDING → Disk-Cleanup "free space" **NÉN NTFS đúng file .mdf/.ldf đang đóng** → SET ONLINE fail `Msg 5118`, login prod 500 "transient failure UseSqlServer" + Gitea push 502/unpacker CÙNG GỐC; fix OFFLINE-từng-DB → backup copy → restart-service-nhả-handle → `compact /U` 4 file + folder → ONLINE, `Recovery is complete` **0 mất data** [58 PE · 21 user · 97 bảng khớp]; luật: Msg 5118 ⇒ check `compact` TRƯỚC khi nghĩ restore + CẤM nén cây SQL DATA; kèm phát hiện **1433 phơi Internet, `sa` bị brute-force** — chờ anh vá firewall). 🧊 87 @S179 flush — đo đĩa `docs/gotchas.md` max anchor = **87** (`lead-stale-auditor` FLAG-5 + máy HIGH `canonical-itself-stale`; #87 sinh ở S177 vá lệch 2 tầng). 🧊 **+S168 #86** (2-push-sát → Gitea auto-CANCEL run đầu cùng-branch: CANCELLED ≠ fail — verifier đọc run CUỐI của range + bằng-chứng artifact-level; side-effect @startup chỉ 1-startup cho cả cụm push; ca thật `aaed699` ship qua #442) · **+S162 #85** (menu-key OR-gate FE ≠ policy per-action BE: cây 4-folder GĐ gate `['Khkk_List','Khkk_G1','KeHoachKyKet'].some(can)` NHƯNG endpoint gắn `[Authorize(Policy="KeHoachKyKet.Read")]` [`ContractSigningPlansController.cs:36`] + `MenuPermissionHandler.cs:40` so khớp **CHÍNH XÁC 1 key** — KHÔNG kế-thừa cha, KHÔNG OR key con ⇒ role `Khkk_List=1 ∧ KeHoachKyKet=0`: FE mở cổng → API **403** → folder GĐ2 hiện "—" dưới **MỌI** gói thầu, claim "gate trước fetch ⇒ 0 rác 403" **vỡ**. Bẫy: luật "OR nhiều key" **ĐÚNG ở tầng HIỂN-THỊ** [`GetMyMenuTreeQuery.cs:61-96` trả node khi con `CanRead`] nhưng **SAI ở tầng API-authz** — mặt thứ 2 của #82, lần này **menu-hiện RỘNG hơn API-cho** [#82 là chiều ngược]. Fix = gate đúng key policy CỦA endpoint; **mở controller đọc attribute TRƯỚC khi wire gate**, đừng suy từ cây menu. Reviewer bắt **trước ship**; Dev 13/13 role đều `Contracts=1 ∧ KeHoachKyKet=1` nên **chưa lộ** — lớp "chưa vỡ vì dữ-liệu hiện tại may mắn", vá ngay không đợi triệu chứng). **+S159 #84** (revoker-2-chiều: sửa data tay trên prod bị code-path ungated @restart ĐẢO NGƯỢC im lặng — grant menu SQL tay bị revoker [S92] lật 447/494 row; fix = code-first seeder + gỡ nhánh + guard 2-vế + khuôn 3-chân-kiềng; liên-quan #75/#76 chiều seeder). **+S133 #81-EXT** (instance 2 indirect-assignment, KHÔNG số mới: snapshot-NS-at-DaDuyet hook đặt ở finalize choke-point NHƯNG 2 site gán Phase TRỰC TIẾP bypass — admin-override `PurchaseEvaluationWorkflowService.cs:308` `= targetPhase` + seeder `DbInitializer:1323` `= current` → phiếu vào DaDuyet qua 2 đường này KHÔNG snapshot [frozen=0, display rơi về live = đúng bug đang vá]; reviewer adversarial bắt pre-commit, em-main + 3 invocation implement đều sót — cùng bài enumeration-writer #81; meta: field-đích có hook/derive/snapshot ⇒ grep MỌI assignment-site kể cả override/seeder). +1 S126 (**#83** số AGGREGATE session-cumulative tự-lão-hoá — audit COUNTS đổi bởi CHÍNH spawn review sau đó [2/2/0→4/4/0]; outward mời-reproduce = bảng per-lane theo run-id, KHÔNG số tổng; reviewer-gate bắt, em-main-solo sót). +1 S118 (**#82** menu-flag grant ≠ quyền API thật: display-layer [Permission `CanRead`→`GetMyMenuTree` FE-menu] ⟂ API-authz-layer [controller `[Authorize(Policy=)]`] = 2 tầng ĐỘC-LẬP; grant `Suppliers` R+C+U cho PRO nhưng `PUT/DELETE /suppliers` gate `[Authorize(Roles="Admin,CatalogManager")]` → PRO vẫn 403 sửa/xóa NCC [chỉ Publish/Import theo `Suppliers.Update` là ăn]; ngược lại `ReportsController` `[Authorize]` trần → mọi authed-user đọc `dashboard` tài-chính-HĐ ngược S92; fix gate `[Authorize(Policy="Reports.Read")]` per-method + grep controller authz TRƯỚC khi tin hiệu-lực grant; reviewer adversarial bắt, em-main-solo tin menu-flag=quyền thì sót; liên-quan #44). +1 S114 (**#81** derived-invariant re-derive ở write/select NHƯNG SÓT DELETE writer [DeleteQuote/DeleteDetail cascade] → xóa quote-selected cuối của NCC → `Supplier.IsWinner` kẹt stale [Any(IsSelected)=false] → `CreateContract Where(IsWinner)` gom NCC phantom → **HĐ giaTri=0**; fix helper `PeWinnerInvariant.ReSyncAfterQuoteRemovalAsync` gọi sau SaveChanges 2 handler; **enumeration-writer-trong-spec KHÔNG đáng tin** → grep MỌI `.Remove(`/`.Add(`/`=field` chạm entity chứa field-nguồn; chỉ **verify-adversarial-refute** bắt [invest/review/implement đều sót]; cardinality S87/S88 lên tầng mutation-path). +1 S113 (**#80** null-forgiving `!` che NRE trên external-parsed data: `SupplierExcelImportService.NewSupplier` gán `Code = r.Code!.Trim()` → NullReferenceException khi parser trả `null` cho ô cột-4 rỗng [ĐÚNG path R4 "thiếu Mã NCC → nháp"] → confirm nổ 500 CẢ batch dù preview classify New-draft KHÔNG lỗi; sibling `IsPublic=!IsNullOrWhiteSpace(row.Code)` null-safe nhưng NewSupplier thì KHÔNG; empty-string `""` chạy OK nhưng FE round-trip blank→JSON `null` → nổ. Fix `(r.Code ?? "").Trim()`. **Build 0/0 KHÔNG bắt** [`!` tắt nullable-warning]; chỉ test-before-critical bắt. Lesson: `!` null-forgiving trên dữ-liệu-parse-ngoài = mìn, dùng `?? default`). +1 S97 (**#79** backfill soi NHẦM bảng — transition-marker "[Duyệt KẾT THÚC]" nằm ở `PurchaseEvaluationChangelogs.ContextNote` KHÔNG phải `PurchaseEvaluationApprovals` → Mig 60 backfill match 0, phiếu finalize-CCM giữ flag=0 hiện sai "Đã được CEO duyệt"; fix Mig 61 query đúng bảng. PE 2 bảng lịch-sử tách vai [Approvals=ai-duyệt record :745 · Changelogs=chuyển-phase+marker LogTransition :1073]; verify marker-count prod trước khi tin backfill). +1 S97 (**#78** display-flag suy CONFIG thay RUNTIME → nói dối khi instance lệch config: `endsBeforeCeo` từ "workflow CÓ cấp finalize" thay "phiếu NÀY finalize" → phiếu lên CEO báo sai "không qua CEO"; nặng sau S97 default-flip opt-in; fix field runtime `EndedByLevelFinalize` Mig 60. Flag = "instance ĐÃ làm-gì" KHÔNG "config CÓ THỂ"). +1 S96 (**#77** SPA deploy bundle cache-lag: team thấy UI CŨ dù đã deploy + Ctrl+F5 — index.html no-cache ĐÚNG-chuẩn + JS hash-immutable nhưng browser giữ tab-SPA-mở/cache-local → verify bundle prod **BYTE-LEVEL** (curl + grep string-fix trong `/assets/index-.js`) phân-biệt cache-vs-deploy; **incognito = golden-test**; no-service-worker nên clear-site-data đủ). +2 S91 (**#76** deploy render config từ `.example` KHÔNG phải file-gitignored → prod thiếu setting tới khi điền tay · **#75** seed ungated per-code RESURRECT data đã-wipe mỗi restart → gate sau env-flag `DemoSeed:Disabled` + verify-by-restart-thật). +1 S90 (**#74** Chrome-MCP screenshot kẹt SPA-SignalR `document_idle` → PowerShell `CopyFromScreen` + Win32 lọc-process-`chrome.exe`). +2 S85 (**#72** lệch giờ +7h — DateTime lưu UTC nhưng serialize thiếu 'Z' → FE `new Date()`=local → `UtcDateTimeJsonConverter` global ép UTC-with-Z · **#73** UpdateDraft absolute-set CLEAR field SÓT khỏi pattern null-safe = bug-class S42 → guard `if (request.X is not null)`). +1 S78 **#71** (thêm enum value vào entity DÙNG-CHUNG → pollute UI/guard phân-loại theo PROXY-predicate `supplierId===null` thay vì enum tường minh; file-khi-duyệt purpose=5 lẫn "Bảng so sánh" + false-pass submit-guard → grep mọi field-proxy predicate loại value mới + build-verify TỪNG app; em-main grep no-leak bắt trước deploy). +1 S76 **#70** (FE absolute-set echo field anh-em từ server-snapshot + `invalidate()` fire-and-forget → cửa-sổ stale-echo mất-dữ-liệu khi lưu 2 ô liên-tiếp; guard `useIsFetching` gate nút Lưu tới khi refetch land; reviewer workflow bắt, impl self-review sót). +1 S72 **#69** FE bundle-hash non-deterministic + `deploy.yml` rebuild-FE-unconditional → bundle rotate kể cả commit governance (SPA-fallback-200 trap; cicd Run #312). +2 S69: **#67** Tailwind v4 accent palette thiếu stop (teal/violet/amberx/greenx chỉ 50/100/500/600/700; dùng -300 → teal/violet rơi DEFAULT Tailwind sai-hệ, amberx/greenx drop hẳn — build PASS, phải soi dist CSS) · **#68** IDE TS diagnostic giữa background-agent/workflow = snapshot dở-dang (TS server bắn mỗi save), chỉ tin build SẠCH sau-cùng (2× session này: re-skin + PE-FE đều false-alarm). Prev +1 S68 **#66** Tailwind v4 rule element thô `h1-h4{color:#0b1220}` viết NGOÀI `@layer` thắng utility `text-white` → heading render đen; fix ĐIỂM `text-white!` (important, grep dist confirm), KHÔNG move `@layer` (load-bearing ~30+ heading toàn app). Prev +1 S65 **#65** build csproj con (vd `SolutionErp.Api.csproj`) ≠ `dotnet build SolutionErp.slnx` (gồm tests) → miss test-compile khi đổi chữ ký record command (CreateDepartmentCommand +ParentId) → CI CS7036 FAIL-gated Run #291 (deploy chặn, prod nguyên — test-gate làm đúng việc). Fix: build full slnx trước push BE signature-change. Prev +2 S61: **#63** EF scaffold tự sinh `RenameColumn` SAI-semantics khi drop+add cùng type (test xanh không bắt — SQLite EnsureCreated không replay migration) · **#64** `dotnet ef database update` áp Design-DB 0-rows ≠ Dev-DB → data-migrate `Sql()` chạy thật lần đầu trên prod. Prev +2 S59 (#61 sqlcmd `-f 65001` · #62 rename natural-key UPDATE trước deploy) | | User memory | **60** | 🔄 **re-ground S179** — đo lại ĐÚNG phương-pháp dòng này tự khai: `ls .claude/auto-memory/*.md | wc -l` = **61**, trừ 1 index `MEMORY.md` = **60** (`lead-stale-auditor` FLAG-9; máy MÙ trục này — không có cross-check). 🔴 Ô này trôi lại chỉ sau **9 nhãn phiên** kể từ lần re-ground trước ⇒ đây là ô **rụng nhanh**, đừng tin số cũ. 🧊 re-ground S170 — đếm đĩa 60 file trừ 1 index = 59 (số "31" stale từ S84; lead-stale FLAG @S169, ring2 verify, vá @S170 SAU khi ring chấm — đúng thứ-tự H24 §2(4) fire→chứng-kiến→vá). +S84 `feedback_derived_flush_execution` (derived→canonical flush = em-main execution KHÔNG gate user-confirm; monitor INFORM-only ≠ cần-anh-duyệt; chỉ ĐỔI-con-số/policy = quyền anh). +S79 `feedback_canonical_spec_over_broadcast` (adap pull canonical-command/§P đừng phỏng-theo broadcast + report-before-stamp dogfood + residual own-diary OK). +S75 `feedback_harness11_engine` (engine governance tự-bảo-trì + gotcha #30-via-Edit + multi-workflow-verify + git-add-specific lesson). re-ground S71 (H1 disk-count: 25 `feedback_*` + 3 `project_*`; STATUS "21" stale từ S64). +S71 `feedback_harness10_run_trace` (run-trace + 3 lesson). Prev +1 S64 `feedback_harness7_outward_writing_quality`; re-grounded S54 base 19 | | Skills | 6 | 3 domain + 3 ops | diff --git a/docs/governance/so-yeu-cau-chu-du-an.md b/docs/governance/so-yeu-cau-chu-du-an.md index 59853d30..e5ded0c9 100644 --- a/docs/governance/so-yeu-cau-chu-du-an.md +++ b/docs/governance/so-yeu-cau-chu-du-an.md @@ -68,6 +68,8 @@ Dữ-liệu cũ (WAL · HANDOFF · auto-memory · migration-todos) **KHÔNG** đ | `YC-036` | "OK xong hết hoàn toàn đi rồi tao sẽ test" | 2026-08-14 · S194 | **mở** | — | WAL chain G1-G5 *(thi-công → WAL)* | Lệnh làm TRỌN `YC-032` — hết 4 vế, không dừng giữa chừng để hỏi, rồi anh tự kiểm trên prod. Đội hiểu: chạy nốt G1 (office + verify FE) → G2 khung document-view → G3 pre-fill → G4 in + khoá bản cứng → G5 review + deploy, và **deploy lên prod** vì "tao sẽ test" hàm ý anh test trên máy thật. 🔴 Ràng buộc GIỮ NGUYÊN, không được lấy chữ "xong hết" để bỏ qua: 4 nợ đã-biết ở spec `§⑥.c` (VAT · con-trỏ chết ĐIỀU 8/12 · authz đính kèm · 12 điều PHẦN 2) là owner CỐ Ý hoãn — vẫn phải KHAI khi báo cáo, cấm im lặng cho tròn. | **`YC-032`** | | `YC-037` | *(chốt bằng chọn-đáp-án ở AskUserQuestion, không phải free-text)* — câu 1 **"Chuẩn thuận + vá cửa git (Đề xuất)"** · câu 2 việc-kế **"Vá 3 lỗ hạ-tầng vừa bắt"** | 2026-08-14 · S196 | **mở** | 1 | `.claude/commands/tiep.md` §0 khối ⑤(a) *(vá đã land)* + `runs/2026-08-14-S196-bookend-open/` | Hai lệnh tách bạch. **(1)** Chuẩn thuận việc lead đi tiếp sau khi Sàn-3 ⑤ kêu — chẩn đoán dương-giả ĐÚNG, nhưng lead **vượt quyền** khi tự thi hành (`ctx-audit` C-1: trụ bằng chứng là phép trích RỖNG; ⑤ là phép HỢP, lead đổi thành "đĩa thắng git"). Chuẩn thuận = tha ca này, **KHÔNG** nới luật — anh cố ý chọn nhánh vá MÁY thay vì nhánh "cho lead tự phán". **(2)** Việc kế = 3 lỗ hạ-tầng: `agents/README.md:264` thiếu 3 vai (`ring5-audit`·`score-counter`·`score-count-auditor`) ⇒ vai gọi trong workflow sẽ THROW · 4 ô canonical stale `STATUS:6/9/11/484` · self-compact `harvest-curator` (L1 **bị cắt thật** khi spawn phiên này, 27.360 B > hook read-limit ~24,4 KB). 🔴 Lead khai ràng buộc: 3 lỗ này nằm trong **bề-mặt-đo bị đóng băng** (Mirror C1) nên phải chờ `ring1`/`ring2` trả verdict mới sửa — không phải hoãn, là thứ-tự bắt buộc. | | | `YC-038` | "disposition 52 slug -> OK" | 2026-08-14 · S196 | **mở** | 1 | `docs/governance/carry-disposition-S196.md` *(sinh trong lượt)* + `docs/HANDOFF.md` khối RE-STAMP | Lệnh CHẠY disposition per-slug cho 52 slug `[carry:*]` ở `HANDOFF:44`, thay vì hoãn tiếp. Nền: `lead-gap` F-2 @S196 chứng lời hẹn *"disposition per-slug = việc bookend kế"* đã hoãn **3 bookend liên tiếp** (S192-open → S195-close → S196-open) vì neo vào chữ **"bookend kế"** ⇒ **tự gia hạn vô hạn, không bao giờ quá hạn**; `grep -rn "disposition per-slug"` = 7 hit, **7/7 đều là lời NHẮC, 0 hit là kết quả**. Đội hiểu: mỗi slug phải ra đúng 1 trong 3 nhãn {ĐÓNG · MỞ · CHƯA ĐO ĐƯỢC} kèm bằng chứng đo, **CẤM làm tròn "không thấy" thành "đã đóng"**; rồi **đổi chữ "bookend kế" thành mốc SỐ thật** để hết trôi. 🔴 Caveat mang theo (`ring2` @S196): con số 52 đúng theo `HANDOFF:44`, nhưng tập slug trên đĩa **phụ thuộc SCOPE** (docs/ = 71 · tracked trừ run-folder = 72 · toàn tracked = 86) ⇒ phải khai scope, đừng trình 2 thước như một phép trừ. | | +| `YC-039` | "Hiện tạo hợp đồng -> thì phải bắt từ KHKK vậy thì các thông tin sẽ liên kết dối dài thêm từ Duyệt NCC -> KHKK -> Hợp đồng." | 2026-08-14 · S197 | **mở** | 1 | báo-cáo assessment S197 *(thi-công → WAL nếu thành việc)* | Hai tầng, khai ranh rõ: **(i) vế XÁC-NHẬN chuỗi kế thừa** Duyệt NCC → KHKK → HĐ = trùng `YC-024`(81) + `YC-032`(3) **đã ship** (bridge K7 hội-2-khóa + pre-fill 17 ô, deploy #490) · **(ii) vế "PHẢI bắt từ KHKK"** đọc được thành RÀNG BUỘC MỚI: hiện máy **CÒN đường tạo HĐ tự do KHÔNG qua KHKK** (`POST /contracts` + 3 lối UI: menu Create per-loại, nút Tạo mới MyContracts, Dashboard) — HĐ tạo đường này **0 liên kết** ngược KHKK/NCC. Cần anh phán siết hay giữ (câu hỏi ở báo-cáo S197). *Anh bác cách hiểu (ii) ⇒ lật dòng này thành nhắc-lại-của `YC-024`, không xoá.* | | +| `YC-040` | "(b) Chặn HẾT 7 loại — mọi HĐ đều phải từ KHKK -> Tao nghĩ nên như vậy trước đã." | 2026-08-14 · S197 | **mở** | 1 | WAL chain *(thi-công → WAL)* | Phán quyết câu **(106)** = nhánh **(b)**: chặn đường tạo HĐ tự do cho CẢ 7 loại (kể cả 2 loại Nguyên tắc) — mọi HĐ chỉ sinh từ KHKK đã duyệt qua "Đưa vào HĐ". Vế *"nên như vậy trước đã"* = posture TẠM-THỜI-SIẾT: code chặn phải GỌN-DỄ-ĐẢO (guard 1 chỗ BE + đổi lối UI, không phá máy tạo — sau anh muốn nới loại nào thì mở lại rẻ). Thi công 2 tầng theo #82: API `POST /contracts` chặn 409 + UI đổi 3 lối tạo tự do (menu Create · nút Tạo mới · Dashboard) về hướng KHKK; giữ nguyên nhánh EDIT Nháp + đường bridge. Kèm test guard (test-before cho guard theo rules §7). | | | `YC-018` | "2 phần bookend và bookstart -> trong đợt adapt này AI_INFRA cũng có hướng dẫn bọn mày chạy 1 mạch hết toàn bộ ko còn pause/tiep nữa, chú ý cấu hình cái đó kỹ nhé" | 2026-08-09 · S185 | **đóng** *(cấu hình land 4 site @S185 + chạy thật trọn 1 mạch bookend @close cùng đêm — nghiệm bằng vận hành; 🔴 nhắc lần 2 @S188 = PN-001: lead bỏ 3 vai @close với lý do "ngân sách" — owner bác, chạy nốt ngay trong lượt)* | **2** | WAL chain (TASK → WAL) | Nghi-thức 2 đầu phiên (`/session-start` bookstart + `/session-end` bookend) phải chạy **TRỌN TRONG MỘT LƯỢT** như hub kể ở thư 08-08 Phần 1b ("lượt mở phiên gần nhất chạy trọn trong một lượt duy nhất, không cần cửa tạm dừng nào chen giữa") — cấu hình luật ONE-SHOT vào 2 file lệnh, CẤM rải nghi-thức qua nhiều cửa pause/tiep, CẤM hẹn-cửa-sau. *(Đội hiểu: pause/tiep VẪN là recovery khi đứt bất-khả-kháng [limit/crash — H22 giữ nguyên]; thứ bị cấm là DÙNG chúng làm nhịp chia-chặng nghi-thức. Anh bác cách hiểu này thì em sửa.)* | | > 🔴 **MỘT yêu-cầu, nhắc 4 lần.** Đây là dữ-liệu **TC-01** sinh ra để bắt, và nó tố-giác chính lead: hiểu sai ở `YC-002` nên anh phải nói lại 3 lần. **Ghi lại chứ không làm đẹp.** diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index 6273e14d..3a7b004e 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -134,7 +134,12 @@ function resolvePath(key: string): string | null { const typeInt = TYPE_CODE_TO_INT[code] if (!typeInt) return null if (action === 'List') return `/contracts?type=${typeInt}` - if (action === 'Create') return `/contracts/new?type=${typeInt}` + // [YC-040 (106)(b) @S197] Menu "Tạo mới" per-loại ẨN — mọi HĐ sinh từ KHKK ("Đưa vào HĐ"). + // Menu-row + policy `Ct_*_Create` GIỮ DB; nới gate thì trả lại: + // if (action === 'Create') return `/contracts/new?type=${typeInt}` + // ⚠️ Edge chấp nhận (reviewer #5 @S197): role cấp ĐÚNG MỖI `Ct_X_Create` ⇒ header nhóm + // bung thân RỖNG (không crash; seeding hiện tại cấp rộng nên không tái hiện). + if (action === 'Create') return null if (action === 'Pending') return `/contracts?type=${typeInt}&pendingMe=1` } diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 4b561876..232474b2 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -1924,7 +1924,11 @@ function SuggestedPriceRows({ ev }: { ev: PeDetailBundle }) { // ===== Section 2 — Chọn NCC/TP (spec: a/b/c/d) ===== function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { - const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.suppliers.some(s => s.isWinner) + // [YC-040 (106)(b) @S197] Đường PE→HĐ 1-click ĐÓNG (di tích Phase 6 trước-KHKK) — chuỗi + // đúng nay: PE (DaDuyet) → KHKK → HĐ. BE gate 409 (`ContractCreationGate`); nút ẩn theo. + // 🧊 điều kiện cũ giữ làm vết — nới gate thì khôi phục: + // !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.suppliers.some(s => s.isWinner) + const canCreateContract = false const [createOpen, setCreateOpen] = useState(false) // [anh Kiệt FDC S87+] c/d/e (Giá chào thầu + Đề xuất PRO/CCM + Bảng so sánh + Link hồ @@ -2183,6 +2187,7 @@ function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBun mutationFn: async () => // [Mig 58 — anh Kiệt FDC] Multi-winner: BE tạo 1 HĐ / mỗi đơn vị trúng (IsWinner) // → trả LIST contractIds. ContractType người dùng chọn áp cho mọi HĐ. + // [YC-040] KHÔNG mở lại dialog này khi gate còn đóng — BE trả 409 (ContractCreationGate). api.post<{ contractIds: string[] }>(`/purchase-evaluations/${evaluation.id}/create-contract`, form), onSuccess: res => { toast.success(`Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.`) diff --git a/fe-admin/src/pages/contracts/ContractCreatePage.tsx b/fe-admin/src/pages/contracts/ContractCreatePage.tsx index 9beb67fc..fc1df631 100644 --- a/fe-admin/src/pages/contracts/ContractCreatePage.tsx +++ b/fe-admin/src/pages/contracts/ContractCreatePage.tsx @@ -12,7 +12,7 @@ // hiển thị Chi tiết section. import { useCallback, useState, useMemo, type FormEvent, useEffect } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Link, useSearchParams } from 'react-router-dom' +import { Link, useNavigate, useSearchParams } from 'react-router-dom' import { FileText, Plus, Search, Save, Pencil, Trash2, Lock } from 'lucide-react' import { toast } from 'sonner' import { ContractDetailsTab } from '@/components/contracts/ContractDetailsTab' @@ -69,6 +69,7 @@ const BODY_DOC_BY_KIND: Record = { export function ContractCreatePage() { const qc = useQueryClient() + const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const typeFilter = searchParams.get('type') ? Number(searchParams.get('type')) : 2 const selectedId = searchParams.get('id') @@ -133,10 +134,12 @@ export function ContractCreatePage() { } function startNew() { - const next = new URLSearchParams(searchParams) - next.set('mode', 'new') - next.delete('id') - setSearchParams(next, { replace: false }) + // [YC-040 (106)(b) @S197] Cửa tạo TRỰC TIẾP đã đóng — mọi HĐ sinh từ KHKK đã duyệt + // qua "Đưa vào HĐ" (KhkkDetailPage). Nút này nay dẫn sang KHKK thay vì mở form trống. + // 🧊 Thân cũ (mở ?mode=new) giữ làm vết — nới lại thì khôi phục: + // const next = new URLSearchParams(searchParams); next.set('mode','new'); + // next.delete('id'); setSearchParams(next, { replace: false }) + navigate('/khkk/list') } const typeLabel = ContractTypeLabel[typeFilter] ?? 'HĐ' @@ -182,7 +185,7 @@ export function ContractCreatePage() { )} @@ -263,12 +266,9 @@ export function ContractCreatePage() { {/* + Thêm mới button — sticky cuối Panel 1 */}
-
@@ -280,22 +280,23 @@ export function ContractCreatePage() { )} {isNewMode && ( - { - // Sau khi tạo: refresh list + chuyển sang edit mode để user nhập Chi tiết - const next = new URLSearchParams(searchParams) - next.delete('mode') - next.set('id', newId) - setSearchParams(next, { replace: true }) - }} - /> + // [YC-040 (106)(b)] ?mode=new chỉ còn tới được qua URL cũ/bookmark — chặn form, + // chỉ đường về KHKK. ContractHeaderForm giữ nguyên (không render) để nới lại rẻ. +
+ navigate('/khkk/list')}>Mở Kế hoạch Ký kết + } + /> +
)} {!isNewMode && selectedId && detail.data && ( { + // [YC-040] KHÔNG import/gọi lại form này khi gate còn đóng — BE trả 409 (ContractCreationGate). const res = await api.post<{ id: string }>('/contracts', { type: Number(type), supplierId, diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 67e90542..b13a1a6a 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -175,7 +175,14 @@ function resolvePath(key: string): string | null { const typeInt = TYPE_CODE_TO_INT[code] if (!typeInt) return null if (action === 'List') return `/my-contracts?type=${typeInt}` - if (action === 'Create') return `/contracts/new?type=${typeInt}` + // [YC-040 (106)(b) @S197] Menu "Tạo mới" per-loại ẨN (return null ⇒ MenuLeaf drop — + // gotcha #50, ở đây là CHỦ ĐÍCH): mọi HĐ sinh từ KHKK qua "Đưa vào HĐ". Menu-row + + // policy `Ct_*_Create` GIỮ nguyên DB (máy rộng UI hẹp) — nới gate thì trả lại 1 dòng: + // if (action === 'Create') return `/contracts/new?type=${typeInt}` + // ⚠️ Edge chấp nhận (reviewer #5 @S197): role được cấp ĐÚNG MỖI `Ct_X_Create` (không leaf + // nào khác của loại đó) sẽ thấy header nhóm bung thân RỖNG — không crash, seeding hiện + // tại cấp rộng nên không tái hiện; đóng hẳn thì lọc children đã-resolve trước MenuGroup. + if (action === 'Create') return null // 🔴 [YC-029 W3 S190] "Đang duyệt" Ở LẠI trang danh sách thay vì nhảy sang `/inbox` // (anh: bấm mục con của loại HĐ mà bị đá sang hộp thư tổng = mất ngữ cảnh loại). // `pendingMe=1` là DANH-TÍNH leaf (KHÔNG vào `TRANSIENT_QUERY_KEYS` — xem chú ở đó), diff --git a/fe-user/src/components/contracts/ContractListPanel.tsx b/fe-user/src/components/contracts/ContractListPanel.tsx index ce31c392..718fa677 100644 --- a/fe-user/src/components/contracts/ContractListPanel.tsx +++ b/fe-user/src/components/contracts/ContractListPanel.tsx @@ -191,7 +191,7 @@ export function ContractListPanel({ ? 'Không HĐ nào khớp bộ lọc đang bật.' : typeLabel ? 'Loại HĐ này chưa có dữ liệu.' - : 'Tạo HĐ mới để bắt đầu.' + : 'Tạo Kế hoạch Ký kết (KHKK) để bắt đầu — HĐ sinh từ KHKK đã duyệt.' return ( @@ -280,22 +280,23 @@ export function ContractCreatePage() { )} {isNewMode && ( - { - // Sau khi tạo: refresh list + chuyển sang edit mode để user nhập Chi tiết - const next = new URLSearchParams(searchParams) - next.delete('mode') - next.set('id', newId) - setSearchParams(next, { replace: true }) - }} - /> + // [YC-040 (106)(b)] ?mode=new chỉ còn tới được qua URL cũ/bookmark — chặn form, + // chỉ đường về KHKK. ContractHeaderForm giữ nguyên (không render) để nới lại rẻ. +
+ navigate('/khkk/list')}>Mở Kế hoạch Ký kết + } + /> +
)} {!isNewMode && selectedId && detail.data && ( { + // [YC-040] KHÔNG import/gọi lại form này khi gate còn đóng — BE trả 409 (ContractCreationGate). const res = await api.post<{ id: string }>('/contracts', { type: Number(type), supplierId, diff --git a/fe-user/src/pages/contracts/MyContractsPage.tsx b/fe-user/src/pages/contracts/MyContractsPage.tsx index 1081830a..54750e20 100644 --- a/fe-user/src/pages/contracts/MyContractsPage.tsx +++ b/fe-user/src/pages/contracts/MyContractsPage.tsx @@ -153,9 +153,10 @@ export function MyContractsPage() { )} {!showDeleted && ( - )} diff --git a/src/Backend/SolutionErp.Application/Contracts/ContractCreationGate.cs b/src/Backend/SolutionErp.Application/Contracts/ContractCreationGate.cs new file mode 100644 index 00000000..d2614a38 --- /dev/null +++ b/src/Backend/SolutionErp.Application/Contracts/ContractCreationGate.cs @@ -0,0 +1,33 @@ +using SolutionErp.Application.Common.Exceptions; +using SolutionErp.Domain.Contracts; + +namespace SolutionErp.Application.Contracts; + +// [YC-040 @S197 2026-08-14] Câu (106) = (b), owner nguyên văn: "Chặn HẾT 7 loại — mọi HĐ +// đều phải từ KHKK -> Tao nghĩ nên như vậy trước đã." +// +// Mọi Hợp đồng chỉ được sinh từ dòng KHKK đã duyệt qua bridge "Đưa vào HĐ" +// (`ContractSigningPlans/CreateContractFromSigningPlanFeatures.cs` — write-site duy nhất +// được phép; bridge KHÔNG đi qua gate này). Hai cửa BỊ CHẶN: +// 1. POST /contracts (tạo tự do — `ContractFeatures.cs`) +// 2. POST /purchase-evaluations/{id}/create-contract (PE→HĐ 1-click, nhảy cóc KHKK — +// `CreateContractFromEvaluationFeatures.cs`, di tích Phase 6 trước khi KHKK ra đời) +// Write-site thứ 4 MIỄN-TRỪ CÓ CHỦ ĐÍCH (reviewer #1 @S197): `DbInitializer.SeedDemoContractsAsync` +// tạo HĐ [DEMO] thẳng DB, KHÔNG qua gate — chỉ chạy khi cờ demo-seed BẬT (prod tắt); +// gắn gate vào seeder sẽ giết demo data mà không thêm an toàn prod nào. +// +// "trước đã" = posture TẠM-SIẾT: muốn nới loại nào (vd nhánh (a) câu (106) — mở lại 2 loại +// Nguyên tắc) thì THÊM loại đó vào AllowedDirectTypes; không xoá class, không sửa 2 handler. +public static class ContractCreationGate +{ + // (106)(b) ⇒ tập RỖNG: cả 7 loại đều phải đi đường KHKK. + private static readonly HashSet AllowedDirectTypes = new(); + + public static void EnsureDirectCreationAllowed(ContractType type) + { + if (!AllowedDirectTypes.Contains(type)) + throw new ConflictException( + "Hợp đồng phải được tạo từ Kế hoạch Ký kết (KHKK) đã duyệt — mở phiếu KHKK, " + + "chọn dòng hạng mục rồi bấm 'Đưa vào HĐ'. Đường tạo trực tiếp đã đóng theo YC-040."); + } +} diff --git a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs index 59c800ae..878ca0a3 100644 --- a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs +++ b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs @@ -95,6 +95,10 @@ public class CreateContractCommandHandler( { public async Task Handle(CreateContractCommand request, CancellationToken ct) { + // [YC-040 (106)(b)] Cửa tạo TỰ DO đã đóng — mọi HĐ sinh từ bridge KHKK. + // Gate đứng TRƯỚC mọi load: forge payload cũng 409 sớm, không lộ tồn-tại dữ liệu. + ContractCreationGate.EnsureDirectCreationAllowed(request.Type); + // Load supplier + project full (cần Code cho gen mã RG-001) var supplier = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == request.SupplierId, ct) ?? throw new NotFoundException("Supplier", request.SupplierId); diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs index 3591b327..60590d7f 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs @@ -3,6 +3,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; using SolutionErp.Application.Common.Exceptions; using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Application.Contracts; using SolutionErp.Application.Contracts.Services; using SolutionErp.Application.PurchaseEvaluations.Dtos; using SolutionErp.Domain.Contracts; @@ -44,6 +45,10 @@ public class CreateContractFromEvaluationCommandHandler( { public async Task> Handle(CreateContractFromEvaluationCommand request, CancellationToken ct) { + // [YC-040 (106)(b)] Đường PE→HĐ 1-click nhảy cóc KHKK đã đóng — chuỗi đúng nay là + // PE (DaDuyet) → KHKK → HĐ (bridge "Đưa vào HĐ"). Gate TRƯỚC mọi load (mirror ContractFeatures). + ContractCreationGate.EnsureDirectCreationAllowed(request.ContractType); + var pe = await db.PurchaseEvaluations .Include(p => p.Details).ThenInclude(d => d.Quotes) // [Mig 58] Quotes → GiaTri per-winner .Include(p => p.Suppliers) // [Mig 58] IsWinner = tập winner diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/ContractCreationGateTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/ContractCreationGateTests.cs new file mode 100644 index 00000000..90366fe9 --- /dev/null +++ b/tests/SolutionErp.Infrastructure.Tests/Application/ContractCreationGateTests.cs @@ -0,0 +1,39 @@ +using SolutionErp.Application.Common.Exceptions; +using SolutionErp.Application.Contracts; +using SolutionErp.Domain.Contracts; + +namespace SolutionErp.Infrastructure.Tests.Application; + +// [YC-040 @S197] Câu (106) = (b) — owner: "Chặn HẾT 7 loại — mọi HĐ đều phải từ KHKK". +// Gate = luật nghiệp-vụ CỨNG (guard ⇒ test-before-merge theo rules §7). +// +// Positive-control cho đường SỐNG: bridge KHKK (`CreateContractFromSigningPlanFeatures`) +// KHÔNG gọi gate — suite `CreateContractFromSigningPlanTests` hiện có PASS nguyên vẹn +// chính là bằng chứng bridge không bị chặn nhầm; không lặp test đó ở đây. +public class ContractCreationGateTests +{ + [Theory] + [InlineData(ContractType.HopDongThauPhu)] // loại thi-công phổ biến nhất + [InlineData(ContractType.HopDongNguyenTacNCC)] // (106)(b) chặn CẢ Nguyên tắc — khác nhánh (a) + public void EnsureDirectCreationAllowed_MoiLoai_DeuThrowConflict(ContractType type) + { + var act = () => ContractCreationGate.EnsureDirectCreationAllowed(type); + + act.Should().Throw() + .WithMessage("*Kế hoạch Ký kết (KHKK)*") + .WithMessage("*YC-040*"); + } + + [Fact] + public void EnsureDirectCreationAllowed_Chan_Tron_Ca7Loai() + { + // (106)(b) = tập cho-phép RỖNG. Quét TRỌN enum thay vì liệt tay 7 giá trị — + // thêm ContractType mới về sau thì mặc-định cũng bị chặn (fail-closed), + // và test này tự phủ giá trị mới, không cần nhớ sửa. + foreach (var type in Enum.GetValues()) + { + var act = () => ContractCreationGate.EnsureDirectCreationAllowed(type); + act.Should().Throw($"loại {type} phải bị chặn theo (106)(b)"); + } + } +} diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractCommandApplicableTypeTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractCommandApplicableTypeTests.cs index 7d30f973..92d5de90 100644 --- a/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractCommandApplicableTypeTests.cs +++ b/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractCommandApplicableTypeTests.cs @@ -80,13 +80,14 @@ public class CreateContractCommandApplicableTypeTests var act = async () => await handler.Handle(cmd, CancellationToken.None); - // Note: spec BW5 mention ValidationException — thực tế ContractFeatures.cs - // line 84 throw ConflictException ("Quy trình {Code} áp dụng cho {ApplicableType}, - // không khớp với HĐ (cần ApplicableType=Contract)."). FluentValidation - // (line 38-50) chỉ rule field-level (MaximumLength, GreaterThanOrEqualTo), - // KHÔNG có rule cross-table. + // [YC-040 (106)(b) @S197] Cửa POST /contracts nay bị ContractCreationGate chặn + // TRƯỚC mọi load/validate ⇒ forge PE-workflow còn không tới được cửa-1/4 — + // defense NÂNG BẬC, không phải mất: validate ApplicableType vẫn sống cho đường + // bridge KHKK (ContractWorkflowGuards, suite CreateContractFromSigningPlanTests phủ). + // 🧊 expectation cũ giữ làm vết: ConflictException "*ApplicableType=Contract*" + // (ContractFeatures cửa-1/4) — đúng cho tới khi gate đóng cửa này @S197. await act.Should().ThrowAsync() - .WithMessage("*ApplicableType=Contract*"); + .WithMessage("*Kế hoạch Ký kết (KHKK)*"); } } diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromEvaluationMultiWinnerTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromEvaluationMultiWinnerTests.cs index fabbe2ad..1a78cc97 100644 --- a/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromEvaluationMultiWinnerTests.cs +++ b/tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromEvaluationMultiWinnerTests.cs @@ -165,7 +165,7 @@ public class CreateContractFromEvaluationMultiWinnerTests // ===== (a) ≥2 winners → N HĐ, mỗi GiaTri = winner's quote total ===== - [Fact] + [Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")] public async Task Create_TwoWinners_CreatesTwoContracts_EachGiaTriIsThatWinnerQuoteTotal() { using var fix = new IdentityFixture(); @@ -221,7 +221,7 @@ public class CreateContractFromEvaluationMultiWinnerTests // NAY single = SUM(Quote.IsSelected của winner), giống multi. Trước đây test này assert // GiaTri = 800 (budget sum); giờ = 140 (100+40 = quote ĐƯỢC CHỌN của winner0). - [Fact] + [Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")] public async Task Create_SingleWinner_GiaTriIsSelectedQuoteTotal_NotBudgetSum() { using var fix = new IdentityFixture(); @@ -262,7 +262,7 @@ public class CreateContractFromEvaluationMultiWinnerTests // Winner thắng CHỈ 1 trong 2 hạng mục (detail0 IsSelected=true, detail1 IsSelected=false) // → GiaTri = SUM(selected quotes) = quote detail0 THÔI, KHÁC SUM-mọi-quote-của-winner // (chống bug detailsSum/all-quote cũ). Chứng minh handler lọc theo Quote.IsSelected per hạng mục. - [Fact] + [Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")] public async Task Create_SingleWinner_WinsSubsetOfDetails_GiaTriSumsOnlySelectedQuotes() { using var fix = new IdentityFixture(); @@ -299,7 +299,7 @@ public class CreateContractFromEvaluationMultiWinnerTests // ===== (c) idempotency → re-call throws ConflictException ===== - [Fact] + [Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")] public async Task Create_Again_AfterSuccess_ThrowsConflict_DaTaoHopDongRoi() { using var fix = new IdentityFixture(); @@ -329,7 +329,7 @@ public class CreateContractFromEvaluationMultiWinnerTests // ===== (d) 0 winner → ConflictException (chưa chọn NCC/TP trúng thầu) ===== - [Fact] + [Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")] public async Task Create_NoWinner_ThrowsConflict_ChuaChonTrungThau() { using var fix = new IdentityFixture(); @@ -353,7 +353,7 @@ public class CreateContractFromEvaluationMultiWinnerTests // ===== (e) not DaDuyet phase → ConflictException ===== - [Fact] + [Fact(Skip = "YC-040 (106)(b) @S197: duong PE->HD dong - ContractCreationGate chan truoc handler. Spec multi-winner giu lam di san (Mig 58/S114), mo lai khi owner noi AllowedDirectTypes.")] public async Task Create_NotDaDuyet_ThrowsConflict() { using var fix = new IdentityFixture(); @@ -375,4 +375,24 @@ public class CreateContractFromEvaluationMultiWinnerTests (await db.Contracts.AsNoTracking().CountAsync()).Should().Be(0); } + + // [YC-040 (106)(b) @S197] Test SỐNG duy nhất của file khi cửa đóng: gate chặn TRƯỚC + // MỌI LOAD — Pe.Id không tồn tại mà vẫn 409-KHKK (không phải NotFoundException) + // = chứng thứ-tự guard đứng đầu handler, forge không dò được tồn-tại phiếu. + [Fact] + public async Task Create_GateDong_ChanTruocMoiLoad_KhongLoNotFound() + { + using var fix = new IdentityFixture(); + var db = fix.Services.GetRequiredService(); + var actor = await fix.CreateUserAsync("gate-yc040@test.local", "Gate YC040", + departmentId: null, roles: new[] { AppRoles.Drafter }); + var handler = BuildHandler(db, fix.Services, actor); + + var act = async () => await handler.Handle( + new CreateContractFromEvaluationCommand(Guid.NewGuid(), ContractType.HopDongThauPhu, null), + CancellationToken.None); + + await act.Should().ThrowAsync().WithMessage("*Kế hoạch Ký kết (KHKK)*"); + (await db.Contracts.AsNoTracking().CountAsync()).Should().Be(0); + } }