# sub-reviewer-anchor-B-s160 — LĂNG KÍNH: verify ANCHOR/CLAIM vs ĐĨA (cụm B)

> Run: 2026-07-29-S160-khkk-dryrun-plan · vai: reviewer (adversarial) · 3 file đích:
> `spec-wave-w3-duyet-3-tram-29-07-2026.md` · `spec-wave-w5-cau-khkk-hd-29-07-2026.md` · `spec-wave-w6-duong-ong-hd-v2-29-07-2026.md`
> Quy ước: mỗi finding = F-B<i> + SEV ∈ {HIGH, MED, LOW, INFO} + {spec-file:dòng} + {bằng-chứng ĐĨA file:line hoặc lệnh đo}.
> Đĩa đo tại commit `950054e` (wal: flush 20260729T1553).

## VERDICT (dòng-1)

**SUA-TRUOC-THI-CONG** — 2 HIGH + 4 MED + 3 LOW-MED + 4 LOW.
Neo dòng (line-anchor) của 3 spec **rất tốt**: 28 neo đo, **0 neo trỏ mã không tồn tại** (không có anchor-trôi sau arc-2), 24/28 khớp từng dòng. Lỗi KHÔNG nằm ở số dòng mà ở **mô-tả nội dung tại neo** và ở **acceptance** — 2 lỗi HIGH đủ để mất số chốt giá im lặng (F-B1) và nổ FK runtime + mâu thuẫn W1 (F-B2).

---

## A. BẢNG NEO — đo từng cái

| # | Spec | Neo | Đĩa | Phán |
|---|---|---|---|---|
| 1 | w3:13,26 | `ContractWorkflowService.cs:217-394` (ApproveV2Async) | :217 `private async Task ApproveV2Async(` … :394 `}` | ✅ khớp 2 đầu |
| 2 | w3:13 | `:246` GroupBy Order | `var levelGroups = currentStep.Levels.OrderBy(l => l.Order).GroupBy(l => l.Order).ToList();` | ✅ |
| 3 | w3:13,26 | `:259-260` OR-of-N | `allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet();` + `if (!allowedUserIds.Contains(actorUserId.Value))` | ✅ |
| 4 | w3:13,26 | `:292-316` LevelOpinions UPSERT | `db.ContractLevelOpinions.FirstOrDefaultAsync(...)` → if/else Add/update | ✅ |
| 5 | w3:13 | `:320-352` skipToFinal | `if (skipToFinal) { … }` | ✅ |
| 6 | w3:19 | `:407-427` LogTransitionAsync | method = :396-428; :407-427 = **khối notify Drafter** (thiếu :405 `changelog.LogWorkflowTransitionAsync`) | ⚠️ lệch biên (LOW, F-B14) |
| 7 | w3:16,26 | `ApprovalWorkflow.cs:95` class `ApprovalWorkflowLevel` trong file | :95 `public class ApprovalWorkflowLevel : BaseEntity` | ✅ (cảnh báo "KHÔNG có file riêng" ĐÚNG) |
| 8 | w3:16,26 | `:116-125` 4 return-mode | :116 OneLevel · :119 OneStep · :122 ToAssignee · :125 ToDrafter | ✅ đúng 4 cờ |
| 9 | w3:14,26 | `PurchaseEvaluationWorkflowService.cs:1008` RULE | dòng tồn tại — **nội dung ≠ mô-tả spec** | ❌ F-B4 |
| 10 | w3:13 | `ProposalFeatures.cs:427-439` | flatten :427-429 + `ElementAtOrDefault` :433 + match-1-người :439 ✅; **câu "assume 1 step" ở :424 NGOÀI range** | ⚠️ F-B10 |
| 11 | w5:22 | `CreateContractFromEvaluationFeatures.cs:53-60` guard | 3 guard: DaDuyet :53-54 · winners :57-58 · ContractId :59-60 | ✅ |
| 12 | w5:22 | `:68-71` pin V1-only | `db.WorkflowDefinitions…ContractType==… && IsActive` | ✅ (đúng: V1, 0 dòng V2) |
| 13 | w5:22 | `:88-90` SUM | `pe.Details.SelectMany(d=>d.Quotes).Where(q=>…IsSelected).Sum(q=>q.ThanhTien)` | ✅ |
| 14 | w5:16,22 | `:145` idempotency | `pe.ContractId = contracts[0].Id;` | ✅ (check nằm :59-60, write :145 — cả 2 phủ) |
| 15 | w5:15 | `:82-115` vòng lặp per-winner | :82 khai list, :83 `foreach (var w in winners)` … :115 `}` | ✅ |
| 16 | w5:16 | `:101-107` mapping giữ nguyên | TenHopDong/NoiDung/Bypass/DraftData/BudgetManual* | ✅ (cố ý loại GiaTri :100 + Workflow :108 — 2 chỗ W5 sửa) |
| 17 | w5:16 | `:78-82` atomic codegen | :78-81 = **COMMENT**, :82 = khai list; codegen thật ở :113, atomic thật = 1 `SaveChangesAsync` :147 | ⚠️ F-B15 |
| 18 | w5:13 | `:121-140` khuôn ContextNote | ContractChangelog :121-130 + PeChangelog :131-140 | ✅ |
| 19 | w5:14 | `PurchaseEvaluationsController.cs:379-382` body record | `CreateContractFromEvaluationBody(ContractType, TenHopDong, Bypass=false)` = 3 field == command 3 field (trừ route Id) | ✅ |
| 20 | w5:22 | `ContractWorkflowService.cs:115-116` ConflictException | `if (def == null \|\| def.Steps.Count == 0) throw new ConflictException("HĐ chưa pin workflow definition…")` | ✅ neo đúng — nhưng §1/§27 định vị SAI ý nghĩa (F-B5) |
| 21 | w6:8,17,25 | `ContractFeatures.cs:334-351` GetEligiblePhases | method trọn; 7 phase legacy, **0 entry ChoDuyet** | ✅ claim đúng |
| 22 | w6:9,25 | `:421-430` PhaseActorRoles | 7 key legacy, 0 ChoDuyet | ✅ claim "100% legacy" đúng |
| 23 | w6:8,25 | `:488-489` Forbidden | `if (!isDrafter && !isEligibleByRole) throw new ForbiddenException("Bạn không có quyền xem HĐ này.")` | ✅ |
| 24 | w6:17 | `:298` / `:440` admin thấy-hết | :298 = List handler · :440 = Inbox handler — **KHÔNG phải handler đang sửa** (GetContractQuery admin-bypass = :482-483) | ⚠️ F-B11 |
| 25 | w6:10,19,25 | `ContractWorkflowService.cs:73-78` guard trình | `!Drafter && !DeptManager → ForbiddenException` | ✅ |
| 26 | w6:25 | `ContractsController.cs:38-40` inbox | `[HttpGet("inbox")] … GetMyInboxQuery()` | ✅ |
| 27 | w6:32 | `:49-66` Reject-trước-guard | khớp `ContractWorkflowService.cs:49-66` — **thiếu tên file** | ⚠️ F-B12 |
| 28 | w6:18 | khuôn PE `ResolveV2InboxIdsAsync` | có thật: `PurchaseEvaluationFeatures.cs:817` (decl) + `:748` (call) | ✅ (thiếu file:line — nên neo) |

**Tổng:** 24 ✅ khớp từng dòng · 4 ⚠️ lệch biên/nhầm handler · 1 ❌ mô-tả-sai-nội-dung (#9) · **0 anchor trỏ mã đã biến mất**.

---

## B. FINDINGS

### F-B1 — HIGH — "choke-point DUY NHẤT" SAI: nguồn copy có ≥2 đường tới terminal, đường thứ 2 gán QUA BIẾN (đúng bug #81 đã trả giá ở S133)
- **Spec:** `spec-wave-w3-...:14` — *"helper `ApplyApprovedValuesOnFinalize` … tại choke-point **DUY NHẤT** + RULE comment 'mọi nhánh set DaDuyet PHẢI gọi helper' + grep-site tự kiểm"*.
- **Đĩa (nguồn copy):** `ContractWorkflowService.cs:190-199` — sau nhánh V2 còn nhánh **Admin manual override**: `if (isAdmin) { contract.Phase = targetPhase; … }` (:191-193). Đây là đường **thứ 2** tới phase terminal, **gán qua BIẾN** ⇒ mọi phép `grep "Phase = …DaDuyet"` literal **KHÔNG thấy**.
- **Tiền lệ đã trả giá (nguyên văn đĩa):** `PurchaseEvaluationWorkflowService.cs:308-311` — `// [S133 site 5 — reviewer catch] Override thẳng → DaDuyet cũng PHẢI chốt snapshot NS (RULE helper; assignment qua BIẾN targetPhase nên literal-grep 4-site không thấy — #81).`
- **Hệ quả:** acceptance W3 `:30` (`Lines WHERE PlanId=@id AND ApprovedAmount IS NULL = 0`) chỉ chạy đường thường ⇒ phiếu DaDuyet qua admin-override để `ApprovedAmount` NULL **im lặng**, đúng loại lỗi phải vá lại 1 lần rồi.
- **Acceptance đề nghị (không phải sửa code):** spec kê **đủ 2 write-path** (V2-terminal + admin-override) và bổ 1 ca test admin-override→DaDuyet cũng phải có `ApprovedAmount != NULL`.

### F-B2 — HIGH — `LogWorkflowTransitionAsync` là hàm CỦA HỢP ĐỒNG (FK vật lý) ⇒ W3 §4 bất khả thi + mâu thuẫn thẳng W1
- **Spec:** `w3:17` — *"mỗi transition ghi `ContractSigningPlanApprovals` (bảng #7 W1) + `Changelogs` **qua `LogWorkflowTransitionAsync`** (4 cột đủ từ W1)"*.
- **Đĩa:** `IChangelogService.cs:28-33` = `LogWorkflowTransitionAsync(Guid contractId, ContractPhase fromPhase, ContractPhase toPhase, string? comment, ct)`; impl `ChangelogService.cs:72-89` ghi thẳng `_db.ContractChangelogs`; cấu hình `ContractChangelogConfiguration.cs:22-26` có **FK VẬT LÝ**: `HasOne(x => x.Contract).WithMany(c => c.Changelogs).HasForeignKey(x => x.ContractId).OnDelete(Cascade)`.
- ⇒ truyền `plan.Id` = **FK violation runtime (SQL 547)**, không phải lỗi biên dịch. Tham số `ContractPhase` ≠ `ContractSigningPlanPhase` (W1 `:16`) **cast được** ⇒ compiler KHÔNG chặn.
- **Mâu thuẫn W1:** `spec-wave-w1-...:16` bảng #6 = `Changelogs` RIÊNG của KHKK *"[ĐỦ 4 cột EntityType/EntityId/UserName/FieldChangesJson]"* — tức W1 đã dựng bảng riêng, W3 lại chỉ sang service của HĐ.
- **Acceptance đề nghị:** W3 nêu rõ ghi qua đường changelog **của KHKK** (bảng #6 W1) + khai Δ nếu cố ý tái dùng.

### F-B3 — MED-HIGH — "PORT NHƯNG KHÔNG KÍCH HOẠT (workflow seed không bật cờ)" = containment GIẢ + lấn quyền owner
- **Spec:** `w3:15` — *"2 nhánh kết thúc sớm: **PORT NHƯNG KHÔNG KÍCH HOẠT** (code path có, workflow seed không bật cờ) — default #1"*; `w3:7` dẫn lời anh *"PHÁ VỠ" S155*.
- **Đĩa:** 2 cờ là **per-Level admin opt-in**, không phải hằng seed: `ApprovalWorkflow.cs:143` `AllowApproverSkipToFinal`, `:154` `AllowApproverFinalize` (comment :151-153: *"AUTO theo cấu hình quy trình … admin per-level designation = cổng duy nhất"*). Designer V2 **dùng chung** render checkbox thật: `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:1267-1268` (`checked={entry.allowApproverFinalize}` + `onChange`).
- **Ghép với W1:** `spec-wave-w1-...:24` mở chính Designer đó cho `type=10` ⇒ **bất kỳ admin tick 1 ô là bật đường anh đã phán "PHÁ VỠ"**, seed không chặn được gì.
- **Quyền owner:** spec cũ ratified `:363-367` để ngỏ *"2 đường kia **chờ anh xác nhận còn muốn không**"* — tức chờ quyết định CÓ/KHÔNG. W3 tự quyết **PORT** (viết code) rồi mới nói không kích hoạt ⇒ default lấn phần chờ anh.

### F-B4 — MED — W3 mô-tả SAI nội dung khuôn `PurchaseEvaluationWorkflowService.cs:1008` (và số trong khuôn đó đã lệch tuỳ thước)
- **Spec:** `w3:14` — *"(khuôn `PurchaseEvaluationWorkflowService.cs:1008` — comment nêu **LUẬT + lệnh grep**, **CẤM số-đếm site**)"*.
- **Đĩa :1008 nguyên văn:** `// RULE: MỌI nhánh set Phase=DaDuyet PHẢI gọi helper này ngay TRƯỚC assignment (reviewer grep-check 4 site).` ⇒ **KHÔNG có lệnh grep**; **CÓ số-đếm "4 site"** — đúng thứ spec bảo cấm.
- **Đo số "4":** `grep -c "Phase = PurchaseEvaluationPhase.DaDuyet"` = **4** (:877 :916 :949 :1154) nhưng call-site của helper = **5** (:311 :876 :915 :948 :1153). Cùng 1 comment, 2 thước ra 2 số ⇒ số trong chú thích tự-lan sai (đúng lớp F-10/S159).
- **Khuôn ĐÚNG có thật trên đĩa** (LUẬT + lệnh grep + không đếm): `ContractFeatures.cs:356-360` — *"🔴 LUẬT (không đếm số …) … Tự kiểm: grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs"*.
- Phụ: helper tại :1009 là `ApplyBudgetSnapshotOnFinalizeAsync` (ngân sách); khuôn **giá** mà spec cũ `:65` trỏ là `ApplyApprovedPriceOnFinalize` `:973-1002` — W3 rút còn 1 neo và rút nhầm cái.

### F-B5 — MED — W5 định vị SAI `ConflictException` ⇒ acceptance prod 0-bit ở nửa vế + tên test mã hoá cùng ngộ nhận
- **Spec:** `w5:7` *"HĐ **pin workflow V2 để trình được ngay** (hết kẹt `ConflictException`)"*; `w5:27` *"`POST /{id}/transitions {targetPhase:10}` bởi Drafter → **204 (KHÔNG 409)**"*; tên test `w5:24` `Bridge_PinApprovalWorkflowV2_HopDongTrinhDuoc_KhongConConflictException`.
- **Đĩa:** nhánh **trình** `ContractWorkflowService.cs:70-89` KHÔNG đọc workflow, chỉ check role (:73-78) rồi set `Phase=ChoDuyet` (:80) — **204 kể cả HĐ pin V1 hoặc không workflow nào**. Handler `ContractFeatures.cs:216-238` không thêm guard. `ConflictException` `:115-116` chỉ nổ ở nhánh **DUYỆT** (`fromPhase==ChoDuyet && decision==Approve`).
- ⇒ vế "204 KHÔNG 409" **PASS SẴN trước khi sửa** = 0 bit. Vế CÓ răng = `currentApprovalLevelOrder=1` (đo được: DTO `ContractDtos.cs:55`, projection `ContractFeatures.cs:605`, chỉ set khi `ApprovalWorkflowId != null` — `ContractWorkflowService.cs:84`).
- **Acceptance đề nghị:** phải chạy tới **lượt duyệt Cấp-1 bởi approver V2** (2xx + con-trỏ đổi + 1 row LevelOpinion) mới chứng pin V2 sống; giữ vế pointer, bỏ/hạ vế 204.

### F-B6 — MED-HIGH — W5 mở cửa thứ 2 pin `ApprovalWorkflowId` mà KHÔNG nhắc guard `ApplicableType == Contract` (cửa 1 có, và có test canh)
- **Spec:** `w5:14` — *"+param `ApprovalWorkflowId?` vào command + body record … → pin V2 cho HĐ tạo ra"* (0 chữ về ApplicableType).
- **Đĩa cửa 1:** `ContractFeatures.cs:84-86` `if (aw.ApplicableType != …Contract) throw …`, comment `:77-78` nguyên văn *"BE guard chặn attacker forge POST với PE workflow ID (ApplicableType=1/2)"*; test sống `Create_PinApprovalWorkflowId_ApplicableType_DuyetNcc_Throws` (`tests/SolutionErp.Infrastructure.Tests/Application/CreateContractCommandApplicableTypeTests.cs`).
- W5 `:7` tự nêu luật *"2 endpoint cùng nghĩa = lớp lỗi 2-site S155"* rồi mở cửa 2 thiếu đúng guard ấy ⇒ pin HĐ vào workflow type 1/2/**10 (KHKK)**. Cần acceptance: ca ÂM forge `approvalWorkflowId` của type≠3 → 4xx.

### F-B7 — MED — 13/13 tên test đề pin LỆCH khuôn đặt tên của repo (tiền lệ 0/460)
- **Đo:** 460 method `public async Task` trong `tests/**/*Tests.cs`. Grep vị-ngữ thuần Việt (`Duoc|Ve1|ChiMot|_Cua|Bang[A-Z]|Thay|Xem|GuiLai|ChayLai|DungTung`) = **0 hit**.
- **Khuôn đĩa** = vị-ngữ tiếng Anh + danh từ VN không dấu: `ApproveV2_LastLevel_FinalStep_TransitionsToDaPhatHanh_GeneratesMaHopDong`, `Create_TwoWinners_CreatesTwoContracts_EachGiaTriIsThatWinnerQuoteTotal`, `T21_DeleteByApprover_ChoDuyetPe_MovesFourAccumulatorsInFourDirections`.
- **Spec pin 13 tên thuần Việt:** w3:35 (6) · w5:24 (4) · w6:27 (3).
- **Provenance (công bằng):** 6 tên W3 đến từ spec cũ **ratified** `:377-382`; 7 tên W5/W6 từ lane-2 `sub-investigator-codebase-2.md:152,:159`. Lệch có gốc thượng nguồn — nhưng 3 spec biến nó thành **cổng cứng** (`w3:27` *"thiếu 1 tên = TRƯỢT"*) ⇒ khoá vĩnh viễn deviation vào cây test. Cần anh/owner chốt: giữ tên ratified hay chuẩn hoá về khuôn repo (đổi tên = phải sửa cả spec cũ, không được sửa lén).

### F-B8 — MED — "baseline" trong acceptance đếm test KHÔNG định nghĩa ở W3/W5/W6, trong khi W1 dùng thước chặt hơn
- `w3:35` *"số-sau ≥ baseline + 6"* · `w5:25` *"≥ baseline + 4"* · `w6:28` *"≥ baseline + 3"* — "baseline" đo lúc nào, ai đo, không nói ⇒ đo sau khi thêm test vẫn "đạt".
- Đối chiếu **cùng bộ**: `spec-wave-w1-...:34` *"số-sau ≥ **số-đo-ngay-trước** + 2"* (đúng thước, chống tự-lão-hoá); spec cũ ratified `:369` pin literal **"562 + N"** (literal tự lão hoá — `docs/STATUS.md:6` hiện ghi 562 PASS 45D+517I, sẽ lệch ngay commit sau).
- ⇒ 3 spec nên dùng đúng 1 idiom "số-đo-ngay-trước + N" như W1.

### F-B9 — LOW-MED — W5 sửa acceptance của nền RATIFIED mà không khai Δ (W1 có mục Δ, W3/W5/W6 không)
- Ratified `:402`: *"HĐ tạo từ PE có KH DaDuyet → `GiaTri == SUM(ApprovedAmount)`"*.
- W5 `:26`: *"MỖI HĐ `giaTri == ApprovedAmount` của Line cùng supplierId; tổng W HĐ == SUM(ApprovedAmount) toàn plan"*.
- W5 **đúng hơn** (nền multi-winner: `CreateContractFromEvaluationFeatures.cs:83-115` tạo N HĐ), nhưng đây là **sửa nền ratified im lặng**. W1 `:9-11` đã lập khuôn khai Δ ⇒ W5 (và W3 ở F-B3) nên có mục Δ tương ứng.

### F-B10 — LOW — W3 trích câu "assume 1 step" nằm NGOÀI range đã neo
- `w3:13` *"CẤM copy ProposalFeatures `:427-439` … comment nó **tự thú** 'assume 1 step'"*.
- Đĩa: câu tự thú ở **`:424`** (`// Multi-step workflow: traverse step-by-step. Lite version: assume 1 step per workflow`). Trong range :427-439 có đủ bằng chứng khác (flatten :427-429 · `ElementAtOrDefault(p.CurrentApprovalLevelOrder-1)` :433 · match 1-người :439). Range nên là **:423-440**.

### F-B11 — LOW — W6 "Admin giữ thấy-hết (`:298/:440`)" trỏ 2 handler KHÁC handler đang sửa
- W6 `:17` sửa view-guard trong `GetContractQueryHandler` (`:488-489`), nhưng admin-bypass của **chính handler đó** nằm ở `ContractFeatures.cs:482-483` (`var isAdmin = currentUser.Roles.Contains(AppRoles.Admin); if (!isAdmin) {…}`). `:298` = `ListContractsQueryHandler`; `:440` = `GetMyInboxQueryHandler`. Giữ nguyên :298/:440 **không bảo toàn** gì cho :488-489.

### F-B12 — LOW — W6 `:49-66` là anchor TRẦN (không tên file), dễ đọc nhầm sang controller
- `w6:32` *"đụng nhánh Reject-trước-guard (`:49-66`)"*. Nội dung khớp `ContractWorkflowService.cs:49-66` (nhánh Reject chạy trước mọi guard — đúng lỗ S156/gotcha #82). Nhưng file gần nhất được nêu trong cùng §③-A là `ContractsController.cs:38-40` ⇒ mơ hồ. Ghi rõ tên file.

### F-B13 — MED — W6 vá inbox chỉ theo actor-là-approver, BỎ nhánh admin ⇒ admin vẫn mù HĐ `ChoDuyet` sau W6
- Đĩa `ContractFeatures.cs:440-441`: `eligiblePhases = isAdmin ? PhaseActorRoles.Keys.ToList() : …` mà dict `:421-430` **không có ChoDuyet** ⇒ admin gọi `/api/contracts/inbox` vẫn ra 0 HĐ ChoDuyet kể cả sau khi ghép precompute-set V2 cho actor.
- Acceptance W6 `:29` chỉ đo **approver-V2 KHÔNG-admin** ⇒ không bắt được lỗ này. Bổ 1 vế: admin gọi /inbox thấy HĐ ChoDuyet (hoặc khai rõ CỐ Ý không cho admin thấy).

### F-B14 — LOW — neo notify `:407-427` cắt mất chính dòng ghi changelog `:405`
- `w3:19` *"Notify: mirror `LogTransitionAsync` (`ContractWorkflowService.cs:407-427`)"* trong khi `w3:17` lại yêu cầu ghi changelog — dòng làm việc đó là `:405` `await changelog.LogWorkflowTransitionAsync(...)`, **ngoài range**. Method trọn = `:396-428`.

### F-B15 — LOW — neo "atomic codegen `:78-82`" trỏ vào COMMENT, trong khi W5 lại thêm ghi-vào-vòng-lặp
- Đĩa `:78-81` = comment giải thích, `:82` = `var contracts = new List<Contract>();`. Bất-biến atomic THẬT = codegen tự-commit `:113` + **duy nhất 1** `await db.SaveChangesAsync(ct)` `:147`.
- W5 `:15` thêm ghi ngược `Line.ContractId` **trong vòng lặp** ⇒ rủi ro chính là ai đó chèn `SaveChanges` vào loop; "giữ nguyên :78-82" (comment) không canh được. Nên neo :113 + :147.

### F-B16 — LOW — W5 §2(a)/(c) chưa chốt cardinality + chế độ đọc plan
- `w5:13` *"đọc plan `DaDuyet` của PE (`Plans.Where(PurchaseEvaluationId==peId && Phase==DaDuyet)`)"* — không nói **1 PE có tối đa mấy plan DaDuyet** (spec cũ `:157` xác nhận tra ngược bằng đúng query này nhưng cũng không chốt 1-1; W1 `:16` chỉ UNIQUE `(PlanId,SupplierId)` ở Lines, không có ràng buộc 1-plan-DaDuyet/PE) ⇒ 2 plan = lấy cái nào, không định nghĩa.
- `w5:15` ghi ngược `Line.ContractId` cần đọc **tracked** (không `AsNoTracking`) để `SaveChanges` :147 flush — spec im lặng.

---

## C. XÁC NHẬN DƯƠNG (chống rubber-stamp ngược — những chỗ spec ĐÚNG và chắc)
- Gate 5-anchor W3 (`:217-394`, `:246`, `:259-260`, `:292-316`, `:320-352`) — **5/5 đúng từng dòng**, kể cả 2 đầu method.
- `ApprovalWorkflow.cs:95` + `:116-125` — đúng cả class-trong-file lẫn đúng 4 cờ return-mode; cảnh báo "KHÔNG có file riêng tên đó" là **thật**.
- Gate 5-anchor W5 (`:53-60`, `:68-71`, `:88-90`, `:145`, `CWS:115-116`) — 5/5 đúng; `:121-140` khuôn changelog đúng; `PurchaseEvaluationsController.cs:379-382` đúng 3 field.
- Gate 5-anchor W6 (`:334-351`, `:421-430`, `:488-489`, `CWS:73-78`, `ContractsController.cs:38-40`) — 5/5 đúng; 3 claim chẩn đoán (`0 entry ChoDuyet` · `100% legacy phase` · `guard trình đòi Drafter∨DeptManager`) đều **đo đúng**.
- `w5:25` *"`CreateContractFromEvaluationMultiWinnerTests` (ĐÃ TỒN TẠI trên đĩa)"* — đúng: `tests/SolutionErp.Infrastructure.Tests/Application/CreateContractFromEvaluationMultiWinnerTests.cs` (6 test).
- `w6:18` khuôn `ResolveV2InboxIdsAsync` — có thật `PurchaseEvaluationFeatures.cs:817` (+ call :748).
- `w3:18` guard trình `CreatedBy==actor` — khả thi: `BaseEntity.CreatedBy` là `Guid?` và được set thật ở `AuditingInterceptor.cs:44-45` (state Added). `DepartmentId` của phiếu có trong W1 `:16` (C5).
- `w3:30` acceptance finalize CÓ `AND PlanId=@id` (giữ đúng bản vá của spec cũ `:359`) — không tái phạm FAIL-giả toàn bảng.
- `w3:28` (c) `LevelOpinions WHERE PlanId=@id = 1 row` khớp UNIQUE `(PlanId,LevelId)` W1 `:16` (#4).

## D. GHI CHÚ CHÉO (ngoài cụm B, để lane khác không sót)
- W1 `:24` neo `ApprovalWorkflowsV2Page.tsx:143` cho `typeCode→10`: đĩa `:143` là **dòng comment**, map thật `const TYPE_CODE_TO_INT` ở **`:144-148`** (3 entry: DuyetNcc/DuyetNccPhuongAn/Contract). W1 tự hedge "grep lại lúc làm" ⇒ INFO.

## END
