26 KiB
sub-reviewer-precommit — S182 YC-013 đợt-2 (PRE-COMMIT, diff thật)
VERDICT: PASS_WITH_FIXES — 0C / 3M (+ 6 minor). Không có blocker. 3 MAJOR đều propose-only, không chặn commit nếu lead chấp nhận rủi ro có khai.
0. Diff scope (đo tươi, KHÔNG chép từ lane)
git status --porcelain + git diff --stat:
| Nhóm | File | Ghi chú |
|---|---|---|
| BE | ApprovalWorkflowV2AdminFeatures.cs (+184) · ContractSigningPlanFeatures.cs (+55) · ContractSigningPlanWorkflowService.cs (+10) · ContractSigningPlanWorkflowGuards.cs (untracked ??, 72 dòng) |
4/4 khớp spec |
| FE | ApprovalWorkflowsV2Page.tsx (+213) · menuKeys.ts ×2 · KhkkCreatePage.tsx ×2 |
5/5 khớp spec |
| Test | UpdateAwDefinitionTests.cs (+567) · ContractSigningPlanApprovalTests.cs (+326) · ContractSigningPlanGroupAndCatalogTests.cs (+14) |
3 file, spec/lệnh khai 2 → mục 9 |
| Ngoài scope | .claude/governance/.session-counter.json (+54/-53) · docs/governance/so-yeu-cau-chu-du-an.md (+2) |
→ mục 9 |
Tổng tracked: +1400 / −106, 13 file M + 1 file ??.
Máy — đo LẠI độc lập (không nhận số của lane)
| Phép đo | Lệnh chạy | Kết quả |
|---|---|---|
| Domain tests | dotnet test SolutionErp.slnx |
45 P / 0 F |
| Infrastructure tests | (cùng lệnh) | 614 P / 0 F |
| Tổng | 659 P / 0 F — khớp claim; baseline 645 (45+600) ⇒ +14 đúng ở Infrastructure | |
npm run build fe-admin |
exit 0 | ✓ built 15.36s |
npm run build fe-user |
exit 0 | ✓ built 1.04s |
| tsc fe-admin | npx tsc --noEmit -p tsconfig.app.json |
0 err (225 file) |
| tsc fe-user | npx tsc --noEmit -p tsconfig.app.json |
0 err (214 file) |
1. W1a — công thức R-6 · HELD
ApprovalWorkflowV2AdminFeatures.cs:355-361:
var actives = typeEnum == ApprovalWorkflowApplicableType.ContractSigningPlan
? await db.ApprovalWorkflows
.Where(w => w.Code == request.Code && w.ApplicableType == typeEnum && w.IsActive)
.ToListAsync(ct)
: await db.ApprovalWorkflows
.Where(w => w.ApplicableType == typeEnum && w.IsActive)
.ToListAsync(ct);
- ✅ Đúng 3 vế R-6, đúng thứ tự, KHÔNG có biến-thể regex
^KHKK-N[1-8]$ở site này (grepKHKK-NtrongAdminFeatures.cschỉ ra ở comment + gọiParseGroupcho D5, không ở vị-ngữ W1a). - ✅ Comment
:339cũ ("only ONE active per type") đã bị xoá, thay bằng khối 20 dòng nêu cả 2 nhánh + cấm regex. Không còn neo thối. - ✅ Nhánh
elsegiữ nguyên byte semantics per-type cho mọi loại khác. - ✅ Ăn khớp với
nextVersion :336-338vốn đã per-Code (Where(w => w.Code == request.Code)), nên 2 vế cùng một trục.
2. W1b — Guards 3-site · HELD
- ✅ File MỚI
ContractSigningPlanWorkflowGuards.cs—public static classở Application (đúng F1 của review-1: không nhét vàoScopeinternal, khôngInternalsVisibleTo, không inline-duplicate). Đây đúng là finding "spec-unbuildable vì assembly-ranh" đợt-1 và nó đã được đóng đúng cách. - ✅ 3 call-site, đối số đúng thứ tự ở cả 3 (tôi đối chiếu tay từng site):
- Create —
ContractSigningPlanFeatures.cs:516-517→(db, request.ApprovalWorkflowId, approvalGroup, ct) - UpdateDraft —
:643-644→(db, awId, plan.ApprovalGroup, ct) - Submit —
ContractSigningPlanWorkflowService.cs:196-197→(db, awId, plan.ApprovalGroup, ct)
- Create —
- ✅ Semantics cho-qua code ngoài họ GIỮ NGUYÊN:
ParseGrouptrảnull⇒if (wfGroup is int k && ...)không vào ⇒ pass. Ghi chú:49-57nêu đúng lý do gốc:477-478. - ✅ Điểm chèn Submit: sau 2 rào hạng-mục (
:177-187), ngay trênLoadStepsAsync(awId, ct):199. Cũng nằm sau guard phase:160và sauplan.ApprovalWorkflowId is not Guid awId:163⇒awIdluôn hợp lệ, không có nhánh nào bỏ qua. - ✅ Thân rào cũ ở Create đã xoá hẳn (không để 2 bản luật song song).
3. W1b2 / D6 · HELD
- ✅
EnsureWorkflowPinnableAsync(ContractSigningPlanFeatures.cs:331-353) chỉ có 2 caller: Create:507, UpdateDraft:642(grep toàn repo — 3 hit gồm 1 định-nghĩa). - ✅ UpdateDraft: cả 3 rào (
Type/Pinnable/GroupMatch) nằm TRONGif (request.ApprovalWorkflowId is Guid awId && awId != plan.ApprovalWorkflowId):638. Đúng D6 — giữ-nguyên-pin không bị hỏi. T7 chân (4) đo đúng vế này. - ✅ 2 message TÁCH (
:346-352): "đã ngừng hoạt động" ⟂ "chưa được ghim cho người dùng chọn". Không gộp!IsActive || !IsUserSelectable. - ✅ Không gọi ở đường Submit — đúng "D6 chốt 2 site".
4. W1c — changelog shape · HELD
AdminFeatures.cs:916-955:
- ✅
EntityType = ContractSigningPlanEntityType.Workflow— enum =4 (verifiedContractSigningPlanChangelog.cs:12), slot có sẵn, không đẻ slot mới. - ✅
Action = ChangelogAction.Update·EntityId = def.Id·PhaseAtChange = plan.Phase·UserId·UserName·Summary·ContextNote— đủ 8 field mirror khối PE. - ✅ Filter:
ApprovalWorkflowId == def.Id && Phase != DaDuyet && Phase != TuChoi—TraLaiKHÔNG bị loại (đúng spec: phiếu trả lại sẽ trình lại). - ✅ CỐ Ý không
IgnoreQueryFilters()ở đây (ngược với DELETE) — có khai lý do tại chỗ:930-934. Đây là điểm mà đợt-1 tôi FAIL, nay đã được phân biệt đúng 2 chiều (changelog = ngữ nghĩa "sống"; DELETE = ngữ nghĩa "FK còn thật"). - ✅
note/summary/actorNamehoist ra ngoài 2 nhánh — không đổi hành vi (chỉ string op), khối PE dùng lại y hệt giá trị cũ. - ✅ Ghi nằm trước
await db.SaveChangesAsync(ct):958+tx.CommitAsync(ct):962⇒ atomic cùng lượt PUT. - ✅ Câu cũ xếp KHKK vào rọ "chỉ có UpdatedAt/UpdatedBy" đã gỡ (
:875-882) — không để lại neo thối.
5. W1d — usage-count + D5 · HELD (2 minor)
(i) pin-count 8 module — đếm ĐÚNG 8, không thiếu không thừa
Tôi grep Domain thay vì tin comment:
grep -E "public (Guid|Guid\?) ApprovalWorkflowId" src/Backend/SolutionErp.Domain
→ Contract · ContractSigningPlan · LeaveRequest · OtRequest · Proposal
· TravelRequest · VehicleBooking · PurchaseEvaluation = 8
(+ ApprovalWorkflow.cs:76 = FK nội bộ của Step, không phải module)
- ✅
IgnoreQueryFilters()áp đúng PE / Contract / KHKK (:1287-1292). Tôi grepHasQueryFiltertoànsrc/Backend: trong 8 module pin, chỉ 3 entity đó có global filter (ContractConfiguration.cs:44·ContractSigningPlanConfiguration.cs:55·PurchaseEvaluationConfiguration.cs:84). 5 module Office không có ⇒ đếm thẳng là đúng. Claim trong comment:1281-1285HELD bằng phép đo độc lập. - ✅ Đây chính là finding 🔴 đợt-1 của tôi ("usage-count trên bảng có HasQueryFilter thiếu IgnoreQueryFilters") — đã đóng, và T11 đo đúng cái bẫy đó.
(ii) opinions 8 bảng
- ✅ Đủ 8 (
:1300-1319): PE · Contract · ContractSigningPlan · Proposal · LeaveRequest · OtRequest · TravelRequest · VehicleBooking. - ✅
levelIdslấy qual.Step!.ApprovalWorkflowId == def.Id+IgnoreQueryFilters(). - ✅
IgnoreQueryFilters()trên 8 bảng opinion = no-op (không bảng nào cóHasQueryFilter;ContractSigningPlanLevelOpinion.cs:18khai rõ "CỐ Ý KHÔNG"). Vô hại + đồng hình, đúng như comment nói. Và KHÔNG lọc!IsDeletedlà ĐÚNG cho câu hỏi FK (khácCountLiveOpinionsAsync :969hỏi câu khác — "chữ ký còn hiệu lực"). Hai hàm hỏi 2 câu khác nhau, không phải mâu thuẫn.
(iii) 404
- ✅
throw new NotFoundException("ApprovalWorkflow", request.Id)thayKeyNotFoundException(:1275). T13 có control dương.
(iv) D5
- ✅ Chỉ áp cho họ, qua
ContractSigningPlanWorkflowGuards.ParseGroup(def.Code) is int group:1337— dùng lại đúng 1 nguồn regex, không chép chuỗi lần 3. - ✅ Thứ tự: usage (i+ii) TRƯỚC, D5 SAU — khai lý do
:1259-1261.
6. W2d — danh-sách-chốt 6 ô · HELD trên trục dữ-liệu, hở trên trục hiển-thị (→ MAJOR-1)
🔴 #8 allowApproverFinalize — 0-CHẠM, chứng minh bằng grep trên DIFF
git diff -- fe-admin/.../ApprovalWorkflowsV2Page.tsx | grep "allowApproverFinalize"
→ exit 1 (0 hit)
Control dương: grep trên file ra 7 hit (:57 :123 :217 :238 :267 :1456 :1457) ⇒ lưới grep có răng, và ô #8 ở :1456 không có disabled, không có title, checked={entry.allowApproverFinalize} nguyên vẹn. Prod 8/88 cờ chịu lực AN TOÀN.
#7 allowApproverSkipToFinal — giữ nguyên hành vi
Diff chỉ đổi 3 biểu thức trùng lặp applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN → isKhkk. isKhkk định nghĩa :715 = cùng biểu thức, cùng scope, cùng prop. Giá trị y hệt. checked={isKhkk ? false : ...} giữ ép-false (đúng spec "GIỮ NGUYÊN").
levelFlagsPayload — 0-chạm
git diff | grep levelFlagsPayload → 1 hit duy nhất, nằm trong COMMENT (:486 của diff). Payload không đổi ⇒ ô mờ vẫn echo state thật lên BE.
6 ô disabled — GIỮ state thật, CẤM ép false: tuân thủ
| # | Cờ | Dòng | checked |
disabled |
tooltip |
|---|---|---|---|---|---|
| 1 | allowReturnOneLevel |
1358-1362 | entry.… ✅ |
isKhkk |
chung |
| 2 | allowReturnOneStep |
1369-1373 | entry.… ✅ |
isKhkk |
chung |
| 3 | allowReturnToAssignee |
1380-1384 | entry.… ✅ |
isKhkk |
chung |
| 5 | allowApproverEditDetails |
1407-1411 | entry.… ✅ |
isKhkk |
chung |
| 6 | allowApproverEditBudget |
1418-1422 | entry.… ✅ |
isKhkk |
chung |
| 9 | allowApproverDelete |
1468-1472 | entry.… ✅ |
isKhkk |
chung |
Không ô nào bị checked={false}. ✅ ERRATA D3 (state-thật, không forced-true) được thi hành đúng ở ô #4 :1396-1400: checked={entry.allowReturnToDrafter} + disabled + tooltip RIÊNG + label KHÔNG bị text-slate-400 (đúng ý "không mờ kiểu chưa-có").
Ô Ngưỡng CEO (W2f)
- ✅
disabled={isEdit || isKhkk}:1052— vô điều kiện cho type-10. - ✅ POST gửi
ceoApprovalThreshold: isKhkk ? null : …:889-891. - ✅ helper-text riêng
:1057-1058.
7. W2g — find(w => w.isActive && …) · HELD (hợp-đồng FE↔BE đã đo)
- ✅ 2 app mirror LOGIC — 2 diff byte-identical ở phần code (
fe-admin/.../KhkkCreatePage.tsx:125-127⟂fe-user/.../KhkkCreatePage.tsx:125-127). - ✅ 🔴
isActiveCÓ THẬT trong DTO BE — đây là chỗ dễ vỡ nhất (nếu BE không trả,w.isActive=undefined⇒ falsy ⇒autoWorkflowLUÔN null ⇒ chết đường tạo phiếu KHKK, mà tsc + build + 659 test đều xanh). Đo:AwDefinitionDtonhậnd.IsActiveởApprovalWorkflowV2AdminFeatures.cs:178(đối số thứ 8), serialize camelCase ⇒isActive. Hợp đồng khớp. - ✅
history= FULL list kể cả active — verified:205-213:versions= mọi definition của type,active=versions.FirstOrDefault(v => v.IsActive). Claim của cả W2b lẫn W2g HELD. - ✅ Filter
isUserSelectableclient-side ở:109vẫn giữ.
8. 14 test — đúng 14 ca spec, KHÔNG có ca nào nới để xanh
| Ca | File | Nhận xét đối-kháng |
|---|---|---|
| T1 | UpdateAwDefinitionTests.cs |
Có tiền-đề đo (Be(8) trước khi act) — chống "xanh ở thế giới sai" |
| T2 | ″ | Regression type-1 + chứng-nhân xuyên-type |
| T3 | ″ | 2 chân: (a) code lạ không nuke 8 nhóm; (b) code lạ vẫn ratchet chính nó ⇒ giết được cả bản vá regex lẫn bản vá "type-10 đừng tắt gì" |
| T4 | ContractSigningPlanApprovalTests.cs |
3 vế: chặn + đĩa bất động (ReadPinAsync từ context MỚI + changelog=0) + control dương |
| T5 | ″ | Seed phiếu pin lệch thẳng DbContext ✅ + chứng-nhân phiếu đúng nhóm submit LỌT cùng lượt; fixture kéo hạng-mục theo approvalGroup ⇒ loại 2 cửa 409 khác |
| T6 | ″ | Vế âm + chân (2) chứng minh rào đang sống (không phải xanh-mù) |
| T7 | ″ | 4 chân, đủ cả (4) giữ-nguyên-pin-vẫn-sửa-được |
| T8 | UpdateAwDefinitionTests.cs |
Đo SHAPE 9 field + sanity changes ≠ rỗng + assert PE changelog = 0 (không rải chéo module) |
| T9 | ″ | Control dương TRONG CÙNG LƯỢT (4 phiếu: 1 sống → có vết; DaDuyet/TuChoi/xoá-mềm → không) |
| T10 | ″ | Control dương + vi-sai message (1 vs 2 hồ sơ ⇒ câu phải khác — giết "câu hằng") + chứng-nhân DbUpdateException |
| T11 | ″ | Có tiền-đề 2 chiều: filter-on = 0, filter-off = 1 ⇒ đo đúng cái bẫy F2 |
| T12 | ″ | Seed LeaveRequest thật ✅ + chứng-nhân xoá thẳng module 2 ⇒ dangling THẬT trong DB |
| T13 | ″ | Control dương id thật |
| T14 | ″ | 3 chân (không phải 2): (1) không-phải-cuối ⇒ OK · (2) cuối ⇒ 409 · (3) ngoài họ 1-version ⇒ OK. Tiền-đề 0 usage tách bạch D5 với rào (i) |
Đếm: 10 [Fact] mới ở UpdateAwDefinitionTests + 4 ở ContractSigningPlanApprovalTests = 14 ⟂ suite +14 (600→614). Khớp 2 nguồn.
Có test nào bị NỚI không? Có đúng 1 assertion đổi, ở file thứ 3 (ContractSigningPlanGroupAndCatalogTests.cs:858-868):
Contain("KHKK-N5") → Contain("N5"). Đây là spec-change bắt buộc, không phải nới-để-xanh: message mới do W1b chốt bỏ mã, giữ số nhóm; câu cũ sẽ ĐỎ. Có ghi lý do 8 dòng tại chỗ. Chấp nhận — nhưng xem MINOR-1 (assertion mù thứ-tự).
9. Residual — 3 khoản NGOÀI danh sách
tests/…/ContractSigningPlanGroupAndCatalogTests.cs(+14/−6) — file test thứ 3, lệnh chỉ khai 2. Chính-đáng (hệ quả tất yếu của đổi message W1b), nhưng phải khai trong commit-body, đừng để người đọc log sau này tưởng có ai lén sửa test cũ..claude/governance/.session-counter.json(+54/−53) — tick S182 của lead. Không liên quan YC-013. → xem MINOR-2 (BOM).docs/governance/so-yeu-cau-chu-du-an.md(+2) — thêm dòngYC-013+YC-014. Đúng luật sổ (GHI NGAY), không liên quan code.
#8 / levelFlagsPayload / SkipToFinal có bị đụng ngoài ý không? — KHÔNG. Đã grep trên diff (không phải trên file) cho cả 3, kết quả ở mục 6.
10. FLAG-A của FE lane — CLAIM ĐÚNG, và tôi đo được
| Lệnh | fe-admin | fe-user |
|---|---|---|
npx tsc --noEmit (nguyên văn acceptance cũ) |
exit 0 — --listFiles = 0 dòng |
exit 0 — --listFiles = 0 dòng |
npx tsc --noEmit -p tsconfig.app.json |
exit 0 — --listFiles = 225 dòng |
exit 0 — --listFiles = 214 dòng |
fe-admin/tsconfig.json = {"files": [], "references": [...]} ⇒ dạng trần biên dịch 0 file, luôn xanh theo cấu trúc, không phải may mắn. FLAG-A HELD.
🔸 Bổ sung có ích cho lane: npm run build = tsc -b && vite build — tsc -b (build mode) CÓ đi theo project references ⇒ acceptance npm run build là thước THẬT. Chỉ acceptance npx tsc --noEmit trần là rỗng. Đề nghị: mọi acceptance FE về sau ghi npm run build hoặc -p tsconfig.app.json, và kèm --listFiles | wc -l một lần để chứng thước không rỗng.
FINDINGS
🔴 MAJOR-1 — tooltip gắn trên <input disabled>: Chrome/Edge không hiện ⇒ nửa D3=C có thể chết câm
Vị trí: fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx:1361, 1372, 1383, **1399**, 1410, 1421, 1471 (và tiền lệ cũ :1443).
title được đặt trên chính <input type="checkbox" disabled={isKhkk}>. Trình duyệt Chromium (Chrome/Edge — đúng trình duyệt đích, xem feedback_hosolink_open_from_web_browser_block) chặn sự kiện chuột trên form-control bị disabled, nên native tooltip thường không bật lên; Firefox thì có. <label> bao ngoài KHÔNG mang title, nên rê chuột vào phần chữ cũng không ra gì.
Vì sao là MAJOR chứ không phải nit: D3=C của anh là "mờ ô chưa nghe + tooltip". Nếu tooltip không hiện thì admin chỉ thấy 7 ô xám không lý do — đúng thứ mà rule-box :1088-1096 cố tránh. Nặng nhất là ô #4 :1399: câu "KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh" là thông tin DUY NHẤT phân biệt ô #4 với 6 ô kia (rule-box chỉ nói #4 "có tác dụng thật", không nói "cờ chưa phân nhánh"). Tooltip không hiện ⇒ ô #4 trông y hệt 6 ô "chưa áp dụng", tức ngược nghĩa — đúng class lỗi mà ERRATA D3 vừa sửa xong ở tầng checked.
Đây là máy dựng mà không có cửa gọi (feedback_guard_built_but_never_called).
Acceptance đề nghị (chọn 1):
- (a) chuyển
title=từ<input>lên<label>bao ngoài — label không disabled nên tooltip sống; hoặc - (b) render câu của ô #4 thành chữ nhìn thấy được (một dòng
text-[10px]dưới ô), giữ tooltip cho 6 ô kia. - Kiểm chứng: mở dialog nhóm bất kỳ trên Chrome, rê chuột vào chữ của ô #4 → phải đọc được câu riêng.
- 🔸 Tiền lệ
:1443(#7, có từ K3) cùng bệnh — vá thì vá cùng lớp (bàicitation-trapgen-2: vá 1 CLASS phải grep MỌI matcher cùng-lớp TRONG diff).
🔴 MAJOR-2 — 3 thành phần bất đồng về HOA/thường của Code; test mù theo CẤU TRÚC (SQLite ≠ SQL Server)
Vị trí: ContractSigningPlanWorkflowGuards.cs:37-38 ⟂ ApprovalWorkflowV2AdminFeatures.cs:357 ⟂ KhkkCreatePage.tsx:126 (×2 app).
Ba chỗ hỏi cùng một câu "code này có phải nhóm N{n} không", bằng ba luật khác nhau:
| Chỗ | Phép so | Với khkk-n1 (thường) |
|---|---|---|
GroupCodePattern = new(@"^KHKK-N([1-8])$", CultureInvariant) — KHÔNG IgnoreCase |
case-SENSITIVE | → null = ngoài họ ⇒ rào nhóm 3-site TẮT, D5 TẮT |
w.Code == request.Code (EF → SQL Server) |
theo collation DB, mặc định *_CI_AS = case-INSENSITIVE |
→ khớp KHKK-N1 ⇒ per-Code vẫn hạ IsActive bản thật |
(w.code ?? '').trim().toUpperCase() === 'KHKK-N1' |
case-INSENSITIVE | → khớp ⇒ FE auto-pin nó cho phiếu nhóm 1 |
Hệ quả cụ thể: admin gõ khkk-n1 trên panel type-10 gộp (ô Mã ở panel đó không khoá — lockedCode = null) ⇒ bản mới tắt KHKK-N1 thật, FE vẫn auto-pin sang nó, còn rào nhóm + D5 im lặng tắt cho toàn bộ phiếu nhóm 1. Không có lỗi nào báo.
Vì sao test không thấy: 14 ca chạy trên SQLite (SqliteDbFixture), nơi = trên TEXT mặc định là BINARY = case-sensitive. Tức phép so ở tầng DB trong test có ngữ nghĩa NGƯỢC với prod. Đây không phải test viết ẩu — nó mù theo cấu trúc, không assertion nào bịt được.
Acceptance đề nghị: chuẩn hoá tại đường ghi — Code = request.Code.Trim().ToUpperInvariant() trong CreateAwDefinitionCommandHandler (và dùng cùng giá trị đó cho nextVersion + actives), hoặc thêm RegexOptions.IgnoreCase cho GroupCodePattern + khai rõ "W1a phụ thuộc collation" ngay tại :350. Kèm 1 ca test khkk-n1 để bất-biến này có chứng nhân (dù SQLite, nó vẫn bắt được vế regex).
🔴 MAJOR-3 — rào D6 ở đường TẠO có 0 test; T7 chỉ phủ đường SỬA NHÁP
Vị trí: ContractSigningPlanFeatures.cs:507 (EnsureWorkflowPinnableAsync tại Create).
Spec W1b2 nói rõ 2 site (Create + UpdateDraft). Danh sách T1–T14 chỉ có T7, và T7 gọi UpdateDraftAsync ở cả 4 chân — không chân nào đi qua Create. Vậy một guard hard-409 mới cắm vào write-path chính của prod (mọi phiếu KHKK đều sinh ra qua đây) đang có 0 răng đo.
Rủi ro cụ thể: nếu ai đó về sau dời EnsureWorkflowPinnableAsync ra khỏi Create, hoặc đảo nó lên trước EnsureWorkflowTypeAsync (làm 404 đổi thành 409), hoặc gỡ nó khi refactor — 659 test vẫn xanh. Và chiều ngược cũng không được đo: rào này có thể đang chặn oan một luồng tạo hợp lệ nào đó mà chưa ai chạm tới.
Ghi chú công bằng: đây là lỗ của SPEC (spec liệt 14 ca và thiếu ca này), implementer làm đúng spec. Nhưng luật §7 nói "New endpoint / guard mới ở write-path → test", và tôi không hạ thước theo spec.
Acceptance đề nghị: thêm 1 ca vào ContractSigningPlanGroupAndCatalogTests (nơi đã có sẵn CreateAsync helper + seed PE): Create pin workflow IsActive=false ⇒ 409 · pin IsUserSelectable=false ⇒ 409 message khác · control dương pin bản active+ghim ⇒ tạo được và không đốt mã (ReadPlanScopesAsync rỗng ở 2 ca đầu — khuôn đã có ở :869).
MINOR
- MINOR-1 — assertion mù THỨ TỰ toán hạng.
Contain("N5").And.Contain("N3")(GroupAndCatalogTests:865,ApprovalTests:188+:246) vẫn xanh nếu ai đó đảo 2 đối số ở call-site (EnsureWorkflowGroupMatchAsync(db, awId, approvalGroup, ct)— 2 tham số kề nhau, 3 call-site). Tôi đã đối chiếu tay: cả 3 site hiện ĐÚNG. Nhưng lưới không giữ được bất-biến đó. Đề nghị pin nguyên câu có thứ tự:Contain("Phiếu nhóm N3 đang gắn quy trình N5")ở ít nhất 1 ca. - MINOR-2 —
.session-counter.jsonbị thêm BOM + đổi sang CRLF. HEAD:7b 0a({+LF). Worktree:ef bb bf 7b 0d 0a(BOM +{+ CRLF). CRLF sẽ được git normalize, BOM thì được commit. Hiện chưa có script nào parse file này (.claude/hooks/chỉ cówal-flush.ps1, không đọc), nên rủi ro thấp — nhưng đây đúng là bẫyBOM-jsonđã ghi trongfeedback_resume_premise_reverify, và nó vào commit kèm theo code mà không ai chủ ý. Đề nghị:git addtừng file cụ thể (không-A), hoặc tách thành commitwal:/governance riêng. - MINOR-3 — D5 đếm anh-em KHÔNG lọc
ApplicableType.AdminFeatures.cs:1339-1340:Count(w => w.Code == def.Code && w.Id != def.Id). Một dòngKHKK-N1ở type khác (UNIQUE là(Code, Version)nên chỉ cần Version khác) sẽ được tính là "anh em" và cho phép xoá bản type-10 cuối cùng. Ca hiếm, nhưng vá là 1 mệnh đề:&& w.ApplicableType == def.ApplicableType. - MINOR-4 — panel nhóm tạo version MỚI ⇒ 6 cờ khoá bị đóng băng ở DEFAULT của FE, không phải "state thật". Trên panel nhóm, "Tạo phiên bản mới" (không clone) khởi tạo entry từ
makeEmptyStep; 6 ôdisabled⇒ admin không thể sửa ⇒ POST luôn ghi đúng bộ mặc định FE. Với KHKK thì vô hại (BE không đọc 6 cờ đó), nhưng chữ "state THẬT" trong D3=C chỉ đúng cho đường CLONE/EDIT. Đề nghị: khai 1 dòng trong commit-body hoặc nợ-khai, đừng để lượt sau đọc "state thật" rồi suy sang ca tạo-mới. - MINOR-5 — bỏ ghim bản active = đóng cửa tạo phiếu nhóm (không có rào). Spec §54 đã khai; sau W2g còn chặt hơn (
isActive && isUserSelectable). Không đòi vá đợt này, chỉ xác nhận răng đã sắc hơn spec mô tả ⇒ khi đóng sổ nên ghi vào accepted-risk cho đủ 2 vế (bỏ-ghim và bỏ-active). - MINOR-6 —
EnsureWorkflowGroupMatchAsynccho qua khiawIdkhông tồn tại. Có khai chủ đích:55-57và ở Create/UpdateDraft thìEnsureWorkflowTypeAsyncchạy trước nên vô hại. Ở Submit không có rào type đứng trước — nhưngContractSigningPlans.ApprovalWorkflowIdcó FK Restrict (ContractSigningPlanConfiguration.cs:50-53) nênawIdluôn resolve. Ghi lại để lượt sau đừng gỡ FK mà quên chỗ này.
Điểm CHỊU ĐƯỢC soi (positive validation — ghi để lượt sau đừng phá)
- 3/3 finding 🔴 của tôi ở review đợt-1 đều được đóng ĐÚNG BẢN CHẤT, không phải đóng bằng chữ: (a) assembly-ranh → file
public static classriêng; (b)IgnoreQueryFilterscho bảng cóHasQueryFilter→ đúng 3 bảng, và phân biệt được 2 chiều (DELETE cần, changelog cố ý không); (c) FK-thật → grep 8 module, 5 module loose-0-FK được khai là "rào DUY NHẤT" + T12 chứng minh bằng dangling THẬT trong DB. - Mọi ca test đều có control dương CÙNG LƯỢT — T9/T10/T11/T13/T14 và cả T6 chân (2). Không có ca "vế âm xanh-mù" nào.
- T5 dựng lại đúng trạng thái không-thể-tạo-qua-API (seed thẳng DbContext) và loại trước 2 cửa 409 nhiễu bằng cách kéo hạng-mục theo
approvalGroup. Đây là chống "xanh vì nhầm cửa" ở mức tôi không bẻ được. key={typeCode ?? 'all'}trênTypePanel(:400) — remount khi đổi nhóm, chặn state-bleed giữa 8 panel dùng chung component. Chi tiết nhỏ nhưng đúng chỗ.- POST gửi
code: lockedCode ?? code(:882) chứ không tin ô input đãdisabled— bất-biến theo CẤU TRÚC, không theo kỷ luật UI.