24 KiB
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_KhkkN8— NGOÀIAll(0 policy mới, tiền lệHopDongCung/leafPe_*)". K3.c liệt: const + MenuItem row +Layout.tsxresolvePath + mirrormenuKeys.ts. Không có bước cấp quyền. - Đĩa:
src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:2058—foreach (var menuKey in MenuKeys.All)là nguồn DUY NHẤT của grant admin generic. Key ngoàiAllKHÔNG sinh rowPermissions. src/Backend/SolutionErp.Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs— grepAdmin|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
Alltrong repo đều đi kèm danh sách grant TƯỜNG MINH —Pe_*→peKeysDbInitializer.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 TRONGAll:src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs:182—ApprovalWorkflowsV2, 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àoAllvà chấp nhận +32 policy kèm sửa 2 số canonicaldocs/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ÀIAll…) + 1 MenuItem row con groupCatalogs… + 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 trongAll(MenuKeys.cs:169, dòng thứ 29 của khốiAll) và nằm trong Master-set review-scopeDbInitializer.cs:2181-2183— leaf mới ngoàiAllcũ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ụccó 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
:44comment 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-46và 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-46và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-293cà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-43cù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:342là "= targetPhase(gán qua BIẾN — đúng class bẫy:39comment)" rồi gắn "🔴 PHẢI XỬ", đề 2 phương án: (i) gọiApplyApprovedAmountsngay trước:342khitargetPhase == DaDuyet, (ii) assert + test khoá hành vi. - Đọc
:39trong 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:
:342nằm trongprivate async Task ReturnOrRejectAsync(...)khai báo tại:310.- Toàn repo có đúng 2 call-site:
:128-129truyền literalContractSigningPlanPhase.TraLai,:133-134truyền literalContractSigningPlanPhase.TuChoi. - Cửa vào là
TransitionAsync(Guid planId, string action, …):86-90— chữ ký không cótargetPhase;AllowedActions:73chặn trước mọi truy vấn:95-96; switch đóng kín vớidefault: throw:137-139. - Thân hàm còn tự khoá thêm:
:318isReturn = targetPhase == TraLai,:320-323chỉ chấp nhận phiếu đangChoDuyet,:346-347xoá con-trỏ,:349-355ghi log "Trả lại/Từ chối".
- ⇒
targetPhase == DaDuyetkhông reachable hôm nay. Phương án (i) sẽ mở đường thứ 2 tớiDaDuyetchạ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-43nó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ồiCatalogEntryId = 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-400auto-sinh line trong chính handler create,:389plan.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-24làHasIndex(new { ContractSigningPlanId, SupplierId }).IsUnique().HasFilter("[IsDeleted] = 0"), và index mới cũng filter[IsDeleted]=0nê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:105—b.HasIndex(x => new { x.PurchaseEvaluationId, x.SupplierId }).IsUnique();(không filtered). Nhờ nó mà vòng lặpFeatures.cs:383(foreach w in winners) với:392 SupplierId = w.SupplierIdkhô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:105như 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-713khô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óiProposedAmountcủ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. SauDaDuyet, detail hiệnApprovedAmount(display-gatebudgetFrozen, K2.b dòng 107) còn list vẫn hiệnProposedAmount⇒ hai màn hai con số cho cùng một phiếu đã chốt. - Đề nghị: thêm
:712-713vào bảng consumer; quy địnhProposedAmountcủa row tách; và cho list đọcApprovedAmount ?? ProposedAmountkhi 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.cs1 ·SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs18 ·SolutionErp.Application/Contracts/ContractFeatures.cs3 ·SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs3 (đúng file cụm-1 sẽ sửa) ·SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs4. - Kèm theo:
PurchaseEvaluationsController.cs:186cũng đang khai "DUY NHẤT dùngIgnoreQueryFilters()trong toànsrc/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ônSeedRealMasterDataAsyncthì đúng là không có (DbInitializer.cs:2970var 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.csnằm ởDomain/Master/Catalogs/(cùngMaterialItem.cs,ServiceItem.cs,UnitOfMeasure.cs). - Ảnh hưởng thật: namespace
SolutionErp.Domain.MastervsSolutionErp.Domain.Master.Catalogs⇒usingở Configuration, Features,ApplicationDbContext,IApplicationDbContext. Đề nghị: chốtDomain/Master/Catalogs/ContractCatalogEntry.cs(Application pathApplication/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.csvùng :24-26)". - Đĩa: unique cũ ở
:22-24;:25làHasIndex(x => x.SupplierId),:26làHasIndex(x => x.ContractId)— hai index thường, không liên quan. Đi theo:24-26là đụ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
AddContractSigningPlanGroupAndLineCatalogkhô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ẫylong.chau@vslong.nguyen@(:1616vs: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ònanh.nguyen@"Nguyễn Thị Kim Ánh":1624mà spec không nhắc (CCM có 6 NV:1621-1626, roster chọn 4). - Fail-soft theo khuôn
:178-183chỉ 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_PerWorkflowchỉ đếm hình dạng (Order1×4 + Order2×1), không assert danh tính 11 email.
- Ứng viên thật trong DB:
- 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:
:185var 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-1202hiện loại PE đã có plan (:1201-1202— spec cite chuẩn). K2.c bỏ vế loại-trừ ⇒ DTOApprovedPeAwaitingPlanDto(: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 trongselect 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)
- 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-:130spec nói) · grepAuthorize(Policy = "(WorkItems|MaterialItems|UnitsOfMeasure|ServiceItems)= 0 hit. Comment:9củ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.Readsẽ 403) khớp gotcha #85. - Chứng cứ máy-freeze — ĐÚNG từng dòng.
ContractSigningPlanLine.cs:17-19comment snapshot +:20 PeReferenceAmount·:23 ApprovedAmountkèm chú "CHỈ ghi tại choke-point finalize (W3)" ·WorkflowService.cs:277 ApplyApprovedValuesOnFinalize(plan);đứng trước:278 plan.Phase = DaDuyet·:301-305helper với??=·:288-298khối LUẬT, trong đó:290đúng nguyên văn spec trích. - 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àoplan.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. - Citation khác đều chuẩn:
Features.cs:350+:460(2 call-siteEnsureWorkflowTypeAsync, spec ghi:458-462— nằm trong dải) ·:500Include Lines ·:524supplierIds ·:568-574projection line ·:622-626query record ·:711Σ PeReference ·:1201-1202picker ·DbInitializer.cs:174-176idempotent per-type ·:178-183fail-soft ·:1780-1787Khkk_G1 + 6 leaf ·:1801node ApprovalWorkflowsV2 ·:2970khuôn thiếu IgnoreQueryFilters ·Layout.tsx:166-172resolvePath 3-code +:174 return null(rớt im lặng #50 — đúng) ·ApprovalWorkflowsV2Page.tsx:147slot 10,:151-152note khoá,:164-171DEFAULT_CODE_BY_TYPE ·KhkkCreatePage.tsx:8"FE KHÔNG gửi lines" +:56-66lọcisUserSelectable·KhkkDetailPage.tsx:425-441bả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 AllowApproverFinalizecó sẵn (đúng: 0 mig cho cột này). - 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ẫnCatalogEntryIdbắ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-43nó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
:342SAI · (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.