# Rules — Coding Conventions SOLUTION_ERP

> Nguồn duy nhất cho rules. Bất cứ commit nào vi phạm cần justify trong message.

## 1. Ngôn ngữ

| Thành phần | Ngôn ngữ |
|---|---|
| UI FE | **100% tiếng Việt** |
| Code (.cs/.ts) | **English** (tên biến/hàm/class) |
| Comment code | Tiếng Anh hoặc Việt — miễn rõ |
| Tên DB table + column | **English PascalCase** (Contracts, SupplierId) |
| Business label | Tiếng Việt (vd `"Đang soạn thảo"` cho phase 2) |
| Commit message | English hoặc Việt — preferred English |
| Doc MD (docs/) | **Tiếng Việt** (audience là người Việt) |

### 1.1 Chất lượng viết — hướng ra ngoài vs nội bộ (Harness-7, adopt S64 2026-06-15)

> Adopt từ AI_INFRA broadcast `2026-06-15-Governance-harness-7-writing-quality` (body-hash `a4580ea9…` đã verify khớp). Đây là **sàn chất lượng văn bản**, chỉ áp cho **văn xuôi** và chỉ cho phần **hướng ra ngoài**. Sàn chỉ được nâng, không được hạ; không mâu thuẫn quyền tự quyết hình thức (bảng/văn xuôi/độ dài/emoji/tên mục).

- **O1 — Hướng ra ngoài phải viết tiếng Việt rõ nghĩa: câu hoàn chỉnh, đủ dấu câu, đúng ngữ pháp.** Bốn nhóm "hướng ra ngoài": ① nội dung email cross-project (`/send-email`, `/check-email`); ② broadcast và adap-report gửi AI_INFRA (`broadcasts/outbox/`, `docs/governance/adap-reports/`); ③ tài liệu chia sẻ cho sister; ④ **câu trả lời của em main (lead) gửi cho anh trong hội thoại**. Viết để người đọc hiểu trọn ý ngay, không viết kiểu điện tín cụt ngủn.
- **O2 — Ranh giới bất đối xứng (KHÔNG áp vào nội bộ).** Phần nội bộ — `docs/STATUS.md`, `docs/HANDOFF.md`, `docs/gotchas.md`, agent-memory (`.claude/agent-memory/`), spawn-record, `error-ledger.md`, suy luận nội bộ — **giữ lối viết nén** theo §6.4 và §6.5 (KEEP-vs-CUT), miễn là vẫn rõ nghĩa, đúng chính tả và không lỗi mã hóa (mojibake). Lý do bất đối xứng: hướng ra ngoài cần người ngoài hiểu trọn, còn nội bộ cần gọn để chống loãng context.
- **O3 — Thực thi = cổng reviewer hướng ra ngoài + tự kiểm.** Reviewer thêm trục "chất lượng viết" {câu hoàn chỉnh · dấu câu · ngữ pháp · rõ nghĩa} khi review nội dung hướng ra ngoài (xem `.claude/agents/reviewer.md` Category 6). Em main tự đọc lại một lượt trước khi gửi email, adap-report, hoặc trả lời anh, bảo đảm từng câu đứng vững một mình.
- **Token kỹ thuật giữ nguyên:** đường dẫn file, tham chiếu `§`, định danh, mã hash, đoạn code, tên riêng tiếng Anh — KHÔNG dịch. Luật chỉ áp cho câu và đoạn văn xuôi.

## 2. Backend — .NET 10

### 2.1 Clean Architecture dependency rule

```
Api → Application ← Domain
      ↑
Infrastructure ──┘
```

- **Domain:** pure, chỉ dùng `Microsoft.Extensions.Identity.Stores` (cần cho IdentityUser) — không depend ASP.NET Core framework
- **Application:** depend Domain; CQRS handlers + interfaces + DTOs + validators
- **Infrastructure:** depend Application + Domain; impl interfaces (EF, JWT, rendering, services)
- **Api:** depend Application + Infrastructure; Controllers + middleware + composition root

### 2.2 CQRS + MediatR pattern

- 1 feature = 1 file `{Verb}{Entity}Command.cs` / `{Verb}{Entity}Query.cs`
- File chứa: Command record + Validator + Handler (cùng 1 file cho compact)
- Dài thì tách: `{Feature}/Commands/{Create}/...`

**Command naming:**
```
Create{Entity}Command      // tạo mới
Update{Entity}Command      // update full
Patch{Entity}FieldCommand  // update 1 field
Delete{Entity}Command      // soft delete (default)
{Action}{Entity}Command    // action domain-specific (vd TransitionContract)
```

**Query naming:**
```
Get{Entity}Query            // single by Id
List{Entity}sQuery          // list với paging
{Entity}{Purpose}Query      // custom query (vd GetMyInboxQuery)
```

### 2.3 Validation

- FluentValidation (KHÔNG dùng DataAnnotation)
- Validator = class trong cùng file command: `{Command}Validator : AbstractValidator<{Command}>`
- Pipeline: `ValidationBehavior` auto-chạy trước Handler — throw `ValidationException` → 400 ProblemDetails

### 2.4 Error handling

- KHÔNG try-catch ở Controller
- Throw domain exception: `NotFoundException`, `ForbiddenException`, `UnauthorizedException`, `ConflictException`, `ValidationException`
- `GlobalExceptionMiddleware` map exception → HTTP status + ProblemDetails JSON
- Logging: Serilog structured, không `Console.WriteLine`

### 2.5 Async/await

- Tất cả I/O (DB, HTTP, file) PHẢI async
- Truyền `CancellationToken` mọi async call
- Tên method async: suffix `Async` (trừ ASP.NET action signature)

### 2.6 Entity rules

- Mọi entity mới extend `BaseEntity` (Id Guid + audit) hoặc `AuditableEntity` (+ soft delete)
- PK: `Id` type `Guid`, default `Guid.NewGuid()` trong constructor
- FK: `{Entity}Id` type `Guid`
- Enum: `HasConversion<int>()` trong EF config
- String: luôn `HasMaxLength(n)` trong EF config — không để `nvarchar(max)` trừ khi là JSON/rich text
- Money: `decimal` + `HasPrecision(18, 2)`
- DateTime: luôn UTC, lấy từ `IDateTime.UtcNow` (không `DateTime.Now`)
- Query filter soft delete: `HasQueryFilter(x => !x.IsDeleted)` cho AuditableEntity

### 2.7 DI registration

- Singleton: stateless services (DateTimeService, FormRenderer)
- Scoped: per-request services (JwtTokenService, ContractWorkflowService, DbContext)
- Transient: lightweight utility (hiếm dùng)

### 2.8 Package version pinning

- **KHÔNG dùng `*` hoặc `latest`** — luôn pin version cụ thể
- Check compatibility với .NET 10 trước khi add. Gotchas đã biết:
  - MediatR 14 → lỗi IMediator DI, **pin 12.4.1**
  - Swashbuckle 10 → conflict OpenApi 2, **pin 6.9.0**
- Xem [`gotchas.md`](gotchas.md) cho full list

## 3. Frontend — React 19 + Vite 8 + TS 6

### 3.1 Named export, không default export

```tsx
// ✅
export function SuppliersPage() { }
export const MyConstant = 1

// ❌
export default function SuppliersPage() { }

// Exception: App.tsx dùng default export (Vite convention)
```

### 3.2 erasableSyntaxOnly (Vite 8)

KHÔNG dùng `enum` — dùng const-object pattern:

```ts
// ❌
export enum SupplierType { NhaCungCap = 1 }

// ✅
export const SupplierType = { NhaCungCap: 1, NhaThauPhu: 2 } as const
export type SupplierType = typeof SupplierType[keyof typeof SupplierType]
```

### 3.3 Path alias

- Dùng `@/` cho absolute import: `import { api } from '@/lib/api'`
- Config ở `vite.config.ts` + `tsconfig.app.json` (chỉ `paths`, không `baseUrl`)

### 3.4 Data fetching

- **TanStack Query** cho mọi API call (KHÔNG useState + useEffect thủ công)
- `useQuery` cho GET, `useMutation` cho POST/PUT/DELETE
- `invalidateQueries` sau mutation thành công

### 3.5 API calls

- Qua `api` instance (`src/lib/api.ts`) — đã wire axios interceptor JWT + 401 redirect
- Base URL: `/api` (Vite proxy → `:5443`)

### 3.6 Permission guard

```tsx
<PermissionGuard menuKey="Contracts" action="Update">
  <Button>Sửa</Button>
</PermissionGuard>

// Hook
const { can } = usePermission()
if (!can('Contracts', 'Update')) return null
```

Nhớ: FE guard chỉ là UX. BE policy `[Authorize(Policy = "Contracts.Update")]` là source of truth.

### 3.7 Component naming

- PascalCase cho file: `SuppliersPage.tsx`, `DataTable.tsx`
- kebab-case cho utility: `api.ts`, `cn.ts`
- React component = PascalCase
- Hook: prefix `use`, ví dụ `usePermission`

### 3.8 Toast + error

- Dùng `sonner` (đã wire `<Toaster>` ở App)
- `toast.success('...')`, `toast.error(getErrorMessage(err))`
- Error helper: `src/lib/apiError.ts` extract từ ProblemDetails

### 3.9 Duplicate giữa 2 FE

**CÓ CHỦ ĐÍCH.** Mỗi app (`fe-admin` + `fe-user`) là standalone — copy shared code giữa 2 app thay vì extract ra package chung. Lý do: 2 app có UX rất khác (admin mission-critical, user workflow-heavy), evolution độc lập.

Khi share code: `cp` file từ app này sang app kia. Đồng bộ tay khi có breaking change.

## 4. Database

### 4.1 Convention

| Item | Rule |
|---|---|
| Schema | 1 schema duy nhất: `dbo` |
| Table | PascalCase tiếng Anh, plural: `Contracts`, `Suppliers` |
| Column | PascalCase: `FullName`, `CreatedAt` |
| PK | `Id` — `uniqueidentifier` |
| FK | `{Entity}Id` — `uniqueidentifier` |
| Index | `IX_{Table}_{Col}` |
| Unique | `UX_{Table}_{Col}` |

Full: xem [`database/database-guide.md`](database/database-guide.md).

### 4.2 Migration

- Một commit = 1 migration (trừ khi nhiều change nhỏ trong cùng feature)
- Naming PascalCase: `AddMasterData`, `AddContractsWorkflow`
- Commit đủ 3 file: `{Name}.cs`, `{Name}.Designer.cs`, `ApplicationDbContextModelSnapshot.cs`

## 5. Git & commits

### 5.1 Branch

- `main` — deploy branch
- Feature branch (nếu nhiều người): `feature/phase{N}-{feature}`

### 5.2 Commit format

```
[CLAUDE] <scope>: <imperative message>

<body optional — chi tiết what + why>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```

**Scope:** `Contract` · `Form` · `Workflow` · `Supplier` · `Auth` · `Admin` · `Api` · `App` · `Domain` · `Infra` · `FE-Admin` · `FE-User` · `Docs` · `CICD` · `Scripts` · `Report` · `Dashboard`

### 5.3 Một commit nên

- Build pass (BE + FE)
- Không leave WIP nửa chừng (trừ khi commit message nói rõ)
- Test E2E với endpoint mới (curl trong session log)

### 5.4 Không commit

- `.env`, `appsettings.*.Local.json`
- `node_modules/`, `bin/`, `obj/`
- `wwwroot/uploads/` (user files)
- `SolutionErp_Design` database (tạm cho EF CLI)

## 6. Docs

### 6.1 Khi nào viết session log

- Session có commit thay đổi code → PHẢI tạo `docs/changelog/sessions/YYYY-MM-DD-HHMM-{topic}.md`
- Session chỉ Q&A, không đụng file → không cần

### 6.2 Session log format

```markdown
# Session YYYY-MM-DD HH:MM — <topic>

**Dev:** Claude / Copilot / <name>
**Duration:** ~Nh
**Base commit:** <sha>

## Làm được

### Chunk X — <name>
- bullet list deliverable

## E2E verified

<curl output hoặc screenshot>

## Bug gặp + fix

| Bug | Fix |

## Docs updates

## Handoff

## Thông số cumulative
```

### 6.3 Update khi thêm feature

- Thêm entity → update [`database/database-guide.md`](database/database-guide.md) schema section
- Thêm endpoint → update [`flows/`](flows/) relevant flow
- Thêm bug fix lặp lại → update [`gotchas.md`](gotchas.md)
- Thêm pattern → update skill tương ứng ở `.claude/skills/`
- Phase đổi → update [`STATUS.md`](STATUS.md) + [`HANDOFF.md`](HANDOFF.md) + [`changelog/migration-todos.md`](changelog/migration-todos.md)

### 6.4 Audit + compact MD định kỳ

**Triết lý:** KHÔNG rewrite toàn bộ. Chỉ **compact + patch drift** định kỳ. Test/code là source of truth, MD theo sau.

**Cadence + trigger:**

| Tần suất | Action | Effort | Trigger |
|---|---|---|---|
| **Cuối session** (đã làm tự động) | Update STATUS Recently Done + HANDOFF cảnh báo session tiếp | ~10 phút | Mỗi session có code commit |
| **Cuối phase** (~1 tháng) | Compact STATUS Recently Done > 30 row → `changelog/recently-done-archive-{YYYY-MM}.md`. Collapse migration-todos Phase done thành 1 paragraph cross-ref session log | ~30 phút | Phase đóng |
| **Đầu mỗi tháng** (cron `solution-erp-skill-audit-monthly`) | Audit skill + doc drift + count consistency. Patch stale qua Edit (KHÔNG rewrite). Log kết quả | ~45 phút | 9:00 ngày 1 mỗi tháng |
| **Khi trigger riêng** | Selective rewrite 1 file (skill drift > 30%, architecture redesign, phase major) | 1-2 giờ | Quantitative trigger |
| **KHÔNG BAO GIỜ** | Rewrite toàn bộ docs | — | — |

**Doc audit checklist (mở rộng từ §9.4 skill audit, chạy cùng cron):**

```
1. dotnet test SolutionErp.slnx → verify count khớp STATUS/CLAUDE/HANDOFF
2. ls migrations/ → verify count khớp ef-core-migration skill + database-guide
3. grep '#[0-9]\+' gotchas.md → verify khớp count claim trong HANDOFF/STATUS
4. ls .claude/skills/ → verify khớp count trong rules §9 + skills/README
5. STATUS Recently Done > 30 row? → đề xuất archive cũ
6. HANDOFF có section duplicate file khác (PROJECT-MAP/workflow/git log)? → đề xuất bỏ
7. Skill SKILL.md mention "Phase N MVP" mà N <= phase hiện tại? → stale, refresh
8. Output report stale items + suggest patches → human review
9. Log vào docs/changelog/skill-audit-{YYYY-MM}.md (1 page max)
```

**Anti-pattern (đã trải nghiệm):**
- ❌ Rewrite toàn bộ MD định kỳ — mất context, paraphrase mất nuance, ROI thấp
- ❌ Bulk-clone 3rd party docs — đã từ chối ở Skill governance §9.5
- ❌ Skip audit "vì sắp UAT/sắp deploy" — drift tích lũy → onboard agent mới sai

**Trigger override:** User nói "audit MD", "kiểm tra docs", "định kỳ kiểm tra", "compact docs" → chạy doc audit ngay không đợi cron.

### 6.5 Consolidate MD đúng cách — KEEP vs CUT

> **Bài học session 6 (2026-04-30):** Compact -288 dòng nhanh, paraphrase + cắt narrative để "rõ đẹp" → user phản hồi "viết MD gọn lại tý là mất mẹ luôn tính cách cũ". Docs đọc 6 tháng sau như machine output, không hiểu tại sao chọn approach. Rule §6.5 này chốt để KHÔNG TÁI PHẠM.

**Mục tiêu consolidate:** Giảm độ dài để session sau đọc context không mệt. **KHÔNG = cắt thông tin / paraphrase / "đẹp hóa".**

#### KEEP — CẤM CẮT (asset không tái tạo được)

| Loại | Vì sao bắt buộc giữ NGUYÊN VĂN | Ví dụ |
|---|---|---|
| **Narrative tích lũy qua sessions** | Không tái tạo. Là tài sản chỉ ra "đã đi qua đâu, đã đổi hướng ra sao" | "Ban đầu chọn MediatR 14, fail DI runtime, downgrade 12.4.1" |
| **Rationale (lý do quyết định)** | Quan trọng hơn quyết định. Mất rationale → agent mới revert vì không biết tại sao | "Pin Swashbuckle 6.9.0 vì 10.x conflict OpenApi 2", "Skip E2E Playwright vì brittle cho solo dev" |
| **Gotcha context (incident bối cảnh)** | Gotcha = fix + bối cảnh đau. Mất context → instructions khô không apply được | G-084 "Next.js bind 0.0.0.0 → Gitea fallback IPv6 → IIS ARR resolve IPv4 first → leak homepage" |
| **Anecdote / bài học** | Nuance không có trong code/git, chỉ có trong docs | "Bài học NamGroup: Node latest fail CI Windows" |
| **Decision why over decision what** | Code chỉ có "what", docs phải bù "why" | "PE workflow tách enum riêng vì Phase ≠ ContractPhase" |

#### CUT — XÓA hoặc CHUYỂN CHỖ được

| Loại | Cách xử lý | Ví dụ |
|---|---|---|
| Số liệu lặp giữa nhiều file | Giữ 1 chỗ canonical, các file khác cross-ref | "52 bảng" canonical ở PROJECT-MAP, STATUS/HANDOFF cross-ref |
| Section duplicate copy-paste | Cross-ref thay vì copy nguyên văn | "Versioned WF quick ref" → cross-ref `workflow-contract.md §7bis` |
| List Recently Done > 30 row | Archive sang file riêng `recently-done-archive-{YYYY-MM}.md` (CHUYỂN CHỖ, KHÔNG xóa) | Phase 0-7 cũ |
| Phase đã đóng > 1 tháng | Collapse thành 1 paragraph + link session log đầy đủ | Phase 0-5 → "Phase 0-5 done, [session logs](changelog/sessions/)" |
| Stats lặp | Cumulative table giữ ở STATUS, file khác chỉ con số mới nhất | LOC / endpoint / migration count |

#### CẤM TUYỆT ĐỐI

- ❌ **Paraphrase câu chữ original** — dù "rõ ràng hơn", KHÔNG paraphrase. Giữ y nguyên văn cũ.
- ❌ **Summary đoạn đã có narrative** — không tóm tắt 5 dòng kể chuyện thành 1 dòng "X done". Mất context.
- ❌ **"Đẹp hóa" bằng cách cắt** — đẹp đạt qua format (bảng, anchor link, heading), KHÔNG qua cắt nội dung.
- ❌ **Compact với mindset machine-first** — viết cho agent mới đọc 6 tháng sau hiểu, KHÔNG phải tổng hợp ngắn nhất.

#### Decision tree khi gặp đoạn dài

```
Đoạn này có narrative / rationale / gotcha context / anecdote / "tại sao"?
├─ Có → GIỮ NGUYÊN VĂN, kể cả dài (lossless)
└─ Không → kiểm tra:
    ├─ Duplicate file khác?      → cross-ref
    ├─ Số liệu cũ stale?         → archive sang file riêng
    ├─ Phase đã đóng > 1 tháng?  → collapse + link session log
    └─ KHÔNG match gì            → GIỮ NGUYÊN (default lossless)
```

#### Validation sau consolidate (BẮT BUỘC chạy)

Đọc lại file đã compact, tự hỏi 3 câu:
1. **Agent mới đọc 6 tháng sau có biết "tại sao chọn approach này" không?**
2. **Có còn cảm giác "kể chuyện" tích lũy không, hay chỉ là instructions khô?**
3. **Gotcha context vẫn còn ý nghĩa khi đọc rời rạc không?**

→ Nếu "không" cho **bất kỳ** câu nào → revert đoạn đó về nguyên văn cũ.

#### Trigger áp §6.5

- Audit định kỳ §6.4 (cron đầu tháng)
- User nói "consolidate", "compact", "gọn lại MD", "rõ ràng MD"
- Cuối phase đóng (>1 tháng) khi compact STATUS/HANDOFF/migration-todos

### 6.6 Quyết-định kiến-trúc/chức-năng = tiêu-chí khách-quan (KHÔNG cảm-tính) [User-Mark H-12/13 + H-14, S79 · +H-15 value-gate S81]

> **Adopt S79 (2026-06-20)** — AI_INFRA `rc-signature` (06-19) + `harness-14-adopt` (06-20) + User-Mark canonical §P. Bịt một kẽ-hở: từ-chối/cắt một chức-năng bằng **lập-luận cảm-tính**. Quyền tự-quyết HÌNH-THỨC (form-autonomy) GIỮ nguyên — rule này chỉ chặn cảm-tính. Marks Active-High đã anh-confirm-stamp → [`.claude/governance/ACTIVE-MARKS.md`](../.claude/governance/ACTIVE-MARKS.md) (3 mark: objective-criteria `RC-…10-29-09` · User-Mark `…10-29-10` · time/age `…10-29-11`); cơ-chế P1-P10 → [`harness-11-engine.md §E`](governance/harness-11-engine.md).

**Một chức-năng được quyết bởi 3 trục KHÁCH-QUAN — KHÔNG gì khác:**

1. **Điểm-đau (pain-point)** — một lỗi đã **thực-sự xảy ra** (không phải giả-định).
2. **Khối-lượng công-việc.**
3. **Chất-lượng công-việc.**

**🔴 Căn-cứ SAI (cùng một họ — đều cảm-tính, đều CẤM dùng để bỏ/giảm chức-năng):**

| Căn-cứ sai | Vì sao sai | Nguồn |
|---|---|---|
| **Quy-mô-đội** ("quá mức cho solo-dev", "1 người làm thì cần gì") | Số người KHÔNG làm điểm-đau/khối-lượng/chất-lượng nhỏ đi. ERP solo dựng-1-tuần đã có khối-lượng lớn. | mark `RC-…10-29-09` (rc-signature) |
| **Thời-gian / tuổi-dự-án / recency-decay** ("cũ rồi nên bỏ", "lâu chưa dùng nên hạ", "tự-giảm theo tuổi") | Tuổi KHÔNG liên-quan nhu-cầu. Trần budget = (dung-lượng ÷ tốc-độ-thay-mới), KHÔNG núm-giảm-theo-tuổi; phát-hiện-trôi neo đường-nền-cuộn, KHÔNG cửa-sổ-theo-tuổi. | mark `RC-…10-29-11` (H-14) |

**Hệ-quả vận-hành (🔴 floor):**
- **AI = neo-lý-tính.** Gặp quyết-định biện-minh bằng cảm-giác ("nhiều quá", "không cần cho quy-mô tụi tôi", "cũ rồi") → em-main **nêu-cờ + bác trên cơ-sở khách-quan**, KHÔNG xoa-dịu. "AI phải lý-tính HƠN ở chỗ này, không để con-người kéo mình về phía cảm-tính."
- **Thẩm-quyền phán "cần/thừa" = tầng xuyên-dự-án (AI_INFRA)**, KHÔNG dự-án đơn-lẻ tự-phán "thừa" (một dự-án chỉ thấy phạm-vi mình → thiếu bối-cảnh). Chức-năng adopt **đầy-đủ + nhất-quán**; chỉ HÌNH-THỨC là phần tự-chọn.
- **Chống "kinh-tế giả" (Goodhart)** — cắt chất-lượng để tiết-kiệm token thường khiến **làm-đi-làm-lại → tốn HƠN**. Tổng-quát rule H-8 "KHÔNG hạ model để chạy nhanh" thành "KHÔNG hạ chất-lượng bất-kỳ để tiết-kiệm". Áp NGAY, không chờ data. Mỗi chỉ-báo tự-động (recall/hit-rate/byte) phải gắn câu hỏi "có làm anh chốt-OK sớm hơn không".
- **Cắt-tỉa bộ-nhớ theo GIÁ-TRỊ, KHÔNG theo TUỔI (H-15 B(b)).** Khi lưu-trữ/cắt L1→L2: GIỮ lỗi-lặp-lại + anti-pattern + gotcha + root-cause **bất-kể tuổi** (cắt theo giá-trị-thấp, KHÔNG FIFO-theo-ngày) — cùng họ time/age ở trên, áp xuống tầng-bộ-nhớ. Cơ-chế → [`memory-budget.json:archive_gate.value_protect`](../.claude/agent-memory/memory-budget.json) + [`harness-11-engine.md §G`](governance/harness-11-engine.md). Token-budget = **sàn-tận-dụng KHÔNG trần-tiết-kiệm** (tiết-kiệm-token = quên-việc).
- **Chữ-ký RC + User-Mark:** mỗi đổi-luật cấp-governance từ S79 mang `RC-pqhuy1987-dd-mm-yyyy-hh-mm-ss` (anh ký-duyệt qua report-trước-đóng-dấu §E.2/P4) → ledger [`.claude/governance/ACTIVE-MARKS.md`](../.claude/governance/ACTIVE-MARKS.md). 4 cấp Active-High/Active/Medium/Disable (lệnh `/user-mark-*`). Forward-only.

## 7. Testing (Phase 9 active — 77 test pass post-Mig 21 + CI gate live)

### Stack đã apply

- **xUnit 2.9.3** + **FluentAssertions 7.2** (pin trước v8 commercial license)
- **Microsoft.EntityFrameworkCore.Sqlite 10** (in-memory testing — supports transactions, không như InMemory provider)
- **Custom `TestApplicationDbContext`** override `nvarchar(max) → TEXT` cho SQLite compat (xem `tests/.../Common/SqliteDbFixture.cs`)
- **NO mock framework** (tránh mock-heavy, prefer EF in-memory hoặc fake data)

### Test pyramid (bottom-heavy, KHÔNG E2E)

```
✅ Phase 1: Domain policy            54 test  (pure functions, no DB)
✅ Phase 2: Infra code generator     17 test  (SQLite in-mem)
✅ Phase 3 mini: Application versioning  6 test  (SQLite, no UserManager)
⏸️ Phase 3 full: Application handlers w/ Identity (cần UserManager DI helper)
⏸️ Phase 4: API smoke (WebApplicationFactory)
⏸️ Phase 5: FE Vitest cho lib utility
❌ E2E (Playwright) — KHÔNG làm (brittle cho solo dev)
```

### Khi nào viết test — timing rule

| Tình huống | Quy tắc |
|---|---|
| **Feature mới** | **Test-after**: UAT 2-3 lần ổn → viết test (≤1 commit theo sau). Pattern Phase 1-8 đã làm. |
| **Bug fix** | **Test-before BẮT BUỘC**: reproduce bug bằng test failing → fix → green. "1 bug = 1 regression test before merge". |
| **Critical algorithm** (code generator atomic, workflow guard, financial calc, security check) | **Test-before merge** — edge case nhiều, race condition đắt nếu break prod. |
| **Spec change** (không phải bug) | Update test cũ + code, commit chung. **KHÔNG xóa, KHÔNG skip** test cũ. Coverage chỉ tăng. |
| **Skip test** | DTO mapping, CRUD master đơn giản, FE component snapshot, wrapper passthrough, migration backfill idempotent. |

### Quy tắc bổ sung mỗi feature mới

- **Domain entity / enum** → 0 test (compile check đủ)
- **Domain policy method** → 2-3 test (positive + negative + edge case)
- **CQRS handler có guard logic** → 1 test mỗi guard branch
- **Migration mới** → smoke test "DbInitializer chạy được + seed không lỗi"
- **API endpoint mới** → 1 happy path smoke (cho endpoint critical)
- **Bug found in production / staging** → 1 regression test BEFORE merge fix

Pattern: **"1 bug found → 1 regression test added"**.

### Workflow user end-of-task

```bash
# Sau mỗi task code (BẮT BUỘC trước commit code):
dotnet test SolutionErp.slnx        # local verify ~3s
git add -A && git commit -m "..."
git push                             # CI tự run test gate trước build/deploy
```

Test fail local → fix trước khi push. Test fail trên CI → no deploy.

### CI gate behavior

`.gitea/workflows/deploy.yml`:

```yaml
# Path filter: skip CI hoàn toàn cho commit MD-only
on:
  push:
    paths-ignore:
      - 'docs/**'
      - '**/*.md'
      - '.claude/skills/**'

# 2 step test gate trước build/deploy (fail → exit → no deploy)
- name: Run unit tests (Domain)
- name: Run integration tests (Infrastructure)
```

### Pending (Phase 3-5 future)

- Application handler tests cần Identity UserManager DI setup helper
- API smoke tests via `WebApplicationFactory` + SQLite
- FE Vitest cho `lib/queryMatches`, `lib/cn`, money formatter

Detail: `docs/architecture.md §11 Testing strategy`.

## 8. Security baseline

- HTTPS everywhere prod
- JWT Secret trong user-secrets / env var (không commit appsettings)
- Password hash: PBKDF2 (Identity default)
- CORS whitelist chỉ 2 FE origin
- SQL injection: EF Core parameterized (không raw SQL trừ khi phải)
- Audit log: mọi ghi phải log qua `ContractApproval` hoặc `AuditLog` (future)
- Rate limit: `/api/auth/login` 5 req/min/IP (chưa implement — Phase 5)

## 9. Skills (.claude/skills/)

> Project-level skills encode kiến thức SOLUTION_ERP-specific. Claude tự auto-invoke qua Skill tool dựa trên semantic match với `description` trong `SKILL.md`. **PHẢI dùng skill khi gặp task tương ứng — không tự suy luận lại.**

### 9.1 Skills hiện có (6)

**Domain (3) — logic nghiệp vụ:**

| Skill | Trigger khi |
|---|---|
| `contract-workflow` | Bug chuyển phase, 403, mã HĐ sai, versioned WF, "HĐ cũ giữ cũ" |
| `form-engine` | Render template docx/xlsx, FieldSpec, PDF export, upload form mới |
| `permission-matrix` | Permission denied, gán role, menu không hiện, inherit Contracts/Workflows |

**Ops (3) — devops + security + schema:**

| Skill | Trigger khi |
|---|---|
| `dependency-audit-erp` | `npm audit`, `dotnet vulnerable`, scan CVE, nâng cấp package |
| `ef-core-migration` | `migrations add`, schema change, snapshot lỗi, revert migration, DesignTimeDbContextFactory |
| `iis-deploy-runbook` | Prod 500/502, cert hết hạn, restart app pool, gitea-runner fail, SignalR 401 |

### 9.2 Nguyên tắc tạo skill mới

**Project-specific, KHÔNG clone generic.** User-level skills global đã có sẵn (`code-reviewer`, `sql-database-assistant`, `focused-fix`, `senior-frontend`, `mcp-builder`, `webapp-testing`, `review`, `security-review`, ...). Project-level skills chỉ thêm khi:

- Encode kiến thức **SOLUTION_ERP-only** (path, version pin, gotcha number, commit convention)
- Generic skill không thể có (vd: 8 migration history cụ thể của project)
- Có YAML `when-to-use` triggers rõ ràng (cả tiếng Việt + Anh)
- Workflow Vietnamese-first (audience là dev Việt)

### 9.3 Format bắt buộc `SKILL.md`

```markdown
---
name: kebab-case-name
description: 1-3 câu mô tả + stack specificity (cho semantic match đúng)
when-to-use:
  - "trigger english"
  - "trigger tiếng Việt"
---

# Skill Name

## Context
## Workflow / Commands (copy-pastable, không pseudocode)
## Pitfalls
## Code pointers (path đầy đủ src/Backend/...)
## Related (dẫn chiếu gotcha #N, docs/...)
```

### 9.4 Audit định kỳ — mỗi đầu tháng (chạy chung với doc audit §6.4)

**Cadence:** Đầu mỗi tháng, cron `solution-erp-skill-audit-monthly` fire 9:00 ngày 1 → chạy combined audit (skill + doc drift theo §6.4 checklist).

**Skill audit phần riêng (mỗi skill SKILL.md):**

```
1. Đọc .claude/skills/README.md — list 6 skill hiện có
2. Cross-check stale:
   - SKILL.md mention "Phase N MVP" mà N < phase hiện tại → stale
   - Code pointers paths có còn tồn tại không (Glob check)
   - Count claim (vd "24 DbSet", "13 migration") khớp thực tế không
   - Trigger phrases có còn hiệu quả không
3. Cross-check feature gap:
   - docs/STATUS.md Recently Done — feature mới nào chưa có skill cover?
   - docs/gotchas.md — gotcha mới (>3 dòng / cluster) nào nên gom thành skill?
   - migration-todos.md — task lớn sắp tới có cần skill mới?
4. Hành động:
   - Skill stale → Edit SKILL.md (KHÔNG rewrite) + commit `[CLAUDE] Skill: refresh <name>`
   - Skill cần thêm → tạo theo §9.2 §9.3 + commit `[CLAUDE] Skill: add <name>`
   - Skill không còn cần → archive vào .claude/skills/_archived/ + commit
5. Update bảng §9.1 trong rules.md + .claude/skills/README.md + docs/CLAUDE.md count
6. Log skill + doc drift report vào docs/changelog/skill-audit-{YYYY-MM}.md (1 page max)
```

**Trigger:** User nói "audit skill", "check skill list", "skill staleness", "audit MD", "kiểm tra docs", "định kỳ kiểm tra" → chạy combined audit (§6.4 + §9.4) ngay không đợi cron.

**Threshold trigger ngoài cron:**
- `gotchas.md` +5 gotcha mới → audit (xem có gom thành skill mới được không)
- Phase mới đóng → audit ngay (track stale từ feature mới)
- Skill drift > 30% (count check, path broken, mention phase cũ) → selective rewrite

### 9.5 Anti-patterns

❌ **KHÔNG bulk-clone repo skill 3rd party** — phần lớn doc-dump generic, không có `when-to-use`, dễ trigger sai.

❌ **KHÔNG viết skill chỉ để "có thêm"** — skill = trigger semantic match, nếu description quá chung sẽ trigger nhầm trong session khác.

❌ **KHÔNG copy nội dung skill từ docs/ sang SKILL.md** — skill phải ngắn gọn, action-oriented; docs/ là reference dài. Skill dẫn chiếu docs.

❌ **KHÔNG bỏ qua skill khi gặp task khớp** — tự suy luận lại = lãng phí context + dễ sai.

## 10. Liên quan

- [`CLAUDE.md`](../CLAUDE.md) — entry point cho AI agent
- [`architecture.md`](architecture.md) — kiến trúc chi tiết (layered, CQRS, state machine)
- [`gotchas.md`](gotchas.md) — pitfalls đã gặp
- [`database/database-guide.md`](database/database-guide.md) — DB conventions
- [`flows/`](flows/) — sequence diagram per feature
- [`../.claude/skills/README.md`](../.claude/skills/README.md) — skill index 6 skill
