# sub-database-agent-1 — Design-review Mig 69 `AddContractSigningPlans` (S161 W1, READ-ONLY advisory) > 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: **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. --- ## 0. Vật liệu đã đọc (evidence anchors) | Vật | Đường dẫn | Ghi chú | |---|---|---| | Migration | `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260729122015_AddContractSigningPlans.cs` | 373 dòng | | Designer | `...20260729122015_AddContractSigningPlans.Designer.cs` | 264.515 B | | Snapshot | `...Migrations/ApplicationDbContextModelSnapshot.cs` | 264.399 B, git diff +550/-0 | | Config ×7 | `src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractSigningPlan*.cs` | 7 file | | Spec W1 | `.claude/workflows/runs/2026-07-29-S160-khkk-dryrun-plan/spec-wave-w1-schema-designer-29-07-2026.md:16-20` | §②-2/4/6 | | Spec gốc maxLength | `.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md:134-137` | danh sách 11 cột chuỗi | Đếm operation trong migration (grep + uniq): - `CreateTable` ×7 · `CreateIndex` ×20 · `DropTable` ×7 (Down) - `AddColumn` / `AlterColumn` / `DropColumn` / `RenameColumn` / `Sql(` = **0** (không match nào) --- ## 3. Đối chứng sqlcmd LocalDB (DB1/DB10 — schema THẬT, không trí nhớ) `sqlcmd -S "(localdb)\MSSQLLocalDB" -d SolutionErp_Dev -E` : ``` TABLECOUNT|96 CSPTABLE|ContractSigningPlanApprovals CSPTABLE|ContractSigningPlanAttachments CSPTABLE|ContractSigningPlanChangelogs CSPTABLE|ContractSigningPlanDossierItems CSPTABLE|ContractSigningPlanLevelOpinions CSPTABLE|ContractSigningPlanLines CSPTABLE|ContractSigningPlans MIGTOP|20260729122015_AddContractSigningPlans MIGCOUNT|69 ``` `-d SolutionErp_Design` : ``` DESIGN_TABLECOUNT|96 · DESIGN_CSP|7 · DESIGN_MIGTOP|20260729122015_AddContractSigningPlans · DESIGN_IXCOUNT|27 ``` → **2 DB parity TUYỆT ĐỐI** (96/96 bảng · 7/7 CSP · cùng mig-top · 27 index = 20 IX + 7 PK). → **KHÔNG có committed-but-unapplied-local drift** (khác bẫy S53). → Acceptance §③-B "sys.tables 89 → 96" = **ĐẠT** (đo được 96; delta +7 khớp 7 CreateTable). ### Index thật trong DB (27 dòng, khớp 1:1 file mig) Nhóm filtered (soi gotcha #57): ``` ContractSigningPlanLines | IX_..._ContractSigningPlanId_SupplierId | unique=1 | ([IsDeleted]=(0)) | ContractSigningPlanId,SupplierId ContractSigningPlans | IX_ContractSigningPlans_MaKeHoach | unique=1 | ([MaKeHoach] IS NOT NULL) | MaKeHoach ``` Các IX còn lại filter = `-` (không filter), unique=0 trừ `IX_ContractSigningPlanLevelOpinions_ContractSigningPlanId_ApprovalWorkflowLevelId` (unique=1, **không** filter — bàn ở §2e/§4). ### FK thật (8 FK, cột `delete_referential_action_desc`) ``` Approvals → ContractSigningPlans CASCADE Attachments → ContractSigningPlans CASCADE Changelogs → ContractSigningPlans CASCADE DossierItems → ContractSigningPlans CASCADE LevelOpinions→ ContractSigningPlans CASCADE Lines → ContractSigningPlans CASCADE LevelOpinions→ ApprovalWorkflowLevels NO_ACTION (= Restrict) 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.