From e05763f46c28909d678b5993317fae2cb145c866 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Sun, 12 Jul 2026 20:45:57 +0700 Subject: [PATCH] wal: flush 20260712T2045 --- .claude/WAL.md | 13 ++-- .../investigator-codebase/MEMORY.md | 4 ++ .../run.md | 14 ++-- .../spec-multi-ncc-per-hangmuc-12-07-2026.md | 66 +++++++++++++++++++ 4 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 .claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md diff --git a/.claude/WAL.md b/.claude/WAL.md index eb736c2..0c27ced 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -3,13 +3,14 @@ updated: 2026-07-12 | session: S114 | branch: main goal: PE multi-NCC per hạng mục (anh Kiệt) — cho ≥2 NCC-TP trúng thầu/1 hạng mục, mỗi nhà 1 giá riêng — 6-bước fable-clone→hmw→verify→deploy→session-end chain: -[!] B1 fable-clone invest ensemble (4-lane inv-cb multi-lens) → spec — wf_dd49ddc0-f29 RUNNING -[ ] B2 fable-clone review (reviewer ensemble) → điều chỉnh → spec cuối -[ ] B3 hmw Opus 4.8 MAX implement (BE + FE 2-app + test) -[ ] B4 fable-clone reviewer verify implementation +[x] B1 fable-clone invest ensemble (4-lane inv-cb) DONE — wf_dd49ddc0-f29, spec-file ghi +[x] B2a anh Kiệt chốt D1=SUM · D2=THAY-THẾ · D3=giá-gốc · D4=1HĐ/NCC (all recommended, D5 default) — spec LOCKED, 0 bảng/cột mới +[!] B2b fable-clone review ensemble (3 reviewer lane: consumer-completeness + correctness + checklist-risk) → spec cuối — RUNNING +[ ] B3 hmw Opus 4.8 MAX implement (BE re-key 6 read-site + FE 2-app + test 6-7 file) +[ ] B4 fable-clone reviewer verify (grep-suite 0 sót IsWinner-aggregation) [ ] B5 deploy + commit (cicd verify) [ ] B6 session-end -next: chờ wf_dd49ddc0-f29 return → em-main synthesize + refute → spec-file 3-mục -verify: 4 lane return schema-clean (findings+decisionPoints+risks) → present spec + anh Kiệt chốt decisionPoints TRƯỚC B3 (cardinality-change, memory feedback_cardinality_change_grep_consumers: grep MỌI consumer) +next: chờ review ensemble return → em-main synthesize → cập nhật spec cuối → B3 hmw implement +verify: reuse Quote.IsSelected (0 migration cột), re-key 6 BE read-site (winnerQuoteTotal:1189 · submit-guard:196 · ccm-threshold:885 · CreateContract:56 · budget-B:907 · projection) + FE PeDetailTabs SHA-lock 2-app + backfill IsWinner→IsSelected phiếu cũ. Adversarial-grep tìm consumer sót (S87/S88 lesson) run-folder: .claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/ diff --git a/.claude/agent-memory/investigator-codebase/MEMORY.md b/.claude/agent-memory/investigator-codebase/MEMORY.md index 27ed004..fda9d22 100644 --- a/.claude/agent-memory/investigator-codebase/MEMORY.md +++ b/.claude/agent-memory/investigator-codebase/MEMORY.md @@ -17,6 +17,9 @@ Read-only INTERNAL audit SOLUTION_ERP codebase. Tools: Read, Grep, Glob, Bash + --- +## 🆕 2026-07-12 (S114-ens) — PE multi-winner PER-HANG-MUC invest, LENS product-history (fable-clone, RETURN-only) +De: cho ≥2 NCC-TP trung CUNG 1 hang-muc, moi NCC 1 gia rieng; bang so sanh chon nhieu winner. **KEY history:** multi-winner Mig58/59 (S87/S88) DA SHIP nhung = **WHOLE-SUPPLIER** granularity — `PurchaseEvaluationSupplier.IsWinner` (:28), toggle per-supplier-column `PeDetailTabs.tsx:2746`, tooltip "chon ≥2 = lien danh". winnerQuoteTotal (`PurchaseEvaluationFeatures.cs:1189-1196`) = SUM ThanhTien MOI quote cua supplier IsWinner qua MOI hang-muc → **KHONG the split per-hang-muc** (A win item1-2, B win item3) vi all-or-nothing per supplier + double-count neu ≥2 winner cung quote 1 item. **`PurchaseEvaluationQuote.IsSelected` (:15, per hang-muc×NCC, "1 row=1 NCC chao 1 hang-muc") = latent hook TU Mig12 day-1 nhung DEAD** (persisted+echoed DTO+seeded, 0 consumer financial; FE hardcode `isSelected:false` never-toggle :2283/2916). Yeu cau moi = re-key winner-truth Supplier.IsWinner→Quote.IsSelected = **CARDINALITY CHANGE #2** (feedback_cardinality_change_grep_consumers — S87 da dinh 1 lan, sot 2 BE Block-B). Consumers IsWinner: ~9 BE (winnerQuoteTotal :1193 · Block-B :908-922 · submit-guard `WorkflowService:192-209` · ccm-threshold :885-908 · create-contract `CreateContractFromEvaluation:56/89-93/189`) + ~8 FE + 7 test file (PeMultiWinner/SelectWinnerClear/CreateContractMultiWinner/ListWinnerNames/WorkItemBudget/CcmThresholdFinalize/SubmitGuard). Contract inherit = 1 HĐ/winner-supplier, GiaTri per-winner (single=detailsSum legacy). Budget per-phieu (PE.WorkItemId single) khong doi cau truc. **#1 decision anh Kiet:** N-win 1 hang-muc = split KHOI LUONG hay full-price-moi-ben? (schema THIEU per-winner volume field — Quote.ThanhTien tinh tren full KL). Tag [s114, pe-multiwinner-per-hangmuc, isselected-dead-hook-mig12, cardinality-change-2, whole-supplier-vs-per-cell]. + ## 🆕 2026-07-12 (S113) — Supplier import v2 invest ensemble (5-lane, Opus MAX, RETURN-only) `[engine: fable-clone-ensemble · Opus-S113]` Em-main harvest @closeout (H2 — 5 lane RETURN-only; content ở `runs/2026-07-12-S113-supplier-import-v2-invest/` journal.jsonl + spec). - **task:** invest cách làm Supplier import v2 (R1-R7) — 5 lăng-kính CÙNG vai investigator-codebase: schema-data-model · import-service · fe-ux · public-visibility-consumers · dedup-validation. `wf_e7896ca6-901`. @@ -84,6 +87,7 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte ## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`) +- **2026-07-12 PE per-HANGMUC multi-winner invest LENS BE-consumer-audit (fable-clone ensemble, RETURN-only):** ⭐ **3 cardinality axes coexist — request ≠ existing multi-winner.** (A) `PE.SelectedSupplierId` = single whole-phiếu winner (legacy). (B) **Mig 58 "multi-winner" = WHOLE-SUPPLIER liên-danh** (`PurchaseEvaluationSupplier.IsWinner` bool): N suppliers each win ENTIRE package → 1 full contract each. NOT per-hangmuc. (C) **`PurchaseEvaluationQuote.IsSelected`** (per hangmuc×supplier, each row own price BgVat/ChuaVat/ThanhTien) = maps EXACTLY to anh Kiệt request BUT **INERT** — written `PeDetailFeatures.cs:308,332` + DbInit:1257, displayed DTO `PeFeatures.cs:1236`, **ZERO money/contract read** (grep confirm). **5 MONEY consumers ALL key off supplier-IsWinner** → over-count for per-hangmuc: R1 winnerQuoteTotal `PeFeatures.cs:1189-1196` · R2 submit-guard `PeWorkflowService.cs:196-211` · R3 **CCM-threshold-finalize FINANCIAL GATE** `PeWorkflowService.cs:885-895` (< CeoThreshold) · R4 CreateContract giaTri `CreateContractFromEvaluationFeatures.cs:56,84-93` (single=detailsSum vs multi=quote-sum INCONSISTENCY) · R5 budget-compare block-B `PeFeatures.cs:907-927`. Write-site SelectWinner `PeDetailFeatures.cs:400-431` = supplier-level list only, NO detail dim. ~6 test files break (PeMultiWinnerTests/PeWorkItemBudgetTests/CreateContractFromEvaluationMultiWinnerTests/PeCcmThresholdFinalizeTests/PeSubmitGuardAndBypassTests/PeSelectWinnerClearTests). Per-hangmuc = re-key money truth IsWinner→Quote.IsSelected (classic cardinality-grep-consumers trap). Tag `[pe-multiwinner, per-hangmuc, quote-isselected-inert, iswinner-whole-supplier, ccm-threshold-gate, cardinality-5-consumers]`. - **2026-07-12 (S112+ Supplier-import-v2 LENS-3 FE-dialog-UX invest `wf` fable-clone):** Task R2/R3/R7 FE. Ground-truth: SupplierImportDialog.tsx + SuppliersPage.tsx **SHA-identical 2-app** (`6313529c`/`e9143d93`) — but types/master.ts DRIFTS (fe-user thiếu `Department.parentId` Mig51, Supplier-portion vẫn mirror). Dialog = 2-step preview→confirm, table READ-ONLY (`:284-325`), **round-trips `preview.rows` verbatim** (`:190`+`:132-134`) → per-row edit ĐÃ khả-thi cho R3 (mutate `row.code`→confirm). Confirm gate `errorCount>0` disable (`:191`). **R2:** 0 template endpoint tồn tại; `ExpectedHeaderTokens` 30-token baked `SupplierExcelImportService.cs:35-68` = single-source cho BE-gen .xlsx row-4 (test Case6 chứng round-trip); blob-download precedent `PeDetailTabs.tsx:3273-79` (`api.get responseType:'blob'`+createObjectURL). **R7:** SuppliersPage cols code/name/type/taxCode/phone/actions (`:174-206`) — **KHÔNG có status/public col**; `Supplier` type có `status` (biz-status ≠ public) NHƯNG **0 field isPublic/visibility** (grep⌀) → R4/R7 cần field MỚI. **Mã NCC=Code** (Supplier.cs:7 comment + col4 TÊN-VIẾT-TẮT→Code). ⚠️**authz-mismatch:** import BE `[Roles=Admin,CatalogManager]` (`SuppliersController.cs:64,77`) vs FE btn gate `Suppliers/Create` (mở-all S59) → non-admin thấy nút→403; template sẽ kế-thừa. NO new menuKey/route. Tag `[s112, supplier-import-v2, fe-lens3, sha-mirror-drift-parentId, expectedheadertokens-template, no-ispublic-field, import-authz-mismatch]`. - **[→ git+session-log 07-07] S105 BVAAU cross-repo auth RCA:** Q1 root-cause = FE Home MÙ session (bundle cũ relative `/api` trên site TĨNH `web.config` loại `^/api/` → `/api/auth/me` 404 → anonymous); fix API_BASE-absolute `51c69fb` LIVE. Q2 CRM1 hở 2 tầng (FE soft + API `enforceCrmGate:false`) → flip `AUTH_ENFORCE_CRM_GATE=true`. Q3 Zalo "User_ID" KHÔNG có UI. Class **"hub+site-tĩnh relative-`/api` chết im prod"**; #53-garble-7 → SendMessage-resume recover. *(verbatim → archive/2026-07.md)* - **[→ 07-12 LENS-3 entry above + git 07-06] S104 Supplier 30-field 🅰️ schema-invest WF `wf_5ac2cdb0`:** SUPERSEDED anchors giữ: ClosedXML 0.105.0 = Excel-standard (KHÔNG OpenXml-raw, `csproj:8`); reuse-existing-cols>parallel; `UpdateSupplierCommand.cs:49-57` blind-set=latent#73; seed fill-nulls-if-exists (`DbInitializer.cs:2781`); `SupplierDto` hand-projected 2-site (`GetSupplierQuery.cs:21`+`ListSuppliersQuery.cs:48-50`); `Dialog.tsx:48` max-h-70vh; extract `ui/PathLink.tsx`. Chốt Mig 62 +18 nullable. *(verbatim → archive/2026-07.md)* diff --git a/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md index 023deac..0300008 100644 --- a/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md +++ b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/run.md @@ -8,8 +8,8 @@ owner: anh Kiệt (FDC) Cho phép chọn **≥2 NCC-TP trúng thầu vào 1 hạng mục** (PurchaseEvaluationDetail) trong phiếu Duyệt NCC, thay vì hiện tại 1 NCC/hạng mục. Mỗi NCC giữ **1 giá riêng**. Bảng so sánh giá hỗ trợ chọn nhiều NCC. ## taskList snapshot (6-bước anh Kiệt) -- [!] **B1** fable-clone invest ensemble (4-lane inv-cb) → spec — `wf_dd49ddc0-f29` RUNNING -- [ ] **B2** fable-clone review (reviewer ensemble) → điều chỉnh → spec cuối +- [x] **B1** fable-clone invest ensemble (4-lane inv-cb) → spec — `wf_dd49ddc0-f29` DONE (4/4 clean) +- [!] **B2** CHỜ anh Kiệt chốt D1-D5 → fable-clone review (reviewer ensemble) → spec cuối - [ ] **B3** hmw Opus 4.8 MAX implement (BE + FE 2-app + test) - [ ] **B4** fable-clone reviewer verify implementation - [ ] **B5** deploy + commit (cicd-monitor verify) @@ -21,8 +21,12 @@ Cho phép chọn **≥2 NCC-TP trúng thầu vào 1 hạng mục** (PurchaseEval - `invest:fe-consumer` — FE 2-app consumer audit (PeDetailTabs/HangMucCard/QuoteDialog/Bảng so sánh) - `invest:product-history` — Mig 58/59 multi-winner history + product decision-points -## Synthesis -_(pending — em-main fill sau khi 4 lane return + verify/refute)_ +## Synthesis (4-lane hội tụ) +- **3 trục winner:** A `PE.SelectedSupplierId` (single legacy) · B `Supplier.IsWinner` (Mig 58 liên-danh cả gói) · **C `Quote.IsSelected`** (per hạng-mục×NCC, Mig 12 DORMANT). **Feature = trục C**, KHÁC Mig 58 (trục B). +- **Hook `Quote.IsSelected` sẵn từ Mig 12** — persist + echo DTO + seed, mỗi cell có ThanhTien riêng, nhưng 0 consumer tài-chính → storage ≈ 0 migration, chỉ wire + re-key aggregation. +- **Cardinality-change lần 3** (S87 single→multi sót 2 read-site, S88 bắt). 6 BE read-site tài-chính + ~8 FE key off `IsWinner` → grep MỌI consumer. +- 4 lane token: schema 117.9K · be-consumer 130.9K · fe-consumer 100.6K · product-history 148.7K. Tổng 498K, 0 empty/error. Model `claude-opus-4-8` ×4. +- Engine stamp: `[engine: fable-clone-ensemble · Opus-S114]` (em-main harvest @closeout). ## Spec-file -_(pending — `spec-multi-ncc-per-hangmuc-12-07-2026.md` sau synthesis)_ +`spec-multi-ncc-per-hangmuc-12-07-2026.md` — Bước 1 kết-tinh (3-mục: đề-bài+ground-truth · cách-implement decision-gated · checklist C1-C6). D1-D5 CHỜ anh Kiệt. diff --git a/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md new file mode 100644 index 0000000..bc5e6a8 --- /dev/null +++ b/.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/spec-multi-ncc-per-hangmuc-12-07-2026.md @@ -0,0 +1,66 @@ +# SPEC — PE multi-NCC per hạng mục (chọn ≥2 NCC-TP/1 hạng mục, mỗi nhà 1 giá riêng) + +> Bước 1 kết-tinh (fable-clone invest ensemble `wf_dd49ddc0-f29`, 4-lane investigator-codebase Opus 4.8). Lead verify + synthesize. **Decision D1-D5 CHỜ anh Kiệt chốt trước Bước 2 review + Bước 3 implement.** + +## ① Tính-năng / đề-bài +Hiện tại mỗi **hạng mục** (`PurchaseEvaluationDetail`) trong phiếu Duyệt NCC chỉ có winner ở **cấp phiếu** (1 NCC toàn phiếu, hoặc ≥2 NCC liên-danh cả gói Mig 58). Yêu cầu: cho phép **mỗi hạng mục chọn ≥2 NCC-TP trúng thầu riêng**, **mỗi NCC giữ 1 giá riêng**, Bảng so sánh giá hỗ trợ chọn nhiều NCC per hạng mục. + +### Ground-truth (4-lane đồng thuận) +- **3 trục winner:** (A) `PurchaseEvaluation.SelectedSupplierId` [PE.cs:28, single legacy] · (B) `PurchaseEvaluationSupplier.IsWinner` [PES.cs:28, Mig 58 = liên-danh whole-supplier] · (C) **`PurchaseEvaluationQuote.IsSelected`** [Quote.cs:15, per (Detail×Supplier), Mig 12 — **DORMANT**]. +- **Feature = trục C.** Mig 58 (trục B) KHÔNG giải quyết — đó là "N NCC thắng cả gói", khác "chia winner giữa các hạng mục". +- **`Quote.IsSelected` = hook tự nhiên:** đã persist (`PurchaseEvaluationDetailFeatures.cs:308/332`) + echo DTO (`PurchaseEvaluationDtos.cs:75`) + seed (`DbInitializer.cs:1257`), mỗi Quote có `ThanhTien` = giá riêng. Nhưng **0 consumer tài chính** đọc nó → storage sẵn, chỉ cần wire. +- **Quote unique = (DetailId, SupplierId)** [Config:134] → mỗi NCC tối đa 1 quote/hạng mục = 1 giá/NCC/hạng mục (đúng "mỗi nhà 1 giá riêng"). + +## ✅ Decision-points — LOCKED (anh Kiệt chốt S114, all recommended) +- **D1 = Cộng giá NCC được chọn** → `winnerQuoteTotal = SUM(Quote.ThanhTien các NCC IsSelected)`. **0 field mới.** +- **D2 = THAY THẾ** → `Quote.IsSelected` = nguồn-sự-thật; `Supplier.IsWinner` = **DERIVED** (NCC có ≥1 IsSelected quote), giữ cho luồng tạo-HĐ + display. +- **D3 = Giá chào gốc** → dùng `Quote.ThanhTien` sẵn có. **0 cột mới.** +- **D4 = 1 HĐ/NCC** → mỗi HĐ gộp hạng mục NCC đó thắng, giaTri = `SUM(IsSelected quotes của NCC)`. Giữ pattern S88. +- **D5 (default em) = submit-guard giữ `winnerQuoteTotal>0` toàn phiếu** (nay tính theo IsSelected), KHÔNG hard-require winner mỗi hạng mục (nới, cảnh-báo-mềm nếu cần). + +→ **Migration KẾT QUẢ: 0 bảng mới, 0 cột mới.** Chỉ wire `Quote.IsSelected` + re-key 6 BE read-site + FE + backfill phiếu cũ. Path ② dưới = ACTIVE. + +## ② Cách implement (recommended path — nếu D1=SUM, D2=THAY-THẾ, D3=giá-gốc, D4=1HĐ/NCC) + +### Schema / Migration +- **KHÔNG bảng mới.** Tái dùng `Quote.IsSelected` làm winner-truth per (hạng mục × NCC). +- (D3 nếu tách giá) → 1 AddColumn `Quote.WinnerPrice decimal?` (nullable, fallback ThanhTien). +- (D1 nếu chia KL) → 1 AddColumn `Quote.AllocatedKhoiLuong decimal?`. +- **Backfill** (D2 thay-thế): phiếu cũ có `Supplier.IsWinner=true` nhưng `Quote.IsSelected=false` → set `IsSelected=1` cho mọi quote của NCC IsWinner (mirror backfill Mig 58 :31-35), tránh phiếu cũ mất winner → total=0. + +### BE write-site (1 chỗ) +- `SelectPurchaseEvaluationWinnerCommand` [`PurchaseEvaluationDetailFeatures.cs:397-431`] — hiện set `Supplier.IsWinner` toàn phiếu. **Đổi/thêm:** command per (DetailId → list SupplierId) set `Quote.IsSelected` per cell. Derive `Supplier.IsWinner = có ≥1 IsSelected quote` (D2 giữ luồng tạo-HĐ). + +### 🔴 BE read-sites PHẢI re-key `Supplier.IsWinner` → `Quote.IsSelected` (anti-cardinality-trap — chống sót) +1. **winnerQuoteTotal** `PurchaseEvaluationFeatures.cs:1189-1196` → `SUM(Quotes.ThanhTien WHERE q.IsSelected)`. +2. **Submit-guard** `PurchaseEvaluationWorkflowService.cs:196-211` → total theo IsSelected. +3. **CCM-threshold-finalize (GATE TÀI CHÍNH)** `PurchaseEvaluationWorkflowService.cs:885-908` — `< CeoApprovalThreshold` strict → total theo IsSelected (sai = chặn/cho-qua sai quyền CCM). +4. **CreateContractFromEvaluation** `CreateContractFromEvaluationFeatures.cs:56` (winners) + giaTri `:84-93` → winners = NCC có ≥1 IsSelected; giaTri = SUM(IsSelected quotes của NCC đó). +5. **Budget-compare Block-B** `PurchaseEvaluationFeatures.cs:907-927` (currentProposalTotal + prevSelected) → re-key IsSelected. +6. **Projection display** List `:637` · Inbox `:758` · ListApproved `:188-192` (winner-names) + JOIN tên NCC `:560/:874/:1209` → gộp distinct NCC có ≥1 IsSelected; `SelectedSupplierId` mất nghĩa khi split. + +### FE 2-app (`PeDetailTabs.tsx` SHA-locked `2dca7cee` — patch byte-identical CẢ 2 app; QuoteDialog/HangMucCard = inline function) +- **DetailCard bảng NCC** `:2638-2792` — `isWinner = s.isWinner` [:2677] → đổi source per-cell (`q.isSelected`); `toggleWinner` [:2746] → mutation per-detail; styling/ring [:2687-2690] + `canDelete` [:2679] revisit. +- **NccSelectorRow Section 3 picker** `:941-1020` — `checked={s.isWinner}` [:998] picker phiếu-wide → hạ-cấp read-only summary hoặc rethink per-hạng-mục. +- **Read-sites vỡ nếu quên:** submit-guard `:195/:229` · Section 4 winner-names `:1955-1976` · canCreateContract `:1906` + confirm `:2182` · Section title `:344/:389` · List card `winnerSupplierNames` [types:144]. `PeWorkflowPanel.tsx:111/135`. +- **types drift:** `types/purchaseEvaluation.ts` fe-admin `66ffd707` ≠ fe-user `7de399e1` (cosmetic badge-tint) → **patch delta**, KHÔNG blind-copy nguyên file. + +### Test (test-before — financial algorithm, rules §7) +- ~6-7 file khoá cứng semantics whole-supplier sẽ ĐỎ: `PeMultiWinnerTests` · `PeWorkItemBudgetTests(Block-B)` · `CreateContractFromEvaluationMultiWinnerTests` · `PeCcmThresholdFinalizeTests` · `PeSubmitGuardAndBypassTests` · `PeSelectWinnerClearTests` (+ `PeListWinnerNamesProjectionTests`). Update đồng-commit + thêm test per-hạng-mục total. + +## ③ Checklist (vai · deliverable · acceptance ĐO ĐƯỢC) +| # | Vai | Deliverable | Acceptance đo được | +|---|---|---|---| +| C1 | database-agent (review) | Xác nhận reuse Quote.IsSelected + backfill strategy + (D1/D3 cột mới nếu cần) | Migration design review PASS, Down reversible | +| C2 | implementer-backend | Command per-detail set IsSelected + derive IsWinner + re-key 6 read-site + Migration (nếu cột mới) + backfill | `dotnet build` 0 error; 6 read-site đọc IsSelected (grep 0 sót `.IsWinner` ở aggregation) | +| C3 | implementer-frontend | FE 2-app: DetailCard per-cell winner toggle + picker + wean read-sites khỏi s.isWinner | `npm build` ×2 PASS; PeDetailTabs SHA-identical 2 app; chọn ≥2 NCC/hạng mục OK | +| C4 | test-specialist | Update 6-7 test + thêm test winnerQuoteTotal per-hạng-mục (split-award SUM đúng) | test xanh; test mới cover ≥2 NCC/hạng mục + backfill + threshold-gate | +| C5 | reviewer | Verify 6 read-site re-key đủ (không sót) + total đúng + kế-thừa-HĐ N-winner | PASS: grep-suite 0 sót IsWinner-aggregation; giaTri HĐ = SUM won-lines | +| C6 | cicd-monitor | Deploy verify bundle rotate + Mig prod + smoke PE | Run PASS; byte-verify feature string | + +## Rủi ro then chốt +- **Silent total-lệch:** 6 read-site tài chính — sót 1 = số sai runtime, build/test cũ vẫn PASS (S87/S88 chính vụ). +- **R3 CCM-threshold = gate tài chính:** over/under-count → chặn/cho-qua sai quyền CCM duyệt-miễn-CEO. +- **Double-count nếu coexist** (D2): supplier-IsWinner SUM mọi quote vs quote-IsSelected → 2 nguồn lệch. +- **Backfill phiếu cũ** (D2 thay-thế): quên → phiếu prod mất winner, total=0. +- **DbInitializer seed :1167-1257** đã set IsSelected per-cell → bật aggregation IsSelected = tổng seed đổi ngay khi deploy (cần verify).