# BLOCKING POST-CHECKs

> **Referenced by:** step-04-examine.md (section 6b)
> These checks run on the actual generated files. Model-interpreted checks are unreliable.

## Run MCP Tools FIRST

Before running these bash checks, call these 3 MCP tools (they cover ~10 additional checks automatically):

1. `mcp__smartstack__validate_conventions()` — covers: controller routes, NavRoute kebab-case, naming conventions
2. `mcp__smartstack__validate_security()` — covers: tenant isolation, TenantId filters, authorization, Guid.Empty, TenantId!.Value patterns
3. `mcp__smartstack__validate_frontend_routes()` — covers: lazy loading in routes, route alignment

**The checks below provide defense-in-depth** — they catch patterns the MCP tools may miss and serve as a safety net.

---

## Security — Tenant Isolation & Authorization (defense-in-depth with MCP)

### POST-CHECK S1: All services must filter by TenantId (OWASP A01)

```bash
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  for f in $SERVICE_FILES; do
    HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
    HAS_OPTIONAL_ENTITY=false
    if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
      HAS_OPTIONAL_ENTITY=true
    fi
    if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
      echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
      echo "Every service query MUST filter by _currentTenant.TenantId"
      exit 1
    fi
    if grep -q "Guid.Empty" "$f"; then
      echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
      exit 1
    fi
  done
fi
```

### POST-CHECK S2: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)

```bash
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
      echo "BLOCKING: Controller uses [Authorize] without [RequirePermission]: $f"
      echo "[Authorize] alone provides NO RBAC enforcement — any authenticated user has access"
      echo "Fix: Add [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
      exit 1
    fi
  done
fi
```

### POST-CHECK S3: Services must inject ICurrentTenantService (tenant isolation)

```bash
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  for f in $SERVICE_FILES; do
    if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
      echo "BLOCKING: Service missing tenant context injection: $f"
      echo "All services MUST inject ICurrentTenantService for tenant isolation"
      exit 1
    fi
  done
fi
```

### POST-CHECK S4: HasQueryFilter must not use Guid.Empty (OWASP A01)

```bash
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
if [ -n "$CONFIG_FILES" ]; then
  BAD_FILTER=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
  if [ -n "$BAD_FILTER" ]; then
    echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty — bypasses tenant isolation: $BAD_FILTER"
    echo "The EF global query filter MUST use the injected TenantId, not Guid.Empty"
    exit 1
  fi
fi
```

### POST-CHECK S5: Services must NOT use TenantId!.Value (null-forgiving crash pattern)

```bash
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  BAD_PATTERN=$(grep -Pn 'TenantId!\.' $SERVICE_FILES 2>/dev/null)
  if [ -n "$BAD_PATTERN" ]; then
    echo "BLOCKING: TenantId!.Value (null-forgiving) detected — NullReferenceException risk: $BAD_PATTERN"
    echo "Fix: Use guard clause: var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();"
    exit 1
  fi
fi
```

### POST-CHECK S6: Pages must use lazy loading (no static page imports in routes)

```bash
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
if [ -n "$APP_TSX" ]; then
  STATIC_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" "$APP_TSX" $ROUTE_FILES 2>/dev/null)
  if [ -n "$STATIC_IMPORTS" ]; then
    echo "BLOCKING: Static page imports in route files — MUST use React.lazy()"
    echo "Static imports load ALL pages upfront, killing initial load performance"
    echo "$STATIC_IMPORTS"
    echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })))"
    exit 1
  fi
fi
```

---

## Backend — Entity, Service & Controller Checks

### POST-CHECK 1: Navigation routes must be full paths starting with /

```bash
# Find all seed data files and check route values
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
if [ -n "$SEED_FILES" ]; then
  # Check for short routes (no leading /) in Create() calls for navigation entities
  BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
  if [ -n "$BAD_ROUTES" ]; then
    echo "BLOCKING: Navigation routes must be full paths starting with /"
    echo "$BAD_ROUTES"
    echo "Expected: \"/human-resources\" NOT \"humanresources\""
    exit 1
  fi
fi
```

### POST-CHECK 2: Seed data must not use deterministic/sequential/fixed GUIDs

```bash
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
if [ -n "$SEED_FILES" ]; then
  BAD_GUIDS=$(grep -Pn 'GenerateDeterministicGuid|GenerateGuid\(int|11111111-1111-1111-1111-' $SEED_FILES 2>/dev/null)
  if [ -n "$BAD_GUIDS" ]; then
    echo "BLOCKING: Seed data must use Guid.NewGuid(), not deterministic/sequential/fixed GUIDs"
    echo "$BAD_GUIDS"
    exit 1
  fi
fi
```

## Frontend — CSS, Forms, Components, I18n

### POST-CHECK 3: Translation files must exist for all 4 languages (if frontend)

```bash
# Find all i18n namespaces used in tsx files
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
if [ -n "$TSX_FILES" ]; then
  NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
  for NS in $NAMESPACES; do
    for LANG in fr en it de; do
      if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
        echo "BLOCKING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
        exit 1
      fi
    done
  done
fi
```

### POST-CHECK 4: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs

```bash
# Check for modal/dialog/drawer/slide-over imports AND inline form state in page files
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  FAIL=false

  # 8a. Component imports (Modal, Dialog, Drawer, etc.)
  MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
  if [ -n "$MODAL_IMPORTS" ]; then
    echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
    echo "$MODAL_IMPORTS"
    FAIL=true
  fi

  # 8b. Inline form state (catches drawers/slide-overs built without external components)
  FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
  if [ -n "$FORM_STATE" ]; then
    echo "BLOCKING: Inline form state detected — forms embedded in ListPage as drawers/panels"
    echo "Create/Edit forms MUST be separate page components with their own URL routes"
    echo "$FORM_STATE"
    FAIL=true
  fi

  if [ "$FAIL" = true ]; then
    echo ""
    echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
    echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
    exit 1
  fi
fi
```

### POST-CHECK 5: Create/Edit pages must exist as separate route pages (BLOCKING)

```bash
# For each module with a list page, verify create and edit pages exist
# If ListPage has navigate() calls to /create or /:id/edit, the target pages MUST exist
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
FAIL=false
if [ -n "$LIST_PAGES" ]; then
  for LIST_PAGE in $LIST_PAGES; do
    PAGE_DIR=$(dirname "$LIST_PAGE")
    MODULE_NAME=$(basename "$PAGE_DIR")

    # Detect if ListPage navigates to /create or /edit routes
    HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null)
    HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit|navigate\(.*/:id/edit" "$LIST_PAGE" 2>/dev/null)

    # Check for create page
    CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
    if [ -z "$CREATE_PAGE" ]; then
      if [ -n "$HAS_CREATE_NAV" ]; then
        echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /create but CreatePage does NOT exist"
        echo "  Dead link: $HAS_CREATE_NAV"
        echo "  Fix: Create ${MODULE_NAME}CreatePage.tsx in $PAGE_DIR"
        FAIL=true
      else
        echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
      fi
    fi

    # Check for edit page
    EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
    if [ -z "$EDIT_PAGE" ]; then
      if [ -n "$HAS_EDIT_NAV" ]; then
        echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /:id/edit but EditPage does NOT exist"
        echo "  Dead link: $HAS_EDIT_NAV"
        echo "  Fix: Create ${MODULE_NAME}EditPage.tsx in $PAGE_DIR"
        FAIL=true
      else
        echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
      fi
    fi
  done
fi

if [ "$FAIL" = true ]; then
  echo ""
  echo "BLOCKING: Create/Edit pages are MISSING but ListPage buttons link to them."
  echo "Users will see white screen / 404 when clicking Create or Edit buttons."
  echo "Fix: Generate form pages using /ui-components skill patterns (smartstack-frontend.md section 3b)"
  exit 1
fi
```

### POST-CHECK 6: Form pages must have companion test files

```bash
# Minimum requirement: if frontend pages exist, at least 1 test file must be present
PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
  if [ -z "$ALL_TESTS" ]; then
    echo "BLOCKING: No frontend test files found in src/pages/"
    echo "Every form page MUST have a companion .test.tsx file"
    exit 1
  fi
fi

# Every CreatePage and EditPage must have a .test.tsx file
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
if [ -n "$FORM_PAGES" ]; then
  for FORM_PAGE in $FORM_PAGES; do
    TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
    if [ ! -f "$TEST_FILE" ]; then
      echo "BLOCKING: Form page missing test file: $FORM_PAGE"
      echo "Expected: $TEST_FILE"
      echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
      exit 1
    fi
  done
fi
```

### POST-CHECK 7: FK fields must use EntityLookup — NO `<input>`, NO `<select>` (BLOCKING)

```bash
# Check ALL page files for FK fields rendered as <input> or <select> instead of EntityLookup
# Scans ALL .tsx files (not just CreatePage/EditPage — forms may be embedded in ListPage drawers)
ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
if [ -n "$ALL_PAGES" ]; then
  FAIL=false

  # 1. Detect <input> with name/value binding to FK fields (fields ending in "Id")
  FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]')
  if [ -n "$FK_INPUTS" ]; then
    echo "BLOCKING: FK fields rendered as <input> — MUST use EntityLookup component"
    echo "$FK_INPUTS"
    FAIL=true
  fi

  # 2. Detect <select> with value binding to FK fields (e.g., value={formData.departmentId})
  FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null)
  if [ -n "$FK_SELECTS" ]; then
    echo "BLOCKING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
    echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
    echo "EntityLookup provides: debounced search, paginated results, display name resolution."
    echo "$FK_SELECTS"
    FAIL=true
  fi

  # 3. Detect onChange handlers setting FK fields from <select> (e.g., setFormData({...formData, departmentId: e.target.value}))
  FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null)
  if [ -n "$FK_SELECT_ONCHANGE" ]; then
    echo "BLOCKING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
    echo "$FK_SELECT_ONCHANGE"
    FAIL=true
  fi

  # 4. Check for placeholders mentioning "ID", "GUID", or "Select..." for FK fields
  FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*(?:[Ee]nter.*[Ii][Dd]|[Gg][Uu][Ii][Dd]|[Ss]elect.*[Ee]mployee|[Ss]elect.*[Dd]epartment|[Ss]elect.*[Pp]osition|[Ss]elect.*[Pp]roject|[Ss]elect.*[Cc]ategory)' $ALL_PAGES 2>/dev/null)
  if [ -n "$FK_PLACEHOLDER" ]; then
    echo "BLOCKING: Form has placeholder for FK field selection — use EntityLookup search instead"
    echo "$FK_PLACEHOLDER"
    FAIL=true
  fi

  # 5. Detect <option> elements with GUID-like values (sign of FK <select>)
  FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null)
  if [ -n "$FK_OPTIONS" ]; then
    echo "BLOCKING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
    echo "Replace the entire <select>/<option> block with <EntityLookup />"
    echo "$FK_OPTIONS"
    FAIL=true
  fi

  if [ "$FAIL" = true ]; then
    echo ""
    echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
    echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
    echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
    echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
    exit 1
  fi
fi
```

### POST-CHECK 8: Backend APIs must support search parameter for EntityLookup

```bash
# Check that controller GetAll methods accept search parameter
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    # Check if controller has GetAll but no search parameter
    if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
      if ! grep -q "search" "$f"; then
        echo "WARNING: Controller missing search parameter on GetAll: $f"
        echo "GetAll endpoints MUST accept ?search= to enable EntityLookup on frontend"
        echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
      fi
    fi
  done
fi
```

### POST-CHECK 9: No hardcoded Tailwind colors in generated pages (BLOCKING)

```bash
# Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
if [ -n "$ALL_PAGES" ]; then
  HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
  if [ -n "$HARDCODED" ]; then
    echo "BLOCKING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
    echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
    echo ""
    echo "Fix mapping:"
    echo "  bg-white         → bg-[var(--bg-card)]"
    echo "  bg-gray-50       → bg-[var(--bg-primary)]"
    echo "  text-gray-900    → text-[var(--text-primary)]"
    echo "  text-gray-500    → text-[var(--text-secondary)]"
    echo "  border-gray-200  → border-[var(--border-color)]"
    echo "  bg-blue-600      → bg-[var(--color-accent-500)]"
    echo "  text-blue-600    → text-[var(--color-accent-500)]"
    echo "  text-red-500     → text-[var(--error-text)]"
    echo "  bg-green-500     → bg-[var(--success-bg)]"
    echo ""
    echo "See references/smartstack-frontend.md section 4 for full variable reference"
    echo ""
    echo "$HARDCODED"
    exit 1
  fi
fi
```

## Seed Data — Navigation, Roles, Permissions

### POST-CHECK 10: Routes seed data must match frontend application routes

```bash
SEED_ROUTES=$(grep -Poh 'Route\s*=\s*"([^"]+)"' $(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null) 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
  FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
  if [ -n "$FRONTEND_PATHS" ]; then
    MISMATCH_FOUND=false
    for SEED_ROUTE in $SEED_ROUTES; do
      DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
      if [ "$DEPTH" -lt 3 ]; then continue; fi
      SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
      SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
      MATCH_FOUND=false
      for FE_PATH in $FRONTEND_PATHS; do
        # Flag FORBIDDEN /list suffix BEFORE normalization
        if echo "$FE_PATH" | grep -qP '/list$'; then
          echo "WARNING: Frontend route ends with /list — should use index route instead: $FE_PATH"
        fi
        FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
        FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
        if [ "$SEED_NORM" = "$FE_NORM" ]; then
          MATCH_FOUND=true
          break
        fi
      done
      if [ "$MATCH_FOUND" = false ]; then
        echo "BLOCKING: Seed data route has no matching frontend route: $SEED_ROUTE"
        MISMATCH_FOUND=true
      fi
    done
    if [ "$MISMATCH_FOUND" = true ]; then
      echo "Fix: Ensure every NavigationSeedData route has a corresponding route entry in App.tsx (applicationRoutes or JSX Route wrappers)"
      exit 1
    fi
  fi
fi
```

### POST-CHECK 14b: Frontend routes must use kebab-case (BLOCKING)

```bash
# POST-CHECK 14 normalizes hyphens for existence check, but does NOT catch kebab-case mismatches.
# This supplementary check detects concatenated multi-word route segments without hyphens.
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
if [ -n "$APP_TSX" ]; then
  # Extract route path strings from App.tsx
  FE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"')
  for FE_PATH in $FE_PATHS; do
    # Split path by / and check each segment
    for SEG in $(echo "$FE_PATH" | tr '/' '\n'); do
      # Skip dynamic segments (:id, :slug) and single words (< 10 chars likely single word)
      echo "$SEG" | grep -qP '^:' && continue
      # Detect multi-word segments without hyphens: 2+ consecutive lowercase sequences
      # e.g., "humanresources" (human+resources), "timemanagement" (time+management)
      if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
        # Potential concatenated multi-word — cross-check with seed data
        SEED_MATCH=$(echo "$SEED_ROUTES" | tr '/' '\n' | grep -P "^[a-z]+-[a-z]+" | tr -d '-' | grep -x "$SEG")
        if [ -n "$SEED_MATCH" ]; then
          echo "BLOCKING: Frontend route segment '$SEG' appears to be missing hyphens"
          echo "Seed data uses kebab-case (e.g., 'human-resources') but frontend has '$SEG'"
          echo "Fix: Use kebab-case in App.tsx route paths to match seed data exactly"
          exit 1
        fi
      fi
    done
  done
fi
```

### POST-CHECK 11: GetAll methods must return PaginatedResult<T>

```bash
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
  if [ -n "$BAD_RETURNS" ]; then
    echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
    echo "$BAD_RETURNS"
    echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
    exit 1
  fi
fi
```

### POST-CHECK 12: i18n files must contain required structural keys

```bash
I18N_DIR="src/i18n/locales/fr"
if [ -d "$I18N_DIR" ]; then
  REQUIRED_KEYS="actions columns empty errors form labels messages validation"
  for JSON_FILE in "$I18N_DIR"/*.json; do
    [ ! -f "$JSON_FILE" ] && continue
    BASENAME=$(basename "$JSON_FILE")
    case "$BASENAME" in common.json|navigation.json) continue;; esac
    for KEY in $REQUIRED_KEYS; do
      if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
        echo "BLOCKING: i18n file missing required key '$KEY': $JSON_FILE"
        echo "Module i18n files MUST contain: $REQUIRED_KEYS"
        exit 1
      fi
    done
  done
fi
```

### POST-CHECK 13: Entities must implement IAuditableEntity + Validators must have Create/Update pairs

```bash
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
if [ -n "$ENTITY_FILES" ]; then
  for f in $ENTITY_FILES; do
    if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
      echo "BLOCKING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
      echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
      exit 1
    fi
  done
fi
CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
if [ -n "$CREATE_VALIDATORS" ]; then
  for f in $CREATE_VALIDATORS; do
    VALIDATOR_DIR=$(dirname "$f")
    ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
    if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
      echo "BLOCKING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
      echo "  Found: $f"
      echo "  Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
      exit 1
    fi
  done
fi
```

### POST-CHECK 14: RolePermission seed data must NOT use deterministic role GUIDs

```bash
# System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core.
# RolePermission mappings MUST look up roles by Code at runtime, NEVER use deterministic GUIDs.
SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
if [ -n "$SEED_ALL_FILES" ]; then
  BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
  if [ -n "$BAD_ROLE_GUID" ]; then
    echo "BLOCKING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
    echo "System roles are pre-seeded by SmartStack core with their own IDs"
    echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
    echo "  var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
    echo "  var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
    echo "$BAD_ROLE_GUID"
    exit 1
  fi
fi
# Also check for GenerateRoleGuid usage in RolePermission mapping files (not in ApplicationRolesSeedData itself)
ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
if [ -n "$ROLE_PERM_FILES" ]; then
  BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null)
  if [ -n "$BAD_ROLE_REF" ]; then
    echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
    echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
    echo "$BAD_ROLE_REF"
  fi
fi
```

### POST-CHECK 15: Cross-tenant entities must use Guid? TenantId

```bash
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
  if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
    if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
      echo "BLOCKING: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
      exit 1
    fi
  fi
done
echo "POST-CHECK 15: OK"
```

### POST-CHECK 16: Scoped entities must have EntityScope property

```bash
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
  if grep -q "IScopedTenantEntity" "$entity"; then
    if ! grep -q "EntityScope\|Scope" "$entity"; then
      echo "BLOCKING: Entity with IScopedTenantEntity must have EntityScope Scope property"
      exit 1
    fi
  fi
done
echo "POST-CHECK 16: OK"
```

### POST-CHECK 17: Permissions.cs static constants must exist (BLOCKING)

```bash
# Every module with controllers MUST have a Permissions.cs with static constants
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  PERM_REFS=$(grep -ohP 'Permissions\.\w+\.\w+' $CTRL_FILES 2>/dev/null | sed 's/Permissions\.\([^.]*\)\..*/\1/' | sort -u)
  for MODULE in $PERM_REFS; do
    PERM_FILE=$(find src/ -name "Permissions.cs" -exec grep -l "static class $MODULE" {} \; 2>/dev/null)
    if [ -z "$PERM_FILE" ]; then
      echo "BLOCKING: Controller references Permissions.${MODULE}.* but no Permissions.cs defines static class ${MODULE}"
      echo "Fix: Create Application/Authorization/Permissions.cs with: public static class ${MODULE} { public const string Read = \"...\"; ... }"
      exit 1
    fi
  done
fi
```

### POST-CHECK 18: ApplicationRolesSeedData.cs must exist (BLOCKING)

```bash
# If any RolesSeedData exists, ApplicationRolesSeedData MUST also exist
ROLE_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null | head -1)
if [ -n "$ROLE_SEED" ]; then
  APP_ROLE_SEED=$(find src/ -path "*/Seeding/Data/ApplicationRolesSeedData.cs" 2>/dev/null | head -1)
  if [ -z "$APP_ROLE_SEED" ]; then
    echo "BLOCKING: RolesSeedData exists but ApplicationRolesSeedData.cs NOT FOUND"
    echo "ApplicationRolesSeedData defines the 4 application-scoped roles (admin, manager, contributor, viewer)"
    echo "Without it, SeedRolesAsync() has no role entries to create → RBAC broken"
    echo "Fix: Create src/Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs"
    exit 1
  fi
fi
```

### POST-CHECK 25b: Section route completeness (NavigationSection → frontend route + permissions)

```bash
# Every NavigationSection seed data route MUST have a corresponding frontend route in App.tsx
# and section-level permissions MUST exist for each section defined in seed data
SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSectionSeedData.cs" 2>/dev/null)
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$APP_TSX" ]; then
  # Extract section routes from seed data
  SECTION_ROUTES=$(grep -Poh '"/[a-z][a-z0-9/-]+"' $SECTION_SEED_FILES 2>/dev/null | tr -d '"' | sort -u)
  for SECTION_ROUTE in $SECTION_ROUTES; do
    # Extract the last segment (section-kebab) for frontend route matching
    SECTION_SEG=$(echo "$SECTION_ROUTE" | rev | cut -d'/' -f1 | rev)
    if ! grep -q "'$SECTION_SEG'" "$APP_TSX" && ! grep -q "\"$SECTION_SEG\"" "$APP_TSX"; then
      echo "BLOCKING: NavigationSection seed data route has no matching frontend route: $SECTION_ROUTE"
      echo "Expected path segment '$SECTION_SEG' in App.tsx application route block"
      echo "Fix: Add section child routes to the module's children array in App.tsx"
    fi
  done
fi

# All controllers with [NavRoute] must have matching [RequirePermission]
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    # Match NavRoute with 2+ dot-separated segments (minimum: app.module)
    SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z-]+\.[a-z-]+' "$f" 2>/dev/null)
    if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
      echo "BLOCKING: Controller has [NavRoute] but no [RequirePermission]: $f"
      echo "Fix: Add [RequirePermission(Permissions.{Section}.{Action})] on each endpoint"
      exit 1
    fi
  done
fi

# Section-level permissions must exist for each section in seed data
PERM_FILE=$(find src/ -name "Permissions.cs" -path "*/Authorization/*" 2>/dev/null | head -1)
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
  SECTION_CODES=$(grep -oP 'Code\s*=\s*"([a-z]+)"' $SECTION_SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
  for CODE in $SECTION_CODES; do
    PASCAL=$(echo "$CODE" | sed 's/^./\U&/')
    if ! grep -q "static class $PASCAL" "$PERM_FILE" 2>/dev/null; then
      echo "WARNING: Section '$CODE' in seed data has no matching Permissions.$PASCAL static class"
      echo "Fix: Add section-level permissions via MCP generate_permissions with 3-segment navRoute (app.module.section)"
    fi
  done
fi
```

### POST-CHECK 19: FORBIDDEN route patterns — /list and /detail/:id (BLOCKING)

```bash
# 1. Check seed data for FORBIDDEN suffixes
SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "*NavigationSectionSeedData.cs" 2>/dev/null)
if [ -n "$SEED_NAV_FILES" ]; then
  BAD_ROUTES=$(grep -Pn 'Route\s*=\s*.*"[^"]*/(list|detail)["/]' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route')
  if [ -n "$BAD_ROUTES" ]; then
    echo "BLOCKING: FORBIDDEN route pattern in seed data"
    echo "  - 'list' section route = module route (NO /list suffix)"
    echo "  - 'detail' section route = module route + /:id (NOT /detail/:id)"
    echo "$BAD_ROUTES"
    exit 1
  fi
fi

# 2. Check frontend routes for FORBIDDEN path segments
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
if [ -n "$APP_TSX" ]; then
  BAD_FE=$(grep -Pn "path:\s*['\"](?:list|detail)" "$APP_TSX" 2>/dev/null)
  if [ -n "$BAD_FE" ]; then
    echo "BLOCKING: FORBIDDEN frontend route path"
    echo "  - list = index: true (no 'list' path segment)"
    echo "  - detail = ':id' (no 'detail' path segment)"
    echo "$BAD_FE"
    exit 1
  fi
fi
echo "OK: No forbidden /list or /detail route patterns found"
```

### POST-CHECK 20: Permission path segment count (WARNING)

```bash
PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
if [ -n "$PERM_FILES" ]; then
  while IFS= read -r line; do
    PATH_VAL=$(echo "$line" | grep -oP '"[^"]*\.[^"]*"' | tr -d '"')
    if [ -n "$PATH_VAL" ]; then
      DOTS=$(echo "$PATH_VAL" | tr -cd '.' | wc -c)
      # Module permissions: 2 dots (app.module.action = 3 segments = 2+1)
      # Section permissions: 3 dots (app.module.section.action = 4 segments = 3+1)
      # Wildcard: ends with .* (valid at any level)
      if echo "$PATH_VAL" | grep -qP '\.\*$'; then
        continue  # Wildcards are valid
      elif [ "$DOTS" -lt 2 ] || [ "$DOTS" -gt 4 ]; then
        echo "WARNING: Permission path has unexpected segment count ($((DOTS+1)) segments): $PATH_VAL"
      fi
    fi
  done < <(grep -n 'Path\s*=' $PERM_FILES 2>/dev/null)
fi
```

### POST-CHECK 21: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)

```bash
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
if [ -n "$PROVIDER" ]; then
  METHODS_FOUND=0
  for METHOD in SeedNavigationAsync SeedRolesAsync SeedPermissionsAsync SeedRolePermissionsAsync; do
    if grep -q "$METHOD" "$PROVIDER"; then
      METHODS_FOUND=$((METHODS_FOUND + 1))
    else
      echo "BLOCKING: IClientSeedDataProvider missing method: $METHOD in $PROVIDER"
    fi
  done
  if [ "$METHODS_FOUND" -lt 4 ]; then
    echo "Fix: IClientSeedDataProvider must implement all 4 methods: SeedNavigationAsync, SeedRolesAsync, SeedPermissionsAsync, SeedRolePermissionsAsync"
    exit 1
  fi

  # Check DI registration
  DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
  if [ -n "$DI_FILE" ]; then
    if ! grep -q "IClientSeedDataProvider" "$DI_FILE"; then
      echo "BLOCKING: IClientSeedDataProvider not registered in DependencyInjection.cs"
      echo "Fix: Add services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()"
      exit 1
    fi
  fi
fi
```

### POST-CHECK 22: i18n must use separate JSON files per language (not embedded in index.ts)

```bash
# Translations MUST be in src/i18n/locales/{lang}/{module}.json, NOT embedded in a single .ts file
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
if [ -n "$TSX_FILES" ]; then
  # Check if i18n locales directory exists
  if [ ! -d "src/i18n/locales" ]; then
    echo "BLOCKING: Missing src/i18n/locales/ directory"
    echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
    echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
    exit 1
  fi

  # Check for embedded translations in index.ts (common anti-pattern)
  I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null)
  if [ -n "$I18N_INDEX" ]; then
    EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null)
    if [ -n "$EMBEDDED" ]; then
      echo "BLOCKING: Translations embedded in i18n config file — must be in separate JSON files"
      echo "Found embedded translations in:"
      echo "$EMBEDDED"
      echo ""
      echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
      echo "The i18n config should import from locales/ directory, not contain inline translations"
      exit 1
    fi
  fi

  # Verify all 4 language directories exist
  for LANG in fr en it de; do
    if [ ! -d "src/i18n/locales/$LANG" ]; then
      echo "BLOCKING: Missing language directory: src/i18n/locales/$LANG/"
      echo "SmartStack requires 4 languages: fr, en, it, de"
      exit 1
    fi
  done

  # Verify at least one JSON file exists per language
  for LANG in fr en it de; do
    JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
    if [ "$JSON_COUNT" -eq 0 ]; then
      echo "BLOCKING: No translation JSON files in src/i18n/locales/$LANG/"
      echo "Each module must have a {module}.json file per language"
      exit 1
    fi
  done
fi
```

### POST-CHECK 23: Pages must use useTranslation hook (no hardcoded user-facing strings)

```bash
# Verify that page components use i18n — detect hardcoded strings in JSX
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
if [ -n "$PAGE_FILES" ]; then
  # Check that at least 80% of pages import useTranslation
  TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
  I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
  if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
    echo "BLOCKING: No page files use useTranslation — all user-facing text must be translated"
    echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
    echo ""
    echo "Fix: Import and use useTranslation in every page component:"
    echo "  const { t } = useTranslation(['{module}']);"
    echo "  t('{module}:title', 'Fallback text')"
    exit 1
  fi

  # Check for common hardcoded English strings in JSX (heuristic)
  HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No data|Not found|Submit|Back|Actions|Name|Status|Description)\s*<' $PAGE_FILES 2>/dev/null | grep -v '{t(' | head -10)
  if [ -n "$HARDCODED_TEXT" ]; then
    echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
    echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
    echo "$HARDCODED_TEXT"
  fi
fi
```

### POST-CHECK 24: List/Detail pages must include DocToggleButton (documentation panel)

```bash
# Every list and detail page MUST have DocToggleButton for inline documentation access
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
if [ -n "$LIST_PAGES" ]; then
  MISSING_DOC=0
  for PAGE in $LIST_PAGES; do
    if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
      echo "WARNING: Page missing DocToggleButton: $PAGE"
      echo "  Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
      echo "  Place in header actions: <DocToggleButton />"
      MISSING_DOC=$((MISSING_DOC + 1))
    fi
  done
  if [ "$MISSING_DOC" -gt 0 ]; then
    echo ""
    echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
    echo "See smartstack-frontend.md section 7 for placement pattern"
  fi
fi
```

### POST-CHECK 25: Module documentation must be generated (doc-data.ts)

```bash
# After frontend pages exist, /documentation should have been called
TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/")
DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null)
if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
  echo "WARNING: Frontend pages exist but no documentation generated"
  echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
  echo "The DocToggleButton in page headers will link to this documentation"
fi
```

### POST-CHECK 26: Pagination type must be PaginatedResult<T> — no aliases (BLOCKING)

```bash
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
ALL_FILES="$SERVICE_FILES $CTRL_FILES"
if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
  BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null)
  if [ -n "$BAD_NAMES" ]; then
    echo "BLOCKING: Pagination type must be PaginatedResult<T> — found non-canonical names"
    echo "$BAD_NAMES"
    echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
    echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
    exit 1
  fi
fi
```

## Infrastructure — Migration & Build

### POST-CHECK 27: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)

```bash
# If feature.json has entities with codePattern.strategy != "manual",
# verify that ICodeGenerator<Entity> is registered in DI
FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
  for FEATURE in $FEATURE_FILES; do
    ENTITIES_WITH_CODE=$(python3 -c "
import json, sys
try:
  with open('$FEATURE') as f:
    data = json.load(f)
  for e in data.get('analysis', {}).get('entities', []):
    cp = e.get('codePattern', {})
    if cp.get('strategy', 'manual') != 'manual':
      print(e['name'])
except: pass
" 2>/dev/null)
    for ENTITY in $ENTITIES_WITH_CODE; do
      if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
        echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
        echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
        exit 1
      fi
    done
  done
fi
```

### POST-CHECK 28: Code regex must support hyphens (BLOCKING)

```bash
VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
if [ -n "$VALIDATOR_FILES" ]; then
  OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-')
  if [ -n "$OLD_REGEX" ]; then
    echo "BLOCKING: Code validator uses old regex without hyphen support"
    echo "$OLD_REGEX"
    echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
    exit 1
  fi
fi
```

### POST-CHECK 29: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)

```bash
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  for f in $SERVICE_FILES; do
    if grep -q "ICodeGenerator" "$f"; then
      ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
      DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
      if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
        echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
        echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
      fi
    fi
  done
fi
```

### POST-CHECK 30: Translation seed data must have idempotency guard (BLOCKING)

```bash
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
if [ -n "$PROVIDER" ]; then
  # Check if NavigationTranslations.Add is used WITHOUT a preceding AnyAsync guard
  # Pattern: any .Add(NavigationTranslation.Create(...)) that is NOT inside an AnyAsync check
  TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null)
  TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null)

  if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
    echo "BLOCKING: Translation seed data inserts without idempotency guard in $PROVIDER"
    echo "Fix: Before each NavigationTranslations.Add block, check existence:"
    echo "  if (!await context.NavigationTranslations.AnyAsync("
    echo "      t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
    echo "           && t.EntityType == NavigationEntityType.Module, ct))"
    echo "  { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
    echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
    exit 1
  fi
fi
```

### POST-CHECK 31: Resource seed data must use actual section IDs from DB (BLOCKING)

```bash
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
if [ -n "$PROVIDER" ]; then
  # Check if NavigationResource.Create uses secEntry.Id or resEntry.SectionId (seed-time GUIDs)
  # instead of actualSection.Id (real DB ID). This causes FK_nav_Resources_nav_Sections_SectionId violation.
  if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
    echo "BLOCKING: Resource seed data uses seed-time GUID as SectionId in $PROVIDER"
    echo "NavigationSection.Create() generates its own ID — seed-time GUIDs do NOT exist in nav_Sections."
    echo "Fix: Query actual section from DB before creating resources:"
    echo "  var actualSection = await context.NavigationSections"
    echo "      .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
    echo "  NavigationResource.Create(actualSection.Id, ...)  // NOT secEntry.Id or resEntry.SectionId"
    exit 1
  fi
fi
```

### POST-CHECK 32: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)

```bash
# NavRoute segments are navigation entity Codes joined by dots.
# Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
# Verified from SmartStack.app: "support-client.my-tickets", "administration.access-requests"
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
    if [ -n "$NAVROUTE_VAL" ]; then
      # Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
      for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
        if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
          echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
          echo "  Full NavRoute: $NAVROUTE_VAL"
          echo "  Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
          echo "  SmartStack convention (from SmartStack.app): 'support-client.my-tickets'"
          exit 1
        fi
      done
    fi
  done
fi

# Also check seed data Code values for navigation entities
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
if [ -n "$SEED_FILES" ]; then
  CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
  for CODE in $CODES; do
    if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
      echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
      echo "  Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
      exit 1
    fi
  done
fi
```

### POST-CHECK 33: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)

```bash
# Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
# SmartStack.app convention: "support-client.my-tickets.read" (kebab-case everywhere)
# FORBIDDEN: "humanresources.employees.read" — must be "human-resources.employees.read"

# Check [RequirePermission] attributes in controllers
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
    for PERM in $PERM_VALS; do
      # Check each segment (except the action suffix) for concatenated multi-word without hyphens
      SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)  # remove last segment (action: read/create/update/delete)
      for SEG in $SEGMENTS; do
        if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
          echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
          echo "  Full permission: $PERM"
          echo "  Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
          echo "  SmartStack convention: 'support-client.my-tickets.read'"
          exit 1
        fi
      done
    done
  done
fi

# Check Permissions.cs constants
PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
if [ -n "$PERM_FILES" ]; then
  for f in $PERM_FILES; do
    CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
    for PERM in $CONST_VALS; do
      SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
      for SEG in $SEGMENTS; do
        if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
          echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
          echo "  Full permission: $PERM"
          echo "  Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
          exit 1
        fi
      done
    done
  done
fi

# Check PermissionsSeedData.cs for mismatched paths
SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
if [ -n "$SEED_PERM_FILES" ]; then
  PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
  for PERM in $PATHS; do
    SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
    for SEG in $SEGMENTS; do
      if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
        echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
        echo "  Full permission path: $PERM"
        echo "  Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
        exit 1
      fi
    done
  done
fi
```

### POST-CHECK 34: Frontend navigate() calls must have matching route definitions (BLOCKING)

```bash
# Detect dead links: navigate() calls to paths that don't have corresponding page components.
# Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  # Extract navigate targets (relative paths like '../leave-types', './create', etc.)
  NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
  # Extract route paths from App.tsx or route config
  APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
  if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
    ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
    for TARGET in $NAV_TARGETS; do
      # Skip dynamic segments (:id), back navigation (-1), and absolute URLs
      if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
      # Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
      LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
      if [ -z "$LAST_SEG" ]; then continue; fi
      # Check if any route path contains this segment
      FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
      if [ -z "$FOUND" ]; then
        # Verify no page component exists for this path
        SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
        PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
        if [ -z "$PAGE_EXISTS" ]; then
          # Find which file has this navigate call
          SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
          echo "BLOCKING: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
          echo "  Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
          echo "  Fix: Either create the page component + route, or remove the navigate() button"
          exit 1
        fi
      fi
    done
  fi
fi
```

### POST-CHECK 35: Detail page tabs must NOT navigate() — content switches locally (BLOCKING)

```bash
# Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
# Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
# instead of rendering sub-resource content inline. Users lost detail page context.
DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
if [ -n "$DETAIL_PAGES" ]; then
  FAIL=false
  for DP in $DETAIL_PAGES; do
    # Check if the page has tabs (activeTab state)
    HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
    if [ -z "$HAS_TABS" ]; then continue; fi

    # Check if any tab click handler calls navigate()
    # Pattern: function that both references setActiveTab AND navigate()
    # Look for navigate() calls inside handlers that also set tab state
    TAB_NAVIGATE=$(grep -Pn "navigate\(" "$DP" 2>/dev/null | grep -v "navigate\(\s*['\"]edit['\"]" | grep -v "navigate\(\s*-1\s*\)" | grep -v "navigate\(\s*['\`].*/:id/edit" | grep -v "//")
    if [ -n "$TAB_NAVIGATE" ]; then
      # Verify this navigate is in a tab handler context (near setActiveTab usage)
      # Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
      RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
      if [ -n "$RELATIVE_NAV" ]; then
        echo "BLOCKING: Detail page tabs use navigate() instead of local content switching: $DP"
        echo "  Tab click handlers MUST only call setActiveTab() — render content inline"
        echo "  Found navigate() calls (likely in tab handlers):"
        echo "$RELATIVE_NAV"
        echo ""
        echo "  Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
        echo "    {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
        echo "  See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
        FAIL=true
      fi
    fi
  done
  if [ "$FAIL" = true ]; then
    exit 1
  fi
fi
```

### POST-CHECK 36: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)

```bash
# Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
# Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
# are added in subsequent iterations without re-running migration.
SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
  # Extract DbSet entity names from DbContext (DbSet<EntityName>)
  DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
  FAIL=false
  for ENTITY in $DBSET_ENTITIES; do
    # Skip base SmartStack entities (handled by core migrations)
    if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
      continue
    fi
    # Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
    if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
      echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
      echo "  This means no migration was created for this entity — it will not exist in the database."
      echo "  Fix: Run 'dotnet ef migrations add' to include all new entities"
      FAIL=true
    fi
  done
  if [ "$FAIL" = true ]; then
    echo ""
    echo "  Root cause: Migration was likely created once for the first batch of entities,"
    echo "  but additional entities were added later without regenerating the migration."
    echo "  Fix: Create a new migration that covers ALL missing entities."
    exit 1
  fi
fi
```

### POST-CHECK 37: I18n namespace files must be registered in i18n config (BLOCKING)

```bash
# Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
# registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
# got empty translations at runtime.
I18N_CONFIG=$(find src/ web/ -path "*/i18n/config.ts" -o -path "*/i18n/index.ts" -o -path "*/i18n/i18n.ts" 2>/dev/null | grep -v node_modules | head -1)
if [ -n "$I18N_CONFIG" ]; then
  # Find all module JSON files in the primary language (fr)
  FR_FILES=$(find src/ web/ -path "*/i18n/locales/fr/*.json" 2>/dev/null | grep -v node_modules | grep -v common.json | grep -v navigation.json)
  if [ -n "$FR_FILES" ]; then
    FAIL=false
    for JSON_FILE in $FR_FILES; do
      NS=$(basename "$JSON_FILE" .json)
      # Check if namespace is referenced in config (import or resource key)
      if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
        echo "BLOCKING: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
        echo "  Pages using useTranslation(['$NS']) will get empty translations at runtime"
        echo "  Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
        FAIL=true
      fi
    done
    if [ "$FAIL" = true ]; then
      exit 1
    fi
  fi
fi
```

### POST-CHECK 38: FluentValidation validators must be registered via DI (BLOCKING)

```bash
# Root cause (test-apex-007): Validators existed but were never registered in DI.
# Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
if [ -n "$VALIDATOR_FILES" ]; then
  # Check DI registration file exists
  DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
  if [ -z "$DI_FILE" ]; then
    echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
    exit 1
  fi
  # Check for AddValidatorsFromAssembly or individual validator registration
  HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
  if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
    # Check individual registrations as fallback
    VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
    REGISTERED_COUNT=0
    for VF in $VALIDATOR_FILES; do
      VN=$(basename "$VF" .cs)
      if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
        REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
      fi
    done
    if [ "$REGISTERED_COUNT" -eq 0 ]; then
      echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
      echo "  Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
      echo "  Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
      exit 1
    fi
  fi
fi
```

### POST-CHECK 39: Date/date properties in DTOs must use DateOnly, not string (BLOCKING)

```bash
# Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
# This causes: invalid date parsing, no date validation, inconsistent formats across clients.
DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
if [ -n "$DTO_FILES" ]; then
  FAIL=false
  for f in $DTO_FILES; do
    # Find string properties whose name contains "Date" (case-insensitive)
    BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
    if [ -n "$BAD_DATES" ]; then
      echo "BLOCKING: DTO has string type for date field — must use DateOnly: $f"
      echo "$BAD_DATES"
      echo "  Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
      echo "  DateOnly is the correct .NET type for date-only values (no time component)"
      FAIL=true
    fi
  done
  if [ "$FAIL" = true ]; then
    exit 1
  fi
fi
```

### POST-CHECK 40: Every module with entities must have a migration covering them (BLOCKING)

```bash
# Complementary to POST-CHECK 36 — checks from the entity side.
# Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
  MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
  if [ -z "$MIGRATION_FILES" ]; then
    echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
    exit 1
  fi
  FAIL=false
  for EF in $ENTITY_FILES; do
    ENTITY_NAME=$(basename "$EF" .cs)
    # Skip abstract base classes and interfaces
    if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
    # Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
    FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
    if [ -z "$FOUND" ]; then
      echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
      echo "  This entity will NOT have a database table."
      echo "  Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
      FAIL=true
    fi
  done
  if [ "$FAIL" = true ]; then
    exit 1
  fi
fi
```

### POST-CHECK 41: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)

```bash
# Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
# In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
# [Route] is standard ASP.NET Core static routing. When both exist:
# - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
# - [Route] may or may not take over depending on middleware order
# - Result: 404 on ALL endpoints
# The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  FAIL=false
  for f in $CTRL_FILES; do
    HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
    HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
    if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
      NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
      ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
      echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
      echo "  Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
      echo "  In SmartStack, [NavRoute] resolves routes dynamically from the database."
      echo "  Having [Route] alongside it causes route conflicts and 404s."
      echo "  Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
      FAIL=true
    fi
  done
  if [ "$FAIL" = true ]; then
    exit 1
  fi
fi
```

### POST-CHECK 42: RolesSeedData must map standard role-permission matrix (BLOCKING)

```bash
# SmartStack standard role-permission matrix:
#   Admin    = wildcard (*) — full access
#   Manager  = CRU (read + create + update) — no delete
#   Contributor = CR (read + create) — no update, no delete
#   Viewer   = R (read only)
# If RolesSeedData deviates from this matrix, the RBAC model is broken.
ROLE_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" ! -name "ApplicationRolesSeedData.cs" 2>/dev/null)
if [ -n "$ROLE_SEED_FILES" ]; then
  FAIL=false
  for f in $ROLE_SEED_FILES; do
    # Skip ApplicationRolesSeedData (defines roles, not mappings)
    BASENAME=$(basename "$f")
    if [ "$BASENAME" = "ApplicationRolesSeedData.cs" ]; then continue; fi

    # Check Admin has wildcard
    HAS_ADMIN_WILDCARD=$(grep -Pc '(admin|Admin).*\*' "$f" 2>/dev/null)
    if [ "$HAS_ADMIN_WILDCARD" -eq 0 ]; then
      # Also accept .Access or wildcard pattern
      HAS_ADMIN_ACCESS=$(grep -Pc '(admin|Admin).*(Access|Wildcard|IsWildcard)' "$f" 2>/dev/null)
      if [ "$HAS_ADMIN_ACCESS" -eq 0 ]; then
        echo "BLOCKING: Admin role missing wildcard (*) permission in $f"
        echo "Fix: Admin must map to wildcard permission (navRoute.*) or use IsWildcard=true"
        FAIL=true
      fi
    fi

    # Check Viewer has NO delete/create/update
    VIEWER_WRITE=$(grep -Pc '(viewer|Viewer).*(\.delete|\.create|\.update|Delete|Create|Update)' "$f" 2>/dev/null)
    if [ "$VIEWER_WRITE" -gt 0 ]; then
      echo "BLOCKING: Viewer role has write permissions (create/update/delete) in $f"
      echo "Fix: Viewer must only have read permission. Remove create/update/delete mappings."
      FAIL=true
    fi

    # Check Manager has NO delete
    MANAGER_DELETE=$(grep -Pc '(manager|Manager).*(\.delete|Delete)' "$f" 2>/dev/null)
    if [ "$MANAGER_DELETE" -gt 0 ]; then
      echo "WARNING: Manager role has delete permission in $f"
      echo "SmartStack standard: Manager = CRU (no delete). Verify this is intentional."
    fi
  done
  if [ "$FAIL" = true ]; then
    exit 1
  fi
fi
```

### POST-CHECK 43: PermissionAction enum must use valid typed values only (BLOCKING)

```bash
# Valid PermissionAction enum values: Access(0), Read(1), Create(2), Update(3), Delete(4),
# Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)
# FORBIDDEN: Enum.Parse<PermissionAction>("...") — runtime crash if value doesn't exist
# FORBIDDEN: (PermissionAction)99 or any cast beyond 0-10
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
if [ -n "$SEED_FILES" ]; then
  FAIL=false
  for f in $SEED_FILES; do
    # Check for Enum.Parse<PermissionAction> usage
    ENUM_PARSE=$(grep -Pn 'Enum\.Parse<PermissionAction>' "$f" 2>/dev/null)
    if [ -n "$ENUM_PARSE" ]; then
      echo "BLOCKING: Enum.Parse<PermissionAction> detected — runtime crash risk: $f"
      echo "$ENUM_PARSE"
      echo "Fix: Use typed enum directly: PermissionAction.Read (NOT Enum.Parse<PermissionAction>(\"Read\"))"
      FAIL=true
    fi

    # Check for invalid cast values (PermissionAction)N where N > 10
    INVALID_CAST=$(grep -Pn '\(PermissionAction\)\s*([1-9]\d{1,}|[2-9]\d)' "$f" 2>/dev/null)
    if [ -n "$INVALID_CAST" ]; then
      echo "BLOCKING: Invalid PermissionAction cast detected (value > 10): $f"
      echo "$INVALID_CAST"
      echo "Valid values: Access(0), Read(1), Create(2), Update(3), Delete(4), Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)"
      FAIL=true
    fi
  done
  if [ "$FAIL" = true ]; then
    exit 1
  fi
fi
```

### POST-CHECK 44: Navigation translation completeness — 4 languages per level (BLOCKING)

```bash
# Every navigation seed data file must provide translations for ALL 4 languages (fr, en, it, de).
# If sections exist (GetSectionEntries), GetSectionTranslationEntries MUST also exist.
# If resources exist (GetResourceEntries), resource translation entries MUST also exist.
NAV_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
if [ -n "$NAV_SEED_FILES" ]; then
  FAIL=false
  for f in $NAV_SEED_FILES; do
    # Check module translations have all 4 languages
    LANG_COUNT=$(grep -c 'LanguageCode\s*=' "$f" 2>/dev/null)
    HAS_FR=$(grep -c '"fr"' "$f" 2>/dev/null)
    HAS_EN=$(grep -c '"en"' "$f" 2>/dev/null)
    HAS_IT=$(grep -c '"it"' "$f" 2>/dev/null)
    HAS_DE=$(grep -c '"de"' "$f" 2>/dev/null)

    if [ "$HAS_FR" -eq 0 ] || [ "$HAS_EN" -eq 0 ] || [ "$HAS_IT" -eq 0 ] || [ "$HAS_DE" -eq 0 ]; then
      echo "BLOCKING: Missing language(s) in navigation translations: $f"
      echo "  fr=$HAS_FR, en=$HAS_EN, it=$HAS_IT, de=$HAS_DE (all must be > 0)"
      echo "Fix: Add NavigationTranslationSeedEntry for all 4 languages (fr, en, it, de)"
      FAIL=true
    fi

    # If sections exist, section translations MUST exist
    HAS_SECTION_ENTRIES=$(grep -c 'GetSectionEntries' "$f" 2>/dev/null)
    HAS_SECTION_TRANSLATIONS=$(grep -c 'GetSectionTranslationEntries' "$f" 2>/dev/null)
    if [ "$HAS_SECTION_ENTRIES" -gt 0 ] && [ "$HAS_SECTION_TRANSLATIONS" -eq 0 ]; then
      echo "BLOCKING: Sections defined but GetSectionTranslationEntries() missing: $f"
      echo "Fix: Add GetSectionTranslationEntries() with 4 languages per section (ref core-seed-data.md §2b)"
      FAIL=true
    fi

    # If resources exist, resource translations MUST exist
    HAS_RESOURCE_ENTRIES=$(grep -c 'GetResourceEntries' "$f" 2>/dev/null)
    HAS_RESOURCE_TRANSLATIONS=$(grep -Pc 'ResourceTranslation|GetResourceTranslation|NavigationEntityType\.Resource.*LanguageCode' "$f" 2>/dev/null)
    if [ "$HAS_RESOURCE_ENTRIES" -gt 0 ] && [ "$HAS_RESOURCE_TRANSLATIONS" -eq 0 ]; then
      echo "BLOCKING: Resources defined but resource translations missing: $f"
      echo "Fix: Add resource translation entries with 4 languages per resource (ref core-seed-data.md §2b)"
      FAIL=true
    fi
  done
  if [ "$FAIL" = true ]; then
    exit 1
  fi
fi
```

---

## Architecture — Clean Architecture Layer Isolation

### POST-CHECK A1: Domain must not import other layers (BLOCKING)

```bash
DOMAIN_FILES=$(find src/ -path "*/Domain/*" -name "*.cs" 2>/dev/null)
if [ -n "$DOMAIN_FILES" ]; then
  BAD_IMPORTS=$(grep -Pn 'using\s+[\w.]*\.(Application|Infrastructure|Api)[\w.]*;' $DOMAIN_FILES 2>/dev/null)
  if [ -n "$BAD_IMPORTS" ]; then
    echo "BLOCKING: Domain layer imports Application/Infrastructure/Api — violates Clean Architecture"
    echo "Domain is the core, it must not depend on any other layer"
    echo "$BAD_IMPORTS"
    echo "Fix: Move shared types to Domain or remove the dependency"
    exit 1
  fi
fi
```

### POST-CHECK A2: Application must not import Infrastructure or Api (BLOCKING)

```bash
APP_FILES=$(find src/ -path "*/Application/*" -name "*.cs" 2>/dev/null)
if [ -n "$APP_FILES" ]; then
  BAD_IMPORTS=$(grep -Pn 'using\s+[\w.]*\.(Infrastructure|Api)[\w.]*;' $APP_FILES 2>/dev/null)
  if [ -n "$BAD_IMPORTS" ]; then
    echo "BLOCKING: Application layer imports Infrastructure/Api — violates Clean Architecture"
    echo "Application defines interfaces, Infrastructure implements them"
    echo "$BAD_IMPORTS"
    echo "Fix: Define an interface in Application and implement it in Infrastructure"
    exit 1
  fi
fi
```

### POST-CHECK A3: Controllers must not inject DbContext (BLOCKING)

```bash
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  BAD_DBCONTEXT=$(grep -Pn 'private\s+readonly\s+\w*DbContext|DbContext\s+\w+[,)]' $CTRL_FILES 2>/dev/null)
  if [ -n "$BAD_DBCONTEXT" ]; then
    echo "BLOCKING: Controller injects DbContext directly — violates Clean Architecture"
    echo "Controllers must use Application services, not access the database directly"
    echo "$BAD_DBCONTEXT"
    echo "Fix: Create an Application service with the required business logic and inject it instead"
    exit 1
  fi
fi
```

### POST-CHECK A4: API must return DTOs, not Domain entities (WARNING)

```bash
# Scan controller return types for Domain entity names without Dto/Response/ViewModel suffix
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ] && [ -n "$ENTITY_FILES" ]; then
  ENTITY_NAMES=$(grep -ohP 'public\s+class\s+(\w+)\s*:' $ENTITY_FILES 2>/dev/null | grep -oP '\w+(?=\s*:)' | grep -v '^public$' | sort -u)
  for ENTITY in $ENTITY_NAMES; do
    BAD_RETURN=$(grep -Pn "ActionResult<$ENTITY>|ActionResult<IEnumerable<$ENTITY>>|ActionResult<List<$ENTITY>>" $CTRL_FILES 2>/dev/null)
    if [ -n "$BAD_RETURN" ]; then
      echo "WARNING: Controller returns Domain entity '$ENTITY' instead of a DTO"
      echo "$BAD_RETURN"
      echo "Fix: Return ${ENTITY}ResponseDto instead of $ENTITY"
    fi
  done
fi
```

### POST-CHECK A5: Service interfaces in Application, implementations in Infrastructure (WARNING)

```bash
# Check for service implementations (non-interface classes) in Application or Api layers
APP_SERVICES=$(find src/ -path "*/Application/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$APP_SERVICES" ]; then
  for f in $APP_SERVICES; do
    if grep -q 'public class.*Service' "$f" 2>/dev/null; then
      echo "WARNING: Service implementation found in Application layer: $f"
      echo "Fix: Move implementation to Infrastructure/Services/. Application should only contain interfaces."
    fi
  done
fi

# Check for service interfaces (I*Service) in Domain or Api layers
DOMAIN_INTERFACES=$(find src/ -path "*/Domain/*" -name "I*Service.cs" 2>/dev/null)
API_INTERFACES=$(find src/ -path "*/Api/*" -name "I*Service.cs" 2>/dev/null)
for f in $DOMAIN_INTERFACES $API_INTERFACES; do
  if [ -n "$f" ] && grep -q 'public interface.*Service' "$f" 2>/dev/null; then
    echo "WARNING: Service interface found outside Application layer: $f"
    echo "Fix: Move to Application/Interfaces/"
  fi
done
```

### POST-CHECK A6: No EF Core attributes in Domain entities (BLOCKING)

```bash
DOMAIN_FILES=$(find src/ -path "*/Domain/*" -name "*.cs" 2>/dev/null)
if [ -n "$DOMAIN_FILES" ]; then
  BAD_EF=$(grep -Pn '\[Table\(|\[Column\(|\[Index\(|using\s+Microsoft\.EntityFrameworkCore' $DOMAIN_FILES 2>/dev/null)
  if [ -n "$BAD_EF" ]; then
    echo "BLOCKING: EF Core attributes or using directives found in Domain layer"
    echo "Domain entities must be persistence-ignorant — EF configuration belongs in Infrastructure"
    echo "$BAD_EF"
    echo "Fix: Move [Table], [Column], [Index] to IEntityTypeConfiguration<T> in Infrastructure/Persistence/Configurations/"
    exit 1
  fi
fi
```

### POST-CHECK A7: No direct repository usage in controllers (WARNING)

```bash
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  BAD_REPO=$(grep -Pn 'IRepository<|IGenericRepository<|private\s+readonly\s+IRepository|private\s+readonly\s+IGenericRepository' $CTRL_FILES 2>/dev/null)
  if [ -n "$BAD_REPO" ]; then
    echo "WARNING: Controller injects repository directly — should use Application services"
    echo "$BAD_REPO"
    echo "Fix: Controllers should depend on Application services (I*Service), not repositories"
  fi
fi
```

---

## Summary

After running all checks above, report the total count:
- **N BLOCKING** issues must be fixed before committing
- **M WARNING** issues should be reviewed but are non-blocking

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