# 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 <prev>`** 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.

