21 KiB
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 → DossierItemskhô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
DropIndexriê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, configContractSigningPlanConfiguration.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
DossierItemKHÔNG tự dọnAttachmentstrỏ nó →ContractSigningPlanDossierItemIdthà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ônContracts.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ọ*LevelOpinionlà 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)
- 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 AddContractSigningPlansvsApplicationDbContextModelSnapshot : ModelSnapshot·BuildTargetModelvsBuildModel. 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.) - 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-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 tronggit status(2 file đầu untracked-mới, snapshot modified). ✓ skillef-core-migration. - Tên file dùng timestamp EF chuẩn
20260729122015_, không có tiền tố69_⇒ khớp acceptance §③-B dòng 1.__EFMigrationsHistoryCOUNT = 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ếuWorkflowAppCodeGen).(DbContext)dbcast (:22) để với tớiDatabase— đúng cách vìIApplicationDbContextchỉ 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ộtPrefix(is_unique=1) ⇒ nếu 2 giao dịch cùng lần-đầu-tiên-của-năm chènKHKK/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_MaKeHoachunique-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 →
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.