# Category Execution Rules

> **Loaded by:** step-02 (first iteration) and compact-loop.md (subsequent iterations)
> **Purpose:** Defines execution rules, MCP tools, folder paths, and forbidden patterns per task category.

---

## Domain

**Folder:** `Domain/Entities/{ContextPascal}/{App}/{Module}/`
**Enums:** `Domain/Enums/{ContextPascal}/{App}/{Module}/`
**Exceptions:** `Domain/Exceptions/{ContextPascal}/{App}/{Module}/`
**MCP:** `validate_conventions`

Rules:
- Entity base class inheritance (AuditableEntity, IHasData, ISoftDeletable)
- Audit fields: CreatedAt, CreatedBy, ModifiedAt, ModifiedBy
- Domain events for state changes
- Value objects for composite values

---

## Infrastructure — EF Core Configurations

**Folder:** `Infrastructure/Persistence/Configurations/{ContextPascal}/{App}/{Module}/`
**MCP:** `validate_conventions`, `check_migrations`

Rules:
- One configuration per entity
- Table name = plural entity name
- All relationships explicitly configured (no convention-based)
- Indexes on foreign keys and frequently queried fields
- Register DbSet in ExtensionsDbContext

---

## Infrastructure — Migration (BLOCKING)

> **CRITICAL:** After EF configs, a migration MUST be created and applied.
> Without migrations, no database schema = no tests = no API = broken module.

**Folder:** `Infrastructure/Persistence/Migrations/` (NEVER subdirectories)
**MCP:** `suggest_migration` → get migration name

Execution sequence:
0. Ensure `dotnet ef` is on PATH (platform-aware):
   ```bash
   if ! dotnet ef --version &>/dev/null; then
     for TOOLS_DIR in "$USERPROFILE/.dotnet/tools" "$HOME/.dotnet/tools" "$LOCALAPPDATA/Microsoft/dotnet/tools"; do
       [ -n "$TOOLS_DIR" ] && [ -d "$TOOLS_DIR" ] && export PATH="$TOOLS_DIR:$PATH"
     done
     dotnet ef --version &>/dev/null || { echo "ERROR: dotnet-ef not found. Install: dotnet tool install --global dotnet-ef"; exit 1; }
   fi
   ```
   > **Why:** On Windows (Git Bash), `$USERPROFILE/.dotnet/tools` is the correct path.
   > NEVER use `$HOME/.dotnet/tools` alone — on WSL, `$HOME` resolves to `/home/{user}` where .NET SDK is not installed.
1. Call `mcp__smartstack__suggest_migration` → get standardized name
2. `dotnet ef migrations add {Name} --project src/{Infra}.csproj --startup-project src/{Api}.csproj -o Persistence/Migrations`
3. Cleanup corrupted EF Core artifacts: `for d in src/*/bin?Debug; do [ -d "$d" ] && rm -rf "$d"; done`
4. `dotnet ef database update --project src/{Infra}.csproj --startup-project src/{Api}.csproj`
5. `dotnet build --no-restore` → verify build passes

**BLOCKING:** If migration fails, DO NOT proceed. Fix the EF configs first.

**Rules:**
- NEVER create subdirectories under Migrations/
- Always use `-o Persistence/Migrations` flag
- One migration per module (all entities together)
- Migration name from MCP (NEVER invented)

---

## Infrastructure — Seed Data (MANDATORY reference loading)

**Folder:** `Infrastructure/Persistence/Seeding/Data/{Module}/`
**MCP:** `generate_permissions` (PRIMARY), fallback to templates

> **IF** task description contains "seed data", "SeedData", "NavigationModule",
> "PermissionsSeedData", "RolesSeedData", or "IClientSeedDataProvider":
> **THEN read `references/core-seed-data.md`** — this is MANDATORY, DO NOT improvise.

**Seed Data Chain (9 files minimum):**

Application-level (created ONCE, shared across modules):
- **NavigationApplicationSeedData.cs**: Application navigation entry (MUST be first). Provides ApplicationId.
- **ApplicationRolesSeedData.cs**: 4 application-scoped roles (admin, manager, contributor, viewer). Provides role entries for SeedRolesAsync().

Per-module:
- **NavigationModuleSeedData.cs**: deterministic GUIDs (SHA256), 4 languages (fr, en, it, de)
- **Permissions.cs**: Static permission constants (`Permissions.{Module}.Read`). Referenced by `[RequirePermission]`.
- **PermissionsSeedData.cs**: MCP `generate_permissions` first, fallback to template
- **RolesSeedData.cs**: code-based role-permission mapping (Admin=wildcard, Manager=CRU, Contributor=CR, Viewer=R)

Per-module (MANDATORY when `seedDataCore.navigationSections` exists in feature.json):
- **NavigationSectionSeedData**: section entries, section translations (4 languages), section-level permissions, and section-level role mappings — all within `NavigationModuleSeedData.cs`, `PermissionsSeedData.cs`, and `RolesSeedData.cs`
- Section-level permissions: wildcard + CRUD per section (same pattern as module-level)
- Section-level role mappings: Admin=wildcard, Manager=CRU, Contributor=CR, Viewer=R per section

Infrastructure:
- **SeedConstants.cs**: shared deterministic GUIDs (ApplicationId, ModuleIds)
- **{App}SeedDataProvider.cs**: implements IClientSeedDataProvider with 4 methods
- DI: `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()`

**Rules:**
- IClientSeedDataProvider: SeedNavigationAsync + SeedRolesAsync + SeedPermissionsAsync + SeedRolePermissionsAsync
- Admin=wildcard(*), Manager=CRU (read+create+update), Contributor=CR (read+create), Viewer=R (read only)

**Business seed data (DevDataSeeder):**
- ALL seeded business entities MUST include `TenantId = {tenantGuid}`
- Reference entities (types, categories, statuses) MUST set TenantId
- Use deterministic TenantId from SeedConstants (NEVER hardcoded inline)
- DevDataSeeder MUST implement `IDevDataSeeder` with idempotent `SeedAsync()` method

**Section route conventions (BLOCKING):**
- `list` section route = module route (e.g., `/business/human-resources/employees`) — NO `/list` suffix
- `detail` section route = module route + `/:id` (e.g., `/business/human-resources/employees/:id`) — NOT `/detail/:id`
- Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (normal)
- FORBIDDEN: `/{module}/list`, `/{module}/detail/:id` — these are CRUD view modes, not sub-areas

**FORBIDDEN:**
- `Guid.NewGuid()` → use deterministic GUIDs
- Empty seed data classes with only GUIDs and no seeding methods
- Missing translations (must have all 4 languages)
- Seeding business entities WITHOUT `TenantId`
- Navigation section routes ending in `/list` or `/detail/:id`

### POST-CHECK: Navigation translations diacritical marks

After generating `NavigationTranslationSeedEntry` strings, verify ALL non-English translations contain proper diacritical marks:

- **FR** must use: é, è, ê, ë, à, â, ç, ù, û, ô, î (e.g., "Employés" NOT "Employes", "Activités" NOT "Activites")
- **DE** must use: ä, ö, ü, ß (e.g., "Aktivitäten" NOT "Aktivitaten")
- **IT** must use: à, è, é, ì, ò, ù (e.g., "Attività" NOT "Attivita")

Cross-reference navigation seed data labels with the corresponding frontend `i18n/index.ts` translations for consistency.

**BLOCKING** if any `NavigationTranslationSeedEntry` for fr/de/it contains only ASCII characters where diacritical marks are expected in the target language.

---

## Infrastructure — SQL Objects

**Folder:** `Infrastructure/Persistence/SqlObjects/Functions/`

Rules:
- `.sql` files with `CREATE OR ALTER` for idempotency
- `SqlObjectHelper.cs` for embedded resource loading

---

## Post-Infrastructure Build Check (BLOCKING)

> **After ALL infrastructure tasks (configs + migration + seed data) are done:**

```bash
dotnet build --no-restore
```

**BLOCKING:** Build MUST pass before proceeding to application/api/test/frontend.
If build fails, fix infrastructure code first.

> **NOTE:** If build passes but you suspect assembly issues (e.g., new packages added to Infrastructure
> that may not be in the meta-package), also run a quick startup check:
> `dotnet run --project {ApiProject} --urls "http://localhost:5098" &` and verify the process survives 5 seconds.
> See `references/error-classification.md` for error classification if it crashes.

---

## Application

**Services:** `Application/Services/{ContextPascal}/{App}/{Module}/`
**DTOs:** `Application/DTOs/{ContextPascal}/{App}/{Module}/`
**Validators:** `Application/Validators/{ContextPascal}/{App}/{Module}/`
**MCP:** `validate_conventions`, `scaffold_extension`

Rules:
- CQRS pattern with MediatR
- FluentValidation for all commands
- DTOs separate from domain entities
- Service interfaces in Application, implementations in Infrastructure

**Tenant isolation (BLOCKING — SECURITY CRITICAL):**

> **ROOT CAUSE (test-v4-005):** Services were generated WITHOUT TenantId filtering,
> creating cross-tenant data leakage on ALL 70+ CRUD endpoints.
> This is an OWASP A01 (Broken Access Control) vulnerability.

- ALL queries on tenant entities MUST include `.Where(x => x.TenantId == _currentUser.TenantId)`
- ALL entity creation MUST pass `_currentUser.TenantId` as first parameter to `Entity.Create(tenantId, ...)`
- NEVER use `new Entity { }` without `TenantId =` — always prefer factory method `Entity.Create()`
- NEVER use `Guid.Empty` as a placeholder for userId, tenantId, or any business identifier
- Service constructor MUST inject `ICurrentUserService _currentUser` to access TenantId

**MANDATORY Service Template (use as skeleton for ALL services):**

```csharp
public class {Entity}Service : I{Entity}Service
{
    private readonly IExtensionsDbContext _db;
    private readonly ICurrentUserService _currentUser;

    public {Entity}Service(IExtensionsDbContext db, ICurrentUserService currentUser)
    {
        _db = db;
        _currentUser = currentUser;
    }

    public async Task<PaginatedResult<{Entity}Response>> GetAllAsync(/* filters */, CancellationToken ct)
    {
        var tenantId = _currentUser.TenantId;
        var query = _db.{Entities}
            .Where(x => x.TenantId == tenantId)  // ← MANDATORY tenant filter
            .AsQueryable();
        // ... apply filters, pagination, projection to Response DTO
    }

    public async Task<{Entity}Response?> GetByIdAsync(Guid id, CancellationToken ct)
    {
        var tenantId = _currentUser.TenantId;
        var entity = await _db.{Entities}
            .Where(x => x.TenantId == tenantId)  // ← MANDATORY tenant filter
            .FirstOrDefaultAsync(x => x.Id == id, ct);
        // ...
    }

    public async Task<{Entity}Response> CreateAsync(Create{Entity}Dto dto, CancellationToken ct)
    {
        var entity = {Entity}.Create(_currentUser.TenantId, /* dto fields */);
        // ← TenantId as FIRST parameter
        _db.{Entities}.Add(entity);
        await _db.SaveChangesAsync(ct);
        return MapToResponse(entity);
    }

    public async Task<{Entity}Response> UpdateAsync(Guid id, Update{Entity}Dto dto, CancellationToken ct)
    {
        var entity = await _db.{Entities}
            .Where(x => x.TenantId == _currentUser.TenantId)  // ← MANDATORY
            .FirstOrDefaultAsync(x => x.Id == id, ct)
            ?? throw new NotFoundException(nameof({Entity}), id);
        // ... update fields
    }

    public async Task DeleteAsync(Guid id, CancellationToken ct)
    {
        var entity = await _db.{Entities}
            .Where(x => x.TenantId == _currentUser.TenantId)  // ← MANDATORY
            .FirstOrDefaultAsync(x => x.Id == id, ct)
            ?? throw new NotFoundException(nameof({Entity}), id);
        // ...
    }

    private static {Entity}Response MapToResponse({Entity} entity) => new()
    {
        // Map entity fields to response DTO
    };
}
```

**POST-CHECK after writing ANY service:** Grep the file for `TenantId`. If 0 occurrences → FAIL, rewrite with tenant filtering.

**Lifecycle-aware services:**
- Services operating on entities with a `lifeCycle` (status field) MUST validate entity state before mutations
- Example: `if (entity.Status == EmployeeStatus.Terminated) throw new BusinessException("Cannot update terminated employee")`
- Guard checks MUST match the `allowedTransitions` from the entity's lifecycle definition
- Delete/archive operations MUST respect terminal states (`isTerminal: true`)

**Validator completeness (BLOCKING):**
- For EVERY `Create{Entity}Validator`, a matching `Update{Entity}Validator` MUST exist
- ALL validators MUST be registered in `Application/DependencyInjection.cs`:
  `services.AddValidatorsFromAssemblyContaining<Create{Entity}Validator>()`
- DependencyInjection.cs MUST NOT be empty or contain only TODO comments
- After writing validators, VERIFY DI registration exists — if missing, add it immediately

**POST-CHECK after writing validators:**
```bash
# Count Create validators vs Update validators
CREATE_COUNT=$(find . -path "*/Validators/*" -name "Create*Validator.cs" | wc -l)
UPDATE_COUNT=$(find . -path "*/Validators/*" -name "Update*Validator.cs" | wc -l)
if [ "$CREATE_COUNT" -ne "$UPDATE_COUNT" ]; then
  echo "VALIDATOR MISMATCH: $CREATE_COUNT Create vs $UPDATE_COUNT Update → MUST be equal"
  # List missing UpdateValidators and CREATE them
fi
```

**Mapper pattern (DRY):**
- Each service MUST include a `private static {Entity}Response MapToResponse({Entity} entity)` method
- For complex mappings with related entities, use an extension method in `Application/Mappings/{Module}Mappings.cs`
- NEVER duplicate mapping logic between GetAll, GetById, Create, Update — always call MapToResponse

**FORBIDDEN:**
- Empty DependencyInjection.cs with `// TODO` placeholder
- CreateValidator without matching UpdateValidator
- Validators not registered in DI container
- Service query without TenantId filter (cross-tenant data leak)
- `new Entity { }` without TenantId assignment
- `Guid.Empty` as a business value in services or controllers
- Entity.Create() without tenantId as first parameter
- Duplicated mapping logic (entity→response) in multiple methods

---

## API

**Controllers:** `Api/Controllers/{ContextShort}/{App}/{Entity}Controller.cs`
**MCP:** `scaffold_routes`, `validate_security`

**Context-to-folder mapping (`{ContextShort}`):**

| NavRoute Prefix | Controller Folder |
|-----------------|-------------------|
| `platform.administration` | `Admin` |
| `platform.support` | `Support` |
| `business.*` | `Business` |
| `personal.*` | `User` |

**Rules:**
- `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint (NOT `[Authorize]`)
- Swagger XML documentation
- Consistent route patterns: `api/{context}/{app}/{module}`
- Return DTOs, never domain entities
- **ALL GetAll endpoints MUST accept `?search=` query parameter** (enables EntityLookup on frontend)
- **GetAll returns paginated results:** `PaginatedResult<T>` with items, totalCount, page, pageSize

**Controller coverage (BLOCKING):**
- EVERY entity in the module MUST have a controller with CRUD endpoints (GET list, GET by id, POST, PUT, DELETE)
- Reference/lookup entities (types, categories, statuses) MUST also have controllers — they are needed for dropdowns and configuration
- Count: `controllers created >= entities in module`. If fewer → FAIL

**Section-level controllers (CONDITIONAL: when `navSections[]` defined in feature.json):**
- File path: `Api/Controllers/{ContextShort}/{App}/{Section}Controller.cs`
- NavRoute attribute: `[NavRoute("{context}.{app}.{module}.{section}")]`
- Permission attribute: `[RequirePermission(Permissions.{Module}.{Section}.{Action})]`
- Route prefix: `api/{context}/{app}/{module}/{section}`
- Each section gets its own controller with CRUD endpoints scoped to the section

**FORBIDDEN:**
- `[Authorize]` without specific permission → use `[RequirePermission]`
- Returning domain entities directly
- Skipping controllers for reference/lookup entities

---

## Frontend (MCP-FIRST PROTOCOL)

> **CRITICAL:** Frontend code is generated via MCP tools, NOT written from scratch.

**Page hierarchy:** `src/pages/{ContextPascal}/{AppPascal}/{Module}/{Page}.tsx`
**FORBIDDEN:** `src/pages/{Module}/` (flat structure without Context/App)

**Execution sequence (IN ORDER):**
0. **Read back feature.json** (via `prd.source.featurePath`) — The PRD only carries the file list (structural skeleton). The behavioral specs (columnDefs, componentMapping, layout, rowActions, emptyState, tab structure, i18n key-value pairs, dashboard KPIs, state machine transitions) are ONLY in the original feature.json. Without this step, generated code will be generic and miss entity-specific UI details.
1. `mcp__smartstack__scaffold_api_client` → API client + types + React Query hook
2. `mcp__smartstack__scaffold_routes` (with `outputFormat: "clientRoutes"`) → route registry + route fragments
3. **Wire routes to App.tsx (detect pattern: `contextRoutes` array OR JSX `<Route>`):**
   - **Pattern A** (`contextRoutes: ContextRouteExtensions` in App.tsx): add to `contextRoutes.{context}[]` with RELATIVE paths → auto-injected into both standard + tenant trees
   - **Pattern B** (JSX `<Route>` in App.tsx): insert `<Route>` entries inside correct Layout wrapper (BOTH standard AND tenant-prefixed `/t/:slug/...` blocks)
4. Create pages using **`/ui-components` skill** — **LOAD the skill step files before creating any page.** This is MANDATORY for entity lists, grids, tables, dashboards, charts. The skill ensures CSS variables, EntityCard, SmartTable, loading/error/empty states, and responsive grid patterns. **Pages MUST use `t()` from `useTranslation()` for ALL visible text from the very first line of code.** Do NOT hardcode English strings with the intent of adding i18n later — the i18n keys are created in step 6 based on what keys the pages reference. Pattern: `t('{moduleLower}:actions.create', 'Create')`.
5. Create preferences hook: `use{Module}Preferences.ts`
6. Generate i18n (4 languages: fr, en, it, de) — see I18n section below for detailed rules
7. `npm run typecheck` MUST pass (BLOCKING)

**Components:**
- Lists: `SmartTable` + `SmartFilter` (NOT HTML `<table>`)
- Grids: `EntityCard` (NOT custom `<div>` cards)
- Detail: `EntityDetailCard`, `StatusBadge`, tab layout
- Forms: `SmartForm` with FluentValidation-backed fields — **MUST be full pages, NEVER modals**
- FK fields: `EntityLookup` for searchable entity selection (NEVER plain text for Guid FK)
- Dashboard: `StatCard`, Recharts components

**Per-page /ui-components validation (MANDATORY before commit):**
- After creating EACH page, verify it follows /ui-components patterns:
  1. List pages: uses `DataTable` or `SmartTable` (NOT raw `<table>`)
  2. List pages: uses `EntityCard` for card/grid views (NOT custom `<div>` cards)
  3. All pages: ALL colors use CSS variables (`bg-[var(--xxx)]`) — ZERO hardcoded Tailwind colors
  4. All pages: loading skeleton, error state with retry, empty state are present
  5. All pages: ALL visible text uses `t()` from `useTranslation()`
  6. Delete actions: use `ConfirmDialog` component (NOT `window.confirm()`)
  7. Status displays: use `StatusBadge` with CSS variable colors (NOT inline styled spans)
- **If ANY check fails: fix the page BEFORE moving to the next page**
- The POST-CHECKs in step-02 enforce these rules with BLOCKING severity

**Form pages (CRITICAL — ZERO modals/popups/drawers):**
- Create form: `EntityCreatePage.tsx` with route `/{module}/create`
- Edit form: `EntityEditPage.tsx` with route `/{module}/:id/edit`
- ALL forms are full pages with their own URL — NEVER Modal/Dialog/Drawer/Popup
- Back button with `navigate(-1)` on every form page
- Use React.lazy() + `<Suspense fallback={<PageLoader />}>` for ALL page imports (not just forms — see "Lazy loading" section above)
- **Form tests (MANDATORY):** Co-located `EntityCreatePage.test.tsx` and `EntityEditPage.test.tsx`
  → Cover: rendering, validation, submit, pre-fill (edit), navigation, error handling

**Layout wrapper mapping:**

| Context | Layout | Route path |
|---------|--------|------------|
| `platform.*` | `AdminLayout` | `/platform` |
| `business.*` | `BusinessLayout` | `/business` |
| `personal.*` | `UserLayout` | `/personal/myspace` |

**Route naming convention (CRITICAL — must match backend):**
- Frontend route paths MUST use **kebab-case** matching the backend API route convention
- Convention: `HumanResources` (C# namespace) → `human-resources` (URL segment)
- ALL multi-word route segments MUST contain hyphens: `time-management`, `human-resources`, `leave-balance`
- FORBIDDEN: `humanresources`, `timemanagement`, `leavebalance` (concatenated without hyphens)
- Frontend path `/business/human-resources/clients` matches API `api/business/human-resources/clients`
- Navigation seed data Route values MUST also use kebab-case
- The POST-CHECK in step-02 will BLOCK if frontend routes don't match backend kebab-case convention

**Section-level pages (CONDITIONAL: when `navSections[]` defined in feature.json):**
- Page file: `src/pages/{ContextPascal}/{AppPascal}/{Module}/{Section}Page.tsx`
- Route: nested as child of module route in App.tsx (e.g., `/business/human-resources/projects/timesheets`)
- Add to `contextRoutes.{context}[]` (Pattern A) or as nested `<Route>` (Pattern B)
- Each section page has its own route and permission check

**React Router mapping for sections:**
- `list` section → already the module's `index: true` route (NO separate `path: 'list'`)
- `detail` section → already the module's `path: ':id'` route (NO separate `path: 'detail'`)
- Other sections → `path: '{section-kebab}'` as child of module route
- FORBIDDEN frontend paths: `path: 'list'`, `path: 'detail'` — these are handled by the module's index and :id routes

**CSS:** Variables ONLY → `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`

**Form error handling (MANDATORY):**
- ALL SmartForm components MUST include `onError` callback for API errors
- Validation errors MUST be displayed inline next to the relevant field
- API errors (4xx, 5xx) MUST show a user-friendly error notification (toast/banner)
- Forms MUST preserve user input on error (no data loss on failed submit)

**Hook error handling pattern (MANDATORY):**
- ALL hooks MUST preserve server validation errors from axios responses
- Pattern: `catch (err) { if (axios.isAxiosError(err)) { return err.response?.data; } throw err; }`
- NEVER discard `err.response.data` — it contains field-level validation errors from FluentValidation
- Error messages MUST use i18n: `setError(t('{mod}:errors.saveFailed'))` — NEVER hardcoded English strings
- Toast/notification messages MUST also use i18n

**Service call pattern (MANDATORY — NO custom entity hooks):**

> **ROOT CAUSE (test-v4-014):** ralph-loop generated custom hooks (`useEmployees()`, `useProjects()`,
> `useTimeManagement()`) that wrapped services with `useState`/`useEffect`/`try-catch`.
> These hooks swallowed 401 errors with generic `catch → setError(string)`, creating a race condition
> with SmartStack's auth interceptor (`window.location.href = "/login"`).
> Result: token cleared + redirect + React re-render → cascade of unauthenticated requests.

- Pages MUST call API services **directly** in `useCallback` + `useEffect` — NOT through custom wrapper hooks
- FORBIDDEN: `useEmployees()`, `useProjects()`, `use{Entity}()` — custom hooks that wrap service calls with useState/useEffect/try-catch
- Only allowed custom hook: `use{Module}Preferences.ts` (preferences only, step 5)
- Pattern for pages:
  ```tsx
  // CORRECT — direct service call in page component
  const [data, setData] = useState<EmployeeResponseDto[]>([]);
  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const response = await employeeApi.getAll({ pageSize: 200 });
      setData(response.items);  // ← extract .items from PaginatedResult
    } catch { setError(t('employees:errors.loadFailed')); }
    finally { setLoading(false); }
  }, []);
  useEffect(() => { fetchData(); }, [fetchData]);
  ```
- **PaginatedResult<T>**: ALL service `getAll` methods MUST type responses as `PaginatedResult<T>` — extract `.items` in the page
- FORBIDDEN: `api.get<Employee[]>(url)` — use `api.get<PaginatedResult<EmployeeResponseDto>>(url)` then `.items`

**Lazy loading (MANDATORY — ALL pages, not just forms):**
- ALL page imports in App.tsx MUST use `React.lazy()` + `<Suspense fallback={<PageLoader />}>`
- FORBIDDEN: `lazy(() => import(...))` without a `<Suspense>` boundary in the rendering tree
- Pattern for App.tsx:
  ```tsx
  import { lazy, Suspense } from 'react';
  const EmployeesListPage = lazy(() => import('./pages/.../EmployeesListPage'));
  // In routes:
  { path: 'employees', element: <Suspense fallback={<PageLoader />}><EmployeesListPage /></Suspense> }
  ```

**Dependency verification (BLOCKING):**
- BEFORE writing any import, verify the package exists in `package.json`
- If package not present: run `npm install {package}` BEFORE writing the import
- Common missing packages: @tanstack/react-query, recharts, date-fns, react-router-dom
- After ALL frontend tasks: run `npm ls --depth=0` to detect any MISSING dependencies

**FORBIDDEN patterns (any = FAIL):**
```
import axios from 'axios'                    → use @/services/api/apiClient
<table>...</table>                           → use SmartTable
<div className="bg-blue-600">               → use bg-[var(--color-accent-600)]
<Route path="/business/app/mod" />           → MUST be nested inside Layout
Only fr/en translations                      → MUST have 4 languages
src/pages/{Module}/                          → MUST be src/pages/{Context}/{App}/{Module}/
Routes generated but NOT added to App.tsx    → MUST wire routes to App.tsx after scaffold_routes
Routes in standard block only               → MUST also add to /t/:slug/ tenant block (Pattern B only; Pattern A auto-handles)
Adding routes to clientRoutes[] instead of contextRoutes.{context}[] → MUST use contextRoutes for business/platform/personal routes
'00000000-0000-0000-0000-000000000000'       → use dynamic ID from auth context or route params
const api = axios.create(...)                → use @/services/api/apiClient (single instance)
useXxx with raw axios inside hooks           → hooks MUST use the shared apiClient from @/services/api
<input type="text" value={...employeeId}    → FK Guid fields MUST use EntityLookup (searchable select)
placeholder="Enter ID" / "Enter GUID"       → EntityLookup provides search-based entity selection
<Modal>/<Dialog>/<Drawer>/<Popup> for forms  → forms are FULL PAGES with own URL routes (/create, /:id/edit)
useState(showCreateModal/editDialog)         → navigate to form page, NEVER toggle modal visibility
<table>/<thead>/<tbody>                    → MUST use SmartTable or DataTable component
bg-blue-600 / bg-red-500 / etc.           → MUST use CSS variables: bg-[var(--color-accent-600)]
text-green-700 / text-gray-500 / etc.     → MUST use CSS variables: text-[var(--success-text)]
window.confirm() / confirm()              → MUST use ConfirmDialog component with i18n
Hardcoded English text in JSX (>Create<)  → MUST use t('{mod}:actions.create', 'Create')
Hardcoded error strings in hooks           → MUST use t('{mod}:errors.loadFailed') in all hooks
/business/humanresources/                  → MUST use kebab-case: /business/human-resources/
useEmployees() / use{Entity}() hooks       → pages call services DIRECTLY in useCallback (no hook wrapper)
api.get<Employee[]>(url)                   → api.get<PaginatedResult<Employee>>(url) then .items
lazy(() => import(...)) without Suspense   → ALL lazy pages MUST have <Suspense fallback={<PageLoader />}>
```

---

## I18n (MANDATORY for frontend)

> **CRITICAL:** Every frontend module MUST have 4 translation files. Missing i18n = broken UI (untranslated labels, empty buttons).

**File path:** `src/i18n/locales/{lang}/{moduleLower}.json` (4 files: fr, en, it, de)

**JSON key structure (identical across all 4 languages):**
```json
{
  "actions": { "create": "...", "edit": "...", "delete": "...", "save": "...", "cancel": "...", "search": "...", "filter": "...", "export": "...", "refresh": "..." },
  "labels": { "title": "...", "description": "...", "status": "...", "createdAt": "...", "updatedAt": "..." },
  "columns": { "name": "...", "status": "...", "date": "...", "actions": "..." },
  "form": { "name": "...", "description": "...", "submit": "...", "required": "..." },
  "errors": { "loadFailed": "...", "saveFailed": "...", "deleteFailed": "...", "notFound": "..." },
  "validation": { "required": "...", "minLength": "...", "maxLength": "...", "invalid": "..." },
  "messages": { "created": "...", "updated": "...", "deleted": "...", "confirmDelete": "..." },
  "empty": { "title": "...", "description": "...", "action": "..." }
}
```

**Usage pattern in TSX:** `t('{moduleLower}:actions.create', 'Create')` — ALWAYS namespace prefix + fallback value.

**Rules:**
- 4 JSON files per module: fr, en, it, de — ALL mandatory (not just fr/en)
- File structure: `src/i18n/locales/{lang}/{moduleLower}.json` — NEVER inline translations in index.ts
- Identical key structures across all languages — same keys, translated values
- All UI labels, validation messages, button text, empty states, error messages
- Depends on frontend page completion (pages define which keys are needed)
- Add entity-specific keys under `labels`, `columns`, `form` for each entity field
- **Hooks MUST also use i18n** — error messages like "Failed to load" MUST use `t('{mod}:errors.loadFailed')`
- **FORBIDDEN:** Inline translation objects in `i18n/index.ts` — use separate JSON files per language per module
- Reference `smartstack-frontend.md` for the complete template

---

## Test (BLOCKING)

**MCP:** `scaffold_tests`, `analyze_test_coverage`

> **CRITICAL:** Test generation is a MANDATORY category. If the PRD has no test tasks,
> the category completeness check (step-01 section 4b) will inject a guardrail task.
> Test projects MUST be created as the FIRST action in this category — before generating any test files.

**Execution sequence:**

1. **Ensure test projects exist (FIRST — before any test generation):**
   ```bash
   # Unit test project
   UNIT_TEST_PROJECT="tests/${PROJECT_NAME}.Tests.Unit"
   if [ ! -d "$UNIT_TEST_PROJECT" ]; then
     dotnet new xunit -n "${PROJECT_NAME}.Tests.Unit" -o "$UNIT_TEST_PROJECT"
     dotnet add "$UNIT_TEST_PROJECT" package Moq
     dotnet add "$UNIT_TEST_PROJECT" package FluentAssertions
     for proj in src/*/*.csproj; do dotnet add "$UNIT_TEST_PROJECT" reference "$proj"; done
     dotnet sln add "$UNIT_TEST_PROJECT/${PROJECT_NAME}.Tests.Unit.csproj"
   fi

   # Integration test project (SQL Server LocalDB)
   INT_TEST_PROJECT="tests/${PROJECT_NAME}.Tests.Integration"
   if [ ! -d "$INT_TEST_PROJECT" ]; then
     dotnet new xunit -n "${PROJECT_NAME}.Tests.Integration" -o "$INT_TEST_PROJECT"
     dotnet add "$INT_TEST_PROJECT" package FluentAssertions
     dotnet add "$INT_TEST_PROJECT" package Microsoft.AspNetCore.Mvc.Testing
     dotnet add "$INT_TEST_PROJECT" package Microsoft.EntityFrameworkCore.SqlServer
     dotnet add "$INT_TEST_PROJECT" package Microsoft.Data.SqlClient
     dotnet add "$INT_TEST_PROJECT" package Respawn
     for proj in src/*/*.csproj; do dotnet add "$INT_TEST_PROJECT" reference "$proj"; done
     dotnet sln add "$INT_TEST_PROJECT/${PROJECT_NAME}.Tests.Integration.csproj"
   fi
   ```

2. **Generate test infrastructure** (FIRST, before entity tests):
   - `scaffold_tests` with target="infrastructure" → generates DatabaseFixture, DatabaseCollection, SmartStackTestFactory, TestAuthHandler, TestTenantService, IntegrationTestBase, TestDataSeeder
   - These use **SQL Server LocalDB** (not SQLite) → real migrations, real LINQ→SQL

3. **Generate entity tests via MCP** (NOT manually):
   - Domain: `scaffold_tests` with target="entity"
   - Service: `scaffold_tests` with target="service"
   - Controller: `scaffold_tests` with target="controller", testTypes=["integration"]
   - Repository: `scaffold_tests` with target="repository", testTypes=["integration"]
   - Security: `scaffold_tests` with target="controller", testTypes=["security"]

4. **Run tests (BLOCKING):**
   ```bash
   dotnet build --no-restore
   dotnet test --no-build --verbosity normal
   ```
   Integration tests run against **real SQL Server LocalDB** via DatabaseFixture:
   - Migrations are applied automatically (validates migration chain)
   - Queries execute real T-SQL (validates LINQ→SQL translation)
   - Multi-tenant isolation is enforced via global query filters on SQL Server
   - Respawn resets data between tests (~50ms)

5. **Fix loop:** If tests fail → analyze → fix code (NOT tests) → rebuild → retest → loop until 100% pass

6. **Coverage check:** `analyze_test_coverage` → must be >= 80%

**Completion criteria (ALL required):**
- Unit test project exists
- Integration test project exists with SQL Server LocalDB infrastructure
- Tests generated via MCP (scaffold_tests)
- `dotnet test` exits 0 (all pass — including integration tests on SQL Server)
- Coverage >= 80%
- No `[Fact(Skip = "...")]`

**Frontend test completeness (BLOCKING):**

After all frontend pages are generated, verify test coverage:

```bash
# Count page files vs test files
PAGE_COUNT=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null | wc -l)
TEST_COUNT=$(find src/pages/ -name "*.test.tsx" 2>/dev/null | wc -l)

if [ "$PAGE_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
  echo "BLOCKING: Category D — No .test.tsx files found in src/pages/"
  echo "Every module with pages MUST have at least one test file"
  echo "Pages found: $PAGE_COUNT, Tests found: 0"
  exit 1
fi

# Minimum ratio: at least 1 test per 3 pages
MIN_TESTS=$(( (PAGE_COUNT + 2) / 3 ))
if [ "$TEST_COUNT" -lt "$MIN_TESTS" ]; then
  echo "WARNING: Low test coverage — $TEST_COUNT tests for $PAGE_COUNT pages (minimum: $MIN_TESTS)"
fi
```

---

## Validation (FINAL — BLOCKING)

**Execution sequence:**

1. `dotnet clean && dotnet restore && dotnet build` → MUST pass

1a. **DB migration validation (BLOCKING — if infrastructure tasks exist):**
   ```bash
   INFRA_PROJECT=$(ls src/*Infrastructure*/*.csproj 2>/dev/null | head -1)
   API_PROJECT=$(ls src/*Api*/*.csproj 2>/dev/null | head -1)
   if [ -n "$INFRA_PROJECT" ] && [ -n "$API_PROJECT" ]; then
     # Check no pending model changes
     dotnet ef migrations has-pending-model-changes \
       --project "$INFRA_PROJECT" --startup-project "$API_PROJECT"
     # Apply migrations on fresh SQL Server LocalDB
     DB_NAME="SmartStack_Validation_$(date +%s)"
     CONN_STRING="Server=(localdb)\\MSSQLLocalDB;Database=$DB_NAME;Integrated Security=true;TrustServerCertificate=true;Connect Timeout=120;"
     dotnet ef database update --connection "$CONN_STRING" \
       --project "$INFRA_PROJECT" --startup-project "$API_PROJECT"
     # Cleanup
     sqlcmd -S "(localdb)\MSSQLLocalDB" -Q "IF DB_ID('$DB_NAME') IS NOT NULL BEGIN ALTER DATABASE [$DB_NAME] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [$DB_NAME]; END" 2>/dev/null
   fi
   ```
   If FAIL → migration is broken → fix before continuing

1b. **Runtime assembly validation (BLOCKING):**
   ```bash
   API_PROJECT=$(ls src/*Api*/*.csproj 2>/dev/null | head -1)
   if [ -n "$API_PROJECT" ]; then
     dotnet run --project "$API_PROJECT" --urls "http://localhost:5098" > /tmp/ralph-validation-startup.log 2>&1 &
     VAL_PID=$!

     # Wait up to 10 seconds
     STARTED=false
     for i in $(seq 1 10); do
       if ! kill -0 $VAL_PID 2>/dev/null; then
         # Process crashed
         CRASH_LOG=$(cat /tmp/ralph-validation-startup.log 2>/dev/null)
         echo "VALIDATION FAILED: API startup crash"
         echo "$CRASH_LOG"
         break
       fi
       HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5098/health 2>/dev/null)
       if [ "$HTTP_CODE" != "000" ]; then
         STARTED=true
         break
       fi
       sleep 1
     done

     kill $VAL_PID 2>/dev/null
     wait $VAL_PID 2>/dev/null
     rm -f /tmp/ralph-validation-startup.log

     if [ "$STARTED" != "true" ]; then
       # Classify error per references/error-classification.md
       # Provide actionable fix command
       # DO NOT proceed to tests — fix the startup issue first
       echo "VALIDATION FAILED: Runtime assembly error detected"
     fi
   fi
   ```

2. `dotnet test` (full suite) → MUST pass
3. `mcp__smartstack__validate_conventions` → 0 errors
4. Generate validation report in progress.txt

**Completion criteria:**
- Build exit code 0
- DB migrations apply on SQL Server LocalDB (no pending model changes)
- Runtime startup check passes (no assembly errors)
- Test exit code 0 (unit tests + integration tests on real SQL Server)
- MCP validation 0 errors
- Production ready = true
