---
name: step-03d-validate
description: Per-module validation, write feature.json, incremental HTML deploy, loop decision
model: sonnet
next_step: steps/step-03a1-setup.md OR steps/step-04a-collect.md (conditional)
---

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

# Step 3d: Validation & Loop

## MANDATORY EXECUTION RULES

- ALWAYS use ULTRATHINK mode
- This step VALIDATES the specification from step-03c, writes it to feature.json, and decides loop continuation
- ALWAYS verify specification completeness before writing
- ALL communication in `{language}`

## MODE DETECTION (inherited from step-03a1)

> **CRITICAL: Re-check your execution mode before proceeding.**

**IF you are running as a TEAM AGENT** (your prompt contains `PROPOSE & REVIEW` or `team-lead` as recipient):
  → **NEVER** use `AskUserQuestion` in ANY section below (sections 9d, 10, 12)
  → Section 9d: if validation fails, AUTO-CORRECT silently (no user options)
  → Section 12: **SKIP ENTIRELY** — go directly to section 12-bis (Agent Mode)
  → After writing feature.json, send `PROPOSAL_READY` to team lead via SendMessage
  → NEVER present options/menus to the user — you are an autonomous agent

**IF you are running in the MAIN CONVERSATION** (classic inline mode):
  → Normal interactive mode — use `AskUserQuestion` as documented
  → Section 12-bis does NOT apply to you
- **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

  NEVER use bare IDs (UC-001, BR-VAL-001) in multi-module mode.

## YOUR TASK

Validate the module specification for completeness and consistency, write to feature.json, deploy incremental HTML, and decide whether to loop to the next module or proceed to consolidation.

---

### 9. Per-Module Validation

#### 9a. Completeness Checks

> **CRITICAL:** Checks MUST count ACTUAL elements in arrays, NOT declarative values.
> A check that reports PASS when the array is empty is a LIE and BLOCKS downstream quality.

| Section | Minimum | How to verify | Status |
|---------|---------|---------------|--------|
| actors | 2 | `specification.actors.length >= 2` | PASS/FAIL |
| useCases | 2 | `specification.useCases.length >= 2` | PASS/FAIL |
| functionalRequirements | 4 | `specification.functionalRequirements.length >= 4` | PASS/FAIL |
| permissionMatrix | 1 resource × 2 roles | `specification.permissionMatrix.permissions.length >= 1 && specification.permissionMatrix.roleAssignments.length >= 2` | PASS/FAIL |
| entities | 1 | `analysis.entities.length >= 1` | PASS/FAIL |
| entitySchemaFormat | attributes[] not fields[] (BLOCKING) | `analysis.entities.every(e => e.attributes?.length > 0)` | PASS/FAIL |
| entityAttributeTypes | ALL attributes have `type` field (BLOCKING) | `analysis.entities.every(e => e.attributes.every(a => a.type))` — **NOTE:** Entities in analysis phase may NOT have `type`. Step-03c auto-fix MUST have run first. If types are missing, RE-RUN step-03c auto-fix algorithm. | PASS/FAIL |
| navigation | entries[] present (BLOCKING) | `specification.navigation?.entries?.length >= 1` | PASS/FAIL |
| dataLifecycle | present in analysis (BLOCKING) | `analysis.dataLifecycle !== undefined` | PASS/FAIL |
| wireframes | 1 per section (BLOCKING) | `(specification.uiWireframes \|\| specification.wireframes \|\| []).length >= (specification.sections \|\| []).length` (count REAL elements, check BOTH key names) | PASS/FAIL |
| wireframeSchema | All required fields present (BLOCKING) | `(specification.uiWireframes \|\| specification.wireframes \|\| []).every(w => (w.screen \|\| w.title) && w.section && (w.mockup \|\| w.ascii \|\| w.content))` | PASS/FAIL |
| sections | 1 (BLOCKING) | `specification.sections.length >= 1` (EVERY module needs at least 1 section) | PASS/FAIL |
| gherkinScenarios | 1 array entry | `Array.isArray(specification.gherkinScenarios) && specification.gherkinScenarios.length >= 1` | PASS/FAIL |
| gherkinFormat | Array not object (BLOCKING) | `Array.isArray(specification.gherkinScenarios)` (NOT a single object) | PASS/FAIL |
| validations | 1 | `specification.validations.length >= 1` | PASS/FAIL |
| validationFormat | rules[] array (BLOCKING) | `specification.validations.every(v => Array.isArray(v.rules))` (NOT singular `rule`) | PASS/FAIL |
| messages | 4 | `specification.messages.length >= 4` | PASS/FAIL |
| messageFormat | `message` field present (BLOCKING) | `specification.messages.every(m => m.message)` | PASS/FAIL |
| lifeCycles | 1 (if entity has status) | `specification.lifeCycles.length >= 1` (if any entity has status/state field) | PASS/FAIL |
| seedDataCore | 9 arrays present with content | See detailed check below | PASS/FAIL (BLOCKING) |
| apiEndpoints | 1 | `specification.apiEndpoints.length >= 1` | PASS/FAIL |
| i18nKeys | present | `specification.i18nKeys !== undefined && specification.i18nKeys !== null` | PASS/FAIL |
| navigationIcons | non-null | `specification.seedDataCore.navigationModules.every(m => m.icon !== null)` | PASS/FAIL |

**seedDataCore detailed check (BLOCKING):**
```javascript
const sdc = specification.seedDataCore;
const currentModuleIndex = metadata.workflow?.currentModuleIndex || 0;
const checks = [
  // navigationApplications and applicationRoles: required for first module (index 0), can be empty for subsequent modules
  { key: "navigationApplications", actual: sdc.navigationApplications?.length || 0, min: currentModuleIndex === 0 ? 1 : 0 },
  { key: "applicationRoles",      actual: sdc.applicationRoles?.length || 0,       min: currentModuleIndex === 0 ? 1 : 0 },
  { key: "navigationModules",      actual: sdc.navigationModules?.length || 0,      min: 1 },
  { key: "navigationSections",     actual: sdc.navigationSections?.length || 0,     min: 1 },  // EVERY module needs ≥1 section
  { key: "navigationResources",    actual: sdc.navigationResources?.length || 0,    min: 1 },
  { key: "navigationTranslations", actual: sdc.navigationTranslations?.length || 0, min: 2 },  // min fr+en
  { key: "permissions",            actual: sdc.permissions?.length || 0,            min: 1 },
  { key: "rolePermissions",        actual: sdc.rolePermissions?.length || 0,        min: 1 },
  { key: "permissionConstants",    actual: sdc.permissionConstants?.length || 0,    min: 1 }
];
const failures = checks.filter(c => c.actual < c.min);
IF failures.length > 0:
  BLOCKING ERROR: "seedDataCore incomplete — empty arrays: {failures.map(f => f.key).join(', ')}"
  → Fix: Ensure specification.sections[] has ≥1 entry, then re-run 8f-bis transform
```

#### 9b. Consistency Checks

- Every UC has ≥1 linked FR
- Every FR has ≥1 linked BR
- All BR references exist in analysis.businessRules
- All actors appear in permissionMatrix
- Permission paths use correct format: module-level `{app}.{module}.{action}` or section-level `{app}.{module}.{section}.{action}`
- rolePermissions paths match permissions paths
- API routes use consistent prefix

#### 9c. Convention Checks

- UC-NNN format
- FR-NNN format
- BR-{CATEGORY}-NNN format
- Entity names PascalCase
- Field names camelCase
- Entity attribute format: `attributes[]` with {name, description}, NOT `fields[]` with {name, type} — entities must NOT have tableName or primaryKey
- Wireframe structure: `screen` (not `name`/`title`), `mockup` (not `ascii`/`content`), `componentMapping` is array of {wireframeElement, reactComponent} (not plain key-value object), `layout` is object with regions (not string)
- Wireframe field naming: uses `screen` (not `title`), `mockup` (not `ascii`) — auto-fix if wrong (see 9c-fix below)
- Permission paths dot-separated lowercase

#### 9c-fix. Auto-Fix Wireframe Field Names (MANDATORY before writing)

> **CRITICAL:** The agent may use non-canonical field names (`title`, `ascii`, `name`, `content`).
> These MUST be normalized to canonical names before writing to feature.json.

```javascript
// AUTO-FIX: Normalize wireframe field names before writing
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; }
}
// Store normalized wireframes back under canonical key
specification.uiWireframes = wireframes;
```

#### 9c-fix-entities. Auto-Fix Entity Attribute Types (MANDATORY before writing)

> **DEFENSE-IN-DEPTH:** step-03c POST-CHECK should catch attributes without `type`,
> but if any slip through to step-03d, auto-fix them here before writing to feature.json.
> Uses the same type inference algorithm as step-03c POST-CHECK.

```javascript
// AUTO-FIX: Ensure all entity attributes have structured type
const entities = analysis.entities || [];
let entityAutoFixCount = 0;
for (const entity of entities) {
  for (const attr of entity.attributes || []) {
    if (!attr.type) {
      // Infer type from attribute name pattern
      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";
      } else {
        attr.type = "string"; // safe default
      }
      // Normalize free-text maxLength
      if (typeof attr.validation === 'string' && !attr.maxLength) {
        const m = attr.validation.match(/max\s*(\d+)/i);
        if (m) attr.maxLength = parseInt(m[1]);
      }
      if (attr.required === undefined) attr.required = true;
      entityAutoFixCount++;
    }
  }
}
if (entityAutoFixCount > 0) {
  console.warn(`DEFENSE-IN-DEPTH: auto-fixed ${entityAutoFixCount} attributes without type in step-03d`);
}
```

#### 9d. Decision

> **TEAM AGENT MODE:** If running as team agent, SKIP AskUserQuestion. On FAIL → auto-correct silently. On PASS → proceed directly to section 11 (write). NEVER present options to the user.

**INLINE MODE ONLY (main conversation):**

IF validation PASS:
  Display summary, ask client for confirmation

IF validation FAIL:
  Display errors, offer:
  ```
  options:
    - label: "Auto-corriger"
      description: "Corriger automatiquement les erreurs détectées"
    - label: "Réviser manuellement"
      description: "Revenir aux sections problématiques"
    - label: "Forcer la validation"
      description: "Accepter malgré les erreurs (risqué)"
  ```

#### 9e. Write Module Validation Section (MANDATORY)

> **The module feature.json MUST have a `validation` section recording the results of steps 9a-9d.**

```
ba-writer.enrichSection({
  featureId: {module_feature_id},
  section: "validation",
  data: {
    validatedAt: "{ISO timestamp}",
    completenessChecks: [
      { "section": "useCases", "count": {count}, "minimum": 2, "status": "PASS|FAIL" },
      { "section": "functionalRequirements", "count": {count}, "minimum": 4, "status": "PASS|FAIL" },
      { "section": "wireframes", "count": {count}, "minimum": {section_count}, "status": "PASS|FAIL" },
      { "section": "seedDataCore", "count": 7, "minimum": 7, "status": "PASS|FAIL" }
    ],
    consistencyChecks: [
      { "check": "UC-FR linkage", "passed": {n}, "warnings": {n}, "errors": {n}, "status": "PASS|FAIL" },
      { "check": "BR references valid", "passed": {n}, "warnings": {n}, "errors": {n}, "status": "PASS|FAIL" },
      { "check": "Permission path format", "passed": {n}, "warnings": {n}, "errors": {n}, "status": "PASS|FAIL" }
    ],
    conventionChecks: [
      { "check": "ID naming with module prefix", "status": "PASS|FAIL", "details": "..." },
      { "check": "Entity names PascalCase", "status": "PASS|FAIL", "details": "..." }
    ],
    semanticChecks: [
      { "check": "orphan-permissions", "module": "{currentModule}", "status": "PASS|WARNING|ERROR", "details": "...", "autoFixed": false },
      { "check": "uc-sans-fr", "module": "{currentModule}", "status": "PASS|WARNING|ERROR", "details": "...", "autoFixed": false },
      { "check": "missing-wireframes", "module": "{currentModule}", "status": "PASS|WARNING|ERROR", "details": "...", "autoFixed": false }
    ],
    warnings: ["List of non-blocking warnings"],
    decision: {
      "approved": true,
      "reason": "All checks passed",
      "approvalMode": "standard",
      "approvedBy": "Client",
      "approvedAt": "{ISO timestamp}"
    }
  }
})
```

#### 9e-POST-CHECK: Validation Section Persistence (BLOCKING)

> **CRITICAL:** The `validation` section MUST be persisted to feature.json. Without it, consolidation checks fail.

```bash
MODULE_JSON="{module_feature_json_path}"
node -e "const d=JSON.parse(require('fs').readFileSync(process.argv[1],'utf-8'));
if(!d.validation||!d.validation.decision) { console.error('FAIL: validation section missing or incomplete'); process.exit(1); }
console.log('PASS: validation section present with decision');" "$MODULE_JSON"
```

IF this check FAILS → re-execute section 9e write and re-run this check.

#### 9f. Module Specification Checklist (BLOCKING)

> **CRITICAL:** This checklist MUST be FULLY COMPLETED before marking module status = "specified".

**Execute the comprehensive validation checklist:**

Run the 29-check validation process across 10 categories:
- Data Model (4 checks) | Business Rules (3 checks) | Use Cases & FRs (4 checks)
- Permissions (3 checks) | UI & Navigation (4 checks) | I18N & Messages (3 checks)
- Seed Data (2 checks) | API Endpoints (2 checks) | Validations (1 check) | Gherkin (3 checks)

```javascript
const blockingFailures = checks.filter(c => c.blocking && c.status === "FAIL");

IF blockingFailures.length > 0:
  // CONDITIONAL LOAD: Only load detailed checklist on validation failure
  Read references/validation-checklist.md
  Display:
    - Full validation logic for FAILED checks
    - Exact requirements (minimums, patterns, formats)
    - Fix recommendations for each failure

  BLOCKING ERROR: Module specification incomplete
  → Fix all failures before proceeding

ELSE:
  All blocking checks passed ✓
  Display: "{warnings_count} warnings (non-blocking)"
  → Proceed to mark module as "specified"
```

**Optimization:** The detailed 280-line validation-checklist.md is loaded **only when validation fails** (saves ~10,000 tokens on success path).

---

### 9g. Anti-Premature-Completion Guard (MANDATORY)

> **CRITICAL — NEVER say "the analysis is complete" or "ready for /ralph-loop" at this point.**
> Step-03d is the END of SPECIFICATION, NOT the end of the BA workflow.
> Steps 04a → 04b → 04c (consolidation) + 05a → 05b → 05c (handoff) are STILL REQUIRED.
> Without handoff, /ralph-loop will generate an INCOMPLETE PRD (no frontend, no tests).

**FORBIDDEN phrases in step-03d output:**
- "L'analyse métier est complète" / "The business analysis is complete"
- "Prêt pour /ralph-loop" / "Ready for /ralph-loop"
- "Vous pouvez maintenant lancer /ralph-loop"
- Any variation suggesting the BA is finished or /ralph-loop can be invoked

**REQUIRED phrasing:**
- "Module {name} spécifié. {Remaining} modules restants." (if more modules)
- "Tous les modules spécifiés. Passage à la consolidation (étapes 04a-04c)..." (if last module)
- NEVER mention /ralph-loop — only step-05c (ralph readiness) validates the handoff

### 10. Module Summary with Roles & Permissions

Display comprehensive summary:

```
═══════════════════════════════════════════════════════════
  MODULE SPÉCIFIÉ: {currentModule} ({currentModuleIndex + 1}/{total})
═══════════════════════════════════════════════════════════

| Aspect | Valeur |
|--------|--------|
| Entités | {count} ({entity names}) |
| Business Rules | {count} (VAL: X, CALC: Y, WF: Z) |
| Use Cases | {count} (UC-001 → UC-NNN) |
| Exigences | {count} FRs |
| Sections | {count} ({section names}) |
| Maquettes | {count} wireframes |
| Permissions | {count} paths |
| Tests Gherkin | {count} scénarios |
| Messages | {count} |

### Rôles & Permissions pour {currentModule}
| Rôle | Permissions |
|------|------------|
| {App} Admin | {app}.{module}.* |
| {App} Manager | read, create, update, assign |
| {App} Contributor | read, create, update |
| {App} Viewer | read |

→ Validation: {PASS/FAIL}
═══════════════════════════════════════════════════════════

{IF validation PASS}:
  → Module validé automatiquement
  → Passage automatique au module suivant (section 12)

{IF validation FAIL}:
  → Afficher les erreurs et offrir les options de correction (section 9d)
```

---

### 11. Write Module Feature.json

```
// Write analysis section with ALL subsections
ba-writer.enrichSection({
  featureId: {module_feature_id},
  section: "analysis",
  data: {
    objectives: [{id, objective, metric, target}],       // from 6a
    entities: [{name, description, attributes, relationships}],  // from 6b — SCHEMA FORMAT
    businessRules: [{id, name, category, statement, priority, conditions, examples, testability}],  // from 7 — SCHEMA FORMAT
    processFlow: {entryPoints, mainFlow, decisionPoints, alternativeFlows},  // from 6c
    dataLifecycle: {retentionPeriod, archiveStrategy, gdprCompliance, states}  // from 6d
  }
})

// Write specification section with ALL subsections
ba-writer.enrichSection({
  featureId: {module_feature_id},
  section: "specification",
  data: {
    actors: [{role, description, permissions}],                   // 8a
    useCases: [{id, name, primaryActor, permission, mainScenario, linkedRules}],  // 8b
    functionalRequirements: [{id, statement, priority, linkedRules, linkedUseCases, acceptanceCriteria}],  // 8c
    permissionMatrix: {permissions: [], roleAssignments: []},     // 8d
    navigation: {entries: []},                                    // 8e
    seedDataCore: {navigationModules, navigationSections, navigationResources, navigationTranslations, permissions, rolePermissions, permissionConstants},  // 8f (7 arrays)
    gherkinScenarios: [{feature, scenarios: [{name, tags, given, when, then}]}],  // 8g
    validations: [{entity, field, rules, errorMessageKey}],       // 8h
    messages: [{code, type, title, message, i18nKey}],            // 8i
    lifeCycles: [{entity, field, initialState, states: [{id, displayName, color, allowedTransitions, isTerminal}], transitions}],  // 8j
    apiEndpoints: [{method, path, permission, requestDto, responseDto, description}],  // 8k
    i18nKeys: {...},                                              // 8l
    sections: [{code, labels, route, icon, permission, wireframe, useCases, businessRules, resources}],  // from 3a-bis
    dashboards: [{code, title, kpis, filters, refreshMode}],     // from 3d
    uiWireframes: [{screen, section, mockup, elements, actions, componentMapping, layout}]  // from 3b
  }
})

// Write validation results from step 9
// (Already written in step 9e above)

ba-writer.updateStatus({module_feature_id}, "specified")

// Update master
ba-writer.updateModuleStatus({feature_id}, {currentModule.code}, "specified")
```

#### 11-POST-CHECK: Verify Written Data (BLOCKING)

> **CRITICAL — Data loss prevention.** After writing, READ BACK the module feature.json and verify the following arrays are **non-empty**:

```javascript
// READ BACK the written feature.json
const written = ba-reader.read({module_feature_id});

// BLOCKING checks — if ANY fails, the write was incomplete
const checks = [
  { key: "specification.actors",          actual: written.specification?.actors?.length,          min: 2 },
  { key: "specification.useCases",        actual: written.specification?.useCases?.length,        min: 2 },
  { key: "specification.wireframes",      actual: (written.specification?.uiWireframes?.length || written.specification?.wireframes?.length || 0), min: 1 },
  { key: "specification.sections",        actual: written.specification?.sections?.length,        min: 1 },
  { key: "specification.seedDataCore",    actual: Object.keys(written.specification?.seedDataCore || {}).length, min: 7 },
  { key: "specification.lifeCycles",      actual: written.specification?.lifeCycles?.length,      min: 0 },
  { key: "specification.gherkinScenarios",actual: written.specification?.gherkinScenarios?.length,min: 1 },
  { key: "specification.apiEndpoints",    actual: written.specification?.apiEndpoints?.length,    min: 1 }
];

const failures = checks.filter(c => (c.actual || 0) < c.min);

IF failures.length > 0:
  BLOCKING ERROR: "Feature.json write INCOMPLETE — missing data in: {failures.map(f => f.key).join(', ')}"
  → Re-execute section 11 write with ALL specification data
  → DO NOT proceed to next module until ALL checks pass

// SPECIAL CHECK: wireframes content verification (check BOTH key names)
const wireframes = written.specification?.uiWireframes || written.specification?.wireframes || [];
IF wireframes.length > 0:
  const emptyMockups = wireframes.filter(wf => !wf.mockup && !wf.ascii && !wf.content);
  IF emptyMockups.length > 0:
    WARNING: "{emptyMockups.length} wireframes have empty mockup content — verify step-03b data"

// SPECIAL CHECK: wireframes >= sections
const sectionCount = written.specification?.sections?.length || 0;
IF wireframes.length < sectionCount:
  BLOCKING ERROR: "{wireframes.length} wireframes < {sectionCount} sections — wireframes MISSING for some sections"
  → Re-read wireframes from conversation context and re-write to feature.json
```

#### 11-POST-CHECK-BASH: Acceptance Criteria Verification (BLOCKING)

> **CRITICAL:** The pseudocode checks above are interpreted by the model — they can be "passed" incorrectly.
> This bash check reads the REAL file on disk and provides an objective verification.
> See [references/acceptance-criteria.md](../references/acceptance-criteria.md) for the full acceptance criteria definition.

```bash
MODULE_JSON="{module_feature_json_path}"
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf-8'));
const spec = data.specification || {};
const analysis = data.analysis || {};
const wf = spec.uiWireframes || spec.wireframes || [];
const sections = spec.sections || [];
const checks = [
  ['entities >= 1',          (analysis.entities||[]).length,          1],
  ['useCases >= 2',          (spec.useCases||[]).length,              2],
  ['FRs >= 4',               (spec.functionalRequirements||[]).length,4],
  ['wireframes >= 1',        wf.length,                               1],
  ['wireframes >= sections', wf.length,                               sections.length],
  ['sections >= 1',          sections.length,                          1],
  ['seedDataCore 7 arrays',  Object.keys(spec.seedDataCore||{}).filter(k=>(spec.seedDataCore||{})[k]&&(spec.seedDataCore||{})[k].length>0).length, 7],
  ['gherkin is array',       Array.isArray(spec.gherkinScenarios)?1:0, 1],
  ['apiEndpoints >= 1',      (spec.apiEndpoints||[]).length,           1],
  ['messages >= 4',          (spec.messages||[]).length,               4],
  ['validations >= 1',       (spec.validations||[]).length,            1],
  ['navigation entries',     (spec.navigation?.entries||[]).length,    1],
  ['dataLifecycle',          analysis.dataLifecycle ? 1 : 0,           1],
  ['validation section',     data.validation ? 1 : 0,                  1]
];
// AC-18: i18n 4 languages check
const i18n = spec.i18nKeys || {};
const i18nLangs = ['fr','en','it','de'];
let i18nMissing = 0;
function checkI18nLeaf(obj) {
  if (typeof obj !== 'object' || obj === null) return;
  const hasLang = i18nLangs.some(l => typeof obj[l] === 'string');
  if (hasLang) { i18nLangs.forEach(l => { if (!obj[l]) i18nMissing++; }); }
  else { Object.values(obj).forEach(v => checkI18nLeaf(v)); }
}
checkI18nLeaf(i18n);
checks.push(['i18n 4 languages (missing keys)', 0, i18nMissing > 10 ? 1 : 0]);

const fails = checks.filter(c => c[1] < c[2]);
fails.forEach(f => console.error('FAIL: ' + f[0] + ' = ' + f[1] + ' (min: ' + f[2] + ')'));
if (i18nMissing > 0 && i18nMissing <= 10) { console.warn('WARNING: ' + i18nMissing + ' i18n leaf keys missing translations (IT/DE)'); }
// Check wireframe content
const emptyWf = wf.filter(w => !w.mockup && !w.ascii && !w.content);
if (emptyWf.length > 0) { fails.push(['wireframe content', 0, 1]); console.error('FAIL: ' + emptyWf.length + ' wireframes have EMPTY content'); }
// Check entity attribute types
const badAttrs = (analysis.entities||[]).flatMap(e => (e.attributes||[]).filter(a => !a.type).map(a => e.name+'.'+a.name));
if (badAttrs.length > 0) { fails.push(['attr.type', 0, 1]); console.error('FAIL: attributes without type: ' + badAttrs.join(', ')); }
// AC-15: Validation rules format
const badRules = (spec.validations||[]).filter(v => v.rules && !Array.isArray(v.rules));
if (badRules.length > 0) { fails.push(['AC-15: rules not array', badRules.length, 0]); console.error('FAIL: AC-15: ' + badRules.length + ' validations have rules as string'); }
// AC-16: Messages must have message field
const noMsg = (spec.messages||[]).filter(m => !m.message);
if (noMsg.length > 0) { fails.push(['AC-16: message missing', noMsg.length, 0]); console.error('FAIL: AC-16: ' + noMsg.length + ' messages missing message field'); }
// AC-17: Gherkin content structure
if (Array.isArray(spec.gherkinScenarios)) { const badG = spec.gherkinScenarios.filter(g => !g.feature || !Array.isArray(g.scenarios)); if (badG.length > 0) { fails.push(['AC-17: gherkin content', badG.length, 0]); console.error('FAIL: AC-17: ' + badG.length + ' gherkin entries invalid'); } }
if (fails.length > 0) { console.error('BLOCKING: ' + fails.length + ' acceptance criteria failed'); process.exit(1); }
console.log('PASS: All acceptance criteria met');
" "$MODULE_JSON"
```

IF this check FAILS:
- Identify which criteria failed from the output
- Fix the corresponding data (re-read from conversation context, re-generate, or re-run the step that produces it)
- Re-write to feature.json
- Re-run the POST-CHECK until PASS

> **WHY:** Step-03b now writes wireframes intermediately (since fix), but step-03d section 11 does the FULL write.
> This bash POST-CHECK is the ultimate safety net — it reads the REAL file and verifies ALL acceptance criteria.
> The pseudocode checks (section 9a) catch issues early; this bash check catches anything the model missed.

---

### 11-bis. Deploy Incremental Interactive HTML (MANDATORY)

> **After each module is specified, deploy/update the interactive HTML document with all available data.**
> The client can review completed modules while the next module is being specified.

See [references/validate-incremental-html.md](../references/validate-incremental-html.md) for the complete deployment process (read sources, build FEATURE_DATA, replace placeholders, write HTML).

Uses the **same mapping** as step-05b-deploy.md — only difference is `moduleSpecs` only includes completed modules.

---

### 12. Loop Decision

> **TEAM AGENT MODE: SKIP THIS ENTIRE SECTION.** If you are a team agent (your prompt contains `PROPOSE & REVIEW`), go directly to **section 12-bis** below. Section 12 is for inline mode ONLY. You MUST NOT load the next step or advance the module loop — the team lead handles orchestration.

**INLINE MODE ONLY (main conversation):**

```
ba-writer.advanceModuleLoop({feature_id})
→ Increments currentModuleIndex
→ Updates completedModules
→ Updates currentModule

IF currentModuleIndex < moduleOrder.length:
  Display: "→ Module suivant: {nextModule} ({currentModuleIndex + 1}/{total})"
  Load: steps/step-03a1-setup.md

IF currentModuleIndex >= moduleOrder.length:
  ba-writer.updateStatus({feature_id}, "specified")

  // CHECKPOINT: Save progress BEFORE transition (protects against context exhaustion)
  ba-writer.enrichSection({
    featureId: {feature_id},
    section: "metadata.workflow",
    data: {
      lastCompletedStep: "step-03d-validate",
      allModulesSpecified: true
    }
  })

  // CHECK: Are we in project mode (multi-application)?
  IF workflow.mode === "project":
    // APPLICATION LOOP: Check if more applications remain
    projectFeature = ba-reader.findProjectFeature()
    ba-writer.updateApplicationStatus(projectId, currentApp.code, "specified")
    ba-writer.advanceApplicationLoop(projectId)

    IF projectFeature.metadata.workflow.currentApplicationIndex + 1 < projectFeature.metadata.workflow.applicationOrder.length:
      nextApp = projectFeature.metadata.workflow.applicationOrder[currentApplicationIndex + 1]
      Display: "═══ Application {currentApp.name} terminée ({N}/{total}). Prochaine : {nextApp} ═══"
      Display: "⚠ NE PAS lancer /ralph-loop — d'autres applications et la consolidation sont encore nécessaires."
      // Load step-02 for next application's module decomposition
      Load: steps/step-02-decomposition.md
    ELSE:
      Display: "═══ Toutes les applications spécifiées! Passage à la consolidation cross-application... ═══"
      Display: "⚠ NE PAS lancer /ralph-loop — la consolidation et le handoff sont encore nécessaires."
      ba-writer.updateStatus(projectId, "specified")
      Load: steps/step-04a-collect.md
  ELSE:
    // SINGLE APPLICATION: Current behavior
    Display: "═══ Tous les modules spécifiés! Passage à la consolidation (04a→04c) puis handoff (05a→05c)... ═══"
    Display: "⚠ NE PAS lancer /ralph-loop — la consolidation et le handoff sont encore nécessaires."

    // MANDATORY TRANSITION — DO NOT STOP HERE
    // If context is near exhaustion, the checkpoint above ensures the user
    // can resume with /business-analyse and it will route to step-04a automatically.
    Load: steps/step-04a-collect.md
```

---

## 12-bis. Agent Mode: Send Proposal to Team Lead

**IF running as TEAM AGENT (autonomous mode — detected in step-03a1 MODE DETECTION):**

After all validation checks PASS and module feature.json is written:

### A. Build Structured Summary

Read the written module feature.json via ba-reader and build:

```
## Module: {moduleCode} — Specification Summary

### Entities ({count})
| Entity | Key Attributes | Relationships |
|--------|---------------|---------------|
| {name} | {attr1} ({type}), {attr2} ({type}), ... | → {relatedEntity} (FK) |

### Sections ({count})
| Section | Description | Resources |
|---------|-------------|-----------|
| {code} | {description} | {resource1}, {resource2}, ... |

### Use Cases ({count})
- {UC-ID}: {title}

### Business Rules ({count})
- {BR-ID}: {title} ({category})

### Permissions
- {permission.path}: {roles with access}

### Seed Data Counts
- Modules: {n} | Sections: {n} | Resources: {n}
- Translations: {n} | Permissions: {n} | Role mappings: {n} | Constants: {n}

### Wireframes
- {section}: {1-line description of layout and key components}
```

### B. Send Proposal

```
SendMessage({
  type: "message",
  recipient: "team-lead",
  content: "PROPOSAL_READY:{moduleCode}\n\n{structured_summary}",
  summary: "{moduleCode} specification ready for review"
})
```

### C. Wait for Response

**WAIT** for team lead to respond:

- **`APPROVED:{moduleCode}`** → Proceed to step D (Module Complete)
- **`REVISION:{moduleCode}\n{feedback}`** → Apply changes:
  1. Read the feedback
  2. Update the relevant sections in feature.json via ba-writer
  3. Re-run validation checks (section 9)
  4. Re-build summary and send new `PROPOSAL_READY`
  5. Max **3 revision cycles** — after 3, send `ERROR:{moduleCode}:Max revisions reached` and stop

### D. Module Complete

After approval:

```
ba-writer.updateModuleStatus({feature_id}, {moduleCode}, "specified")
```

```
SendMessage({
  type: "message",
  recipient: "team-lead",
  content: "MODULE_COMPLETE:{moduleCode}",
  summary: "{moduleCode} module complete"
})
```

Then WAIT — the team lead will send you a `shutdown_request`.
Do NOT loop to next module. The team lead handles module ordering and will spawn a new agent for the next module.

### E. Shutdown (MANDATORY)

When you receive a `shutdown_request` from the team lead:

```
SendMessage({
  type: "shutdown_response",
  request_id: "{requestId from the shutdown_request message}",
  approve: true
})
```

This terminates your process. Do NOT output any text after `shutdown_response`.

**Sequence reminder:** APPROVED → MODULE_COMPLETE → shutdown_request → shutdown_response (4 separate steps, never skip).

---

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

Before proceeding to step-03a (next module) or step-04 (consolidation), VERIFY:

1. **Module feature.json has specification section** with ALL sub-sections (actors, useCases, functionalRequirements, permissionMatrix, navigation, seedDataCore, gherkinScenarios, validations, messages, lifeCycles, apiEndpoints, i18nKeys, sections, uiWireframes)
2. **Module feature.json status** = "specified" (set by section 11)
3. **Master modules[].status** for this module = "specified" (set by section 11)
4. **ba-interactive.html exists** and includes data for this module (set by section 11-bis)

**IF any check fails → FIX before proceeding.** Do NOT advance the module loop with incomplete data.

---

## SINGLE-MODULE MODE

When only 1 module in moduleOrder:

- Same full validation flow
- Skip cross-module references (section 5)
- After validation → direct to step-04a-collect.md (auto-consolidation)

---

## RESUME SUPPORT

IF the step is resumed (e.g., after interruption):

```
Read metadata.workflow.currentModuleIndex
Read metadata.workflow.currentModule
→ Resume at the correct module
→ If module has partial data, show what exists and continue from there
```

---

## CONTEXT MANAGEMENT

| Loaded | Not Loaded |
|--------|------------|
| _shared.md | Other module feature.json (full) |
| feature-schema.json | Questionnaires not needed |
| Questionnaires for this module | Templates |
| Completed modules summary (100 lines max) | React viewer files |

---

## STATE VARIABLES

| Variable | Description |
|----------|-------------|
| `{currentModule}` | Current module being specified |
| `{currentModuleIndex}` | Index in moduleOrder |
| `{completedModules}` | Array of already specified module codes |
| `{module_feature_id}` | Feature ID for the module-level feature.json |

---

## SUCCESS METRICS

- Module specification validation PASS
- Per-module validation results recorded in feature.json
- Client confirmed module specification
- Module feature.json written with all sections
- Master updated with module status
- Interactive HTML deployed/updated with completed module data (incremental)
- Loop advanced correctly or consolidation phase initiated

---

## FAILURE MODES

- Validation FAIL → offer auto-correct / manual revise / force
- Client rejects → revise specific sections via step-03a or step-03b
- Cross-module reference error → clarify with client
- Interruption → resumable via workflow state

---

## NEXT STEP

Conditional:

- IF modules remaining → Load `steps/step-03a1-setup.md`
- IF all modules done → Load `steps/step-04a-collect.md`
