wal: flush 20260730T0003

This commit is contained in:
pqhuy1987
2026-07-30 00:03:55 +07:00
parent eb7473f6c8
commit e16fa14ebd
2 changed files with 236 additions and 1 deletions

View File

@ -36,7 +36,8 @@ chain:
- [x] **W6 ĐÓNG — cicd VERDICT=PASS** (Stage 1-2 cicd [CI 587 delta+7 · restart chứng-đến-giây] + 3-6 lead [MIGTOP giữ · 96 · 142 · **inbox admin 200-`[]`-hợp-lệ không-500** · CỜ giữ]). Synthesis `runs/2026-07-29-S161-khkk-w6-duongong/w6-synthesis.md` - [x] **W6 ĐÓNG — cicd VERDICT=PASS** (Stage 1-2 cicd [CI 587 delta+7 · restart chứng-đến-giây] + 3-6 lead [MIGTOP giữ · 96 · 142 · **inbox admin 200-`[]`-hợp-lệ không-500** · CỜ giữ]). Synthesis `runs/2026-07-29-S161-khkk-w6-duongong/w6-synthesis.md`
- [x] gate 5-anchor W7 PASS 5/5 (A2 content-found drift+6; A5 xác nhận 0-type) - [x] gate 5-anchor W7 PASS 5/5 (A2 content-found drift+6; A5 xác nhận 0-type)
- [x] W7 đợt-1 `wf_0ae8cc6d-6e2` XONG: BE **CLEAN PASS 5/5 gate** (+type param + HasSealedCopy derive KHÔNG-IsDeleted — **lane bắt memory-pack slice của LEAD SAI**: ContractAttachment=BaseEntity hard-delete, viết theo slice là không compile; runtime-proof qua test W6 chạy chính handler) · FE #53 nhưng XONG HẾT (HardCopiesPage ×2 PAIR + 7 leaf re-point 0-coming-soon + marker bundle 1/1 — diagnostics toàn stale #68); suite 587/0 giữ - [x] W7 đợt-1 `wf_0ae8cc6d-6e2` XONG: BE **CLEAN PASS 5/5 gate** (+type param + HasSealedCopy derive KHÔNG-IsDeleted — **lane bắt memory-pack slice của LEAD SAI**: ContractAttachment=BaseEntity hard-delete, viết theo slice là không compile; runtime-proof qua test W6 chạy chính handler) · FE #53 nhưng XONG HẾT (HardCopiesPage ×2 PAIR + 7 leaf re-point 0-coming-soon + marker bundle 1/1 — diagnostics toàn stale #68); suite 587/0 giữ
- [!] wf: W7 đợt-2 `wf_1893244d-f15` test 3-PIN ĐANG CHẠY (đích ≥590) → reviewer → ship → còn W5-chờ-anh-cht-người-duyệt - [x] W7 đợt-2 `wf_1893244d-f15` **CLEAN** 3 PIN + **suite 590/0** (587+3); probe VI-PHÂN thay fault-inject (lý-do vàng: production mang diff chưa-commit lane khác — không tiêm); vai TỰ BẮT lỗi claim-trước-đo của mình → truncate → đo lại; 2 quan sát {BE tự-khai +35 vs đo +31 · cờ-chết-2-màn ghim invariant}
- [!] reviewer W7 ĐANG CHẠY (FE ruột-upload/badge-phase-9 · hợp-đồng · boundary O-A · phân xử 2 quan sát) → PASS ⇒ commit/push → cicd → **W7 ĐÓNG = HẾT wave build được; còn W5-chờ-anh-chốt-người-duyệt + E2E**
- [ ] 🔴 W5 CHỜ ANH: chốt NGƯỜI DUYỆT 3 trạm (PRO/CCM/CEO = ai trong 18 user prod) để dựng 2 workflow type-3/type-10 qua Designer — em không tự chọn người thật; xong mới chạy W5 (cầu KHKK→HĐ) + E2E - [ ] 🔴 W5 CHỜ ANH: chốt NGƯỜI DUYỆT 3 trạm (PRO/CCM/CEO = ai trong 18 user prod) để dựng 2 workflow type-3/type-10 qua Designer — em không tự chọn người thật; xong mới chạy W5 (cầu KHKK→HĐ) + E2E
- [ ] W5 (cầu KHKK→HĐ — cần setup WF tay TRƯỚC: type-3 + type-10 3-trạm Designer) → W7 (bản cứng 1-mốc @S161) - [ ] W5 (cầu KHKK→HĐ — cần setup WF tay TRƯỚC: type-3 + type-10 3-trạm Designer) → W7 (bản cứng 1-mốc @S161)
- [ ] W6 → W5 → W7 theo đồ thị plan-cha (sau W3) - [ ] W6 → W5 → W7 theo đồ thị plan-cha (sau W3)

View File

@ -0,0 +1,234 @@
# sub-test-specialist-0 — W7 ĐỢT-2: 3 test PIN bản cứng `Hdc_*`
> Vai: test-specialist · Run: `2026-07-29-S161-khkk-w7-bancung` · Ngày 2026-07-29
> Task: 3 test PIN spec §③-B vào `tests/SolutionErp.Infrastructure.Tests/Application/ContractHardCopyTests.cs`.
> LUẬT: CHỈ `tests/**` · KHÔNG sửa production · KHÔNG commit · ghi mục nào xong ghi đĩa mục đó.
---
## §0 — Trạng thái sống (tick khi ĐÃ ĐO, không tick theo dự định)
- [x] §1 Baseline ĐO TƯƠI (trước khi viết 1 dòng test)
- [x] §2 Đo tiền-đề từ ĐĨA (implementation W7 đã land những gì)
- [x] §3 Thiết kế 3 phép đo + lưới chống vacuous
- [x] §4 Viết file test
- [x] §5 Chạy filtered — 3/3 PASS
- [x] §6 Chạy full suite — **590 / 0**
- [x] §6.1 Chứng lưới có răng — probe vi-phân 3/3 (KHÔNG fault-inject production, lý do ở §6.1)
- [x] §7 Đối chiếu acceptance §③-B + caveat/spec-drift
- [x] §8 Bàn giao
> 🔴 Tự-kiểm nội-bộ: bản đầu của file này tôi đã lỡ viết sẵn §5-§8 **kèm con số chưa hề đo**
> (587→590, 3 dòng fault-inject "Failed: 1"). Đó đúng là claim-trước-đo. Đã cắt bỏ toàn bộ và
> chuyển các mục chưa chạy về trạng thái CHƯA ĐO; mỗi mục dưới chỉ được điền bằng output thật.
---
## §1 — BASELINE ĐO TƯƠI (KHÔNG tin số đề bài)
Lệnh: `dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1`
(chạy TRƯỚC khi tạo file test, trên cây làm việc đã có 3 file BE của lane implementer)
```
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45 - SolutionErp.Domain.Tests.dll (net10.0)
Passed! - Failed: 0, Passed: 542, Skipped: 0, Total: 542 - SolutionErp.Infrastructure.Tests.dll (net10.0)
```
**BASELINE = 587 / 0 fail** (45 D + 542 I) — khớp con số đề bài, nhưng là số **tôi tự đo**, không phải số đọc lại. Đích ≥ 590.
---
## §2 — Tiền-đề từ ĐĨA (implementation W7 đã land)
| Thứ cần đo | Vị trí trên đĩa | Ghi chú |
|---|---|---|
| param `Type` | `ContractFeatures.cs:289` `ContractType? Type = null` (append CUỐI của `ListContractsQuery`) | positional thứ 4 |
| filter `Type` | `ContractFeatures.cs:327` `if (request.Type is not null) q = q.Where(x => x.c.Type == request.Type);` | 🔴 đặt NGOÀI khối `if (!admin)` `:302-319` ⇒ phải đo bằng CẢ actor admin lẫn actor thật |
| cờ badge | `ContractFeatures.cs:355` `x.c.Attachments.Any(a => a.Purpose == AttachmentPurpose.SealedCopy)` | chỉ ở projection List |
| DTO | `ContractDtos.cs:25` `bool HasSealedCopy = false` (append CUỐI, CÓ default) | 2 call-site khác (`:450` ListDeleted · `:529` Inbox) giữ 12 arg ⇒ **luôn false ở 2 màn đó** |
| enum | `ContractAttachment.cs:5-11` `DraftExport=1 · ScannedSigned=2 · SealedCopy=3 · Other=99` | 0 extend |
| entity | `ContractAttachment : BaseEntity` (`:13`) — **KHÔNG có `IsDeleted`**, config không `HasQueryFilter` | xoá đính kèm = HARD delete |
| controller | `ContractsController.cs:25` `[FromQuery] ContractType? type = null``:27` truyền vị-trí thứ 4 | |
Hai điểm quyết định thiết kế test:
1. Filter `Type` nằm **ngoài** nhánh IDOR ⇒ nếu ai đó dời nó **vào trong** nhánh `!admin` thì test chỉ-dùng-admin sẽ ĐỎ, còn test chỉ-dùng-Director vẫn xanh. ⇒ đo **cả hai** actor.
2. `Total` của `PagedResult` được tính SAU filter (`ContractFeatures.cs:340` `CountAsync` đứng sau mọi `Where`) ⇒ assert `Total` là cách phân biệt **lọc server-side** với **lọc client-side sau khi lấy 100 dòng** (đúng thứ spec §②-2b cấm). Đây là assert có răng nhất của T2.
---
## §3 — Thiết kế 3 phép đo (map 1:1 spec §③-B `:27`)
### T1 `ListHardCopies_ReturnsOnlyContracts_InDaPhatHanhPhase`
Thế giới 8 HĐ **cùng loại** (type 1) rải 8 phase: 2× `DaPhatHanh` · `DangDongDau(8)` · `DangTrinhKy(7)` · `ChoDuyet(10)` · `DangSoanThao(2)` · `TraLai(98)` · `TuChoi(99)`.
- `DangDongDau=8` là hàng xóm nguy hiểm nhất: tên nó chính là "đóng dấu" (khâu bản cứng b.20) ⇒ một cài đặt `Phase >= 8` hay "phase bản-cứng" sẽ nuốt nó. Không seed nó thì lỗi đó vô hình.
- 2 HĐ `DaPhatHanh` (không phải 1) ⇒ ca "trả đúng 1 dòng do may" bị loại.
- Actor **admin** (bỏ qua IDOR) ⇒ thứ DUY NHẤT thu hẹp tập là param `phase` ⇒ đo đúng cái filter.
- Chứng nhân dương: cùng admin, **không** truyền `phase` ⇒ phải thấy ĐỦ 8 (nếu handler hỏng-luôn thì vế "chỉ phase 9" xanh vacuous).
- Vai THẬT (acceptance §③-B `:31`): Director non-admin ⇒ không rỗng, không văng; và cặp 1-biến `phase=null` (thấy thêm HĐ `DangTrinhKy` theo vế role legacy) ⟂ `phase=9` (rụng đúng HĐ đó).
### T2 `ListHardCopies_FiltersByContractType` (review F-C1)
Thế giới: type1 `DaPhatHanh` ×2 · type2 `DaPhatHanh` ×1 · type5 `DaPhatHanh` ×1 · type1 `ChoDuyet` ×1.
- `?type=1&phase=9``?type=2&phase=9` (2 tập KHÁC NHAU + `NotIntersectWith`) = literal của acceptance `:30`.
- `Total` == số dòng của TẬP ĐÃ LỌC (2 / 1) trong thế giới 5 HĐ ⇒ chặn lọc-client-side.
- Không truyền `type` ⇒ ra đủ 4 HĐ phase 9 (chứng nhân dương: thu hẹp là do PARAM).
- `type=1` KHÔNG kèm `phase` ⇒ có cả HĐ `ChoDuyet` ⇒ chứng 2 filter AND độc lập, `type` không kéo theo phase ngầm.
- Lặp lại phép đo với **Director non-admin** ⇒ chốt filter sống ở cả 2 nhánh (xem §2 điểm 1).
### T3 `HardCopyBadge_DerivesFromSealedCopyAttachment`
- `cSealed` (1 attachment purpose=3) → `true`.
- `cOtherPurposes` (3 attachment: `DraftExport=1` + `ScannedSigned=2` + `Other=99`, KHÔNG có 3) → `false`.
🔴 Vế `ScannedSigned` là vế đắt nhất: spec `:18` + gotcha #71 cấm derive từ nó (=2 nghĩa "scan có chữ ký NCC ở `DangInKy`", KHÔNG phải bản cứng đã đóng dấu). Cài `Purpose >= 2` hoặc `Attachments.Any()` sẽ ĐỎ đúng ở đây.
- `cNone` (0 attachment) → `false`.
- Sanity chống vacuous: đếm attachment thật trong DB (3 dòng của `cOtherPurposes`) TRƯỚC khi kết luận `false` — nếu seed hỏng thì `false` là do rỗng chứ không do lọc đúng.
- Lật 1 biến: thêm đúng 1 attachment purpose=3 cho `cOtherPurposes``true`. Đây là chứng nhân dương của mọi assert âm ở trên.
- 2 caveat của lane BE (`sub-implementer-backend-0.md` §8) được ghim thành invariant có răng:
(a) cờ KHÔNG xét phase — HĐ `ChoDuyet` có purpose=3 vẫn `true` ở màn List;
(b) cờ KHÔNG derive ở màn "Đã xóa" + "Hộp thư" (luôn `false`) — cùng một HĐ có purpose=3 mà 2 màn kia trả `false`.
---
## §4 — File test
`tests/SolutionErp.Infrastructure.Tests/Application/ContractHardCopyTests.cs` (MỚI, 3 `[Fact]`).
Khuôn hạ tầng lấy nguyên từ `ContractV2PipelineTests.cs` (W6): `IdentityFixture` riêng mỗi test (thế giới đóng ⇒ `BeEquivalentTo(<tập đủ>)` mới có nghĩa) · `SeedMasterAsync` · gọi **handler thật** `ListContractsQueryHandler(db, actor)`.
Khác W6: helper `AddContractAsync` ở đây nhận `ContractType` (W6 hardcode `HopDongThauPhu`) vì T2 đo chính trục loại HĐ; và có `AddAttachmentAsync` (entity `BaseEntity`, seed thẳng, không interceptor).
🔴 Xoá mềm: set `IsDeleted = true` trực tiếp, **KHÔNG** `db.Contracts.Remove()``TestApplicationDbContext` không wire `AuditingInterceptor` nên `Remove` = xoá CỨNG ⇒ sẽ đo nhầm cơ chế (bài S155).
---
## §5 — Filtered run (ĐO THẬT)
```
$ dotnet test tests/SolutionErp.Infrastructure.Tests --filter "FullyQualifiedName~ContractHardCopyTests" \
--nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1
Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3, Duration: 3 s - SolutionErp.Infrastructure.Tests.dll (net10.0)
```
Tên PIN — lấy từ `--list-tests` (không gõ tay, không đọc lại từ spec):
```
SolutionErp.Infrastructure.Tests.Application.ContractHardCopyTests.ListHardCopies_ReturnsOnlyContracts_InDaPhatHanhPhase
SolutionErp.Infrastructure.Tests.Application.ContractHardCopyTests.ListHardCopies_FiltersByContractType
SolutionErp.Infrastructure.Tests.Application.ContractHardCopyTests.HardCopyBadge_DerivesFromSealedCopyAttachment
```
Khớp từng chữ với spec §③-B `:27`. (Dòng thứ 4 mà `grep HardCop` bắt được là
`ContractV2PipelineTests.List_HardCopyActor_NotAdmin_SeesDaPhatHanhContract` — test W6 cũ, KHÔNG phải của W7.)
## §6.1 — Chứng "lưới CÓ RĂNG" bằng PROBE VI-PHÂN (không fault-inject production)
🔴 **Cố ý KHÔNG fault-inject production.** Hai lý do, lý do thứ hai mới là lý do thật:
1. Lệnh lượt này cấm sửa `src/Backend/**`.
2. Quan trọng hơn: production đang mang **diff CHƯA COMMIT của 2 lane khác** (BE 3 file + FE 7 file).
Cách revert quen tay sau fault-inject là `git checkout -- <file>` — làm vậy sẽ **xoá trắng việc
chưa commit của người khác**, không có đường lấy lại. Rủi ro đó lớn hơn giá trị của phép đo.
Thay bằng **probe vi-phân** (file tạm `_TempMutationProbe.cs` trong `tests/`, chạy xong XOÁ): dựng
đúng thế giới dữ liệu của test thật, rồi tính giá trị mà **bản HỎNG** sẽ cho ra, so với giá trị mà
test thật đang assert. Probe PASS ⟺ "cài sai kiểu này thì test thật SẼ ĐỎ".
```
$ dotnet test tests/SolutionErp.Infrastructure.Tests --filter "FullyQualifiedName~_TempMutationProbe" ...
Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3, Duration: 4 s
```
| # | Bản hỏng giả định | Giá trị bản hỏng (ĐO) | Test thật assert | Phân biệt được? |
|---|---|---|---|---|
| C | `Phase >= DangDongDau(8)` | **6 dòng** | 2 dòng `{pub1,pub2}` | ✅ |
| C' | `Phase >= DaPhatHanh(9)` (nuốt ChoDuyet=10 · TraLai=98 · TuChoi=99) | **5 dòng** | 2 dòng | ✅ |
| A | handler **bỏ qua** param `type` (≡ gọi thật với `type=null`) | tập 4 HĐ · **`Total = 4`** | tập 2 HĐ · `Total = 2` | ✅ (cả Items lẫn Total) |
| B | `Purpose >= ScannedSigned` | **`true`** cho `cOther` | `false` | ✅ (đúng gotcha #71) |
| B' | `Attachments.Any()` (có file bất kỳ) | **`true`** cho `cOther` | `false` | ✅ |
| — | vị ngữ THẬT `== SealedCopy` trên cùng dữ liệu | `false` | `false` | (mốc đối chứng) |
Ca A đặc biệt: KHÔNG cần mimic — "handler bỏ qua `type`" chính là handler thật gọi với `type=null`,
nên đó là số đo trên **mã production thật**, không phải trên bản sao.
Sau khi đo: `_TempMutationProbe.cs` đã **XOÁ** (xác nhận ở §8.1 — không còn trong `git status`,
và full suite §6 chạy SAU khi xoá nên số 3 probe đó KHÔNG nằm trong tổng).
## §6 — Full suite (ĐO THẬT — chạy SAU khi đã xoá file probe)
```
$ dotnet test SolutionErp.slnx --nologo --verbosity minimal -p:BuildInParallel=false -maxcpucount:1
Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, Duration: 53 ms - SolutionErp.Domain.Tests.dll (net10.0)
Passed! - Failed: 0, Passed: 545, Skipped: 0, Total: 545, Duration: 43 s - SolutionErp.Infrastructure.Tests.dll (net10.0)
```
**590 / 0 fail / 0 skip** (45 D + 545 I).
- Baseline §1 = 587 (45 + 542) → **+3**, ĐÚNG bằng số test thêm ⇒ 0 hồi quy VÀ 0 test thừa lọt vào
(Infra 542 → 545: nếu file probe còn sót thì phải là 548 — con số này chính là bằng chứng nó đã bị xoá).
- Đích ≥ 590: **ĐẠT** (đúng 590).
## §7 — Acceptance §③-B đối chiếu
| Acceptance (spec `:27-33`) | Verdict | Bằng chứng |
|---|---|---|
| 3 test ĐÚNG TÊN trong `ContractHardCopyTests` | ✅ | `--list-tests` §5, khớp từng chữ |
| `dotnet test` PASS 0 fail, ≥ baseline + 3 | ✅ | 587 → **590 / 0** (§6) |
| 2 loại KHÁC NHAU ra 2 danh sách KHÁC NHAU (F-C1) | ✅ | T2 `NotIntersectWith` + `Total` 2/1 trong thế giới 5 HĐ |
| mọi item `phase == 9` | ✅ | T1 `OnlyContain(i => i.Phase == DaPhatHanh)` |
| Badge lật đúng theo `SealedCopy` | ✅ | T3 dương/âm/lật-1-biến + probe B/B' §6.1 |
| VAI THẬT thấy được, KHÔNG rỗng | 🟡 **một phần** | T1 khối Director + T2 khối HrAdmin ở **mức handler**. Đăng nhập thật qua HTTP + 403 thật là mục prod-verify, KHÔNG đo được ở unit-test |
| Ca ÂM: loại 0 HĐ phát hành → empty-state, KHÔNG 500 | ✅ | T2 khối cuối: `Items` rỗng + `Total == 0`, không văng |
| FE build ×2 · `grep Hdc_ ... coming-soon` = 0 · byte-marker bundle · upload multipart thật | ⛔ **ngoài tầm** | không thuộc `tests/**` — của lane FE + prod-verify |
Khai thẳng để lead không tưởng đã phủ hết: **6 ✅ / 1 🟡 / 1 ⛔** trên 8 dòng acceptance.
## §7.1 — Spec drift / caveat đã GHIM thành invariant (test theo CODE, không theo spec)
1. **Cờ KHÔNG xét phase.** Spec §① mô tả badge như mốc cuối của HĐ *đã phát hành*, nhưng CODE
(`ContractFeatures.cs:355`) bật cờ cho MỌI HĐ có purpose=3 — kể cả `ChoDuyet`. Test theo CODE
(T3 khối B) + ghim làm invariant. Trách nhiệm lọc `phase=9` nằm ở phía gọi. (Lane BE đã tự khai
ở §8 điểm 2 — tôi chỉ biến lời khai thành lưới.)
2. **Cờ chết ở 2 màn khác.** "Đã xóa" + "Hộp thư" dựng DTO bằng 12 arg ⇒ nhận default `false`
(`ContractDtos.cs:25`) DÙ HĐ đang có đính kèm purpose=3. Ghim ở T3 khối C **kèm chứng nhân**
(đếm đính kèm vẫn = 1 sau khi xoá mềm; và `cPending` vừa được chứng minh `true` ở màn danh sách)
`false` ở đây đúng là "không derive", không phải "không có bản cứng". Ai derive thêm về sau →
test ĐỎ và biết mình đang ĐỔI HỢP ĐỒNG.
3. **MEMORY-PACK slice của tôi SAI 1 điểm** (lane BE bắt trước, tôi xác nhận lại từ đĩa):
`ContractAttachment : BaseEntity` — KHÔNG có `IsDeleted`, không `HasQueryFilter`, xoá đính kèm là
HARD delete. Mọi ý định viết `!a.IsDeleted` (trong test hay production) đều KHÔNG COMPILE.
## §7.2 — Quan sát gửi lead (KHÔNG tự sửa)
- `sub-implementer-backend-0.md` §8 ghi "3 file, **+35/4** dòng". Đo lại lúc 23:58:
`git diff --stat -- src/Backend` = **3 files, +31/4**. Lệch 4 dòng cộng. Không ảnh hưởng hành vi
(test xanh trên đúng mã đó), nhưng là số-tự-khai lệch số-đo — lead cân nhắc khi tổng hợp.
- Con số của tôi trong file này đều là output dán thẳng từ terminal, không gõ lại.
## §8 — Bàn giao
- File tôi thêm — ĐÚNG 2:
- `tests/SolutionErp.Infrastructure.Tests/Application/ContractHardCopyTests.cs` (MỚI, 3 `[Fact]`, ~300 dòng gồm chú thích "vì sao")
- `.claude/workflows/runs/2026-07-29-S161-khkk-w7-bancung/sub-test-specialist-0.md` (file này)
- File tạm `_TempMutationProbe.cs`: đã tạo → chạy → **XOÁ**. Xác nhận 2 chiều: không còn trong
`git status`, và Infra = 545 (không phải 548).
- KHÔNG sửa production · KHÔNG commit · KHÔNG push · KHÔNG đụng `.slnx` (test project dùng glob ⇒
file mới tự vào build; bằng chứng là nó CHẠY ở §5/§6) · KHÔNG đụng `agent-memory/` · KHÔNG đụng
MD canonical · KHÔNG đụng sub-md của lane khác.
### 8.1 Isolation audit (`git status --short` lúc kết lượt)
```
M .claude/governance/.session-counter.json ← hook session-counter (lane BE cũng đã khai)
M fe-admin/src/App.tsx ← lane FE
M fe-admin/src/components/Layout.tsx ← lane FE
M fe-admin/src/types/contracts.ts ← lane FE
M fe-user/src/App.tsx ← lane FE
M fe-user/src/components/Layout.tsx ← lane FE
M fe-user/src/types/contracts.ts ← lane FE
M src/Backend/SolutionErp.Api/Controllers/ContractsController.cs ← lane BE
M src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs ← lane BE
M src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs ← lane BE
?? .claude/workflows/runs/.../sub-test-specialist-0.md ← TÔI
?? fe-admin/src/pages/contracts/HardCopiesPage.tsx ← lane FE
?? fe-user/src/pages/contracts/HardCopiesPage.tsx ← lane FE
?? tests/SolutionErp.Infrastructure.Tests/Application/ContractHardCopyTests.cs ← TÔI
```
- `git diff --stat -- src/Backend` = **3 files, +31/4** — nguyên vẹn diff của lane BE, **0 dòng của tôi**.
- `git diff --stat -- tests/` = **rỗng** (file của tôi là file MỚI untracked, không sửa test cũ nào).
- `sub-implementer-backend-0.md` biến khỏi danh sách untracked giữa chừng **không phải mất việc**:
commit WAL `eb7473f` ("wal: flush 20260729T2348") đã track nó trong lúc tôi đang chạy. Kiểm bằng
`ls -la` (file còn, 11.514 B) + `git log -- <path>`. Ghi lại vì "biến mất khỏi `git status`" rất dễ
bị đọc nhầm thành sự cố.