[CLAUDE] Docs: S114 PE multi-NCC per hạng mục closeout (STATUS/HANDOFF/gotcha #81/Mig 65/session-log)

Closeout feature multi-NCC per hạng mục (ship d436af2):
- STATUS/HANDOFF: S114 blurb + CURRENT STATE canonical Mig 64→65 · test 477→486 · gotcha 80→81 · bundle D51OYyGV/BVdssm5S.
- gotcha #81: derived-invariant re-derive sót DELETE writer (DeleteQuote/DeleteDetail) → IsWinner kẹt stale → HĐ giaTri=0 phantom (Bước 4 fable-clone verify bắt; fix helper PeWinnerInvariant test-before).
- ef-core skill: +Mig 65 BackfillPeQuoteIsSelectedAndWinnerInvariant row.
- session log 2026-07-12-S114 + 2 reviewer diary harvest (verify run) + run-trace fold.
- cicd on-behalf record (garble #53 ×5 → em-main-solo deploy verify).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-12 23:45:08 +07:00
parent d436af2971
commit 16a199f031
11 changed files with 108 additions and 24 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,32 @@
# 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).

View File

@ -1340,6 +1340,14 @@ for h in resp.points: # ← .points không phải iterable trực tiếp
**Fix:** `Code = (r.Code ?? string.Empty).Trim()` null + "" đồng nhất Code="" + IsPublic=false (nháp, ẩn khỏi filtered-unique `[Code]<>''`). **Quy tắc:** `!` null-forgiving trên dữ-liệu-parse-từ-ngoài (Excel/JSON/form-input) = MÌN dùng `?? default` chứ KHÔNG `!`. Build-0/0 KHÔNG bảo-đảm null-safety khi `!` che. Chỉ **test-before-critical** bắt được (test-specialist viết characterization-test reproduce NRE em-main fix prod flip test sang NotThrow+draft). Liên-quan #73 (absolute-set null-clobber).
### 81. Derived-flag invariant re-derive ở write/select NHƯNG SÓT DELETE writer → giá-trị-phái-sinh kẹt stale → phantom (Session 114)
**Triệu chứng:** S114 đổi winner-truth singlemulti per-hạng-mục `Supplier.IsWinner` thành **DERIVED** (== `Any(Quote.IsSelected)`), maintain SelectWinner + UpsertQuote(insert+update) + seed + backfill. NHƯNG **DeleteQuote + DeleteDetail (cascade) xóa quote-selected KHÔNG re-derive IsWinner** xóa quote-selected CUỐI của 1 NCC để `IsWinner` kẹt=true trong khi `Any(IsSelected)=false`. Hậu quả TÀI CHÍNH: `CreateContractFromEvaluation Where(s.IsWinner)` gom NCC phantom sinh **HĐ giaTri=0** cho NCC không-winner + winner-names phantom + FE row kẹt (`canDelete=!isWinner`). Build+test PASS, silent (0 test gọi Delete command).
**Fix:** helper `PeWinnerInvariant.ReSyncAfterQuoteRemovalAsync` (re-derive IsWinner toàn phiếu + re-sync SelectedSupplierId từ DB **đã-flush** gọi SAU SaveChanges nên không cần loại-trừ quote-đã-xóa) gọi CẢ DeleteQuote + DeleteDetail. Test-before: 2 test REDGREEN + guard-test (xóa hạng mục khác KHÔNG over-clear).
**Bài học:** derived-invariant phải re-establish **MỌI mutation đổi tập-nguồn — kể cả DELETE + cascade-delete**, không chỉ write/select. **Enumeration writer trong spec KHÔNG đáng tin** grep độc-lập MỌI `.Remove(` chạm entity chứa field-nguồn (S87/S88 cardinality class mở rộng lên tầng mutation-path: writer-miss read-site-miss). Chỉ **verify adversarial-refute** bắt (invest/review/implement đều sót cùng theo enumeration). Liên-quan #72/#73 (cardinality) + `feedback_cardinality_change_grep_consumers`.
---
## Checklist debug bug mới