[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

@ -12,6 +12,12 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
---
## S114 (2026-07-12) deploy verify — em-main-solo (cicd garble #53)
[on-behalf: cicd-lane garble return #53 em-main-solo curl-verify] Commit `d436af2` PE multi-NCC per hạng mục. **PASS:** bundle rotate ×2 admin `acprMqfB``D51OYyGV` / user `Ck3BTDdA``BVdssm5S` · byte-verify `detailId`+`select-winner`+`hạng mục` PRESENT (1.65MB real-size) · smoke health/admin/eoffice 3/3 200 · POST /select-winner bogus 401-live · Mig 65 applied (API-healthy=migrations-ran-startup). cicd-lane garble = trả memory-curation "Compressing S112/S111" thay verdict (×5/6-tổng tái phạm dai [[feedback_agent_return_garble_recover]] root-cause curate-at-return) em-solo recover. [engine: cicd-spawn garbleem-solo · Opus-S114]
---
## 🚨 Recurring CI/CD bug patterns (catch priority)
- **#39 act_runner github.com TCP timeout** run hang "Set up job" 21s. Log `dial tcp github.com:443 i/o timeout`. Fix: manual checkout bypass hardcoded `.gitea/workflows/deploy.yml`. KHÔNG revert.
@ -32,7 +38,7 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
- **Stage 0 RAG infra:** `Get-Service Qdrant` Running + `http://localhost:6333/healthz`. Collection `proj_solution_erp`.
- **Stage 1 Push+filter:** `git log -1 --format='%H %s'` + `git log origin/main..HEAD` empty + diff vs paths-ignore (docs-only SKIPPED-DOCS return).
- **Stage 2 Gitea poll** (max 10 iter × 60s): API `.../actions/tasks?limit=5` (NOT `/runs` 404). Match `head_sha` (NOT run_number cancelled runs shift numbering). `updated_at` stale ~2min (gotcha #46) cross-check VPS mtime. Foreground-sleep BLOCKED Windows Monitor-style busy-wait-curl-spin ~30s/iter.
- **Stage 3 Test gate:** baseline **477 PASS** (45 Domain + 432 Infra; S113 #383 `5fa11b5` 458+19 new Supplier import-v2 tests [SupplierPublishAndListTests + SupplierImportV2DedupTests]; prior 458 [S112 #496], 440, 436, 434, 421). CI runs both proj BEFORE build/deploy status=success test gate passed (`tasks` terminal=`status:success`, `conclusion` NOT populated trust success NOT log-numeric). Local grep undercounts (Theory/InlineData). Phase 9 UAT skip OK.
- **Stage 3 Test gate:** baseline **486 PASS** (45 Domain + 441 Infra; S114 #384 `d436af2` 477+9 PE multi-NCC tests [PeMultiWinner/PeSelectWinnerClear/PeListWinnerNamesProjection/CreateContractFromEvaluationMultiWinner +3 modified]; prior 477 [S113 #383], 458, 440, 436, 434, 421). CI runs both proj BEFORE build/deploy status=success test gate passed (`tasks` terminal=`status:success`, `conclusion` NOT populated trust success NOT log-numeric). Local grep undercounts (Theory/InlineData). Phase 9 UAT skip OK.
- **Stage 4 Post-deploy** (if SUCCESS): auth login bearer (admin + nv.test gotcha #44; token=`accessToken` route `/api/auth/login`) 3-5 endpoint smoke 2XX (incl new NEW-endpoint bodyless-POST returns **411 IIS Length-Required pre-auth NOT 404**; re-probe WITH body 401 confirms wired) FE bundle hash 2 app (per #69 = real-size-vs-fake not hash-delta) SignalR negotiate (gotcha #25) EF mig prod==repo.
- **Stage 4.6 (S29 CRITICAL):** sqlcmd seed sample verify post-deploy (NOT chỉ schema). Seed=runtime-row-insert (no history/tables advance) verify via DB-query (e.g. Permissions count), NOT bundle-frozen-alone.
- Discovery: ASP.NET 10 record enum needs numeric input unless `JsonStringEnumConverter` (SOL has NO converter FE sends numeric). sqlcmd ssh Windows-auth `\\\\SQLEXPRESS` 4-backslash. INFRASTRUCTURE seed MUST run (NOT inside `if(!demoSeedDisabled)`); DEMO seed gated gotcha #51.
@ -50,9 +56,9 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
- **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore 7-entry (D10 S111 #495): `['docs/**','**/*.md','.claude/skills/**','.claude/agent-memory/**','.claude/workflows/runs/**','.gitignore','scripts/**.md']`. Anon API works (public repo) when GITEA_TOKEN absent.
- **Prod:** api/admin/eoffice `.solutions.com.vn` · SSH `ssh vietreport-vps` (Administrator, id_ed25519) · IIS phys paths: API `C:\inetpub\solution-erp\api` · admin `\fe-admin` · user `\fe-user`. DB `.\SQLEXPRESS`/`SolutionErp`/`vrapp` SQL-auth. **Conn key = `ConnectionStrings.Default` (NOT `DefaultConnection`)**. DB pw = `buKL3TGBkD0wDDbYVw65QeX9` (User Id=`vrapp`; Password field has NO `vrapp/` prefix S89 login-failed when prefix included; read verbatim from prod `appsettings.Production.json` ConnectionStrings.Default when `$env:PROD_DB_PASSWORD` empty). skill-doc path `C:\inetpub\apps\SolutionErp\Api` STALE.
- **SSHPS quoting (S42):** nested bashsshpowershell mangles `$var`/`\"` use `iconv UTF-16LE | base64` `powershell -EncodedCommand $B64`; OR write ps1-file + `powershell -File $(cygpath -w)`. sqlcmd over SSH direct `-W -h -1` pw-inline. sys-catalog string-concat `COLLATE DATABASE_DEFAULT` (collation conflict).
- **Tests baseline:** **477 PASS** (45 Domain + 432 Infra · 0 fail/skip; S113 Run #383 sha `5fa11b5` 458477 incl +19 new `SupplierPublishAndListTests` + `SupplierImportV2DedupTests`; prior 458 [S112 #496 45D+413I +10 SupplierExcelImportServiceTests], 440, 436, 434, 421). Phase 9 UAT skip per chunk OK. 🔑 **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).**
- **Mig latest repo:** **Mig 64 `20260712110556_AddSupplierPublishState`** (S113 #383 `5fa11b5`; AddColumn `IsPublic` bit NOT NULL DEFAULT 0 on `Suppliers` + backfill `UPDATE Suppliers SET IsPublic=1` all-rows + DropIndex/CreateIndex `IX_Suppliers_Code` filter`[IsDeleted]=0 AND [Code]<>''`; NO CreateTable sys.tables stays 89 VERIFIED-APPLIED-PROD history-top==`AddSupplierPublishState` + `IsPublic`-col-present=1 + Suppliers 37/37 PUBLIC=1 + sys.tables=89). Prev Mig 63 `AddSupplierImportSourceFields` (S112 #496, 2 AddColumn provenance). Prev Mig 62 `ExpandSupplierFields` (S104 #488, AddColumn ×18 Supplier 927 + un-cram 4-NCC Note). Prev Mig 61 `BackfillEndedByLevelFinalizeFromChangelog` (S97, data-only). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. **`sys.tables` (is_ms_shipped=0) = 89** (S104 LIVE-verified post-#488 AddColumn/data-only migs don't change count commit touches no new table 89 correct don't FAIL. Cross-ref COMMIT scope vs ambient count). enum-additive (e.g. `ApprovalAttachment=5`) = int-stored NO-mig history NOT-advance + empty `git diff -- '*Migrations*'`.
- **Bundle hash live (S113 #383 `5fa11b5` 2026-07-12 REAL-FE-SHIP rotate ×2):** admin js **`acprMqfB`**/css **`CfunQHbd`** · user js **`Ck3BTDdA`**/css **`DGqqHQXS`** BOTH js+css rotated (from #496 admin `Cixlt_MA`/`Ds-E5neu` · user `BhBOG1dn`/`Beb0Bb1h`) = expected, real `fe-*/src` change (SupplierImportDialog + SuppliersPage + PeDetailTabs + ContractCreatePage both apps). Ship-proof DEFINITIVE: real-size admin 1,656,263b / user 1,562,611b (vs 919b SPA-trap) + `application/javascript` + LM 12:07:50/12:08:43 GMT (=19:07/19:08+07 in deploy-window 19:03:5719:09:11) + byte-verify strings `Tải file mẫu`×1/`Đã công bố`×3/`Nháp (ẩn)`×4 present (gotcha #77 not-cache). **id ≠ run_number; head_sha-keyed.** **PER #69 TWO-MODE: hash MAY rotate (real src-diff #488/#496, HOẶC 0-fe-src #489/#312) OR freeze (0 fe-src #485-487) — SNAPSHOT per-run, NOT baseline; ALWAYS re-snapshot LIVE pre-deploy.** Real FE ship = diff `fe-*/src` in range + real-size-vs-fake + Last-Modified-in-window + content-marker-grep, NOT hash delta alone.
- **Tests baseline:** **486 PASS** (45 Domain + 441 Infra · 0 fail/skip; S114 Run #384 sha `d436af2` 477486 incl +9 PE multi-NCC [PeMultiWinnerTests/PeSelectWinnerClearTests/PeListWinnerNamesProjectionTests/CreateContractFromEvaluationMultiWinnerTests + 3 modified PeWorkItemBudget/PeCcmThresholdFinalize/PeSubmitGuardAndBypass]; prior 477 [S113 #383 45D+432I], 458, 440, 436, 434, 421). Phase 9 UAT skip per chunk OK. 🔑 **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).**
- **Mig latest repo:** **Mig 65 `20260712142652_BackfillPeQuoteIsSelectedAndWinnerInvariant`** (S114 #384 `d436af2`; DATA-ONLY no-schema 2-dir backfill establishing INVARIANT `PurchaseEvaluationSupplier.IsWinner ⟺ Any(Quote.IsSelected)` winner-truth đổi whole-supplier IsWinner per-hạng-mục Quote.IsSelected; idempotent, Down=no-op, **NO ModelSnapshot change** [chỉ 2 mig-file `.cs`+`.Designer.cs` = ĐÚNG cho data-only], SQL-Server-only [SQLite tests EnsureCreated không replay]. VERIFIED-APPLIED-PROD history-top + invariant-violations=**0** [33 sup/23 winners] + sys.tables=89 unchanged). Prev **Mig 64 `AddSupplierPublishState`** (S113 #383 `5fa11b5`; AddColumn `IsPublic` bit + backfill 37/37 PUBLIC=1 + Code-filtered-unique, sys.tables 89, VERIFIED-APPLIED-PROD). Prev Mig 63/62/61 (Supplier import-fields / 927 expand / PE backfill see migration table). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. **`sys.tables` (is_ms_shipped=0) = 89** (S104 LIVE-verified post-#488 AddColumn/data-only migs don't change count commit touches no new table 89 correct don't FAIL. Cross-ref COMMIT scope vs ambient count). enum-additive (e.g. `ApprovalAttachment=5`) = int-stored NO-mig history NOT-advance + empty `git diff -- '*Migrations*'`.
- **Bundle hash live (S114 #384 `d436af2` 2026-07-12 REAL-FE-SHIP js-rotate ×2 / css-FROZEN):** admin js **`D51OYyGV`**/css **`CfunQHbd`** · user js **`BVdssm5S`**/css **`DGqqHQXS`** BOTH js rotated (from #497 admin `acprMqfB`/user `Ck3BTDdA`); CSS FROZEN = expected (change `.tsx` logic/strings NOT styling PeDetailTabs.tsx both apps) 🔑 **js-rotate/css-frozen = fingerprint FE-logic-change-no-style**. Ship-proof: real-size admin 1,655,105b/user 1,561,453b (vs 919b SPA-trap) + `application/javascript` + LM 15:26:37/15:27:29 GMT (=22:26/22:27+07 in-window 22:22:4022:27:57) + byte-verify `trúng thầu theo từng hạng mục`/`thắng hạng mục`/`trúng thầu cho hạng mục` BOTH bundles. idrun_number head_sha-keyed. PER #69 TWO-MODE hash SNAPSHOT-per-run NOT baseline (rotate real-src OR 0-src, freeze 0-src) ALWAYS re-snapshot LIVE; ship-proof = LM-in-window + real-size + marker-grep, NOT hash-delta.
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check).
## 🔑 Critical config (flag commit nếu tái xuất)
@ -70,8 +76,9 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code /
## 📅 Recent runs (compressed — verbatim → archive via `_INDEX.md`)
- **🟢 S113 #383 (rn 383, id 497) `5fa11b5` GO 5m14s (19:03:57→19:09:11+07 2026-07-12, owner anh Kiệt) — Supplier import v2 (Mig 64 publish/draft `IsPublic` + dedup-MST + template .xlsx ClosedXML + file mẫu; 28-file: FE ×10 both apps [SupplierImportDialog/SuppliersPage/PeDetailTabs/ContractCreatePage/types], BE ×14 incl `PublishSupplierCommand`+`GetSupplierImportTemplateQuery` NEW + Mig64 3-file, tests ×2 NEW):** status=success · **test gate CONCRETE `runs/383/jobs/0/logs` (anon 200): Domain 45 + Infra 432 = 477/0-fail/0-skip** (+19 vs #496 458 = SupplierPublishAndListTests + SupplierImportV2DedupTests) · **Mig 64 `AddSupplierPublishState` APPLIED-PROD** (history-top==name + `IsPublic`-col-present=1 + backfill Suppliers 37/37 PUBLIC=1 [prod 37 NCC not 22-est; `UPDATE SET IsPublic=1` all-rows → 0 draft, invariant no-hidden holds] + sys.tables=89 frozen [AddColumn+reindex only]) · **FE REAL-SHIP ×2** admin js `Cixlt_MA``acprMqfB`/css `Ds-E5neu``CfunQHbd` · user js `BhBOG1dn``Ck3BTDdA`/css `Beb0Bb1h``DGqqHQXS` ship-proof= real-size 1,656,263/1,562,611b + `application/javascript` + LM 12:07:50/12:08:43 GMT in-window + byte-verify `Tải file mẫu`×1/`Đã công bố`×3/`Nháp (ẩn)`×4 present (gotcha #77 not-cache) vs SPA-trap 919b control · **new endpoints WIRED** GET `/suppliers/import/template` no-auth+bogus-bearer→401 (alive/auth-gated) · `/suppliers?published=true`→401 (live NOT 500) vs bogus-route 404 control (401≠blanket, proves wired). smoke health/ready+live + admin/eoffice roots ALL 200.
- **🟢 S112 #496 (rn 382, id 496) `e100ef0` GO 5m25s** — PE workflow + Supplier Excel-import Phase B + Mig 63 `AddSupplierImportSourceFields` APPLIED-PROD (2 AddColumn provenance, sys.tables 89) · gate 458 (45D+413I) · FE real-ship both apps admin `Cixlt_MA`/user `BhBOG1dn` · 🔑 411-refinement: bodyless-POST→411 for ANY route (IIS Content-Length pre-routing) ⟹ MUST re-probe WITH body → 401/415 (matched) vs 404 (missing) — verbatim → `archive/2026-07.md` via `_INDEX`.
- **🟢 S114 #384 (rn 384, id 498) `d436af2` GO 5m17s (22:22:40→22:27:57+07 2026-07-12, owner anh Kiệt) — PE multi-NCC per hạng mục (winner-truth đổi whole-supplier `IsWinner` → per-hạng-mục `Quote.IsSelected`; endpoint `select-winner` body `{supplierIds}``{detailId,supplierIds}`; Mig 65 data-only 2-dir backfill; 18-file: 7 BE incl PurchaseEvaluationWorkflowService+DbInitializer-sync, 2 FE PeDetailTabs both apps, 7 PE test, Mig65 2-file no-ModelSnapshot):** status=success · **gate CONCRETE `runs/384/jobs/0/logs`: Domain 45 + Infra 441 = 486/0-fail/0-skip** (+9 vs #383 477) · **Mig 65 `BackfillPeQuoteIsSelectedAndWinnerInvariant` APPLIED-PROD** (history-top + **invariant-violations=0** [IsWinner⟺Any(IsSelected), 33 sup/23 winners] + sys.tables=89 frozen [data-only, 2 mig-file no-ModelSnapshot = correct]) · **FE REAL-SHIP js×2 css-FROZEN** admin js `acprMqfB``D51OYyGV` · user js `Ck3BTDdA``BVdssm5S` (CSS `CfunQHbd`/`DGqqHQXS` frozen — `.tsx`-only) ship-proof= real-size 1,655,105/1,561,453b + `application/javascript` + LM 15:26:37/15:27:29 GMT in-window + byte-verify `trúng thầu theo từng hạng mục`+`thắng hạng mục`+`trúng thầu cho hạng mục` present both vs SPA-trap 919b · **select-winner WIRED** POST bogus-bearer+body→401 vs bogus-route 404 (401≠blanket). smoke health/ready+live + admin/eoffice roots ALL 200. 🔑 js-rotate+css-frozen = FE-logic-no-style fingerprint.
- **🟢 S113 #383 (rn 383, id 497) `5fa11b5` GO 5m14s** — Supplier import v2 + Mig 64 `AddSupplierPublishState` APPLIED-PROD (IsPublic backfill 37/37, sys.tables 89) · gate 477 (45D+432I) · FE real-ship js+css both apps admin `acprMqfB`/`CfunQHbd` · user `Ck3BTDdA`/`DGqqHQXS` (byte-verify `Tải file mẫu`/`Đã công bố`/`Nháp (ẩn)`) · endpoints `/suppliers/import/template`+`?published=true` WIRED 401 · smoke ALL 200 — verbatim → git `8304d08` + `_INDEX`.
- **🟢 S112 #496 `e100ef0` GO 5m25s** — PE workflow + Supplier Excel-import Phase B + Mig 63 `AddSupplierImportSourceFields` APPLIED-PROD (2 AddColumn, sys.tables 89) · gate 458 (45D+413I) · FE admin `Cixlt_MA`/user `BhBOG1dn` · 🔑 411-refine: bodyless-POST→411 ANY route → re-probe WITH body → 401/415 (matched) vs 404 (missing) — verbatim → `2026-07.md`/`_INDEX`.
- **🟢 S111 #495 (rn 381, id 495) `8aa3869` GO 5m11s (11:45:21→11:50:32+07 2026-07-12, iter6) — H22 WAL adopt 30-file +767 (0 src/BE/FE/mig; non-ignored: settings.json + wal-flush.ps1 + hmw.js + deploy.yml + governance-detectors.ps1 + .gitattributes → CI-run ĐÚNG thiết kế):** status=success (gate 440) · **deploy.yml TỰ-ĐỔI trong chính commit (D10 +2 paths-ignore `.claude/agent-memory/**` + `.claude/workflows/runs/**`) — run created+parse+chạy trọn 5m11s = YAML hợp lệ, workflow-self-change PASS** · Mig FROZEN 62 history-top `ExpandSupplierFields` + sys.tables=89 ✓ · smoke 3×200 (health/ready + 2 FE roots) · **bundle FROZEN ×6-streak** (admin `BVyH-FY7`/`Bbtesa6B` · user `EDsZebvn`/`BmuPz_Dy` = #490#494) + LM-in-window 11:49:07/11:50:00+07 + size 1,644,931/1,551,280b byte-identical #494 vs fake-919b = ship-proof. 🔑 **AS-6 BASELINE SHIFT từ #495: paths-ignore 7-entry — memory-budget.json/agent-memory/run-trace push HẾT trigger (trigger-pattern #490/#492 DEAD); phân loại trigger giờ soi file ngoài 7-glob.**
- _(**S107-closeout→S110-spot #490-#494** GOVERNANCE/MEMORY-push GO ×5 [`d74f67f`·`69957fd`·`f5a5c3e`·`6ee60cd`·`ac3f122` — trigger #41 non-.md-in-push: budget.json ×2 + hmw.js ×3, pattern DEAD từ D10 #495] — bundle FROZEN-streak ×5 sau ROTATE #489 [#69 TWO-MODE; LM-in-window + real-size 1,644,931/1,551,280b = signal duy nhất] · Mig 62 + sys.tables 89 frozen · gate 440 · smoke 3×200 ALL · 🔑 #494 spot-detection: LM-prod-newer-than-last-verified-window = missed-run detector · full verbatim → `archive/2026-07.md` via `_INDEX`)_
- _(**S107 #489 `21387cd` GOVERNANCE-ONLY (+office-document 12th sub) GO 5m33s** — JS ROTATED-benign `ChSNAJBK``BVyH-FY7` / `Bn2w1znH``EDsZebvn` CSS-frozen, 🔑 #69 TWO-MODE CONFIRMED (0-fe-src ROTATE #312/#489 hoặc FROZEN #333/#485-487 — ship-proof = LM-in-window + real-size, hash-delta phụ) · **S104 #488 `778fc98` FULL-STACK Supplier 9→27 + Mig 62 `ExpandSupplierFields` APPLIED-PROD** — AddColumn×18-exact + sys.tables-89-invariant + un-cram 4-NCC Note=NULL/Pkg/Bank/LegalRep-populated-EXACT + DTO-18-new-keys-live + JS-rotate-real-src + 🔑 FULL-STACK-SCHEMA-ADD+DATA-UN-CRAM PASS-pattern — full verbatim → `archive/2026-07.md` via `_INDEX`)_

View File

@ -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.
---

View File

@ -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 đã đóng).

View File

@ -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.