# 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. `dotnet ef database update --project src/{Infra}.csproj --startup-project src/{Api}.csproj`
4. `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.

**Rules:**
- NavigationModuleSeedData.cs: deterministic GUIDs (SHA256), 4 languages (fr, en, it, de)
- PermissionsSeedData.cs: MCP `generate_permissions` first, fallback to template
- RolesSeedData.cs: context-based role mapping (Admin=CRUD, Manager=CRU, Contributor=CR, Viewer=R)
- SeedConstants.cs: shared deterministic GUIDs
- IClientSeedDataProvider: SeedNavigationAsync + SeedPermissionsAsync + SeedRolePermissionsAsync
- DI: `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()`

**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

**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`

---

## 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<PagedResult<{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

**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

**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):**
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** → insert `<Route>` entries inside correct Layout wrapper (BOTH standard AND tenant-prefixed `/t/:slug/...` blocks)
4. Create pages using SmartStack components
5. Create preferences hook: `use{Module}Preferences.ts`
6. Generate i18n (4 languages: fr, en, it, de)
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
- Dashboard: `StatCard`, Recharts components

**Layout wrapper mapping:**

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

**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)

**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
'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
```

---

## I18n

**Rules:**
- 4 JSON files: fr, en, it, de
- Identical key structures across all languages
- All UI labels, validation messages, button text, empty states
- Depends on frontend task completion

---

## 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 = "...")]`

---

## 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
