Files
solution-erp/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-reviewer-lens-feasibility-s171.md
2026-08-04 18:53:50 +07:00

26 KiB
Raw Blame History

reviewer — LĂNG-KÍNH 2/3: KHẢ-THI KỸ-THUẬT + SỰ-THẬT DỮ-LIỆU

Lane 2/3 của ENSEMBLE /fable-clone S171. Chấm bản sub-investigator-codebase-invest-s171.md. PROPOSE-ONLY / READ-ONLY. Mọi con số dưới đây do lane này TỰ đo, không chép từ bản invest.

0. Tái-dựng số load-bearing (bắt buộc ≥2 — tôi làm 4)

# Số bản invest nêu Lệnh tôi chạy Đo được Phán
R1 WorkflowService.cs:534 ghi ContractSigningPlanApprovals grep -n "ContractSigningPlanApprovals\.Add" :534 đúng y KHỚP
R2 changelog ghi ≥9 chỗ (liệt kê 8: :547 + Features.cs:497,562,639,1078,1116,1224,1304) grep -n "ContractSigningPlanChangelogs\.Add" src/Backend 10 chỗ: WorkflowService.cs:546 · Features.cs:497,562,639,1078,1116,1224,1304,1372 · CreateContractFromSigningPlanFeatures.cs:226 KHỚP về mệnh-đề (10 ≥ 9) — nhưng liệt-kê thiếu 2 (xem §7-B3)
R3 BuildStagesOptions chỉ stage1Content/stage1Count (:163) đọc usePipelineStages.ts:161-165 đúng 2 field, đúng dòng KHỚP
R4 DetailDtoFeatures.cs:145-181 đọc :145-181 record đóng đúng tại :181 (bool BudgetFrozen);), 37 tham-số KHỚP

⇒ 4/4 con-số load-bearing của bản invest tái-dựng được. Bản đó không bịa số. Chỗ nó vỡ là suy-luận từ số, không phải số. Ghi rõ để lead đừng hạ tín-nhiệm nhầm chỗ.

1. D-2 — ContractSigningPlanWorkflowService.cs:534 ghi Approvals: LUÔN hay 1-NHÁNH?

1.0 Đính-chính ĐƯỜNG-DẪN (bản invest ghi thiếu, người sau sẽ mở trượt)

File KHÔNG nằm ở Application/ContractSigningPlans/. Đường-dẫn thật: src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs (find src -iname "*ContractSigningPlan*" → chỉ 1 file mang tên đó). Bản invest trích ContractSigningPlanWorkflowService.cs:534 trần, không path; ai đoán theo hàng-xóm ContractSigningPlanFeatures.cs (Application) sẽ tìm không ra. Lỗi nhẹ, nhưng A-1/A-2 là checklist thi-công nên phải đúng path.

1.1 🟢 Giả-thuyết "chỉ ghi 1 nhánh" của lane tôi: SAI. Claim D-2 HELD.

Tôi vào săn kẽ này và không có kẽ. Đo bằng grep -n "LogSigningPlanTransitionAsync"6 call-site, phủ TRỌN mọi transition:

call-site Ngữ-cảnh Decision truyền vào
:204 Gửi duyệt / gửi lại sau Trả lại ApprovalDecision.Pending
:283 Duyệt + level-finalize (kết thúc sớm) Approve
:296 Duyệt → chuyển Cấp kế Approve
:311 Duyệt → hoàn tất Bước, sang Bước kế Approve
:325 Duyệt Cấp cuối → DaDuyet Approve
:393 Trả lại VÀ Từ chốitargetPhase động, comment, isReturn ? … : … chọn summary ApprovalDecision.Reject

:534 db.ContractSigningPlanApprovals.Add(...) nằm thẳng trong thân LogSigningPlanTransitionAsync (:521-544), không có if nào bọc — mọi tham-số quyết-định (fromPhase/toPhase/decision/actingLevelId/summary) đều là đối-số, không phải hằng. ⇒ Entry "Trả lại → Lùi về Cấp 2" của Ảnh 2 SẼ CÓ. Kẽ mà lệnh của lead nghi ngờ không tồn tại; ước-lượng chi-phí D-2 không bị lật. 🔸 Ghi rõ để lead khỏi mất công: đây là chỗ bản invest đúng mà owner/lead có thể tưởng sai — nó đúng vì tác-giả LogSigningPlanTransitionAsync đã cố-ý gom 2 bảng vào một unit-of-work chung (docstring :515-520), chứ không phải vì may.

1.2 Nhưng có 1 điều D-2 không nói mà ảnh-hưởng đến hình hiển-thị

Approvals cũng chứa dòng Decision = Pending của hành-vi GỬI duyệt (:204-207), tức "Lịch sử duyệt (N)" của KHKK sẽ có nhiều dòng hơn số lần ai đó thật sự bấm Duyệt/Trả/Từ-chối. PE xử việc này bằng lớp merge + dedupe 5s-bucket + synthetic-reject (PeDetailTabs.tsx:3152-3192, bản invest có ghi ở §1.3 mục 10 nhưng không kéo hệ-quả sang §4/§7). ⇒ FE KHKK phải quyết: hiện dòng Pending (nhật-ký đầy-đủ) hay lọc (giống PE hơn). Đây là việc không có trong bảng D, và nó nằm trên đường tới A-8d.

1.3 Tái-dựng số load-bearing #1: KHỚP tuyệt-đối

:534 = db.ContractSigningPlanApprovals.Add(...) · :547 = db.ContractSigningPlanChangelogs.Add(...) lệch 1 dòng — đo thật là :546 (:547{). Sai-số 1 dòng, vô-hại, ghi ra cho đủ. Còn ResolveActorFullNameAsync bản invest ghi WorkflowService.cs:552: đo thật trong file KHKK là call-site :485:554; định-nghĩa hàm nằm ở file KHÁC (PurchaseEvaluationWorkflowService.cs:1301, ContractWorkflowService.cs:446). :552 không trỏ vào cái gì cả → trích-dẫn hỏng (dạng nhẹ). Nội-dung claim ("BE đã có sẵn helper") vẫn HELD.

2. D-3 — changelog ≥9 chỗ ghi + UserName denorm + 0 endpoint đọc

Cả 3 vế: HELD.

  1. ≥9 chỗ ghi → HELD (thật 10). Xem R2 §0.
  2. UserName đã denorm → HELD. ContractSigningPlanWorkflowService.cs:554: UserName = Truncate(await ResolveActorFullNameAsync(actorId, ct), 200) — tên ghi thẳng vào hàng changelog, không cần join lúc đọc. Đúng như bản invest nói.
  3. 0 endpoint đọc → HELD. grep -in "changelog" trên src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs0 hit. Liệt-kê đủ 18 route (HttpGet/Post/Put/Delete tại :36,54,60,73,79,86,95,108,117,129,140,152,162,182,191,205,222,243), không route nàochangelogs. Cửa đọc thật sự chưa có.

Kèm 1 dữ-kiện làm A-13 vỡ (xem §6): đếm thật trên controller — 19 dòng [Authorize(Policy = …)] (1 class-level :29 + 18 action) / 18 route ⇒ tỷ-lệ 19 ≥ 18 và 0 [Authorize] trần THẬT. Tức hôm nay file này sạch theo gotcha #82; nhưng thước A-13 mà bản invest viết lại không đo được điều đó (§6-G1).

3. D-9 / D-8 — usePipelineStages.ts:150-320 (lỗ hổng bản invest TỰ KHAI)

3.1 Trả lời DỨT câu hỏi D-9 (unknown → known)

Đã đọc fe-admin/src/hooks/usePipelineStages.ts:120-350. Trả lời:

Phiếu KHKK cho folder GĐ2 lấy từ QUERY ĐÃ CÓ SẴN — KHÔNG phải thêm fetch mới.

  • Hook usePipelineLinkage() đã chạy query khkkIndex và trả khkkByPeId: Map<peId, KhkkListItemDto[]> (usePipelineStages.ts:123-134, :147).
  • buildStages gom phiếu bằng peItems.flatMap(p => linkage.khkkByPeId.get(p.id) ?? []) (:204) — đúng tập phiếu KHKK của gói thầu, kiểu KhkkListItemDto (đủ maKeHoach peTenGoiThau phase approvalGroup contractIds).
  • toKhkkLeaf (:205-212) đã dựng sẵn leaf. stage2 (:265-273) đã có leaves + groups + locked/failed/loading/truncated. ⇒ Chặn-cửa A-0 GỠ ĐƯỢC NGAY. Không cần query mới, không cần endpoint mới cho panel 1. Đây là điểm bản invest bỏ ngỏ và nó rơi về phía RẺ HƠN ước-lượng.

3.2 D-8 — claim BuildStagesOptions chỉ có stage1: HELD (tự đo)

usePipelineStages.ts:161-165:

export type BuildStagesOptions = {
  /** GĐ1 tự vẽ (trang Duyệt NCC truyền khối leaf phiếu CŨ vào đây). */
  stage1Content?: ReactNode
  stage1Count?: number
}

Đúng 2 field, đúng số dòng bản invest nêu (:163). HELD.

3.3 🔴 D-8 claim "sửa 1 hook, 0 đụng component trình-bày": BROKE — bản invest bỏ sót groups

Đây là chỗ vỡ thật, không phải bắt bẻ chữ:

  • Stage 1 (PE) là folder PHẲNG: { n:1, leaves:[], content, count } (:183-186) — không có groups. Cho nên stage1Content thay chỗ leaves là an-toàn.
  • Stage 2 (KHKK) KHÔNG PHẲNG: nó có groups: khkkGroups = 8 ngăn cố định N1-N8 + ngăn "(chưa phân nhóm)" (:220-252, :268), là feature K5 S166 cố ý "HIỆN ĐỦ 8 NGĂN KỂ CẢ RỖNG" (comment :232-234), mỗi ngăn có onOpenAll → /khkk/list?group=n khớp URL leaf sidebar (:239-241), và có bất-biến Σ cảnh báo DEV khi tổng ngăn ≠ tổng folder (:254-263).
  • ⇒ Nhét stage2Content theo khuôn đối-xứng stage1 (content thay leaves) sẽ đặt card phiếu KHKK8 ngăn nhóm vào tranh-chấp cùng một ô. Hoặc mất 8 ngăn, hoặc phải quyết-định thứ-tự content ⟂ groups — đó là đụng vào PipelineStageFolders, và đụng vào một bất-biến có chuông báo.
  • Đối-xứng "stage1 ↔ stage2" mà bản invest dựa vào là đối-xứng giả: hai stage khác CARDINALITY (phẳng vs 2 tầng). Đây đúng lớp bài học feedback_cardinality_change_grep_consumers.

Hệ quả chi-phí: D-8 không phải "sửa 1 hook". Tối thiểu là 1 hook + 1 quyết-định trình-bày (giữ 8 ngăn hay bỏ) + đo lại bất-biến Σ. Vẫn RẺ, nhưng không phải 0 đụng component.

3.3-bis 🔴 BẰNG-CHỨNG CỨNG: content THẮNG groups — 8 ngăn bị nuốt IM LẶNG

fe-admin/src/components/pipeline/PipelineStageFolders.tsx:285-295 (tự đo, grep -n -C4):

285:        ) : stage.content !== undefined ? (
286:          stage.content
287:        ) : stage.groups !== undefined ? (
...
292:            {stage.groups.map(g => (

content đứng TRƯỚC groups trong chuỗi tam-nguyên ⇒ hễ truyền stage2Content thì nhánh groups không bao giờ chạy. 8 ngăn N1-N8 + ngăn "(chưa phân nhóm)" biến mất, không lỗi, không cảnh-báo. Và stageCount (:95-97) khi có content thì đọc count ?? 0 ⇒ quên truyền stage2Count là badge đếm về 0 trong khi folder đầy phiếu — lại một dạng "vắng-mặt trông giống ổn". ⇒ Claim D-8 "0 đụng component trình-bày": BROKE (2/2 bằng-chứng tại :285-287:95-97). 🔸 Đường ra rẻ: đừng dùng content cho stage 2. Panel 1 KHKK có thể giữ nguyên leaves/groups sẵn có và chỉ làm giàu PipelineLeaf (thêm badge Σ tiền / người soạn), hoặc thêm ô groupContent per-ngăn. Cả hai đều KHÔNG giẫm lên nhánh content của GĐ1.

3.4 Sai địa-chỉ dòng của A-0 (nhỏ nhưng làm người sau đọc trượt)

A-0 và D-9 bảo đọc :150-320 và mô-tả "stage 2 dựng bằng leaves (link) :265". Đúng dòng :265const stage2: PipelineStage = {, nhưng nguồn dữ-liệu thật nằm ở :204 (khkkByPeId) và :123-134 (map dựng trong usePipelineLinkage) — tức NGOÀI khoảng :150-320 mà A-0 chỉ định (:123-134 nằm trước :150). Ai đọc đúng khoảng A-0 vẫn có thể không thấy query nguồn. Sửa A-0 thành :100-320.

4. Chi-phí BE thật (ContractSigningPlanFeatures.cs:145-181) + D-6 join Users

4.1 Chỗ RẺ HƠN ước-lượng (nói ra cho công bằng)

grep -n "new ContractSigningPlanDetailDto"đúng 1 call-site: ContractSigningPlanFeatures.cs:784. Record positional 37 tham-số ⇒ thêm field bình thường sẽ vỡ mọi call-site, nhưng ở đây chỉ có một. ⇒ Chi-phí "append field vào DetailDto" thật sự thấp, đúng như bản invest hàm-ý. HELD.

4.2 D-6 "entity chỉ có ApprovedByUserId, không có tên ⇒ phải join Users": HELD

Đọc thân ContractSigningPlanApproval được khởi-tạo tại WorkflowService.cs:534-544: 8 field ContractSigningPlanId · FromPhase · ToPhase · Decision · Comment · ApprovalWorkflowLevelId · ApprovedByUserId · ApprovedAtkhông có field tên. Đối-chiếu: hàng changelog ngay dưới (:546-557) UserName. Hai bảng ghi cùng lúc, một denorm tên một không ⇒ D-6 đúng, và đúng vì lý-do cấu-trúc chứ không phải suy đoán. 🔸 Gợi-ý rẻ hơn "join Users" mà bản invest không nêu: ApprovalsChangelogs ghi cặp 1-1 trong cùng unit-of-work (:534 + :546, cùng now) ⇒ có thể lấy tên từ changelog cùng transition, hoặc denorm thêm cột tên vào Approval cho khớp anh-em. Cả hai đều tránh join lúc đọc.

4.3 🔴 BROKE — D-1 "+3 field (append cuối)" ước-lượng THIẾU phần đắt nhất

ContractSigningPlan.cs:51 chỉ có public bool EndedByLevelFinalize — một cờ boolean. Không có cột nào lưu Bước nào / Cấp nào đã kết-thúc. ⇒ FinalizeStepNameFinalizeLevelName không phải "field append", chúng là giá-trị phải DẪN XUẤT. PE làm việc đó bằng subquery where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == … lặp 3 lần trong projection (PurchaseEvaluationFeatures.cs:665-683), cộng nhánh 3-ngả phase == DaDuyet ? runtime-flag : suy-config (:665-673). ⇒ D-1 thật sự = 1 field append (EndedByLevelFinalize) + port ~20 dòng projection có 3 subquery. Vẫn làm được, nhưng gọi nó là "+3 field" là ước-lượng sai chiều RẺ — đúng loại sai mà §8-2 của chính bản invest cảnh-báo ("dễ ước-lượng sai đắt nhất theo cả hai chiều"). Nó cảnh-báo đúng rồi tự dính chiều còn lại.

4.4 Chỗ phải sửa mà bản invest không đếm (BE)

# Việc bị bỏ khỏi bảng D / A-1 Bằng-chứng
U-1 ContractSigningPlanApprovalDto CHƯA TỒN TẠI — phải tạo record mới + projection grep -n "ContractSigningPlanApprovalDto" src/Backend0 hit
U-2 Projection Approvals phải .Include/join ApprovalWorkflowLevels nếu muốn hiện "Cấp nào duyệt" (Ảnh 2 có "Lùi về Cấp 2") — ApprovalWorkflowLevelId là Guid trần WorkflowService.cs:541
U-3 types/khkk.ts phải mirror ×2 app cho mọi field mới; A-9 có phủ, nhưng A-1 (BE) không nhắc ⇒ dễ làm 1 app rồi tick A-1 xong R-7 của chính bản invest
U-4 Hàng Decision = Pending (hành-vi gửi duyệt, :204-207) sẽ nằm trong Approvals ⇒ phải quyết lọc hay không trước khi dựng "Lịch sử duyệt (N)" :204-207

5. §5 Phương-án A vs B — lý-do bác B có đứng vững?

5.1 Nửa DỮ-KIỆN: HELD (tôi tự mở 2 file)

  • fe-admin/src/components/pe/PeWorkflowPanel.tsx:384const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○' ⇒ PE đọc step.status có sẵn trên DTO.
  • fe-admin/src/pages/khkk/KhkkWorkflowPanel.tsx:106-120stepStatus(idx) / levelStatus(idx, order) tự tính từ currentWorkflowStepIndex + currentApprovalLevelOrder. ⇒ "shape KHÁC NHAU THẬT" là sự-thật đo được, không phải cái cớ. HELD.

5.2 Nửa LẬP-LUẬN: BROKE — nhị-phân giả (false dilemma)

Bản invest viết: "Generic hoá = phải hoặc (i) ép BE KHKK precompute, hoặc (ii) nhồi adapter 2 chiều. Cả hai đều đụng vào đường duyệt của PE đang chạy prod."

Hai lỗi:

  1. Có đường (iii) mà nó không xét: KHKK đã có sẵn hàm suy status — 15 dòng thuần-tuý, không side-effect (:106-120). Chỉ cần KHKK tự chuẩn-hoá cây thô → shape có status ngay trước khi truyền vào component generic. Đó là adapter MỘT chiều, nằm TRỌN trong nhánh KHKK, và 0 dòng PE bị đọc lại hay sửa.
  2. Câu "cả hai đều đụng vào đường duyệt PE" SAI với (ii): adapter ở phía KHKK không chạm PE. Câu này gán rủi-ro của (i) cho cả (ii) rồi dùng nó để đóng cửa B.

5.3 Nhưng KẾT-LUẬN chọn A vẫn ĐỨNG — vì một lý-do khác, mạnh hơn

Lý-do thật sự để bác B không phải shape, mà là: trích StepOpinionsBox ra khỏi PeDetailTabs.tsx:705-774 buộc phải sửa file trong components/pe/, mà chính bản invest đặt A-10 = "git diff --name-only0 đường-dẫn khớp components/pe/". ⇒ B mâu-thuẫn trực-tiếp với acceptance của chính nó. Cộng thêm ≥6 nhánh riêng của PE (đúng, và đây là lý-do có sức nặng). 🔸 Khuyến-nghị lead: giữ quyết-định A, thay lý-do. Lý-do hiện tại sai kỹ-thuật; nếu owner (hoặc reviewer sau) bẻ được lý-do, quyết-định đúng sẽ bị lật oan. Đây là dạng "kết-luận đúng, chứng-minh hỏng".


6. §7 Checklist — mục nào GOODHART-able?

G1 🔴 A-13 VỠ CẢ HAI ĐẦU (nghiêm nhất — nó là mục canh quyền)

Thước viết: grep -rn "Authorize" …Controller.cs | grep -c "Policy" phải ≥ số route, và 0 dòng [Authorize] trần.

  • Đầu 1 — tử-số bơm bằng COMMENT. Dòng :15chú-thích: // tầng 1 = class `[Authorize(Policy = "KeHoachKyKet.Read")]` (mọi endpoint tối thiểu Read) — chứa cả "Authorize" lẫn "Policy" ⇒ được đếm. Viết thêm vài dòng chú-thích là con-số tự tăng. Ai xoá 1 [Authorize(Policy=…)] thật rồi thêm 1 dòng comment ⇒ A-13 vẫn PASS, API mở toang.
  • Đầu 2 — vế "0 dòng [Authorize] trần" FAIL NGAY HÔM NAY. :17:241 chứa literal [Authorize]` trần trong chú-thích cảnh-báo. Đo thật: file KHÔNG có [Authorize] trần nào (19 dòng Policy / 18 route, §2). ⇒ thước báo đỏ trên một file đang sạch — đây đúng lớp bẫy feedback_citation_trap_selfreference (doc/nguồn nói VỀ anti-pattern bị matcher bắt như anti-pattern).
  • Thước thay thế (đề-xuất): bỏ grep chuỗi, đếm trên AST-lite bằng cách chỉ soi dòng bắt đầu bằng [ sau khi cắt comment:
    grep -nE '^\s*\[(Http(Get|Post|Put|Delete))' <file> | wc -l      # R = số route
    grep -nE '^\s*\[Authorize\(Policy'          <file> | wc -l      # P
    grep -nE '^\s*\[Authorize\]\s*$'            <file> | wc -l      # T phải = 0
    
    Neo ^\s*\[ loại sạch comment //. Chứng thước có RĂNG: thêm tạm 1 action [Authorize] trần vào cây tạm ⇒ T phải nhảy lên 1 (fault-injection, feedback_faultinjection_proves_teeth).

G2 🔴 A-10 đo SAI TRỤC — đo đường-dẫn chạm, không đo hành-vi đổi

A-10: "git diff --name-only → 0 path khớp components/pe/ · pages/pe/ · components/contracts/ · pages/contracts/". Đo thật, buildStages3 consumer trong fe-admin:

components/pipeline/PipelineTreePanel.tsx:309   stages={buildStages(wg.items)}
pages/pe/PurchaseEvaluationsListPage.tsx:524    stages={buildStages(wg.items, { stage1Content: … })}

(và PipelineTreePanel lại là thứ KHKK dùng ở panel-1 lẫn GĐ3 dùng ở panel-3). ⇒ Việc D-8 sửa hooks/usePipelineStages.ts hoặc components/pipeline/PipelineStageFolders.tsxhai path đều KHÔNG nằm trong danh-sách A-10 — có thể làm cây của trang PE mất 8 ngăn GĐ2 (§3.3-bis: content thắng groups) mà A-10 vẫn PASS 100%. Hồi-quy đi vòng qua cửa mà thước không canh. Thước thay thế: thêm 2 path vào A-10 (hooks/usePipelineStages.ts, components/pipeline/) với ý nghĩa "chạm ⇒ phải mở trang PE và trang HĐ đếm lại folder GĐ2 bằng mắt", chứ không cấm chạm.

G3 A-7d Goodhart-able trực-diện

grep -c "Ý kiến cấp duyệt" …KhkkWorkflowPanel.tsx = 0. Đổi tiêu-đề thành "Ý kiến duyệt" / "Ý kiến các cấp" ⇒ PASS trong khi khối vẫn nằm nguyên panel 3. Thước đo chuỗi chữ, mục-tiêu là vị-trí khối. Thay bằng: grep -c "opinions" …KhkkWorkflowPanel.tsx = 0 grep -c "levelOpinions" …KhkkDetailContent.tsx ≥ 1 — đo luồng dữ-liệu, thứ không đổi được bằng cách sửa nhãn.

G4 A-7c / A-8a / A-8b / A-8c / A-8d — cùng một lớp, nhẹ hơn

Đều là grep -c "<cụm tiếng Việt>" ≥1. Gõ đúng cụm chữ vào JSX chết (khối render sau return null, hoặc điều-kiện luôn false) vẫn PASS. Không cần thay hết — nhưng A-8d ("Lịch sử duyệt" + "Lịch sử thay đổi") nên buộc thêm 1 dòng gọi API: grep -c "/changelogs" … ≥1, vì khối đó vô nghĩa nếu không fetch.

G5 A-4 regex sai cú-pháp — sẽ FAIL trên chính bố-cục nó đề-xuất

A-4 đòi khớp ^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\]. Nhưng §5 "Bố-cục đích" ghi Panel 1 = 400px … trong khi khuôn nó bảo chép là ContractsListPage.tsx:125 dùng lg:grid-cols-[340px_1fr_360px] — khớp. Rủi ro thật: nếu ai dùng rem (như KHKK đang dùng 19rem/21rem, KhkkListPage.tsx:197) thì regex [0-9]+px FAIL dù bố-cục đúng. Nới thành \[[^]]*_1fr_[^]]*\].


7. Bỏ-sót lane này bắt thêm (thách-CLEAN)

B1 🔴 content nuốt groups — 8 nhóm N1-N8 biến mất im lặng. §3.3-bis. Đây là món nặng nhất: nó vừa lật claim D-8, vừa là hồi-quy chéo sang trang PE, vừa không có thước nào trong §7 bắt được (A-10 miss, A-11 build vẫn xanh, A-14 chụp màn KHKK chứ không chụp PE).

B2 ContractSigningPlanApprovalDto chưa tồn tại (0 hit) — A-1 viết như thể chỉ cần "thêm field", thực tế phải đẻ record + projection + mirror types ×2 app. (U-1 §4.4)

B3 D-3 liệt-kê thiếu 2 chỗ ghi changelog, một trong đó có nghĩa nghiệp-vụ. Đo được 10, bản invest kể 8. Chỗ bị bỏ đáng kể nhất: CreateContractFromSigningPlanFeatures.cs:226 — đó là bắc-cầu KHKK→HĐ (K7). Nghĩa là "Lịch sử thay đổi" của KHKK sẽ hiện entry tạo Hợp đồng, một loại dòng mà spec §7 không hề nhắc và FE chưa có nhãn cho nó. Thiếu ⇒ hoặc hiện chuỗi thô, hoặc rơi im lặng.

B4 Dòng Decision = Pending (gửi duyệt) vào thẳng "Lịch sử duyệt (N)". PE chống bằng merge + dedupe 5s + synthetic-reject (PeDetailTabs.tsx:3152-3192); bản invest có mô-tả lớp đó ở §1.3 nhưng không kéo hệ-quả vào §4/§7 ⇒ số (N) của KHKK sẽ không cùng nghĩa với số (N) của PE, trong khi cả đề-bài là "cho đồng nhất".

B5 A-0 chỉ sai khoảng dòng. Nguồn dữ-liệu thật ở :123-134 + :204, nằm ngoài :150-320 mà A-0 bắt đọc (§3.4). Người làm đúng theo A-0 vẫn có thể không thấy query nguồn ⇒ chặn-cửa tự vô-hiệu.

B6 Path ContractSigningPlanWorkflowService.cs ghi thiếu lớp. Nó ở Infrastructure/Services/, không phải Application/ContractSigningPlans/ như ngữ-cảnh xung quanh gợi ý (§1.0). Và :552 (ResolveActorFullNameAsync) không trỏ vào gì — call-site thật :485/:554, định-nghĩa ở file khác.


8. Chỗ bản invest ĐÚNG mà owner có thể tưởng sai

  1. 🟢 "Lịch sử duyệt của KHKK ĐÃ được ghi đầy đủ, kể cả Trả lại" — tôi vào để lật và không lật được. 6/6 call-site phủ trọn Pending/Approve/Reject-Return (§1.1). Entry Trả lại → Lùi về Cấp 2 ở Ảnh 2 sẽ có dữ-liệu. Đừng cắt ngân-sách mục này vì nghi ngờ.
  2. 🟢 "Không ép BE KHKK precompute status" (P3-3) — đúng. Hàm suy FE :106-120 là 15 dòng thuần-tuý, đọc currentWorkflowStepIndex/currentApprovalLevelOrder đã có trên DTO (Features.cs:162-163). Ép BE precompute là đổi hợp-đồng API để lấy về một thứ FE tính được miễn phí.
  3. 🟢 Đ-2 (GĐ3 đã 3-panel ⇒ GĐ2 là mắt-xích lệch duy nhất) — tôi không kiểm trực-tiếp ContractsListPage.tsx:125 (ngoài lăng-kính, lane 1/3 nên chấm), nhưng gián-tiếp KHỚP: PipelineTreePanel được cả KHKK và GĐ3 dùng, và buildStages chỉ có 3 consumer đúng như bố-cục đó hàm-ý.
  4. 🟢 Chi-phí append DetailDto thật sự RẺ — 1 call-site duy nhất :784 (§4.1). Owner có thể nghĩ "sửa record 37 tham-số = đụng khắp nơi"; không phải.

9. Chưa đo được / lệnh cần chạy

# Chưa đo Lệnh cần chạy
C-1 Toàn bộ đo của tôi chỉ trên fe-admin/. Không xác-nhận fe-user/ identical for f in hooks/usePipelineStages.ts components/pipeline/PipelineStageFolders.tsx pages/khkk/KhkkWorkflowPanel.tsx; do sha256sum fe-admin/src/$f fe-user/src/$f; done
C-2 A-2b (tên người trong Approvals ≠ null) — cần gọi API thật curl -H "Authorization: Bearer …" https://api…/api/contract-signing-plans/{id} trên 1 phiếu DaDuyet
C-3 hideEmpty tương-tác thế nào với content/groups khi lọc bật (PE truyền hideEmpty={filterActive}) đọc PipelineStageFolders.tsx:230-311
C-4 Số phiếu KHKK thật ở prod (bản invest nói PE 45 phiếu, không nói KHKK) — quyết-định trần 200 (R-11) có cắn không đếm SELECT COUNT(*) FROM ContractSigningPlans WHERE IsDeleted=0
C-5 ContractsListPage.tsx (khuôn được đề-xuất chép) — tôi không đọc, để lane khác chấm sed -n '80,260p' fe-admin/src/pages/contracts/ContractsListPage.tsx

END reviewer-lens-feasibility-s171 — VERDICT=8 HELD / 6 BROKE / 6 bỏ-sót-bắt-thêm