---
name: fluent-frontend-build
description: Build, test, and validate a Mystique SDK component project. Local dev mode starts a hot-reload server on localhost:3001, wires it into the Fluent manifest, and supports live iteration. Production mode runs 7 quality gates (TypeScript, lint, tests, webpack, bundle, externals, GraphQL schema validation). Frontend equivalent of /fluent-build. Triggers on "build frontend", "build component", "yarn build", "frontend build", "sdk build", "build bundle", "test locally", "local dev", "start dev server", "run locally".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [project-path] [full|build|test|lint|check]
cursor-tier: manual
planning-gate: advisory
---

# Frontend Build — SDK Component Projects

Build, test, and validate a Mystique SDK component project. This is the frontend equivalent of `/fluent-build` (which handles Maven/Java).

**Recommended workflow: local dev first, production build second.**

1. **`/fluent-frontend-build dev`** — Start local dev server (localhost:3001), wire manifest, iterate with hot-reload. Use this first to develop and test components in the live Fluent UI.
2. **`/fluent-frontend-build`** — Run 7-gate production pipeline (TypeScript → lint → tests → webpack → bundle verify → externals check → GraphQL schema validation). Use this when ready to deploy to CDN.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-frontend-build.jsonl` exists
2. If yes, read last 20 records
3. Filter to: FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal guidance during execution
5. Do NOT show raw feedback to the user — silently adjust approach based on past issues

## Planning Gate

**Advisory.** For a fresh build of an already-scaffolded project, no plan is needed. For builds that are part of a larger feature deployment, check for an approved plan in `accounts/<PROFILE>/features/*/status.json`.

## Ownership Boundary

| This skill does | This skill does NOT do |
|---|---|
| TypeScript compilation check (`tsc --noEmit`) | Create or scaffold the project (→ `/fluent-mystique-scaffold`) |
| Run ESLint (`yarn lint`) | Edit source code to fix issues (→ user or `/fluent-frontend-review`) |
| Run Jest tests with coverage (`yarn test --coverage`) | Build or edit manifest JSON (→ `/fluent-mystique-builder`) |
| Production webpack build (`yarn build`) | Deploy manifest settings (→ `/fluent-settings`) |
| Verify bundle output (size, version.json, externals) | Deploy bundle to CDN (→ user / CI/CD) |
| Generate structured build report | Browser-test the deployed UI (→ `/fluent-ui-test`) |

## Handoff Protocol

| Signal | Meaning | Example |
|---|---|---|
| `→ READY: <path>` | Build passed, bundle produced | `→ READY: accounts/<PROFILE>/SOURCE/frontend/my-plugin/dist/bundle.abc123.js` |
| `→ REVIEW: <path>` | Build report produced, needs review | `→ REVIEW: accounts/<PROFILE>/reports/frontend-build/my-plugin-2026-02-28.json` |
| `→ BLOCKED: <reason>` | Build failed | `→ BLOCKED: VALIDATION_FAILED — 3 TypeScript errors, 2 failing tests` |

## When to Use

- **Local testing first** — After scaffolding or modifying components, use `dev` mode to test in the live Fluent UI before production build
- After scaffolding a new SDK project (`/fluent-mystique-scaffold`) — start with `dev` to verify components render
- After adding or modifying components (`/fluent-mystique-component`) — iterate locally, then production build
- Before deploying to CDN — run full production pipeline to verify quality gates
- As part of a full-stack feature deployment pipeline
- When the user says "build", "compile", "test", "check", "test locally", "run locally", "start dev" in a frontend context

## Required Inputs

| Input | Source | Required |
|---|---|---|
| Project path | `accounts/<PROFILE>/SOURCE/frontend/<project>/` | Yes |
| PROFILE | Environment or user-specified | Yes |

## Pre-Flight Checks

Before running any build command:

1. **Project exists**: `accounts/<PROFILE>/SOURCE/frontend/<project>/package.json` must exist
2. **SDK types present**: `accounts/<PROFILE>/SOURCE/frontend/<project>/@types/mystique/` directory exists
3. **Dependencies installed**: `node_modules/` exists. If not, run `yarn install` first
4. **Lib runtime bundles present**: `lib/mystique.js` and `lib/material-ui.js` exist (required for Jest runtime globals). Storybook uses `.storybook/mocks/mystique/` directory alias instead
5. **Bridge files present**: `lib/init.js` and `lib/mystique-export.js` exist (required by webpack/Jest wiring)
6. **Local server reuse gate (dev mode)**: before running `yarn start`, check whether `http://localhost:3001/version.json` is already responding and reuse the existing server when it matches the target project.

If any pre-flight check fails, report it and STOP — do not proceed to build phases.

## Build Pipeline (7 Gates)

Run gates sequentially. If any gate fails, report ALL errors from that gate, then STOP.

### Gate 1: TypeScript Compilation

```bash
cd accounts/<PROFILE>/SOURCE/frontend/<project>
npx tsc --noEmit
```

**Pass criteria:** Exit code 0, zero errors.
**On failure:** Report all TypeScript errors with file:line references. Do NOT proceed.

### Gate 2: Lint

```bash
cd accounts/<PROFILE>/SOURCE/frontend/<project>
yarn lint 2>&1
```

**Pass criteria:** Zero errors. Warnings are acceptable.
**On failure:** Report lint errors. Suggest `yarn lint:fix` for auto-fixable issues.

If lint errors are only in vendored SDK files (`@types/mystique/**`, `lib/**`) or build/config plumbing (`webpack.config.ts`, `build/**`), run a targeted lint gate against authored source:

```bash
npx eslint src manifests --ext .ts,.tsx --no-error-on-unmatched-pattern 2>&1
```

Use the targeted lint result as the blocking decision for Gate 2.

### Gate 3: Tests with Coverage

```bash
cd accounts/<PROFILE>/SOURCE/frontend/<project>
yarn test --coverage 2>&1
```

If this fails because the project test script already includes `--coverage` or has CLI-sensitive `testPathIgnorePatterns`, rerun:

```bash
yarn test 2>&1
```

**Pass criteria:** All tests pass. Coverage reported.
**On failure:** Report failing tests with assertion details. Do NOT proceed.

Coverage thresholds:
- Use project-defined `jest.config.js` `coverageThreshold` when present.
- If no explicit threshold is configured, use **70%** as the recommended minimum for statements/branches/functions/lines and report below-threshold coverage as WARN.

### Gate 3b: Storybook Story Coverage

**Purpose:** Verify that registered ComponentRegistry and FieldRegistry components have corresponding Storybook stories. Stories enable visual regression, isolated testing, and design review without deploying to the live app.

**Skip conditions:**
- No `.storybook/` directory in project → SKIP (Storybook not configured)
- `--skip-stories` flag → SKIP

**Step 1 — Extract registered components and fields:**

```bash
grep -n "ComponentRegistry.register\|FieldRegistry.register" src/index.tsx
```

For each registration, extract the component name and source file path.

**Step 2 — Check for matching story files:**

For each registered component/field, check for a co-located `.stories.tsx` file:

| Registry | Source path | Expected story path |
|----------|-----------|-------------------|
| ComponentRegistry | `src/components/<Name>/<Name>.tsx` | `src/components/<Name>/<Name>.stories.tsx` |
| FieldRegistry | `src/fields/<Name>/<Name>.tsx` | `src/fields/<Name>/<Name>.stories.tsx` |

**Checks:**

| Check | Severity | Pass Condition |
|-------|----------|----------------|
| ComponentRegistry component has story | WARN | `.stories.tsx` file exists in component directory |
| FieldRegistry field has story | WARN | `.stories.tsx` file exists in field directory |
| Story file has multiple exports | INFO | At least 2 named exports (Default + one variant) — encourages edge case coverage |

**Story requirement matrix:**

| Artifact type | Stories mandatory? | Rationale |
|---|---|---|
| FieldRegistry components | Yes | High-risk — handle user input, validation, onChange contract. Must test all states visually. |
| ComponentRegistry (content) | Yes | Render data — need to verify empty, loading, error, and various data shapes. |
| ComponentRegistry (data-heavy, 3+ hooks) | Recommended | Mock setup cost is high — `/fluent-mystique-preview` may be more efficient for visual validation. |
| TemplateRegistry helpers | No | No visual output to story. |

**Build impact:** Gate 3b failures produce WARN, not FAIL — the bundle is valid, but story coverage gaps reduce confidence in visual correctness. Present as a soft warning.

**On WARN:** List components/fields missing stories. Suggest: `"Run yarn storybook to develop stories in isolation before production build."`

### Gate 4: Production Build

```bash
cd accounts/<PROFILE>/SOURCE/frontend/<project>
yarn build 2>&1
```

**Pass criteria:** Exit code 0, `dist/` directory produced with files.
**On failure:** Report webpack errors. Common issues:
- Missing imports → TypeScript should have caught this (Gate 1 failure)
- Emotion/babel config → Check `.babelrc` for ESM-incompatible presets
- Missing externals config → webpack.config.ts `externals` block

### Gate 5: Bundle Verification

After successful build, verify the output:

```bash
# Check bundle exists
ls accounts/<PROFILE>/SOURCE/frontend/<project>/dist/bundle.*.js

# Check version.json exists
cat accounts/<PROFILE>/SOURCE/frontend/<project>/dist/version.json

# Check bundle size
wc -c accounts/<PROFILE>/SOURCE/frontend/<project>/dist/bundle.*.js
```

**Checks:**

| Check | Pass | Warn | Fail |
|---|---|---|---|
| `dist/bundle.*.js` exists | File found | — | No bundle file |
| `dist/version.json` exists | File found, valid JSON with `bundle` key | — | Missing or invalid |
| Bundle size | < 200KB | 200KB–500KB | > 500KB |
| Multiple bundle files | Exactly 1 | — | 0 or 2+ |

### Gate 5b: i18n Verification

Check that the i18n directory structure exists and translation keys are consistent.

**Checks:**

| Check | Pass | Warn | Fail |
|---|---|---|---|
| `i18n/locales/en/translation.json` exists | File found, valid JSON | — | Missing (skip if project has zero `translateOr` calls) |
| `dist/i18n/` populated after build | Directory exists with locale files | — | Missing (webpack CopyPlugin misconfigured) |
| Source `translateOr` keys have matching i18n entries | All keys found in translation.json | Orphaned keys (in JSON but not in source) | Missing keys (in source but not in JSON) |

**Key matching algorithm:**
1. Grep all `translateOr(['key'` patterns from `src/**/*.{ts,tsx}`
2. Extract the first array element (the i18n key)
3. Check if each key exists in `i18n/locales/en/translation.json`
4. Report missing keys as WARN (not FAIL) — `translateOr` fallbacks ensure runtime works

**On WARN:** List missing keys. These will display the English fallback at runtime but won't be translatable to other locales.

### Gate 6: Externals Verification

**Critical check.** The OMX host app provides React, ReactDOM, and Material-UI as globals. If these are accidentally bundled, it causes version conflicts and bloated bundles.

```bash
cd accounts/<PROFILE>/SOURCE/frontend/<project>

# Check if React is bundled (should NOT be)
grep -c "react-dom" dist/bundle.*.js || true

# Check webpack config has externals
grep -A 10 "externals" webpack.config.ts
```

**Heuristic checks on the bundle:**
1. Search for `react-dom` string in bundle — if found more than ~5 times, React may be accidentally included
2. Search for `createMuiTheme` or `@material-ui` deep imports — if found, MUI may be bundled
3. Bundle size > 200KB usually indicates missing externals (a typical SDK bundle with only custom code should be 20–80KB)

**Webpack 4 vs 5 externals signatures:** Both are acceptable. Webpack 4 uses string values (`externals: { react: 'React', 'react-dom': 'ReactDOM' }`). Webpack 5 also accepts the object form (`externals: { react: { root: 'React', commonjs: 'react' } }`) or the function form (`externals: ({ request }, callback) => ...`). Do NOT flag Webpack 5 object/function externals syntax as incorrect — both versions work with the SDK's build tooling.

**On failure:** Report which external appears bundled. Fix: check `webpack.config.ts` externals block matches the SDK template.

### Gate 6b: Pre-Deploy CSS Scoping Check

Before deploying a production bundle, verify that component registrations wrap with `StylesProvider` to prevent CSS class name collisions in multi-plugin environments.

**Step 1 — Detect MUI styling usage:**

```bash
cd accounts/<PROFILE>/SOURCE/frontend/<project>
grep -rn "makeStyles\|withStyles\|styled(" src/ --include="*.ts" --include="*.tsx" | grep -v node_modules | grep -v ".test."
```

**Step 2 — Check for StylesProvider:**

```bash
grep -rn "StylesProvider\|createGenerateClassName" src/index.tsx
```

**Checks:**

| Check | Severity | Pass Condition |
|-------|----------|----------------|
| `StylesProvider` wraps registrations | WARN | `src/index.tsx` imports `StylesProvider` from `@material-ui/core/styles` AND `createGenerateClassName` is configured |
| `createGenerateClassName` uses unique seed | WARN | `createGenerateClassName({ seed: '<project-name>' })` or similar unique prefix — prevents `jss1`, `jss2` collisions across plugins |

**When to flag:**
- Project uses `makeStyles()`, `withStyles()`, or MUI `styled()` **AND** registers 2+ components (ComponentRegistry or FieldRegistry) **AND** no `StylesProvider` + `createGenerateClassName` wrapper is found in `src/index.tsx`.
- Single-component plugins with no `makeStyles` usage have lower collision risk — flag as INFO, not WARN.

**Correct pattern:**
```tsx
import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles';

const generateClassName = createGenerateClassName({ seed: 'myPlugin' });

const WrappedComponent = (props) => (
  <StylesProvider generateClassName={generateClassName}>
    <MyComponent {...props} />
  </StylesProvider>
);

ComponentRegistry.register(['acme.content.my-widget'], WrappedComponent, { category: 'content' });
```

**Build impact:** Gate 6b failures produce WARN, not FAIL — the bundle is valid, but CSS collisions may occur in environments with multiple plugins loaded on the same page.

### Gate 7: GraphQL & Cross-Artifact Validation

**Requires MCP.** If `fluent-mcp` or `fluent-mcp-extn` is available, run 3-tier validation. If no MCP server is connected, SKIP with WARN.

#### Tier A — Source-Embedded Queries

Extract all GraphQL queries from local source and validate against the live schema.

**Extraction:**
1. Scan `src/**/*.{ts,tsx}` for template literals containing `query ` or `mutation ` keywords (typically `const *_QUERY` / `const *_MUTATION`)
2. Scan `src/**/*.{ts,tsx}` for `gql\`` tagged template literals
3. Scan `manifests/*.ts` for `data.query` strings in page definitions

**Validation:** For each query, call `graphql.validate` (official CLI MCP). Check the `valid` field in the response (not `ok` — `ok` is always `true` when the tool runs). Report PASS (`valid: true`) / FAIL (`valid: false`, see `errors` array) per query.

**Common schema mismatches:**
- Fields that don't exist on the type (e.g., `currency` on `Order` — use `OrderItem.currency` or `attributes`)
- Treating scalar fields as objects (e.g., `totalPrice { ... }` when `totalPrice` is `Float`)
- Wrong connection shape (e.g., `roles { edges { node { ... } } }` when `roles` is flat: `roles { role { name } }`)
- Non-existent edges (e.g., `customer { billingAccount { ... } }`)

**FieldRegistry fields are the PRIMARY target** — they use `useQuery()` with self-contained queries (since `useData()` returns empty in dialogs). No other validation catches these.

#### Tier B — Deployed Manifest Page Queries

Deployed manifest settings contain page-level `data.query` strings that may differ from local `manifests/*.ts` files. Download, extract, and validate.

**Step 1 — Download manifest settings locally (keep large payloads out of LLM context):**

Use MCP `setting.get` with `outputFile` (preferred — returns only metadata, writes 10-120KB content to disk):

```
setting.get({
  name: "fc.mystique.manifest.oms",
  outputFile: "accounts/<PROFILE>/manifests/deployed/fc.mystique.manifest.oms.json"
})
```

Repeat for each fragment and root manifest. For bulk discovery:
```
setting.get({
  name: "fc.mystique.manifest.%",
  first: 50,
  outputFile: "accounts/<PROFILE>/manifests/deployed/"
})
```

**Fallback 1** (setting.get unavailable): Use official CLI MCP `graphql.execute` (no response budget):
```
graphql.execute({ query: "{ settings(name: [\"fc.mystique.manifest.oms\"], context: ACCOUNT, contextId: 0, first: 1) { edges { node { name lobValue } } } }" })
```

**Fallback 2** (CLI MCP unavailable): Use Bash + REST API direct to save to file:
```bash
TOKEN=$(fluent auth token -p <PROFILE>)
curl -s -X POST "https://<env>.api.fluentcommerce.com/graphql" \
  -H "Authorization: Bearer $TOKEN" -H "fluent.account: $ACCT" -H "Content-Type: application/json" \
  -d '{"query":"{ settings(name: [\"fc.mystique.manifest.oms\"], context: ACCOUNT, contextId: 0, first: 1) { edges { node { name lobValue } } } }"}' \
  -o accounts/<PROFILE>/manifests/deployed/fc.mystique.manifest.oms.json
```

Save each to `accounts/<PROFILE>/manifests/deployed/<setting-name>.json`. If cached copies exist (< 24h old), reuse.

**NEVER use MCP extension `graphql.query`** for manifest settings — the 50K response budget truncates them silently.

**Step 2 — Extract page queries from each manifest JSON:**

Parse the `lobValue` JSON. For each `routes[].pages[]` (or direct `routes[]` with `type: "page"`), extract the `data.query` string. Track the setting name and page path for error reporting.

**Step 3 — Validate each extracted query:**

Call `graphql.validate` (official CLI MCP) for each page query. Check `valid` field in response (not `ok`). Report with `[setting-name] page:<path>` as the source location.

**Size management:** Manifest settings can be 50-100KB each. NEVER pull them through MCP summarize — download to local file first, then parse locally. Total fragments for OMS alone = 11, each 10-60KB.

#### Tier C — Workflow User Action Cross-Reference

Workflows define user actions whose `attributes[]` must match what the frontend sends. Download workflows and cross-reference.

**Step 1 — Ensure local workflow files exist:**

```bash
# Check if workflows are downloaded
ls accounts/<PROFILE>/workflows/<RETAILER_REF>/

# If missing or stale, download:
fluent workflow download -p <PROFILE> -r <RETAILER_REF> -w all -o accounts/<PROFILE>/workflows/<RETAILER_REF>/
```

**Step 2 — Extract user action attributes from workflows:**

For each workflow JSON, find `userActions[]` entries. For each user action, collect:
- `attributes[].name` — attribute names the backend expects
- `attributes[].type` — field type (may reference a FieldRegistry alias like `appeasement.fields.amount-calculator`)
- `attributes[].source` — if `"settings.<KEY>"`, validate the setting exists

**Step 3 — Cross-reference with frontend registrations:**

| Check | Source | Target | Severity |
|-------|--------|--------|----------|
| Field type alias exists | Workflow `attributes[].type` | `FieldRegistry.register()` aliases in `src/index.tsx` | HIGH |
| Attribute names match event payload | Workflow `attributes[].name` | `@EventAttribute` names in custom rules (if source available) | MEDIUM |
| Settings source exists | Workflow `attributes[].source: "settings.X"` | MCP `graphql.query` for `settings(name: ["X"])` | HIGH |

**Step 4 — Validate settings-driven dropdowns (3 silent-failure checks):**

If any attribute has `source: "settings.<KEY>"`:
1. Prefix is lowercase `"settings."` (not `"Settings."`) → CRITICAL if wrong
2. Setting value has `{ "active": [{ "name": "...", "value": "..." }] }` wrapper → HIGH if missing
3. Setting exists for the correct retailer context → HIGH if missing

#### Gate 7 Summary

| Tier | What it validates | Severity on failure |
|------|-------------------|---------------------|
| A — Source queries | GraphQL queries embedded in `src/` | HIGH |
| B — Deployed manifests | Page queries in live manifest settings | HIGH |
| C — Workflow cross-ref | User action attribute ↔ FieldRegistry alignment | HIGH |

**Build impact:** Gate 7 failures produce WARN, not FAIL — the bundle is valid, but runtime may break. `/fluent-frontend-review` treats these as CRITICAL.

**Skip conditions:**
- No MCP server → SKIP with WARN (Tier A/B/C all skipped)
- No GraphQL strings found → PASS for Tier A
- No manifest settings found → SKIP Tier B
- No workflow files found → SKIP Tier C

### Gate 8: i18n Validation

**Purpose:** Verify i18n files are present, translation keys are consistent, and user-facing strings use `translateOr`.

**Skip conditions:**
- No `i18n/` directory AND no `translateOr`/`translate` imports → SKIP (project does not use i18n)
- `--skip-i18n` flag → SKIP

**Step 1 — Check i18n file structure:**

| Check | Severity | Pass Condition |
|-------|----------|----------------|
| `i18n/locales/en/translation.json` exists | HIGH | File present and valid JSON |
| No duplicate keys in translation.json | HIGH | All keys unique (JSON parse doesn't error but last-wins hides duplicates — use raw text scan) |
| Key naming convention | MEDIUM | Keys follow `{namespace}.{area}.{component}.{element}.{attribute}` dot-separated pattern |

**Step 2 — Check source files use i18n:**

Scan all `src/**/*.{ts,tsx}` files:

| Check | Severity | What It Catches |
|-------|----------|-----------------|
| Hardcoded user-facing strings in JSX | MEDIUM | String literals inside JSX elements not wrapped in `translateOr`/`translate`. Exclude: className, style, data-testid, aria-label, console.log, error messages for developers. |
| `translate()` without fallback | LOW | `translate('key')` returns raw key if missing — suggest `translateOr(['key', 'Fallback'])` instead |
| `translateOr` keys missing from translation.json | HIGH | Key used in source but not defined in `en/translation.json` — will show fallback text instead of translated value |
| Unused translation keys | LOW | Keys in translation.json not referenced in any source file — bloats translation files |

**Step 3 — Check manifest i18n alignment (if manifests/ exists):**

| Check | Severity | What It Catches |
|-------|----------|-----------------|
| Manifest `title`/`label`/`menuLabel` without `i18n:` prefix | MEDIUM | Manifest string values that should use `i18n:` prefix for host-app translation |

#### Gate 8 Summary

| What it validates | Severity on failure |
|-------------------|---------------------|
| Translation file exists and is valid | HIGH |
| Source strings use translateOr | MEDIUM |
| Translation keys are consistent | HIGH |

**Build impact:** Gate 8 failures produce WARN, not FAIL — the bundle works without i18n but is not production-ready for multi-language deployments.

### Gate 9: TemplateRegistry Helpers

**Purpose:** Validate custom template helpers registered in `src/index.tsx` are well-formed and consistent with manifest usage.

**Skip conditions:**
- No `TemplateRegistry.register` calls found in `src/` → SKIP (project has no custom helpers)

**Step 1 — Extract registered helpers:**
- Scan `src/index.tsx` (and any files it imports) for `TemplateRegistry.register(alias, fn)` calls
- Extract alias name(s) for each helper
- Verify naming convention: `<namespace>.helpers.<kebab-name>` format

**Step 2 — Cross-reference with manifests (if available):**
- If manifest files exist in `accounts/<PROFILE>/manifests/` or `manifests/`, scan template strings for `{{helperName ...}}` usage
- Report: registered but unused helpers (dead code), referenced but unregistered helpers (runtime error)

**Step 3 — Pass custom helpers to validator:**
- When running `/fluent-mystique-validate`, pass `customHelpers` option with the extracted helper names to suppress T02 false positives

#### Gate 9 Summary

| What it validates | Severity on failure |
|-------------------|---------------------|
| Helper naming convention | MEDIUM |
| Registered-but-unused helpers | LOW |
| Referenced-but-unregistered helpers | HIGH |

**Build impact:** Gate 9 failures produce WARN, not FAIL — helpers may work at runtime but cross-referencing catches mismatches early.

## Build Report

After all gates complete, generate a structured report:

```
accounts/<PROFILE>/reports/frontend-build/<project>-<YYYY-MM-DD-HHmmss>.json
```

Report structure:
```json
{
  "project": "<project-name>",
  "profile": "<PROFILE>",
  "timestamp": "<ISO-8601>",
  "version": "<from package.json>",
  "gates": {
    "typescript": { "status": "PASS|FAIL", "errors": 0 },
    "lint": { "status": "PASS|FAIL|WARN", "errors": 0, "warnings": 3 },
    "tests": { "status": "PASS|FAIL", "total": 40, "passed": 40, "failed": 0, "coverage": { "statements": 77, "branches": 76, "functions": 69, "lines": 78 } },
    "stories": { "status": "PASS|WARN|SKIP", "registered": 5, "withStories": 4, "missing": ["src/fields/MyField/"] },
    "build": { "status": "PASS|FAIL" },
    "bundle": { "status": "PASS|WARN|FAIL", "file": "bundle.abc123.js", "sizeBytes": 37000, "versionJson": true },
    "externals": { "status": "PASS|WARN|FAIL", "reactBundled": false, "muiBundled": false },
    "cssScoping": { "status": "PASS|WARN|SKIP", "stylesProviderFound": true, "generateClassNameFound": true, "muiStyleUsageCount": 5, "registeredComponentCount": 3 },
    "graphql": {
      "status": "PASS|WARN|FAIL|SKIP",
      "tierA": { "queriesFound": 3, "valid": 2, "invalid": 1, "errors": ["src/fields/.../AppeasementAmountCalculator.tsx:50 — Cannot query field 'currency' on type 'Order'"] },
      "tierB": { "manifestsChecked": 3, "pagesChecked": 15, "valid": 14, "invalid": 1, "errors": ["fc.mystique.manifest.oms.fragment.ordermanagement page:orders/:id — totalPrice is Float, not object"] },
      "tierC": { "workflowsChecked": 2, "attributesChecked": 5, "mismatches": 0 }
    },
    "i18n": {
      "status": "PASS|WARN|SKIP",
      "translationFile": true,
      "totalKeys": 42,
      "hardcodedStrings": 0,
      "missingKeys": 0,
      "unusedKeys": 3
    }
  },
  "verdict": "PASS|WARN|FAIL",
  "bundlePath": "dist/bundle.abc123.js"
}
```

**Verdict rules:**
- Any gate FAIL → verdict FAIL
- Any gate WARN (but no FAIL) → verdict WARN
- All gates PASS → verdict PASS

## Quick Commands

| Command | What it runs |
|---|---|
| `/fluent-frontend-build dev` | **Start local dev server** + wire manifest + browser verify + iterate (recommended first) |
| `/fluent-frontend-build` | Full production pipeline (default: all 8 gates) |
| `/fluent-frontend-build full` | Full production pipeline (all 8 gates) |
| `/fluent-frontend-build test` | Gates 1-3 only (TypeScript + lint + tests) |
| `/fluent-frontend-build lint` | Gate 2 only (lint) |
| `/fluent-frontend-build build` | Gates 1-6 (everything except GraphQL + i18n validation) |
| `/fluent-frontend-build check` | Gates 5-8 only (verify existing dist/ + schema + i18n validation) |
| `/fluent-frontend-build graphql` | Gate 7 only (GraphQL schema validation via MCP) |
| `/fluent-frontend-build i18n` | Gate 8 only (i18n file + key validation) |
| `/fluent-frontend-build storybook` | **Start Storybook** dev server + run story coverage check (Gate 3b) |

## Local Development Mode (`dev`) — Recommended First Step

Start a local dev server with hot-reload, wire it into a Fluent manifest, and verify in the browser. **This is the primary development workflow** — edit code, see changes instantly. Always offer local testing first before suggesting CDN/Vercel/S3 deployment.

### Hosting Decision

When the user is ready to test, ask:

> "Would you like to test locally first (recommended) or deploy directly to a CDN (Vercel, S3, etc.)?"

- **Local** (default) → `dev` mode below. Zero infrastructure setup. Instant iteration.
- **CDN** → Production build (`full`) → user deploys `dist/` to their preferred host → update manifest `plugins[]` with CDN URL.

### How It Works

The SDK's webpack-dev-server serves the bundle at `http://localhost:3001/` with:
- **Hot Module Replacement (HMR)** — code changes apply without full page reload
- **`version.json`** auto-served — the Fluent host app reads this to discover the bundle filename
- **Source maps** — full debugging in browser DevTools

### Dev Mode Flow

```
1. PRE-FLIGHT
   - Run pre-flight checks (same as production)

2. CHECK IF SERVER ALREADY RUNNING (MANDATORY — prevents duplicate servers)
   curl -s http://localhost:3001/version.json
   # If returns valid JSON (status 200) → server is ALREADY running
   #   → Confirm it is the intended project (same cwd/terminal or expected bundle behavior)
   #   → If same project: SKIP step 3, go straight to step 4 (WIRE INTO MANIFEST)
   #   → Log: "Dev server already running on localhost:3001 — reusing existing instance"
   #   → If different project: DO NOT start a second server on 3001.
   #      Ask user whether to (a) stop the existing one, or (b) run this project on another port and update plugins[].
   # If connection refused / timeout / non-200 → no server running, proceed to step 3

3. START DEV SERVER (only if step 2 found no running server)
   cd accounts/<PROFILE>/SOURCE/frontend/<project>
   yarn start
   # Keep this running in a dedicated terminal
   # Wait for "Compiled successfully" or "webpack compiled" in output
   # Verify with: curl -s http://localhost:3001/version.json

4. VERIFY SERVER RUNNING
   curl -s http://localhost:3001/version.json
   # Should return { "bundle": "bundle.js", "version": "dev" } or similar
   # If still failing after step 3, check port conflict or webpack errors

5. WIRE INTO MANIFEST
   a. Identify ALL target manifest fragment settings where the component is used
      - OMS fragments: fc.mystique.manifest.oms.fragment.ordermanagement, .products, .stores, .admin, etc.
      - Store App fragments: fc.mystique.manifest.store.fragment.*
      - NOTE: Root manifest (fc.mystique.manifest.oms) is loaded from a platform plugin file, NOT
        the setting — you CANNOT add plugins there. Always use fragment settings.
   b. BACKUP each existing setting to accounts/<PROFILE>/manifests/backups/
   c. Add or update the plugins[] array in EACH fragment that uses the component:
      "plugins": [
        { "type": "url", "src": "http://localhost:3001/" },
        ...existing plugins
      ]
   d. Deploy each updated manifest setting (JSON/ACCOUNT/0/lobValue).
      Size routing: root manifests (<4KB) → MCP `setting.upsert`;
      fragments (>4KB) → CLI `fluent module install --include settings --force` from local file
   e. Note: the trailing "/" on localhost URL is REQUIRED for version.json auto-discovery
   f. If the same component is used across multiple apps (OMS + Store), add the plugin
      entry to each app's relevant fragment settings

6. OPEN BROWSER
   - Navigate to the target page in the Fluent UI
   - If Chrome DevTools MCP available: use navigate_page + take_snapshot
   - Verify the custom component renders (not a blank area or error)

7. ITERATE
   - Edit source files in src/components/ or src/fields/
   - Webpack auto-rebuilds (watch mode) — typically 1-3 seconds
   - Browser hot-reloads the changed module
   - Code-only changes with localhost plugin active do NOT require manifest redeploy
   - If HMR fails (full page reload), check the browser console for errors
   - Use Chrome DevTools MCP list_console_messages to check for runtime errors

8. STOP DEV SERVER
   - When done iterating, stop the background yarn start process
   - Update manifest plugins[] to production CDN URL (or remove localhost entry)
   - Redeploy manifest setting
```

### Dev Mode Gotchas

- **Server already running (multi-agent)**: When multiple agents work concurrently, one may have already started the dev server. ALWAYS check `curl -s http://localhost:3001/version.json` BEFORE attempting `yarn start`. If it returns valid JSON, reuse the existing server — do NOT start another one. Starting a duplicate server causes port conflicts, silent failures, or webpack picking a random port
- **Port 3001 in use (non-dev-server)**: If a non-webpack process uses 3001, the SDK's dev server may fail silently or pick another port. Check `webpack.config.ts` for the `devServer.port` setting
- **CORS**: The Fluent host app loads plugins from `localhost:3001`. Modern browsers allow this for localhost, but if you see CORS errors, ensure the dev server has `headers: { 'Access-Control-Allow-Origin': '*' }` in webpack config
- **Manifest cache**: After deploying the localhost manifest, hard-refresh the browser (Ctrl+Shift+R) to bypass any cached manifest
- **Multiple plugins**: If the manifest already has production plugins, add localhost as the FIRST entry in `plugins[]` — components registered later override earlier ones with the same alias
- **HMR boundaries**: HMR works best for component content changes. Changes to `src/index.tsx` (registration) or `webpack.config.ts` require a full dev server restart
- **Don't forget to switch back**: After local testing, ALWAYS update `plugins[]` back to the production CDN URL before leaving. A localhost URL in a deployed manifest breaks the UI for all users
- **Multi-app cleanup**: If you added localhost plugin to multiple fragment settings (OMS App + Store App), remove it from ALL of them. Use `graphql.queryAll` or `fluent settings list` to find all settings containing `localhost:3001`

## Storybook Mode (`storybook`) — Visual Component Development

Start a Storybook dev server for isolated component development and visual regression testing. No manifest wiring needed — components render in isolation with mock data.

### Storybook Mode Flow

```
1. PRE-FLIGHT
   - Run pre-flight checks (same as production)
   - Verify .storybook/ directory exists. If missing:
     "Storybook is not configured in this project.
      Run: npx storybook@6 init --type react
      (Use Storybook 6 — the SDK uses React 17 + Webpack 5)"

2. CHECK IF STORYBOOK ALREADY RUNNING
   curl -s http://localhost:6006 > /dev/null 2>&1
   # If running → reuse. Tell user: "Storybook already running at http://localhost:6006"
   # If not running → proceed to step 3

3. VALIDATE STORYBOOK SCRIPTS
   # Check package.json "storybook" script for NODE_OPTIONS
   grep -q "openssl-legacy-provider" package.json
   # If MISSING and Node >= 18: WARN — Storybook will fail with ERR_OSSL_EVP_UNSUPPORTED
   # Suggest: update package.json storybook script to prefix with
   #   NODE_OPTIONS='--openssl-legacy-provider'
   # e.g., "storybook": "NODE_OPTIONS='--openssl-legacy-provider' start-storybook -p 6006"
   # This is a WARN, not a block — user may be on Node 16/17 where it's not needed

4. START STORYBOOK
   cd accounts/<PROFILE>/SOURCE/frontend/<project>
   yarn storybook
   # Default port: 6006
   # Wait for "Storybook started" in output
   # Opens browser automatically

5. RUN STORY COVERAGE CHECK (Gate 3b)
   - Extract registered components/fields from src/index.tsx
   - Check for co-located .stories.tsx files
   - Report missing stories as WARN

6. ITERATE
   - Edit component source → Storybook hot-reloads (<1s)
   - Add story variants for edge cases (empty, error, loading, disabled)
   - No manifest deploy needed — components render in isolation
   - Use Storybook's Controls addon to test prop variations interactively

7. STOP STORYBOOK
   - Ctrl+C to stop the server
   - Run /fluent-frontend-build test to verify tests still pass
```

### Storybook Gotchas

- **Storybook 6, not 7+**: The SDK uses React 17 + Webpack 5. Storybook 7+ requires React 18. Always use `@storybook/react@6`
- **Do NOT externalize React, ReactDOM, or MaterialUI in Storybook**: The production `webpack.config.ts` externalizes all three to host app globals (`window.React`, `window.MaterialUI`, etc.), but Storybook runs standalone — those globals don't exist. The `lib/material-ui.js` and `lib/mystique.js` UMD bundles also fail because they need `window.React` to initialize, which isn't set in the `<head>`. **Fix**: remove ALL externals from `.storybook/main.ts` and let `@material-ui/core` resolve from `node_modules` normally
- **Mock `mystique/*` via webpack alias**: Since `mystique` is a host-app-only package (no npm package), Storybook needs a mock. Create `.storybook/mocks/mystique/` with directory structure matching import paths (`hooks/index.js`, `hooks/useQuery.js`, `registry/FieldRegistry.js`, `registry/ComponentRegistry.js`). Add webpack alias in `main.ts`: `config.resolve.alias.mystique = path.resolve(__dirname, 'mocks/mystique')`. Each mock file exports no-op functions (e.g., `useQuery = () => [{ fetching: false, stale: false, data: undefined, error: undefined }, () => {}]`)
- **Jest uses a different mock path**: Jest maps `mystique` → `lib/mystique-export.js` (backed by `lib/mystique.js` global). Storybook uses the `.storybook/mocks/` directory alias instead. Both achieve the same result through different mechanisms
- **Port conflict**: If port 6006 is in use, Storybook auto-picks another port (6007, 6008...). Check terminal output for the actual URL
- **`storyTemplate` helper**: SDK projects include `.storybook/helpers.ts` with a `storyTemplate()` function that wraps components with required providers. Always use it instead of raw story functions
- **Stale webpack cache**: If Storybook hangs at 8% compilation after config changes, clear cache: `rm -rf node_modules/.cache/storybook` and restart

### Switching Between Dev and Production

```
# Visual development in isolation (no manifest needed)
/fluent-frontend-build storybook

# ... iterate on components with hot-reload ...

# Test in live Fluent UI (requires manifest wiring)
/fluent-frontend-build dev

# ... iterate with real data ...

# Ready for production
# 1. Stop dev server (Ctrl+C or kill background process)
# 2. Run production build
/fluent-frontend-build full

# 3. Upload dist/ to CDN
# 4. Update manifest plugins[] to CDN URL
# 5. Redeploy manifest
# 6. Verify with /fluent-ui-test
```

## Integration with Other Skills

| Artifact | Consumed by | Path |
|---|---|---|
| Bundle (`dist/bundle.*.js`) | Manifest `plugins[]` via `/fluent-mystique-builder` | `accounts/<PROFILE>/SOURCE/frontend/<project>/dist/` |
| Build report | `/fluent-pre-deploy-check` Phase 9 | `accounts/<PROFILE>/reports/frontend-build/` |
| Coverage data | `/fluent-frontend-review` | `accounts/<PROFILE>/SOURCE/frontend/<project>/coverage/` |

## Error Reporting

| Error | Severity | Recovery |
|---|---|---|
| `PREREQ_MISSING` | CRITICAL | Pre-flight check failed — install deps, check SDK types |
| `TYPESCRIPT_ERRORS` | CRITICAL | Fix TypeScript errors before proceeding |
| `LINT_ERRORS` | HIGH | Fix lint errors or run `yarn lint:fix` |
| `TEST_FAILURES` | CRITICAL | Fix failing tests |
| `BUILD_FAILED` | CRITICAL | Check webpack config, babel config, imports |
| `DEV_SERVER_CONFLICT` | HIGH | Port 3001 already in use by another project; reuse existing server or switch port |
| `BUNDLE_MISSING` | CRITICAL | Build did not produce output |
| `BUNDLE_OVERSIZED` | HIGH | Check externals, tree-shaking, dead code |
| `EXTERNALS_BUNDLED` | HIGH | Fix webpack externals config |
| `COVERAGE_LOW` | MEDIUM | Add tests for uncovered code |
| `GRAPHQL_INVALID` | WARN | Fix invalid GraphQL queries — run `/fluent-frontend-review` for full details |
| `GRAPHQL_SKIP` | INFO | MCP not connected — cannot validate GraphQL queries. Connect `fluent-mcp` and re-run |

## Gotchas

- **`yarn` not `npm`**: SDK projects use Yarn. Always use `yarn install`, `yarn build`, `yarn test`
- **Coverage flag duplication**: Some projects already define `--coverage` in `package.json` scripts. If `yarn test --coverage` fails, use `yarn test` and confirm coverage output.
- **Lint scope drift**: If `eslint .` fails on vendored SDK typings/config files, lint authored source (`src`, `manifests`) as the quality gate.
- **Node 16-18**: SDK webpack config may not work on Node 20+ due to OpenSSL changes. If build fails with `ERR_OSSL_EVP_UNSUPPORTED`, set `NODE_OPTIONS=--openssl-legacy-provider`
- **`.babelrc` ESM issues**: `@emotion/babel-preset-css-prop` is ESM-only and breaks with babel-loader. Replace with `@emotion/babel-plugin` + explicit `importSource` config
- **`setupFilesAfterSetup` typo**: Some SDK templates have this wrong — correct key is `setupFilesAfterEnv`
- **Windows paths**: Use forward slashes in webpack config. Jest `moduleNameMapper` may need Windows-compatible regex
- **Windows `testPathIgnorePatterns` regex failure**: If `yarn test` includes `--testPathIgnorePatterns ./build/**`, this fails on Windows because `**` is invalid regex. Workaround: run `npx jest --coverage` directly without the ignore pattern, or fix the script to use `"./build"` instead of `./build/**`
- **Coverage with styled components**: Emotion/styled-components generate wrapper components that inflate function count in coverage
- **MCP response budget truncates large payloads**: `fluent-mcp-extn` has a 50K char budget (`FLUENT_RESPONSE_BUDGET_CHARS`). Manifest settings (10-60KB each) and workflows (50K+) get silently truncated. For Gate 7, always use official CLI MCP `graphql.execute` or Bash REST API for downloading settings. Use CLI `fluent workflow download` for workflows (writes directly to file).

## Post-Execution: Feedback Capture

After completing this skill (whether success or failure), write a feedback record:

1. **Classify outcome:** `SUCCESS` (completed cleanly), `PARTIAL_SUCCESS` (completed after fixing issues), `FAILURE` (could not complete), `BLOCKED` (prereq missing), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-frontend-build"`
   - `feature`: feature slug if applicable, null otherwise
   - `phases`: trace of each phase with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip tokens, passwords, API keys. Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-frontend-build.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user asked a question, no actual write operation happened).

## Known Pitfalls
<!-- feedback-promoted: managed section, updated by feedback loop -->

_(No promoted learnings yet.)_

<!-- end feedback-promoted -->
