---
name: step-04-examine
description: MCP validation, build verification, seed data check, acceptance criteria
model: opus
prev_step: steps/step-03-execute.md
next_step: steps/step-05-deep-review.md
---

# Step 4: eXamine

> **This is the X in APEX.** Mandatory automated examination of all generated code.

**Goal:** Verify everything works. MCP conventions, build, seed data, acceptance criteria.

---

## 1. MCP Convention Validation

```
Call: mcp__smartstack__validate_conventions

Expected: 0 errors
If errors found:
  → Fix each error (use appropriate skill/MCP)
  → Re-validate until 0 errors
```

---

## 2. EF Core Migration Check (if needs_migration)

```
Call: mcp__smartstack__check_migrations

Verify:
- Migration exists and is applied
- No pending model changes
- ModelSnapshot matches
```

---

## 3. Frontend Route Validation (if frontend modified)

```
Call: mcp__smartstack__validate_frontend_routes

Verify:
- Routes nested inside correct Layout wrapper
- Route paths match controller patterns
- No orphan routes
```

### 3b. Frontend Page Completeness (BLOCKING)

> **ROOT CAUSE (test-v4-015):** Frontend generation created List + Detail pages but SKIPPED
> Create + Edit pages. ListPage buttons navigated to /create and /:id/edit → white screen / 404.
> Tests were also missing. This section catches that failure mode.

```bash
PAGE_DIR=$(find . -path "*/src/pages" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
APP_TSX=$(find . -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)

if [ -n "$PAGE_DIR" ]; then
  # Check each module's ListPage for dead navigation links
  LIST_PAGES=$(find "$PAGE_DIR" -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
  FAIL=false
  for LIST_PAGE in $LIST_PAGES; do
    LP_DIR=$(dirname "$LIST_PAGE")
    MOD=$(basename "$LP_DIR")

    HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null)
    HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit" "$LIST_PAGE" 2>/dev/null)

    CREATE_PAGE=$(find "$LP_DIR" -name "*CreatePage.tsx" 2>/dev/null | head -1)
    EDIT_PAGE=$(find "$LP_DIR" -name "*EditPage.tsx" 2>/dev/null | head -1)

    if [ -n "$HAS_CREATE_NAV" ] && [ -z "$CREATE_PAGE" ]; then
      echo "BLOCKING: $MOD ListPage navigates to /create but CreatePage does NOT exist → dead link"
      FAIL=true
    fi
    if [ -n "$HAS_EDIT_NAV" ] && [ -z "$EDIT_PAGE" ]; then
      echo "BLOCKING: $MOD ListPage navigates to /:id/edit but EditPage does NOT exist → dead link"
      FAIL=true
    fi
  done

  # Check route wiring in App.tsx
  if [ -n "$APP_TSX" ]; then
    CREATE_PAGES=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null | grep -v test)
    EDIT_PAGES=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null | grep -v test)
    if [ -n "$CREATE_PAGES" ] && ! grep -qP "path:.*create" "$APP_TSX"; then
      echo "BLOCKING: CreatePage exists but no 'create' route in App.tsx"
      FAIL=true
    fi
    if [ -n "$EDIT_PAGES" ] && ! grep -qP "path:.*:id/edit" "$APP_TSX"; then
      echo "BLOCKING: EditPage exists but no ':id/edit' route in App.tsx"
      FAIL=true
    fi
  fi

  # Check test files exist for form pages
  FORM_PAGES=$(find "$PAGE_DIR" -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
  if [ -n "$FORM_PAGES" ]; then
    for FP in $FORM_PAGES; do
      TF="${FP%.tsx}.test.tsx"
      if [ ! -f "$TF" ]; then
        echo "BLOCKING: Form page missing test: $FP → expected $TF"
        FAIL=true
      fi
    done
  fi

  # Check at least SOME tests exist when pages exist
  PAGE_COUNT=$(find "$PAGE_DIR" -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null | grep -v node_modules | wc -l)
  TEST_COUNT=$(find "$PAGE_DIR" -name "*.test.tsx" 2>/dev/null | grep -v node_modules | wc -l)
  if [ "$PAGE_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
    echo "BLOCKING: $PAGE_COUNT pages but ZERO test files"
    FAIL=true
  fi

  if [ "$FAIL" = true ]; then
    echo "Fix: Go back to step-03 and generate missing pages/tests/routes"
    # MUST fix before proceeding
  fi
fi
```

**If ANY check fails → return to step-03 to generate the missing pages, tests, or routes.**

---

## 4. Build Verification

```bash
# Backend
dotnet clean && dotnet restore && dotnet build
# Note: WSL bin\Debug cleanup handled by PostToolUse hook (wsl-dotnet-cleanup.sh)

# Frontend (if applicable)
npm run typecheck
```

**BLOCKING:** Both must pass. If failure, classify error per `references/error-classification.md` (categories A-F).

---

## 5. Migration Validation (if needs_migration)

```bash
INFRA_PROJECT=$(ls src/*Infrastructure*/*.csproj 2>/dev/null | head -1)
API_PROJECT=$(ls src/*Api*/*.csproj 2>/dev/null | head -1)

# Pending model changes check (BLOCKING if pending)
dotnet ef migrations has-pending-model-changes \
  --project "$INFRA_PROJECT" \
  --startup-project "$API_PROJECT"

# Migration application test (SQL Server LocalDB)
DB_NAME="SmartStack_Apex_Examine_$(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"

# Integration tests on real SQL Server (if project exists)
INT_TEST_PROJECT=$(ls tests/*Tests.Integration*/*.csproj 2>/dev/null | head -1)
if [ -n "$INT_TEST_PROJECT" ]; then
  dotnet test "$INT_TEST_PROJECT" --no-build --verbosity normal
fi

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

**BLOCKING** if migration fails on SQL Server (common: SQLite-only syntax, column type mismatches, missing FK targets).

---

## 6. Seed Data Completeness Check (if needs_seed_data)

| File | Checks |
|------|--------|
| NavigationApplicationSeedData | MUST be first, random GUID via Guid.NewGuid() (resolved by Code at runtime), 4 lang translations |
| ApplicationRolesSeedData | 4 roles (admin, manager, contributor, viewer), random GUIDs, references NavigationApplicationSeedData.ApplicationId |
| NavigationModuleSeedData | Random GUIDs (`Guid.NewGuid()`), 4 languages (fr, en, it, de), GetModuleEntry + GetTranslationEntries |
| ↳ Section methods (same file) | (conditional: if sections exist) GetSectionEntries + GetSectionTranslationEntries, 4 languages, random GUIDs, query actual module from DB for FK |
| ↳ Resource methods (same file) | (conditional: if resources exist) GetResourceEntries + resource translations, 4 languages, random GUIDs, query actual section from DB for FK |
| Permissions.cs | Static constants class with `public static class {Module} { Read, Create, Update, Delete }`, paths match PermissionsSeedData |
| PermissionsSeedData | MCP generate_permissions used, paths match Permissions.cs, wildcard + CRUD |
| RolesSeedData | Admin=wildcard(*), Manager=CRU, Contributor=CR, Viewer=R. Role-permission entries reference permission paths from PermissionsSeedData |
| IClientSeedDataProvider | 4 Seed methods (Navigation, Roles, Permissions, RolePermissions), idempotent, factory methods, SaveChanges per group |
| DI Registration | `services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()` |

---

## 6b. BLOCKING POST-CHECKs

> **MCP FIRST:** Run `validate_conventions`, `validate_security`, and `validate_frontend_routes` before bash checks.
> These 3 MCP tools cover ~10 additional checks automatically.

Load and execute all checks from `references/post-checks.md` (6 security defense-in-depth + 44 convention checks).

**If ANY POST-CHECK fails → fix in step-03, re-validate.**

---

## 7. Acceptance Criteria POST-CHECK

For each AC inferred in step-01:

```
AC1: {criterion} → PASS / FAIL (evidence: {file:line or test})
AC2: {criterion} → PASS / FAIL (evidence: {file:line or test})
...
```

**All ACs must PASS. If any FAIL, go back to step-03 to fix.**

---

## 8. eXamine Summary

```
**APEX SmartStack - eXamine Complete**

| Layer | Status | Details |
|-------|--------|---------|
| Layer 0 — Domain + Infrastructure | PASS / N/A | Entities, EF configs, migration, build gate |
| Layer 1 — Seed Data | PASS / N/A | Navigation, permissions, roles, provider, build gate |
| Layer 2 — Backend | PASS / N/A | Services, controllers, build gate, inline tests |
| Layer 3 — Frontend | PASS / N/A | Pages, i18n, docs, compliance gate, inline tests |
| Layer 4 — DevData | PASS / N/A / SKIPPED | Optional test data |

| Category | Checks | Status |
|----------|--------|--------|
| MCP conventions | validate_conventions | PASS (0 errors) |
| Build | dotnet build + npm typecheck | PASS |
| EF Core | migrations, pending changes, SQL Server apply | PASS / N/A |
| DB tests | Integration tests on SQL Server | PASS / N/A |
| Frontend | routes, lazy loading, forms as pages, i18n (4 langs + keys) | PASS / N/A |
| Security | TenantId filter, RequirePermission, no Guid.Empty, no !.Value | PASS / N/A |
| Seed data | completeness, random GUIDs (Guid.NewGuid()), no ContextId/RoleId constants | PASS / N/A |
| Code quality | PaginatedResult, EntityLookup (no FK select/input), CSS vars, search param | PASS / N/A |
| Migration completeness | ModelSnapshot covers ALL DbSet entities (POST-CHECK 36, 40) | PASS / N/A |
| NavRoute kebab-case | NavRoute + permissions use kebab-case (POST-CHECK 32) | PASS / N/A |
| DateOnly DTOs | Date fields use DateOnly not string (POST-CHECK 39) | PASS / N/A |
| I18n registration | Namespaces registered in i18n config (POST-CHECK 37) | PASS / N/A |
| Validators DI | FluentValidation registered in DI (POST-CHECK 38) | PASS / N/A |
| Route/NavRoute conflict | No [Route] alongside [NavRoute] on controllers (POST-CHECK 41) | PASS / N/A |
| Role-permission matrix | Admin=wildcard, Manager=CRU, Contributor=CR, Viewer=R (POST-CHECK 42) | PASS / N/A |
| PermissionAction enum | No Enum.Parse, only typed enum values 0-10 (POST-CHECK 43) | PASS / N/A |
| Navigation translations | 4 langs per level, section/resource translations present (POST-CHECK 44) | PASS / N/A |
| Inline tests (Layer 2) | Backend unit + integration tests | PASS / N/A |
| Inline tests (Layer 3) | Frontend form tests | PASS / N/A |
| POST-CHECKs | 3 MCP tools + 50 bash checks (6 security + 44 convention) | PASS / N/A |
| Acceptance criteria | AC1..ACn | {X}/{Y} PASS |
```

---

## 9. Save Output (if save_mode)

Write to `{output_dir}/04-examine.md` with validation results.

---

## 10. Route to Next Step — FINAL TEST SWEEP

> **Note:** Backend tests (Layer 2) and frontend tests (Layer 3) have already been scaffolded and run inline.
> Step-07 "Final Test Sweep" handles: security tests, coverage analysis, cross-layer scenarios, and remaining failures.

```
IF examine_mode (-x flag):
  → Load steps/step-05-deep-review.md (adversarial review)
  → Then step-06 (resolve) if BLOCKING findings
  → Then step-07 (Final Test Sweep) — ALWAYS

ELSE:
  → Load steps/step-07-tests.md — IMMEDIATELY after eXamine completes
```

**BLOCKING RULE:** The APEX workflow is NOT complete until steps 07-08 execute.
The success criteria require: "Tests: 100% pass, >= 80% coverage" — step-07 verifies coverage and adds security tests.

**NEXT ACTION:** Load `steps/step-07-tests.md` now.

---

## 11. PRD Final Reconciliation (delegate mode only)

> **After all validation passes**, ensure the PRD file reflects the completed state.

```
IF delegate_mode:
  Read {delegate_prd_path}

  For ALL tasks belonging to this module:
    IF task was executed by apex AND all checks passed:
      Verify task.status == 'completed'
    IF task was NOT in scope (different module):
      Leave unchanged

  Set prd.updated_at = new Date().toISOString()

  If ALL module tasks completed:
    Set prd.status = 'completed' (if single module)
    OR leave prd.status = 'in_progress' (if multi-module, other modules remain)

  Write back to {delegate_prd_path}
```
