[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

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