[CLAUDE] Docs: S147 closeout — PE attach-fix (Trả lại) + project-dedup ship-prod

① nút đính kèm 'Bảng so sánh' gate drafter+phase (cả 2 app) · ② gom 3 cặp dự án trùng dấu-cách trên PROD (5 phiếu+5 NS repoint, 3 ẩn mềm) + vá gốc chuẩn hóa Code +4 test. CI #407 PASS, bundle rotate ×2 verify 2-nguồn. Session log + merge script (docs/changelog/data-fixes) + STATUS/HANDOFF + garble tally ×3 + harvest investigator/reviewer/cicd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-23 17:37:59 +07:00
parent 4510cea1e3
commit 5f6c3becfb
13 changed files with 264 additions and 179 deletions

View File

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

View File

@ -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×**)

View File

@ -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 <server> -d <db> -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;

View File

@ -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 S135S146) · 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).**