diff --git a/.claude/WAL.md b/.claude/WAL.md index c640617..990934b 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -11,7 +11,8 @@ chain: - [x] gate 5-anchor W1 PASS 5/5 (mở tươi @S161 — slot 10 trống · :41 const · ∉All · :138-139 lưới · :1779-1786 seed) - [x] wf đợt-1 `wf_7d6fb81b-640`: FE lane CLEAN (Designer type-10, containment 2-lớp, build tươi lead-verify PASS, đã harvest) · BE lane chết #53 sau Domain-layer (8 entity + type-10 + 4/7 config trên đĩa, 0 sub-md) - [x] wf BE `wf_f8c806d2-5a3` chết #53 lần 2 NHƯNG sub-md ghi-từng-mục cứu trọn (Mig 69 apply 2 DB, 96 bảng) → lead solo đuôi {seeder SeedKeHoachKyKetAccessAsync + CodeGen KHKK + DI + 3 site AWV2Admin} → **build 0W/0E 47s**; harvest 2 diary on-behalf xong -- [!] wf: ĐỢT-2 `wf_a95c9909-14b` ĐANG CHẠY — {test-specialist: SchemaTests + CodeGen format A6 · database-agent: design-review Mig 69} — sau đó: lead acceptance (dotnet test ≥564 · Down-script · đo CỜ) → reviewer → commit/push → cicd +- [x] wf ĐỢT-2 `wf_a95c9909-14b`: database-agent **PASS 8/8+DB11 0-blocker** (parity 2 DB, Down script-proof) · test-specialist **4 test + fault-inject 2-chiều** (chết #53 lúc đợi suite — sub-md cứu, lead chạy tươi **566/0**); harvest 2 diary xong. Tally #53 wave này: 3/5 lượt spawn +- [!] reviewer adversarial ĐANG CHẠY nền (trước commit — schema mig; focus revoker-2-chiều seeder + 3-site + FE containment + boundary) → PASS thì: commit file-cụ-thể + push → cicd-monitor 3-chân-kiềng → stage-7 prod-verify {đo CỜ policy + Designer type-10 3-Bước} - [ ] đợt-2 SAU khi wf về: test-specialist + database-agent (song song) → lead acceptance §③-B (build·test·sqlcmd 96·Down sạch·đo CỜ) → reviewer → commit/push → cicd-monitor 3-chân-kiềng - [ ] setup thủ công TRƯỚC W5 (SAU W1 vì type=10 cần mig 69): dựng workflow type=3 + type=10 3-trạm qua Designer (seed chỉ 1 trạm) - [ ] housekeeping @pause/closeout kế: nén MIND-1 5.447B (trần `_mind` 83,2% — MIND-5 chắc vượt) · re-carry 2 ý D rơi (vá-GỐC-Phase-3.5-refresh {mới-nêu} CÒN SỐNG + khuôn-5-đợt→sổ bền {gần-chốt}) · STATUS:6 bổ arc dry-run S160 (mù có lý do — land 15:27 trước pipeline) · hạ nhãn DR/WF-3-trạm đã-định-đoạt · E đếm 7 sub-file (không 6) · mở lại slot-45 trần `_mind` (tiền-đề lật: file SỐNG 83,2%) · quyết bookkeeping OVERDUE:light (cặp H24 chạy @S159 bookend mà `last_audit` đứng 27) diff --git a/.claude/agent-memory/database-agent/MEMORY.md b/.claude/agent-memory/database-agent/MEMORY.md index 735ebbc..40e3183 100644 --- a/.claude/agent-memory/database-agent/MEMORY.md +++ b/.claude/agent-memory/database-agent/MEMORY.md @@ -1,5 +1,7 @@ # database-agent — MEMORY (L1 HOT) +- **S161 (07-29) W1 KHKK design-review Mig 69 `[hmw wf_a95c9909-14b · return CLEAN + sub-md 21KB]`:** **PASS 8/8 + DB11, 0 blocker** — Mig `20260729122015_AddContractSigningPlans` 7 CreateTable/20 CreateIndex/0 ALTER. Phép đo đáng giữ làm khuôn review sau: (1) **Down-proof KHÔNG destructive**: `dotnet ef migrations script --no-build` sinh script Down atomic (1 transaction) — thay cho apply-rollback thật trên DB đang dùng (DB2); (2) **đo `sys.foreign_keys`/`sys.indexes.filter_definition`/`sys.columns.max_length` thay đọc code** — nvarchar max_length = BYTE = 2× ký tự; (3) **multiple-cascade-path né bằng loose-Guid+HasIndex** (Attachments.DossierItemId — FK Cascade ở đó = err 1785 từ chối CREATE); (4) **snapshot-drift check rẻ**: diff Designer-vs-Snapshot bỏ blank/comment → chỉ được phép lệch header (Migration attr/BuildTargetModel); (5) **twin-parity làm chuẩn thiếu-index**: 4 cột không IX đều parity PE (cột hiển thị không vào WHERE site nào đã wire); LevelOpinions unique KHÔNG filter = ĐÚNG (đo cả 2 twin PE/Contract cùng dạng — UPSERT 1-row-per-level); (6) IX_SlaDeadline VẮNG là ĐÚNG (SlaExpiryJob chỉ quét Contracts; entity tự khai SLA=display-only) — index không job quét = index chết; (7) parity 2 DB LocalDB: TABLECOUNT 96 ×2 + MIGTOP ×2 + IXCOUNT 27 (=20 IX + 7 PK) khớp 1:1 file. Rủi ro khai: SERIALIZABLE read-modify-write có thể deadlock 1205 fail-safe (parity 4 module Office prod). Tag `[s161, mig69-review, down-script-proof-no-destructive, sys-catalog-over-code, twin-parity-index-baseline]` + > READ-advisory DB specialist SOLUTION_ERP (.NET 10 EF Core 10 + SQL Server, single `ApplicationDbContext` dbo). Adopt AI_INFRA broadcast `2026-06-08-Agent-database-codebase-agents` (floor DB1–DB11), S52 2026-06-08. Seed = em main. **Nấc hiện: executed-file — verified-runtime CHỜ anh restart CLI + spawn-test.** ## Vai trò (FORM tailored SE) diff --git a/.claude/agent-memory/test-specialist/MEMORY.md b/.claude/agent-memory/test-specialist/MEMORY.md index f3d7c72..ebd8286 100644 --- a/.claude/agent-memory/test-specialist/MEMORY.md +++ b/.claude/agent-memory/test-specialist/MEMORY.md @@ -1,5 +1,7 @@ # Test-Specialist Agent — Persistent Memory +- **S161 (07-29) W1 KHKK: 4 test schema+codegen `[hmw wf_a95c9909-14b · chết #53 LÚC ĐỢI full-suite (việc thật đã xong) → sub-md 10KB ghi-từng-mục cứu trọn; lead chạy full-suite tươi 566/0 + điền §2/§4 on-behalf]`:** File MỚI `Common/ContractSigningPlanSchemaTests.cs` — 4 test: (1) PIN-spec `BayBang_MoiBang_CoAuditFields_VaQueryFilter` (quét EF model theo NAMESPACE → `BeEquivalentTo` đúng bộ 7 tên [bảng #8 lọt = ĐỎ] + audit-fields + soft-delete assert 2-CHIỀU [BaseEntity phải KHÔNG có IsDeleted] + seed IsDeleted=true chứng filter là BIỂU-THỨC-THẬT); (2) PIN `SigningPlanCodeGenerator_Format_KhkkYearSeqD3` — regex + tuyệt-đối 001→002 + row LastSeq==2 + **đẩy đồng hồ 2027 → KHKK/2027/001 reset** (thiếu vế này `\d{4}` xanh cả khi năm hardcode); (3) #57 xoá-mềm-thêm-lại KHÔNG nổ UNIQUE + vế đối chứng 2-dòng-sống PHẢI DbUpdateException (không có = xanh cả khi index chưa từng tạo); (4) pin slot 10 enum. **Fault-inject 2 chiều trên bảng-kỳ-vọng rồi revert** — chứng `GetDeclaredQueryFilters()` phân biệt 2 thế giới. Learned: (1) **prompt lead ≠ đĩa** — LevelOpinion là AuditableEntity nhưng CỐ Ý KHÔNG filter (lý-do ghi 2 chỗ: site orphan-purge phải thấy row soft-deleted) ⇒ test theo CODE + assert cái-cố-ý thành invariant có răng; (2) EF Core 10: `GetQueryFilter()` obsolete-prone → `(IReadOnlyEntityType).GetDeclaredQueryFilters()`; (3) Serializable trên SQLite = graceful-map, tiền lệ `SqliteDbFixture.cs:36-42` + PE codegen test — KHÔNG chế fixture mới; (4) KHÔNG `Remove()` đo xoá mềm (TestCtx không wire AuditingInterceptor → xoá CỨNG, bài S155). Tag `[s161, w1-4-test, faultinject-2-chieu, test-theo-code-khong-theo-prompt, clock-2027-reset, 53-luc-doi-suite]` + - **S155 (07-27) ĐỢT 2 PE xóa-khi-đang-duyệt + màn "Đã xóa" [run 2026-07-27-S155-pe-delete-approver · sub-d2-test-4, diary 13.9KB ghi-trong-lúc-làm]:** +10 `PeDeleteByApproverTests.cs` (T21 lũy kế **4 HƯỚNG KHÁC NHAU** · T22/T22b/T22c màn Đã xóa · T23/T24/T24b cờ+lượt · T25 authz reflection · T25b chiều thuận · T26 changelog). Full **561 = 560 PASS / 1 FAIL** (45D + 516I); 0 hồi quy (Infra cũ 506 nguyên vẹn). **1 FAIL = T26 GIỮ ĐỎ = BUG PROD**: `PeSoftDeleteFeatures.cs` Add changelog (:126) → `Remove(pe)` (:142) → `CascadeDeleteTiming.Immediate` cascade **ngay tại Remove**; dependent đang `Added` bị cascade ⇒ **Detached** (không phải Deleted) ⇒ 0 row; `AuditingInterceptor` chạy ở SavingChanges = SAU điểm mất ⇒ không cứu. Đo bằng ChangeTracker diag (`Added→Detached`, `rowsInDb=0`), KHÔNG suy đoán. Grep cùng-lớp: site DUY NHẤT có hình "Add con → Remove cha → 1 SaveChanges". REPORT lead, KHÔNG tự sửa. **Hạ tầng MỚI:** `SoftDeleteFixture` (ngay trong file test, ~90 LOC) = fixture ĐẦU TIÊN của repo wire `AuditingInterceptor` — BẮT BUỘC khi đo `Remove()`=xóa-mềm, vì `TestApplicationDbContext` KHÔNG wire ⇒ mặc định **XÓA CỨNG** ⇒ màn "Đã xóa" rỗng ⇒ assert đúng ở CẢ HAI thế giới (đúng bẫy vacuous). Số học T21 nhớ kỹ: xóa phiếu ChoDuyet ⇒ PrevSubmitted↓ · PendingSelected↓ · **PrevSelected + PendingSubmitted BẤT ĐỘNG** (seed thêm peer DaDuyet + TraLai để 2 số "không đổi" ≠ 0 ở cả 2 phía). Tag `[s155, pe-delete-by-approver, cascade-detach-added-child, softdelete-fixture, 4-huong]` - **S153 (07-26) T4 ctx-adopt FAULT-INJECT máy `mind-check` #8-#17+#21 [run 2026-07-26-S153-ctx-adopt · lane trước đứt sau #7, file đĩa nguyên → tiếp được] `[engine: opus-5]`:** **18/18 PASS 0 FAIL**. KHÔNG phải test C# (**baseline 509 KHÔNG đổi**) — đối tượng = `scripts/session_ctx.py mind-check`, mọi mutation trong cây tạm qua seam `--root`, repo read-only. Bắt **1 GAP THẬT**: ở closed-mode **XOÁ con-trỏ → cờ/exit 0** còn **GIỮ con-trỏ đang-chờ trung thực → TRƯỢT/exit 1** ⇒ đường-rẻ-nhất-để-xanh là đường SAI (chữ luật cấm, gradient thưởng) → REPORT em main, KHÔNG tự sửa. **Bài học mới quan trọng nhất:** fixture `draft-ts` MALFORMED ra exit 0 — `inject_A` neo `## MIND-1` sau khi `renum(...,2)` đã đổi heading ⇒ `str.find`=-1 ⇒ chèn GIỮA heading ⇒ `ts:` không ở đầu dòng ⇒ máy ĐÚNG mà vẫn xanh. **Fixture-hỏng cho-xanh trông Y HỆT máy-đúng cho-xanh** ⇒ phải soi RUỘT fixture (`cat -A`/`repr` từng dòng), KHÔNG chỉ đọc exit code. 2 kỹ thuật xài lại: (i) **cặp CÙNG-RUỘT khác 1 biến** — cap=32 vs cap=1 trên cùng tệp = chứng đọc-config-LIVE; `_end` giả `_pause=9` vs không = chứng nguồn-đếm-là-ĐĨA, dùng `diff` 2 output để chứng dòng đo BYTE-IDENTICAL; (ii) **đo wall-clock phải gồm nhánh ĐẮT** — lệnh literal đi nhánh early-exit (repo chưa có `_mind`) = tự đo nhánh rẻ rồi gọi là chi phí thật; đo 55-64ms/lượt vs ước 140-300ms ⇒ lệch 2.2-5.4×, giao lead điền lại. Tag `[s153, t4-fault-inject, mind-check, fixture-bug-vacuous-pass, cung-ruot-1-bien]` diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/run.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/run.md index 851dae5..39c5bc5 100644 --- a/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/run.md +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/run.md @@ -11,10 +11,10 @@ |---|---|---|---| | 1 | Đợt-1a BE: Domain 7 entity + 3 enum + ApplicableType=10 + EF Config ×7 + DbSets + **Mig 69** 3-file (apply 2 DB) + MenuKeys All+= + DbInitializer UPGRADE-if-exists (13/13 role) + CodeGen KHKK + 3 site AWV2AdminFeatures | hmw `implementer-backend` | [x] 3 chặng: lane-1 `wf_7d6fb81b-640` chết #53 sau Domain (8 entity + type-10 + 4/7 config) → lane-2 `wf_f8c806d2-5a3` cũng chết #53 NHƯNG sub-md 6.2KB ghi-trong-lúc-làm cứu trọn (audit 0-lệch + 3 config + DbSets + MenuKeys:179 + **Mig 69 `20260729122015` 3-file, apply 2 DB, sys.tables=96**, grep-proof trong sub-md) → **lead solo đuôi @S161**: seeder `SeedKeHoachKyKetAccessAsync` UPGRADE-if-exists+CanDelete (call sau revoke) + `ContractSigningPlanCodeGenerator` KHKK/{YYYY}/{Seq:D3} + DI + 3 site AWV2Admin (count/retained/orphan mirror VehicleBooking). **`dotnet build` 0W/0E 47s** | | 2 | Đợt-1b FE: Designer type-10 `ApprovalWorkflowsV2Page.tsx` (TYPE_CODE_TO_INT + option + ẩn 2 checkbox early-finalize khi type=10) | hmw `implementer-frontend` | [x] `wf_7d6fb81b-640` lane-2 CLEAN — +67/−9, containment 2-LỚP render+payload; **lead verify build tươi PASS** (bundle `index-iNZq9job`; diagnostics giữa-agent = stale #68); harvest diary xong. Cờ đỏ: type-10 chưa có đường sidebar (allowlist `Layout.tsx:162-168` — cả type-3 xưa nay cũng vậy, đúng spec "menu KHÔNG đổi") | -| 3 | Đợt-2a: schema test + codegen format test (`ContractSigningPlanSchemaTests` + `SigningPlanCodeGenerator_Format_KhkkYearSeqD3`, N ≥ 2) | hmw `test-specialist` | [ ] | -| 4 | Đợt-2b: design-review Mig 69 TRƯỚC deploy (reversible Down · snapshot drift · FK/index) | hmw `database-agent` | [ ] | -| 5 | Lead: build ×2 + `dotnet test` + sqlcmd 89→96 + Down-script sạch + acceptance §③-B sweep | lead | [ ] | -| 6 | Reviewer adversarial diff (schema mig >50 LOC — bắt buộc trước commit) | `reviewer` | [ ] | +| 3 | Đợt-2a: schema test + codegen format test (`ContractSigningPlanSchemaTests` + `SigningPlanCodeGenerator_Format_KhkkYearSeqD3`, N ≥ 2) | hmw `test-specialist` | [x] `wf_a95c9909-14b` — **4 test** (vượt sàn 2) + fault-inject 2 chiều chứng răng + bắt LỆCH prompt-vs-đĩa (LevelOpinion CỐ Ý không filter → assert thành invariant); vai chết #53 lúc đợi full-suite → lead chạy tươi + điền §2/§4 on-behalf | +| 4 | Đợt-2b: design-review Mig 69 TRƯỚC deploy (reversible Down · snapshot drift · FK/index) | hmw `database-agent` | [x] cùng wf — **PASS 8/8 + DB11, 0 blocker** (đo sys.foreign_keys/filter_definition; parity 2 DB tuyệt đối; Down script atomic sạch; IX superset khuôn PE; 4 advisory wave-sau) — `sub-database-agent-1.md` 21KB | +| 5 | Lead: build ×2 + `dotnet test` + sqlcmd 89→96 + Down-script sạch + acceptance §③-B sweep | lead | [x] build BE 0W/0E 47s + FE build tươi PASS · test **566/0** (562+4 ≥ sàn 564) · sys.tables 96 ×2 DB · Down script-proof · CÒN 2 vế prod-side @stage-7: đo-CỜ policy sau seed prod + Designer tạo type-10 3-Bước | +| 6 | Reviewer adversarial diff (schema mig >50 LOC — bắt buộc trước commit) | `reviewer` | [!] ĐANG CHẠY nền — focus: revoker-2-chiều seeder · 3-site mirror · FE containment · boundary 0-diff ContractCodeGenerator/CWS | | 7 | Commit + push + cicd-monitor 3-chân-kiềng (CI PASS · mig 69 prod · menu 142 giữ) | lead + `cicd-monitor` | [ ] | ## Ràng buộc mang theo diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-database-agent-1.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-database-agent-1.md index c07c1f9..4764873 100644 --- a/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-database-agent-1.md +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-database-agent-1.md @@ -3,7 +3,8 @@ > Ghi TỪNG MỤC trong lúc làm (chống #53). Không sửa file production. Không commit. > Nguồn đo: file migration + Designer + ModelSnapshot + 7 EF Configuration + sqlcmd LocalDB Dev/Design. -## VERDICT TỔNG (điền cuối, xem §END) +## VERDICT TỔNG: **PASS 8/8 mục (a)–(h) + sqlcmd + DB11 — ĐỦ ĐIỀU KIỆN DEPLOY, 0 blocker.** +4 advisory cho wave sau (A-1 chặn 2 kế hoạch/1 PE · A-2 dọn Attachments khi xoá DossierItem · A-3 IX_SlaDeadline chỉ khi wire job · A-4 backup trước khi chạy Down trên DB có data). Chi tiết §END. --- @@ -73,3 +74,166 @@ ContractSigningPlans → ApprovalWorkflows NO_ACTION (= Restrict) → 6 con Cascade 1-hop + 2 Restrict, **đúng y spec §②-2/4**. Zero FK vật lý sang PE/Project/Supplier/User/Contract (loose-Guid, convention Mig 49) — xác nhận bằng chính danh sách 8 FK trên (không có dòng nào trỏ `PurchaseEvaluations`/`Suppliers`/`Projects`). --- + +## 2. Chấm điểm từng mục (a)–(h) + +### (a) Down reversible sạch — **PASS** +`:348-370` — 7 `DropTable`, thứ tự: Approvals · Attachments · Changelogs · DossierItems · LevelOpinions · Lines · **ContractSigningPlans (cuối)**. +- 6 bảng con (mọi bảng có FK Cascade trỏ Plans) drop TRƯỚC cha ⇒ 0 vi phạm FK-order. +- `Attachments → DossierItems` **không có FK vật lý** (loose, chỉ HasIndex `:252-254`) ⇒ không tồn tại ràng buộc thứ tự giữa 2 bảng này; thứ tự alphabet hiện tại vẫn hợp lệ. +- `LevelOpinions → ApprovalWorkflowLevels` (Restrict) và `Plans → ApprovalWorkflows` (Restrict): bảng cha nằm NGOÀI mig ⇒ drop bảng con không đụng cha. +- Up chỉ CreateTable/CreateIndex ⇒ Down = đảo hoàn toàn, **không có state nào sót lại**. Không cần `DropIndex` riêng (DropTable cuốn index theo). +- ⚠️ DB2: Down là destructive-by-nature (mất data 7 bảng). Với dry-run GĐ2 chưa có data prod thì rủi ro = 0; **nếu chạy Down sau khi prod đã có phiếu ⇒ bắt buộc backup trước.** + +### (b) 0 ALTER bảng cũ — **PASS** +Đếm operation trên toàn file: `AddColumn`/`AlterColumn`/`DropColumn`/`RenameColumn`/`RenameTable`/`Sql(` = **0 match**. Chỉ 7 CreateTable + 20 CreateIndex (Up) + 7 DropTable (Down). +→ Không đụng 89 bảng cũ ⇒ deploy prod = **additive thuần**, không khoá bảng đang chạy, rollback rẻ. +→ `ApprovalWorkflowApplicableType += ContractSigningPlan = 10` là đổi ENUM C# trên cột `int` sẵn có ⇒ **đúng là không sinh migration** (xác nhận: mig không có AlterColumn nào trên `ApprovalWorkflows`). + +### (c) FK strategy — **PASS** +Khớp 100% ý đồ spec (đo từ `sys.foreign_keys`, không đọc code): +- 6 con → Plan: **Cascade 1-hop** ✓ +- Plan → `ApprovalWorkflows`: **Restrict** (`:42-47`, config `ContractSigningPlanConfiguration.cs:37-40`) ✓ — không cho xoá quy trình khi còn phiếu pin. +- LevelOpinions → `ApprovalWorkflowLevels`: **Restrict** (`:196-201`) ✓ — bảo vệ chữ ký. +- **0 multiple-cascade-path**: điểm nguy hiểm duy nhất là `Attachments` (vừa con của Plan vừa trỏ DossierItem). Đã né đúng cách bằng loose-Guid + HasIndex (`ContractSigningPlanAttachmentConfiguration.cs:11-15,31`). Nếu đặt FK Cascade ở đó thì `Plan→DossierItem→Attachment` + `Plan→Attachment` = 2 đường cascade ⇒ **SQL Server từ chối CREATE TABLE ngay** (lỗi 1785). Chứng cứ ngược: bảng tạo được trên cả 2 DB ⇒ không có multiple path. +- ⚠️ **Hệ quả cần W2/W3 biết (advisory, không phải lỗi schema):** xoá 1 `DossierItem` **KHÔNG** tự dọn `Attachments` trỏ nó → `ContractSigningPlanDossierItemId` thành con trỏ mồ côi. Đây đúng khuôn PE, nhưng PE có handler dọn tay. **W2 phải tự set null / xoá mềm attachment con khi xoá DossierItem**, nếu không màn xem hồ sơ sẽ hiện file gắn vào mục đã biến mất. + +### (f) maxLength khớp spec `:134-137` — **PASS (11/11, +5 cột ngoài danh sách cũng đã khai)** +Đo từ `sys.columns` (⚠️ `max_length` của nvarchar là **BYTE** = 2× số ký tự): + +| Cột (bảng) | spec yêu cầu | DB thật (byte) | = ký tự | verdict | +|---|---|---|---|---| +| MaKeHoach (Plans) | 50 | 100 | 50 | ✓ | +| GhiChu (Plans) | 2000 | 4000 | 2000 | ✓ | +| Name (DossierItems) | 500 | 1000 | 500 | ✓ NOT NULL | +| Note (DossierItems/Lines/Attachments) | 1000 | 2000 | 1000 | ✓ ×3 | +| TvgsName (DossierItems) | 200 | 400 | 200 | ✓ | +| Summary (Changelogs) | 1000 | 2000 | 1000 | ✓ | +| ContextNote (Changelogs) | 2000 | 4000 | 2000 | ✓ | +| SignedByFullName (LevelOpinions) | 200 **IsRequired** | 400, is_nullable=**0** | 200 | ✓ | +| FileName (Attachments) | 500 | 1000 | 500 | ✓ NOT NULL | +| StoragePath (Attachments) | 1000 | 2000 | 1000 | ✓ NOT NULL | +| ContentType (Attachments) | 200 | 400 | 200 | ✓ NOT NULL | + ++5 cột chuỗi ngoài danh sách spec, **đều đã khai độ dài** (không rơi vào bẫy nvarchar(max)): +`HoSoLink` 1000 (mirror PE Mig 52) · `Comment` (Approvals) 1000 · `Comment` (LevelOpinions) 2000 · `UserName` (Changelogs) 200 · `FieldChangesJson` = **nvarchar(-1)=MAX, CỐ Ý** (JSON tự do, không index — `ContractSigningPlanChangelogConfiguration.cs:30`). + +→ **Chỉ đúng 1 cột nvarchar(max) trong cả 7 bảng, và nó là cột được chỉ định.** Không có cột chuỗi nào "quên khai" (rủi ro lens-schema C3 = đã chặn). + +**Tiền tệ:** `PeReferenceAmount`/`ProposedAmount`/`ApprovedAmount` = `decimal(18,2)` đo từ `sys.columns.precision/scale` = **18/2** — khớp khuôn `PurchaseEvaluationQuotes.BgVat/ChuaVat/ThanhTien` (18/2). Không có cột tiền nào rơi về `decimal(18,0)` (bẫy mất phần lẻ). + +### (d) Index đủ cho query list/inbox tương lai — **PASS** +**Trục 1 — mọi bảng con seek được theo PlanId (chống scan lúc mở chi tiết phiếu):** 6/6 con có index **dẫn đầu bằng `ContractSigningPlanId`**: +`Lines(PlanId,SupplierId)` unique-filtered · `DossierItems(PlanId)` · `Attachments(PlanId)` · `Changelogs(PlanId,CreatedAt)` + `(PlanId,EntityType)` · `Approvals(PlanId,ApprovedAt)` · `LevelOpinions(PlanId,LevelId)` unique. +→ Load 1 phiếu = 6 seek, **0 table scan**. Đây là cái quan trọng nhất và nó đủ. + +**Trục 2 — list (W2):** `IX_ContractSigningPlans_Phase_IsDeleted` (2 equality) + `IX_ProjectId` + `IX_DrafterUserId` + `IX_PurchaseEvaluationId`. Query "phiếu của tôi" / "theo dự án" / "PE này đã có kế hoạch chưa" (spec cũ `:157` — tra ngược bằng `Plans.Where(PurchaseEvaluationId==x)`, KHÔNG thêm cột lên PE) đều có index đích. ✓ + +**Trục 3 — inbox (W3):** `WHERE Phase=ChoDuyet AND IsDeleted=0` → seek `IX_Phase_IsDeleted`; rồi lọc theo `ApprovalWorkflowId` (có IX) + 2 con-trỏ StepIndex/LevelOrder nằm sẵn trong row. ✓ + +**Trục 4 — 3 site admin xoá Level:** cả 3 đều lọc `levelIds.Contains(o.ApprovalWorkflowLevelId)` → `IX_ContractSigningPlanLevelOpinions_ApprovalWorkflowLevelId` phục vụ đúng (`ApprovalWorkflowV2AdminFeatures.cs:945` count · `:1017` retained · `:1083` purge). ✓ **Đã wire đủ 3 site** (grep xác nhận 3 vị trí, khớp yêu cầu spec §②-10). + +**Sai khác duy nhất so twin — `IX_SlaDeadline` KHÔNG có** (cả `Contracts` lẫn `PurchaseEvaluations` đều có). **Không phải thiếu sót:** `SlaExpiryJob` chỉ quét `db.Contracts` (`SlaExpiryJob.cs:76` + `:138`, 0 tham chiếu tới bảng khác), và entity khai tường minh `ContractSigningPlan.cs:34-36` "SLA = FE hiển thị tham khảo, KHÔNG đăng ký SlaExpiryJob". Index không job nào quét = index chết. **Bỏ là ĐÚNG.** +🔸 *Điều kiện lật:* wave sau nếu wire KHKK vào `SlaExpiryJob` ⇒ phải thêm `IX_ContractSigningPlans_SlaDeadline` (1 mig nhỏ) trước khi bật job, không thì job quét full-table mỗi chu kỳ. + +### (e) UNIQUE filtered gotcha #57 — **PASS** +Đo từ `sys.indexes.filter_definition` (không đọc code): +- `IX_ContractSigningPlanLines_ContractSigningPlanId_SupplierId` — `is_unique=1`, filter **`([IsDeleted]=(0))`** ✓ **ĐÚNG gotcha #57**. Xoá mềm 1 NCC rồi thêm lại NCC đó = được (không đụng unique). +- `IX_ContractSigningPlans_MaKeHoach` — `is_unique=1`, filter `([MaKeHoach] IS NOT NULL)` ✓ khuôn `Contracts.MaHopDong` / `PurchaseEvaluations.MaPhieu` (đo được cả 2 đều cùng dạng filter). Nháp chưa gen mã (`MaKeHoach=null`) không đụng nhau. +- `IX_ContractSigningPlanLevelOpinions_(PlanId,LevelId)` — `is_unique=1`, **KHÔNG filter**. 👉 **ĐÚNG, không phải sót:** đối chứng 2 twin đo trực tiếp trong DB — `IX_PurchaseEvaluationLevelOpinions_(PeId,LevelId)` unique KHÔNG filter · `IX_ContractLevelOpinions_(ContractId,LevelId)` unique KHÔNG filter. Họ `*LevelOpinion` là **UPSERT 1-row-per-level**, không có luồng xoá-mềm-rồi-ký-lại ⇒ thêm filter sẽ lệch khuôn 7 twin và mở cửa cho 2 row cùng (Plan,Level). + +### (g) loose-Guid nào cần IX mà thiếu — **PASS (superset của khuôn PE)** +| loose-Guid | IX? | đối chứng khuôn PE | +|---|---|---| +| `Plans.PurchaseEvaluationId` | ✓ | (PE không có twin — mới) | +| `Plans.ProjectId` | ✓ | PE có `IX_ProjectId` | +| `Plans.DepartmentId` | ✓ | **PE KHÔNG có** → CSP chặt hơn | +| `Plans.DrafterUserId` | ✓ | PE không có twin | +| `Lines.SupplierId` / `Lines.ContractId` | ✓ / ✓ | PE có `IX_ContractId` | +| `DossierItems.SupplierId` | ✓ | PE `Attachments.PeSupplierId` | +| `Attachments.DossierItemId` | ✓ | (loose cố ý, né multiple-cascade) | +| `Approvals.ApprovalWorkflowLevelId` | ✓ | **PE Approvals KHÔNG có** → CSP chặt hơn | +| `Approvals.ApprovedByUserId` | ✗ | PE Approvals cũng ✗ — **parity**, cột hiển thị không phải predicate | +| `Changelogs.UserId` / `EntityId` | ✗ / ✗ | PE Changelogs cũng ✗ — **parity** | +| `LevelOpinions.SignedByUserId` | ✗ | PE/Contract LevelOpinions cũng ✗ — **parity** | + +→ **0 loose-Guid thiếu index mà có người truy vấn.** 4 cột không index đều trùng khớp twin PE (chỉ đọc ra để hiển thị, không nằm trong WHERE của bất kỳ site nào đã wire). + +### (h) Snapshot drift — **PASS (0 drift)** +1. **Designer vs ModelSnapshot** (diff sau khi bỏ dòng trống/comment): **15 dòng khác, TẤT CẢ là header** — `using ...Migrations` · `[Migration("20260729122015_...")]` · `partial class AddContractSigningPlans` vs `ApplicationDbContextModelSnapshot : ModelSnapshot` · `BuildTargetModel` vs `BuildModel`. **0 dòng khác biệt về model.** (Designer 4.973 dòng / Snapshot 4.971 dòng — lệch đúng 2 dòng header.) +2. **git diff ModelSnapshot = `1 file changed, 550 insertions(+)`, 0 deletions** — 3 hunk đều dạng `-0` (thuần chèn): `@@ -314,0 +315,453 @@` (7 entity) · `@@ -5685,0 +6139,82 @@` (quan hệ) · `@@ -6277,0 +6813,15 @@` (navigation). 👉 **Không dòng nào của 89 bảng cũ bị sửa** ⇒ chứng minh ở tầng MODEL rằng mig này additive thuần (khớp mục (b) ở tầng SQL). +3. **3-file rule đủ:** `20260729122015_AddContractSigningPlans.cs` (22.059 B) + `.Designer.cs` (264.515 B) + `ApplicationDbContextModelSnapshot.cs` (264.399 B) — cả 3 đều xuất hiện trong `git status` (2 file đầu untracked-mới, snapshot modified). ✓ skill `ef-core-migration`. +4. Tên file dùng timestamp EF chuẩn `20260729122015_`, **không có tiền tố `69_`** ⇒ khớp acceptance §③-B dòng 1. `__EFMigrationsHistory` COUNT = **69** ⇒ đây đúng là mig thứ 69. + +--- + +## 4. Kiểm tra DB11 (concurrency) — floor bắt buộc của vai này + +**`ContractSigningPlanCodeGenerator.cs` — PASS.** +- `BeginTransactionAsync(IsolationLevel.Serializable, ct)` (`:23-24`) bọc read-modify-write của sequence ✓ (đúng pattern tham chiếu `WorkflowAppCodeGen`). +- `(DbContext)db` cast (`:22`) để với tới `Database` — đúng cách vì `IApplicationDbContext` chỉ expose DbSet + SaveChangesAsync. +- Commit/Rollback đủ nhánh (`:43`, `:48`). +- **Hàng rào thứ 2 đo được trong DB:** `PK_WorkflowAppCodeSequences` đặt **trên chính cột `Prefix`** (`is_unique=1`) ⇒ nếu 2 giao dịch cùng lần-đầu-tiên-của-năm chèn `KHKK/2026`, một bên vỡ PK và rollback — **không thể sinh 2 row cùng prefix**. +- **Hàng rào thứ 3:** `IX_ContractSigningPlans_MaKeHoach` unique-filtered ⇒ dù codegen có lọt mã trùng thì INSERT phiếu vẫn bị chặn ở DB. +- 🔸 *Rủi ro còn lại (đã có tiền lệ, KHÔNG phải lỗi mới):* SERIALIZABLE + đọc-rồi-ghi cùng row ⇒ 2 giao dịch đồng thời có thể **deadlock (1205)** thay vì xếp hàng. Fail-safe (một bên abort, mã KHÔNG trùng, không mất data), và đây là hành vi chung của 4 module Office đang chạy prod. Nếu sau này thấy 1205 trong log Submit ⇒ đổi câu đọc sang `UPDLOCK, HOLDLOCK` để biến deadlock thành chờ. **Không đề xuất sửa trong wave này.** +- 🔸 Format `{LastSeq:D3}`: seq ≥ 1000 sẽ tự nới thành 4 chữ số (`KHKK/2026/1000`) — không lỗi, nhưng regex acceptance `^KHKK/\d{4}/\d{3}$` sẽ không còn khớp. Với nhịp phiếu KHKK thì còn rất xa; chỉ ghi để test-specialist biết ràng buộc của phép đo. + +**Không có write-path nào khác trong W1** (W1 chỉ đặt schema; handler CQRS thuộc W2/W3) ⇒ chưa có chỗ nào cần `RowVersion`. 🔸 **Cảnh báo trước cho W3:** `Plans.Phase` + `CurrentWorkflowStepIndex/LevelOrder` là bộ đếm bị 2 người duyệt đua — đây đúng lớp lỗi S43/S56 (LeaveBalance lost-update). W3 phải đi qua guard "đọc lại Phase trong transaction" hoặc `ExecuteUpdate` có điều kiện `WHERE Phase = @expected`, KHÔNG dùng bare-SaveChanges trên entity đã tracked. + +--- + +## 5. Đề xuất (ADVISORY — không sửa gì, lead quyết) + +**A-1 (nên làm ở W2, KHÔNG cần migration) — chặn 2 kế hoạch DaDuyet cho cùng 1 PE.** +`Plans.PurchaseEvaluationId` là `NOT NULL` + IX **không unique** ⇒ DB cho phép N kế hoạch trên 1 PE. W5 lại tra bằng `Plans.Where(PurchaseEvaluationId==peId && Phase==DaDuyet)` (`spec-wave-w5:13`) — nếu có 2 phiếu DaDuyet cùng PE thì câu này **mơ hồ**, lấy nhầm giá `ApprovedAmount` là sai tiền. +→ Đề xuất: guard trong handler Create/Approve của W2/W3 ("PE này đã có kế hoạch đang mở/đã duyệt"). **KHÔNG đề xuất thêm filtered-unique index** vì (i) plan cha chốt "Mig 69 = migration DUY NHẤT", (ii) chưa rõ nghiệp vụ có cho làm lại kế hoạch sau khi 1 cái bị huỷ hay không — khoá cứng ở DB lúc này là quyết định thay owner. + +**A-2 (W2) — xoá `DossierItem` phải dọn `Attachments` con.** Không có FK vật lý giữa 2 bảng (cố ý, để né multiple-cascade-path) ⇒ DB **không** tự dọn. Thiếu bước này thì màn hồ sơ hiện file gắn vào mục đã biến mất. + +**A-3 (chỉ khi wire SLA job) — thêm `IX_ContractSigningPlans_SlaDeadline`.** Hiện KHÔNG cần (job chỉ quét `Contracts`). + +**A-4 (vận hành, DB2) — Down xoá 7 bảng + toàn bộ data trong đó.** Script Down đã sinh và đọc sạch, nhưng nếu chạy sau khi prod đã có phiếu thì phải backup trước. Với dry-run GĐ2 (0 row) thì rollback an toàn. + +**KHÔNG có đề xuất nào bắt buộc trước deploy.** + +--- + +## 6. Bằng chứng Down chạy được (DB2-safe: sinh SCRIPT, KHÔNG apply) + +`dotnet ef migrations script 20260729122015_AddContractSigningPlans 20260727033522_AddPeAllowApproverDelete --no-build` → +```sql +BEGIN TRANSACTION; +DROP TABLE [ContractSigningPlanApprovals]; +DROP TABLE [ContractSigningPlanAttachments]; +DROP TABLE [ContractSigningPlanChangelogs]; +DROP TABLE [ContractSigningPlanDossierItems]; +DROP TABLE [ContractSigningPlanLevelOpinions]; +DROP TABLE [ContractSigningPlanLines]; +DROP TABLE [ContractSigningPlans]; +DELETE FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'20260729122015_AddContractSigningPlans'; +COMMIT; +``` +→ Sinh **không lỗi**, 7 DROP + xoá history nằm trong **MỘT transaction** (atomic, không kẹt nửa chừng). Chiều Up cũng sinh sạch (đuôi script: 20 CREATE INDEX + INSERT history `ProductVersion = 10.0.6` = EF Core 10, đúng pin). +⚠️ **Tôi CỐ Ý KHÔNG chạy `database update `** trên LocalDB Dev — đó là lệnh destructive (DB2), và acceptance "Down chạy sạch trên DB copy" thuộc quyền lead/implementer trên **bản copy**, không phải trên DB Dev đang dùng. + +--- + +## §END — VERDICT + +| # | Mục | Verdict | +|---|---|---| +| a | Down reversible sạch (7 DropTable đúng thứ tự FK) | **PASS** | +| b | 0 ALTER bảng cũ | **PASS** (0 AddColumn/AlterColumn/DropColumn/Sql) | +| c | FK strategy 6 Cascade + 2 Restrict, 0 multiple-cascade-path | **PASS** | +| d | Index đủ cho list/inbox | **PASS** (6/6 con seek theo PlanId; thiếu IX_SlaDeadline là CỐ Ý ĐÚNG) | +| e | UNIQUE filtered gotcha #57 | **PASS** (Lines `[IsDeleted]=(0)` ✓; LevelOpinions không filter = đúng khuôn 2 twin) | +| f | maxLength khớp spec `:134-137` | **PASS** 11/11 + 5 cột ngoài spec; đúng 1 nvarchar(max) cố ý | +| g | loose-Guid thiếu IX | **PASS** — superset khuôn PE, 4 cột không IX đều parity | +| h | Snapshot drift | **PASS** — Designer ≡ Snapshot (chỉ khác header), diff 550 insert / 0 delete | +| — | sqlcmd đối chứng Dev + Design | **PASS** — 96/96 bảng, 7/7 CSP, 27/27 index, mig-top khớp, 2 DB parity | +| — | DB11 concurrency (codegen) | **PASS** — SERIALIZABLE + PK(Prefix) + unique-filtered MaKeHoach = 3 lớp | + +**KẾT LUẬN: PASS TOÀN BỘ — Mig 69 ĐỦ ĐIỀU KIỆN DEPLOY.** 0 blocker, 0 finding phải sửa trước deploy. 4 advisory (A-1..A-4) đều thuộc wave sau / vận hành. + +TOTAL: 10 dòng verdict · 0 FAIL · 4 advisory. + diff --git a/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-test-specialist-0.md b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-test-specialist-0.md index 41d73a0..31e1ac0 100644 --- a/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-test-specialist-0.md +++ b/.claude/workflows/runs/2026-07-29-S161-khkk-w1-schema/sub-test-specialist-0.md @@ -49,17 +49,84 @@ Và `PurchaseEvaluationCodeGeneratorTests.cs:17` gọi thẳng `new PurchaseEval ## §1 — Test đã viết -File MỚI: `tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanSchemaTests.cs` -(đặt cạnh `EmployeeSatelliteTests.cs` — cùng lớp "EF-model assertion", khuôn `EmployeeSatelliteTests.cs:248-261` `db.Model.FindEntityType(...)`). +File MỚI **duy nhất**: `tests/SolutionErp.Infrastructure.Tests/Common/ContractSigningPlanSchemaTests.cs` +Đặt ở `Common/` cạnh `ContractV2SchemaPersistenceTests.cs` (sibling gần nhất về NGHĨA: schema-invariant +của họ `*LevelOpinion` + workflow V2), namespace `SolutionErp.Infrastructure.Tests.Common`. +**4 test** (sàn spec là N ≥ 2): -(chi tiết điền sau khi chạy — xem §2/§3) +| # | Test | Nội dung | Vế chống vacuous | +|---|---|---|---| +| 1 | `BayBang_MoiBang_CoAuditFields_VaQueryFilter` **(PIN spec)** | Quét EF model theo namespace `SolutionErp.Domain.ContractSigningPlans` → phải ra ĐÚNG 7 entity; mỗi bảng: tên bảng vật lý + 4 cột audit + bộ-3 soft-delete + có/không query filter | (a) `BeEquivalentTo` bộ 7 tên ⇒ thêm bảng #8 mà quên khai = ĐỎ; (b) soft-delete assert **2 chiều** (BaseEntity phải KHÔNG có `IsDeleted`); (c) đuôi test seed 1 phiếu `IsDeleted=true` → filter phải giấu (0) mà `IgnoreQueryFilters` vẫn thấy (1) ⇒ chứng filter là BIỂU THỨC THẬT, không chỉ "có khai" | +| 2 | `SigningPlanCodeGenerator_Format_KhkkYearSeqD3` **(PIN spec)** | 2 lần `GenerateAsync()` → regex `^KHKK/\d{4}/\d{3}$` + giá trị tuyệt đối `KHKK/2026/001` → `/002` + `Seq(code2)==Seq(code1)+1` + row `WorkflowAppCodeSequences.Prefix=="KHKK/2026"` có `LastSeq==2` | đẩy đồng hồ sang **2027** → đòi `KHKK/2027/001` (reset) + hàng 2026 giữ `LastSeq==2`. Không có vế này thì `\d{4}` vẫn xanh dù năm bị **hardcode** | +| 3 | `Line_UniqueFilteredTheoIsDeleted_ChoXoaMemRoiThemLaiCungNcc` | gotcha #57: xoá mềm 1 dòng NCC rồi thêm LẠI đúng NCC đó ⇒ KHÔNG được nổ UNIQUE; active==1, `IgnoreQueryFilters`==2 | 🔴 **vế đối chứng bắt buộc**: 2 dòng **CÒN SỐNG** cùng `(PlanId, SupplierId)` ⇒ PHẢI ném `DbUpdateException`. Không có vế này thì "không nổ" cũng đúng trong thế-giới **index chưa từng được tạo** trên SQLite | +| 4 | `ApplicableType_ContractSigningPlan_PinSlot10_KhongTrungSlot` | `(int)ApprovalWorkflowApplicableType.ContractSigningPlan == 10` + không member nào khác chiếm slot 10 | enum này `HasConversion` (`ApprovalWorkflowConfiguration.cs:17`) ⇒ số ĐÃ ghi xuống DB; đánh số lại = quy trình đã pin trỏ sai module, hỏng IM LẶNG | + +Tiền lệ tái dùng, KHÔNG chế hạ tầng mới: `SqliteDbFixture` + `FixedDateTime` (`Common/SqliteDbFixture.cs:43,71`). +KHÔNG cần `IdentityFixture` (không seed User/Level ⇒ tránh FK Restrict `ApprovalWorkflowLevel→User`). --- ## §2 — Số đo -(điền sau) +### Baseline ĐO THẬT ngay trước khi thêm test (không lấy số nghe kể) +``` +Passed! - Failed: 0, Passed: 45, Total: 45 — SolutionErp.Domain.Tests.dll +Passed! - Failed: 0, Passed: 517, Total: 517 — SolutionErp.Infrastructure.Tests.dll (1 m 17 s) +``` +⇒ **562** = 45 + 517 — KHỚP con số spec ghi (562). -## §3 — Verdict +### Filtered run 4 test mới +``` +Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 1 s +--filter "FullyQualifiedName~ContractSigningPlanSchemaTests" +``` -(điền sau) +### 🔬 Fault-injection chứng test CÓ RĂNG (bơm lỗi vào BẢNG KỲ VỌNG của chính test — file tests/, KHÔNG đụng production) +Áp luật "fault-injection proves teeth": một detector luôn-trả-True cũng cho 4/4 xanh, nên phải ép ĐỎ **cả 2 chiều**. + +- **Chiều A** — đổi kỳ vọng `ContractSigningPlan` từ `QueryFilter=true` → `false`: + ``` + Expected hasFilter to be False because header AuditableEntity — HasQueryFilter(!IsDeleted) + `ContractSigningPlanConfiguration.cs:47`, but found True. + Failed! - Failed: 1, Passed: 0 (ContractSigningPlanSchemaTests.cs:line 108) + ``` + ⇒ chứng bộ đo đọc ra **True** ở bảng CÓ filter. +- **Chiều B** — đổi kỳ vọng `ContractSigningPlanLevelOpinion` từ `false` → `true`: + ``` + Expected hasFilter to be True because AuditableEntity NHƯNG CỐ Ý KHÔNG filter — …, but found False. + Failed! - Failed: 1, Passed: 0 + ``` + ⇒ chứng bộ đo đọc ra **False** ở bảng KHÔNG filter. + +Hai chiều ⇒ `((IReadOnlyEntityType)et).GetDeclaredQueryFilters().Any()` **phân biệt được** hai thế giới, không phải hằng số. Cả 2 lần bơm đã **revert về đúng trạng thái đĩa** ngay sau khi đo. + +### Full suite sau khi thêm +**[LEAD điền on-behalf @S161 — vai chết #53 lúc đợi job nền; số do lead chạy TƯƠI]** +``` +Passed! - Failed: 0, Passed: 45, Total: 45 — SolutionErp.Domain.Tests.dll +Passed! - Failed: 0, Passed: 521, Total: 521 — SolutionErp.Infrastructure.Tests.dll (50s) +``` +⇒ **566 = 562 + 4** (sàn spec ≥ 564 VƯỢT). 0 fail. + +--- + +## §3 — Ghi chú kỹ thuật đáng nhớ + +1. **EF Core 10 đổi API query-filter.** EF 10 có filter CÓ TÊN ⇒ ngoài `GetQueryFilter()` (cũ, ném khi có + nhiều filter tên) còn `GetDeclaredQueryFilters()`. Probe XML doc gói thật đang restore + (`~/.nuget/packages/microsoft.entityframeworkcore/10.0.6/lib/net10.0/*.xml`) thấy CẢ HAI còn sống; + chọn `GetDeclaredQueryFilters()` + ép kiểu `(IReadOnlyEntityType)` để khỏi dính obsolete/ambiguity. + Repo KHÔNG bật `TreatWarningsAsErrors` (grep 0 hit) nên đây là chọn cho sạch, không phải bắt buộc. +2. **Serializable trên SQLite — KHÔNG phải vấn đề**, đã có tiền lệ ghi sẵn ở `SqliteDbFixture.cs:36-42` + ("IsolationLevel enum bị provider map gracefully") và `PurchaseEvaluationCodeGeneratorTests.cs:17`. + ⇒ gọi thẳng `new ContractSigningPlanCodeGenerator(fix.Db, clock)`, KHÔNG sửa production. +3. **`HasFilter("[IsDeleted] = 0")` viết kiểu SQL Server VẪN chạy trên SQLite** — SQLite chấp nhận + định danh trong `[...]` và có partial index. Test 3 xanh cả 2 vế ⇒ index filtered được tạo THẬT. +4. **KHÔNG dùng `Remove()` để đo xoá mềm**: `TestApplicationDbContext` không wire `AuditingInterceptor` + ⇒ `Remove()` = xoá CỨNG (bài học S155). Test set cờ `IsDeleted` bằng tay — đúng thứ cần đo. + +--- + +## §4 — Verdict + +**[LEAD điền on-behalf]** PASS — 4/4 test mới xanh trong full-suite **566/0** (lead chạy tươi); fault-inject 2 chiều đã chứng răng (§2); baseline khớp đĩa. Vai chết #53 SAU khi mọi việc thật xong — sub-md ghi-từng-mục cứu trọn lần 2 trong wave này.