---
name: step-05a-handoff
description: Build development handoff data, map business rules to code, write handoff to feature.json
model: sonnet
next_step: steps/step-05b-deploy.md
---

> **Context files:** `_shared.md` | `_architecture.md`

# Step 5a: Handoff - Build & Write

## MANDATORY EXECUTION RULES

- **ALWAYS** verify status = "consolidated" before proceeding
- **ALWAYS** ask user for implementation strategy preference (multi-module only)
- **ALWAYS** derive prd.json from feature.json (NEVER independently)
- **NEVER** invent entities/FRs/BRs not in feature.json
- **ALL** API routes from specification.apiEndpoints (exact copy)
- **Permission** paths from specification.permissionMatrix (full format)
- **ALWAYS** generate CORE SeedData entries: 2 app-level (NavigationApplication + ApplicationRoles) + per module (NavigationModule + NavigationSections + Permissions + Roles)

## YOUR TASK

Build the development handoff data: verify consolidation, choose implementation strategy, calculate complexity, map specification to files (7 categories), map business rules to code, generate API endpoint summary, and write handoff to EACH module feature.json.

---

## EXECUTION SEQUENCE

### 1. Verify Consolidation Passed

Use ba-reader to locate the feature and verify consolidation status:

```
// Determine feature source based on workflow mode
IF workflow.mode === "project":
  projectFeature = ba-reader.findProjectFeature()
  // Verify ALL applications are specified
  FOR each app in projectFeature.applications:
    appFeature = ba-reader.findFeature(app.featureJsonPath)
    IF appFeature.status !== "consolidated":
      BLOCKING ERROR: "Application {app.code} not consolidated (status: {appFeature.status})"
      → Return to step-04a-collect.md
  // Use project-level feature for overall consolidation
  feature = projectFeature
ELSE:
  ba-reader.findFeature({feature_id})
  → Check status = "consolidated"
```

**IF** status ≠ "consolidated" → **STOP**. Return to step-04a-collect.md.

Display validation summary:

```
✓ Consolidation: APPROVED
✓ Applications: {app_count} (project mode only)
✓ Modules: {count} specified and validated
✓ Cross-module: interactions mapped
✓ Cross-application: interactions mapped (project mode only)
✓ Permissions: coherent
→ Proceeding to handoff...
```

Include:
- Number of applications (project mode only)
- Number of modules across all applications
- Number of entities across all modules
- Number of use cases across all modules
- Number of business rules across all modules
- Cross-module interaction count
- Cross-application interaction count (project mode only)

---

### 1b. Cache Warming for Handoff & Deploy

> **Performance Optimization:** Pre-load handoff templates and references to reduce redundant reads during handoff generation.
> This step runs ONCE at start of step-05a, files retained through step-05b.

```
// Pre-load handoff & deploy references (Bucket 5)
const handoffRefs = [
  "~/.claude/skills/business-analyse/references/handoff-file-templates.md",
  "~/.claude/skills/business-analyse/references/handoff-mappings.md",
  "~/.claude/skills/business-analyse/references/deploy-data-build.md",
  "~/.claude/skills/business-analyse/references/deploy-modes.md",
  "~/.claude/skills/business-analyse/references/html-data-mapping.md"
];

for (const file of handoffRefs) {
  read(file);  // Pre-load into cache
}

// Pre-load HTML template (large file, loaded once)
read("~/.claude/skills/business-analyse/html/ba-interactive.html");

Display: "✓ Cache warmed: handoff templates (139KB, 6 files)"
Display: "  Expected token savings: ~3,000 tokens (handoff refs read 2× → 1×)"
Display: "  Retention: through step-05b (deploy)"
```

**Rationale:**

- Handoff templates are read 2× during step-05a + step-05b (without caching)
- HTML template (85KB) is read once but benefits from explicit pre-load (faster deploy)
- Token savings: ~3,000 tokens for handoff process
- Cache retained until session end (needed for both handoff and deploy steps)

**Why NOT loaded at step-00:**
- Large files (139KB total), especially ba-interactive.html (85KB)
- Only used at END of workflow (steps 05a, 05b)
- Low re-use (1-2× each)
- Loading at step-05a is more efficient (just-in-time caching)

See [references/cache-warming-strategy.md](../references/cache-warming-strategy.md) § Bucket 5 for details.

---

### 2. Implementation Strategy Choice (Multi-Module/Multi-App)

**IF** more than 1 module defined in feature.json (or project mode with multiple applications):

Ask via AskUserQuestion:

```
question: "Quelle stratégie d'implémentation préférez-vous ?"
header: "Stratégie d'implémentation"
options:
  - label: "Module par module (Recommandé)"
    description: "Implémenter chaque module complètement avant de passer au suivant. Suit l'ordre topologique des dépendances."
  - label: "Couche par couche"
    description: "Implémenter toutes les entités, puis tous les services, puis tous les contrôleurs, etc. Plus de parallélisation possible mais plus complexe."
  - label: "Hybride"
    description: "Modules fondation en premier (couche par couche), puis modules dépendants (module par module)"
  - label: "Application par application (Recommandé multi-app)"
    description: "Implémenter chaque application complètement avant de passer à la suivante. Suit l'ordre topologique inter-applications."
```

> **Note:** "Application par application" only appears for project mode with 2+ applications.

Store the chosen strategy in `handoff.implementationStrategy`.

**IF** only 1 module → default to "Module par module" (no choice needed).

---

### 3. Calculate Complexity

For each module in feature.json.modules[], calculate:

```json
{
  "complexity": "simple|medium|complex",
  "complexityDetails": {
    "entities": {count},
    "useCases": {count},
    "businessRules": {count},
    "calculated": "{level} (≤X entities, ≤Y UCs, ≤Z BRs)"
  }
}
```

| Criteria | Simple | Medium | Complex |
|----------|--------|--------|---------|
| Entities | ≤3 | ≤6 | >6 |
| Use Cases | ≤5 | ≤12 | >12 |
| Business Rules | ≤10 | ≤20 | >20 |

**Global complexity** = highest complexity across all modules.

Example:
- Module A: 4 entities, 6 use cases, 8 BRs → **simple**
- Module B: 8 entities, 15 use cases, 25 BRs → **complex**
- Overall: **complex**

---

### 4. Map Specification to Files

> **IMPORTANT:** Generate `handoff.filesToCreate` as structured object with 7 categories.
> Each file entry is `{path, type, linkedFRs, linkedUCs, module}`. NO free text.
> Read `.smartstack/config.json` to extract baseNamespace for project namespace.

**Order by topological dependency** (modules with no deps first, then dependents).

For **EACH module** in topological order:

> **PATH HIERARCHY CONVENTION:** All backend file paths MUST include the application hierarchy.
> Derive from module feature.json metadata:
> - `{ApplicationName}` = `metadata.application` (e.g., `"HumanResources"`)
> - `{ModuleName}` = `metadata.module` (e.g., `"Projects"`)
>
> **Example** for application=HumanResources, module=Projects:
> - Domain: `src/Domain/Entities/HumanResources/Projects/Project.cs`
> - Application: `src/Application/Services/HumanResources/Projects/ProjectService.cs`
> - Infrastructure: `src/Infrastructure/Persistence/Configurations/HumanResources/Projects/ProjectConfiguration.cs`
> - API: `src/API/Controllers/HumanResources/ProjectsController.cs`
> - Tests: `src/Tests/Unit/Domain/HumanResources/Projects/ProjectTests.cs`

#### 4.1-4.7 File Category Templates

See [references/handoff-file-templates.md](../references/handoff-file-templates.md) for the complete JSON templates for all 7 categories:

| Category | Source | Key rules |
|----------|--------|-----------|
| **4.1 Domain** | `analysis.entities[]` | Entities, ValueObjects, Enums, Exceptions |
| **4.2 Application** | `analysis.useCases[]` | Services, DTOs, Validators |
| **4.3 Infrastructure** | `analysis.entities[]` | EF Configurations, DbSet, DI. **DEPENDENCY:** Each EF config task MUST `dependsOn` its corresponding domain entity task. |
| **4.4 API** | `specification.apiEndpoints[]` | Controllers |
| **4.5 Frontend** | `specification.uiWireframes[]` | Pages, Components, Hooks + wireframe traceability |
| **4.6 SeedData** | `specification.seedDataCore` | 2 app-level + per module CORE (NavigationModule + NavigationSections + Permissions + Roles) + business + IClientSeedDataProvider |
| **4.7 Tests** | All layers | Unit, Integration, Security tests |

#### Route Convention (CRITICAL)

> **MANDATORY:** All NavigationModule/Section routes MUST use kebab-case transformation.
> See [references/naming-conventions.md](../references/naming-conventions.md) for complete guide.

**Problem:**
- Module codes in feature.json are **PascalCase** (e.g., `"HumanResources"`)
- Routes in seed data MUST be **kebab-case** (e.g., `"/human-resources"`)
- Failing to transform causes 404 when clicking menu items

**Solution:**

When generating `seedDataCore` for handoff, transform PascalCase codes to kebab-case routes:

```javascript
// Transform module codes to kebab-case routes
function toKebabCase(code) {
  return code
    .replace(/([a-z])([A-Z])/g, '$1-$2')
    .toLowerCase();
}

// Example usage in seedDataCore generation
const moduleCode = "TimeManagement";     // PascalCase (C#)
const appCode = "HumanResources";       // PascalCase (C#)

const route = `/${toKebabCase(appCode)}/${toKebabCase(moduleCode)}`;
// Result: "/human-resources/time-management"
```

**Validation:** All kebab-case routes in seed data MUST NOT include `/business/` prefix.

Example in NavigationModule:
```csharp
route = "/human-resources";  // ✓ Correct (kebab-case module code only)
route = "/business/human-resources";  // ✗ WRONG (contains /business/)
```

**Include in task instructions:**

For NavigationSeedData tasks in `handoff.filesToCreate`, add:

```json
{
  "path": "Infrastructure/Persistence/Seeding/Data/{Module}/NavigationSeedData.cs",
  "type": "SeedData",
  "category": "core",
  "instructions": "Generate NavigationModule seed data. CRITICAL: Use ToKebabCase() helper for Route property. Example: Route = ToKebabCase($\"/{app}/{module}\"). See core-seed-data.md template."
}
```

**Validation:**
- ralph-loop POST-CHECK validates routes after generation (see step-02-execute.md)
- MCP `validate_frontend_routes` detects case mismatches
- Convention documented in naming-conventions.md

**Critical rules:**
- All backend paths include `{ApplicationName}/` hierarchy
- Frontend pages MUST have `linkedWireframes[]` + `wireframeAcceptanceCriteria`
- SeedData: 2 app-level CORE (NavigationApplication + ApplicationRoles) + per-module CORE (NavigationModule + NavigationSections + Permissions + Roles) + IClientSeedDataProvider for client projects
- **Acceptance Criteria Mapping:** Each task's `acceptanceCriteria` MUST be derived from its own `linkedFRs[]` entries (lookup FR → `acceptanceCriteria`). NEVER map by sequential FR index — use the task's explicit linkedFRs to resolve the correct criteria.
- Path convention: `Persistence/Seeding/Data/` (NEVER `Data/SeedData/`)

---

### 5. Map Business Rules to Code

See [references/handoff-mappings.md](../references/handoff-mappings.md) — Section "Business Rules to Code Mapping".

For each BR in `analysis.businessRules[]` of EACH module, generate:
- **ruleId**, **title**, **module**, **severity** (critical/high/medium/low)
- **implementationPoints**: Array of `{layer, component, method, implementation}` across Domain, Application, Infrastructure, API, Frontend

---

### 6. API Endpoint Summary

> **ABSOLUTE RULE:** Copy **EXACTLY** from `specification.apiEndpoints[]`. **NEVER** reinvent routes.

See [references/handoff-mappings.md](../references/handoff-mappings.md) — Section "API Endpoint Summary".

For each endpoint: operation, method, route, linkedUC, linkedFR, permissions, requestSchema, responseSchema, errorCodes, module.

Total endpoints = count of specification.apiEndpoints[] across all modules.

### 6b. Frontend Task Splitting (MANDATORY)

> **CRITICAL:** Frontend MUST be split into discrete tasks, NOT bundled as a single mega-task.
> A single mega-task covering 5-8 files prevents granular error recovery in ralph-loop.

For each module, generate these SEPARATE frontend file entries (not one monolithic entry):

```json
"frontend": [
  { "path": "src/pages/{App}/{Mod}/{ListPage}Page.tsx", "type": "Page", "linkedWireframes": ["{module}-list"], "module": "{moduleCode}", "skill": "/ui-components" },
  { "path": "src/pages/{App}/{Mod}/{Entity}CreatePage.tsx", "type": "Page", "linkedWireframes": ["{module}-create"], "module": "{moduleCode}", "skill": "/ui-components", "fkFields": ["EmployeeId→Employee", "DepartmentId→Department"] },
  { "path": "src/pages/{App}/{Mod}/{Entity}EditPage.tsx", "type": "Page", "linkedWireframes": ["{module}-edit"], "module": "{moduleCode}", "skill": "/ui-components", "fkFields": ["EmployeeId→Employee", "DepartmentId→Department"] },
  { "path": "src/pages/{App}/{Mod}/{DetailPage}Page.tsx", "type": "Page", "linkedWireframes": ["{module}-detail"], "module": "{moduleCode}", "skill": "/ui-components" },
  { "path": "src/hooks/use{Module}.ts", "type": "Hook", "module": "{moduleCode}" },
  { "path": "src/i18n/locales/fr/{moduleLower}.json", "type": "I18n", "language": "fr", "module": "{moduleCode}" },
  { "path": "src/i18n/locales/en/{moduleLower}.json", "type": "I18n", "language": "en", "module": "{moduleCode}" },
  { "path": "src/i18n/locales/it/{moduleLower}.json", "type": "I18n", "language": "it", "module": "{moduleCode}" },
  { "path": "src/i18n/locales/de/{moduleLower}.json", "type": "I18n", "language": "de", "module": "{moduleCode}" }
]
```

**FK fields in forms:** Create/Edit page entries MUST include `"fkFields"` array listing FK properties and their target entities (format: `"PropertyId→TargetEntity"`). This tells ralph-loop to use `EntityLookup` component instead of plain text inputs. Derive FK fields from `analysis.entities[].relationships[]` where `type = "N:1"` or from properties ending in `Id` with a navigation property. See `smartstack-frontend.md` section 6 for the EntityLookup pattern.

**Page generation:** ALL pages MUST use `/ui-components` skill (indicated by `"skill"` field). NEVER write raw TSX without the skill patterns.

**I18n generation:** 4 JSON files per module with identical key structures. `{moduleLower}` = lowercase module code. Keys: actions, labels, columns, form, errors, validation, messages, empty. See `smartstack-frontend.md` template.

Additional per-section pages (dashboard, import, etc.) are separate entries.

**Route wiring task (MANDATORY — separate entry):**
```json
{ "path": "src/App.tsx", "type": "RouteWiring", "module": "{moduleCode}", "description": "Wire {module} routes into App.tsx application route block. BLOCKING: pages without route wiring = blank pages." }
```

### 6c. Cross-Module PRD (MANDATORY for multi-module features)

> **CRITICAL:** Cross-module integration tasks MUST exist in a dedicated PRD file.
> Without this, ralph-loop will NOT execute E2E tests or FK validation.

Generate `.ralph/prd-CrossModule.json` with:
```json
{
  "$version": "3.0.0",
  "implementation": {
    "filesToCreate": {
      "tests": [
        { "path": "src/Tests/Integration/CrossModule/ForeignKeyValidationTests.cs", "type": "IntegrationTests", "module": "CrossModule" },
        { "path": "src/Tests/Integration/CrossModule/PermissionMatrixTests.cs", "type": "SecurityTests", "module": "CrossModule" },
        { "path": "src/Tests/Integration/CrossModule/E2EFlowTests.cs", "type": "E2ETests", "module": "CrossModule" }
      ]
    }
  }
}
```

Add to progress.txt after all module tasks.

### 6d. Cross-Application PRD (Project Mode Only)

> **Only generated when `workflow.mode === "project"`.**
> Contains cross-application integration tests and shared entity validation.

```json
{
  "$version": "3.0.0",
  "implementation": {
    "filesToCreate": {
      "tests": [
        { "path": "src/Tests/Integration/CrossApplication/SharedEntityValidationTests.cs", "type": "IntegrationTests", "module": "CrossApplication" },
        { "path": "src/Tests/Integration/CrossApplication/CrossAppPermissionTests.cs", "type": "SecurityTests", "module": "CrossApplication" },
        { "path": "src/Tests/Integration/CrossApplication/CrossAppE2EFlowTests.cs", "type": "E2ETests", "module": "CrossApplication" }
      ]
    }
  }
}
```

Write to: `.ralph/prd-CrossApplication.json`

> This PRD is generated IN ADDITION to the per-module cross-module PRD.
> It validates interactions between applications, not just between modules within the same application.

---

### 7. Write Handoff to Feature.json

> **BLOCKING RULE: The handoff MUST be written in EACH module feature.json.**
> A handoff at master level alone is INSUFFICIENT. Ralph-loop consumes module-level handoffs.
> An empty module handoff (`"handoff": {}`) is a CRITICAL BUG that blocks all downstream generation.

#### 7a. Module Handoff Loop (MANDATORY)

> **STRUCTURE CARD: handoff** — Field names are EXACT. Include ALL fields below.
> **PATH RULE:** All backend paths MUST include `{ApplicationName}/` hierarchy (see section 4).
> ```json
> {
>   "complexity": "simple|medium|complex",
>   "filesToCreate": {
>     "domain": [{"path": "src/Domain/Entities/{App}/{Mod}/{Entity}.cs", "type": "Entity|ValueObject|Enum", "linkedFRs": [], "linkedUCs": [], "module": "..."}],
>     "application": [{"path": "src/Application/Services/{App}/{Mod}/{Svc}Service.cs", "type": "Service|Dto|Validator", "linkedFRs": [], "linkedUCs": [], "module": "..."}],
>     "infrastructure": [{"path": "src/Infrastructure/Persistence/Configurations/{App}/{Mod}/{Entity}Configuration.cs", "type": "EFConfiguration", "linkedFRs": [], "module": "..."}],
>     "api": [{"path": "src/API/Controllers/{App}/{Entity}Controller.cs", "type": "ApiController", "linkedUCs": [], "linkedFRs": [], "module": "..."}],
>     "frontend": [{"path": "src/pages/{App}/{Mod}/{Page}Page.tsx", "type": "Page|Component|Hook|DashboardPage|I18n", "linkedUCs": [], "linkedWireframes": [], "module": "...", "skill": "/ui-components"}],
>     "seedData": [{"path": "src/Infrastructure/Persistence/Seeding/Data/{Mod}/...SeedData.cs", "type": "SeedData", "category": "core|business", "source": "...", "module": "..."}],
>     "tests": [{"path": "src/Tests/Unit/Domain/{App}/{Mod}/{Entity}Tests.cs", "type": "UnitTests|IntegrationTests|SecurityTests", "linkedFRs": [], "linkedUCs": [], "module": "..."}]
>   },
>   "brToCodeMapping": [{"ruleId": "BR-...", "files": ["path1", "path2"], "implementation": "description"}],
>   "apiEndpointSummary": [{"method": "GET|POST|PUT|DELETE", "path": "/api/...", "permission": "{app}.{module}.{action}", "linkedUC": "UC-..."}],
>   "prdFile": ".ralph/prd-{module}.json",
>   "totalFiles": 0,
>   "totalTasks": 0,
>   "handedOffAt": "{ISO timestamp}"
> }
> ```
> **MANDATORY fields:** ALL of the above. `filesToCreate` MUST have all 7 categories (even if empty arrays).
> **FORBIDDEN:** `handoff: {}` (empty object is a CRITICAL BUG). Missing `brToCodeMapping` or `apiEndpointSummary`.

**For i = 0; i < modules.length; i++:**

```
1. moduleCode = modules[i].code
2. moduleFeatureId = modules[i].featureJsonPath or find via ba-reader.findFeature()
3. Read the module feature.json via ba-reader.findFeature(moduleFeatureId)
4. Build the handoff payload for THIS module:
   - complexity (from section 3 calculation)
   - filesToCreate (full 7-category structure from section 4, filtered for this module)
   - brToCodeMapping (from section 5, filtered for this module)
   - apiEndpointSummary (from section 6, filtered for this module)
   - prdFile path
   - totalFiles count
   - totalTasks count
   - handedOffAt timestamp

5. Write via ba-writer:
   ba-writer.enrichModuleHandoff({
     moduleFeatureId: {moduleFeatureId},
     handoffData: {
       complexity: "{simple|medium|complex}",
       filesToCreate: {...},
       brToCodeMapping: [...],
       apiEndpointSummary: [...],
       prdFile: ".ralph/prd-{moduleCode}.json",
       totalFiles: {count},
       totalTasks: {count},
       handedOffAt: "{ISO timestamp}"
     }
   })

6. VERIFICATION (MANDATORY - done automatically by enrichModuleHandoff):
   - handoff !== {}
   - handoff.filesToCreate has 7 categories
   - handoff.brToCodeMapping.length > 0
   IF verification fails → STOP, report error, do NOT continue

7. Display progress:
   "✓ Handoff module {i+1}/{N} : {moduleCode} ({fileCount} fichiers, {brCount} BRs mappées)"
```

#### 7b. Generate seedDataCore (MANDATORY for master feature.json)

> **Reference:** Load `references/handoff-seeddata-generation.md` for complete seedDataCore generation logic.

**The seedDataCore provides ralph-loop with the exact navigation, permissions, and translations to seed.**
Without seedDataCore, ralph-loop must INVENT this data — leading to inconsistencies.

The reference includes:
- Helper functions (toHumanReadable, toKebabCase)
- Complete seedDataCore template with all 9 arrays
- Icon inference for application and module levels
- POST-CHECK validation rules (BLOCKING for icons/application, warning for counts)

Key points:
- Icons must NEVER be null (use fallback if not inferred)
- All 9 seedDataCore arrays must be present (even if empty for multi-module scenarios)
- PROJECT MODE: Generate one entry per application from project feature.json
- SINGLE-APP MODE: Generate one entry from master feature.json

```
// Derive seedDataCore from modules[], applicationRoles[], and coverageMatrix[]

// Helper: PascalCase → "Human Readable" (e.g., "HumanResources" → "Human Resources")
function toHumanReadable(pascalCase) {
  return pascalCase
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
}

const appCode = master.metadata.application;
const appLabel = toHumanReadable(appCode);
const appDesc = master.cadrage?.problem?.split('.')[0] || `Gestion ${appLabel}`;
const lang = master.metadata.language || "fr";

const seedDataCore = {
  // Application-level navigation (MANDATORY — one entry per application)
  // PROJECT MODE: Generate one entry per application from project feature.json
  // SINGLE-APP MODE: Generate one entry from master feature.json
  navigationApplications: (() => {
    if (workflow.mode === "project") {
      // Multi-app: one navigation entry per application
      return projectFeature.applications.map((app, idx) => {
        const appLabel = toHumanReadable(app.code);
        return {
          code: app.code.toLowerCase(),
          name: app.code,
          labels: {
            fr: lang === "fr" ? (app.name || appLabel) : appLabel,
            en: lang === "en" ? (app.name || appLabel) : appLabel,
            it: appLabel,
            de: appLabel
          },
          description: {
            fr: lang === "fr" ? (app.description || `Gestion ${appLabel}`) : `Gestion ${appLabel}`,
            en: lang === "en" ? (app.description || `${appLabel} management`) : `${appLabel} management`,
            it: `Gestione ${appLabel}`,
            de: `${appLabel} Verwaltung`
          },
          icon: app.icon || inferIconFromApplication({ metadata: { application: app.code } }) || "layout-grid",
          iconType: "lucide",
          route: `/${toKebabCase(app.code)}`,
          displayOrder: (idx + 1) * 10
        };
      });
    } else {
      // Single-app: one navigation entry
      return [{
        code: appCode.toLowerCase(),
        name: appCode,
        labels: {
          fr: lang === "fr" ? (master.cadrage?.applicationName || appLabel) : appLabel,
          en: lang === "en" ? (master.cadrage?.applicationName || appLabel) : appLabel,
          it: appLabel,
          de: appLabel
        },
        description: {
          fr: lang === "fr" ? appDesc : `Gestion ${appLabel}`,
          en: lang === "en" ? appDesc : `${appLabel} management`,
          it: `Gestione ${appLabel}`,
          de: `${appLabel} Verwaltung`
        },
        icon: inferIconFromApplication(master) || "layout-grid",
        iconType: "lucide",
        route: `/${toKebabCase(appCode)}`,
        displayOrder: 1
      }];
    }
  })(),

  // PROJECT MODE: Flatten modules from ALL applications
  // SINGLE-APP MODE: Use master.modules directly
  navigationModules: (() => {
    if (workflow.mode === "project") {
      let order = 0;
      return projectFeature.applications.flatMap(app => {
        return (app.modules || []).map(m => ({
          code: m.code,
          applicationCode: app.code,
          label: m.name || m.code,
          description: m.description,
          icon: inferIconFromModule(m) || "folder",
          iconType: "lucide",
          route: `/${toKebabCase(app.code)}/${toKebabCase(m.code)}`,
          displayOrder: (++order) * 10
        }));
      });
    } else {
      return master.modules.map((m, i) => ({
        code: m.code,
        label: m.name || m.code,
        description: m.description,
        icon: inferIconFromModule(m) || "folder",
        iconType: "lucide",
        route: `/${toKebabCase(appCode)}/${toKebabCase(m.code)}`,
        displayOrder: (i + 1) * 10
      }));
    }
  })(),

  navigationSections: (() => {
    const buildSections = (modules, appCode) =>
      modules.flatMap(m =>
        (m.anticipatedSections || []).map((s, j) => ({
          moduleCode: m.code,
          code: s.code,
          label: s.description?.split(':')[0] || s.code,
          description: s.description || "",
          route: s.code === "list"
            ? `/${toKebabCase(appCode)}/${toKebabCase(m.code)}`
            : s.code === "detail"
              ? `/${toKebabCase(appCode)}/${toKebabCase(m.code)}/:id`
              : `/${toKebabCase(appCode)}/${toKebabCase(m.code)}/${toKebabCase(s.code)}`,
          displayOrder: (j + 1) * 10,
          navigation: s.code === "detail" ? "hidden" : "visible"
        }))
      );

    if (workflow.mode === "project") {
      return projectFeature.applications.flatMap(app =>
        buildSections(app.modules || [], app.code)
      );
    } else {
      return buildSections(master.modules, appCode);
    }
  })(),

  navigationResources: master.cadrage.coverageMatrix
    .filter(cm => cm.module && cm.anticipatedResources?.length > 0)
    .flatMap(cm =>
      cm.anticipatedResources.map((r, k) => ({
        moduleCode: cm.module,
        sectionCode: cm.anticipatedSections?.[0] || "list",
        code: r,
        label: r.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
        displayOrder: (k + 1) * 10
      }))
    ),

  navigationTranslations: master.modules.flatMap(m => {
    const langs = ["fr", "en", "it", "de"];  // all supported languages
    return langs.map(lang => ({
      moduleCode: m.code,
      language: lang,
      label: lang === master.metadata.language ? (m.name || m.code) : m.code,
      description: lang === master.metadata.language ? m.description : ""
    }));
  }),

  // PROJECT MODE: permissions scoped per application context
  // SINGLE-APP MODE: all permissions under {app}
  permissions: (() => {
    const buildPermissions = (modules, appCode) =>
      modules.flatMap(m => {
        const basePath = `${toKebabCase(appCode)}.${toKebabCase(m.code)}`;
        const actions = ['read', 'create', 'update', 'delete'];
        return [
          { path: `${basePath}.*`, action: '*', description: `Full ${m.name || m.code} access` },
          ...actions.map(action => ({
            path: `${basePath}.${action}`,
            action: action,
            description: `${action.charAt(0).toUpperCase() + action.slice(1)} ${m.name || m.code}`
          }))
        ];
      });

    if (workflow.mode === "project") {
      return projectFeature.applications.flatMap(app =>
        buildPermissions(app.modules || [], app.code)
      );
    } else {
      return buildPermissions(master.modules, appCode);
    }
  })(),

  rolePermissions: (() => {
    const buildRolePermissions = (roles, modules, appCode) =>
      roles.map(role => ({
        role: role.role,
        level: role.level,
        permissions: modules.map(m => `${toKebabCase(appCode)}.${toKebabCase(m.code)}.${
          role.level === 'admin' ? '*' :
          role.level === 'manager' ? 'read,create,update' :
          role.level === 'contributor' ? 'read,create' : 'read'
        }`)
      }));

    if (workflow.mode === "project") {
      return projectFeature.applications.flatMap(app =>
        buildRolePermissions(app.applicationRoles || [], app.modules || [], app.code)
      );
    } else {
      return buildRolePermissions(master.cadrage.applicationRoles, master.modules, appCode);
    }
  })(),

  permissionConstants: (() => {
    const buildConstants = (modules, appCode) =>
      modules.flatMap(m =>
        ['Read', 'Create', 'Update', 'Delete', 'Validate', 'Export'].map(action => ({
          module: m.code,
          application: appCode,
          action: action,
          constant: `${appCode}${m.code}${action}`,
          path: `${toKebabCase(appCode)}.${toKebabCase(m.code)}.${action.toLowerCase()}`
        }))
      );

    if (workflow.mode === "project") {
      return projectFeature.applications.flatMap(app =>
        buildConstants(app.modules || [], app.code)
      );
    } else {
      return buildConstants(master.modules, appCode);
    }
  })()
};

// Icon inference functions are defined in the reference
// inferIconFromApplication(master) - Application level icons
// inferIconFromModule(module) - Module level icons
// See references/handoff-seeddata-generation.md for complete implementation

ba-writer.enrichSection({
  featureId: {feature_id},
  section: "seedDataCore",
  data: seedDataCore
})
```

**POST-CHECK (BLOCKING for icons/application, warning for counts):**
```
IF !seedDataCore.navigationApplications || seedDataCore.navigationApplications.length === 0:
  BLOCKING ERROR: "navigationApplications is empty — Application navigation will NOT be seeded. Menu will show (nothing) → Modules."
IF seedDataCore.navigationApplications[0].icon === null || seedDataCore.navigationApplications[0].icon === undefined:
  BLOCKING ERROR: "Application icon is null — ralph-loop will render empty navigation"
IF !seedDataCore.navigationApplications[0].labels?.fr || !seedDataCore.navigationApplications[0].labels?.en:
  BLOCKING ERROR: "Application labels missing fr/en — translations will fail"
IF seedDataCore.navigationModules.length !== master.modules.length:
  WARNING: seedDataCore has ${seedDataCore.navigationModules.length} nav modules but ${master.modules.length} modules exist
IF seedDataCore.permissions.length === 0:
  WARNING: seedDataCore has 0 permissions — applicationRoles may be missing
IF seedDataCore.navigationModules.some(m => !m.icon || m.icon === null):
  BLOCKING ERROR: "Navigation modules with null icons detected — ralph-loop will render empty navigation"
  → Fix: Re-run inferIconFromModule() for modules with null icons
IF seedDataCore.navigationSections.length === 0:
  WARNING: "0 navigation sections — every module should have at least 1 section (e.g., 'list')"
```

#### 7c. Master Handoff (after ALL modules written + seedDataCore generated)

```
// Write handoff to application-level feature.json (single-app mode)
// OR to each application feature.json + project feature.json (project mode)

IF workflow.mode === "project":
  // Write handoff to EACH application feature.json
  FOR each app in projectFeature.applications:
    ba-writer.enrichSection({
      featureId: app.featureJsonPath,
      section: "handoff",
      data: {
        status: "handed-off",
        complexity: "{app-level complexity}",
        implementationStrategy: "{strategy}",
        moduleCount: app.modules.length,
        moduleOrder: app.modules.map(m => m.code),
        totalFilesToCreate: {sum across app modules},
        totalTasks: {sum across app modules},
        prdFiles: app.modules.map(m => ({ module: m.code, path: `.ralph/prd-${m.code}.json` })),
        handedOffAt: "{ISO timestamp}"
      }
    })

  // Write project-level handoff summary
  ba-writer.enrichSection({
    featureId: {project_id},
    section: "handoff",
    data: {
      status: "handed-off",
      complexity: "{global complexity}",
      implementationStrategy: "{strategy}",
      applicationCount: projectFeature.applications.length,
      applicationOrder: projectFeature.applicationDependencyGraph.topologicalOrder,
      totalModuleCount: {sum of all app module counts},
      totalFilesToCreate: {sum across ALL applications and modules},
      totalTasks: {sum across ALL applications and modules},
      prdStructure: "per-module",
      prdFiles: [
        ...allModulePrdFiles,
        { module: "CrossModule", path: ".ralph/prd-CrossModule.json" },
        { module: "CrossApplication", path: ".ralph/prd-CrossApplication.json" }
      ],
      progressTrackerPath: ".ralph/progress.txt",
      handedOffAt: "{ISO timestamp}"
    }
  })
ELSE:
  ba-writer.enrichSection({
    featureId: {feature_id},
    section: "handoff",
    data: {
      status: "handed-off",
      complexity: "{simple|medium|complex}",
      implementationStrategy: "{strategy}",
      moduleCount: {count},
      moduleOrder: [...],
      totalFilesToCreate: {sum across all modules},
      totalTasks: {sum across all modules},
      prdStructure: "per-module | consolidated",
      prdFiles: [
        { module: "{module1}", path: ".ralph/prd-{module1}.json" },
        { module: "{module2}", path: ".ralph/prd-{module2}.json" }
      ],
      progressTrackerPath: ".ralph/progress.txt",
      handedOffAt: "{ISO timestamp}"
    }
  })
```

#### 7d. Final Verification (BLOCKING)

```
count = 0
FOR each module in modules[]:
  Read module feature.json
  IF module.handoff !== {} AND module.status === "handed-off":
    count++

IF count < modules.length:
  → BLOCKING ERROR: {modules.length - count} modules missing handoff
  → List the missing modules by name
  → DO NOT proceed to step-05b
  → Return to 7a for missing modules only

IF count === modules.length:
  ba-writer.updateStatus({feature_id}, "handed-off")
  Display: "✓ Handoff complet: {count}/{modules.length} modules avec handoff valide"
```

Status journey: analyze → consolidate → handed-off

---

## SELF-VERIFICATION

Before proceeding to step-05b-deploy.md, VERIFY:

1. **filesToCreate** has all 7 categories populated (domain, application, infrastructure, api, frontend, seedData, tests) for each module
2. **Complexity** calculated for all modules
3. **Implementation strategy** chosen (for multi-module features)
4. **ALL module feature.json** have `handoff.brToCodeMapping.length > 0`
5. **ALL module feature.json** have `handoff.apiEndpointSummary.length > 0`
6. **ALL module feature.json** status = "handed-off"
7. **Master feature.json** has `handoff` section with implementationStrategy, moduleOrder, totalFilesToCreate
8. **Master status** = "handed-off"

**IF any check fails → FIX before proceeding.**

---

## NEXT STEP

Load: `steps/step-05b-deploy.md`
