---
name: 02-screen-format-detection
description: Detection and normalization logic for screens.json format variants (A, B-CANONICAL, B-LEGACY)
---

# Screen Format Detection

The `screens.json` file can be produced in multiple formats depending on the BA workflow step that generated it. This reference documents the detection logic and normalization for each format variant.

## Format Overview

| Format | Structure | Source | Key Fields |
|--------|-----------|--------|-----------|
| **A** | `screens[]` (flat) | ba-003 (original BA output) | `screen`, `section`, `columns[]`, `filters[]`, `tabs[]`, `actions[]`, `kpis[]`, `charts[]` |
| **B-CANONICAL** | `sections[].resources[]` | ba-005+ (design output) | `sectionCode`, `sectionLabel`, `resources[]` with nested structure |
| **B-LEGACY** | `sections[]` (flat within section) | ba-004 (intermediate step) | `sections[]` with data directly on section, no resources[] nesting |

## Format A: Flat screens[] Array

**Detection:** `mod.screens?.screens` array exists and has length > 0.

**Schema:**
```javascript
screens: [
  {
    screen: "ScreenName",
    section: "SectionCode",
    sectionLabel: "Section Label",
    description: "...",
    sectionDescription: "...",
    componentType: "SmartTable|SmartForm|Dashboard|Kanban",
    columns: [...],           // for table components
    filters: [...],           // simple filters
    tabs: [...],              // form tabs
    actions: [...],           // action buttons
    kpis: [...],              // dashboard KPIs
    charts: [...],            // dashboard charts
    options: [...],           // component options
    permission: "...",        // ACL permission
    mockup: "...",            // wireframe content
    mockupFormat: "..."       // wireframe format
  }
]
```

**Normalization:**

Group flat screens by section, then map each to a `{sectionCode, sectionLabel, resources[]}` structure:

```javascript
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),
      charts: (s.charts || []).map(normalizeChart),
      options: s.options || [],
      permission: s.permission || "",
      notes: s.description || s.sectionDescription || ""
    });
  });
  screens = Object.values(bySec);
}
```

**Output:** Array of `{sectionCode, sectionLabel, resources[]}` objects (equivalent to B-CANONICAL).

## Format B: Detect Sub-variant (B-CANONICAL vs B-LEGACY)

**Detection:** `mod.screens?.sections` array exists and has length > 0.

**Decision point:** Check if any section contains `resources[]`:

```javascript
const hasNestedResources = sectionScr.some(sec => (sec.resources || []).length > 0);
```

### Format B-CANONICAL: sections[] with resources[]

**Schema:**
```javascript
sections: [
  {
    sectionCode: "SectionCode",
    sectionLabel: "Section Label",
    displayName: "...",
    label: "...",
    id: "...",
    code: "...",
    resources: [
      {
        code: "ResourceCode",
        id: "...",
        label: "Resource Label",
        displayName: "...",
        type: "SmartTable|SmartForm|Dashboard|Kanban",
        componentType: "...",
        layout: "...",
        columns: [...],       // normalized column definitions
        filters: [...],       // SmartFilter-style filters
        tabs: [...],          // form tabs
        actions: [...],       // action buttons
        kpis: [...],          // KPIs
        charts: [...],        // chart definitions
        options: [...],       // component options
        permission: "...",    // ACL permission
        notes: "...",         // description
        description: "..."
      }
    ]
  }
]
```

**Normalization:**

```javascript
if (hasNestedResources) {
  // FORMAT B-CANONICAL: sections[] with resources[] (ba-005+ style)
  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.type === 'SmartFilter'
        ? (res.filters || []).map(normalizeSmartFilterField)
        : (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),
      charts: (res.charts || []).map(normalizeChart),
      options: res.options || [],
      permission: res.permission || "",
      notes: res.notes || res.description || ""
    }))
  }));
}
```

**Key feature:** SmartFilter type detected and uses rich filter normalization (`normalizeSmartFilterField`) instead of simple label extraction.

### Format B-LEGACY: sections[] without resources[]

**Schema:**
```javascript
sections: [
  {
    id: "SectionId",
    code: "SectionCode",
    sectionCode: "...",
    displayName: "Section Label",
    label: "...",
    layout: "SmartTable|SmartForm|Dashboard",
    componentType: "...",
    columns: [...],       // data directly on section
    filters: [...],       // simple filters
    tabs: [...],          // form tabs
    actions: [...],       // action buttons
    kpis: [...],          // KPIs
    charts: [...],        // chart definitions
    options: [...],       // options
    permissions: {view: "..."} or permission: "...",
    description: "...",
    // NO resources[] nesting
  }
]
```

**Normalization:**

Group sections by ID/code, then map into `{sectionCode, sectionLabel, resources[]}` structure:

```javascript
} else {
  // FORMAT B-LEGACY: sections[] without resources[] (ba-004 style)
  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),
      charts: (sec.charts || []).map(normalizeChart),
      options: sec.options || [],
      permission: (typeof sec.permissions === 'object' ? sec.permissions?.view : sec.permission) || "",
      notes: sec.description || ""
    });
  });
  screens = Object.values(bySec);
}
```

**Key differences:**
- Sections have data directly (no `resources[]` nesting)
- Permissions may be object `{view: "..."}` or simple string
- One resource per section (grouped by ID)

## Output Normalization

All three formats normalize to a **canonical output structure**:

```javascript
screens: [
  {
    sectionCode: "string",
    sectionLabel: "string",
    resources: [
      {
        code: "string",
        label: "string",
        type: "string",
        columns: [...],         // normalized via normalizeColumn()
        filters: [...],         // normalized via normalizeFilter() or normalizeSmartFilterField()
        fields: [],
        tabs: [...],            // with normalized fields
        actions: [...],         // normalized via normalizeAction()
        kpis: [...],            // normalized via normalizeKpi()
        charts: [...],          // normalized via normalizeChart()
        options: [...],
        permission: "string",
        notes: "string"
      }
    ]
  }
]
```

This canonical form is then used to populate `moduleSpecs[moduleCode].screens` and render in the HTML viewer.

## Detection Flow

```
mod.screens exists?
├─ YES: Do we have mod.screens.screens[] with length > 0?
│  └─ YES: Format A (flat) → normalize by section → canonical
│  └─ NO: Continue to B detection
├─ Do we have mod.screens.sections[] with length > 0?
│  └─ YES: Check hasNestedResources (any section.resources[] exists?)
│     ├─ YES: Format B-CANONICAL → normalize permissions → canonical
│     └─ NO: Format B-LEGACY → group by ID → canonical
└─ NO: screens = [] (empty)
```
