---
name: step-03c-compile
description: Module specification compilation - actors, use cases, FRs, permissions, navigation, seed data, Gherkin, i18n
model: opus
next_step: steps/step-03d-validate.md
---

> **Context files:** `_shared.md` | `_module-loop.md`

# Step 3c: Specification Compilation

## MANDATORY EXECUTION RULES

- ALWAYS use ULTRATHINK mode
- This step COMPILES the specification from the interactive phase (step-03a) into feature.json
- ALWAYS validate specification completeness before writing
- ALL communication in `{language}`
- NEVER skip per-module validation
- **ID NAMING RULE (MANDATORY, NO EXCEPTION):**
  All IDs MUST include a module prefix to guarantee application-wide uniqueness.
  The prefix is derived from the module code initials (2-4 chars):
    UserManagement → UM | VehicleManagement → VM | PartsInventory → PI
    RepairManagement → RM | MaintenanceSchedule → MS | DataSync → DS
    Notifications → NT | Dashboard → DB | Orders → OR | Customers → CU

  Patterns:
    UC-{PREFIX}-{NNN}        → UC-RM-001, UC-PI-003
    BR-{CAT}-{PREFIX}-{NNN}  → BR-VAL-RM-001, BR-CALC-PI-002
    FR-{PREFIX}-{NNN}        → FR-RM-001
    OBJ-{PREFIX}-{NNN}       → OBJ-RM-001
    AC-{PREFIX}-{NNN}        → AC-RM-001
    RISK-{PREFIX}-{NNN}      → RISK-RM-001

  NEVER use bare IDs (UC-001, BR-VAL-001) in multi-module mode.
- **SCHEMA CONFORMITY RULE:**
  ALL data MUST fit within the defined feature-schema.json structure.
  NEVER create custom top-level fields (KPIDefinitions, ChartConfigurations, etc.)
  Dashboard modules MUST use specification.dashboards[] (it exists in the schema).
  If truly needed, use specification.extensions: {} (additionalProperties: true).

## YOUR TASK

Compile all data collected in step-03a (data) and step-03b (UI) into the module specification: generate actors, use cases, functional requirements, permission matrix, navigation, seed data, Gherkin scenarios, validations, messages, lifecycle, API endpoints, and i18n keys.

---

### 8. Full Specification

Generate the complete specification for this module. **Each subsection below includes a STRUCTURE CARD showing the EXACT JSON format. Follow them precisely.**

#### ENTITY ATTRIBUTE SCHEMA (MANDATORY — applies to ALL entities in section 6b of step-03a)

> **CRITICAL:** Entity attributes MUST use STRUCTURED properties, not free-text validation strings.
> The ralph-loop needs parseable types to generate correct C# code.

> **STRUCTURE CARD: analysis.entities[].attributes[]**
> ```json
> {
>   "name": "occupancyRate",
>   "description": "Employee occupancy percentage",
>   "type": "decimal",
>   "maxLength": null,
>   "nullable": true,
>   "required": false,
>   "unique": false,
>   "indexed": false,
>   "defaultValue": "100",
>   "validation": "min:0, max:100, step:0.01",
>   "foreignKey": null
> }
> ```
> **MANDATORY fields:** `name`, `type`
> **type values:** `string`, `int`, `long`, `decimal`, `double`, `bool`, `DateTime`, `DateOnly`, `TimeOnly`, `Guid`, `enum:{EnumName}`, `byte[]`
> **For FK attributes:**
> ```json
> {
>   "name": "departmentId",
>   "description": "FK to Department",
>   "type": "Guid",
>   "required": true,
>   "foreignKey": { "targetEntity": "Department", "targetField": "Id", "onDelete": "restrict" }
> }
> ```
> **FORBIDDEN:** Free-text-only validation like `"FK vers Employee"` or `"Max 100 caractères"` without corresponding structured properties.
>
> **BaseEntity inheritance:** All entities inherit `tenantId`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` from SmartStack.BaseEntity. Do NOT redeclare these. Document this once at the top of the entities section.

#### ENTITY ATTRIBUTE FORMAT POST-CHECK (BLOCKING — runs for EVERY module including the first)

> **CRITICAL:** This check ensures EVERY entity attribute has a structured `type` field.
> It runs for ALL modules, not just modules 2+. The first module is NOT exempt.
> Without `type`, ralph-loop cannot generate correct C# properties, EF configurations, or validators.

After compiling entities for this module, verify and auto-fix:

```javascript
const entities = analysis.entities || [];
let autoFixCount = 0;
for (const entity of entities) {
  for (const attr of entity.attributes || []) {
    if (!attr.type) {
      // BLOCKING: attribute has no type — likely free-text validation only
      console.error(`BLOCKING: ${entity.name}.${attr.name} has no "type" field`);
      console.error(`  Found: ${JSON.stringify(attr)}`);
      console.error(`  Expected: { "name": "${attr.name}", "type": "string|int|decimal|...", "maxLength": ..., "required": ... }`);
      // AUTO-FIX: Infer type from validation string or attribute name
      if (attr.validation?.match(/max\s*\d+/i) || attr.name.match(/name|title|code|description|label|email|phone|address/i)) {
        attr.type = "string";
        const maxMatch = attr.validation?.match(/max\s*(\d+)/i);
        if (maxMatch) attr.maxLength = parseInt(maxMatch[1]);
      } else if (attr.name.match(/id$/i)) {
        attr.type = "Guid";
      } else if (attr.name.match(/date|At$/i)) {
        attr.type = "DateTime";
      } else if (attr.name.match(/is[A-Z]|has[A-Z]|active|enabled/)) {
        attr.type = "bool";
      } else if (attr.name.match(/amount|salary|rate|price|total/i)) {
        attr.type = "decimal";
      } else if (attr.name.match(/count|number|order|sort|index/i)) {
        attr.type = "int";
      }
      // Default to string if no pattern matched
      if (!attr.type) attr.type = "string";
      autoFixCount++;
    }
    // Normalize free-text validation into structured maxLength
    if (typeof attr.validation === 'string' && !attr.maxLength) {
      const maxMatch = attr.validation.match(/max\s*(\d+)/i);
      if (maxMatch) attr.maxLength = parseInt(maxMatch[1]);
    }
    // Ensure required defaults to true if absent
    if (attr.required === undefined) attr.required = true;
  }
}
if (autoFixCount > 0) {
  console.warn(`AUTO-FIXED ${autoFixCount} attributes with missing type — verify correctness`);
}
```

> **Type inference priority table:**
>
> | Attribute name pattern | Inferred type | Example |
> |----------------------|---------------|---------|
> | `name\|title\|code\|description\|label\|email\|phone\|address` | `string` | `positionTitle` → `string` |
> | `*Id` (ends with Id) | `Guid` | `departmentId` → `Guid` |
> | `*date\|*At` (ends with date/At) | `DateTime` | `hireDate` → `DateTime` |
> | `is*\|has*\|active\|enabled` | `bool` | `isActive` → `bool` |
> | `amount\|salary\|rate\|price\|total` | `decimal` | `hourlyRate` → `decimal` |
> | `count\|number\|order\|sort\|index` | `int` | `displayOrder` → `int` |
> | `validation` contains `max NNN` | `string` + `maxLength: NNN` | `"Max 200"` → `string`, `200` |
> | No match | `string` (safe default) | — |

---

#### 8a. Actors

Inherited from application roles → mapped to module permissions.

> **STRUCTURE CARD: specification.actors[]**
> ```json
> {
>   "role": "Sales Manager",
>   "description": "Creates and approves orders",
>   "permissions": [
>     "business.{app}.{module}.read",
>     "business.{app}.{module}.create",
>     "business.{app}.{module}.update"
>   ]
> }
> ```
> **MANDATORY fields:** `role`, `permissions` (array of permission paths)
> **FORBIDDEN fields:** Do NOT use `systemRole`. Use `permissions` array.

#### 8b. Use Cases (UC-{PREFIX}-NNN)

Per section: list, create, read, update, delete, approve, etc.

> **STRUCTURE CARD: specification.useCases[]**
> ```json
> {
>   "id": "UC-{PREFIX}-001",
>   "name": "Create Order",
>   "primaryActor": "Sales Representative",
>   "permission": "business.{app}.{module}.create",
>   "preconditions": ["Customer exists", "Products in stock"],
>   "postconditions": ["Order created with Draft status"],
>   "mainScenario": [
>     "1. User navigates to creation form",
>     "2. User fills in required fields",
>     "3. System validates data (BR-VAL-{PREFIX}-001)",
>     "4. System creates the record",
>     "5. System displays confirmation"
>   ],
>   "alternativeScenarios": [
>     { "name": "Validation failure", "steps": ["1. System detects invalid data", "2. System highlights errors"] }
>   ],
>   "errorScenarios": [
>     { "name": "Server error", "steps": ["1. System shows error message", "2. Data preserved for retry"] }
>   ],
>   "linkedRules": ["BR-VAL-{PREFIX}-001", "BR-WF-{PREFIX}-002"]
> }
> ```
> **MANDATORY fields:** `id`, `name`, `primaryActor`, `permission`, `mainScenario`, `linkedRules`
> **FORBIDDEN fields:** Do NOT use `actor` (use `primaryActor`), `linkedBRs` (use `linkedRules`), `linkedFRs` (not in UC, FRs link to UCs instead)

#### 8c. Functional Requirements (FR-{PREFIX}-NNN)

Linked to BRs and UCs.

> **STRUCTURE CARD: specification.functionalRequirements[]**
> ```json
> {
>   "id": "FR-{PREFIX}-001",
>   "statement": "System MUST validate customer budget before order creation",
>   "priority": "must|should|could",
>   "linkedRules": ["BR-VAL-{PREFIX}-001"],
>   "linkedUseCases": ["UC-{PREFIX}-001"],
>   "acceptanceCriteria": [
>     "Order rejected if total > budget",
>     "Error message displayed with remaining budget"
>   ]
> }
> ```
> **MANDATORY fields:** `id`, `statement`, `priority`, `linkedUseCases`
> **FORBIDDEN fields:** Do NOT use `name`/`description` (use `statement` with System MUST/SHOULD/COULD format), `linkedUCs` (use `linkedUseCases`), `linkedBRs` (use `linkedRules`)

#### 8d. Permission Matrix

Roles × resources × operations with full paths.

> **STRUCTURE CARD: specification.permissionMatrix**
> ```json
> {
>   "permissions": [
>     { "path": "business.{app}.{module}.read", "action": "read", "description": "View records" },
>     { "path": "business.{app}.{module}.create", "action": "create", "description": "Create new records" },
>     { "path": "business.{app}.{module}.update", "action": "update", "description": "Update existing records" },
>     { "path": "business.{app}.{module}.delete", "action": "delete", "description": "Delete records" },
>     { "path": "business.{app}.{module}.export", "action": "export", "description": "Export data" },
>     { "path": "business.{app}.{module}.dashboard.read", "action": "read", "description": "View dashboard (section-level)" }
>   ],
>   "roleAssignments": [
>     { "role": "{App} Admin", "permissions": ["business.{app}.{module}.read", "business.{app}.{module}.create", "business.{app}.{module}.update", "business.{app}.{module}.delete", "business.{app}.{module}.export", "business.{app}.{module}.dashboard.read"] },
>     { "role": "{App} Manager", "permissions": ["business.{app}.{module}.read", "business.{app}.{module}.create", "business.{app}.{module}.update", "business.{app}.{module}.dashboard.read"] },
>     { "role": "{App} Viewer", "permissions": ["business.{app}.{module}.read"] }
>   ]
> }
> ```
> **STRUCTURE:** Object with 2 arrays: `permissions[]` and `roleAssignments[]`
> **Permission levels:** Module-level = `business.{app}.{module}.{action}` (4 segments). Section-level = `business.{app}.{module}.{section}.{action}` (5 segments, for sections needing distinct access like dashboard, approve, import).
> **FORBIDDEN:** Do NOT use a flat array with `resource`/`roles` fields. Always use the nested structure above.

#### 8e. Navigation

Module → Sections → Resources (levels 3-4-5 of the hierarchy).

> **STRUCTURE CARD: specification.navigation**
> ```json
> {
>   "entries": [
>     { "level": "module", "code": "{module}", "labels": {"fr": "...", "en": "...", "it": "...", "de": "..."}, "route": "/business/{app}/{module}", "icon": "list" },
>     { "level": "section", "code": "list", "labels": {"fr": "Liste", "en": "List", "it": "Elenco", "de": "Liste"}, "route": "/business/{app}/{module}", "icon": "list" },
>     { "level": "section", "code": "dashboard", "labels": {"fr": "Tableau de bord", "en": "Dashboard", "it": "Cruscotto", "de": "Dashboard"}, "route": "/business/{app}/{module}/dashboard", "icon": "chart-bar", "isNew": true }
>   ]
> }
> ```

#### 8e-POST-CHECK: Navigation Enforcement (BLOCKING)

> **CRITICAL:** `specification.navigation.entries[]` is MANDATORY. If absent, auto-generate from sections.

```javascript
const nav = specification.navigation;
if (!nav || !nav.entries || nav.entries.length === 0) {
  console.warn('AUTO-FIX: navigation.entries is empty — generating from sections');
  const sections = specification.sections || [];
  const entries = [
    { level: "module", code: "{module}", labels: {fr: "{ModuleName}", en: "{ModuleName}", it: "{ModuleName}", de: "{ModuleName}"}, route: "/business/{app}/{module}", icon: "list" }
  ];
  for (const section of sections) {
    entries.push({
      level: "section",
      code: section.code,
      labels: {fr: section.name || section.code, en: section.name || section.code, it: section.name || section.code, de: section.name || section.code},
      route: section.route || `/business/{app}/{module}/${section.code}`,
      icon: section.icon || "file-text"
    });
  }
  specification.navigation = { entries };
}
// Verify ALL entries have 4 languages
for (const entry of specification.navigation.entries) {
  for (const lang of ['fr', 'en', 'it', 'de']) {
    if (!entry.labels[lang]) {
      entry.labels[lang] = entry.labels['en'] || entry.labels['fr'] || entry.code;
      console.warn(`AUTO-FIX: navigation entry "${entry.code}" missing ${lang} label — copied from fallback`);
    }
  }
}
```

#### 8f. SeedData Core

7 MANDATORY typed arrays — each with structured objects, NOT flat strings or objects.

> **STRUCTURE CARD: specification.seedDataCore**
> ```json
> {
>   "navigationModules": [
>     { "code": "{module}", "label": "{Module Name}", "icon": "list", "route": "/business/{app}/{module}", "parentCode": "{app}", "sort": 1 }
>   ],
>   "navigationSections": [
>     { "code": "list", "label": "Liste", "icon": "List", "route": "/business/{app}/{module}", "parentCode": "{module}", "permission": "business.{app}.{module}.read", "sort": 1 },
>     { "code": "detail", "label": "Détail", "icon": "FileText", "route": "/business/{app}/{module}/:id", "parentCode": "{module}", "permission": "business.{app}.{module}.read", "sort": 2, "navigation": "hidden" },
>     { "code": "dashboard", "label": "Dashboard", "icon": "BarChart", "route": "/business/{app}/{module}/dashboard", "parentCode": "{module}", "permission": "business.{app}.{module}.dashboard.read", "sort": 3 }
>   ],
>   "navigationResources": [
>     { "code": "{module}-grid", "type": "SmartTable", "entity": "{Entity}", "parentCode": "list", "permission": "business.{app}.{module}.read" },
>     { "code": "{module}-detail-card", "type": "DetailCard", "entity": "{Entity}", "parentCode": "detail", "permission": "business.{app}.{module}.read" }
>   ],
>   "navigationTranslations": [
>     { "moduleCode": "{module}", "language": "fr", "label": "..." },
>     { "moduleCode": "{module}", "language": "en", "label": "..." },
>     { "moduleCode": "{module}", "language": "it", "label": "..." },
>     { "moduleCode": "{module}", "language": "de", "label": "..." }
>   ],
>   "permissions": [
>     { "path": "business.{app}.{module}.read", "action": "read", "description": "View {module}" },
>     { "path": "business.{app}.{module}.create", "action": "create", "description": "Create {module}" },
>     { "path": "business.{app}.{module}.dashboard.read", "action": "read", "description": "View {module} dashboard (section-level)" }
>   ],
>   "rolePermissions": [
>     { "role": "{App} Admin", "permissionPath": "business.{app}.{module}.*" },
>     { "role": "{App} Manager", "permissionPath": "business.{app}.{module}.read" }
>   ],
>   "permissionConstants": [
>     { "constantName": "{Module}Read", "path": "business.{app}.{module}.read" },
>     { "constantName": "{Module}Create", "path": "business.{app}.{module}.create" }
>   ]
> }
> ```
> **MANDATORY:** All 7 arrays must be present. Each element must be an object, NOT a string.
> **CRITICAL:** `navigationSections` and `navigationResources` are DERIVED from `specification.sections[]` — use the transform algorithm below (section 8f-bis).
> **IMPORTANT:** `create` and `edit` are NEVER sections — they are action pages reached via buttons. Do NOT include them in `navigationSections`. Only include actual sidebar sections (list, dashboard, approve, import, etc.) and hidden route sections (detail).
> **FORBIDDEN:** Do NOT use `navigationModule` (singular string), `permissions` as flat string array, `rolePermissions` as flat object, `permissionsConstants` as comma-separated string.
>
> **FORBIDDEN in navigation routes:**
> - `/business/{app}/{module}/list` → use `/business/{app}/{module}` (list IS the module route)
> - `/business/{app}/{module}/detail/:id` → use `/business/{app}/{module}/:id`
> - Navigation routes must match React Router paths exactly

#### 8f-bis. Transform Sections into Navigation SeedData

> **CRITICAL:** `navigationSections` and `navigationResources` must be generated from `specification.sections[]`.
> This ensures the navigation hierarchy (Module → Section → Resource) is complete in the database.

**Transform algorithm:**

```javascript
// 1. Transform specification.sections[] into navigationSections
const navigationSections = specification.sections.map((section, index) => ({
  code: section.code,                    // e.g., "list", "detail", "dashboard"
  label: section.labels.fr,              // Default language (fr)
  icon: section.icon,                    // Lucide icon name
  route: section.route,                  // Full route path
  parentCode: metadata.module,           // Module code (parent)
  permission: section.permission,        // Required permission
  sort: index + 1                        // Display order (1-based)
}));

// 2. Transform specification.sections[].resources[] into navigationResources
const navigationResources = specification.sections.flatMap(section =>
  section.resources.map(resource => ({
    code: resource.code,                 // e.g., "employees-grid"
    type: resource.type,                 // e.g., "SmartTable", "SmartForm"
    entity: resource.entity,             // e.g., "Employee"
    parentCode: section.code,            // Section code (parent)
    permission: resource.permission      // Optional (can inherit from section)
  }))
);
```

**Write seedDataCore to specification:**

```
ba-writer.enrichSection({
  feature_id,
  section: "specification",
  data: {
    seedDataCore: {
      navigationModules: [ ... ],
      navigationSections: navigationSections,     // DERIVED
      navigationResources: navigationResources,   // DERIVED
      navigationTranslations: [ ... ],
      permissions: [ ... ],
      rolePermissions: [ ... ],
      permissionConstants: [ ... ]
    }
  }
})
```

> **NOTE:** This step writes ONLY seedDataCore. The FULL specification write (actors, useCases, wireframes, sections, etc.) happens in step-03d section 11.
> ALL data from 8a-8l MUST be carried forward to step-03d. Do NOT discard in-memory data.

**Validation:**

- EVERY section in `specification.sections[]` MUST appear in `navigationSections`
- EVERY resource in `specification.sections[].resources[]` MUST appear in `navigationResources`
- Total count: `navigationSections.length === specification.sections.length`
- Total count: `navigationResources.length === sum(sections[].resources.length)`

#### 8g. Gherkin Scenarios

BDD acceptance tests per UC.

> **STRUCTURE CARD: specification.gherkinScenarios[]**
> ```json
> {
>   "feature": "{Module} Management",
>   "scenarios": [
>     {
>       "name": "Create a new record with valid data",
>       "tags": ["@{module}", "@create", "@smoke"],
>       "given": ["An authenticated user with role Manager", "No existing record with code 'TEST-001'"],
>       "when": ["The user fills the creation form", "The user submits the form"],
>       "then": ["The record is created with Draft status", "A success message is displayed"]
>     }
>   ]
> }
> ```
> **STRUCTURE:** MUST be an ARRAY of objects (NOT a single object). Each object has `feature` string + `scenarios[]` array. Each scenario has `given`, `when`, `then` as ARRAYS of strings.
> **FORBIDDEN:** Do NOT use flat arrays of `{uc, scenario, given, when, then}` where given/when/then are single strings.
> **FORBIDDEN:** Do NOT use a single object `{feature, scenarios}` — MUST be an ARRAY `[{feature, scenarios}]` even with one feature.

#### 8h. Validations

Field validation rules per entity.

> **STRUCTURE CARD: specification.validations[]**
> ```json
> {
>   "entity": "Order",
>   "field": "amount",
>   "rules": ["required", "decimal", "min:0.01", "max:999999.99"],
>   "errorMessageKey": "validation.{module}.amount.invalid"
> }
> ```
> **MANDATORY fields:** `entity`, `field`, `rules` (array), `errorMessageKey`
> **FORBIDDEN fields:** Do NOT use `rule` (singular string) or `message` (use `errorMessageKey`).

#### 8i. Business Messages

Minimum 4: success, error CRUD, error validation, error permission.

> **STRUCTURE CARD: specification.messages[]**
> ```json
> {
>   "code": "{MODULE}-SUCCESS-CREATE",
>   "type": "success|error|warning|info",
>   "title": "Record Created",
>   "message": "The record {code} has been created successfully.",
>   "i18nKey": "message.{module}.created"
> }
> ```
> **MANDATORY fields:** `code`, `type`, `message`, `i18nKey`
> **FORBIDDEN:** Do NOT omit `title` or `i18nKey`. Every message MUST have an i18n key.

#### 8j. Entity Lifecycle

State machines for entities with status/state.
**Note:** If depth = full and 3a-state was executed, lifeCycles are already defined. Verify completeness and add any missing states/transitions.

> **STRUCTURE CARD: specification.lifeCycles[]**
> ```json
> {
>   "entity": "Order",
>   "field": "status",
>   "initialState": "draft",
>   "states": [
>     { "id": "draft", "displayName": "Brouillon", "color": "gray", "allowedTransitions": ["submitted"], "isTerminal": false },
>     { "id": "submitted", "displayName": "Soumis", "color": "blue", "allowedTransitions": ["approved", "rejected"], "isTerminal": false },
>     { "id": "approved", "displayName": "Approuvé", "color": "green", "allowedTransitions": ["archived"], "isTerminal": false },
>     { "id": "rejected", "displayName": "Rejeté", "color": "red", "allowedTransitions": [], "isTerminal": true },
>     { "id": "archived", "displayName": "Archivé", "color": "purple", "allowedTransitions": [], "isTerminal": true }
>   ],
>   "transitions": [
>     {
>       "from": "draft", "to": "submitted", "action": "submit",
>       "label": { "fr": "Soumettre", "en": "Submit" },
>       "permission": "business.{app}.{module}.update",
>       "guards": ["BR-VAL-{PREFIX}-001"],
>       "effects": [{ "type": "notification", "target": "role:manager", "template": "{module}-submitted" }],
>       "confirm": true
>     }
>   ]
> }
> ```
> **MANDATORY:** `states` MUST be an array of OBJECTS with `id`, `displayName`, `color`, `allowedTransitions`, `isTerminal`. NEVER use flat string arrays.
> **Colors:** gray (draft/new), blue (in-progress), green (active/approved), yellow (warning/pending), orange (review), red (error/rejected), purple (archived/terminal)
> **FORBIDDEN:** Do NOT use `states: ["Active", "Inactive"]` (flat strings), `terminalStates` (use `isTerminal: true` on each state).

#### 8k. API Endpoints

RESTful routes following SmartStack patterns.

> **STRUCTURE CARD: specification.apiEndpoints[]**
> ```json
> {
>   "method": "GET|POST|PUT|PATCH|DELETE",
>   "path": "/api/business/{app}/{module}",
>   "permission": "business.{app}.{module}.read",
>   "requestDto": "Get{Module}Query",
>   "responseDto": "{Module}Dto[]",
>   "description": "List all records with pagination and filters"
> }
> ```
> **MANDATORY fields:** `method`, `path`, `permission`, `description`
> **Recommended:** Include `requestDto` and `responseDto` for implementation clarity.

#### 8l. i18n Keys

Translation keys for all UI text (4 languages: fr, en, it, de).

> **STRUCTURE CARD: specification.i18nKeys**
> **CRITICAL:** ALL leaf translation keys MUST have 4 languages (fr, en, it, de). IT/DE can use shorter translations but MUST exist.
> ```json
> {
>   "title": { "fr": "{Module}", "en": "{Module}", "it": "{Module}", "de": "{Module}" },
>   "list": {
>     "title": { "fr": "Liste", "en": "List", "it": "Elenco", "de": "Liste" },
>     "empty": { "fr": "Aucun enregistrement", "en": "No records", "it": "Nessun record", "de": "Keine Einträge" }
>   },
>   "create": { "title": { "fr": "Nouveau", "en": "New", "it": "Nuovo", "de": "Neu" } },
>   "detail": { "title": { "fr": "Détail", "en": "Detail", "it": "Dettaglio", "de": "Detail" } },
>   "buttons": {
>     "create": { "fr": "Créer", "en": "Create", "it": "Crea", "de": "Erstellen" },
>     "edit": { "fr": "Modifier", "en": "Edit", "it": "Modifica", "de": "Bearbeiten" },
>     "delete": { "fr": "Supprimer", "en": "Delete", "it": "Elimina", "de": "Löschen" },
>     "save": { "fr": "Enregistrer", "en": "Save", "it": "Salva", "de": "Speichern" },
>     "cancel": { "fr": "Annuler", "en": "Cancel", "it": "Annulla", "de": "Abbrechen" }
>   },
>   "validation": {
>     "required": { "fr": "Ce champ est requis", "en": "This field is required", "it": "Campo obbligatorio", "de": "Pflichtfeld" }
>   }
> }
> ```

#### 8l-POST-CHECK: i18n 4 Languages Enforcement (BLOCKING)

> **CRITICAL:** i18n keys with missing IT/DE translations are a recurring issue.
> This POST-CHECK walks ALL leaf keys and auto-fills missing translations.

```javascript
const i18n = specification.i18nKeys || {};
const REQUIRED_LANGS = ['fr', 'en', 'it', 'de'];
let missingCount = 0;

function walkI18n(obj, path) {
  if (typeof obj !== 'object' || obj === null) return;
  // Leaf node: has at least 'fr' or 'en' as direct string properties
  const hasLangKey = REQUIRED_LANGS.some(l => typeof obj[l] === 'string');
  if (hasLangKey) {
    for (const lang of REQUIRED_LANGS) {
      if (!obj[lang] || typeof obj[lang] !== 'string') {
        // Fallback: copy from en > fr > first available
        obj[lang] = obj['en'] || obj['fr'] || Object.values(obj).find(v => typeof v === 'string') || path;
        missingCount++;
      }
    }
  } else {
    // Branch node: recurse
    for (const key of Object.keys(obj)) {
      walkI18n(obj[key], `${path}.${key}`);
    }
  }
}
walkI18n(i18n, 'i18nKeys');
if (missingCount > 0) {
  console.warn(`AUTO-FIX: filled ${missingCount} missing i18n translations (IT/DE fallbacks from EN/FR)`);
}
```

---

## SELF-VERIFICATION (MANDATORY before loading next step)

Before loading step-03d-validate, verify all 12 subsections (8a-8l) are populated:

1. **8a. Actors** - At least 2 roles with permissions
2. **8b. Use Cases** - At least 2 UCs with complete structure
3. **8c. Functional Requirements** - At least 4 FRs with linked UCs and BRs
4. **8d. Permission Matrix** - Resources and role assignments defined
5. **8e. Navigation** - Module, sections, and resource routes specified
6. **8f. SeedData Core** - All 7 arrays populated with structured objects (navigationModules, navigationSections, navigationResources, navigationTranslations, permissions, rolePermissions, permissionConstants)
7. **8g. Gherkin Scenarios** - BDD scenarios for each major UC
8. **8h. Validations** - Field rules with error message keys
9. **8i. Business Messages** - Minimum 4 messages (success, error CRUD, validation, permission)
10. **8j. Entity Lifecycle** - State machines with transitions if entity has status
11. **8k. API Endpoints** - RESTful routes with permissions
12. **8l. i18n Keys** - Translation keys in 4 languages (fr, en, it, de)

13. **Wireframes present** — `(specification.uiWireframes || specification.wireframes || []).length >= 1` (BLOCKING)
    Quick check: the wireframe data from step-03b MUST still be in memory OR persisted to feature.json

14. **Wireframes create/edit** — For each data-centric section with a create form, a wireframe `{section}-create` MUST exist (BLOCKING).
    For each modifiable entity, a wireframe `{section}-edit` SHOULD exist (WARNING if absent).
    Create/edit wireframes are action-page wireframes — they are NOT separate navigation sections.
    They describe the full-page form layout (fields, validation, submit/cancel buttons).
    (step-03b now writes wireframes intermediately). If wireframes are empty:
    - First, re-read the module feature.json to check if step-03b wrote them
    - If present in file but not in memory → load them from file
    - If absent everywhere → **STOP** and reload step-03b to regenerate wireframes

**IF any subsection is missing → STOP and request the missing data before proceeding.**

**ABSOLUTE FORMAT CHECKS (apply to ALL modules, including the first):**

> These checks validate against the canonical schema — NOT comparatively against another module.
> They run BEFORE the SCHEMA UNIFORMITY comparative check, ensuring the first module has the
> correct format before being used as baseline for subsequent modules.

```javascript
// Gherkin MUST be array (not single object)
if (!Array.isArray(specification.gherkinScenarios)) {
  console.warn('AUTO-FIX: gherkinScenarios is object, wrapping in array');
  specification.gherkinScenarios = [specification.gherkinScenarios];
}

// Validations[].rules MUST be array (not singular rule string)
for (const v of specification.validations || []) {
  if (typeof v.rules === 'string') { v.rules = [v.rules]; }
  if (v.rule && !v.rules) {
    v.rules = Array.isArray(v.rule) ? v.rule : [v.rule];
    delete v.rule;
  }
}

// Messages MUST have "message" field (not only "description")
for (const m of specification.messages || []) {
  if (!m.message && m.description) { m.message = m.description; }
}

// Wireframes MUST use canonical field names
const wireframes = specification.uiWireframes || specification.wireframes || [];
for (const wf of wireframes) {
  if (wf.title && !wf.screen) { wf.screen = wf.title; delete wf.title; }
  if (wf.ascii && !wf.mockup) { wf.mockup = wf.ascii; delete wf.ascii; }
  if (wf.content && !wf.mockup) { wf.mockup = wf.content; delete wf.content; }
  if (wf.name && !wf.screen) { wf.screen = wf.name; delete wf.name; }
}
specification.uiWireframes = wireframes;

// Entity attributes MUST have "type" field (already checked in ENTITY ATTRIBUTE FORMAT POST-CHECK above)
```

**SCHEMA UNIFORMITY CHECK (multi-module mode):**
IF this is NOT the first module in moduleOrder:
  Compare THIS module's specification structure against the FIRST specified module:
  - `gherkinScenarios` MUST be array (not object) in ALL modules
  - `validations[].rules` MUST be array (not singular `rule`) in ALL modules
  - `messages[]` MUST have `message` field in ALL modules
  - `specification.apiEndpoints[]` MUST be present in ALL modules
  - `specification.i18nKeys` MUST be present in ALL modules
  - `analysis.entities[].attributes[].type` MUST be present on ALL attributes in ALL modules
  - IF first module has free-text-only attributes without `type` field → AUTO-FIX with type inference
  IF any structural divergence detected → AUTO-FIX to match the canonical format before proceeding.

---

## NEXT STEP

Load: `steps/step-03d-validate.md`
