Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-reviewer-c1-l1.md
2026-07-31 13:04:48 +07:00

24 KiB
Raw Blame History

c1-l1 QUYẾT-vs-code

Lane 1/3 review cụm-1 — lens: QUYẾT (spec chốt) đối chiếu CODE trên đĩa. Đối tượng: .claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-invest-fable-b2-cum1.md (33.048B, 258 dòng). Mọi dòng dưới đây đo từ đĩa @2026-07-31, HEAD e5f53d0.

Điểm số

  • Citation hygiene: 23/25 chuẩn — file:line trỏ đúng chỗ gần như tuyệt đối.
  • Claim chịu lực: 2/9 ĐÚNG · 4 LỆCH · 3 SAI — lỗi KHÔNG nằm ở "trỏ sai dòng", mà ở suy ra cái gì từ dòng đã trỏ đúng và ở cái không được trỏ.
  • Blocker: 2 (F-1, F-2) · Phải sửa trước khi implement: 4 (F-3, F-4, F-6, F-13).

Findings

F-1 [HIGH · BLOCKER] K3 8 menu-con khai "NGOÀI MenuKeys.All" ⇒ 0 permission row ⇒ sidebar không bao giờ hiện

  • Spec dòng 214 (K3.c): "MenuKeys.cs +8 const AwV2_KhkkN1..AwV2_KhkkN8NGOÀI All (0 policy mới, tiền lệ HopDongCung/leaf Pe_*)". K3.c liệt: const + MenuItem row + Layout.tsx resolvePath + mirror menuKeys.ts. Không có bước cấp quyền.
  • Đĩa: src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:2058foreach (var menuKey in MenuKeys.All) là nguồn DUY NHẤT của grant admin generic. Key ngoài All KHÔNG sinh row Permissions.
  • src/Backend/SolutionErp.Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs — grep Admin|IsAdmin = 0 hit ⇒ cây menu KHÔNG có nhánh admin-bypass, không có đường vớt.
  • Tiền lệ mà spec viện dẫn nói NGƯỢC lại: mọi key ngoài All trong repo đều đi kèm danh sách grant TƯỜNG MINH — Pe_*peKeys DbInitializer.cs:2196-2208; HopDongCung.Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung }) :2214; Khkk_*KhkkKeys() :2185-2189 + SeedKeHoachKyKetAccessAsync :2415.
  • Và 3 leaf AwV2_* đang sống thì NẰM TRONG All: src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs:182ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2. Tức 8 leaf mới phá chính khuôn anh em ruột của nó.
  • Hệ quả đo được: acceptance K3.e mục 3 ("sidebar đếm được 8 menu-con") sẽ FAIL tại UAT.
  • Đề nghị: K3.c phải khai thêm 1 việc — hoặc đưa 8 key vào danh sách grant explicit (mẫu KhkkKeys() :2185-2189), hoặc đưa vào All và chấp nhận +32 policy kèm sửa 2 số canonical docs/STATUS.md (Policies 220 / Menu keys 55).

F-2 [HIGH · BLOCKER] K1.d mắc đúng lỗi F-1, nhưng K1.f không có phép đo nào bắt được

  • Spec dòng 57 (K1.d): "+const MenuKeys.ContractCatalog (NGOÀI All …) + 1 MenuItem row con group Catalogs … + grant admin (SeedAdminPermissionsAsync loop)". Hai vế này loại trừ nhau: nguồn của loop chính là MenuKeys.All (DbInitializer.cs:2058).
  • Acceptance K1.f (dòng 69-75) có 7 mục: 3-file mig · seed 86 · restart ×2 · curl 200/403 · test +5 · DoD 86 dòng · số STATUS. Không mục nào chạm menu.
  • Tức: bản cài đặt trong đó trang danh mục không vào được từ sidebar vẫn PASS 7/7. Đây là câu trả lời cho phép thử "impl SAI nào vẫn PASS?".
  • Ghi chú thêm: 4 leaf catalog hiện có (Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems) đều nằm trong All (MenuKeys.cs:169, dòng thứ 29 của khối All) và nằm trong Master-set review-scope DbInitializer.cs:2181-2183 — leaf mới ngoài All cũng sẽ vắng mặt ở review-scope, tức non-admin không thấy.
  • Đề nghị: +1 acceptance đo được ("đăng nhập admin → sidebar Danh mục có leaf mới → click mở đúng trang :kind=contract-catalog") và khai đúng đường grant.

F-3 [HIGH] K3.⚠️ đảo ngược một quyết định owner @S155 mà không khai — trích dẫn :44 bị cắt đúng chỗ nói lý do

  • Spec dòng 180 viết: "…(KHÔNG có nhánh AllowApproverFinalize; MEMORY-verified :44 comment không port CeoApprovalThreshold)".
  • Nguyên văn src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs:44-46:

    (b) 🔴 KHÔNG PORT 2 nhánh kết thúc sớm (AllowApproverFinalize, CeoApprovalThreshold) và cũng KHÔNG port AllowApproverSkipToFinal (:318-352 nguồn copy) — chủ dự án phán "PHÁ VỠ" @S155. W3 chỉ đường THƯỜNG: PMH → CCM → CEO ký thật.

  • Spec trích 1 trong 3 tên nhánh, và tên bị bỏ đúng là AllowApproverFinalize — thứ K3.⚠️(a) định port. Mệnh đề owner-veto cũng bị bỏ.
  • Công bằng với lane: spec-4gd-khkk-tong-quat-31-07-2026.md:46 (hàng OG-3) CÓ ghi lời owner @S164 ("a Chương sẽ quyết định là cái đó cần CEO ko hay là anh ấy đc duyệt") ⇒ có cơ sở hợp lệ để SUPERSEDE quyết định @S155. Vấn đề là B2-cụm-1 không hề nói đây là đảo quyết cũ, nên người đọc sau sẽ gặp comment :44-46 và tưởng code đang bị làm trái ý owner.
  • Đề nghị: K3.⚠️ trích :44-46 đầy đủ + thêm đúng 1 câu "OG-3 @S164 SUPERSEDE veto @S155", và đưa việc sửa lại comment :44-46 vào danh sách việc của K3.

F-4 [MED-HIGH] K3 thiếu việc code thứ 4: khối comment :36-43 sẽ thành lời khai sai ngay khi (a) land

  • K3.⚠️ liệt 3 việc (a)(b)(c); (c) chỉ gỡ khoá + note FE ApprovalWorkflowsV2Page.tsx:151-152.
  • Nhưng ContractSigningPlanWorkflowService.cs:37-43 đang khai: "🔴 KHAI RÕ: service này có ĐÚNG MỘT nhánh set DaDuyet, KHÔNG có site thứ hai kiểu admin-override của nguồn copy". Và :292-293 cài sẵn 2 lệnh tự-kiểm:
    grep -rn "Phase = ContractSigningPlanPhase.DaDuyet" src/Backend --include=*.cs
    grep -rn "ApplyApprovedValuesOnFinalize" src/Backend --include=*.cs
    
  • Sau khi port nhánh duyệt-sớm, mệnh đề "ĐÚNG MỘT" hoặc vẫn đúng (nếu nhánh mới nhảy vào cùng choke-point) hoặc thành sai (nếu sinh site thứ 2) — phải chứng, không phải đoán.
  • Đây đúng class lỗi mà chính spec dùng để bắt FE ở K3.⚠️(c) ("không gỡ = config-lie NGƯỢC"), chỉ khác chiều: lần này là comment BE nói "không có" trong khi code đã có.
  • Đề nghị: thêm acceptance "chạy 2 lệnh :292-293, dán output vào PR, hai lệnh phải khớp nhau" + sửa :36-43 cùng commit K3.

F-5 [MED] K2.b đọc sai comment :39; site :342 không phải bẫy — và phương án (i) sẽ TẠO RA bẫy đó

  • Spec dòng 105 xếp WorkflowService.cs:342 là "= targetPhase (gán qua BIẾN — đúng class bẫy :39 comment)" rồi gắn "🔴 PHẢI XỬ", đề 2 phương án: (i) gọi ApplyApprovedAmounts ngay trước :342 khi targetPhase == DaDuyet, (ii) assert + test khoá hành vi.
  • Đọc :39 trong ngữ cảnh :36-43: câu đó đang mô tả NGUỒN COPY (ContractWorkflowService.cs:190-193), và cả khối khẳng định KHKK cố ý không có site đó, lý do nêu ở :40-42: "hợp-đồng transition KHKK chỉ nhận 4 action cố định, KHÔNG có tham số targetPhase".
  • Đo lại từ đĩa, đúng như code khai:
    • :342 nằm trong private async Task ReturnOrRejectAsync(...) khai báo tại :310.
    • Toàn repo có đúng 2 call-site: :128-129 truyền literal ContractSigningPlanPhase.TraLai, :133-134 truyền literal ContractSigningPlanPhase.TuChoi.
    • Cửa vào là TransitionAsync(Guid planId, string action, …) :86-90 — chữ ký không có targetPhase; AllowedActions :73 chặn trước mọi truy vấn :95-96; switch đóng kín với default: throw :137-139.
    • Thân hàm còn tự khoá thêm: :318 isReturn = targetPhase == TraLai, :320-323 chỉ chấp nhận phiếu đang ChoDuyet, :346-347 xoá con-trỏ, :349-355 ghi log "Trả lại/Từ chối".
  • targetPhase == DaDuyet không reachable hôm nay. Phương án (i) sẽ mở đường thứ 2 tới DaDuyet chạy xuyên thân hàm return/reject (xoá con-trỏ + log sai nhãn) — tức tự tay dựng đúng cái bẫy :37-43 nói là đã tránh được.
  • Đề nghị: bỏ (i), giữ (ii); đổi nhãn ô bảng từ "🔴 PHẢI XỬ" thành "an toàn do thiết kế — khoá bằng test hành vi". Test K2.g #6 giữ nguyên, nhưng phải phát biểu là "không action string nào đưa phiếu tới DaDuyet mà bỏ qua freeze".

F-6 [MED-HIGH] K2 tự mâu thuẫn về CatalogEntryId bắt buộc; hệ quả là rào "phiếu phải có hạng mục" không tồn tại ở đâu

Ba câu trong cùng một cụm, đọc liền nhau thì triệt tiêu nhau:

  • dòng 87 (K2.a-4): "Chặn phát sinh mới bằng validator: line MỚI/SỬA bắt buộc có CatalogEntryId (chỉ row legacy được giữ null)".
  • dòng 113 (K2.c): auto-gen Features.cs:389 "mồi CatalogEntryId = null, TenHangMuc = null" — tức mọi line của mọi phiếu mới đều null.
  • dòng 126 (K2.d): submit-guard "Line CatalogEntryId == null … → CHO QUA".
  • Đối chiếu code: ContractSigningPlanFeatures.cs:383-400 auto-sinh line trong chính handler create, :389 plan.Lines.Add(new ContractSigningPlanLine { … }).
  • ⇒ Phiếu vừa tạo (100% line null) submit và duyệt trọn được mà không mang hạng mục nào. Mục tiêu K2 ("1 phiếu = 1 nhóm, N hạng mục") không được rào tại bất kỳ tầng nào, còn DoD "line khác nhóm → 409" thì PASS trivially vì tập line-có-catalog rỗng.
  • Đề nghị: khai rõ luật NOT-NULL chỉ áp cho lines-editor K4b; và hoặc thêm rào submit "≥1 line có CatalogEntryId" với acceptance đo được, hoặc defer TƯỜNG MINH sang K4b kèm ghi nợ.

F-7 [MED] Chứng "2 line NULL cùng (Plan,Supplier) không lọt" hở mắt xích — mắt xích thật nằm ở module khác

  • Spec dòng 87 lập luận: "Data cũ AN TOÀN (unique cũ bảo đảm ≤1 row/cặp ⇒ tối đa 1 NULL-row/cặp)". Vế data cũ này ĐÚNG: ContractSigningPlanLineConfiguration.cs:22-24HasIndex(new { ContractSigningPlanId, SupplierId }).IsUnique().HasFilter("[IsDeleted] = 0"), và index mới cũng filter [IsDeleted]=0 nên row xoá mềm không tham gia. Migrate sẽ không vỡ.
  • Nhưng unique cũ bị DROP trong chính Mig 71 (K2.a-4) ⇒ sau migrate nó không còn che auto-gen nữa. Thứ thật sự chặn auto-gen sinh 2 line cùng supplier là index ở module khác, spec không hề nhắc: PurchaseEvaluationConfiguration.cs:105b.HasIndex(x => new { x.PurchaseEvaluationId, x.SupplierId }).IsUnique(); (không filtered). Nhờ nó mà vòng lặp Features.cs:383 (foreach w in winners) với :392 SupplierId = w.SupplierId không thể ra 2 line trùng.
  • Kết luận của spec đúng, nhưng chuỗi chứng đứt: bảo đảm đang được chuyển từ một index sắp bị xoá sang một index chưa từng được nêu tên.
  • Đề nghị: cite PurchaseEvaluationConfiguration.cs:105 như bất-biến chịu lực và khoá bằng 1 test; nếu K4b sau này cho thêm line tay thì bất-biến này hết hiệu lực, phải khai luôn.

F-8 [MED] Tiền: list DTO có HAI tổng, spec chỉ xử một — sau freeze, list và detail sẽ nói 2 con số khác nhau

  • ContractSigningPlanFeatures.cs:709-713 (projection list): Count(lines) :709 · Sum(PeReferenceAmount) :710-711 · Sum(ProposedAmount) :712-713.
  • Bảng 17-consumer K2.f dòng 148 chỉ có :711. :712-713 không xuất hiện trong bảng.
  • K2.e (dòng 132) định nghĩa lại semantics cho row tách: PeReferenceAmount = 0, nhưng không nói ProposedAmount của row tách bằng bao nhiêu ⇒ tổng đề-xuất trên list hoặc tụt hoặc nhân đôi, không dấu vết.
  • List cũng không có tổng ApprovedAmount. Sau DaDuyet, detail hiện ApprovedAmount (display-gate budgetFrozen, K2.b dòng 107) còn list vẫn hiện ProposedAmount ⇒ hai màn hai con số cho cùng một phiếu đã chốt.
  • Đề nghị: thêm :712-713 vào bảng consumer; quy định ProposedAmount của row tách; và cho list đọc ApprovedAmount ?? ProposedAmount khi phiếu đã đóng băng (hoặc khai tường minh là chấp nhận lệch).

F-9 [MED] Claim "hit IgnoreQueryFilters đầu tiên trong src/Backend — S155 đo 0-hit" là SAI

  • Spec dòng 38 (K1.b): "(Đây là hit IgnoreQueryFilters đầu tiên trong src/Backend — S155 đo 0-hit — cố ý, khai trong PR.)"
  • Đo lại: 29 occurrence / 5 file trong src/Backend: SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs 1 · SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs 18 · SolutionErp.Application/Contracts/ContractFeatures.cs 3 · SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs 3 (đúng file cụm-1 sẽ sửa) · SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs 4.
  • Kèm theo: PurchaseEvaluationsController.cs:186 cũng đang khai "DUY NHẤT dùng IgnoreQueryFilters() trong toàn src/Backend (D3)" — hai claim độc quyền cùng sai, claim của spec nhiều khả năng thừa kế từ claim này.
  • Nội dung kỹ thuật vẫn ĐÚNG và đáng giữ: seeder phải IgnoreQueryFilters() mới né được #75/#76, còn khuôn SeedRealMasterDataAsync thì đúng là không có (DbInitializer.cs:2970 var existingProjectCodes = await db.Projects.Select(p => p.Code).ToListAsync(); — không IgnoreQueryFilters, đúng như spec mô tả). Chỉ cái nhãn "đầu tiên" là sai.
  • Đề nghị: bỏ nhãn "đầu tiên / S155 đo 0-hit"; nếu tiện thì ghi errata cho PurchaseEvaluationsController.cs:186.

F-10 [LOW-MED] Ba citation trỏ vào USE-SITE trong khi việc phải sửa nằm ở DECLARATION

Spec nói Thứ ở đó Declaration thật
LineDto :570-574 +2 field (dòng 131) projection .Select(l => new ContractSigningPlanLineDto(…)) public record ContractSigningPlanLineDto(Features.cs:59
DetailDto :608-616 +2 field (dòng 131) lời gọi ctor return new ContractSigningPlanDetailDto( :608 public record ContractSigningPlanDetailDto(Features.cs:119
types/khkk.ts:203 KhkkLineDto +2 field (dòng 149) field lines: KhkkLineDto[] của interface Detail interface KhkkLineDto (chỗ khác trong file)

Ngược lại 2 citation cùng loại thì chuẩn: ContractSigningPlanListItemDto :34 (đúng decl) và ListContractSigningPlansQuery :622-626 (đúng decl). Đề nghị: với mỗi DTO ghi CẶP (decl + mọi construction site), vì sửa 1 nơi thì không biên dịch được.

F-11 [LOW] Đường entity K1.a không nằm "cạnh WorkItem"

  • Spec dòng 11: src/Backend/SolutionErp.Domain/Master/ContractCatalogEntry.cs "(cạnh WorkItem — folder Domain/Master hiện hữu)".
  • Đĩa: Domain/Master/ = Catalogs/, Department.cs, Project.cs, Supplier.cs, SupplierStatus.cs, SupplierType.cs. WorkItem.cs nằm ở Domain/Master/Catalogs/ (cùng MaterialItem.cs, ServiceItem.cs, UnitOfMeasure.cs).
  • Ảnh hưởng thật: namespace SolutionErp.Domain.Master vs SolutionErp.Domain.Master.Catalogsusing ở Configuration, Features, ApplicationDbContext, IApplicationDbContext. Đề nghị: chốt Domain/Master/Catalogs/ContractCatalogEntry.cs (Application path Application/Master/Catalogs/ mà spec chọn thì đã đúng khuôn).

F-12 [LOW] Index-swap cite lệch 2 dòng

  • Spec dòng 86: "DropIndex unique cũ … (tên EF-sinh — lấy đúng từ ContractSigningPlanLineConfiguration.cs vùng :24-26)".
  • Đĩa: unique cũ ở :22-24; :25HasIndex(x => x.SupplierId), :26HasIndex(x => x.ContractId) — hai index thường, không liên quan. Đi theo :24-26 là đụng nhầm.
  • Cùng mục: ":29 WithMany(p => p.Lines) giữ nguyên" chính xác.

F-13 [MED] "Item 6" của Mig 71 là tham chiếu treo — và trả lời câu hỏi (c) "hợp lý hay trộn concern"

  • K3.⚠️(b) dòng 181: "gắn vào Mig 71 item 6 (K2.a)". §Thứ tự commit dòng 242: "Mig 71 gồm cả item 6 EndedByLevelFinalize".
  • K2.a (dòng 83-88) liệt 1..5. Không có item 6. Người làm K2 mà chỉ đọc K2.a sẽ không thêm cột ⇒ K3 mất tiền đề "0-mig" và test K3.d #5 (EndedByLevelFinalize=1) fail.
  • Trả lời (c): là TRỘN CONCERN, ba lý do đo được: (1) tên mig AddContractSigningPlanGroupAndLineCatalog không phủ cột này, mà tên mig là nhãn duy nhất người sau đọc trong __EFMigrationsHistory; (2) giữa commit K2 và commit K3 cột là ghost — defined-but-unread, đúng class ghost-wire; (3) chính spec phải viết thêm 2 chỗ cross-ref để bù, và đã hụt 1 chỗ (đây).
  • Vẫn chấp nhận được NẾU làm đủ 3 việc: liệt item 6 tường minh vào K2.a · đổi tên mig cho phủ · khai ghost-window trong PR. Tách Mig 72 thì sạch concern hơn nhưng tốn 1 mig — trade-off để lead chốt, không phải lỗi.

F-14 [MED] Roster K3: acceptance chỉ đo BAO NHIÊU, không đo AI ⇒ chọn sai người vẫn PASS trọn bộ

  • 11/11 mapping email trong bảng K3.a đối chiếu đúng DbInitializer.cs:1613-1632, kể cả cảnh báo bẫy long.chau@ vs long.nguyen@ (:1616 vs :1629) — phần này lane làm tốt.
  • Nhưng cơ chế resolve cho ô ⚠️ "Trưởng" không đủ an toàn:
    • Ứng viên thật trong DB: truong.le@ "Lê Trần Đăng Trường" :1626 · tring.le@ "Lê Tự Đăng Trình" :1625 · và còn anh.nguyen@ "Nguyễn Thị Kim Ánh" :1624 mà spec không nhắc (CCM có 6 NV :1621-1626, roster chọn 4).
    • Fail-soft theo khuôn :178-183 chỉ bắt email không tồn tại. Chọn nhầm một email có tồn tại thì seed 8/8 thành công ⇒ K3.e mục 2 (đếm 8) PASS, mục 4/5 PASS ⇒ sai người duyệt lọt trọn vẹn. Không mục acceptance nào của K3.e hỏi "ai".
    • Test K3.d #2 SeedKhkk_Shape_PerWorkflow chỉ đếm hình dạng (Order1×4 + Order2×1), không assert danh tính 11 email.
  • Cặp nhập nhằng thứ 2 không được gắn ⚠️: PRO "Thanh" → thanh.lethanh@ "Lê Thanh Bình" :1615, trong khi CCM "Binh" → binh.le@ "Lê Văn Bình" :1621. Nếu roster gốc ghi "Bình" thì cùng lớp nhập nhằng như "Trưởng".
  • Thêm một lỗ của khuôn được sao chép: :185 var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM"); không có null-guard. K3 cần cả PRO/CCM/BOD, mà fail-soft spec mô tả chỉ phủ user.
  • Đề nghị: (1) test #2 assert đủ 11 email theo bộ ba (Code, StepOrder, LevelOrder), không chỉ đếm; (2) chốt ô ⚠️ TRƯỚC khi viết seed, không để thành hậu-kiểm; (3) fail-soft nên log ĐỦ email thiếu thay vì return ở email hỏng đầu tiên (một email hỏng hiện giết cả 8 workflow); (4) phủ dept vào fail-soft.

F-15 [LOW] Picker đổi nghĩa nhưng giữ tên cũ; existingGroups là collection-projection trong ctor record

  • Features.cs:1199-1202 hiện loại PE đã có plan (:1201-1202 — spec cite chuẩn). K2.c bỏ vế loại-trừ ⇒ DTO ApprovedPeAwaitingPlanDto (:150) không còn "AwaitingPlan" nữa; tên thành sai nghĩa cho cả BE lẫn FE.
  • existingGroups: int[] là subquery collection tương quan đặt trong select new ApprovedPeAwaitingPlanDto(…) — cần kiểm dịch EF Core trước khi cam kết (rủi ro không translate / client-eval), hoặc lấy bằng 1 query phụ rồi ghép.

Những claim CHỊU ĐƯỢC soi (khai để lead khỏi soi lại)

  1. QUYẾT-1 catalog authz — ĐÚNG 4/4 vế. CatalogsController.cs:13 [Route("api/catalogs")] · :14 [Authorize] class-level · 12 action ghi đều [Authorize(Roles = "Admin")] tại :23,31,40,53,61,70,83,91,100,113,121,130 (đúng dải :23-:130 spec nói) · grep Authorize(Policy = "(WorkItems|MaterialItems|UnitsOfMeasure|ServiceItems) = 0 hit. Comment :9 của chính controller cũng khai cùng ý ("Authorize = đăng nhập đủ … ghi cần Admin"). Lý do (ii) của spec (picker cần READ rộng, khoá KeHoachKyKet.Read sẽ 403) khớp gotcha #85.
  2. Chứng cứ máy-freeze — ĐÚNG từng dòng. ContractSigningPlanLine.cs:17-19 comment snapshot + :20 PeReferenceAmount · :23 ApprovedAmount kèm chú "CHỈ ghi tại choke-point finalize (W3)" · WorkflowService.cs:277 ApplyApprovedValuesOnFinalize(plan); đứng trước :278 plan.Phase = DaDuyet · :301-305 helper với ??= · :288-298 khối LUẬT, trong đó :290 đúng nguyên văn spec trích.
  3. Liệt kê site gán Phase — ĐỦ, không sót. Grep Phase\s*=[^=] toàn repo cho KHKK ra đúng 4 write-site vào plan.Phase: Features.cs:373 (initializer, không có dấu chấm — spec cảnh báo đúng), WorkflowService.cs:174, :278, :342. Phần còn lại là đọc (:173/:245/:341), là Changelog.FromPhase/ToPhase (:493-494), là comment (:39/:292), hoặc nằm trong tests. DbInitializer 0 site — đúng như spec khai.
  4. Citation khác đều chuẩn: Features.cs:350 + :460 (2 call-site EnsureWorkflowTypeAsync, spec ghi :458-462 — nằm trong dải) · :500 Include Lines · :524 supplierIds · :568-574 projection line · :622-626 query record · :711 Σ PeReference · :1201-1202 picker · DbInitializer.cs:174-176 idempotent per-type · :178-183 fail-soft · :1780-1787 Khkk_G1 + 6 leaf · :1801 node ApprovalWorkflowsV2 · :2970 khuôn thiếu IgnoreQueryFilters · Layout.tsx:166-172 resolvePath 3-code + :174 return null (rớt im lặng #50 — đúng) · ApprovalWorkflowsV2Page.tsx:147 slot 10, :151-152 note khoá, :164-171 DEFAULT_CODE_BY_TYPE · KhkkCreatePage.tsx:8 "FE KHÔNG gửi lines" + :56-66 lọc isUserSelectable · KhkkDetailPage.tsx:425-441 bảng lines (lưu ý: thêm cột phải sửa cả <td>:443+, ngoài dải cite) · types/khkk.ts:203 · ApprovalWorkflow.cs:158 AllowApproverFinalize có sẵn (đúng: 0 mig cho cột này).
  5. Số canonical khớp docs/STATUS.md:6 (Mig 69 · 96 bảng · 590 test · Policies 220 · Menu keys 55) ⇒ "96→97", "69→70→71", "baseline +17" đều nhất quán.

Verdict

PASS_WITH_BLOCKERS — spec cụm-1 có nền chắc (citation 23/25 chuẩn, phần chứng-cứ freeze và authz mirror miễn nhiễm soi), nhưng không được implement nguyên trạng.

  • 2 blocker phải sửa trước khi viết dòng code nào: F-1 (8 menu-con K3 ngoài All ⇒ sidebar chết, chính acceptance K3.e sẽ FAIL) và F-2 (K1.d cùng lỗi, K1.f mù không bắt được).
  • 4 phải sửa trong spec trước khi giao: F-3 (khai việc đảo quyết owner @S155 + trích đủ :44-46), F-4 (việc code thứ 4: comment :36-43 + 2 lệnh tự-kiểm :292-293), F-6 (mâu thuẫn CatalogEntryId bắt buộc ⇄ auto-gen null ⇒ rào hạng mục không tồn tại), F-13 (item 6 treo + tên Mig 71 không phủ).
  • 1 phải gỡ khỏi spec: F-5 phương án (i) — nó dựng đúng cái bẫy mà :37-43 nói đã tránh; giữ (ii).
  • Còn lại F-7…F-12, F-14, F-15 là siết chặt + sửa nhãn.
  • Câu trả lời gọn cho 6 câu hỏi giao: (a) ĐÚNG · (b) chứng-cứ ĐÚNG, disposition site :342 SAI · (c) TRỘN CONCERN, chấp nhận được kèm 3 điều kiện · (d) kết luận ĐÚNG nhưng chuỗi chứng HỞ, và "line mới bắt buộc có CatalogEntryId" mâu thuẫn K2.c · (e) mapping ĐÚNG 11/11, cơ chế resolve KHÔNG đủ an toàn · (f) 17/17 dòng cite chuẩn, nhưng thiếu :712-713 Σ ProposedAmount.