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

**ULTRA THINK about completeness. Did ALL new sections/entities get their navigation entries, permissions, and roles? Cross-check the scope from step-00 against what was actually generated in Layer 1.**

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

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

---

## 0. Task Progress

```
TaskUpdate(taskId: progress_tracker_id, description: "Module: {module_code}. Current: step-04 (eXamine)", activeForm: "Validating conventions")
TaskUpdate(taskId: examine_task_id, status: "in_progress")
```

---

## 1. MCP Convention Validation

```
Call: mcp__smartstack__validate_conventions
Expected: 0 errors. Fix and re-validate if any found.
```

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

```
Call: mcp__smartstack__check_migrations
Verify: Migration exists, no pending changes, ModelSnapshot matches.
```

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

```
Call: mcp__smartstack__validate_frontend_routes

Verify:
- Routes nested in correct Layout wrapper
- Route paths match controller patterns, include module segment
- No orphan routes
- Compare frontend routes (componentRegistry.generated.ts or App.tsx) against backend NavigationSeedData routes
```

> **Page completeness:** POST-CHECKs C4, C5, C6 (dead links, missing Create/Edit, missing tests) executed in section 6b.
> **If ANY check fails → return to step-03.**

---

## 4. Build Verification

```bash
dotnet clean && dotnet restore && dotnet build
npm run typecheck  # if applicable
```

Both must pass. Classify errors per `references/error-classification.md` (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)

# Check pending model changes
dotnet ef migrations has-pending-model-changes --project "$INFRA_PROJECT" --startup-project "$API_PROJECT"

# Apply to SQL Server LocalDB
DB_NAME="SmartStack_Apex_Examine_$(date +%s)"
dotnet ef database update --connection "Server=(localdb)\\MSSQLLocalDB;Database=$DB_NAME;Integrated Security=true;TrustServerCertificate=true;Connect Timeout=120;" --project "$INFRA_PROJECT" --startup-project "$API_PROJECT"

# Run integration tests if present
INT_TEST_PROJECT=$(ls tests/*Tests.Integration*/*.csproj 2>/dev/null | head -1)
[ -n "$INT_TEST_PROJECT" ] && dotnet test "$INT_TEST_PROJECT" --no-build --verbosity normal

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

If migration fails (SQLite-only syntax, type mismatches, missing FK targets), fix the DbContext or migration first.

---

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

> **CRITICAL — Cross-check against {sections} from step-00.** Every section in scope MUST have navigation, permissions, and roles. This is the most common miss when UPDATING an existing module.

| File | Checks |
|------|--------|
| NavigationApplicationSeedData | 4 lang translations |
| ApplicationRolesSeedData | 4 roles, references NavigationApplicationSeedData.ApplicationId |
| NavigationModuleSeedData | 4 languages, GetModuleEntry + translations |
| ↳ Section methods | GetSectionEntries + translations, 4 languages, random GUIDs |
| ↳ Resource methods | GetResourceEntries + translations, 4 languages, random GUIDs |
| Permissions.cs | Static class with Read/Create/Update/Delete constants, match PermissionsSeedData |
| PermissionsSeedData | MCP generate_permissions, paths match Permissions.cs |
| RolesSeedData | Admin=wildcard(*), Manager=CRU, Contributor=CR, Viewer=R |
| IClientSeedDataProvider | 4 Seed methods, idempotent, SaveChanges per group |
| DI Registration | `services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()` |

### 6a. Section Coverage Matrix (MANDATORY for updates)

```
For EACH section in {sections} from step-00:
  Read NavigationModuleSeedData.cs → has section entry?       YES / NO
  Read NavigationModuleSeedData.cs → has translations (4)?    YES / NO
  Read NavigationModuleSeedData.cs → has route?               YES / NO
  Read Permissions.cs → has section permissions?              YES / NO
  Read PermissionsSeedData.cs → has HasData() entries?        YES / NO
  Read RolesSeedData.cs → has role-permission mappings?       YES / NO

IF ANY cell = NO → BLOCKING — return to step-03b to fix
```

---

## 6b. POST-CHECKs

**MCP FIRST:** Run `validate_conventions`, `validate_security`, `validate_frontend_routes` (covers ~10 checks).

Execute all checks from `references/post-checks.md`. If any fails, fix in step-03 and re-validate.

### 6c. SeedDataProvider Content Verification (BLOCKING)

> **LESSON LEARNED (audit ba-003):** SeedDataProvider existed and had 4 methods (passed C23),
> but all methods were `return Task.CompletedTask` — stubs generated when `core-seed-data.md`
> was evicted from context by compression. Result: empty menu, 0 applications visible.

```javascript
// Find the SeedDataProvider file
const providerFiles = Glob("**/Seeding/*SeedDataProvider.cs");
for (const providerFile of providerFiles) {
  const content = readFile(providerFile);

  // Check 1: No stub methods — each Seed*Async must have real implementation
  const methods = ['SeedNavigationAsync', 'SeedRolesAsync', 'SeedPermissionsAsync', 'SeedRolePermissionsAsync'];
  for (const method of methods) {
    const methodRegex = new RegExp(`${method}[^{]*\\{([^}]*)\\}`, 's');
    const match = content.match(methodRegex);
    if (match) {
      const body = match[1].trim();
      if (body === 'return Task.CompletedTask;' || body === '' || body === 'return;') {
        BLOCKING_ERROR(`${providerFile}: ${method} is a STUB (${body || 'empty body'}). ` +
          `Re-read references/core-seed-data.md Section 6 and generate real implementation. ` +
          `Stub seed methods cause EMPTY MENU — 0 applications visible in UI.`);
      }
    }
  }

  // Check 2: Must contain actual entity creation calls
  if (!content.includes('.Add(') && !content.includes('.AddRange(')) {
    BLOCKING_ERROR(`${providerFile}: No .Add() or .AddRange() calls found. ` +
      `SeedDataProvider must actually insert seed data, not just return.`);
  }
}
```

---

## 6d. PRD Compliance Verification (delegate mode only)

> **Run ONLY when `delegate_mode` is true AND companion spec files exist.**
> This checks that generated code matches PRD specifications — not just technical quality.
> The PRD companion files are the AUTHORITATIVE source for columns, actions, labels, and permissions.

```javascript
if (delegate_mode) {
  const specsDir = path.dirname(delegate_prd_path);
  const prd = readJSON(delegate_prd_path);

  const screensPath = path.join(specsDir, prd.specificationFiles?.screens);
  const permsPath = path.join(specsDir, prd.specificationFiles?.permissions);
  const screens = fileExists(screensPath) ? readJSON(screensPath) : null;
  const perms = fileExists(permsPath) ? readJSON(permsPath) : null;

  if (screens) {
    // ──── PC-1 (BLOCKING): Column Completeness ────
    // Every column defined in screens.json SmartTable resources MUST exist in the generated ListPage.
    for (const section of screens.sections) {
      for (const resource of (section.resources || [])) {
        if (resource.type !== 'SmartTable' || !resource.columns) continue;
        const entityName = resource.entity;
        // Find the corresponding ListPage
        const listPages = Glob(`src/pages/**/${entityName}*ListPage.tsx`);
        if (listPages.length === 0) {
          BLOCKING(`PC-1: No ListPage found for entity ${entityName} (section: ${section.code})`);
          continue;
        }
        const pageContent = readFile(listPages[0]);
        const missingCols = [];
        for (const col of resource.columns) {
          // Check for the column field name in the page content
          if (!pageContent.includes(col.field)) {
            missingCols.push(`${col.field} (${col.format})`);
          }
        }
        if (missingCols.length > 0) {
          BLOCKING(`PC-1: ${listPages[0]} missing ${missingCols.length}/${resource.columns.length} columns: ${missingCols.join(', ')}. ` +
            `Fix: re-invoke Skill("ui-components") with the PRD SPECIFICATION block containing ALL columns.`);
        }
      }
    }

    // ──── PC-2 (BLOCKING): Action Button Completeness ────
    // Actions defined in screens.json MUST have corresponding handlers in the generated pages.
    for (const section of screens.sections) {
      for (const resource of (section.resources || [])) {
        if (!resource.actions || resource.actions.length === 0) continue;
        const entityName = resource.entity;
        const pages = Glob(`src/pages/**/${entityName}*Page.tsx`);
        const allContent = pages.map(p => readFile(p)).join('\n');

        for (const action of resource.actions) {
          let found = false;
          switch (action) {
            case 'create': found = allContent.includes('navigate') && allContent.includes('create'); break;
            case 'edit':    found = allContent.includes('edit') || allContent.includes('Edit'); break;
            case 'delete':  found = allContent.includes('delete') || allContent.includes('Delete'); break;
            case 'approve': found = allContent.includes('approve') || allContent.includes('Approve'); break;
            case 'reject':  found = allContent.includes('reject') || allContent.includes('Reject'); break;
            case 'export':  found = allContent.includes('export') || allContent.includes('Export'); break;
            default:        found = allContent.toLowerCase().includes(action.toLowerCase());
          }
          if (!found) {
            BLOCKING(`PC-2: ${entityName} pages missing action handler for '${action}' (section: ${section.code}). ` +
              `Fix: add ${action} button/handler to the relevant page.`);
          }
        }
      }
    }

    // ──── PC-3 (WARNING): I18n Label Accent Accuracy ────
    // French/German/Italian labels MUST have proper UTF-8 accents.
    const ACCENT_PATTERNS_FR = {
      'Employes': 'Employés', 'Departement': 'Département', 'Departements': 'Départements',
      'Prenom': 'Prénom', 'Numero': 'Numéro', 'Societe': 'Société',
      'Categorie': 'Catégorie', 'Securite': 'Sécurité', 'Conge': 'Congé', 'Conges': 'Congés',
      'Generalites': 'Généralités', 'Resume': 'Résumé', 'Echeance': 'Échéance'
    };
    const i18nFiles = Glob(`src/**/i18n/locales/fr/*.json`);
    for (const file of i18nFiles) {
      const content = readFile(file);
      for (const [ascii, accented] of Object.entries(ACCENT_PATTERNS_FR)) {
        if (content.includes(`"${ascii}"`) || content.includes(`"${ascii} `)) {
          WARNING(`PC-3: ${file} contains ASCII-only '${ascii}' — should be '${accented}'. Fix the accent.`);
        }
      }
    }

    // ──── PC-5 (WARNING): Section Page Completeness ────
    // Every primary/functional section in screens.json should have a corresponding page.
    for (const section of screens.sections) {
      if (!['primary', 'functional'].includes(section.sectionType)) continue;
      const sectionCode = section.code;
      const resources = section.resources || [];
      const entityNames = resources.map(r => r.entity).filter(Boolean);

      // Check if any page exists for this section's entities
      let pageFound = false;
      for (const entity of entityNames) {
        const pages = Glob(`src/pages/**/${entity}*Page.tsx`);
        if (pages.length > 0) { pageFound = true; break; }
      }
      // Also check by section code (e.g., CalendarPage, ApprovePage)
      if (!pageFound) {
        const sectionPages = Glob(`src/pages/**/*${sectionCode}*Page.tsx`);
        pageFound = sectionPages.length > 0;
      }
      if (!pageFound) {
        WARNING(`PC-5: Section '${sectionCode}' (${section.sectionType}) defined in screens.json ` +
          `but no corresponding page found. Consider generating it.`);
      }
    }
  }

  if (perms) {
    // ──── PC-4 (BLOCKING): Permission Seed Data Completeness ────
    // All permissionPaths from permissions.json MUST be seeded in PermissionsSeedData.
    const permSeedFiles = Glob(`**/PermissionsSeedData.cs`);
    const permClassFiles = Glob(`**/Permissions.cs`);
    const allPermContent = [...permSeedFiles, ...permClassFiles].map(f => readFile(f)).join('\n');

    for (const path of perms.permissionPaths) {
      // Check for the permission path (may be stored as kebab-case or segments)
      const segments = path.split('.');
      const lastSegment = segments[segments.length - 1].toLowerCase();
      if (!allPermContent.toLowerCase().includes(lastSegment)) {
        BLOCKING(`PC-4: Permission path '${path}' not found in seed data. ` +
          `Fix: add to PermissionsSeedData.cs and Permissions.cs constants.`);
      }
    }

    // Verify roles from permissions.json exist in RolesSeedData
    const roleSeedFiles = Glob(`**/RolesSeedData.cs`);
    const allRoleContent = roleSeedFiles.map(f => readFile(f)).join('\n');
    for (const role of perms.roles) {
      if (!allRoleContent.includes(role.role)) {
        WARNING(`PC-4b: Role '${role.role}' from permissions.json not found in RolesSeedData.cs.`);
      }
    }
  }

  // ──── PC-6 (WARNING): I18n Namespace Registration ────
  // This overlaps with GATE PG-4 but cross-checks against screens.json sections.
  const i18nConfig = Glob(`src/**/i18n/config.ts`)[0] || Glob(`src/**/i18n/index.ts`)[0];
  if (i18nConfig && screens) {
    const configContent = readFile(i18nConfig);
    const moduleCode = prd.project?.module;
    if (moduleCode && !configContent.includes(moduleCode)) {
      WARNING(`PC-6: Module namespace '${moduleCode}' not registered in ${i18nConfig}. ` +
        `Translation keys will render as raw strings.`);
    }
  }
}
```

> **If ANY BLOCKING check fails:** Return to step-03d to fix the specific issue, then re-run section 6d.
> BLOCKING failures indicate the generated code does NOT match the PRD specification.

---

## 7. Acceptance Criteria POST-CHECK

For each AC from step-01:
```
AC1: {criterion} → PASS / FAIL (evidence)
...
```
**All ACs must PASS. If any FAIL, return to step-03.**

---

## 8. eXamine Summary

**APEX SmartStack - eXamine Complete**

| Layer | Status |
|-------|--------|
| Layer 0 — Domain + Infrastructure | PASS / N/A |
| Layer 1 — Seed Data | PASS / N/A |
| Layer 2 — Backend | PASS / N/A |
| Layer 3 — Frontend | PASS / N/A |
| Layer 4 — DevData | PASS / N/A / SKIPPED |

| Category | Status |
|----------|--------|
| MCP conventions | PASS (0 errors) |
| Build (dotnet + npm) | PASS |
| EF Core (migrations, SQL Server) | PASS / N/A |
| Integration tests | PASS / N/A |
| Frontend routes + i18n | PASS / N/A |
| Security checks | PASS / N/A |
| Seed data completeness | PASS / N/A |
| Code quality | PASS / N/A |
| NavRoute kebab-case | PASS / N/A |
| DateOnly DTOs | PASS / N/A |
| I18n registration | PASS / N/A |
| FluentValidation DI | PASS / N/A |
| Route/NavRoute conflict | PASS / N/A |
| Role-permission matrix | PASS / N/A |
| Navigation translations (4 langs) | PASS / N/A |
| Inline tests | PASS / N/A |
| POST-CHECKs | PASS / N/A |
| PRD Compliance (delegate) | PASS / N/A |
| Acceptance criteria | {X}/{Y} PASS |

---

## 8b. Runtime Validation

For runtime validation (if NOT foundation/economy mode): Run `/validate-feature {EntityName}` (OPTIONAL but RECOMMENDED).

---

## 9. Route to Next Step

```
IF examine_mode: Load step-05 → step-06 → step-07
ELSE: Load step-07-tests.md

TaskUpdate(taskId: examine_task_id, status: "completed")
```

**RULE:** APEX requires steps 07-08 (100% tests, >= 80% coverage).

## 10. PRD Reconciliation (delegate mode)

```
For all tasks in module:
  IF executed AND checks passed: Set task.status = 'completed'
Set prd.updated_at = now
If all done: Set prd.status = 'completed' (single) or 'in_progress' (multi)
```
