---
name: step-02-build-data
description: Build FEATURE_DATA and EMBEDDED_ARTIFACTS objects from collected JSON data
model: opus
next_step: steps/step-03-render.md
---

> **Reference files:** `references/data-mapping.md` | `references/data-build.md`

# Step 2: Build Data Objects

## YOUR TASK

Build `FEATURE_DATA` and `EMBEDDED_ARTIFACTS` objects from `{collected_data}` collected in step-01.

---

## EXECUTION SEQUENCE

### 1. Build FEATURE_DATA

Construct the complete FEATURE_DATA object following the exact mapping in `references/data-mapping.md`:

**metadata:**
```javascript
metadata: {
  applicationName: master.metadata.application,
  applicationId: master.id,
  version: master.version,
  createdAt: master.metadata.createdAt,
  lastModified: master.metadata.updatedAt,
  analysisMode: master.metadata.analysisMode || "interactive"
}
```

**cadrage.context** (lean format — MANDATORY fields logged if empty):
```javascript
const problem = master.cadrage.problem || "";
const trigger = master.cadrage.trigger || "";
const currentSituation = master.cadrage.asIs || "";
const desiredSituation = master.cadrage.toBe || "";

// MANDATORY field check — warn if core cadrage data is empty
if (!problem) console.warn("⚠ cadrage.problem is empty — HTML will show blank problem section");
if (!currentSituation && !desiredSituation) console.warn("⚠ cadrage has no current/desired situation");

context: {
  problem: problem,
  trigger: trigger,
  currentSituation: currentSituation,                 // asIs → currentSituation
  desiredSituation: desiredSituation,                  // toBe → desiredSituation
  painPoints: (master.cadrage.stakeholders || []).flatMap(s => s.painPoints || []).join("\n"),
  acceptanceCriteria: (master.cadrage.acceptanceCriteria || []).map(ac => ac.criterion).join("\n")
}
```

**cadrage.stakeholders** (with frequency/access conversion):
```javascript
stakeholders: master.cadrage.stakeholders.map(s => ({
  role: s.role,
  function: s.function || "",
  tasks: Array.isArray(s.tasks)
    ? s.tasks
    : typeof s.tasks === 'string'
      ? s.tasks.split(',').map(t => t.trim()).filter(Boolean)
      : [],
  frequency: mapFrequency(s.frequency),     // "Quotidien"→"daily", "Hebdomadaire"→"weekly", "Mensuel"→"monthly"
  access: mapAccess(s.involvement),         // "decision-maker"→"admin", "end-user"→"contributor", "observer"→"viewer"
  frustrations: (s.painPoints || []).join("\n")
}))
```

**cadrage.scope** (KEY CONVERSION — CRITICAL):
```javascript
scope: {
  inscope: (master.cadrage.globalScope?.inScope || []).map(item => ({ name: item, description: "" })),
  outofscope: (master.cadrage.globalScope?.outOfScope || []).map(item => ({ name: item, description: "" }))
}
```

> Safety net: also check `master.cadrage.coverageMatrix` as alternative source for scope items.


**modules[]:**
```javascript
modules: master.modules.map(m => ({
  code: m.code,
  name: m.name || m.code,                   // display name (MANDATORY)
  description: m.description || "",
  featureType: m.featureType || "data-centric",
  entities: m.entities || [],
  anticipatedSections: (m.anticipatedSections || []).map(s =>
    typeof s === 'string'
      ? { code: s, name: s, description: "", resources: [] }
      : { code: s.code || s.name || "", name: s.label || s.name || s.code || "", description: s.description || "", resources: s.resources || [], route: s.route || "", permission: s.permission || "" }
  ),
  dependencies: m.dependencies || [],
  dependents: m.dependents || [],
  estimatedComplexity: m.estimatedComplexity || "medium",
  status: m.status || "handed-off"
}))
```

**moduleSpecs{} — ONE entry per module (CRITICAL):**

> **FLAT-FILE ARCHITECTURE:** Module data comes from separate JSON files collected in step-01,
> NOT from index.json fields. Use `collected_data.modules[moduleCode].entities`, `.usecases`, etc.

For EACH module, use the flat file data from `collected_data.modules[moduleCode]`:

```javascript
const mod = collected_data.modules[moduleCode];

// NORMALIZE: usecases.json may use "useCases" (camelCase) or "usecases" (lowercase)
const rawUCs = mod.usecases?.useCases || mod.usecases?.usecases || [];
// NORMALIZE: rules.json may use "rules" or "businessRules"
const rawBRs = mod.rules?.rules || mod.rules?.businessRules || [];

moduleSpecs[moduleCode] = {
  useCases: rawUCs.map(uc => ({
    name: uc.name || uc.title || uc.id || "",
    sectionCode: uc.sectionCode || "",
    actor: uc.primaryActor || uc.actor || "",
    // SAFETY NET: steps may be strings[] (mainScenario) or objects[] ({step, action})
    steps: (uc.mainScenario || uc.steps || []).map(s =>
      typeof s === 'string' ? s : (s.action || s.description || "")
    ).join("\n"),
    alternative: (uc.alternativeScenarios || uc.alternativeFlows || []).map(a =>
      (a.name || a.trigger || "") + ": " + (a.steps || a.actions || []).map(s =>
        typeof s === 'string' ? s : (s.action || s.description || "")
      ).join(", ")
    ).join("\n")
  })),
  businessRules: rawBRs.map(br => ({
    name: br.name || br.id || "",
    sectionCode: br.sectionCode || "",
    category: br.category || "",
    statement: br.statement || br.description || "",
    example: (br.examples || []).map(e =>
      typeof e === 'string' ? e : ((e.input || "") + " → " + (e.expected || ""))
    ).join("; ")
  })),
  // ENTITY SAFETY NET: map fields[] → attributes[] if agent deviated
  entities: (mod.entities?.entities || []).map(ent => ({
    name: ent.name,
    description: ent.description || "",
    attributes: (ent.attributes || []).length > 0
      ? ent.attributes.map(a => ({ name: a.name, type: a.type || "string", required: a.required || false, description: a.description || "" }))
      : (ent.fields || []).map(f => ({ name: f.name, type: f.type || "string", required: f.required || false, description: f.description || "" })),
    relationships: (ent.relationships || []).map(r =>
      typeof r === 'string' ? r : r.target + " (" + r.type + ") - " + (r.description || ""))
  })),
  permissions: buildPermissionKeys(mod.permissions),
  apiEndpoints: mod.usecases?.apiEndpoints || []
}

// BUILD screens[] for HTML interactive mockups (SmartTable/SmartForm rendering)
// NORMALIZE: screens.json may use "screens[]" (flat) or "sections[]" (nested) format
const flatScr = mod.screens?.screens || [];
const sectionScr = mod.screens?.sections || [];
let screens = [];

// Helper: normalize a column definition regardless of field naming convention
function normalizeColumn(c) {
  return {
    field: c.code || c.name || c.id || c.field || c.fieldCode || "",
    label: c.label || c.displayName || c.code || c.name || "",
    type: c.renderAs === "badge" ? "badge" : (c.type || c.dataType || "text"),
    sortable: !!c.sortable
  };
}

// Helper: normalize a form field
function normalizeField(f) {
  return {
    field: f.code || f.name || f.id || f.fieldCode || f.field || "",
    label: f.label || f.displayName || f.code || f.name || "",
    type: f.type || f.fieldType || f.dataType || "text",
    required: !!f.required
  };
}

// Helper: normalize a filter
function normalizeFilter(f) {
  if (typeof f === 'string') return f;
  return f.label || f.displayName || f.filterLabel || f.name || f.code || f.filterCode || "";
}

// Helper: normalize an action
function normalizeAction(a) {
  if (typeof a === 'string') return a;
  return a.code || a.id || a.label || a.actionCode || "";
}

// Helper: normalize a KPI — replace formula values with sample display values
function normalizeKpi(k) {
  if (typeof k !== 'object') return { label: String(k), value: "0" };
  const label = k.label || k.displayName || "";
  const rawValue = k.value || k.metric || "0";
  let displayValue = rawValue;
  // Detect formula patterns (e.g., count(...), sum(...), ratio(...), avg(...))
  if (typeof rawValue === 'string' && rawValue.includes('(')) {
    const lbl = label.toLowerCase();
    if (rawValue.includes('ratio') || rawValue.includes('%') || lbl.includes('taux') || lbl.includes('rate'))
      displayValue = "87.5%";
    else if (rawValue.includes('avg') || lbl.includes('moyen') || lbl.includes('average'))
      displayValue = "12.4";
    else if (rawValue.includes('sum'))
      displayValue = (lbl.includes('jour') || lbl.includes('day') || lbl.includes('heure') || lbl.includes('hour')) ? "156" : "45,678";
    else
      displayValue = "1,234";
  }
  // Also handle format hint from the KPI definition
  if (k.format === 'percentage' && displayValue === rawValue) displayValue = "80%";
  return { label: label, value: displayValue };
}

if (flatScr.length > 0) {
  // FORMAT A: flat screens[] array (ba-003 style)
  const bySec = {};
  flatScr.forEach(s => {
    const sec = s.section || "default";
    if (!bySec[sec]) bySec[sec] = {
      sectionCode: sec,
      sectionLabel: s.sectionLabel || s.sectionDescription || sec,
      resources: []
    };
    bySec[sec].resources.push({
      code: s.screen || s.name || "",
      label: s.sectionLabel || s.description || s.screen || "",
      type: s.componentType || "unknown",
      columns: (s.columns || []).map(normalizeColumn),
      filters: (s.filters || []).map(normalizeFilter),
      fields: [],
      tabs: (s.tabs || []).map(t => ({
        label: t.label || t.tabLabel || t.code || "",
        fields: (t.fields || []).map(normalizeField)
      })),
      actions: (s.actions || []).map(normalizeAction),
      kpis: (s.kpis || []).map(normalizeKpi),
      options: s.options || [],
      permission: s.permission || "",
      notes: s.description || s.sectionDescription || ""
    });
  });
  screens = Object.values(bySec);
} else if (sectionScr.length > 0) {
  // DETECT sub-format: canonical (resources[] inside sections) vs ba-004 (data on section itself)
  const hasNestedResources = sectionScr.some(sec => (sec.resources || []).length > 0);

  if (hasNestedResources) {
    // FORMAT B-CANONICAL: sections[] with resources[] (ba-005+ style — sectionCode/sectionLabel/resources[])
    // Resources have their own code/type/columns/tabs/kpis — iterate over them
    screens = sectionScr.map(sec => ({
      sectionCode: sec.sectionCode || sec.id || sec.code || "",
      sectionLabel: sec.sectionLabel || sec.displayName || sec.label || sec.sectionCode || "",
      resources: (sec.resources || []).map(res => ({
        code: res.code || res.id || "",
        label: res.label || res.displayName || res.code || "",
        type: res.type || res.componentType || res.layout || "unknown",
        columns: (res.columns || []).map(normalizeColumn),
        filters: (res.filters || []).map(normalizeFilter),
        fields: [],
        tabs: (res.tabs || []).map(t => ({
          label: t.label || t.displayName || t.code || t.id || "",
          fields: (t.fields || []).map(normalizeField)
        })),
        actions: (res.actions || []).map(normalizeAction),
        kpis: (res.kpis || []).map(normalizeKpi),
        options: res.options || [],
        permission: res.permission || "",
        notes: res.notes || res.description || ""
      }))
    }));
  } else {
    // FORMAT B-LEGACY: sections[] without resources[] (ba-004 style — data directly on section)
    const bySec = {};
    sectionScr.forEach(sec => {
      const secId = sec.id || sec.code || sec.sectionCode || "";
      const secLabel = sec.displayName || sec.label || sec.sectionLabel || secId;
      const componentType = sec.layout || sec.componentType || "unknown";
      if (!bySec[secId]) bySec[secId] = {
        sectionCode: secId,
        sectionLabel: secLabel,
        resources: []
      };
      bySec[secId].resources.push({
        code: secId,
        label: secLabel,
        type: componentType,
        columns: (sec.columns || []).map(normalizeColumn),
        filters: (sec.filters || []).map(normalizeFilter),
        fields: [],
        tabs: (sec.tabs || []).map(t => ({
          label: t.displayName || t.label || t.code || t.id || "",
          fields: (t.fields || []).map(normalizeField)
        })),
        actions: (sec.actions || []).map(normalizeAction),
        kpis: (sec.kpis || []).map(normalizeKpi),
        options: sec.options || [],
        permission: (typeof sec.permissions === 'object' ? sec.permissions?.view : sec.permission) || "",
        notes: sec.description || ""
      });
    });
    screens = Object.values(bySec);
  }
}
moduleSpecs[moduleCode].screens = screens;
```

> See `references/data-mapping.md` for `buildPermissionKeys()` implementation.

**Enrich anticipatedSections with section-level UC/BR (AFTER moduleSpecs is built):**

```javascript
// Post-processing: populate each section's useCases[] and businessRules[]
// using the sectionCode field from moduleSpecs
FEATURE_DATA.modules.forEach(m => {
  const spec = FEATURE_DATA.moduleSpecs[m.code];
  if (!spec) return;
  (m.anticipatedSections || []).forEach(section => {
    section.useCases = (spec.useCases || []).filter(uc => uc.sectionCode === section.code);
    section.businessRules = (spec.businessRules || []).filter(br => br.sectionCode === section.code);
  });
});
```

> This step is CRITICAL for the HTML viewer to display UC/BR grouped by section.

### Post-Build Self-Check (MANDATORY — BLOCKING)

After enriching anticipatedSections, run this self-check to detect data loss:

```javascript
// SELF-CHECK: compare source file counts vs FEATURE_DATA counts
const errors = [];
FEATURE_DATA.modules.forEach(m => {
  const spec = FEATURE_DATA.moduleSpecs[m.code];
  const source = collected_data.modules[m.code];
  if (!spec) { errors.push(`MISSING moduleSpecs[${m.code}]`); return; }

  // Check useCases (handle both key conventions)
  const srcUC = (source?.usecases?.useCases || source?.usecases?.usecases || []).length;
  const bltUC = (spec.useCases || []).length;
  if (srcUC > 0 && bltUC === 0)
    errors.push(`${m.code}: useCases EMPTY but source has ${srcUC}`);

  // Check businessRules (handle both key conventions)
  const srcBR = (source?.rules?.rules || source?.rules?.businessRules || []).length;
  const bltBR = (spec.businessRules || []).length;
  if (srcBR > 0 && bltBR === 0)
    errors.push(`${m.code}: businessRules EMPTY but source has ${srcBR}`);

  // Check entities
  const srcEnt = (source?.entities?.entities || []).length;
  const bltEnt = (spec.entities || []).length;
  if (srcEnt > 0 && bltEnt === 0)
    errors.push(`${m.code}: entities EMPTY but source has ${srcEnt}`);

  // Check screens (handle both formats: screens[] and sections[])
  const srcScreens = (source?.screens?.screens || source?.screens?.sections || []).length;
  const bltScreenResources = (spec.screens || []).reduce(
    (acc, s) => acc + (s.resources || []).length, 0);
  if (srcScreens > 0 && bltScreenResources === 0)
    errors.push(`${m.code}: screens EMPTY (0 resources) but source has ${srcScreens} screens/sections`);

  // Check serialization: [object Object] in useCases steps = BUG
  (spec.useCases || []).forEach(uc => {
    if (uc.steps && uc.steps.includes("[object Object]"))
      errors.push(`${m.code}: UC "${uc.name}" has [object Object] in steps — steps[] contains objects not strings`);
  });
});

if (errors.length > 0) {
  Display("⛔ SELF-CHECK FAILED — data loss detected:");
  errors.forEach(e => Display("  - " + e));
  // FIX: re-map the failing modules from collected_data before continuing
}
```

> If self-check detects errors, you MUST re-read the source data and re-build the failing moduleSpecs entries.
> Do NOT proceed with empty arrays when source data exists.

**dependencies[] (TOP-LEVEL — CRITICAL for HTML navigation):**
```javascript
dependencies: (master.consolidation?.crossModuleInteractions || master.dependencyGraph?.edges || []).map(i => ({
  from: i.fromModule || i.from || "",
  to: i.toModule || i.to || "",
  description: i.description || ""
}))
```

> **WARNING:** `data.dependencies` is used by the sidebar (navigation), module rendering, consolidation views,
> handoff summary, and export. If this array is missing, the entire HTML page crashes with a TypeError.
> ALWAYS include it, even if empty (`dependencies: []`).

**consolidation:**
```javascript
consolidation: {
  interactions: (master.consolidation?.crossModuleInteractions || []).map(i => ({
    from: i.fromModule,
    to: i.toModule,
    description: i.description || ""
  })),
  sharedEntities: master.consolidation?.sharedEntities || [],
  e2eFlows: (master.consolidation?.e2eFlows || []).map(f => ({
    name: f.name,
    steps: (f.steps || []).map(s => ({ module: s.module, action: s.action })),
    actors: (f.steps || []).map(s => s.permission).join(", ")
  }))
}
```

**handoff:**
```javascript
handoff: {
  complexity: master.handoff?.complexity,
  implementationStrategy: master.handoff?.implementationStrategy,
  moduleOrder: master.handoff?.moduleOrder,
  filesToCreate: master.handoff?.filesToCreate,
  brToCodeMapping: master.handoff?.brToCodeMapping,
  apiEndpointSummary: master.handoff?.apiEndpointSummary
}
```

---

### 2. Build EMBEDDED_ARTIFACTS

> **FIELD RENAME WARNING:** Module JSON uses `mockupFormat`/`mockup`. HTML reads `format`/`content`. You MUST rename.

**wireframes{} — per moduleCode:**

> **FLAT-FILE:** Wireframes come from `screens.json` in each module directory (collected in step-01),
> NOT from `moduleFeature.specification.uiWireframes`.

```javascript
const mod = collected_data.modules[moduleCode];

// STEP 1: Collect wireframe sources from BOTH flat and nested structures
// Flat: mod.screens.screens[] with mockupFormat/mockup at top level
// Nested: mod.screens.sections[].resources[].wireframe (design step output)
let rawWireframes = [];

// Source A: flat screens[] array (original BA output or manually enriched)
const flatScreens = mod.screens?.screens || [];
rawWireframes.push(...flatScreens.filter(s => s.mockup || s.mockupFormat));

// Source B: nested sections[].resources[].wireframe (design step output)
const sections = mod.screens?.sections || [];
for (const section of sections) {
  // B1: wireframe directly on section (ba-004 style: sections[].wireframe)
  if (section.wireframe) {
    rawWireframes.push({
      ...section.wireframe,
      section: section.wireframe.section || section.id || section.code || section.sectionCode || "",
      screen: section.wireframe.screen || section.id || section.code || section.sectionCode || ""
    });
  }
  // B2: wireframe on resources (nested: sections[].resources[].wireframe)
  for (const resource of (section.resources || [])) {
    if (resource.wireframe) {
      rawWireframes.push({
        ...resource.wireframe,
        section: resource.wireframe.section || section.sectionCode || section.id || "",
        screen: resource.wireframe.screen || `${section.sectionCode || section.id}-${resource.code}` || ""
      });
    }
  }
}

// Source C: screens/sections without mockup → auto-generate text description as wireframe fallback
// C1: from flat screens[]
const screensWithoutMockup = flatScreens.filter(s => !s.mockup && !s.mockupFormat);
// C2: from sections[] (ba-004 style) — sections without wireframe property
const sectionsWithoutWireframe = sections.filter(s => !s.wireframe);
const allNoMockup = [
  ...screensWithoutMockup.map(s => ({
    type: s.componentType || "", columns: s.columns || [], tabs: s.tabs || [],
    kpis: s.kpis || [], screen: s.screen || "", section: s.section || "",
    description: s.sectionDescription || ""
  })),
  ...sectionsWithoutWireframe.map(s => ({
    type: s.layout || s.componentType || "", columns: s.columns || [], tabs: s.tabs || [],
    kpis: s.kpis || [], screen: s.id || s.code || "", section: s.id || s.code || "",
    description: s.description || ""
  }))
];
for (const screen of allNoMockup) {
  const type = screen.type;
  let desc = "";
  if (type.includes("Table") || type.includes("Grid")) {
    const cols = screen.columns;
    desc = "Tableau avec " + cols.length + " colonnes : " + cols.map(c => c.label || c.displayName || c.code || c.name).join(", ");
  } else if (type.includes("Form")) {
    const tabs = screen.tabs;
    desc = "Formulaire " + (tabs.length > 0 ? "avec " + tabs.length + " onglet(s)" : "");
  } else if (type.includes("Dashboard")) {
    desc = "Tableau de bord avec " + (screen.kpis.length || "") + " KPIs";
  } else if (type.includes("Kanban")) {
    desc = "Vue Kanban";
  }
  if (desc) {
    rawWireframes.push({
      screen: screen.screen, section: screen.section,
      mockupFormat: "text", mockup: desc,
      description: screen.description, elements: [], componentMapping: []
    });
  }
}

// STEP 2: Map to HTML format (RENAME: mockupFormat → format, mockup → content)
wireframes: {
  [moduleCode]: rawWireframes.map(wf => ({
      screen: wf.screen || wf.name || wf.title || wf.id || "",
      section: wf.section || "",
      format: wf.mockupFormat || "ascii",                   // RENAME: mockupFormat → format
      content: wf.mockup || wf.ascii || wf.content || "",   // RENAME: mockup → content
      svgContent: null,
      description: wf.description || "",
      elements: wf.elements || [],
      actions: wf.actions || [],
      componentMapping: Array.isArray(wf.componentMapping) ? wf.componentMapping
        : typeof wf.componentMapping === 'object' && wf.componentMapping !== null
          ? Object.entries(wf.componentMapping).map(([k, v]) => ({ wireframeElement: k, reactComponent: v }))
          : [],
      layout: typeof wf.layout === 'object' ? wf.layout : null,
      permissionsRequired: wf.permissionsRequired || []
    }))
}
```

**e2eFlows[]:**
```javascript
e2eFlows: (master.consolidation?.e2eFlows || []).map(flow => ({
  name: flow.name,
  diagram: flow.steps.map(s => s.action + "(" + s.module + ")").join(" ──→ "),
  steps: flow.steps || [],
  actors: [...new Set(flow.steps.map(s => s.permission?.split(".")[0]).filter(Boolean))].join(", "),
  modules: [...new Set(flow.steps.map(s => s.module))].join(" → ")
}))
```

**dependencyGraph:**
```javascript
dependencyGraph: {
  nodes: (master.modules || []).map(m => ({
    id: m.code, label: m.code, type: m.featureType || "data-centric"
  })),
  edges: (master.dependencyGraph?.edges || []).map(e => ({
    from: e.from, to: e.to, description: e.description || ""
  }))
}
```

---

### 3. Generate SVG Wireframes (Parallel, Optional)

> This step is NEVER blocking — ASCII-only wireframes are acceptable.
> **Reference:** `business-analyse/references/wireframe-svg-style-guide.md` for full agent prompt template

1. Collect all wireframes where `content` exists and `svgContent` is null
2. Spawn parallel Task(sonnet) agents — ONE per wireframe
3. Validate results: strip markdown fences, verify SVG syntax
4. Inject valid SVGs into `EMBEDDED_ARTIFACTS.wireframes[moduleCode][index].svgContent`
5. Display: `"SVG wireframe generation: {successCount}/{total} generated, {failCount} fallback (ASCII)"`

---

## VALIDATION

- FEATURE_DATA.moduleSpecs must have ONE entry per module
- cadrage.scope uses HTML keys (inscope/outofscope)
- Wireframe fields use format/content (NOT mockupFormat/mockup)
- Per-module: useCases/businessRules/entities count must match source (empty when source has data = BUG)

## NEXT STEP

Load: `steps/step-03-render.md`
