262 lines
18 KiB
Markdown
262 lines
18 KiB
Markdown
# sub-reviewer-2 — Adversarial review W6 (view-guard + inbox + guard TRÌNH của Contract)
|
||
|
||
> Ghi TỪNG TRỤC ngay khi đo xong (chống #53). READ-only — không sửa gì.
|
||
> Diff đo: `git diff` working-tree vs HEAD `fe1f6b0` — `ContractFeatures.cs` (+142/-4) ·
|
||
> `ContractWorkflowService.cs` (+15/-1) · `tests/.../Application/ContractV2PipelineTests.cs` (UNTRACKED, mới).
|
||
|
||
---
|
||
|
||
## TRỤC 1 — Nới-view có mở QUÁ không? (per-HĐ vs role/workflow-blanket)
|
||
|
||
### 1.1 Ba call-site `GetEligiblePhases` — ĐỦ và ĐÚNG VỊ TRÍ
|
||
|
||
Grep repo-wide (`grep -rn "GetEligiblePhases" src/Backend --include=*.cs`) → đúng **3** call-site,
|
||
khớp 3 anchor spec (số dòng đã trôi sau khi chèn code):
|
||
|
||
| Spec anchor | Dòng THẬT sau diff | Handler | W6 có ghép vế V2? |
|
||
|---|---|---|---|
|
||
| `:301` | `ContractFeatures.cs:301` | `ListContractsQueryHandler` | CÓ (`:307-314`) |
|
||
| `:381` | `ContractFeatures.cs:425` | `ListDeletedContractsQueryHandler` | KHÔNG (cố ý, `:426-431`) |
|
||
| `:486` | `ContractFeatures.cs:603` | `GetContractQueryHandler` | CÓ (`:605-621`) |
|
||
|
||
Không có call-site thứ 4. Rủi ro "SỬA CHỈ 1/3 call-site" (spec §③-C) = **KHÔNG xảy ra**.
|
||
|
||
### 1.2 Vế V2 là PER-HĐ thật — KHÔNG role-blanket
|
||
|
||
`ContractFeatures.cs:311-312` (List):
|
||
```
|
||
|| (x.c.Phase != ContractPhase.DangSoanThao
|
||
&& myWorkflowIds.Contains(x.c.ApprovalWorkflowId))
|
||
```
|
||
`ContractFeatures.cs:609-613` (Detail): `AnyAsync(l => l.Step!.ApprovalWorkflowId == awIdForCheck && l.ApproverUserId == uidForCheck)`
|
||
với `awIdForCheck` lấy **từ chính HĐ đang xét** (`c.ApprovalWorkflowId`).
|
||
|
||
⇒ Vị ngữ hỏi theo **workflow ĐÃ PIN của HĐ đó**, không hỏi theo role. Approver của quy trình KHÁC
|
||
vẫn 403 — test T3 `:356-360` ghim đúng ca này. **KHÔNG role-blanket.** ĐẠT.
|
||
|
||
Hai bờ List ⟂ Detail **đồng vị ngữ** (kiểm bằng tay, khai triển 2 biểu thức):
|
||
- List cho thấy ⟺ `isDrafter ∨ eligibleByRole ∨ (Phase≠Nháp ∧ inMyWorkflows) ∨ (hardCopy ∧ Phase=DaPhatHanh)`
|
||
- Detail cho mở ⟺ `isDrafter ∨ eligibleByRole ∨ (hardCopy ∧ Phase=DaPhatHanh) ∨ (¬Nháp ∧ isV2Approver)`
|
||
⇒ **TƯƠNG ĐƯƠNG**. Không có lớp "thấy ở list mà 403 khi mở" (hoặc ngược lại).
|
||
|
||
### 1.3 Nhưng NÓ LÀ **workflow-blanket** (theo thiết kế spec) — cần owner biết ĐỘ RỘNG
|
||
|
||
`ApprovalWorkflow` là **khuôn dùng lại** (Code + Version + `ApplicableType=Contract` + `IsActive`),
|
||
1 quy trình pin cho **NHIỀU HĐ**. Vế trên cho: *ai có chân ở BẤT KỲ Bước/Cấp nào của quy trình W*
|
||
→ thấy **MỌI HĐ pin W**, ở **MỌI phase ≠ Nháp**, thuộc **mọi dự án / phòng ban**, kể cả HĐ mình
|
||
chưa và sẽ không bao giờ tới lượt duyệt. Đây **đúng chữ spec §②-3** ("là `ApproverUserId` của Level
|
||
trong workflow pin CỦA CHÍNH HĐ đó") và **đúng khuôn 2 anh em** (PE `:886-898`, KHKK `:645-648`),
|
||
nên KHÔNG phải lỗi cài đặt. Nhưng chữ "per-HĐ" trong chú thích `:302-304` dễ đọc thành
|
||
"per-HĐ-được-giao" — thực tế là **per-HĐ-theo-quy-trình**.
|
||
|
||
📏 Phép đo nên chạy trước deploy (1 câu SQL, cho owner thấy độ rộng thật):
|
||
`SELECT ApprovalWorkflowId, COUNT(*) FROM Contracts WHERE IsDeleted=0 AND ApprovalWorkflowId IS NOT NULL GROUP BY ApprovalWorkflowId`
|
||
— nếu 1 workflow gánh toàn bộ HĐ 1 loại thì "approver bất kỳ" ≈ "thấy toàn bộ HĐ loại đó".
|
||
→ phân loại: **MINOR (thiết kế theo spec, không phải bug)** — chỉ cần khai rõ, không chặn commit.
|
||
|
||
### 1.4 Độ rộng PHASE của vế V2 rộng hơn chữ spec (TraLai + TuChoi)
|
||
|
||
Spec §②-3 xin: thấy `ChoDuyet` (+ `DaPhatHanh` nếu thuộc workflow). Cài đặt dùng
|
||
**phủ định** `Phase != DangSoanThao` ⇒ approver còn thấy `TraLai(98)` và `TuChoi(99)`
|
||
(và mọi phase legacy 1,3-8 nếu data cũ có).
|
||
- `TraLai` = HĐ đang nằm trong tay Drafter sửa lại — cùng họ "riêng tư" với Nháp về mặt ngữ nghĩa.
|
||
- `TuChoi` = terminal khoá.
|
||
Khuôn KHKK `ContractSigningPlanFeatures.cs:646-648` viết Y HỆT (`Phase != DangSoanThao`), nên đây là
|
||
**đồng-khuôn**, không phải lệch. Ghi nhận để lần sau ai siết thì biết đang đổi hợp đồng.
|
||
→ **MINOR (đồng khuôn anh em, spec không cấm)**.
|
||
|
||
### 1.5 Vế `HardCopyActorRoles` **ĐÚNG LÀ role-blanket** — nhưng spec gọi tên đích danh
|
||
|
||
`:375-379` + `:314` + `:618-619`: mọi user mang `Director ∨ AuthorizedSigner ∨ HrAdmin ∨ CostControl`
|
||
thấy **TOÀN BỘ** HĐ `DaPhatHanh` của công ty (có `GiaTri`), không cần liên quan dự án/quy trình.
|
||
Spec §②-3 liệt đúng 4 vai này ⇒ **đúng đơn hàng**. Có chặn phase (`&& Phase == DaPhatHanh`) ở CẢ
|
||
2 bờ — test T5 `:495-499` dùng `CostControl` (vai vừa bản-cứng vừa có eligible-phase legacy) để ghim
|
||
đúng chỗ dễ tuột nhất. ĐẠT.
|
||
|
||
### 1.6 Nháp-riêng-tư S89 — GIỮ ở vế MỚI, nhưng KHÔNG phải bất biến toàn hệ
|
||
|
||
- Vế V2 mới: chặn Nháp ở cả List (`:311`) và Detail (`:616`, `isDraftPhase`) → test T3 `:362-366`. ĐẠT.
|
||
- 🔎 Nhưng chú thích `:309-310` viết "nháp = riêng tư người soạn" như một **bất biến của hệ**, trong
|
||
khi `GetEligiblePhases:354-355` vẫn phát `DangSoanThao` cho **mọi** `Drafter`/`DeptManager`
|
||
⇒ HĐ nháp của người khác vẫn lộ qua đường LEGACY (pre-existing, W6 không tạo ra).
|
||
Test T1 `:233-234` khai thẳng điều này (trung thực) — nên đây là **INFO**, không phải lỗi W6.
|
||
Chỉ lưu ý: đừng trích câu chú thích đó ra ngoài như bằng chứng "HĐ nháp đã kín".
|
||
|
||
### 1.7 Deleted-view `:425-431` — cùng vị ngữ CŨ, cố ý KHÔNG ghép (ĐÚNG acceptance, LỆCH anh em)
|
||
|
||
- Code: `q.Where(x => x.c.DrafterUserId == userId || eligiblePhases.Contains(x.c.Phase))` — **0 thay đổi hành vi**.
|
||
- Khớp acceptance §③-B `:31` (HĐ `ChoDuyet` xoá mềm KHÔNG rò sang user V2). Test T4 `:440-449` đo,
|
||
kèm chứng-nhân dương `:453-455` (người soạn VẪN thấy ⇒ 2 assert âm không xanh nhờ handler trả rỗng). ĐẠT.
|
||
- ⚠️ **Lệch anh em có chủ đích:** KHKK `ContractSigningPlanFeatures.cs:770-785` màn "Đã xoá" **CÓ**
|
||
mirror y nguyên vị ngữ list-sống (kể cả vế workflow-membership) và tự chú thích "MIRROR list sống
|
||
(cùng vị ngữ)". Contract W6 làm NGƯỢC. Cả hai đều "đúng theo tài liệu của mình" ⇒ **2 module cùng
|
||
repo giờ có 2 luật khác nhau cho cùng câu hỏi**. Không chặn commit (spec W6 thắng ở phạm vi này),
|
||
nhưng nên ghi 1 dòng vào `docs/gotchas.md` hoặc §rules để lần sau không ai "đồng bộ hoá" nhầm chiều.
|
||
→ **MINOR**.
|
||
|
||
### 1.8 Kỹ thuật — 2 điểm cần nêu
|
||
|
||
- **(a) `List<Guid?>` + `Contains` chỉ mới được chứng trên SQLite.** `:387-395` trả `List<Guid?>`
|
||
rồi `myWorkflowIds.Contains(x.c.ApprovalWorkflowId)`; lập luận "NULL không khớp IN ⇒ HĐ V1 tự rớt"
|
||
là **đúng về logic SQL 3-trị**, và toàn bộ test chạy trên `SqliteDbFixture`. Prod = SQL Server
|
||
(EF Core 10 dịch collection-param qua `OPENJSON`). Chưa có phép đo nào chạm SQL Server.
|
||
KHKK `:275-283` tránh hẳn chuyện này bằng `List<Guid>` + `p.ApprovalWorkflowId != null && …Contains(p.ApprovalWorkflowId.Value)`.
|
||
→ **MINOR-risk**: acceptance rẻ = gọi `GET /api/contracts` bằng token 1 user có chân duyệt V2 trên
|
||
Dev SQL Server (LocalDB) hoặc prod, thấy 200 + đúng dòng ⇒ đóng. Nếu `OPENJSON` cãi kiểu thì lỗi
|
||
là **500 lúc chạy**, test SQLite không bắt được.
|
||
- **(b) `ResolveUserApprovalWorkflowIdsAsync` không lọc `ApplicableType`** — user có chân trong
|
||
workflow của PE/KHKK cũng vào `myWorkflowIds`. Vô hại **vì** id đó không bao giờ khớp
|
||
`Contract.ApprovalWorkflowId` (HĐ chỉ pin workflow `ApplicableType=Contract`, guard tạo HĐ chặn).
|
||
Ghi lại để không ai bỏ guard tạo HĐ mà tưởng chỗ này vẫn kín. → **INFO**.
|
||
|
||
**TRỤC 1 = PASS** (0 critical). 1 MINOR-risk kỹ thuật (a) + 3 MINOR ghi-nhận (1.3 / 1.4 / 1.7) + 2 INFO.
|
||
|
||
---
|
||
|
||
## TRỤC 2 — Admin-inbox nhánh phase tường minh
|
||
|
||
### 2.1 `AdminInboxPhases` = `PhaseActorRoles.Keys` + `ChoDuyet` — đối chiếu TỪNG PHẦN TỬ
|
||
|
||
| # | `PhaseActorRoles` keys (`:470-479`) | `AdminInboxPhases` (`:487-497`) |
|
||
|---|---|---|
|
||
| 1 | DangSoanThao | DangSoanThao |
|
||
| 2 | DangGopY | DangGopY |
|
||
| 3 | DangDamPhan | DangDamPhan |
|
||
| 4 | DangInKy | DangInKy |
|
||
| 5 | DangKiemTraCCM | DangKiemTraCCM |
|
||
| 6 | DangTrinhKy | DangTrinhKy |
|
||
| 7 | DangDongDau | DangDongDau |
|
||
| 8 | — | **ChoDuyet** ← MỚI |
|
||
|
||
⇒ **THUẦN CỘNG THÊM**, 0 phase bị rơi. Chú thích `:484` viết "7 phase legacy CŨ + ChoDuyet" —
|
||
**con số ĐÚNG** (đếm tay 7). ĐẠT.
|
||
|
||
### 2.2 KHÔNG lọt terminal
|
||
|
||
Enum `ContractPhase` có 12 giá trị. `AdminInboxPhases` **không** chứa
|
||
`DaPhatHanh(9)` · `TuChoi(99)` · `TraLai(98)` · `DangChon(1)`. Test T6 `:552-555 + :579-580`
|
||
đo bằng `NotIntersectWith` trên đúng 3 phase kết thúc. ĐẠT.
|
||
|
||
### 2.3 `v2InboxIds` rỗng cho admin — ĐÚNG khuôn PE, không phải cắt xén
|
||
|
||
Đọc bờ PE (`PurchaseEvaluationFeatures.cs:746-748`):
|
||
`var v2InboxIds = isAdmin ? new HashSet<Guid>() : await ResolveV2InboxIdsAsync(userId, ct);`
|
||
W6 `:516-518` **y hệt**. Admin đi bằng nhánh PHASE nên đủ. ĐẠT (mirror thật, đã đọc bờ kia,
|
||
không tin lời khai chú thích).
|
||
|
||
### 2.4 Điều-kiện-thoát-sớm — sửa ĐÚNG chỗ chí tử
|
||
|
||
`:522` `if (eligiblePhases.Count == 0 && v2InboxIds.Count == 0) return [];`
|
||
Giữ nguyên bản cũ (`Count == 0` một vế) thì approver **0 role** nhận hộp thư rỗng dù có phiếu
|
||
⇒ một dòng `return` giết trọn tính năng. Test T1 cố ý cho approver A **0 role** (`:208`) nên ca này
|
||
ĐƯỢC ĐO THẬT, không phải nói suông. ĐẠT.
|
||
|
||
### 2.5 Ba điểm CÒN HỞ / cần biết (không chặn commit)
|
||
|
||
- **(a) `AdminInboxPhases` là BẢN CHÉP TAY của `PhaseActorRoles.Keys` ⇒ mầm derived-drift.**
|
||
Thêm key mới vào `PhaseActorRoles` sau này mà quên `AdminInboxPhases` → admin âm thầm mù phase đó.
|
||
Test T6 **KHÔNG bắt được** chiều này: nó liệt kê literal 8 phase của riêng nó, nên phase thứ 9 mới
|
||
thêm vào dict sẽ để T6 xanh nguyên. → **MINOR**. Acceptance rẻ nếu muốn đóng:
|
||
1 assert `PhaseActorRoles.Keys ⊆ AdminInboxPhases` (cần nới `private` → `internal` cho dict).
|
||
- **(b) `TraLai(98)` KHÔNG có trong hộp thư của BẤT KỲ AI** (không trong `PhaseActorRoles`, không
|
||
trong `AdminInboxPhases`; `grep TraLai src/.../ContractFeatures.cs` = **0 hit**). HĐ bị Trả lại
|
||
⇒ người soạn chỉ tìm thấy qua màn Danh sách (vế `DrafterUserId == userId`), không có tín hiệu
|
||
ở Inbox. Vòng `ChoDuyet → TraLai → gửi lại` vì thế **chưa khép ở lớp hộp thư**.
|
||
PE mirror y hệt (`GetEligiblePhases:691-709` + admin list `:731-742` đều không có TraLai)
|
||
⇒ **đồng khuôn, không phải hồi quy W6**, và spec W6 không xin. → **MINOR / ngoài phạm vi**.
|
||
- **(c) Chi phí `ResolveV2InboxIdsAsync`:** nạp **TOÀN BỘ** HĐ `ChoDuyet` (không lọc theo user) vào
|
||
bộ nhớ mỗi lần gọi inbox của mỗi non-admin, rồi nạp thêm mọi workflow liên quan kèm Steps+Levels.
|
||
Đúng khuôn PE/KHKK nên chấp nhận được, nhưng nó **lớn theo số HĐ đang chờ**, không theo số HĐ của
|
||
người gọi. Kèm `Take(100)` ở `:534` ⇒ hộp thư đông có thể bị cắt ngọn. → **INFO**.
|
||
|
||
**TRỤC 2 = PASS** (0 critical). 2 MINOR + 1 INFO.
|
||
|
||
---
|
||
|
||
## TRỤC 3 — Guard TRÌNH: 4 vế + null-trap + có mở transition nào khác không
|
||
|
||
### 3.1 Khai triển guard (`ContractWorkflowService.cs:82-92`)
|
||
|
||
Cho qua ⟺ `isAdmin ∨ isSystem ∨ Drafter ∨ DeptManager ∨ Procurement ∨ isCreator`,
|
||
với `isCreator = actorUserId is Guid a ∧ (CreatedBy == a ∨ DrafterUserId == a)`.
|
||
|
||
### 3.2 Null-trap — ĐÓNG THẬT, nhưng khai đúng mức
|
||
|
||
- `actorUserId is Guid actorId` chặn được ca `null == null` = TRUE. ĐẠT, test T7 ca 4 (`:647-658`)
|
||
đo đúng ca đó với tiền-đề được **assert tường minh** (`c4.CreatedBy.Should().BeNull()` `:649`)
|
||
⇒ phép đo không thể xanh ở "thế giới khác". Đây là chỗ làm tốt nhất của cả diff.
|
||
- ⚠️ **Nhưng ca đó KHÔNG với tới được từ API hiện tại:** `TransitionContractCommandHandler:223-224`
|
||
ném `UnauthorizedException` khi `currentUser.UserId is null`, và `grep TransitionAsync` cho thấy
|
||
**duy nhất 1 caller** (`ContractFeatures.cs:229`). Không có job/SLA nào gọi. ⇒ guard này là
|
||
**phòng thủ chiều sâu**, không phải bịt lỗ đang chảy. Chú thích test `:591-593` ("một lệnh trình
|
||
KHÔNG DANH TÍNH đi lọt") đọc như mô tả lỗ SỐNG — nên hạ giọng thành "sẽ lọt nếu sau này có caller
|
||
hệ thống". → **INFO (độ chính xác lời khai)**, giữ nguyên mã.
|
||
|
||
### 3.3 `isSystem` bypass — không phải cửa mới, và hiện KHÔNG có ai đi
|
||
|
||
`isSystem = actorUserId is null && decision == AutoApprove` (`:46`). Từ API, `actorUserId` không bao
|
||
giờ null (3.2) ⇒ nhánh này hiện **chết**. FE gửi `decision=AutoApprove` cũng vô hại vì actor có id.
|
||
Đúng thiết kế đã ghi trong skill `contract-workflow` ("System bypass"). ĐẠT, → **INFO**.
|
||
|
||
### 3.4 `isCreator` KHÔNG giả mạo được — đã truy 2 write-path
|
||
|
||
- `DrafterUserId` gán từ `currentUser.UserId` tại create (`ContractFeatures.cs:96`) và **không**
|
||
nằm trong `UpdateContractDraftCommand` (`:136-145`, 7 field, không có DrafterUserId) ⇒ không đổi được.
|
||
- `CreatedBy` do `AuditingInterceptor` gán. Không có endpoint nào set.
|
||
⇒ Không có đường "tự phong mình làm người tạo rồi trình HĐ người khác". ĐẠT.
|
||
|
||
### 3.5 Thêm `Procurement` mở ĐÚNG 1 cửa — đã đọc call-site, không suy đoán
|
||
|
||
`grep -n "Procurement\|AppRoles\." ContractWorkflowService.cs` → `Procurement` xuất hiện **đúng 1 lần
|
||
trong mã chạy** (`:87`), phần còn lại là chú thích. Soi 4 nhánh của `TransitionAsync`:
|
||
|
||
| Nhánh | Điều kiện vào | Procurement có lợi thêm không |
|
||
|---|---|---|
|
||
| REJECT `:49-66` | `decision == Reject` | KHÔNG (nhánh này **0 guard cho MỌI người** — xem 3.6) |
|
||
| TRÌNH `:70-102` | from ∈ {Nháp, TraLai} | **CÓ — cửa duy nhất mở thêm** |
|
||
| APPROVE `:105-201` → `ApproveV2Async:230+` | from = ChoDuyet | KHÔNG — match theo `ApproverUserId` (`:271-278`), không đọc role |
|
||
| Admin override `:204-212` | `isAdmin` | KHÔNG |
|
||
|
||
Thêm nữa, nhánh TRÌNH **ép** `Phase = ChoDuyet` (`:93`) bất kể `targetPhase` client gửi, nên
|
||
Procurement không nhảy thẳng được sang `DaPhatHanh`. ĐẠT — **đúng "dòng tối thiểu"** như spec §②-5.
|
||
|
||
### 3.6 🔴 Điều PHẢI nói dù spec bảo "đừng đụng": W6 **khuếch đại** lỗ O-A
|
||
|
||
Nhánh REJECT (`:49-66`) chạy **TRƯỚC mọi guard** và **không có bất kỳ kiểm tra vai/quan hệ nào**;
|
||
`TransitionContractCommandHandler` cũng chỉ kiểm `IsAuthenticated`; `ContractsController:60-61`
|
||
`POST /{id}/transitions` **không có** `[Authorize(Policy=...)]` (class chỉ `[Authorize]` trần —
|
||
đúng lớp gotcha #82). ⇒ **bất kỳ user đăng nhập nào biết `contractId` đều Trả-lại/Từ-chối được HĐ
|
||
bất kỳ**, kể cả HĐ mình không có quyền XEM.
|
||
|
||
Đây **KHÔNG phải lỗi W6** (0-diff, anh chốt ĐỂ MỞ) — nhưng W6 vừa mở màn Danh sách/Chi tiết cho một
|
||
tập người dùng rộng hơn nhiều, tức **làm việc lấy `contractId` trở nên dễ**. Bán kính nổ của lỗ cũ
|
||
tăng theo. Không chặn commit; ghi ở đây để khi đóng O-A thì biết nó đã rộng ra từ lúc nào.
|
||
→ **MAJOR (pre-existing, amplified — không yêu cầu sửa trong W6)**.
|
||
|
||
### 3.7 Hệ quả phụ của việc thêm `Procurement` (spec-sanctioned, cần nói rõ)
|
||
|
||
`Procurement` là vế **theo ROLE**, không kèm quan hệ với HĐ ⇒ một NV Mua hàng **trình được MỌI HĐ**
|
||
đang ở Nháp/TraLai, kể cả HĐ người khác soạn. Cùng lớp với `Drafter`/`DeptManager` vốn đã vậy
|
||
(pre-existing), và spec §②-5 xin đúng chữ đó. Rào tự nhiên còn lại: Procurement **không thấy** HĐ
|
||
Nháp của người khác (`GetEligiblePhases:354-355` chỉ phát `DangSoanThao` cho Drafter/DeptManager;
|
||
vế V2 mới loại Nháp) ⇒ khó lấy id. Hành vi có log (`LogTransitionAsync` ghi `actorUserId`).
|
||
→ **MINOR**.
|
||
|
||
**TRỤC 3 = PASS** (0 critical trong phạm vi W6). 1 MAJOR pre-existing-amplified + 1 MINOR + 2 INFO.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## TRỤC 4-7 + VERDICT — [LEAD đóng on-behalf @S161: vai chết #53 sau Trục-3]
|
||
|
||
**TRỤC 4 — fidelity Resolve mirror: ĐẠT** (hành vi khoá bởi 7 test pass — OR-of-N/con-trỏ đôi qua Inbox_Pending/OtherLevel; trục-1 đã xác nhận per-HĐ AnyAsync đúng workflowId).
|
||
**TRỤC 5 — test-integrity: ĐẠT** — 7 test = 5 PIN đúng NGUYÊN VĂN + 2 bonus đúng đặt hàng (`Inbox_Admin_SeesChoDuyetContract` DR-4 · `Submit_ProcurementAndCreator_Allowed_UnrelatedRoleAndNullActor_Forbidden` khoá cả 4-vế guard + null-trap trong 1 test). Suite 587/0 lead-tươi.
|
||
**TRỤC 6-7 — boundary: ĐẠT** — CWS diff CHỈ khối guard :73-78 (Reject :49-66 + ApproveV2Async :217-394 KHÔNG đụng) · KHKK module + Migrations 0-diff.
|
||
|
||
**VERDICT: PASS_WITH_FLAGS — 0 chặn.** 2 ghi-nhận chuyển owner (KHÔNG phải lỗi W6):
|
||
1. **Độ-rộng view = "workflow-blanket by design"** (trục-1.3): vế V2 là per-HĐ-THEO-QUY-TRÌNH (có chân trong quy trình ⇒ thấy MỌI HĐ pin quy trình đó), không phải per-HĐ-được-giao — đúng chữ spec, nhưng owner cần biết độ rộng khi số HĐ tăng.
|
||
2. **Reject :49-66 pre-existing AMPLIFIED** (trục-3.6): W6 mở list/detail rộng hơn ⇒ lấy `contractId` dễ hơn ⇒ bán kính nổ của lỗ O-A-để-mở TĂNG. Ghi mốc để khi đóng O-A biết nó rộng ra từ đây.
|
||
|
||
<!-- END sub-reviewer-2 · TOTAL=0 chặn + 2 ghi-nhận-owner + (1 MAJOR-preexisting-amplified · ~8 MINOR/INFO trong 3 trục vai) -->
|