-**Testsbaseline:****477 PASS**(45Domain+432Infra·0fail/skip;S113Run#383sha`5fa11b5`—458→477incl+19new`SupplierPublishAndListTests`+`SupplierImportV2DedupTests`;prior458[S112#49645D+413I+10SupplierExcelImportServiceTests],440,436,434,421).Phase9UATskipperchunkOK.🔑**Anon log endpoint `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (HTTP 200 ~18KB) EXPOSES per-project `Passed! - Failed: 0, Passed: N, Skipped: 0` — concrete test count now available, NOT just status=success (the `/api/v1/.../tasks/{id}/logs` variant 404s; use web-UI runs/{run_number}/jobs/0/logs).**
-**Miglatestrepo:****Mig 64 `20260712110556_AddSupplierPublishState`**(S113#383`5fa11b5`;AddColumn`IsPublic`bitNOTNULLDEFAULT0on`Suppliers`+backfill`UPDATE Suppliers SET IsPublic=1`all-rows+DropIndex/CreateIndex`IX_Suppliers_Code`filter→`[IsDeleted]=0 AND [Code]<>''`;NOCreateTable⟹sys.tablesstays89—VERIFIED-APPLIED-PRODhistory-top==`AddSupplierPublishState`+`IsPublic`-col-present=1+Suppliers37/37PUBLIC=1+sys.tables=89).PrevMig63`AddSupplierImportSourceFields`(S112#496,2AddColumnprovenance).PrevMig62`ExpandSupplierFields`(S104#488,AddColumn×18Supplier9→27+un-cram4-NCCNote).PrevMig61`BackfillEndedByLevelFinalizeFromChangelog`(S97,data-only).Path`src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`.Prodcheck`sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`.⚠️**`sys.tables`(is_ms_shipped=0)=89**(S104LIVE-verifiedpost-#488—AddColumn/data-onlymigsdon'tchangecount→committouchesnonewtable⟹89correctdon'tFAIL.Cross-refCOMMITscopevsambientcount).enum-additive(e.g.`ApprovalAttachment=5`)=int-storedNO-mig→historyNOT-advance+empty`git diff -- '*Migrations*'`.
-**Bundlehashlive(S113#383`5fa11b5`2026-07-12—REAL-FE-SHIProtate×2):**adminjs**`acprMqfB`**/css**`CfunQHbd`**·userjs**`Ck3BTDdA`**/css**`DGqqHQXS`**—BOTHjs+cssrotated(from#496admin`Cixlt_MA`/`Ds-E5neu`·user`BhBOG1dn`/`Beb0Bb1h`)=expected,real`fe-*/src`change(SupplierImportDialog+SuppliersPage+PeDetailTabs+ContractCreatePagebothapps).Ship-proof DEFINITIVE:real-sizeadmin1,656,263b/user1,562,611b(vs919bSPA-trap)+`application/javascript`+LM12:07:50/12:08:43GMT(=19:07/19:08+07in deploy-window19:03:57→19:09:11)+byte-verifystrings`Tải file mẫu`×1/`Đã công bố`×3/`Nháp (ẩn)`×4present(gotcha#77not-cache).⚠️**id ≠ run_number; head_sha-keyed.**⚠️**PER #69 TWO-MODE: hash MAY rotate (realsrc-diff #488/#496, HOẶC 0-fe-src #489/#312) OR freeze (0 fe-src #485-487) — SNAPSHOT per-run, NOT baseline; ALWAYSre-snapshotLIVE pre-deploy.**RealFEship =diff`fe-*/src`inrange+real-size-vs-fake+Last-Modified-in-window+content-marker-grep,NOThash deltaalone.
-**Testsbaseline:****486 PASS**(45Domain+441Infra·0fail/skip;S114Run#384sha`d436af2`—477→486incl+9PEmulti-NCC[PeMultiWinnerTests/PeSelectWinnerClearTests/PeListWinnerNamesProjectionTests/CreateContractFromEvaluationMultiWinnerTests+3modifiedPeWorkItemBudget/PeCcmThresholdFinalize/PeSubmitGuardAndBypass];prior477[S113#38345D+432I],458,440,436,434,421).Phase9UATskipperchunkOK.🔑**Anon log endpoint `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (HTTP 200 ~18KB) EXPOSES per-project `Passed! - Failed: 0, Passed: N, Skipped: 0` — concrete test count now available, NOT just status=success (the `/api/v1/.../tasks/{id}/logs` variant 404s; use web-UI runs/{run_number}/jobs/0/logs).**
-**Miglatestrepo:****Mig 65 `20260712142652_BackfillPeQuoteIsSelectedAndWinnerInvariant`**(S114#384`d436af2`;DATA-ONLYno-schema2-dirbackfillestablishingINVARIANT`PurchaseEvaluationSupplier.IsWinner ⟺ Any(Quote.IsSelected)`—winner-truthđổiwhole-supplierIsWinner→per-hạng-mụcQuote.IsSelected;idempotent,Down=no-op,**NO ModelSnapshot change**[chỉ2mig-file`.cs`+`.Designer.cs`=ĐÚNGchodata-only],SQL-Server-only[SQLitetestsEnsureCreatedkhôngreplay].VERIFIED-APPLIED-PRODhistory-top+invariant-violations=**0**[33sup/23winners]+sys.tables=89 unchanged).Prev**Mig 64 `AddSupplierPublishState`**(S113#383`5fa11b5`;AddColumn`IsPublic`bit+backfill37/37PUBLIC=1+Code-filtered-unique,sys.tables89,VERIFIED-APPLIED-PROD).PrevMig63/62/61(Supplierimport-fields/9→27expand/PEbackfill—seemigrationtable).Path`src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`.Prodcheck`sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`.⚠️**`sys.tables`(is_ms_shipped=0)=89**(S104LIVE-verifiedpost-#488—AddColumn/data-onlymigsdon'tchangecount→committouchesnonewtable⟹89correctdon'tFAIL.Cross-refCOMMITscopevsambientcount).enum-additive(e.g.`ApprovalAttachment=5`)=int-storedNO-mig→historyNOT-advance+empty`git diff -- '*Migrations*'`.
**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).
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.