---
name: fluent-mystique-validate
description: Validate Mystique manifest JSON against the v2.0 schema, lint component configurations, and cross-reference aliases, data sources, and i18n keys. Triggers on "validate manifest", "check mystique", "lint manifest", "validate component", "mystique issues".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: <manifest-file-or-directory> [--strict] [--fix-suggestions] [--component <alias>]
cursor-tier: manual
---

# Mystique Manifest Validator

Validate Fluent Commerce Mystique manifest JSON files against the v2.0 schema, lint component configurations, cross-reference aliases against the known component registry, validate data sources and GraphQL queries, check template string syntax, and verify i18n key usage.

This is a **frontend/UI validation** skill. Mystique manifests are JSON configuration files deployed to Fluent UX Apps that define the entire UI layout, data binding, and component composition. They are separate from backend workflow definitions.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-mystique-validate.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

## Ownership Boundary

This skill owns manifest schema validation, component prop type checking, cross-reference resolution, template string syntax, and i18n key completeness checking.

Other skills own:
- Manifest creation and editing: `/fluent-mystique-builder`
- SDK component project structure: `/fluent-mystique-scaffold`
- Deployed manifest analysis and metrics: `/fluent-mystique-analyze`
- GraphQL query execution against live API: MCP tools (`graphql.query`)
- Workflow validation: `/fluent-workflow-analyzer`
- Settings management and manifest deployment: `/fluent-settings`

## When to Use

- Before deploying a manifest to a Fluent environment
- After editing manifest JSON to catch structural errors before runtime
- When inheriting an unfamiliar manifest to assess its structural health
- After `/fluent-mystique-builder` generates or modifies manifest JSON (always run as post-build hook)
- To validate a single component's configuration within a manifest (`--component` flag)
- To get actionable fix suggestions for identified issues (`--fix-suggestions` flag)

## Required Inputs

- **Manifest file or directory**: Path to a `.json` manifest file, or a directory containing manifest files
  - If a directory, validate all `.json` files that contain `"manifestVersion": "2.0"`
  - Search recursively; skip `node_modules/`, `dist/`, `.git/`
- **Flags**:
  - `--strict`: Treat MEDIUM and LOW issues as errors (default: only CRITICAL and HIGH are errors)
  - `--fix-suggestions`: Include suggested JSON patches for each issue
  - `--component <alias>`: Validate only instances of the specified component alias

## Mystique Architecture Context

### What is Mystique?

Mystique is Fluent Commerce's declarative UI framework. Instead of writing React code for every screen, implementers define **manifest JSON** files that describe:

- **Routes**: URL paths organized into sections and pages
- **Pages**: Top-level containers with GraphQL data queries
- **Components**: Nested tree of UI building blocks (lists, cards, charts, forms)
- **Data binding**: JSONPath-based `dataSource` scoping from page query results to components
- **Template strings**: Handlebars expressions for dynamic values (`{{ref}}`, `{{currency amount code}}`)
- **Role-based visibility**: Component/page/section access control via `roles[]`
- **Fragments**: Reusable manifest snippets loaded via setting references

### Manifest v2.0 Schema

```
MystiqueManifest
  manifestVersion: "2.0"           # REQUIRED, must be exactly "2.0"
  name: string                     # REQUIRED, app identifier (no spaces)
  title: string                    # REQUIRED, display name (supports i18n: prefix)
  icon?: string                    # Material icon name
  context?: { level, role? }       # account | retailer | location
  orchestrationAlias?: string      # entity type alias mapping
  plugins?: MystiquePlugin[]       # external component bundles
  homePath: string                 # REQUIRED, landing route path
  routes: MystiqueManifestRoute[]  # REQUIRED, non-empty array
```

### Plugin and Custom Component Model

Fluent UX Apps render built-in components from the core registry. Custom components are **React bundles** (`bundle.js`) built with the OMX Component SDK, registered via `ComponentRegistry.register()`, and hosted externally.

- **Development**: Custom bundles served from `localhost` (e.g., `http://localhost:3001/bundle.js`)
- **Production**: Custom bundles hosted on CDN/Vercel/S3 (e.g., `https://cdn.example.com/plugins/bundle.abc123.js`)
- **Fluent does NOT host custom component bundles** -- they must be self-hosted
- **Plugins array**: Manifest references bundles via `plugins: [{ type: "url", src: "https://..." }]` or `plugins: [{ type: "setting", setting: "settingName" }]`

### Component Instance Structure

Every UI element in a manifest is a `MystiqueComponentInstance`:

```
MystiqueComponentInstance
  component: string           # REQUIRED, registry alias (e.g., "fc.list")
  dataSource?: string         # JSONPath scoping data context
  props?: Record<string,any>  # Component-specific configuration
  descendants?: Instance[]    # Nested child components
  roles?: string[]            # Role-based visibility
```

---

## Component Alias Registry

The following table lists all **verified** component aliases extracted from the official OMX Mystique component documentation and source code. The validator uses this registry to resolve `component` field values.

### Layout and Container Components

| Alias | Legacy Alias | Category | Expects dataSource | Expects descendants | Key Props |
|-------|-------------|----------|-------------------|--------------------|----|
| `fc.page` | `shared.components.material.DynamicPage` | page | No (uses page `data`) | Yes | `title` (recommended) |
| `fc.page.wizard` | -- | page | No | Yes | `steps[]`, `title` |
| `fc.page.wizard.summary` | -- | page | No | No | `width`, `noCard`, `sticky` |
| `fc.page.section` | -- | layout | Optional | Yes | `title`, `icon` |
| `fc.page.section.column` | -- | layout | No | Yes | `width` (CardWidth), `height` (`start`/`stretch`) |
| `fc.page.section.header` | -- | layout | No | Yes | `title`, `icon` |
| `fc.page.refresh` | -- | layout | No | Yes | `interval` |
| `fc.conditional` | -- | layout | No | Yes | **`value`** (required), **`matches`** (required) |
| `fc.repeater` | -- | layout | **Yes** (required) | Yes | `dataSource` (path to array/connection) |
| `fc.provider.graphql` | -- | layout | No | Yes | **`query`** (required), `variables`, **`path`** (required), `max`, `loadingBehavior` |
| `fc.workflow.provider` | -- | layout | No | Yes | (no props — fetches workflow data via REST) |
| `fc.tabs` | `shared.components.material.MaterialTabsSet` | layout | No | Yes | **`layouts[]`** (required), `urlKey` |
| `fc.tabs.card` | -- | layout | No | Yes | **`tabs[]`** (required), `width`, `urlKey` |
| `fc.tab.content` | `shared.components.material.SubLayoutContent` | layout | No | Yes | (pass-through wrapper) |
| `fc.accordion` | -- | content | No | Optional | `summary`, `details`, `expandIcon`, `expanded`, `defaultExpanded`, `disabled`, `indent` |
| `fc.filterPanel` | -- | layout | No | Yes | filter configuration |

### Content and Display Components

| Alias | Legacy Alias | Category | Expects dataSource | Expects descendants | Key Props |
|-------|-------------|----------|-------------------|--------------------|----|
| `fc.list` | `shared.components.material.DynamicList` | content | **Yes** (required) | Optional (row expansion) | **`attributes[]`** (required), **`defaultPageSize`** (required), `title`, `rowLink`, `filter`, `actions[]`, `density`, `responsiveness`, `vAlign`, `rowComponent`, `rowsPerPageOptions[]`, `loading` |
| `fc.card.attribute` | `shared.components.material.DynamicCard` | content | Optional | No | **`attributes[]`** (required), `title`, `highlight`, `width`, `cardImage` |
| `fc.card.image` | -- | content | No | Optional | **`image`** (required, `{imageUrl, width?, height?}`), `width`, `indent`, `descendantSpacing`, `descendantDirection` |
| `fc.card.product` | -- | content | Optional | Optional | `title`, `width`, `image`, `attributes[]` |
| `fc.card.multi` | -- | content | Optional | Yes | `title`, `width` |
| `fc.card.attributes.grid` | -- | content | **Yes** (required) | No | `exclude[]` |
| `fc.card.map.point` | -- | content | Optional | No | map configuration |
| `fc.tile.metric` | -- | content | No | No | **`label`** (required), **`value`** (required), `options[]`, `variant`, `width`, `link` |
| `fc.attribute.json` | -- | content | Optional | No | JSON display |
| `fc.attribute.column` | -- | content | Optional | No | column attribute display |
| `fc.attribute.locationId` | -- | content | No | No | location display |
| `fc.attribute.retailerId` | -- | content | No | No | retailer display |
| `fc.mystique.collapsible.text` | -- | content | No | No | **`text`** (required), **`charCutoff`** (required) |
| `fc.mystique.collapsible.attributes` | -- | content | Optional | No | collapsible attribute list |
| `fc.mystique.link` | -- | content | No | No | `label`, `link` |
| `fc.progress.circular` | -- | content | No | No | progress indicator |
| `fc.dashboard.threshold` | -- | content | Optional | No | dashboard threshold config |
| `fc.stepper` | -- | content | No | No | step progress indicator |
| `fc.json.viewer` | -- | content | Optional | No | read-only JSON display |
| `fc.json.editor` | -- | content | Optional | No | editable JSON with validation |
| `fc.quantity.list` | -- | content | **Yes** (required) | No | `attributes[]`, `defaultPageSize`, `quantitySelectorProps` |
| `fc.scanner.barcode` | -- | content | No | No | USB barcode scanner input |
| `fc.scanner.camera` | -- | content | No | No | camera barcode scanner |
| `fc.scanner.barcodeFilter` | -- | content | No | No | barcode filter bar |
| `fc.settingForm` | -- | content | No | No | `setting`, `context`, `contextId` |
| `fc.action.inline` | -- | content | Optional | No | inline user action |
| `fc.mutation.inline` | -- | content | No | No | `mutation` (config), `title`, `width` |
| `fc.list.customAction` | -- | content | **Yes** (required) | No | `attributes[]`, `action` (callback config) |

### Chart Components

| Alias | Category | Expects dataSource | Key Props |
|-------|----------|-------------------|-----------|
| `fc.chart.area` | content | Yes | chart data config |
| `fc.chart.area.wrapper.feed` | content | Yes | feed-based area chart |
| `fc.chart.bar` | content | Yes | chart data config |
| `fc.chart.bar.wrapper.source` | content | Yes | source-based bar chart |
| `fc.chart.line` | content | Yes | chart data config |
| `fc.chart.gauge` | content | Yes | chart data config |
| `fc.chart.gauge.wrapper.threshold` | content | Yes | threshold-based gauge |

### List Wrapper Components

| Alias | Category | Expects dataSource | Key Props |
|-------|----------|-------------------|-----------|
| `fc.list.wrapper.feed` | content | Yes | feed-based list |
| `fc.list.wrapper.source` | content | Yes | source-based list |
| `fc.list.wrapper.bppmetrics` | content | Yes | BPP metrics list |

### Action and Button Components

| Alias | Category | Key Props |
|-------|----------|----|
| `fc.button.print` | action | `label`, print configuration |
| `fc.button.print.pick` | action | `label`, pick-specific print |
| `fc.button.print.inline` | action | `label`, inline print |
| `fc.button.print.inline.compatibility` | action | legacy inline print |
| `fc.button.print.download` | action | `label`, download print |
| `fc.modal.button` | action | modal trigger button |
| `fc.modal.button.addItem` | action | add item modal trigger |
| `fc.drawer.button` | action | drawer trigger button |
| `fc.buttons.add.reject` | action | add/reject action buttons |
| `fc.button.bar` | action | grouped action button bar |
| `fc.sourcing.profile.drawer.button` | action | sourcing profile drawer trigger |
| `fc.sourcing.profile.modal.button` | action | sourcing profile modal trigger |
| `fc.sourcing.strategy.modal.button` | action | sourcing strategy modal trigger |

### Form and Field Components

| Alias | Category | Key Props |
|-------|----------|----|
| `select` | field | query/setting/manifest options |
| `retailer` | field | retailer selector |
| `fc.field.daterange` / `daterange` | field | date range filter |
| `fc.field.intrange` / `intrange` | field | number range filter |
| `fc.field.filterComplex` | field | complex filter |
| `fc.field.multistring` | field | multi-value search |
| `fc.page.filter.select` | field | page-level filter select |
| `fc.attribute.jsoneditor` | field | JSON editor |
| `AttributeInput.list` | field | attribute input list |
| `ExtendedAddressInput` | field | address input |
| `fc.scanner.barcode` / `fc.barcode.scanner` | field | USB barcode scanner (see also Content section) |
| `fc.action.field.wavepick` | field | wave pick list |
| `fc.action.field.fulfilmentpack` / `FULFILMENT_PACK` | field | fulfilment pack field |
| `fc.action.field.multiparcel` / `MULTI_PARCEL_FIELD` | field | multi-parcel field |
| `fc.action.field.returnitems` / `RETURN_ITEMS` | field | return items field |
| `FULFILMENT_IDS` | field | wave create details |
| `fc.daterange.wrapper.forwarder` | field | date range forwarding |

### OMS-Specific Components

| Alias | Category | Key Props |
|-------|----------|----|
| `fc.events.search` | content | event search component |
| `fc.event.detail` | content | event detail drawer |
| `fc.activity.entity` | content | entity activity timeline |
| `fc.order.itemDetails` | content | order item details drawer |
| `fc.order.shipmentDetails` | content | shipment details drawer |
| `fc.return.rowExpansion` | content | return row expansion |
| `fc.reports.ipuipc` | content | IPU/IPC report |

### Analytics Components

| Alias | Category | Key Props |
|-------|----------|----|
| `fc.analytics.viz` | content | Looker visualization |

### Custom Components

Custom component aliases are registered via `ComponentRegistry.register()` in SDK plugin bundles. They follow the pattern:
- `custom.<name>` -- common custom prefix
- `<project>.<name>` -- project-scoped prefix

Custom aliases are NOT in this registry. The validator flags them as **INFO** (not errors) with a suggestion to verify the component is registered in a loaded plugin.

---

## Validation Rules

### Phase 1: Schema Compliance

| ID | Rule | Severity | Description |
|----|------|----------|-------------|
| S01 | `manifestVersion` required | CRITICAL | Must be exactly `"2.0"`. Missing or wrong version means the manifest will not load. |
| S02 | `name` required | CRITICAL | Non-empty string, no spaces. Used for logging and user action filtering. |
| S03 | `homePath` required | CRITICAL | Must be a non-empty string matching a defined route path. |
| S04 | `routes` non-empty | CRITICAL | At least one route must be defined. |
| S05 | `title` required | HIGH | Non-empty string. Displayed in app header and browser title bar. Supports `i18n:` prefix. |
| S06 | `context.level` enum | HIGH | If `context` is present, `level` must be `account`, `retailer`, or `location`. |
| S07 | Route `type` enum | HIGH | Every route must have `type` set to `section`, `page`, or `reference`. |
| S08 | Page `path` required | HIGH | Every page must have a non-empty `path` string. |
| S09 | Page `component` required | HIGH | Every page must have a `component` field. Typically `fc.page` or `fc.page.wizard`. |
| S10 | Section must have `pages` | MEDIUM | Sections must contain a non-empty `pages` array. |
| S11 | Section must have `nav` | MEDIUM | Sections need `nav.label` and `nav.icon` for sidebar rendering. |
| S12 | Reference must have `settingName` | HIGH | Fragment references require a non-empty `settingName`. |
| S13 | Plugin structure valid | MEDIUM | Each plugin must be `{ type: "url", src: "<url>" }` or `{ type: "setting", setting: "<name>" }`. |
| S14 | `descendants` must be array | MEDIUM | If present, must be an array of component instances. |
| S15 | Fragment `manifestVersion` | HIGH | Manifest fragments must also declare `manifestVersion: "2.0"`. |
| S17 | Fragment pages need section wrapper | HIGH | Fragment routes must be wrapped in a `type: "section"` with `nav: { label, icon }`. Pages placed directly in `routes[]` without a section wrapper will NOT appear in the sidebar — Mystique renders sidebar entries from section `nav` objects only. If a fragment's `routes[]` contains pages without any section, emit: *"Pages at fragment root without a section wrapper — sidebar menu items will be missing. Wrap routes in a section with nav: { label, icon }."* |
| S16 | `dataSource` placement | HIGH | `dataSource` should be a **sibling** of `component` (same level as `props`), not inside `props`. Correct: `{ "component": "fc.list", "dataSource": "items", "props": {...} }`. Wrong: `{ "component": "fc.list", "props": { "dataSource": "items", ... } }`. The core `Children.tsx` renderer has a fallback (`comp.dataSource \|\| comp.props?.['dataSource']`) so it may work in the standard rendering path, but components that render descendants directly (bypassing `<Children />`) will NOT see it. The canonical TypeScript interface (`MystiqueComponentInstance`) defines `dataSource` as a peer of `component` and `props` — always use that placement. |

### Phase 2: Component Resolution

| ID | Rule | Severity | Description |
|----|------|----------|-------------|
| C01 | Component alias exists | HIGH | Every `component` field must resolve to a known alias from the registry above, a legacy alias, or be flagged as a custom component (prefixed or plugin-registered). |
| C02 | No duplicate route paths | HIGH | Two pages must not share the same `path` pattern within the same manifest. |
| C03 | `homePath` resolves | CRITICAL | The `homePath` value must match an existing route's `path` field. |
| C04 | Fragment `settingName` convention | MEDIUM | Fragment references should follow `fc.mystique.manifest.*` naming pattern. |
| C05 | Plugin name uniqueness | MEDIUM | No duplicate `name` values in the `plugins` array. |
| C08 | Plugins only in root manifest | CRITICAL | `plugins[]` array must ONLY appear in the root manifest (`fc.mystique.manifest.<app>`), NEVER in fragment settings. Fragments contain `routes[]` only. If a fragment has a `plugins` key, emit: *"CRITICAL: plugins[] found in fragment setting. Move plugin references to the root manifest."* |
| C06 | Nesting depth limit | LOW | Warn if component descendants are nested more than 8 levels deep. Deep nesting impacts rendering performance. |
| C07 | Role string format | LOW | Warn on role strings that do not match common patterns (e.g., `ADMIN`, `FULFILLMENT_ADMIN`, uppercase with underscores). |

### Phase 3: Data and Query Validation

| ID | Rule | Severity | Description |
|----|------|----------|-------------|
| D01 | `data.query` is valid GraphQL | HIGH | The `query` string in page `data` must be syntactically valid GraphQL. Check for balanced braces, valid field names, proper variable declarations. |
| D02 | `dataSource` path format | MEDIUM | Must be valid dot-notation JSONPath (e.g., `orderById.items`). No leading dots, no trailing dots, no double dots. For list/repeater bindings, stop at the connection root and do not append `.edges`. |
| D03 | `dataSource` references page data | MEDIUM | The root segment of a `dataSource` path should correspond to a top-level field in the nearest ancestor page or provider query. Implemented: the validator extracts root field names from `data.query` and `dataSources` keys, then checks that each component's `dataSource` root segment matches one of them. Components inside `fc.provider.graphql` are excluded (they have their own data context). Template expressions in `dataSource` are skipped (resolved dynamically). |
| D04 | Query variables referenced | LOW | Variables declared in `data.variables` should appear as `$varName` in the query string. Unused variables are likely copy-paste errors. |
| D05 | No query without consumer | LOW | If a page defines `data.query` but has no descendants that use `dataSource` or template strings referencing the data, the query is wasted. |
| D06 | Provider `query` required | HIGH | `fc.provider.graphql` components must have `props.query` defined. |
| D07 | `data` and `dataSources` mutually exclusive | HIGH | A page must not define both `data` (singular) and `dataSources` (plural). The router reads one or the other — if both are present, behavior is undefined. Use `data` for single queries, `dataSources` for multiple independent queries. |
| D08 | `dataSource` path avoids `.edges` for lists | MEDIUM | When `fc.list` or `fc.repeater` uses `dataSource`, the path should point to the connection root (e.g., `orderById.items`), not include `.edges`. The list component handles edge/node unwrapping internally. Including `.edges` causes double-unwrapping or empty renders. |

### Phase 4: Template String Validation

| ID | Rule | Severity | Description |
|----|------|----------|-------------|
| T01 | Handlebars syntax balanced | MEDIUM | All `{{` must have matching `}}`. Unbalanced delimiters cause runtime rendering failures. |
| T02 | Known template helpers | LOW | Warn on helper function names that are not in the standard set (50+ built-in helpers) **and** not recognized as custom helpers. **Built-in helpers:** **Comparison:** `eq`, `ne`, `gt`, `lt`, `gte`, `lte`. **Logic:** `and`, `or`, `not`, `if`, `unless`, `in`, `switch`, `hasRole`, `firstDefinedValue`, `anyMatch`. **Iteration:** `each`, `with`, `lookup`. **String:** `capitalise`/`capitalize`, `humanify`, `toLowerCase`, `toUpperCase`, `concat`, `stringify`, `substring`, `replace`, `split`, `join`, `includes`, `startsWith`, `endsWith`, `preserveWhitespace`, `partialStringMatch`, `decodeUrlParams`, `currentUrlParams`. **Placeholder:** `placeholderText`, `placeholderDate`, `placeholderArray`. **Date:** `dateFormat`, `dateStringFormatter`, `dateTimeFormatter`, `dateAdd`, `dateSubtract`, `dateRelative`, `dateFormatByLocale`. **Number:** `currency`, `formatNumber`, `add`, `subtract`, `multiply`, `divide`, `pow`, `math`. **Array:** `len`, `arraySum`, `length`, `pluralize`. **Special:** `barcode`, `json`, `i18n`. **Custom helper detection (3 layers):** (1) **Namespace-prefix auto-detect:** Helpers matching `<namespace>.helpers.<name>` pattern (e.g., `acme.helpers.format-currency`) are recognized as custom TemplateRegistry helpers — emit INFO: *"Custom helper '{{name}}' — verify SDK plugin is loaded in root manifest plugins[]"*, not WARN. (2) **Auto-scan:** When an SDK project is in scope, scan `src/index.tsx` for `TemplateRegistry.register()` calls and extract registered aliases automatically. (3) **Explicit whitelist:** Pass `customHelpers: ['helperName']` option to whitelist additional plugin-registered helpers. Only emit WARN for helpers that fail all three checks. |
| T03 | `i18n:` prefix format | MEDIUM | Strings prefixed with `i18n:` should follow the key convention pattern (e.g., `i18n:fc.om.orders.detail.title`). Warn on keys with spaces or special characters. |
| T04 | `rowLink` template produces URL | MEDIUM | `rowLink` values must produce valid relative URL paths. They should start with `/` or use template expressions that resolve to paths (e.g., `/orders/{{node.id}}`). |
| T05 | `_locale='en'` on backend date queries | HIGH | When `dateFormat` or `dateStringFormatter` is used inside `data.variables` (query variable values), warn if `_locale='en'` is missing. User locale can produce non-ASCII digits (Arabic, Thai) that break backend date parsing. |
| T06 | Triple-brace for unescaped output | LOW | `{{{helper}}}` (triple-brace) outputs unescaped HTML. Warn when used outside URL construction patterns — unescaped output can create XSS risks. `{{{currentUrlParams}}}` in `href`/`link` is acceptable. |
| T07 | User-facing strings should use `i18n:` or `{{` | MEDIUM | `title`, `label`, `menuLabel`, `emptyMessage`, and `placeholder` prop values that are plain English strings (not starting with `i18n:`, `{{`, or containing only template expressions) should use `i18n:` prefix for multi-language support. **Exceptions:** Values that are pure template expressions (`{{node.ref}}`), boolean/number values, or `data-testid` strings. Checks sections, pages, and component instances. |

### Phase 5: Component Props Validation

| ID | Rule | Severity | Description |
|----|------|----------|-------------|
| P01 | `fc.list` requires `attributes` | HIGH | The list component must have `props.attributes` as a non-empty array of column definitions. |
| P02 | `fc.list` requires `defaultPageSize` | MEDIUM | Should have `props.defaultPageSize` set. Defaults to 10 if missing but explicit is preferred. |
| P03 | `fc.list` attribute needs `label` | MEDIUM | Each attribute in a list should have a `label` (or i18n key) for the column header. |
| P04 | `fc.list` needs `dataSource` | HIGH | List components must have a `dataSource` pointing to a connection or array. |
| P05 | `fc.card.attribute` requires `attributes` | HIGH | Attribute card must have `props.attributes` as a non-empty array. |
| P06 | `fc.conditional` requires `value` and `matches` | HIGH | Must have both `props.value` (template string) and `props.matches` (string or string array). |
| P07 | `fc.repeater` requires `dataSource` | HIGH | Repeater component must have `dataSource` set to an array or connection path. |
| P08 | `fc.repeater` requires `descendants` | HIGH | Repeater must have at least one descendant to repeat. |
| P09 | `fc.provider.graphql` requires `query` | HIGH | Data provider must have `props.query` with a valid GraphQL query string. |
| P10 | `fc.tabs.card` requires `tabs` | HIGH | Tab card must have `props.tabs` as a non-empty array of tab definitions with `label`. |
| P11 | `fc.tabs.card` tabs-descendants count match | MEDIUM | Number of `tabs` entries should match number of `descendants`. Mismatches cause empty or unreachable tabs. |
| P12 | `fc.tile.metric` requires `label` and `value` | HIGH | Tile metric must have both `props.label` and `props.value`. |
| P13 | `fc.mystique.collapsible.text` requires `text` and `charCutoff` | HIGH | Collapsible text must have both `props.text` (string) and `props.charCutoff` (number). |
| P14 | `fc.card.image` requires `image` | HIGH | Image card must have `props.image` with at least `imageUrl`. |
| P15 | `fc.page` title recommended | LOW | Pages should have `props.title` for header rendering. Missing titles result in blank page headers. |
| P16 | `fc.page` recommended component | LOW | Page `component` field should typically be `fc.page` or `fc.page.wizard`. Other values may indicate misconfiguration. |
| P17 | `width` prop valid CardWidth | LOW | When present, `width` should be a valid CardWidth: `"quarter"`, `"third"`, `"half"`, `"two-thirds"`, `"full"`, or a number 1-12. |
| P18 | `fc.settingForm` requires setting reference | MEDIUM | Setting form component needs a setting name or reference to render. |
| P19 | Dynamic attribute `type` valid | MEDIUM | Attribute `type` if present must be `"image"` or `"component"`. Unknown types render nothing. |
| P20 | Image attribute needs `value` URL | MEDIUM | Attributes with `type: "image"` must have `value` (image URL template) and optionally `options.width`/`options.height`. |
| P21 | Component attribute needs `options.component` | HIGH | Attributes with `type: "component"` must have `options.component` pointing to a known alias. |
| P22 | Icon name has library prefix | LOW | Icon names in `options.styles[].icon.name` should include library prefix (`Md*` for Material Design, `Fa*` for Font Awesome). `"CheckCircle"` is wrong — use `"MdCheckCircle"`. |
| P23 | `enableCopyIcon` valid value | LOW | If present, `enableCopyIcon` must be `"displayAlways"` or `"displayOnHover"`. |

### Phase 6: Cross-Manifest and Plugin Validation

| ID | Rule | Severity | Description |
|----|------|----------|-------------|
| X01 | Fragment references resolve | HIGH | When `--strict` is set and other manifests/settings are available, verify that `settingName` in `type: "reference"` routes points to a known setting. |
| X02 | Plugin URL format | MEDIUM | Plugin `src` must be a valid URL (starts with `http://` or `https://`). |
| X03 | Plugin URL localhost warning | LOW | Warn if plugin `src` contains `localhost` or `127.0.0.1` -- this indicates a development configuration that will fail in production. |
| X04 | Plugin URL HTTPS for production | LOW | Warn if plugin `src` uses `http://` instead of `https://` (excluding localhost for dev). Production environments should use HTTPS. |
| X05 | Shared alias consistency | LOW | If validating a directory of manifests, flag cases where the same custom component alias has different prop patterns across manifests. |

### Cross-Artifact Validation (User Action Alignment)

When validating manifests, optionally check workflow user action alignment:
- The manifest's module name is the suffix after `fc.mystique.manifest.` (e.g., `fc.mystique.manifest.oms` → `"oms"`)
- If workflows are available locally, cross-check that user actions reference this module name in their `modules[]` array
- Report as WARNING if user actions reference a module name that has no corresponding `fc.mystique.manifest.<module>` setting

---

## Automated Validator

An automated Node.js linter script is available at `fluent-ai-skills/content/dev/skills/fluent-mystique-validate/validator.mjs`. **Always run this first** before manual review.

```bash
# Validate a single manifest
node fluent-ai-skills/content/dev/skills/fluent-mystique-validate/validator.mjs <manifest.json>

# Validate with fix suggestions (default: on)
node validator.mjs <manifest.json> --fix-suggestions

# Strict mode (MEDIUM/LOW also count as errors)
node validator.mjs <manifest.json> --strict

# Validate only specific component instances
node validator.mjs <manifest.json> --component fc.list

# JSON output for CI/CD
node validator.mjs <manifest.json> --json

# Validate all manifests in a directory
node validator.mjs <directory/>
```

Exit code: `0` = PASS, `1` = FAIL (CRITICAL or HIGH issues found, or all issues in `--strict` mode).

Implements all 65 rules across 6 phases: Schema (S01-S16), Components (C01-C08), Data/Query (D01-D08), Templates (T01-T07), Props (P01-P23), Cross-Manifest (X01-X05).

## Execution Flow

### Step 1: Discover and Load Manifests

```
1. Resolve input path:
   - If file: load single manifest JSON
   - If directory: recursively find all .json files containing "manifestVersion": "2.0"
   - Skip: node_modules/, dist/, .git/, build/

2. Parse each manifest:
   - Validate JSON syntax (report parse errors immediately)
   - Detect manifest vs fragment (presence of "name" + "homePath" = full manifest)
   - Record file path, size, route count for summary
```

### Step 2: Schema Compliance (Phase 1)

```
For each manifest:
  1. Check required top-level fields (S01-S04)
  2. Check recommended fields (S05)
  3. Validate context, plugins, routes structure (S06-S15)
  4. Record all issues with JSON path locations
```

### Step 3: Component Tree Walk (Phase 2 + Phase 5)

```
For each manifest:
  1. Walk all routes recursively:
     - Sections: validate nav, walk pages
     - Pages: validate path/component, walk descendants
     - References: validate settingName
  2. For each component instance encountered:
     a. Resolve alias against registry (C01)
     b. Track nesting depth (C06)
     c. Validate component-specific required props (P01-P18)
     d. Validate dataSource format (D02-D03)
     e. Validate template strings in props (T01-T04)
  3. Check route path uniqueness (C02)
  4. Verify homePath resolves (C03)
```

### Step 4: Data and Query Validation (Phase 3)

```
For each page with data.query:
  1. Validate GraphQL syntax (D01):
     - Balanced braces and parentheses
     - Valid query/mutation keyword
     - Variable declarations match usage
  2. Cross-reference variables (D04)
  3. Check data consumption by descendants (D05)

For each fc.provider.graphql:
  1. Validate props.query exists and is syntactically valid (D06)
```

### Step 5: Cross-Manifest Checks (Phase 6)

```
If validating multiple manifests:
  1. Check fragment reference resolution (X01)
  2. Validate plugin URLs (X02-X04)
  3. Check cross-manifest alias consistency (X05)
```

### Step 6: Generate Report

```
1. Aggregate all issues by severity
2. Sort: CRITICAL first, then HIGH, MEDIUM, LOW
3. Generate summary statistics
4. If --fix-suggestions: include suggested JSON patches
5. Output report to console and optionally to file
```

---

## Output Format

### Summary Banner

```
+----------------------------------------------------------+
|  Mystique Manifest Validation Report                      |
+----------------------------------------------------------+
|  File:       admin-manifest.json                          |
|  Version:    2.0                                          |
|  Type:       Full Manifest                                |
|  Routes:     4 sections, 12 pages, 1 fragment reference   |
|  Components: 87 instances (22 unique aliases)             |
|  Plugins:    2 (1 URL, 1 setting)                         |
+----------------------------------------------------------+
|  CRITICAL: 0  |  HIGH: 2  |  MEDIUM: 3  |  LOW: 1        |
+----------------------------------------------------------+
```

### Issue Detail Format

Each issue is reported with:

```
<SEVERITY> [<RULE_ID>] <description>
  at <json-path>
  > <suggested fix or context>
```

### Example Issues

```
CRITICAL [S01] Missing required field "manifestVersion"
  at (root)
  > Add "manifestVersion": "2.0" to the manifest root

HIGH [C01] Unknown component alias "fc.card.detail" at routes[0].pages[2].descendants[0]
  > Did you mean "fc.card.attribute"? Check ComponentRegistry for registered aliases.

HIGH [P01] fc.list missing required "attributes" prop at routes[1].pages[0].descendants[0]
  > Add "attributes": [...] array with column definitions: { "label": "...", "template": "{{...}}" }

HIGH [P06] fc.conditional missing required "value" and "matches" props at routes[0].pages[1].descendants[3]
  > Add "value": "{{templateExpression}}" and "matches": "expectedValue" to props

MEDIUM [D02] Invalid dataSource path "orders..edges" (double dot) at routes[0].pages[0].descendants[1]
  > Fix path to "orders.edges" -- remove duplicate dot separator

MEDIUM [T01] Unbalanced template delimiters "{{ref}" at routes[0].pages[1].descendants[0].props.title
  > Fix closing delimiter: "{{ref}}"

MEDIUM [T03] i18n key "i18n:my custom label" contains spaces at routes[0].pages[0].descendants[0].props.title
  > Use dot-separated key format: "i18n:fc.app.myCustomLabel"

MEDIUM [T07] Plain English string "Order Details" in title at routes[0].pages[1].props.title
  > Use i18n prefix for multi-language support: "i18n:fc.om.orders.detail.title"

LOW [C06] Component nesting depth is 9 levels at routes[2].pages[0].descendants[0]...descendants[0]
  > Consider flattening the component tree. Deep nesting impacts rendering performance.

LOW [X03] Plugin src contains "localhost": "http://localhost:3001/bundle.js" at plugins[0]
  > Development URL detected. Replace with production CDN URL before deploying.
```

### Pass/Fail Summary

```
RESULT: FAIL (2 HIGH issues must be resolved before deployment)

Fix the HIGH and CRITICAL issues above, then re-run validation.
MEDIUM and LOW issues are recommendations (enforce with --strict).
```

Or on success:

```
RESULT: PASS (0 CRITICAL, 0 HIGH issues)

Manifest is structurally valid and ready for deployment.
3 MEDIUM and 1 LOW recommendations noted above.

Next steps:
  - Deploy manifest as a Fluent setting via /fluent-settings
  - Or run /fluent-mystique-analyze for deeper usage analysis
```

---

## Fix Suggestions (--fix-suggestions)

When enabled, each issue includes a concrete JSON patch suggestion:

```
HIGH [P01] fc.list missing required "attributes" prop
  at routes[0].pages[0].descendants[2]

  Suggested fix -- add to props:
  "attributes": [
    {
      "label": "Reference",
      "template": "{{node.ref}}"
    },
    {
      "label": "Status",
      "template": "{{node.status}}"
    }
  ]
```

```
HIGH [P06] fc.conditional missing required props
  at routes[1].pages[0].descendants[0]

  Suggested fix -- add to props:
  "value": "{{status}}",
  "matches": ["ACTIVE", "APPROVED"]
```

---

## Template String Reference

### Supported Handlebars Helpers

The Mystique template renderer supports these built-in helpers. **Only use documented helpers — guessing undocumented helpers produces broken manifests.**

**Comparison:** `eq`, `ne`, `gt`, `lt`, `gte`, `lte`
**Logic:** `and`, `or`, `not`, `if`, `unless`, `in`, `switch`, `hasRole`, `firstDefinedValue`, `anyMatch`
**Iteration:** `each`, `with`, `lookup`
**String:** `capitalise`/`capitalize`, `humanify`, `toLowerCase`, `toUpperCase`, `concat`, `stringify`, `substring`, `replace`, `split`, `join`, `includes`, `startsWith`, `endsWith`, `preserveWhitespace`, `partialStringMatch`, `decodeUrlParams`, `currentUrlParams`
**Placeholder:** `placeholderText`, `placeholderDate`, `placeholderArray`
**Date:** `dateFormat`, `dateStringFormatter`, `dateTimeFormatter`, `dateAdd`, `dateSubtract`, `dateRelative`, `dateFormatByLocale`
**Number:** `currency`, `formatNumber`, `add`, `subtract`, `multiply`, `divide`, `pow`, `math`
**Array:** `len`, `arraySum`, `length`, `pluralize`
**Special:** `barcode`, `json`, `i18n`

**CRITICAL: Always use `_locale='en'` for dates in query variables.** Without it, user locale may produce non-ASCII digits that break backend queries.

### Context Variables

| Expression | Purpose |
|-----------|---------|
| `{{activeUser.username}}` | Current user's username |
| `{{activeUser.firstName}}` | Current user's first name |
| `{{activeUser.timezone}}` | Current user's timezone |
| `{{activeContext.id}}` | Current context ID |
| `{{activeRetailer.id}}` | Current retailer ID |
| `{{activeRetailer.ref}}` | Current retailer ref |
| `{{activeLocation.id}}` | Current location ID |
| `{{params.id}}` | Route parameter access |

### Template Expression Patterns

```
{{fieldName}}                           -- simple field access
{{node.ref}}                            -- nested field access (list rows)
{{attributes.byName.X}}                 -- attribute by name shorthand
{{edges.length}}                        -- connection count
{{capitalise status}}                   -- helper with argument
{{currency node.totalPrice node.currency}}  -- helper with multiple args
{{dateFormat createdOn 'MMM DD, YYYY'}} -- date formatting
{{dateFormat (dateAdd day=-1) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' true _locale='en'}}  -- backend-safe date
{{eq status "ACTIVE"}}                  -- comparison (returns boolean)
{{i18n "fc.om.orders.title"}}           -- i18n helper form
i18n:fc.om.orders.title                 -- i18n prefix form (in string props)
{{barcode fulfilmentById.ref}}          -- barcode SVG generation
{{{currentUrlParams}}}                  -- triple-brace for unescaped output
```

---

## Validation Against Single Component (--component)

When `--component <alias>` is specified, the validator focuses on all instances of that component:

```
$ /fluent-mystique-validate manifest.json --component fc.list

Validating all fc.list instances in manifest.json...

Instance 1: routes[0].pages[0].descendants[0]
  PASS  [P01] attributes present (5 columns)
  PASS  [P02] defaultPageSize set (20)
  PASS  [P04] dataSource present ("orders")
  PASS  attributes have labels
  INFO  rowLink uses template: "/orders/{{node.id}}"

Instance 2: routes[1].pages[2].descendants[1].descendants[0]
  FAIL  [P01] attributes missing
  PASS  [P04] dataSource present ("fulfilments")
  WARN  [P02] defaultPageSize not set (will default to 10)

Summary: 2 instances found, 1 FAIL, 1 PASS
```

---

## Integration with Other Skills

### Post-Build Validation

After `/fluent-mystique-builder` creates or modifies a manifest:
1. Builder outputs `READY: <manifest-path>`
2. **Always run** `/fluent-mystique-validate <manifest-path>` as next step
3. If validation fails: `BLOCKED: <count> issues` -- return to builder for fixes
4. If validation passes: `READY: 0 CRITICAL, 0 HIGH` -- proceed to deployment

### Pre-Deploy Check Integration

`/fluent-pre-deploy-check` should invoke this validator if manifest artifacts are found in the deployment scope. This catches manifest issues before they reach a live environment.

### Handoff Signals

| Signal | Meaning |
|--------|---------|
| `READY: 0 CRITICAL, 0 HIGH` | Manifest passes validation, safe to deploy via `/fluent-settings` |
| `BLOCKED: N CRITICAL/HIGH issues` | Manifest has structural problems, return to `/fluent-mystique-builder` to fix |
| `REVIEW: validation-report.md` | Report generated for user review |
| `NEXT: /fluent-settings` | After passing validation, deploy manifest as a Fluent setting |
| `NEXT: /fluent-mystique-analyze` | Optional deeper analysis after validation passes |

### Deployment Guardrail Reminder (emit after PASS)

When validation passes and the manifest is ready for deployment, always remind:
- **Setting metadata**: `valueType: "JSON"`, `context: "ACCOUNT"`, `contextId: 0` — non-negotiable for ALL manifest settings
- **lobValue**: Must be passed as a raw JSON object, NEVER `JSON.stringify()`'d — double-encoding causes "invalid route" errors and sidebar sections disappear silently
- **Backup**: Existing setting MUST be backed up before overwriting
- **Approval**: User MUST explicitly approve before any setting is created or updated
- **MCP setting.upsert**: If using MCP `setting.upsert`, pass `valueType: "JSON"` explicitly — without it, defaults to LOB which breaks manifests. Preferred: CLI `fluent module install --include settings` or GraphQL mutations.
- **Fetching manifests**: Use MCP `setting.get` with `outputFile` to download existing manifests for backup or validation input

### Error Format

Report errors following the cross-skill convention:

- **Skill:** fluent-mystique-validate
- **Phase:** Schema / Component / Data / Template / Props / CrossManifest
- **Severity:** CRITICAL / HIGH / MEDIUM / LOW
- **Recovery:** Specific fix instructions with JSON path

---

## Common Manifest Mistakes & Fixes

These are the most frequently encountered manifest configuration errors, distilled from production troubleshooting across Fluent Commerce implementations. Use this table as a quick-reference when investigating validation failures or runtime rendering issues.

| Mistake | Symptom | Fix |
|---------|---------|-----|
| Template missing `{{}}` braces | Shows literal text like "order.ref" instead of the value | Wrap in Handlebars delimiters: `{{order.ref}}` |
| Condition as string `"false"` | Component always renders (any non-empty string is truthy) | Use a Handlebars expression: `{{and field1 field2}}` |
| Link without condition | Renders broken/empty link when target entity is null | Add `"condition": "{{and field}}"` to guard the link |
| Icon missing library prefix | Icon does not render (silent failure) | Use prefixed name: `MdCheckCircle` (Material Design) or `FaCheck` (Font Awesome) |
| Date query without locale | Localized digits (Arabic, Thai) break backend date parsing | Add `_locale='en'` and UTC flag: `{{dateFormat (dateAdd day=-1) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' true _locale='en'}}` |
| Invalid GraphQL field in page query | Entire page data returns null (one bad field poisons the whole query) | Use `graphql.generateFull` to get the valid schema, then remove fields you do not need. Never add fields by guessing. |
| Missing `me` query on action pages | Console warning "Error building user action lookups", action buttons may not render | Add `me { username primaryRetailer { id } roles { role { name } } }` to the page query |
| Using `value` instead of `lobValue` | Manifest JSON silently truncated at 255 characters, pages break unpredictably | Always use `lobValue` for manifest settings. The `value` field has a 255 char limit. |
| Attributes accessed by array index | Fragile binding that breaks when attribute order changes | Use the `byName` shorthand: `{{entity.attributes.byName.fieldName}}` |
| Double-encoded lobValue | "Manifest contains an invalid route" error, sidebar sections disappear silently | Pass raw JSON object as lobValue. Never call `JSON.stringify()` before passing to GraphQL variables or deploy scripts. |
| `context: "RETAILER"` on manifest setting | Mystique cannot find the manifest (looks in ACCOUNT context) | Always use `context: "ACCOUNT"`, `contextId: 0` for all manifest settings |
| Empty `modules: []` in workflow user actions | Action buttons invisible in all apps (OMS App, Store App) | Include at least one target app module: `["adminconsole"]` (the OMS App's module alias, legacy alias) for OMS App, `["store"]` for Store App |

---

## Validation Checklist for Detail Pages

Before deploying any manifest detail page (entity view with cards, lists, and user actions), walk through this checklist. Each item maps to a common runtime failure mode.

### Query Validation

- [ ] **GraphQL query is schema-valid** -- Run `graphql.validate` or `graphql.execute` (dry run) against the page query. One invalid field nullifies the entire query response.
- [ ] **`me` query included** -- If the page has `"actions": true` (user action buttons), the query MUST include `me { username primaryRetailer { id } roles { role { name } } }`. Without it, the Transition API call fails and buttons do not render.
- [ ] **Variables bind correctly** -- Every `{{params.id}}` or `{{activeContext.id}}` in `data.variables` resolves at runtime. Check that route parameter names match variable references.

### Data Binding

- [ ] **`dataSource` paths resolve** -- Each component's `dataSource` must point to a valid path in the page query response (e.g., `orderById`, `orderById.items`, `orderById.customer`). Mismatched paths produce empty components with no error.
- [ ] **Nested connections use correct path** -- For lists binding to edges, use `dataSource: "orderById.items"` (the framework handles `.edges[].node` automatically). Do NOT include `.edges` in the dataSource path.

### Links and Navigation

- [ ] **Link format is correct** -- Internal links use hash routing: `"link": "#/order/{{id}}"`. Missing `#` prefix breaks navigation.
- [ ] **Links have conditions** -- Every `"link"` attribute should have a `"condition": "{{and targetId}}"` to prevent broken links when the target entity is null.

### Conditions and Visibility

- [ ] **Conditions use Handlebars, not strings** -- `"condition": "{{and field}}"` evaluates correctly. `"condition": "false"` (string) always evaluates as truthy.
- [ ] **`fc.conditional` has both `value` and `matches`** -- Missing either prop causes the conditional block to not render or to always render.

### Formatting

- [ ] **Currency values use `currency` helper** -- `{{currency totalPrice currency}}` with both amount and currency code arguments.
- [ ] **Dates use `dateFormat` helper** -- `{{dateFormat createdOn}}` for display. For query variables: include `_locale='en'` and UTC flag.
- [ ] **i18n keys follow convention** -- All user-visible labels use `i18n:fc.<app>.<section>.<key>` format. No hardcoded English strings.

### Icons and Styling

- [ ] **Icon names include library prefix** -- `MdCheckCircle` (Material Design), `FaCheck` (Font Awesome). Plain names like `CheckCircle` silently fail.
- [ ] **Width values are valid CardWidth** -- `"quarter"`, `"third"`, `"half"`, `"two-thirds"`, `"full"`, or numeric 1-12.

### Settings Deployment

- [ ] **`valueType: "JSON"`** -- Non-negotiable for all manifest settings.
- [ ] **`context: "ACCOUNT"`, `contextId: 0`** -- Non-negotiable for all manifest settings.
- [ ] **Body in `lobValue`** -- Never use `value` (255 char truncation).
- [ ] **Backup taken** -- Existing setting saved to `accounts/<PROFILE>/manifests/backups/` before any update.
- [ ] **User approval obtained** -- Never deploy without explicit user confirmation.

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