SUA-1..6b tron 6/6, acceptance C1-C6b 12/12. Vong-2 sua chinh vong-1 sau reviewer C7 (PASS-WITH-8, 3 MAJOR):
- Doi ba-cham -> hai-cham la REGRESSION co dieu-kien, KHONG phai cosmetic: chi tuong-duong khi origin/main
con ancestor; cay phan-ky thi hai-cham liet ca file CHI CO tren origin/main (= DA PRODUCTION) vao "phan MOI".
Thu pham that = git diff mu untracked. Sandbox 2 chieu CONFIRMED.
- git status --porcelain | cut -c4- de path RAC (rename in MOT dong "R old -> new"; path co dau cach bi quote).
Sandbox: 2/2 MISS; thuoc moi 2/2 OK.
- Thuoc CHOT 4/4 site: { git diff --name-only origin/main...HEAD; git diff --name-only HEAD;
git ls-files -o --exclude-standard; } | sort -u
Bai: chan-doan sai VAN pass acceptance — acceptance do HINH-DANG chuoi, khong do HANH-VI.
Lop mo-neo TU-VO-HIEU: con-tro so-dong tro vao CHINH tep chua no tu thoi moi lan sua dau tep
(112->118->121 trong 1 phien) => neo TEN, khong neo SO DONG. Ap cho index.css, gotchas.md, PipelineStageFolders.
Con-tro verbatim _mind doi ve commit DA PUSH (ee21056/d081681) — ban truoc tro 727a512 nam trong dai squash
=> tu de con-tro mo-coi.
H24 light-audit 9 FLAG, flush 8 o canonical: Mig 71->72 - Gotchas 86->87 - Tests 644->645 (chay that
dotnet test: 45 Domain + 600 Infra, 0 fail) - user-mem 59->60 - header S168->S179 - roster 23->26 (go han so
theo B1) - HANDOFF dong 4 slot (68)-(71) + E4.
"So noi doi theo huong BI QUAN" x3 ca/1 phien (4 slot - E4 - run.md S173) => leo thang lan 3 phai DO LAI TIEN-DE.
Kem: 2 NO vong-3 - 2 synthesis ORPHAN (run-chua-gom 2->0) - di-tru cua-so 6 vao _context - keo+ap 2 thu
loi-do-luong (BAC Lop 8 CO SO: mtime cum SE 0,7% vs hub 69%) - drift-audit thang PASS_WITH_FLAGS-8 (va 2, con 6)
- _mind 99,67%->84,2% - don 9 dir rong - archive-gate strike 1/2.
#53: 4/4 vai garble return dau, vot tron 4/4 bang resume-in-session. Mat 0 byte.
0 file code nghiep-vu. Build 2/2 app PASS. Test 645/645.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 KiB
Gotchas — SOLUTION_ERP
Bẫy/pitfall đã gặp + cách xử lý. Đọc trước khi debug tương tự để không mất thời gian. Cập nhật liên tục khi gặp bug mới.
Tech stack constraints (.NET 10 + TS 6 + Vite 8)
1. MediatR 14.x không tương thích → pin 12.4.1
Triệu chứng: Unable to resolve service for type 'MediatR.IMediator' — AddMediatR vẫn chạy nhưng không register IMediator.
Fix: Pin MediatR 12.4.1. Khi đó RequestHandlerDelegate<TResponse> là delegate không tham số (v14 có thêm CancellationToken).
2. Swashbuckle 10.x + Microsoft.OpenApi 2.x breaking change
Triệu chứng: Build fail The type or namespace 'Models' does not exist in 'Microsoft.OpenApi'. Swagger 404.
Fix:
- Remove
Microsoft.AspNetCore.OpenApikhỏi Api - Downgrade Swashbuckle về
6.9.0
3. TypeScript 6 erasableSyntaxOnly cấm enum
Fix: Dùng const + as const + typeof[keyof] pattern:
export const SupplierType = { NhaCungCap: 1 } as const
export type SupplierType = typeof SupplierType[keyof typeof SupplierType]
4. TypeScript 6 deprecate baseUrl
Fix: Bỏ baseUrl trong tsconfig, chỉ giữ paths. Paths resolve relative tsconfig location.
5. Node 22 local vs CI pin 20
Bài học NamGroup: CI build fail trên Node latest.
Fix:
package.jsonengines:">=20"(min, không upper).nvmrc=20cho CI- GitHub/Gitea Actions:
actions/setup-node@v4vớinode-version: '20.x'
EF Core 10
6. Expression tree không support switch expression
Triệu chứng: CS8514: An expression tree may not contain a switch expression.
Fix: Tách switch ra ngoài LINQ:
var hasPermission = action switch
{
"Read" => await query.AnyAsync(p => p.CanRead),
"Create" => await query.AnyAsync(p => p.CanCreate),
_ => false,
};
7. Design-time DbContext resolve fail
Triệu chứng: dotnet ef migrations add → Unable to resolve service for type 'DbContextOptions<T>'.
Fix: Tạo IDesignTimeDbContextFactory<ApplicationDbContext> trong Infrastructure.
8. AddDefaultTokenProviders() không có trong AddIdentityCore
Fix: Bỏ call nếu chưa cần password reset. Khi cần, chuyển AddIdentity hoặc add package Microsoft.AspNetCore.Identity.UI.
OpenXml / ClosedXML
9. SpaceProcessingModeValues namespace
Fix: Full path + wrap EnumValue<>:
textElement.Space = new DocumentFormat.OpenXml.EnumValue<
DocumentFormat.OpenXml.SpaceProcessingModeValues>(
DocumentFormat.OpenXml.SpaceProcessingModeValues.Preserve);
10. Placeholder {{field}} bị split runs
Vấn đề: Word hay split text thành nhiều <w:t> — placeholder miss khi regex replace.
Fix: Iterate Paragraph, gom text tất cả <w:t> → replace → gán lại text đầu + clear rest. Đã implement trong DocxRenderer.
11. Word COM SaveAs PowerShell type conversion
Fix: Dùng SaveAs2:
$doc.SaveAs2($outPath, 16) # 16 = wdFormatDocumentDefault
12. Word COM stuck
Fix:
$word.DisplayAlerts = 0- Nếu stuck →
Get-Process WINWORD | Stop-Process -Force - Fallback: LibreOffice headless
soffice --headless --convert-to docx
System.Text.Json
13. Record deserialization fail với Unicode qua CLI
Triệu chứng: POST JSON tiếng Việt từ Windows bash/curl → 400 "JSON value could not be converted".
Fix: Dùng curl --data-binary @file.json (file UTF-8). API handle đúng qua axios/Swagger.
File operations
14. Dropbox sync có thể revert file đang edit
Triệu chứng: Write thành công, build pass, runtime chạy code cũ.
Fix: Sau Write quan trọng → Read lại verify. Nếu revert → Write lại.
15. .gitignore wwwroot rules
wwwroot/uploads/→ ignore (user files)wwwroot/templates/→ commit (source of truth)wwwroot/exports/→ ignore (temp)
Dev workflow
16. Port conflict khi restart dev server
Fix: TaskStop task cũ, hoặc netstat -ano | findstr :8082 → taskkill /F /PID <pid>.
17. EF migration 3-file rule
Mỗi migration tạo: {name}.cs + {name}.Designer.cs + ApplicationDbContextModelSnapshot.cs. Commit đủ 3.
Claude Code harness quirks
18. Edit tool "File not read" sau system-reminder
Triệu chứng: Edit file vừa Read, lỗi "File has not been read yet".
Nguyên nhân: System reminder interrupt reset read-cache.
Fix: Read lại file rồi Write/Edit. Hoặc dùng Write (ghi đè full) thay Edit.
19. Build pass nhưng DI thiếu registration
Triệu chứng: dotnet build → 0 errors nhưng runtime throw Unable to resolve service.
Nguyên nhân: C# compiler chỉ check type, không check DI graph.
Fix: Sau thêm interface mới + impl → luôn add services.AddScoped<IX, X>() trong DependencyInjection.cs. Test API start up là OK check.
Contract workflow
20. Mã HĐ gen 2 lần sau reject → approve lại
Fix: Check if (contract.MaHopDong is null) trước khi gen. Đã implement trong ContractWorkflowService.TransitionAsync.
21. BE adjacency vs FE NEXT_PHASES sync (RESOLVED)
Đã xử lý: FE không còn hardcode NEXT_PHASES nữa. BE expose contract.workflow.nextPhases trong ContractDetailDto từ WorkflowPolicyRegistry.ForContract(contract). FE render dynamic từ đó — single source of truth.
Nếu đổi policy BE: chỉ cần update WorkflowPolicies.Standard hoặc WorkflowPolicies.SkipCcm trong Domain/Contracts/WorkflowPolicy.cs. FE tự reflect.
22. Race condition gen mã HĐ khi 2 user cùng transition tới DangDongDau
Fix: IsolationLevel.Serializable transaction trong ContractCodeGenerator. Không skip.
42. Dual schema workflow V1 vs V2 — Service phải branch theo pin field (Session 17)
Symptom: Phiếu PE pin V2 (ApprovalWorkflowId set qua workspace Select) nhưng Service vẫn match approver theo schema cũ (Dept+PositionLevel). Approver V2 không duyệt được, button Duyệt báo Forbidden.
Root cause: Sau Mig 23-24 entity PE có 2 field workflow pin:
WorkflowDefinitionId(Mig 21 V1 legacy) — pin schema flat cũApprovalWorkflowId(Mig 23 V2 mới) — pin schema 3-table mới
Service trước đó chỉ đọc WorkflowDefinitionId → bỏ qua V2.
Fix (b41484b): PurchaseEvaluationWorkflowService.TransitionAsync branch:
if (evaluation.ApprovalWorkflowId is Guid awId)
await ApproveV2Async(evaluation, awId, ...); // iterate ApprovalWorkflowSteps + Levels match ApproverUserId
else
await ApproveV1LegacyAsync(evaluation, ...); // iterate WorkflowSteps match Dept+PositionLevel
Pattern reusable khi wire schema mới song song schema cũ (Contract V2 sắp tới): pin field flag để rẽ logic, KHÔNG drop legacy ngay (giữ backward compat phiếu cũ).
43. Step.Order ≠ index 0-based — không thể EF query trực tiếp (Session 17)
Symptom: Implement ResolveV2InboxIdsAsync (V2-aware Inbox) bằng EF query thẳng:
.Where(s => s.Order == e.CurrentWorkflowStepIndex.Value + 1) // FAIL — Step.Order là logical, không phải position
Logic sai: WorkflowStep.Order không phải position 0-based mà là số sort thứ tự (vd 5, 10, 20). Steps.OrderBy(s => s.Order).ToList()[idx] mới đúng.
Fix: Precompute candidates EF query → in-memory sort by Order → array index access:
var candidates = await db.PE.Where(...).ToListAsync(ct); // Step 1: EF lấy phiếu V2 pending
var workflows = await db.AW.Include(...).ToDictionaryAsync(w => w.Id, ct);
foreach (var c in candidates) {
var steps = wf.Steps.OrderBy(s => s.Order).ToList(); // Step 2: in-memory sort
var step = steps[c.CurrentWorkflowStepIndex.Value]; // Step 3: array index
...
}
Trade-off: scalable đến vài trăm phiếu pending, không ngon cho >10k. Optimize sau nếu cần.
Permission matrix
23. Permission update không real-time
Triệu chứng: Admin tick permission cho role X → user X vẫn thấy menu cũ.
Nguyên nhân: FE cache menu trong localStorage, không auto refetch.
Fix: User phải logout/login. Phase 3 iteration 2 có thể thêm SignalR push "permission-changed" → FE tự refetch /menus/me.
24. MenuKey typo — không check type
Fix: Luôn dùng MenuKeys.Contracts const (BE) + MenuKeys.Contracts (FE menuKeys.ts). Không hardcode string.
IIS / Windows Server
25. Install-WindowsFeature Web-WebSockets khóa section <webSocket> ở applicationHost
Triệu chứng: Sau khi install WebSocket feature → TẤT CẢ IIS site có <webSocket enabled="true" /> trong web.config trả về HTTP 500.19 với error code 0x80070021 "configuration section cannot be used at this path" — kể cả site khác project không liên quan.
Nguyên nhân: Feature install thêm <webSocket> section vào applicationHost.config với overrideModeDefault="Deny". Site web.config override section đó → fail.
Fix: Unlock section ở server level:
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" unlock config -section:system.webServer/webSocket
Tương tự khi dùng URL Rewrite <serverVariables> cần unlock system.webServer/rewrite/allowedServerVariables.
Cảnh báo co-existence: Trên VPS shared với project khác, enable feature mới qua Install-WindowsFeature có thể làm sập site project khác. Luôn test all site sau mỗi enable.
SignalR / Realtime
26. SignalR WebSocket không cho custom Authorization header
Triệu chứng: new HubConnectionBuilder().withUrl('/hubs/x', { headers: { Authorization: ... } }) — WebSocket transport vẫn 401.
Nguyên nhân: Browser WebSocket API không cho set custom headers cho handshake. Chỉ 2 transport khác (SSE / LongPolling) mới dùng headers.
Fix:
- FE: dùng
accessTokenFactory: () => token— SignalR client tự append?access_token=query cho WebSocket - BE: Wire JWT bearer
OnMessageReceivedđể đọc token từ query khi path matches/hubs/*:
options.Events = new JwtBearerEvents {
OnMessageReceived = ctx => {
var accessToken = ctx.Request.Query["access_token"];
var path = ctx.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
ctx.Token = accessToken;
return Task.CompletedTask;
}
};
27. SignalR SaveChangesInterceptor — capture Added ở SavingChanges, push ở SavedChanges
Lý do: SavedChanges chỉ có entries sau commit thành công. Nhưng ở SavedChanges thì EntityEntry.State đã về Unchanged → không thể filter Added.
Fix: 2-phase pattern:
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(...) {
_pending = eventData.Context.ChangeTracker.Entries<Notification>()
.Where(e => e.State == EntityState.Added)
.Select(e => e.Entity).ToList();
return base.SavingChangesAsync(...);
}
public override async ValueTask<int> SavedChangesAsync(..., int result, ...) {
foreach (var n in _pending) await _realtimeNotifier.PushAsync(n);
_pending.Clear();
return result;
}
DevOps / CI/CD
28. LibreOffice download URL 404 khi pin wrong version
Triệu chứng: Invoke-WebRequest https://download.documentfoundation.org/libreoffice/stable/25.2.7/... → 404.
Nguyên nhân: LibreOffice mirror chỉ giữ vài version mới nhất. 25.2.7, 24.8.7 không có. Chỉ 25.8.6 tồn tại tại thời điểm cài.
Fix: Check mirror URL trước khi pin. Dùng Invoke-WebRequest -Method Head verify trước download thật.
29. PowerShell 5.1 >> $GITHUB_PATH ghi UTF-16 → NUL byte crash Gitea Actions
Triệu chứng: Gitea Actions job fail với "NUL byte in PATH". echo "C:\\dotnet" >> $env:GITHUB_PATH.
Nguyên nhân: PS 5.1 default encoding UTF-16 LE BOM khi redirect >>. Gitea reads PATH as UTF-8 → NUL byte xuất hiện sau mỗi ASCII char.
Fix: Dùng Out-File -Encoding utf8 -Append:
"C:\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
Hoặc drop step GITHUB_PATH hoàn toàn nếu NSSM PATH đã có sẵn dotnet+node.
30. PS 5.1 scripts với Vietnamese diacritics → parser error
Triệu chứng: Cannot parse script: Unexpected character khi chạy PS script có text tiếng Việt inline.
Nguyên nhân: PS 5.1 đọc file script với ANSI codepage (Windows-1258 hoặc default 1252), không phải UTF-8.
Fix (1): Save script với BOM UTF-8 (Write-Host có dấu vẫn work):
[System.IO.File]::WriteAllText($path, $content, [System.Text.Encoding]::UTF8)
Fix (2, safer): Rewrite script ASCII-only. Text tiếng Việt nằm trong log messages thay dùng code:
Write-Host "Setup IIS sites done" # thay vi "Hoan tat"
TypeScript / FE
31. Dialog size="xl" TS2322 nếu variant không khai báo
Triệu chứng: <Dialog size="xl"> → Type '"xl"' is not assignable to type '"sm" | "md" | "lg"'.
Fix: Sửa usage về "lg", hoặc add "xl" vào DialogSize type union trong components/ui/Dialog.tsx. Đừng lazy as any.
FE architecture
32. NavLink end prop cho query-param URL variants
Triệu chứng: /contracts?type=1 highlight cả /contracts lẫn /contracts?type=2 cùng lúc.
Nguyên nhân: Default NavLink startsWith match. Query string không parse distinct paths.
Fix: end={path.includes('?')} trong resolvePath để query-variants match exact:
<NavLink to={path} end={path.includes('?')}>
IIS / Windows Server (continued)
33. IPv4/IPv6 port hijack trên VPS shared (G-084)
Triệu chứng: git.baocaogiaoduc.vn trả về homepage Next.js của VietReport
thay vì Gitea UI. Headers lộ x-nextjs-cache: HIT + X-Powered-By: ARR/3.0
(request đã qua IIS ARR proxy rồi mới hit Next.js).
Root cause: Next.js app (NSSM service) được deploy lên VPS shared với
Gitea, ignore env PORT=3001 HOSTNAME=127.0.0.1 và bind 0.0.0.0:3000.
Gitea bind 0.0.0.0:3000 trước đó bị Windows fallback xuống IPv6-only
[::]:3000 (default IPV6_V6ONLY=1). IIS ARR rewrite http://localhost:3000
→ Windows DNS resolve IPv4 first → hit Next.js → leak homepage cho TẤT CẢ
subdomain có ARR proxy về :3000.
Fix (VietReport applied):
- Next.js NSSM env
PORT=3001 HOSTNAME=127.0.0.1— bind loopback IPv4 - Gitea
HTTP_ADDR=127.0.0.1— bind loopback IPv4 explicit - IIS
web.configrewrite URL dùng127.0.0.1thaylocalhost - NSSM
DependOnService=gitea— boot order tránh race
3 rules rút ra — áp dụng mọi service trên VPS shared:
- Reverse-proxy luôn IP literal
127.0.0.1, KHÔNG dùnglocalhost - Backend services bind loopback IPv4 explicit, KHÔNG
0.0.0.0 - Service dependency cho boot order khi nhiều service cùng port family
SOLUTION_ERP relevance:
- API host trong IIS app pool out-of-process (ANCM tự quản lý port Kestrel ephemeral) → risk THẤP
- FE gọi trực tiếp
https://api.solutions.com.vn(không ARR proxy) → risk THẤP - NHƯNG nếu tương lai thêm ARR reverse proxy (fe-admin/user
/apiproxy) hoặc deploy Kestrel standalone qua NSSM → PHẢI apply 3 rules trên - Scripts + skill doc đã update
localhost→127.0.0.1để đồng bộ
FE routing + state (Phase 6)
34. React Router NavLink isActive chỉ match pathname, không query string
Triệu chứng: 2 NavLink cùng pathname (/purchase-evaluations?type=2 vs
/purchase-evaluations?type=2&pendingMe=1) cùng highlight khi URL là một
trong 2. User thấy menu "Danh sách" + "Duyệt" active đồng thời.
Nguyên nhân: React Router v6 NavLink's built-in isActive chỉ so
pathname. end prop chỉ thêm exact-match cho pathname segment, không check
query string.
Fix: Custom isActive với queryMatches helper (URLSearchParams set
equality). Xem Layout.tsx cả 2 FE:
function queryMatches(current: string, target: string): boolean {
const a = new URLSearchParams(current)
const b = new URLSearchParams(target)
const aKeys = [...a.keys()].sort()
const bKeys = [...b.keys()].sort()
if (aKeys.length !== bKeys.length) return false
return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
}
function MenuLeaf({ node }: { node: MenuNode }) {
const location = useLocation()
const path = resolvePath(node.key)
const [targetPath, targetQuery = ''] = path.split('?')
const isActive = location.pathname === targetPath
&& queryMatches(location.search.replace(/^\?/, ''), targetQuery)
return <NavLink to={path} className={isActive ? 'active' : ''}>...</NavLink>
}
35. Menu tree inheritance phải extend khi thêm root mới
Triệu chứng: Admin/role đã grant PurchaseEvaluations.Read (inherit parent)
nhưng menu children Pe_DuyetNcc_List / Pe_DuyetNcc_Create không hiển thị.
Chỉ thấy root PurchaseEvaluations ở Layout sidebar.
Nguyên nhân: GetMyMenuTreeQuery hardcode 2 inherit root: Contracts và
Workflows. Descendant Ct_/Wf_ auto-inherit CRUD flags từ parent qua switch
statement. Khi thêm root mới (PurchaseEvaluations, PeWorkflows) — không có
trong switch → children mặc định (false,false,false,false) → filter
HasAccess hide children.
Fix: Extend switch + nextInherit propagation:
var contractsFlags = GetFlags(MenuKeys.Contracts);
var workflowsFlags = GetFlags(MenuKeys.Workflows);
var peFlags = GetFlags(MenuKeys.PurchaseEvaluations); // NEW
var peWorkflowsFlags = GetFlags(MenuKeys.PeWorkflows); // NEW
// Trong BuildChildren:
if (inheritFromKey is not null && !resolved.ContainsKey(m.Key))
{
flags = inheritFromKey switch
{
var k when k == MenuKeys.Contracts => contractsFlags,
var k when k == MenuKeys.Workflows => workflowsFlags,
var k when k == MenuKeys.PurchaseEvaluations => peFlags, // NEW
var k when k == MenuKeys.PeWorkflows => peWorkflowsFlags, // NEW
_ => flags,
};
}
var nextInherit = inheritFromKey
?? (m.Key == MenuKeys.Contracts ? MenuKeys.Contracts
: m.Key == MenuKeys.Workflows ? MenuKeys.Workflows
: m.Key == MenuKeys.PurchaseEvaluations ? MenuKeys.PurchaseEvaluations
: m.Key == MenuKeys.PeWorkflows ? MenuKeys.PeWorkflows
: null);
Rule: Khi thêm 1 root mới có child leaves (vd PeWorkflows → PeWf_*) —
PHẢI update cả 3 chỗ: (1) MenuKeys.All, (2) GetMyMenuTreeQuery GetFlags + switch,
(3) nextInherit propagation.
36. Vite env var embed compile-time — đổi .env.production phải rebuild FE
Triệu chứng: Đổi VITE_API_BASE_URL=... trong .env.production nhưng FE
vẫn gọi URL cũ. Hot reload không giúp.
Nguyên nhân: Vite inline import.meta.env.VITE_* tại build time vào JS
bundle (minified). File .env* chỉ đọc khi vite build — không runtime.
Fix: Sau đổi env:
- Rebuild:
cd fe-admin ; npm run build - Deploy dist mới lên IIS
- Clear CDN/browser cache (Ctrl+Shift+R)
Verify bundle có URL mới: curl dist/assets/index-*.js | grep -oE 'https://[^"]+api'.
Deploy / Production (continued)
37. PowerShell 5.1 diacritics trong script — gotcha #30 tái phát
Triệu chứng (bis): migrate-domains.ps1 viết với "Phương Án", "→",
em-dash → PS 5.1 parser fail Missing closing '', Unexpected character.
Fix: Luôn ASCII-only cho .ps1 — rule lặp lại gotcha #30. Cách phát hiện: grep file cho UTF-8 multi-byte chars trước khi deploy:
grep -P '[\x80-\xff]' scripts/*.ps1
# Nếu có match → rewrite ASCII-only
Email / Users
38. Email rename Identity user — 4 field cần update đồng thời
Triệu chứng: Đổi user.Email xong login với email mới vẫn 401. Hoặc
UserManager.FindByEmail trả null.
Nguyên nhân: Identity lookup qua NormalizedEmail (uppercase), không
Email. Username cũng dùng email. 4 field phải sync:
u.Email = newEmail;
u.NormalizedEmail = newEmail.ToUpperInvariant();
u.UserName = newEmail;
u.NormalizedUserName = newEmail.ToUpperInvariant();
await userManager.UpdateAsync(u);
Bonus: Check conflict trước khi rename (user khác đã có email mới) → skip để tránh duplicate.
39. act_runner v0.2.13 fetch actions/checkout từ github.com timeout 21s
Triệu chứng: Run #108/#109 fail trong 22s với:
Get "https://github.com/actions/checkout/info/refs?service=git-upload-pack":
dial tcp 20.205.243.166:443: connectex: A connection attempt failed
because the connected party did not properly respond...
Test gate (Domain + Infra) chưa kịp chạy. Build/deploy không tới.
Nguyên nhân: act_runner mỗi run đều git fetch action source code từ
github.com (kiểm tra update actions/checkout@v4). Khi VPS → github.com
TCP có vấn đề (intermittent firewall/network), 21s timeout → toàn job fail
TRƯỚC step nào của workflow chạy.
Fix: Thay uses: actions/checkout@v4 bằng manual git checkout từ Gitea
internal — bypass github.com hoàn toàn.
- name: Checkout (manual git, bypass github.com)
shell: powershell
run: |
git config --global --add safe.directory '*'
git init -q
git remote add origin "https://gitea-actions:${{ github.token }}@git.baocaogiaoduc.vn/${{ github.repository }}.git"
$ref = "${{ github.ref }}"
if ($ref -like "refs/heads/*") { $ref = $ref.Substring(11) }
git fetch --depth=30 origin $ref
git checkout --quiet "${{ github.sha }}"
Tương tự với actions/upload-artifact@v4 — bỏ vì cũng phụ thuộc github.com.
TRX file vẫn save local trong test-results/ cho debug.
Long-term option: config github_mirror trong gitea-runner config.yaml
mirror github.com → Gitea internal repo. Hoặc pre-cache .cache/act/<hash>/
manually 1 lần.
Reference: Run #108 commit 52999f3 fail, run #110 commit 14b7d18 fix pass.
40. npm junction cache tsc not found sau Move-Item — chưa xác định root cause
Triệu chứng: Implement npm cache strategy bằng junction (mklink /J) +
Move-Item node_modules → cache dir → fail 'tsc' is not recognized ở step
npm run build. Log NO Write-Host "cache MISS" output, NO npm install
output. Timing 1.6s từ end-of-BE-build → start-of-fe-admin npm run build
(impossible cho npm install 49s).
Hypothesis:
- (A) Move-Item của
node_moduleschứa nested junctions/symlinks → .bin/ relative paths broken sau move - (B) act_runner PowerShell stream capture có quirk với cache MISS branch → output bị silenced
- (C)
Test-Pathtrả về stale TRUE từ một state khác
Workaround tạm: Rollback về fresh npm install mỗi run (49s + 33s = 82s).
Path filter docs-only skip CI là alternative win lớn hơn.
TODO khi debug session sau:
- Thử
robocopy /MIRthayMove-Item(handle symlinks tốt hơn) - Hoặc Copy-Item với
-Force -Recurse(slower nhưng safer) - Hoặc dùng act_runner built-in
cache.hostserver (có sẵn trong config.yaml)
Reference: Run #111 commit 29eb5d9 fail, rollback ở a21790d.
41. Gitea Actions paths-ignore — workflow file change vẫn trigger
Triệu chứng: Setup paths-ignore: ['docs/**', '**/*.md'] để skip CI
khi commit MD-only. Tự nhiên commit .gitea/workflows/deploy.yml (chính
workflow file) cũng bị skip → không thể test workflow change.
Nguyên nhân: paths-ignore evaluate set của file thay đổi. Nếu TẤT CẢ
file thay đổi match patterns → skip. Workflow file .gitea/workflows/**
không trong list ignore → trigger normal. OK behavior.
Edge case ngược: commit thay đổi cả docs/STATUS.md + src/Backend/...cs
→ NOT skip vì có file ngoài ignore patterns. Cũng OK.
Verify: Commit chỉ touch docs/STATUS.md → check Gitea Actions UI →
phải KHÔNG có run mới trigger. Test với curl /api/v1/.../runs/<id>
trả Not found cho run-id tiếp theo.
Pattern hiện áp dụng:
on:
push:
branches: [main]
paths-ignore:
- 'docs/**'
- '**/*.md'
- '.claude/skills/**'
- '.gitignore'
- 'scripts/**.md'
KHÔNG ignore: .gitea/workflows/**, *.cs, *.tsx, *.ts, *.csproj,
*.json, *.slnx, tests/**.
Saving: ~196s/commit cho ~30% commit thuộc loại docs-only (chốt MD, session log, etc).
Reference: Commit 29eb5d9 add filter, verify ở commit 512880c
(docs-only) → Gitea NO trigger run #113.
44. Silent 403 từ class-level [Authorize(Policy = ...)] quá strict (Session 18)
Triệu chứng: UAT 2026-05-08 — Drafter nv.test Workspace tạo phiếu B, dropdown "Quy trình duyệt" empty silent. Admin Designer cùng URL endpoint thấy data đầy đủ. Không có toast error / network panel hint.
Root cause: ApprovalWorkflowsV2Controller class-level [Authorize(Policy = "Workflows.Read")] → non-admin role (Drafter chỉ có PurchaseEvaluations.Read) bị 403 Forbidden khi GET /api/approval-workflows-v2. TanStack Query catch HTTP error trả data=undefined, FE component render dropdown empty không có "loading" / "error" state visible.
Fix (f77ea38): Tách policy theo action — class-level [Authorize] only (any authenticated), action-level chỉ POST/DELETE giữ [Authorize(Policy = "Workflows.Create")]:
[ApiController]
[Route("api/approval-workflows-v2")]
[Authorize] // ← any authenticated, không hardcode policy
public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase
{
[HttpGet] // ← Drafter pick workflow lúc create — read-only OK
public async Task<...> Overview(...) { ... }
[HttpPost]
[Authorize(Policy = "Workflows.Create")] // ← admin Designer
public async Task<...> Create(...) { ... }
}
Pattern reusable: Endpoint dùng cho nhiều use case (admin Designer + user list-pick) — split policy per action thay vì class-level uniform. Read-only list workflow KHÔNG nhạy cảm (chỉ là cấu hình quy trình, không expose business data).
Phòng tránh tương lai: Khi controller class-level [Authorize(Policy)], audit role nào cần access từng action. Nếu GET cần broader role hơn POST → split policy. Default [Authorize] (any authenticated) cho list-pick endpoint.
FE diagnostic improvement: TanStack Query error nên hiển thị warning UI (toast hoặc banner) thay vì silent. Hiện tại useQuery catch silent → debug khó. Future: wire onError handler global show generic error toast.
45. PE "Trả về nhưng hệ thống vẫn duyệt" — FE button label vs decision payload mismatch (Session 21 turn 3)
Triệu chứng: UAT 2026-05-12 — User bro screenshot button labeled ← Trả lại trong PE Workflow Panel (menu "Duyệt"), nhấn vào nhưng phiếu KHÔNG về phase TraLai — ngược lại tiến qua Cấp tiếp theo (hệ thống ghi nhận approve). User mô tả hành vi: "Trả về nhưng hệ thống vẫn duyệt".
Root cause: PeWorkflowPanel.tsx có 3 chỗ check transition type với logic KHÔNG sync giữa nhau:
- L205-207
isSendBack(button label color): include cảDangSoanThaolẫnTraLaitừ phase trung gian → label hiển thị← Trả lạiđúng. - L64-66
isReject(payloaddecisiongửi BE): CHỈ checkDangSoanThao, thiếuTraLai→ khi target=TraLai (98),isReject=false→ payloaddecision: 1(Approve) thay vì2(Reject). - L247-248 dialog
isSendBack(title + warning): CHỈ checkDangSoanThao, thiếuTraLai→ dialog title fallback'✓ Duyệt → Trả lại'(sai semantic) + KHÔNG hiển thị amber warning "Phiếu sẽ về Đang soạn thảo".
BE PurchaseEvaluationWorkflowService.TransitionAsync:
- L51
if (decision == Reject)branch → set Phase=TraLai correctly khi decision=Reject. - L97
APPROVE STEPbranch khi decision=Approve + fromPhase=ChoDuyet →ApproveV2AsyncUPSERT opinion = "đã duyệt" + advance Cấp. - → Khi FE gửi
decision=1(do bugisReject), BE đi vào nhánh APPROVE thay vì REJECT → phiếu được ghi nhận approve dù user định trả lại.
Severity: 🔴 CRITICAL — data integrity issue. NV nhấn "Trả lại" sẽ vô tình "duyệt" phiếu sang Cấp tiếp theo + UPSERT opinion vĩnh viễn vào PurchaseEvaluationLevelOpinions (Mig 26). Khó rollback vì BE đã SaveChangesAsync.
Fix Chunk A (de00887 BE defense-in-depth):
// PurchaseEvaluationWorkflowService.cs sau set isAdmin/isSystem (L48), trước REJECT branch (L51)
if ((targetPhase == PurchaseEvaluationPhase.TraLai
|| targetPhase == PurchaseEvaluationPhase.TuChoi)
&& decision != ApprovalDecision.Reject)
{
throw new ConflictException(
$"Transition tới {targetPhase} BẮT BUỘC decision=Reject (nhận {decision}). " +
"Báo lỗi caller — payload mismatch giữa target phase và decision.");
}
Boundary protection cho mọi caller tương lai (API client / mobile / cron retry). 3 regression test:
TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState(bug reproduce)TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState(consistency cover)TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai(happy path control)
Fix Chunk B (4b29d00 FE mirror 2 app):
// PeWorkflowPanel.tsx (fe-user + fe-admin) — 3 chỗ × 2 app
// Chỗ 1: isReject payload (line 64-66)
const isReject = target === PurchaseEvaluationPhase.TuChoi
|| (target === PurchaseEvaluationPhase.DangSoanThao
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao)
|| (target === PurchaseEvaluationPhase.TraLai // ← THÊM
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai)
// Chỗ 2: dialog isSendBack (line 247-248)
const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao
|| target === PurchaseEvaluationPhase.TraLai) // ← THÊM
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai // ← THÊM
Chỗ 3 (button label isSendBack L205-207) đã đúng từ S17, KHÔNG đụng.
Pattern reusable — invariant check khi viết FE workflow transition:
- Button label condition (visual) phải SYNC với payload decision (semantic).
- Dialog title/warning condition phải SYNC với button label + payload.
- Tốt nhất: extract
isReject(target, currentPhase)thành 1 helper FE + BE share semantic — KHÔNG duplicate logic giữa 3 chỗ.
Phòng tránh tương lai:
- Khi spec mới có thêm phase terminal/intermediate (vd Session 17 thêm TraLai làm Phase RIÊNG thay vì DangSoanThao revert), audit grep TOÀN BỘ logic check
=== DangSoanThaođể xem chỗ nào cần thêm|| === NewPhase. - BE guard early invariant
(targetPhase ∈ terminalSet) ⇔ (decision == Reject)thay vì trust FE payload. - Test-before bug fix BẮT BUỘC §7 — 3 test cover bug reproduce + consistency + happy path.
References:
- Commit fix:
de00887(BE Chunk A) +4b29d00(FE Chunk B) - Spec Session 17:
feedback_n_stage_workflow_patternDEPRECATED + spec mới trongPurchaseEvaluationWorkflowService.cscomment L15-19 - State machine 5 trạng thái: Nháp / Đã gửi duyệt / Trả lại (98) — Phase RIÊNG / Từ chối / Đã duyệt
46. Gitea Actions API path /tasks not /runs + cache stale ~2 min (CICD Monitor S21 t4 discovery)
Triệu chứng: CICD Monitor sub-agent S21 t4 run đầu poll Gitea Actions runs sau push → GET https://git.baocaogiaoduc.vn/api/v1/repos/vietreport-admin/solution-erp/actions/runs?limit=5 → 404 Not Found. Tưởng repo không có Actions enabled hoặc API endpoint sai. Debug 10 phút retry path/auth/header trước khi tìm ra đúng path.
Root cause: Gitea API v1 spec dùng /actions/tasks (NOT /actions/runs). Naming khác GitHub Actions API (GitHub: /actions/runs). Public no-auth read OK cho repo public.
Endpoint đúng:
GET https://git.baocaogiaoduc.vn/api/v1/repos/vietreport-admin/solution-erp/actions/tasks?limit=5
Response fields:
id(task internal ID)run_number(display number Run #N)head_sha(commit triggered task)status(queued / running / success / failure / cancelled)conclusion(final state when status=success/failure)created_at,updated_atdisplay_title(commit message summary)
Match task to commit: Filter head_sha == $commitSha thay vì rely on order.
Bonus gotcha — cache stale: updated_at field caches ~2 phút sau khi deploy thật xong (act_runner ghi log final, Gitea API chưa update DB ngay). Cross-check VPS file timestamps khi cần time-sensitive verify:
ssh vietreport-vps "Get-Item C:\inetpub\solution-erp\admin\index.html | Select LastWriteTime"
ssh vietreport-vps "Get-Item C:\inetpub\solution-erp\api\SolutionErp.Api.dll | Select LastWriteTime"
File LastWriteTime VPS = thực sự deploy completion time (NSSM copy + IIS recycle xong).
Phòng tránh tương lai:
- CICD Monitor system prompt + Bash command preset đã update sang
/actions/tasks(saved trong MEMORY S21 t4). - Khi setup automation script với Gitea API → đọc rõ Gitea API spec v1.20+ (
https://docs.gitea.com/api/) thay vì assume GitHub naming. - Time-sensitive verify (vd "deploy xong chưa, có an toàn trigger task tiếp không"): KHÔNG trust API status timestamp đơn lẻ → cross với VPS file mtime hoặc curl bundle hash live.
47. .claude/agent-memory/** paths-ignore — hypothesis disproven, preventive note cho non-.md state files (S22 discovery + S22 chốt revise)
Triệu chứng ban đầu (em main HYPOTHESIS S22): Cuối session flush 3-4 sub-agent MEMORY.md drift patch + commit dạng [CLAUDE] Docs: chốt S22 ... — em main đoán push trigger Gitea Actions full deploy ~3.5min waste vì .claude/agent-memory/** thiếu trong paths-ignore.
Verify thực tế (CICD Monitor Run #193 S22 chốt — 2026-05-13 23:16):
Hypothesis DISPROVEN. paths-ignore của workflow file đã có pattern **/*.md — glob này match mọi .md file ở mọi độ sâu (kể cả .claude/agent-memory/{investigator,implementer,reviewer,cicd-monitor}/MEMORY.md).
Push cc8a7d3 (Docs S22 chốt + 4 agent MEMORY flush, 8 files tất cả là .md) → CI correctly SKIPPED. Run #193 cuối cùng cho code change là b04a11a (Mig 30 BE+FE), KHÔNG phải cc8a7d3 Docs.
Cross-check: git show --name-only cc8a7d3 → 8 files với extension .md only → match **/*.md glob → SKIPPED đúng.
Preventive note cho future: Nếu tương lai add non-.md state files vào .claude/agent-memory/ (vd archive.json, metrics.log, cache.bin) — sẽ trigger CI vì KHÔNG match **/*.md. Khi đó add .claude/agent-memory/** vào paths-ignore explicit:
paths-ignore:
- 'docs/**'
- '**/*.md'
- '.claude/skills/**'
- '.claude/agent-memory/**' # ← preventive nếu add non-.md state files
Severity: Informational (hiện tại KHÔNG có vấn đề thật vì all MEMORY files là .md). KHÔNG cần fix .gitea/workflows/deploy.yml.
Cross-ref: Gotcha #41 paths-ignore docs-only skip pattern.
Lesson: Verify hypothesis qua actual Gitea API task list TRƯỚC KHI claim CI waste. Em main S22 đoán nhầm — CICD Monitor catch sai.
References:
- CICD Monitor Run #186 (S21 t4 2026-05-13 19:13) — first discovery
- CICD Monitor Run #187 (S21 t5 2026-05-13 20:12) — confirmed pattern + cache stale bonus
- Memory
feedback_multi_agent_setupPlan G Trial Week 1 evidence
48. Multi-Changelog.Add() trong cùng SaveChangesAsync → SQLite frozen-clock tie-break → tests OrderByDescending(CreatedAt).First() non-deterministic (Session 25 Plan AB + Run #215 catch)
Triệu chứng: Plan AB Chunk A cdfd542 add SECOND Changelog.Add() entry vào ApplyReturnModeAsync (cover Bug 2 — Return mode log) end-of-function. Caller TransitionAsync:100 đã có sẵn LogTransitionAsync add FIRST Changelog entry (Action=Transition + ContextNote=comment chứa "không lùi được"). 2 entries cùng SaveChangesAsync transaction → SQLite test fixture frozen clock → CreatedAt identical microseconds cho cả 2 rows.
Plan M edge case tests (S23 t3) query .OrderByDescending(c => c.CreatedAt).FirstAsync() assert ContextNote.Contains("không lùi được") — sau Plan AB, SQLite tie-break non-deterministic, pick Plan AB row (EntityType=Workflow, Action=Update, ContextNote=null) → Expected ContextNote not to be <null> FAIL.
CI Run #215 sha=cdfd542 test_infra FAIL 2/53 (51 PASS, 2 FAIL):
ApplyReturnMode_OneStep_AtStep1_ResetsToBuoc1Cap1_KeepsChoDuyet(line 350)ApplyReturnMode_OneLevel_AtStep1Level1_ResetsToBuoc1Cap1_KeepsChoDuyet(line 308)
Test gate caught regression → deploy never reached → prod spared broken state.
Fix Option A (chốt): Test query filter by Summary.Contains("Chuyển phase") để pick đúng LogTransition entry. Plan AB BE code stays clean.
// Trước Plan AB Chunk A2:
var changelog = await db.PurchaseEvaluationChangelogs
.Where(c => c.PurchaseEvaluationId == pe.Id)
.OrderByDescending(c => c.CreatedAt)
.FirstAsync();
// Sau Plan AB Chunk A2 fix (commit 8c05947):
var changelog = await db.PurchaseEvaluationChangelogs
.Where(c => c.PurchaseEvaluationId == pe.Id && c.Summary!.Contains("Chuyển phase"))
.OrderByDescending(c => c.CreatedAt)
.FirstAsync();
Pattern reusable: Khi handler/service add NEW Changelog row trong existing flow đã có LogTransition row, tests query audit table MUST filter EntityType / Action / Summary keyword discriminator thay vì raw OrderByDescending timestamp. Cross-ref Contract V2 test setup tương lai.
Severity: Major — caught by CI before prod ship, no user impact. Lesson reinforced UAT mode feedback_uat_skip_verify skip dotnet test per chunk RISK khi BE refactor > 100 LOC + signature change → em main resumed local test verify post Plan AB Chunk A2.
References:
- Plan AB Chunk A commit
cdfd542(Run #215 FAIL) - Plan AB Chunk A2 fix commit
8c05947(Run #216 PASS) - Memory
feedback_uat_skip_verifylesson reinforced S25 - File:
tests/SolutionErp.Infrastructure.Tests/Services/PurchaseEvaluationWorkflowServiceReturnModeTests.cs:304-310, 346-352
49. UI dual-phase badge fromPhase → toPhase gây nhầm khi 3/4 Reject mode giữ Phase=ChoDuyet (Session 25 Plan AD)
Triệu chứng: Bro UAT 2026-05-19 Plan AC deploy: panel "Lịch sử duyệt" 6 entries TẤT CẢ hiện Đã gửi duyệt → Đã gửi duyệt (vì 3 mode Return OneLevel/OneStep/Assignee giữ Phase=ChoDuyet sau Mig 28 — chỉ Drafter mode set TraLai). Reject entry visually IDENTICAL Approve entry → user nhầm "Đã trả lại nhưng vẫn hiện đã duyệt".
Fix Plan AD (commit 0aaf2df):
-
Drop fromPhase → toPhase badges entirely trong ApprovalsTab (cả
fe-user+fe-adminmirror §3.9). Visual confusion gỡ bỏ. -
Thay bằng next-target hint parse từ comment via helper
extractNextTargetHint(decision, toPhase, comment):- Approve: Summary "sang Cấp X" → "→ Cấp X", "sang Bước Y" → "→ Bước Y (Cấp 1)", "Duyệt vượt cấp" → "→ Vượt cấp tới Cấp cuối", toPhase=DaDuyet(20) → "→ Đã duyệt hoàn tất"
- Reject: ContextNote "Người chỉ định" → "→ Trả về Người chỉ định (Bước X Cấp Y)" parse regex, "Người soạn thảo"/"Drafter" → "→ Trả về Người soạn thảo", "không lùi được" → "→ Không lùi được", "Trả về 1 Cấp"/"Trả về Cấp X" → "→ Lùi về Cấp X", toPhase=TuChoi(99) → "→ Từ chối hoàn toàn"
-
Decision badge (Plan AC
a734bf2đã add): Duyệt emerald / Trả lại amber / Từ chối rose — phân biệt Action level KHÔNG dựa vào phase.
Pattern reusable cross-project: UI audit history KHÔNG nên render dual-phase badge khi state machine self-loop (e.g. ChoDuyet → ChoDuyet là advance pointer trong cùng phase, KHÔNG phải transition). Thay bằng Decision badge + semantic next-target hint parse từ structured comment.
Severity: UX confusion (KHÔNG functional bug). Bro UAT phản hồi sau Plan AC deploy.
References:
- Plan AD commit
0aaf2df(Run #219 PASS) - File:
fe-user/src/components/pe/PeDetailTabs.tsx:1995-2070(ApprovalsTab + decisionBadge + extractNextTargetHint) - Mirror:
fe-admin/src/components/pe/PeDetailTabs.tsx
51. INFRASTRUCTURE seed vs DEMO seed phân biệt — DemoSeed:Disabled flag gate trap (Session 29 Plan B Chunk A2 Hotfix CICD)
Triệu chứng: Plan B Chunk A2 Implementer scaffold SeedSampleContractWorkflowV2Async mirror PE SeedSampleApprovalWorkflowsV2Async pattern — nested inside if (!demoSeedDisabled) branch trong DbInitializer.cs:105-111. Prod has DemoSeed:Disabled=true (Plan T S23 t10 — UAT permanent clean slate). Run #231 PASS deploy NHƯNG QT-HD-V2-001 workflow KHÔNG seed prod. CICD Monitor (agentId a2ea2e3a) verify Stage 4 catch smoking gun log: "DemoSeed:Disabled=true → skip workflow + contracts + PE + sample V2 seed (Plan T S23 t10 + Plan B Chunk A2 Contract V2)". → Drafter Workspace dropdown V2 EMPTY → V2 contract path BLOCKED end-to-end UAT.
Root cause: Implementer mirror PE pattern (which IS gated for valid reason — PE workflow A/B already seeded V1 historically) → áp dụng nguyên xi cho Contract V2 (no V1 sample existed) → V2 path không có default workflow để Drafter pick.
Fix Hotfix CICD (38f1c4d): PROMOTE SeedSampleContractWorkflowV2Async ra ngoài DemoSeed gate:
if (!demoSeedDisabled)
{
await SeedDemoContractsAsync(...);
await SeedDemoPurchaseEvaluationsAsync(...);
await SeedSampleApprovalWorkflowsV2Async(...); // PE: gated (historical V1 sample existed)
}
// [Plan B S29 2026-05-22 Hotfix CICD] OUT of gate
await SeedSampleContractWorkflowV2Async(...); // INFRASTRUCTURE — V2 path requires
Pattern reusable — Seed classification table:
| Category | Examples | Gate? |
|---|---|---|
| INFRASTRUCTURE always run | Roles, Departments, Catalogs, MenuTree, AdminPermissions, ContractTemplates, SampleWorkflowV2 (V2 path requires) | NO gate |
| DEMO gated | DemoUsers (30 sample, Plan T disabled prod), DemoContracts ([DEMO]), DemoPE ([DEMO]), SampleApprovalWorkflowsV2 (historical PE A/B legacy) | if (!demoSeedDisabled) |
Decision tree khi add new Seed method:
- Production có cần seed này để work end-to-end không?
- YES → INFRASTRUCTURE (no gate)
- NO → DEMO (gated)
- Có safe để admin edit/delete/disable không?
- YES → INFRASTRUCTURE OK (admin sửa qua Designer khi cần)
- NO → architect re-think (immutable seed = anti-pattern)
Bonus — Smart Friend ROI: CICD Monitor catch BEFORE bro UAT 401/empty experience. Cumulative pattern proven 4× S22 #44 + S25 #48 + S29 Plan B Reviewer #ApplicableType + S29 Plan B CICD #DemoSeed.
Phòng tránh tương lai: Khi Implementer mirror PE V2 pattern cho new module (Budget V2, Notification V2), explicit check question: "PE pattern gated/ungated, có applicable cho new module không?" — KHÔNG copy nguyên xi DemoSeed gate placement.
References:
- Hotfix CICD commit:
38f1c4d(Run #232 PASS 3m33s) - File:
src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:105-122 - Cross-ref: Plan T S23 t10 DemoSeed flag (gotcha implicit)
- Memory cross-ref:
feedback_demo_seed_flag_disable.md(Plan T pattern)
50. Page move cross-app — Layout.tsx resolvePath staticMap missed mirror → silent sidebar drop (Session 29 Plan CA Hotfix 1)
Triệu chứng: Plan CA move 4 master pages từ fe-admin → fe-user (commit 06a441c Implementer Case 2). BE /api/menus/me return tree đầy đủ (Master + 4 children + Catalogs sub-tree 4 children, ALL CanRead=true). Admin login eoffice → sidebar group "DANH MỤC" expand chỉ thấy "Danh mục chi tiết" (Catalogs), 3 leaf Suppliers/Projects/Departments + 4 sub-catalogs biến mất silent.
Root cause: fe-user/src/components/Layout.tsx:55-94 resolvePath(key) staticMap chỉ có 7 entries cho Dashboard/Contracts/PE/Budgets/Bg_*. THIẾU 7 entries cho master/catalog leaf mới move:
Suppliers,Projects,DepartmentsCatalogUnits,CatalogMaterials,CatalogServices,CatalogWorkItems
Component MenuLeaf line 238: if (!path) return null → khi resolvePath trả null, component returns null → React render nothing → silent drop khỏi DOM, không có error toast/console warn.
Fix Hotfix 1 (e55d96b): Thêm 7 entries vào fe-user staticMap mirror EXACT từ fe-admin Layout.tsx:33-47:
const staticMap: Record<string, string> = {
Dashboard: '/dashboard',
// ... existing 6 entries
Suppliers: '/master/suppliers',
Projects: '/master/projects',
Departments: '/master/departments',
CatalogUnits: '/master/catalogs/units',
CatalogMaterials: '/master/catalogs/materials',
CatalogServices: '/master/catalogs/services',
CatalogWorkItems: '/master/catalogs/work-items',
}
Pattern reusable — 4-place mirror checklist khi Implementer Case 2 cookie-cutter copy page cross-app:
- ✅ Page file (
pages/<dir>/*.tsx) — copy nguyên content - ✅
App.tsxRoutes — add<Route path="..." element={...} /> - ✅
lib/menuKeys.tsconstants — mirror BEMenuKeys.cs - ⚠️
components/Layout.tsxresolvePathstaticMap — KEY mapping → route path. DỄ MISS vì khác file scope với pages directory.
Bonus: fe-admin có staticMap đầy đủ từ trước Plan CA → mirror dễ. fe-user trước Plan CA KHÔNG có master keys → Implementer cookie-cutter copy page nhưng quên check staticMap diff.
Phòng tránh tương lai: Implementer Case 2 cross-app page move task prompt MUST list 4 places explicit. Reviewer Cat 1 wire claim verify SHOULD include: "Sidebar menu visible end-to-end test post-build".
References:
- Commit Hotfix 1:
e55d96b(Run #230 PASS, fe-user bundleDVBLmZlt→Dgn1iU9E) - File:
fe-user/src/components/Layout.tsx:55-94(resolvePath staticMap + line 238 MenuLeaf null guard) - Original Plan CA Chunk B commit:
06a441c(Implementer missed point 4) - Mirror:
fe-admin/src/components/Layout.tsx:33-53
52. qdrant-client 1.18 xóa search() API — except Exception: continue nuốt lỗi silent → vector search luôn trả [] (Session 31 RAG eval diagnosis)
Triệu chứng: search_memory MCP tool chỉ trả kết quả cho queries có BM25 exact-match tốt (có tất cả token trong cùng 1 chunk). Queries dùng ngữ nghĩa / multi-hop concept → 0 results dù Qdrant points_count=2949 green.
Root cause: qdrant-client 1.18.0 removed QdrantClient.search() method hoàn toàn. retrieval.py vẫn gọi _qdrant.search(...):
# OLD (broken in 1.18):
hits = _qdrant.search(
collection_name=c,
query_vector=query_vector,
limit=top_k,
with_payload=True,
)
# AttributeError: 'QdrantClient' object has no attribute 'search'
Exception bị except Exception: continue nuốt silent → vec_results = [] mọi lúc → pipeline chỉ có BM25.
Kết quả trước fix: RAG eval v1.1 recall@5 = 0.455 (chỉ BM25 queries). v1.0 = 0.455 (same reason). BM25 strict AND-match: ALL tokens phải cùng chunk → multi-token query 8+ tokens fail.
Fix (retrieval.py):
# NEW (qdrant-client 1.12+ query_points API):
resp = _qdrant.query_points(
collection_name=c,
query=query_vector, # ← param renamed: query_vector → query
limit=top_k,
with_payload=True,
)
for h in resp.points: # ← .points không phải iterable trực tiếp
Kết quả sau fix: recall@5 = 1.000 (11/11), avg rerank = 0.847. PASS gate.
Phòng tránh: Pin qdrant-client version trong deps (==1.x.y). Hoặc thêm health-check startup: assert hasattr(_qdrant, 'query_points'), "upgrade qdrant-client". KHÔNG dùng except Exception: continue che-mờ lỗi API — ít nhất log warning.
53. Sub-agent truncation / stall pattern khi heavy MEMORY update phase end-of-task — Reviewer + CICD bị cut mid-sentence ở Update MEMORY.md step (Session 35 × 3 occurrence)
Triệu chứng: Sub-agent (Reviewer + CICD spawn ~100K token budget, ~30+ tool uses) chạy adversarial checks / smoke verify hoàn chỉnh, returning verdict PASS qua snippet visible, NHƯNG output bị truncate ở final "Update MEMORY.md BEFORE stop" step. Em main không nhận structured verdict đầy đủ.
Pattern empirical S35:
- Reviewer FE forms (1200 LOC + 60 mutation): Cat 1 "wire BE PERFECT" + 33 tool uses → truncated mid-MEMORY append
- Reviewer BE CRUD (576 LOC + 16 endpoint): "MEMORY size warning 24.6KB exceeds 24.4KB. Let me append concise entry but trim verbose..." → truncated mid-trim
- CICD Run #244 verify (FE Admin deploy): "VPS mtime cross-check confirms ship at 10:05" → stalled 600s watchdog timeout
Root cause hypothesis:
- Sub-agent context window approaches limit khi cumulative tool output (Read MEMORY ~25KB initial + Read references + Bash output + grep results) + 100K spawn budget
- MEMORY.md size ~25-31KB borderline triggers Edit/Write large operation late-stage → token overflow during streaming
- Stream watchdog 600s timeout không recover (CICD case) — process hung internally
Mitigation S35 verified:
- A. Tight brief scope — Reviewer FE Admin spawn (~5K token brief, 4 cat tight, "concise findings only") → PASS clean 5K return không truncated. Pattern: brief budget < 8K + scope ≤ 4 cat + explicit "DO NOT curate MEMORY heavy — short append only this time"
- B. Em main manual verify post-truncation — Cat 2-6 grep-based verify (SHA256 diff exit code + grep count match) takes ~5 phút em main, faster than re-spawn
- C. Curate MEMORY pre-spawn nếu > 25KB — agent MEMORY > threshold trigger truncation risk. Em main curate proxy archive q3.md trước spawn heavy.
- D. Avoid forcing MEMORY heavy update in agent spec — phase "Update MEMORY.md BEFORE stop with detailed findings" → switch to "short append 1 entry FIFO most-recent-first, KHÔNG curate old"
Cumulative occurrences S35: 3/8 sub-agent spawn (Reviewer × 2 + CICD × 1 = 37.5% truncation rate at borderline ~25-31KB MEMORY). Heavy task + large MEMORY = correlation point.
References:
- S33 Implementer truncation pattern 2/3 (memory
feedback_implementer_truncation_mitigationuser-level — heavy scaffold ≥30 file) - S35 Reviewer FE forms (Session 35 push #1 verify): output cut after Cat 1 PERFECT statement
- S35 Reviewer BE CRUD (Session 35 push #2 verify): cut mid-MEMORY trim
- S35 CICD Run #244 (Session 35 push #3 verify): stalled 600s watchdog after VPS mtime cross-check
Phòng tránh:
- Tight brief scope ≤ 8K tokens cho Reviewer/CICD nếu task verifiable qua grep/diff em main
- MEMORY pre-spawn audit: nếu > 25KB → curate proxy archive trước spawn
- Agent spec ghi rõ "short append MEMORY only, NO curate", remove "BEFORE stop with detailed" directive khi MEMORY borderline
- Em main backup verify Cat 2-6 manual grep nếu Reviewer truncated mid-verdict
References:
- AI_INFRA:
claude-rag/lib/retrieval.pyvector_search()function — fixed 2026-05-26 S31 - Eval run:
eval/runs/2026-05-26-baseline-v1.1-final.json - Diagnosis: Qdrant REST
/collections/proj_solution_erpgreen (2949 points),bm25.db2949 chunks → pipeline broken, not data. Confirmed viapython -c "from qdrant_client import QdrantClient; client.search(...)"→ AttributeError.
54. Anthropic API 529 Overloaded transient khi spawn sub-agent → 0 token fail (Session 37 Plan G-O3 + Session 29 cumulative)
Triệu chứng: Spawn Implementer/Reviewer/CICD qua Agent tool → completed status nhưng result = API Error: 529 Overloaded + subagent_tokens=0 + tool_uses=0. Agent KHÔNG chạy gì, 0 token billed. Khác hẳn truncation (#53 — agent chạy nhưng cut output).
Pattern empirical: S37 FE Proposal spawn fail 529 (0 token, 218s duration = pure wait) + S29 Plan CA CICD verify fail 529 × 2 (transient Anthropic API overload window). Recurring khi Anthropic API load cao (peak hours / model release window).
Phân biệt 529 vs #53 truncation:
- 529 Overload:
tokens=0, agent KHÔNG start → retry-able HOẶC em main solo fallback - #53 truncation: agent chạy đầy đủ (~100-150K token) nhưng cut output mid-MEMORY/mid-exploration → KHÔNG retry (đã tốn token), em main grep verify manual
Mitigation verified S37:
- A. Em main solo fallback — KHÔNG retry loop (529 transient nhưng spawn lại có thể fail tiếp). Em main viết code trực tiếp reliable hơn (S37: BE 700 LOC + FE 4 file × 2 app solo sau 2 spawn fail). Proven faster than wait-retry.
- B. Critical-path task KHÔNG để 1 agent block — nếu task on critical path (cần ship trong session) → em main có sẵn fallback plan solo, KHÔNG block chờ agent.
- C. Off-peak spawn — heavy parallel spawn (3-4 agent) tránh giờ peak nếu không critical.
Cumulative occurrence: S29 × 2 (Plan CA CICD) + S37 × 1 (FE Proposal) = 3× across project. ~5-10% spawn fail rate observed at peak.
55. Sub-agent truncation mid-EXPLORATION phase (extend #53 — Session 37 Implementer BE)
Triệu chứng: Khác #53 (truncate mid-MEMORY end-of-task), S37 Implementer BE Proposal truncate NGAY ĐẦU ở exploration phase — return "Now I need to look at Common Models... ICurrentUser, IDateTime..." sau 30 tool uses, CHƯA write file nào. 150K token wasted (đọc reference + diagnose compile error mid-research).
Root cause: Heavy spec brief (~10K token) + agent đọc nhiều reference file (PE WorkflowService + Features + CodeSequence + Common Models) → context bloat trước khi bắt đầu write → truncate giữa research.
Mitigation:
- Brief WRITE agent ≤ 8K (gotcha #53 rule A reinforced — heavy spec ~10K = quá rủi ro)
- Pre-supply reference snippets trong brief (em main đọc + paste shape thay vì để agent đọc full) → agent KHÔNG cần exploration phase tốn token
- HOẶC em main solo cho task spec phức tạp cần đọc > 4 reference file (S37 lesson: BE Proposal mirror PE = nhiều reference → em main solo reliable)
References: S37 Implementer BE spawn a3afd177 (truncate mid-exploration) + memory feedback_implementer_truncation_mitigation (heavy scaffold ≥30 file pattern). Cumulative truncation S35 × 3 (mid-MEMORY) + S37 × 1 (mid-exploration) = 4× extend #53.
56. Sub-agent ghi MEMORY nhầm path do CWD drift (Session 42-43 × 3 occurrence)
Triệu chứng: Sau khi em main cd fe-user (PowerShell npm build) rồi spawn agent trong CÙNG message → agent (test-specialist S42, reviewer + cicd-monitor S43) ghi MEMORY vào fe-user/.claude/agent-memory/<name>/ thay vì root .claude/agent-memory/<name>/. Agent KHÔNG thấy root MEMORY (CWD=fe-user) → viết lại minimal from scratch (mất history L1). Stray ?? fe-user/.claude/ untracked, dễ commit nhầm.
Root cause: Agent relative-path resolution dùng CWD shell hiện tại (đã drift sang fe-user do cd trước đó — PowerShell CWD persist cross-call). Path memory relative .claude/... → resolve sai gốc.
Mitigation:
- KHÔNG
cd(đặc biệt PowerShell) TRƯỚC khi spawn agent. Dùng absolute path /dotnet --project/git -C/npm --prefixthaycd. - Em main recovery: đọc stray → merge entry mới vào root MEMORY (append Recent activity) →
rm -rf fe-user/.claude→ KHÔNG stage stray. - Stage chọn lọc (
git add <path cụ thể>, KHÔNG-A) để stray + carry-over không lọt commit.
References: S42 test-specialist + S43 reviewer/cicd-monitor — cùng pattern, 3× recover thủ công.
57. Soft-delete entity + UNIQUE index PHẢI filter [IsDeleted] = 0 (Session 45, ext S51)
Triệu chứng: Entity soft-delete (AuditableEntity) có UNIQUE index trên business key (Code / composite). Handler check trùng đã loại soft-deleted (AnyAsync(x => x.Key == k && !x.IsDeleted)) → định cho phép reuse slot. NHƯNG nếu DB UNIQUE index KHÔNG filter → xoá (soft) 1 row rồi tạo lại cùng key → handler PASS app-check nhưng SaveChangesAsync ném DbUpdateException (SQL Server 2627 / SQLite Error 19) → HTTP 500 (không phải Conflict sạch hay insert OK). Reachable thật: admin xoá nhầm 1 ngày lễ / mã catalog rồi nhập lại đúng.
Root cause: UNIQUE index mặc định tính CẢ row IsDeleted=1 → mâu thuẫn app-level !IsDeleted intent.
Fix: EF config filtered index — e.HasIndex(x => x.Key).IsUnique().HasFilter("[IsDeleted] = 0") (composite: new { x.A, x.B }). Migration DropIndex + CreateIndex(filter). SQL Server + SQLite test đều honor (bracket-quote + partial index OK).
Đã áp sẵn: Catalogs ×4, Contract/PE/Proposal/Budget/WorkflowApps code-unique. Fixed S45: Holiday (Year,Date) Mig 43. Fixed S51 (Mig 45 FilterHrmCatalogUniqueIndexesByIsDeleted): LeaveType + ShiftPattern + OtPolicy Code = 3 HRM catalog (OtPolicy bị BỎ SÓT khỏi backlog "2 catalog" — bắt được khi grep TOÀN BỘ config). Vehicle/Driver (Mig 44) filtered day-1. ✅ test-before HrmConfigFilteredUniqueTests.cs (5 case, RED→GREEN).
⚠️ EXT backlog (worktree session S51, Mig 46): FIX 3 (Master) Department/Supplier/Project Code — CONFIRMED-reachable: AuditableEntity + GLOBAL HasQueryFilter(!IsDeleted) auto-ẩn soft-deleted khỏi Create check → check PASS → unfiltered index ném 500 (nghịch lý: global filter LÀM lộ bug, ngược HRM cần manual !IsDeleted). SKIP 3 (audit-verified KHÔNG reachable): ContractClause (no CRUD handler — chỉ DbSet), MeetingRoom (Delete set IsActive=false NOT IsDeleted), EmployeeProfile (Create chặn reuse by-design — UserId ConflictException "Cần khôi phục" + EmployeeCode auto-gen atomic). Mọi bare-unique khác = composite junction / nullable-code đã IS NOT NULL filter / no-soft-delete.
References: Mig 43 FilterHolidayUniqueIndexByIsDeleted · HolidayConfiguration.cs · HrmConfigHolidayTests.cs Case 7 · surfaced bởi test-specialist Gap1 S45.
58. EF read-modify-write lost-update — dùng ExecuteUpdateAsync atomic + Serializable tx (Session 56)
Triệu chứng: Handler trừ/cộng counter kiểu đọc-sửa-ghi in-memory: entity.X += n; await SaveChangesAsync(). 2 request đồng thời (vd 2 lượt duyệt cuối 1 đơn nghỉ, hoặc admin + approver bấm cùng lúc) cùng đọc X cũ → cùng += n → lần ghi sau đè lần trước → mất 1 update (quota lệch). Im lặng, không exception, không corruption — chỉ sai số. Reachable: LeaveBalance.UsedDays trừ phép (S43 gap, fixed S56).
Root cause: read-modify-write KHÔNG atomic dưới READ COMMITTED (default). EF tải value vào RAM, tính ở app, ghi lại — cửa sổ race giữa SELECT và UPDATE.
Fix (proven S56, NO migration): atomic server-side increment — db.Set.Where(pred).ExecuteUpdateAsync(s => s.SetProperty(b => b.X, b => b.X + n), ct). EF Core 7+ phát UPDATE SET X = X + @n 1 lệnh atomic dưới row-lock → 2 increment đồng thời serialize, zero lost-update, BẤT KỂ isolation. ⚠️ ExecuteUpdate bypass change tracker → tracked instance giữ value CŨ; KHÔNG đọc lại entity đó (dùng .AsNoTracking() re-query / ChangeTracker.Clear()), KHÔNG thêm entity.X += n (double-count). Bọc trong explicit BeginTransactionAsync(IsolationLevel.Serializable, ct) để (a) atomic với các write khác cùng handler, (b) serialize nhánh auto-create row mới (2 insert cùng key). Convention codebase = Serializable (codegen WorkflowAppCodeGen:34, ProposalFeatures, TravelVehicle).
References: LeaveOtApprovalFeatures.cs:354-405 (ApproveLeaveRequestHandler terminal DaDuyet) · LeaveBalanceTests.cs (TwoSeparateRequests accumulate test) · database-agent design S56 (DB11) · surfaced bởi pre-golive-verify workflow.
59. PowerShell 5.1 vỡ git commit -m khi message chứa " — dùng git commit -F <file> (Session 57bis)
Triệu chứng: git commit -m @'...here-string có "quote kép"...'@ qua PowerShell 5.1 → git báo error: pathspec 'án' did not match any file(s) known to git — message bị CẮT tại dấu " đầu tiên, phần sau git hiểu thành pathspec args. Commit KHÔNG được tạo. Dễ tưởng nhầm lỗi Unicode tiếng Việt (không phải — tiếng Việt OK nếu không có ").
Root cause: here-string single-quote giữ literal ĐÚNG ở tầng PowerShell, nhưng khi gọi native exe (git.exe), PS 5.1 rebuild command-line và escape " không chuẩn (legacy native-arg passing) → " trong arg phá vỡ arg boundary.
Fix (proven S57bis): message dài / nhiều dòng / chứa quote → Write tool ghi file UTF-8 (vd %TEMP%\commit_msg.txt) → git commit -F <file>. Tránh hẳn native-arg escaping. Message 1 dòng không quote thì -m vẫn OK. Cùng họ bài thuốc "file payload" của #8 (Unicode CLI).
References: S57bis commit dd117b7 (lần 1 -m fail pathspec — commit 1 cùng batch KHÔNG có " nên pass; lần 2 -F PASS) · họ hàng #30/#37 (PS 5.1 encoding class).
60. Identity seed CreateAsync silent-fail vs prod password policy — population Dev ≠ prod, lock/seed-by-email phải dump data thật (Session 58)
Triệu chứng: LockDemoSampleUsersAsync (S57bis) ship + chạy trên prod nhưng locked=0 — NO-OP hoàn toàn. 14 email hardcode (bod.huynh@...) không tồn tại trên prod; demo user thật trên prod là 20 account UAT-matrix scheme khác (bod.1@, pm.nv@... tạo TAY qua admin UI 05-13, chưa từng nằm trong code). Test xanh + deploy PASS + health 200 — không gì báo lỗi.
Root cause (2 tầng):
- Seed silent-fail:
DemoUserPassword = "User@123456"(11 ký tự) < prodIdentity:Password:RequiredLength=12(appsettings.Production.json; Dev fallback 8) →userManager.CreateAsynctrảIdentityResult.Failed— seed code chỉLogWarning+continue(by-design 1-fail-không-abort) → named-person user CHƯA BAO GIỜ được tạo trên prod, luôn cảnv.cao/nv.truong(IT pool — chính là root cause "helpdesk inert phòng IT 0 user" S56) + 5 real staff. - Lock-by-email viết theo population Dev: author đọc seed code (Dev truth) thay vì dump prod Users → list lệch hoàn toàn.
Fix (S58 5998163): (a) DemoUserPassword → "User@1234567" (12 ký tự, thỏa policy mọi env); (b) lock list = union 14 Dev-population + 20 prod-population (exact-email, KHÔNG pattern — binh.le@ là người thật sát scheme demo).
Phòng tái diễn: (1) Mọi thao tác theo-email trên user (lock/deactivate/migrate) → dump bảng Users env đích TRƯỚC khi viết list; assertion trả 0/-1 ⟹ nghi data-mismatch trước khi nghi code. (2) Seed tạo user → password const phải thỏa policy NGHIÊM NHẤT mọi env (prod 12). (3) IdentityResult không throw — grep LogWarning sau deploy có user-seed mới.
References: DbInitializer.cs DemoUserPassword + LockDemoSampleUsersAsync · DependencyInjection.cs:67 (RequiredLength fallback 8) · appsettings.Production.json:18 (12) · cicd-monitor Run #381 entry (phát hiện PARTIAL) · investigator-codebase recon S58.
61. sqlcmd chạy file .sql chứa tiếng Việt PHẢI -f 65001 — console mojibake là display-only nhưng thiếu flag thì DATA hỏng thật (Session 59)
Triệu chứng: Output sqlcmd trên VPS hiện H? th?ng trung th?, Ph?n th<74> — không phân biệt được "console hiển thị sai" (vô hại) với "data trong DB đã hỏng" (thảm họa, nhất là với UPDATE/INSERT chứa nvarchar tiếng Việt).
Root cause 2 lớp: (1) Console VPS codepage OEM → mọi output Unicode đều mojibake = display-only, data vẫn đúng. (2) NHƯNG sqlcmd mặc định cũng đọc input file theo OEM codepage — file .sql UTF-8 no-BOM (như Write tool tạo) chứa N'Bê tông' sẽ bị decode sai → ghi vào DB chuỗi hỏng THẬT. Hai hiện tượng nhìn giống hệt nhau trên console.
Fix + phòng tái diễn (S59 proven): (1) Mọi lệnh sqlcmd -i <file> với file chứa tiếng Việt → thêm -f 65001 (đọc input UTF-8). (2) KHÔNG kết luận data đúng/hỏng từ console VPS — verify độc lập: chạy cùng file trên LocalDB local (console local hiện tiếng Việt đúng) + curl API prod đọc JSON (UTF-8 end-to-end). S59 rename 71 WorkItems: console VPS mojibake nhưng API JSON trả "2 MEP Sub Hệ thống trung thế" nguyên vẹn → data đúng.
References: scripts/s59-rename-workitems-pmh.sql (run với -f 65001) · verify pattern: login admin → GET /api/catalogs/work-items JSON.
62. Đổi mã (natural-key) của data có seed per-code idempotent — PHẢI UPDATE DB prod TRƯỚC khi deploy seed mới, sai thứ tự = nhân đôi rows (Session 59)
Triệu chứng tiềm năng (đã né): Seed kiểu SeedRealMasterDataAsync so khớp theo Code (if (existingCodes.Contains(code)) continue). Đổi 71 mã WorkItems VT-01→MAT-1… nếu push code trước: app restart chạy seed với tuples MỚI → thấy 71 mã mới "chưa tồn tại" (DB còn mã cũ) → INSERT 71 row mới = 142 rows trùng tên khác mã trên prod.
Quy trình đúng (S59 proven, Run #276 verify 71 — không phải 142):
- Viết SQL
UPDATE … SET Code=mới, Name=mới WHERE Code=cũ(GIỮ NGUYÊN Id — record đang được trỏ tới không gãy; phiếu PE/2026/A/001 vừa tạo vẫn đúng hạng mục). - Chạy SQL trên prod + LocalDB Dev trước.
- Sửa seed tuples trong code → commit + push SAU. App restart: seed thấy đủ mã mới → add 0.
- cicd verify count ĐÚNG N (N×2 = FAIL nghiêm trọng — đưa vào prompt verify).
Tổng quát: idempotent-by-natural-key chỉ an toàn khi key bất biến — mọi rename key phải đổi DB và seed "cùng khoảnh khắc" theo thứ tự DB-trước-code-sau (với CI auto-deploy-on-push).
References: scripts/s59-rename-workitems-pmh.sql · DbInitializer.cs SeedRealMasterDataAsync · gotcha #57 họ hàng (soft-delete vs UNIQUE filtered) · Run #276 cicd verdict.
63. EF scaffold tự đoán RenameColumn SAI SEMANTICS khi drop + add cột cùng type — review migration trước khi tin, test xanh KHÔNG bắt được (Session 61)
Triệu chứng (đã né): Mig 50 drop BudgetManualAmount + add BudgetPeriodAmount/ExpectedRemainingAmount (đều decimal(18,2) nullable) → dotnet ef migrations add scaffold sinh RenameColumn(BudgetManualAmount → ExpectedRemainingAmount). Nếu tin scaffold: số ngân sách nhập tay của 8 phiếu UAT prod rơi vào cột "Giá trị thực hiện dự kiến còn lại" (row 8) thay vì "Ngân sách kỳ này" (row 3) — data đúng chỗ SAI semantics, không lỗi runtime nào báo.
Root cause: EF model-diff heuristic map cột-bị-xóa ↔ cột-mới cùng type/nullability thành RENAME (tối ưu giữ data) — máy không biết semantics nghiệp vụ.
Fix đúng (S61 proven, prod-verified Run #285): bỏ RenameColumn → AddColumn cả 2 cột mới → Sql("UPDATE ... SET BudgetPeriodAmount = BudgetManualAmount WHERE ... IS NOT NULL") → DropColumn cột cũ. Precedent cùng shape: Mig 20260513130144 (add→backfill→drop).
Vì sao test không bắt: SQLite test dựng schema từ MODEL hiện tại (EnsureCreated), KHÔNG chạy migration — mọi sai trong migration operations vô hình với 263 test xanh. Guard duy nhất = đọc file migration sau scaffold + cicd spot-check data sau deploy.
References: Migrations/20260612173224_ReplaceBudgetModuleWithPeWorkItemBudgets.cs Up() comment đầu · reviewer S61 verify "KHÔNG còn RenameColumn" grep · gotcha #64 (cặp đôi — backfill chưa test local).
64. dotnet ef database update áp lên Design DB (DesignTimeDbContextFactory) — KHÔNG phải runtime Dev DB; Sql() data-migrate trong Up() có thể CHƯA TỪNG chạy trên data thật trước prod (Session 61)
Triệu chứng: claim "Mig 50 applied local OK" sau dotnet ef database update — đúng nhưng là DB SolutionErp_Design (pin tại DesignTimeDbContextFactory.cs:14, 0 rows). Runtime SolutionErp_Dev (appsettings.Development.json) vẫn ở Mig 49. Nguy hiểm thật: mọi Sql() backfill/UPDATE trong Up() chưa từng chạy trên DB có data — prod deploy là lần ĐẦU TIÊN data-migrate chạy thật.
Cơ chế 2 DB: dotnet ef CLI → DesignTimeDbContextFactory → Design DB · runtime app start → DbInitializer.cs:64 MigrateAsync() tự heal Dev/prod DB. 2 đường KHÁC NHAU, đừng lẫn.
Guard (S61 áp dụng): migration có data-migrate ⟹ (1) ghi rõ trong commit "backfill lần đầu chạy trên prod"; (2) cicd-monitor brief BẮT BUỘC mục DATA-PRESERVE spot-check sau deploy (S61: SELECT MaPhieu, BudgetPeriodAmount ... → 8/8 phiếu giữ số, gồm phiếu 1.243.820.600 đ của anh Kiệt ✓); (3) optional: dotnet run local 1 lần trước push để Dev DB có data chạy thử backfill.
Credit: 🟥 reviewer S61 catch (đào connection-string mới lộ — "claim applied-local trên DB 0-rows = backfill untested"). Họ hàng S53 database-agent catch "committed-but-unapplied-local".
References: DesignTimeDbContextFactory.cs:14 · DbInitializer.cs:64 · reviewer S61 MINOR #1 · cicd S61 self-verify BACKFILL 8/8.
65. Build csproj con (vd SolutionErp.Api.csproj) ≠ dotnet build SolutionErp.slnx (gồm tests) — đổi chữ ký record command lọt test-compile → CI CS7036 FAIL-gated (Session 65)
Triệu chứng: thêm ParentId làm tham số positional thứ 5 BẮT BUỘC vào CreateDepartmentCommand; build local dotnet build src/Backend/SolutionErp.Api/SolutionErp.Api.csproj PASS → push → CI test-gate FAIL CS7036 Run #291 ("no argument for required parameter 'ParentId'") vì MasterCatalogFilteredUniqueTests.cs:63 còn gọi 4-arg. Deploy bị chặn (prod NGUYÊN — test-gate làm đúng việc), tốn 1 vòng FAIL→fix→re-push.
Cơ chế: Api.csproj (và mọi csproj con src/Backend/*) KHÔNG reference tests/* → build nó KHÔNG compile tests. CI chạy dotnet build SolutionErp.slnx / dotnet test = build CẢ tests → bắt call-site cũ. Đổi chữ ký record command/DTO/entity ctor = spec change (CLAUDE.md §7 "spec change = sửa test cũ + code chung commit").
Guard: đổi chữ ký BE ⟹ (1) dotnet build SolutionErp.slnx FULL (gồm tests) TRƯỚC push, KHÔNG build csproj con lẻ; (2) grep repo-wide new <Command>( (gồm tests/) sửa mọi call-site; (3) cân nhắc trailing optional = null (như CreatePurchaseEvaluationCommand +HoSoLink Mig 52 — backward-compat, 0 call-site break) đối lại positional-required (CreateDepartmentCommand +ParentId — vỡ test).
Credit: 🟩 cicd-monitor S65 Run #291 FAIL-detect (reproduced local CS7036) → em main fix MasterCatalogFilteredUniqueTests.cs:63 +5th arg null → 6ce5803 → Run #292 PASS.
References: MasterCatalogFilteredUniqueTests.cs:63 · DepartmentFeatures.cs (CreateDepartmentCommand) · Run #291 FAIL / #292 PASS · CLAUDE.md §7.
66. Tailwind v4 — rule element thô h1-h4{color} viết NGOÀI @layer thắng MỌI utility text-white → heading không đổi màu; fix ĐIỂM bằng important text-white!, KHÔNG move @layer (load-bearing) (Session 68)
Triệu chứng: Header chi tiết NV (app-gradient-brand nền gradient xanh) — <h2 className="text-white"> render ĐEN #0b1220, trong khi <div className="text-white"> (dòng meta cùng banner) render trắng đúng. Anh UAT báo "màu đen nhìn không nổi bật" — chính xác, dù code ghi text-white.
Cơ chế: fe-user/src/index.css khối h1, h2, h3, h4 { (🔴 neo theo TÊN, KHÔNG theo số dòng — số này đã thối 2 lần trong CÙNG phiên S179: 79→85→89 vì header đầu tệp bị thêm dòng. Đo: grep -n '^h1, h2, h3, h4 {' fe-user/src/index.css) có h1,h2,h3,h4 { color:#0b1220 } viết thô, NGOÀI mọi @layer (thêm S65 #290 "darker ink"). Tailwind v4: CSS unlayered có priority CAO HƠN tất cả @layer (kể cả utilities) → text-white (trong layer utilities) THUA. <div> không bị rule element này nhắm nên text-white thắng → nghịch lý meta-trắng / heading-đen cùng 1 banner.
Guard: (1) User báo màu render ≠ class trong code ⟹ NGHI ngay rule CSS priority cao hơn (unlayered element / !important / global) — grep index.css rule element TRƯỚC khi tin utility. (2) Ép utility thắng = important modifier Tailwind v4 trailing-bang text-white! → compile .text-white\!{color:…!important} (verify bằng grep dist CSS — đừng tin suông). (3) KHÔNG move rule unlayered vào @layer base nếu load-bearing — đo blast-radius grep '<h[1-4][^>]*text-': rule này đang ép ~30+ heading toàn app về #0b1220 (chúng gắn text-slate-700/900/brand-800 nhưng bị override) → move = đổi màu loạt giữa UAT. (4) Authed page không screenshot dev-rig (gotcha #3) → visual gate = anh mắt thường qua deploy prod.
Credit: em main solo S68 — anh UAT realtime ("chữ đen nền xanh ko nổi bật") → diagnose unlayered heading rule → text-white! + thu nhỏ text-lg → grep dist confirm !important → Run #304 PASS, x2 app SHA256.
References: fe-user/src/index.css khối h1, h2, h3, h4 { (unlayered; 🔴 neo TÊN — xem Cơ chế, số dòng thối 2 lần @S179) · fe-{user,admin}/src/pages/hrm/EmployeesListPage.tsx:653 (h2 name text-white!) · Tailwind v4 layer precedence · gotcha #3 (authed screenshot).
67. Tailwind v4 — accent palette tự-chế thiếu stop (chỉ 50/100/500/600/700) → dùng -300 "vỡ màu im lặng": tên-trùng-built-in rơi DEFAULT, tên-tự-chế drop hẳn; build PASS (Session 69)
Triệu chứng: Component KpiCard tái dùng có activeBorder: 'border-{accent}-300' cho 5 accent. Build PASS 0 error, nhưng active-border render SAI: teal/violet ra tone teal/violet MẶC ĐỊNH Tailwind (khác hệ custom #0ea5a4...), amberx/greenx KHÔNG có border (class drop). brand-300 OK (brand full 50-900).
Cơ chế: @theme SE định nghĩa accent palette (teal/violet/amberx/greenx) CHỈ ship 50/100/500/600/700 (brand ngoại lệ full). Tailwind v4: border-teal-300 — teal TRÙNG tên built-in → emit DEFAULT teal-300 (#5eead4, khác --color-teal-*); border-amberx-300 — amberx TỰ CHẾ không có --color-amberx-300 → class drop, không emit. Cả 2 build KHÔNG báo lỗi (Tailwind không validate color tồn tại) → phải SOI dist CSS.
Guard: (1) Component tái dùng accent-aware CHỈ dùng stop trong "hợp đồng chung" mọi accent (50/100/500/600/700) — đừng mượn stop chỉ brand có. (2) Nghi "vỡ màu im lặng" → grep dist CSS class tồn tại + đúng var(--color-...). (3) Tên tự-chế (amberx/greenx) AN TOÀN hơn tên-trùng-built-in (teal/violet): miss = drop hẳn (dễ thấy) thay vì rơi-default-sai-tone (ẩn). (4) reviewer dimension "color-trap" = grep added-lines (teal|violet|amberx|greenx)-(200|300|400|800|900).
Credit: reviewer S69 (soi dist Office foundation) → em main fix activeBorder -300 → -500 ×2 app SHA256.
References: fe-{user,admin}/src/components/ui/KpiCard.tsx ACCENT map · fe-*/src/index.css @theme accent stops · gotcha #66.
68. IDE TypeScript diagnostic giữa background-agent/workflow = snapshot DỞ-DANG — chỉ tin build SẠCH chạy SAU agent xong (Session 69)
Triệu chứng: Sau workflow re-skin 7 designer song song (+ sau PE-FE agent), harness bắn loạt × 'X' is declared but never read / × Type 'Element' not assignable. Em suýt sửa theo. Build npm run build chạy SAU → exit 0, 0 error cả 2 app. Các × đều FALSE-ALARM.
Cơ chế: IDE TS language-server bắn diagnostic theo TỪNG lần save dở-dang của agent (thêm import/state TRƯỚC khi render JSX dùng nó → "unused"; xóa JSX TRƯỚC khi gỡ import → "unused"). Nhiều agent ghi SONG SONG → snapshot càng nhiễu, KHÔNG phản ánh trạng-thái-cuối nhất-quán nào.
Guard: (1) Agent/workflow NỀN: bỏ qua diagnostic giữa-chừng, tín hiệu thật = 1 lần build SẠCH chạy SAU agent hoàn-tất (lý do em main build-tập-trung). (2) Đừng vội sửa/cp-đè theo diagnostic — suýt cp đè file vốn đã hoàn chỉnh (2×). (3) Phân biệt diagnostic ai-edit: em main edit real-time → tin; agent nền edit → verify build trước. (4) Cùng họ gotcha #53 (return truncated): disk + build = source-of-truth, KHÔNG tin return/diagnostic suông.
Credit: em main S69 — 2× suýt sửa theo stale-diagnostic (re-skin + PE-FE), build sau-cùng vạch false-alarm.
References: workflow office-puro-reskin-all + PE-FE implementer-frontend · gotcha #53 · gotcha #3.
69. FE bundle hash KHÔNG deterministic + deploy.yml rebuild FE vô-điều-kiện mỗi run → bundle ROTATE kể cả commit BE-only/governance (Session 72)
Triệu chứng: Commit governance-only (0 FE source change, git diff fe-*/src <range> = 0 file) push → CI Run #312 → bundle admin+user ĐỀU ROTATE (BgNCjwsG/CBvh0vtf → fc_xkNpJ/DP-tBcg0). Suýt báo "FE thay đổi ngoài ý muốn"; STATUS/HANDOFF/commit đã claim nhầm "bundle frozen".
Cơ chế: (1) .gitea/workflows/deploy.yml path-filter gate CẢ workflow (chạy/skip toàn-bộ), KHÔNG gate từng step → 1 file non-ignored (vd .claude/workflows/hmw.js) đủ trigger → FE rebuild + deploy CHẠY HẾT. (2) Step deploy (deploy.yml:161-167) Remove-Item fe-*\* + Copy-Item dist\* VÔ-ĐIỀU-KIỆN mỗi run. (3) Vite/rolldown emit content-hash KHÔNG deterministic từ source y-hệt → hash mới mỗi build. ⟹ "BE-only ⟹ bundle frozen" là TRÙNG-HỢP quá-khứ, KHÔNG phải cơ-chế. Bundle rotate = EXPECTED mỗi deploy.
Guard: (1) Phát-hiện FE-ship THẬT = git diff fe-admin/src fe-user/src <range>, KHÔNG tin hash-delta. (2) SPA-fallback 200 trap: GET /assets/index-<fake>.js trả 200 (qua /*→index.html rewrite) → verify bundle = parse index.html refs + check size + Last-Modified, KHÔNG GET trực-tiếp asset hash-named. (3) Cosmetic-only, KHÔNG rollback; muốn hết false-alarm → thêm .claude/** vào paths-ignore (governance commit khỏi trigger deploy).
Credit: cicd-monitor S72 Run #312 — flagged-then-resolved bundle rotation trên governance commit 18fced6; lật ngược invariant "BE-only frozen".
References: .gitea/workflows/deploy.yml:17-30,161-167 · gotcha #41 (path-filter) · gotcha #46 (stale SHA).
70. FE absolute-set form echo field anh-em từ server-snapshot + invalidate() fire-and-forget → cửa-sổ STALE-ECHO mất dữ-liệu khi lưu 2 ô liên-tiếp (Session 76)
Triệu chứng: Form ngân sách PE — 2 ô cùng cột PRO (ban hành + hiệu chỉnh) lưu qua CÙNG proMut, mỗi save echo field anh-em từ bs (server snapshot). Lưu ô A (proInitial) → invalidate() refetch BẤT-ĐỒNG-BỘ; TRƯỚC khi refetch về, bs.proInitial còn CŨ → lưu ô B (proAdjust) echo bs.proInitial cũ → đè mất giá-trị A. Workflow review (reviewer) bắt — implementer self-review SÓT.
Cơ chế: BE handler absolute-set (set thẳng cả 3 field, thiếu=null=CLEAR) → FE PHẢI echo field không-đổi. Echo lấy từ bs (props từ query) — STALE trong cửa-sổ [mutate-resolve → refetch-land]. mut.isPending chỉ true trong HTTP mutate, KHÔNG cover refetch → nút Lưu re-enable khi bs vẫn cũ.
Guard: Gate nút Lưu = mut.isPending || useIsFetching({queryKey:['pe-detail',ev.id]}) > 0 — khoá save tới khi refetch land (bs fresh). Đóng cửa-sổ stale-echo. Áp CẢ PRO+CCM cells + notes. (Pattern absolute-set-echo có từ CCM Mig 50/55 prod chưa-báo-lỗi nhưng PRO nhân-đôi bề-mặt → vá trước go-live tài-chính.)
Credit: reviewer S76 (2-lane workflow review Part 2/3) escalate MAJOR data-loss; em-main vá useIsFetching ×2 app trước deploy.
References: fe-*/src/components/pe/PeDetailTabs.tsx (PeBudgetSummaryTable peFetching) · gotcha #44 (silent issue self-review bias).
71. Thêm enum value vào entity DÙNG-CHUNG → pollute UI/guard phân-loại theo PROXY-predicate (không phải enum) (Session 78)
Triệu chứng: Thêm PurchaseEvaluationAttachmentPurpose.ApprovalAttachment=5 (file người duyệt đính kèm khi DUYỆT — tái dùng entity PurchaseEvaluationAttachment, migration-free vì enum lưu int). File mới có PurchaseEvaluationSupplierId=null. NHƯNG 2 chỗ FE dùng supplierId === null làm PROXY ngầm cho "Bảng so sánh": (1) banSoSanhAttachments → file-khi-duyệt LẪN vào section Bảng so sánh; (2) submit-guard "Chưa đính kèm Bảng so sánh" → file-khi-duyệt FALSE-PASS guard (cho gửi-duyệt mà chưa có bảng so sánh thật, khi phiếu Trả-lại re-submit có sẵn file vòng trước).
Cơ chế: Reuse-enum migration-free RẺ (0 schema change, tái dùng storage+endpoint+component), NHƯNG predicate cũ phân-loại bằng FIELD-PROXY (supplierId===null = "không gắn NCC" ≈ "bảng so sánh") thay vì check enum tường minh → giá-trị enum MỚI rơi nhầm bucket. Build PASS (TS không bắt logic-bucket) + test xanh (FE-filter no xUnit) → CÂM.
Guard: Khi thêm enum value vào entity dùng-chung → grep MỌI predicate lọc bằng field-proxy (không phải enum) liên-quan, loại-trừ value mới: supplierId===null && purpose !== ApprovalAttachment. Áp cả 2 app SHA-mirror. Kiểm thêm: per-supplier list (lọc supplierId===s.id) tự loại (value mới supplierId=null); GET bundle projection KHÔNG .Where(purpose) → value mới tới được FE.
Credit: em-main self-review (grep toàn-bộ .purpose / supplierId===null 2 app) bắt TRƯỚC deploy — thay vai reviewer (over-cap). Build fe-admin riêng cũng bắt mirror-truncate </Dialog> sót (new_string copy-truncate) → build-verify TỪNG app, KHÔNG tin "mirror identical".
References: fe-*/src/components/pe/PeDetailTabs.tsx (banSoSanhAttachments + missingForApproval) · PeWorkflowPanel.tsx (approvalAttachments) · src/Backend/.../PurchaseEvaluations/PurchaseEvaluationAttachment.cs enum · gotcha #70 (self-review bias).
72. DateTime serialize KHÔNG có 'Z' (EF đọc Unspecified kind) → FE new Date() hiểu là local → lệch +7h (Session 85)
Triệu chứng: anh Kiệt "mới tạo phiếu mà báo cách 7h trước" — relative-time thông-báo (Date.now() - new Date(createdAt).getTime()) lệch đúng +7h (VN UTC+7).
Cơ chế: timestamps lưu UTC (DateTime.UtcNow) nhưng EF Core đọc SQL Server datetime2 về DateTimeKind.Unspecified. AddControllers() mặc-định (KHÔNG JSON config) → System.Text.Json serialize DateTime Unspecified-kind KHÔNG có hậu-tố 'Z' ("2026-06-23T08:20:00"). FE new Date(naive) hiểu chuỗi-không-TZ là giờ LOCAL → nhưng giá-trị là UTC → lệch +7h. Build PASS + test xanh (serialization không unit-test) → CÂM.
Guard: global JsonConverter<DateTime> ép UTC-with-Z: Kind==Unspecified ? SpecifyKind(Utc) : ToUniversalTime() → .ToString("O"). Đăng-ký AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new UtcDateTimeJsonConverter())). System.Text.Json tự áp cho DateTime? (Nullable non-null). Verify: curl authed endpoint → field createdAt JSON CÓ 'Z'. Lưu-UTC + convert-on-display (FE) là CHUẨN — KHÔNG band-aid tzutil đổi giờ máy-chủ (ảnh-hưởng GETUTCDATE + lệch khác). SignalR realtime-push dùng serializer RIÊNG (AddJsonProtocol) → nếu giờ realtime còn lệch thì áp converter tương-tự.
References: src/Backend/SolutionErp.Api/Serialization/UtcDateTimeJsonConverter.cs · Program.cs AddControllers · fe-user/src/components/NotificationBell.tsx:23.
73. UPDATE-handler absolute-set field → CLEAR khi FE KHÔNG gửi field (bug-class S42 recurrence) (Session 85)
Triệu chứng: phiếu tạo CÓ Quy trình duyệt → bấm "Sửa" (đổi tên/dự án) → MẤT workflow → kẹt submit "chưa chọn quy trình duyệt" (whack-a-mole: SQL-fix vá xong, ai bấm Sửa lại rụng tiếp). long.chau + anh Kiệt + nhiều account.
Cơ chế: UpdatePeDraftHandler absolute-set entity.ApprovalWorkflowId = request.ApprovalWorkflowId. Form Sửa (PeHeaderForm) KHÔNG gửi approvalWorkflowId → command bind null (default Guid? = null) → handler set = null = CLEAR. Field SÓT khỏi pattern null-safe (if (request.X is not null)) mà BudgetPeriodAmount/ExpectedRemainingAmount/WorkItemId NGAY DƯỚI đã có (comment "client không gửi → GIỮ giá trị cũ" — bug-class S42). 1 field lọt khỏi quy-ước đã-thiết-lập.
Guard: MỌI field optional trong UPDATE-handler PHẢI null-safe if (request.X is not null) entity.X = request.X (preserve khi không gửi); CHỈ field text-clearable CỐ-Ý (TenGoiThau/DiaDiem/MoTa) mới absolute-set. Khi thêm field vào UPDATE command → đối-chiếu pattern null-safe của field cạnh. Triệu-chứng "vá-điểm xong TÁI-DIỄN" = nghi field-clear-on-omit, truy root KHÔNG vá-data lặp (root-cause-over-symptom).
References: src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs:~282 (UpdatePeDraftHandler) · fe-user/src/components/pe/PeHeaderForm.tsx:112 (PUT no-workflow) · bug-class S42 (BudgetPeriodAmount null-safe S61) · tests/.../PeUpdateDraftWorkflowPreserveTests.cs.
74. MCP save_to_disk (Chrome MCP + computer-use) KHÔNG tạo file truy-cập được trong env này → chụp ảnh prod cho docs bằng PowerShell CopyFromScreen (Session 90)
Triệu chứng: Cần chụp 12 màn prod (eoffice) khoanh-đỏ chèn vào Word guide. mcp__Claude_in_Chrome__computer {action:screenshot, save_to_disk:true} + mcp__computer-use__screenshot {save_to_disk:true} đều báo "Successfully captured" + ID nhưng KHÔNG in path + KHÔNG tạo file nào tìm thấy được (search TEMP/LOCALAPPDATA/APPDATA/Downloads/session-dir = rỗng) → PIL/python-docx không có file ảnh để chèn. gif_creator cũng vô-dụng (ảnh tĩnh = 0 frame).
Cơ chế: save_to_disk chỉ đính ảnh vào hội-thoại cho user xem, KHÔNG persist ra path mà lead truy-cập được (harness/env này).
Fix — pipeline tự-chế (ra PNG thật):
- Chụp: PowerShell .NET
[System.Drawing.Graphics]::CopyFromScreen(sauSetProcessDPIAware) → PNG full-res (2560×1600, DPR=1). Đưa đúng cửa-sổ Chrome lên trước bằng Win32:EnumWindows+GetWindowText+GetWindowThreadProcessIdlọc processchrome.exe+SetForegroundWindow+ ALT-tap unlock-foreground. ⚠️ Lọc theo title "Solutions ERP" KHÔNG đủ — app Claude Code (Electron) cùng classChrome_WidgetWin_1+ title chứa tên project → khớp nhầm; PHẢI lọc process-name =chrome. - Khoanh đỏ: inject DOM overlay
window.__ccA([{text|q, n, minLeft/maxLeft/minTop/maxTop, exact, up, nth}])— element-based (bámgetBoundingClientRect, miễn-nhiễm viewport đổi). Toạ-độ-cứng (rect px) KHÔNG ổn vì Chrome-screenshot render kích-thước thay-đổi (812↔833). Match case-INSENSITIVE (label CSStext-transform:uppercase→textContentmixed-case → exact-uppercase MISS). - Crop: bỏ chrome trình-duyệt top + taskbar bottom (top=92/bottom=72). ⚠️ Banner CDP "Claude started debugging this browser" hiện sau mỗi
navigate(CDP re-attach), tự ẩn sau vài giây → ảnh chụp ngay-sau-navigate cần crop top≈146; verify nhanh bằng montage dải-trên 12 ảnh đọc 1 lần. - Chèn:
python-docxcell...add_picture(width=Inches(6.0))— A4 content = 6.27" nên 6.3" TRÀN; dùng ≤6.1".
75. Seed demo UNGATED + per-code idempotent → RE-ADD data đã wipe mỗi restart (wipe KHÔNG bền) (Session 91)
Triệu chứng: Sau wipe 18 NCC demo (s91-wipe-golive.sql) cho go-live sạch, định deploy → SeedDemoMasterDataAsync (DbInitializer.cs:111) chạy NGOÀI mọi if (!demoSeedDisabled) + idempotent per-code (existingSupplierCodes.Contains(s.Code) ? continue : Add). Code 18 NCC demo giờ THIẾU (đã xóa) → mỗi restart/IIS-recycle TỰ Add lại → Suppliers 7→25, undo go-live clean.
Cơ chế: wipe = triệu chứng; nguồn re-populate (seed) là gốc. Demo seed cố-ý-ungated (Production.json comment cũ "KEEP SeedMasterData") để giữ data UAT — nhưng go-live cần demo GONE. Khác user test (38): S89 đã gỡ KHỎI MẢNG SeedDemoUsers → không resurrect.
Fix (root-cause, 55494ad): gate SeedDemoMasterDataAsync sau DemoSeed:Disabled (=true ở prod, mirror Contracts/PE/Workflow demo đã gated). Prod skip → wipe BỀN; Dev (false) vẫn seed local. Verify: deploy restart 2× → demo_codes=0 (binary Last-Modified fresh + count THẬT từ prod). Quy tắc: TRƯỚC/SAU mọi prod data-wipe → audit DbInitializer seeder cho ungated per-code re-seed (resurrect). Liên-quan #73 / feedback_root_cause_over_symptom.
76. deploy.yml render appsettings.Production.json từ .example template mỗi deploy → prod config KHÔNG lấy từ file gitignored (Session 91)
Triệu chứng: Sửa appsettings.Production.json (gitignored) tưởng đổi config prod → vô-tác-dụng; git add báo file ignored.
Cơ chế: deploy.yml:146-154 mỗi deploy đọc appsettings.Production.json.example (tracked) → ConvertFrom-Json → set DB_CONNECTION+JWT_SECRET từ secrets → ghi đè C:\inetpub\...\appsettings.Production.json. File local gitignored KHÔNG bao giờ deploy. Effective config = appsettings.json (tracked) ⊕ rendered-Production.json (từ .example).
Fix/Quy tắc: đổi prod config = sửa .example (tracked) HOẶC appsettings.json, KHÔNG file local gitignored. .example KHÔNG có DemoSeed → prod fallback appsettings.json DemoSeed:Disabled=true (đúng fallback đã giữ Contracts/PE gated nhiều tháng → gate #75 hiệu-lực ở prod).
77. SPA deploy bundle cache-lag — user thấy UI CŨ dù đã deploy (index.html cache / tab-SPA-mở) (Session 96)
Triệu chứng: Deploy FE xong (bundle rotate verified prod), nhưng user (anh Chương UAT) VẪN thấy UI CŨ dù Ctrl+F5 + logout/login — checkbox opt-out mới không hiện, vẫn "✅ Cấp này KẾT THÚC" static cũ. Team "bó tay rồi".
Cơ chế: Vite đặt hash trong TÊN file (index-<hash>.js, Cache-Control: immutable max-age=1năm = ĐÚNG). index.html = Cache-Control: no-cache (ĐÚNG chuẩn — revalidate ETag mỗi load). Nhưng: (a) tab SPA đang-mở chạy JS cũ trong RAM (cần reload thật, không chỉ F5-nửa-vời); (b) browser đôi khi giữ index.html cached bất-chấp no-cache → trỏ file JS CŨ (tên hash cũ). KHÔNG có service-worker (fe-user grep serviceWorker|workbox|VitePWA = 0) → không phải SW-cache.
Chẩn-đoán (phân-biệt cache-vs-deploy — QUAN TRỌNG, đừng đoán): verify bundle prod BYTE-LEVEL: JS=$(curl -s $BASE/ | grep -oE '/assets/index-[A-Za-z0-9_-]+\.js') → curl -s "$BASE$JS" | grep -ao "<string-fix-mới>" + grep "<string-cũ>". Fix-string PRESENT + old-string GONE = deploy OK → 100% client-cache (KHÔNG phải deploy-fail). S96: prod 0MU-kb4N chứa "BỎ tick"/"Đã BỎ tick", "bạn duyệt xong" cũ MẤT → xác nhận cache.
Fix (client): incognito window (Ctrl+Shift+N) = golden-test (không dùng cache, chắc-chắn bundle mới → nếu incognito OK = 100% cache). Hoặc F12→Application→Clear site data→reload. Đừng chỉ Ctrl+F5 (đôi khi không xóa index.html cached). Server ĐÃ đúng chuẩn (index.html no-cache + JS hash-immutable) — KHÔNG cần fix infra.
Quy tắc: UAT báo "fix chưa ăn" sau deploy → verify bundle prod byte-level TRƯỚC khi nghi deploy-fail; incognito = phép-thử vàng tách cache khỏi bug. Liên-quan #69 (bundle non-determ) · feedback_verify_deployed_artifact_byte_level.
An toàn prod: chỉ XEM/scroll/screenshot — KHÔNG bấm nút mutate (Gửi duyệt/Duyệt/Trả lại/Tạo HĐ). Mở + Thêm mới (form ?mode=new) KHÔNG tạo draft cho tới khi sửa field (auto-save on-edit).
References: $env:TEMP\guideshots\capture.ps1 (session-scratch) · docs/Huong-dan-Duyet-NCC.docx. Pattern reusable → user-memory pattern_prod_screenshot_to_docx.
78. Display flag suy từ CONFIG thay RUNTIME → nói dối khi instance lệch config (nặng hơn sau opt-in/opt-out flip) (Session 97)
Triệu chứng: Phiếu PE/2026/A/010 CEO (Bước 3) đã duyệt thật (Lịch sử duyệt 16:47) nhưng sơ đồ báo "Phiếu đã kết thúc tại CCM Cấp 2 — không qua CEO" + làm mờ Bước 3 CEO. Anh Kiệt: "CEO duyệt mà ko thấy gì?".
Cơ chế: endsBeforeCeo (banner "không qua CEO" + làm-mờ bước-sau) tính theo CONFIG workflow (steps.Any(l => l.AllowApproverFinalize) = workflow CÓ cấp finalize) → luôn true nếu workflow có cấp "Duyệt thay CEO", BẤT KỂ phiếu NÀY thực sự finalize hay đã lên CEO. Phiếu opt-out finalize (approver trình tiếp) → lên CEO thật nhưng display suy từ config → nói dối. ComputeLevelStatus cũng blanket "Done" mọi cấp khi DaDuyet. Nặng lên sau S97 đảo default checkbox opt-out→opt-in: mặc định giờ TRÌNH TIẾP CEO → nhiều phiếu lên CEO → bug hiện thường xuyên (fix = follow-up trực-tiếp của chính S97 default-flip).
Fix (root-cause, Mig 60 8037fa0): thêm field RUNTIME EndedByLevelFinalize (set true CHỈ ở nhánh level-finalize service) → EndsBeforeCeo cho phiếu DaDuyet đọc runtime thay config (ChoDuyet giữ heads-up config); FE gate làm-mờ theo endsBeforeCeo; backfill dò marker "Duyệt KẾT THÚC tại". prod-verified Run #482 (PE-detail endsBeforeCeo=False live). Quy tắc: flag hiển-thị/trạng-thái = "instance NÀY ĐÃ làm-gì" (RUNTIME) — KHÔNG "workflow CÓ THỂ làm-gì" (CONFIG); config-flag chỉ đúng khi instance không-bao-giờ-lệch-config → đổi default opt-in/opt-out làm lệch phổ-biến → soi lại mọi display suy-từ-config. Liên-quan feedback_cardinality_change_grep_consumers · feedback_root_cause_over_symptom.
79. Backfill marker soi NHẦM bảng — transition-marker ở Changelogs KHÔNG phải Approvals (Session 97)
Triệu chứng: Mig 60 backfill EndedByLevelFinalize (nhận diện phiếu finalize-sớm qua marker "[Duyệt KẾT THÚC tại...]") match 0 dòng trên prod → phiếu finalize pre-Mig60 (PE/2026/A/005, A/008) giữ =0 → hiển thị SAI "Đã được CEO duyệt" thay vì "Kết thúc tại CCM".
Cơ chế: 2 bảng lịch-sử RIÊNG trong PE workflow: (1) PurchaseEvaluationApprovals = record per-approver (thêm ở main-flow ApproveV2Async:745, comment "[Bước X — Cấp Y] {user}"); (2) PurchaseEvaluationChangelogs = transition-log (LogTransitionAsync:1073 "chỉ log Changelog ở đây", ContextNote = marker "[Duyệt KẾT THÚC tại...không trình CEO]"). Marker finalize CHỈ nằm ở Changelogs. Backfill query Approvals.Comment LIKE '%KẾT THÚC%' → sai bảng → 0 match. (Bonus: approval-record ToPhase=evaluation.Phase chụp TRƯỚC khi terminal-branch đổi Phase → MỌI approval-record ToPhase=ChoDuyet; terminal chỉ ghi Changelog PhaseAtChange=DaDuyet — đừng tìm ToPhase=7 ở Approvals.)
Fix (Mig 61 BackfillEndedByLevelFinalizeFromChangelog): query ĐÚNG PurchaseEvaluationChangelogs.ContextNote LIKE N'%Duyệt KẾT THÚC tại%' + PhaseAtChange=7. Quy tắc: trước khi viết backfill/query dựa "transition-marker", XÁC ĐỊNH bảng nào thực-sự lưu marker (đọc code writer) — PE có 2 bảng lịch sử tách vai (Approvals=ai-duyệt · Changelogs=chuyển-phase+marker). Verify marker-count trên prod TRƯỚC khi tin backfill. Liên-quan #78 · #64 (data-migrate Sql prod).
80. ! null-forgiving trên dữ-liệu-parse-ngoài → NRE ẩn (build 0/0 KHÔNG bắt) (Session 113)
Triệu chứng: Import Excel NCC v2 — dòng thiếu Mã NCC (ô cột-4 trống): preview classify New/draft KHÔNG lỗi + báo "sẽ lưu nháp", nhưng Confirm nổ NullReferenceException → 500 CẢ batch (all-or-nothing → DB 0 ghi). Đúng path R4 lõi "thiếu Mã NCC → nháp" của feature.
Cơ chế: SupplierExcelImportService.NewSupplier gán Code = r.Code!.Trim(). Parser (ReadRaw) trả null cho ô trống (KHÔNG phải ""). Toán tử ! (null-forgiving) TẮT cảnh-báo nullable của compiler → build 0 warn / 0 err dù có mìn. Sibling entity.IsPublic = !string.IsNullOrWhiteSpace(row.Code) null-safe nhưng NewSupplier thì KHÔNG. Empty-string "" chạy OK; chỉ null nổ — và FE round-trip ô blank thành JSON null (không phải "") → reachable đường-thường.
Fix: Code = (r.Code ?? string.Empty).Trim() → null + "" đồng nhất → Code="" + IsPublic=false (nháp, ẩn khỏi filtered-unique [Code]<>''). Quy tắc: ! null-forgiving trên dữ-liệu-parse-từ-ngoài (Excel/JSON/form-input) = MÌN — dùng ?? default chứ KHÔNG !. Build-0/0 KHÔNG bảo-đảm null-safety khi có ! che. Chỉ test-before-critical bắt được (test-specialist viết characterization-test reproduce NRE → em-main fix prod → flip test sang NotThrow+draft). Liên-quan #73 (absolute-set null-clobber).
81. Derived-flag invariant re-derive ở write/select NHƯNG SÓT DELETE writer → giá-trị-phái-sinh kẹt stale → phantom (Session 114)
Triệu chứng: S114 đổi winner-truth single→multi per-hạng-mục — Supplier.IsWinner thành DERIVED (== Any(Quote.IsSelected)), maintain ở SelectWinner + UpsertQuote(insert+update) + seed + backfill. NHƯNG DeleteQuote + DeleteDetail (cascade) xóa quote-selected KHÔNG re-derive IsWinner → xóa quote-selected CUỐI của 1 NCC để IsWinner kẹt=true trong khi Any(IsSelected)=false. Hậu quả TÀI CHÍNH: CreateContractFromEvaluation Where(s.IsWinner) gom NCC phantom → sinh HĐ giaTri=0 cho NCC không-winner + winner-names phantom + FE row kẹt (canDelete=!isWinner). Build+test PASS, silent (0 test gọi Delete command).
Fix: helper PeWinnerInvariant.ReSyncAfterQuoteRemovalAsync (re-derive IsWinner toàn phiếu + re-sync SelectedSupplierId từ DB đã-flush — gọi SAU SaveChanges nên không cần loại-trừ quote-đã-xóa) gọi ở CẢ DeleteQuote + DeleteDetail. Test-before: 2 test RED→GREEN + guard-test (xóa hạng mục khác KHÔNG over-clear).
Bài học: derived-invariant phải re-establish ở MỌI mutation đổi tập-nguồn — kể cả DELETE + cascade-delete, không chỉ write/select. Enumeration writer trong spec KHÔNG đáng tin — grep độc-lập MỌI .Remove( chạm entity chứa field-nguồn (S87/S88 cardinality class mở rộng lên tầng mutation-path: writer-miss ≡ read-site-miss). Chỉ verify adversarial-refute bắt (invest/review/implement đều sót vì cùng theo enumeration). Liên-quan #72/#73 (cardinality) + feedback_cardinality_change_grep_consumers.
EXT S133 (instance 2 — indirect-assignment): snapshot ngân-sách-at-DaDuyet (Mig 67) đặt hook ở finalize choke-point (ApplyApprovedPriceOnFinalize + helper PeBudgetAccumulator 4-site) NHƯNG 2 site gán Phase TRỰC TIẾP bypass hook: admin-override PurchaseEvaluationWorkflowService.cs:308 (= targetPhase) + seeder DbInitializer:1323 (= current) → phiếu vào DaDuyet qua 2 đường này KHÔNG snapshot (frozen=0, display rơi về live — đúng bug đang vá, chỉ ở nhánh hiếm hơn). Reviewer adversarial bắt pre-commit; em-main + 3 invocation implement đều sót — cùng bài enumeration. Fix: site-5 chốt snapshot trong admin-override (comment [S133 site 5]) + seeder + test AdminOverride. Meta-class chung với instance 1: field-đích có hook/derive/snapshot ⇒ grep MỌI assignment-site của field đó (= targetPhase, .Phase =, seeder, override) chứ không chỉ mutation-API chính-tắc.
82. Menu-flag permission grant ≠ quyền API thật — controller gate GET bằng [Authorize] trần (Session 118)
Triệu chứng: S118 phân quyền role Procurement — grant menu Suppliers (R+C+U) tưởng "menu hiện = làm được mọi thứ". THỰC TẾ 2 chiều lệch: (1) PUT/DELETE /suppliers gate [Authorize(Roles="Admin,CatalogManager")] → PRO vẫn 403 khi Sửa/Xóa NCC dù CanUpdate=1 (chỉ Publish/Import theo policy Suppliers.Update là ăn); nút Sửa hiện nhưng bấm lỗi. (2) Ngược lại ReportsController chỉ [Authorize] trần (KHÔNG policy) → menu bị S92 ẩn nhưng MỌI user đăng nhập vẫn gọi GET /reports/dashboard lấy tổng giá trị HĐ + top NCC/dự án theo giá trị (gõ URL/API trực tiếp, bỏ qua menu).
Cơ chế: Menu-tree (GetMyMenuTree) filter theo Permission CanRead → CHỈ đổi HIỂN THỊ menu FE. Hiệu-lực API THẬT chỉ ở endpoint mang [Authorize(Policy="X.Y")] (map qua MenuPermissionHandler). Nhiều controller master gate GET bằng [Authorize] trần (mở mọi authed user, chủ ý S59) + gate write bằng [Authorize(Roles=...)] (KHÔNG theo menu-key). Nên grant "R/C/U" phần lớn = FE-button-visibility; delta API thật ≪ mong đợi.
Fix: (1) Endpoint lộ data nhạy → gate [Authorize(Policy="Reports.Read")] per-method (S118 dashboard + contracts/export; my-dashboard giữ mở vì scope theo currentUser). (2) Quy tắc: TRƯỚC khi tin hiệu-lực 1 grant → grep authz-attribute của CHÍNH controller đích ([Authorize(Policy=...)] vs [Authorize(Roles=...)] vs [Authorize] trần). Bài học: grant permission = 2 tầng ĐỘC-LẬP: menu-display (Permission table) ⟂ API-authz (controller attribute) — verify CẢ HAI. reviewer (adversarial) bắt được vì grep authz thật; em-main-solo tin "menu-flag=quyền" thì sót. Liên-quan #44 (silent 403).
83. Số đo AGGREGATE session-cumulative tự-lão-hoá — audit COUNTS đổi bởi chính spawn sau đó (Session 126)
Triệu chứng: Email báo hub trích "Audit: 2 dispatch / 2 explicit / 0 mismatch" kèm lệnh reproduce (spawn-model-audit.ps1 -SessionId …). Reviewer-gate chạy lại ĐÚNG lệnh đó → 4/4/0 — non-reproducible. Thủ-phạm: 2 lane của CHÍNH vòng review (B1-sweep + email-gate) spawn SAU phép đo → count 2→4, còn tăng khi phiên còn sống.
Cơ chế: spawn-model-audit.ps1 Section B scope = per-SESSION (đếm mọi agent-*.jsonl dưới subagents/workflows/ của phiên), KHÔNG per-RUN. Mọi số aggregate lấy từ scope "phiên đang sống" = snapshot tự-lão-hoá — hành-vi review/closeout về sau CHÍNH NÓ đổi số (quan-sát làm nhiễu vật đo).
Fix: (1) Outward artifact mời-reproduce: trích bảng per-lane theo run-id (bất-biến), KHÔNG trích COUNTS tổng; buộc trích tổng → scope rõ "cumulative cả phiên". (2) Internal doc đông-cứng số tổng = ĐÚNG point-in-time nhưng CẤM thừa-kế nguyên-văn ra thư. (3) Biến-thể số-đếm của bài "acceptance literal tự-lão-hoá" (S121). Reviewer-gate G-015 bắt; em-main-solo sót.
84. Sửa data tay trên prod bị code-path ungated @restart ĐẢO NGƯỢC im lặng — revoker 2 chiều (Session 159)
Triệu chứng: S159 đợt-2 grant menu 4-GĐ bằng SQL tay trên prod (494 row canread) → deploy kế restart pool → cicd mục-9 đo lại chỉ còn 47/494 (Admin 38 + Procurement 9): RevokeTemporarilyHiddenModulesAsync (nhánh ẩn-module [S92], ungated, chạy MỌI restart) lật 447 row về false. Grant "thành công" sống đúng tới lần restart kế — không lỗi, không log phía người sửa.
Cơ chế — 2 CHIỀU của cùng một lớp lỗi: DbInitializer chạy mỗi boot có cả 2 loại code-path ngược nhau: (a) seeder grant skip-existing — chỉ insert row THIẾU, KHÔNG nâng row false sẵn có ⇒ xóa/hạ tay sẽ KHÔNG được seed lại đúng kỳ vọng "seeder tự chữa" (mặt này là gotcha #75/#76); (b) revoker ungated — lật XUỐNG mọi row khớp điều kiện ⇒ grant tay bị nuốt @restart (mặt mới S159). Hệ quả chung: MỌI thay đổi permission/menu tay trên prod đều tạm thời nếu tồn tại code-path @startup theo hướng ngược — phải grep DbInitializer cả 2 hướng (Seed* VÀ Revoke*) trước khi tin thao tác tay là bền.
Fix: (1) Đổi trạng thái menu/permission = code-first qua seeder + deploy, KHÔNG SQL tay (S159 đợt-3 gỡ nhánh [S92] khỏi revoker rồi mới regrant). (2) Muốn chứng "fix bền": khuôn 3-chân-kiềng — (i) chứng restart THẬT (w3wp StartTime in-window + dòng đầu log mới), (ii) đo chỉ-số đích SAU restart, (iii) control ÂM chứng checker còn sống (Hrm/Off/Personal non-Admin vẫn =0 ⇒ revoker không bị vô hiệu hoá cả hàm — thiếu vế này thì "fix" bằng cách xoá luôn revoker cũng xanh y hệt). (3) Khoá regression bằng test 2-vế StillHiddenKeys (false) ∧ ReopenedKeys (true) — re-add nhánh là CI đỏ. (4) Comment quanh seeder nêu LUẬT + lệnh grep tự-kiểm, CẤM ghi số-đếm (số thành nợ tự-lan — 2 lần lệch trong 1 ngày @S159). Liên-quan: #75/#76 (chiều seeder re-add), #51 (seed gate), #82 (menu-flag ⟂ API-authz).
85. Menu-key OR-gate FE ≠ policy per-action BE — gate rộng hơn endpoint ⇒ 403 rải khắp UI (Session 162)
Triệu chứng (chưa kịp xảy ra trên prod — reviewer bắt trước ship): cây 4-folder GĐ gate quyền GĐ2 bằng ['Khkk_List','Khkk_G1','KeHoachKyKet'].some(can), nhưng endpoint /contract-signing-plans gắn [Authorize(Policy="KeHoachKyKet.Read")] (ContractSigningPlansController.cs:36) và MenuPermissionHandler.cs:40 so khớp CHÍNH XÁC 1 key — KHÔNG kế thừa cha, KHÔNG OR key con. ⇒ role có Khkk_List=1 mà KeHoachKyKet=0 sẽ: FE mở cổng → gọi API → 403 → folder GĐ2 hiện "—" dưới MỌI gói thầu, và claim "gate trước fetch ⇒ 0 rác 403" vỡ.
Vì sao dễ mắc: luật "gate phải OR nhiều key" là ĐÚNG cho tầng HIỂN THỊ — GetMyMenuTreeQuery.cs:61-96 trả node khi CanRead=true hoặc có con CanRead nên root có thể tồn tại với canRead=false. Người viết mang nguyên luật đúng-ở-tầng-này sang tầng API-authz nơi nó sai. Đây là mặt thứ hai của #82 (menu-hiện ≠ API-mở); lần này là menu-hiện rộng hơn API-cho.
Fix: gate mỗi query bằng đúng key policy của endpoint (['KeHoachKyKet']). Endpoint chỉ có [Authorize] trần (/contracts — #82 còn treo có chủ đích) thì OR key hiển-thị vô hại vì không có policy per-action để đá. Trước khi wire gate: mở controller đích đọc attribute, đừng suy từ cây menu.
Đo trước khi tin: Dev DB 13/13 role đều có Contracts=1 ∧ KeHoachKyKet=1 ⇒ ca này chưa lộ; nó chờ đúng 1 lần chỉnh ma-trận quyền. Lớp lỗi "chưa vỡ vì dữ-liệu hiện tại may mắn" phải vá ngay, không đợi triệu chứng.
87. Mã lỗi PRE-AUTH (415/411) trả lời SAI câu hỏi authz — phải re-probe đúng dạng body (Session 172)
Triệu chứng. Probe endpoint MỚI để kiểm quyền: POST /api/contract-signing-plans/{id}/approval-attachments với Content-Type: application/json ⇒ 415. Đọc thành "route tồn tại nhưng authz chưa wire" hoặc "deploy hỏng" — cả hai đều sai.
Cơ chế. Endpoint nhận multipart (upload file). Content-type mismatch bị loại ở tầng action-selection, tức TRƯỚC authorization filter ⇒ request chưa bao giờ chạm [Authorize(Policy = …)]. Cùng cơ chế với 411 Length Required của IIS ở POST /hubs/notifications/negotiate bodyless (họ hàng #25). Gửi đúng multipart ⇒ 401; kèm bearer ⇒ 400 liệt tên field (validator MỚI đã chạy).
Luật. Mã pre-auth (411 · 415) là vô nghĩa cho câu hỏi authz — nó chỉ chứng route tồn tại. Muốn đọc được authz thì re-probe đúng dạng body, rồi mới tin 401/403.
Bonus — wire-proof mạnh hơn 401. 401 chỉ chứng route + có gác. 400 mà thân lỗi GỌI ĐÚNG TÊN FIELD của command MỚI (kèm whitelist đuôi file) chứng end-to-end Api → Application rằng binary mới đang phục vụ — mạnh hơn cả đo w3wp StartTime. Dùng làm ship-proof khi hash-delta không đủ.
⚠️ Kèm bẫy phạm-vi (cùng phiên): probe bằng 2 tài-khoản test rồi kết luận "role thường có đủ quyền" là suy-luận vượt mẫu — cả 2 đều mang vai cấp riêng, không cái nào là user trần ⇒ nhánh "vai thường khác vẫn 403" (chính hình dạng gốc của #85) chưa bị loại. Phát biểu kết luận đúng bằng tập đã đo.
86. Hai push sát nhau — Gitea auto-CANCEL run đầu: CANCELLED ≠ FAIL, verifier đọc run CUỐI của range (Session 167)
Triệu chứng: K7 aaed699 push xong, governance 628132c push sát sau. Run Gitea Actions của aaed699 chuyển CANCELLED → tra run theo commit thấy như deploy fail, trong khi K7 ĐÃ LIVE: ship qua run #442 của push sau (checkout HEAD ⊇ K7). cicd-K7 verify artifact-level xác nhận: bundle rotate ×2 app + marker 8/8 + seeder log "0 added + 2 upgraded" + CanCreate 1→3 — live sạch.
Cơ chế: Gitea auto-cancel run đang chạy/queued của CÙNG branch khi push mới tới ⇒ (a) không bảo-đảm tồn tại verdict per-commit — run-theo-commit có thể chết cơ-chế chứ không phải lỗi build; (b) kỳ vọng "N push = N deploy/restart" SAI — run đầu bị cancel KHÔNG deploy ⇒ side-effect @startup (seeder/revoker/migration) chỉ chạy 1 lần cho cả cụm push; đếm restart theo số push là đếm ảo (liên #84 3-chân-kiềng: chứng restart phải đo w3wp StartTime, không đếm push).
Fix: (1) Verifier đọc run CUỐI của commit-range (run của push mới nhất — HEAD chứa mọi commit trước), CANCELLED giữa range = cơ-chế, KHÔNG phán fail; (2) bằng-chứng ship lấy ở artifact-level (bundle-hash rotate + marker set + seeder log + smoke), không ở nhãn run per-commit; (3) cần verdict/side-effect per-commit riêng (vd đo seeder từng đợt) → giãn push hoặc re-run tay. Nguồn: runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k7.md M0/M4.
Checklist debug bug mới
- Build pass không? → fail → check using + package version compat
- DI register đủ? → runtime error "Unable to resolve" → add
AddScoped/Singleton - API log startup có error ẩn? →
tailoutput file - File đã persist đúng chưa? →
head -5verify sau Write - Nếu package exotic → thử downgrade về stable trước
- Nếu TS error → check
erasableSyntaxOnly,verbatimModuleSyntax - Nếu EF expression tree → tách logic ra ngoài query
- Nếu Unicode CLI → dùng file payload
- Nếu workflow 403 → check FE
workflow.nextPhasessync từ BE pinned policy - Nếu SignalR 401 → dùng
accessTokenFactory+ BE OnMessageReceived hook (#26) - Nếu PS 5.1 script fail → check encoding UTF-8 / BOM / ASCII-only (#30)
- Nếu subdomain trả sai content / bị hijack → check IPv4/IPv6 port collision trên VPS shared (#33)
- Nếu 2 NavLink cùng active / không đúng highlight → custom isActive match query string (#34)
- Nếu menu item có quyền nhưng không hiện → check GetMyMenuTreeQuery inheritance extend (#35)
- Nếu FE gọi API sai URL sau đổi env → rebuild + clear bundle cache (#36)
- Nếu .ps1 fail parser trên PS 5.1 → ASCII-only, grep multi-byte chars (#30, #37)
- Nếu rename email Identity vẫn 401 → update 4 field NormalizedEmail/UserName (#38)
- Nếu CI fail TCP timeout 21s ở "Set up job" → bypass github.com, manual checkout từ Gitea (#39)
- Nếu npm install caching fail
tsc not found→ KHÔNG dùng junction Move-Item, thử robocopy/Copy-Item (#40) - Nếu CI vẫn trigger khi commit MD-only → paths-ignore trong on:push không match patterns đúng (#41)
- Nếu user phàn nàn "feature work cho admin nhưng user empty/403 silent" → check class-level Authorize policy có over-restrict cho non-admin không, split per action (#44)
- Nếu button workflow label nói "Trả lại" nhưng phiếu vẫn tiến approve → audit FE
isRejectpayload condition vs buttonisSendBacklabel condition vs dialogisSendBackwarning condition — phải sync 3 chỗ với CÙNG set target phase. BE thêm guard(target ∈ terminalSet) ⇔ (decision=Reject)chặn caller mismatch (#45) - Nếu Gitea Actions API trả 404 trên
/actions/runs→ đúng path là/actions/tasks(Gitea naming khác GitHub). Cacheupdated_atstale ~2 min → cross-check VPS file LastWriteTime cho time-sensitive verify (#46) - Nếu test
OrderByDescending(CreatedAt).First()query audit table fail sau add Changelog mới → SQLite frozen-clock tie-break, MUST filter Summary/EntityType discriminator (#48) - Nếu page move cross-app (Implementer Case 2) nhưng menu leaf KHÔNG hiện sidebar dù BE trả permission OK → check
Layout.tsxresolvePathstaticMap miss key mapping → MenuLeaf null guard silent drop (#50). 4-place mirror checklist: page + Routes + menuKeys.ts + Layout.tsx staticMap - Nếu new Seed method KHÔNG chạy prod dù dotnet build PASS + deploy SUCCESS → check nested inside
if (!demoSeedDisabled)gate (Plan T S23 t10 flag enabled prod) → INFRASTRUCTURE seed phải PROMOTE OUT of DemoSeed gate (#51). Decision tree: production cần để work end-to-end? YES → ungate - Nếu UI audit list show
Đã gửi duyệt → Đã gửi duyệtlặp gây nhầm → drop dual-phase badge khi state machine self-loop, thay Decision badge + next-target hint parse từ comment (#49) - Nếu RAG
search_memorytrả 0 results dù Qdrant green + BM25 có data →qdrant-clientupgrade xóasearch()method, bị nuốt silent. Test:python -c "from qdrant_client import QdrantClient; c=QdrantClient(url='http://127.0.0.1:6333'); c.search". Fix: dùngquery_points(query=...).points(#52) - Nếu sub-agent (Reviewer/CICD) return PASS verdict bị cut mid-sentence ở "Update MEMORY.md" step → MEMORY > 25KB triggers truncation risk. Mitigation: tight brief ≤ 8K + em main grep verify manual + curate MEMORY pre-spawn nếu > 25KB (#53)
- Nếu spawn sub-agent trả
API Error: 529 Overloaded+tokens=0→ Anthropic API transient overload, agent KHÔNG chạy. KHÔNG retry loop → em main solo fallback reliable (#54). Phân biệt với #53 truncation (agent chạy đủ token nhưng cut output) - Nếu sub-agent WRITE truncate NGAY ĐẦU exploration phase (chưa write file, đọc > 4 reference) → heavy spec ~10K + context bloat. Mitigation: brief ≤ 8K + pre-supply reference snippet trong brief HOẶC em main solo nếu cần đọc > 4 reference file (#55)
- Nếu
git commit -mqua PS 5.1 báoerror: pathspec 'xxx' did not matchvới message tiếng Việt có"→ native-arg escaping vỡ tại quote kép → Write message ra file UTF-8 +git commit -F <file>(#59) - Nếu thao tác theo-email/code trên data prod (lock/seed/migrate) trả 0 row affected → DUMP bảng env đích trước khi nghi code — population Dev ≠ prod (seed silent-fail
IdentityResultkhông throw) (#60) - Nếu thêm enum value vào entity DÙNG-CHUNG mà UI/guard phân-loại sai (file lẫn section / false-pass guard) → grep MỌI predicate field-proxy (
supplierId===null...) loại value mới + build-verify TỪNG app riêng (#71) - Nếu Gitea Actions run CANCELLED ngay sau khi push 2 commit sát nhau → auto-cancel cùng-branch: đọc run CUỐI của range + verify artifact-level (bundle/marker/seeder-log), KHÔNG đọc CANCELLED thành deploy-fail; side-effect @startup chỉ chạy 1 lần cho cả cụm (#86)
- Nếu probe endpoint để hỏi authz mà nhận 415/411 → đó là mã PRE-AUTH, vô nghĩa cho câu hỏi authz: request bị loại ở tầng action-selection / IIS TRƯỚC authorization filter. Re-probe đúng dạng body (multipart cho endpoint upload; có body cho SignalR negotiate) rồi mới đọc 401/403 (#87)