Files
solution-erp/.claude/workflows/runs/2026-08-08-S182-khkk-awf-mirror-ncc/sub-reviewer-precommit.md
2026-08-08 02:24:47 +07:00

26 KiB
Raw Blame History

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 (grep KHKK-N trong AdminFeatures.cs chỉ ra ở comment + gọi ParseGroup cho D5, không ở vị-ngữ W1a).
  • Comment :339 cũ ("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 else giữ nguyên byte semantics per-type cho mọi loại khác.
  • Ăn khớp với nextVersion :336-338 vố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.cspublic static classApplication (đúng F1 của review-1: không nhét vào Scope internal, không InternalsVisibleTo, 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)
  • Semantics cho-qua code ngoài họ GIỮ NGUYÊN: ParseGroup trả nullif (wfGroup is int k && ...) không vào ⇒ pass. Ghi chú :49-57 nê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ên LoadStepsAsync(awId, ct) :199. Cũng nằm sau guard phase :160 và sau plan.ApprovalWorkflowId is not Guid awId :163awId luô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 TRONG if (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 (verified ContractSigningPlanChangelog.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 != TuChoiTraLai KHÔ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/actorName hoist 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 grep HasQueryFilter toàn src/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-1285 HELD 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.
  • levelIds lấy qua l.Step!.ApprovalWorkflowId == def.Id + IgnoreQueryFilters().
  • IgnoreQueryFilters() trên 8 bảng opinion = no-op (không bảng nào có HasQueryFilter; ContractSigningPlanLevelOpinion.cs:18 khai rõ "CỐ Ý KHÔNG"). Vô hại + đồng hình, đúng như comment nói. Và KHÔNG lọc !IsDeleted là ĐÚNG cho câu hỏi FK (khác CountLiveOpinionsAsync :969 hỏ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) thay KeyNotFoundException (:1275). T13 có control dương.

(iv) D5

  • Chỉ áp cho họ, qua ContractSigningPlanWorkflowGuards.ParseGroup(def.Code) is int group :1337dù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 allowApproverFinalize0-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ôngdisabled, khôngtitle, 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_PLANisKhkk. 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 levelFlagsPayload1 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-127fe-user/.../KhkkCreatePage.tsx:125-127).
  • 🔴 isActive CÓ THẬT trong DTO BE — đây là chỗ dễ vỡ nhất (nếu BE không trả, w.isActive = undefined ⇒ falsy ⇒ autoWorkflow LUÔN null ⇒ chết đường tạo phiếu KHKK, mà tsc + build + 659 test đều xanh). Đo: AwDefinitionDto nhận d.IsActiveApprovalWorkflowV2AdminFeatures.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 isUserSelectable client-side ở :109 vẫ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 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 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?đú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

  1. 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ũ.
  2. .claude/governance/.session-counter.json (+54/53) — tick S182 của lead. Không liên quan YC-013. → xem MINOR-2 (BOM).
  3. docs/governance/so-yeu-cau-chu-du-an.md (+2) — thêm dòng YC-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 buildtsc -b (build mode) đ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"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ài citation-trap gen-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-38ApprovalWorkflowV2AdminFeatures.cs:357KhkkCreatePage.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 ghiCode = 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 T1T14 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.json bị 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ẫy BOM-json đã ghi trong feedback_resume_premise_reverify, và nó vào commit kèm theo code mà không ai chủ ý. Đề nghị: git add từng file cụ thể (không -A), hoặc tách thành commit wal:/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òng KHKK-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 bỏ-active).
  • MINOR-6 — EnsureWorkflowGroupMatchAsync cho qua khi awId không tồn tại. Có khai chủ đích :55-57 và ở Create/UpdateDraft thì EnsureWorkflowTypeAsync chạy trước nên vô hại. Ở Submit không có rào type đứng trước — nhưng ContractSigningPlans.ApprovalWorkflowIdFK Restrict (ContractSigningPlanConfiguration.cs:50-53) nên awId luô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á)

  1. 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 class riêng; (b) IgnoreQueryFilters cho 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.
  2. 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.
  3. 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.
  4. key={typeCode ?? 'all'} trên TypePanel (: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ỗ.
  5. 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.