---
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
# Cleanup corrupted EF Core design-time artifacts (Roslyn BuildHost bug on Windows)
for d in src/*/bin?Debug; do [ -d "$d" ] && echo "Removing corrupted artifact: $d" && rm -rf "$d"; done

# Backend
dotnet clean && dotnet restore && dotnet build

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

**BLOCKING:** Both must pass. If failure, classify error per `references/error-classification.md` before attempting fix:
- Category A (missing package) → `dotnet add package` → rebuild
- Category B (assembly conflict) → resolve version → rebuild
- Category C (DI missing) → fix DI registration → rebuild
- Category D (migration broken) → fix migration → rebuild
- Category E (config) → fix config → rebuild
- Category F (source code) → fix code → rebuild

---

## 5. Database & Migration Validation (if needs_migration)

### 5a. Pending Model Changes Check

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

dotnet ef migrations has-pending-model-changes \
  --project "$INFRA_PROJECT" \
  --startup-project "$API_PROJECT"
```

**BLOCKING** if pending changes detected → migration is missing.

### 5b. Migration Application Test (SQL Server LocalDB)

```bash
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"
```

**BLOCKING** if migration fails on SQL Server. Common issues:
- SQLite-only syntax in migrations (fix: regenerate migration)
- Column type mismatches (fix: update EF configuration)
- Missing foreign key targets (fix: reorder migrations)

### 5c. Integration Tests on Real SQL Server

```bash
# Integration tests use DatabaseFixture → real SQL Server LocalDB
# This validates: LINQ→SQL, multi-tenant isolation, soft delete, EF configs
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
```

Tests running against SQL Server catch issues that SQLite misses:
- Case sensitivity in string comparisons
- Date/time function differences
- IDENTITY vs AUTOINCREMENT behavior
- Global query filter translation to T-SQL

### 5d. Cleanup

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

---

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

| File | Checks |
|------|--------|
| NavigationApplicationSeedData | MUST be first, deterministic GUID for application (NOT for context — context is looked up by code), 4 lang translations |
| ApplicationRolesSeedData | 4 roles (admin, manager, contributor, viewer), deterministic GUIDs, references NavigationApplicationSeedData.ApplicationId |
| NavigationModuleSeedData | Deterministic GUIDs (SHA256), 4 languages, GetModuleEntry + GetTranslationEntries |
| 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

Load and execute all checks from `references/post-checks.md`.

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

| 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, deterministic GUIDs, no ContextId/RoleId constants | PASS / N/A |
| Code quality | PaginatedResult, EntityLookup (no FK select/input), CSS vars, search param | PASS / N/A |
| POST-CHECKs | 43 checks from references/post-checks.md | 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 — TESTS ARE MANDATORY

> **CRITICAL — DO NOT STOP HERE.**
> Step-04 (eXamine) is NOT the final step. Tests MUST be scaffolded and run.
> Completing APEX without tests violates the methodology — skip is FORBIDDEN.

```
IF examine_mode (-x flag):
  → Load steps/step-05-deep-review.md (adversarial review)
  → Then step-06 (resolve) if BLOCKING findings
  → Then step-07 (tests) — 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" — this is impossible without step-07.

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