# S114 — PE multi-NCC per hạng mục (2026-07-12)

**Feature ship prod:** cho chọn **≥2 NCC-TP trúng thầu vào 1 hạng mục** (PurchaseEvaluationDetail), mỗi nhà 1 giá riêng. Anh Kiệt (FDC). Commit `d436af2`. em-main-led, 6-bước theo plan anh, HMW-ON, lead=Opus 4.8 owner-choice.

## Flow 6 bước (fable-clone → hmw → verify)
1. **B1 `/fable-clone invest`** — 4-lane investigator-codebase ensemble (`wf_dd49ddc0`, schema/be-consumer/fe-consumer/product-history). Phát hiện **3 trục winner**: A `SelectedSupplierId` (single legacy) · B `Supplier.IsWinner` (Mig58 liên-danh cả-gói) · **C `Quote.IsSelected`** (per Detail×Supplier, hook Mig12 DORMANT — 0 consumer tài-chính). Feature = trục C, KHÁC Mig58.
2. **B2a anh Kiệt 4-decision** (AskUserQuestion, all recommended): **D1**=winnerQuoteTotal SUM(IsSelected) · **D2**=THAY-THẾ (IsSelected nguồn-sự-thật, IsWinner DERIVED) · **D3**=giá-chào-gốc Quote.ThanhTien · **D4**=1HĐ/NCC. → **0 bảng/cột mới**.
3. **B2b `/fable-clone review`** — 3-lane reviewer ensemble (`wf_1e127f4b`, consumer-completeness/correctness/checklist-risk). 6 adjustment A1-A6: A1 giaTri bỏ isSingle→detailsSum · A2 derive-invariant MỌI writer + backfill 2-chiều · A3 re-key SPLIT (5 financial→IsSelected, winner-names/existence GIỮ IsWinner-derived) · A4 endpoint per-detail + FE per-cell grid · A5 test-before + threshold-crossing · A6 mig 3-file Down-noop. *(lane correctness-edgecase #53-garble → em-main recover, territory phủ bởi 2 lane hội-tụ.)*
4. **B3 hmw implement** — staged BE→(FE+test) (`wf_e7ee6abe` stage-1 + `wf_410896c4` stage-2). BE: command `{detailId, supplierIds}` set Quote.IsSelected + derive IsWinner (SelectWinner + UpsertQuote + seed); re-key 5 financial-SUM; giaTri bỏ detailsSum; Mig 65 backfill 2-chiều. FE: HangMucCard per-cell winner grid + toggle per-detail; NccSelectorRow hạ-cấp summary; mirror 2-app SHA `ae3788e9`. Test: 7 file update + tests mới.
5. **B4 `/fable-clone verify`** — 2-lane reviewer refute (`wf_14850736`, CLEAN 0-garble). **HỘI TỤ 1 MUST bug** invest/review/implement đều sót (gotcha #81, dưới). em-main fix test-before.
6. **B5 deploy** commit `d436af2` push → CI. **B6 session-end** (file này).

## 🎯 MUST bug Bước 4 bắt (gotcha #81)
`DeleteQuote` + `DeleteDetail` (cascade) xóa quote-selected KHÔNG re-derive `Supplier.IsWinner` → xóa quote-selected cuối của NCC để `IsWinner` kẹt stale (Any(IsSelected)=false) → `CreateContract Where(IsWinner)` gom NCC **phantom** → sinh **HĐ giaTri=0** + winner-names sai + FE row kẹt. Reachable FE nút "Xóa" quote. **0 test cover.** Fix: helper `PeWinnerInvariant.ReSyncAfterQuoteRemovalAsync` (re-derive từ DB đã-flush) gọi ở 2 delete handler. Test-before: 2 test RED→GREEN + guard (không over-clear) + UpsertQuote-update coverage. **Bài học meta:** enumeration writer trong spec KHÔNG đáng tin — verify adversarial-refute grep-MỌI-`.Remove(` bắt (S87/S88 cardinality lên tầng mutation-path).

## ⚠️ #53 return-garble ×5 phiên này (0 mất việc)
review-lane-2 + BE-lane + FE-lane + test-lane + cicd — tất cả garble RETURN (sub xong việc, không gọi StructuredOutput). **Recovery khác theo loại lane:**
- **READ-only lane** (review/verify): garble = mất verdict → recover bằng **redundancy** (lane sạch phủ territory).
- **WRITE lane** (BE/FE/test): garble = mất return NHƯNG **code trên disk** → recover bằng **disk-truth + build/test verify** (`feedback_agent_kill_recovery`).
- **cicd**: garble (trả memory-curation) → **em-main-solo verify** (curl prod bundle rotate + byte-verify + smoke).
**Lesson kỹ-thuật Workflow-tool:** stage-1 `await agent()` TRỰC-TIẾP → 1 garble throw giết cả workflow (FE+test stage-2 không chạy). `parallel()` = garble→null, workflow SỐNG → verify từ disk. **Với WRITE-lane, return chỉ "nice-to-have", disk mới ground-truth.** FE-lane garble giữa mirror → chỉ fe-admin xong, em-main mirror fe-user (SHA verify).

## State
**Mig 65 · 89 bảng · 486 test (45D+441I, +9) · gotcha 81 · menu 54 · bundle admin `D51OYyGV`/user `BVdssm5S` · prod 37 NCC/17 user.**

## Deploy verify (em-main-solo, cicd garble)
bundle admin `acprMqfB`→`D51OYyGV` / user `Ck3BTDdA`→`BVdssm5S` rotate ×2 · byte-verify `detailId`+`select-winner`+`hạng mục` PRESENT (bundle 1.65MB real) · smoke api/admin/eoffice 3/3 200 · endpoint POST select-winner bogus 401-live · mig applied (API-healthy = migrations-ran-startup).

## Carry / follow-up
- **UAT anh Kiệt (incognito):** giaTri single-winner budget→bid · UX per-cell grid · nit winner-no-quote no-op.
- **NEXT em:** follow-up nit FE winner-toggle gate quote-tồn-tại (non-blocking) · inv-cb L1 98.6% cap WATCH · watch AI_INFRA 2 email · monthly re-tier 2026-08-01.
- **Run-trace:** `.claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/` (run.md + spec-multi-ncc-per-hangmuc-12-07-2026.md).
