---
name: fluent-frontend-review
description: Code quality audit for Mystique SDK component projects. Checks TypeScript strictness, React patterns, Mystique SDK compliance, GraphQL schema validation (via MCP), code documentation, test quality, and README completeness. Frontend equivalent of /fluent-custom-code. Triggers on "review frontend", "code quality", "frontend review", "review component", "audit sdk", "check quality".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [project-path]
cursor-tier: manual
planning-gate: none
---

# Frontend Code Review

Code quality audit for SDK component projects. Read-only analysis that produces a structured report with severity-graded findings. Does NOT auto-fix — reports issues for the user to address.

## Ownership Boundary

| Does | Does NOT |
|---|---|
| TypeScript quality audit (any types, strict null) | Fix code (user decides) |
| React pattern compliance | Build the project (-> `/fluent-frontend-build`) |
| Mystique SDK compliance checks | Create manifests (-> `/fluent-mystique-builder`) |
| GraphQL schema validation (via MCP `graphql.validate`) | Deploy anything |
| Code documentation audit (JSDoc) | Scaffold projects (-> `/fluent-mystique-scaffold`) |
| Test quality/coverage audit | |
| README completeness check | |

## Handoff Protocol

| Signal | Condition | Example |
|--------|-----------|---------|
| `-> REVIEW: <path>` | Report generated, awaiting user review | `-> REVIEW: accounts/<PROFILE>/reports/frontend-review/my-plugin-2026-02-28.json` |
| `-> BLOCKED: <reason>` | Prerequisites missing | `-> BLOCKED: PREREQ_MISSING — package.json not found` |
| `-> NEXT: /fluent-frontend-build` | Report clean, ready for build | `-> NEXT: /fluent-frontend-build` |

### Error Codes

| Code | Condition | Recovery |
|------|-----------|----------|
| `PREREQ_MISSING` | SDK project not found or missing markers | Verify project path, check for package.json and @types/mystique/ |
| `VALIDATION_FAILED` | CRITICAL findings in report | Fix critical issues and re-run |

## Required Inputs

Project path under `accounts/<PROFILE>/SOURCE/frontend/<project>/`

If no path given, search `accounts/<PROFILE>/SOURCE/frontend/*/package.json`:
- Exactly 1 project found -> use it
- Multiple found -> list and ask user to choose
- None found -> BLOCKED: PREREQ_MISSING

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-frontend-review.jsonl` exists
2. If yes, read last 20 records
3. Filter to: same SDK project if known, FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal "WATCH OUT" guidance during review
5. Do NOT show raw feedback to the user — silently adjust false-positive handling and expected local conventions

## Pre-flight: Project Knowledge Check

Check for project-specific knowledge that should inform review expectations:

1. Check if `accounts/<PROFILE>/knowledge/` exists
2. If yes, read it and find files where `relevant-skills` includes `fluent-frontend-review`
   **Account knowledge:** Read from `accounts/<PROFILE>/knowledge/` when `FLUENT_PROFILE` is set. These contain project-specific conventions.

3. Read matching knowledge files — typically:
   - `knowledge/frontend/mystique-sdk.md` — use for SDK contracts, default testing stack, and field guidance
   - `knowledge/frontend/mystique-advanced-sdk.md` — use for advanced category, field, and data-loading rules
   - `knowledge/frontend/mystique-patterns.md` — use for styling, OMS integration, and plugin structure expectations
   - `knowledge/platform/user-action-contract.md` — use for FieldRegistry and user action dialog checks
   - `accounts/<PROFILE>/knowledge/naming-conventions.md` — use for alias naming, i18n keys, and project naming rules
   - `accounts/<PROFILE>/knowledge/coding-standards.md` — use for local test, lint, and code structure expectations
   - `accounts/<PROFILE>/knowledge/client/data-model.md` — use for entity-specific UI assumptions
   - `accounts/<PROFILE>/knowledge/client/integrations.md` — use when review touches external APIs or integration-specific behavior
4. If `accounts/<PROFILE>/memory/index.md` exists, read the top account-specific frontend, GraphQL, test, and deployment gotchas and apply them silently during review.
5. Apply knowledge silently — do NOT show raw knowledge to user, just use it to calibrate findings and avoid false positives.

## Pre-Flight

All three checks must pass before proceeding:

1. **package.json exists** in project root
2. **@types/mystique/ directory exists** (confirms this is an SDK project)
3. **src/index.tsx exists** (component registration entry point)

If any check fails, emit `-> BLOCKED: PREREQ_MISSING` with the specific missing item.

## Audit Phases

### Phase 1: TypeScript Quality

Scan all `.ts` and `.tsx` files under `src/` (exclude `node_modules/`, `dist/`, `coverage/`, `.storybook/`):

| Check | Severity | Pattern | Notes |
|-------|----------|---------|-------|
| `any` type usage | MEDIUM | `: any`, `as any` in production source | Exclude `.test.tsx` files |
| Missing return types on exported functions | LOW | Exported function without explicit return type | Only flag public API |
| Non-null assertions (`!`) outside test files | LOW | `variable!.prop` in `.ts`/`.tsx` (not test) | |
| `@ts-ignore` or `@ts-expect-error` comments | MEDIUM | Direct grep in source files | |
| Implicit `any` from untyped imports | MEDIUM | Imports from packages without `@types/` | |

### Phase 2: React Patterns

Check for common React anti-patterns in component files:

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **Infinite loop risk** | CRITICAL | `onChange` or state setters called directly in component body (not in useEffect/useCallback/event handler) |
| **Missing cleanup** | HIGH | useEffect with subscriptions/timers but no cleanup return function |
| **Module-level mutable state** | CRITICAL | `let` declarations at module scope that are modified by components at runtime (e.g., `let cache = null`, `let globalStore = ...`). Module scope persists across dialog open/close and page navigations — causes data leakage between form sessions. Constants (`const QUERY = "..."`) are fine. |
| **Direct DOM manipulation** | MEDIUM | `document.getElementById`, `document.querySelector` outside test files |
| **Missing keys** | HIGH | `Array.map()` without `key` prop on returned elements |
| **Stale closures** | MEDIUM | useEffect/useCallback with missing dependency array items (if eslint-plugin-react-hooks is configured, check lint output instead of manual scan) |
| **Missing StylesProvider** | HIGH | Plugin uses `makeStyles()` or MUI `styled()` but no `StylesProvider` + `createGenerateClassName` wrapper. CSS class names will collide with other plugins on the same page. Check for `createGenerateClassName` in source. |
| **Multiple useState where useReducer fits** | MEDIUM | Component has 4+ `useState` calls with interdependent values updated together. Suggest `useReducer` with discriminated union actions. |
| **useEffect without dependency array** | HIGH | `useEffect(() => { ... })` called without `[]` or `[deps]` — runs on every render, causes performance issues and potential infinite loops. Every `useEffect` must have an explicit dependency array. |
| **getQuery in render path** | HIGH | `getQuery()` called during render (top-level component body or inline in JSX) instead of inside an event handler, `useEffect`, or `useCallback`. `getQuery` is async and fires on every render — use `useQuery()` for reactive data binding or call `getQuery` only in response to user actions. |
| **Hard-coded pagination limit** | MEDIUM | GraphQL queries with `first: 100` or similar hard-coded limits without pagination handling (no `hasNextPage`/`cursor` check). Silently truncates results when the dataset exceeds the limit. Add cursor-based pagination or document the limit as intentional. |
| **Missing error boundary** | MEDIUM | Complex async components (multiple `useQuery`/`getQuery`/`getRest` calls) without try-catch blocks or a React error boundary wrapper. Unhandled errors crash the entire parent page section. Wrap with an error boundary or add per-query error guards. |
| **Uncaught promise** | MEDIUM | `.then()` chain on `getQuery()`/`getRest()` calls without a `.catch()` handler. Network errors and GraphQL failures fail silently, leaving the component in a stale or empty state. Always add `.catch()` or use `try/await/catch`. |
| **useSettings without loading check** | MEDIUM | Accessing `settings.myKey.result` or `settings.myKey.result.value` without first checking `settings.myKey.status === 'ok'`. When status is `'loading'` or `'error'`, `result` is undefined — causes runtime TypeError. |
| **Direct localStorage without fallback** | LOW | `localStorage.getItem()` / `localStorage.setItem()` without try-catch. Throws in private browsing mode (Safari) and when storage quota is exceeded. Always wrap in try-catch with a sensible fallback. |
| **Console.log in production** | LOW | `console.log` / `console.debug` / `console.info` statements not wrapped in `process.env.NODE_ENV === 'development'` or `__DEV__` check. Pollutes browser console in production. Exclude `console.warn` and `console.error` (those are intentional). |
| **Missing minHeight on Card inner divs** | LOW | Card components (especially `fc.card.*` equivalents) without a `minHeight` on their inner container div. Causes visual collapse when data is empty or loading — the card shrinks to zero height, breaking page layout. OOTB reference plugins set `minHeight: 100` or similar. |
| **lastSubmit treated as boolean** | MEDIUM | `if (lastSubmit)` or `!lastSubmit` checks on the `lastSubmit` prop from `FormFieldProps`. `lastSubmit` is a numeric timestamp (ms since epoch), not a boolean. Truthiness checks work due to coercion (0 is falsy, any timestamp is truthy), but indicate a misunderstanding of the prop's semantics. Correct: `if (lastSubmit > 0)` or `if (typeof lastSubmit === 'number' && lastSubmit > 0)`. |

**Infinite loop detection detail:**
1. Component has a state setter (useState, useReducer)
2. State setter is called OUTSIDE of: useEffect callback, event handler (onClick, onChange, onSubmit), useCallback callback
3. If found -> CRITICAL finding

### Phase 3: Mystique SDK Compliance

Check for SDK-specific issues:

| Check | Severity | What to Look For | Correct Pattern |
|-------|----------|-----------------|-----------------|
| **Wrong Material-UI import** | MEDIUM | `@mui/material` without being listed in `dependencies` | If bundled as a dependency, v5 imports are valid. If NOT in dependencies, must use `@material-ui/core` (v4 externalized from host) |
| **Hardcoded color values** | MEDIUM | Hex colors (`#00A9E0`, `#fff`, etc.) in `makeStyles`, `style={{}}`, or JSX instead of theme tokens | Use `theme.palette.primary.main`, `theme.palette.mystique.colours.*`, etc. |
| **Own ThemeProvider** | HIGH | Component creates its own `<ThemeProvider>` or `<MuiThemeProvider>` | Remove — the Mystique shell provides theme via `FluentThemeProvider` |
| **MUI v5 sx prop** | MEDIUM | `sx={{...}}` prop on MUI components without `@mui/material` in dependencies | Valid if project bundles v5 as a dependency. If v4-only, use `makeStyles` or `className` |
| **Inline style for theme values** | LOW | `style={{ color: '#00A9E0', padding: 16 }}` for values available via theme | Use `makeStyles((theme) => ({...}))` for theme-dependent styling |
| **React 18 APIs** | MEDIUM | `useId`, `useSyncExternalStore`, `useTransition`, `startTransition`, `createRoot` | SDK baseline is React 17. Newer SDK versions (v1.2024.9+) support React 18 — check `sdk-version.json` before flagging |
| **useData vs useQuery priority** | MEDIUM | ComponentRegistry component uses `useQuery` for data the parent page query already fetches | Suggest `useData` instead |
| **FieldRegistry using useData** | HIGH | FieldRegistry field (in `src/fields/`) uses `useData()` for data access | `useData()` returns empty context in user action dialogs — use `useQuery()` with `entityContext`-first entity identity. URL hash parsing is fallback-only. See `/fluent-mystique-sdk-reference` for the user action dialog data-access contract. |
| **Embedded GraphQL not validated** | MEDIUM | `useQuery()` call with inline GraphQL query string in a FieldRegistry field | Flag for MCP `graphql.validate` verification — embedded queries in fields have no other validation mechanism |
| **ComponentRegistry.register alias format** | HIGH | Alias not matching `<namespace>.<category>.<kebab-name>` (3 segments, dot-separated, kebab-case). A custom namespace (e.g., `appeasement.`, `acme.`) is CORRECT — only flag when the 3-segment dot-separated pattern itself is wrong. | Fix alias string |
| **FieldRegistry.register alias format** | HIGH | Alias not matching `<namespace>.fields.<kebab-name>` (3 segments, dot-separated, kebab-case). Custom namespace is correct — do NOT flag for using a non-`fc.` prefix. | Must follow same convention as ComponentRegistry |
| **Multiple aliases per registration** | MEDIUM | Registering 2+ aliases for any registry (e.g., both dot-separated and camelCase) | Use one canonical alias per component/field. Extra aliases only for migration. |
| **`fc.` prefix on custom component** | LOW | Custom alias using `fc.` prefix (used by OOTB Mystique components) | Recommend using a project-specific namespace (e.g., `acme.`, `supermart.`). `fc.custom.*` is acceptable but may collide with future OOTB additions |
| **Missing namespace prefix** | HIGH | Custom component alias doesn't follow `<namespace>.<category>.<name>` 3-segment dot-separated pattern. Single-segment aliases (e.g., `myComponent`) or 2-segment aliases (e.g., `content.myWidget`) collide in the global registry. Must have a namespace prefix identifying the partner/project. | Use `<namespace>.<category>.<kebab-name>` format |
| **Reimplements OOTB component** | HIGH | Custom component rebuilds functionality already available in OOTB `fc.*` components (e.g., custom list when `fc.quantity.list` exists, custom scanner when `BarcodeFilter` is built into QuantityList). Check if the component's render output duplicates what an OOTB component provides. | Wrap the OOTB component via `ComponentRegistry.get('fc.<name>')` and pass data as props. Only build custom for the delta logic. Reference: Store plugin `WavePickList.tsx` pattern. |
| **Custom FieldProps instead of SDK FormFieldProps** | HIGH | Field components that define their own `FieldProps<T>` interface instead of importing `FormFieldProps<T>` from `mystique/registry/FieldRegistry`. Custom interfaces miss critical SDK props: `onBlur`, `entityContext`, `name`, `defaultValue`, `options`, `helperText`, `validation`. Detect by searching for `interface FieldProps` or `interface.*Props.*extends.*` that don't extend `FormFieldProps`. | Import and extend `FormFieldProps<T>` from `mystique/registry/FieldRegistry` |
| **FieldRegistry typing** | MEDIUM | Fields not using `FormFieldProps<T>` or a documented superset | Add proper type parameter. `as any` cast in `register()` is acceptable when the component extends `FormFieldProps<T>` with additional props (e.g., `data`). Flag only when no typed props interface exists at all. |
| **TemplateRegistry alias format** | MEDIUM | Template helper alias not matching `<namespace>.helpers.<kebab-name>` | Use consistent dot-separated kebab-case |
| **Missing category in register()** | HIGH | `ComponentRegistry.register()` without `{ category: 'page'\|'layout'\|'content'\|'filter' }` | Add category option. Decision tree: `page` (route-level replacement), `layout` (arranges children), `content` (displays info — 80%+ of components), `filter` (modifies query variables via `useData().variables.setVariables()`). Full guide: `knowledge/frontend/mystique-advanced-sdk.md` §1. |
| **Hardcoded strings** | LOW | User-visible strings not using i18n |
| **Missing registration logging** | LOW | `src/index.tsx` registers components without `console.groupCollapsed`/`console.groupEnd` logging around registrations. OOTB reference plugins use grouped console logging (e.g., `console.groupCollapsed('MyPlugin registering...')`) so that multi-plugin environments can debug which plugins loaded and which aliases were registered. Advisory — not blocking. |
| **Missing OpenSSL legacy provider in Storybook** | MEDIUM | If `.storybook/` exists and `package.json` has a `storybook` script, check whether it includes `NODE_OPTIONS='--openssl-legacy-provider'`. Without it, Storybook fails on Node 18+ with `ERR_OSSL_EVP_UNSUPPORTED` due to Webpack 4/5 OpenSSL usage. Flag as MEDIUM — does not affect production build, only local Storybook development. |

### Phase 4: GraphQL Schema Validation

**Purpose:** Extract all GraphQL queries from source files and validate them against the live Fluent schema using MCP tools. An invalid field in a `useQuery` call nullifies the ENTIRE query response at runtime — no partial results, no error message in the UI, just an empty/null data object.

**Pre-requisite:** MCP server `fluent-mcp` must be connected (provides `graphql.validate`). If unavailable, SKIP this phase with a MEDIUM finding: "GraphQL schema validation skipped — fluent-mcp not connected. Run manually with `/fluent-mcp-core`."

**Step 1 — Extract queries from source files:**

Scan all `.ts` and `.tsx` files under `src/` for GraphQL query strings:

| Pattern | Example | How to Extract |
|---------|---------|----------------|
| `useQuery<...>(queryString)` | `const [data] = useQuery<OrderResponse>(ORDER_QUERY)` | Resolve `ORDER_QUERY` to its declaration — typically a `const` with a template literal |
| `getQuery<...>(queryString)` | `getQuery<MeResponse>(meQuery)` | Same — resolve variable to string value |
| Template literal query declarations | `` const q = `query { orders { ... } }` `` | Direct extraction — look for `query {`, `mutation {`, or `{ ... }` patterns in template literals |
| `gql` tagged templates (manifests/) | `` gql`query OrderById($id: ID!) { ... }` `` | Extract the template literal content |
| String literal queries | `const q = "query { me { id } }"` | Less common but check for string assignments with `query {` |

**Step 2 — Validate each query:**

For each extracted query string, call:

```
graphql.validate({ query: "<extracted-query-string>" })
```

Response: `{ ok: true, valid: true|false, errors?: [...] }`. Check the `valid` field (not `ok`) — `ok` is always `true` when the tool runs successfully. `valid: false` means the query has schema errors listed in the `errors` array.

**Step 3 — Report findings:**

| Check | Severity | What It Catches |
|-------|----------|-----------------|
| **Invalid field name** | CRITICAL | Field doesn't exist on type (e.g., `order { name }` — Order has `ref` not `name`). Causes entire query to return null. |
| **Invalid argument** | CRITICAL | Wrong argument name or type (e.g., `$retailerId: Int` — should be `ID`). Query silently fails. |
| **Missing required argument** | CRITICAL | Required argument not provided (e.g., `orderById` without `$id`). |
| **Deprecated field** | MEDIUM | Field exists but is deprecated — may be removed in future versions. |
| **Undeclared variable** | CRITICAL | Variable used in query body but not declared in `query(...)` header. Fluent rejects the entire query. |
| **Unused variable** | HIGH | Variable declared but not used in query body. Fluent rejects queries with unused variables. |
| **Unresolvable query variable** | MEDIUM | Query string could not be statically resolved (dynamic construction, import from external module) — flag for manual review. |
| **No queries found** | LOW | No GraphQL queries detected in source files. Unusual for an SDK component — verify this is intentional. |

Also validate queries found in manifest TypeScript files under `manifests/`:

```
graphql.validate({ query: "<manifest-page-query>" })
```

**Gotchas:**
- **Dynamic query construction**: If a query is built with string concatenation or template interpolation (e.g., `` `query { ${entityType}ById ... }` ``), it cannot be statically extracted. Flag as MEDIUM for manual review.
- **Re-exported queries**: If a query is imported from another file (`import { ORDER_QUERY } from '../queries'`), follow the import chain to find the actual string.
- **useData vs useQuery**: Components using `useData()` get their data from the parent page's query — the query validation for those lives in the manifest, not the component source. Only validate queries passed directly to `useQuery`/`getQuery`.
- **Mutation queries**: `useRest` calls to `/api/v4.1/event/sync` are REST, not GraphQL — don't try to validate those. Only validate actual GraphQL `query { }` or `mutation { }` strings.

**Step 4 — Deployed manifest page queries (Tier B):**

Deployed manifest settings contain page-level `data.query` strings that may differ from local `manifests/*.ts` files. These are the queries that actually run in production.

1. **Download manifest settings locally** (large payloads — bypass MCP extension budget):
   - **Preferred**: Use official CLI MCP `graphql.execute` — no response budget, returns full payload:
     ```
     graphql.execute({ query: "{ settings(name: [\"fc.mystique.manifest.oms\"], context: ACCOUNT, contextId: 0, first: 1) { edges { node { name lobValue } } } }" })
     ```
   - **Fallback** (if CLI MCP unavailable): Use Bash + REST API direct — saves to file, no size limit:
     ```bash
     TOKEN=$(fluent auth token -p <PROFILE>)
     ACCT=$(fluent profile show -p <PROFILE> --json 2>/dev/null | node -e "process.stdin.on('data',d=>{const j=JSON.parse(d);console.log(j.accountId||j.account)})")
     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
     ```
   - **DO NOT use extension `graphql.query`** for manifest settings — the 50K response budget (`FLUENT_RESPONSE_BUDGET_CHARS`) truncates manifests silently.
   - Save each setting to `accounts/<PROFILE>/manifests/deployed/<setting-name>.json`. If cached copies exist (< 24h old), reuse.

2. **Extract page queries:**
   - Parse each manifest JSON. Walk `routes[]` recursively (sections contain `pages[]`, pages may be nested).
   - For each page with `data.query`, extract the query string. Track `[setting-name] page:<path>` as the source.

3. **Validate each query** via `graphql.validate`. Check `valid` field in response (not `ok`). Report with setting name + page path as location.

4. **Runtime execution gate (MANDATORY for changed/critical pages):**
   - Execute the exact extracted page query with representative real variables from target environment (real entity IDs/refs, retailer context).
   - Treat any GraphQL `errors[]` as a blocking finding (CRITICAL for production deploy readiness).
   - Pay special attention to non-null field failures (they often nullify entire page data).
   - If page overrides/forms use `{{me.username}}`, verify query includes and returns `me { username }`.

**Step 5 — Workflow user action cross-reference (Tier C):**

Workflows define user actions whose `attributes[]` reference FieldRegistry aliases and settings. Cross-reference with the frontend.

1. **Ensure local workflow files exist:**
   ```
   ls accounts/<PROFILE>/workflows/<RETAILER_REF>/
   # If missing: fluent workflow download -p <PROFILE> -r <RETAILER_REF> -w all -o accounts/<PROFILE>/workflows/<RETAILER_REF>/
   ```

2. **Extract user action attributes** from each workflow JSON: `attributes[].name`, `attributes[].type`, `attributes[].source`.

3. **Cross-reference checks:**

| Check | Severity | What It Catches |
|-------|----------|-----------------|
| **Field type not registered** | HIGH | Workflow `attributes[].type` references a FieldRegistry alias not found in `src/index.tsx` |
| **Settings source missing** | HIGH | `source: "settings.<KEY>"` but setting doesn't exist |
| **Settings source case** | CRITICAL | `source: "Settings.<KEY>"` (capital S) — silently fails, dropdown is empty |
| **Settings value shape** | HIGH | Setting exists but value is plain array, not `{ "active": [...] }` wrapper |

### Phase 4b: Data Fetching & Performance

Check for data fetching anti-patterns and performance issues. Reference: `/fluent-mystique-sdk-reference` (Tier 1/2/3 data-fetching hierarchy).

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **N+1 query pattern** | CRITICAL | Component rendered inside a list/map that calls `useQuery()` per item — each instance fires a separate API call. Move query to parent or page level. |
| **Unbounded connection fetch** | HIGH | GraphQL query on a connection (`edges/node`) without `first: N` parameter — can fetch thousands of records. Add pagination. |
| **Over-fetching (3+ levels deep)** | MEDIUM | GraphQL query selecting 3+ nested connection levels (e.g., `order.items.product.categories.edges`) — exponential response size. Select only displayed fields. |
| **Entity data in useState** | MEDIUM | Storing API response data in `useState` instead of reading via `useData()` — risks stale data after concurrent updates. State should hold UI state (expanded, selected tab), not entity data. Exception: FieldRegistry fields in dialogs. |
| **useQuery where useData suffices** | MEDIUM | ComponentRegistry component calls `useQuery()` for data the page query already fetches — wasted API call. Use `useData()` to read parent page data. |
| **Duplicate queries across components** | HIGH | Two sibling components on the same page call `useQuery()` with identical query+variables — no shared cache, two API calls. Lift to parent or page query. |
| **Missing React.memo on list items** | LOW | Component rendered in a list (50+ items) without `React.memo()` — potential re-render cost on parent state changes. |

### Phase 4c: Component Structure

Check internal project organization for maintainability:

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **Fat registered components** | MEDIUM | Registered ComponentRegistry entry exceeds ~200 lines mixing data fetching and presentation. Split into container + view. |
| **Container+View split opportunity** | LOW | Component uses 3+ hooks (`useQuery`, `useSettings`, `useAuth`, `useData`, `useRest`, custom hooks) in the same file without a container/view separation. Splitting into a container (data fetching + state management) and a pure view (props-only rendering) improves testability — the view can be tested with plain props, no hook mocks needed. Advisory — OOTB reference plugins follow this pattern for complex components. |
| **useQuery when page data available** | MEDIUM | Content component (`category: 'content'`) calls `useQuery()` to fetch data that is already available via `StdProps.data` from the page-level query. This adds an unnecessary GraphQL API call. Verify the needed fields are in the page query; if so, use `data` prop with manifest `dataSource` scoping instead. Exception: FieldRegistry fields in user action dialogs MUST use `useQuery()` since page data is unavailable. |
| **N+1 query in list/repeater** | HIGH | Component rendered per-row inside `fc.list` row expansion or `fc.repeater` descendants calls `useQuery()`. This fires a separate API call PER ROW with no shared query cache. Use `fc.provider.graphql` in the manifest to wrap the list/repeater, or expand the page query to include the needed data. |
| **Duplicated logic across components** | MEDIUM | Same utility function or hook pattern copy-pasted in 2+ component files. Extract to `src/shared/`. |
| **Missing shared layer** | LOW | Project has 3+ registered components but no `src/shared/`, `src/hooks/`, or `src/utils/` directory — may indicate missed reuse opportunities. |
| **Business logic in presentation** | MEDIUM | Price formatting, date formatting, or status mapping done inline in JSX. Extract to shared utility. |
| **Missing loading/error guard** | HIGH | Component uses `useQuery()` or `useSettings()` but renders data without checking `fetching`/`status` first. Must implement 4-stage guard: loading → error → empty → success. |
| **Missing Product union fragments** | HIGH | GraphQL query selects `product { ref gtin }` without inline fragments (`... on StandardProduct`, etc.). Returns null for union-specific fields. |
| **Wrong useQuery destructuring** | CRITICAL | `const [data, { loading, error }] = useQuery(...)` — wrong tuple shape. Correct: `const [result] = useQuery<T>(...)` then access `result.fetching`, `result.data`, `result.error`. |
| **useSettings without status check** | HIGH | Accessing `settings.key.result.value` without first checking `settings.key.status === 'ok'` — crashes at runtime when status is `'loading'` or `'error'`. |
| **Hardcoded user-facing strings** | HIGH | User-visible strings in JSX not wrapped in `translateOr(['i18n.key', 'Fallback'])`. Every production string should use i18n with a fallback. Exclude: className, data-testid, console.log, developer error messages. |
| **i18n key naming convention** | MEDIUM | Translation keys not following `{namespace}.{area}.{component}.{element}.{attribute}` dot-separated pattern (e.g., `acme.oms.appeasement.field.amount.label`). Keys like `amount_label` or `AmountLabel` break convention. |
| **translate() without fallback** | MEDIUM | Using `translate('key')` instead of `translateOr(['key', 'Fallback'])` — returns raw key string if missing, visible to end users. |
| **Missing translation file** | HIGH | Project uses `translateOr`/`useI18n` but has no `i18n/locales/en/translation.json` — translations will all fall through to inline fallbacks, breaking multi-language support. |
| **Orphaned translation keys** | LOW | Keys in `translation.json` not referenced in any source file — bloats translation files and confuses translators. |
| **Missing StylesProvider isolation** | HIGH | Plugin uses `makeStyles()` but has no `StylesProvider` + `createGenerateClassName` wrapper in `src/index.tsx`. CSS classes collide with other plugins. |

### Phase 5: Code Documentation

Check documentation quality in source files:

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **Exported components without JSDoc** | HIGH | Every exported React component should have a `/** */` block with `@description` and `@example` |
| **Props interfaces without field-level comments** | MEDIUM | Every props interface/type should have JSDoc on each field |
| **src/index.tsx missing header comment** | HIGH | Should have a comment block explaining what the plugin does and what components it registers |
| **Complex logic without comments** | LOW | Functions with cyclomatic complexity > 5 should have explanatory comments |
| **Custom hooks without return type docs** | MEDIUM | Custom hooks should document their return type and usage pattern |

### Phase 6: Test Quality

Check test coverage and quality:

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **Component without test file** | HIGH | Every component in `src/components/*/` should have a corresponding test. Check BOTH co-located `*.test.tsx` files AND project-root `__tests__/` directory. Only flag if NO test file covers that component in either location. |
| **Field without test file** | HIGH | Every field in `src/fields/*/` should have a corresponding test. Check BOTH co-located `*.test.tsx` files AND project-root `__tests__/` directory. Only flag if NO test file covers that field in either location. |
| **Test without assertions** | MEDIUM | Test files should have at least one `expect()` call (not just render-only tests) |
| **Coverage threshold** | MEDIUM | If coverage data exists (from a prior build), check against 70% threshold |
| **Excessive snapshot testing** | LOW | > 50% of assertions are `toMatchSnapshot` — prefer behavioral assertions. Storyshots are acceptable for selective visual regression coverage, but should not dominate the suite. |
| **Missing Mystique hook mocks** | MEDIUM | Test files use `useQuery`/`useSettings`/`useI18n` without mock setup. Check for `jest.spyOn` on Mystique hook modules or a shared `MystiqueMocks.ts`/`TestUtils.ts` utility. Without mocks, tests crash at runtime. |
| **Missing Mystique Jest bootstrap** | HIGH | `jest.config.js` lacks `setupFiles` for `lib/material-ui.js` / `lib/init.js` / `lib/mystique.js`, or lacks `moduleNameMapper` for `^mystique` | Mystique globals are host-injected at runtime; without the bootstrap shim, unit tests fail before component code executes. |
| **ComponentRegistry without story** | HIGH | Registered component in `src/components/*/` has no co-located `.stories.tsx` file. Stories enable visual regression, design review, and edge case coverage (empty, error, loading states) without deploying. |
| **FieldRegistry without story** | HIGH | Registered field in `src/fields/*/` has no co-located `.stories.tsx` file. Fields are high-risk — they handle user input, validation, and the onChange contract. Must test all visual states. |
| **Story lacks variant coverage** | MEDIUM | Story file exists but has only 1 named export (just `Default`). Good stories cover: default, empty data, error state, disabled, edge cases (long text, many items). Aim for 3+ variants per component. |

### Phase 7: README Completeness

Check `README.md` exists in project root and has required sections:

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **README exists** | HIGH | `README.md` in project root |
| **Description section** | MEDIUM | What the plugin does |
| **Components section** | MEDIUM | Table of registered components with aliases |
| **Build section** | MEDIUM | `yarn install`, `yarn build`, `yarn test` instructions |
| **Local test loop section** | HIGH | Documents localhost workflow (`yarn start`, `plugins[]` -> `http://localhost:3001/`, iteration steps, switch-back to CDN) |
| **Configure section** | HIGH | How to add to manifest `plugins[]` |
| **Deploy section** | MEDIUM | CDN hosting instructions |

### Phase 8: Bundle Health (if dist/ exists)

If a prior build exists in `dist/`, check:

| Check | Severity | Threshold |
|-------|----------|-----------|
| **Bundle size** | HIGH/CRITICAL | < 200KB = OK, 200-500KB = WARN (HIGH), > 500KB = FAIL (CRITICAL) |
| **Source maps in production** | MEDIUM | Production build should NOT include `.map` files in `dist/` |
| **version.json** | HIGH | Must exist with valid `{ bundle, version }` |

### Phase 8b: i18n Coverage (if i18n/ exists)

Check translation file health and manifest i18n compliance:

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **Missing translation.json** | MEDIUM | Source has `translateOr` calls but no `i18n/locales/en/translation.json` |
| **Orphaned i18n keys** | LOW | Keys in translation.json not referenced by any source file — dead translations |
| **Missing i18n keys** | MEDIUM | `translateOr(['key', ...])` in source where `key` is absent from translation.json — relies on fallback string, not translatable |
| **Manifest labels without i18n prefix** | MEDIUM | `manifests/*.ts` files with `title`, `label`, or `menuLabel` values that don't start with `i18n:` or `{{` — not translatable. Check: literal English strings in manifest component props. |
| **i18n key naming violation** | LOW | Keys not following `{company}.{app}.{area}.{component}.{element}` hierarchical pattern |

### Phase 9: Deployment Safety (if manifests exist)

Scan manifest files in `manifests/` and any deployed manifest settings for localhost references:

| Check | Severity | What to Look For |
|-------|----------|-----------------|
| **localhost in plugins[]** | CRITICAL | `"src": "http://localhost"` in any manifest `plugins[]` array — breaks UI for all users if deployed |
| **Dev server URL in production config** | CRITICAL | `localhost:3001` or `127.0.0.1` in any `.json` manifest file intended for deployment |
| **Missing version.json auto-discovery** | MEDIUM | Plugin `src` URL without trailing `/` — Mystique can't discover `version.json` |

## Report Output

Generate a structured report at:

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

Create the directory with `mkdir -p` if it does not exist.

### Report Structure

```json
{
  "project": "<name>",
  "profile": "<PROFILE>",
  "timestamp": "<ISO>",
  "phases": {
    "typescript": { "findings": [...], "score": "A|B|C|D|F" },
    "react": { "findings": [...], "score": "..." },
    "sdk": { "findings": [...], "score": "..." },
    "graphql": { "findings": [...], "score": "...", "tierA": { "queriesFound": 0, "valid": 0, "invalid": 0 }, "tierB": { "manifestsChecked": 0, "pagesChecked": 0, "valid": 0, "invalid": 0 }, "tierC": { "workflowsChecked": 0, "attributesChecked": 0, "mismatches": 0 } },
    "dataFetching": { "findings": [...], "score": "..." },
    "structure": { "findings": [...], "score": "..." },
    "documentation": { "findings": [...], "score": "..." },
    "tests": { "findings": [...], "score": "..." },
    "readme": { "findings": [...], "score": "..." },
    "bundle": { "findings": [...], "score": "..." },
    "deployment": { "findings": [...], "score": "..." }
  },
  "overall": {
    "score": "A|B|C|D|F",
    "critical": 0,
    "high": 2,
    "medium": 5,
    "low": 3,
    "total": 10
  }
}
```

### Scoring

| Grade | Condition |
|-------|-----------|
| A | 0 critical + 0 high |
| B | 0 critical + 1-3 high |
| C | 0 critical + 4+ high |
| D | 1 critical |
| F | 2+ critical |

### Finding Structure

Each finding in the report:

```json
{
  "phase": "typescript|react|sdk|graphql|dataFetching|structure|documentation|tests|readme|bundle|deployment",
  "severity": "CRITICAL|HIGH|MEDIUM|LOW",
  "rule": "rule-id",
  "message": "Human-readable description",
  "file": "src/...",
  "line": 45
}
```

## Severity Reference

| Severity | Meaning | Examples |
|---|---|---|
| CRITICAL | Will break at runtime or violate platform constraints | React 18 API on React 17, `@mui/material` import, infinite loop pattern |
| HIGH | Significant quality issue or missing coverage | No tests for component, README missing configure section, wrong register alias format |
| MEDIUM | Quality improvement opportunity | `any` types, missing JSDoc, `useQuery` where `useData` would work |
| LOW | Minor style/documentation issue | Missing return type, hardcoded string, excessive snapshots |

## Presentation

Present findings to the user grouped by phase, sorted by severity (CRITICAL first):

```
## Frontend Code Review: <project-name>

### TypeScript Quality  [B]
- [MEDIUM] src/components/MyComponent/MyComponent.tsx:45 -- Explicit `any` on event handler
- [LOW] src/hooks/useMyData.ts:12 -- Missing return type on exported function

### React Patterns  [A]
(no findings)

### SDK Compliance  [D]
- [MEDIUM] src/components/MyComponent/MyComponent.tsx:3 -- @mui/material import without @mui/material in dependencies (if bundled as dependency, this is valid; otherwise must use @material-ui/core)

### GraphQL Schema  [C]
- [CRITICAL] src/queries/orderQuery.ts:5 -- Invalid field `order.name` (field does not exist on type Order — did you mean `ref`?)
- [HIGH] src/queries/orderQuery.ts:2 -- Unused variable `$status` declared but not used in query body (Fluent rejects unused variables)
- [MEDIUM] src/hooks/useCustomData.ts:12 -- Query could not be statically extracted (dynamic template interpolation) — validate manually
Validated: 4 queries, 2 invalid, 1 skipped

### Documentation  [B]
- [HIGH] src/index.tsx -- Missing header comment explaining plugin purpose
- [MEDIUM] src/components/MyComponent/MyComponent.tsx -- Exported component without JSDoc

### Test Quality  [B]
- [HIGH] src/components/MyComponent/ -- Missing test file

### README  [B]
- [HIGH] README.md -- Missing "Configure" section

### Bundle Health  [A]
(not applicable -- no dist/ found)

---
**Overall: D** (1 CRITICAL, 2 HIGH, 2 MEDIUM, 1 LOW)
-> REVIEW: accounts/<PROFILE>/reports/frontend-review/<project>-2026-02-28.json
```

## Integration with Other Skills

| Artifact | Consumed by | Path |
|---|---|---|
| Review report | `/fluent-frontend-build` (optional pre-build quality gate) | `accounts/<PROFILE>/reports/frontend-review/` |
| README findings | `/fluent-frontend-readme` (generates missing README) | In report JSON |
| SDK compliance findings | `/fluent-mystique-scaffold` (fix templates for new projects) | In report JSON |

## Gotchas

- **Don't read node_modules/**: Skip all files under `node_modules/`, `dist/`, `coverage/`, `.storybook/`
- **Test files are lenient**: `any` types and non-null assertions in `.test.tsx` files are acceptable -- only flag in production source
- **Emotion styled components**: `styled()` from `@emotion` creates wrapper components -- don't flag these as "missing JSDoc on exported component"
- **Re-exports**: `index.ts` barrel files that just re-export don't need JSDoc
- **useData availability**: `useData` works anywhere inside an active Mystique data context (page descendants, provider descendants, filter/layout/content components). Components rendered outside that chain, such as user action dialogs, modals, or standalone mounts, must use `useQuery`
- **FieldRegistry fields in user action dialogs**: Both `data` prop and `useData()` return empty/undefined in user action dialogs. FieldRegistry fields that need entity data MUST use `useQuery()` with a self-contained GraphQL query and should prefer `entityContext` over URL parsing for entity identity. The dialog renders outside the page's React data context provider. This is the #1 gotcha for custom form fields.
- **Embedded GraphQL validation**: FieldRegistry fields often embed GraphQL queries directly in source (for `useQuery()`). These are invisible to manifest validation and only fail at runtime. Always flag for MCP `graphql.validate` in Phase 4.
- **StylesProvider is plugin-level**: Only check for `StylesProvider`/`createGenerateClassName` if the plugin uses `makeStyles` or MUI `styled()`. Plugins using only inline styles or having a single component have lower collision risk.
- **Product union types**: The `product` field in Fluent GraphQL is a union (`StandardProduct | GroupProduct | VariantProduct`). Queries selecting `product { ref gtin }` without inline fragments get null for union-specific fields. `GroupProduct` does NOT have `gtin`.
- **MCP response budget truncates large payloads**: `fluent-mcp-extn` has a 50K char budget. Manifest settings (10-60KB each) and workflows (50K+) get silently truncated. For Phase 4 Steps 4-5, 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).

This is an analysis skill — follow the **Learning Capture Protocol** (see CLAUDE.md Cross-Skill Conventions). After review, when the user confirms or corrects findings about SDK runtime behavior, local testing conventions, or deployment quirks, write the confirmed learning to the `implementation-learnings.md` auto-memory file under the relevant account heading.

## 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-review"`
   - `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-review.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 -->
