[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

@ -3,17 +3,15 @@ updated: 2026-07-12 | session: S114 | branch: main
goal: PE multi-NCC per hạng mục (anh Kiệt) — cho ≥2 NCC-TP trúng thầu/1 hạng mục, mỗi nhà 1 giá riêng — 6-bước fable-clone→hmw→verify→deploy→session-end
chain:
[x] B1 fable-clone invest DONE — wf_dd49ddc0-f29
[x] B1 fable-clone invest DONE — wf_dd49ddc0-f29 (3 trục winner, hook Quote.IsSelected)
[x] B2 fable-clone review DONE — wf_1e127f4b-de5 (6 adjustment A1-A6)
[x] B3-BE stage-1 DONE-via-disk-recover (garble #53; 6 file BE + mig verify disk, spec-compliant A1/A2/A3/command/mig)
[x] B3-test DONE (stage-2 lane garble #53 return NHƯNG code disk: 7 test file update + 4 test mới → dotnet test 482 PASS [45D+437I, +5])
[~] B3-FE stage-2: fe-admin/PeDetailTabs.tsx DONE-verify-disk (per-cell grid + toggle POST {detailId,supplierIds} + downgrade picker + keep derived guard) — npm build fe-admin RUNNING (bx20beby3)
[!] B3-FE mirror: copy fe-admin/PeDetailTabs.tsx → fe-user (SHA-lock byte-identical, fe-user còn 2dca7cee cũ) + npm build fe-user
[ ] B4 fable-clone reviewer verify (grep-suite 0 sót + threshold-crossing + FE 2-app SHA)
[ ] B5 deploy + commit (cicd verify)
[ ] B6 session-end
[x] B3 hmw implement DONE-via-disk-recover (garble #53 ×3 return-only, code verify disk: BE compile + FE 2-app SHA + em-main mirror fe-user)
[x] B4 fable-clone verify DONE — wf_14850736-a0a (2/2 clean HỘI TỤ MUST bug DeleteQuote/DeleteDetail sót re-derive → em-main fix test-before helper PeWinnerInvariant + 486 test)
[x] B5 deploy+commit DONE — d436af2 pushed; cicd garble #53 ×5 → em-main-solo verify PASS (bundle admin acprMqfB→D51OYyGV / user Ck3BTDdA→BVdssm5S rotate ×2 · byte-verify detailId+select-winner+hạng-mục PRESENT · smoke 3/3 200 · endpoint 401-live · mig applied API-healthy)
[!] B6 session-end (/session-end) — docs closeout + harvest 4-run-trace + reviewer 2 diary + memory + eval
next: chờ npm build fe-admin (bx20beby3) → nếu PASS: cp fe-admin/PeDetailTabs.tsx fe-user/ → git hash-object 2 file khớp → npm build fe-user → build ✅ cả stack thì B4
verify: BE+test GREEN 482 (dotnet test). FE fe-admin verify-diff đúng. CÒN: fe-admin build + fe-user mirror+build. ⚠️ #53 garble ×4 phiên này (BE + review-lane + FE + test) — mọi lane recover-from-disk, việc KHÔNG mất
next: chạy /session-end — harvest 4 run-trace + 2 reviewer diary (project_s114_*) · docs STATUS/HANDOFF/gotcha/ef-core-skill · memory feedback (#53-garble-resilient-parallel + verify-catches-enumeration-gap) · closeout commit docs/memory (CI-skip)
verify: 486 test PASS · C5 grep 0-sót IsWinner-SUM · FE SHA ae3788e9 2-app · commit d436af2 (18 file, snapshot unchanged data-only)
⚠️ HEADS-UP anh Kiệt (UAT prod): (1) giaTri phiếu SINGLE-winner đổi budget→bid (theo D4, đúng hơn) — verify số HĐ; (2) UX chọn winner nay PER-Ô hạng mục (grid Detail×NCC) thay picker phiếu-wide; (3) test incognito (cache≠deploy #77)
run-folder: .claude/workflows/runs/2026-07-12-S114-multi-ncc-per-hangmuc/
notes: FE lane chỉ đụng fe-admin/PeDetailTabs.tsx (git status), KHÔNG types/PeListPanel — dùng field sẵn (q.isSelected struct-identical 2 app), mirror an-toàn
harvest-pending: 4 run-trace (invest wf_dd49ddc0 · review wf_1e127f4b · impl wf_e7ee6abe+wf_410896c4 · verify wf_14850736) + 2 reviewer diary (project_s114_*) + #53-garble ×4-recover lesson

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.

View File

@ -84,6 +84,7 @@ when-to-use:
| **62** | **`ExpandSupplierFields`** | **🎯 S104 Supplier 927 field (Phase A của 30-col NCC Excel import, anh Kiệt FDC): 18 AddColumn nullable + 18 DropColumn Down + `SupplierType +CaHai=6` append-only + enum `SupplierStatus` mới + guarded un-cram `Sql()` (clear legacy crammed Note 4 seeded rows exact-match, reversible). **No new table (89 giữ — 18 AddColumn/0 CreateTable)**. Seed fill-nulls-if-exists 30-field TRUONGGIANG/TANPHU/TGN/DONGDUONG idempotent. Commit `778fc98` shipped UN-closed-out S104 docs-reconcile; prod-verified Run #488 PASS; 440 test #65-no-bite đổi chữ Create/UpdateSupplierCommand. Phase B (Excel-auto-map) defer.** |
| **63** | **`AddSupplierImportSourceFields`** | **🎯 S112 Supplier +`SourceUpdatedAt datetime2?` + `SourceUpdatedBy nvarchar(200)?` (import-provenance cột 29/30 file Excel "Database NCC" Phase B Excel-import Phase B). 2 AddColumn nullable + 2 DropColumn Down (reversible). **No new table (89 giữ)**. Prod-verified Run #496 PASS `__EFMigrationsHistory` top + 2/2-col present; commit `e100ef0`.** |
| **64** | **`AddSupplierPublishState`** | **🎯 S113 Supplier +`IsPublic` bit NOT NULL default 0 (cổng công-bố "thiếu NCC nháp/ẩn" R4): AddColumn + `Sql("UPDATE Suppliers SET IsPublic=1")` backfill 37/37 prodpublic + DropIndexCreateIndex đổi Code filtered-unique `WHERE [IsDeleted]=0 AND [Code]<>''` (cho phép nhiều nháp Code=''). **No new table (89 giữ)**. Reversible Down (⚠ edge: rollback sau khi 2 nháp Code='' sẽ kẹt CreateIndex filter ). Prod-verified Run #497; commit `5fa11b5`. Kèm gotcha #80 (null-Code NRE NewSupplier).** |
| **65** | **`BackfillPeQuoteIsSelectedAndWinnerInvariant`** | **🎯 S114 DATA-ONLY no-schema (multi-NCC per hạng mục anh Kiệt): backfill **2-CHIỀU** thiết lập bất-biến `Supplier.IsWinner == Any(Quote.IsSelected)`. CHIỀU-1 `IsWinner→IsSelected` (phiếu whole-supplier winner Mig58 set IsSelected mọi quote NCC, **preserve tổng** winnerQuoteTotal) + CHIỀU-2 `IsSelected→IsWinner` (dormant/seed EXISTS). Idempotent (`WHERE ...=0`), SQL-Server-only (SQLite EnsureCreated bỏ qua migration), Down **no-op** (data backfill không reversible, mirror Mig 61). **No new table (89 giữ)** snapshot UNCHANGED (0 CreateColumn). Prod-verified bundle rotate ×2 `D51OYyGV`/`BVdssm5S`; commit `d436af2`. Kèm **gotcha #81** (delete-writer sót re-derive IsWinner phantom giaTri=0 verify-bắt).** |
Total: **89 bảng** `sys.tables` (= 88 dbo-app + `__EFMigrationsHistory`; re-ground S97 cicd Run #482 LIVE số canonical `docs/STATUS.md`; last schema-changing Mig 50 net 9388-app, Mig 51-64 đều AddColumn/index/data-only/schema-expand không đổi số bảng). Xem `docs/database/schema-diagram.md` migration table + §11-15 module ERD 16+ Mig 27-64 chi tiết pending).

View File

@ -12,7 +12,8 @@ Cho phép chọn **≥2 NCC-TP trúng thầu vào 1 hạng mục** (PurchaseEval
- [x] **B2a** anh Kiệt chốt D1-D4 all recommended (D5 default) — spec LOCKED
- [x] **B2b** fable-clone review ensemble → spec cuối — `wf_1e127f4b-de5` DONE (2/3 clean, lane correctness #53-garble→em-recover; 6 adjustment A1-A6 fold)
- [x] **B3** hmw implement DONE-via-DISK-RECOVER (2 workflow garble #53 return; BE stage-1 `wf_e7ee6abe` + FE/test stage-2 `wf_410896c4` — code trên disk verify: BE compile + 482 test PASS + FE 2-app build + SHA `ae3788e9` + C5 grep 0-sót). ⚠️ FE lane chỉ làm fe-admin → em-main mirror fe-user.
- [!] **B4** fable-clone reviewer verify (adversarial-refute, garble-resilient) — `wf_?`
- [x] **B4** fable-clone verify DONE — `wf_14850736-a0a` (2/2 lane CLEAN 0-garble, HỘI TỤ 1 MUST bug: DeleteQuote/DeleteDetail sót re-derive IsWinner → phantom winner HĐ giaTri=0). em-main fix test-before: 2 test RED→GREEN + helper `PeWinnerInvariant` + 3 stale-comment. **486 test PASS**.
- [!] **B5** deploy + commit (cicd verify)
- [ ] **B3** hmw Opus 4.8 MAX implement (BE + FE 2-app + test)
- [ ] **B4** fable-clone reviewer verify implementation
- [ ] **B5** deploy + commit (cicd-monitor verify)

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