diff --git a/.claude/WAL.md b/.claude/WAL.md
index 3fd36b9..b72d0d9 100644
--- a/.claude/WAL.md
+++ b/.claude/WAL.md
@@ -1,40 +1,12 @@
# WAL — auto-generated, không sửa tay
-closing: S146 @2026-07-22T2325
-updated: 2026-07-22 | session: S146 | branch: main
-goal: /tiep -> /session-end DANG CHAY (Phase 1 + 1.5 xong phan lon). Cho H24-omission resume.
-
-## OWNER-DECISION (ghi de, KHONG hoi lai) — 🔴 SLOT-INDEX CHI-TANG, CAM TAI-DUNG (luat moi @S146)
-[5] session_ctx_kb = 64 TUONG-MINH · [6] hmw 4.8 MAX
-[7] ⚠️ KHOI-PHUC (bi ghi de @e7d06f2, H24-omission bat): "con lai xu ly het xong roi session-end"
- = **UY QUYEN commit+push + chay session-end**. Nguon: git show ad02483/9c68bae :.claude/WAL.md:9
-[8] session-model: (1) scaffold idempotent (cap SO van doc-quyen session-start) · (2) closed.md WIRE
- · (3) V2 auto (go cong NGUOI, GIU cong NHIP 6/15/3)
+updated: — | session: — | branch: —
+goal: (trống — không có mạch dở)
chain:
-[x] /tiep: San-3 4/5 MANH keu (du-doan truoc); ⑤ FIRE LAN DAU va DUNG. ground-truth KHOP 4/4.
- TICK NO-OP ×2 (guard chan bay +2). session-model chay TRON VONG: pause->resume->ORPHAN-L im dung.
-[x] Phase 1: implement-synthesis -> orphan 1->0 (probe: run-chua-gom 0). reviewer spawn-record
- APPEND 19758B AS-8 OK. session-log ghi xong.
-[x] §L.c 4 phep: p2 DAT (nguyen van) · p3 DAT (3/3 ky du 4 so) · p4 thu du bang-chung · p1 ⚠️ P=8 L=0 proxy
-[x] FORCE-FIRE H24 (P=8, luat "auto" = chay thang). CA 2 VAI GARBLE #53. lead-view ghi-dia-SOM
- -> cuu 3 FLAG (sweep consent sot 6 site · closed.md wired-but-declared-unwired = ghost-wire
- CHIEU NGUOC · draft noi nguoc chinh no) -> verify doc-lap + VA HET + re-verify SACH.
- lead-omission ghi-dia-MUON -> mat trang -> RESUME (ghi-dia-TRUOC) -> 4 FLAG.
-[x] 🔴 F1-omission XAC NHAN bang git: owner-decision [7] uy-quyen-push BI GHI DE (tai-dung slot).
- Khoi phuc thanh [7] + [8], them luat SLOT-INDEX CHI-TANG. Lead da hoi lai 3 luot = lam NGUOC.
-[!] h24-audit DANG CHAY (KIEM 7 FLAG cua 2 vai). V2 da chay => het cua vacuous.
-[ ] Phase 2 STATUS/HANDOFF + carry **14+6=20** (F4: 6 viec-moi TACH DOI 2 so, chua co tag
- [carry:*] nen H24-2 mu; WAL reset Phase 6 se lam mat 2 muc) — CHAN boi dong-bang §L.b(c)
-[ ] F2-omission: ghi su-co S144+S146 vao error-ledger (cung dang bi dong-bang)
-[ ] Phase 3/4 · Phase 5 squash->push (HOI ANH) · Phase 6 closed.md + reset WAL + xoa sentinel
+(rỗng)
-next: doi h24-audit -> Phase 2 (STATUS/HANDOFF + carry 14+6=20, tag [carry:*]) -> 3/4
- -> Phase 5 squash->push (🔴 KHONG hoi lai — owner-decision [7] DA uy quyen) -> Phase 6.
+next: (trống)
-verify:
-git status --short ; ls .claude/sessions/session-1/
-git show ad02483:.claude/WAL.md | sed -n '9p' # chung owner-[7] goc
-powershell.exe -ExecutionPolicy Bypass -File scripts/nhip-no-probe.ps1
+verify: (trống)
-carry: 🔢 CHO ANH (1)/snapshot (2)_context STOCK+nen (3)bookend 5/5 (4)H1/H2-hay-khong-nhip (5)trio-gat?
- +6 viec-moi (F4: gom du o pause-block:37-47) + 14 carry cu HANDOFF -> tong 20, CHUA tag [carry:*]
+carry: (sống ở docs/HANDOFF.md — 20 carry MỞ, đã re-stamp + gắn tag [carry:*] @S146)
diff --git a/.claude/agent-memory/cicd-monitor/MEMORY.md b/.claude/agent-memory/cicd-monitor/MEMORY.md
index 8b2e0a4..6757a4a 100644
--- a/.claude/agent-memory/cicd-monitor/MEMORY.md
+++ b/.claude/agent-memory/cicd-monitor/MEMORY.md
@@ -1,8 +1,9 @@
# CI/CD Monitor Agent — Persistent Memory
-- **S134 (07-17) verify 6664a61 PE lũy-kế tạm-tính [wf_f366f38d-498 · return CLEAN no-garble] `[engine: opus-worker]`:** VERDICT **GO 5/5** — Run **#399** (id=513) success 5m27s (14:27:29→14:32:56+07); gate **528 EXACT từ log** (45D+483I, +8 PePendingAccumulationTests); Mig 67 top FROZEN + sys.tables 89 (0-mig by-construction, `git diff -- *Migrations*` empty); bundle rotate ×2 admin js `3NCdyODe`→**`BVCukFP5`** css `B--dUHcm`→**`CBzUlgOA`** · eoffice js `CKsoiNCI`→**`DoxyEOxh`** css `BuntB63r`→**`BHsBUA8e`** (FE-src đổi 6 file ⇒ rotate expected; CSS = amber utilities benign #69), real-size 1.659.037/1.565.367B + LM in-window; **byte-verify #77 qua python urllib BYTES**: `pendingPriorPes`=2 + `Tạm tính`=3 + `phiếu trước cùng gói thầu CHƯA duyệt`=2 cả 2 js, control old-hash 900B **ct=text/html** 0-hit; smoke 4×200 + PE 401. Learned: (1) **urllib bytes + `body.count(marker.encode())` = bulletproof cp1252** cho marker tiếng Việt (khác subprocess text=True crash S133); (2) **ct=text/html là SPA-trap discriminator SẠCH HƠN size-919B** (shell size drift per build — 900B lượt này); (3) tasks-API có thể blip empty-200 transient → poll-loop absorb. Poll held-off verify khi running (iter 0-2) đúng bài #242. Tag `[s134, run-399, go-5-5, ct-html-discriminator, urllib-bytes-marker]`
+- **07-23 verify 4510cea (App: chuẩn-hóa mã dự-án normalize + PE attach-gate FE both-apps) [em-main spawn, return CLEAN]:** VERDICT **PASS 5/5** — Run **#407** (id=521) success ~5m36s (17:11:54→17:17:30+07); gate **532 EXACT từ log** (45 Domain + 487 Infra, 0 fail/skip) — **+4 vs prev run #406/8b6df0a (483I)** = 4 `ProjectCodeNormalizationTests` (delta-proof: dotnet summary KHÔNG in tên-test riêng → dùng delta prev-run + build-success + all-pass). NO-Mig: `git diff 8b6df0a..4510cea -- *Migrations*` EMPTY + prod history-top `AddPeApprovedBudgetSnapshot` (Mig 67, từ S133 KHÔNG phải push này) = repo-HEAD unchanged + sys.tables **89** — code-only no-drift by-construction. **Bundle JS rotate cả 2 app / CSS FROZEN cả 2** (đúng dự-đoán — PeDetailTabs.tsx ×2 = pure button-gating logic, no new Tailwind utility): admin js `DGPE6Nkb`→**`B2j1LQ-v`** css `CBzUlgOA` frozen · eoffice js `DlTwAgJj`→**`C3wbJegk`** css `BHsBUA8e` frozen; real-size 1.660.902/1.567.232B, LM 17:16:01/17:16:54+07 in-window; SPA-trap control fake-hash `index-ZZfakehash0.js`=900B **ct=text/html** (discriminator OK). smoke 4×200 (health/live+ready, admin, eoffice). Baseline shift: **Infra 464(S118)→483(run406)→487**, Mig-latest = **Mig 67** ("Mig 66" essentials-line STALE — updated). Pre-deploy snapshot captured WHILE running (đúng bài #242 verify-post-success). Tag `[run-407, pass-5-5, js-rotate-css-frozen, delta-proof-+4, code-only-no-mig]`
-- **S133 (07-17) verify d4fbced PE budget-freeze [wf_4eadbcf6-c25, return sạch — verdict landed SAU khi lead /pause; harvest-liền lúc notification, lead CHƯA re-verify độc lập]:** VERDICT **GO 5/5** — Run **#398** success ~5.5m (gate **520** = 45D+475I, 0 fail/skip); Mig 67 prod: history-top + **11 cột ApprovedBudget\* exact** + backfill **18/18 phiếu DaDuyet chua=0** + sys.tables **89** no-new-table; bundle rotate ×2: admin js `yCkX-2Bp`→**`3NCdyODe`** css `CXD7q9xH`→**`B--dUHcm`** · eoffice js `DKSD4p9k`→**`CKsoiNCI`** css `CjwZwYJo`→**`BuntB63r`**, real-size 1.657.025/1.563.373B, LM 11:18:42/11:19:36+07 in-window; byte-verify #77: `budgetFrozen`=1 + `Ngân sách chốt tại thời điểm duyệt`=1 cả 2 js mới, old-hash control = 900B SPA-trap 0-hit; smoke 4×200 + PE 401-secured. **Surprise: CSS CŨNG rotate cả 2 app** (spec đoán "CSS có thể frozen") — PeDetailTabs badge sinh utility-class mới → content-hash đổi theo content, benign #69; phá streak bundle-FROZEN S129-S132. Lesson tool: python `capture_output text=True` crash cp1252 trên title CI tiếng Việt → dùng `text=False` bytes + `json.loads(bytes)`; foreground sleep bị chặn → `python time.sleep` poll-loop OK. Tag `[s133, run-398, go-5-5, css-rotate-benign, mig67-backfill-18]`
+- _(**S134 #399 `6664a61` PE lũy-kế tạm-tính (0-mig) GO 5/5** — gate 528 [45+483, +8 PePendingAccumulation]; js+css rotate ×2 admin `BVCukFP5`/`CBzUlgOA` eoffice `DoxyEOxh`/`BHsBUA8e`; byte-verify #77 urllib-BYTES; 🔑 **ct=text/html discriminator SẠCH HƠN size-919B** [shell size drift per-build]; `body.count(marker.encode())` bulletproof cp1252 · verbatim → `archive/2026-07.md` via `_INDEX`)_
+- _(**S133 #398 `d4fbced` PE budget-freeze + Mig 67 `AddPeApprovedBudgetSnapshot` APPLIED-PROD GO 5/5** — gate 520 [45+475]; 11-col ApprovedBudget\* + backfill 18/18 DaDuyet + 89-table; js+CSS rotate ×2 [badge utility-class → CSS content-hash đổi, benign #69]; python text=True crash cp1252 → text=False bytes+json.loads · verbatim → `archive/2026-07.md` via `_INDEX`)_
> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
> Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
@@ -60,9 +61,9 @@ cicd-spawn đôi khi trả memory-curation slice ("Compressing S112/S111") thay
- **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.
- **SSH→PS quoting (S42):** nested bash→ssh→powershell 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:** **509 PASS** (45 Domain + 464 Infra · 0 fail/skip; S118 #389 `c808b9a`; prior 505/501/495/486/458/440/421). Phase 9 UAT skip OK. 🔑 **Concrete count via web-UI log path `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, NO `/api/v1/` prefix) → per-project `Passed! - Failed: 0, Passed: N, Skipped: 0`. ⚠️ S117 REFINE: `/api/v1/…/actions/runs/{n}/jobs/0/logs` AND `/api/v1/…/tasks/{id}/logs` BOTH now 404 (Gitea) — ONLY the bare web-UI runs path (no api/v1) returns log body; status=success alone still ⟹ gate-passed (tests run pre-deploy).**
-- **Mig latest repo: Mig 66 `AddPeApprovedPriceRange`** (`20260714035636`, S117 `316a82f`; single AddColumn `PurchaseEvaluations.ApprovedPriceMaxAmount decimal(18,2) nullable` — cột giá-chốt MAX của "duyệt theo khoảng Min–Max" src ProMinMax; VERIFIED-APPLIED-PROD `COL_LENGTH=9` + history-top `20260714035636_AddPeApprovedPriceRange` + sys.tables **89 giữ** [AddColumn no-new-table]). Prev Mig 65 `BackfillPeQuoteIsSelectedAndWinnerInvariant` / 64 `AddSupplierPublishState` / 62 `ExpandSupplierFields` (see docs/CLAUDE.md migration table). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. ⚠️ **`sys.tables`(is_ms_shipped=0)=89** — AddColumn/data-only don't change count; commit touching no new table ⟹ 89 correct, don't FAIL. enum-additive = int NO-mig (history NOT-advance + empty `git diff -- '*Migrations*'`).
-- **Bundle hash live:** current ship-baseline = S129 #396 (admin js `yCkX-2Bp`/css `CXD7q9xH` · eoffice js `DKSD4p9k`/css `CjwZwYJo`; size 1,656,696/1,563,044b; LM 16:21:00/16:21:55 GMT = 23:21/23:21:55+07 07-16) — FROZEN both-apps vs S128 (0 fe-src ⟹ byte-id) NHƯNG fresh-LM-in-window = ship-proven #69/#333. S128 #395 had FULL-ROTATE from S117 (`cFI7ih4a`/`CfunQHbd` · `MxgooVZw`/`DGqqHQXS`). ⚠️ id≠run_number (head_sha-keyed). PER #69 TWO-MODE hash is SNAPSHOT-per-run NOT stable-baseline (rotate on real-src OR benign-0-src; freeze on 0-src) — ALWAYS re-snapshot LIVE post-success; ship-proof = LM-in-window + real-size (vs 919b SPA-trap) + marker-grep, NOT hash-delta. js-rotate/css-frozen = FE-logic-no-style fingerprint.
+- **Tests baseline:** **532 PASS** (45 Domain + 487 Infra · 0 fail/skip; run #407 `4510cea` 07-23; prior climb 528/520/509/505/501/495/486/458/440/421). Phase 9 UAT skip OK. 🔑 **Concrete count via web-UI log `https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/{run_number}/jobs/0/logs` (~18KB, NO `/api/v1/` prefix — api/v1 runs+tasks logs BOTH 404) → per-project `Passed! - Failed: 0, Passed: N, Skipped: 0`. Delta-proof new-tests = compare prev-run count (dotnet summary hides names). status=success alone ⟹ gate-passed (tests pre-deploy).**
+- **Mig latest repo: Mig 67 `AddPeApprovedBudgetSnapshot`** (`20260717032812`, S133 `d4fbced` — PE freeze-budget 11-col AddColumn + backfill 18/18, no-new-table; VERIFIED-APPLIED-PROD history-top + sys.tables **89**). Prev Mig 66 `AddPeApprovedPriceRange` / 65 backfill-invariant / 62 `ExpandSupplierFields` (full table → docs/CLAUDE.md). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/` (67 `.cs` excl Designer/Snapshot). Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. ⚠️ **`sys.tables`(is_ms_shipped=0)=89** — AddColumn/data-only/enum-additive don't change count; no-new-table commit ⟹ 89 correct, don't FAIL. **NO-Mig proof = history NOT-advance + empty `git diff ..
-- '*Migrations*'`.**
+- **Bundle hash live:** ship-baseline = run #407 (07-23) admin js `B2j1LQ-v`/css `CBzUlgOA` · eoffice js `C3wbJegk`/css `BHsBUA8e` (real-size 1,660,902/1,567,232b; LM 17:16:01/17:16:54+07). PER #69 TWO-MODE = SNAPSHOT-per-run NOT stable-baseline (rotate on real-src, freeze byte-id on 0-src) — ALWAYS re-snapshot LIVE post-success; **ship-proof = LM-in-window + real-size (vs 900b `ct=text/html` SPA-trap) + marker-grep, NOT hash-delta**. js-rotate/css-frozen = FE-logic-no-style fingerprint (S134-refine: `ct=text/html` cleaner discriminator than 919b, shell size drifts per-build). ⚠️ id≠run_number (head_sha-keyed).
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check).
- **API Serilog log (S118 correction):** real sink = `C:\inetpub\solution-erp\api\logs\solution-erp-YYYYMMDD.log` (lowercase `logs`, rolling daily). ⚠️ skill-runbook path `…\Logs\log-YYYYMMDD.txt` STALE → empty grep. MinimumLevel Default=Information (DbInitializer `LogInformation` seed/revoke lines DO persist) — Microsoft/System overridden Warning (no "Now listening" INFO). **Startup seed-race audit pattern (durability deploys):** grep this log for revoke-line THEN seeder-line same-second → "N added + M upgraded" proves seeder actively flipped rows revoke knocked down (0-upgraded = seeder no-op = FAIL flag).
@@ -81,15 +82,11 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code /
## 📅 Recent runs (compressed — verbatim → archive via `_INDEX.md`)
-- **🟢 S132 (2026-07-17) run #397 `72f68d1` GOVERNANCE session-end S131-S132 GO ~4m [em-main append on-behalf từ return 0-garble]:** gate **509 EXACT** (Domain 45 + Infra 464, 0 fail/skip — log `runs/397/jobs/0/logs` 17.9KB) · smoke 4×200 (health/live + ready, admin, eoffice) · **bundle both-apps FROZEN vs S129** (`yCkX-2Bp`/`CXD7q9xH` · `DKSD4p9k`/`CjwZwYJo` — 0 fe-src ⟹ byte-id Vite; benign #69/#333 frozen-ship PROVEN: LM-in-window 08:42:57/08:43:52 +07 + real-size 1,656,696/1,563,044b ≠ 919b SPA-trap) · NO-BE/NO-Mig (Mig 66 / 89-table no-drift-by-construction) · **AS-6-refined tái-xác-nhận lần 3:** sole non-.md `.claude/governance/.session-counter.json` thoát 7-glob paths-ignore ⇒ trigger, 17 file .md kia ignored (cùng lớp rag.json S128 / .ps1 S129) · `conclusion:None` dù success — trust `status` (Gitea tasks-API đặc-tính, tái-xác-nhận S126/S128/S129).
-- **🟢 S129 (2026-07-16) run 510/rn 396 `295c70c` SCRIPTS+GOVERNANCE GO ~3m13s CI (23:19:15→23:22:28+07 / e2e 5m38s từ commit 23:16:50):** diff = 15 file — 2 ops-`.ps1` (`scripts/mfe-eval.ps1` reader+writer EOL/BOM 2-phía · `scripts/agent-frontmatter-eol-check.ps1` +4-comment-header) + 13 `.md` (broadcasts ×6 / run-trace sub-reviewer+synthesis ×3 / agent-memory ×2 / WAL). **CI TRIGGERED (NOT docs-skip): 2 `.ps1` thoát 7 paths-ignore — glob `scripts/**.md` CHỈ cover `.md`, `.ps1` lọt** (khớp S128 `crystallized-backfill.ps1` precedent; AS-6 #41 confirm "non-.md scripts VẪN trigger"). ✅ 2 `.ps1` KHÔNG ở build/deploy path (0 ref trong `.gitea/`) → nếu-fail ≠ do .ps1 (task#5 N/A, PASS). gate **509 EXACT** (Domain 45 + Infra 464, 0 fail/skip — web-UI log `runs/396/jobs/0/logs` 18KB, path SỐNG lại dù S117 dự-đoán 404). smoke ALL 200 (api live+ready, admin, eoffice). **Bundle both-apps FROZEN** (admin `yCkX-2Bp`/`CXD7q9xH` · eoffice `DKSD4p9k`/`CjwZwYJo` — IDENTICAL vs S128, 0 fe-src ⟹ byte-id Vite) NHƯNG **LM fresh-in-window** admin 16:21:00 / eoffice 16:21:55 GMT (+ size 1,656,696/1,563,044 real ≠919b SPA-trap) → #69/#333 frozen-ship PROVEN (breaks S128 rotate, resumes S117-S126 freeze). NO-BE/NO-Mig (0 src/Backend, 0 Migrations → Mig 66 giữ, no drift by-construction). ⚠️ python `open()` phải `encoding='utf-8'` (cp1252 crash JSON Vietnamese); `conclusion`=None dù success → trust `status`.
-- _(**S128 #395 `289ba96` GOVERNANCE+TOOLING GO 5m42s** [gate 509; **CI-trigger dù governance:** `.claude/rag.json` (config trực dưới `.claude/`, ngoài skills|agent-memory|workflows/runs) + `scripts/crystallized-backfill.ps1` (.ps1 thoát `scripts/**.md`) → refines AS-6 #41 "soi TỪNG file vs 7-glob, đừng giả-định governance=skip"; Bundle FULL-ROTATE both-apps từ S117 → admin `yCkX-2Bp`/`CXD7q9xH` eoffice `DKSD4p9k`/`CjwZwYJo`, benign #69 breaks S117-S126 freeze; NO-BE/NO-Mig] · verbatim → `archive/2026-07.md` via `_INDEX`)_
+- _(**S128 #395 `289ba96` + S129 #396 `295c70c` + S132 #397 `72f68d1` GOVERNANCE/SCRIPTS/TOOLING GO** — gate **509** [45+464, 0 fail/skip]; smoke 4×200; bundle benign #69 [S128 FULL-ROTATE from S117 → `yCkX-2Bp`/`DKSD4p9k`; S129+S132 FROZEN byte-id 0-fe-src] ship-proof = LM-in-window + real-size ≠919b NOT hash-delta; NO-BE/NO-Mig [Mig 66/89-table]; 🔑 **AS-6 #41: soi TỪNG file vs 7-glob** — `.ps1` thoát `scripts/**.md` glob + `.claude/rag.json` / `.session-counter.json` thoát ⟹ non-.md governance VẪN trigger CI; `conclusion:None` dù success → trust `status` [tái-xác-nhận S126/S128/S129/S132] · verbatim → `archive/2026-07.md`)_
- _(**S118 #389 `c808b9a` SECURITY-AUTHZ** ReportsController→Reports.Read GO 5m34s [gate 509=45+464 (+4 authz-reflection vs 505); DB `Reports.Read`→Admin-ONLY sqlcmd (Procurement CanRead=0 → non-admin 403 by-construction); no-token 401 class-`[Authorize]` intact; NO-FE/NO-Mig; cicd-lane GARBLED #53 → em-main curl+sqlcmd recovered, verdict disk-TRUE] · **+S117 #387 `316a82f`** PE duyệt-khoảng Min–Max + Mig 66 `AddPeApprovedPriceRange` APPLIED-PROD [gate 501; js-rotate×2 css-frozen; byte-verify #77 4-str PRESENT; Mig66 `COL_LENGTH=9`+sys.tables 89; 🔑 pre-deploy snapshot BEFORE poll-success] · verbatim → `archive/2026-07.md` via `_INDEX`)_
- _(**S111-S116 #383-#386/#495/#496** PE multi-NCC per-hạng-mục (Mig 65 backfill) + nhập-giá-ÂM + submit-guard + Supplier-import Phase-B/v2 (Mig63 `AddSupplierImportSourceFields`/Mig64 `AddSupplierPublishState` APPLIED-PROD, IsPublic 37/37) + H22-WAL-adopt (deploy.yml self-change +2 paths-ignore) + GOVERNANCE GO [gate 458→495; js-rotate/css-frozen; 🔑 AS-6 baseline-shift memory-push HẾT trigger; 411-refine bodyless-POST→411 → re-probe-with-body 401/415≠404; #69-REFINE 0-src FROZEN-proof; byte-verify #77 str-PRESENT] · verbatim → `archive/2026-07.md` via `_INDEX`)_
- _(**S98-S110 + S126 #485-#494/#394** GOVERNANCE/MEMORY/TOOLING-push FROZEN-streak sau ROTATE #489 (#69 TWO-MODE; ship-proof = LM-in-window + real-size, hash-delta phụ); gate 440→509; Mig 62 + 89-table frozen · 🔑 #494 LM-newer-than-verified-window = missed-run detector; bare `/health`=404 UNMAPPED (đừng FAIL — thật `/health/live`+`/ready`) · verbatim → `archive/2026-07.md` via `_INDEX`)_
-- _(**S104 #488 `778fc98` FULL-STACK Supplier 9→27 + Mig 62 `ExpandSupplierFields` APPLIED-PROD** — AddColumn×18-exact + 89-invariant + un-cram 4-NCC EXACT + DTO-18-keys-live = SCHEMA-ADD+DATA-UN-CRAM PASS-pattern · verbatim → `archive/2026-07.md`)_
-- _(**S96-S97 #478-#484** 11-commit whole-session-GO — 🔑 #484 Mig61 DATA-FIX-BACKFILL PASS = CI + Mig-top==HEAD + target-rows-flip-EXACT + control-untouched + derived-API-field-FLIP-live; #482 Mig60 EF-TERNARY-SQL-translate 200-NOT-500; gate 440 · verbatim → `archive/2026-07.md`)_
-- _(**S90→S96 #465-#477 + S69b→S89 #307-#348** all PASS — 🔑 #477 DATA-SEED-PASS = pre/post-DB-row-EXACT-delta + DLL-mtime + w3wp-recycle + Mig-frozen + API-payload-row; #472 PROD Identity NON-prefixed `Roles`; #470 no-resurrect = DB-count-diff + dll-in-window; Mig54-59 applied-prod; #348 411→401 wired · verbatim → git + `2026-06.md`)_
+- _(**PASS-pattern library S69b→S104 #307-#494** all GO — 🔑 reusable verify-recipes: **SCHEMA-ADD+DATA-UN-CRAM** [#488 Supplier 9→27 Mig62: AddColumn×18-exact + 89-invariant + un-cram 4-NCC + DTO-live] · **DATA-FIX-BACKFILL** [#484 Mig61: Mig-top==HEAD + target-rows-flip-EXACT + control-untouched + derived-API-FLIP-live] · **DATA-SEED-PASS** [#477: pre/post-DB-row-delta + DLL-mtime + w3wp-recycle]; #482 Mig60 EF-ternary-SQL 200≠500; #472 PROD Identity NON-prefix `Roles`; #470 no-resurrect DB-count-diff; Mig54-62 applied-prod; #348 411→401 · verbatim → git + `2026-06/07.md`)_
- **Older (S83 #325 ← S29 #232) → git `d2f52ba` + `archive/2026-06.md`** (incl #291 06-16 FAIL [gotcha #65]); pre-S38 → `archive/2026-05-*`.
---
diff --git a/.claude/agent-memory/cicd-monitor/archive/2026-07.md b/.claude/agent-memory/cicd-monitor/archive/2026-07.md
index 35f9668..e7f7b3a 100644
--- a/.claude/agent-memory/cicd-monitor/archive/2026-07.md
+++ b/.claude/agent-memory/cicd-monitor/archive/2026-07.md
@@ -34,3 +34,7 @@
- **🟢 S118 #389 (rn 389, id 503) `c808b9a` SECURITY-AUTHZ gate ReportsController→Reports.Read GO 5m34s (15:55→16:00:57+07 2026-07-14; 3-file: ReportsController.cs [+2 `[Authorize(Policy="Reports.Read")]` on `dashboard`+`contracts/export`; `my-dashboard` kept-open scope-currentUser] + AuthorizePolicyRegressionTests.cs [+4 reflection] + gotchas.md):** status=success · **gate `runs/389/jobs/0/logs`: Domain 45 + Infra 464 = 509** (+4 vs #388 505; Infra 460→464 = +4 authz reflection; 0-fail/skip) · **dll NEW** 15:58:21+07 (was 15:33:21 = app restarted in-window) · **🎯 DB `Reports.Read`→Admin-ONLY** sqlcmd prod (scp .sql→`-i`, cleaned): 1 role CanRead=1 (`Admin`); `Procurement` row CanRead=0 → non-admin dashboard/export=**403 by-construction** (4-attr-test + policy-Admin-only-DB) · smoke: health/live 200, admin 200, eoffice 200, dashboard/export/my-dashboard no-token=**401** (class-level `[Authorize]` intact) · **NO-FE** (0 fe-src → bundle=S117 `cFI7ih4a`/`MxgooVZw` frozen) · **NO-Mig**. Password-free per task (403 by-construction not curl-proven). **cicd-lane return GARBLED #53** (memory-curation slice, NOT verdict) → **em-main-solo curl+sqlcmd recovered**; verdict-substance disk-verified TRUE (509-gate + `Reports.Read`→Admin-only + 401 no-token all git/prod-corroborated). [harvest-curator S118-end F1: nhãn "CLEAN-return" cũ SAI, đã sửa.]
- **S126 (2026-07-16) run 508/#394 `e9124fc` GOVERNANCE-TOOLING GO [opus, 0-garble]:** gate **509 PASS exact** (45+464, 0 fail) · bundle **FROZEN byte-id S117 #387** (`cFI7ih4a`/`MxgooVZw`, LM in-window + real-size = re-shipped hash-frozen, benign #69 same-hash, 0 hoist) · smoke 200×5 · **89 bảng + Mig 66 top = no-drift**. 4 non-.md (.json/.js/.ps1) vẫn trigger full CI per #41. ⚠️ bare `/health`=404 UNMAPPED (thật = `/health/live`+`/ready`) — đừng FAIL; jq không có → python json; python capture_output cp1252 → curl -o + open(utf-8); Windows-python không thấy `/tmp`.
+
+- **S133 (07-17) verify d4fbced PE budget-freeze [wf_4eadbcf6-c25, return sạch — verdict landed SAU khi lead /pause; harvest-liền lúc notification, lead CHƯA re-verify độc lập]:** VERDICT **GO 5/5** — Run **#398** success ~5.5m (gate **520** = 45D+475I, 0 fail/skip); Mig 67 prod: history-top + **11 cột ApprovedBudget\* exact** + backfill **18/18 phiếu DaDuyet chua=0** + sys.tables **89** no-new-table; bundle rotate ×2: admin js `yCkX-2Bp`→**`3NCdyODe`** css `CXD7q9xH`→**`B--dUHcm`** · eoffice js `DKSD4p9k`→**`CKsoiNCI`** css `CjwZwYJo`→**`BuntB63r`**, real-size 1.657.025/1.563.373B, LM 11:18:42/11:19:36+07 in-window; byte-verify #77: `budgetFrozen`=1 + `Ngân sách chốt tại thời điểm duyệt`=1 cả 2 js mới, old-hash control = 900B SPA-trap 0-hit; smoke 4×200 + PE 401-secured. **Surprise: CSS CŨNG rotate cả 2 app** (spec đoán "CSS có thể frozen") — PeDetailTabs badge sinh utility-class mới → content-hash đổi theo content, benign #69; phá streak bundle-FROZEN S129-S132. Lesson tool: python `capture_output text=True` crash cp1252 trên title CI tiếng Việt → dùng `text=False` bytes + `json.loads(bytes)`; foreground sleep bị chặn → `python time.sleep` poll-loop OK. Tag `[s133, run-398, go-5-5, css-rotate-benign, mig67-backfill-18]`
+
+- **S134 (07-17) verify 6664a61 PE lũy-kế tạm-tính [wf_f366f38d-498 · return CLEAN no-garble] `[engine: opus-worker]`:** VERDICT **GO 5/5** — Run **#399** (id=513) success 5m27s (14:27:29→14:32:56+07); gate **528 EXACT từ log** (45D+483I, +8 PePendingAccumulationTests); Mig 67 top FROZEN + sys.tables 89 (0-mig by-construction, `git diff -- *Migrations*` empty); bundle rotate ×2 admin js `3NCdyODe`→**`BVCukFP5`** css `B--dUHcm`→**`CBzUlgOA`** · eoffice js `CKsoiNCI`→**`DoxyEOxh`** css `BuntB63r`→**`BHsBUA8e`** (FE-src đổi 6 file ⇒ rotate expected; CSS = amber utilities benign #69), real-size 1.659.037/1.565.367B + LM in-window; **byte-verify #77 qua python urllib BYTES**: `pendingPriorPes`=2 + `Tạm tính`=3 + `phiếu trước cùng gói thầu CHƯA duyệt`=2 cả 2 js, control old-hash 900B **ct=text/html** 0-hit; smoke 4×200 + PE 401. Learned: (1) **urllib bytes + `body.count(marker.encode())` = bulletproof cp1252** cho marker tiếng Việt (khác subprocess text=True crash S133); (2) **ct=text/html là SPA-trap discriminator SẠCH HƠN size-919B** (shell size drift per build — 900B lượt này); (3) tasks-API có thể blip empty-200 transient → poll-loop absorb. Poll held-off verify khi running (iter 0-2) đúng bài #242. Tag `[s134, run-399, go-5-5, ct-html-discriminator, urllib-bytes-marker]`
diff --git a/.claude/agent-memory/cicd-monitor/archive/_INDEX.md b/.claude/agent-memory/cicd-monitor/archive/_INDEX.md
index 0cd6a06..a9a5a2d 100644
--- a/.claude/agent-memory/cicd-monitor/archive/_INDEX.md
+++ b/.claude/agent-memory/cicd-monitor/archive/_INDEX.md
@@ -132,4 +132,6 @@
- 2026-07-14 · S117 #387 `316a82f` PE endsBeforeCeo-pointer-aware + duyet-theo-khoang Min-Max + Mig66 AddPeApprovedPriceRange APPLIED-PROD GO — js-rotate x2/css-frozen cFI7ih4a/MxgooVZw, gate 501 (45+456), byte-verify 4-str PRESENT · PASS · `2026-07.md` · substring:"S117 #387 (rn 387, id 501)"
- 2026-07-14 · S118 #389 `c808b9a` SECURITY-AUTHZ ReportsController→Reports.Read GO — gate 509 (45+464), DB Admin-only 403-by-construction, cicd garble #53 em-main-recovered · PASS · `2026-07.md` · substring:"S118 #389 (rn 389, id 503)"
-- 2026-07-16 · S126 #394 `e9124fc` GOVERNANCE-TOOLING GO — gate 509 exact, bundle FROZEN byte-id S117, 89-table no-drift · PASS · `2026-07.md` · substring:"S126 (2026-07-16) run 508/#394"
\ No newline at end of file
+- 2026-07-16 · S126 #394 `e9124fc` GOVERNANCE-TOOLING GO — gate 509 exact, bundle FROZEN byte-id S117, 89-table no-drift · PASS · `2026-07.md` · substring:"S126 (2026-07-16) run 508/#394"
+- 2026-07-17 · S133 #398 `d4fbced` PE budget-freeze + Mig67 AddPeApprovedBudgetSnapshot APPLIED-PROD GO — js+css rotate x2 (3NCdyODe/CKsoiNCI), gate 520 (45+475), backfill 18/18, byte-verify budgetFrozen · PASS · `2026-07.md` · substring:"S133 (07-17) verify d4fbced"
+- 2026-07-17 · S134 #399 `6664a61` PE lũy-kế tạm-tính (0-mig) GO — js+css rotate x2 (BVCukFP5/DoxyEOxh), gate 528 (45+483 +8 PePendingAccumulation), ct=text/html discriminator · PASS · `2026-07.md` · substring:"S134 (07-17) verify 6664a61"
\ No newline at end of file
diff --git a/.claude/agent-memory/investigator-codebase/MEMORY.md b/.claude/agent-memory/investigator-codebase/MEMORY.md
index c1c1fd5..63f5294 100644
--- a/.claude/agent-memory/investigator-codebase/MEMORY.md
+++ b/.claude/agent-memory/investigator-codebase/MEMORY.md
@@ -1,131 +1,58 @@
# Investigator-Codebase Agent — Persistent Memory
-- **S141 (07-20) adap đợt-9/10 invest 5-lane [engine: fable-clone-ensemble `wf_3b74219c-561` · 5/5 CLEAN 0-garble; em-main verify+append]:** thiết kế trio harness-{eval,refine,audit} + contract-v2 dangling + stamp + phép-8 + session-model-ĐO + sweep 14→17. ⭐ (ii)-nới-có-vết MỘT MÌNH đủ đóng dangling-head — 3/3 ca lịch-sử (S127/S133/S139) reclassify squash-benign (object-exists ∧ counter-không-lùi, `.session-counter.json:38/43/48`); tick-head LUÔN = wal-commit (4c43d74 proven) ⇒ dangling mỗi cycle = expected-drift KHÔNG tamper. ⭐ byte-safe(S125) ⟂ tolerant-EXTRACTION = 2 lỗ TÁCH — S141 em-main quote-strict regex whiff hash-có-ngoặc 3/3 suýt oan; pin mẫu `\"?([0-9a-fA-F]{8,64})\"?` vào DOC + port stamp_verify.py. ⭐ K1 gitignore PASS không cần whitelist (`!.claude/**` :83 negation cover sessions/ — S116-fear STALE trong memory-pack). ⭐ Roster-count 3 TẦNG khi sweep: hardcode-prose (11 site, co-land R3-M2) / GLOB-script (6 auto-adapt 0-đổi) / B1-pointer (0-đổi); `workflows/README:64` \"9 roster\" stale-cổ — grep literal \"14\" MISS ⇒ grep same-CLASS mọi roster-ref. ⭐ Ranh SẮC NHẤT trio: refine⟂lead-omission — discriminator WHOSE-memory (LEAD hot-feed=omission / AGENT corpus=refine), cần owner ratify. hmw parallel() KHÔNG tuần-tự-hoá ⇒ trio = 3 spawn riêng + lead scribe per-con. Annex phép-8 mốc=da349fc 2026-07-17; đo-thử đầu PASS (2 gộp-closeout hậu-baseline có 2 bù). Tag `[s141, adap-dot-9-10, trio-design, squash-benign, tolerant-extraction]`
-
-- **S134 (07-17) PE lũy-kế tạm-tính invest [fable-real Agent-lẻ · #53 return-garble → sub-file 8-phần 20.7KB CỨU TRỌN (lần 3 pattern thắng); entry on-behalf em-main] `[engine: fable-real-single]`:** ground-truth LẬT KHUNG đề bài — row1 "NS trình duyệt trước" ĐÃ gồm ChoDuyet sẵn (`PeBudgetAccumulator.cs:42-48`), gap thật = row2 "đã chọn thầu" (DaDuyet+IsWinner :50-60) + row5/6 dẫn xuất ⇒ spec thu hẹp **0-mig/0-endpoint/0-đụng-chính-xác**. Map 3-bản predicate: helper runtime DUY NHẤT + live `:962` + snapshot `:1018` đều GỌI CHUNG helper (không duplicate) + backfill Mig 67 one-time. Phase-set propose: PriorPes NOT IN {7,99} · delta2 NOT IN {1,7,99}+winner (GỒM ChoDuyet=gap) · delta1 NOT IN {1,7,10,99} (no-double-count); D1 nháp list-only (S89 riêng-tư `:893-898`). 2 errata lead bắt: path thiếu `Dtos/` + hash-value stale (`47c97df1` vs lead-đo `5534addb` — kết luận identical vẫn ĐÚNG). Learned: **đề bài UAT "X không được tính" phải đo predicate THẬT từng-row trước khi nhận khung** — row1 vs row2 khác semantics, gộp chung "lũy kế" là bẫy; PeWorkflowPanel `:510 shouldPickPrice` = predicate "đang duyệt CUỐI" tái dụng được. Tag `[s134, luyke-tam-tinh, invest-lat-khung, 53-garble-diskfile-lan3]`
-
-> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
-> Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
-> Full verbatim history pre-S40 → git `d2f52ba` + `archive/2026-05-q1..q4.md` + `archive/2026-06.md`. 🗺️ **Lookup map: `archive/_INDEX.md`** — 1 dòng/bản-ghi + con-trỏ substring (sha-keyed, Ctrl-F fallback); đọc verbatim + `2026-0{5,6}.gist.md` theo nhu cầu. **S80 curate: moved 15 recon entries S65→S76 + 2× 06-20 → archive.**
-> **Renamed S39:** investigator → investigator-codebase (internal half; external → investigator-api).
-
----
+> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT). Update BEFORE every stop; keep entries to 1 line (gotcha #53 truncation — index hit 28KB @S148, hard-cap 24.4KB). Detail → `archive/2026-07.md` + `archive/_INDEX.md` (sha-keyed substring, Ctrl-F) + git `d2f52ba` + `archive/2026-0{5,6}.md` + `runs/*/sub-*.md`. Renamed S39: investigator → investigator-codebase (internal; external → investigator-api).
## 🎯 Role baseline
+Read-only INTERNAL audit SOLUTION_ERP. Tools: Read/Grep/Glob/Bash + 5 RAG MCP. Output: concise <500w + file:line. Skills: contract-workflow + permission-matrix + ef-core-migration.
+- ✅ MINE: sqlcmd schema scan, EF/grep, controller/authz audit, migration diff, count grounding
+- ❌ NOT: external docs/CVE/lib → investigator-api · write → implementer · test → test-specialist · architecture decision → em main
-Read-only INTERNAL audit SOLUTION_ERP codebase. Tools: Read, Grep, Glob, Bash + 5 RAG MCP. Output: concise findings <500 words + file:line refs. Skills: `contract-workflow` + `permission-matrix` + `ef-core-migration`.
+## 📌 Recent invest (1-line digest; full detail → archive/runs/git)
+- **S148 (07-23) PE tree DUP project by-space:** `FLOCK 01`/`FLOCK01` = **path-A TWO real Project rows** diff-by-space. Tree groups on `p.projectId` GUID (`PurchaseEvaluationsListPage.tsx:192-197`, display projectCode); PE→Project FK `ProjectId` (`PurchaseEvaluation.cs:15`) JOIN-resolved (`PeFeatures.cs:579/:647`) — NO denorm string. CRUD zero-trim (`ProjectFeatures.cs:80` validator, `:95/:138` exact `Code==` dupe-check). Both seeds exact-Code key emit ONLY no-space: `SeedRealMasterDataAsync:2776` (UNGATED :128) + `SeedDemoMasterDataAsync:2681` (gated :117 OFF-prod). Space=legacy/manual. Blast=3 tables ProjectId (Contracts/PeWorkItemBudgets/PurchaseEvaluations); only `PeWorkItemBudgets` UNIQUE(ProjectId,WorkItemId) filtered [IsDeleted]=0 (`PeWorkItemBudgetConfiguration.cs:27`). Direction: KEEP no-space (in seed→idempotent), RETIRE space (not-in-seed); soft-del survivor→seed RE-CREATES (#75/#76). Dev only FLOCK 01 pair (FLOCK03/CAL01 space-twins prod-only); prod UNVERIFIED (SSH dies on SQL-client, S134). Tag `[s148, pe-tree-dup-project-by-space, path-A-two-rows, no-normalize-crud]`
+- **S147 (07-23) PE attach TraLai edit-block:** FE-ONLY block, BE zero phase-guard (`PurchaseEvaluationsController.cs:15` class `[Authorize]` only; handlers check exists only `PurchaseEvaluationAttachmentFeatures.cs:66/:160`). Gate = raw `readOnly` PROP not phase (`GeneralAttachmentsSection` :3555/:3577 hide-when-readOnly); List detail HARDCODE `readOnly=true` (:574/:669) ⇒ TraLai loại khỏi SCREEN không khỏi editable-SET (workspace `PeListPanel.tsx:88` isEditablePhase GỒM TraLai). fe-admin MIRROR. BE no-guard CỐ Ý (S78). Fix FE: `attachEditable=isDrafter&&isEditablePhase`. Tag `[s147, pe-attach-tralai, fe-only-screen-gate]`
+- **S141 (07-20) adap đợt-9/10 5-lane [fable-clone-ensemble, 5/5 clean]:** (ii)-nới-có-vết đủ đóng dangling-head (3/3 squash-benign, `.session-counter.json`); tick-head=wal-commit. byte-safe⟂tolerant-EXTRACTION 2-lỗ tách → pin regex `\"?([0-9a-fA-F]{8,64})\"?`. Roster-count 3-tầng (prose/glob/pointer). refine⟂lead-omission = discriminator WHOSE-memory. Detail → `runs/…S141…`.
+- **S138 (07-17) adap 3-bản 17-07 [fable-real]:** 3/3 FIT; delta hẹp hub PHẢI grep landing-site trong luật SỐNG (SE chép 4-nhãn lỗi vào `reviewer.md:46` = adopt-verbatim kế-thừa lỗi nguồn). Detail → `runs/…S138…`.
+- **S135/S134/S133 (07-17) PE budget invest [fable-real, #53→sub-file cứu trọn]:** S134 lũy-kế lệch-tiền-đề row1 (`PeBudgetAccumulator.cs:42-48` PrevSubmitted ĐÃ gồm ChoDuyet; DaDuyet-only chỉ row2 :50-60) predicate 1-bản helper Features:962+WorkflowService:1018. S133 freeze-at-DaDuyet 4-assignment (`PurchaseEvaluationWorkflowService.cs:861/:899/:931/:1053`, Mig67). S135 tick H24 single-entry `session-start:181-206`. Detail → archive + runs.
+- **S128 (07-16) adap-16-07 [fable-real, garble→on-behalf]:** RAG config in-repo (`AI_INFRA\claude-rag\lib\projects.py:39` owner-final, `rag.json:26` override); nhãn-đo "calibrated" chỉ C1; ghi-đĩa-trong-lúc-làm cứu draft khi garble. Detail → runs.
+- **S126 (07-16) worker-pin + all-inherit sweep [opus×2]:** 14/14 opus+max (no-tier lane resolve opus DÙ lead=Fable); B1 284→15 floor-sống; mega-line blow Read-cap → perl-window. `[s126, h23-precedence]`
+- **S125 (07-16) adap-review [fable-real]:** auto-memory 50 tracked/0 porcelain; 14/14 opus+effort:max; residue all-inherit floor-sống 2 (`fable-real.md:37`+`fable-clone.md:43`) vá; verifier-suspect-first.
+- **S122 (07-15) retro-harvest [6/6 clean]:** 🔴 0/7 mất 7/7 XONG synthesis-sai-chỗ (orphan đo TÊN không NỘI-DUNG); lane-RỖNG≠mất; lane tự truy git đừng tin brief.
+- **S116/S117 (07-13) PE negative-quote + giá-chốt RANGE [fable]:** neg-quote FE-ONLY (BE/DB nhận âm; quote sống `details[].quotes` KHÔNG `suppliers[].quotes`). S117 ApprovedPrice WRITE 1-site `ApplyApprovedPriceOnFinalize`; HĐ-gen KHÔNG đọc ApprovedPrice (GiaTri=SUM IsSelected). endsBeforeCeo=config-lie #78 (detail :1152 / list :647).
+- **[archived → archive/2026-07.md + git]** S87-S114 PE recon (multi-winner IsSelected per-hạng-mục Mig65 · WinnerQuoteTotal DERIVED · budget gate PURE-ROLE · V2 routing=ApproverUserId LINEAR) · Supplier-import-v2 (ClosedXML, Mã NCC=Code, IsPublic fail-closed, import authz-mismatch) · S105 BVAAU auth RCA (relative-`/api` prod-404) · S104 30-field · S98-S101 harness · Office/HRM/Governance recon. Ctrl-F `_INDEX.md`.
-## 🚫 Split boundary (S39)
-- ✅ MINE: internal SQL/EF/grep/reference mirror, sqlcmd schema scan, controller audit, migration diff, count grounding
-- ❌ NOT: external docs/CVE/lib → `investigator-api` · write → implementer · test → test-specialist · architecture decision → em main
-
----
-
-## 🆕 Recent invest (S112-S114 stubs — verbatim → git history + `archive/2026-07.md`)
-- **S135 (07-17) adap khung-nhịp 13-trục + spec [fable-real; ghi-đĩa 12-phần → sạch; detail TRỌN ở sub-file]:** ⭐ LỆCH-1 lần 2: hub bảo "crystallized-backfill.ps1 mang `draft 2026-07-15`" — `git log -S` = chưa-TỪNG-có; sống ở `memory-budget.json:65+:90` ⇒ claim thư PHẢI re-đo đĩa. ⭐ Tick H24 single-entry `session-start:181-206`; tiep/pause/session-end 0-tick; tick S133 UNCOMMITTED (counter ngoài 3-path hook). Bẫy +1: tick cả pause+tiep = +2/cycle FAIL phép-7 → OR-guard `session==last_ticked_session`. ⭐ Detector closeout đo UNION-window per-label (delta sổ nằm ở wal-flush trước closeout — per-commit = 2/3 FAIL oan). H1/H2 0-vết @S133/S134 (E-011 tái diễn); việc-2 hub (seed 2 diary H24) ĐÃ XONG @`72f68d1`. Phép-1 FAIL +3tick/9nhãn/7pause · phép-3 union 3/3 · phép-4 14/14. Sleep 7d = ngày-lịch LỆCH khung. → `runs/2026-07-17-S135-adap-khung-nhip-nam-vong/sub-investigator-codebase-0.md`. Tag `[s135, lech-1-missite-lan2, union-window]`
-- **S134 (07-17) PE lũy-kế TẠM-TÍNH invest+spec [fable-real-single; #53 garble → sub-file 8-phần 20.7KB CỨU TRỌN (lần 3); 2 errata lead bắt: path thiếu `Dtos/` + hash stale `47c97df1` vs `5534addb` (kết luận identical vẫn ĐÚNG); phase-set: PriorPes NOT IN {7,99} · delta2 NOT IN {1,7,99}+winner · delta1 NOT IN {1,7,10,99}]:** ⭐ LỆCH-TIỀN-ĐỀ: "lũy kế chỉ bắt DaDuyet" SAI một nửa — `PeBudgetAccumulator.cs:42-48` PrevSubmitted (row1) ĐÃ gồm ChoDuyet=10 từ trước; DaDuyet-only CHỈ ở PrevSelected (row2, :50-60) ⇒ feature thu hẹp = delta row2/row6 (+row1 cho TraLai/nháp tuỳ tập). Predicate runtime 1 BẢN duy nhất (helper) — Features `:962` + WorkflowService `:1018` đều GỌI, Mig67 SQL = bản dịch one-time; "3-4 bản phải khớp" thực tế = 2 sống. FE matrix: row5=1+3 per-cột `PeDetailTabs:1325`, row6=2+4 `:1318`; PeDetailTabs+PeWorkflowPanel 2-app SHA IDENTICAL, types KHÁC (re-confirm S117). Final-approver predicate tái dụng được: `shouldPickPrice` PeWorkflowPanel:510. Dev DB còn phase legacy 3/6 SỐNG → predicate tạm-tính viết loại-trừ `NOT IN (7,99)`. Gotcha mới: SSH prod password extraction 2-step fail ×2 (ConvertFrom-Json null-method) — prod ground UNVERIFIED không chặn spec. Spec 3-mục + 5 decision-points (nháp S89-privacy list-only) → `runs/2026-07-17-S134-pe-luyke-tam-tinh/sub-investigator-codebase-0.md`. Tag `[s134, pe-luyke-tamtinh, lech-tien-de-row1, no-mig]`
-- **S133 (07-17) PE budget FREEZE-at-DaDuyet invest [engine: fable-real-single `wf_348f6155-167`; #53 return-fail (no StructuredOutput) → sub-file 22.5KB cứu trọn, lead ghi entry on-behalf sau verify-đĩa 7/7]:** retro-change 2 đường: BE detail-bundle load pair LIVE `PurchaseEvaluationFeatures.cs:911-912` (DTO :964 — 1 construction site duy nhất) + FE fallback `?? bs.proInitialAmount`/`?? bs.initialAmount` (`PeDetailTabs.tsx:1511/:1526` — draft cite :1288 lệch dòng = class cite-drift S108, bản chất đúng). Finalize = ĐÚNG 4 assignment DaDuyet ALL `PurchaseEvaluationWorkflowService.cs:861/:899/:931/:1053`; `ApplyApprovedPriceOnFinalize` (def :955 static) chỉ 3-call V2 — V1 :1053 KHÔNG gọi ⇒ choke-point = helper MỚI 4-call, KHÔNG tái dụng hook giá-chốt. Vector per-slip: `SetPeCcmBudgetPeriod` NO-phase-guard (:228-242) · `AdjustBudget` admin-bypass (:419-422). Lũy kế row1/2 peer-derived (:917-943) = drift-vector thứ 2 → chụp luôn. Design: 11 cột `ApprovedBudget*` trên PE + display-gate `BudgetFrozen` + 2 terminal-guard + backfill=live-now (Changelogs CÓ log value NHƯNG attribution-gap: log gắn phiếu-đang-mở-lúc-sửa → không tái dựng số lúc duyệt). PeDetailTabs 2-app hash IDENTICAL `931025f2` · types 2-app KHÁC (S117). Tag `[s133, pe-budget-freeze, fable-real, ghi-dia-cuu-2/2, attribution-gap-changelog]`
-- **[→ archive/2026-07.md @S134] S129 (07-16) adap errata EOL 3-lane:** key giữ — 13 dương-giả do filter erratum thiếu docx/xlsx (tailor-per-class) · `git log|tail -1`=first-touch KHÔNG phải policy-landing · BOM-in-blob `mfe-eval.ps1:204` · meta-count class "40=+1 agent" bịa.
-- **[→ archive/2026-07.md @S134] S112/S113/S114 (07-12) Supplier-import-v2 + PE multi-winner per-hạng-mục:** key giữ — `Quote.IsSelected` per hạng-mục LIVE (Mig65, cardinality-change #2) · winnerQuoteTotal=SUM IsSelected `PeFeatures.cs:1188` · Mã NCC=Code · IsPublic fail-closed · PE bypass `ApplyDrafterBypassOnSubmitAsync`.
-
-## 📋 Patterns proven (apply confidently)
-
-### Schema scan via sqlcmd
-```bash
-sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -Q "..." # runtime API (primary)
-sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Design -Q "..." # ef tooling
-ssh vietreport-vps "sqlcmd -S .\SQLEXPRESS -d SolutionErp -U vrapp -P '...' -Q '...'" # prod
+## 📋 Patterns proven
+### Schema scan sqlcmd
```
-Queries: `sys.columns`, `sys.triggers`, `__EFMigrationsHistory`, `COUNT(*)`, `sys.indexes`.
-**Gotcha 2 LocalDB distinct** (`feedback_designtime_runtime_db`): `_Dev`=runtime (appsettings.Development), `_Design`=`dotnet ef` default. Prod password fallback `C:\inetpub\solution-erp\api\appsettings.Production.json` khi `$env:PROD_DB_PASSWORD` empty.
+sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -Q "..." # runtime API (primary)
+sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Design -Q "..." # ef tooling
+```
+Prod: `ssh vietreport-vps` powershell-wrapper `sqlcmd -S '.\SQLEXPRESS' -d SolutionErp -E …` — ⚠️ **SSH session DIES whenever SQL-client loads** (S134/S148 re-confirm); prod-DB effectively UNVERIFIABLE this path, flag + give query for implementer. **2 LocalDB distinct**: `_Dev`=runtime · `_Design`=`dotnet ef` default. Queries: `sys.columns/indexes/tables`, `__EFMigrationsHistory`, dup-detector `REPLACE(Code,' ','')`.
### Controller / wire-claim audit
-- Grep `\[Route\("api/[a-z]+"\)\]` enumerate controllers · `\[Authorize(Policy = "..."` per-action policy (gotcha #44 silent 403 class-level quá strict)
-- Grep `// Mock` / `alert(` / `setEditing(null) // close UI` — wire claim bugs · `IActionResult` vs `ActionResult`
-
-### Smoke verify catalog
-Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expected vs actual + file:line evidence.
+- Grep `\[Route\("api/[a-z]+"\)\]` controllers · `\[Authorize(Policy="…"` per-action (gotcha #44 silent-403; class-level `[Authorize]` trần = no phase-guard, S147)
+- Grep `// Mock` / `alert(` / `setEditing(null) // close UI` — wire bugs
### Memory cross-reference
-30 user-memory tại `C:\Users\pqhuy\.claude\projects\D--...\memory\MEMORY.md` (index). Key: per_chunk_commit · uat_skip_verify · designtime_runtime_db · per_nv_permission_scope · ef_migration_backfill_reorder · workflow_fanout_reliability · canonical_spec_over_broadcast (S79).
-
-### External research → DEFER `investigator-api` (split S39)
-
----
+30 user-memory `C:\Users\pqhuy\.claude\projects\D--…\memory\MEMORY.md`. Key: designtime_runtime_db · workflow_fanout_reliability · canonical_spec_over_broadcast · root_cause_over_symptom · cardinality_change_grep_consumers.
## ⚠️ Anti-patterns
-❌ Skip MEMORY update · ❌ Vague "seems like/probably" · ❌ Missing file:line · ❌ >500 words · ❌ Scope drift to architecture recommendation (em main decides)
-
----
-
-## 🧠 SOLUTION_ERP context essentials (S80 verified — re-ground từ docs/STATUS.md canonical)
+❌ Skip MEMORY update · ❌ Vague "seems/probably" · ❌ Missing file:line · ❌ >500 words · ❌ Scope drift to architecture rec (em main decides) · ❌ Multi-line MEMORY entries (index truncates >24.4KB)
+## 🧠 SOLUTION_ERP context (numbers canonical → docs/STATUS.md; re-ground don't trust)
- **DB:** Dev `SolutionErp_Dev` · Design `SolutionErp_Design` (distinct) · Prod `.\SQLEXPRESS`/`SolutionErp`/`vrapp` via SSH `vietreport-vps`
-- **Migration path:** `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/*.cs` (⚠️ NOT root `/Migrations/`). **57 mig**, last `AddPeSuggestedPriceNotes`.
-- **Counts S80 (canonical docs/STATUS.md):** 57 mig · **88 SQL tables** (count `.ToTable()` ModelSnapshot NOT DbSet) · ~253 endpoints · 68 FE pages · **54 menu keys** (BE `MenuKeys` const) · **354 test** (45 Domain + 309 Infra) · **71 gotchas** (format `### N.`) · 30 user-memory · 6 skills · **11 sub-agents** (S80-snapshot — roster nay 14 vai + worker-tier-pin S124; số sống đọc STATUS.md)
-- **Tech:** .NET 10 Clean Arch (Api→Application←Domain + Infra) + CQRS MediatR + EF Core 10 + 2 React 19 Vite 8 TS 6 (fe-admin :8082 + fe-user :8080) + SQL Server + Gitea CI + IIS
-- **Prod:** api/admin/eoffice.solutions.com.vn · Gitea `git.baocaogiaoduc.vn/vietreport-admin/solution-erp` (Actions API `/api/v1/repos/.../actions/tasks` NOT `/runs` 404, cache stale ~2min gotcha #46 — cross-check VPS mtime). Bundle live admin `CsJetgZH`/user `BVS0ApIm` (Run #330).
-- **Auth:** `admin@solutions.com.vn/Admin@123456` (full) / `nv.test@solutions.com.vn/TestUser@123456` (Drafter). Response `accessToken`+`refreshToken`+`user`. Password ≥12 chars.
-
----
+- **Migration path:** `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/*.cs` (NOT root). Last-schema Mig 67 `AddPeApprovedBudgetSnapshot` (S133); ~67 mig.
+- **Counts (docs/STATUS.md canonical):** ~88 SQL tables (count `.ToTable()` snapshot NOT DbSet) · ~253 endpoints · 68 FE pages · 54 menu keys (`MenuKeys.cs`) · ~354 test · ~71+ gotchas · 14 sub-agents (worker-tier-pin S124)
+- **Tech:** .NET 10 Clean Arch (Api→Application←Domain + Infra) + CQRS MediatR + EF Core 10 + 2 React Vite TS (fe-admin :8082 + fe-user :8080) + SQL Server + Gitea CI + IIS
+- **Prod:** api/admin/eoffice.solutions.com.vn · Gitea `git.baocaogiaoduc.vn/…/solution-erp` (Actions API `/actions/tasks` NOT `/runs`, cache stale ~2min #46)
+- **Auth:** `admin@solutions.com.vn/Admin@123456` (full) / `nv.test@solutions.com.vn/TestUser@123456` (Drafter). Password ≥12.
## 🔄 Active workflow schemas (V1 + V2 coexist post-S17)
+- **V1 Mig 21 flat** — `WorkflowDefinition` pin cũ, match Dept+PositionLevel.
+- **V2 Mig 22-31** — `ApprovalWorkflow` pin PE/Contract, match `ApproverUserId` 1-1 OR-of-N cùng Cấp. Steps(Phòng)>Levels(Cấp). Proposal V2 Mig38, WorkflowApps Mig39. (Mig25 IsUserSelectable · 26 LevelOpinions UPSERT · 30 AllowApproverEditBudget · 31 SkipToFinal→ApproverLevel · 53 cờ-gấp+CeoThreshold · 54 giá-đề-xuất · 58 IsWinner multi + AllowApproverFinalize · 65 Quote.IsSelected per-hạng-mục · 66 ApprovedPrice Min-Max · 67 budget-freeze snapshot).
+- **State PE 5:** Nháp / Đã gửi duyệt / Trả lại (TraLai=98) / Từ chối (TuChoi, REMOVED-path S60) / Đã duyệt
+- **Mode Trả lại 4:** OneLevel / OneStep / Assignee / Drafter (Phase=TraLai). 3 đầu giữ ChoDuyet lùi pointer. Admin bypass `level.Allow*`.
+- **Seeds gating:** `SeedDemoMasterDataAsync` gated `DemoSeed:Disabled` (OFF prod) · `SeedRealMasterDataAsync` UNGATED (62 dự án + 71 hạng mục Excel, per-code idempotent, exact-Code key #75/#76 resurrect risk).
-- **V1 Mig 21 flat** — `WorkflowDefinition` pin PE/Contract cũ. Match Dept+PositionLevel.
-- **V2 Mig 22-31** — `ApprovalWorkflow` pin PE/Contract mới, match `ApproverUserId` 1-1 OR-of-N cùng Cấp. Steps (Phòng) > Levels (Cấp). PE + Contract đã wire V2 (Mig 32+33 S29). Proposal V2 (Mig 38). WorkflowApps skeleton (Mig 39).
- - Mig 25 IsUserSelectable · Mig 26 PE LevelOpinions UPSERT · Mig 29 Allow* per-NV · Mig 30 F4 AllowApproverEditBudget · Mig 31 SkipToFinal→ApproverLevel · Mig 53 cờ-gấp+CeoApprovalThreshold · Mig 54 giá-đề-xuất PRO/CCM+giá-chốt · Mig 56 ProInitial/ProAdjust · Mig 57 ghi-chú-giá.
-- **State machine PE 5 trạng thái:** Nháp / Đã gửi duyệt / **Trả lại (TraLai=98)** / Từ chối (REMOVED-S60 hard-guard) / Đã duyệt
-- **Mode Trả lại 4 option per-Level:** OneLevel (lùi 1 Cấp) / OneStep (lùi Bước trước) / Assignee (pick NV đã ký) / Drafter (Phase=TraLai). 3 mode đầu giữ ChoDuyet lùi pointer. Admin bypass `level.Allow*`.
-
----
-
-## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
-
-- **S125 (2026-07-16) adap-review invest-lens [engine:fable-real single deep-pass · vai compound anh gán reviewer+inv-cb · chạy inline-lead]:** probes: auto-memory **50 tracked/0 porcelain** (R6 git-track ĐẠT — còn WRITE-path post-restart) · agents **14/14 `model:opus`+`effort:max`** (file 15=README benign) · 6/6 product-carry còn HANDOFF · H24 counter=4 tick-S125 content-addressed (light@9/deep@18, 0 class-repeat≥3) · Stop-hook wal-flush KHÔNG fire per-turn phiên này (HEAD đứng `3aacdf9` suốt) · residue `all-inherit` floor-sống chỉ 2: `fable-real.md:37` (gap thật — file NGOÀI sync-list S124) + `fable-clone.md:43` (asym trong-file, :57 updated :43 sót) — CẢ 2 vá S125; `session-start.md:52`+`hmw.js` comments = lineage GIỮ. Hub-side: outbox/all=65, `stamp_verify.py` 3-OK / 2-NO-DECLARED-SHA (do-token·h17-bookend) · hub ĐÃ CÓ script-verify s76 `check_email_pull.py` (hub check-email.md:31) — bootstrap-copy sister CHƯA mang. Tag `[s125, adap-review, verifier-suspect-first, worker-tier-pin-residue]`.
-- **[→ archive/2026-07.md] 2026-07-13 PE negative-quote 2× LENS (Budget-consumers + edge/precedent/test)** — key giữ: budget-not-mutated-by-quote (architectural) · submit-guard `Sum()<=0` `:206` duy-nhất reject · no-quote-validator · parseVnd strip-minus · giaTri-âm bypass validator · gotcha #81 orthogonal-dấu. Executed S116; anchors sống trong S116/S117 entries.
-- **[→ git 07-12 · superseded by S115 entry above] PE per-HANGMUC multi-winner LENS BE-consumer-audit:** 3 cardinality axes — `SelectedSupplierId` single (legacy) · Mig58 `IsWinner`=WHOLE-SUPPLIER liên-danh · `Quote.IsSelected` per hangmuc×supplier (S114 made LIVE). 5 money consumers keyed off IsSelected now (winnerQuoteTotal/submit-guard/ccm-threshold/giaTri/budget-block-B). ~6 test files. Tag `[pe-multiwinner, per-hangmuc, quote-isselected, cardinality]`.
-- **[→ git 07-12] Supplier-import-v2 LENS-3 FE-dialog-UX:** SupplierImportDialog+SuppliersPage SHA-identical 2-app; dialog 2-step preview→confirm round-trips `preview.rows` → per-row edit feasible; `ExpectedHeaderTokens` 30-token baked `SupplierExcelImportService.cs:35-68` = template single-source; Mã NCC=Code; import authz-mismatch BE `[Roles=Admin,CatalogManager]` vs FE `Suppliers/Create` open. Tag `[s112, supplier-import-v2, fe-lens3, import-authz-mismatch]`.
-- **[→ archive/2026-07.md verbatim + digest @S125] S105 BVAAU auth RCA:** class **"hub+site-tĩnh relative-`/api` chết im prod"** (web.config loại `^/api/` → 404 → anonymous); fix API_BASE-absolute `51c69fb`.
-- **[→ archive/2026-07.md verbatim + digest @S125] S104 Supplier 30-field schema-invest:** ClosedXML 0.105.0 chuẩn (KHÔNG OpenXml-raw) · `UpdateSupplierCommand.cs:49-57` blind-set=latent#73 · SupplierDto hand-projected 2-site · chốt Mig 62 +18 nullable.
-- **[→ git+session-log 07-03 + adap-reports] S101 H19-adopt Lane-2:** map reviewer=review-gate / investigator=ground-truth-lane; 12-pt tier-checklist ALL✅ (11/11 inherit, C3 no-`[1m]`-in-frontmatter); toggle-marker MUST copy `.gitignore:87` last-match placement; VACUOUS-MET all-inherit+Fable. *(verbatim → archive/2026-07.md)*
-- **[→ git+session-log 07-02 + adap-reports] S100 H18-adopt WF1:** ⭐ ghost-wire-class **config-key-defined-but-unread** (`budget.json:54-55` script re-hardcode same-value→no-flag; catch=grep-key-across-scripts); cron=ghost-wire (CronList=0); `_ledger` orphan-catch 0/15-lifetime. *(verbatim → archive/2026-07.md)*
-- **[→ git 07-12 · Mig64 IsPublic shipped since] Supplier import-v2 LENS-4 public/visibility recon:** 3 SELECTION consumers ALL hit `GET /suppliers` ListSuppliersQuery (SuppliersPage:83 · PeDetailTabs:2222 · ContractCreatePage:321); DISPLAY-of-existing joins by stored SupplierId MUST NOT filter (ContractFeatures/PeFeatures/Dashboard); write-guard backstops `ContractFeatures.cs:64`+`PurchaseEvaluationSupplierFeatures.cs:49`. Rec fail-closed default + stored `IsPublic bit`. Tag `[s112b, supplier-visibility, fail-closed-default]`.
-- **[all verbatim → archive/2026-07.md] S98 verify-lane (detector 28→25, 8 derived-stale=FP vs canonical 61/89/440/79; grep exact misses README variant) · S97 feedback-audit (#77/#78/#79 `gotchas.md:1301/1317/1325`; #53-garble×6=cicd MEMORY-self-target ~17.5KB; Mig60 wrong-table Approvals→Mig61 Changelogs) · 07-01 Supplier-predecessor SUPERSEDED by 07-06 · S93 MFE denom=marks/AS/guards dedup-AS-xref=29, coverage exact-id OR ≥2-content-word.**
-
-- **[→ archive/2026-07.md @S125 + 2026-06.md] PE FROZEN-Mig59 recon (S87→S89):** giữ nóng ⭐ LATENT #73-class HoSoLink PUT-clears (`PurchaseEvaluationFeatures.cs:280`) · 3 cơ-chế finalize coexist AUTO-first · budget-matrix authz-asymm PRO=drafter/CCM=role.
-- **[→ archive/2026-07.md @S125 + _INDEX] 2026-06-20 governance recon:** `harness-11-engine.md`=CANONICAL (D5-D10) · `error-ledger.md` §L.a/§L.b · H14 age-decay KHÔNG căn-cứ cắt feature · EVAL golden-set 14q weekly-manual.
-- **[→ archive/2026-07.md @S125 + _INDEX] PE recon (S69→S76):** WinnerQuoteTotal DERIVED 3-site · Budget gate PURE-ROLE (`PurchaseEvaluationFeatures.cs:800-801`) · V2 routing IGNORES roles (=ApproverUserId) 100% LINEAR.
-- **[→ archive/2026-07.md @S125 + _INDEX] Office/HRM recon (S65→S69):** 21 `Off_*` keys · 10 office pages SHA-mirror · golive-flip=remove `RevokeTemporarilyHiddenModulesAsync` (`DbInitializer.cs:2040`) · **gotcha #66** `index.css` ngoài @layer → heading cần `text-white!`.
-- **[→ archive/2026-07.md @S125 + _INDEX] Governance/Harness recon (S71):** run-trace TRACKED = 2-level (`check-ignore` eligible vs `ls-files` committed) · path-trap `.claude/workflows/runs/` (bare `runs/`=0) · G-015 containment H10.
-- **[→ git pre-S60]** S60 recon#2 V2-engine-map (ApproveV2Async:446-634 guard+UPSERT+advance; skipToFinal F2:561-602 precedent) · S60 PE Section-3 submit-guard ROLE-only. Full text git.
-- **[→ archive/2026-06.md + git]** S52 P11-D/E/F 6-gap recon · S50 HrmConfigs add-kind 11-spot · S51 gotcha #57 EXT reachability (curated S59).
-- **[→ archive/2026-05-q4.md + git d2f52ba]** S29-S37: clean-room > NamGroup-port verified 4× · Pattern 12-bis cross-module mirror · FK+freetext dual-write.
-
-- **[→ git 07-10] S108 first-real-run H19 cite-audit:** 103/111 exact; class = worker-draft cite lệch ±1-2 quanh code-fence (audit MỞ ĐÚNG DÒNG đừng tin range) + drift-note-stale + email-fact honesty-trap. 0 garble.
-
-- **[→ archive/2026-07.md] S111 (07-12) H22 WAL deep-pass `[fable-real-single]`** — READY_WITH_NUANCES; hook=STAGED-until-verified (S100 ghost-wire class); engine:402 = MÔ-TẢ không phải luật cấm; Stop-hook fire TURN-boundary only. (H22 nay VERIFIED-RUNTIME per project memory.)
-- **[→ archive/2026-07.md] S110 (07-11) H21+MTv3 deep-pass `[fable-real-single]`** — PASS; 12 persona 0-hit; regex-heuristic-miss class (sister diễn-đạt khác cụm); hmw.js:38 comment-conflict; standing-imperative-trong-history-blurb class; #53 garble→SendMessage re-emit recover OK.
-
----
-
-- **[→ archive/2026-07.md @S125 · Mig 66 SHIPPED] S117 PE giá-chốt RANGE Min+Max invest [fable-real-single]:** ApprovedPrice WRITE 1-site `ApplyApprovedPriceOnFinalize` (3 nhánh DaDuyet; skipToFinal chỉ dời pointer) · ⭐ HĐ-gen KHÔNG đọc ApprovedPrice (GiaTri=SUM IsSelected `CreateContractFromEvaluationFeatures.cs:88-100`) · `types/purchaseEvaluation.ts` KHÔNG SHA-mirror 2-app (edit riêng) · latent: winnerQuoteTotal subset ÂM vs validator `>=0 :501`. `[s117, prominmax]`
-- **[→ archive/2026-07.md @S125] S117 PE lũy-kế RCA [fable-real-single]:** phiếu A/018 KHÔNG DaDuyet (opt-in finalize UNCHECKED S97 `PeWorkflowPanel.tsx:57`) · ⭐ residual DISPLAY-LIE #78 nửa-còn-lại: `endsBeforeCeo`=config-TRUE bất chấp pointer (detail `:1152-1154` · list `:647-651`) · 3 cơ-chế finalize phân biệt Mig58-optin ≠ Mig53/54-threshold ≠ Mig31-pointer · fix-rec pointer-aware, KHÔNG nới filter lũy-kế `PeFeatures.cs:906`. `[fable-S117]`
-
-- **[→ archive/2026-07.md @S126] S116 PE negative-quote invest (fable-clone):** hội-tụ **FE-ONLY** (BE/DB nhận âm sẵn); ⚠️ quote sống `details[].quotes` KHÔNG `suppliers[].quotes`. Detail: `runs/2026-07-13-pe-negative-quote/`. `[Opus-S116]`
-
-- **[→ archive/2026-07.md @S125 verbatim] S122 retro-harvest 7 orphan (6/6 lane 0-garble):** 🔴 LẬT tiền-đề — **0/7 mất, 7/7 XONG chỉ synthesis SAI CHỖ** (orphan đo TÊN không đo NỘI-DUNG, 8 dương-giả/25=32%) · lane-RỖNG ≠ mất (per-turn ≠ per-task) · memoryPack premise CÓ THỂ SAI — lane tự truy git đừng tin brief. Tag `[s122-retro-harvest, false-orphan-by-naming]`.
-- **S126 (2026-07-16) A2 worker-pin tally + B1 all-inherit sweep [opus ×2, em-main VERIFY+APPEND]:** A2: **14/14 opus + 14/14 max** — lane no-tier CHÍNH NÓ resolve `claude-opus-4-8` DÙ lead=Fable ⇒ pin ăn runtime. B1: 284 instance → **15 floor-sống/5 file** + 26 lineage + 235 frozen; ⭐ nặng nhất `vocab-alias-map.md` §3 TỰ khai all-inherit="Canonical (hiện hành)" — doc chống-fork tự fork; class = S125 vá 2 command-file KHÔNG grep sibling governance. Lesson: mega-line blow Read-cap → perl-window giữ file:line; 2-pattern grep LỌT biến-thể không-literal (runbook:119 "lead-runtime (inherit)") — cần mắt-người pass khi vá. Em-main vá 15+3, fault-inject Section-D 2-chiều PASS, re-grep 0 floor-sống. Tag `[s126, h23-precedence, all-inherit-sweep]`.
-
-- **S128 (2026-07-16) adap-đợt-16-07 B ground-truth 6-việc + spec-draft [fable-real-single; return GARBLE #53 → lead ghi on-behalf sau verify-đĩa 5/5]:** ⭐ LỆCH-1: config RAG per-project nằm TRONG repo con — `AI_INFRA\claude-rag\lib\projects.py:43-56` walk-up cwd tìm `.claude/rag.json`, `cfg.setdefault` chỉ merge key THIẾU ⇒ SE `rag.json:26 true` override THẮNG default `False` khung (`projects.py:39` owner-final) ⇒ flip = in-repo edit, KHÔNG cross-repo. · Nhãn-đo sweep: CHỈ C1 `crystallized-backfill.ps1:25/:117` mang "calibrated"; `mfe-eval.ps1:166-180` CÙNG divisor /4.0-/3.0 nhưng honest-RANGE không-nhãn ⇒ cùng-số-khác-tội — nhãn là vấn-đề, không phải hằng-số. · `budget.measured` 0 automated reader (`memory-selfimprove-audit.ps1:18/:110` khai stale+ignore; archive-gate/backfill đo live) — MẠNH hơn seed verdict A "1 reader ignore". · Ghi-đĩa-TRONG-lúc-làm cứu trọn draft 6/6 việc khi return garble (bài S123 lần đầu chạy THẬT ở lane invest). Draft: `runs/2026-07-16-S127-adap-dot-16-07/sub-investigator-2-spec-draft.md`. Tag `[s128, adap-dot-16-07, lech-1-config-in-repo, ghi-dia-trong-luc-lam]`
-
-## 2026-07-17 S138 — /fable-real invest wave adap 3 bản 17-07 (engine: fable-real single deep-pass · model `claude-fable-5` — em-main stamp @P3)
-- **Task:** invest + SPEC-propose wave 3 bản AI_INFRA (đính-chính-kép `624c378d` · master-checklist `cadd24ae` · năm-quyết `123d8272`) — run `2026-07-17-S138-adap-3-ban-17-07` `wf_56d248ea-f8e`, sub-MD ghi-đĩa-trong-lúc-làm TRỌN trước return (0 garble, 35 tool-use).
-- **Verdict:** 3/3 FIT; cần-implement THẬT = 3 cụm (khối MASTER-CHECKLIST 2-tầng 4 cửa · detector C6 cite-2-nấc · H24-4 pending-flip); Δ2 = **SE-originated** (outbox errata-report `:20` = chính ca 13-dương-giả trong broadcast); còn lại already-compliant/text-only. Lead verify độc-lập 4/4 claim chịu-lực khớp đĩa.
-- **Learned:** Delta chữ-hẹp của hub PHẢI grep **landing-site trong luật SỐNG** của mình — SE chép nguyên danh-sách 4-nhãn lỗi vào `reviewer.md:46` (adopt-verbatim = kế-thừa cả lỗi nguồn). Detector mới có matcher prose ⇒ khai `Test-Quoted` + biết-trượt ngay TRONG spec (spec tự-quy-chiếu vì mô tả token CHỜ GẬT).
-- **Surprise:** (a) G-011 nấc-ladder 0 định-nghĩa canonical on-disk (import ngầm hub-convention); (b) sweep hứa-máy = 0 unbacked — SE quen cite thẳng tên script; (c) `mfe-eval.ps1` vừa đo vừa ghi `.mfe-state.json` vào vùng-họ-đo (benign — khai ngoại-lệ trong câu luật K1); (d) HANDOFF L15/L19 lineage còn CHỜ-GẬT của việc đã đóng = 2 dương-giả nếu H24-4 quét cả file → scope segment-hiện-hành.
-
-## 🔄 Curate trigger
-- >~30KB → archive recent → L2 `archive/.md` (byte-exact append) + `_INDEX.md` substring pointer. Stale >3mo → remove.
-- **Last curate: 2026-07-16 S125 (em-main single-writer, hook-triggered @23.9KB)** — 14 dòng ≈9.5KB (5 cluster digest S87-89/06-20/S69-76/S65-69/S71 + 2 section S117 + block S122 retro-harvest) verbatim byte-exact → `archive/2026-07.md`, thay stub giữ anchor nóng. L1 ~15KB.
-- **Prev curate: 2026-07-06 (em-main single-writer, hook-triggered @21.4KB)** — 6 stale governance/superseded entries (S93/S97/S98/S100/S101 + 07-01 Supplier-predecessor) verbatim → `archive/2026-07.md`, replaced with pointer-stubs. L1 back under target.
-- **Prev curate: 2026-06-18 S71** (29.8→23.2KB FIFO-trim 3 + gist gen:2). **Prev S70** (47.0→24.1KB dark-matter recovery + built _INDEX/gist). **Prev S40** (35.7→~20KB).
+## 🔄 Curate log
+- **S148 (07-23):** hard-cap rewrite 28.2→~14KB — collapsed multi-line S133-S148 → 1-liner, dropped stale pointer-list (S87-S114 → grouped 1-line, verbatim in archive/git). Single-writer inv-cb (hook @28KB).
+- **Prev:** S125 (23.9→~15KB, 14 lines→archive/2026-07.md) · S71/S70/S40. Verbatim byte-exact in `archive/`.
diff --git a/.claude/agent-memory/reviewer/MEMORY.md b/.claude/agent-memory/reviewer/MEMORY.md
index 9258b24..b07da55 100644
--- a/.claude/agent-memory/reviewer/MEMORY.md
+++ b/.claude/agent-memory/reviewer/MEMORY.md
@@ -1,14 +1,13 @@
# Reviewer Agent — Persistent Memory
-- **S146 (07-22) PRE-COMMIT spec-v2-applied (governance-only, 20 file) — 2 MAJOR THẬT, cả hai của LEAD; nấc `verified` (tự chạy lệnh, không tin lead):** **F1** `session-end.md` ×2 còn `hook-3-path` **GẠCH NỐI** — worker CE **đã chỉ đích-danh trong sub-file**, worker D vẫn rơi ⇒ class *"residual có người chỉ tận nơi mà vẫn rớt"*; bất-đối-xứng nội-bộ (`pause.md` đã 4-path, `session-end` chưa) = 2 command anh em nói ngược nhau về **cùng một hook**. **F2** phép quét cardinality của lead **siết LỆCH TRỤC** (co-occurrence CÙNG DÒNG + tập-file hẹp + 1 mẫu chữ) ⇒ chạy lại trên HEAD tiền-vá: bắt **7/11**, **lọt 4 ca thật — gồm chính F1** (`3 path` dấu-cách KHÔNG khớp `hook-3-path` gạch-nối). **F3 HELD** — tự `git show da7ee8a` xác-minh hook 4-path nuốt `sessions/session-1/probe.md` + `settings.json` dùng `-File` ⇒ body re-đọc mỗi Stop, claim lead ĐỨNG. **F4** probe residue → ô-nhiễm ID-space `` + làm ⑤ kêu tới khi push. 🔴 **Bài rút:** lead sửa 1 phép quét **4 lần/1 phiên** (rộng→hẹp→rộng→chốt) vì cố mã-hoá **ý-định NGỮ-NGHĨA** vào regex; nấc đúng = **lưới-soát-cho-người + allowlist CÓ TÊN + use⟂mention**. Lần 4 = **citation-trap**: bộ dò bắt chính nốt-vá vì nốt vá trích-dẫn giá-trị cũ.
+- **[verbatim → git 8b6df0a closeout] S146 (07-22) PRE-COMMIT spec-v2-applied (governance 20 file) — 2 MAJOR THẬT (cả hai của LEAD); nấc `verified` tự-chạy-lệnh không-tin-lead:** F1 `session-end.md` còn `hook-3-path` GẠCH-NỐI dù worker CE đã chỉ đích-danh (class "residual có người chỉ tận nơi vẫn rớt") + bất-đối-xứng nội-bộ (`pause.md` đã 4-path). F2 phép quét cardinality của lead siết LỆCH-TRỤC (co-occurrence CÙNG-DÒNG + 1 mẫu-chữ) → re-run HEAD tiền-vá lọt 4 ca thật gồm chính F1. F3 HELD (git show da7ee8a: hook 4-path + `-File` re-đọc mỗi Stop). 🔴 Bài rút: lead sửa 1 phép quét 4 lần/phiên vì mã-hoá ý-định NGỮ-NGHĨA vào regex; nấc đúng = lưới-soát-cho-người + allowlist CÓ TÊN + use⟂mention; lần-4 = citation-trap (dò bắt chính nốt-vá vì nó trích giá-trị cũ).
-- **[→ archive/2026-07.md @S145-curate] S145 Axis-E lane (tiep/pause/snapshot + CROSS-CUT) SPEC apply-hub — FAIL 2C/1M/4m:** corroborated Axis-A "4 surface→2"=FALSE independently. UNIQUE 2 seam-catch: (C↔E CRITICAL) C.2 hook 3→4 path (+sessions/) phá Sàn-3 tiep.md — "3 path" hardcode 4 site (:21/:23/:75/:78) ⇒ sessions/ orphan KHÔNG signal = blind-spot ON RECOVERY FLOOR + misclassify-dirt logic-bug; cả 2 death-path ko nhắc ripple. (MAJOR) session_ctx_kb=64 nghịch owner-authority (OWNER-GATED + K6.2 cấm mượn-số-hub-chưa-đo) + ghost-wire #H18 (0 script đọc key). LESSON: bất-biến chia-đôi giữa 2 axis nằm ĐÚNG SEAM → chỉ cross-cut sweep bắt; "áp hub-answer" ≠ "số owner-giữ".
-- **[→ archive/2026-07.md @S145-curate] S145 lane-4 (Axis-D 5-vòng-closure) SPEC apply-hub — PWF 2M/1m:** D2 RULING (owner-asked): session-start-KIỂM defer LEGIT — canonical "hai đầu" binds MEASURE (H1 báo-diff session-start.md:127 both-ends), self-verify col=CLOSE-time ⇒ 3 KIỂM end-only canonical-consistent. 🔴 phép-4 LANDMINE live: `agent-memory/tooling-harvest-audit/` EMPTY + h24/sleep-audit folders MISSING; tooling-harvest-audit AUTO-fires close ⇒ ko-write-sổ = phép-4 FAIL. FIX: D1 acceptance per-check disk-evidence ko bare-PASS + D2 NAME defer-doc-site. LESSON: 'vai-có-sổ' acceptance ⇒ MUST `ls agent-memory//` (folder≠sổ).
-- **[→ archive/2026-07.md @S145-curate] S145 C7-lane (Axis A) — PWF 1C:** spec "4 surface→2" stale-fix = FALSE (`nhip-no-probe.ps1` 4 caller THẬT, header self-doc đúng; spec conflate 2 `*-probe.ps1`); A2 grep-"4 surface"=0-hit = ANTI-TEETH Goodhart (PASS bằng xóa info đúng). LESSON: stale-fix claim verify caller-trên-đĩa trước; 2-script-same-suffix = conflation-vector.
+- **[→ archive/2026-07.md @S145-curate] S145 Axis-E lane — FAIL 2C/1M/4m:** cross-cut seam catch (hook 3→4 path sessions/ orphan phá Sàn-3 tiep.md; session_ctx_kb=64 nghịch owner + ghost-wire #H18). LESSON: bất-biến chia-đôi giữa 2 axis nằm ĐÚNG SEAM → chỉ cross-cut sweep bắt.
+- **[→ archive/2026-07.md @S145-curate] S145 lane-4 (Axis-D) — PWF 2M/1m:** phép-4 LANDMINE (`agent-memory/tooling-harvest-audit/` EMPTY, auto-fires close ⇒ phép-4 FAIL). LESSON: 'vai-có-sổ' acceptance ⇒ MUST `ls agent-memory//` (folder≠sổ).
+- **[→ archive/2026-07.md @S145-curate] S145 C7-lane (Axis A) — PWF 1C:** spec "4 surface→2" stale-fix FALSE (4 caller THẬT); grep-"4 surface"=0-hit = ANTI-TEETH Goodhart. LESSON: stale-fix verify caller-trên-đĩa; 2-script-same-suffix = conflation-vector.
- **[→ archive/2026-07.md @S145-curate] S145b (07-22) GATE roster #2/#3 h24-audit+sleep-audit (18→20) — PASS 1m:** implementer vá ĐÚNG mọi S145a-finding + tự sweep 2 NUMERIC site ngoài checklist. 1 MINOR measured-label: `README:249` "17 folder" falsified by ls=18 (tooling-harvest-audit RỖNG). LESSON: đếm VALID_ROLES = per-file grep bidirectional KHÔNG regex (hmw.js `[2]`/`[S110` comment lừa parser).
-- **[→ archive/2026-07.md @S145b-curate] S145a — GATE roster vai#1 FAIL 3M/4m:** NUMERIC "roster 17" hardcode trốn name-enum sweep ⇒ LUẬT sweep repo-wide grep CẢ numeric LẪN name-enum; "self-verify 0-stale" FALSIFIED (scoped≠repo-wide). Superseded S145b PASS.
-- **[→ archive/2026-07.md @S145b-curate + sub-reviewer-0.md] S143 — GATE outward FAIL 1C/4M:** CLASS `bằng-chứng-tự-huỷ-sau-squash` (cite outward = commit SỐNG-sau-squash, KHÔNG `wal:`); `git status --porcelain` = bước ĐẦU outward-gate; số tuyệt-đối neo-mốc (46 vs 47 vs 45); errata CẤM sửa file đã-stamp; #53 "ghi-đĩa-trong-lúc-làm" vớt.
-- **[→ archive/2026-07.md @S140/@S134] S139 · S134b · S133 (digest, 1 mệnh-đề/entry):** S139 GO — re-measure MỌI acceptance từ git HEAD (không tin sub/EM) + **m-3 no-self-exempt: chấm cả edit vào file CHÍNH VAI MÌNH**; nhãn "measured" sống CHỈ KHI falsify-path tồn-tại ∧ reviewer CHẠY ∧ nó đứng · S134b PASS — "insertion-only + `grep '^-'`=0" chắc hơn diff-mắt; D4-persist = trace TỪNG caller tới SaveChanges · S133 PWF — literal-grep 4-site PASS nhưng `.Phase = ` lòi 2 đường GIÁN TIẾP bypass helper ⇒ **"mọi nhánh set X" phải grep CẢ assignment-qua-BIẾN** (#81).
+- **[→ archive/2026-07.md @S145b-curate] S145a·S143 (digest, archived):** S145a NUMERIC "roster 17" hardcode trốn name-enum sweep ⇒ sweep repo-wide CẢ numeric LẪN name-enum (superseded S145b PASS) · S143 CLASS `bằng-chứng-tự-huỷ-sau-squash` (cite outward = commit SỐNG-sau-squash) + số neo-mốc + errata CẤM sửa file đã-stamp + #53 ghi-đĩa-trong-lúc-làm.
+- **[→ archive/2026-07.md @S140/@S134] S139·S134b·S133 (digest, archived):** S139 re-measure MỌI acceptance từ git HEAD + no-self-exempt (chấm cả file CHÍNH VAI MÌNH); "measured" sống CHỈ KHI falsify-path CHẠY ∧ đứng · S134b insertion-only + `grep '^-'`=0 chắc hơn diff-mắt · S133 "mọi nhánh set X" phải grep CẢ assignment-qua-BIẾN (#81).
> **Persistent diary cross-session.** Auto-injected first ~200 lines at spawn (L1 HOT).
> Update BEFORE every stop. Tiered Memory v1: L1 HOT cap ~17KB (hook 24.4KB read-limit) · L2 `archive/` on-demand · L3 RAG `search_memory` just-in-time. Keep entry ≤ 1.5K chars (gotcha #53).
@@ -25,6 +24,7 @@
- [S113 Supplier import v2 close-review (owner anh Kiệt)](project_s113_supplier_import_v2_review.md) — GO-WITH-ADJ 2 must-fix (consumers không pass published=true → drafts leak). Lesson: "filter added" ≠ "drafts hidden" — grep-all-consumer + create write-path.
- [S118 Procurement master-access seeder review](project_s118_procurement_master_access_review.md) — PASS(cond); MAJOR Reports.Read leaks financials + Suppliers.Update grant ≠ edit. Lesson: menu-flag grant ≠ API capability; bare `[Authorize]` GET = open-to-all.
- [S123 governance 4-change review (backtick-guard + H24-3 + retire dạng-3 + mark)](project_s123_governance_4change_review.md) — PASS_WITH_FIXES; CAUGHT H24-3 thiếu enclosure-guard mà cùng diff vừa chứng cần (gen-2 citation-trap) + greedy `.*` lấy `(S` CUỐI≠MAX + lý-do-retire áp cho dạng-2 thì giết C8. Q5 "HOÀN-THÀNH mark" = nguỵ-biện. Q8 ENDORSE không-bump.
+- [S147 Project whitespace-dupe merge verify](project_project_dupe_merge_verify.md) — 7/7 PASS(2 FK-terminology corrections); "FK Projects" COMMENT ≠ physical FK (sys.foreign_keys=0 to Projects, all loose-Guid); seed-durability FORCES survivor=no-space blanket; INNER-join+filter=silent-vanish→repoint-before-soft-delete; prod ref-dist unverifiable (Server=localhost+secret).
---
@@ -80,23 +80,16 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` + `archive/2026-07.md` via `archive/_INDEX.md`)
+- **S147 (07-23) DIAGNOSIS-review PE attach TraLai (no diff — verify root-cause + stress fix) — Diagnosis PASS 5/5, fix CORE-safe/EXT-unsafe:** 5 claims verified from disk; BE-no-guard CONFIRMED **intentional** by S78 changelog L9 ("handler KHÔNG guard drafter-only → approver upload no-403") + sole pipeline behavior=ValidationBehavior. 🔴 **MAJOR blind-spot:** `attachEditable=isDrafter&&isEditablePhase` SAFE for GeneralAttachmentsSection (:2013 readOnly=raw, no carve-out) but UNSAFE for SupplierAttachmentsCell (:2758) whose readOnly=**itemsReadOnly** (`readOnly&&!approverEditMode`, Mig28-F3 ChoDuyet approver-edit) → naive REPLACE breaks approver QuoteDocument edit in ChoDuyet; SAME gap in optional BE guard (carve-out {ChoDuyet+ApprovalAttachment} misses ChoDuyet+QuoteDocument). **Scope trap:** isDrafter@:140 in MAIN scope, OUT-of-scope at :2013/:2758 (child comps take only {ev,readOnly}); changing :421 over-broadly hits HoSoLinkRow:2017. **Completeness:** WORKSPACE (readOnly=false, picker editableOnly∋TraLai) already edits comparison-doc in TraLai → bug is LIST-detail-only (:574/:669 hardcode). LESSON: **decouple a gate → trace if shared prop already carries a carve-out (itemsReadOnly ≠ raw readOnly); "same fix" for sibling surface may hit a DIFFERENT gate source.**
-- **[→ archive/2026-07.md @S134] S131b GATE#2 outward follow-up — PWF 1M:** follow-up bounded-source phải GIỮ con-số source ("hàng loạt" re-inflate "3 closeout" = re-introduce class morning-gate đã gỡ) · verbatim-quote owner so TỪNG CHỮ kể cả homophone x/s.
-- **[→ archive/2026-07.md @S134] S131 GATE outward 3-op-gap — PWF:** → [topic](project_s131_h24_h22_3gap_outward_gate.md). Số atomic đúng HẾT vẫn fencepost (ghép lệch cột-mốc đẻ "gấp-đôi" 2× khi thật 1.6×) — hero-ratio re-derive từ raw + đếm tay từng nhãn.
-- **[→ archive/2026-07.md @S134] S129 adap-errata-EOL 2-lane — GO-WITH-FIXES ×2:** writer-only = NỬA-VÁ (consumer-sweep áp cho cả ĐỀ-XUẤT-VÁ) · tally "7/7 ADOPTED" = rung-flatten · quick-test tự chạy lại = chứng không-bịa rẻ nhất · story-delta BỊA bắt bằng glob-scope.
-- **[→ archive/2026-07.md @S128/@S126] S124 ×2 + S125 ×2 (digest):** push-guard "ahead-of-broadcast" OVERCLAIM (looser ≠ ahead) · do-token trap bắt WHILE adopting ⇒ verify = re-Read full + tự đọc truncation-line · notice-N = CẬN-TRÊN heuristic · false-TAMPER → RCA verifier-bệnh (sibling-test 2-CHIỀU: MATCH⇒file-bệnh · FAIL⇒verifier-bệnh); stamp-then-edit = re-stamp + re-selftest cùng lượt.
-- **[→ archive/2026-07.md @S134] S123 governance 4-change — PWF:** → [topic](project_s123_governance_4change_review.md). 🔴 #53 garble tại TAO ⇒ ghi diary TRƯỚC return · vá-1-lớp ⇒ grep MỌI matcher cùng-lớp TRONG diff.
+- **[→ archive/2026-07.md @S126/@S134] S123·S124·S125·S129·S131·S131b digest (all archived):** S131b follow-up phải GIỮ con-số source · S131 số-atomic-đúng vẫn fencepost → hero-ratio re-derive từ raw ([topic](project_s131_h24_h22_3gap_outward_gate.md)) · S129 writer-only = NỬA-VÁ + quick-test tự-chạy = chứng rẻ nhất · S124/125 do-token trap → re-Read full + sibling-test 2-CHIỀU (MATCH⇒file-bệnh·FAIL⇒verifier-bệnh) · S123 🔴 #53 ghi diary TRƯỚC return + vá-1-lớp grep MỌI matcher cùng-lớp ([topic](project_s123_governance_4change_review.md)).
- **Digest S93→S117 (verbatim → `archive/2026-07.md`; 1 mệnh-đề/entry):** S93 code-gate re-derived denom · S97 default-flip byte-mirror SHA ×2 · S98 Windows byte-verify = .NET (MSYS strip
báo sai) · S100 HELD stale run-id NOT-stamped · S101 persist-claim cần reader-side · S108 outward-claim "đã sửa X" phải cat X trước gate · S109 gist meta-count ≠ disk · S110 claim-về-code grep lại kể cả reword · S111 nâng-nấc-quên-sync cùng-diff · S115 verify EACH regex-alt by-hand · S116 persist-claim = tracked + 0-drift · S117 2-tier guard compare SUM-EXPRESSION.
- **S92 PROD-security hide 5 menu-groups admin-only — PASS (1 note):** → [project_s92_admin_only_modules_revoke.md](project_s92_admin_only_modules_revoke.md).
- **June-2026 digest — LESSON-ONLY (verbatim → `archive/2026-06.md` + `_INDEX`):** merge-distill fabricates false-specificity ⇒ QUALITY gate AFTER presence · sha = self-declared NOT recompute · fail-closed guard BEFORE terminal-state + server-recompute · TRACKED = check-ignore ≠ ls-files · menu-hide ≠ API-lock · single→multi stops at display-layer.
-- **[→ archive/2026-07.md @S128 + topic] S112 close-reviews+FINAL — GO 0-must:** derive guard từ ENDPOINT authz; EDGE-5 gác FROM-state ĐỈNH method; self-flip ≠ external-accept. → [topic](project_s112_supplier_import_review.md).
-- **[→ archive/2026-07.md @S128] S126 A1-H23-probe + email-H23-gate — gate FAIL bắt 3 lỗi trước stamp:** số AGGREGATE cumulative KHÔNG vào outward artifact (evidence bền = per-lane run-id) · "ba việc" = 2 ASK (mở thư đếm thật) · self-obs R1-open → hedge + quy-thuộc. Stamp `945cf3ad`. Evidence: runs/2026-07-16-S126-adap-spec-execute/.
-- **[→ archive/2026-07.md @S134] S128 D-review 2-lane adap — GO-COMMIT 2/2:** direction-of-error quyết mức lỗi outward (lệch CONSERVATIVE = minor) · sha-variant-per-publisher: brute-force canonical-variant với known-good TRƯỚC khi phán tamper · soi literal-fidelity evidence-paste CẢ KHI giá-trị đúng. Trace: `runs/2026-07-16-S127-adap-dot-16-07/`.
-- **[→ archive/2026-07.md @S134] S128 fable-real #2 spec-execute — GO-WITH-FIXES 4SF:** lệnh verify trong spec = CODE chạy thử trước (2/4 sai thật) · Get-FileHash ≠ body-sha (sai-loại-hash) · fold = disposition TỪNG DÒNG.
-- **[→ archive/2026-07.md @S134] S127 fable-real #1 gate đầu-vào — GO-ADAP, BÁC claim hub:** hedge broadcast test bằng MỞ config THẬT (rag.json override ACTIVE) · "already-aligned" tinh-thần ≠ luật. Full: `runs/2026-07-16-S127-adap-dot-16-07/sub-reviewer-1-verdict.md`.
+- **[→ archive/2026-07.md @S128/@S134] S112·S126·S127·S128 digest (all archived):** S112 GO — derive guard từ ENDPOINT authz, self-flip ≠ external-accept ([topic](project_s112_supplier_import_review.md)) · S126 gate — AGGREGATE cumulative KHÔNG vào outward artifact, "ba việc"=2 ASK · S128 — direction-of-error quyết mức lỗi outward + brute-force sha-variant TRƯỚC phán tamper + Get-FileHash ≠ body-sha · S127 — hedge broadcast bằng MỞ config THẬT.
---
diff --git a/.claude/agent-memory/reviewer/project_project_dupe_merge_verify.md b/.claude/agent-memory/reviewer/project_project_dupe_merge_verify.md
new file mode 100644
index 0000000..5685fc7
--- /dev/null
+++ b/.claude/agent-memory/reviewer/project_project_dupe_merge_verify.md
@@ -0,0 +1,25 @@
+---
+name: project-project-dupe-merge-verify
+description: S147 adversarial verify of Project whitespace-dupe merge plan (FLOCK 01/FLOCK01) — FK-in-comment ≠ physical FK; seed-durability forces survivor; INNER-join+filter = silent vanish
+metadata:
+ type: project
+---
+
+# Project whitespace-dupe merge — adversarial verify (2026-07-23)
+
+Investigator plan: merge "FLOCK 01"/"FLOCK01" (+prod FLOCK 03, CAL 01) space-dupes → no-space canonical, retire space-variant, fix dupe-check root cause. Verified all 7 claims + stress i-v. Overall plan SOUND/safe-by-design; 2 terminology corrections; biggest residual = prod ref-distribution unverifiable from dev.
+
+## Reusable lessons (load-bearing)
+- **"FK Projects" in a code COMMENT ≠ physical FK.** `PurchaseEvaluation.cs:15` comment says "FK Projects" but `sys.foreign_keys` scan = ZERO FKs to Projects (68 total FKs in db, none ref Projects). All 3 refs (Contracts/PurchaseEvaluations/PeWorkItemBudgets) are **loose-Guid** (HasIndex only, no HasOne). Claims 1+2 inherited the wrong "FK" word. **Always verify FK reality via `sys.foreign_keys`, not the entity comment.**
+- Loose-Guid (no FK) makes merge repoint SAFER re: ordering (no constraint breaks) but removes ALL DB safety net: orphan ProjectIds already possible, no cascade/restrict, a botched/incomplete repoint is NOT caught by the db.
+- **Blast radius via `sys.columns LIKE '%ProjectId%'` = exactly 3 tables** (authoritative). No denormalized ProjectCode/MaDuAn column anywhere (PE resolves Code via JOIN `db.Projects on e.ProjectId equals p.Id`, DTO projects `x.p.Code`). Only PeWorkItemBudgets has unique index touching ProjectId (filtered `(ProjectId,WorkItemId) WHERE IsDeleted=0`) → sole merge-collision surface.
+- **INNER join + HasQueryFilter(!IsDeleted) = silent vanish.** PE list (`PurchaseEvaluationFeatures.cs:579`) + Contract list (`ContractFeatures.cs:294`) INNER JOIN Projects. Soft-deleting a Project drops every still-referencing row from lists (no error). FORCES repoint-BEFORE-soft-delete + verify 0 live refs (count=0/0/0) before retiring. Detail path (`ContractFeatures.cs:435` FirstOrDefault + `?.Name ?? ""`) tolerates missing → list/detail asymmetry.
+- **Seed-durability FORCES survivor choice (not ref-location).** Ungated `SeedRealMasterDataAsync` (`DbInitializer.cs:128`, outside `if(!demoSeedDisabled)`) emits no-space FLOCK01/FLOCK03/CAL01 every startup; idempotency = exact Code via `db.Projects.Select(Code)` which IS subject to query-filter (no IgnoreQueryFilters). ⇒ soft-deleted no-space canonical is invisible to Select → RE-CREATED (resurrection). Space-variant in NO seed → retire = stays dead. So survivor MUST be no-space **blanket**, even if prod space-variant holds the refs (refs just get repointed; loose-Guid unconstrained). App delete = soft (`AuditingInterceptor.cs:56-61` Deleted→Modified+IsDeleted=true).
+- **MaHopDong irreversibility:** embeds project.Code at gen (`ContractCodeGenerator.cs:32-33`), immutable (`ContractWorkflowService.cs:166` if-null guard; no Update rewrites). Repoint ProjectId does NOT fix embedded space in contract number — accepted limitation.
+- **Root cause:** manual Project Create/Update store Code raw (no trim) + dupe-check EXACT equality (`ProjectFeatures.cs:95,138` `x.Code==request.Code`). No Project import exists. Trim-only INSUFFICIENT (embedded space survives) — needs whitespace-collapse on BOTH paths, ideally + normalized-key unique index (app-check races under concurrency).
+
+## Prod-access constraint (stress v)
+`appsettings.Production.json`: `Server=localhost;...User Id=vrapp;Password=__SET_VIA_SECRETS__`. Prod SQL bound to localhost on VPS (not network-exposed) + secret creds + S134 SSH-crash gotcha ⇒ NO read-only path from dev. Prod ref-distribution (which row holds refs per pair) UNVERIFIABLE pre-execution — the plan's single biggest residual unknown.
+
+## Dev DB snapshot (2026-07-23)
+70 live projects, 0 soft-deleted. ONE whitespace anomaly `[FLOCK 01]` (D680DB3D, 0C/0PE/0budget). `[FLOCK01]` (F1B311EE) = 2C/2PE/0budget. ONE collision group (flock01). CAL01/FLOCK03 exist no-space only; FLOCK 03/CAL 01 space = prod-only.
diff --git a/.claude/auto-memory/feedback_agent_return_garble_recover.md b/.claude/auto-memory/feedback_agent_return_garble_recover.md
index a12c313..cb9f3d3 100644
--- a/.claude/auto-memory/feedback_agent_return_garble_recover.md
+++ b/.claude/auto-memory/feedback_agent_return_garble_recover.md
@@ -5,11 +5,11 @@ metadata:
node_type: memory
type: feedback
originSessionId: 67157d21-1ead-475d-9a50-cf7f07468583
- modified: 2026-07-22T07:02:24.261Z
+ modified: 2026-07-23T10:35:19.103Z
---
Sub-agent đôi khi trả về message CUỐI = text bước memory-curation của nó ("Still 18KB, let me trim...", "Memory updated, splice correct...") THAY VÌ verdict/findings của task (#53 return-channel garble). Nhưng WORK + agent-memory diary VẪN ĐÚNG (nó làm task xong, ghi diary chuẩn, chỉ return nhầm text bước cuối). **Recover: đọc `agent-memory//MEMORY.md` recent-entry lấy verdict thật — KHÔNG re-spawn** (tốn + có thể garble tiếp; hoặc em-main re-do solo cho task cần chắc). Instruction-fix spawn-prompt "🔴 message CUỐI PHẢI là verdict, KHÔNG phải memory-note" GIẢM garble nhưng KHÔNG 100%.
-**Why:** S96 ×3 + **S97 ×3 = ×6 tổng** (cicd-monitor = 5/6 = kẻ tái phạm dai). S96: investigator-codebase 1× + cicd-monitor 2×. **S97: cicd-monitor ×3** (#481 "18.7KB above" + #482 "18.1KB trim S79/S93" + #484 "DATA-SEED-PASS keys merge") — ĐỀU garble text trim-memory **DÙ spawn-prompt explicit "return verdict NOT memory-curation"** → instruction-fix **KHÔNG reliable** (bác claim S96 "lần-3 fixed"); recover verdict từ diary (Run #481 + #482 đủ chi tiết: Mig 60 applied + EF-ternary 200-live + bundle) KHÔNG re-spawn. **🔴 Root-cause structural (mới S97):** cicd-monitor MEMORY.md tự đặt target ~17.1KB, mỗi return CHẠM cap → curate-at-return, và "thinking" bước curate = message CUỐI = return value. Fix bền = **giảm áp lực curate** (archive cicd sớm để return KHÔNG trùng curate-step) HOẶC hook tách verdict-return khỏi memory-step — KHÔNG phải instruction. **Chốt:** recover-from-diary = net ROBUST; instruction "return-verdict" chỉ giảm tần suất (persona memory-curation stochastic — giống AS-10 self-write S95). **+ schema-param KHÔNG chặn return-fail (S97 review-workflow):** 2/4 lane (reviewer+investigator) FAIL StructuredOutput DÙ có `schema` ép-call → return-reliability rộng hơn curate-garble; recover từ diary (reviewer) + em-main tự-cover (grep). → lane QUAN-TRỌNG luôn cần fallback recover-diary/em-main-solo, đừng tin schema chặn được 100%. **+ S103 ×3 = ×9 tổng** (investigator recon + 2 review-lane, DÙ schema+nudge). 🔴 **NEW correlation task-shape (S103):** build-workflow lane NARROW-scoped ("viết script X + return schema") **3/3 CLEAN** vs review-workflow lane OPEN-reasoning ("cố phá + phán-xử") **1/3** → **garble ∝ open-ended-reasoning-task, KHÔNG narrow-build** (cùng schema/nudge/model). Lane-B garble-return NHƯNG full-review Ở DIARY → recover-from-diary hoàn-chỉnh (verdict PASS + 2-CONCERN). → **mitigation = N-lane-redundancy (1 survivor phủ redundant core-concern) + narrow-scoped-schema-lane + em-main-recover-partial** (SE adap-request lên AI_INFRA S103). **+ S105 ×2 = ×11 tổng** (tooling-auditor @bootstrap trả NOTE-DỞ-DANG mid-work + investigator BVAAU trả CÂU MỞ ĐẦU — cả 2 reasoning-lane khớp correlation S103; class RỘNG hơn curate-text: return = wrong-slice-of-transcript bất kỳ, kể cả dòng đầu). 🔴 **NEW recover-path (S105): diary TRỐNG (sub chưa kịp ghi entry cho task đó) → SendMessage-RESUME chính agentId trong-session** (context 150K còn sống) với yêu cầu "nhả verdict theo format, KHÔNG investigate thêm" → verdict đầy đủ chất-lượng-cao lần 2 (investigator BVAAU 3-Q RCA hoàn chỉnh, ~3'). Thứ tự recover: **(1) diary → (2) SendMessage-resume in-session → (3) em-main re-do solo.** **+ S110 ×2 = ×13 tổng** (invest + reviewer — CẢ 2 lane Fable-deep-pass engine-đắt, khớp correlation reasoning-lane; return = 1 câu status giữa-chừng). Cả 2 diary TRỐNG → **SendMessage-resume 2/2 SẠCH → path-2 lifetime 3/3** (S105 + S110×2) — resume re-emit verdict đầy-đủ chất-lượng-cao, 0 investigate lại. Path-2 = default khi diary trống, robust hơn dự đoán. (+monitor-return transcript-LOST khác class: S109 cicd + S110 lượt-sáng 2 monitor — work+diary XONG nhưng return không tới context; disk-diary cứu — WATCH tần suất riêng.) **+ S114 ×5 = ×18 tổng** (review-lane + BE + FE + test + cicd — HMW implement + verify workflow, lead=Opus). 🔴 **NEW class WRITE-lane-garble (S114 quan-trọng):** lane GHI CODE (implementer-backend/frontend/test) garble return → recover KHÔNG từ diary (write-lane không có verdict-diary) mà từ **DISK: `git status`/`git diff` + `dotnet build`/`dotnet test`** ([[feedback_agent_kill_recovery]]). BE-lane 6-file+mig verify-diff-đúng+compile-sạch; FE-lane garble GIỮA mirror → chỉ fe-admin xong (git-status thiếu fe-user) → em-main `cp fe-admin→fe-user` + SHA-verify; test-lane 7-file → `dotnet test` 482 xanh. **Workflow-tool teeth (chốt):** stage-1 `await agent()` TRỰC-TIẾP → 1 garble throw **giết cả workflow** (FE+test stage-2 không chạy); dùng **`parallel()` = thunk-garble→null, workflow SỐNG** → verify-từ-disk bất-kể return. Với WRITE-lane, **return chỉ nice-to-have, DISK = ground-truth**. cicd-lane ×5/6-tổng tái phạm dai → em-main-solo curl-verify (bundle-rotate + byte-verify + smoke). Path recover mở-rộng: **(0) WRITE-lane → disk-truth (git+build+test) → (1) diary → (2) SendMessage-resume → (3) em-main solo.** **+ S118 ×2 = ×20 tổng** (cicd-monitor ×2 — CẢ 2 verify-lane deploy-durability garble return memory-curation ["#388 entry compress prior" + "older stubs archived, tighten longest"], khớp correlation reasoning-lane + cicd tái-phạm dai nhất). 🔴 **VERIFY-lane recover (S118) = READ-lane biến-thể của WRITE-lane disk-truth:** cicd verify-deploy garble → **em-main-solo re-verify từ prod ĐỘC-LẬP** (curl health/401/bundle + `ssh sqlcmd` Serilog/permission-state) → **2 nguồn ĐỘC-LẬP khớp** = verdict chắc. verify-lane garble ≠ mất-việc (verify là READ prod, em tự làm lại được, KHÔNG cần agent). **Serilog log-line = bằng-chứng mạnh nhất** ("Revoked 9"→"Seeded 9 upgraded" 44ms = seeder-thắng-revoke durable, mạnh hơn end-state-DB đơn-thuần). Chốt tổng: WRITE→disk · VERIFY→em-main-re-curl+query · REASONING→diary/resume. **+ S143 ×5 = ×25 tổng** — 🔴 **kỷ-lục 1 phiên: 5/5 sub garble** (reviewer ×2-lần · lead-view · lead-omission · tooling-auditor · harvest-curator), TẤT CẢ đều reasoning-lane, khớp correlation S103. 🔴 **3 data-point MỚI:** **(a) prompt-đúng-từ-đầu KHÔNG chặn được — lần thứ 3 liên tiếp xác nhận:** spawn-prompt của cả 5 vai đã cấm bẫy "trả verdict trước, làm sau" + ép "GHI ĐĨA TRƯỚC, RETURN SAU" NGAY từ lượt đầu ⇒ vẫn 5/5 garble. Thứ **thật sự cứu** = chính câu ép-ghi-đĩa đó: **0 dữ-liệu mất**, mỗi lần garble đều vớt trọn từ `runs//sub-*.md`. Chống #53 là **thiết-kế artifact**, KHÔNG phải thiết-kế prompt. **(b) recover-path THỨ 4 — resume KHÔNG cứu được diary:** `lead-view-auditor` garble ×2, sau resume nó ghi tiếp sub-file nhưng **KHÔNG ghi diary** (mtime đứng nguyên) ⇒ path-2 (SendMessage-resume) **thất bại ở trục diary** lần đầu ⇒ phải **lead APPEND on-behalf sau VERIFY từ sub-file** (B3, tiền-lệ retro-harvest S132); harvest-curator chấm Fidelity ca này **PASS STRONG 0-fabrication** với 2 nit nhãn-nguồn. ⇒ path đầy-đủ nay: **(0) WRITE→disk · (1) diary · (2) resume · (3) lead-on-behalf-từ-sub-file · (4) em-main solo.** **(c) tally tự nó là bẫy:** lead nâng sổ giữa phiên khi mới 3 sub garble (ghi "×23") rồi 2 sub nữa garble ⇒ số sai ngay trong cùng phiên ⇒ **cập-nhật tally ở CUỐI phiên, đừng giữa phiên**; và sổ này từng đứng yên 8 nhãn phiên (×20 từ S118) trong khi class vẫn fire — chính là ca đẻ ra class `gap-incident-unrecorded` (@S143 owner mở enum).
+**Why:** S96 ×3 + **S97 ×3 = ×6 tổng** (cicd-monitor = 5/6 = kẻ tái phạm dai). S96: investigator-codebase 1× + cicd-monitor 2×. **S97: cicd-monitor ×3** (#481 "18.7KB above" + #482 "18.1KB trim S79/S93" + #484 "DATA-SEED-PASS keys merge") — ĐỀU garble text trim-memory **DÙ spawn-prompt explicit "return verdict NOT memory-curation"** → instruction-fix **KHÔNG reliable** (bác claim S96 "lần-3 fixed"); recover verdict từ diary (Run #481 + #482 đủ chi tiết: Mig 60 applied + EF-ternary 200-live + bundle) KHÔNG re-spawn. **🔴 Root-cause structural (mới S97):** cicd-monitor MEMORY.md tự đặt target ~17.1KB, mỗi return CHẠM cap → curate-at-return, và "thinking" bước curate = message CUỐI = return value. Fix bền = **giảm áp lực curate** (archive cicd sớm để return KHÔNG trùng curate-step) HOẶC hook tách verdict-return khỏi memory-step — KHÔNG phải instruction. **Chốt:** recover-from-diary = net ROBUST; instruction "return-verdict" chỉ giảm tần suất (persona memory-curation stochastic — giống AS-10 self-write S95). **+ schema-param KHÔNG chặn return-fail (S97 review-workflow):** 2/4 lane (reviewer+investigator) FAIL StructuredOutput DÙ có `schema` ép-call → return-reliability rộng hơn curate-garble; recover từ diary (reviewer) + em-main tự-cover (grep). → lane QUAN-TRỌNG luôn cần fallback recover-diary/em-main-solo, đừng tin schema chặn được 100%. **+ S103 ×3 = ×9 tổng** (investigator recon + 2 review-lane, DÙ schema+nudge). 🔴 **NEW correlation task-shape (S103):** build-workflow lane NARROW-scoped ("viết script X + return schema") **3/3 CLEAN** vs review-workflow lane OPEN-reasoning ("cố phá + phán-xử") **1/3** → **garble ∝ open-ended-reasoning-task, KHÔNG narrow-build** (cùng schema/nudge/model). Lane-B garble-return NHƯNG full-review Ở DIARY → recover-from-diary hoàn-chỉnh (verdict PASS + 2-CONCERN). → **mitigation = N-lane-redundancy (1 survivor phủ redundant core-concern) + narrow-scoped-schema-lane + em-main-recover-partial** (SE adap-request lên AI_INFRA S103). **+ S105 ×2 = ×11 tổng** (tooling-auditor @bootstrap trả NOTE-DỞ-DANG mid-work + investigator BVAAU trả CÂU MỞ ĐẦU — cả 2 reasoning-lane khớp correlation S103; class RỘNG hơn curate-text: return = wrong-slice-of-transcript bất kỳ, kể cả dòng đầu). 🔴 **NEW recover-path (S105): diary TRỐNG (sub chưa kịp ghi entry cho task đó) → SendMessage-RESUME chính agentId trong-session** (context 150K còn sống) với yêu cầu "nhả verdict theo format, KHÔNG investigate thêm" → verdict đầy đủ chất-lượng-cao lần 2 (investigator BVAAU 3-Q RCA hoàn chỉnh, ~3'). Thứ tự recover: **(1) diary → (2) SendMessage-resume in-session → (3) em-main re-do solo.** **+ S110 ×2 = ×13 tổng** (invest + reviewer — CẢ 2 lane Fable-deep-pass engine-đắt, khớp correlation reasoning-lane; return = 1 câu status giữa-chừng). Cả 2 diary TRỐNG → **SendMessage-resume 2/2 SẠCH → path-2 lifetime 3/3** (S105 + S110×2) — resume re-emit verdict đầy-đủ chất-lượng-cao, 0 investigate lại. Path-2 = default khi diary trống, robust hơn dự đoán. (+monitor-return transcript-LOST khác class: S109 cicd + S110 lượt-sáng 2 monitor — work+diary XONG nhưng return không tới context; disk-diary cứu — WATCH tần suất riêng.) **+ S114 ×5 = ×18 tổng** (review-lane + BE + FE + test + cicd — HMW implement + verify workflow, lead=Opus). 🔴 **NEW class WRITE-lane-garble (S114 quan-trọng):** lane GHI CODE (implementer-backend/frontend/test) garble return → recover KHÔNG từ diary (write-lane không có verdict-diary) mà từ **DISK: `git status`/`git diff` + `dotnet build`/`dotnet test`** ([[feedback_agent_kill_recovery]]). BE-lane 6-file+mig verify-diff-đúng+compile-sạch; FE-lane garble GIỮA mirror → chỉ fe-admin xong (git-status thiếu fe-user) → em-main `cp fe-admin→fe-user` + SHA-verify; test-lane 7-file → `dotnet test` 482 xanh. **Workflow-tool teeth (chốt):** stage-1 `await agent()` TRỰC-TIẾP → 1 garble throw **giết cả workflow** (FE+test stage-2 không chạy); dùng **`parallel()` = thunk-garble→null, workflow SỐNG** → verify-từ-disk bất-kể return. Với WRITE-lane, **return chỉ nice-to-have, DISK = ground-truth**. cicd-lane ×5/6-tổng tái phạm dai → em-main-solo curl-verify (bundle-rotate + byte-verify + smoke). Path recover mở-rộng: **(0) WRITE-lane → disk-truth (git+build+test) → (1) diary → (2) SendMessage-resume → (3) em-main solo.** **+ S118 ×2 = ×20 tổng** (cicd-monitor ×2 — CẢ 2 verify-lane deploy-durability garble return memory-curation ["#388 entry compress prior" + "older stubs archived, tighten longest"], khớp correlation reasoning-lane + cicd tái-phạm dai nhất). 🔴 **VERIFY-lane recover (S118) = READ-lane biến-thể của WRITE-lane disk-truth:** cicd verify-deploy garble → **em-main-solo re-verify từ prod ĐỘC-LẬP** (curl health/401/bundle + `ssh sqlcmd` Serilog/permission-state) → **2 nguồn ĐỘC-LẬP khớp** = verdict chắc. verify-lane garble ≠ mất-việc (verify là READ prod, em tự làm lại được, KHÔNG cần agent). **Serilog log-line = bằng-chứng mạnh nhất** ("Revoked 9"→"Seeded 9 upgraded" 44ms = seeder-thắng-revoke durable, mạnh hơn end-state-DB đơn-thuần). Chốt tổng: WRITE→disk · VERIFY→em-main-re-curl+query · REASONING→diary/resume. **+ S143 ×5 = ×25 tổng** — 🔴 **kỷ-lục 1 phiên: 5/5 sub garble** (reviewer ×2-lần · lead-view · lead-omission · tooling-auditor · harvest-curator), TẤT CẢ đều reasoning-lane, khớp correlation S103. 🔴 **3 data-point MỚI:** **(a) prompt-đúng-từ-đầu KHÔNG chặn được — lần thứ 3 liên tiếp xác nhận:** spawn-prompt của cả 5 vai đã cấm bẫy "trả verdict trước, làm sau" + ép "GHI ĐĨA TRƯỚC, RETURN SAU" NGAY từ lượt đầu ⇒ vẫn 5/5 garble. Thứ **thật sự cứu** = chính câu ép-ghi-đĩa đó: **0 dữ-liệu mất**, mỗi lần garble đều vớt trọn từ `runs//sub-*.md`. Chống #53 là **thiết-kế artifact**, KHÔNG phải thiết-kế prompt. **(b) recover-path THỨ 4 — resume KHÔNG cứu được diary:** `lead-view-auditor` garble ×2, sau resume nó ghi tiếp sub-file nhưng **KHÔNG ghi diary** (mtime đứng nguyên) ⇒ path-2 (SendMessage-resume) **thất bại ở trục diary** lần đầu ⇒ phải **lead APPEND on-behalf sau VERIFY từ sub-file** (B3, tiền-lệ retro-harvest S132); harvest-curator chấm Fidelity ca này **PASS STRONG 0-fabrication** với 2 nit nhãn-nguồn. ⇒ path đầy-đủ nay: **(0) WRITE→disk · (1) diary · (2) resume · (3) lead-on-behalf-từ-sub-file · (4) em-main solo.** **(c) tally tự nó là bẫy:** lead nâng sổ giữa phiên khi mới 3 sub garble (ghi "×23") rồi 2 sub nữa garble ⇒ số sai ngay trong cùng phiên ⇒ **cập-nhật tally ở CUỐI phiên, đừng giữa phiên**; và sổ này từng đứng yên 8 nhãn phiên (×20 từ S118) trong khi class vẫn fire — chính là ca đẻ ra class `gap-incident-unrecorded` (@S143 owner mở enum). **+ S147 ×3 (product session — tally ×25 S143 + S146 ×3 per-HANDOFF chưa gộp riêng ⇒ ~×31):** `investigator-codebase`(①-attachment) + `reviewer`(②-project-merge) + `cicd-monitor` — cả 3 memory-curation-fragment, khớp correlation reasoning/verify-lane. 🔴 **Data-point MỚI:** cả 3 là **Agent-tool spawn LẺ** (không phải workflow-lane) ⇒ **path-2 SendMessage-resume 3/3 SẠCH** (khác S143 nơi resume fail trục diary của workflow-sub `lead-view`). Giả-thuyết: sub Agent-tool đơn giữ context sống + re-emit trọn khi resume, KHÔNG cần path-4 lead-on-behalf. ⇒ với **sub lẻ**, thứ-tự recover rẻ nhất = **(2) resume-in-session TRƯỚC**, chỉ khi resume fail mới (3) em-main-solo. cicd-lane tái-phạm-dai xác nhận lần nữa (verify-lane) — nhưng em-main-curl độc-lập **2-nguồn-khớp** cross-verify vẫn là backstop chắc nhất cho verdict deploy.
**How to apply:** sub return text lạ (memory-curation/off-topic) → ĐỪNG tin nó fail/vô-dụng; đọc diary lấy kết-quả. Task-cần-chắc (bug-reasoning) → em-main re-do solo (reliable). Liên [[feedback_agent_kill_recovery]] · [[feedback_workflow_fanout_reliability]].
diff --git a/.claude/sessions/session-1/closed.md b/.claude/sessions/session-1/closed.md
new file mode 100644
index 0000000..da0e277
--- /dev/null
+++ b/.claude/sessions/session-1/closed.md
@@ -0,0 +1,15 @@
+# closed — phiên-LOGIC L1 (FROZEN, ghi MỘT lần, CẤM sửa)
+
+> `_end` chốt-kết theo floor hub `101e69d6` §1 · SE form = `closed.md` · nghi-thức [`session-end.md §6.3-bis`](../../commands/session-end.md).
+> 🥇 **Lần ghi THẬT ĐẦU TIÊN** — chân này wire @S146 (owner chốt *"giống hub"*) và dùng ngay trong chính phiên wire nó.
+
+**phien-logic:** L1
+**cua-so:** S146 (một cửa-sổ vật-lý; mở bằng `/tiep` nối S145, không qua `/session-start` — `=1` cấp ngoài `/session-start`, đã khai lý-do ở `pause-S146-20260722T2328.md`)
+**closeout-commit:** `8b6df0a` — *"[CLAUDE] Docs: S146 closeout — session-model 3 owner-decision + hook 4-path + H24 auto"* (đã push `a1e2416..8b6df0a`)
+
+**ket-qua:**
+Áp trọn `spec-apply-hub-s145-v2` 5 axis (governance-only, 0 prod-code / 0 mig) sau khi workflow S145 chết giữa chừng → `/tiep §4` relaunch-cắt-gọt 3/3 DONE. Owner chốt **3 câu session-model** (scaffold idempotent · `closed.md` WIRE · V2 auto) + `session_ctx_kb=64` tường-minh. **Hook 4-path live-verify PASS repo thật** (`da7ee8a`). **Cơ-chế session-model chạy trọn vòng đầu tiên**: `pause` → `resume` → `closed` — và chính khối này là mắt cuối khép vòng đó. H24 force-fire (P=8) → **7 FLAG**, vá hết; bắt được ca nặng nhất là **owner-decision `[7]` bị ghi đè do tái-dụng slot-index** (mất uỷ-quyền push, lead hỏi lại 3 lượt).
+
+---
+🔒 **Cân-sổ L1 khép:** `pause=1` · `resume=1` · `closed=1` ⇒ `p ≤ r + c` ⇒ ORPHAN-L **im đúng**.
+🔸 Ghi-chú cho phiên sau: `` kế = **2** (`max+1`). Cấp số vẫn là **độc-quyền `/session-start`**.
diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md
index d09ec6d..1f6e642 100644
--- a/docs/HANDOFF.md
+++ b/docs/HANDOFF.md
@@ -2,7 +2,10 @@
> **Tiering rule (S40):** giữ **2-3 session gần nhất**. Cũ hơn → `docs/changelog/sessions/`. Full brief history pre-S40 → `docs/_archive/HANDOFF-preS40-fullhistory.md`.
-**Last updated:** 2026-07-23 (**S146** — `/tiep` nối S145 → áp `spec-v2` 5 axis (A/C+E/D; B landed S145) → owner chốt **3 câu session-model** (scaffold idempotent · `closed.md` WIRE · V2 **auto**) + `session_ctx_kb=64` tường-minh. **LIVE-VERIFY hook 4-path PASS** (`da7ee8a`). **session-model chạy trọn vòng đầu tiên**: `pause-S146` → `resume-S146` → ORPHAN-L cân sổ. **H24 force-fire** (P=8): 2 vai + `h24-audit`, **cả 3 garble #53** — ghi-đĩa-sớm cứu được, ghi-muộn mất trắng. **7 FLAG** → vá hết + re-verify. Governance-only, 0 prod-code.)
+**🆕 Last updated:** 2026-07-23 (**S147** — product/UAT session, KHÁC chuỗi governance S135–S146. **2 việc anh giao SHIP PROD:** ① vá bug đính-kèm "Bảng so sánh" không sửa được khi phiếu Duyệt NCC bị **Trả lại** — gate nút theo `(isAdmin||isDrafter) && isEditablePhase(phase)` thay `readOnly` màn-hình, cắm `:2013` không `:421`, cả 2 app (reviewer né 3 bẫy: `:421` HoSoLink-bleed · SupplierAttachmentsCell Mig28-F3 carve-out · BE-guard vỡ S78 → descope+no-BE-guard) · ② **gom 3 cặp dự án trùng dấu-cách trên PROD** (`FLOCK 01`/`FLOCK 03`/`CAL 01`→no-space; 5 phiếu+5 NS repoint, 3 ẩn mềm ĐẢO ĐƯỢC, qua `ssh vietreport-vps` integrated-auth `-E` không chạm password) + vá gốc chuẩn-hóa `Code` (Trim-lưu + kiểm-trùng bỏ-dấu-cách, +4 test `ProjectCodeNormalizationTests`). Anh dặn "sau này tối ưu kỹ lại" = first-pass. 2 commit `7d39d8d`+`4510cea`, **CI #407 PASS 5m36s**, bundle rotate ×2 verify 2-nguồn (curl em-main khớp cicd-monitor). **#53 garble ×3** (invest-① + review-② + cicd) resume 3/3 sạch. State: **Mig 67 · 89 bảng · 532 test (45D+487I, +4) · bundle admin `B2j1LQ-v`/user `C3wbJegk`.** → log `2026-07-23-1730-S147-pe-attach-fix-project-dedup.md`.)
+> ↓ **S146 governance + 20 carry MỞ + NEXT items bên dưới GIỮ NGUYÊN** — S147 là product-work, KHÔNG đụng chuỗi governance/session-model ↓
+
+**Last updated (kỳ trước):** 2026-07-23 (**S146** — `/tiep` nối S145 → áp `spec-v2` 5 axis (A/C+E/D; B landed S145) → owner chốt **3 câu session-model** (scaffold idempotent · `closed.md` WIRE · V2 **auto**) + `session_ctx_kb=64` tường-minh. **LIVE-VERIFY hook 4-path PASS** (`da7ee8a`). **session-model chạy trọn vòng đầu tiên**: `pause-S146` → `resume-S146` → ORPHAN-L cân sổ. **H24 force-fire** (P=8): 2 vai + `h24-audit`, **cả 3 garble #53** — ghi-đĩa-sớm cứu được, ghi-muộn mất trắng. **7 FLAG** → vá hết + re-verify. Governance-only, 0 prod-code.)
**🆕 Carry MỚI S146 (6) — 🔴 gộp vào khối dưới ⇒ TỔNG 20, KHÔNG phải 14:** (15) `/snapshot` — lệnh **chụp-nhanh không-dừng** của hub floor (manifest `101e69d6` §1, lệnh thứ-3), SE **thiếu hẳn** `[carry:snapshot-cmd]` · (16) `_context` tầng **STOCK** + nén-thành-con-trỏ (SE mới có tầng *flow*) `[carry:context-stock]` — 🔸 đây là chỗ `session_ctx_kb=64` sẽ có **reader thật**, hết ghost-wire · (17) `/pause` **tự chạy 1 chụp-nhanh + nén** rồi mới dừng `[carry:pause-autosnap]` · (18) `/tiep` đọc **snapshot mới nhất** + phòng **cửa-sổ-sập** `[carry:tiep-window-crash]` · (19) **meld-forward** `.claude/sessions/` qua squash — **CHƯA ĐO** `[carry:meld-forward-unmeasured]` · (20) **bookend đủ 5/5 vòng?** — nay V1+V3 hai-đầu-thật · V2 auto-theo-nhịp · V4 on-signal · V5 liên-tục `[carry:bookend-5-5]`.
> 🔴 **Vì sao phải nằm ở ĐÂY:** H24 `lead-omission-auditor` bắt @S146 — 6 mục này từng **tách đôi** giữa WAL `carry:` (5/6) và session-log (4/6), **không sổ nào đủ**; nguồn đủ chỉ ở `sessions/session-1/pause-S146-*.md` = **artifact theo-cửa-sổ, KHÔNG phải sổ backlog**. `/session-end §6.4` **reset WAL** ⇒ 2 mục sẽ **bốc hơi**. Và cả 6 **chưa có tag `[carry:*]`** nên detector H24-2 chỉ soi được 6/14 — nay đã gắn tag.
diff --git a/docs/STATUS.md b/docs/STATUS.md
index 6ec05d9..701450d 100644
--- a/docs/STATUS.md
+++ b/docs/STATUS.md
@@ -3,6 +3,8 @@
> **Update rule:** trước khi bắt đầu 1 task → ghi row `🔥 In Progress`. Xong → `✅ Recently Done`.
> **Tiering rule (S40):** chỉ giữ **state hiện tại + 3 session gần nhất** ở file này. Session cũ hơn → `docs/changelog/sessions/`. Full history pre-S40 → `docs/_archive/STATUS-preS40-fullhistory.md`. (Tránh over-context — xóa double, không cắt nội dung.)
+**🔥 CURRENT (S147, 2026-07-23 — product/UAT):** Mig 67 · 89 bảng · **532 test** (45D+487I) · gotcha 82 · menu 54 · bundle admin `B2j1LQ-v`/user `C3wbJegk`. **Vừa SHIP (CI #407 PASS):** ① PE cho người-soạn xóa/tải file "Bảng so sánh" khi phiếu **Trả lại** (gate `(isAdmin||isDrafter)&&isEditablePhase` thay `readOnly` màn-hình, cả 2 app; reviewer né `:421` HoSoLink-bleed + SupplierAttachmentsCell Mig28 + BE-guard S78) · ② **gom 3 cặp dự án trùng dấu-cách trên PROD** (`FLOCK 01`/`FLOCK 03`/`CAL 01`→no-space; 5 phiếu+5 NS repoint, 3 ẩn mềm, đảo được, qua `ssh vietreport-vps` integrated-auth) + vá gốc chuẩn-hóa `Code` (Trim + kiểm-trùng bỏ-dấu-cách, +4 test). → log `changelog/sessions/2026-07-23-1730-S147-pe-attach-fix-project-dedup.md` · merge script `changelog/data-fixes/2026-07-23-merge-duplicate-projects.sql`.
+
**Mega-diary lịch-sử (FROZEN — current-state đọc CURRENT STATE table + Recently Done bên dưới, đừng đọc dòng này làm hiện-hành):** (S119 2026-07-15 — **Adap 6 broadcast AI_INFRA: điều-tra chiều-rộng + spec v3 — DỪNG TRƯỚC WAVE (anh chốt B); governance-only 0 prod-code/0 mig/0 FE, test 509 giữ.** Lead **Opus 4.8 owner-choice = flip-9, KHÔNG outage** (MTv3: 0 caveat/0 revert). Pipeline H21 trọn: `/check-email` **6 broadcast mới** (hash 7/7 + tamper 4/4 MATCH; naive-diff ra 56 "NEW" **đa số GIẢ** — H1→H20 đã adopt, chưa copy body = process-gap S101) → `/fable-clone invest` 5-lane `wf_1f6bd5e2-478` **5/5 clean 0-garble** → spec v1 → `/fable-clone reviewer` 4-lane `wf_78a84f9b-03e` **R2 FAIL 4C+8M
R3 FAIL 5C+6M
R1/R4 chết #53** → v2 → `/fable-real reviewer` `wf_cb964f83-331` **GO-WITH-FIXES + 8 fix + 2 honesty-C** → **v3**. **🔴 Phơi-nhiễm ĐO THẬT: 57 `wal:` đã lọt `origin/main`** (hub phát cảnh-báo fleet vì đo được **1** ⇒ **SE = 57×**)
diff --git a/docs/changelog/data-fixes/2026-07-23-merge-duplicate-projects.sql b/docs/changelog/data-fixes/2026-07-23-merge-duplicate-projects.sql
new file mode 100644
index 0000000..2ccab6f
--- /dev/null
+++ b/docs/changelog/data-fixes/2026-07-23-merge-duplicate-projects.sql
@@ -0,0 +1,99 @@
+-- ============================================================================
+-- Merge du an trung do khac DAU-CACH trong Code (giu ban KHONG-dau-cach).
+-- "FLOCK 01" -> "FLOCK01" | "FLOCK 03" -> "FLOCK03" | "CAL 01" -> "CAL01"
+--
+-- AN-TOAN / IDEMPOTENT:
+-- * Survivor = ban no-space (co trong seed => restart-durable). Retire ban space.
+-- * Moi cap chay trong 1 TRANSACTION rieng; VERIFY 0 live-ref TRUOC khi an mem;
+-- lech -> ROLLBACK cap do + in [ABORT], KHONG an mem.
+-- * >1 ban song cung code (reseed cycle) -> [ABORT], can nguoi reconcile 1 GUID.
+-- * Chay lai an-toan: cap da gop -> [SKIP] (khong con ban space song).
+-- * PeWorkItemBudgets co UNIQUE (ProjectId,WorkItemId) filter IsDeleted=0:
+-- repoint neu survivor CHUA co budget live cung WorkItem; nguoc lai (collision)
+-- -> soft-delete ban space (giu survivor). KHONG xoa cung. DeletedBy = NULL.
+-- * Refs la loose-Guid (KHONG co physical FK) -> khong vo thu tu; nhung DB KHONG
+-- co luoi an toan => VERIFY la bat buoc.
+-- Chay: sqlcmd -S -d -b -i merge-duplicate-projects.sql (ASCII -> khong can -f 65001)
+-- ============================================================================
+-- Filtered-unique index tren PeWorkItemBudgets => BAT BUOC QUOTED_IDENTIFIER ON +
+-- ANSI_NULLS ON khi UPDATE. Set o batch rieng (GO) de ap cho connection, tranh
+-- phu thuoc caller nho truyen -I.
+SET QUOTED_IDENTIFIER ON;
+SET ANSI_NULLS ON;
+GO
+SET NOCOUNT ON;
+SET XACT_ABORT ON;
+
+DECLARE @pairs TABLE (spaceCode nvarchar(50), keepCode nvarchar(50));
+INSERT INTO @pairs (spaceCode, keepCode) VALUES
+ (N'FLOCK 01', N'FLOCK01'),
+ (N'FLOCK 03', N'FLOCK03'),
+ (N'CAL 01', N'CAL01');
+
+DECLARE @spaceCode nvarchar(50), @keepCode nvarchar(50);
+DECLARE @spaceId uniqueidentifier, @keepId uniqueidentifier;
+DECLARE @spaceCnt int, @keepCnt int, @ct int, @pe int, @bgMove int, @bgCollide int, @liveRef int;
+
+DECLARE cur CURSOR LOCAL FAST_FORWARD FOR SELECT spaceCode, keepCode FROM @pairs;
+OPEN cur;
+FETCH NEXT FROM cur INTO @spaceCode, @keepCode;
+WHILE @@FETCH_STATUS = 0
+BEGIN
+ SELECT @spaceCnt = COUNT(*), @spaceId = MIN(Id) FROM Projects WHERE Code = @spaceCode AND IsDeleted = 0;
+ SELECT @keepCnt = COUNT(*), @keepId = MIN(Id) FROM Projects WHERE Code = @keepCode AND IsDeleted = 0;
+
+ IF @spaceCnt = 0
+ PRINT '[SKIP] ' + @spaceCode + ' -> ' + @keepCode + ' : khong co ban co-dau-cach song (da gop / khong ton tai).';
+ ELSE IF @spaceCnt > 1
+ PRINT '[ABORT] ' + @spaceCode + ' : >1 ban song cung code — can nguoi xem tay.';
+ ELSE IF @keepCnt = 0
+ PRINT '[ABORT] ' + @spaceCode + ' -> ' + @keepCode + ' : KHONG co ban giu (no-space) song — dung gop.';
+ ELSE IF @keepCnt > 1
+ PRINT '[ABORT] ' + @keepCode + ' : >1 ban giu song (reseed cycle?) — reconcile 1 GUID truoc.';
+ ELSE
+ BEGIN
+ BEGIN TRAN;
+ UPDATE Contracts SET ProjectId = @keepId WHERE ProjectId = @spaceId; SET @ct = @@ROWCOUNT;
+ UPDATE PurchaseEvaluations SET ProjectId = @keepId WHERE ProjectId = @spaceId; SET @pe = @@ROWCOUNT;
+
+ -- Budget: repoint neu survivor chua co live-budget cung WorkItem
+ UPDATE b SET b.ProjectId = @keepId
+ FROM PeWorkItemBudgets b
+ WHERE b.ProjectId = @spaceId AND b.IsDeleted = 0
+ AND NOT EXISTS (SELECT 1 FROM PeWorkItemBudgets s
+ WHERE s.ProjectId = @keepId AND s.WorkItemId = b.WorkItemId AND s.IsDeleted = 0);
+ SET @bgMove = @@ROWCOUNT;
+
+ -- Budget: collision (survivor da co) -> soft-delete ban space
+ UPDATE b SET b.IsDeleted = 1, b.DeletedAt = SYSUTCDATETIME()
+ FROM PeWorkItemBudgets b
+ WHERE b.ProjectId = @spaceId AND b.IsDeleted = 0
+ AND EXISTS (SELECT 1 FROM PeWorkItemBudgets s
+ WHERE s.ProjectId = @keepId AND s.WorkItemId = b.WorkItemId AND s.IsDeleted = 0);
+ SET @bgCollide = @@ROWCOUNT;
+
+ -- Budget da soft-delete: repoint de gon audit (khong dung unique filter IsDeleted=0)
+ UPDATE PeWorkItemBudgets SET ProjectId = @keepId WHERE ProjectId = @spaceId AND IsDeleted = 1;
+
+ SET @liveRef = (SELECT COUNT(*) FROM Contracts WHERE ProjectId = @spaceId)
+ + (SELECT COUNT(*) FROM PurchaseEvaluations WHERE ProjectId = @spaceId)
+ + (SELECT COUNT(*) FROM PeWorkItemBudgets WHERE ProjectId = @spaceId AND IsDeleted = 0);
+
+ IF @liveRef <> 0
+ BEGIN
+ ROLLBACK TRAN;
+ PRINT '[ABORT] ' + @spaceCode + ' : con ' + CAST(@liveRef AS varchar(9)) + ' live-ref sau repoint — ROLLBACK, KHONG an mem.';
+ END
+ ELSE
+ BEGIN
+ UPDATE Projects SET IsDeleted = 1, DeletedAt = SYSUTCDATETIME() WHERE Id = @spaceId;
+ COMMIT TRAN;
+ PRINT '[OK] ' + @spaceCode + ' -> ' + @keepCode + ' : Ct=' + CAST(@ct AS varchar(9))
+ + ' PE=' + CAST(@pe AS varchar(9)) + ' BgMoved=' + CAST(@bgMove AS varchar(9))
+ + ' BgCollideSoftDel=' + CAST(@bgCollide AS varchar(9)) + ' ; da an mem ban space.';
+ END
+ END
+
+ FETCH NEXT FROM cur INTO @spaceCode, @keepCode;
+END
+CLOSE cur; DEALLOCATE cur;
diff --git a/docs/changelog/sessions/2026-07-23-1730-S147-pe-attach-fix-project-dedup.md b/docs/changelog/sessions/2026-07-23-1730-S147-pe-attach-fix-project-dedup.md
new file mode 100644
index 0000000..5464cdb
--- /dev/null
+++ b/docs/changelog/sessions/2026-07-23-1730-S147-pe-attach-fix-project-dedup.md
@@ -0,0 +1,46 @@
+# S147 — PE đính-kèm sau Trả-lại + gom dự án trùng (SHIP PROD)
+
+**2026-07-23** · product/UAT session (khác chuỗi governance S135–S146) · lead Opus 4.8 · `/tiep` (WAL trống, S146 đóng sạch) → 2 việc UAT anh giao.
+
+## Bối cảnh vào phiên
+`/tiep`: WAL chain rỗng nhưng chạy đủ Sàn-3 (5 tín-hiệu MẠNH). Tín-hiệu ⑤ fire (unpushed `wal:` chạm `.claude/sessions/session-1/closed.md`) NHƯNG ORPHAN-L cân sổ (pause=1/resume=1/closed=1) ⇒ L1(S146) đóng sạch, **không có mạch dở**. Rồi anh giao 2 việc + chỉ đạo **"invest + review chạy → sau đó fix"**.
+
+## Việc ① — Bug đính kèm "Bảng so sánh" không sửa được khi phiếu bị Trả lại
+**Triệu chứng (UAT, @Kenny/C Trà):** phiếu Duyệt NCC bị **Trả lại (TraLai=98)** → người soạn KHÔNG xóa file đính kèm sai + KHÔNG tải file mới được.
+
+**Gốc (investigator + reviewer xác nhận):** KHÔNG phải phân quyền. Nút xóa/tải của `GeneralAttachmentsSection` (khối "Bảng so sánh giá", `purpose=ComparisonTable`) gate theo **`readOnly` của MÀN HÌNH**, không theo *trạng-thái + người-soạn*. Màn **"Danh sách"** mở phiếu `readOnly=true` cứng (`PurchaseEvaluationsListPage.tsx`) → ẩn nút ở **MỌI** phase, kể cả TraLai vốn ĐÃ nằm trong `isEditablePhase = {DangSoanThao, TraLai}`. Màn "Thao tác" (workspace) vốn chạy được (đó là cách gỡ tạm). **BE không hề chặn theo phase** — cố ý (S78 cần approver upload `ApprovalAttachment` lúc ChoDuyet).
+
+**Fix (surgical, cả 2 app):** trong `ThongTinChonThauSection` (`PeDetailTabs.tsx`) tính `attachEditable = (isAdmin || isDrafter) && isEditablePhase(ev.phase)`, truyền `readOnly={!attachEditable}` **CHỈ** vào `GeneralAttachmentsSection` (`:2013`). **KHÔNG** đụng `readOnly` cấp `:421` (kẻo mở nhầm `HoSoLinkRow` + winner). Blast-radius = 1 hàm, 0 signature-change.
+
+**3 bẫy reviewer bắt (đã né):** (1) `:421` bleed → HoSoLink; (2) `SupplierAttachmentsCell` gate `readOnly && !approverEditMode` — áp công-thức naive sẽ **vỡ Mig 28 F3** (approver sửa QuoteDocument lúc ChoDuyet) → **descope** (bug chỉ về comparison-doc); (3) BE guard naive vỡ **cả S78 lẫn Mig 28 F3** → **không thêm BE guard**. Test: FE-only, không có .NET test home → verify bằng `tsc -b` + `vite build` ×2 sạch.
+
+## Việc ② — Gom dự án trùng (khác dấu-cách trong mã) + vá gốc
+**Yêu cầu (UAT):** `FLOCK 01`→`FLOCK01`, `FLOCK 03`→`FLOCK03`, `CAL 01`→`CAL01` (cây "Duyệt NCC" hiện trùng do mã lệch 1 dấu cách).
+
+**Gốc:** Project Create/Update lưu `Code` **thô** + kiểm-trùng **so-khớp-đúng-nguyên-văn** (`ProjectFeatures.cs` `x.Code == request.Code`) → "FLOCK 01" ≠ "FLOCK01" đều lọt. Seed `SeedRealMasterDataAsync` chỉ sinh mã **không-dấu-cách** (idempotency key exact-Code, ungated) ⇒ nguồn dấu-cách = nhập tay. Reviewer sửa investigator: refs là **loose-Guid (0 FK vật lý tới Projects)** — trỏ lại an toàn thứ tự nhưng **DB không có lưới an toàn** → phải verify 0-ref trước ẩn.
+
+**Vá gốc (code, anh chốt "chỉ cắt đầu/cuối"):** LƯU `Code = request.Code.Trim()` (giữ mã như gõ), nhưng KIỂM-TRÙNG so trên `Code.Replace(" ","")` (bỏ-dấu-cách) → chặn tạo/sửa thành bản trùng chỉ khác khoảng trắng. **+4 test guard** `ProjectCodeNormalizationTests` (EF dịch `Replace` trên SQLite OK).
+
+**Gom dữ liệu — Dev trước, prod sau:**
+- Script `docs/changelog/data-fixes/2026-07-23-merge-duplicate-projects.sql` — idempotent, mỗi cặp 1 transaction, ABORT nếu >1 bản-giữ sống (reseed cycle), budget UNIQUE-collision → soft-delete, VERIFY 0 live-ref → else ROLLBACK. `SET QUOTED_IDENTIFIER ON; GO` baked-in (filtered-index DML bắt buộc, sqlcmd default OFF).
+- **Dev:** cặp `FLOCK 01` (rỗng) ẩn mềm, idempotent re-run = SKIP.
+- **PROD (qua `ssh vietreport-vps`, integrated auth `-E` → KHÔNG chạm password vrapp):** đo trước (read-only) = cả 3 cặp **live 2 bên, 0 HĐ, 1 bản-giữ/mã, 0 budget-collision** → chạy gộp: **5 phiếu + 5 ngân sách trỏ lại, 0 số bỏ, 3 bản dấu-cách ẩn mềm**. App-visible = 1 live row/mã. Đảo lại được (soft-delete). Verify em-main + kết-quả script **2-nguồn-khớp**.
+
+| Mã giữ | Trước | Sau |
+|---|---|---|
+| CAL01 | 4 phiếu/2 NS | 5 phiếu/3 NS |
+| FLOCK01 | 2 phiếu/2 NS | 4 phiếu/4 NS |
+| FLOCK03 | 1 phiếu/1 NS | 3 phiếu/3 NS |
+
+> ⚠️ Anh dặn: "sau này sẽ chạy tối ưu kỹ lại chỗ này" — đây là gom-sạch first-pass, chưa phải tối ưu cuối.
+
+## Deploy
+2 commit tách việc: `7d39d8d` (① FE 2-app) + `4510cea` (② BE + test). Squash 5 `wal:` local trước push (reset `origin/main`, 0 `wal:` lọt origin). **CI Run #407 SUCCESS 5m36s.** Verify (cicd-monitor + em-main curl **2-nguồn-khớp**): test gate 45D + **487I = 532** (was 483I → **+4**); bundle **fe-admin `DGPE6Nkb`→`B2j1LQ-v`** + **fe-user `DlTwAgJj`→`C3wbJegk`** (CSS frozen = logic-only); smoke api/admin/eoffice + /health = 200; **0 migration** (code-only, prod giữ Mig 67).
+
+## Lessons
+- **#53 return-garble ×3** (investigator ①-attachment + reviewer ②-merge + cicd-monitor) — tất cả memory-curation-fragment, tất cả recover **SendMessage-resume in-session** 3/3 sạch. → [[feedback_agent_return_garble_recover]] (tally ×25→×28).
+- **Prod access:** S134 SSH flaky (reset trên reconnect nhanh) nhưng **one-shot non-interactive OK** + retry-loop; sqlcmd over SSH KHÔNG chết (S134 chỉ interactive); integrated auth `-E` (Administrator) né password; **QUOTED_IDENTIFIER ON bắt buộc cho DML trên bảng có filtered-index** (sqlcmd default OFF ≠ SSMS).
+- **Reviewer-adversarial đáng giá:** self-review sẽ ship 3 regression (bleed + Mig28 + S78). Reviewer ② sửa cả claim "FK" của investigator (thật là loose-Guid).
+
+## State
+**Mig 67 · 89 bảng · 532 test (45D+487I) · gotcha 82 · menu 54 · bundle admin `B2j1LQ-v`/user `C3wbJegk` · prod dự án hết trùng (5 phiếu repoint, 3 ẩn mềm).**