# BLOCKING POST-CHECKs

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

### 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: \"/business/human-resources\" NOT \"humanresources\""
    exit 1
  fi
fi
```

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

```bash
# Find all service implementation files
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  # Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
  for f in $SERVICE_FILES; do
    # Accept either _currentTenant.TenantId (strict guard clause or nullable usage)
    # or entities with IOptionalTenantEntity/IScopedTenantEntity (optional tenant pattern)
    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"
      echo "OR work with entities that implement IOptionalTenantEntity/IScopedTenantEntity"
      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 3: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)

```bash
# Find all controller files
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    # Check controller has at least one RequirePermission attribute
    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 4: Seed data must not use Guid.NewGuid()

```bash
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
if [ -n "$SEED_FILES" ]; then
  BAD_GUIDS=$(grep -n "Guid.NewGuid()" $SEED_FILES 2>/dev/null)
  if [ -n "$BAD_GUIDS" ]; then
    echo "BLOCKING: Seed data must use deterministic GUIDs (SHA256), not Guid.NewGuid()"
    echo "$BAD_GUIDS"
    exit 1
  fi
fi
```

### POST-CHECK 5: 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
    # Accept either ICurrentTenantService or ICurrentUser (legacy) for tenant context
    if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
      echo "BLOCKING: Service missing tenant context injection: $f"
      echo "All services MUST inject ICurrentTenantService for tenant isolation"
      echo "Pattern: private readonly ICurrentTenantService _currentTenant;"
      exit 1
    fi
  done
fi
```

### POST-CHECK 6: 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 7: Pages must use lazy loading (no static page imports in routes)

```bash
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
if [ -n "$ROUTE_FILES" ]; then
  STATIC_PAGE_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" $ROUTE_FILES 2>/dev/null)
  if [ -n "$STATIC_PAGE_IMPORTS" ]; then
    echo "BLOCKING: Route files must use React.lazy() for page imports, not static imports"
    echo "$STATIC_PAGE_IMPORTS"
    echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })));"
    exit 1
  fi
fi
```

### POST-CHECK 8: 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 9: 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 10: 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 11: 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 12: 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 13: 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
```

### POST-CHECK 14: Routes seed data must match frontend contextRoutes

```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 contextRoutes entry in App.tsx"
      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 15: 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_FILTERS=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
  if [ -n "$BAD_FILTERS" ]; then
    echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty instead of runtime tenant isolation"
    echo "$BAD_FILTERS"
    echo ""
    echo "Anti-pattern: .HasQueryFilter(e => e.TenantId != Guid.Empty)"
    echo "Fix: Remove HasQueryFilter. Tenant isolation is handled by SmartStack base DbContext"
    exit 1
  fi
fi
```

### POST-CHECK 16: 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 17: 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 18: 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 19: SeedConstants must NOT contain ContextId (pre-seeded by SmartStack core)

```bash
# NavigationContext IDs (business, platform, personal) are pre-seeded by SmartStack core
# with hardcoded GUIDs. Client code MUST look them up by code at runtime, NEVER generate them.
SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
if [ -n "$SEED_CONST_FILES" ]; then
  BAD_CONTEXT_ID=$(grep -Pn 'ContextId\s*=' $SEED_CONST_FILES 2>/dev/null)
  if [ -n "$BAD_CONTEXT_ID" ]; then
    echo "BLOCKING: SeedConstants must NOT contain a ContextId constant"
    echo "NavigationContext IDs are pre-seeded by SmartStack core with hardcoded GUIDs"
    echo "Fix: Remove ContextId from SeedConstants. In SeedDataProvider, query:"
    echo "  var ctx = await db.NavigationContexts.FirstOrDefaultAsync(c => c.Code == \"business\", ct);"
    echo "$BAD_CONTEXT_ID"
    exit 1
  fi
fi
if [ -n "$SEED_ALL_FILES" ]; then
  BAD_CTX_GUID=$(grep -Pn 'DeterministicGuid\("nav:(business|platform|personal)"\)' $SEED_ALL_FILES 2>/dev/null)
  if [ -n "$BAD_CTX_GUID" ]; then
    echo "BLOCKING: Deterministic GUID for NavigationContext detected"
    echo "Context IDs (business, platform, personal) are pre-seeded by SmartStack core"
    echo "Fix: Look up context by code at runtime in SeedDataProvider.SeedNavigationAsync()"
    echo "$BAD_CTX_GUID"
    exit 1
  fi
fi
```

### POST-CHECK 20: 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 21: Services must NOT use TenantId!.Value (null-forgiving crash pattern)

```bash
# The !.Value pattern on Guid? throws InvalidOperationException (500) instead of clean 401
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!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
  if [ -n "$BAD_PATTERN" ]; then
    echo "BLOCKING: Services use TenantId!.Value — causes 500 instead of 400 when tenant context is missing"
    echo "$BAD_PATTERN"
    echo ""
    echo "Fix: Replace with guard clause at the start of every method:"
    echo "  var tenantId = _currentTenant.TenantId"
    echo "      ?? throw new TenantContextRequiredException();"
    echo ""
    echo "This produces a clean 400 Bad Request via GlobalExceptionHandlerMiddleware."
    echo "NEVER use UnauthorizedAccessException for tenant context — it returns 401 which clears the frontend token."
    exit 1
  fi
fi

# POST-CHECK: Services must NOT use UnauthorizedAccessException for tenant context (causes token clearing)
if [ -n "$SERVICE_FILES" ]; then
  BAD_UNAUTH=$(grep -Pn 'UnauthorizedAccessException.*[Tt]enant' $SERVICE_FILES 2>/dev/null)
  if [ -n "$BAD_UNAUTH" ]; then
    echo "BLOCKING: Services use UnauthorizedAccessException for tenant context — causes 401 which clears the frontend token"
    echo "$BAD_UNAUTH"
    echo ""
    echo "Fix: Replace with:"
    echo "  var tenantId = _currentTenant.TenantId"
    echo "      ?? throw new TenantContextRequiredException();"
    echo ""
    echo "TenantContextRequiredException returns 400 Bad Request (does not clear token)."
    echo "UnauthorizedAccessException returns 401 Unauthorized (clears token + redirects to login)."
    exit 1
  fi
fi
```

### POST-CHECK 22: 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 22: OK"
```

### POST-CHECK 23: 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 23: OK"
```

### POST-CHECK 24: 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 25: 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 contextRoutes"
      echo "Fix: Add section child routes to the module's children array in App.tsx"
    fi
  done
fi

# Controllers with section-level [NavRoute] (4 segments) 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 4 dot-separated segments (section-level)
    SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z]+\.[a-z]+\.[a-z]+\.[a-z]+"\)' "$f" 2>/dev/null)
    if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
      echo "BLOCKING: Section 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 4-segment navRoute"
    fi
  done
fi
```

### POST-CHECK 26: 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 27: 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: 3 dots (context.app.module.action = 4 segments = 3+1)
      # Section permissions: 4 dots (context.app.module.section.action = 5 segments = 4+1)
      # Wildcard: ends with .* (valid at any level)
      if echo "$PATH_VAL" | grep -qP '\.\*$'; then
        continue  # Wildcards are valid
      elif [ "$DOTS" -lt 3 ] || [ "$DOTS" -gt 5 ]; 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 28: 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 29: 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 30: 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 31: 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 32: 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 33: 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
```

### POST-CHECK 34: 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 35: 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 36: 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 37: 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 38: 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 (deterministic 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 deterministic GUID as SectionId in $PROVIDER"
    echo "NavigationSection.Create() generates its own ID — deterministic seed 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 39: Controllers must NOT have [Route] alongside [NavRoute] (BLOCKING)

```bash
# [NavRoute] REPLACES [Route] — it resolves HTTP routes from the navigation DB at startup.
# Having both is redundant and may cause route conflicts.
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    HAS_NAVROUTE=$(grep -P '\[NavRoute\(' "$f" 2>/dev/null)
    HAS_ROUTE=$(grep -P '\[Route\(' "$f" 2>/dev/null)
    if [ -n "$HAS_NAVROUTE" ] && [ -n "$HAS_ROUTE" ]; then
      echo "BLOCKING: Controller has both [Route] and [NavRoute] — [NavRoute] replaces [Route]: $f"
      echo "  Found [NavRoute]: $HAS_NAVROUTE"
      echo "  Found [Route]:    $HAS_ROUTE"
      echo "Fix: Remove the [Route(\"api/...\")] attribute. [NavRoute] resolves routes from navigation DB at startup."
      exit 1
    fi
  done
fi
```

### POST-CHECK 40: 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: "business.support-client.my-tickets", "platform.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): 'business.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 41: 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: "business.support-client.my-tickets.read" (kebab-case everywhere)
# FORBIDDEN: "business.humanresources.employees.read" — must be "business.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: 'business.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 42: 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 43: 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
```

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