Files
solution-erp/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/cicd-verify-998ea55.md
pqhuy1987 3a6eb92cda
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
[CLAUDE] Docs: S172 closeout — verdict 998ea55 PASS + bookend 5 vòng/11 sub + vá stale
Verdict sản phẩm: `998ea55` (KHKK 3-panel mirror Duyệt NCC) = PASS.
Test-gate #447 644/644 Failed 0 khớp baseline tách-phần Δ0 · bundle 4/4 rotate
khớp log CI tới byte · 2 endpoint MỚI 401 · mig repo 71 = prod 71 set-diff 0/0
hai chiều, tables 97 · smoke 8/8. Không rollback, prod khoẻ.

Chân #44/#85 đóng bằng đo — nấc cũ "credential UAT chết" SAI (tra nhầm account
đời cũ; bộ sống ở HANDOFF slot 64). Phạm vi = 2 tổ-hợp vai
(Drafter+Procurement · CostControl+DeptManager); CHƯA loại nhánh vai thường
khác vẫn 403 — siết 5 site/3 file sau ring2 ESCALATE-1.

Bookend-close 5 vòng / 11 sub → runs/2026-08-05-S172-bookend-close/:
H1 DRIFT 7 · H2 GATE-HOLD 6 · lead-stale 9 FLAG(SÀN)+1 ESCALATE ·
lead-gap 8 FLAG(3 HIGH) · ring1 67/69 · ring2 ĐẠT 17/17 ·
trio MIXED → 2 action/15 bác → MIXED-PASS 50/59 · ctx-audit TRUOT 3 FLAG.

Vá stale @closeout:
- STATUS:479 bundle hash (stale 2 phiên, lần 2 cùng ô) → Ajv-MaCz/YsXRkBSR
- STATUS:6 counter 42→46, deep 2/15→6/15; Recently Done S171-S172
- HANDOFF segment @S172: E-7 + 5 acceptance có nhà (trước đó 0 hit/6 sổ bền),
  carry re-stamp sau 4 phiên bỏ, 4 site neo tuyệt đối (1 site sai DẤU), slot 67-72
- skills/README ×2 ổ số cứng nằm cạnh chính con trỏ B1
- gotcha #87 (mã pre-auth 411/415 trả lời sai câu hỏi authz)
- run.md S171 hết mồ côi (0→4 hit) · 8 dir rỗng mis-land đã dọn
- MIND-3 neo xuất xứ sai 2 trường (ts tương lai + HEAD stale) — ctx-audit F-1

§L.c completeness-gate: vòng 4/5 (V4 không-nhịp) | phép ĐẠT 2 / TRƯỢT 2 / vacuous 0.
2 TRƯỢT cùng một bệnh: S169·S170·S171 chạy xong mà 0 session-log durable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:18:34 +07:00

26 KiB
Raw Blame History

cicd-verify — S171 KHKK 3-panel mirror Duyệt NCC (998ea55)

Lane: cicd-monitor (READ-ONLY / propose-only) · Spawn: 2026-08-04 Nguyên tắc ghi: khung rỗng lượt 1, append sau MỖI phép đo (bài học ngay trong phiên này: lane implementer-frontend ghi khung mà không append ⇒ chết ở lượt trả về ⇒ nhật-ký 642 B rỗng).

0. Đề bài PIN

Mục Giá trị
Commit đích 998ea55 [CLAUDE] Contract: KHKK 3-panel mirror Duyet NCC… (CODE — không docs-only)
HEAD sau push bf0200b ([CLAUDE] Docs: … — docs-only)
Range đã đẩy 4b1905d..bf0200b
Baseline Tests (STATUS.md B1) 644 PASS = 45 Domain + 599 Infra
Baseline Migrations (STATUS.md B1) 71 — đợt này KHÔNG có mig mới (enum +ApprovalAttachment=3 không đổi schema)

Flake đã biết (lead đo local TRƯỚC push): Domain 45/45 · Infra 598/599 — 1 FAIL = SolutionErp.Infrastructure.Tests.Application.CreatePeWorkflowDefinitionCommandHandlerTests.Create_DifferentEvaluationType_DoesNotAffect_Other. Bất-định đã chứng: full-suite 2→1→1, chạy riêng 0/0/0. Nghi hạ-tầng: PeWorkflowAdminTests.cs tạo new SqliteDbFixture() riêng ×6 test ⇒ flake dưới song-song xUnit. 🔴 Việc phải chốt: lead CHƯA đo được baseline trên HEAD sạch (git worktree fail vì đường-dẫn Dropbox) ⇒ "flake có TRƯỚC S171" hiện là SUY-LUẬN. CI runner sạch = đúng baseline đó ⇒ soi log ≥2 run TRƯỚC 998ea55.


1. Stage 1 — Push + path-filter ĐO 19:50

git log -1            → bf0200bc730bc40596500b6c01058b1ec6a63f5a  [CLAUDE] Docs: va 4 diem stale…
git log origin/main..HEAD → (rỗng)  ⇒ đã push đủ, không còn commit treo
git diff --name-only 998ea55^..998ea55 → 21 file

21 file của 998ea55 — 18 FE (fe-admin ×9 + fe-user ×9: components/khkk/*, components/pipeline/PipelineTreePanel.tsx, hooks/usePipelineStages.ts, pages/khkk/*, types/khkk.ts) + 3 BE (ContractSigningPlansController.cs, ContractSigningPlanFeatures.cs, ContractSigningPlanAttachment.cs).

Path-filter (gotcha #41) — 7-glob live trong .gitea/workflows/deploy.yml:19-26: docs/** · **/*.md · .claude/skills/** · .claude/agent-memory/** · .claude/workflows/runs/** · .gitignore · scripts/**.md. ⇒ 998ea55 KHÔNG khớp glob nào (toàn .tsx/.ts/.cs) ⇒ CI PHẢI chạy. đúng như đề bài PIN. ⇒ bf0200b (4 file: 2 .claude/agents/*.md + docs/HANDOFF.md + docs/STATUS.md) khớp **/*.md + docs/** ⇒ tự nó docs-only.

🔴 Nhưng CI keyed theo TIP của range, không theo từng commit (AS-6): Gitea đánh giá cả push-range, ≥1 commit có file non-ignored ⇒ toàn range build, và run mang head_sha = tip = bf0200b. ⇒ KHÔNG có run riêng mang sha 998ea55 — tra theo 998ea55 sẽ ra 0 hit và dễ kết luận nhầm "CI không chạy". Run phủ 998ea55 = run của bf0200b.

2. Stage 2 — Gitea Actions poll

Lượt 1 — 19:50:56. GET /api/v1/repos/vietreport-admin/solution-erp/actions/tasks?limit=6 (anon OK, repo public):

id run_number head_sha status created_at
560 #447 bf0200bc running 2026-08-04T19:49:08+07:00
559 #446 4b1905d5 success 2026-08-01T20:41:12
558 #445 ff7f6559 cancelled 2026-08-01T20:37:54
557 #444 0bfadc20 success 2026-08-01T17:08:56
556 #443 a2b37dcc success 2026-08-01T16:12:23
555 #442 628132cf success 2026-08-01T13:08:26

⇒ Run đích = #447 / id 560, head bf0200b, phủ 998ea55. Bắt đầu 19:49:08, đang chạy. Link: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/447

2b. 🔑 Chụp PRE kịp giờ (spawn 19:50 / deploy land ~19:55)

Đo TRƯỚC khi deploy đáp ⇒ có differential THẬT thay vì chỉ ảnh chụp cuối.

Đo PRE @19:50-19:51 Giá trị
admin bundle index-DLRf9TeA.js + index-IuRRquXK.css
eoffice bundle index-C7TGA4Kn.js + index-DesmJ6-_.css
GET …/{guid0}/changelogs (no token) 404
POST …/{guid0}/approval-attachments (bodyless, no token) 411 ⚠️ (IIS Length-Required, pre-auth — KHÔNG phân biệt được)
POST …/{guid0}/approval-attachments (có body {}, no token) 404 (thoát 411, đo được thật)
control ÂM POST …/{guid0}/khong-ton-tai-xyz (có body) 404
control DƯƠNG GET /api/contract-signing-plans (route KHKK cũ) 401

🔑 4 điểm mà bảng này khoá lại:

  1. Bundle PRE trùng khít baseline #444 (DLRf9TeA/C7TGA4Kn) ⇒ prod đứng im từ 08-01, chưa có gì của S171 lên.
  2. 2 route mới 404 PRE ⇒ chưa tồn tại ⇒ nếu POST-deploy thành 401 thì đó là wire-proof (route mới thật sự lên sóng), mạnh hơn chỉ nhìn "CI xanh".
  3. Control DƯƠNG 401 chứng phép-đo 401-vs-404 có răng — không phải mọi thứ ở host này đều trả 404.
  4. Bẫy 411 đã né: POST bodyless bị IIS chặn TRƯỚC routing ⇒ 411 vô nghĩa; phải gửi body mới đọc được 404/401.

3. Stage 3 — Test gate + phán quyết flaky vs hồi-quy ĐO 2026-08-05 (phiên nối)

Run #447 / id 560 — status=success, 19:49:08 → 19:56:33 = 7 phút 25 giây (conclusion=None là bình thường với endpoint tasks — tin status, xem MEMORY Stage 2).

Log web-UI /actions/runs/447/jobs/0/logs = 18 666 B, 2 dòng tổng kết (đối chiếu TÁCH-PHẦN, không chỉ khớp tổng):

12:49:38  Passed!  - Failed:     0, Passed:    45, Skipped:     0, Total:    45  — SolutionErp.Domain.Tests.dll (net10.0)   [167 ms]
12:52:51  Passed!  - Failed:     0, Passed:   599, Skipped:     0, Total:   599  — SolutionErp.Infrastructure.Tests.dll     [1 m 50 s]
Vế Baseline STATUS.md (B1) Run #447 Δ
Domain 45 45 0
Infrastructure 599 599 0
Tổng 644 644 0
Failed 0 0 0
Skipped 0 0 0

Δ = 0 đúng kỳ vọng: 998ea55 chạm 0 file test (§4.5) ⇒ không được phép có test mới. Số khớp tách-phần cả 2 project.

Grep phủ định trên log #447 (đếm thật, không suy): Failed! = 0 · [FAIL] = 0 · error CS = 0 · Create_DifferentEvaluationType_DoesNotAffect_Other = 0 hit.

3.1 🔴 Phán quyết flaky vs hồi-quy — GIỮ 2 VẾ TÁCH BẠCH, cấm nén thành 1 câu

  • Vế A — không có hồi-quy do S171. Đây là vế được chứng. Điều kiện "đủ" mà §4.3 treo lại chờ #447 nay đã đo: cây S171, trên runner sạch, Release, 2 step tách ⇒ 599/599 Failed 0. Cộng với #446 (cây không S171 ⇒ cũng 599/599) ⇒ cặp đối-chứng khép kín, delta đúng bằng 21 file của 998ea55. Không FAIL nào khác flaky đã biết ⇒ không có hồi-quy THẬT để báo động.
  • Vế B — vẫn KHÔNG chứng được "flake pre-exist". #447 là log CI thứ 4 liên tiếp có 0 hit tên test đó ⇒ tổng cộng 0/4 log CI từng tái hiện. Runner tiếp tục không nổ. Đây vẫn là vắng mặt bằng chứng, không phải bằng chứng vắng mặt. Nguyên trạng §4.3/§4.4: flake chỉ nổ ở local (2 project cùng lượt + máy nhiều tiến-trình), CI xanh không rửa sạch được nó và cũng không xác nhận được nó.

Test gate: PASS. Nợ kỹ-thuật PeWorkflowAdminTests.cs new SqliteDbFixture() ×6 vẫn treo, chưa ai chứng được nguồn gốc bằng CI.

4. Stage 3b — Baseline lịch-sử: flake có TRƯỚC S171 không? ĐO 19:52-19:54

4.1 Cô-lập delta: run #446 XANH ⟶ run #447, ở giữa đúng bằng 998ea55

git diff --name-only 4b1905d..bf0200b | (loại 7-glob paths-ignore)  →  21 file

21 file đó trùng khít 21 file của 998ea55. ⇒ Run #446 (4b1905d) và run #447 (bf0200b) khác nhau đúng một commit code. Đây là cặp đối-chứng sạch nhất có thể có, miễn phí — không cần git worktree (thứ đã fail vì đường-dẫn Dropbox).

4.2 Log CI các run TRƯỚC 998ea55 (web-UI /actions/runs/{n}/jobs/0/logs)

Run head_sha Domain Infra Dòng Failed!/[FAIL] Nhắc tên test flaky
#446 4b1905dbase ngay trước S171 45/45 599/599 0 0
#444 0bfadc2 45/45 599/599 0 0
#443 a2b37dc 45/45 594/594 0 0

Passed! - Failed: 0, Passed: 599, Skipped: 0 (log #446, 18 657 B).

4.3 🔴 Kết luận trung thực — CI KHÔNG chứng minh được "flake có sẵn"

Mệnh đề lead cần chốt: "flake này có TRƯỚC S171". CI trả lời được một nửa, và tao nói rõ nửa nào:

  • Bác được "hồi-quy tất định": run #446 chạy chính cái cây tiền-S171 trên runner sạch ⇒ Infra 599/599, Failed 0. Nếu S171 gây lỗi thì #446 (không có S171) phải xanh — nó xanh — nhưng đó là điều kiện cần, chưa đủ. Phần "đủ" nằm ở #447 (§3).
  • KHÔNG chứng được "flake pre-exist": chưa run CI nào từng tái hiện test này fail (0 hit tên test trong cả 3 log). Một flake không nổ trên runner thì log xanh không phải bằng chứng nó có sẵn — nó chỉ là vắng mặt bằng chứng. Ghi đúng như vậy, không nâng cấp thành "đã chốt".

4.4 Vì sao runner khó tái hiện — 3 khác biệt đo được (không suy đoán)

Trục Local (lead) CI (deploy.yml:64-80)
Phạm vi lệnh dotnet test SolutionErp.slnx (2 project cùng lượt) 2 step TÁCH: dotnet test tests/…Domain.Tests.csproj rồi …Infrastructure.Tests.csproj
Cấu hình mặc định (Debug) --configuration Release
Máy workstation, nhiều tiến-trình agent song song runner riêng

Mẫu-số chung của flake vẫn giữ nguyên 2 phía: PeWorkflowAdminTests.cs new SqliteDbFixture() × 6 (đếm grep -c = 6, khớp chẩn đoán của lead) ⇒ 6 SQLite riêng dựng/huỷ dưới song-song-collection của xUnit. Chạy 2 project cùng lượt (local) làm tăng tranh-chấp CPU/IO ⇒ hợp lý là local dễ nổ hơn CI, và đó cũng là lý do CI xanh không rửa sạch được flake.

4.5 Trục nhân-quả: S171 có chạm được vào test đó không?

  • git diff --name-only 4b1905d..bf0200b | grep -iE 'tests?/'0 file test bị chạm.
  • 3 file BE của 998ea55 đều thuộc ContractSigningPlans (KHKK); test flaky thuộc CreatePeWorkflowDefinitionCommandHandler (PE workflow) — hai module rời.
  • File chứa flake PeWorkflowAdminTests.cs sửa lần cuối 2026-05-08 (dbb0089), tức ~3 tháng trước S171.

⇒ Không có đường nhân-quả nào từ 998ea55 tới test đó.

4.6 Quét TOÀN BỘ 446 task lịch sử: CI đã bao giờ đỏ vì test này chưa?

API trả về 446 task (limit=30 nhưng server trả hết). Lọc status != success|running:

  • Kỷ nguyên gần (từ 2026-06-16 tới nay): DUY NHẤT 1 run failure = #291 (8c8179cd, 06-16). Còn lại chỉ là cancelled (7 lần — push-đè, gotcha #86).
  • Các failure khác đều nằm ở 04-2026/05-2026 (giai đoạn dựng pipeline: #1-#15, #29-#45, #85, #108-#111, #125-#127, #196/#197, #215).

Loại trừ #291 bằng ĐO, không bằng trí nhớ: log #291 (9 326 B) → grep -c tên test flaky = 0; Domain Passed! - Failed: 0, Passed: 45; nguyên nhân thật = error CS7036lỗi biên-dịch, chưa từng chạy tới test Infra. Không liên quan.

Chốt §4: trong toàn bộ lịch sử CI, Create_DifferentEvaluationType_DoesNotAffect_Other chưa từng làm đỏ một run nào.

5. Stage 4 — Ship thật? (FE bundle byte-level ×2 app) ĐO 2026-08-05 (phiên nối)

5.1 Hash PRE → POST: rotate cả 2 app, và khớp log CI tới từng byte

deploy.yml:111-122 build fe-admin trước, fe-user sau ⇒ ánh xạ 2 khối build trong log là xác định, không phải đoán.

App PRE (§2b, = baseline #444) POST (live index.html) Log CI #447 Content-Length Log CI (kB)
admin .js index-DLRf9TeA.js index-Ajv-MaCz.js dist/assets/index-Ajv-MaCz.js @12:54:41 1 783 465 1,783.46 kB
admin .css index-IuRRquXK.css index-Cd15r15R.css dist/assets/index-Cd15r15R.css 86 551 86.55 kB
eoffice .js index-C7TGA4Kn.js index-YsXRkBSR.js dist/assets/index-YsXRkBSR.js @12:55:53 1 701 224 1,701.22 kB
eoffice .css index-DesmJ6-_.css index-BpzM6R0V.css dist/assets/index-BpzM6R0V.css 92 289 92.28 kB

4/4 rotate. Kích-thước live khớp con số Vite in trong log CI tới từng byte ⇒ file trên đĩa prod chính là artifact run #447 sinh ra, không phải bản nào khác.

5.2 Last-Modified nằm TRONG cửa-sổ deploy (ship-proof theo #69, không dựa hash-delta)

Run #447: 19:49:08 → 19:56:33 (+07). Last-Modified (UTC) = 12:54:41 (admin ×2 file) và 12:55:53 (eoffice ×2 file) = 19:54:41 / 19:55:53 +07 ⇒ đúng trong cửa-sổ, và trùng khít giây với dòng build tương ứng trong log. Không có FROZEN, không có LM cũ ⇒ loại sạch nhánh "bundle cũ kẹt".

5.3 Control ÂM tầng-2 — bundle CŨ đã biến mất khỏi đĩa

Hash cũ HTTP Content-Type Size
admin/assets/index-DLRf9TeA.js 200 text/html 900 B
eoffice/assets/index-C7TGA4Kn.js 200 text/html 876 B

200 ở đây là bẫy SPA-fallback, đã né đúng cách: discriminator là Content-Type chứ không phải mã HTTP. text/html ~876-900 B ⇒ file cũ không còn trên đĩa, IIS rewrite /*index.html ⇒ chứng minh ghi đè thật, không phải cộng thêm.

5.4 Byte-marker (gotcha #77 — phân biệt deploy vs cache), có control 2 chiều

Marker chọn theo luật "0-hit PRE phải chứng được", đo bằng git grep -F trên cả 2 cây rồi python str.find trên bundle tải về (không grep -P — chết locale Git-Bash):

Marker 998ea55^ (admin/user) 998ea55 (admin/user) Bundle admin Bundle eoffice
M1 Bước này chưa cấu hình cấp duyệt. 0 / 0 1 / 1 1 1
CTRL-DƯƠNG Chưa khai căn cứ nào. (có TRƯỚC S171) 1 / 1 1 / 1 1 1
CTRL-ÂM chuỗi bịa 0 0
M3 Bước không gắn phòng. 0 / 0 1 / 1 0 ⚠️ 0 ⚠️

Ngữ cảnh trích thật quanh M1 (giống hệt 2 app, minify ra backtick):

text-[11px] italic text-slate-400`,children:`Bước này chưa cấu hình cấp duyệt.`}),e.levels.ma…
  • M1 = wire-proof mã FE mới: vắng mặt ở cây tiền-commit ⇒ nó chỉ có thể tới từ 998ea55.
  • CTRL-DƯƠNG khoá tiền-đề "tiếng Việt đọc được trong bundle" ⇒ 0-hit của M3 không thể đổ cho unicode-escape.
  • ⚠️ M3 0-hit là ĐÚNG LUẬT, không phải thiếu ship: M3 nằm trong comment // tại fe-admin|fe-user/src/types/khkk.ts:288 ⇒ Vite strip. Ghi lại để không ai đọc nhầm thành cờ đỏ (đúng cơ-chế đã gặp @S168).

Stage 4: PASS — 4 chân độc lập (hash rotate · size khớp-byte với log CI · LM trong cửa-sổ · marker mới có mặt + control 2 chiều) + control-âm tầng-2.

6. Stage 4b — Smoke prod + 2 endpoint MỚI (gotcha #82) ĐO 2026-08-05 (phiên nối)

6.1 Negative-control 2 endpoint MỚI: PRE 404 → POST 401 (cả hai)

Attribute thật trong ContractSigningPlansController.cs (đọc từ mã, không suy từ cây menu — luật gotcha #85): [HttpGet("{id:guid}/changelogs")] + [Authorize(Policy = "KeHoachKyKet.Read")] · [HttpPost("{id:guid}/approval-attachments")] + [Authorize(Policy = "KeHoachKyKet.Update")]policy per-method, đúng key của endpoint, không OR key con.

Probe (không token) PRE §2b POST Ý nghĩa
GET …/{guid0}/changelogs 404 401 route MỚI đã lên sóng + authz gác
POST …/{guid0}/approval-attachments (json {}) 404 415 ⚠️ pre-auth, xem 6.2
POST …/{guid0}/approval-attachments (multipart) 401 chạm được authz ⇒ 401 thật
CTRL-ÂM POST …/{guid0}/khong-ton-tai-xyz (multipart) 404 404 401 KHÔNG phải phản-xạ chung của host

🔴 Cả 2 đều 401 — không 200 (không lỗ hổng authz), không 500 (không hỏng deploy). Đúng yêu-cầu negative-control.

6.2 Bẫy 415 (họ hàng bẫy 411 ở §2b) — ghi lại để lần sau khỏi mất công

POST với Content-Type: application/json trả 415, KHÔNG phải 401. Nguyên nhân: endpoint nhận multipart (upload file) ⇒ mismatch bị loại ở tầng action-selection, tức TRƯỚC authorization filter — y hệt cơ-chế 411 của IIS. 415 chỉ chứng "route tồn tại", không chứng authz. Muốn đọc được authz phải gửi đúng multipart. Sau khi sửa: 401. ⇒ Luật: mã lỗi pre-auth (411/415) là vô nghĩa cho câu hỏi authz, phải re-probe đúng dạng.

6.3 🔑 Wire-proof MẠNH NHẤT — thông-điệp-lỗi gọi tên field (mạnh hơn 401)

POST …/{guid0}/approval-attachments + bearer admin + multipart ⇒ 400 với thân:

{"status":400,"errors":{
  "ContractSigningPlanId":["'Contract Signing Plan Id' must not be empty."],
  "ContentType":["Định dạng file không được hỗ trợ. Cho phép: pdf, doc(x), xls(x), png, jpg, webp."]}}

Chuỗi này chứng end-to-end Api → Application: qua routing, qua authz, model-binding đọc được file multipart, và validator MỚI của command MỚI đã chạy (nó gọi đúng 2 tên field và bung whitelist đuôi file). 401 chỉ chứng route tồn tại; 400-liệt-kê-field chứng binary mới đang phục vụ — mạnh hơn cả đo w3wp StartTime.

Đối xứng cho endpoint kia: GET …/{guid0}/changelogs + bearer admin ⇒ 404 (phiếu GUID-toàn-0 không có thật) — cùng route mà 401 khi vắng token / 404 khi có token ⇒ 401 kia đúng là authz, không phải routing.

6.4 Smoke prod (bearer admin) — 8/8 xanh

Endpoint
GET /health/ready 200
GET /api/contract-signing-plans (KHKK) 200
GET /api/contracts 200
GET /api/purchase-evaluations 200
GET /api/menus 200
GET /api/suppliers 200
GET /api/catalogs/contract-catalog 200
POST /hubs/notifications/negotiate (có body) 200 + connectionId

(SignalR: bodyless trả 411 — lại đúng bẫy IIS Length-Required ở §2b; gửi kèm body thì 200 ⇒ WebSocket/negotiate lành, gotcha #25 không tái phát.)

6.5 ĐÃ ĐÓNG — chân non-admin (gotcha #44) — lead đo 2026-08-05, phiên nối S172

🧊 Nấc cũ (giữ làm vết): nv.test@solutions.com.vn / TestUser@123456401 ×2 ⇒ kết luận "credential UAT chết trên prod, chân này KHÔNG ĐO ĐƯỢC". Kết luận đó SAI — không phải credential chết, mà là tra nhầm sổ: nv.test@ là account đời cũ. Bộ user test đang sống nằm ở docs/HANDOFF.md slot (64): test.drafter@ · test.approver@, password chung Test@12345678 (đổi theo policy prod ≥12 ký tự). ⇒ Bài: "KHÔNG ĐO ĐƯỢC" phải kèm câu hỏi "đã tra hết sổ có sẵn chưa?" — cùng lớp feedback_mechanism_right_data_absent (cơ-chế đúng, dữ-liệu tưởng không có nhưng thật ra có, chỉ nằm chỗ khác).

Login 200/200 cả hai. Probe bằng bearer NON-ADMIN (multipart đúng dạng theo bẫy §6.2):

Probe (bearer non-admin) test.drafter@ test.approver@ Ý nghĩa
GET /api/contract-signing-plans 200 200 qua KeHoachKyKet.Read
GET …/{guid0}/changelogs 404 404 404 chứ KHÔNG 403 ⇒ qua authz, chỉ là phiếu GUID-0 không có thật
POST …/{guid0}/approval-attachments (multipart pdf) 400 400 qua KeHoachKyKet.Update validator MỚI chạy — thân lỗi gọi đúng tên field ContractSigningPlanId
CTRL-ÂM POST …/{guid0}/khong-ton-tai-xyz 404 404 400/404 KHÔNG phải phản-xạ chung của host

🔴 Kết luận — phạm-vi ĐÚNG BẰNG MẪU ĐÃ ĐO (siết @S172 sau ring2-audit ESCALATE-1): 2 tổ-hợp vai đã đoDrafter+Procurement (test.drafter@) và CostControl+DeptManager (test.approver@) — có đủ KeHoachKyKet.Read+Update (4/4 probe, CTRL-ÂM 404). 🔴 CHƯA LOẠI nhánh "một vai thường KHÁC (vd Employee trần) vẫn 403"#85 không tái phát TRÊN 2 TỔ-HỢP NÀY, chưa phải kết-luận toàn-cục.

🧊 Bản S172 đầu viết: "role thường có đủ … ⇒ #85 KHÔNG tái phát" + "chứng wire cho đúng role người dùng thật". Cả hai vế đều rộng hơn mẫu — mẫu là 2 tài-khoản, và HANDOFF slot (64) khai 0/2 là user trần (cả hai mang vai cấp riêng; test.approver@ còn "KHÔNG nằm trong trạm nào" nên không đại-diện được "người dùng thật"). Giữ vết vì đây là ca mẫu của class suy-luận vượt mẫu — thứ enum ĐÓNG hiện chưa có ô để đếm (đề-nghị owner mở view-claim-broader-than-sample).

⚠️ Câu hỏi MỚI đẻ ra từ phép đo này (KHÔNG phải bug — là quyết-định thiết-kế, để owner phán): test.drafter@ (Drafter+Procurement) cũng qua KeHoachKyKet.Update ⇒ người soạn phiếu upload được file-khi-duyệt. Spec 3-QĐ-owner đặt upload ở bước duyệt. Nếu chủ-ý là chỉ người duyệt mới upload thì policy đang rộng hơn ý-định. Đo được, chưa phán — xem WAL mục chờ-anh.

7. Stage 5 — Migration (kỳ vọng: KHÔNG có mig mới) ĐO 2026-08-05 (phiên nối)

NO-Mig proof = 2 vế, cả 2 đều đo:

  1. git diff --name-only 998ea55^..998ea55 -- '*Migrations*'0 file. Commit không đẻ mig — khớp đề bài PIN (enum +ApprovalAttachment=3 là int, không đổi schema).
  2. Lịch sử prod không nhích: vẫn 71, top vẫn 20260731085624_… (của S164/S165), không có entry nào mang dấu 04-08.
Phép đo Repo Prod (ssh vietreport-vps + sqlcmd -E) Khớp
Số migration 71 71
Mig mới nhất 20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime (y hệt)
sys.tables (is_ms_shipped=0) 97 = STATUS.md B1

Mạnh hơn khớp-tổng — set-diff 2 CHIỀU (comm -23 / comm -13 trên danh sách đầy đủ; đếm repo neo-đuôi grep -vE 'Designer\.cs$|ModelSnapshot\.cs$' để không ăn nhầm …Designer.cs):

repo \ prod  →  0 dòng
prod \ repo  →  0 dòng

Cả hai rỗng ⇒ hai tập trùng khít, không phải "tình cờ bằng số lượng". Baseline Mig 71 + tables 97 giữ nguyên sau đợt này.

(Chân "pool đã recycle & phục vụ binary mới" không cần đo riêng: §6.3 đã chứng bằng validator MỚI chạy thật trên prod — mạnh hơn w3wp StartTime.)

8. Verdict 2026-08-05

PASS🔄 7/7 chân xanh (cập-nhật S172: chân phụ non-admin ĐÃ ĐÓNG, xem §6.5)

# Chân Kết quả Bằng chứng gọn
1 Push + path-filter 21 file code, 0 khớp 7-glob; run keyed theo tip bf0200b (AS-6)
2 CI run #447 / id 560 status=success, 19:49:08→19:56:33 (7m25s)
3 Test gate 45/45 + 599/599 = 644, Failed 0, Skipped 0 — khớp B1 tách-phần, Δ0 đúng kỳ vọng
4 Ship FE ×2 app 4/4 hash rotate · size khớp log CI tới byte · LM trong cửa-sổ · marker M1 mới có mặt ×2 + control 2 chiều · hash cũ rơi text/html
5 Endpoint MỚI (negative-control) cả 2 = 401 (không 200, không 500); +400-gọi-tên-field khi có bearer = wire-proof end-to-end
6 Migration repo 71 = prod 71, set-diff 0/0 hai chiều, tables 97, commit chạm 0 file mig
Smoke prod 8/8 200 (kèm SignalR negotiate 200 + connectionId)
7 Non-admin (gotcha #44/#85) — phạm-vi = 2 tổ-hợp vai 🔄 ĐÓNG @S172 (siết phạm-vi cùng ngày) Drafter+Procurement + CostControl+DeptManager: login 200 · list 200 · changelogs 404 (không 403) · attach 400 gọi tên field · CTRL-ÂM 404 ⇒ #85 không tái phát TRÊN 2 TỔ-HỢP NÀY; 🔴 chưa loại nhánh vai thường khác (vd Employee trần) vẫn 403

Việc cần em main xử (theo thứ tự)

  1. Cấp lại tài-khoản non-admin để đóng chân #44XONG @S172: không cần cấp mới, account đang sống nằm ở HANDOFF slot (64); nấc cũ tra nhầm nv.test@ đời cũ. Xem §6.5.
  2. Flake Create_DifferentEvaluationType_DoesNotAffect_Other vẫn treo — 0/4 log CI tái hiện. Đừng nén "CI xanh" thành "flake đã hết"; muốn chốt thì phải tấn công PeWorkflowAdminTests.cs (new SqliteDbFixture() ×6) chứ CI không trả lời hộ được. Đây nay là việc treo DUY NHẤT của đợt.
  3. ⚠️ MỚI (đẻ từ §6.5, chờ owner phán): test.drafter@ cũng qua KeHoachKyKet.Update ⇒ người soạn upload được file-khi-duyệt. Policy có thể rộng hơn ý-định spec. Đo được, chưa phán.
  4. Không có gì để rollback. Prod khoẻ.

END cicd-verify 998ea55 — VERDICT=PASS (🔄 S172: 7/7 chân xanh; chân non-admin đóng bằng đo, không còn "KHÔNG ĐO ĐƯỢC").