@getrheo/rheo-skill 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +76 -0
  2. package/package.json +51 -0
  3. package/rheo/SKILL.md +32 -0
  4. package/rheo/rheo-best-practices/SKILL.md +46 -0
  5. package/rheo/rheo-best-practices/examples/react-native-install-snippet.md +23 -0
  6. package/rheo/rheo-best-practices/examples/swiftui-install-snippet.md +20 -0
  7. package/rheo/rheo-best-practices/references/implement-workflow.md +27 -0
  8. package/rheo/rheo-best-practices/references/integrations.md +51 -0
  9. package/rheo/rheo-best-practices/references/react-native-bare.md +36 -0
  10. package/rheo/rheo-best-practices/references/react-native-expo.md +49 -0
  11. package/rheo/rheo-best-practices/references/swiftui.md +52 -0
  12. package/rheo/rheo-best-practices/references/troubleshooting.md +57 -0
  13. package/rheo/rheo-flow-import/SKILL.md +68 -0
  14. package/rheo/rheo-flow-import/examples/branching-onboarding.manifest.json +155 -0
  15. package/rheo/rheo-flow-import/examples/flow-spec.example.json +56 -0
  16. package/rheo/rheo-flow-import/examples/linear-onboarding.manifest.json +104 -0
  17. package/rheo/rheo-flow-import/examples/revenuecat-paywall.manifest.json +154 -0
  18. package/rheo/rheo-flow-import/references/animation-import.md +79 -0
  19. package/rheo/rheo-flow-import/references/capabilities.md +107 -0
  20. package/rheo/rheo-flow-import/references/carousel-import.md +64 -0
  21. package/rheo/rheo-flow-import/references/flow-spec.md +214 -0
  22. package/rheo/rheo-flow-import/references/font-import.md +67 -0
  23. package/rheo/rheo-flow-import/references/import-workflow.md +240 -0
  24. package/rheo/rheo-flow-import/references/layer-schema-pitfalls.md +179 -0
  25. package/rheo/rheo-flow-import/references/localization-import.md +128 -0
  26. package/rheo/rheo-flow-import/references/manifest-agent-profile-fallback.md +74 -0
  27. package/rheo/rheo-flow-import/references/manifest-rules.md +197 -0
  28. package/rheo/rheo-flow-import/references/publish-gates.md +91 -0
  29. package/rheo/rheo-flow-import/references/react-native-source-patterns.md +99 -0
  30. package/rheo/rheo-flow-import/references/swiftui-source-patterns.md +99 -0
  31. package/rheo/rheo-flow-import/scripts/audit-import.mjs +4 -0
  32. package/rheo/rheo-flow-import/scripts/audit-publish-manifest.mjs +4 -0
  33. package/rheo/rheo-flow-import/scripts/fetch-profile.mjs +4 -0
  34. package/rheo/rheo-flow-import/scripts/lib/rheo-cli.mjs +12753 -0
  35. package/rheo/rheo-flow-import/scripts/normalize-manifest.mjs +4 -0
  36. package/rheo/rheo-flow-import/scripts/print-manifest-summary.mjs +4 -0
  37. package/rheo/rheo-flow-import/scripts/scaffold-manifest.mjs +4 -0
  38. package/rheo/rheo-flow-import/scripts/validate-manifest.mjs +4 -0
  39. package/src/index.ts +32 -0
@@ -0,0 +1,179 @@
1
+ # Layer Schema Pitfalls
2
+
3
+ Read this before writing `rheo-import.manifest.json`. Dashboard import runs full Zod validation; layers that look "close enough" often fail with `invalid_union` at paths like `regions.header.children[0]` or `regions.body.children[1]`.
4
+
5
+ Run **`node scripts/validate-manifest.mjs ./rheo-import.manifest.json`** before zipping. Exit code must be **0**.
6
+
7
+ ## Layer IDs vs media placeholder UUIDs
8
+
9
+ Two different id systems — do not mix them up.
10
+
11
+ | Use case | Format | Example |
12
+ |----------|--------|---------|
13
+ | Screen / layer / decision ids | `scr_*`, `lyr_*`, `dec_*`, `surf_*` | `lyr_scr_gender_back` |
14
+ | `media.mediaAssetId` in manifest | UUID placeholder | `00000000-0000-0000-0000-000000000101` |
15
+ | `optionBindings[].rootLayerId` | **`lyr_*` layer id** (not UUID) | `lyr_scr_gender_opt_m` |
16
+
17
+ **Invalid:** `"id": "00000000-0000-0000-0000-000000000101"` on a layer, `"id": "back_btn"`, `"id": "headerBack"`, random strings with punctuation.
18
+
19
+ Every layer and nested child needs its own unique `lyr_*` id.
20
+
21
+ ## Button and back_button variants
22
+
23
+ Rheo accepts **only** these `variant` values on `button` and `back_button`:
24
+
25
+ `primary` | `secondary` | `ghost` | `destructive`
26
+
27
+ **Invalid variants** (common from Tailwind / shadcn / Material — map them, do not copy literally):
28
+
29
+ | Source / habit | Map to |
30
+ |----------------|--------|
31
+ | `outline`, `bordered`, `tertiary` | `secondary` or `ghost` |
32
+ | `text`, `link`, `plain` | `ghost` |
33
+ | `default`, `filled` | `primary` |
34
+ | `danger`, `error` | `destructive` |
35
+
36
+ `variant` is **required** on every `button` and `back_button`.
37
+
38
+ ## back_button (header chrome)
39
+
40
+ Use in `regions.header` for back/close controls above content.
41
+
42
+ **Do:**
43
+
44
+ - `kind: "back_button"` (not `"button"` with a custom back action)
45
+ - `variant` from the allowed enum above
46
+ - `children` array with nested `icon` and/or `text`
47
+ - Unique `lyr_*` ids on the back button and every child
48
+
49
+ **Do not:**
50
+
51
+ - Omit `children`
52
+ - Add `action` (back navigation is built-in)
53
+ - Use invalid `variant` values
54
+ - Use non-`lyr_*` layer ids
55
+
56
+ Chevron-only header control:
57
+
58
+ ```json
59
+ {
60
+ "id": "lyr_scr_approach_back",
61
+ "kind": "back_button",
62
+ "variant": "ghost",
63
+ "children": [
64
+ {
65
+ "id": "lyr_scr_approach_back_icon",
66
+ "kind": "icon",
67
+ "family": "ionicons",
68
+ "iconName": "chevron-back-outline",
69
+ "style": { "width": 20, "height": 20, "color": "#0A0A0A" }
70
+ }
71
+ ]
72
+ }
73
+ ```
74
+
75
+ Icons: **`family` must be `"ionicons"`** with a valid `iconName` (e.g. `chevron-back-outline`, `close-outline`). No SF Symbols, no custom icon font names on `icon` layers.
76
+
77
+ ## single_choice / multiple_choice
78
+
79
+ Choice inputs are **not** generic option lists. Never emit `"options": [...]`, `"choices": [...]`, `"kind": "choice"`, or `"kind": "radio"`.
80
+
81
+ **Required on every choice layer:**
82
+
83
+ | Field | Rule |
84
+ |-------|------|
85
+ | `fieldKey` | snake_case, starts with lowercase letter (`gender`, `language`, `age_range`) |
86
+ | `children` | Array of **≥2** option `stack` layers, each with its own `lyr_*` id |
87
+ | `optionBindings` | One entry per option: `{ "optionId": "<stable id>", "rootLayerId": "<child stack lyr_* id>" }` |
88
+ | `branching` | Always present: `{ "enabled": false, "conditions": [] }` unless source has real branch rules |
89
+
90
+ `optionBindings.length` must equal `children.length`. Every `rootLayerId` must match a direct child stack's `id`.
91
+
92
+ Full minimal example:
93
+
94
+ ```json
95
+ {
96
+ "id": "lyr_scr_gender_choice",
97
+ "kind": "single_choice",
98
+ "fieldKey": "gender",
99
+ "children": [
100
+ {
101
+ "id": "lyr_scr_gender_opt_m",
102
+ "kind": "stack",
103
+ "direction": "horizontal",
104
+ "align": "center",
105
+ "gap": 8,
106
+ "style": {
107
+ "border": { "width": 1, "color": "#E5E7EB" },
108
+ "background": "#F9FAFB",
109
+ "radius": 12,
110
+ "padding": 16
111
+ },
112
+ "selectedStyle": {
113
+ "border": { "width": 1, "color": "#6D5DF6" },
114
+ "background": "#6D5DF610"
115
+ },
116
+ "children": [
117
+ {
118
+ "id": "lyr_scr_gender_opt_m_text",
119
+ "kind": "text",
120
+ "text": { "default": "Male" },
121
+ "style": { "color": "#0A0A0A" }
122
+ }
123
+ ]
124
+ },
125
+ {
126
+ "id": "lyr_scr_gender_opt_f",
127
+ "kind": "stack",
128
+ "direction": "horizontal",
129
+ "align": "center",
130
+ "gap": 8,
131
+ "style": {
132
+ "border": { "width": 1, "color": "#E5E7EB" },
133
+ "background": "#F9FAFB",
134
+ "radius": 12,
135
+ "padding": 16
136
+ },
137
+ "selectedStyle": {
138
+ "border": { "width": 1, "color": "#6D5DF6" },
139
+ "background": "#6D5DF610"
140
+ },
141
+ "children": [
142
+ {
143
+ "id": "lyr_scr_gender_opt_f_text",
144
+ "kind": "text",
145
+ "text": { "default": "Female" },
146
+ "style": { "color": "#0A0A0A" }
147
+ }
148
+ ]
149
+ }
150
+ ],
151
+ "optionBindings": [
152
+ { "optionId": "male", "rootLayerId": "lyr_scr_gender_opt_m" },
153
+ { "optionId": "female", "rootLayerId": "lyr_scr_gender_opt_f" }
154
+ ],
155
+ "branching": {
156
+ "enabled": false,
157
+ "conditions": []
158
+ }
159
+ }
160
+ ```
161
+
162
+ ## Diagnosing `invalid_union` in API logs
163
+
164
+ When dashboard import returns `invalid seeded flow manifest` with `code: "invalid_union"`:
165
+
166
+ | Path pattern | Inspect |
167
+ |--------------|---------|
168
+ | `regions.header.children[0]` | First header layer — usually `back_button`: check `variant`, `children`, `lyr_*` ids, nested `icon.family` |
169
+ | `regions.body.children[1]` | Often `single_choice` on question screens: check `fieldKey`, `optionBindings`, `branching`, option stack ids |
170
+ | Any `children[N]` | Open that layer object; compare to templates in this doc |
171
+
172
+ Extract and validate locally:
173
+
174
+ ```bash
175
+ unzip -p rheo-import.zip rheo-import.manifest.json > /tmp/rheo-import.manifest.json
176
+ node scripts/validate-manifest.mjs /tmp/rheo-import.manifest.json
177
+ ```
178
+
179
+ Fix every reported path, re-run until exit 0, then rebuild the ZIP.
@@ -0,0 +1,128 @@
1
+ # Localization / i18n import
2
+
3
+ When the host app uses i18n (i18next, react-intl, Lingui, i18n-js, expo-localization + JSON, etc.), Rheo import must still ship **human-readable copy**, not translation keys.
4
+
5
+ ## Hard rule
6
+
7
+ 1. Detect the app's **default / fallback locale** (e.g. `en` from `fallbackLng`, `defaultLocale`, or `locales/en.json`).
8
+ 2. Set `manifest.defaultLocale` to that locale (and include it in `manifest.locales`).
9
+ 3. For every text layer, button label, placeholder, and choice option: set `text.default` to the **resolved string in that locale**.
10
+ 4. **Never** put raw keys in `text.default` (`onboarding.welcome.title`, `COMMON_CONTINUE`, `common:button.next`).
11
+
12
+ Optional: after defaults are correct, add `text.translations` for other locales from the same source JSON files.
13
+
14
+ ## Wrong vs right
15
+
16
+ | Source (i18next) | Wrong manifest | Right manifest |
17
+ |------------------|----------------|----------------|
18
+ | `t('onboarding.welcome.title')` | `{ "default": "onboarding.welcome.title" }` | `{ "default": "Welcome to Rheo" }` |
19
+ | `formatMessage({ id: 'cta.continue' })` | `{ "default": "cta.continue" }` | `{ "default": "Continue" }` |
20
+
21
+ ## How to resolve copy
22
+
23
+ 1. Run `audit-import` — check **Localization / i18n Evidence** for detected library, default locale, and sample `t("key") -> "value"` rows.
24
+ 2. Open the default locale file (`locales/en.json`, `translations/en.json`, etc.).
25
+ 3. For each key used on traced screens, walk nested JSON (`onboarding.welcome.title` → `en.onboarding.welcome.title`).
26
+ 4. If the key uses a namespace (`common:save`), load from that namespace object in the locale file.
27
+ 5. Use screenshots as a sanity check when JSON is incomplete.
28
+
29
+ ## TypeScript locale modules (`export const en`)
30
+
31
+ Many Expo/React Native apps keep copy in `.ts` files, not JSON:
32
+
33
+ ```ts
34
+ // i18n/locales/en.ts
35
+ export const en = {
36
+ onboarding: { stepWelcome: { title: 'Track Your Drinks' } },
37
+ };
38
+ ```
39
+
40
+ When generating a manifest with a Node script (`tsx`, `ts-node`, etc.):
41
+
42
+ 1. **Use the app's i18n library** — e.g. `i18n-js` with the same `defaultLocale`, `enableFallback`, and locale objects as `LocaleContext` / `i18n/index`. Do not hand-roll a dot-path walker unless you have verified it matches nested keys and interpolation.
43
+ 2. **Resolve every `t('key')` before writing the manifest** — map each traced key through that library (or an equivalent) and put the return value in `text.default`.
44
+ 3. **Optional `text.translations`** — load `de`, `fr`, etc. from sibling locale modules and call `t` per locale for the same keys.
45
+
46
+ ## Script import pitfall (`tsx` + `scripts/`)
47
+
48
+ If the generator lives under `scripts/` and imports locales with a **relative path** (`../i18n/locales/en.ts`), `tsx` may not expose named exports (`en`, `de`, …). You often get only:
49
+
50
+ ```ts
51
+ { default: { en: { /* locale tree */ } } }
52
+ ```
53
+
54
+ Symptoms:
55
+
56
+ - `import { en } from '../i18n/locales/en.ts'` → `does not provide an export named 'en'`
57
+ - `import * as locales from '../i18n/locales/en.ts'` → `locales.en` is `undefined`; only `locales.default` exists
58
+ - A custom `t()` then returns the **key unchanged** → manifest full of `onboarding.stepWelcome.title` strings
59
+
60
+ **Fix (pick one):**
61
+
62
+ ```ts
63
+ // A) Default import + unwrap (works from scripts/)
64
+ import enModule from '../i18n/locales/en.ts';
65
+
66
+ const localeFromModule = (mod: unknown, code: string) => {
67
+ const root = (mod as { default?: Record<string, unknown> }).default ?? mod;
68
+ const nested = (root as Record<string, unknown>)[code];
69
+ return (nested ?? root) as Record<string, unknown>;
70
+ };
71
+
72
+ const en = localeFromModule(enModule, 'en');
73
+ ```
74
+
75
+ ```ts
76
+ // B) Import from repo root in a one-liner smoke test (named exports often work here)
77
+ import { en } from './i18n/locales/en.ts';
78
+ ```
79
+
80
+ ```ts
81
+ // C) Absolute file URL from repo root (stable for async generators)
82
+ import { pathToFileURL } from 'node:url';
83
+ import { join } from 'node:path';
84
+
85
+ const mod = await import(pathToFileURL(join(ROOT, 'i18n/locales/en.ts')).href);
86
+ const en = mod.en ?? mod.default?.en ?? mod.default;
87
+ ```
88
+
89
+ **Smoke test before zipping** — must pass with zero matches:
90
+
91
+ ```bash
92
+ grep -E '"default": "(onboarding|common|auth)\.' rheo-import.manifest.json
93
+ # or search the manifest inside the zip the same way
94
+ ```
95
+
96
+ Also log one known key after loading locales, e.g. `onboarding.stepWelcome.trackDrinks.title` → `Track Your Drinks`. If the log prints the key, abort generation and fix imports — do not ship the bundle.
97
+
98
+ ## Manifest shape
99
+
100
+ ```json
101
+ {
102
+ "defaultLocale": "en",
103
+ "locales": ["en"],
104
+ "screens": [
105
+ {
106
+ "regions": {
107
+ "body": {
108
+ "kind": "text",
109
+ "text": {
110
+ "default": "Welcome",
111
+ "translations": { "fr": "Bienvenue" }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ `default` is required and must be the default-locale string. `translations` is optional.
121
+
122
+ ## Intake
123
+
124
+ When audit reports localization, confirm with the user which locale is the app fallback if ambiguous. Still **always** resolve strings from that locale — do not import keys because other locales exist.
125
+
126
+ ## Validation
127
+
128
+ `validate-manifest.mjs` warns when `text.default` values look like i18n keys. Fix before zipping for dashboard import.
@@ -0,0 +1,74 @@
1
+ # Rheo Manifest Agent Profile
2
+
3
+ Profile version: bundled-0.1.0
4
+ Manifest schema version: 7
5
+ Audience: AI agents generating Rheo `FlowManifest` JSON.
6
+
7
+ This bundled profile is fallback guidance. Prefer the latest raw docs profile at `/docs/md/developer-guide/agent-manifest-profile` when available.
8
+
9
+ ## Audit-First Import
10
+
11
+ Before generating a manifest, run the bundled audit command:
12
+
13
+ ```bash
14
+ node scripts/audit-import.mjs --entry app/onboarding.tsx --out rheo-import.audit.md
15
+ ```
16
+
17
+ Use the audit report for header/footer, style tokens, screen backgrounds, carousels, layout, custom fonts, choice selected states, assets, Lottie, motion, and follow-up questions. If the audit cannot run, explain why and manually cover the same sections.
18
+
19
+ **Mandatory intake (blocking):** Ask and record answers before generating:
20
+
21
+ 1. Flow entry file/route/coordinator
22
+ 2. Business purpose of the flow
23
+ 3. Visual fidelity vs editable structure
24
+ 4. Source code vs screenshots when they disagree
25
+ 5. Native-only steps vs Rheo-approximated steps
26
+ 6. Match motion from the codebase (may differ slightly from Rheo presets)? (yes/no)
27
+
28
+ When intake Q6 is yes and the plan supports animations, read [animation-import.md](animation-import.md) and use `--suggest-animations rheo-import.animations.json` on the audit command.
29
+
30
+ ## Output Contract
31
+
32
+ Generate one raw JSON `FlowManifest`, normally saved as `rheo-import.manifest.json`. When media assets are used, create `rheo-import.zip` with `rheo-import.manifest.json`, `rheo-import.assets.json`, and files under `assets/`. Asset bundling is mandatory for local images, Lottie JSON, and videos; JSON-only output is acceptable only after confirming no local assets were found. Do not include comments, Markdown, secrets, or source code.
33
+
34
+ ## Required Top-Level Fields
35
+
36
+ `flowId`, `schemaVersion`, `version`, `defaultLocale`, `locales`, `entryScreenId`, `theme`, `screens`, `decisionNodes`, `externalSurfaceNodes`, `sdkAttributeKeys`.
37
+
38
+ ## IDs
39
+
40
+ Screens use `scr_*`, layers use `lyr_*`, decisions use `dec_*`, and external surfaces use `surf_*`. UUID placeholders are **only** for `media.mediaAssetId` and font sidecars — never as layer ids.
41
+
42
+ Before zipping, read [layer-schema-pitfalls.md](layer-schema-pitfalls.md) and run validate until exit 0.
43
+
44
+ ## Layer Kinds
45
+
46
+ Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`.
47
+
48
+ ## Rules
49
+
50
+ - Text copy goes in `text.default`.
51
+ - Button labels are nested text layers.
52
+ - `button` / `back_button` `variant` must be `primary`, `secondary`, `ghost`, or `destructive` (map source `outline`/`text`/`link` — do not copy). `back_button` has no `action`.
53
+ - `single_choice` / `multiple_choice` require `fieldKey`, `children`, `optionBindings`, and `branching` — never `"options"` arrays.
54
+ - Use `regions.header` for top chrome such as back buttons and progress, `regions.body` for main content, and `regions.footer` for sticky bottom CTAs.
55
+ - Inspect theme/style/token files, StyleSheet, and Tailwind classes before using black-and-white defaults.
56
+ - Set `style.color` on text for dark/saturated screen backgrounds.
57
+ - Gradients: `screen.containerStyle.backgroundFill.color` as `linear-gradient(...)` CSS when `kind` is `color`.
58
+ - In-screen pagers → `kind: "carousel"` with one slide per page; swipe-only; bundle every slide asset. See carousel-import.md in references.
59
+ - Center images with parent stack `align: "center"`; map card borders/shadows to wrapping stacks.
60
+ - Custom fonts: bundle files in `rheo-import.fonts.json` only (never `rheo-import.assets.json`), `manifest.theme.fontFamily`. See `font-import.md`.
61
+ - Choice options: each option stack uses `style` (default) and `selectedStyle` (selected).
62
+ - Publish gates: explicit `style.color` on all text (including button labels), Continue on manual-submit screens, valid entry/completion path. Run `scripts/audit-publish-manifest.mjs` before finishing.
63
+ - Black-and-white fallback is acceptable only when the audit finds no style/token evidence and the user confirms no theme source.
64
+ - Use at most one input layer kind per screen.
65
+ - Non-reserved `sdk.*` decision keys must be listed in `sdkAttributeKeys`.
66
+ - RevenueCat paywalls are external surface nodes and always need `fallback`.
67
+ - Emit complete graph edges for imported flows.
68
+ - Use placeholder UUID media ids and `rheo-import.assets.json`; never put file paths directly in `mediaAssetId`.
69
+ - Do not silently drop media layers. If a traced asset cannot be copied, report the missing file and do not call the import complete.
70
+ - Ask targeted follow-up questions when the audit finds meaningful ambiguity, such as conflicting token files or unclear gradient fidelity.
71
+
72
+ ## Validation
73
+
74
+ Run `node scripts/validate-manifest.mjs ./rheo-import.manifest.json` before presenting the import as ready.
@@ -0,0 +1,197 @@
1
+ # Manifest Rules
2
+
3
+ Use the fetched Manifest Agent Profile as the current source of truth. These are stable baseline rules.
4
+
5
+ ## Top Level
6
+
7
+ Required fields: `flowId`, `schemaVersion`, `version`, `defaultLocale`, `locales`, `entryScreenId`, `theme`, `screens`, `decisionNodes`, `externalSurfaceNodes`, `sdkAttributeKeys`.
8
+
9
+ Use a placeholder UUID only when generating outside the dashboard. The dashboard import replaces `flowId` with the created flow id.
10
+
11
+ ## IDs
12
+
13
+ - Screens: `scr_*`
14
+ - Layers: `lyr_*`
15
+ - Decisions: `dec_*`
16
+ - External surfaces: `surf_*`
17
+
18
+ Use readable ids, for example `scr_welcome`, `lyr_welcome_title`, `surf_paywall`.
19
+
20
+ **UUID placeholders** (`00000000-0000-0000-0000-000000000101`) are **only** for `media.mediaAssetId` in the manifest and font entries in `rheo-import.fonts.json`. Never use UUIDs as layer ids or as `optionBindings[].rootLayerId`.
21
+
22
+ See [layer-schema-pitfalls.md](layer-schema-pitfalls.md) for common id mistakes that cause `invalid_union` validation failures.
23
+
24
+ ## Layer Kinds
25
+
26
+ Allowed kinds: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`.
27
+
28
+ ## Regions
29
+
30
+ Screens support `regions.header`, required `regions.body`, and optional `regions.footer`.
31
+
32
+ - Use `header` for top chrome: back buttons, close buttons, title chrome, step indicators, and progress bars.
33
+ - Use `body` for main content and scrollable content.
34
+ - Use `footer` for sticky bottom CTAs and legal/checklist rows that sit below content.
35
+ - Use `back_button` and `progress` layers instead of rebuilding obvious navigation chrome as generic text/buttons.
36
+
37
+ ## Style Tokens
38
+
39
+ Before generating a plain black-and-white manifest, inspect the repo for tokens:
40
+
41
+ - Tailwind/NativeWind config and global CSS.
42
+ - theme files and design-token JSON/TS files.
43
+ - shared `Button`, `Text`, `Typography`, `Screen`, `Header`, and `Footer` primitives.
44
+ - React Native `StyleSheet.create` constants.
45
+ - SwiftUI `Color`, `Font`, and modifier extensions.
46
+
47
+ Map clear brand values into `manifest.theme` and layer styles. Set `style.color` on text for dark/saturated screen backgrounds.
48
+
49
+ ## Screen Backgrounds And Gradients
50
+
51
+ - Use `screen.containerStyle.backgroundFill` for per-screen fills.
52
+ - Gradients: `{ "kind": "color", "color": "linear-gradient(180deg, #hex1 0%, #hex2 100%)" }`.
53
+ - Shared shell gradients apply to all default screens unless a screen overrides with a solid color.
54
+
55
+ ## Carousels
56
+
57
+ - In-screen pagers (`infoSteps`, horizontal pager, dot indicators) → `kind: "carousel"` with one slide stack per page.
58
+ - Do not collapse multi-slide routes to one static screen.
59
+ - `pageControl` is optional dot chrome only. Carousels are swipe-only (no pager buttons).
60
+ - Do not duplicate paging with `regions.footer` Continue when the source footer only increments pager index. See [carousel-import.md](carousel-import.md).
61
+
62
+ ## Layout
63
+
64
+ - Center images: parent stack `align: "center"`.
65
+ - Card chrome: wrapping stacks with `border`, `shadow`, `background`, `radius`, `padding`.
66
+
67
+ ## Explicit Sizing
68
+
69
+ Set `style.width` and `style.height` on every layer — do not omit them. `width` accepts `"full"`, `"auto"`, a fraction (`"1/2"`, `"1/3"`, `"2/3"`, `"1/4"`, `"3/4"`), or a pixel number; `height` accepts `"fill"`, `"auto"`, or a pixel number. Use these per-kind defaults unless the design requires otherwise:
70
+
71
+ | Layer kinds | `width` | `height` |
72
+ |-------------|---------|----------|
73
+ | `stack`, `text_input`, `scale_input`, `oauth_login`, `email_password_auth`, `email_password_field`, `progress`, `loader` | `"full"` | `"fill"` |
74
+ | `button`, `back_button`, `oauth_provider`, `email_password_submit`, `checkbox`, `single_choice`, `multiple_choice` | `"full"` | `"auto"` |
75
+ | `text`, `counter`, `icon`, `hyperlink` | `"auto"` | `"auto"` |
76
+ | `image`, `lottie`, `video` | `"full"` | number (e.g. `160`) |
77
+ | `carousel` | — (no outer `style` sizing) | — |
78
+
79
+ Dashboard import backfills these defaults automatically, but emitting them explicitly produces higher-fidelity first drafts.
80
+
81
+ ## Container Layers (Required `children`)
82
+
83
+ Container layers **must always include a `children` array** (use `[]` only when the schema allows an empty container — never omit the key). Omitting `children` produces JSON that may parse but **crashes dashboard import** when motion is stripped (Indie plan) and fails schema validation.
84
+
85
+ | Layer kind | Required child field | Label / content rule |
86
+ |------------|---------------------|----------------------|
87
+ | `stack` | `children: []` minimum | Nested layers |
88
+ | `button` | `children` | **Required** nested `text` for the label (optional leading `icon`) |
89
+ | `back_button` | `children` | **Required** nested `text` and/or `icon` — never emit header back chrome without `children` |
90
+ | `hyperlink` | `children` | **Required** nested `text` for link copy |
91
+ | `carousel` | `slides` | One stack per slide |
92
+ | `single_choice` / `multiple_choice` | `children` | One option `stack` per choice |
93
+ | `oauth_login` | `children` | Provider rows / chrome |
94
+ | `email_password_auth` / `email_password_submit` | `children` | Field rows and submit label |
95
+ | `oauth_provider` (`variant: "custom"`) | `children` | Custom provider row content |
96
+
97
+ ### Anti-pattern (invalid — do not emit)
98
+
99
+ ```json
100
+ {
101
+ "id": "lyr_header_back",
102
+ "kind": "back_button",
103
+ "variant": "ghost"
104
+ }
105
+ ```
106
+
107
+ ### Valid `back_button` (header chrome)
108
+
109
+ ```json
110
+ {
111
+ "id": "lyr_header_back",
112
+ "kind": "back_button",
113
+ "variant": "ghost",
114
+ "children": [
115
+ {
116
+ "id": "lyr_header_back_label",
117
+ "kind": "text",
118
+ "text": { "default": "Back" },
119
+ "style": { "color": "#0A0A0A" }
120
+ }
121
+ ]
122
+ }
123
+ ```
124
+
125
+ When source uses a chevron-only back control, still nest an `icon` child (and optional empty/minimal `text` if needed for hit area — prefer visible `icon` + `text` when source shows a label).
126
+
127
+ `scripts/normalize-manifest.mjs` repairs legacy `button.label` and `hyperlink.text` shapes only — it does **not** invent missing `back_button.children`. Fix those in the manifest before validation.
128
+
129
+ ## Text And Buttons
130
+
131
+ - Text uses `text: { "default": "..." }`.
132
+ - Do not use `label` on text layers or top-level `label` on `button` layers (use `children` instead).
133
+ - Button labels are nested text children — same for `back_button` and `hyperlink`.
134
+ - **`button` and `back_button` `variant`** must be one of: `primary`, `secondary`, `ghost`, `destructive`. Map source design-system names (`outline`, `text`, `link`, `default`) — do not copy them literally.
135
+ - **`back_button`** has no `action` field; navigation is built-in. Use in `regions.header` for back/close chrome.
136
+ - Prefer `continue`, `skip`, and `end_flow` actions for imported first drafts (on `button` only).
137
+ - **`request_app_review`** is allowed for human/explicit requests only (not default imports): empty action object, requires `screen.next.default`, single CTA after a positive moment.
138
+
139
+ ## Custom Fonts
140
+
141
+ - When source loads custom fonts (`Font.loadAsync`, `useFonts`, bundled `.ttf`/`.otf`), copy files into `assets/fonts/` and add `rheo-import.fonts.json` ([font-import.md](font-import.md)).
142
+ - Set `manifest.theme.fontFamily` to the primary family name string used in source.
143
+ - Each font style needs a stable placeholder UUID in `rheo-import.fonts.json`; dashboard import uploads files and merges families into app branding.
144
+ - **Never** list font files in `rheo-import.assets.json` — fonts are not `media.mediaAssetId` assets and must not use image MIME types.
145
+
146
+ ## Choice Inputs (single_choice / multiple_choice)
147
+
148
+ Each choice layer is a **structured input**, not a flat options list. Required fields: `fieldKey`, `children`, `optionBindings`, `branching`.
149
+
150
+ - Each selectable option is a child **`stack`** on the input layer with its own `lyr_*` id.
151
+ - **`optionBindings`**: one `{ optionId, rootLayerId }` per option; `rootLayerId` must equal the matching child stack's `id`.
152
+ - **`branching`**: always include `{ "enabled": false, "conditions": [] }` unless the source has real per-option navigation.
153
+ - **`fieldKey`**: snake_case (`gender`, `language`, `age_range`) — not PascalCase or camelCase.
154
+ - Never emit `"options"`, `"choices"`, `"kind": "choice"`, or `"kind": "radio"`.
155
+ - Map unselected chrome to `style` and selected chrome to `selectedStyle` on the same stack (border, background, padding, radius).
156
+ - Do not import choice screens with only default styling when source uses selected/unselected ternaries.
157
+
158
+ Full template: [layer-schema-pitfalls.md](layer-schema-pitfalls.md#single_choice--multiple_choice).
159
+
160
+ ## Inputs
161
+
162
+ - Use at most one input layer kind per screen.
163
+ - Use stable snake_case `fieldKey` values.
164
+ - Mark text input classification as `safe` or `sensitive`.
165
+
166
+ ## Decisions
167
+
168
+ - Use decisions for real source-code branches.
169
+ - Non-reserved `sdk.*` keys used in decisions must be listed in `sdkAttributeKeys`.
170
+ - Prefer source semantics over visual guessing.
171
+
172
+ ## External Surfaces
173
+
174
+ - RevenueCat paywalls become `externalSurfaceNodes` with provider `revenuecat`.
175
+ - Every external surface needs a `fallback`.
176
+ - Map known paywall outcomes to `purchase_completed`, `restore_completed`, `dismissed`, and `failed`.
177
+
178
+ ## Assets
179
+
180
+ For local images, Lottie JSON, or videos, use valid placeholder UUIDs in `media.mediaAssetId`, then include those files in `rheo-import.zip` with `rheo-import.assets.json`. Do not put file paths directly in `mediaAssetId`. Font binaries belong only in `rheo-import.fonts.json`.
181
+
182
+ ## Localization (i18n)
183
+
184
+ When source screens use i18n ([localization-import.md](localization-import.md)):
185
+
186
+ - Set `defaultLocale` and `locales` to match the app fallback (usually `en`).
187
+ - Every `text.default` must be the **resolved default-locale string**, not a translation key.
188
+ - Optional `text.translations` for other locales after defaults are correct.
189
+
190
+ ## Publish Readiness
191
+
192
+ - `entryScreenId` must point to an existing screen, decision, or external surface.
193
+ - The served graph should have a completion path.
194
+ - Avoid orphaned screens unless intentionally parked for later editing.
195
+ - Run `scripts/audit-publish-manifest.mjs` before finishing — it enforces dashboard **Publish** rules (see [publish-gates.md](publish-gates.md)).
196
+ - Every `text` and `icon` layer needs explicit `style.color` (including nested button label text).
197
+ - Screens with `text_input`, `multiple_choice`, or `scale_input` need a `continue` button.
@@ -0,0 +1,91 @@
1
+ # Publish Gates (import completion)
2
+
3
+ After generating `rheo-import.manifest.json`, run the **publish gate audit** so the manifest matches what the Rheo dashboard **Publish** button enforces. Goal: one-click import and publish with zero blockers.
4
+
5
+ ```bash
6
+ node scripts/audit-publish-manifest.mjs ./rheo-import.manifest.json
7
+ ```
8
+
9
+ `validate-manifest.mjs` now includes the same blocking rules (not only Zod schema).
10
+
11
+ ## What the audit checks
12
+
13
+ These mirror `apps/web/src/features/builder/validateFlow.ts` and API `preflightPublishManifest` + integration/canvas/plan gates.
14
+
15
+ ### Schema (Zod)
16
+
17
+ - Valid `FlowManifest` shape, ids, layer kinds, required fields.
18
+
19
+ ### Builder rules (`collectFlowBuilderIssues`)
20
+
21
+ | Rule | Typical agent mistake |
22
+ |------|------------------------|
23
+ | **Layer ids** | UUIDs or non-`lyr_*` strings used as layer ids or `optionBindings.rootLayerId` (UUIDs are for `mediaAssetId` only). |
24
+ | **Button `variant`** | Source names copied literally (`outline`, `text`, `link`, `default`) instead of Rheo enum (`primary`/`secondary`/`ghost`/`destructive`). |
25
+ | **`back_button` shape** | Missing `variant`, invalid variant, `action` field present, or nested `icon` without `family: "ionicons"`. |
26
+ | **Choice input shape** | `single_choice` / `multiple_choice` missing `optionBindings` or `branching`, using `"options"` instead of `children`, or `fieldKey` not snake_case. |
27
+ | **Container `children`** | `back_button`, `button`, or `hyperlink` emitted without a `children` array (or with label text on the parent instead of nested `text` children). Crashes import on Indie plans during motion strip; fails Zod validation. |
28
+ | **Text/icon `style.color`** | Body text or **button label** (nested text child) left without `style.color` — native does not inherit CSS colors. |
29
+ | **Continue button** | `text_input`, `multiple_choice`, or `scale_input` without a `button` with `action.kind: "continue"`. |
30
+ | **One input per screen** | Multiple inputs, or OAuth/email-password combined with inputs. |
31
+ | **fieldKey** | Missing or non–snake_case on input layers. |
32
+ | **Graph targets** | `go_to_step`, choice `goTo`, permission outcomes, fallbacks point at missing ids. |
33
+ | **Media triggers** | Lottie/video with `autoPlay: false` needs a `play_media` button targeting that layer. |
34
+ | **Screen backgrounds** | Image/video fills need `mediaAssetId`; manual background video needs trigger wiring. |
35
+
36
+ ### Publishable graph (`validatePublishable`)
37
+
38
+ - At least one screen.
39
+ - `entryScreenId` set and valid.
40
+ - A **completion path** from entry (`end_flow`, terminal `next`, or external surface end).
41
+ - Decision nodes: every case and `elseNext` connected.
42
+
43
+ ### Integrations (default: enabled)
44
+
45
+ - External surfaces need `config.provider` (not `unspecified`).
46
+ - Every external surface needs `fallback`.
47
+ - RevenueCat surfaces require RevenueCat integration enabled (import assumes enabled).
48
+
49
+ ### Canvas editor gates (default: all enabled)
50
+
51
+ Import audit assumes all capabilities are on. If the target app disables Lottie, OAuth, email/password, or OS permission buttons, remove those layers or enable the gate in App settings.
52
+
53
+ **`request_app_review`** is not canvas-gated. When used, ensure the screen has **`next.default`** wired to a valid target.
54
+
55
+ ### Branding gradients
56
+
57
+ - `$brandGradient:<uuid>` only on background-like fields.
58
+ - Unknown preset ids when branding is known.
59
+
60
+ ## Required styling pattern (buttons)
61
+
62
+ Primary CTA example — **label text must have explicit color**:
63
+
64
+ ```json
65
+ {
66
+ "kind": "button",
67
+ "variant": "primary",
68
+ "action": { "kind": "continue" },
69
+ "children": [
70
+ {
71
+ "kind": "text",
72
+ "text": { "default": "Continue" },
73
+ "style": { "color": "#FFFFFF" }
74
+ }
75
+ ]
76
+ }
77
+ ```
78
+
79
+ Body copy on light backgrounds: `"style": { "color": "#0A0A0A" }` or map from `manifest.theme.foreground`.
80
+
81
+ On dark or saturated screen fills, use `#FFFFFF` or `theme.primaryForeground` on text layers.
82
+
83
+ ## Completion gate (blocking)
84
+
85
+ Before delivering the import:
86
+
87
+ - [ ] `node scripts/validate-manifest.mjs` exits 0
88
+ - [ ] `node scripts/audit-publish-manifest.mjs` exits 0 and `rheo-import.publish-gates.md` shows **PASS**
89
+ - [ ] Every blocking issue has been fixed in the manifest (not only explained in chat)
90
+
91
+ If publish gate audit fails, fix the manifest and re-run until PASS. Do not skip because a weaker model missed button label colors.