[CLAUDE] Docs: S88 closeout — PE stability audit + D1/D2 prod-verified

STATUS/HANDOFF S88 prepend (State THAT: Mig 59 · 88 bang · 419 test (45D+374I) ·
gotcha 73 · bundle admin BQq2eTP6/user dHa64I59) + session log + agent-memory
harvest (6 sub: cicd/impl-backend/impl-frontend/inv-codebase/reviewer/test-spec).

D1/D2 production already shipped: 73cce1f (Run #347 PASS, +11 test, the 6 fixes) +
1aa3bdb (Run success, +6 test, create-contract 1->N + list winner names). Both
prod-verified (bundle rotation + Gitea run=success; cicd-monitor verdict truncated
#53 -> curl-self-verify).

Curate-debt: test-specialist L1 34.8KB OVER-SOFT (root-caused #53 x3 this session)
DEFERRED next session as #1 NEXT-em (proper byte-exact L1->L2 + _INDEX, not rushed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-25 10:58:15 +07:00
parent 1aa3bdb591
commit 6bfe3db5e2
9 changed files with 96 additions and 11 deletions

View File

@ -32,7 +32,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 **402 PASS** (45 Domain + 357 Infra; S88 #345 395 + 7 new `PeCcmBudgetPeriodSetterAuthzTests`; prior S87 #344 395, S86 377). 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 **413 PASS** (45 Domain + 368 Infra; S88-quad #347 402 + 11 D1 [PeHoSoLinkTests+PeWorkItemBudgetTests EXTEND + new PeUrgentNotifyDirectorTests]; prior S88 #345 402, S87 #344 395). 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.
@ -48,11 +48,11 @@ Read-only CI/CD + post-deploy verifier SOLUTION_ERP. Polls Gitea Actions API, ve
## 🧠 SOLUTION_ERP CI/CD essentials (S80 verified — re-ground từ docs/STATUS.md canonical)
- **Gitea:** `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` · workflow `.gitea/workflows/deploy.yml` · paths-ignore `['docs/**','**/*.md','.claude/skills/**']`. 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`)**. pw `vrapp/buKL3TGBkD0wDDbYVw65QeX9` read from prod `appsettings.Production.json` when `$env:PROD_DB_PASSWORD` empty. skill-doc path `C:\inetpub\apps\SolutionErp\Api` STALE.
- **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:** **402 PASS** (45 Domain + 357 Infra · 0 fail/skip; S88 Run #345 sha 9b1ee0c 395 + 7 CCM-budget-period setter authz; prior S87 #344 395). Phase 9 UAT skip per chunk OK.
- **Tests baseline:** **419 PASS** (45 Domain + 374 Infra · 0 fail/skip; S89 Run #348 sha 1aa3bdb 413 + 6 D2 multi-winner [CreateContractFromEvaluationMultiWinnerTests + PeListWinnerNamesProjectionTests]; prior S88-quad #347 413). Phase 9 UAT skip per chunk OK.
- **Mig latest repo:** **Mig 59 `20260624084608_AddCcmBudgetPeriodToPurchaseEvaluation`** (S88 #345; PE +1 AddColumn `CcmBudgetPeriodAmount` decimal(18,2) NULL no-backfill VERIFIED-APPLIED-PROD history-HEAD + col-exists 0-nonnull). Prev Mig 58 `AddPeMultiWinnerAndLevelFinalize` (S87 #344). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`. **`sys.tables` (is_ms_shipped=0) = 88** (S61 Budget-replace DROPPED 9388 narrative-93 STALE; when commit touches no schema, 88 correct, don't FAIL on 88. Always cross-ref COMMIT scope vs ambient count). enum-additive (e.g. `ApprovalAttachment=5`) = int-stored NO-mig expect history NOT-advance + empty `git diff -- '*Migrations*'`.
- **Bundle hash live (S88-bis Run #346):** admin js `BABhPh2s`/css `Pltrs5ku` · user js `DLulO6uP`/css `Foiw55B6` (BOTH js+css ROTATED from #345 `C8lWxEQK`/`BFfMO5l8` + `Kz5cSW2u`/`B11h2RoV` by real fe-src PE "4. Thông tin chọn thầu" layout restructure css rotated = new utility classes for highlighted block). **PER #69: hash MAY rotate per deploy OR stay frozen if 0 fe-src diff (byte-identical) — SNAPSHOT for THIS run, NOT a frozen baseline.** Real FE ship = diff `fe-*/src` in commit/range + real-size-vs-fake (1.6/1.5MB vs ~919b SPA-trap), NOT hash delta. ASYMMETRIC-deploy framing (S66) RETIRED by #69.
- **Bundle hash live (S89 Run #348):** admin js `BQq2eTP6`/css `Pltrs5ku` · user js `dHa64I59`/css `Foiw55B6` (JS BOTH ROTATED from #347 `CP_iKhTu`/`DUe0fndl` by real fe-src D2 PeDetailTabs+PeListPanel; **css FROZEN `Pltrs5ku`/`Foiw55B6` = #347/#346 — correct, D2 FE logic-only no new utility class**). **PER #69: hash MAY rotate per deploy OR stay frozen if 0 fe-src diff (byte-identical) — SNAPSHOT for THIS run, NOT a frozen baseline.** Real FE ship = diff `fe-*/src` in commit/range + real-size-vs-fake (1.6/1.5MB vs ~919b SPA-trap), NOT hash delta. ASYMMETRIC-deploy framing (S66) RETIRED by #69.
- **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,16 +70,16 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code /
## 📅 Recent runs (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **S88-bis #346 (id=460) `0d3c0cf` PASS ~5m (FE-ONLY PE layout, anh Kiệt FDC)** — restructure PeDetailTabs: tách c/d/e → khối nổi bật "4. Thông tin chọn thầu". Files=2 ONLY: `fe-admin/src/components/pe/PeDetailTabs.tsx` + `fe-user/...` — ZERO BE, ZERO `*Migrations*`/`*Persistence*` (verified empty diff), ZERO test. status=success ⟹ test gate passed (**baseline 402 unchanged**, FE-only). **Bundle BOTH js+css ROTATE (real fe-src layout ⟹ new content-hash incl new utility classes): admin js `C8lWxEQK`→`BABhPh2s` css `BFfMO5l8`→`Pltrs5ku`, user js `Kz5cSW2u`→`DLulO6uP` css `B11h2RoV`→`Foiw55B6`** (#345 baseline). Real-ship proven: admin js 1,628,719b Last-Modified 10:12:55Z=17:12+07 in-window (run 17:09→17:14) · user js 1,532,620b 10:13:52Z in-window · SPA-trap control `ZZfakehash0.js`→200 but 919b text/html (size-check meaningful). Smoke api/health ready+live 200, admin+eoffice root 200, GET /api/purchase-evaluations + /api/menus both 401 unauth (wired NOT 404/500). **Mig FROZEN 59** (prod history HEAD==`AddCcmBudgetPeriodToPurchaseEvaluation`==repo, no-pending) + sys.tables 88 (89 incl __EFMigrationsHistory, 0 schema change). gotcha #69-inverse re-confirmed (real fe-src ⟹ hash rotates). conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api`. ⚠️ SSH→PS table-count `<>'literal'` mangled via base64/CLIXML → use `NOT LIKE` ASCII-escape or count-incl-history1.
- **S88 #345 `9b1ee0c` PASS ~4m (FULL-STACK PE Section-B 3-col + Mig 59, anh Kiệt FDC/CCM)** — Section B (THỰC HIỆN) rewrite 1-col-list → 3-col table (per-col kỳ-này) + new setter endpoint `PATCH /purchase-evaluations/{id}/budget/ccm-period` (`SetPeCcmBudgetPeriodCommand`, fail-closed Admin|CostControl else Forbidden) + DTO field `ccmBudgetPeriodAmount`. Files 14: BE 4 (Controller/DTO/Domain/Config) + 2 App-features (PeSuggestedPriceFeatures + PurchaseEvaluationFeatures) + FE **both apps** 4 (PeDetailTabs + types ×2) + Mig 3-file + 1 new test. **Mig 59 `20260624084608_AddCcmBudgetPeriodToPurchaseEvaluation`** = single AddColumn `PurchaseEvaluations.CcmBudgetPeriodAmount` decimal(18,2) NULL **no backfill** — VERIFIED-APPLIED-PROD (history top==`AddCcmBudgetPeriod`==repo HEAD; sys.columns COL=1 type `decimal(18,2) null=1`; NONNULLrows=0 as designed). Test gate=success ⟹ passed (**baseline 402** = 45 Domain + 357 Infra = 395 + **7 `PeCcmBudgetPeriodSetterAuthzTests`**: Admin-set/CostControl-set/Drafter-forbidden/Procurement-forbidden/Zero-valid/Null-clears/Unknown-NotFound). **Bundle BOTH js+css ROTATE (real fe-src Section-B rewrite): admin js `CXCKlNay`→`C8lWxEQK` css `BKB2NsgQ`→`BFfMO5l8`, user js `Di40ewrF`→`Kz5cSW2u` css `BaqeI4iG`→`B11h2RoV`** (css rotated = new table-grid utility classes, NOT logic-only). Real-ship proven: admin js 1,627,505b Last-Modified 09:10:47Z=16:10+07 in-window (16:08→16:12) · user js 1,531,406b 09:11:40Z in-window · SPA-trap control `ZZfake0.js`→200 but 919b text/html (size-check meaningful). Endpoint live: `PATCH .../budget/ccm-period` unauth→**401** bodyless AND with-json-body (wired: NOT 404=route-exists, NOT 500=no-crash; PATCH auth short-circuits before IIS Length-check so 401 not 411). Health api ready/live + admin + eoffice 4×200. GET /api/purchase-evaluations + /api/menus both 401. ⚠️ ambient sys.tables=88 (NOT 89 as run-spec hedged — sqlcmd direct COUNT WHERE is_ms_shipped=0 AND name<>'__EFMigrationsHistory' = 88; Mig 59 AddColumn-only 0-new-table, consistent w/ S62 re-ground baseline). conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api`. gotcha #69 inverse re-confirmed (real fe-src ⟹ hash rotates).
- **S89 #348 (id=462) `1aa3bdb` PASS ~5m10s (FULL-STACK PE D2 multi-winner outward, NO-MIG)** — create-contract 1→N per winner (command→List<Guid>, multi-winner handler in CreateContractFromEvaluationFeatures, Controller returns `{contractIds}`) + list-card winner-names projection (PurchaseEvaluationFeatures + Dtos WinnerSupplierNames). Files 12: BE 4 (Controller/CreateContractFromEvaluationFeatures/Dtos/PurchaseEvaluationFeatures) + FE **both apps** 6 (PeDetailTabs+PeListPanel+types ×2) + 2 test (CreateContractFromEvaluationMultiWinnerTests + PeListWinnerNamesProjectionTests). **NO `*Migrations*`/`*Persistence*` in diff** (git diff EMPTY — logic-only, no schema). status=success ⟹ test gate passed (**baseline 419** = 45 Domain + 374 Infra = 413 +6; logs 404 anon so count not log-scraped — trusted gate+baseline). **Bundle JS BOTH ROTATE (real fe-src PeDetailTabs+PeListPanel ⟹ new content-hash): admin js `CP_iKhTu`→`BQq2eTP6` · user js `DUe0fndl`→`dHa64I59`** (#347 baseline); **css FROZEN admin `Pltrs5ku` user `Foiw55B6` = #347/#346 — correct, D2 FE logic-only no new utility class** (gotcha #69-inverse). Real-ship proven (SPA-trap defeated): admin js 1,628,901b Last-Modified 03:34:13Z=10:34+07 in-window (run 10:30:30→10:35:40 = 03:30:30→03:35:40Z) · user js 1,532,802b 03:35:07Z in-window · control fake `ZZfakehash00.js`→200 but 919b text/html (size-check meaningful). **BE dll ALL 4 fresh in-window: Api 03:33:00Z (Controller `{contractIds}` live) · Application 03:31:04Z (CreateContractFromEvaluation multi-winner handler + PE winner-names projection live) · Infrastructure 03:31:28Z · Domain 03:30:47Z.** **Mig FROZEN 59** (repo MIGCOUNT=59 + prod history HEAD==`AddCcmBudgetPeriodToPurchaseEvaluation`==repo + HistCount=59 no-pending) + sys.tables 89 incl __EFMigrationsHistory = 88 business (no schema change). **Endpoint live: `POST /api/purchase-evaluations/{id}/create-contract` bodyless→411 (IIS Length-Required pre-auth), +json-body→401 (wired: NOT 404=route-exists, NOT 500=no-crash; `{contractIds}` multi-winner contract live).** Health api ready/live + admin + eoffice 4×200. GET /api/purchase-evaluations + /api/menus + /api/approval-workflows-v2 all 401 unauth (wired). gotcha #69-inverse re-confirmed (real fe-src ⟹ js rotates, css frozen when logic-only). ⚠️ **DB pw = `buKL3TGBkD0wDDbYVw65QeX9` (NOT `vrapp/buKL3...` — `vrapp/` was user/pw-combined-notation in memory essentials, actual Password field has no prefix; read from prod `appsettings.Production.json` ConnectionStrings.Default).** SSH→PS `$_.LastWriteTimeUtc` mangled in nested double-quote → base64 EncodedCommand worked; sqlcmd `-Q` quoting mangled → scp .sql + `-i` file worked cleanest. conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api`.
- **S88-quad #347 (id=461) `73cce1f` PASS ~5m04s (FULL-STACK PE D1 stability, NO-MIG)** — Block-B multi-winner financials re-key (PurchaseEvaluationFeatures) + CEO-notify SaveChanges-persist (PurchaseEvaluationUrgentFeatures +5 lines) + setter RELOCATE PeSuggestedPriceFeatures→PeWorkItemBudgetFeatures (56/+57) + HoSoLink trim + #70 guard + Dtos comment. Files 12: BE 5 (Dtos+4 features) + FE **both apps** 4 (PeDetailTabs+types ×2) + 3 test (PeHoSoLinkTests+PeWorkItemBudgetTests EXTEND, **new PeUrgentNotifyDirectorTests +237 lines**). **NO `*Migrations*`/`*Persistence*` in diff** (logic-only re-key, no schema). status=success ⟹ test gate passed (**baseline 413** = 45 Domain + 368 Infra = 402 +11; logs 404 anon so count not log-scraped — trusted gate+baseline). **Bundle JS BOTH ROTATE (real fe-src #70-guard+HoSoLink-trim ⟹ new content-hash): admin js `BABhPh2s`→`CP_iKhTu` · user js `DLulO6uP`→`DUe0fndl`** (#346 baseline); **css FROZEN admin `Pltrs5ku` user `Foiw55B6` — correct, D1 FE logic-only no new utility class**. Real-ship proven: admin js 1,628,726b Last-Modified 02:57:57Z=09:57+07 in-window (run 09:54:15→09:59:19 = 02:54:15→02:59:19Z) · user js 1,532,627b 02:58:50Z in-window · SPA-trap control `ZZfakehash00.js`→200 but 919b text/html (size-check meaningful). **BE dll ALL 4 fresh in-window: Api 02:56:42Z · Infrastructure 02:55:16Z (PeWorkItemBudgetFeatures setter-relocate live) · Application 02:54:51Z (PurchaseEvaluationFeatures Block-B + UrgentFeatures CEO-persist live) · Domain 02:54:36Z.** **Mig FROZEN 59** (MIGCOUNT=59, prod history HEAD==`AddCcmBudgetPeriodToPurchaseEvaluation`==repo, no-pending; correct NO-MIG D1) + sys.tables 88 (89 incl __EFMigrationsHistory). Health api ready/live + admin + eoffice 4×200. GET /api/purchase-evaluations + /api/menus + /api/approval-workflows-v2 all 401 unauth (wired NOT 404/500). gotcha #69-REFINE inverse re-confirmed (real fe-src ⟹ js rotates, css frozen when logic-only). conn key `ConnectionStrings.Default`, prod path `C:\inetpub\solution-erp\api` (skill-doc `\apps\...` STALE). ⚠️ SSH→PS table-count `<>'literal'` mangled via base64/CLIXML AGAIN → use count-incl-history1 (88) workaround.
- _(S88-bis #346 `0d3c0cf` FE-only PE layout "4. Thông tin chọn thầu" 2-files js+css-BOTH-rotate baseline-402 · S88 #345 `9b1ee0c` FULL-STACK Section-B 3-col + **Mig 59 `AddCcmBudgetPeriodToPurchaseEvaluation`** AddColumn-no-backfill VERIFIED-PROD + setter `PATCH .../budget/ccm-period` PATCH-401-not-411 + 7 authz-test → PASS, FIFO-trimmed, full verbatim git + archive `_INDEX`)_
- **S87 #344 (id=458) `f1b10e9` PASS ~5m11s (FULL-STACK PE CCM 3-fix + Mig 58, anh Kiệt FDC)** — multi-winner SUM (SelectWinner→SupplierIds[] array body) + budget=0 valid-submit + per-level auto-finalize (`ApproveV2Async if matchingLevel.AllowApproverFinalize`). Files: BE 13 (Controller/App/Domain/Infra/Service + Mig 3-file) + FE **BOTH apps** 7 (PeDetailTabs/PeWorkflowPanel/types ×2 + fe-admin ApprovalWorkflowsV2Page Designer) + 6 test. **Mig 58 `20260624071509_AddPeMultiWinnerAndLevelFinalize`** = 2 AddColumn bit (`PurchaseEvaluationSuppliers.IsWinner` default0 + `ApprovalWorkflowLevels.AllowApproverFinalize` default0) + 1 backfill `UPDATE ... SET IsWinner=1 WHERE pes.SupplierId=pe.SelectedSupplierId` — VERIFIED-APPLIED-PROD (history top==`AddPeMultiWinnerAndLevelFinalize`==repo HEAD; both cols sys.columns=1; **backfill ran: winners=13 == pe_with_SelectedSupplierId=13 / 30 total supplier rows** — exactly 1 winner per pre-existing selected PE). Test gate=success ⟹ passed (**baseline 395** = 45 Domain + 350 Infra per spec; was 397 2 because per-level-finalize moved runtime-flag→auto-config, NOT regression). **Bundle BOTH ROTATE (real fe-src ⟹ new content-hash, contrasts #69-REFINE BE-only): admin js `aKuNip19`→`CXCKlNay` (css FROZEN `BKB2NsgQ`, logic-only diff no new chunk), user js `BUsL0HeQ`→`Di40ewrF` (css FROZEN `BaqeI4iG`).** Real-ship proven: admin js 1,621,660b Last-Modified 08:26:32Z=15:26+07 in-window · user js 1,525,569b 08:27:26Z in-window · control fake-hash `ZZfakehash0.js`→200 but 919b text/html (SPA-trap confirms 200-alone meaningless). Endpoint live: `POST /api/purchase-evaluations/{id}/select-winner` bodyless→411 (IIS Length-Required pre-auth), +json-body→401 (wired, NOT 404/400/500; `SelectWinnerBody(IReadOnlyList<Guid>? SupplierIds)``SelectPurchaseEvaluationWinnerCommand` array contract live). Health api ready/live + admin + eoffice 4×200. ⚠️ **ambient sys.tables=89 (was 88) — NOT from Mig 58** (AddColumn-only, 0 new table; newest table `PeWorkItemBudgets` 06-13 Mig 50, none created 06-24); 88 baseline last re-grounded S62, +1 pre-existing drift in Mig 50-57 range, harmless. ⚠️ prod path `C:\inetpub\solution-erp\api` (skill-doc `\apps\SolutionErp\Api` STALE — re-confirmed); conn key `ConnectionStrings.Default`; base64 EncodedCommand + 1 path-retry. gotcha #69-REFINE inverse confirmed (real fe-src ⟹ hash DOES rotate).
- **S86 #343 (id=457) `ae4f106` PASS ~5m00s (BE-ONLY notify-filter)** — PE "cần duyệt" notify EXCLUDE drafter + 1 new test (`PeApproverNotifyExcludesDrafterTests`) + STATUS + agent-memory. Only-BE-src: 1 file `PurchaseEvaluationWorkflowService.cs`, ZERO `fe-*/src`, ZERO `*Migrations*`. status=success ⟹ test gate passed (**baseline 377** = 45 Domain + 332 Infra per spec; S85 374 +3; reviewer's 2 transient parallel-collection flake did NOT surface red in CI — gate green). **Ship-proof #69-REFINE (BE-only) CONFIRMED via dll Last-Modified in run-window (16:37:38→16:42:35+07 = 09:37:38→09:42:35Z):** Api.dll 09:40:04Z · **Application.dll 09:38:12Z · Infrastructure.dll 09:38:34Z** (PurchaseEvaluationWorkflowService lives in Infrastructure → recompiled fresh) — all 3 in-window. **FE bundle FROZEN (correct, 0 fe-src diff ⟹ byte-identical): admin `aKuNip19`/`BKB2NsgQ`, user `BUsL0HeQ`/`BaqeI4iG` = S85 baseline.** Mig FROZEN 57 (`AddPeSuggestedPriceNotes`, prod head==repo) + tables 88. Prod 4×200 (api ready/live, admin, eoffice) + PE wired (GET /api/purchase-evaluations + /api/menus both 401 unauth, NOT 404/500). Notify-filter BE-internal no-new-endpoint → test 377 covers behavior (no curl-probe of notify logic). gotcha #69-REFINE reaffirmed (BE-only: dll Last-Modified = sole ship-proof, hash-delta N/A). ⚠️ SSH transient drop ×3 mid-session (`Connection closed by 103.124.94.38`) — base64 EncodedCommand + retry-loop recovered; not a deploy issue.
- **S85 #342 `d4f6021` PASS ~5m00s (DOCS+SQL NO-OP, INVERTED criteria)** — 6×.md + `scripts/s85-wipe-uat-data.sql` (only non-ignored path → CI triggers; whole-workflow still builds FE+publish). **PASS-when-FROZEN, NOT rotate.** Verified: status=success ⟹ test gate passed (baseline **374** = 45 Domain + 329 Infra per STATUS.md canonical, logs API 404 anon so count not log-scraped — trusted gate+baseline) + **bundle FROZEN admin `aKuNip19`/user `BUsL0HeQ` (correct: 0 fe-src diff ⟹ byte-identical ⟹ same hash, gotcha #69-REFINE)** but DID ship (admin js Last-Modified 08:56:34Z=15:56+07 IN-window 15:53→15:58, real 1.62MB/user 1.52MB vs SPA-trap 919b, 2nd-fetch byte-stable). Prod 4×200 (ready/live/admin/eoffice) + endpoint smoke 4×200 (PE/awf-v2/menus/projects). **Prior S85 deploys still live: R6 TZ fix `49ac086` → `createdAt:"…7331140Z"` trailing-Z CONFIRMED; R5 UpdateDraft `1263c0d` → both ancestors of d4f6021, shipped via #341 (#340 cancelled but #341 success built later commit incl TZ).** SignalR negotiate 411 (bodyless-POST IIS, wired, notif untouched). gotcha #73 (TZ) + #69-REFINE reaffirmed.
- **S83 #333 `fc1f19d` PASS ~4m59s** — BE-only PE-guard (require ApprovalWorkflowId V2 at create-validator + submit-branch ConflictException; close FE-only-validation gap → phiếu reached "Đã gửi duyệt" w/ null workflow = V1-legacy-banner trap). +2 validator tests → **test 356** (gate=success ⟹ pass). NO-mig (frozen 57, prod head `AddPeSuggestedPriceNotes`==repo, no-pending) + ZERO fe-*/src diff. **Bundle hash UNCHANGED (admin `CsJetgZH`/user `BVS0ApIm` = #330) BUT shipped — Last-Modified 17:10:35/17:11:27 in-window** (real js 1.62/1.52MB vs SPA-trap 919b) → **gotcha #69-REFINE: 0-FE-src ⟹ byte-identical ⟹ same-hash; Last-Modified is true ship-proof.** Guard BE-only no-endpoint → no curl-probe (356 tests cover RED→GREEN). Health api/ready+live 200, admin/eoffice 200, api-root 404 (no `/` route, expected).
- **S78 #330 `7886fd0` PASS ~4m56s** — cross-stack NO-MIG enum `ApprovalAttachment=5` (attach-on-approve) + FE 2-app PeWorkflowPanel modal-upload. Verified: empty-migrations-diff + Mig-frozen-57 + tables88 + bundle BOTH rotate js+css (real 1.6/1.5MB vs fake-919b) + endpoint bodyless-POST-411→re-probe-body-401-wired + health 4×200. → _INDEX.
- **S76 #318 `e33481e` PASS ~4m58s** — full-stack Mig56 ProInitial/ProAdjust VERIFIED-APPLIED-PROD (sys.columns decimal(18,2)) + **5th-axis BACKFILL-verify** 4-rows-0-violation (gotcha #64 prod-data-UPDATE-first-time) + history-advance-55→56 + tables88 + bundle BOTH+css rotate + health 4×200. Pre-deploy DB snapshot (Mig55) → post (Mig56) = unambiguous proof. → _INDEX.
- **S83 #325 `e29391e` PASS ~4m39s** — FE-only tiny budget-subitem indent/dash + bundle BOTH-js-rotate css-FROZEN (utility-reuse, no new css chunk) + Mig-frozen-57 + tables88 + test351. → _INDEX.
- _(S77 #329 `e823694` FE-only banner Trả-lại · #328 `424131d` BE-only notify-block test-gate-KEY · #327 `fa6654b` FE-list-restructure · #326 `b5aa72d` BE-authz asymmetric · #323 #322 #321 #320 PE UX batch → all PASS, FIFO-trimmed, full verbatim git + archive `_INDEX`)_
- _(S83 #325 `e29391e` FE-only budget-subitem indent js-rotate-css-FROZEN test351 · S77 #329 `e823694` FE-only banner Trả-lại · #328 `424131d` BE-only notify-block test-gate-KEY · #327 `fa6654b` FE-list-restructure · #326 `b5aa72d` BE-authz asymmetric · #323 #322 #321 #320 PE UX batch → all PASS, FIFO-trimmed, full verbatim git + archive `_INDEX`)_
- _(S74-bis #315 `8655ebf` Mig55 CcmNote · S73 #313 `1d86abc` Mig54 5-cols + endpoint-401-not-404-probe · S69b #307 `1f8947e` Office-golive seed-16/16-DB-query-proof → archive `_INDEX` + git)_
- **Older runs (S75 #301 ← S62 #286 ← … → S29 #232) full verbatim → git `d2f52ba` + `archive/2026-06.md`** (incl #291 06-16 FAIL forensic [gotcha #65]); pre-S38 → `archive/2026-05-{runs,q2,q3,q4}.md`.

View File

@ -74,6 +74,7 @@ UI `disabled={!canX}` + BE helper `EnsureCanXAsync(id, userId)` throw 403 (NOT i
## 📅 Recent activity (FIFO — older → archive/git)
- **2026-06-25 (S88 PE stability cleanup — FROZEN-module mechanical relocation + 1 stale-comment fix BE — NO migration, 3 edit/2 file, em-main spec deterministic 100% file/line/boundary given → ACCEPT Case 1):** Pure mechanical move ZERO behavior-change. **T1 misplaced-command relocate:** `SetPeCcmBudgetPeriodCommand`+Validator+Handler (BUDGET setter writes `pe.CcmBudgetPeriodAmount` Section B "NS kỳ này" CCM, Mig 59) sống NHẦM trong suggested-PRICE file → move về budget file. (a) REMOVE `PeSuggestedPriceFeatures.cs` lines 144-199 (blank + `===== CCM NS kỳ này =====` header → `SetPeCcmBudgetPeriodCommandHandler` closing `}`); file end clean tại line 143 (`UpdatePeSuggestedPriceCcmCommandHandler` `}`), suggested-price PRO/CCM trên giữ nguyên. (b) APPEND `PeWorkItemBudgetFeatures.cs` END sau line 200 (`UpdatePeBudgetCcmCommandHandler` `}`) → cạnh budget setters `UpdatePeBudgetPro/Ccm`. Code BYTE-IDENTICAL (logic/authz Admin\|CostControl fail-closed/validation GTE(0).When(HasValue)/absolute-set null=clear/changelog UNCHANGED); CHỈ prepend 1 comment line `// [S88] Chuyển từ PeSuggestedPriceFeatures.cs về đây (đúng nhóm budget setter).`. Usings 8-header IDENTICAL 2 file → KHÔNG add duplicate. Namespace `SolutionErp.Application.PurchaseEvaluations` SAME → MediatR type-resolution transparent, controller `:125` KHÔNG đụng. **T2 stale-comment fix:** `Dtos/PurchaseEvaluationDtos.cs` ~250-254 comment trên `decimal WinnerQuoteTotal` ghi SUM "của SelectedSupplierId" (OLD single-winner) → verified actual compute `PurchaseEvaluationFeatures.cs:1093-1099` = `Suppliers.Where(s=>s.IsWinner)` (multi, Mig 58) → sửa CHỈ comment-text "MỌI đơn vị IsWinner (multi-winner Mig 58)". Field/code UNCHANGED. **LESSON:** frozen-relocation = read BOTH file FIRST exact boundary + verify usings-superset target + verify stale-comment claim vs real compute-site TRƯỚC sửa (KHÔNG trust comment blind, mirror S35 EF-source-of-truth). Build `SolutionErp.Application.csproj -v q` **0 warn 0 err** (4s). KHÔNG test (test-specialist)/FE/mig/commit. Tag `[s88, frozen-relocation, byte-identical-move, mediatr-type-resolve-transparent, stale-comment-verify-compute-site, no-behavior-change]`.
- **2026-06-24 (PE Section B 3-cột THỰC HIỆN — CCM "NS kỳ này" cột riêng BE — Mig 59 `AddCcmBudgetPeriodToPurchaseEvaluation` 3-file, 5 edit/0 new file, em-main CHỐT spec+API-contract 100% → ACCEPT Case 1):** Section B tách 3 cột Dự án|PRO|CCM giống Section A. PRO=`BudgetPeriodAmount` sẵn có; CCM nhập cột riêng độc-lập. COOKIE-CUTTER mirror S77 suggested-price-Ccm + S65b HoSoLink one-column. (1) `PurchaseEvaluation.cs` +`decimal? CcmBudgetPeriodAmount` sau ExpectedRemainingAmount. (2) `PurchaseEvaluationConfiguration.cs` +`HasColumnType("decimal(18,2)")` sau ExpectedRemaining (spec asked literal decimal(18,2); siblings dùng HasPrecision(18,2) NHƯNG emit identical column — tôi follow spec verbatim HasColumnType). (3) Mig: Up=1 AddColumn decimal(18,2) nullable NO table/index, Down=1 DropColumn, snapshot verified — clean mirror Mig 52. (4) DTO `PurchaseEvaluationDetailBundleDto` +`decimal? CcmBudgetPeriodAmount` sau ExpectedRemainingAmount TRƯỚC BudgetSummary + projection PEFeatures.cs:1110 positional-insert đúng order (positional record → projection order BẮT BUỘC khít). (5) Setter `SetPeCcmBudgetPeriodCommand(Guid Id, decimal? CcmBudgetPeriodAmount):IRequest` append `PeSuggestedPriceFeatures.cs` (same concern PE role-gate setters) + Validator `GreaterThanOrEqualTo(0).When(HasValue)` (budget=0 hợp lệ) + Handler fail-closed Forbidden Admin|CostControl TRƯỚC side-effect (S56 #5) + absolute-set null=clear (gotcha #73) + changelog Header (sibling-consistency; spec min=SaveChanges). (6) Controller `[HttpPatch("{id:guid}/budget/ccm-period")]` + `CcmBudgetPeriodBody` (spec EXPLICIT PATCH dù siblings PUT — follow spec). **Call-site verify:** `new PurchaseEvaluationDetailBundleDto(` repo-wide → EXACTLY 1 (PEFeatures:1100, đã sửa) → positional-safe. Submit-guard `BudgetPeriodAmount` PRO KHÔNG đụng. Build SolutionErp.slnx (6 proj incl 2 test, gotcha #65) **0 warn 0 err**. KHÔNG apply DB/FE/test/commit. Route FE/test mirror: DTO field `ccmBudgetPeriodAmount` camelCase + PATCH `/api/purchase-evaluations/{id}/budget/ccm-period` body `{ ccmBudgetPeriodAmount }`. Tag `[pe-section-b-3col, ccm-budget-period, mig59, one-column-no-table, absolute-set-fail-closed, positional-dto-order, patch-per-spec]`.
- **2026-06-19 (PE chuông báo approver BE — NO migration, 1 edit/1 file, em-main spec deterministic 100% → ACCEPT Case 1):** Tra Sol (Zalo) "không thấy chuông — việc có hồ sơ cần duyệt": approver Cấp hiện tại KHÔNG nhận notify khi phiếu ENTER/ADVANCE tới ChoDuyet (chỉ drafter báo terminal). FIX = +1 block trong `LogTransitionAsync` (PurchaseEvaluationWorkflowService.cs ~line 1058) NGAY SAU drafter-notify, KHÔNG endpoint mới (Pattern 4 — notify = side-effect của Submit/Approve-advance, cả 2 đã gọi LogTransitionAsync + end ChoDuyet). **Resolution mirror EXACT canonical** `EnsureActorInLevel` (line ~301): `db.ApprovalWorkflows.AsNoTracking().Include(Steps).ThenInclude(Levels)``stepsOrdered[CurrentWorkflowStepIndex]` (bounds-check) → `.Levels.Where(Order==CurrentApprovalLevelOrder && ApproverUserId!=Guid.Empty && !=actorUserId).Select(ApproverUserId).Distinct()``NotifyManyAsync(ids, Generic, "Phiếu cần bạn duyệt: {MaPhieu??TenGoiThau}", "Có phiếu Duyệt NCC đang chờ bạn duyệt.", /purchase-evaluations/{Id}, refId:Id, ct)`. **Recon:** (1) `ApprovalWorkflowLevel.ApproverUserId` = non-null `Guid` (NOT Guid?) → "exclude null" = `!=Guid.Empty` defensive (OR-of-N rows mỗi 1 NV). (2) `NotifyManyAsync` sig verified = `(IEnumerable<Guid>, NotificationType, title, string? desc, string? href, Guid? refId, CT)` khớp urgent-feature ref. (3) actor-exclude qua `!= actorUserId` (Guid != Guid? lift OK). **Guard V2-only:** `ApprovalWorkflowId is Guid && CurrentWorkflowStepIndex is int && CurrentApprovalLevelOrder is int` → V1/no-workflow skip. **Best-effort try/catch** nuốt lỗi (phiếu đã SaveChanges; notify fail KHÔNG rollback). Block đọc pointer SAU set → Submit (idx0/lvl1) + Approve-advance (pointer advance rồi LogTransition) đều fire đúng. Build SolutionErp.slnx (2 test project, gotcha #65) **0 warn 0 err**. KHÔNG test/FE/mig/commit. Tag `[pe-approver-bell, notify-many, log-transition-hook, no-mig, v2-only, best-effort-trycatch, pattern4-side-effect]`.
- **2026-06-19 (S77 PE +2 ghi-chú giá đề xuất BE — Mig 57 `AddPeSuggestedPriceNotes` 3-file, 6 edit/0 new file, COOKIE-CUTTER S73 suggested-price + S74 CcmNote, em-main CONTRACT-locked names 100% → ACCEPT Case 1):** PRO dải Min/Max + CCM 1 giá (S73) +ô GIẢI THÍCH vì-sao min vs max (anh Kiệt FDC + Tra Sol). 2 cột nullable, no table/index/backfill. (1) `PurchaseEvaluation.cs` +`string? ProSuggestedPriceNote`/`CcmSuggestedPriceNote` ngay sau CcmSuggestedPrice. (2) `PurchaseEvaluationConfiguration.cs` +`HasMaxLength(1000)` ×2 sau ApprovedPriceSource (KHÔNG index — free-text). (3) Mig: Up=2 AddColumn nvarchar(1000) nullable, Down=2 DropColumn, snapshot ×2 verified — clean mirror Mig 55. (4a) `UpdatePeSuggestedPriceProCommand` +trailing `string? Note=null` + validator `MaximumLength(1000)` MATCH-EF (S35) + handler **absolute-set** `pe.ProSuggestedPriceNote=request.Note` (null=clear, gia-đình text-field) + changelog part khi đổi. (4b) `UpdatePeSuggestedPriceCcmCommand` mirror + changelog suffix "(kèm ghi chú)". Role-gate KHÔNG đụng (Note gated y giá: Forbidden fail-closed TRƯỚC side-effect đã có). (5) Controller `SuggestedPriceProBody`/`CcmBody` +`string? Note` + pass `body.Note` vào cmd (4th/3rd positional). (6) `PurchaseEvaluationDetailBundleDto` +`ProSuggestedPriceNote`/`CcmSuggestedPriceNote` sau CcmSuggestedPrice + projection PEFeatures.cs positional-insert ĐÚNG order (DTO positional → order khít projection BẮT BUỘC). **Call-site grep verify (S65b lesson):** `new UpdatePeSuggestedPrice(Pro|Ccm)Command\(` repo-wide → 2 controller (đã sửa) + 18 test named-arg/positional-dừng-trước-Note → trailing-optional fully backward-compat, 0 test edit. FE 0 ref (đúng — implementer-frontend next). Build SolutionErp.slnx (2 test project, gotcha #65) **0 warn 0 err**. KHÔNG apply DB/FE/test/commit. Route: `note` camelCase qua PUT `/suggested-price/{pro,ccm}` body + GET detail bundle. Tag `[s77, pe-suggested-price-note, mig57, two-column-no-table, absolute-set, trailing-optional-callsite-safe, positional-dto-order]`.

View File

@ -6,6 +6,21 @@
---
## 🆕 S88 D2 (2026-06-25) — PE multi-winner OUTWARD FE ×2 app (BE đã đổi: create→LIST, list DTO winner names)
3 edit OUTWARD, em-main chốt FE behavior, coord BE (impl-backend đã viết). 2 SHA-pair + 1 type-DIVERGE.
- **E1 CreateContractDialog (PeDetailTabs ~:2144):** mut type `{contractId:string}`→**`{contractIds:string[]}`** (BE tạo 1 HĐ/mỗi IsWinner). onSuccess: toast `` `Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.` `` + `navigate(/contracts/${res.data.contractIds[0]})` (giữ destination `/contracts/${...}` của bản single). Dialog GIỮ joined winner-names (:2174) + 1 ContractType-cho-mọi-winner. Bản gốc KHÔNG invalidate → nothing to preserve.
- **E2 PeListPanel label (~:168):** `{p.selectedSupplierName && …✓…}` → prefer `winnerSupplierNames.join(', ')` khi `winnerSupplierNames?.length>0`, else `selectedSupplierName`. Giữ ✓ + `text-emerald-600`. Ternary lặp 2 lần (guard + render) — optional-chain `?.length` an-toàn undefined.
- **E3 type (DIVERGE 2 file):** `PeListItem` +`winnerSupplierNames:string[]` sau urgent-flags (anchor `isUrgentByPro/Ccm`+`}` byte-identical 2 app → Edit same string ×2, grep-confirm KHÔNG vào inter-app diff, stat giữ 30ins/26del). Create-contract response = **inline generic** trong PeDetailTabs (KHÔNG named type ở types file) → E3b no-op; `contractId:string|null` :121/:443 = link-HĐ-đã-gắn field KHÁC, GIỮ.
- **SHA256 ×2 pair:** PeDetailTabs `9d8120b6…`, PeListPanel `c97e133d…` (cp admin→user, git-diff --no-index IDENTICAL). Build PASS ×2 (admin `index-C3j9HaFh.js` 2.18s / user `index-Ct2cpC7h.js` 2.23s, 0 TS err). Pre-existing warn: >500KB + realtime.ts dynamic. NO BE/test touch. **LESSON: OUTWARD multi-winner = winnerSupplierNames[] (BE null selectedSupplierName khi ≥2) → FE prefer-list-fallback-single; type DIVERGED ≠ cp → edit-same + verify-not-in-diff (S87+ confirm).** Tag `[s88, pe-multi-winner-outward, contractIds-list, winner-names-fallback, sha256-2pair, type-diverge-edit-same]`.
## 🆕 S87+ (2026-06-25) — FROZEN PE behavior-preserving cleanup ×2 app (em-main chốt, coord BE null-safe)
4 edit `PeDetailTabs.tsx` (SHA-pair) + 2 wording `types/purchaseEvaluation.ts` (DIVERGED — KHÔNG SHA-pair, edit identical 2 file).
- **E1 HoSoLink clear:** `HoSoLinkRow.save` (~:2020) `hoSoLink: hoSoLink || null`**`hoSoLink.trim()`** (state đã string nên `??""`=no-op; trim ra `""` khi xóa). Coord BE `UpdatePeDraftHandler` null-safe: `""`=CLEAR, null/omit=PRESERVE, val=set (fix data-loss xóa link khi edit header). **GUARD: InfoTab.save (:817) + `PeHeaderForm` PUT body GIỮ OMIT hoSoLink** (omit=preserve = đúng fix) — verify KHÔNG thêm.
- **E2 #70 stale-echo guard:** 2 ô PRO `adjustMut` (row3 NS-kỳ-này :1548 + row8 GT-còn-lại :1649) chỉ `saving={adjustMut.isPending}``|| peFetching` (`replace_all` đúng 2 occ; `peFetching` computed :1268). Sibling ccmPeriod/proPrice/ccmPrice/proMut/ccmMut đã có. Chống fast-double-save clobber stale `ev.*` (2 ô share 1 absolute-set mut cross-echo).
- **E3 stale comment :2507:** "AddItemDialog giữ (dead)…" → đã xóa (grep chỉ comment match; live = `DetailDialog`). Sửa thành "nút đã bỏ (dialog = DetailDialog)".
- **E4 type wording (2 file):** `previousSelectedTotal`/`currentProposalTotal` inline cmt "của SelectedSupplier" → "NCC trúng (IsWinner)" (BE giờ multi-winner). Block 7-dòng byte-identical 2 app → edit same → KHÔNG new divergence (grep-confirm my-line ∉ diff).
- **SHA256 PeDetailTabs ×2 = `36292924…`** (cp admin→user, git-diff empty). PeHeaderForm untouched `987291c1…` identical. Build PASS ×2 (admin 1946mod `index-CqiQWR8e.js` / user 1935mod `index-Cp0DuKFQ.js`, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO BE/test touch. **LESSON: type files DIVERGED (S76/S79 color+prose) ≠ SHA-pair → mirror = edit-identical-block + verify-not-in-diff, KHÔNG cp.** Tag `[s87plus, pe-frozen-cleanup, hosolink-clear-empty, gotcha70-guard, type-diverge-edit-identical]`.
## 🆕 S79 (2026-06-19) — PE detail: live thousand-sep + ghi chú giá PRO/CCM + typo (×2 app)
3 task presentation+wiring `PeDetailTabs.tsx` (+`PeWorkspaceCreateView.tsx` typo). BE thêm `note` body song song; FE chốt UX rồi.
- **A live phân cách:** chỉ **2 comp** thiếu sep — `VndInlineEdit` (~990) + `BudgetCell` (~1100): đổi `onChange` `value.replace(/[^\d.]/g,'')`**`formatVndInput(parseVnd(e.target.value))`** (format-on-keystroke, reuse helper module-level :51/52). Dialog money inputs (2111/2555/2645) ĐÃ dùng `formatVndInput`/`parseVnd` sẵn → no-op. KHÔNG đụng phone(tel)/%/qty.

View File

@ -70,8 +70,14 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **2026-06-25 PE FROZEN patchwork audit AREA-6 (FE consumers BOTH apps — multi-winner + mirror-drift + dead-code + #70):** ⭐ Post-Mig58 multi-winner (`isWinner` bool) NOW implemented (supersedes 06-24 "SINGLE" recon). **All 6 PE components + 3 PE pages SHA-IDENTICAL fe-admin↔fe-user** (PeDetailTabs/PeHeaderForm/PeUrgentChips/PeListPanel/PeWorkspaceCreateView/PeWorkflowPanel). type files DIFFER but ONLY badge-tint (-700→-800 ink) + comments (intentional per-app UX, struct fields identical incl ccmBudgetPeriodAmount/isWinner/8 Allow flags). **RESIDUAL single-winner BUG (low):** `PeListPanel.tsx:168` renders `p.selectedSupplierName` (list-card winner label) → BLANK when ≥2 joint winners (BE list-DTO `PurchaseEvaluationFeatures.cs:615,706` projects from `SelectedSupplierId` which BE sets null when count≠1 `PurchaseEvaluationDetailFeatures.cs:431`). List DTO has NO isWinner/winner-list field; detail-view multi-winner correct everywhere (8 detail consumers all converted: computeGiaChaoThau:78, missingForApproval:195, submitChecklist:229, ThongTinChonThau:1949/1970, Section-B row4:1578, CreateContractDialog:2171 fallback `||selectedSupplierName`, HangMucCard toggle:2560). **#70 stale-echo ASYMMETRY:** budget Section-B CCM-period editor gates `ccmPeriodMut.isPending||peFetching` (`PeDetailTabs.tsx:1563`) but PRO sibling row3/row8 `adjustMut` gates `.isPending` ONLY (`:1548,:1649`, NO peFetching) — rapid PRO row3→row8 absolute-set pair can echo stale ev.* pre-refetch (same #70 class CCM guards). 3 peFetching sites exist (945/1268/1736). **Stale comment** PeDetailTabs:2507 references `AddItemDialog` "giữ(dead)" but function DELETED (grep 0 def) — misleading comment only, not dead code. PeWorkflowsPage admin-only = by-design (workflow config not mirrored). → _INDEX.
- **2026-06-25 PE FROZEN patchwork audit AREA-2 (budget matrix + Section B 3-col, Mig 56/59):** ⭐ **BAND-AID CONFIRMED** `SetPeCcmBudgetPeriodCommand+Validator+Handler` (`PeSuggestedPriceFeatures.cs:153-199`) is a BUDGET setter (writes `pe.CcmBudgetPeriodAmount`) MISPLACED in a price-feature file; SHOULD live in `PeWorkItemBudgetFeatures.cs` (where UpdatePeBudgetPro/Ccm sit). Test+comments already self-flag (PeCcmBudgetPeriodSetterAuthzTests.cs:14). ⚠️ **Role-gate ASYMMETRY (intentional, NOT a bug — but inconsistent surface):** CCM budget setters = flat role-gate (CostControl|Admin fail-closed): UpdatePeBudgetCcm `PeWorkItemBudgetFeatures.cs:160-165` + SetPeCcmBudgetPeriod `PeSuggestedPriceFeatures.cs:176-181`. BUT the PRO sibling `/budget-adjust``AdjustPurchaseEvaluationBudgetCommandHandler` `PurchaseEvaluationFeatures.cs:349-394` = **phase/drafter/workflow-slot gate** (Drafter when Nháp/TraLai; Approver+AllowApproverEditBudget F4 when ChoDuyet) NOT role-PRO. So "PRO col" & "CCM col" of same Section-B row 3 have DIFFERENT authz models (PRO=drafter-flow on `pe.BudgetPeriodAmount`; CCM=role-flow on `pe.CcmBudgetPeriodAmount`). Both per-PE fields (NOT PeWorkItemBudget per-pair — that's the matrix rows Initial/Adjustment). Section-B **"Dự án" col = intentional FE-only "—"** hardcoded (`PeDetailTabs.tsx:1520,1542,1596,1606`; Domain `PeWorkItemBudget.cs:19` "chưa wire BE — sau mới có người dự án nhập") — documented, NOT a stub-bug. Controller class-level `[Authorize]` only (`PurchaseEvaluationsController.cs:15`) → handler gates ARE the boundary. Tests solid (PeWorkItemBudgetTests 30+ cases, PeCcmBudgetPeriodSetterAuthzTests 7). → _INDEX.
- **2026-06-24 PE 3-change recon (Lưu Trần/anh Kiệt CCM — multi-winner + budget=0-valid + per-step terminal):** ⭐ (A1 winner) cardinality=**SINGLE** scalar `PE.SelectedSupplierId Guid?` (`PurchaseEvaluation.cs:28`); cmd `SelectPurchaseEvaluationWinnerCommand(PEId,Guid?)` `PurchaseEvaluationDetailFeatures.cs:392` handler `:398-423` (null=clear S85-C5); ctrl `/select-winner` `PurchaseEvaluationsController.cs:167-172`. winnerQuoteTotal=**DERIVED** SUM `Quote.ThanhTien WHERE SupplierId==Selected` 3 sites: read `PurchaseEvaluationFeatures.cs:1089` + submit-guard `PurchaseEvaluationWorkflowService.cs:191` + ccm-delegate `:868`. ⚠️ Quote ALREADY has `IsSelected bool` (`PurchaseEvaluationQuote.cs:15` per-item winner, UNUSED by total). Multi-winner = NEW junction OR reuse IsSelected + change predicate `==Selected``IN selectedSet`. FE single `<Select>` `PeDetailTabs.tsx:961-991` (NccSelectorRow). (A2 budget=0) BLOCKED 2 layers: validator `.GreaterThan(0).When(HasValue)` create `:55` + UpdateDraft `:233` + budget-adjust `:339`; submit-guard `effectiveBudget<=0`→missing `PurchaseEvaluationWorkflowService.cs:213-216` (effective=`BudgetPeriodAmount??ProInitialAmount` S85-D3). FE `effBudget<=0` `PeDetailTabs.tsx:211,237` canSubmit `:242`. To allow 0: relax `>0``>=0` validator + submit `<0` + FE. winnerQuoteTotal-gate SEPARATE `:194`. (A3 terminal) `ApprovalWorkflowLevel` already has 6 per-slot bool flags (`ApprovalWorkflow.cs:104-131`: AllowReturnOneLevel/OneStep/ToAssignee/ToDrafter/EditDetails/EditBudget/**SkipToFinal**); `CeoApprovalThreshold decimal?` on WORKFLOW `:43`. Finalize branches `ApproveV2Async`: terminal `nextIdx>=steps.Count` `:905-916` + ccm-delegate opt-in `:854-891` + skipToFinal `:803-844`. "Approve-ends-here" = NEW per-Level bool mirror SkipToFinal but set Phase=DaDuyet (like ccm-delegate but no threshold). Designer FE = `fe-admin/ApprovalWorkflowsV2Page.tsx`. **schema-decision forks→em-main:** (1) multi-winner storage: reuse Quote.IsSelected vs new PE↔Supplier winner-junction (2) budget=0 vs budget-null distinction post-relax (3) terminal flag scope per-Level vs per-Step + interaction w/ CeoApprovalThreshold (both finalize → ordering). → _INDEX.
- **2026-06-25 PE AREA-4 UpdatePeDraft edit-preserve audit (gotcha #73-class hunt, FROZEN Mig 59):** ⭐ **LATENT #73-CLASS BUG FOUND:** `UpdatePeDraftHandler` absolute-sets `entity.HoSoLink = request.HoSoLink` (`PurchaseEvaluationFeatures.cs:280`) BUT `PeHeaderForm.tsx` PUT payload (admin+user `:112-120`) OMITS hoSoLink → editing header CLEARS HoSoLink (Mig 52) to null. HoSoLink set separately in `PeDetailTabs.tsx:2012-2020` HoSoLinkRow (re-sends all absolute fields but only IT edits link). Two PUT call-sites to SAME /purchase-evaluations/{id} endpoint are mutually destructive. PeHeaderForm reachable as EDIT (`PurchaseEvaluationCreatePage.tsx:13,26` editId from URL `?id=`, deep-link "Sửa header"). Field table: TenGoiThau/DiaDiem/MoTa/PaymentTerms absolute-set BUT PeHeaderForm re-sends all (safe); HoSoLink absolute-set + OMITTED (BUG); ApprovalWorkflowId/Budget/ExpectedRemaining/WorkItemId all null-safe `if(x is not null)` `:285-297` (S85 fix). ExpectedRemaining absolute-set in budget-adjust `:423-424` (FE sends both, OK). Require-workflow NOT duplicated: CREATE-validator `:48` + submit-guard `PurchaseEvaluationWorkflowService.cs:237` = different lifecycle (coherent defense-in-depth); UPDATE-validator correctly omits. Tests SOLID: PeUpdateDraftWorkflowPreserveTests (3 null-safe cases incl ApprovalWorkflowId+Budget) + PeSubmitGuardAndBypassTests (14) + PurchaseEvaluationDraftGuardTests (8). NO HoSoLink-preserve test (test-gap for the bug). → _INDEX.
- **2026-06-25 PE AREA-3 finalize/price audit (FROZEN Mig 59 Run#346 — patchwork hunt):** ⭐ TWO finalize mechanisms COEXIST + BOTH LIVE (NOT dead): (1) `AllowApproverFinalize` per-Level AUTO config (Mig 58) `PurchaseEvaluationWorkflowService.cs:861-877` + (2) `finalizeByCcmDelegation` OPT-IN param threshold-gated (Mig 54) `:879-915`. AUTO runs FIRST + returns → if both eligible, OPT-IN flag silently ignored (coherent, no double). `finalizeByCcmDelegation` FULLY wired E2E (ctrl `:141`+`:351` · Feature `:474,:526` · IPe…Service:32 · FE both `PeWorkflowPanel.tsx:184,584` ô-tích) — NOT orphan. ALL 3 DaDuyet paths bind giá chốt via `ApplyApprovedPriceOnFinalize` (private static `:956-971`, allow-list `{Ncc,ProMin,ProMax,Ccm}` `:955`): terminal `:933` + AllowApproverFinalize `:863` + ccm-delegate `:901`. isSystem-exempt branch (`:961-964`) = DEAD via public ApproveV2Async (needs decision=AutoApprove but APPROVE-STEP gates Approve; PE has NO SLA-job — only Contract) — tested-only-via-reflection (PeApprovedPriceFinalizeTests:238-295, header DISCLOSES "defensive/dead"). **MISPLACED-CODE CONFIRMED (known band-aid):** `SetPeCcmBudgetPeriodCommand` (Mig 59 Section-B BUDGET setter) lives in `PeSuggestedPriceFeatures.cs:153-199` (a SUGGESTED-PRICE file) — wrong file, own header admits Section-B. Has test `PeCcmBudgetPeriodSetterAuthzTests.cs` (NOT test-gap). DTO `currentLevelOptions` per-NV-discriminated `PurchaseEvaluationFeatures.cs:950-963` = same source as service `matchingLevel` (coherent). 4 finalize test files all present + green-design. → _INDEX.
- **2026-06-25 PE AREA-5 urgent/notify/TZ audit (FROZEN Mig 59 — gotcha #72):** ⭐ (a-TZ) `UtcDateTimeJsonConverter` registered ONLY on `AddControllers().AddJsonOptions` `Program.cs:33-34` — covers REST. ⚠️ **SignalR push BYPASSES it** (`AddSignalR()` no `.AddJsonProtocol`, separate STJ): `NotificationPushInterceptor.cs:77` pushes `createdAt=n.CreatedAt` over `SendAsync`. BENIGN at runtime: `n.CreatedAt=clock.UtcNow`=Kind.Utc (DateTimeService.cs:7) → default STJ writes 'Z' for Utc-kind, FE `new Date(iso)` NotificationBell.tsx:23 OK. Latent: any future SignalR push of an EF-READ (Unspecified) DateTime would skew. NOT a bug now, inconsistency-low. Converter handles DateTime? auto (non-null) — no double-convert. (b-authz) Urgent SET/UNSET branches `PurchaseEvaluationUrgentFeatures.cs:54-73` SYMMETRIC + fail-CLOSED both (`if(!allowed)throw` BEFORE mutate); 14 tests `PeUrgentToggleAuthzTests` cover plain/TP/Admin/drafter×set/unset. No fail-open. Controller class-`[Authorize]` only `:91` → handler IS boundary. (c-notify) Drafter excluded 2 sites: advance `:1138` (`!=DrafterUserId`) + terminal-result KEPT `:1085`; tests `PeApproverNotifyExcludesDrafterTests` RED-on-remove. ⚠️ **POSSIBLE double-notify-then-skip:** submit `:252` LogTransition notifies Cấp-1 approvers (excl drafter+actor) THEN `:260` ApplyDrafterBypass advances pointer + `:650/660` re-notifies new level — if drafter=Cấp1 w/ co-approver, co-approver pinged for a level immediately bypassed. Low-sev (extra ping, not wrong recipient); no de-dup across the 2 LogTransition calls. → _INDEX.
- **2026-06-23 PE UAT recon D2/D4/D5 (anh Kiệt FDC go-live):** ⭐ (D2-urgent authz) `PurchaseEvaluationUrgentFeatures.cs:49-50` BẤT-ĐỐI-XỨNG `isPro=IsUrgent?hasPro:(hasPro&&isDeptManager)` — GẮN=role-only / GỠ=DeptManager-only. NO DrafterUserId match (handler chỉ load entity, never reads DrafterUserId). Để drafter gỡ cờ mình: sửa :49-50 OR `actor.Id==entity.DrafterUserId`. Endpoint `PUT /purchase-evaluations/{id}/urgent` (controller:91). ⚠️ cờ KHÔNG split-by-drafter (PRO/CCM = role, drafter ≠ tất nhiên PRO/CCM). (D5) FE PE-create dropdown=`PeWorkspaceCreateView.tsx:94-104` GET `/approval-workflows-v2?applicableType={1|2}` filter `isUserSelectable`. **Dev DB: PE type1/type2 selectable=0** (6 selectable đều type3-9 Contract/Proposal/Leave/OT/Xe/Ticket NOT PE) → prod UNVERIFIED (SSH-paren-quote-trap). Validator REQUIRE workflow CẢ 2 tầng: `PurchaseEvaluationFeatures.cs:48` create NotEmpty (S83) + submit-guard `PurchaseEvaluationWorkflowService.cs:225` ConflictException. (D4 required) create-validator `:35-57`: Type/TenGoiThau≤500/ProjectId/WorkItemId/ApprovalWorkflowId NotEmpty + BudgetPeriodAmount>0-when-present. Submit-guard `:174-228`: SelectedSupplierId + winnerQuoteTotal>0 + BudgetPeriodAmount>0 + Bảng-so-sánh-attachment + ApprovalWorkflowId. → _INDEX.
- **2026-06-20 governance-landing map (RC-sig + User-Mark H12/13 + objective-criteria):** ⭐ WHERE-to-land 3 AI_INFRA gov broadcasts. `harness-11-engine.md` = CANONICAL engine (D5/D6/D7 safety-tier + D8 one-direction-lock + D9 single-writer + D10 Bash-residual + CAVEAT no-OS-hook). `error-ledger.md` = §L.a AS-1..13 + §L.b 7-step. adap-report FORMAT richest = `2026-06-18-Governance-harness-11.md`. rules.md §6.6 = objective-criteria. **report-before-stamp ⊂ D7 (extend, đừng duplicate → C3 vocab-fork).** → _INDEX.
- **2026-06-20 Harness-14 Eval/Budget/Outcome audit:** ⭐ H-14 = time/age/recency-decay KHÔNG được làm căn-cứ cắt feature. (1) BUDGET aligned: `memory-budget.json` 0 decay/TTL/age hit; `keep_floor=5`=newest-protection (drains OLDEST), cap seed-by-MEASURE. (2) detectors = canonical-anchor-vs-STATUS + disk-cross-check, ZERO age-window. (3) EVAL genuine: `eval/golden-set-solution_erp.jsonl` (14q) + `evaluator.md` recall@5≥0.7 — weekly-manual no-automation. (4) anti-downgrade = Harness-8 "all-inherit chất-lượng>chi-phí" PARTIAL (no generic rule in rules.md, grep 0). → _INDEX.

View File

@ -7,6 +7,7 @@
## 📁 Area memory (L2 on-demand — Read khi review vùng tương ứng)
- [S62 PE budget soft-warning](project_s62_pe_budget_soft_warning.md) — PASS: hard-block→soft-warning; submit-guard intact + validator giữ `BudgetPeriodAmount>0`, row8 negative-safe (additive-only). Validator class `PurchaseEvaluationFeatures.cs:317`.
- [Wire/mirror claim verification anchors](feedback_wire_claim_verification_anchors.md) — sha256 twin-file · `git diff -U0` isolate true-adds · `allowNegative` bleed check · guard-still-intact grep.
- [S89 PE budget-sectionB review](project_s89_pe_sectionb_review.md) — investigator 3/3 upheld (do-not-touch FROZEN) + CAUGHT MISSED #70 race: Block B PRO row3/row8 share adjustMut + cross-echo `ev` stale + NO `||peFetching` (every other cell gated). authz-lens ≠ concurrency-lens.
---
@ -62,6 +63,10 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **S91 (2026-06-25) Adversarial PE D2 create-contract 1→N multi-winner + list winner-names (OUTWARD, FROZEN module, NOT-deployed) — PASS (genuine, 0 blocking):** This IS the fix for the dead-end I caught in S89-bis. BE handler (`CreateContractFromEvaluationFeatures.cs:45-152`) rewritten `IRequest<Guid>``List<Guid>`, 1 Contract per `IsWinner` supplier. The S89-bis hard-block `if(SelectedSupplierId is null) throw` REMOVED → replaced `winners=Suppliers.Where(IsWinner); if Count==0 throw` (joint ≥2 now accepted). **All 5 spec concerns verified true:** (a) **codegen mid-loop SaveChanges flushes ONLY seq-row** — loop-1 builds `contract` LOCAL (never `db.Contracts.Add`), `GenerateAsync:51 db.SaveChangesAsync` on SHARED scoped ctx flushes only the tracked `ContractCodeSequences` row; `pe` (tracked via Include) NOT mutated until `pe.ContractId=contracts[0].Id` at :148 AFTER both loops; loop-2 does the Add+changelogs+final SaveChanges :150. (b) **GiaTri**: single→`detailsSum=Details.Sum(ThanhTienNganSach)` (legacy); ≥2→per-winner `Details.SelectMany(Quotes).Where(q.PurchaseEvaluationSupplierId==w.Id).Sum(ThanhTien)` — join key is PES-row Id (Quote.PurchaseEvaluationSupplierId FK→PES.Id, entity:11 confirmed), mirrors submit-guard WorkflowService:190 + detail-bundle winnerQuoteTotal:1109-1112 byte-for-shape (all 3 layers same IsWinner+row-Id join; create sums PER-winner, guard/display sum ALL → N contracts sum to gate total, test asserts w0=140+w1=260). (c) **idempotency**: `contract.Id` from BaseEntity:5 `Guid.NewGuid()` valid pre-SaveChanges; guard `pe.ContractId is not null` :59. (d) **guards+Includes** all present (`.Include(Suppliers)`+`.Include(Details).ThenInclude(Quotes)` :48-49). (e) **no partial-create**: codegen self-commits seq per-call (Serializable tx :37/52) → later-winner throw wastes seq numbers (gap, acknowledged) but no contract Add'd, final SaveChanges not reached = clean. **Positional record arg-order (spec's #1 skeptic-ask) VERIFIED across all 3 sites** — DTO `PurchaseEvaluationListItemDto` ends `IsUrgentByPro, IsUrgentByCcm, WinnerSupplierNames` (Dtos:41-45); all 3 projections (ListPE:616, Inbox:705, ListApprovedPE:178) append the winner-names subquery as FINAL positional arg after identical-shape preceding args → no misorder. **Controller** :297 `{contractIds}` (was `{contractId}`); grep confirms ONLY 2 byte-identical PeDetailTabs read `{contractIds:string[]}`, ZERO stale `.data.contractId` readers. **FE mirror**: PeDetailTabs+PeListPanel SHA-256 byte-identical both apps; `PeListItem` type block diff=0 (winnerSupplierNames:string[] identical, only pre-existing PHASE_BADGE color/comment drift remains = documented intentional). `winnerSupplierNames?.length>0` optional-chain safe (undefined>0=false). **S89-bis loop CLOSED**: CreateContractDialog now renders `suppliers.filter(isWinner).map(supplierName).join(', ')||selectedSupplierName` (no blank); approved-pending-contract endpoint has ZERO FE consumer (picker dead → joint-blank moot). Verify: build slnx **0-err** · fe-admin+fe-user tsc exit 0 · full suite **45+374=419 PASS** 0-skip (S90 413 +6) · 2 new test classes 6/6 PASS via REAL ContractCodeGenerator Serializable-on-SQLite + REAL SQLite collection-subquery translation (NoOp-vacuous explicitly avoided: single-winner test asserts GiaTri=800 budget NOT 140 quote; 2-winner asserts 140/260 with budget 800 DIFFERENT) · mock-grep clean · no migration in diff (Mig 58 pre-committed S87) · agent-memory diff .md-only. **Anti-pattern positive-validation: cardinality single→multi conversion now propagated through ALL layers consistently (FE visibility predicate `isWinner` + BE action precondition `winners.Count` + 3 financial-total sites + dialog render) — the exact FE-shows/BE-blocks mismatch I flagged S89-bis resolved by aligning the BE action on the same IsWinner source-of-truth, not by hiding the button.**
- **S90 (2026-06-25) Adversarial PE stability-fix batch D1 (5 changes + 11 tests, FROZEN module, NOT-deployed) — PASS (genuine, 0 blocking):** Closes my own S89/S89-bis flagged latent bugs. (1) **Block B financials re-key** `SelectedSupplierId``Suppliers.Where(IsWinner)` (PurchaseEvaluationFeatures.cs:840/855): single-winner UNCHANGED proven — sole writer `PurchaseEvaluationDetailFeatures.cs:427/431` sets `IsWinner=selectedIds.Contains(SupplierId)` AND `SelectedSupplierId=Count==1?First():null` in same action ⇒ for 1 winner both predicates select identical row-set (even survives duplicate-SupplierId-rows since :427 flags all). ≥2 → sums all (approved fix). EF-safe: `peers` is IQueryable (`db.PE.AsNoTracking().Where`) so `Any(s=>IsWinner)`+`CountAsync`/`SumAsync` translate server-side; `currentProposalTotal` uses already-Included `e.Suppliers` in-mem + 1 SumAsync = no N+1. Matches submit-guard WorkflowService:189-191 byte-for-shape + winnerQuoteTotal:1093 (Quote→Detail FK mandatory so in-mem `e.Details.SelectMany(Quotes)` == direct query set). (2) **CEO-notify SaveChanges** (UrgentFeatures.cs:100): NotificationService confirmed Add-only (no internal SaveChanges) → pre-fix Director rows Add'd-but-never-flushed (only :75 flush BEFORE notify block) → silent drop + no SignalR; new SaveChanges inside try/catch = best-effort, gate :80 `!wasUrgent` snapshot@:52 = no double-notify. (3) **HoSoLink null-safe+clear-via-empty** (:280-281): contract coherent — BOTH UpdatePeDraft FE forms OMIT hoSoLink (PeHeaderForm.tsx:112-120 + InfoTab.save PeDetailTabs:817) → preserve; ONLY HoSoLinkRow:2023 sends `.trim()` ("" clears, value sets); PeWorkspaceCreateView `||null` is CREATE (api.post) not Update; NO other UpdatePeDraft caller sends null-as-clear. (4) **Setter relocation** PeSuggestedPrice→PeWorkItemBudget byte-identical (only comment header added), same namespace ⇒ MediatR resolves, controller:125 unchanged, source file ends clean. (5) **FE #70 guard** `||peFetching` added to the EXACT 2 PRO cells (1548/1649) that lacked it — now consistent w/ all siblings; `peFetching` in scope @1268; PeDetailTabs.tsx sha256 byte-identical both apps. **Tests RED-pre-fix (logic-proven all): joint-winner Current=200/Prev=100 (pre-fix 0/0 via null gate); HoSoLink null→PRESERVE (old test asserted BeNull — assertion flipped); CEO-notify asserts db.Notifications has Director row via REAL Add-only service (NoOp would pass vacuously — explicitly avoided).** Verify: build slnx 0-err/0-warn · full suite **45+368=413 PASS** 0-skip · 3 affected classes 52 PASS · fe-admin+fe-user tsc exit 0 · mock-grep clean · agent-memory diff .md-only. Joint-winner number change is the approved frozen-spec exception (soft-warning `proposalOver`:1380 now fires correctly for joint, no hard-gate keys off it). **Anti-pattern positive-validation: the impl chose the behavior-PRESERVING fix shape I prescribed in S89 — HoSoLink option(b)-equivalent (null-safe + "" clear) keeps HoSoLinkRow-clear working WITHOUT touching its FE send, resolving the option(a)-breaks-clear nuance I flagged; and Block B re-key picks `IsWinner` (the new source-of-truth) over `SelectedSupplierId` consistently across all 3 winner-total sites.**
- **S89-bis (2026-06-25) Adversarial AREA-6 FE-consumers (PE FROZEN audit, investigator verify) — 4/4 finding confirmed + 1 NEW miss:** All 4 investigator findings hold under independent Read. #1 (list-panel `PeListPanel.tsx:168` blank winner label when ≥2 joint winners) CONFIRMED — list DTO carries only `selectedSupplierName` projected from `SelectedSupplierId` (PurchaseEvaluationFeatures.cs:615/706), BE nulls it for ≥2 (`PurchaseEvaluationDetailFeatures.cs:431 selectedIds.Count==1?First():null`); detail-side fully converted to `isWinner` (verified 14 call-sites grep). do-not-touch (DTO-extend + render = behavior). #2 (#70 stale-echo asymmetry) CONFIRMED PRECISE — ONLY lines 1548+1649 (PRO `adjustMut` absolute-set pair echoing un-edited sibling from `ev.budgetPeriodAmount`/`ev.expectedRemainingAmount` 1551/1652) gate bare `{adjustMut.isPending}`; CCM sibling 1563 + suggested-price 1736 + notes all add `|| peFetching`. Adding `||peFetching` = behavior-preserving (gates Save btn during refetch only) = SAFE fix. #3 (stale `AddItemDialog giữ (dead)` comment line 2507, fn deleted grep=0) CONFIRMED safe drop. #4 (mirror clean) CONFIRMED ACCURATE — 6 PE components SHA-256 byte-identical both apps (verified), type-file diff is LINE-NUMBER-only on structural fields (`isWinner`/`ccmBudgetPeriodAmount`/`winnerQuoteTotal`/`allowApproverFinalize` content-identical, fe-user shifted by extra comment lines). **NEW catch investigator MISSED (same null-when-≥2 class, HARDER consequence): create-contract joint-winner DEAD-END.** FE `canCreateContract` gate (`PeDetailTabs.tsx:1900`) shows "Tạo HĐ từ phiếu" button on `suppliers.some(isWinner)` (≥1 incl joint ≥2) + dialog header 2171 even renders joint names; but BE `CreateContractFromEvaluationCommandHandler` (CreateContractFromEvaluationFeatures.cs:52) hard-blocks `if (pe.SelectedSupplierId is null) throw ConflictException("Phiếu chưa chọn NCC thắng")` = EXACTLY joint-winner case. Submit-guard (WorkflowService:180 IsWinner ≥1) lets ≥2-winner reach DaDuyet → user clicks Tạo → 409 despite chosen winners. ListApprovedPE picker join (CreateContractFromEvaluationFeatures.cs:137) also blanks supplier for joint. **Anti-pattern: cardinality single→multi conversion stops at the DETAIL display layer — the FE button-VISIBILITY predicate was migrated to `isWinner` but the BE ACTION it triggers + its precondition still resolve single `SelectedSupplierId` → FE-shows/BE-blocks mismatch.** Fix = behavior decision (block earlier / pick-winner / multi-contract) → do-not-touch in this area, flag em-main. Mirror byte-identical so fe-user has IDENTICAL bug.
- **S89 (2026-06-25) Adversarial AREA-4 workflow-edit (PE FROZEN audit, investigator verify) — 3 confirm / 1 confirm-but-do-not-touch:** Investigator finding #1 (HoSoLink #73-class clear-on-partial-edit) CONFIRMED real high-sev latent bug: `UpdatePeDraftHandler` PurchaseEvaluationFeatures.cs:280 absolute-set `entity.HoSoLink=request.HoSoLink` (record default null line 222) while header-edit forms OMIT hoSoLink → set link via HoSoLinkRow then edit header = silent wipe. **NEW catch investigator MISSED: a 2nd destructive call-site**`PeDetailTabs.tsx:817` InfoTab.save (Panel-1 pencil inline-edit, both fe-admin+fe-user) PUTs `{tenGoiThau,diaDiem,moTa,paymentTerms,budgetPeriodAmount,expectedRemainingAmount}` NO hoSoLink. Team comment 823-825 "echo … để PUT update không xóa nhầm" PROVES they knew absolute-set hazard + defended budget but missed hoSoLink. **fixRisk nuance: investigator's option(a) (make HoSoLink null-safe) BREAKS HoSoLinkRow clear** — HoSoLinkRow sends `hoSoLink||null` (empty→null to clear) line 2020; null-safe guard would ignore the clear. Spec FROZEN → behavior-preserving fix = option(b) only (echo hoSoLink in both header forms), option(a) needs HoSoLinkRow→empty-string change = behavior touch. Anti-pattern: **"echo-all-siblings" mitigation convention silently rots when a NEW absolute-set field (Mig 52 HoSoLink) is added but not back-filled into every sibling-echo call-site.** Finding #3 (SetPeCcmBudgetPeriod misplaced in PeSuggestedPriceFeatures.cs:153 not PeWorkItemBudgetFeatures) CONFIRMED low-sev org-debt, functional+tested (PeCcmBudgetPeriodSetterAuthzTests). Finding #4 (require-workflow NON-dup: CREATE-validator NotEmpty line 48 + submit-guard WorkflowService:237 + UPDATE-validator-omit = 3 distinct lifecycle, intentional defense-in-depth) CONFIRMED accurate verification, no patchwork. Finding #2 (no HoSoLink preserve test) CONFIRMED gap but test-before only AFTER picking the behavior-safe fix.
- **S86 (2026-06-24) PE Section B THỰC HIỆN tách 3 cột Dự án|PRO|CCM (Mig 59 `CcmBudgetPeriodAmount`) — lane [authz] PASS:** role-gate đúng — handler `SetPeCcmBudgetPeriod` byte-mirror `UpdatePeSuggestedPriceCcm` (NotFound→Forbidden order; fail-closed `!Admin && !CostControl``ForbiddenException`→403 via GlobalExceptionMiddleware TRƯỚC mọi set/changelog). FE gate `bs.canEditCcm` (BE-computed `PurchaseEvaluationFeatures.cs:812 isAdmin||CostControl`) KHỚP BIT-EXACT BE endpoint gate → no FE-show-BE-block mismatch + no FE-hide-BE-allow hole. PRO cột giữ `drafterEditable` (/budget-adjust unchanged) · Dự-án cột hardcode `—` (chưa wire, an toàn). Budget=0 hợp-lệ (`GTE(0).When(HasValue)`, test `ZeroValue_IsValid`). **Submit-guard PROOF untouched:** `grep CcmBudgetPeriod` trong Infrastructure/Services = ZERO; `missingForApproval` chỉ build trên `BudgetPeriodAmount`(PRO)+ProInitial fallback (Mig 58 "0 valid only null block"). Class `[Authorize]` any-auth controller (gotcha #44 N-A: write-endpoint fine-grained Forbidden). 7/7 new authz test PASS (Admin/CCM allow · Drafter/Procurement deny · null-clear absolute-set · NotFound-before-authz). Full suite 45+357=**402 PASS** (395+7). fe-admin+fe-user tsc 0 + 2-app byte-identical (§3.9) + Mig 3-file OK. **Lane [math] (sibling, kept):** per-cột độc-lập đúng — `colCalc(periodAmount,colFull,remaining)` đóng-gói r3/r5/r7/r8/r9+3 cmp KHÔNG cross-cột (`pro=colCalc(draftRow3,proFull,draftRow8)` · `ccm=colCalc(draftCcmRow3,ccmFull,null)`); số chung row1/2/4/6 KHÔNG nhân per-cột (r5=row1+r3 cột, cmpPeriod=r3-row4 chung, cmp56=r5-row6 chung). **Div-by-0 safe:** `fmtPct(num,denom)=denom>0?...:null` + consumer `{pct&&...}` guard → cột full=0/null KHÔNG NaN/Infinity; Dự-án cột hardcode "—" KHÔNG vào colCalc. CCM r8=default(r7) đúng spec (chưa có field CCM-remaining). **#70 note:** new ccmPeriodMut single-field body (no sibling echo) + thêm `||peFetching` gate = clean; PRO adjustMut 2-field-echo KHÔNG peFetching-gate = PRE-EXISTING (base HEAD unchanged, spec "PRO giữ luồng hiện có") không phải regression. Authz mirror byte-exact `UpdatePeSuggestedPriceCcm` (NotFound→Forbidden order, fail-closed Admin|CostControl). Build slnx 0-err + test-proj rebuild 0-err (MSB3030/3026 = Dropbox bin-lock flake, benign) + 7/7 new authz test PASS + fe-admin tsc 0 + 2 app byte-identical (mirror §3.9). Mig 3-file OK. DTO positional slot line-up OK.
- **S82 (2026-06-21) Harness-15-v2 adopt review (governance delta, 0 code) — 3/3 lane PASS:** floor-completeness + faithfulness + honesty-regression, all clean ("genuine PASS not courtesy"). Caught 3 MINOR (đã fix): volume "~42K" stale trong mark · mark What-cell mang nội-dung S82 mà status ghi "anh-confirm S81" → flag pending-confirm · `_index` pending. **Memory-note (chống false-alarm):** `broadcasts/_index.md` sha = notify's **self-declared `content_sha256`** (frontmatter), KHÔNG recompute full-file/body → đừng flag index-vs-file "mismatch" trước khi đọc frontmatter field. **Stamped-mark mid-session edit OK** khi chỉ refresh What-cell content của decision đã-confirm + status flag pending (KHÔNG mint RC-sig mới, KHÔNG đổi tier) = accuracy-keeping, không phải frozen-record breach.
- **S76 (2026-06-19) PE budget ma-trận 3 cột Mig 56 + badge display-only — PASS:** MAJOR race = 2 PRO cell cùng-cột share mutation + echo sibling từ `bs` + fire-and-forget `invalidate()` → data-loss window (fixed **gotcha #70** useIsFetching gate). Badge role-set MUST mirror gate bit-for-bit (đảo-chiều set-lookup = forward Roles.Contains). **SURPRISE:** spec "KHÔNG migration" FALSE (diff bundle Part1+2+3) — đừng tin scope-framing em-main, đọc changed-set thật. → _INDEX S76 Part1/Part2+3.

File diff suppressed because one or more lines are too long