[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:
@ -16,7 +16,9 @@
|
||||
- [S111 PE sign-off 6-decision design review (schema/history lens)](project_pe_signoff_history_schema_review.md) — GO-WITH-ADJ; Decision-5 opinion-history: RECOMMEND (a) append-only table, REJECT (b) version-col (drops UNIQUE + breaks UPSERT read every site), (c) Changelog zero-mig fallback. Hook @ ApproveV2Async UPSERT :784-790. Decision-3 caller-safe (SlaExpiryJob=Contract-only, not PE). A1 breaks 4 bypass tests.
|
||||
- [PE multi-NCC per-hạng-mục cardinality#3 review (consumer-completeness lens)](project_pe_multiwinner_perhangmuc_review.md) — PASS_WITH_ADJ; 6 BE read-site inventory COMPLETE (grep 1:1, no sót — unlike S87). 2 MUST: (1) giaTri isSingle/detailsSum branch silent-financial under per-hạng-mục; (2) derive-completeness — seed:1310-1316 sets IsSelected never IsWinner → seeded phiếu empty names + CreateContract throws. Lesson: derived source-of-truth = EVERY write-path of base field must re-derive.
|
||||
- [S113 Supplier import v2 close-review (owner anh Kiệt)](project_s113_supplier_import_v2_review.md) — GO-WITH-ADJ 2 must-fix: consumers (PeDetailTabs:2222 + ContractCreatePage:321 ×2app) DON'T pass published=true → drafts leak into pickers (R4/R6 "ẩn" undelivered) + CreateSupplier:86 missing IsPublic=true. Dedup/publish/mig/authz SOLID. Lesson: "filter added" ≠ "drafts hidden" — grep-all-consumer + create write-path.
|
||||
- [S114 PE multi-NCC per-hạng-mục BE-financial close-review](project_s114_pe_multi_ncc_perhangmuc_financial_review.md) — NEEDS-FIX 1 MUST: 6 financial-SUM đều→IsSelected (grep độc-lập, 0 sót) NHƯNG DeleteQuote:396 + DeleteDetail:262(cascade) KHÔNG re-derive IsWinner → stranded winner → phantom HĐ GiaTri=0 @CreateContract:56 + FE row kẹt; reachable "Xóa" btn:2897; 0 test. Lesson: derived-invariant re-establish MỌI mutation kể cả DELETE.
|
||||
- [PE multi-NCC per-hạng-mục spec review (winner-truth cardinality lần 3)](project_pe_multi_ncc_perhangmuc_specreview.md) — PASS_WITH_ADJ. MUST: CreateContract isSingle→detailsSum (`:76-77,89-90`) over-count khi 1-winner≠cả-gói, phá `Σgiari==winnerQuoteTotal`; spec cite `84-93` LOẠI isSingle = bẫy partial-re-key S87. SHOULD: derive-integrity 3 write-path (winner-cmd + UpsertQuote:308/332 + seed:1257 KHÔNG set IsWinner). 6-site IsWinner grep ĐỦ (0 sót). Lesson: grep write-path field-NGUỒN (IsSelected) không chỉ field-đọc; cite-range bọc branch-decision.
|
||||
- [S114 PE multi-NCC per-hạng-mục IMPL review (winner-truth flip lần 3)](project_s114_multi_ncc_perhangmuc_impl_review.md) — NEEDS-FIX. CAUGHT MUST: DeleteQuote (`PurchaseEvaluationDetailFeatures.cs:371-397`) KHÔNG re-derive IsWinner → xóa quote IsSelected cuối (nút Xóa FE:2897) VỠ invariant → phantom winner + HĐ giá 0 (CreateContract:56 Where(IsWinner)). Spec A2 enumerate writer sót DeleteQuote. Lesson: cardinality grep-consumer PHẢI gồm Delete-handler field-nguồn; spec-enum writer KHÔNG đáng tin. SOLID: 5 SUM IsSelected + test non-tautology (threshold-cross 1.2tỷ/500tr, giaTri 300/800/2000) + 2-app hash ae3788e9.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
---
|
||||
name: project-s114-multi-ncc-perhangmuc-impl-review
|
||||
description: S114 PE multi-NCC per-hạng-mục IMPL review (winner-truth flip lần 3) — CAUGHT MUST DeleteQuote invariant-break missed writer
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
S114 (2026-07-12) close-review PE multi-NCC per hạng mục — winner-truth flip whole-supplier `Supplier.IsWinner` → per-(Detail×Supplier) `Quote.IsSelected` (IsWinner nay DERIVED). Lens refute-fe-test. Verdict NEEDS-FIX (1 MUST).
|
||||
|
||||
**CAUGHT MUST (missed writer — S87/S88 cardinality anti-pattern lần 3):** `DeletePurchaseEvaluationQuoteCommandHandler` (`PurchaseEvaluationDetailFeatures.cs:371-397`) KHÔNG re-derive `supplier.IsWinner` sau `Remove(quote)`. Spec A2 liệt kê writer "SelectWinner + UpsertQuote(update+insert) + seed" — **DeleteQuote vắng mặt trong enumeration** → implementer theo spec faithfully nhưng spec sót. Xóa quote IsSelected cuối của 1 NCC qua nút "Xóa" (FE wired `PeDetailTabs.tsx:2897`) → invariant `IsWinner==Any(IsSelected)` VỠ → phantom winner: winnerSupplierNames projection (`where sw.IsWinner`) hiện NCC + `CreateContractFromEvaluationFeatures.cs:56` `winners=Where(IsWinner)` tạo **HĐ giá 0** (giaTri=SUM selected=0, :88-90). Reachable Draft-phase, sống tới DaDuyet.
|
||||
|
||||
**Lesson:** cardinality-change grep-consumer PHẢI gồm cả writer XÓA (Delete handler) của field-nguồn, KHÔNG chỉ Select/Upsert. Field-nguồn = `Quote.IsSelected` → mọi path đổi TẬP quote (thêm/sửa/**xóa**) phải re-derive flag phái sinh. Spec-enumeration của writer KHÔNG đáng tin — grep độc lập MỌI handler chạm entity (grep `Remove(.*Quote`/`db.*Quotes.Remove`).
|
||||
|
||||
**SHOULD:** (1) derive-invariant test (`PeSelectWinnerClearTests.cs:566`) chỉ cover UpsertQuote INSERT + SelectWinner; KHÔNG cover UpsertQuote UPDATE-path (exclusion `q.Id != existing.Id` :313-316 — subtle stale-DB-read guard) NÊN test-before-fix cũng thiếu DeleteQuote. (2) FE winner-toggle (`:2707`) không gate trên `q` tồn tại → chọn winner NCC chưa nhập giá = silent no-op + toast success sai (BE chỉ flip IsSelected quote CÓ SẴN :433-434).
|
||||
|
||||
**Verified SOLID (resisted scrutiny):** FE 2-app byte-identical `ae3788e9` (hash-object khớp). 5 financial-SUM đều `WHERE IsSelected` (winnerQuoteTotal/submit-guard/CCM-threshold/giaTri/budget-B). giaTri A1 bỏ detailsSum single+multi. **Test discriminate THẬT (non-tautology):** threshold-crossing dựng whole-supplier 1.2 tỷ≥ngưỡng vs selected 500tr<ngưỡng; giaTri-subset 300 vs 800(all-quote) vs 2000(budget). SelectWinner derive IsWinner toàn phiếu (giữ winner hạng mục KHÁC). Seed set IsSelected=true (`DbInitializer.cs:1385`) → derive IsWinner đúng, KHÔNG empty-winner regression (concern plan-review cũ đã đóng).
|
||||
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: s114-pe-multi-ncc-perhangmuc-financial-review
|
||||
description: S114 PE multi-NCC per-hạng-mục (winner-truth Quote.IsSelected, IsWinner derived) — refute-BE-financial lens; NEEDS-FIX 1 MUST (delete paths strand invariant)
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
# S114 PE multi-NCC per-hạng-mục — refute-BE-financial (fable-clone reviewer lane)
|
||||
|
||||
Verdict **NEEDS-FIX** (1 MUST). Winner-truth đổi whole-supplier `Supplier.IsWinner` → per-hạng-mục `Quote.IsSelected`; IsWinner nay DERIVED == Any(quote.IsSelected).
|
||||
|
||||
## Financial SUM axis = CORRECT (đã verify grep độc lập)
|
||||
6 SUM site đều key `q.IsSelected` (không sót IsWinner-keyed sum nào toàn `src/Backend`): winnerQuoteTotal `PeFeatures.cs:1188` · currentProposalTotal:920 · prevSelectedTotal:909 · submit-guard `WorkflowService.cs:196` · CCM-threshold:882 (`>= ceoThreshold` boundary GIỮ, đúng) · giaTri per-winner `CreateContract:88`. Grep `IsWinner` toàn BE = CHỈ existence/name-projection (List:186, PeFeatures:637/758, "đã chọn thầu":907) + entity/config/migration — KHÔNG financial-sum. Backfill 2-chiều idempotent + total-preserving. Seed derive + SelectWinner derive + SelectedSupplierId sync đúng.
|
||||
|
||||
## MUST — A2 invariant KHÔNG maintain trên DELETE paths
|
||||
`supplier.IsWinner == supplier.Quotes.Any(IsSelected)` chỉ re-derive ở SelectWinner/UpsertQuote(ins+upd)/seed/backfill. **DeleteQuote `PurchaseEvaluationDetailFeatures.cs:396` + DeleteDetail:262 (cascade Detail→Quotes, config line118) xóa selected-quote KHÔNG re-derive IsWinner** (cũng không re-sync SelectedSupplierId). Xóa quote-selected cuối của 1 NCC → IsWinner stranded=true, 0 selected. Reachable FE: quote-dialog "Xóa" btn `PeDetailTabs.tsx:2897` (unconditional, mọi existing quote) + xóa-hạng-mục. Hậu quả: CreateContract `:56` winners chứa phantom → HĐ GiaTri=0 cho NCC không-winner + winner-name/existence sai + FE row kẹt (canDelete=`!isWinner && !hasQuotes`:2640 → không xóa được). Self-heal 1-phần: SelectWinner/UpsertQuote kế tiếp re-derive, NHƯNG không đảm bảo trước CreateContract. **0 test** (không test-source nào gọi DeletePurchaseEvaluationQuote/DetailCommand).
|
||||
|
||||
## Lesson
|
||||
Derived-invariant (IsWinner từ IsSelected) phải re-establish ở **MỌI mutation đổi tập nguồn — kể cả DELETE + cascade-delete**, không chỉ write/select. Acceptance liệt kê maintenance-point (SelectWinner+Upsert+seed+backfill) BỎ SÓT delete = đúng chỗ reviewer phải bắt (S87/S88 class mở rộng: mutation-path miss ≡ read-site miss). Grep write-path field-NGUỒN + đếm MỌI `.Remove(` chạm entity chứa nguồn.
|
||||
Reference in New Issue
Block a user