@getrheo/flow-runtime 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 (126) hide show
  1. package/dist/agentPrompt/index.d.ts +72 -0
  2. package/dist/agentPrompt/index.js +739 -0
  3. package/dist/agentPrompt/index.js.map +1 -0
  4. package/dist/aiFlowGenerationMerge.d.ts +32 -0
  5. package/dist/aiFlowGenerationMerge.js +120 -0
  6. package/dist/aiFlowGenerationMerge.js.map +1 -0
  7. package/dist/animations.d.ts +110 -0
  8. package/dist/animations.js +312 -0
  9. package/dist/animations.js.map +1 -0
  10. package/dist/assignment.d.ts +7 -0
  11. package/dist/assignment.js +25 -0
  12. package/dist/assignment.js.map +1 -0
  13. package/dist/brandGradient.d.ts +57 -0
  14. package/dist/brandGradient.js +137 -0
  15. package/dist/brandGradient.js.map +1 -0
  16. package/dist/brandGradientManifestIssues.d.ts +11 -0
  17. package/dist/brandGradientManifestIssues.js +302 -0
  18. package/dist/brandGradientManifestIssues.js.map +1 -0
  19. package/dist/buildFlowPreview.d.ts +7 -0
  20. package/dist/buildFlowPreview.js +81 -0
  21. package/dist/buildFlowPreview.js.map +1 -0
  22. package/dist/buttonVariantChrome.d.ts +26 -0
  23. package/dist/buttonVariantChrome.js +59 -0
  24. package/dist/buttonVariantChrome.js.map +1 -0
  25. package/dist/checkboxGlyphStyle.d.ts +31 -0
  26. package/dist/checkboxGlyphStyle.js +241 -0
  27. package/dist/checkboxGlyphStyle.js.map +1 -0
  28. package/dist/choiceOptionSelection.d.ts +11 -0
  29. package/dist/choiceOptionSelection.js +120 -0
  30. package/dist/choiceOptionSelection.js.map +1 -0
  31. package/dist/colorAlpha.d.ts +8 -0
  32. package/dist/colorAlpha.js +48 -0
  33. package/dist/colorAlpha.js.map +1 -0
  34. package/dist/counterLayer.d.ts +42 -0
  35. package/dist/counterLayer.js +95 -0
  36. package/dist/counterLayer.js.map +1 -0
  37. package/dist/decisionEval.d.ts +27 -0
  38. package/dist/decisionEval.js +197 -0
  39. package/dist/decisionEval.js.map +1 -0
  40. package/dist/dropShadow.d.ts +26 -0
  41. package/dist/dropShadow.js +76 -0
  42. package/dist/dropShadow.js.map +1 -0
  43. package/dist/emailPasswordAuthValidation.d.ts +16 -0
  44. package/dist/emailPasswordAuthValidation.js +25 -0
  45. package/dist/emailPasswordAuthValidation.js.map +1 -0
  46. package/dist/flowBuilderRules.d.ts +15 -0
  47. package/dist/flowBuilderRules.js +368 -0
  48. package/dist/flowBuilderRules.js.map +1 -0
  49. package/dist/flowGraph.d.ts +19 -0
  50. package/dist/flowGraph.js +373 -0
  51. package/dist/flowGraph.js.map +1 -0
  52. package/dist/hyperlinkLabel.d.ts +19 -0
  53. package/dist/hyperlinkLabel.js +232 -0
  54. package/dist/hyperlinkLabel.js.map +1 -0
  55. package/dist/index.d.ts +48 -0
  56. package/dist/index.js +4200 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/interpolateTemplate.d.ts +44 -0
  59. package/dist/interpolateTemplate.js +188 -0
  60. package/dist/interpolateTemplate.js.map +1 -0
  61. package/dist/layerRotate.d.ts +10 -0
  62. package/dist/layerRotate.js +9 -0
  63. package/dist/layerRotate.js.map +1 -0
  64. package/dist/layerTypography.d.ts +36 -0
  65. package/dist/layerTypography.js +68 -0
  66. package/dist/layerTypography.js.map +1 -0
  67. package/dist/layers.d.ts +69 -0
  68. package/dist/layers.js +257 -0
  69. package/dist/layers.js.map +1 -0
  70. package/dist/layout/index.d.ts +57 -0
  71. package/dist/layout/index.js +151 -0
  72. package/dist/layout/index.js.map +1 -0
  73. package/dist/manifestBillingSlice.d.ts +17 -0
  74. package/dist/manifestBillingSlice.js +102 -0
  75. package/dist/manifestBillingSlice.js.map +1 -0
  76. package/dist/prepareAiGeneratedScreen.d.ts +17 -0
  77. package/dist/prepareAiGeneratedScreen.js +99 -0
  78. package/dist/prepareAiGeneratedScreen.js.map +1 -0
  79. package/dist/publish-exports.json +166 -0
  80. package/dist/responsive/breakpoints.d.ts +34 -0
  81. package/dist/responsive/breakpoints.js +52 -0
  82. package/dist/responsive/breakpoints.js.map +1 -0
  83. package/dist/responsive/index.d.ts +8 -0
  84. package/dist/responsive/index.js +307 -0
  85. package/dist/responsive/index.js.map +1 -0
  86. package/dist/responsive/layerResolve.d.ts +43 -0
  87. package/dist/responsive/layerResolve.js +168 -0
  88. package/dist/responsive/layerResolve.js.map +1 -0
  89. package/dist/responsive/merge.d.ts +19 -0
  90. package/dist/responsive/merge.js +74 -0
  91. package/dist/responsive/merge.js.map +1 -0
  92. package/dist/responsive/previewSafeAreaInsets.d.ts +14 -0
  93. package/dist/responsive/previewSafeAreaInsets.js +24 -0
  94. package/dist/responsive/previewSafeAreaInsets.js.map +1 -0
  95. package/dist/responsive/screenContainerResolve.d.ts +11 -0
  96. package/dist/responsive/screenContainerResolve.js +122 -0
  97. package/dist/responsive/screenContainerResolve.js.map +1 -0
  98. package/dist/responsive/screenShellInsets.d.ts +11 -0
  99. package/dist/responsive/screenShellInsets.js +26 -0
  100. package/dist/responsive/screenShellInsets.js.map +1 -0
  101. package/dist/restingMotion.d.ts +167 -0
  102. package/dist/restingMotion.js +484 -0
  103. package/dist/restingMotion.js.map +1 -0
  104. package/dist/rheoAgentManifestMerge.d.ts +33 -0
  105. package/dist/rheoAgentManifestMerge.js +55 -0
  106. package/dist/rheoAgentManifestMerge.js.map +1 -0
  107. package/dist/scaleInputStyle.d.ts +35 -0
  108. package/dist/scaleInputStyle.js +77 -0
  109. package/dist/scaleInputStyle.js.map +1 -0
  110. package/dist/scaleValidation.d.ts +9 -0
  111. package/dist/scaleValidation.js +21 -0
  112. package/dist/scaleValidation.js.map +1 -0
  113. package/dist/stateMachine.d.ts +105 -0
  114. package/dist/stateMachine.js +674 -0
  115. package/dist/stateMachine.js.map +1 -0
  116. package/dist/stepResponse-BXgoZ7o-.d.ts +112 -0
  117. package/dist/textInputValidation.d.ts +14 -0
  118. package/dist/textInputValidation.js +46 -0
  119. package/dist/textInputValidation.js.map +1 -0
  120. package/dist/translationPlaceholders.d.ts +9 -0
  121. package/dist/translationPlaceholders.js +52 -0
  122. package/dist/translationPlaceholders.js.map +1 -0
  123. package/dist/validation.d.ts +31 -0
  124. package/dist/validation.js +233 -0
  125. package/dist/validation.js.map +1 -0
  126. package/package.json +242 -0
@@ -0,0 +1,25 @@
1
+ // src/emailPasswordAuthValidation.ts
2
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3
+ var validateEmailPasswordAuthFields = (args) => {
4
+ const email = args.email.trim();
5
+ if (!email) return { ok: false, message: "Email is required" };
6
+ if (!EMAIL_RE.test(email)) return { ok: false, message: "Enter a valid email" };
7
+ if (!args.password) return { ok: false, message: "Password is required" };
8
+ if (args.password.length < args.minPasswordLength) {
9
+ return {
10
+ ok: false,
11
+ message: `Password must be at least ${args.minPasswordLength} characters`
12
+ };
13
+ }
14
+ if (args.mode === "sign_up") {
15
+ if (!args.confirmPassword) return { ok: false, message: "Confirm your password" };
16
+ if (args.password !== args.confirmPassword) {
17
+ return { ok: false, message: "Passwords do not match" };
18
+ }
19
+ }
20
+ return { ok: true };
21
+ };
22
+
23
+ export { validateEmailPasswordAuthFields };
24
+ //# sourceMappingURL=emailPasswordAuthValidation.js.map
25
+ //# sourceMappingURL=emailPasswordAuthValidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/emailPasswordAuthValidation.ts"],"names":[],"mappings":";AACA,IAAM,QAAA,GAAW,4BAAA;AAcV,IAAM,+BAAA,GAAkC,CAC7C,IAAA,KACoC;AACpC,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK;AAC9B,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,SAAS,mBAAA,EAAoB;AAC7D,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA,SAAU,EAAE,EAAA,EAAI,KAAA,EAAO,OAAA,EAAS,qBAAA,EAAsB;AAC9E,EAAA,IAAI,CAAC,KAAK,QAAA,EAAU,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,SAAS,sBAAA,EAAuB;AACxE,EAAA,IAAI,IAAA,CAAK,QAAA,CAAS,MAAA,GAAS,IAAA,CAAK,iBAAA,EAAmB;AACjD,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,OAAA,EAAS,CAAA,0BAAA,EAA6B,IAAA,CAAK,iBAAiB,CAAA,WAAA;AAAA,KAC9D;AAAA,EACF;AACA,EAAA,IAAI,IAAA,CAAK,SAAS,SAAA,EAAW;AAC3B,IAAA,IAAI,CAAC,KAAK,eAAA,EAAiB,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,SAAS,uBAAA,EAAwB;AAChF,IAAA,IAAI,IAAA,CAAK,QAAA,KAAa,IAAA,CAAK,eAAA,EAAiB;AAC1C,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,OAAA,EAAS,wBAAA,EAAyB;AAAA,IACxD;AAAA,EACF;AACA,EAAA,OAAO,EAAE,IAAI,IAAA,EAAK;AACpB","file":"emailPasswordAuthValidation.js","sourcesContent":["/** Basic email shape check — host apps should still verify server-side. */\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nexport type ValidateEmailPasswordAuthArgs = {\n mode: 'sign_in' | 'sign_up';\n email: string;\n password: string;\n confirmPassword: string;\n minPasswordLength: number;\n};\n\nexport type ValidateEmailPasswordAuthResult =\n | { ok: true }\n | { ok: false; message: string };\n\nexport const validateEmailPasswordAuthFields = (\n args: ValidateEmailPasswordAuthArgs,\n): ValidateEmailPasswordAuthResult => {\n const email = args.email.trim();\n if (!email) return { ok: false, message: 'Email is required' };\n if (!EMAIL_RE.test(email)) return { ok: false, message: 'Enter a valid email' };\n if (!args.password) return { ok: false, message: 'Password is required' };\n if (args.password.length < args.minPasswordLength) {\n return {\n ok: false,\n message: `Password must be at least ${args.minPasswordLength} characters`,\n };\n }\n if (args.mode === 'sign_up') {\n if (!args.confirmPassword) return { ok: false, message: 'Confirm your password' };\n if (args.password !== args.confirmPassword) {\n return { ok: false, message: 'Passwords do not match' };\n }\n }\n return { ok: true };\n};\n"]}
@@ -0,0 +1,15 @@
1
+ import { FlowManifest } from '@getrheo/contracts/manifest';
2
+ import { IconLayer, TextLayer } from '@getrheo/contracts/layers';
3
+
4
+ /** Agent-facing bullets for rules enforced by {@link collectFlowBuilderIssues}. Keep in sync with checks below. */
5
+ declare const BUILDER_RULES_AGENT_BULLETS: readonly string[];
6
+ /** Text must carry explicit `color` (base or any breakpoint) so native SDK matches canvas without CSS inheritance. */
7
+ declare const textLayerHasAuthoringColor: (l: TextLayer) => boolean;
8
+ declare const iconLayerHasAuthoringColor: (l: IconLayer) => boolean;
9
+ /**
10
+ * Builder / dashboard semantic checks beyond {@link FlowManifestSchema}.
11
+ * Kept in flow-runtime so seeds, API, and the web editor share one definition.
12
+ */
13
+ declare const collectFlowBuilderIssues: (manifest: FlowManifest) => string[];
14
+
15
+ export { BUILDER_RULES_AGENT_BULLETS, collectFlowBuilderIssues, iconLayerHasAuthoringColor, textLayerHasAuthoringColor };
@@ -0,0 +1,368 @@
1
+ import { screenBackgroundPlaybackId, isScreenBackgroundPlaybackId } from '@getrheo/contracts';
2
+ import { isInputLayer, OS_PERMISSION_OUTCOME_END, OS_PERMISSION_OUTCOME_CONTINUE } from '@getrheo/contracts/layers';
3
+ import '@getrheo/contracts/localized';
4
+
5
+ // src/flowBuilderRules.ts
6
+ var walkLayers = (root, fn) => {
7
+ const visit = (l, depth) => {
8
+ fn(l, depth);
9
+ if (l.kind === "stack") l.children.forEach((c) => visit(c, depth + 1));
10
+ else if (l.kind === "carousel") l.slides.forEach((c) => visit(c, depth + 1));
11
+ else if (l.kind === "button") l.children.forEach((c) => visit(c, depth + 1));
12
+ else if (l.kind === "back_button") l.children.forEach((c) => visit(c, depth + 1));
13
+ else if (l.kind === "hyperlink") l.children.forEach((c) => visit(c, depth + 1));
14
+ else if (l.kind === "single_choice" || l.kind === "multiple_choice") {
15
+ l.children.forEach((c) => visit(c, depth + 1));
16
+ } else if (l.kind === "text_input" || l.kind === "scale_input") {
17
+ l.children?.forEach((c) => visit(c, depth + 1));
18
+ } else if (l.kind === "oauth_login") {
19
+ l.children.forEach((c) => visit(c, depth + 1));
20
+ } else if (l.kind === "oauth_provider" && l.variant === "custom") {
21
+ l.children.forEach((c) => visit(c, depth + 1));
22
+ } else if (l.kind === "email_password_auth") {
23
+ l.children.forEach((c) => visit(c, depth + 1));
24
+ } else if (l.kind === "email_password_field") {
25
+ l.children?.forEach((c) => visit(c, depth + 1));
26
+ } else if (l.kind === "email_password_submit") {
27
+ l.children.forEach((c) => visit(c, depth + 1));
28
+ }
29
+ };
30
+ visit(root, 0);
31
+ };
32
+ var walkScreen = (screen, fn) => {
33
+ if (screen.regions.header) walkLayers(screen.regions.header, fn);
34
+ walkLayers(screen.regions.body, fn);
35
+ if (screen.regions.footer) walkLayers(screen.regions.footer, fn);
36
+ };
37
+ var findLayerById = (screen, id) => {
38
+ let found = null;
39
+ walkScreen(screen, (l) => {
40
+ if (!found && l.id === id) found = l;
41
+ });
42
+ return found;
43
+ };
44
+
45
+ // src/flowBuilderRules.ts
46
+ var BUILDER_RULES_AGENT_BULLETS = [
47
+ "Connect flow entry on the canvas before publishing (entryScreenId must exist when screens are present).",
48
+ "Every text and icon layer needs explicit style.color (including nested button label text \u2014 native does not inherit colors).",
49
+ 'Screens with text_input, multiple_choice, or scale_input need a button with action.kind "continue".',
50
+ "At most one input layer per screen (single_choice, multiple_choice, text_input, scale_input).",
51
+ "Do not combine oauth_login or email_password_auth with other input layers on the same screen.",
52
+ "Only one oauth_login and one email_password_auth per screen; never both on the same screen.",
53
+ "fieldKey values must be unique snake_case across the flow.",
54
+ "Choice branch goTo and go_to_step screenId must reference existing screen ids.",
55
+ "request_app_review buttons require screen.next.default wired to a valid target.",
56
+ 'request_os_permission outcomes must target existing screens, "continue", or "end".',
57
+ 'Lottie/video with autoPlay false needs a button with action.kind "play_media" targeting that layer (or screen background video id).',
58
+ "play_media targetLayerIds must reference Lottie/video layers on the same screen or the screen background video playback id."
59
+ ];
60
+ var FIELD_KEY_RE = /^[a-z][a-z0-9_]*$/;
61
+ var styleBucketHasColor = (s) => s !== void 0 && s.color !== void 0;
62
+ var textLayerHasAuthoringColor = (l) => {
63
+ if (styleBucketHasColor(l.style)) return true;
64
+ const bp = l.styleBreakpoints;
65
+ if (!bp) return false;
66
+ return styleBucketHasColor(bp.sm) || styleBucketHasColor(bp.md) || styleBucketHasColor(bp.lg) || styleBucketHasColor(bp.xl) || styleBucketHasColor(bp["2xl"]);
67
+ };
68
+ var iconLayerHasAuthoringColor = (l) => {
69
+ if (styleBucketHasColor(l.style)) return true;
70
+ const bp = l.styleBreakpoints;
71
+ if (!bp) return false;
72
+ return styleBucketHasColor(bp.sm) || styleBucketHasColor(bp.md) || styleBucketHasColor(bp.lg) || styleBucketHasColor(bp.xl) || styleBucketHasColor(bp["2xl"]);
73
+ };
74
+ var collectFlowBuilderIssues = (manifest) => {
75
+ const issues = [];
76
+ const fieldKeyOwners = /* @__PURE__ */ new Map();
77
+ const screenIds = new Set(manifest.screens.map((s) => s.id));
78
+ const jumpTargetIds = /* @__PURE__ */ new Set([
79
+ ...screenIds,
80
+ ...manifest.decisionNodes.map((d) => d.id),
81
+ ...(manifest.externalSurfaceNodes ?? []).map((n) => n.id)
82
+ ]);
83
+ if (manifest.entryScreenId == null) {
84
+ if (manifest.screens.length > 0) {
85
+ issues.push(
86
+ "Connect the flow entry node on the canvas to where the flow starts (a screen, decision, or integration step)."
87
+ );
88
+ }
89
+ } else if (!jumpTargetIds.has(manifest.entryScreenId)) {
90
+ issues.push(`Flow entry target "${manifest.entryScreenId}" does not exist.`);
91
+ }
92
+ for (const screen of manifest.screens) {
93
+ let inputCount = 0;
94
+ let oauthLoginLayerCount = 0;
95
+ let emailPasswordAuthLayerCount = 0;
96
+ let needsManualSubmit = false;
97
+ let hasContinueButton = false;
98
+ const screenLabel = screen.name || screen.id;
99
+ const mediaLayerIds = /* @__PURE__ */ new Set();
100
+ const buttonLayerIds = /* @__PURE__ */ new Set();
101
+ const shellPlaybackId = screenBackgroundPlaybackId(screen.id);
102
+ const shellFill = screen.containerStyle?.backgroundFill;
103
+ const shellVideoFill = shellFill?.kind === "video" ? shellFill : void 0;
104
+ walkScreen(screen, (l) => {
105
+ if (l.kind === "button") buttonLayerIds.add(l.id);
106
+ });
107
+ if (shellFill?.kind === "image" || shellFill?.kind === "video") {
108
+ if (!shellFill.media?.mediaAssetId) {
109
+ issues.push(
110
+ `Screen "${screenLabel}" ${shellFill.kind} background needs a media asset.`
111
+ );
112
+ }
113
+ }
114
+ if (shellVideoFill) {
115
+ if (shellVideoFill.autoPlay === false) {
116
+ const triggerId = shellVideoFill.triggerLayerId?.trim();
117
+ if (!triggerId) {
118
+ issues.push(
119
+ `Screen "${screenLabel}" background video needs a trigger button when auto-play is off.`
120
+ );
121
+ } else if (!buttonLayerIds.has(triggerId)) {
122
+ issues.push(
123
+ `Screen "${screenLabel}" background video references a missing trigger button "${triggerId}".`
124
+ );
125
+ } else {
126
+ const btn = findLayerById(screen, triggerId);
127
+ if (!btn || btn.kind !== "button") {
128
+ issues.push(
129
+ `Screen "${screenLabel}" background video trigger must be a button layer.`
130
+ );
131
+ } else if (btn.action.kind !== "play_media") {
132
+ issues.push(
133
+ `Screen "${screenLabel}" background video trigger button must use On Tap \u2192 Play media.`
134
+ );
135
+ } else if (!btn.action.targetLayerIds.includes(shellPlaybackId)) {
136
+ issues.push(
137
+ `Screen "${screenLabel}" background video is not listed on trigger button "${triggerId}".`
138
+ );
139
+ }
140
+ }
141
+ } else if (shellVideoFill.triggerLayerId) {
142
+ const btn = findLayerById(screen, shellVideoFill.triggerLayerId);
143
+ if (btn?.kind === "button" && btn.action.kind === "play_media" && !btn.action.targetLayerIds.includes(shellPlaybackId)) {
144
+ issues.push(
145
+ `Screen "${screenLabel}" background video trigger button does not target the screen background.`
146
+ );
147
+ }
148
+ }
149
+ }
150
+ walkScreen(screen, (l) => {
151
+ if (l.kind === "lottie" || l.kind === "video") mediaLayerIds.add(l.id);
152
+ if (l.kind === "oauth_login") {
153
+ oauthLoginLayerCount += 1;
154
+ }
155
+ if (l.kind === "email_password_auth") {
156
+ emailPasswordAuthLayerCount += 1;
157
+ }
158
+ if (l.kind === "button" && l.action.kind === "continue") {
159
+ hasContinueButton = true;
160
+ }
161
+ if (isInputLayer(l)) {
162
+ inputCount += 1;
163
+ if (l.kind === "multiple_choice" || l.kind === "text_input" || l.kind === "scale_input") {
164
+ needsManualSubmit = true;
165
+ }
166
+ const key = l.fieldKey;
167
+ const label = screen.name || screen.id;
168
+ if (!key || key.length === 0) {
169
+ issues.push(`Screen "${label}" is missing a variable name (fieldKey).`);
170
+ } else if (!FIELD_KEY_RE.test(key)) {
171
+ issues.push(
172
+ `Screen "${label}" has an invalid variable name "${key}" \u2014 use snake_case (a\u2013z, 0\u20139, _).`
173
+ );
174
+ } else {
175
+ const owners = fieldKeyOwners.get(key) ?? [];
176
+ owners.push(label);
177
+ fieldKeyOwners.set(key, owners);
178
+ }
179
+ if (l.kind === "single_choice" || l.kind === "multiple_choice") {
180
+ for (const cond of l.branching.conditions) {
181
+ if (!screenIds.has(cond.goTo)) {
182
+ issues.push(
183
+ `Screen "${label}" branches choice "${cond.choiceId}" to a missing screen "${cond.goTo}".`
184
+ );
185
+ }
186
+ }
187
+ }
188
+ }
189
+ if (l.kind === "button" && l.action.kind === "go_to_step") {
190
+ if (!screenIds.has(l.action.screenId)) {
191
+ issues.push(
192
+ `Button "${l.name || l.id}" on screen "${screen.name || screen.id}" targets a missing screen "${l.action.screenId}".`
193
+ );
194
+ }
195
+ }
196
+ if (l.kind === "button" && l.action.kind === "request_app_review") {
197
+ const def = screen.next?.default;
198
+ if (def == null) {
199
+ issues.push(
200
+ `Button "${l.name || l.id}" on screen "${screen.name || screen.id}" requests app review but the screen has no default next step.`
201
+ );
202
+ } else if (!screenIds.has(def) && !manifest.decisionNodes?.some((d) => d.id === def)) {
203
+ issues.push(
204
+ `Button "${l.name || l.id}" on screen "${screen.name || screen.id}" requests app review but default next "${def}" is missing.`
205
+ );
206
+ }
207
+ }
208
+ if (l.kind === "button" && l.action.kind === "request_os_permission") {
209
+ const o = l.action.outcomes;
210
+ for (const slot of ["granted", "denied", "blocked"]) {
211
+ const sid = o[slot];
212
+ if (sid === OS_PERMISSION_OUTCOME_END) {
213
+ continue;
214
+ }
215
+ if (sid === OS_PERMISSION_OUTCOME_CONTINUE) {
216
+ const def = screen.next?.default;
217
+ if (def == null) {
218
+ continue;
219
+ }
220
+ if (!screenIds.has(def) && !manifest.decisionNodes?.some((d) => d.id === def)) {
221
+ issues.push(
222
+ `Button "${l.name || l.id}" on screen "${screen.name || screen.id}" (${slot}) continues to missing target "${def}".`
223
+ );
224
+ }
225
+ continue;
226
+ }
227
+ if (!screenIds.has(sid)) {
228
+ issues.push(
229
+ `Button "${l.name || l.id}" on screen "${screen.name || screen.id}" (${slot}) targets a missing screen "${sid}".`
230
+ );
231
+ }
232
+ }
233
+ }
234
+ if (l.kind === "back_button" && l.fallbackScreenId && !screenIds.has(l.fallbackScreenId)) {
235
+ issues.push(
236
+ `Back button "${l.name || l.id}" on screen "${screen.name || screen.id}" uses a missing fallback screen "${l.fallbackScreenId}".`
237
+ );
238
+ }
239
+ if (l.kind === "button" && l.action.kind === "go_back_one_screen" && l.action.fallbackScreenId) {
240
+ if (!screenIds.has(l.action.fallbackScreenId)) {
241
+ issues.push(
242
+ `Button "${l.name || l.id}" on screen "${screen.name || screen.id}" uses a missing fallback screen "${l.action.fallbackScreenId}".`
243
+ );
244
+ }
245
+ }
246
+ if (l.kind === "lottie" || l.kind === "video") {
247
+ const media = l;
248
+ if (media.autoPlay === false) {
249
+ const triggerId = media.triggerLayerId?.trim();
250
+ if (!triggerId) {
251
+ issues.push(
252
+ `${media.kind === "video" ? "Video" : "Lottie"} "${media.name || media.id}" on screen "${screenLabel}" needs a trigger button when auto-play is off.`
253
+ );
254
+ } else if (!buttonLayerIds.has(triggerId)) {
255
+ issues.push(
256
+ `${media.kind === "video" ? "Video" : "Lottie"} "${media.name || media.id}" on screen "${screenLabel}" references a missing trigger button "${triggerId}".`
257
+ );
258
+ } else {
259
+ const btn = findLayerById(screen, triggerId);
260
+ if (!btn || btn.kind !== "button") {
261
+ issues.push(
262
+ `${media.kind === "video" ? "Video" : "Lottie"} "${media.name || media.id}" on screen "${screenLabel}" trigger must be a button layer.`
263
+ );
264
+ } else if (btn.action.kind !== "play_media") {
265
+ issues.push(
266
+ `${media.kind === "video" ? "Video" : "Lottie"} "${media.name || media.id}" on screen "${screenLabel}" trigger button must use On Tap \u2192 Play media (or pick the trigger again from this screen).`
267
+ );
268
+ } else if (!btn.action.targetLayerIds.includes(media.id)) {
269
+ issues.push(
270
+ `${media.kind === "video" ? "Video" : "Lottie"} "${media.name || media.id}" on screen "${screenLabel}" is not listed on trigger button "${triggerId}".`
271
+ );
272
+ }
273
+ }
274
+ } else if (media.triggerLayerId) {
275
+ const btn = findLayerById(screen, media.triggerLayerId);
276
+ if (btn?.kind === "button" && btn.action.kind === "play_media" && !btn.action.targetLayerIds.includes(media.id)) {
277
+ issues.push(
278
+ `${media.kind === "video" ? "Video" : "Lottie"} "${media.name || media.id}" on screen "${screenLabel}" trigger button does not target this layer.`
279
+ );
280
+ }
281
+ }
282
+ }
283
+ if (l.kind === "button" && l.action.kind === "play_media") {
284
+ for (const targetId of l.action.targetLayerIds) {
285
+ if (targetId === shellPlaybackId) {
286
+ if (!shellVideoFill) {
287
+ issues.push(
288
+ `Button "${l.name || l.id}" on screen "${screenLabel}" targets screen background video, but this screen has no video background.`
289
+ );
290
+ }
291
+ continue;
292
+ }
293
+ if (isScreenBackgroundPlaybackId(targetId)) {
294
+ issues.push(
295
+ `Button "${l.name || l.id}" on screen "${screenLabel}" play-media target "${targetId}" is not valid for this screen.`
296
+ );
297
+ continue;
298
+ }
299
+ if (!mediaLayerIds.has(targetId)) {
300
+ issues.push(
301
+ `Button "${l.name || l.id}" on screen "${screenLabel}" play-media target "${targetId}" must be a Lottie or video layer on this screen, or screen background video.`
302
+ );
303
+ }
304
+ }
305
+ }
306
+ });
307
+ if (oauthLoginLayerCount > 0 && inputCount > 0) {
308
+ issues.push(
309
+ `Screen "${screen.name || screen.id}" cannot combine OAuth Login with input layers (${inputCount}). Split them onto separate screens.`
310
+ );
311
+ }
312
+ if (emailPasswordAuthLayerCount > 0 && inputCount > 0) {
313
+ issues.push(
314
+ `Screen "${screen.name || screen.id}" cannot combine Email / password login with input layers (${inputCount}). Split them onto separate screens.`
315
+ );
316
+ }
317
+ if (oauthLoginLayerCount > 0 && emailPasswordAuthLayerCount > 0) {
318
+ issues.push(
319
+ `Screen "${screen.name || screen.id}" cannot combine OAuth Login with Email / password login. Use one login block per screen.`
320
+ );
321
+ }
322
+ if (emailPasswordAuthLayerCount > 1) {
323
+ issues.push(
324
+ `Screen "${screen.name || screen.id}" has ${emailPasswordAuthLayerCount} Email / password login layers; only one is allowed per screen.`
325
+ );
326
+ }
327
+ if (oauthLoginLayerCount > 1) {
328
+ issues.push(
329
+ `Screen "${screen.name || screen.id}" has ${oauthLoginLayerCount} OAuth Login layers; only one is allowed per screen.`
330
+ );
331
+ }
332
+ if (inputCount > 1) {
333
+ issues.push(
334
+ `Screen "${screen.name || screen.id}" has ${inputCount} input layers; only one is allowed.`
335
+ );
336
+ }
337
+ if (needsManualSubmit && !hasContinueButton) {
338
+ issues.push(
339
+ `Screen "${screen.name || screen.id}" has a multiple_choice, text_input, or scale_input but no Button with action "continue". Add a Continue button so users can submit.`
340
+ );
341
+ }
342
+ }
343
+ for (const [key, owners] of fieldKeyOwners) {
344
+ if (owners.length > 1) {
345
+ issues.push(`Variable name "${key}" is used by multiple screens: ${owners.join(", ")}.`);
346
+ }
347
+ }
348
+ for (const screen of manifest.screens) {
349
+ walkScreen(screen, (l) => {
350
+ const screenLabel = screen.name || screen.id;
351
+ if (l.kind === "text" && !textLayerHasAuthoringColor(l)) {
352
+ issues.push(
353
+ `Screen "${screenLabel}": text layer "${l.id}" must set style.color for light and dark (CSS inheritance does not apply on native).`
354
+ );
355
+ }
356
+ if (l.kind === "icon" && !iconLayerHasAuthoringColor(l)) {
357
+ issues.push(
358
+ `Screen "${screenLabel}": icon layer "${l.id}" must set style.color for light and dark.`
359
+ );
360
+ }
361
+ });
362
+ }
363
+ return issues;
364
+ };
365
+
366
+ export { BUILDER_RULES_AGENT_BULLETS, collectFlowBuilderIssues, iconLayerHasAuthoringColor, textLayerHasAuthoringColor };
367
+ //# sourceMappingURL=flowBuilderRules.js.map
368
+ //# sourceMappingURL=flowBuilderRules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/layers.ts","../src/flowBuilderRules.ts"],"names":["isInputLayer"],"mappings":"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","file":"flowBuilderRules.js","sourcesContent":["import type { Branding } from '@getrheo/contracts/dashboard';\nimport type { FlowManifest, Theme } from '@getrheo/contracts/manifest';\nimport type { Screen } from '@getrheo/contracts/screens';\nimport {\n brandGradientFromThemedColor,\n brandGradientNativeLinear,\n brandGradientSolidFallback,\n isStoredLinearGradientCss,\n nativeLinearFromAngleAndStops,\n parseLinearGradientCss,\n resolveBrandGradientToken,\n type BrandGradientNativeLinear,\n} from './brandGradient';\nimport type {\n InputLayer,\n Layer,\n MultipleChoiceLayer,\n ScaleInputLayer,\n SingleChoiceLayer,\n StackLayer,\n TextInputLayer,\n ThemedColor,\n} from '@getrheo/contracts/layers';\nimport type { LocalizedText } from '@getrheo/contracts/localized';\nimport { resolveLocalizedText } from '@getrheo/contracts/localized';\nimport { isInputLayer } from '@getrheo/contracts/layers';\n\n/** Walk a layer tree depth-first. */\nexport const walkLayers = (root: Layer, fn: (l: Layer, depth: number) => void): void => {\n const visit = (l: Layer, depth: number): void => {\n fn(l, depth);\n if (l.kind === 'stack') l.children.forEach((c) => visit(c, depth + 1));\n else if (l.kind === 'carousel') l.slides.forEach((c) => visit(c, depth + 1));\n else if (l.kind === 'button') l.children.forEach((c) => visit(c, depth + 1));\n else if (l.kind === 'back_button') l.children.forEach((c) => visit(c, depth + 1));\n else if (l.kind === 'hyperlink') l.children.forEach((c) => visit(c, depth + 1));\n else if (l.kind === 'single_choice' || l.kind === 'multiple_choice') {\n l.children.forEach((c) => visit(c, depth + 1));\n } else if (l.kind === 'text_input' || l.kind === 'scale_input') {\n l.children?.forEach((c) => visit(c, depth + 1));\n } else if (l.kind === 'oauth_login') {\n l.children.forEach((c) => visit(c, depth + 1));\n } else if (l.kind === 'oauth_provider' && l.variant === 'custom') {\n l.children.forEach((c) => visit(c, depth + 1));\n } else if (l.kind === 'email_password_auth') {\n l.children.forEach((c) => visit(c, depth + 1));\n } else if (l.kind === 'email_password_field') {\n l.children?.forEach((c) => visit(c, depth + 1));\n } else if (l.kind === 'email_password_submit') {\n l.children.forEach((c) => visit(c, depth + 1));\n }\n };\n visit(root, 0);\n};\n\n/** Walk every layer in a screen's regions (header → body → footer). */\nexport const walkScreen = (screen: Screen, fn: (l: Layer) => void): void => {\n if (screen.regions.header) walkLayers(screen.regions.header, fn);\n walkLayers(screen.regions.body, fn);\n if (screen.regions.footer) walkLayers(screen.regions.footer, fn);\n};\n\n/** Find the screen's lone input layer (if any). Schema enforces ≤1. */\nexport const findInputLayer = (screen: Screen): InputLayer | null => {\n let found: InputLayer | null = null;\n walkScreen(screen, (l) => {\n if (!found && isInputLayer(l)) found = l;\n });\n return found;\n};\n\n/** Input kinds that use a screen draft and require an explicit Continue to submit. */\nexport const findManualSubmitInputLayer = (\n screen: Screen,\n): MultipleChoiceLayer | TextInputLayer | ScaleInputLayer | null => {\n const input = findInputLayer(screen);\n if (!input) return null;\n if (input.kind === 'multiple_choice' || input.kind === 'text_input' || input.kind === 'scale_input') {\n return input;\n }\n return null;\n};\n\n/**\n * Whether the screen contains any Button layer that submits the screen\n * (i.e. `action.kind === 'continue'`). Used by input layers to decide\n * between auto-submit-on-tap (legacy behaviour for choice-only screens)\n * and writing into the screen-level draft for a Button to submit.\n */\nexport const screenHasContinueButton = (screen: Screen): boolean => {\n let found = false;\n walkScreen(screen, (l) => {\n if (l.kind === 'button' && l.action.kind === 'continue') found = true;\n });\n return found;\n};\n\n/**\n * Resolve a choice layer's option stack by stable optionId via its\n * binding. Returns null when the binding is missing or the bound child\n * was removed (manifest validation rejects that, but runtime stays safe).\n */\nexport const findOptionStackForChoice = (\n layer: SingleChoiceLayer | MultipleChoiceLayer,\n optionId: string,\n): StackLayer | null => {\n const binding = layer.optionBindings.find((b) => b.optionId === optionId);\n if (!binding) return null;\n const stack = layer.children.find((c) => c.id === binding.rootLayerId);\n return stack ?? null;\n};\n\n/**\n * Best-effort textual label for a choice option, used by interpolation\n * (e.g. `{{ goal }}` rendering the chosen option's label) and by editor\n * surfaces that show option rows. Walks the option's child subtree\n * depth-first and returns the first `text` layer's content; falls back\n * to the option's stable id when no text is present.\n */\nexport const choiceOptionLabel = (\n layer: SingleChoiceLayer | MultipleChoiceLayer,\n optionId: string,\n locale: string,\n): string => {\n const stack = findOptionStackForChoice(layer, optionId);\n if (!stack) return '';\n let foundText: LocalizedText | null = null;\n walkLayers(stack, (l) => {\n if (foundText) return;\n if (l.kind === 'text') foundText = l.text;\n });\n if (foundText) return resolveLocalizedText(foundText, locale);\n return optionId;\n};\n\n/** Find a layer in a screen by id, including nested children/slides. */\nexport const findLayerById = (screen: Screen, id: string): Layer | null => {\n let found: Layer | null = null;\n walkScreen(screen, (l) => {\n if (!found && l.id === id) found = l;\n });\n return found;\n};\n\n/** Collect all input fieldKeys across a manifest. */\nexport const collectFieldKeys = (manifest: FlowManifest): { fieldKey: string; screenId: string }[] => {\n const out: { fieldKey: string; screenId: string }[] = [];\n for (const screen of manifest.screens) {\n walkScreen(screen as unknown as Screen, (l) => {\n if (isInputLayer(l)) out.push({ fieldKey: l.fieldKey, screenId: screen.id });\n if (l.kind === 'checkbox') out.push({ fieldKey: l.fieldKey, screenId: screen.id });\n if (l.kind === 'email_password_auth') {\n out.push({ fieldKey: l.fieldKey, screenId: screen.id });\n }\n });\n }\n return out;\n};\n\n/**\n * Pick a snake_case field key starting from `base` that is not in `used`\n * (e.g. `text` → `text_2` → `text_3` when `text` is taken).\n */\nexport const nextUniqueFieldKey = (base: string, used: Iterable<string>): string => {\n const set = used instanceof Set ? used : new Set(used);\n if (!set.has(base)) return base;\n let n = 2;\n while (set.has(`${base}_${n}`)) n += 1;\n return `${base}_${n}`;\n};\n\n/**\n * Resolve a token reference like `$primary` to a literal value from `theme`.\n * Pass-through for non-token strings; returns undefined for `undefined`.\n */\nexport const resolveTokens = <T extends string | undefined>(\n theme: Theme | undefined,\n value: T,\n): T | string => {\n if (value === undefined) return value;\n if (typeof value !== 'string' || !value.startsWith('$')) return value;\n const key = value.slice(1) as keyof Theme;\n const literal = theme?.[key];\n if (typeof literal === 'string') return literal;\n return value;\n};\n\n/**\n * Resolve a layer color for the current appearance (`light` | `dark`).\n * Plain string uses `resolveTokens` for both modes (legacy). Object form\n * picks `light` / `dark` with fallback to the other key when one is omitted.\n */\nexport const resolveThemedColor = (\n theme: Theme | undefined,\n palette: 'light' | 'dark',\n value: ThemedColor | undefined,\n): string | undefined => {\n if (value === undefined) return undefined;\n if (typeof value === 'string') return resolveTokens(theme, value) as string;\n const raw = palette === 'dark' ? (value.dark ?? value.light) : (value.light ?? value.dark);\n if (raw === undefined) return undefined;\n return resolveTokens(theme, raw) as string;\n};\n\n/**\n * Resolve a themed value used for CSS `background` (or RN background fill).\n * Supports `$brandGradient:<uuid>` when branding presets are provided; other values match {@link resolveThemedColor}.\n */\nexport const resolveThemedBackground = (\n theme: Theme | undefined,\n branding: Branding | undefined,\n palette: 'light' | 'dark',\n value: ThemedColor | undefined,\n): string | undefined => {\n if (value === undefined) return undefined;\n if (typeof value === 'string') {\n if (value.startsWith('$brandGradient:')) {\n return resolveBrandGradientToken(branding, value);\n }\n return resolveTokens(theme, value) as string;\n }\n const raw = palette === 'dark' ? (value.dark ?? value.light) : (value.light ?? value.dark);\n if (raw === undefined) return undefined;\n if (raw.startsWith('$brandGradient:')) {\n return resolveBrandGradientToken(branding, raw);\n }\n return resolveTokens(theme, raw) as string;\n};\n\nexport const nativeBrandBackgroundFromThemedColor = (\n theme: Theme | undefined,\n branding: Branding | undefined,\n palette: 'light' | 'dark',\n value: ThemedColor | undefined,\n): { solid?: string; linear?: BrandGradientNativeLinear } => {\n const preset = brandGradientFromThemedColor(branding, palette, value);\n if (preset) {\n const lin = brandGradientNativeLinear(preset);\n if (lin) return { linear: lin };\n return { solid: brandGradientSolidFallback(preset) };\n }\n const bg = resolveThemedBackground(theme, branding, palette, value) as string | undefined;\n if (!bg) return {};\n const parsed = parseLinearGradientCss(bg);\n if (parsed) {\n return {\n linear: nativeLinearFromAngleAndStops(\n parsed.angleDeg,\n parsed.stops.map((s) => ({ color: s.color, offsetPct: s.offsetPct })),\n ),\n };\n }\n if (isStoredLinearGradientCss(bg)) {\n const first = bg.match(/#[0-9a-fA-F]{3,8}/);\n return { solid: first ? first[0] : '#808080' };\n }\n return { solid: bg };\n};\n","import type { FlowManifest } from '@getrheo/contracts/manifest';\nimport type { Screen, ScreenBackgroundVideoFill } from '@getrheo/contracts';\nimport { isScreenBackgroundPlaybackId, screenBackgroundPlaybackId } from '@getrheo/contracts';\nimport {\n isInputLayer,\n OS_PERMISSION_OUTCOME_CONTINUE,\n OS_PERMISSION_OUTCOME_END,\n type ButtonLayer,\n type IconLayer,\n type LottieLayer,\n type TextLayer,\n type VideoLayer,\n} from '@getrheo/contracts/layers';\nimport { findLayerById, walkScreen } from './layers';\n\n/** Agent-facing bullets for rules enforced by {@link collectFlowBuilderIssues}. Keep in sync with checks below. */\nexport const BUILDER_RULES_AGENT_BULLETS: readonly string[] = [\n 'Connect flow entry on the canvas before publishing (entryScreenId must exist when screens are present).',\n 'Every text and icon layer needs explicit style.color (including nested button label text — native does not inherit colors).',\n 'Screens with text_input, multiple_choice, or scale_input need a button with action.kind \"continue\".',\n 'At most one input layer per screen (single_choice, multiple_choice, text_input, scale_input).',\n 'Do not combine oauth_login or email_password_auth with other input layers on the same screen.',\n 'Only one oauth_login and one email_password_auth per screen; never both on the same screen.',\n 'fieldKey values must be unique snake_case across the flow.',\n 'Choice branch goTo and go_to_step screenId must reference existing screen ids.',\n 'request_app_review buttons require screen.next.default wired to a valid target.',\n 'request_os_permission outcomes must target existing screens, \"continue\", or \"end\".',\n 'Lottie/video with autoPlay false needs a button with action.kind \"play_media\" targeting that layer (or screen background video id).',\n 'play_media targetLayerIds must reference Lottie/video layers on the same screen or the screen background video playback id.',\n];\n\nconst FIELD_KEY_RE = /^[a-z][a-z0-9_]*$/;\n\nconst styleBucketHasColor = (s: { color?: unknown } | undefined): boolean =>\n s !== undefined && s.color !== undefined;\n\n/** Text must carry explicit `color` (base or any breakpoint) so native SDK matches canvas without CSS inheritance. */\nexport const textLayerHasAuthoringColor = (l: TextLayer): boolean => {\n if (styleBucketHasColor(l.style)) return true;\n const bp = l.styleBreakpoints;\n if (!bp) return false;\n return (\n styleBucketHasColor(bp.sm) ||\n styleBucketHasColor(bp.md) ||\n styleBucketHasColor(bp.lg) ||\n styleBucketHasColor(bp.xl) ||\n styleBucketHasColor(bp['2xl'])\n );\n};\n\nexport const iconLayerHasAuthoringColor = (l: IconLayer): boolean => {\n if (styleBucketHasColor(l.style)) return true;\n const bp = l.styleBreakpoints;\n if (!bp) return false;\n return (\n styleBucketHasColor(bp.sm) ||\n styleBucketHasColor(bp.md) ||\n styleBucketHasColor(bp.lg) ||\n styleBucketHasColor(bp.xl) ||\n styleBucketHasColor(bp['2xl'])\n );\n};\n\n/**\n * Builder / dashboard semantic checks beyond {@link FlowManifestSchema}.\n * Kept in flow-runtime so seeds, API, and the web editor share one definition.\n */\nexport const collectFlowBuilderIssues = (manifest: FlowManifest): string[] => {\n const issues: string[] = [];\n const fieldKeyOwners = new Map<string, string[]>();\n const screenIds = new Set(manifest.screens.map((s) => s.id));\n const jumpTargetIds = new Set<string>([\n ...screenIds,\n ...manifest.decisionNodes.map((d) => d.id),\n ...(manifest.externalSurfaceNodes ?? []).map((n) => n.id),\n ]);\n\n if (manifest.entryScreenId == null) {\n if (manifest.screens.length > 0) {\n issues.push(\n 'Connect the flow entry node on the canvas to where the flow starts (a screen, decision, or integration step).',\n );\n }\n } else if (!jumpTargetIds.has(manifest.entryScreenId)) {\n issues.push(`Flow entry target \"${manifest.entryScreenId}\" does not exist.`);\n }\n\n for (const screen of manifest.screens as unknown as Screen[]) {\n let inputCount = 0;\n let oauthLoginLayerCount = 0;\n let emailPasswordAuthLayerCount = 0;\n let needsManualSubmit = false;\n let hasContinueButton = false;\n const screenLabel = screen.name || screen.id;\n const mediaLayerIds = new Set<string>();\n const buttonLayerIds = new Set<string>();\n const shellPlaybackId = screenBackgroundPlaybackId(screen.id);\n const shellFill = screen.containerStyle?.backgroundFill;\n const shellVideoFill =\n shellFill?.kind === 'video' ? (shellFill as ScreenBackgroundVideoFill) : undefined;\n\n walkScreen(screen, (l) => {\n if (l.kind === 'button') buttonLayerIds.add(l.id);\n });\n\n if (shellFill?.kind === 'image' || shellFill?.kind === 'video') {\n if (!shellFill.media?.mediaAssetId) {\n issues.push(\n `Screen \"${screenLabel}\" ${shellFill.kind} background needs a media asset.`,\n );\n }\n }\n\n if (shellVideoFill) {\n if (shellVideoFill.autoPlay === false) {\n const triggerId = shellVideoFill.triggerLayerId?.trim();\n if (!triggerId) {\n issues.push(\n `Screen \"${screenLabel}\" background video needs a trigger button when auto-play is off.`,\n );\n } else if (!buttonLayerIds.has(triggerId)) {\n issues.push(\n `Screen \"${screenLabel}\" background video references a missing trigger button \"${triggerId}\".`,\n );\n } else {\n const btn = findLayerById(screen, triggerId) as ButtonLayer | null;\n if (!btn || btn.kind !== 'button') {\n issues.push(\n `Screen \"${screenLabel}\" background video trigger must be a button layer.`,\n );\n } else if (btn.action.kind !== 'play_media') {\n issues.push(\n `Screen \"${screenLabel}\" background video trigger button must use On Tap → Play media.`,\n );\n } else if (!btn.action.targetLayerIds.includes(shellPlaybackId)) {\n issues.push(\n `Screen \"${screenLabel}\" background video is not listed on trigger button \"${triggerId}\".`,\n );\n }\n }\n } else if (shellVideoFill.triggerLayerId) {\n const btn = findLayerById(screen, shellVideoFill.triggerLayerId);\n if (\n btn?.kind === 'button' &&\n btn.action.kind === 'play_media' &&\n !btn.action.targetLayerIds.includes(shellPlaybackId)\n ) {\n issues.push(\n `Screen \"${screenLabel}\" background video trigger button does not target the screen background.`,\n );\n }\n }\n }\n\n walkScreen(screen, (l) => {\n if (l.kind === 'lottie' || l.kind === 'video') mediaLayerIds.add(l.id);\n if (l.kind === 'oauth_login') {\n oauthLoginLayerCount += 1;\n }\n if (l.kind === 'email_password_auth') {\n emailPasswordAuthLayerCount += 1;\n }\n if (l.kind === 'button' && l.action.kind === 'continue') {\n hasContinueButton = true;\n }\n if (isInputLayer(l)) {\n inputCount += 1;\n if (l.kind === 'multiple_choice' || l.kind === 'text_input' || l.kind === 'scale_input') {\n needsManualSubmit = true;\n }\n const key = l.fieldKey;\n const label = screen.name || screen.id;\n if (!key || key.length === 0) {\n issues.push(`Screen \"${label}\" is missing a variable name (fieldKey).`);\n } else if (!FIELD_KEY_RE.test(key)) {\n issues.push(\n `Screen \"${label}\" has an invalid variable name \"${key}\" — use snake_case (a–z, 0–9, _).`,\n );\n } else {\n const owners = fieldKeyOwners.get(key) ?? [];\n owners.push(label);\n fieldKeyOwners.set(key, owners);\n }\n\n if (l.kind === 'single_choice' || l.kind === 'multiple_choice') {\n for (const cond of l.branching.conditions) {\n if (!screenIds.has(cond.goTo)) {\n issues.push(\n `Screen \"${label}\" branches choice \"${cond.choiceId}\" to a missing screen \"${cond.goTo}\".`,\n );\n }\n }\n }\n }\n if (l.kind === 'button' && l.action.kind === 'go_to_step') {\n if (!screenIds.has(l.action.screenId)) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screen.name || screen.id}\" targets a missing screen \"${l.action.screenId}\".`,\n );\n }\n }\n if (l.kind === 'button' && l.action.kind === 'request_app_review') {\n const def = screen.next?.default;\n if (def == null) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screen.name || screen.id}\" requests app review but the screen has no default next step.`,\n );\n } else if (!screenIds.has(def) && !manifest.decisionNodes?.some((d) => d.id === def)) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screen.name || screen.id}\" requests app review but default next \"${def}\" is missing.`,\n );\n }\n }\n if (l.kind === 'button' && l.action.kind === 'request_os_permission') {\n const o = l.action.outcomes;\n for (const slot of ['granted', 'denied', 'blocked'] as const) {\n const sid = o[slot];\n if (sid === OS_PERMISSION_OUTCOME_END) {\n continue;\n }\n if (sid === OS_PERMISSION_OUTCOME_CONTINUE) {\n const def = screen.next?.default;\n if (def == null) {\n continue;\n }\n if (!screenIds.has(def) && !manifest.decisionNodes?.some((d) => d.id === def)) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screen.name || screen.id}\" (${slot}) continues to missing target \"${def}\".`,\n );\n }\n continue;\n }\n if (!screenIds.has(sid)) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screen.name || screen.id}\" (${slot}) targets a missing screen \"${sid}\".`,\n );\n }\n }\n }\n if (l.kind === 'back_button' && l.fallbackScreenId && !screenIds.has(l.fallbackScreenId)) {\n issues.push(\n `Back button \"${l.name || l.id}\" on screen \"${screen.name || screen.id}\" uses a missing fallback screen \"${l.fallbackScreenId}\".`,\n );\n }\n if (l.kind === 'button' && l.action.kind === 'go_back_one_screen' && l.action.fallbackScreenId) {\n if (!screenIds.has(l.action.fallbackScreenId)) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screen.name || screen.id}\" uses a missing fallback screen \"${l.action.fallbackScreenId}\".`,\n );\n }\n }\n if (l.kind === 'lottie' || l.kind === 'video') {\n const media = l as LottieLayer | VideoLayer;\n if (media.autoPlay === false) {\n const triggerId = media.triggerLayerId?.trim();\n if (!triggerId) {\n issues.push(\n `${media.kind === 'video' ? 'Video' : 'Lottie'} \"${media.name || media.id}\" on screen \"${screenLabel}\" needs a trigger button when auto-play is off.`,\n );\n } else if (!buttonLayerIds.has(triggerId)) {\n issues.push(\n `${media.kind === 'video' ? 'Video' : 'Lottie'} \"${media.name || media.id}\" on screen \"${screenLabel}\" references a missing trigger button \"${triggerId}\".`,\n );\n } else {\n const btn = findLayerById(screen, triggerId) as ButtonLayer | null;\n if (!btn || btn.kind !== 'button') {\n issues.push(\n `${media.kind === 'video' ? 'Video' : 'Lottie'} \"${media.name || media.id}\" on screen \"${screenLabel}\" trigger must be a button layer.`,\n );\n } else if (btn.action.kind !== 'play_media') {\n issues.push(\n `${media.kind === 'video' ? 'Video' : 'Lottie'} \"${media.name || media.id}\" on screen \"${screenLabel}\" trigger button must use On Tap → Play media (or pick the trigger again from this screen).`,\n );\n } else if (!btn.action.targetLayerIds.includes(media.id)) {\n issues.push(\n `${media.kind === 'video' ? 'Video' : 'Lottie'} \"${media.name || media.id}\" on screen \"${screenLabel}\" is not listed on trigger button \"${triggerId}\".`,\n );\n }\n }\n } else if (media.triggerLayerId) {\n const btn = findLayerById(screen, media.triggerLayerId);\n if (btn?.kind === 'button' && btn.action.kind === 'play_media' && !btn.action.targetLayerIds.includes(media.id)) {\n issues.push(\n `${media.kind === 'video' ? 'Video' : 'Lottie'} \"${media.name || media.id}\" on screen \"${screenLabel}\" trigger button does not target this layer.`,\n );\n }\n }\n }\n if (l.kind === 'button' && l.action.kind === 'play_media') {\n for (const targetId of l.action.targetLayerIds) {\n if (targetId === shellPlaybackId) {\n if (!shellVideoFill) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screenLabel}\" targets screen background video, but this screen has no video background.`,\n );\n }\n continue;\n }\n if (isScreenBackgroundPlaybackId(targetId)) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screenLabel}\" play-media target \"${targetId}\" is not valid for this screen.`,\n );\n continue;\n }\n if (!mediaLayerIds.has(targetId)) {\n issues.push(\n `Button \"${l.name || l.id}\" on screen \"${screenLabel}\" play-media target \"${targetId}\" must be a Lottie or video layer on this screen, or screen background video.`,\n );\n }\n }\n }\n });\n\n if (oauthLoginLayerCount > 0 && inputCount > 0) {\n issues.push(\n `Screen \"${screen.name || screen.id}\" cannot combine OAuth Login with input layers (${inputCount}). Split them onto separate screens.`,\n );\n }\n\n if (emailPasswordAuthLayerCount > 0 && inputCount > 0) {\n issues.push(\n `Screen \"${screen.name || screen.id}\" cannot combine Email / password login with input layers (${inputCount}). Split them onto separate screens.`,\n );\n }\n\n if (oauthLoginLayerCount > 0 && emailPasswordAuthLayerCount > 0) {\n issues.push(\n `Screen \"${screen.name || screen.id}\" cannot combine OAuth Login with Email / password login. Use one login block per screen.`,\n );\n }\n\n if (emailPasswordAuthLayerCount > 1) {\n issues.push(\n `Screen \"${screen.name || screen.id}\" has ${emailPasswordAuthLayerCount} Email / password login layers; only one is allowed per screen.`,\n );\n }\n\n if (oauthLoginLayerCount > 1) {\n issues.push(\n `Screen \"${screen.name || screen.id}\" has ${oauthLoginLayerCount} OAuth Login layers; only one is allowed per screen.`,\n );\n }\n\n if (inputCount > 1) {\n issues.push(\n `Screen \"${screen.name || screen.id}\" has ${inputCount} input layers; only one is allowed.`,\n );\n }\n\n if (needsManualSubmit && !hasContinueButton) {\n issues.push(\n `Screen \"${screen.name || screen.id}\" has a multiple_choice, text_input, or scale_input but no Button with action \"continue\". Add a Continue button so users can submit.`,\n );\n }\n }\n\n for (const [key, owners] of fieldKeyOwners) {\n if (owners.length > 1) {\n issues.push(`Variable name \"${key}\" is used by multiple screens: ${owners.join(', ')}.`);\n }\n }\n\n for (const screen of manifest.screens as unknown as Screen[]) {\n walkScreen(screen, (l) => {\n const screenLabel = screen.name || screen.id;\n if (l.kind === 'text' && !textLayerHasAuthoringColor(l)) {\n issues.push(\n `Screen \"${screenLabel}\": text layer \"${l.id}\" must set style.color for light and dark (CSS inheritance does not apply on native).`,\n );\n }\n if (l.kind === 'icon' && !iconLayerHasAuthoringColor(l)) {\n issues.push(\n `Screen \"${screenLabel}\": icon layer \"${l.id}\" must set style.color for light and dark.`,\n );\n }\n });\n }\n\n return issues;\n};\n"]}
@@ -0,0 +1,19 @@
1
+ import { FlowManifest } from '@getrheo/contracts/manifest';
2
+
3
+ /**
4
+ * Screens that may have already been completed before `screenId` is shown
5
+ * (strict predecessors on some path from entry), intersected with reachability from entry.
6
+ */
7
+ declare const upstreamScreenIdsForPicker: (manifest: FlowManifest, screenId: string) => string[];
8
+ /** Dominator sets for nodes reachable from entry (each set includes the node itself). */
9
+ declare const computeDominators: (manifest: FlowManifest) => Map<string, Set<string>>;
10
+ /**
11
+ * Non-blocking warnings for Text layer `{{ … }}` usage (dominance, same-screen, invalid tokens).
12
+ */
13
+ declare const collectInterpolationWarnings: (manifest: FlowManifest) => string[];
14
+ /** Dominance warnings for `fieldKey` references inside decision expressions. */
15
+ declare const collectDecisionWarnings: (manifest: FlowManifest) => string[];
16
+ /** `fieldKey` values available in the variable picker for `screenId`. */
17
+ declare const upstreamFieldKeysForPicker: (manifest: FlowManifest, screenId: string) => string[];
18
+
19
+ export { collectDecisionWarnings, collectInterpolationWarnings, computeDominators, upstreamFieldKeysForPicker, upstreamScreenIdsForPicker };